sealkeep 0.9.0 → 0.11.1
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 +252 -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.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +53 -13
- 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 +1698 -163
- 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 +332 -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.js +27 -21
- 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 +5940 -297
- 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 +226 -30
- package/dist/src/vault.js +1776 -192
- 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/sha256-stream.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ContentIndex } from "./search.js";
|
|
2
|
+
import type { TeamIndexCacheIdentity } from "./team-index-cache.js";
|
|
3
|
+
/** Derived facts, never authority: callers still fetch the current native head and grants. */
|
|
4
|
+
export type TeamSourceFactsCacheIdentity = TeamIndexCacheIdentity & {
|
|
5
|
+
project: string;
|
|
6
|
+
projectKey?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare const TEAM_SOURCE_FACTS_MAX_BYTES: number;
|
|
9
|
+
export declare const TEAM_SOURCE_FACTS_CACHE_BYTES: number;
|
|
10
|
+
export declare const TEAM_SOURCE_FACTS_MAX_ENTRIES = 32;
|
|
11
|
+
export declare function readTeamSourceFactsCache(dataDir: string, identity: TeamSourceFactsCacheIdentity, privateRaw: Buffer, options?: {
|
|
12
|
+
maxDecodedBytes?: number;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
onDecoded?: (bytes: number) => void;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
index: ContentIndex;
|
|
17
|
+
decodedBytes: number;
|
|
18
|
+
} | null>;
|
|
19
|
+
/** Call only after the checkpoint's AEAD, original hash and coverage verifier succeeds,
|
|
20
|
+
* and before filtering grants. Grant permissions are neither cached nor inferred. */
|
|
21
|
+
export declare function writeTeamSourceFactsCache(dataDir: string, identity: TeamSourceFactsCacheIdentity, privateRaw: Buffer, verifiedIndex: ContentIndex, options?: {
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { mkdir, open, readdir, rename, stat, unlink } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
export const TEAM_SOURCE_FACTS_MAX_BYTES = 2 * 1024 * 1024;
|
|
6
|
+
export const TEAM_SOURCE_FACTS_CACHE_BYTES = 64 * 1024 * 1024;
|
|
7
|
+
export const TEAM_SOURCE_FACTS_MAX_ENTRIES = 32;
|
|
8
|
+
const MAX_ARCHIVES = 10_000;
|
|
9
|
+
const HEADER_BYTES = 32; // magic, random 96-bit nonce, 128-bit GCM tag
|
|
10
|
+
const MAGIC = Buffer.from("SKF1");
|
|
11
|
+
const NAME = /^[a-f0-9]{64}\.facts$/;
|
|
12
|
+
const PARTIAL = /^\.(\d+)\.[a-f0-9-]{36}\.partial$/;
|
|
13
|
+
const activePartials = new Set();
|
|
14
|
+
const directory = (dataDir) => join(dataDir, "index", "team-source-facts-cache");
|
|
15
|
+
const hash = (value) => createHash("sha256").update(value).digest();
|
|
16
|
+
const text = (value, max = 8192) => typeof value === "string" && value.length <= max;
|
|
17
|
+
const sha = (value) => typeof value === "string" && /^[a-f0-9]{64}$/i.test(value);
|
|
18
|
+
const integer = (value, min = 0) => Number.isSafeInteger(value) && Number(value) >= min;
|
|
19
|
+
function scope(identity) {
|
|
20
|
+
const r = identity.receipt;
|
|
21
|
+
const strings = [identity.origin, identity.vaultId, identity.spaceKey, identity.project, identity.projectKey ?? "",
|
|
22
|
+
r.generationId, r.committedAt, r.meta.objectKey, r.blob.objectKey];
|
|
23
|
+
if (strings.some((value) => !text(value)) || !integer(identity.accountId, 1) || !integer(r.revision, 1)
|
|
24
|
+
|| !integer(r.membershipVersion, 1) || !integer(r.meta.bytes, 1) || !integer(r.blob.bytes, 1)
|
|
25
|
+
|| !sha(r.meta.sha256) || !sha(r.blob.sha256))
|
|
26
|
+
return null;
|
|
27
|
+
const coverage = Object.entries(r.coverageJson);
|
|
28
|
+
if (coverage.length > MAX_ARCHIVES || coverage.some(([id, digest]) => !text(id, 512) || !sha(digest)))
|
|
29
|
+
return null;
|
|
30
|
+
let coverageBytes = 0;
|
|
31
|
+
for (const entry of coverage) {
|
|
32
|
+
coverageBytes += Buffer.byteLength(JSON.stringify(entry)) + 1;
|
|
33
|
+
if (coverageBytes > TEAM_SOURCE_FACTS_MAX_BYTES)
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
coverage.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
37
|
+
return hash(JSON.stringify(["sealkeep-team-source-facts-v1", ...strings, identity.accountId,
|
|
38
|
+
r.revision, r.membershipVersion, r.meta.bytes, r.meta.sha256, r.blob.bytes, r.blob.sha256, coverage]));
|
|
39
|
+
}
|
|
40
|
+
function copyFacts(value) {
|
|
41
|
+
if (!value || typeof value !== "object")
|
|
42
|
+
return null;
|
|
43
|
+
const f = value;
|
|
44
|
+
if (f.v !== 1 || !text(f.createdAt, 128) || !text(f.algorithm, 128) || !integer(f.sourceBytes)
|
|
45
|
+
|| !integer(f.storedBytes, 1) || !integer(f.chunks, 1) || !sha(f.sourceSha256) || !sha(f.ciphertextSha256)
|
|
46
|
+
|| (f.envelopeArchiveId !== undefined && !text(f.envelopeArchiveId, 512))
|
|
47
|
+
|| (f.envelopeSha256 !== undefined && !sha(f.envelopeSha256))
|
|
48
|
+
|| (f.teamCustody !== undefined && f.teamCustody !== true))
|
|
49
|
+
return null;
|
|
50
|
+
if (f.delta && (!text(f.delta.baseArchiveId, 512) || !integer(f.delta.baseBytes, 1) || f.delta.baseBytes >= f.sourceBytes))
|
|
51
|
+
return null;
|
|
52
|
+
if (f.supersedes && (!text(f.supersedes.archiveId, 512) || !integer(f.supersedes.bytes)))
|
|
53
|
+
return null;
|
|
54
|
+
return { v: 1, createdAt: f.createdAt, algorithm: f.algorithm, sourceBytes: f.sourceBytes,
|
|
55
|
+
storedBytes: f.storedBytes, chunks: f.chunks, sourceSha256: f.sourceSha256, ciphertextSha256: f.ciphertextSha256,
|
|
56
|
+
...(f.envelopeArchiveId !== undefined ? { envelopeArchiveId: f.envelopeArchiveId } : {}),
|
|
57
|
+
...(f.envelopeSha256 !== undefined ? { envelopeSha256: f.envelopeSha256 } : {}),
|
|
58
|
+
...(f.teamCustody ? { teamCustody: true } : {}),
|
|
59
|
+
...(f.delta ? { delta: { baseArchiveId: f.delta.baseArchiveId, baseBytes: f.delta.baseBytes } } : {}),
|
|
60
|
+
...(f.supersedes ? { supersedes: { archiveId: f.supersedes.archiveId, bytes: f.supersedes.bytes } } : {}) };
|
|
61
|
+
}
|
|
62
|
+
/** Whitelist facts only, preserving every archive/dependency, never query tokens or summaries. */
|
|
63
|
+
function factsIndex(value) {
|
|
64
|
+
if (!value || typeof value !== "object")
|
|
65
|
+
return null;
|
|
66
|
+
const raw = value;
|
|
67
|
+
if (!text(raw.builtAt, 128) || !raw.archives || !raw.meta)
|
|
68
|
+
return null;
|
|
69
|
+
const rows = Object.entries(raw.archives);
|
|
70
|
+
if (rows.length > MAX_ARCHIVES)
|
|
71
|
+
return null;
|
|
72
|
+
const index = { version: 2, builtAt: raw.builtAt, archives: Object.create(null), tokens: {}, meta: Object.create(null) };
|
|
73
|
+
let serializedBytes = 128;
|
|
74
|
+
for (const [id, row] of rows) {
|
|
75
|
+
const facts = copyFacts(raw.meta[id]?.archiveRecord);
|
|
76
|
+
if (!text(id, 512) || !Array.isArray(row) || row.length > 8 || !row.every((item) => text(item)) || !facts)
|
|
77
|
+
return null;
|
|
78
|
+
const line = [...row];
|
|
79
|
+
serializedBytes += Buffer.byteLength(JSON.stringify([id, line, facts])) + Buffer.byteLength(JSON.stringify(id)) + 40;
|
|
80
|
+
if (serializedBytes > TEAM_SOURCE_FACTS_MAX_BYTES)
|
|
81
|
+
return null;
|
|
82
|
+
index.archives[id] = line;
|
|
83
|
+
index.meta[id] = { archiveRecord: facts };
|
|
84
|
+
}
|
|
85
|
+
for (const [id, metadata] of Object.entries(index.meta)) {
|
|
86
|
+
const delta = metadata.archiveRecord.delta;
|
|
87
|
+
if (delta && (!index.archives[delta.baseArchiveId]
|
|
88
|
+
|| index.meta[delta.baseArchiveId]?.archiveRecord?.sourceBytes !== delta.baseBytes
|
|
89
|
+
|| delta.baseArchiveId === id))
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return index;
|
|
93
|
+
}
|
|
94
|
+
async function cleanup(dataDir, keep) {
|
|
95
|
+
const dir = directory(dataDir);
|
|
96
|
+
const names = await readdir(dir).catch(() => []);
|
|
97
|
+
for (const name of names) {
|
|
98
|
+
const match = PARTIAL.exec(name);
|
|
99
|
+
if (!match)
|
|
100
|
+
continue;
|
|
101
|
+
const path = join(dir, name), pid = Number(match[1]);
|
|
102
|
+
if (!integer(pid, 1) || activePartials.has(path))
|
|
103
|
+
continue;
|
|
104
|
+
let alive = pid !== process.pid;
|
|
105
|
+
if (alive) {
|
|
106
|
+
try {
|
|
107
|
+
process.kill(pid, 0);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
alive = error.code !== "ESRCH";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!alive)
|
|
114
|
+
await unlink(path).catch(() => undefined);
|
|
115
|
+
}
|
|
116
|
+
const rows = await Promise.all(names.filter((name) => NAME.test(name)).map(async (name) => {
|
|
117
|
+
const info = await stat(join(dir, name)).catch(() => null);
|
|
118
|
+
return info?.isFile() ? { name, bytes: info.size, at: info.mtimeMs } : null;
|
|
119
|
+
}));
|
|
120
|
+
const ordered = rows.filter((row) => row !== null)
|
|
121
|
+
.sort((a, b) => Number(b.name === keep) - Number(a.name === keep) || b.at - a.at || a.name.localeCompare(b.name));
|
|
122
|
+
let bytes = 0, count = 0;
|
|
123
|
+
for (const row of ordered) {
|
|
124
|
+
bytes += row.bytes;
|
|
125
|
+
count += 1;
|
|
126
|
+
if (row.bytes > TEAM_SOURCE_FACTS_MAX_BYTES + HEADER_BYTES || bytes > TEAM_SOURCE_FACTS_CACHE_BYTES
|
|
127
|
+
|| count > TEAM_SOURCE_FACTS_MAX_ENTRIES)
|
|
128
|
+
await unlink(join(dir, row.name)).catch(() => undefined);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export async function readTeamSourceFactsCache(dataDir, identity, privateRaw, options = {}) {
|
|
132
|
+
options.signal?.throwIfAborted();
|
|
133
|
+
const aad = scope(identity);
|
|
134
|
+
const limit = options.maxDecodedBytes ?? TEAM_SOURCE_FACTS_MAX_BYTES;
|
|
135
|
+
if (!aad || privateRaw.length !== 32 || !integer(limit))
|
|
136
|
+
return null;
|
|
137
|
+
let key, plaintext;
|
|
138
|
+
let callbackFailure;
|
|
139
|
+
try {
|
|
140
|
+
await cleanup(dataDir);
|
|
141
|
+
const handle = await open(join(directory(dataDir), `${aad.toString("hex")}.facts`), constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
142
|
+
let bytes;
|
|
143
|
+
try {
|
|
144
|
+
const before = await handle.stat();
|
|
145
|
+
if (!before.isFile() || before.size <= HEADER_BYTES || before.size > HEADER_BYTES + Math.min(limit, TEAM_SOURCE_FACTS_MAX_BYTES))
|
|
146
|
+
return null;
|
|
147
|
+
bytes = Buffer.alloc(before.size);
|
|
148
|
+
let at = 0;
|
|
149
|
+
while (at < bytes.length) {
|
|
150
|
+
options.signal?.throwIfAborted();
|
|
151
|
+
const read = await handle.read(bytes, at, bytes.length - at, at);
|
|
152
|
+
if (!read.bytesRead)
|
|
153
|
+
return null;
|
|
154
|
+
at += read.bytesRead;
|
|
155
|
+
}
|
|
156
|
+
const after = await handle.stat();
|
|
157
|
+
if (after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs)
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
await handle.close();
|
|
162
|
+
}
|
|
163
|
+
if (!bytes.subarray(0, 4).equals(MAGIC))
|
|
164
|
+
return null;
|
|
165
|
+
key = Buffer.from(hkdfSync("sha256", privateRaw, aad, "sealkeep-source-facts-cache-v1", 32));
|
|
166
|
+
const decipher = createDecipheriv("aes-256-gcm", key, bytes.subarray(4, 16));
|
|
167
|
+
decipher.setAAD(aad);
|
|
168
|
+
decipher.setAuthTag(bytes.subarray(16, HEADER_BYTES));
|
|
169
|
+
plaintext = decipher.update(bytes.subarray(HEADER_BYTES));
|
|
170
|
+
// Even rejected ciphertext consumed bounded decoded memory. Account for
|
|
171
|
+
// that attempt before a caller decides whether a full-verifier fallback fits.
|
|
172
|
+
try {
|
|
173
|
+
options.onDecoded?.(plaintext.length);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
callbackFailure = { error };
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
const final = decipher.final(); // GCM authentication before JSON parsing or publication.
|
|
180
|
+
if (final.length) {
|
|
181
|
+
final.fill(0);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
options.signal?.throwIfAborted();
|
|
185
|
+
const index = factsIndex(JSON.parse(plaintext.toString("utf8")));
|
|
186
|
+
if (!index)
|
|
187
|
+
return null;
|
|
188
|
+
return { index, decodedBytes: plaintext.length };
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (callbackFailure)
|
|
192
|
+
throw callbackFailure.error;
|
|
193
|
+
if (options.signal?.aborted)
|
|
194
|
+
throw error;
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
key?.fill(0);
|
|
199
|
+
plaintext?.fill(0);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** Call only after the checkpoint's AEAD, original hash and coverage verifier succeeds,
|
|
203
|
+
* and before filtering grants. Grant permissions are neither cached nor inferred. */
|
|
204
|
+
export async function writeTeamSourceFactsCache(dataDir, identity, privateRaw, verifiedIndex, options = {}) {
|
|
205
|
+
options.signal?.throwIfAborted();
|
|
206
|
+
const aad = scope(identity), index = factsIndex(verifiedIndex);
|
|
207
|
+
if (!aad || privateRaw.length !== 32 || !index)
|
|
208
|
+
return;
|
|
209
|
+
const plaintext = Buffer.from(JSON.stringify(index));
|
|
210
|
+
if (plaintext.length > TEAM_SOURCE_FACTS_MAX_BYTES) {
|
|
211
|
+
plaintext.fill(0);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const dir = directory(dataDir), name = `${aad.toString("hex")}.facts`;
|
|
215
|
+
const temporary = join(dir, `.${process.pid}.${randomUUID()}.partial`);
|
|
216
|
+
let key;
|
|
217
|
+
try {
|
|
218
|
+
key = Buffer.from(hkdfSync("sha256", privateRaw, aad, "sealkeep-source-facts-cache-v1", 32));
|
|
219
|
+
const nonce = randomBytes(12), cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
220
|
+
cipher.setAAD(aad);
|
|
221
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
222
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
223
|
+
await cleanup(dataDir);
|
|
224
|
+
activePartials.add(temporary);
|
|
225
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
226
|
+
try {
|
|
227
|
+
await handle.writeFile(Buffer.concat([MAGIC, nonce, cipher.getAuthTag(), ciphertext]));
|
|
228
|
+
await handle.sync();
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
await handle.close();
|
|
232
|
+
}
|
|
233
|
+
options.signal?.throwIfAborted();
|
|
234
|
+
await rename(temporary, join(dir, name));
|
|
235
|
+
const parent = await open(dir, "r").catch(() => null);
|
|
236
|
+
try {
|
|
237
|
+
await parent?.sync().catch(() => undefined);
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await parent?.close();
|
|
241
|
+
}
|
|
242
|
+
await cleanup(dataDir, name);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
if (options.signal?.aborted)
|
|
246
|
+
throw error;
|
|
247
|
+
// Derived cache failure never prevents an otherwise verified source read.
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
key?.fill(0);
|
|
251
|
+
plaintext.fill(0);
|
|
252
|
+
activePartials.delete(temporary);
|
|
253
|
+
await unlink(temporary).catch(() => undefined);
|
|
254
|
+
}
|
|
255
|
+
}
|
package/dist/src/trash.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type TrashResult = {
|
|
2
2
|
movedTo: string;
|
|
3
|
-
strategy: "macos-trash" | "xdg-trash" | "windows-recycle-bin" | "
|
|
3
|
+
strategy: "macos-trash" | "xdg-trash" | "windows-recycle-bin" | "sealkeep-trash";
|
|
4
4
|
};
|
|
5
5
|
/**
|
|
6
6
|
* Moves a reclaimed source to the operating system's trash, so a mistake is
|
package/dist/src/trash.js
CHANGED
|
@@ -50,7 +50,7 @@ export async function moveToTrash(path, options) {
|
|
|
50
50
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
51
51
|
const target = await uniquePath(root, name);
|
|
52
52
|
await rename(path, target);
|
|
53
|
-
return { movedTo: target, strategy: "
|
|
53
|
+
return { movedTo: target, strategy: "sealkeep-trash" };
|
|
54
54
|
}
|
|
55
55
|
export function defaultStrategy() {
|
|
56
56
|
if (platform() === "darwin")
|
|
@@ -59,5 +59,5 @@ export function defaultStrategy() {
|
|
|
59
59
|
return "xdg-trash";
|
|
60
60
|
if (platform() === "win32")
|
|
61
61
|
return "windows-recycle-bin";
|
|
62
|
-
return "
|
|
62
|
+
return "sealkeep-trash";
|
|
63
63
|
}
|
package/dist/src/tui.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { PRODUCT_MARK, PRODUCT_NAME } from "./branding.js";
|
|
1
|
+
import { PRODUCT_MARK, PRODUCT_NAME, providerLabel } from "./branding.js";
|
|
2
2
|
import { emitKeypressEvents } from "node:readline";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { localApiToken } from "./local-api.js";
|
|
4
|
+
import { startUi } from "./ui-server.js";
|
|
5
5
|
/**
|
|
6
6
|
* A keyboard-first terminal view over the local API.
|
|
7
7
|
*
|
|
8
8
|
* It renders with plain ANSI and reads only from `/v1`, so it shows exactly what
|
|
9
|
-
* the dashboard shows and works unchanged over SSH. If no
|
|
10
|
-
*
|
|
9
|
+
* the dashboard shows and works unchanged over SSH. If no address was supplied
|
|
10
|
+
* it borrows the vault's singleton loopback API, or owns it for this session.
|
|
11
11
|
*/
|
|
12
12
|
const ESC = "[";
|
|
13
13
|
const RESET = `${ESC}0m`;
|
|
@@ -67,7 +67,7 @@ export function renderFrame(view, state, width = 100) {
|
|
|
67
67
|
const retention = state.retention ?? {};
|
|
68
68
|
lines.push(`Archives ${BOLD}${status.archiveCount ?? 0}${RESET} (${formatBytes(status.archivedBytes ?? 0)} of source preserved)`);
|
|
69
69
|
lines.push(`Queue ${status.queue?.ready ?? 0} ready · ${status.queue?.leased ?? 0} in flight · ${status.queue?.done ?? 0} archived · ${status.queue?.failed ?? 0} failed`);
|
|
70
|
-
lines.push(`Remote ${status.remoteStorage ? `${status.remoteStorage.provider}
|
|
70
|
+
lines.push(`Remote ${status.remoteStorage ? `${providerLabel(status.remoteStorage.provider)} · ${status.remoteStorage.bucket}/${status.remoteStorage.prefix}` : `${DIM}none configured${RESET}`}`);
|
|
71
71
|
lines.push(`Retention policy ${retention.policy ?? "?"} · ${formatBytes(retention.reclaimableBytes ?? 0)} reclaimable`);
|
|
72
72
|
lines.push("");
|
|
73
73
|
for (const agent of state.agents ?? []) {
|
|
@@ -96,7 +96,7 @@ export function renderFrame(view, state, width = 100) {
|
|
|
96
96
|
if (view === "retention") {
|
|
97
97
|
const retention = state.retention ?? {};
|
|
98
98
|
lines.push(`Policy ${BOLD}${retention.policy ?? "?"}${RESET} · older than ${retention.olderThanDays ?? "?"} days · ${retention.graceDays ?? "?"}-day grace`);
|
|
99
|
-
lines.push(`${DIM}
|
|
99
|
+
lines.push(`${DIM}Confirmed reclaim permanently frees only verified, searchable, unchanged, idle originals. Preview never touches the disk.${RESET}`);
|
|
100
100
|
lines.push("");
|
|
101
101
|
for (const candidate of (retention.candidates ?? []).slice(0, 15)) {
|
|
102
102
|
lines.push(`${candidate.eligible ? `${GREEN}eligible${RESET}` : `${DIM}held ${RESET}`} ${truncate(candidate.path, 40)} ${DIM}${candidate.blockers?.[0] ?? ""}${RESET}`);
|
|
@@ -130,10 +130,8 @@ export async function runTui(dataDir, options = {}) {
|
|
|
130
130
|
let base = options.base;
|
|
131
131
|
const token = options.token ?? await localApiToken(dataDir);
|
|
132
132
|
if (!base) {
|
|
133
|
-
owned =
|
|
134
|
-
|
|
135
|
-
await new Promise((ready) => owned.listen(port, "127.0.0.1", ready));
|
|
136
|
-
base = `http://127.0.0.1:${port}`;
|
|
133
|
+
owned = await startUi(dataDir, { port: 4190, openBrowser: false, owner: "interactive" });
|
|
134
|
+
base = owned.origin;
|
|
137
135
|
}
|
|
138
136
|
const client = apiClient(base, token);
|
|
139
137
|
let view = "overview";
|
|
@@ -175,5 +173,6 @@ export async function runTui(dataDir, options = {}) {
|
|
|
175
173
|
};
|
|
176
174
|
input.on("keypress", onKey);
|
|
177
175
|
});
|
|
178
|
-
|
|
176
|
+
if (owned)
|
|
177
|
+
await owned.close();
|
|
179
178
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
import type { Envelope, Suite } from "../packages/
|
|
1
|
+
import type { Envelope, Suite } from "../packages/sealkeep-crypto/src/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Immutable, non-secret identity of an object-store destination.
|
|
4
|
+
*
|
|
5
|
+
* A routing target id is deliberately mutable, while bucket names are not
|
|
6
|
+
* globally unique for R2, B2, MinIO, or other S3-compatible accounts. Copies
|
|
7
|
+
* and locally-held credentials therefore bind to this canonical description,
|
|
8
|
+
* not to the routing label alone. `endpoint` is a canonical origin. `prefix`
|
|
9
|
+
* is included for credentials because changing it changes the namespace that
|
|
10
|
+
* credential was explicitly tested against; an individual copy's objectKey
|
|
11
|
+
* still names the exact object within it.
|
|
12
|
+
*/
|
|
13
|
+
export type StorageDestinationIdentity = {
|
|
14
|
+
version: 1;
|
|
15
|
+
provider: "s3" | "r2" | "b2" | "gcs" | "gdrive";
|
|
16
|
+
bucket: string;
|
|
17
|
+
prefix: string;
|
|
18
|
+
region: string | null;
|
|
19
|
+
endpoint: string | null;
|
|
20
|
+
/** Provider account identity when routing metadata exposes one separately. */
|
|
21
|
+
account: string | null;
|
|
22
|
+
};
|
|
2
23
|
/** The original prototype envelope. Still readable; never written by this build. */
|
|
3
24
|
export type ArchiveRecordV1 = {
|
|
4
25
|
version: 1;
|
|
@@ -26,8 +47,20 @@ export type ArchiveRecordV1 = {
|
|
|
26
47
|
};
|
|
27
48
|
objectPath: string;
|
|
28
49
|
};
|
|
50
|
+
export type TeamCustody = {
|
|
51
|
+
version: 1;
|
|
52
|
+
spaceKey: string;
|
|
53
|
+
envelopeArchiveId: string;
|
|
54
|
+
};
|
|
55
|
+
/** Custody is a narrowly scoped exception to locator/envelope identity equality. */
|
|
56
|
+
export declare function teamCustodyRecordIsValid(value: unknown): boolean;
|
|
29
57
|
/** A remote copy the client uploaded and then verified. Written only after verification. */
|
|
30
58
|
export type RemoteObject = {
|
|
59
|
+
/**
|
|
60
|
+
* Stable identity for this physical copy. Older records do not carry it;
|
|
61
|
+
* archive-copies derives the same id from provider, bucket and object key.
|
|
62
|
+
*/
|
|
63
|
+
copyId?: string;
|
|
31
64
|
provider: string;
|
|
32
65
|
bucket: string;
|
|
33
66
|
/** Single-object layout: the object's key. Chunk layout: the FOLDER prefix (no trailing slash). */
|
|
@@ -38,7 +71,7 @@ export type RemoteObject = {
|
|
|
38
71
|
verifiedAt: string;
|
|
39
72
|
/**
|
|
40
73
|
* Present when the remote is a FOLDER of chunk objects (`chunk-000000`…)
|
|
41
|
-
* plus an `envelope.
|
|
74
|
+
* plus an `envelope.skmeta` sidecar, instead of one blob. Every chunk is
|
|
42
75
|
* independently sealed and independently fetchable; the sidecar makes the
|
|
43
76
|
* folder self-recovering with nothing but the recovery phrase. Absent means
|
|
44
77
|
* the original single-object layout, which stays readable forever.
|
|
@@ -49,8 +82,15 @@ export type RemoteObject = {
|
|
|
49
82
|
};
|
|
50
83
|
/** Which configured storage target this copy went to, for usage accounting. */
|
|
51
84
|
targetId?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Immutable physical destination recorded when this copy verified. Older
|
|
87
|
+
* records omit both fields and are handled by an explicit legacy policy;
|
|
88
|
+
* they must never be silently rebound to an arbitrary custom endpoint.
|
|
89
|
+
*/
|
|
90
|
+
destination?: StorageDestinationIdentity;
|
|
91
|
+
destinationFingerprint?: string;
|
|
52
92
|
};
|
|
53
|
-
/** The current record: a `
|
|
93
|
+
/** The current record: a `sealkeep-crypto` v2 envelope plus local bookkeeping. */
|
|
54
94
|
export type ArchiveRecordV2 = {
|
|
55
95
|
version: 2;
|
|
56
96
|
id: string;
|
|
@@ -60,6 +100,12 @@ export type ArchiveRecordV2 = {
|
|
|
60
100
|
agent: string;
|
|
61
101
|
bytes: number;
|
|
62
102
|
sha256: string;
|
|
103
|
+
/** The project this session belonged to, recorded at seal time — the only place that knows it for certain. */
|
|
104
|
+
project?: string;
|
|
105
|
+
/** Collision-safe local identity used to select a team binding. `project` is display-only. */
|
|
106
|
+
projectKey?: string;
|
|
107
|
+
/** Stable shared-project scope. Local folder/project names may differ on every machine; this opaque id does not. */
|
|
108
|
+
projectScope?: string;
|
|
63
109
|
};
|
|
64
110
|
/** Summary of the stored object, shared with v1 so callers can treat both alike. */
|
|
65
111
|
cipher: {
|
|
@@ -69,13 +115,42 @@ export type ArchiveRecordV2 = {
|
|
|
69
115
|
chunks: number;
|
|
70
116
|
};
|
|
71
117
|
envelope: Envelope;
|
|
118
|
+
/** Explicit owner custody alias: the storage locator differs from the sealed
|
|
119
|
+
* AEAD identity. This is authorized only for the named shared project. */
|
|
120
|
+
teamCustody?: TeamCustody;
|
|
72
121
|
objectPath: string;
|
|
122
|
+
/**
|
|
123
|
+
* Additional verified remote copies. `remote` remains the primary copy so
|
|
124
|
+
* older Sealkeep builds keep reading v2 records; this array never needs to
|
|
125
|
+
* repeat the primary and is normalised by archive-copies on mutation.
|
|
126
|
+
*/
|
|
127
|
+
copies?: RemoteObject[];
|
|
73
128
|
remote?: RemoteObject;
|
|
74
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* Set once the verified/searchable local source has been reclaimed. Current
|
|
131
|
+
* builds permanently purge the transaction-staged original only after the
|
|
132
|
+
* record and audit decision are durable. Older records may still name a
|
|
133
|
+
* recoverable Trash path.
|
|
134
|
+
*/
|
|
75
135
|
reclaimed?: {
|
|
76
136
|
at: string;
|
|
77
|
-
|
|
137
|
+
/** Legacy recoverable-trash location. Absent for permanent disk reclaim. */
|
|
138
|
+
trashPath?: string;
|
|
139
|
+
/** Internal crash-recovery staging path; normally absent by the time callers read the record. */
|
|
140
|
+
stagingPath?: string;
|
|
78
141
|
strategy: string;
|
|
142
|
+
transactionId?: string;
|
|
143
|
+
originalBytes?: number;
|
|
144
|
+
/** Allocated filesystem blocks released, net of a tiny native resume pointer. */
|
|
145
|
+
freedBytes?: number;
|
|
146
|
+
purgedAt?: string;
|
|
147
|
+
/** Exact tiny rollout left behind so Codex can still resolve `resume <sessionId>`. */
|
|
148
|
+
resumeStub?: {
|
|
149
|
+
version: 1;
|
|
150
|
+
sessionId: string;
|
|
151
|
+
bytes: number;
|
|
152
|
+
sha256: string;
|
|
153
|
+
};
|
|
79
154
|
};
|
|
80
155
|
/**
|
|
81
156
|
* Set once the local *archive* has been removed because a verified remote
|
|
@@ -92,6 +167,12 @@ export type ArchiveRecordV2 = {
|
|
|
92
167
|
provider: string;
|
|
93
168
|
bucket: string;
|
|
94
169
|
objectKey: string;
|
|
170
|
+
/** Present only while a crash-recoverable local-cache removal is in flight. */
|
|
171
|
+
transactionId?: string;
|
|
172
|
+
/** Logical ciphertext length displaced from this machine. */
|
|
173
|
+
localBytes?: number;
|
|
174
|
+
/** Filesystem allocation actually released (st_blocks * 512 where available). */
|
|
175
|
+
freedBytes?: number;
|
|
95
176
|
};
|
|
96
177
|
/**
|
|
97
178
|
* The archive whose bytes this one re-archived, and how many of them.
|
|
@@ -127,15 +208,34 @@ export type ArchiveRecord = ArchiveRecordV1 | ArchiveRecordV2;
|
|
|
127
208
|
export type StorageTargetConfig = {
|
|
128
209
|
id: string;
|
|
129
210
|
provider: "vaultline" | "s3" | "r2" | "b2" | "gcs" | "gdrive";
|
|
211
|
+
/** False keeps the destination available for restore/move but excludes it from all new writes. */
|
|
212
|
+
enabled?: boolean;
|
|
130
213
|
bucket?: string;
|
|
131
214
|
prefix?: string;
|
|
132
215
|
region?: string;
|
|
216
|
+
/** Non-secret provider endpoint shared with every machine (required by R2/custom S3). */
|
|
217
|
+
endpoint?: string;
|
|
133
218
|
maxGb?: number;
|
|
134
219
|
projects?: string[];
|
|
135
220
|
priority?: number;
|
|
136
221
|
/** Stamped by machines when they push: bytes this target holds, per their records. */
|
|
137
222
|
usedBytes?: number;
|
|
138
223
|
usedAt?: string;
|
|
224
|
+
/**
|
|
225
|
+
* Non-secret local credential evidence reported by each enrolled machine.
|
|
226
|
+
* Provider credentials never leave that machine. This does not claim the
|
|
227
|
+
* provider answered now; `verifiedAt` names the last explicit full probe.
|
|
228
|
+
*/
|
|
229
|
+
connectionHealth?: Record<string, {
|
|
230
|
+
/** Backward-compatible availability bit: local credentials/session exist. */
|
|
231
|
+
connected: boolean;
|
|
232
|
+
/** What was actually observed; this is never presented as a live provider ping. */
|
|
233
|
+
status?: "credentials_verified" | "credentials_saved" | "credentials_missing" | "account_session";
|
|
234
|
+
checkedAt: string;
|
|
235
|
+
/** Time of the last successful provider write/read/delete/absence probe. */
|
|
236
|
+
verifiedAt?: string | null;
|
|
237
|
+
label?: string;
|
|
238
|
+
}>;
|
|
139
239
|
};
|
|
140
240
|
export declare function isV2(record: ArchiveRecord): record is ArchiveRecordV2;
|
|
141
241
|
export type VaultConfig = {
|
|
@@ -162,6 +262,20 @@ export type VaultConfig = {
|
|
|
162
262
|
};
|
|
163
263
|
/** Multiple simultaneous storage targets with routing rules; wins over remoteStorage when present. */
|
|
164
264
|
storageTargets?: StorageTargetConfig[];
|
|
265
|
+
/**
|
|
266
|
+
* Provenance for the local routing copy. Account-derived rows are usable only
|
|
267
|
+
* while this machine is still in the exact Cloud auth generation which read
|
|
268
|
+
* or edited them; otherwise an account switch could route the next archive
|
|
269
|
+
* through the previous account's Drive or bucket credentials while offline.
|
|
270
|
+
* `local` is reserved for an explicit machine-only configuration. Absence is
|
|
271
|
+
* a legacy, unknown origin and is treated conservatively while signed in.
|
|
272
|
+
*/
|
|
273
|
+
storageTargetsScope?: {
|
|
274
|
+
kind: "local";
|
|
275
|
+
} | {
|
|
276
|
+
kind: "account";
|
|
277
|
+
accountScope: string | null;
|
|
278
|
+
};
|
|
165
279
|
recovery: {
|
|
166
280
|
phraseCheck: string;
|
|
167
281
|
};
|
|
@@ -175,6 +289,8 @@ export type VaultConfig = {
|
|
|
175
289
|
group?: string;
|
|
176
290
|
/** Sharing scope: set, this key is a member of one project and is wrapped into that project's archives only. */
|
|
177
291
|
project?: string;
|
|
292
|
+
/** Stable project identity for new scoped recipients. Legacy label-only recipients remain readable. */
|
|
293
|
+
projectKey?: string;
|
|
178
294
|
}[];
|
|
179
295
|
/** `deleteAfterDays` is optional so vaults written before archive lifecycle existed still load; absent means never. */
|
|
180
296
|
retention?: {
|
|
@@ -185,8 +301,58 @@ export type VaultConfig = {
|
|
|
185
301
|
};
|
|
186
302
|
/** Ed25519 public keys, by key id, that may sign a release this client will accept. */
|
|
187
303
|
updateKeys?: Record<string, string>;
|
|
188
|
-
/**
|
|
189
|
-
|
|
304
|
+
/**
|
|
305
|
+
* Per-project sharing tempo. "active": the moment a session of this project
|
|
306
|
+
* seals, the content index syncs and a `finished` presence line is published
|
|
307
|
+
* — teammates' agents can find the work in minutes. Absent or "passive":
|
|
308
|
+
* archive-at-rest behaviour, nothing extra moves.
|
|
309
|
+
*/
|
|
310
|
+
projectSharing?: Record<string, "active" | "passive">;
|
|
311
|
+
/**
|
|
312
|
+
* Visible local bindings between a checkout/project and an opaque hosted
|
|
313
|
+
* collaboration space. The space key is an identifier, not an encryption
|
|
314
|
+
* key; the actual feed key lives only in the OS keystore. A binding is made
|
|
315
|
+
* once from the dashboard and every supported agent hook uses it thereafter.
|
|
316
|
+
*/
|
|
317
|
+
teamSpaces?: Record<string, {
|
|
318
|
+
/** Stable logical space id used by REST, encryption AAD, and local inboxes. */
|
|
319
|
+
spaceKey: string;
|
|
320
|
+
role?: "owner" | "member";
|
|
321
|
+
keyVersion?: number;
|
|
322
|
+
/** Rotatable opaque Socket.IO route. Absent on pre-rotation/v1 bindings. */
|
|
323
|
+
realtimeKey?: string;
|
|
324
|
+
/** True only after this machine's user chose the local project mapping. */
|
|
325
|
+
localConfirmed?: true;
|
|
326
|
+
/** Human-facing label for bindings stored under an opaque project key. */
|
|
327
|
+
localProject?: string;
|
|
328
|
+
}>;
|
|
329
|
+
/**
|
|
330
|
+
* Hosted spaces this particular machine deliberately disconnected from.
|
|
331
|
+
* Membership remains owned by SealKeep Cloud, but automatic reconciliation
|
|
332
|
+
* must not silently reconnect a checkout the user removed here.
|
|
333
|
+
*/
|
|
334
|
+
teamSpaceOptOuts?: string[];
|
|
335
|
+
/**
|
|
336
|
+
* A local, privacy-preserving handoff from folder discovery to the account
|
|
337
|
+
* panel. The browser records only the logical project name in the cloud;
|
|
338
|
+
* these machine-specific folder/project names never leave this vault.
|
|
339
|
+
*
|
|
340
|
+
* When the matching cloud project appears, the daemon binds every listed
|
|
341
|
+
* local project to its one stable space id and removes the draft. Keeping
|
|
342
|
+
* this in config (rather than browser storage) makes the handoff survive a
|
|
343
|
+
* closed tab, service restart, or package upgrade.
|
|
344
|
+
*/
|
|
345
|
+
teamProjectDrafts?: Array<{
|
|
346
|
+
projectName: string;
|
|
347
|
+
/** Legacy display labels, retained so older clients can still render the draft. */
|
|
348
|
+
projects: string[];
|
|
349
|
+
/** Exact selections used by new clients; labels never decide access when these are present. */
|
|
350
|
+
projectSelections?: Array<{
|
|
351
|
+
projectKey: string;
|
|
352
|
+
project: string;
|
|
353
|
+
}>;
|
|
354
|
+
createdAt: string;
|
|
355
|
+
}>;
|
|
190
356
|
/**
|
|
191
357
|
* How chunk-folder remotes are named. "readable" (default): project/date/
|
|
192
358
|
* session as words — browsable in your own bucket's console. "hashed":
|
package/dist/src/types.js
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/** Custody is a narrowly scoped exception to locator/envelope identity equality. */
|
|
2
|
+
export function teamCustodyRecordIsValid(value) {
|
|
3
|
+
const object = (entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry) ? entry : null;
|
|
4
|
+
const uuid = (entry) => typeof entry === "string"
|
|
5
|
+
&& /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(entry);
|
|
6
|
+
const record = object(value);
|
|
7
|
+
const custody = object(record?.teamCustody);
|
|
8
|
+
const cipher = object(record?.cipher);
|
|
9
|
+
const envelope = object(record?.envelope);
|
|
10
|
+
const source = object(record?.source);
|
|
11
|
+
return record?.version === 2 && uuid(record.id)
|
|
12
|
+
&& custody?.version === 1 && uuid(custody.envelopeArchiveId)
|
|
13
|
+
&& typeof custody.spaceKey === "string" && /^[A-Za-z0-9_-]{43}$/.test(custody.spaceKey)
|
|
14
|
+
&& source?.projectScope === custody.spaceKey
|
|
15
|
+
&& (cipher?.algorithm === "aes-256-gcm" || cipher?.algorithm === "chacha20-poly1305")
|
|
16
|
+
&& envelope?.version === 2 && envelope.archiveId === custody.envelopeArchiveId
|
|
17
|
+
&& envelope.suite === cipher.algorithm
|
|
18
|
+
&& Array.isArray(envelope.wrappedKeys) && envelope.wrappedKeys.length === 1
|
|
19
|
+
&& object(envelope.wrappedKeys[0])?.type === "phrase";
|
|
20
|
+
}
|
|
1
21
|
export function isV2(record) {
|
|
2
22
|
return record.version === 2;
|
|
3
23
|
}
|