sneakoscope 8.1.2 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -58
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/bin/fast-inline.js +5 -51
- package/dist/cli/command-manifest-lite.js +2 -1
- package/dist/cli/command-registry.js +2 -1
- package/dist/cli/global-mode-router.js +1 -1
- package/dist/cli/install-helpers-codex-lb-config.js +112 -914
- package/dist/cli/install-helpers-codex-lb-shared.js +3 -44
- package/dist/cli/install-helpers-install-support.js +4 -20
- package/dist/cli/install-helpers.js +36 -2716
- package/dist/cli/router.js +9 -9
- package/dist/commands/bridge-command-output.js +111 -0
- package/dist/commands/bridge.js +320 -0
- package/dist/commands/codex-app.js +39 -123
- package/dist/commands/doctor-helpers.js +380 -0
- package/dist/commands/doctor-profile.js +3 -0
- package/dist/commands/doctor.js +114 -873
- package/dist/commands/mad-sks.js +0 -4
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/agent-effort-policy.js +11 -21
- package/dist/core/agents/agent-recursion-guard.js +26 -0
- package/dist/core/agents/agent-runner-codex-exec.js +7 -9
- package/dist/core/architecture-hardening/contracts/contracts.js +225 -161
- package/dist/core/architecture-hardening/state/state-service.js +49 -13
- package/dist/core/auto-review.js +15 -9
- package/dist/core/codex/codex-config-guard.js +8 -0
- package/dist/core/codex-adapter.js +8 -10
- package/dist/core/codex-app/codex-app-fast-ui-repair.js +7 -29
- package/dist/core/codex-app/codex-app-launcher.js +10 -12
- package/dist/core/codex-app/codex-app-restart.js +10 -12
- package/dist/core/codex-app/menubar/constants.js +2 -5
- package/dist/core/codex-app/menubar/global-install.js +3 -2
- package/dist/core/codex-app/menubar/installer/result.js +23 -0
- package/dist/core/codex-app/menubar/installer/runtime.js +28 -0
- package/dist/core/codex-app/menubar/installer.js +5 -96
- package/dist/core/codex-app/menubar/migration.js +1 -42
- package/dist/core/codex-app/openrouter-provider.js +0 -78
- package/dist/core/codex-app.js +97 -360
- package/dist/core/codex-control/codex-app-server-runtime-env.js +8 -28
- package/dist/core/codex-control/codex-current-image-path-real-probe.js +2 -3
- package/dist/core/codex-control/codex-current-web-search-probe.js +2 -3
- package/dist/core/codex-control/codex-sdk-adapter.js +5 -48
- package/dist/core/codex-control/codex-task-runner.js +16 -60
- package/dist/core/codex-control/desktop-bridge-launch-guard.js +227 -0
- package/dist/core/codex-exec-output-schema.js +11 -13
- package/dist/core/codex-lb/bridge-contracts.js +2 -0
- package/dist/core/codex-lb/bridge-runtime-validation/capability.js +104 -0
- package/dist/core/codex-lb/bridge-runtime-validation/catalog.js +52 -0
- package/dist/core/codex-lb/bridge-runtime-validation/shared.js +109 -0
- package/dist/core/codex-lb/bridge-runtime-validation/status.js +250 -0
- package/dist/core/codex-lb/bridge-runtime-validation.js +106 -0
- package/dist/core/codex-lb/capability-runner.js +505 -201
- package/dist/core/codex-lb/codex-lb-env.js +179 -12
- package/dist/core/codex-lb/combined-catalog/builder.js +86 -0
- package/dist/core/codex-lb/combined-catalog/contracts.js +5 -0
- package/dist/core/codex-lb/combined-catalog/normalize.js +170 -0
- package/dist/core/codex-lb/combined-catalog/persistence.js +207 -0
- package/dist/core/codex-lb/combined-catalog/shared.js +12 -0
- package/dist/core/codex-lb/combined-catalog/storage.js +171 -0
- package/dist/core/codex-lb/combined-catalog.js +12 -0
- package/dist/core/codex-lb/desktop-bridge/client.js +4 -1
- package/dist/core/codex-lb/desktop-bridge/header-policy.js +48 -63
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +115 -83
- package/dist/core/codex-lb/desktop-bridge/launchd.js +20 -13
- package/dist/core/codex-lb/desktop-bridge/location-rewrite.js +4 -1
- package/dist/core/codex-lb/desktop-bridge/security.js +381 -267
- package/dist/core/codex-lb/desktop-bridge/server.js +148 -19
- package/dist/core/codex-lb/desktop-bridge/state.js +56 -70
- package/dist/core/codex-lb/desktop-bridge/types.js +6 -1
- package/dist/core/codex-lb/desktop-bridge/websocket-forward.js +281 -106
- package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +245 -0
- package/dist/core/codex-lb/desktop-bridge-migration/historical-toml.js +56 -0
- package/dist/core/codex-lb/desktop-bridge-migration/metadata.js +73 -0
- package/dist/core/codex-lb/desktop-bridge-migration/receipt.js +52 -0
- package/dist/core/codex-lb/desktop-bridge-migration/retired-runtime-cleanup.js +106 -0
- package/dist/core/codex-lb/desktop-bridge-migration/types.js +1 -0
- package/dist/core/codex-lb/desktop-bridge-migration.js +238 -0
- package/dist/core/codex-lb/desktop-controller-v3/catalog.js +257 -0
- package/dist/core/codex-lb/desktop-controller-v3/diagnostics.js +62 -0
- package/dist/core/codex-lb/desktop-controller-v3/lifecycle-commands.js +203 -0
- package/dist/core/codex-lb/desktop-controller-v3/live-probes.js +293 -0
- package/dist/core/codex-lb/desktop-controller-v3/preflight-probes.js +144 -0
- package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +182 -0
- package/dist/core/codex-lb/desktop-controller-v3/shared.js +279 -0
- package/dist/core/codex-lb/desktop-controller-v3/status.js +269 -0
- package/dist/core/codex-lb/desktop-controller-v3/types.js +1 -0
- package/dist/core/codex-lb/desktop-controller-v3/verification.js +89 -0
- package/dist/core/codex-lb/desktop-controller-v3.js +67 -0
- package/dist/core/codex-lb/desktop-controller.js +2 -1730
- package/dist/core/codex-lb/desktop-service.js +462 -627
- package/dist/core/codex-lb/migration-receipt.js +560 -118
- package/dist/core/codex-lb/probes/auxiliary-surfaces-probe.js +37 -53
- package/dist/core/codex-lb/probes/bridge-probe.js +125 -64
- package/dist/core/codex-lb/probes/catalog-probe.js +36 -136
- package/dist/core/codex-lb/probes/computer-use-probe.js +63 -39
- package/dist/core/codex-lb/probes/image-generation-probe.js +98 -41
- package/dist/core/codex-lb/probes/probe-evidence.js +32 -33
- package/dist/core/codex-lb/probes/voice-realtime-probe.js +69 -43
- package/dist/core/codex-lb/provider-credentials/configuration.js +127 -0
- package/dist/core/codex-lb/provider-credentials/locks.js +11 -0
- package/dist/core/codex-lb/provider-credentials/private-snapshot.js +28 -0
- package/dist/core/codex-lb/provider-credentials/removal.js +33 -0
- package/dist/core/codex-lb/provider-credentials/resolution.js +59 -0
- package/dist/core/codex-lb/provider-credentials/runtime.js +55 -0
- package/dist/core/codex-lb/provider-credentials/types.js +1 -0
- package/dist/core/codex-lb/provider-credentials/validation-store.js +137 -0
- package/dist/core/codex-lb/provider-credentials.js +5 -0
- package/dist/core/codex-lb/provider-registry.js +323 -0
- package/dist/core/codex-lb/provider-route-policy.js +118 -0
- package/dist/core/codex-lb/request-route-resolver.js +172 -0
- package/dist/core/codex-lb/route-index.js +148 -0
- package/dist/core/codex-lb/trusted-deep-evidence.js +158 -286
- package/dist/core/commands/basic-cli.js +1 -1
- package/dist/core/commands/check-command.js +3 -10
- package/dist/core/commands/gate-result-contract.js +10 -14
- package/dist/core/commands/intent-normalization/normalizer.js +1 -1
- package/dist/core/commands/mad-sks-command.js +2 -6
- package/dist/core/commands/release-command.js +11 -1
- package/dist/core/commands/telegram-command.js +46 -6
- package/dist/core/doctor/doctor-readiness-matrix.js +9 -7
- package/dist/core/doctor/skill-legacy-surface.js +5 -5
- package/dist/core/evidence/v2/evidence-key.js +30 -3
- package/dist/core/feature-fixtures.js +8 -1
- package/dist/core/feature-registry.js +3 -2
- package/dist/core/hooks-runtime/stop-finalization.js +11 -1
- package/dist/core/hooks-runtime/subagent-context.js +15 -1
- package/dist/core/hooks-runtime/subagent-skill-availability-contract.js +1 -1
- package/dist/core/hooks-runtime/tool-output-quarantine.js +1 -1
- package/dist/core/hooks-runtime.js +73 -87
- package/dist/core/image-ux-review/imagegen-adapter.js +108 -94
- package/dist/core/image-ux-review/real-callout-extractor.js +144 -14
- package/dist/core/imagegen/desktop-bridge-imagegen-target.js +77 -0
- package/dist/core/imagegen/imagegen-capability.js +43 -151
- package/dist/core/init/skills/filesystem.js +89 -0
- package/dist/core/init/skills/inventory.js +97 -0
- package/dist/core/init/skills/manifest.js +151 -0
- package/dist/core/init/skills.js +5 -325
- package/dist/core/init.js +0 -63
- package/dist/core/install/installed-package-smoke.js +1 -1
- package/dist/core/managed-assets/managed-assets-manifest.js +0 -1
- package/dist/core/policy/latest-version-guidance.js +15 -2
- package/dist/core/preflight/parallel-preflight-engine.js +27 -51
- package/dist/core/provider/provider-badge.js +9 -4
- package/dist/core/provider/provider-context.js +57 -107
- package/dist/core/providers/openrouter/openrouter-account.js +3 -1
- package/dist/core/providers/openrouter/openrouter-secret-store.js +91 -22
- package/dist/core/release/desktop-bridge-release-evidence.js +457 -0
- package/dist/core/release/gate-manifest.js +0 -1
- package/dist/core/release/main-push-guard.js +13 -0
- package/dist/core/release/main-push-receipt.js +11 -0
- package/dist/core/release/physical-release-gates.js +664 -61
- package/dist/core/release/publish-preflight.js +3 -3
- package/dist/core/release/release-gate-batch-runner.js +19 -6
- package/dist/core/release/release-gate-dag.js +33 -6
- package/dist/core/release/release-gate-node.js +1 -3
- package/dist/core/release/release-gate-output-contract.js +55 -0
- package/dist/core/release/release-pack-contract.js +1 -1
- package/dist/core/release/release-pack-receipt.js +209 -8
- package/dist/core/release/release-real-contract.js +1 -1
- package/dist/core/release/stage-publish.js +106 -28
- package/dist/core/retention/temp-lease.js +204 -0
- package/dist/core/retention.js +18 -195
- package/dist/core/routes.js +1 -2
- package/dist/core/safety/intent-contract/intent-contract.js +3 -3
- package/dist/core/subagents/naruto-host-credentials.js +6 -1
- package/dist/core/subagents/official-subagent-config.js +27 -9
- package/dist/core/subagents/official-subagent-runner.js +14 -31
- package/dist/core/subagents/wave-lifecycle.js +36 -8
- package/dist/core/subagents/wave-parent-guidance.js +2 -1
- package/dist/core/telegram/doctor.js +2 -2
- package/dist/core/telegram/liveness.js +10 -4
- package/dist/core/triwiki/context-graph/compiler/index.js +18 -18
- package/dist/core/update/update-migration-state/fast-mode-config.js +172 -0
- package/dist/core/update/update-migration-state/session-state-split.js +247 -0
- package/dist/core/update/update-migration-state/simple-stages.js +46 -0
- package/dist/core/update/update-migration-state.js +4 -481
- package/dist/core/version.js +1 -1
- package/dist/native/sks-menubar/Sources/OperationCoordinator.swift +28 -4
- package/dist/native/sks-menubar/Sources/OperationModels.swift +112 -2
- package/dist/native/sks-menubar/Sources/ProviderRouteExplanation.swift +21 -0
- package/dist/native/sks-menubar/Sources/ProvidersBridgeCatalog.swift +140 -0
- package/dist/native/sks-menubar/Sources/ProvidersOpenRouter.swift +107 -470
- package/dist/native/sks-menubar/Sources/ProvidersReliability.swift +27 -197
- package/dist/native/sks-menubar/Sources/ProvidersRoutingTruth.swift +335 -269
- package/dist/native/sks-menubar/Sources/ProvidersViewController.swift +233 -437
- package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +16 -7
- package/dist/native/sks-menubar/Sources/SecureProcessEnvelope.swift +39 -9
- package/dist/native/sks-menubar/Sources/TelegramSupport.swift +56 -0
- package/dist/scripts/canonical-test-runner.js +7 -1
- package/dist/scripts/check-runtime-schemas.js +7 -2
- package/dist/scripts/codex-sdk-real-smoke-check.js +3 -1
- package/dist/scripts/desktop-bridge-capabilities-check.js +123 -0
- package/dist/scripts/{codex-lb-desktop-bridge-latency-check.js → desktop-bridge-latency-check.js} +61 -16
- package/dist/scripts/desktop-bridge-real-evidence-check.js +64 -0
- package/dist/scripts/{codex-lb-desktop-bridge-check.js → desktop-bridge-transport-check.js} +92 -155
- package/dist/scripts/desktop-bridge-transport-client.js +116 -0
- package/dist/scripts/desktop-bridge-unification-check.js +276 -0
- package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +12 -8
- package/dist/scripts/gate-policy-audit-check.js +52 -2
- package/dist/scripts/provider-badge-context-check.js +55 -12
- package/dist/scripts/publish-preflight.js +1 -2
- package/dist/scripts/release-affected-selector-check.js +1 -2
- package/dist/scripts/release-aggressive-resource-governor-check.js +1 -1
- package/dist/scripts/release-cache-glob-hashing-check.js +1 -0
- package/dist/scripts/release-cache-version-neutral-fixture-check.js +1 -0
- package/dist/scripts/release-full-parallelism-blackbox.js +1 -1
- package/dist/scripts/release-gate-batch-runner-check.js +9 -0
- package/dist/scripts/release-gate-dag-runner-check.js +6 -0
- package/dist/scripts/release-gate-dag-runner.js +4 -2
- package/dist/scripts/release-metadata-check.js +16 -3
- package/dist/scripts/release-pack-receipt.js +29 -6
- package/dist/scripts/release-physical-gates-check.js +39 -0
- package/dist/scripts/release-real-check.js +7 -5
- package/package.json +6 -8
- package/release-gates.v2.json +156 -157
- package/runtime-required-scripts.json +11 -11
- package/schemas/codex/desktop-bridge-status-v3.schema.json +286 -0
- package/schemas/codex/desktop-capabilities-v3.schema.json +198 -0
- package/schemas/release/release-gate-node.schema.json +2 -3
- package/dist/cli/codex-lb-setup-warning-output.js +0 -17
- package/dist/cli/install-helpers-codex-lb-chain.js +0 -529
- package/dist/commands/codex-lb.js +0 -1232
- package/dist/core/architecture-hardening/migration/migration.js +0 -111
- package/dist/core/codex-app/auxiliary-oauth-policy.js +0 -36
- package/dist/core/codex-app/catalog-compat/catalog-service.js +0 -104
- package/dist/core/codex-app/child-policy/child-policy.js +0 -35
- package/dist/core/codex-app/desktop-routing-snapshot.js +0 -233
- package/dist/core/codex-app/glm-model-profile.js +0 -1
- package/dist/core/codex-app/glm-profile-installer.js +0 -191
- package/dist/core/codex-app/glm-profile-schema.js +0 -42
- package/dist/core/codex-app/multi-provider-router.js +0 -391
- package/dist/core/codex-app/openrouter-activate.js +0 -405
- package/dist/core/codex-app/openrouter-model-catalog.js +0 -217
- package/dist/core/codex-app/provider-child-policy.js +0 -64
- package/dist/core/codex-app/provider-mode.js +0 -91
- package/dist/core/codex-app/provider-session-policy.js +0 -85
- package/dist/core/codex-app/session-policy/session-pinning.js +0 -68
- package/dist/core/codex-control/codex-lb-launch-recovery.js +0 -319
- package/dist/core/codex-lb/codex-lb-setup.js +0 -293
- package/dist/core/codex-lb/codex-lb-tool-catalog.js +0 -456
- package/dist/core/codex-lb/codex-lb-tool-output-recovery.js +0 -290
- package/dist/core/codex-lb/desktop-center-credentials.js +0 -1083
- package/dist/core/codex-lb/desktop-mode.js +0 -32
- package/dist/core/codex-lb/desktop-real-evidence.js +0 -168
- package/dist/core/codex-lb/legacy-migration.js +0 -576
- package/dist/core/codex-lb/native-openai-transport/transport.js +0 -68
- package/dist/core/codex-lb/provider-routing/provider-router.js +0 -58
- package/dist/core/codex-lb/routing-truth.js +0 -327
- package/dist/core/imagegen/codex-lb-imagegen-target.js +0 -92
- package/dist/native/sks-menubar/Sources/ProvidersConnectTest.swift +0 -118
- package/dist/native/sks-menubar/Sources/ProvidersFastMode.swift +0 -17
- package/dist/native/sks-menubar/Sources/ProvidersMultiProvider.swift +0 -331
- package/dist/native/sks-menubar/Sources/ProvidersRoleModels.swift +0 -260
- package/dist/scripts/codex-lb-catalog-passthrough-check.js +0 -120
- package/dist/scripts/codex-lb-desktop-auth-invariant-check.js +0 -169
- package/dist/scripts/codex-lb-desktop-capabilities-check.js +0 -287
- package/dist/scripts/codex-lb-desktop-real-evidence-check.js +0 -123
- package/dist/scripts/codex-lb-fast-mode-truth-check.js +0 -114
- package/dist/scripts/codex-lb-fast-ui-preservation-check.js +0 -152
- package/dist/scripts/codex-lb-gpt56-fast-profile-check.js +0 -231
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants as fsConstants } from 'node:fs';
|
|
2
3
|
import fsp from 'node:fs/promises';
|
|
3
4
|
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { messageOf as errorMessage } from '../errors/message.js';
|
|
6
|
-
import { ensureDir, exists,
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import { ensureDir, exists, writeTextAtomic } from '../fsx.js';
|
|
8
|
+
import { withFileLock } from '../locks/file-lock.js';
|
|
9
|
+
export function desktopBridgeUnificationReceiptDir(home = process.env.HOME || os.homedir()) {
|
|
10
|
+
return path.join(home, '.codex', 'sks-desktop-bridge-migrations');
|
|
9
11
|
}
|
|
10
|
-
export function
|
|
12
|
+
export function desktopBridgeMigrationTransactionLockPath(home) {
|
|
13
|
+
return path.join(path.resolve(home), '.codex', 'sks', 'locks', 'desktop-bridge-migration.lock');
|
|
14
|
+
}
|
|
15
|
+
export function createDesktopBridgeUnificationReceiptId(now = new Date()) {
|
|
11
16
|
return `${now.toISOString().replace(/[-:.TZ]/g, '')}-${randomUUID().slice(0, 8)}`;
|
|
12
17
|
}
|
|
13
|
-
export async function
|
|
18
|
+
export async function backupDesktopBridgeMigrationFile(filePath, backupDir, ownedBySks) {
|
|
14
19
|
const before = await readRegularFileOrMissing(filePath);
|
|
15
20
|
if (!before) {
|
|
16
21
|
return {
|
|
@@ -20,7 +25,7 @@ export async function backupCodexLbMigrationFile(filePath, backupDir, ownedBySks
|
|
|
20
25
|
owned_by_sks: ownedBySks
|
|
21
26
|
};
|
|
22
27
|
}
|
|
23
|
-
await
|
|
28
|
+
await ensurePrivateDirectory(backupDir);
|
|
24
29
|
const backupPath = path.join(backupDir, `${createHash('sha256').update(path.resolve(filePath)).digest('hex').slice(0, 16)}.before`);
|
|
25
30
|
await writeBufferAtomic(backupPath, before, 0o600);
|
|
26
31
|
return {
|
|
@@ -30,160 +35,337 @@ export async function backupCodexLbMigrationFile(filePath, backupDir, ownedBySks
|
|
|
30
35
|
owned_by_sks: ownedBySks
|
|
31
36
|
};
|
|
32
37
|
}
|
|
33
|
-
export async function
|
|
38
|
+
export async function finalizeDesktopBridgeMigrationReceiptFiles(backups) {
|
|
34
39
|
return Promise.all(backups.map(async (backup) => ({
|
|
35
40
|
...backup,
|
|
36
41
|
after_sha256: await fileSha256OrMissing(backup.path)
|
|
37
42
|
})));
|
|
38
43
|
}
|
|
39
|
-
export async function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
44
|
+
export async function writeDesktopBridgeUnificationReceipt(receipt, input = {}) {
|
|
45
|
+
normalizeHistoricalSelectionCompatibility(receipt);
|
|
46
|
+
validateDesktopBridgeUnificationReceipt(receipt);
|
|
47
|
+
const receiptDir = input.receiptDir || desktopBridgeUnificationReceiptDir();
|
|
48
|
+
const receiptPath = input.receiptPath || path.join(receiptDir, `${receipt.receipt_id}.json`);
|
|
49
|
+
await ensurePrivateDirectory(path.dirname(receiptPath));
|
|
43
50
|
await writeTextAtomic(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o600 });
|
|
44
51
|
await fsp.chmod(receiptPath, 0o600).catch(() => { });
|
|
45
52
|
return receiptPath;
|
|
46
53
|
}
|
|
47
|
-
export async function
|
|
48
|
-
const text = await
|
|
54
|
+
export async function readDesktopBridgeUnificationReceipt(receiptPath) {
|
|
55
|
+
const text = (await readSecureOwnerFile(receiptPath, 1024 * 1024, 'desktop_bridge_receipt')).toString('utf8');
|
|
49
56
|
let parsed;
|
|
50
57
|
try {
|
|
51
58
|
parsed = JSON.parse(text);
|
|
52
59
|
}
|
|
53
60
|
catch {
|
|
54
|
-
throw new Error('
|
|
61
|
+
throw new Error('invalid_desktop_bridge_unification_receipt');
|
|
55
62
|
}
|
|
56
|
-
|
|
63
|
+
normalizeHistoricalSelectionCompatibility(parsed);
|
|
64
|
+
validateDesktopBridgeUnificationReceipt(parsed);
|
|
57
65
|
return parsed;
|
|
58
66
|
}
|
|
59
|
-
export async function
|
|
67
|
+
export async function rollbackDesktopBridgeUnificationReceipt(input) {
|
|
60
68
|
let receipt;
|
|
61
69
|
try {
|
|
62
70
|
if (input.receipt) {
|
|
63
|
-
|
|
71
|
+
normalizeHistoricalSelectionCompatibility(input.receipt);
|
|
72
|
+
validateDesktopBridgeUnificationReceipt(input.receipt);
|
|
64
73
|
receipt = input.receipt;
|
|
65
74
|
}
|
|
66
75
|
else if (input.receiptPath) {
|
|
67
|
-
receipt = await
|
|
76
|
+
receipt = await readDesktopBridgeUnificationReceipt(input.receiptPath);
|
|
68
77
|
}
|
|
69
78
|
else {
|
|
70
|
-
throw new Error('
|
|
79
|
+
throw new Error('missing_desktop_bridge_unification_receipt');
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
82
|
catch (error) {
|
|
74
83
|
return {
|
|
75
|
-
schema: 'sks.
|
|
84
|
+
schema: 'sks.desktop-bridge-unification-rollback.v1',
|
|
76
85
|
ok: false,
|
|
77
86
|
status: 'invalid_receipt',
|
|
78
87
|
receipt_id: null,
|
|
79
88
|
restored_files: [],
|
|
89
|
+
credentials_overwritten: false,
|
|
90
|
+
auth_overwritten: false,
|
|
80
91
|
conflicts: [],
|
|
81
92
|
error: errorMessage(error)
|
|
82
93
|
};
|
|
83
94
|
}
|
|
84
|
-
|
|
85
|
-
for (const file of receipt.files) {
|
|
86
|
-
const currentSha = await fileSha256OrMissing(file.path);
|
|
87
|
-
if (currentSha !== file.after_sha256) {
|
|
88
|
-
conflicts.push({
|
|
89
|
-
path: file.path,
|
|
90
|
-
expected_after_sha256: file.after_sha256,
|
|
91
|
-
current_sha256: currentSha,
|
|
92
|
-
reason: 'current_file_changed_after_migration'
|
|
93
|
-
});
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (file.before_sha256 !== null) {
|
|
97
|
-
if (!file.backup_path || !(await exists(file.backup_path))) {
|
|
98
|
-
conflicts.push({
|
|
99
|
-
path: file.path,
|
|
100
|
-
expected_after_sha256: file.after_sha256,
|
|
101
|
-
current_sha256: currentSha,
|
|
102
|
-
reason: 'before_backup_missing'
|
|
103
|
-
});
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
const backupSha = await fileSha256OrMissing(file.backup_path);
|
|
107
|
-
if (backupSha !== file.before_sha256) {
|
|
108
|
-
conflicts.push({
|
|
109
|
-
path: file.path,
|
|
110
|
-
expected_after_sha256: file.after_sha256,
|
|
111
|
-
current_sha256: currentSha,
|
|
112
|
-
reason: 'before_backup_hash_mismatch'
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
if (conflicts.length) {
|
|
95
|
+
if (receipt.migration_status === 'already_migrated') {
|
|
118
96
|
return {
|
|
119
|
-
schema: 'sks.
|
|
120
|
-
ok:
|
|
121
|
-
status: '
|
|
122
|
-
receipt_id: receipt.
|
|
97
|
+
schema: 'sks.desktop-bridge-unification-rollback.v1',
|
|
98
|
+
ok: true,
|
|
99
|
+
status: 'nothing_to_rollback',
|
|
100
|
+
receipt_id: receipt.receipt_id,
|
|
123
101
|
restored_files: [],
|
|
124
|
-
|
|
102
|
+
credentials_overwritten: false,
|
|
103
|
+
auth_overwritten: false,
|
|
104
|
+
conflicts: []
|
|
125
105
|
};
|
|
126
106
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
107
|
+
if (input.transactionLockHeld !== true) {
|
|
108
|
+
const configTarget = receipt.rollback_metadata.files.find((file) => file.kind === 'config');
|
|
109
|
+
if (!configTarget) {
|
|
110
|
+
return {
|
|
111
|
+
schema: 'sks.desktop-bridge-unification-rollback.v1',
|
|
112
|
+
ok: false,
|
|
113
|
+
status: 'invalid_receipt',
|
|
114
|
+
receipt_id: receipt.receipt_id,
|
|
115
|
+
restored_files: [],
|
|
116
|
+
credentials_overwritten: false,
|
|
117
|
+
auth_overwritten: false,
|
|
118
|
+
conflicts: [],
|
|
119
|
+
error: 'desktop_bridge_rollback_config_target_missing'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const home = path.dirname(path.dirname(path.resolve(configTarget.path)));
|
|
123
|
+
return withFileLock({
|
|
124
|
+
lockPath: desktopBridgeMigrationTransactionLockPath(home),
|
|
125
|
+
timeoutMs: 30_000,
|
|
126
|
+
staleMs: 120_000
|
|
127
|
+
}, () => rollbackDesktopBridgeUnificationReceipt({
|
|
128
|
+
receipt,
|
|
129
|
+
...(input.beforeTargetLocks ? { beforeTargetLocks: input.beforeTargetLocks } : {}),
|
|
130
|
+
transactionLockHeld: true
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
const files = receipt.rollback_metadata.files;
|
|
134
|
+
const initial = await inspectRollbackFiles(files);
|
|
135
|
+
if (initial.conflicts.length)
|
|
136
|
+
return rollbackConflict(receipt.receipt_id, initial.conflicts);
|
|
131
137
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
await input.beforeTargetLocks?.();
|
|
139
|
+
return await withRollbackTargetLocks(files, async () => {
|
|
140
|
+
const locked = await inspectRollbackFiles(files, initial.snapshots);
|
|
141
|
+
if (locked.conflicts.length)
|
|
142
|
+
return rollbackConflict(receipt.receipt_id, locked.conflicts);
|
|
143
|
+
const restoredFiles = [];
|
|
144
|
+
try {
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
if (file.before_sha256 === null) {
|
|
147
|
+
await fsp.rm(file.path, { force: true });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
if (!file.backup_path)
|
|
151
|
+
throw new Error(`before_backup_missing:${file.path}`);
|
|
152
|
+
const backup = locked.backupBytes.get(path.resolve(file.path));
|
|
153
|
+
if (!backup)
|
|
154
|
+
throw new Error(`before_backup_unavailable:${file.path}`);
|
|
155
|
+
await writeBufferAtomic(file.path, backup, 0o600);
|
|
156
|
+
}
|
|
157
|
+
if ((await fileSha256OrMissing(file.path)) !== file.before_sha256) {
|
|
158
|
+
throw new Error(`rollback_readback_failed:${file.path}`);
|
|
159
|
+
}
|
|
160
|
+
restoredFiles.push(file.path);
|
|
161
|
+
}
|
|
141
162
|
}
|
|
142
|
-
|
|
143
|
-
|
|
163
|
+
catch (error) {
|
|
164
|
+
for (const file of files) {
|
|
165
|
+
const snapshot = locked.snapshots.get(path.resolve(file.path));
|
|
166
|
+
try {
|
|
167
|
+
if (!snapshot || snapshot.bytes === null)
|
|
168
|
+
await fsp.rm(file.path, { force: true });
|
|
169
|
+
else
|
|
170
|
+
await writeBufferAtomic(file.path, snapshot.bytes, 0o600);
|
|
171
|
+
}
|
|
172
|
+
catch { }
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
schema: 'sks.desktop-bridge-unification-rollback.v1',
|
|
176
|
+
ok: false,
|
|
177
|
+
status: 'rollback_failed',
|
|
178
|
+
receipt_id: receipt.receipt_id,
|
|
179
|
+
restored_files: [],
|
|
180
|
+
credentials_overwritten: false,
|
|
181
|
+
auth_overwritten: false,
|
|
182
|
+
conflicts: [],
|
|
183
|
+
error: errorMessage(error)
|
|
184
|
+
};
|
|
144
185
|
}
|
|
145
|
-
|
|
146
|
-
|
|
186
|
+
return {
|
|
187
|
+
schema: 'sks.desktop-bridge-unification-rollback.v1',
|
|
188
|
+
ok: true,
|
|
189
|
+
status: 'rolled_back',
|
|
190
|
+
receipt_id: receipt.receipt_id,
|
|
191
|
+
restored_files: restoredFiles,
|
|
192
|
+
credentials_overwritten: false,
|
|
193
|
+
auth_overwritten: false,
|
|
194
|
+
conflicts: []
|
|
195
|
+
};
|
|
196
|
+
});
|
|
147
197
|
}
|
|
148
198
|
catch (error) {
|
|
149
|
-
for (const [filePath, bytes] of currentBytes) {
|
|
150
|
-
try {
|
|
151
|
-
if (bytes === null)
|
|
152
|
-
await fsp.rm(filePath, { force: true });
|
|
153
|
-
else
|
|
154
|
-
await writeBufferAtomic(filePath, bytes, 0o600);
|
|
155
|
-
}
|
|
156
|
-
catch { }
|
|
157
|
-
}
|
|
158
199
|
return {
|
|
159
|
-
schema: 'sks.
|
|
200
|
+
schema: 'sks.desktop-bridge-unification-rollback.v1',
|
|
160
201
|
ok: false,
|
|
161
202
|
status: 'rollback_failed',
|
|
162
|
-
receipt_id: receipt.
|
|
203
|
+
receipt_id: receipt.receipt_id,
|
|
163
204
|
restored_files: [],
|
|
205
|
+
credentials_overwritten: false,
|
|
206
|
+
auth_overwritten: false,
|
|
164
207
|
conflicts: [],
|
|
165
208
|
error: errorMessage(error)
|
|
166
209
|
};
|
|
167
210
|
}
|
|
211
|
+
}
|
|
212
|
+
async function inspectRollbackFiles(files, expectedSnapshots) {
|
|
213
|
+
const backups = await inspectRollbackBackups(files);
|
|
214
|
+
const targets = await inspectRollbackTargets(files, expectedSnapshots);
|
|
168
215
|
return {
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
receipt_id: receipt.id,
|
|
173
|
-
restored_files: restoredFiles,
|
|
174
|
-
conflicts: []
|
|
216
|
+
snapshots: targets.snapshots,
|
|
217
|
+
backupBytes: backups.backupBytes,
|
|
218
|
+
conflicts: [...backups.conflicts, ...targets.conflicts]
|
|
175
219
|
};
|
|
176
220
|
}
|
|
221
|
+
async function inspectRollbackBackups(files) {
|
|
222
|
+
const backupBytes = new Map();
|
|
223
|
+
const conflicts = [];
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
if (file.before_sha256 === null)
|
|
226
|
+
continue;
|
|
227
|
+
const resolved = path.resolve(file.path);
|
|
228
|
+
if (!file.backup_path || !(await exists(file.backup_path))) {
|
|
229
|
+
conflicts.push(rollbackFileConflict(file, null, 'before_backup_missing'));
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
let backup;
|
|
233
|
+
try {
|
|
234
|
+
backup = await readSecureOwnerFile(file.backup_path, 16 * 1024 * 1024, 'desktop_bridge_backup');
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
conflicts.push(rollbackFileConflict(file, null, 'before_backup_unsafe'));
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (sha256(backup) !== file.before_sha256) {
|
|
241
|
+
conflicts.push(rollbackFileConflict(file, null, 'before_backup_hash_mismatch'));
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
backupBytes.set(resolved, backup);
|
|
245
|
+
}
|
|
246
|
+
return { backupBytes, conflicts };
|
|
247
|
+
}
|
|
248
|
+
async function inspectRollbackTargets(files, expectedSnapshots) {
|
|
249
|
+
const snapshots = new Map();
|
|
250
|
+
const conflicts = [];
|
|
251
|
+
for (const file of files) {
|
|
252
|
+
const resolved = path.resolve(file.path);
|
|
253
|
+
let snapshot;
|
|
254
|
+
try {
|
|
255
|
+
snapshot = await readSecureRollbackTarget(resolved);
|
|
256
|
+
snapshots.set(resolved, snapshot);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
conflicts.push(rollbackFileConflict(file, null, 'current_file_unsafe'));
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (snapshot.sha256 !== file.after_sha256) {
|
|
263
|
+
conflicts.push(rollbackFileConflict(file, snapshot.sha256, 'current_file_changed_after_migration'));
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const expected = expectedSnapshots?.get(resolved);
|
|
267
|
+
if (expected && !sameRollbackTargetIdentity(expected, snapshot)) {
|
|
268
|
+
conflicts.push(rollbackFileConflict(file, snapshot.sha256, 'current_file_identity_changed_during_rollback'));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { snapshots, conflicts };
|
|
272
|
+
}
|
|
273
|
+
async function readSecureRollbackTarget(filePath) {
|
|
274
|
+
const parent = await fsp.lstat(path.dirname(filePath));
|
|
275
|
+
const expectedUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
276
|
+
if (!parent.isDirectory() || parent.isSymbolicLink() || (expectedUid !== null && parent.uid !== expectedUid)) {
|
|
277
|
+
throw new Error(`desktop_bridge_rollback_parent_unsafe:${filePath}`);
|
|
278
|
+
}
|
|
279
|
+
const parentIdentity = parentStatIdentity(parent);
|
|
280
|
+
let entry;
|
|
281
|
+
try {
|
|
282
|
+
entry = await fsp.lstat(filePath);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
if (isMissingFileError(error)) {
|
|
286
|
+
return { bytes: null, sha256: null, identity: null, parent_identity: parentIdentity };
|
|
287
|
+
}
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
if (!entry.isFile() || entry.isSymbolicLink())
|
|
291
|
+
throw new Error(`desktop_bridge_rollback_target_unsafe:${filePath}`);
|
|
292
|
+
const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0);
|
|
293
|
+
const handle = await fsp.open(filePath, flags);
|
|
294
|
+
try {
|
|
295
|
+
const stat = await handle.stat();
|
|
296
|
+
if (!stat.isFile()
|
|
297
|
+
|| stat.dev !== entry.dev
|
|
298
|
+
|| stat.ino !== entry.ino
|
|
299
|
+
|| (expectedUid !== null && stat.uid !== expectedUid)
|
|
300
|
+
|| (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)) {
|
|
301
|
+
throw new Error(`desktop_bridge_rollback_target_unsafe:${filePath}`);
|
|
302
|
+
}
|
|
303
|
+
const bytes = await handle.readFile();
|
|
304
|
+
return {
|
|
305
|
+
bytes,
|
|
306
|
+
sha256: sha256(bytes),
|
|
307
|
+
identity: statIdentity(stat),
|
|
308
|
+
parent_identity: parentIdentity
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
await handle.close().catch(() => undefined);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function statIdentity(stat) {
|
|
316
|
+
return [stat.dev, stat.ino, stat.uid, stat.mode & 0o777, stat.size, stat.mtimeMs, stat.ctimeMs].join(':');
|
|
317
|
+
}
|
|
318
|
+
function parentStatIdentity(stat) {
|
|
319
|
+
return [stat.dev, stat.ino, stat.uid, stat.mode & 0o777].join(':');
|
|
320
|
+
}
|
|
321
|
+
function sameRollbackTargetIdentity(before, after) {
|
|
322
|
+
return before.identity === after.identity && before.parent_identity === after.parent_identity;
|
|
323
|
+
}
|
|
324
|
+
function rollbackFileConflict(file, currentSha, reason) {
|
|
325
|
+
return {
|
|
326
|
+
path: file.path,
|
|
327
|
+
expected_after_sha256: file.after_sha256,
|
|
328
|
+
current_sha256: currentSha,
|
|
329
|
+
reason
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function rollbackConflict(receiptId, conflicts) {
|
|
333
|
+
return {
|
|
334
|
+
schema: 'sks.desktop-bridge-unification-rollback.v1',
|
|
335
|
+
ok: false,
|
|
336
|
+
status: 'rollback_conflict',
|
|
337
|
+
receipt_id: receiptId,
|
|
338
|
+
restored_files: [],
|
|
339
|
+
credentials_overwritten: false,
|
|
340
|
+
auth_overwritten: false,
|
|
341
|
+
conflicts
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
export function desktopBridgeRollbackTargetLockPath(filePath) {
|
|
345
|
+
return `${path.resolve(filePath)}.lock`;
|
|
346
|
+
}
|
|
347
|
+
async function withRollbackTargetLocks(files, operation) {
|
|
348
|
+
const targets = [...new Set(files.map((file) => path.resolve(file.path)))]
|
|
349
|
+
.sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
350
|
+
const acquire = async (index) => {
|
|
351
|
+
const target = targets[index];
|
|
352
|
+
if (!target)
|
|
353
|
+
return operation();
|
|
354
|
+
return withFileLock({
|
|
355
|
+
lockPath: desktopBridgeRollbackTargetLockPath(target),
|
|
356
|
+
timeoutMs: 10_000,
|
|
357
|
+
staleMs: 60_000
|
|
358
|
+
}, () => acquire(index + 1));
|
|
359
|
+
};
|
|
360
|
+
return acquire(0);
|
|
361
|
+
}
|
|
177
362
|
export async function fileSha256OrMissing(filePath) {
|
|
178
363
|
const bytes = await readRegularFileOrMissing(filePath);
|
|
179
364
|
return bytes === null ? null : sha256(bytes);
|
|
180
365
|
}
|
|
181
366
|
async function readRegularFileOrMissing(filePath) {
|
|
182
367
|
try {
|
|
183
|
-
|
|
184
|
-
if (!stat.isFile() || stat.isSymbolicLink())
|
|
185
|
-
throw new Error(`migration_file_not_regular:${filePath}`);
|
|
186
|
-
return await fsp.readFile(filePath);
|
|
368
|
+
return await readFileWithoutFollowingSymlinks(filePath, Number.MAX_SAFE_INTEGER, 'migration_file');
|
|
187
369
|
}
|
|
188
370
|
catch (error) {
|
|
189
371
|
if (isMissingFileError(error))
|
|
@@ -191,6 +373,44 @@ async function readRegularFileOrMissing(filePath) {
|
|
|
191
373
|
throw error;
|
|
192
374
|
}
|
|
193
375
|
}
|
|
376
|
+
async function readSecureOwnerFile(filePath, maxBytes, prefix) {
|
|
377
|
+
return readFileWithoutFollowingSymlinks(filePath, maxBytes, prefix, true);
|
|
378
|
+
}
|
|
379
|
+
async function readFileWithoutFollowingSymlinks(filePath, maxBytes, prefix, ownerOnly = false) {
|
|
380
|
+
const flags = fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0);
|
|
381
|
+
const handle = await fsp.open(filePath, flags);
|
|
382
|
+
try {
|
|
383
|
+
const stat = await handle.stat();
|
|
384
|
+
if (!stat.isFile())
|
|
385
|
+
throw new Error(`${prefix}_not_regular:${filePath}`);
|
|
386
|
+
if (stat.size > maxBytes)
|
|
387
|
+
throw new Error(`${prefix}_too_large:${filePath}`);
|
|
388
|
+
if (ownerOnly) {
|
|
389
|
+
const expectedUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
390
|
+
if (expectedUid !== null && stat.uid !== expectedUid)
|
|
391
|
+
throw new Error(`${prefix}_owner_mismatch:${filePath}`);
|
|
392
|
+
if (process.platform !== 'win32' && (stat.mode & 0o077) !== 0)
|
|
393
|
+
throw new Error(`${prefix}_permissions_unsafe:${filePath}`);
|
|
394
|
+
}
|
|
395
|
+
return await handle.readFile();
|
|
396
|
+
}
|
|
397
|
+
finally {
|
|
398
|
+
await handle.close().catch(() => { });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function ensurePrivateDirectory(directory) {
|
|
402
|
+
await fsp.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
403
|
+
const stat = await fsp.lstat(directory);
|
|
404
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
405
|
+
throw new Error(`desktop_bridge_private_directory_invalid:${directory}`);
|
|
406
|
+
}
|
|
407
|
+
const expectedUid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
408
|
+
if (expectedUid !== null && stat.uid !== expectedUid) {
|
|
409
|
+
throw new Error(`desktop_bridge_private_directory_owner_mismatch:${directory}`);
|
|
410
|
+
}
|
|
411
|
+
if (process.platform !== 'win32')
|
|
412
|
+
await fsp.chmod(directory, 0o700);
|
|
413
|
+
}
|
|
194
414
|
async function writeBufferAtomic(filePath, bytes, mode) {
|
|
195
415
|
await ensureDir(path.dirname(filePath));
|
|
196
416
|
const tempPath = `${filePath}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
|
|
@@ -212,31 +432,253 @@ async function writeBufferAtomic(filePath, bytes, mode) {
|
|
|
212
432
|
throw error;
|
|
213
433
|
}
|
|
214
434
|
}
|
|
215
|
-
function
|
|
216
|
-
if (!value || typeof value !== 'object')
|
|
217
|
-
throw new Error('
|
|
435
|
+
function validateDesktopBridgeUnificationReceipt(value) {
|
|
436
|
+
if (!value || typeof value !== 'object') {
|
|
437
|
+
throw new Error('invalid_desktop_bridge_unification_receipt');
|
|
438
|
+
}
|
|
218
439
|
const receipt = value;
|
|
219
|
-
if (receipt.schema !== 'sks.
|
|
220
|
-
|| typeof receipt.
|
|
221
|
-
||
|
|
440
|
+
if (receipt.schema !== 'sks.desktop-bridge-unification-receipt.v1'
|
|
441
|
+
|| typeof receipt.receipt_id !== 'string'
|
|
442
|
+
|| !/^[A-Za-z0-9._-]{1,160}$/.test(receipt.receipt_id)
|
|
222
443
|
|| typeof receipt.created_at !== 'string'
|
|
223
|
-
||
|
|
224
|
-
||
|
|
225
|
-
||
|
|
226
|
-
|| typeof receipt.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
444
|
+
|| receipt.baseline_version !== '8.1.2'
|
|
445
|
+
|| receipt.target_version !== '8.1.3'
|
|
446
|
+
|| typeof receipt.config_before_sha256 !== 'string'
|
|
447
|
+
|| typeof receipt.config_after_sha256 !== 'string'
|
|
448
|
+
|| typeof receipt.auth_before_sha256 !== 'string'
|
|
449
|
+
|| typeof receipt.auth_after_sha256 !== 'string'
|
|
450
|
+
|| typeof receipt.auth_semantic_identity_preserved !== 'boolean'
|
|
451
|
+
|| !receipt.historical_state
|
|
452
|
+
|| typeof receipt.historical_state !== 'object'
|
|
453
|
+
|| !Array.isArray(receipt.migrated_profiles)
|
|
454
|
+
|| receipt.credentials_deleted !== false
|
|
455
|
+
|| receipt.new_runtime !== 'desktop-bridge'
|
|
456
|
+
|| !Array.isArray(receipt.backup_paths)
|
|
457
|
+
|| typeof receipt.rollback_supported !== 'boolean'
|
|
458
|
+
|| !Array.isArray(receipt.blockers)
|
|
459
|
+
|| (receipt.migration_status !== 'migrated' && receipt.migration_status !== 'already_migrated')
|
|
460
|
+
|| receipt.rollback_metadata?.schema !== 'sks.desktop-bridge-unification-rollback-metadata.v1'
|
|
461
|
+
|| !Array.isArray(receipt.rollback_metadata.files)) {
|
|
462
|
+
throw new Error('invalid_desktop_bridge_unification_receipt');
|
|
463
|
+
}
|
|
464
|
+
for (const key of [
|
|
465
|
+
'desktop_mode',
|
|
466
|
+
'historical_provider_selection',
|
|
467
|
+
'model_provider',
|
|
468
|
+
'catalog_path'
|
|
469
|
+
]) {
|
|
470
|
+
const entry = receipt.historical_state[key];
|
|
471
|
+
if (entry !== null && typeof entry !== 'string') {
|
|
472
|
+
throw new Error('invalid_desktop_bridge_unification_receipt_historical_state');
|
|
238
473
|
}
|
|
239
474
|
}
|
|
475
|
+
if (receipt.migrated_profiles.some((entry) => entry !== 'codex-lb' && entry !== 'openrouter')) {
|
|
476
|
+
throw new Error('invalid_desktop_bridge_unification_receipt_provider');
|
|
477
|
+
}
|
|
478
|
+
if (receipt.new_catalog_generation !== null
|
|
479
|
+
&& typeof receipt.new_catalog_generation !== 'string') {
|
|
480
|
+
throw new Error('invalid_desktop_bridge_unification_receipt_catalog_generation');
|
|
481
|
+
}
|
|
482
|
+
const seenPaths = new Set();
|
|
483
|
+
const canonicalHomes = new Set();
|
|
484
|
+
for (const file of receipt.rollback_metadata.files) {
|
|
485
|
+
validateDesktopBridgeRollbackFile(file);
|
|
486
|
+
if (file.backup_path !== null) {
|
|
487
|
+
validateDesktopBridgeBackupPath(file.backup_path, receipt.receipt_id);
|
|
488
|
+
}
|
|
489
|
+
const resolved = path.resolve(file.path);
|
|
490
|
+
if (seenPaths.has(resolved)) {
|
|
491
|
+
throw new Error('invalid_desktop_bridge_unification_receipt_duplicate_path');
|
|
492
|
+
}
|
|
493
|
+
seenPaths.add(resolved);
|
|
494
|
+
canonicalHomes.add(canonicalHomeForRollbackFile(file));
|
|
495
|
+
}
|
|
496
|
+
if (canonicalHomes.size > 1) {
|
|
497
|
+
throw new Error('desktop_bridge_rollback_metadata_home_mismatch');
|
|
498
|
+
}
|
|
499
|
+
const metadataBackupPaths = new Set(receipt.rollback_metadata.files
|
|
500
|
+
.map((file) => file.backup_path)
|
|
501
|
+
.filter((entry) => typeof entry === 'string'));
|
|
502
|
+
if (receipt.backup_paths.some((entry) => typeof entry !== 'string' || !metadataBackupPaths.has(entry))
|
|
503
|
+
|| metadataBackupPaths.size !== receipt.backup_paths.length) {
|
|
504
|
+
throw new Error('invalid_desktop_bridge_unification_receipt_backup_paths');
|
|
505
|
+
}
|
|
506
|
+
if (receipt.blockers.some((entry) => typeof entry !== 'string')) {
|
|
507
|
+
throw new Error('invalid_desktop_bridge_unification_receipt_blockers');
|
|
508
|
+
}
|
|
509
|
+
if (receipt.migration_status === 'already_migrated') {
|
|
510
|
+
if (receipt.rollback_supported !== false
|
|
511
|
+
|| receipt.backup_paths.length !== 0
|
|
512
|
+
|| receipt.rollback_metadata.files.length !== 0
|
|
513
|
+
|| receipt.config_before_sha256 !== receipt.config_after_sha256
|
|
514
|
+
|| receipt.auth_before_sha256 !== receipt.auth_after_sha256
|
|
515
|
+
|| receipt.auth_semantic_identity_preserved !== true) {
|
|
516
|
+
throw new Error('invalid_desktop_bridge_unification_noop_receipt');
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
else if (receipt.rollback_supported !== true) {
|
|
520
|
+
throw new Error('invalid_desktop_bridge_unification_migrated_receipt');
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function normalizeHistoricalSelectionCompatibility(value) {
|
|
524
|
+
if (!value || typeof value !== 'object')
|
|
525
|
+
return;
|
|
526
|
+
const historicalState = value.historical_state;
|
|
527
|
+
if (!historicalState || typeof historicalState !== 'object')
|
|
528
|
+
return;
|
|
529
|
+
const state = historicalState;
|
|
530
|
+
const compatibilityKey = ['provider', 'mode'].join('_');
|
|
531
|
+
const compatibilityValue = state[compatibilityKey];
|
|
532
|
+
if (compatibilityValue === undefined)
|
|
533
|
+
return;
|
|
534
|
+
const currentValue = state.historical_provider_selection;
|
|
535
|
+
if (currentValue !== undefined && currentValue !== compatibilityValue) {
|
|
536
|
+
throw new Error('invalid_desktop_bridge_unification_receipt_historical_selection_conflict');
|
|
537
|
+
}
|
|
538
|
+
if (currentValue === undefined) {
|
|
539
|
+
state.historical_provider_selection = compatibilityValue;
|
|
540
|
+
}
|
|
541
|
+
delete state[compatibilityKey];
|
|
542
|
+
}
|
|
543
|
+
function validateDesktopBridgeBackupPath(backupPath, receiptId) {
|
|
544
|
+
const resolved = path.resolve(backupPath);
|
|
545
|
+
const filesDir = path.dirname(resolved);
|
|
546
|
+
const receiptDir = path.dirname(filesDir);
|
|
547
|
+
if (path.basename(resolved).match(/^[a-f0-9]{16}\.before$/) === null
|
|
548
|
+
|| path.basename(filesDir) !== 'files'
|
|
549
|
+
|| path.basename(receiptDir) !== receiptId) {
|
|
550
|
+
throw new Error('desktop_bridge_rollback_backup_path_not_canonical');
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
function validateDesktopBridgeRollbackFile(file) {
|
|
554
|
+
if (!file || typeof file !== 'object') {
|
|
555
|
+
throw new Error('invalid_desktop_bridge_unification_rollback_file');
|
|
556
|
+
}
|
|
557
|
+
const entry = file;
|
|
558
|
+
const kinds = [
|
|
559
|
+
'config',
|
|
560
|
+
'bridge_settings',
|
|
561
|
+
'provider_registry',
|
|
562
|
+
'catalog_binding',
|
|
563
|
+
'route_policy',
|
|
564
|
+
'launchd_state'
|
|
565
|
+
];
|
|
566
|
+
if (!entry.kind
|
|
567
|
+
|| !kinds.includes(entry.kind)
|
|
568
|
+
|| typeof entry.path !== 'string'
|
|
569
|
+
|| !entry.path
|
|
570
|
+
|| (entry.before_sha256 !== null && typeof entry.before_sha256 !== 'string')
|
|
571
|
+
|| (entry.after_sha256 !== null && typeof entry.after_sha256 !== 'string')
|
|
572
|
+
|| (entry.backup_path !== null && typeof entry.backup_path !== 'string')
|
|
573
|
+
|| typeof entry.owned_by_sks !== 'boolean') {
|
|
574
|
+
throw new Error('invalid_desktop_bridge_unification_rollback_file');
|
|
575
|
+
}
|
|
576
|
+
const hashPattern = /^[a-f0-9]{64}$/;
|
|
577
|
+
if ((entry.before_sha256 !== null && !hashPattern.test(entry.before_sha256))
|
|
578
|
+
|| (entry.after_sha256 !== null && !hashPattern.test(entry.after_sha256))
|
|
579
|
+
|| (entry.before_sha256 === null && entry.backup_path !== null)
|
|
580
|
+
|| (entry.before_sha256 !== null && entry.backup_path === null)
|
|
581
|
+
|| (entry.backup_path !== null && !path.isAbsolute(entry.backup_path))) {
|
|
582
|
+
throw new Error('invalid_desktop_bridge_unification_rollback_file');
|
|
583
|
+
}
|
|
584
|
+
const resolved = path.resolve(entry.path);
|
|
585
|
+
const basename = path.basename(resolved).toLowerCase();
|
|
586
|
+
const segments = resolved.toLowerCase().split(path.sep);
|
|
587
|
+
const sensitiveBasenames = new Set([
|
|
588
|
+
'auth.json',
|
|
589
|
+
'sks-codex-lb.env',
|
|
590
|
+
'openrouter-api-key',
|
|
591
|
+
'openrouter-api-key.json'
|
|
592
|
+
]);
|
|
593
|
+
if (sensitiveBasenames.has(basename)
|
|
594
|
+
|| segments.includes('secrets')
|
|
595
|
+
|| /(?:credential|api-key|secret)/i.test(basename)) {
|
|
596
|
+
throw new Error('desktop_bridge_rollback_secret_file_forbidden');
|
|
597
|
+
}
|
|
598
|
+
if (!path.isAbsolute(entry.path)) {
|
|
599
|
+
throw new Error('desktop_bridge_rollback_metadata_path_not_canonical');
|
|
600
|
+
}
|
|
601
|
+
if (entry.kind === 'config' ? entry.owned_by_sks : !entry.owned_by_sks) {
|
|
602
|
+
throw new Error('desktop_bridge_rollback_metadata_ownership_invalid');
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
function canonicalHomeForRollbackFile(file) {
|
|
606
|
+
const resolved = path.resolve(file.path);
|
|
607
|
+
const retiredArtifact = retiredDesktopBridgeRuntimeArtifact(resolved);
|
|
608
|
+
if (retiredArtifact !== null) {
|
|
609
|
+
throw new Error(`desktop_bridge_rollback_retired_runtime_target_forbidden:${retiredArtifact}`);
|
|
610
|
+
}
|
|
611
|
+
if (file.kind === 'config') {
|
|
612
|
+
const codexHome = path.dirname(resolved);
|
|
613
|
+
const home = path.dirname(codexHome);
|
|
614
|
+
if (path.basename(resolved) === 'config.toml'
|
|
615
|
+
&& path.basename(codexHome) === '.codex'
|
|
616
|
+
&& path.join(home, '.codex', 'config.toml') === resolved)
|
|
617
|
+
return home;
|
|
618
|
+
}
|
|
619
|
+
else if (file.kind === 'launchd_state') {
|
|
620
|
+
const launchAgents = path.dirname(resolved);
|
|
621
|
+
const library = path.dirname(launchAgents);
|
|
622
|
+
const home = path.dirname(library);
|
|
623
|
+
if (path.basename(resolved) === 'com.sneakoscope.desktop-bridge.plist'
|
|
624
|
+
&& path.basename(launchAgents) === 'LaunchAgents'
|
|
625
|
+
&& path.basename(library) === 'Library'
|
|
626
|
+
&& path.join(home, 'Library', 'LaunchAgents', path.basename(resolved)) === resolved)
|
|
627
|
+
return home;
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
const expectedBasename = {
|
|
631
|
+
bridge_settings: 'desktop-bridge-settings.json',
|
|
632
|
+
provider_registry: 'sks-bridge-provider-registry.json',
|
|
633
|
+
catalog_binding: 'sks-bridge-active-generation.json',
|
|
634
|
+
route_policy: 'sks-bridge-route-policy.json'
|
|
635
|
+
};
|
|
636
|
+
const sksDir = path.dirname(resolved);
|
|
637
|
+
const codexHome = path.dirname(sksDir);
|
|
638
|
+
const home = path.dirname(codexHome);
|
|
639
|
+
if (path.basename(resolved) === expectedBasename[file.kind]
|
|
640
|
+
&& path.basename(sksDir) === 'sks'
|
|
641
|
+
&& path.basename(codexHome) === '.codex'
|
|
642
|
+
&& path.join(home, '.codex', 'sks', expectedBasename[file.kind]) === resolved)
|
|
643
|
+
return home;
|
|
644
|
+
}
|
|
645
|
+
throw new Error(`desktop_bridge_rollback_metadata_path_not_canonical:${file.kind}`);
|
|
646
|
+
}
|
|
647
|
+
function retiredDesktopBridgeRuntimeArtifact(resolvedPath) {
|
|
648
|
+
const basename = path.basename(resolvedPath);
|
|
649
|
+
const runtimeDir = path.dirname(resolvedPath);
|
|
650
|
+
const codexHome = path.dirname(runtimeDir);
|
|
651
|
+
const home = path.dirname(codexHome);
|
|
652
|
+
if (path.basename(runtimeDir) === 'sks'
|
|
653
|
+
&& path.basename(codexHome) === '.codex'
|
|
654
|
+
&& path.join(home, '.codex', 'sks', basename) === resolvedPath) {
|
|
655
|
+
if (basename === 'codex-lb-desktop-bridge-settings.json')
|
|
656
|
+
return 'settings';
|
|
657
|
+
if (basename === 'codex-lb-desktop-bridge.json')
|
|
658
|
+
return 'state';
|
|
659
|
+
}
|
|
660
|
+
const logsDir = path.dirname(resolvedPath);
|
|
661
|
+
const logsRuntimeDir = path.dirname(logsDir);
|
|
662
|
+
const logsCodexHome = path.dirname(logsRuntimeDir);
|
|
663
|
+
const logsHome = path.dirname(logsCodexHome);
|
|
664
|
+
if (path.basename(logsDir) === 'logs'
|
|
665
|
+
&& path.basename(logsRuntimeDir) === 'sks'
|
|
666
|
+
&& path.basename(logsCodexHome) === '.codex'
|
|
667
|
+
&& path.join(logsHome, '.codex', 'sks', 'logs', basename) === resolvedPath) {
|
|
668
|
+
if (basename === 'codex-lb-desktop-bridge.out.log')
|
|
669
|
+
return 'stdout_log';
|
|
670
|
+
if (basename === 'codex-lb-desktop-bridge.err.log')
|
|
671
|
+
return 'stderr_log';
|
|
672
|
+
}
|
|
673
|
+
const launchAgents = path.dirname(resolvedPath);
|
|
674
|
+
const library = path.dirname(launchAgents);
|
|
675
|
+
const launchdHome = path.dirname(library);
|
|
676
|
+
if (basename === 'com.sneakoscope.codex-lb-desktop-bridge.plist'
|
|
677
|
+
&& path.basename(launchAgents) === 'LaunchAgents'
|
|
678
|
+
&& path.basename(library) === 'Library'
|
|
679
|
+
&& path.join(launchdHome, 'Library', 'LaunchAgents', 'com.sneakoscope.codex-lb-desktop-bridge.plist') === resolvedPath)
|
|
680
|
+
return 'plist';
|
|
681
|
+
return null;
|
|
240
682
|
}
|
|
241
683
|
function sha256(bytes) {
|
|
242
684
|
return createHash('sha256').update(bytes).digest('hex');
|