@indigoai-us/hq-cloud 6.14.7 → 6.14.9
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/dist/backup-prune.d.ts +68 -0
- package/dist/backup-prune.d.ts.map +1 -0
- package/dist/backup-prune.js +196 -0
- package/dist/backup-prune.js.map +1 -0
- package/dist/backup-prune.test.d.ts +2 -0
- package/dist/backup-prune.test.d.ts.map +1 -0
- package/dist/backup-prune.test.js +89 -0
- package/dist/backup-prune.test.js.map +1 -0
- package/dist/bin/backup-prune-runner.d.ts +3 -0
- package/dist/bin/backup-prune-runner.d.ts.map +1 -0
- package/dist/bin/backup-prune-runner.js +50 -0
- package/dist/bin/backup-prune-runner.js.map +1 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +8 -0
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts +1 -1
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +15 -1
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +28 -0
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +14 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +26 -0
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +68 -1
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/rescue-classify-ordering.test.js +105 -0
- package/dist/cli/rescue-classify-ordering.test.js.map +1 -1
- package/dist/cli/rescue-core.d.ts +1 -0
- package/dist/cli/rescue-core.d.ts.map +1 -1
- package/dist/cli/rescue-core.js +478 -300
- package/dist/cli/rescue-core.js.map +1 -1
- package/dist/cli/rescue-snapshot.d.ts +14 -0
- package/dist/cli/rescue-snapshot.d.ts.map +1 -0
- package/dist/cli/rescue-snapshot.js +39 -0
- package/dist/cli/rescue-snapshot.js.map +1 -0
- package/dist/cli/rescue-snapshot.test.d.ts +2 -0
- package/dist/cli/rescue-snapshot.test.d.ts.map +1 -0
- package/dist/cli/rescue-snapshot.test.js +46 -0
- package/dist/cli/rescue-snapshot.test.js.map +1 -0
- package/dist/cli/sync-scope.test.js +3 -1
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +6 -6
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +51 -41
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +102 -2
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/manifest-reconcile.d.ts +27 -0
- package/dist/manifest-reconcile.d.ts.map +1 -0
- package/dist/manifest-reconcile.js +120 -0
- package/dist/manifest-reconcile.js.map +1 -0
- package/dist/manifest-reconcile.test.d.ts +2 -0
- package/dist/manifest-reconcile.test.d.ts.map +1 -0
- package/dist/manifest-reconcile.test.js +97 -0
- package/dist/manifest-reconcile.test.js.map +1 -0
- package/dist/object-io.d.ts +19 -2
- package/dist/object-io.d.ts.map +1 -1
- package/dist/object-io.js +168 -32
- package/dist/object-io.js.map +1 -1
- package/dist/object-io.test.js +157 -1
- package/dist/object-io.test.js.map +1 -1
- package/dist/s3.d.ts +12 -1
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +51 -18
- package/dist/s3.js.map +1 -1
- package/dist/s3.test.js +12 -1
- package/dist/s3.test.js.map +1 -1
- package/dist/signals/get.test.js +21 -1
- package/dist/signals/get.test.js.map +1 -1
- package/dist/signals/list.test.js +21 -1
- package/dist/signals/list.test.js.map +1 -1
- package/dist/sources/get.test.js +21 -1
- package/dist/sources/get.test.js.map +1 -1
- package/dist/sources/list.test.js +21 -1
- package/dist/sources/list.test.js.map +1 -1
- package/package.json +3 -2
- package/src/backup-prune.test.ts +98 -0
- package/src/backup-prune.ts +182 -0
- package/src/bin/backup-prune-runner.ts +33 -0
- package/src/bin/sync-runner-company.ts +7 -0
- package/src/bin/sync-runner-planning.ts +16 -2
- package/src/bin/sync-runner-watch-loop.ts +18 -0
- package/src/bin/sync-runner.test.ts +82 -1
- package/src/bin/sync-runner.ts +45 -0
- package/src/cli/rescue-classify-ordering.test.ts +121 -0
- package/src/cli/rescue-core.ts +261 -86
- package/src/cli/rescue-snapshot.test.ts +57 -0
- package/src/cli/rescue-snapshot.ts +51 -0
- package/src/cli/sync-scope.test.ts +3 -1
- package/src/cli/sync.test.ts +121 -2
- package/src/cli/sync.ts +71 -53
- package/src/manifest-reconcile.test.ts +107 -0
- package/src/manifest-reconcile.ts +160 -0
- package/src/object-io.test.ts +175 -0
- package/src/object-io.ts +213 -32
- package/src/s3.test.ts +18 -0
- package/src/s3.ts +64 -30
- package/src/signals/get.test.ts +26 -2
- package/src/signals/list.test.ts +26 -2
- package/src/sources/get.test.ts +26 -2
- package/src/sources/list.test.ts +26 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconcile successfully pulled cloud companies into the local manifest.
|
|
3
|
+
*
|
|
4
|
+
* The personal-vault leg also carries `companies/manifest.yaml`, so this runs
|
|
5
|
+
* only after the complete fanout has settled. That makes the locally
|
|
6
|
+
* materialized cloud companies authoritative for their own manifest entries
|
|
7
|
+
* without discarding entries the personal vault already had.
|
|
8
|
+
*/
|
|
9
|
+
import type { EntityInfo } from "./vault-client.js";
|
|
10
|
+
export interface ManifestReconcileTarget {
|
|
11
|
+
uid: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
personalMode?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface ManifestReconcileOptions {
|
|
16
|
+
hqRoot: string;
|
|
17
|
+
targets: readonly ManifestReconcileTarget[];
|
|
18
|
+
completedCompanySlugs: ReadonlySet<string>;
|
|
19
|
+
getEntity: (uid: string) => Promise<EntityInfo>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Atomically merge live, successfully pulled company targets into
|
|
23
|
+
* `companies/manifest.yaml`. Targets that are personal, incomplete, deleted,
|
|
24
|
+
* no longer resolvable, or not materialized on disk are intentionally ignored.
|
|
25
|
+
*/
|
|
26
|
+
export declare function reconcileCompanyManifest(options: ManifestReconcileOptions): Promise<void>;
|
|
27
|
+
//# sourceMappingURL=manifest-reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-reconcile.d.ts","sourceRoot":"","sources":["../src/manifest-reconcile.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAC5C,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3C,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;CACjD;AA6DD;;;;GAIG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAgEf"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconcile successfully pulled cloud companies into the local manifest.
|
|
3
|
+
*
|
|
4
|
+
* The personal-vault leg also carries `companies/manifest.yaml`, so this runs
|
|
5
|
+
* only after the complete fanout has settled. That makes the locally
|
|
6
|
+
* materialized cloud companies authoritative for their own manifest entries
|
|
7
|
+
* without discarding entries the personal vault already had.
|
|
8
|
+
*/
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
import * as fs from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import yaml from "js-yaml";
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function isLiveCompany(entity, uid) {
|
|
17
|
+
return (entity.uid === uid &&
|
|
18
|
+
entity.type === "company" &&
|
|
19
|
+
entity.status === "active" &&
|
|
20
|
+
typeof entity.name === "string" &&
|
|
21
|
+
entity.name.length > 0 &&
|
|
22
|
+
typeof entity.bucketName === "string" &&
|
|
23
|
+
entity.bucketName.length > 0);
|
|
24
|
+
}
|
|
25
|
+
function hasCompanyDirectory(companiesDir, slug) {
|
|
26
|
+
const resolvedCompaniesDir = path.resolve(companiesDir);
|
|
27
|
+
const companyDir = path.resolve(resolvedCompaniesDir, slug);
|
|
28
|
+
if (path.dirname(companyDir) !== resolvedCompaniesDir)
|
|
29
|
+
return false;
|
|
30
|
+
try {
|
|
31
|
+
return fs.statSync(companyDir).isDirectory();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function loadManifest(manifestPath) {
|
|
38
|
+
try {
|
|
39
|
+
const raw = fs.readFileSync(manifestPath, "utf8");
|
|
40
|
+
const parsed = yaml.load(raw);
|
|
41
|
+
return isRecord(parsed) ? parsed : null;
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err.code === "ENOENT")
|
|
45
|
+
return {};
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function writeManifestAtomically(manifestPath, document) {
|
|
50
|
+
const temporaryPath = path.join(path.dirname(manifestPath), `.manifest-${process.pid}-${randomUUID()}.yaml`);
|
|
51
|
+
fs.writeFileSync(temporaryPath, yaml.dump(document, { lineWidth: -1 }), "utf8");
|
|
52
|
+
fs.renameSync(temporaryPath, manifestPath);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Atomically merge live, successfully pulled company targets into
|
|
56
|
+
* `companies/manifest.yaml`. Targets that are personal, incomplete, deleted,
|
|
57
|
+
* no longer resolvable, or not materialized on disk are intentionally ignored.
|
|
58
|
+
*/
|
|
59
|
+
export async function reconcileCompanyManifest(options) {
|
|
60
|
+
const companiesDir = path.join(options.hqRoot, "companies");
|
|
61
|
+
const candidates = [];
|
|
62
|
+
for (const target of options.targets) {
|
|
63
|
+
if (target.personalMode === true ||
|
|
64
|
+
!options.completedCompanySlugs.has(target.slug) ||
|
|
65
|
+
!hasCompanyDirectory(companiesDir, target.slug)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const entity = await options.getEntity(target.uid);
|
|
70
|
+
if (isLiveCompany(entity, target.uid)) {
|
|
71
|
+
candidates.push({ slug: target.slug, entity });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// The target is not presently a live, resolvable company. Never mint a
|
|
76
|
+
// manifest entry from stale fanout data.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (candidates.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
const manifestPath = path.join(companiesDir, "manifest.yaml");
|
|
82
|
+
const document = loadManifest(manifestPath);
|
|
83
|
+
if (!document)
|
|
84
|
+
return;
|
|
85
|
+
let companies;
|
|
86
|
+
if (document.companies === undefined) {
|
|
87
|
+
companies = {};
|
|
88
|
+
document.companies = companies;
|
|
89
|
+
}
|
|
90
|
+
else if (isRecord(document.companies)) {
|
|
91
|
+
companies = document.companies;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// A malformed `companies` key is user state; refusing to replace it is
|
|
95
|
+
// safer than losing a manifest that cannot be interpreted unambiguously.
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
let changed = false;
|
|
99
|
+
for (const { slug, entity } of candidates) {
|
|
100
|
+
const existing = companies[slug];
|
|
101
|
+
if (existing !== undefined && !isRecord(existing))
|
|
102
|
+
continue;
|
|
103
|
+
const entry = existing ?? {};
|
|
104
|
+
if (entry.name === entity.name &&
|
|
105
|
+
entry.cloud_uid === entity.uid &&
|
|
106
|
+
entry.bucket_name === entity.bucketName) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
companies[slug] = {
|
|
110
|
+
...entry,
|
|
111
|
+
name: entity.name,
|
|
112
|
+
cloud_uid: entity.uid,
|
|
113
|
+
bucket_name: entity.bucketName,
|
|
114
|
+
};
|
|
115
|
+
changed = true;
|
|
116
|
+
}
|
|
117
|
+
if (changed)
|
|
118
|
+
writeManifestAtomically(manifestPath, document);
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=manifest-reconcile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-reconcile.js","sourceRoot":"","sources":["../src/manifest-reconcile.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-reconcile.test.d.ts","sourceRoot":"","sources":["../src/manifest-reconcile.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import yaml from "js-yaml";
|
|
6
|
+
import { reconcileCompanyManifest } from "./manifest-reconcile.js";
|
|
7
|
+
let hqRoot;
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
if (hqRoot) {
|
|
10
|
+
fs.rmSync(hqRoot, { recursive: true, force: true });
|
|
11
|
+
hqRoot = undefined;
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
describe("reconcileCompanyManifest", () => {
|
|
15
|
+
it("atomically restores one successful cloud entry after the personal leg without replacing existing manifest state", async () => {
|
|
16
|
+
hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-manifest-reconcile-"));
|
|
17
|
+
const companiesDir = path.join(hqRoot, "companies");
|
|
18
|
+
fs.mkdirSync(path.join(companiesDir, "acme"), { recursive: true });
|
|
19
|
+
fs.mkdirSync(path.join(companiesDir, "failed"), { recursive: true });
|
|
20
|
+
fs.mkdirSync(path.join(companiesDir, "deleted"), { recursive: true });
|
|
21
|
+
// This is the older manifest the personal-vault leg pulled after the
|
|
22
|
+
// cloud-company legs completed. The final merge must retain this state and
|
|
23
|
+
// add only the live, successful cloud target.
|
|
24
|
+
const manifestPath = path.join(companiesDir, "manifest.yaml");
|
|
25
|
+
fs.writeFileSync(manifestPath, `version: 3\nsettings:\n keep: true\ncompanies:\n local-only:\n name: Local Only\n repos:\n - repos/local\n preserved:\n name: Keep Me\n cloud_uid: cmp_preserved\n`);
|
|
26
|
+
const entities = {
|
|
27
|
+
cmp_acme: {
|
|
28
|
+
uid: "cmp_acme",
|
|
29
|
+
slug: "acme",
|
|
30
|
+
type: "company",
|
|
31
|
+
status: "active",
|
|
32
|
+
name: "Acme Inc",
|
|
33
|
+
bucketName: "hq-vault-cmp-acme",
|
|
34
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
35
|
+
},
|
|
36
|
+
cmp_failed: {
|
|
37
|
+
uid: "cmp_failed",
|
|
38
|
+
slug: "failed",
|
|
39
|
+
type: "company",
|
|
40
|
+
status: "active",
|
|
41
|
+
name: "Failed Corp",
|
|
42
|
+
bucketName: "hq-vault-cmp-failed",
|
|
43
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
44
|
+
},
|
|
45
|
+
cmp_deleted: {
|
|
46
|
+
uid: "cmp_deleted",
|
|
47
|
+
slug: "deleted",
|
|
48
|
+
type: "company",
|
|
49
|
+
status: "deleted",
|
|
50
|
+
name: "Deleted Corp",
|
|
51
|
+
bucketName: "hq-vault-cmp-deleted",
|
|
52
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
const options = {
|
|
56
|
+
hqRoot,
|
|
57
|
+
targets: [
|
|
58
|
+
{ uid: "cmp_acme", slug: "acme" },
|
|
59
|
+
{ uid: "cmp_failed", slug: "failed" },
|
|
60
|
+
{ uid: "cmp_deleted", slug: "deleted" },
|
|
61
|
+
{ uid: "cmp_missing-directory", slug: "missing-directory" },
|
|
62
|
+
{ uid: "prs_me", slug: "personal", personalMode: true },
|
|
63
|
+
],
|
|
64
|
+
completedCompanySlugs: new Set(["acme", "deleted", "missing-directory", "personal"]),
|
|
65
|
+
getEntity: async (uid) => {
|
|
66
|
+
const entity = entities[uid];
|
|
67
|
+
if (!entity)
|
|
68
|
+
throw new Error("entity not found");
|
|
69
|
+
return entity;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
await reconcileCompanyManifest(options);
|
|
73
|
+
const firstWrite = fs.readFileSync(manifestPath, "utf8");
|
|
74
|
+
await reconcileCompanyManifest(options);
|
|
75
|
+
expect(fs.readFileSync(manifestPath, "utf8")).toBe(firstWrite);
|
|
76
|
+
expect(yaml.load(firstWrite)).toEqual({
|
|
77
|
+
version: 3,
|
|
78
|
+
settings: { keep: true },
|
|
79
|
+
companies: {
|
|
80
|
+
"local-only": {
|
|
81
|
+
name: "Local Only",
|
|
82
|
+
repos: ["repos/local"],
|
|
83
|
+
},
|
|
84
|
+
preserved: {
|
|
85
|
+
name: "Keep Me",
|
|
86
|
+
cloud_uid: "cmp_preserved",
|
|
87
|
+
},
|
|
88
|
+
acme: {
|
|
89
|
+
name: "Acme Inc",
|
|
90
|
+
cloud_uid: "cmp_acme",
|
|
91
|
+
bucket_name: "hq-vault-cmp-acme",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=manifest-reconcile.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-reconcile.test.js","sourceRoot":"","sources":["../src/manifest-reconcile.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AACzD,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,IAAI,MAAM,SAAS,CAAC;AAE3B,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAGnE,IAAI,MAA0B,CAAC;AAE/B,SAAS,CAAC,GAAG,EAAE;IACb,IAAI,MAAM,EAAE,CAAC;QACX,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,MAAM,GAAG,SAAS,CAAC;IACrB,CAAC;AACH,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,iHAAiH,EAAE,KAAK,IAAI,EAAE;QAC/H,MAAM,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,wBAAwB,CAAC,CAAC,CAAC;QAC1E,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QACpD,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACnE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAEtE,qEAAqE;QACrE,2EAA2E;QAC3E,8CAA8C;QAC9C,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,eAAe,CAAC,CAAC;QAC9D,EAAE,CAAC,aAAa,CACd,YAAY,EACZ,wLAAwL,CACzL,CAAC;QAEF,MAAM,QAAQ,GAA+B;YAC3C,QAAQ,EAAE;gBACR,GAAG,EAAE,UAAU;gBACf,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE,SAAS;gBACf,MAAM,EAAE,QAAQ;gBAChB,IAAI,EAAE,UAAU;gBAChB,UAAU,EAAE,mBAAmB;gBAC/B,SAAS,EAAE,sBAAsB;aAClC;YACD,UAAU,EAAE;gBACV,GAAG,EAAE,YAAY;gBACjB,IAAI,EAAE,QAAQ;gBACd,IAAI,EAAE,SAAS;gBACf,MAAM,EAAE,QAAQ;gBAChB,IAAI,EAAE,aAAa;gBACnB,UAAU,EAAE,qBAAqB;gBACjC,SAAS,EAAE,sBAAsB;aAClC;YACD,WAAW,EAAE;gBACX,GAAG,EAAE,aAAa;gBAClB,IAAI,EAAE,SAAS;gBACf,IAAI,EAAE,SAAS;gBACf,MAAM,EAAE,SAAS;gBACjB,IAAI,EAAE,cAAc;gBACpB,UAAU,EAAE,sBAAsB;gBAClC,SAAS,EAAE,sBAAsB;aAClC;SACF,CAAC;QACF,MAAM,OAAO,GAAG;YACd,MAAM;YACN,OAAO,EAAE;gBACP,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE;gBACjC,EAAE,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACrC,EAAE,GAAG,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE;gBACvC,EAAE,GAAG,EAAE,uBAAuB,EAAE,IAAI,EAAE,mBAAmB,EAAE;gBAC3D,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,IAAI,EAAE;aACxD;YACD,qBAAqB,EAAE,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,SAAS,EAAE,mBAAmB,EAAE,UAAU,CAAC,CAAC;YACpF,SAAS,EAAE,KAAK,EAAE,GAAW,EAAE,EAAE;gBAC/B,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;gBAC7B,IAAI,CAAC,MAAM;oBAAE,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;gBACjD,OAAO,MAAM,CAAC;YAChB,CAAC;SACF,CAAC;QAEF,MAAM,wBAAwB,CAAC,OAAO,CAAC,CAAC;QACxC,MAAM,UAAU,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;QACzD,MAAM,wBAAwB,CAAC,OAAO,CAAC,CAAC;QAExC,MAAM,CAAC,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC/D,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC;YACpC,OAAO,EAAE,CAAC;YACV,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;YACxB,SAAS,EAAE;gBACT,YAAY,EAAE;oBACZ,IAAI,EAAE,YAAY;oBAClB,KAAK,EAAE,CAAC,aAAa,CAAC;iBACvB;gBACD,SAAS,EAAE;oBACT,IAAI,EAAE,SAAS;oBACf,SAAS,EAAE,eAAe;iBAC3B;gBACD,IAAI,EAAE;oBACJ,IAAI,EAAE,UAAU;oBAChB,SAAS,EAAE,UAAU;oBACrB,WAAW,EAAE,mBAAmB;iBACjC;aACF;SACF,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/object-io.d.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* - `PresignObjectIO` — the presigned-URL path. The vault-service decides
|
|
14
14
|
* access as a runtime DDB check (no IAM policy ceiling) and hands back
|
|
15
15
|
* short-lived presigned GET/PUT/DELETE URLs + (for PUT) the exact headers
|
|
16
|
-
* to replay. The client never holds AWS credentials — it
|
|
17
|
-
*
|
|
16
|
+
* to replay. The client never holds AWS credentials — it requests the
|
|
17
|
+
* signed URLs directly.
|
|
18
18
|
*
|
|
19
19
|
* The seam is a per-EntityContext factory resolved INSIDE s3.ts, so every
|
|
20
20
|
* existing call site (`uploadFile(ctx, …)`, `downloadFile(ctx, …)`, …) keeps
|
|
@@ -165,6 +165,23 @@ export declare class S3SdkObjectIO implements ObjectIO {
|
|
|
165
165
|
deleteObject(key: string): Promise<void>;
|
|
166
166
|
headObject(key: string): Promise<HeadObjectResult | null>;
|
|
167
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Response shape shared by every presigned GET consumer. Unlike the runtime
|
|
170
|
+
* `fetch` response, its body is backed by Node's core HTTP client, so it never
|
|
171
|
+
* enters the runtime-bundled undici parser path.
|
|
172
|
+
*/
|
|
173
|
+
export interface PresignedGetResponse {
|
|
174
|
+
status: number;
|
|
175
|
+
headers: Headers;
|
|
176
|
+
body?: AsyncIterable<Uint8Array>;
|
|
177
|
+
destroy(): void;
|
|
178
|
+
}
|
|
179
|
+
/** Transport seam retained for deterministic wire-level regression tests. */
|
|
180
|
+
export interface PresignedGetTransport {
|
|
181
|
+
get(url: string, headers: Record<string, string> | undefined): Promise<PresignedGetResponse>;
|
|
182
|
+
}
|
|
183
|
+
/** Test-only override; production always uses Node's core HTTP(S) transport. */
|
|
184
|
+
export declare function setPresignedGetTransportForTesting(transport: PresignedGetTransport | null): void;
|
|
168
185
|
/**
|
|
169
186
|
* Opt-in gate for strict fail-closed enforcement of fenced presigned PUTs.
|
|
170
187
|
*
|
package/dist/object-io.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"object-io.d.ts","sourceRoot":"","sources":["../src/object-io.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;
|
|
1
|
+
{"version":3,"file":"object-io.d.ts","sourceRoot":"","sources":["../src/object-io.ts"],"names":[],"mappings":"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"}
|
package/dist/object-io.js
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* - `PresignObjectIO` — the presigned-URL path. The vault-service decides
|
|
14
14
|
* access as a runtime DDB check (no IAM policy ceiling) and hands back
|
|
15
15
|
* short-lived presigned GET/PUT/DELETE URLs + (for PUT) the exact headers
|
|
16
|
-
* to replay. The client never holds AWS credentials — it
|
|
17
|
-
*
|
|
16
|
+
* to replay. The client never holds AWS credentials — it requests the
|
|
17
|
+
* signed URLs directly.
|
|
18
18
|
*
|
|
19
19
|
* The seam is a per-EntityContext factory resolved INSIDE s3.ts, so every
|
|
20
20
|
* existing call site (`uploadFile(ctx, …)`, `downloadFile(ctx, …)`, …) keeps
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
* preserving today's behavior for every non-gated caller.
|
|
24
24
|
*/
|
|
25
25
|
import { S3Client, PutObjectCommand, GetObjectCommand, ListObjectsV2Command, DeleteObjectCommand, HeadObjectCommand, } from "@aws-sdk/client-s3";
|
|
26
|
+
import * as http from "node:http";
|
|
27
|
+
import * as https from "node:https";
|
|
26
28
|
import { VaultClientError } from "./vault-client.js";
|
|
27
29
|
// ---------------------------------------------------------------------------
|
|
28
30
|
// S3 SDK transport (default)
|
|
@@ -167,6 +169,107 @@ function metaFromHeaders(headers) {
|
|
|
167
169
|
});
|
|
168
170
|
return meta;
|
|
169
171
|
}
|
|
172
|
+
function headersFromNode(headers) {
|
|
173
|
+
const out = new Headers();
|
|
174
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
175
|
+
if (value === undefined)
|
|
176
|
+
continue;
|
|
177
|
+
out.set(name, Array.isArray(value) ? value.join(", ") : value);
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
function expectedContentLength(headers) {
|
|
182
|
+
const value = headers.get("content-length");
|
|
183
|
+
if (value === null)
|
|
184
|
+
return undefined;
|
|
185
|
+
if (!/^\d+$/.test(value)) {
|
|
186
|
+
throw new Error(`invalid presigned GET content-length: ${value}`);
|
|
187
|
+
}
|
|
188
|
+
const length = Number(value);
|
|
189
|
+
if (!Number.isSafeInteger(length)) {
|
|
190
|
+
throw new Error(`invalid presigned GET content-length: ${value}`);
|
|
191
|
+
}
|
|
192
|
+
return length;
|
|
193
|
+
}
|
|
194
|
+
function contentLengthError(expected, actual) {
|
|
195
|
+
return new Error(`presigned GET content-length mismatch: expected ${expected} bytes, received ${actual}`);
|
|
196
|
+
}
|
|
197
|
+
async function* checkedNodeBody(message, expected) {
|
|
198
|
+
let received = 0;
|
|
199
|
+
let exhausted = false;
|
|
200
|
+
try {
|
|
201
|
+
try {
|
|
202
|
+
for await (const chunk of message) {
|
|
203
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
204
|
+
received += bytes.byteLength;
|
|
205
|
+
if (expected !== undefined && received > expected) {
|
|
206
|
+
throw contentLengthError(expected, received);
|
|
207
|
+
}
|
|
208
|
+
yield bytes;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
// A FIN before Node has received the declared bytes normally surfaces as
|
|
213
|
+
// an "aborted" stream error. Preserve the useful data-integrity cause.
|
|
214
|
+
if (expected !== undefined && received !== expected) {
|
|
215
|
+
throw contentLengthError(expected, received);
|
|
216
|
+
}
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
exhausted = true;
|
|
220
|
+
if (expected !== undefined && received !== expected) {
|
|
221
|
+
throw contentLengthError(expected, received);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
// Consumers such as HEAD only inspect headers, and a caller can stop after
|
|
226
|
+
// a prefix. Destroying in both cases releases the socket immediately.
|
|
227
|
+
if (!exhausted)
|
|
228
|
+
message.destroy();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function nodePresignedGet(url, headers) {
|
|
232
|
+
const target = new URL(url);
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const handleResponse = (message) => {
|
|
235
|
+
const responseHeaders = headersFromNode(message.headers);
|
|
236
|
+
let expected;
|
|
237
|
+
try {
|
|
238
|
+
expected = expectedContentLength(responseHeaders);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
message.destroy();
|
|
242
|
+
reject(err);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
resolve({
|
|
246
|
+
status: message.statusCode ?? 0,
|
|
247
|
+
headers: responseHeaders,
|
|
248
|
+
body: checkedNodeBody(message, expected),
|
|
249
|
+
destroy: () => message.destroy(),
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
const request = target.protocol === "https:"
|
|
253
|
+
? https.request(target, { method: "GET", headers }, handleResponse)
|
|
254
|
+
: target.protocol === "http:"
|
|
255
|
+
? http.request(target, { method: "GET", headers }, handleResponse)
|
|
256
|
+
: undefined;
|
|
257
|
+
if (!request) {
|
|
258
|
+
reject(new Error(`unsupported presigned GET protocol: ${target.protocol}`));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
request.once("error", reject);
|
|
262
|
+
request.end();
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const NODE_PRESIGNED_GET_TRANSPORT = {
|
|
266
|
+
get: nodePresignedGet,
|
|
267
|
+
};
|
|
268
|
+
let presignedGetTransport = NODE_PRESIGNED_GET_TRANSPORT;
|
|
269
|
+
/** Test-only override; production always uses Node's core HTTP(S) transport. */
|
|
270
|
+
export function setPresignedGetTransportForTesting(transport) {
|
|
271
|
+
presignedGetTransport = transport ?? NODE_PRESIGNED_GET_TRANSPORT;
|
|
272
|
+
}
|
|
170
273
|
/**
|
|
171
274
|
* Per-key presign denial codes that are EXPECTED and must be reclassified as
|
|
172
275
|
* skip-with-log — throwing the `name: "Forbidden"` shape (see
|
|
@@ -572,20 +675,17 @@ export class PresignObjectIO {
|
|
|
572
675
|
}
|
|
573
676
|
async getObjectStream(key) {
|
|
574
677
|
const row = await this.resolveGetUrlForBody(key);
|
|
575
|
-
const res = await
|
|
678
|
+
const res = await presignedGetWithRetry(row.url, row.headers, `presigned GET ${key}`);
|
|
576
679
|
if (res.status === 404) {
|
|
577
|
-
|
|
680
|
+
res.destroy();
|
|
578
681
|
throw notFoundError(key);
|
|
579
682
|
}
|
|
580
|
-
if (
|
|
581
|
-
const detail = await
|
|
683
|
+
if (res.status < 200 || res.status >= 300) {
|
|
684
|
+
const detail = await safePresignedGetText(res);
|
|
582
685
|
throw new Error(`presigned GET failed for ${key}: ${res.status} ${detail}`);
|
|
583
686
|
}
|
|
584
|
-
if (!res.body) {
|
|
585
|
-
return { body: (async function* () { })(), metadata: metaFromHeaders(res.headers) };
|
|
586
|
-
}
|
|
587
687
|
return {
|
|
588
|
-
body:
|
|
688
|
+
body: res.body ?? (async function* () { })(),
|
|
589
689
|
metadata: metaFromHeaders(res.headers),
|
|
590
690
|
};
|
|
591
691
|
}
|
|
@@ -673,9 +773,9 @@ export class PresignObjectIO {
|
|
|
673
773
|
}
|
|
674
774
|
url = row.url;
|
|
675
775
|
}
|
|
676
|
-
const res = await
|
|
776
|
+
const res = await presignedGetWithRetry(url, undefined, `presigned HEAD ${key}`);
|
|
677
777
|
if (res.status === 404) {
|
|
678
|
-
|
|
778
|
+
res.destroy();
|
|
679
779
|
return null;
|
|
680
780
|
}
|
|
681
781
|
if (res.status === 403) {
|
|
@@ -684,12 +784,11 @@ export class PresignObjectIO {
|
|
|
684
784
|
// denial branch above. The SDK transport throws name:"Forbidden" here;
|
|
685
785
|
// mirror it so both transports agree and no caller mistakes a denial
|
|
686
786
|
// for a missing object.
|
|
687
|
-
|
|
787
|
+
res.destroy();
|
|
688
788
|
throw accessDeniedError(key, "presigned HEAD returned 403");
|
|
689
789
|
}
|
|
690
|
-
if (
|
|
691
|
-
await
|
|
692
|
-
const detail = await safeText(res);
|
|
790
|
+
if (res.status < 200 || res.status >= 300) {
|
|
791
|
+
const detail = await safePresignedGetText(res);
|
|
693
792
|
throw new Error(`presigned HEAD failed for ${key}: ${res.status} ${detail}`);
|
|
694
793
|
}
|
|
695
794
|
const result = {
|
|
@@ -698,7 +797,7 @@ export class PresignObjectIO {
|
|
|
698
797
|
size: Number(res.headers.get("content-length") ?? "0"),
|
|
699
798
|
metadata: metaFromHeaders(res.headers),
|
|
700
799
|
};
|
|
701
|
-
|
|
800
|
+
res.destroy();
|
|
702
801
|
return result;
|
|
703
802
|
}
|
|
704
803
|
}
|
|
@@ -718,21 +817,6 @@ async function cancelBody(res) {
|
|
|
718
817
|
// Best-effort — the socket is released either way once GC'd.
|
|
719
818
|
}
|
|
720
819
|
}
|
|
721
|
-
async function* webReadableToAsyncIterable(stream) {
|
|
722
|
-
const reader = stream.getReader();
|
|
723
|
-
try {
|
|
724
|
-
while (true) {
|
|
725
|
-
const { done, value } = await reader.read();
|
|
726
|
-
if (done)
|
|
727
|
-
return;
|
|
728
|
-
if (value && value.byteLength > 0)
|
|
729
|
-
yield value;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
finally {
|
|
733
|
-
reader.releaseLock();
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
820
|
function parseLastModified(value) {
|
|
737
821
|
if (!value)
|
|
738
822
|
return new Date();
|
|
@@ -758,6 +842,58 @@ function isTransientStatus(status) {
|
|
|
758
842
|
function sleep(ms) {
|
|
759
843
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
760
844
|
}
|
|
845
|
+
/**
|
|
846
|
+
* Shared presigned-GET path for streaming reads and header-only probes.
|
|
847
|
+
*
|
|
848
|
+
* GET deliberately uses Node core HTTP(S), not global fetch: on affected
|
|
849
|
+
* Node/Windows builds the bundled undici parser can assert after a FIN arrives
|
|
850
|
+
* while the consumer is backpressured. Keep the retry budget identical to the
|
|
851
|
+
* PUT/DELETE fetch path below.
|
|
852
|
+
*/
|
|
853
|
+
async function presignedGetWithRetry(url, headers, what) {
|
|
854
|
+
let lastError;
|
|
855
|
+
for (let attempt = 0; attempt <= FETCH_MAX_RETRIES; attempt++) {
|
|
856
|
+
if (attempt > 0) {
|
|
857
|
+
const backoff = FETCH_BASE_DELAY_MS * 2 ** (attempt - 1);
|
|
858
|
+
const jitter = Math.floor(Math.random() * FETCH_BASE_DELAY_MS);
|
|
859
|
+
await sleep(backoff + jitter);
|
|
860
|
+
}
|
|
861
|
+
let res;
|
|
862
|
+
try {
|
|
863
|
+
res = await presignedGetTransport.get(url, headers);
|
|
864
|
+
}
|
|
865
|
+
catch (err) {
|
|
866
|
+
lastError = err;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (isTransientStatus(res.status) && attempt < FETCH_MAX_RETRIES) {
|
|
870
|
+
res.destroy();
|
|
871
|
+
lastError = new Error(`${what}: transient ${res.status}`);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
return res;
|
|
875
|
+
}
|
|
876
|
+
throw lastError instanceof Error
|
|
877
|
+
? lastError
|
|
878
|
+
: new Error(`${what}: failed after ${FETCH_MAX_RETRIES} retries`);
|
|
879
|
+
}
|
|
880
|
+
async function safePresignedGetText(res) {
|
|
881
|
+
try {
|
|
882
|
+
let text = "";
|
|
883
|
+
for await (const chunk of res.body ?? []) {
|
|
884
|
+
text += Buffer.from(chunk).toString("utf-8");
|
|
885
|
+
if (text.length >= 200)
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
return text.slice(0, 200);
|
|
889
|
+
}
|
|
890
|
+
catch {
|
|
891
|
+
return "";
|
|
892
|
+
}
|
|
893
|
+
finally {
|
|
894
|
+
res.destroy();
|
|
895
|
+
}
|
|
896
|
+
}
|
|
761
897
|
/**
|
|
762
898
|
* `fetch` with bounded retry on network errors + transient 5xx. The presigned
|
|
763
899
|
* URL is reusable until expiry and the bodies are in-memory Buffers, so a
|