@fragmentpay/server 0.3.1 → 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;
@@ -212,6 +213,28 @@ declare class RefundResource {
212
213
  }): Promise<{
213
214
  data: Refund[];
214
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
+ }>;
215
238
  }
216
239
  declare class PayoutResource {
217
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;
@@ -212,6 +213,28 @@ declare class RefundResource {
212
213
  }): Promise<{
213
214
  data: Refund[];
214
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
+ }>;
215
238
  }
216
239
  declare class PayoutResource {
217
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,7 +327,10 @@ 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);
@@ -315,7 +351,8 @@ var RefundResource = class {
315
351
  }
316
352
  req;
317
353
  create(input) {
318
- 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");
319
356
  v.id("refunds.create.paymentIntentId", input.paymentIntentId);
320
357
  v.num("refunds.create.amountUsd", input.amountUsd, { min: 0.01, max: 1e7 });
321
358
  v.str("refunds.create.destination", input.destination, { min: 26, max: 128 });
@@ -337,12 +374,24 @@ var RefundResource = class {
337
374
  return this.req(`/api/public/v1/refunds/${encodeURIComponent(id)}`);
338
375
  }
339
376
  list(params = {}) {
340
- 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);
341
379
  v.optNum("refunds.list.limit", params.limit, { min: 1, max: 100, int: true });
342
380
  return this.req(`/api/public/v1/refunds`, {
343
381
  query: { payment_intent_id: params.paymentIntentId, limit: params.limit }
344
382
  });
345
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
+ }
346
395
  };
347
396
  var PayoutResource = class {
348
397
  constructor(req) {
@@ -355,10 +404,15 @@ var PayoutResource = class {
355
404
  }
356
405
  list(params = {}) {
357
406
  v.optEnum("payouts.list.status", params.status, PAYOUT_STATUSES);
358
- 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);
359
409
  v.optNum("payouts.list.limit", params.limit, { min: 1, max: 100, int: true });
360
410
  return this.req(`/api/public/v1/payouts`, {
361
- 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
+ }
362
416
  });
363
417
  }
364
418
  replay(id) {
@@ -372,12 +426,18 @@ var WebhookEndpointResource = class {
372
426
  }
373
427
  req;
374
428
  create(input) {
375
- 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");
376
431
  v.url("webhookEndpoints.create.url", input.url);
377
432
  if (!Array.isArray(input.events) || input.events.length === 0) {
378
- 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
+ );
379
437
  }
380
- 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
+ );
381
441
  return this.req("/api/public/v1/webhook-endpoints", {
382
442
  method: "POST",
383
443
  body: JSON.stringify(input)
@@ -394,8 +454,11 @@ var WebhookEndpointResource = class {
394
454
  v.id("webhookEndpoints.update.id", id);
395
455
  if (patch.url !== void 0) v.url("webhookEndpoints.update.url", patch.url);
396
456
  if (patch.events !== void 0) {
397
- if (!Array.isArray(patch.events)) throw new FragValidationError("webhookEndpoints.update.events", "expected array");
398
- 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
+ );
399
462
  }
400
463
  return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}`, {
401
464
  method: "PATCH",
@@ -404,11 +467,16 @@ var WebhookEndpointResource = class {
404
467
  }
405
468
  delete(id) {
406
469
  v.id("webhookEndpoints.delete.id", id);
407
- 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
+ });
408
473
  }
409
474
  rotateSecret(id) {
410
475
  v.id("webhookEndpoints.rotateSecret.id", id);
411
- 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
+ );
412
480
  }
413
481
  };
414
482
  var TokenResource = class {
@@ -438,19 +506,34 @@ var WebhookHelpers = class {
438
506
  }
439
507
  };
440
508
  async function verifyWebhook(input) {
441
- if (!input || typeof input !== "object") throw new FragValidationError("verifyWebhook", "expected object");
509
+ if (!input || typeof input !== "object")
510
+ throw new FragValidationError("verifyWebhook", "expected object");
442
511
  v.str("verifyWebhook.payload", input.payload, { min: 1 });
443
512
  v.str("verifyWebhook.secret", input.secret, { min: 8 });
444
- 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 });
445
515
  const helper = new WebhookHelpers();
446
- const ok = await helper.verify(input.payload, input.signature ?? null, input.secret, input.tolerance);
447
- 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);
448
524
  return helper.parse(input.payload);
449
525
  }
450
526
  var Frag = class extends FragmentPay {
451
527
  constructor(opts) {
452
528
  const apiKey = "secretKey" in opts ? opts.secretKey : opts.apiKey;
453
- 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
+ });
454
537
  }
455
538
  };
456
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,7 +297,10 @@ 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);
@@ -285,7 +321,8 @@ var RefundResource = class {
285
321
  }
286
322
  req;
287
323
  create(input) {
288
- 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");
289
326
  v.id("refunds.create.paymentIntentId", input.paymentIntentId);
290
327
  v.num("refunds.create.amountUsd", input.amountUsd, { min: 0.01, max: 1e7 });
291
328
  v.str("refunds.create.destination", input.destination, { min: 26, max: 128 });
@@ -307,12 +344,24 @@ var RefundResource = class {
307
344
  return this.req(`/api/public/v1/refunds/${encodeURIComponent(id)}`);
308
345
  }
309
346
  list(params = {}) {
310
- 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);
311
349
  v.optNum("refunds.list.limit", params.limit, { min: 1, max: 100, int: true });
312
350
  return this.req(`/api/public/v1/refunds`, {
313
351
  query: { payment_intent_id: params.paymentIntentId, limit: params.limit }
314
352
  });
315
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
+ }
316
365
  };
317
366
  var PayoutResource = class {
318
367
  constructor(req) {
@@ -325,10 +374,15 @@ var PayoutResource = class {
325
374
  }
326
375
  list(params = {}) {
327
376
  v.optEnum("payouts.list.status", params.status, PAYOUT_STATUSES);
328
- 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);
329
379
  v.optNum("payouts.list.limit", params.limit, { min: 1, max: 100, int: true });
330
380
  return this.req(`/api/public/v1/payouts`, {
331
- 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
+ }
332
386
  });
333
387
  }
334
388
  replay(id) {
@@ -342,12 +396,18 @@ var WebhookEndpointResource = class {
342
396
  }
343
397
  req;
344
398
  create(input) {
345
- 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");
346
401
  v.url("webhookEndpoints.create.url", input.url);
347
402
  if (!Array.isArray(input.events) || input.events.length === 0) {
348
- 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
+ );
349
407
  }
350
- 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
+ );
351
411
  return this.req("/api/public/v1/webhook-endpoints", {
352
412
  method: "POST",
353
413
  body: JSON.stringify(input)
@@ -364,8 +424,11 @@ var WebhookEndpointResource = class {
364
424
  v.id("webhookEndpoints.update.id", id);
365
425
  if (patch.url !== void 0) v.url("webhookEndpoints.update.url", patch.url);
366
426
  if (patch.events !== void 0) {
367
- if (!Array.isArray(patch.events)) throw new FragValidationError("webhookEndpoints.update.events", "expected array");
368
- 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
+ );
369
432
  }
370
433
  return this.req(`/api/public/v1/webhook-endpoints/${encodeURIComponent(id)}`, {
371
434
  method: "PATCH",
@@ -374,11 +437,16 @@ var WebhookEndpointResource = class {
374
437
  }
375
438
  delete(id) {
376
439
  v.id("webhookEndpoints.delete.id", id);
377
- 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
+ });
378
443
  }
379
444
  rotateSecret(id) {
380
445
  v.id("webhookEndpoints.rotateSecret.id", id);
381
- 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
+ );
382
450
  }
383
451
  };
384
452
  var TokenResource = class {
@@ -408,19 +476,34 @@ var WebhookHelpers = class {
408
476
  }
409
477
  };
410
478
  async function verifyWebhook(input) {
411
- if (!input || typeof input !== "object") throw new FragValidationError("verifyWebhook", "expected object");
479
+ if (!input || typeof input !== "object")
480
+ throw new FragValidationError("verifyWebhook", "expected object");
412
481
  v.str("verifyWebhook.payload", input.payload, { min: 1 });
413
482
  v.str("verifyWebhook.secret", input.secret, { min: 8 });
414
- 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 });
415
485
  const helper = new WebhookHelpers();
416
- const ok = await helper.verify(input.payload, input.signature ?? null, input.secret, input.tolerance);
417
- 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);
418
494
  return helper.parse(input.payload);
419
495
  }
420
496
  var Frag = class extends FragmentPay {
421
497
  constructor(opts) {
422
498
  const apiKey = "secretKey" in opts ? opts.secretKey : opts.apiKey;
423
- 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
+ });
424
507
  }
425
508
  };
426
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.1",
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"