Files
NetMesh/infrastructure/services/cloudSync/convergentSyncStorageMethods.ts
zhaolei 3c72efcb7f
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
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

197 lines
7.5 KiB
TypeScript

/* 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;
}
}