@opendatalabs/personal-server-ts-server 1.33.8 → 1.34.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.
Files changed (167) hide show
  1. package/dist/__fixtures__/bound-record-store.d.ts +19 -0
  2. package/dist/__fixtures__/bound-record-store.d.ts.map +1 -0
  3. package/dist/__fixtures__/bound-record-store.js +60 -0
  4. package/dist/__fixtures__/bound-record-store.js.map +1 -0
  5. package/dist/__fixtures__/cg-pdpp-client/pdpp-authorization-strategy.d.ts +41 -0
  6. package/dist/__fixtures__/cg-pdpp-client/pdpp-authorization-strategy.d.ts.map +1 -0
  7. package/dist/__fixtures__/cg-pdpp-client/pdpp-authorization-strategy.js +34 -0
  8. package/dist/__fixtures__/cg-pdpp-client/pdpp-authorization-strategy.js.map +1 -0
  9. package/dist/__fixtures__/cg-pdpp-client/pdpp-context-client.d.ts +112 -0
  10. package/dist/__fixtures__/cg-pdpp-client/pdpp-context-client.d.ts.map +1 -0
  11. package/dist/__fixtures__/cg-pdpp-client/pdpp-context-client.js +226 -0
  12. package/dist/__fixtures__/cg-pdpp-client/pdpp-context-client.js.map +1 -0
  13. package/dist/__fixtures__/cg-pdpp-client/pdpp-types.d.ts +135 -0
  14. package/dist/__fixtures__/cg-pdpp-client/pdpp-types.d.ts.map +1 -0
  15. package/dist/__fixtures__/cg-pdpp-client/pdpp-types.js +21 -0
  16. package/dist/__fixtures__/cg-pdpp-client/pdpp-types.js.map +1 -0
  17. package/dist/api-auth.d.ts +18 -2
  18. package/dist/api-auth.d.ts.map +1 -1
  19. package/dist/api-auth.js +114 -22
  20. package/dist/api-auth.js.map +1 -1
  21. package/dist/app.d.ts +60 -1
  22. package/dist/app.d.ts.map +1 -1
  23. package/dist/app.js +139 -1
  24. package/dist/app.js.map +1 -1
  25. package/dist/bootstrap.d.ts +10 -0
  26. package/dist/bootstrap.d.ts.map +1 -1
  27. package/dist/bootstrap.js +358 -7
  28. package/dist/bootstrap.js.map +1 -1
  29. package/dist/enclave-main.d.ts +4 -0
  30. package/dist/enclave-main.d.ts.map +1 -1
  31. package/dist/enclave-main.js +152 -1
  32. package/dist/enclave-main.js.map +1 -1
  33. package/dist/middleware/body-limit.d.ts +1 -1
  34. package/dist/middleware/body-limit.d.ts.map +1 -1
  35. package/dist/middleware/body-limit.js +8 -3
  36. package/dist/middleware/body-limit.js.map +1 -1
  37. package/dist/middleware/cors.d.ts.map +1 -1
  38. package/dist/middleware/cors.js +1 -0
  39. package/dist/middleware/cors.js.map +1 -1
  40. package/dist/package.json +9 -1
  41. package/dist/pdpp/bootstrap.d.ts +72 -0
  42. package/dist/pdpp/bootstrap.d.ts.map +1 -0
  43. package/dist/pdpp/bootstrap.js +270 -0
  44. package/dist/pdpp/bootstrap.js.map +1 -0
  45. package/dist/pdpp/chain-enforcement.d.ts +95 -0
  46. package/dist/pdpp/chain-enforcement.d.ts.map +1 -0
  47. package/dist/pdpp/chain-enforcement.js +145 -0
  48. package/dist/pdpp/chain-enforcement.js.map +1 -0
  49. package/dist/pdpp/client-document-fetch.d.ts +63 -0
  50. package/dist/pdpp/client-document-fetch.d.ts.map +1 -0
  51. package/dist/pdpp/client-document-fetch.js +254 -0
  52. package/dist/pdpp/client-document-fetch.js.map +1 -0
  53. package/dist/pdpp/declaration-registry.d.ts +83 -0
  54. package/dist/pdpp/declaration-registry.d.ts.map +1 -0
  55. package/dist/pdpp/declaration-registry.js +251 -0
  56. package/dist/pdpp/declaration-registry.js.map +1 -0
  57. package/dist/pdpp/deployment.d.ts +70 -0
  58. package/dist/pdpp/deployment.d.ts.map +1 -0
  59. package/dist/pdpp/deployment.js +104 -0
  60. package/dist/pdpp/deployment.js.map +1 -0
  61. package/dist/pdpp/record-schema.d.ts +2 -0
  62. package/dist/pdpp/record-schema.d.ts.map +1 -0
  63. package/dist/pdpp/record-schema.js +2 -0
  64. package/dist/pdpp/record-schema.js.map +1 -0
  65. package/dist/pdpp/records-bootstrap.d.ts +111 -0
  66. package/dist/pdpp/records-bootstrap.d.ts.map +1 -0
  67. package/dist/pdpp/records-bootstrap.js +353 -0
  68. package/dist/pdpp/records-bootstrap.js.map +1 -0
  69. package/dist/pdpp/source-id-compat.d.ts +5 -0
  70. package/dist/pdpp/source-id-compat.d.ts.map +1 -0
  71. package/dist/pdpp/source-id-compat.js +16 -0
  72. package/dist/pdpp/source-id-compat.js.map +1 -0
  73. package/dist/pdpp/test-connections.d.ts +6 -0
  74. package/dist/pdpp/test-connections.d.ts.map +1 -0
  75. package/dist/pdpp/test-connections.js +37 -0
  76. package/dist/pdpp/test-connections.js.map +1 -0
  77. package/dist/routes/auth-device.d.ts.map +1 -1
  78. package/dist/routes/auth-device.js +3 -0
  79. package/dist/routes/auth-device.js.map +1 -1
  80. package/dist/routes/chunked-scope-import.d.ts +41 -0
  81. package/dist/routes/chunked-scope-import.d.ts.map +1 -0
  82. package/dist/routes/chunked-scope-import.js +901 -0
  83. package/dist/routes/chunked-scope-import.js.map +1 -0
  84. package/dist/routes/data.d.ts +23 -1
  85. package/dist/routes/data.d.ts.map +1 -1
  86. package/dist/routes/data.js +112 -1
  87. package/dist/routes/data.js.map +1 -1
  88. package/dist/routes/grants.d.ts.map +1 -1
  89. package/dist/routes/grants.js +2 -0
  90. package/dist/routes/grants.js.map +1 -1
  91. package/dist/routes/mcp.d.ts.map +1 -1
  92. package/dist/routes/mcp.js +3 -0
  93. package/dist/routes/mcp.js.map +1 -1
  94. package/dist/routes/pdpp-auth.d.ts +157 -0
  95. package/dist/routes/pdpp-auth.d.ts.map +1 -0
  96. package/dist/routes/pdpp-auth.js +953 -0
  97. package/dist/routes/pdpp-auth.js.map +1 -0
  98. package/dist/routes/pdpp-blobs.d.ts +26 -0
  99. package/dist/routes/pdpp-blobs.d.ts.map +1 -0
  100. package/dist/routes/pdpp-blobs.js +179 -0
  101. package/dist/routes/pdpp-blobs.js.map +1 -0
  102. package/dist/routes/pdpp-connections.d.ts +28 -0
  103. package/dist/routes/pdpp-connections.d.ts.map +1 -0
  104. package/dist/routes/pdpp-connections.js +144 -0
  105. package/dist/routes/pdpp-connections.js.map +1 -0
  106. package/dist/routes/pdpp-declarations.d.ts +58 -0
  107. package/dist/routes/pdpp-declarations.d.ts.map +1 -0
  108. package/dist/routes/pdpp-declarations.js +94 -0
  109. package/dist/routes/pdpp-declarations.js.map +1 -0
  110. package/dist/routes/pdpp-instance-bindings.d.ts +26 -0
  111. package/dist/routes/pdpp-instance-bindings.d.ts.map +1 -0
  112. package/dist/routes/pdpp-instance-bindings.js +124 -0
  113. package/dist/routes/pdpp-instance-bindings.js.map +1 -0
  114. package/dist/routes/pdpp-records.d.ts +158 -0
  115. package/dist/routes/pdpp-records.d.ts.map +1 -0
  116. package/dist/routes/pdpp-records.js +1537 -0
  117. package/dist/routes/pdpp-records.js.map +1 -0
  118. package/dist/routes/pdpp-well-known.d.ts +13 -0
  119. package/dist/routes/pdpp-well-known.d.ts.map +1 -0
  120. package/dist/routes/pdpp-well-known.js +20 -0
  121. package/dist/routes/pdpp-well-known.js.map +1 -0
  122. package/dist/storage/declaration-witness.d.ts +56 -0
  123. package/dist/storage/declaration-witness.d.ts.map +1 -0
  124. package/dist/storage/declaration-witness.js +273 -0
  125. package/dist/storage/declaration-witness.js.map +1 -0
  126. package/dist/storage/hierarchy.d.ts +25 -1
  127. package/dist/storage/hierarchy.d.ts.map +1 -1
  128. package/dist/storage/hierarchy.js +210 -16
  129. package/dist/storage/hierarchy.js.map +1 -1
  130. package/dist/storage/import-operations.d.ts +11 -0
  131. package/dist/storage/import-operations.d.ts.map +1 -0
  132. package/dist/storage/import-operations.js +261 -0
  133. package/dist/storage/import-operations.js.map +1 -0
  134. package/dist/storage/index-manager.d.ts +24 -2
  135. package/dist/storage/index-manager.d.ts.map +1 -1
  136. package/dist/storage/index-manager.js +279 -30
  137. package/dist/storage/index-manager.js.map +1 -1
  138. package/dist/storage/index-schema.d.ts +1 -1
  139. package/dist/storage/index-schema.d.ts.map +1 -1
  140. package/dist/storage/index-schema.js +189 -8
  141. package/dist/storage/index-schema.js.map +1 -1
  142. package/dist/storage/legacy-scope-authority.d.ts +14 -0
  143. package/dist/storage/legacy-scope-authority.d.ts.map +1 -0
  144. package/dist/storage/legacy-scope-authority.js +298 -0
  145. package/dist/storage/legacy-scope-authority.js.map +1 -0
  146. package/dist/storage/node-data-storage.d.ts +6 -0
  147. package/dist/storage/node-data-storage.d.ts.map +1 -1
  148. package/dist/storage/node-data-storage.js +161 -2
  149. package/dist/storage/node-data-storage.js.map +1 -1
  150. package/dist/storage/pdpp-grant-binding-sqlite-store.d.ts +17 -0
  151. package/dist/storage/pdpp-grant-binding-sqlite-store.d.ts.map +1 -0
  152. package/dist/storage/pdpp-grant-binding-sqlite-store.js +159 -0
  153. package/dist/storage/pdpp-grant-binding-sqlite-store.js.map +1 -0
  154. package/dist/storage/pdpp-records-sqlite-store.d.ts +112 -0
  155. package/dist/storage/pdpp-records-sqlite-store.d.ts.map +1 -0
  156. package/dist/storage/pdpp-records-sqlite-store.js +1440 -0
  157. package/dist/storage/pdpp-records-sqlite-store.js.map +1 -0
  158. package/dist/storage/pdpp-stream-completion.d.ts +5 -0
  159. package/dist/storage/pdpp-stream-completion.d.ts.map +1 -0
  160. package/dist/storage/pdpp-stream-completion.js +10 -0
  161. package/dist/storage/pdpp-stream-completion.js.map +1 -0
  162. package/dist/storage/scope-deletion-history.d.ts +8 -0
  163. package/dist/storage/scope-deletion-history.d.ts.map +1 -0
  164. package/dist/storage/scope-deletion-history.js +40 -0
  165. package/dist/storage/scope-deletion-history.js.map +1 -0
  166. package/dist/ui/ps-lite-debug.js +3644 -869
  167. package/package.json +12 -4
@@ -0,0 +1,901 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { mkdir, open, readFile, readdir, rename, rm, stat, unlink, } from "node:fs/promises";
4
+ import { dirname, join, relative } from "node:path";
5
+ import { buildDataFilePath } from "@opendatalabs/personal-server-ts-core/storage/hierarchy";
6
+ import { cacheRequestBodyBytes } from "@opendatalabs/personal-server-ts-core/auth";
7
+ import { publishStagedDataFile } from "../storage/hierarchy.js";
8
+ export const SCOPE_IMPORT_CHUNK_BYTES = 8 * 1024 * 1024;
9
+ export const SCOPE_IMPORT_MAX_BYTES = 256 * 1024 * 1024;
10
+ export const SCOPE_IMPORT_MAX_CHUNKS = 32;
11
+ export const SCOPE_IMPORT_TTL_MS = 24 * 60 * 60 * 1000;
12
+ const IMPORT_DIR = ".scope-imports";
13
+ const RECEIPT_TTL_MS = 30 * 24 * 60 * 60 * 1000;
14
+ const MAX_DURABLE_RECEIPTS = 10_000;
15
+ const SHA256 = /^[a-f0-9]{64}$/i;
16
+ const openingScopes = new Set();
17
+ let beginReservationActive = false;
18
+ const activeChunkIndexes = new Set();
19
+ const finalizingImports = new Set();
20
+ const abortingImports = new Set();
21
+ let activeChunkWrites = 0;
22
+ /** Validates JSON incrementally without retaining property names or string values. */
23
+ class JsonObjectStreamValidator {
24
+ // Preserve a leading BOM so the JSON grammar rejects it. The input bytes are
25
+ // embedded inside an envelope, where silently stripping a BOM would make the
26
+ // stored payload invalid JSON.
27
+ decoder = new TextDecoder("utf-8", {
28
+ fatal: true,
29
+ ignoreBOM: true,
30
+ });
31
+ frames = [];
32
+ rootState = "value";
33
+ stringState;
34
+ unicodeRemaining = 0;
35
+ unicodeDigits = "";
36
+ capturingRootKey = false;
37
+ rootKey = "";
38
+ primitive;
39
+ numberState;
40
+ write(bytes) {
41
+ this.consume(this.decoder.decode(bytes, { stream: true }));
42
+ }
43
+ end() {
44
+ this.consume(this.decoder.decode());
45
+ if (this.stringState ||
46
+ this.primitive ||
47
+ this.numberState ||
48
+ this.frames.length ||
49
+ this.rootState !== "done") {
50
+ throw new Error("Incomplete JSON value");
51
+ }
52
+ }
53
+ current() {
54
+ return this.frames.at(-1);
55
+ }
56
+ isSpace(char) {
57
+ return char === " " || char === "\n" || char === "\r" || char === "\t";
58
+ }
59
+ canEndPrimitive(char) {
60
+ return this.isSpace(char) || char === "," || char === "]" || char === "}";
61
+ }
62
+ beginValue(char) {
63
+ const frame = this.current();
64
+ if (frame) {
65
+ if (frame.kind === "object" && frame.state !== "value")
66
+ throw new Error("Unexpected JSON value");
67
+ if (frame.kind === "array" &&
68
+ frame.state !== "valueOrEnd" &&
69
+ frame.state !== "value")
70
+ throw new Error("Unexpected JSON value");
71
+ frame.state = "commaOrEnd";
72
+ }
73
+ else {
74
+ if (this.rootState !== "value" || char !== "{")
75
+ throw new Error("Top-level scope data must be an object");
76
+ this.rootState = "done";
77
+ }
78
+ if (char === "{") {
79
+ if (this.frames.length >= 256)
80
+ throw new Error("JSON nesting is too deep");
81
+ this.frames.push({ kind: "object", state: "keyOrEnd" });
82
+ }
83
+ else if (char === "[") {
84
+ if (this.frames.length >= 256)
85
+ throw new Error("JSON nesting is too deep");
86
+ this.frames.push({ kind: "array", state: "valueOrEnd" });
87
+ }
88
+ else if (char === '"') {
89
+ this.stringState = "normal";
90
+ this.capturingRootKey = false;
91
+ }
92
+ else if (char === "t")
93
+ this.primitive = { word: "true", offset: 1 };
94
+ else if (char === "f")
95
+ this.primitive = { word: "false", offset: 1 };
96
+ else if (char === "n")
97
+ this.primitive = { word: "null", offset: 1 };
98
+ else if (char === "-")
99
+ this.numberState = "minus";
100
+ else if (char === "0")
101
+ this.numberState = "zero";
102
+ else if (/[1-9]/.test(char))
103
+ this.numberState = "integer";
104
+ else
105
+ throw new Error("Invalid JSON value");
106
+ }
107
+ consume(text) {
108
+ for (let index = 0; index < text.length; index++) {
109
+ const char = text[index];
110
+ if (this.stringState) {
111
+ if (this.stringState === "unicode") {
112
+ if (!/[0-9a-f]/i.test(char))
113
+ throw new Error("Invalid JSON escape");
114
+ this.unicodeDigits += char;
115
+ this.unicodeRemaining--;
116
+ if (this.unicodeRemaining === 0) {
117
+ this.appendRootKey(String.fromCharCode(Number.parseInt(this.unicodeDigits, 16)));
118
+ this.unicodeDigits = "";
119
+ this.stringState = "normal";
120
+ }
121
+ }
122
+ else if (this.stringState === "escape") {
123
+ if (char === "u") {
124
+ this.stringState = "unicode";
125
+ this.unicodeRemaining = 4;
126
+ }
127
+ else if ('"\\/bfnrt'.includes(char)) {
128
+ const escaped = {
129
+ '"': '"',
130
+ "\\": "\\",
131
+ "/": "/",
132
+ b: "\b",
133
+ f: "\f",
134
+ n: "\n",
135
+ r: "\r",
136
+ t: "\t",
137
+ };
138
+ this.appendRootKey(escaped[char]);
139
+ this.stringState = "normal";
140
+ }
141
+ else
142
+ throw new Error("Invalid JSON escape");
143
+ }
144
+ else if (char === "\\")
145
+ this.stringState = "escape";
146
+ else if (char === '"') {
147
+ this.stringState = undefined;
148
+ const frame = this.current();
149
+ if (frame?.kind === "object" && frame.state === "key") {
150
+ frame.state = "colon";
151
+ if (this.capturingRootKey) {
152
+ if (this.rootKey === "$writtenBy" ||
153
+ this.rootKey === "$lineage" ||
154
+ this.rootKey === "$pdpp") {
155
+ throw new Error("Reserved root property");
156
+ }
157
+ this.capturingRootKey = false;
158
+ }
159
+ }
160
+ }
161
+ else if (char.charCodeAt(0) < 0x20)
162
+ throw new Error("Invalid control character in JSON string");
163
+ else
164
+ this.appendRootKey(char);
165
+ continue;
166
+ }
167
+ if (this.primitive) {
168
+ if (this.primitive.offset < this.primitive.word.length) {
169
+ if (char !== this.primitive.word[this.primitive.offset])
170
+ throw new Error("Invalid JSON literal");
171
+ this.primitive.offset++;
172
+ continue;
173
+ }
174
+ if (!this.canEndPrimitive(char))
175
+ throw new Error("Invalid JSON literal ending");
176
+ this.primitive = undefined;
177
+ index--;
178
+ continue;
179
+ }
180
+ if (this.numberState) {
181
+ const state = this.numberState;
182
+ const digit = char >= "0" && char <= "9";
183
+ if (state === "minus") {
184
+ if (char === "0")
185
+ this.numberState = "zero";
186
+ else if (char >= "1" && char <= "9")
187
+ this.numberState = "integer";
188
+ else
189
+ throw new Error("Invalid JSON number");
190
+ }
191
+ else if (state === "zero") {
192
+ if (char === ".")
193
+ this.numberState = "fractionStart";
194
+ else if (char === "e" || char === "E")
195
+ this.numberState = "exponentStart";
196
+ else if (this.canEndPrimitive(char)) {
197
+ this.numberState = undefined;
198
+ index--;
199
+ }
200
+ else
201
+ throw new Error("Invalid JSON number");
202
+ }
203
+ else if (state === "integer" ||
204
+ state === "fraction" ||
205
+ state === "exponent") {
206
+ if (digit)
207
+ continue;
208
+ if (state === "integer" && char === ".")
209
+ this.numberState = "fractionStart";
210
+ else if ((state === "integer" || state === "fraction") &&
211
+ (char === "e" || char === "E"))
212
+ this.numberState = "exponentStart";
213
+ else if (this.canEndPrimitive(char)) {
214
+ this.numberState = undefined;
215
+ index--;
216
+ }
217
+ else
218
+ throw new Error("Invalid JSON number");
219
+ }
220
+ else if (state === "fractionStart") {
221
+ if (!digit)
222
+ throw new Error("Invalid JSON number");
223
+ this.numberState = "fraction";
224
+ }
225
+ else if (state === "exponentStart") {
226
+ if (char === "+" || char === "-")
227
+ this.numberState = "exponentSign";
228
+ else if (digit)
229
+ this.numberState = "exponent";
230
+ else
231
+ throw new Error("Invalid JSON number");
232
+ }
233
+ else if (state === "exponentSign") {
234
+ if (!digit)
235
+ throw new Error("Invalid JSON number");
236
+ this.numberState = "exponent";
237
+ }
238
+ continue;
239
+ }
240
+ if (this.isSpace(char))
241
+ continue;
242
+ const frame = this.current();
243
+ if (frame?.kind === "object") {
244
+ if (frame.state === "keyOrEnd" && char === "}")
245
+ this.frames.pop();
246
+ else if (frame.state === "keyOrEnd" && char === '"') {
247
+ frame.state = "key";
248
+ this.stringState = "normal";
249
+ this.beginRootKey();
250
+ }
251
+ else if (frame.state === "key" && char === '"') {
252
+ this.stringState = "normal";
253
+ this.beginRootKey();
254
+ }
255
+ else if (frame.state === "colon" && char === ":")
256
+ frame.state = "value";
257
+ else if (frame.state === "commaOrEnd" && char === ",")
258
+ frame.state = "key";
259
+ else if (frame.state === "commaOrEnd" && char === "}")
260
+ this.frames.pop();
261
+ else if (frame.state === "value")
262
+ this.beginValue(char);
263
+ else
264
+ throw new Error("Invalid JSON object structure");
265
+ }
266
+ else if (frame?.kind === "array") {
267
+ if (frame.state === "valueOrEnd" && char === "]")
268
+ this.frames.pop();
269
+ else if (frame.state === "commaOrEnd" && char === ",")
270
+ frame.state = "value";
271
+ else if (frame.state === "commaOrEnd" && char === "]")
272
+ this.frames.pop();
273
+ else if (frame.state === "valueOrEnd" || frame.state === "value")
274
+ this.beginValue(char);
275
+ else
276
+ throw new Error("Invalid JSON array structure");
277
+ }
278
+ else if (this.rootState === "value")
279
+ this.beginValue(char);
280
+ else
281
+ throw new Error("Unexpected content after JSON value");
282
+ }
283
+ }
284
+ beginRootKey() {
285
+ this.capturingRootKey = this.frames.length === 1;
286
+ this.rootKey = "";
287
+ }
288
+ appendRootKey(char) {
289
+ if (this.capturingRootKey && this.rootKey.length <= 32)
290
+ this.rootKey += char;
291
+ }
292
+ }
293
+ export class ScopeImportError extends Error {
294
+ status;
295
+ code;
296
+ constructor(status, code, message) {
297
+ super(message);
298
+ this.status = status;
299
+ this.code = code;
300
+ }
301
+ }
302
+ function notifyCommittedScope(deps, scope, collectedAt) {
303
+ try {
304
+ deps.syncManager?.notifyNewData?.();
305
+ if (deps.syncManager && !deps.syncManager.notifyNewData) {
306
+ void deps.syncManager.trigger?.().catch(() => undefined);
307
+ }
308
+ }
309
+ catch {
310
+ // The scope is committed. Notification hooks cannot roll it back.
311
+ }
312
+ try {
313
+ deps.onDataWritten?.({ scope, collectedAt });
314
+ }
315
+ catch {
316
+ // The scope is committed. Notification hooks cannot roll it back.
317
+ }
318
+ }
319
+ function importsDir(deps) {
320
+ return join(deps.hierarchyOptions.dataDir, IMPORT_DIR);
321
+ }
322
+ function importPath(deps, id) {
323
+ return join(importsDir(deps), id);
324
+ }
325
+ function receiptPath(deps, id) {
326
+ if (!/^[0-9a-f-]{36}$/i.test(id)) {
327
+ throw new ScopeImportError(404, "IMPORT_NOT_FOUND", "Import not found");
328
+ }
329
+ return join(importsDir(deps), "receipts", `${id}.json`);
330
+ }
331
+ async function loadMetadata(deps, id, allowExpired = false) {
332
+ if (!/^[0-9a-f-]{36}$/i.test(id)) {
333
+ throw new ScopeImportError(404, "IMPORT_NOT_FOUND", "Import not found");
334
+ }
335
+ try {
336
+ const metadata = JSON.parse(await readFile(join(importPath(deps, id), "metadata.json"), "utf8"));
337
+ if (!allowExpired &&
338
+ Date.now() - metadata.createdAt > SCOPE_IMPORT_TTL_MS) {
339
+ throw new ScopeImportError(410, "IMPORT_EXPIRED", "Import has expired");
340
+ }
341
+ return metadata;
342
+ }
343
+ catch (error) {
344
+ if (error instanceof ScopeImportError)
345
+ throw error;
346
+ throw new ScopeImportError(404, "IMPORT_NOT_FOUND", "Import not found");
347
+ }
348
+ }
349
+ async function saveMetadata(deps, metadata) {
350
+ const directory = importPath(deps, metadata.id);
351
+ const tempPath = join(directory, `metadata.${randomUUID()}.tmp`);
352
+ await writeAtomic(tempPath, join(directory, "metadata.json"), Buffer.from(JSON.stringify(metadata)));
353
+ }
354
+ async function writeAtomic(tempPath, finalPath, bytes) {
355
+ await mkdir(dirname(finalPath), { recursive: true });
356
+ await (await open(tempPath, "wx")).close();
357
+ const handle = await open(tempPath, "w");
358
+ try {
359
+ await handle.writeFile(bytes);
360
+ await handle.sync();
361
+ }
362
+ finally {
363
+ await handle.close();
364
+ }
365
+ await rename(tempPath, finalPath);
366
+ const directory = await open(dirname(finalPath), "r");
367
+ try {
368
+ await directory.sync();
369
+ }
370
+ finally {
371
+ await directory.close();
372
+ }
373
+ }
374
+ async function pathExists(path) {
375
+ try {
376
+ await stat(path);
377
+ return true;
378
+ }
379
+ catch {
380
+ return false;
381
+ }
382
+ }
383
+ async function removeImportArtifacts(deps, metadata) {
384
+ const finalPath = buildDataFilePath(deps.hierarchyOptions.dataDir, metadata.scope, metadata.collectedAt);
385
+ await unlink(`${finalPath}.pending.${metadata.id}`).catch(() => undefined);
386
+ const relativePath = relative(deps.hierarchyOptions.dataDir, finalPath);
387
+ if (!deps.indexManager.findByPath(relativePath)) {
388
+ await unlink(finalPath).catch(() => undefined);
389
+ }
390
+ }
391
+ export async function cleanupExpiredScopeImports(deps, now = Date.now(), removeTemps = false) {
392
+ const root = importsDir(deps);
393
+ await mkdir(root, { recursive: true });
394
+ for (const entry of await readdir(root, { withFileTypes: true })) {
395
+ if (!entry.isDirectory() || entry.name === "receipts")
396
+ continue;
397
+ const directory = join(root, entry.name);
398
+ try {
399
+ const metadata = JSON.parse(await readFile(join(directory, "metadata.json"), "utf8"));
400
+ if (now - metadata.createdAt > SCOPE_IMPORT_TTL_MS) {
401
+ await removeImportArtifacts(deps, metadata);
402
+ await rm(directory, { recursive: true, force: true });
403
+ continue;
404
+ }
405
+ if (removeTemps) {
406
+ for (const item of await readdir(directory, { withFileTypes: true })) {
407
+ if (item.isFile() && item.name.endsWith(".tmp")) {
408
+ await unlink(join(directory, item.name));
409
+ }
410
+ }
411
+ }
412
+ }
413
+ catch {
414
+ await rm(directory, { recursive: true, force: true });
415
+ }
416
+ }
417
+ const receipts = join(root, "receipts");
418
+ await mkdir(receipts, { recursive: true });
419
+ for (const name of await readdir(receipts)) {
420
+ const path = join(receipts, name);
421
+ try {
422
+ const receipt = JSON.parse(await readFile(path, "utf8"));
423
+ if (now - Date.parse(receipt.completedAt) > RECEIPT_TTL_MS) {
424
+ await unlink(path);
425
+ }
426
+ }
427
+ catch {
428
+ await unlink(path).catch(() => undefined);
429
+ }
430
+ }
431
+ }
432
+ export async function beginScopeImport(deps, request, scope, body) {
433
+ await deps.authorizeOwner(request, scope);
434
+ if (!deps.serverOwner) {
435
+ throw new ScopeImportError(500, "SERVER_NOT_CONFIGURED", "Server owner is not configured");
436
+ }
437
+ const scopeKey = `${deps.serverOwner.toLowerCase()}:${scope}`;
438
+ if (openingScopes.has(scopeKey)) {
439
+ throw new ScopeImportError(409, "IMPORT_IN_PROGRESS", "An import for this scope is already being started");
440
+ }
441
+ openingScopes.add(scopeKey);
442
+ if (beginReservationActive) {
443
+ openingScopes.delete(scopeKey);
444
+ throw new ScopeImportError(429, "IMPORT_CAPACITY", "Another import is being reserved; retry shortly");
445
+ }
446
+ beginReservationActive = true;
447
+ try {
448
+ await cleanupExpiredScopeImports(deps);
449
+ const input = body !== null && typeof body === "object" && !Array.isArray(body)
450
+ ? body
451
+ : {};
452
+ if (!Number.isSafeInteger(input.totalBytes) ||
453
+ input.totalBytes <= 0 ||
454
+ input.totalBytes > SCOPE_IMPORT_MAX_BYTES ||
455
+ !Number.isSafeInteger(input.totalChunks) ||
456
+ input.totalChunks <= 0 ||
457
+ input.totalChunks > SCOPE_IMPORT_MAX_CHUNKS ||
458
+ input.totalChunks !==
459
+ Math.ceil(input.totalBytes / SCOPE_IMPORT_CHUNK_BYTES) ||
460
+ typeof input.sha256 !== "string" ||
461
+ !SHA256.test(input.sha256)) {
462
+ throw new ScopeImportError(400, "INVALID_IMPORT", "Import size, chunk count, or SHA-256 is invalid");
463
+ }
464
+ const receiptCount = (await readdir(join(importsDir(deps), "receipts")))
465
+ .length;
466
+ const active = (await readdir(importsDir(deps), { withFileTypes: true })).filter((item) => item.isDirectory() && item.name !== "receipts");
467
+ let activeBytes = 0;
468
+ let activeForScope = false;
469
+ for (const item of active) {
470
+ try {
471
+ const metadata = JSON.parse(await readFile(join(importsDir(deps), item.name, "metadata.json"), "utf8"));
472
+ activeBytes += metadata.totalBytes;
473
+ activeForScope ||= metadata.scope === scope;
474
+ }
475
+ catch {
476
+ // Invalid entries are removed by cleanup on the next request.
477
+ }
478
+ }
479
+ if (receiptCount + active.length >= MAX_DURABLE_RECEIPTS) {
480
+ throw new ScopeImportError(429, "RECEIPT_CAPACITY", "Import receipt storage is full");
481
+ }
482
+ if (activeForScope ||
483
+ active.length >= 4 ||
484
+ activeBytes + input.totalBytes > SCOPE_IMPORT_MAX_BYTES * 2) {
485
+ throw new ScopeImportError(429, "IMPORT_CAPACITY", "Import storage capacity is full");
486
+ }
487
+ const id = randomUUID();
488
+ let collectedAt = new Date().toISOString();
489
+ while (await pathExists(buildDataFilePath(deps.hierarchyOptions.dataDir, scope, collectedAt))) {
490
+ collectedAt = new Date(Date.parse(collectedAt) + 1).toISOString();
491
+ }
492
+ const metadata = {
493
+ id,
494
+ owner: deps.serverOwner.toLowerCase(),
495
+ scope,
496
+ totalBytes: input.totalBytes,
497
+ totalChunks: input.totalChunks,
498
+ sha256: input.sha256.toLowerCase(),
499
+ collectedAt,
500
+ createdAt: Date.now(),
501
+ chunks: {},
502
+ };
503
+ await mkdir(importPath(deps, id), { recursive: true });
504
+ await saveMetadata(deps, metadata);
505
+ return {
506
+ importId: id,
507
+ chunkBytes: SCOPE_IMPORT_CHUNK_BYTES,
508
+ expiresAt: new Date(metadata.createdAt + SCOPE_IMPORT_TTL_MS).toISOString(),
509
+ };
510
+ }
511
+ finally {
512
+ beginReservationActive = false;
513
+ openingScopes.delete(scopeKey);
514
+ }
515
+ }
516
+ async function writeChunkToFile(bytes, path) {
517
+ if (bytes.byteLength === 0)
518
+ throw new ScopeImportError(400, "EMPTY_CHUNK", "Chunk body is required");
519
+ const handle = await open(path, "wx");
520
+ const hash = createHash("sha256");
521
+ try {
522
+ if (bytes.byteLength > SCOPE_IMPORT_CHUNK_BYTES) {
523
+ throw new ScopeImportError(413, "CHUNK_TOO_LARGE", "Chunk exceeds 8 MiB");
524
+ }
525
+ hash.update(bytes);
526
+ await handle.writeFile(bytes);
527
+ await handle.sync();
528
+ }
529
+ catch (error) {
530
+ await unlink(path).catch(() => undefined);
531
+ throw error;
532
+ }
533
+ finally {
534
+ await handle.close();
535
+ }
536
+ return { bytes: bytes.byteLength, sha256: hash.digest("hex") };
537
+ }
538
+ export async function putScopeImportChunk(deps, request, scope, id, index) {
539
+ await deps.authorizeOwner(request, scope);
540
+ const bodyBytes = await cacheRequestBodyBytes(request, SCOPE_IMPORT_CHUNK_BYTES);
541
+ if (!bodyBytes) {
542
+ throw new ScopeImportError(400, "EMPTY_CHUNK", "Chunk body is required");
543
+ }
544
+ if (abortingImports.has(id)) {
545
+ throw new ScopeImportError(409, "IMPORT_ABORTING", "Import is being aborted");
546
+ }
547
+ const metadata = await loadMetadata(deps, id);
548
+ if (metadata.owner !== deps.serverOwner?.toLowerCase() ||
549
+ metadata.scope !== scope) {
550
+ throw new ScopeImportError(404, "IMPORT_NOT_FOUND", "Import not found");
551
+ }
552
+ if (!Number.isSafeInteger(index) ||
553
+ index < 0 ||
554
+ index >= metadata.totalChunks) {
555
+ throw new ScopeImportError(400, "INVALID_CHUNK_INDEX", "Chunk index is out of range");
556
+ }
557
+ if (request.headers.get("content-type")?.split(";", 1)[0] !==
558
+ "application/octet-stream") {
559
+ throw new ScopeImportError(415, "INVALID_CHUNK_TYPE", "Chunk must use application/octet-stream");
560
+ }
561
+ if (abortingImports.has(id)) {
562
+ throw new ScopeImportError(409, "IMPORT_ABORTING", "Import is being aborted");
563
+ }
564
+ const chunkKey = `${id}:${index}`;
565
+ if (activeChunkIndexes.has(chunkKey)) {
566
+ throw new ScopeImportError(409, "CHUNK_IN_PROGRESS", "This chunk is already being uploaded");
567
+ }
568
+ const present = new Set(Object.keys(metadata.chunks).map(Number));
569
+ let firstMissing = 0;
570
+ while (present.has(firstMissing))
571
+ firstMissing++;
572
+ if (index > firstMissing) {
573
+ throw new ScopeImportError(409, "CHUNK_GAP", `Expected chunk ${firstMissing}`);
574
+ }
575
+ const expectedBytes = index === metadata.totalChunks - 1
576
+ ? metadata.totalBytes - SCOPE_IMPORT_CHUNK_BYTES * index
577
+ : SCOPE_IMPORT_CHUNK_BYTES;
578
+ const expectedHash = request.headers.get("x-chunk-sha256")?.toLowerCase();
579
+ if (!expectedHash || !SHA256.test(expectedHash)) {
580
+ throw new ScopeImportError(400, "INVALID_CHUNK_HASH", "X-Chunk-SHA256 must be a SHA-256 hex digest");
581
+ }
582
+ const directory = importPath(deps, id);
583
+ const target = join(directory, `chunk-${index}.bin`);
584
+ const temp = join(directory, `chunk-${index}.${randomUUID()}.tmp`);
585
+ if (activeChunkWrites >= 4) {
586
+ throw new ScopeImportError(429, "UPLOAD_CAPACITY", "Too many chunks are being uploaded");
587
+ }
588
+ activeChunkWrites++;
589
+ activeChunkIndexes.add(chunkKey);
590
+ try {
591
+ const received = await writeChunkToFile(bodyBytes, temp);
592
+ if (received.bytes !== expectedBytes || received.sha256 !== expectedHash) {
593
+ await unlink(temp).catch(() => undefined);
594
+ throw new ScopeImportError(422, "CHUNK_HASH_MISMATCH", "Chunk size or SHA-256 does not match");
595
+ }
596
+ const existing = metadata.chunks[String(index)];
597
+ if (existing) {
598
+ await unlink(temp);
599
+ if (existing.bytes !== received.bytes ||
600
+ existing.sha256 !== received.sha256) {
601
+ throw new ScopeImportError(409, "CHUNK_CONFLICT", "Chunk index already has different content");
602
+ }
603
+ return { accepted: true, index, ...existing };
604
+ }
605
+ await rename(temp, target);
606
+ const directoryHandle = await open(directory, "r");
607
+ try {
608
+ await directoryHandle.sync();
609
+ }
610
+ finally {
611
+ await directoryHandle.close();
612
+ }
613
+ metadata.chunks[String(index)] = received;
614
+ await saveMetadata(deps, metadata);
615
+ return { accepted: true, index, ...received };
616
+ }
617
+ finally {
618
+ activeChunkWrites--;
619
+ activeChunkIndexes.delete(chunkKey);
620
+ }
621
+ }
622
+ export async function finalizeScopeImport(deps, request, scope, id) {
623
+ await deps.authorizeOwner(request, scope);
624
+ if (abortingImports.has(id)) {
625
+ throw new ScopeImportError(409, "IMPORT_ABORTING", "Import is being aborted");
626
+ }
627
+ if (finalizingImports.has(id)) {
628
+ throw new ScopeImportError(409, "IMPORT_FINALIZING", "Import is already being finalized");
629
+ }
630
+ finalizingImports.add(id);
631
+ try {
632
+ return await finalizeScopeImportAuthorized(deps, scope, id);
633
+ }
634
+ finally {
635
+ finalizingImports.delete(id);
636
+ }
637
+ }
638
+ export async function abortScopeImport(deps, request, scope, id) {
639
+ await deps.authorizeOwner(request, scope);
640
+ if (finalizingImports.has(id)) {
641
+ throw new ScopeImportError(409, "IMPORT_FINALIZING", "Import is already being finalized");
642
+ }
643
+ if (abortingImports.has(id)) {
644
+ throw new ScopeImportError(409, "IMPORT_ABORTING", "Import is already being aborted");
645
+ }
646
+ abortingImports.add(id);
647
+ try {
648
+ if ([...activeChunkIndexes].some((key) => key.startsWith(`${id}:`))) {
649
+ throw new ScopeImportError(409, "CHUNK_IN_PROGRESS", "A chunk for this import is still being uploaded");
650
+ }
651
+ const receiptFile = receiptPath(deps, id);
652
+ let prior;
653
+ try {
654
+ prior = JSON.parse(await readFile(receiptFile, "utf8"));
655
+ }
656
+ catch {
657
+ // No terminal receipt exists yet.
658
+ }
659
+ if (prior) {
660
+ if (prior.owner !== deps.serverOwner?.toLowerCase() ||
661
+ prior.scope !== scope) {
662
+ throw new ScopeImportError(404, "IMPORT_NOT_FOUND", "Import not found");
663
+ }
664
+ if (prior.status !== "aborted") {
665
+ throw new ScopeImportError(409, "IMPORT_FINALIZED", "A finalized import cannot be aborted");
666
+ }
667
+ try {
668
+ const metadata = await loadMetadata(deps, id, true);
669
+ if (metadata.owner === prior.owner && metadata.scope === prior.scope) {
670
+ await removeImportArtifacts(deps, metadata);
671
+ await rm(importPath(deps, id), { recursive: true, force: true });
672
+ }
673
+ }
674
+ catch (error) {
675
+ if (!(error instanceof ScopeImportError) || error.status !== 404) {
676
+ throw error;
677
+ }
678
+ }
679
+ return prior;
680
+ }
681
+ const metadata = await loadMetadata(deps, id, true);
682
+ if (metadata.owner !== deps.serverOwner?.toLowerCase() ||
683
+ metadata.scope !== scope) {
684
+ throw new ScopeImportError(404, "IMPORT_NOT_FOUND", "Import not found");
685
+ }
686
+ const abortedAt = new Date().toISOString();
687
+ const receipt = {
688
+ importId: id,
689
+ owner: metadata.owner,
690
+ scope,
691
+ status: "aborted",
692
+ abortedAt,
693
+ completedAt: abortedAt,
694
+ };
695
+ await writeAtomic(`${receiptFile}.${randomUUID()}.tmp`, receiptFile, Buffer.from(JSON.stringify(receipt)));
696
+ await removeImportArtifacts(deps, metadata);
697
+ await rm(importPath(deps, id), { recursive: true, force: true });
698
+ return receipt;
699
+ }
700
+ finally {
701
+ abortingImports.delete(id);
702
+ }
703
+ }
704
+ async function finalizeScopeImportAuthorized(deps, scope, id) {
705
+ try {
706
+ const prior = JSON.parse(await readFile(receiptPath(deps, id), "utf8"));
707
+ if (prior.scope === scope &&
708
+ prior.owner === deps.serverOwner?.toLowerCase()) {
709
+ if (prior.status === "aborted") {
710
+ throw new ScopeImportError(410, "IMPORT_ABORTED", "Import was aborted");
711
+ }
712
+ return prior;
713
+ }
714
+ }
715
+ catch (error) {
716
+ if (error instanceof ScopeImportError)
717
+ throw error;
718
+ // No prior receipt.
719
+ }
720
+ const metadata = await loadMetadata(deps, id);
721
+ if (metadata.owner !== deps.serverOwner?.toLowerCase() ||
722
+ metadata.scope !== scope) {
723
+ throw new ScopeImportError(404, "IMPORT_NOT_FOUND", "Import not found");
724
+ }
725
+ if (Object.keys(metadata.chunks).length !== metadata.totalChunks) {
726
+ throw new ScopeImportError(409, "IMPORT_INCOMPLETE", "Not all declared chunks have been uploaded");
727
+ }
728
+ let finalPath = buildDataFilePath(deps.hierarchyOptions.dataDir, scope, metadata.collectedAt);
729
+ let stagePath = `${finalPath}.pending.${id}`;
730
+ let relativePath = relative(deps.hierarchyOptions.dataDir, finalPath);
731
+ const committedEntry = deps.indexManager.findByPath(relativePath);
732
+ if (committedEntry?.scope === scope &&
733
+ committedEntry.collectedAt === metadata.collectedAt) {
734
+ if (!(await pathExists(finalPath)) && (await pathExists(stagePath))) {
735
+ await publishStagedDataFile(stagePath, finalPath);
736
+ }
737
+ if (await pathExists(finalPath)) {
738
+ const receipt = {
739
+ importId: id,
740
+ owner: metadata.owner,
741
+ scope,
742
+ sha256: metadata.sha256,
743
+ totalBytes: metadata.totalBytes,
744
+ collectedAt: metadata.collectedAt,
745
+ completedAt: new Date().toISOString(),
746
+ };
747
+ const receiptFile = receiptPath(deps, id);
748
+ await writeAtomic(`${receiptFile}.${randomUUID()}.tmp`, receiptFile, Buffer.from(JSON.stringify(receipt)));
749
+ await rm(importPath(deps, id), { recursive: true, force: true });
750
+ notifyCommittedScope(deps, scope, metadata.collectedAt);
751
+ return receipt;
752
+ }
753
+ }
754
+ // A crash can publish the fully validated envelope but stop before its
755
+ // index transaction. Remove that unindexed publication before retrying with
756
+ // a fresh timestamp so repeated retries cannot leave visible orphan files.
757
+ if (!committedEntry && (await pathExists(finalPath))) {
758
+ await unlink(finalPath);
759
+ }
760
+ const latestBeforeStage = deps.indexManager.findLatestByScope(scope);
761
+ const latestTime = latestBeforeStage
762
+ ? Date.parse(latestBeforeStage.collectedAt)
763
+ : Number.NEGATIVE_INFINITY;
764
+ let collectedAt = new Date(Math.max(Date.now(), latestTime + 1)).toISOString();
765
+ while (await pathExists(buildDataFilePath(deps.hierarchyOptions.dataDir, scope, collectedAt))) {
766
+ collectedAt = new Date(Date.parse(collectedAt) + 1).toISOString();
767
+ }
768
+ metadata.collectedAt = collectedAt;
769
+ await saveMetadata(deps, metadata);
770
+ finalPath = buildDataFilePath(deps.hierarchyOptions.dataDir, scope, metadata.collectedAt);
771
+ stagePath = `${finalPath}.pending.${id}`;
772
+ relativePath = relative(deps.hierarchyOptions.dataDir, finalPath);
773
+ await mkdir(dirname(finalPath), { recursive: true });
774
+ const file = await open(stagePath, "wx");
775
+ const hash = createHash("sha256");
776
+ const validator = new JsonObjectStreamValidator();
777
+ let total = 0;
778
+ const write = async (bytes) => {
779
+ await file.writeFile(bytes);
780
+ };
781
+ try {
782
+ await write(Buffer.from(`{"version":"1.0","scope":${JSON.stringify(scope)},"collectedAt":${JSON.stringify(metadata.collectedAt)},"data":`));
783
+ for (let index = 0; index < metadata.totalChunks; index++) {
784
+ const chunkPath = join(importPath(deps, id), `chunk-${index}.bin`);
785
+ const chunkHash = createHash("sha256");
786
+ let chunkBytes = 0;
787
+ for await (const chunk of createReadStream(chunkPath)) {
788
+ const bytes = chunk;
789
+ chunkBytes += bytes.byteLength;
790
+ total += bytes.byteLength;
791
+ chunkHash.update(bytes);
792
+ hash.update(bytes);
793
+ try {
794
+ validator.write(bytes);
795
+ }
796
+ catch {
797
+ throw new ScopeImportError(400, "INVALID_SCOPE_JSON", "Scope data is not valid JSON");
798
+ }
799
+ await write(bytes);
800
+ }
801
+ const declared = metadata.chunks[String(index)];
802
+ if (chunkBytes !== declared.bytes ||
803
+ chunkHash.digest("hex") !== declared.sha256) {
804
+ throw new ScopeImportError(422, "CHUNK_CORRUPT", `Stored chunk ${index} failed its hash check`);
805
+ }
806
+ }
807
+ try {
808
+ validator.end();
809
+ }
810
+ catch {
811
+ throw new ScopeImportError(400, "INVALID_SCOPE_JSON", "Scope data is not valid JSON");
812
+ }
813
+ const digest = hash.digest("hex");
814
+ if (total !== metadata.totalBytes || digest !== metadata.sha256) {
815
+ throw new ScopeImportError(422, "IMPORT_HASH_MISMATCH", "Imported scope size or SHA-256 does not match");
816
+ }
817
+ await write(Buffer.from("}"));
818
+ await file.sync();
819
+ }
820
+ catch (error) {
821
+ await unlink(stagePath).catch(() => undefined);
822
+ if (error instanceof ScopeImportError)
823
+ throw error;
824
+ throw error;
825
+ }
826
+ finally {
827
+ await file.close();
828
+ }
829
+ try {
830
+ const sizeBytes = (await stat(stagePath)).size;
831
+ const afterTombstoneVersion = (await deps.afterTombstoneVersion?.(scope)) ?? null;
832
+ const latestAtCommit = deps.indexManager.findLatestByScope(scope);
833
+ if (latestAtCommit &&
834
+ Date.parse(latestAtCommit.collectedAt) >= Date.parse(metadata.collectedAt)) {
835
+ await unlink(stagePath).catch(() => undefined);
836
+ metadata.collectedAt = new Date(Date.parse(latestAtCommit.collectedAt) + 1).toISOString();
837
+ await saveMetadata(deps, metadata);
838
+ throw new ScopeImportError(409, "SCOPE_CHANGED", "Scope changed while the import was in progress; retry finalize");
839
+ }
840
+ // Publish the complete, fsynced envelope before swapping the SQLite index.
841
+ // Readers continue to resolve the old entry until the new file is ready.
842
+ await publishStagedDataFile(stagePath, finalPath);
843
+ let indexed;
844
+ try {
845
+ indexed = deps.indexManager.insertIfCurrent({
846
+ fileId: null,
847
+ schemaId: null,
848
+ path: relativePath,
849
+ scope,
850
+ collectedAt: metadata.collectedAt,
851
+ sizeBytes,
852
+ dataPointId: null,
853
+ afterTombstoneVersion,
854
+ producer: null,
855
+ producerProvenance: null,
856
+ }, latestAtCommit
857
+ ? {
858
+ kind: "match",
859
+ version: latestAtCommit.casRevision ?? latestAtCommit.version,
860
+ }
861
+ : { kind: "none" });
862
+ }
863
+ catch (error) {
864
+ // The synchronous index API can throw. Keep the data file only if the
865
+ // transaction committed before the error became observable.
866
+ if (!deps.indexManager.findByPath(relativePath)) {
867
+ await unlink(finalPath).catch(() => undefined);
868
+ }
869
+ throw error;
870
+ }
871
+ if (!indexed.ok) {
872
+ await unlink(finalPath).catch(() => undefined);
873
+ const latest = deps.indexManager.findLatestByScope(scope);
874
+ if (latest) {
875
+ metadata.collectedAt = new Date(Date.parse(latest.collectedAt) + 1).toISOString();
876
+ await saveMetadata(deps, metadata);
877
+ }
878
+ throw new ScopeImportError(409, "SCOPE_CHANGED", "Scope changed while the import was in progress");
879
+ }
880
+ const receipt = {
881
+ importId: id,
882
+ owner: metadata.owner,
883
+ scope,
884
+ sha256: metadata.sha256,
885
+ totalBytes: metadata.totalBytes,
886
+ collectedAt: metadata.collectedAt,
887
+ completedAt: new Date().toISOString(),
888
+ };
889
+ const receiptFile = receiptPath(deps, id);
890
+ const receiptTemp = `${receiptFile}.${randomUUID()}.tmp`;
891
+ await writeAtomic(receiptTemp, receiptFile, Buffer.from(JSON.stringify(receipt)));
892
+ await rm(importPath(deps, id), { recursive: true, force: true });
893
+ notifyCommittedScope(deps, scope, metadata.collectedAt);
894
+ return receipt;
895
+ }
896
+ catch (error) {
897
+ await removeImportArtifacts(deps, metadata);
898
+ throw error;
899
+ }
900
+ }
901
+ //# sourceMappingURL=chunked-scope-import.js.map