@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/index.js DELETED
@@ -1,420 +0,0 @@
1
- "use strict";
2
- // ---------------------------------------------------------------------------
3
- // Types (inlined from @agentspend/types to avoid cross-repo publish)
4
- // ---------------------------------------------------------------------------
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.AgentSpendChargeError = void 0;
7
- exports.getPaymentContext = getPaymentContext;
8
- exports.createAgentSpend = createAgentSpend;
9
- // ---------------------------------------------------------------------------
10
- // x402 imports – server-side only (HTTP calls to facilitator, no crypto deps)
11
- // ---------------------------------------------------------------------------
12
- const server_1 = require("@x402/core/server");
13
- const server_2 = require("@x402/evm/exact/server");
14
- class AgentSpendChargeError extends Error {
15
- statusCode;
16
- details;
17
- constructor(message, statusCode, details) {
18
- super(message);
19
- this.statusCode = statusCode;
20
- this.details = details;
21
- }
22
- }
23
- exports.AgentSpendChargeError = AgentSpendChargeError;
24
- // ---------------------------------------------------------------------------
25
- // Payment context helper
26
- // ---------------------------------------------------------------------------
27
- const PAYMENT_CONTEXT_KEY = "payment";
28
- function getPaymentContext(c) {
29
- const ctx = c.get(PAYMENT_CONTEXT_KEY);
30
- return ctx ?? null;
31
- }
32
- // ---------------------------------------------------------------------------
33
- // Factory
34
- // ---------------------------------------------------------------------------
35
- function createAgentSpend(options) {
36
- // Validate: at least one of serviceApiKey or crypto must be provided
37
- if (!options.serviceApiKey && !options.crypto) {
38
- throw new AgentSpendChargeError("At least one of serviceApiKey or crypto config must be provided", 500);
39
- }
40
- const fetchImpl = options.fetchImpl ?? globalThis.fetch;
41
- if (!fetchImpl) {
42
- throw new AgentSpendChargeError("No fetch implementation available", 500);
43
- }
44
- const platformApiBaseUrl = resolvePlatformApiBaseUrl(options.platformApiBaseUrl);
45
- // -------------------------------------------------------------------
46
- // Lazy service_id fetch + cache
47
- // -------------------------------------------------------------------
48
- let cachedServiceId = null;
49
- async function getServiceId() {
50
- if (cachedServiceId)
51
- return cachedServiceId;
52
- if (!options.serviceApiKey)
53
- return null;
54
- try {
55
- const res = await fetchImpl(joinUrl(platformApiBaseUrl, "/v1/service/me"), {
56
- headers: { authorization: `Bearer ${options.serviceApiKey}` }
57
- });
58
- if (res.ok) {
59
- const data = (await res.json());
60
- cachedServiceId = data.id ?? null;
61
- }
62
- }
63
- catch { /* graceful fallback */ }
64
- return cachedServiceId;
65
- }
66
- // -------------------------------------------------------------------
67
- // x402 singleton setup (Decision 9)
68
- // Server-side: facilitator handles verify + settle over HTTP.
69
- // No client-side EVM scheme needed — we delegate to the facilitator.
70
- // -------------------------------------------------------------------
71
- let facilitator = null;
72
- let resourceServer = null;
73
- const cryptoNetwork = (options.crypto?.network ?? "eip155:8453");
74
- if (options.crypto || options.serviceApiKey) {
75
- const facilitatorUrl = options.crypto?.facilitatorUrl ?? "https://x402.org/facilitator";
76
- facilitator = new server_1.HTTPFacilitatorClient({ url: facilitatorUrl });
77
- resourceServer = new server_1.x402ResourceServer(facilitator);
78
- (0, server_2.registerExactEvmScheme)(resourceServer);
79
- }
80
- // -------------------------------------------------------------------
81
- // charge() — card-only, unchanged
82
- // -------------------------------------------------------------------
83
- async function charge(cardIdInput, opts) {
84
- if (!options.serviceApiKey) {
85
- throw new AgentSpendChargeError("charge() requires serviceApiKey", 500);
86
- }
87
- const cardId = toCardId(cardIdInput);
88
- if (!cardId) {
89
- throw new AgentSpendChargeError("card_id must start with card_", 400);
90
- }
91
- if (!Number.isInteger(opts.amount_cents) || opts.amount_cents <= 0) {
92
- throw new AgentSpendChargeError("amount_cents must be a positive integer", 400);
93
- }
94
- const payload = {
95
- card_id: cardId,
96
- amount_cents: opts.amount_cents,
97
- currency: opts.currency ?? "usd",
98
- ...(opts.description ? { description: opts.description } : {}),
99
- ...(opts.metadata ? { metadata: opts.metadata } : {}),
100
- idempotency_key: opts.idempotency_key ?? bestEffortIdempotencyKey()
101
- };
102
- const response = await fetchImpl(joinUrl(platformApiBaseUrl, "/v1/charge"), {
103
- method: "POST",
104
- headers: {
105
- authorization: `Bearer ${options.serviceApiKey}`,
106
- "content-type": "application/json"
107
- },
108
- body: JSON.stringify(payload)
109
- });
110
- const responseBody = (await response.json().catch(() => ({})));
111
- if (!response.ok) {
112
- throw new AgentSpendChargeError(typeof responseBody.error === "string" ? responseBody.error : "AgentSpend charge failed", response.status, responseBody);
113
- }
114
- return responseBody;
115
- }
116
- // -------------------------------------------------------------------
117
- // paywall() — unified card + crypto middleware
118
- // -------------------------------------------------------------------
119
- function paywall(opts) {
120
- const { amount } = opts;
121
- // Validate fixed-price amount at creation time
122
- if (typeof amount === "number") {
123
- if (!Number.isInteger(amount) || amount <= 0) {
124
- throw new AgentSpendChargeError("amount must be a positive integer", 500);
125
- }
126
- }
127
- return async function paywallMiddleware(c, next) {
128
- // Step 1: Parse body once (Decision 11)
129
- const body = await c.req.json().catch(() => ({}));
130
- // Step 2: Determine effective amount
131
- let effectiveAmount;
132
- if (typeof amount === "number") {
133
- effectiveAmount = amount;
134
- }
135
- else if (typeof amount === "string") {
136
- const raw = body?.[amount];
137
- effectiveAmount = typeof raw === "number" ? raw : 0;
138
- }
139
- else {
140
- effectiveAmount = amount(body);
141
- }
142
- if (!Number.isInteger(effectiveAmount) || effectiveAmount <= 0) {
143
- return c.json({ error: "Could not determine payment amount from request" }, 400);
144
- }
145
- const currency = opts.currency ?? "usd";
146
- // Step 3: Check for payment header → crypto payment
147
- // x402 v2 uses "Payment-Signature", v1 uses "X-Payment"
148
- const paymentHeader = c.req.header("payment-signature") ?? c.req.header("x-payment");
149
- if (paymentHeader) {
150
- return handleCryptoPayment(c, next, paymentHeader, effectiveAmount, currency, body, opts);
151
- }
152
- // Step 4: Check for x-card-id header or body.card_id → card payment
153
- const cardIdFromHeader = c.req.header("x-card-id");
154
- let cardId = cardIdFromHeader ? toCardId(cardIdFromHeader) : null;
155
- if (!cardId) {
156
- const bodyCardId = typeof body?.card_id === "string"
157
- ? body.card_id
158
- : null;
159
- cardId = toCardId(bodyCardId);
160
- }
161
- if (cardId) {
162
- return handleCardPayment(c, next, cardId, effectiveAmount, currency, body, opts);
163
- }
164
- // Step 5: Neither → return 402 with Payment-Required header (Decision 8)
165
- return return402Response(c, effectiveAmount, currency);
166
- };
167
- }
168
- // -------------------------------------------------------------------
169
- // handleCardPayment — existing charge() flow
170
- // -------------------------------------------------------------------
171
- async function handleCardPayment(c, next, cardId, amountCents, currency, body, opts) {
172
- if (!options.serviceApiKey) {
173
- return c.json({ error: "Card payments require serviceApiKey" }, 500);
174
- }
175
- try {
176
- const chargeResult = await charge(cardId, {
177
- amount_cents: amountCents,
178
- currency,
179
- description: opts.description,
180
- metadata: opts.metadata ? toStringMetadata(opts.metadata(body)) : undefined,
181
- idempotency_key: c.req.header("x-request-id") ?? c.req.header("idempotency-key") ?? undefined
182
- });
183
- const paymentContext = {
184
- method: "card",
185
- amount_cents: amountCents,
186
- currency,
187
- card_id: cardId,
188
- remaining_limit_cents: chargeResult.remaining_limit_cents
189
- };
190
- c.set(PAYMENT_CONTEXT_KEY, paymentContext);
191
- }
192
- catch (error) {
193
- if (error instanceof AgentSpendChargeError) {
194
- if (error.statusCode === 403) {
195
- // No binding — return 402 so agent can discover service_id and bind
196
- return return402Response(c, amountCents, currency);
197
- }
198
- if (error.statusCode === 402) {
199
- return c.json({ error: "Payment required", details: error.details }, 402);
200
- }
201
- return c.json({ error: error.message, details: error.details }, error.statusCode);
202
- }
203
- return c.json({ error: "Unexpected paywall failure" }, 500);
204
- }
205
- await next();
206
- }
207
- // -------------------------------------------------------------------
208
- // handleCryptoPayment — x402 verify + settle via facilitator
209
- // -------------------------------------------------------------------
210
- async function handleCryptoPayment(c, next, paymentHeader, amountCents, currency, _body, _opts) {
211
- if (!resourceServer) {
212
- return c.json({ error: "Crypto payments not configured" }, 500);
213
- }
214
- try {
215
- // Decode the x-payment header (base64 JSON payment payload)
216
- let paymentPayload;
217
- try {
218
- paymentPayload = JSON.parse(Buffer.from(paymentHeader, "base64").toString("utf-8"));
219
- }
220
- catch {
221
- return c.json({ error: "Invalid payment payload encoding" }, 400);
222
- }
223
- // Extract payTo from the payment payload's accepted requirements
224
- // rather than generating a new deposit address.
225
- // resolvePayToAddress() creates a fresh Stripe PaymentIntent each
226
- // time, which would return a *different* address than the one in the
227
- // 402 response the client signed against → facilitator verification
228
- // would fail.
229
- const acceptedPayTo = paymentPayload
230
- .accepted?.payTo;
231
- const payTo = acceptedPayTo ?? await resolvePayToAddress();
232
- // Build the payment requirements that the payment should satisfy
233
- const paymentRequirements = {
234
- scheme: "exact",
235
- network: cryptoNetwork,
236
- amount: String(amountCents),
237
- asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
238
- payTo,
239
- maxTimeoutSeconds: 300,
240
- extra: { name: "USD Coin", version: "2" }
241
- };
242
- // Verify payment via resource server (delegates to facilitator)
243
- const verifyResult = await resourceServer.verifyPayment(paymentPayload, paymentRequirements);
244
- if (!verifyResult.isValid) {
245
- return c.json({ error: "Payment verification failed", details: verifyResult.invalidReason }, 402);
246
- }
247
- // Settle payment via resource server (delegates to facilitator)
248
- const settleResult = await resourceServer.settlePayment(paymentPayload, paymentRequirements);
249
- if (!settleResult.success) {
250
- return c.json({ error: "Payment settlement failed", details: settleResult.errorReason }, 402);
251
- }
252
- const paymentContext = {
253
- method: "crypto",
254
- amount_cents: amountCents,
255
- currency,
256
- transaction_hash: settleResult.transaction,
257
- payer_address: verifyResult.payer ?? undefined,
258
- network: cryptoNetwork
259
- };
260
- c.set(PAYMENT_CONTEXT_KEY, paymentContext);
261
- await next();
262
- }
263
- catch (error) {
264
- if (error instanceof AgentSpendChargeError) {
265
- return c.json({ error: error.message, details: error.details }, error.statusCode);
266
- }
267
- return c.json({ error: "Crypto payment processing failed", details: error.message }, 500);
268
- }
269
- }
270
- // -------------------------------------------------------------------
271
- // return402Response — x402 Payment-Required format (Decision 8)
272
- // -------------------------------------------------------------------
273
- async function return402Response(c, amountCents, currency) {
274
- const serviceId = await getServiceId();
275
- try {
276
- const payTo = await resolvePayToAddress();
277
- const paymentRequirements = {
278
- scheme: "exact",
279
- network: cryptoNetwork,
280
- amount: String(amountCents),
281
- asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
282
- payTo,
283
- maxTimeoutSeconds: 300,
284
- extra: { name: "USD Coin", version: "2" }
285
- };
286
- // Build x402 v2 PaymentRequired response
287
- const paymentRequired = {
288
- x402Version: 2,
289
- error: "Payment required",
290
- resource: {
291
- url: c.req.url,
292
- description: `Payment of ${amountCents} cents`,
293
- mimeType: "application/json"
294
- },
295
- accepts: [paymentRequirements]
296
- };
297
- // Set Payment-Required header (base64 encoded)
298
- const headerValue = Buffer.from(JSON.stringify(paymentRequired)).toString("base64");
299
- c.header("Payment-Required", headerValue);
300
- return c.json({
301
- error: "Payment required",
302
- amount_cents: amountCents,
303
- currency,
304
- ...(serviceId ? {
305
- agentspend: {
306
- service_id: serviceId,
307
- amount_cents: amountCents,
308
- }
309
- } : {})
310
- }, 402);
311
- }
312
- catch (error) {
313
- // Log clearly so service developers can diagnose why crypto is unavailable
314
- console.error("[agentspend] Failed to resolve crypto payTo address — returning card-only 402:", error instanceof Error ? error.message : error);
315
- // Return card-only 402 (no Payment-Required header → crypto clients
316
- // will see "Invalid payment required response")
317
- return c.json({
318
- error: "Payment required",
319
- amount_cents: amountCents,
320
- currency,
321
- ...(serviceId ? {
322
- agentspend: {
323
- service_id: serviceId,
324
- amount_cents: amountCents,
325
- }
326
- } : {})
327
- }, 402);
328
- }
329
- }
330
- // -------------------------------------------------------------------
331
- // resolvePayToAddress — static address or Stripe Machine Payments
332
- // -------------------------------------------------------------------
333
- async function resolvePayToAddress() {
334
- // Static address for crypto-only services
335
- if (options.crypto?.receiverAddress) {
336
- return options.crypto.receiverAddress;
337
- }
338
- // Stripe Connect service → get deposit address from platform
339
- if (options.serviceApiKey) {
340
- const response = await fetchImpl(joinUrl(platformApiBaseUrl, "/v1/crypto/deposit-address"), {
341
- method: "POST",
342
- headers: {
343
- authorization: `Bearer ${options.serviceApiKey}`,
344
- "content-type": "application/json"
345
- },
346
- body: JSON.stringify({ amount_cents: 0, currency: "usd" })
347
- });
348
- if (!response.ok) {
349
- throw new AgentSpendChargeError("Failed to resolve crypto deposit address", 502);
350
- }
351
- const data = (await response.json());
352
- if (!data.deposit_address) {
353
- throw new AgentSpendChargeError("No deposit address returned", 502);
354
- }
355
- return data.deposit_address;
356
- }
357
- throw new AgentSpendChargeError("No crypto payTo address available", 500);
358
- }
359
- // -------------------------------------------------------------------
360
- // Return public interface
361
- // -------------------------------------------------------------------
362
- return {
363
- charge,
364
- paywall
365
- };
366
- }
367
- // ---------------------------------------------------------------------------
368
- // Helpers (unchanged from original)
369
- // ---------------------------------------------------------------------------
370
- function toCardId(input) {
371
- if (typeof input !== "string") {
372
- return null;
373
- }
374
- const trimmed = input.trim();
375
- if (!trimmed.startsWith("card_")) {
376
- return null;
377
- }
378
- return trimmed;
379
- }
380
- function joinUrl(base, path) {
381
- const normalizedBase = base.endsWith("/") ? base.slice(0, -1) : base;
382
- const normalizedPath = path.startsWith("/") ? path : `/${path}`;
383
- return `${normalizedBase}${normalizedPath}`;
384
- }
385
- function bestEffortIdempotencyKey() {
386
- const uuid = globalThis.crypto?.randomUUID?.();
387
- if (uuid) {
388
- return uuid;
389
- }
390
- return `auto_${Date.now()}_${Math.random().toString(16).slice(2)}`;
391
- }
392
- function toStringMetadata(input) {
393
- if (!input || typeof input !== "object" || Array.isArray(input)) {
394
- return {};
395
- }
396
- const result = {};
397
- for (const [key, value] of Object.entries(input)) {
398
- if (typeof value === "string") {
399
- result[key] = value;
400
- }
401
- else if (typeof value === "number" && Number.isFinite(value)) {
402
- result[key] = String(value);
403
- }
404
- else if (typeof value === "boolean") {
405
- result[key] = value ? "true" : "false";
406
- }
407
- }
408
- return result;
409
- }
410
- const DEFAULT_PLATFORM_API_BASE_URL = "https://api.agentspend.co";
411
- function resolvePlatformApiBaseUrl(explicitBaseUrl) {
412
- if (explicitBaseUrl && explicitBaseUrl.trim().length > 0) {
413
- return explicitBaseUrl.trim();
414
- }
415
- const envValue = typeof process !== "undefined" && process.env ? process.env.AGENTSPEND_API_URL : undefined;
416
- if (typeof envValue === "string" && envValue.trim().length > 0) {
417
- return envValue.trim();
418
- }
419
- return DEFAULT_PLATFORM_API_BASE_URL;
420
- }