nexarch 0.12.15 → 0.12.18

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.
@@ -3,7 +3,7 @@ import { execFileSync } from "child_process";
3
3
  import { resolve } from "path";
4
4
  import { callMcpTool } from "../lib/mcp.js";
5
5
  import { parseTerraformStateBuffer, projectTerraformState, inferEnvironmentFromTags, environmentSubtypeFor, } from "../lib/terraform-projection.js";
6
- import { loadTerraformCatalogue, describeCatalogue } from "../lib/terraform-catalogue.js";
6
+ import { loadIacCatalogue, describeCatalogue } from "../lib/iac-catalogue.js";
7
7
  import { discoverRootModules, environmentFromPath } from "../lib/terraform-detect.js";
8
8
  const ENTITY_BATCH = 50;
9
9
  function readResult(raw) {
@@ -189,6 +189,7 @@ export function buildIngestPayload(params) {
189
189
  const identityEntityRefByName = new Map();
190
190
  const identityRefByPrincipalId = new Map();
191
191
  const catalogueByResourceType = new Map(catalogue.map((entry) => [entry.resourceType, entry]));
192
+ const canonicalTechnologies = new Set();
192
193
  for (const resource of projection.resources) {
193
194
  const entityRef = `${resource.entityTypeCode}:${slug(resource.name ?? resource.address)}`;
194
195
  entities.push({
@@ -219,6 +220,22 @@ export function buildIngestPayload(params) {
219
220
  relationships.push({ relationshipTypeCode: "runs_on", fromEntityRef: entityRef, toEntityRef: environmentRef });
220
221
  relationships.push({ relationshipTypeCode: "runs_on", fromEntityRef: entityRef, toEntityRef: platformRef });
221
222
  }
223
+ // What product is this? Without it the graph holds a resource that knows
224
+ // where it runs and whose cloud it is on, but not what it *is* — the only
225
+ // record of that being a raw `terraform_type` attribute, which no reader
226
+ // can join across environments or tenants. The canonical entry is shared
227
+ // vocabulary, so one query answers "everything we run on Azure AI
228
+ // Services" across every environment and every IaC tool.
229
+ const canonicalRef = catalogueByResourceType.get(resource.resourceType)?.canonicalEntityRef;
230
+ if (canonicalRef && !canonicalTechnologies.has(canonicalRef)) {
231
+ canonicalTechnologies.add(canonicalRef);
232
+ }
233
+ if (canonicalRef) {
234
+ // `instance_of`, not `runs_on`: this vault does not run on Azure Key
235
+ // Vault, it is one. runs_on is a hosting relation and is already covered
236
+ // above by the edges to the platform and the environment.
237
+ relationships.push({ relationshipTypeCode: "instance_of", fromEntityRef: entityRef, toEntityRef: canonicalRef });
238
+ }
222
239
  if (resource.name) {
223
240
  entityRefByTypeAndName.set(`${resource.resourceType}::${resource.name}`, entityRef);
224
241
  entityRefByName.set(resource.name, entityRef);
@@ -298,8 +315,38 @@ export function buildIngestPayload(params) {
298
315
  addTopologyEdge("depends_on", entityRef, identityEntityRefByName.get(identityName));
299
316
  }
300
317
  }
318
+ // The canonical technologies referenced above, upserted so the edges have
319
+ // something to land on. Named from the ref rather than invented per tenant:
320
+ // these are shared vocabulary, and a tenant-local "Azure AI Services" that
321
+ // differs by a word defeats the point of having a canonical entry at all.
322
+ for (const canonicalRef of canonicalTechnologies) {
323
+ const canonicalTypeCode = canonicalRef.split(":")[1] ?? "platform_component";
324
+ entities.push({
325
+ entityRef: canonicalRef,
326
+ entityTypeCode: canonicalTypeCode,
327
+ name: canonicalTechnologyName(canonicalRef),
328
+ description: "Cloud service referenced by infrastructure ingestion.",
329
+ attributes: { source: "terraform_ingest", canonical: true },
330
+ });
331
+ // A managed service belongs to the cloud that offers it. Only from a
332
+ // platform_component: part_of does not accept a technology_component
333
+ // source, so a canonical that is one is left unattached rather than sent
334
+ // to fail governance.
335
+ if (canonicalTypeCode === "platform_component") {
336
+ relationships.push({ relationshipTypeCode: "part_of", fromEntityRef: canonicalRef, toEntityRef: platformRef });
337
+ }
338
+ }
301
339
  return { entities, relationships, environmentRef };
302
340
  }
341
+ /** Turns global:platform_component:azure_ai_services into "Azure AI Services". */
342
+ function canonicalTechnologyName(canonicalRef) {
343
+ const slugPart = canonicalRef.split(":").slice(2).join(":") || canonicalRef;
344
+ return slugPart
345
+ .split("_")
346
+ .filter(Boolean)
347
+ .map((word) => (word.length <= 3 && word === word.toLowerCase() && ["ai", "api", "dns", "sql", "cdn"].includes(word) ? word.toUpperCase() : word[0].toUpperCase() + word.slice(1)))
348
+ .join(" ");
349
+ }
303
350
  function chunk(items, size) {
304
351
  const out = [];
305
352
  for (let i = 0; i < items.length; i += size)
@@ -321,7 +368,7 @@ export async function ingestInfra(args) {
321
368
  // Fetched once and used for both projection and payload: two different
322
369
  // catalogues within one ingest would produce entities whose attributes and
323
370
  // whose type mapping disagreed about what a resource is.
324
- const catalogueResult = await loadTerraformCatalogue();
371
+ const catalogueResult = await loadIacCatalogue();
325
372
  if (!asJson)
326
373
  console.log(` ${describeCatalogue(catalogueResult)}`);
327
374
  const catalogue = catalogueResult.entries;
@@ -338,6 +385,22 @@ export async function ingestInfra(args) {
338
385
  ` 4. if this repository has several root modules, ingest each separately with --dir\n` +
339
386
  ` The estate may also simply not have been applied yet.`);
340
387
  }
388
+ // Nothing recognised is a coverage problem, and has to be said before the
389
+ // environment is even considered. Otherwise a root whose every resource type
390
+ // is missing from the catalogue fails with "could not determine the
391
+ // environment — 0 of those carried any tags", which sends the reader off to
392
+ // tag resources that would still not be ingested afterwards. The environment
393
+ // is the next question only once there is something to attach it to.
394
+ if (projection.stats.resourcesProjected === 0) {
395
+ const seen = projection.unknownTypes
396
+ .slice(0, 12)
397
+ .map((entry) => ` ${String(entry.count).padStart(3)} ${entry.resourceType}`)
398
+ .join("\n");
399
+ throw new Error(`Nothing in this root module is in the catalogue yet — read ${projection.stats.resourcesSeen} resources, matched none.\n` +
400
+ ` Tagging or naming an environment will not help: there would still be nothing to ingest.\n` +
401
+ `\n Resource types found here:\n${seen}\n` +
402
+ `\n These need adding to the platform catalogue before this root can be ingested.`);
403
+ }
341
404
  const { environment, how } = resolveEnvironment(projection, environmentOverride, workingDir, statePath ? null : workingDir);
342
405
  projection.environment = environment;
343
406
  // Azure is the only provider the seed catalogue covers today; resolving it
@@ -359,6 +422,26 @@ export async function ingestInfra(args) {
359
422
  // Reference resolution is an optimisation, not a requirement.
360
423
  }
361
424
  }
425
+ // Types the catalogue does not cover are filed as reference candidates, the
426
+ // same way an unrecognised package name is. Printing them to a console nobody
427
+ // aggregates meant the platform could never learn which services customers
428
+ // actually run: curation happened when someone happened to notice. A resource
429
+ // type is vendor vocabulary — `azurerm_static_web_app` contains nothing of the
430
+ // tenant's — so it is safe to file in a cross-tenant queue, where seen_count
431
+ // accumulates the evidence for covering it.
432
+ //
433
+ // Resolving doubles as filing: a type someone has already promoted comes back
434
+ // resolved, and only genuine misses become candidates.
435
+ if (!dryRun && projection.unknownTypes.length > 0) {
436
+ try {
437
+ await callMcpTool("nexarch_resolve_reference", {
438
+ names: projection.unknownTypes.slice(0, 200).map((entry) => entry.resourceType),
439
+ });
440
+ }
441
+ catch {
442
+ // Coverage reporting must never fail an ingest: the estate is the point.
443
+ }
444
+ }
362
445
  const { entities, relationships } = buildIngestPayload({ projection, environment, platformRef, platformName, catalogue });
363
446
  const report = {
364
447
  ok: true,
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Fallback catalogue for Terraform resource types.
3
+ *
4
+ * GENERATED FILE — DO NOT EDIT BY HAND.
5
+ * Regenerate with: node web/scripts/export-iac-catalogue.mjs
6
+ *
7
+ * The catalogue proper is platform reference data, curated in
8
+ * `iac_catalogue_entry` and served to every tenant, so supporting a new
9
+ * service is a data change rather than a CLI release. This file is what the CLI
10
+ * falls back to when it cannot reach the platform — offline, or a CI runner with
11
+ * no route to the gateway. It is generated from that table so it can never get
12
+ * ahead of it: editing it by hand recreates the two-sources-of-truth problem
13
+ * moving the catalogue out of the CLI was meant to end.
14
+ *
15
+ * 30 entries, exported 2026-08-21.
16
+ */
17
+ export const IAC_CATALOGUE_SEED = [
18
+ {
19
+ resourceType: "azurerm_container_app_environment",
20
+ entityTypeCode: "platform_component",
21
+ entitySubtypeCode: "platform_runtime",
22
+ role: "compute_environment",
23
+ attributes: ["name", "location", "resource_group_name", "default_domain", "infrastructure_subnet_id", "internal_load_balancer_enabled", "static_ip_address", "zone_redundancy_enabled"],
24
+ references: { "infrastructure_subnet_id": "azurerm_subnet" },
25
+ canonicalEntityRef: "global:platform_component:azure_container_apps",
26
+ cloudResourceType: "Microsoft.App/managedEnvironments",
27
+ },
28
+ {
29
+ resourceType: "azurerm_service_plan",
30
+ entityTypeCode: "platform_component",
31
+ entitySubtypeCode: "platform_runtime",
32
+ role: "compute_environment",
33
+ attributes: ["name", "location", "resource_group_name", "sku_name", "os_type", "worker_count"],
34
+ canonicalEntityRef: "global:platform_component:azure_app_service",
35
+ cloudResourceType: "Microsoft.Web/serverfarms",
36
+ },
37
+ {
38
+ resourceType: "azurerm_cognitive_account",
39
+ entityTypeCode: "platform_component",
40
+ entitySubtypeCode: "platform_service",
41
+ role: "compute_host",
42
+ attributes: ["name", "location", "resource_group_name", "kind", "sku_name", "endpoint", "custom_subdomain_name", "public_network_access_enabled", "local_auth_enabled"],
43
+ canonicalEntityRef: "global:platform_component:azure_ai_services",
44
+ cloudResourceType: "Microsoft.CognitiveServices/accounts",
45
+ },
46
+ {
47
+ resourceType: "azurerm_container_app",
48
+ entityTypeCode: "platform_component",
49
+ entitySubtypeCode: "platform_runtime",
50
+ role: "compute_host",
51
+ bindable: true,
52
+ attributes: ["name", "location", "resource_group_name", "container_app_environment_id", "revision_mode", "workload_profile_name"],
53
+ references: { "container_app_environment_id": "azurerm_container_app_environment" },
54
+ canonicalEntityRef: "global:platform_component:azure_container_apps",
55
+ cloudResourceType: "Microsoft.App/containerApps",
56
+ },
57
+ {
58
+ resourceType: "azurerm_container_app_job",
59
+ entityTypeCode: "platform_component",
60
+ entitySubtypeCode: "platform_runtime",
61
+ role: "compute_host",
62
+ bindable: true,
63
+ attributes: ["name", "location", "resource_group_name", "container_app_environment_id", "replica_timeout_in_seconds", "workload_profile_name"],
64
+ references: { "container_app_environment_id": "azurerm_container_app_environment" },
65
+ canonicalEntityRef: "global:platform_component:azure_container_apps",
66
+ cloudResourceType: "Microsoft.App/jobs",
67
+ },
68
+ {
69
+ resourceType: "azurerm_linux_web_app",
70
+ entityTypeCode: "platform_component",
71
+ entitySubtypeCode: "platform_runtime",
72
+ role: "compute_host",
73
+ bindable: true,
74
+ attributes: ["name", "location", "resource_group_name", "service_plan_id", "https_only", "default_hostname"],
75
+ references: { "service_plan_id": "azurerm_service_plan" },
76
+ canonicalEntityRef: "global:platform_component:azure_app_service",
77
+ cloudResourceType: "Microsoft.Web/sites",
78
+ },
79
+ {
80
+ resourceType: "azurerm_static_web_app",
81
+ entityTypeCode: "platform_component",
82
+ entitySubtypeCode: "platform_service",
83
+ role: "compute_host",
84
+ bindable: true,
85
+ attributes: ["name", "location", "resource_group_name", "default_host_name", "sku_tier", "sku_size", "public_network_access_enabled", "preview_environments_enabled", "repository_url"],
86
+ canonicalEntityRef: "global:platform_component:azure_static_web_apps",
87
+ cloudResourceType: "Microsoft.Web/staticSites",
88
+ },
89
+ {
90
+ resourceType: "azurerm_cosmosdb_account",
91
+ entityTypeCode: "data_store",
92
+ entitySubtypeCode: "store_document",
93
+ role: "data_store",
94
+ attributes: ["name", "location", "resource_group_name", "kind", "offer_type", "public_network_access_enabled", "endpoint"],
95
+ canonicalEntityRef: "global:platform_component:azure_cosmos_db",
96
+ cloudResourceType: "Microsoft.DocumentDB/databaseAccounts",
97
+ },
98
+ {
99
+ resourceType: "azurerm_postgresql_flexible_server",
100
+ entityTypeCode: "data_store",
101
+ entitySubtypeCode: "store_relational",
102
+ role: "data_store",
103
+ attributes: ["name", "location", "resource_group_name", "fqdn", "version", "sku_name", "storage_mb", "storage_tier", "public_network_access_enabled", "delegated_subnet_id", "private_dns_zone_id", "backup_retention_days", "geo_redundant_backup_enabled", "zone"],
104
+ canonicalEntityRef: "global:platform_component:azure_database_for_postgresql",
105
+ cloudResourceType: "Microsoft.DBforPostgreSQL/flexibleServers",
106
+ },
107
+ {
108
+ resourceType: "azurerm_postgresql_flexible_server_database",
109
+ entityTypeCode: "data_store",
110
+ entitySubtypeCode: "store_relational",
111
+ role: "data_store",
112
+ attributes: ["name", "server_id", "charset", "collation"],
113
+ canonicalEntityRef: "global:platform_component:azure_database_for_postgresql",
114
+ cloudResourceType: "Microsoft.DBforPostgreSQL/flexibleServers/databases",
115
+ },
116
+ {
117
+ resourceType: "azurerm_storage_account",
118
+ entityTypeCode: "data_store",
119
+ entitySubtypeCode: "store_object",
120
+ role: "data_store",
121
+ attributes: ["name", "location", "resource_group_name", "account_kind", "account_tier", "account_replication_type", "access_tier", "https_traffic_only_enabled", "min_tls_version", "public_network_access_enabled", "allow_nested_items_to_be_public", "shared_access_key_enabled", "primary_blob_endpoint", "primary_location", "secondary_location"],
122
+ canonicalEntityRef: "global:platform_component:azure_storage",
123
+ cloudResourceType: "Microsoft.Storage/storageAccounts",
124
+ },
125
+ {
126
+ resourceType: "azurerm_storage_container",
127
+ entityTypeCode: "data_store",
128
+ entitySubtypeCode: "store_object",
129
+ role: "data_store",
130
+ attributes: ["name", "container_access_type", "storage_account_id", "has_immutability_policy"],
131
+ canonicalEntityRef: "global:platform_component:azure_storage",
132
+ cloudResourceType: "Microsoft.Storage/storageAccounts/blobServices/containers",
133
+ },
134
+ {
135
+ resourceType: "azurerm_cdn_frontdoor_endpoint",
136
+ entityTypeCode: "platform_component",
137
+ entitySubtypeCode: "platform_service",
138
+ role: "edge",
139
+ attributes: ["name", "host_name", "enabled", "cdn_frontdoor_profile_id"],
140
+ references: { "cdn_frontdoor_profile_id": "azurerm_cdn_frontdoor_profile" },
141
+ canonicalEntityRef: "global:platform_component:azure_front_door",
142
+ cloudResourceType: "Microsoft.Cdn/profiles/afdEndpoints",
143
+ },
144
+ {
145
+ resourceType: "azurerm_cdn_frontdoor_firewall_policy",
146
+ entityTypeCode: "policy_control",
147
+ role: "edge",
148
+ attributes: ["name", "location", "resource_group_name", "sku_name", "mode", "enabled"],
149
+ canonicalEntityRef: "global:platform_component:azure_web_application_firewall",
150
+ cloudResourceType: "Microsoft.Network/frontDoorWebApplicationFirewallPolicies",
151
+ },
152
+ {
153
+ resourceType: "azurerm_cdn_frontdoor_profile",
154
+ entityTypeCode: "platform_component",
155
+ entitySubtypeCode: "platform_service",
156
+ role: "edge",
157
+ attributes: ["name", "location", "resource_group_name", "sku_name", "response_timeout_seconds"],
158
+ canonicalEntityRef: "global:platform_component:azure_front_door",
159
+ cloudResourceType: "Microsoft.Cdn/profiles",
160
+ },
161
+ {
162
+ resourceType: "azurerm_static_web_app_custom_domain",
163
+ entityTypeCode: "platform_component",
164
+ entitySubtypeCode: "platform_service",
165
+ role: "edge",
166
+ attributes: ["domain_name", "validation_type"],
167
+ references: { "static_web_app_id": "azurerm_static_web_app" },
168
+ canonicalEntityRef: "global:platform_component:azure_static_web_apps",
169
+ cloudResourceType: "Microsoft.Web/staticSites/customDomains",
170
+ },
171
+ {
172
+ resourceType: "azurerm_traffic_manager_external_endpoint",
173
+ entityTypeCode: "platform_component",
174
+ entitySubtypeCode: "platform_service",
175
+ role: "edge",
176
+ attributes: ["name", "target", "endpoint_location", "enabled", "priority", "weight", "always_serve_enabled"],
177
+ references: { "profile_id": "azurerm_traffic_manager_profile" },
178
+ canonicalEntityRef: "global:platform_component:azure_traffic_manager",
179
+ cloudResourceType: "Microsoft.Network/trafficManagerProfiles/externalEndpoints",
180
+ },
181
+ {
182
+ resourceType: "azurerm_traffic_manager_profile",
183
+ entityTypeCode: "platform_component",
184
+ entitySubtypeCode: "platform_service",
185
+ role: "edge",
186
+ attributes: ["name", "resource_group_name", "fqdn", "traffic_routing_method", "profile_status", "traffic_view_enabled", "max_return"],
187
+ canonicalEntityRef: "global:platform_component:azure_traffic_manager",
188
+ cloudResourceType: "Microsoft.Network/trafficManagerProfiles",
189
+ },
190
+ {
191
+ resourceType: "azurerm_role_assignment",
192
+ entityTypeCode: "policy_control",
193
+ role: "identity",
194
+ attributes: ["name", "principal_id", "principal_type", "role_definition_name", "scope"],
195
+ canonicalEntityRef: "global:platform_component:azure_rbac",
196
+ cloudResourceType: "Microsoft.Authorization/roleAssignments",
197
+ },
198
+ {
199
+ resourceType: "azurerm_user_assigned_identity",
200
+ entityTypeCode: "platform_component",
201
+ entitySubtypeCode: "platform_control_plane",
202
+ role: "identity",
203
+ attributes: ["name", "location", "resource_group_name", "client_id", "principal_id", "tenant_id"],
204
+ canonicalEntityRef: "global:platform_component:azure_managed_identity",
205
+ cloudResourceType: "Microsoft.ManagedIdentity/userAssignedIdentities",
206
+ },
207
+ {
208
+ resourceType: "azurerm_servicebus_namespace",
209
+ entityTypeCode: "platform_component",
210
+ entitySubtypeCode: "platform_service",
211
+ role: "messaging",
212
+ attributes: ["name", "location", "resource_group_name", "sku", "public_network_access_enabled"],
213
+ canonicalEntityRef: "global:platform_component:azure_service_bus",
214
+ cloudResourceType: "Microsoft.ServiceBus/namespaces",
215
+ },
216
+ {
217
+ resourceType: "azurerm_servicebus_queue",
218
+ entityTypeCode: "platform_component",
219
+ entitySubtypeCode: "platform_service",
220
+ role: "messaging",
221
+ attributes: ["name", "namespace_id", "max_delivery_count", "requires_session"],
222
+ references: { "namespace_id": "azurerm_servicebus_namespace" },
223
+ canonicalEntityRef: "global:platform_component:azure_service_bus",
224
+ cloudResourceType: "Microsoft.ServiceBus/namespaces/queues",
225
+ },
226
+ {
227
+ resourceType: "azurerm_private_dns_zone",
228
+ entityTypeCode: "platform_component",
229
+ entitySubtypeCode: "platform_service",
230
+ role: "network_boundary",
231
+ attributes: ["name", "resource_group_name", "number_of_record_sets"],
232
+ canonicalEntityRef: "global:platform_component:azure_private_dns",
233
+ cloudResourceType: "Microsoft.Network/privateDnsZones",
234
+ },
235
+ {
236
+ resourceType: "azurerm_private_endpoint",
237
+ entityTypeCode: "platform_component",
238
+ entitySubtypeCode: "platform_service",
239
+ role: "network_boundary",
240
+ attributes: ["name", "location", "resource_group_name", "subnet_id"],
241
+ references: { "subnet_id": "azurerm_subnet" },
242
+ canonicalEntityRef: "global:platform_component:azure_private_link",
243
+ cloudResourceType: "Microsoft.Network/privateEndpoints",
244
+ },
245
+ {
246
+ resourceType: "azurerm_subnet",
247
+ entityTypeCode: "platform_component",
248
+ entitySubtypeCode: "platform_service",
249
+ role: "network_boundary",
250
+ attributes: ["name", "resource_group_name", "virtual_network_name", "address_prefixes", "default_outbound_access_enabled", "private_endpoint_network_policies"],
251
+ nameReferences: { "virtual_network_name": "azurerm_virtual_network" },
252
+ canonicalEntityRef: "global:platform_component:azure_virtual_network",
253
+ cloudResourceType: "Microsoft.Network/virtualNetworks/subnets",
254
+ },
255
+ {
256
+ resourceType: "azurerm_virtual_network",
257
+ entityTypeCode: "platform_component",
258
+ entitySubtypeCode: "platform_service",
259
+ role: "network_boundary",
260
+ attributes: ["name", "location", "resource_group_name", "address_space", "guid"],
261
+ canonicalEntityRef: "global:platform_component:azure_virtual_network",
262
+ cloudResourceType: "Microsoft.Network/virtualNetworks",
263
+ },
264
+ {
265
+ resourceType: "azurerm_log_analytics_workspace",
266
+ entityTypeCode: "platform_component",
267
+ entitySubtypeCode: "platform_service",
268
+ role: "observability",
269
+ attributes: ["name", "location", "resource_group_name", "sku", "retention_in_days", "workspace_id"],
270
+ canonicalEntityRef: "global:platform_component:azure_monitor",
271
+ cloudResourceType: "Microsoft.OperationalInsights/workspaces",
272
+ },
273
+ {
274
+ resourceType: "azurerm_container_registry",
275
+ entityTypeCode: "platform_component",
276
+ entitySubtypeCode: "platform_service",
277
+ role: "registry",
278
+ attributes: ["name", "location", "resource_group_name", "login_server", "sku", "admin_enabled", "public_network_access_enabled"],
279
+ canonicalEntityRef: "global:platform_component:azure_container_registry",
280
+ cloudResourceType: "Microsoft.ContainerRegistry/registries",
281
+ },
282
+ {
283
+ resourceType: "azurerm_resource_group",
284
+ entityTypeCode: "platform_component",
285
+ entitySubtypeCode: "platform_service",
286
+ role: "resource_group",
287
+ attributes: ["name", "location"],
288
+ canonicalEntityRef: "global:platform_component:azure_resource_group",
289
+ cloudResourceType: "Microsoft.Resources/resourceGroups",
290
+ },
291
+ {
292
+ resourceType: "azurerm_key_vault",
293
+ entityTypeCode: "platform_component",
294
+ entitySubtypeCode: "platform_service",
295
+ role: "secret_store",
296
+ attributes: ["name", "location", "resource_group_name", "vault_uri", "sku_name", "enable_rbac_authorization", "rbac_authorization_enabled", "public_network_access_enabled", "purge_protection_enabled", "soft_delete_retention_days"],
297
+ canonicalEntityRef: "global:platform_component:azure_key_vault",
298
+ cloudResourceType: "Microsoft.KeyVault/vaults",
299
+ },
300
+ ];
@@ -0,0 +1,41 @@
1
+ import { callMcpTool } from "./mcp.js";
2
+ import { IAC_CATALOGUE_SEED } from "./iac-catalogue-seed.js";
3
+ /**
4
+ * Rejects a payload that would widen what may be read.
5
+ *
6
+ * A served catalogue is a network response, and this one decides what leaves
7
+ * the machine. An entry with no attributes is meaningless — it would project a
8
+ * resource stripped of all evidence — and an entry missing its type mapping
9
+ * cannot be applied at all, so neither is accepted quietly. Terraform's own
10
+ * `sensitive_values` gate still runs behind whatever survives here.
11
+ */
12
+ function usableEntries(payload) {
13
+ return (payload.entries ?? []).filter((entry) => typeof entry?.resourceType === "string" &&
14
+ typeof entry?.entityTypeCode === "string" &&
15
+ Array.isArray(entry?.attributes) &&
16
+ entry.attributes.length > 0 &&
17
+ entry.attributes.every((attribute) => typeof attribute === "string"));
18
+ }
19
+ export async function loadIacCatalogue() {
20
+ try {
21
+ const raw = await callMcpTool("nexarch_get_iac_catalogue", {});
22
+ const payload = JSON.parse(raw.content?.[0]?.text ?? "{}");
23
+ const entries = usableEntries(payload);
24
+ // An empty or unparseable response is not a reason to ingest nothing: the
25
+ // bundled copy is known-good and was generated from this same table.
26
+ if (entries.length === 0) {
27
+ return { entries: IAC_CATALOGUE_SEED, source: "bundled", reason: "the platform returned no usable entries" };
28
+ }
29
+ return { entries, source: "platform", reason: null };
30
+ }
31
+ catch (error) {
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ return { entries: IAC_CATALOGUE_SEED, source: "bundled", reason: message };
34
+ }
35
+ }
36
+ /** One line describing which catalogue was used, so a stale ingest is never silent. */
37
+ export function describeCatalogue(result) {
38
+ if (result.source === "platform")
39
+ return `Catalogue: ${result.entries.length} resource types from the platform.`;
40
+ return `Catalogue: ${result.entries.length} resource types from the bundled copy — ${result.reason}.\n Types added to the platform since this CLI was published will not be recognised.`;
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexarch",
3
- "version": "0.12.15",
3
+ "version": "0.12.18",
4
4
  "description": "Your architecture workspace for AI delivery.",
5
5
  "keywords": [
6
6
  "nexarch",