emulate 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js ADDED
@@ -0,0 +1,713 @@
1
+ import {
2
+ importPKCS8,
3
+ jwtVerify
4
+ } from "./chunk-D6EKRYGP.js";
5
+ import {
6
+ Hono,
7
+ cors
8
+ } from "./chunk-TEPNEZ63.js";
9
+
10
+ // ../@emulators/core/dist/index.js
11
+ import { createHmac } from "crypto";
12
+ import { readFileSync } from "fs";
13
+ import { fileURLToPath } from "url";
14
+ import { dirname, join } from "path";
15
+ var Collection = class {
16
+ constructor(indexFields = []) {
17
+ this.indexFields = indexFields;
18
+ this.fieldNames = indexFields.map(String).sort();
19
+ for (const field of indexFields) {
20
+ this.indexes.set(String(field), /* @__PURE__ */ new Map());
21
+ }
22
+ }
23
+ items = /* @__PURE__ */ new Map();
24
+ indexes = /* @__PURE__ */ new Map();
25
+ autoId = 1;
26
+ fieldNames;
27
+ addToIndex(item) {
28
+ for (const field of this.indexFields) {
29
+ const value = item[field];
30
+ if (value === void 0 || value === null) continue;
31
+ const indexMap = this.indexes.get(String(field));
32
+ const key = String(value);
33
+ if (!indexMap.has(key)) {
34
+ indexMap.set(key, /* @__PURE__ */ new Set());
35
+ }
36
+ indexMap.get(key).add(item.id);
37
+ }
38
+ }
39
+ removeFromIndex(item) {
40
+ for (const field of this.indexFields) {
41
+ const value = item[field];
42
+ if (value === void 0 || value === null) continue;
43
+ const indexMap = this.indexes.get(String(field));
44
+ const key = String(value);
45
+ indexMap.get(key)?.delete(item.id);
46
+ }
47
+ }
48
+ insert(data) {
49
+ const now = (/* @__PURE__ */ new Date()).toISOString();
50
+ const explicitId = data.id != null && data.id > 0 ? data.id : void 0;
51
+ const id = explicitId ?? this.autoId++;
52
+ if (id >= this.autoId) {
53
+ this.autoId = id + 1;
54
+ }
55
+ const item = {
56
+ ...data,
57
+ id,
58
+ created_at: now,
59
+ updated_at: now
60
+ };
61
+ this.items.set(id, item);
62
+ this.addToIndex(item);
63
+ return item;
64
+ }
65
+ get(id) {
66
+ return this.items.get(id);
67
+ }
68
+ findBy(field, value) {
69
+ if (this.indexes.has(String(field))) {
70
+ const ids = this.indexes.get(String(field)).get(String(value));
71
+ if (!ids) return [];
72
+ return Array.from(ids).map((id) => this.items.get(id)).filter(Boolean);
73
+ }
74
+ return this.all().filter((item) => item[field] === value);
75
+ }
76
+ findOneBy(field, value) {
77
+ return this.findBy(field, value)[0];
78
+ }
79
+ update(id, data) {
80
+ const existing = this.items.get(id);
81
+ if (!existing) return void 0;
82
+ this.removeFromIndex(existing);
83
+ const updated = {
84
+ ...existing,
85
+ ...data,
86
+ id,
87
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
88
+ };
89
+ this.items.set(id, updated);
90
+ this.addToIndex(updated);
91
+ return updated;
92
+ }
93
+ delete(id) {
94
+ const existing = this.items.get(id);
95
+ if (!existing) return false;
96
+ this.removeFromIndex(existing);
97
+ return this.items.delete(id);
98
+ }
99
+ all() {
100
+ return Array.from(this.items.values());
101
+ }
102
+ query(options = {}) {
103
+ let results = this.all();
104
+ if (options.filter) {
105
+ results = results.filter(options.filter);
106
+ }
107
+ const total_count = results.length;
108
+ if (options.sort) {
109
+ results.sort(options.sort);
110
+ }
111
+ const page = options.page ?? 1;
112
+ const per_page = Math.min(options.per_page ?? 30, 100);
113
+ const start = (page - 1) * per_page;
114
+ const paged = results.slice(start, start + per_page);
115
+ return {
116
+ items: paged,
117
+ total_count,
118
+ page,
119
+ per_page,
120
+ has_next: start + per_page < total_count,
121
+ has_prev: page > 1
122
+ };
123
+ }
124
+ count(filter) {
125
+ if (!filter) return this.items.size;
126
+ return this.all().filter(filter).length;
127
+ }
128
+ clear() {
129
+ this.items.clear();
130
+ for (const indexMap of this.indexes.values()) {
131
+ indexMap.clear();
132
+ }
133
+ this.autoId = 1;
134
+ }
135
+ };
136
+ var Store = class {
137
+ collections = /* @__PURE__ */ new Map();
138
+ _data = /* @__PURE__ */ new Map();
139
+ collection(name, indexFields = []) {
140
+ const existing = this.collections.get(name);
141
+ if (existing) {
142
+ if (indexFields.length > 0) {
143
+ const requested = indexFields.map(String).sort();
144
+ if (existing.fieldNames.length !== requested.length || existing.fieldNames.some((f, i) => f !== requested[i])) {
145
+ throw new Error(
146
+ `Collection "${name}" already exists with indexes [${existing.fieldNames}] but was requested with [${requested}]`
147
+ );
148
+ }
149
+ }
150
+ return existing;
151
+ }
152
+ const col = new Collection(indexFields);
153
+ this.collections.set(name, col);
154
+ return col;
155
+ }
156
+ getData(key) {
157
+ return this._data.get(key);
158
+ }
159
+ setData(key, value) {
160
+ this._data.set(key, value);
161
+ }
162
+ reset() {
163
+ for (const collection of this.collections.values()) {
164
+ collection.clear();
165
+ }
166
+ this._data.clear();
167
+ }
168
+ };
169
+ var MAX_DELIVERIES = 1e3;
170
+ var WebhookDispatcher = class {
171
+ subscriptions = [];
172
+ deliveries = [];
173
+ subscriptionIdCounter = 1;
174
+ deliveryIdCounter = 1;
175
+ register(sub) {
176
+ const { id: explicitId, ...rest } = sub;
177
+ const id = explicitId !== void 0 ? explicitId : this.subscriptionIdCounter++;
178
+ if (id >= this.subscriptionIdCounter) {
179
+ this.subscriptionIdCounter = id + 1;
180
+ }
181
+ const subscription = { ...rest, id };
182
+ this.subscriptions.push(subscription);
183
+ return subscription;
184
+ }
185
+ unregister(id) {
186
+ const idx = this.subscriptions.findIndex((s) => s.id === id);
187
+ if (idx === -1) return false;
188
+ this.subscriptions.splice(idx, 1);
189
+ return true;
190
+ }
191
+ getSubscription(id) {
192
+ return this.subscriptions.find((s) => s.id === id);
193
+ }
194
+ getSubscriptions(owner, repo) {
195
+ return this.subscriptions.filter((s) => {
196
+ if (owner && s.owner !== owner) return false;
197
+ if (repo !== void 0 && s.repo !== repo) return false;
198
+ return true;
199
+ });
200
+ }
201
+ updateSubscription(id, data) {
202
+ const sub = this.subscriptions.find((s) => s.id === id);
203
+ if (!sub) return void 0;
204
+ Object.assign(sub, data);
205
+ return sub;
206
+ }
207
+ async dispatch(event, action, payload, owner, repo) {
208
+ const matchingSubs = this.subscriptions.filter((s) => {
209
+ if (!s.active) return false;
210
+ if (s.owner !== owner) return false;
211
+ if (repo !== void 0) {
212
+ if (s.repo !== repo) return false;
213
+ } else if (s.repo !== void 0) {
214
+ return false;
215
+ }
216
+ return event === "ping" || s.events.includes("*") || s.events.includes(event);
217
+ });
218
+ for (const sub of matchingSubs) {
219
+ const delivery = {
220
+ id: this.deliveryIdCounter++,
221
+ hook_id: sub.id,
222
+ event,
223
+ action,
224
+ payload,
225
+ status_code: null,
226
+ delivered_at: (/* @__PURE__ */ new Date()).toISOString(),
227
+ duration: null,
228
+ success: false
229
+ };
230
+ const body = JSON.stringify(payload);
231
+ const signatureHeaders = {};
232
+ if (sub.secret) {
233
+ const hmac = createHmac("sha256", sub.secret).update(body).digest("hex");
234
+ signatureHeaders["X-Hub-Signature-256"] = `sha256=${hmac}`;
235
+ }
236
+ try {
237
+ const start = Date.now();
238
+ const response = await fetch(sub.url, {
239
+ method: "POST",
240
+ headers: {
241
+ "Content-Type": "application/json",
242
+ "X-GitHub-Event": event,
243
+ "X-GitHub-Delivery": String(delivery.id),
244
+ ...signatureHeaders
245
+ },
246
+ body,
247
+ signal: AbortSignal.timeout(1e4)
248
+ });
249
+ delivery.duration = Date.now() - start;
250
+ delivery.status_code = response.status;
251
+ delivery.success = response.ok;
252
+ } catch {
253
+ delivery.duration = 0;
254
+ delivery.success = false;
255
+ }
256
+ this.deliveries.push(delivery);
257
+ if (this.deliveries.length > MAX_DELIVERIES) {
258
+ this.deliveries.splice(0, this.deliveries.length - MAX_DELIVERIES);
259
+ }
260
+ }
261
+ }
262
+ getDeliveries(hookId) {
263
+ if (hookId !== void 0) {
264
+ return this.deliveries.filter((d) => d.hook_id === hookId);
265
+ }
266
+ return [...this.deliveries];
267
+ }
268
+ clear() {
269
+ this.subscriptions.length = 0;
270
+ this.deliveries.length = 0;
271
+ this.subscriptionIdCounter = 1;
272
+ this.deliveryIdCounter = 1;
273
+ }
274
+ };
275
+ var DEFAULT_DOCS_URL = "https://emulate.dev";
276
+ function getDocsUrl(c) {
277
+ return c.get("docsUrl") ?? DEFAULT_DOCS_URL;
278
+ }
279
+ function errorStatus(err) {
280
+ if (err && typeof err === "object" && "status" in err) {
281
+ const s = err.status;
282
+ if (typeof s === "number" && Number.isFinite(s)) return s;
283
+ }
284
+ return 500;
285
+ }
286
+ function createApiErrorHandler(documentationUrl) {
287
+ return (err, c) => {
288
+ if (documentationUrl) {
289
+ c.set("docsUrl", documentationUrl);
290
+ }
291
+ const status = errorStatus(err);
292
+ const message = err instanceof Error ? err.message : "Internal Server Error";
293
+ return c.json(
294
+ {
295
+ message,
296
+ documentation_url: getDocsUrl(c)
297
+ },
298
+ status
299
+ );
300
+ };
301
+ }
302
+ function createErrorHandler(documentationUrl) {
303
+ return async (c, next) => {
304
+ if (documentationUrl) {
305
+ c.set("docsUrl", documentationUrl);
306
+ }
307
+ await next();
308
+ };
309
+ }
310
+ var errorHandler = createErrorHandler();
311
+ var isDebug = typeof process !== "undefined" && (process.env.DEBUG === "1" || process.env.DEBUG === "true" || process.env.EMULATE_DEBUG === "1");
312
+ function debug(label, ...args) {
313
+ if (isDebug) {
314
+ console.log(`[${label}]`, ...args);
315
+ }
316
+ }
317
+ function authMiddleware(tokens, appKeyResolver, fallbackUser) {
318
+ return async (c, next) => {
319
+ const authHeader = c.req.header("Authorization");
320
+ if (authHeader) {
321
+ const token = authHeader.replace(/^(Bearer|token)\s+/i, "").trim();
322
+ if (token.startsWith("eyJ") && appKeyResolver) {
323
+ try {
324
+ const [, payloadB64] = token.split(".");
325
+ const payload = JSON.parse(
326
+ Buffer.from(payloadB64, "base64url").toString()
327
+ );
328
+ const appId = typeof payload.iss === "string" ? parseInt(payload.iss, 10) : payload.iss;
329
+ if (typeof appId === "number" && !isNaN(appId)) {
330
+ const appInfo = appKeyResolver(appId);
331
+ if (appInfo) {
332
+ const key = await importPKCS8(appInfo.privateKey, "RS256");
333
+ await jwtVerify(token, key, { algorithms: ["RS256"] });
334
+ c.set("authApp", {
335
+ appId,
336
+ slug: appInfo.slug,
337
+ name: appInfo.name
338
+ });
339
+ }
340
+ }
341
+ } catch {
342
+ }
343
+ } else {
344
+ let user = tokens.get(token);
345
+ if (!user && fallbackUser && token.length > 0) {
346
+ debug("auth", "fallback user for unknown token", { login: fallbackUser.login, id: fallbackUser.id });
347
+ user = { login: fallbackUser.login, id: fallbackUser.id, scopes: fallbackUser.scopes };
348
+ }
349
+ if (user) {
350
+ c.set("authUser", user);
351
+ c.set("authToken", token);
352
+ c.set("authScopes", user.scopes);
353
+ }
354
+ }
355
+ }
356
+ await next();
357
+ };
358
+ }
359
+ var __dirname = dirname(fileURLToPath(import.meta.url));
360
+ var FONTS = {
361
+ "geist-sans.woff2": readFileSync(join(__dirname, "fonts", "geist-sans.woff2")),
362
+ "GeistPixel-Square.woff2": readFileSync(join(__dirname, "fonts", "GeistPixel-Square.woff2"))
363
+ };
364
+ function registerFontRoutes(app) {
365
+ app.get("/_emulate/fonts/:name", (c) => {
366
+ const name = c.req.param("name");
367
+ const buf = FONTS[name];
368
+ if (!buf) return c.notFound();
369
+ return new Response(buf, {
370
+ headers: {
371
+ "Content-Type": "font/woff2",
372
+ "Cache-Control": "public, max-age=31536000, immutable",
373
+ "Access-Control-Allow-Origin": "*"
374
+ }
375
+ });
376
+ });
377
+ }
378
+ function createServer(plugin, options = {}) {
379
+ const port = options.port ?? 4e3;
380
+ const baseUrl = options.baseUrl ?? `http://localhost:${port}`;
381
+ const app = new Hono();
382
+ const store = new Store();
383
+ const webhooks = new WebhookDispatcher();
384
+ const tokenMap = /* @__PURE__ */ new Map();
385
+ if (options.tokens) {
386
+ for (const [token, user] of Object.entries(options.tokens)) {
387
+ tokenMap.set(token, {
388
+ login: user.login,
389
+ id: user.id,
390
+ scopes: user.scopes ?? ["repo", "user", "admin:org", "admin:repo_hook"]
391
+ });
392
+ }
393
+ }
394
+ const docsUrl = options.docsUrl ?? `https://emulate.dev/${plugin.name}`;
395
+ registerFontRoutes(app);
396
+ app.onError(createApiErrorHandler(docsUrl));
397
+ app.use("*", cors());
398
+ app.use("*", createErrorHandler(docsUrl));
399
+ app.use("*", authMiddleware(tokenMap, options.appKeyResolver, options.fallbackUser));
400
+ const rateLimitCounters = /* @__PURE__ */ new Map();
401
+ let lastPruneAt = Math.floor(Date.now() / 1e3);
402
+ app.use("*", async (c, next) => {
403
+ const token = c.get("authToken") ?? "__anonymous__";
404
+ const now = Math.floor(Date.now() / 1e3);
405
+ if (now - lastPruneAt > 3600) {
406
+ for (const [key, val] of rateLimitCounters) {
407
+ if (val.resetAt <= now) rateLimitCounters.delete(key);
408
+ }
409
+ lastPruneAt = now;
410
+ }
411
+ let counter = rateLimitCounters.get(token);
412
+ if (!counter || counter.resetAt <= now) {
413
+ counter = { remaining: 5e3, resetAt: now + 3600 };
414
+ rateLimitCounters.set(token, counter);
415
+ }
416
+ counter.remaining = Math.max(0, counter.remaining - 1);
417
+ c.header("X-RateLimit-Limit", "5000");
418
+ c.header("X-RateLimit-Remaining", String(counter.remaining));
419
+ c.header("X-RateLimit-Reset", String(counter.resetAt));
420
+ c.header("X-RateLimit-Resource", "core");
421
+ if (counter.remaining === 0) {
422
+ return c.json(
423
+ {
424
+ message: "API rate limit exceeded",
425
+ documentation_url: docsUrl
426
+ },
427
+ 403
428
+ );
429
+ }
430
+ await next();
431
+ });
432
+ plugin.register(app, store, webhooks, baseUrl, tokenMap);
433
+ app.notFound(
434
+ (c) => c.json(
435
+ {
436
+ message: "Not Found",
437
+ documentation_url: docsUrl
438
+ },
439
+ 404
440
+ )
441
+ );
442
+ return { app, store, webhooks, port, baseUrl, tokenMap };
443
+ }
444
+
445
+ // src/registry.ts
446
+ var SERVICE_REGISTRY = {
447
+ vercel: {
448
+ label: "Vercel REST API emulator",
449
+ endpoints: "projects, deployments, domains, env vars, users, teams, file uploads, protection bypass",
450
+ async load() {
451
+ const mod = await import("./dist-JYDZIVC6.js");
452
+ return { plugin: mod.vercelPlugin, seedFromConfig: mod.seedFromConfig };
453
+ },
454
+ defaultFallback(cfg) {
455
+ const firstLogin = cfg?.users?.[0]?.username ?? "admin";
456
+ return { login: firstLogin, id: 1, scopes: [] };
457
+ },
458
+ initConfig: {
459
+ vercel: {
460
+ users: [{ username: "developer", name: "Developer", email: "dev@example.com" }],
461
+ teams: [{ slug: "my-team", name: "My Team" }],
462
+ projects: [{ name: "my-app", team: "my-team", framework: "nextjs" }],
463
+ integrations: [{
464
+ client_id: "oac_example_client_id",
465
+ client_secret: "example_client_secret",
466
+ name: "My Vercel App",
467
+ redirect_uris: ["http://localhost:3000/api/auth/callback/vercel"]
468
+ }]
469
+ }
470
+ }
471
+ },
472
+ github: {
473
+ label: "GitHub REST API emulator",
474
+ endpoints: "users, repos, issues, PRs, comments, reviews, labels, milestones, branches, git data, orgs, teams, releases, webhooks, search, actions, checks, rate limit",
475
+ async load() {
476
+ const mod = await import("./dist-OCDKIMRJ.js");
477
+ return {
478
+ plugin: mod.githubPlugin,
479
+ seedFromConfig: mod.seedFromConfig,
480
+ createAppKeyResolver(store) {
481
+ return (appId) => {
482
+ try {
483
+ const gh = mod.getGitHubStore(store);
484
+ const ghApp = gh.apps.all().find((a) => a.app_id === appId);
485
+ if (!ghApp) return null;
486
+ return { privateKey: ghApp.private_key, slug: ghApp.slug, name: ghApp.name };
487
+ } catch {
488
+ return null;
489
+ }
490
+ };
491
+ }
492
+ };
493
+ },
494
+ defaultFallback(cfg) {
495
+ const firstLogin = cfg?.users?.[0]?.login ?? "admin";
496
+ return { login: firstLogin, id: 1, scopes: ["repo", "user", "admin:org", "admin:repo_hook"] };
497
+ },
498
+ initConfig: {
499
+ github: {
500
+ users: [{
501
+ login: "octocat",
502
+ name: "The Octocat",
503
+ email: "octocat@github.com",
504
+ bio: "I am the Octocat",
505
+ company: "GitHub",
506
+ location: "San Francisco"
507
+ }],
508
+ orgs: [{ login: "my-org", name: "My Organization", description: "A test organization" }],
509
+ repos: [
510
+ { owner: "octocat", name: "hello-world", description: "My first repository", language: "JavaScript", topics: ["hello", "world"], auto_init: true },
511
+ { owner: "my-org", name: "org-repo", description: "An organization repository", language: "TypeScript", auto_init: true }
512
+ ],
513
+ oauth_apps: [{
514
+ client_id: "Iv1.example_client_id",
515
+ client_secret: "example_client_secret",
516
+ name: "My App",
517
+ redirect_uris: ["http://localhost:3000/api/auth/callback/github"]
518
+ }]
519
+ }
520
+ }
521
+ },
522
+ google: {
523
+ label: "Google OAuth 2.0 / OpenID Connect + Gmail, Calendar, and Drive emulator",
524
+ endpoints: "OAuth authorize, token exchange, userinfo, OIDC discovery, token revocation, Gmail messages/drafts/threads/labels/history/settings, Calendar lists/events/freebusy, Drive files/uploads",
525
+ async load() {
526
+ const mod = await import("./dist-BKXG6HVH.js");
527
+ return { plugin: mod.googlePlugin, seedFromConfig: mod.seedFromConfig };
528
+ },
529
+ defaultFallback(cfg) {
530
+ const firstEmail = cfg?.users?.[0]?.email ?? "testuser@gmail.com";
531
+ return { login: firstEmail, id: 1, scopes: ["openid", "email", "profile"] };
532
+ },
533
+ initConfig: {
534
+ google: {
535
+ users: [{ email: "testuser@example.com", name: "Test User", picture: "https://lh3.googleusercontent.com/a/default-user", email_verified: true }],
536
+ oauth_clients: [{
537
+ client_id: "example-client-id.apps.googleusercontent.com",
538
+ client_secret: "GOCSPX-example_secret",
539
+ name: "Code App (Google)",
540
+ redirect_uris: ["http://localhost:3000/api/auth/callback/google"]
541
+ }],
542
+ labels: [{ id: "Label_ops", user_email: "testuser@example.com", name: "Ops/Review", color_background: "#DDEEFF", color_text: "#111111" }],
543
+ messages: [{
544
+ id: "msg_welcome",
545
+ user_email: "testuser@example.com",
546
+ from: "welcome@example.com",
547
+ to: "testuser@example.com",
548
+ subject: "Welcome to the Gmail emulator",
549
+ body_text: "You can now test Gmail, Calendar, and Drive flows locally.",
550
+ label_ids: ["INBOX", "UNREAD", "CATEGORY_UPDATES"],
551
+ date: "2025-01-04T10:00:00.000Z"
552
+ }],
553
+ calendars: [{ id: "primary", user_email: "testuser@example.com", summary: "testuser@example.com", primary: true, selected: true, time_zone: "UTC" }],
554
+ calendar_events: [{
555
+ id: "evt_kickoff",
556
+ user_email: "testuser@example.com",
557
+ calendar_id: "primary",
558
+ summary: "Project Kickoff",
559
+ start_date_time: "2025-01-10T09:00:00.000Z",
560
+ end_date_time: "2025-01-10T09:30:00.000Z"
561
+ }],
562
+ drive_items: [{ id: "drv_docs", user_email: "testuser@example.com", name: "Docs", mime_type: "application/vnd.google-apps.folder", parent_ids: ["root"] }]
563
+ }
564
+ }
565
+ },
566
+ slack: {
567
+ label: "Slack API emulator",
568
+ endpoints: "auth, chat, conversations, users, reactions, team, OAuth, incoming webhooks",
569
+ async load() {
570
+ const mod = await import("./dist-UZSUUE3Y.js");
571
+ return { plugin: mod.slackPlugin, seedFromConfig: mod.seedFromConfig };
572
+ },
573
+ defaultFallback() {
574
+ return { login: "U000000001", id: 1, scopes: ["chat:write", "channels:read", "users:read", "reactions:write"] };
575
+ },
576
+ initConfig: {
577
+ slack: {
578
+ team: { name: "My Workspace", domain: "my-workspace" },
579
+ users: [{ name: "developer", real_name: "Developer", email: "dev@example.com" }],
580
+ channels: [{ name: "general", topic: "General discussion" }, { name: "random", topic: "Random stuff" }],
581
+ bots: [{ name: "my-bot" }],
582
+ oauth_apps: [{
583
+ client_id: "12345.67890",
584
+ client_secret: "example_client_secret",
585
+ name: "My Slack App",
586
+ redirect_uris: ["http://localhost:3000/api/auth/callback/slack"]
587
+ }]
588
+ }
589
+ }
590
+ },
591
+ apple: {
592
+ label: "Apple Sign In / OAuth emulator",
593
+ endpoints: "OAuth authorize, token exchange, JWKS",
594
+ async load() {
595
+ const mod = await import("./dist-O4KFIBVU.js");
596
+ return { plugin: mod.applePlugin, seedFromConfig: mod.seedFromConfig };
597
+ },
598
+ defaultFallback(cfg) {
599
+ const firstEmail = cfg?.users?.[0]?.email ?? "testuser@icloud.com";
600
+ return { login: firstEmail, id: 1, scopes: ["openid", "email", "name"] };
601
+ },
602
+ initConfig: {
603
+ apple: {
604
+ users: [{ email: "testuser@icloud.com", name: "Test User" }],
605
+ oauth_clients: [{
606
+ client_id: "com.example.app",
607
+ team_id: "TEAM001",
608
+ name: "My Apple App",
609
+ redirect_uris: ["http://localhost:3000/api/auth/callback/apple"]
610
+ }]
611
+ }
612
+ }
613
+ },
614
+ microsoft: {
615
+ label: "Microsoft Entra ID OAuth 2.0 / OpenID Connect emulator",
616
+ endpoints: "OAuth authorize, token exchange, userinfo, OIDC discovery, Graph /me, logout, token revocation",
617
+ async load() {
618
+ const mod = await import("./dist-DSSB3LYT.js");
619
+ return { plugin: mod.microsoftPlugin, seedFromConfig: mod.seedFromConfig };
620
+ },
621
+ defaultFallback(cfg) {
622
+ const firstEmail = cfg?.users?.[0]?.email ?? "testuser@outlook.com";
623
+ return { login: firstEmail, id: 1, scopes: ["openid", "email", "profile", "User.Read"] };
624
+ },
625
+ initConfig: {
626
+ microsoft: {
627
+ users: [{ email: "testuser@outlook.com", name: "Test User" }],
628
+ oauth_clients: [{
629
+ client_id: "example-client-id",
630
+ client_secret: "example-client-secret",
631
+ name: "My Microsoft App",
632
+ redirect_uris: ["http://localhost:3000/api/auth/callback/microsoft-entra-id"]
633
+ }]
634
+ }
635
+ }
636
+ },
637
+ aws: {
638
+ label: "AWS cloud service emulator",
639
+ endpoints: "S3 (buckets, objects), SQS (queues, messages), IAM (users, roles, access keys), STS (assume role, caller identity)",
640
+ async load() {
641
+ const mod = await import("./dist-VVXVP5EZ.js");
642
+ return { plugin: mod.awsPlugin, seedFromConfig: mod.seedFromConfig };
643
+ },
644
+ defaultFallback() {
645
+ return { login: "admin", id: 1, scopes: ["s3:*", "sqs:*", "iam:*", "sts:*"] };
646
+ },
647
+ initConfig: {
648
+ aws: {
649
+ region: "us-east-1",
650
+ s3: { buckets: [{ name: "my-app-bucket" }, { name: "my-app-uploads" }] },
651
+ sqs: { queues: [{ name: "my-app-events" }, { name: "my-app-dlq" }] },
652
+ iam: {
653
+ users: [{ user_name: "developer", create_access_key: true }],
654
+ roles: [{ role_name: "lambda-execution-role", description: "Role for Lambda function execution" }]
655
+ }
656
+ }
657
+ }
658
+ }
659
+ };
660
+
661
+ // src/api.ts
662
+ import { serve } from "@hono/node-server";
663
+ async function createEmulator(options) {
664
+ const { service, port = 4e3, seed: seedConfig } = options;
665
+ const entry = SERVICE_REGISTRY[service];
666
+ if (!entry) {
667
+ throw new Error(`Unknown service: ${service}`);
668
+ }
669
+ const loaded = await entry.load();
670
+ const tokens = {};
671
+ if (seedConfig?.tokens) {
672
+ let tokenId = 100;
673
+ for (const [token, user] of Object.entries(seedConfig.tokens)) {
674
+ tokens[token] = { login: user.login, id: tokenId++, scopes: user.scopes };
675
+ }
676
+ } else {
677
+ tokens["test_token_admin"] = { login: "admin", id: 2, scopes: ["repo", "user", "admin:org", "admin:repo_hook"] };
678
+ }
679
+ const baseUrl = `http://localhost:${port}`;
680
+ let cachedResolver;
681
+ const appKeyResolver = loaded.createAppKeyResolver ? (appId) => cachedResolver(appId) : void 0;
682
+ const svcSeedConfig = seedConfig?.[service];
683
+ const fallbackUser = entry.defaultFallback(svcSeedConfig);
684
+ const { app, store } = createServer(loaded.plugin, { port, baseUrl, tokens, appKeyResolver, fallbackUser });
685
+ cachedResolver = loaded.createAppKeyResolver?.(store);
686
+ const seed = () => {
687
+ loaded.plugin.seed?.(store, baseUrl);
688
+ if (svcSeedConfig && loaded.seedFromConfig) {
689
+ loaded.seedFromConfig(store, baseUrl, svcSeedConfig);
690
+ }
691
+ };
692
+ seed();
693
+ const httpServer = serve({ fetch: app.fetch, port });
694
+ return {
695
+ url: baseUrl,
696
+ reset() {
697
+ store.reset();
698
+ seed();
699
+ },
700
+ close() {
701
+ return new Promise((resolve, reject) => {
702
+ httpServer.close((err) => {
703
+ if (err) reject(err);
704
+ else resolve();
705
+ });
706
+ });
707
+ }
708
+ };
709
+ }
710
+ export {
711
+ createEmulator
712
+ };
713
+ //# sourceMappingURL=api.js.map