@fragmentpay/server 0.3.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.
package/dist/index.js ADDED
@@ -0,0 +1,476 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ Frag: () => Frag,
24
+ FragError: () => FragError,
25
+ FragValidationError: () => FragValidationError,
26
+ FragmentPay: () => FragmentPay,
27
+ verifyWebhook: () => verifyWebhook
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/validate.ts
32
+ var FragValidationError = class extends Error {
33
+ path;
34
+ constructor(path, message) {
35
+ super(`[frag] ${path}: ${message}`);
36
+ this.name = "FragValidationError";
37
+ this.path = path;
38
+ }
39
+ };
40
+ var fail = (path, msg) => {
41
+ throw new FragValidationError(path, msg);
42
+ };
43
+ var v = {
44
+ str(path, val, opts = {}) {
45
+ if (typeof val !== "string") return fail(path, `expected string, got ${typeof val}`);
46
+ if (opts.min != null && val.length < opts.min) fail(path, `min length ${opts.min}`);
47
+ if (opts.max != null && val.length > opts.max) fail(path, `max length ${opts.max}`);
48
+ if (opts.pattern && !opts.pattern.test(val)) fail(path, `does not match ${opts.pattern}`);
49
+ return val;
50
+ },
51
+ optStr(path, val, opts) {
52
+ if (val === void 0 || val === null) return void 0;
53
+ return v.str(path, val, opts);
54
+ },
55
+ num(path, val, opts = {}) {
56
+ const n = typeof val === "string" ? Number(val) : val;
57
+ if (typeof n !== "number" || !Number.isFinite(n)) return fail(path, `expected finite number, got ${JSON.stringify(val)}`);
58
+ if (opts.int && !Number.isInteger(n)) fail(path, "expected integer");
59
+ if (opts.min != null && n < opts.min) fail(path, `min ${opts.min}`);
60
+ if (opts.max != null && n > opts.max) fail(path, `max ${opts.max}`);
61
+ return n;
62
+ },
63
+ optNum(path, val, opts) {
64
+ if (val === void 0 || val === null) return void 0;
65
+ return v.num(path, val, opts);
66
+ },
67
+ enum(path, val, allowed) {
68
+ if (typeof val !== "string" || !allowed.includes(val)) {
69
+ return fail(path, `expected one of ${allowed.join("|")}`);
70
+ }
71
+ return val;
72
+ },
73
+ optEnum(path, val, allowed) {
74
+ if (val === void 0 || val === null) return void 0;
75
+ return v.enum(path, val, allowed);
76
+ },
77
+ obj(path, val) {
78
+ if (!val || typeof val !== "object" || Array.isArray(val)) return fail(path, "expected object");
79
+ return val;
80
+ },
81
+ id(path, val) {
82
+ return v.str(path, val, { min: 3, max: 128, pattern: /^[A-Za-z0-9_-]+$/ });
83
+ },
84
+ url(path, val) {
85
+ const s = v.str(path, val, { min: 1, max: 2048 });
86
+ try {
87
+ new URL(s);
88
+ } catch {
89
+ fail(path, "must be an absolute URL");
90
+ }
91
+ return s;
92
+ },
93
+ email(path, val) {
94
+ return v.str(path, val, { max: 254, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ });
95
+ }
96
+ };
97
+ var CHAINS = ["solana", "ethereum", "base", "arbitrum", "optimism", "polygon", "bnb"];
98
+ var INTENT_STATUSES = [
99
+ "created",
100
+ "requires_payment",
101
+ "quoted",
102
+ "awaiting_signature",
103
+ "executing",
104
+ "partially_filled",
105
+ "settled",
106
+ "failed",
107
+ "expired",
108
+ "cancelled",
109
+ "refunded"
110
+ ];
111
+ var PAYOUT_STATUSES = ["queued", "pending", "paid", "failed", "paused"];
112
+ var WEBHOOK_EVENTS = [
113
+ "payment_intent.created",
114
+ "payment_intent.quoted",
115
+ "payment_intent.executing",
116
+ "payment_intent.executed",
117
+ "payment_intent.settled",
118
+ "payment_intent.failed",
119
+ "payment_intent.expired",
120
+ "payment_intent.cancelled",
121
+ "payment_intent.refunded",
122
+ "quote.created",
123
+ "settlement.confirmed",
124
+ "payout.pending",
125
+ "payout.paid",
126
+ "payout.failed",
127
+ "refund.pending",
128
+ "refund.paid",
129
+ "refund.failed"
130
+ ];
131
+
132
+ // src/index.ts
133
+ var FragError = class extends Error {
134
+ status;
135
+ code;
136
+ requestId;
137
+ raw;
138
+ constructor(status, code, message, raw, requestId) {
139
+ super(message);
140
+ this.name = "FragError";
141
+ this.status = status;
142
+ this.code = code;
143
+ this.raw = raw;
144
+ this.requestId = requestId;
145
+ }
146
+ };
147
+ var FragmentPay = class {
148
+ paymentIntents;
149
+ refunds;
150
+ payouts;
151
+ webhookEndpoints;
152
+ webhooks;
153
+ tokens;
154
+ #opts;
155
+ constructor(opts) {
156
+ if (!opts || typeof opts !== "object") {
157
+ throw new FragValidationError("options", "expected FragOptions object");
158
+ }
159
+ if (typeof opts.apiKey !== "string" || !/^sk_(test|live)_[A-Za-z0-9_-]{8,}$/.test(opts.apiKey)) {
160
+ throw new FragValidationError("options.apiKey", "must match sk_test_\u2026 or sk_live_\u2026 (min 8 chars after prefix)");
161
+ }
162
+ if (opts.baseUrl !== void 0) v.url("options.baseUrl", opts.baseUrl);
163
+ if (opts.timeout !== void 0) v.num("options.timeout", opts.timeout, { min: 0, max: 6e5, int: true });
164
+ if (opts.retries !== void 0) v.num("options.retries", opts.retries, { min: 0, max: 10, int: true });
165
+ this.#opts = {
166
+ apiKey: opts.apiKey,
167
+ baseUrl: (opts.baseUrl ?? "https://api.frag.dev").replace(/\/$/, ""),
168
+ fetch: opts.fetch ?? globalThis.fetch.bind(globalThis),
169
+ timeout: opts.timeout ?? 3e4,
170
+ retries: opts.retries ?? 2,
171
+ debug: opts.debug ?? false
172
+ };
173
+ const req = ((path, init) => this.#req(path, init));
174
+ this.paymentIntents = new PaymentIntentResource(req);
175
+ this.refunds = new RefundResource(req);
176
+ this.payouts = new PayoutResource(req);
177
+ this.webhookEndpoints = new WebhookEndpointResource(req);
178
+ this.tokens = new TokenResource(req);
179
+ this.webhooks = new WebhookHelpers();
180
+ }
181
+ setDebug(on) {
182
+ this.#opts.debug = on;
183
+ }
184
+ async #req(path, init = {}) {
185
+ const query = init.query ? "?" + new URLSearchParams(
186
+ Object.entries(init.query).filter(([, v2]) => v2 !== void 0 && v2 !== null && v2 !== "").map(([k, v2]) => [k, String(v2)])
187
+ ).toString() : "";
188
+ const url = `${this.#opts.baseUrl}${path}${query}`;
189
+ const headers = new Headers(init.headers);
190
+ headers.set("authorization", `Bearer ${this.#opts.apiKey}`);
191
+ if (init.body && !headers.has("content-type")) headers.set("content-type", "application/json");
192
+ if (init.idempotencyKey) headers.set("idempotency-key", init.idempotencyKey);
193
+ const maxAttempts = Math.max(1, this.#opts.retries + 1);
194
+ let lastErr;
195
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
196
+ const ctrl = this.#opts.timeout ? new AbortController() : null;
197
+ const to = ctrl ? setTimeout(() => ctrl.abort(), this.#opts.timeout) : null;
198
+ try {
199
+ if (this.#opts.debug) {
200
+ console.debug(`[frag] ${init.method ?? "GET"} ${url} attempt=${attempt}`);
201
+ }
202
+ const res = await this.#opts.fetch(url, {
203
+ ...init,
204
+ headers,
205
+ signal: ctrl?.signal ?? init.signal
206
+ });
207
+ const text = await res.text();
208
+ let body = null;
209
+ try {
210
+ body = text ? JSON.parse(text) : null;
211
+ } catch {
212
+ body = text;
213
+ }
214
+ if (res.ok) return body;
215
+ const retryable = res.status === 429 || res.status >= 500;
216
+ if (retryable && attempt < maxAttempts) {
217
+ const retryAfter = Number(res.headers.get("retry-after"));
218
+ const backoff = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1e3 : Math.min(2e3 * 2 ** (attempt - 1), 1e4) + Math.random() * 250;
219
+ await sleep(backoff);
220
+ continue;
221
+ }
222
+ const b = body ?? {};
223
+ throw new FragError(
224
+ res.status,
225
+ b.code ?? `http_${res.status}`,
226
+ b.error ?? res.statusText,
227
+ body,
228
+ res.headers.get("x-request-id") ?? void 0
229
+ );
230
+ } catch (e) {
231
+ lastErr = e;
232
+ if (e instanceof FragError) throw e;
233
+ if (attempt < maxAttempts) {
234
+ await sleep(500 * attempt + Math.random() * 200);
235
+ continue;
236
+ }
237
+ throw e;
238
+ } finally {
239
+ if (to) clearTimeout(to);
240
+ }
241
+ }
242
+ throw lastErr instanceof Error ? lastErr : new Error("frag request failed");
243
+ }
244
+ };
245
+ function sleep(ms) {
246
+ return new Promise((r) => setTimeout(r, ms));
247
+ }
248
+ var PaymentIntentResource = class {
249
+ constructor(req) {
250
+ this.req = req;
251
+ }
252
+ req;
253
+ create(input) {
254
+ if (!input || typeof input !== "object") throw new FragValidationError("createIntent", "expected object");
255
+ const amount = v.num("createIntent.amount", input.amount, { min: 0.01, max: 1e7 });
256
+ const currency = v.optStr("createIntent.currency", input.currency, { min: 3, max: 8, pattern: /^[A-Z]{3,8}$/ }) ?? "USD";
257
+ if (input.settlement !== void 0) v.obj("createIntent.settlement", input.settlement);
258
+ const settlementChain = input.settlement?.chain ? v.enum("createIntent.settlement.chain", input.settlement.chain, CHAINS) : void 0;
259
+ const settlementToken = v.optStr("createIntent.settlement.token", input.settlement?.token, { min: 1, max: 64 });
260
+ const settlementAddress = v.optStr("createIntent.settlement.address", input.settlement?.address, { min: 26, max: 128 });
261
+ const customerEmail = input.customerEmail !== void 0 ? v.email("createIntent.customerEmail", input.customerEmail) : void 0;
262
+ const expiresInSeconds = v.optNum("createIntent.expiresInSeconds", input.expiresInSeconds, { min: 60, max: 86400, int: true });
263
+ const idempotencyKey = v.optStr("createIntent.idempotencyKey", input.idempotencyKey, { min: 8, max: 128 });
264
+ const successUrl = input.successUrl !== void 0 ? v.url("createIntent.successUrl", input.successUrl) : void 0;
265
+ const cancelUrl = input.cancelUrl !== void 0 ? v.url("createIntent.cancelUrl", input.cancelUrl) : void 0;
266
+ if (input.metadata !== void 0) v.obj("createIntent.metadata", input.metadata);
267
+ return this.req("/api/public/v1/payment-intents", {
268
+ method: "POST",
269
+ body: JSON.stringify({
270
+ amount,
271
+ currency,
272
+ settlement_chain: settlementChain,
273
+ settlement_token: settlementToken,
274
+ settlement_address: settlementAddress,
275
+ customer_email: customerEmail,
276
+ expires_in_seconds: expiresInSeconds,
277
+ metadata: {
278
+ ...input.metadata ?? {},
279
+ ...successUrl ? { success_url: successUrl } : {},
280
+ ...cancelUrl ? { cancel_url: cancelUrl } : {}
281
+ }
282
+ }),
283
+ idempotencyKey
284
+ });
285
+ }
286
+ retrieve(id) {
287
+ v.id("paymentIntents.retrieve.id", id);
288
+ return this.req(`/api/public/v1/payment-intents/${encodeURIComponent(id)}`);
289
+ }
290
+ list(params = {}) {
291
+ const limit = v.optNum("list.limit", params.limit, { min: 1, max: 100, int: true });
292
+ const status = v.optEnum("list.status", params.status, INTENT_STATUSES);
293
+ return this.req(`/api/public/v1/payment-intents`, { query: { limit, status } });
294
+ }
295
+ cancel(id) {
296
+ v.id("paymentIntents.cancel.id", id);
297
+ return this.req(`/api/public/v1/payment-intents/${encodeURIComponent(id)}/cancel`, { method: "POST" });
298
+ }
299
+ transactions(id) {
300
+ v.id("paymentIntents.transactions.id", id);
301
+ return this.req(`/api/public/v1/payment-intents/${encodeURIComponent(id)}/transactions`);
302
+ }
303
+ route(id) {
304
+ v.id("paymentIntents.route.id", id);
305
+ return this.req(`/api/public/v1/payment-intents/${encodeURIComponent(id)}/route`);
306
+ }
307
+ };
308
+ var RefundResource = class {
309
+ constructor(req) {
310
+ this.req = req;
311
+ }
312
+ req;
313
+ create(input) {
314
+ if (!input || typeof input !== "object") throw new FragValidationError("refunds.create", "expected object");
315
+ v.id("refunds.create.paymentIntentId", input.paymentIntentId);
316
+ v.num("refunds.create.amountUsd", input.amountUsd, { min: 0.01, max: 1e7 });
317
+ v.str("refunds.create.destination", input.destination, { min: 26, max: 128 });
318
+ v.optStr("refunds.create.reason", input.reason, { max: 500 });
319
+ v.optStr("refunds.create.idempotencyKey", input.idempotencyKey, { min: 8, max: 128 });
320
+ return this.req("/api/public/v1/refunds", {
321
+ method: "POST",
322
+ body: JSON.stringify({
323
+ intent_id: input.paymentIntentId,
324
+ amount_usd: input.amountUsd,
325
+ destination: input.destination,
326
+ reason: input.reason
327
+ }),
328
+ idempotencyKey: input.idempotencyKey
329
+ });
330
+ }
331
+ retrieve(id) {
332
+ v.id("refunds.retrieve.id", id);
333
+ return this.req(`/api/public/v1/refunds/${encodeURIComponent(id)}`);
334
+ }
335
+ list(params = {}) {
336
+ if (params.paymentIntentId !== void 0) v.id("refunds.list.paymentIntentId", params.paymentIntentId);
337
+ v.optNum("refunds.list.limit", params.limit, { min: 1, max: 100, int: true });
338
+ return this.req(`/api/public/v1/refunds`, {
339
+ query: { payment_intent_id: params.paymentIntentId, limit: params.limit }
340
+ });
341
+ }
342
+ };
343
+ var PayoutResource = class {
344
+ constructor(req) {
345
+ this.req = req;
346
+ }
347
+ req;
348
+ retrieve(id) {
349
+ v.id("payouts.retrieve.id", id);
350
+ return this.req(`/api/public/v1/payouts/${encodeURIComponent(id)}`);
351
+ }
352
+ list(params = {}) {
353
+ v.optEnum("payouts.list.status", params.status, PAYOUT_STATUSES);
354
+ if (params.paymentIntentId !== void 0) v.id("payouts.list.paymentIntentId", params.paymentIntentId);
355
+ v.optNum("payouts.list.limit", params.limit, { min: 1, max: 100, int: true });
356
+ return this.req(`/api/public/v1/payouts`, {
357
+ query: { status: params.status, payment_intent_id: params.paymentIntentId, limit: params.limit }
358
+ });
359
+ }
360
+ replay(id) {
361
+ v.id("payouts.replay.id", id);
362
+ return this.req(`/api/public/v1/payouts/${encodeURIComponent(id)}/replay`, { method: "POST" });
363
+ }
364
+ };
365
+ var WebhookEndpointResource = class {
366
+ constructor(req) {
367
+ this.req = req;
368
+ }
369
+ req;
370
+ create(input) {
371
+ if (!input || typeof input !== "object") throw new FragValidationError("webhookEndpoints.create", "expected object");
372
+ v.url("webhookEndpoints.create.url", input.url);
373
+ if (!Array.isArray(input.events) || input.events.length === 0) {
374
+ throw new FragValidationError("webhookEndpoints.create.events", "at least one event required");
375
+ }
376
+ input.events.forEach((e, i) => v.enum(`webhookEndpoints.create.events[${i}]`, e, WEBHOOK_EVENTS));
377
+ return this.req("/api/public/v1/webhook-endpoints", {
378
+ method: "POST",
379
+ body: JSON.stringify(input)
380
+ });
381
+ }
382
+ list() {
383
+ return this.req(`/api/public/v1/webhook-endpoints`);
384
+ }
385
+ retrieve(id) {
386
+ v.id("webhookEndpoints.retrieve.id", id);
387
+ return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}`);
388
+ }
389
+ update(id, patch) {
390
+ v.id("webhookEndpoints.update.id", id);
391
+ if (patch.url !== void 0) v.url("webhookEndpoints.update.url", patch.url);
392
+ if (patch.events !== void 0) {
393
+ if (!Array.isArray(patch.events)) throw new FragValidationError("webhookEndpoints.update.events", "expected array");
394
+ patch.events.forEach((e, i) => v.enum(`webhookEndpoints.update.events[${i}]`, e, WEBHOOK_EVENTS));
395
+ }
396
+ return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}`, {
397
+ method: "PATCH",
398
+ body: JSON.stringify(patch)
399
+ });
400
+ }
401
+ delete(id) {
402
+ v.id("webhookEndpoints.delete.id", id);
403
+ return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}`, { method: "DELETE" });
404
+ }
405
+ rotateSecret(id) {
406
+ v.id("webhookEndpoints.rotateSecret.id", id);
407
+ return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}/rotate-secret`, { method: "POST" });
408
+ }
409
+ };
410
+ var TokenResource = class {
411
+ constructor(req) {
412
+ this.req = req;
413
+ }
414
+ req;
415
+ list(params = {}) {
416
+ if (params.chain !== void 0) v.enum("tokens.list.chain", params.chain, CHAINS);
417
+ return this.req(`/api/public/v1/tokens`, { query: { chain: params.chain } });
418
+ }
419
+ };
420
+ var WebhookHelpers = class {
421
+ async verify(payload, header, secret, tolerance = 300) {
422
+ if (!header) return false;
423
+ const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
424
+ const t = Number(parts.t);
425
+ const sig = parts.v1;
426
+ if (!t || !sig) return false;
427
+ const skew = Math.abs(Math.floor(Date.now() / 1e3) - t);
428
+ if (skew > tolerance) return false;
429
+ const expected = await hmacHex(secret, `${t}.${payload}`);
430
+ return timingSafeEqualHex(expected, sig);
431
+ }
432
+ parse(payload) {
433
+ return JSON.parse(payload);
434
+ }
435
+ };
436
+ async function verifyWebhook(input) {
437
+ if (!input || typeof input !== "object") throw new FragValidationError("verifyWebhook", "expected object");
438
+ v.str("verifyWebhook.payload", input.payload, { min: 1 });
439
+ v.str("verifyWebhook.secret", input.secret, { min: 8 });
440
+ if (input.tolerance !== void 0) v.num("verifyWebhook.tolerance", input.tolerance, { min: 0, max: 3600, int: true });
441
+ const helper = new WebhookHelpers();
442
+ const ok = await helper.verify(input.payload, input.signature ?? null, input.secret, input.tolerance);
443
+ if (!ok) throw new FragError(401, "invalid_signature", "webhook signature verification failed", null);
444
+ return helper.parse(input.payload);
445
+ }
446
+ var Frag = class extends FragmentPay {
447
+ constructor(opts) {
448
+ const apiKey = "secretKey" in opts ? opts.secretKey : opts.apiKey;
449
+ super({ apiKey, baseUrl: opts.baseUrl, fetch: opts.fetch, timeout: opts.timeout, retries: opts.retries, debug: opts.debug });
450
+ }
451
+ };
452
+ async function hmacHex(secret, message) {
453
+ const key = await crypto.subtle.importKey(
454
+ "raw",
455
+ new TextEncoder().encode(secret),
456
+ { name: "HMAC", hash: "SHA-256" },
457
+ false,
458
+ ["sign"]
459
+ );
460
+ const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(message));
461
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
462
+ }
463
+ function timingSafeEqualHex(a, b) {
464
+ if (a.length !== b.length) return false;
465
+ let mismatch = 0;
466
+ for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
467
+ return mismatch === 0;
468
+ }
469
+ // Annotate the CommonJS export names for ESM import in node:
470
+ 0 && (module.exports = {
471
+ Frag,
472
+ FragError,
473
+ FragValidationError,
474
+ FragmentPay,
475
+ verifyWebhook
476
+ });