sealkeep 0.9.0 → 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 +223 -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 +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 +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.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 +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 +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/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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ArchiveRecordV2 } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Seal straight into the bucket: read a slice, compress it, encrypt it, send
|
|
4
4
|
* it, forget it. The full ciphertext exists nowhere — not on this disk, not in
|
|
@@ -72,5 +72,9 @@ export declare function sealArchiveToCloud(dataDir: string, sourcePath: string,
|
|
|
72
72
|
/** Resume an interrupted upload of this source when a journal for it exists. Defaults on; `--fresh` turns it off. */
|
|
73
73
|
resume?: boolean;
|
|
74
74
|
now?: number;
|
|
75
|
+
/** Storage-route identity stamped on the verified copy. */
|
|
76
|
+
targetId?: string;
|
|
77
|
+
project?: string | null;
|
|
78
|
+
projectKey?: string | null;
|
|
75
79
|
}): Promise<StreamSealOutcome>;
|
|
76
80
|
export {};
|
|
@@ -2,20 +2,22 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { open, stat } from "node:fs/promises";
|
|
3
3
|
import { gzipSync } from "node:zlib";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
|
-
import { fail,
|
|
5
|
+
import { fail, isSealkeepError } from "./errors.js";
|
|
6
6
|
import { recordAudit } from "./audit.js";
|
|
7
7
|
import { canonicalPhrase } from "./mnemonic.js";
|
|
8
|
-
import {
|
|
8
|
+
import { isV2 } from "./types.js";
|
|
9
|
+
import { ADAPTER_VERSION, configuredRecipients, mutateArchiveRecord, readConfig, teamSpaceOf } from "./vault.js";
|
|
10
|
+
import { appendArchiveCopy } from "./archive-copies.js";
|
|
9
11
|
import { uploadClientFromStore } from "./providers/index.js";
|
|
10
12
|
import { compositeChecksum } from "./providers/s3.js";
|
|
11
13
|
import { acquireSpoolLock, createSpool, findSpoolForSource, shredSpool, unwrapSpoolKey, updateSpool } from "./spool.js";
|
|
12
|
-
import { DEFAULT_CHUNK_BYTES, ENVELOPE_VERSION, KEY_BYTES, StreamingSha256, TAG_BYTES, chunkAad, chunkNonce, hashFileRange, sealChunksToSink, wrapAll, zeroize } from "../packages/
|
|
14
|
+
import { DEFAULT_CHUNK_BYTES, ENVELOPE_VERSION, KEY_BYTES, StreamingSha256, TAG_BYTES, chunkAad, chunkNonce, hashFileRange, sealChunksToSink, wrapAll, zeroize } from "../packages/sealkeep-crypto/src/index.js";
|
|
13
15
|
// The package's index deliberately exports formats and flows, not raw AEAD
|
|
14
16
|
// primitives. Resuming needs exactly one primitive operation the flows do not
|
|
15
17
|
// expose: re-producing a single chunk's ciphertext to prove the source still
|
|
16
18
|
// seals to the bytes the provider already holds. Reaching into the package for
|
|
17
19
|
// that beats re-implementing the cipher setup here and drifting from it.
|
|
18
|
-
import { aeadCipher, aeadDecipher } from "../packages/
|
|
20
|
+
import { aeadCipher, aeadDecipher } from "../packages/sealkeep-crypto/src/aead.js";
|
|
19
21
|
const STREAMABLE = new Set(["s3", "r2", "b2", "gcs"]);
|
|
20
22
|
const SEAL_SUITE = "chacha20-poly1305";
|
|
21
23
|
const sha256hex = (input) => createHash("sha256").update(input).digest("hex");
|
|
@@ -123,7 +125,7 @@ function interpretProviderState(state) {
|
|
|
123
125
|
* Anything else (network drop, full disk, a provider 5xx) keeps the journal.
|
|
124
126
|
*/
|
|
125
127
|
function resumeRefused(error) {
|
|
126
|
-
if (!
|
|
128
|
+
if (!isSealkeepError(error))
|
|
127
129
|
return false;
|
|
128
130
|
return /cannot resume/i.test(error.message) || /resumable session/.test(error.message) || /NoSuchUpload/.test(error.message);
|
|
129
131
|
}
|
|
@@ -182,7 +184,7 @@ export async function sealArchiveToCloud(dataDir, sourcePath, rawPhrase, agent,
|
|
|
182
184
|
fail("invalid_argument", `The ${remoteStorage.provider} client cannot stream uploads`);
|
|
183
185
|
}
|
|
184
186
|
const client = rawClient;
|
|
185
|
-
const shared = { dataDir, client, config, remoteStorage, absolute, totalBytes: source.size, phrase, agent, now: options.now };
|
|
187
|
+
const shared = { dataDir, client, config, remoteStorage, absolute, totalBytes: source.size, phrase, agent, project: options.project ?? null, projectKey: options.projectKey ?? null, targetId: options.targetId, now: options.now };
|
|
186
188
|
// Journal triage. Each pass either resumes the newest journal for this
|
|
187
189
|
// source or abandons it with the reason and looks again, so a stack of
|
|
188
190
|
// crashed attempts drains to the one worth continuing — or to none.
|
|
@@ -395,20 +397,38 @@ async function verifyAndRecord(ctx, flight) {
|
|
|
395
397
|
const now = new Date(ctx.now ?? Date.now()).toISOString();
|
|
396
398
|
// `remote.checksum` stays OUR whole-object stored SHA-256 (base64), resumed
|
|
397
399
|
// or not — the resumable hasher covered every stored byte across processes.
|
|
398
|
-
const remote = {
|
|
399
|
-
|
|
400
|
+
const remote = {
|
|
401
|
+
provider: ctx.remoteStorage.provider,
|
|
402
|
+
bucket: ctx.remoteStorage.bucket,
|
|
403
|
+
objectKey,
|
|
404
|
+
bytes: flight.wholeStoredBytes,
|
|
405
|
+
checksum: flight.hashers.stored.digestBase64(),
|
|
406
|
+
verifiedAt: now,
|
|
407
|
+
...(ctx.targetId ? { targetId: ctx.targetId } : {})
|
|
408
|
+
};
|
|
409
|
+
const completed = {
|
|
400
410
|
version: 2, id: archiveId, createdAt: flight.envelope.manifest.createdAt,
|
|
401
|
-
source: {
|
|
411
|
+
source: {
|
|
412
|
+
path: ctx.absolute, agent: ctx.agent, bytes: ctx.totalBytes, sha256: flight.hashers.plaintext.digestHex(),
|
|
413
|
+
...(ctx.project ? { project: ctx.project } : {}),
|
|
414
|
+
...(ctx.projectKey ? { projectKey: ctx.projectKey } : {}),
|
|
415
|
+
...(teamSpaceOf(ctx.config, ctx.project, ctx.projectKey)?.spaceKey ? { projectScope: teamSpaceOf(ctx.config, ctx.project, ctx.projectKey).spaceKey } : {}),
|
|
416
|
+
},
|
|
402
417
|
cipher: { algorithm: flight.envelope.suite, ciphertextSha256: flight.hashers.stored.digestHex(), storedBytes: flight.wholeStoredBytes, chunks: flight.envelope.chunks.length },
|
|
403
418
|
envelope: flight.envelope,
|
|
404
419
|
// Born offloaded: this path names where the blob would live, and the
|
|
405
420
|
// offloaded marker says truthfully that no file is there — the same state
|
|
406
421
|
// an uploaded-then-offloaded archive reaches, arrived at without the detour.
|
|
407
|
-
objectPath: join(ctx.config.storage.root, `${archiveId}.
|
|
422
|
+
objectPath: join(ctx.config.storage.root, `${archiveId}.skarchive`),
|
|
408
423
|
remote,
|
|
409
424
|
offloaded: { at: now, provider: ctx.remoteStorage.provider, bucket: ctx.remoteStorage.bucket, objectKey }
|
|
410
425
|
};
|
|
411
|
-
await
|
|
426
|
+
const record = await mutateArchiveRecord(ctx.config.storage.root, archiveId, (current) => {
|
|
427
|
+
if (!isV2(current))
|
|
428
|
+
fail("invalid_argument", `Archive ${archiveId} uses format v1 and cannot receive a streamed copy`, { archiveId });
|
|
429
|
+
const { remote: _remote, copies: _copies, ...completedFacts } = completed;
|
|
430
|
+
return appendArchiveCopy({ ...current, ...completedFacts }, remote, { makePrimary: true });
|
|
431
|
+
}, { initial: completed });
|
|
412
432
|
await recordAudit(ctx.dataDir, "archive.create", "allowed", { archiveId, agent: ctx.agent, bytes: ctx.totalBytes, recipients: flight.envelope.wrappedKeys.length, streamed: true, ...(flight.resumed ? { resumed: true } : {}) });
|
|
413
433
|
await recordAudit(ctx.dataDir, "upload.verify", "allowed", { archiveId, objectKey, bytes: flight.wholeStoredBytes, provider: ctx.remoteStorage.provider, streamed: true, ...(flight.resumed ? { resumed: true } : {}) });
|
|
414
434
|
await shredSpool(ctx.dataDir, archiveId);
|
|
@@ -419,10 +439,10 @@ async function verifyAndRecord(ctx, flight) {
|
|
|
419
439
|
async function freshSeal(ctx) {
|
|
420
440
|
const archiveId = randomUUID();
|
|
421
441
|
const prefix = ctx.remoteStorage.prefix.replace(/^\/+|\/+$/g, "");
|
|
422
|
-
const objectKey = `${prefix}/${archiveId}.
|
|
442
|
+
const objectKey = `${prefix}/${archiveId}.skarchive`;
|
|
423
443
|
const chunkBytes = DEFAULT_CHUNK_BYTES;
|
|
424
444
|
const totalChunks = Math.max(1, Math.ceil(ctx.totalBytes / chunkBytes));
|
|
425
|
-
const recipients = configuredRecipients(ctx.config, ctx.phrase);
|
|
445
|
+
const recipients = configuredRecipients(ctx.config, ctx.phrase, { project: ctx.project ?? undefined, projectKey: ctx.projectKey ?? undefined });
|
|
426
446
|
// The key is minted here, not inside the seal, because the journal must
|
|
427
447
|
// carry it (wrapped for the same recipients an envelope would name) BEFORE
|
|
428
448
|
// the first byte goes out — a journal written any later could describe
|
|
@@ -520,7 +540,7 @@ async function resumeSeal(ctx, journal) {
|
|
|
520
540
|
const archiveKey = unwrapSpoolKey(journal, { phrase: ctx.phrase });
|
|
521
541
|
try {
|
|
522
542
|
const suite = journal.suite;
|
|
523
|
-
const recipients = configuredRecipients(ctx.config, ctx.phrase);
|
|
543
|
+
const recipients = configuredRecipients(ctx.config, ctx.phrase, { project: ctx.project ?? undefined, projectKey: ctx.projectKey ?? undefined });
|
|
524
544
|
/**
|
|
525
545
|
* Re-seals chunks [0, count) from the CURRENT source, verifying each
|
|
526
546
|
* against the journaled header — and, when the S3 part map is known,
|
package/dist/src/sync-rules.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export declare const DEFAULT_SYNC_RULES: SyncRules;
|
|
|
31
31
|
* response, one file, everything it said.
|
|
32
32
|
*/
|
|
33
33
|
export type AccountSnapshot = {
|
|
34
|
-
plan: "free" | "pro_lite" | "pro" | null;
|
|
34
|
+
plan: "free" | "pro_lite" | "pro" | "team" | null;
|
|
35
35
|
email: string | null;
|
|
36
36
|
quota: {
|
|
37
37
|
usedBytes: number;
|
|
@@ -40,12 +40,39 @@ export type AccountSnapshot = {
|
|
|
40
40
|
/** When the control plane last actually answered — this file's write time, in the data. */
|
|
41
41
|
fetchedAt: string | null;
|
|
42
42
|
};
|
|
43
|
-
export declare function cacheSyncRules(dataDir: string, raw: unknown
|
|
43
|
+
export declare function cacheSyncRules(dataDir: string, raw: unknown, options?: {
|
|
44
|
+
authGeneration?: string | null;
|
|
45
|
+
}): Promise<SyncRules>;
|
|
44
46
|
export declare function cachedSyncRules(dataDir: string): Promise<SyncRules | null>;
|
|
45
47
|
/** The last account answer this machine ever received; null on a vault that never had one. */
|
|
46
48
|
export declare function cachedAccount(dataDir: string): Promise<AccountSnapshot | null>;
|
|
47
49
|
/** How long the last account answer is served as-is before asking again. */
|
|
48
50
|
export declare const ACCOUNT_TTL_MS: number;
|
|
51
|
+
/** How long dashboard polling backs off after a best-effort Cloud refresh fails. */
|
|
52
|
+
export declare const ACCOUNT_REFRESH_RETRY_MS = 30000;
|
|
53
|
+
/** A display refresh may be stale, but it may never remain in flight forever. */
|
|
54
|
+
export declare const DASHBOARD_ACCOUNT_REQUEST_TIMEOUT_MS = 10000;
|
|
55
|
+
export type AccountResolution = {
|
|
56
|
+
account: AccountSnapshot;
|
|
57
|
+
source: "account" | "cache" | "none";
|
|
58
|
+
};
|
|
59
|
+
type DashboardAccountDependencies = {
|
|
60
|
+
cloudAuthGeneration?: (dataDir: string) => Promise<string | null>;
|
|
61
|
+
accountStatus?: (dataDir: string, signal?: AbortSignal) => Promise<unknown>;
|
|
62
|
+
now?: () => number;
|
|
63
|
+
/** Deterministic test seam; production uses the fixed display deadline. */
|
|
64
|
+
requestTimeoutMs?: number;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Dashboard-only stale-while-refresh account resolution.
|
|
68
|
+
*
|
|
69
|
+
* A page render never waits for the control plane: a matching cached account
|
|
70
|
+
* is returned immediately (labelled `cache` when stale), while one detached
|
|
71
|
+
* refresh updates the next read. A never-linked vault returns `none`
|
|
72
|
+
* immediately. The blocking `resolveAccount` below intentionally keeps its old
|
|
73
|
+
* semantics for entitlement and action paths that require a live decision.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveDashboardAccount(dataDir: string, dependencies?: DashboardAccountDependencies): Promise<AccountResolution>;
|
|
49
76
|
/**
|
|
50
77
|
* The account, remembered if recent, live if stale, remembered again if the
|
|
51
78
|
* control plane is unreachable.
|
|
@@ -57,10 +84,7 @@ export declare const ACCOUNT_TTL_MS: number;
|
|
|
57
84
|
* put a ~4-second network round trip inside GET /v1/status, which every page's
|
|
58
85
|
* identity card and plan bar sat waiting on.
|
|
59
86
|
*/
|
|
60
|
-
export declare function resolveAccount(dataDir: string): Promise<
|
|
61
|
-
account: AccountSnapshot;
|
|
62
|
-
source: "account" | "cache" | "none";
|
|
63
|
-
}>;
|
|
87
|
+
export declare function resolveAccount(dataDir: string): Promise<AccountResolution>;
|
|
64
88
|
/**
|
|
65
89
|
* Reads the rules from the account, caching them; falls back to the last cached
|
|
66
90
|
* copy when the control plane cannot be reached, and only to "everything" when
|
|
@@ -83,3 +107,4 @@ export declare function applySyncRules(found: readonly TranscriptCandidate[], ru
|
|
|
83
107
|
heldBack: TranscriptCandidate[];
|
|
84
108
|
};
|
|
85
109
|
export declare function describeSyncRules(rules: SyncRules): string;
|
|
110
|
+
export {};
|
package/dist/src/sync-rules.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, resolve as resolvePath } from "node:path";
|
|
3
3
|
export const DEFAULT_SYNC_RULES = { mode: "all", keepLast: 20, minAgeDays: 7 };
|
|
4
4
|
const CACHE = "sync-rules.json";
|
|
5
5
|
function coerce(raw) {
|
|
@@ -14,7 +14,7 @@ function coerce(raw) {
|
|
|
14
14
|
}
|
|
15
15
|
/** Reads both shapes: the control plane's snake_case reply, and this file's own saved snapshot. */
|
|
16
16
|
function coerceAccount(raw) {
|
|
17
|
-
const plan = raw?.plan === "pro" || raw?.plan === "pro_lite" || raw?.plan === "free" ? raw.plan : null;
|
|
17
|
+
const plan = raw?.plan === "team" || raw?.plan === "pro" || raw?.plan === "pro_lite" || raw?.plan === "free" ? raw.plan : null;
|
|
18
18
|
const used = Number(raw?.quota?.usedBytes ?? raw?.used_bytes);
|
|
19
19
|
const limit = Number(raw?.quota?.quotaBytes ?? raw?.quota_bytes);
|
|
20
20
|
return {
|
|
@@ -24,10 +24,18 @@ function coerceAccount(raw) {
|
|
|
24
24
|
fetchedAt: typeof raw?.fetchedAt === "string" ? raw.fetchedAt : null
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
-
export async function cacheSyncRules(dataDir, raw) {
|
|
27
|
+
export async function cacheSyncRules(dataDir, raw, options = {}) {
|
|
28
28
|
const rules = coerce(raw);
|
|
29
29
|
const account = coerceAccount(raw);
|
|
30
|
-
|
|
30
|
+
// Account data must be tied to the Cloud login that produced it. Without
|
|
31
|
+
// this marker a logout followed by another person's login could briefly show
|
|
32
|
+
// the first person's email, plan and quota while /me was still in flight.
|
|
33
|
+
// Callers that already sampled the generation pass it explicitly; ordinary
|
|
34
|
+
// action paths pay only one local-file read here.
|
|
35
|
+
const authGeneration = Object.hasOwn(options, "authGeneration")
|
|
36
|
+
? options.authGeneration ?? null
|
|
37
|
+
: await import("./cloud.js").then(({ cloudAuthGeneration }) => cloudAuthGeneration(dataDir)).catch(() => null);
|
|
38
|
+
await writeFile(join(dataDir, CACHE), JSON.stringify({ ...rules, ...account, authGeneration, fetchedAt: new Date().toISOString() }, null, 2), { mode: 0o600 });
|
|
31
39
|
return rules;
|
|
32
40
|
}
|
|
33
41
|
export async function cachedSyncRules(dataDir) {
|
|
@@ -39,21 +47,140 @@ export async function cachedSyncRules(dataDir) {
|
|
|
39
47
|
return null;
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
|
-
|
|
43
|
-
export async function cachedAccount(dataDir) {
|
|
50
|
+
async function cachedAccountRecord(dataDir) {
|
|
44
51
|
try {
|
|
45
52
|
const saved = JSON.parse(await readFile(join(dataDir, CACHE), "utf8"));
|
|
46
53
|
const account = coerceAccount(saved);
|
|
47
54
|
// A pre-snapshot cache file (rules only) has no account half; report null
|
|
48
55
|
// rather than an all-null snapshot pretending the account said nothing.
|
|
49
|
-
|
|
56
|
+
if (account.plan === null && account.email === null && account.quota === null)
|
|
57
|
+
return null;
|
|
58
|
+
const authGeneration = typeof saved?.authGeneration === "string"
|
|
59
|
+
? saved.authGeneration
|
|
60
|
+
: saved?.authGeneration === null
|
|
61
|
+
? null
|
|
62
|
+
: undefined;
|
|
63
|
+
return { account, authGeneration };
|
|
50
64
|
}
|
|
51
65
|
catch {
|
|
52
66
|
return null;
|
|
53
67
|
}
|
|
54
68
|
}
|
|
69
|
+
/** The last account answer this machine ever received; null on a vault that never had one. */
|
|
70
|
+
export async function cachedAccount(dataDir) {
|
|
71
|
+
return (await cachedAccountRecord(dataDir))?.account ?? null;
|
|
72
|
+
}
|
|
55
73
|
/** How long the last account answer is served as-is before asking again. */
|
|
56
74
|
export const ACCOUNT_TTL_MS = 5 * 60_000;
|
|
75
|
+
/** How long dashboard polling backs off after a best-effort Cloud refresh fails. */
|
|
76
|
+
export const ACCOUNT_REFRESH_RETRY_MS = 30_000;
|
|
77
|
+
/** A display refresh may be stale, but it may never remain in flight forever. */
|
|
78
|
+
export const DASHBOARD_ACCOUNT_REQUEST_TIMEOUT_MS = 10_000;
|
|
79
|
+
const NO_ACCOUNT = { plan: null, email: null, quota: null, fetchedAt: null };
|
|
80
|
+
/** One live refresh per vault *and Cloud login*, shared by simultaneous UI reads. */
|
|
81
|
+
const dashboardAccountRefreshes = new Map();
|
|
82
|
+
/** A timestamp rather than a timer: this never keeps the daemon alive. */
|
|
83
|
+
const dashboardAccountRetryAt = new Map();
|
|
84
|
+
const accountIsFresh = (account, now) => {
|
|
85
|
+
if (!account.fetchedAt)
|
|
86
|
+
return false;
|
|
87
|
+
const fetchedAt = Date.parse(account.fetchedAt);
|
|
88
|
+
return Number.isFinite(fetchedAt) && now - fetchedAt < ACCOUNT_TTL_MS;
|
|
89
|
+
};
|
|
90
|
+
const cacheBelongsToGeneration = (record, generation) => (
|
|
91
|
+
// A legacy cache can only be used on a legacy machine which has never
|
|
92
|
+
// emitted an auth generation. Every login/logout/renewal now emits one.
|
|
93
|
+
record.authGeneration === undefined ? generation === null : record.authGeneration === generation);
|
|
94
|
+
async function dashboardCloudGeneration(dataDir, dependencies) {
|
|
95
|
+
if (dependencies.cloudAuthGeneration)
|
|
96
|
+
return dependencies.cloudAuthGeneration(dataDir);
|
|
97
|
+
const { cloudAuthGeneration } = await import("./cloud.js");
|
|
98
|
+
return cloudAuthGeneration(dataDir);
|
|
99
|
+
}
|
|
100
|
+
async function dashboardAccountStatus(dataDir, dependencies) {
|
|
101
|
+
const controller = new AbortController();
|
|
102
|
+
const timeoutMs = dependencies.requestTimeoutMs ?? DASHBOARD_ACCOUNT_REQUEST_TIMEOUT_MS;
|
|
103
|
+
let timer;
|
|
104
|
+
const timedOut = new Promise((_, reject) => {
|
|
105
|
+
timer = setTimeout(() => {
|
|
106
|
+
controller.abort();
|
|
107
|
+
reject(new Error("Sealkeep Cloud account display refresh timed out"));
|
|
108
|
+
}, timeoutMs);
|
|
109
|
+
timer.unref?.();
|
|
110
|
+
});
|
|
111
|
+
const work = dependencies.accountStatus
|
|
112
|
+
? dependencies.accountStatus(dataDir, controller.signal)
|
|
113
|
+
: import("./cloud.js").then(({ accountStatus }) => accountStatus(dataDir, undefined, {
|
|
114
|
+
signal: controller.signal,
|
|
115
|
+
// A passive poll must not start the process-wide token renewal which a
|
|
116
|
+
// later write may need to await. An expired snapshot simply leaves the
|
|
117
|
+
// last authenticated account cache visible.
|
|
118
|
+
renewToken: false,
|
|
119
|
+
}));
|
|
120
|
+
try {
|
|
121
|
+
return await Promise.race([work, timedOut]);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
if (timer)
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function refreshDashboardAccount(dataDir, authGeneration, dependencies) {
|
|
129
|
+
const key = `${resolvePath(dataDir)}\0${authGeneration ?? "<no-auth-generation>"}`;
|
|
130
|
+
if (dashboardAccountRefreshes.has(key))
|
|
131
|
+
return;
|
|
132
|
+
const now = dependencies.now?.() ?? Date.now();
|
|
133
|
+
if ((dashboardAccountRetryAt.get(key) ?? 0) > now)
|
|
134
|
+
return;
|
|
135
|
+
const refresh = (async () => {
|
|
136
|
+
try {
|
|
137
|
+
const raw = await dashboardAccountStatus(dataDir, dependencies);
|
|
138
|
+
// A /me request can outlive logout, another login, or token renewal.
|
|
139
|
+
// Never let that old answer overwrite the identity now using the vault.
|
|
140
|
+
if (await dashboardCloudGeneration(dataDir, dependencies) !== authGeneration)
|
|
141
|
+
return;
|
|
142
|
+
await cacheSyncRules(dataDir, raw, { authGeneration });
|
|
143
|
+
dashboardAccountRetryAt.delete(key);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Dashboard refresh is deliberately best-effort. The stale local answer
|
|
147
|
+
// remains useful, and this detached promise must never become unhandled.
|
|
148
|
+
// Only back off the login which actually failed; if auth changed while
|
|
149
|
+
// /me was pending, the new login gets its own immediate refresh.
|
|
150
|
+
const currentGeneration = await dashboardCloudGeneration(dataDir, dependencies).catch(() => null);
|
|
151
|
+
if (currentGeneration === authGeneration) {
|
|
152
|
+
dashboardAccountRetryAt.set(key, (dependencies.now?.() ?? Date.now()) + ACCOUNT_REFRESH_RETRY_MS);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
dashboardAccountRefreshes.set(key, refresh);
|
|
157
|
+
void refresh.then(() => { if (dashboardAccountRefreshes.get(key) === refresh)
|
|
158
|
+
dashboardAccountRefreshes.delete(key); }, () => { if (dashboardAccountRefreshes.get(key) === refresh)
|
|
159
|
+
dashboardAccountRefreshes.delete(key); });
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Dashboard-only stale-while-refresh account resolution.
|
|
163
|
+
*
|
|
164
|
+
* A page render never waits for the control plane: a matching cached account
|
|
165
|
+
* is returned immediately (labelled `cache` when stale), while one detached
|
|
166
|
+
* refresh updates the next read. A never-linked vault returns `none`
|
|
167
|
+
* immediately. The blocking `resolveAccount` below intentionally keeps its old
|
|
168
|
+
* semantics for entitlement and action paths that require a live decision.
|
|
169
|
+
*/
|
|
170
|
+
export async function resolveDashboardAccount(dataDir, dependencies = {}) {
|
|
171
|
+
const [authGeneration, cached] = await Promise.all([
|
|
172
|
+
dashboardCloudGeneration(dataDir, dependencies).catch(() => null),
|
|
173
|
+
cachedAccountRecord(dataDir),
|
|
174
|
+
]);
|
|
175
|
+
const matching = cached && cacheBelongsToGeneration(cached, authGeneration) ? cached.account : null;
|
|
176
|
+
if (matching && accountIsFresh(matching, dependencies.now?.() ?? Date.now())) {
|
|
177
|
+
return { account: matching, source: "account" };
|
|
178
|
+
}
|
|
179
|
+
refreshDashboardAccount(dataDir, authGeneration, dependencies);
|
|
180
|
+
return matching
|
|
181
|
+
? { account: matching, source: "cache" }
|
|
182
|
+
: { account: NO_ACCOUNT, source: "none" };
|
|
183
|
+
}
|
|
57
184
|
/**
|
|
58
185
|
* The account, remembered if recent, live if stale, remembered again if the
|
|
59
186
|
* control plane is unreachable.
|
|
@@ -66,21 +193,33 @@ export const ACCOUNT_TTL_MS = 5 * 60_000;
|
|
|
66
193
|
* identity card and plan bar sat waiting on.
|
|
67
194
|
*/
|
|
68
195
|
export async function resolveAccount(dataDir) {
|
|
69
|
-
const
|
|
70
|
-
|
|
196
|
+
const authGeneration = await dashboardCloudGeneration(dataDir, {}).catch(() => null);
|
|
197
|
+
const cachedRecord = await cachedAccountRecord(dataDir);
|
|
198
|
+
const recent = cachedRecord && cacheBelongsToGeneration(cachedRecord, authGeneration)
|
|
199
|
+
? cachedRecord.account
|
|
200
|
+
: null;
|
|
201
|
+
if (recent && accountIsFresh(recent, Date.now())) {
|
|
71
202
|
return { account: recent, source: "account" };
|
|
72
203
|
}
|
|
73
204
|
try {
|
|
74
205
|
const { accountStatus } = await import("./cloud.js");
|
|
75
206
|
const raw = await accountStatus(dataDir);
|
|
76
|
-
|
|
207
|
+
// The request authenticated before it left. If login/logout/renewal raced
|
|
208
|
+
// it, its answer belongs to the old session and must neither authorize an
|
|
209
|
+
// action nor overwrite the new session's cache.
|
|
210
|
+
if (await dashboardCloudGeneration(dataDir, {}).catch(() => null) !== authGeneration) {
|
|
211
|
+
return { account: NO_ACCOUNT, source: "none" };
|
|
212
|
+
}
|
|
213
|
+
await cacheSyncRules(dataDir, raw, { authGeneration });
|
|
77
214
|
return { account: { ...coerceAccount(raw), fetchedAt: new Date().toISOString() }, source: "account" };
|
|
78
215
|
}
|
|
79
216
|
catch {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
217
|
+
const currentGeneration = await dashboardCloudGeneration(dataDir, {}).catch(() => null);
|
|
218
|
+
const fallback = await cachedAccountRecord(dataDir);
|
|
219
|
+
const matching = fallback && cacheBelongsToGeneration(fallback, currentGeneration) ? fallback.account : null;
|
|
220
|
+
return matching
|
|
221
|
+
? { account: matching, source: "cache" }
|
|
222
|
+
: { account: NO_ACCOUNT, source: "none" };
|
|
84
223
|
}
|
|
85
224
|
}
|
|
86
225
|
/**
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TeamHistoryBackfillJob } from "./team-backfill.js";
|
|
2
|
+
/**
|
|
3
|
+
* Prefer recent work, promoting every base it still needs. Every fourth claim
|
|
4
|
+
* instead takes the longest-waiting eligible job, so new work and repeated
|
|
5
|
+
* failures cannot monopolize that lane. This only orders candidates; dependency/auth/claim gates stay with
|
|
6
|
+
* the durable ledger owner.
|
|
7
|
+
*/
|
|
8
|
+
export declare function orderTeamBackfillJobs(jobs: TeamHistoryBackfillJob[], eligible: TeamHistoryBackfillJob[], sequence: number): TeamHistoryBackfillJob[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefer recent work, promoting every base it still needs. Every fourth claim
|
|
3
|
+
* instead takes the longest-waiting eligible job, so new work and repeated
|
|
4
|
+
* failures cannot monopolize that lane. This only orders candidates; dependency/auth/claim gates stay with
|
|
5
|
+
* the durable ledger owner.
|
|
6
|
+
*/
|
|
7
|
+
export function orderTeamBackfillJobs(jobs, eligible, sequence) {
|
|
8
|
+
const stamp = (job) => {
|
|
9
|
+
const value = Date.parse(job.sourceCreatedAt ?? job.createdAt);
|
|
10
|
+
return Number.isFinite(value) ? value : 0;
|
|
11
|
+
};
|
|
12
|
+
const oldest = (a, b) => (a.lastScheduledAt ?? a.createdAt).localeCompare(b.lastScheduledAt ?? b.createdAt)
|
|
13
|
+
|| a.createdAt.localeCompare(b.createdAt) || stamp(a) - stamp(b) || a.id.localeCompare(b.id);
|
|
14
|
+
if (sequence % 4 === 3)
|
|
15
|
+
return [...eligible].sort(oldest);
|
|
16
|
+
const byId = new Map(jobs.map((job) => [job.id, job]));
|
|
17
|
+
const priority = new Map();
|
|
18
|
+
for (const source of [...jobs].sort((a, b) => stamp(b) - stamp(a))) {
|
|
19
|
+
const value = stamp(source);
|
|
20
|
+
const pending = [source.id];
|
|
21
|
+
while (pending.length) {
|
|
22
|
+
const id = pending.pop();
|
|
23
|
+
if (priority.has(id) && priority.get(id) >= value)
|
|
24
|
+
continue;
|
|
25
|
+
const job = byId.get(id);
|
|
26
|
+
if (!job)
|
|
27
|
+
continue;
|
|
28
|
+
priority.set(id, value);
|
|
29
|
+
pending.push(...job.dependencyJobIds);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return [...eligible].sort((a, b) => (priority.get(b.id) ?? 0) - (priority.get(a.id) ?? 0) || oldest(a, b));
|
|
33
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { type Envelope } from "../packages/sealkeep-crypto/src/index.js";
|
|
2
|
+
import type { TeamRosterWire } from "./cloud.js";
|
|
3
|
+
import { type ArchiveRecordV2, type RemoteObject } from "./types.js";
|
|
4
|
+
export type TeamHistoryBackfillStage = "pending" | "prepared" | "uploaded" | "registered" | "indexed" | "blocked" | "superseded";
|
|
5
|
+
export type TeamHistoryBackfillJob = {
|
|
6
|
+
version: 1;
|
|
7
|
+
id: string;
|
|
8
|
+
shareRef: string;
|
|
9
|
+
sourceArchiveId: string;
|
|
10
|
+
/** Source archive creation time, not the time a backlog was discovered. */
|
|
11
|
+
sourceCreatedAt?: string;
|
|
12
|
+
/** Scheduling fairness, separate from renewal/progress timestamps. */
|
|
13
|
+
lastScheduledAt?: string;
|
|
14
|
+
spaceKey: string;
|
|
15
|
+
project: string;
|
|
16
|
+
projectKey?: string;
|
|
17
|
+
rosterHash: string;
|
|
18
|
+
membership_version?: number;
|
|
19
|
+
memberPublicKeys: string[];
|
|
20
|
+
dependencyJobIds: string[];
|
|
21
|
+
dependencyRefs: string[];
|
|
22
|
+
stage: TeamHistoryBackfillStage;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
updatedAt: string;
|
|
25
|
+
attempts: number;
|
|
26
|
+
nextAttemptAt?: string;
|
|
27
|
+
lastError?: string;
|
|
28
|
+
blockedReason?: string;
|
|
29
|
+
envelope?: Envelope;
|
|
30
|
+
remote?: RemoteObject;
|
|
31
|
+
claim?: {
|
|
32
|
+
token: string;
|
|
33
|
+
pid: number;
|
|
34
|
+
processId?: string;
|
|
35
|
+
processStart?: string;
|
|
36
|
+
at: number;
|
|
37
|
+
};
|
|
38
|
+
cleanupNeeded?: boolean;
|
|
39
|
+
cleanupError?: string;
|
|
40
|
+
};
|
|
41
|
+
export type TeamHistoryBackfillSpaceStatus = {
|
|
42
|
+
state: "idle" | "working" | "attention";
|
|
43
|
+
total: number;
|
|
44
|
+
indexed: number;
|
|
45
|
+
registered: number;
|
|
46
|
+
pending: number;
|
|
47
|
+
failed: number;
|
|
48
|
+
blocked: number;
|
|
49
|
+
activeArchiveId?: string;
|
|
50
|
+
lastError?: string;
|
|
51
|
+
};
|
|
52
|
+
export type TeamHistoryBackfillStatus = {
|
|
53
|
+
state: "idle" | "working" | "attention";
|
|
54
|
+
total: number;
|
|
55
|
+
indexed: number;
|
|
56
|
+
registered: number;
|
|
57
|
+
pending: number;
|
|
58
|
+
failed: number;
|
|
59
|
+
blocked: number;
|
|
60
|
+
lastError?: string;
|
|
61
|
+
spaces: Record<string, TeamHistoryBackfillSpaceStatus>;
|
|
62
|
+
};
|
|
63
|
+
export type TeamHistoryBackfillOutcome = TeamHistoryBackfillStatus & {
|
|
64
|
+
scheduled: number;
|
|
65
|
+
processed: number;
|
|
66
|
+
uploadedNow: number;
|
|
67
|
+
registeredNow: number;
|
|
68
|
+
};
|
|
69
|
+
export type TeamHistoryBackfillPublication = {
|
|
70
|
+
jobId: string;
|
|
71
|
+
sourceArchiveId: string;
|
|
72
|
+
record: ArchiveRecordV2;
|
|
73
|
+
};
|
|
74
|
+
export type TeamHistoryBackfillInventoryDependencies = {
|
|
75
|
+
readArchiveNames?: (storageRoot: string) => Promise<string[]>;
|
|
76
|
+
statArchive?: (path: string) => Promise<{
|
|
77
|
+
size: number;
|
|
78
|
+
mtimeMs: number;
|
|
79
|
+
} | null>;
|
|
80
|
+
yieldAfterStat?: () => Promise<void>;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Discover exact old-history work and persist it before any bytes move.
|
|
84
|
+
* Network failures leave the previous durable plan alone; they never turn an
|
|
85
|
+
* unknown roster into a guessed recipient set.
|
|
86
|
+
*/
|
|
87
|
+
export declare function scheduleTeamHistoryBackfill(dataDir: string, now?: number, inventory?: TeamHistoryBackfillInventoryDependencies, options?: {
|
|
88
|
+
signal?: AbortSignal;
|
|
89
|
+
idleMs?: number;
|
|
90
|
+
}): Promise<number>;
|
|
91
|
+
/** Drain a bounded number of durable jobs. Every stage is committed separately. */
|
|
92
|
+
export declare function drainTeamHistoryBackfill(dataDir: string, rawPhrase: string, options?: {
|
|
93
|
+
limit?: number;
|
|
94
|
+
now?: () => number;
|
|
95
|
+
signal?: AbortSignal;
|
|
96
|
+
idleMs?: number;
|
|
97
|
+
maxBatchMs?: number;
|
|
98
|
+
}): Promise<{
|
|
99
|
+
processed: number;
|
|
100
|
+
uploaded: number;
|
|
101
|
+
registered: number;
|
|
102
|
+
}>;
|
|
103
|
+
export declare function teamHistoryBackfillStatus(dataDir: string): Promise<TeamHistoryBackfillStatus>;
|
|
104
|
+
export declare function reconcileTeamHistoryBackfill(dataDir: string, phrase: string, options?: {
|
|
105
|
+
limit?: number;
|
|
106
|
+
now?: () => number;
|
|
107
|
+
inventory?: TeamHistoryBackfillInventoryDependencies;
|
|
108
|
+
signal?: AbortSignal;
|
|
109
|
+
idleMs?: number;
|
|
110
|
+
maxBatchMs?: number;
|
|
111
|
+
}): Promise<TeamHistoryBackfillOutcome>;
|
|
112
|
+
/** Current, verified, membership-wrapped jobs suitable for a team shard. */
|
|
113
|
+
export declare function teamHistoryBackfillPublications(dataDir: string, spaceKey: string, currentRosterSnapshot: TeamRosterWire): Promise<TeamHistoryBackfillPublication[]>;
|
|
114
|
+
/** All refs ever produced for this space, used only to remove stale roster aliases. */
|
|
115
|
+
export declare function allTeamHistoryBackfillRefs(dataDir: string, spaceKey: string): Promise<Set<string>>;
|
|
116
|
+
export declare function markTeamHistoryBackfillsIndexed(dataDir: string, jobIds: readonly string[]): Promise<void>;
|