Files
NetMesh/domain/convergentSync/conflicts.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

81 lines
3.2 KiB
TypeScript

import { dotKey } from './clock';
import { applyConvergentMutations } from './state';
import type {
ConvergentConflictAddress,
ConvergentConflictCandidate,
ConvergentFieldConflict,
ConvergentSyncStateV2,
JsonValue,
RegisterAddress,
} from './types';
export function convergentConflictAddressKey(address: ConvergentConflictAddress): string {
switch (address.kind) {
case 'entity-presence':
case 'entity-position':
return JSON.stringify([address.kind, address.collection, address.entityId]);
case 'entity-field':
return JSON.stringify([address.kind, address.collection, address.entityId, address.field]);
case 'setting':
return JSON.stringify([address.kind, ...address.path]);
case 'setting-structure':
return JSON.stringify([address.kind, ...address.paths]);
case 'string-entry-presence':
case 'string-entry-position':
return JSON.stringify([address.kind, address.collection, address.value]);
}
}
function selectedAddress(
conflict: ConvergentFieldConflict,
candidate: ConvergentConflictCandidate,
): RegisterAddress {
if (conflict.address.kind !== 'setting-structure') return conflict.address;
if (!candidate.settingPath?.length) {
throw new Error('A setting-structure candidate must identify its setting path');
}
return { kind: 'setting', path: candidate.settingPath };
}
export function resolveConvergentFieldConflict(
state: ConvergentSyncStateV2,
conflict: ConvergentFieldConflict,
candidateDot: string,
deviceId: string,
now: number,
): ConvergentSyncStateV2 {
const candidate = conflict.candidates.find((entry) => dotKey(entry.dot) === candidateDot);
if (!candidate) throw new Error('The selected convergent conflict candidate no longer exists');
if (!candidate.tombstone && candidate.value === undefined) {
throw new Error('The selected convergent conflict candidate has no value');
}
return applyConvergentMutations(state, deviceId, [{
kind: 'resolve-register',
address: selectedAddress(conflict, candidate),
...(candidate.tombstone ? { tombstone: true } : { value: candidate.value }),
}], now);
}
const SECRET_SEGMENT = /(?:password|passphrase|privatekey|secret|token|api[_-]?key|access[_-]?key)/i;
function valueContainsSecretField(value: JsonValue | undefined): boolean {
if (!value || typeof value !== 'object') return false;
if (Array.isArray(value)) return value.some((nested) => valueContainsSecretField(nested));
return Object.entries(value).some(([key, nested]) =>
SECRET_SEGMENT.test(key) || valueContainsSecretField(nested));
}
/** Values from secret-bearing registers must never be rendered or logged. */
export function isConvergentConflictSecret(conflict: ConvergentFieldConflict): boolean {
const { address } = conflict;
if (address.kind === 'entity-field' && SECRET_SEGMENT.test(address.field)) return true;
if (address.kind === 'setting' && address.path.some((segment) => SECRET_SEGMENT.test(segment))) {
return true;
}
if (
address.kind === 'setting-structure'
&& address.paths.some((path) => path.some((segment) => SECRET_SEGMENT.test(segment)))
) return true;
return conflict.candidates.some((candidate) => valueContainsSecretField(candidate.value));
}