@waaskey/sdk 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -245,7 +245,11 @@ interface DeviceSignParams extends CeremonyParams {
245
245
  }
246
246
  /** Result of the device half of sign. */
247
247
  interface DeviceSignResult {
248
- /** The cggmp24 signature (JSON). */
248
+ /**
249
+ * The signature as LOW-S–normalized compact `r ‖ s` hex (128 chars, no `0x`) — byte-identical to
250
+ * what the platform's own party produces, so the backend can cross-check the two before embedding
251
+ * one into a transaction, and chain adapters can slice `r`/`s` by offset.
252
+ */
249
253
  signature: string;
250
254
  }
251
255
  /**
@@ -570,7 +574,7 @@ declare class WasmMpcCore implements MpcCore {
570
574
  * exact, optional peer dependency in `package.json` — so an app that installs `@waaskey/client-wasm`
571
575
  * alongside the SDK runs the real ed25519 create/send path end to end.
572
576
  */
573
- declare const CLIENT_WASM_VERSION = "0.2.1";
577
+ declare const CLIENT_WASM_VERSION = "0.2.2";
574
578
  /**
575
579
  * Verify the integrity of raw wasm bytes against an expected **SHA-384** hash, in
576
580
  * Subresource-Integrity (`sha384-<base64>`) form (issue #40).
@@ -609,24 +613,6 @@ interface VerifiedWasmLoaderOptions {
609
613
  * ```
610
614
  */
611
615
  declare function createVerifiedClientWasmLoader(options: VerifiedWasmLoaderOptions): ClientWasmLoader;
612
- /**
613
- * Convenience loader for the Waaskey client-wasm engine — dynamically imports `@waaskey/client-wasm`
614
- * (the wasm-pack/bundler build, auto-initialized on import) and returns it as a {@link ClientWasmModule}:
615
- *
616
- * ```ts
617
- * import { Waaskey, WasmMpcCore, loadClientWasm } from '@waaskey/sdk';
618
- * const waaskey = new Waaskey({ apiKey, mpc: new WasmMpcCore(loadClientWasm), shareStore });
619
- * ```
620
- *
621
- * ⚠️ **No integrity verification (issue #40).** A bundler-target build instantiates the wasm on
622
- * import, so its bytes cannot be checked first. For production — where a registry compromise or
623
- * dependency-confusion attack on `@waaskey/client-wasm` could exfiltrate the plaintext device share
624
- * — use {@link createVerifiedClientWasmLoader} with a vendored `.wasm` and the pinned SHA-384 instead.
625
- *
626
- * `@waaskey/client-wasm` is an OPTIONAL peer dependency pinned to {@link CLIENT_WASM_VERSION}: install
627
- * it alongside the SDK in apps that create wallets. The indirect import specifier keeps it out of
628
- * static module resolution — so a missing install fails here with an actionable message, not at build.
629
- */
630
616
  declare const loadClientWasm: ClientWasmLoader;
631
617
 
632
618
  /**
@@ -1317,24 +1303,33 @@ interface SendResult {
1317
1303
  * signing quorum roster (`roles` in `participants` order) and the WHOLE `message` bytes (ed25519 signs
1318
1304
  * the message, not a digest).
1319
1305
  */
1320
- interface EddsaSendSession {
1306
+ interface SendSessionResponse {
1321
1307
  /** The pending signing-activity row id — echoed back to the ASSEMBLE phase to finalize the signed tx. */
1322
1308
  txId: string;
1323
1309
  /** Relay websocket URL the device connects to. */
1324
1310
  relayUrl: string;
1325
1311
  /** Relay session id shared by the device + server parties. */
1326
1312
  sessionId: string;
1327
- /** The FROST signing quorum's relay roles, in `participants` order (`roles[i]` `participants[i]`), e.g. `['device','server']`. */
1313
+ /** The wallet's curve it decides which payload field below is populated and how `participants` is numbered. */
1314
+ curve: WalletCurve;
1315
+ /** The signing quorum's relay roles, in `participants` order (`roles[i]` ↔ `participants[i]`), e.g. `['device','server']`. */
1328
1316
  roles: string[];
1329
- /** The 1-based FROST identifiers of the quorum, parallel to {@link roles} (e.g. `[1, 2]`). */
1317
+ /**
1318
+ * The quorum's party identifiers, parallel to {@link roles} — **numbered per curve**: ed25519
1319
+ * (FROST) uses 1-based identifiers (`[1, 2]`), secp256k1 (cggmp24) 0-based keygen indices (`[0, 1]`).
1320
+ */
1330
1321
  participants: number[];
1331
1322
  /** This device's own 0-based slot into {@link roles} (its position in the quorum). */
1332
1323
  signerPosition: number;
1333
- /** The raw message bytes to sign, hex (`0x` prefix optional) the chain adapter's serialized tx message. */
1334
- message: string;
1324
+ /** The raw message bytes to sign, hex **ed25519 only** (FROST signs the message, not a hash). */
1325
+ message?: string;
1326
+ /** The 32-byte tx digest to sign, hex — **secp256k1 only** (cggmp24 signs a digest). */
1327
+ digest?: string;
1335
1328
  /** Short-lived relay token (JWT) the device presents to join this session; present only when relay auth is enabled. */
1336
1329
  relayToken?: string;
1337
1330
  }
1331
+ /** @deprecated Use {@link SendSessionResponse} — the shape is curve-agnostic (secp256k1 + ed25519). */
1332
+ type EddsaSendSession = SendSessionResponse;
1338
1333
  /** Body of the ASSEMBLE phase (`POST …/send-session/:txId/assemble`) — the aggregated ed25519 signature the backend embeds into the wire tx (backend `AssembleEddsaTxRequest`). */
1339
1334
  interface EddsaAssembleRequest {
1340
1335
  /** The 64-byte RFC 8032 ed25519 signature (hex) the device + server co-produced. */
@@ -1542,33 +1537,37 @@ interface OnrampWidgetUrl {
1542
1537
  /** The recovery factors a wallet enrols (backend `RecoveryFactor`). */
1543
1538
  type RecoveryFactor = 'recovery_code' | 'totp' | 'email_otp';
1544
1539
  /**
1545
- * Enrolment of one factor at register time (backend `FactorEnrollment`).
1540
+ * Enrolment of one factor at register time (backend `FactorEnrollmentDto`).
1541
+ *
1542
+ * The wire field is `credential` for EVERY factor — the API's DTO declares exactly
1543
+ * `{ type, credential }` and its validation pipe rejects any other property.
1546
1544
  *
1547
1545
  * Contract A: the recovery code is a client-side **sealing secret** and must never
1548
- * reach the server, so the `recovery_code` factor enrols `credentialHash` (the
1549
- * lowercase-hex SHA-256 of the code), never the code itself. `totp` / `email_otp`
1550
- * enrol their non-sealing `credential` (base32 secret / email) as before.
1546
+ * reach the server, so for `recovery_code` the `credential` carries the lowercase-hex
1547
+ * SHA-256 of the code, never the code itself (the SDK hashes it in
1548
+ * {@link buildRecoveryRegistration}). `totp` / `email_otp` enrol their non-sealing
1549
+ * credential (base32 secret / email address) verbatim.
1551
1550
  */
1552
1551
  interface FactorEnrollment {
1553
1552
  type: RecoveryFactor;
1554
- /** totp → base32 secret; email_otp → email address. Omitted for `recovery_code`. */
1555
- credential?: string;
1556
- /** recovery_code → lowercase-hex SHA-256 of the recovery code (the raw code never leaves the client). */
1557
- credentialHash?: string;
1553
+ /** recovery_code → lowercase-hex SHA-256 of the code; totp → base32 secret; email_otp → email address. */
1554
+ credential: string;
1558
1555
  }
1559
1556
  /**
1560
- * One factor's proof at recovery time (backend `FactorVerification`).
1557
+ * One factor's proof at recovery time (backend `FactorVerificationDto`).
1561
1558
  *
1562
- * Contract A: `recovery_code` proves possession with `credentialHash` (the same
1563
- * SHA-256 the server stored), never the plaintext code the server compares the
1564
- * hash in constant time and can never derive the code to unseal the ciphertext.
1559
+ * The wire field is `token` for EVERY factor the API's DTO declares exactly
1560
+ * `{ type, token }` and its validation pipe rejects any other property.
1561
+ *
1562
+ * Contract A: a `recovery_code` proof is sent as the same lowercase-hex SHA-256 the
1563
+ * server stored, never the plaintext code — the server compares the hash in constant
1564
+ * time and can never derive the code to unseal the ciphertext. Callers pass the
1565
+ * PLAINTEXT code here; the SDK hashes it before the request leaves the device.
1565
1566
  */
1566
1567
  interface FactorVerification {
1567
1568
  type: RecoveryFactor;
1568
- /** totp → current 6-digit OTP; email_otp → the emailed OTP. Omitted for `recovery_code`. */
1569
- token?: string;
1570
- /** recovery_code → lowercase-hex SHA-256 of the code (matches the enrolled hash). */
1571
- credentialHash?: string;
1569
+ /** recovery_code → the plaintext code (the SDK sends its SHA-256); totp → current 6-digit OTP; email_otp → the emailed OTP. */
1570
+ token: string;
1572
1571
  }
1573
1572
  /** A registered recovery record's metadata (backend `RecoveryShareResponse`). */
1574
1573
  interface RecoveryShareInfo {
@@ -2106,14 +2105,35 @@ declare class Wallet {
2106
2105
  */
2107
2106
  sign(digest: string, options?: SignOptions): Promise<string>;
2108
2107
  /**
2109
- * Start this device's half of a secp sign ceremony when the wallet's sign quorum requires it.
2110
- * The quorum is the first `threshold` roles of the wallet's party list (#292) — for the default
2111
- * `[device, server, recovery]`/2 that is `[device, server]`, so the device MUST be online and
2112
- * co-signing. Returns `undefined` when the quorum is platform-only (or the wallet is not secp) —
2113
- * the POST then completes alone, unchanged. A device-present quorum without the device deps or
2114
- * stored share fails fast with a typed error instead of a guaranteed server-side timeout.
2108
+ * The roles that will actually sign the first `threshold` of the wallet's party list (#292),
2109
+ * mirroring the signer's own derivation. `undefined` when the topology is unknown (a legacy
2110
+ * wallet record), which callers treat as "let the server decide".
2111
+ */
2112
+ private signQuorum;
2113
+ /**
2114
+ * Everything this device needs to join a ceremony, resolved BEFORE one is started (#89): the MPC
2115
+ * core, the routed-sign capability a >2-party quorum needs, and the stored key share.
2116
+ *
2117
+ * Each failure is a typed error thrown straight to the caller, and the ordering is the point: a
2118
+ * device that cannot co-sign must never leave the platform party waiting on the relay for a
2119
+ * counterpart that will never arrive. That wait ends at the party-runner timeout (~210s) and
2120
+ * reaches the caller as an opaque 5xx — minutes after a knowable, local cause.
2121
+ */
2122
+ private loadDeviceParty;
2123
+ /**
2124
+ * Normalize a sign-session descriptor into the shared {@link DeviceCeremony}. The 2-party roster
2125
+ * comes from the descriptor's own `role`/`peerRole` (authoritative — the relay token is bound to
2126
+ * that role); a larger quorum carries no server-sent roster, so the wallet's own party slice — the
2127
+ * same slice the signer derives — supplies it.
2115
2128
  */
2116
- private startDeviceCoSign;
2129
+ private toSignCeremony;
2130
+ /**
2131
+ * Run this device's half of a started ceremony. A 2-party quorum uses the plain single-peer
2132
+ * transport; anything larger MUST be roster-routed, or the transport attributes every inbound
2133
+ * message to the one configured peer and the protocol aborts on the third party's first message
2134
+ * (waas-core#131).
2135
+ */
2136
+ private runDeviceSign;
2117
2137
  /**
2118
2138
  * Send a transaction from this wallet. The platform builds the chain-specific transaction
2119
2139
  * and co-signs it with the 2-of-3 MPC quorum, returning the **signed raw transaction**.
@@ -2129,13 +2149,34 @@ declare class Wallet {
2129
2149
  * server-side). Alternatively supply a pre-built assertion via `options.passkeyAssertion`.
2130
2150
  */
2131
2151
  send(params: SendParams, options?: SendOptions): Promise<SendResult>;
2152
+ /**
2153
+ * Device-co-signed send for a secp wallet — the cggmp24 counterpart of {@link sendEd25519}:
2154
+ *
2155
+ * 1. **START** (`POST …/send-session`): the platform runs the transfer gates, builds the unsigned
2156
+ * tx, puts its own party on the relay in the background, and returns the 32-byte digest plus
2157
+ * the relay coordination. It does NOT wait for the ceremony.
2158
+ * 2. **CO-SIGN**: this device runs its half over the relay with its stored share; cggmp24 hands
2159
+ * the completed signature to both parties.
2160
+ * 3. **ASSEMBLE** (`POST …/send-session/:txId/assemble`): the platform verifies the signature
2161
+ * (recovers to the wallet key AND equals its own party's) and embeds it into the wire tx.
2162
+ *
2163
+ * Every device-side precondition is resolved BEFORE the START (see {@link loadDeviceParty}), so a
2164
+ * device that cannot co-sign costs nothing: no tx is built and no platform party is left waiting.
2165
+ *
2166
+ * **A co-sign that fails after a successful START rejects with its typed error.** START is not the
2167
+ * commit point — it yields an *unsigned* tx and a pending session, and nothing broadcastable exists
2168
+ * until ASSEMBLE returns a `signedTx` — so there is no result to salvage by swallowing the failure,
2169
+ * and no fallback to `POST /send` (the platform cannot reach the threshold on this wallet alone, so
2170
+ * a retry there would only hang). The backend expires the abandoned session and fails the tx row.
2171
+ */
2172
+ private sendWithDevice;
2132
2173
  /**
2133
2174
  * Device-co-signed send for an ed25519 (FROST) wallet (#110) — the browser holds the device FROST
2134
2175
  * share and co-signs 2-party with the backend `server` party over the relay:
2135
2176
  *
2136
2177
  * 1. **START** (`POST …/send-session`): the backend builds the unsigned tx (chain adapter), starts
2137
2178
  * its server FROST party on the relay in the background, and returns the raw `message` bytes to
2138
- * sign + the relay coordination ({@link EddsaSendSession}).
2179
+ * sign + the relay coordination ({@link SendSessionResponse}).
2139
2180
  * 2. **CO-SIGN**: the device runs `signEddsa` over the relay with its stored `{keyPackage,
2140
2181
  * publicKeyPackage}` share; the two parties aggregate the RFC 8032 signature (returned locally).
2141
2182
  * 3. **ASSEMBLE** (`POST …/send-session/:txId/assemble`): the backend embeds the aggregated
package/dist/index.d.ts CHANGED
@@ -245,7 +245,11 @@ interface DeviceSignParams extends CeremonyParams {
245
245
  }
246
246
  /** Result of the device half of sign. */
247
247
  interface DeviceSignResult {
248
- /** The cggmp24 signature (JSON). */
248
+ /**
249
+ * The signature as LOW-S–normalized compact `r ‖ s` hex (128 chars, no `0x`) — byte-identical to
250
+ * what the platform's own party produces, so the backend can cross-check the two before embedding
251
+ * one into a transaction, and chain adapters can slice `r`/`s` by offset.
252
+ */
249
253
  signature: string;
250
254
  }
251
255
  /**
@@ -570,7 +574,7 @@ declare class WasmMpcCore implements MpcCore {
570
574
  * exact, optional peer dependency in `package.json` — so an app that installs `@waaskey/client-wasm`
571
575
  * alongside the SDK runs the real ed25519 create/send path end to end.
572
576
  */
573
- declare const CLIENT_WASM_VERSION = "0.2.1";
577
+ declare const CLIENT_WASM_VERSION = "0.2.2";
574
578
  /**
575
579
  * Verify the integrity of raw wasm bytes against an expected **SHA-384** hash, in
576
580
  * Subresource-Integrity (`sha384-<base64>`) form (issue #40).
@@ -609,24 +613,6 @@ interface VerifiedWasmLoaderOptions {
609
613
  * ```
610
614
  */
611
615
  declare function createVerifiedClientWasmLoader(options: VerifiedWasmLoaderOptions): ClientWasmLoader;
612
- /**
613
- * Convenience loader for the Waaskey client-wasm engine — dynamically imports `@waaskey/client-wasm`
614
- * (the wasm-pack/bundler build, auto-initialized on import) and returns it as a {@link ClientWasmModule}:
615
- *
616
- * ```ts
617
- * import { Waaskey, WasmMpcCore, loadClientWasm } from '@waaskey/sdk';
618
- * const waaskey = new Waaskey({ apiKey, mpc: new WasmMpcCore(loadClientWasm), shareStore });
619
- * ```
620
- *
621
- * ⚠️ **No integrity verification (issue #40).** A bundler-target build instantiates the wasm on
622
- * import, so its bytes cannot be checked first. For production — where a registry compromise or
623
- * dependency-confusion attack on `@waaskey/client-wasm` could exfiltrate the plaintext device share
624
- * — use {@link createVerifiedClientWasmLoader} with a vendored `.wasm` and the pinned SHA-384 instead.
625
- *
626
- * `@waaskey/client-wasm` is an OPTIONAL peer dependency pinned to {@link CLIENT_WASM_VERSION}: install
627
- * it alongside the SDK in apps that create wallets. The indirect import specifier keeps it out of
628
- * static module resolution — so a missing install fails here with an actionable message, not at build.
629
- */
630
616
  declare const loadClientWasm: ClientWasmLoader;
631
617
 
632
618
  /**
@@ -1317,24 +1303,33 @@ interface SendResult {
1317
1303
  * signing quorum roster (`roles` in `participants` order) and the WHOLE `message` bytes (ed25519 signs
1318
1304
  * the message, not a digest).
1319
1305
  */
1320
- interface EddsaSendSession {
1306
+ interface SendSessionResponse {
1321
1307
  /** The pending signing-activity row id — echoed back to the ASSEMBLE phase to finalize the signed tx. */
1322
1308
  txId: string;
1323
1309
  /** Relay websocket URL the device connects to. */
1324
1310
  relayUrl: string;
1325
1311
  /** Relay session id shared by the device + server parties. */
1326
1312
  sessionId: string;
1327
- /** The FROST signing quorum's relay roles, in `participants` order (`roles[i]` `participants[i]`), e.g. `['device','server']`. */
1313
+ /** The wallet's curve it decides which payload field below is populated and how `participants` is numbered. */
1314
+ curve: WalletCurve;
1315
+ /** The signing quorum's relay roles, in `participants` order (`roles[i]` ↔ `participants[i]`), e.g. `['device','server']`. */
1328
1316
  roles: string[];
1329
- /** The 1-based FROST identifiers of the quorum, parallel to {@link roles} (e.g. `[1, 2]`). */
1317
+ /**
1318
+ * The quorum's party identifiers, parallel to {@link roles} — **numbered per curve**: ed25519
1319
+ * (FROST) uses 1-based identifiers (`[1, 2]`), secp256k1 (cggmp24) 0-based keygen indices (`[0, 1]`).
1320
+ */
1330
1321
  participants: number[];
1331
1322
  /** This device's own 0-based slot into {@link roles} (its position in the quorum). */
1332
1323
  signerPosition: number;
1333
- /** The raw message bytes to sign, hex (`0x` prefix optional) the chain adapter's serialized tx message. */
1334
- message: string;
1324
+ /** The raw message bytes to sign, hex **ed25519 only** (FROST signs the message, not a hash). */
1325
+ message?: string;
1326
+ /** The 32-byte tx digest to sign, hex — **secp256k1 only** (cggmp24 signs a digest). */
1327
+ digest?: string;
1335
1328
  /** Short-lived relay token (JWT) the device presents to join this session; present only when relay auth is enabled. */
1336
1329
  relayToken?: string;
1337
1330
  }
1331
+ /** @deprecated Use {@link SendSessionResponse} — the shape is curve-agnostic (secp256k1 + ed25519). */
1332
+ type EddsaSendSession = SendSessionResponse;
1338
1333
  /** Body of the ASSEMBLE phase (`POST …/send-session/:txId/assemble`) — the aggregated ed25519 signature the backend embeds into the wire tx (backend `AssembleEddsaTxRequest`). */
1339
1334
  interface EddsaAssembleRequest {
1340
1335
  /** The 64-byte RFC 8032 ed25519 signature (hex) the device + server co-produced. */
@@ -1542,33 +1537,37 @@ interface OnrampWidgetUrl {
1542
1537
  /** The recovery factors a wallet enrols (backend `RecoveryFactor`). */
1543
1538
  type RecoveryFactor = 'recovery_code' | 'totp' | 'email_otp';
1544
1539
  /**
1545
- * Enrolment of one factor at register time (backend `FactorEnrollment`).
1540
+ * Enrolment of one factor at register time (backend `FactorEnrollmentDto`).
1541
+ *
1542
+ * The wire field is `credential` for EVERY factor — the API's DTO declares exactly
1543
+ * `{ type, credential }` and its validation pipe rejects any other property.
1546
1544
  *
1547
1545
  * Contract A: the recovery code is a client-side **sealing secret** and must never
1548
- * reach the server, so the `recovery_code` factor enrols `credentialHash` (the
1549
- * lowercase-hex SHA-256 of the code), never the code itself. `totp` / `email_otp`
1550
- * enrol their non-sealing `credential` (base32 secret / email) as before.
1546
+ * reach the server, so for `recovery_code` the `credential` carries the lowercase-hex
1547
+ * SHA-256 of the code, never the code itself (the SDK hashes it in
1548
+ * {@link buildRecoveryRegistration}). `totp` / `email_otp` enrol their non-sealing
1549
+ * credential (base32 secret / email address) verbatim.
1551
1550
  */
1552
1551
  interface FactorEnrollment {
1553
1552
  type: RecoveryFactor;
1554
- /** totp → base32 secret; email_otp → email address. Omitted for `recovery_code`. */
1555
- credential?: string;
1556
- /** recovery_code → lowercase-hex SHA-256 of the recovery code (the raw code never leaves the client). */
1557
- credentialHash?: string;
1553
+ /** recovery_code → lowercase-hex SHA-256 of the code; totp → base32 secret; email_otp → email address. */
1554
+ credential: string;
1558
1555
  }
1559
1556
  /**
1560
- * One factor's proof at recovery time (backend `FactorVerification`).
1557
+ * One factor's proof at recovery time (backend `FactorVerificationDto`).
1561
1558
  *
1562
- * Contract A: `recovery_code` proves possession with `credentialHash` (the same
1563
- * SHA-256 the server stored), never the plaintext code the server compares the
1564
- * hash in constant time and can never derive the code to unseal the ciphertext.
1559
+ * The wire field is `token` for EVERY factor the API's DTO declares exactly
1560
+ * `{ type, token }` and its validation pipe rejects any other property.
1561
+ *
1562
+ * Contract A: a `recovery_code` proof is sent as the same lowercase-hex SHA-256 the
1563
+ * server stored, never the plaintext code — the server compares the hash in constant
1564
+ * time and can never derive the code to unseal the ciphertext. Callers pass the
1565
+ * PLAINTEXT code here; the SDK hashes it before the request leaves the device.
1565
1566
  */
1566
1567
  interface FactorVerification {
1567
1568
  type: RecoveryFactor;
1568
- /** totp → current 6-digit OTP; email_otp → the emailed OTP. Omitted for `recovery_code`. */
1569
- token?: string;
1570
- /** recovery_code → lowercase-hex SHA-256 of the code (matches the enrolled hash). */
1571
- credentialHash?: string;
1569
+ /** recovery_code → the plaintext code (the SDK sends its SHA-256); totp → current 6-digit OTP; email_otp → the emailed OTP. */
1570
+ token: string;
1572
1571
  }
1573
1572
  /** A registered recovery record's metadata (backend `RecoveryShareResponse`). */
1574
1573
  interface RecoveryShareInfo {
@@ -2106,14 +2105,35 @@ declare class Wallet {
2106
2105
  */
2107
2106
  sign(digest: string, options?: SignOptions): Promise<string>;
2108
2107
  /**
2109
- * Start this device's half of a secp sign ceremony when the wallet's sign quorum requires it.
2110
- * The quorum is the first `threshold` roles of the wallet's party list (#292) — for the default
2111
- * `[device, server, recovery]`/2 that is `[device, server]`, so the device MUST be online and
2112
- * co-signing. Returns `undefined` when the quorum is platform-only (or the wallet is not secp) —
2113
- * the POST then completes alone, unchanged. A device-present quorum without the device deps or
2114
- * stored share fails fast with a typed error instead of a guaranteed server-side timeout.
2108
+ * The roles that will actually sign the first `threshold` of the wallet's party list (#292),
2109
+ * mirroring the signer's own derivation. `undefined` when the topology is unknown (a legacy
2110
+ * wallet record), which callers treat as "let the server decide".
2111
+ */
2112
+ private signQuorum;
2113
+ /**
2114
+ * Everything this device needs to join a ceremony, resolved BEFORE one is started (#89): the MPC
2115
+ * core, the routed-sign capability a >2-party quorum needs, and the stored key share.
2116
+ *
2117
+ * Each failure is a typed error thrown straight to the caller, and the ordering is the point: a
2118
+ * device that cannot co-sign must never leave the platform party waiting on the relay for a
2119
+ * counterpart that will never arrive. That wait ends at the party-runner timeout (~210s) and
2120
+ * reaches the caller as an opaque 5xx — minutes after a knowable, local cause.
2121
+ */
2122
+ private loadDeviceParty;
2123
+ /**
2124
+ * Normalize a sign-session descriptor into the shared {@link DeviceCeremony}. The 2-party roster
2125
+ * comes from the descriptor's own `role`/`peerRole` (authoritative — the relay token is bound to
2126
+ * that role); a larger quorum carries no server-sent roster, so the wallet's own party slice — the
2127
+ * same slice the signer derives — supplies it.
2115
2128
  */
2116
- private startDeviceCoSign;
2129
+ private toSignCeremony;
2130
+ /**
2131
+ * Run this device's half of a started ceremony. A 2-party quorum uses the plain single-peer
2132
+ * transport; anything larger MUST be roster-routed, or the transport attributes every inbound
2133
+ * message to the one configured peer and the protocol aborts on the third party's first message
2134
+ * (waas-core#131).
2135
+ */
2136
+ private runDeviceSign;
2117
2137
  /**
2118
2138
  * Send a transaction from this wallet. The platform builds the chain-specific transaction
2119
2139
  * and co-signs it with the 2-of-3 MPC quorum, returning the **signed raw transaction**.
@@ -2129,13 +2149,34 @@ declare class Wallet {
2129
2149
  * server-side). Alternatively supply a pre-built assertion via `options.passkeyAssertion`.
2130
2150
  */
2131
2151
  send(params: SendParams, options?: SendOptions): Promise<SendResult>;
2152
+ /**
2153
+ * Device-co-signed send for a secp wallet — the cggmp24 counterpart of {@link sendEd25519}:
2154
+ *
2155
+ * 1. **START** (`POST …/send-session`): the platform runs the transfer gates, builds the unsigned
2156
+ * tx, puts its own party on the relay in the background, and returns the 32-byte digest plus
2157
+ * the relay coordination. It does NOT wait for the ceremony.
2158
+ * 2. **CO-SIGN**: this device runs its half over the relay with its stored share; cggmp24 hands
2159
+ * the completed signature to both parties.
2160
+ * 3. **ASSEMBLE** (`POST …/send-session/:txId/assemble`): the platform verifies the signature
2161
+ * (recovers to the wallet key AND equals its own party's) and embeds it into the wire tx.
2162
+ *
2163
+ * Every device-side precondition is resolved BEFORE the START (see {@link loadDeviceParty}), so a
2164
+ * device that cannot co-sign costs nothing: no tx is built and no platform party is left waiting.
2165
+ *
2166
+ * **A co-sign that fails after a successful START rejects with its typed error.** START is not the
2167
+ * commit point — it yields an *unsigned* tx and a pending session, and nothing broadcastable exists
2168
+ * until ASSEMBLE returns a `signedTx` — so there is no result to salvage by swallowing the failure,
2169
+ * and no fallback to `POST /send` (the platform cannot reach the threshold on this wallet alone, so
2170
+ * a retry there would only hang). The backend expires the abandoned session and fails the tx row.
2171
+ */
2172
+ private sendWithDevice;
2132
2173
  /**
2133
2174
  * Device-co-signed send for an ed25519 (FROST) wallet (#110) — the browser holds the device FROST
2134
2175
  * share and co-signs 2-party with the backend `server` party over the relay:
2135
2176
  *
2136
2177
  * 1. **START** (`POST …/send-session`): the backend builds the unsigned tx (chain adapter), starts
2137
2178
  * its server FROST party on the relay in the background, and returns the raw `message` bytes to
2138
- * sign + the relay coordination ({@link EddsaSendSession}).
2179
+ * sign + the relay coordination ({@link SendSessionResponse}).
2139
2180
  * 2. **CO-SIGN**: the device runs `signEddsa` over the relay with its stored `{keyPackage,
2140
2181
  * publicKeyPackage}` share; the two parties aggregate the RFC 8032 signature (returned locally).
2141
2182
  * 3. **ASSEMBLE** (`POST …/send-session/:txId/assemble`): the backend embeds the aggregated