@atlasauth/cli 0.0.0-stage → 0.2.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 (71) hide show
  1. package/README.md +91 -2
  2. package/dist/args.d.ts +84 -0
  3. package/dist/args.js +76 -0
  4. package/dist/args.js.map +1 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +19 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/commands/actions.d.ts +11 -0
  9. package/dist/commands/actions.js +120 -0
  10. package/dist/commands/actions.js.map +1 -0
  11. package/dist/commands/api-keys.d.ts +4 -0
  12. package/dist/commands/api-keys.js +81 -0
  13. package/dist/commands/api-keys.js.map +1 -0
  14. package/dist/commands/auth.d.ts +25 -0
  15. package/dist/commands/auth.js +94 -0
  16. package/dist/commands/auth.js.map +1 -0
  17. package/dist/commands/config.d.ts +4 -0
  18. package/dist/commands/config.js +716 -0
  19. package/dist/commands/config.js.map +1 -0
  20. package/dist/commands/orgs.d.ts +4 -0
  21. package/dist/commands/orgs.js +72 -0
  22. package/dist/commands/orgs.js.map +1 -0
  23. package/dist/commands/sessions.d.ts +4 -0
  24. package/dist/commands/sessions.js +28 -0
  25. package/dist/commands/sessions.js.map +1 -0
  26. package/dist/commands/shared.d.ts +32 -0
  27. package/dist/commands/shared.js +60 -0
  28. package/dist/commands/shared.js.map +1 -0
  29. package/dist/commands/users.d.ts +4 -0
  30. package/dist/commands/users.js +87 -0
  31. package/dist/commands/users.js.map +1 -0
  32. package/dist/commands/webhooks.d.ts +4 -0
  33. package/dist/commands/webhooks.js +34 -0
  34. package/dist/commands/webhooks.js.map +1 -0
  35. package/dist/config-http.d.ts +27 -0
  36. package/dist/config-http.js +63 -0
  37. package/dist/config-http.js.map +1 -0
  38. package/dist/config.d.ts +48 -0
  39. package/dist/config.js +69 -0
  40. package/dist/config.js.map +1 -0
  41. package/dist/context.d.ts +51 -0
  42. package/dist/context.js +27 -0
  43. package/dist/context.js.map +1 -0
  44. package/dist/errors.d.ts +15 -0
  45. package/dist/errors.js +25 -0
  46. package/dist/errors.js.map +1 -0
  47. package/dist/help.d.ts +8 -0
  48. package/dist/help.js +114 -0
  49. package/dist/help.js.map +1 -0
  50. package/dist/index.d.ts +22 -0
  51. package/dist/index.js +49 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/io.d.ts +21 -0
  54. package/dist/io.js +28 -0
  55. package/dist/io.js.map +1 -0
  56. package/dist/output.d.ts +41 -0
  57. package/dist/output.js +72 -0
  58. package/dist/output.js.map +1 -0
  59. package/dist/prompt.d.ts +15 -0
  60. package/dist/prompt.js +46 -0
  61. package/dist/prompt.js.map +1 -0
  62. package/dist/run.d.ts +12 -0
  63. package/dist/run.js +94 -0
  64. package/dist/run.js.map +1 -0
  65. package/dist/serialize.d.ts +31 -0
  66. package/dist/serialize.js +248 -0
  67. package/dist/serialize.js.map +1 -0
  68. package/dist/version.d.ts +7 -0
  69. package/dist/version.js +21 -0
  70. package/dist/version.js.map +1 -0
  71. package/package.json +35 -3
@@ -0,0 +1,716 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.config = config;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const args_1 = require("../args");
7
+ const config_http_1 = require("../config-http");
8
+ const errors_1 = require("../errors");
9
+ const serialize_1 = require("../serialize");
10
+ const shared_1 = require("./shared");
11
+ // ───────────────────────────── small helpers ─────────────────────────────
12
+ /** GET a `{ data: [...] }` list envelope, tolerating an absent/garbled body. */
13
+ async function listData(http, path) {
14
+ const res = await http.get(path);
15
+ return Array.isArray(res?.data) ? res.data : [];
16
+ }
17
+ /** Build a config object from a row, keeping only the named keys and dropping null/undefined. */
18
+ function pick(row, keys) {
19
+ const out = {};
20
+ for (const k of keys) {
21
+ const v = row[k];
22
+ if (v === null || v === undefined)
23
+ continue;
24
+ out[k] = v;
25
+ }
26
+ return out;
27
+ }
28
+ /** The write body for a create/update: strip null/undefined so the API never sees them. */
29
+ function compact(config) {
30
+ const out = {};
31
+ if (config && typeof config === 'object' && !Array.isArray(config)) {
32
+ for (const [k, v] of Object.entries(config)) {
33
+ if (v === null || v === undefined)
34
+ continue;
35
+ out[k] = v;
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+ function enc(segment) {
41
+ return encodeURIComponent(segment);
42
+ }
43
+ // ───────────────────────────── the registry ─────────────────────────────
44
+ /**
45
+ * The single source of truth for what `atlas config` round-trips — the same
46
+ * resource set the Terraform provider manages. Each entry maps a resource type
47
+ * to its list/create/update/delete endpoints and its redacted projection.
48
+ * SECRETS ARE NEVER PART OF A PROJECTION: the management API never returns them
49
+ * on a read, so they never reach disk; a credential that must be (re)written is
50
+ * supplied at import time via a `${ENV_VAR}` placeholder in the file.
51
+ */
52
+ function registry() {
53
+ const specs = [
54
+ // 1. Instance config (auth_config + allowed origins) — the §9.3 singleton.
55
+ {
56
+ kind: 'singleton',
57
+ type: 'instance',
58
+ async fetchLive(http) {
59
+ const inst = await http.get('/v1/instance');
60
+ return {
61
+ allowed_origins: inst.allowed_origins ?? [],
62
+ auth_config: inst.auth_config ?? {},
63
+ };
64
+ },
65
+ async apply(http, config) {
66
+ const c = compact(config);
67
+ await http.patch('/v1/instance', {
68
+ allowed_origins: c.allowed_origins ?? [],
69
+ auth_config: c.auth_config ?? {},
70
+ });
71
+ },
72
+ },
73
+ // 2. Org settings schema (P1-5) — a singleton JSON Schema; 404 when unset.
74
+ {
75
+ kind: 'singleton',
76
+ type: 'org_settings_schema',
77
+ async fetchLive(http) {
78
+ try {
79
+ const s = await http.get('/v1/organization_settings_schema');
80
+ return { schema: s.schema ?? {} };
81
+ }
82
+ catch (err) {
83
+ if ((0, config_http_1.isNotFound)(err))
84
+ return null;
85
+ throw err;
86
+ }
87
+ },
88
+ async apply(http, config) {
89
+ const c = compact(config);
90
+ await http.put('/v1/organization_settings_schema', { schema: c.schema ?? {} });
91
+ },
92
+ },
93
+ // 3. Redirect URL allowlist — key = the (normalised) url; immutable, so no update.
94
+ {
95
+ kind: 'collection',
96
+ type: 'redirect_urls',
97
+ keyOf: (c) => String(obj(c).url ?? ''),
98
+ async fetchLive(http) {
99
+ const rows = await listData(http, '/v1/redirect_urls');
100
+ return rows.map((r) => ({ key: r.url, id: r.id, config: pick(r, ['url']) }));
101
+ },
102
+ async create(http, config) {
103
+ await http.post('/v1/redirect_urls', { url: obj(config).url });
104
+ },
105
+ async remove(http, live) {
106
+ await http.delete(`/v1/redirect_urls/${enc(live.id)}`);
107
+ },
108
+ },
109
+ // 4. OAuth clients (first-party / M2M apps) — key = name.
110
+ {
111
+ kind: 'collection',
112
+ type: 'oauth_clients',
113
+ keyOf: (c) => String(obj(c).name ?? ''),
114
+ async fetchLive(http) {
115
+ const rows = await listData(http, '/v1/oauth_clients');
116
+ return rows.map((r) => ({
117
+ key: String(r.name ?? ''),
118
+ id: String(r.id ?? ''),
119
+ config: pick(r, OAUTH_CLIENT_KEYS),
120
+ }));
121
+ },
122
+ async create(http, config) {
123
+ await http.post('/v1/oauth_clients', compact(config));
124
+ },
125
+ async update(http, live, config) {
126
+ await http.patch(`/v1/oauth_clients/${enc(live.id)}`, compact(config));
127
+ },
128
+ async remove(http, live) {
129
+ await http.delete(`/v1/oauth_clients/${enc(live.id)}`);
130
+ },
131
+ },
132
+ // 5. Social / enterprise OAuth providers — key = the catalog provider slug.
133
+ {
134
+ kind: 'collection',
135
+ type: 'oauth_providers',
136
+ keyOf: (c) => String(obj(c).provider ?? ''),
137
+ async fetchLive(http) {
138
+ const rows = await listData(http, '/v1/oauth_providers');
139
+ return rows
140
+ .filter((r) => r.configured === true)
141
+ .map((r) => ({
142
+ key: String(r.provider ?? ''),
143
+ id: String(r.provider ?? ''),
144
+ config: pick(r, OAUTH_PROVIDER_KEYS),
145
+ }));
146
+ },
147
+ create: putProvider,
148
+ update: (http, _live, config) => putProvider(http, config),
149
+ async remove(http, live) {
150
+ await http.delete(`/v1/oauth_providers/${enc(live.id)}`);
151
+ },
152
+ },
153
+ // 6. Resource servers (API audiences) — key = identifier.
154
+ {
155
+ kind: 'collection',
156
+ type: 'resource_servers',
157
+ keyOf: (c) => String(obj(c).identifier ?? ''),
158
+ async fetchLive(http) {
159
+ const rows = await listData(http, '/v1/resource_servers');
160
+ return rows.map((r) => ({
161
+ key: String(r.identifier ?? ''),
162
+ id: String(r.id ?? ''),
163
+ config: pick(r, RESOURCE_SERVER_KEYS),
164
+ }));
165
+ },
166
+ async create(http, config) {
167
+ await http.post('/v1/resource_servers', compact(config));
168
+ },
169
+ async update(http, live, config) {
170
+ const body = compact(config);
171
+ delete body.identifier; // immutable
172
+ await http.patch(`/v1/resource_servers/${enc(live.id)}`, body);
173
+ },
174
+ async remove(http, live) {
175
+ await http.delete(`/v1/resource_servers/${enc(live.id)}`);
176
+ },
177
+ },
178
+ // 7. JWT templates — key = name; claims is a flat template map.
179
+ {
180
+ kind: 'collection',
181
+ type: 'jwt_templates',
182
+ keyOf: (c) => String(obj(c).name ?? ''),
183
+ async fetchLive(http) {
184
+ const rows = await listData(http, '/v1/jwt_templates');
185
+ return rows.map((r) => ({
186
+ key: r.name,
187
+ id: r.name,
188
+ config: { name: r.name, claims: r.claims ?? {} },
189
+ }));
190
+ },
191
+ async create(http, config) {
192
+ const c = obj(config);
193
+ await http.post('/v1/jwt_templates', { name: c.name, claims: c.claims ?? {} });
194
+ },
195
+ async update(http, live, config) {
196
+ await http.patch(`/v1/jwt_templates/${enc(live.id)}`, { claims: obj(config).claims ?? {} });
197
+ },
198
+ async remove(http, live) {
199
+ await http.delete(`/v1/jwt_templates/${enc(live.id)}`);
200
+ },
201
+ },
202
+ // 8. Roles — key = key; label via PATCH, permission set via PUT. System roles skipped.
203
+ {
204
+ kind: 'collection',
205
+ type: 'roles',
206
+ keyOf: (c) => String(obj(c).key ?? ''),
207
+ async fetchLive(http) {
208
+ const rows = await listData(http, '/v1/roles');
209
+ return rows
210
+ .filter((r) => r.is_system !== true)
211
+ .map((r) => ({
212
+ key: String(r.key ?? ''),
213
+ id: String(r.id ?? ''),
214
+ config: pick(r, ROLE_KEYS),
215
+ }));
216
+ },
217
+ async create(http, config) {
218
+ await http.post('/v1/roles', compact(config));
219
+ },
220
+ async update(http, live, config) {
221
+ const c = obj(config);
222
+ await http.patch(`/v1/roles/${enc(live.id)}`, {
223
+ name: c.name,
224
+ description: c.description ?? null,
225
+ });
226
+ await http.put(`/v1/roles/${enc(live.id)}/permissions`, {
227
+ permissions: Array.isArray(c.permissions) ? c.permissions : [],
228
+ });
229
+ },
230
+ async remove(http, live) {
231
+ await http.delete(`/v1/roles/${enc(live.id)}`);
232
+ },
233
+ },
234
+ // 9. Webhook endpoints — key = url.
235
+ {
236
+ kind: 'collection',
237
+ type: 'webhook_endpoints',
238
+ keyOf: (c) => String(obj(c).url ?? ''),
239
+ async fetchLive(http) {
240
+ const rows = await listData(http, '/v1/webhook_endpoints');
241
+ return rows.map((r) => ({
242
+ key: String(r.url ?? ''),
243
+ id: String(r.id ?? ''),
244
+ config: pick(r, WEBHOOK_KEYS),
245
+ }));
246
+ },
247
+ async create(http, config) {
248
+ const c = obj(config);
249
+ await http.post('/v1/webhook_endpoints', {
250
+ url: c.url,
251
+ enabled_events: c.enabled_events ?? [],
252
+ });
253
+ },
254
+ async update(http, live, config) {
255
+ await http.patch(`/v1/webhook_endpoints/${enc(live.id)}`, compact(config));
256
+ },
257
+ async remove(http, live) {
258
+ await http.delete(`/v1/webhook_endpoints/${enc(live.id)}`);
259
+ },
260
+ },
261
+ // 10. SSO connections — key = a stable identifier derived from the connection.
262
+ {
263
+ kind: 'collection',
264
+ type: 'sso_connections',
265
+ keyOf: ssoKey,
266
+ async fetchLive(http) {
267
+ const rows = await listData(http, '/v1/sso_connections');
268
+ return rows.map((r) => {
269
+ const config = pick(r, SSO_KEYS);
270
+ return { key: ssoKey(config), id: String(r.id ?? ''), config };
271
+ });
272
+ },
273
+ async create(http, config) {
274
+ await http.post('/v1/sso_connections', compact(config));
275
+ },
276
+ async update(http, live, config) {
277
+ await http.patch(`/v1/sso_connections/${enc(live.id)}`, compact(config));
278
+ },
279
+ async remove(http, live) {
280
+ await http.delete(`/v1/sso_connections/${enc(live.id)}`);
281
+ },
282
+ },
283
+ // 11. Billing plans — key = slug.
284
+ {
285
+ kind: 'collection',
286
+ type: 'billing_plans',
287
+ keyOf: (c) => String(obj(c).slug ?? ''),
288
+ async fetchLive(http) {
289
+ const rows = await listData(http, '/v1/billing/plans');
290
+ return rows.map((r) => ({
291
+ key: String(r.slug ?? ''),
292
+ id: String(r.id ?? ''),
293
+ config: pick(r, BILLING_KEYS),
294
+ }));
295
+ },
296
+ async create(http, config) {
297
+ await http.post('/v1/billing/plans', compact(config));
298
+ },
299
+ async update(http, live, config) {
300
+ const body = compact(config);
301
+ delete body.slug; // immutable
302
+ await http.patch(`/v1/billing/plans/${enc(live.id)}`, body);
303
+ },
304
+ async remove(http, live) {
305
+ await http.delete(`/v1/billing/plans/${enc(live.id)}`);
306
+ },
307
+ },
308
+ // 12. Notification templates (P2-12) — key = name; replace via PUT.
309
+ {
310
+ kind: 'collection',
311
+ type: 'notification_templates',
312
+ keyOf: (c) => String(obj(c).name ?? ''),
313
+ async fetchLive(http) {
314
+ const rows = await listData(http, '/v1/notification_templates');
315
+ return rows.map((r) => ({
316
+ key: String(r.name ?? ''),
317
+ id: String(r.name ?? ''),
318
+ config: pick(r, NOTIFICATION_KEYS),
319
+ }));
320
+ },
321
+ async create(http, config) {
322
+ await http.post('/v1/notification_templates', compact(config));
323
+ },
324
+ async update(http, live, config) {
325
+ const body = compact(config);
326
+ delete body.name; // the name is the URL key
327
+ await http.put(`/v1/notification_templates/${enc(live.id)}`, body);
328
+ },
329
+ async remove(http, live) {
330
+ await http.delete(`/v1/notification_templates/${enc(live.id)}`);
331
+ },
332
+ },
333
+ // 13. Email templates (§11.1) — key = name; only the CUSTOMISED overrides are config.
334
+ {
335
+ kind: 'collection',
336
+ type: 'email_templates',
337
+ keyOf: (c) => String(obj(c).name ?? ''),
338
+ async fetchLive(http) {
339
+ const rows = await listData(http, '/v1/email_templates');
340
+ return rows
341
+ .filter((r) => r.customised && r.override)
342
+ .map((r) => ({
343
+ key: r.name,
344
+ id: r.name,
345
+ config: pick({ name: r.name, ...(r.override ?? {}) }, EMAIL_KEYS),
346
+ }));
347
+ },
348
+ async create(http, config) {
349
+ await putEmail(http, config);
350
+ },
351
+ async update(http, _live, config) {
352
+ await putEmail(http, config);
353
+ },
354
+ async remove(http, live) {
355
+ await http.delete(`/v1/email_templates/${enc(live.id)}`);
356
+ },
357
+ },
358
+ // 14. Organization policies — key = organization_id; attaches to existing orgs.
359
+ {
360
+ kind: 'collection',
361
+ type: 'organization_policies',
362
+ keyOf: (c) => String(obj(c).organization_id ?? ''),
363
+ async fetchLive(http) {
364
+ const orgs = await listData(http, '/v1/organizations');
365
+ const out = [];
366
+ for (const org of orgs) {
367
+ // There is no GET for org policy; an empty PATCH returns it unchanged.
368
+ const res = await http.patch(`/v1/organizations/${enc(org.id)}/policy`, {});
369
+ const policy = res?.policy ?? {};
370
+ const config = pick({ organization_id: org.id, ...policy }, ORG_POLICY_KEYS);
371
+ // Only orgs with a real (non-default) policy are config.
372
+ if (Object.keys(obj(config)).length > 1) {
373
+ out.push({ key: org.id, id: org.id, config });
374
+ }
375
+ }
376
+ return out;
377
+ },
378
+ async create(http, config) {
379
+ await patchOrgPolicy(http, config);
380
+ },
381
+ async update(http, _live, config) {
382
+ await patchOrgPolicy(http, config);
383
+ },
384
+ // No delete: pruning a policy would silently loosen an org's security, so
385
+ // org policies are never deleted by import (reported as skipped).
386
+ },
387
+ ];
388
+ return specs;
389
+ }
390
+ const OAUTH_CLIENT_KEYS = [
391
+ 'name',
392
+ 'redirect_uris',
393
+ 'allowed_scopes',
394
+ 'grant_types',
395
+ 'token_endpoint_auth_method',
396
+ 'logo_url',
397
+ 'first_party',
398
+ ];
399
+ const OAUTH_PROVIDER_KEYS = [
400
+ 'provider',
401
+ 'enabled',
402
+ 'allow_sign_in',
403
+ 'allow_sign_up',
404
+ 'scopes',
405
+ 'client_id',
406
+ ];
407
+ const RESOURCE_SERVER_KEYS = [
408
+ 'identifier',
409
+ 'name',
410
+ 'scopes',
411
+ 'token_ttl_seconds',
412
+ 'signing_alg',
413
+ ];
414
+ const ROLE_KEYS = ['key', 'name', 'description', 'permissions'];
415
+ const WEBHOOK_KEYS = ['url', 'enabled_events', 'active'];
416
+ const SSO_KEYS = [
417
+ 'type',
418
+ 'status',
419
+ 'organization_id',
420
+ 'oidc_issuer',
421
+ 'oidc_client_id',
422
+ 'saml_idp_entity_id',
423
+ 'saml_idp_sso_url',
424
+ 'saml_sp_entity_id',
425
+ 'saml_allow_idp_initiated',
426
+ 'saml_sign_authn_requests',
427
+ 'saml_want_response_signed',
428
+ 'discourse_provider_url',
429
+ 'allowed_domains',
430
+ 'claim_role_mappings',
431
+ 'default_role_id',
432
+ ];
433
+ const BILLING_KEYS = [
434
+ 'name',
435
+ 'slug',
436
+ 'stripe_price_id',
437
+ 'interval',
438
+ 'amount',
439
+ 'currency',
440
+ 'features',
441
+ 'limits',
442
+ 'audience',
443
+ 'active',
444
+ 'pricing_model',
445
+ 'trial_days',
446
+ 'stripe_meter_id',
447
+ 'usage_unit',
448
+ ];
449
+ const NOTIFICATION_KEYS = ['name', 'subject', 'body', 'category'];
450
+ const EMAIL_KEYS = ['name', 'subject', 'text'];
451
+ const ORG_POLICY_KEYS = [
452
+ 'organization_id',
453
+ 'requireMfa',
454
+ 'ssoRequired',
455
+ 'sessionIdleOverrideMs',
456
+ 'allowedSignInMethods',
457
+ 'ipAllowlist',
458
+ 'maxSessionAgeSeconds',
459
+ ];
460
+ /** Narrow a Config to a plain string-keyed record for field access. */
461
+ function obj(config) {
462
+ return config && typeof config === 'object' && !Array.isArray(config)
463
+ ? config
464
+ : {};
465
+ }
466
+ /** A stable natural key for an SSO connection, which has no name field. */
467
+ function ssoKey(config) {
468
+ const c = obj(config);
469
+ const type = String(c.type ?? 'sso');
470
+ const id = c.oidc_issuer ?? c.saml_idp_entity_id ?? c.saml_sp_entity_id ?? c.discourse_provider_url ?? '';
471
+ return `${type}:${String(id)}`;
472
+ }
473
+ /** Configure an OAuth provider: credentials/scopes (PUT), then scope + enabled toggles. */
474
+ async function putProvider(http, config) {
475
+ const c = obj(config);
476
+ const values = {};
477
+ if (c.client_id !== undefined && c.client_id !== null)
478
+ values.client_id = c.client_id;
479
+ // A secret only exists here when the file carried a ${ENV} placeholder for it.
480
+ if (c.client_secret !== undefined && c.client_secret !== null) {
481
+ values.client_secret = c.client_secret;
482
+ }
483
+ const body = {};
484
+ if (Object.keys(values).length)
485
+ body.values = values;
486
+ if (Array.isArray(c.scopes))
487
+ body.scopes = c.scopes;
488
+ const provider = String(c.provider ?? '');
489
+ if (Object.keys(body).length) {
490
+ await http.put(`/v1/oauth_providers/${enc(provider)}`, body);
491
+ }
492
+ if (c.allow_sign_in !== undefined || c.allow_sign_up !== undefined) {
493
+ await http.post(`/v1/oauth_providers/${enc(provider)}/scope`, {
494
+ allow_sign_in: c.allow_sign_in ?? true,
495
+ allow_sign_up: c.allow_sign_up ?? true,
496
+ });
497
+ }
498
+ if (c.enabled !== undefined) {
499
+ await http.post(`/v1/oauth_providers/${enc(provider)}/enabled`, { enabled: c.enabled });
500
+ }
501
+ }
502
+ async function putEmail(http, config) {
503
+ const c = obj(config);
504
+ const body = {};
505
+ if (c.subject !== undefined)
506
+ body.subject = c.subject;
507
+ if (c.text !== undefined)
508
+ body.text = c.text;
509
+ await http.put(`/v1/email_templates/${enc(String(c.name ?? ''))}`, body);
510
+ }
511
+ async function patchOrgPolicy(http, config) {
512
+ const c = obj(config);
513
+ const body = {};
514
+ for (const k of ORG_POLICY_KEYS) {
515
+ if (k === 'organization_id')
516
+ continue;
517
+ if (c[k] !== undefined && c[k] !== null)
518
+ body[k] = c[k];
519
+ }
520
+ await http.patch(`/v1/organizations/${enc(String(c.organization_id ?? ''))}/policy`, body);
521
+ }
522
+ // ───────────────────────────── the command ─────────────────────────────
523
+ /** `atlas config export|import` — Auth0 `deploy-cli`-style config-as-code. */
524
+ async function config(ctx, args) {
525
+ const sub = args.positionals[1];
526
+ switch (sub) {
527
+ case 'export':
528
+ return exportConfig(ctx, args.values);
529
+ case 'import':
530
+ return importConfig(ctx, args.values);
531
+ default:
532
+ throw new errors_1.CliError(sub
533
+ ? `Unknown subcommand 'config ${sub}'. Try: export, import.`
534
+ : 'config needs a subcommand: export, import.');
535
+ }
536
+ }
537
+ function resolveFormat(values) {
538
+ const raw = (0, args_1.str)(values, 'format');
539
+ if (raw === undefined)
540
+ return 'json';
541
+ if (raw !== 'json' && raw !== 'yaml') {
542
+ throw new errors_1.CliError(`--format must be 'json' or 'yaml', got '${raw}'.`);
543
+ }
544
+ return raw;
545
+ }
546
+ function resolveDir(values) {
547
+ return (0, args_1.str)(values, 'dir') || './atlas-config';
548
+ }
549
+ /** Build the config HTTP client from the resolved profile credential. */
550
+ function requireHttp(ctx, values) {
551
+ const auth = (0, shared_1.resolveAuth)(ctx, values);
552
+ const make = ctx.makeHttp ?? config_http_1.createConfigHttp;
553
+ return make({ secretKey: auth.secretKey, apiUrl: auth.apiUrl });
554
+ }
555
+ async function exportConfig(ctx, values) {
556
+ const http = requireHttp(ctx, values);
557
+ const dir = resolveDir(values);
558
+ const format = resolveFormat(values);
559
+ const ext = format === 'json' ? 'json' : 'yaml';
560
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
561
+ let fileCount = 0;
562
+ for (const spec of registry()) {
563
+ if (spec.kind === 'singleton') {
564
+ const live = await spec.fetchLive(http);
565
+ if (live === null)
566
+ continue; // nothing to write (e.g. no schema registered)
567
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, `${spec.type}.${ext}`), (0, serialize_1.serialize)(live, format));
568
+ fileCount += 1;
569
+ }
570
+ else {
571
+ const entries = await spec.fetchLive(http);
572
+ // Deterministic: entries sorted by natural key, keys sorted inside each.
573
+ const sorted = [...entries].sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
574
+ const doc = sorted.map((e) => (0, serialize_1.canonicalize)(e.config));
575
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(dir, `${spec.type}.${ext}`), (0, serialize_1.serialize)(doc, format));
576
+ fileCount += 1;
577
+ }
578
+ }
579
+ ctx.io.out(`Exported ${fileCount} resource file(s) to ${dir} (${format}).`);
580
+ ctx.io.out('Secrets are never exported — supply any credential at import via a ${ENV} placeholder.');
581
+ return 0;
582
+ }
583
+ async function importConfig(ctx, values) {
584
+ const http = requireHttp(ctx, values);
585
+ const dir = resolveDir(values);
586
+ const format = resolveFormat(values);
587
+ const ext = format === 'json' ? 'json' : 'yaml';
588
+ const dryRun = values['dry-run'] === true;
589
+ const prune = values.prune === true;
590
+ if (!(0, node_fs_1.existsSync)(dir)) {
591
+ throw new errors_1.CliError(`Config directory not found: ${dir}. Run 'atlas config export' first.`);
592
+ }
593
+ const present = new Set((0, node_fs_1.readdirSync)(dir));
594
+ const changes = [];
595
+ const applications = [];
596
+ let unchanged = 0;
597
+ const skippedDeletes = [];
598
+ for (const spec of registry()) {
599
+ const file = `${spec.type}.${ext}`;
600
+ // A type with no file is simply NOT MANAGED — leave live untouched.
601
+ if (!present.has(file))
602
+ continue;
603
+ const text = (0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, file), 'utf8');
604
+ const parsed = interpolate((0, serialize_1.deserialize)(text, format), ctx.env);
605
+ if (spec.kind === 'singleton') {
606
+ const desired = parsed;
607
+ const live = await spec.fetchLive(http);
608
+ if (live !== null && (0, serialize_1.deepEqual)(desired, live)) {
609
+ unchanged += 1;
610
+ continue;
611
+ }
612
+ changes.push({ type: spec.type, action: live === null ? 'create' : 'update', key: '' });
613
+ applications.push(() => spec.apply(http, desired));
614
+ continue;
615
+ }
616
+ // Collection: diff desired-by-key against live-by-key.
617
+ const desiredList = asArray(parsed);
618
+ const desiredByKey = new Map();
619
+ for (const cfg of desiredList)
620
+ desiredByKey.set(spec.keyOf(cfg), cfg);
621
+ const live = await spec.fetchLive(http);
622
+ const liveByKey = new Map(live.map((e) => [e.key, e]));
623
+ for (const [key, cfg] of desiredByKey) {
624
+ const existing = liveByKey.get(key);
625
+ if (!existing) {
626
+ changes.push({ type: spec.type, action: 'create', key });
627
+ applications.push(() => spec.create(http, cfg));
628
+ }
629
+ else if (!(0, serialize_1.deepEqual)(cfg, existing.config)) {
630
+ if (!spec.update) {
631
+ throw new errors_1.CliError(`${spec.type} '${key}' changed but is immutable; recreate it instead.`);
632
+ }
633
+ const upd = spec.update;
634
+ changes.push({ type: spec.type, action: 'update', key });
635
+ applications.push(() => upd(http, existing, cfg));
636
+ }
637
+ else {
638
+ unchanged += 1;
639
+ }
640
+ }
641
+ for (const entry of live) {
642
+ if (desiredByKey.has(entry.key))
643
+ continue;
644
+ const change = { type: spec.type, action: 'delete', key: entry.key };
645
+ if (!spec.remove) {
646
+ skippedDeletes.push(change);
647
+ continue;
648
+ }
649
+ if (prune) {
650
+ const rm = spec.remove;
651
+ changes.push(change);
652
+ applications.push(() => rm(http, entry));
653
+ }
654
+ else {
655
+ skippedDeletes.push(change);
656
+ }
657
+ }
658
+ }
659
+ printPlan(ctx, changes, unchanged, skippedDeletes, { dryRun, prune });
660
+ if (dryRun)
661
+ return 0;
662
+ for (const apply of applications)
663
+ await apply();
664
+ if (changes.length)
665
+ ctx.io.out(`Applied ${changes.length} change(s).`);
666
+ else
667
+ ctx.io.out('No changes to apply.');
668
+ return 0;
669
+ }
670
+ function printPlan(ctx, changes, unchanged, skippedDeletes, opts) {
671
+ const heading = opts.dryRun ? 'Plan (dry run — nothing applied):' : 'Plan:';
672
+ ctx.io.out(heading);
673
+ if (changes.length === 0) {
674
+ ctx.io.out(' (no changes)');
675
+ }
676
+ const sign = { create: '+', update: '~', delete: '-' };
677
+ for (const c of [...changes].sort(sortChange)) {
678
+ const label = c.key ? `${c.type} ${c.key}` : c.type;
679
+ ctx.io.out(` ${sign[c.action]} ${c.action} ${label}`);
680
+ }
681
+ if (!opts.prune && skippedDeletes.length) {
682
+ ctx.io.out(` ${skippedDeletes.length} resource(s) exist live but not in config — pass --prune to delete.`);
683
+ }
684
+ ctx.io.out(` ${unchanged} unchanged.`);
685
+ }
686
+ function sortChange(a, b) {
687
+ if (a.type !== b.type)
688
+ return a.type < b.type ? -1 : 1;
689
+ if (a.action !== b.action)
690
+ return a.action < b.action ? -1 : 1;
691
+ return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
692
+ }
693
+ function asArray(value) {
694
+ return Array.isArray(value) ? value : [];
695
+ }
696
+ /**
697
+ * Replace `${ENV_VAR}` placeholders inside any string with the environment's
698
+ * value — the one sanctioned way a secret enters an import without ever living
699
+ * on disk. An unset variable is left verbatim so the plan surfaces it rather
700
+ * than silently blanking a credential.
701
+ */
702
+ function interpolate(value, env) {
703
+ if (typeof value === 'string') {
704
+ return value.replace(/\$\{([A-Z0-9_]+)\}/g, (match, name) => env[name] ?? match);
705
+ }
706
+ if (Array.isArray(value))
707
+ return value.map((v) => interpolate(v, env));
708
+ if (value !== null && typeof value === 'object') {
709
+ const out = {};
710
+ for (const [k, v] of Object.entries(value))
711
+ out[k] = interpolate(v, env);
712
+ return out;
713
+ }
714
+ return value;
715
+ }
716
+ //# sourceMappingURL=config.js.map