Skip to content

feat(widget): bind chainId into bridge sign-in (multi-signer) — D-027-v3 §4 - #13

Merged
gitchadd merged 4 commits into
mainfrom
feat/multisig-signin
Jun 5, 2026
Merged

gitchadd merged 4 commits into
mainfrom
feat/multisig-signin

Conversation

@gitchadd

@gitchadd gitchadd commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

What

Binds chainId into the support bridge sign-in, the widget half of the D-027-v3 multi-signer (EOA + ERC-1271 + ERC-6492) upgrade.

The bridge is changing its sign-in message format to bind chainId:

${purpose}:${address.toLowerCase()}:${chainId}:${timestamp}

where purpose is support.p2p.me:sign-in, and the POST /auth/sign-in body gains a chainId field. ERC-1271 verification is chain-bound (it reads isValidSignature on the wallet contract at a specific chain), so the chainId must be bound into the signed string, not sent as a sidecar, or a Base Sepolia signature replays against the Base verifier. The widget signs and posts that payload, so it MUST match the bridge byte-for-byte or every sign-in 401s.

Source of truth: support/SYMMETRIC_CHATWOOT_DESIGN.md §4.3–4.4.

Changes

  • SupportSigner gains optional chainId (the wallet's chain).
  • signInWithBridge binds chainId into both the signed message and the POST body, sourced from signer.chainId, defaulting to 84532 (Base Sepolia) to match the widget's existing chain default across contracts.ts / useOrderStates / the order machines. New exported buildSignMessage helper mirrors the bridge's, with the address lowercased.
  • Privy and Thirdweb adapters carry chainId through to the SupportSigner.
  • Tests: new message-format assertion (...:8453:<ts>), body.chainId assertion, message-vs-body consistency check, default-to-84532 case, and adapter passthrough. README sign-in step updated to the new format.

All customer sign-in callers (Support, ContactSupport, PaymentHistoryWithSupport) route through the single signInWithBridge chokepoint, so binding chainId there covers every path. (The ops sign-in path / opsSessionCache.ts is not on main yet — it lives on feat/support-ops-mode — so it is out of scope here and will pick up the same change when that branch lands.)

⚠️ BREAKING — lockstep release

This is a breaking, lockstep change with the companion bridge multi-signer PR. The widget and bridge must merge and deploy together or every sign-in fails on a message mismatch (the bridge recomputes a different message and returns 401). Rollback = revert both.

The bridge side also moves an eth_call onto the sign-in hot path (1271/6492 verification), which intersects the open support bridge rpc fix item (move off public RPC onto Alchemy).

Verification

npm run verify green:

  • tsc --noEmit (typecheck) clean
  • tsc --noEmit --project tsconfig.examples.json (examples typecheck) clean
  • node:test 86 pass / 0 fail
  • vitest 60 pass / 0 fail
  • tsup build (ESM + CJS + DTS) success

gitchadd added 2 commits June 1, 2026 18:15
…-v3 §4

The bridge is upgrading sign-in to multi-signer (EOA + ERC-1271 + ERC-6492).
ERC-1271 verification is chain-bound, so the signed message must carry the
chainId or a signature from one chain can be replayed against a verifier on
another. Per D-027-v3 §4 the message format becomes:

  ${purpose}:${address.toLowerCase()}:${chainId}:${timestamp}

and the POST /auth/sign-in body gains a chainId field. This widget half must
match the bridge byte-for-byte or every sign-in 401s.

- SupportSigner gains optional chainId (the wallet's chain).
- signInWithBridge binds chainId into both the signed message and the body,
  sourced from signer.chainId, defaulting to 84532 (Base Sepolia) to match
  the widget's existing chain default. New buildSignMessage helper mirrors
  the bridge's.
- Privy + Thirdweb adapters carry chainId through.
- Tests assert the new message format, the body.chainId field, the default,
  and adapter passthrough. README sign-in step updated.

BREAKING, lockstep with the companion bridge multi-signer PR: they must
merge and deploy together or sign-in fails on a message mismatch.
…t — D-027-v3 §4

Adversarial-review blockers on the multi-signer sign-in (hard cutover):

- Resolve chainId from the LIVE connector at sign time via a
  SupportSigner.getChainId resolver, not a cached optional prop. Privy
  parses its CAIP-2 chainId string ("eip155:8453" -> 8453) inside the
  adapter; Thirdweb reads the active chain id (getChain().id) inside the
  adapter. Both read live on every call.
- Remove the DEFAULT_SUPPORT_CHAIN_ID (84532) fallback. chainId is now
  MANDATORY on the support sign-in path: if it cannot be resolved to a
  finite number, signInWithBridge throws "support sign-in requires a
  chainId from the connected wallet" before signing or POSTing.
- The signed message and the POST body both carry the resolved chainId
  (number), matching the bridge's 4-field format
  ${purpose}:${address.toLowerCase()}:${chainId}:${timestamp}.

Tests: assert the POST body chainId equals the connector's chain id;
assert a missing chainId throws (not defaults); symmetric Thirdweb
adapter coverage mirroring Privy; cross-repo GOLDEN-VECTOR assertion of
the exact expected sign-in message string.
@gitchadd

gitchadd commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed bf8d94e addressing the adversarial review (hard cutover, lockstep with bridge #20):

  • chainId is now resolved from the live connector at sign time (Privy CAIP-2 eip155:<n> parsed in-adapter; Thirdweb active chain.id), not a cached prop.
  • Removed the 84532 default; chainId is mandatory on the support path and throws a clear error if unresolvable — no silent wrong-chain signing.
  • Golden-vector test asserts the produced message is byte-identical to the bridge contract; added symmetric Thirdweb adapter coverage.

build + 86 node + 69 vitest green. Merge/deploy together with bridge #20.

…ard-cutover

Re-review fixes on the multi-signer support sign-in path (D-027-v3 §4):

- README §What happens on click: drop the stale 'chainId comes from
  signer.chainId defaulting to 84532' text. chainId is resolved live from
  the connected wallet via signer.getChainId(), is mandatory, is bound into
  the signed message, and sign-in throws before any signature if it cannot
  be resolved (no Base Sepolia default).
- bridge/privy/thirdweb chainId guards: Number.isFinite -> Number.isInteger
  so a non-integer (e.g. 8453.5) throws instead of being signed/POSTed.
- parsePrivyChainId: reject empty/whitespace tails ('eip155:' and '' both
  yield Number === 0 under a finite check); require a positive integer.
- adapters.test.ts: add 'eip155:' and '' cases asserting getChainId rejects.
@gitchadd

gitchadd commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator Author

Pushed 659e27c (fresh re-review fixes):

  • (MED, doc) README 'What happens on click' rewritten to the shipped hard cutover — chainId resolved live via signer.getChainId(), mandatory, throws before signing if unresolvable, no 84532 default.
  • (LOW) chainId guards tightened to Number.isInteger (rejects e.g. eip155:8453.5); Privy CAIP-2 parser now rejects empty/0/non-positive instead of silently yielding 0.

build + 71 tests green. Lockstep with bridge #20.

)

The separator before "## Fraud screening (B2B)" was `---cla` (a botched
horizontal rule). Replace with a clean `---`. The mixed-case cross-repo golden
vector KeccaK also flagged is already present in test/bridge.test.ts (added in
the re-review, byte-identical to the bridge suite), so no test change is needed.
@gitchadd
gitchadd marked this pull request as ready for review June 5, 2026 08:04
@gitchadd
gitchadd merged commit 406b44e into main Jun 5, 2026
2 checks passed
Software-Artist-Aash added a commit that referenced this pull request Jun 7, 2026
…kout gate)

#15 bumped package.json to 1.2.1 but didn't update CHANGELOG. Backfill the
1.2.0 entry (offramp v2 #11 + multi-signer sign-in #13, already published)
and document 1.2.1 as: support-widget fix (#15) + screening rejection copy
+ Checkout one-in-flight-order gate. Version stays 1.2.1 (unpublished).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gitchadd added a commit that referenced this pull request Aug 21, 2026
)

The separator before "## Fraud screening (B2B)" was `---cla` (a botched
horizontal rule). Replace with a clean `---`. The mixed-case cross-repo golden
vector KeccaK also flagged is already present in test/bridge.test.ts (added in
the re-review, byte-identical to the bridge suite), so no test change is needed.
vvictor-dev pushed a commit to vvictor-dev/widgets that referenced this pull request Aug 22, 2026
* feat(widget): bind chainId into bridge sign-in (multi-signer) — D-027-v3 §4

The bridge is upgrading sign-in to multi-signer (EOA + ERC-1271 + ERC-6492).
ERC-1271 verification is chain-bound, so the signed message must carry the
chainId or a signature from one chain can be replayed against a verifier on
another. Per D-027-v3 §4 the message format becomes:

  ${purpose}:${address.toLowerCase()}:${chainId}:${timestamp}

and the POST /auth/sign-in body gains a chainId field. This widget half must
match the bridge byte-for-byte or every sign-in 401s.

- SupportSigner gains optional chainId (the wallet's chain).
- signInWithBridge binds chainId into both the signed message and the body,
  sourced from signer.chainId, defaulting to 84532 (Base Sepolia) to match
  the widget's existing chain default. New buildSignMessage helper mirrors
  the bridge's.
- Privy + Thirdweb adapters carry chainId through.
- Tests assert the new message format, the body.chainId field, the default,
  and adapter passthrough. README sign-in step updated.

BREAKING, lockstep with the companion bridge multi-signer PR: they must
merge and deploy together or sign-in fails on a message mismatch.

* fix(support): resolve chainId live from connector, drop silent default — D-027-v3 §4

Adversarial-review blockers on the multi-signer sign-in (hard cutover):

- Resolve chainId from the LIVE connector at sign time via a
  SupportSigner.getChainId resolver, not a cached optional prop. Privy
  parses its CAIP-2 chainId string ("eip155:8453" -> 8453) inside the
  adapter; Thirdweb reads the active chain id (getChain().id) inside the
  adapter. Both read live on every call.
- Remove the DEFAULT_SUPPORT_CHAIN_ID (84532) fallback. chainId is now
  MANDATORY on the support sign-in path: if it cannot be resolved to a
  finite number, signInWithBridge throws "support sign-in requires a
  chainId from the connected wallet" before signing or POSTing.
- The signed message and the POST body both carry the resolved chainId
  (number), matching the bridge's 4-field format
  ${purpose}:${address.toLowerCase()}:${chainId}:${timestamp}.

Tests: assert the POST body chainId equals the connector's chain id;
assert a missing chainId throws (not defaults); symmetric Thirdweb
adapter coverage mirroring Privy; cross-repo GOLDEN-VECTOR assertion of
the exact expected sign-in message string.

* fix(support): integer chainId guards + reject empty CAIP-2 + README hard-cutover

Re-review fixes on the multi-signer support sign-in path (D-027-v3 §4):

- README §What happens on click: drop the stale 'chainId comes from
  signer.chainId defaulting to 84532' text. chainId is resolved live from
  the connected wallet via signer.getChainId(), is mandatory, is bound into
  the signed message, and sign-in throws before any signature if it cannot
  be resolved (no Base Sepolia default).
- bridge/privy/thirdweb chainId guards: Number.isFinite -> Number.isInteger
  so a non-integer (e.g. 8453.5) throws instead of being signed/POSTed.
- parsePrivyChainId: reject empty/whitespace tails ('eip155:' and '' both
  yield Number === 0 under a finite check); require a positive integer.
- adapters.test.ts: add 'eip155:' and '' cases asserting getChainId rejects.

* docs(widgets): fix malformed `---cla` section rule in README (KeccaK p2pdotme#13)

The separator before "## Fraud screening (B2B)" was `---cla` (a botched
horizontal rule). Replace with a clean `---`. The mixed-case cross-repo golden
vector KeccaK also flagged is already present in test/bridge.test.ts (added in
the re-review, byte-identical to the bridge suite), so no test change is needed.

---------

Co-authored-by: gitchadd <gitchad@icloud.com>
vvictor-dev pushed a commit to vvictor-dev/widgets that referenced this pull request Aug 22, 2026
…kout gate)

p2pdotme#15 bumped package.json to 1.2.1 but didn't update CHANGELOG. Backfill the
1.2.0 entry (offramp v2 p2pdotme#11 + multi-signer sign-in p2pdotme#13, already published)
and document 1.2.1 as: support-widget fix (p2pdotme#15) + screening rejection copy
+ Checkout one-in-flight-order gate. Version stays 1.2.1 (unpublished).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant