api-emulator 0.5.1 → 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.
package/dist/api.js CHANGED
@@ -1,5 +1,23 @@
1
1
  // src/external-plugin-adapter.ts
2
2
  import { isAbsolute, resolve } from "path";
3
+
4
+ // src/plugin-manifest.ts
5
+ function readPluginManifest(mod) {
6
+ return mod.manifest ?? {};
7
+ }
8
+ function validatePluginManifest(manifest, pluginName) {
9
+ if (manifest.name && manifest.name !== pluginName) {
10
+ throw new Error(`Plugin manifest name "${manifest.name}" does not match plugin name "${pluginName}"`);
11
+ }
12
+ return {
13
+ ...manifest,
14
+ label: manifest.label ?? `${pluginName} (external plugin)`,
15
+ endpoints: manifest.endpoints ?? "",
16
+ initConfig: manifest.initConfig ?? {}
17
+ };
18
+ }
19
+
20
+ // src/external-plugin-adapter.ts
3
21
  async function loadExternalPluginModule(specifier) {
4
22
  const modulePath = specifier.startsWith(".") || isAbsolute(specifier) ? resolve(specifier) : specifier;
5
23
  const mod = await import(modulePath);
@@ -8,10 +26,12 @@ async function loadExternalPluginModule(specifier) {
8
26
  throw new Error(`Plugin "${specifier}" must export a ServicePlugin (as "plugin" or default export)`);
9
27
  }
10
28
  const name = plugin.name;
29
+ const manifest = validatePluginManifest(readPluginManifest(mod), name);
11
30
  return {
12
31
  name,
13
- label: mod.label ?? `${name} (external plugin)`,
14
- endpoints: mod.endpoints ?? "",
32
+ label: manifest.label,
33
+ endpoints: manifest.endpoints,
34
+ manifest,
15
35
  async load() {
16
36
  return {
17
37
  plugin,
@@ -19,428 +39,38 @@ async function loadExternalPluginModule(specifier) {
19
39
  };
20
40
  },
21
41
  defaultFallback: mod.defaultFallback ?? (() => ({ login: "admin", id: 1, scopes: [] })),
22
- initConfig: mod.initConfig ?? {}
42
+ initConfig: manifest.initConfig
23
43
  };
24
44
  }
25
45
 
26
- // src/default-plugin-catalog.ts
27
- var DEFAULT_PLUGIN_REGISTRY = {
28
- vercel: {
29
- name: "vercel",
30
- label: "Vercel REST API emulator",
31
- endpoints: "projects, deployments, domains, env vars, users, teams, file uploads, protection bypass",
32
- async load() {
33
- const mod = await import("@emulators/vercel");
34
- return { plugin: mod.vercelPlugin, seedFromConfig: mod.seedFromConfig };
35
- },
36
- defaultFallback(cfg) {
37
- const firstLogin = cfg?.users?.[0]?.username ?? "admin";
38
- return { login: firstLogin, id: 1, scopes: [] };
39
- },
40
- initConfig: {
41
- vercel: {
42
- users: [{ username: "developer", name: "Developer", email: "dev@example.com" }],
43
- teams: [{ slug: "my-team", name: "My Team" }],
44
- projects: [{ name: "my-app", team: "my-team", framework: "nextjs" }],
45
- integrations: [
46
- {
47
- client_id: "oac_example_client_id",
48
- client_secret: "example_client_secret",
49
- name: "My Vercel App",
50
- redirect_uris: ["http://localhost:3000/api/auth/callback/vercel"]
51
- }
52
- ]
53
- }
54
- }
55
- },
56
- github: {
57
- name: "github",
58
- label: "GitHub REST API emulator",
59
- endpoints: "users, repos, issues, PRs, comments, reviews, labels, milestones, branches, git data, orgs, teams, releases, webhooks, search, actions, checks, rate limit",
60
- async load() {
61
- const mod = await import("@emulators/github");
62
- return {
63
- plugin: mod.githubPlugin,
64
- seedFromConfig: mod.seedFromConfig,
65
- createAppKeyResolver(store) {
66
- return (appId) => {
67
- try {
68
- const gh = mod.getGitHubStore(store);
69
- const ghApp = gh.apps.all().find((a) => a.app_id === appId);
70
- if (!ghApp) return null;
71
- return { privateKey: ghApp.private_key, slug: ghApp.slug, name: ghApp.name };
72
- } catch {
73
- return null;
74
- }
75
- };
76
- }
77
- };
78
- },
79
- defaultFallback(cfg) {
80
- const firstLogin = cfg?.users?.[0]?.login ?? "admin";
81
- return { login: firstLogin, id: 1, scopes: ["repo", "user", "admin:org", "admin:repo_hook"] };
82
- },
83
- initConfig: {
84
- github: {
85
- users: [
86
- {
87
- login: "octocat",
88
- name: "The Octocat",
89
- email: "octocat@github.com",
90
- bio: "I am the Octocat",
91
- company: "GitHub",
92
- location: "San Francisco"
93
- }
94
- ],
95
- orgs: [{ login: "my-org", name: "My Organization", description: "A test organization" }],
96
- repos: [
97
- {
98
- owner: "octocat",
99
- name: "hello-world",
100
- description: "My first repository",
101
- language: "JavaScript",
102
- topics: ["hello", "world"],
103
- auto_init: true
104
- },
105
- {
106
- owner: "my-org",
107
- name: "org-repo",
108
- description: "An organization repository",
109
- language: "TypeScript",
110
- auto_init: true
111
- }
112
- ],
113
- oauth_apps: [
114
- {
115
- client_id: "Iv1.example_client_id",
116
- client_secret: "example_client_secret",
117
- name: "My App",
118
- redirect_uris: ["http://localhost:3000/api/auth/callback/github"]
119
- }
120
- ]
121
- }
122
- }
123
- },
124
- google: {
125
- name: "google",
126
- label: "Google OAuth 2.0 / OpenID Connect + Gmail, Calendar, and Drive emulator",
127
- endpoints: "OAuth authorize, token exchange, userinfo, OIDC discovery, token revocation, Gmail messages/drafts/threads/labels/history/settings, Calendar lists/events/freebusy, Drive files/uploads",
128
- async load() {
129
- const mod = await import("@emulators/google");
130
- return { plugin: mod.googlePlugin, seedFromConfig: mod.seedFromConfig };
131
- },
132
- defaultFallback(cfg) {
133
- const firstEmail = cfg?.users?.[0]?.email ?? "testuser@gmail.com";
134
- return { login: firstEmail, id: 1, scopes: ["openid", "email", "profile"] };
135
- },
136
- initConfig: {
137
- google: {
138
- users: [
139
- {
140
- email: "testuser@example.com",
141
- name: "Test User",
142
- picture: "https://lh3.googleusercontent.com/a/default-user",
143
- email_verified: true
144
- }
145
- ],
146
- oauth_clients: [
147
- {
148
- client_id: "example-client-id.apps.googleusercontent.com",
149
- client_secret: "GOCSPX-example_secret",
150
- name: "Code App (Google)",
151
- redirect_uris: ["http://localhost:3000/api/auth/callback/google"]
152
- }
153
- ],
154
- labels: [
155
- {
156
- id: "Label_ops",
157
- user_email: "testuser@example.com",
158
- name: "Ops/Review",
159
- color_background: "#DDEEFF",
160
- color_text: "#111111"
161
- }
162
- ],
163
- messages: [
164
- {
165
- id: "msg_welcome",
166
- user_email: "testuser@example.com",
167
- from: "welcome@example.com",
168
- to: "testuser@example.com",
169
- subject: "Welcome to the Gmail emulator",
170
- body_text: "You can now test Gmail, Calendar, and Drive flows locally.",
171
- label_ids: ["INBOX", "UNREAD", "CATEGORY_UPDATES"],
172
- date: "2025-01-04T10:00:00.000Z"
173
- }
174
- ],
175
- calendars: [
176
- {
177
- id: "primary",
178
- user_email: "testuser@example.com",
179
- summary: "testuser@example.com",
180
- primary: true,
181
- selected: true,
182
- time_zone: "UTC"
183
- }
184
- ],
185
- calendar_events: [
186
- {
187
- id: "evt_kickoff",
188
- user_email: "testuser@example.com",
189
- calendar_id: "primary",
190
- summary: "Project Kickoff",
191
- start_date_time: "2025-01-10T09:00:00.000Z",
192
- end_date_time: "2025-01-10T09:30:00.000Z"
193
- }
194
- ],
195
- drive_items: [
196
- {
197
- id: "drv_docs",
198
- user_email: "testuser@example.com",
199
- name: "Docs",
200
- mime_type: "application/vnd.google-apps.folder",
201
- parent_ids: ["root"]
202
- }
203
- ]
204
- }
205
- }
206
- },
207
- slack: {
208
- name: "slack",
209
- label: "Slack API emulator",
210
- endpoints: "auth, chat, conversations, users, reactions, team, OAuth, incoming webhooks",
211
- async load() {
212
- const mod = await import("@emulators/slack");
213
- return { plugin: mod.slackPlugin, seedFromConfig: mod.seedFromConfig };
214
- },
215
- defaultFallback() {
216
- return { login: "U000000001", id: 1, scopes: ["chat:write", "channels:read", "users:read", "reactions:write"] };
217
- },
218
- initConfig: {
219
- slack: {
220
- team: { name: "My Workspace", domain: "my-workspace" },
221
- users: [{ name: "developer", real_name: "Developer", email: "dev@example.com" }],
222
- channels: [
223
- { name: "general", topic: "General discussion" },
224
- { name: "random", topic: "Random stuff" }
225
- ],
226
- bots: [{ name: "my-bot" }],
227
- oauth_apps: [
228
- {
229
- client_id: "12345.67890",
230
- client_secret: "example_client_secret",
231
- name: "My Slack App",
232
- redirect_uris: ["http://localhost:3000/api/auth/callback/slack"]
233
- }
234
- ]
235
- }
236
- }
237
- },
238
- apple: {
239
- name: "apple",
240
- label: "Apple Sign In / OAuth emulator",
241
- endpoints: "OAuth authorize, token exchange, JWKS",
242
- async load() {
243
- const mod = await import("@emulators/apple");
244
- return { plugin: mod.applePlugin, seedFromConfig: mod.seedFromConfig };
245
- },
246
- defaultFallback(cfg) {
247
- const firstEmail = cfg?.users?.[0]?.email ?? "testuser@icloud.com";
248
- return { login: firstEmail, id: 1, scopes: ["openid", "email", "name"] };
249
- },
250
- initConfig: {
251
- apple: {
252
- users: [{ email: "testuser@icloud.com", name: "Test User" }],
253
- oauth_clients: [
254
- {
255
- client_id: "com.example.app",
256
- team_id: "TEAM001",
257
- name: "My Apple App",
258
- redirect_uris: ["http://localhost:3000/api/auth/callback/apple"]
259
- }
260
- ]
261
- }
262
- }
263
- },
264
- microsoft: {
265
- name: "microsoft",
266
- label: "Microsoft Entra ID OAuth 2.0 / OpenID Connect emulator",
267
- endpoints: "OAuth authorize, token exchange, userinfo, OIDC discovery, Graph /me, logout, token revocation",
268
- async load() {
269
- const mod = await import("@emulators/microsoft");
270
- return { plugin: mod.microsoftPlugin, seedFromConfig: mod.seedFromConfig };
271
- },
272
- defaultFallback(cfg) {
273
- const firstEmail = cfg?.users?.[0]?.email ?? "testuser@outlook.com";
274
- return { login: firstEmail, id: 1, scopes: ["openid", "email", "profile", "User.Read"] };
275
- },
276
- initConfig: {
277
- microsoft: {
278
- users: [{ email: "testuser@outlook.com", name: "Test User" }],
279
- oauth_clients: [
280
- {
281
- client_id: "example-client-id",
282
- client_secret: "example-client-secret",
283
- name: "My Microsoft App",
284
- redirect_uris: ["http://localhost:3000/api/auth/callback/microsoft-entra-id"]
285
- }
286
- ]
287
- }
288
- }
289
- },
290
- okta: {
291
- name: "okta",
292
- label: "Okta OAuth 2.0 / OpenID Connect + management API emulator",
293
- endpoints: "OIDC discovery, JWKS, OAuth authorize/token/userinfo/introspect/revoke/logout, users, groups, apps, authorization servers",
294
- async load() {
295
- const mod = await import("@emulators/okta");
296
- return { plugin: mod.oktaPlugin, seedFromConfig: mod.seedFromConfig };
297
- },
298
- defaultFallback(cfg) {
299
- const firstLogin = cfg?.users?.[0]?.login ?? cfg?.users?.[0]?.email ?? "testuser@okta.local";
300
- return { login: firstLogin, id: 1, scopes: ["openid", "profile", "email", "groups"] };
301
- },
302
- initConfig: {
303
- okta: {
304
- users: [{ login: "testuser@okta.local", email: "testuser@okta.local", first_name: "Test", last_name: "User" }],
305
- groups: [{ name: "Everyone", description: "All users", type: "BUILT_IN", okta_id: "00g_everyone" }],
306
- authorization_servers: [{ id: "default", name: "default", audiences: ["api://default"] }],
307
- oauth_clients: [
308
- {
309
- client_id: "okta-test-client",
310
- client_secret: "okta-test-secret",
311
- name: "Sample OIDC Client",
312
- redirect_uris: ["http://localhost:3000/callback"],
313
- auth_server_id: "default"
314
- }
315
- ]
316
- }
317
- }
318
- },
319
- aws: {
320
- name: "aws",
321
- label: "AWS cloud service emulator",
322
- endpoints: "S3 (buckets, objects), SQS (queues, messages), IAM (users, roles, access keys), STS (assume role, caller identity)",
323
- async load() {
324
- const mod = await import("@emulators/aws");
325
- return { plugin: mod.awsPlugin, seedFromConfig: mod.seedFromConfig };
326
- },
327
- defaultFallback() {
328
- return { login: "admin", id: 1, scopes: ["s3:*", "sqs:*", "iam:*", "sts:*"] };
329
- },
330
- initConfig: {
331
- aws: {
332
- region: "us-east-1",
333
- s3: { buckets: [{ name: "my-app-bucket" }, { name: "my-app-uploads" }] },
334
- sqs: { queues: [{ name: "my-app-events" }, { name: "my-app-dlq" }] },
335
- iam: {
336
- users: [{ user_name: "developer", create_access_key: true }],
337
- roles: [{ role_name: "lambda-execution-role", description: "Role for Lambda function execution" }]
338
- }
339
- }
340
- }
341
- },
342
- resend: {
343
- name: "resend",
344
- label: "Resend email API emulator",
345
- endpoints: "emails, domains, contacts, API keys, inbox UI",
346
- async load() {
347
- const mod = await import("@emulators/resend");
348
- return { plugin: mod.resendPlugin, seedFromConfig: mod.seedFromConfig };
349
- },
350
- defaultFallback() {
351
- return { login: "re_test_admin", id: 1, scopes: [] };
352
- },
353
- initConfig: {
354
- resend: {
355
- domains: [{ name: "example.com", region: "us-east-1" }],
356
- contacts: [{ email: "test@example.com", first_name: "Test", last_name: "User" }]
357
- }
358
- }
359
- },
360
- stripe: {
361
- name: "stripe",
362
- label: "Stripe payments emulator",
363
- endpoints: "customers, payment methods, customer sessions, payment intents, charges, products, prices, checkout sessions, webhooks",
364
- async load() {
365
- const mod = await import("@emulators/stripe");
366
- return { plugin: mod.stripePlugin, seedFromConfig: mod.seedFromConfig };
367
- },
368
- defaultFallback() {
369
- return { login: "sk_test_admin", id: 1, scopes: [] };
370
- },
371
- initConfig: {
372
- stripe: {
373
- customers: [{ email: "test@example.com", name: "Test Customer" }],
374
- products: [{ name: "Pro Plan", description: "Monthly pro subscription" }],
375
- prices: [{ product_name: "Pro Plan", currency: "usd", unit_amount: 2e3 }]
376
- }
377
- }
378
- },
379
- mongoatlas: {
380
- name: "mongoatlas",
381
- label: "MongoDB Atlas service emulator",
382
- endpoints: "Atlas Admin API v2 (projects, clusters, database users, databases, collections), Atlas Data API v1 (findOne, find, insertOne, insertMany, updateOne, updateMany, deleteOne, deleteMany, aggregate)",
383
- async load() {
384
- const mod = await import("@emulators/mongoatlas");
385
- return { plugin: mod.mongoatlasPlugin, seedFromConfig: mod.seedFromConfig };
386
- },
387
- defaultFallback() {
388
- return { login: "admin", id: 1, scopes: [] };
389
- },
390
- initConfig: {
391
- mongoatlas: {
392
- projects: [{ name: "Project0" }],
393
- clusters: [{ name: "Cluster0", project: "Project0" }],
394
- database_users: [{ username: "admin", project: "Project0" }],
395
- databases: [{ cluster: "Cluster0", name: "test", collections: ["items"] }]
396
- }
397
- }
398
- },
399
- clerk: {
400
- name: "clerk",
401
- label: "Clerk authentication and user management emulator",
402
- endpoints: "OIDC discovery, JWKS, OAuth authorize/token/userinfo, users, email addresses, organizations, memberships, invitations, sessions",
403
- async load() {
404
- const mod = await import("@emulators/clerk");
405
- return { plugin: mod.clerkPlugin, seedFromConfig: mod.seedFromConfig };
406
- },
407
- defaultFallback(cfg) {
408
- const firstEmail = cfg?.users?.[0]?.email_addresses?.[0] ?? "test@example.com";
409
- return { login: firstEmail, id: 1, scopes: [] };
410
- },
411
- initConfig: {
412
- clerk: {
413
- users: [
414
- {
415
- first_name: "Test",
416
- last_name: "User",
417
- email_addresses: ["test@example.com"],
418
- password: "clerk_test_password"
419
- }
420
- ],
421
- organizations: [
422
- {
423
- name: "My Company",
424
- slug: "my-company",
425
- members: [{ email: "test@example.com", role: "admin" }]
426
- }
427
- ],
428
- oauth_applications: [
429
- {
430
- client_id: "clerk_emulate_client",
431
- client_secret: "clerk_emulate_secret",
432
- name: "api-emulator App",
433
- redirect_uris: ["http://localhost:3000/api/auth/callback/clerk"]
434
- }
435
- ]
436
- }
437
- }
46
+ // src/plugin-lock.ts
47
+ import { existsSync, readFileSync, writeFileSync } from "fs";
48
+ import { resolve as resolve2 } from "path";
49
+ var PLUGIN_LOCK_FILE = "api-emulator.lock";
50
+ function createEmptyPluginLock() {
51
+ return { version: 1, plugins: {} };
52
+ }
53
+ function readPluginLock(cwd = process.cwd()) {
54
+ const path = resolve2(cwd, PLUGIN_LOCK_FILE);
55
+ if (!existsSync(path)) return createEmptyPluginLock();
56
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
57
+ if (parsed.version !== 1 || typeof parsed.plugins !== "object" || parsed.plugins === null) {
58
+ throw new Error(`Invalid ${PLUGIN_LOCK_FILE}`);
438
59
  }
439
- };
60
+ return parsed;
61
+ }
62
+ function getLockedPluginSpecifiers(cwd = process.cwd()) {
63
+ return Object.values(readPluginLock(cwd).plugins).map((entry) => entry.specifier);
64
+ }
65
+
66
+ // src/default-plugin-catalog.ts
67
+ var DEFAULT_PLUGIN_REGISTRY = {};
440
68
 
441
69
  // src/registry.ts
442
- async function resolvePluginModules(pluginSpecifiers = []) {
443
- const results = await Promise.all(pluginSpecifiers.map(loadExternalPluginModule));
70
+ async function resolvePluginModules(pluginSpecifiers = [], options = {}) {
71
+ const installedSpecifiers = options.includeInstalled ? getLockedPluginSpecifiers() : [];
72
+ const allSpecifiers = [...installedSpecifiers, ...pluginSpecifiers];
73
+ const results = await Promise.all(allSpecifiers.map(loadExternalPluginModule));
444
74
  const externalEntries = {};
445
75
  for (const pluginModule of results) {
446
76
  if (pluginModule.name in DEFAULT_PLUGIN_REGISTRY) {
@@ -462,7 +92,7 @@ function resolveBaseUrl(opts) {
462
92
  if (opts.baseUrl) {
463
93
  return opts.baseUrl.replace(/\{service\}/g, opts.service);
464
94
  }
465
- const envBaseUrl = process.env.API_EMULATOR_BASE_URL ?? process.env.EMULATE_BASE_URL;
95
+ const envBaseUrl = process.env.API_EMULATOR_BASE_URL;
466
96
  if (envBaseUrl) {
467
97
  return envBaseUrl.replace(/\{service\}/g, opts.service);
468
98
  }
@@ -474,7 +104,11 @@ function resolveBaseUrl(opts) {
474
104
  }
475
105
 
476
106
  // src/service-runtime.ts
477
- import { createServer } from "@emulators/core";
107
+ import {
108
+ createServer,
109
+ createStoreFixture,
110
+ fixtureStoreSnapshot
111
+ } from "@api-emulator/core";
478
112
  import { serve } from "@hono/node-server";
479
113
  function createAuthTokens(seedConfig) {
480
114
  const tokens = {};
@@ -513,15 +147,32 @@ function createServiceRuntime(options) {
513
147
  service,
514
148
  url: baseUrl,
515
149
  store,
150
+ snapshot() {
151
+ return store.snapshot();
152
+ },
153
+ restore(fixture) {
154
+ store.restore(fixtureStoreSnapshot(fixture));
155
+ },
156
+ exportFixture(options2 = {}) {
157
+ const interactions = store.getData("api-emulator:interactions");
158
+ return createStoreFixture(service, store.snapshot(), {
159
+ ...options2,
160
+ interactions: options2.interactions ?? interactions
161
+ });
162
+ },
163
+ resetToFixture(fixture) {
164
+ store.reset();
165
+ store.restore(fixtureStoreSnapshot(fixture));
166
+ },
516
167
  reset() {
517
168
  store.reset();
518
169
  seed();
519
170
  },
520
171
  close() {
521
- return new Promise((resolve2, reject) => {
172
+ return new Promise((resolve3, reject) => {
522
173
  httpServer.close((err) => {
523
174
  if (err) reject(err);
524
- else resolve2();
175
+ else resolve3();
525
176
  });
526
177
  });
527
178
  }
@@ -551,6 +202,10 @@ async function createEmulator(options) {
551
202
  });
552
203
  return {
553
204
  url: running.url,
205
+ snapshot: running.snapshot,
206
+ restore: running.restore,
207
+ exportFixture: running.exportFixture,
208
+ resetToFixture: running.resetToFixture,
554
209
  reset: running.reset,
555
210
  close: running.close
556
211
  };