@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 +40 -8
- package/dist/index.d.mts +25 -2
- package/dist/index.d.ts +25 -2
- package/dist/index.js +112 -29
- package/dist/index.mjs +112 -29
- package/package.json +26 -6
package/README.md
CHANGED
|
@@ -17,11 +17,11 @@ npm install @fragmentpay/server
|
|
|
17
17
|
|
|
18
18
|
## Required environment variables
|
|
19
19
|
|
|
20
|
-
| Name | Where
|
|
21
|
-
| ------------------------ |
|
|
22
|
-
| `FRAG_SECRET_KEY` | server only
|
|
23
|
-
| `FRAG_WEBHOOK_SECRET` | server only
|
|
24
|
-
| `FRAG_BASE_URL`
|
|
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:
|
|
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("
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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))
|
|
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 = [
|
|
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(
|
|
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)
|
|
164
|
-
|
|
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")
|
|
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, {
|
|
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, {
|
|
260
|
-
|
|
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, {
|
|
263
|
-
|
|
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(
|
|
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")
|
|
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)
|
|
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)
|
|
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: {
|
|
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")
|
|
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(
|
|
433
|
+
throw new FragValidationError(
|
|
434
|
+
"webhookEndpoints.create.events",
|
|
435
|
+
"at least one event required"
|
|
436
|
+
);
|
|
379
437
|
}
|
|
380
|
-
input.events.forEach(
|
|
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))
|
|
398
|
-
|
|
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)}`, {
|
|
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(
|
|
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")
|
|
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)
|
|
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(
|
|
447
|
-
|
|
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({
|
|
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))
|
|
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 = [
|
|
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(
|
|
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)
|
|
134
|
-
|
|
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")
|
|
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, {
|
|
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, {
|
|
230
|
-
|
|
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, {
|
|
233
|
-
|
|
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(
|
|
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")
|
|
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)
|
|
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)
|
|
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: {
|
|
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")
|
|
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(
|
|
403
|
+
throw new FragValidationError(
|
|
404
|
+
"webhookEndpoints.create.events",
|
|
405
|
+
"at least one event required"
|
|
406
|
+
);
|
|
349
407
|
}
|
|
350
|
-
input.events.forEach(
|
|
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))
|
|
368
|
-
|
|
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)}`, {
|
|
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(
|
|
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")
|
|
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)
|
|
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(
|
|
417
|
-
|
|
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({
|
|
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.
|
|
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": [
|
|
16
|
-
|
|
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": {
|
|
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": {
|
|
27
|
-
|
|
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"
|