@executor-js/emulate 0.6.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.
Files changed (48) hide show
  1. package/README.md +1044 -0
  2. package/dist/api.d.ts +24 -0
  3. package/dist/api.js +2665 -0
  4. package/dist/api.js.map +1 -0
  5. package/dist/chunk-D6EKRYGP.js +1615 -0
  6. package/dist/chunk-D6EKRYGP.js.map +1 -0
  7. package/dist/chunk-WVQMFHQM.js +83 -0
  8. package/dist/chunk-WVQMFHQM.js.map +1 -0
  9. package/dist/dist-7FDUSG5I.js +24368 -0
  10. package/dist/dist-7FDUSG5I.js.map +1 -0
  11. package/dist/dist-7N4COJHK.js +1814 -0
  12. package/dist/dist-7N4COJHK.js.map +1 -0
  13. package/dist/dist-BTEY33DJ.js +2334 -0
  14. package/dist/dist-BTEY33DJ.js.map +1 -0
  15. package/dist/dist-DK26ESP2.js +595 -0
  16. package/dist/dist-DK26ESP2.js.map +1 -0
  17. package/dist/dist-IYZPDKJW.js +1284 -0
  18. package/dist/dist-IYZPDKJW.js.map +1 -0
  19. package/dist/dist-JJ2ZRCAX.js +189 -0
  20. package/dist/dist-JJ2ZRCAX.js.map +1 -0
  21. package/dist/dist-K4CVTD6K.js +1570 -0
  22. package/dist/dist-K4CVTD6K.js.map +1 -0
  23. package/dist/dist-M3GVASMR.js +1254 -0
  24. package/dist/dist-M3GVASMR.js.map +1 -0
  25. package/dist/dist-OYYGWKZQ.js +1533 -0
  26. package/dist/dist-OYYGWKZQ.js.map +1 -0
  27. package/dist/dist-P3SBBRFR.js +3169 -0
  28. package/dist/dist-P3SBBRFR.js.map +1 -0
  29. package/dist/dist-RMPDKZUA.js +1183 -0
  30. package/dist/dist-RMPDKZUA.js.map +1 -0
  31. package/dist/dist-WBKONLOE.js +2154 -0
  32. package/dist/dist-WBKONLOE.js.map +1 -0
  33. package/dist/dist-XM5HSBDC.js +1090 -0
  34. package/dist/dist-XM5HSBDC.js.map +1 -0
  35. package/dist/dist-XVVIYXQG.js +4241 -0
  36. package/dist/dist-XVVIYXQG.js.map +1 -0
  37. package/dist/dist-YPRJYQHW.js +5109 -0
  38. package/dist/dist-YPRJYQHW.js.map +1 -0
  39. package/dist/dist-ZEC77OKZ.js +913 -0
  40. package/dist/dist-ZEC77OKZ.js.map +1 -0
  41. package/dist/fonts/GeistPixel-Square.woff2 +0 -0
  42. package/dist/fonts/favicon.ico +0 -0
  43. package/dist/fonts/geist-sans.woff2 +0 -0
  44. package/dist/helpers-LXLP3DFE-LBOTATT5.js +17 -0
  45. package/dist/helpers-LXLP3DFE-LBOTATT5.js.map +1 -0
  46. package/dist/index.js +3005 -0
  47. package/dist/index.js.map +1 -0
  48. package/package.json +83 -0
package/dist/index.js ADDED
@@ -0,0 +1,3005 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ importPKCS8,
4
+ jwtVerify
5
+ } from "./chunk-D6EKRYGP.js";
6
+
7
+ // src/index.ts
8
+ import { Command } from "commander";
9
+
10
+ // ../@emulators/core/dist/index.js
11
+ import { createServer as createNodeServer } from "http";
12
+ import { createHmac } from "crypto";
13
+ import { readFileSync } from "fs";
14
+ import { fileURLToPath } from "url";
15
+ import { dirname, join } from "path";
16
+ function serializeValue(value) {
17
+ if (value instanceof Map) {
18
+ return { __type: "Map", entries: [...value.entries()].map(([k, v]) => [k, serializeValue(v)]) };
19
+ }
20
+ if (value instanceof Set) {
21
+ return { __type: "Set", values: [...value.values()] };
22
+ }
23
+ return value;
24
+ }
25
+ function deserializeValue(value) {
26
+ if (value !== null && typeof value === "object" && "__type" in value) {
27
+ const tagged = value;
28
+ if (tagged.__type === "Map") {
29
+ const entries = tagged.entries;
30
+ return new Map(entries.map(([k, v]) => [k, deserializeValue(v)]));
31
+ }
32
+ if (tagged.__type === "Set") {
33
+ return new Set(tagged.values);
34
+ }
35
+ }
36
+ return value;
37
+ }
38
+ var Collection = class {
39
+ constructor(indexFields = []) {
40
+ this.indexFields = indexFields;
41
+ this.fieldNames = indexFields.map(String).sort();
42
+ for (const field of indexFields) {
43
+ this.indexes.set(String(field), /* @__PURE__ */ new Map());
44
+ }
45
+ }
46
+ items = /* @__PURE__ */ new Map();
47
+ indexes = /* @__PURE__ */ new Map();
48
+ autoId = 1;
49
+ fieldNames;
50
+ addToIndex(item) {
51
+ for (const field of this.indexFields) {
52
+ const value = item[field];
53
+ if (value === void 0 || value === null) continue;
54
+ const indexMap = this.indexes.get(String(field));
55
+ const key = String(value);
56
+ if (!indexMap.has(key)) {
57
+ indexMap.set(key, /* @__PURE__ */ new Set());
58
+ }
59
+ indexMap.get(key).add(item.id);
60
+ }
61
+ }
62
+ removeFromIndex(item) {
63
+ for (const field of this.indexFields) {
64
+ const value = item[field];
65
+ if (value === void 0 || value === null) continue;
66
+ const indexMap = this.indexes.get(String(field));
67
+ const key = String(value);
68
+ indexMap.get(key)?.delete(item.id);
69
+ }
70
+ }
71
+ insert(data) {
72
+ const now = (/* @__PURE__ */ new Date()).toISOString();
73
+ const explicitId = data.id != null && data.id > 0 ? data.id : void 0;
74
+ const id = explicitId ?? this.autoId++;
75
+ if (id >= this.autoId) {
76
+ this.autoId = id + 1;
77
+ }
78
+ const item = {
79
+ ...data,
80
+ id,
81
+ created_at: now,
82
+ updated_at: now
83
+ };
84
+ this.items.set(id, item);
85
+ this.addToIndex(item);
86
+ return item;
87
+ }
88
+ get(id) {
89
+ return this.items.get(id);
90
+ }
91
+ findBy(field, value) {
92
+ if (this.indexes.has(String(field))) {
93
+ const ids = this.indexes.get(String(field)).get(String(value));
94
+ if (!ids) return [];
95
+ return Array.from(ids).map((id) => this.items.get(id)).filter(Boolean);
96
+ }
97
+ return this.all().filter((item) => item[field] === value);
98
+ }
99
+ findOneBy(field, value) {
100
+ return this.findBy(field, value)[0];
101
+ }
102
+ update(id, data) {
103
+ const existing = this.items.get(id);
104
+ if (!existing) return void 0;
105
+ this.removeFromIndex(existing);
106
+ const updated = {
107
+ ...existing,
108
+ ...data,
109
+ id,
110
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
111
+ };
112
+ this.items.set(id, updated);
113
+ this.addToIndex(updated);
114
+ return updated;
115
+ }
116
+ delete(id) {
117
+ const existing = this.items.get(id);
118
+ if (!existing) return false;
119
+ this.removeFromIndex(existing);
120
+ return this.items.delete(id);
121
+ }
122
+ all() {
123
+ return Array.from(this.items.values());
124
+ }
125
+ query(options = {}) {
126
+ let results = this.all();
127
+ if (options.filter) {
128
+ results = results.filter(options.filter);
129
+ }
130
+ const total_count = results.length;
131
+ if (options.sort) {
132
+ results.sort(options.sort);
133
+ }
134
+ const page = options.page ?? 1;
135
+ const per_page = Math.min(options.per_page ?? 30, 100);
136
+ const start = (page - 1) * per_page;
137
+ const paged = results.slice(start, start + per_page);
138
+ return {
139
+ items: paged,
140
+ total_count,
141
+ page,
142
+ per_page,
143
+ has_next: start + per_page < total_count,
144
+ has_prev: page > 1
145
+ };
146
+ }
147
+ count(filter) {
148
+ if (!filter) return this.items.size;
149
+ return this.all().filter(filter).length;
150
+ }
151
+ clear() {
152
+ this.items.clear();
153
+ for (const indexMap of this.indexes.values()) {
154
+ indexMap.clear();
155
+ }
156
+ this.autoId = 1;
157
+ }
158
+ snapshot() {
159
+ return {
160
+ items: this.all(),
161
+ autoId: this.autoId,
162
+ indexFields: this.fieldNames
163
+ };
164
+ }
165
+ restore(snap) {
166
+ this.clear();
167
+ this.autoId = snap.autoId;
168
+ for (const item of snap.items) {
169
+ this.items.set(item.id, item);
170
+ this.addToIndex(item);
171
+ }
172
+ }
173
+ };
174
+ var Store = class {
175
+ collections = /* @__PURE__ */ new Map();
176
+ _data = /* @__PURE__ */ new Map();
177
+ collection(name, indexFields = []) {
178
+ const existing = this.collections.get(name);
179
+ if (existing) {
180
+ if (indexFields.length > 0) {
181
+ const requested = indexFields.map(String).sort();
182
+ if (existing.fieldNames.length !== requested.length || existing.fieldNames.some((f, i) => f !== requested[i])) {
183
+ throw new Error(
184
+ `Collection "${name}" already exists with indexes [${existing.fieldNames}] but was requested with [${requested}]`
185
+ );
186
+ }
187
+ }
188
+ return existing;
189
+ }
190
+ const col = new Collection(indexFields);
191
+ this.collections.set(name, col);
192
+ return col;
193
+ }
194
+ getData(key) {
195
+ return this._data.get(key);
196
+ }
197
+ setData(key, value) {
198
+ this._data.set(key, value);
199
+ }
200
+ reset() {
201
+ for (const collection of this.collections.values()) {
202
+ collection.clear();
203
+ }
204
+ this._data.clear();
205
+ }
206
+ snapshot() {
207
+ const collections = {};
208
+ for (const [name, col] of this.collections) {
209
+ collections[name] = col.snapshot();
210
+ }
211
+ const data = {};
212
+ for (const [key, value] of this._data) {
213
+ data[key] = serializeValue(value);
214
+ }
215
+ return { collections, data };
216
+ }
217
+ restore(snap) {
218
+ const snapshotNames = new Set(Object.keys(snap.collections));
219
+ for (const name of this.collections.keys()) {
220
+ if (!snapshotNames.has(name)) {
221
+ this.collections.delete(name);
222
+ }
223
+ }
224
+ for (const [name, colSnap] of Object.entries(snap.collections)) {
225
+ const indexFields = colSnap.indexFields;
226
+ const col = this.collection(name, indexFields);
227
+ col.restore(colSnap);
228
+ }
229
+ this._data.clear();
230
+ for (const [key, value] of Object.entries(snap.data)) {
231
+ this._data.set(key, deserializeValue(value));
232
+ }
233
+ }
234
+ };
235
+ var HonoRequest = class {
236
+ constructor(request, params, routePath) {
237
+ this.params = params;
238
+ this.raw = request;
239
+ this.url = request.url;
240
+ this.method = request.method;
241
+ this.path = new URL(request.url).pathname;
242
+ this.routePath = routePath;
243
+ }
244
+ raw;
245
+ url;
246
+ method;
247
+ path;
248
+ /** The matched route pattern (e.g. /repos/:owner/:repo), when a route matched. */
249
+ routePath;
250
+ header(name) {
251
+ if (name) return this.raw.headers.get(name) ?? void 0;
252
+ const headers = {};
253
+ this.raw.headers.forEach((value, key) => {
254
+ headers[key] = value;
255
+ });
256
+ return headers;
257
+ }
258
+ query(name) {
259
+ return new URL(this.url).searchParams.get(name) ?? void 0;
260
+ }
261
+ queries(name) {
262
+ const values = new URL(this.url).searchParams.getAll(name);
263
+ return values.length > 0 ? values : void 0;
264
+ }
265
+ param(name) {
266
+ if (!name) return { ...this.params };
267
+ return this.params[name] ?? "";
268
+ }
269
+ json() {
270
+ return this.raw.json();
271
+ }
272
+ text() {
273
+ return this.raw.text();
274
+ }
275
+ arrayBuffer() {
276
+ return this.raw.arrayBuffer();
277
+ }
278
+ async parseBody() {
279
+ const contentType = this.header("Content-Type") ?? "";
280
+ if (contentType.includes("multipart/form-data")) {
281
+ return formDataToObject(await this.raw.formData());
282
+ }
283
+ if (contentType.includes("application/x-www-form-urlencoded")) {
284
+ const params = new URLSearchParams(await this.raw.text());
285
+ const out = {};
286
+ for (const [key, value] of params) {
287
+ appendBodyValue(out, key, value);
288
+ }
289
+ return out;
290
+ }
291
+ if (contentType.includes("application/json")) {
292
+ const body = await this.raw.json().catch(() => ({}));
293
+ return body && typeof body === "object" && !Array.isArray(body) ? body : {};
294
+ }
295
+ return {};
296
+ }
297
+ };
298
+ var Context = class {
299
+ constructor(request, params, notFoundHandler, routePath) {
300
+ this.notFoundHandler = notFoundHandler;
301
+ this.req = new HonoRequest(request, params, routePath);
302
+ }
303
+ req;
304
+ vars = /* @__PURE__ */ new Map();
305
+ responseHeaders = new Headers();
306
+ responseStatus = 200;
307
+ get(key) {
308
+ return this.vars.get(key);
309
+ }
310
+ set(key, value) {
311
+ this.vars.set(key, value);
312
+ }
313
+ header(name, value) {
314
+ this.responseHeaders.set(name, value);
315
+ }
316
+ status(status) {
317
+ this.responseStatus = status;
318
+ }
319
+ json(data, status, headers) {
320
+ return this.response(JSON.stringify(data), status, defaultContentType(headers, "application/json; charset=UTF-8"));
321
+ }
322
+ text(text, status, headers) {
323
+ return this.response(text, status, defaultContentType(headers, "text/plain; charset=UTF-8"));
324
+ }
325
+ html(html, status, headers) {
326
+ return this.response(html, status, defaultContentType(headers, "text/html; charset=UTF-8"));
327
+ }
328
+ body(body, status, headers) {
329
+ return this.response(body, status, headers);
330
+ }
331
+ redirect(location, status = 302) {
332
+ return this.response(null, status, { Location: location });
333
+ }
334
+ notFound() {
335
+ return this.notFoundHandler(this);
336
+ }
337
+ finalize(response) {
338
+ if (!hasHeaders(this.responseHeaders)) return response;
339
+ const headers = new Headers(response.headers);
340
+ this.responseHeaders.forEach((value, key) => {
341
+ headers.set(key, value);
342
+ });
343
+ return new Response(response.body, {
344
+ status: response.status,
345
+ statusText: response.statusText,
346
+ headers
347
+ });
348
+ }
349
+ response(body, status, headers) {
350
+ const merged = new Headers(headers);
351
+ this.responseHeaders.forEach((value, key) => {
352
+ merged.set(key, value);
353
+ });
354
+ return new Response(body, {
355
+ status: status ?? this.responseStatus,
356
+ headers: merged
357
+ });
358
+ }
359
+ };
360
+ var Hono = class {
361
+ middleware = [];
362
+ routes = [];
363
+ errorHandler = (err) => {
364
+ const message = err instanceof Error ? err.message : "Internal Server Error";
365
+ return new Response(message, { status: 500 });
366
+ };
367
+ notFoundHandler = () => new Response("404 Not Found", { status: 404 });
368
+ use(pathOrHandler, ...handlers) {
369
+ if (typeof pathOrHandler === "string") {
370
+ this.middleware.push({ method: "ALL", compiled: compilePath(pathOrHandler), handlers });
371
+ } else {
372
+ this.middleware.push({ method: "ALL", compiled: compilePath("*"), handlers: [pathOrHandler, ...handlers] });
373
+ }
374
+ return this;
375
+ }
376
+ on(method, path, ...handlers) {
377
+ this.routes.push({ method: method.toUpperCase(), compiled: compilePath(path), handlers });
378
+ return this;
379
+ }
380
+ get(path, ...handlers) {
381
+ return this.on("GET", path, ...handlers);
382
+ }
383
+ post(path, ...handlers) {
384
+ return this.on("POST", path, ...handlers);
385
+ }
386
+ put(path, ...handlers) {
387
+ return this.on("PUT", path, ...handlers);
388
+ }
389
+ patch(path, ...handlers) {
390
+ return this.on("PATCH", path, ...handlers);
391
+ }
392
+ delete(path, ...handlers) {
393
+ return this.on("DELETE", path, ...handlers);
394
+ }
395
+ onError(handler) {
396
+ this.errorHandler = handler;
397
+ return this;
398
+ }
399
+ notFound(handler) {
400
+ this.notFoundHandler = handler;
401
+ return this;
402
+ }
403
+ async request(input, init) {
404
+ if (input instanceof Request) return this.fetch(input);
405
+ const url = input.startsWith("/") ? `http://localhost${input}` : input;
406
+ return this.fetch(new Request(url, init));
407
+ }
408
+ fetch = async (request) => {
409
+ const url = new URL(request.url);
410
+ const path = url.pathname;
411
+ const method = request.method.toUpperCase();
412
+ const matched = this.match(method, path);
413
+ const context = new Context(request, matched.params, this.notFoundHandler, matched.routePattern);
414
+ try {
415
+ const response = await this.dispatch(context, matched.handlers);
416
+ return context.finalize(response ?? await this.notFoundHandler(context));
417
+ } catch (err) {
418
+ return context.finalize(await this.errorHandler(err, context));
419
+ }
420
+ };
421
+ match(method, path) {
422
+ const handlers = [];
423
+ const params = {};
424
+ for (const route2 of this.middleware) {
425
+ const match = matchPath(route2.compiled, path);
426
+ if (!match) continue;
427
+ Object.assign(params, match);
428
+ for (const handler of route2.handlers) {
429
+ handlers.push({ handler, params: match });
430
+ }
431
+ }
432
+ const route = this.routes.find((candidate) => candidate.method === method && matchPath(candidate.compiled, path) != null) ?? (method === "HEAD" ? this.routes.find((candidate) => candidate.method === "GET" && matchPath(candidate.compiled, path) != null) : void 0);
433
+ if (route) {
434
+ const match = matchPath(route.compiled, path) ?? {};
435
+ Object.assign(params, match);
436
+ for (const handler of route.handlers) {
437
+ handlers.push({ handler, params: match });
438
+ }
439
+ }
440
+ return { handlers, params, routePattern: route?.compiled.pattern };
441
+ }
442
+ async dispatch(context, handlers) {
443
+ let index = -1;
444
+ const run = async (nextIndex) => {
445
+ if (nextIndex <= index) throw new Error("next() called multiple times");
446
+ index = nextIndex;
447
+ const matched = handlers[nextIndex];
448
+ if (!matched) return void 0;
449
+ const originalParams = context.req.param();
450
+ Object.assign(originalParams, matched.params);
451
+ let nextResponse = void 0;
452
+ let nextCalled = false;
453
+ const next = async () => {
454
+ nextCalled = true;
455
+ nextResponse = await run(nextIndex + 1);
456
+ return nextResponse;
457
+ };
458
+ const response = await matched.handler(context, next);
459
+ if (response instanceof Response) return response;
460
+ if (nextCalled) return nextResponse;
461
+ return response;
462
+ };
463
+ return run(0);
464
+ }
465
+ };
466
+ function cors(options = {}) {
467
+ const origin = options.origin ?? "*";
468
+ const allowMethods = options.allowMethods ?? ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH", "OPTIONS"];
469
+ return async (c, next) => {
470
+ c.header("Access-Control-Allow-Origin", origin);
471
+ if (options.credentials) c.header("Access-Control-Allow-Credentials", "true");
472
+ if (c.req.method.toUpperCase() === "OPTIONS") {
473
+ c.header("Access-Control-Allow-Methods", allowMethods.join(","));
474
+ const allowHeaders = options.allowHeaders?.join(",") ?? c.req.header("Access-Control-Request-Headers");
475
+ if (allowHeaders) c.header("Access-Control-Allow-Headers", allowHeaders);
476
+ if (options.maxAge != null) c.header("Access-Control-Max-Age", String(options.maxAge));
477
+ return c.body(null, 204);
478
+ }
479
+ await next();
480
+ };
481
+ }
482
+ function serve(options) {
483
+ const port = options.port ?? 3e3;
484
+ const server = createNodeServer(async (req, res) => {
485
+ try {
486
+ const request = nodeRequestToFetchRequest(req);
487
+ const response = await options.fetch(request);
488
+ await writeFetchResponse(res, response, req.method?.toUpperCase() === "HEAD");
489
+ } catch (err) {
490
+ const message = err instanceof Error ? err.message : "Internal Server Error";
491
+ res.statusCode = 500;
492
+ res.setHeader("Content-Type", "text/plain; charset=UTF-8");
493
+ res.end(message);
494
+ }
495
+ });
496
+ server.listen(port, options.hostname);
497
+ return server;
498
+ }
499
+ function compilePath(pattern) {
500
+ if (pattern === "*" || pattern === "/*") {
501
+ return { pattern, regex: /^.*$/, paramNames: [] };
502
+ }
503
+ const paramNames = [];
504
+ let source = "^";
505
+ for (let i = 0; i < pattern.length; i++) {
506
+ const char = pattern[i];
507
+ if (char !== ":") {
508
+ source += escapeRegex(char);
509
+ continue;
510
+ }
511
+ let name = "";
512
+ i++;
513
+ while (i < pattern.length && /[A-Za-z0-9_]/.test(pattern[i])) {
514
+ name += pattern[i];
515
+ i++;
516
+ }
517
+ i--;
518
+ paramNames.push(name);
519
+ if (pattern[i + 1] === "{") {
520
+ const close = pattern.indexOf("}", i + 2);
521
+ if (close < 0) throw new Error(`Invalid route pattern: ${pattern}`);
522
+ const expr = pattern.slice(i + 2, close);
523
+ source += `(${expr})`;
524
+ i = close;
525
+ } else {
526
+ source += "([^/]+)";
527
+ }
528
+ }
529
+ source += "$";
530
+ return { pattern, regex: new RegExp(source), paramNames };
531
+ }
532
+ function matchPath(compiled, path) {
533
+ const match = compiled.regex.exec(path);
534
+ if (!match) return null;
535
+ const params = {};
536
+ for (let i = 0; i < compiled.paramNames.length; i++) {
537
+ params[compiled.paramNames[i]] = decodePathParam(match[i + 1] ?? "");
538
+ }
539
+ return params;
540
+ }
541
+ function decodePathParam(value) {
542
+ try {
543
+ return decodeURIComponent(value);
544
+ } catch {
545
+ return value;
546
+ }
547
+ }
548
+ function escapeRegex(value) {
549
+ return value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
550
+ }
551
+ function hasHeaders(headers) {
552
+ for (const _ of headers) return true;
553
+ return false;
554
+ }
555
+ function defaultContentType(headers, contentType) {
556
+ const out = new Headers(headers);
557
+ if (!out.has("Content-Type")) {
558
+ out.set("Content-Type", contentType);
559
+ }
560
+ return out;
561
+ }
562
+ function formDataToObject(formData) {
563
+ const out = {};
564
+ for (const [key, value] of formData) {
565
+ appendBodyValue(out, key, value);
566
+ }
567
+ return out;
568
+ }
569
+ function appendBodyValue(target, key, value) {
570
+ const existing = target[key];
571
+ if (existing === void 0) {
572
+ target[key] = value;
573
+ } else if (Array.isArray(existing)) {
574
+ existing.push(value);
575
+ } else {
576
+ target[key] = [existing, value];
577
+ }
578
+ }
579
+ function nodeRequestToFetchRequest(req) {
580
+ const host = req.headers.host ?? "localhost";
581
+ const url = new URL(req.url ?? "/", `http://${host}`);
582
+ const headers = new Headers();
583
+ for (const [key, value] of Object.entries(req.headers)) {
584
+ if (value == null) continue;
585
+ if (Array.isArray(value)) {
586
+ for (const item of value) headers.append(key, item);
587
+ } else {
588
+ headers.set(key, value);
589
+ }
590
+ }
591
+ const method = req.method ?? "GET";
592
+ const hasBody = method !== "GET" && method !== "HEAD";
593
+ return new Request(url.toString(), {
594
+ method,
595
+ headers,
596
+ body: hasBody ? req : void 0,
597
+ duplex: "half"
598
+ });
599
+ }
600
+ async function writeFetchResponse(res, response, headOnly) {
601
+ res.statusCode = response.status;
602
+ res.statusMessage = response.statusText;
603
+ const headersWithCookies = response.headers;
604
+ const cookies = headersWithCookies.getSetCookie?.();
605
+ response.headers.forEach((value, key) => {
606
+ if (key.toLowerCase() === "set-cookie" && cookies && cookies.length > 0) return;
607
+ res.setHeader(key, value);
608
+ });
609
+ if (cookies && cookies.length > 0) {
610
+ res.setHeader("Set-Cookie", cookies);
611
+ }
612
+ if (headOnly || !response.body) {
613
+ res.end();
614
+ return;
615
+ }
616
+ const reader = response.body.getReader();
617
+ try {
618
+ while (true) {
619
+ const { done, value } = await reader.read();
620
+ if (done) break;
621
+ if (!res.write(value)) {
622
+ await new Promise((resolve3) => res.once("drain", resolve3));
623
+ }
624
+ }
625
+ res.end();
626
+ } catch (err) {
627
+ res.destroy(err instanceof Error ? err : void 0);
628
+ }
629
+ }
630
+ var MAX_DELIVERIES = 1e3;
631
+ var WebhookDispatcher = class {
632
+ subscriptions = [];
633
+ deliveries = [];
634
+ subscriptionIdCounter = 1;
635
+ deliveryIdCounter = 1;
636
+ register(sub) {
637
+ const { id: explicitId, ...rest } = sub;
638
+ const id = explicitId !== void 0 ? explicitId : this.subscriptionIdCounter++;
639
+ if (id >= this.subscriptionIdCounter) {
640
+ this.subscriptionIdCounter = id + 1;
641
+ }
642
+ const subscription = { ...rest, id };
643
+ this.subscriptions.push(subscription);
644
+ return subscription;
645
+ }
646
+ unregister(id) {
647
+ const idx = this.subscriptions.findIndex((s) => s.id === id);
648
+ if (idx === -1) return false;
649
+ this.subscriptions.splice(idx, 1);
650
+ return true;
651
+ }
652
+ getSubscription(id) {
653
+ return this.subscriptions.find((s) => s.id === id);
654
+ }
655
+ getSubscriptions(owner, repo) {
656
+ return this.subscriptions.filter((s) => {
657
+ if (owner && s.owner !== owner) return false;
658
+ if (repo !== void 0 && s.repo !== repo) return false;
659
+ return true;
660
+ });
661
+ }
662
+ updateSubscription(id, data) {
663
+ const sub = this.subscriptions.find((s) => s.id === id);
664
+ if (!sub) return void 0;
665
+ Object.assign(sub, data);
666
+ return sub;
667
+ }
668
+ async dispatch(event, action, payload, owner, repo) {
669
+ const matchingSubs = this.subscriptions.filter((s) => {
670
+ if (!s.active) return false;
671
+ if (s.owner !== owner) return false;
672
+ if (repo !== void 0) {
673
+ if (s.repo !== repo) return false;
674
+ } else if (s.repo !== void 0) {
675
+ return false;
676
+ }
677
+ return event === "ping" || s.events.includes("*") || s.events.includes(event);
678
+ });
679
+ for (const sub of matchingSubs) {
680
+ const delivery = {
681
+ id: this.deliveryIdCounter++,
682
+ hook_id: sub.id,
683
+ event,
684
+ action,
685
+ payload,
686
+ status_code: null,
687
+ delivered_at: (/* @__PURE__ */ new Date()).toISOString(),
688
+ duration: null,
689
+ success: false
690
+ };
691
+ const body = JSON.stringify(payload);
692
+ const signatureHeaders = {};
693
+ if (sub.secret) {
694
+ const hmac = createHmac("sha256", sub.secret).update(body).digest("hex");
695
+ signatureHeaders["X-Hub-Signature-256"] = `sha256=${hmac}`;
696
+ }
697
+ try {
698
+ const start = Date.now();
699
+ const response = await fetch(sub.url, {
700
+ method: "POST",
701
+ headers: {
702
+ "Content-Type": "application/json",
703
+ "X-GitHub-Event": event,
704
+ "X-GitHub-Delivery": String(delivery.id),
705
+ ...signatureHeaders
706
+ },
707
+ body,
708
+ signal: AbortSignal.timeout(1e4)
709
+ });
710
+ delivery.duration = Date.now() - start;
711
+ delivery.status_code = response.status;
712
+ delivery.success = response.ok;
713
+ } catch {
714
+ delivery.duration = 0;
715
+ delivery.success = false;
716
+ }
717
+ this.deliveries.push(delivery);
718
+ if (this.deliveries.length > MAX_DELIVERIES) {
719
+ this.deliveries.splice(0, this.deliveries.length - MAX_DELIVERIES);
720
+ }
721
+ }
722
+ }
723
+ getDeliveries(hookId) {
724
+ if (hookId !== void 0) {
725
+ return this.deliveries.filter((d) => d.hook_id === hookId);
726
+ }
727
+ return [...this.deliveries];
728
+ }
729
+ clear() {
730
+ this.subscriptions.length = 0;
731
+ this.deliveries.length = 0;
732
+ this.subscriptionIdCounter = 1;
733
+ this.deliveryIdCounter = 1;
734
+ }
735
+ };
736
+ var DEFAULT_DOCS_URL = "https://emulate.dev";
737
+ function getDocsUrl(c) {
738
+ return c.get("docsUrl") ?? DEFAULT_DOCS_URL;
739
+ }
740
+ function errorStatus(err) {
741
+ if (err && typeof err === "object" && "status" in err) {
742
+ const s = err.status;
743
+ if (typeof s === "number" && Number.isFinite(s)) return s;
744
+ }
745
+ return 500;
746
+ }
747
+ function createApiErrorHandler(documentationUrl) {
748
+ return (err, c) => {
749
+ if (documentationUrl) {
750
+ c.set("docsUrl", documentationUrl);
751
+ }
752
+ const status = errorStatus(err);
753
+ const message = err instanceof Error ? err.message : "Internal Server Error";
754
+ return c.json(
755
+ {
756
+ message,
757
+ documentation_url: getDocsUrl(c)
758
+ },
759
+ status
760
+ );
761
+ };
762
+ }
763
+ function createErrorHandler(documentationUrl) {
764
+ return async (c, next) => {
765
+ if (documentationUrl) {
766
+ c.set("docsUrl", documentationUrl);
767
+ }
768
+ await next();
769
+ };
770
+ }
771
+ var errorHandler = createErrorHandler();
772
+ var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
773
+ function debug(label, ...args) {
774
+ if (isDebug) {
775
+ console.log(`[${label}]`, ...args);
776
+ }
777
+ }
778
+ function authMiddleware(tokens, appKeyResolver, fallbackUser) {
779
+ return async (c, next) => {
780
+ const authHeader = c.req.header("Authorization");
781
+ if (authHeader) {
782
+ const token = authHeader.replace(/^(Bearer|token)\s+/i, "").trim();
783
+ if (token.startsWith("eyJ") && appKeyResolver) {
784
+ try {
785
+ const [, payloadB64] = token.split(".");
786
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
787
+ const appId = typeof payload.iss === "string" ? parseInt(payload.iss, 10) : payload.iss;
788
+ if (typeof appId === "number" && !isNaN(appId)) {
789
+ const appInfo = appKeyResolver(appId);
790
+ if (appInfo) {
791
+ const key = await importPKCS8(appInfo.privateKey, "RS256");
792
+ await jwtVerify(token, key, { algorithms: ["RS256"] });
793
+ c.set("authApp", {
794
+ appId,
795
+ slug: appInfo.slug,
796
+ name: appInfo.name
797
+ });
798
+ }
799
+ }
800
+ } catch {
801
+ }
802
+ } else {
803
+ let user = tokens.get(token);
804
+ if (!user && fallbackUser && token.length > 0) {
805
+ debug("auth", "fallback user for unknown token", { login: fallbackUser.login, id: fallbackUser.id });
806
+ user = { login: fallbackUser.login, id: fallbackUser.id, scopes: fallbackUser.scopes };
807
+ }
808
+ if (user) {
809
+ c.set("authUser", user);
810
+ c.set("authToken", token);
811
+ c.set("authScopes", user.scopes);
812
+ }
813
+ }
814
+ }
815
+ await next();
816
+ };
817
+ }
818
+ var assetCache = /* @__PURE__ */ new Map();
819
+ function loadAsset(name) {
820
+ if (assetCache.has(name)) return assetCache.get(name);
821
+ let buf = null;
822
+ try {
823
+ const dir = dirname(fileURLToPath(import.meta.url));
824
+ buf = readFileSync(join(dir, "fonts", name));
825
+ } catch {
826
+ buf = null;
827
+ }
828
+ assetCache.set(name, buf);
829
+ return buf;
830
+ }
831
+ var FONT_NAMES = /* @__PURE__ */ new Set(["geist-sans.woff2", "GeistPixel-Square.woff2"]);
832
+ function registerFontRoutes(app) {
833
+ app.get("/_emulate/fonts/:name", (c) => {
834
+ const name = c.req.param("name");
835
+ if (!FONT_NAMES.has(name)) return c.notFound();
836
+ const buf = loadAsset(name);
837
+ if (!buf) return c.notFound();
838
+ return new Response(buf, {
839
+ headers: {
840
+ "Content-Type": "font/woff2",
841
+ "Cache-Control": "public, max-age=31536000, immutable",
842
+ "Access-Control-Allow-Origin": "*"
843
+ }
844
+ });
845
+ });
846
+ app.get("/_emulate/favicon.ico", (c) => {
847
+ const buf = loadAsset("favicon.ico");
848
+ if (!buf) return c.notFound();
849
+ return new Response(buf, {
850
+ headers: {
851
+ "Content-Type": "image/x-icon",
852
+ "Cache-Control": "public, max-age=31536000, immutable"
853
+ }
854
+ });
855
+ });
856
+ }
857
+ var DEFAULT_MAX_ENTRIES = 1e3;
858
+ var DEFAULT_MAX_BODY_CHARS = 2e4;
859
+ var REDACTED = "[redacted]";
860
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
861
+ "authorization",
862
+ "cookie",
863
+ "set-cookie",
864
+ "x-api-key",
865
+ "x-github-token",
866
+ "stripe-signature"
867
+ ]);
868
+ var SENSITIVE_KEYS = /token|secret|password|authorization|api[_-]?key|client[_-]?secret|private[_-]?key/i;
869
+ var RequestLedger = class {
870
+ constructor(options = {}) {
871
+ this.options = options;
872
+ }
873
+ entries = [];
874
+ counter = 1;
875
+ add(entry) {
876
+ const saved = { ...entry, id: `req_${this.counter++}` };
877
+ this.entries.push(saved);
878
+ const max = this.options.maxEntries ?? DEFAULT_MAX_ENTRIES;
879
+ if (this.entries.length > max) {
880
+ this.entries.splice(0, this.entries.length - max);
881
+ }
882
+ return saved;
883
+ }
884
+ list(limit) {
885
+ const all = [...this.entries].reverse();
886
+ return limit != null ? all.slice(0, limit) : all;
887
+ }
888
+ clear() {
889
+ this.entries.length = 0;
890
+ this.counter = 1;
891
+ }
892
+ /** Serialize for durable persistence (e.g. a Cloudflare Durable Object). */
893
+ serialize() {
894
+ return { entries: [...this.entries], counter: this.counter };
895
+ }
896
+ restore(snapshot) {
897
+ if (!snapshot) return;
898
+ this.entries = Array.isArray(snapshot.entries) ? [...snapshot.entries] : [];
899
+ this.counter = typeof snapshot.counter === "number" ? snapshot.counter : this.entries.length + 1;
900
+ }
901
+ };
902
+ function correlationIdFor(headers) {
903
+ const provided = headers["x-correlation-id"] ?? headers["x-request-id"];
904
+ if (provided && provided.length <= 200) return provided;
905
+ return `cor_${crypto.randomUUID().replace(/-/g, "")}`;
906
+ }
907
+ function createLedgerMiddleware(ledger, options = {}) {
908
+ const maxBodyChars = options.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS;
909
+ const webhooks = options.webhooks;
910
+ return async (c, next) => {
911
+ if (c.req.path.startsWith("/_emulate")) {
912
+ await next();
913
+ return;
914
+ }
915
+ const started = Date.now();
916
+ const url = new URL(c.req.url);
917
+ const rawHeaders = c.req.header();
918
+ const correlationId = correlationIdFor(rawHeaders);
919
+ c.set("correlationId", correlationId);
920
+ c.set("ledgerEffects", []);
921
+ c.header("X-Correlation-Id", correlationId);
922
+ const requestBody = await readBody(c.req.raw.clone(), maxBodyChars);
923
+ const requestHeaders = redactHeaders(rawHeaders);
924
+ const beforeDeliveryIds = webhooks ? new Set(webhooks.getDeliveries().map((d) => d.id)) : void 0;
925
+ const response = await next();
926
+ if (!response) return;
927
+ const responseBody = await readBody(response.clone(), maxBodyChars);
928
+ const route = c.req.routePath;
929
+ const operationId = c.get("operationId");
930
+ const sideEffects = c.get("ledgerEffects") ?? [];
931
+ const webhookDeliveries = webhooks && beforeDeliveryIds ? webhooks.getDeliveries().filter((d) => !beforeDeliveryIds.has(d.id)).map((d) => ({
932
+ id: d.id,
933
+ hook_id: d.hook_id,
934
+ event: d.event,
935
+ action: d.action,
936
+ status_code: d.status_code,
937
+ success: d.success
938
+ })) : [];
939
+ ledger.add({
940
+ correlationId,
941
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
942
+ method: c.req.method.toUpperCase(),
943
+ host: url.host,
944
+ path: url.pathname,
945
+ query: url.search,
946
+ route,
947
+ operationId,
948
+ request: {
949
+ headers: requestHeaders,
950
+ ...requestBody
951
+ },
952
+ identity: {
953
+ user: c.get("authUser"),
954
+ app: c.get("authApp")
955
+ },
956
+ response: {
957
+ status: response.status,
958
+ headers: redactHeaders(headersToRecord(response.headers)),
959
+ ...responseBody
960
+ },
961
+ summary: `${c.req.method.toUpperCase()} ${route ?? url.pathname} -> ${response.status}`,
962
+ sideEffects,
963
+ webhookDeliveries,
964
+ durationMs: Date.now() - started
965
+ });
966
+ return response;
967
+ };
968
+ }
969
+ async function readBody(responseOrRequest, maxChars) {
970
+ const method = responseOrRequest instanceof Request ? responseOrRequest.method.toUpperCase() : void 0;
971
+ if (method === "GET" || method === "HEAD") return {};
972
+ const contentType = responseOrRequest.headers.get("content-type") ?? "";
973
+ if (responseOrRequest instanceof Response && responseOrRequest.status === 204) return {};
974
+ let text;
975
+ try {
976
+ text = await responseOrRequest.text();
977
+ } catch {
978
+ return {};
979
+ }
980
+ if (!text) return {};
981
+ const truncated = text.length > maxChars;
982
+ const clipped = truncated ? text.slice(0, maxChars) : text;
983
+ if (contentType.includes("application/json")) {
984
+ try {
985
+ return { body: redactValue(JSON.parse(clipped)), bodyTruncated: truncated || void 0 };
986
+ } catch {
987
+ return { body: clipped, bodyTruncated: truncated || void 0 };
988
+ }
989
+ }
990
+ if (contentType.includes("application/x-www-form-urlencoded")) {
991
+ const params = {};
992
+ for (const [key, value] of new URLSearchParams(clipped)) {
993
+ params[key] = SENSITIVE_KEYS.test(key) ? REDACTED : value;
994
+ }
995
+ return { body: params, bodyTruncated: truncated || void 0 };
996
+ }
997
+ return { body: clipped, bodyTruncated: truncated || void 0 };
998
+ }
999
+ function headersToRecord(headers) {
1000
+ const out = {};
1001
+ headers.forEach((value, key) => {
1002
+ out[key] = value;
1003
+ });
1004
+ return out;
1005
+ }
1006
+ function redactHeaders(headers) {
1007
+ const out = {};
1008
+ for (const [key, value] of Object.entries(headers)) {
1009
+ out[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? REDACTED : value;
1010
+ }
1011
+ return out;
1012
+ }
1013
+ function redactValue(value) {
1014
+ if (Array.isArray(value)) return value.map(redactValue);
1015
+ if (!value || typeof value !== "object") return value;
1016
+ const out = {};
1017
+ for (const [key, child] of Object.entries(value)) {
1018
+ out[key] = SENSITIVE_KEYS.test(key) ? REDACTED : redactValue(child);
1019
+ }
1020
+ return out;
1021
+ }
1022
+ var CORE_RESET_BEHAVIOR = {
1023
+ description: "Resets the instance to its seeded baseline.",
1024
+ reseeds: true,
1025
+ clearsLedger: true,
1026
+ clearsWebhooks: true
1027
+ };
1028
+ function coreLedgerCapabilities(persistent) {
1029
+ return {
1030
+ description: "Recent provider requests with sensitive headers and fields redacted.",
1031
+ recordsFields: [
1032
+ "timestamp",
1033
+ "method",
1034
+ "host",
1035
+ "path",
1036
+ "route",
1037
+ "operationId",
1038
+ "correlationId",
1039
+ "identity",
1040
+ "request",
1041
+ "response",
1042
+ "summary",
1043
+ "sideEffects",
1044
+ "webhookDeliveries",
1045
+ "durationMs"
1046
+ ],
1047
+ redactsSensitive: true,
1048
+ correlationId: true,
1049
+ webhookDeliveries: true,
1050
+ sideEffects: true,
1051
+ persistent,
1052
+ maxEntries: 1e3
1053
+ };
1054
+ }
1055
+ function coreInspectorTabs(manifest) {
1056
+ const tabs = [
1057
+ {
1058
+ id: "overview",
1059
+ title: "Overview",
1060
+ kind: "landing",
1061
+ description: "Service surfaces, base URLs, and connection snippets."
1062
+ },
1063
+ {
1064
+ id: "ledger",
1065
+ title: "Ledger",
1066
+ kind: "ledger",
1067
+ description: "Recent provider requests recorded by the emulator."
1068
+ },
1069
+ { id: "state", title: "State", kind: "state", description: "Current seeded and mutated instance state." },
1070
+ {
1071
+ id: "credentials",
1072
+ title: "Credentials",
1073
+ kind: "credentials",
1074
+ description: "Mint tokens, API keys, or OAuth clients."
1075
+ }
1076
+ ];
1077
+ if (manifest.seedSchema || manifest.scenarios && manifest.scenarios.length > 0) {
1078
+ tabs.push({ id: "seed", title: "Seed", kind: "seed", description: "Seed state or load a scenario." });
1079
+ }
1080
+ if (manifest.specs.some((s) => s.kind === "openapi" || s.kind === "graphql")) {
1081
+ tabs.push({ id: "spec", title: "Spec", kind: "spec", description: "OpenAPI / GraphQL spec sources and coverage." });
1082
+ }
1083
+ if (manifest.surfaces.some((s) => s.kind === "webhooks")) {
1084
+ tabs.push({
1085
+ id: "logs",
1086
+ title: "Webhooks",
1087
+ kind: "logs",
1088
+ description: "Webhook deliveries dispatched by the emulator."
1089
+ });
1090
+ }
1091
+ return tabs;
1092
+ }
1093
+ function coreConnections() {
1094
+ return [
1095
+ {
1096
+ id: "base-url",
1097
+ title: "Base URL (env)",
1098
+ kind: "env",
1099
+ language: "bash",
1100
+ description: "Point your SDK or app at the emulator instead of the real provider.",
1101
+ template: "{{SERVICE_UPPER}}_BASE_URL={{baseUrl}}"
1102
+ },
1103
+ {
1104
+ id: "create-credential",
1105
+ title: "Create a credential",
1106
+ kind: "curl",
1107
+ language: "bash",
1108
+ description: "Mint a working credential for this instance.",
1109
+ template: `curl -s -X POST {{controlBaseUrl}}/credentials \\
1110
+ -H "content-type: application/json" \\
1111
+ -d '{"type":"{{defaultAuthType}}"}'`
1112
+ },
1113
+ {
1114
+ id: "inspect-ledger",
1115
+ title: "Inspect requests",
1116
+ kind: "curl",
1117
+ language: "bash",
1118
+ description: "Read the request ledger to validate how your app called the service.",
1119
+ template: "curl -s {{controlBaseUrl}}/ledger"
1120
+ }
1121
+ ];
1122
+ }
1123
+ function enrichManifest(manifest, opts = {}) {
1124
+ return {
1125
+ ...manifest,
1126
+ resetBehavior: manifest.resetBehavior ?? CORE_RESET_BEHAVIOR,
1127
+ ledger: manifest.ledger ?? coreLedgerCapabilities(opts.ledgerPersistent ?? false),
1128
+ inspectorTabs: manifest.inspectorTabs ?? coreInspectorTabs(manifest),
1129
+ connections: [...manifest.connections ?? [], ...coreConnections()]
1130
+ };
1131
+ }
1132
+ function resolveConnections(connections, vars) {
1133
+ const map = {
1134
+ baseUrl: vars.baseUrl,
1135
+ providerBaseUrl: vars.providerBaseUrl,
1136
+ controlBaseUrl: vars.controlBaseUrl,
1137
+ service: vars.service,
1138
+ SERVICE_UPPER: vars.service.toUpperCase().replace(/[^A-Z0-9]+/g, "_"),
1139
+ instance: vars.instance ?? "default",
1140
+ token: vars.token ?? "<token>",
1141
+ clientId: vars.clientId ?? "<client_id>",
1142
+ clientSecret: vars.clientSecret ?? "<client_secret>",
1143
+ defaultAuthType: vars.defaultAuthType ?? "bearer-token"
1144
+ };
1145
+ return connections.map((snippet) => ({
1146
+ ...snippet,
1147
+ body: snippet.template.replace(/\{\{(\w+)\}\}/g, (_, key) => map[key] ?? `{{${key}}}`)
1148
+ }));
1149
+ }
1150
+ function coverageReport(manifest) {
1151
+ const operations = [];
1152
+ const summary = {
1153
+ generated: 0,
1154
+ "hand-authored": 0,
1155
+ partial: 0,
1156
+ unsupported: 0
1157
+ };
1158
+ const specs = manifest.specs.map((spec) => {
1159
+ const ops = spec.operations ?? [];
1160
+ for (const op of ops) {
1161
+ operations.push(op);
1162
+ summary[op.status] += 1;
1163
+ }
1164
+ return { kind: spec.kind, title: spec.title, coverage: spec.coverage, operationCount: ops.length };
1165
+ });
1166
+ return { operations, summary, specs };
1167
+ }
1168
+ function createDefaultManifest(service) {
1169
+ return {
1170
+ id: service,
1171
+ name: service,
1172
+ description: `Stateful ${service} API emulator.`,
1173
+ surfaces: [{ id: "rest", kind: "rest", title: "REST API", status: "partial" }],
1174
+ auth: [{ id: "bearer", title: "Bearer token", type: "bearer-token", status: "partial" }],
1175
+ specs: [{ kind: "manual", title: "Hand-authored emulator behavior", coverage: "partial" }]
1176
+ };
1177
+ }
1178
+ function escapeHtml(s) {
1179
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1180
+ }
1181
+ var CSS = `
1182
+ @font-face{
1183
+ font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
1184
+ src:url('/_emulate/fonts/geist-sans.woff2') format('woff2');
1185
+ }
1186
+ @font-face{
1187
+ font-family:'Geist Pixel';font-style:normal;font-weight:400;font-display:swap;
1188
+ src:url('/_emulate/fonts/GeistPixel-Square.woff2') format('woff2');
1189
+ }
1190
+ *{box-sizing:border-box;margin:0;padding:0}
1191
+ body{
1192
+ font-family:'Geist',-apple-system,BlinkMacSystemFont,sans-serif;
1193
+ background:#000;color:#33ff00;min-height:100vh;
1194
+ -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
1195
+ }
1196
+ .emu-bar{
1197
+ border-bottom:1px solid #0a3300;padding:10px 20px;
1198
+ display:flex;align-items:center;gap:10px;font-size:.8125rem;color:#1a8c00;
1199
+ }
1200
+ .emu-bar-title{font-weight:600;color:#33ff00;font-family:'Geist Pixel',monospace;}
1201
+ .emu-bar-links{margin-left:auto;display:flex;gap:16px;}
1202
+ .emu-bar-links a{
1203
+ color:#1a8c00;font-size:.75rem;text-decoration:none;transition:color .15s;
1204
+ }
1205
+ .emu-bar-links a:hover{color:#33ff00;}
1206
+ .emu-bar-links a .full{display:inline;}
1207
+ .emu-bar-links a .short{display:none;}
1208
+ @media(max-width:600px){
1209
+ .emu-bar-links a .full{display:none;}
1210
+ .emu-bar-links a .short{display:inline;}
1211
+ }
1212
+
1213
+ .content{
1214
+ display:flex;align-items:center;justify-content:center;
1215
+ min-height:calc(100vh - 42px);padding:24px 16px;
1216
+ }
1217
+ .content-inner{width:100%;max-width:420px;}
1218
+ .card-title{
1219
+ font-family:'Geist Pixel',monospace;
1220
+ font-size:1.125rem;font-weight:600;margin-bottom:4px;color:#33ff00;
1221
+ }
1222
+ .card-subtitle{color:#1a8c00;font-size:.8125rem;margin-bottom:18px;line-height:1.45;}
1223
+ .powered-by{
1224
+ position:fixed;bottom:0;left:0;right:0;
1225
+ text-align:center;padding:12px;font-size:.6875rem;color:#0a3300;
1226
+ font-family:'Geist Pixel',monospace;
1227
+ }
1228
+ .powered-by a{color:#1a8c00;text-decoration:none;transition:color .15s;}
1229
+ .powered-by a:hover{color:#33ff00;}
1230
+
1231
+ .error-title{
1232
+ font-family:'Geist Pixel',monospace;
1233
+ color:#ff4444;font-size:1.125rem;font-weight:600;margin-bottom:8px;
1234
+ }
1235
+ .error-msg{color:#1a8c00;font-size:.875rem;line-height:1.5;}
1236
+ .error-card{text-align:center;}
1237
+
1238
+ .user-form{margin-bottom:8px;}
1239
+ .user-form:last-of-type{margin-bottom:0;}
1240
+ .user-btn{
1241
+ width:100%;display:flex;align-items:center;gap:12px;
1242
+ padding:10px 12px;border:1px solid #0a3300;border-radius:8px;
1243
+ background:#000;color:inherit;cursor:pointer;text-align:left;
1244
+ font:inherit;transition:border-color .15s;
1245
+ }
1246
+ .user-btn:hover{border-color:#33ff00;}
1247
+ .avatar{
1248
+ width:36px;height:36px;border-radius:50%;
1249
+ background:#0a3300;color:#33ff00;font-weight:600;font-size:.875rem;
1250
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
1251
+ font-family:'Geist Pixel',monospace;
1252
+ }
1253
+ .user-text{min-width:0;}
1254
+ .user-login{font-weight:600;font-size:.875rem;display:block;color:#33ff00;}
1255
+ .user-meta{color:#1a8c00;font-size:.75rem;margin-top:1px;}
1256
+ .user-email{font-size:.6875rem;color:#116600;word-break:break-all;margin-top:1px;}
1257
+
1258
+ .settings-layout{
1259
+ max-width:920px;margin:0 auto;padding:28px 20px;
1260
+ display:flex;gap:28px;
1261
+ }
1262
+ .settings-sidebar{width:200px;flex-shrink:0;}
1263
+ .settings-sidebar a{
1264
+ display:block;padding:6px 10px;border-radius:6px;color:#1a8c00;
1265
+ text-decoration:none;font-size:.8125rem;transition:color .15s;
1266
+ }
1267
+ .settings-sidebar a:hover{color:#33ff00;}
1268
+ .settings-sidebar a.active{color:#33ff00;font-weight:600;}
1269
+ .settings-main{flex:1;min-width:0;}
1270
+
1271
+ .s-card{
1272
+ padding:18px 0;margin-bottom:14px;border-bottom:1px solid #0a3300;
1273
+ }
1274
+ .s-card:last-child{border-bottom:none;}
1275
+ .s-card-header{display:flex;align-items:center;gap:14px;margin-bottom:14px;}
1276
+ .s-icon{
1277
+ width:42px;height:42px;border-radius:8px;
1278
+ background:#0a3300;display:flex;align-items:center;justify-content:center;
1279
+ font-size:1.125rem;font-weight:700;color:#116600;flex-shrink:0;
1280
+ font-family:'Geist Pixel',monospace;
1281
+ }
1282
+ .s-title{
1283
+ font-family:'Geist Pixel',monospace;
1284
+ font-size:1.25rem;font-weight:600;color:#33ff00;
1285
+ }
1286
+ .s-subtitle{font-size:.75rem;color:#1a8c00;margin-top:2px;}
1287
+ .section-heading{
1288
+ font-size:.9375rem;font-weight:600;margin-bottom:10px;color:#33ff00;
1289
+ display:flex;align-items:center;justify-content:space-between;
1290
+ }
1291
+ .perm-list{list-style:none;}
1292
+ .perm-list li{padding:5px 0;font-size:.8125rem;display:flex;align-items:center;gap:6px;color:#1a8c00;}
1293
+ .check{color:#33ff00;}
1294
+ .org-row{
1295
+ display:flex;align-items:center;gap:8px;padding:7px 0;
1296
+ border-bottom:1px solid #0a3300;font-size:.8125rem;
1297
+ }
1298
+ .org-row:last-child{border-bottom:none;}
1299
+ .org-icon{
1300
+ width:22px;height:22px;border-radius:4px;background:#0a3300;
1301
+ display:flex;align-items:center;justify-content:center;
1302
+ font-size:.625rem;font-weight:700;color:#116600;flex-shrink:0;
1303
+ font-family:'Geist Pixel',monospace;
1304
+ }
1305
+ .org-name{font-weight:600;color:#33ff00;}
1306
+ .badge{font-size:.6875rem;padding:1px 7px;border-radius:999px;font-weight:500;}
1307
+ .badge-granted{background:#0a3300;color:#33ff00;}
1308
+ .badge-denied{background:#1a0a0a;color:#ff4444;}
1309
+ .badge-requested{background:#0a3300;color:#1a8c00;}
1310
+ .btn-revoke{
1311
+ display:inline-block;padding:5px 14px;border-radius:6px;
1312
+ border:1px solid #0a3300;background:transparent;color:#ff4444;
1313
+ font-size:.75rem;font-weight:600;cursor:pointer;transition:border-color .15s;
1314
+ }
1315
+ .btn-revoke:hover{border-color:#ff4444;}
1316
+ .info-text{color:#1a8c00;font-size:.75rem;line-height:1.5;margin-top:10px;}
1317
+ .info-text a,.section-heading a{color:#1a8c00;text-decoration:none;transition:color .15s;}
1318
+ .info-text a:hover,.section-heading a:hover{color:#33ff00;}
1319
+ code{font-family:'Geist Mono','SF Mono',ui-monospace,monospace;font-size:.8125rem;color:#33ff00;word-break:break-all;}
1320
+ .code-block{
1321
+ background:#020;border:1px solid #0a3300;border-radius:6px;padding:10px 12px;
1322
+ margin:8px 0 12px;overflow-x:auto;
1323
+ }
1324
+ .code-block code{white-space:pre;word-break:normal;display:block;line-height:1.5;}
1325
+ .app-link{
1326
+ display:flex;align-items:center;gap:12px;padding:12px;
1327
+ border:1px solid #0a3300;border-radius:8px;background:#000;
1328
+ text-decoration:none;color:inherit;margin-bottom:8px;transition:border-color .15s;
1329
+ }
1330
+ .app-link:hover{border-color:#33ff00;}
1331
+ .app-link-name{font-weight:600;font-size:.875rem;color:#33ff00;}
1332
+ .app-link-scopes{font-size:.6875rem;color:#1a8c00;margin-top:1px;}
1333
+ .empty{color:#1a8c00;text-align:center;padding:28px 0;font-size:.875rem;}
1334
+
1335
+ .inspector-layout{max-width:960px;margin:0 auto;padding:28px 20px;}
1336
+ .inspector-tabs{display:flex;gap:4px;margin-bottom:20px;}
1337
+ .inspector-tabs a{
1338
+ padding:7px 16px;border-radius:6px;text-decoration:none;
1339
+ font-size:.8125rem;color:#1a8c00;border:1px solid transparent;
1340
+ transition:color .15s,border-color .15s;
1341
+ }
1342
+ .inspector-tabs a:hover{color:#33ff00;}
1343
+ .inspector-tabs a.active{color:#33ff00;font-weight:600;border-color:#0a3300;background:#0a3300;}
1344
+ .inspector-section{margin-bottom:24px;}
1345
+ .inspector-section h2{
1346
+ font-family:'Geist Pixel',monospace;
1347
+ font-size:1rem;font-weight:600;color:#33ff00;margin-bottom:10px;
1348
+ }
1349
+ .inspector-section h3{
1350
+ font-family:'Geist Pixel',monospace;
1351
+ font-size:.875rem;font-weight:600;color:#1a8c00;margin:16px 0 8px;
1352
+ }
1353
+ .inspector-table{width:100%;border-collapse:collapse;margin-bottom:12px;}
1354
+ .inspector-table th,.inspector-table td{
1355
+ text-align:left;padding:8px 12px;border-bottom:1px solid #0a3300;
1356
+ font-size:.8125rem;
1357
+ }
1358
+ .inspector-table th{color:#1a8c00;font-weight:600;font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;}
1359
+ .inspector-table td{color:#33ff00;}
1360
+ .inspector-table tbody tr{transition:background .1s;}
1361
+ .inspector-table tbody tr:hover{background:#0a3300;}
1362
+ .inspector-empty{color:#1a8c00;text-align:center;padding:20px 0;font-size:.8125rem;}
1363
+
1364
+ .checkout-layout{
1365
+ display:flex;min-height:calc(100vh - 42px);
1366
+ }
1367
+ .checkout-summary{
1368
+ flex:1;background:#020;padding:48px 40px 48px 10%;
1369
+ display:flex;flex-direction:column;justify-content:center;
1370
+ border-right:1px solid #0a3300;
1371
+ }
1372
+ .checkout-form-side{
1373
+ flex:1;background:#000;padding:48px 10% 48px 40px;
1374
+ display:flex;flex-direction:column;justify-content:center;
1375
+ }
1376
+ .checkout-merchant{
1377
+ display:flex;align-items:center;gap:10px;margin-bottom:6px;
1378
+ }
1379
+ .checkout-merchant-name{
1380
+ font-family:'Geist Pixel',monospace;
1381
+ font-size:.9375rem;font-weight:600;color:#33ff00;
1382
+ }
1383
+ .checkout-test-badge{
1384
+ font-size:.625rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;
1385
+ background:#0a3300;color:#1a8c00;padding:2px 8px;border-radius:4px;
1386
+ }
1387
+ .checkout-total{
1388
+ font-family:'Geist Pixel',monospace;
1389
+ font-size:2rem;font-weight:700;color:#33ff00;margin:8px 0 28px;
1390
+ }
1391
+ .checkout-line-item{
1392
+ display:flex;align-items:center;gap:14px;padding:14px 0;
1393
+ border-bottom:1px solid #0a3300;
1394
+ }
1395
+ .checkout-line-item:first-child{border-top:1px solid #0a3300;}
1396
+ .checkout-item-icon{
1397
+ width:42px;height:42px;border-radius:6px;background:#0a3300;
1398
+ display:flex;align-items:center;justify-content:center;flex-shrink:0;
1399
+ font-family:'Geist Pixel',monospace;font-size:.875rem;font-weight:700;color:#116600;
1400
+ }
1401
+ .checkout-item-details{flex:1;min-width:0;}
1402
+ .checkout-item-name{font-size:.875rem;font-weight:600;color:#33ff00;}
1403
+ .checkout-item-qty{font-size:.75rem;color:#1a8c00;margin-top:2px;}
1404
+ .checkout-item-price{
1405
+ font-size:.875rem;font-weight:600;color:#33ff00;text-align:right;white-space:nowrap;
1406
+ }
1407
+ .checkout-item-unit{font-size:.6875rem;color:#1a8c00;text-align:right;margin-top:2px;}
1408
+ .checkout-totals{margin-top:20px;}
1409
+ .checkout-totals-row{
1410
+ display:flex;justify-content:space-between;padding:6px 0;
1411
+ font-size:.8125rem;color:#1a8c00;
1412
+ }
1413
+ .checkout-totals-row.total{
1414
+ border-top:1px solid #0a3300;margin-top:8px;padding-top:14px;
1415
+ font-size:.9375rem;font-weight:600;color:#33ff00;
1416
+ }
1417
+ .checkout-form-section{margin-bottom:24px;}
1418
+ .checkout-form-label{
1419
+ font-size:.8125rem;font-weight:600;color:#33ff00;margin-bottom:8px;display:block;
1420
+ }
1421
+ .checkout-input{
1422
+ width:100%;padding:10px 12px;border:1px solid #0a3300;border-radius:6px;
1423
+ background:#020;color:#33ff00;font:inherit;font-size:.875rem;
1424
+ transition:border-color .15s;outline:none;
1425
+ }
1426
+ .checkout-input:focus{border-color:#33ff00;}
1427
+ .checkout-input::placeholder{color:#116600;}
1428
+ .checkout-card-box{
1429
+ border:1px solid #0a3300;border-radius:6px;padding:14px;
1430
+ background:#020;
1431
+ }
1432
+ .checkout-card-row{
1433
+ display:flex;gap:12px;margin-top:10px;
1434
+ }
1435
+ .checkout-card-row .checkout-input{flex:1;}
1436
+ .checkout-sim-note{
1437
+ font-size:.6875rem;color:#1a8c00;margin-top:10px;text-align:center;
1438
+ font-style:italic;
1439
+ }
1440
+ .checkout-pay-btn{
1441
+ width:100%;padding:14px;border:none;border-radius:8px;
1442
+ background:#33ff00;color:#000;font:inherit;font-size:.9375rem;font-weight:700;
1443
+ cursor:pointer;transition:background .15s;
1444
+ font-family:'Geist Pixel',monospace;
1445
+ }
1446
+ .checkout-pay-btn:hover{background:#44ff22;}
1447
+ .checkout-cancel{
1448
+ text-align:center;margin-top:14px;
1449
+ }
1450
+ .checkout-cancel a{
1451
+ color:#1a8c00;text-decoration:none;font-size:.8125rem;
1452
+ transition:color .15s;
1453
+ }
1454
+ .checkout-cancel a:hover{color:#33ff00;}
1455
+ @media(max-width:768px){
1456
+ .checkout-layout{flex-direction:column;}
1457
+ .checkout-summary{padding:32px 20px;border-right:none;border-bottom:1px solid #0a3300;}
1458
+ .checkout-form-side{padding:32px 20px;}
1459
+ }
1460
+ `;
1461
+ var POWERED_BY = `<div class="powered-by">Powered by <a href="https://emulate.dev" target="_blank" rel="noopener">emulate</a></div>`;
1462
+ function emuBar(service) {
1463
+ const title = service ? `${escapeHtml(service)} Emulator` : "Emulator";
1464
+ return `<div class="emu-bar">
1465
+ <span class="emu-bar-title">${title}</span>
1466
+ <nav class="emu-bar-links">
1467
+ <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>
1468
+ <a href="https://github.com/vercel-labs/emulate" target="_blank" rel="noopener"><span class="full">Source Code</span><span class="short">Source</span></a>
1469
+ <a href="https://emulate.dev" target="_blank" rel="noopener"><span class="full">Learn More</span><span class="short">Learn</span></a>
1470
+ </nav>
1471
+ </div>`;
1472
+ }
1473
+ function head(title) {
1474
+ return `<!DOCTYPE html>
1475
+ <html lang="en">
1476
+ <head>
1477
+ <meta charset="utf-8"/>
1478
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
1479
+ <link rel="icon" href="/_emulate/favicon.ico"/>
1480
+ <title>${escapeHtml(title)} | emulate</title>
1481
+ <style>${CSS}</style>
1482
+ </head>`;
1483
+ }
1484
+ function renderCardPage(title, subtitle, body, service) {
1485
+ return `${head(title)}
1486
+ <body>
1487
+ ${emuBar(service)}
1488
+ <div class="content">
1489
+ <div class="content-inner">
1490
+ <div class="card-title">${escapeHtml(title)}</div>
1491
+ <div class="card-subtitle">${subtitle}</div>
1492
+ ${body}
1493
+ </div>
1494
+ </div>
1495
+ ${POWERED_BY}
1496
+ </body></html>`;
1497
+ }
1498
+ var INSTANCE_NOTES = "Hosted deployments create instances lazily when the returned URL is first used.";
1499
+ function buildInstanceCreation(args) {
1500
+ return {
1501
+ service: args.service,
1502
+ instance: args.instance,
1503
+ providerBaseUrl: args.providerBaseUrl,
1504
+ controlBaseUrl: `${args.providerBaseUrl}/_emulate`,
1505
+ pathUrl: `${args.pathOrigin}/${args.service}/${args.instance}`,
1506
+ hostHint: `${args.service}.${args.instance}.${args.hostSuffix}`,
1507
+ notes: INSTANCE_NOTES
1508
+ };
1509
+ }
1510
+ function connectionVars(manifest, instance, overrides) {
1511
+ return {
1512
+ baseUrl: instance.providerBaseUrl,
1513
+ providerBaseUrl: instance.providerBaseUrl,
1514
+ controlBaseUrl: instance.controlBaseUrl,
1515
+ service: manifest.id,
1516
+ instance: instance.instance,
1517
+ defaultAuthType: manifest.auth[0]?.type ?? "bearer-token",
1518
+ ...overrides
1519
+ };
1520
+ }
1521
+ function registerControlPlane(app, options) {
1522
+ const { instance, store, webhooks, ledger } = options;
1523
+ const manifest = enrichManifest(options.manifest, { ledgerPersistent: options.ledgerPersistent });
1524
+ const hostSuffix = options.hostSuffix ?? "emulators.dev";
1525
+ app.get("/_emulate", (c) => c.html(renderLandingPage(manifest, instance)));
1526
+ app.get(
1527
+ "/_emulate/manifest",
1528
+ (c) => c.json({
1529
+ manifest,
1530
+ instance,
1531
+ connections: resolveConnections(manifest.connections ?? [], connectionVars(manifest, instance))
1532
+ })
1533
+ );
1534
+ app.get("/_emulate/quickstart", (c) => c.text(renderQuickstart(manifest, instance)));
1535
+ app.get("/_emulate/specs", (c) => c.json({ specs: manifest.specs, surfaces: manifest.surfaces }));
1536
+ app.get("/_emulate/coverage", (c) => c.json(coverageReport(manifest)));
1537
+ app.get("/_emulate/connections", (c) => {
1538
+ const overrides = {};
1539
+ const token = c.req.query("token");
1540
+ const clientId = c.req.query("client_id");
1541
+ const clientSecret = c.req.query("client_secret");
1542
+ if (token) overrides.token = token;
1543
+ if (clientId) overrides.clientId = clientId;
1544
+ if (clientSecret) overrides.clientSecret = clientSecret;
1545
+ return c.json({
1546
+ connections: resolveConnections(manifest.connections ?? [], connectionVars(manifest, instance, overrides))
1547
+ });
1548
+ });
1549
+ app.get("/_emulate/openapi", (c) => redirectToSpec(c, manifest, instance, "openapi"));
1550
+ app.get("/_emulate/graphql", (c) => endpointForSurface(c, manifest, instance, "graphql"));
1551
+ app.get("/_emulate/mcp", (c) => endpointForSurface(c, manifest, instance, "mcp"));
1552
+ app.get("/_emulate/state", (c) => c.json(store.snapshot()));
1553
+ app.get("/_emulate/ledger", (c) => {
1554
+ const limitParam = c.req.query("limit");
1555
+ const limit = limitParam ? Number.parseInt(limitParam, 10) : void 0;
1556
+ return c.json({ entries: ledger.list(Number.isFinite(limit) ? limit : void 0) });
1557
+ });
1558
+ app.delete("/_emulate/ledger", (c) => {
1559
+ ledger.clear();
1560
+ return c.json({ ok: true });
1561
+ });
1562
+ app.get("/_emulate/logs", (c) => c.json({ webhooks: webhooks.getDeliveries(), requests: ledger.list(100) }));
1563
+ app.post("/_emulate/reset", async (c) => {
1564
+ if (options.reset) {
1565
+ await options.reset();
1566
+ } else {
1567
+ store.reset();
1568
+ webhooks.clear();
1569
+ ledger.clear();
1570
+ }
1571
+ return c.json({ ok: true });
1572
+ });
1573
+ app.post("/_emulate/seed", async (c) => {
1574
+ if (!options.seed) {
1575
+ return c.json({ error: "unsupported", message: "This emulator does not support runtime seeding." }, 501);
1576
+ }
1577
+ const body = await c.req.json().catch(() => void 0);
1578
+ try {
1579
+ await options.seed(body);
1580
+ } catch (err) {
1581
+ return c.json({ error: "invalid_seed", message: err instanceof Error ? err.message : "Seed failed." }, 400);
1582
+ }
1583
+ return c.json({ ok: true });
1584
+ });
1585
+ app.post("/_emulate/credentials", async (c) => {
1586
+ const body = await c.req.json().catch(() => ({}));
1587
+ try {
1588
+ if (options.issueCredential) {
1589
+ const credential2 = await options.issueCredential(body);
1590
+ return c.json({ credential: credential2 });
1591
+ }
1592
+ const credential = issueDefaultCredential(body, manifest, options.tokenMap);
1593
+ if (!credential) {
1594
+ return c.json({ error: "unsupported", message: "This emulator cannot create that credential type." }, 501);
1595
+ }
1596
+ return c.json({ credential });
1597
+ } catch (err) {
1598
+ return c.json(
1599
+ { error: "unsupported", message: err instanceof Error ? err.message : "Credential creation failed." },
1600
+ 400
1601
+ );
1602
+ }
1603
+ });
1604
+ app.post("/_emulate/instances", async (c) => {
1605
+ const body = await c.req.json().catch(() => ({}));
1606
+ const nextInstance = slug(body.instance ?? `${manifest.id}-${randomId().slice(0, 8)}`);
1607
+ const service = slug(body.service ?? manifest.id);
1608
+ const origin = new URL(instance.providerBaseUrl).origin;
1609
+ return c.json(
1610
+ buildInstanceCreation({
1611
+ service,
1612
+ instance: nextInstance,
1613
+ providerBaseUrl: `${origin}/${service}/${nextInstance}`,
1614
+ pathOrigin: origin,
1615
+ hostSuffix
1616
+ })
1617
+ );
1618
+ });
1619
+ }
1620
+ function renderLandingPage(manifest, instance) {
1621
+ const surfaces = manifest.surfaces.map(
1622
+ (surface) => `<tr><td>${escapeHtml(surface.title)}</td><td><span class="badge">${escapeHtml(surface.status)}</span></td><td><code>${escapeHtml(surface.basePath ?? "")}</code></td></tr>`
1623
+ ).join("");
1624
+ const auth = manifest.auth.map((cap) => `<li><span class="badge">${escapeHtml(cap.status)}</span> ${escapeHtml(cap.title)}</li>`).join("");
1625
+ const connections = resolveConnections(manifest.connections ?? [], connectionVars(manifest, instance));
1626
+ const instanceLabel = instance.instance ? ` \xB7 instance <code>${escapeHtml(instance.instance)}</code>` : "";
1627
+ return renderCardPage(
1628
+ `${manifest.name} Emulator`,
1629
+ escapeHtml(manifest.description),
1630
+ `
1631
+ <div class="s-card">
1632
+ <div class="section-heading">Base URLs${instanceLabel}</div>
1633
+ <p class="info-text">Provider: <code>${escapeHtml(instance.providerBaseUrl)}</code></p>
1634
+ <p class="info-text">Control: <code>${escapeHtml(instance.controlBaseUrl)}</code></p>
1635
+ </div>
1636
+ <div class="s-card">
1637
+ <div class="section-heading">Surfaces</div>
1638
+ <table class="inspector-table">
1639
+ <thead><tr><th>Surface</th><th>Status</th><th>Path</th></tr></thead>
1640
+ <tbody>${surfaces}</tbody>
1641
+ </table>
1642
+ </div>
1643
+ <div class="s-card">
1644
+ <div class="section-heading">Credentials</div>
1645
+ <ul class="perm-list">${auth}</ul>
1646
+ </div>
1647
+ ${renderConnectionsHtml(connections)}
1648
+ <div class="s-card">
1649
+ <div class="section-heading">Control API</div>
1650
+ <p class="info-text">${controlLinks()}</p>
1651
+ </div>
1652
+ `,
1653
+ manifest.id
1654
+ );
1655
+ }
1656
+ function renderConnectionsHtml(connections) {
1657
+ if (connections.length === 0) return "";
1658
+ const blocks = connections.map(
1659
+ (c) => `<div class="section-heading">${escapeHtml(c.title)}</div><pre class="code-block"><code>${escapeHtml(c.body)}</code></pre>`
1660
+ ).join("");
1661
+ return `<div class="s-card"><div class="section-heading">Connect</div>${blocks}</div>`;
1662
+ }
1663
+ function controlLinks() {
1664
+ const routes = ["manifest", "quickstart", "specs", "coverage", "connections", "state", "ledger", "logs"];
1665
+ return routes.map((r) => `<a href="/_emulate/${r}">${r}</a>`).join(" | ");
1666
+ }
1667
+ function renderQuickstart(manifest, instance) {
1668
+ const connections = resolveConnections(manifest.connections ?? [], connectionVars(manifest, instance));
1669
+ const lines = [
1670
+ `# ${manifest.name} Emulator`,
1671
+ "",
1672
+ manifest.description,
1673
+ "",
1674
+ `Provider base URL: ${instance.providerBaseUrl}`,
1675
+ `Control base URL: ${instance.controlBaseUrl}`,
1676
+ "",
1677
+ "Supported surfaces:",
1678
+ ...manifest.surfaces.map((s) => `- ${s.title}: ${s.status}${s.basePath ? ` at ${s.basePath}` : ""}`),
1679
+ "",
1680
+ "Control endpoints:",
1681
+ `- ${instance.controlBaseUrl}/manifest`,
1682
+ `- ${instance.controlBaseUrl}/coverage`,
1683
+ `- ${instance.controlBaseUrl}/connections`,
1684
+ `- ${instance.controlBaseUrl}/state`,
1685
+ `- ${instance.controlBaseUrl}/ledger`,
1686
+ `- POST ${instance.controlBaseUrl}/credentials`,
1687
+ `- POST ${instance.controlBaseUrl}/seed`,
1688
+ `- POST ${instance.controlBaseUrl}/reset`,
1689
+ "",
1690
+ "Connect:",
1691
+ ...connections.flatMap((c) => ["", `## ${c.title}`, c.body])
1692
+ ];
1693
+ return lines.join("\n");
1694
+ }
1695
+ function issueDefaultCredential(request, manifest, tokenMap) {
1696
+ const type = request.type ?? manifest.auth[0]?.type ?? "bearer-token";
1697
+ if (type !== "bearer-token" && type !== "api-key") return null;
1698
+ if (!tokenMap) return null;
1699
+ const token = typeof request.token === "string" && request.token ? request.token : `emu_${manifest.id}_${randomId()}`;
1700
+ const login = request.login ?? "admin";
1701
+ const scopes = Array.isArray(request.scopes) ? request.scopes.filter((s) => typeof s === "string") : [];
1702
+ tokenMap.set(token, { login, id: Date.now(), scopes });
1703
+ return { type, token, login, scopes };
1704
+ }
1705
+ function redirectToSpec(c, manifest, instance, kind) {
1706
+ const spec = manifest.specs.find((s) => s.kind === kind && s.url);
1707
+ if (spec?.url) return c.redirect(resolveUrl(instance.providerBaseUrl, spec.url));
1708
+ const advertised = manifest.specs.some((s) => s.kind === kind);
1709
+ if (kind === "openapi" && advertised) return c.redirect(`${instance.providerBaseUrl}/openapi.json`);
1710
+ return c.json({ error: "not_found", message: `No ${kind} spec is advertised for this emulator.` }, 404);
1711
+ }
1712
+ function endpointForSurface(c, manifest, instance, kind) {
1713
+ const surface = manifest.surfaces.find((s) => s.kind === kind && s.basePath);
1714
+ if (!surface?.basePath) {
1715
+ return c.json({ error: "not_found", message: `No ${kind} surface is advertised for this emulator.` }, 404);
1716
+ }
1717
+ return c.json({ endpoint: resolveUrl(instance.providerBaseUrl, surface.basePath), surface });
1718
+ }
1719
+ function resolveUrl(baseUrl, pathOrUrl) {
1720
+ if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
1721
+ return `${baseUrl}${pathOrUrl.startsWith("/") ? "" : "/"}${pathOrUrl}`;
1722
+ }
1723
+ function randomId() {
1724
+ return crypto.randomUUID().replace(/-/g, "");
1725
+ }
1726
+ function slug(value) {
1727
+ return value.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1728
+ }
1729
+ function createServer(plugin, options = {}) {
1730
+ const port = options.port ?? 4e3;
1731
+ const baseUrl = options.baseUrl ?? `http://localhost:${port}`;
1732
+ const app = new Hono();
1733
+ const store = new Store();
1734
+ const webhooks = new WebhookDispatcher();
1735
+ const ledger = new RequestLedger();
1736
+ const tokenMap = /* @__PURE__ */ new Map();
1737
+ if (options.tokens) {
1738
+ for (const [token, user] of Object.entries(options.tokens)) {
1739
+ tokenMap.set(token, {
1740
+ login: user.login,
1741
+ id: user.id,
1742
+ scopes: user.scopes ?? ["repo", "user", "admin:org", "admin:repo_hook"]
1743
+ });
1744
+ }
1745
+ }
1746
+ const docsUrl = options.docsUrl ?? `https://docs.emulators.dev/${plugin.name}`;
1747
+ registerFontRoutes(app);
1748
+ app.onError(createApiErrorHandler(docsUrl));
1749
+ app.use("*", cors());
1750
+ app.use("*", createErrorHandler(docsUrl));
1751
+ app.use("*", authMiddleware(tokenMap, options.appKeyResolver, options.fallbackUser));
1752
+ if (options.enableControlPlane !== false) {
1753
+ const manifest = options.manifest ?? createDefaultManifest(plugin.name);
1754
+ registerControlPlane(app, {
1755
+ manifest,
1756
+ instance: {
1757
+ service: manifest.id,
1758
+ instance: options.instance,
1759
+ baseUrl,
1760
+ providerBaseUrl: baseUrl,
1761
+ controlBaseUrl: `${baseUrl}/_emulate`
1762
+ },
1763
+ store,
1764
+ webhooks,
1765
+ ledger,
1766
+ tokenMap,
1767
+ ledgerPersistent: options.ledgerPersistent,
1768
+ hostSuffix: options.hostSuffix,
1769
+ reset: options.reset,
1770
+ seed: options.seed,
1771
+ issueCredential: options.issueCredential
1772
+ });
1773
+ }
1774
+ app.use("*", createLedgerMiddleware(ledger, { webhooks }));
1775
+ const rateLimitCounters = /* @__PURE__ */ new Map();
1776
+ let lastPruneAt = Math.floor(Date.now() / 1e3);
1777
+ app.use("*", async (c, next) => {
1778
+ const token = c.get("authToken") ?? "__anonymous__";
1779
+ const now = Math.floor(Date.now() / 1e3);
1780
+ if (now - lastPruneAt > 3600) {
1781
+ for (const [key, val] of rateLimitCounters) {
1782
+ if (val.resetAt <= now) rateLimitCounters.delete(key);
1783
+ }
1784
+ lastPruneAt = now;
1785
+ }
1786
+ let counter = rateLimitCounters.get(token);
1787
+ if (!counter || counter.resetAt <= now) {
1788
+ counter = { remaining: 5e3, resetAt: now + 3600 };
1789
+ rateLimitCounters.set(token, counter);
1790
+ }
1791
+ counter.remaining = Math.max(0, counter.remaining - 1);
1792
+ c.header("X-RateLimit-Limit", "5000");
1793
+ c.header("X-RateLimit-Remaining", String(counter.remaining));
1794
+ c.header("X-RateLimit-Reset", String(counter.resetAt));
1795
+ c.header("X-RateLimit-Resource", "core");
1796
+ if (counter.remaining === 0) {
1797
+ return c.json(
1798
+ {
1799
+ message: "API rate limit exceeded",
1800
+ documentation_url: docsUrl
1801
+ },
1802
+ 403
1803
+ );
1804
+ }
1805
+ await next();
1806
+ });
1807
+ plugin.register(app, store, webhooks, baseUrl, tokenMap);
1808
+ app.notFound(
1809
+ (c) => c.json(
1810
+ {
1811
+ message: "Not Found",
1812
+ documentation_url: docsUrl
1813
+ },
1814
+ 404
1815
+ )
1816
+ );
1817
+ return { app, store, webhooks, ledger, port, baseUrl, tokenMap };
1818
+ }
1819
+
1820
+ // src/registry.ts
1821
+ var SERVICE_NAME_LIST = [
1822
+ "vercel",
1823
+ "github",
1824
+ "google",
1825
+ "slack",
1826
+ "apple",
1827
+ "microsoft",
1828
+ "okta",
1829
+ "aws",
1830
+ "resend",
1831
+ "stripe",
1832
+ "mongoatlas",
1833
+ "clerk",
1834
+ "spotify",
1835
+ "x",
1836
+ "workos",
1837
+ "autumn"
1838
+ ];
1839
+ var SERVICE_NAMES = SERVICE_NAME_LIST;
1840
+ function issueServiceCredential(service, loaded, store, baseUrl, tokenMap, request, webhooks) {
1841
+ if (loaded.issueCredential) {
1842
+ return loaded.issueCredential(store, baseUrl, tokenMap, request, webhooks);
1843
+ }
1844
+ const type = request.type ?? loaded.manifest.auth[0]?.type ?? "bearer-token";
1845
+ if (type === "bearer-token" || type === "api-key") {
1846
+ const login = request.login ?? "admin";
1847
+ const scopes = Array.isArray(request.scopes) ? request.scopes.filter((s) => typeof s === "string") : [];
1848
+ const id = loaded.ensureUser?.(store, baseUrl, login) ?? Date.now();
1849
+ const token = typeof request.token === "string" && request.token.length > 0 ? request.token : defaultToken(service, type);
1850
+ tokenMap.set(token, { login, id, scopes });
1851
+ return { type, token, login, scopes };
1852
+ }
1853
+ if (type === "oauth-authorization-code" || type === "oauth-client-credentials" || type === "dynamic-client-registration") {
1854
+ if (!loaded.seedFromConfig) throw new Error(`Credential type ${type} is not supported by ${service}`);
1855
+ const clientId = request.client_id ?? defaultClientId(service);
1856
+ const clientSecret = request.client_secret ?? defaultClientSecret(service);
1857
+ const redirectUris = normalizeRedirectUris(request.redirect_uris);
1858
+ const name = request.name ?? `${SERVICE_REGISTRY[service].label.replace(/ emulator$/i, "")} Client`;
1859
+ const seed = credentialSeed(service, { clientId, clientSecret, redirectUris, name, request });
1860
+ if (!seed) throw new Error(`Credential type ${type} is not supported by ${service}`);
1861
+ loaded.seedFromConfig(store, baseUrl, seed, webhooks);
1862
+ return {
1863
+ type,
1864
+ client_id: clientId,
1865
+ client_secret: clientSecret,
1866
+ redirect_uris: redirectUris,
1867
+ authorization_url: authorizationUrlFor(service, baseUrl),
1868
+ token_url: tokenUrlFor(service, baseUrl)
1869
+ };
1870
+ }
1871
+ throw new Error(`Credential type ${type} is not supported by ${service}`);
1872
+ }
1873
+ function defaultToken(service, type) {
1874
+ const prefix = type === "api-key" ? apiKeyPrefix(service) : `emu_${service}`;
1875
+ return `${prefix}_${randomId2()}`;
1876
+ }
1877
+ function apiKeyPrefix(service) {
1878
+ if (service === "stripe") return "sk_test";
1879
+ if (service === "resend") return "re";
1880
+ if (service === "clerk") return "sk_test";
1881
+ return `emu_${service}`;
1882
+ }
1883
+ function defaultClientId(service) {
1884
+ if (service === "spotify") return `app_${randomId2().slice(0, 18)}`;
1885
+ if (service === "github") return `Iv1.${randomId2().slice(0, 16)}`;
1886
+ if (service === "google") return `${randomId2().slice(0, 24)}.apps.googleusercontent.com`;
1887
+ return `${service}_${randomId2().slice(0, 18)}`;
1888
+ }
1889
+ function defaultClientSecret(service) {
1890
+ if (service === "google") return `GOCSPX-${randomId2().slice(0, 24)}`;
1891
+ return `secret_${randomId2()}`;
1892
+ }
1893
+ function normalizeRedirectUris(value) {
1894
+ if (Array.isArray(value)) {
1895
+ const uris = value.filter((uri) => typeof uri === "string" && uri.length > 0);
1896
+ if (uris.length > 0) return uris;
1897
+ }
1898
+ return ["http://localhost:3000/callback"];
1899
+ }
1900
+ function credentialSeed(service, args) {
1901
+ const { clientId, clientSecret, redirectUris, name, request } = args;
1902
+ if (service === "github") {
1903
+ return { oauth_apps: [{ client_id: clientId, client_secret: clientSecret, name, redirect_uris: redirectUris }] };
1904
+ }
1905
+ if (service === "google" || service === "apple" || service === "microsoft") {
1906
+ return { oauth_clients: [{ client_id: clientId, client_secret: clientSecret, name, redirect_uris: redirectUris }] };
1907
+ }
1908
+ if (service === "okta") {
1909
+ return {
1910
+ oauth_clients: [
1911
+ {
1912
+ client_id: clientId,
1913
+ client_secret: clientSecret,
1914
+ name,
1915
+ redirect_uris: redirectUris,
1916
+ auth_server_id: typeof request.auth_server_id === "string" ? request.auth_server_id : "default"
1917
+ }
1918
+ ]
1919
+ };
1920
+ }
1921
+ if (service === "slack") {
1922
+ return {
1923
+ oauth_apps: [
1924
+ {
1925
+ client_id: clientId,
1926
+ client_secret: clientSecret,
1927
+ name,
1928
+ redirect_uris: redirectUris,
1929
+ scopes: Array.isArray(request.scopes) ? request.scopes : ["chat:write", "channels:read", "users:read"],
1930
+ user_scopes: Array.isArray(request.user_scopes) ? request.user_scopes : []
1931
+ }
1932
+ ]
1933
+ };
1934
+ }
1935
+ if (service === "vercel") {
1936
+ return { integrations: [{ client_id: clientId, client_secret: clientSecret, name, redirect_uris: redirectUris }] };
1937
+ }
1938
+ if (service === "spotify") {
1939
+ return { clients: [{ client_id: clientId, client_secret: clientSecret, name }] };
1940
+ }
1941
+ if (service === "clerk") {
1942
+ return {
1943
+ oauth_applications: [{ client_id: clientId, client_secret: clientSecret, name, redirect_uris: redirectUris }]
1944
+ };
1945
+ }
1946
+ if (service === "x") {
1947
+ return {
1948
+ oauth_clients: [
1949
+ {
1950
+ client_id: clientId,
1951
+ client_secret: clientSecret,
1952
+ client_type: "confidential",
1953
+ name,
1954
+ redirect_uris: redirectUris
1955
+ }
1956
+ ]
1957
+ };
1958
+ }
1959
+ return null;
1960
+ }
1961
+ function tokenUrlFor(service, baseUrl) {
1962
+ const paths = {
1963
+ github: "/login/oauth/access_token",
1964
+ google: "/token",
1965
+ apple: "/auth/token",
1966
+ microsoft: "/oauth2/v2.0/token",
1967
+ okta: "/oauth2/default/v1/token",
1968
+ slack: "/api/oauth.v2.access",
1969
+ vercel: "/v2/oauth/access_token",
1970
+ spotify: "/api/token",
1971
+ clerk: "/oauth/token",
1972
+ x: "/2/oauth2/token"
1973
+ };
1974
+ const path = paths[service];
1975
+ return path ? `${baseUrl}${path}` : void 0;
1976
+ }
1977
+ function authorizationUrlFor(service, baseUrl) {
1978
+ const paths = {
1979
+ github: "/login/oauth/authorize",
1980
+ google: "/o/oauth2/v2/auth",
1981
+ apple: "/auth/authorize",
1982
+ microsoft: "/oauth2/v2.0/authorize",
1983
+ okta: "/oauth2/default/v1/authorize",
1984
+ slack: "/oauth/v2/authorize",
1985
+ vercel: "/integrations/oauth/authorize",
1986
+ clerk: "/oauth/authorize",
1987
+ x: "/2/oauth2/authorize"
1988
+ };
1989
+ const path = paths[service];
1990
+ return path ? `${baseUrl}${path}` : void 0;
1991
+ }
1992
+ function randomId2() {
1993
+ return crypto.randomUUID().replace(/-/g, "");
1994
+ }
1995
+ var SERVICE_REGISTRY = {
1996
+ vercel: {
1997
+ label: "Vercel REST API emulator",
1998
+ endpoints: "projects, deployments, domains, env vars, users, teams, file uploads, protection bypass",
1999
+ async load() {
2000
+ const mod = await import("./dist-P3SBBRFR.js");
2001
+ return {
2002
+ plugin: mod.vercelPlugin,
2003
+ manifest: mod.manifest,
2004
+ seedFromConfig: mod.seedFromConfig,
2005
+ ensureUser(store, baseUrl, login) {
2006
+ mod.seedFromConfig(store, baseUrl, { users: [{ username: login }] });
2007
+ return mod.getVercelStore(store).users.findOneBy("username", login)?.id ?? 1;
2008
+ }
2009
+ };
2010
+ },
2011
+ defaultFallback(cfg) {
2012
+ const firstLogin = cfg?.users?.[0]?.username ?? "admin";
2013
+ return { login: firstLogin, id: 1, scopes: [] };
2014
+ },
2015
+ initConfig: {
2016
+ vercel: {
2017
+ users: [{ username: "developer", name: "Developer", email: "dev@example.com" }],
2018
+ teams: [{ slug: "my-team", name: "My Team" }],
2019
+ projects: [{ name: "my-app", team: "my-team", framework: "nextjs" }],
2020
+ integrations: [
2021
+ {
2022
+ client_id: "oac_example_client_id",
2023
+ client_secret: "example_client_secret",
2024
+ name: "My Vercel App",
2025
+ redirect_uris: ["http://localhost:3000/api/auth/callback/vercel"]
2026
+ }
2027
+ ]
2028
+ }
2029
+ }
2030
+ },
2031
+ github: {
2032
+ label: "GitHub REST API emulator",
2033
+ endpoints: "users, repos, issues, PRs, comments, reviews, labels, milestones, branches, git data, orgs, teams, releases, webhooks, search, actions, checks, rate limit",
2034
+ async load() {
2035
+ const mod = await import("./dist-7FDUSG5I.js");
2036
+ return {
2037
+ plugin: mod.githubPlugin,
2038
+ manifest: mod.manifest,
2039
+ seedFromConfig: mod.seedFromConfig,
2040
+ createAppKeyResolver(store) {
2041
+ return (appId) => {
2042
+ try {
2043
+ const gh = mod.getGitHubStore(store);
2044
+ const ghApp = gh.apps.all().find((a) => a.app_id === appId);
2045
+ if (!ghApp) return null;
2046
+ return { privateKey: ghApp.private_key, slug: ghApp.slug, name: ghApp.name };
2047
+ } catch {
2048
+ return null;
2049
+ }
2050
+ };
2051
+ },
2052
+ ensureUser(store, baseUrl, login) {
2053
+ mod.seedFromConfig(store, baseUrl, { users: [{ login }] });
2054
+ return mod.getGitHubStore(store).users.findOneBy("login", login)?.id ?? 1;
2055
+ }
2056
+ };
2057
+ },
2058
+ defaultFallback(cfg) {
2059
+ const firstLogin = cfg?.users?.[0]?.login ?? "admin";
2060
+ return { login: firstLogin, id: 1, scopes: ["repo", "user", "admin:org", "admin:repo_hook"] };
2061
+ },
2062
+ initConfig: {
2063
+ github: {
2064
+ users: [
2065
+ {
2066
+ login: "octocat",
2067
+ name: "The Octocat",
2068
+ email: "octocat@github.com",
2069
+ bio: "I am the Octocat",
2070
+ company: "GitHub",
2071
+ location: "San Francisco"
2072
+ }
2073
+ ],
2074
+ orgs: [{ login: "my-org", name: "My Organization", description: "A test organization" }],
2075
+ repos: [
2076
+ {
2077
+ owner: "octocat",
2078
+ name: "hello-world",
2079
+ description: "My first repository",
2080
+ language: "JavaScript",
2081
+ topics: ["hello", "world"],
2082
+ auto_init: true
2083
+ },
2084
+ {
2085
+ owner: "my-org",
2086
+ name: "org-repo",
2087
+ description: "An organization repository",
2088
+ language: "TypeScript",
2089
+ auto_init: true
2090
+ }
2091
+ ],
2092
+ oauth_apps: [
2093
+ {
2094
+ client_id: "Iv1.example_client_id",
2095
+ client_secret: "example_client_secret",
2096
+ name: "My App",
2097
+ redirect_uris: ["http://localhost:3000/api/auth/callback/github"]
2098
+ }
2099
+ ]
2100
+ }
2101
+ }
2102
+ },
2103
+ google: {
2104
+ label: "Google OAuth 2.0 / OpenID Connect + Gmail, Calendar, and Drive emulator",
2105
+ endpoints: "OAuth authorize, token exchange, userinfo, OIDC discovery, token revocation, Gmail messages/drafts/threads/labels/history/settings, Calendar lists/events/freebusy, Drive files/uploads",
2106
+ async load() {
2107
+ const mod = await import("./dist-XVVIYXQG.js");
2108
+ return { plugin: mod.googlePlugin, manifest: mod.manifest, seedFromConfig: mod.seedFromConfig };
2109
+ },
2110
+ defaultFallback(cfg) {
2111
+ const firstEmail = cfg?.users?.[0]?.email ?? "testuser@gmail.com";
2112
+ return { login: firstEmail, id: 1, scopes: ["openid", "email", "profile"] };
2113
+ },
2114
+ initConfig: {
2115
+ google: {
2116
+ users: [
2117
+ {
2118
+ email: "testuser@example.com",
2119
+ name: "Test User",
2120
+ picture: "https://lh3.googleusercontent.com/a/default-user",
2121
+ email_verified: true
2122
+ }
2123
+ ],
2124
+ oauth_clients: [
2125
+ {
2126
+ client_id: "example-client-id.apps.googleusercontent.com",
2127
+ client_secret: "GOCSPX-example_secret",
2128
+ name: "Code App (Google)",
2129
+ redirect_uris: ["http://localhost:3000/api/auth/callback/google"]
2130
+ }
2131
+ ],
2132
+ labels: [
2133
+ {
2134
+ id: "Label_ops",
2135
+ user_email: "testuser@example.com",
2136
+ name: "Ops/Review",
2137
+ color_background: "#DDEEFF",
2138
+ color_text: "#111111"
2139
+ }
2140
+ ],
2141
+ messages: [
2142
+ {
2143
+ id: "msg_welcome",
2144
+ user_email: "testuser@example.com",
2145
+ from: "welcome@example.com",
2146
+ to: "testuser@example.com",
2147
+ subject: "Welcome to the Gmail emulator",
2148
+ body_text: "You can now test Gmail, Calendar, and Drive flows locally.",
2149
+ label_ids: ["INBOX", "UNREAD", "CATEGORY_UPDATES"],
2150
+ date: "2025-01-04T10:00:00.000Z"
2151
+ }
2152
+ ],
2153
+ calendars: [
2154
+ {
2155
+ id: "primary",
2156
+ user_email: "testuser@example.com",
2157
+ summary: "testuser@example.com",
2158
+ primary: true,
2159
+ selected: true,
2160
+ time_zone: "UTC"
2161
+ }
2162
+ ],
2163
+ calendar_events: [
2164
+ {
2165
+ id: "evt_kickoff",
2166
+ user_email: "testuser@example.com",
2167
+ calendar_id: "primary",
2168
+ summary: "Project Kickoff",
2169
+ start_date_time: "2025-01-10T09:00:00.000Z",
2170
+ end_date_time: "2025-01-10T09:30:00.000Z"
2171
+ }
2172
+ ],
2173
+ drive_items: [
2174
+ {
2175
+ id: "drv_docs",
2176
+ user_email: "testuser@example.com",
2177
+ name: "Docs",
2178
+ mime_type: "application/vnd.google-apps.folder",
2179
+ parent_ids: ["root"]
2180
+ }
2181
+ ]
2182
+ }
2183
+ }
2184
+ },
2185
+ slack: {
2186
+ label: "Slack API emulator",
2187
+ endpoints: "auth, chat, conversations, users, profiles, presence, files, pins, bookmarks, views, reactions, team, OAuth, incoming webhooks, inspector",
2188
+ async load() {
2189
+ const mod = await import("./dist-YPRJYQHW.js");
2190
+ return { plugin: mod.slackPlugin, manifest: mod.manifest, seedFromConfig: mod.seedFromConfig };
2191
+ },
2192
+ defaultFallback() {
2193
+ return {
2194
+ login: "U000000001",
2195
+ id: 1,
2196
+ scopes: []
2197
+ };
2198
+ },
2199
+ initConfig: {
2200
+ slack: {
2201
+ team: { name: "My Workspace", domain: "my-workspace" },
2202
+ users: [
2203
+ {
2204
+ name: "developer",
2205
+ real_name: "Developer",
2206
+ email: "dev@example.com",
2207
+ profile: {
2208
+ title: "Local Developer",
2209
+ status_text: "Testing locally",
2210
+ status_emoji: ":computer:"
2211
+ },
2212
+ presence: "active"
2213
+ }
2214
+ ],
2215
+ channels: [
2216
+ { name: "general", topic: "General discussion" },
2217
+ { name: "random", topic: "Random stuff" }
2218
+ ],
2219
+ bots: [{ name: "my-bot" }],
2220
+ oauth_apps: [
2221
+ {
2222
+ client_id: "12345.67890",
2223
+ client_secret: "example_client_secret",
2224
+ app_id: "A000000001",
2225
+ name: "My Slack App",
2226
+ redirect_uris: ["http://localhost:3000/api/auth/callback/slack"],
2227
+ scopes: [
2228
+ "chat:write",
2229
+ "channels:read",
2230
+ "channels:history",
2231
+ "channels:join",
2232
+ "channels:manage",
2233
+ "channels:write",
2234
+ "groups:read",
2235
+ "groups:history",
2236
+ "groups:write",
2237
+ "im:read",
2238
+ "im:history",
2239
+ "im:write",
2240
+ "mpim:read",
2241
+ "mpim:history",
2242
+ "mpim:write",
2243
+ "users:read",
2244
+ "users:read.email",
2245
+ "users.profile:read",
2246
+ "users.profile:write",
2247
+ "users:write",
2248
+ "files:read",
2249
+ "files:write",
2250
+ "pins:read",
2251
+ "pins:write",
2252
+ "bookmarks:read",
2253
+ "bookmarks:write",
2254
+ "reactions:read",
2255
+ "reactions:write",
2256
+ "team:read"
2257
+ ],
2258
+ user_scopes: ["users:read", "users.profile:read"],
2259
+ bot_name: "my-bot"
2260
+ }
2261
+ ],
2262
+ strict_scopes: false
2263
+ }
2264
+ }
2265
+ },
2266
+ apple: {
2267
+ label: "Apple Sign In / OAuth emulator",
2268
+ endpoints: "OAuth authorize, token exchange, JWKS",
2269
+ async load() {
2270
+ const mod = await import("./dist-ZEC77OKZ.js");
2271
+ return { plugin: mod.applePlugin, manifest: mod.manifest, seedFromConfig: mod.seedFromConfig };
2272
+ },
2273
+ defaultFallback(cfg) {
2274
+ const firstEmail = cfg?.users?.[0]?.email ?? "testuser@icloud.com";
2275
+ return { login: firstEmail, id: 1, scopes: ["openid", "email", "name"] };
2276
+ },
2277
+ initConfig: {
2278
+ apple: {
2279
+ users: [{ email: "testuser@icloud.com", name: "Test User" }],
2280
+ oauth_clients: [
2281
+ {
2282
+ client_id: "com.example.app",
2283
+ team_id: "TEAM001",
2284
+ name: "My Apple App",
2285
+ redirect_uris: ["http://localhost:3000/api/auth/callback/apple"]
2286
+ }
2287
+ ]
2288
+ }
2289
+ }
2290
+ },
2291
+ microsoft: {
2292
+ label: "Microsoft Entra ID OAuth 2.0 / OpenID Connect emulator",
2293
+ endpoints: "OAuth authorize, token exchange, userinfo, OIDC discovery, Graph /me, logout, token revocation",
2294
+ async load() {
2295
+ const mod = await import("./dist-M3GVASMR.js");
2296
+ return { plugin: mod.microsoftPlugin, manifest: mod.manifest, seedFromConfig: mod.seedFromConfig };
2297
+ },
2298
+ defaultFallback(cfg) {
2299
+ const firstEmail = cfg?.users?.[0]?.email ?? "testuser@outlook.com";
2300
+ return { login: firstEmail, id: 1, scopes: ["openid", "email", "profile", "User.Read"] };
2301
+ },
2302
+ initConfig: {
2303
+ microsoft: {
2304
+ users: [{ email: "testuser@outlook.com", name: "Test User" }],
2305
+ oauth_clients: [
2306
+ {
2307
+ client_id: "example-client-id",
2308
+ client_secret: "example-client-secret",
2309
+ name: "My Microsoft App",
2310
+ redirect_uris: ["http://localhost:3000/api/auth/callback/microsoft-entra-id"]
2311
+ }
2312
+ ]
2313
+ }
2314
+ }
2315
+ },
2316
+ okta: {
2317
+ label: "Okta OAuth 2.0 / OpenID Connect + management API emulator",
2318
+ endpoints: "OIDC discovery, JWKS, OAuth authorize/token/userinfo/introspect/revoke/logout, users, groups, apps, authorization servers",
2319
+ async load() {
2320
+ const mod = await import("./dist-BTEY33DJ.js");
2321
+ return { plugin: mod.oktaPlugin, manifest: mod.manifest, seedFromConfig: mod.seedFromConfig };
2322
+ },
2323
+ defaultFallback(cfg) {
2324
+ const firstLogin = cfg?.users?.[0]?.login ?? cfg?.users?.[0]?.email ?? "testuser@okta.local";
2325
+ return { login: firstLogin, id: 1, scopes: ["openid", "profile", "email", "groups"] };
2326
+ },
2327
+ initConfig: {
2328
+ okta: {
2329
+ users: [{ login: "testuser@okta.local", email: "testuser@okta.local", first_name: "Test", last_name: "User" }],
2330
+ groups: [{ name: "Everyone", description: "All users", type: "BUILT_IN", okta_id: "00g_everyone" }],
2331
+ authorization_servers: [{ id: "default", name: "default", audiences: ["api://default"] }],
2332
+ oauth_clients: [
2333
+ {
2334
+ client_id: "okta-test-client",
2335
+ client_secret: "okta-test-secret",
2336
+ name: "Sample OIDC Client",
2337
+ redirect_uris: ["http://localhost:3000/callback"],
2338
+ auth_server_id: "default"
2339
+ }
2340
+ ]
2341
+ }
2342
+ }
2343
+ },
2344
+ aws: {
2345
+ label: "AWS cloud service emulator",
2346
+ endpoints: "S3 (buckets, objects), SQS (queues, messages), IAM (users, roles, access keys), STS (assume role, caller identity)",
2347
+ async load() {
2348
+ const mod = await import("./dist-7N4COJHK.js");
2349
+ return {
2350
+ plugin: mod.awsPlugin,
2351
+ manifest: mod.manifest,
2352
+ seedFromConfig: mod.seedFromConfig,
2353
+ issueCredential(store, baseUrl, _tokenMap, request) {
2354
+ const userName = request.login ?? "developer";
2355
+ mod.seedFromConfig(store, baseUrl, { iam: { users: [{ user_name: userName, create_access_key: true }] } });
2356
+ const user = mod.getAwsStore(store).iamUsers.findOneBy("user_name", userName);
2357
+ const key = user?.access_keys.find((candidate) => candidate.status === "Active");
2358
+ if (!user || !key) throw new Error("Failed to create AWS access key");
2359
+ return {
2360
+ type: "provider-specific",
2361
+ provider: "aws",
2362
+ user_name: user.user_name,
2363
+ access_key_id: key.access_key_id,
2364
+ secret_access_key: key.secret_access_key,
2365
+ region: "us-east-1"
2366
+ };
2367
+ }
2368
+ };
2369
+ },
2370
+ defaultFallback() {
2371
+ return { login: "admin", id: 1, scopes: ["s3:*", "sqs:*", "iam:*", "sts:*"] };
2372
+ },
2373
+ initConfig: {
2374
+ aws: {
2375
+ region: "us-east-1",
2376
+ s3: { buckets: [{ name: "my-app-bucket" }, { name: "my-app-uploads" }] },
2377
+ sqs: { queues: [{ name: "my-app-events" }, { name: "my-app-dlq" }] },
2378
+ iam: {
2379
+ users: [{ user_name: "developer", create_access_key: true }],
2380
+ roles: [{ role_name: "lambda-execution-role", description: "Role for Lambda function execution" }]
2381
+ }
2382
+ }
2383
+ }
2384
+ },
2385
+ resend: {
2386
+ label: "Resend email API emulator",
2387
+ endpoints: "emails, domains, contacts, API keys, inbox UI",
2388
+ async load() {
2389
+ const mod = await import("./dist-XM5HSBDC.js");
2390
+ return { plugin: mod.resendPlugin, manifest: mod.manifest, seedFromConfig: mod.seedFromConfig };
2391
+ },
2392
+ defaultFallback() {
2393
+ return { login: "re_test_admin", id: 1, scopes: [] };
2394
+ },
2395
+ initConfig: {
2396
+ resend: {
2397
+ domains: [{ name: "example.com", region: "us-east-1" }],
2398
+ contacts: [{ email: "test@example.com", first_name: "Test", last_name: "User" }]
2399
+ }
2400
+ }
2401
+ },
2402
+ stripe: {
2403
+ label: "Stripe payments emulator",
2404
+ endpoints: "customers, payment methods, customer sessions, payment intents, charges, products, prices, checkout sessions, webhooks",
2405
+ async load() {
2406
+ const mod = await import("./dist-K4CVTD6K.js");
2407
+ return { plugin: mod.stripePlugin, manifest: mod.manifest, seedFromConfig: mod.seedFromConfig };
2408
+ },
2409
+ defaultFallback() {
2410
+ return { login: "sk_test_admin", id: 1, scopes: [] };
2411
+ },
2412
+ initConfig: {
2413
+ stripe: {
2414
+ customers: [{ email: "test@example.com", name: "Test Customer" }],
2415
+ products: [{ name: "Pro Plan", description: "Monthly pro subscription" }],
2416
+ prices: [{ product_name: "Pro Plan", currency: "usd", unit_amount: 2e3 }]
2417
+ }
2418
+ }
2419
+ },
2420
+ mongoatlas: {
2421
+ label: "MongoDB Atlas service emulator",
2422
+ endpoints: "Atlas Admin API v2 (projects, clusters, database users, databases, collections), Atlas Data API v1 (findOne, find, insertOne, insertMany, updateOne, updateMany, deleteOne, deleteMany, aggregate)",
2423
+ async load() {
2424
+ const mod = await import("./dist-RMPDKZUA.js");
2425
+ return { plugin: mod.mongoatlasPlugin, manifest: mod.manifest, seedFromConfig: mod.seedFromConfig };
2426
+ },
2427
+ defaultFallback() {
2428
+ return { login: "admin", id: 1, scopes: [] };
2429
+ },
2430
+ initConfig: {
2431
+ mongoatlas: {
2432
+ projects: [{ name: "Project0" }],
2433
+ clusters: [{ name: "Cluster0", project: "Project0" }],
2434
+ database_users: [{ username: "admin", project: "Project0" }],
2435
+ databases: [{ cluster: "Cluster0", name: "test", collections: ["items"] }]
2436
+ }
2437
+ }
2438
+ },
2439
+ clerk: {
2440
+ label: "Clerk authentication and user management emulator",
2441
+ endpoints: "OIDC discovery, JWKS, OAuth authorize/token/userinfo, users, email addresses, organizations, memberships, invitations, sessions",
2442
+ async load() {
2443
+ const mod = await import("./dist-WBKONLOE.js");
2444
+ return { plugin: mod.clerkPlugin, manifest: mod.manifest, seedFromConfig: mod.seedFromConfig };
2445
+ },
2446
+ defaultFallback(cfg) {
2447
+ const firstEmail = cfg?.users?.[0]?.email_addresses?.[0] ?? "test@example.com";
2448
+ return { login: firstEmail, id: 1, scopes: [] };
2449
+ },
2450
+ initConfig: {
2451
+ clerk: {
2452
+ users: [
2453
+ {
2454
+ first_name: "Test",
2455
+ last_name: "User",
2456
+ email_addresses: ["test@example.com"],
2457
+ password: "clerk_test_password"
2458
+ }
2459
+ ],
2460
+ organizations: [
2461
+ {
2462
+ name: "My Company",
2463
+ slug: "my-company",
2464
+ members: [{ email: "test@example.com", role: "admin" }]
2465
+ }
2466
+ ],
2467
+ oauth_applications: [
2468
+ {
2469
+ client_id: "clerk_emulate_client",
2470
+ client_secret: "clerk_emulate_secret",
2471
+ name: "Emulate App",
2472
+ redirect_uris: ["http://localhost:3000/api/auth/callback/clerk"]
2473
+ }
2474
+ ]
2475
+ }
2476
+ }
2477
+ },
2478
+ spotify: {
2479
+ label: "Spotify Web API emulator",
2480
+ endpoints: "client credentials token endpoint, catalog search, artists, albums, and tracks",
2481
+ async load() {
2482
+ const mod = await import("./dist-DK26ESP2.js");
2483
+ return { plugin: mod.spotifyPlugin, manifest: mod.manifest, seedFromConfig: mod.seedFromConfig };
2484
+ },
2485
+ defaultFallback() {
2486
+ return { login: "spotify-app", id: 1, scopes: [] };
2487
+ },
2488
+ initConfig: {
2489
+ spotify: {
2490
+ clients: [{ client_id: "demo-client-id", client_secret: "demo-client-secret", name: "Demo App" }]
2491
+ }
2492
+ }
2493
+ },
2494
+ x: {
2495
+ label: "X (Twitter) API v2 emulator",
2496
+ endpoints: "OAuth 2.0 authorize/token/revoke (Authorization Code with PKCE + app-only client credentials), users, tweets",
2497
+ async load() {
2498
+ const mod = await import("./dist-OYYGWKZQ.js");
2499
+ return {
2500
+ plugin: mod.xPlugin,
2501
+ manifest: mod.manifest,
2502
+ seedFromConfig: mod.seedFromConfig,
2503
+ ensureUser(store, baseUrl, login) {
2504
+ mod.seedFromConfig(store, baseUrl, { users: [{ username: login }] });
2505
+ return mod.getXStore(store).users.findOneBy("username", login.toLowerCase().replace(/^@/, ""))?.id ?? 1;
2506
+ }
2507
+ };
2508
+ },
2509
+ defaultFallback(cfg) {
2510
+ const firstUsername = cfg?.users?.[0]?.username ?? "developer";
2511
+ return { login: firstUsername, id: 1, scopes: ["tweet.read", "users.read"] };
2512
+ },
2513
+ initConfig: {
2514
+ x: {
2515
+ users: [
2516
+ {
2517
+ username: "developer",
2518
+ name: "Developer",
2519
+ description: "Building with the X API v2 emulator.",
2520
+ verified: true,
2521
+ followers_count: 1200,
2522
+ following_count: 320
2523
+ }
2524
+ ],
2525
+ oauth_clients: [
2526
+ {
2527
+ client_id: "x-confidential-client",
2528
+ client_secret: "x-confidential-secret",
2529
+ client_type: "confidential",
2530
+ name: "My X App (confidential)",
2531
+ redirect_uris: ["http://localhost:3000/api/auth/callback/twitter"]
2532
+ },
2533
+ {
2534
+ client_id: "x-public-client",
2535
+ client_type: "public",
2536
+ name: "My X App (public)",
2537
+ redirect_uris: ["http://localhost:3000/api/auth/callback/twitter"]
2538
+ }
2539
+ ],
2540
+ tweets: [{ text: "Hello from the X API v2 emulator.", author: "developer", like_count: 42, retweet_count: 7 }]
2541
+ }
2542
+ }
2543
+ },
2544
+ workos: {
2545
+ label: "WorkOS emulator",
2546
+ endpoints: "AuthKit user management (hosted login, code/refresh grants, sealed-session JWKS), organizations, memberships, invitations, API keys, Vault KV, OAuth authorization server",
2547
+ async load() {
2548
+ const mod = await import("./dist-IYZPDKJW.js");
2549
+ return {
2550
+ plugin: mod.workosPlugin,
2551
+ manifest: mod.manifest,
2552
+ seedFromConfig: mod.seedFromConfig,
2553
+ ensureUser(store, baseUrl, login) {
2554
+ mod.seedFromConfig(store, baseUrl, { users: [{ email: login }] });
2555
+ return mod.getWorkosStore(store).users.findOneBy("email", login)?.id ?? 1;
2556
+ }
2557
+ };
2558
+ },
2559
+ defaultFallback() {
2560
+ return { login: "sk_emulate_admin", id: 1, scopes: [] };
2561
+ },
2562
+ initConfig: {
2563
+ workos: {
2564
+ users: [{ email: "admin@example.com", first_name: "Admin", last_name: "User" }],
2565
+ organizations: [{ name: "Acme", members: ["admin@example.com"] }]
2566
+ }
2567
+ }
2568
+ },
2569
+ autumn: {
2570
+ label: "Autumn billing emulator",
2571
+ endpoints: "customers (get_or_create with seedable subscriptions), usage tracking, plans/features/events lists",
2572
+ async load() {
2573
+ const mod = await import("./dist-JJ2ZRCAX.js");
2574
+ return {
2575
+ plugin: mod.autumnPlugin,
2576
+ manifest: mod.manifest,
2577
+ seedFromConfig: mod.seedFromConfig
2578
+ };
2579
+ },
2580
+ defaultFallback() {
2581
+ return { login: "am_emulate_admin", id: 1, scopes: [] };
2582
+ },
2583
+ initConfig: {
2584
+ autumn: {
2585
+ customers: [{ id: "org_paid_example", subscriptions: [{ plan_id: "pro", status: "active" }] }]
2586
+ }
2587
+ }
2588
+ }
2589
+ };
2590
+ var DEFAULT_TOKENS = {
2591
+ tokens: {
2592
+ test_token_admin: {
2593
+ login: "admin",
2594
+ scopes: ["repo", "user", "admin:org", "admin:repo_hook"]
2595
+ },
2596
+ test_token_user1: {
2597
+ login: "octocat",
2598
+ scopes: ["repo", "user"]
2599
+ }
2600
+ }
2601
+ };
2602
+
2603
+ // src/commands/start.ts
2604
+ import { readFileSync as readFileSync2, existsSync } from "fs";
2605
+ import { resolve } from "path";
2606
+ import { parse as parseYaml } from "yaml";
2607
+ import pc from "picocolors";
2608
+
2609
+ // src/portless.ts
2610
+ import { execSync, spawnSync } from "child_process";
2611
+ import { createInterface } from "readline";
2612
+ function isInteractive() {
2613
+ return Boolean(process.stdin.isTTY) && !process.env.CI;
2614
+ }
2615
+ function hasPortless() {
2616
+ const result = spawnSync("portless", ["--version"], { stdio: "ignore" });
2617
+ return result.status === 0;
2618
+ }
2619
+ function promptYesNo(question) {
2620
+ return new Promise((resolve3) => {
2621
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2622
+ rl.question(question, (answer) => {
2623
+ rl.close();
2624
+ const normalized = answer.trim().toLowerCase();
2625
+ resolve3(normalized === "" || normalized === "y" || normalized === "yes");
2626
+ });
2627
+ });
2628
+ }
2629
+ function isProxyRunning() {
2630
+ const result = spawnSync("portless", ["list"], { stdio: "ignore" });
2631
+ return result.status === 0;
2632
+ }
2633
+ async function ensurePortless() {
2634
+ if (!hasPortless()) {
2635
+ if (!isInteractive()) {
2636
+ console.error("portless is required but not installed. Run: npm i -g portless");
2637
+ process.exit(1);
2638
+ }
2639
+ const yes = await promptYesNo("portless is not installed. Install it now? (npm i -g portless) [Y/n] ");
2640
+ if (!yes) {
2641
+ console.error("Cannot continue without portless.");
2642
+ process.exit(1);
2643
+ }
2644
+ try {
2645
+ execSync("npm i -g portless", { stdio: "inherit" });
2646
+ } catch {
2647
+ console.error("Failed to install portless.");
2648
+ process.exit(1);
2649
+ }
2650
+ if (!hasPortless()) {
2651
+ console.error("portless was installed but could not be found on PATH.");
2652
+ process.exit(1);
2653
+ }
2654
+ }
2655
+ if (!isProxyRunning()) {
2656
+ console.error("portless proxy is not running. Start it with: portless proxy start");
2657
+ process.exit(1);
2658
+ }
2659
+ }
2660
+ function registerAliases(aliases) {
2661
+ const registered = [];
2662
+ for (const { name, port } of aliases) {
2663
+ const result = spawnSync("portless", ["alias", name, String(port), "--force"], {
2664
+ stdio: "inherit"
2665
+ });
2666
+ if (result.status !== 0) {
2667
+ if (registered.length > 0) {
2668
+ removeAliases(registered);
2669
+ }
2670
+ throw new Error(`Failed to register portless alias: ${name} -> ${port}`);
2671
+ }
2672
+ registered.push({ name, port });
2673
+ }
2674
+ }
2675
+ function removeAliases(aliases) {
2676
+ for (const { name } of aliases) {
2677
+ const result = spawnSync("portless", ["alias", "--remove", name], { stdio: "ignore" });
2678
+ if (result.status !== 0) {
2679
+ console.error(`Warning: failed to remove portless alias: ${name}`);
2680
+ }
2681
+ }
2682
+ }
2683
+ function portlessBaseUrl(serviceName) {
2684
+ return `https://${serviceName}.emulate.localhost`;
2685
+ }
2686
+
2687
+ // src/base-url.ts
2688
+ function resolveBaseUrl(opts) {
2689
+ if (opts.seedBaseUrl) {
2690
+ return opts.seedBaseUrl.replace(/\{service\}/g, opts.service);
2691
+ }
2692
+ if (opts.baseUrl) {
2693
+ return opts.baseUrl.replace(/\{service\}/g, opts.service);
2694
+ }
2695
+ const envBaseUrl = process.env.EMULATE_BASE_URL;
2696
+ if (envBaseUrl) {
2697
+ return envBaseUrl.replace(/\{service\}/g, opts.service);
2698
+ }
2699
+ const portlessUrl = process.env.PORTLESS_URL;
2700
+ if (portlessUrl) {
2701
+ return portlessUrl.replace(/\{service\}/g, opts.service);
2702
+ }
2703
+ return `http://localhost:${opts.port}`;
2704
+ }
2705
+
2706
+ // src/commands/start.ts
2707
+ var pkg = { version: "0.6.0" };
2708
+ function loadSeedConfig(seedPath) {
2709
+ if (seedPath) {
2710
+ const fullPath = resolve(seedPath);
2711
+ if (!existsSync(fullPath)) {
2712
+ console.error(`Seed file not found: ${fullPath}`);
2713
+ process.exit(1);
2714
+ }
2715
+ const content = readFileSync2(fullPath, "utf-8");
2716
+ try {
2717
+ const config = fullPath.endsWith(".json") ? JSON.parse(content) : parseYaml(content);
2718
+ return { config, source: seedPath };
2719
+ } catch (err) {
2720
+ console.error(`Failed to parse ${seedPath}: ${err instanceof Error ? err.message : err}`);
2721
+ process.exit(1);
2722
+ }
2723
+ }
2724
+ const autoFiles = [
2725
+ "emulate.config.yaml",
2726
+ "emulate.config.yml",
2727
+ "emulate.config.json",
2728
+ "service-emulator.config.yaml",
2729
+ "service-emulator.config.yml",
2730
+ "service-emulator.config.json"
2731
+ ];
2732
+ for (const file of autoFiles) {
2733
+ const fullPath = resolve(file);
2734
+ if (existsSync(fullPath)) {
2735
+ const content = readFileSync2(fullPath, "utf-8");
2736
+ try {
2737
+ const config = fullPath.endsWith(".json") ? JSON.parse(content) : parseYaml(content);
2738
+ return { config, source: file };
2739
+ } catch (err) {
2740
+ console.error(`Failed to parse ${file}: ${err instanceof Error ? err.message : err}`);
2741
+ process.exit(1);
2742
+ }
2743
+ }
2744
+ }
2745
+ return null;
2746
+ }
2747
+ function inferServicesFromConfig(config) {
2748
+ const found = SERVICE_NAMES.filter((k) => k in config);
2749
+ return found.length > 0 ? [...found] : null;
2750
+ }
2751
+ async function startCommand(options) {
2752
+ const { port: basePort } = options;
2753
+ if (options.portless && options.baseUrl) {
2754
+ console.error("--portless and --base-url are mutually exclusive.");
2755
+ process.exit(1);
2756
+ }
2757
+ const loaded = loadSeedConfig(options.seed);
2758
+ const seedConfig = loaded?.config ?? null;
2759
+ const configSource = loaded?.source ?? null;
2760
+ let services;
2761
+ if (options.service) {
2762
+ services = options.service.split(",").map((s) => s.trim());
2763
+ } else if (seedConfig) {
2764
+ services = inferServicesFromConfig(seedConfig) ?? [...SERVICE_NAMES];
2765
+ } else {
2766
+ services = [...SERVICE_NAMES];
2767
+ }
2768
+ for (const svc of services) {
2769
+ if (!SERVICE_REGISTRY[svc]) {
2770
+ console.error(`Unknown service: ${svc}`);
2771
+ process.exit(1);
2772
+ }
2773
+ }
2774
+ const tokens = {};
2775
+ if (seedConfig?.tokens) {
2776
+ let tokenId = 100;
2777
+ for (const [token, user] of Object.entries(seedConfig.tokens)) {
2778
+ tokens[token] = { login: user.login, id: tokenId++, scopes: user.scopes };
2779
+ }
2780
+ } else {
2781
+ tokens["test_token_admin"] = { login: "admin", id: 2, scopes: ["repo", "user", "admin:org", "admin:repo_hook"] };
2782
+ }
2783
+ if (options.portless) {
2784
+ await ensurePortless();
2785
+ }
2786
+ const portlessAliases = [];
2787
+ const prepared = [];
2788
+ for (let i = 0; i < services.length; i++) {
2789
+ const svc = services[i];
2790
+ const entry = SERVICE_REGISTRY[svc];
2791
+ const loadedSvc = await entry.load();
2792
+ const svcSeedConfig = seedConfig?.[svc];
2793
+ const port = svcSeedConfig?.port ?? basePort + i;
2794
+ if (options.portless) {
2795
+ portlessAliases.push({ name: `${svc}.emulate`, port });
2796
+ }
2797
+ const seedBaseUrl = typeof svcSeedConfig?.baseUrl === "string" && svcSeedConfig.baseUrl.length > 0 ? svcSeedConfig.baseUrl : void 0;
2798
+ const effectiveBaseUrl = options.portless ? portlessBaseUrl(svc) : options.baseUrl;
2799
+ const baseUrl = resolveBaseUrl({ service: svc, port, baseUrl: effectiveBaseUrl, seedBaseUrl });
2800
+ prepared.push({ svc, entry, loadedSvc, svcSeedConfig, port, baseUrl });
2801
+ }
2802
+ if (portlessAliases.length > 0) {
2803
+ registerAliases(portlessAliases);
2804
+ }
2805
+ const serviceUrls = [];
2806
+ const stores = [];
2807
+ const httpServers = [];
2808
+ for (const { svc, entry, loadedSvc, svcSeedConfig, port, baseUrl } of prepared) {
2809
+ serviceUrls.push({ name: svc, url: baseUrl });
2810
+ let cachedResolver;
2811
+ const appKeyResolver = loadedSvc.createAppKeyResolver ? (appId) => cachedResolver(appId) : void 0;
2812
+ const fallbackUser = entry.defaultFallback(svcSeedConfig);
2813
+ let resetService = () => {
2814
+ };
2815
+ let applyRuntimeSeed = (_seed) => {
2816
+ };
2817
+ const { app, store, webhooks, ledger, tokenMap } = createServer(loadedSvc.plugin, {
2818
+ port,
2819
+ baseUrl,
2820
+ tokens,
2821
+ appKeyResolver,
2822
+ fallbackUser,
2823
+ manifest: loadedSvc.manifest,
2824
+ instance: svc,
2825
+ reset: () => resetService(),
2826
+ seed: (seed) => applyRuntimeSeed(seed),
2827
+ issueCredential: (request) => issueServiceCredential(svc, loadedSvc, store, baseUrl, tokenMap, request, webhooks)
2828
+ });
2829
+ cachedResolver = loadedSvc.createAppKeyResolver?.(store);
2830
+ stores.push(store);
2831
+ resetService = () => {
2832
+ store.reset();
2833
+ webhooks.clear();
2834
+ ledger.clear();
2835
+ loadedSvc.plugin.seed?.(store, baseUrl);
2836
+ if (svcSeedConfig && loadedSvc.seedFromConfig) {
2837
+ loadedSvc.seedFromConfig(store, baseUrl, svcSeedConfig, webhooks);
2838
+ }
2839
+ };
2840
+ applyRuntimeSeed = (seed) => {
2841
+ if (seed && loadedSvc.seedFromConfig) {
2842
+ loadedSvc.seedFromConfig(store, baseUrl, seed, webhooks);
2843
+ }
2844
+ };
2845
+ resetService();
2846
+ const httpServer = serve({ fetch: app.fetch, port });
2847
+ httpServers.push(httpServer);
2848
+ }
2849
+ printBanner(serviceUrls, tokens, configSource);
2850
+ const shutdown = () => {
2851
+ console.log(`
2852
+ ${pc.dim("Shutting down...")}`);
2853
+ if (portlessAliases.length > 0) {
2854
+ removeAliases(portlessAliases);
2855
+ }
2856
+ for (const store of stores) {
2857
+ store.reset();
2858
+ }
2859
+ for (const srv of httpServers) {
2860
+ srv.close();
2861
+ }
2862
+ process.exit(0);
2863
+ };
2864
+ process.once("SIGINT", shutdown);
2865
+ process.once("SIGTERM", shutdown);
2866
+ }
2867
+ function printBanner(services, tokens, configSource) {
2868
+ const lines = [];
2869
+ lines.push("");
2870
+ lines.push(` ${pc.bold("emulate")} ${pc.dim(`v${pkg.version}`)}`);
2871
+ lines.push("");
2872
+ const maxNameLen = Math.max(...services.map((s) => s.name.length));
2873
+ for (const { name, url } of services) {
2874
+ lines.push(` ${pc.cyan(name.padEnd(maxNameLen + 2))}${pc.bold(url)}`);
2875
+ }
2876
+ lines.push("");
2877
+ const tokenEntries = Object.entries(tokens);
2878
+ if (tokenEntries.length > 0) {
2879
+ lines.push(` ${pc.dim("Tokens")}`);
2880
+ for (const [token, user] of tokenEntries) {
2881
+ lines.push(` ${pc.dim(token)} ${pc.dim("->")} ${user.login}`);
2882
+ }
2883
+ lines.push("");
2884
+ }
2885
+ if (configSource) {
2886
+ lines.push(` ${pc.dim("Config:")} ${configSource}`);
2887
+ } else {
2888
+ lines.push(` ${pc.dim("Config:")} defaults ${pc.dim("(run")} npx emulate init ${pc.dim("to customize)")}`);
2889
+ }
2890
+ lines.push("");
2891
+ console.log(lines.join("\n"));
2892
+ }
2893
+
2894
+ // src/commands/init.ts
2895
+ import { writeFileSync, existsSync as existsSync2 } from "fs";
2896
+ import { resolve as resolve2 } from "path";
2897
+ import { stringify as yamlStringify } from "yaml";
2898
+ function initCommand(options) {
2899
+ const filename = "emulate.config.yaml";
2900
+ const fullPath = resolve2(filename);
2901
+ if (existsSync2(fullPath)) {
2902
+ console.error(`Config file already exists: ${filename}`);
2903
+ process.exit(1);
2904
+ }
2905
+ let config;
2906
+ if (options.service === "all") {
2907
+ config = { ...DEFAULT_TOKENS };
2908
+ for (const name of SERVICE_NAMES) {
2909
+ Object.assign(config, SERVICE_REGISTRY[name].initConfig);
2910
+ }
2911
+ } else {
2912
+ const entry = SERVICE_REGISTRY[options.service];
2913
+ if (!entry) {
2914
+ console.error(`Unknown service: ${options.service}. Available: ${SERVICE_NAMES.join(", ")}, all`);
2915
+ process.exit(1);
2916
+ }
2917
+ config = { ...DEFAULT_TOKENS, ...entry.initConfig };
2918
+ }
2919
+ const content = yamlStringify(config);
2920
+ writeFileSync(fullPath, content, "utf-8");
2921
+ console.log(`Created ${filename}`);
2922
+ console.log(`
2923
+ Run 'npx emulate' to start the emulator.`);
2924
+ }
2925
+
2926
+ // src/commands/list.ts
2927
+ function listCommand() {
2928
+ console.log("\nAvailable services:\n");
2929
+ for (const [name, entry] of Object.entries(SERVICE_REGISTRY)) {
2930
+ console.log(` ${name.padEnd(10)}${entry.label}`);
2931
+ console.log(` Endpoints: ${entry.endpoints}`);
2932
+ console.log();
2933
+ }
2934
+ }
2935
+
2936
+ // src/index.ts
2937
+ var pkg2 = { version: "0.6.0" };
2938
+ var defaultPort = process.env.EMULATE_PORT ?? process.env.PORT ?? "4000";
2939
+ var program = new Command();
2940
+ program.name("emulate").description("Local drop-in replacement services for CI and no-network sandboxes").version(pkg2.version);
2941
+ program.command("start", { isDefault: true }).description("Start the emulator server").option("-p, --port <port>", "Base port", defaultPort).option("-s, --service <services>", "Comma-separated services to enable").option("--seed <file>", "Path to seed config file").option("--base-url <url>", "Override advertised base URL (supports {service} template)").option("--portless", "Serve over HTTPS via portless (auto-registers aliases)").addHelpText(
2942
+ "after",
2943
+ `
2944
+
2945
+ Control plane (under /_emulate on each service):
2946
+ GET /_emulate HTML landing page
2947
+ GET /_emulate/manifest machine-readable service manifest
2948
+ GET /_emulate/quickstart copy/paste getting-started snippet
2949
+ GET /_emulate/specs spec sources and coverage status
2950
+ GET /_emulate/coverage per-operation coverage and summary
2951
+ GET /_emulate/connections copyable SDK, CLI, env, and curl snippets
2952
+ GET /_emulate/openapi OpenAPI document (when supported)
2953
+ GET /_emulate/graphql GraphQL surface (when supported)
2954
+ GET /_emulate/mcp MCP surface (when supported)
2955
+ GET /_emulate/state current emulator state
2956
+ GET /_emulate/ledger request ledger (DELETE to clear)
2957
+ GET /_emulate/logs webhook deliveries and recent requests
2958
+ POST /_emulate/instances create an instance
2959
+ POST /_emulate/seed seed state
2960
+ POST /_emulate/reset reset state
2961
+ POST /_emulate/credentials mint a credential (bearer token, API key, or
2962
+ OAuth client, depending on the service's auth)
2963
+
2964
+ Global catalog:
2965
+ GET /_emulate/services machine-readable catalog of every hosted service
2966
+
2967
+ Use /_emulate/manifest and /_emulate/coverage to discover supported surfaces
2968
+ and honest coverage, /_emulate/credentials to create credentials,
2969
+ /_emulate/seed to load fixtures, and /_emulate/ledger to validate API calls.
2970
+
2971
+ Hosted services:
2972
+ All 13 services are available on emulators.dev: github, vercel, google, okta,
2973
+ microsoft, spotify, slack, apple, aws, resend, stripe, mongoatlas, clerk,
2974
+ x, workos, autumn.
2975
+ Service host: <service>.emulators.dev (useful without an instance; serves
2976
+ a service-level /_emulate control plane)
2977
+ Instance host: <service>.<instance>.emulators.dev
2978
+ Local/path form: <origin>/<service>/<instance>
2979
+
2980
+ The apex emulators.dev is the catalog landing page that lists every emulator
2981
+ and links to its host. Per-service docs live at https://docs.emulators.dev/
2982
+ <service>.
2983
+ `
2984
+ ).action(async (opts) => {
2985
+ const port = parseInt(opts.port, 10);
2986
+ if (Number.isNaN(port) || port < 1 || port > 65535) {
2987
+ console.error(`Invalid port: ${opts.port}`);
2988
+ process.exit(1);
2989
+ }
2990
+ await startCommand({
2991
+ port,
2992
+ service: opts.service,
2993
+ seed: opts.seed,
2994
+ baseUrl: opts.baseUrl,
2995
+ portless: opts.portless
2996
+ });
2997
+ });
2998
+ program.command("init").description("Generate a starter config file").option("-s, --service <service>", "Service to generate config for", "all").action((opts) => {
2999
+ initCommand({ service: opts.service });
3000
+ });
3001
+ program.command("list").alias("list-services").description("List available services").action(() => {
3002
+ listCommand();
3003
+ });
3004
+ program.parse();
3005
+ //# sourceMappingURL=index.js.map