Skip to content

feat: multi-device SSO V2 + chat V2#178

Merged
johnthecat merged 12 commits into
release/0.8from
feat/multi-device-sdk-v2-rebased
May 22, 2026
Merged

feat: multi-device SSO V2 + chat V2#178
johnthecat merged 12 commits into
release/0.8from
feat/multi-device-sdk-v2-rebased

Conversation

@kalininilya

@kalininilya kalininilya commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Brings multi-device SSO + V2 chat content to host-papp and host-chat. The public auth surface stays V1-shaped: createAuth / pappAdapter.sso keeps the pairingStatus / authenticate() / abortAuthentication() triad, authenticate() still resolves to a StoredUserSession | null, and pairingStatus.finished still carries session. What's new is the protocol underneath — pairing now runs the V2 multi-device handshake.

For a 0.7.8 consumer, the migration is two field renames (metadata URL → inline hostMetadata, osType/osVersionplatformType/platformVersion). Everything else is a drop-in upgrade.

Spec: https://hackmd.io/@1JCaGppGSUqHtJilikYaKw/Ski9naYdWe

Desktop counterpart: paritytech/polkadot-desktop#489.

host-papp — the public surface

const adapter = createPappAdapter({
  appId,
  hostMetadata,                              // { hostName?, hostVersion?, hostIcon?, platformType?, platformVersion?, custom? }
  // optional overrides — most hosts skip these:
  // deviceIdentity: () => deviceIdentityService.loadOrCreate(),
  // onAuthSuccess:   async ({ session, identityChatPrivateKey }) => { /* extras */ },
});

const result = await adapter.sso.authenticate();   // ResultAsync<StoredUserSession | null, Error>
// or: subscribe to adapter.sso.pairingStatus and call abortAuthentication() to cancel

What the SDK now owns (was previously consumer-side or part of the original PR)

  • Device identity persistence. The SDK creates and persists a fresh DeviceIdentityForPairing to the configured StorageAdapter on first run and reuses it on subsequent launches. Hosts that want a different backend (Electron Keychain, native secure storage) plug it in with an optional deviceIdentity: () => Promise<DeviceIdentityForPairing> factory.
  • Pairing-topic statement dedupe. Stale Success statements on chain no longer get replayed across launches — the SDK persists the dedupe hex via the same StorageAdapter. No consumer plumbing.
  • Internal persistence on Success. createAuth builds a V2-shaped StoredUserSession and writes it + per-session secrets to ssoSessionRepository and userSecretRepository. authenticate() resolves with the StoredUserSession; pairingStatus.finished.session carries the same object.
  • Peer device statement account capture. Lifted off statement.proof.value.signer during pairing and exposed via session.remoteAccount.accountId (no follow-up chain query needed for device-sync seeding).

Consumer-facing changes vs 0.7.8

0.7.8 0.8.0
metadata: string (URL) dropped — host name/icon/platform inline via hostMetadata
hostMetadata: { hostVersion, osType, osVersion } reshape — { hostName?, hostVersion?, hostIcon?, platformType?, platformVersion?, custom? }
pairingStatus: { step, … } unchanged (same step set, same shape)
authenticate(): ResultAsync<StoredUserSession | null, Error> unchanged signature; the resolved StoredUserSession gains optional V2 fields (identityAccountId, identityChatPublicKey)
abortAuthentication() unchanged
internal V1 sessions wiped on first read via safe-decode-or-discard (V1 codec ≠ V2 codec)

Public exports

createPappAdapter / PappAdapter, AuthComponent / HostMetadata / OnAuthSuccess / PairingStatus / DeviceIdentityForPairing, UserSession / StoredUserSession / Identity, signing request/response types, RingVrf*, SS_*_ENDPOINTS.

V2 internals — startPairingV2, codecs (Handshake*, *HandshakeResponse*, VersionedHandshake*, MetadataEntry, MetadataKey, Device), the state-machine helpers, topic / envelope / proposal helpers, deriveIdentityChatPublicKey, the Handshake*State types — are all internal to the package.

host-papp — what changed under the hood

  • V2 SSO inside createAuthstartPairingV2 is now called only from inside createAuth. The caller (or the SDK's default) provides a persistent DeviceIdentityForPairing; the SDK runs the full pairing dance (codec, ECDH envelope, polling fallback, state machine, peer-signer capture) and resolves authenticate() with the persisted StoredUserSession.
  • HandshakeSuccessV2 wire — spec v0.2.1 161-byte shape: identityAccountId(32) || rootAccountId(32) || identityChatPrivateKey(32) || deviceEncPubKey(65). The 129-byte v0.2 variant emitted by Android feature/location-for-handshake is still accepted (with rootAccountId === null).
  • Multi-device authorisation lives in the user-identity-signed roster events (DeviceAdded / DeviceRemoved); the success body has no per-device identitySignature.
  • EncryptedHandshakeResponseV2 uses native scale-ts Enum on the inner discriminant. The peer SCALE library does NOT elide the inner index, so Pending(AllowanceAllocation) arrives as 0x00 0x00.
  • HostMetadata reshape to mirror the V2 proposal — name / icon / platform now ride inside the QR proposal instead of being fetched from the V1 metadata: string URL (which is dropped).
  • Schemas evolved + safe-decode-or-wipe. StoredUserSession gains optional V2 fields (identityAccountId, identityChatPublicKey). UserSecretRepository gains identityChatPrivateKey. Old V1 blobs decode short and are silently wiped on first read.
  • identity/rpcAdapter tolerates camel/snake-case Resources.Consumers fields (fullUsername/full_username, lastUpdate/last_update, accountId/account_id) — the V2 multi-device runtime metadata emits camelCase.
  • V1 handshake codec deletedsrc/sso/auth/scale/handshake.ts is gone, along with the V1 ephemeral-keypair derivation in createAuth.

host-chat

  • New MessageContent variants at the spec'd indices 14–20:
    • chatAccepted (14) — legacy single-device accept now carries { messageId } for iOS V1 backward decode (was _void).
    • _reserved16 (16), _reserved19 (19) — placeholders so subsequent indices land at the spec values.
    • deviceAdded (17) { statementAccountId, encryptionPublicKey }
    • deviceRemoved (18) { statementAccountId }
    • deviceChatAccepted (20) { requestId, device: DeviceInfo }. Sent on the identity-level session SessionId(B, A) encrypted with K(A,B) so all of A's devices can decrypt without a per-device envelope.
  • DeviceAdded / DeviceRemoved use length-prefixed Bytes() rather than fixed-size codecs, because substrate-sdk-android falls through to length-prefixed Vec<u8> for the AccountId / EncodedPublicKey wrapper types (no @FixedLength).
  • accountService.getConsumerInfo gets the same camel/snake-case tolerance as host-papp.

Backwards compatibility (vs. 0.7.8)

  • V1 SSO is not supported. Paired Polkadot Mobile clients still on the V1 wire format will not pair against this build.
  • V1 chat MessageContent variants 0–13 are preserved on the receive path, but chatAccepted (14) payload shape changed and older clients emitting _void will not decode.
  • The transitional 129-byte V2 success body (Android feature/location-for-handshake) is still accepted.
  • Persisted V1 SSO sessions and per-session secrets are wiped on first read (codec schema changed). Users will need to re-pair against a multi-device PApp build.

Test plan

  • npm run build
  • npm run typecheck
  • npm run lint
  • npm test — 524/524 (52 test files); auth.spec.ts covers V2-driven createAuth end-to-end (success path, internal persistence, onAuthSuccess hook, default deviceIdentity fallback, Failed inner response, abort, debug emits)
  • Downstream consumer (polkadot-desktop, branch feat/multi-device-messaging-and-sync-rebased) typechecks, lints, builds, and unit tests pass against the linked SDK
  • CI green
  • Manual: pair against a multi-device PApp build through Onboarding

Release 0.8.0

(See CHANGELOG.md on this branch for the full entry. The summary below reflects the diff vs. 0.7.8, the last released version.)

What's new

  • host-papp: multi-device SSO behind the existing createAuth / pappAdapter.sso surface. V2 wire format runs inside the SDK. authenticate() still resolves to StoredUserSession | null; pairingStatus.finished still carries session.
  • host-papp: SDK persists the V2 device identity by default (overridable via deviceIdentity factory).
  • host-papp: SDK persists pairing-topic statement dedupe internally; no consumer wiring.
  • host-papp: StoredUserSession exposes the peer device statement account (via remoteAccount.accountId) and the user identity / chat keys (via identityAccountId / identityChatPublicKey).
  • host-papp: optional onAuthSuccess hook for consumer post-pairing work (telemetry, custom peer caches).
  • host-chat: new MessageContent variants at indices 14, 17, 18, 20 for multi-device chat.

Breaking (two field renames; nothing else)

  • createPappAdapter drops metadata: string; host name / icon / platform now in hostMetadata.
  • HostMetadata: osType → platformType, osVersion → platformVersion; add hostName? / hostIcon? / custom?.
  • V1 SSO handshake removed; V1-only paired clients won't pair. Persisted V1 SSO sessions are wiped on first read.
  • MessageContent.chatAccepted (14) payload changed from _void to { messageId: string }.

…ng service

Wire-compatible with the V2 SSO protocol:
  VersionedHandshakeProposal::V2 — emitted by the host via QR carrying
    Device { statementAccountId, encryptionPublicKey } and metadata
    (HostName / HostVersion / HostIcon / PlatformType / PlatformVersion / Custom)
  VersionedHandshakeResponse — answer over Statement Store, ECDH-encrypted
    body; inner payload is `EncryptedHandshakeResponseV2 = Pending | Success
    | Failed`. Success carries identity_chat_pubkey + identity_sr25519_pubkey
    + 64-byte sr25519 identity_signature.

Pieces:
  - scale/handshakeV2.ts — SCALE codecs. V2 is at discriminant 1 (with a
    `_v1Reserved: _void` slot to push it past the legacy V1 index 0).
    EncryptedHandshakeResponseV2 is a length-dispatched custom codec
    (1 byte / 161 bytes / variable str) since the peer's SCALE library
    elides the outer enum index for class-wrapped sealed-interface variants.
  - v2/topic.ts — pairing topic + channel derivation:
      khash(statementAccountId, encryptionPublicKey || "topic"|"channel")
  - v2/proposal.ts — encode + build `polkadotapp://pair?handshake=<hex>`.
  - v2/envelope.ts — ECDH (P-256) + AES-GCM via @novasamatech/statement-store
    createEncryption.
  - v2/state.ts — Idle -> Submitted -> Pending(AllowanceAllocation) ->
    Success | Failed; forward-only transitions with same-tag idempotence.
  - v2/service.ts — orchestrator: subscribes + polls the pairing topic,
    SCALE-decodes statements, drives the state machine. Exposes optional
    initialProcessedDataHex + onStatementProcessed so callers can
    persist byte-level dedupe across reloads (e.g. for proper logout).

Adds rxjs and @polkadot-api/utils to host-papp deps. 60 new tests.
Adds a `## V2 SSO handshake` section covering:
  - protocol overview vs V1 (incompat)
  - flow diagram (host ↔ peer)
  - building the proposal QR via `buildPairingDeeplink`
  - driving the handshake via `startPairingV2`, the state machine, and abort
  - surviving reloads + proper logout via `initialProcessedDataHex` /
    `onStatementProcessed` byte-level dedupe
  - topic/channel derivation helpers
  - SCALE codec exports table (proposal, response, success, signature payload)

The existing "Authentication and pairing" section is now flagged as the V1
flow and links to the V2 section, so callers can pick the right path.
Per the multi-device chat spec (HackMD Ski9naYdWe), PApp shares the user
identity chat keypair with each paired device so the device can decrypt
incoming chat traffic addressed to the user identity. Today's
HandshakeSuccessV2 wire payload only carries the public half; this commit
extends it with a 32-byte raw P-256 private scalar (identityChatPrivateKey)
appended after identitySignature.

Wire impact: Success payload grows from 161 to 193 bytes. The length-
dispatched EncryptedHandshakeResponseV2 codec recognises the new length
as Success; older Pending (1 byte) and Failed (variable string) variants
keep the same shape. Encryption is unchanged - the outer envelope's
ECDH-AES wrap already protects the payload in transit.

Mirrored on the responder side (PApp) per the same spec; consumers
(paired devices) persist the priv only in OS-keychain-backed secure
storage and never forward it.
Initial pass forced HandshakeSuccessV2 to 193 bytes (with the new chat
priv field), which broke pairing against PApp builds that haven't yet
shipped the multi-device extension - the codec saw 161-byte legacy
Success payloads and fell through to the variable-length Failed branch.

Now length-dispatched: 161 bytes decodes as legacy Success with
identityChatPrivateKey = undefined, 193 bytes decodes as the new
extended Success. Encode emits the 193-byte form when a priv key is
provided, otherwise falls back to the legacy struct.

HandshakeSuccessState.identityChatPrivateKey becomes Uint8Array | undefined
so consumers can branch on availability. Send-only V2 paths continue to
work without it; inbound chat-request decryption is gated on the field
being present.

Removes the wire-incompatibility introduced in the previous commit
without rolling back the spec-aligned shape; once every PApp build
ships the priv key the legacy branch can be retired.
…vate_key

The V2 multi-device handshake spec now ships only the user identity
chat P-256 private scalar (32 bytes) on the wire; the matching public
key is derived locally via P-256 scalar multiplication. Wire format
shrinks from 193 to 128 bytes (accountId || identityChatPrivateKey ||
identitySignature). Legacy 161-byte payloads (encryptionKey || accountId
|| signature) are still accepted for PApp builds without the multi-device
extension. identitySignature now commits to (accountId ||
derive_pub(identityChatPrivateKey)).
Align the V2 SSO handshake codec with the multi-device spec
(https://hackmd.io/@1JCaGppGSUqHtJilikYaKw/Ski9naYdWe).

Success body shape:
  identityAccountId(32) || rootAccountId(32) || identityChatPrivateKey(32) ||
  deviceEncPubKey(65) = 161 bytes (spec v0.2.1)

Also accept the 129-byte v0.2 variant (no rootAccountId) emitted by
Android's `feature/location-for-handshake`. Surface `rootAccountId: null`
in that case — chat does not need it; product-account derivation
degrades gracefully.

Removed:
  - `identitySignature` field (multi-device authorisation moves to the
    user-identity-signed roster events DeviceAdded/DeviceRemoved)
  - `IDENTITY_SIGNATURE_PAYLOAD_BYTES` export
  - `HandshakeSuccessV2WithChatPriv` (was the experimental 128-byte shape)

Added:
  - `HandshakeSuccessV2Value` exported type
  - `decodeEncryptedHandshakeResponseV2` — explicit length-dispatched
    decoder for the inner plaintext
  - `deriveIdentityChatPublicKey` — P-256 scalar mult helper
  - `EncryptedHandshakeResponseV2` now built with native scale-ts `Enum`
    on the inner discriminant. The peer SCALE library does NOT elide the
    variant index, so `Pending(AllowanceAllocation)` arrives as `0x00 0x00`
    and was being misclassified as `Failed("")` by the prior length-only
    dispatch.

`HandshakeSuccessState` now exposes identityAccountId, rootAccountId,
identityChatPrivateKey, identityChatPublicKey (derived locally) and
deviceEncPubKey. Pairing service decodes via the new
`decodeEncryptedHandshakeResponseV2` and logs failure reasons with the
raw inner bytes for diagnosis.
V2 multi-device runtime metadata exposes Resources.Consumers fields in
camelCase, which crashed `raw.stmt_store_slots.map(...)` in the host-papp
identity adapter. Read each field with snake/camel fallback and treat
slots as optional. Same defensiveness applied to host-chat's
getConsumerInfo. The .papi descriptor types only model snake_case, so
widen via `Record<string, unknown>` at the read site.
Adopt the multi-device chat content shape per the chat spec v0.1
(https://hackmd.io/@1JCaGppGSUqHtJilikYaKw/Ski9naYdWe).

New MessageContent variants:
  - chatAccepted (14) — legacy single-device accept payload changed from
    `_void` to `ChatAcceptedContent { messageId: String }` for iOS V1
    backward decode.
  - _reserved16 (16) — reserved (Android `coinagePayment`, unused on desktop)
  - deviceAdded (17) — `{ statementAccountId, encryptionPublicKey }`
  - deviceRemoved (18) — `{ statementAccountId }`
  - _reserved19 (19) — placeholder so deviceChatAccepted lands at the
    spec'd index 20.
  - deviceChatAccepted (20) — `{ requestId, device: DeviceInfo }`. Sent
    via identity-level session SessionId(B, A) encrypted with K(A,B);
    identity-level encryption lets all of A's devices decrypt without a
    per-device envelope.

DeviceAdded/Removed use length-prefixed `Bytes()` instead of fixed-size
codecs because substrate-sdk-android emits the `AccountId` /
`EncodedPublicKey` wrapper types without `@FixedLength`, falling through
to length-prefixed `Vec<u8>` on the wire. DeviceInfoContent (used by
deviceChatAccepted) stays fixed-size — Android's `DeviceInfoScale`
declares `@FixedLength` explicitly.
@socket-security

socket-security Bot commented May 20, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedverifiablejs@​1.2.075100979070

View full report

`npm ci` in CI requires package-lock.json to be in sync with
package.json. The V2 SSO commits added @polkadot-api/utils, rxjs, and
verifiablejs to host-papp but the lockfile wasn't refreshed when the
branch was first pushed.
@kalininilya
kalininilya marked this pull request as ready for review May 20, 2026 23:10
@kalininilya kalininilya changed the title feat: multi-device SSO V2 + chat content variants (rebased) feat: multi-device SSO V2 + chat V2 May 20, 2026
`createAuth` / `pappAdapter.sso` is now V2-driven end-to-end with the same
`pairingStatus` / `authenticate()` / `abortAuthentication()` surface as
before — the V2 wire format, ECDH envelope, pairing service, state
machine, topic derivation, and peer-signer capture all run inside the
SDK. The V1 handshake codec and ephemeral-keypair logic are gone;
callers must inject a persistent `DeviceIdentityForPairing` via a thunk
on `createPappAdapter`.

`createPappAdapter` reshape:
  - drops `metadata: string` (the V1 metadata URL) — host name / icon /
    platform now ride inside `hostMetadata` (which mirrors V2's
    `HandshakeMetadata`)
  - requires `deviceIdentity: () => Promise<DeviceIdentityForPairing>`
  - new `onAuthSuccess`, `initialProcessedDataHex`,
    `onPairingStatementProcessed` callbacks for consumer-side
    persistence and reload-survival dedupe

`AuthSuccess.peerStatementAccountId` is lifted off
`statement.proof.value.signer` during pairing so device-sync can seed
PApp without a follow-up chain query. Threaded through
`HandshakeSuccessState` and `fromInnerResponse`.

Public surface trimmed to `createAuth` and the types you need to use
it. `startPairingV2`, the state-machine helpers (`idle` / `submitted` /
`advance` / `fromInnerResponse` / `isTerminal` /
`canSubmitV2Statements`), topic / envelope / proposal helpers
(`computePairingTopic` / `computePairingChannel` /
`buildPairingDeeplink` / `encodeProposal` / `decryptResponseEnvelope` /
`deriveIdentityChatPublicKey`), every `Handshake*` /
`*HandshakeResponse*` / `VersionedHandshake*` / `MetadataEntry` /
`MetadataKey` / `Device` SCALE codec, and the `Handshake*State` /
`HandshakeMetadata` / `HandshakeProposalDevice` /
`HandshakeResponseEnvelope` / `Pairing` / `StartPairingDeps` types are
all internal now. `PairingStatus.finished` no longer carries `session`
— read the resolved `AuthSuccess` off `authenticate()` instead.

`host-papp-react-ui`:
  - `useAuthStatus` returns `{ status, isSignedIn }` (was `signedInUser`,
    which depended on the V1 session shape)
  - `Flow.stories.tsx` updated for the new `createPappAdapter` params

523/523 tests pass. `auth.spec.ts` rewritten to drive the V2-backed
`createAuth` end-to-end (success, persistOnSuccess hook, Failed inner
response, abort, debug emits) using a real SCALE-encoded
HandshakeSuccessV2 envelope.
The previous draft mixed two reference frames: some items described the
diff vs. 0.7.8 (correct for a changelog), others described the diff vs.
an earlier state of this branch where V2 helpers were briefly public
(internal to PR #178 development, irrelevant to consumers). The latter
read as "no longer something callers need to wire up" / "public surface
trimmed" / "fix: EncryptedHandshakeResponseV2 misclassification" — none
of those make sense for a consumer upgrading from 0.7.8, where V2 SSO
didn't exist publicly at all.

Reframed:
  - Lead Features bullet now states V2 SSO as a fresh capability behind
    the existing createAuth surface, with the V2 codecs/service/state-
    machine flagged as SDK internals (not as "no longer public").
  - Dropped the "public surface trimmed" Breaking Change — those names
    were never in 0.7.8.
  - Dropped the EncryptedHandshakeResponseV2 fix bullet — that was a bug
    introduced and fixed inside this PR, not visible to 0.7.8 users.
  - Merged the legacy-payload-removal bullet into the main V1-removed
    bullet to avoid double-flagging the same break.
…K; restore V1-shape auth surface

The 0.7.x → 0.8.0 migration is now two field renames (`metadata` →
inline `hostMetadata`, `osType`/`osVersion` → `platformType`/
`platformVersion`). Everything else about the auth surface — the
`pairingStatus` / `authenticate()` / `abortAuthentication()` triad,
`PairingStatus.finished.session`, `authenticate()` resolving to
`StoredUserSession | null` — is what it was.

What moved into the SDK:

  - New internal `deviceIdentityStore` (encrypted via the same
    gcm/blake2b pattern as `UserSecretRepository`). `createPappAdapter`
    now defaults `deviceIdentity` to a `StorageAdapter`-backed factory
    that generates and persists a fresh identity on first run. Override
    with the new optional `deviceIdentity` param if you need a
    different backend (Electron Keychain, native secure storage).

  - Pairing-topic statement dedupe (`initialProcessedDataHex` /
    `onPairingStatementProcessed` in the previous draft) is now
    fully internal; the consumer-facing surface drops both.

  - On Success, `createAuth` builds a V2-shaped `StoredUserSession` and
    writes it + the per-session secrets to `ssoSessionRepository` and
    `userSecretRepository` itself. `authenticate()` returns the
    persisted `StoredUserSession`. The optional `onAuthSuccess` hook
    fires after persistence with `{ session, identityChatPrivateKey }`
    so consumers can fan the user-identity bits out to their own
    stores (e.g. polkadot-desktop's `deviceIdentityRepository`).

Schema changes:

  - `StoredUserSession` gains `identityAccountId?` and
    `identityChatPublicKey?` as trailing `Option` fields; the peer
    device statement account is exposed via `remoteAccount.accountId`.
    `from` decoder wraps in try/catch and returns `[]` on V1-blob
    decode failure, so the SDK silently wipes 0.7.x SsoSessions on
    first read instead of crashing.

  - `UserSecretRepository` gains `identityChatPrivateKey: Bytes(32)`
    as a trailing required field. `decode` wraps in try/catch and
    returns `null` on V1-blob decode failure.

  - `DeviceIdentityForPairing` adds a required `statementAccountSecret`
    (64-byte expanded sr25519 secret) so the V1 sessionManager prover
    can sign session statements with the device's stable identity.

Public surface stays compact: `createPappAdapter`, `PappAdapter`,
`AuthComponent`, `HostMetadata`, `OnAuthSuccess`, `PairingStatus`,
`DeviceIdentityForPairing`, `StoredUserSession`, `UserSession`,
`Identity`, signing request/response types, `RingVrf*`,
`SS_*_ENDPOINTS`. `AuthSuccess` is gone — `StoredUserSession` carries
the same fields.

`host-papp-react-ui`:
  - `useAuthStatus` restores `signedInUser` (reads
    `pairingStatus.finished.session`)
  - `Flow.stories.tsx` drops the `deviceIdentity` stub — the SDK now
    defaults it.

524/524 tests pass. New tests cover internal persistence, the
`onAuthSuccess` hook, and the default-deviceIdentity fallback.
@johnthecat
johnthecat changed the base branch from main to release/0.8 May 22, 2026 12:35
@johnthecat
johnthecat merged commit a393468 into release/0.8 May 22, 2026
5 checks passed
johnthecat added a commit that referenced this pull request Jun 12, 2026
Main (0.8.7) had independently absorbed nearly all of this branch's work
via PRs #178/#202/#205/#206/#212/#215/#216, with newer/cleaner versions.

Conflict resolution (38 files):
- package.json (x14), package-lock.json, CHANGELOG.md, migration doc:
  took main (newer versions, canonical release history). Dropped the
  unused `verifiablejs` dep.
- All SSO V2, statement-store, handoff-service, and host-chat *impl*
  files: took main — strictly newer (ECDH session-key derivation #206,
  RFC-7 entropy #205, statement-store fix #215). The branch's parallel
  May implementations are superseded.
- watchIdentity series: already identical in main (no real conflict).
- Kept BRANCH version for two files where it is genuinely ahead:
  * identity/rpcAdapter.ts — defensive snake_case/camelCase Resources
    decode (fixes "Unknown user" regression on V2 runtime); a clean
    superset of main's version.
  * host-chat/codec/attachment.spec.ts — extra blurhash-thumbnail and
    NodeEndpoint round-trip tests against a byte-identical codec impl.

Verified: build, typecheck, lint (0 errors), 679/679 tests pass.
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.

2 participants