[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import {
|
||||
SYNC_STORAGE_KEYS,
|
||||
type CloudProvider,
|
||||
type ConvergentProviderBaselineV2,
|
||||
type ConvergentReplicaRecordV2,
|
||||
type MasterKeyConfig,
|
||||
} from '../../../domain/sync';
|
||||
import {
|
||||
assertValidConvergentSyncState,
|
||||
canonicalizeConvergentSyncState,
|
||||
stripConvergentSyncEnvelope,
|
||||
} from '../../../domain/convergentSync';
|
||||
import {
|
||||
decryptLocalStorageValue,
|
||||
encryptLocalStorageValue,
|
||||
} from './encryptedLocalStorage';
|
||||
|
||||
/** Built-ins plus any dynamic plugin providers currently in manager state. */
|
||||
function listedProviders(manager: any): CloudProvider[] {
|
||||
const fromState = Object.keys(manager?.state?.providers ?? {}) as CloudProvider[];
|
||||
if (fromState.length > 0) return fromState.sort();
|
||||
return ['github', 'google', 'onedrive', 'webdav', 's3'];
|
||||
}
|
||||
|
||||
export function convergentProviderBaselineKeyImpl(this: any, provider: CloudProvider): string {
|
||||
return `${SYNC_STORAGE_KEYS.CONVERGENT_PROVIDER_BASELINE}_${provider}`;
|
||||
}
|
||||
|
||||
function requireLocalEncryptionKey(manager: any): CryptoKey {
|
||||
const key = manager.state.unlockedKey?.derivedKey;
|
||||
if (!key) throw new Error('Convergent sync encryption key is unavailable');
|
||||
return key;
|
||||
}
|
||||
|
||||
export async function saveConvergentReplicaImpl(
|
||||
this: any,
|
||||
record: ConvergentReplicaRecordV2,
|
||||
): Promise<void> {
|
||||
if (record.schemaVersion !== 2) throw new Error('Unsupported convergent replica schema');
|
||||
assertValidConvergentSyncState(record.state);
|
||||
const normalized: ConvergentReplicaRecordV2 = {
|
||||
schemaVersion: 2,
|
||||
state: canonicalizeConvergentSyncState(record.state),
|
||||
updatedAt: record.updatedAt,
|
||||
};
|
||||
if (this.saveToStorage(
|
||||
SYNC_STORAGE_KEYS.CONVERGENT_REPLICA,
|
||||
await encryptLocalStorageValue(normalized, requireLocalEncryptionKey(this)),
|
||||
) === false) throw new Error('Unable to persist convergent sync replica');
|
||||
}
|
||||
|
||||
export async function loadConvergentReplicaImpl(this: any): Promise<ConvergentReplicaRecordV2 | null> {
|
||||
const encoded = this.loadFromStorage(SYNC_STORAGE_KEYS.CONVERGENT_REPLICA) as unknown;
|
||||
if (!encoded) return null;
|
||||
if (typeof encoded !== 'string') throw new Error('Convergent replica record is invalid');
|
||||
const record = await decryptLocalStorageValue<ConvergentReplicaRecordV2>(
|
||||
encoded,
|
||||
requireLocalEncryptionKey(this),
|
||||
);
|
||||
if (record?.schemaVersion !== 2 || !Number.isFinite(record.updatedAt)) {
|
||||
throw new Error('Convergent replica record has an unsupported schema');
|
||||
}
|
||||
assertValidConvergentSyncState(record.state);
|
||||
return {
|
||||
schemaVersion: 2,
|
||||
state: canonicalizeConvergentSyncState(record.state),
|
||||
updatedAt: record.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export async function saveConvergentProviderBaselineImpl(
|
||||
this: any,
|
||||
baseline: ConvergentProviderBaselineV2,
|
||||
): Promise<void> {
|
||||
if (baseline.schemaVersion !== 2) throw new Error('Unsupported convergent baseline schema');
|
||||
assertValidConvergentSyncState(baseline.state);
|
||||
const normalized: ConvergentProviderBaselineV2 = {
|
||||
...baseline,
|
||||
materializedPayload: stripConvergentSyncEnvelope(baseline.materializedPayload),
|
||||
state: canonicalizeConvergentSyncState(baseline.state),
|
||||
};
|
||||
if (this.saveToStorage(
|
||||
this.convergentProviderBaselineKey(baseline.provider),
|
||||
await encryptLocalStorageValue(normalized, requireLocalEncryptionKey(this)),
|
||||
) === false) throw new Error(`Unable to persist convergent baseline for ${baseline.provider}`);
|
||||
}
|
||||
|
||||
export async function loadConvergentProviderBaselineImpl(
|
||||
this: any,
|
||||
provider: CloudProvider,
|
||||
): Promise<ConvergentProviderBaselineV2 | null> {
|
||||
const encoded = this.loadFromStorage(this.convergentProviderBaselineKey(provider)) as unknown;
|
||||
if (!encoded) return null;
|
||||
if (typeof encoded !== 'string') throw new Error(`Convergent baseline for ${provider} is invalid`);
|
||||
const baseline = await decryptLocalStorageValue<ConvergentProviderBaselineV2>(
|
||||
encoded,
|
||||
requireLocalEncryptionKey(this),
|
||||
);
|
||||
if (baseline?.schemaVersion !== 2 || baseline.provider !== provider) {
|
||||
throw new Error(`Convergent baseline for ${provider} has an unsupported schema`);
|
||||
}
|
||||
assertValidConvergentSyncState(baseline.state);
|
||||
return {
|
||||
...baseline,
|
||||
materializedPayload: stripConvergentSyncEnvelope(baseline.materializedPayload),
|
||||
state: canonicalizeConvergentSyncState(baseline.state),
|
||||
};
|
||||
}
|
||||
|
||||
export function clearConvergentSyncStorageImpl(this: any, confirmed: boolean): void {
|
||||
if (!confirmed) throw new Error('Explicit confirmation is required to remove convergent sync state');
|
||||
this.removeFromStorage(SYNC_STORAGE_KEYS.CONVERGENT_REPLICA);
|
||||
for (const provider of listedProviders(this)) {
|
||||
this.removeFromStorage(this.convergentProviderBaselineKey(provider));
|
||||
}
|
||||
}
|
||||
|
||||
function encryptedSyncStorageKeys(manager: any): string[] {
|
||||
const keys = new Set<string>([
|
||||
manager.syncBaseKey(),
|
||||
manager.syncSnapshotsKey(),
|
||||
SYNC_STORAGE_KEYS.CONVERGENT_REPLICA,
|
||||
]);
|
||||
for (const provider of listedProviders(manager)) {
|
||||
keys.add(manager.syncBaseKey(provider));
|
||||
keys.add(manager.syncSnapshotsKey(provider));
|
||||
keys.add(manager.convergentProviderBaselineKey(provider));
|
||||
}
|
||||
return [...keys];
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-encrypt every derived-key local record before committing the new master
|
||||
* configuration. Values are prepared first, concurrent changes abort the
|
||||
* transaction, and any write failure rolls all keys back to their exact prior
|
||||
* ciphertext.
|
||||
*/
|
||||
export async function reencryptSyncStorageImpl(
|
||||
this: any,
|
||||
oldKey: CryptoKey,
|
||||
newKey: CryptoKey,
|
||||
newConfig: MasterKeyConfig,
|
||||
): Promise<void> {
|
||||
const keys = encryptedSyncStorageKeys(this);
|
||||
const previousConfig = (
|
||||
this.loadFromStorage(SYNC_STORAGE_KEYS.MASTER_KEY_CONFIG)
|
||||
?? this.state.masterKeyConfig
|
||||
) as MasterKeyConfig | null;
|
||||
const originals = new Map<string, string | null>();
|
||||
const replacements = new Map<string, string>();
|
||||
for (const key of keys) {
|
||||
const encoded = this.loadFromStorage(key) as unknown;
|
||||
if (encoded == null) {
|
||||
originals.set(key, null);
|
||||
continue;
|
||||
}
|
||||
if (typeof encoded !== 'string') throw new Error(`Encrypted sync record ${key} is invalid`);
|
||||
originals.set(key, encoded);
|
||||
const value = await decryptLocalStorageValue<unknown>(encoded, oldKey);
|
||||
replacements.set(key, await encryptLocalStorageValue(value, newKey));
|
||||
}
|
||||
for (const [key, original] of originals) {
|
||||
const current = this.loadFromStorage(key) as unknown;
|
||||
if ((current ?? null) !== original) {
|
||||
throw new Error('Sync data changed while the master key was being rotated');
|
||||
}
|
||||
}
|
||||
const currentConfig = this.loadFromStorage(
|
||||
SYNC_STORAGE_KEYS.MASTER_KEY_CONFIG,
|
||||
) as MasterKeyConfig | null;
|
||||
if (JSON.stringify(currentConfig) !== JSON.stringify(previousConfig)) {
|
||||
throw new Error('Master key configuration changed while it was being rotated');
|
||||
}
|
||||
const committed: string[] = [];
|
||||
try {
|
||||
for (const [key, replacement] of replacements) {
|
||||
if (this.saveToStorage(key, replacement) === false) {
|
||||
throw new Error(`Unable to persist re-encrypted sync record: ${key}`);
|
||||
}
|
||||
committed.push(key);
|
||||
}
|
||||
if (this.saveToStorage(SYNC_STORAGE_KEYS.MASTER_KEY_CONFIG, newConfig) === false) {
|
||||
throw new Error('Unable to persist the new master key configuration');
|
||||
}
|
||||
} catch (error) {
|
||||
for (const key of committed.reverse()) {
|
||||
const original = originals.get(key);
|
||||
if (original !== undefined) this.saveToStorage(key, original);
|
||||
}
|
||||
if (previousConfig) this.saveToStorage(SYNC_STORAGE_KEYS.MASTER_KEY_CONFIG, previousConfig);
|
||||
else this.removeFromStorage(SYNC_STORAGE_KEYS.MASTER_KEY_CONFIG);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user