@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.
Files changed (104) hide show
  1. package/dist/backup-prune.d.ts +68 -0
  2. package/dist/backup-prune.d.ts.map +1 -0
  3. package/dist/backup-prune.js +196 -0
  4. package/dist/backup-prune.js.map +1 -0
  5. package/dist/backup-prune.test.d.ts +2 -0
  6. package/dist/backup-prune.test.d.ts.map +1 -0
  7. package/dist/backup-prune.test.js +89 -0
  8. package/dist/backup-prune.test.js.map +1 -0
  9. package/dist/bin/backup-prune-runner.d.ts +3 -0
  10. package/dist/bin/backup-prune-runner.d.ts.map +1 -0
  11. package/dist/bin/backup-prune-runner.js +50 -0
  12. package/dist/bin/backup-prune-runner.js.map +1 -0
  13. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  14. package/dist/bin/sync-runner-company.js +8 -0
  15. package/dist/bin/sync-runner-company.js.map +1 -1
  16. package/dist/bin/sync-runner-planning.d.ts +1 -1
  17. package/dist/bin/sync-runner-planning.d.ts.map +1 -1
  18. package/dist/bin/sync-runner-planning.js +15 -1
  19. package/dist/bin/sync-runner-planning.js.map +1 -1
  20. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  21. package/dist/bin/sync-runner-watch-loop.js +28 -0
  22. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  23. package/dist/bin/sync-runner.d.ts +14 -0
  24. package/dist/bin/sync-runner.d.ts.map +1 -1
  25. package/dist/bin/sync-runner.js +26 -0
  26. package/dist/bin/sync-runner.js.map +1 -1
  27. package/dist/bin/sync-runner.test.js +68 -1
  28. package/dist/bin/sync-runner.test.js.map +1 -1
  29. package/dist/cli/rescue-classify-ordering.test.js +105 -0
  30. package/dist/cli/rescue-classify-ordering.test.js.map +1 -1
  31. package/dist/cli/rescue-core.d.ts +1 -0
  32. package/dist/cli/rescue-core.d.ts.map +1 -1
  33. package/dist/cli/rescue-core.js +478 -300
  34. package/dist/cli/rescue-core.js.map +1 -1
  35. package/dist/cli/rescue-snapshot.d.ts +14 -0
  36. package/dist/cli/rescue-snapshot.d.ts.map +1 -0
  37. package/dist/cli/rescue-snapshot.js +39 -0
  38. package/dist/cli/rescue-snapshot.js.map +1 -0
  39. package/dist/cli/rescue-snapshot.test.d.ts +2 -0
  40. package/dist/cli/rescue-snapshot.test.d.ts.map +1 -0
  41. package/dist/cli/rescue-snapshot.test.js +46 -0
  42. package/dist/cli/rescue-snapshot.test.js.map +1 -0
  43. package/dist/cli/sync-scope.test.js +3 -1
  44. package/dist/cli/sync-scope.test.js.map +1 -1
  45. package/dist/cli/sync.d.ts +6 -6
  46. package/dist/cli/sync.d.ts.map +1 -1
  47. package/dist/cli/sync.js +51 -41
  48. package/dist/cli/sync.js.map +1 -1
  49. package/dist/cli/sync.test.js +102 -2
  50. package/dist/cli/sync.test.js.map +1 -1
  51. package/dist/manifest-reconcile.d.ts +27 -0
  52. package/dist/manifest-reconcile.d.ts.map +1 -0
  53. package/dist/manifest-reconcile.js +120 -0
  54. package/dist/manifest-reconcile.js.map +1 -0
  55. package/dist/manifest-reconcile.test.d.ts +2 -0
  56. package/dist/manifest-reconcile.test.d.ts.map +1 -0
  57. package/dist/manifest-reconcile.test.js +97 -0
  58. package/dist/manifest-reconcile.test.js.map +1 -0
  59. package/dist/object-io.d.ts +19 -2
  60. package/dist/object-io.d.ts.map +1 -1
  61. package/dist/object-io.js +168 -32
  62. package/dist/object-io.js.map +1 -1
  63. package/dist/object-io.test.js +157 -1
  64. package/dist/object-io.test.js.map +1 -1
  65. package/dist/s3.d.ts +12 -1
  66. package/dist/s3.d.ts.map +1 -1
  67. package/dist/s3.js +51 -18
  68. package/dist/s3.js.map +1 -1
  69. package/dist/s3.test.js +12 -1
  70. package/dist/s3.test.js.map +1 -1
  71. package/dist/signals/get.test.js +21 -1
  72. package/dist/signals/get.test.js.map +1 -1
  73. package/dist/signals/list.test.js +21 -1
  74. package/dist/signals/list.test.js.map +1 -1
  75. package/dist/sources/get.test.js +21 -1
  76. package/dist/sources/get.test.js.map +1 -1
  77. package/dist/sources/list.test.js +21 -1
  78. package/dist/sources/list.test.js.map +1 -1
  79. package/package.json +3 -2
  80. package/src/backup-prune.test.ts +98 -0
  81. package/src/backup-prune.ts +182 -0
  82. package/src/bin/backup-prune-runner.ts +33 -0
  83. package/src/bin/sync-runner-company.ts +7 -0
  84. package/src/bin/sync-runner-planning.ts +16 -2
  85. package/src/bin/sync-runner-watch-loop.ts +18 -0
  86. package/src/bin/sync-runner.test.ts +82 -1
  87. package/src/bin/sync-runner.ts +45 -0
  88. package/src/cli/rescue-classify-ordering.test.ts +121 -0
  89. package/src/cli/rescue-core.ts +261 -86
  90. package/src/cli/rescue-snapshot.test.ts +57 -0
  91. package/src/cli/rescue-snapshot.ts +51 -0
  92. package/src/cli/sync-scope.test.ts +3 -1
  93. package/src/cli/sync.test.ts +121 -2
  94. package/src/cli/sync.ts +71 -53
  95. package/src/manifest-reconcile.test.ts +107 -0
  96. package/src/manifest-reconcile.ts +160 -0
  97. package/src/object-io.test.ts +175 -0
  98. package/src/object-io.ts +213 -32
  99. package/src/s3.test.ts +18 -0
  100. package/src/s3.ts +64 -30
  101. package/src/signals/get.test.ts +26 -2
  102. package/src/signals/list.test.ts +26 -2
  103. package/src/sources/get.test.ts +26 -2
  104. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=manifest-reconcile.test.d.ts.map
@@ -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":"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"}
@@ -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 just `fetch`es
17
- * the signed URLs.
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
  *
@@ -1 +1 @@
1
- {"version":3,"file":"object-io.d.ts","sourceRoot":"","sources":["../src/object-io.ts"],"names":[],"mappings":"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"}
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 just `fetch`es
17
- * the signed URLs.
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 fetchWithRetry(row.url, { method: "GET" }, `presigned GET ${key}`);
678
+ const res = await presignedGetWithRetry(row.url, row.headers, `presigned GET ${key}`);
576
679
  if (res.status === 404) {
577
- await cancelBody(res);
680
+ res.destroy();
578
681
  throw notFoundError(key);
579
682
  }
580
- if (!res.ok) {
581
- const detail = await safeText(res);
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: webReadableToAsyncIterable(res.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 fetchWithRetry(url, { method: "GET" }, `presigned HEAD ${key}`);
776
+ const res = await presignedGetWithRetry(url, undefined, `presigned HEAD ${key}`);
677
777
  if (res.status === 404) {
678
- await cancelBody(res);
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
- await cancelBody(res);
787
+ res.destroy();
688
788
  throw accessDeniedError(key, "presigned HEAD returned 403");
689
789
  }
690
- if (!res.ok) {
691
- await cancelBody(res);
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
- await cancelBody(res);
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