api-emulator 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.js CHANGED
@@ -1,5 +1,29 @@
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 {
7
+ label: mod.label,
8
+ endpoints: mod.endpoints,
9
+ initConfig: mod.initConfig,
10
+ contract: mod.contract,
11
+ ...mod.manifest
12
+ };
13
+ }
14
+ function validatePluginManifest(manifest, pluginName) {
15
+ if (manifest.name && manifest.name !== pluginName) {
16
+ throw new Error(`Plugin manifest name "${manifest.name}" does not match plugin name "${pluginName}"`);
17
+ }
18
+ return {
19
+ ...manifest,
20
+ label: manifest.label ?? `${pluginName} (external plugin)`,
21
+ endpoints: manifest.endpoints ?? "",
22
+ initConfig: manifest.initConfig ?? {}
23
+ };
24
+ }
25
+
26
+ // src/external-plugin-adapter.ts
3
27
  async function loadExternalPluginModule(specifier) {
4
28
  const modulePath = specifier.startsWith(".") || isAbsolute(specifier) ? resolve(specifier) : specifier;
5
29
  const mod = await import(modulePath);
@@ -8,10 +32,12 @@ async function loadExternalPluginModule(specifier) {
8
32
  throw new Error(`Plugin "${specifier}" must export a ServicePlugin (as "plugin" or default export)`);
9
33
  }
10
34
  const name = plugin.name;
35
+ const manifest = validatePluginManifest(readPluginManifest(mod), name);
11
36
  return {
12
37
  name,
13
- label: mod.label ?? `${name} (external plugin)`,
14
- endpoints: mod.endpoints ?? "",
38
+ label: manifest.label,
39
+ endpoints: manifest.endpoints,
40
+ manifest,
15
41
  async load() {
16
42
  return {
17
43
  plugin,
@@ -19,428 +45,38 @@ async function loadExternalPluginModule(specifier) {
19
45
  };
20
46
  },
21
47
  defaultFallback: mod.defaultFallback ?? (() => ({ login: "admin", id: 1, scopes: [] })),
22
- initConfig: mod.initConfig ?? {}
48
+ initConfig: manifest.initConfig
23
49
  };
24
50
  }
25
51
 
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
- }
52
+ // src/plugin-lock.ts
53
+ import { existsSync, readFileSync, writeFileSync } from "fs";
54
+ import { resolve as resolve2 } from "path";
55
+ var PLUGIN_LOCK_FILE = "api-emulator.lock";
56
+ function createEmptyPluginLock() {
57
+ return { version: 1, plugins: {} };
58
+ }
59
+ function readPluginLock(cwd = process.cwd()) {
60
+ const path = resolve2(cwd, PLUGIN_LOCK_FILE);
61
+ if (!existsSync(path)) return createEmptyPluginLock();
62
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
63
+ if (parsed.version !== 1 || typeof parsed.plugins !== "object" || parsed.plugins === null) {
64
+ throw new Error(`Invalid ${PLUGIN_LOCK_FILE}`);
438
65
  }
439
- };
66
+ return parsed;
67
+ }
68
+ function getLockedPluginSpecifiers(cwd = process.cwd()) {
69
+ return Object.values(readPluginLock(cwd).plugins).map((entry) => entry.specifier);
70
+ }
71
+
72
+ // src/default-plugin-catalog.ts
73
+ var DEFAULT_PLUGIN_REGISTRY = {};
440
74
 
441
75
  // src/registry.ts
442
- async function resolvePluginModules(pluginSpecifiers = []) {
443
- const results = await Promise.all(pluginSpecifiers.map(loadExternalPluginModule));
76
+ async function resolvePluginModules(pluginSpecifiers = [], options = {}) {
77
+ const installedSpecifiers = options.includeInstalled ? getLockedPluginSpecifiers() : [];
78
+ const allSpecifiers = [...installedSpecifiers, ...pluginSpecifiers];
79
+ const results = await Promise.all(allSpecifiers.map(loadExternalPluginModule));
444
80
  const externalEntries = {};
445
81
  for (const pluginModule of results) {
446
82
  if (pluginModule.name in DEFAULT_PLUGIN_REGISTRY) {
@@ -474,7 +110,11 @@ function resolveBaseUrl(opts) {
474
110
  }
475
111
 
476
112
  // src/service-runtime.ts
477
- import { createServer } from "@emulators/core";
113
+ import {
114
+ createServer,
115
+ createStoreFixture,
116
+ fixtureStoreSnapshot
117
+ } from "@api-emulator/core";
478
118
  import { serve } from "@hono/node-server";
479
119
  function createAuthTokens(seedConfig) {
480
120
  const tokens = {};
@@ -513,15 +153,32 @@ function createServiceRuntime(options) {
513
153
  service,
514
154
  url: baseUrl,
515
155
  store,
156
+ snapshot() {
157
+ return store.snapshot();
158
+ },
159
+ restore(fixture) {
160
+ store.restore(fixtureStoreSnapshot(fixture));
161
+ },
162
+ exportFixture(options2 = {}) {
163
+ const interactions = store.getData("api-emulator:interactions");
164
+ return createStoreFixture(service, store.snapshot(), {
165
+ ...options2,
166
+ interactions: options2.interactions ?? interactions
167
+ });
168
+ },
169
+ resetToFixture(fixture) {
170
+ store.reset();
171
+ store.restore(fixtureStoreSnapshot(fixture));
172
+ },
516
173
  reset() {
517
174
  store.reset();
518
175
  seed();
519
176
  },
520
177
  close() {
521
- return new Promise((resolve2, reject) => {
178
+ return new Promise((resolve3, reject) => {
522
179
  httpServer.close((err) => {
523
180
  if (err) reject(err);
524
- else resolve2();
181
+ else resolve3();
525
182
  });
526
183
  });
527
184
  }
@@ -551,6 +208,10 @@ async function createEmulator(options) {
551
208
  });
552
209
  return {
553
210
  url: running.url,
211
+ snapshot: running.snapshot,
212
+ restore: running.restore,
213
+ exportFixture: running.exportFixture,
214
+ resetToFixture: running.resetToFixture,
554
215
  reset: running.reset,
555
216
  close: running.close
556
217
  };