@fragmentpay/server 0.3.0 → 0.3.4

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/README.md CHANGED
@@ -17,11 +17,11 @@ npm install @fragmentpay/server
17
17
 
18
18
  ## Required environment variables
19
19
 
20
- | Name | Where | Example |
21
- | ------------------------ | -------------- | --------------------------------------- |
22
- | `FRAG_SECRET_KEY` | server only | `sk_live_…` or `sk_test_…` |
23
- | `FRAG_WEBHOOK_SECRET` | server only | value shown when you create the webhook |
24
- | `FRAG_BASE_URL` *(opt.)* | server only | override host, default `https://api.frag.dev` |
20
+ | Name | Where | Example |
21
+ | ------------------------ | ----------- | ------------------------------------------ |
22
+ | `FRAG_SECRET_KEY` | server only | `sk_live_…` or `sk_test_…` |
23
+ | `FRAG_WEBHOOK_SECRET` | server only | value shown when you create the webhook |
24
+ | `FRAG_BASE_URL` _(opt.)_ | server only | override host, default `https://frag.cash` |
25
25
 
26
26
  Never expose `sk_…` in a browser bundle.
27
27
 
@@ -46,7 +46,7 @@ export async function startCheckout(orderId: string, cents: number, email: strin
46
46
  address: process.env.MERCHANT_USDC_ADDRESS!,
47
47
  },
48
48
  successUrl: "https://shop.example.com/thanks",
49
- cancelUrl: "https://shop.example.com/cart",
49
+ cancelUrl: "https://shop.example.com/cart",
50
50
  metadata: { order_id: orderId },
51
51
  idempotencyKey: `order_${orderId}`,
52
52
  });
@@ -64,7 +64,7 @@ export async function POST(req: Request) {
64
64
  const payload = await req.text();
65
65
  const event = await verifyWebhook({
66
66
  payload,
67
- signature: req.headers.get("x-frag-signature"),
67
+ signature: req.headers.get("frag-signature"),
68
68
  secret: process.env.FRAG_WEBHOOK_SECRET!,
69
69
  });
70
70
 
@@ -77,6 +77,22 @@ export async function POST(req: Request) {
77
77
 
78
78
  **3. React drop-in on the frontend** — see `@fragmentpay/react`.
79
79
 
80
+ ## Advanced options
81
+
82
+ ```ts
83
+ const frag = new Frag({
84
+ secretKey: process.env.FRAG_SECRET_KEY!,
85
+ baseUrl: "https://frag.cash", // default
86
+ timeoutMs: 15_000, // per-request timeout
87
+ maxRetries: 3, // retries on 5xx / network errors with Retry-After honored
88
+ fetch: myCustomFetch, // inject a custom fetch (Workers, tracing, etc.)
89
+ });
90
+ ```
91
+
92
+ Every method throws `FragError` on non-2xx responses. It carries `status`,
93
+ `code`, `message`, and `requestId` for observability, plus the raw provider
94
+ body when available.
95
+
80
96
  ## Validation errors
81
97
 
82
98
  Every method validates its arguments and throws a `FragValidationError`
@@ -89,7 +105,8 @@ frag.paymentIntents.create({ amount: -5 });
89
105
 
90
106
  ## Public API surface
91
107
 
92
- - `frag.paymentIntents.{create, retrieve, list, cancel, transactions, route}`
108
+ - `frag.paymentIntents.{create, retrieve, list, cancel, transactions, routing}`
109
+ - `routing(id)` returns router candidates evaluated for the intent (chosen + rejected with reasons). `route(id)` is kept as a deprecated alias.
93
110
  - `frag.refunds.{create, retrieve, list}`
94
111
  - `frag.payouts.{retrieve, list, replay}`
95
112
  - `frag.webhookEndpoints.{create, list, retrieve, update, delete, rotateSecret}`
@@ -97,6 +114,21 @@ frag.paymentIntents.create({ amount: -5 });
97
114
  - `verifyWebhook({ payload, signature, secret })`
98
115
  - `FragError` (HTTP + upstream) and `FragValidationError` (input)
99
116
 
117
+ ## Idempotency
118
+
119
+ Every `POST` accepts an optional `Idempotency-Key` header (SDK auto-sends it
120
+ when you set `idempotencyKey`). Replays within 24h with the **same body**
121
+ return the original response; a **different body** returns HTTP `409` with
122
+ `code: "idempotency_conflict"`.
123
+
124
+ ## Webhook events
125
+
126
+ The SDK's `WebhookEventType` union stays in sync with the API. Notable events:
127
+ `payment_intent.created`, `payment_intent.quoted`, `payment_intent.executing`,
128
+ `payment_intent.settled`, `payment_intent.failed`, `payment_intent.expired`,
129
+ `payment_intent.cancelled`, `payment_intent.refunded`, `payout.created`,
130
+ `payout.paid`, `payout.failed`, `ping`.
131
+
100
132
  ## License
101
133
 
102
134
  MIT
package/dist/index.d.mts CHANGED
@@ -94,7 +94,7 @@ interface Refund {
94
94
  status: RefundStatus;
95
95
  tx_hash: string | null;
96
96
  created_at: string;
97
- paid_at: string | null;
97
+ updated_at: string;
98
98
  failure_reason?: string | null;
99
99
  }
100
100
  interface Payout {
@@ -132,7 +132,7 @@ interface FragToken {
132
132
  verified: boolean;
133
133
  min_liquidity_usd?: number;
134
134
  }
135
- type WebhookEventType = "payment_intent.created" | "payment_intent.quoted" | "payment_intent.executing" | "payment_intent.executed" | "payment_intent.settled" | "payment_intent.failed" | "payment_intent.expired" | "payment_intent.cancelled" | "payment_intent.refunded" | "quote.created" | "settlement.confirmed" | "payout.pending" | "payout.paid" | "payout.failed" | "refund.pending" | "refund.paid" | "refund.failed";
135
+ type WebhookEventType = "payment_intent.created" | "payment_intent.quoted" | "payment_intent.executing" | "payment_intent.executed" | "payment_intent.settled" | "payment_intent.failed" | "payment_intent.expired" | "payment_intent.cancelled" | "payment_intent.refunded" | "quote.created" | "settlement.confirmed" | "payout.created" | "payout.paid" | "payout.failed" | "refund.created" | "refund.processing" | "refund.paid" | "refund.failed";
136
136
  interface WebhookEvent<T = unknown> {
137
137
  id: string;
138
138
  type: WebhookEventType;
@@ -160,6 +160,7 @@ declare class FragError extends Error {
160
160
  type Requester = <T>(path: string, init?: RequestInit & {
161
161
  idempotencyKey?: string;
162
162
  query?: Record<string, string | number | boolean | undefined>;
163
+ skipAuth?: boolean;
163
164
  }) => Promise<T>;
164
165
  declare class FragmentPay {
165
166
  #private;
@@ -187,6 +188,10 @@ declare class PaymentIntentResource {
187
188
  transactions(id: string): Promise<{
188
189
  data: IntentTransaction[];
189
190
  }>;
191
+ routing(id: string): Promise<{
192
+ data: RouteCandidate[];
193
+ }>;
194
+ /** @deprecated use `routing()` — kept for backwards compatibility. */
190
195
  route(id: string): Promise<{
191
196
  data: RouteCandidate[];
192
197
  }>;
@@ -208,6 +213,28 @@ declare class RefundResource {
208
213
  }): Promise<{
209
214
  data: Refund[];
210
215
  }>;
216
+ /**
217
+ * Fetch the payer-facing safe status view for a refund. UNAUTHENTICATED —
218
+ * safe to expose in a customer-facing page or email. Redacts the destination
219
+ * address (only the last 6 chars) and omits merchant PII.
220
+ */
221
+ status(id: string): Promise<{
222
+ id: string;
223
+ payment_intent_id: string;
224
+ amount_usd: number;
225
+ chain: string | null;
226
+ token: string | null;
227
+ destination_tail: string;
228
+ reason: string;
229
+ status: RefundStatus;
230
+ tx_hash: string | null;
231
+ failure_reason: string | null;
232
+ attempts: number;
233
+ created_at: string;
234
+ updated_at: string;
235
+ paid_at: string | null;
236
+ terminal: boolean;
237
+ }>;
211
238
  }
212
239
  declare class PayoutResource {
213
240
  private req;
package/dist/index.d.ts CHANGED
@@ -94,7 +94,7 @@ interface Refund {
94
94
  status: RefundStatus;
95
95
  tx_hash: string | null;
96
96
  created_at: string;
97
- paid_at: string | null;
97
+ updated_at: string;
98
98
  failure_reason?: string | null;
99
99
  }
100
100
  interface Payout {
@@ -132,7 +132,7 @@ interface FragToken {
132
132
  verified: boolean;
133
133
  min_liquidity_usd?: number;
134
134
  }
135
- type WebhookEventType = "payment_intent.created" | "payment_intent.quoted" | "payment_intent.executing" | "payment_intent.executed" | "payment_intent.settled" | "payment_intent.failed" | "payment_intent.expired" | "payment_intent.cancelled" | "payment_intent.refunded" | "quote.created" | "settlement.confirmed" | "payout.pending" | "payout.paid" | "payout.failed" | "refund.pending" | "refund.paid" | "refund.failed";
135
+ type WebhookEventType = "payment_intent.created" | "payment_intent.quoted" | "payment_intent.executing" | "payment_intent.executed" | "payment_intent.settled" | "payment_intent.failed" | "payment_intent.expired" | "payment_intent.cancelled" | "payment_intent.refunded" | "quote.created" | "settlement.confirmed" | "payout.created" | "payout.paid" | "payout.failed" | "refund.created" | "refund.processing" | "refund.paid" | "refund.failed";
136
136
  interface WebhookEvent<T = unknown> {
137
137
  id: string;
138
138
  type: WebhookEventType;
@@ -160,6 +160,7 @@ declare class FragError extends Error {
160
160
  type Requester = <T>(path: string, init?: RequestInit & {
161
161
  idempotencyKey?: string;
162
162
  query?: Record<string, string | number | boolean | undefined>;
163
+ skipAuth?: boolean;
163
164
  }) => Promise<T>;
164
165
  declare class FragmentPay {
165
166
  #private;
@@ -187,6 +188,10 @@ declare class PaymentIntentResource {
187
188
  transactions(id: string): Promise<{
188
189
  data: IntentTransaction[];
189
190
  }>;
191
+ routing(id: string): Promise<{
192
+ data: RouteCandidate[];
193
+ }>;
194
+ /** @deprecated use `routing()` — kept for backwards compatibility. */
190
195
  route(id: string): Promise<{
191
196
  data: RouteCandidate[];
192
197
  }>;
@@ -208,6 +213,28 @@ declare class RefundResource {
208
213
  }): Promise<{
209
214
  data: Refund[];
210
215
  }>;
216
+ /**
217
+ * Fetch the payer-facing safe status view for a refund. UNAUTHENTICATED —
218
+ * safe to expose in a customer-facing page or email. Redacts the destination
219
+ * address (only the last 6 chars) and omits merchant PII.
220
+ */
221
+ status(id: string): Promise<{
222
+ id: string;
223
+ payment_intent_id: string;
224
+ amount_usd: number;
225
+ chain: string | null;
226
+ token: string | null;
227
+ destination_tail: string;
228
+ reason: string;
229
+ status: RefundStatus;
230
+ tx_hash: string | null;
231
+ failure_reason: string | null;
232
+ attempts: number;
233
+ created_at: string;
234
+ updated_at: string;
235
+ paid_at: string | null;
236
+ terminal: boolean;
237
+ }>;
211
238
  }
212
239
  declare class PayoutResource {
213
240
  private req;
package/dist/index.js CHANGED
@@ -54,7 +54,8 @@ var v = {
54
54
  },
55
55
  num(path, val, opts = {}) {
56
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)}`);
57
+ if (typeof n !== "number" || !Number.isFinite(n))
58
+ return fail(path, `expected finite number, got ${JSON.stringify(val)}`);
58
59
  if (opts.int && !Number.isInteger(n)) fail(path, "expected integer");
59
60
  if (opts.min != null && n < opts.min) fail(path, `min ${opts.min}`);
60
61
  if (opts.max != null && n > opts.max) fail(path, `max ${opts.max}`);
@@ -94,7 +95,15 @@ var v = {
94
95
  return v.str(path, val, { max: 254, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ });
95
96
  }
96
97
  };
97
- var CHAINS = ["solana", "ethereum", "base", "arbitrum", "optimism", "polygon", "bnb"];
98
+ var CHAINS = [
99
+ "solana",
100
+ "ethereum",
101
+ "base",
102
+ "arbitrum",
103
+ "optimism",
104
+ "polygon",
105
+ "bnb"
106
+ ];
98
107
  var INTENT_STATUSES = [
99
108
  "created",
100
109
  "requires_payment",
@@ -157,11 +166,16 @@ var FragmentPay = class {
157
166
  throw new FragValidationError("options", "expected FragOptions object");
158
167
  }
159
168
  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)");
169
+ throw new FragValidationError(
170
+ "options.apiKey",
171
+ "must match sk_test_\u2026 or sk_live_\u2026 (min 8 chars after prefix)"
172
+ );
161
173
  }
162
174
  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 });
175
+ if (opts.timeout !== void 0)
176
+ v.num("options.timeout", opts.timeout, { min: 0, max: 6e5, int: true });
177
+ if (opts.retries !== void 0)
178
+ v.num("options.retries", opts.retries, { min: 0, max: 10, int: true });
165
179
  this.#opts = {
166
180
  apiKey: opts.apiKey,
167
181
  baseUrl: (opts.baseUrl ?? "https://api.frag.dev").replace(/\/$/, ""),
@@ -187,7 +201,7 @@ var FragmentPay = class {
187
201
  ).toString() : "";
188
202
  const url = `${this.#opts.baseUrl}${path}${query}`;
189
203
  const headers = new Headers(init.headers);
190
- headers.set("authorization", `Bearer ${this.#opts.apiKey}`);
204
+ if (!init.skipAuth) headers.set("authorization", `Bearer ${this.#opts.apiKey}`);
191
205
  if (init.body && !headers.has("content-type")) headers.set("content-type", "application/json");
192
206
  if (init.idempotencyKey) headers.set("idempotency-key", init.idempotencyKey);
193
207
  const maxAttempts = Math.max(1, this.#opts.retries + 1);
@@ -251,16 +265,35 @@ var PaymentIntentResource = class {
251
265
  }
252
266
  req;
253
267
  create(input) {
254
- if (!input || typeof input !== "object") throw new FragValidationError("createIntent", "expected object");
268
+ if (!input || typeof input !== "object")
269
+ throw new FragValidationError("createIntent", "expected object");
255
270
  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";
271
+ const currency = v.optStr("createIntent.currency", input.currency, {
272
+ min: 3,
273
+ max: 8,
274
+ pattern: /^[A-Z]{3,8}$/
275
+ }) ?? "USD";
257
276
  if (input.settlement !== void 0) v.obj("createIntent.settlement", input.settlement);
258
277
  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 });
278
+ const settlementToken = v.optStr("createIntent.settlement.token", input.settlement?.token, {
279
+ min: 1,
280
+ max: 64
281
+ });
282
+ const settlementAddress = v.optStr(
283
+ "createIntent.settlement.address",
284
+ input.settlement?.address,
285
+ { min: 26, max: 128 }
286
+ );
261
287
  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 });
288
+ const expiresInSeconds = v.optNum("createIntent.expiresInSeconds", input.expiresInSeconds, {
289
+ min: 60,
290
+ max: 86400,
291
+ int: true
292
+ });
293
+ const idempotencyKey = v.optStr("createIntent.idempotencyKey", input.idempotencyKey, {
294
+ min: 8,
295
+ max: 128
296
+ });
264
297
  const successUrl = input.successUrl !== void 0 ? v.url("createIntent.successUrl", input.successUrl) : void 0;
265
298
  const cancelUrl = input.cancelUrl !== void 0 ? v.url("createIntent.cancelUrl", input.cancelUrl) : void 0;
266
299
  if (input.metadata !== void 0) v.obj("createIntent.metadata", input.metadata);
@@ -294,15 +327,22 @@ var PaymentIntentResource = class {
294
327
  }
295
328
  cancel(id) {
296
329
  v.id("paymentIntents.cancel.id", id);
297
- return this.req(`/api/public/v1/payment-intents/${encodeURIComponent(id)}/cancel`, { method: "POST" });
330
+ return this.req(
331
+ `/api/public/v1/payment-intents/${encodeURIComponent(id)}/cancel`,
332
+ { method: "POST" }
333
+ );
298
334
  }
299
335
  transactions(id) {
300
336
  v.id("paymentIntents.transactions.id", id);
301
337
  return this.req(`/api/public/v1/payment-intents/${encodeURIComponent(id)}/transactions`);
302
338
  }
339
+ routing(id) {
340
+ v.id("paymentIntents.routing.id", id);
341
+ return this.req(`/api/public/v1/payment-intents/${encodeURIComponent(id)}/routing`);
342
+ }
343
+ /** @deprecated use `routing()` — kept for backwards compatibility. */
303
344
  route(id) {
304
- v.id("paymentIntents.route.id", id);
305
- return this.req(`/api/public/v1/payment-intents/${encodeURIComponent(id)}/route`);
345
+ return this.routing(id);
306
346
  }
307
347
  };
308
348
  var RefundResource = class {
@@ -311,7 +351,8 @@ var RefundResource = class {
311
351
  }
312
352
  req;
313
353
  create(input) {
314
- if (!input || typeof input !== "object") throw new FragValidationError("refunds.create", "expected object");
354
+ if (!input || typeof input !== "object")
355
+ throw new FragValidationError("refunds.create", "expected object");
315
356
  v.id("refunds.create.paymentIntentId", input.paymentIntentId);
316
357
  v.num("refunds.create.amountUsd", input.amountUsd, { min: 0.01, max: 1e7 });
317
358
  v.str("refunds.create.destination", input.destination, { min: 26, max: 128 });
@@ -333,12 +374,24 @@ var RefundResource = class {
333
374
  return this.req(`/api/public/v1/refunds/${encodeURIComponent(id)}`);
334
375
  }
335
376
  list(params = {}) {
336
- if (params.paymentIntentId !== void 0) v.id("refunds.list.paymentIntentId", params.paymentIntentId);
377
+ if (params.paymentIntentId !== void 0)
378
+ v.id("refunds.list.paymentIntentId", params.paymentIntentId);
337
379
  v.optNum("refunds.list.limit", params.limit, { min: 1, max: 100, int: true });
338
380
  return this.req(`/api/public/v1/refunds`, {
339
381
  query: { payment_intent_id: params.paymentIntentId, limit: params.limit }
340
382
  });
341
383
  }
384
+ /**
385
+ * Fetch the payer-facing safe status view for a refund. UNAUTHENTICATED —
386
+ * safe to expose in a customer-facing page or email. Redacts the destination
387
+ * address (only the last 6 chars) and omits merchant PII.
388
+ */
389
+ status(id) {
390
+ v.id("refunds.status.id", id);
391
+ return this.req(`/api/public/refunds/${encodeURIComponent(id)}/status`, {
392
+ skipAuth: true
393
+ });
394
+ }
342
395
  };
343
396
  var PayoutResource = class {
344
397
  constructor(req) {
@@ -351,10 +404,15 @@ var PayoutResource = class {
351
404
  }
352
405
  list(params = {}) {
353
406
  v.optEnum("payouts.list.status", params.status, PAYOUT_STATUSES);
354
- if (params.paymentIntentId !== void 0) v.id("payouts.list.paymentIntentId", params.paymentIntentId);
407
+ if (params.paymentIntentId !== void 0)
408
+ v.id("payouts.list.paymentIntentId", params.paymentIntentId);
355
409
  v.optNum("payouts.list.limit", params.limit, { min: 1, max: 100, int: true });
356
410
  return this.req(`/api/public/v1/payouts`, {
357
- query: { status: params.status, payment_intent_id: params.paymentIntentId, limit: params.limit }
411
+ query: {
412
+ status: params.status,
413
+ payment_intent_id: params.paymentIntentId,
414
+ limit: params.limit
415
+ }
358
416
  });
359
417
  }
360
418
  replay(id) {
@@ -368,12 +426,18 @@ var WebhookEndpointResource = class {
368
426
  }
369
427
  req;
370
428
  create(input) {
371
- if (!input || typeof input !== "object") throw new FragValidationError("webhookEndpoints.create", "expected object");
429
+ if (!input || typeof input !== "object")
430
+ throw new FragValidationError("webhookEndpoints.create", "expected object");
372
431
  v.url("webhookEndpoints.create.url", input.url);
373
432
  if (!Array.isArray(input.events) || input.events.length === 0) {
374
- throw new FragValidationError("webhookEndpoints.create.events", "at least one event required");
433
+ throw new FragValidationError(
434
+ "webhookEndpoints.create.events",
435
+ "at least one event required"
436
+ );
375
437
  }
376
- input.events.forEach((e, i) => v.enum(`webhookEndpoints.create.events[${i}]`, e, WEBHOOK_EVENTS));
438
+ input.events.forEach(
439
+ (e, i) => v.enum(`webhookEndpoints.create.events[${i}]`, e, WEBHOOK_EVENTS)
440
+ );
377
441
  return this.req("/api/public/v1/webhook-endpoints", {
378
442
  method: "POST",
379
443
  body: JSON.stringify(input)
@@ -390,8 +454,11 @@ var WebhookEndpointResource = class {
390
454
  v.id("webhookEndpoints.update.id", id);
391
455
  if (patch.url !== void 0) v.url("webhookEndpoints.update.url", patch.url);
392
456
  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));
457
+ if (!Array.isArray(patch.events))
458
+ throw new FragValidationError("webhookEndpoints.update.events", "expected array");
459
+ patch.events.forEach(
460
+ (e, i) => v.enum(`webhookEndpoints.update.events[${i}]`, e, WEBHOOK_EVENTS)
461
+ );
395
462
  }
396
463
  return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}`, {
397
464
  method: "PATCH",
@@ -400,11 +467,16 @@ var WebhookEndpointResource = class {
400
467
  }
401
468
  delete(id) {
402
469
  v.id("webhookEndpoints.delete.id", id);
403
- return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}`, { method: "DELETE" });
470
+ return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}`, {
471
+ method: "DELETE"
472
+ });
404
473
  }
405
474
  rotateSecret(id) {
406
475
  v.id("webhookEndpoints.rotateSecret.id", id);
407
- return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}/rotate-secret`, { method: "POST" });
476
+ return this.req(
477
+ `/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}/rotate-secret`,
478
+ { method: "POST" }
479
+ );
408
480
  }
409
481
  };
410
482
  var TokenResource = class {
@@ -434,19 +506,34 @@ var WebhookHelpers = class {
434
506
  }
435
507
  };
436
508
  async function verifyWebhook(input) {
437
- if (!input || typeof input !== "object") throw new FragValidationError("verifyWebhook", "expected object");
509
+ if (!input || typeof input !== "object")
510
+ throw new FragValidationError("verifyWebhook", "expected object");
438
511
  v.str("verifyWebhook.payload", input.payload, { min: 1 });
439
512
  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 });
513
+ if (input.tolerance !== void 0)
514
+ v.num("verifyWebhook.tolerance", input.tolerance, { min: 0, max: 3600, int: true });
441
515
  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);
516
+ const ok = await helper.verify(
517
+ input.payload,
518
+ input.signature ?? null,
519
+ input.secret,
520
+ input.tolerance
521
+ );
522
+ if (!ok)
523
+ throw new FragError(401, "invalid_signature", "webhook signature verification failed", null);
444
524
  return helper.parse(input.payload);
445
525
  }
446
526
  var Frag = class extends FragmentPay {
447
527
  constructor(opts) {
448
528
  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 });
529
+ super({
530
+ apiKey,
531
+ baseUrl: opts.baseUrl,
532
+ fetch: opts.fetch,
533
+ timeout: opts.timeout,
534
+ retries: opts.retries,
535
+ debug: opts.debug
536
+ });
450
537
  }
451
538
  };
452
539
  async function hmacHex(secret, message) {
package/dist/index.mjs CHANGED
@@ -24,7 +24,8 @@ var v = {
24
24
  },
25
25
  num(path, val, opts = {}) {
26
26
  const n = typeof val === "string" ? Number(val) : val;
27
- if (typeof n !== "number" || !Number.isFinite(n)) return fail(path, `expected finite number, got ${JSON.stringify(val)}`);
27
+ if (typeof n !== "number" || !Number.isFinite(n))
28
+ return fail(path, `expected finite number, got ${JSON.stringify(val)}`);
28
29
  if (opts.int && !Number.isInteger(n)) fail(path, "expected integer");
29
30
  if (opts.min != null && n < opts.min) fail(path, `min ${opts.min}`);
30
31
  if (opts.max != null && n > opts.max) fail(path, `max ${opts.max}`);
@@ -64,7 +65,15 @@ var v = {
64
65
  return v.str(path, val, { max: 254, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ });
65
66
  }
66
67
  };
67
- var CHAINS = ["solana", "ethereum", "base", "arbitrum", "optimism", "polygon", "bnb"];
68
+ var CHAINS = [
69
+ "solana",
70
+ "ethereum",
71
+ "base",
72
+ "arbitrum",
73
+ "optimism",
74
+ "polygon",
75
+ "bnb"
76
+ ];
68
77
  var INTENT_STATUSES = [
69
78
  "created",
70
79
  "requires_payment",
@@ -127,11 +136,16 @@ var FragmentPay = class {
127
136
  throw new FragValidationError("options", "expected FragOptions object");
128
137
  }
129
138
  if (typeof opts.apiKey !== "string" || !/^sk_(test|live)_[A-Za-z0-9_-]{8,}$/.test(opts.apiKey)) {
130
- throw new FragValidationError("options.apiKey", "must match sk_test_\u2026 or sk_live_\u2026 (min 8 chars after prefix)");
139
+ throw new FragValidationError(
140
+ "options.apiKey",
141
+ "must match sk_test_\u2026 or sk_live_\u2026 (min 8 chars after prefix)"
142
+ );
131
143
  }
132
144
  if (opts.baseUrl !== void 0) v.url("options.baseUrl", opts.baseUrl);
133
- if (opts.timeout !== void 0) v.num("options.timeout", opts.timeout, { min: 0, max: 6e5, int: true });
134
- if (opts.retries !== void 0) v.num("options.retries", opts.retries, { min: 0, max: 10, int: true });
145
+ if (opts.timeout !== void 0)
146
+ v.num("options.timeout", opts.timeout, { min: 0, max: 6e5, int: true });
147
+ if (opts.retries !== void 0)
148
+ v.num("options.retries", opts.retries, { min: 0, max: 10, int: true });
135
149
  this.#opts = {
136
150
  apiKey: opts.apiKey,
137
151
  baseUrl: (opts.baseUrl ?? "https://api.frag.dev").replace(/\/$/, ""),
@@ -157,7 +171,7 @@ var FragmentPay = class {
157
171
  ).toString() : "";
158
172
  const url = `${this.#opts.baseUrl}${path}${query}`;
159
173
  const headers = new Headers(init.headers);
160
- headers.set("authorization", `Bearer ${this.#opts.apiKey}`);
174
+ if (!init.skipAuth) headers.set("authorization", `Bearer ${this.#opts.apiKey}`);
161
175
  if (init.body && !headers.has("content-type")) headers.set("content-type", "application/json");
162
176
  if (init.idempotencyKey) headers.set("idempotency-key", init.idempotencyKey);
163
177
  const maxAttempts = Math.max(1, this.#opts.retries + 1);
@@ -221,16 +235,35 @@ var PaymentIntentResource = class {
221
235
  }
222
236
  req;
223
237
  create(input) {
224
- if (!input || typeof input !== "object") throw new FragValidationError("createIntent", "expected object");
238
+ if (!input || typeof input !== "object")
239
+ throw new FragValidationError("createIntent", "expected object");
225
240
  const amount = v.num("createIntent.amount", input.amount, { min: 0.01, max: 1e7 });
226
- const currency = v.optStr("createIntent.currency", input.currency, { min: 3, max: 8, pattern: /^[A-Z]{3,8}$/ }) ?? "USD";
241
+ const currency = v.optStr("createIntent.currency", input.currency, {
242
+ min: 3,
243
+ max: 8,
244
+ pattern: /^[A-Z]{3,8}$/
245
+ }) ?? "USD";
227
246
  if (input.settlement !== void 0) v.obj("createIntent.settlement", input.settlement);
228
247
  const settlementChain = input.settlement?.chain ? v.enum("createIntent.settlement.chain", input.settlement.chain, CHAINS) : void 0;
229
- const settlementToken = v.optStr("createIntent.settlement.token", input.settlement?.token, { min: 1, max: 64 });
230
- const settlementAddress = v.optStr("createIntent.settlement.address", input.settlement?.address, { min: 26, max: 128 });
248
+ const settlementToken = v.optStr("createIntent.settlement.token", input.settlement?.token, {
249
+ min: 1,
250
+ max: 64
251
+ });
252
+ const settlementAddress = v.optStr(
253
+ "createIntent.settlement.address",
254
+ input.settlement?.address,
255
+ { min: 26, max: 128 }
256
+ );
231
257
  const customerEmail = input.customerEmail !== void 0 ? v.email("createIntent.customerEmail", input.customerEmail) : void 0;
232
- const expiresInSeconds = v.optNum("createIntent.expiresInSeconds", input.expiresInSeconds, { min: 60, max: 86400, int: true });
233
- const idempotencyKey = v.optStr("createIntent.idempotencyKey", input.idempotencyKey, { min: 8, max: 128 });
258
+ const expiresInSeconds = v.optNum("createIntent.expiresInSeconds", input.expiresInSeconds, {
259
+ min: 60,
260
+ max: 86400,
261
+ int: true
262
+ });
263
+ const idempotencyKey = v.optStr("createIntent.idempotencyKey", input.idempotencyKey, {
264
+ min: 8,
265
+ max: 128
266
+ });
234
267
  const successUrl = input.successUrl !== void 0 ? v.url("createIntent.successUrl", input.successUrl) : void 0;
235
268
  const cancelUrl = input.cancelUrl !== void 0 ? v.url("createIntent.cancelUrl", input.cancelUrl) : void 0;
236
269
  if (input.metadata !== void 0) v.obj("createIntent.metadata", input.metadata);
@@ -264,15 +297,22 @@ var PaymentIntentResource = class {
264
297
  }
265
298
  cancel(id) {
266
299
  v.id("paymentIntents.cancel.id", id);
267
- return this.req(`/api/public/v1/payment-intents/${encodeURIComponent(id)}/cancel`, { method: "POST" });
300
+ return this.req(
301
+ `/api/public/v1/payment-intents/${encodeURIComponent(id)}/cancel`,
302
+ { method: "POST" }
303
+ );
268
304
  }
269
305
  transactions(id) {
270
306
  v.id("paymentIntents.transactions.id", id);
271
307
  return this.req(`/api/public/v1/payment-intents/${encodeURIComponent(id)}/transactions`);
272
308
  }
309
+ routing(id) {
310
+ v.id("paymentIntents.routing.id", id);
311
+ return this.req(`/api/public/v1/payment-intents/${encodeURIComponent(id)}/routing`);
312
+ }
313
+ /** @deprecated use `routing()` — kept for backwards compatibility. */
273
314
  route(id) {
274
- v.id("paymentIntents.route.id", id);
275
- return this.req(`/api/public/v1/payment-intents/${encodeURIComponent(id)}/route`);
315
+ return this.routing(id);
276
316
  }
277
317
  };
278
318
  var RefundResource = class {
@@ -281,7 +321,8 @@ var RefundResource = class {
281
321
  }
282
322
  req;
283
323
  create(input) {
284
- if (!input || typeof input !== "object") throw new FragValidationError("refunds.create", "expected object");
324
+ if (!input || typeof input !== "object")
325
+ throw new FragValidationError("refunds.create", "expected object");
285
326
  v.id("refunds.create.paymentIntentId", input.paymentIntentId);
286
327
  v.num("refunds.create.amountUsd", input.amountUsd, { min: 0.01, max: 1e7 });
287
328
  v.str("refunds.create.destination", input.destination, { min: 26, max: 128 });
@@ -303,12 +344,24 @@ var RefundResource = class {
303
344
  return this.req(`/api/public/v1/refunds/${encodeURIComponent(id)}`);
304
345
  }
305
346
  list(params = {}) {
306
- if (params.paymentIntentId !== void 0) v.id("refunds.list.paymentIntentId", params.paymentIntentId);
347
+ if (params.paymentIntentId !== void 0)
348
+ v.id("refunds.list.paymentIntentId", params.paymentIntentId);
307
349
  v.optNum("refunds.list.limit", params.limit, { min: 1, max: 100, int: true });
308
350
  return this.req(`/api/public/v1/refunds`, {
309
351
  query: { payment_intent_id: params.paymentIntentId, limit: params.limit }
310
352
  });
311
353
  }
354
+ /**
355
+ * Fetch the payer-facing safe status view for a refund. UNAUTHENTICATED —
356
+ * safe to expose in a customer-facing page or email. Redacts the destination
357
+ * address (only the last 6 chars) and omits merchant PII.
358
+ */
359
+ status(id) {
360
+ v.id("refunds.status.id", id);
361
+ return this.req(`/api/public/refunds/${encodeURIComponent(id)}/status`, {
362
+ skipAuth: true
363
+ });
364
+ }
312
365
  };
313
366
  var PayoutResource = class {
314
367
  constructor(req) {
@@ -321,10 +374,15 @@ var PayoutResource = class {
321
374
  }
322
375
  list(params = {}) {
323
376
  v.optEnum("payouts.list.status", params.status, PAYOUT_STATUSES);
324
- if (params.paymentIntentId !== void 0) v.id("payouts.list.paymentIntentId", params.paymentIntentId);
377
+ if (params.paymentIntentId !== void 0)
378
+ v.id("payouts.list.paymentIntentId", params.paymentIntentId);
325
379
  v.optNum("payouts.list.limit", params.limit, { min: 1, max: 100, int: true });
326
380
  return this.req(`/api/public/v1/payouts`, {
327
- query: { status: params.status, payment_intent_id: params.paymentIntentId, limit: params.limit }
381
+ query: {
382
+ status: params.status,
383
+ payment_intent_id: params.paymentIntentId,
384
+ limit: params.limit
385
+ }
328
386
  });
329
387
  }
330
388
  replay(id) {
@@ -338,12 +396,18 @@ var WebhookEndpointResource = class {
338
396
  }
339
397
  req;
340
398
  create(input) {
341
- if (!input || typeof input !== "object") throw new FragValidationError("webhookEndpoints.create", "expected object");
399
+ if (!input || typeof input !== "object")
400
+ throw new FragValidationError("webhookEndpoints.create", "expected object");
342
401
  v.url("webhookEndpoints.create.url", input.url);
343
402
  if (!Array.isArray(input.events) || input.events.length === 0) {
344
- throw new FragValidationError("webhookEndpoints.create.events", "at least one event required");
403
+ throw new FragValidationError(
404
+ "webhookEndpoints.create.events",
405
+ "at least one event required"
406
+ );
345
407
  }
346
- input.events.forEach((e, i) => v.enum(`webhookEndpoints.create.events[${i}]`, e, WEBHOOK_EVENTS));
408
+ input.events.forEach(
409
+ (e, i) => v.enum(`webhookEndpoints.create.events[${i}]`, e, WEBHOOK_EVENTS)
410
+ );
347
411
  return this.req("/api/public/v1/webhook-endpoints", {
348
412
  method: "POST",
349
413
  body: JSON.stringify(input)
@@ -360,8 +424,11 @@ var WebhookEndpointResource = class {
360
424
  v.id("webhookEndpoints.update.id", id);
361
425
  if (patch.url !== void 0) v.url("webhookEndpoints.update.url", patch.url);
362
426
  if (patch.events !== void 0) {
363
- if (!Array.isArray(patch.events)) throw new FragValidationError("webhookEndpoints.update.events", "expected array");
364
- patch.events.forEach((e, i) => v.enum(`webhookEndpoints.update.events[${i}]`, e, WEBHOOK_EVENTS));
427
+ if (!Array.isArray(patch.events))
428
+ throw new FragValidationError("webhookEndpoints.update.events", "expected array");
429
+ patch.events.forEach(
430
+ (e, i) => v.enum(`webhookEndpoints.update.events[${i}]`, e, WEBHOOK_EVENTS)
431
+ );
365
432
  }
366
433
  return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}`, {
367
434
  method: "PATCH",
@@ -370,11 +437,16 @@ var WebhookEndpointResource = class {
370
437
  }
371
438
  delete(id) {
372
439
  v.id("webhookEndpoints.delete.id", id);
373
- return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}`, { method: "DELETE" });
440
+ return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}`, {
441
+ method: "DELETE"
442
+ });
374
443
  }
375
444
  rotateSecret(id) {
376
445
  v.id("webhookEndpoints.rotateSecret.id", id);
377
- return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}/rotate-secret`, { method: "POST" });
446
+ return this.req(
447
+ `/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}/rotate-secret`,
448
+ { method: "POST" }
449
+ );
378
450
  }
379
451
  };
380
452
  var TokenResource = class {
@@ -404,19 +476,34 @@ var WebhookHelpers = class {
404
476
  }
405
477
  };
406
478
  async function verifyWebhook(input) {
407
- if (!input || typeof input !== "object") throw new FragValidationError("verifyWebhook", "expected object");
479
+ if (!input || typeof input !== "object")
480
+ throw new FragValidationError("verifyWebhook", "expected object");
408
481
  v.str("verifyWebhook.payload", input.payload, { min: 1 });
409
482
  v.str("verifyWebhook.secret", input.secret, { min: 8 });
410
- if (input.tolerance !== void 0) v.num("verifyWebhook.tolerance", input.tolerance, { min: 0, max: 3600, int: true });
483
+ if (input.tolerance !== void 0)
484
+ v.num("verifyWebhook.tolerance", input.tolerance, { min: 0, max: 3600, int: true });
411
485
  const helper = new WebhookHelpers();
412
- const ok = await helper.verify(input.payload, input.signature ?? null, input.secret, input.tolerance);
413
- if (!ok) throw new FragError(401, "invalid_signature", "webhook signature verification failed", null);
486
+ const ok = await helper.verify(
487
+ input.payload,
488
+ input.signature ?? null,
489
+ input.secret,
490
+ input.tolerance
491
+ );
492
+ if (!ok)
493
+ throw new FragError(401, "invalid_signature", "webhook signature verification failed", null);
414
494
  return helper.parse(input.payload);
415
495
  }
416
496
  var Frag = class extends FragmentPay {
417
497
  constructor(opts) {
418
498
  const apiKey = "secretKey" in opts ? opts.secretKey : opts.apiKey;
419
- super({ apiKey, baseUrl: opts.baseUrl, fetch: opts.fetch, timeout: opts.timeout, retries: opts.retries, debug: opts.debug });
499
+ super({
500
+ apiKey,
501
+ baseUrl: opts.baseUrl,
502
+ fetch: opts.fetch,
503
+ timeout: opts.timeout,
504
+ retries: opts.retries,
505
+ debug: opts.debug
506
+ });
420
507
  }
421
508
  };
422
509
  async function hmacHex(secret, message) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fragmentpay/server",
3
- "version": "0.3.0",
3
+ "version": "0.3.4",
4
4
  "description": "Frag server SDK — accept crypto payments from any token, on any chain, settle in one asset.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -12,19 +12,39 @@
12
12
  "require": "./dist/index.js"
13
13
  }
14
14
  },
15
- "files": ["dist", "README.md", "LICENSE"],
16
- "keywords": ["crypto", "payments", "stablecoin", "solana", "ethereum", "usdc", "fragmentpay", "frag"],
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "keywords": [
21
+ "crypto",
22
+ "payments",
23
+ "stablecoin",
24
+ "solana",
25
+ "ethereum",
26
+ "usdc",
27
+ "fragmentpay",
28
+ "frag"
29
+ ],
17
30
  "license": "MIT",
18
31
  "sideEffects": false,
19
- "engines": { "node": ">=18" },
32
+ "engines": {
33
+ "node": ">=18"
34
+ },
20
35
  "repository": {
21
36
  "type": "git",
22
37
  "url": "git+https://github.com/fragmentpay/fragpay.git",
23
38
  "directory": "sdk/frag-server"
24
39
  },
25
40
  "homepage": "https://github.com/fragmentpay/fragpay#readme",
26
- "bugs": { "url": "https://github.com/fragmentpay/fragpay/issues" },
27
- "publishConfig": { "access": "public", "provenance": true },
41
+ "bugs": {
42
+ "url": "https://github.com/fragmentpay/fragpay/issues"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public",
46
+ "provenance": true
47
+ },
28
48
  "scripts": {
29
49
  "build": "tsup src/index.ts --format esm,cjs --dts --clean",
30
50
  "prepublishOnly": "npm run build"