@keeperhub/wallet 0.1.4 → 0.1.6

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
@@ -272,8 +272,51 @@ type PaySignerOptions = {
272
272
  maxAttempts: number;
273
273
  };
274
274
  };
275
+ /**
276
+ * Retry options threaded through `pay()` and `fetch()` into the post-sign
277
+ * retry. Lets callers forward the original request body and headers so the
278
+ * paid workflow receives the same payload on the retry as on the 402 attempt
279
+ * -- otherwise a workflow whose input schema requires a body (e.g.
280
+ * `{address}` on `/api/mcp/workflows/<slug>/call`) rejects the retry with
281
+ * 400 "Invalid JSON body".
282
+ */
283
+ type PayRetryOptions = {
284
+ /**
285
+ * Body to re-send on the retry. Must be a type that can be sent twice --
286
+ * string, ArrayBuffer, Uint8Array, FormData, URLSearchParams, or Blob.
287
+ * ReadableStream bodies are NOT supported because the first fetch() already
288
+ * consumed the stream; pass a string/Buffer instead.
289
+ */
290
+ body?: RequestInit["body"];
291
+ /**
292
+ * Additional request headers to merge onto the retry (e.g. Content-Type).
293
+ * The payment auth header (PAYMENT-SIGNATURE or Authorization) is set by
294
+ * the signer and overrides any same-named header in this map.
295
+ */
296
+ headers?: RequestInit["headers"];
297
+ /** HTTP method for the retry. Defaults to "POST". */
298
+ method?: string;
299
+ };
275
300
  type PaymentSigner = {
276
- pay: (response: Response) => Promise<Response>;
301
+ /**
302
+ * Pays a 402 response and returns the post-payment retry Response.
303
+ * Non-402 responses are returned unchanged.
304
+ *
305
+ * Pass `options.body` (and usually `options.headers`) if the paid
306
+ * workflow's input schema requires a body -- `pay()` does not have access
307
+ * to the original request otherwise.
308
+ *
309
+ * For most agent code, prefer `signer.fetch(url, init)` which threads the
310
+ * body/headers automatically.
311
+ */
312
+ pay: (response: Response, options?: PayRetryOptions) => Promise<Response>;
313
+ /**
314
+ * `fetch(url, init)` wrapper: does the initial fetch, and on 402 calls
315
+ * `pay()` with `init.body` + `init.headers` so the retry carries the
316
+ * original payload. Returns whatever the retry (or first response, if not
317
+ * 402) returns. No-op for non-402 responses.
318
+ */
319
+ fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
277
320
  };
278
321
  declare function createPaymentSigner(opts?: PaySignerOptions): PaymentSigner;
279
322
  declare const paymentSigner: PaymentSigner;
package/dist/index.d.ts CHANGED
@@ -272,8 +272,51 @@ type PaySignerOptions = {
272
272
  maxAttempts: number;
273
273
  };
274
274
  };
275
+ /**
276
+ * Retry options threaded through `pay()` and `fetch()` into the post-sign
277
+ * retry. Lets callers forward the original request body and headers so the
278
+ * paid workflow receives the same payload on the retry as on the 402 attempt
279
+ * -- otherwise a workflow whose input schema requires a body (e.g.
280
+ * `{address}` on `/api/mcp/workflows/<slug>/call`) rejects the retry with
281
+ * 400 "Invalid JSON body".
282
+ */
283
+ type PayRetryOptions = {
284
+ /**
285
+ * Body to re-send on the retry. Must be a type that can be sent twice --
286
+ * string, ArrayBuffer, Uint8Array, FormData, URLSearchParams, or Blob.
287
+ * ReadableStream bodies are NOT supported because the first fetch() already
288
+ * consumed the stream; pass a string/Buffer instead.
289
+ */
290
+ body?: RequestInit["body"];
291
+ /**
292
+ * Additional request headers to merge onto the retry (e.g. Content-Type).
293
+ * The payment auth header (PAYMENT-SIGNATURE or Authorization) is set by
294
+ * the signer and overrides any same-named header in this map.
295
+ */
296
+ headers?: RequestInit["headers"];
297
+ /** HTTP method for the retry. Defaults to "POST". */
298
+ method?: string;
299
+ };
275
300
  type PaymentSigner = {
276
- pay: (response: Response) => Promise<Response>;
301
+ /**
302
+ * Pays a 402 response and returns the post-payment retry Response.
303
+ * Non-402 responses are returned unchanged.
304
+ *
305
+ * Pass `options.body` (and usually `options.headers`) if the paid
306
+ * workflow's input schema requires a body -- `pay()` does not have access
307
+ * to the original request otherwise.
308
+ *
309
+ * For most agent code, prefer `signer.fetch(url, init)` which threads the
310
+ * body/headers automatically.
311
+ */
312
+ pay: (response: Response, options?: PayRetryOptions) => Promise<Response>;
313
+ /**
314
+ * `fetch(url, init)` wrapper: does the initial fetch, and on 402 calls
315
+ * `pay()` with `init.body` + `init.headers` so the retry carries the
316
+ * original payload. Returns whatever the retry (or first response, if not
317
+ * 402) returns. No-op for non-402 responses.
318
+ */
319
+ fetch: (input: string | URL, init?: RequestInit) => Promise<Response>;
277
320
  };
278
321
  declare function createPaymentSigner(opts?: PaySignerOptions): PaymentSigner;
279
322
  declare const paymentSigner: PaymentSigner;
package/dist/index.js CHANGED
@@ -755,6 +755,19 @@ function parseMppChallenge(response) {
755
755
  // src/payment-signer.ts
756
756
  import { randomBytes } from "crypto";
757
757
 
758
+ // src/workflow-slug.ts
759
+ var KEEPERHUB_WORKFLOW_RE = /\/api\/mcp\/workflows\/([a-zA-Z0-9_-]+)\/call(?:\/?)(?:\?|$|#)/;
760
+ function extractKeeperHubWorkflowSlug(url) {
761
+ if (!url || url.length === 0) {
762
+ return { ok: false, reason: "EMPTY_URL" };
763
+ }
764
+ const match = KEEPERHUB_WORKFLOW_RE.exec(url);
765
+ if (!match || !match[1]) {
766
+ return { ok: false, reason: "URL_PATTERN_MISMATCH" };
767
+ }
768
+ return { ok: true, slug: match[1] };
769
+ }
770
+
758
771
  // src/x402-detect.ts
759
772
  function isX402Shape(value) {
760
773
  if (typeof value !== "object" || value === null) {
@@ -852,22 +865,33 @@ function createPaymentSigner(opts = {}) {
852
865
  }
853
866
  return result.signature;
854
867
  }
855
- async function payViaMpp(response, mpp, wallet) {
868
+ async function payViaMpp(response, mpp, wallet, retry) {
869
+ const slug = extractKeeperHubWorkflowSlug(response.url);
870
+ if (!slug.ok) {
871
+ throw new KeeperHubError(
872
+ "UNSUPPORTED_RECIPIENT",
873
+ `This wallet only signs payments for KeeperHub workflows. The 402 came from a URL that does not match /api/mcp/workflows/<slug>/call (reason: ${slug.reason}). See KEEP-311 for generic x402 support.`
874
+ );
875
+ }
856
876
  const client = clientFactory(wallet);
857
877
  const signature = await signOrPoll(client, {
858
878
  chain: "tempo",
879
+ workflowSlug: slug.slug,
859
880
  paymentChallenge: {
860
881
  kind: "mpp",
861
882
  serialized: mpp.serialized,
862
883
  chainId: TEMPO_CHAIN_ID
863
884
  }
864
885
  });
886
+ const headers = new Headers(retry?.headers);
887
+ headers.set("Authorization", `Payment ${signature}`);
865
888
  return fetchImpl(response.url, {
866
- method: "POST",
867
- headers: { Authorization: `Payment ${signature}` }
889
+ method: retry?.method ?? "POST",
890
+ headers,
891
+ body: retry?.body ?? void 0
868
892
  });
869
893
  }
870
- async function payViaX402(response, x402, wallet) {
894
+ async function payViaX402(response, x402, wallet, retry) {
871
895
  const accept = x402.accepts[0];
872
896
  if (!accept) {
873
897
  throw new KeeperHubError(
@@ -875,6 +899,13 @@ function createPaymentSigner(opts = {}) {
875
899
  "x402 challenge has no accepts entries"
876
900
  );
877
901
  }
902
+ const slug = extractKeeperHubWorkflowSlug(x402.resource.url || response.url);
903
+ if (!slug.ok) {
904
+ throw new KeeperHubError(
905
+ "UNSUPPORTED_RECIPIENT",
906
+ `This wallet only signs payments for KeeperHub workflows. The 402 came from a URL that does not match /api/mcp/workflows/<slug>/call (reason: ${slug.reason}). See KEEP-311 for generic x402 support.`
907
+ );
908
+ }
878
909
  const now = Math.floor(Date.now() / 1e3);
879
910
  const validAfter = now - VALID_AFTER_PAST_SLACK_SECONDS;
880
911
  const validBefore = now + accept.maxTimeoutSeconds;
@@ -882,6 +913,7 @@ function createPaymentSigner(opts = {}) {
882
913
  const client = clientFactory(wallet);
883
914
  const signature = await signOrPoll(client, {
884
915
  chain: "base",
916
+ workflowSlug: slug.slug,
885
917
  paymentChallenge: {
886
918
  kind: "x402",
887
919
  payTo: accept.payTo,
@@ -908,29 +940,44 @@ function createPaymentSigner(opts = {}) {
908
940
  JSON.stringify(paymentSigPayload)
909
941
  ).toString("base64");
910
942
  const retryUrl = x402.resource.url || response.url;
943
+ const headers = new Headers(retry?.headers);
944
+ headers.set("PAYMENT-SIGNATURE", paymentSigHeader);
911
945
  return fetchImpl(retryUrl, {
912
- method: "POST",
913
- headers: { "PAYMENT-SIGNATURE": paymentSigHeader }
946
+ method: retry?.method ?? "POST",
947
+ headers,
948
+ body: retry?.body ?? void 0
914
949
  });
915
950
  }
951
+ async function pay(response, options) {
952
+ if (response.status !== 402) {
953
+ return response;
954
+ }
955
+ const x402 = await parseX402Challenge(response);
956
+ const mpp = parseMppChallenge(response);
957
+ if (!(x402 || mpp)) {
958
+ return response;
959
+ }
960
+ const wallet = await walletLoader();
961
+ if (mpp) {
962
+ return payViaMpp(response, mpp, wallet, options);
963
+ }
964
+ if (x402) {
965
+ return payViaX402(response, x402, wallet, options);
966
+ }
967
+ return response;
968
+ }
916
969
  return {
917
- async pay(response) {
918
- if (response.status !== 402) {
919
- return response;
920
- }
921
- const x402 = await parseX402Challenge(response);
922
- const mpp = parseMppChallenge(response);
923
- if (!(x402 || mpp)) {
924
- return response;
925
- }
926
- const wallet = await walletLoader();
927
- if (mpp) {
928
- return payViaMpp(response, mpp, wallet);
970
+ pay,
971
+ async fetch(input, init) {
972
+ const first = await fetchImpl(input, init);
973
+ if (first.status !== 402) {
974
+ return first;
929
975
  }
930
- if (x402) {
931
- return payViaX402(response, x402, wallet);
932
- }
933
- return response;
976
+ return pay(first, {
977
+ body: init?.body ?? void 0,
978
+ headers: init?.headers,
979
+ method: init?.method
980
+ });
934
981
  }
935
982
  };
936
983
  }