@warmhub/sdk-ts 0.126.0 → 0.128.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,439 @@
1
1
  # @warmhub/sdk-ts changelog
2
2
 
3
+ <!-- warmhub-changelog-artifact:v1 -->
4
+
5
+ ## 0.128.0
6
+
7
+ ### Documentation
8
+
9
+ Versioned release notes are available as [HTML, Markdown, and JSON](https://docs.warmhub.ai/sdk/typescript/changelog/).
10
+
11
+ ---
12
+
13
+ Historical changelog shipped with @warmhub/sdk-ts 0.127.0:
14
+
15
+ # @warmhub/sdk-ts changelog
16
+
17
+ Release 0.127.0.
18
+
19
+ Changelog generation is temporarily disabled while the release integration is repaired.
20
+
21
+
22
+ Earlier changelog shipped with @warmhub/sdk-ts 0.126.0:
23
+
24
+ # @warmhub/sdk-ts changelog
25
+
3
26
  Release 0.126.0.
4
27
 
5
28
  Changelog generation is temporarily disabled while the release integration is repaired.
29
+
30
+
31
+ Earlier changelog shipped with @warmhub/sdk-ts 0.125.0:
32
+
33
+ # @warmhub/sdk-ts changelog
34
+
35
+ Release 0.125.0.
36
+
37
+ Changelog generation is temporarily disabled while the release integration is repaired.
38
+
39
+
40
+ Earlier changelog shipped with @warmhub/sdk-ts 0.124.0:
41
+
42
+ # @warmhub/sdk-ts changelog
43
+
44
+ Release 0.124.0.
45
+
46
+ Changelog generation is temporarily disabled while the release integration is repaired.
47
+
48
+
49
+ Earlier changelog shipped with @warmhub/sdk-ts 0.123.0:
50
+
51
+ # @warmhub/sdk-ts changelog
52
+
53
+ Release 0.123.0.
54
+
55
+ Changelog generation is temporarily disabled while the release integration is repaired.
56
+
57
+
58
+ Earlier changelog shipped with @warmhub/sdk-ts 0.122.3:
59
+
60
+ # @warmhub/sdk-ts changelog
61
+
62
+ Release 0.122.3.
63
+
64
+ Changelog generation is temporarily disabled while the release integration is repaired.
65
+
66
+
67
+ Earlier changelog shipped with @warmhub/sdk-ts 0.120.0:
68
+
69
+ # Changelog — `@warmhub/sdk-ts`
70
+
71
+ ## How to read this file
72
+
73
+ Entries are grouped by release. `## Unreleased` collects everything merged but
74
+ not yet published; the release pipeline is the sole version authority and
75
+ stamps the exact version at publish time, so no heading here ever invents one.
76
+
77
+ Within a release, four change sections appear, in this order. A section is
78
+ present only when it has entries; a release may add plain narrative sections
79
+ after them ("Behavior worth knowing", "Still supported") when the change needs
80
+ context that is not itself a change entry.
81
+
82
+ - **Breaking** — your code must change. Every item is written as a migration:
83
+ what it was → what it is now → the exact edit, with before/after code → why,
84
+ in one line, citing the ruling.
85
+ - **Added** — new surface. Nothing to do unless you want it.
86
+ - **Fixed** — behavior that was wrong and now is not.
87
+ - **Removed** — surface that is gone with nothing to put in its place. Anything
88
+ a caller can migrate to is filed under Breaking instead, so this section is
89
+ often absent.
90
+
91
+ WarmHub's source repository is private, so rulings are cited by pull-request
92
+ number rather than linked. Release notes live at
93
+ <https://docs.warmhub.ai/releases/overview/>.
94
+
95
+ The Python SDK, `warmhub`, keeps the same file in the same shape.
96
+
97
+ ---
98
+
99
+ ## Unreleased
100
+
101
+ ## 0.120.0
102
+
103
+ Repository export v3. The exact version is stamped at publish time by the
104
+ release pipeline; source manifests stay at `0.0.0-development`.
105
+
106
+ ### Breaking
107
+
108
+ #### Checkpoint generation is retired — take an export instead
109
+
110
+ **What it was.** Producing a fresh snapshot of a repository meant asking the
111
+ server to mint a checkpoint, polling until the job completed, requesting a
112
+ short-lived signed URL for the archive, downloading it outside the SDK, and
113
+ verifying the bytes locally.
114
+
115
+ **What it is now.** One call that streams verified rows, with the header,
116
+ per-row canonical decode, ordering and trailer checks applied as the bytes
117
+ arrive. There is no job to start, no state to poll, and no separate verify
118
+ step — a row that reaches you has already been proven.
119
+
120
+ **Migration.**
121
+
122
+ Before:
123
+
124
+ ```ts
125
+ import { createReadStream } from 'node:fs'
126
+ import { writeFile } from 'node:fs/promises'
127
+ import { Readable } from 'node:stream'
128
+ import { verifyRepositoryCheckpointArchive } from '@warmhub/sdk-ts/checkpoint'
129
+
130
+ let status = await client.repo.checkpoint.generate('acme', 'catalog')
131
+ while (status.state !== 'complete') {
132
+ await new Promise((resolve) => setTimeout(resolve, 1_000))
133
+ status = await client.repo.checkpoint.status('acme', 'catalog', {
134
+ checkpointId: status.checkpointId,
135
+ })
136
+ }
137
+
138
+ const access = await client.repo.checkpoint.getAccess('acme', 'catalog', {
139
+ checkpoint: 'latest',
140
+ artifact: 'archive',
141
+ })
142
+ // Signed URL: fetched WITHOUT the WarmHub bearer token.
143
+ const archive = await fetch(access.url)
144
+ await writeFile('catalog.zip', Readable.fromWeb(archive.body))
145
+ await verifyRepositoryCheckpointArchive(createReadStream('catalog.zip'))
146
+ ```
147
+
148
+ After:
149
+
150
+ ```ts
151
+ for await (const row of client.repo.export('acme', 'catalog')) {
152
+ console.log(row.kind, row.durableId, row.version)
153
+ }
154
+ ```
155
+
156
+ If you were relying on `generate` returning before the download — starting the
157
+ build in one process and fetching it in another, or on another machine — that
158
+ shape survives as the async token flow:
159
+
160
+ ```ts
161
+ // Process A: ask for it, get a token, do not read a byte.
162
+ const prepared = await client.repo.exportPrepare('acme', 'catalog')
163
+ console.log(prepared.exportToken, prepared.atRepoSeq)
164
+
165
+ // Process B: redeem it. `export` polls while the fold is still running.
166
+ for await (const row of client.repo.export('acme', 'catalog', {
167
+ token: prepared.exportToken,
168
+ })) {
169
+ // ...
170
+ }
171
+ ```
172
+
173
+ The token is redeemable only at that repository's URL, by a principal holding
174
+ checkpoint read there.
175
+
176
+ **Why.** Export format v3 replaces checkpoint generation outright; the whole
177
+ generation pipeline is deleted server-side (PR #10020).
178
+
179
+ #### `client.repo.checkpoint.generate` and `.retry` are gone
180
+
181
+ **What it was.** `generate(orgName, repoName, { atLeastRepoSeq })` minted a
182
+ checkpoint; `retry(orgName, repoName, checkpointId)` re-ran a failed one. Both
183
+ returned a `RepositoryCheckpointStatus`.
184
+
185
+ **What it is now.** Neither method exists, and the tRPC procedures behind them
186
+ (`repo.checkpoint.generate`, `repo.checkpoint.retry`) are no longer served. The
187
+ `repo:checkpoint-generate` scope no longer buys anything.
188
+
189
+ **Migration.** Replace both with `client.repo.export`.
190
+
191
+ Before:
192
+
193
+ ```ts
194
+ const fresh = await client.repo.checkpoint.generate('acme', 'catalog', {
195
+ atLeastRepoSeq: 42,
196
+ })
197
+ const recovered = await client.repo.checkpoint.retry(
198
+ 'acme',
199
+ 'catalog',
200
+ failed.checkpointId,
201
+ )
202
+ ```
203
+
204
+ After:
205
+
206
+ ```ts
207
+ // `atRepoSeq` pins the fence the way `atLeastRepoSeq` asked for a floor.
208
+ // Omit it and the server pins current and echoes it back in the header.
209
+ for await (const row of client.repo.export('acme', 'catalog', {
210
+ atRepoSeq: 42,
211
+ })) {
212
+ // ...
213
+ }
214
+ ```
215
+
216
+ There is no retry to replace: an export is a read, so a failed one is re-read,
217
+ not re-queued.
218
+
219
+ **Why.** Same retirement (PR #10020).
220
+
221
+ #### Failed checkpoint statuses now all answer `nextAction: 'none'`
222
+
223
+ **What it was.** `RepositoryCheckpointStatus` carried `nextAction: 'retry'` for
224
+ `deadline_exceeded` and `attempts_exhausted`, and `nextAction: 'generate'` for
225
+ `invalid_source`. Each named a procedure a reader could call.
226
+
227
+ **What it is now.** Those three failure codes join `repository_deleted` in a
228
+ single variant with `nextAction: 'none'`. The `'retry'` and `'generate'`
229
+ members are gone from the union, so a `switch` over `nextAction` that had cases
230
+ for them will no longer typecheck against them.
231
+
232
+ **Migration.**
233
+
234
+ Before:
235
+
236
+ ```ts
237
+ if (status.state === 'failed') {
238
+ switch (status.nextAction) {
239
+ case 'retry':
240
+ await client.repo.checkpoint.retry('acme', 'catalog', status.checkpointId)
241
+ break
242
+ case 'generate':
243
+ await client.repo.checkpoint.generate('acme', 'catalog')
244
+ break
245
+ case 'contact_support':
246
+ report(status.checkpointId, status.failureCode)
247
+ break
248
+ }
249
+ }
250
+ ```
251
+
252
+ After:
253
+
254
+ ```ts
255
+ if (status.state === 'failed') {
256
+ if (status.nextAction === 'contact_support') {
257
+ report(status.checkpointId, status.failureCode)
258
+ } else {
259
+ // Terminal. Take an export instead of trying to revive the checkpoint.
260
+ for await (const row of client.repo.export('acme', 'catalog')) {
261
+ // ...
262
+ }
263
+ }
264
+ }
265
+ ```
266
+
267
+ **Why.** Each retired value pointed at a route the server no longer serves;
268
+ leaving them in the vocabulary would send readers to a 404 (PR #10020).
269
+
270
+ ### Added
271
+
272
+ - **`client.repo.export(orgName, repoName, options?)`** — an
273
+ `AsyncGenerator<RepositoryExportRow>` over a verified export stream. Options:
274
+ `mode` (`'heads'`, the default, or `'ops'`), `sinceRepoSeq` (delta base;
275
+ `0` means a full export), `atRepoSeq` (pin the fence), `prefer`
276
+ (`'sync'` | `'async'`), `token`, `session`, `afterDurableId`,
277
+ `pollIntervalMs`, and `signal`.
278
+
279
+ - **`client.repo.exportPrepare(orgName, repoName, options?)`** — returns
280
+ `{ exportToken, atRepoSeq }` without waiting for the fold or reading the
281
+ body. Takes `mode`, `sinceRepoSeq`, `atRepoSeq`, and `signal`.
282
+
283
+ - **`RepositoryExportSession` / `createRepositoryExportSession()`** — the
284
+ rolling verification state of one logical export: the opening `header`, the
285
+ resume cursor `lastDurableId`, `rowCount`, `complete`, and — once the stream
286
+ is finalized — `contentSha256`. Pass the same session back into `export` to
287
+ resume a stream that broke:
288
+
289
+ ```ts
290
+ import {
291
+ createRepositoryExportSession,
292
+ RepositoryExportError,
293
+ } from '@warmhub/sdk-ts'
294
+
295
+ const session = createRepositoryExportSession()
296
+ const rows: unknown[] = []
297
+ for (let attempt = 0; attempt < 3 && !session.complete; attempt += 1) {
298
+ try {
299
+ // The session's own cursor and fence pin the resumed request; do not
300
+ // recompute them.
301
+ for await (const row of client.repo.export('acme', 'catalog', {
302
+ session,
303
+ })) {
304
+ rows.push(row)
305
+ }
306
+ } catch (error) {
307
+ if (
308
+ !(error instanceof RepositoryExportError) ||
309
+ error.reason !== 'truncated'
310
+ ) {
311
+ throw error
312
+ }
313
+ }
314
+ }
315
+ console.log(session.rowCount, session.contentSha256)
316
+ ```
317
+
318
+ - **`readRepositoryExportStream(source, session, expected, options?)`** — read
319
+ a response body you fetched with your own transport, verified against a
320
+ session as it goes. `client.repo.export` is the usual entry point.
321
+
322
+ - **`restoreRepositoryExportSession(source, options?)`** — the offline twin:
323
+ replay a saved export's bytes back through the same header, canonical-row,
324
+ ordering and trailer checks, and get the session back rather than the rows.
325
+ Pass `{ partial: true }` for a download that was interrupted, then resume
326
+ from the session it returns.
327
+
328
+ ```ts
329
+ import { createReadStream } from 'node:fs'
330
+ import { restoreRepositoryExportSession } from '@warmhub/sdk-ts'
331
+
332
+ const session = await restoreRepositoryExportSession(
333
+ createReadStream('catalog.ndjson'),
334
+ { partial: true },
335
+ )
336
+ for await (const row of client.repo.export('acme', 'catalog', { session })) {
337
+ // continues from session.lastDurableId
338
+ }
339
+ ```
340
+
341
+ - **`applyRepositoryExportDelta(base, delta)`** — fold a delta export onto a
342
+ base by `durableId`: active rows upsert, tombstones remove. Pure; neither
343
+ argument is mutated. Both arguments accept either an iterable of rows or a
344
+ `Map` returned by a previous fold, so folds chain.
345
+
346
+ ```ts
347
+ import { applyRepositoryExportDelta } from '@warmhub/sdk-ts'
348
+
349
+ const session = createRepositoryExportSession()
350
+ const base = []
351
+ for await (const row of client.repo.export('acme', 'catalog', { session })) {
352
+ base.push(row)
353
+ }
354
+ const fence = session.header?.atRepoSeq ?? 0
355
+
356
+ // Later: only what changed since that fence.
357
+ const delta = []
358
+ for await (const row of client.repo.export('acme', 'catalog', {
359
+ sinceRepoSeq: fence,
360
+ })) {
361
+ delta.push(row)
362
+ }
363
+
364
+ const current = applyRepositoryExportDelta(base, delta)
365
+ ```
366
+
367
+ `wref` is an opaque rendering and is never keyed on, so the fold survives a
368
+ rename.
369
+
370
+ - **`RepositoryExportError`** with a typed `reason`:
371
+ `'count_mismatch'`, `'digest_mismatch'`, `'fence_mismatch'`,
372
+ `'header_invalid'`, `'row_invalid'`, `'row_out_of_order'`,
373
+ `'since_below_epoch_floor'`, `'trailer_invalid'`, `'truncated'`. Every reason
374
+ except `'since_below_epoch_floor'` is a local verification verdict — the
375
+ bytes did not prove what the trailer claimed. `'since_below_epoch_floor'` is
376
+ the server refusing a delta base it no longer retains; recover by re-running
377
+ the export with `sinceRepoSeq: 0`.
378
+
379
+ - **`RepositoryExportRowMap` / `RepositoryExportRows`** — the collection types
380
+ `applyRepositoryExportDelta` accepts and returns.
381
+
382
+ ### Behavior worth knowing
383
+
384
+ - **A redeemed token's bytes come from storage, not the API, and the client
385
+ checks what the API said.** A complete redemption answers with a short-lived
386
+ presigned URL rather than the export bytes, and the SDK fetches that URL
387
+ *without* your WarmHub bearer: the signature in the URL is the whole
388
+ capability, and the storage host has no business seeing a credential. The
389
+ record count and content digest the session derives are then checked against
390
+ the ones the API named — a download that does not match raises
391
+ `count_mismatch` or `digest_mismatch` rather than handing you rows nothing
392
+ independent vouched for. A refused or expired URL re-redeems the token, which
393
+ is a deterministic lookup, and retries; a break after rows have already been
394
+ yielded surfaces as an error instead, because a silent restart would hand you
395
+ those rows twice.
396
+
397
+ - **Ordering is verified, in `heads` mode only.** A heads export is a set,
398
+ emitted in ascending `durableId` by unsigned UTF-8 bytes, and that order is
399
+ part of what the digest proves. A row that does not advance the cursor raises
400
+ `row_out_of_order`. An `ops` export is a log — it repeats identities by
401
+ design — and is not order-checked.
402
+
403
+ - **Resume trailers are segment-scoped.** A resumed request carries no header,
404
+ and its trailer covers only that segment's rows. The session keeps two
405
+ hashers: a per-segment one the trailer is checked against, and a whole-stream
406
+ one that accumulates across segments. `session.contentSha256` is the
407
+ whole-stream value, and is what you should record as the export's digest.
408
+
409
+ - **A trailer failure poisons the session.** `truncated` is the resumable
410
+ failure. `count_mismatch`, `digest_mismatch`, `fence_mismatch` and
411
+ `trailer_invalid` are not: the whole-stream digest has already absorbed that
412
+ segment's rows, so the session refuses to resume and you must start a new
413
+ one.
414
+
415
+ - **`afterDurableId` requires a session with a header.** A resumed request gets
416
+ no header back, so with nothing to check the repository and fence against the
417
+ call is refused before it is sent. Reuse the session the broken stream wrote,
418
+ or rebuild one with `restoreRepositoryExportSession`.
419
+
420
+ - **The async redemption poll has a floor of 50 ms**, and defaults to 1000 ms.
421
+ Each poll re-runs the full authorization gate, so `pollIntervalMs: 0` — or a
422
+ `NaN` from an unvalidated config value — is clamped rather than honored.
423
+
424
+ - **Flat server error bodies keep their message.** The export route answers
425
+ with a flat `{ code, message }` body rather than the client's usual nested
426
+ `{ error: { ... } }` envelope. Those responses are re-wrapped before the
427
+ standard mapping runs, so classification stays standard and the server's
428
+ message is not discarded.
429
+
430
+ ### Still supported
431
+
432
+ The checkpoint **read** plane is untouched: `client.repo.checkpoint.status`,
433
+ `.latest`, `.getAccess`, and `verifyRepositoryCheckpointArchive` from
434
+ `@warmhub/sdk-ts/checkpoint` all continue to work against archives already in
435
+ object storage. Nothing mints new ones.
436
+
437
+ **Deprecation horizon:** those read surfaces are supported until the announced
438
+ cleanup, 30 days after GA. Their removal is a separately-filed post-GA change.
439
+ Move download-and-verify workflows to `client.repo.export` before then.
package/changelog.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "sdk": "sdk",
4
+ "baseline": {
5
+ "version": "0.127.0",
6
+ "markdown": "# @warmhub/sdk-ts changelog\n\nRelease 0.127.0.\n\nChangelog generation is temporarily disabled while the release integration is repaired.\n\n\nEarlier changelog shipped with @warmhub/sdk-ts 0.126.0:\n\n# @warmhub/sdk-ts changelog\n\nRelease 0.126.0.\n\nChangelog generation is temporarily disabled while the release integration is repaired.\n\n\nEarlier changelog shipped with @warmhub/sdk-ts 0.125.0:\n\n# @warmhub/sdk-ts changelog\n\nRelease 0.125.0.\n\nChangelog generation is temporarily disabled while the release integration is repaired.\n\n\nEarlier changelog shipped with @warmhub/sdk-ts 0.124.0:\n\n# @warmhub/sdk-ts changelog\n\nRelease 0.124.0.\n\nChangelog generation is temporarily disabled while the release integration is repaired.\n\n\nEarlier changelog shipped with @warmhub/sdk-ts 0.123.0:\n\n# @warmhub/sdk-ts changelog\n\nRelease 0.123.0.\n\nChangelog generation is temporarily disabled while the release integration is repaired.\n\n\nEarlier changelog shipped with @warmhub/sdk-ts 0.122.3:\n\n# @warmhub/sdk-ts changelog\n\nRelease 0.122.3.\n\nChangelog generation is temporarily disabled while the release integration is repaired.\n\n\nEarlier changelog shipped with @warmhub/sdk-ts 0.120.0:\n\n# Changelog — `@warmhub/sdk-ts`\n\n## How to read this file\n\nEntries are grouped by release. `## Unreleased` collects everything merged but\nnot yet published; the release pipeline is the sole version authority and\nstamps the exact version at publish time, so no heading here ever invents one.\n\nWithin a release, four change sections appear, in this order. A section is\npresent only when it has entries; a release may add plain narrative sections\nafter them (\"Behavior worth knowing\", \"Still supported\") when the change needs\ncontext that is not itself a change entry.\n\n- **Breaking** — your code must change. Every item is written as a migration:\n what it was → what it is now → the exact edit, with before/after code → why,\n in one line, citing the ruling.\n- **Added** — new surface. Nothing to do unless you want it.\n- **Fixed** — behavior that was wrong and now is not.\n- **Removed** — surface that is gone with nothing to put in its place. Anything\n a caller can migrate to is filed under Breaking instead, so this section is\n often absent.\n\nWarmHub's source repository is private, so rulings are cited by pull-request\nnumber rather than linked. Release notes live at\n<https://docs.warmhub.ai/releases/overview/>.\n\nThe Python SDK, `warmhub`, keeps the same file in the same shape.\n\n---\n\n## Unreleased\n\n## 0.120.0\n\nRepository export v3. The exact version is stamped at publish time by the\nrelease pipeline; source manifests stay at `0.0.0-development`.\n\n### Breaking\n\n#### Checkpoint generation is retired — take an export instead\n\n**What it was.** Producing a fresh snapshot of a repository meant asking the\nserver to mint a checkpoint, polling until the job completed, requesting a\nshort-lived signed URL for the archive, downloading it outside the SDK, and\nverifying the bytes locally.\n\n**What it is now.** One call that streams verified rows, with the header,\nper-row canonical decode, ordering and trailer checks applied as the bytes\narrive. There is no job to start, no state to poll, and no separate verify\nstep — a row that reaches you has already been proven.\n\n**Migration.**\n\nBefore:\n\n```ts\nimport { createReadStream } from 'node:fs'\nimport { writeFile } from 'node:fs/promises'\nimport { Readable } from 'node:stream'\nimport { verifyRepositoryCheckpointArchive } from '@warmhub/sdk-ts/checkpoint'\n\nlet status = await client.repo.checkpoint.generate('acme', 'catalog')\nwhile (status.state !== 'complete') {\n await new Promise((resolve) => setTimeout(resolve, 1_000))\n status = await client.repo.checkpoint.status('acme', 'catalog', {\n checkpointId: status.checkpointId,\n })\n}\n\nconst access = await client.repo.checkpoint.getAccess('acme', 'catalog', {\n checkpoint: 'latest',\n artifact: 'archive',\n})\n// Signed URL: fetched WITHOUT the WarmHub bearer token.\nconst archive = await fetch(access.url)\nawait writeFile('catalog.zip', Readable.fromWeb(archive.body))\nawait verifyRepositoryCheckpointArchive(createReadStream('catalog.zip'))\n```\n\nAfter:\n\n```ts\nfor await (const row of client.repo.export('acme', 'catalog')) {\n console.log(row.kind, row.durableId, row.version)\n}\n```\n\nIf you were relying on `generate` returning before the download — starting the\nbuild in one process and fetching it in another, or on another machine — that\nshape survives as the async token flow:\n\n```ts\n// Process A: ask for it, get a token, do not read a byte.\nconst prepared = await client.repo.exportPrepare('acme', 'catalog')\nconsole.log(prepared.exportToken, prepared.atRepoSeq)\n\n// Process B: redeem it. `export` polls while the fold is still running.\nfor await (const row of client.repo.export('acme', 'catalog', {\n token: prepared.exportToken,\n})) {\n // ...\n}\n```\n\nThe token is redeemable only at that repository's URL, by a principal holding\ncheckpoint read there.\n\n**Why.** Export format v3 replaces checkpoint generation outright; the whole\ngeneration pipeline is deleted server-side (PR #10020).\n\n#### `client.repo.checkpoint.generate` and `.retry` are gone\n\n**What it was.** `generate(orgName, repoName, { atLeastRepoSeq })` minted a\ncheckpoint; `retry(orgName, repoName, checkpointId)` re-ran a failed one. Both\nreturned a `RepositoryCheckpointStatus`.\n\n**What it is now.** Neither method exists, and the tRPC procedures behind them\n(`repo.checkpoint.generate`, `repo.checkpoint.retry`) are no longer served. The\n`repo:checkpoint-generate` scope no longer buys anything.\n\n**Migration.** Replace both with `client.repo.export`.\n\nBefore:\n\n```ts\nconst fresh = await client.repo.checkpoint.generate('acme', 'catalog', {\n atLeastRepoSeq: 42,\n})\nconst recovered = await client.repo.checkpoint.retry(\n 'acme',\n 'catalog',\n failed.checkpointId,\n)\n```\n\nAfter:\n\n```ts\n// `atRepoSeq` pins the fence the way `atLeastRepoSeq` asked for a floor.\n// Omit it and the server pins current and echoes it back in the header.\nfor await (const row of client.repo.export('acme', 'catalog', {\n atRepoSeq: 42,\n})) {\n // ...\n}\n```\n\nThere is no retry to replace: an export is a read, so a failed one is re-read,\nnot re-queued.\n\n**Why.** Same retirement (PR #10020).\n\n#### Failed checkpoint statuses now all answer `nextAction: 'none'`\n\n**What it was.** `RepositoryCheckpointStatus` carried `nextAction: 'retry'` for\n`deadline_exceeded` and `attempts_exhausted`, and `nextAction: 'generate'` for\n`invalid_source`. Each named a procedure a reader could call.\n\n**What it is now.** Those three failure codes join `repository_deleted` in a\nsingle variant with `nextAction: 'none'`. The `'retry'` and `'generate'`\nmembers are gone from the union, so a `switch` over `nextAction` that had cases\nfor them will no longer typecheck against them.\n\n**Migration.**\n\nBefore:\n\n```ts\nif (status.state === 'failed') {\n switch (status.nextAction) {\n case 'retry':\n await client.repo.checkpoint.retry('acme', 'catalog', status.checkpointId)\n break\n case 'generate':\n await client.repo.checkpoint.generate('acme', 'catalog')\n break\n case 'contact_support':\n report(status.checkpointId, status.failureCode)\n break\n }\n}\n```\n\nAfter:\n\n```ts\nif (status.state === 'failed') {\n if (status.nextAction === 'contact_support') {\n report(status.checkpointId, status.failureCode)\n } else {\n // Terminal. Take an export instead of trying to revive the checkpoint.\n for await (const row of client.repo.export('acme', 'catalog')) {\n // ...\n }\n }\n}\n```\n\n**Why.** Each retired value pointed at a route the server no longer serves;\nleaving them in the vocabulary would send readers to a 404 (PR #10020).\n\n### Added\n\n- **`client.repo.export(orgName, repoName, options?)`** — an\n `AsyncGenerator<RepositoryExportRow>` over a verified export stream. Options:\n `mode` (`'heads'`, the default, or `'ops'`), `sinceRepoSeq` (delta base;\n `0` means a full export), `atRepoSeq` (pin the fence), `prefer`\n (`'sync'` | `'async'`), `token`, `session`, `afterDurableId`,\n `pollIntervalMs`, and `signal`.\n\n- **`client.repo.exportPrepare(orgName, repoName, options?)`** — returns\n `{ exportToken, atRepoSeq }` without waiting for the fold or reading the\n body. Takes `mode`, `sinceRepoSeq`, `atRepoSeq`, and `signal`.\n\n- **`RepositoryExportSession` / `createRepositoryExportSession()`** — the\n rolling verification state of one logical export: the opening `header`, the\n resume cursor `lastDurableId`, `rowCount`, `complete`, and — once the stream\n is finalized — `contentSha256`. Pass the same session back into `export` to\n resume a stream that broke:\n\n ```ts\n import {\n createRepositoryExportSession,\n RepositoryExportError,\n } from '@warmhub/sdk-ts'\n\n const session = createRepositoryExportSession()\n const rows: unknown[] = []\n for (let attempt = 0; attempt < 3 && !session.complete; attempt += 1) {\n try {\n // The session's own cursor and fence pin the resumed request; do not\n // recompute them.\n for await (const row of client.repo.export('acme', 'catalog', {\n session,\n })) {\n rows.push(row)\n }\n } catch (error) {\n if (\n !(error instanceof RepositoryExportError) ||\n error.reason !== 'truncated'\n ) {\n throw error\n }\n }\n }\n console.log(session.rowCount, session.contentSha256)\n ```\n\n- **`readRepositoryExportStream(source, session, expected, options?)`** — read\n a response body you fetched with your own transport, verified against a\n session as it goes. `client.repo.export` is the usual entry point.\n\n- **`restoreRepositoryExportSession(source, options?)`** — the offline twin:\n replay a saved export's bytes back through the same header, canonical-row,\n ordering and trailer checks, and get the session back rather than the rows.\n Pass `{ partial: true }` for a download that was interrupted, then resume\n from the session it returns.\n\n ```ts\n import { createReadStream } from 'node:fs'\n import { restoreRepositoryExportSession } from '@warmhub/sdk-ts'\n\n const session = await restoreRepositoryExportSession(\n createReadStream('catalog.ndjson'),\n { partial: true },\n )\n for await (const row of client.repo.export('acme', 'catalog', { session })) {\n // continues from session.lastDurableId\n }\n ```\n\n- **`applyRepositoryExportDelta(base, delta)`** — fold a delta export onto a\n base by `durableId`: active rows upsert, tombstones remove. Pure; neither\n argument is mutated. Both arguments accept either an iterable of rows or a\n `Map` returned by a previous fold, so folds chain.\n\n ```ts\n import { applyRepositoryExportDelta } from '@warmhub/sdk-ts'\n\n const session = createRepositoryExportSession()\n const base = []\n for await (const row of client.repo.export('acme', 'catalog', { session })) {\n base.push(row)\n }\n const fence = session.header?.atRepoSeq ?? 0\n\n // Later: only what changed since that fence.\n const delta = []\n for await (const row of client.repo.export('acme', 'catalog', {\n sinceRepoSeq: fence,\n })) {\n delta.push(row)\n }\n\n const current = applyRepositoryExportDelta(base, delta)\n ```\n\n `wref` is an opaque rendering and is never keyed on, so the fold survives a\n rename.\n\n- **`RepositoryExportError`** with a typed `reason`:\n `'count_mismatch'`, `'digest_mismatch'`, `'fence_mismatch'`,\n `'header_invalid'`, `'row_invalid'`, `'row_out_of_order'`,\n `'since_below_epoch_floor'`, `'trailer_invalid'`, `'truncated'`. Every reason\n except `'since_below_epoch_floor'` is a local verification verdict — the\n bytes did not prove what the trailer claimed. `'since_below_epoch_floor'` is\n the server refusing a delta base it no longer retains; recover by re-running\n the export with `sinceRepoSeq: 0`.\n\n- **`RepositoryExportRowMap` / `RepositoryExportRows`** — the collection types\n `applyRepositoryExportDelta` accepts and returns.\n\n### Behavior worth knowing\n\n- **A redeemed token's bytes come from storage, not the API, and the client\n checks what the API said.** A complete redemption answers with a short-lived\n presigned URL rather than the export bytes, and the SDK fetches that URL\n *without* your WarmHub bearer: the signature in the URL is the whole\n capability, and the storage host has no business seeing a credential. The\n record count and content digest the session derives are then checked against\n the ones the API named — a download that does not match raises\n `count_mismatch` or `digest_mismatch` rather than handing you rows nothing\n independent vouched for. A refused or expired URL re-redeems the token, which\n is a deterministic lookup, and retries; a break after rows have already been\n yielded surfaces as an error instead, because a silent restart would hand you\n those rows twice.\n\n- **Ordering is verified, in `heads` mode only.** A heads export is a set,\n emitted in ascending `durableId` by unsigned UTF-8 bytes, and that order is\n part of what the digest proves. A row that does not advance the cursor raises\n `row_out_of_order`. An `ops` export is a log — it repeats identities by\n design — and is not order-checked.\n\n- **Resume trailers are segment-scoped.** A resumed request carries no header,\n and its trailer covers only that segment's rows. The session keeps two\n hashers: a per-segment one the trailer is checked against, and a whole-stream\n one that accumulates across segments. `session.contentSha256` is the\n whole-stream value, and is what you should record as the export's digest.\n\n- **A trailer failure poisons the session.** `truncated` is the resumable\n failure. `count_mismatch`, `digest_mismatch`, `fence_mismatch` and\n `trailer_invalid` are not: the whole-stream digest has already absorbed that\n segment's rows, so the session refuses to resume and you must start a new\n one.\n\n- **`afterDurableId` requires a session with a header.** A resumed request gets\n no header back, so with nothing to check the repository and fence against the\n call is refused before it is sent. Reuse the session the broken stream wrote,\n or rebuild one with `restoreRepositoryExportSession`.\n\n- **The async redemption poll has a floor of 50 ms**, and defaults to 1000 ms.\n Each poll re-runs the full authorization gate, so `pollIntervalMs: 0` — or a\n `NaN` from an unvalidated config value — is clamped rather than honored.\n\n- **Flat server error bodies keep their message.** The export route answers\n with a flat `{ code, message }` body rather than the client's usual nested\n `{ error: { ... } }` envelope. Those responses are re-wrapped before the\n standard mapping runs, so classification stays standard and the server's\n message is not discarded.\n\n### Still supported\n\nThe checkpoint **read** plane is untouched: `client.repo.checkpoint.status`,\n`.latest`, `.getAccess`, and `verifyRepositoryCheckpointArchive` from\n`@warmhub/sdk-ts/checkpoint` all continue to work against archives already in\nobject storage. Nothing mints new ones.\n\n**Deprecation horizon:** those read surfaces are supported until the announced\ncleanup, 30 days after GA. Their removal is a separately-filed post-GA change.\nMove download-and-verify workflows to `client.repo.export` before then.\n"
7
+ },
8
+ "releases": [
9
+ {
10
+ "version": "0.128.0",
11
+ "sourceSha": "f990f080d6f8b7310700b1d430ee1aa11dcaaf23",
12
+ "notes": [
13
+ {
14
+ "id": "published-changelog-artifacts",
15
+ "body": "### Documentation\n\nVersioned release notes are available as [HTML, Markdown, and JSON](https://docs.warmhub.ai/sdk/typescript/changelog/)."
16
+ }
17
+ ]
18
+ }
19
+ ]
20
+ }
@@ -1,2 +1,2 @@
1
- import {K as K$1,J as J$1,L as L$1,P as P$1,H as H$1,M,N as N$1,O,I}from'./chunk-4OSQJILK.js';import {mkdtemp,chmod,rm,open}from'fs/promises';import {tmpdir}from'os';import {join}from'path';import {createHash}from'crypto';import {Writable}from'stream';import {pipeline}from'stream/promises';import {callbackify}from'util';import {createInflateRaw,crc32}from'zlib';import {once}from'events';import {createWriteStream,createReadStream}from'fs';var d=class extends Error{constructor(e){super(rt(e));this.reason=e;}reason;name="RepositoryCheckpointVerificationError"};function rt(r){switch(r){case "source_failed":return "Repository checkpoint source could not be read";case "archive_invalid":return "Repository checkpoint archive is invalid";case "archive_integrity_mismatch":return "Repository checkpoint archive integrity does not match";case "manifest_invalid":return "Repository checkpoint manifest is invalid";case "chunk_invalid":return "Repository checkpoint chunk is invalid";case "duplicate_durable_id":return "Repository checkpoint contains a duplicate durable identity";case "scratch_cleanup_failed":return "Repository checkpoint verifier scratch cleanup failed"}}var H=67324752,ht=33639248,dt=134695760,ut=101075792,ft=117853008,lt=101010256,Y=2056,Z=8,R=45,q=813,z=65535,m=4294967295,W=33,yt=33188,S=64*1024,pt=new RegExp(`^records/[0-9a-f]{64}/part-\\d{${I}}\\.jsonl$`),mt=new TextDecoder("utf-8",{fatal:true}),E=class extends Error{};async function G(r,t){let e=new C(r);try{let i=[],n=new Set,s=!1;for(;await e.peekU32()===H;){if(s)throw new Error("manifest must be the final entry");let c=BigInt(e.position),h=await e.readExact(30);It(h);let f=h.readUInt16LE(26),p=h.readUInt16LE(28);if(f===0||p!==20)throw new Error("local entry metadata is invalid");let a=K(await e.readExact(f));if(St(a),n.has(a))throw new Error("archive entry is duplicated");n.add(a),s=a===J$1,bt(await e.readExact(p));let y=t(a),l=await wt(e,y),et=await e.readExact(24);kt(et,l),await y.finish(),i.push({name:a,localOffset:c,...l});}if(!s||i.length===0)throw new Error("archive manifest entry is missing");return await vt(e,i),{...await e.finish(),entryNames:i.map(c=>c.name)}}finally{await e.close();}}async function wt(r,t){let e=createInflateRaw({chunkSize:S}),i=0n,n=0n,s=0,o=callbackify(async a=>{try{await t.write(a);}catch(y){throw y instanceof Error?y:new Error("checkpoint sink failed")}s=crc32(a,s),n+=BigInt(a.byteLength);}),c=new Writable({highWaterMark:S,write(a,y,l){o(a,l);}}),h=pipeline(e,c),f=new A,p=Et(h,f);try{for(;!e.readableEnded;){let a=await r.readSome(S);if(!a)throw new Error("archive entry is truncated");let y=e.bytesWritten;await gt(e,a,f);let l=e.bytesWritten-y;if(!Number.isSafeInteger(l)||l<0||l>a.byteLength)throw new Error("archive inflater consumed invalid input");if(i+=BigInt(l),l<a.byteLength&&r.unread(a.subarray(l)),e.readableEnded)break;if(l===0)throw new Error("archive inflater made no progress")}return await h,{crc:s>>>0,compressedSize:i,uncompressedSize:n}}finally{e.destroyed||e.destroy(),await p;}}var A=class{#i;#t;fail(t){let e=t instanceof Error?t:new Error("archive inflater failed");this.#i=e,this.#t?.(e),this.#t=void 0;}async race(t){if(this.#i)throw this.#i;let e,i=new Promise((n,s)=>{e=o=>s(o),this.#t=e;});try{return await Promise.race([t,i])}finally{e&&this.#t===e&&(this.#t=void 0);}}};async function Et(r,t){try{await r;}catch(e){t.fail(e);}}async function gt(r,t,e){let i=new Promise((n,s)=>{r.write(t,o=>{o?s(o):n();});});await e.race(i);}async function vt(r,t){let e=BigInt(r.position);for(let h of t){let f=await r.readExact(46);Ut(f,h);let p=f.readUInt16LE(28),a=f.readUInt16LE(30);if(a!==28||f.readUInt16LE(32)!==0)throw new Error("central entry metadata is invalid");if(K(await r.readExact(p))!==h.name)throw new Error("central entry order differs");Lt(await r.readExact(a),h);}let i=BigInt(r.position)-e,n=BigInt(r.position),s=await r.readExact(56);if(w(s,0,ut),s.readBigUInt64LE(4)!==44n||s.readUInt16LE(12)!==q||s.readUInt16LE(14)!==R||s.readUInt32LE(16)!==0||s.readUInt32LE(20)!==0||s.readBigUInt64LE(24)!==BigInt(t.length)||s.readBigUInt64LE(32)!==BigInt(t.length)||s.readBigUInt64LE(40)!==i||s.readBigUInt64LE(48)!==e)throw new Error("ZIP64 end record is invalid");let o=await r.readExact(20);if(w(o,0,ft),o.readUInt32LE(4)!==0||o.readBigUInt64LE(8)!==n||o.readUInt32LE(16)!==1)throw new Error("ZIP64 locator is invalid");let c=await r.readExact(22);if(w(c,0,lt),c.readUInt16LE(4)!==0||c.readUInt16LE(6)!==0||c.readUInt16LE(8)!==z||c.readUInt16LE(10)!==z||c.readUInt32LE(12)!==m||c.readUInt32LE(16)!==m||c.readUInt16LE(20)!==0)throw new Error("classic ZIP end record is invalid")}function It(r){if(w(r,0,H),r.readUInt16LE(4)!==R||r.readUInt16LE(6)!==Y||r.readUInt16LE(8)!==Z||r.readUInt16LE(10)!==0||r.readUInt16LE(12)!==W||r.readUInt32LE(14)!==0||r.readUInt32LE(18)!==m||r.readUInt32LE(22)!==m)throw new Error("local entry header is invalid")}function bt(r){if(r.readUInt16LE(0)!==1||r.readUInt16LE(2)!==16||r.readBigUInt64LE(4)!==0n||r.readBigUInt64LE(12)!==0n)throw new Error("local ZIP64 extra is invalid")}function kt(r,t){if(w(r,0,dt),r.readUInt32LE(4)!==t.crc||r.readBigUInt64LE(8)!==t.compressedSize||r.readBigUInt64LE(16)!==t.uncompressedSize)throw new Error("archive data descriptor is invalid")}function Ut(r,t){if(w(r,0,ht),r.readUInt16LE(4)!==q||r.readUInt16LE(6)!==R||r.readUInt16LE(8)!==Y||r.readUInt16LE(10)!==Z||r.readUInt16LE(12)!==0||r.readUInt16LE(14)!==W||r.readUInt32LE(16)!==t.crc||r.readUInt32LE(20)!==m||r.readUInt32LE(24)!==m||r.readUInt16LE(34)!==0||r.readUInt16LE(36)!==0||(r.readUInt32LE(38)>>>16&65535)!==yt||r.readUInt32LE(42)!==m)throw new Error("central entry header is invalid")}function Lt(r,t){if(r.readUInt16LE(0)!==1||r.readUInt16LE(2)!==24||r.readBigUInt64LE(4)!==t.uncompressedSize||r.readBigUInt64LE(12)!==t.compressedSize||r.readBigUInt64LE(20)!==t.localOffset)throw new Error("central ZIP64 extra is invalid")}function St(r){if(r!==J$1&&!pt.test(r))throw new Error("archive entry name is invalid")}function K(r){let t=mt.decode(r);if(Buffer.byteLength(t,"utf8")!==r.byteLength)throw new Error("archive entry name is not canonical UTF-8");return t}function w(r,t,e){if(r.readUInt32LE(t)!==e)throw new Error("archive signature is invalid")}var C=class{#i;#t=createHash("sha256");#e=[];#r=0;#n=false;#o=0;position=0;constructor(t){this.#i=At(t)[Symbol.asyncIterator]();}async peekU32(){let t=await this.readExact(4);return this.unread(t),t.readUInt32LE(0)}async readExact(t){let e=Buffer.allocUnsafe(t),i=0;for(;i<t;){let n=await this.readSome(t-i);if(!n)throw new Error("archive is truncated");e.set(n,i),i+=n.byteLength;}return e}async readSome(t){for(;this.#e.length===0;)if(!await this.#s())return;let e=this.#e[0];if(!e)return;let i=e.byteLength-this.#r,n=Math.min(t,i),s=e.subarray(this.#r,this.#r+n);return this.#r+=n,this.position+=n,this.#r===e.byteLength&&(this.#e.shift(),this.#r=0),s}unread(t){if(t.byteLength!==0){if(this.#r>0){let e=this.#e[0];if(!e)throw new Error("archive reader queue is inconsistent");let i=e.subarray(this.#r);this.#e[0]=i,this.#r=0;}this.#e.unshift(t),this.position-=t.byteLength;}}async finish(){if(this.#e.length>0||await this.#s())throw new Error("archive contains trailing bytes");return {byteLength:this.#o,sha256:this.#t.digest("hex")}}async close(){try{await this.#i.return?.();}catch{}}async#s(){if(this.#n)return false;for(;;){let t;try{t=await this.#i.next();}catch{throw new E}if(t.done)return this.#n=true,false;if(!(t.value instanceof Uint8Array))throw new E;if(t.value.byteLength!==0){if(this.#t.update(t.value),this.#o+=t.value.byteLength,!Number.isSafeInteger(this.#o))throw new Error("archive byte length is unsafe");return this.#e.push(t.value),true}}}};function At(r){return Symbol.asyncIterator in r?r:{async*[Symbol.asyncIterator](){let t=r.getReader(),e=false;try{for(;;){let{value:i,done:n}=await t.read();if(n){e=!0;return}yield i;}}finally{if(!e)try{await t.cancel();}catch{}t.releaseLock();}}}}var xt=4*1024*1024,j=8,J=64*1024,Tt=16*1024,u=4,$=4,Ot=1,Vt=u+Ot,X=Vt+$,g=class extends Error{},k=class{#i;#t;#e=[];#r=0;#n;constructor(t,e={}){this.#i=t;let i=e.sortBudgetBytes??xt;if(this.#n=e.mergeFanIn??j,!Number.isSafeInteger(i)||i<X||!Number.isSafeInteger(this.#n)||this.#n<2||this.#n>j)throw new Error("checkpoint identity sort limits are invalid");this.#t=new _(i);}add(t){let e=Buffer.byteLength(t,"utf8");if(!this.#t.canFit(e))throw new Error("checkpoint durable identity exceeds the sort budget");if(this.#t.canAdd(e)){this.#t.add(t,e);return}return this.#o(t,e)}async finish(){if(this.#e.length===0){this.#t.sortAndAssertUnique(),this.#t.clear();return}for(this.#t.isEmpty||await this.#o();this.#e.length>this.#n;){let e=[];for(let i=0;i<this.#e.length;i+=this.#n){let n=this.#e.slice(i,i+this.#n),s=this.#s();await Ft(n,s),await Promise.all(n.map(o=>rm(o,{force:true}))),e.push(s);}this.#e=e;}let t;await tt(this.#e,e=>{if(t&&P(t,e)===0)throw new g;t=e;});}async#o(t,e){let i=this.#s();this.#t.sortAndAssertUnique(),await Mt(i,this.#t),this.#e.push(i),this.#t.clear(),t!==void 0&&e!==void 0&&this.#t.add(t,e);}#s(){let t=join(this.#i,`identity-run-${String(this.#r).padStart(8,"0")}.bin`);return this.#r+=1,t}},_=class{byteBudget;#i;#t;#e=0;#r=0;constructor(t){this.byteBudget=t;let e=Math.floor(t/X);this.#t=new Uint32Array(e),this.#i=Buffer.allocUnsafe(t-e*$);}get isEmpty(){return this.#r===0}canAdd(t){return this.canFit(t)&&this.#r<this.#t.length&&this.#e+u+t<=this.#i.length}canFit(t){return t<=4294967295&&this.#t.length>0&&u+t<=this.#i.length}add(t,e){if(!this.canAdd(e))throw new Error("checkpoint durable identity exceeds the sort budget");let i=this.#e;this.#t[this.#r]=i,this.#i.writeUInt32BE(e,i),this.#i.write(t,i+u,e,"utf8"),this.#e+=u+e,this.#r+=1;}sortAndAssertUnique(){let t=this.#t.subarray(0,this.#r);t.sort((e,i)=>this.#n(e,i));for(let e=1;e<t.length;e+=1){let i=t[e-1],n=t[e];if(i===void 0||n===void 0)throw new Error("checkpoint identity offset is missing");if(this.#n(i,n)===0)throw new g}}clear(){this.#e=0,this.#r=0;}forEachSorted(t){return Dt(this.#t.subarray(0,this.#r),e=>t(this.#s(e)))}#n(t,e){return P(this.#o(t),this.#o(e))}#o(t){let e=this.#i.readUInt32BE(t);return this.#i.subarray(t+u,t+u+e)}#s(t){let e=this.#i.readUInt32BE(t);return this.#i.subarray(t,t+u+e)}};async function Mt(r,t){await Q(r,async e=>{let i=new U(e);await t.forEachSorted(n=>i.write(n)),await i.finish();});}async function Ft(r,t){await Q(t,async e=>{let i=new U(e);await tt(r,n=>i.writeValue(n)),await i.finish();});}async function Q(r,t){await(await open(r,"wx",384)).close();let i=createWriteStream(r,{flags:"a",mode:384,highWaterMark:J});try{await t(i),i.end(),await once(i,"close");}catch(n){throw i.destroy(),n}}var U=class{#i;#t=Buffer.allocUnsafe(J);#e=0;constructor(t){this.#i=t;}write(t){if(t.byteLength>this.#t.length)return this.#r(t);if(this.#e+t.byteLength>this.#t.length)return this.#n(t);this.#t.set(t,this.#e),this.#e+=t.byteLength;}writeValue(t){let e=u+t.byteLength;if(e>this.#t.length)return this.#o(t);if(this.#e+e>this.#t.length)return this.#s(t);this.#c(t);}async finish(){await this.#a();}async#r(t){await this.#a(),await b(this.#i,t);}async#n(t){await this.#a(),this.#t.set(t,this.#e),this.#e+=t.byteLength;}async#o(t){await this.#a();let e=Buffer.allocUnsafe(u);e.writeUInt32BE(t.byteLength,0),await b(this.#i,e),await b(this.#i,t);}async#s(t){await this.#a(),this.#c(t);}async#a(){if(this.#e===0)return;let t=this.#t.subarray(0,this.#e);await b(this.#i,t),this.#e=0;}#c(t){this.#t.writeUInt32BE(t.byteLength,this.#e),this.#t.set(t,this.#e+u),this.#e+=u+t.byteLength;}};async function tt(r,t){let e=r.map(i=>new B(i));try{for(await Promise.all(e.map(i=>i.advance()));;){let i;for(let s of e)s.current&&(!i||P(s.current,i.current)<0)&&(i=s);if(!i?.current)return;let n=t(i.current);n&&await n,await i.advance();}}finally{for(let i of e)i.close();}}var B=class{#i;#t;#e=Buffer.alloc(0);#r=0;current;constructor(t){this.#i=createReadStream(t,{highWaterMark:Tt}),this.#t=this.#i[Symbol.asyncIterator]();}async advance(){let t=await this.#n(u);if(!t){this.current=void 0;return}let e=t.readUInt32BE(0);if(e===0)throw new Error("checkpoint identity run is invalid");let i=await this.#n(e);if(!i)throw new Error("checkpoint identity run is truncated");this.current=i;}close(){this.#i.destroy();}async#n(t){if(t===0)return Buffer.alloc(0);if(this.#e.length-this.#r>=t){let n=this.#e.subarray(this.#r,this.#r+t);return this.#r+=t,n}let e=Buffer.allocUnsafe(t),i=0;for(;i<t;){if(this.#r<this.#e.length){let s=Math.min(t-i,this.#e.length-this.#r);this.#e.copy(e,i,this.#r,this.#r+s),this.#r+=s,i+=s;continue}let n=await this.#t.next();if(n.done){if(i===0)return;throw new Error("checkpoint identity run is truncated")}this.#e=n.value,this.#r=0;}return e}};async function Dt(r,t){for(let e of r){let i=t(e);i&&await i;}}async function b(r,t){await new Promise((e,i)=>{r.write(t,n=>{n?i(n):e();});});}function P(r,t){let e=Math.min(r.length,t.length);for(let i=0;i<e;i+=1){let n=r[i],s=t[i];if(n===void 0||s===void 0)throw new Error("checkpoint identity byte is missing");let o=n-s;if(o!==0)return o}return r.length-t.length}var L=class{#i;#t=new Map;#e=new Map;#r=[];#n;constructor(t,e={}){this.#i=new k(t,{sortBudgetBytes:e.identitySortBudgetBytes,mergeFanIn:e.identityMergeFanIn});}openEntry(t){if(t===J$1){let e=[];return {write:async i=>{e.push(i.slice());},finish:async()=>{this.#n=Buffer.concat(e);}}}return new N(t,this.#t,this.#e,this.#i,e=>this.#r.push(e))}async finish(){if(!this.#n)throw new Error("checkpoint manifest is missing");let t=await L$1(this.#n),e=t.streams.flatMap(i=>i.chunks.map(n=>({...n,shapeName:i.shapeName})));if(e.length!==this.#r.length)throw new Error("checkpoint chunk inventory does not match");for(let[i,n]of e.entries()){let s=this.#r[i];if(s.path!==n.path||s.shapeName!==n.shapeName||s.recordCount!==n.recordCount||s.byteLength!==n.byteLength||s.sha256!==n.sha256)throw new Error("checkpoint chunk integrity does not match")}for(let i of t.streams){let n=this.#t.get(i.shapeName);if(!n||P$1(n)!==i.recordCount)throw new Error("checkpoint stream does not match its manifest")}if(this.#t.size!==t.streams.length)throw new Error("checkpoint contains an undeclared stream");return await this.#i.finish(),{manifest:t,chunkCount:e.length}}},N=class{#i;#t;#e;#r;#n;#o=createHash("sha256");#s=[];#a=0;#c;#h=0;#d=0;constructor(t,e,i,n,s){this.#i=t,this.#t=e,this.#e=i,this.#r=n,this.#n=s;}async write(t){if(this.#o.update(t),this.#d+=t.byteLength,!Number.isSafeInteger(this.#d))throw new Error("checkpoint chunk byte length is unsafe");let e=0;for(;e<t.byteLength;){let i=t.indexOf(10,e);if(i<0)break;let n=this.#u(this.#l(t.subarray(e,i+1)));n&&await n,e=i+1;}e<t.byteLength&&this.#f(t.subarray(e));}async finish(){if(this.#s.length!==0||this.#h===0||!this.#c)throw new Error("checkpoint chunk framing is invalid");if(this.#d>H$1&&this.#h!==1)throw new Error("oversized checkpoint chunk has multiple rows");this.#n({path:this.#i,shapeName:this.#c,recordCount:this.#h,byteLength:this.#d,sha256:this.#o.digest("hex")});let t=this.#e.get(this.#c);if(!t)throw new Error("checkpoint stream chunk state is missing");t.previous={byteLength:this.#d};}#u(t){let e=M(t),i=e.kind==="shape"?e.wref:e.shapeName;if(this.#c!==void 0&&this.#c!==i)throw new Error("checkpoint chunk mixes Shape streams");this.#c=i,this.#h===0&&this.#y(i,t.byteLength);let n=this.#t.get(i)??N$1(i);this.#t.set(i,n),O(n,e);let s=this.#r.add(e.durableId);if(this.#h+=1,!Number.isSafeInteger(this.#h))throw new Error("checkpoint chunk row count is unsafe");return s}#f(t){if(t.byteLength!==0&&(this.#s.push(t.slice()),this.#a+=t.byteLength,!Number.isSafeInteger(this.#a)))throw new Error("checkpoint row byte length is unsafe")}#l(t){if(this.#s.length===0)return t;let e=Buffer.concat([...this.#s,t],this.#a+t.byteLength);return this.#s=[],this.#a=0,e}#y(t,e){let i=Number(this.#i.match(/part-(\d{10})\.jsonl$/)?.[1]??-1),n=this.#e.get(t)??{nextPart:0};if(i!==n.nextPart)throw new Error("checkpoint stream chunk parts are not contiguous");if(n.previous&&n.previous.byteLength<=H$1&&n.previous.byteLength+e<=H$1)throw new Error("checkpoint stream chunk closed before the byte limit");n.nextPart+=1,this.#e.set(t,n);}};async function Gt(r,t){return Kt(r,t)}async function Kt(r,t,e={}){let i=await mkdtemp(join(e.scratchParent??tmpdir(),"warmhub-checkpoint-verify-"));await chmod(i,448);let n,s;try{let o=new L(i,e),c=await G(r,p=>o.openEntry(p)),{manifest:h,chunkCount:f}=await o.finish();jt(t,c,h),n={valid:!0,checkpointId:h.checkpointId,repoSeq:h.repoSeq,recordCount:h.recordCount,chunkCount:f,totalBytes:c.byteLength,rootDigest:h.contentSha256,archiveDigest:c.sha256};}catch(o){s=Jt(o);}try{await rm(i,{recursive:!0,force:!0});}catch{s??=new d("scratch_cleanup_failed");}if(s)throw s;if(!n)throw new d("archive_invalid");return n}function jt(r,t,e){if(r&&(K$1.parse(r.sha256),r.checkpointId!==e.checkpointId||r.repoSeq!==e.repoSeq||r.byteLength!==t.byteLength||r.sha256!==t.sha256))throw new d("archive_integrity_mismatch")}function Jt(r){if(r instanceof d)return r;if(r instanceof E)return new d("source_failed");if(r instanceof g)return new d("duplicate_durable_id");let t=r instanceof Error?r.message:"";return t.includes("manifest")?new d("manifest_invalid"):t.includes("chunk")||t.includes("row")||t.includes("Shape stream")?new d("chunk_invalid"):new d("archive_invalid")}export{d as RepositoryCheckpointVerificationError,Gt as verifyRepositoryCheckpointArchive};//# sourceMappingURL=checkpoint.js.map
1
+ import {L as L$1,K as K$1,M,Q as Q$1,I,N as N$1,O,P as P$1,J as J$1}from'./chunk-NOX7T26R.js';import {mkdtemp,chmod,rm,open}from'fs/promises';import {tmpdir}from'os';import {join}from'path';import {createHash}from'crypto';import {Writable}from'stream';import {pipeline}from'stream/promises';import {callbackify}from'util';import {createInflateRaw,crc32}from'zlib';import {once}from'events';import {createWriteStream,createReadStream}from'fs';var d=class extends Error{constructor(e){super(rt(e));this.reason=e;}reason;name="RepositoryCheckpointVerificationError"};function rt(r){switch(r){case "source_failed":return "Repository checkpoint source could not be read";case "archive_invalid":return "Repository checkpoint archive is invalid";case "archive_integrity_mismatch":return "Repository checkpoint archive integrity does not match";case "manifest_invalid":return "Repository checkpoint manifest is invalid";case "chunk_invalid":return "Repository checkpoint chunk is invalid";case "duplicate_durable_id":return "Repository checkpoint contains a duplicate durable identity";case "scratch_cleanup_failed":return "Repository checkpoint verifier scratch cleanup failed"}}var H=67324752,ht=33639248,dt=134695760,ut=101075792,ft=117853008,lt=101010256,Y=2056,Z=8,R=45,q=813,z=65535,m=4294967295,W=33,yt=33188,S=64*1024,pt=new RegExp(`^records/[0-9a-f]{64}/part-\\d{${J$1}}\\.jsonl$`),mt=new TextDecoder("utf-8",{fatal:true}),E=class extends Error{};async function G(r,t){let e=new C(r);try{let i=[],n=new Set,s=!1;for(;await e.peekU32()===H;){if(s)throw new Error("manifest must be the final entry");let c=BigInt(e.position),h=await e.readExact(30);It(h);let f=h.readUInt16LE(26),p=h.readUInt16LE(28);if(f===0||p!==20)throw new Error("local entry metadata is invalid");let a=K(await e.readExact(f));if(St(a),n.has(a))throw new Error("archive entry is duplicated");n.add(a),s=a===K$1,bt(await e.readExact(p));let y=t(a),l=await wt(e,y),et=await e.readExact(24);kt(et,l),await y.finish(),i.push({name:a,localOffset:c,...l});}if(!s||i.length===0)throw new Error("archive manifest entry is missing");return await vt(e,i),{...await e.finish(),entryNames:i.map(c=>c.name)}}finally{await e.close();}}async function wt(r,t){let e=createInflateRaw({chunkSize:S}),i=0n,n=0n,s=0,o=callbackify(async a=>{try{await t.write(a);}catch(y){throw y instanceof Error?y:new Error("checkpoint sink failed")}s=crc32(a,s),n+=BigInt(a.byteLength);}),c=new Writable({highWaterMark:S,write(a,y,l){o(a,l);}}),h=pipeline(e,c),f=new A,p=Et(h,f);try{for(;!e.readableEnded;){let a=await r.readSome(S);if(!a)throw new Error("archive entry is truncated");let y=e.bytesWritten;await gt(e,a,f);let l=e.bytesWritten-y;if(!Number.isSafeInteger(l)||l<0||l>a.byteLength)throw new Error("archive inflater consumed invalid input");if(i+=BigInt(l),l<a.byteLength&&r.unread(a.subarray(l)),e.readableEnded)break;if(l===0)throw new Error("archive inflater made no progress")}return await h,{crc:s>>>0,compressedSize:i,uncompressedSize:n}}finally{e.destroyed||e.destroy(),await p;}}var A=class{#i;#t;fail(t){let e=t instanceof Error?t:new Error("archive inflater failed");this.#i=e,this.#t?.(e),this.#t=void 0;}async race(t){if(this.#i)throw this.#i;let e,i=new Promise((n,s)=>{e=o=>s(o),this.#t=e;});try{return await Promise.race([t,i])}finally{e&&this.#t===e&&(this.#t=void 0);}}};async function Et(r,t){try{await r;}catch(e){t.fail(e);}}async function gt(r,t,e){let i=new Promise((n,s)=>{r.write(t,o=>{o?s(o):n();});});await e.race(i);}async function vt(r,t){let e=BigInt(r.position);for(let h of t){let f=await r.readExact(46);Ut(f,h);let p=f.readUInt16LE(28),a=f.readUInt16LE(30);if(a!==28||f.readUInt16LE(32)!==0)throw new Error("central entry metadata is invalid");if(K(await r.readExact(p))!==h.name)throw new Error("central entry order differs");Lt(await r.readExact(a),h);}let i=BigInt(r.position)-e,n=BigInt(r.position),s=await r.readExact(56);if(w(s,0,ut),s.readBigUInt64LE(4)!==44n||s.readUInt16LE(12)!==q||s.readUInt16LE(14)!==R||s.readUInt32LE(16)!==0||s.readUInt32LE(20)!==0||s.readBigUInt64LE(24)!==BigInt(t.length)||s.readBigUInt64LE(32)!==BigInt(t.length)||s.readBigUInt64LE(40)!==i||s.readBigUInt64LE(48)!==e)throw new Error("ZIP64 end record is invalid");let o=await r.readExact(20);if(w(o,0,ft),o.readUInt32LE(4)!==0||o.readBigUInt64LE(8)!==n||o.readUInt32LE(16)!==1)throw new Error("ZIP64 locator is invalid");let c=await r.readExact(22);if(w(c,0,lt),c.readUInt16LE(4)!==0||c.readUInt16LE(6)!==0||c.readUInt16LE(8)!==z||c.readUInt16LE(10)!==z||c.readUInt32LE(12)!==m||c.readUInt32LE(16)!==m||c.readUInt16LE(20)!==0)throw new Error("classic ZIP end record is invalid")}function It(r){if(w(r,0,H),r.readUInt16LE(4)!==R||r.readUInt16LE(6)!==Y||r.readUInt16LE(8)!==Z||r.readUInt16LE(10)!==0||r.readUInt16LE(12)!==W||r.readUInt32LE(14)!==0||r.readUInt32LE(18)!==m||r.readUInt32LE(22)!==m)throw new Error("local entry header is invalid")}function bt(r){if(r.readUInt16LE(0)!==1||r.readUInt16LE(2)!==16||r.readBigUInt64LE(4)!==0n||r.readBigUInt64LE(12)!==0n)throw new Error("local ZIP64 extra is invalid")}function kt(r,t){if(w(r,0,dt),r.readUInt32LE(4)!==t.crc||r.readBigUInt64LE(8)!==t.compressedSize||r.readBigUInt64LE(16)!==t.uncompressedSize)throw new Error("archive data descriptor is invalid")}function Ut(r,t){if(w(r,0,ht),r.readUInt16LE(4)!==q||r.readUInt16LE(6)!==R||r.readUInt16LE(8)!==Y||r.readUInt16LE(10)!==Z||r.readUInt16LE(12)!==0||r.readUInt16LE(14)!==W||r.readUInt32LE(16)!==t.crc||r.readUInt32LE(20)!==m||r.readUInt32LE(24)!==m||r.readUInt16LE(34)!==0||r.readUInt16LE(36)!==0||(r.readUInt32LE(38)>>>16&65535)!==yt||r.readUInt32LE(42)!==m)throw new Error("central entry header is invalid")}function Lt(r,t){if(r.readUInt16LE(0)!==1||r.readUInt16LE(2)!==24||r.readBigUInt64LE(4)!==t.uncompressedSize||r.readBigUInt64LE(12)!==t.compressedSize||r.readBigUInt64LE(20)!==t.localOffset)throw new Error("central ZIP64 extra is invalid")}function St(r){if(r!==K$1&&!pt.test(r))throw new Error("archive entry name is invalid")}function K(r){let t=mt.decode(r);if(Buffer.byteLength(t,"utf8")!==r.byteLength)throw new Error("archive entry name is not canonical UTF-8");return t}function w(r,t,e){if(r.readUInt32LE(t)!==e)throw new Error("archive signature is invalid")}var C=class{#i;#t=createHash("sha256");#e=[];#r=0;#n=false;#o=0;position=0;constructor(t){this.#i=At(t)[Symbol.asyncIterator]();}async peekU32(){let t=await this.readExact(4);return this.unread(t),t.readUInt32LE(0)}async readExact(t){let e=Buffer.allocUnsafe(t),i=0;for(;i<t;){let n=await this.readSome(t-i);if(!n)throw new Error("archive is truncated");e.set(n,i),i+=n.byteLength;}return e}async readSome(t){for(;this.#e.length===0;)if(!await this.#s())return;let e=this.#e[0];if(!e)return;let i=e.byteLength-this.#r,n=Math.min(t,i),s=e.subarray(this.#r,this.#r+n);return this.#r+=n,this.position+=n,this.#r===e.byteLength&&(this.#e.shift(),this.#r=0),s}unread(t){if(t.byteLength!==0){if(this.#r>0){let e=this.#e[0];if(!e)throw new Error("archive reader queue is inconsistent");let i=e.subarray(this.#r);this.#e[0]=i,this.#r=0;}this.#e.unshift(t),this.position-=t.byteLength;}}async finish(){if(this.#e.length>0||await this.#s())throw new Error("archive contains trailing bytes");return {byteLength:this.#o,sha256:this.#t.digest("hex")}}async close(){try{await this.#i.return?.();}catch{}}async#s(){if(this.#n)return false;for(;;){let t;try{t=await this.#i.next();}catch{throw new E}if(t.done)return this.#n=true,false;if(!(t.value instanceof Uint8Array))throw new E;if(t.value.byteLength!==0){if(this.#t.update(t.value),this.#o+=t.value.byteLength,!Number.isSafeInteger(this.#o))throw new Error("archive byte length is unsafe");return this.#e.push(t.value),true}}}};function At(r){return Symbol.asyncIterator in r?r:{async*[Symbol.asyncIterator](){let t=r.getReader(),e=false;try{for(;;){let{value:i,done:n}=await t.read();if(n){e=!0;return}yield i;}}finally{if(!e)try{await t.cancel();}catch{}t.releaseLock();}}}}var xt=4*1024*1024,j=8,J=64*1024,Tt=16*1024,u=4,$=4,Ot=1,Vt=u+Ot,X=Vt+$,g=class extends Error{},k=class{#i;#t;#e=[];#r=0;#n;constructor(t,e={}){this.#i=t;let i=e.sortBudgetBytes??xt;if(this.#n=e.mergeFanIn??j,!Number.isSafeInteger(i)||i<X||!Number.isSafeInteger(this.#n)||this.#n<2||this.#n>j)throw new Error("checkpoint identity sort limits are invalid");this.#t=new _(i);}add(t){let e=Buffer.byteLength(t,"utf8");if(!this.#t.canFit(e))throw new Error("checkpoint durable identity exceeds the sort budget");if(this.#t.canAdd(e)){this.#t.add(t,e);return}return this.#o(t,e)}async finish(){if(this.#e.length===0){this.#t.sortAndAssertUnique(),this.#t.clear();return}for(this.#t.isEmpty||await this.#o();this.#e.length>this.#n;){let e=[];for(let i=0;i<this.#e.length;i+=this.#n){let n=this.#e.slice(i,i+this.#n),s=this.#s();await Ft(n,s),await Promise.all(n.map(o=>rm(o,{force:true}))),e.push(s);}this.#e=e;}let t;await tt(this.#e,e=>{if(t&&P(t,e)===0)throw new g;t=e;});}async#o(t,e){let i=this.#s();this.#t.sortAndAssertUnique(),await Mt(i,this.#t),this.#e.push(i),this.#t.clear(),t!==void 0&&e!==void 0&&this.#t.add(t,e);}#s(){let t=join(this.#i,`identity-run-${String(this.#r).padStart(8,"0")}.bin`);return this.#r+=1,t}},_=class{byteBudget;#i;#t;#e=0;#r=0;constructor(t){this.byteBudget=t;let e=Math.floor(t/X);this.#t=new Uint32Array(e),this.#i=Buffer.allocUnsafe(t-e*$);}get isEmpty(){return this.#r===0}canAdd(t){return this.canFit(t)&&this.#r<this.#t.length&&this.#e+u+t<=this.#i.length}canFit(t){return t<=4294967295&&this.#t.length>0&&u+t<=this.#i.length}add(t,e){if(!this.canAdd(e))throw new Error("checkpoint durable identity exceeds the sort budget");let i=this.#e;this.#t[this.#r]=i,this.#i.writeUInt32BE(e,i),this.#i.write(t,i+u,e,"utf8"),this.#e+=u+e,this.#r+=1;}sortAndAssertUnique(){let t=this.#t.subarray(0,this.#r);t.sort((e,i)=>this.#n(e,i));for(let e=1;e<t.length;e+=1){let i=t[e-1],n=t[e];if(i===void 0||n===void 0)throw new Error("checkpoint identity offset is missing");if(this.#n(i,n)===0)throw new g}}clear(){this.#e=0,this.#r=0;}forEachSorted(t){return Dt(this.#t.subarray(0,this.#r),e=>t(this.#s(e)))}#n(t,e){return P(this.#o(t),this.#o(e))}#o(t){let e=this.#i.readUInt32BE(t);return this.#i.subarray(t+u,t+u+e)}#s(t){let e=this.#i.readUInt32BE(t);return this.#i.subarray(t,t+u+e)}};async function Mt(r,t){await Q(r,async e=>{let i=new U(e);await t.forEachSorted(n=>i.write(n)),await i.finish();});}async function Ft(r,t){await Q(t,async e=>{let i=new U(e);await tt(r,n=>i.writeValue(n)),await i.finish();});}async function Q(r,t){await(await open(r,"wx",384)).close();let i=createWriteStream(r,{flags:"a",mode:384,highWaterMark:J});try{await t(i),i.end(),await once(i,"close");}catch(n){throw i.destroy(),n}}var U=class{#i;#t=Buffer.allocUnsafe(J);#e=0;constructor(t){this.#i=t;}write(t){if(t.byteLength>this.#t.length)return this.#r(t);if(this.#e+t.byteLength>this.#t.length)return this.#n(t);this.#t.set(t,this.#e),this.#e+=t.byteLength;}writeValue(t){let e=u+t.byteLength;if(e>this.#t.length)return this.#o(t);if(this.#e+e>this.#t.length)return this.#s(t);this.#c(t);}async finish(){await this.#a();}async#r(t){await this.#a(),await b(this.#i,t);}async#n(t){await this.#a(),this.#t.set(t,this.#e),this.#e+=t.byteLength;}async#o(t){await this.#a();let e=Buffer.allocUnsafe(u);e.writeUInt32BE(t.byteLength,0),await b(this.#i,e),await b(this.#i,t);}async#s(t){await this.#a(),this.#c(t);}async#a(){if(this.#e===0)return;let t=this.#t.subarray(0,this.#e);await b(this.#i,t),this.#e=0;}#c(t){this.#t.writeUInt32BE(t.byteLength,this.#e),this.#t.set(t,this.#e+u),this.#e+=u+t.byteLength;}};async function tt(r,t){let e=r.map(i=>new B(i));try{for(await Promise.all(e.map(i=>i.advance()));;){let i;for(let s of e)s.current&&(!i||P(s.current,i.current)<0)&&(i=s);if(!i?.current)return;let n=t(i.current);n&&await n,await i.advance();}}finally{for(let i of e)i.close();}}var B=class{#i;#t;#e=Buffer.alloc(0);#r=0;current;constructor(t){this.#i=createReadStream(t,{highWaterMark:Tt}),this.#t=this.#i[Symbol.asyncIterator]();}async advance(){let t=await this.#n(u);if(!t){this.current=void 0;return}let e=t.readUInt32BE(0);if(e===0)throw new Error("checkpoint identity run is invalid");let i=await this.#n(e);if(!i)throw new Error("checkpoint identity run is truncated");this.current=i;}close(){this.#i.destroy();}async#n(t){if(t===0)return Buffer.alloc(0);if(this.#e.length-this.#r>=t){let n=this.#e.subarray(this.#r,this.#r+t);return this.#r+=t,n}let e=Buffer.allocUnsafe(t),i=0;for(;i<t;){if(this.#r<this.#e.length){let s=Math.min(t-i,this.#e.length-this.#r);this.#e.copy(e,i,this.#r,this.#r+s),this.#r+=s,i+=s;continue}let n=await this.#t.next();if(n.done){if(i===0)return;throw new Error("checkpoint identity run is truncated")}this.#e=n.value,this.#r=0;}return e}};async function Dt(r,t){for(let e of r){let i=t(e);i&&await i;}}async function b(r,t){await new Promise((e,i)=>{r.write(t,n=>{n?i(n):e();});});}function P(r,t){let e=Math.min(r.length,t.length);for(let i=0;i<e;i+=1){let n=r[i],s=t[i];if(n===void 0||s===void 0)throw new Error("checkpoint identity byte is missing");let o=n-s;if(o!==0)return o}return r.length-t.length}var L=class{#i;#t=new Map;#e=new Map;#r=[];#n;constructor(t,e={}){this.#i=new k(t,{sortBudgetBytes:e.identitySortBudgetBytes,mergeFanIn:e.identityMergeFanIn});}openEntry(t){if(t===K$1){let e=[];return {write:async i=>{e.push(i.slice());},finish:async()=>{this.#n=Buffer.concat(e);}}}return new N(t,this.#t,this.#e,this.#i,e=>this.#r.push(e))}async finish(){if(!this.#n)throw new Error("checkpoint manifest is missing");let t=await M(this.#n),e=t.streams.flatMap(i=>i.chunks.map(n=>({...n,shapeName:i.shapeName})));if(e.length!==this.#r.length)throw new Error("checkpoint chunk inventory does not match");for(let[i,n]of e.entries()){let s=this.#r[i];if(s.path!==n.path||s.shapeName!==n.shapeName||s.recordCount!==n.recordCount||s.byteLength!==n.byteLength||s.sha256!==n.sha256)throw new Error("checkpoint chunk integrity does not match")}for(let i of t.streams){let n=this.#t.get(i.shapeName);if(!n||Q$1(n)!==i.recordCount)throw new Error("checkpoint stream does not match its manifest")}if(this.#t.size!==t.streams.length)throw new Error("checkpoint contains an undeclared stream");return await this.#i.finish(),{manifest:t,chunkCount:e.length}}},N=class{#i;#t;#e;#r;#n;#o=createHash("sha256");#s=[];#a=0;#c;#h=0;#d=0;constructor(t,e,i,n,s){this.#i=t,this.#t=e,this.#e=i,this.#r=n,this.#n=s;}async write(t){if(this.#o.update(t),this.#d+=t.byteLength,!Number.isSafeInteger(this.#d))throw new Error("checkpoint chunk byte length is unsafe");let e=0;for(;e<t.byteLength;){let i=t.indexOf(10,e);if(i<0)break;let n=this.#u(this.#l(t.subarray(e,i+1)));n&&await n,e=i+1;}e<t.byteLength&&this.#f(t.subarray(e));}async finish(){if(this.#s.length!==0||this.#h===0||!this.#c)throw new Error("checkpoint chunk framing is invalid");if(this.#d>I&&this.#h!==1)throw new Error("oversized checkpoint chunk has multiple rows");this.#n({path:this.#i,shapeName:this.#c,recordCount:this.#h,byteLength:this.#d,sha256:this.#o.digest("hex")});let t=this.#e.get(this.#c);if(!t)throw new Error("checkpoint stream chunk state is missing");t.previous={byteLength:this.#d};}#u(t){let e=N$1(t),i=e.kind==="shape"?e.wref:e.shapeName;if(this.#c!==void 0&&this.#c!==i)throw new Error("checkpoint chunk mixes Shape streams");this.#c=i,this.#h===0&&this.#y(i,t.byteLength);let n=this.#t.get(i)??O(i);this.#t.set(i,n),P$1(n,e);let s=this.#r.add(e.durableId);if(this.#h+=1,!Number.isSafeInteger(this.#h))throw new Error("checkpoint chunk row count is unsafe");return s}#f(t){if(t.byteLength!==0&&(this.#s.push(t.slice()),this.#a+=t.byteLength,!Number.isSafeInteger(this.#a)))throw new Error("checkpoint row byte length is unsafe")}#l(t){if(this.#s.length===0)return t;let e=Buffer.concat([...this.#s,t],this.#a+t.byteLength);return this.#s=[],this.#a=0,e}#y(t,e){let i=Number(this.#i.match(/part-(\d{10})\.jsonl$/)?.[1]??-1),n=this.#e.get(t)??{nextPart:0};if(i!==n.nextPart)throw new Error("checkpoint stream chunk parts are not contiguous");if(n.previous&&n.previous.byteLength<=I&&n.previous.byteLength+e<=I)throw new Error("checkpoint stream chunk closed before the byte limit");n.nextPart+=1,this.#e.set(t,n);}};async function Gt(r,t){return Kt(r,t)}async function Kt(r,t,e={}){let i=await mkdtemp(join(e.scratchParent??tmpdir(),"warmhub-checkpoint-verify-"));await chmod(i,448);let n,s;try{let o=new L(i,e),c=await G(r,p=>o.openEntry(p)),{manifest:h,chunkCount:f}=await o.finish();jt(t,c,h),n={valid:!0,checkpointId:h.checkpointId,repoSeq:h.repoSeq,recordCount:h.recordCount,chunkCount:f,totalBytes:c.byteLength,rootDigest:h.contentSha256,archiveDigest:c.sha256};}catch(o){s=Jt(o);}try{await rm(i,{recursive:!0,force:!0});}catch{s??=new d("scratch_cleanup_failed");}if(s)throw s;if(!n)throw new d("archive_invalid");return n}function jt(r,t,e){if(r&&(L$1.parse(r.sha256),r.checkpointId!==e.checkpointId||r.repoSeq!==e.repoSeq||r.byteLength!==t.byteLength||r.sha256!==t.sha256))throw new d("archive_integrity_mismatch")}function Jt(r){if(r instanceof d)return r;if(r instanceof E)return new d("source_failed");if(r instanceof g)return new d("duplicate_durable_id");let t=r instanceof Error?r.message:"";return t.includes("manifest")?new d("manifest_invalid"):t.includes("chunk")||t.includes("row")||t.includes("Shape stream")?new d("chunk_invalid"):new d("archive_invalid")}export{d as RepositoryCheckpointVerificationError,Gt as verifyRepositoryCheckpointArchive};//# sourceMappingURL=checkpoint.js.map
2
2
  //# sourceMappingURL=checkpoint.js.map