mrmainspring 0.1.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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/audit/service.d.ts +7 -0
  4. package/dist/audit/service.js +98 -0
  5. package/dist/audit/store.d.ts +7 -0
  6. package/dist/audit/store.js +37 -0
  7. package/dist/audit/supabase-store.d.ts +9 -0
  8. package/dist/audit/supabase-store.js +22 -0
  9. package/dist/audit/types.d.ts +31 -0
  10. package/dist/audit/types.js +1 -0
  11. package/dist/casper/anchorClient.d.ts +99 -0
  12. package/dist/casper/anchorClient.js +412 -0
  13. package/dist/config.d.ts +51 -0
  14. package/dist/config.js +215 -0
  15. package/dist/env-file.d.ts +1 -0
  16. package/dist/env-file.js +51 -0
  17. package/dist/grimoire/service.d.ts +13 -0
  18. package/dist/grimoire/service.js +199 -0
  19. package/dist/grimoire/store.d.ts +10 -0
  20. package/dist/grimoire/store.js +64 -0
  21. package/dist/grimoire/supabase-store.d.ts +13 -0
  22. package/dist/grimoire/supabase-store.js +50 -0
  23. package/dist/grimoire/types.d.ts +60 -0
  24. package/dist/grimoire/types.js +1 -0
  25. package/dist/index.d.ts +2 -0
  26. package/dist/index.js +17 -0
  27. package/dist/mcp/auditTools.d.ts +3 -0
  28. package/dist/mcp/auditTools.js +13 -0
  29. package/dist/mcp/grimoireTools.d.ts +3 -0
  30. package/dist/mcp/grimoireTools.js +91 -0
  31. package/dist/mcp/jsonResult.d.ts +2 -0
  32. package/dist/mcp/jsonResult.js +10 -0
  33. package/dist/mcp/memoryTools.d.ts +3 -0
  34. package/dist/mcp/memoryTools.js +73 -0
  35. package/dist/mcp/paymentTools.d.ts +3 -0
  36. package/dist/mcp/paymentTools.js +33 -0
  37. package/dist/memory/canonical.d.ts +4 -0
  38. package/dist/memory/canonical.js +49 -0
  39. package/dist/memory/hash.d.ts +1 -0
  40. package/dist/memory/hash.js +4 -0
  41. package/dist/memory/service.d.ts +37 -0
  42. package/dist/memory/service.js +175 -0
  43. package/dist/memory/store.d.ts +8 -0
  44. package/dist/memory/store.js +49 -0
  45. package/dist/memory/supabase-store.d.ts +10 -0
  46. package/dist/memory/supabase-store.js +30 -0
  47. package/dist/memory/types.d.ts +56 -0
  48. package/dist/memory/types.js +7 -0
  49. package/dist/payments/service.d.ts +26 -0
  50. package/dist/payments/service.js +613 -0
  51. package/dist/payments/store.d.ts +10 -0
  52. package/dist/payments/store.js +64 -0
  53. package/dist/payments/supabase-store.d.ts +13 -0
  54. package/dist/payments/supabase-store.js +51 -0
  55. package/dist/payments/types.d.ts +101 -0
  56. package/dist/payments/types.js +1 -0
  57. package/dist/server.d.ts +5 -0
  58. package/dist/server.js +68 -0
  59. package/dist/storage/json-file-store.d.ts +17 -0
  60. package/dist/storage/json-file-store.js +87 -0
  61. package/dist/storage/store-factory.d.ts +12 -0
  62. package/dist/storage/store-factory.js +26 -0
  63. package/dist/storage/supabase-rest.d.ts +26 -0
  64. package/dist/storage/supabase-rest.js +85 -0
  65. package/dist/x402/client.d.ts +44 -0
  66. package/dist/x402/client.js +95 -0
  67. package/dist/x402/facilitator.d.ts +84 -0
  68. package/dist/x402/facilitator.js +800 -0
  69. package/dist/x402/readiness.d.ts +55 -0
  70. package/dist/x402/readiness.js +433 -0
  71. package/dist/x402/redaction.d.ts +1 -0
  72. package/dist/x402/redaction.js +30 -0
  73. package/dist/x402/resource.d.ts +69 -0
  74. package/dist/x402/resource.js +325 -0
  75. package/dist/x402/settlement.d.ts +176 -0
  76. package/dist/x402/settlement.js +1210 -0
  77. package/dist/x402/signer.d.ts +71 -0
  78. package/dist/x402/signer.js +616 -0
  79. package/package.json +61 -0
@@ -0,0 +1,13 @@
1
+ import { SupabaseRestClient } from "../storage/supabase-rest.js";
2
+ import type { PaymentIntentRecord, PaymentReceiptRecord, PaymentStore } from "./types.js";
3
+ export declare class SupabasePaymentStore implements PaymentStore {
4
+ private readonly client;
5
+ private readonly intentsTable;
6
+ private readonly receiptsTable;
7
+ constructor(client: SupabaseRestClient);
8
+ saveIntent(intent: PaymentIntentRecord): Promise<void>;
9
+ getIntent(paymentId: string): Promise<PaymentIntentRecord | null>;
10
+ findIntentByIdempotencyKey(agentId: string, idempotencyKey: string): Promise<PaymentIntentRecord | null>;
11
+ saveReceipt(receipt: PaymentReceiptRecord): Promise<void>;
12
+ getReceipt(paymentId: string): Promise<PaymentReceiptRecord | null>;
13
+ }
@@ -0,0 +1,51 @@
1
+ export class SupabasePaymentStore {
2
+ client;
3
+ intentsTable;
4
+ receiptsTable;
5
+ constructor(client) {
6
+ this.client = client;
7
+ this.intentsTable = client.table("payment_intents");
8
+ this.receiptsTable = client.table("payment_receipts");
9
+ }
10
+ async saveIntent(intent) {
11
+ await this.client.upsert(this.intentsTable, {
12
+ id: intent.id,
13
+ agent_id: intent.agent_id,
14
+ idempotency_key: intent.idempotency_key,
15
+ created_at: intent.created_at,
16
+ updated_at: intent.updated_at,
17
+ record: intent
18
+ }, "id");
19
+ }
20
+ async getIntent(paymentId) {
21
+ const [record] = await this.client.selectRecords(this.intentsTable, {
22
+ filters: { id: paymentId },
23
+ limit: 1
24
+ });
25
+ return record ?? null;
26
+ }
27
+ async findIntentByIdempotencyKey(agentId, idempotencyKey) {
28
+ const [record] = await this.client.selectRecords(this.intentsTable, {
29
+ filters: { agent_id: agentId, idempotency_key: idempotencyKey },
30
+ order: { column: "created_at" },
31
+ limit: 1
32
+ });
33
+ return record ?? null;
34
+ }
35
+ async saveReceipt(receipt) {
36
+ await this.client.upsert(this.receiptsTable, {
37
+ id: receipt.id,
38
+ payment_id: receipt.payment_id,
39
+ created_at: receipt.created_at,
40
+ record: receipt
41
+ }, "id");
42
+ }
43
+ async getReceipt(paymentId) {
44
+ const [record] = await this.client.selectRecords(this.receiptsTable, {
45
+ filters: { payment_id: paymentId },
46
+ order: { column: "created_at" },
47
+ limit: 1
48
+ });
49
+ return record ?? null;
50
+ }
51
+ }
@@ -0,0 +1,101 @@
1
+ export type PaymentFetchInput = {
2
+ agent_id: string;
3
+ policy_id: string;
4
+ method: string;
5
+ url: string;
6
+ expected_amount?: string;
7
+ idempotency_key?: string;
8
+ request_challenge?: boolean;
9
+ };
10
+ export type PaymentPreflightInput = PaymentFetchInput;
11
+ export type PaymentDenialReason = "policy_not_found" | "policy_disabled" | "url_not_allowed" | "method_not_allowed" | "amount_over_limit" | "period_limit_exceeded" | "invalid_amount";
12
+ export type PaymentStatus = "created" | "policy_denied" | "policy_checked" | "challenge_received" | "settlement_unavailable" | "settled";
13
+ export type PaymentNextState = "ready_for_x402_challenge" | "challenge_received" | "settlement_unavailable" | null;
14
+ export type PaymentSettlementState = "not_started" | "not_required" | "unavailable" | "settled";
15
+ export type PaymentChallengeSummary = {
16
+ status: "payment_required" | "free_response" | "unexpected_response";
17
+ status_code: number;
18
+ facilitator_url: string | null;
19
+ resource_url: string | null;
20
+ request_url: string;
21
+ settlement_status: "not_started" | "not_required";
22
+ requirements_json?: string | null;
23
+ requirements?: unknown;
24
+ requirements_source?: string;
25
+ response_hash?: string;
26
+ };
27
+ export type PaymentIntentRecord = {
28
+ id: string;
29
+ agent_id: string;
30
+ policy_id: string;
31
+ method: string;
32
+ url: string;
33
+ amount: string | null;
34
+ status: PaymentStatus;
35
+ idempotency_key: string | null;
36
+ policy_hash: string | null;
37
+ denial_reason: PaymentDenialReason | null;
38
+ requirements_json: string | null;
39
+ signed_payload_hash: string | null;
40
+ settlement_blocker: string | null;
41
+ created_at: string;
42
+ updated_at: string;
43
+ };
44
+ export type PaymentReceiptRecord = {
45
+ id: string;
46
+ payment_id: string;
47
+ facilitator_url: string;
48
+ casper_transaction_hash: string | null;
49
+ settlement_status: "pending" | "settled" | "failed" | "settlement_unavailable";
50
+ response_hash: string | null;
51
+ response_status: number | null;
52
+ receipt_json: string;
53
+ created_at: string;
54
+ };
55
+ export type PaymentStore = {
56
+ saveIntent(intent: PaymentIntentRecord): Promise<void>;
57
+ getIntent(paymentId: string): Promise<PaymentIntentRecord | null>;
58
+ findIntentByIdempotencyKey(agentId: string, idempotencyKey: string): Promise<PaymentIntentRecord | null>;
59
+ saveReceipt(receipt: PaymentReceiptRecord): Promise<void>;
60
+ getReceipt(paymentId: string): Promise<PaymentReceiptRecord | null>;
61
+ };
62
+ export type PaymentFetchAllowed = {
63
+ allowed: true;
64
+ payment_id: string;
65
+ status: "policy_checked" | "challenge_received" | "settlement_unavailable" | "settled";
66
+ next_state: PaymentNextState;
67
+ agent_id: string;
68
+ policy_id: string;
69
+ method: string;
70
+ url: string;
71
+ expected_amount: string | null;
72
+ policy_hash: string;
73
+ idempotency_key: string | null;
74
+ persisted: true;
75
+ settlement: PaymentSettlementState;
76
+ requirements_json: string | null;
77
+ requirements?: unknown;
78
+ challenge?: PaymentChallengeSummary;
79
+ settlement_blocker?: string;
80
+ };
81
+ export type PaymentFetchDenied = {
82
+ allowed: false;
83
+ payment_id: string;
84
+ status: "policy_denied";
85
+ reason: PaymentDenialReason;
86
+ agent_id: string;
87
+ policy_id: string;
88
+ method: string;
89
+ url: string;
90
+ expected_amount: string | null;
91
+ idempotency_key: string | null;
92
+ persisted: true;
93
+ };
94
+ export type PaymentFetchResult = PaymentFetchAllowed | PaymentFetchDenied;
95
+ export type PaymentPreflightResult = PaymentFetchResult;
96
+ export type PaymentReceiptResult = {
97
+ found: boolean;
98
+ payment_id: string;
99
+ intent?: PaymentIntentRecord;
100
+ receipt?: PaymentReceiptRecord | null;
101
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { SigilConfig } from "./config.js";
3
+ import { type X402SettlementProvider } from "./x402/settlement.js";
4
+ export declare function createSigilServer(config: SigilConfig): McpServer;
5
+ export declare function createX402SettlementProvider(config: SigilConfig): X402SettlementProvider;
package/dist/server.js ADDED
@@ -0,0 +1,68 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { AuditService } from "./audit/service.js";
3
+ import { createCasperAnchorClient } from "./casper/anchorClient.js";
4
+ import { GrimoireService } from "./grimoire/service.js";
5
+ import { registerAuditTools } from "./mcp/auditTools.js";
6
+ import { registerGrimoireTools } from "./mcp/grimoireTools.js";
7
+ import { registerMemoryTools } from "./mcp/memoryTools.js";
8
+ import { registerPaymentTools } from "./mcp/paymentTools.js";
9
+ import { MemoryService } from "./memory/service.js";
10
+ import { PaymentService } from "./payments/service.js";
11
+ import { createBackendStores } from "./storage/store-factory.js";
12
+ import { X402ChallengeClient } from "./x402/client.js";
13
+ import { CasperCliX402SettlementProvider, DisabledX402SettlementProvider, FacilitatorX402SettlementProvider, HttpX402SigningProvider, ResourceRetryX402SettlementProvider } from "./x402/settlement.js";
14
+ export function createSigilServer(config) {
15
+ const server = new McpServer({
16
+ name: config.serverName,
17
+ version: config.serverVersion
18
+ });
19
+ const stores = createBackendStores(config);
20
+ const auditService = new AuditService(stores.audit);
21
+ const anchorClient = createCasperAnchorClient(config.casper);
22
+ const memoryService = new MemoryService(stores.memory, auditService, anchorClient);
23
+ const grimoireService = new GrimoireService(stores.grimoire, config.grimoireMasterKey, auditService);
24
+ const paymentService = new PaymentService(grimoireService, stores.payments, auditService, new X402ChallengeClient({
25
+ facilitatorUrl: config.x402.facilitatorUrl,
26
+ resourceUrl: config.x402.resourceDemoUrl
27
+ }), createX402SettlementProvider(config));
28
+ registerMemoryTools(server, memoryService);
29
+ registerGrimoireTools(server, grimoireService);
30
+ registerPaymentTools(server, paymentService);
31
+ registerAuditTools(server, auditService);
32
+ return server;
33
+ }
34
+ export function createX402SettlementProvider(config) {
35
+ if (!config.x402.settlementEnabled) {
36
+ return new DisabledX402SettlementProvider("x402_settlement_disabled");
37
+ }
38
+ if (!config.x402.signerUrl) {
39
+ return new DisabledX402SettlementProvider("x402_signing_provider_not_configured");
40
+ }
41
+ const signer = new HttpX402SigningProvider({
42
+ signerUrl: config.x402.signerUrl,
43
+ authToken: config.x402.signerAuthToken,
44
+ timeoutMs: config.x402.signerTimeoutMs
45
+ });
46
+ if (config.x402.settlementMode === "facilitator") {
47
+ return new FacilitatorX402SettlementProvider(signer);
48
+ }
49
+ if (config.x402.settlementMode === "casper-cli") {
50
+ return new CasperCliX402SettlementProvider(signer, {
51
+ networkName: config.casper.networkName,
52
+ caip2ChainId: config.casper.caip2ChainId,
53
+ rpcUrl: config.casper.rpcUrl,
54
+ accountKeyPath: config.casper.accountKeyPath,
55
+ submissionEnabled: config.casper.submissionEnabled,
56
+ clientBin: config.casper.clientBin,
57
+ clientWslDistro: config.casper.clientWslDistro,
58
+ gasPriceTolerance: config.casper.gasPriceTolerance,
59
+ pricingMode: config.casper.pricingMode,
60
+ paymentAmountMotes: config.x402.casperSettlementPaymentAmountMotes,
61
+ confirmationPollIntervalMs: config.x402.casperConfirmationPollIntervalMs,
62
+ confirmationTimeoutMs: config.x402.casperConfirmationTimeoutMs
63
+ });
64
+ }
65
+ return new ResourceRetryX402SettlementProvider(signer, undefined, {
66
+ paymentHeaderName: config.x402.paymentHeaderName
67
+ });
68
+ }
@@ -0,0 +1,17 @@
1
+ type JsonFileStoreOptions<T> = {
2
+ filePath: string;
3
+ empty: () => T;
4
+ normalize: (parsed: unknown) => T;
5
+ };
6
+ export declare class JsonFileStore<T> {
7
+ private readonly filePath;
8
+ private readonly lockKey;
9
+ private readonly empty;
10
+ private readonly normalize;
11
+ constructor(options: JsonFileStoreOptions<T>);
12
+ read(): Promise<T>;
13
+ update(mutator: (data: T) => void | Promise<void>): Promise<void>;
14
+ private readUnlocked;
15
+ private writeUnlocked;
16
+ }
17
+ export {};
@@ -0,0 +1,87 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ const fileLocks = new Map();
5
+ export class JsonFileStore {
6
+ filePath;
7
+ lockKey;
8
+ empty;
9
+ normalize;
10
+ constructor(options) {
11
+ this.filePath = options.filePath;
12
+ this.lockKey = resolve(options.filePath);
13
+ this.empty = options.empty;
14
+ this.normalize = options.normalize;
15
+ }
16
+ async read() {
17
+ return this.readUnlocked();
18
+ }
19
+ async update(mutator) {
20
+ await withFileLock(this.lockKey, async () => {
21
+ const data = await this.readUnlocked();
22
+ await mutator(data);
23
+ await this.writeUnlocked(data);
24
+ });
25
+ }
26
+ async readUnlocked() {
27
+ let raw;
28
+ try {
29
+ raw = await readFile(this.filePath, "utf8");
30
+ }
31
+ catch (error) {
32
+ if (isMissingFile(error)) {
33
+ return this.empty();
34
+ }
35
+ throw error;
36
+ }
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(raw);
40
+ }
41
+ catch (error) {
42
+ throw new Error(`Failed to parse JSON store file at ${this.filePath}: ${formatErrorMessage(error)}`, { cause: error });
43
+ }
44
+ return this.normalize(parsed);
45
+ }
46
+ async writeUnlocked(data) {
47
+ const fileDir = dirname(this.filePath);
48
+ const tempPath = join(fileDir, `.${basename(this.filePath)}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`);
49
+ await mkdir(fileDir, { recursive: true });
50
+ try {
51
+ await writeFile(tempPath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
52
+ await rename(tempPath, this.filePath);
53
+ }
54
+ catch (error) {
55
+ await rm(tempPath, { force: true }).catch(() => undefined);
56
+ throw error;
57
+ }
58
+ }
59
+ }
60
+ async function withFileLock(lockKey, action) {
61
+ const previous = fileLocks.get(lockKey) ?? Promise.resolve();
62
+ let release;
63
+ const current = new Promise((resolveCurrent) => {
64
+ release = resolveCurrent;
65
+ });
66
+ const next = previous.catch(() => undefined).then(() => current);
67
+ fileLocks.set(lockKey, next);
68
+ await previous.catch(() => undefined);
69
+ try {
70
+ return await action();
71
+ }
72
+ finally {
73
+ release();
74
+ if (fileLocks.get(lockKey) === next) {
75
+ fileLocks.delete(lockKey);
76
+ }
77
+ }
78
+ }
79
+ function isMissingFile(error) {
80
+ return Boolean(error &&
81
+ typeof error === "object" &&
82
+ "code" in error &&
83
+ error.code === "ENOENT");
84
+ }
85
+ function formatErrorMessage(error) {
86
+ return error instanceof Error ? error.message : String(error);
87
+ }
@@ -0,0 +1,12 @@
1
+ import type { AuditStore } from "../audit/types.js";
2
+ import type { SigilConfig } from "../config.js";
3
+ import type { GrimoireStore } from "../grimoire/types.js";
4
+ import type { MemoryStore } from "../memory/types.js";
5
+ import type { PaymentStore } from "../payments/types.js";
6
+ export type BackendStores = {
7
+ audit: AuditStore;
8
+ grimoire: GrimoireStore;
9
+ memory: MemoryStore;
10
+ payments: PaymentStore;
11
+ };
12
+ export declare function createBackendStores(config: SigilConfig): BackendStores;
@@ -0,0 +1,26 @@
1
+ import { SupabaseAuditStore } from "../audit/supabase-store.js";
2
+ import { FileAuditStore } from "../audit/store.js";
3
+ import { SupabaseGrimoireStore } from "../grimoire/supabase-store.js";
4
+ import { FileGrimoireStore } from "../grimoire/store.js";
5
+ import { SupabaseMemoryStore } from "../memory/supabase-store.js";
6
+ import { FileMemoryStore } from "../memory/store.js";
7
+ import { SupabasePaymentStore } from "../payments/supabase-store.js";
8
+ import { FilePaymentStore } from "../payments/store.js";
9
+ import { SupabaseRestClient } from "./supabase-rest.js";
10
+ export function createBackendStores(config) {
11
+ if (config.storage.backend === "supabase") {
12
+ const client = new SupabaseRestClient(config.storage.supabase);
13
+ return {
14
+ audit: new SupabaseAuditStore(client),
15
+ grimoire: new SupabaseGrimoireStore(client),
16
+ memory: new SupabaseMemoryStore(client),
17
+ payments: new SupabasePaymentStore(client)
18
+ };
19
+ }
20
+ return {
21
+ audit: new FileAuditStore(config.dataDir),
22
+ grimoire: new FileGrimoireStore(config.dataDir),
23
+ memory: new FileMemoryStore(config.dataDir),
24
+ payments: new FilePaymentStore(config.dataDir)
25
+ };
26
+ }
@@ -0,0 +1,26 @@
1
+ export type SupabaseRestConfig = {
2
+ url: string;
3
+ key: string;
4
+ schema: string;
5
+ tablePrefix: string;
6
+ };
7
+ type SupabaseSelectOptions = {
8
+ filters?: Record<string, string | null>;
9
+ order?: {
10
+ column: string;
11
+ ascending?: boolean;
12
+ };
13
+ limit?: number;
14
+ };
15
+ type SupabaseRecord = Record<string, unknown>;
16
+ export declare class SupabaseRestClient {
17
+ private readonly config;
18
+ private readonly baseUrl;
19
+ constructor(config: SupabaseRestConfig);
20
+ table(name: string): string;
21
+ upsert(table: string, row: SupabaseRecord, onConflict: string): Promise<void>;
22
+ insert(table: string, row: SupabaseRecord): Promise<void>;
23
+ selectRecords<T>(table: string, options?: SupabaseSelectOptions): Promise<T[]>;
24
+ private request;
25
+ }
26
+ export {};
@@ -0,0 +1,85 @@
1
+ export class SupabaseRestClient {
2
+ config;
3
+ baseUrl;
4
+ constructor(config) {
5
+ this.config = config;
6
+ this.baseUrl = `${config.url.replace(/\/+$/, "")}/rest/v1`;
7
+ }
8
+ table(name) {
9
+ return `${this.config.tablePrefix}${name}`;
10
+ }
11
+ async upsert(table, row, onConflict) {
12
+ const url = new URL(`${this.baseUrl}/${table}`);
13
+ url.searchParams.set("on_conflict", onConflict);
14
+ await this.request(url, {
15
+ method: "POST",
16
+ headers: {
17
+ Prefer: "resolution=merge-duplicates,return=minimal"
18
+ },
19
+ body: JSON.stringify(row)
20
+ });
21
+ }
22
+ async insert(table, row) {
23
+ await this.request(new URL(`${this.baseUrl}/${table}`), {
24
+ method: "POST",
25
+ headers: {
26
+ Prefer: "return=minimal"
27
+ },
28
+ body: JSON.stringify(row)
29
+ });
30
+ }
31
+ async selectRecords(table, options = {}) {
32
+ const url = new URL(`${this.baseUrl}/${table}`);
33
+ url.searchParams.set("select", "record");
34
+ for (const [column, value] of Object.entries(options.filters ?? {})) {
35
+ url.searchParams.set(column, value === null ? "is.null" : `eq.${value}`);
36
+ }
37
+ if (options.order) {
38
+ const direction = options.order.ascending === true ? "asc" : "desc";
39
+ url.searchParams.set("order", `${options.order.column}.${direction}`);
40
+ }
41
+ if (options.limit !== undefined) {
42
+ url.searchParams.set("limit", String(options.limit));
43
+ }
44
+ const rows = await this.request(url, { method: "GET" });
45
+ if (!Array.isArray(rows)) {
46
+ throw new Error(`Supabase table ${table} returned a non-array response`);
47
+ }
48
+ return rows.map((row) => {
49
+ const record = asRecord(row).record;
50
+ if (!record || typeof record !== "object" || Array.isArray(record)) {
51
+ throw new Error(`Supabase table ${table} returned a row without a JSON record`);
52
+ }
53
+ return record;
54
+ });
55
+ }
56
+ async request(url, init) {
57
+ const response = await fetch(url, {
58
+ ...init,
59
+ headers: {
60
+ apikey: this.config.key,
61
+ Authorization: `Bearer ${this.config.key}`,
62
+ "Content-Type": "application/json",
63
+ Accept: "application/json",
64
+ "Accept-Profile": this.config.schema,
65
+ "Content-Profile": this.config.schema,
66
+ ...init.headers
67
+ }
68
+ });
69
+ if (!response.ok) {
70
+ const body = await response.text().catch(() => "");
71
+ throw new Error(`Supabase request failed: ${response.status} ${response.statusText}${body ? ` ${body}` : ""}`);
72
+ }
73
+ if (response.status === 204) {
74
+ return null;
75
+ }
76
+ const body = await response.text();
77
+ return body ? JSON.parse(body) : null;
78
+ }
79
+ }
80
+ function asRecord(value) {
81
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
82
+ throw new Error("Expected Supabase row object");
83
+ }
84
+ return value;
85
+ }
@@ -0,0 +1,44 @@
1
+ export type PaymentRequirements = unknown;
2
+ export type X402ChallengeClientConfig = {
3
+ facilitatorUrl?: string | null;
4
+ resourceUrl?: string | null;
5
+ };
6
+ export type X402ChallengeRequest = {
7
+ method: string;
8
+ url: string;
9
+ headers?: Record<string, string>;
10
+ };
11
+ export type X402RequirementsSource = "payment-required-header" | "x-payment-required-header" | "json-body" | "raw-body";
12
+ type X402ChallengeMetadata = {
13
+ facilitator_url: string | null;
14
+ resource_url: string | null;
15
+ request_url: string;
16
+ };
17
+ export type X402ChallengeResult = {
18
+ status: "payment_required";
19
+ status_code: 402;
20
+ requirements: PaymentRequirements;
21
+ requirements_json: string;
22
+ requirements_source: X402RequirementsSource;
23
+ raw_body: string;
24
+ settlement_status: "not_started";
25
+ } & X402ChallengeMetadata | {
26
+ status: "free_response";
27
+ status_code: number;
28
+ body_text: string;
29
+ response_hash: string;
30
+ settlement_status: "not_required";
31
+ } & X402ChallengeMetadata | {
32
+ status: "unexpected_response";
33
+ status_code: number;
34
+ body_text: string;
35
+ response_hash: string;
36
+ settlement_status: "not_started";
37
+ } & X402ChallengeMetadata;
38
+ export declare class X402ChallengeClient {
39
+ private readonly config;
40
+ constructor(config?: X402ChallengeClientConfig);
41
+ requestChallenge(input: X402ChallengeRequest): Promise<X402ChallengeResult>;
42
+ private metadata;
43
+ }
44
+ export {};
@@ -0,0 +1,95 @@
1
+ import { sha256Hex } from "../memory/hash.js";
2
+ import { redactX402Value } from "./redaction.js";
3
+ export class X402ChallengeClient {
4
+ config;
5
+ constructor(config = {}) {
6
+ this.config = config;
7
+ }
8
+ async requestChallenge(input) {
9
+ const response = await fetch(input.url, {
10
+ method: input.method.toUpperCase(),
11
+ headers: input.headers
12
+ });
13
+ const bodyText = await response.text();
14
+ const metadata = this.metadata(input.url);
15
+ if (response.status === 402) {
16
+ const requirements = parseRequirements(response.headers, bodyText);
17
+ const redactedRequirements = redactX402Value(requirements.value);
18
+ return {
19
+ ...metadata,
20
+ status: "payment_required",
21
+ status_code: 402,
22
+ requirements: redactedRequirements,
23
+ requirements_json: JSON.stringify(redactedRequirements),
24
+ requirements_source: requirements.source,
25
+ raw_body: bodyText,
26
+ settlement_status: "not_started"
27
+ };
28
+ }
29
+ if (response.ok) {
30
+ return {
31
+ ...metadata,
32
+ status: "free_response",
33
+ status_code: response.status,
34
+ body_text: bodyText,
35
+ response_hash: sha256Hex(bodyText),
36
+ settlement_status: "not_required"
37
+ };
38
+ }
39
+ return {
40
+ ...metadata,
41
+ status: "unexpected_response",
42
+ status_code: response.status,
43
+ body_text: bodyText,
44
+ response_hash: sha256Hex(bodyText),
45
+ settlement_status: "not_started"
46
+ };
47
+ }
48
+ metadata(requestUrl) {
49
+ return {
50
+ facilitator_url: this.config.facilitatorUrl ?? null,
51
+ resource_url: this.config.resourceUrl ?? requestUrl,
52
+ request_url: requestUrl
53
+ };
54
+ }
55
+ }
56
+ const PAYMENT_REQUIRED_HEADERS = [
57
+ { name: "PAYMENT-REQUIRED", source: "payment-required-header" },
58
+ { name: "X-PAYMENT-REQUIRED", source: "x-payment-required-header" }
59
+ ];
60
+ function parseRequirements(headers, bodyText) {
61
+ for (const header of PAYMENT_REQUIRED_HEADERS) {
62
+ const value = headers.get(header.name);
63
+ if (!value) {
64
+ continue;
65
+ }
66
+ const parsed = parseBase64Json(value);
67
+ if (parsed.ok) {
68
+ return { value: parsed.value, source: header.source };
69
+ }
70
+ }
71
+ const parsedBody = parseJson(bodyText);
72
+ if (parsedBody.ok) {
73
+ return { value: parsedBody.value, source: "json-body" };
74
+ }
75
+ return { value: { raw: bodyText }, source: "raw-body" };
76
+ }
77
+ function parseBase64Json(value) {
78
+ try {
79
+ return parseJson(Buffer.from(value.trim(), "base64").toString("utf8"));
80
+ }
81
+ catch {
82
+ return { ok: false };
83
+ }
84
+ }
85
+ function parseJson(value) {
86
+ if (!value.trim()) {
87
+ return { ok: false };
88
+ }
89
+ try {
90
+ return { ok: true, value: JSON.parse(value) };
91
+ }
92
+ catch {
93
+ return { ok: false };
94
+ }
95
+ }