From f65f5dd17ee1b25aa921bcb52989dd5f4da2e62b Mon Sep 17 00:00:00 2001 From: Reinhard Hatko Date: Thu, 24 Sep 2026 08:04:18 +0800 Subject: [PATCH 1/3] feat(auth): fix QR pairing and sign as the RFC-0022 product account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves to @parity/product-sdk-terminal@^0.10.0 (host-papp 0.10.0), which carries product-sdk #364 and #366 together. Pairing has been impossible since mid-August. host-papp 0.9.0 moved the SSO envelope from P-256/AES-GCM to X25519/ChaCha20-Poly1305, shrinking the device key from 65 to 32 bytes; the handshake codec is fixed-width, so a 65-byte sender is undecodable rather than degraded — iOS said "Something went wrong", Android "invalid QR code". Every terminal up to 0.8.2 pinned host-papp ^0.8.9, whose 0.x ceiling is the last release of the old wire, so no bump below 0.9 could have helped. Do NOT take 0.9.x on the way past: that is #364 without #366, where pairing succeeds and then checkMapping fails permanently, because the phone mints PGAS to the RFC-0022 address while an unmigrated CLI checks the old one — reported in the UI as transient, so it loops forever. RFC-0022 puts the product account at //product//{productId}/{index} with the two //product//{productId} junctions HARD. A public key cannot cross a hard junction, so the parent is now a subtree key the wallet provides, and the derivation is async. `deriveSessionAddresses` becomes async with it. Two things the pre-merge notes got wrong, both caught here: - `ProductAccountRef.derivationIndex` stayed a plain number. The tagged DerivationIndex lives inside @parity/product-sdk-keys, whose `deriveProductAccountPublicKey(subtree, {tag,value})` is a 2-arg form, so the root moves to keys ^0.4.0. On 0.3.x that call is a tsc error; bypass tsc and it derives from a stringified object instead. Worth noting because a vitest-only run does not typecheck — which is how it reached a test run at all. - `publicKey` on the ref is optional; we omit it deliberately so the signer and the address we display resolve through the same cached subtree and cannot desync. Tests pin the derivation at the primitive against host-rust-core's own cross-host vector (wasm_crypto_vectors.rs), not against our output — a fixture regenerated from the code it checks is worthless. They also mock the SDK's subtree fetch: unmocked it hangs waiting for a phone and writes a bogus key into the developer's real ~/.polkadot-apps cache, breaking their live session. `productAccount.test.ts` is deleted: the product id no longer enters that primitive, so it could no longer test the wiring it claimed to, and its assertions are subsumed by the exact vector test. Verified against a real paired session: the built binary derives 0x8755a050e0b47300a7454942dac4f5dafd8222e5 — the address playground-app prints and the owner of the apps deployed today. Users pair again once (playground logout + playground login); anything already published stays owned by the old address. --- .changeset/rfc0022-product-accounts.md | 25 ++ CLAUDE.md | 4 +- package.json | 4 +- ...ovasamatech__statement-store@0.10.0.patch} | 0 pnpm-lock.yaml | 243 ++++++++++++++++-- pnpm-workspace.yaml | 29 ++- src/utils/auth.connect.test.ts | 18 ++ src/utils/auth.test.ts | 71 +++-- src/utils/auth.ts | 20 +- src/utils/productAccount.test.ts | 33 --- src/utils/sessionSigner.test.ts | 128 ++++++--- src/utils/sessionSigner.ts | 50 ++-- src/utils/signer.ts | 3 +- 13 files changed, 482 insertions(+), 146 deletions(-) create mode 100644 .changeset/rfc0022-product-accounts.md rename patches/{@novasamatech__statement-store@0.8.11.patch => @novasamatech__statement-store@0.10.0.patch} (100%) delete mode 100644 src/utils/productAccount.test.ts diff --git a/.changeset/rfc0022-product-accounts.md b/.changeset/rfc0022-product-accounts.md new file mode 100644 index 00000000..4d5a6d72 --- /dev/null +++ b/.changeset/rfc0022-product-accounts.md @@ -0,0 +1,25 @@ +--- +"playground-cli": minor +--- + +Fix QR pairing, and sign as the RFC-0022 product account. + +`playground login` has been unable to pair with current phones since +mid-August: `@novasamatech/host-papp` 0.9.0 moved the pairing envelope from +P-256/AES-GCM to X25519/ChaCha20-Poly1305, shrinking the device key from 65 to +32 bytes. The handshake codec is fixed-width, so there was no graceful +degrade — iOS reported "Something went wrong" and Android "invalid QR code". +Moving to `@parity/product-sdk-terminal@0.10.0` (host-papp 0.10.0) puts the CLI +back on the wire both shipping apps speak. + +The same release changes how the product account is derived. RFC-0022 places it +at `//product//{productId}/{index}`, where the two `//product//{productId}` +junctions are HARD — and a public key cannot cross a hard junction, so the +account is now derived from a subtree key the wallet provides rather than from +the session's root key locally. + +**Your product account address changes, and you must pair again.** Run +`playground logout` and then `playground login`. Apps published from the old +address stay owned by it; allowances and PGAS are re-granted at the next login. +The new derivation is what current phones and the playground web app already +use, so this brings the CLI back into agreement with them rather than away. diff --git a/CLAUDE.md b/CLAUDE.md index 5c5d3b60..64e3d528 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,7 @@ These aren't self-evident from reading the code and have bitten us before. Treat - **`bulletin-deploy` (formerly `@parity/polkadot-app-deploy`) is pinned to an explicit version (`0.18.4` as of 2026-09-17; previously `0.15.0` — see the generations note above for why that was harmful), 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