{
  "schemaVersion": 1,
  "sdk": "sdk",
  "baseline": {
    "version": "0.127.0",
    "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"
  },
  "releases": [
    {
      "version": "0.128.0",
      "sourceSha": "f990f080d6f8b7310700b1d430ee1aa11dcaaf23",
      "notes": [
        {
          "id": "published-changelog-artifacts",
          "body": "### Documentation\n\nVersioned release notes are available as [HTML, Markdown, and JSON](https://docs.warmhub.ai/sdk/typescript/changelog/)."
        }
      ]
    }
  ]
}
