sealkeep 0.8.1 → 0.11.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/ARCHITECTURE.md +163 -14
- package/CHANGELOG.md +244 -1
- package/CONTROL_PLANE.md +2 -2
- package/LICENSE +1 -1
- package/README.md +108 -22
- package/THIRD_PARTY.md +2 -2
- package/THREAT_MODEL.md +23 -4
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
- package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
- package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
- package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
- package/dist/site/index.html +1808 -1904
- package/dist/site/llms.txt +67 -0
- package/dist/site/trust/architecture-data-flow.html +53 -0
- package/dist/site/trust/audit-roadmap.html +37 -0
- package/dist/site/trust/deployment-responsibility.html +11 -0
- package/dist/site/trust/dpa-sample.html +30 -0
- package/dist/site/trust/release-provenance.html +21 -0
- package/dist/site/trust/subprocessors.html +15 -0
- package/dist/site/trust/threat-model.html +34 -0
- package/dist/site/trust/toms.html +41 -0
- package/dist/site/trust-document.css +32 -0
- package/dist/site/trust.html +73 -0
- package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
- package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
- package/dist/site/visual/index.html +18 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +9 -0
- package/dist/src/activity.js +90 -1
- package/dist/src/adapters.d.ts +175 -5
- package/dist/src/adapters.js +961 -73
- package/dist/src/agent-context.d.ts +135 -0
- package/dist/src/agent-context.js +1059 -0
- package/dist/src/archive-copies.d.ts +47 -0
- package/dist/src/archive-copies.js +179 -0
- package/dist/src/audit.d.ts +1 -1
- package/dist/src/audit.js +29 -4
- package/dist/src/autopilot.d.ts +52 -7
- package/dist/src/autopilot.js +143 -25
- package/dist/src/background-bandwidth.d.ts +46 -0
- package/dist/src/background-bandwidth.js +301 -0
- package/dist/src/background-cpu.d.ts +82 -0
- package/dist/src/background-cpu.js +212 -0
- package/dist/src/background-worker-error.d.ts +12 -0
- package/dist/src/background-worker-error.js +18 -0
- package/dist/src/branding.d.ts +24 -1
- package/dist/src/branding.js +31 -1
- package/dist/src/bridge.d.ts +233 -0
- package/dist/src/bridge.js +604 -0
- package/dist/src/byte-stream.d.ts +91 -0
- package/dist/src/byte-stream.js +385 -0
- package/dist/src/chunk-store.d.ts +41 -8
- package/dist/src/chunk-store.js +161 -65
- package/dist/src/cli.js +1746 -166
- package/dist/src/cloud.d.ts +841 -31
- package/dist/src/cloud.js +3196 -277
- package/dist/src/context-background.d.ts +37 -0
- package/dist/src/context-background.js +309 -0
- package/dist/src/context-drain-child.d.ts +1 -0
- package/dist/src/context-drain-child.js +98 -0
- package/dist/src/context-reader.d.ts +118 -0
- package/dist/src/context-reader.js +447 -0
- package/dist/src/control-plane/auth.d.ts +32 -4
- package/dist/src/control-plane/auth.js +85 -24
- package/dist/src/control-plane/server.js +19 -6
- package/dist/src/control-plane.d.ts +17 -1
- package/dist/src/control-plane.js +32 -6
- package/dist/src/crypto.d.ts +1 -1
- package/dist/src/crypto.js +5 -5
- package/dist/src/daemon-lease.d.ts +70 -0
- package/dist/src/daemon-lease.js +420 -0
- package/dist/src/daemon.d.ts +94 -1
- package/dist/src/daemon.js +1082 -105
- package/dist/src/darwin-service-policy.d.ts +41 -0
- package/dist/src/darwin-service-policy.js +60 -0
- package/dist/src/dashboard-cli.js +15 -15
- package/dist/src/device-authorization.d.ts +37 -0
- package/dist/src/device-authorization.js +199 -0
- package/dist/src/device-enrollment.d.ts +91 -0
- package/dist/src/device-enrollment.js +349 -0
- package/dist/src/disk.d.ts +17 -12
- package/dist/src/disk.js +43 -17
- package/dist/src/doctor.d.ts +35 -1
- package/dist/src/doctor.js +316 -41
- package/dist/src/durable-ticket-lock.d.ts +24 -0
- package/dist/src/durable-ticket-lock.js +232 -0
- package/dist/src/enroll.d.ts +1 -1
- package/dist/src/enroll.js +13 -7
- package/dist/src/env.d.ts +10 -1
- package/dist/src/env.js +11 -3
- package/dist/src/errors.d.ts +8 -8
- package/dist/src/errors.js +6 -6
- package/dist/src/flush.d.ts +12 -0
- package/dist/src/flush.js +37 -0
- package/dist/src/heartbeat.d.ts +86 -12
- package/dist/src/heartbeat.js +415 -29
- package/dist/src/index-background-watchdog.d.ts +1 -0
- package/dist/src/index-background-watchdog.js +94 -0
- package/dist/src/index-background-work.d.ts +21 -0
- package/dist/src/index-background-work.js +25 -0
- package/dist/src/index-background.d.ts +64 -0
- package/dist/src/index-background.js +394 -0
- package/dist/src/index-build-child.d.ts +1 -0
- package/dist/src/index-build-child.js +109 -0
- package/dist/src/index-manifest.d.ts +52 -0
- package/dist/src/index-manifest.js +444 -0
- package/dist/src/index-publication-proof.d.ts +84 -0
- package/dist/src/index-publication-proof.js +380 -0
- package/dist/src/index-publication-state.d.ts +149 -0
- package/dist/src/index-publication-state.js +696 -0
- package/dist/src/index-publication-verifier.d.ts +89 -0
- package/dist/src/index-publication-verifier.js +341 -0
- package/dist/src/index-publish.d.ts +62 -0
- package/dist/src/index-publish.js +540 -0
- package/dist/src/index-scratch-cleanup.d.ts +19 -0
- package/dist/src/index-scratch-cleanup.js +166 -0
- package/dist/src/index-segment-types.d.ts +132 -0
- package/dist/src/index-segment-types.js +21 -0
- package/dist/src/index-segments.d.ts +9 -0
- package/dist/src/index-segments.js +516 -0
- package/dist/src/index-store.d.ts +123 -0
- package/dist/src/index-store.js +495 -0
- package/dist/src/index-sync.d.ts +91 -15
- package/dist/src/index-sync.js +286 -53
- package/dist/src/index-upgrade-publication.d.ts +30 -0
- package/dist/src/index-upgrade-publication.js +179 -0
- package/dist/src/integration-manager.d.ts +32 -0
- package/dist/src/integration-manager.js +394 -0
- package/dist/src/leakscan.js +1 -1
- package/dist/src/local-api.d.ts +56 -15
- package/dist/src/local-api.js +4979 -541
- package/dist/src/machine-settings.d.ts +51 -0
- package/dist/src/machine-settings.js +166 -0
- package/dist/src/managed-chunks.d.ts +5 -2
- package/dist/src/managed-chunks.js +14 -14
- package/dist/src/mcp-install.d.ts +11 -9
- package/dist/src/mcp-install.js +73 -28
- package/dist/src/mcp-workspace.d.ts +18 -0
- package/dist/src/mcp-workspace.js +50 -0
- package/dist/src/mcp.js +294 -25
- package/dist/src/migrate.d.ts +1 -0
- package/dist/src/migrate.js +33 -5
- package/dist/src/notify.d.ts +1 -1
- package/dist/src/notify.js +5 -5
- package/dist/src/offload.d.ts +201 -14
- package/dist/src/offload.js +1848 -140
- package/dist/src/onboarding.d.ts +8 -1
- package/dist/src/onboarding.js +4 -4
- package/dist/src/packages.d.ts +2 -2
- package/dist/src/packages.js +10 -2
- package/dist/src/passkey.d.ts +0 -1
- package/dist/src/passkey.js +2 -7
- package/dist/src/password-lock.d.ts +2 -2
- package/dist/src/password-lock.js +6 -6
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/presence.d.ts +86 -0
- package/dist/src/presence.js +240 -0
- package/dist/src/progress-deadline.d.ts +21 -0
- package/dist/src/progress-deadline.js +91 -0
- package/dist/src/project-repair.d.ts +55 -0
- package/dist/src/project-repair.js +131 -0
- package/dist/src/providers/gcs.d.ts +28 -7
- package/dist/src/providers/gcs.js +35 -24
- package/dist/src/providers/gdrive.d.ts +71 -8
- package/dist/src/providers/gdrive.js +223 -50
- package/dist/src/providers/index.d.ts +11 -3
- package/dist/src/providers/index.js +30 -10
- package/dist/src/providers/s3.d.ts +30 -8
- package/dist/src/providers/s3.js +41 -30
- package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
- package/dist/src/providers/safe-storage-fetch.js +72 -0
- package/dist/src/queue.d.ts +136 -19
- package/dist/src/queue.js +862 -96
- package/dist/src/reclaim-transaction.d.ts +156 -0
- package/dist/src/reclaim-transaction.js +1027 -0
- package/dist/src/recovery-codes.d.ts +32 -0
- package/dist/src/recovery-codes.js +338 -0
- package/dist/src/recovery.js +12 -9
- package/dist/src/rehydrate.d.ts +25 -22
- package/dist/src/rehydrate.js +319 -23
- package/dist/src/restore.d.ts +57 -4
- package/dist/src/restore.js +272 -36
- package/dist/src/resume-stub.d.ts +92 -0
- package/dist/src/resume-stub.js +417 -0
- package/dist/src/retention.d.ts +98 -7
- package/dist/src/retention.js +1066 -62
- package/dist/src/rotate.js +3 -3
- package/dist/src/search.d.ts +566 -8
- package/dist/src/search.js +5897 -290
- package/dist/src/secrets.d.ts +51 -7
- package/dist/src/secrets.js +316 -24
- package/dist/src/service.d.ts +49 -11
- package/dist/src/service.js +776 -35
- package/dist/src/share.js +3 -3
- package/dist/src/shared-spaces.d.ts +98 -0
- package/dist/src/shared-spaces.js +214 -0
- package/dist/src/source-reader.d.ts +73 -0
- package/dist/src/source-reader.js +715 -0
- package/dist/src/spool.d.ts +1 -1
- package/dist/src/spool.js +1 -1
- package/dist/src/start-tui.js +2 -1
- package/dist/src/start.js +2 -2
- package/dist/src/storage-endpoint.d.ts +21 -0
- package/dist/src/storage-endpoint.js +122 -0
- package/dist/src/storage-setup.js +12 -12
- package/dist/src/storage-targets.d.ts +109 -6
- package/dist/src/storage-targets.js +975 -67
- package/dist/src/stream-to-cloud.d.ts +5 -1
- package/dist/src/stream-to-cloud.js +34 -14
- package/dist/src/sync-rules.d.ts +31 -6
- package/dist/src/sync-rules.js +153 -14
- package/dist/src/team-backfill-scheduling.d.ts +8 -0
- package/dist/src/team-backfill-scheduling.js +33 -0
- package/dist/src/team-backfill.d.ts +116 -0
- package/dist/src/team-backfill.js +1429 -0
- package/dist/src/team-index-cache.d.ts +16 -0
- package/dist/src/team-index-cache.js +152 -0
- package/dist/src/team-offboarding.d.ts +38 -0
- package/dist/src/team-offboarding.js +1043 -0
- package/dist/src/team-presence.d.ts +127 -0
- package/dist/src/team-presence.js +904 -0
- package/dist/src/team-publication-policy.d.ts +20 -0
- package/dist/src/team-publication-policy.js +140 -0
- package/dist/src/team-realtime.d.ts +68 -0
- package/dist/src/team-realtime.js +816 -0
- package/dist/src/team-source-facts-cache.d.ts +23 -0
- package/dist/src/team-source-facts-cache.js +255 -0
- package/dist/src/trash.d.ts +1 -1
- package/dist/src/trash.js +2 -2
- package/dist/src/tui.js +11 -12
- package/dist/src/types.d.ts +173 -7
- package/dist/src/types.js +20 -0
- package/dist/src/ui-server.d.ts +163 -35
- package/dist/src/ui-server.js +712 -72
- package/dist/src/ui.d.ts +1 -2
- package/dist/src/ui.js +1 -2
- package/dist/src/upload.d.ts +27 -0
- package/dist/src/upload.js +383 -43
- package/dist/src/vault.d.ts +235 -29
- package/dist/src/vault.js +1783 -190
- package/dist/src/watcher.d.ts +7 -1
- package/dist/src/watcher.js +198 -55
- package/dist/src/worker.d.ts +27 -3
- package/dist/src/worker.js +274 -55
- package/package.json +33 -12
- package/scripts/native-reboot-rehearsal.mjs +90 -0
- package/web/app.js +6032 -343
- package/web/bootstrap.js +17 -0
- package/web/index.html +255 -57
- package/web/rail.js +317 -40
- package/web/retention.html +2 -2
- package/web/rules-view.js +188 -16
- package/web/sessions-view.js +485 -62
- package/web/sessions.html +2 -2
- package/web/setup-api.js +152 -29
- package/web/setup-logic.js +68 -9
- package/web/setup.html +113 -44
- package/web/setup.js +604 -71
- package/web/style.css +513 -98
- package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
type PolicyExec = (file: string, args: string[]) => Promise<unknown>;
|
|
2
|
+
export type DarwinServicePolicyOptions = {
|
|
3
|
+
/** True only for the daemon child of Sealkeep's package-independent manager. */
|
|
4
|
+
serviceOwned: boolean;
|
|
5
|
+
/** Dependency seams keep non-Darwin and live-process tests hermetic. */
|
|
6
|
+
platform?: NodeJS.Platform;
|
|
7
|
+
pid?: number;
|
|
8
|
+
exec?: PolicyExec;
|
|
9
|
+
};
|
|
10
|
+
export type LegacyDarwinManagerHandoffOptions = {
|
|
11
|
+
serviceOwned: boolean;
|
|
12
|
+
platform?: NodeJS.Platform;
|
|
13
|
+
environment?: NodeJS.ProcessEnv;
|
|
14
|
+
/** Rewrites the exact already-owned manager, receipt, and native unit. */
|
|
15
|
+
reconcile: () => Promise<boolean>;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Prepare the one upgrade generation still launched by a pre-policy manager.
|
|
19
|
+
*
|
|
20
|
+
* Returning true means durable reconciliation completed and the caller must
|
|
21
|
+
* exit 75 before acquiring a UI or worker lease. The old manager already
|
|
22
|
+
* propagates that non-zero child result, so launchd starts the newly written
|
|
23
|
+
* manager, whose generation marker makes this gate a one-shot operation.
|
|
24
|
+
*/
|
|
25
|
+
export declare function prepareLegacyDarwinManagerHandoff(options: LegacyDarwinManagerHandoffOptions): Promise<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Remove the legacy launchd `ProcessType=Background` runtime policy from the
|
|
28
|
+
* already-running daemon without restarting it.
|
|
29
|
+
*
|
|
30
|
+
* Older plist generations marked the whole service as background even though
|
|
31
|
+
* that process also owns the user-facing loopback UI. Reconciliation writes
|
|
32
|
+
* the corrected plist for future launches, but launchd does not apply a plist
|
|
33
|
+
* edit to a process it already started. `taskpolicy -B` changes only Darwin's
|
|
34
|
+
* PRIO_DARWIN_BG flag for this exact process; it does not change the separate
|
|
35
|
+
* Nice=5 or LowPriorityIO settings which keep archive work deferential.
|
|
36
|
+
*
|
|
37
|
+
* This is deliberately best-effort. A missing or restricted taskpolicy must
|
|
38
|
+
* never prevent memory preservation or the recovery/settings UI from opening.
|
|
39
|
+
*/
|
|
40
|
+
export declare function normalizeDarwinServicePolicy(options: DarwinServicePolicyOptions): Promise<boolean>;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { INTEGRATION_MANAGER_GENERATION, INTEGRATION_MANAGER_GENERATION_ENV, } from "./integration-manager.js";
|
|
5
|
+
const run = promisify(execFile);
|
|
6
|
+
/**
|
|
7
|
+
* Prepare the one upgrade generation still launched by a pre-policy manager.
|
|
8
|
+
*
|
|
9
|
+
* Returning true means durable reconciliation completed and the caller must
|
|
10
|
+
* exit 75 before acquiring a UI or worker lease. The old manager already
|
|
11
|
+
* propagates that non-zero child result, so launchd starts the newly written
|
|
12
|
+
* manager, whose generation marker makes this gate a one-shot operation.
|
|
13
|
+
*/
|
|
14
|
+
export async function prepareLegacyDarwinManagerHandoff(options) {
|
|
15
|
+
const environment = options.environment ?? process.env;
|
|
16
|
+
if (!options.serviceOwned
|
|
17
|
+
|| (options.platform ?? platform()) !== "darwin"
|
|
18
|
+
|| environment[INTEGRATION_MANAGER_GENERATION_ENV] === INTEGRATION_MANAGER_GENERATION)
|
|
19
|
+
return false;
|
|
20
|
+
try {
|
|
21
|
+
return await options.reconcile();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// The runtime normalization below is still a safe fallback. Never turn a
|
|
25
|
+
// service-definition inspection failure into an unavailable local UI.
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Remove the legacy launchd `ProcessType=Background` runtime policy from the
|
|
31
|
+
* already-running daemon without restarting it.
|
|
32
|
+
*
|
|
33
|
+
* Older plist generations marked the whole service as background even though
|
|
34
|
+
* that process also owns the user-facing loopback UI. Reconciliation writes
|
|
35
|
+
* the corrected plist for future launches, but launchd does not apply a plist
|
|
36
|
+
* edit to a process it already started. `taskpolicy -B` changes only Darwin's
|
|
37
|
+
* PRIO_DARWIN_BG flag for this exact process; it does not change the separate
|
|
38
|
+
* Nice=5 or LowPriorityIO settings which keep archive work deferential.
|
|
39
|
+
*
|
|
40
|
+
* This is deliberately best-effort. A missing or restricted taskpolicy must
|
|
41
|
+
* never prevent memory preservation or the recovery/settings UI from opening.
|
|
42
|
+
*/
|
|
43
|
+
export async function normalizeDarwinServicePolicy(options) {
|
|
44
|
+
if (!options.serviceOwned || (options.platform ?? platform()) !== "darwin")
|
|
45
|
+
return false;
|
|
46
|
+
const pid = options.pid ?? process.pid;
|
|
47
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
48
|
+
return false;
|
|
49
|
+
const exec = options.exec ?? ((file, args) => run(file, args, {
|
|
50
|
+
timeout: 2_000,
|
|
51
|
+
killSignal: "SIGTERM",
|
|
52
|
+
}));
|
|
53
|
+
try {
|
|
54
|
+
await exec("/usr/sbin/taskpolicy", ["-B", "-p", String(pid)]);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// `npm run dashboard` is a thin alias for the local
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// `npm run dashboard` is a thin alias for the singleton local UI. Keeping one
|
|
3
|
+
// owner per vault means the development shortcut cannot create a second process
|
|
4
|
+
// that races the installed service's upload/reclaim journals.
|
|
5
5
|
import { defaultDataDir } from "./vault.js";
|
|
6
|
-
import {
|
|
7
|
-
import { freePort } from "./net.js";
|
|
6
|
+
import { localApiTokenPath } from "./local-api.js";
|
|
8
7
|
import { envVar } from "./env.js";
|
|
8
|
+
import { startUi } from "./ui-server.js";
|
|
9
9
|
const dataDir = envVar("DATA_DIR") ?? defaultDataDir();
|
|
10
10
|
async function listen() {
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
console.log(`Sealkeep dashboard: http://127.0.0.1:${port}/#token=${token}`);
|
|
18
|
-
console.log(`Token file (do not share): ${localApiTokenPath(dataDir)}`);
|
|
19
|
-
});
|
|
11
|
+
const requestedPort = Number(process.env.PORT ?? "4173");
|
|
12
|
+
const ui = await startUi(dataDir, { port: requestedPort, openBrowser: false, owner: "interactive" });
|
|
13
|
+
console.log(`Sealkeep dashboard: ${ui.url}`);
|
|
14
|
+
console.log(`Token file (do not share): ${localApiTokenPath(dataDir)}`);
|
|
15
|
+
if (ui.reused)
|
|
16
|
+
console.log("Using the Sealkeep local UI that already owns this vault.");
|
|
20
17
|
}
|
|
21
|
-
void listen()
|
|
18
|
+
void listen().catch((error) => {
|
|
19
|
+
console.error(`Unable to start dashboard: ${error instanceof Error ? error.message : String(error)}`);
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type CloudAccount, type CloudDeviceAuthorizationMemory } from "./cloud.js";
|
|
2
|
+
import { type DeviceEnrollmentDraft } from "./device-enrollment.js";
|
|
3
|
+
import { type BackendName } from "./secrets.js";
|
|
4
|
+
export type DeviceOsFamily = "macos" | "linux" | "windows";
|
|
5
|
+
export type DeviceAuthorizationStart = {
|
|
6
|
+
requestId: string;
|
|
7
|
+
approvalUrl: string;
|
|
8
|
+
expiresAt: string;
|
|
9
|
+
pollAfterMs: number;
|
|
10
|
+
status: "waiting_for_approval";
|
|
11
|
+
};
|
|
12
|
+
/** The browser needs a comparison code and timing, never even the target's public key. */
|
|
13
|
+
type PublicEnrollment = Pick<DeviceEnrollmentDraft, "requestId" | "label" | "verificationCode" | "createdAt" | "expiresAt"> & {
|
|
14
|
+
status: "waiting_for_approval";
|
|
15
|
+
};
|
|
16
|
+
export type DeviceAuthorizationCompletion = {
|
|
17
|
+
status: "waiting_for_approval" | "denied" | "expired" | "complete";
|
|
18
|
+
expiresAt?: string;
|
|
19
|
+
retryAfterMs?: number;
|
|
20
|
+
account?: {
|
|
21
|
+
plan: CloudAccount["plan"];
|
|
22
|
+
email: string;
|
|
23
|
+
quotaBytes: number;
|
|
24
|
+
usedBytes: number;
|
|
25
|
+
};
|
|
26
|
+
memory?: CloudDeviceAuthorizationMemory;
|
|
27
|
+
enrollment?: PublicEnrollment;
|
|
28
|
+
};
|
|
29
|
+
export declare function currentOsFamily(value?: NodeJS.Platform): DeviceOsFamily;
|
|
30
|
+
export declare function publicDetectedAgents(values: readonly string[]): string[];
|
|
31
|
+
export declare function beginDeviceAuthorization(dataDir: string, input: {
|
|
32
|
+
label: string;
|
|
33
|
+
osFamily?: DeviceOsFamily;
|
|
34
|
+
agents?: string[];
|
|
35
|
+
}, preferred?: BackendName): Promise<DeviceAuthorizationStart>;
|
|
36
|
+
export declare function completeDeviceAuthorization(dataDir: string, requestId: string, preferred?: BackendName): Promise<DeviceAuthorizationCompletion>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { platform } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { claimCloudDeviceAuthorization, cloudUrl, rememberCloudDeviceAuthorization, startCloudDeviceAuthorization, } from "./cloud.js";
|
|
5
|
+
import { generateDeviceEnrollmentKeys, rememberHostedDeviceEnrollment, } from "./device-enrollment.js";
|
|
6
|
+
import { fail } from "./errors.js";
|
|
7
|
+
import { chooseBackend } from "./secrets.js";
|
|
8
|
+
const STATE_SERVICE = "sealkeep-device-authorization";
|
|
9
|
+
const REQUEST_ID = /^[A-Za-z0-9._~-]{1,200}$/;
|
|
10
|
+
const SAFE_AGENT_IDS = new Set(["codex", "claude", "cursor", "gemini", "windsurf", "antigravity"]);
|
|
11
|
+
function publicEnrollment(draft) {
|
|
12
|
+
return {
|
|
13
|
+
requestId: draft.requestId,
|
|
14
|
+
label: draft.label,
|
|
15
|
+
verificationCode: draft.verificationCode,
|
|
16
|
+
createdAt: draft.createdAt,
|
|
17
|
+
expiresAt: draft.expiresAt,
|
|
18
|
+
status: "waiting_for_approval",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function currentOsFamily(value = platform()) {
|
|
22
|
+
if (value === "darwin")
|
|
23
|
+
return "macos";
|
|
24
|
+
if (value === "win32")
|
|
25
|
+
return "windows";
|
|
26
|
+
return "linux";
|
|
27
|
+
}
|
|
28
|
+
export function publicDetectedAgents(values) {
|
|
29
|
+
return [...new Set(values.filter((value) => SAFE_AGENT_IDS.has(value)))].sort();
|
|
30
|
+
}
|
|
31
|
+
function stateRef(dataDir) {
|
|
32
|
+
const scope = createHash("sha256").update(resolve(dataDir)).digest("hex");
|
|
33
|
+
return { service: STATE_SERVICE, account: `${scope}@${cloudUrl()}` };
|
|
34
|
+
}
|
|
35
|
+
const s256 = (verifier) => createHash("sha256").update(verifier).digest("base64url");
|
|
36
|
+
function normalPoll(value) {
|
|
37
|
+
const number = Number(value);
|
|
38
|
+
return Number.isFinite(number) ? Math.min(10_000, Math.max(1_000, Math.round(number))) : 3_000;
|
|
39
|
+
}
|
|
40
|
+
function assertApprovalUrl(value) {
|
|
41
|
+
if (typeof value !== "string")
|
|
42
|
+
return fail("internal", "Sealkeep Cloud did not return an account approval page");
|
|
43
|
+
let url;
|
|
44
|
+
try {
|
|
45
|
+
url = new URL(value);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return fail("internal", "Sealkeep Cloud returned an invalid account approval page");
|
|
49
|
+
}
|
|
50
|
+
if (url.protocol !== "https:" || url.username || url.password) {
|
|
51
|
+
return fail("internal", "Sealkeep Cloud returned an unsafe account approval page");
|
|
52
|
+
}
|
|
53
|
+
// The opaque request id may identify the browser approval, but credentials,
|
|
54
|
+
// verifier material and one-time codes never belong in a browser URL.
|
|
55
|
+
if (/(?:token|secret|verifier|machine_key|account_token|pairing_code)=/i.test(`${url.search}&${url.hash}`)) {
|
|
56
|
+
return fail("internal", "Sealkeep Cloud put secret material in the account approval URL");
|
|
57
|
+
}
|
|
58
|
+
return url.toString();
|
|
59
|
+
}
|
|
60
|
+
async function rememberDraft(dataDir, draft, preferred) {
|
|
61
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
62
|
+
await backend.set(stateRef(dataDir), JSON.stringify(draft));
|
|
63
|
+
}
|
|
64
|
+
async function readDraft(dataDir, preferred) {
|
|
65
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
66
|
+
const raw = await backend.get(stateRef(dataDir));
|
|
67
|
+
if (!raw)
|
|
68
|
+
return null;
|
|
69
|
+
try {
|
|
70
|
+
const value = JSON.parse(raw);
|
|
71
|
+
if (value.version !== 1 || typeof value.requestId !== "string" || !REQUEST_ID.test(value.requestId))
|
|
72
|
+
return null;
|
|
73
|
+
if (typeof value.verifier !== "string" || value.verifier.length < 32)
|
|
74
|
+
return null;
|
|
75
|
+
if (!value.keys || typeof value.keys.publicKey !== "string" || typeof value.keys.privateKey !== "string")
|
|
76
|
+
return null;
|
|
77
|
+
if (typeof value.label !== "string" || !value.label.trim())
|
|
78
|
+
return null;
|
|
79
|
+
if (typeof value.expiresAt !== "string" || !Number.isFinite(Date.parse(value.expiresAt)))
|
|
80
|
+
return null;
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function forgetDraft(dataDir, preferred) {
|
|
88
|
+
const backend = await chooseBackend(dataDir, preferred);
|
|
89
|
+
await backend.delete(stateRef(dataDir)).catch(() => undefined);
|
|
90
|
+
}
|
|
91
|
+
export async function beginDeviceAuthorization(dataDir, input, preferred) {
|
|
92
|
+
const label = input.label.trim();
|
|
93
|
+
if (!label || label.length > 80)
|
|
94
|
+
fail("invalid_argument", "Name this machine with 1 to 80 characters");
|
|
95
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
96
|
+
const keys = generateDeviceEnrollmentKeys();
|
|
97
|
+
const created = await startCloudDeviceAuthorization({
|
|
98
|
+
challenge: s256(verifier),
|
|
99
|
+
label,
|
|
100
|
+
osFamily: input.osFamily ?? currentOsFamily(),
|
|
101
|
+
agents: publicDetectedAgents(input.agents ?? []),
|
|
102
|
+
publicKey: keys.publicKey,
|
|
103
|
+
});
|
|
104
|
+
const requestId = String(created?.request_id ?? "");
|
|
105
|
+
if (!REQUEST_ID.test(requestId))
|
|
106
|
+
fail("internal", "Sealkeep Cloud returned an invalid device authorization request");
|
|
107
|
+
const approvalUrl = assertApprovalUrl(created.approval_url);
|
|
108
|
+
const expiresAt = String(created.expires_at ?? "");
|
|
109
|
+
if (!Number.isFinite(Date.parse(expiresAt)) || Date.parse(expiresAt) <= Date.now()) {
|
|
110
|
+
fail("internal", "Sealkeep Cloud returned an expired device authorization request");
|
|
111
|
+
}
|
|
112
|
+
const draft = {
|
|
113
|
+
version: 1,
|
|
114
|
+
requestId,
|
|
115
|
+
verifier,
|
|
116
|
+
label,
|
|
117
|
+
keys,
|
|
118
|
+
createdAt: created.created_at && Number.isFinite(Date.parse(created.created_at)) ? created.created_at : new Date().toISOString(),
|
|
119
|
+
expiresAt,
|
|
120
|
+
pollAfterMs: normalPoll(created.poll_after_ms),
|
|
121
|
+
};
|
|
122
|
+
try {
|
|
123
|
+
await rememberDraft(dataDir, draft, preferred);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
fail("internal", `This machine could not protect its authorization request locally: ${error instanceof Error ? error.message : "secret backend refused it"}`);
|
|
127
|
+
}
|
|
128
|
+
return { requestId, approvalUrl, expiresAt, pollAfterMs: draft.pollAfterMs, status: "waiting_for_approval" };
|
|
129
|
+
}
|
|
130
|
+
function publicAccount(account) {
|
|
131
|
+
return {
|
|
132
|
+
plan: account.plan,
|
|
133
|
+
email: account.email,
|
|
134
|
+
quotaBytes: account.quota_bytes,
|
|
135
|
+
usedBytes: account.used_bytes,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export async function completeDeviceAuthorization(dataDir, requestId, preferred) {
|
|
139
|
+
if (!REQUEST_ID.test(requestId))
|
|
140
|
+
fail("invalid_argument", "That device authorization request is not valid");
|
|
141
|
+
const draft = await readDraft(dataDir, preferred);
|
|
142
|
+
if (!draft || draft.requestId !== requestId) {
|
|
143
|
+
fail("archive_not_found", "This machine no longer holds that account authorization request. Start a new one.");
|
|
144
|
+
}
|
|
145
|
+
if (Date.parse(draft.expiresAt) <= Date.now()) {
|
|
146
|
+
await forgetDraft(dataDir, preferred);
|
|
147
|
+
return { status: "expired", expiresAt: draft.expiresAt };
|
|
148
|
+
}
|
|
149
|
+
const claimed = await claimCloudDeviceAuthorization({ requestId, verifier: draft.verifier });
|
|
150
|
+
if (claimed.status === "pending") {
|
|
151
|
+
return {
|
|
152
|
+
status: "waiting_for_approval",
|
|
153
|
+
expiresAt: claimed.expires_at ?? draft.expiresAt,
|
|
154
|
+
retryAfterMs: normalPoll(claimed.poll_after_ms ?? draft.pollAfterMs),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (claimed.status === "denied" || claimed.status === "declined" || claimed.status === "expired") {
|
|
158
|
+
await forgetDraft(dataDir, preferred);
|
|
159
|
+
return {
|
|
160
|
+
// Older hosted requests could still answer `declined`; the loopback
|
|
161
|
+
// contract intentionally has one public denial state.
|
|
162
|
+
status: claimed.status === "expired" ? "expired" : "denied",
|
|
163
|
+
expiresAt: claimed.expires_at ?? draft.expiresAt,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const accountToken = claimed.account_token ?? claimed.token;
|
|
167
|
+
if (claimed.status !== "approved" || typeof accountToken !== "string" || !accountToken || !claimed.machine || !claimed.account || !claimed.memory) {
|
|
168
|
+
fail("internal", "Sealkeep Cloud returned an incomplete device authorization");
|
|
169
|
+
}
|
|
170
|
+
if (!Number.isSafeInteger(claimed.machine.device_id) || claimed.machine.device_id < 1 || !claimed.machine.machine_key) {
|
|
171
|
+
fail("internal", "Sealkeep Cloud returned an invalid machine credential");
|
|
172
|
+
}
|
|
173
|
+
if (typeof claimed.memory.preserved !== "boolean" || !Number.isFinite(claimed.memory.bytes) || claimed.memory.bytes < 0) {
|
|
174
|
+
fail("internal", "Sealkeep Cloud returned an invalid preserved-memory summary");
|
|
175
|
+
}
|
|
176
|
+
await rememberCloudDeviceAuthorization(dataDir, {
|
|
177
|
+
accountToken,
|
|
178
|
+
machine: claimed.machine,
|
|
179
|
+
}, preferred);
|
|
180
|
+
let enrollment;
|
|
181
|
+
if (claimed.memory.preserved && claimed.enrollment) {
|
|
182
|
+
const started = await rememberHostedDeviceEnrollment(dataDir, {
|
|
183
|
+
requestId: claimed.enrollment.request_id,
|
|
184
|
+
requestSecret: claimed.enrollment.request_secret,
|
|
185
|
+
label: draft.label,
|
|
186
|
+
createdAt: claimed.enrollment.created_at,
|
|
187
|
+
expiresAt: claimed.enrollment.expires_at,
|
|
188
|
+
keys: draft.keys,
|
|
189
|
+
}, preferred);
|
|
190
|
+
enrollment = publicEnrollment(started);
|
|
191
|
+
}
|
|
192
|
+
await forgetDraft(dataDir, preferred);
|
|
193
|
+
return {
|
|
194
|
+
status: "complete",
|
|
195
|
+
account: publicAccount(claimed.account),
|
|
196
|
+
memory: claimed.memory,
|
|
197
|
+
...(enrollment ? { enrollment } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type Envelope } from "../packages/sealkeep-crypto/src/index.js";
|
|
2
|
+
import { type CloudDeviceEnrollmentRequest } from "./cloud.js";
|
|
3
|
+
import { type BackendName } from "./secrets.js";
|
|
4
|
+
import type { VaultConfig } from "./types.js";
|
|
5
|
+
export type DeviceEnrollmentDraft = {
|
|
6
|
+
version: 1;
|
|
7
|
+
requestId: number;
|
|
8
|
+
label: string;
|
|
9
|
+
publicKey: string;
|
|
10
|
+
privateKey: string;
|
|
11
|
+
requestSecret: string;
|
|
12
|
+
verificationCode: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
expiresAt: string;
|
|
15
|
+
};
|
|
16
|
+
export type PersonalVaultMaterial = {
|
|
17
|
+
vaultId: string;
|
|
18
|
+
recoveryPhrase: string;
|
|
19
|
+
storageMode: "sealed" | "plain";
|
|
20
|
+
recipients?: VaultConfig["recipients"];
|
|
21
|
+
remoteNaming?: VaultConfig["remoteNaming"];
|
|
22
|
+
};
|
|
23
|
+
export type DeviceEnrollmentCompletion = {
|
|
24
|
+
requestId: number;
|
|
25
|
+
vaultId: string;
|
|
26
|
+
verificationCode: string;
|
|
27
|
+
cloudReceiptCleared: boolean;
|
|
28
|
+
};
|
|
29
|
+
/** A short comparison code derived from the key itself, never supplied by Cloud. */
|
|
30
|
+
export declare function deviceEnrollmentVerificationCode(publicKey: string): string;
|
|
31
|
+
export type DeviceEnrollmentKeyPair = {
|
|
32
|
+
publicKey: string;
|
|
33
|
+
privateKey: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function generateDeviceEnrollmentKeys(): DeviceEnrollmentKeyPair;
|
|
36
|
+
export declare function readDeviceEnrollmentDraft(dataDir: string, requestId: number, preferred?: BackendName): Promise<DeviceEnrollmentDraft | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Starts the target side after ordinary Cloud sign-in. The request secret is a
|
|
39
|
+
* capability for claiming this one response; it is not a vault key and cannot
|
|
40
|
+
* decrypt any archive.
|
|
41
|
+
*/
|
|
42
|
+
export declare function beginDeviceEnrollment(dataDir: string, label: string, preferred?: BackendName): Promise<Omit<DeviceEnrollmentDraft, "privateKey" | "requestSecret">>;
|
|
43
|
+
/**
|
|
44
|
+
* Starts the existing-vault approval lane with a key pair prepared before
|
|
45
|
+
* hosted account authorization. Reusing that exact public key means the
|
|
46
|
+
* account page and the existing device approve the same target, instead of
|
|
47
|
+
* silently changing identity between the two steps.
|
|
48
|
+
*/
|
|
49
|
+
export declare function beginDeviceEnrollmentWithKeys(dataDir: string, label: string, keys: DeviceEnrollmentKeyPair, preferred?: BackendName): Promise<Omit<DeviceEnrollmentDraft, "privateKey" | "requestSecret">>;
|
|
50
|
+
/**
|
|
51
|
+
* Persists a request the hosted approval endpoint already created from the
|
|
52
|
+
* public key supplied at authorization start. The returned request secret is
|
|
53
|
+
* consumed only here, over the verifier-protected claim response, and then
|
|
54
|
+
* remains in the native secret backend with the private key.
|
|
55
|
+
*/
|
|
56
|
+
export declare function rememberHostedDeviceEnrollment(dataDir: string, input: {
|
|
57
|
+
requestId: number;
|
|
58
|
+
requestSecret: string;
|
|
59
|
+
label: string;
|
|
60
|
+
createdAt: string;
|
|
61
|
+
expiresAt: string;
|
|
62
|
+
keys: DeviceEnrollmentKeyPair;
|
|
63
|
+
}, preferred?: BackendName): Promise<Omit<DeviceEnrollmentDraft, "privateKey" | "requestSecret">>;
|
|
64
|
+
export declare function sealDeviceEnrollmentPayload(config: VaultConfig, phrase: string, targetPublicKey: string): {
|
|
65
|
+
envelope: Envelope;
|
|
66
|
+
sealed: string;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Existing-device side. Requiring the independently displayed code prevents a
|
|
70
|
+
* Cloud-side public-key substitution from being approved silently.
|
|
71
|
+
*/
|
|
72
|
+
export declare function approveDeviceEnrollment(dataDir: string, requestId: number, displayedCode: string, preferred?: BackendName): Promise<{
|
|
73
|
+
approved: true;
|
|
74
|
+
requestId: number;
|
|
75
|
+
label: string;
|
|
76
|
+
}>;
|
|
77
|
+
/**
|
|
78
|
+
* Read-only adoption gate. Recovery codes call this before atomically spending
|
|
79
|
+
* a code, so an occupied data directory or a lost Cloud login cannot burn a
|
|
80
|
+
* valid code and then fail locally.
|
|
81
|
+
*/
|
|
82
|
+
export declare function assertPersonalVaultAdoptable(dataDir: string, payload: PersonalVaultMaterial, preferred?: BackendName): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Checks everything a fresh recovery target can check before a one-time code
|
|
85
|
+
* is redeemed. This deliberately does not need the encrypted vault payload.
|
|
86
|
+
*/
|
|
87
|
+
export declare function assertFreshPersonalVaultTarget(dataDir: string, preferred?: BackendName): Promise<VaultConfig | null>;
|
|
88
|
+
export declare function adoptPersonalVaultMaterial(dataDir: string, payload: PersonalVaultMaterial, preferred?: BackendName): Promise<void>;
|
|
89
|
+
/** Target side: claim ciphertext, decrypt locally, adopt, then acknowledge. */
|
|
90
|
+
export declare function completeDeviceEnrollment(dataDir: string, requestId: number, preferred?: BackendName): Promise<DeviceEnrollmentCompletion>;
|
|
91
|
+
export type { CloudDeviceEnrollmentRequest };
|