@waaskey/sdk 0.3.1 → 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).
@@ -1299,24 +1303,33 @@ interface SendResult {
1299
1303
  * signing quorum roster (`roles` in `participants` order) and the WHOLE `message` bytes (ed25519 signs
1300
1304
  * the message, not a digest).
1301
1305
  */
1302
- interface EddsaSendSession {
1306
+ interface SendSessionResponse {
1303
1307
  /** The pending signing-activity row id — echoed back to the ASSEMBLE phase to finalize the signed tx. */
1304
1308
  txId: string;
1305
1309
  /** Relay websocket URL the device connects to. */
1306
1310
  relayUrl: string;
1307
1311
  /** Relay session id shared by the device + server parties. */
1308
1312
  sessionId: string;
1309
- /** 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']`. */
1310
1316
  roles: string[];
1311
- /** 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
+ */
1312
1321
  participants: number[];
1313
1322
  /** This device's own 0-based slot into {@link roles} (its position in the quorum). */
1314
1323
  signerPosition: number;
1315
- /** The raw message bytes to sign, hex (`0x` prefix optional) the chain adapter's serialized tx message. */
1316
- 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;
1317
1328
  /** Short-lived relay token (JWT) the device presents to join this session; present only when relay auth is enabled. */
1318
1329
  relayToken?: string;
1319
1330
  }
1331
+ /** @deprecated Use {@link SendSessionResponse} — the shape is curve-agnostic (secp256k1 + ed25519). */
1332
+ type EddsaSendSession = SendSessionResponse;
1320
1333
  /** Body of the ASSEMBLE phase (`POST …/send-session/:txId/assemble`) — the aggregated ed25519 signature the backend embeds into the wire tx (backend `AssembleEddsaTxRequest`). */
1321
1334
  interface EddsaAssembleRequest {
1322
1335
  /** The 64-byte RFC 8032 ed25519 signature (hex) the device + server co-produced. */
@@ -1524,33 +1537,37 @@ interface OnrampWidgetUrl {
1524
1537
  /** The recovery factors a wallet enrols (backend `RecoveryFactor`). */
1525
1538
  type RecoveryFactor = 'recovery_code' | 'totp' | 'email_otp';
1526
1539
  /**
1527
- * 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.
1528
1544
  *
1529
1545
  * Contract A: the recovery code is a client-side **sealing secret** and must never
1530
- * reach the server, so the `recovery_code` factor enrols `credentialHash` (the
1531
- * lowercase-hex SHA-256 of the code), never the code itself. `totp` / `email_otp`
1532
- * 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.
1533
1550
  */
1534
1551
  interface FactorEnrollment {
1535
1552
  type: RecoveryFactor;
1536
- /** totp → base32 secret; email_otp → email address. Omitted for `recovery_code`. */
1537
- credential?: string;
1538
- /** recovery_code → lowercase-hex SHA-256 of the recovery code (the raw code never leaves the client). */
1539
- credentialHash?: string;
1553
+ /** recovery_code → lowercase-hex SHA-256 of the code; totp → base32 secret; email_otp → email address. */
1554
+ credential: string;
1540
1555
  }
1541
1556
  /**
1542
- * One factor's proof at recovery time (backend `FactorVerification`).
1557
+ * One factor's proof at recovery time (backend `FactorVerificationDto`).
1558
+ *
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.
1543
1561
  *
1544
- * Contract A: `recovery_code` proves possession with `credentialHash` (the same
1545
- * SHA-256 the server stored), never the plaintext code — the server compares the
1546
- * hash in constant time and can never derive the code to unseal the ciphertext.
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.
1547
1566
  */
1548
1567
  interface FactorVerification {
1549
1568
  type: RecoveryFactor;
1550
- /** totp → current 6-digit OTP; email_otp → the emailed OTP. Omitted for `recovery_code`. */
1551
- token?: string;
1552
- /** recovery_code → lowercase-hex SHA-256 of the code (matches the enrolled hash). */
1553
- 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;
1554
1571
  }
1555
1572
  /** A registered recovery record's metadata (backend `RecoveryShareResponse`). */
1556
1573
  interface RecoveryShareInfo {
@@ -2088,14 +2105,35 @@ declare class Wallet {
2088
2105
  */
2089
2106
  sign(digest: string, options?: SignOptions): Promise<string>;
2090
2107
  /**
2091
- * Start this device's half of a secp sign ceremony when the wallet's sign quorum requires it.
2092
- * The quorum is the first `threshold` roles of the wallet's party list (#292) — for the default
2093
- * `[device, server, recovery]`/2 that is `[device, server]`, so the device MUST be online and
2094
- * co-signing. Returns `undefined` when the quorum is platform-only (or the wallet is not secp) —
2095
- * the POST then completes alone, unchanged. A device-present quorum without the device deps or
2096
- * 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.
2128
+ */
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).
2097
2135
  */
2098
- private startDeviceCoSign;
2136
+ private runDeviceSign;
2099
2137
  /**
2100
2138
  * Send a transaction from this wallet. The platform builds the chain-specific transaction
2101
2139
  * and co-signs it with the 2-of-3 MPC quorum, returning the **signed raw transaction**.
@@ -2111,13 +2149,34 @@ declare class Wallet {
2111
2149
  * server-side). Alternatively supply a pre-built assertion via `options.passkeyAssertion`.
2112
2150
  */
2113
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;
2114
2173
  /**
2115
2174
  * Device-co-signed send for an ed25519 (FROST) wallet (#110) — the browser holds the device FROST
2116
2175
  * share and co-signs 2-party with the backend `server` party over the relay:
2117
2176
  *
2118
2177
  * 1. **START** (`POST …/send-session`): the backend builds the unsigned tx (chain adapter), starts
2119
2178
  * its server FROST party on the relay in the background, and returns the raw `message` bytes to
2120
- * sign + the relay coordination ({@link EddsaSendSession}).
2179
+ * sign + the relay coordination ({@link SendSessionResponse}).
2121
2180
  * 2. **CO-SIGN**: the device runs `signEddsa` over the relay with its stored `{keyPackage,
2122
2181
  * publicKeyPackage}` share; the two parties aggregate the RFC 8032 signature (returned locally).
2123
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).
@@ -1299,24 +1303,33 @@ interface SendResult {
1299
1303
  * signing quorum roster (`roles` in `participants` order) and the WHOLE `message` bytes (ed25519 signs
1300
1304
  * the message, not a digest).
1301
1305
  */
1302
- interface EddsaSendSession {
1306
+ interface SendSessionResponse {
1303
1307
  /** The pending signing-activity row id — echoed back to the ASSEMBLE phase to finalize the signed tx. */
1304
1308
  txId: string;
1305
1309
  /** Relay websocket URL the device connects to. */
1306
1310
  relayUrl: string;
1307
1311
  /** Relay session id shared by the device + server parties. */
1308
1312
  sessionId: string;
1309
- /** 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']`. */
1310
1316
  roles: string[];
1311
- /** 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
+ */
1312
1321
  participants: number[];
1313
1322
  /** This device's own 0-based slot into {@link roles} (its position in the quorum). */
1314
1323
  signerPosition: number;
1315
- /** The raw message bytes to sign, hex (`0x` prefix optional) the chain adapter's serialized tx message. */
1316
- 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;
1317
1328
  /** Short-lived relay token (JWT) the device presents to join this session; present only when relay auth is enabled. */
1318
1329
  relayToken?: string;
1319
1330
  }
1331
+ /** @deprecated Use {@link SendSessionResponse} — the shape is curve-agnostic (secp256k1 + ed25519). */
1332
+ type EddsaSendSession = SendSessionResponse;
1320
1333
  /** Body of the ASSEMBLE phase (`POST …/send-session/:txId/assemble`) — the aggregated ed25519 signature the backend embeds into the wire tx (backend `AssembleEddsaTxRequest`). */
1321
1334
  interface EddsaAssembleRequest {
1322
1335
  /** The 64-byte RFC 8032 ed25519 signature (hex) the device + server co-produced. */
@@ -1524,33 +1537,37 @@ interface OnrampWidgetUrl {
1524
1537
  /** The recovery factors a wallet enrols (backend `RecoveryFactor`). */
1525
1538
  type RecoveryFactor = 'recovery_code' | 'totp' | 'email_otp';
1526
1539
  /**
1527
- * 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.
1528
1544
  *
1529
1545
  * Contract A: the recovery code is a client-side **sealing secret** and must never
1530
- * reach the server, so the `recovery_code` factor enrols `credentialHash` (the
1531
- * lowercase-hex SHA-256 of the code), never the code itself. `totp` / `email_otp`
1532
- * 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.
1533
1550
  */
1534
1551
  interface FactorEnrollment {
1535
1552
  type: RecoveryFactor;
1536
- /** totp → base32 secret; email_otp → email address. Omitted for `recovery_code`. */
1537
- credential?: string;
1538
- /** recovery_code → lowercase-hex SHA-256 of the recovery code (the raw code never leaves the client). */
1539
- credentialHash?: string;
1553
+ /** recovery_code → lowercase-hex SHA-256 of the code; totp → base32 secret; email_otp → email address. */
1554
+ credential: string;
1540
1555
  }
1541
1556
  /**
1542
- * One factor's proof at recovery time (backend `FactorVerification`).
1557
+ * One factor's proof at recovery time (backend `FactorVerificationDto`).
1558
+ *
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.
1543
1561
  *
1544
- * Contract A: `recovery_code` proves possession with `credentialHash` (the same
1545
- * SHA-256 the server stored), never the plaintext code — the server compares the
1546
- * hash in constant time and can never derive the code to unseal the ciphertext.
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.
1547
1566
  */
1548
1567
  interface FactorVerification {
1549
1568
  type: RecoveryFactor;
1550
- /** totp → current 6-digit OTP; email_otp → the emailed OTP. Omitted for `recovery_code`. */
1551
- token?: string;
1552
- /** recovery_code → lowercase-hex SHA-256 of the code (matches the enrolled hash). */
1553
- 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;
1554
1571
  }
1555
1572
  /** A registered recovery record's metadata (backend `RecoveryShareResponse`). */
1556
1573
  interface RecoveryShareInfo {
@@ -2088,14 +2105,35 @@ declare class Wallet {
2088
2105
  */
2089
2106
  sign(digest: string, options?: SignOptions): Promise<string>;
2090
2107
  /**
2091
- * Start this device's half of a secp sign ceremony when the wallet's sign quorum requires it.
2092
- * The quorum is the first `threshold` roles of the wallet's party list (#292) — for the default
2093
- * `[device, server, recovery]`/2 that is `[device, server]`, so the device MUST be online and
2094
- * co-signing. Returns `undefined` when the quorum is platform-only (or the wallet is not secp) —
2095
- * the POST then completes alone, unchanged. A device-present quorum without the device deps or
2096
- * 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.
2128
+ */
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).
2097
2135
  */
2098
- private startDeviceCoSign;
2136
+ private runDeviceSign;
2099
2137
  /**
2100
2138
  * Send a transaction from this wallet. The platform builds the chain-specific transaction
2101
2139
  * and co-signs it with the 2-of-3 MPC quorum, returning the **signed raw transaction**.
@@ -2111,13 +2149,34 @@ declare class Wallet {
2111
2149
  * server-side). Alternatively supply a pre-built assertion via `options.passkeyAssertion`.
2112
2150
  */
2113
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;
2114
2173
  /**
2115
2174
  * Device-co-signed send for an ed25519 (FROST) wallet (#110) — the browser holds the device FROST
2116
2175
  * share and co-signs 2-party with the backend `server` party over the relay:
2117
2176
  *
2118
2177
  * 1. **START** (`POST …/send-session`): the backend builds the unsigned tx (chain adapter), starts
2119
2178
  * its server FROST party on the relay in the background, and returns the raw `message` bytes to
2120
- * sign + the relay coordination ({@link EddsaSendSession}).
2179
+ * sign + the relay coordination ({@link SendSessionResponse}).
2121
2180
  * 2. **CO-SIGN**: the device runs `signEddsa` over the relay with its stored `{keyPackage,
2122
2181
  * publicKeyPackage}` share; the two parties aggregate the RFC 8032 signature (returned locally).
2123
2182
  * 3. **ASSEMBLE** (`POST …/send-session/:txId/assemble`): the backend embeds the aggregated