auth0-deploy-cli 9.0.0-beta.2 → 9.0.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 (126) hide show
  1. package/AGENTS.md +2 -357
  2. package/CHANGELOG.md +158 -10
  3. package/CLAUDE.md +138 -2
  4. package/README.md +1 -1
  5. package/lib/context/defaults.d.ts +7 -0
  6. package/lib/context/defaults.js +74 -30
  7. package/lib/context/directory/handlers/actionModules.js +1 -4
  8. package/lib/context/directory/handlers/actions.js +1 -4
  9. package/lib/context/directory/handlers/attackProtection.js +1 -1
  10. package/lib/context/directory/handlers/branding.js +4 -1
  11. package/lib/context/directory/handlers/clientAuthCredentials.d.ts +5 -0
  12. package/lib/context/directory/handlers/clientAuthCredentials.js +13 -0
  13. package/lib/context/directory/handlers/clientGrants.js +78 -24
  14. package/lib/context/directory/handlers/clients.js +20 -3
  15. package/lib/context/directory/handlers/connections.js +19 -5
  16. package/lib/context/directory/handlers/databases.js +1 -4
  17. package/lib/context/directory/handlers/emailTemplates.js +5 -0
  18. package/lib/context/directory/handlers/guardianEmailFactorSettings.d.ts +5 -0
  19. package/lib/context/directory/handlers/guardianEmailFactorSettings.js +38 -0
  20. package/lib/context/directory/handlers/guardianPhoneFactorSettings.d.ts +5 -0
  21. package/lib/context/directory/handlers/guardianPhoneFactorSettings.js +38 -0
  22. package/lib/context/directory/handlers/guardianSettings.d.ts +5 -0
  23. package/lib/context/directory/handlers/guardianSettings.js +38 -0
  24. package/lib/context/directory/handlers/hooks.js +1 -4
  25. package/lib/context/directory/handlers/index.js +12 -0
  26. package/lib/context/directory/handlers/networkACLKeys.d.ts +6 -0
  27. package/lib/context/directory/handlers/networkACLKeys.js +54 -0
  28. package/lib/context/directory/handlers/prompts.js +7 -3
  29. package/lib/context/directory/handlers/rateLimitPolicies.d.ts +6 -0
  30. package/lib/context/directory/handlers/rateLimitPolicies.js +53 -0
  31. package/lib/context/directory/handlers/rules.js +1 -4
  32. package/lib/context/directory/index.js +1 -4
  33. package/lib/context/index.js +31 -31
  34. package/lib/context/yaml/handlers/attackProtection.js +1 -1
  35. package/lib/context/yaml/handlers/branding.js +4 -1
  36. package/lib/context/yaml/handlers/clientAuthCredentials.d.ts +5 -0
  37. package/lib/context/yaml/handlers/clientAuthCredentials.js +13 -0
  38. package/lib/context/yaml/handlers/clients.js +19 -1
  39. package/lib/context/yaml/handlers/connections.js +8 -1
  40. package/lib/context/yaml/handlers/flows.js +3 -0
  41. package/lib/context/yaml/handlers/forms.js +3 -0
  42. package/lib/context/yaml/handlers/guardianEmailFactorSettings.d.ts +5 -0
  43. package/lib/context/yaml/handlers/guardianEmailFactorSettings.js +15 -0
  44. package/lib/context/yaml/handlers/guardianPhoneFactorSettings.d.ts +5 -0
  45. package/lib/context/yaml/handlers/guardianPhoneFactorSettings.js +15 -0
  46. package/lib/context/yaml/handlers/guardianSettings.d.ts +5 -0
  47. package/lib/context/yaml/handlers/guardianSettings.js +15 -0
  48. package/lib/context/yaml/handlers/index.js +12 -0
  49. package/lib/context/yaml/handlers/networkACLKeys.d.ts +6 -0
  50. package/lib/context/yaml/handlers/networkACLKeys.js +42 -0
  51. package/lib/context/yaml/handlers/prompts.js +3 -1
  52. package/lib/context/yaml/handlers/rateLimitPolicies.d.ts +6 -0
  53. package/lib/context/yaml/handlers/rateLimitPolicies.js +27 -0
  54. package/lib/context/yaml/index.js +1 -4
  55. package/lib/keywordPreservation.d.ts +1 -1
  56. package/lib/keywordPreservation.js +2 -1
  57. package/lib/readonly.js +5 -0
  58. package/lib/tools/auth0/handlers/attackProtection.js +19 -6
  59. package/lib/tools/auth0/handlers/clientAuthCredentials.d.ts +27 -0
  60. package/lib/tools/auth0/handlers/clientAuthCredentials.js +234 -0
  61. package/lib/tools/auth0/handlers/clientAuthCredentialsPre.d.ts +38 -0
  62. package/lib/tools/auth0/handlers/clientAuthCredentialsPre.js +123 -0
  63. package/lib/tools/auth0/handlers/clientGrants.d.ts +1 -1
  64. package/lib/tools/auth0/handlers/clients.d.ts +106 -0
  65. package/lib/tools/auth0/handlers/clients.js +206 -2
  66. package/lib/tools/auth0/handlers/connectionProfiles.d.ts +27 -0
  67. package/lib/tools/auth0/handlers/connectionProfiles.js +30 -0
  68. package/lib/tools/auth0/handlers/connections.d.ts +32 -6
  69. package/lib/tools/auth0/handlers/connections.js +49 -7
  70. package/lib/tools/auth0/handlers/databases.d.ts +28 -0
  71. package/lib/tools/auth0/handlers/databases.js +58 -24
  72. package/lib/tools/auth0/handlers/default.d.ts +23 -0
  73. package/lib/tools/auth0/handlers/default.js +47 -29
  74. package/lib/tools/auth0/handlers/eventStreams.js +10 -2
  75. package/lib/tools/auth0/handlers/guardianEmailFactorSettings.d.ts +22 -0
  76. package/lib/tools/auth0/handlers/guardianEmailFactorSettings.js +77 -0
  77. package/lib/tools/auth0/handlers/guardianFactorTemplates.js +13 -4
  78. package/lib/tools/auth0/handlers/guardianPhoneFactorMessageTypes.js +11 -9
  79. package/lib/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.js +11 -9
  80. package/lib/tools/auth0/handlers/guardianPhoneFactorSettings.d.ts +22 -0
  81. package/lib/tools/auth0/handlers/guardianPhoneFactorSettings.js +77 -0
  82. package/lib/tools/auth0/handlers/guardianPolicies.js +11 -1
  83. package/lib/tools/auth0/handlers/guardianSettings.d.ts +30 -0
  84. package/lib/tools/auth0/handlers/guardianSettings.js +85 -0
  85. package/lib/tools/auth0/handlers/hooks.js +1 -1
  86. package/lib/tools/auth0/handlers/index.js +16 -0
  87. package/lib/tools/auth0/handlers/networkACLKeys.d.ts +45 -0
  88. package/lib/tools/auth0/handlers/networkACLKeys.js +177 -0
  89. package/lib/tools/auth0/handlers/networkACLs.d.ts +132 -2
  90. package/lib/tools/auth0/handlers/networkACLs.js +100 -16
  91. package/lib/tools/auth0/handlers/organizations.d.ts +33 -1
  92. package/lib/tools/auth0/handlers/organizations.js +177 -19
  93. package/lib/tools/auth0/handlers/phoneProvider.js +10 -2
  94. package/lib/tools/auth0/handlers/phoneTemplates.d.ts +4 -1
  95. package/lib/tools/auth0/handlers/phoneTemplates.js +67 -10
  96. package/lib/tools/auth0/handlers/prompts.js +1 -1
  97. package/lib/tools/auth0/handlers/rateLimitPolicies.d.ts +81 -0
  98. package/lib/tools/auth0/handlers/rateLimitPolicies.js +203 -0
  99. package/lib/tools/auth0/handlers/resourceServers.d.ts +44 -0
  100. package/lib/tools/auth0/handlers/resourceServers.js +44 -0
  101. package/lib/tools/auth0/handlers/riskAssessment.d.ts +1 -1
  102. package/lib/tools/auth0/handlers/riskAssessment.js +19 -1
  103. package/lib/tools/auth0/handlers/roles.d.ts +4 -0
  104. package/lib/tools/auth0/handlers/roles.js +10 -0
  105. package/lib/tools/auth0/handlers/rules.js +19 -7
  106. package/lib/tools/auth0/handlers/tenant.d.ts +45 -0
  107. package/lib/tools/auth0/handlers/tenant.js +48 -0
  108. package/lib/tools/auth0/handlers/themes.d.ts +37 -1
  109. package/lib/tools/auth0/handlers/themes.js +64 -0
  110. package/lib/tools/auth0/handlers/tokenExchangeProfiles.js +3 -1
  111. package/lib/tools/auth0/index.js +12 -6
  112. package/lib/tools/calculateDryRunChanges.js +12 -8
  113. package/lib/tools/constants.d.ts +2 -0
  114. package/lib/tools/constants.js +3 -0
  115. package/lib/tools/utils.d.ts +3 -0
  116. package/lib/tools/utils.js +41 -2
  117. package/lib/types.d.ts +9 -1
  118. package/lib/utils.d.ts +6 -0
  119. package/lib/utils.js +17 -2
  120. package/package.json +2 -2
  121. package/references/code-style.md +133 -0
  122. package/references/commands.md +98 -0
  123. package/references/docs-update.md +34 -0
  124. package/references/git-workflow.md +135 -0
  125. package/references/pitfalls.md +62 -0
  126. package/references/testing.md +118 -0
package/CLAUDE.md CHANGED
@@ -1,3 +1,139 @@
1
- # For project context see
1
+ # AI Agent Guidelines for auth0-deploy-cli
2
2
 
3
- - [AGENTS.md](./AGENTS.md)
3
+ ## Your Role
4
+
5
+ You are a TypeScript CLI engineer maintaining auth0-deploy-cli — a bidirectional sync tool for Auth0 tenant configurations. You write resource handlers, context parsers, and change-detection logic.
6
+
7
+ ---
8
+
9
+ ## Project Structure
10
+
11
+ ```
12
+ auth0-deploy-cli/
13
+ ├── src/ # TypeScript source
14
+ │ ├── index.ts # CLI entry point (export + import commands)
15
+ │ ├── commands/ # import.ts, export.ts — command wiring
16
+ │ ├── configFactory.ts # Configuration loading and validation
17
+ │ ├── types.ts # Shared TypeScript interfaces
18
+ │ ├── context/
19
+ │ │ ├── directory/ # Directory-format parser + keyword replacement
20
+ │ │ └── yaml/ # YAML-format parser + keyword replacement
21
+ │ └── tools/
22
+ │ ├── deploy.ts # Main deployment orchestrator
23
+ │ ├── calculateChanges.ts # Change detection (create / update / delete)
24
+ │ ├── constants.ts # Supported resource type registry
25
+ │ ├── validationError.ts # ValidationError class
26
+ │ └── auth0/
27
+ │ ├── client.ts # Management API client wrapper
28
+ │ ├── handlers/ # 49 resource-specific handlers (schemas embedded per handler)
29
+ │ └── schema.ts # AJV schema aggregator (compiled from handler schemas)
30
+ ├── test/ # Mirrors src/
31
+ │ ├── tools/auth0/handlers/ # Handler unit tests
32
+ │ ├── context/ # Context parser tests
33
+ │ └── e2e/ # E2E tests (require real Auth0 tenant)
34
+ ├── examples/
35
+ │ ├── directory/ # Sample directory-format config
36
+ │ └── yaml/ # Sample YAML-format config
37
+ ├── docs/ # Extended documentation
38
+ ├── .circleci/config.yml # CI: lint → unit tests → publish
39
+ └── package.json # npm scripts and dependencies
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Boundaries
45
+
46
+ ### ✅ Always Do
47
+
48
+ - Run `npm run build && npm test && npm run lint` before committing
49
+ - Make surgical changes — touch only what the request requires; do not refactor or reformat adjacent code that isn't broken
50
+ - Support both YAML and directory context formats when implementing or changing resource handling
51
+ - Preserve `@@KEY@@` (JSON-stringified / array values) and `##KEY##` (literal string) keyword replacement in every new field that flows through export/import
52
+ - Strip write-only fields (secrets, key material) on export; exclude read-only API-generated fields (`created_at`, `updated_at`, `id`, `fingerprint`) from create/update payloads
53
+ - Use `name` (or a stable name-like field) as the primary entry in a handler's `identifiers[]` array — never solely an auto-generated UUID `id`
54
+ - Place new handlers in `@order()` after all their dependencies
55
+ - Add unit tests for new logic; test both the happy path and error paths
56
+ - Update `README.md` and `examples/` in the same PR when changing CLI commands, flags, output formats, or supported resource types
57
+
58
+ ### ⚠️ Ask First
59
+
60
+ - Any breaking change — never break backward compatibility on your own initiative; stop and ask first
61
+ - Adding or bumping dependencies in `package.json`
62
+ - Modifying public CLI commands, subcommands, or flag names
63
+ - Changes to `.circleci/config.yml`
64
+ - Modifying keyword replacement logic or the export/import schema
65
+ - Running E2E tests — they hit a real Auth0 tenant and can mutate resources (see [references/testing.md](references/testing.md))
66
+ - **EA / entitlement-gated features**: Before marking complete — document every code path you could not test end-to-end, spell out the exact API shape assumptions your code relies on, verify that the list endpoint returns the fields read from `this.existing`, and create a follow-up ticket to re-verify once entitlement is available
67
+
68
+ ### 🚫 Never Do
69
+
70
+ - Commit secrets, API keys, Auth0 credentials, or PEM keys
71
+ - Mutate state on the dry-run path — `processChanges()` must be a complete no-op during dry run on every affected path
72
+ - Use an auto-generated UUID as the sole entry in `identifiers[]`; always include `name` as the primary matching key
73
+ - Log or export write-only fields (secrets, key material, tokens)
74
+ - Add `additionalProperties: false` to a schema for an EA-gated object without confirming the full API response shape from a real tenant response
75
+ - Modify `node_modules/`, `lib/`, or other build-output directories by hand
76
+
77
+ ---
78
+
79
+ ## Security Considerations
80
+
81
+ - Auth0 credentials (`AUTH0_DOMAIN`, `AUTH0_CLIENT_ID`, `AUTH0_CLIENT_SECRET`) are never committed; `config*.json` and `.env` are gitignored
82
+ - Write-only fields (secrets, signing keys, PEM material) are stripped on export and never written to disk
83
+ - Delete operations require explicit `AUTH0_ALLOW_DELETE=true`; absent this flag, the handler silently skips deletes
84
+ - Resource identifiers are sanitized before use in API calls
85
+ - No telemetry in this repo — deploy-cli delegates all Auth0 API calls to the `auth0` npm SDK, which handles its own telemetry
86
+
87
+ ---
88
+
89
+ > The sections below are **reference** — each keeps a one-line anchor inline and offloads its body to `references/*.md`.
90
+
91
+ ## Commands
92
+
93
+ ```bash
94
+ npm run build # compile TypeScript → lib/
95
+ npm test # unit tests (safe — no credentials required)
96
+ npm run lint # eslint + kacl changelog lint
97
+ ```
98
+
99
+ See [references/commands.md](references/commands.md) for the full command reference including E2E tests, watch mode, and direct CLI invocation. Read when you need to build, test, or run the CLI.
100
+
101
+ ---
102
+
103
+ ## Testing
104
+
105
+ Unit tests (`npm test`) are safe — no credentials required. E2E tests require a real Auth0 tenant; ask before running (see Boundaries).
106
+
107
+ See [references/testing.md](references/testing.md) for test conventions, sinon mock patterns, handler test scaffolding, and E2E requirements. Read when writing or debugging tests.
108
+
109
+ ---
110
+
111
+ ## Code Style
112
+
113
+ Single quotes, semicolons required, `no-unused-vars` — all three fail CI via ESLint (`eslint.config.js`). TypeScript strict mode is off; do not enable without asking first.
114
+
115
+ See [references/code-style.md](references/code-style.md) for naming conventions, handler implementation patterns, and good/bad examples. Read when adding a new handler or context parser.
116
+
117
+ ---
118
+
119
+ ## Git Workflow
120
+
121
+ Conventional Commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`). Branch naming: `DXCDT-XXXX` (Jira ticket number).
122
+
123
+ See [references/git-workflow.md](references/git-workflow.md) for PR conventions and the full PR review checklist. Read before creating or reviewing a PR.
124
+
125
+ ---
126
+
127
+ ## Common Pitfalls
128
+
129
+ See [references/pitfalls.md](references/pitfalls.md). Read before implementing a new handler or touching keyword replacement, dry-run, EA-gated features, or the export/import roundtrip.
130
+
131
+ ---
132
+
133
+ ## Docs Update Rules
134
+
135
+ `README.md` — present. `EXAMPLES.md` — missing (create when adding a new integration pattern or command example). `examples/` — present (yaml/ and directory/ samples).
136
+
137
+ > Update `README.md` and `examples/` in the same PR when changing CLI commands, flags, output formats, or supported resource types. Do not defer.
138
+
139
+ See [references/docs-update.md](references/docs-update.md) for the full tracked-docs inventory and code-to-docs mapping table. Read when adding or changing commands, flags, or resource types.
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  The Auth0 Deploy CLI is a tool that helps you manage your Auth0 tenant configuration. It integrates into your development workflows as a standalone CLI or as a node module.
10
10
 
11
- **Supported resource types:** actions, branding, client grants, clients (applications), connections, databases, custom domains, email templates, emails, grants, guardian, hook secrets, log streams, organizations, pages, prompts, resource servers (APIs), roles, tenant settings, themes, forms, flows, self-service profiles, network ACLs.
11
+ **Supported resource types:** actions, branding, client grants, clients (applications), connections, databases, custom domains, email templates, emails, grants, guardian, hook secrets, log streams, organizations, pages, prompts, resource servers (APIs), roles, tenant settings, themes, forms, flows, self-service profiles, network ACLs, network ACL keys.
12
12
 
13
13
  🎢 [Highlights](#highlights) • 📚 [Documentation](#documentation) • 🚀 [Getting Started](#getting-started) • 📝 [Feedback](#feedback) • 💬 [Discussions](https://github.com/auth0/auth0-deploy-cli/discussions)
14
14
 
@@ -6,4 +6,11 @@ export declare function phoneTemplatesDefaults(phoneTemplate: any): any;
6
6
  export declare function connectionDefaults(connection: any, config?: Pick<Config, 'AUTH0_EXPORT_SECRETS'>): any;
7
7
  export declare function logStreamDefaults(logStreams: any, config?: Pick<Config, 'AUTH0_EXPORT_SECRETS'>): any;
8
8
  export declare function eventStreamDefaults(eventStreams: any[], config?: Pick<Config, 'AUTH0_EXPORT_SECRETS'>): any[];
9
+ /**
10
+ * Sorts primitive arrays (e.g. `shields`, `admin_notification_frequency`) within the
11
+ * attackProtection subtree in place, so repeated exports produce deterministic output.
12
+ * These are unordered, string-typed enum sets — ordering is cosmetic. Object arrays are
13
+ * left as-is.
14
+ */
15
+ export declare function sortAttackProtectionArrays(attackProtection: AttackProtection): AttackProtection;
9
16
  export declare function attackProtectionDefaults(attackProtection: AttackProtection, config?: Pick<Config, 'AUTH0_EXPORT_SECRETS'>): AttackProtection;
@@ -6,6 +6,7 @@ exports.phoneTemplatesDefaults = phoneTemplatesDefaults;
6
6
  exports.connectionDefaults = connectionDefaults;
7
7
  exports.logStreamDefaults = logStreamDefaults;
8
8
  exports.eventStreamDefaults = eventStreamDefaults;
9
+ exports.sortAttackProtectionArrays = sortAttackProtectionArrays;
9
10
  exports.attackProtectionDefaults = attackProtectionDefaults;
10
11
  const utils_1 = require("../tools/utils");
11
12
  // env vars arrive as strings, so check both forms
@@ -22,45 +23,57 @@ function emailProviderDefaults(emailProvider, config) {
22
23
  // Add placeholder for credentials as they cannot be exported
23
24
  const { name } = updated;
24
25
  if (apiKeyProviders.includes(name)) {
25
- updated.credentials = {
26
- api_key: `##${name.toUpperCase()}_API_KEY##`,
27
- ...(updated.credentials || {}),
28
- };
26
+ if (typeof updated.credentials !== 'string') {
27
+ updated.credentials = {
28
+ api_key: `##${name.toUpperCase()}_API_KEY##`,
29
+ ...(updated.credentials || {}),
30
+ };
31
+ }
29
32
  }
30
33
  if (name === 'smtp') {
31
- // This is to mask smtp_user to '##SMTP_USER##'
32
- if (updated.credentials && 'smtp_user' in updated.credentials) {
33
- delete updated.credentials.smtp_user;
34
+ // If credentials is a keyword placeholder string (e.g. @@SMTP_CREDENTIALS@@), preserve it as-is.
35
+ // The `in` operator requires an object and would throw a TypeError on a string.
36
+ if (typeof updated.credentials !== 'string') {
37
+ // This is to mask smtp_user to '##SMTP_USER##'
38
+ if (updated.credentials && 'smtp_user' in updated.credentials) {
39
+ delete updated.credentials.smtp_user;
40
+ }
41
+ updated.credentials = {
42
+ smtp_host: '##SMTP_HOSTNAME##',
43
+ smtp_port: '##SMTP_PORT##',
44
+ smtp_user: '##SMTP_USER##',
45
+ smtp_pass: '##SMTP_PASS##',
46
+ ...(updated.credentials || {}),
47
+ };
34
48
  }
35
- updated.credentials = {
36
- smtp_host: '##SMTP_HOSTNAME##',
37
- smtp_port: '##SMTP_PORT##',
38
- smtp_user: '##SMTP_USER##',
39
- smtp_pass: '##SMTP_PASS##',
40
- ...(updated.credentials || {}),
41
- };
42
49
  }
43
50
  if (name === 'ses') {
44
- updated.credentials = {
45
- accessKeyId: '##SES_ACCESS_KEY_ID##',
46
- secretAccessKey: '##SES_ACCESS_SECRET_KEY##',
47
- region: '##SES_AWS_REGION##',
48
- ...(updated.credentials || {}),
49
- };
51
+ if (typeof updated.credentials !== 'string') {
52
+ updated.credentials = {
53
+ accessKeyId: '##SES_ACCESS_KEY_ID##',
54
+ secretAccessKey: '##SES_ACCESS_SECRET_KEY##',
55
+ region: '##SES_AWS_REGION##',
56
+ ...(updated.credentials || {}),
57
+ };
58
+ }
50
59
  }
51
60
  if (name === 'azure_cs') {
52
- updated.credentials = {
53
- connectionString: '##AZURE_CS_CONNECTION_KEY##',
54
- ...(updated.credentials || {}),
55
- };
61
+ if (typeof updated.credentials !== 'string') {
62
+ updated.credentials = {
63
+ connectionString: '##AZURE_CS_CONNECTION_KEY##',
64
+ ...(updated.credentials || {}),
65
+ };
66
+ }
56
67
  }
57
68
  if (name === 'ms365') {
58
- updated.credentials = {
59
- tenantId: '##MS365_TENANT_ID##',
60
- clientId: '##MS365_CLIENT_ID##',
61
- clientSecret: '##MS365_CLIENT_SECRET##',
62
- ...(updated.credentials || {}),
63
- };
69
+ if (typeof updated.credentials !== 'string') {
70
+ updated.credentials = {
71
+ tenantId: '##MS365_TENANT_ID##',
72
+ clientId: '##MS365_CLIENT_ID##',
73
+ clientSecret: '##MS365_CLIENT_SECRET##',
74
+ ...(updated.credentials || {}),
75
+ };
76
+ }
64
77
  }
65
78
  return updated;
66
79
  }
@@ -87,6 +100,12 @@ function phoneProviderDefaults(phoneProvider) {
87
100
  auth_token: `##${name.toUpperCase()}_AUTH_TOKEN##`,
88
101
  };
89
102
  }
103
+ else if (name === 'custom') {
104
+ // The `custom` provider has no exportable secrets, but the API requires a
105
+ // `credentials` object on import even when empty. Emit an empty object so
106
+ // exported configs round-trip cleanly.
107
+ updated.credentials = {};
108
+ }
90
109
  return updated;
91
110
  }
92
111
  function phoneTemplatesDefaults(phoneTemplate) {
@@ -193,6 +212,31 @@ function eventStreamDefaults(eventStreams, config) {
193
212
  };
194
213
  });
195
214
  }
215
+ /**
216
+ * Sorts primitive arrays (e.g. `shields`, `admin_notification_frequency`) within the
217
+ * attackProtection subtree in place, so repeated exports produce deterministic output.
218
+ * These are unordered, string-typed enum sets — ordering is cosmetic. Object arrays are
219
+ * left as-is.
220
+ */
221
+ function sortAttackProtectionArrays(attackProtection) {
222
+ const sortArraysDeep = (value) => {
223
+ if (Array.isArray(value)) {
224
+ const isPrimitiveArray = value.every((item) => item === null || typeof item !== 'object');
225
+ if (isPrimitiveArray) {
226
+ value.sort();
227
+ }
228
+ else {
229
+ value.forEach(sortArraysDeep);
230
+ }
231
+ return;
232
+ }
233
+ if (value && typeof value === 'object') {
234
+ Object.values(value).forEach(sortArraysDeep);
235
+ }
236
+ };
237
+ sortArraysDeep(attackProtection);
238
+ return attackProtection;
239
+ }
196
240
  function attackProtectionDefaults(attackProtection, config) {
197
241
  if (isExportSecrets(config))
198
242
  return attackProtection;
@@ -24,10 +24,7 @@ function parse(context) {
24
24
  const normalizedCode = module.code.replace(/\\/g, '/');
25
25
  const configRoot = path_1.default.resolve(context.filePath);
26
26
  const resolvedPath = path_1.default.resolve(context.filePath, normalizedCode);
27
- if (!resolvedPath.startsWith(configRoot + path_1.default.sep)) {
28
- throw new Error(`Path "${module.code}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
29
- `Move the file inside your config directory.`);
30
- }
27
+ (0, utils_1.assertInsideConfigRoot)(module.code, resolvedPath, configRoot);
31
28
  module.code = (0, tools_1.loadFileAndReplaceKeywords)(resolvedPath, {
32
29
  mappings: context.mappings,
33
30
  disableKeywordReplacement: context.disableKeywordReplacement,
@@ -26,10 +26,7 @@ function parse(context) {
26
26
  const normalizedCode = action.code.replace(/\\/g, '/');
27
27
  const configRoot = path_1.default.resolve(context.filePath);
28
28
  const resolvedPath = path_1.default.resolve(context.filePath, normalizedCode);
29
- if (!resolvedPath.startsWith(configRoot + path_1.default.sep)) {
30
- throw new Error(`Path "${action.code}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
31
- `Move the file inside your config directory.`);
32
- }
29
+ (0, utils_1.assertInsideConfigRoot)(action.code, resolvedPath, configRoot);
33
30
  action.code = (0, tools_1.loadFileAndReplaceKeywords)(resolvedPath, {
34
31
  mappings: context.mappings,
35
32
  disableKeywordReplacement: context.disableKeywordReplacement,
@@ -65,7 +65,7 @@ async function dump(context) {
65
65
  return;
66
66
  const files = attackProtectionFiles(context.filePath);
67
67
  fs_extra_1.default.ensureDirSync(files.directory);
68
- const maskedAttackProtection = (0, defaults_1.attackProtectionDefaults)(attackProtection, context.config);
68
+ const maskedAttackProtection = (0, defaults_1.sortAttackProtectionArrays)((0, defaults_1.attackProtectionDefaults)(attackProtection, context.config));
69
69
  if (maskedAttackProtection.botDetection) {
70
70
  (0, utils_1.dumpJSON)(files.botDetection, maskedAttackProtection.botDetection);
71
71
  }
@@ -31,7 +31,10 @@ function parse(context) {
31
31
  disableKeywordReplacement: context.disableKeywordReplacement,
32
32
  });
33
33
  const normalizedPathArray = (0, utils_1.nomalizedYAMLPath)(definition.body);
34
- definition.body = (0, tools_1.loadFileAndReplaceKeywords)(path_1.default.join(brandingTemplatesFolder, ...normalizedPathArray), {
34
+ const resolvedBodyFile = path_1.default.resolve(brandingTemplatesFolder, ...normalizedPathArray);
35
+ const configRoot = path_1.default.resolve(context.filePath);
36
+ (0, utils_1.assertInsideConfigRoot)(definition.body, resolvedBodyFile, configRoot);
37
+ definition.body = (0, tools_1.loadFileAndReplaceKeywords)(resolvedBodyFile, {
35
38
  mappings: context.mappings,
36
39
  disableKeywordReplacement: context.disableKeywordReplacement,
37
40
  });
@@ -0,0 +1,5 @@
1
+ import { DirectoryHandler } from '.';
2
+ import { ParsedAsset } from '../../../types';
3
+ type ParsedClientAuthCredentials = ParsedAsset<'clientAuthCredentials', null>;
4
+ declare const clientAuthCredentialsHandler: DirectoryHandler<ParsedClientAuthCredentials>;
5
+ export default clientAuthCredentialsHandler;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ function parse(_context) {
4
+ return { clientAuthCredentials: null };
5
+ }
6
+ async function dump(_context) {
7
+ // Credentials are embedded inside each client's JSON file — nothing to dump separately.
8
+ }
9
+ const clientAuthCredentialsHandler = {
10
+ parse,
11
+ dump,
12
+ };
13
+ exports.default = clientAuthCredentialsHandler;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const path_1 = __importDefault(require("path"));
7
7
  const fs_extra_1 = __importDefault(require("fs-extra"));
8
8
  const tools_1 = require("../../../tools");
9
+ const logger_1 = __importDefault(require("../../../logger"));
9
10
  const utils_1 = require("../../../utils");
10
11
  const client_1 = require("../../../tools/auth0/client");
11
12
  const keywordPreservation_1 = require("../../../keywordPreservation");
@@ -41,34 +42,23 @@ async function dump(context) {
41
42
  paginate: true,
42
43
  include_totals: true,
43
44
  });
44
- // Filter out grants for excluded clients
45
- if (excludedClientsByNames.length) {
46
- const excludedClientIds = new Set(allClients
47
- .filter((c) => c.name !== undefined && excludedClientsByNames.includes(c.name))
48
- .map((c) => c.client_id));
49
- clientGrants = clientGrants.filter((grant) => !excludedClientIds.has(grant.client_id));
50
- }
51
- // Convert client_id to the client name for readability
52
- clientGrants.forEach((grant) => {
53
- const dumpGrant = { ...grant };
54
- if (context.assets.clientsOrig) {
55
- dumpGrant.client_id = (0, utils_1.convertClientIdToName)(dumpGrant.client_id, context.assets.clientsOrig);
56
- }
45
+ // Convert audience to the API name for readability
46
+ const apiName = (grantAudience) => {
47
+ if (!grantAudience)
48
+ return grantAudience;
49
+ const associatedAPI = allResourceServers.find((resourceServer) => resourceServer.identifier === grantAudience);
50
+ if (associatedAPI === undefined)
51
+ return grantAudience; // Use the audience if the API is not found
52
+ return associatedAPI.name; // Use the name of the API
53
+ };
54
+ // Derive the filename for a grant.
55
+ const nameFor = (grant) => {
57
56
  const clientName = (() => {
58
57
  const associatedClient = allClients.find((client) => client.client_id === grant.client_id);
59
58
  if (associatedClient === undefined)
60
59
  return grant.client_id;
61
60
  return associatedClient.name;
62
61
  })();
63
- // Convert audience to the API name for readability
64
- const apiName = (grantAudience) => {
65
- if (!grantAudience)
66
- return grantAudience;
67
- const associatedAPI = allResourceServers.find((resourceServer) => resourceServer.identifier === grantAudience);
68
- if (associatedAPI === undefined)
69
- return grantAudience; // Use the audience if the API is not found
70
- return associatedAPI.name; // Use the name of the API
71
- };
72
62
  // Replace keyword markers if necessary
73
63
  const clientNameNonMarker = (0, keywordPreservation_1.doesHaveKeywordMarker)(clientName, context.mappings)
74
64
  ? (0, tools_1.keywordReplace)(clientName, context.mappings)
@@ -76,14 +66,78 @@ async function dump(context) {
76
66
  const apiAudienceNonMarker = (0, keywordPreservation_1.doesHaveKeywordMarker)(grant.audience, context.mappings)
77
67
  ? (0, tools_1.keywordReplace)(grant.audience, context.mappings)
78
68
  : grant.audience;
79
- // Construct the name using non-marker names
80
- const name = (0, utils_1.sanitize)(`${clientNameNonMarker}-${apiName(apiAudienceNonMarker)}`);
69
+ // Construct the name using non-marker names. `subject_type` is part of a grant's identity
70
+ // (see `identifiers` in src/tools/auth0/handlers/clientGrants.ts), so it must be included:
71
+ // without it, grants differing only by subject type (e.g. `client` vs `user` on the same
72
+ // client and audience) resolve to the same filename and silently overwrite each other.
73
+ const baseName = `${clientNameNonMarker}-${apiName(apiAudienceNonMarker)}`;
74
+ return (0, utils_1.sanitize)(grant.subject_type ? `${baseName}-${grant.subject_type}` : baseName);
75
+ };
76
+ const excludedClients = allClients.filter((c) => c.name !== undefined && excludedClientsByNames.includes(c.name));
77
+ // Values that can stand for an excluded client in the `client_id` field of a dumped file: the
78
+ // client name when `clientsOrig` was available at dump time (see `convertClientIdToName` below),
79
+ // the raw client_id otherwise. Names come from the exclude list rather than from `allClients` so
80
+ // that excluding a client absent from the tenant still protects its file.
81
+ const excludedClientIdentities = new Set([
82
+ ...excludedClientsByNames,
83
+ ...excludedClients.map((c) => c.client_id).filter((id) => !!id),
84
+ ]);
85
+ // Whether a file this dump did not write must nonetheless survive the cleanup pass. Its name
86
+ // cannot answer that: the name is derived from the client name, the API name, the grant's
87
+ // subject_type and the current naming format, so a file written by an earlier version — or
88
+ // before its API was renamed — no longer matches the name `nameFor` produces today. Read the
89
+ // file instead, because the client identity recorded inside it does not drift.
90
+ const mustPreserve = (file) => {
91
+ if (excludedClientIdentities.size === 0)
92
+ return false;
93
+ let grant;
94
+ try {
95
+ grant = (0, utils_1.loadJSON)(file, {
96
+ mappings: context.mappings,
97
+ disableKeywordReplacement: context.disableKeywordReplacement,
98
+ });
99
+ }
100
+ catch (err) {
101
+ // Deleting a file it cannot read is not the export's call to make, and one bad file must not
102
+ // fail the whole export. Keep it and let `parse` report the problem on the next import.
103
+ logger_1.default.warn(`Keeping ${file}, it could not be read while cleaning up client grants: ${err}`);
104
+ return true;
105
+ }
106
+ return excludedClientIdentities.has(grant?.client_id);
107
+ };
108
+ // Track files written by this dump; everything else in the folder is a cleanup candidate.
109
+ const expectedFiles = new Set();
110
+ // Filter out grants for excluded clients
111
+ if (excludedClientsByNames.length) {
112
+ const excludedClientIds = new Set(excludedClients.map((c) => c.client_id));
113
+ clientGrants = clientGrants.filter((grant) => !excludedClientIds.has(grant.client_id));
114
+ }
115
+ // Convert client_id to the client name for readability
116
+ clientGrants.forEach((grant) => {
117
+ const dumpGrant = { ...grant };
118
+ if (context.assets.clientsOrig) {
119
+ dumpGrant.client_id = (0, utils_1.convertClientIdToName)(dumpGrant.client_id, context.assets.clientsOrig);
120
+ }
121
+ const name = nameFor(grant);
81
122
  // Ensure the name is not empty or invalid
82
123
  if (!name || name.trim().length === 0) {
83
124
  throw new Error(`Invalid name generated for client grant: ${JSON.stringify(grant)}`);
84
125
  }
85
126
  const grantFile = path_1.default.join(grantsFolder, `${name}.json`);
86
127
  (0, utils_1.dumpJSON)(grantFile, dumpGrant);
128
+ expectedFiles.add(`${name}.json`);
129
+ });
130
+ // Remove files that belong to grants no longer present (and not excluded). Without this, a grant
131
+ // whose filename changes is left behind under its old name and parsed back as a duplicate on the
132
+ // next import, and grants deleted from the tenant are silently recreated.
133
+ //
134
+ // Restricted to the `.json` files `parse` reads: anything else in the folder (a README, notes)
135
+ // can never come back as a grant, so it is not stale state and must not be deleted.
136
+ (0, utils_1.getFiles)(grantsFolder, ['.json'])
137
+ .filter((file) => !expectedFiles.has(path_1.default.basename(file)) && !mustPreserve(file))
138
+ .forEach((file) => {
139
+ logger_1.default.info(`Removing ${file}`);
140
+ fs_extra_1.default.removeSync(file);
87
141
  });
88
142
  }
89
143
  const clientGrantsHandler = {
@@ -20,9 +20,11 @@ function parse(context) {
20
20
  disableKeywordReplacement: context.disableKeywordReplacement,
21
21
  });
22
22
  if (client.custom_login_page) {
23
- const htmlFileName = path_1.default.join(clientsFolder, client.custom_login_page);
24
- if ((0, utils_1.isFile)(htmlFileName)) {
25
- client.custom_login_page = (0, tools_1.loadFileAndReplaceKeywords)(htmlFileName, {
23
+ const configRoot = path_1.default.resolve(context.filePath);
24
+ const resolvedLoginPage = path_1.default.resolve(clientsFolder, client.custom_login_page);
25
+ if ((0, utils_1.isFile)(resolvedLoginPage)) {
26
+ (0, utils_1.assertInsideConfigRoot)(client.custom_login_page, resolvedLoginPage, configRoot);
27
+ client.custom_login_page = (0, tools_1.loadFileAndReplaceKeywords)(resolvedLoginPage, {
26
28
  mappings: context.mappings,
27
29
  disableKeywordReplacement: context.disableKeywordReplacement,
28
30
  });
@@ -106,6 +108,21 @@ async function dump(context) {
106
108
  organization_require_behavior: client.organization_require_behavior,
107
109
  };
108
110
  }
111
+ else {
112
+ // strip id-only credentials and remove client_authentication_methods entirely
113
+ // if no named credentials remain — an absent field is treated as deletion on deploy
114
+ if (client.client_authentication_methods) {
115
+ Object.values(client.client_authentication_methods).forEach((method) => {
116
+ if (method?.credentials) {
117
+ method.credentials = method.credentials.filter((c) => c.name);
118
+ }
119
+ });
120
+ const hasAnyCredentials = Object.values(client.client_authentication_methods).some((method) => method?.credentials?.length > 0);
121
+ if (!hasAnyCredentials) {
122
+ delete client.client_authentication_methods;
123
+ }
124
+ }
125
+ }
109
126
  (0, utils_1.dumpJSON)(clientFile, (0, utils_1.clearClientArrays)(client));
110
127
  });
111
128
  }
@@ -23,11 +23,13 @@ function parse(context) {
23
23
  });
24
24
  if (connection.strategy === 'email') {
25
25
  (0, utils_1.ensureProp)(connection, 'options.email.body');
26
- const htmlFileName = path_1.default.join(connectionsFolder, connection.options.email.body);
27
- if (!(0, utils_1.isFile)(htmlFileName)) {
28
- throw new Error(`Passwordless email template purportedly located at ${htmlFileName} does not exist for connection. Ensure the existence of this file to proceed with deployment.`);
26
+ const configRoot = path_1.default.resolve(context.filePath);
27
+ const resolvedHtmlFile = path_1.default.resolve(connectionsFolder, connection.options.email.body);
28
+ if (!(0, utils_1.isFile)(resolvedHtmlFile)) {
29
+ throw new Error(`Passwordless email template purportedly located at ${resolvedHtmlFile} does not exist for connection. Ensure the existence of this file to proceed with deployment.`);
29
30
  }
30
- connection.options.email.body = (0, tools_1.loadFileAndReplaceKeywords)(htmlFileName, {
31
+ (0, utils_1.assertInsideConfigRoot)(connection.options.email.body, resolvedHtmlFile, configRoot);
32
+ connection.options.email.body = (0, tools_1.loadFileAndReplaceKeywords)(resolvedHtmlFile, {
31
33
  mappings: context.mappings,
32
34
  disableKeywordReplacement: context.disableKeywordReplacement,
33
35
  });
@@ -49,6 +51,11 @@ async function dump(context) {
49
51
  if (excludedConnections.length) {
50
52
  connections = connections.filter((connection) => !excludedConnections.includes(connection.name));
51
53
  }
54
+ // Filter to included connections
55
+ const includedConnections = (context.assets.include && context.assets.include.connections) || [];
56
+ if (includedConnections.length) {
57
+ connections = connections.filter((connection) => includedConnections.includes(connection.name));
58
+ }
52
59
  const connectionsFolder = path_1.default.join(context.filePath, tools_1.constants.CONNECTIONS_DIRECTORY);
53
60
  fs_extra_1.default.ensureDirSync(connectionsFolder);
54
61
  // Track files that should remain after dump (written + excluded).
@@ -93,11 +100,18 @@ async function dump(context) {
93
100
  if (dumpedConnection.strategy === 'email')
94
101
  expectedFiles.add(`${connectionName}.html`);
95
102
  });
103
+ // With an include list configured, connections outside it are unmanaged, so limit pruning
104
+ // to the listed names rather than every file in the folder.
105
+ const prunableFiles = includedConnections.length
106
+ ? new Set(includedConnections.flatMap((name) => [`${(0, utils_1.sanitize)(name)}.json`, `${(0, utils_1.sanitize)(name)}.html`]))
107
+ : null;
96
108
  // Remove files that belong to connections no longer present (and not excluded)
97
109
  if (fs_extra_1.default.existsSync(connectionsFolder)) {
98
110
  for (const existing of fs_extra_1.default.readdirSync(connectionsFolder)) {
99
111
  const fullPath = path_1.default.join(connectionsFolder, existing);
100
- if (fs_extra_1.default.statSync(fullPath).isFile() && !expectedFiles.has(existing)) {
112
+ if (fs_extra_1.default.statSync(fullPath).isFile() &&
113
+ !expectedFiles.has(existing) &&
114
+ (prunableFiles === null || prunableFiles.has(existing))) {
101
115
  fs_extra_1.default.removeSync(fullPath);
102
116
  }
103
117
  }
@@ -42,10 +42,7 @@ function getDatabase(folder, configRoot, mappingOpts) {
42
42
  else {
43
43
  const resolvedBase = path_1.default.resolve(configRoot);
44
44
  const toLoad = path_1.default.resolve(folder, script.replace(/\\/g, '/'));
45
- if (!toLoad.startsWith(resolvedBase + path_1.default.sep)) {
46
- throw new Error(`Path "${script}" resolves to "${toLoad}" which is outside the config directory "${resolvedBase}". ` +
47
- `Move the file inside your config directory.`);
48
- }
45
+ (0, utils_1.assertInsideConfigRoot)(script, toLoad, resolvedBase);
49
46
  database.options.customScripts[name] = (0, tools_1.loadFileAndReplaceKeywords)(toLoad, mappingOpts);
50
47
  }
51
48
  });
@@ -34,6 +34,11 @@ function parse(context) {
34
34
  logger_1.default.warn(`Skipping email template file ${meta.body} as missing the corresponding '.json' file`);
35
35
  return [];
36
36
  }
37
+ if (meta.body !== undefined) {
38
+ const configRoot = path_1.default.resolve(context.filePath);
39
+ const resolvedTemplatePath = path_1.default.resolve(templateFilePath);
40
+ (0, utils_1.assertInsideConfigRoot)(meta.body, resolvedTemplatePath, configRoot);
41
+ }
37
42
  return {
38
43
  ...meta,
39
44
  body: (0, tools_1.loadFileAndReplaceKeywords)(templateFilePath, {
@@ -0,0 +1,5 @@
1
+ import { DirectoryHandler } from '.';
2
+ import { Asset, ParsedAsset } from '../../../types';
3
+ type ParsedGuardianEmailFactorSettings = ParsedAsset<'guardianEmailFactorSettings', Asset>;
4
+ declare const guardianEmailFactorSettingsHandler: DirectoryHandler<ParsedGuardianEmailFactorSettings>;
5
+ export default guardianEmailFactorSettingsHandler;