diff --git a/.changeset/bump-product-sdk-truapi.md b/.changeset/bump-product-sdk-truapi.md new file mode 100644 index 00000000..7db827de --- /dev/null +++ b/.changeset/bump-product-sdk-truapi.md @@ -0,0 +1,23 @@ +--- +"playground-cli": minor +--- + +Bump the full `@parity` dependency stack to latest published and migrate to the +`Result`-based SDK error API. + +- `@parity/product-sdk-*` to latest: host 0.14.1 (now built on `@parity/truapi` + instead of the former `@novasamatech` in-container host-api), cloud-storage + 0.8.1, contracts 0.9.2, descriptors 0.8.0, keys 0.3.16, terminal 0.6.2, tx + 0.3.2. Dropped the temporary vendored headless-host prerelease. +- `@parity/polkadot-app-deploy` 0.11.0 → 0.13.1, `@parity/dotns-cli` 0.7.2 → + 0.8.0, `@parity/cdm-builder` 3.1.7 → 3.2.0, `@parity/cdm-codegen` 0.6.20 → + 0.6.23, `@parity/cdm-env` 2.0.6 → 2.1.0, and `polkadot-api` aligned to 2.2.1 + (single runtime instance). +- Migrated command handlers to the `Result` API (`submitAndWatch`, contract + `.tx`, cloud-storage `checkAuthorization`, `ContractManager.fromLiveClient`), + preserving user-facing error messages — and a failed registry publish now + surfaces the on-chain revert reason (e.g. `Unauthorized`, `NotRevealed`) + instead of a generic "reverted", failing fast on deterministic reverts. +- Retired the `summit` / `w3s` environment: polkadot-app-deploy 0.13.x removed + it from `environments.json` and product-sdk-descriptors 0.8.0 dropped the + `summit-*` descriptors. Only `paseo-next-v2` is wired. diff --git a/CLAUDE.md b/CLAUDE.md index e772ba98..f032cc92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ pnpm typecheck pnpm test ``` -`pnpm build` compiles with bun, which STRIPS types without checking them — it is NOT a type signal. The tsc baseline was burned to zero in June 2026 and `pnpm typecheck` (`tsc --noEmit`) now runs as a CI job, so any new type error fails the build — keep it at zero. The cdm-builder descriptor skew is bridged through two cast seams: `asCloudStorageApi` in `src/utils/allowances/bulletin.ts` (the `TypedApi` vs `BulletinTypedApi` case) and `asCdmAssetHubApi`/`asCdmAssetHubDescriptor` in `src/commands/contract.ts` (the paseo-asset-hub `DotnsGateway` shape drift that arose when our root `@parity/product-sdk-descriptors` moved to `0.6.0` while older cdm-builder still generated its `PipelineChainClient` type from a `0.5.x`-era descriptor — `PopControllerAddressSet` vs `DispatcherAddressSet`; the contract pipeline never touches DotnsGateway, so the cast is runtime-safe). As of `cdm-builder@3.1.6` the descriptor generation realigned: 3.1.6 generates `PipelineChainClient` from descriptors `0.6.0` (`DispatcherAddressSet`), matching our `^0.6.1` root — so the **asset-hub** seam (`asCdmAssetHubApi`/`asCdmAssetHubDescriptor`) is now redundant and safe to delete in a follow-up PR gated on `pnpm typecheck`. The `asCloudStorageApi` seam STAYS: it bridges a `BulletinTypedApi` vs `TypedApi` codegen skew (two codegens of the same chain), not a descriptor-version skew, so it does not realign with this bump. If `lint:license` flags a file you authored, run `./scripts/check-license-headers.sh --fix` to prepend the standard Parity Apache-2.0 header (`SPDX-License-Identifier: Apache-2.0` + `Copyright (C) Parity Technologies (UK) Ltd.`, both lines required). The check script keeps shebangs on line 1 and places the header below them. +`pnpm build` compiles with bun, which STRIPS types without checking them — it is NOT a type signal. The tsc baseline was burned to zero in June 2026 and `pnpm typecheck` (`tsc --noEmit`) now runs as a CI job, so any new type error fails the build — keep it at zero. The cdm-builder version skew is bridged through THREE cast seams: `asCloudStorageApi` in `src/utils/allowances/bulletin.ts` (the `TypedApi` vs `BulletinTypedApi` codegen skew — two codegens of the same chain), plus `asCdmAssetHubApi`/`asCdmAssetHubDescriptor` and `asCdmRegistryContract` in `src/commands/contract.ts`. The asset-hub seams bridge the paseo-asset-hub `DotnsGateway`/descriptor shape drift between cdm-builder's pinned `@parity/product-sdk-descriptors` and our root's (the contract pipeline never touches DotnsGateway, so the cast is runtime-safe). `asCdmRegistryContract` was added when our root `@parity/product-sdk-contracts` reached `0.9.x`: `Contract.tx()` now resolves a `Result`, nominally unassignable to cdm-builder's older `RegistryContract` param — but the install-time registry is query-only (no signer wired in), so `.tx()` is never invoked through it and the cast is runtime-safe. At `cdm-builder@3.2.0` the pinned product-sdk moved up to `contracts@0.9.0` / `descriptors@0.7.0` while our root is `contracts@^0.9.2` / `descriptors@^0.8.0` — so the `.tx()` Result shape realigns (making `asCdmRegistryContract` likely redundant) while a descriptors minor gap (0.7 vs 0.8) remains. Before deleting any seam, remove it and run `pnpm typecheck`. If `lint:license` flags a file you authored, run `./scripts/check-license-headers.sh --fix` to prepend the standard Parity Apache-2.0 header (`SPDX-License-Identifier: Apache-2.0` + `Copyright (C) Parity Technologies (UK) Ltd.`, both lines required). The check script keeps shebangs on line 1 and places the header below them. ## Non-obvious invariants @@ -22,24 +22,24 @@ These aren't self-evident from reading the code and have bitten us before. Treat ### Dependency pins / lockfile - **Import from `@parity/product-sdk-*`, never `@polkadot-apps/*`.** The CLI runtime is fully on product-sdk. `@polkadot-apps/*` is gone from the lockfile and CI's `Format` job runs `grep -rnE "['\"]@polkadot-apps/" src/ e2e/ scripts/ tools/` as a guard. Product-sdk uses caret ranges (`^0.x.y`); on a 0.x line `^` only widens patches, so a true breaking change still needs an explicit `package.json` bump. -- **The CDM contract packages are now `@parity/cdm-*`, not `@dotdm/*` (migrated June 2026).** The republish renamed `@dotdm/cdm` to `@parity/cdm-codegen` (pinned EXACT `0.6.20`), `@dotdm/contracts` to `@parity/cdm-builder` (pinned EXACT `3.1.7`), and `@dotdm/env` to `@parity/cdm-env` (`^2.0.6`); `@parity/cdm-utils` arrives transitively. `cdm-builder`/`cdm-codegen` stay EXACT because this line has shipped breaking changes in PATCH releases. The `3.1.4` line dropped `resolveTargetRegistryAddress` and `computeTargetHash` and flattened `CdmJson`: the `targets`/`targetHash` layer is gone, so `dependencies` (library to version) and `contracts` (library to contract) are now flat maps plus a single optional top-level `registry` string. As a side effect, cdm.json no longer stores connection URLs (asset-hub/bulletin); those resolve only from CLI opts, the chain preset, or config defaults. The `3.1.5 → 3.1.6` / `0.6.18 → 0.6.19` / `2.0.5 → 2.0.6` bump (June 2026) is a verified-safe patch: `CdmJson`/`CdmJsonContract` are byte-identical (no re-flattening), the cdm-codegen dist is byte-identical, and the only substantive change is that cdm-builder's product-sdk pins moved UP toward our caret roots — now EXACT `contracts@0.7.3`, `tx@0.2.10`, `cloud-storage@0.6.0`, `descriptors@0.6.0` (was `0.7.0`/`0.2.7`/`0.5.3`/`0.5.2`). The `descriptors@0.6.0` move is what realigns the asset-hub cast seam (see the cdm-builder cast-seams note up top). The `3.1.6 → 3.1.7` / `0.6.19 → 0.6.20` bump makes CDM contract metadata publish through direct Bulletin `TransactionStorage.store`, matching playground metadata uploads and bypassing product-sdk's stale remaining-quota preflight (`need 1 transactions, have 0`); `cdm-env` remains `2.0.6`. `getBulletinAllowanceSigner` in `src/commands/contract.ts` still casts `client.bulletin` to `CloudStorageApi` (same runtime API, nominal codegen skew — unaffected by the version bump). `cdm-env@2.0.6` flips the `w3s` (Summit) registry address from `""` to `0xa5747e60ae27f93e92019e4021abfc4957050141`, satisfying one Summit-switch precondition (see the Summit checklist). CI's `ci.yml` greps for `['"]@dotdm/` to block re-introduction. When bumping, run the contract tests + `tsc` first. -- **Two local pnpm patches remain — statement-store and sdk-statement.** The patch on `@novasamatech/statement-store@0.8.7` silences its unconditional `console.error` for the expected post-destroy `DestroyedError` in the subscription error callback (real subscription errors still log; still unfixed upstream at 0.8.7). It was re-pinned from `@0.8.6` when the terminal bump (next bullet) moved the host stack to 0.8.7; the patched region in `dist/adapter/rpc.js` is byte-identical between 0.8.6 and 0.8.7, so the same diff applied unchanged (only the version key + the patch filename changed). The patch on `@novasamatech/sdk-statement@0.6.0` fixes a TDZ crash in `getStatements` (`const unsubscribe` referenced from its own callbacks — when the observable settles synchronously at subscribe time, e.g. a poll firing after client destroy, it threw `ReferenceError: Cannot access 'unsubscribe' before initialization` as an uncaughtException; hit on Ctrl+C during an active pairing; that package lives in novasamatech/papi-sdks, not triangle-js-sdks). The former third patch — `@novasamatech/host-papp@0.8.5` (ECDH session-key derivation in `persistAndNotify` + gating the `[sso-v2]` console.info logging) — was deleted when we moved to 0.8.6: both fixes shipped upstream (the ECDH fix, which now also hard-rejects a missing `ssoEncPubKey`, and a default-off `__DEBUG` gate on the logging). Patches are local-only (applied at install into node_modules, never published), version-pinned, marked `PATCH(playground-cli)`, and should be dropped as upstream ships the fixes. -- **`@novasamatech/*` resolves through `@parity/product-sdk-terminal@^0.5.0` (host-papp 0.8.7) — do NOT re-pin to 0.7.x, and do NOT slide back to 0.8.5.** The mobile app's Handshake V2 rewrite accepts ONLY the V2 pairing offer (SCALE discriminant 1); host-papp 0.7.x emits V1 (discriminant 0), which the phone rejects as the generic "Invalid QR code". There is no version-negotiation knob anywhere in the stack — compatibility is purely which host-papp version resolves. host-papp 0.8.6's `HandshakeSuccessV2` is a fixed 258-byte struct requiring `rootEntropySource` (RFC-0007); the matching mobile change merged as Android PR #754 (master `50dfcadb8`, 2026-06-05) and ships in builds v1.0.0-1231+. Phones on builds ≤ 1230 send the 226-byte body, which 0.8.6 cannot decode — pairing against an old phone fails at success-decode, and the remedy is updating the phone app, NOT downgrading host-papp (0.8.5's session key was broken without the local patch we deleted; see the patches bullet). 0.8.6 first moved stored SSO sessions/secrets to new `SsoSessionsV2` / `UserSecretsV2_*` storage keys with NO migration; **host-papp 0.8.7 renamed the session list again `SsoSessionsV2` → `SsoSessionsV3`** and added a required `deviceEncPubKey: Bytes(65)` field on the persisted session, also with no migration and no graceful-degrade (old blobs throw / are ignored at decode), so **every user must re-pair once after the 0.5.0 upgrade** (`playground logout` + `playground login`). `UserSecretsV2_*` is unchanged across 0.8.7. `clearLocalAppStorage()` deletes by `${DAPP_ID}_` prefix, so logout already covers `SsoSessionsV2`, `SsoSessionsV3`, and the orphaned secret blobs. `@parity/product-sdk-terminal@0.3.2` was the historical floor (its codec mirror matched the 0.8.6 `*V2` schemas). We now pin `^0.5.0` (June 2026): 0.5.0 bumps host-papp from `^0.8.6` to `^0.8.7` and updates the internal `createTestSession` codec mirror to the 0.8.7 `SsoSessionsV3` shape; the `./host` subpath we consume and every public signature are unchanged (0.5.0 is additive over 0.4.0's cache-only allowance probe helpers + `AllowanceError` re-export) — the frozen-vector tests (`auth.test.ts`, `signerMode*.test.ts`) pass unchanged across the bump. The statement-store patch is re-pinned to 0.8.7 — the version host-papp 0.8.7 pins exactly; pnpm fails the install loudly (`ERR_PNPM_UNUSED_PATCH`) if resolution drifts off a patched version (designed tripwire, not a bug). Stable 0.8.7 of the whole `@novasamatech/*` family publishes the same day as its prerelease line, so the `minimumReleaseAge` gate would otherwise pin us to the older `0.8.7-N` prerelease — `pnpm-workspace.yaml::minimumReleaseAgeExclude` opts the 0.8.7 family onto the stable line (drop those entries once the version ages past the window). polkadot-app-deploy's own subtree still pins `@parity/product-sdk-terminal@^0.4.0`, but its host-papp now DEDUPES onto our 0.8.7 (its `^0.8.6` range is satisfied by 0.8.7) — so the whole tree shares one host-papp/statement-store 0.8.7 and the patch covers it all; only `product-sdk-terminal` stays split (0.4.0 for pad, 0.5.0 for us). That dedup means pad's SSO-session probe now looks for `dot-cli_SsoSessionsV3.json` (which `playground login` DOES write) — still fully defused because we pass explicit auth/signers into polkadot-app-deploy in every mode (see the "Dev mode must pass EXPLICIT auth options" invariant). +- **The CDM contract packages are now `@parity/cdm-*`, not `@dotdm/*` (migrated June 2026).** The republish renamed `@dotdm/cdm` to `@parity/cdm-codegen` (pinned EXACT `0.6.23`), `@dotdm/contracts` to `@parity/cdm-builder` (pinned EXACT `3.2.0`), and `@dotdm/env` to `@parity/cdm-env` (`^2.1.0`); `@parity/cdm-utils` arrives transitively. `cdm-builder`/`cdm-codegen` stay EXACT because this line has shipped breaking changes in PATCH releases. The `3.1.4` line dropped `resolveTargetRegistryAddress` and `computeTargetHash` and flattened `CdmJson`: the `targets`/`targetHash` layer is gone, so `dependencies` (library to version) and `contracts` (library to contract) are now flat maps plus a single optional top-level `registry` string. As a side effect, cdm.json no longer stores connection URLs (asset-hub/bulletin); those resolve only from CLI opts, the chain preset, or config defaults. The `3.1.5 → 3.1.6` / `0.6.18 → 0.6.19` / `2.0.5 → 2.0.6` bump (June 2026) is a verified-safe patch: `CdmJson`/`CdmJsonContract` are byte-identical (no re-flattening), the cdm-codegen dist is byte-identical, and the only substantive change is that cdm-builder's product-sdk pins moved UP toward our caret roots — now EXACT `contracts@0.7.3`, `tx@0.2.10`, `cloud-storage@0.6.0`, `descriptors@0.6.0` (was `0.7.0`/`0.2.7`/`0.5.3`/`0.5.2`). The `descriptors@0.6.0` move is what realigns the asset-hub cast seam (see the cdm-builder cast-seams note up top). The `3.1.6 → 3.1.7` / `0.6.19 → 0.6.20` bump makes CDM contract metadata publish through direct Bulletin `TransactionStorage.store`, matching playground metadata uploads and bypassing product-sdk's stale remaining-quota preflight (`need 1 transactions, have 0`); `cdm-env` remains `2.0.6`. `getBulletinAllowanceSigner` in `src/commands/contract.ts` still casts `client.bulletin` to `CloudStorageApi` (same runtime API, nominal codegen skew — unaffected by the version bump). The `3.1.7 → 3.2.0` / `0.6.20 → 0.6.23` / `2.0.6 → 2.1.0` bump (July 2026, part of the #465 upgrade) is CdmJson-safe: `CdmJson`/`CdmJsonContract` are byte-identical (no re-flattening), so the `cdm.json` import cast and read/write paths are unchanged. Its only substantive change is that cdm-builder's pinned product-sdk moved UP again — now `contracts@0.9.0`, `descriptors@0.7.0`, `tx@0.3.0`, `cloud-storage@0.7.0` — which pulls `polkadot-api@2.2.1` transitively (aligned to a single instance by bumping our root `polkadot-api` to `^2.2.1`) and realigns the `.tx()` Result shape behind `asCdmRegistryContract` (see the cast-seams note up top). CI's `ci.yml` greps for `['"]@dotdm/` to block re-introduction. When bumping, run the contract tests + `tsc` first. +- **Two local pnpm patches remain — statement-store and sdk-statement.** The patch on `@novasamatech/statement-store@0.8.11` silences its unconditional `console.error` for the expected post-destroy `DestroyedError` / bare `Error("Not connected")` in the subscription error callback (real subscription errors still log; still unfixed upstream at 0.8.11). It has been re-pinned across host-stack bumps (`@0.8.6` → `@0.8.7` → `@0.8.11`); the patched region in `dist/adapter/rpc.js` is byte-identical across that whole range (verified at 0.8.11 by git blob hash `b6574289…`, matching the diff's `index` line), so the same diff applies unchanged — only the version key + patch filename move with each bump. The patch on `@novasamatech/sdk-statement@0.6.0` fixes a TDZ crash in `getStatements` (`const unsubscribe` referenced from its own callbacks — when the observable settles synchronously at subscribe time, e.g. a poll firing after client destroy, it threw `ReferenceError: Cannot access 'unsubscribe' before initialization` as an uncaughtException; hit on Ctrl+C during an active pairing; that package lives in novasamatech/papi-sdks, not triangle-js-sdks). The former third patch — `@novasamatech/host-papp@0.8.5` (ECDH session-key derivation in `persistAndNotify` + gating the `[sso-v2]` console.info logging) — was deleted when we moved to 0.8.6: both fixes shipped upstream (the ECDH fix, which now also hard-rejects a missing `ssoEncPubKey`, and a default-off `__DEBUG` gate on the logging). Patches are local-only (applied at install into node_modules, never published), version-pinned, marked `PATCH(playground-cli)`, and should be dropped as upstream ships the fixes. +- **`@novasamatech/*` resolves through `@parity/product-sdk-terminal@^0.6.2` (host-papp 0.8.11) — do NOT re-pin to 0.7.x, and do NOT slide back to 0.8.5.** The mobile app's Handshake V2 rewrite accepts ONLY the V2 pairing offer (SCALE discriminant 1); host-papp 0.7.x emits V1 (discriminant 0), which the phone rejects as the generic "Invalid QR code". There is no version-negotiation knob anywhere in the stack — compatibility is purely which host-papp version resolves. host-papp 0.8.6's `HandshakeSuccessV2` is a fixed 258-byte struct requiring `rootEntropySource` (RFC-0007); the matching mobile change merged as Android PR #754 (master `50dfcadb8`, 2026-06-05) and ships in builds v1.0.0-1231+. Phones on builds ≤ 1230 send the 226-byte body, which 0.8.6 cannot decode — pairing against an old phone fails at success-decode, and the remedy is updating the phone app, NOT downgrading host-papp (0.8.5's session key was broken without the local patch we deleted; see the patches bullet). 0.8.6 first moved stored SSO sessions/secrets to new `SsoSessionsV2` / `UserSecretsV2_*` storage keys with NO migration; **host-papp 0.8.7 renamed the session list again `SsoSessionsV2` → `SsoSessionsV3`** and added a required `deviceEncPubKey: Bytes(65)` field on the persisted session, also with no migration and no graceful-degrade (old blobs throw / are ignored at decode), so **every user must re-pair once after the 0.5.0 upgrade** (`playground logout` + `playground login`). `UserSecretsV2_*` is unchanged across 0.8.7. `clearLocalAppStorage()` deletes by `${DAPP_ID}_` prefix, so logout already covers `SsoSessionsV2`, `SsoSessionsV3`, and the orphaned secret blobs. `@parity/product-sdk-terminal@0.3.2` was the historical floor (its codec mirror matched the 0.8.6 `*V2` schemas). We pinned `^0.5.0` (June 2026): 0.5.0 bumps host-papp from `^0.8.6` to `^0.8.7` and updates the internal `createTestSession` codec mirror to the 0.8.7 `SsoSessionsV3` shape; the `./host` subpath we consume and every public signature are unchanged (0.5.0 is additive over 0.4.0's cache-only allowance probe helpers + `AllowanceError` re-export) — the frozen-vector tests (`auth.test.ts`, `signerMode*.test.ts`) pass unchanged across the bump. The statement-store patch is re-pinned to whatever version host-papp pins exactly (0.8.7 then, **0.8.11** now); pnpm fails the install loudly (`ERR_PNPM_UNUSED_PATCH`) if resolution drifts off a patched version (designed tripwire, not a bug). Stable 0.8.7 of the whole `@novasamatech/*` family publishes the same day as its prerelease line, so the `minimumReleaseAge` gate would otherwise pin us to the older `0.8.7-N` prerelease — `pnpm-workspace.yaml::minimumReleaseAgeExclude` opts the 0.8.7 family onto the stable line (drop those entries once the version ages past the window). polkadot-app-deploy's own subtree still pins `@parity/product-sdk-terminal@^0.4.0`, but its host-papp now DEDUPES onto our 0.8.7 (its `^0.8.6` range is satisfied by 0.8.7) — so the whole tree shares one host-papp/statement-store 0.8.7 and the patch covers it all; only `product-sdk-terminal` stays split (0.4.0 for pad, 0.5.0 for us). That dedup means pad's SSO-session probe now looks for `dot-cli_SsoSessionsV3.json` (which `playground login` DOES write) — still fully defused because we pass explicit auth/signers into polkadot-app-deploy in every mode (see the "Dev mode must pass EXPLICIT auth options" invariant). **As of July 2026 (#465) we pin `@parity/product-sdk-terminal@^0.6.2`, which moves the whole `@novasamatech/*` family (host-papp / statement-store / storage-adapter / host-api) to 0.8.11** (terminal 0.6.2 requires `host-papp@^0.8.9`, which resolves to the 0.8.11 line); the statement-store patch is re-pinned to `0.8.11` and `minimumReleaseAgeExclude` lists the 0.8.11 family. The truAPI switch (#464) landed only in `@parity/product-sdk-host` (now `0.14.1`, built on `@parity/truapi` instead of the former `@novasamatech/host-api` in-container transport); `product-sdk-terminal`'s SSO/pairing path STILL rides `@novasamatech/host-papp`, so the family stays in the tree until an upstream terminal moves SSO onto truAPI. `deviceEncPubKey` / re-pair-once semantics carry over unchanged from the 0.8.7 note above (0.8.9→0.8.11 are patch-level on the same session schema). - **`@polkadot-api/json-rpc-provider: ^0.2.0` override is load-bearing.** Removing it splits the lockfile across three versions of `json-rpc-provider` (`0.0.1`/`0.0.4`/`0.2.0`) — different PAPI 2.x transitive consumers ask for different versions. Forcing everyone onto `0.2.0` avoids subtle wire-shape divergence and reduces bundle/process memory. -- **`@parity/dotns-cli` (pinned EXACT `0.7.2`) ships a broken publish manifest** declaring `"@polkadot-api/descriptors": "file:.papi/descriptors"` — a workspace path missing from the tarball. pnpm refuses; we redirect that sub-dep to `stubs/papi-descriptors-stub/` (an empty `{}` export) via the version-agnostic `@parity/dotns-cli>@polkadot-api/descriptors` override, so the stub carries across version bumps. dotns-cli's `dist/cli.js` is a fully-bundled Bun build, so the stub is functionally correct. The break persists through every published 0.6.x AND `0.7.2` (verified: the published `0.7.2` manifest still lists the `file:.papi/descriptors` dep — the `prepare` script generates `.papi/descriptors` locally but `files` is `["dist"]`, so the manifest keeps the unresolvable `file:` dep). The CLI consumes dotns-cli ONLY as the bundled `dist/cli.js` binary (auto-run on import under the `dotns` argv, see `src/dotns-cli-dispatch.ts`); the `dotns` CLI verbs are unchanged across 0.6.8→0.7.2 (0.7.x is an internal command-tree refactor plus a new, unused `./core` library + escrow fixes), so the bump is a binary refresh only. Remove the override + stub only once `@parity/dotns-cli` republishes a clean manifest. -- **`polkadot-app-deploy` is pinned to an explicit version (`0.11.0`), not `latest`.** A previous `latest` (0.6.8) had a WebSocket-heartbeat bug that tore chunk uploads down mid-flight. The pin avoids ever silently sliding onto a broken `latest`. When bumping, read release notes for changes to `deploy()`, DotNS methods, or the `DeployOptions` we use (`jsMerkle`, `signer`, `signerAddress`, `storageSigner`, `storageSignerAddress`, `mnemonic`, `rpc`, `attributes`). Newer releases now also export environment helpers (`loadEnvironments`, `resolveEndpoints`, etc.); we don't consume them — our env table lives in `src/config.ts::CONFIGS`. Do NOT downgrade below 0.8.3: 0.7.30-rc/0.8.0 changed storage routing to use the injected `signer` (so phone-mode chunk uploads would phone-sign and die with "message too big"), and 0.8.3 is the first release with the `storageSigner` slot-key escape hatch. The `0.8.3 → 0.9.0` bump (June 2026) was a code/API drop-in: index exports unchanged, `deploy()` signature unchanged, `DeployOptions` only additive, console log strings unchanged (so `src/utils/deploy/progress.ts`'s banner/`[N/M]` parser still matches), `storageSigner` routing precedence unchanged. It aligns polkadot-app-deploy onto `@novasamatech/host-papp@0.8.6` / `@parity/product-sdk-terminal@0.3.x` (collapsing the prior split tree onto our root line), and prefix-matches `dot-cli_SsoSessions*` in its SSO-session probe (still defused because we always pass explicit auth/signer/storageSigner). 0.9.0 also ships a `postinstall` (`patch-package || true`) bundling an `@novasamatech/sdk-statement@0.6.0` patch; we DENY that build script (`allowBuilds: polkadot-app-deploy: false` in `pnpm-workspace.yaml`) because our own pnpm patch on that exact package already applies the fix — pnpm 11 will hard-fail every `pnpm