@artblocks/abx-storage 0.1.0-alpha.20 → 0.1.0-alpha.22

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 (2) hide show
  1. package/CHANGELOG.md +559 -0
  2. package/package.json +3 -2
package/CHANGELOG.md ADDED
@@ -0,0 +1,559 @@
1
+ # @artblocks/abx-storage
2
+
3
+ ## 0.1.0-alpha.22
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [993e095]
8
+ - @artblocks/abx-sdk@0.1.0-alpha.22
9
+
10
+ ## 0.1.0-alpha.21
11
+
12
+ ### Patch Changes
13
+
14
+ - c9f7aeb: Fold the burn, and make "canonically ABX v2" sayable — from abx-services' 2026-08-20 reply
15
+ (`reviews/2026-08/services-reply-docs-23.md`). All off-chain: no contract changed, no address moved.
16
+
17
+ **SDK — `foldSpine` now folds an ERC-721 burn, and `TokenState.minted` is replaced by `lifecycle`
18
+ (BREAKING).** A burn wrote the zero address into `TokenState.owner` — where nothing downstream could
19
+ tell it from a holder — and `minted` latched `true` at mint and was never recomputed, so a burned
20
+ token reconstructed as live. `lifecycle: 'unminted' | 'live' | 'burned' | 'no-live-copies'` replaces it,
21
+ and `owner` is `null` for a burned id rather than a sentinel. An enum rather than the `minted` +
22
+ `burned` pair the report asked for: a boolean whose `false` has two meanings leaves the burn case one
23
+ forgotten field away from rendering as "not yet minted", which is exactly what our own CLI did.
24
+
25
+ **`'burned'` is terminal and ERC-721-only; an edition at zero live copies is `'no-live-copies'`.** The
26
+ first cut of this enum shared `'burned'` across both standards, on the grounds that "one vocabulary on
27
+ both lanes" was the honest shape. It wasn't: the correct _response_ to destruction differs by standard
28
+ (a 721 id is gone forever and must `410`; an edition id can mint again and must not), so a shared word
29
+ put every consumer one forgotten `contractType` branch away from serving `410 Gone` for a token the
30
+ contract still resolves — and our own migration note told the first consumer to map the shared word
31
+ straight onto their `410` sites. abx-services caught it in review before it published. Splitting the
32
+ word moves the rule out of prose and into the type: **`lifecycle === 'burned'` is safe to treat as
33
+ permanent on either standard, with no carve-out**, which let the reference resolver's `isEditionState`
34
+ guard go away entirely. Pinned from both ends — no edition history can fold to `'burned'`, and the
35
+ route asserts its own standard-blindness.
36
+
37
+ **The two lanes now agree id-for-id.** The same review found `TokenRow.lifecycle` (head reads) reporting
38
+ `'unknown'` for a fully-burned edition id while the fold reported `'burned'` for that same id — under a
39
+ docstring claiming the two lanes used the same words. That is the sibling-drift class this repo treats
40
+ as a bug (see `TokenState.maxSupply`'s note, the last time one field name carried two meanings). Both
41
+ lanes now answer `'no-live-copies'` there: the fold _could_ distinguish never-minted from fully-burned
42
+ and deliberately does not, because the distinction has no consumer. `'unknown'` survives for the one
43
+ case where a head read genuinely cannot say — a 1/1 has no mint frontier, so a reverting `ownerOf`
44
+ there is evidence of nothing.
45
+
46
+ What the latched boolean was costing, all in our own tree: the effects harness re-rendered destroyed
47
+ tokens on every sweep, forever; `onchain-uri`'s probe reads the LOWEST live id, so burning token 0 made
48
+ a healthy collection's on-chain-URI lane report as **broken**; the resolver served metadata — and the
49
+ pre-mint _warming placeholder_, i.e. "still loading" forever — for ids the contract disowns; `mintedCount`
50
+ never went down; and `abx state` printed "not yet minted" for a token that had been destroyed.
51
+
52
+ **SDK — `BurnConfigured` and `MaxRoyaltyBpsUpdated` fold into `ProjectState.burnable` /
53
+ `.maxRoyaltyBps`.** Both events shipped in the ABI and in `SPINE_EVENT_DOC` and reached no field of
54
+ state; the upgrade memo then told consumers the doc entry _was_ the fold, which it has never been (it
55
+ supplies `register`/`what` on the event record, full stop). Both are tri-state: `null` means the spine
56
+ never stated it — an implementation with no `burn` entrypoint and an unpublished ceiling — which is not
57
+ `false`, and not 10%. The ceiling is deliberately not nested inside `royalty`, since clearing a royalty
58
+ nulls that field while the ceiling stays binding on chain.
59
+
60
+ **SDK — a fold-coverage guard, because this was the second instance in four days.** `DefaultMaxSupplySet`
61
+ did the same thing on 2026-08-17. `SPINE_EVENT_NO_FOLD` now lists the events that deliberately reach no
62
+ state _and why_ (a ping whose value is a head read, a factory's own log, a minter sibling, an allowance),
63
+ and `spine-fold-coverage.test.ts` asserts every decodable event is either folded or excused. A new event
64
+ is unfolded and unexcused until someone decides which.
65
+
66
+ **SDK — a burned 1/1 no longer vanishes from `listTokens`.** The id range fell back to `totalSupply`
67
+ (live: mints − burns) when a token type exposes no `nextTokenId`, so burning a 1/1's only token made the
68
+ listing enumerate ZERO ids — `abx tokens` printed nothing at all, indistinguishable from a collection
69
+ with no tokens. A 1/1's id space is `{0}` forever regardless of what is live. Found by burning a real
70
+ token on Sepolia rather than by any fixture, which is the only way this one surfaces.
71
+
72
+ **SDK — head reads can now say `burned`.** `nextTokenId` is a mint frontier that only rises, so an id
73
+ below it whose `ownerOf` reverts was minted and destroyed. `TokenRow.lifecycle` (`'live'` / `'burned'` /
74
+ `'unminted'` / `'unknown'`) and `TokenListing.burnedCount` (`nextTokenId − totalSupply`) drop out of that,
75
+ with no event log. `'unknown'` stays a real member where the chain declines to say: a 1/1 has no frontier,
76
+ and an edition's `supply == 0` cannot distinguish never-minted from fully-burned.
77
+
78
+ **SDK — `burned` joins `ServiceErrorCode`, paired with `410 Gone`.** A remote token API had nothing
79
+ honest to say about a destroyed id and was sending `410` with `code: 'not_registered'` — a statement
80
+ about the _contract_. The rule behind the pairing is the general one: **a resolver answers what the
81
+ contract's own URI getter answers.** A burned 721's `tokenURI` reverts `NonexistentToken`, so serving a
82
+ document would contradict the contract; an ERC-1155's `uri(id)` has no existence gate and a zero-supply
83
+ id can mint again, so **an edition never 410s** — it serves, with `supply: 0`.
84
+
85
+ **SDK — `readCollectionPolicy`, `canonicalFactories`, `verifyProvenance`: protocol knowledge moves out
86
+ of the CLI.** The CLI was declaring its own ABI fragments for `burnable`/`maxRoyaltyBps` and reading
87
+ them itself in two places, so no other integrator could reach either fact. `canonicalFactories(chainId)`
88
+ replaces two copies of the six-anchor enumeration inside `anchors.ts` — the list whose completeness _is_
89
+ the trust model. `verifyCanonical` gains `opts.factories`, which **replaces** the manifest set rather
90
+ than appending to it, so a multi-tenant operator's pinned allowlist can drive the gate without silently
91
+ widening it.
92
+
93
+ **SDK — anchor generations: "canonically ABX v2", not a bare `false`.** `verifyCanonical` reported
94
+ "deployed by an ABX factory since replaced" and "deployed outside the toolkit entirely" identically —
95
+ its own docstring admitted it — and the first is a perfectly good collection. `ANCHOR_GENERATIONS` keys
96
+ each generation of the six trust anchors by the **on-chain core version** its clones report, and
97
+ `verifyProvenance` returns `{canonical, generation: 'current' | 'prior' | null, coreVersion, factory,
98
+ anchorsAnswered}`. The version is on the clone itself, so the answer is chain-verified rather than a
99
+ manifest claim, and it keeps working after a factory retires. Retired generations never feed
100
+ `verifyCanonical`: provenance is not trust — a superseded generation can predate a security remediation.
101
+ Pre-launch testnet generations are deliberately not backfilled.
102
+
103
+ **Redeploy process — a token-layer runtime change is a new anchor generation.** It bumps
104
+ `AbxVersion.CORE_VERSION` and moves the SDK's `isCurrent*` probes in lockstep. That is now a checklist
105
+ step (`contracts/README.md`, `CLAUDE.md`) _enforced by_ `deployments.test.ts`: recorded generations must
106
+ have unique, increasing core versions and the newest must match the constant in `AbxVersion.sol`, so a
107
+ batch cannot record a generation without bumping or bump without recording. The gap that prompted it: the
108
+ 2026-08-20 batch added `burn()`, `burnable()`, `maxRoyaltyBps()` and `reduceMaxRoyaltyBps()` to the CORE
109
+ base — burn is not an extension, so no extension version moved either — and left the constant at 2, so
110
+ `abxVersion() == 2` cannot tell a burn-capable token from a pre-burn one.
111
+
112
+ **CLI — the royalty ceiling is shown with its rate, and headroom is named.** A ceiling above the live
113
+ rate is royalty the owner can add unilaterally, and a listing page never shows it. `abx state` prints the
114
+ pair and, when there is headroom, says the useful thing: reducing the cap **to** the current rate is what
115
+ turns "5% today" into "5%, provably, forever". (Named by abx-services as their `royalty-headroom` flag —
116
+ a marketplace can compute it, so a creator should hear it from us first.)
117
+
118
+ **Reference stack — the same fixes, so the reference is exemplary rather than the counter-example.**
119
+ `mintedCount` keeps its name and counts LIVE ids, with `burnedCount` added only when non-zero (so a
120
+ project with no burns serves byte-identical JSON, and a consumer's fixtures keep telling the truth);
121
+ the indexer projection carries `lifecycle`; the effects status route and the render sweep skip destroyed
122
+ ids; the dashboard badges a burned token instead of calling it minted; and `abx verify` / the migrate and
123
+ re-point probes never choose a burned token as their subject.
124
+
125
+ **CLI — `--send` is accepted on the deploy paths, and two lane flags is refused.** Every deploy's help
126
+ advertises the signing lanes as `--send hot/env key · --sign wallet page · --unsigned print tx`, in 25
127
+ places, and `--send` was the one of the three the deploy allowlist rejected: typing what the help showed
128
+ produced `unrecognized flag(s): --send` from the guard whose whole purpose is catching flags that would
129
+ be silently ignored — the guard firing on the tool's own documentation. Found by using the CLI as a cold
130
+ reader of its own `--help` while setting up an on-chain test. While there, lane resolution moved from
131
+ truthiness to **presence** (`--sign=` parses to `''`, which is falsy, so a caller who explicitly named
132
+ the wallet lane was routed to the unattended env key — the one direction of that bug that costs
133
+ something), and naming two lanes at once now refuses instead of resolving by an undocumented precedence.
134
+
135
+ **Packaging — every published tarball ships its CHANGELOG.** `files` was `['dist', 'LICENSE']` on the
136
+ SDK, indexer, storage, effects and token-api, so the only package whose release notes reached consumers
137
+ was the CLI. A downstream team upgrading the SDK had no CHANGELOG in the tarball and diffed two `dist/`
138
+ trees to find out what moved — then reported the release notes as missing, which cost a whole exchange
139
+ to adjudicate. One line each.
140
+
141
+ **Docs + spec.** The event spine gains a Burn section and the `MaxRoyaltyBpsUpdated` row, both with the
142
+ fold rules an indexer relies on; three stale deploy-event-order lists are corrected; the remote-services
143
+ error taxonomy gains the `410 burned` row and the edition carve-out; `owner-powers` explains headroom and
144
+ burn permanence; and `indexing-notes` states plainly that documenting an event is not folding it.
145
+
146
+ - Updated dependencies [c9f7aeb]
147
+ - @artblocks/abx-sdk@0.1.0-alpha.21
148
+
149
+ ## 0.1.0-alpha.20
150
+
151
+ ### Patch Changes
152
+
153
+ - Updated dependencies [84ca9dd]
154
+ - Updated dependencies [84ca9dd]
155
+ - @artblocks/abx-sdk@0.1.0-alpha.20
156
+
157
+ ## 0.1.0-alpha.19
158
+
159
+ ### Patch Changes
160
+
161
+ - Updated dependencies [b1c333d]
162
+ - Updated dependencies [b1c333d]
163
+ - @artblocks/abx-sdk@0.1.0-alpha.19
164
+
165
+ ## 0.1.0-alpha.18
166
+
167
+ ### Patch Changes
168
+
169
+ - Updated dependencies [77a6248]
170
+ - Updated dependencies [77a6248]
171
+ - @artblocks/abx-sdk@0.1.0-alpha.18
172
+
173
+ ## 0.1.0-alpha.17
174
+
175
+ ### Patch Changes
176
+
177
+ - Updated dependencies [c5b8ec3]
178
+ - Updated dependencies [5b29f53]
179
+ - @artblocks/abx-sdk@0.1.0-alpha.17
180
+
181
+ ## 0.1.0-alpha.16
182
+
183
+ ### Patch Changes
184
+
185
+ - Updated dependencies [9c287c0]
186
+ - Updated dependencies [b5744a1]
187
+ - Updated dependencies [9c287c0]
188
+ - @artblocks/abx-sdk@0.1.0-alpha.16
189
+
190
+ ## 0.1.0-alpha.15
191
+
192
+ ### Patch Changes
193
+
194
+ - Updated dependencies [7fec2d7]
195
+ - @artblocks/abx-sdk@0.1.0-alpha.15
196
+
197
+ ## 0.1.0-alpha.14
198
+
199
+ ### Patch Changes
200
+
201
+ - Updated dependencies [1b6b741]
202
+ - Updated dependencies [1b6b741]
203
+ - Updated dependencies [1b6b741]
204
+ - Updated dependencies [1b6b741]
205
+ - Updated dependencies [1b6b741]
206
+ - Updated dependencies [1b6b741]
207
+ - Updated dependencies [f64a31f]
208
+ - Updated dependencies [1b6b741]
209
+ - Updated dependencies [1b6b741]
210
+ - Updated dependencies [1b6b741]
211
+ - Updated dependencies [1b6b741]
212
+ - Updated dependencies [1b6b741]
213
+ - Updated dependencies [1b6b741]
214
+ - Updated dependencies [1b6b741]
215
+ - Updated dependencies [1b6b741]
216
+ - Updated dependencies [1b6b741]
217
+ - Updated dependencies [1b6b741]
218
+ - @artblocks/abx-sdk@0.1.0-alpha.14
219
+
220
+ ## 0.1.0-alpha.13
221
+
222
+ ### Patch Changes
223
+
224
+ - Updated dependencies [528c6c6]
225
+ - @artblocks/abx-sdk@0.1.0-alpha.13
226
+
227
+ ## 0.1.0-alpha.12
228
+
229
+ ### Patch Changes
230
+
231
+ - Updated dependencies [afa9dd4]
232
+ - Updated dependencies [8c254d5]
233
+ - @artblocks/abx-sdk@0.1.0-alpha.12
234
+
235
+ ## 0.1.0-alpha.11
236
+
237
+ ### Patch Changes
238
+
239
+ - d40caf4: Requested by an integrator (abx-services) — the conformance surface now lives in the neutral
240
+ layer. Five pieces that only token-api or storage exposed before, and that any third-party
241
+ resolver needs to reproduce the reference behavior byte-for-byte, move into `@artblocks/abx-sdk`:
242
+
243
+ - **The generator-document family** (`ABX_JS`, `escapeInlineScript`/`escapeInlineJson`,
244
+ `buildGeneratorDocument`, `injectTokenDataIntoHtml` — new `src/generator-document.ts`): pure
245
+ string operations with no resolver-specific behavior, so the SDK, `abx preview`, and any
246
+ third-party provider now share one definition instead of the CLI importing token-api just for
247
+ this.
248
+ - **The registry-dependency family** (`DEPENDENCY_REGISTRY_ABI`, `activeRegistry`,
249
+ `resolveRegistryDep`, `registryDepUrl`, `dependencyScriptTags`, `URL_BUDGET_BYTES`), merged into
250
+ the SDK's existing `deps.ts` alongside its registry-pointer logic. `node:zlib` can't come along
251
+ — SDK core has to stay reachable from a browser bundle — so decompression is now an INJECTED
252
+ `inflate?: (bytes: Uint8Array) => Uint8Array` threaded through `resolveRegistryDep`/
253
+ `dependencyScriptTags`; omitting it when a resolved dep actually needs decompressing throws a
254
+ new typed `InflateRequiredError` naming the fix, rather than crashing opaquely or silently
255
+ degrading. `@artblocks/abx-sdk/node` gains `nodeInflate` (a one-line `gunzipSync` wrapper) so a
256
+ Node host wires it in one line; a browser host passes a `DecompressionStream`-based
257
+ implementation instead. (Merge note: the prior `dependencyRegistryReadAbi` and token-api's
258
+ `DEPENDENCY_REGISTRY_ABI` declared the identical `getDependencyDetails` entry twice — deduped
259
+ into one array that also carries `getDependencyScript`.)
260
+ - **`contentTypeFromPath`** (new `src/mime.ts`): the MIME extension-map lookup, verbatim.
261
+ - **Gateway resolution, split pure/env** (new `src/gateways.ts`): `resolveGatewayBase` is now PURE
262
+ (no env read — takes resolved `{ipfs?, arweave?}` overrides) and `gatewayUrlFor` moves alongside
263
+ it; `gatewayConfigFromEnv()` is the one place that reads `ABX_IPFS_GATEWAY`/
264
+ `ABX_ARWEAVE_GATEWAY`, kept separate so a host with its own gateway config never has to touch
265
+ `process.env` through this module at all.
266
+ - **`exponentialBackoffDelay(attempt, baseMs, capMs)`** (`util.ts`): `min(baseMs * 2^(attempt-1),
267
+ capMs)`, 1-indexed like the existing `linearBackoffDelay`. Prefer it for a sustained rate limit
268
+ or an overloaded upstream; `linearBackoffDelay` stays right for a one-off transient failure.
269
+ `service.ts`'s own retry ladder is unchanged (still linear — its rationale comment stands).
270
+
271
+ Token-api and storage keep every existing export working, re-exported from the SDK where the
272
+ implementation moved — no removals, and token-api's own `nodeInflate`-pre-wired wrappers mean its
273
+ internal call sites (`code.ts`'s document assembly, `deps.ts`'s `depStatusReport`) needed no
274
+ signature changes at all. The browser-bundle test (`packages/sdk/test/browser-bundle.test.ts`)
275
+ stays green with all of this now exported from the core index — proof that the injected-`inflate`
276
+ design actually keeps `node:zlib` out of the bundle.
277
+
278
+ - Updated dependencies [d40caf4]
279
+ - Updated dependencies [d40caf4]
280
+ - @artblocks/abx-sdk@0.1.0-alpha.11
281
+
282
+ ## 0.1.0-alpha.10
283
+
284
+ ### Minor Changes
285
+
286
+ - 11fa933: The extraction phase of the simplification refactor: business logic that lived only inside the CLI
287
+ is now importable — the CLI calls the same functions you can.
288
+
289
+ **Into the SDK:** the five trust-anchor bootstraps (`ensureFactory`, `ensureSeriesFactory`,
290
+ `ensureSeriesCodeFactory`, `ensureRenderer`, `ensureSeedSource` — `anchors.ts`, on the same
291
+ injected-`send` + `onEvent` pattern as `ensureChunkStore`, with a typed `AnchorUnavailableError`),
292
+ `detectCanonicalFactory`/`resolveScanFloor`, the on-chain-URI setup composer (`onchain-uri.ts`),
293
+ interrupted-deploy resume planning (`resume.ts`), migration plan/parity reconciliation
294
+ (`migrate.ts`), the `abx.js` static analyzer (`inspect.ts`), dependency setup legs (`deps.ts`),
295
+ content-staging plans (`staging.ts` — `planStagedContent`, `stageFieldContent` with `StagingEvent`),
296
+ `mintedTokenIds`, and a typed `readSaleConfig` for the fixed-price minter.
297
+
298
+ **Into storage:** `uploadAndLocate`, `repinNodeCustody` (the byte-custody half of migration),
299
+ `decideImageContentLane`, `assessStorageReadiness`/`assessTurboFunds` (the Arweave/Turbo funding
300
+ math), and `awaitLocatorReady` (poll a locator until it serves).
301
+
302
+ **CLI hardening that fell out of the dedup:** one risk gate (`gatedSend`) now guards every write —
303
+ `--dry-run` and `--confirm` mean the same thing on every command, all owner-ops gain `--confirm`,
304
+ and a write reaching the send lane under `--dry-run` is structurally impossible (grep-enforced by
305
+ test). One `CHAIN` source of truth; one memoized local-indexer accessor.
306
+
307
+ ### Patch Changes
308
+
309
+ - 11fa933: The remaining phases of the simplification refactor that hadn't yet gotten a changeset: the CLI's
310
+ internal module split, the token-api/effects/mint-page convergence on the SDK, the shipped skill's
311
+ rewrite for the simplified surface, and a new SDK README.
312
+
313
+ - **`abx`'s `main.ts` split into domain command modules** (`commands/{deploy,project,reads,service,
314
+ scaffold,storage}.ts`, shared `output.ts`/`errors.ts`), with one exit-discipline rule
315
+ (`process.exitCode` + return, or a typed `CliError`, everywhere — bare `process.exit` only at the
316
+ entry guard, the top-level catch, and the keep-alive SIGINT handler). Purely internal: a 207-fixture
317
+ byte-diff matrix (every help text, dry-run, error path, and exit code) confirmed identical output
318
+ before and after.
319
+ - **token-api / effects / mint-page converge on the SDK**: `@artblocks/abx-storage` gains one
320
+ `resolveGatewayBase` (`readiness.ts`), replacing three near-identical copies (two in token-api, one
321
+ inline in storage itself); token-api exports `buildGeneratorDocument` so the CLI's `abx preview`
322
+ consumes the real generator-document assembler instead of a hand-kept duplicate; the effects runner
323
+ now resolves its config via the SDK's `readEnv` and gets a `makePublicClient` fallback transport, so
324
+ `ABX_RPC_URL` accepts a comma-separated failover list like every other RPC var; the scaffolded
325
+ mint-page app now imports ABIs from `@artblocks/abx-sdk/abi` and a browser-safe `makePublicClient` +
326
+ typed `readSaleConfig` instead of hand-rolled fetch/decode, and pins its generated `package.json` to
327
+ the SDK's _resolved_ version via a new `@artblocks/abx-sdk/package.json` export (alpha version
328
+ counters diverge per package under changesets, so pinning the CLI's own number could produce an
329
+ unsatisfiable range).
330
+ - **The shipped skill (`.claude/skills/abx-self-host/`) is rewritten for the surface phases 0–5
331
+ actually shipped**: every warning made obsolete by an enforcement is deleted rather than softened —
332
+ predict-only deploy-preview addresses, the `approvals N` line, the single `ABX_REMOTE_SELF_*`
333
+ credential grammar, `doctor`'s version/provenance ladder, `storage show --check`, the render/storage
334
+ combo validator's dry-run row, and `ABX_DEPLOYER_PK` as the only key name. Retired names swept from
335
+ `dev-loop-test`, the agent-eval scenarios, and spec prose. The skill ships bundled inside this CLI
336
+ package (co-versioned via `SKILL.md` frontmatter), so it rides this same patch.
337
+ - **New `packages/sdk/README.md`**: what the SDK is, the send-injection model (`PreparedTx` +
338
+ `SendTx`, `makeHotSender` for a hot key, bring-your-own for a wallet/multisig), a complete
339
+ deploy → upload → mint → read walkthrough against real exports, and browser-use notes (explicit
340
+ `rpcUrls`, no env, the `/node` subpath is Node-only). Included in the npm tarball automatically
341
+ (README is one of the files npm always packs, regardless of the `files` allowlist).
342
+
343
+ - 11fa933: Phase 5 interface unifications: the self resolver becomes a named remote, doctor absorbs the
344
+ version/install/provenance ladder, deploy previews stop showing addresses they can't keep,
345
+ approvals are counted honestly, render×storage combos are validated once, and storage config gets
346
+ a real probe.
347
+
348
+ - **Self resolver is a named remote (breaking, client-side only)**: the CLIENT credential for the
349
+ local/self resolver moves from `ABX_RESOLVER_ADMIN_TOKEN` to the named-remote grammar
350
+ (`ABX_REMOTE_SELF_URL` / `ABX_REMOTE_SELF_TOKEN` — "self" is just a conventional remote name, zero
351
+ special-casing). The SERVER side (token-api control-plane auth, `provision.ts`, the Fly.io secret)
352
+ keeps its old name — it's the service's own config, not a client credential, so deployed resolvers
353
+ need no change. The old client var, if still set, is now a pointed `CliError` naming both new vars
354
+ (no silent fallback); `abx doctor` flags it too, and the near-miss `_KEY`-vs-`_TOKEN` detector keeps
355
+ working under the unified grammar.
356
+ - **`abx doctor` absorbs the version/install/provenance ladder** the skill used to only teach in
357
+ prose: one new ✓/✗ block for binary provenance (source checkout / npm install / npx — the silent-
358
+ stale-npx-cache trap), npm currency (reuses the existing cached update check), and skill↔CLI
359
+ version match (reuses `abx skill`'s own discovery). Each row names the exact fix.
360
+ - **Deploy previews stop showing addresses they can't keep**: `--dry-run` (`deploy` /
361
+ `deploy-series` / `deploy-code`) **without `--salt`** no longer prints a "deterministic address" —
362
+ that salt was just freshly, randomly reserved, so the address was real for that one preview and
363
+ never reproducible by a plain re-run. It now prints the salt itself, prominently, plus how to pin
364
+ it: re-run with `--salt <shown>` (guaranteed same address), or `abx predict --salt <shown> --for
365
+ <signer>`. **With `--salt`, the address prints exactly as before** (it IS stable) — `abx predict`
366
+ is unchanged. The same rule applies to `--json`: without `--salt`, `address` reports `null` rather
367
+ than a value the real deploy won't land at (`saltPinned` still says why).
368
+ - **`approvals` — a labeled, honest signature count**: every deploy-family `--dry-run` preview and
369
+ `--confirm` summary now carries one uniform `approvals N wallet approval(s)` line/clause — the
370
+ number of **wallet TX signatures** the real run will ask for (a connected-wallet storage upload,
371
+ e.g. Arweave via `--storage-signer eth`, is a message signature, not a transaction, and stays
372
+ listed separately, as it already was). It's derived from the exact same staging/setup-multicall
373
+ math that sizes the wallet-lane session's own `total`, so the two can never disagree — this caught
374
+ (and fixed) `deploy-code`'s wallet-lane session `total` being hardcoded to `2` even on the 1-tx path
375
+ (no chunks/schema/deps/on-chain-uri legs/setup-carried mints), which would have shown "transaction
376
+ 1 of 2" and then silently never asked for a second.
377
+ - **One validator for render×storage combos**: `@artblocks/abx-storage` gains
378
+ `validateRenderStorageCombo()` (`content-plan.ts`) — the single source of truth for two known-bad
379
+ configurations, each checked against the ACTUAL fact that makes it bad (not against an unrelated
380
+ flag): (1) `--image-base` needs a backend that can overwrite a stable per-token key in place; an
381
+ ipfs/arweave-_shaped URL_ is refused regardless of this deploy's own `--backend`, because those are
382
+ content-addressed — a re-upload gets a new address, so no fixed URL can point at it. (2) publishing
383
+ a render to a resolver that doesn't share this machine's disk needs a backend that can hand back a
384
+ public URL at all — `fs` (or `cloud` with no public base) can't. `deploy-code` wires this into BOTH
385
+ its real-run refusal (`--image-base`) and a new `render/storage ✓|✗ <reason>` dry-run row;
386
+ `requirePublishableBackend` (the existing `abx render --remote` / `abx effects` guard) now consults
387
+ the same validator for its ok/not-ok decision, so the two surfaces can't drift apart.
388
+ - **`abx storage show --check`**: a real read/write against the resolved storage config, not just
389
+ "is it configured." `cloud` PUTs a tiny object through the signed API and GETs it back over the
390
+ PUBLIC base with a plain unsigned fetch — the only check that catches the R2/S3
391
+ endpoint-vs-public-base trap (`health()` alone only proves the API credentials work); on failure
392
+ both URLs print, so the mismatch is visible. `ipfs`/`fs` reuse their existing `health()` verbatim
393
+ (gateway/API reachability, dir writability — no upload, no pin — reused, not reimplemented).
394
+ `arweave` adds an identity+balance READ, never a paid upload. Exit code is meaningful (0 ok / 1 any
395
+ ✗) so a script can gate a launch on it. `abx doctor`'s storage row now runs the same fuller probe
396
+ (bounded to 1.5s, matching its other fast network checks) instead of a bare `health()` call.
397
+
398
+ - Updated dependencies [11fa933]
399
+ - Updated dependencies [11fa933]
400
+ - Updated dependencies [11fa933]
401
+ - @artblocks/abx-sdk@0.1.0-alpha.10
402
+
403
+ ## 0.1.0-alpha.9
404
+
405
+ ### Patch Changes
406
+
407
+ - df298d8: `abx storage status <locator>` — is it retrievable yet, or only accepted?
408
+
409
+ Backlog B21, and the second half of a gap two independent integrations hit eight days apart. An upload
410
+ service answers "accepted" the moment it holds your bytes; a gateway serves them only once they
411
+ propagate, and on Arweave that runs to minutes. Nothing in the upload result distinguished the two, so
412
+ the natural implementation — upload during a mint, write the locator into the token — mints a token
413
+ that renders broken for the first minutes of its life.
414
+
415
+ The first reporter rebuilt this layer themselves (ranged GETs, a propagating/ready model, retry ladders
416
+ lengthened after measuring real times) and concluded "every serious integrator will rebuild some
417
+ version of this." The second published 32 renders and found **32/32 404ing on `arweave.net` while 22/32
418
+ already served from `permagate.io` and `vilenarios.com`**, with the uploader reporting `CONFIRMED`
419
+ throughout — and the expensive part is what a creator does next, since a placeholder on a fresh drop
420
+ reads as a failed render, so you re-run `abx render --force` and re-upload everything for nothing.
421
+
422
+ That second observation shapes the design: propagation is **per-gateway**, so the check probes the
423
+ gateway your project actually uses _plus two others_, which buys a third verdict the reporters' own
424
+ two-state model couldn't express.
425
+
426
+ - **`ready`** — your gateway serves the bytes. Safe to reference.
427
+ - **`propagating`** — another gateway serves them, so the data **provably exists** on the network and
428
+ yours is merely behind. Waiting is the fix, and the command says plainly not to re-upload.
429
+ - **`unreachable`** — nothing probed serves them. Deliberately _not_ called propagating: from outside,
430
+ a locator that is still settling and one that is simply wrong look identical, and reporting the
431
+ friendlier of the two is how a tool teaches someone to ignore it. When every gateway rejects the id
432
+ itself (a 4xx that isn't 404) rather than just missing it, that _is_ evidence, and the output says
433
+ "malformed locator, waiting will not fix it" — the inverse mistake of waiting out a typo costs more
434
+ than a needless re-upload.
435
+
436
+ Accepts every form a locator arrives in (`ar://`, `ipfs://`, a gateway URL, a bare txid/CID, with a
437
+ directory path suffix), reads **headers only** via a ranged request with the body cancelled — so
438
+ checking a 40 MB asset doesn't download it — and **exits non-zero unless ready**, which makes waiting a
439
+ one-liner instead of a retry ladder: `until abx storage status <loc> --json; do sleep 10; done`.
440
+
441
+ The primitive is `locatorStatus()` in `@artblocks/abx-storage`, not CLI-only (per B20): anything
442
+ programmatic should call it in-process rather than spawning the CLI per check. `abx render`'s existing
443
+ propagation note now points at the command, so the advisory has an answer attached.
444
+
445
+ - Updated dependencies [df298d8]
446
+ - Updated dependencies [df298d8]
447
+ - @artblocks/abx-sdk@0.1.0-alpha.9
448
+
449
+ ## 0.1.0-alpha.8
450
+
451
+ ### Patch Changes
452
+
453
+ - Updated dependencies [e325b46]
454
+ - @artblocks/abx-sdk@0.1.0-alpha.8
455
+
456
+ ## 0.1.0-alpha.7
457
+
458
+ ### Patch Changes
459
+
460
+ - 1b50f9d: Fixes from an integrator batch: a machine-readable `tokenuri`, one Arweave identity across CLI and SDK, a correct OpenSea refresh, and attach telling the truth.
461
+
462
+ **`abx tokenuri --json`.** The command abbreviated long values (`… (382 chars)`) with no way to turn it
463
+ off, so for a token whose whole point is on-chain content it returned something that _looked_ like the
464
+ metadata and wasn't. An integrator scraped it, stored a `data:` URI cut to 96 characters, and only
465
+ found out in production; they abandoned the CLI as a read path and reimplemented `eth_call`. `--json`
466
+ now emits the verbatim decoded document — no banner, no ANSI, no truncation — so
467
+ `abx tokenuri <addr> --json | jq` is a supported read path. The human view still abbreviates, and now
468
+ says `[--json for the full value]`.
469
+
470
+ **One Arweave identity, resolved in one place.** `arweaveConfigFromEnv()` read `ARWEAVE_JWK` and
471
+ nothing else, while the CLI mints and manages `.abx-self-host/arweave-key.json`. Porting a working CLI
472
+ flow to the SDK — same machine, minutes later — failed every upload with "Arweave via Turbo needs an
473
+ identity", a message that says storage was never configured when the truth was that two layers
474
+ disagreed about where the identity lives. `@artblocks/abx-storage` now exports `resolveArweaveJwk()`
475
+ (env → managed key file) and the CLI delegates to it. Its diagnostics come with it: an empty key file
476
+ now reports the **path** and the remedy instead of `Unexpected end of JSON input`, and a corrupt one
477
+ says the same.
478
+
479
+ **`abx refresh` on the default chain.** The OpenSea slug map held only `sepolia` and `mainnet`, so
480
+ `base-sepolia` — the CLI's own default — fell through to the raw key: the refresh POST went to a slug
481
+ OpenSea doesn't know, and the printed link pointed at **mainnet** `opensea.io` for a testnet token.
482
+ Slugs are now correct (`base_sepolia`), `testnet` comes from the chain registry rather than a second
483
+ hand-maintained set, and a chain with no known slug produces **no link** instead of a wrong one. Same
484
+ shape as the hardcoded explorer table that once sent every Base Sepolia link to Etherscan.
485
+
486
+ **`abx attach` names its dependency.** Attaching artifacts to a project that resolves on-chain now
487
+ warns, before the send, that they will **not** appear in `tokenURI` — the on-chain renderer carries
488
+ reserved fields only, and the artifacts manifest comes from a resolver. A team attached five audio
489
+ stems to a fully-on-chain token and found them "paid for, stored on-chain, and invisible"; the note
490
+ that existed was one dim line that read as a footnote rather than as a missing service.
491
+
492
+ **`ensureChunkStore` moved to the SDK.** The bootstrap every on-chain-content path needs existed only
493
+ inside the CLI, so an SDK integrator got `resolveChunkStore()` (may return undefined) plus a separate
494
+ `storeSupportsWriteContent()` they had to remember — forget it and an incapable store fails _deep
495
+ inside a mint, after transactions have landed_. One team hand-rolled the guard for exactly that reason.
496
+ `ensureChunkStore(publicClient, send, {chainId, override, onEvent})` is now exported; the SDK reports
497
+ progress through `onEvent` instead of printing, and the CLI keeps its narration.
498
+
499
+ **`abx storage upload --json`.** The locator as data. They scraped this line, captured its ANSI colour
500
+ codes along with the URL, wrote the result into a _stored_ player URL, and found out when it 404'd in
501
+ production. In `--json` mode stdout carries the JSON and nothing else; progress moves to stderr.
502
+
503
+ **`--backend ipfs` no longer hides a missing credential.** Without `PINATA_JWT` the backend resolves to
504
+ **kubo against a local node**, so a dry run looked fine and the real upload failed for anyone not running
505
+ one. The preview now says so. Related correction: the skill claimed "a backend missing its secret falls
506
+ back to `fs`" — it does not. `cloud` refuses up front naming the missing values, and `ipfs` goes to the
507
+ local node; nothing silently degrades to local disk. Both sides now say the same thing.
508
+
509
+ Reported in the 2026-08-03 MXRR integration batch (feedback 869f27b1, a256217f, 119d7e8e, 975c363e,
510
+ e38216db, e267078b).
511
+
512
+ - Updated dependencies [1b50f9d]
513
+ - Updated dependencies [1b50f9d]
514
+ - @artblocks/abx-sdk@0.1.0-alpha.7
515
+
516
+ ## 0.1.0-alpha.6
517
+
518
+ ### Patch Changes
519
+
520
+ - Updated dependencies [1158420]
521
+ - Updated dependencies [1158420]
522
+ - Updated dependencies [1158420]
523
+ - @artblocks/abx-sdk@0.1.0-alpha.6
524
+
525
+ ## 0.1.0-alpha.5
526
+
527
+ ### Patch Changes
528
+
529
+ - Updated dependencies [feba8c2]
530
+ - @artblocks/abx-sdk@0.1.0-alpha.5
531
+
532
+ ## 0.1.0-alpha.4
533
+
534
+ ### Patch Changes
535
+
536
+ - Updated dependencies [67b686b]
537
+ - @artblocks/abx-sdk@0.1.0-alpha.4
538
+
539
+ ## 0.1.0-alpha.3
540
+
541
+ ### Patch Changes
542
+
543
+ - Updated dependencies [a72723d]
544
+ - @artblocks/abx-sdk@0.1.0-alpha.3
545
+
546
+ ## 0.1.0-alpha.2
547
+
548
+ ### Patch Changes
549
+
550
+ - Updated dependencies [3745bd3]
551
+ - Updated dependencies [3745bd3]
552
+ - @artblocks/abx-sdk@0.1.0-alpha.2
553
+
554
+ ## 0.1.0-alpha.1
555
+
556
+ ### Patch Changes
557
+
558
+ - Updated dependencies [4074766]
559
+ - @artblocks/abx-sdk@0.1.0-alpha.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artblocks/abx-storage",
3
- "version": "0.1.0-alpha.20",
3
+ "version": "0.1.0-alpha.22",
4
4
  "license": "MIT",
5
5
  "description": "ABX Self-Host Toolkit (Layer 3) — the reference byte-custody / storage service. Holds the source-of-truth bytes a token's content commitment points at, content-addressed by hash, behind a pluggable backend interface.",
6
6
  "type": "module",
@@ -13,6 +13,7 @@
13
13
  },
14
14
  "files": [
15
15
  "dist",
16
+ "CHANGELOG.md",
16
17
  "LICENSE"
17
18
  ],
18
19
  "sideEffects": false,
@@ -39,7 +40,7 @@
39
40
  },
40
41
  "dependencies": {
41
42
  "viem": "^2.21.0",
42
- "@artblocks/abx-sdk": "0.1.0-alpha.20"
43
+ "@artblocks/abx-sdk": "0.1.0-alpha.22"
43
44
  },
44
45
  "optionalDependencies": {
45
46
  "@ardrive/turbo-sdk": "^1.23.0",