@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/README.md +18 -1
- package/dist/index.cjs +69 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +69 -22
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skill/keeperhub-wallet.skill.md +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
}
|