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
package/dist/src/providers/s3.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { fail } from "../errors.js";
|
|
3
3
|
import { presign } from "./sigv4.js";
|
|
4
|
+
import { collectChunks, responseBodyChunks } from "../byte-stream.js";
|
|
4
5
|
export const DEFAULT_PART_BYTES = 8 * 1024 * 1024;
|
|
5
6
|
export const MIN_PART_BYTES = 5 * 1024 * 1024;
|
|
6
7
|
const sha256b64 = (body) => createHash("sha256").update(body).digest("base64");
|
|
@@ -88,14 +89,16 @@ export class S3UploadClient {
|
|
|
88
89
|
endpoint;
|
|
89
90
|
credentials;
|
|
90
91
|
partBytes;
|
|
92
|
+
fetchImpl;
|
|
91
93
|
constructor(kind, endpoint, credentials, partBytes = DEFAULT_PART_BYTES,
|
|
92
94
|
/** S3 requires every part except the last to be at least 5 MiB. Local emulators and
|
|
93
95
|
* MinIO do not, so this is the deliberate opt-out for those targets only. */
|
|
94
|
-
allowSmallParts = false) {
|
|
96
|
+
allowSmallParts = false, fetchImpl = fetch) {
|
|
95
97
|
this.kind = kind;
|
|
96
98
|
this.endpoint = endpoint;
|
|
97
99
|
this.credentials = credentials;
|
|
98
100
|
this.partBytes = partBytes;
|
|
101
|
+
this.fetchImpl = fetchImpl;
|
|
99
102
|
if (partBytes < MIN_PART_BYTES && !allowSmallParts)
|
|
100
103
|
fail("invalid_argument", `S3 multipart parts must be at least ${MIN_PART_BYTES} bytes`, { partBytes });
|
|
101
104
|
if (partBytes <= 0)
|
|
@@ -105,10 +108,10 @@ export class S3UploadClient {
|
|
|
105
108
|
const { host, path } = target(this.endpoint, key);
|
|
106
109
|
return presign({ method, host, port: this.endpoint.port, protocol: this.endpoint.protocol, path, region: this.endpoint.region, credentials: this.credentials, query, headers, expiresInSeconds });
|
|
107
110
|
}
|
|
108
|
-
async put(key, body, query = {}) {
|
|
111
|
+
async put(key, body, query = {}, signal) {
|
|
109
112
|
const checksum = sha256b64(body);
|
|
110
113
|
const { url } = this.sign("PUT", key, query, { "x-amz-checksum-sha256": checksum });
|
|
111
|
-
const response = await
|
|
114
|
+
const response = await this.fetchImpl(url, { method: "PUT", body: new Uint8Array(body), headers: { "x-amz-checksum-sha256": checksum }, signal });
|
|
112
115
|
if (!response.ok)
|
|
113
116
|
fail("internal", `Upload failed for ${key}: ${await readError(response)}`, { key, status: response.status });
|
|
114
117
|
return { etag: (response.headers.get("etag") ?? "").replace(/"/g, ""), checksum };
|
|
@@ -119,44 +122,51 @@ export class S3UploadClient {
|
|
|
119
122
|
* a plain checksummed PUT does everything multipart machinery did — which
|
|
120
123
|
* is exactly why the chunk layout needs none of it.
|
|
121
124
|
*/
|
|
122
|
-
async putObject(key, body) {
|
|
123
|
-
const { checksum } = await this.put(key, body);
|
|
125
|
+
async putObject(key, body, options = {}) {
|
|
126
|
+
const { checksum } = await this.put(key, body, {}, options.signal);
|
|
124
127
|
return { checksum };
|
|
125
128
|
}
|
|
126
|
-
async headObject(key) {
|
|
129
|
+
async headObject(key, options = {}) {
|
|
127
130
|
const { url } = this.sign("HEAD", key, {}, { "x-amz-checksum-mode": "ENABLED" });
|
|
128
|
-
const response = await
|
|
131
|
+
const response = await this.fetchImpl(url, { method: "HEAD", headers: { "x-amz-checksum-mode": "ENABLED" }, signal: options.signal });
|
|
129
132
|
if (response.status === 404)
|
|
130
133
|
return { exists: false, bytes: 0 };
|
|
131
134
|
if (!response.ok)
|
|
132
135
|
fail("internal", `Could not read ${key}: ${response.status} ${response.statusText}`, { key, status: response.status });
|
|
133
136
|
return { exists: true, bytes: Number(response.headers.get("content-length") ?? "0"), checksum: response.headers.get("x-amz-checksum-sha256") ?? undefined };
|
|
134
137
|
}
|
|
135
|
-
async getObject(key) {
|
|
138
|
+
async getObject(key, options = {}) {
|
|
139
|
+
return collectChunks(await this.downloadKeyStream(key, options.signal), Number.MAX_SAFE_INTEGER);
|
|
140
|
+
}
|
|
141
|
+
/** Object-key streaming counterpart used by the background index store. */
|
|
142
|
+
async getObjectStream(key, options = {}) {
|
|
143
|
+
return this.downloadKeyStream(key, options.signal);
|
|
144
|
+
}
|
|
145
|
+
async downloadKeyStream(key, signal) {
|
|
136
146
|
const { url } = this.sign("GET", key, {});
|
|
137
|
-
const response = await
|
|
147
|
+
const response = await this.fetchImpl(url, { signal });
|
|
138
148
|
if (!response.ok)
|
|
139
149
|
fail("internal", `Could not download ${key}: ${await readError(response)}`, { key, status: response.status });
|
|
140
|
-
return
|
|
150
|
+
return responseBodyChunks(response);
|
|
141
151
|
}
|
|
142
152
|
async deleteObject(key) {
|
|
143
153
|
const { url } = this.sign("DELETE", key, {});
|
|
144
|
-
const response = await
|
|
154
|
+
const response = await this.fetchImpl(url, { method: "DELETE" });
|
|
145
155
|
if (!response.ok && response.status !== 404)
|
|
146
156
|
fail("internal", `Could not delete ${key}: ${await readError(response)}`, { key, status: response.status });
|
|
147
157
|
}
|
|
148
|
-
async initiateMultipart(key) {
|
|
158
|
+
async initiateMultipart(key, signal) {
|
|
149
159
|
const { url } = this.sign("POST", key, { uploads: "" });
|
|
150
|
-
const response = await
|
|
160
|
+
const response = await this.fetchImpl(url, { method: "POST", signal });
|
|
151
161
|
if (!response.ok)
|
|
152
162
|
fail("internal", `Could not start a multipart upload for ${key}: ${await readError(response)}`, { key });
|
|
153
163
|
const uploadId = xmlValue(await response.text(), "UploadId");
|
|
154
164
|
return uploadId ?? fail("internal", `Multipart initiation for ${key} returned no UploadId`, { key });
|
|
155
165
|
}
|
|
156
|
-
async completeMultipart(key, uploadId, parts) {
|
|
166
|
+
async completeMultipart(key, uploadId, parts, signal) {
|
|
157
167
|
const body = Buffer.from(`<CompleteMultipartUpload>${parts.map((part) => `<Part><PartNumber>${part.partNumber}</PartNumber><ETag>"${part.etag}"</ETag><ChecksumSHA256>${part.checksum}</ChecksumSHA256></Part>`).join("")}</CompleteMultipartUpload>`);
|
|
158
168
|
const { url } = this.sign("POST", key, { uploadId });
|
|
159
|
-
const response = await
|
|
169
|
+
const response = await this.fetchImpl(url, { method: "POST", body: new Uint8Array(body), signal });
|
|
160
170
|
if (!response.ok)
|
|
161
171
|
fail("internal", `Could not complete the multipart upload for ${key}: ${await readError(response)}`, { key });
|
|
162
172
|
const text = await response.text();
|
|
@@ -173,7 +183,7 @@ export class S3UploadClient {
|
|
|
173
183
|
*/
|
|
174
184
|
async listParts(key, uploadId) {
|
|
175
185
|
const { url } = this.sign("GET", key, { uploadId });
|
|
176
|
-
const response = await
|
|
186
|
+
const response = await this.fetchImpl(url);
|
|
177
187
|
if (!response.ok)
|
|
178
188
|
fail("internal", `Cannot resume this upload: the provider would not list its parts (${await readError(response)})`, { key, uploadId });
|
|
179
189
|
const text = await response.text();
|
|
@@ -190,9 +200,9 @@ export class S3UploadClient {
|
|
|
190
200
|
parts.sort((a, b) => a.partNumber - b.partNumber);
|
|
191
201
|
return parts;
|
|
192
202
|
}
|
|
193
|
-
async abortMultipart(key, uploadId) {
|
|
203
|
+
async abortMultipart(key, uploadId, signal) {
|
|
194
204
|
const { url } = this.sign("DELETE", key, { uploadId });
|
|
195
|
-
await
|
|
205
|
+
await this.fetchImpl(url, { method: "DELETE", signal }).catch(() => undefined);
|
|
196
206
|
}
|
|
197
207
|
/** Uploads ciphertext and returns the checksum the provider confirmed. */
|
|
198
208
|
async upload(lease, ciphertext) {
|
|
@@ -270,6 +280,7 @@ export class S3UploadClient {
|
|
|
270
280
|
};
|
|
271
281
|
try {
|
|
272
282
|
for await (const chunk of chunks) {
|
|
283
|
+
options.signal?.throwIfAborted();
|
|
273
284
|
if (completionOnly && chunk.length > 0) {
|
|
274
285
|
fail("invalid_argument", "Cannot resume this upload with more data: the seeded parts already end in the object's final short part, so the only thing left is completion.", { key });
|
|
275
286
|
}
|
|
@@ -281,8 +292,8 @@ export class S3UploadClient {
|
|
|
281
292
|
const part = buffered.subarray(0, this.partBytes);
|
|
282
293
|
held = part.length < buffered.length ? [buffered.subarray(this.partBytes)] : [];
|
|
283
294
|
heldBytes = buffered.length - part.length;
|
|
284
|
-
uploadId = uploadId ?? await this.initiateMultipart(key);
|
|
285
|
-
const { etag, checksum } = await this.put(key, part, { partNumber: String(parts.length + 1), uploadId });
|
|
295
|
+
uploadId = uploadId ?? await this.initiateMultipart(key, options.signal);
|
|
296
|
+
const { etag, checksum } = await this.put(key, part, { partNumber: String(parts.length + 1), uploadId }, options.signal);
|
|
286
297
|
parts.push({ partNumber: parts.length + 1, etag, checksum, bytes: part.length });
|
|
287
298
|
await journal(uploadId);
|
|
288
299
|
}
|
|
@@ -292,15 +303,15 @@ export class S3UploadClient {
|
|
|
292
303
|
// The whole object fit inside one part: a single PUT stores it with a
|
|
293
304
|
// whole-object checksum, exactly like the buffered small-object path.
|
|
294
305
|
// (Never reached on resume — a seeded uploadId always exists there.)
|
|
295
|
-
const { checksum } = await this.put(key, tail);
|
|
306
|
+
const { checksum } = await this.put(key, tail, {}, options.signal);
|
|
296
307
|
return { remoteChecksum: checksum, bytes: total };
|
|
297
308
|
}
|
|
298
309
|
if (tail.length > 0 || parts.length === 0) {
|
|
299
|
-
const { etag, checksum } = await this.put(key, tail, { partNumber: String(parts.length + 1), uploadId });
|
|
310
|
+
const { etag, checksum } = await this.put(key, tail, { partNumber: String(parts.length + 1), uploadId }, options.signal);
|
|
300
311
|
parts.push({ partNumber: parts.length + 1, etag, checksum, bytes: tail.length });
|
|
301
312
|
await journal(uploadId);
|
|
302
313
|
}
|
|
303
|
-
await this.completeMultipart(key, uploadId, parts);
|
|
314
|
+
await this.completeMultipart(key, uploadId, parts, options.signal);
|
|
304
315
|
return { remoteChecksum: compositeChecksum(parts.map((part) => part.checksum)), bytes: total };
|
|
305
316
|
}
|
|
306
317
|
catch (error) {
|
|
@@ -317,12 +328,12 @@ export class S3UploadClient {
|
|
|
317
328
|
* GET with the short-lived credential it was vended, exactly as it does for
|
|
318
329
|
* the upload.
|
|
319
330
|
*/
|
|
320
|
-
async download(lease) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return
|
|
331
|
+
async download(lease, options = {}) {
|
|
332
|
+
return collectChunks(await this.downloadStream(lease, options), Number.MAX_SAFE_INTEGER);
|
|
333
|
+
}
|
|
334
|
+
/** The production restore/transfer door: response chunks flow straight to disk. */
|
|
335
|
+
async downloadStream(lease, options = {}) {
|
|
336
|
+
return this.downloadKeyStream(lease.objectKey, options.signal);
|
|
326
337
|
}
|
|
327
338
|
/**
|
|
328
339
|
* Confirms the object exists remotely and reports what the provider stored.
|
|
@@ -335,7 +346,7 @@ export class S3UploadClient {
|
|
|
335
346
|
async head(lease) {
|
|
336
347
|
const mode = { "x-amz-checksum-mode": "ENABLED" };
|
|
337
348
|
const { url } = this.sign("HEAD", lease.objectKey, {}, mode);
|
|
338
|
-
const response = await
|
|
349
|
+
const response = await this.fetchImpl(url, { method: "HEAD", headers: mode });
|
|
339
350
|
if (response.status === 404)
|
|
340
351
|
return { exists: false, bytes: 0 };
|
|
341
352
|
if (!response.ok)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type StorageAddressResolver } from "../storage-endpoint.js";
|
|
2
|
+
export type StorageFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
3
|
+
/**
|
|
4
|
+
* Fetches only from one already-parsed storage origin. DNS policy executes in
|
|
5
|
+
* Undici's connector lookup, so the addresses it approves are the exact ones
|
|
6
|
+
* handed to the socket. The URL hostname remains untouched: signed Host,
|
|
7
|
+
* HTTPS SNI, and certificate hostname verification continue to use it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createPinnedStorageFetch(endpoint: string | URL, options?: {
|
|
10
|
+
allowPrivate?: boolean;
|
|
11
|
+
resolver?: StorageAddressResolver;
|
|
12
|
+
}): StorageFetch;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
3
|
+
import { fail } from "../errors.js";
|
|
4
|
+
import { allowedStorageAddresses, parseStorageEndpoint, systemStorageAddressResolver, } from "../storage-endpoint.js";
|
|
5
|
+
function guardedLookup(allowPrivate, resolver) {
|
|
6
|
+
return (hostname, options, callback) => {
|
|
7
|
+
void allowedStorageAddresses(hostname, { allowPrivate, resolver }).then((answers) => {
|
|
8
|
+
const family = options.family === 4 || options.family === 6 ? options.family : 0;
|
|
9
|
+
const eligible = family ? answers.filter((answer) => answer.family === family) : answers;
|
|
10
|
+
if (eligible.length === 0) {
|
|
11
|
+
const error = new Error(`Storage endpoint ${hostname} has no address in the requested network family.`);
|
|
12
|
+
error.code = "EAI_ADDRFAMILY";
|
|
13
|
+
callback(error, "", 0);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (options.all)
|
|
17
|
+
callback(null, eligible);
|
|
18
|
+
else
|
|
19
|
+
callback(null, eligible[0].address, eligible[0].family);
|
|
20
|
+
}, (error) => callback(error instanceof Error ? error : new Error("Storage endpoint DNS validation failed."), "", 0));
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const dispatchers = new WeakMap();
|
|
24
|
+
function dispatcherFor(resolver, allowPrivate) {
|
|
25
|
+
let pair = dispatchers.get(resolver);
|
|
26
|
+
if (!pair) {
|
|
27
|
+
pair = {
|
|
28
|
+
public: new Agent({ connect: { lookup: guardedLookup(false, resolver) } }),
|
|
29
|
+
private: new Agent({ connect: { lookup: guardedLookup(true, resolver) } }),
|
|
30
|
+
};
|
|
31
|
+
dispatchers.set(resolver, pair);
|
|
32
|
+
}
|
|
33
|
+
return allowPrivate ? pair.private : pair.public;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Fetches only from one already-parsed storage origin. DNS policy executes in
|
|
37
|
+
* Undici's connector lookup, so the addresses it approves are the exact ones
|
|
38
|
+
* handed to the socket. The URL hostname remains untouched: signed Host,
|
|
39
|
+
* HTTPS SNI, and certificate hostname verification continue to use it.
|
|
40
|
+
*/
|
|
41
|
+
export function createPinnedStorageFetch(endpoint, options = {}) {
|
|
42
|
+
const expected = parseStorageEndpoint(String(endpoint));
|
|
43
|
+
const allowPrivate = options.allowPrivate === true;
|
|
44
|
+
const resolver = options.resolver ?? systemStorageAddressResolver;
|
|
45
|
+
const dispatcher = dispatcherFor(resolver, allowPrivate);
|
|
46
|
+
return async (input, init = {}) => {
|
|
47
|
+
let requested;
|
|
48
|
+
try {
|
|
49
|
+
requested = input instanceof URL ? new URL(input.href) : new URL(input);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return fail("invalid_argument", "Storage request did not contain a valid URL.");
|
|
53
|
+
}
|
|
54
|
+
if (requested.origin !== expected.origin) {
|
|
55
|
+
fail("invalid_argument", "Storage request tried to leave its configured endpoint; cross-origin storage requests are refused.");
|
|
56
|
+
}
|
|
57
|
+
// net.connect may bypass lookup for a literal. Apply the same rule before
|
|
58
|
+
// Undici can open that socket.
|
|
59
|
+
const literal = requested.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
60
|
+
if (isIP(literal))
|
|
61
|
+
await allowedStorageAddresses(literal, { allowPrivate, resolver });
|
|
62
|
+
const requestInit = {
|
|
63
|
+
...init,
|
|
64
|
+
// A provider redirect can otherwise escape the origin check internally.
|
|
65
|
+
// Signed storage endpoints should already be final; fail closed on 3xx.
|
|
66
|
+
redirect: "manual",
|
|
67
|
+
dispatcher,
|
|
68
|
+
};
|
|
69
|
+
const response = await undiciFetch(requested, requestInit);
|
|
70
|
+
return response;
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/src/queue.d.ts
CHANGED
|
@@ -46,13 +46,29 @@ export type EnqueueRequest = {
|
|
|
46
46
|
event: string;
|
|
47
47
|
sessionId?: string;
|
|
48
48
|
};
|
|
49
|
+
export type EnqueueResult = {
|
|
50
|
+
job: ArchiveJob;
|
|
51
|
+
deduped: boolean;
|
|
52
|
+
superseded: string[];
|
|
53
|
+
};
|
|
54
|
+
export type BatchEnqueueOutcome = ({
|
|
55
|
+
ok: true;
|
|
56
|
+
request: EnqueueRequest;
|
|
57
|
+
} & EnqueueResult) | {
|
|
58
|
+
ok: false;
|
|
59
|
+
request: EnqueueRequest;
|
|
60
|
+
error: unknown;
|
|
61
|
+
};
|
|
49
62
|
export type QueueOptions = {
|
|
50
63
|
now?: () => number;
|
|
51
64
|
leaseMs?: number;
|
|
52
65
|
maxAttempts?: number;
|
|
53
66
|
backoffMs?: number;
|
|
54
67
|
maxBackoffMs?: number;
|
|
68
|
+
/** Real-time bound for one tiny queue-record state transition. */
|
|
69
|
+
lockAcquireMs?: number;
|
|
55
70
|
};
|
|
71
|
+
export type ClaimEligibility = (job: ArchiveJob) => boolean;
|
|
56
72
|
export declare function queueDir(dataDir: string): string;
|
|
57
73
|
/**
|
|
58
74
|
* Two lifecycle events describing the same unchanged transcript produce the same
|
|
@@ -76,14 +92,53 @@ export declare class ArchiveQueue {
|
|
|
76
92
|
private readonly maxAttempts;
|
|
77
93
|
private readonly backoffMs;
|
|
78
94
|
private readonly maxBackoffMs;
|
|
95
|
+
private readonly lockAcquireMs;
|
|
96
|
+
/**
|
|
97
|
+
* `hasClaimable` already opened and parsed this record. Keep that answer for
|
|
98
|
+
* the immediately following claim so a mature queue does not enumerate and
|
|
99
|
+
* parse every completed job a second time merely to lease the work it just
|
|
100
|
+
* found. The hint is deliberately in-memory: the job file remains the
|
|
101
|
+
* durable source of truth and is revalidated before a lease is written.
|
|
102
|
+
*/
|
|
103
|
+
private claimableHint?;
|
|
104
|
+
/** One startup reconciliation absorbs queue files written by older builds. */
|
|
105
|
+
private reconciledBacklog;
|
|
79
106
|
constructor(dataDir: string, options?: QueueOptions);
|
|
80
107
|
private path;
|
|
108
|
+
private lockPath;
|
|
81
109
|
private stamp;
|
|
110
|
+
/**
|
|
111
|
+
* Serialises one job's read-check-write transition across every Sealkeep
|
|
112
|
+
* process on this machine. Atomic rename protects readers from partial JSON,
|
|
113
|
+
* but it is not compare-and-swap: without this lock, a delayed renewal can
|
|
114
|
+
* overwrite a newer worker's reclaimed lease, and a delayed progress write
|
|
115
|
+
* can resurrect a job after completion.
|
|
116
|
+
*/
|
|
117
|
+
private withJobLock;
|
|
82
118
|
private writeAtomic;
|
|
83
119
|
/** Creates the file only if the id is unseen; the content is complete before it becomes visible. */
|
|
84
120
|
private createExclusive;
|
|
85
121
|
get(id: string): Promise<ArchiveJob | null>;
|
|
86
122
|
private require;
|
|
123
|
+
private readQueueDirectory;
|
|
124
|
+
private readQueueFile;
|
|
125
|
+
private scanJobs;
|
|
126
|
+
/**
|
|
127
|
+
* Atomic job replacement changes the queue directory's mtime/ctime. Reading
|
|
128
|
+
* those at nanosecond precision gives all Sealkeep processes a cheap durable
|
|
129
|
+
* generation check: sequential dashboard endpoints can reuse parsed jobs,
|
|
130
|
+
* while a hook/worker in another process invalidates that snapshot merely by
|
|
131
|
+
* committing its ordinary rename.
|
|
132
|
+
*/
|
|
133
|
+
private directoryGeneration;
|
|
134
|
+
private startSharedScan;
|
|
135
|
+
private readJobsSnapshot;
|
|
136
|
+
/**
|
|
137
|
+
* Returns rows only when one durable directory generation covered the whole
|
|
138
|
+
* scan. `enqueueMany` uses this as a read-only fast path for settled ids; an
|
|
139
|
+
* unstable snapshot is never evidence that a job already exists.
|
|
140
|
+
*/
|
|
141
|
+
private stableJobsSnapshot;
|
|
87
142
|
list(filter?: {
|
|
88
143
|
status?: JobStatus;
|
|
89
144
|
}): Promise<ArchiveJob[]>;
|
|
@@ -92,16 +147,29 @@ export declare class ArchiveQueue {
|
|
|
92
147
|
ready: number;
|
|
93
148
|
leased: number;
|
|
94
149
|
done: number;
|
|
150
|
+
completed: number;
|
|
151
|
+
superseded: number;
|
|
95
152
|
failed: number;
|
|
96
153
|
}>;
|
|
154
|
+
/**
|
|
155
|
+
* Answers the worker's startup question from the same generation-validated
|
|
156
|
+
* snapshot used by the local API. A mature vault can have thousands of
|
|
157
|
+
* completed job files; independently reading every one merely to learn that
|
|
158
|
+
* one ready job exists made preservation compete with the status UI.
|
|
159
|
+
*
|
|
160
|
+
* Expired leases count too: after a crash they are work the next worker can
|
|
161
|
+
* reclaim, even though a plain `stats().ready` check would call the queue
|
|
162
|
+
* empty and wait for discovery first.
|
|
163
|
+
*/
|
|
164
|
+
hasClaimable(eligible?: ClaimEligibility): Promise<boolean>;
|
|
97
165
|
/**
|
|
98
166
|
* Whether `candidate` describes a later state of the transcript than `over`.
|
|
99
167
|
*
|
|
100
168
|
* A total order over snapshots of one file, so two hook processes enqueueing
|
|
101
169
|
* at once reach the same verdict and the most advanced snapshot is never the
|
|
102
|
-
* one superseded.
|
|
103
|
-
*
|
|
104
|
-
* the same content address, which is the same job.
|
|
170
|
+
* one superseded. Modification time leads because compaction can make a later
|
|
171
|
+
* transcript smaller; byte length settles the rare same-time observation.
|
|
172
|
+
* Equal on both means the same content address, which is the same job.
|
|
105
173
|
*/
|
|
106
174
|
private advances;
|
|
107
175
|
/**
|
|
@@ -117,16 +185,46 @@ export declare class ArchiveQueue {
|
|
|
117
185
|
*
|
|
118
186
|
* Nothing is lost by dropping the earlier job: the bytes it would have
|
|
119
187
|
* archived are the bytes still on disk, and the surviving job archives those.
|
|
120
|
-
*
|
|
121
|
-
* a done or failed job
|
|
188
|
+
* Ready jobs and abandoned expired leases may be retired; an active lease may
|
|
189
|
+
* already be mid-archive, and a done or failed job records something that
|
|
190
|
+
* actually happened.
|
|
122
191
|
*/
|
|
123
|
-
private
|
|
192
|
+
private sourceKey;
|
|
193
|
+
/**
|
|
194
|
+
* Retires older ready snapshots for a whole discovery pass with one queue read.
|
|
195
|
+
*
|
|
196
|
+
* `enqueue()` used to call `list()` once per discovered transcript. A startup
|
|
197
|
+
* scan of N transcripts against Q queue records therefore parsed N*Q JSON
|
|
198
|
+
* files before it could seal anything. All prospective jobs are made visible
|
|
199
|
+
* first, then this method indexes the queue once and applies the same
|
|
200
|
+
* source-local ordering rule.
|
|
201
|
+
*
|
|
202
|
+
* A hook in another process remains safe: job creation is still exclusive. A
|
|
203
|
+
* hook that lands before this snapshot is included here; one that lands after
|
|
204
|
+
* it sees every batch job in its own reconciliation. Re-read before writing so
|
|
205
|
+
* a job claimed by a worker since the snapshot is never knowingly retired.
|
|
206
|
+
*/
|
|
207
|
+
private supersedeMany;
|
|
208
|
+
/**
|
|
209
|
+
* Settles obsolete observations after a worker archives the live path.
|
|
210
|
+
*
|
|
211
|
+
* Queue rows contain metadata and a path, not a private copy of those old
|
|
212
|
+
* bytes. If an older row was already leased when discovery observed a newer
|
|
213
|
+
* state, that worker reads the newer live state and its completion covers
|
|
214
|
+
* the newer ready row too. Active *other* leases remain untouched; they may
|
|
215
|
+
* already have captured their own bounded snapshot.
|
|
216
|
+
*/
|
|
217
|
+
private settleCoveredSourceSnapshots;
|
|
218
|
+
private describe;
|
|
219
|
+
private persist;
|
|
124
220
|
/** Records intent to archive a transcript. It never reads transcript contents. */
|
|
125
|
-
enqueue(request: EnqueueRequest): Promise<
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
221
|
+
enqueue(request: EnqueueRequest): Promise<EnqueueResult>;
|
|
222
|
+
/**
|
|
223
|
+
* Batch form used by startup/rescan discovery. Individual unreadable sources
|
|
224
|
+
* are reported without aborting the rest of the pass, matching the watcher's
|
|
225
|
+
* previous per-file failure isolation.
|
|
226
|
+
*/
|
|
227
|
+
enqueueMany(requests: readonly EnqueueRequest[]): Promise<BatchEnqueueOutcome[]>;
|
|
130
228
|
private claimable;
|
|
131
229
|
/** Claims from this instance run one at a time; see `claim`. */
|
|
132
230
|
private claimTurn;
|
|
@@ -140,18 +238,38 @@ export declare class ArchiveQueue {
|
|
|
140
238
|
* several workers against the same instance, and two claims listing the
|
|
141
239
|
* directory at the same moment would pick the same candidate and both write
|
|
142
240
|
* a lease on it — the loser then failing its own job with a lease error.
|
|
143
|
-
* Serialising costs nothing next to the seal that follows
|
|
144
|
-
*
|
|
145
|
-
* and
|
|
241
|
+
* Serialising costs nothing next to the seal that follows. The per-job
|
|
242
|
+
* filesystem lock inside `claimNext` extends that same exclusivity across
|
|
243
|
+
* hook, daemon and manual-command processes; atomic rename alone cannot act
|
|
244
|
+
* as compare-and-swap and previously let both callers believe they won.
|
|
146
245
|
*/
|
|
147
|
-
claim(owner: string, excluding?: ReadonlySet<string
|
|
246
|
+
claim(owner: string, excluding?: ReadonlySet<string>, eligible?: ClaimEligibility): Promise<ArchiveJob | null>;
|
|
148
247
|
private claimNext;
|
|
248
|
+
private assertLeaseOwner;
|
|
149
249
|
private assertLease;
|
|
250
|
+
/**
|
|
251
|
+
* How often a worker should prove it is still alive while holding a lease.
|
|
252
|
+
* Kept below one third of the lease, with a one-minute ceiling for the
|
|
253
|
+
* ordinary five-minute lease. Tests and small embedded queues can shorten
|
|
254
|
+
* the lease without having to guess a separate heartbeat setting.
|
|
255
|
+
*/
|
|
256
|
+
leaseHeartbeatMs(): number;
|
|
257
|
+
/**
|
|
258
|
+
* Extends an unchanged lease without changing its owner, attempt count or
|
|
259
|
+
* original acquisition time. A delayed event loop may reach this write just
|
|
260
|
+
* after the deadline. The per-job lock makes that safe: if another worker
|
|
261
|
+
* reclaimed first, its new lease id wins and this renewal is refused; if the
|
|
262
|
+
* id is still ours, nobody took the work and renewal prevents a false orphan.
|
|
263
|
+
*/
|
|
264
|
+
renew(id: string, leaseId: string): Promise<ArchiveJob>;
|
|
150
265
|
complete(id: string, leaseId: string, result: {
|
|
151
266
|
archiveId: string;
|
|
152
267
|
ciphertextSha256: string;
|
|
153
268
|
bytes: number;
|
|
154
269
|
deduplicated?: boolean;
|
|
270
|
+
}, sourceSnapshot?: {
|
|
271
|
+
bytes: number;
|
|
272
|
+
modifiedAt: string;
|
|
155
273
|
}): Promise<ArchiveJob>;
|
|
156
274
|
/**
|
|
157
275
|
* Puts a claimed job back without spending an attempt.
|
|
@@ -165,10 +283,9 @@ export declare class ArchiveQueue {
|
|
|
165
283
|
/** Progress on the job being worked — visible, not merely true. */
|
|
166
284
|
progress(id: string, leaseId: string, bytes: number, of: number): Promise<void>;
|
|
167
285
|
/**
|
|
168
|
-
* Takes a waiting job back out of the queue.
|
|
169
|
-
* can go
|
|
170
|
-
* on its own
|
|
171
|
-
* and removing that is a different, bigger decision made elsewhere.
|
|
286
|
+
* Takes a waiting job back out of the queue. Ready, failed, and expired-lease
|
|
287
|
+
* jobs can go. An unexpired lease is still owned by a worker and must finish
|
|
288
|
+
* or fail on its own; a done row is an archive and belongs to the vault.
|
|
172
289
|
*/
|
|
173
290
|
remove(id: string): Promise<ArchiveJob>;
|
|
174
291
|
release(id: string, leaseId: string, reason?: {
|