decane-connect-kit 2.1.0 → 2.2.0

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
@@ -283,6 +283,25 @@ interface DecaneConnectConfig {
283
283
  apiBaseUrl?: string;
284
284
  sessionDurationMinutes?: number;
285
285
  promptPin?: () => Promise<string>;
286
+ /**
287
+ * Require a fresh WebAuthn assertion for every signature, instead of letting
288
+ * an unlocked session sign freely until it expires. The TEE session stays
289
+ * warm — this costs one authenticator prompt per signature, not a re-auth or
290
+ * re-attestation — and the check is enforced inside the enclave, not here.
291
+ *
292
+ * Default false, which preserves the session-signing model: unlock once, sign
293
+ * for the session's lifetime with no further prompts.
294
+ *
295
+ * Two things worth knowing before turning this on:
296
+ * - Multi-step flows prompt per leg. An ERC-20 approve + swap is two
297
+ * prompts, not one.
298
+ * - The policy is declared by the client at session open, so it hardens a
299
+ * leaked session handle (useless without the authenticator) but a fully
300
+ * compromised page could open a session without it. Making it
301
+ * unbypassable requires the policy to come from the server per user or
302
+ * per project; that is not implemented yet.
303
+ */
304
+ requireAssertionPerSignature?: boolean;
286
305
  rpId?: string;
287
306
  rpName?: string;
288
307
  rpcUrls?: Record<string, string>;
package/dist/index.d.ts CHANGED
@@ -283,6 +283,25 @@ interface DecaneConnectConfig {
283
283
  apiBaseUrl?: string;
284
284
  sessionDurationMinutes?: number;
285
285
  promptPin?: () => Promise<string>;
286
+ /**
287
+ * Require a fresh WebAuthn assertion for every signature, instead of letting
288
+ * an unlocked session sign freely until it expires. The TEE session stays
289
+ * warm — this costs one authenticator prompt per signature, not a re-auth or
290
+ * re-attestation — and the check is enforced inside the enclave, not here.
291
+ *
292
+ * Default false, which preserves the session-signing model: unlock once, sign
293
+ * for the session's lifetime with no further prompts.
294
+ *
295
+ * Two things worth knowing before turning this on:
296
+ * - Multi-step flows prompt per leg. An ERC-20 approve + swap is two
297
+ * prompts, not one.
298
+ * - The policy is declared by the client at session open, so it hardens a
299
+ * leaked session handle (useless without the authenticator) but a fully
300
+ * compromised page could open a session without it. Making it
301
+ * unbypassable requires the policy to come from the server per user or
302
+ * per project; that is not implemented yet.
303
+ */
304
+ requireAssertionPerSignature?: boolean;
286
305
  rpId?: string;
287
306
  rpName?: string;
288
307
  rpcUrls?: Record<string, string>;
package/dist/index.js CHANGED
@@ -2200,6 +2200,12 @@ var TeeClient = class {
2200
2200
  this.teeUrl = teeUrl;
2201
2201
  this.handle = null;
2202
2202
  this.attested = false;
2203
+ /** Mirrors what the signer confirmed for this session, not what we asked for. */
2204
+ this.assertionPerSignature = false;
2205
+ }
2206
+ /** Whether this session actually got per-signature assertions. */
2207
+ requiresAssertionPerSignature() {
2208
+ return this.assertionPerSignature;
2203
2209
  }
2204
2210
  async ensureAttested() {
2205
2211
  if (this.attested) return;
@@ -2215,16 +2221,20 @@ var TeeClient = class {
2215
2221
  * couldn't derive them (older signer build, or a transient share-fetch
2216
2222
  * failure); callers fall back to the backend's addresses in that case.
2217
2223
  */
2218
- async openSession(deviceShare, jwt) {
2224
+ async openSession(deviceShare, jwt, requireAssertionPerSignature = false) {
2219
2225
  await this.ensureAttested();
2220
2226
  const res = await fetch(`${this.teeUrl}/signer/session`, {
2221
2227
  method: "POST",
2222
2228
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
2223
- body: JSON.stringify({ deviceShare: bytesToBase64(deviceShare) })
2229
+ body: JSON.stringify({
2230
+ deviceShare: bytesToBase64(deviceShare),
2231
+ requireAssertionPerSignature
2232
+ })
2224
2233
  });
2225
2234
  if (!res.ok) throw new Error("session open failed");
2226
2235
  const body = await res.json();
2227
2236
  this.handle = body.handle;
2237
+ this.assertionPerSignature = body.requireAssertionPerSignature === true;
2228
2238
  return body.addresses ?? null;
2229
2239
  }
2230
2240
  /**
@@ -2315,14 +2325,33 @@ var TeeClient = class {
2315
2325
  hasSession() {
2316
2326
  return this.handle !== null;
2317
2327
  }
2318
- async sign(req) {
2328
+ /**
2329
+ * `jwt` is only needed when this session requires per-signature assertions —
2330
+ * it authenticates the challenge request. Passed per call rather than cached
2331
+ * on the instance deliberately: the backend's JWTs are shorter-lived than the
2332
+ * TEE session TTL, so a stored copy would go stale mid-session.
2333
+ */
2334
+ async sign(req, jwt) {
2319
2335
  if (!this.handle) throw new Error("no session");
2336
+ let gate = null;
2337
+ if (this.assertionPerSignature) {
2338
+ if (!jwt) throw new Error("sign requires a jwt when per-signature assertion is enabled");
2339
+ gate = await this.completeAssertion(jwt);
2340
+ }
2320
2341
  const res = await fetch(`${this.teeUrl}/signer/sign`, {
2321
2342
  method: "POST",
2322
2343
  headers: { "Content-Type": "application/json" },
2323
- body: JSON.stringify({ handle: this.handle, ...req })
2344
+ body: JSON.stringify({
2345
+ handle: this.handle,
2346
+ ...req,
2347
+ ...gate ? { ...gate, origin: window.location.origin } : {}
2348
+ })
2324
2349
  });
2325
2350
  if (res.status === 401) {
2351
+ const body = await res.clone().json().catch(() => ({}));
2352
+ if (body?.error === "assertion_required") {
2353
+ throw new ExportNotAuthorizedError();
2354
+ }
2326
2355
  this.handle = null;
2327
2356
  throw new SessionExpiredError();
2328
2357
  }
@@ -2345,10 +2374,11 @@ var TeeClient = class {
2345
2374
  // src/social/session.ts
2346
2375
  init_errors();
2347
2376
  var SessionManager = class {
2348
- constructor(events, durationMs, teeUrl) {
2377
+ constructor(events, durationMs, teeUrl, requireAssertionPerSignature = false) {
2349
2378
  this.events = events;
2350
2379
  this.durationMs = durationMs;
2351
2380
  this.teeUrl = teeUrl;
2381
+ this.requireAssertionPerSignature = requireAssertionPerSignature;
2352
2382
  this.tee = null;
2353
2383
  this.evmAddress = null;
2354
2384
  this.solanaAddress = null;
@@ -2368,7 +2398,18 @@ var SessionManager = class {
2368
2398
  */
2369
2399
  async start(deviceShare, jwt, fallback) {
2370
2400
  this.tee = new TeeClient(this.teeUrl);
2371
- const verified = await this.tee.openSession(deviceShare, jwt);
2401
+ const verified = await this.tee.openSession(
2402
+ deviceShare,
2403
+ jwt,
2404
+ this.requireAssertionPerSignature
2405
+ );
2406
+ if (this.requireAssertionPerSignature && !this.tee.requiresAssertionPerSignature()) {
2407
+ void this.tee.closeSession();
2408
+ this.tee = null;
2409
+ throw new Error(
2410
+ "requireAssertionPerSignature was requested but this signer does not support it \u2014 refusing to open a session with weaker authority than configured."
2411
+ );
2412
+ }
2372
2413
  const addresses = verified ?? fallback;
2373
2414
  if (!addresses) {
2374
2415
  this.tee = null;
@@ -2597,15 +2638,15 @@ async function verifyEmailCode(api, email, code) {
2597
2638
 
2598
2639
  // src/social/signers/evm.ts
2599
2640
  import { keccak256, Transaction, resolveProperties } from "ethers";
2600
- async function signEvmTransaction(tee, chain, tx) {
2641
+ async function signEvmTransaction(tee, chain, tx, jwt) {
2601
2642
  const resolved = await resolveProperties(tx);
2602
2643
  const unsigned = Transaction.from(resolved).unsignedSerialized;
2603
2644
  const payload = unsigned.startsWith("0x") ? unsigned.slice(2) : unsigned;
2604
2645
  const txHash = keccak256("0x" + payload);
2605
- return tee.sign({ chain, kind: "transaction", payload, txHash });
2646
+ return tee.sign({ chain, kind: "transaction", payload, txHash }, jwt);
2606
2647
  }
2607
- async function signEvmMessage(tee, chain, message) {
2608
- return tee.sign({ chain, kind: "message", payload: message });
2648
+ async function signEvmMessage(tee, chain, message, jwt) {
2649
+ return tee.sign({ chain, kind: "message", payload: message }, jwt);
2609
2650
  }
2610
2651
 
2611
2652
  // src/social/signers/solana.ts
@@ -2618,13 +2659,13 @@ function b64(bytes) {
2618
2659
  function unb64(s) {
2619
2660
  return Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
2620
2661
  }
2621
- async function signSolanaTransaction(tee, chain, tx) {
2662
+ async function signSolanaTransaction(tee, chain, tx, jwt) {
2622
2663
  const bytes = tx instanceof VTx ? tx.serialize() : tx.serialize({ requireAllSignatures: false });
2623
- const signed = await tee.sign({ chain, kind: "transaction", payload: b64(bytes) });
2664
+ const signed = await tee.sign({ chain, kind: "transaction", payload: b64(bytes) }, jwt);
2624
2665
  return unb64(signed);
2625
2666
  }
2626
- async function signSolanaMessage(tee, chain, message) {
2627
- const sig = await tee.sign({ chain, kind: "message", payload: b64(message) });
2667
+ async function signSolanaMessage(tee, chain, message, jwt) {
2668
+ const sig = await tee.sign({ chain, kind: "message", payload: b64(message) }, jwt);
2628
2669
  return unb64(sig);
2629
2670
  }
2630
2671
 
@@ -2897,7 +2938,12 @@ async function createDecaneConnect(config) {
2897
2938
  const events = new EventEmitter();
2898
2939
  const api = new ApiClient(baseUrl, config.apiKey, config.appId);
2899
2940
  const storage = new Storage();
2900
- const session = new SessionManager(events, durationMs, teeUrl);
2941
+ const session = new SessionManager(
2942
+ events,
2943
+ durationMs,
2944
+ teeUrl,
2945
+ config.requireAssertionPerSignature === true
2946
+ );
2901
2947
  let auth = null;
2902
2948
  let persisted = loadPersisted(PERSIST_KEY);
2903
2949
  async function pinSetup() {
@@ -3296,11 +3342,11 @@ async function createDecaneConnect(config) {
3296
3342
  async signMessage(req) {
3297
3343
  const tee = session.requireActive();
3298
3344
  if (req.chain.startsWith("evm:")) {
3299
- return signEvmMessage(tee, req.chain, req.message);
3345
+ return signEvmMessage(tee, req.chain, req.message, auth?.jwt);
3300
3346
  }
3301
3347
  if (req.chain.startsWith("solana:")) {
3302
3348
  const msg = typeof req.message === "string" ? new TextEncoder().encode(req.message) : req.message;
3303
- const sig = await signSolanaMessage(tee, req.chain, msg);
3349
+ const sig = await signSolanaMessage(tee, req.chain, msg, auth?.jwt);
3304
3350
  return bufferToB64u(sig);
3305
3351
  }
3306
3352
  throw new CallSequenceError(`Unsupported chain: ${req.chain}`);
@@ -3348,7 +3394,7 @@ async function createDecaneConnect(config) {
3348
3394
  maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? 0n
3349
3395
  } : { gasPrice: req.gasPrice ?? feeData.gasPrice ?? void 0 }
3350
3396
  };
3351
- const signedTx = await signEvmTransaction(tee, req.chain, tx);
3397
+ const signedTx = await signEvmTransaction(tee, req.chain, tx, auth?.jwt);
3352
3398
  const txResponse = await provider.broadcastTransaction(signedTx);
3353
3399
  return txResponse.hash;
3354
3400
  },
@@ -3361,7 +3407,7 @@ async function createDecaneConnect(config) {
3361
3407
  tx = Transaction2.from(serializedTx);
3362
3408
  }
3363
3409
  const solChain = config.chains.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
3364
- return signSolanaTransaction(tee, solChain, tx);
3410
+ return signSolanaTransaction(tee, solChain, tx, auth?.jwt);
3365
3411
  },
3366
3412
  async disconnect() {
3367
3413
  session.end();