@runtab/mcp 0.0.1

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.
Files changed (82) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bootstrap.d.ts +18 -0
  3. package/dist/bootstrap.js +125 -0
  4. package/dist/cli-config.d.ts +11 -0
  5. package/dist/cli-config.js +59 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +33 -0
  8. package/dist/control-plane-origin.d.ts +4 -0
  9. package/dist/control-plane-origin.js +27 -0
  10. package/dist/detect.d.ts +10 -0
  11. package/dist/detect.js +74 -0
  12. package/dist/durable-mcp-payment.d.ts +40 -0
  13. package/dist/durable-mcp-payment.js +105 -0
  14. package/dist/durable-payment-fetch.d.ts +19 -0
  15. package/dist/durable-payment-fetch.js +117 -0
  16. package/dist/eip3009-authorization.d.ts +59 -0
  17. package/dist/eip3009-authorization.js +142 -0
  18. package/dist/errors.d.ts +4 -0
  19. package/dist/errors.js +7 -0
  20. package/dist/fetch-wire.d.ts +46 -0
  21. package/dist/fetch-wire.js +143 -0
  22. package/dist/fetch-wrapper.d.ts +24 -0
  23. package/dist/fetch-wrapper.js +70 -0
  24. package/dist/fetch.d.ts +3 -0
  25. package/dist/fetch.js +1 -0
  26. package/dist/index.d.ts +6 -0
  27. package/dist/index.js +4 -0
  28. package/dist/origin-context.d.ts +5 -0
  29. package/dist/origin-context.js +15 -0
  30. package/dist/paid-fetch-server.d.ts +49 -0
  31. package/dist/paid-fetch-server.js +104 -0
  32. package/dist/payment-authorization-state.d.ts +48 -0
  33. package/dist/payment-authorization-state.js +150 -0
  34. package/dist/payment-client.d.ts +4 -0
  35. package/dist/payment-client.js +14 -0
  36. package/dist/payment-correlation.d.ts +9 -0
  37. package/dist/payment-correlation.js +36 -0
  38. package/dist/payment-envelope-capacity.d.ts +2 -0
  39. package/dist/payment-envelope-capacity.js +28 -0
  40. package/dist/payment-envelope-disk.d.ts +5 -0
  41. package/dist/payment-envelope-disk.js +97 -0
  42. package/dist/payment-envelope-journal.d.ts +54 -0
  43. package/dist/payment-envelope-journal.js +116 -0
  44. package/dist/payment-envelope-lock.d.ts +5 -0
  45. package/dist/payment-envelope-lock.js +88 -0
  46. package/dist/payment-envelope-model.d.ts +32 -0
  47. package/dist/payment-envelope-model.js +123 -0
  48. package/dist/payment-envelope-store.d.ts +31 -0
  49. package/dist/payment-envelope-store.js +210 -0
  50. package/dist/payment-envelope.d.ts +17 -0
  51. package/dist/payment-envelope.js +145 -0
  52. package/dist/payment-idempotency.d.ts +2 -0
  53. package/dist/payment-idempotency.js +8 -0
  54. package/dist/payment-observation-reporter.d.ts +31 -0
  55. package/dist/payment-observation-reporter.js +151 -0
  56. package/dist/payment-profile.d.ts +3 -0
  57. package/dist/payment-profile.js +4 -0
  58. package/dist/payment-request-fingerprint.d.ts +5 -0
  59. package/dist/payment-request-fingerprint.js +54 -0
  60. package/dist/payment-settlement-observation.d.ts +9 -0
  61. package/dist/payment-settlement-observation.js +74 -0
  62. package/dist/payment-signal.d.ts +2 -0
  63. package/dist/payment-signal.js +8 -0
  64. package/dist/payment-target-network.d.ts +16 -0
  65. package/dist/payment-target-network.js +178 -0
  66. package/dist/payment-target-policy.d.ts +10 -0
  67. package/dist/payment-target-policy.js +75 -0
  68. package/dist/proxy.d.ts +47 -0
  69. package/dist/proxy.js +72 -0
  70. package/dist/remote-signer-http.d.ts +16 -0
  71. package/dist/remote-signer-http.js +182 -0
  72. package/dist/remote-signer.d.ts +34 -0
  73. package/dist/remote-signer.js +157 -0
  74. package/dist/resource-url.d.ts +5 -0
  75. package/dist/resource-url.js +36 -0
  76. package/dist/routing.d.ts +13 -0
  77. package/dist/routing.js +42 -0
  78. package/dist/runtime.d.ts +13 -0
  79. package/dist/runtime.js +92 -0
  80. package/dist/upstream.d.ts +43 -0
  81. package/dist/upstream.js +38 -0
  82. package/package.json +65 -0
@@ -0,0 +1,116 @@
1
+ import { readPaymentAuthorizationState } from "./payment-authorization-state.js";
2
+ import { parsePaymentEnvelope } from "./payment-envelope.js";
3
+ import { PaymentEnvelopeStoreError } from "./payment-envelope-store.js";
4
+ import { parsePaymentSettlementObservation } from "./payment-settlement-observation.js";
5
+ export class PaymentIdempotencyRequiredError extends Error {
6
+ code = "PAYMENT_IDEMPOTENCY_KEY_REQUIRED";
7
+ constructor() {
8
+ super("A payment idempotency key is required for paid requests.");
9
+ this.name = "PaymentIdempotencyRequiredError";
10
+ }
11
+ }
12
+ export class PaymentReconciliationRequiredError extends Error {
13
+ code = "PAYMENT_RECONCILIATION_REQUIRED";
14
+ constructor() {
15
+ super("A previous payment must be reconciled before a new authorization can be created.");
16
+ this.name = "PaymentReconciliationRequiredError";
17
+ }
18
+ }
19
+ export class PaymentEnvelopeJournal {
20
+ #address;
21
+ #authorizationState;
22
+ #nowSeconds;
23
+ #paymentProfile;
24
+ #signer;
25
+ #store;
26
+ constructor(options) {
27
+ this.#address = options.address;
28
+ this.#authorizationState = options.authorizationState ?? readPaymentAuthorizationState;
29
+ this.#nowSeconds = options.nowSeconds ?? (() => Math.floor(Date.now() / 1_000));
30
+ this.#paymentProfile = options.paymentProfile;
31
+ this.#signer = options.signer;
32
+ this.#store = options.store;
33
+ }
34
+ async #removeIfExpiredAndUnused(idempotencyKey, requestFingerprint, envelope) {
35
+ if (envelope.state === "settled" || envelope.validBefore > this.#nowSeconds())
36
+ return false;
37
+ let authorizationState;
38
+ try {
39
+ return await this.#store.removeIfPending(idempotencyKey, requestFingerprint, async (current) => {
40
+ if (current.validBefore > this.#nowSeconds())
41
+ return false;
42
+ const parsed = await parsePaymentEnvelope(current.paymentSignature, this.#address, this.#paymentProfile);
43
+ authorizationState = await this.#authorizationState(parsed);
44
+ if (authorizationState !== "unused")
45
+ return false;
46
+ return this.#signer.reconcileExpiredPayment(current.receiptId);
47
+ });
48
+ }
49
+ catch (error) {
50
+ if (error instanceof PaymentEnvelopeStoreError &&
51
+ error.code === "PAYMENT_ENVELOPE_CHAIN_STATE_UNRESOLVED" &&
52
+ authorizationState === "used") {
53
+ return false;
54
+ }
55
+ throw error;
56
+ }
57
+ }
58
+ async load(idempotencyKey, requestFingerprint) {
59
+ const pending = await this.#store.findUnsettled();
60
+ if (pending && pending.idempotencyKey !== idempotencyKey) {
61
+ const removed = await this.#removeIfExpiredAndUnused(pending.idempotencyKey, pending.record.requestFingerprint, pending.record);
62
+ if (!removed)
63
+ throw new PaymentReconciliationRequiredError();
64
+ }
65
+ const existing = await this.#store.find(idempotencyKey, requestFingerprint);
66
+ if (!existing)
67
+ return null;
68
+ const removed = await this.#removeIfExpiredAndUnused(idempotencyKey, requestFingerprint, existing);
69
+ return removed ? null : existing;
70
+ }
71
+ async getOrCreate(idempotencyKey, requestFingerprint, factory) {
72
+ try {
73
+ return (await this.#store.getOrCreate(idempotencyKey, requestFingerprint, factory)).record;
74
+ }
75
+ catch (error) {
76
+ if (error instanceof PaymentEnvelopeStoreError && error.code === "PAYMENT_ENVELOPE_PENDING") {
77
+ throw new PaymentReconciliationRequiredError();
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+ async parse(envelope) {
83
+ const parsed = await parsePaymentEnvelope(envelope.paymentSignature, this.#address, this.#paymentProfile);
84
+ if (parsed.validBefore !== envelope.validBefore) {
85
+ throw new Error("Stored payment envelope is invalid");
86
+ }
87
+ this.#signer.restorePaymentCorrelation(parsed.signature, envelope.receiptId, envelope.validBefore);
88
+ return parsed;
89
+ }
90
+ async recordObservation(idempotencyKey, requestFingerprint, parsed, value) {
91
+ const observation = parsePaymentSettlementObservation(value, {
92
+ amount: parsed.payload.accepted.amount,
93
+ network: parsed.payload.accepted.network,
94
+ payer: this.#address,
95
+ });
96
+ if (!observation)
97
+ return null;
98
+ return this.#store.markObserved(idempotencyKey, requestFingerprint, observation);
99
+ }
100
+ async reconcileObserved(idempotencyKey, requestFingerprint, envelope, parsed) {
101
+ if (!envelope.settlementObservation)
102
+ return { status: "ignored", verified: false };
103
+ const result = await this.#signer.reportPaymentObservation({
104
+ paymentPayload: parsed.payload,
105
+ requirements: parsed.payload.accepted,
106
+ settleResponse: envelope.settlementObservation,
107
+ });
108
+ if (result.status === "settled" && result.verified) {
109
+ await this.#store.markSettled(idempotencyKey, requestFingerprint);
110
+ }
111
+ return result;
112
+ }
113
+ markSettled(idempotencyKey, requestFingerprint) {
114
+ return this.#store.markSettled(idempotencyKey, requestFingerprint);
115
+ }
116
+ }
@@ -0,0 +1,5 @@
1
+ export interface PaymentEnvelopeLockOptions {
2
+ lockRetryDelayMs: number;
3
+ lockTimeoutMs: number;
4
+ }
5
+ export declare function withPaymentEnvelopeLock<T>(directory: string, options: PaymentEnvelopeLockOptions, deadline: number, task: () => Promise<T>): Promise<T>;
@@ -0,0 +1,88 @@
1
+ import { constants } from "node:fs";
2
+ import { open } from "node:fs/promises";
3
+ import { performance } from "node:perf_hooks";
4
+ import { tryLock, unlock } from "fs-native-extensions";
5
+ import { PaymentEnvelopeStoreError } from "./payment-envelope-model.js";
6
+ const LOCK_FILE = ".payment-envelopes.lock";
7
+ function lockFailure() {
8
+ return new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_CORRUPT", "The durable payment envelope lock failed safely.");
9
+ }
10
+ function pause(milliseconds) {
11
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
12
+ }
13
+ async function openAnchor(directory) {
14
+ let handle;
15
+ try {
16
+ handle = await open(`${directory}/${LOCK_FILE}`, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600);
17
+ const metadata = await handle.stat();
18
+ if (!metadata.isFile())
19
+ throw lockFailure();
20
+ await handle.chmod(0o600);
21
+ return handle;
22
+ }
23
+ catch (error) {
24
+ await handle?.close().catch(() => undefined);
25
+ if (error instanceof PaymentEnvelopeStoreError)
26
+ throw error;
27
+ throw lockFailure();
28
+ }
29
+ }
30
+ async function acquire(directory, options, deadline) {
31
+ const handle = await openAnchor(directory);
32
+ try {
33
+ while (true) {
34
+ if (performance.now() >= deadline) {
35
+ throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_LOCK_TIMEOUT", "The durable payment envelope lock timed out.");
36
+ }
37
+ let acquired;
38
+ try {
39
+ acquired = tryLock(handle.fd);
40
+ }
41
+ catch {
42
+ throw lockFailure();
43
+ }
44
+ if (acquired)
45
+ return handle;
46
+ const remaining = deadline - performance.now();
47
+ if (remaining <= 0) {
48
+ throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_LOCK_TIMEOUT", "The durable payment envelope lock timed out.");
49
+ }
50
+ await pause(Math.min(options.lockRetryDelayMs, remaining));
51
+ }
52
+ }
53
+ catch (error) {
54
+ await handle.close().catch(() => undefined);
55
+ throw error;
56
+ }
57
+ }
58
+ export async function withPaymentEnvelopeLock(directory, options, deadline, task) {
59
+ const handle = await acquire(directory, options, deadline);
60
+ let completed = false;
61
+ let releaseError;
62
+ let taskError;
63
+ let value;
64
+ try {
65
+ value = await task();
66
+ completed = true;
67
+ }
68
+ catch (error) {
69
+ taskError = error;
70
+ }
71
+ try {
72
+ unlock(handle.fd);
73
+ }
74
+ catch {
75
+ releaseError = lockFailure();
76
+ }
77
+ try {
78
+ await handle.close();
79
+ }
80
+ catch {
81
+ releaseError = lockFailure();
82
+ }
83
+ if (releaseError)
84
+ throw releaseError;
85
+ if (!completed)
86
+ throw taskError;
87
+ return value;
88
+ }
@@ -0,0 +1,32 @@
1
+ import { type PaymentSettlementObservation } from "./payment-settlement-observation.js";
2
+ export declare const MAX_PAYMENT_ENVELOPE_RECORDS = 1024;
3
+ export declare const MAX_PAYMENT_ENVELOPE_STORE_BYTES: number;
4
+ export type PaymentEnvelopeState = "observed" | "pending" | "settled";
5
+ export interface PaymentEnvelopeRecord {
6
+ readonly createdAt: string;
7
+ readonly paymentSignature: string;
8
+ readonly receiptId: string;
9
+ readonly requestFingerprint: string;
10
+ readonly settlementObservation?: PaymentSettlementObservation;
11
+ readonly state: PaymentEnvelopeState;
12
+ readonly updatedAt: string;
13
+ readonly validBefore: number;
14
+ }
15
+ export interface NewPaymentEnvelope {
16
+ readonly paymentSignature: string;
17
+ readonly receiptId: string;
18
+ readonly validBefore: number;
19
+ }
20
+ export interface PaymentEnvelopeDocument {
21
+ records: Record<string, PaymentEnvelopeRecord>;
22
+ version: 1;
23
+ }
24
+ export type PaymentEnvelopeStoreErrorCode = "PAYMENT_ENVELOPE_CAPACITY" | "PAYMENT_ENVELOPE_CHAIN_STATE_UNRESOLVED" | "PAYMENT_ENVELOPE_CONFLICT" | "PAYMENT_ENVELOPE_CORRUPT" | "PAYMENT_ENVELOPE_INVALID_INPUT" | "PAYMENT_ENVELOPE_LOCK_TIMEOUT" | "PAYMENT_ENVELOPE_NOT_FOUND" | "PAYMENT_ENVELOPE_OVERSIZE" | "PAYMENT_ENVELOPE_PENDING";
25
+ export declare class PaymentEnvelopeStoreError extends Error {
26
+ readonly code: PaymentEnvelopeStoreErrorCode;
27
+ constructor(code: PaymentEnvelopeStoreErrorCode, message: string);
28
+ }
29
+ export declare function validatePaymentIdempotencyKey(value: string): string;
30
+ export declare function validateRequestFingerprint(value: string): string;
31
+ export declare function validateNewPaymentEnvelope(value: NewPaymentEnvelope): NewPaymentEnvelope;
32
+ export declare function parsePaymentEnvelopeDocument(value: unknown): PaymentEnvelopeDocument;
@@ -0,0 +1,123 @@
1
+ import { parsePaymentSettlementObservation, } from "./payment-settlement-observation.js";
2
+ export const MAX_PAYMENT_ENVELOPE_RECORDS = 1_024;
3
+ export const MAX_PAYMENT_ENVELOPE_STORE_BYTES = 4 * 1_024 * 1_024;
4
+ export class PaymentEnvelopeStoreError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = "PaymentEnvelopeStoreError";
10
+ }
11
+ }
12
+ function invalid() {
13
+ throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_CORRUPT", "The durable payment envelope store is invalid.");
14
+ }
15
+ function record(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ function exactKeys(value, expected) {
19
+ const actual = Object.keys(value).sort();
20
+ const wanted = [...expected].sort();
21
+ if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
22
+ invalid();
23
+ }
24
+ }
25
+ function canonicalTimestamp(value) {
26
+ if (typeof value !== "string")
27
+ return false;
28
+ const timestamp = new Date(value);
29
+ return Number.isFinite(timestamp.valueOf()) && timestamp.toISOString() === value;
30
+ }
31
+ export function validatePaymentIdempotencyKey(value) {
32
+ if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value)) {
33
+ throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_INVALID_INPUT", "The payment idempotency key is invalid.");
34
+ }
35
+ return value;
36
+ }
37
+ export function validateRequestFingerprint(value) {
38
+ if (!/^[0-9a-f]{64}$/.test(value)) {
39
+ throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_INVALID_INPUT", "The payment request fingerprint is invalid.");
40
+ }
41
+ return value;
42
+ }
43
+ function validPaymentSignature(value) {
44
+ return typeof value === "string" && value.length <= 32_768 && /^[A-Za-z0-9+/_=-]+$/.test(value);
45
+ }
46
+ function validReceiptId(value) {
47
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value);
48
+ }
49
+ function validBefore(value) {
50
+ return Number.isSafeInteger(value) && typeof value === "number" && value > 0;
51
+ }
52
+ export function validateNewPaymentEnvelope(value) {
53
+ if (!record(value) ||
54
+ !validPaymentSignature(value.paymentSignature) ||
55
+ !validReceiptId(value.receiptId) ||
56
+ !validBefore(value.validBefore)) {
57
+ throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_INVALID_INPUT", "The new payment envelope is invalid.");
58
+ }
59
+ exactKeys(value, ["paymentSignature", "receiptId", "validBefore"]);
60
+ return value;
61
+ }
62
+ function validateStoredRecord(value) {
63
+ if (!record(value))
64
+ invalid();
65
+ const baseKeys = [
66
+ "createdAt",
67
+ "paymentSignature",
68
+ "receiptId",
69
+ "requestFingerprint",
70
+ "state",
71
+ "updatedAt",
72
+ "validBefore",
73
+ ];
74
+ const hasObservation = Object.hasOwn(value, "settlementObservation");
75
+ exactKeys(value, hasObservation ? [...baseKeys, "settlementObservation"] : baseKeys);
76
+ if (!canonicalTimestamp(value.createdAt) ||
77
+ !validPaymentSignature(value.paymentSignature) ||
78
+ !validReceiptId(value.receiptId) ||
79
+ typeof value.requestFingerprint !== "string" ||
80
+ !/^[0-9a-f]{64}$/.test(value.requestFingerprint) ||
81
+ (value.state !== "pending" && value.state !== "observed" && value.state !== "settled") ||
82
+ !canonicalTimestamp(value.updatedAt) ||
83
+ !validBefore(value.validBefore)) {
84
+ invalid();
85
+ }
86
+ const observation = hasObservation
87
+ ? parsePaymentSettlementObservation(value.settlementObservation)
88
+ : null;
89
+ if ((hasObservation && !observation) ||
90
+ (value.state === "observed" && !observation) ||
91
+ (value.state === "pending" && hasObservation)) {
92
+ invalid();
93
+ }
94
+ if (observation)
95
+ value.settlementObservation = observation;
96
+ return value;
97
+ }
98
+ export function parsePaymentEnvelopeDocument(value) {
99
+ if (!record(value))
100
+ invalid();
101
+ exactKeys(value, ["records", "version"]);
102
+ if (value.version !== 1 || !record(value.records))
103
+ invalid();
104
+ const entries = Object.entries(value.records);
105
+ if (entries.length > MAX_PAYMENT_ENVELOPE_RECORDS)
106
+ invalid();
107
+ const records = {};
108
+ let pending = 0;
109
+ for (const [key, stored] of entries) {
110
+ try {
111
+ validatePaymentIdempotencyKey(key);
112
+ }
113
+ catch {
114
+ invalid();
115
+ }
116
+ records[key] = validateStoredRecord(stored);
117
+ if (records[key].state !== "settled")
118
+ pending += 1;
119
+ }
120
+ if (pending > 1)
121
+ invalid();
122
+ return { records, version: 1 };
123
+ }
@@ -0,0 +1,31 @@
1
+ import { type NewPaymentEnvelope, type PaymentEnvelopeRecord } from "./payment-envelope-model.js";
2
+ import { type PaymentSettlementObservation } from "./payment-settlement-observation.js";
3
+ export type { NewPaymentEnvelope, PaymentEnvelopeRecord } from "./payment-envelope-model.js";
4
+ export { PaymentEnvelopeStoreError } from "./payment-envelope-model.js";
5
+ export interface PaymentEnvelopeStoreOptions {
6
+ lockRetryDelayMs?: number;
7
+ lockTimeoutMs?: number;
8
+ now?: () => Date;
9
+ }
10
+ export declare function agentPaymentStateDirectory(stateRoot: string, address: `0x${string}`): string;
11
+ export declare function defaultPaymentStateDirectory(environment?: Readonly<Record<string, string | undefined>>, home?: string): string;
12
+ export declare class PaymentEnvelopeStore {
13
+ #private;
14
+ constructor(address: `0x${string}`, stateDirectory: string, options?: PaymentEnvelopeStoreOptions);
15
+ find(idempotencyKey: string, requestFingerprint: string): Promise<PaymentEnvelopeRecord | null>;
16
+ findUnsettled(): Promise<{
17
+ idempotencyKey: string;
18
+ record: PaymentEnvelopeRecord;
19
+ } | null>;
20
+ findPending(): Promise<{
21
+ idempotencyKey: string;
22
+ record: PaymentEnvelopeRecord;
23
+ } | null>;
24
+ getOrCreate(idempotencyKey: string, requestFingerprint: string, factory: () => Promise<NewPaymentEnvelope>): Promise<{
25
+ created: boolean;
26
+ record: PaymentEnvelopeRecord;
27
+ }>;
28
+ markSettled(idempotencyKey: string, requestFingerprint: string): Promise<PaymentEnvelopeRecord>;
29
+ markObserved(idempotencyKey: string, requestFingerprint: string, value: PaymentSettlementObservation): Promise<PaymentEnvelopeRecord>;
30
+ removeIfPending(idempotencyKey: string, requestFingerprint: string, proveUnused: (record: PaymentEnvelopeRecord) => Promise<boolean>): Promise<boolean>;
31
+ }
@@ -0,0 +1,210 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ import { performance } from "node:perf_hooks";
4
+ import { assertNewPaymentEnvelopeCapacity } from "./payment-envelope-capacity.js";
5
+ import { preparePaymentEnvelopeDirectory, readPaymentEnvelopeDocument, writePaymentEnvelopeDocument, } from "./payment-envelope-disk.js";
6
+ import { withPaymentEnvelopeLock } from "./payment-envelope-lock.js";
7
+ import { PaymentEnvelopeStoreError, validateNewPaymentEnvelope, validatePaymentIdempotencyKey, validateRequestFingerprint, } from "./payment-envelope-model.js";
8
+ import { parsePaymentSettlementObservation, } from "./payment-settlement-observation.js";
9
+ export { PaymentEnvelopeStoreError } from "./payment-envelope-model.js";
10
+ const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
11
+ function invalidConfiguration(message) {
12
+ throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_INVALID_INPUT", message);
13
+ }
14
+ function positiveInteger(value, name) {
15
+ if (!Number.isSafeInteger(value) || value < 1)
16
+ invalidConfiguration(`${name} is invalid.`);
17
+ return value;
18
+ }
19
+ export function agentPaymentStateDirectory(stateRoot, address) {
20
+ if (!ADDRESS.test(address))
21
+ invalidConfiguration("The agent address is invalid.");
22
+ if (!stateRoot || stateRoot.trim() !== stateRoot) {
23
+ invalidConfiguration("The payment state directory is invalid.");
24
+ }
25
+ return join(resolve(stateRoot), address.toLowerCase());
26
+ }
27
+ export function defaultPaymentStateDirectory(environment = process.env, home = homedir()) {
28
+ const configured = environment.TAB_STATE_DIRECTORY;
29
+ if (configured !== undefined) {
30
+ if (!configured || configured.trim() !== configured) {
31
+ invalidConfiguration("TAB_STATE_DIRECTORY is invalid.");
32
+ }
33
+ return resolve(configured);
34
+ }
35
+ const xdg = environment.XDG_STATE_HOME;
36
+ if (xdg !== undefined && (!xdg || xdg.trim() !== xdg || !isAbsolute(xdg))) {
37
+ invalidConfiguration("XDG_STATE_HOME is invalid.");
38
+ }
39
+ return join(xdg ?? join(home, ".local", "state"), "tab", "leash");
40
+ }
41
+ function clone(record) {
42
+ return {
43
+ ...record,
44
+ ...(record.settlementObservation
45
+ ? { settlementObservation: { ...record.settlementObservation } }
46
+ : {}),
47
+ };
48
+ }
49
+ export class PaymentEnvelopeStore {
50
+ #directory;
51
+ #lockOptions;
52
+ #now;
53
+ constructor(address, stateDirectory, options = {}) {
54
+ this.#directory = agentPaymentStateDirectory(stateDirectory, address);
55
+ this.#lockOptions = {
56
+ lockRetryDelayMs: positiveInteger(options.lockRetryDelayMs ?? 25, "Lock retry delay"),
57
+ lockTimeoutMs: positiveInteger(options.lockTimeoutMs ?? 5_000, "Lock timeout"),
58
+ };
59
+ this.#now = options.now ?? (() => new Date());
60
+ }
61
+ async #locked(task) {
62
+ const lockDeadline = performance.now() + this.#lockOptions.lockTimeoutMs;
63
+ await preparePaymentEnvelopeDirectory(this.#directory);
64
+ return withPaymentEnvelopeLock(this.#directory, this.#lockOptions, lockDeadline, async () => {
65
+ const document = await readPaymentEnvelopeDocument(this.#directory);
66
+ return task(document);
67
+ });
68
+ }
69
+ async find(idempotencyKey, requestFingerprint) {
70
+ const key = validatePaymentIdempotencyKey(idempotencyKey);
71
+ const fingerprint = validateRequestFingerprint(requestFingerprint);
72
+ return this.#locked(async (document) => {
73
+ const existing = this.#existing(document, key);
74
+ if (!existing)
75
+ return null;
76
+ if (existing.requestFingerprint !== fingerprint)
77
+ this.#conflict();
78
+ return clone(existing);
79
+ });
80
+ }
81
+ async findUnsettled() {
82
+ return this.#locked(async (document) => {
83
+ const pending = Object.entries(document.records).find(([, value]) => value.state !== "settled");
84
+ return pending ? { idempotencyKey: pending[0], record: clone(pending[1]) } : null;
85
+ });
86
+ }
87
+ findPending() {
88
+ return this.findUnsettled();
89
+ }
90
+ async getOrCreate(idempotencyKey, requestFingerprint, factory) {
91
+ const key = validatePaymentIdempotencyKey(idempotencyKey);
92
+ const fingerprint = validateRequestFingerprint(requestFingerprint);
93
+ if (typeof factory !== "function")
94
+ invalidConfiguration("The envelope factory is invalid.");
95
+ return this.#locked(async (document) => {
96
+ const existing = this.#existing(document, key);
97
+ if (existing) {
98
+ if (existing.requestFingerprint !== fingerprint)
99
+ this.#conflict();
100
+ return { created: false, record: clone(existing) };
101
+ }
102
+ if (Object.values(document.records).some((candidate) => candidate.state !== "settled")) {
103
+ throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_PENDING", "Another payment envelope is still pending.");
104
+ }
105
+ const timestamp = this.#timestamp();
106
+ assertNewPaymentEnvelopeCapacity(document, key, fingerprint, timestamp);
107
+ const created = validateNewPaymentEnvelope(await factory());
108
+ const stored = {
109
+ createdAt: timestamp,
110
+ paymentSignature: created.paymentSignature,
111
+ receiptId: created.receiptId,
112
+ requestFingerprint: fingerprint,
113
+ state: "pending",
114
+ updatedAt: timestamp,
115
+ validBefore: created.validBefore,
116
+ };
117
+ document.records[key] = stored;
118
+ await writePaymentEnvelopeDocument(this.#directory, document);
119
+ return { created: true, record: clone(stored) };
120
+ });
121
+ }
122
+ async markSettled(idempotencyKey, requestFingerprint) {
123
+ const key = validatePaymentIdempotencyKey(idempotencyKey);
124
+ const fingerprint = validateRequestFingerprint(requestFingerprint);
125
+ return this.#locked(async (document) => {
126
+ const existing = this.#required(document, key, fingerprint);
127
+ if (existing.state === "settled")
128
+ return clone(existing);
129
+ const settled = {
130
+ ...existing,
131
+ state: "settled",
132
+ updatedAt: this.#timestamp(),
133
+ };
134
+ document.records[key] = settled;
135
+ await writePaymentEnvelopeDocument(this.#directory, document);
136
+ return clone(settled);
137
+ });
138
+ }
139
+ async markObserved(idempotencyKey, requestFingerprint, value) {
140
+ const key = validatePaymentIdempotencyKey(idempotencyKey);
141
+ const fingerprint = validateRequestFingerprint(requestFingerprint);
142
+ const settlementObservation = parsePaymentSettlementObservation(value);
143
+ if (!settlementObservation)
144
+ invalidConfiguration("The settlement observation is invalid.");
145
+ return this.#locked(async (document) => {
146
+ const existing = this.#required(document, key, fingerprint);
147
+ if (existing.state === "settled")
148
+ return clone(existing);
149
+ const observed = {
150
+ ...existing,
151
+ settlementObservation,
152
+ state: "observed",
153
+ updatedAt: this.#timestamp(),
154
+ };
155
+ document.records[key] = observed;
156
+ await writePaymentEnvelopeDocument(this.#directory, document);
157
+ return clone(observed);
158
+ });
159
+ }
160
+ async removeIfPending(idempotencyKey, requestFingerprint, proveUnused) {
161
+ const key = validatePaymentIdempotencyKey(idempotencyKey);
162
+ const fingerprint = validateRequestFingerprint(requestFingerprint);
163
+ if (typeof proveUnused !== "function")
164
+ invalidConfiguration("The chain proof is invalid.");
165
+ return this.#locked(async (document) => {
166
+ const existing = this.#existing(document, key);
167
+ if (!existing)
168
+ return false;
169
+ if (existing.requestFingerprint !== fingerprint)
170
+ this.#conflict();
171
+ if (existing.state === "settled")
172
+ return false;
173
+ let unused = false;
174
+ try {
175
+ unused = (await proveUnused(clone(existing))) === true;
176
+ }
177
+ catch {
178
+ // The proof callback receives the exact header; its errors must not escape unsanitized.
179
+ }
180
+ if (!unused) {
181
+ throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_CHAIN_STATE_UNRESOLVED", "Independent chain evidence did not prove the pending authorization unused.");
182
+ }
183
+ delete document.records[key];
184
+ await writePaymentEnvelopeDocument(this.#directory, document);
185
+ return true;
186
+ });
187
+ }
188
+ #required(document, key, fingerprint) {
189
+ const existing = this.#existing(document, key);
190
+ if (!existing) {
191
+ throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_NOT_FOUND", "The payment envelope was not found.");
192
+ }
193
+ if (existing.requestFingerprint !== fingerprint)
194
+ this.#conflict();
195
+ return existing;
196
+ }
197
+ #existing(document, key) {
198
+ return Object.hasOwn(document.records, key) ? document.records[key] : undefined;
199
+ }
200
+ #conflict() {
201
+ throw new PaymentEnvelopeStoreError("PAYMENT_ENVELOPE_CONFLICT", "The payment idempotency key belongs to a different request.");
202
+ }
203
+ #timestamp() {
204
+ const value = this.#now();
205
+ if (!(value instanceof Date) || !Number.isFinite(value.valueOf())) {
206
+ invalidConfiguration("The payment envelope clock is invalid.");
207
+ }
208
+ return value.toISOString();
209
+ }
210
+ }
@@ -0,0 +1,17 @@
1
+ import type { PaymentPayload } from "@x402/core/types";
2
+ import type { NewPaymentEnvelope } from "./payment-envelope-model.js";
3
+ import type { PaymentProfile } from "./payment-profile.js";
4
+ import type { TabRemoteSigner } from "./remote-signer.js";
5
+ export declare class PaymentEnvelopeValidationError extends Error {
6
+ constructor();
7
+ }
8
+ export declare function newPaymentEnvelope(payload: PaymentPayload, paymentSignature: string, signer: TabRemoteSigner): NewPaymentEnvelope;
9
+ export declare function parsePaymentEnvelope(paymentSignature: string, expectedAddress: `0x${string}`, profile: PaymentProfile): Promise<{
10
+ asset: `0x${string}`;
11
+ from: `0x${string}`;
12
+ network: "eip155:8453" | "eip155:42161" | "eip155:84532";
13
+ nonce: `0x${string}`;
14
+ payload: PaymentPayload;
15
+ signature: `0x${string}`;
16
+ validBefore: number;
17
+ }>;