@agentspend/sdk 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/client.d.ts +6 -0
- package/dist/core/client.js +308 -0
- package/dist/core/error.d.ts +5 -0
- package/dist/core/error.js +9 -0
- package/dist/core/helpers.d.ts +8 -0
- package/dist/core/helpers.js +77 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +4 -0
- package/dist/{index.d.ts → core/types.d.ts} +21 -22
- package/dist/core/types.js +4 -0
- package/dist/express/index.d.ts +22 -0
- package/dist/express/index.js +50 -0
- package/dist/fastify/index.d.ts +27 -0
- package/dist/fastify/index.js +58 -0
- package/dist/hono/index.d.ts +20 -0
- package/dist/hono/index.js +49 -0
- package/dist/next/index.d.ts +15 -0
- package/dist/next/index.js +39 -0
- package/package.json +50 -3
- package/src/core/client.ts +433 -0
- package/src/core/error.ts +10 -0
- package/src/core/helpers.ts +87 -0
- package/src/core/index.ts +5 -0
- package/src/core/types.ts +104 -0
- package/src/express/index.ts +101 -0
- package/src/fastify/index.ts +120 -0
- package/src/hono/index.ts +97 -0
- package/src/next/index.ts +92 -0
- package/dist/index.js +0 -420
- package/src/index.ts +0 -662
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgentSpendOptions, ChargeOptions, ChargeResponse, PaywallOptions, PaywallRequest, PaywallResult } from "./types.js";
|
|
2
|
+
export interface AgentSpendClient {
|
|
3
|
+
charge(cardId: string, opts: ChargeOptions): Promise<ChargeResponse>;
|
|
4
|
+
processPaywall(opts: PaywallOptions, request: PaywallRequest): Promise<PaywallResult>;
|
|
5
|
+
}
|
|
6
|
+
export declare function createAgentSpendClient(options: AgentSpendOptions): AgentSpendClient;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { HTTPFacilitatorClient, x402ResourceServer } from "@x402/core/server";
|
|
2
|
+
import { registerExactEvmScheme } from "@x402/evm/exact/server";
|
|
3
|
+
import { AgentSpendChargeError } from "./error.js";
|
|
4
|
+
import { toCardId, joinUrl, bestEffortIdempotencyKey, toStringMetadata, resolvePlatformApiBaseUrl, resolveAmount, extractCardId, extractPaymentHeader } from "./helpers.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Factory
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
export function createAgentSpendClient(options) {
|
|
9
|
+
if (!options.serviceApiKey && !options.crypto) {
|
|
10
|
+
throw new AgentSpendChargeError("At least one of serviceApiKey or crypto config must be provided", 500);
|
|
11
|
+
}
|
|
12
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
13
|
+
if (!fetchImpl) {
|
|
14
|
+
throw new AgentSpendChargeError("No fetch implementation available", 500);
|
|
15
|
+
}
|
|
16
|
+
const platformApiBaseUrl = resolvePlatformApiBaseUrl(options.platformApiBaseUrl);
|
|
17
|
+
// -------------------------------------------------------------------
|
|
18
|
+
// Lazy service_id fetch + cache
|
|
19
|
+
// -------------------------------------------------------------------
|
|
20
|
+
let cachedServiceId = null;
|
|
21
|
+
async function getServiceId() {
|
|
22
|
+
if (cachedServiceId)
|
|
23
|
+
return cachedServiceId;
|
|
24
|
+
if (!options.serviceApiKey)
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetchImpl(joinUrl(platformApiBaseUrl, "/v1/service/me"), {
|
|
28
|
+
headers: { authorization: `Bearer ${options.serviceApiKey}` }
|
|
29
|
+
});
|
|
30
|
+
if (res.ok) {
|
|
31
|
+
const data = (await res.json());
|
|
32
|
+
cachedServiceId = data.id ?? null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch { /* graceful fallback */ }
|
|
36
|
+
return cachedServiceId;
|
|
37
|
+
}
|
|
38
|
+
// -------------------------------------------------------------------
|
|
39
|
+
// x402 singleton setup
|
|
40
|
+
// -------------------------------------------------------------------
|
|
41
|
+
let facilitator = null;
|
|
42
|
+
let resourceServer = null;
|
|
43
|
+
const cryptoNetwork = (options.crypto?.network ?? "eip155:8453");
|
|
44
|
+
if (options.crypto || options.serviceApiKey) {
|
|
45
|
+
const facilitatorUrl = options.crypto?.facilitatorUrl ?? "https://facilitator.openx402.ai";
|
|
46
|
+
facilitator = new HTTPFacilitatorClient({ url: facilitatorUrl });
|
|
47
|
+
resourceServer = new x402ResourceServer(facilitator);
|
|
48
|
+
registerExactEvmScheme(resourceServer);
|
|
49
|
+
}
|
|
50
|
+
// -------------------------------------------------------------------
|
|
51
|
+
// charge() — card-only
|
|
52
|
+
// -------------------------------------------------------------------
|
|
53
|
+
async function charge(cardIdInput, opts) {
|
|
54
|
+
if (!options.serviceApiKey) {
|
|
55
|
+
throw new AgentSpendChargeError("charge() requires serviceApiKey", 500);
|
|
56
|
+
}
|
|
57
|
+
const cardId = toCardId(cardIdInput);
|
|
58
|
+
if (!cardId) {
|
|
59
|
+
throw new AgentSpendChargeError("card_id must start with card_", 400);
|
|
60
|
+
}
|
|
61
|
+
if (!Number.isInteger(opts.amount_cents) || opts.amount_cents <= 0) {
|
|
62
|
+
throw new AgentSpendChargeError("amount_cents must be a positive integer", 400);
|
|
63
|
+
}
|
|
64
|
+
const payload = {
|
|
65
|
+
card_id: cardId,
|
|
66
|
+
amount_cents: opts.amount_cents,
|
|
67
|
+
currency: opts.currency ?? "usd",
|
|
68
|
+
...(opts.description ? { description: opts.description } : {}),
|
|
69
|
+
...(opts.metadata ? { metadata: opts.metadata } : {}),
|
|
70
|
+
idempotency_key: opts.idempotency_key ?? bestEffortIdempotencyKey()
|
|
71
|
+
};
|
|
72
|
+
const response = await fetchImpl(joinUrl(platformApiBaseUrl, "/v1/charge"), {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: {
|
|
75
|
+
authorization: `Bearer ${options.serviceApiKey}`,
|
|
76
|
+
"content-type": "application/json"
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify(payload)
|
|
79
|
+
});
|
|
80
|
+
const responseBody = (await response.json().catch(() => ({})));
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new AgentSpendChargeError(typeof responseBody.error === "string" ? responseBody.error : "AgentSpend charge failed", response.status, responseBody);
|
|
83
|
+
}
|
|
84
|
+
return responseBody;
|
|
85
|
+
}
|
|
86
|
+
// -------------------------------------------------------------------
|
|
87
|
+
// resolvePayToAddress
|
|
88
|
+
// -------------------------------------------------------------------
|
|
89
|
+
async function resolvePayToAddress() {
|
|
90
|
+
if (options.crypto?.receiverAddress) {
|
|
91
|
+
return options.crypto.receiverAddress;
|
|
92
|
+
}
|
|
93
|
+
if (options.serviceApiKey) {
|
|
94
|
+
const response = await fetchImpl(joinUrl(platformApiBaseUrl, "/v1/crypto/deposit-address"), {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: {
|
|
97
|
+
authorization: `Bearer ${options.serviceApiKey}`,
|
|
98
|
+
"content-type": "application/json"
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify({ amount_cents: 0, currency: "usd" })
|
|
101
|
+
});
|
|
102
|
+
if (!response.ok) {
|
|
103
|
+
throw new AgentSpendChargeError("Failed to resolve crypto deposit address", 502);
|
|
104
|
+
}
|
|
105
|
+
const data = (await response.json());
|
|
106
|
+
if (!data.deposit_address) {
|
|
107
|
+
throw new AgentSpendChargeError("No deposit address returned", 502);
|
|
108
|
+
}
|
|
109
|
+
return data.deposit_address;
|
|
110
|
+
}
|
|
111
|
+
throw new AgentSpendChargeError("No crypto payTo address available", 500);
|
|
112
|
+
}
|
|
113
|
+
// -------------------------------------------------------------------
|
|
114
|
+
// build402Result — x402 Payment-Required format
|
|
115
|
+
// -------------------------------------------------------------------
|
|
116
|
+
async function build402Result(requestUrl, amountCents, currency) {
|
|
117
|
+
const serviceId = await getServiceId();
|
|
118
|
+
try {
|
|
119
|
+
const payTo = await resolvePayToAddress();
|
|
120
|
+
const paymentRequirements = {
|
|
121
|
+
scheme: "exact",
|
|
122
|
+
network: cryptoNetwork,
|
|
123
|
+
amount: String(amountCents),
|
|
124
|
+
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
|
|
125
|
+
payTo,
|
|
126
|
+
maxTimeoutSeconds: 300,
|
|
127
|
+
extra: { name: "USD Coin", version: "2" }
|
|
128
|
+
};
|
|
129
|
+
const paymentRequired = {
|
|
130
|
+
x402Version: 2,
|
|
131
|
+
error: "Payment required",
|
|
132
|
+
resource: {
|
|
133
|
+
url: requestUrl,
|
|
134
|
+
description: `Payment of ${amountCents} cents`,
|
|
135
|
+
mimeType: "application/json"
|
|
136
|
+
},
|
|
137
|
+
accepts: [paymentRequirements]
|
|
138
|
+
};
|
|
139
|
+
const headerValue = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
|
|
140
|
+
return {
|
|
141
|
+
outcome: "payment_required",
|
|
142
|
+
statusCode: 402,
|
|
143
|
+
body: {
|
|
144
|
+
error: "Payment required",
|
|
145
|
+
amount_cents: amountCents,
|
|
146
|
+
currency,
|
|
147
|
+
...(serviceId ? {
|
|
148
|
+
agentspend: {
|
|
149
|
+
service_id: serviceId,
|
|
150
|
+
amount_cents: amountCents,
|
|
151
|
+
}
|
|
152
|
+
} : {})
|
|
153
|
+
},
|
|
154
|
+
headers: { "Payment-Required": headerValue }
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
console.error("[agentspend] Failed to resolve crypto payTo address — returning card-only 402:", error instanceof Error ? error.message : error);
|
|
159
|
+
return {
|
|
160
|
+
outcome: "payment_required",
|
|
161
|
+
statusCode: 402,
|
|
162
|
+
body: {
|
|
163
|
+
error: "Payment required",
|
|
164
|
+
amount_cents: amountCents,
|
|
165
|
+
currency,
|
|
166
|
+
...(serviceId ? {
|
|
167
|
+
agentspend: {
|
|
168
|
+
service_id: serviceId,
|
|
169
|
+
amount_cents: amountCents,
|
|
170
|
+
}
|
|
171
|
+
} : {})
|
|
172
|
+
},
|
|
173
|
+
headers: {}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// -------------------------------------------------------------------
|
|
178
|
+
// handleCardPayment
|
|
179
|
+
// -------------------------------------------------------------------
|
|
180
|
+
async function handleCardPayment(request, cardId, amountCents, currency, body, opts) {
|
|
181
|
+
if (!options.serviceApiKey) {
|
|
182
|
+
return { outcome: "error", statusCode: 500, body: { error: "Card payments require serviceApiKey" } };
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const chargeResult = await charge(cardId, {
|
|
186
|
+
amount_cents: amountCents,
|
|
187
|
+
currency,
|
|
188
|
+
description: opts.description,
|
|
189
|
+
metadata: opts.metadata ? toStringMetadata(opts.metadata(body)) : undefined,
|
|
190
|
+
idempotency_key: request.headers["x-request-id"] ?? request.headers["idempotency-key"] ?? undefined
|
|
191
|
+
});
|
|
192
|
+
const paymentContext = {
|
|
193
|
+
method: "card",
|
|
194
|
+
amount_cents: amountCents,
|
|
195
|
+
currency,
|
|
196
|
+
card_id: cardId,
|
|
197
|
+
remaining_limit_cents: chargeResult.remaining_limit_cents
|
|
198
|
+
};
|
|
199
|
+
return { outcome: "charged", paymentContext };
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error instanceof AgentSpendChargeError) {
|
|
203
|
+
if (error.statusCode === 403) {
|
|
204
|
+
return build402Result(request.url, amountCents, currency);
|
|
205
|
+
}
|
|
206
|
+
if (error.statusCode === 402) {
|
|
207
|
+
return { outcome: "error", statusCode: 402, body: { error: "Payment required", details: error.details } };
|
|
208
|
+
}
|
|
209
|
+
return { outcome: "error", statusCode: error.statusCode, body: { error: error.message, details: error.details } };
|
|
210
|
+
}
|
|
211
|
+
return { outcome: "error", statusCode: 500, body: { error: "Unexpected paywall failure" } };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// -------------------------------------------------------------------
|
|
215
|
+
// handleCryptoPayment
|
|
216
|
+
// -------------------------------------------------------------------
|
|
217
|
+
async function handleCryptoPayment(paymentHeader, amountCents, currency) {
|
|
218
|
+
if (!resourceServer) {
|
|
219
|
+
return { outcome: "error", statusCode: 500, body: { error: "Crypto payments not configured" } };
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
let paymentPayload;
|
|
223
|
+
try {
|
|
224
|
+
paymentPayload = JSON.parse(Buffer.from(paymentHeader, "base64").toString("utf-8"));
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return { outcome: "error", statusCode: 400, body: { error: "Invalid payment payload encoding" } };
|
|
228
|
+
}
|
|
229
|
+
const acceptedPayTo = paymentPayload
|
|
230
|
+
.accepted?.payTo;
|
|
231
|
+
const payTo = acceptedPayTo ?? await resolvePayToAddress();
|
|
232
|
+
const paymentRequirements = {
|
|
233
|
+
scheme: "exact",
|
|
234
|
+
network: cryptoNetwork,
|
|
235
|
+
amount: String(amountCents),
|
|
236
|
+
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
|
|
237
|
+
payTo,
|
|
238
|
+
maxTimeoutSeconds: 300,
|
|
239
|
+
extra: { name: "USD Coin", version: "2" }
|
|
240
|
+
};
|
|
241
|
+
const verifyResult = await resourceServer.verifyPayment(paymentPayload, paymentRequirements);
|
|
242
|
+
if (!verifyResult.isValid) {
|
|
243
|
+
return {
|
|
244
|
+
outcome: "error",
|
|
245
|
+
statusCode: 402,
|
|
246
|
+
body: { error: "Payment verification failed", details: verifyResult.invalidReason }
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const settleResult = await resourceServer.settlePayment(paymentPayload, paymentRequirements);
|
|
250
|
+
if (!settleResult.success) {
|
|
251
|
+
return {
|
|
252
|
+
outcome: "error",
|
|
253
|
+
statusCode: 402,
|
|
254
|
+
body: { error: "Payment settlement failed", details: settleResult.errorReason }
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const paymentContext = {
|
|
258
|
+
method: "crypto",
|
|
259
|
+
amount_cents: amountCents,
|
|
260
|
+
currency,
|
|
261
|
+
transaction_hash: settleResult.transaction,
|
|
262
|
+
payer_address: verifyResult.payer ?? undefined,
|
|
263
|
+
network: cryptoNetwork
|
|
264
|
+
};
|
|
265
|
+
return { outcome: "crypto_paid", paymentContext };
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (error instanceof AgentSpendChargeError) {
|
|
269
|
+
return { outcome: "error", statusCode: error.statusCode, body: { error: error.message, details: error.details } };
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
outcome: "error",
|
|
273
|
+
statusCode: 500,
|
|
274
|
+
body: { error: "Crypto payment processing failed", details: error.message }
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// -------------------------------------------------------------------
|
|
279
|
+
// processPaywall() — unified entry point for adapters
|
|
280
|
+
// -------------------------------------------------------------------
|
|
281
|
+
async function processPaywall(opts, request) {
|
|
282
|
+
const { amount } = opts;
|
|
283
|
+
if (typeof amount === "number") {
|
|
284
|
+
if (!Number.isInteger(amount) || amount <= 0) {
|
|
285
|
+
throw new AgentSpendChargeError("amount must be a positive integer", 500);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const body = request.body;
|
|
289
|
+
const effectiveAmount = resolveAmount(amount, body);
|
|
290
|
+
if (!Number.isInteger(effectiveAmount) || effectiveAmount <= 0) {
|
|
291
|
+
return { outcome: "error", statusCode: 400, body: { error: "Could not determine payment amount from request" } };
|
|
292
|
+
}
|
|
293
|
+
const currency = opts.currency ?? "usd";
|
|
294
|
+
// Check for crypto payment header
|
|
295
|
+
const paymentHeader = extractPaymentHeader(request.headers);
|
|
296
|
+
if (paymentHeader) {
|
|
297
|
+
return handleCryptoPayment(paymentHeader, effectiveAmount, currency);
|
|
298
|
+
}
|
|
299
|
+
// Check for card payment
|
|
300
|
+
const cardId = extractCardId(request.headers, body);
|
|
301
|
+
if (cardId) {
|
|
302
|
+
return handleCardPayment(request, cardId, effectiveAmount, currency, body, opts);
|
|
303
|
+
}
|
|
304
|
+
// Neither → 402
|
|
305
|
+
return build402Result(request.url, effectiveAmount, currency);
|
|
306
|
+
}
|
|
307
|
+
return { charge, processPaywall };
|
|
308
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function toCardId(input: unknown): string | null;
|
|
2
|
+
export declare function joinUrl(base: string, path: string): string;
|
|
3
|
+
export declare function bestEffortIdempotencyKey(): string;
|
|
4
|
+
export declare function toStringMetadata(input: unknown): Record<string, string>;
|
|
5
|
+
export declare function resolvePlatformApiBaseUrl(explicitBaseUrl: string | undefined): string;
|
|
6
|
+
export declare function resolveAmount(amount: number | string | ((body: unknown) => number), body: unknown): number;
|
|
7
|
+
export declare function extractCardId(headers: Record<string, string | undefined>, body: unknown): string | null;
|
|
8
|
+
export declare function extractPaymentHeader(headers: Record<string, string | undefined>): string | undefined;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export function toCardId(input) {
|
|
2
|
+
if (typeof input !== "string") {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
const trimmed = input.trim();
|
|
6
|
+
if (!trimmed.startsWith("card_")) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
return trimmed;
|
|
10
|
+
}
|
|
11
|
+
export function joinUrl(base, path) {
|
|
12
|
+
const normalizedBase = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
13
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
14
|
+
return `${normalizedBase}${normalizedPath}`;
|
|
15
|
+
}
|
|
16
|
+
export function bestEffortIdempotencyKey() {
|
|
17
|
+
const uuid = globalThis.crypto?.randomUUID?.();
|
|
18
|
+
if (uuid) {
|
|
19
|
+
return uuid;
|
|
20
|
+
}
|
|
21
|
+
return `auto_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
22
|
+
}
|
|
23
|
+
export function toStringMetadata(input) {
|
|
24
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
const result = {};
|
|
28
|
+
for (const [key, value] of Object.entries(input)) {
|
|
29
|
+
if (typeof value === "string") {
|
|
30
|
+
result[key] = value;
|
|
31
|
+
}
|
|
32
|
+
else if (typeof value === "number" && Number.isFinite(value)) {
|
|
33
|
+
result[key] = String(value);
|
|
34
|
+
}
|
|
35
|
+
else if (typeof value === "boolean") {
|
|
36
|
+
result[key] = value ? "true" : "false";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
const DEFAULT_PLATFORM_API_BASE_URL = "https://api.agentspend.co";
|
|
42
|
+
export function resolvePlatformApiBaseUrl(explicitBaseUrl) {
|
|
43
|
+
if (explicitBaseUrl && explicitBaseUrl.trim().length > 0) {
|
|
44
|
+
return explicitBaseUrl.trim();
|
|
45
|
+
}
|
|
46
|
+
const envValue = typeof process !== "undefined" && process.env ? process.env.AGENTSPEND_API_URL : undefined;
|
|
47
|
+
if (typeof envValue === "string" && envValue.trim().length > 0) {
|
|
48
|
+
return envValue.trim();
|
|
49
|
+
}
|
|
50
|
+
return DEFAULT_PLATFORM_API_BASE_URL;
|
|
51
|
+
}
|
|
52
|
+
export function resolveAmount(amount, body) {
|
|
53
|
+
if (typeof amount === "number") {
|
|
54
|
+
return amount;
|
|
55
|
+
}
|
|
56
|
+
else if (typeof amount === "string") {
|
|
57
|
+
const raw = body?.[amount];
|
|
58
|
+
return typeof raw === "number" ? raw : 0;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
return amount(body);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function extractCardId(headers, body) {
|
|
65
|
+
const cardIdFromHeader = headers["x-card-id"];
|
|
66
|
+
let cardId = cardIdFromHeader ? toCardId(cardIdFromHeader) : null;
|
|
67
|
+
if (!cardId) {
|
|
68
|
+
const bodyCardId = typeof body?.card_id === "string"
|
|
69
|
+
? body.card_id
|
|
70
|
+
: null;
|
|
71
|
+
cardId = toCardId(bodyCardId);
|
|
72
|
+
}
|
|
73
|
+
return cardId;
|
|
74
|
+
}
|
|
75
|
+
export function extractPaymentHeader(headers) {
|
|
76
|
+
return headers["payment-signature"] ?? headers["x-payment"] ?? undefined;
|
|
77
|
+
}
|
|
@@ -58,23 +58,6 @@ export interface ChargeOptions {
|
|
|
58
58
|
metadata?: Record<string, string>;
|
|
59
59
|
idempotency_key?: string;
|
|
60
60
|
}
|
|
61
|
-
export declare class AgentSpendChargeError extends Error {
|
|
62
|
-
statusCode: number;
|
|
63
|
-
details: unknown;
|
|
64
|
-
constructor(message: string, statusCode: number, details?: unknown);
|
|
65
|
-
}
|
|
66
|
-
export interface HonoContextLike {
|
|
67
|
-
req: {
|
|
68
|
-
header(name: string): string | undefined;
|
|
69
|
-
json(): Promise<unknown>;
|
|
70
|
-
url: string;
|
|
71
|
-
method: string;
|
|
72
|
-
};
|
|
73
|
-
json(body: unknown, status?: number): Response;
|
|
74
|
-
header(name: string, value: string): void;
|
|
75
|
-
set(key: string, value: unknown): void;
|
|
76
|
-
get(key: string): unknown;
|
|
77
|
-
}
|
|
78
61
|
export interface PaywallOptions {
|
|
79
62
|
/**
|
|
80
63
|
* Amount in cents.
|
|
@@ -87,9 +70,25 @@ export interface PaywallOptions {
|
|
|
87
70
|
description?: string;
|
|
88
71
|
metadata?: (body: unknown) => Record<string, unknown>;
|
|
89
72
|
}
|
|
90
|
-
export
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
73
|
+
export interface PaywallRequest {
|
|
74
|
+
url: string;
|
|
75
|
+
method: string;
|
|
76
|
+
headers: Record<string, string | undefined>;
|
|
77
|
+
body: unknown;
|
|
94
78
|
}
|
|
95
|
-
export
|
|
79
|
+
export type PaywallResult = {
|
|
80
|
+
outcome: "charged";
|
|
81
|
+
paymentContext: PaywallPaymentContext;
|
|
82
|
+
} | {
|
|
83
|
+
outcome: "crypto_paid";
|
|
84
|
+
paymentContext: PaywallPaymentContext;
|
|
85
|
+
} | {
|
|
86
|
+
outcome: "payment_required";
|
|
87
|
+
statusCode: 402;
|
|
88
|
+
body: Record<string, unknown>;
|
|
89
|
+
headers: Record<string, string>;
|
|
90
|
+
} | {
|
|
91
|
+
outcome: "error";
|
|
92
|
+
statusCode: number;
|
|
93
|
+
body: Record<string, unknown>;
|
|
94
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AgentSpendOptions, ChargeOptions, ChargeResponse, PaywallOptions, PaywallPaymentContext } from "../core/types.js";
|
|
2
|
+
export * from "../core/index.js";
|
|
3
|
+
interface ExpressRequest {
|
|
4
|
+
body: unknown;
|
|
5
|
+
url: string;
|
|
6
|
+
method: string;
|
|
7
|
+
get(name: string): string | undefined;
|
|
8
|
+
header(name: string): string | undefined;
|
|
9
|
+
paymentContext?: PaywallPaymentContext;
|
|
10
|
+
}
|
|
11
|
+
interface ExpressResponse {
|
|
12
|
+
status(code: number): ExpressResponse;
|
|
13
|
+
set(name: string, value: string): ExpressResponse;
|
|
14
|
+
json(body: unknown): void;
|
|
15
|
+
}
|
|
16
|
+
type NextFunction = (err?: unknown) => void;
|
|
17
|
+
export declare function getPaymentContext(req: ExpressRequest): PaywallPaymentContext | null;
|
|
18
|
+
export interface AgentSpend {
|
|
19
|
+
charge(cardId: string, opts: ChargeOptions): Promise<ChargeResponse>;
|
|
20
|
+
paywall(opts: PaywallOptions): (req: ExpressRequest, res: ExpressResponse, next: NextFunction) => void;
|
|
21
|
+
}
|
|
22
|
+
export declare function createAgentSpend(options: AgentSpendOptions): AgentSpend;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createAgentSpendClient } from "../core/client.js";
|
|
2
|
+
export * from "../core/index.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Payment context helper
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
export function getPaymentContext(req) {
|
|
7
|
+
return req.paymentContext ?? null;
|
|
8
|
+
}
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Factory
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export function createAgentSpend(options) {
|
|
13
|
+
const client = createAgentSpendClient(options);
|
|
14
|
+
function paywall(opts) {
|
|
15
|
+
return function paywallMiddleware(req, res, next) {
|
|
16
|
+
const headerGet = (name) => req.get?.(name) ?? req.header?.(name);
|
|
17
|
+
const request = {
|
|
18
|
+
url: req.url,
|
|
19
|
+
method: req.method,
|
|
20
|
+
headers: {
|
|
21
|
+
"x-card-id": headerGet("x-card-id"),
|
|
22
|
+
"payment-signature": headerGet("payment-signature"),
|
|
23
|
+
"x-payment": headerGet("x-payment"),
|
|
24
|
+
"x-request-id": headerGet("x-request-id"),
|
|
25
|
+
"idempotency-key": headerGet("idempotency-key"),
|
|
26
|
+
},
|
|
27
|
+
body: req.body
|
|
28
|
+
};
|
|
29
|
+
client.processPaywall(opts, request).then((result) => {
|
|
30
|
+
switch (result.outcome) {
|
|
31
|
+
case "charged":
|
|
32
|
+
case "crypto_paid":
|
|
33
|
+
req.paymentContext = result.paymentContext;
|
|
34
|
+
next();
|
|
35
|
+
return;
|
|
36
|
+
case "payment_required":
|
|
37
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
38
|
+
res.set(key, value);
|
|
39
|
+
}
|
|
40
|
+
res.status(result.statusCode).json(result.body);
|
|
41
|
+
return;
|
|
42
|
+
case "error":
|
|
43
|
+
res.status(result.statusCode).json(result.body);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}).catch((err) => next(err));
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return { charge: client.charge, paywall };
|
|
50
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AgentSpendOptions, ChargeOptions, ChargeResponse, PaywallOptions, PaywallPaymentContext } from "../core/types.js";
|
|
2
|
+
export * from "../core/index.js";
|
|
3
|
+
interface FastifyRequest {
|
|
4
|
+
body: unknown;
|
|
5
|
+
url: string;
|
|
6
|
+
method: string;
|
|
7
|
+
headers: Record<string, string | string[] | undefined>;
|
|
8
|
+
paymentContext?: PaywallPaymentContext;
|
|
9
|
+
}
|
|
10
|
+
interface FastifyReply {
|
|
11
|
+
code(statusCode: number): FastifyReply;
|
|
12
|
+
header(name: string, value: string): FastifyReply;
|
|
13
|
+
send(body: unknown): FastifyReply;
|
|
14
|
+
}
|
|
15
|
+
interface FastifyInstance {
|
|
16
|
+
decorate(name: string, value: unknown): void;
|
|
17
|
+
decorateRequest(name: string, value: unknown): void;
|
|
18
|
+
addHook(name: string, handler: (req: FastifyRequest, reply: FastifyReply) => Promise<void>): void;
|
|
19
|
+
}
|
|
20
|
+
type DoneCallback = (err?: Error) => void;
|
|
21
|
+
export declare function getPaymentContext(req: FastifyRequest): PaywallPaymentContext | null;
|
|
22
|
+
export interface AgentSpend {
|
|
23
|
+
charge(cardId: string, opts: ChargeOptions): Promise<ChargeResponse>;
|
|
24
|
+
paywall(opts: PaywallOptions): (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
25
|
+
plugin(opts: PaywallOptions): (fastify: FastifyInstance, _options: unknown, done: DoneCallback) => void;
|
|
26
|
+
}
|
|
27
|
+
export declare function createAgentSpend(options: AgentSpendOptions): AgentSpend;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createAgentSpendClient } from "../core/client.js";
|
|
2
|
+
export * from "../core/index.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Payment context helper
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
export function getPaymentContext(req) {
|
|
7
|
+
return req.paymentContext ?? null;
|
|
8
|
+
}
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Factory
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export function createAgentSpend(options) {
|
|
13
|
+
const client = createAgentSpendClient(options);
|
|
14
|
+
function paywall(opts) {
|
|
15
|
+
return async function preHandler(req, reply) {
|
|
16
|
+
const headerGet = (name) => {
|
|
17
|
+
const val = req.headers[name];
|
|
18
|
+
return Array.isArray(val) ? val[0] : val;
|
|
19
|
+
};
|
|
20
|
+
const request = {
|
|
21
|
+
url: req.url,
|
|
22
|
+
method: req.method,
|
|
23
|
+
headers: {
|
|
24
|
+
"x-card-id": headerGet("x-card-id"),
|
|
25
|
+
"payment-signature": headerGet("payment-signature"),
|
|
26
|
+
"x-payment": headerGet("x-payment"),
|
|
27
|
+
"x-request-id": headerGet("x-request-id"),
|
|
28
|
+
"idempotency-key": headerGet("idempotency-key"),
|
|
29
|
+
},
|
|
30
|
+
body: req.body
|
|
31
|
+
};
|
|
32
|
+
const result = await client.processPaywall(opts, request);
|
|
33
|
+
switch (result.outcome) {
|
|
34
|
+
case "charged":
|
|
35
|
+
case "crypto_paid":
|
|
36
|
+
req.paymentContext = result.paymentContext;
|
|
37
|
+
return;
|
|
38
|
+
case "payment_required":
|
|
39
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
40
|
+
reply.header(key, value);
|
|
41
|
+
}
|
|
42
|
+
reply.code(result.statusCode).send(result.body);
|
|
43
|
+
return;
|
|
44
|
+
case "error":
|
|
45
|
+
reply.code(result.statusCode).send(result.body);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function plugin(opts) {
|
|
51
|
+
return function agentSpendPlugin(fastify, _options, done) {
|
|
52
|
+
fastify.decorateRequest("paymentContext", null);
|
|
53
|
+
fastify.addHook("preHandler", paywall(opts));
|
|
54
|
+
done();
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { charge: client.charge, paywall, plugin };
|
|
58
|
+
}
|