@veltrixsecops/app-sdk 3.2.0 → 3.5.1

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 (77) hide show
  1. package/README.md +68 -1
  2. package/dist/byol/index.cjs +3802 -0
  3. package/dist/byol/index.cjs.map +1 -0
  4. package/dist/byol/index.d.cts +578 -0
  5. package/dist/byol/index.d.ts +578 -0
  6. package/dist/byol/index.js +2587 -0
  7. package/dist/byol/index.js.map +1 -0
  8. package/dist/chunk-3WK4EVQ3.js +842 -0
  9. package/dist/chunk-3WK4EVQ3.js.map +1 -0
  10. package/dist/{chunk-EOOEHZGC.js → chunk-HIL6RIDG.js} +107 -3
  11. package/dist/chunk-HIL6RIDG.js.map +1 -0
  12. package/dist/{chunk-VWFTOFTI.js → chunk-Y6XBNZGR.js} +8 -2
  13. package/dist/chunk-Y6XBNZGR.js.map +1 -0
  14. package/dist/client/index.cjs +112 -2
  15. package/dist/client/index.cjs.map +1 -1
  16. package/dist/client/index.d.cts +118 -6
  17. package/dist/client/index.d.ts +118 -6
  18. package/dist/client/index.js +13 -1
  19. package/dist/connections/index.cjs +1787 -0
  20. package/dist/connections/index.cjs.map +1 -0
  21. package/dist/connections/index.d.cts +78 -0
  22. package/dist/connections/index.d.ts +78 -0
  23. package/dist/connections/index.js +754 -0
  24. package/dist/connections/index.js.map +1 -0
  25. package/dist/hooks/index.cjs.map +1 -1
  26. package/dist/hooks/index.d.cts +2 -2
  27. package/dist/hooks/index.d.ts +2 -2
  28. package/dist/hooks/index.js +1 -1
  29. package/dist/index.cjs +7 -0
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +3 -3
  32. package/dist/index.d.ts +3 -3
  33. package/dist/index.js +3 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/opentofu/index.cjs +138 -0
  36. package/dist/opentofu/index.cjs.map +1 -0
  37. package/dist/opentofu/index.d.cts +131 -0
  38. package/dist/opentofu/index.d.ts +131 -0
  39. package/dist/opentofu/index.js +109 -0
  40. package/dist/opentofu/index.js.map +1 -0
  41. package/dist/pipeline/index.cjs +7 -0
  42. package/dist/pipeline/index.cjs.map +1 -1
  43. package/dist/pipeline/index.d.cts +9 -2
  44. package/dist/pipeline/index.d.ts +9 -2
  45. package/dist/pipeline/index.js +3 -1
  46. package/dist/pipeline-beeT8dje.d.cts +408 -0
  47. package/dist/pipeline-beeT8dje.d.ts +408 -0
  48. package/dist/ui/index.cjs +136 -0
  49. package/dist/ui/index.cjs.map +1 -1
  50. package/dist/ui/index.d.cts +109 -1
  51. package/dist/ui/index.d.ts +109 -1
  52. package/dist/ui/index.js +35 -677
  53. package/dist/ui/index.js.map +1 -1
  54. package/dist/{use-app-context-OQlcmq7t.d.cts → use-app-context-Byv_fam0.d.cts} +206 -4
  55. package/dist/{use-app-context-OQlcmq7t.d.ts → use-app-context-Byv_fam0.d.ts} +206 -4
  56. package/opentofu/README.md +114 -0
  57. package/opentofu/modules/aws/main.tf +1008 -0
  58. package/opentofu/modules/aws/outputs.tf +90 -0
  59. package/opentofu/modules/aws/variables.tf +437 -0
  60. package/opentofu/modules/aws/versions.tf +23 -0
  61. package/opentofu/modules/azure/main.tf +920 -0
  62. package/opentofu/modules/azure/outputs.tf +72 -0
  63. package/opentofu/modules/azure/variables.tf +498 -0
  64. package/opentofu/modules/azure/versions.tf +27 -0
  65. package/opentofu/modules/gcp/main.tf +645 -0
  66. package/opentofu/modules/gcp/outputs.tf +73 -0
  67. package/opentofu/modules/gcp/variables.tf +428 -0
  68. package/opentofu/modules/gcp/versions.tf +27 -0
  69. package/opentofu/modules/hetzner/main.tf +432 -0
  70. package/opentofu/modules/hetzner/outputs.tf +72 -0
  71. package/opentofu/modules/hetzner/variables.tf +423 -0
  72. package/opentofu/modules/hetzner/versions.tf +26 -0
  73. package/package.json +33 -1
  74. package/dist/chunk-EOOEHZGC.js.map +0 -1
  75. package/dist/chunk-VWFTOFTI.js.map +0 -1
  76. package/dist/pipeline-A-gSsPRR.d.cts +0 -189
  77. package/dist/pipeline-A-gSsPRR.d.ts +0 -189
@@ -24,6 +24,20 @@ interface AppManifest {
24
24
  database?: {
25
25
  migrations: string;
26
26
  tablePrefix: string;
27
+ /**
28
+ * How the app's tables are namespaced in Postgres.
29
+ * - 'shared' (default): prefixed tables in the shared `public` schema.
30
+ * Reserved for trusted first-party apps whose SQL the platform ships.
31
+ * - 'schema': the app gets its own Postgres schema (+ least-privilege
32
+ * role); its migrations run with search_path pinned to it.
33
+ * - 'database': the app gets its own Postgres database — hard,
34
+ * cross-database-query-proof isolation.
35
+ * - 'external': the app owns its datastore entirely; the platform manages
36
+ * no schema for it (connection supplied at runtime via app settings).
37
+ * The platform forces at least 'schema' for marketplace and customer-
38
+ * authored (self-managed) apps, which may opt up to 'database'/'external'.
39
+ */
40
+ isolation?: 'shared' | 'schema' | 'database' | 'external';
27
41
  };
28
42
  pipeline: {
29
43
  configurationTypes: AppConfigurationTypeManifest[];
@@ -61,10 +75,101 @@ interface AppManifest {
61
75
  onEnable?: string;
62
76
  onDisable?: string;
63
77
  onUpgrade?: string;
78
+ /** Handler the platform invokes for inbound webhooks routed to this app. */
79
+ onWebhook?: string;
80
+ /** Handler the platform invokes for inbound message-bus events routed to this app. */
81
+ onEvent?: string;
64
82
  };
65
83
  events?: string[];
84
+ /**
85
+ * Connection-level connectivity testing. `testHandler` is an extensionless path
86
+ * to a module whose default export is a `testConnection` handler — the platform
87
+ * runs it in-process (with the decrypted credential) to verify a Connection's
88
+ * endpoint + credentials. Optional: apps without it report "test not supported".
89
+ */
90
+ connectivity?: {
91
+ testHandler?: string;
92
+ };
93
+ /**
94
+ * Connection lifecycle declarations. `onboarding` opts the app into the
95
+ * platform's one-click connection onboarding: the app declares what it needs
96
+ * (a named onboarding adapter + parameters) and the platform drives it.
97
+ * Microsoft Entra admin-consent (`provider: 'entra-admin-consent'`) is the
98
+ * first adapter; nothing here is provider-specific to the platform core.
99
+ */
100
+ connection?: AppConnectionDeclaration;
66
101
  settings?: AppSettingDeclaration[];
67
102
  }
103
+ /** App-declared connection lifecycle capabilities. */
104
+ interface AppConnectionDeclaration {
105
+ onboarding?: ConnectionOnboardingDescriptor;
106
+ }
107
+ /**
108
+ * Declarative "one-click connect" descriptor. The platform reads it to render a
109
+ * "Connect …" button and to drive a named onboarding adapter; the app supplies
110
+ * only data, never platform code.
111
+ */
112
+ interface ConnectionOnboardingDescriptor {
113
+ /** Names a platform onboarding adapter (e.g. `entra-admin-consent`). */
114
+ provider: string;
115
+ /** Button label in the Connections UI (e.g. "Connect Microsoft Defender"). */
116
+ label: string;
117
+ params?: ConnectionOnboardingParams;
118
+ /**
119
+ * Optional app-provided finalize hook (extensionless path). Run in-process
120
+ * after a successful onboarding, exactly like `connectivity.testHandler`.
121
+ */
122
+ onboardingHandler?: string;
123
+ }
124
+ interface ConnectionOnboardingParams {
125
+ /** App-setting key whose value selects the sovereign cloud (e.g. `azure_cloud`). */
126
+ cloudSetting?: string;
127
+ /**
128
+ * App permissions this connection needs — for display + audience selection.
129
+ * The effective grant is fixed on the connector app registration, not here.
130
+ */
131
+ requiredResourceAccess?: OnboardingRequiredResource[];
132
+ /** What the flow captures and where it maps back onto the connection. */
133
+ capture?: OnboardingCapture;
134
+ /** True → the connection uses the platform token broker and stores NO secret. */
135
+ brokered?: boolean;
136
+ /**
137
+ * App settings the admin must supply BEFORE the consent click (they cannot be
138
+ * derived from consent), e.g. Sentinel's subscription/resource-group/workspace.
139
+ */
140
+ requiredSettings?: string[];
141
+ /** Post-consent provisioning steps the adapter runs (e.g. Sentinel ARM RBAC). */
142
+ provisioning?: OnboardingProvisioningStep[];
143
+ }
144
+ interface OnboardingRequiredResource {
145
+ /** Well-known resource name or appId (e.g. `WindowsDefenderATP`, `Graph`). */
146
+ resource: string;
147
+ /** Application permissions requested on that resource (display only). */
148
+ appPermissions: string[];
149
+ }
150
+ interface OnboardingCapture {
151
+ /**
152
+ * Where to write the consented tenant id. `setting:<key>` writes it into the
153
+ * named app setting (the app libs read it as their `tenant_id`).
154
+ */
155
+ tenantId?: string;
156
+ }
157
+ /** A post-consent provisioning step. Only ARM role assignment exists today. */
158
+ interface OnboardingProvisioningStep {
159
+ type: 'arm-role-assignment';
160
+ /** Well-known built-in role name (resolved to a role-definition id by the adapter). */
161
+ role: string;
162
+ /** ARM scope granularity for the assignment. */
163
+ scope: 'resourceGroup' | 'subscription';
164
+ /**
165
+ * How the ARM token for the assignment is obtained:
166
+ * - `manual` (default): show a portal deep-link/CLI + a verify probe. No
167
+ * extra platform privilege — consent does not grant ARM RBAC.
168
+ * - `delegated`: opt-in second delegated-ARM leg (requires the admin to hold
169
+ * Owner / User Access Administrator). Not implemented in the first cut.
170
+ */
171
+ armToken?: 'manual' | 'delegated';
172
+ }
68
173
  /**
69
174
  * App brand identity. The platform renders it in a per-app navbar above the
70
175
  * app's pages and exposes the colors to app pages as scoped CSS variables
@@ -76,9 +181,10 @@ interface AppBrandingDeclaration {
76
181
  /** Optional secondary color as #RGB or #RRGGBB hex. */
77
182
  accentColor?: string;
78
183
  /**
79
- * Vendor logo shown in the app navbar. Repo-relative .svg (preferred) or
80
- * .png, at most 128 KB; rendered at 28px height, so use a wide/landscape
81
- * mark with transparent background.
184
+ * Vendor logo shown in the app navbar and on the marketplace card. Either a
185
+ * repo-relative .svg (preferred) or .png at most 128 KB, OR an absolute
186
+ * https:// URL to an externally hosted asset. Rendered at ~28px height, so
187
+ * use a wide/landscape mark with a transparent background.
82
188
  */
83
189
  logo?: string;
84
190
  /** Optional logo variant for dark backgrounds; same constraints as logo. */
@@ -97,6 +203,12 @@ interface AppConfigurationTypeManifest {
97
203
  healthCheck: string;
98
204
  driftDetect?: string | null;
99
205
  getStatus: string;
206
+ /**
207
+ * Optional options provider — powers live `remote-multiselect` config fields
208
+ * (returns {value,label} options fetched from the target system). Loaded like
209
+ * driftDetect (optional); omit when the config type has no live-option fields.
210
+ */
211
+ options?: string | null;
100
212
  };
101
213
  targets: {
102
214
  componentTypes: string[];
@@ -232,6 +344,12 @@ interface InventoryItem {
232
344
  hostname: string;
233
345
  /** Management/API port, as a string (e.g. "8089"). */
234
346
  port?: string;
347
+ /** Optional secondary service port, e.g. a web UI (Splunk Web "8000"). */
348
+ webPort?: string | null;
349
+ /** Optional OS login user for SSH to this target (e.g. "root", "ubuntu"). */
350
+ sshUser?: string | null;
351
+ /** Optional $SPLUNK_HOME on the target (e.g. "/opt/splunk", "/opt/splunkforwarder"). */
352
+ splunkHome?: string | null;
235
353
  /** Component type tags (e.g. ["server"]) that classify the target. */
236
354
  type?: string[];
237
355
  /** DNS names this target is reachable at. */
@@ -258,6 +376,12 @@ interface InventoryItem {
258
376
  interface InventoryItemInput {
259
377
  hostname: string;
260
378
  port?: string;
379
+ /** Optional secondary service port, e.g. a web UI (Splunk Web "8000"). */
380
+ webPort?: string | null;
381
+ /** Optional OS login user for SSH to this target (e.g. "root", "ubuntu"). */
382
+ sshUser?: string | null;
383
+ /** Optional $SPLUNK_HOME on the target. */
384
+ splunkHome?: string | null;
261
385
  type?: string[];
262
386
  domains?: string[];
263
387
  ipRanges?: string[];
@@ -309,6 +433,8 @@ interface CredentialSummary {
309
433
  type: string | null;
310
434
  /** API base URL / endpoint this connection authenticates to, or null. */
311
435
  endpoint: string | null;
436
+ /** Environment type these creds apply to (from the env's types), or null = any. */
437
+ environmentType: string | null;
312
438
  /** The tool this credential belongs to. */
313
439
  toolId: string;
314
440
  /** True when a write-only secret (apiToken or password) is stored. */
@@ -334,6 +460,8 @@ interface CredentialInput {
334
460
  type?: string;
335
461
  /** API base URL / endpoint this connection authenticates to. Not a secret. */
336
462
  endpoint?: string;
463
+ /** Environment type these creds apply to (from the env's environmentTypes); null = any. */
464
+ environmentType?: string | null;
337
465
  /** The tool this credential belongs to. Required by the platform on create. */
338
466
  toolId?: string;
339
467
  /** IDs of existing platform tags to attach to the credential. */
@@ -416,6 +544,67 @@ interface AppHookContext {
416
544
  appId: string;
417
545
  customerId?: string;
418
546
  }
547
+ /**
548
+ * Context passed to an app's `onWebhook` hook when the platform receives an
549
+ * inbound webhook. The platform stays ignorant of the app's semantics — the
550
+ * app inspects `source`/`event` and decides whether and how to act (e.g. update
551
+ * its own tables via `db`).
552
+ */
553
+ interface AppWebhookContext {
554
+ db: PlatformDatabaseClient;
555
+ appId: string;
556
+ /** Origin of the webhook, e.g. 'github'. */
557
+ source: string;
558
+ /** Event name within the source, e.g. 'deployment'. */
559
+ event: string;
560
+ /** Normalized webhook payload. */
561
+ payload: any;
562
+ }
563
+ /**
564
+ * Context passed to an app's `onEvent` hook when the platform routes a message-
565
+ * bus event to it. Symmetric with `events.publish` (outbound): the app inspects
566
+ * `topic` and acts on its own data.
567
+ */
568
+ interface AppEventContext {
569
+ db: PlatformDatabaseClient;
570
+ appId: string;
571
+ /** Event topic, e.g. 'deployment.status'. */
572
+ topic: string;
573
+ /** Event payload. */
574
+ payload: any;
575
+ }
576
+ /**
577
+ * Publish an event/message from an app to the platform's message bus. The
578
+ * platform routes it on a topic exchange keyed by `<appId>.<topic>`; consumers
579
+ * (provisioning workers, other apps) subscribe by routing key. The platform is
580
+ * ignorant of the payload's meaning — the app owns the message shape.
581
+ */
582
+ interface AppEventPublisher {
583
+ publish(topic: string, payload: unknown): Promise<void>;
584
+ }
585
+ /**
586
+ * A tenant Connection (a stored {@link Credential}) resolved to its DECRYPTED
587
+ * secret + endpoint, returned by {@link AppRouteContext.resolveConnection}.
588
+ *
589
+ * SERVER-SIDE ONLY. The secret fields (`password` / `apiToken` / `certificate`)
590
+ * are in the clear — use them to reach the connected system from a route, and
591
+ * NEVER return them to the client or log them.
592
+ */
593
+ interface ResolvedConnection {
594
+ id: string;
595
+ /** Human-readable connection label. */
596
+ name: string;
597
+ /** API base URL / management endpoint (non-secret), or null. */
598
+ endpoint: string | null;
599
+ /** Account / client id the credential authenticates as. */
600
+ username: string;
601
+ /** Decrypted password (empty string for token-only connections). */
602
+ password: string;
603
+ /** Decrypted API token, or null. */
604
+ apiToken: string | null;
605
+ /** Decrypted client certificate, or null. */
606
+ certificate: string | null;
607
+ }
419
608
  /**
420
609
  * Context passed to an app's server route module (the `server.entry`
421
610
  * declared in manifest.yaml). The platform mounts the module as a Fastify
@@ -427,12 +616,25 @@ interface AppRouteContext {
427
616
  appDir: string;
428
617
  manifest: AppManifest;
429
618
  db: PlatformDatabaseClient;
619
+ /** Publish events/messages from this app to the platform message bus. */
620
+ events: AppEventPublisher;
430
621
  /**
431
622
  * Returns a Fastify preHandler enforcing an app-scoped permission.
432
623
  * Typed `any` so the SDK does not depend on Fastify's hook types —
433
624
  * pass it straight into a route's `preHandler` array.
434
625
  */
435
626
  hasPermission: (resource: string, action: string) => any;
627
+ /**
628
+ * Resolve one of the tenant's Connections (a stored Credential, by id) to its
629
+ * DECRYPTED secret + endpoint, so a route can reach the connected system
630
+ * WITHOUT re-implementing the platform's credential decryption or querying
631
+ * platform tables directly. Scoped to `customerId` (the tenant boundary);
632
+ * returns null when no matching connection exists. The decrypted secret stays
633
+ * server-side — never send it to the client. Pass a `credentialId` the app
634
+ * obtained from its own connections list (e.g. the SDK `listCredentials`
635
+ * helper), mirroring the platform's own "test connection" resolution.
636
+ */
637
+ resolveConnection: (customerId: string, credentialId: string) => Promise<ResolvedConnection | null>;
436
638
  }
437
639
 
438
640
  interface AppContextValue {
@@ -470,4 +672,4 @@ declare const AppContext: Context<AppContextValue | null>;
470
672
  */
471
673
  declare function useAppContext(): AppContextValue;
472
674
 
473
- export { APP_PAGE_LAYOUTS as A, type InventoryItemInput as B, type Component as C, type PermissionEntry as D, type PlatformDatabaseClient as E, AppContext as F, useAppContext as G, type InventoryItem as I, type PermissionCheckOptions as P, type Tag as T, type User as U, APP_PAGE_NAV as a, type AppBrandingDeclaration as b, type AppConfigurationTypeManifest as c, type AppContextValue as d, type AppDetail as e, type AppHookContext as f, type AppInstallationDetail as g, type AppInstallationStatus as h, type AppListItem as i, type AppManifest as j, type AppPageDeclaration as k, type AppPageLayout as l, type AppPageNav as m, type AppPagePermission as n, type AppPermissionDeclaration as o, type AppPermissionsApi as p, type AppRouteContext as q, type AppSettingDeclaration as r, type AppSource as s, type AppStatusType as t, type Connectivity as u, type ConnectivityProviderRef as v, type Credential as w, type CredentialInput as x, type CredentialSummary as y, type Customer as z };
675
+ export { APP_PAGE_LAYOUTS as A, type CredentialInput as B, type Component as C, type CredentialSummary as D, type Customer as E, type InventoryItemInput as F, type PermissionEntry as G, type PlatformDatabaseClient as H, type InventoryItem as I, AppContext as J, useAppContext as K, type PermissionCheckOptions as P, type Tag as T, type User as U, APP_PAGE_NAV as a, type AppBrandingDeclaration as b, type AppConfigurationTypeManifest as c, type AppContextValue as d, type AppDetail as e, type AppEventContext as f, type AppEventPublisher as g, type AppHookContext as h, type AppInstallationDetail as i, type AppInstallationStatus as j, type AppListItem as k, type AppManifest as l, type AppPageDeclaration as m, type AppPageLayout as n, type AppPageNav as o, type AppPagePermission as p, type AppPermissionDeclaration as q, type AppPermissionsApi as r, type AppRouteContext as s, type AppSettingDeclaration as t, type AppSource as u, type AppStatusType as v, type AppWebhookContext as w, type Connectivity as x, type ConnectivityProviderRef as y, type Credential as z };
@@ -0,0 +1,114 @@
1
+ # @veltrixsecops/app-sdk — OpenTofu (generic BYOI provisioning)
2
+
3
+ Tool-agnostic OpenTofu modules that every BYOI app composes to provision a fully
4
+ Veltrix-hosted environment. **There is no per-tool HCL** — an app declares its
5
+ shape as data (an `InfraSpec`) and the SDK renders it into these modules' tfvars.
6
+ Any tool — a log/analytics platform, a sensor fleet, a clustered database, and so
7
+ on — reuses the same modules.
8
+
9
+ ## The three layers
10
+
11
+ ```
12
+ sdk/opentofu/modules/<cloud>/ ← generic HCL (this dir). Nothing tool-specific.
13
+ sdk/src/opentofu/ ← InfraSpec types + renderInfraVars() + validateInfraSpec()
14
+ apps/<app>/infra/spec.ts ← the app's InfraSpec (its ports/LB/DNS/roles as DATA)
15
+ apps/<app>/infra/bringup/ ← the app's config management + readiness gate
16
+ ```
17
+
18
+ The platform's render+apply **worker** (in the Veltrix platform repo) builds the
19
+ base tfvars from the deploy request, merges `renderInfraVars(spec)` on top,
20
+ `tofu apply`s the pinned module, then runs the app's `spec.bringup` entrypoint.
21
+
22
+ ## What the module does (and what the app supplies)
23
+
24
+ | Concern | Module (generic) | App (via InfraSpec) |
25
+ |---|---|---|
26
+ | Compute nodes | one instance per non-foundation plan item | roles are just plan `kind`s |
27
+ | Security groups | SG-to-SG, one rule per `(port, source)` | `securityRules[]` |
28
+ | Front door | ALB + WAF + optional Cognito MFA + target group | `loadBalancer` (port, health path, target kinds) |
29
+ | DNS | public ALB record + private zone + per-node FQDNs | `dnsPrefixes` (kind → label) |
30
+ | Object storage | private S3 bucket | `storage[]` |
31
+ | TLS | ACM public wildcard (ALB); Private-CA per node (fabric) | — |
32
+ | Tags | every resource merges the canonical Veltrix tag set | — |
33
+
34
+ Compute detection is **foundation-exclusion**: any plan item whose `kind` is not
35
+ in `foundation_kinds` (network/storage/secrets/tls/load-balancer/dns/license-file/
36
+ hec) is a compute node — so a new tool's roles are compute automatically.
37
+
38
+ ## Authoring an app's infra (example)
39
+
40
+ ```ts
41
+ // apps/<app>/infra/spec.ts
42
+ import type { InfraSpec } from '@veltrixsecops/app-sdk/opentofu';
43
+
44
+ export const spec: InfraSpec = {
45
+ securityRules: [
46
+ { port: 443, sources: ['alb'], description: 'Web UI' },
47
+ { port: 8089, sources: ['self', 'admin'] },
48
+ ],
49
+ loadBalancer: { targetPort: 443, healthCheckPath: '/healthz', targetKinds: ['web'] },
50
+ dnsPrefixes: { web: 'web', sensor: 'sensor', manager: 'mgr' },
51
+ bringup: './bringup/run.mjs',
52
+ };
53
+ ```
54
+
55
+ Validate it in a test with `validateInfraSpec(spec)` (returns `[]` when valid).
56
+
57
+ ## Deployment modes — Veltrix-hosted vs BYOC
58
+
59
+ The same modules deploy into Veltrix's account *or* the customer's own cloud
60
+ account (Bring Your Own Cloud). This is controlled by two **worker-set**
61
+ variables — they are NOT part of the app's `InfraSpec` (which describes only the
62
+ tool), so one app spec serves both:
63
+
64
+ - **`network_mode`**
65
+ - `shared` — Veltrix-hosted: data-source the shared VPC + the IPAM-allocated
66
+ per-stack subnet.
67
+ - `dedicated` — BYOC: **create** a fresh VPC/VNet/network + multi-AZ
68
+ public/private subnets + gateway + NAT in the customer's account.
69
+ - `existing` — BYOC: data-source a customer-designated network + create
70
+ subnets in it.
71
+ - **`dns_mode`** (private intra-cluster DNS is always created in the deploy
72
+ account's network regardless)
73
+ - `managed` — module creates the public record + cert in-account (Veltrix zone
74
+ for hosted, customer zone for BYOC customer-owned).
75
+ - `delegated` — BYOC cross-account: the **worker** writes the public record +
76
+ cert-validation into Veltrix's zone; the module takes a pre-validated
77
+ `certificate_arn` and creates no public record/cert.
78
+ - `private-only` — no public DNS; reached via the customer's network.
79
+
80
+ **Credentials/trust** are worker-level (the customer registers a least-privilege
81
+ role/SP/SA/token per cloud via Connections; the worker configures the provider
82
+ with it) — the module body is credential-agnostic.
83
+
84
+ ## Contract notes / invariants
85
+
86
+ - **`plan_key` is the status-back key.** Every compute node and foundation tier
87
+ maps `plan_key → cloud ref` via `output "resource_refs"`; keep it 1:1.
88
+ - **`node_fqdns` output** (`plan_key → function FQDN`) is what an app's bring-up
89
+ inventory consumes.
90
+ - A `securityRules` entry using source `"alb"` requires a `loadBalancer`
91
+ (enforced by `validateInfraSpec`).
92
+ - `alb_auth` (Cognito MFA) is off by default; when enabled all three
93
+ `userPool*` fields are required.
94
+ - The module is **pinned + shipped in the package** (`files: ["opentofu"]`);
95
+ the worker resolves it by path from the installed SDK.
96
+
97
+ ## Status
98
+
99
+ All four clouds implement the identical spec-derived variable + output contract
100
+ and both deployment modes (`network_mode`, `dns_mode`). None is `tofu validate`-
101
+ gated yet — no OpenTofu binary in this environment, so they were built by close
102
+ translation of the AWS reference + manual review; **run `tofu validate` per
103
+ module in CI before first apply.**
104
+
105
+ | Cloud | Front door / WAF | SG-to-SG | Private DNS | Notable gaps (documented, no-op) |
106
+ |---|---|---|---|---|
107
+ | **aws** (reference) | ALB + WAFv2 + Cognito MFA | SG self-ref | Route53 private zone | — |
108
+ | **azure** | App Gateway + WAF_v2 | NSG + ASG | Private DNS zone | MFA (→ AAD App Proxy); TLS needs a KV cert + identity (no in-module ACM); `web_ingress_cidr` not wired |
109
+ | **gcp** | Global HTTPS LB + Cloud Armor | network tag + firewall | Cloud DNS private zone | MFA (→ IAP); no HTTP→HTTPS redirect; single-zone compute |
110
+ | **hetzner** | hcloud LB (no WAF) | firewall (CIDR) | **none in-provider** — FQDNs emitted for the bring-up / `hetznerdns` | no WAF; no managed DNS; object store → block volume; secrets → platform vault; no MFA |
111
+
112
+ Tags map to **labels** (sanitized) on GCP and Hetzner; Azure and AWS use tags
113
+ directly. Provider-specific vars (image, sizing, zone, public-zone refs) differ
114
+ per cloud by name; the **spec-derived** vars are identical everywhere.