@peterhauge/apiops-cli 0.1.3-alpha.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 (199) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +135 -0
  3. package/dist/cli/extract-command.d.ts +12 -0
  4. package/dist/cli/extract-command.d.ts.map +1 -0
  5. package/dist/cli/extract-command.js +157 -0
  6. package/dist/cli/extract-command.js.map +1 -0
  7. package/dist/cli/index.d.ts +7 -0
  8. package/dist/cli/index.d.ts.map +1 -0
  9. package/dist/cli/index.js +74 -0
  10. package/dist/cli/index.js.map +1 -0
  11. package/dist/cli/init-command.d.ts +11 -0
  12. package/dist/cli/init-command.d.ts.map +1 -0
  13. package/dist/cli/init-command.js +87 -0
  14. package/dist/cli/init-command.js.map +1 -0
  15. package/dist/cli/publish-command.d.ts +12 -0
  16. package/dist/cli/publish-command.d.ts.map +1 -0
  17. package/dist/cli/publish-command.js +159 -0
  18. package/dist/cli/publish-command.js.map +1 -0
  19. package/dist/clients/apim-client.d.ts +110 -0
  20. package/dist/clients/apim-client.d.ts.map +1 -0
  21. package/dist/clients/apim-client.js +586 -0
  22. package/dist/clients/apim-client.js.map +1 -0
  23. package/dist/clients/artifact-store.d.ts +23 -0
  24. package/dist/clients/artifact-store.d.ts.map +1 -0
  25. package/dist/clients/artifact-store.js +188 -0
  26. package/dist/clients/artifact-store.js.map +1 -0
  27. package/dist/clients/iapim-client.d.ts +52 -0
  28. package/dist/clients/iapim-client.d.ts.map +1 -0
  29. package/dist/clients/iapim-client.js +6 -0
  30. package/dist/clients/iapim-client.js.map +1 -0
  31. package/dist/clients/iartifact-store.d.ts +50 -0
  32. package/dist/clients/iartifact-store.d.ts.map +1 -0
  33. package/dist/clients/iartifact-store.js +6 -0
  34. package/dist/clients/iartifact-store.js.map +1 -0
  35. package/dist/lib/auto-generated.d.ts +27 -0
  36. package/dist/lib/auto-generated.d.ts.map +1 -0
  37. package/dist/lib/auto-generated.js +34 -0
  38. package/dist/lib/auto-generated.js.map +1 -0
  39. package/dist/lib/cloud-config.d.ts +29 -0
  40. package/dist/lib/cloud-config.d.ts.map +1 -0
  41. package/dist/lib/cloud-config.js +60 -0
  42. package/dist/lib/cloud-config.js.map +1 -0
  43. package/dist/lib/config-loader.d.ts +21 -0
  44. package/dist/lib/config-loader.d.ts.map +1 -0
  45. package/dist/lib/config-loader.js +131 -0
  46. package/dist/lib/config-loader.js.map +1 -0
  47. package/dist/lib/dependency-graph.d.ts +43 -0
  48. package/dist/lib/dependency-graph.d.ts.map +1 -0
  49. package/dist/lib/dependency-graph.js +163 -0
  50. package/dist/lib/dependency-graph.js.map +1 -0
  51. package/dist/lib/exit-codes.d.ts +27 -0
  52. package/dist/lib/exit-codes.d.ts.map +1 -0
  53. package/dist/lib/exit-codes.js +33 -0
  54. package/dist/lib/exit-codes.js.map +1 -0
  55. package/dist/lib/logger.d.ts +39 -0
  56. package/dist/lib/logger.d.ts.map +1 -0
  57. package/dist/lib/logger.js +128 -0
  58. package/dist/lib/logger.js.map +1 -0
  59. package/dist/lib/parallel-runner.d.ts +38 -0
  60. package/dist/lib/parallel-runner.d.ts.map +1 -0
  61. package/dist/lib/parallel-runner.js +70 -0
  62. package/dist/lib/parallel-runner.js.map +1 -0
  63. package/dist/lib/resource-path.d.ts +205 -0
  64. package/dist/lib/resource-path.d.ts.map +1 -0
  65. package/dist/lib/resource-path.js +401 -0
  66. package/dist/lib/resource-path.js.map +1 -0
  67. package/dist/lib/resource-uri.d.ts +40 -0
  68. package/dist/lib/resource-uri.d.ts.map +1 -0
  69. package/dist/lib/resource-uri.js +86 -0
  70. package/dist/lib/resource-uri.js.map +1 -0
  71. package/dist/lib/user-agent.d.ts +2 -0
  72. package/dist/lib/user-agent.d.ts.map +1 -0
  73. package/dist/lib/user-agent.js +5 -0
  74. package/dist/lib/user-agent.js.map +1 -0
  75. package/dist/models/config.d.ts +83 -0
  76. package/dist/models/config.d.ts.map +1 -0
  77. package/dist/models/config.js +6 -0
  78. package/dist/models/config.js.map +1 -0
  79. package/dist/models/resource-types.d.ts +66 -0
  80. package/dist/models/resource-types.d.ts.map +1 -0
  81. package/dist/models/resource-types.js +243 -0
  82. package/dist/models/resource-types.js.map +1 -0
  83. package/dist/models/types.d.ts +47 -0
  84. package/dist/models/types.d.ts.map +1 -0
  85. package/dist/models/types.js +6 -0
  86. package/dist/models/types.js.map +1 -0
  87. package/dist/services/api-extractor.d.ts +36 -0
  88. package/dist/services/api-extractor.d.ts.map +1 -0
  89. package/dist/services/api-extractor.js +319 -0
  90. package/dist/services/api-extractor.js.map +1 -0
  91. package/dist/services/api-publisher.d.ts +18 -0
  92. package/dist/services/api-publisher.d.ts.map +1 -0
  93. package/dist/services/api-publisher.js +290 -0
  94. package/dist/services/api-publisher.js.map +1 -0
  95. package/dist/services/delete-unmatched-service.d.ts +17 -0
  96. package/dist/services/delete-unmatched-service.d.ts.map +1 -0
  97. package/dist/services/delete-unmatched-service.js +143 -0
  98. package/dist/services/delete-unmatched-service.js.map +1 -0
  99. package/dist/services/dry-run-reporter.d.ts +30 -0
  100. package/dist/services/dry-run-reporter.d.ts.map +1 -0
  101. package/dist/services/dry-run-reporter.js +111 -0
  102. package/dist/services/dry-run-reporter.js.map +1 -0
  103. package/dist/services/extract-service.d.ts +47 -0
  104. package/dist/services/extract-service.d.ts.map +1 -0
  105. package/dist/services/extract-service.js +374 -0
  106. package/dist/services/extract-service.js.map +1 -0
  107. package/dist/services/filter-service.d.ts +29 -0
  108. package/dist/services/filter-service.d.ts.map +1 -0
  109. package/dist/services/filter-service.js +143 -0
  110. package/dist/services/filter-service.js.map +1 -0
  111. package/dist/services/git-diff-service.d.ts +23 -0
  112. package/dist/services/git-diff-service.d.ts.map +1 -0
  113. package/dist/services/git-diff-service.js +135 -0
  114. package/dist/services/git-diff-service.js.map +1 -0
  115. package/dist/services/identity-guide-service.d.ts +11 -0
  116. package/dist/services/identity-guide-service.d.ts.map +1 -0
  117. package/dist/services/identity-guide-service.js +227 -0
  118. package/dist/services/identity-guide-service.js.map +1 -0
  119. package/dist/services/init-service.d.ts +16 -0
  120. package/dist/services/init-service.d.ts.map +1 -0
  121. package/dist/services/init-service.js +304 -0
  122. package/dist/services/init-service.js.map +1 -0
  123. package/dist/services/keyvault-checker.d.ts +58 -0
  124. package/dist/services/keyvault-checker.d.ts.map +1 -0
  125. package/dist/services/keyvault-checker.js +390 -0
  126. package/dist/services/keyvault-checker.js.map +1 -0
  127. package/dist/services/override-merger.d.ts +20 -0
  128. package/dist/services/override-merger.d.ts.map +1 -0
  129. package/dist/services/override-merger.js +102 -0
  130. package/dist/services/override-merger.js.map +1 -0
  131. package/dist/services/product-extractor.d.ts +26 -0
  132. package/dist/services/product-extractor.d.ts.map +1 -0
  133. package/dist/services/product-extractor.js +141 -0
  134. package/dist/services/product-extractor.js.map +1 -0
  135. package/dist/services/product-publisher.d.ts +15 -0
  136. package/dist/services/product-publisher.d.ts.map +1 -0
  137. package/dist/services/product-publisher.js +113 -0
  138. package/dist/services/product-publisher.js.map +1 -0
  139. package/dist/services/prompt-service.d.ts +13 -0
  140. package/dist/services/prompt-service.d.ts.map +1 -0
  141. package/dist/services/prompt-service.js +69 -0
  142. package/dist/services/prompt-service.js.map +1 -0
  143. package/dist/services/publish-service.d.ts +31 -0
  144. package/dist/services/publish-service.d.ts.map +1 -0
  145. package/dist/services/publish-service.js +445 -0
  146. package/dist/services/publish-service.js.map +1 -0
  147. package/dist/services/resource-extractor.d.ts +52 -0
  148. package/dist/services/resource-extractor.d.ts.map +1 -0
  149. package/dist/services/resource-extractor.js +168 -0
  150. package/dist/services/resource-extractor.js.map +1 -0
  151. package/dist/services/resource-publisher.d.ts +23 -0
  152. package/dist/services/resource-publisher.d.ts.map +1 -0
  153. package/dist/services/resource-publisher.js +349 -0
  154. package/dist/services/resource-publisher.js.map +1 -0
  155. package/dist/services/secret-redactor.d.ts +20 -0
  156. package/dist/services/secret-redactor.d.ts.map +1 -0
  157. package/dist/services/secret-redactor.js +45 -0
  158. package/dist/services/secret-redactor.js.map +1 -0
  159. package/dist/services/transitive-resolver.d.ts +45 -0
  160. package/dist/services/transitive-resolver.d.ts.map +1 -0
  161. package/dist/services/transitive-resolver.js +177 -0
  162. package/dist/services/transitive-resolver.js.map +1 -0
  163. package/dist/services/workspace-extractor.d.ts +34 -0
  164. package/dist/services/workspace-extractor.d.ts.map +1 -0
  165. package/dist/services/workspace-extractor.js +120 -0
  166. package/dist/services/workspace-extractor.js.map +1 -0
  167. package/dist/templates/azure-devops/extract-pipeline.d.ts +9 -0
  168. package/dist/templates/azure-devops/extract-pipeline.d.ts.map +1 -0
  169. package/dist/templates/azure-devops/extract-pipeline.js +95 -0
  170. package/dist/templates/azure-devops/extract-pipeline.js.map +1 -0
  171. package/dist/templates/azure-devops/publish-pipeline.d.ts +10 -0
  172. package/dist/templates/azure-devops/publish-pipeline.d.ts.map +1 -0
  173. package/dist/templates/azure-devops/publish-pipeline.js +100 -0
  174. package/dist/templates/azure-devops/publish-pipeline.js.map +1 -0
  175. package/dist/templates/configs/filter-config.d.ts +6 -0
  176. package/dist/templates/configs/filter-config.d.ts.map +1 -0
  177. package/dist/templates/configs/filter-config.js +51 -0
  178. package/dist/templates/configs/filter-config.js.map +1 -0
  179. package/dist/templates/configs/override-config.d.ts +6 -0
  180. package/dist/templates/configs/override-config.d.ts.map +1 -0
  181. package/dist/templates/configs/override-config.js +45 -0
  182. package/dist/templates/configs/override-config.js.map +1 -0
  183. package/dist/templates/configs/package-json.d.ts +10 -0
  184. package/dist/templates/configs/package-json.d.ts.map +1 -0
  185. package/dist/templates/configs/package-json.js +19 -0
  186. package/dist/templates/configs/package-json.js.map +1 -0
  187. package/dist/templates/copilot/identity-setup-prompt.d.ts +13 -0
  188. package/dist/templates/copilot/identity-setup-prompt.d.ts.map +1 -0
  189. package/dist/templates/copilot/identity-setup-prompt.js +279 -0
  190. package/dist/templates/copilot/identity-setup-prompt.js.map +1 -0
  191. package/dist/templates/github-actions/extract-workflow.d.ts +9 -0
  192. package/dist/templates/github-actions/extract-workflow.d.ts.map +1 -0
  193. package/dist/templates/github-actions/extract-workflow.js +126 -0
  194. package/dist/templates/github-actions/extract-workflow.js.map +1 -0
  195. package/dist/templates/github-actions/publish-workflow.d.ts +10 -0
  196. package/dist/templates/github-actions/publish-workflow.d.ts.map +1 -0
  197. package/dist/templates/github-actions/publish-workflow.js +105 -0
  198. package/dist/templates/github-actions/publish-workflow.js.map +1 -0
  199. package/package.json +65 -0
@@ -0,0 +1,390 @@
1
+ /**
2
+ * Key Vault access pre-flight check for KeyVault-backed NamedValues.
3
+ *
4
+ * Validates that the APIM service's managed identity has been granted access
5
+ * to the Key Vault secret. Uses Azure ARM APIs to:
6
+ * 1. Retrieve the APIM service identity (system- or user-assigned)
7
+ * 2. Locate the Key Vault resource in the subscription
8
+ * 3. Check RBAC role assignments or access policies
9
+ *
10
+ * This check is best-effort when infrastructure queries fail (ARM token,
11
+ * vault in another subscription, etc.) — a warning is logged and the check
12
+ * is skipped. Hard errors are raised only for definitive misconfigurations
13
+ * such as "APIM has no managed identity" or "no matching RBAC / access policy".
14
+ */
15
+ import { DefaultAzureCredential } from '@azure/identity';
16
+ import { logger } from '../lib/logger.js';
17
+ import { USER_AGENT } from '../lib/user-agent.js';
18
+ /* ------------------------------------------------------------------ */
19
+ /* ARM API versions */
20
+ /* ------------------------------------------------------------------ */
21
+ const APIM_API_VERSION = '2024-05-01';
22
+ const RESOURCES_API_VERSION = '2021-04-01';
23
+ const KEYVAULT_API_VERSION = '2023-07-01';
24
+ const AUTHZ_API_VERSION = '2022-04-01';
25
+ /** ARM management scope */
26
+ const ARM_SCOPE = 'https://management.azure.com/.default';
27
+ /**
28
+ * Well-known Azure RBAC role-definition GUIDs that grant
29
+ * the `Microsoft.KeyVault/vaults/secrets/getSecret/action` data-action.
30
+ * Used only as a fast-path optimisation — if none match we still resolve
31
+ * each role definition to inspect its dataActions.
32
+ */
33
+ const SECRET_GET_ROLE_IDS = new Set([
34
+ '4633458b-17de-408a-b874-0445c86b69e6', // Key Vault Secrets User
35
+ 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7', // Key Vault Secrets Officer
36
+ '00482a5a-887f-4fb3-b363-3b7fe8e74483', // Key Vault Administrator
37
+ ]);
38
+ /* ------------------------------------------------------------------ */
39
+ /* Public types */
40
+ /* ------------------------------------------------------------------ */
41
+ /**
42
+ * Error thrown when the APIM managed identity clearly lacks access to
43
+ * Key Vault, or the APIM service is missing the required identity
44
+ * configuration. Signals that the publish should fail immediately.
45
+ */
46
+ export class KeyVaultAccessError extends Error {
47
+ constructor(message, options) {
48
+ super(message, options);
49
+ this.name = 'KeyVaultAccessError';
50
+ }
51
+ }
52
+ /* ------------------------------------------------------------------ */
53
+ /* Default production implementations */
54
+ /* ------------------------------------------------------------------ */
55
+ function defaultTokenProviderFactory() {
56
+ return new DefaultAzureCredential();
57
+ }
58
+ async function defaultArmRequest(url, token) {
59
+ const headers = new Headers();
60
+ headers.set('Authorization', `Bearer ${token}`);
61
+ headers.set('Accept', 'application/json');
62
+ headers.set('User-Agent', USER_AGENT);
63
+ const response = await fetch(url, { headers });
64
+ return {
65
+ status: response.status,
66
+ json: () => response.json(),
67
+ };
68
+ }
69
+ /* ------------------------------------------------------------------ */
70
+ /* Main entry point */
71
+ /* ------------------------------------------------------------------ */
72
+ /**
73
+ * Verify that the APIM service's managed identity has access to the
74
+ * specified Key Vault secret via ARM RBAC or access policies.
75
+ *
76
+ * @param secretIdentifier Full Key Vault secret URI, e.g.
77
+ * `https://myvault.vault.azure.net/secrets/my-secret[/version]`
78
+ * @param identityClientId Client ID of the user-assigned managed identity
79
+ * that APIM will use. Omit for system-assigned identity.
80
+ * @param apimContext Subscription / resource-group / service-name of the
81
+ * APIM instance.
82
+ * @param tokenProviderFactory (testing) Override the ARM credential.
83
+ * @param armRequest (testing) Override the HTTP call.
84
+ */
85
+ export async function checkKeyVaultSecretAccess(secretIdentifier, identityClientId, apimContext, tokenProviderFactory = defaultTokenProviderFactory, armRequest = defaultArmRequest) {
86
+ /* ---- 1. Parse vault name & secret name from the URI ---- */
87
+ let vaultName;
88
+ let secretName;
89
+ try {
90
+ const url = new URL(secretIdentifier);
91
+ vaultName = url.hostname.split('.')[0];
92
+ const segments = url.pathname.split('/').filter(Boolean);
93
+ secretName = segments[1] ?? '';
94
+ if (!secretName)
95
+ throw new Error('Missing secret name in path');
96
+ if (!vaultName)
97
+ throw new Error('Missing vault name in host');
98
+ }
99
+ catch (error) {
100
+ throw new KeyVaultAccessError(`Invalid Key Vault secretIdentifier: '${secretIdentifier}'`, { cause: error });
101
+ }
102
+ /* ---- 2. Acquire ARM token ---- */
103
+ const credential = tokenProviderFactory();
104
+ let token;
105
+ try {
106
+ token = (await credential.getToken(ARM_SCOPE)).token;
107
+ }
108
+ catch (error) {
109
+ logger.warn(`Unable to acquire ARM token — skipping Key Vault pre-flight check. ` +
110
+ `(Error: ${error.message})`);
111
+ return;
112
+ }
113
+ /* ---- 3. Get APIM service identity ---- */
114
+ const { principalId, identityLabel } = await resolveApimPrincipal(apimContext, identityClientId, token, armRequest);
115
+ if (!principalId)
116
+ return; // soft-skip — warning already logged
117
+ logger.debug(`Pre-flight: APIM ${identityLabel} principalId = ${principalId}`);
118
+ /* ---- 4. Locate the vault in the same subscription ---- */
119
+ const vault = await findVaultInSubscription(apimContext.subscriptionId, vaultName, token, armRequest);
120
+ if (!vault)
121
+ return; // soft-skip — warning already logged
122
+ /* ---- 5. Check RBAC or access-policy permissions ---- */
123
+ const vaultProps = vault.json
124
+ .properties;
125
+ if (vaultProps?.enableRbacAuthorization === true) {
126
+ await checkRbacAccess(vault.resourceId, principalId, identityLabel, vaultName, secretName, token, armRequest);
127
+ }
128
+ else {
129
+ checkAccessPolicies(vaultProps, principalId, identityLabel, vaultName, secretName);
130
+ }
131
+ logger.debug(`Key Vault access confirmed for APIM ${identityLabel} ` +
132
+ `on secret '${secretName}' in vault '${vaultName}'`);
133
+ }
134
+ /* ------------------------------------------------------------------ */
135
+ /* Internal helpers */
136
+ /* ------------------------------------------------------------------ */
137
+ /**
138
+ * Resolve the APIM managed-identity principal ID via the ARM service resource.
139
+ *
140
+ * Returns `{ principalId, identityLabel }` on success.
141
+ * Throws `KeyVaultAccessError` when APIM has no identity or the specified
142
+ * user-assigned identity is not found.
143
+ * Returns `{ principalId: undefined }` when the ARM call itself fails
144
+ * (warns and degrades gracefully).
145
+ */
146
+ async function resolveApimPrincipal(ctx, identityClientId, token, armRequest) {
147
+ const identityLabel = identityClientId
148
+ ? `user-assigned identity '${identityClientId}'`
149
+ : 'system-assigned identity';
150
+ const apimUrl = `https://management.azure.com/subscriptions/${ctx.subscriptionId}` +
151
+ `/resourceGroups/${ctx.resourceGroup}` +
152
+ `/providers/Microsoft.ApiManagement/service/${ctx.serviceName}` +
153
+ `?api-version=${APIM_API_VERSION}`;
154
+ let apimJson;
155
+ try {
156
+ const resp = await armRequest(apimUrl, token);
157
+ if (resp.status !== 200) {
158
+ logger.warn(`Failed to fetch APIM service (HTTP ${resp.status}) — ` +
159
+ `skipping Key Vault pre-flight check.`);
160
+ return { principalId: undefined, identityLabel };
161
+ }
162
+ apimJson = (await resp.json());
163
+ }
164
+ catch (error) {
165
+ logger.warn(`Failed to fetch APIM service — skipping Key Vault pre-flight check. ` +
166
+ `(Error: ${error.message})`);
167
+ return { principalId: undefined, identityLabel };
168
+ }
169
+ const identity = apimJson.identity;
170
+ if (!identity) {
171
+ throw new KeyVaultAccessError(`APIM service '${ctx.serviceName}' has no managed identity configured. ` +
172
+ `KeyVault-backed NamedValues require a managed identity to access secrets.`);
173
+ }
174
+ let principalId;
175
+ if (identityClientId) {
176
+ // User-assigned: find the matching clientId in userAssignedIdentities
177
+ const uaMap = identity.userAssignedIdentities;
178
+ if (uaMap) {
179
+ for (const entry of Object.values(uaMap)) {
180
+ if (entry.clientId === identityClientId) {
181
+ principalId = entry.principalId;
182
+ break;
183
+ }
184
+ }
185
+ }
186
+ if (!principalId) {
187
+ throw new KeyVaultAccessError(`User-assigned managed identity with clientId '${identityClientId}' ` +
188
+ `not found on APIM service '${ctx.serviceName}'.`);
189
+ }
190
+ }
191
+ else {
192
+ principalId = identity.principalId;
193
+ if (!principalId) {
194
+ throw new KeyVaultAccessError(`APIM service '${ctx.serviceName}' has no system-assigned managed ` +
195
+ `identity enabled. KeyVault-backed NamedValues require a managed identity.`);
196
+ }
197
+ }
198
+ return { principalId, identityLabel };
199
+ }
200
+ /**
201
+ * Search for a Key Vault resource in the given subscription by vault name.
202
+ * Returns the vault's resource ID and its full JSON, or `undefined` when the
203
+ * vault is not found (e.g. it lives in a different subscription).
204
+ */
205
+ async function findVaultInSubscription(subscriptionId, vaultName, token, armRequest) {
206
+ const filter = encodeURIComponent(`resourceType eq 'Microsoft.KeyVault/vaults' and name eq '${vaultName}'`);
207
+ const listUrl = `https://management.azure.com/subscriptions/${subscriptionId}` +
208
+ `/resources?$filter=${filter}&api-version=${RESOURCES_API_VERSION}`;
209
+ let vaultResourceId;
210
+ try {
211
+ const resp = await armRequest(listUrl, token);
212
+ if (resp.status !== 200) {
213
+ logger.warn(`Failed to search for Key Vault '${vaultName}' (HTTP ${resp.status}) — ` +
214
+ `skipping pre-flight check.`);
215
+ return undefined;
216
+ }
217
+ const body = (await resp.json());
218
+ vaultResourceId = body.value?.[0]?.id;
219
+ }
220
+ catch (error) {
221
+ logger.warn(`Failed to search for Key Vault '${vaultName}' — skipping pre-flight check. ` +
222
+ `(Error: ${error.message})`);
223
+ return undefined;
224
+ }
225
+ if (!vaultResourceId) {
226
+ logger.warn(`Key Vault '${vaultName}' not found in subscription '${subscriptionId}'. ` +
227
+ `It may be in a different subscription — skipping pre-flight access check.`);
228
+ return undefined;
229
+ }
230
+ // GET the vault resource to read properties (RBAC mode, access policies)
231
+ const vaultUrl = `https://management.azure.com${vaultResourceId}` +
232
+ `?api-version=${KEYVAULT_API_VERSION}`;
233
+ try {
234
+ const resp = await armRequest(vaultUrl, token);
235
+ if (resp.status !== 200) {
236
+ logger.warn(`Failed to read Key Vault '${vaultName}' (HTTP ${resp.status}) — ` +
237
+ `skipping pre-flight check.`);
238
+ return undefined;
239
+ }
240
+ const json = await resp.json();
241
+ return { resourceId: vaultResourceId, json };
242
+ }
243
+ catch (error) {
244
+ logger.warn(`Failed to read Key Vault '${vaultName}' — skipping pre-flight check. ` +
245
+ `(Error: ${error.message})`);
246
+ return undefined;
247
+ }
248
+ }
249
+ /** The data-action that grants secret read access in Key Vault RBAC. */
250
+ const SECRET_GET_DATA_ACTION = 'microsoft.keyvault/vaults/secrets/getsecret/action';
251
+ /** Wildcard patterns that also grant the secret-get data-action. */
252
+ const SECRET_GET_WILDCARDS = [
253
+ 'microsoft.keyvault/vaults/secrets/*',
254
+ 'microsoft.keyvault/vaults/*',
255
+ 'microsoft.keyvault/*',
256
+ '*',
257
+ ];
258
+ /**
259
+ * RBAC-mode check: list role assignments on the vault for the given principal.
260
+ * Throws `KeyVaultAccessError` when no assignments exist at all, or when
261
+ * none of the assigned roles grant the secret-get data-action.
262
+ *
263
+ * Strategy (fast-path first):
264
+ * 1. Check if any assignment matches a well-known role GUID — skip role
265
+ * definition resolution if so.
266
+ * 2. Otherwise, GET each role definition and inspect its `permissions[].dataActions`
267
+ * for the `Microsoft.KeyVault/vaults/secrets/getSecret/action` data-action.
268
+ */
269
+ async function checkRbacAccess(vaultResourceId, principalId, identityLabel, vaultName, secretName, token, armRequest) {
270
+ const filter = encodeURIComponent(`principalId eq '${principalId}'`);
271
+ const url = `https://management.azure.com${vaultResourceId}` +
272
+ `/providers/Microsoft.Authorization/roleAssignments` +
273
+ `?$filter=${filter}&api-version=${AUTHZ_API_VERSION}`;
274
+ let assignments;
275
+ try {
276
+ const resp = await armRequest(url, token);
277
+ if (resp.status !== 200) {
278
+ logger.warn(`Failed to list role assignments (HTTP ${resp.status}) — ` +
279
+ `skipping RBAC pre-flight check for '${vaultName}'.`);
280
+ return;
281
+ }
282
+ const body = (await resp.json());
283
+ assignments = body.value ?? [];
284
+ }
285
+ catch (error) {
286
+ logger.warn(`Failed to list role assignments — skipping RBAC pre-flight check. ` +
287
+ `(Error: ${error.message})`);
288
+ return;
289
+ }
290
+ if (assignments.length === 0) {
291
+ throw new KeyVaultAccessError(`APIM ${identityLabel} (principalId: ${principalId}) has no RBAC role ` +
292
+ `assignments on Key Vault '${vaultName}'. ` +
293
+ `Grant the identity 'Key Vault Secrets User' role on the vault ` +
294
+ `so APIM can read secret '${secretName}'.`);
295
+ }
296
+ // --- Fast path: match well-known role GUIDs ---
297
+ const hasKnownRole = assignments.some((a) => {
298
+ const roleDefId = a.properties?.roleDefinitionId ?? '';
299
+ const roleGuid = roleDefId.split('/').pop()?.toLowerCase() ?? '';
300
+ return SECRET_GET_ROLE_IDS.has(roleGuid);
301
+ });
302
+ if (hasKnownRole) {
303
+ return; // confirmed — no need to resolve definitions
304
+ }
305
+ // --- Slow path: resolve each role definition and check dataActions ---
306
+ logger.debug(`No well-known secret role found; resolving ${assignments.length} role ` +
307
+ `definition(s) to check dataActions.`);
308
+ for (const assignment of assignments) {
309
+ const roleDefId = assignment.properties?.roleDefinitionId ?? '';
310
+ if (!roleDefId)
311
+ continue;
312
+ const hasAccess = await roleDefinitionGrantsSecretGet(roleDefId, token, armRequest);
313
+ if (hasAccess)
314
+ return; // confirmed via dataActions
315
+ }
316
+ throw new KeyVaultAccessError(`APIM ${identityLabel} (principalId: ${principalId}) has ` +
317
+ `${assignments.length} RBAC role assignment(s) on Key Vault '${vaultName}', ` +
318
+ `but none grant the '${SECRET_GET_DATA_ACTION}' data-action. ` +
319
+ `Grant the identity 'Key Vault Secrets User' role on the vault ` +
320
+ `so APIM can read secret '${secretName}'.`);
321
+ }
322
+ /**
323
+ * GET a role definition by its full ARM ID and check whether any of its
324
+ * `permissions[].dataActions` grant the secret-get data-action.
325
+ *
326
+ * Returns `undefined` (rather than `false`) when the definition cannot be
327
+ * fetched — the caller should treat this as inconclusive and keep checking.
328
+ */
329
+ async function roleDefinitionGrantsSecretGet(roleDefinitionId, token, armRequest) {
330
+ const url = `https://management.azure.com${roleDefinitionId}` +
331
+ `?api-version=${AUTHZ_API_VERSION}`;
332
+ let json;
333
+ try {
334
+ const resp = await armRequest(url, token);
335
+ if (resp.status !== 200) {
336
+ logger.debug(`Failed to fetch role definition (HTTP ${resp.status}): ${roleDefinitionId}`);
337
+ return undefined;
338
+ }
339
+ json = (await resp.json());
340
+ }
341
+ catch {
342
+ return undefined;
343
+ }
344
+ const props = json.properties;
345
+ const permissions = (props?.permissions ?? []);
346
+ for (const perm of permissions) {
347
+ const dataActions = (perm.dataActions ?? []);
348
+ const hasAction = dataActions.some((da) => {
349
+ const lower = da.toLowerCase();
350
+ return lower === SECRET_GET_DATA_ACTION ||
351
+ SECRET_GET_WILDCARDS.includes(lower);
352
+ });
353
+ if (hasAction) {
354
+ // Also check notDataActions don't deny it
355
+ const notDataActions = (perm.notDataActions ?? []);
356
+ const denied = notDataActions.some((nda) => {
357
+ const lower = nda.toLowerCase();
358
+ return lower === SECRET_GET_DATA_ACTION ||
359
+ SECRET_GET_WILDCARDS.includes(lower);
360
+ });
361
+ if (!denied)
362
+ return true;
363
+ }
364
+ }
365
+ return false;
366
+ }
367
+ /**
368
+ * Access-policy mode check: scan the vault's `accessPolicies` array for the
369
+ * APIM principal with `secrets.get` permission.
370
+ * Throws `KeyVaultAccessError` when no matching policy is found.
371
+ */
372
+ function checkAccessPolicies(vaultProperties, principalId, identityLabel, vaultName, secretName) {
373
+ const policies = vaultProperties?.accessPolicies ?? [];
374
+ const match = policies.find((p) => {
375
+ if (p.objectId?.toLowerCase() !== principalId.toLowerCase()) {
376
+ return false;
377
+ }
378
+ const perms = p.permissions;
379
+ const secretPerms = perms?.secrets ?? [];
380
+ return secretPerms.some((s) => s.toLowerCase() === 'get' || s.toLowerCase() === 'all');
381
+ });
382
+ if (!match) {
383
+ throw new KeyVaultAccessError(`APIM ${identityLabel} (principalId: ${principalId}) does not have an ` +
384
+ `access policy with 'get' secret permission on Key Vault '${vaultName}'. ` +
385
+ `Add an access policy granting 'get' on secrets for the identity, ` +
386
+ `or enable RBAC authorization and assign 'Key Vault Secrets User'. ` +
387
+ `Secret: '${secretName}'.`);
388
+ }
389
+ }
390
+ //# sourceMappingURL=keyvault-checker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyvault-checker.js","sourceRoot":"","sources":["../../src/services/keyvault-checker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * T033: Override merger service
3
+ * Apply environment-specific overrides from OverrideConfig to resource JSON payloads.
4
+ * Deep-merges with case-insensitive key matching; warns on unknown override keys.
5
+ */
6
+ import { ResourceDescriptor } from '../models/types.js';
7
+ import { OverrideConfig } from '../models/config.js';
8
+ /**
9
+ * Apply environment overrides from OverrideConfig to a resource JSON payload.
10
+ * Deep-merges matching override properties using case-insensitive key matching.
11
+ * Logs a warning for any override keys that don't match resource type.
12
+ * Returns a new object (does not mutate input).
13
+ *
14
+ * @param descriptor - Resource descriptor (type + name identify the resource)
15
+ * @param json - Original resource JSON payload
16
+ * @param overrides - Environment-specific override configuration
17
+ * @returns New JSON object with overrides applied
18
+ */
19
+ export declare function applyOverrides(descriptor: ResourceDescriptor, json: Record<string, unknown>, overrides: OverrideConfig | undefined): Record<string, unknown>;
20
+ //# sourceMappingURL=override-merger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"override-merger.d.ts","sourceRoot":"","sources":["../../src/services/override-merger.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAIrD;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,UAAU,EAAE,kBAAkB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,SAAS,EAAE,cAAc,GAAG,SAAS,GACpC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsCzB"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * T033: Override merger service
3
+ * Apply environment-specific overrides from OverrideConfig to resource JSON payloads.
4
+ * Deep-merges with case-insensitive key matching; warns on unknown override keys.
5
+ */
6
+ import { ResourceType } from '../models/resource-types.js';
7
+ import { logger } from '../lib/logger.js';
8
+ import { getNameFromNameParts } from '../lib/resource-path.js';
9
+ /**
10
+ * Apply environment overrides from OverrideConfig to a resource JSON payload.
11
+ * Deep-merges matching override properties using case-insensitive key matching.
12
+ * Logs a warning for any override keys that don't match resource type.
13
+ * Returns a new object (does not mutate input).
14
+ *
15
+ * @param descriptor - Resource descriptor (type + name identify the resource)
16
+ * @param json - Original resource JSON payload
17
+ * @param overrides - Environment-specific override configuration
18
+ * @returns New JSON object with overrides applied
19
+ */
20
+ export function applyOverrides(descriptor, json, overrides) {
21
+ if (!overrides) {
22
+ return { ...json };
23
+ }
24
+ // Map resource type to override config section
25
+ const overrideSection = getOverrideSectionForType(descriptor.type, overrides);
26
+ if (!overrideSection) {
27
+ return { ...json };
28
+ }
29
+ // Find matching override using case-insensitive name comparison
30
+ const matchingKey = Object.keys(overrideSection).find((key) => key.toLowerCase() === getNameFromNameParts(descriptor.nameParts).toLowerCase());
31
+ if (!matchingKey) {
32
+ return { ...json };
33
+ }
34
+ const overrideValues = overrideSection[matchingKey];
35
+ if (overrideValues === null || overrideValues === undefined || typeof overrideValues !== 'object') {
36
+ return { ...json };
37
+ }
38
+ // ARM resources have all overridable fields inside 'properties'
39
+ // Wrap the override values inside 'properties' to merge at the correct level
40
+ const wrappedOverride = { properties: overrideValues };
41
+ // Deep-merge the override into the resource JSON
42
+ const result = deepMerge(json, wrappedOverride);
43
+ logger.debug(`Applied overrides to ${descriptor.type} '${descriptor.nameParts.join('/')}'`, { overrideKeys: Object.keys(overrideValues) });
44
+ return result;
45
+ }
46
+ /**
47
+ * Get the override section for a given resource type.
48
+ * Returns the relevant Record<string, Override> map or undefined.
49
+ */
50
+ function getOverrideSectionForType(type, overrides) {
51
+ switch (type) {
52
+ case ResourceType.NamedValue:
53
+ return overrides.namedValues;
54
+ case ResourceType.Backend:
55
+ return overrides.backends;
56
+ case ResourceType.Api:
57
+ return overrides.apis;
58
+ case ResourceType.Diagnostic:
59
+ return overrides.diagnostics;
60
+ case ResourceType.Logger:
61
+ return overrides.loggers;
62
+ default:
63
+ return undefined;
64
+ }
65
+ }
66
+ /**
67
+ * Deep-merge two objects recursively.
68
+ * - Objects are merged recursively
69
+ * - Arrays are replaced (not merged)
70
+ * - Primitives from source override target
71
+ * - Returns a new object (immutable)
72
+ *
73
+ * @param target - Base object
74
+ * @param source - Override object
75
+ * @returns New merged object
76
+ */
77
+ function deepMerge(target, source) {
78
+ const result = { ...target };
79
+ for (const [key, sourceValue] of Object.entries(source)) {
80
+ const targetValue = result[key];
81
+ // If both are plain objects, merge recursively
82
+ if (isPlainObject(sourceValue) &&
83
+ isPlainObject(targetValue)) {
84
+ result[key] = deepMerge(targetValue, sourceValue);
85
+ }
86
+ else {
87
+ // Otherwise replace (arrays, primitives, or type mismatch)
88
+ result[key] = sourceValue;
89
+ }
90
+ }
91
+ return result;
92
+ }
93
+ /**
94
+ * Check if a value is a plain object (not an array, null, or other special object).
95
+ */
96
+ function isPlainObject(value) {
97
+ return (typeof value === 'object' &&
98
+ value !== null &&
99
+ !Array.isArray(value) &&
100
+ Object.prototype.toString.call(value) === '[object Object]');
101
+ }
102
+ //# sourceMappingURL=override-merger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"override-merger.js","sourceRoot":"","sources":["../../src/services/override-merger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * T023: Product-specific extraction logic
3
+ * Product associations (apis.json, groups.json, tags.json), product policies, product wikis.
4
+ */
5
+ import { IApimClient } from '../clients/iapim-client.js';
6
+ import { IArtifactStore } from '../clients/iartifact-store.js';
7
+ import { ApimServiceContext, ResourceDescriptor } from '../models/types.js';
8
+ import { FilterConfig } from '../models/config.js';
9
+ /**
10
+ * Result of product-specific extraction for a single product.
11
+ */
12
+ export interface ProductExtractionResult {
13
+ productName: string;
14
+ apis: string[];
15
+ groups: string[];
16
+ policy: string | undefined;
17
+ wiki: boolean;
18
+ tags: string[];
19
+ policies: string[];
20
+ }
21
+ /**
22
+ * Extract all product-specific resources for a single product.
23
+ * This includes API associations, group associations, policies, and wikis.
24
+ */
25
+ export declare function extractProductResources(client: IApimClient, store: IArtifactStore, context: ApimServiceContext, productDescriptor: ResourceDescriptor, outputDir: string, _filter?: FilterConfig, _workspace?: string): Promise<ProductExtractionResult>;
26
+ //# sourceMappingURL=product-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"product-extractor.d.ts","sourceRoot":"","sources":["../../src/services/product-extractor.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAKnD;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,kBAAkB,EAC3B,iBAAiB,EAAE,kBAAkB,EACrC,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,YAAY,EACtB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,uBAAuB,CAAC,CAyClC"}