@emulators/stripe 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/dist/fonts/GeistPixel-Square.woff2 +0 -0
- package/dist/fonts/geist-sans.woff2 +0 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +952 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,952 @@
|
|
|
1
|
+
// src/store.ts
|
|
2
|
+
function getStripeStore(store) {
|
|
3
|
+
return {
|
|
4
|
+
customers: store.collection("stripe.customers", ["stripe_id", "email"]),
|
|
5
|
+
products: store.collection("stripe.products", ["stripe_id"]),
|
|
6
|
+
prices: store.collection("stripe.prices", ["stripe_id", "product_id"]),
|
|
7
|
+
paymentIntents: store.collection("stripe.payment_intents", ["stripe_id", "customer_id"]),
|
|
8
|
+
charges: store.collection("stripe.charges", ["stripe_id", "customer_id", "payment_intent_id"]),
|
|
9
|
+
checkoutSessions: store.collection("stripe.checkout_sessions", ["stripe_id", "customer_id"])
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/helpers.ts
|
|
14
|
+
import { randomBytes } from "crypto";
|
|
15
|
+
var NUMERIC_KEYS = /* @__PURE__ */ new Set([
|
|
16
|
+
"amount",
|
|
17
|
+
"unit_amount",
|
|
18
|
+
"quantity",
|
|
19
|
+
"amount_total",
|
|
20
|
+
"amount_subtotal",
|
|
21
|
+
"application_fee_amount",
|
|
22
|
+
"transfer_amount"
|
|
23
|
+
]);
|
|
24
|
+
function stripeId(prefix) {
|
|
25
|
+
return `${prefix}_${randomBytes(12).toString("base64url").slice(0, 24)}`;
|
|
26
|
+
}
|
|
27
|
+
function toUnixTimestamp(iso) {
|
|
28
|
+
return Math.floor(new Date(iso).getTime() / 1e3);
|
|
29
|
+
}
|
|
30
|
+
async function parseStripeBody(c) {
|
|
31
|
+
const contentType = c.req.header("Content-Type") ?? "";
|
|
32
|
+
const rawText = await c.req.text();
|
|
33
|
+
if (!rawText) return {};
|
|
34
|
+
if (contentType.includes("application/json")) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(rawText);
|
|
37
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
38
|
+
} catch {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const params = new URLSearchParams(rawText);
|
|
43
|
+
const result = {};
|
|
44
|
+
for (const [key, value] of params) {
|
|
45
|
+
if (key.includes("[")) {
|
|
46
|
+
const parts = key.replace(/]/g, "").split("[");
|
|
47
|
+
let target = result;
|
|
48
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
49
|
+
const part = parts[i];
|
|
50
|
+
const nextIsIndex = /^\d+$/.test(parts[i + 1]);
|
|
51
|
+
const current = target[part];
|
|
52
|
+
if (current === void 0 || current === null || typeof current !== "object") {
|
|
53
|
+
target[part] = nextIsIndex ? [] : {};
|
|
54
|
+
}
|
|
55
|
+
target = target[part];
|
|
56
|
+
}
|
|
57
|
+
const lastKey = parts[parts.length - 1];
|
|
58
|
+
const num = Number(value);
|
|
59
|
+
const coerced = NUMERIC_KEYS.has(lastKey) && Number.isFinite(num) ? num : value;
|
|
60
|
+
if (Array.isArray(target)) {
|
|
61
|
+
const idx = lastKey === "" ? target.length : parseInt(lastKey, 10);
|
|
62
|
+
target[idx] = coerced;
|
|
63
|
+
} else {
|
|
64
|
+
target[lastKey] = coerced;
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
const num = Number(value);
|
|
68
|
+
result[key] = NUMERIC_KEYS.has(key) && Number.isFinite(num) ? num : value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
function stripeError(c, status, type, message, code, param) {
|
|
74
|
+
return c.json(
|
|
75
|
+
{
|
|
76
|
+
error: {
|
|
77
|
+
type,
|
|
78
|
+
message,
|
|
79
|
+
...code && { code },
|
|
80
|
+
...param && { param }
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
status
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
function stripeList(c, items, url, formatFn) {
|
|
87
|
+
const limit = Math.min(parseInt(c.req.query("limit") ?? "10", 10), 100);
|
|
88
|
+
const startingAfter = c.req.query("starting_after");
|
|
89
|
+
const endingBefore = c.req.query("ending_before");
|
|
90
|
+
const createdGte = c.req.query("created[gte]");
|
|
91
|
+
const createdLte = c.req.query("created[lte]");
|
|
92
|
+
let filtered = items;
|
|
93
|
+
if (createdGte) {
|
|
94
|
+
const gte = parseInt(createdGte, 10);
|
|
95
|
+
filtered = filtered.filter((item) => toUnixTimestamp(item.created_at) >= gte);
|
|
96
|
+
}
|
|
97
|
+
if (createdLte) {
|
|
98
|
+
const lte = parseInt(createdLte, 10);
|
|
99
|
+
filtered = filtered.filter((item) => toUnixTimestamp(item.created_at) <= lte);
|
|
100
|
+
}
|
|
101
|
+
filtered.sort((a, b) => b.id - a.id);
|
|
102
|
+
if (startingAfter) {
|
|
103
|
+
const idx = filtered.findIndex((item) => item.stripe_id === startingAfter);
|
|
104
|
+
if (idx !== -1) {
|
|
105
|
+
filtered = filtered.slice(idx + 1);
|
|
106
|
+
}
|
|
107
|
+
} else if (endingBefore) {
|
|
108
|
+
const idx = filtered.findIndex((item) => item.stripe_id === endingBefore);
|
|
109
|
+
if (idx !== -1) {
|
|
110
|
+
filtered = filtered.slice(0, idx);
|
|
111
|
+
filtered = filtered.slice(-limit);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const page = filtered.slice(0, limit);
|
|
115
|
+
const hasMore = filtered.length > limit;
|
|
116
|
+
return c.json({
|
|
117
|
+
object: "list",
|
|
118
|
+
url,
|
|
119
|
+
has_more: hasMore,
|
|
120
|
+
data: page.map(formatFn)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function applyExpand(obj, expandPaths, resolvers) {
|
|
124
|
+
if (!expandPaths || expandPaths.length === 0) return obj;
|
|
125
|
+
const result = { ...obj };
|
|
126
|
+
for (const path of expandPaths) {
|
|
127
|
+
const resolver = resolvers[path];
|
|
128
|
+
const id = result[path];
|
|
129
|
+
if (resolver && typeof id === "string") {
|
|
130
|
+
const expanded = resolver(id);
|
|
131
|
+
if (expanded) {
|
|
132
|
+
result[path] = expanded;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
function parseExpand(c) {
|
|
139
|
+
const fromQuery = c.req.queries("expand[]") ?? [];
|
|
140
|
+
return fromQuery;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/formatters.ts
|
|
144
|
+
function formatCustomer(c) {
|
|
145
|
+
return {
|
|
146
|
+
id: c.stripe_id,
|
|
147
|
+
object: "customer",
|
|
148
|
+
email: c.email,
|
|
149
|
+
name: c.name,
|
|
150
|
+
description: c.description,
|
|
151
|
+
metadata: c.metadata,
|
|
152
|
+
created: toUnixTimestamp(c.created_at),
|
|
153
|
+
livemode: false
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function formatPaymentIntent(pi) {
|
|
157
|
+
return {
|
|
158
|
+
id: pi.stripe_id,
|
|
159
|
+
object: "payment_intent",
|
|
160
|
+
amount: pi.amount,
|
|
161
|
+
currency: pi.currency,
|
|
162
|
+
status: pi.status,
|
|
163
|
+
customer: pi.customer_id,
|
|
164
|
+
description: pi.description,
|
|
165
|
+
payment_method: pi.payment_method,
|
|
166
|
+
metadata: pi.metadata,
|
|
167
|
+
created: toUnixTimestamp(pi.created_at),
|
|
168
|
+
livemode: false
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/routes/customers.ts
|
|
173
|
+
function customerRoutes({ app, store, webhooks }) {
|
|
174
|
+
const ss = getStripeStore(store);
|
|
175
|
+
app.post("/v1/customers", async (c) => {
|
|
176
|
+
const body = await parseStripeBody(c);
|
|
177
|
+
const customer = ss.customers.insert({
|
|
178
|
+
stripe_id: stripeId("cus"),
|
|
179
|
+
email: body.email ?? null,
|
|
180
|
+
name: body.name ?? null,
|
|
181
|
+
description: body.description ?? null,
|
|
182
|
+
metadata: body.metadata ?? {}
|
|
183
|
+
});
|
|
184
|
+
await webhooks.dispatch(
|
|
185
|
+
"customer.created",
|
|
186
|
+
void 0,
|
|
187
|
+
{ type: "customer.created", data: { object: formatCustomer(customer) } },
|
|
188
|
+
"stripe"
|
|
189
|
+
);
|
|
190
|
+
return c.json(formatCustomer(customer), 200);
|
|
191
|
+
});
|
|
192
|
+
app.get("/v1/customers/:id", (c) => {
|
|
193
|
+
const customer = ss.customers.findOneBy("stripe_id", c.req.param("id"));
|
|
194
|
+
if (!customer) return stripeError(c, 404, "invalid_request_error", `No such customer: '${c.req.param("id")}'`, "resource_missing");
|
|
195
|
+
return c.json(formatCustomer(customer));
|
|
196
|
+
});
|
|
197
|
+
app.post("/v1/customers/:id", async (c) => {
|
|
198
|
+
const customer = ss.customers.findOneBy("stripe_id", c.req.param("id"));
|
|
199
|
+
if (!customer) return stripeError(c, 404, "invalid_request_error", `No such customer: '${c.req.param("id")}'`, "resource_missing");
|
|
200
|
+
const body = await parseStripeBody(c);
|
|
201
|
+
const updated = ss.customers.update(customer.id, {
|
|
202
|
+
...body.email !== void 0 && { email: body.email },
|
|
203
|
+
...body.name !== void 0 && { name: body.name },
|
|
204
|
+
...body.description !== void 0 && { description: body.description },
|
|
205
|
+
...body.metadata !== void 0 && { metadata: body.metadata }
|
|
206
|
+
});
|
|
207
|
+
await webhooks.dispatch(
|
|
208
|
+
"customer.updated",
|
|
209
|
+
void 0,
|
|
210
|
+
{ type: "customer.updated", data: { object: formatCustomer(updated) } },
|
|
211
|
+
"stripe"
|
|
212
|
+
);
|
|
213
|
+
return c.json(formatCustomer(updated));
|
|
214
|
+
});
|
|
215
|
+
app.delete("/v1/customers/:id", async (c) => {
|
|
216
|
+
const customer = ss.customers.findOneBy("stripe_id", c.req.param("id"));
|
|
217
|
+
if (!customer) return stripeError(c, 404, "invalid_request_error", `No such customer: '${c.req.param("id")}'`, "resource_missing");
|
|
218
|
+
for (const pi of ss.paymentIntents.findBy("customer_id", customer.stripe_id)) {
|
|
219
|
+
ss.paymentIntents.update(pi.id, { customer_id: null });
|
|
220
|
+
}
|
|
221
|
+
for (const ch of ss.charges.findBy("customer_id", customer.stripe_id)) {
|
|
222
|
+
ss.charges.update(ch.id, { customer_id: null });
|
|
223
|
+
}
|
|
224
|
+
for (const cs of ss.checkoutSessions.findBy("customer_id", customer.stripe_id)) {
|
|
225
|
+
ss.checkoutSessions.update(cs.id, { customer_id: null });
|
|
226
|
+
}
|
|
227
|
+
ss.customers.delete(customer.id);
|
|
228
|
+
await webhooks.dispatch(
|
|
229
|
+
"customer.deleted",
|
|
230
|
+
void 0,
|
|
231
|
+
{ type: "customer.deleted", data: { object: { ...formatCustomer(customer), deleted: true } } },
|
|
232
|
+
"stripe"
|
|
233
|
+
);
|
|
234
|
+
return c.json({ id: customer.stripe_id, object: "customer", deleted: true });
|
|
235
|
+
});
|
|
236
|
+
app.get("/v1/customers", (c) => {
|
|
237
|
+
let items = ss.customers.all();
|
|
238
|
+
const email = c.req.query("email");
|
|
239
|
+
if (email) items = items.filter((cust) => cust.email === email);
|
|
240
|
+
return stripeList(c, items, "/v1/customers", formatCustomer);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/routes/payment-intents.ts
|
|
245
|
+
function paymentIntentRoutes({ app, store, webhooks }) {
|
|
246
|
+
const ss = getStripeStore(store);
|
|
247
|
+
const expandResolvers = {
|
|
248
|
+
customer: (id) => {
|
|
249
|
+
const cust = ss.customers.findOneBy("stripe_id", id);
|
|
250
|
+
return cust ? formatCustomer(cust) : void 0;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
app.post("/v1/payment_intents", async (c) => {
|
|
254
|
+
const body = await parseStripeBody(c);
|
|
255
|
+
if (!body.amount || !body.currency) {
|
|
256
|
+
return stripeError(c, 400, "invalid_request_error", "Missing required param: amount and currency are required.", void 0, "amount");
|
|
257
|
+
}
|
|
258
|
+
if (body.customer && !ss.customers.findOneBy("stripe_id", body.customer)) {
|
|
259
|
+
return stripeError(c, 400, "invalid_request_error", `No such customer: '${body.customer}'`, "resource_missing", "customer");
|
|
260
|
+
}
|
|
261
|
+
const status = body.payment_method ? "requires_confirmation" : "requires_payment_method";
|
|
262
|
+
const pi = ss.paymentIntents.insert({
|
|
263
|
+
stripe_id: stripeId("pi"),
|
|
264
|
+
amount: body.amount,
|
|
265
|
+
currency: body.currency.toLowerCase(),
|
|
266
|
+
status,
|
|
267
|
+
customer_id: body.customer ?? null,
|
|
268
|
+
description: body.description ?? null,
|
|
269
|
+
payment_method: body.payment_method ?? null,
|
|
270
|
+
metadata: body.metadata ?? {}
|
|
271
|
+
});
|
|
272
|
+
await webhooks.dispatch(
|
|
273
|
+
"payment_intent.created",
|
|
274
|
+
void 0,
|
|
275
|
+
{ type: "payment_intent.created", data: { object: formatPaymentIntent(pi) } },
|
|
276
|
+
"stripe"
|
|
277
|
+
);
|
|
278
|
+
return c.json(formatPaymentIntent(pi), 200);
|
|
279
|
+
});
|
|
280
|
+
app.get("/v1/payment_intents/:id", (c) => {
|
|
281
|
+
const pi = ss.paymentIntents.findOneBy("stripe_id", c.req.param("id"));
|
|
282
|
+
if (!pi) return stripeError(c, 404, "invalid_request_error", `No such payment_intent: '${c.req.param("id")}'`, "resource_missing");
|
|
283
|
+
const expand = parseExpand(c);
|
|
284
|
+
const result = applyExpand(formatPaymentIntent(pi), expand, expandResolvers);
|
|
285
|
+
return c.json(result);
|
|
286
|
+
});
|
|
287
|
+
app.post("/v1/payment_intents/:id", async (c) => {
|
|
288
|
+
const pi = ss.paymentIntents.findOneBy("stripe_id", c.req.param("id"));
|
|
289
|
+
if (!pi) return stripeError(c, 404, "invalid_request_error", `No such payment_intent: '${c.req.param("id")}'`, "resource_missing");
|
|
290
|
+
const body = await parseStripeBody(c);
|
|
291
|
+
const updates = {};
|
|
292
|
+
if (body.amount !== void 0) updates.amount = body.amount;
|
|
293
|
+
if (body.currency !== void 0) updates.currency = body.currency.toLowerCase();
|
|
294
|
+
if (body.description !== void 0) updates.description = body.description;
|
|
295
|
+
if (body.metadata !== void 0) updates.metadata = body.metadata;
|
|
296
|
+
if (body.payment_method !== void 0) {
|
|
297
|
+
updates.payment_method = body.payment_method;
|
|
298
|
+
if (pi.status === "requires_payment_method") {
|
|
299
|
+
updates.status = "requires_confirmation";
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const updated = ss.paymentIntents.update(pi.id, updates);
|
|
303
|
+
return c.json(formatPaymentIntent(updated));
|
|
304
|
+
});
|
|
305
|
+
app.post("/v1/payment_intents/:id/confirm", async (c) => {
|
|
306
|
+
const pi = ss.paymentIntents.findOneBy("stripe_id", c.req.param("id"));
|
|
307
|
+
if (!pi) return stripeError(c, 404, "invalid_request_error", `No such payment_intent: '${c.req.param("id")}'`, "resource_missing");
|
|
308
|
+
const body = await parseStripeBody(c);
|
|
309
|
+
if (pi.status !== "requires_confirmation" && pi.status !== "requires_payment_method") {
|
|
310
|
+
return stripeError(c, 400, "invalid_request_error", `This PaymentIntent's status is ${pi.status}, which does not allow confirmation.`, "payment_intent_unexpected_state");
|
|
311
|
+
}
|
|
312
|
+
if (body.payment_method) {
|
|
313
|
+
ss.paymentIntents.update(pi.id, { payment_method: body.payment_method });
|
|
314
|
+
}
|
|
315
|
+
const updated = ss.paymentIntents.update(pi.id, { status: "succeeded" });
|
|
316
|
+
const charge = ss.charges.insert({
|
|
317
|
+
stripe_id: stripeId("ch"),
|
|
318
|
+
amount: updated.amount,
|
|
319
|
+
currency: updated.currency,
|
|
320
|
+
status: "succeeded",
|
|
321
|
+
customer_id: updated.customer_id,
|
|
322
|
+
payment_intent_id: updated.stripe_id,
|
|
323
|
+
description: updated.description,
|
|
324
|
+
metadata: updated.metadata
|
|
325
|
+
});
|
|
326
|
+
await webhooks.dispatch(
|
|
327
|
+
"payment_intent.succeeded",
|
|
328
|
+
void 0,
|
|
329
|
+
{ type: "payment_intent.succeeded", data: { object: formatPaymentIntent(updated) } },
|
|
330
|
+
"stripe"
|
|
331
|
+
);
|
|
332
|
+
await webhooks.dispatch(
|
|
333
|
+
"charge.succeeded",
|
|
334
|
+
void 0,
|
|
335
|
+
{ type: "charge.succeeded", data: { object: { id: charge.stripe_id, object: "charge", amount: charge.amount, currency: charge.currency, status: charge.status } } },
|
|
336
|
+
"stripe"
|
|
337
|
+
);
|
|
338
|
+
return c.json(formatPaymentIntent(updated));
|
|
339
|
+
});
|
|
340
|
+
app.post("/v1/payment_intents/:id/cancel", async (c) => {
|
|
341
|
+
const pi = ss.paymentIntents.findOneBy("stripe_id", c.req.param("id"));
|
|
342
|
+
if (!pi) return stripeError(c, 404, "invalid_request_error", `No such payment_intent: '${c.req.param("id")}'`, "resource_missing");
|
|
343
|
+
if (pi.status === "succeeded" || pi.status === "canceled") {
|
|
344
|
+
return stripeError(c, 400, "invalid_request_error", `This PaymentIntent's status is ${pi.status}, which does not allow cancellation.`, "payment_intent_unexpected_state");
|
|
345
|
+
}
|
|
346
|
+
const updated = ss.paymentIntents.update(pi.id, { status: "canceled" });
|
|
347
|
+
await webhooks.dispatch(
|
|
348
|
+
"payment_intent.canceled",
|
|
349
|
+
void 0,
|
|
350
|
+
{ type: "payment_intent.canceled", data: { object: formatPaymentIntent(updated) } },
|
|
351
|
+
"stripe"
|
|
352
|
+
);
|
|
353
|
+
return c.json(formatPaymentIntent(updated));
|
|
354
|
+
});
|
|
355
|
+
app.get("/v1/payment_intents", (c) => {
|
|
356
|
+
let items = ss.paymentIntents.all();
|
|
357
|
+
const customerId = c.req.query("customer");
|
|
358
|
+
const status = c.req.query("status");
|
|
359
|
+
if (customerId) items = items.filter((pi) => pi.customer_id === customerId);
|
|
360
|
+
if (status) items = items.filter((pi) => pi.status === status);
|
|
361
|
+
return stripeList(c, items, "/v1/payment_intents", formatPaymentIntent);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/routes/charges.ts
|
|
366
|
+
function formatCharge(ch) {
|
|
367
|
+
return {
|
|
368
|
+
id: ch.stripe_id,
|
|
369
|
+
object: "charge",
|
|
370
|
+
amount: ch.amount,
|
|
371
|
+
currency: ch.currency,
|
|
372
|
+
status: ch.status,
|
|
373
|
+
customer: ch.customer_id,
|
|
374
|
+
payment_intent: ch.payment_intent_id,
|
|
375
|
+
description: ch.description,
|
|
376
|
+
metadata: ch.metadata,
|
|
377
|
+
created: toUnixTimestamp(ch.created_at),
|
|
378
|
+
livemode: false
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function chargeRoutes({ app, store }) {
|
|
382
|
+
const ss = getStripeStore(store);
|
|
383
|
+
const expandResolvers = {
|
|
384
|
+
customer: (id) => {
|
|
385
|
+
const cust = ss.customers.findOneBy("stripe_id", id);
|
|
386
|
+
return cust ? formatCustomer(cust) : void 0;
|
|
387
|
+
},
|
|
388
|
+
payment_intent: (id) => {
|
|
389
|
+
const pi = ss.paymentIntents.findOneBy("stripe_id", id);
|
|
390
|
+
return pi ? formatPaymentIntent(pi) : void 0;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
app.get("/v1/charges/:id", (c) => {
|
|
394
|
+
const charge = ss.charges.findOneBy("stripe_id", c.req.param("id"));
|
|
395
|
+
if (!charge) return stripeError(c, 404, "invalid_request_error", `No such charge: '${c.req.param("id")}'`, "resource_missing");
|
|
396
|
+
const expand = parseExpand(c);
|
|
397
|
+
const result = applyExpand(formatCharge(charge), expand, expandResolvers);
|
|
398
|
+
return c.json(result);
|
|
399
|
+
});
|
|
400
|
+
app.get("/v1/charges", (c) => {
|
|
401
|
+
let items = ss.charges.all();
|
|
402
|
+
const customerId = c.req.query("customer");
|
|
403
|
+
const piId = c.req.query("payment_intent");
|
|
404
|
+
if (customerId) items = items.filter((ch) => ch.customer_id === customerId);
|
|
405
|
+
if (piId) items = items.filter((ch) => ch.payment_intent_id === piId);
|
|
406
|
+
return stripeList(c, items, "/v1/charges", formatCharge);
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/routes/products.ts
|
|
411
|
+
function formatProduct(p) {
|
|
412
|
+
return {
|
|
413
|
+
id: p.stripe_id,
|
|
414
|
+
object: "product",
|
|
415
|
+
name: p.name,
|
|
416
|
+
description: p.description,
|
|
417
|
+
active: p.active,
|
|
418
|
+
metadata: p.metadata,
|
|
419
|
+
created: toUnixTimestamp(p.created_at),
|
|
420
|
+
livemode: false
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function productRoutes({ app, store, webhooks }) {
|
|
424
|
+
const ss = getStripeStore(store);
|
|
425
|
+
app.post("/v1/products", async (c) => {
|
|
426
|
+
const body = await parseStripeBody(c);
|
|
427
|
+
if (!body.name) return stripeError(c, 400, "invalid_request_error", "Missing required param: name.", void 0, "name");
|
|
428
|
+
const product = ss.products.insert({
|
|
429
|
+
stripe_id: stripeId("prod"),
|
|
430
|
+
name: body.name,
|
|
431
|
+
description: body.description ?? null,
|
|
432
|
+
active: body.active ?? true,
|
|
433
|
+
metadata: body.metadata ?? {}
|
|
434
|
+
});
|
|
435
|
+
await webhooks.dispatch(
|
|
436
|
+
"product.created",
|
|
437
|
+
void 0,
|
|
438
|
+
{ type: "product.created", data: { object: formatProduct(product) } },
|
|
439
|
+
"stripe"
|
|
440
|
+
);
|
|
441
|
+
return c.json(formatProduct(product), 200);
|
|
442
|
+
});
|
|
443
|
+
app.get("/v1/products/:id", (c) => {
|
|
444
|
+
const product = ss.products.findOneBy("stripe_id", c.req.param("id"));
|
|
445
|
+
if (!product) return stripeError(c, 404, "invalid_request_error", `No such product: '${c.req.param("id")}'`, "resource_missing");
|
|
446
|
+
return c.json(formatProduct(product));
|
|
447
|
+
});
|
|
448
|
+
app.get("/v1/products", (c) => {
|
|
449
|
+
let items = ss.products.all();
|
|
450
|
+
const active = c.req.query("active");
|
|
451
|
+
if (active !== void 0) items = items.filter((p) => p.active === (active === "true"));
|
|
452
|
+
return stripeList(c, items, "/v1/products", formatProduct);
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/routes/prices.ts
|
|
457
|
+
function formatPrice(p) {
|
|
458
|
+
return {
|
|
459
|
+
id: p.stripe_id,
|
|
460
|
+
object: "price",
|
|
461
|
+
product: p.product_id,
|
|
462
|
+
currency: p.currency,
|
|
463
|
+
unit_amount: p.unit_amount,
|
|
464
|
+
type: p.type,
|
|
465
|
+
active: p.active,
|
|
466
|
+
metadata: p.metadata,
|
|
467
|
+
created: toUnixTimestamp(p.created_at),
|
|
468
|
+
livemode: false
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
function formatProduct2(p) {
|
|
472
|
+
return { id: p.stripe_id, object: "product", name: p.name, active: p.active, created: toUnixTimestamp(p.created_at), livemode: false };
|
|
473
|
+
}
|
|
474
|
+
function priceRoutes({ app, store, webhooks }) {
|
|
475
|
+
const ss = getStripeStore(store);
|
|
476
|
+
const expandResolvers = {
|
|
477
|
+
product: (id) => {
|
|
478
|
+
const prod = ss.products.findOneBy("stripe_id", id);
|
|
479
|
+
return prod ? formatProduct2(prod) : void 0;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
app.post("/v1/prices", async (c) => {
|
|
483
|
+
const body = await parseStripeBody(c);
|
|
484
|
+
if (!body.currency || !body.product) {
|
|
485
|
+
return stripeError(c, 400, "invalid_request_error", "Missing required param: currency and product are required.", void 0, "currency");
|
|
486
|
+
}
|
|
487
|
+
if (!ss.products.findOneBy("stripe_id", body.product)) {
|
|
488
|
+
return stripeError(c, 400, "invalid_request_error", `No such product: '${body.product}'`, "resource_missing", "product");
|
|
489
|
+
}
|
|
490
|
+
const price = ss.prices.insert({
|
|
491
|
+
stripe_id: stripeId("price"),
|
|
492
|
+
product_id: body.product,
|
|
493
|
+
currency: body.currency.toLowerCase(),
|
|
494
|
+
unit_amount: body.unit_amount ?? null,
|
|
495
|
+
type: body.recurring ? "recurring" : "one_time",
|
|
496
|
+
active: body.active ?? true,
|
|
497
|
+
metadata: body.metadata ?? {}
|
|
498
|
+
});
|
|
499
|
+
await webhooks.dispatch(
|
|
500
|
+
"price.created",
|
|
501
|
+
void 0,
|
|
502
|
+
{ type: "price.created", data: { object: formatPrice(price) } },
|
|
503
|
+
"stripe"
|
|
504
|
+
);
|
|
505
|
+
return c.json(formatPrice(price), 200);
|
|
506
|
+
});
|
|
507
|
+
app.get("/v1/prices/:id", (c) => {
|
|
508
|
+
const price = ss.prices.findOneBy("stripe_id", c.req.param("id"));
|
|
509
|
+
if (!price) return stripeError(c, 404, "invalid_request_error", `No such price: '${c.req.param("id")}'`, "resource_missing");
|
|
510
|
+
const expand = parseExpand(c);
|
|
511
|
+
const result = applyExpand(formatPrice(price), expand, expandResolvers);
|
|
512
|
+
return c.json(result);
|
|
513
|
+
});
|
|
514
|
+
app.get("/v1/prices", (c) => {
|
|
515
|
+
let items = ss.prices.all();
|
|
516
|
+
const productId = c.req.query("product");
|
|
517
|
+
const active = c.req.query("active");
|
|
518
|
+
if (productId) items = items.filter((p) => p.product_id === productId);
|
|
519
|
+
if (active !== void 0) items = items.filter((p) => p.active === (active === "true"));
|
|
520
|
+
return stripeList(c, items, "/v1/prices", formatPrice);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ../core/dist/index.js
|
|
525
|
+
import { Hono } from "hono";
|
|
526
|
+
import { cors } from "hono/cors";
|
|
527
|
+
import { readFileSync } from "fs";
|
|
528
|
+
import { fileURLToPath } from "url";
|
|
529
|
+
import { dirname, join } from "path";
|
|
530
|
+
function createErrorHandler(documentationUrl) {
|
|
531
|
+
return async (c, next) => {
|
|
532
|
+
if (documentationUrl) {
|
|
533
|
+
c.set("docsUrl", documentationUrl);
|
|
534
|
+
}
|
|
535
|
+
await next();
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
var errorHandler = createErrorHandler();
|
|
539
|
+
var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
|
|
540
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
541
|
+
var FONTS = {
|
|
542
|
+
"geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
|
|
543
|
+
"GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
|
|
544
|
+
};
|
|
545
|
+
function escapeHtml(s) {
|
|
546
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
547
|
+
}
|
|
548
|
+
function escapeAttr(s) {
|
|
549
|
+
return escapeHtml(s).replace(/'/g, "'");
|
|
550
|
+
}
|
|
551
|
+
var CSS = `
|
|
552
|
+
@font-face{
|
|
553
|
+
font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
|
|
554
|
+
src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
|
|
555
|
+
}
|
|
556
|
+
@font-face{
|
|
557
|
+
font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
|
|
558
|
+
src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
|
|
559
|
+
}
|
|
560
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
561
|
+
body{
|
|
562
|
+
font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
|
|
563
|
+
background:#000;color:#33ff00;min-height:100vh;
|
|
564
|
+
-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
|
|
565
|
+
}
|
|
566
|
+
.emu-bar{
|
|
567
|
+
border-bottom:1px solid #0a3300;padding:10px 20px;
|
|
568
|
+
display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
|
|
569
|
+
}
|
|
570
|
+
.emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
|
|
571
|
+
.emu-bar-links{margin-left:auto;display:flex;gap:16px;}
|
|
572
|
+
.emu-bar-links a{
|
|
573
|
+
color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
|
|
574
|
+
}
|
|
575
|
+
.emu-bar-links a:hover{color:#33ff00;}
|
|
576
|
+
.emu-bar-links a .full{display:inline;}
|
|
577
|
+
.emu-bar-links a .short{display:none;}
|
|
578
|
+
@media(max-width:600px){
|
|
579
|
+
.emu-bar-links a .full{display:none;}
|
|
580
|
+
.emu-bar-links a .short{display:inline;}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.content{
|
|
584
|
+
display:flex;align-items:center;justify-content:center;
|
|
585
|
+
min-height:calc(100vh - 42px);padding:24px 16px;
|
|
586
|
+
}
|
|
587
|
+
.content-inner{width:100%;max-width:420px;}
|
|
588
|
+
.card-title{
|
|
589
|
+
font-family:'Geist Pixel',monospace;
|
|
590
|
+
font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
|
|
591
|
+
}
|
|
592
|
+
.card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
|
|
593
|
+
.powered-by{
|
|
594
|
+
position:fixed;bottom:0;left:0;right:0;
|
|
595
|
+
text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
|
|
596
|
+
font-family:'Geist Pixel',monospace;
|
|
597
|
+
}
|
|
598
|
+
.powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
|
|
599
|
+
.powered-by a:hover{color:#33ff00;}
|
|
600
|
+
|
|
601
|
+
.error-title{
|
|
602
|
+
font-family:'Geist Pixel',monospace;
|
|
603
|
+
color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
|
|
604
|
+
}
|
|
605
|
+
.error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
|
|
606
|
+
.error-card{text-align:center;}
|
|
607
|
+
|
|
608
|
+
.user-form{margin-bottom:8px;}
|
|
609
|
+
.user-form:last-of-type{margin-bottom:0;}
|
|
610
|
+
.user-btn{
|
|
611
|
+
width:100%;display:flex;align-items:center;gap:12px;
|
|
612
|
+
padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
|
|
613
|
+
background:#000;color:inherit;cursor:pointer;text-align:left;
|
|
614
|
+
font:inherit;transition:border-color .15s;
|
|
615
|
+
}
|
|
616
|
+
.user-btn:hover{border-color:#33ff00;}
|
|
617
|
+
.avatar{
|
|
618
|
+
width:36px;height:36px;border-radius:50%;
|
|
619
|
+
background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
|
|
620
|
+
display:flex;align-items:center;justify-content:center;flex-shrink:0;
|
|
621
|
+
font-family:'Geist Pixel',monospace;
|
|
622
|
+
}
|
|
623
|
+
.user-text{min-width:0;}
|
|
624
|
+
.user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
|
|
625
|
+
.user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
|
|
626
|
+
.user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
|
|
627
|
+
|
|
628
|
+
.settings-layout{
|
|
629
|
+
max-width:920px;margin:0 auto;padding:28px 20px;
|
|
630
|
+
display:flex;gap:28px;
|
|
631
|
+
}
|
|
632
|
+
.settings-sidebar{width:200px;flex-shrink:0;}
|
|
633
|
+
.settings-sidebar a{
|
|
634
|
+
display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
|
|
635
|
+
text-decoration:none;font-size:.8125rem;transition:color .15s;
|
|
636
|
+
}
|
|
637
|
+
.settings-sidebar a:hover{color:#33ff00;}
|
|
638
|
+
.settings-sidebar a.active{color:#33ff00;font-weight:600;}
|
|
639
|
+
.settings-main{flex:1;min-width:0;}
|
|
640
|
+
|
|
641
|
+
.s-card{
|
|
642
|
+
padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
|
|
643
|
+
}
|
|
644
|
+
.s-card:last-child{border-bottom:none;}
|
|
645
|
+
.s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
|
|
646
|
+
.s-icon{
|
|
647
|
+
width:42px;height:42px;border-radius:8px;
|
|
648
|
+
background:#0a3300;display:flex;align-items:center;justify-content:center;
|
|
649
|
+
font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
|
|
650
|
+
font-family:'Geist Pixel',monospace;
|
|
651
|
+
}
|
|
652
|
+
.s-title{
|
|
653
|
+
font-family:'Geist Pixel',monospace;
|
|
654
|
+
font-size:1.25rem;font-weight:600;color:#33ff00;
|
|
655
|
+
}
|
|
656
|
+
.s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
|
|
657
|
+
.section-heading{
|
|
658
|
+
font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
|
|
659
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
660
|
+
}
|
|
661
|
+
.perm-list{list-style:none;}
|
|
662
|
+
.perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
|
|
663
|
+
.check{color:#33ff00;}
|
|
664
|
+
.org-row{
|
|
665
|
+
display:flex;align-items:center;gap:8px;padding:7px 0;
|
|
666
|
+
border-bottom:1px solid #0a3300;font-size:.8125rem;
|
|
667
|
+
}
|
|
668
|
+
.org-row:last-child{border-bottom:none;}
|
|
669
|
+
.org-icon{
|
|
670
|
+
width:22px;height:22px;border-radius:4px;background:#0a3300;
|
|
671
|
+
display:flex;align-items:center;justify-content:center;
|
|
672
|
+
font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
|
|
673
|
+
font-family:'Geist Pixel',monospace;
|
|
674
|
+
}
|
|
675
|
+
.org-name{font-weight:600;color:#33ff00;}
|
|
676
|
+
.badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
|
|
677
|
+
.badge-granted{background:#0a3300;color:#33ff00;}
|
|
678
|
+
.badge-denied{background:#1a0a0a;color:#ff4444;}
|
|
679
|
+
.badge-requested{background:#0a3300;color:#1a8c00;}
|
|
680
|
+
.btn-revoke{
|
|
681
|
+
display:inline-block;padding:5px 14px;border-radius:6px;
|
|
682
|
+
border:1px solid #0a3300;background:transparent;color:#ff4444;
|
|
683
|
+
font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
|
|
684
|
+
}
|
|
685
|
+
.btn-revoke:hover{border-color:#ff4444;}
|
|
686
|
+
.info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
|
|
687
|
+
.app-link{
|
|
688
|
+
display:flex;align-items:center;gap:12px;padding:12px;
|
|
689
|
+
border:1px solid #0a3300;border-radius:8px;background:#000;
|
|
690
|
+
text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
|
|
691
|
+
}
|
|
692
|
+
.app-link:hover{border-color:#33ff00;}
|
|
693
|
+
.app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
|
|
694
|
+
.app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
|
|
695
|
+
.empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
|
|
696
|
+
`;
|
|
697
|
+
var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
|
|
698
|
+
function emuBar(service) {
|
|
699
|
+
const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
|
|
700
|
+
return `<div class="emu-bar">
|
|
701
|
+
<span class="emu-bar-title">${title}</span>
|
|
702
|
+
<nav class="emu-bar-links">
|
|
703
|
+
<a href="https://github.com/vercel-labs/emulate/issues" target="_blank" rel="noopener"><span class="full">Report Issue</span><span class="short">Report</span></a>
|
|
704
|
+
<a href="https://github.com/vercel-labs/emulate" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
|
|
705
|
+
<a href="https://emulate.dev" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
|
|
706
|
+
</nav>
|
|
707
|
+
</div>`;
|
|
708
|
+
}
|
|
709
|
+
function head(title) {
|
|
710
|
+
return `<!DOCTYPE html>
|
|
711
|
+
<html lang="en">
|
|
712
|
+
<head>
|
|
713
|
+
<meta charset="utf-8"/>
|
|
714
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
715
|
+
<title>${escapeHtml(title)} | emulate</title>
|
|
716
|
+
<style>${CSS}</style>
|
|
717
|
+
</head>`;
|
|
718
|
+
}
|
|
719
|
+
function renderCardPage(title, subtitle, body, service) {
|
|
720
|
+
return `${head(title)}
|
|
721
|
+
<body>
|
|
722
|
+
${emuBar(service)}
|
|
723
|
+
<div class="content">
|
|
724
|
+
<div class="content-inner">
|
|
725
|
+
<div class="card-title">${escapeHtml(title)}</div>
|
|
726
|
+
<div class="card-subtitle">${subtitle}</div>
|
|
727
|
+
${body}
|
|
728
|
+
</div>
|
|
729
|
+
</div>
|
|
730
|
+
${POWERED_BY}
|
|
731
|
+
</body></html>`;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/routes/checkout-sessions.ts
|
|
735
|
+
var SERVICE_LABEL = "Stripe";
|
|
736
|
+
function formatSession(s, baseUrl) {
|
|
737
|
+
return {
|
|
738
|
+
id: s.stripe_id,
|
|
739
|
+
object: "checkout.session",
|
|
740
|
+
mode: s.mode,
|
|
741
|
+
status: s.status,
|
|
742
|
+
payment_status: s.payment_status,
|
|
743
|
+
customer: s.customer_id,
|
|
744
|
+
success_url: s.success_url,
|
|
745
|
+
cancel_url: s.cancel_url,
|
|
746
|
+
metadata: s.metadata,
|
|
747
|
+
created: toUnixTimestamp(s.created_at),
|
|
748
|
+
livemode: false,
|
|
749
|
+
url: s.status === "open" ? `${baseUrl}/checkout/${s.stripe_id}` : null
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function checkoutSessionRoutes({ app, store, webhooks, baseUrl }) {
|
|
753
|
+
const ss = getStripeStore(store);
|
|
754
|
+
app.post("/v1/checkout/sessions", async (c) => {
|
|
755
|
+
const body = await parseStripeBody(c);
|
|
756
|
+
if (!body.mode) return stripeError(c, 400, "invalid_request_error", "Missing required param: mode.", void 0, "mode");
|
|
757
|
+
if (body.customer && !ss.customers.findOneBy("stripe_id", body.customer)) {
|
|
758
|
+
return stripeError(c, 400, "invalid_request_error", `No such customer: '${body.customer}'`, "resource_missing", "customer");
|
|
759
|
+
}
|
|
760
|
+
const lineItems = [];
|
|
761
|
+
if (body.line_items) {
|
|
762
|
+
if (!Array.isArray(body.line_items)) {
|
|
763
|
+
return stripeError(c, 400, "invalid_request_error", "line_items must be an array.", void 0, "line_items");
|
|
764
|
+
}
|
|
765
|
+
for (let i = 0; i < body.line_items.length; i++) {
|
|
766
|
+
const li = body.line_items[i];
|
|
767
|
+
if (!li || typeof li !== "object") {
|
|
768
|
+
return stripeError(c, 400, "invalid_request_error", `Invalid line_items[${i}]: must be an object.`, void 0, `line_items[${i}]`);
|
|
769
|
+
}
|
|
770
|
+
if (!li.price || typeof li.price !== "string") {
|
|
771
|
+
return stripeError(c, 400, "invalid_request_error", `Missing required param: line_items[${i}][price].`, void 0, `line_items[${i}][price]`);
|
|
772
|
+
}
|
|
773
|
+
if (!ss.prices.findOneBy("stripe_id", li.price)) {
|
|
774
|
+
return stripeError(c, 400, "invalid_request_error", `No such price: '${li.price}'`, "resource_missing", `line_items[${i}][price]`);
|
|
775
|
+
}
|
|
776
|
+
const qty = typeof li.quantity === "number" ? li.quantity : parseInt(li.quantity, 10);
|
|
777
|
+
if (!Number.isFinite(qty) || qty < 1) {
|
|
778
|
+
return stripeError(c, 400, "invalid_request_error", `Invalid line_items[${i}][quantity]: must be a positive integer.`, void 0, `line_items[${i}][quantity]`);
|
|
779
|
+
}
|
|
780
|
+
lineItems.push({ price: li.price, quantity: qty });
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
const session = ss.checkoutSessions.insert({
|
|
784
|
+
stripe_id: stripeId("cs"),
|
|
785
|
+
mode: body.mode,
|
|
786
|
+
status: "open",
|
|
787
|
+
payment_status: "unpaid",
|
|
788
|
+
customer_id: body.customer ?? null,
|
|
789
|
+
success_url: body.success_url ?? null,
|
|
790
|
+
cancel_url: body.cancel_url ?? null,
|
|
791
|
+
line_items: lineItems,
|
|
792
|
+
metadata: body.metadata ?? {}
|
|
793
|
+
});
|
|
794
|
+
return c.json(formatSession(session, baseUrl), 200);
|
|
795
|
+
});
|
|
796
|
+
app.get("/v1/checkout/sessions/:id", (c) => {
|
|
797
|
+
const session = ss.checkoutSessions.findOneBy("stripe_id", c.req.param("id"));
|
|
798
|
+
if (!session) return stripeError(c, 404, "invalid_request_error", `No such checkout session: '${c.req.param("id")}'`, "resource_missing");
|
|
799
|
+
return c.json(formatSession(session, baseUrl));
|
|
800
|
+
});
|
|
801
|
+
app.post("/v1/checkout/sessions/:id/expire", async (c) => {
|
|
802
|
+
const session = ss.checkoutSessions.findOneBy("stripe_id", c.req.param("id"));
|
|
803
|
+
if (!session) return stripeError(c, 404, "invalid_request_error", `No such checkout session: '${c.req.param("id")}'`, "resource_missing");
|
|
804
|
+
if (session.status !== "open") {
|
|
805
|
+
return stripeError(c, 400, "invalid_request_error", "Only open sessions can be expired.", "checkout_session_not_open");
|
|
806
|
+
}
|
|
807
|
+
const updated = ss.checkoutSessions.update(session.id, { status: "expired" });
|
|
808
|
+
await webhooks.dispatch(
|
|
809
|
+
"checkout.session.expired",
|
|
810
|
+
void 0,
|
|
811
|
+
{ type: "checkout.session.expired", data: { object: formatSession(updated, baseUrl) } },
|
|
812
|
+
"stripe"
|
|
813
|
+
);
|
|
814
|
+
return c.json(formatSession(updated, baseUrl));
|
|
815
|
+
});
|
|
816
|
+
app.get("/v1/checkout/sessions", (c) => {
|
|
817
|
+
let items = ss.checkoutSessions.all();
|
|
818
|
+
const customerId = c.req.query("customer");
|
|
819
|
+
const status = c.req.query("status");
|
|
820
|
+
const paymentStatus = c.req.query("payment_status");
|
|
821
|
+
if (customerId) items = items.filter((s) => s.customer_id === customerId);
|
|
822
|
+
if (status) items = items.filter((s) => s.status === status);
|
|
823
|
+
if (paymentStatus) items = items.filter((s) => s.payment_status === paymentStatus);
|
|
824
|
+
return stripeList(c, items, "/v1/checkout/sessions", (s) => formatSession(s, baseUrl));
|
|
825
|
+
});
|
|
826
|
+
app.get("/checkout/:id", (c) => {
|
|
827
|
+
const session = ss.checkoutSessions.findOneBy("stripe_id", c.req.param("id"));
|
|
828
|
+
if (!session) {
|
|
829
|
+
return c.html(renderCardPage("Session Not Found", "This checkout session does not exist.", '<p class="empty">The session ID is invalid or has been removed.</p>', SERVICE_LABEL), 404);
|
|
830
|
+
}
|
|
831
|
+
if (session.status !== "open") {
|
|
832
|
+
return c.html(renderCardPage("Session Expired", "This checkout session is no longer available.", `<p class="empty">Status: ${escapeHtml(session.status)}</p>`, SERVICE_LABEL));
|
|
833
|
+
}
|
|
834
|
+
const lineItemsHtml = session.line_items.length > 0 ? session.line_items.map((li) => {
|
|
835
|
+
const priceObj = ss.prices.findOneBy("stripe_id", li.price);
|
|
836
|
+
const product = priceObj ? ss.products.findOneBy("stripe_id", priceObj.product_id) : null;
|
|
837
|
+
const name = product?.name ?? li.price;
|
|
838
|
+
const amount = priceObj ? `$${(priceObj.unit_amount / 100).toFixed(2)} ${priceObj.currency.toUpperCase()}` : "";
|
|
839
|
+
return `<div class="org-row">
|
|
840
|
+
<span class="org-icon">$</span>
|
|
841
|
+
<span class="org-name">${escapeHtml(name)}</span>
|
|
842
|
+
<span class="emu-bar-service">${escapeHtml(amount)} x ${li.quantity}</span>
|
|
843
|
+
</div>`;
|
|
844
|
+
}).join("") : '<p class="empty">No line items</p>';
|
|
845
|
+
const body = `
|
|
846
|
+
${lineItemsHtml}
|
|
847
|
+
<form class="user-form" method="post" action="/checkout/${escapeAttr(session.stripe_id)}/complete">
|
|
848
|
+
<button type="submit" class="user-btn">
|
|
849
|
+
<span class="avatar">$</span>
|
|
850
|
+
<span class="user-text">
|
|
851
|
+
<span class="user-login">Pay and Complete</span>
|
|
852
|
+
</span>
|
|
853
|
+
</button>
|
|
854
|
+
</form>
|
|
855
|
+
${session.cancel_url ? `<p class="info-text"><a href="${escapeAttr(session.cancel_url)}" class="btn-revoke">Cancel</a></p>` : ""}
|
|
856
|
+
`;
|
|
857
|
+
return c.html(renderCardPage("Checkout", `Complete your ${escapeHtml(session.mode)} payment.`, body, SERVICE_LABEL));
|
|
858
|
+
});
|
|
859
|
+
app.post("/checkout/:id/complete", async (c) => {
|
|
860
|
+
const session = ss.checkoutSessions.findOneBy("stripe_id", c.req.param("id"));
|
|
861
|
+
if (!session || session.status !== "open") {
|
|
862
|
+
return c.redirect("/checkout/" + c.req.param("id"));
|
|
863
|
+
}
|
|
864
|
+
const updated = ss.checkoutSessions.update(session.id, { status: "complete", payment_status: "paid" });
|
|
865
|
+
await webhooks.dispatch(
|
|
866
|
+
"checkout.session.completed",
|
|
867
|
+
void 0,
|
|
868
|
+
{ type: "checkout.session.completed", data: { object: formatSession(updated, baseUrl) } },
|
|
869
|
+
"stripe"
|
|
870
|
+
);
|
|
871
|
+
if (session.success_url) {
|
|
872
|
+
return c.redirect(session.success_url);
|
|
873
|
+
}
|
|
874
|
+
return c.html(renderCardPage("Payment Complete", "Your payment was successful.", '<p class="empty check">Payment received</p>', SERVICE_LABEL));
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/index.ts
|
|
879
|
+
function seedDefaults(store, _baseUrl) {
|
|
880
|
+
const ss = getStripeStore(store);
|
|
881
|
+
ss.customers.insert({
|
|
882
|
+
stripe_id: stripeId("cus"),
|
|
883
|
+
email: "test@example.com",
|
|
884
|
+
name: "Test Customer",
|
|
885
|
+
description: null,
|
|
886
|
+
metadata: {}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
function seedFromConfig(store, _baseUrl, config) {
|
|
890
|
+
const ss = getStripeStore(store);
|
|
891
|
+
if (config.customers) {
|
|
892
|
+
for (const c of config.customers) {
|
|
893
|
+
if (c.email) {
|
|
894
|
+
const existing = ss.customers.findOneBy("email", c.email);
|
|
895
|
+
if (existing) continue;
|
|
896
|
+
}
|
|
897
|
+
ss.customers.insert({
|
|
898
|
+
stripe_id: stripeId("cus"),
|
|
899
|
+
email: c.email ?? null,
|
|
900
|
+
name: c.name ?? null,
|
|
901
|
+
description: c.description ?? null,
|
|
902
|
+
metadata: {}
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
if (config.products) {
|
|
907
|
+
for (const p of config.products) {
|
|
908
|
+
const product = ss.products.insert({
|
|
909
|
+
stripe_id: stripeId("prod"),
|
|
910
|
+
name: p.name,
|
|
911
|
+
description: p.description ?? null,
|
|
912
|
+
active: true,
|
|
913
|
+
metadata: {}
|
|
914
|
+
});
|
|
915
|
+
const matchingPrices = config.prices?.filter((pr) => pr.product_name === p.name) ?? [];
|
|
916
|
+
for (const pr of matchingPrices) {
|
|
917
|
+
ss.prices.insert({
|
|
918
|
+
stripe_id: stripeId("price"),
|
|
919
|
+
product_id: product.stripe_id,
|
|
920
|
+
currency: pr.currency.toLowerCase(),
|
|
921
|
+
unit_amount: pr.unit_amount,
|
|
922
|
+
type: "one_time",
|
|
923
|
+
active: true,
|
|
924
|
+
metadata: {}
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
var stripePlugin = {
|
|
931
|
+
name: "stripe",
|
|
932
|
+
register(app, store, webhooks, baseUrl, tokenMap) {
|
|
933
|
+
const ctx = { app, store, webhooks, baseUrl, tokenMap };
|
|
934
|
+
customerRoutes(ctx);
|
|
935
|
+
paymentIntentRoutes(ctx);
|
|
936
|
+
chargeRoutes(ctx);
|
|
937
|
+
productRoutes(ctx);
|
|
938
|
+
priceRoutes(ctx);
|
|
939
|
+
checkoutSessionRoutes(ctx);
|
|
940
|
+
},
|
|
941
|
+
seed(store, baseUrl) {
|
|
942
|
+
seedDefaults(store, baseUrl);
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
var index_default = stripePlugin;
|
|
946
|
+
export {
|
|
947
|
+
index_default as default,
|
|
948
|
+
getStripeStore,
|
|
949
|
+
seedFromConfig,
|
|
950
|
+
stripePlugin
|
|
951
|
+
};
|
|
952
|
+
//# sourceMappingURL=index.js.map
|