@chainpayhq/sdk 0.0.0-stage → 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 (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -2
  3. package/dist/accounts.d.ts +23 -0
  4. package/dist/accounts.js +148 -0
  5. package/dist/cards/accounts.d.ts +156 -0
  6. package/dist/cards/accounts.js +346 -0
  7. package/dist/cards/api.d.ts +379 -0
  8. package/dist/cards/api.js +241 -0
  9. package/dist/cards/commitment.d.ts +86 -0
  10. package/dist/cards/commitment.js +288 -0
  11. package/dist/cards/constants.d.ts +160 -0
  12. package/dist/cards/constants.js +163 -0
  13. package/dist/cards/draft.d.ts +57 -0
  14. package/dist/cards/draft.js +131 -0
  15. package/dist/cards/evidence.d.ts +69 -0
  16. package/dist/cards/evidence.js +41 -0
  17. package/dist/cards/hash.d.ts +9 -0
  18. package/dist/cards/hash.js +47 -0
  19. package/dist/cards/index.d.ts +14 -0
  20. package/dist/cards/index.js +14 -0
  21. package/dist/cards/instructions.d.ts +302 -0
  22. package/dist/cards/instructions.js +474 -0
  23. package/dist/cards/layout.d.ts +33 -0
  24. package/dist/cards/layout.js +137 -0
  25. package/dist/cards/math.d.ts +67 -0
  26. package/dist/cards/math.js +138 -0
  27. package/dist/cards/merchants.d.ts +23 -0
  28. package/dist/cards/merchants.js +38 -0
  29. package/dist/cards/pda.d.ts +42 -0
  30. package/dist/cards/pda.js +93 -0
  31. package/dist/cards/private-repayment.d.ts +198 -0
  32. package/dist/cards/private-repayment.js +485 -0
  33. package/dist/cards/redact.d.ts +18 -0
  34. package/dist/cards/redact.js +103 -0
  35. package/dist/cards/tee.d.ts +253 -0
  36. package/dist/cards/tee.js +605 -0
  37. package/dist/cli.d.ts +31 -0
  38. package/dist/cli.js +351 -0
  39. package/dist/client.d.ts +80 -0
  40. package/dist/client.js +493 -0
  41. package/dist/constants.d.ts +44 -0
  42. package/dist/constants.js +43 -0
  43. package/dist/crossmint-adapt.d.ts +43 -0
  44. package/dist/crossmint-adapt.js +68 -0
  45. package/dist/crossmint-order.d.ts +220 -0
  46. package/dist/crossmint-order.js +638 -0
  47. package/dist/delivery.d.ts +66 -0
  48. package/dist/delivery.js +232 -0
  49. package/dist/encoding.d.ts +51 -0
  50. package/dist/encoding.js +128 -0
  51. package/dist/index.d.ts +27 -0
  52. package/dist/index.js +26 -0
  53. package/dist/known-assets.d.ts +30 -0
  54. package/dist/known-assets.js +49 -0
  55. package/dist/mandate-request.d.ts +123 -0
  56. package/dist/mandate-request.js +401 -0
  57. package/dist/mandate.d.ts +44 -0
  58. package/dist/mandate.js +157 -0
  59. package/dist/ops-snapshot.d.ts +157 -0
  60. package/dist/ops-snapshot.js +356 -0
  61. package/dist/payment-request.d.ts +37 -0
  62. package/dist/payment-request.js +218 -0
  63. package/dist/payment.d.ts +40 -0
  64. package/dist/payment.js +214 -0
  65. package/dist/pda.d.ts +13 -0
  66. package/dist/pda.js +36 -0
  67. package/dist/receipt-export.d.ts +55 -0
  68. package/dist/receipt-export.js +149 -0
  69. package/dist/receipt.d.ts +108 -0
  70. package/dist/receipt.js +213 -0
  71. package/dist/solana.d.ts +5 -0
  72. package/dist/solana.js +20 -0
  73. package/dist/token-capabilities.d.ts +9 -0
  74. package/dist/token-capabilities.js +139 -0
  75. package/dist/token.d.ts +18 -0
  76. package/dist/token.js +55 -0
  77. package/dist/transaction-reader.d.ts +10 -0
  78. package/dist/transaction-reader.js +18 -0
  79. package/dist/transaction-v1.d.ts +18 -0
  80. package/dist/transaction-v1.js +73 -0
  81. package/dist/types.d.ts +267 -0
  82. package/dist/types.js +1 -0
  83. package/dist/x402-adapt.d.ts +20 -0
  84. package/dist/x402-adapt.js +62 -0
  85. package/dist/x402-challenge.d.ts +116 -0
  86. package/dist/x402-challenge.js +385 -0
  87. package/dist/x402.d.ts +18 -0
  88. package/dist/x402.js +35 -0
  89. package/package.json +54 -4
@@ -0,0 +1,163 @@
1
+ /**
2
+ * card_policy program ID on Devnet (contracts.md Changelog, Lane 1 / 1A;
3
+ * `address` in programs/card_policy/idl/card_policy.json). Every builder and
4
+ * PDA helper takes an explicit `programId` and falls back to this value.
5
+ */
6
+ export const CARD_POLICY_PROGRAM_ID = "H3aetJdQXG8EeJSCHZrpQa8iKHBw8e1p9fSPjTUsB93n";
7
+ export function resolveCardPolicyProgramId(programId) {
8
+ const value = (programId ?? CARD_POLICY_PROGRAM_ID).trim();
9
+ if (!value) {
10
+ throw new Error("card_policy program ID is not configured yet. Pass programId explicitly until the program is deployed.");
11
+ }
12
+ return value;
13
+ }
14
+ /** MagicBlock Devnet TEE (Private Ephemeral Rollup). Tokens go in the query string; never log the full URL. */
15
+ export const DEVNET_TEE_URL = "https://devnet-tee.magicblock.app";
16
+ export const DEVNET_TEE_WS_URL = "wss://devnet-tee.magicblock.app";
17
+ /** The only validator `delegate_card` accepts on Devnet (contracts.md §1). */
18
+ export const DEVNET_TEE_VALIDATOR = "MTEWGuqxUpYZGFJQcp8tLN7x5v9BSeoFHYWQQ3n3xzo";
19
+ export const TEE_VALIDATOR_ALLOWLIST = [DEVNET_TEE_VALIDATOR];
20
+ export const DELEGATION_PROGRAM_ID = "DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh";
21
+ export const MAGIC_PROGRAM_ID = "Magic11111111111111111111111111111111111111";
22
+ export const MAGIC_CONTEXT_ID = "MagicContext1111111111111111111111111111111";
23
+ export const PERMISSION_PROGRAM_ID = "ACLseoPoyC3cBqoUtkbjZ4aDrkurZW86v19pXz2XQnp1";
24
+ export const EPHEMERAL_VAULT_ID = "MagicVau1t999999999999999999999999999999999";
25
+ export const CARD_SEEDS = {
26
+ binding: "card_binding",
27
+ policy: "card_policy",
28
+ period: "card_period",
29
+ reservation: "res",
30
+ intent: "intent",
31
+ commitment: "card_commit",
32
+ /** Per-card replay guard for closed reservations (PER only). */
33
+ authGuard: "auth_guard",
34
+ /** Lamport-only base PDA: the `approved_agent` of the owner's repayment mandate. */
35
+ repayAgent: "repay_agent",
36
+ };
37
+ export const MAX_MERCHANTS = 8;
38
+ export const MAX_MCCS = 16;
39
+ /** owner + authorizer + 4 finance readers. */
40
+ export const MAX_MEMBERS = 6;
41
+ /** $10,000 sandbox cap on any budget (contracts.md §1.3 #6). */
42
+ export const MAX_BUDGET_CENTS = 1000000n;
43
+ export const MIN_PERIOD_SECONDS = 86_400;
44
+ export const MAX_FEE_BPS = 1_000;
45
+ /** A checkout intent may live at most 10 minutes. */
46
+ export const MAX_INTENT_TTL_SECONDS = 600;
47
+ export const CARD_ISSUER = { lithic_sandbox: 1, card_sim: 2 };
48
+ /** EphemeralPermission member flags used by card_policy (MagicBlock permission program). */
49
+ export const PERMISSION_FLAGS = {
50
+ AUTHORITY: 1,
51
+ TX_LOGS: 2,
52
+ TX_BALANCES: 4,
53
+ TX_MESSAGE: 8,
54
+ };
55
+ /** Anchor instruction discriminators: sha256("global:<name>")[0..8]. */
56
+ export const CARD_POLICY_DISCRIMINATORS = {
57
+ initCard: Uint8Array.from([163, 228, 150, 17, 250, 36, 156, 20]),
58
+ delegateCard: Uint8Array.from([143, 76, 125, 25, 4, 123, 254, 155]),
59
+ initPermission: Uint8Array.from([66, 14, 153, 250, 187, 36, 179, 236]),
60
+ updatePermission: Uint8Array.from([1, 120, 111, 126, 237, 61, 41, 61]),
61
+ syncPermission: Uint8Array.from([164, 186, 105, 85, 229, 147, 214, 246]),
62
+ setPolicy: Uint8Array.from([40, 133, 12, 157, 235, 202, 2, 132]),
63
+ openCheckoutIntent: Uint8Array.from([139, 180, 113, 38, 119, 108, 36, 224]),
64
+ cancelCheckoutIntent: Uint8Array.from([175, 152, 6, 182, 211, 206, 160, 167]),
65
+ closeCheckoutIntent: Uint8Array.from([36, 161, 214, 238, 244, 192, 71, 234]),
66
+ authorize: Uint8Array.from([173, 193, 102, 210, 219, 137, 113, 120]),
67
+ adjustReservation: Uint8Array.from([137, 209, 64, 94, 107, 194, 101, 43]),
68
+ capture: Uint8Array.from([110, 65, 245, 241, 195, 248, 233, 142]),
69
+ reverse: Uint8Array.from([137, 149, 242, 82, 88, 95, 221, 145]),
70
+ refund: Uint8Array.from([2, 96, 183, 251, 63, 208, 46, 46]),
71
+ recordDispute: Uint8Array.from([190, 94, 198, 130, 215, 36, 43, 143]),
72
+ recordException: Uint8Array.from([221, 204, 254, 53, 116, 13, 197, 105]),
73
+ resolveException: Uint8Array.from([131, 144, 179, 147, 199, 157, 77, 127]),
74
+ rollPeriod: Uint8Array.from([30, 184, 166, 42, 251, 204, 47, 107]),
75
+ freeze: Uint8Array.from([255, 91, 207, 84, 251, 194, 254, 63]),
76
+ unfreeze: Uint8Array.from([133, 160, 68, 253, 80, 232, 218, 247]),
77
+ recoveryFreeze: Uint8Array.from([113, 1, 136, 29, 212, 19, 213, 56]),
78
+ restore: Uint8Array.from([77, 37, 122, 128, 139, 35, 6, 58]),
79
+ confirmReconciled: Uint8Array.from([116, 7, 34, 18, 87, 241, 4, 233]),
80
+ checkpoint: Uint8Array.from([213, 200, 19, 204, 240, 143, 184, 252]),
81
+ writeCommitment: Uint8Array.from([159, 54, 242, 213, 46, 162, 174, 75]),
82
+ wipeCard: Uint8Array.from([210, 120, 235, 58, 105, 132, 206, 76]),
83
+ closeCard: Uint8Array.from([142, 206, 170, 182, 227, 204, 185, 115]),
84
+ recordRepayment: Uint8Array.from([193, 155, 76, 246, 27, 189, 147, 102]),
85
+ closeReservation: Uint8Array.from([171, 96, 8, 85, 100, 252, 173, 204]),
86
+ /** Base layer: the card's repay agent pays a statement by ChainPay `execute_payment` CPI. */
87
+ repayStatement: Uint8Array.from([56, 158, 212, 137, 146, 226, 174, 82]),
88
+ /** PER: MagicBlock private payment, authorizer-attested (no ChainPay receipt). */
89
+ recordPrivateRepayment: Uint8Array.from([91, 51, 18, 9, 7, 33, 77, 147]),
90
+ };
91
+ /** Anchor account discriminators: sha256("account:<Name>")[0..8]. */
92
+ export const CARD_ACCOUNT_DISCRIMINATORS = {
93
+ cardBinding: Uint8Array.from([86, 71, 81, 106, 155, 166, 1, 72]),
94
+ cardPolicy: Uint8Array.from([196, 159, 33, 83, 231, 156, 65, 71]),
95
+ cardPeriod: Uint8Array.from([112, 201, 91, 227, 87, 121, 62, 2]),
96
+ reservation: Uint8Array.from([188, 235, 0, 111, 208, 253, 247, 212]),
97
+ checkoutIntent: Uint8Array.from([39, 73, 4, 146, 212, 151, 108, 241]),
98
+ cardCommitment: Uint8Array.from([43, 146, 163, 253, 221, 147, 4, 9]),
99
+ /** Zero-copy, 8 KB: the last 256 closed reservations' auth ids. */
100
+ authGuard: Uint8Array.from([53, 204, 86, 193, 155, 169, 182, 150]),
101
+ };
102
+ /** `CardPolicyError` codes (contracts.md §1.4). Append only. */
103
+ export const CARD_POLICY_ERRORS = {
104
+ 6000: "Unauthorized",
105
+ 6001: "ValidatorNotAllowed",
106
+ 6002: "PolicyNotSet",
107
+ 6003: "InvalidPolicy",
108
+ 6004: "CardFrozen",
109
+ 6005: "RecoveryFrozen",
110
+ 6006: "PolicyExpired",
111
+ 6007: "IntentInvalid",
112
+ 6008: "IntentExpired",
113
+ 6009: "IntentStale",
114
+ 6010: "MerchantMismatch",
115
+ 6011: "MerchantNotAllowed",
116
+ 6012: "MccNotAllowed",
117
+ 6013: "CurrencyMismatch",
118
+ 6014: "AmountExceedsIntent",
119
+ 6015: "AmountExceedsMax",
120
+ 6016: "BudgetExceeded",
121
+ 6017: "VelocityExceeded",
122
+ 6018: "RecurringNotAllowed",
123
+ 6019: "DuplicateAuthorization",
124
+ 6020: "DuplicateCapture",
125
+ 6021: "ReservationClosed",
126
+ 6022: "PeriodNotEnded",
127
+ 6023: "DisclosureBlocked",
128
+ 6024: "MemberLimit",
129
+ 6025: "ExceptionsOpen",
130
+ 6026: "NotInRecovery",
131
+ 6027: "StaleCommitment",
132
+ 6028: "OpenReservations",
133
+ 6029: "OutstandingBalance",
134
+ 6030: "MathOverflow",
135
+ 6031: "AuthorizerChangeRequiresFreeze",
136
+ 6032: "DuplicateRepayment",
137
+ // Appended by card_policy (contracts.md Changelog, Lane 1 / 1A).
138
+ 6033: "MemberNotFound",
139
+ 6034: "PermissionNotInitialized",
140
+ 6035: "InvalidAmount",
141
+ 6036: "NoOpenExceptions",
142
+ 6037: "ReconDigestMismatch",
143
+ 6038: "RepaymentExceedsOutstanding",
144
+ 6039: "InvalidAccount",
145
+ 6040: "PrefundTooLow",
146
+ 6041: "NotWiped",
147
+ 6042: "InvalidEventId",
148
+ 6043: "DuplicateEvent",
149
+ 6044: "EphemeralAccountsOpen",
150
+ 6045: "ReservationNotFinal",
151
+ 6046: "CoSignerRequired",
152
+ 6047: "RefundExceedsCapture",
153
+ 6048: "CaptureLimit",
154
+ 6049: "BudgetBelowCommitted",
155
+ // Appended for the ChainPay repayment CPI (2026-10-04).
156
+ 6050: "InvalidRepaymentReceipt",
157
+ 6051: "RepaymentRecipientMismatch",
158
+ 6052: "RepaymentExceedsReceipt",
159
+ 6053: "InvalidRepaymentMandate",
160
+ };
161
+ export function cardPolicyErrorName(code) {
162
+ return CARD_POLICY_ERRORS[code];
163
+ }
@@ -0,0 +1,57 @@
1
+ export type CardDraft = {
2
+ v: 1;
3
+ label: string;
4
+ budgetCents: string;
5
+ maxPurchaseCents: string;
6
+ /** Registered merchant references (Axum resolves each to an acceptor-id hash). */
7
+ merchants: string[];
8
+ mccs: number[];
9
+ periodDays: number;
10
+ /** ISO time or null for no expiry. */
11
+ expiresAt: string | null;
12
+ feeBps: number;
13
+ };
14
+ export type CardDraftInput = {
15
+ label: unknown;
16
+ budgetCents: unknown;
17
+ maxPurchaseCents: unknown;
18
+ merchants?: unknown;
19
+ mccs?: unknown;
20
+ periodDays: unknown;
21
+ expiresAt?: unknown;
22
+ feeBps?: unknown;
23
+ };
24
+ /** ChainPay's displayed platform fee when a draft doesn't name one (contracts.md §1.5 example). */
25
+ export declare const DEFAULT_CARD_FEE_BPS = 50;
26
+ export declare const MAX_PERIOD_DAYS = 365;
27
+ /** Validate and normalize an agent-proposed draft. Throws plain-language errors. */
28
+ export declare function normalizeCardDraft(input: CardDraftInput, now?: number): CardDraft;
29
+ /** hex sha256("chainpay-card-draft:v1\n" || canonical draft). The dashboard recomputes it before showing the review. */
30
+ export declare function cardDraftDigest(draft: CardDraft): Promise<string>;
31
+ export declare function cardDraftMaxObligationCents(draft: CardDraft): string;
32
+ /** `${app}/app/cards/new#draft=<base64url(JSON)>`. The fragment never reaches a server. */
33
+ export declare function encodeCardDraftLink(draft: CardDraft, appBaseUrl: string): string;
34
+ export declare function decodeCardDraftFragment(fragment: string, now?: number): CardDraft;
35
+ /** Review link with the draft digest alongside: `…/app/cards/new#draft=…&digest=<hex>`. */
36
+ export declare function encodeCardDraftReviewLink(draft: CardDraft, appBaseUrl: string): Promise<string>;
37
+ export type CardDraftIntake = {
38
+ status: "matched";
39
+ draft: CardDraft;
40
+ digest: string;
41
+ } | {
42
+ status: "mismatch";
43
+ digest: string;
44
+ expected: string;
45
+ } | {
46
+ status: "missing_digest";
47
+ digest: string;
48
+ } | {
49
+ status: "invalid";
50
+ reason: string;
51
+ };
52
+ /**
53
+ * Dashboard intake for an agent's review link. The draft is only usable when
54
+ * the digest in the link equals the digest recomputed here; otherwise the
55
+ * caller must leave the form empty.
56
+ */
57
+ export declare function verifyCardDraftFragment(fragment: string, now?: number): Promise<CardDraftIntake>;
@@ -0,0 +1,131 @@
1
+ import { MAX_BUDGET_CENTS, MAX_FEE_BPS, MAX_MCCS, MAX_MERCHANTS } from "./constants.js";
2
+ import { centsToString, maxObligationCents, parseCents } from "./math.js";
3
+ import { cardMerchantByRef } from "./merchants.js";
4
+ import { redactCardNumbersInInput } from "./redact.js";
5
+ /** ChainPay's displayed platform fee when a draft doesn't name one (contracts.md §1.5 example). */
6
+ export const DEFAULT_CARD_FEE_BPS = 50;
7
+ export const MAX_PERIOD_DAYS = 365;
8
+ const MERCHANT_REF = /^[A-Za-z0-9_.:-]{1,64}$/;
9
+ /** Validate and normalize an agent-proposed draft. Throws plain-language errors. */
10
+ export function normalizeCardDraft(input, now = Date.now()) {
11
+ if (typeof input.label !== "string" || !input.label.trim() || input.label.trim().length > 40)
12
+ throw new Error("label must be 1-40 characters");
13
+ const budget = parseCents(input.budgetCents, "budgetCents");
14
+ const maxPurchase = parseCents(input.maxPurchaseCents, "maxPurchaseCents");
15
+ if (budget <= 0n)
16
+ throw new Error("Budget must be more than $0");
17
+ if (budget > MAX_BUDGET_CENTS)
18
+ throw new Error("Budget can't be more than $10,000 in the sandbox");
19
+ if (maxPurchase <= 0n || maxPurchase > budget)
20
+ throw new Error("Max purchase must be more than $0 and no more than the budget");
21
+ const merchants = input.merchants === undefined ? [] : input.merchants;
22
+ const mccs = input.mccs === undefined ? [] : input.mccs;
23
+ if (!Array.isArray(merchants) || !merchants.every((ref) => typeof ref === "string" && MERCHANT_REF.test(ref)))
24
+ throw new Error("merchants must be a list of merchant references");
25
+ if (!Array.isArray(mccs) || !mccs.every((mcc) => Number.isInteger(mcc) && mcc >= 0 && mcc <= 9_999))
26
+ throw new Error("mccs must be a list of 4-digit merchant category codes");
27
+ if (merchants.length === 0 && mccs.length === 0)
28
+ throw new Error("Pick at least one shop or merchant category");
29
+ // A shop ChainPay can't check out at would make the owner's review link dead
30
+ // on arrival, so the draft is never "ready" with one (review F8).
31
+ const unknown = merchants.filter((ref) => !cardMerchantByRef(ref));
32
+ if (unknown.length)
33
+ throw new Error(`${unknown.map((ref) => `"${redactCardNumbersInInput(ref)}"`).join(", ")} ${unknown.length === 1 ? "isn't a shop" : "aren't shops"} ChainPay can check out at`);
34
+ if (merchants.length > MAX_MERCHANTS)
35
+ throw new Error(`At most ${MAX_MERCHANTS} shops`);
36
+ if (mccs.length > MAX_MCCS)
37
+ throw new Error(`At most ${MAX_MCCS} merchant categories`);
38
+ if (new Set(merchants).size !== merchants.length)
39
+ throw new Error("A shop is listed twice");
40
+ if (new Set(mccs).size !== mccs.length)
41
+ throw new Error("A merchant category is listed twice");
42
+ if (!Number.isInteger(input.periodDays) || input.periodDays < 1 || input.periodDays > MAX_PERIOD_DAYS)
43
+ throw new Error(`periodDays must be a whole number from 1 to ${MAX_PERIOD_DAYS}`);
44
+ const feeBps = input.feeBps === undefined ? DEFAULT_CARD_FEE_BPS : input.feeBps;
45
+ if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > MAX_FEE_BPS)
46
+ throw new Error("feeBps must be a whole number from 0 to 1000");
47
+ let expiresAt = null;
48
+ if (input.expiresAt !== undefined && input.expiresAt !== null) {
49
+ const ms = typeof input.expiresAt === "string" ? Date.parse(input.expiresAt) : Number.NaN;
50
+ if (Number.isNaN(ms))
51
+ throw new Error("expiresAt must be an ISO date");
52
+ if (ms <= now)
53
+ throw new Error("expiresAt must be in the future");
54
+ expiresAt = new Date(ms).toISOString();
55
+ }
56
+ return {
57
+ v: 1,
58
+ label: input.label.trim(),
59
+ budgetCents: centsToString(budget),
60
+ maxPurchaseCents: centsToString(maxPurchase),
61
+ merchants: [...merchants],
62
+ mccs: [...mccs],
63
+ periodDays: input.periodDays,
64
+ expiresAt,
65
+ feeBps: feeBps,
66
+ };
67
+ }
68
+ function canonical(draft) {
69
+ return JSON.stringify([draft.v, draft.label, draft.budgetCents, draft.maxPurchaseCents, draft.merchants, draft.mccs, draft.periodDays, draft.expiresAt, draft.feeBps]);
70
+ }
71
+ /** hex sha256("chainpay-card-draft:v1\n" || canonical draft). The dashboard recomputes it before showing the review. */
72
+ export async function cardDraftDigest(draft) {
73
+ const bytes = new TextEncoder().encode(`chainpay-card-draft:v1\n${canonical(draft)}`);
74
+ const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes.slice().buffer));
75
+ return Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
76
+ }
77
+ export function cardDraftMaxObligationCents(draft) {
78
+ return centsToString(maxObligationCents(BigInt(draft.budgetCents), draft.feeBps));
79
+ }
80
+ function toBase64Url(text) {
81
+ const bytes = new TextEncoder().encode(text);
82
+ let binary = "";
83
+ for (const byte of bytes)
84
+ binary += String.fromCharCode(byte);
85
+ return globalThis.btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
86
+ }
87
+ function fromBase64Url(value) {
88
+ const binary = globalThis.atob(value.replace(/-/g, "+").replace(/_/g, "/"));
89
+ return new TextDecoder().decode(Uint8Array.from(binary, (char) => char.charCodeAt(0)));
90
+ }
91
+ /** `${app}/app/cards/new#draft=<base64url(JSON)>`. The fragment never reaches a server. */
92
+ export function encodeCardDraftLink(draft, appBaseUrl) {
93
+ const origin = appBaseUrl.trim().replace(/\/+$/, "");
94
+ if (!/^https?:\/\//.test(origin))
95
+ throw new Error("App URL must be http(s)");
96
+ return `${origin}/app/cards/new#draft=${toBase64Url(JSON.stringify(draft))}`;
97
+ }
98
+ export function decodeCardDraftFragment(fragment, now = Date.now()) {
99
+ const match = fragment.match(/(?:^#?|&)draft=([A-Za-z0-9_-]+)/);
100
+ if (!match)
101
+ throw new Error("No card draft in this link");
102
+ const raw = JSON.parse(fromBase64Url(match[1]));
103
+ if (raw.v !== 1)
104
+ throw new Error("Unsupported card draft version");
105
+ return normalizeCardDraft(raw, now);
106
+ }
107
+ /** Review link with the draft digest alongside: `…/app/cards/new#draft=…&digest=<hex>`. */
108
+ export async function encodeCardDraftReviewLink(draft, appBaseUrl) {
109
+ return `${encodeCardDraftLink(draft, appBaseUrl)}&digest=${await cardDraftDigest(draft)}`;
110
+ }
111
+ /**
112
+ * Dashboard intake for an agent's review link. The draft is only usable when
113
+ * the digest in the link equals the digest recomputed here; otherwise the
114
+ * caller must leave the form empty.
115
+ */
116
+ export async function verifyCardDraftFragment(fragment, now = Date.now()) {
117
+ let draft;
118
+ try {
119
+ draft = decodeCardDraftFragment(fragment, now);
120
+ }
121
+ catch (error) {
122
+ return { status: "invalid", reason: error instanceof Error ? error.message : "This link can't be read" };
123
+ }
124
+ const digest = await cardDraftDigest(draft);
125
+ const claimed = fragment.match(/(?:^#?|&)digest=([0-9a-f]{64})(?:&|$)/)?.[1];
126
+ if (!claimed)
127
+ return { status: "missing_digest", digest };
128
+ if (claimed !== digest)
129
+ return { status: "mismatch", digest, expected: claimed };
130
+ return { status: "matched", draft, digest };
131
+ }
@@ -0,0 +1,69 @@
1
+ export type CardDeclineReason = "frozen" | "merchant_not_allowed" | "over_budget" | "over_max" | "velocity" | "intent_missing" | "internal";
2
+ export type CardMerchantView = {
3
+ displayName: string;
4
+ mcc: string;
5
+ };
6
+ export type SplSettlementEvidence = {
7
+ kind: "spl_settlement";
8
+ receiptPda: string;
9
+ };
10
+ export type CardAuthorizationEvidence = {
11
+ kind: "card_authorization";
12
+ cardId: string;
13
+ intentId: string;
14
+ merchant: CardMerchantView;
15
+ amountCents: string;
16
+ currency: "USD";
17
+ reservationState: "reserved" | "declined" | "ambiguous";
18
+ decision: "approved" | "declined";
19
+ declineReason?: CardDeclineReason;
20
+ at: string;
21
+ private: true;
22
+ };
23
+ export type CardCaptureLifecycle = "captured" | "partially_captured" | "reversed" | "expired" | "late_capture" | "refunded" | "forced_capture";
24
+ export type CardCaptureEvidence = {
25
+ kind: "card_capture";
26
+ cardId: string;
27
+ authId: string;
28
+ merchant: CardMerchantView;
29
+ capturedCents: string;
30
+ reservedCents: string;
31
+ lifecycle: CardCaptureLifecycle;
32
+ exception?: "forced_capture" | "over_capture" | "unpaired_capture";
33
+ commitment?: {
34
+ seq: string;
35
+ root: string;
36
+ slot: string;
37
+ };
38
+ private: true;
39
+ };
40
+ export type StatementRepaymentState = "repayment_observed" | "partner_confirmed" | "discharged" | "repayment_mismatch";
41
+ export type StatementRepaymentEvidence = {
42
+ kind: "statement_repayment";
43
+ cardId: string;
44
+ statementId: string;
45
+ statementDigest: string;
46
+ totalCents: string;
47
+ receiptPda: string;
48
+ repaymentState: StatementRepaymentState;
49
+ simulatedCredit: true;
50
+ };
51
+ export type CardEvidence = CardAuthorizationEvidence | CardCaptureEvidence | StatementRepaymentEvidence;
52
+ export type ReceiptEvidence = SplSettlementEvidence | CardEvidence;
53
+ export type ReceiptEvidenceKind = ReceiptEvidence["kind"];
54
+ export declare const CARD_EVIDENCE_KINDS: readonly ["card_authorization", "card_capture", "statement_repayment"];
55
+ export declare function isCardEvidence(evidence: ReceiptEvidence): evidence is CardEvidence;
56
+ /** Only `spl_settlement` may use the on-chain settlement layout and wording. */
57
+ export declare function mayRenderAsSplSettlement(evidence: ReceiptEvidence): boolean;
58
+ /** Reservation lifecycle (contracts.md §4.1). `pending` and `ambiguous` are off-chain only. */
59
+ export type CardReservationLifecycle = "pending" | "reserved" | "captured" | "partially_captured" | "reversed" | "expired" | "declined" | "ambiguous";
60
+ /** Statement lifecycle (contracts.md §4.2). Every statement is simulated credit. */
61
+ export type StatementState = "open" | "closed" | "repayment_observed" | "partner_confirmed" | "discharged" | "repayment_mismatch" | "overdue";
62
+ export type AsaResult = "APPROVED" | "CARD_PAUSED" | "UNAUTHORIZED_MERCHANT" | "INSUFFICIENT_FUNDS" | "VELOCITY_EXCEEDED" | "SUSPECTED_FRAUD";
63
+ /** Map a program error code to the ASA decision and the receipt decline reason (contracts.md §3.1). */
64
+ export declare function declineForProgramError(code: number): {
65
+ asa: AsaResult;
66
+ reason: CardDeclineReason;
67
+ };
68
+ /** Plain-language decline copy for owners and agents. Never reveals the limit itself. */
69
+ export declare const DECLINE_COPY: Record<CardDeclineReason, string>;
@@ -0,0 +1,41 @@
1
+ import { cardPolicyErrorName } from "./constants.js";
2
+ export const CARD_EVIDENCE_KINDS = ["card_authorization", "card_capture", "statement_repayment"];
3
+ export function isCardEvidence(evidence) {
4
+ return CARD_EVIDENCE_KINDS.includes(evidence.kind);
5
+ }
6
+ /** Only `spl_settlement` may use the on-chain settlement layout and wording. */
7
+ export function mayRenderAsSplSettlement(evidence) {
8
+ return evidence.kind === "spl_settlement";
9
+ }
10
+ const DECLINES = {
11
+ CardFrozen: { asa: "CARD_PAUSED", reason: "frozen" },
12
+ RecoveryFrozen: { asa: "CARD_PAUSED", reason: "frozen" },
13
+ PolicyExpired: { asa: "CARD_PAUSED", reason: "frozen" },
14
+ MerchantMismatch: { asa: "UNAUTHORIZED_MERCHANT", reason: "merchant_not_allowed" },
15
+ MerchantNotAllowed: { asa: "UNAUTHORIZED_MERCHANT", reason: "merchant_not_allowed" },
16
+ MccNotAllowed: { asa: "UNAUTHORIZED_MERCHANT", reason: "merchant_not_allowed" },
17
+ CurrencyMismatch: { asa: "UNAUTHORIZED_MERCHANT", reason: "merchant_not_allowed" },
18
+ RecurringNotAllowed: { asa: "UNAUTHORIZED_MERCHANT", reason: "merchant_not_allowed" },
19
+ IntentInvalid: { asa: "UNAUTHORIZED_MERCHANT", reason: "intent_missing" },
20
+ IntentExpired: { asa: "UNAUTHORIZED_MERCHANT", reason: "intent_missing" },
21
+ IntentStale: { asa: "UNAUTHORIZED_MERCHANT", reason: "intent_missing" },
22
+ BudgetExceeded: { asa: "INSUFFICIENT_FUNDS", reason: "over_budget" },
23
+ AmountExceedsIntent: { asa: "INSUFFICIENT_FUNDS", reason: "over_max" },
24
+ AmountExceedsMax: { asa: "INSUFFICIENT_FUNDS", reason: "over_max" },
25
+ VelocityExceeded: { asa: "VELOCITY_EXCEEDED", reason: "velocity" },
26
+ };
27
+ /** Map a program error code to the ASA decision and the receipt decline reason (contracts.md §3.1). */
28
+ export function declineForProgramError(code) {
29
+ const name = cardPolicyErrorName(code);
30
+ return (name && DECLINES[name]) || { asa: "SUSPECTED_FRAUD", reason: "internal" };
31
+ }
32
+ /** Plain-language decline copy for owners and agents. Never reveals the limit itself. */
33
+ export const DECLINE_COPY = {
34
+ frozen: "The card is frozen, so the purchase was declined.",
35
+ merchant_not_allowed: "This shop isn't on the card's list.",
36
+ over_budget: "This purchase would go over what's left this period.",
37
+ over_max: "This purchase is bigger than the card allows in one go.",
38
+ velocity: "The card has hit its number of purchases for this period.",
39
+ intent_missing: "No matching checkout was opened for this purchase.",
40
+ internal: "ChainPay couldn't confirm this purchase in time, so it was declined.",
41
+ };
@@ -0,0 +1,9 @@
1
+ /** sha256("chainpay-merchant:v1\n" || UPPER(trim(acceptor_id))). */
2
+ export declare function merchantIdHash(acceptorId: string): Promise<Uint8Array>;
3
+ /** sha256("chainpay-auth-id:v1\n" || issuer_u8 || lithic_txn_token_utf8). */
4
+ export declare function authIdHash(issuer: number, issuerTransactionToken: string): Promise<Uint8Array>;
5
+ /** sha256("chainpay-card-ref:v1\n" || lithic_card_token_utf8 || ref_salt32). */
6
+ export declare function issuerCardRefHash(issuerCardToken: string, refSalt: Uint8Array): Promise<Uint8Array>;
7
+ /** `cpcap_v1_<base64url(32 random bytes)>` (contracts.md §6). Opaque; carries no card data. */
8
+ export declare const CHECKOUT_CAPABILITY_PATTERN: RegExp;
9
+ export declare function isCheckoutCapability(value: unknown): value is string;
@@ -0,0 +1,47 @@
1
+ /*
2
+ * Domain-separated hashes the program and Axum agree on (contracts.md §1.2).
3
+ * WebCrypto only, so the same code runs in the browser, Node and workers.
4
+ */
5
+ async function sha256(bytes) {
6
+ return new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", bytes.slice().buffer));
7
+ }
8
+ function utf8(value) {
9
+ return new TextEncoder().encode(value);
10
+ }
11
+ function join(...parts) {
12
+ const out = new Uint8Array(parts.reduce((sum, part) => sum + part.length, 0));
13
+ let offset = 0;
14
+ for (const part of parts) {
15
+ out.set(part, offset);
16
+ offset += part.length;
17
+ }
18
+ return out;
19
+ }
20
+ /** sha256("chainpay-merchant:v1\n" || UPPER(trim(acceptor_id))). */
21
+ export async function merchantIdHash(acceptorId) {
22
+ const normalized = acceptorId.trim().toUpperCase();
23
+ if (!normalized)
24
+ throw new Error("merchant acceptor id is required");
25
+ return sha256(utf8(`chainpay-merchant:v1\n${normalized}`));
26
+ }
27
+ /** sha256("chainpay-auth-id:v1\n" || issuer_u8 || lithic_txn_token_utf8). */
28
+ export async function authIdHash(issuer, issuerTransactionToken) {
29
+ if (!Number.isInteger(issuer) || issuer < 0 || issuer > 255)
30
+ throw new Error("issuer must fit in u8");
31
+ if (!issuerTransactionToken)
32
+ throw new Error("issuer transaction token is required");
33
+ return sha256(join(utf8("chainpay-auth-id:v1\n"), Uint8Array.of(issuer), utf8(issuerTransactionToken)));
34
+ }
35
+ /** sha256("chainpay-card-ref:v1\n" || lithic_card_token_utf8 || ref_salt32). */
36
+ export async function issuerCardRefHash(issuerCardToken, refSalt) {
37
+ if (refSalt.length !== 32)
38
+ throw new Error("refSalt must be exactly 32 bytes");
39
+ if (!issuerCardToken)
40
+ throw new Error("issuer card token is required");
41
+ return sha256(join(utf8("chainpay-card-ref:v1\n"), utf8(issuerCardToken), refSalt));
42
+ }
43
+ /** `cpcap_v1_<base64url(32 random bytes)>` (contracts.md §6). Opaque; carries no card data. */
44
+ export const CHECKOUT_CAPABILITY_PATTERN = /^cpcap_v1_[A-Za-z0-9_-]{43}$/;
45
+ export function isCheckoutCapability(value) {
46
+ return typeof value === "string" && CHECKOUT_CAPABILITY_PATTERN.test(value);
47
+ }
@@ -0,0 +1,14 @@
1
+ export * from "./constants.js";
2
+ export * from "./layout.js";
3
+ export * from "./accounts.js";
4
+ export * from "./pda.js";
5
+ export * from "./instructions.js";
6
+ export * from "./math.js";
7
+ export * from "./hash.js";
8
+ export * from "./redact.js";
9
+ export * from "./evidence.js";
10
+ export * from "./api.js";
11
+ export * from "./tee.js";
12
+ export * from "./draft.js";
13
+ export * from "./commitment.js";
14
+ export * from "./merchants.js";
@@ -0,0 +1,14 @@
1
+ export * from "./constants.js";
2
+ export * from "./layout.js";
3
+ export * from "./accounts.js";
4
+ export * from "./pda.js";
5
+ export * from "./instructions.js";
6
+ export * from "./math.js";
7
+ export * from "./hash.js";
8
+ export * from "./redact.js";
9
+ export * from "./evidence.js";
10
+ export * from "./api.js";
11
+ export * from "./tee.js";
12
+ export * from "./draft.js";
13
+ export * from "./commitment.js";
14
+ export * from "./merchants.js";