kawasekit 0.4.0 → 0.5.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.cjs CHANGED
@@ -533,9 +533,17 @@ var PolicyGatedSignerConfigError = class extends Error {
533
533
  }
534
534
  };
535
535
  var CoSignUnavailableError = class extends Error {
536
+ /**
537
+ * `true` when the failure is the **transient transport class** (connect failed /
538
+ * connection dropped) — the only class the adapter's bounded retry replays
539
+ * (RFC m6-3a §4.7: never a delivered rejection, never a ban/identifiable-abort,
540
+ * never a protocol anomaly or timeout). Defaults to `false`.
541
+ */
542
+ transient;
536
543
  constructor(message, options) {
537
- super(message, options);
544
+ super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
538
545
  this.name = "CoSignUnavailableError";
546
+ this.transient = options?.transient ?? false;
539
547
  }
540
548
  };
541
549
 
@@ -837,6 +845,7 @@ function createLocalPolicyGatedSigner(params) {
837
845
 
838
846
  // src/signer/mpc-2p-wire.ts
839
847
  var WIRE_VERSION = 2;
848
+ var MAX_FRAME_BYTES = 8 * 1024 * 1024;
840
849
  function toWireIntent(intent) {
841
850
  return {
842
851
  token: intent.token.toLowerCase(),
@@ -891,6 +900,12 @@ function createMpc2pPolicyGatedSigner(params) {
891
900
  const { agent, transport, authenticator, session } = params;
892
901
  const pinned = resolveAssetParam(params.asset);
893
902
  const from = viem.getAddress(params.from);
903
+ const wire = params.wire ?? {};
904
+ const maxAttempts = Math.max(1, wire.maxAttempts ?? 2);
905
+ const ceremonyTimeoutMs = wire.ceremonyTimeoutMs ?? 3e4;
906
+ const minWindowSecs = wire.minWindowSecs ?? 30;
907
+ const clockSkewBudgetSecs = wire.clockSkewBudgetSecs ?? 30;
908
+ const maxFrameBytes = wire.maxFrameBytes ?? MAX_FRAME_BYTES;
894
909
  const agentEoa = viem.getAddress(agent.groupEoa());
895
910
  if (agentEoa !== from) {
896
911
  throw new PolicyGatedSignerConfigError(
@@ -927,23 +942,59 @@ function createMpc2pPolicyGatedSigner(params) {
927
942
  nonce: intent.nonce
928
943
  }
929
944
  });
930
- const env = {
931
- ceremonyId: globalThis.crypto.randomUUID(),
932
- ssid: globalThis.crypto.randomUUID(),
933
- intent,
934
- freshnessTs: Math.floor(Date.now() / 1e3),
935
- freshnessNonce: viem.toHex(globalThis.crypto.getRandomValues(new Uint8Array(16)))
936
- };
937
- const authTag = await authenticator.tag(canonicalRequestBytes(env));
938
- const conn = await openConnection(transport);
939
- try {
940
- return await runCeremony(conn, agent, session.id, env, digest, authTag);
941
- } finally {
945
+ const validBeforeSecs = Number(intent.validBefore);
946
+ const nowSecs = Math.floor(Date.now() / 1e3);
947
+ if (validBeforeSecs - nowSecs < minWindowSecs + clockSkewBudgetSecs) {
948
+ throw new CoSignUnavailableError(
949
+ `intent.validBefore (${validBeforeSecs}) leaves under the minimum ceremony window (${minWindowSecs}s + ${clockSkewBudgetSecs}s clock-skew budget) \u2014 refusing a co-sign that risks being born expired (W11)`
950
+ );
951
+ }
952
+ const deadlineMs = Math.min(
953
+ Date.now() + ceremonyTimeoutMs,
954
+ (validBeforeSecs - clockSkewBudgetSecs) * 1e3
955
+ );
956
+ let lastTransient;
957
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
958
+ if (Date.now() >= deadlineMs) break;
959
+ const env = {
960
+ ceremonyId: globalThis.crypto.randomUUID(),
961
+ ssid: globalThis.crypto.randomUUID(),
962
+ intent,
963
+ freshnessTs: Math.floor(Date.now() / 1e3),
964
+ freshnessNonce: viem.toHex(globalThis.crypto.getRandomValues(new Uint8Array(16)))
965
+ };
966
+ const authTag = await authenticator.tag(canonicalRequestBytes(env));
942
967
  try {
943
- await conn.close();
944
- } catch {
968
+ const conn = await openConnection(transport);
969
+ try {
970
+ return await runCeremony({
971
+ conn,
972
+ agent,
973
+ sessionId: session.id,
974
+ env,
975
+ digest,
976
+ authTag,
977
+ from,
978
+ deadlineMs,
979
+ maxFrameBytes
980
+ });
981
+ } finally {
982
+ try {
983
+ await conn.close();
984
+ } catch {
985
+ }
986
+ }
987
+ } catch (error) {
988
+ if (error instanceof CoSignUnavailableError && error.transient && attempt < maxAttempts) {
989
+ lastTransient = error;
990
+ continue;
991
+ }
992
+ throw error;
945
993
  }
946
994
  }
995
+ throw lastTransient ?? new CoSignUnavailableError(
996
+ "co-sign ceremony budget exhausted before validBefore (W11) \u2014 no attempt could start"
997
+ );
947
998
  },
948
999
  describe() {
949
1000
  return {
@@ -962,23 +1013,32 @@ async function openConnection(transport) {
962
1013
  try {
963
1014
  return await transport.connect();
964
1015
  } catch (cause) {
965
- throw new CoSignUnavailableError("co-signer connection failed", { cause });
1016
+ throw new CoSignUnavailableError("co-signer connection failed", { cause, transient: true });
966
1017
  }
967
1018
  }
968
- async function runCeremony(conn, agent, sessionId, env, digest, authTag) {
1019
+ async function runCeremony(ctx) {
1020
+ const { conn, agent, env, digest, from } = ctx;
969
1021
  const send = async (frame) => {
970
1022
  try {
971
- await conn.send(frame);
1023
+ await withDeadline(conn.send(frame), ctx.deadlineMs);
972
1024
  } catch (cause) {
973
- throw new CoSignUnavailableError("co-sign connection dropped while sending", { cause });
1025
+ if (cause instanceof CoSignUnavailableError) throw cause;
1026
+ throw new CoSignUnavailableError("co-sign connection dropped while sending", {
1027
+ cause,
1028
+ transient: true
1029
+ });
974
1030
  }
975
1031
  };
976
1032
  const recv = async () => {
977
1033
  let frame;
978
1034
  try {
979
- frame = await conn.recv();
1035
+ frame = await withDeadline(conn.recv(), ctx.deadlineMs);
980
1036
  } catch (cause) {
981
- throw new CoSignUnavailableError("co-sign connection dropped mid-ceremony", { cause });
1037
+ if (cause instanceof CoSignUnavailableError) throw cause;
1038
+ throw new CoSignUnavailableError("co-sign connection dropped mid-ceremony", {
1039
+ cause,
1040
+ transient: true
1041
+ });
982
1042
  }
983
1043
  if (frame.wire_version !== WIRE_VERSION) {
984
1044
  throw new CoSignUnavailableError(
@@ -990,13 +1050,13 @@ async function runCeremony(conn, agent, sessionId, env, digest, authTag) {
990
1050
  await send({
991
1051
  wire_version: WIRE_VERSION,
992
1052
  kind: "request",
993
- session_id: sessionId,
1053
+ session_id: ctx.sessionId,
994
1054
  ceremony_id: env.ceremonyId,
995
1055
  ssid: env.ssid,
996
1056
  intent: toWireIntent(env.intent),
997
1057
  freshness_ts: env.freshnessTs,
998
1058
  freshness_nonce: env.freshnessNonce,
999
- auth_tag: authTag
1059
+ auth_tag: ctx.authTag
1000
1060
  });
1001
1061
  let first;
1002
1062
  try {
@@ -1010,6 +1070,12 @@ async function runCeremony(conn, agent, sessionId, env, digest, authTag) {
1010
1070
  const frame = await recv();
1011
1071
  switch (frame.kind) {
1012
1072
  case "round": {
1073
+ const payloadBytes = (frame.payload.length - 2) / 2;
1074
+ if (payloadBytes > ctx.maxFrameBytes) {
1075
+ throw new CoSignUnavailableError(
1076
+ `co-signer sent a ${payloadBytes}-byte round frame (bound ${ctx.maxFrameBytes}) \u2014 refused pre-decode`
1077
+ );
1078
+ }
1013
1079
  let step;
1014
1080
  try {
1015
1081
  step = agent.step(frame.payload);
@@ -1024,15 +1090,16 @@ async function runCeremony(conn, agent, sessionId, env, digest, authTag) {
1024
1090
  break;
1025
1091
  }
1026
1092
  case "result": {
1093
+ const signature = assembleSignature({ r: frame.r, s: frame.s, v: frame.v });
1027
1094
  if (agentSig === null) {
1028
- throw new CoSignUnavailableError(
1029
- "the backend returned a result before the agent derived a signature"
1030
- );
1095
+ await verifyRecovers(digest, signature, frame.s, from, "idempotent-replay");
1096
+ return { ok: true, signature, intent: env.intent };
1031
1097
  }
1032
1098
  if (frame.r.toLowerCase() !== agentSig.r.toLowerCase() || frame.s.toLowerCase() !== agentSig.s.toLowerCase() || frame.v !== agentSig.v) {
1033
1099
  throw new CoSignUnavailableError("the backend and agent derived different signatures");
1034
1100
  }
1035
- return { ok: true, signature: assembleSignature(agentSig), intent: env.intent };
1101
+ await verifyRecovers(digest, signature, frame.s, from, "co-signed");
1102
+ return { ok: true, signature, intent: env.intent };
1036
1103
  }
1037
1104
  case "rejection": {
1038
1105
  if (POLICY_REASONS.has(frame.reason)) {
@@ -1051,6 +1118,52 @@ async function runCeremony(conn, agent, sessionId, env, digest, authTag) {
1051
1118
  }
1052
1119
  }
1053
1120
  }
1121
+ function withDeadline(promise, deadlineMs) {
1122
+ const remaining = deadlineMs - Date.now();
1123
+ if (remaining <= 0) {
1124
+ return Promise.reject(
1125
+ new CoSignUnavailableError(
1126
+ "co-sign ceremony timed out (the deadline fires before validBefore \u2014 W11)"
1127
+ )
1128
+ );
1129
+ }
1130
+ return new Promise((resolve, reject) => {
1131
+ const timer = setTimeout(() => {
1132
+ reject(
1133
+ new CoSignUnavailableError(
1134
+ "co-sign ceremony timed out (the deadline fires before validBefore \u2014 W11)"
1135
+ )
1136
+ );
1137
+ }, remaining);
1138
+ promise.then(
1139
+ (value) => {
1140
+ clearTimeout(timer);
1141
+ resolve(value);
1142
+ },
1143
+ (cause) => {
1144
+ clearTimeout(timer);
1145
+ reject(cause);
1146
+ }
1147
+ );
1148
+ });
1149
+ }
1150
+ var SECP256K1_HALF_N = 0x7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0n;
1151
+ async function verifyRecovers(digest, signature, s, from, what) {
1152
+ if (BigInt(s) > SECP256K1_HALF_N) {
1153
+ throw new CoSignUnavailableError(`the ${what} signature is not low-S (EIP-2)`);
1154
+ }
1155
+ let recovered;
1156
+ try {
1157
+ recovered = await viem.recoverAddress({ hash: digest, signature });
1158
+ } catch (cause) {
1159
+ throw new CoSignUnavailableError(`the ${what} signature failed to recover`, { cause });
1160
+ }
1161
+ if (viem.getAddress(recovered) !== from) {
1162
+ throw new CoSignUnavailableError(
1163
+ `the ${what} signature recovers to ${viem.getAddress(recovered)}, not the group EOA ${from}`
1164
+ );
1165
+ }
1166
+ }
1054
1167
  function assembleSignature(sig) {
1055
1168
  const yParity = sig.v - 27;
1056
1169
  if (yParity !== 0 && yParity !== 1) {