@apexops/capabilities 0.10.0-next.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 (130) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/bound-envelope-transport.d.ts +27 -0
  3. package/dist/bound-envelope-transport.d.ts.map +1 -0
  4. package/dist/bound-envelope-transport.js +96 -0
  5. package/dist/bound-envelope-transport.js.map +1 -0
  6. package/dist/capability.d.ts +36 -0
  7. package/dist/capability.d.ts.map +1 -0
  8. package/dist/capability.js +83 -0
  9. package/dist/capability.js.map +1 -0
  10. package/dist/command-plans.d.ts +49 -0
  11. package/dist/command-plans.d.ts.map +1 -0
  12. package/dist/command-plans.js +157 -0
  13. package/dist/command-plans.js.map +1 -0
  14. package/dist/design-capabilities.d.ts +23 -0
  15. package/dist/design-capabilities.d.ts.map +1 -0
  16. package/dist/design-capabilities.js +32 -0
  17. package/dist/design-capabilities.js.map +1 -0
  18. package/dist/encrypted-envelope-transport.d.ts +32 -0
  19. package/dist/encrypted-envelope-transport.d.ts.map +1 -0
  20. package/dist/encrypted-envelope-transport.js +138 -0
  21. package/dist/encrypted-envelope-transport.js.map +1 -0
  22. package/dist/iac-generation.d.ts +52 -0
  23. package/dist/iac-generation.d.ts.map +1 -0
  24. package/dist/iac-generation.js +645 -0
  25. package/dist/iac-generation.js.map +1 -0
  26. package/dist/iac-normalizers.d.ts +22 -0
  27. package/dist/iac-normalizers.d.ts.map +1 -0
  28. package/dist/iac-normalizers.js +198 -0
  29. package/dist/iac-normalizers.js.map +1 -0
  30. package/dist/iac.d.ts +83 -0
  31. package/dist/iac.d.ts.map +1 -0
  32. package/dist/iac.js +217 -0
  33. package/dist/iac.js.map +1 -0
  34. package/dist/index.d.ts +17 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +17 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/local-plan-transport.d.ts +29 -0
  39. package/dist/local-plan-transport.d.ts.map +1 -0
  40. package/dist/local-plan-transport.js +101 -0
  41. package/dist/local-plan-transport.js.map +1 -0
  42. package/dist/native-iac-providers.d.ts +96 -0
  43. package/dist/native-iac-providers.d.ts.map +1 -0
  44. package/dist/native-iac-providers.js +487 -0
  45. package/dist/native-iac-providers.js.map +1 -0
  46. package/dist/operations-placeholders.d.ts +5 -0
  47. package/dist/operations-placeholders.d.ts.map +1 -0
  48. package/dist/operations-placeholders.js +23 -0
  49. package/dist/operations-placeholders.js.map +1 -0
  50. package/dist/pack-manager.d.ts +95 -0
  51. package/dist/pack-manager.d.ts.map +1 -0
  52. package/dist/pack-manager.js +556 -0
  53. package/dist/pack-manager.js.map +1 -0
  54. package/dist/packs.d.ts +29 -0
  55. package/dist/packs.d.ts.map +1 -0
  56. package/dist/packs.js +51 -0
  57. package/dist/packs.js.map +1 -0
  58. package/dist/process-runner.d.ts +30 -0
  59. package/dist/process-runner.d.ts.map +1 -0
  60. package/dist/process-runner.js +97 -0
  61. package/dist/process-runner.js.map +1 -0
  62. package/dist/secret-redaction.d.ts +3 -0
  63. package/dist/secret-redaction.d.ts.map +1 -0
  64. package/dist/secret-redaction.js +48 -0
  65. package/dist/secret-redaction.js.map +1 -0
  66. package/dist/terraform-provider-schema.d.ts +75 -0
  67. package/dist/terraform-provider-schema.d.ts.map +1 -0
  68. package/dist/terraform-provider-schema.js +307 -0
  69. package/dist/terraform-provider-schema.js.map +1 -0
  70. package/dist/terraform-registry-client.d.ts +63 -0
  71. package/dist/terraform-registry-client.d.ts.map +1 -0
  72. package/dist/terraform-registry-client.js +388 -0
  73. package/dist/terraform-registry-client.js.map +1 -0
  74. package/dist/test/bound-envelope-transport.test.d.ts +2 -0
  75. package/dist/test/bound-envelope-transport.test.d.ts.map +1 -0
  76. package/dist/test/bound-envelope-transport.test.js +30 -0
  77. package/dist/test/bound-envelope-transport.test.js.map +1 -0
  78. package/dist/test/capabilities.test.d.ts +2 -0
  79. package/dist/test/capabilities.test.d.ts.map +1 -0
  80. package/dist/test/capabilities.test.js +86 -0
  81. package/dist/test/capabilities.test.js.map +1 -0
  82. package/dist/test/encrypted-envelope-transport.test.d.ts +2 -0
  83. package/dist/test/encrypted-envelope-transport.test.d.ts.map +1 -0
  84. package/dist/test/encrypted-envelope-transport.test.js +74 -0
  85. package/dist/test/encrypted-envelope-transport.test.js.map +1 -0
  86. package/dist/test/fixtures/terraform-provider-schema.d.ts +65 -0
  87. package/dist/test/fixtures/terraform-provider-schema.d.ts.map +1 -0
  88. package/dist/test/fixtures/terraform-provider-schema.js +29 -0
  89. package/dist/test/fixtures/terraform-provider-schema.js.map +1 -0
  90. package/dist/test/fixtures/terraform-registry.d.ts +54 -0
  91. package/dist/test/fixtures/terraform-registry.d.ts.map +1 -0
  92. package/dist/test/fixtures/terraform-registry.js +42 -0
  93. package/dist/test/fixtures/terraform-registry.js.map +1 -0
  94. package/dist/test/iac-generation.test.d.ts +2 -0
  95. package/dist/test/iac-generation.test.d.ts.map +1 -0
  96. package/dist/test/iac-generation.test.js +271 -0
  97. package/dist/test/iac-generation.test.js.map +1 -0
  98. package/dist/test/iac.test.d.ts +2 -0
  99. package/dist/test/iac.test.d.ts.map +1 -0
  100. package/dist/test/iac.test.js +161 -0
  101. package/dist/test/iac.test.js.map +1 -0
  102. package/dist/test/native-iac-providers.test.d.ts +2 -0
  103. package/dist/test/native-iac-providers.test.d.ts.map +1 -0
  104. package/dist/test/native-iac-providers.test.js +594 -0
  105. package/dist/test/native-iac-providers.test.js.map +1 -0
  106. package/dist/test/operations-placeholders.test.d.ts +2 -0
  107. package/dist/test/operations-placeholders.test.d.ts.map +1 -0
  108. package/dist/test/operations-placeholders.test.js +34 -0
  109. package/dist/test/operations-placeholders.test.js.map +1 -0
  110. package/dist/test/pack-manager.test.d.ts +2 -0
  111. package/dist/test/pack-manager.test.d.ts.map +1 -0
  112. package/dist/test/pack-manager.test.js +379 -0
  113. package/dist/test/pack-manager.test.js.map +1 -0
  114. package/dist/test/packs-and-commands.test.d.ts +2 -0
  115. package/dist/test/packs-and-commands.test.d.ts.map +1 -0
  116. package/dist/test/packs-and-commands.test.js +131 -0
  117. package/dist/test/packs-and-commands.test.js.map +1 -0
  118. package/dist/test/process-runner.test.d.ts +2 -0
  119. package/dist/test/process-runner.test.d.ts.map +1 -0
  120. package/dist/test/process-runner.test.js +42 -0
  121. package/dist/test/process-runner.test.js.map +1 -0
  122. package/dist/test/terraform-provider-schema.test.d.ts +2 -0
  123. package/dist/test/terraform-provider-schema.test.d.ts.map +1 -0
  124. package/dist/test/terraform-provider-schema.test.js +259 -0
  125. package/dist/test/terraform-provider-schema.test.js.map +1 -0
  126. package/dist/test/terraform-registry-client.test.d.ts +2 -0
  127. package/dist/test/terraform-registry-client.test.d.ts.map +1 -0
  128. package/dist/test/terraform-registry-client.test.js +321 -0
  129. package/dist/test/terraform-registry-client.test.js.map +1 -0
  130. package/package.json +41 -0
@@ -0,0 +1,645 @@
1
+ import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { sha256 } from "./iac.js";
4
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
5
+ const EXACT_VERSION = /^(?:=\s*)?[0-9]+\.[0-9]+\.[0-9]+(?:[-+][0-9A-Za-z.-]+)?$/;
6
+ const NATIVE_IMPLEMENTATION = /^native:([A-Za-z0-9.]+\/[A-Za-z0-9.]+)@([0-9]{4}-[0-9]{2}-[0-9]{2}(?:-preview)?)$/;
7
+ const AVM_IMPLEMENTATION = /^avm:([^@\s]+)@([^@\s]+)$/;
8
+ function assertIdentifier(value, label) {
9
+ if (!IDENTIFIER.test(value))
10
+ throw new TypeError(`${label} '${value}' is not a valid IaC identifier`);
11
+ }
12
+ function normalizeRelativePath(value) {
13
+ const normalized = value.replaceAll("\\", "/");
14
+ if (normalized.length === 0 || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized)) {
15
+ throw new TypeError(`Virtual tree path '${value}' must be relative`);
16
+ }
17
+ const segments = normalized.split("/");
18
+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
19
+ throw new TypeError(`Virtual tree path '${value}' contains an invalid segment`);
20
+ }
21
+ return segments.join("/");
22
+ }
23
+ function parseBinding(implementation, declaredVersion) {
24
+ const native = NATIVE_IMPLEMENTATION.exec(implementation);
25
+ if (native !== null) {
26
+ if (declaredVersion !== "legacy" && declaredVersion !== native[2]) {
27
+ throw new TypeError(`Binding version '${declaredVersion}' does not match implementation pin '${native[2]}'`);
28
+ }
29
+ return { kind: "native", source: native[1], version: native[2] };
30
+ }
31
+ const avm = AVM_IMPLEMENTATION.exec(implementation);
32
+ if (avm === null)
33
+ throw new TypeError(`Unsupported binding implementation '${implementation}'`);
34
+ const version = avm[2];
35
+ if (!EXACT_VERSION.test(version))
36
+ throw new TypeError(`AVM implementation '${implementation}' must use an exact version`);
37
+ if (declaredVersion !== "legacy" && declaredVersion !== version) {
38
+ throw new TypeError(`Binding version '${declaredVersion}' does not match implementation pin '${version}'`);
39
+ }
40
+ return { kind: "avm", source: avm[1], version };
41
+ }
42
+ function resourceContexts(intent, binding) {
43
+ if (intent.projectId !== binding.projectId || intent.runId !== binding.runId) {
44
+ throw new TypeError("Intent and binding identities do not match");
45
+ }
46
+ const intentIds = new Set(intent.resources.map(({ id }) => id));
47
+ const bindingIds = Object.keys(binding.resourceBindings);
48
+ if (bindingIds.length !== intentIds.size || bindingIds.some((id) => !intentIds.has(id))) {
49
+ throw new TypeError("Binding must cover every logical resource exactly once");
50
+ }
51
+ return [...intent.resources]
52
+ .sort((left, right) => left.id.localeCompare(right.id))
53
+ .map((resource) => {
54
+ assertIdentifier(resource.id, "Logical resource id");
55
+ for (const dependency of resource.dependsOn) {
56
+ assertIdentifier(dependency, "Dependency id");
57
+ if (!intentIds.has(dependency))
58
+ throw new TypeError(`Resource '${resource.id}' has unknown dependency '${dependency}'`);
59
+ }
60
+ const resourceBinding = binding.resourceBindings[resource.id];
61
+ return {
62
+ resource,
63
+ binding: resourceBinding,
64
+ declaration: resource.id,
65
+ parsed: parseBinding(resourceBinding.implementation, resourceBinding.version),
66
+ };
67
+ });
68
+ }
69
+ function requiredGenericParameters(context) {
70
+ const { name, location, parentId, properties } = context.binding.parameters;
71
+ if (typeof name !== "string" || name.length === 0)
72
+ throw new TypeError(`Native resource '${context.resource.id}' requires parameter 'name'`);
73
+ if (typeof location !== "string" || location.length === 0)
74
+ throw new TypeError(`Native resource '${context.resource.id}' requires parameter 'location'`);
75
+ if (typeof parentId !== "string" || parentId.length === 0)
76
+ throw new TypeError(`Native resource '${context.resource.id}' requires parameter 'parentId'`);
77
+ if (properties === null || typeof properties !== "object" || Array.isArray(properties)) {
78
+ throw new TypeError(`Native resource '${context.resource.id}' requires object parameter 'properties'`);
79
+ }
80
+ const { name: _name, location: _location, parentId: _parentId, properties: _properties, ...bodyFields } = context.binding.parameters;
81
+ return { name, location, parentId, properties, bodyFields };
82
+ }
83
+ function canonicalJson(value) {
84
+ if (Array.isArray(value))
85
+ return `[${value.map(canonicalJson).join(",")}]`;
86
+ if (value !== null && typeof value === "object") {
87
+ return `{${Object.entries(value)
88
+ .sort(([left], [right]) => left.localeCompare(right))
89
+ .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
90
+ .join(",")}}`;
91
+ }
92
+ const encoded = JSON.stringify(value);
93
+ if (encoded === undefined)
94
+ throw new TypeError("IaC values must be JSON-compatible");
95
+ return encoded;
96
+ }
97
+ function bicepString(value) {
98
+ return `'${value.replaceAll("'", "''")}'`;
99
+ }
100
+ function bicepObjectKey(value) {
101
+ return IDENTIFIER.test(value) ? value : bicepString(value);
102
+ }
103
+ function renderBicepValue(value, indent = 0) {
104
+ if (value === null)
105
+ return "null";
106
+ if (typeof value === "string")
107
+ return bicepString(value);
108
+ if (typeof value === "number") {
109
+ if (!Number.isFinite(value))
110
+ throw new TypeError("Bicep numbers must be finite");
111
+ return String(value);
112
+ }
113
+ if (typeof value === "boolean")
114
+ return String(value);
115
+ if (Array.isArray(value)) {
116
+ if (value.length === 0)
117
+ return "[]";
118
+ const padding = " ".repeat(indent + 2);
119
+ return `[\n${value.map((entry) => `${padding}${renderBicepValue(entry, indent + 2)}`).join("\n")}\n${" ".repeat(indent)}]`;
120
+ }
121
+ if (typeof value === "object") {
122
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
123
+ if (entries.length === 0)
124
+ return "{}";
125
+ const padding = " ".repeat(indent + 2);
126
+ return `{\n${entries.map(([key, entry]) => `${padding}${bicepObjectKey(key)}: ${renderBicepValue(entry, indent + 2)}`).join("\n")}\n${" ".repeat(indent)}}`;
127
+ }
128
+ throw new TypeError("Bicep values must be JSON-compatible");
129
+ }
130
+ function renderHclValue(value, indent = 0) {
131
+ if (value === null)
132
+ return "null";
133
+ if (typeof value === "string")
134
+ return JSON.stringify(value);
135
+ if (typeof value === "number") {
136
+ if (!Number.isFinite(value))
137
+ throw new TypeError("HCL numbers must be finite");
138
+ return String(value);
139
+ }
140
+ if (typeof value === "boolean")
141
+ return String(value);
142
+ if (Array.isArray(value))
143
+ return `[${value.map((entry) => renderHclValue(entry, indent)).join(", ")}]`;
144
+ if (typeof value === "object") {
145
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
146
+ if (entries.length === 0)
147
+ return "{}";
148
+ const padding = " ".repeat(indent + 2);
149
+ return `{\n${entries.map(([key, entry]) => `${padding}${JSON.stringify(key)} = ${renderHclValue(entry, indent + 2)}`).join("\n")}\n${" ".repeat(indent)}}`;
150
+ }
151
+ throw new TypeError("HCL values must be JSON-compatible");
152
+ }
153
+ function secureStorageProperties(type, properties) {
154
+ if (type.toLowerCase() !== "microsoft.storage/storageaccounts")
155
+ return properties;
156
+ return {
157
+ ...properties,
158
+ allowBlobPublicAccess: false,
159
+ allowSharedKeyAccess: false,
160
+ minimumTlsVersion: "TLS1_2",
161
+ supportsHttpsTrafficOnly: true,
162
+ };
163
+ }
164
+ function secureBicepParameters(context) {
165
+ if (context.resource.type.toLowerCase() !== "microsoft.storage/storageaccounts")
166
+ return context.binding.parameters;
167
+ return {
168
+ ...context.binding.parameters,
169
+ allowBlobPublicAccess: false,
170
+ allowSharedKeyAccess: false,
171
+ minimumTlsVersion: "TLS1_2",
172
+ supportsHttpsTrafficOnly: true,
173
+ };
174
+ }
175
+ function secureTerraformParameters(context) {
176
+ if (context.resource.type.toLowerCase() !== "microsoft.storage/storageaccounts")
177
+ return context.binding.parameters;
178
+ return {
179
+ ...context.binding.parameters,
180
+ allow_nested_items_to_be_public: false,
181
+ https_traffic_only_enabled: true,
182
+ min_tls_version: "TLS1_2",
183
+ shared_access_key_enabled: false,
184
+ };
185
+ }
186
+ function virtualTree(files, logicalManifest) {
187
+ const normalized = files
188
+ .map((file) => ({ path: normalizeRelativePath(file.path), content: file.content }))
189
+ .sort((left, right) => left.path.localeCompare(right.path));
190
+ if (new Set(normalized.map(({ path }) => path)).size !== normalized.length)
191
+ throw new TypeError("Virtual tree paths must be unique");
192
+ return { files: normalized, logicalManifest, treeHash: sha256(normalized) };
193
+ }
194
+ function manifest(intent, binding, sourcePath, existing) {
195
+ return {
196
+ schemaVersion: intent.schemaVersion,
197
+ projectId: intent.projectId,
198
+ runId: intent.runId,
199
+ track: binding.track,
200
+ resources: [...intent.resources]
201
+ .sort((left, right) => left.id.localeCompare(right.id))
202
+ .map((resource) => ({
203
+ logicalId: resource.id,
204
+ type: resource.type,
205
+ implementationAddress: binding.resourceBindings[resource.id].implementation,
206
+ implementationKind: existing.has(resource.id)
207
+ ? binding.track === "terraform"
208
+ ? "data"
209
+ : "existing"
210
+ : binding.resourceBindings[resource.id].implementation.startsWith("avm:")
211
+ ? "module"
212
+ : "resource",
213
+ ownership: existing.has(resource.id) ? "existing" : "managed",
214
+ dependsOn: [...resource.dependsOn].sort(),
215
+ generatedDependencies: [...resource.dependsOn].sort(),
216
+ sourcePath,
217
+ })),
218
+ };
219
+ }
220
+ function assertTrack(binding, expected) {
221
+ if (binding.track !== expected)
222
+ throw new TypeError(`Binding track '${binding.track}' is not '${expected}'`);
223
+ }
224
+ export function generateBicepTree(intent, binding, options = {}) {
225
+ assertTrack(binding, "bicep");
226
+ const contexts = resourceContexts(intent, binding);
227
+ const existing = new Set(options.existingResources ?? []);
228
+ const logicalIds = new Set(contexts.map(({ resource }) => resource.id));
229
+ for (const logicalId of existing) {
230
+ assertIdentifier(logicalId, "Existing resource id");
231
+ if (!logicalIds.has(logicalId))
232
+ throw new TypeError(`Existing resource '${logicalId}' is not present in the intent`);
233
+ }
234
+ const parameterNames = new Set();
235
+ const blocks = contexts.map((context) => {
236
+ if (context.parsed.kind === "native") {
237
+ const parameters = requiredGenericParameters(context);
238
+ const existingKeyword = existing.has(context.resource.id) ? " existing" : "";
239
+ if (existingKeyword !== "") {
240
+ return `resource ${context.declaration} '${context.parsed.source}@${context.parsed.version}' existing = {\n name: ${bicepString(parameters.name)}\n}`;
241
+ }
242
+ const dependencies = context.resource.dependsOn.length === 0
243
+ ? ""
244
+ : `\n dependsOn: [\n${[...context.resource.dependsOn]
245
+ .sort()
246
+ .map((id) => ` ${id}`)
247
+ .join("\n")}\n ]`;
248
+ const bodyFields = Object.entries(parameters.bodyFields)
249
+ .sort(([left], [right]) => left.localeCompare(right))
250
+ .map(([name, value]) => ` ${bicepObjectKey(name)}: ${renderBicepValue(value, 2)}`)
251
+ .join("\n");
252
+ return `resource ${context.declaration} '${context.parsed.source}@${context.parsed.version}' = {\n name: ${bicepString(parameters.name)}\n location: ${bicepString(parameters.location)}${bodyFields.length === 0 ? "" : `\n${bodyFields}`}\n properties: ${renderBicepValue(secureStorageProperties(context.resource.type, parameters.properties), 2)}${dependencies}\n}`;
253
+ }
254
+ if (existing.has(context.resource.id)) {
255
+ throw new TypeError(`Existing AVM resource '${context.resource.id}' is unsupported; use a native binding`);
256
+ }
257
+ const source = context.parsed.source;
258
+ const moduleReference = source.startsWith("br/") ? source : `br/public:${source}`;
259
+ const renderedParameters = Object.entries(secureBicepParameters(context))
260
+ .sort(([left], [right]) => left.localeCompare(right))
261
+ .map(([name, value]) => {
262
+ assertIdentifier(name, "Bicep parameter name");
263
+ parameterNames.add(name);
264
+ return ` ${name}: ${renderBicepValue(value, 4)}`;
265
+ });
266
+ const dependencies = context.resource.dependsOn.length === 0
267
+ ? ""
268
+ : `\n dependsOn: [\n${[...context.resource.dependsOn]
269
+ .sort()
270
+ .map((id) => ` ${id}`)
271
+ .join("\n")}\n ]`;
272
+ return `module ${context.declaration} '${moduleReference}:${context.parsed.version}' = {\n name: ${bicepString(`${context.resource.id}-deployment`)}\n params: {\n${renderedParameters.join("\n")}\n }${dependencies}\n}`;
273
+ });
274
+ const descriptions = [...parameterNames]
275
+ .sort()
276
+ .map((name) => `@description(${bicepString(`Deployment value for ${name}`)})\nparam ${name} string`)
277
+ .join("\n\n");
278
+ const content = ["targetScope = 'resourceGroup'", descriptions, blocks.join("\n\n")]
279
+ .filter((part) => part.length > 0)
280
+ .join("\n\n") + "\n";
281
+ return virtualTree([{ path: "main.bicep", content }], manifest(intent, binding, "main.bicep", existing));
282
+ }
283
+ function exactProviderConstraint(value, fallback, label) {
284
+ const constraint = value ?? fallback;
285
+ if (!EXACT_VERSION.test(constraint))
286
+ throw new TypeError(`${label} must be an exact semantic version constraint`);
287
+ return constraint.startsWith("=") ? constraint : `= ${constraint}`;
288
+ }
289
+ function terraformDependency(context, contexts, existing) {
290
+ const byId = new Map(contexts.map((entry) => [entry.resource.id, entry]));
291
+ const dependencies = [...context.resource.dependsOn].sort().map((id) => {
292
+ const dependency = byId.get(id);
293
+ return dependency.parsed.kind === "native"
294
+ ? existing.has(id)
295
+ ? `data.azapi_resource.${dependency.declaration}`
296
+ : `azapi_resource.${dependency.declaration}`
297
+ : `module.${dependency.declaration}`;
298
+ });
299
+ return dependencies.length === 0 ? "" : `\n depends_on = [${dependencies.join(", ")}]`;
300
+ }
301
+ export function generateTerraformTree(intent, binding, options = {}) {
302
+ assertTrack(binding, "terraform");
303
+ const contexts = resourceContexts(intent, binding);
304
+ const existing = new Set(options.existingResources ?? []);
305
+ const logicalIds = new Set(contexts.map(({ resource }) => resource.id));
306
+ for (const logicalId of existing) {
307
+ assertIdentifier(logicalId, "Existing resource id");
308
+ if (!logicalIds.has(logicalId))
309
+ throw new TypeError(`Existing resource '${logicalId}' is not present in the intent`);
310
+ }
311
+ const azurerm = exactProviderConstraint(options.azurermProviderConstraint, "= 4.0.0", "azurerm provider constraint");
312
+ const azapi = exactProviderConstraint(options.azapiProviderConstraint, "= 2.0.0", "azapi provider constraint");
313
+ const versions = `terraform {\n required_providers {\n azapi = {\n source = "Azure/azapi"\n version = "${azapi}"\n }\n azurerm = {\n source = "hashicorp/azurerm"\n version = "${azurerm}"\n }\n }\n}\n`;
314
+ const variables = [
315
+ ...new Set(contexts.flatMap((context) => Object.keys(context.parsed.kind === "avm" ? secureTerraformParameters(context) : context.binding.parameters))),
316
+ ]
317
+ .sort()
318
+ .map((name) => {
319
+ assertIdentifier(name, "Terraform variable name");
320
+ return `variable "${name}" {\n description = "Deployment value for ${name}"\n type = any\n default = null\n}`;
321
+ })
322
+ .join("\n\n");
323
+ const main = contexts
324
+ .map((context) => {
325
+ if (context.parsed.kind === "native") {
326
+ const parameters = requiredGenericParameters(context);
327
+ if (existing.has(context.resource.id)) {
328
+ return `data "azapi_resource" "${context.declaration}" {\n type = "${context.parsed.source}@${context.parsed.version}"\n name = ${JSON.stringify(parameters.name)}\n parent_id = ${JSON.stringify(parameters.parentId)}\n}`;
329
+ }
330
+ const body = {
331
+ ...parameters.bodyFields,
332
+ properties: secureStorageProperties(context.resource.type, parameters.properties),
333
+ };
334
+ return `resource "azapi_resource" "${context.declaration}" {\n type = "${context.parsed.source}@${context.parsed.version}"\n name = ${JSON.stringify(parameters.name)}\n location = ${JSON.stringify(parameters.location)}\n parent_id = ${JSON.stringify(parameters.parentId)}\n body = ${renderHclValue(body, 2)}${terraformDependency(context, contexts, existing)}\n}`;
335
+ }
336
+ if (existing.has(context.resource.id)) {
337
+ throw new TypeError(`Existing AVM resource '${context.resource.id}' is unsupported; use a native binding`);
338
+ }
339
+ const argumentsText = Object.entries(secureTerraformParameters(context))
340
+ .sort(([left], [right]) => left.localeCompare(right))
341
+ .map(([name, value]) => ` ${name} = ${renderHclValue(value, 2)}`)
342
+ .join("\n");
343
+ return `module "${context.declaration}" {\n source = "${context.parsed.source}"\n version = "${context.parsed.version}"\n${argumentsText}${terraformDependency(context, contexts, existing)}\n}`;
344
+ })
345
+ .join("\n\n") + "\n";
346
+ const outputs = [...intent.outputs]
347
+ .sort()
348
+ .map((name) => {
349
+ assertIdentifier(name, "Terraform output name");
350
+ return `output "${name}" {\n value = null\n}`;
351
+ })
352
+ .join("\n\n");
353
+ const files = [
354
+ { path: "main.tf", content: main },
355
+ { path: "outputs.tf", content: outputs.length === 0 ? "" : `${outputs}\n` },
356
+ { path: "provider.tf", content: 'provider "azurerm" {\n features {}\n}\n\nprovider "azapi" {}\n' },
357
+ { path: "variables.tf", content: variables.length === 0 ? "" : `${variables}\n` },
358
+ { path: "versions.tf", content: versions },
359
+ ];
360
+ if (options.lockFileContent !== undefined)
361
+ files.push({ path: ".terraform.lock.hcl", content: options.lockFileContent });
362
+ return virtualTree(files, manifest(intent, binding, "main.tf", existing));
363
+ }
364
+ function logicalImplementation(value) {
365
+ const parsed = parseBinding(value, "legacy");
366
+ return `${parsed.kind}:${parsed.source}@${parsed.version}`;
367
+ }
368
+ export function compareLogicalParity(bicep, terraform) {
369
+ const left = "logicalManifest" in bicep ? bicep.logicalManifest : bicep;
370
+ const right = "logicalManifest" in terraform ? terraform.logicalManifest : terraform;
371
+ const leftById = new Map(left.resources.map((resource) => [resource.logicalId, resource]));
372
+ const rightById = new Map(right.resources.map((resource) => [resource.logicalId, resource]));
373
+ const differences = [];
374
+ for (const logicalId of [...new Set([...leftById.keys(), ...rightById.keys()])].sort()) {
375
+ const bicepResource = leftById.get(logicalId);
376
+ const terraformResource = rightById.get(logicalId);
377
+ if (bicepResource === undefined || terraformResource === undefined) {
378
+ differences.push({
379
+ logicalId,
380
+ field: "missing",
381
+ ...(bicepResource === undefined ? {} : { bicep: bicepResource }),
382
+ ...(terraformResource === undefined ? {} : { terraform: terraformResource }),
383
+ });
384
+ continue;
385
+ }
386
+ if (bicepResource.type !== terraformResource.type)
387
+ differences.push({ logicalId, field: "type", bicep: bicepResource.type, terraform: terraformResource.type });
388
+ if (canonicalJson([...bicepResource.dependsOn].sort()) !== canonicalJson([...terraformResource.dependsOn].sort())) {
389
+ differences.push({
390
+ logicalId,
391
+ field: "dependsOn",
392
+ bicep: bicepResource.dependsOn,
393
+ terraform: terraformResource.dependsOn,
394
+ });
395
+ }
396
+ if (bicepResource.ownership !== terraformResource.ownership ||
397
+ canonicalJson([...bicepResource.generatedDependencies].sort()) !==
398
+ canonicalJson([...terraformResource.generatedDependencies].sort())) {
399
+ differences.push({
400
+ logicalId,
401
+ field: "implementation",
402
+ bicep: { ownership: bicepResource.ownership, dependencies: bicepResource.generatedDependencies },
403
+ terraform: { ownership: terraformResource.ownership, dependencies: terraformResource.generatedDependencies },
404
+ });
405
+ }
406
+ const leftImplementation = logicalImplementation(bicepResource.implementationAddress);
407
+ const rightImplementation = logicalImplementation(terraformResource.implementationAddress);
408
+ if (leftImplementation !== rightImplementation) {
409
+ const bothNative = leftImplementation.startsWith("native:") && rightImplementation.startsWith("native:");
410
+ const bothAvm = leftImplementation.startsWith("avm:") && rightImplementation.startsWith("avm:");
411
+ if (!bothNative && !bothAvm)
412
+ differences.push({
413
+ logicalId,
414
+ field: "implementation",
415
+ bicep: leftImplementation,
416
+ terraform: rightImplementation,
417
+ });
418
+ }
419
+ }
420
+ return { equal: differences.length === 0, differences };
421
+ }
422
+ function validationPlans(tree, cwd) {
423
+ const paths = new Set(tree.files.map(({ path }) => path));
424
+ if (paths.has("main.bicep"))
425
+ return [{ executable: "bicep", args: ["build", "main.bicep"], cwd }];
426
+ if (paths.has("main.tf")) {
427
+ return [
428
+ { executable: "terraform", args: ["fmt", "-check"], cwd },
429
+ { executable: "terraform", args: ["init", "-backend=false", "-input=false"], cwd },
430
+ { executable: "terraform", args: ["validate"], cwd },
431
+ ];
432
+ }
433
+ return [];
434
+ }
435
+ const BOOLEAN_TRUE = new Set(["true"]);
436
+ const BOOLEAN_FALSE = new Set(["false"]);
437
+ const TLS_1_2 = new Set(["'TLS1_2'", '"TLS1_2"']);
438
+ const BICEP_STORAGE_INVARIANTS = [
439
+ {
440
+ diagnostic: "minimumTlsVersion[^\\n]*['\"]TLS1_2['\"]|minimumTlsVersion[\\s\\S]*['\"]TLS1_2['\"]",
441
+ keys: ["minimumTlsVersion"],
442
+ values: TLS_1_2,
443
+ },
444
+ {
445
+ diagnostic: "supportsHttpsTrafficOnly[^\\n]*true",
446
+ keys: ["supportsHttpsTrafficOnly"],
447
+ values: BOOLEAN_TRUE,
448
+ },
449
+ {
450
+ diagnostic: "allowBlobPublicAccess[^\\n]*false",
451
+ keys: ["allowBlobPublicAccess"],
452
+ values: BOOLEAN_FALSE,
453
+ },
454
+ {
455
+ diagnostic: "allowSharedKeyAccess[^\\n]*false",
456
+ keys: ["allowSharedKeyAccess"],
457
+ values: BOOLEAN_FALSE,
458
+ },
459
+ ];
460
+ const TERRAFORM_STORAGE_INVARIANTS = [
461
+ {
462
+ diagnostic: "(?:minimumTlsVersion|min_tls_version)[^\\n]*['\"]TLS1_2['\"]|(?:minimumTlsVersion|min_tls_version)[\\s\\S]*['\"]TLS1_2['\"]",
463
+ keys: ["minimumTlsVersion", "min_tls_version"],
464
+ values: TLS_1_2,
465
+ },
466
+ {
467
+ diagnostic: "(?:supportsHttpsTrafficOnly|https_traffic_only_enabled)[^\\n]*true",
468
+ keys: ["supportsHttpsTrafficOnly", "https_traffic_only_enabled"],
469
+ values: BOOLEAN_TRUE,
470
+ },
471
+ {
472
+ diagnostic: "(?:allowBlobPublicAccess|allow_nested_items_to_be_public)[^\\n]*false",
473
+ keys: ["allowBlobPublicAccess", "allow_nested_items_to_be_public"],
474
+ values: BOOLEAN_FALSE,
475
+ },
476
+ {
477
+ diagnostic: "(?:allowSharedKeyAccess|shared_access_key_enabled)[^\\n]*false",
478
+ keys: ["allowSharedKeyAccess", "shared_access_key_enabled"],
479
+ values: BOOLEAN_FALSE,
480
+ },
481
+ ];
482
+ function assignmentToken(line) {
483
+ const colon = line.indexOf(":");
484
+ const equals = line.indexOf("=");
485
+ const separator = colon < 0 ? equals : equals < 0 ? colon : Math.min(colon, equals);
486
+ if (separator < 0)
487
+ return undefined;
488
+ const rawKey = line.slice(0, separator).trim();
489
+ const remainder = line.slice(separator + 1).trimStart();
490
+ if (rawKey.length === 0 || remainder.length === 0)
491
+ return undefined;
492
+ const keyQuote = rawKey[0];
493
+ const key = (keyQuote === "'" || keyQuote === '"') && rawKey.at(-1) === keyQuote ? rawKey.slice(1, -1) : rawKey;
494
+ const quote = remainder[0];
495
+ if (quote === "'" || quote === '"') {
496
+ const end = remainder.indexOf(quote, 1);
497
+ return end < 0 ? undefined : [key, remainder.slice(0, end + 1)];
498
+ }
499
+ let end = 0;
500
+ while (end < remainder.length && !" \t\r,}]".includes(remainder[end]))
501
+ end += 1;
502
+ return [key, remainder.slice(0, end)];
503
+ }
504
+ function missingStorageInvariants(source, invariants) {
505
+ const invariantsByKey = new Map();
506
+ for (const invariant of invariants) {
507
+ for (const key of invariant.keys) {
508
+ const entries = invariantsByKey.get(key) ?? [];
509
+ entries.push(invariant);
510
+ invariantsByKey.set(key, entries);
511
+ }
512
+ }
513
+ const found = new Set();
514
+ for (const line of source.split("\n")) {
515
+ const assignment = assignmentToken(line);
516
+ if (assignment === undefined)
517
+ continue;
518
+ const [key, value] = assignment;
519
+ for (const invariant of invariantsByKey.get(key) ?? []) {
520
+ if (invariant.values.has(value))
521
+ found.add(invariant);
522
+ }
523
+ }
524
+ return invariants.filter((invariant) => !found.has(invariant));
525
+ }
526
+ export async function validateGeneratedTree(tree, options = {}) {
527
+ const issues = [];
528
+ const paths = tree.files.map(({ path }) => {
529
+ try {
530
+ return normalizeRelativePath(path);
531
+ }
532
+ catch (error) {
533
+ issues.push(error.message);
534
+ return path;
535
+ }
536
+ });
537
+ if (new Set(paths).size !== paths.length)
538
+ issues.push("Generated tree contains duplicate paths");
539
+ if (sha256([...tree.files]
540
+ .map(({ path, content }) => ({ path: normalizeRelativePath(path), content }))
541
+ .sort((left, right) => left.path.localeCompare(right.path))) !== tree.treeHash) {
542
+ issues.push("Generated tree hash does not match its files");
543
+ }
544
+ const manifestIds = new Set(tree.logicalManifest.resources.map(({ logicalId }) => logicalId));
545
+ if (manifestIds.size !== tree.logicalManifest.resources.length)
546
+ issues.push("Logical manifest contains duplicate resources");
547
+ for (const resource of tree.logicalManifest.resources) {
548
+ const source = tree.files.find(({ path }) => path === resource.sourcePath);
549
+ if (source === undefined) {
550
+ issues.push(`Logical resource '${resource.logicalId}' source path is missing`);
551
+ continue;
552
+ }
553
+ const declaration = tree.logicalManifest.track === "bicep"
554
+ ? new RegExp(`(?:resource|module)\\s+${resource.logicalId}\\s`)
555
+ : new RegExp(`(?:resource\\s+"[^"]+"|data\\s+"[^"]+"|module)\\s+"${resource.logicalId}"`);
556
+ if (!declaration.test(source.content))
557
+ issues.push(`Logical resource '${resource.logicalId}' declaration is missing`);
558
+ }
559
+ const storageResources = tree.logicalManifest.resources.filter(({ type }) => type.toLowerCase() === "microsoft.storage/storageaccounts");
560
+ if (storageResources.length > 0) {
561
+ const source = tree.files.map(({ content }) => content).join("\n");
562
+ const required = tree.logicalManifest.track === "bicep" ? BICEP_STORAGE_INVARIANTS : TERRAFORM_STORAGE_INVARIANTS;
563
+ for (const invariant of missingStorageInvariants(source, required))
564
+ issues.push(`Storage security invariant '${invariant.diagnostic}' is missing`);
565
+ }
566
+ const plans = validationPlans(tree, options.cwd ?? ".");
567
+ const commandResults = [];
568
+ if (options.runner !== undefined) {
569
+ for (const plan of plans) {
570
+ commandResults.push(await options.runner.run({
571
+ ...plan,
572
+ timeoutMs: options.timeoutMs ?? 120_000,
573
+ maxOutputBytes: options.maxOutputBytes ?? 1_048_576,
574
+ }));
575
+ }
576
+ }
577
+ return { valid: issues.length === 0, issues, commandPlans: plans, commandResults };
578
+ }
579
+ async function assertNoSymlinkParents(root, destination) {
580
+ const rootResolved = resolve(root);
581
+ const relativePath = relative(rootResolved, destination);
582
+ let current = rootResolved;
583
+ for (const segment of relativePath.split(sep).slice(0, -1)) {
584
+ current = join(current, segment);
585
+ try {
586
+ if ((await lstat(current)).isSymbolicLink())
587
+ throw new TypeError(`Refusing to write through symlink parent '${current}'`);
588
+ }
589
+ catch (error) {
590
+ if (error.code !== "ENOENT")
591
+ throw error;
592
+ }
593
+ }
594
+ }
595
+ export async function writeVirtualTree(root, tree, options = {}) {
596
+ if (isAbsolute(root) === false && root.trim().length === 0)
597
+ throw new TypeError("Virtual tree root is required");
598
+ const rootResolved = resolve(root);
599
+ await mkdir(rootResolved, { recursive: true });
600
+ if ((await lstat(rootResolved)).isSymbolicLink())
601
+ throw new TypeError("Virtual tree root must not be a symlink");
602
+ const canonicalRoot = await realpath(rootResolved);
603
+ const staged = [];
604
+ try {
605
+ for (const file of tree.files) {
606
+ const normalized = normalizeRelativePath(file.path);
607
+ const destination = resolve(canonicalRoot, normalized);
608
+ if (destination !== canonicalRoot && !destination.startsWith(`${canonicalRoot}${sep}`))
609
+ throw new TypeError(`Path '${file.path}' escapes the root`);
610
+ await assertNoSymlinkParents(canonicalRoot, destination);
611
+ await mkdir(dirname(destination), { recursive: true });
612
+ await assertNoSymlinkParents(canonicalRoot, destination);
613
+ const temporary = `${destination}.apex-${process.pid}-${sha256([normalized, file.content]).slice(0, 12)}.tmp`;
614
+ const handle = await open(temporary, "wx", 0o600);
615
+ try {
616
+ await handle.writeFile(file.content, "utf8");
617
+ await handle.sync();
618
+ }
619
+ finally {
620
+ await handle.close();
621
+ }
622
+ staged.push({ destination, temporary });
623
+ }
624
+ if (options.overwrite !== true) {
625
+ for (const { destination } of staged) {
626
+ try {
627
+ await lstat(destination);
628
+ throw new TypeError(`Refusing to overwrite '${destination}'`);
629
+ }
630
+ catch (error) {
631
+ if (error.code !== "ENOENT")
632
+ throw error;
633
+ }
634
+ }
635
+ }
636
+ for (const { destination, temporary } of staged) {
637
+ await rename(temporary, destination);
638
+ }
639
+ }
640
+ catch (error) {
641
+ await Promise.all(staged.map(async ({ temporary }) => await rm(temporary, { force: true })));
642
+ throw error;
643
+ }
644
+ }
645
+ //# sourceMappingURL=iac-generation.js.map