@api-emulator/core 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1202 @@
1
+ // src/store.ts
2
+ function serializeValue(value) {
3
+ if (value instanceof Map) {
4
+ return { __type: "Map", entries: [...value.entries()].map(([k, v]) => [k, serializeValue(v)]) };
5
+ }
6
+ if (value instanceof Set) {
7
+ return { __type: "Set", values: [...value.values()] };
8
+ }
9
+ return value;
10
+ }
11
+ function deserializeValue(value) {
12
+ if (value !== null && typeof value === "object" && "__type" in value) {
13
+ const tagged = value;
14
+ if (tagged.__type === "Map") {
15
+ const entries = tagged.entries;
16
+ return new Map(entries.map(([k, v]) => [k, deserializeValue(v)]));
17
+ }
18
+ if (tagged.__type === "Set") {
19
+ return new Set(tagged.values);
20
+ }
21
+ }
22
+ return value;
23
+ }
24
+ var Collection = class {
25
+ constructor(indexFields = []) {
26
+ this.indexFields = indexFields;
27
+ this.fieldNames = indexFields.map(String).sort();
28
+ for (const field of indexFields) {
29
+ this.indexes.set(String(field), /* @__PURE__ */ new Map());
30
+ }
31
+ }
32
+ indexFields;
33
+ items = /* @__PURE__ */ new Map();
34
+ indexes = /* @__PURE__ */ new Map();
35
+ autoId = 1;
36
+ fieldNames;
37
+ addToIndex(item) {
38
+ for (const field of this.indexFields) {
39
+ const value = item[field];
40
+ if (value === void 0 || value === null) continue;
41
+ const indexMap = this.indexes.get(String(field));
42
+ const key = String(value);
43
+ if (!indexMap.has(key)) {
44
+ indexMap.set(key, /* @__PURE__ */ new Set());
45
+ }
46
+ indexMap.get(key).add(item.id);
47
+ }
48
+ }
49
+ removeFromIndex(item) {
50
+ for (const field of this.indexFields) {
51
+ const value = item[field];
52
+ if (value === void 0 || value === null) continue;
53
+ const indexMap = this.indexes.get(String(field));
54
+ const key = String(value);
55
+ indexMap.get(key)?.delete(item.id);
56
+ }
57
+ }
58
+ insert(data) {
59
+ const now = (/* @__PURE__ */ new Date()).toISOString();
60
+ const explicitId = data.id != null && data.id > 0 ? data.id : void 0;
61
+ const id = explicitId ?? this.autoId++;
62
+ if (id >= this.autoId) {
63
+ this.autoId = id + 1;
64
+ }
65
+ const item = {
66
+ ...data,
67
+ id,
68
+ created_at: now,
69
+ updated_at: now
70
+ };
71
+ this.items.set(id, item);
72
+ this.addToIndex(item);
73
+ return item;
74
+ }
75
+ get(id) {
76
+ return this.items.get(id);
77
+ }
78
+ findBy(field, value) {
79
+ if (this.indexes.has(String(field))) {
80
+ const ids = this.indexes.get(String(field)).get(String(value));
81
+ if (!ids) return [];
82
+ return Array.from(ids).map((id) => this.items.get(id)).filter(Boolean);
83
+ }
84
+ return this.all().filter((item) => item[field] === value);
85
+ }
86
+ findOneBy(field, value) {
87
+ return this.findBy(field, value)[0];
88
+ }
89
+ update(id, data) {
90
+ const existing = this.items.get(id);
91
+ if (!existing) return void 0;
92
+ this.removeFromIndex(existing);
93
+ const updated = {
94
+ ...existing,
95
+ ...data,
96
+ id,
97
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
98
+ };
99
+ this.items.set(id, updated);
100
+ this.addToIndex(updated);
101
+ return updated;
102
+ }
103
+ delete(id) {
104
+ const existing = this.items.get(id);
105
+ if (!existing) return false;
106
+ this.removeFromIndex(existing);
107
+ return this.items.delete(id);
108
+ }
109
+ all() {
110
+ return Array.from(this.items.values());
111
+ }
112
+ query(options = {}) {
113
+ let results = this.all();
114
+ if (options.filter) {
115
+ results = results.filter(options.filter);
116
+ }
117
+ const total_count = results.length;
118
+ if (options.sort) {
119
+ results.sort(options.sort);
120
+ }
121
+ const page = options.page ?? 1;
122
+ const per_page = Math.min(options.per_page ?? 30, 100);
123
+ const start = (page - 1) * per_page;
124
+ const paged = results.slice(start, start + per_page);
125
+ return {
126
+ items: paged,
127
+ total_count,
128
+ page,
129
+ per_page,
130
+ has_next: start + per_page < total_count,
131
+ has_prev: page > 1
132
+ };
133
+ }
134
+ count(filter) {
135
+ if (!filter) return this.items.size;
136
+ return this.all().filter(filter).length;
137
+ }
138
+ clear() {
139
+ this.items.clear();
140
+ for (const indexMap of this.indexes.values()) {
141
+ indexMap.clear();
142
+ }
143
+ this.autoId = 1;
144
+ }
145
+ snapshot() {
146
+ return {
147
+ items: this.all(),
148
+ autoId: this.autoId,
149
+ indexFields: this.fieldNames
150
+ };
151
+ }
152
+ restore(snap) {
153
+ this.clear();
154
+ this.autoId = snap.autoId;
155
+ for (const item of snap.items) {
156
+ this.items.set(item.id, item);
157
+ this.addToIndex(item);
158
+ }
159
+ }
160
+ };
161
+ var Store = class {
162
+ collections = /* @__PURE__ */ new Map();
163
+ _data = /* @__PURE__ */ new Map();
164
+ collection(name, indexFields = []) {
165
+ const existing = this.collections.get(name);
166
+ if (existing) {
167
+ if (indexFields.length > 0) {
168
+ const requested = indexFields.map(String).sort();
169
+ if (existing.fieldNames.length !== requested.length || existing.fieldNames.some((f, i) => f !== requested[i])) {
170
+ throw new Error(
171
+ `Collection "${name}" already exists with indexes [${existing.fieldNames}] but was requested with [${requested}]`
172
+ );
173
+ }
174
+ }
175
+ return existing;
176
+ }
177
+ const col = new Collection(indexFields);
178
+ this.collections.set(name, col);
179
+ return col;
180
+ }
181
+ getData(key) {
182
+ return this._data.get(key);
183
+ }
184
+ setData(key, value) {
185
+ this._data.set(key, value);
186
+ }
187
+ reset() {
188
+ for (const collection of this.collections.values()) {
189
+ collection.clear();
190
+ }
191
+ this._data.clear();
192
+ }
193
+ snapshot() {
194
+ const collections = {};
195
+ for (const [name, col] of this.collections) {
196
+ collections[name] = col.snapshot();
197
+ }
198
+ const data = {};
199
+ for (const [key, value] of this._data) {
200
+ data[key] = serializeValue(value);
201
+ }
202
+ return { collections, data };
203
+ }
204
+ restore(snap) {
205
+ const snapshotNames = new Set(Object.keys(snap.collections));
206
+ for (const name of this.collections.keys()) {
207
+ if (!snapshotNames.has(name)) {
208
+ this.collections.delete(name);
209
+ }
210
+ }
211
+ for (const [name, colSnap] of Object.entries(snap.collections)) {
212
+ const indexFields = colSnap.indexFields;
213
+ const col = this.collection(name, indexFields);
214
+ col.restore(colSnap);
215
+ }
216
+ this._data.clear();
217
+ for (const [key, value] of Object.entries(snap.data)) {
218
+ this._data.set(key, deserializeValue(value));
219
+ }
220
+ }
221
+ };
222
+
223
+ // src/server.ts
224
+ import { Hono } from "hono";
225
+ import { cors } from "hono/cors";
226
+
227
+ // src/webhooks.ts
228
+ import { createHmac } from "crypto";
229
+ var MAX_DELIVERIES = 1e3;
230
+ var WebhookDispatcher = class {
231
+ subscriptions = [];
232
+ deliveries = [];
233
+ subscriptionIdCounter = 1;
234
+ deliveryIdCounter = 1;
235
+ register(sub) {
236
+ const { id: explicitId, ...rest } = sub;
237
+ const id = explicitId !== void 0 ? explicitId : this.subscriptionIdCounter++;
238
+ if (id >= this.subscriptionIdCounter) {
239
+ this.subscriptionIdCounter = id + 1;
240
+ }
241
+ const subscription = { ...rest, id };
242
+ this.subscriptions.push(subscription);
243
+ return subscription;
244
+ }
245
+ unregister(id) {
246
+ const idx = this.subscriptions.findIndex((s) => s.id === id);
247
+ if (idx === -1) return false;
248
+ this.subscriptions.splice(idx, 1);
249
+ return true;
250
+ }
251
+ getSubscription(id) {
252
+ return this.subscriptions.find((s) => s.id === id);
253
+ }
254
+ getSubscriptions(owner, repo) {
255
+ return this.subscriptions.filter((s) => {
256
+ if (owner && s.owner !== owner) return false;
257
+ if (repo !== void 0 && s.repo !== repo) return false;
258
+ return true;
259
+ });
260
+ }
261
+ updateSubscription(id, data) {
262
+ const sub = this.subscriptions.find((s) => s.id === id);
263
+ if (!sub) return void 0;
264
+ Object.assign(sub, data);
265
+ return sub;
266
+ }
267
+ async dispatch(event, action, payload, owner, repo) {
268
+ const matchingSubs = this.subscriptions.filter((s) => {
269
+ if (!s.active) return false;
270
+ if (s.owner !== owner) return false;
271
+ if (repo !== void 0) {
272
+ if (s.repo !== repo) return false;
273
+ } else if (s.repo !== void 0) {
274
+ return false;
275
+ }
276
+ return event === "ping" || s.events.includes("*") || s.events.includes(event);
277
+ });
278
+ for (const sub of matchingSubs) {
279
+ const delivery = {
280
+ id: this.deliveryIdCounter++,
281
+ hook_id: sub.id,
282
+ event,
283
+ action,
284
+ payload,
285
+ status_code: null,
286
+ delivered_at: (/* @__PURE__ */ new Date()).toISOString(),
287
+ duration: null,
288
+ success: false
289
+ };
290
+ const body = JSON.stringify(payload);
291
+ const signatureHeaders = {};
292
+ if (sub.secret) {
293
+ const hmac = createHmac("sha256", sub.secret).update(body).digest("hex");
294
+ signatureHeaders["X-Hub-Signature-256"] = `sha256=${hmac}`;
295
+ }
296
+ try {
297
+ const start = Date.now();
298
+ const response = await fetch(sub.url, {
299
+ method: "POST",
300
+ headers: {
301
+ "Content-Type": "application/json",
302
+ "X-GitHub-Event": event,
303
+ "X-GitHub-Delivery": String(delivery.id),
304
+ ...signatureHeaders
305
+ },
306
+ body,
307
+ signal: AbortSignal.timeout(1e4)
308
+ });
309
+ delivery.duration = Date.now() - start;
310
+ delivery.status_code = response.status;
311
+ delivery.success = response.ok;
312
+ } catch {
313
+ delivery.duration = 0;
314
+ delivery.success = false;
315
+ }
316
+ this.deliveries.push(delivery);
317
+ if (this.deliveries.length > MAX_DELIVERIES) {
318
+ this.deliveries.splice(0, this.deliveries.length - MAX_DELIVERIES);
319
+ }
320
+ }
321
+ }
322
+ getDeliveries(hookId) {
323
+ if (hookId !== void 0) {
324
+ return this.deliveries.filter((d) => d.hook_id === hookId);
325
+ }
326
+ return [...this.deliveries];
327
+ }
328
+ clear() {
329
+ this.subscriptions.length = 0;
330
+ this.deliveries.length = 0;
331
+ this.subscriptionIdCounter = 1;
332
+ this.deliveryIdCounter = 1;
333
+ }
334
+ };
335
+
336
+ // src/middleware/error-handler.ts
337
+ var DEFAULT_DOCS_URL = "https://api-emulator.jsj.sh";
338
+ function getDocsUrl(c) {
339
+ return c.get("docsUrl") ?? DEFAULT_DOCS_URL;
340
+ }
341
+ function errorStatus(err) {
342
+ if (err && typeof err === "object" && "status" in err) {
343
+ const s = err.status;
344
+ if (typeof s === "number" && Number.isFinite(s)) return s;
345
+ }
346
+ return 500;
347
+ }
348
+ function createApiErrorHandler(documentationUrl) {
349
+ return (err, c) => {
350
+ if (documentationUrl) {
351
+ c.set("docsUrl", documentationUrl);
352
+ }
353
+ const status = errorStatus(err);
354
+ const message = err instanceof Error ? err.message : "Internal Server Error";
355
+ return c.json(
356
+ {
357
+ message,
358
+ documentation_url: getDocsUrl(c)
359
+ },
360
+ status
361
+ );
362
+ };
363
+ }
364
+ function createErrorHandler(documentationUrl) {
365
+ return async (c, next) => {
366
+ if (documentationUrl) {
367
+ c.set("docsUrl", documentationUrl);
368
+ }
369
+ await next();
370
+ };
371
+ }
372
+ var errorHandler = createErrorHandler();
373
+ var ApiError = class extends Error {
374
+ constructor(status, message, errors) {
375
+ super(message);
376
+ this.status = status;
377
+ this.errors = errors;
378
+ this.name = "ApiError";
379
+ }
380
+ status;
381
+ errors;
382
+ };
383
+ function notFound(resource) {
384
+ return new ApiError(404, resource ? `${resource} not found` : "Not Found");
385
+ }
386
+ function validationError(message, errors) {
387
+ return new ApiError(422, message, errors);
388
+ }
389
+ function unauthorized() {
390
+ return new ApiError(401, "Requires authentication");
391
+ }
392
+ function forbidden() {
393
+ return new ApiError(403, "Forbidden");
394
+ }
395
+ async function parseJsonBody(c) {
396
+ try {
397
+ const body = await c.req.json();
398
+ if (body && typeof body === "object" && !Array.isArray(body)) {
399
+ return body;
400
+ }
401
+ return {};
402
+ } catch {
403
+ throw new ApiError(400, "Problems parsing JSON");
404
+ }
405
+ }
406
+
407
+ // src/middleware/auth.ts
408
+ import { jwtVerify, importPKCS8 } from "jose";
409
+
410
+ // src/debug.ts
411
+ var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
412
+ function debug(label, ...args) {
413
+ if (isDebug) {
414
+ console.log(`[${label}]`, ...args);
415
+ }
416
+ }
417
+
418
+ // src/middleware/auth.ts
419
+ function serializeTokenMap(tokenMap) {
420
+ return [...tokenMap.entries()].map(([token, user]) => ({
421
+ token,
422
+ login: user.login,
423
+ id: user.id,
424
+ scopes: user.scopes
425
+ }));
426
+ }
427
+ function restoreTokenMap(tokenMap, tokens) {
428
+ tokenMap.clear();
429
+ for (const t of tokens) {
430
+ tokenMap.set(t.token, { login: t.login, id: t.id, scopes: t.scopes });
431
+ }
432
+ }
433
+ function authMiddleware(tokens, appKeyResolver, fallbackUser) {
434
+ return async (c, next) => {
435
+ const authHeader = c.req.header("Authorization");
436
+ if (authHeader) {
437
+ const token = authHeader.replace(/^(Bearer|token)\s+/i, "").trim();
438
+ if (token.startsWith("eyJ") && appKeyResolver) {
439
+ try {
440
+ const [, payloadB64] = token.split(".");
441
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
442
+ const appId = typeof payload.iss === "string" ? parseInt(payload.iss, 10) : payload.iss;
443
+ if (typeof appId === "number" && !isNaN(appId)) {
444
+ const appInfo = appKeyResolver(appId);
445
+ if (appInfo) {
446
+ const key = await importPKCS8(appInfo.privateKey, "RS256");
447
+ await jwtVerify(token, key, { algorithms: ["RS256"] });
448
+ c.set("authApp", {
449
+ appId,
450
+ slug: appInfo.slug,
451
+ name: appInfo.name
452
+ });
453
+ }
454
+ }
455
+ } catch {
456
+ }
457
+ } else {
458
+ let user = tokens.get(token);
459
+ if (!user && fallbackUser && token.length > 0) {
460
+ debug("auth", "fallback user for unknown token", { login: fallbackUser.login, id: fallbackUser.id });
461
+ user = { login: fallbackUser.login, id: fallbackUser.id, scopes: fallbackUser.scopes };
462
+ }
463
+ if (user) {
464
+ c.set("authUser", user);
465
+ c.set("authToken", token);
466
+ c.set("authScopes", user.scopes);
467
+ }
468
+ }
469
+ }
470
+ await next();
471
+ };
472
+ }
473
+ function requireAuth() {
474
+ return async (c, next) => {
475
+ if (!c.get("authUser")) {
476
+ const docsUrl = c.get("docsUrl") ?? "https://api-emulator.jsj.sh";
477
+ return c.json(
478
+ {
479
+ message: "Requires authentication",
480
+ documentation_url: docsUrl
481
+ },
482
+ 401
483
+ );
484
+ }
485
+ await next();
486
+ };
487
+ }
488
+ function requireAppAuth() {
489
+ return async (c, next) => {
490
+ if (!c.get("authApp")) {
491
+ const docsUrl = c.get("docsUrl") ?? "https://api-emulator.jsj.sh";
492
+ return c.json(
493
+ {
494
+ message: "A JSON web token could not be decoded",
495
+ documentation_url: docsUrl
496
+ },
497
+ 401
498
+ );
499
+ }
500
+ await next();
501
+ };
502
+ }
503
+
504
+ // src/fonts.ts
505
+ import { readFileSync } from "fs";
506
+ import { fileURLToPath } from "url";
507
+ import { dirname, join } from "path";
508
+ var __dirname = dirname(fileURLToPath(import.meta.url));
509
+ var FONTS = {
510
+ "geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
511
+ "GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
512
+ };
513
+ var FAVICON = readFileSync(join(__dirname, "fonts", "favicon.ico"));
514
+ function registerFontRoutes(app) {
515
+ app.get("/_emulate/fonts/:name", (c) => {
516
+ const name = c.req.param("name");
517
+ const buf = FONTS[name];
518
+ if (!buf) return c.notFound();
519
+ return new Response(buf, {
520
+ headers: {
521
+ "Content-Type": "font/woff2",
522
+ "Cache-Control": "public, max-age=31536000, immutable",
523
+ "Access-Control-Allow-Origin": "*"
524
+ }
525
+ });
526
+ });
527
+ app.get("/_emulate/favicon.ico", () => {
528
+ return new Response(FAVICON, {
529
+ headers: {
530
+ "Content-Type": "image/x-icon",
531
+ "Cache-Control": "public, max-age=31536000, immutable"
532
+ }
533
+ });
534
+ });
535
+ }
536
+
537
+ // src/server.ts
538
+ function createServer(plugin, options = {}) {
539
+ const port = options.port ?? 4e3;
540
+ const baseUrl = options.baseUrl ?? `http://localhost:${port}`;
541
+ const app = new Hono();
542
+ const store = new Store();
543
+ const webhooks = new WebhookDispatcher();
544
+ const tokenMap = /* @__PURE__ */ new Map();
545
+ if (options.tokens) {
546
+ for (const [token, user] of Object.entries(options.tokens)) {
547
+ tokenMap.set(token, {
548
+ login: user.login,
549
+ id: user.id,
550
+ scopes: user.scopes ?? ["repo", "user", "admin:org", "admin:repo_hook"]
551
+ });
552
+ }
553
+ }
554
+ const docsUrl = options.docsUrl ?? `https://api-emulator.jsj.sh/${plugin.name}`;
555
+ registerFontRoutes(app);
556
+ app.onError(createApiErrorHandler(docsUrl));
557
+ app.use("*", cors());
558
+ app.use("*", createErrorHandler(docsUrl));
559
+ app.use("*", authMiddleware(tokenMap, options.appKeyResolver, options.fallbackUser));
560
+ const rateLimitCounters = /* @__PURE__ */ new Map();
561
+ let lastPruneAt = Math.floor(Date.now() / 1e3);
562
+ app.use("*", async (c, next) => {
563
+ const token = c.get("authToken") ?? "__anonymous__";
564
+ const now = Math.floor(Date.now() / 1e3);
565
+ if (now - lastPruneAt > 3600) {
566
+ for (const [key, val] of rateLimitCounters) {
567
+ if (val.resetAt <= now) rateLimitCounters.delete(key);
568
+ }
569
+ lastPruneAt = now;
570
+ }
571
+ let counter = rateLimitCounters.get(token);
572
+ if (!counter || counter.resetAt <= now) {
573
+ counter = { remaining: 5e3, resetAt: now + 3600 };
574
+ rateLimitCounters.set(token, counter);
575
+ }
576
+ counter.remaining = Math.max(0, counter.remaining - 1);
577
+ c.header("X-RateLimit-Limit", "5000");
578
+ c.header("X-RateLimit-Remaining", String(counter.remaining));
579
+ c.header("X-RateLimit-Reset", String(counter.resetAt));
580
+ c.header("X-RateLimit-Resource", "core");
581
+ if (counter.remaining === 0) {
582
+ return c.json(
583
+ {
584
+ message: "API rate limit exceeded",
585
+ documentation_url: docsUrl
586
+ },
587
+ 403
588
+ );
589
+ }
590
+ await next();
591
+ });
592
+ plugin.register(app, store, webhooks, baseUrl, tokenMap);
593
+ app.notFound(
594
+ (c) => c.json(
595
+ {
596
+ message: "Not Found",
597
+ documentation_url: docsUrl
598
+ },
599
+ 404
600
+ )
601
+ );
602
+ return { app, store, webhooks, port, baseUrl, tokenMap };
603
+ }
604
+
605
+ // src/fixture.ts
606
+ function createStoreFixture(service, store, options = {}) {
607
+ return {
608
+ version: 1,
609
+ service,
610
+ capturedAt: options.capturedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
611
+ store,
612
+ interactions: options.interactions,
613
+ metadata: options.metadata
614
+ };
615
+ }
616
+ function isStoreFixture(source) {
617
+ return "version" in source && source.version === 1 && "store" in source;
618
+ }
619
+ function fixtureStoreSnapshot(source) {
620
+ return isStoreFixture(source) ? source.store : source;
621
+ }
622
+
623
+ // src/middleware/pagination.ts
624
+ function parsePagination(c) {
625
+ const page = Math.max(1, parseInt(c.req.query("page") ?? "1", 10) || 1);
626
+ const per_page = Math.min(100, Math.max(1, parseInt(c.req.query("per_page") ?? "30", 10) || 30));
627
+ return { page, per_page };
628
+ }
629
+ function setLinkHeader(c, totalCount, page, perPage) {
630
+ const lastPage = Math.max(1, Math.ceil(totalCount / perPage));
631
+ const baseUrl = new URL(c.req.url);
632
+ const links = [];
633
+ const makeLink = (p, rel) => {
634
+ baseUrl.searchParams.set("page", String(p));
635
+ baseUrl.searchParams.set("per_page", String(perPage));
636
+ return `<${baseUrl.toString()}>; rel="${rel}"`;
637
+ };
638
+ if (page < lastPage) {
639
+ links.push(makeLink(page + 1, "next"));
640
+ links.push(makeLink(lastPage, "last"));
641
+ }
642
+ if (page > 1) {
643
+ links.push(makeLink(1, "first"));
644
+ links.push(makeLink(page - 1, "prev"));
645
+ }
646
+ if (links.length > 0) {
647
+ c.header("Link", links.join(", "));
648
+ }
649
+ }
650
+
651
+ // src/ui.ts
652
+ function escapeHtml(s) {
653
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
654
+ }
655
+ function escapeAttr(s) {
656
+ return escapeHtml(s).replace(/'/g, "&#39;");
657
+ }
658
+ var CSS = `
659
+ @font-face{
660
+ font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
661
+ src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
662
+ }
663
+ @font-face{
664
+ font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
665
+ src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
666
+ }
667
+ *{box-sizing:border-box;margin:0;padding:0}
668
+ body{
669
+ font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
670
+ background:#000;color:#33ff00;min-height:100vh;
671
+ -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
672
+ }
673
+ .emu-bar{
674
+ border-bottom:1px solid #0a3300;padding:10px 20px;
675
+ display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
676
+ }
677
+ .emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
678
+ .emu-bar-links{margin-left:auto;display:flex;gap:16px;}
679
+ .emu-bar-links a{
680
+ color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
681
+ }
682
+ .emu-bar-links a:hover{color:#33ff00;}
683
+ .emu-bar-links a .full{display:inline;}
684
+ .emu-bar-links a .short{display:none;}
685
+ @media(max-width:600px){
686
+ .emu-bar-links a .full{display:none;}
687
+ .emu-bar-links a .short{display:inline;}
688
+ }
689
+
690
+ .content{
691
+ display:flex;align-items:center;justify-content:center;
692
+ min-height:calc(100vh - 42px);padding:24px 16px;
693
+ }
694
+ .content-inner{width:100%;max-width:420px;}
695
+ .card-title{
696
+ font-family:'Geist Pixel',monospace;
697
+ font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
698
+ }
699
+ .card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
700
+ .powered-by{
701
+ position:fixed;bottom:0;left:0;right:0;
702
+ text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
703
+ font-family:'Geist Pixel',monospace;
704
+ }
705
+ .powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
706
+ .powered-by a:hover{color:#33ff00;}
707
+
708
+ .error-title{
709
+ font-family:'Geist Pixel',monospace;
710
+ color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
711
+ }
712
+ .error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
713
+ .error-card{text-align:center;}
714
+
715
+ .user-form{margin-bottom:8px;}
716
+ .user-form:last-of-type{margin-bottom:0;}
717
+ .user-btn{
718
+ width:100%;display:flex;align-items:center;gap:12px;
719
+ padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
720
+ background:#000;color:inherit;cursor:pointer;text-align:left;
721
+ font:inherit;transition:border-color .15s;
722
+ }
723
+ .user-btn:hover{border-color:#33ff00;}
724
+ .avatar{
725
+ width:36px;height:36px;border-radius:50%;
726
+ background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
727
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
728
+ font-family:'Geist Pixel',monospace;
729
+ }
730
+ .user-text{min-width:0;}
731
+ .user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
732
+ .user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
733
+ .user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
734
+
735
+ .settings-layout{
736
+ max-width:920px;margin:0 auto;padding:28px 20px;
737
+ display:flex;gap:28px;
738
+ }
739
+ .settings-sidebar{width:200px;flex-shrink:0;}
740
+ .settings-sidebar a{
741
+ display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
742
+ text-decoration:none;font-size:.8125rem;transition:color .15s;
743
+ }
744
+ .settings-sidebar a:hover{color:#33ff00;}
745
+ .settings-sidebar a.active{color:#33ff00;font-weight:600;}
746
+ .settings-main{flex:1;min-width:0;}
747
+
748
+ .s-card{
749
+ padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
750
+ }
751
+ .s-card:last-child{border-bottom:none;}
752
+ .s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
753
+ .s-icon{
754
+ width:42px;height:42px;border-radius:8px;
755
+ background:#0a3300;display:flex;align-items:center;justify-content:center;
756
+ font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
757
+ font-family:'Geist Pixel',monospace;
758
+ }
759
+ .s-title{
760
+ font-family:'Geist Pixel',monospace;
761
+ font-size:1.25rem;font-weight:600;color:#33ff00;
762
+ }
763
+ .s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
764
+ .section-heading{
765
+ font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
766
+ display:flex;align-items:center;justify-content:space-between;
767
+ }
768
+ .perm-list{list-style:none;}
769
+ .perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
770
+ .check{color:#33ff00;}
771
+ .org-row{
772
+ display:flex;align-items:center;gap:8px;padding:7px 0;
773
+ border-bottom:1px solid #0a3300;font-size:.8125rem;
774
+ }
775
+ .org-row:last-child{border-bottom:none;}
776
+ .org-icon{
777
+ width:22px;height:22px;border-radius:4px;background:#0a3300;
778
+ display:flex;align-items:center;justify-content:center;
779
+ font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
780
+ font-family:'Geist Pixel',monospace;
781
+ }
782
+ .org-name{font-weight:600;color:#33ff00;}
783
+ .badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
784
+ .badge-granted{background:#0a3300;color:#33ff00;}
785
+ .badge-denied{background:#1a0a0a;color:#ff4444;}
786
+ .badge-requested{background:#0a3300;color:#1a8c00;}
787
+ .btn-revoke{
788
+ display:inline-block;padding:5px 14px;border-radius:6px;
789
+ border:1px solid #0a3300;background:transparent;color:#ff4444;
790
+ font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
791
+ }
792
+ .btn-revoke:hover{border-color:#ff4444;}
793
+ .info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
794
+ .app-link{
795
+ display:flex;align-items:center;gap:12px;padding:12px;
796
+ border:1px solid #0a3300;border-radius:8px;background:#000;
797
+ text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
798
+ }
799
+ .app-link:hover{border-color:#33ff00;}
800
+ .app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
801
+ .app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
802
+ .empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
803
+
804
+ .inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
805
+ .inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
806
+ .inspector-tabs a{
807
+ padding:7px 16px;border-radius:6px;text-decoration:none;
808
+ font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
809
+ transition:color .15s,border-color .15s;
810
+ }
811
+ .inspector-tabs a:hover{color:#33ff00;}
812
+ .inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
813
+ .inspector-section{margin-bottom:24px;}
814
+ .inspector-section h2{
815
+ font-family:'Geist Pixel',monospace;
816
+ font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
817
+ }
818
+ .inspector-section h3{
819
+ font-family:'Geist Pixel',monospace;
820
+ font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
821
+ }
822
+ .inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
823
+ .inspector-table th,.inspector-table td{
824
+ text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
825
+ font-size:.8125rem;
826
+ }
827
+ .inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
828
+ .inspector-table td{color:#33ff00;}
829
+ .inspector-table tbody tr{transition:background .1s;}
830
+ .inspector-table tbody tr:hover{background:#0a3300;}
831
+ .inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
832
+
833
+ .checkout-layout{
834
+ display:flex;min-height:calc(100vh - 42px);
835
+ }
836
+ .checkout-summary{
837
+ flex:1;background:#020;padding:48px 40px 48px 10%;
838
+ display:flex;flex-direction:column;justify-content:center;
839
+ border-right:1px solid #0a3300;
840
+ }
841
+ .checkout-form-side{
842
+ flex:1;background:#000;padding:48px 10% 48px 40px;
843
+ display:flex;flex-direction:column;justify-content:center;
844
+ }
845
+ .checkout-merchant{
846
+ display:flex;align-items:center;gap:10px;margin-bottom:6px;
847
+ }
848
+ .checkout-merchant-name{
849
+ font-family:'Geist Pixel',monospace;
850
+ font-size:.9375rem;font-weight:600;color:#33ff00;
851
+ }
852
+ .checkout-test-badge{
853
+ font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
854
+ background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
855
+ }
856
+ .checkout-total{
857
+ font-family:'Geist Pixel',monospace;
858
+ font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
859
+ }
860
+ .checkout-line-item{
861
+ display:flex;align-items:center;gap:14px;padding:14px 0;
862
+ border-bottom:1px solid #0a3300;
863
+ }
864
+ .checkout-line-item:first-child{border-top:1px solid #0a3300;}
865
+ .checkout-item-icon{
866
+ width:42px;height:42px;border-radius:6px;background:#0a3300;
867
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
868
+ font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
869
+ }
870
+ .checkout-item-details{flex:1;min-width:0;}
871
+ .checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
872
+ .checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
873
+ .checkout-item-price{
874
+ font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
875
+ }
876
+ .checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
877
+ .checkout-totals{margin-top:20px;}
878
+ .checkout-totals-row{
879
+ display:flex;justify-content:space-between;padding:6px 0;
880
+ font-size:.8125rem;color:#1a8c00;
881
+ }
882
+ .checkout-totals-row.total{
883
+ border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
884
+ font-size:.9375rem;font-weight:600;color:#33ff00;
885
+ }
886
+ .checkout-form-section{margin-bottom:24px;}
887
+ .checkout-form-label{
888
+ font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
889
+ }
890
+ .checkout-input{
891
+ width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
892
+ background:#020;color:#33ff00;font:inherit;font-size:.875rem;
893
+ transition:border-color .15s;outline:none;
894
+ }
895
+ .checkout-input:focus{border-color:#33ff00;}
896
+ .checkout-input::placeholder{color:#116600;}
897
+ .checkout-card-box{
898
+ border:1px solid #0a3300;border-radius:6px;padding:14px;
899
+ background:#020;
900
+ }
901
+ .checkout-card-row{
902
+ display:flex;gap:12px;margin-top:10px;
903
+ }
904
+ .checkout-card-row .checkout-input{flex:1;}
905
+ .checkout-sim-note{
906
+ font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
907
+ font-style:italic;
908
+ }
909
+ .checkout-pay-btn{
910
+ width:100%;padding:14px;border:none;border-radius:8px;
911
+ background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
912
+ cursor:pointer;transition:background .15s;
913
+ font-family:'Geist Pixel',monospace;
914
+ }
915
+ .checkout-pay-btn:hover{background:#44ff22;}
916
+ .checkout-cancel{
917
+ text-align:center;margin-top:14px;
918
+ }
919
+ .checkout-cancel a{
920
+ color:#1a8c00;text-decoration:none;font-size:.8125rem;
921
+ transition:color .15s;
922
+ }
923
+ .checkout-cancel a:hover{color:#33ff00;}
924
+ @media(max-width:768px){
925
+ .checkout-layout{flex-direction:column;}
926
+ .checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
927
+ .checkout-form-side{padding:32px 20px;}
928
+ }
929
+ `;
930
+ var POWERED_BY = `<div class="powered-by">Powered by <a href="https://api-emulator.jsj.sh" target="_blank" rel="noopener">api-emulator</a></div>`;
931
+ function emuBar(service) {
932
+ const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
933
+ return `<div class="emu-bar">
934
+ <span class="emu-bar-title">${title}</span>
935
+ <nav class="emu-bar-links">
936
+ <a href="https://github.com/jsj/api-emulator/issues" target="_blank" rel="noopener"><span class="full">Report Issue</span><span class="short">Report</span></a>
937
+ <a href="https://github.com/jsj/api-emulator" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
938
+ <a href="https://api-emulator.jsj.sh" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
939
+ </nav>
940
+ </div>`;
941
+ }
942
+ function head(title) {
943
+ return `<!DOCTYPE html>
944
+ <html lang="en">
945
+ <head>
946
+ <meta charset="utf-8"/>
947
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
948
+ <link rel="icon" href="/_emulate/favicon.ico"/>
949
+ <title>${escapeHtml(title)} | api-emulator</title>
950
+ <style>${CSS}</style>
951
+ </head>`;
952
+ }
953
+ function renderCardPage(title, subtitle, body, service) {
954
+ return `${head(title)}
955
+ <body>
956
+ ${emuBar(service)}
957
+ <div class="content">
958
+ <div class="content-inner">
959
+ <div class="card-title">${escapeHtml(title)}</div>
960
+ <div class="card-subtitle">${subtitle}</div>
961
+ ${body}
962
+ </div>
963
+ </div>
964
+ ${POWERED_BY}
965
+ </body></html>`;
966
+ }
967
+ function renderErrorPage(title, message, service) {
968
+ return `${head(title)}
969
+ <body>
970
+ ${emuBar(service)}
971
+ <div class="content">
972
+ <div class="content-inner error-card">
973
+ <div class="error-title">${escapeHtml(title)}</div>
974
+ <div class="error-msg">${escapeHtml(message)}</div>
975
+ </div>
976
+ </div>
977
+ ${POWERED_BY}
978
+ </body></html>`;
979
+ }
980
+ function renderSettingsPage(title, sidebarHtml, bodyHtml, service) {
981
+ return `${head(title)}
982
+ <body>
983
+ ${emuBar(service)}
984
+ <div class="settings-layout">
985
+ <nav class="settings-sidebar">${sidebarHtml}</nav>
986
+ <div class="settings-main">${bodyHtml}</div>
987
+ </div>
988
+ ${POWERED_BY}
989
+ </body></html>`;
990
+ }
991
+ function renderInspectorPage(title, tabs, activeTab, body, service) {
992
+ const tabLinks = tabs.map(
993
+ (t) => `<a href="${escapeAttr(t.href)}" class="${t.id === activeTab ? "active" : ""}">${escapeHtml(t.label)}</a>`
994
+ ).join("");
995
+ return `${head(title)}
996
+ <body>
997
+ ${emuBar(service)}
998
+ <div class="inspector-layout">
999
+ <nav class="inspector-tabs">${tabLinks}</nav>
1000
+ ${body}
1001
+ </div>
1002
+ ${POWERED_BY}
1003
+ </body></html>`;
1004
+ }
1005
+ function renderFormPostPage(action, fields, service) {
1006
+ const hiddens = Object.entries(fields).filter(([, v]) => v != null).map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}"/>`).join("\n");
1007
+ return `${head("Redirecting")}
1008
+ <body onload="document.forms[0].submit()">
1009
+ ${emuBar(service)}
1010
+ <div class="content">
1011
+ <div class="content-inner" style="text-align:center">
1012
+ <div class="card-subtitle">Redirecting&hellip;</div>
1013
+ <form method="POST" action="${escapeAttr(action)}">
1014
+ ${hiddens}
1015
+ <noscript><button type="submit" class="user-btn" style="margin-top:12px;justify-content:center">
1016
+ <span class="user-login">Continue</span>
1017
+ </button></noscript>
1018
+ </form>
1019
+ </div>
1020
+ </div>
1021
+ ${POWERED_BY}
1022
+ </body></html>`;
1023
+ }
1024
+ function renderCheckoutPage(opts, service) {
1025
+ const fmt = (cents, cur) => `$${(cents / 100).toFixed(2)} ${cur.toUpperCase()}`;
1026
+ const fmtShort = (cents) => `$${(cents / 100).toFixed(2)}`;
1027
+ const itemsHtml = opts.lineItems.length > 0 ? opts.lineItems.map((li) => {
1028
+ const initial = li.name.charAt(0).toUpperCase();
1029
+ const unitNote = li.quantity > 1 ? `<div class="checkout-item-unit">${fmtShort(li.unitPrice)} each</div>` : "";
1030
+ return `<div class="checkout-line-item">
1031
+ <div class="checkout-item-icon">${escapeHtml(initial)}</div>
1032
+ <div class="checkout-item-details">
1033
+ <div class="checkout-item-name">${escapeHtml(li.name)}</div>
1034
+ <div class="checkout-item-qty">Qty ${li.quantity}</div>
1035
+ </div>
1036
+ <div>
1037
+ <div class="checkout-item-price">${fmtShort(li.totalPrice)}</div>
1038
+ ${unitNote}
1039
+ </div>
1040
+ </div>`;
1041
+ }).join("") : '<p class="empty">No line items</p>';
1042
+ const totalsHtml = `<div class="checkout-totals">
1043
+ <div class="checkout-totals-row">
1044
+ <span>Subtotal</span><span>${fmtShort(opts.subtotal)}</span>
1045
+ </div>
1046
+ <div class="checkout-totals-row total">
1047
+ <span>Total due</span><span>${fmt(opts.total, opts.currency)}</span>
1048
+ </div>
1049
+ </div>`;
1050
+ const cancelHtml = opts.cancelUrl ? `<div class="checkout-cancel"><a href="${escapeAttr(opts.cancelUrl)}">Cancel</a></div>` : "";
1051
+ const merchant = opts.merchantName ? escapeHtml(opts.merchantName) : "Checkout";
1052
+ return `${head("Checkout")}
1053
+ <body>
1054
+ ${emuBar(service)}
1055
+ <div class="checkout-layout">
1056
+ <div class="checkout-summary">
1057
+ <div class="checkout-merchant">
1058
+ <span class="checkout-merchant-name">${merchant}</span>
1059
+ <span class="checkout-test-badge">Test Mode</span>
1060
+ </div>
1061
+ <div class="checkout-total">${fmtShort(opts.total)}</div>
1062
+ ${itemsHtml}
1063
+ ${totalsHtml}
1064
+ </div>
1065
+ <div class="checkout-form-side">
1066
+ <form method="post" action="/checkout/${escapeAttr(opts.sessionId)}/complete">
1067
+ <div class="checkout-form-section">
1068
+ <label class="checkout-form-label">Email</label>
1069
+ <input type="email" name="email" class="checkout-input" placeholder="you@example.com"/>
1070
+ </div>
1071
+ <div class="checkout-form-section">
1072
+ <label class="checkout-form-label">Card information</label>
1073
+ <div class="checkout-card-box">
1074
+ <input type="text" class="checkout-input" placeholder="1234 1234 1234 1234" disabled/>
1075
+ <div class="checkout-card-row">
1076
+ <input type="text" class="checkout-input" placeholder="MM / YY" disabled/>
1077
+ <input type="text" class="checkout-input" placeholder="CVC" disabled/>
1078
+ </div>
1079
+ </div>
1080
+ <div class="checkout-sim-note">Card fields are simulated. Payment will be auto-approved.</div>
1081
+ </div>
1082
+ <button type="submit" class="checkout-pay-btn">Pay ${fmtShort(opts.total)}</button>
1083
+ </form>
1084
+ ${cancelHtml}
1085
+ </div>
1086
+ </div>
1087
+ ${POWERED_BY}
1088
+ </body></html>`;
1089
+ }
1090
+ function renderUserButton(opts) {
1091
+ const hiddens = Object.entries(opts.hiddenFields).map(([k, v]) => `<input type="hidden" name="${escapeAttr(k)}" value="${escapeAttr(v)}"/>`).join("");
1092
+ const nameLine = opts.name ? `<div class="user-meta">${escapeHtml(opts.name)}</div>` : "";
1093
+ const emailLine = opts.email ? `<div class="user-email">${escapeHtml(opts.email)}</div>` : "";
1094
+ return `<form class="user-form" method="post" action="${escapeAttr(opts.formAction)}">
1095
+ ${hiddens}
1096
+ <button type="submit" class="user-btn">
1097
+ <span class="avatar">${escapeHtml(opts.letter)}</span>
1098
+ <span class="user-text">
1099
+ <span class="user-login">${escapeHtml(opts.login)}</span>
1100
+ ${nameLine}${emailLine}
1101
+ </span>
1102
+ </button>
1103
+ </form>`;
1104
+ }
1105
+
1106
+ // src/oauth-helpers.ts
1107
+ import { timingSafeEqual } from "crypto";
1108
+ function normalizeUri(uri) {
1109
+ try {
1110
+ const u = new URL(uri);
1111
+ return `${u.origin}${u.pathname.replace(/\/+$/, "")}`;
1112
+ } catch {
1113
+ return uri.replace(/\/+$/, "").split("?")[0];
1114
+ }
1115
+ }
1116
+ function matchesRedirectUri(incoming, registered) {
1117
+ const normalized = normalizeUri(incoming);
1118
+ return registered.some((r) => normalizeUri(r) === normalized);
1119
+ }
1120
+ function constantTimeSecretEqual(a, b) {
1121
+ const bufA = Buffer.from(a, "utf-8");
1122
+ const bufB = Buffer.from(b, "utf-8");
1123
+ if (bufA.length !== bufB.length) return false;
1124
+ return timingSafeEqual(bufA, bufB);
1125
+ }
1126
+ function bodyStr(v) {
1127
+ if (typeof v === "string") return v;
1128
+ if (Array.isArray(v) && typeof v[0] === "string") return v[0];
1129
+ return "";
1130
+ }
1131
+ function parseCookies(header) {
1132
+ const cookies = {};
1133
+ for (const part of header.split(";")) {
1134
+ const [k, ...v] = part.split("=");
1135
+ if (k) cookies[k.trim()] = v.join("=").trim();
1136
+ }
1137
+ return cookies;
1138
+ }
1139
+
1140
+ // src/persistence.ts
1141
+ import { readFile, writeFile, mkdir } from "fs/promises";
1142
+ import { dirname as dirname2 } from "path";
1143
+ function filePersistence(path) {
1144
+ return {
1145
+ async load() {
1146
+ try {
1147
+ return await readFile(path, "utf-8");
1148
+ } catch {
1149
+ return null;
1150
+ }
1151
+ },
1152
+ async save(data) {
1153
+ await mkdir(dirname2(path), { recursive: true });
1154
+ await writeFile(path, data, "utf-8");
1155
+ }
1156
+ };
1157
+ }
1158
+ export {
1159
+ ApiError,
1160
+ Collection,
1161
+ Store,
1162
+ WebhookDispatcher,
1163
+ authMiddleware,
1164
+ bodyStr,
1165
+ constantTimeSecretEqual,
1166
+ createApiErrorHandler,
1167
+ createErrorHandler,
1168
+ createServer,
1169
+ createStoreFixture,
1170
+ debug,
1171
+ deserializeValue,
1172
+ errorHandler,
1173
+ escapeAttr,
1174
+ escapeHtml,
1175
+ filePersistence,
1176
+ fixtureStoreSnapshot,
1177
+ forbidden,
1178
+ isStoreFixture,
1179
+ matchesRedirectUri,
1180
+ normalizeUri,
1181
+ notFound,
1182
+ parseCookies,
1183
+ parseJsonBody,
1184
+ parsePagination,
1185
+ registerFontRoutes,
1186
+ renderCardPage,
1187
+ renderCheckoutPage,
1188
+ renderErrorPage,
1189
+ renderFormPostPage,
1190
+ renderInspectorPage,
1191
+ renderSettingsPage,
1192
+ renderUserButton,
1193
+ requireAppAuth,
1194
+ requireAuth,
1195
+ restoreTokenMap,
1196
+ serializeTokenMap,
1197
+ serializeValue,
1198
+ setLinkHeader,
1199
+ unauthorized,
1200
+ validationError
1201
+ };
1202
+ //# sourceMappingURL=index.js.map