Repository navigation
feat(auth): fix QR pairing and sign as the RFC-0022 product account (product-sdk-terminal 0.10.0) - #549
Merged
ReinhardHatko merged 3 commits intoSep 28, 2026
Conversation
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.
…ape, expiry path
Four defects found by running the migrated binary end to end against a real
phone. All of them passed the unit suite and a clean build; the first two were
hidden because `pnpm exec tsc` was exiting inside pnpm's deps-status check
(the release-age policy) before tsc ran, so a grep for "error TS" reported a
clean typecheck that never happened. Run tsc directly.
- identityGate.ts read `.productH160` off a Promise, so every gated command
(deploy, deploy-all, mod, decentralize) refused with "Couldn't verify your
builder status" on a healthy chain.
- DripScreen.tsx had the same missed await.
- The SmartContractAllowance destination was a bare `0` instead of the
tagged `{tag:"Index",value:0}`, in two places. The grant still succeeds on
the phone, so it looks fine — but the SDK sub-keys its allowance cache by
destination, and an untagged value takes the `Raw` branch and hexes
`undefined`, so every later read died with "undefined is not an object
(evaluating 'bytes.length')" and login reported "allowances ✕". Latent
before 0.10.0, which started reading that key.
- `wrapSignerWithSssFastFail` only detected the pre-0.10 expiry shape (a
`console.error` line plus a hang). 0.10.0 REJECTS with
`AllowanceExpiredError` instead, so the wrapper stopped producing the one
message that carries the remedy — re-pairing, because a renewal request
would travel over the expired channel. It now handles both, keeps the
original as `cause`, and matches by name as well as `instanceof` (a pnpm
tree can hold two copies of the SDK, where `instanceof` silently fails).
Deploy also stops presenting a retry as progress. The phone-approval counter
counted signature REQUESTS, so a timed-out request re-sent as "step 2" read as
forward progress and contradicted the plan above it, which counts operations.
Observed live: "step 1: Link content" then "step 2: Link content" while the
plan said step 2 was the registry publish, and four prompts arrived for a
two-approval deploy.
Verified against a real session: pairing, login, allowances, mapping, the
identity gate, phone signing, deploy, and re-publish of an app owned by the
product account (which also confirms v2 republish authorization).
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Contributor
E2E Test Pass · ❌ FAILTag:
❌ Failed tests (3)
Sentry traces: view spans for this run |
…ing fix
bulletin-deploy 0.18.4 -> 0.20.0. Verified against paseo-next-v2 with a
dev-signer deploy of a NEW domain (bd-0200-devtest.paseo): build, chunk
upload, DotNS register, playground publish, owner recorded as the dev signer,
and the deployed URL returns HTTP 200. The HTTP check is the part that
matters — a deploy reporting success has never been sufficient evidence that
the CAR was parseable.
0.20.0 fixes a crash we actually hit: `JSON.parse` on incoming RPC data was
unguarded and threw inside the socket handler where nothing upstream could
catch it, killing the process mid-deploy. That is the
`SyntaxError: JSON Parse error: Unexpected EOF` at `_onMessage` seen during a
template deploy. 0.19.x additionally drops the storage-deposit floor for
registering a name from a hardcoded 200 PAS to 5 PAS, stops registers
reverting when the chain moves between dry-run and inclusion, and uploads
duplicate bundle files once instead of twice.
Checked per the invariant before pinning: `deploy()`'s signature is unchanged,
every `DeployOptions` field we pass still exists, the phase banners and `[N/M]`
chunk lines the progress parser matches are intact, and the `patch-package`
postinstall is still shipped (so the `allowBuilds` denial stays).
CLAUDE.md gains two notes from this session:
- `pnpm <script>` can exit in its deps-status check BEFORE running the tool,
which looks like a pass if you grep the output for "error TS". That is how
four type errors reached a live phone test. Check the exit code or run the
binary directly.
- Mapping is not required to CALL a contract, and `checkMapping` returning
false is normal for a plain Substrate signer. Every AccountId32 has a
deterministic H160; `Revive.OriginalAccount` is only the reverse map.
Contract fees can be paid in PAS, so neither PGAS nor mapping should be
blamed for a rejected extrinsic without evidence — both were, wrongly.
The sentry scripts looked up keychain item `sentry-api-token`, which does not
exist; the real one is `sentry-api-token-playground`. All three would have
failed for anyone following the documented backup-before-change workflow.
Contributor
|
Dev build ready — try this branch: |
ReinhardHatko
merged commit Sep 28, 2026
7ef58a6
into
rh/v2-contracts-and-gate
14 of 19 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #545 (which is stacked on #540) — review/merge those first.
Fixes QR pairing, which has been impossible on every shipping phone since mid-August, and migrates the CLI to the RFC-0022 product account. Both come from
@parity/product-sdk-terminal@0.10.0, released 2026-09-23.Why pairing was broken
@novasamatech/host-papp@0.9.0moved 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 SCALE, so a 65-byte sender is undecodable rather than degraded — iOS showed "Something went wrong", Android the generic "invalid QR code". Every published terminal up to 0.8.2 pinnedhost-papp ^0.8.9, whose 0.x caret ceiling is the last release of the old wire, so no bump below 0.9 could have helped.checkMappingfails permanently because the phone mints PGAS to the RFC-0022 address while an unmigrated CLI checks the old one — and the UI reports it as transient, so it loops forever. 0.10.0 carries both.What RFC-0022 changes
A product account now lives at
//product//{productId}/{index}where the two//product//{productId}junctions are HARD. A public key cannot cross a hard junction, so the parent is a subtree key the wallet provides (session.getProductSubtree, consent-free, SDK-cached, reaching the phone only on a cold cache).deriveProductPublicKeyandderiveSessionAddressesbecome async.Two pre-merge predictions turned out wrong and are worth knowing:
ProductAccountRef.derivationIndexstayed a plain number. The taggedDerivationIndexlives inside@parity/product-sdk-keys, whosederiveProductAccountPublicKey(subtree, {tag,value})is a 2-arg form — so the root moves tokeys@^0.4.0.publicKeyon the ref is optional, and we omit it deliberately so the signer and the displayed address resolve through the same cached subtree and cannot desync.Four defects caught by running it, not by CI
All four passed the unit suite and a clean build. The first two hid behind a typecheck that never ran:
pnpm exec tscwas exiting inside pnpm's deps-status check (the release-age policy) before tsc started, so grepping its output forerror TSreported success.identityGate.ts.productH160off a Promise — every gated command refused with "Couldn't verify your builder status" on a healthy chainDripScreen.tsxresources.ts,smartContracts.tsSmartContractAllowancedestination was a bare0, not{tag:"Index",value:0}sessionSigner.tsThe allowance one is the subtle one: the grant still succeeds on the phone, so it looks healthy — but the SDK sub-keys its cache by destination, and an untagged value takes the
Rawbranch and hexesundefined. Every later read then died withundefined is not an object (evaluating 'bytes.length'), andplayground loginreportedallowances ✕on a perfectly good account. Latent before 0.10.0, which started reading that key.The last one is a regression the bump itself introduced: 0.10.0 rejects with
AllowanceExpiredErrorwhere 0.8.x logged and hung, so our wrapper stopped producing the message that carries the only remedy (re-pair — a renewal request would travel over the expired channel). It now handles both shapes, keeps the original ascause, and matches by name as well asinstanceof, since a pnpm tree can hold two copies of the SDK.Also: retries no longer look like progress
The phone-approval counter counted signature requests, so a timed-out request re-sent as "step 2" read as advancing — and contradicted the "Phone approvals expected" plan, which counts operations. Seen live:
step 1: Link contentthenstep 2: Link contentwhile the plan said step 2 was the registry publish, and four prompts arrived for a two-approval deploy.Verification
vitest1034 passed ·tsc --noEmit0 (run directly — see above) ·biome formatclean ·lint:licenseclean.Live, against a real phone on paseo-next-v2:
0x8755a050…), so nothing already published changed handsplayground-template.paseo✅ — new metadata CID, owner still the product account, which also confirms v2 republish authorizationUpgrade note
Users pair once more (
playground logout+playground login). Anything already published stays owned by the old address. The new derivation is what current phones and the playground web app already use, so this brings the CLI back into agreement with them.Follow-ups (tracked, not in this PR)
deploy-allis the one command not yet exercised on this stack — it's the only one with a signing gate serializing on-chain phases, which the async change touches.