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
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** A project identity already detected from a live, currently existing folder. */
|
|
2
|
+
export type DetectedProject = {
|
|
3
|
+
project: string;
|
|
4
|
+
projectKey: string;
|
|
5
|
+
projectPath: string;
|
|
6
|
+
};
|
|
7
|
+
export type HistoricalProjectRepairCandidate = {
|
|
8
|
+
/** Stable identity recorded by the archive, when one was available. */
|
|
9
|
+
historicalProjectKey: string | null;
|
|
10
|
+
/** Display-only label recorded by the archive. */
|
|
11
|
+
historicalProject: string | null;
|
|
12
|
+
/** The old identity is not among the supplied live folder identities. */
|
|
13
|
+
originalFolderAvailable: false;
|
|
14
|
+
reason: "folder-not-detected" | "no-stable-project-key" | "already-scoped";
|
|
15
|
+
archiveCount: number;
|
|
16
|
+
/** Distinct source transcript paths, not snapshots/delta links. */
|
|
17
|
+
sessionCount: number;
|
|
18
|
+
/** Exact choices; labels never select a replacement. Empty for legacy rows. */
|
|
19
|
+
replacementOptions: DetectedProject[];
|
|
20
|
+
repairable: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type HistoricalProjectRepairRequest = {
|
|
23
|
+
historicalProjectKey: string;
|
|
24
|
+
replacementProjectKey: string;
|
|
25
|
+
};
|
|
26
|
+
export type HistoricalProjectRepairPreview = {
|
|
27
|
+
candidate: HistoricalProjectRepairCandidate;
|
|
28
|
+
replacement: DetectedProject;
|
|
29
|
+
/** True only when the replacement already belongs to a local team space. */
|
|
30
|
+
teamSpaceBound: true;
|
|
31
|
+
/** The exact opaque space id used by the alias; not archive key material. */
|
|
32
|
+
spaceKey: string;
|
|
33
|
+
};
|
|
34
|
+
export type HistoricalProjectRepairResult = HistoricalProjectRepairPreview & {
|
|
35
|
+
applied: true;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Finds archive identities that are no longer represented by a live folder.
|
|
39
|
+
*
|
|
40
|
+
* The caller supplies the same exact-key detections used by the dashboard's
|
|
41
|
+
* project picker. This function deliberately does not derive a replacement
|
|
42
|
+
* from a label, path basename, or nearest folder. Legacy archives without a
|
|
43
|
+
* stable key are surfaced for review but are never repairable automatically.
|
|
44
|
+
*/
|
|
45
|
+
export declare function listHistoricalProjectRepairCandidates(dataDir: string, detectedProjects: readonly DetectedProject[]): Promise<HistoricalProjectRepairCandidate[]>;
|
|
46
|
+
/** Builds a no-write impact preview for one exact historical-to-live mapping. */
|
|
47
|
+
export declare function previewHistoricalProjectRepair(dataDir: string, request: HistoricalProjectRepairRequest, detectedProjects: readonly DetectedProject[]): Promise<HistoricalProjectRepairPreview>;
|
|
48
|
+
/**
|
|
49
|
+
* Adds the old stable key as a local alias of the replacement's existing team
|
|
50
|
+
* space. Confirmation is mandatory; no archive sidecar or ciphertext is
|
|
51
|
+
* opened or rewritten. The normal team backfill then handles encrypted copies.
|
|
52
|
+
*/
|
|
53
|
+
export declare function applyHistoricalProjectRepair(dataDir: string, request: HistoricalProjectRepairRequest, detectedProjects: readonly DetectedProject[], options: {
|
|
54
|
+
confirm: boolean;
|
|
55
|
+
}): Promise<HistoricalProjectRepairResult>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import { listArchives, bindTeamSpace, readConfig, teamSpaceOf } from "./vault.js";
|
|
3
|
+
import { projectIdentityOfRecord } from "./adapters.js";
|
|
4
|
+
import { fail } from "./errors.js";
|
|
5
|
+
const STABLE_PROJECT_KEY = /^(?:g1|p1):[a-f0-9]{64}$/;
|
|
6
|
+
function assertStableProjectKey(value, label) {
|
|
7
|
+
if (!STABLE_PROJECT_KEY.test(value))
|
|
8
|
+
fail("invalid_argument", `${label} must be a stable project key`);
|
|
9
|
+
}
|
|
10
|
+
function projectOfRecord(record) {
|
|
11
|
+
const project = "project" in record.source && typeof record.source.project === "string"
|
|
12
|
+
? record.source.project
|
|
13
|
+
: null;
|
|
14
|
+
const projectKey = "projectKey" in record.source && typeof record.source.projectKey === "string"
|
|
15
|
+
? record.source.projectKey
|
|
16
|
+
: null;
|
|
17
|
+
return { project, projectKey };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Finds archive identities that are no longer represented by a live folder.
|
|
21
|
+
*
|
|
22
|
+
* The caller supplies the same exact-key detections used by the dashboard's
|
|
23
|
+
* project picker. This function deliberately does not derive a replacement
|
|
24
|
+
* from a label, path basename, or nearest folder. Legacy archives without a
|
|
25
|
+
* stable key are surfaced for review but are never repairable automatically.
|
|
26
|
+
*/
|
|
27
|
+
export async function listHistoricalProjectRepairCandidates(dataDir, detectedProjects) {
|
|
28
|
+
const detected = (await Promise.all(detectedProjects
|
|
29
|
+
.filter((item) => STABLE_PROJECT_KEY.test(item.projectKey) && item.project.trim() && item.projectPath.trim())
|
|
30
|
+
.map(async (item) => await stat(item.projectPath).then((entry) => entry.isDirectory() ? item : null).catch(() => null))))
|
|
31
|
+
.filter((item) => item !== null);
|
|
32
|
+
const detectedKeys = new Set(detected.map((item) => item.projectKey));
|
|
33
|
+
const archives = await listArchives(dataDir);
|
|
34
|
+
const groups = new Map();
|
|
35
|
+
for (const record of archives) {
|
|
36
|
+
const recorded = projectOfRecord(record);
|
|
37
|
+
const identity = recorded.projectKey
|
|
38
|
+
? recorded
|
|
39
|
+
: await projectIdentityOfRecord(record).catch(() => ({ project: recorded.project, projectKey: null, projectPath: null }));
|
|
40
|
+
const project = identity.project ?? recorded.project;
|
|
41
|
+
const projectKey = identity.projectKey && STABLE_PROJECT_KEY.test(identity.projectKey)
|
|
42
|
+
? identity.projectKey
|
|
43
|
+
: null;
|
|
44
|
+
if (projectKey && detectedKeys.has(projectKey))
|
|
45
|
+
continue;
|
|
46
|
+
const groupKey = projectKey ?? `legacy:${project ?? ""}`;
|
|
47
|
+
const group = groups.get(groupKey) ?? {
|
|
48
|
+
project,
|
|
49
|
+
projectKey,
|
|
50
|
+
hasProjectScope: false,
|
|
51
|
+
archiveCount: 0,
|
|
52
|
+
sourcePaths: new Set(),
|
|
53
|
+
};
|
|
54
|
+
if ("projectScope" in record.source && typeof record.source.projectScope === "string" && record.source.projectScope) {
|
|
55
|
+
group.hasProjectScope = true;
|
|
56
|
+
}
|
|
57
|
+
group.archiveCount += 1;
|
|
58
|
+
group.sourcePaths.add(record.source.path);
|
|
59
|
+
groups.set(groupKey, group);
|
|
60
|
+
}
|
|
61
|
+
return [...groups.values()]
|
|
62
|
+
.map((group) => ({
|
|
63
|
+
historicalProjectKey: group.projectKey,
|
|
64
|
+
historicalProject: group.project,
|
|
65
|
+
originalFolderAvailable: false,
|
|
66
|
+
reason: !group.projectKey ? "no-stable-project-key" : group.hasProjectScope ? "already-scoped" : "folder-not-detected",
|
|
67
|
+
archiveCount: group.archiveCount,
|
|
68
|
+
sessionCount: group.sourcePaths.size,
|
|
69
|
+
replacementOptions: group.projectKey && !group.hasProjectScope ? detected : [],
|
|
70
|
+
repairable: Boolean(group.projectKey && !group.hasProjectScope && detected.length > 0),
|
|
71
|
+
}))
|
|
72
|
+
.sort((left, right) => (left.historicalProject ?? "").localeCompare(right.historicalProject ?? "")
|
|
73
|
+
|| (left.historicalProjectKey ?? "").localeCompare(right.historicalProjectKey ?? ""));
|
|
74
|
+
}
|
|
75
|
+
async function resolvePreview(dataDir, request, detectedProjects) {
|
|
76
|
+
assertStableProjectKey(request.historicalProjectKey, "Historical project key");
|
|
77
|
+
assertStableProjectKey(request.replacementProjectKey, "Replacement project key");
|
|
78
|
+
if (request.historicalProjectKey === request.replacementProjectKey) {
|
|
79
|
+
fail("invalid_argument", "The replacement must be a different exact project key");
|
|
80
|
+
}
|
|
81
|
+
const candidates = await listHistoricalProjectRepairCandidates(dataDir, detectedProjects);
|
|
82
|
+
const candidate = candidates.find((item) => item.historicalProjectKey === request.historicalProjectKey);
|
|
83
|
+
if (!candidate) {
|
|
84
|
+
fail("invalid_argument", "That historical project is not awaiting repair, or is already represented by a detected folder");
|
|
85
|
+
}
|
|
86
|
+
if (!candidate.repairable) {
|
|
87
|
+
fail("invalid_argument", candidate.reason === "already-scoped"
|
|
88
|
+
? "This history already carries a shared-project scope and cannot be reassigned locally"
|
|
89
|
+
: "This history has no stable project key, so it cannot be reassigned safely");
|
|
90
|
+
}
|
|
91
|
+
const replacement = candidate.replacementOptions.find((item) => item.projectKey === request.replacementProjectKey);
|
|
92
|
+
if (!replacement || !STABLE_PROJECT_KEY.test(replacement.projectKey)) {
|
|
93
|
+
fail("invalid_argument", "Choose an exact currently detected folder for the replacement");
|
|
94
|
+
}
|
|
95
|
+
const config = await readConfig(dataDir);
|
|
96
|
+
const binding = teamSpaceOf(config, replacement.project, replacement.projectKey);
|
|
97
|
+
if (!binding) {
|
|
98
|
+
fail("invalid_argument", "The exact replacement folder is not connected to a shared project yet; connect it first");
|
|
99
|
+
}
|
|
100
|
+
const existing = teamSpaceOf(config, candidate.historicalProject, candidate.historicalProjectKey);
|
|
101
|
+
if (existing && existing.spaceKey !== binding.spaceKey) {
|
|
102
|
+
fail("invalid_argument", "This historical identity is already bound to a different shared project");
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
candidate,
|
|
106
|
+
replacement,
|
|
107
|
+
teamSpaceBound: true,
|
|
108
|
+
spaceKey: binding.spaceKey,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Builds a no-write impact preview for one exact historical-to-live mapping. */
|
|
112
|
+
export async function previewHistoricalProjectRepair(dataDir, request, detectedProjects) {
|
|
113
|
+
return resolvePreview(dataDir, request, detectedProjects);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Adds the old stable key as a local alias of the replacement's existing team
|
|
117
|
+
* space. Confirmation is mandatory; no archive sidecar or ciphertext is
|
|
118
|
+
* opened or rewritten. The normal team backfill then handles encrypted copies.
|
|
119
|
+
*/
|
|
120
|
+
export async function applyHistoricalProjectRepair(dataDir, request, detectedProjects, options) {
|
|
121
|
+
if (options.confirm !== true)
|
|
122
|
+
fail("invalid_argument", "Confirm this exact historical-project repair after reviewing its archive and session counts");
|
|
123
|
+
const preview = await resolvePreview(dataDir, request, detectedProjects);
|
|
124
|
+
const config = await readConfig(dataDir);
|
|
125
|
+
const binding = teamSpaceOf(config, preview.replacement.project, preview.replacement.projectKey);
|
|
126
|
+
if (!binding)
|
|
127
|
+
fail("invalid_argument", "The replacement folder's shared-project binding changed; review again");
|
|
128
|
+
const aliasBinding = { ...binding, localConfirmed: true };
|
|
129
|
+
await bindTeamSpace(dataDir, preview.candidate.historicalProject ?? preview.replacement.project, aliasBinding, preview.candidate.historicalProjectKey);
|
|
130
|
+
return { ...preview, applied: true };
|
|
131
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ProviderUploadClient, UploadLease } from "../control-plane.js";
|
|
2
|
+
import { type ByteStream } from "../byte-stream.js";
|
|
3
|
+
import type { StorageFetch } from "./safe-storage-fetch.js";
|
|
2
4
|
/**
|
|
3
5
|
* Google Cloud Storage V4 signing (`GOOG4-RSA-SHA256`) plus resumable uploads.
|
|
4
6
|
*
|
|
@@ -55,6 +57,7 @@ export type GcsStreamOptions = {
|
|
|
55
57
|
/** Awaited after every successful chunk PUT with the cumulative confirmed
|
|
56
58
|
* bytes. This call is the durability point a resume journal writes at. */
|
|
57
59
|
onState?: (state: GcsStreamState) => void | Promise<void>;
|
|
60
|
+
signal?: AbortSignal;
|
|
58
61
|
};
|
|
59
62
|
/**
|
|
60
63
|
* GCS resumable upload client.
|
|
@@ -66,22 +69,34 @@ export declare class GcsUploadClient implements ProviderUploadClient {
|
|
|
66
69
|
private readonly endpoint;
|
|
67
70
|
private readonly serviceAccount;
|
|
68
71
|
private readonly chunkBytes;
|
|
72
|
+
private readonly fetchImpl;
|
|
69
73
|
readonly kind: "gcs";
|
|
70
|
-
constructor(endpoint: GcsEndpoint, serviceAccount: ServiceAccount, chunkBytes?: number);
|
|
74
|
+
constructor(endpoint: GcsEndpoint, serviceAccount: ServiceAccount, chunkBytes?: number, fetchImpl?: StorageFetch);
|
|
71
75
|
private sign;
|
|
72
76
|
/** The chunk-folder layout's object verbs: plain signed single-object HTTP. */
|
|
73
|
-
putObject(object: string, body: Buffer
|
|
77
|
+
putObject(object: string, body: Buffer, options?: {
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
}): Promise<{
|
|
74
80
|
checksum: string;
|
|
75
81
|
}>;
|
|
76
|
-
headObject(object: string
|
|
82
|
+
headObject(object: string, options?: {
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
}): Promise<{
|
|
77
85
|
exists: boolean;
|
|
78
86
|
bytes: number;
|
|
79
87
|
checksum?: string;
|
|
80
88
|
}>;
|
|
81
|
-
getObject(object: string
|
|
89
|
+
getObject(object: string, options?: {
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
}): Promise<Buffer>;
|
|
92
|
+
/** Object-key streaming counterpart used by the background index store. */
|
|
93
|
+
getObjectStream(object: string, options?: {
|
|
94
|
+
signal?: AbortSignal;
|
|
95
|
+
}): Promise<ByteStream>;
|
|
96
|
+
private downloadObjectStream;
|
|
82
97
|
deleteObject(object: string): Promise<void>;
|
|
83
98
|
/** Starts a resumable session and returns its URI. */
|
|
84
|
-
initiateResumable(object: string): Promise<string>;
|
|
99
|
+
initiateResumable(object: string, signal?: AbortSignal): Promise<string>;
|
|
85
100
|
/**
|
|
86
101
|
* Asks a session how many bytes the provider has durably confirmed, speaking
|
|
87
102
|
* the protocol GCS actually implements: an empty PUT whose content-range is
|
|
@@ -89,7 +104,7 @@ export declare class GcsUploadClient implements ProviderUploadClient {
|
|
|
89
104
|
* confirmed — or 308 with no Range at all when nothing is confirmed yet.
|
|
90
105
|
* (Error messages here never carry the session URI: it is a bearer token.)
|
|
91
106
|
*/
|
|
92
|
-
resumableStatus(session: string): Promise<{
|
|
107
|
+
resumableStatus(session: string, signal?: AbortSignal): Promise<{
|
|
93
108
|
confirmedBytes: number;
|
|
94
109
|
}>;
|
|
95
110
|
upload(lease: UploadLease, ciphertext: Buffer): Promise<{
|
|
@@ -124,7 +139,13 @@ export declare class GcsUploadClient implements ProviderUploadClient {
|
|
|
124
139
|
bytes: number;
|
|
125
140
|
}>;
|
|
126
141
|
/** Reads an object back for restore or offload-proof: a signed GET, whole. */
|
|
127
|
-
download(lease: UploadLease
|
|
142
|
+
download(lease: UploadLease, options?: {
|
|
143
|
+
signal?: AbortSignal;
|
|
144
|
+
}): Promise<Buffer>;
|
|
145
|
+
/** The production restore/transfer door: response chunks flow straight to disk. */
|
|
146
|
+
downloadStream(lease: UploadLease, options?: {
|
|
147
|
+
signal?: AbortSignal;
|
|
148
|
+
}): Promise<ByteStream>;
|
|
128
149
|
head(lease: UploadLease): Promise<{
|
|
129
150
|
exists: boolean;
|
|
130
151
|
bytes: number;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, createPrivateKey, createSign } from "node:crypto";
|
|
2
2
|
import { fail } from "../errors.js";
|
|
3
3
|
import { amzDates, canonicalHeaders, canonicalQuery, canonicalUri } from "./sigv4.js";
|
|
4
|
+
import { collectChunks, responseBodyChunks } from "../byte-stream.js";
|
|
4
5
|
/**
|
|
5
6
|
* Google Cloud Storage V4 signing (`GOOG4-RSA-SHA256`) plus resumable uploads.
|
|
6
7
|
*
|
|
@@ -51,26 +52,28 @@ export class GcsUploadClient {
|
|
|
51
52
|
endpoint;
|
|
52
53
|
serviceAccount;
|
|
53
54
|
chunkBytes;
|
|
55
|
+
fetchImpl;
|
|
54
56
|
kind = "gcs";
|
|
55
|
-
constructor(endpoint, serviceAccount, chunkBytes = 8 * 1024 * 1024) {
|
|
57
|
+
constructor(endpoint, serviceAccount, chunkBytes = 8 * 1024 * 1024, fetchImpl = fetch) {
|
|
56
58
|
this.endpoint = endpoint;
|
|
57
59
|
this.serviceAccount = serviceAccount;
|
|
58
60
|
this.chunkBytes = chunkBytes;
|
|
61
|
+
this.fetchImpl = fetchImpl;
|
|
59
62
|
}
|
|
60
63
|
sign(method, object, headers = {}, query = {}) {
|
|
61
64
|
return signGcsUrl({ method, bucket: this.endpoint.bucket, object, serviceAccount: this.serviceAccount, headers, query, host: this.endpoint.host, port: this.endpoint.port, protocol: this.endpoint.protocol });
|
|
62
65
|
}
|
|
63
66
|
/** The chunk-folder layout's object verbs: plain signed single-object HTTP. */
|
|
64
|
-
async putObject(object, body) {
|
|
67
|
+
async putObject(object, body, options = {}) {
|
|
65
68
|
const { url } = this.sign("PUT", object);
|
|
66
|
-
const response = await
|
|
69
|
+
const response = await this.fetchImpl(url, { method: "PUT", body: new Uint8Array(body), signal: options.signal });
|
|
67
70
|
if (!response.ok)
|
|
68
71
|
fail("internal", `Upload failed for ${object}: ${response.status} ${response.statusText}`, { object, status: response.status });
|
|
69
72
|
return { checksum: createHash("md5").update(body).digest("base64") };
|
|
70
73
|
}
|
|
71
|
-
async headObject(object) {
|
|
74
|
+
async headObject(object, options = {}) {
|
|
72
75
|
const { url } = this.sign("HEAD", object);
|
|
73
|
-
const response = await
|
|
76
|
+
const response = await this.fetchImpl(url, { method: "HEAD", signal: options.signal });
|
|
74
77
|
if (response.status === 404)
|
|
75
78
|
return { exists: false, bytes: 0 };
|
|
76
79
|
if (!response.ok)
|
|
@@ -79,23 +82,30 @@ export class GcsUploadClient {
|
|
|
79
82
|
const md5 = /(?:^|[ ,])md5=([A-Za-z0-9+/]+={0,2})/.exec(hash)?.[1];
|
|
80
83
|
return { exists: true, bytes: Number(response.headers.get("content-length") ?? "0"), checksum: md5 };
|
|
81
84
|
}
|
|
82
|
-
async getObject(object) {
|
|
85
|
+
async getObject(object, options = {}) {
|
|
86
|
+
return collectChunks(await this.downloadObjectStream(object, options.signal), Number.MAX_SAFE_INTEGER);
|
|
87
|
+
}
|
|
88
|
+
/** Object-key streaming counterpart used by the background index store. */
|
|
89
|
+
async getObjectStream(object, options = {}) {
|
|
90
|
+
return this.downloadObjectStream(object, options.signal);
|
|
91
|
+
}
|
|
92
|
+
async downloadObjectStream(object, signal) {
|
|
83
93
|
const { url } = this.sign("GET", object);
|
|
84
|
-
const response = await
|
|
94
|
+
const response = await this.fetchImpl(url, { signal });
|
|
85
95
|
if (!response.ok)
|
|
86
96
|
fail("internal", `Could not download ${object}: ${response.status} ${response.statusText}`, { object, status: response.status });
|
|
87
|
-
return
|
|
97
|
+
return responseBodyChunks(response);
|
|
88
98
|
}
|
|
89
99
|
async deleteObject(object) {
|
|
90
100
|
const { url } = this.sign("DELETE", object);
|
|
91
|
-
const response = await
|
|
101
|
+
const response = await this.fetchImpl(url, { method: "DELETE" });
|
|
92
102
|
if (!response.ok && response.status !== 404)
|
|
93
103
|
fail("internal", `Could not delete ${object}: ${response.status} ${response.statusText}`, { object, status: response.status });
|
|
94
104
|
}
|
|
95
105
|
/** Starts a resumable session and returns its URI. */
|
|
96
|
-
async initiateResumable(object) {
|
|
106
|
+
async initiateResumable(object, signal) {
|
|
97
107
|
const { url } = this.sign("POST", object, { "x-goog-resumable": "start" });
|
|
98
|
-
const response = await
|
|
108
|
+
const response = await this.fetchImpl(url, { method: "POST", headers: { "x-goog-resumable": "start" }, signal });
|
|
99
109
|
if (!response.ok)
|
|
100
110
|
fail("internal", `Could not start a resumable upload for ${object}: ${response.status} ${response.statusText}`, { object, status: response.status });
|
|
101
111
|
const session = response.headers.get("location");
|
|
@@ -108,8 +118,8 @@ export class GcsUploadClient {
|
|
|
108
118
|
* confirmed — or 308 with no Range at all when nothing is confirmed yet.
|
|
109
119
|
* (Error messages here never carry the session URI: it is a bearer token.)
|
|
110
120
|
*/
|
|
111
|
-
async resumableStatus(session) {
|
|
112
|
-
const response = await
|
|
121
|
+
async resumableStatus(session, signal) {
|
|
122
|
+
const response = await this.fetchImpl(session, { method: "PUT", headers: { "content-range": "bytes */*" }, signal });
|
|
113
123
|
// 2xx means the session already finalized its object; resuming would try
|
|
114
124
|
// to append to something finished, so the caller should verify instead.
|
|
115
125
|
if (response.ok)
|
|
@@ -131,7 +141,7 @@ export class GcsUploadClient {
|
|
|
131
141
|
const slice = ciphertext.subarray(offset, Math.min(offset + this.chunkBytes, ciphertext.length));
|
|
132
142
|
const last = offset + slice.length >= ciphertext.length;
|
|
133
143
|
const range = ciphertext.length === 0 ? "bytes */0" : `bytes ${offset}-${offset + slice.length - 1}/${ciphertext.length}`;
|
|
134
|
-
const response = await
|
|
144
|
+
const response = await this.fetchImpl(session, { method: "PUT", body: new Uint8Array(slice), headers: { "content-range": range } });
|
|
135
145
|
// 308 means "resume incomplete" and is the expected status between chunks.
|
|
136
146
|
if (!response.ok && !(response.status === 308 && !last)) {
|
|
137
147
|
fail("internal", `Resumable upload for ${object} failed at ${range}: ${response.status} ${response.statusText}`, { object, status: response.status });
|
|
@@ -154,7 +164,7 @@ export class GcsUploadClient {
|
|
|
154
164
|
let session;
|
|
155
165
|
let sent = 0;
|
|
156
166
|
if (resume) {
|
|
157
|
-
const { confirmedBytes } = await this.resumableStatus(resume.session);
|
|
167
|
+
const { confirmedBytes } = await this.resumableStatus(resume.session, options.signal);
|
|
158
168
|
// The caller re-sealed from the offset its journal recorded. If the
|
|
159
169
|
// provider confirmed a different count — a PUT landed after the last
|
|
160
170
|
// journal write, say — the very first flush would splice bytes at the
|
|
@@ -166,7 +176,7 @@ export class GcsUploadClient {
|
|
|
166
176
|
sent = confirmedBytes;
|
|
167
177
|
}
|
|
168
178
|
else {
|
|
169
|
-
session = await this.initiateResumable(objectKey);
|
|
179
|
+
session = await this.initiateResumable(objectKey, options.signal);
|
|
170
180
|
}
|
|
171
181
|
// A resumed run never saw the bytes the earlier run sent, so it cannot
|
|
172
182
|
// roll an MD5 over the whole object — null says "no provider-language
|
|
@@ -183,7 +193,7 @@ export class GcsUploadClient {
|
|
|
183
193
|
const range = final
|
|
184
194
|
? (piece.length === 0 ? `bytes */${sent}` : `bytes ${sent}-${sent + piece.length - 1}/${sent + piece.length}`)
|
|
185
195
|
: `bytes ${sent}-${sent + piece.length - 1}/*`;
|
|
186
|
-
const response = await
|
|
196
|
+
const response = await this.fetchImpl(session, { method: "PUT", body: new Uint8Array(piece), headers: { "content-range": range }, signal: options.signal });
|
|
187
197
|
if (!response.ok && !(response.status === 308 && !final)) {
|
|
188
198
|
fail("internal", `Resumable upload for ${objectKey} failed at ${range}: ${response.status} ${response.statusText}`, { object: objectKey, status: response.status });
|
|
189
199
|
}
|
|
@@ -194,6 +204,7 @@ export class GcsUploadClient {
|
|
|
194
204
|
await options.onState({ session, confirmedBytes: sent });
|
|
195
205
|
};
|
|
196
206
|
for await (const chunk of chunks) {
|
|
207
|
+
options.signal?.throwIfAborted();
|
|
197
208
|
md5?.update(chunk);
|
|
198
209
|
held.push(chunk);
|
|
199
210
|
heldBytes += chunk.length;
|
|
@@ -209,16 +220,16 @@ export class GcsUploadClient {
|
|
|
209
220
|
return { remoteChecksum: md5 ? md5.digest("base64") : null, bytes: sent };
|
|
210
221
|
}
|
|
211
222
|
/** Reads an object back for restore or offload-proof: a signed GET, whole. */
|
|
212
|
-
async download(lease) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return
|
|
223
|
+
async download(lease, options = {}) {
|
|
224
|
+
return collectChunks(await this.downloadStream(lease, options), Number.MAX_SAFE_INTEGER);
|
|
225
|
+
}
|
|
226
|
+
/** The production restore/transfer door: response chunks flow straight to disk. */
|
|
227
|
+
async downloadStream(lease, options = {}) {
|
|
228
|
+
return this.downloadObjectStream(lease.objectKey, options.signal);
|
|
218
229
|
}
|
|
219
230
|
async head(lease) {
|
|
220
231
|
const { url } = this.sign("HEAD", lease.objectKey);
|
|
221
|
-
const response = await
|
|
232
|
+
const response = await this.fetchImpl(url, { method: "HEAD" });
|
|
222
233
|
if (response.status === 404)
|
|
223
234
|
return { exists: false, bytes: 0 };
|
|
224
235
|
if (!response.ok)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ProviderKind, ProviderUploadClient, UploadLease } from "../control-plane.js";
|
|
2
|
+
import { type ByteStream } from "../byte-stream.js";
|
|
3
|
+
import type { StorageFetch } from "./safe-storage-fetch.js";
|
|
2
4
|
/**
|
|
3
5
|
* Google Drive as a bring-your-own storage provider (Drive API v3).
|
|
4
6
|
*
|
|
@@ -17,7 +19,7 @@ import type { ProviderKind, ProviderUploadClient, UploadLease } from "../control
|
|
|
17
19
|
*/
|
|
18
20
|
export declare const GDRIVE_SCOPE = "https://www.googleapis.com/auth/drive.file";
|
|
19
21
|
/** Placeholder until a first-party client id ships. Point SEALKEEP_GDRIVE_CLIENT_ID at your own Desktop-app OAuth client to connect today. */
|
|
20
|
-
export declare const GDRIVE_CLIENT_ID_PLACEHOLDER = "000000000000-
|
|
22
|
+
export declare const GDRIVE_CLIENT_ID_PLACEHOLDER = "000000000000-sealkeep-placeholder.apps.googleusercontent.com";
|
|
21
23
|
export declare function gdriveClientId(env?: NodeJS.ProcessEnv): string;
|
|
22
24
|
export declare const GDRIVE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
23
25
|
export declare const GDRIVE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -27,6 +29,14 @@ export type GdriveCredentials = {
|
|
|
27
29
|
kind: "gdrive";
|
|
28
30
|
refreshToken: string;
|
|
29
31
|
clientId: string;
|
|
32
|
+
/**
|
|
33
|
+
* Stable, non-secret Drive grantee id returned by `about.get` as
|
|
34
|
+
* `user.permissionId`. It binds a refresh token to the physical Drive
|
|
35
|
+
* account without persisting an email address or another bearer secret.
|
|
36
|
+
* Optional only so credentials written by older releases can be read and
|
|
37
|
+
* rejected safely until the person reconnects.
|
|
38
|
+
*/
|
|
39
|
+
accountId?: string;
|
|
30
40
|
/** "cloud" = the refresh needs the account's server (web-client secret lives there). */
|
|
31
41
|
exchange?: "cloud";
|
|
32
42
|
/** Runtime-only: attached at load time, never persisted. Refreshes through the plane. */
|
|
@@ -70,6 +80,18 @@ export declare function exchangeCodeForTokens(input: {
|
|
|
70
80
|
accessToken: string;
|
|
71
81
|
expiresIn: number;
|
|
72
82
|
}>;
|
|
83
|
+
/**
|
|
84
|
+
* Resolve the stable Drive identity represented by an access token.
|
|
85
|
+
*
|
|
86
|
+
* `about.get(fields=user(permissionId))` is available under the existing
|
|
87
|
+
* `drive.file` grant. A permission id is an opaque grantee identifier, not a
|
|
88
|
+
* credential and not personally-readable account metadata, so it is safe to
|
|
89
|
+
* retain beside the refresh token and include in destination fingerprints.
|
|
90
|
+
*/
|
|
91
|
+
export declare function gdriveAccountId(accessToken: string, options?: {
|
|
92
|
+
apiBase?: string;
|
|
93
|
+
fetchImpl?: typeof fetch;
|
|
94
|
+
}): Promise<string>;
|
|
73
95
|
/**
|
|
74
96
|
* Fire-and-forget platform opener. Failing to open a browser is never failing
|
|
75
97
|
* to connect — the consent URL is reported to the caller either way — so this
|
|
@@ -88,6 +110,8 @@ export type ConnectOptions = {
|
|
|
88
110
|
fetchImpl?: typeof fetch;
|
|
89
111
|
authUrl?: string;
|
|
90
112
|
tokenUrl?: string;
|
|
113
|
+
/** Test seam for the post-consent `about.get` identity lookup. */
|
|
114
|
+
accountIdResolver?: (accessToken: string) => Promise<string>;
|
|
91
115
|
/** Loopback port; 0 (the default) asks the OS for a random one, which is what Google's loopback flow expects. */
|
|
92
116
|
port?: number;
|
|
93
117
|
timeoutMs?: number;
|
|
@@ -99,10 +123,19 @@ export type ConnectOptions = {
|
|
|
99
123
|
* flow runs offline under test.
|
|
100
124
|
*/
|
|
101
125
|
export declare function connectGdrive(options?: ConnectOptions): Promise<GdriveCredentials>;
|
|
126
|
+
export type GdriveStreamOptions = {
|
|
127
|
+
/**
|
|
128
|
+
* Drive requires the final object length when a resumable session starts.
|
|
129
|
+
* File-backed archive uploads measure this in a bounded first pass and pass
|
|
130
|
+
* it here; an unknown-length source must be staged before it can use Drive.
|
|
131
|
+
*/
|
|
132
|
+
bytes?: number;
|
|
133
|
+
signal?: AbortSignal;
|
|
134
|
+
};
|
|
102
135
|
/**
|
|
103
136
|
* Drive upload client.
|
|
104
137
|
*
|
|
105
|
-
* A lease's objectKey (e.g. `vaultline/<id>.
|
|
138
|
+
* A lease's objectKey (e.g. `vaultline/<id>.skarchive`) has no path meaning in
|
|
106
139
|
* Drive, so the mapping is: files live flat in one folder (or the hidden
|
|
107
140
|
* appDataFolder space), named by the key's basename, and each carries
|
|
108
141
|
* `appProperties.vaultlineKey = <full objectKey>`. head/download locate by that
|
|
@@ -119,7 +152,7 @@ export declare class GdriveUploadClient implements ProviderUploadClient {
|
|
|
119
152
|
private readonly now;
|
|
120
153
|
constructor(endpoint: GdriveEndpoint, credentials: GdriveCredentials, options?: {
|
|
121
154
|
chunkBytes?: number;
|
|
122
|
-
fetchImpl?:
|
|
155
|
+
fetchImpl?: StorageFetch;
|
|
123
156
|
now?: () => number;
|
|
124
157
|
});
|
|
125
158
|
private base;
|
|
@@ -129,9 +162,14 @@ export declare class GdriveUploadClient implements ProviderUploadClient {
|
|
|
129
162
|
private token;
|
|
130
163
|
private api;
|
|
131
164
|
private list;
|
|
132
|
-
/**
|
|
165
|
+
/**
|
|
166
|
+
* Exact lookup inside this configured physical destination. Object keys are
|
|
167
|
+
* intentionally reusable across Drive folders, so the immutable parent id
|
|
168
|
+
* and Drive space are part of every lookup rather than merely creation
|
|
169
|
+
* metadata.
|
|
170
|
+
*/
|
|
133
171
|
private findByKey;
|
|
134
|
-
/**
|
|
172
|
+
/** Resolves the archive folder to its immutable Drive id, creating it only for uploads. */
|
|
135
173
|
private parentId;
|
|
136
174
|
/**
|
|
137
175
|
* Starts a resumable session (create, or in-place update when the key already
|
|
@@ -144,13 +182,38 @@ export declare class GdriveUploadClient implements ProviderUploadClient {
|
|
|
144
182
|
remoteChecksum: string;
|
|
145
183
|
bytes: number;
|
|
146
184
|
}>;
|
|
185
|
+
/**
|
|
186
|
+
* Uploads a known-length object through Drive's resumable protocol while
|
|
187
|
+
* retaining at most one configured resumable part. The archive pipeline
|
|
188
|
+
* measures a file in a bounded first pass, then this second pass computes
|
|
189
|
+
* Drive's whole-object MD5 as bytes are copied into fixed-size PUTs.
|
|
190
|
+
*
|
|
191
|
+
* Drive requires every non-final chunk to be a positive multiple of 256 KiB.
|
|
192
|
+
* The configured `chunkBytes` already enforces that rule; the last PUT may be
|
|
193
|
+
* short. A 308 response is accepted only for a non-final PUT, and the final
|
|
194
|
+
* PUT must be a success, so an early-finalized or half-accepted session can
|
|
195
|
+
* never be reported as complete.
|
|
196
|
+
*/
|
|
197
|
+
uploadStream(objectKey: string, chunks: ByteStream, options?: GdriveStreamOptions): Promise<{
|
|
198
|
+
remoteChecksum: string;
|
|
199
|
+
bytes: number;
|
|
200
|
+
}>;
|
|
147
201
|
head(lease: UploadLease): Promise<{
|
|
148
202
|
exists: boolean;
|
|
149
203
|
bytes: number;
|
|
150
204
|
checksum?: string;
|
|
151
205
|
}>;
|
|
152
206
|
/** Reads an object back for restore or offload-proof: locate by key, then `alt=media`. */
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Removes the file holding this object key in this exact destination. An
|
|
209
|
+
* empty successful scoped listing proves absence. A DELETE 404 is rechecked
|
|
210
|
+
* in the same scope before callers may discard their ledger metadata.
|
|
211
|
+
*/
|
|
212
|
+
deleteByKey(objectKey: string): Promise<"deleted" | "absent">;
|
|
213
|
+
download(lease: UploadLease, options?: {
|
|
214
|
+
signal?: AbortSignal;
|
|
215
|
+
}): Promise<Buffer>;
|
|
216
|
+
downloadStream(lease: UploadLease, options?: {
|
|
217
|
+
signal?: AbortSignal;
|
|
218
|
+
}): Promise<ByteStream>;
|
|
156
219
|
}
|