kawasekit 0.3.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
 
@@ -836,7 +844,8 @@ function createLocalPolicyGatedSigner(params) {
836
844
  }
837
845
 
838
846
  // src/signer/mpc-2p-wire.ts
839
- var WIRE_VERSION = 1;
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(),
@@ -849,17 +858,23 @@ function toWireIntent(intent) {
849
858
  nonce: intent.nonce.toLowerCase()
850
859
  };
851
860
  }
852
- function canonicalIntentBytes(intent) {
861
+ function canonicalRequestBytes(env) {
862
+ const i = env.intent;
853
863
  const lines = [
854
- "kawasekit-mpc-2p/cosign-request/v2",
855
- `token=${intent.token.toLowerCase()}`,
856
- `chainId=${intent.chainId}`,
857
- `from=${intent.from.toLowerCase()}`,
858
- `to=${intent.to.toLowerCase()}`,
859
- `value=${intent.value}`,
860
- `validAfter=${intent.validAfter}`,
861
- `validBefore=${intent.validBefore}`,
862
- `nonce=${intent.nonce.toLowerCase()}`
864
+ "kawasekit-mpc-2p/cosign-request/v3",
865
+ "wireVersion=2",
866
+ `ceremonyId=${env.ceremonyId}`,
867
+ `ssid=${env.ssid}`,
868
+ `token=${i.token.toLowerCase()}`,
869
+ `chainId=${i.chainId}`,
870
+ `from=${i.from.toLowerCase()}`,
871
+ `to=${i.to.toLowerCase()}`,
872
+ `value=${i.value}`,
873
+ `validAfter=${i.validAfter}`,
874
+ `validBefore=${i.validBefore}`,
875
+ `nonce=${i.nonce.toLowerCase()}`,
876
+ `freshnessTs=${env.freshnessTs}`,
877
+ `freshnessNonce=${env.freshnessNonce.toLowerCase()}`
863
878
  ];
864
879
  return new TextEncoder().encode(lines.map((l) => `${l}
865
880
  `).join(""));
@@ -885,6 +900,12 @@ function createMpc2pPolicyGatedSigner(params) {
885
900
  const { agent, transport, authenticator, session } = params;
886
901
  const pinned = resolveAssetParam(params.asset);
887
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;
888
909
  const agentEoa = viem.getAddress(agent.groupEoa());
889
910
  if (agentEoa !== from) {
890
911
  throw new PolicyGatedSignerConfigError(
@@ -921,16 +942,59 @@ function createMpc2pPolicyGatedSigner(params) {
921
942
  nonce: intent.nonce
922
943
  }
923
944
  });
924
- const authTag = await authenticator.tag(canonicalIntentBytes(intent));
925
- const conn = await openConnection(transport);
926
- try {
927
- return await runCeremony(conn, agent, session.id, intent, digest, authTag);
928
- } 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));
929
967
  try {
930
- await conn.close();
931
- } 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;
932
993
  }
933
994
  }
995
+ throw lastTransient ?? new CoSignUnavailableError(
996
+ "co-sign ceremony budget exhausted before validBefore (W11) \u2014 no attempt could start"
997
+ );
934
998
  },
935
999
  describe() {
936
1000
  return {
@@ -949,23 +1013,32 @@ async function openConnection(transport) {
949
1013
  try {
950
1014
  return await transport.connect();
951
1015
  } catch (cause) {
952
- throw new CoSignUnavailableError("co-signer connection failed", { cause });
1016
+ throw new CoSignUnavailableError("co-signer connection failed", { cause, transient: true });
953
1017
  }
954
1018
  }
955
- async function runCeremony(conn, agent, sessionId, intent, digest, authTag) {
1019
+ async function runCeremony(ctx) {
1020
+ const { conn, agent, env, digest, from } = ctx;
956
1021
  const send = async (frame) => {
957
1022
  try {
958
- await conn.send(frame);
1023
+ await withDeadline(conn.send(frame), ctx.deadlineMs);
959
1024
  } catch (cause) {
960
- 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
+ });
961
1030
  }
962
1031
  };
963
1032
  const recv = async () => {
964
1033
  let frame;
965
1034
  try {
966
- frame = await conn.recv();
1035
+ frame = await withDeadline(conn.recv(), ctx.deadlineMs);
967
1036
  } catch (cause) {
968
- 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
+ });
969
1042
  }
970
1043
  if (frame.wire_version !== WIRE_VERSION) {
971
1044
  throw new CoSignUnavailableError(
@@ -977,9 +1050,13 @@ async function runCeremony(conn, agent, sessionId, intent, digest, authTag) {
977
1050
  await send({
978
1051
  wire_version: WIRE_VERSION,
979
1052
  kind: "request",
980
- session_id: sessionId,
981
- intent: toWireIntent(intent),
982
- auth_tag: authTag
1053
+ session_id: ctx.sessionId,
1054
+ ceremony_id: env.ceremonyId,
1055
+ ssid: env.ssid,
1056
+ intent: toWireIntent(env.intent),
1057
+ freshness_ts: env.freshnessTs,
1058
+ freshness_nonce: env.freshnessNonce,
1059
+ auth_tag: ctx.authTag
983
1060
  });
984
1061
  let first;
985
1062
  try {
@@ -993,6 +1070,12 @@ async function runCeremony(conn, agent, sessionId, intent, digest, authTag) {
993
1070
  const frame = await recv();
994
1071
  switch (frame.kind) {
995
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
+ }
996
1079
  let step;
997
1080
  try {
998
1081
  step = agent.step(frame.payload);
@@ -1007,15 +1090,16 @@ async function runCeremony(conn, agent, sessionId, intent, digest, authTag) {
1007
1090
  break;
1008
1091
  }
1009
1092
  case "result": {
1093
+ const signature = assembleSignature({ r: frame.r, s: frame.s, v: frame.v });
1010
1094
  if (agentSig === null) {
1011
- throw new CoSignUnavailableError(
1012
- "the backend returned a result before the agent derived a signature"
1013
- );
1095
+ await verifyRecovers(digest, signature, frame.s, from, "idempotent-replay");
1096
+ return { ok: true, signature, intent: env.intent };
1014
1097
  }
1015
1098
  if (frame.r.toLowerCase() !== agentSig.r.toLowerCase() || frame.s.toLowerCase() !== agentSig.s.toLowerCase() || frame.v !== agentSig.v) {
1016
1099
  throw new CoSignUnavailableError("the backend and agent derived different signatures");
1017
1100
  }
1018
- return { ok: true, signature: assembleSignature(agentSig), intent };
1101
+ await verifyRecovers(digest, signature, frame.s, from, "co-signed");
1102
+ return { ok: true, signature, intent: env.intent };
1019
1103
  }
1020
1104
  case "rejection": {
1021
1105
  if (POLICY_REASONS.has(frame.reason)) {
@@ -1034,6 +1118,52 @@ async function runCeremony(conn, agent, sessionId, intent, digest, authTag) {
1034
1118
  }
1035
1119
  }
1036
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
+ }
1037
1167
  function assembleSignature(sig) {
1038
1168
  const yParity = sig.v - 27;
1039
1169
  if (yParity !== 0 && yParity !== 1) {
@@ -3075,7 +3205,7 @@ exports.avalancheFuji = avalancheFuji;
3075
3205
  exports.buildPaymentRequiredResponse = buildPaymentRequiredResponse;
3076
3206
  exports.buildPaymentRequirements = buildPaymentRequirements;
3077
3207
  exports.cancelAuthorizationTypes = cancelAuthorizationTypes;
3078
- exports.canonicalIntentBytes = canonicalIntentBytes;
3208
+ exports.canonicalRequestBytes = canonicalRequestBytes;
3079
3209
  exports.chainIdToX402Network = chainIdToX402Network;
3080
3210
  exports.createAgentSmartAccount = createAgentSmartAccount;
3081
3211
  exports.createCoinbaseFacilitator = createCoinbaseFacilitator;