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
package/dist/src/ui.d.ts
CHANGED
|
@@ -11,7 +11,6 @@ export declare const blue: (value: string) => string;
|
|
|
11
11
|
export declare const green: (value: string) => string;
|
|
12
12
|
export declare const amber: (value: string) => string;
|
|
13
13
|
export declare const red: (value: string) => string;
|
|
14
|
-
export declare const underline: (value: string) => string;
|
|
15
14
|
/** State markers. Text-first so they survive `NO_COLOR` and screen readers. */
|
|
16
15
|
export declare const mark: {
|
|
17
16
|
ok: () => string;
|
|
@@ -21,7 +20,7 @@ export declare const mark: {
|
|
|
21
20
|
};
|
|
22
21
|
export declare function heading(text: string): string;
|
|
23
22
|
/** The recurring device: the line past which plaintext does not travel. */
|
|
24
|
-
export declare function
|
|
23
|
+
export declare function divider(label?: string): string;
|
|
25
24
|
export declare function keyValue(pairs: [string, string][], indent?: string): string;
|
|
26
25
|
export declare function bytes(value: number): string;
|
|
27
26
|
export declare function relativeTime(iso: string): string;
|
package/dist/src/ui.js
CHANGED
|
@@ -14,7 +14,6 @@ export const blue = wrap("38;5;33", "39");
|
|
|
14
14
|
export const green = wrap("38;5;29", "39");
|
|
15
15
|
export const amber = wrap("38;5;172", "39");
|
|
16
16
|
export const red = wrap("38;5;160", "39");
|
|
17
|
-
export const underline = wrap("4", "24");
|
|
18
17
|
/** State markers. Text-first so they survive `NO_COLOR` and screen readers. */
|
|
19
18
|
export const mark = {
|
|
20
19
|
ok: () => green("ok"),
|
|
@@ -26,7 +25,7 @@ export function heading(text) {
|
|
|
26
25
|
return `\n${bold(text)}\n${dim("─".repeat(Math.min(text.length + 12, 60)))}`;
|
|
27
26
|
}
|
|
28
27
|
/** The recurring device: the line past which plaintext does not travel. */
|
|
29
|
-
export function
|
|
28
|
+
export function divider(label = "plaintext ends here") {
|
|
30
29
|
return `${dim("─".repeat(4))}${dim("┤")} ${blue(label)} ${dim("├")}${dim("─".repeat(Math.max(4, 52 - label.length)))}`;
|
|
31
30
|
}
|
|
32
31
|
export function keyValue(pairs, indent = " ") {
|
package/dist/src/upload.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ProviderUploadClient, type UploadLease } from "./control-plane.js";
|
|
2
2
|
import { type EndpointOverrides } from "./providers/index.js";
|
|
3
|
+
import { type ArchiveRecordV2 } from "./types.js";
|
|
3
4
|
export type UploadOptions = {
|
|
4
5
|
/** Routed multi-target override: upload to THIS storage instead of config.remoteStorage. */
|
|
5
6
|
remoteStorage?: {
|
|
@@ -16,15 +17,41 @@ export type UploadOptions = {
|
|
|
16
17
|
overrides?: EndpointOverrides;
|
|
17
18
|
now?: number;
|
|
18
19
|
ttlMs?: number;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
/** Cumulative bytes read or sent; used only to reset an owning idle deadline. */
|
|
22
|
+
onProgress?: (bytes: number) => void;
|
|
23
|
+
/** Sustained upload ceiling. The daemon supplies the per-machine setting;
|
|
24
|
+
* an explicit/manual upload may omit it. */
|
|
25
|
+
maxBytesPerSecond?: number;
|
|
26
|
+
/** Deterministic pacing seams. */
|
|
27
|
+
paceNow?: () => number;
|
|
28
|
+
paceSleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
29
|
+
/** Internal bridge path for a provider-to-provider transfer. */
|
|
30
|
+
ciphertextPath?: string;
|
|
31
|
+
/**
|
|
32
|
+
* A transfer run coalesces this advisory account update after all of its
|
|
33
|
+
* destination verifies and source deletes. Ordinary one-off/queue uploads
|
|
34
|
+
* keep the non-blocking default.
|
|
35
|
+
*/
|
|
36
|
+
reportTargetUsage?: boolean;
|
|
19
37
|
};
|
|
20
38
|
export type UploadOutcome = {
|
|
21
39
|
archiveId: string;
|
|
22
40
|
objectKey: string;
|
|
41
|
+
copyId: string;
|
|
23
42
|
bytes: number;
|
|
24
43
|
checksum: string;
|
|
25
44
|
verified: boolean;
|
|
26
45
|
provider: string;
|
|
27
46
|
};
|
|
47
|
+
export declare function archiveWrapsEveryMember(record: ArchiveRecordV2, publicKeys: string[]): boolean;
|
|
48
|
+
/** Retry grants for durable project archives, including ones uploaded offline. */
|
|
49
|
+
export declare function reconcileManagedTeamArchives(dataDir: string, options?: {
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
}): Promise<{
|
|
52
|
+
registered: number;
|
|
53
|
+
failed: number;
|
|
54
|
+
}>;
|
|
28
55
|
/**
|
|
29
56
|
* Uploads one archive's ciphertext and verifies it landed intact.
|
|
30
57
|
*
|
package/dist/src/upload.js
CHANGED
|
@@ -1,12 +1,142 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
3
|
import { assertLeaseUsable } from "./control-plane.js";
|
|
4
4
|
import { fail } from "./errors.js";
|
|
5
5
|
import { recordAudit } from "./audit.js";
|
|
6
6
|
import { createActiveLease } from "./providers/index.js";
|
|
7
7
|
import { uploadClientFromStore } from "./providers/index.js";
|
|
8
8
|
import { isV2 } from "./types.js";
|
|
9
|
-
import { listArchives,
|
|
9
|
+
import { listArchives, mutateArchiveRecord, readConfig } from "./vault.js";
|
|
10
|
+
import { STORAGE_PREFIX_DEFAULT } from "./branding.js";
|
|
11
|
+
import { appendArchiveCopy, archiveCopies, archiveCopyId, hasVerifiedCopy } from "./archive-copies.js";
|
|
12
|
+
import { collectChunks, digestChunks, fileHandleChunks, LEGACY_BUFFER_LIMIT_BYTES, tapDigest } from "./byte-stream.js";
|
|
13
|
+
import { paceBackgroundByteStream } from "./background-bandwidth.js";
|
|
14
|
+
export function archiveWrapsEveryMember(record, publicKeys) {
|
|
15
|
+
if (!record.envelope || publicKeys.length === 0)
|
|
16
|
+
return false;
|
|
17
|
+
const wanted = [...new Set(publicKeys)].sort();
|
|
18
|
+
const actual = record.envelope.wrappedKeys
|
|
19
|
+
.filter((entry) => entry.type === "x25519")
|
|
20
|
+
.map((entry) => entry.recipientPublicKey)
|
|
21
|
+
.sort();
|
|
22
|
+
// Phrase recovery remains private to the owner and is not a team identity.
|
|
23
|
+
// Every x25519 recipient, however, is an account/device that can open the
|
|
24
|
+
// object once the hosted membership gate serves it. A subset check retained
|
|
25
|
+
// removed or unrelated keys and could make an old grant look current.
|
|
26
|
+
return actual.length === wanted.length
|
|
27
|
+
&& actual.every((publicKey, index) => publicKey === wanted[index]);
|
|
28
|
+
}
|
|
29
|
+
function configuredSpaceKeyForRecord(config, record, project) {
|
|
30
|
+
const bindings = config.teamSpaces ?? {};
|
|
31
|
+
const stableScope = record.source.projectScope;
|
|
32
|
+
if (stableScope) {
|
|
33
|
+
return Object.values(bindings).some((binding) => binding.spaceKey === stableScope)
|
|
34
|
+
? stableScope
|
|
35
|
+
: null;
|
|
36
|
+
}
|
|
37
|
+
return project ? bindings[project]?.spaceKey ?? null : null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Publish the membership-gated project grant after the account copy is proven
|
|
41
|
+
* durable. This is deliberately idempotent and best-effort at the upload call
|
|
42
|
+
* site: the daemon-wide reconciliation below retries every missed grant, so a
|
|
43
|
+
* short control-plane outage never turns a successful backup into a failed
|
|
44
|
+
* backup or requires a person to run a sharing command.
|
|
45
|
+
*/
|
|
46
|
+
async function registerManagedTeamArchive(dataDir, record, options = {}) {
|
|
47
|
+
options.signal?.throwIfAborted();
|
|
48
|
+
if (!archiveCopies(record).some((copy) => copy.provider === "vaultline" && copy.verifiedAt))
|
|
49
|
+
return false;
|
|
50
|
+
const config = await readConfig(dataDir);
|
|
51
|
+
const { projectOfRecord } = await import("./adapters.js");
|
|
52
|
+
const project = record.source.projectScope ? null : await projectOfRecord(record).catch(() => null);
|
|
53
|
+
const spaceKey = configuredSpaceKeyForRecord(config, record, project);
|
|
54
|
+
if (!spaceKey)
|
|
55
|
+
return false;
|
|
56
|
+
const { getTeamRoster, listTeamArchives, registerTeamArchive } = await import("./cloud.js");
|
|
57
|
+
const roster = await getTeamRoster(dataDir, spaceKey, undefined, { signal: options.signal });
|
|
58
|
+
if (!archiveWrapsEveryMember(record, roster.members.map((member) => member.public_key)))
|
|
59
|
+
return false;
|
|
60
|
+
const existing = await listTeamArchives(dataDir, spaceKey, undefined, { signal: options.signal });
|
|
61
|
+
if (existing.some((row) => row.vault_ref === record.id && row.membership_version === roster.membership_version))
|
|
62
|
+
return true;
|
|
63
|
+
await registerTeamArchive(dataDir, spaceKey, record.id, roster.membership_version, undefined, { signal: options.signal });
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/** Retry grants for durable project archives, including ones uploaded offline. */
|
|
67
|
+
export async function reconcileManagedTeamArchives(dataDir, options = {}) {
|
|
68
|
+
options.signal?.throwIfAborted();
|
|
69
|
+
const config = await readConfig(dataDir);
|
|
70
|
+
if (!config.teamSpaces || Object.keys(config.teamSpaces).length === 0)
|
|
71
|
+
return { registered: 0, failed: 0 };
|
|
72
|
+
const { projectOfRecord } = await import("./adapters.js");
|
|
73
|
+
const bySpace = new Map();
|
|
74
|
+
for (const record of await listArchives(dataDir)) {
|
|
75
|
+
if (!isV2(record) || !archiveCopies(record).some((copy) => copy.provider === "vaultline" && copy.verifiedAt))
|
|
76
|
+
continue;
|
|
77
|
+
const project = record.source.projectScope ? null : await projectOfRecord(record).catch(() => null);
|
|
78
|
+
const spaceKey = configuredSpaceKeyForRecord(config, record, project);
|
|
79
|
+
if (!spaceKey)
|
|
80
|
+
continue;
|
|
81
|
+
const records = bySpace.get(spaceKey) ?? [];
|
|
82
|
+
records.push(record);
|
|
83
|
+
bySpace.set(spaceKey, records);
|
|
84
|
+
}
|
|
85
|
+
const { getTeamRoster, listTeamArchives, registerTeamArchive } = await import("./cloud.js");
|
|
86
|
+
let registered = 0;
|
|
87
|
+
let failed = 0;
|
|
88
|
+
const membershipVersionBySpace = new Map();
|
|
89
|
+
for (const [spaceKey, records] of bySpace) {
|
|
90
|
+
options.signal?.throwIfAborted();
|
|
91
|
+
let shareable;
|
|
92
|
+
try {
|
|
93
|
+
const roster = await getTeamRoster(dataDir, spaceKey, undefined, { signal: options.signal });
|
|
94
|
+
const memberKeys = roster.members.map((member) => member.public_key);
|
|
95
|
+
shareable = records.filter((record) => archiveWrapsEveryMember(record, memberKeys));
|
|
96
|
+
membershipVersionBySpace.set(spaceKey, roster.membership_version);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (options.signal?.aborted)
|
|
100
|
+
throw options.signal.reason ?? error;
|
|
101
|
+
failed += records.length;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (shareable.length === 0)
|
|
105
|
+
continue;
|
|
106
|
+
let existing;
|
|
107
|
+
try {
|
|
108
|
+
const membershipVersion = membershipVersionBySpace.get(spaceKey);
|
|
109
|
+
existing = new Set((await listTeamArchives(dataDir, spaceKey, undefined, { signal: options.signal }))
|
|
110
|
+
.filter((row) => row.membership_version === membershipVersion)
|
|
111
|
+
.map((row) => row.vault_ref));
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (options.signal?.aborted)
|
|
115
|
+
throw options.signal.reason ?? error;
|
|
116
|
+
failed += shareable.length;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
for (const record of shareable) {
|
|
120
|
+
options.signal?.throwIfAborted();
|
|
121
|
+
if (existing.has(record.id))
|
|
122
|
+
continue;
|
|
123
|
+
try {
|
|
124
|
+
const membershipVersion = membershipVersionBySpace.get(spaceKey);
|
|
125
|
+
if (!membershipVersion)
|
|
126
|
+
throw new Error("team_roster_version_missing");
|
|
127
|
+
await registerTeamArchive(dataDir, spaceKey, record.id, membershipVersion, undefined, { signal: options.signal });
|
|
128
|
+
existing.add(record.id);
|
|
129
|
+
registered += 1;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (options.signal?.aborted)
|
|
133
|
+
throw options.signal.reason ?? error;
|
|
134
|
+
failed += 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { registered, failed };
|
|
139
|
+
}
|
|
10
140
|
/**
|
|
11
141
|
* Uploads one archive's ciphertext and verifies it landed intact.
|
|
12
142
|
*
|
|
@@ -17,64 +147,274 @@ import { listArchives, readConfig, writeRecord } from "./vault.js";
|
|
|
17
147
|
*/
|
|
18
148
|
export async function uploadArchive(dataDir, archiveId, options = {}) {
|
|
19
149
|
const config = await readConfig(dataDir);
|
|
20
|
-
const remoteStorage = options.remoteStorage ?? config.remoteStorage ?? fail("storage_not_configured", "No remote target configured. Run: sealkeep storage configure ...");
|
|
21
150
|
const record = (await listArchives(dataDir)).find((item) => item.id === archiveId);
|
|
22
151
|
if (!record)
|
|
23
152
|
fail("archive_not_found", `Archive not found: ${archiveId}`, { archiveId });
|
|
24
153
|
if (!isV2(record))
|
|
25
154
|
fail("invalid_argument", `Archive ${archiveId} uses format v1. Run: sealkeep migrate`, { archiveId });
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
155
|
+
const project = record.source.project ?? null;
|
|
156
|
+
// A stable scope means the archive was sealed for a team even if this
|
|
157
|
+
// machine has since removed or renamed its local binding. Older records may
|
|
158
|
+
// carry only the project, so the current binding is the second proof.
|
|
159
|
+
const teamBound = Boolean(record.source.projectScope || (project && config.teamSpaces?.[project]));
|
|
160
|
+
const explicitTransport = options.remoteStorage !== undefined || options.client !== undefined;
|
|
161
|
+
let targetId = options.targetId;
|
|
162
|
+
let remoteStorage = options.remoteStorage ?? config.remoteStorage ?? null;
|
|
163
|
+
let routedTarget = null;
|
|
164
|
+
if (explicitTransport) {
|
|
165
|
+
// Explicit transports are test/advanced seams and keep their old behavior,
|
|
166
|
+
// but they may not move a team-readable archive into personal storage.
|
|
167
|
+
const provider = options.remoteStorage?.provider ?? options.client?.kind ?? remoteStorage?.provider;
|
|
168
|
+
if (teamBound && provider !== "vaultline") {
|
|
169
|
+
fail("storage_not_configured", "Shared project memory must be stored in Sealkeep Cloud so every approved member can reach the same encrypted archive.");
|
|
170
|
+
}
|
|
171
|
+
// A caller may choose an account target while supplying its transport
|
|
172
|
+
// shape explicitly (the CLI's Drive single-object path does this). The id
|
|
173
|
+
// still owns the credential and endpoint; explicit transport must not
|
|
174
|
+
// silently fall back to the vault-wide legacy secret.
|
|
175
|
+
if (targetId && options.remoteStorage) {
|
|
176
|
+
const { resolveTargets } = await import("./storage-targets.js");
|
|
177
|
+
const named = (await resolveTargets(dataDir)).find((target) => target.id === targetId);
|
|
178
|
+
if (!named)
|
|
179
|
+
fail("storage_not_configured", `Storage target "${targetId}" is not configured on this machine.`);
|
|
180
|
+
if (named.provider !== options.remoteStorage.provider
|
|
181
|
+
|| named.bucket !== options.remoteStorage.bucket) {
|
|
182
|
+
fail("invalid_argument", `Storage target "${targetId}" no longer describes the supplied ${options.remoteStorage.provider} destination.`);
|
|
183
|
+
}
|
|
184
|
+
routedTarget = named;
|
|
185
|
+
}
|
|
50
186
|
}
|
|
51
|
-
else
|
|
52
|
-
|
|
187
|
+
else {
|
|
188
|
+
// This is the single routing boundary for every ordinary caller: dashboard,
|
|
189
|
+
// daemon, CLI upload-pending, and integrations all inherit account order,
|
|
190
|
+
// caps and project pins without each surface reimplementing them.
|
|
191
|
+
const { chooseTarget } = await import("./storage-targets.js");
|
|
192
|
+
const target = await chooseTarget(dataDir, {
|
|
193
|
+
project,
|
|
194
|
+
projectKey: record.source.projectKey ?? null,
|
|
195
|
+
bytes: record.cipher.storedBytes,
|
|
196
|
+
requireManaged: teamBound,
|
|
197
|
+
...(targetId ? { targetId } : {}),
|
|
198
|
+
});
|
|
199
|
+
routedTarget = target;
|
|
200
|
+
targetId = target.id;
|
|
201
|
+
remoteStorage = target.provider === "vaultline"
|
|
202
|
+
? null
|
|
203
|
+
: {
|
|
204
|
+
provider: target.provider,
|
|
205
|
+
bucket: target.bucket ?? (target.provider === "gdrive" ? "Sealkeep" : ""),
|
|
206
|
+
prefix: target.prefix ?? STORAGE_PREFIX_DEFAULT,
|
|
207
|
+
...(target.region ? { region: target.region } : {}),
|
|
208
|
+
};
|
|
53
209
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
210
|
+
/**
|
|
211
|
+
* A managed account IS a remote target.
|
|
212
|
+
*
|
|
213
|
+
* This gated on a configured bucket alone, so a paying account signed in to
|
|
214
|
+
* managed storage was told "No remote target configured" for every one of
|
|
215
|
+
* 1,035 archives — the plan's 500 GB sat at 0 B used while the only copy of
|
|
216
|
+
* everything stayed on one disk. Managed accounts take the plane's leased
|
|
217
|
+
* path, which verifies durability before reporting success; a configured
|
|
218
|
+
* bucket still wins when both exist.
|
|
219
|
+
*/
|
|
220
|
+
// "vaultline" as an explicit remoteStorage entry is the older of the two
|
|
221
|
+
// equally valid enrolled-vault shapes (the newer carries only the cloud
|
|
222
|
+
// token). Both mean the same thing — managed storage — so both take the
|
|
223
|
+
// plane's leased path; the provider ladder below is for buckets the USER
|
|
224
|
+
// brings, and handing it "vaultline" ends at "enable a signer", an
|
|
225
|
+
// instruction no managed account can follow.
|
|
226
|
+
if (!remoteStorage || remoteStorage.provider === "vaultline") {
|
|
227
|
+
const { cloudToken, pushArchive } = await import("./cloud.js");
|
|
228
|
+
await cloudToken(dataDir, undefined, { signal: options.signal }).catch((error) => {
|
|
229
|
+
if (options.signal?.aborted)
|
|
230
|
+
throw options.signal.reason ?? error;
|
|
231
|
+
return fail("storage_not_configured", "No remote target configured. Run: sealkeep storage configure ... (or sign in with `sealkeep cloud login`)");
|
|
232
|
+
});
|
|
233
|
+
const pushed = await pushArchive(dataDir, archiveId, undefined, {
|
|
234
|
+
signal: options.signal,
|
|
235
|
+
onProgress: options.onProgress,
|
|
236
|
+
maxBytesPerSecond: options.maxBytesPerSecond,
|
|
237
|
+
paceNow: options.paceNow,
|
|
238
|
+
paceSleep: options.paceSleep,
|
|
239
|
+
...(options.ciphertextPath ? { ciphertextPath: options.ciphertextPath } : {}),
|
|
240
|
+
});
|
|
241
|
+
if (!pushed.durable) {
|
|
242
|
+
fail("ciphertext_integrity_failed", pushed.note ?? `Managed storage did not verify archive ${archiveId}; the local copy is untouched.`, {
|
|
243
|
+
archiveId,
|
|
244
|
+
objectKey: pushed.objectKey,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
let current = (await listArchives(dataDir)).find((item) => isV2(item) && item.id === archiveId);
|
|
248
|
+
if (current && targetId) {
|
|
249
|
+
current = await mutateArchiveRecord(config.storage.root, archiveId, (latest) => {
|
|
250
|
+
if (!isV2(latest))
|
|
251
|
+
return latest;
|
|
252
|
+
const managed = archiveCopies(latest).find((copy) => copy.provider === "vaultline" && copy.objectKey === pushed.objectKey);
|
|
253
|
+
return managed ? appendArchiveCopy(latest, { ...managed, targetId }) : latest;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (current) {
|
|
257
|
+
try {
|
|
258
|
+
await registerManagedTeamArchive(dataDir, current, { signal: options.signal });
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
if (options.signal?.aborted)
|
|
262
|
+
throw options.signal.reason ?? error;
|
|
263
|
+
// Registration is retried by the daemon-wide reconciliation lane.
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (options.reportTargetUsage !== false) {
|
|
267
|
+
const { pushTargetUsage } = await import("./storage-targets.js");
|
|
268
|
+
void pushTargetUsage(dataDir);
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
archiveId: pushed.archiveId,
|
|
272
|
+
objectKey: pushed.objectKey,
|
|
273
|
+
copyId: archiveCopyId({ provider: "vaultline", bucket: "vaultline-managed", objectKey: pushed.objectKey }),
|
|
274
|
+
bytes: pushed.bytes,
|
|
275
|
+
checksum: "",
|
|
276
|
+
verified: true,
|
|
277
|
+
provider: "vaultline"
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const ciphertextPath = options.ciphertextPath ?? record.objectPath;
|
|
281
|
+
const handle = await open(ciphertextPath, "r").catch(() => fail("archive_not_found", `Archive ciphertext is not readable: ${ciphertextPath}`, { archiveId, path: ciphertextPath }));
|
|
282
|
+
try {
|
|
283
|
+
const measured = await digestChunks(fileHandleChunks(handle, { signal: options.signal }), options.onProgress);
|
|
284
|
+
if (measured.sha256Hex !== record.cipher.ciphertextSha256 || measured.bytes !== record.cipher.storedBytes) {
|
|
285
|
+
fail("ciphertext_integrity_failed", `The sealed bytes for ${archiveId} changed on this disk; nothing was uploaded.`, { archiveId });
|
|
286
|
+
}
|
|
287
|
+
const localChecksum = measured.sha256Base64;
|
|
288
|
+
const providerConfig = remoteStorage;
|
|
289
|
+
const lease = createActiveLease(providerConfig, { archiveId, ciphertextSha256: record.cipher.ciphertextSha256, bytes: measured.bytes, now: options.now, ttlMs: options.ttlMs });
|
|
290
|
+
assertLeaseUsable(lease, options.now);
|
|
291
|
+
// Target-id credentials let one machine connect more than one personal
|
|
292
|
+
// destination. Only the synthesized `primary` target may fall back to the
|
|
293
|
+
// legacy vault-id secret, so upgrades keep working without letting named
|
|
294
|
+
// targets borrow one another's credentials.
|
|
295
|
+
const routedConnection = routedTarget
|
|
296
|
+
? await (async () => {
|
|
297
|
+
const { credentialConfigIdForTarget, endpointOverridesForResolvedTarget } = await import("./storage-targets.js");
|
|
298
|
+
return {
|
|
299
|
+
credentialId: await credentialConfigIdForTarget(dataDir, routedTarget),
|
|
300
|
+
overrides: await endpointOverridesForResolvedTarget(dataDir, routedTarget),
|
|
301
|
+
};
|
|
302
|
+
})()
|
|
303
|
+
: null;
|
|
304
|
+
if (routedTarget && !routedConnection?.credentialId) {
|
|
305
|
+
fail("signer_not_configured", `Storage target "${routedTarget.id}" is not connected on this machine. Connect it in Settings before uploading.`);
|
|
306
|
+
}
|
|
307
|
+
const client = options.client ?? await uploadClientFromStore(dataDir, options.storageConfigId ?? routedConnection?.credentialId ?? config.vaultId, providerConfig,
|
|
308
|
+
// An explicit/first-run endpoint belongs only to the legacy primary.
|
|
309
|
+
// Named targets always sign against their own account routing metadata.
|
|
310
|
+
routedTarget
|
|
311
|
+
? routedTarget.id === "primary" ? (options.overrides ?? routedConnection.overrides) : routedConnection.overrides
|
|
312
|
+
: options.overrides);
|
|
313
|
+
// Every production storage client implements uploadStream. The bounded
|
|
314
|
+
// compatibility branch keeps small injected/test clients working, but a
|
|
315
|
+
// legacy client can never make a multi-GB file become one giant Buffer.
|
|
316
|
+
const streaming = client;
|
|
317
|
+
let uploaded;
|
|
318
|
+
if (typeof streaming.uploadStream === "function") {
|
|
319
|
+
const sentHash = createHash("sha256");
|
|
320
|
+
let sentBytes = 0;
|
|
321
|
+
const body = tapDigest(fileHandleChunks(handle, { signal: options.signal }), sentHash, (bytes) => {
|
|
322
|
+
sentBytes += bytes;
|
|
323
|
+
options.onProgress?.(sentBytes);
|
|
324
|
+
});
|
|
325
|
+
uploaded = await streaming.uploadStream(lease.objectKey, options.maxBytesPerSecond
|
|
326
|
+
? paceBackgroundByteStream(dataDir, body, options.maxBytesPerSecond, {
|
|
327
|
+
signal: options.signal,
|
|
328
|
+
now: options.paceNow,
|
|
329
|
+
sleep: options.paceSleep,
|
|
330
|
+
})
|
|
331
|
+
: body, { bytes: measured.bytes, ...(options.signal ? { signal: options.signal } : {}) });
|
|
332
|
+
if (sentBytes !== measured.bytes || sentHash.digest("hex") !== record.cipher.ciphertextSha256) {
|
|
333
|
+
fail("ciphertext_integrity_failed", `The sealed bytes for ${archiveId} changed while they were uploading; the remote object was not marked durable.`, { archiveId });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
if (measured.bytes > LEGACY_BUFFER_LIMIT_BYTES) {
|
|
338
|
+
fail("provider_unsupported", `The ${client.kind} client cannot stream this ${measured.bytes}-byte archive. Update the client before retrying; nothing was marked durable.`, { archiveId, bytes: measured.bytes });
|
|
339
|
+
}
|
|
340
|
+
const ciphertext = await collectChunks(fileHandleChunks(handle, { signal: options.signal }), LEGACY_BUFFER_LIMIT_BYTES, options.onProgress);
|
|
341
|
+
uploaded = await client.upload(lease, ciphertext);
|
|
342
|
+
}
|
|
343
|
+
const head = await client.head(lease);
|
|
344
|
+
// What the provider should be holding. For a single-part object that is the
|
|
345
|
+
// whole-object SHA-256; for a multipart one it is S3's composite of the part
|
|
346
|
+
// digests, which `upload` returns. Comparing the stored checksum against
|
|
347
|
+
// `localChecksum` unconditionally would fail every multipart archive.
|
|
348
|
+
const expected = uploaded.remoteChecksum;
|
|
349
|
+
const problems = [];
|
|
350
|
+
if (uploaded.bytes !== measured.bytes)
|
|
351
|
+
problems.push(`the upload client consumed ${uploaded.bytes} bytes; ${measured.bytes} were expected`);
|
|
352
|
+
if (!head.exists)
|
|
353
|
+
problems.push("the provider does not report the object as present");
|
|
354
|
+
if (head.bytes !== measured.bytes)
|
|
355
|
+
problems.push(`the provider stored ${head.bytes} bytes; ${measured.bytes} were sent`);
|
|
356
|
+
if (head.checksum === undefined) {
|
|
357
|
+
// Previously this case silently skipped the comparison, which left byte
|
|
358
|
+
// count as the only real check — an endpoint returning the right length of
|
|
359
|
+
// wrong bytes verified clean. Reclaim depends on this verdict, so an
|
|
360
|
+
// unprovable upload is a failed upload.
|
|
361
|
+
problems.push("the provider did not report a stored checksum, so the bytes it holds cannot be proven");
|
|
362
|
+
}
|
|
363
|
+
else if (!expected || head.checksum !== expected) {
|
|
364
|
+
problems.push(`the provider's stored checksum ${head.checksum} does not match the ${expected ?? "missing checksum"} we uploaded`);
|
|
365
|
+
}
|
|
366
|
+
if (problems.length > 0) {
|
|
367
|
+
await recordAudit(dataDir, "upload.verify", "denied", { archiveId, objectKey: lease.objectKey, problems: problems.length });
|
|
368
|
+
fail("ciphertext_integrity_failed", `Refusing to mark ${archiveId} durable: ${problems.join("; ")}`, { archiveId, objectKey: lease.objectKey });
|
|
369
|
+
}
|
|
370
|
+
const storageTargets = await import("./storage-targets.js");
|
|
371
|
+
const explicitEndpoint = options.overrides?.host
|
|
372
|
+
? `${options.overrides.protocol ?? "https"}://${options.overrides.host}${options.overrides.port ? `:${options.overrides.port}` : ""}`
|
|
373
|
+
: undefined;
|
|
374
|
+
const destination = routedTarget
|
|
375
|
+
? await storageTargets.destinationIdentityForTarget(dataDir, routedTarget)
|
|
376
|
+
: storageTargets.storageDestinationIdentity({
|
|
377
|
+
id: targetId ?? "explicit",
|
|
378
|
+
provider: remoteStorage.provider,
|
|
379
|
+
bucket: remoteStorage.bucket,
|
|
380
|
+
prefix: remoteStorage.prefix,
|
|
381
|
+
...(remoteStorage.region ? { region: remoteStorage.region } : {}),
|
|
382
|
+
...(explicitEndpoint ? { endpoint: explicitEndpoint } : {}),
|
|
383
|
+
}, explicitEndpoint);
|
|
384
|
+
const remote = {
|
|
385
|
+
provider: lease.provider,
|
|
386
|
+
bucket: remoteStorage.bucket,
|
|
387
|
+
objectKey: lease.objectKey,
|
|
388
|
+
bytes: measured.bytes,
|
|
389
|
+
checksum: localChecksum,
|
|
390
|
+
verifiedAt: new Date(options.now ?? Date.now()).toISOString(),
|
|
391
|
+
destination,
|
|
392
|
+
destinationFingerprint: storageTargets.storageDestinationFingerprint(destination),
|
|
393
|
+
...(targetId ? { targetId } : {}),
|
|
394
|
+
};
|
|
395
|
+
await mutateArchiveRecord(config.storage.root, archiveId, (current) => isV2(current) ? appendArchiveCopy(current, remote) : current);
|
|
396
|
+
await recordAudit(dataDir, "upload.verify", "allowed", { archiveId, objectKey: lease.objectKey, bytes: measured.bytes, provider: lease.provider });
|
|
397
|
+
if (options.reportTargetUsage !== false)
|
|
398
|
+
void storageTargets.pushTargetUsage(dataDir);
|
|
399
|
+
return { archiveId, objectKey: lease.objectKey, copyId: archiveCopyId(remote), bytes: measured.bytes, checksum: localChecksum, verified: true, provider: lease.provider };
|
|
57
400
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
await writeRecord(config.storage.root, next);
|
|
61
|
-
await recordAudit(dataDir, "upload.verify", "allowed", { archiveId, objectKey: lease.objectKey, bytes: ciphertext.length, provider: lease.provider });
|
|
62
|
-
{
|
|
63
|
-
const { pushTargetUsage } = await import("./storage-targets.js");
|
|
64
|
-
void pushTargetUsage(dataDir);
|
|
401
|
+
finally {
|
|
402
|
+
await handle.close();
|
|
65
403
|
}
|
|
66
|
-
return { archiveId, objectKey: lease.objectKey, bytes: ciphertext.length, checksum: localChecksum, verified: true, provider: lease.provider };
|
|
67
404
|
}
|
|
68
405
|
/** Uploads every archive that has no verified remote object yet. */
|
|
69
406
|
export async function uploadPending(dataDir, options = {}) {
|
|
70
|
-
const records = await listArchives(dataDir);
|
|
407
|
+
const records = (await listArchives(dataDir)).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
|
|
71
408
|
const uploaded = [];
|
|
72
409
|
const failed = [];
|
|
73
410
|
for (const record of records) {
|
|
74
|
-
if (isV2(record) && record
|
|
411
|
+
if (isV2(record) && hasVerifiedCopy(record))
|
|
75
412
|
continue;
|
|
76
413
|
try {
|
|
77
|
-
|
|
414
|
+
const outcome = await uploadArchive(dataDir, record.id, options);
|
|
415
|
+
if (!outcome.verified)
|
|
416
|
+
throw new Error("storage did not verify this upload as durable");
|
|
417
|
+
uploaded.push(outcome);
|
|
78
418
|
}
|
|
79
419
|
catch (error) {
|
|
80
420
|
failed.push({ archiveId: record.id, error: error instanceof Error ? error.message : "upload failed" });
|