@veltrixsecops/app-sdk 3.1.0 → 3.5.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 (79) hide show
  1. package/README.md +1 -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-6KUTYJW5.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 +127 -6
  17. package/dist/client/index.d.ts +127 -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 +7 -0
  26. package/dist/hooks/index.cjs.map +1 -1
  27. package/dist/hooks/index.d.cts +24 -3
  28. package/dist/hooks/index.d.ts +24 -3
  29. package/dist/hooks/index.js +7 -1
  30. package/dist/hooks/index.js.map +1 -1
  31. package/dist/index.cjs +7 -0
  32. package/dist/index.cjs.map +1 -1
  33. package/dist/index.d.cts +3 -3
  34. package/dist/index.d.ts +3 -3
  35. package/dist/index.js +3 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/opentofu/index.cjs +138 -0
  38. package/dist/opentofu/index.cjs.map +1 -0
  39. package/dist/opentofu/index.d.cts +131 -0
  40. package/dist/opentofu/index.d.ts +131 -0
  41. package/dist/opentofu/index.js +109 -0
  42. package/dist/opentofu/index.js.map +1 -0
  43. package/dist/pipeline/index.cjs +7 -0
  44. package/dist/pipeline/index.cjs.map +1 -1
  45. package/dist/pipeline/index.d.cts +9 -2
  46. package/dist/pipeline/index.d.ts +9 -2
  47. package/dist/pipeline/index.js +3 -1
  48. package/dist/pipeline-beeT8dje.d.cts +408 -0
  49. package/dist/pipeline-beeT8dje.d.ts +408 -0
  50. package/dist/ui/index.cjs +248 -0
  51. package/dist/ui/index.cjs.map +1 -1
  52. package/dist/ui/index.d.cts +238 -1
  53. package/dist/ui/index.d.ts +238 -1
  54. package/dist/ui/index.js +39 -573
  55. package/dist/ui/index.js.map +1 -1
  56. package/dist/{use-app-context-SEU1tyZd.d.cts → use-app-context-Byv_fam0.d.cts} +241 -4
  57. package/dist/{use-app-context-SEU1tyZd.d.ts → use-app-context-Byv_fam0.d.ts} +241 -4
  58. package/opentofu/README.md +114 -0
  59. package/opentofu/modules/aws/main.tf +1008 -0
  60. package/opentofu/modules/aws/outputs.tf +90 -0
  61. package/opentofu/modules/aws/variables.tf +437 -0
  62. package/opentofu/modules/aws/versions.tf +23 -0
  63. package/opentofu/modules/azure/main.tf +920 -0
  64. package/opentofu/modules/azure/outputs.tf +72 -0
  65. package/opentofu/modules/azure/variables.tf +498 -0
  66. package/opentofu/modules/azure/versions.tf +27 -0
  67. package/opentofu/modules/gcp/main.tf +645 -0
  68. package/opentofu/modules/gcp/outputs.tf +73 -0
  69. package/opentofu/modules/gcp/variables.tf +428 -0
  70. package/opentofu/modules/gcp/versions.tf +27 -0
  71. package/opentofu/modules/hetzner/main.tf +432 -0
  72. package/opentofu/modules/hetzner/outputs.tf +72 -0
  73. package/opentofu/modules/hetzner/variables.tf +423 -0
  74. package/opentofu/modules/hetzner/versions.tf +26 -0
  75. package/package.json +33 -1
  76. package/dist/chunk-6KUTYJW5.js.map +0 -1
  77. package/dist/chunk-VWFTOFTI.js.map +0 -1
  78. package/dist/pipeline-A-gSsPRR.d.cts +0 -189
  79. package/dist/pipeline-A-gSsPRR.d.ts +0 -189
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types/manifest.ts","../src/structure.ts"],"sourcesContent":["// ========================================================================\n// App manifest types\n//\n// Self-contained copy of the platform's manifest contract so the SDK can\n// be published and consumed outside the platform monorepo. The platform\n// keeps its own copy in shared/types/app.ts — changes to the manifest\n// contract must be applied to both files.\n// ========================================================================\n\nexport type AppSource = 'BUILT_IN' | 'MARKETPLACE' | 'CUSTOM'\nexport type AppStatusType = 'AVAILABLE' | 'DEPRECATED' | 'REMOVED'\nexport type AppInstallationStatus =\n | 'INSTALLING'\n | 'INSTALLED'\n | 'ENABLED'\n | 'DISABLED'\n | 'FAILED'\n | 'UNINSTALLING'\n\n// --- Manifest Types (parsed from manifest.yaml) ---\n\nexport interface AppManifest {\n id: string\n name: string\n version: string\n vendor: string\n description: string\n category: string\n license?: string\n homepage?: string\n icon?: string\n logo?: string\n\n platform: {\n minVersion: string\n }\n\n permissions: {\n platform: string[] // Platform permissions the app needs\n app: AppPermissionDeclaration[] // Permissions the app exposes\n }\n\n database?: {\n migrations: string\n tablePrefix: string\n }\n\n pipeline: {\n configurationTypes: AppConfigurationTypeManifest[]\n pipelineEvents?: string[]\n }\n\n server: {\n entry: string\n routes?: {\n prefix: string\n }\n }\n\n client?: {\n entry: string\n pages?: AppPageDeclaration[]\n /**\n * How the platform lays out this app's navigation (its client pages and\n * one entry per configuration type):\n * - 'tabs' (default): a horizontal tab strip — best for a few items.\n * - 'sidebar': an embedded left rail, grouped into Pages and\n * Configurations — scales to many configuration types without the\n * tab strip overflowing.\n */\n navLayout?: 'tabs' | 'sidebar'\n }\n\n /**\n * Vendor brand identity, applied by the platform in defined slots only —\n * the app navbar (logo, accent) and scoped CSS variables. The platform,\n * not the app, decides where brand color appears, so one vendor's palette\n * never overwhelms the product shell.\n */\n branding?: AppBrandingDeclaration\n\n hooks?: {\n onInstall?: string\n onUninstall?: string\n onEnable?: string\n onDisable?: string\n onUpgrade?: string\n }\n\n events?: string[] // Platform events this app subscribes to\n\n settings?: AppSettingDeclaration[]\n}\n\n/**\n * App brand identity. The platform renders it in a per-app navbar above the\n * app's pages and exposes the colors to app pages as scoped CSS variables\n * (--veltrix-app-primary, --veltrix-app-accent).\n */\nexport interface AppBrandingDeclaration {\n /** Brand accent color as #RGB or #RRGGBB hex (e.g. CrowdStrike red). */\n primaryColor?: string\n /** Optional secondary color as #RGB or #RRGGBB hex. */\n accentColor?: string\n /**\n * Vendor logo shown in the app navbar. Repo-relative .svg (preferred) or\n * .png, at most 128 KB; rendered at 28px height, so use a wide/landscape\n * mark with transparent background.\n */\n logo?: string\n /** Optional logo variant for dark backgrounds; same constraints as logo. */\n logoDark?: string\n}\n\nexport interface AppConfigurationTypeManifest {\n id: string\n name: string\n description?: string\n canvasTemplate: string // Path to canvas template YAML\n defaultConfig?: string // Path to default config YAML\n\n handlers: {\n validate: string\n deploy: string\n rollback: string\n healthCheck: string\n driftDetect?: string | null\n getStatus: string\n }\n\n targets: {\n componentTypes: string[]\n requiresCredential: boolean\n requiresConnectivity: boolean\n }\n}\n\nexport interface AppPermissionDeclaration {\n resource: string\n actions: string[]\n description?: string\n}\n\n// --- App UI & navigation contract ---\n//\n// The platform owns the chrome: breadcrumb, app header, navigation, permission\n// gating, error boundary and loading states are rendered identically for every\n// app. Apps own the page body and compose it from @veltrixsecops/ui.\n// Predictable shell, flexible body.\n\n/**\n * How the platform frames an app page.\n * - `standard` — page header + padded content area (default; use for most pages)\n * - `full-bleed` — content area with no padding/toolbar (custom canvases, maps)\n * - `canvas` — Configuration Canvas chrome (section rail + save/validate bar)\n */\nexport type AppPageLayout = 'standard' | 'full-bleed' | 'canvas'\nexport const APP_PAGE_LAYOUTS = ['standard', 'full-bleed', 'canvas'] as const\n\n/**\n * Where the page surfaces in navigation.\n * - `sidebar` — an entry beneath the app in the sidebar\n * - `tab` — a tab within its `parent` page\n * - `hidden` — routable but not linked (details/drill-down pages)\n */\nexport type AppPageNav = 'sidebar' | 'tab' | 'hidden'\nexport const APP_PAGE_NAV = ['sidebar', 'tab', 'hidden'] as const\n\n/** An app-scoped permission (declared in `permissions.app`) required to see a page. */\nexport interface AppPagePermission {\n resource: string\n action: string\n}\n\nexport interface AppPageDeclaration {\n /** Route beneath the app, e.g. `/indexes` → `/apps/<app-id>/indexes` */\n path: string\n /** Exported component name from the app's client entry */\n component: string\n label: string\n description?: string\n /** Icon name from the platform icon set; falls back to the app icon */\n icon?: string\n /** @deprecated use `nav: 'sidebar' | 'hidden'` — kept for backward compatibility */\n sidebar?: boolean\n /** Navigation placement. Defaults to `sidebar` when `sidebar: true`, else `hidden`. */\n nav?: AppPageNav\n /** Parent page `path` — required for `nav: 'tab'`, optional nesting for sidebar entries */\n parent?: string\n /** Optional sidebar section label, for apps with many pages */\n group?: string\n /** Deterministic ordering within its group/parent (ascending; ties break on label) */\n order?: number\n /** Layout preset the platform renders around the page body */\n layout?: AppPageLayout\n /** Hide the page (and its nav entry) unless the user holds this app permission */\n requiresPermission?: AppPagePermission\n}\n\nexport interface AppSettingDeclaration {\n key: string\n type: 'string' | 'number' | 'boolean' | 'select'\n label: string\n description?: string\n default?: string | number | boolean\n required?: boolean\n options?: Array<{ label: string; value: string }>\n}\n\n// --- API Response Types ---\n\nexport interface AppListItem {\n id: string\n appId: string\n name: string\n version: string\n vendor: string\n description: string\n category: string\n icon?: string\n logo?: string\n source: AppSource\n isDefault: boolean\n status: AppStatusType\n installed?: boolean\n enabled?: boolean\n}\n\nexport interface AppDetail extends AppListItem {\n license?: string\n homepage?: string\n repository?: string\n configurationTypes: Array<{\n id: string\n name: string\n description?: string\n componentTypes: string[]\n }>\n permissions: AppPermissionDeclaration[]\n settings: AppSettingDeclaration[]\n}\n\nexport interface AppInstallationDetail {\n id: string\n appId: string\n customerId: string\n version: string\n enabled: boolean\n installedBy: string\n installedAt: string\n settings: Record<string, unknown>\n status: AppInstallationStatus\n app: AppListItem\n}\n","// ========================================================================\n// Canonical app layout\n//\n// Every Veltrix app follows one predictable folder structure so apps are\n// easy to set up, review, and load. The `veltrix` CLI scaffolds it,\n// `veltrix validate` (and repo CI) warn on deviations, and the platform's\n// app engine resolves manifest references against it.\n// ========================================================================\n\n/** Canonical locations inside an app directory. */\nexport const APP_LAYOUT = {\n /** The app contract. Always at the app root. */\n manifest: 'manifest.yaml',\n /**\n * The unit of extension: config-types/<configTypeId>/ colocates everything\n * for one configuration type — canvas.yaml, defaults.yaml, the six pipeline\n * handlers (extensionless in the manifest), and __tests__/.\n */\n configTypesDir: 'config-types',\n /** Canvas form schema filename inside a config-type folder. */\n canvasFile: 'canvas.yaml',\n /** Default field values filename inside a config-type folder. */\n defaultsFile: 'defaults.yaml',\n /** Lifecycle hooks (camelCase): hooks/onInstall, hooks/onUninstall, ... */\n hooksDir: 'hooks',\n /** Shared app code used by multiple handlers (API clients, parsers). */\n libDir: 'lib',\n /** SQL migrations (requires manifest `database.tablePrefix`). */\n migrationsDir: 'migrations',\n /** Fastify route module receiving (fastify, AppRouteContext). */\n serverEntry: 'server/index',\n /** Client entry registering pages/sidebar items (optional). */\n clientEntry: 'client/index',\n /** Icons and logos (optional). */\n assetsDir: 'assets',\n /** Tests live next to the code they cover: handlers/<id>/__tests__/ */\n testsDirName: '__tests__',\n} as const\n\n/** The six pipeline handler names, in lifecycle order. */\nexport const HANDLER_NAMES = [\n 'validate',\n 'deploy',\n 'rollback',\n 'healthCheck',\n 'driftDetect',\n 'getStatus',\n] as const\n\nexport type HandlerName = (typeof HANDLER_NAMES)[number]\n\n/**\n * Conventional manifest paths for one configuration type — handy when\n * generating or checking a manifest programmatically.\n *\n * @example\n * conventionalPaths('indexes').handlers.deploy // 'config-types/indexes/deploy'\n */\nexport function conventionalPaths(configTypeId: string): {\n canvasTemplate: string\n defaultConfig: string\n handlers: Record<HandlerName, string>\n} {\n const base = `${APP_LAYOUT.configTypesDir}/${configTypeId}`\n const handlers = {} as Record<HandlerName, string>\n for (const name of HANDLER_NAMES) {\n handlers[name] = `${base}/${name}`\n }\n return {\n canvasTemplate: `${base}/${APP_LAYOUT.canvasFile}`,\n defaultConfig: `${base}/${APP_LAYOUT.defaultsFile}`,\n handlers,\n }\n}\n"],"mappings":";;;;;;;;;AA6JO,IAAM,mBAAmB,CAAC,YAAY,cAAc,QAAQ;AAS5D,IAAM,eAAe,CAAC,WAAW,OAAO,QAAQ;;;AC5JhD,IAAM,aAAa;AAAA;AAAA,EAExB,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMV,gBAAgB;AAAA;AAAA,EAEhB,YAAY;AAAA;AAAA,EAEZ,cAAc;AAAA;AAAA,EAEd,UAAU;AAAA;AAAA,EAEV,QAAQ;AAAA;AAAA,EAER,eAAe;AAAA;AAAA,EAEf,aAAa;AAAA;AAAA,EAEb,aAAa;AAAA;AAAA,EAEb,WAAW;AAAA;AAAA,EAEX,cAAc;AAChB;AAGO,IAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAWO,SAAS,kBAAkB,cAIhC;AACA,QAAM,OAAO,GAAG,WAAW,cAAc,IAAI,YAAY;AACzD,QAAM,WAAW,CAAC;AAClB,aAAW,QAAQ,eAAe;AAChC,aAAS,IAAI,IAAI,GAAG,IAAI,IAAI,IAAI;AAAA,EAClC;AACA,SAAO;AAAA,IACL,gBAAgB,GAAG,IAAI,IAAI,WAAW,UAAU;AAAA,IAChD,eAAe,GAAG,IAAI,IAAI,WAAW,YAAY;AAAA,IACjD;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/types/manifest.ts","../src/structure.ts"],"sourcesContent":["// ========================================================================\n// App manifest types\n//\n// Self-contained copy of the platform's manifest contract so the SDK can\n// be published and consumed outside the platform monorepo. The platform\n// keeps its own copy in shared/types/app.ts — changes to the manifest\n// contract must be applied to both files.\n// ========================================================================\n\nexport type AppSource = 'BUILT_IN' | 'MARKETPLACE' | 'CUSTOM'\nexport type AppStatusType = 'AVAILABLE' | 'DEPRECATED' | 'REMOVED'\nexport type AppInstallationStatus =\n | 'INSTALLING'\n | 'INSTALLED'\n | 'ENABLED'\n | 'DISABLED'\n | 'FAILED'\n | 'UNINSTALLING'\n\n// --- Manifest Types (parsed from manifest.yaml) ---\n\nexport interface AppManifest {\n id: string\n name: string\n version: string\n vendor: string\n description: string\n category: string\n license?: string\n homepage?: string\n icon?: string\n logo?: string\n\n platform: {\n minVersion: string\n }\n\n permissions: {\n platform: string[] // Platform permissions the app needs\n app: AppPermissionDeclaration[] // Permissions the app exposes\n }\n\n database?: {\n migrations: string\n tablePrefix: string\n /**\n * How the app's tables are namespaced in Postgres.\n * - 'shared' (default): prefixed tables in the shared `public` schema.\n * Reserved for trusted first-party apps whose SQL the platform ships.\n * - 'schema': the app gets its own Postgres schema (+ least-privilege\n * role); its migrations run with search_path pinned to it.\n * - 'database': the app gets its own Postgres database — hard,\n * cross-database-query-proof isolation.\n * - 'external': the app owns its datastore entirely; the platform manages\n * no schema for it (connection supplied at runtime via app settings).\n * The platform forces at least 'schema' for marketplace and customer-\n * authored (self-managed) apps, which may opt up to 'database'/'external'.\n */\n isolation?: 'shared' | 'schema' | 'database' | 'external'\n }\n\n pipeline: {\n configurationTypes: AppConfigurationTypeManifest[]\n pipelineEvents?: string[]\n }\n\n server: {\n entry: string\n routes?: {\n prefix: string\n }\n }\n\n client?: {\n entry: string\n pages?: AppPageDeclaration[]\n /**\n * How the platform lays out this app's navigation (its client pages and\n * one entry per configuration type):\n * - 'tabs' (default): a horizontal tab strip — best for a few items.\n * - 'sidebar': an embedded left rail, grouped into Pages and\n * Configurations — scales to many configuration types without the\n * tab strip overflowing.\n */\n navLayout?: 'tabs' | 'sidebar'\n }\n\n /**\n * Vendor brand identity, applied by the platform in defined slots only —\n * the app navbar (logo, accent) and scoped CSS variables. The platform,\n * not the app, decides where brand color appears, so one vendor's palette\n * never overwhelms the product shell.\n */\n branding?: AppBrandingDeclaration\n\n hooks?: {\n onInstall?: string\n onUninstall?: string\n onEnable?: string\n onDisable?: string\n onUpgrade?: string\n /** Handler the platform invokes for inbound webhooks routed to this app. */\n onWebhook?: string\n /** Handler the platform invokes for inbound message-bus events routed to this app. */\n onEvent?: string\n }\n\n events?: string[] // Platform events this app subscribes to\n\n /**\n * Connection-level connectivity testing. `testHandler` is an extensionless path\n * to a module whose default export is a `testConnection` handler — the platform\n * runs it in-process (with the decrypted credential) to verify a Connection's\n * endpoint + credentials. Optional: apps without it report \"test not supported\".\n */\n connectivity?: {\n testHandler?: string\n }\n\n /**\n * Connection lifecycle declarations. `onboarding` opts the app into the\n * platform's one-click connection onboarding: the app declares what it needs\n * (a named onboarding adapter + parameters) and the platform drives it.\n * Microsoft Entra admin-consent (`provider: 'entra-admin-consent'`) is the\n * first adapter; nothing here is provider-specific to the platform core.\n */\n connection?: AppConnectionDeclaration\n\n settings?: AppSettingDeclaration[]\n}\n\n/** App-declared connection lifecycle capabilities. */\nexport interface AppConnectionDeclaration {\n onboarding?: ConnectionOnboardingDescriptor\n}\n\n/**\n * Declarative \"one-click connect\" descriptor. The platform reads it to render a\n * \"Connect …\" button and to drive a named onboarding adapter; the app supplies\n * only data, never platform code.\n */\nexport interface ConnectionOnboardingDescriptor {\n /** Names a platform onboarding adapter (e.g. `entra-admin-consent`). */\n provider: string\n /** Button label in the Connections UI (e.g. \"Connect Microsoft Defender\"). */\n label: string\n params?: ConnectionOnboardingParams\n /**\n * Optional app-provided finalize hook (extensionless path). Run in-process\n * after a successful onboarding, exactly like `connectivity.testHandler`.\n */\n onboardingHandler?: string\n}\n\nexport interface ConnectionOnboardingParams {\n /** App-setting key whose value selects the sovereign cloud (e.g. `azure_cloud`). */\n cloudSetting?: string\n /**\n * App permissions this connection needs — for display + audience selection.\n * The effective grant is fixed on the connector app registration, not here.\n */\n requiredResourceAccess?: OnboardingRequiredResource[]\n /** What the flow captures and where it maps back onto the connection. */\n capture?: OnboardingCapture\n /** True → the connection uses the platform token broker and stores NO secret. */\n brokered?: boolean\n /**\n * App settings the admin must supply BEFORE the consent click (they cannot be\n * derived from consent), e.g. Sentinel's subscription/resource-group/workspace.\n */\n requiredSettings?: string[]\n /** Post-consent provisioning steps the adapter runs (e.g. Sentinel ARM RBAC). */\n provisioning?: OnboardingProvisioningStep[]\n}\n\nexport interface OnboardingRequiredResource {\n /** Well-known resource name or appId (e.g. `WindowsDefenderATP`, `Graph`). */\n resource: string\n /** Application permissions requested on that resource (display only). */\n appPermissions: string[]\n}\n\nexport interface OnboardingCapture {\n /**\n * Where to write the consented tenant id. `setting:<key>` writes it into the\n * named app setting (the app libs read it as their `tenant_id`).\n */\n tenantId?: string\n}\n\n/** A post-consent provisioning step. Only ARM role assignment exists today. */\nexport interface OnboardingProvisioningStep {\n type: 'arm-role-assignment'\n /** Well-known built-in role name (resolved to a role-definition id by the adapter). */\n role: string\n /** ARM scope granularity for the assignment. */\n scope: 'resourceGroup' | 'subscription'\n /**\n * How the ARM token for the assignment is obtained:\n * - `manual` (default): show a portal deep-link/CLI + a verify probe. No\n * extra platform privilege — consent does not grant ARM RBAC.\n * - `delegated`: opt-in second delegated-ARM leg (requires the admin to hold\n * Owner / User Access Administrator). Not implemented in the first cut.\n */\n armToken?: 'manual' | 'delegated'\n}\n\n/**\n * App brand identity. The platform renders it in a per-app navbar above the\n * app's pages and exposes the colors to app pages as scoped CSS variables\n * (--veltrix-app-primary, --veltrix-app-accent).\n */\nexport interface AppBrandingDeclaration {\n /** Brand accent color as #RGB or #RRGGBB hex (e.g. CrowdStrike red). */\n primaryColor?: string\n /** Optional secondary color as #RGB or #RRGGBB hex. */\n accentColor?: string\n /**\n * Vendor logo shown in the app navbar and on the marketplace card. Either a\n * repo-relative .svg (preferred) or .png at most 128 KB, OR an absolute\n * https:// URL to an externally hosted asset. Rendered at ~28px height, so\n * use a wide/landscape mark with a transparent background.\n */\n logo?: string\n /** Optional logo variant for dark backgrounds; same constraints as logo. */\n logoDark?: string\n}\n\nexport interface AppConfigurationTypeManifest {\n id: string\n name: string\n description?: string\n canvasTemplate: string // Path to canvas template YAML\n defaultConfig?: string // Path to default config YAML\n\n handlers: {\n validate: string\n deploy: string\n rollback: string\n healthCheck: string\n driftDetect?: string | null\n getStatus: string\n /**\n * Optional options provider — powers live `remote-multiselect` config fields\n * (returns {value,label} options fetched from the target system). Loaded like\n * driftDetect (optional); omit when the config type has no live-option fields.\n */\n options?: string | null\n }\n\n targets: {\n componentTypes: string[]\n requiresCredential: boolean\n requiresConnectivity: boolean\n }\n}\n\nexport interface AppPermissionDeclaration {\n resource: string\n actions: string[]\n description?: string\n}\n\n// --- App UI & navigation contract ---\n//\n// The platform owns the chrome: breadcrumb, app header, navigation, permission\n// gating, error boundary and loading states are rendered identically for every\n// app. Apps own the page body and compose it from @veltrixsecops/ui.\n// Predictable shell, flexible body.\n\n/**\n * How the platform frames an app page.\n * - `standard` — page header + padded content area (default; use for most pages)\n * - `full-bleed` — content area with no padding/toolbar (custom canvases, maps)\n * - `canvas` — Configuration Canvas chrome (section rail + save/validate bar)\n */\nexport type AppPageLayout = 'standard' | 'full-bleed' | 'canvas'\nexport const APP_PAGE_LAYOUTS = ['standard', 'full-bleed', 'canvas'] as const\n\n/**\n * Where the page surfaces in navigation.\n * - `sidebar` — an entry beneath the app in the sidebar\n * - `tab` — a tab within its `parent` page\n * - `hidden` — routable but not linked (details/drill-down pages)\n */\nexport type AppPageNav = 'sidebar' | 'tab' | 'hidden'\nexport const APP_PAGE_NAV = ['sidebar', 'tab', 'hidden'] as const\n\n/** An app-scoped permission (declared in `permissions.app`) required to see a page. */\nexport interface AppPagePermission {\n resource: string\n action: string\n}\n\nexport interface AppPageDeclaration {\n /** Route beneath the app, e.g. `/indexes` → `/apps/<app-id>/indexes` */\n path: string\n /** Exported component name from the app's client entry */\n component: string\n label: string\n description?: string\n /** Icon name from the platform icon set; falls back to the app icon */\n icon?: string\n /** @deprecated use `nav: 'sidebar' | 'hidden'` — kept for backward compatibility */\n sidebar?: boolean\n /** Navigation placement. Defaults to `sidebar` when `sidebar: true`, else `hidden`. */\n nav?: AppPageNav\n /** Parent page `path` — required for `nav: 'tab'`, optional nesting for sidebar entries */\n parent?: string\n /** Optional sidebar section label, for apps with many pages */\n group?: string\n /** Deterministic ordering within its group/parent (ascending; ties break on label) */\n order?: number\n /** Layout preset the platform renders around the page body */\n layout?: AppPageLayout\n /** Hide the page (and its nav entry) unless the user holds this app permission */\n requiresPermission?: AppPagePermission\n}\n\nexport interface AppSettingDeclaration {\n key: string\n type: 'string' | 'number' | 'boolean' | 'select'\n label: string\n description?: string\n default?: string | number | boolean\n required?: boolean\n options?: Array<{ label: string; value: string }>\n}\n\n// --- API Response Types ---\n\nexport interface AppListItem {\n id: string\n appId: string\n name: string\n version: string\n vendor: string\n description: string\n category: string\n icon?: string\n logo?: string\n source: AppSource\n isDefault: boolean\n status: AppStatusType\n installed?: boolean\n enabled?: boolean\n}\n\nexport interface AppDetail extends AppListItem {\n license?: string\n homepage?: string\n repository?: string\n configurationTypes: Array<{\n id: string\n name: string\n description?: string\n componentTypes: string[]\n }>\n permissions: AppPermissionDeclaration[]\n settings: AppSettingDeclaration[]\n}\n\nexport interface AppInstallationDetail {\n id: string\n appId: string\n customerId: string\n version: string\n enabled: boolean\n installedBy: string\n installedAt: string\n settings: Record<string, unknown>\n status: AppInstallationStatus\n app: AppListItem\n}\n","// ========================================================================\n// Canonical app layout\n//\n// Every Veltrix app follows one predictable folder structure so apps are\n// easy to set up, review, and load. The `veltrix` CLI scaffolds it,\n// `veltrix validate` (and repo CI) warn on deviations, and the platform's\n// app engine resolves manifest references against it.\n// ========================================================================\n\n/** Canonical locations inside an app directory. */\nexport const APP_LAYOUT = {\n /** The app contract. Always at the app root. */\n manifest: 'manifest.yaml',\n /**\n * The unit of extension: config-types/<configTypeId>/ colocates everything\n * for one configuration type — canvas.yaml, defaults.yaml, the six pipeline\n * handlers (extensionless in the manifest), and __tests__/.\n */\n configTypesDir: 'config-types',\n /** Canvas form schema filename inside a config-type folder. */\n canvasFile: 'canvas.yaml',\n /** Default field values filename inside a config-type folder. */\n defaultsFile: 'defaults.yaml',\n /** Lifecycle hooks (camelCase): hooks/onInstall, hooks/onUninstall, ... */\n hooksDir: 'hooks',\n /** Shared app code used by multiple handlers (API clients, parsers). */\n libDir: 'lib',\n /** SQL migrations (requires manifest `database.tablePrefix`). */\n migrationsDir: 'migrations',\n /** Fastify route module receiving (fastify, AppRouteContext). */\n serverEntry: 'server/index',\n /** Client entry registering pages/sidebar items (optional). */\n clientEntry: 'client/index',\n /** Icons and logos (optional). */\n assetsDir: 'assets',\n /** Tests live next to the code they cover: handlers/<id>/__tests__/ */\n testsDirName: '__tests__',\n} as const\n\n/** The six pipeline handler names, in lifecycle order. */\nexport const HANDLER_NAMES = [\n 'validate',\n 'deploy',\n 'rollback',\n 'healthCheck',\n 'driftDetect',\n 'getStatus',\n] as const\n\nexport type HandlerName = (typeof HANDLER_NAMES)[number]\n\n/**\n * Conventional manifest paths for one configuration type — handy when\n * generating or checking a manifest programmatically.\n *\n * @example\n * conventionalPaths('indexes').handlers.deploy // 'config-types/indexes/deploy'\n */\nexport function conventionalPaths(configTypeId: string): {\n canvasTemplate: string\n defaultConfig: string\n handlers: Record<HandlerName, string>\n} {\n const base = `${APP_LAYOUT.configTypesDir}/${configTypeId}`\n const handlers = {} as Record<HandlerName, string>\n for (const name of HANDLER_NAMES) {\n handlers[name] = `${base}/${name}`\n }\n return {\n canvasTemplate: `${base}/${APP_LAYOUT.canvasFile}`,\n defaultConfig: `${base}/${APP_LAYOUT.defaultsFile}`,\n handlers,\n }\n}\n"],"mappings":";;;;;;;;;;AAqRO,IAAM,mBAAmB,CAAC,YAAY,cAAc,QAAQ;AAS5D,IAAM,eAAe,CAAC,WAAW,OAAO,QAAQ;;;ACpRhD,IAAM,aAAa;AAAA;AAAA,EAExB,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMV,gBAAgB;AAAA;AAAA,EAEhB,YAAY;AAAA;AAAA,EAEZ,cAAc;AAAA;AAAA,EAEd,UAAU;AAAA;AAAA,EAEV,QAAQ;AAAA;AAAA,EAER,eAAe;AAAA;AAAA,EAEf,aAAa;AAAA;AAAA,EAEb,aAAa;AAAA;AAAA,EAEb,WAAW;AAAA;AAAA,EAEX,cAAc;AAChB;AAGO,IAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAWO,SAAS,kBAAkB,cAIhC;AACA,QAAM,OAAO,GAAG,WAAW,cAAc,IAAI,YAAY;AACzD,QAAM,WAAW,CAAC;AAClB,aAAW,QAAQ,eAAe;AAChC,aAAS,IAAI,IAAI,GAAG,IAAI,IAAI,IAAI;AAAA,EAClC;AACA,SAAO;AAAA,IACL,gBAAgB,GAAG,IAAI,IAAI,WAAW,UAAU;AAAA,IAChD,eAAe,GAAG,IAAI,IAAI,WAAW,YAAY;AAAA,IACjD;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/opentofu/index.ts
21
+ var opentofu_exports = {};
22
+ __export(opentofu_exports, {
23
+ FOUNDATION_KINDS: () => FOUNDATION_KINDS,
24
+ renderInfraVars: () => renderInfraVars,
25
+ validateInfraSpec: () => validateInfraSpec
26
+ });
27
+ module.exports = __toCommonJS(opentofu_exports);
28
+
29
+ // src/opentofu/spec.ts
30
+ var FOUNDATION_KINDS = [
31
+ "network",
32
+ "storage",
33
+ "secrets",
34
+ "tls",
35
+ "load-balancer",
36
+ "dns",
37
+ "license-file",
38
+ "hec"
39
+ ];
40
+
41
+ // src/opentofu/render.ts
42
+ function renderInfraVars(spec) {
43
+ const security_rules = (spec.securityRules ?? []).map((r) => ({
44
+ port: r.port,
45
+ protocol: r.protocol ?? "tcp",
46
+ sources: [...r.sources],
47
+ description: r.description ?? ""
48
+ }));
49
+ const load_balancer = spec.loadBalancer ? {
50
+ target_port: spec.loadBalancer.targetPort,
51
+ target_protocol: spec.loadBalancer.targetProtocol ?? "HTTP",
52
+ health_check_path: spec.loadBalancer.healthCheckPath,
53
+ health_check_matcher: spec.loadBalancer.healthCheckMatcher ?? "200-399",
54
+ health_check_protocol: spec.loadBalancer.healthCheckProtocol ?? "",
55
+ target_kinds: [...spec.loadBalancer.targetKinds],
56
+ listener_port: spec.loadBalancer.listenerPort ?? 443
57
+ } : null;
58
+ const waf_enabled = spec.waf ?? Boolean(spec.loadBalancer);
59
+ const alb_auth = {
60
+ enabled: spec.albAuth?.enabled ?? false,
61
+ user_pool_arn: spec.albAuth?.userPoolArn ?? "",
62
+ user_pool_client_id: spec.albAuth?.userPoolClientId ?? "",
63
+ user_pool_domain: spec.albAuth?.userPoolDomain ?? ""
64
+ };
65
+ return {
66
+ compute_kinds: spec.computeKinds ? [...spec.computeKinds] : [],
67
+ security_rules,
68
+ load_balancer,
69
+ dns_prefixes: { ...spec.dnsPrefixes },
70
+ waf_enabled,
71
+ alb_auth
72
+ };
73
+ }
74
+
75
+ // src/opentofu/validate.ts
76
+ function validateInfraSpec(spec) {
77
+ const errors = [];
78
+ if (!Array.isArray(spec.securityRules) || spec.securityRules.length === 0) {
79
+ errors.push("securityRules must contain at least one rule.");
80
+ }
81
+ const validSources = /* @__PURE__ */ new Set(["self", "alb", "admin"]);
82
+ const usesAlbSource = (spec.securityRules ?? []).some(
83
+ (r) => (r.sources ?? []).includes("alb")
84
+ );
85
+ for (const r of spec.securityRules ?? []) {
86
+ if (typeof r.port !== "number" || r.port < 1 || r.port > 65535) {
87
+ errors.push(`securityRules: invalid port ${JSON.stringify(r.port)}.`);
88
+ }
89
+ if (!Array.isArray(r.sources) || r.sources.length === 0) {
90
+ errors.push(`securityRules[port ${r.port}]: sources must be non-empty.`);
91
+ }
92
+ for (const s of r.sources ?? []) {
93
+ if (!validSources.has(s)) {
94
+ errors.push(`securityRules[port ${r.port}]: unknown source "${s}" (expected self|alb|admin).`);
95
+ }
96
+ }
97
+ }
98
+ if (usesAlbSource && !spec.loadBalancer) {
99
+ errors.push('a securityRules entry uses source "alb" but no loadBalancer is declared.');
100
+ }
101
+ if (spec.loadBalancer) {
102
+ const lb = spec.loadBalancer;
103
+ if (typeof lb.targetPort !== "number") {
104
+ errors.push("loadBalancer.targetPort is required.");
105
+ }
106
+ if (!lb.healthCheckPath || !lb.healthCheckPath.startsWith("/")) {
107
+ errors.push('loadBalancer.healthCheckPath must be an absolute path (start with "/").');
108
+ }
109
+ if (!Array.isArray(lb.targetKinds) || lb.targetKinds.length === 0) {
110
+ errors.push("loadBalancer.targetKinds must name at least one compute kind.");
111
+ }
112
+ const targetPortOpen = (spec.securityRules ?? []).some(
113
+ (r) => r.port === lb.targetPort && (r.sources ?? []).includes("alb")
114
+ );
115
+ if (!targetPortOpen) {
116
+ errors.push(
117
+ `loadBalancer.targetPort ${lb.targetPort} is not opened to source "alb" in securityRules \u2014 the ALB can't reach the web tier.`
118
+ );
119
+ }
120
+ }
121
+ if (spec.albAuth?.enabled) {
122
+ const a = spec.albAuth;
123
+ if (!a.userPoolArn || !a.userPoolClientId || !a.userPoolDomain) {
124
+ errors.push("albAuth.enabled requires userPoolArn, userPoolClientId and userPoolDomain.");
125
+ }
126
+ if (!spec.loadBalancer) {
127
+ errors.push("albAuth is set but there is no loadBalancer to attach it to.");
128
+ }
129
+ }
130
+ return errors;
131
+ }
132
+ // Annotate the CommonJS export names for ESM import in node:
133
+ 0 && (module.exports = {
134
+ FOUNDATION_KINDS,
135
+ renderInfraVars,
136
+ validateInfraSpec
137
+ });
138
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/opentofu/index.ts","../../src/opentofu/spec.ts","../../src/opentofu/render.ts","../../src/opentofu/validate.ts"],"sourcesContent":["// =============================================================================\n// @veltrixsecops/app-sdk/opentofu\n//\n// Declarative, tool-agnostic infrastructure for BYOI apps. An app ships an\n// InfraSpec (apps/<app>/infra/spec.ts); the SDK renders it into the generic\n// OpenTofu module's tfvars (sdk/opentofu/modules/<cloud>); the platform worker\n// applies it and runs the app's bring-up. No per-tool HCL, ever.\n// =============================================================================\n\nexport type {\n InfraSpec,\n SecurityRule,\n SecuritySource,\n LoadBalancerSpec,\n StorageSpec,\n AlbAuthSpec,\n FoundationKind,\n} from './spec';\nexport { FOUNDATION_KINDS } from './spec';\n\nexport type {\n AwsInfraVars,\n AwsLoadBalancerVars,\n AwsSecurityRuleVars,\n AwsAlbAuthVars,\n} from './render';\nexport { renderInfraVars } from './render';\n\nexport { validateInfraSpec } from './validate';\n","// =============================================================================\n// InfraSpec — the declarative, tool-agnostic infrastructure contract.\n//\n// An app ships one of these in `apps/<app>/infra/spec.ts`. The SDK renders it\n// into the generic OpenTofu module's tfvars (see ./render). NOTHING in the\n// module is tool-specific: an app supplies all of its shape (roles, ports,\n// front-door, DNS, bring-up) as DATA here, so the same modules provision Splunk,\n// Security Onion, or any other BYOI tool without a line of per-tool HCL.\n//\n// Pairs with sdk/opentofu/modules/<cloud> (the generic HCL) and the platform\n// render+apply worker, which invokes the app's `bringup` entrypoint after apply.\n// =============================================================================\n\n/** Where a security-group ingress rule may allow traffic from. */\nexport type SecuritySource =\n | 'self' // peer nodes in this stack (the node SG referencing itself)\n | 'alb' // the public ALB security group (only valid when loadBalancer is set)\n | 'admin'; // the Veltrix control-plane / admin CIDR (var.admin_cidr)\n\n/** One security-group ingress rule: a port and who may reach it. */\nexport interface SecurityRule {\n /** Destination port to open on the compute nodes. */\n port: number;\n /** IP protocol. Default 'tcp'. */\n protocol?: 'tcp' | 'udp';\n /** Origins allowed to reach `port`. At least one. */\n sources: SecuritySource[];\n /** Human-readable description (surfaced on the rule). */\n description?: string;\n}\n\n/** Front-door ALB spec. Omit entirely for forwarder-only / headless tools. */\nexport interface LoadBalancerSpec {\n /** Port the tool's web UI listens on behind the ALB (e.g. Splunk Web 8000). */\n targetPort: number;\n /** ALB→instance protocol. Default 'HTTP' (TLS terminates at the ALB). */\n targetProtocol?: 'HTTP' | 'HTTPS';\n /** Health-check path on the target (e.g. '/en-US/account/login'). */\n healthCheckPath: string;\n /** Health-check success matcher. Default '200-399'. */\n healthCheckMatcher?: string;\n /** Health-check protocol. Default = targetProtocol. */\n healthCheckProtocol?: 'HTTP' | 'HTTPS';\n /** Compute kinds that sit behind the ALB (e.g. ['search-head','standalone']). */\n targetKinds: string[];\n /** Public HTTPS listener port. Default 443. */\n listenerPort?: number;\n}\n\n/** An object-storage bucket the tool needs (e.g. Splunk SmartStore, frozen). */\nexport interface StorageSpec {\n /** Logical suffix for the bucket name (e.g. 'smartstore'). */\n name: string;\n}\n\n/** Optional Cognito/OIDC MFA enforced at the ALB, in front of the tool's UI. */\nexport interface AlbAuthSpec {\n enabled: boolean;\n userPoolArn?: string;\n userPoolClientId?: string;\n userPoolDomain?: string;\n}\n\n/**\n * The complete declarative infra spec for one app. Everything tool-shaped is a\n * value here; the generic module reads these via tfvars.\n */\nexport interface InfraSpec {\n /**\n * Explicit compute kinds. When omitted, ANY plan item whose `kind` is not a\n * generic FOUNDATION kind (see FOUNDATION_KINDS) is treated as compute — so an\n * app's roles become compute automatically without listing them here.\n */\n computeKinds?: string[];\n\n /** SG ingress rules: the tool's ports and who may reach each. */\n securityRules: SecurityRule[];\n\n /** ALB front-door. Omit for tools with no web UI. */\n loadBalancer?: LoadBalancerSpec;\n\n /**\n * kind → DNS label prefix for per-node function FQDNs\n * (e.g. { indexer: 'idx', 'search-head': 'sh', 'cluster-manager': 'mgmt' }).\n * A kind absent from the map falls back to the kind string itself.\n */\n dnsPrefixes: Record<string, string>;\n\n /** Object-storage buckets to create. Omit if the tool needs none. */\n storage?: StorageSpec[];\n\n /**\n * Attach a WAFv2 web ACL (AWS-managed rule sets + IP rate limit) to the ALB.\n * Defaults to true when `loadBalancer` is set; ignored otherwise.\n */\n waf?: boolean;\n\n /** Optional ALB Cognito MFA. */\n albAuth?: AlbAuthSpec;\n\n /**\n * Path, relative to the app's `infra/` dir, to the bring-up entrypoint the\n * generic worker runs after `tofu apply` succeeds (config management +\n * readiness gate). ALL tool-specific configuration lives behind this — e.g.\n * './bringup/ansible/site.yml' for Splunk. Omit for tools that are ready at\n * boot (no post-provision configuration).\n */\n bringup?: string;\n}\n\n/**\n * Generic foundation kinds the module realizes as shared infra (not compute).\n * Any plan `kind` NOT in this set is compute. Kept in sync with the module's\n * `var.foundation_kinds` default.\n */\nexport const FOUNDATION_KINDS = [\n 'network',\n 'storage',\n 'secrets',\n 'tls',\n 'load-balancer',\n 'dns',\n 'license-file',\n 'hec',\n] as const;\n\nexport type FoundationKind = (typeof FOUNDATION_KINDS)[number];\n","// =============================================================================\n// render — turn an app's declarative InfraSpec into the generic OpenTofu\n// module's variable values (tfvars).\n//\n// The platform render+apply worker builds the BASE vars (app_id, customer_id,\n// plan, subnet_cidr, tags, region, ...) from the DeployRequest, and merges these\n// SPEC-derived vars on top. Field names are snake_case to match the HCL module\n// variables 1:1. Defaults mirror the module's `optional(...)` defaults, applied\n// here so the emitted tfvars are explicit and self-describing.\n// =============================================================================\n\nimport type { InfraSpec } from './spec';\n\n/** ALB spec as the module expects it (snake_case). */\nexport interface AwsLoadBalancerVars {\n target_port: number;\n target_protocol: 'HTTP' | 'HTTPS';\n health_check_path: string;\n health_check_matcher: string;\n health_check_protocol: string; // \"\" => module falls back to target_protocol\n target_kinds: string[];\n listener_port: number;\n}\n\n/** One rendered SG ingress rule (snake_case). */\nexport interface AwsSecurityRuleVars {\n port: number;\n protocol: 'tcp' | 'udp';\n sources: string[];\n description: string;\n}\n\n/** Cognito MFA vars (snake_case), always present so `var.alb_auth.enabled` resolves. */\nexport interface AwsAlbAuthVars {\n enabled: boolean;\n user_pool_arn: string;\n user_pool_client_id: string;\n user_pool_domain: string;\n}\n\n/** The spec-derived subset of the generic AWS module's variables. */\nexport interface AwsInfraVars {\n compute_kinds: string[];\n security_rules: AwsSecurityRuleVars[];\n load_balancer: AwsLoadBalancerVars | null;\n dns_prefixes: Record<string, string>;\n waf_enabled: boolean;\n alb_auth: AwsAlbAuthVars;\n}\n\n/**\n * Render the spec-derived tfvars for the generic AWS module. Pure and\n * deterministic — no I/O, no clock, no randomness.\n */\nexport function renderInfraVars(spec: InfraSpec): AwsInfraVars {\n const security_rules: AwsSecurityRuleVars[] = (spec.securityRules ?? []).map((r) => ({\n port: r.port,\n protocol: r.protocol ?? 'tcp',\n sources: [...r.sources],\n description: r.description ?? '',\n }));\n\n const load_balancer: AwsLoadBalancerVars | null = spec.loadBalancer\n ? {\n target_port: spec.loadBalancer.targetPort,\n target_protocol: spec.loadBalancer.targetProtocol ?? 'HTTP',\n health_check_path: spec.loadBalancer.healthCheckPath,\n health_check_matcher: spec.loadBalancer.healthCheckMatcher ?? '200-399',\n health_check_protocol: spec.loadBalancer.healthCheckProtocol ?? '',\n target_kinds: [...spec.loadBalancer.targetKinds],\n listener_port: spec.loadBalancer.listenerPort ?? 443,\n }\n : null;\n\n // WAF defaults ON when there is a load balancer, OFF otherwise (the module\n // ignores it without an ALB, but keep the emitted value honest).\n const waf_enabled = spec.waf ?? Boolean(spec.loadBalancer);\n\n const alb_auth: AwsAlbAuthVars = {\n enabled: spec.albAuth?.enabled ?? false,\n user_pool_arn: spec.albAuth?.userPoolArn ?? '',\n user_pool_client_id: spec.albAuth?.userPoolClientId ?? '',\n user_pool_domain: spec.albAuth?.userPoolDomain ?? '',\n };\n\n return {\n compute_kinds: spec.computeKinds ? [...spec.computeKinds] : [],\n security_rules,\n load_balancer,\n dns_prefixes: { ...spec.dnsPrefixes },\n waf_enabled,\n alb_auth,\n };\n}\n","// =============================================================================\n// validateInfraSpec — cheap, dependency-free checks that catch the InfraSpec\n// mistakes the OpenTofu module can't (or would only surface at apply time).\n// Returns a list of human-readable errors; empty === valid.\n// =============================================================================\n\nimport type { InfraSpec } from './spec';\n\nexport function validateInfraSpec(spec: InfraSpec): string[] {\n const errors: string[] = [];\n\n if (!Array.isArray(spec.securityRules) || spec.securityRules.length === 0) {\n errors.push('securityRules must contain at least one rule.');\n }\n\n const validSources = new Set(['self', 'alb', 'admin']);\n const usesAlbSource = (spec.securityRules ?? []).some((r) =>\n (r.sources ?? []).includes('alb'),\n );\n\n for (const r of spec.securityRules ?? []) {\n if (typeof r.port !== 'number' || r.port < 1 || r.port > 65535) {\n errors.push(`securityRules: invalid port ${JSON.stringify(r.port)}.`);\n }\n if (!Array.isArray(r.sources) || r.sources.length === 0) {\n errors.push(`securityRules[port ${r.port}]: sources must be non-empty.`);\n }\n for (const s of r.sources ?? []) {\n if (!validSources.has(s)) {\n errors.push(`securityRules[port ${r.port}]: unknown source \"${s}\" (expected self|alb|admin).`);\n }\n }\n }\n\n // \"alb\" sources are meaningless without a load balancer.\n if (usesAlbSource && !spec.loadBalancer) {\n errors.push('a securityRules entry uses source \"alb\" but no loadBalancer is declared.');\n }\n\n if (spec.loadBalancer) {\n const lb = spec.loadBalancer;\n if (typeof lb.targetPort !== 'number') {\n errors.push('loadBalancer.targetPort is required.');\n }\n if (!lb.healthCheckPath || !lb.healthCheckPath.startsWith('/')) {\n errors.push('loadBalancer.healthCheckPath must be an absolute path (start with \"/\").');\n }\n if (!Array.isArray(lb.targetKinds) || lb.targetKinds.length === 0) {\n errors.push('loadBalancer.targetKinds must name at least one compute kind.');\n }\n // The web tier must be reachable from the ALB.\n const targetPortOpen = (spec.securityRules ?? []).some(\n (r) => r.port === lb.targetPort && (r.sources ?? []).includes('alb'),\n );\n if (!targetPortOpen) {\n errors.push(\n `loadBalancer.targetPort ${lb.targetPort} is not opened to source \"alb\" in securityRules — the ALB can't reach the web tier.`,\n );\n }\n }\n\n if (spec.albAuth?.enabled) {\n const a = spec.albAuth;\n if (!a.userPoolArn || !a.userPoolClientId || !a.userPoolDomain) {\n errors.push('albAuth.enabled requires userPoolArn, userPoolClientId and userPoolDomain.');\n }\n if (!spec.loadBalancer) {\n errors.push('albAuth is set but there is no loadBalancer to attach it to.');\n }\n }\n\n return errors;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,131 @@
1
+ /** Where a security-group ingress rule may allow traffic from. */
2
+ type SecuritySource = 'self' | 'alb' | 'admin';
3
+ /** One security-group ingress rule: a port and who may reach it. */
4
+ interface SecurityRule {
5
+ /** Destination port to open on the compute nodes. */
6
+ port: number;
7
+ /** IP protocol. Default 'tcp'. */
8
+ protocol?: 'tcp' | 'udp';
9
+ /** Origins allowed to reach `port`. At least one. */
10
+ sources: SecuritySource[];
11
+ /** Human-readable description (surfaced on the rule). */
12
+ description?: string;
13
+ }
14
+ /** Front-door ALB spec. Omit entirely for forwarder-only / headless tools. */
15
+ interface LoadBalancerSpec {
16
+ /** Port the tool's web UI listens on behind the ALB (e.g. Splunk Web 8000). */
17
+ targetPort: number;
18
+ /** ALB→instance protocol. Default 'HTTP' (TLS terminates at the ALB). */
19
+ targetProtocol?: 'HTTP' | 'HTTPS';
20
+ /** Health-check path on the target (e.g. '/en-US/account/login'). */
21
+ healthCheckPath: string;
22
+ /** Health-check success matcher. Default '200-399'. */
23
+ healthCheckMatcher?: string;
24
+ /** Health-check protocol. Default = targetProtocol. */
25
+ healthCheckProtocol?: 'HTTP' | 'HTTPS';
26
+ /** Compute kinds that sit behind the ALB (e.g. ['search-head','standalone']). */
27
+ targetKinds: string[];
28
+ /** Public HTTPS listener port. Default 443. */
29
+ listenerPort?: number;
30
+ }
31
+ /** An object-storage bucket the tool needs (e.g. Splunk SmartStore, frozen). */
32
+ interface StorageSpec {
33
+ /** Logical suffix for the bucket name (e.g. 'smartstore'). */
34
+ name: string;
35
+ }
36
+ /** Optional Cognito/OIDC MFA enforced at the ALB, in front of the tool's UI. */
37
+ interface AlbAuthSpec {
38
+ enabled: boolean;
39
+ userPoolArn?: string;
40
+ userPoolClientId?: string;
41
+ userPoolDomain?: string;
42
+ }
43
+ /**
44
+ * The complete declarative infra spec for one app. Everything tool-shaped is a
45
+ * value here; the generic module reads these via tfvars.
46
+ */
47
+ interface InfraSpec {
48
+ /**
49
+ * Explicit compute kinds. When omitted, ANY plan item whose `kind` is not a
50
+ * generic FOUNDATION kind (see FOUNDATION_KINDS) is treated as compute — so an
51
+ * app's roles become compute automatically without listing them here.
52
+ */
53
+ computeKinds?: string[];
54
+ /** SG ingress rules: the tool's ports and who may reach each. */
55
+ securityRules: SecurityRule[];
56
+ /** ALB front-door. Omit for tools with no web UI. */
57
+ loadBalancer?: LoadBalancerSpec;
58
+ /**
59
+ * kind → DNS label prefix for per-node function FQDNs
60
+ * (e.g. { indexer: 'idx', 'search-head': 'sh', 'cluster-manager': 'mgmt' }).
61
+ * A kind absent from the map falls back to the kind string itself.
62
+ */
63
+ dnsPrefixes: Record<string, string>;
64
+ /** Object-storage buckets to create. Omit if the tool needs none. */
65
+ storage?: StorageSpec[];
66
+ /**
67
+ * Attach a WAFv2 web ACL (AWS-managed rule sets + IP rate limit) to the ALB.
68
+ * Defaults to true when `loadBalancer` is set; ignored otherwise.
69
+ */
70
+ waf?: boolean;
71
+ /** Optional ALB Cognito MFA. */
72
+ albAuth?: AlbAuthSpec;
73
+ /**
74
+ * Path, relative to the app's `infra/` dir, to the bring-up entrypoint the
75
+ * generic worker runs after `tofu apply` succeeds (config management +
76
+ * readiness gate). ALL tool-specific configuration lives behind this — e.g.
77
+ * './bringup/ansible/site.yml' for Splunk. Omit for tools that are ready at
78
+ * boot (no post-provision configuration).
79
+ */
80
+ bringup?: string;
81
+ }
82
+ /**
83
+ * Generic foundation kinds the module realizes as shared infra (not compute).
84
+ * Any plan `kind` NOT in this set is compute. Kept in sync with the module's
85
+ * `var.foundation_kinds` default.
86
+ */
87
+ declare const FOUNDATION_KINDS: readonly ["network", "storage", "secrets", "tls", "load-balancer", "dns", "license-file", "hec"];
88
+ type FoundationKind = (typeof FOUNDATION_KINDS)[number];
89
+
90
+ /** ALB spec as the module expects it (snake_case). */
91
+ interface AwsLoadBalancerVars {
92
+ target_port: number;
93
+ target_protocol: 'HTTP' | 'HTTPS';
94
+ health_check_path: string;
95
+ health_check_matcher: string;
96
+ health_check_protocol: string;
97
+ target_kinds: string[];
98
+ listener_port: number;
99
+ }
100
+ /** One rendered SG ingress rule (snake_case). */
101
+ interface AwsSecurityRuleVars {
102
+ port: number;
103
+ protocol: 'tcp' | 'udp';
104
+ sources: string[];
105
+ description: string;
106
+ }
107
+ /** Cognito MFA vars (snake_case), always present so `var.alb_auth.enabled` resolves. */
108
+ interface AwsAlbAuthVars {
109
+ enabled: boolean;
110
+ user_pool_arn: string;
111
+ user_pool_client_id: string;
112
+ user_pool_domain: string;
113
+ }
114
+ /** The spec-derived subset of the generic AWS module's variables. */
115
+ interface AwsInfraVars {
116
+ compute_kinds: string[];
117
+ security_rules: AwsSecurityRuleVars[];
118
+ load_balancer: AwsLoadBalancerVars | null;
119
+ dns_prefixes: Record<string, string>;
120
+ waf_enabled: boolean;
121
+ alb_auth: AwsAlbAuthVars;
122
+ }
123
+ /**
124
+ * Render the spec-derived tfvars for the generic AWS module. Pure and
125
+ * deterministic — no I/O, no clock, no randomness.
126
+ */
127
+ declare function renderInfraVars(spec: InfraSpec): AwsInfraVars;
128
+
129
+ declare function validateInfraSpec(spec: InfraSpec): string[];
130
+
131
+ export { type AlbAuthSpec, type AwsAlbAuthVars, type AwsInfraVars, type AwsLoadBalancerVars, type AwsSecurityRuleVars, FOUNDATION_KINDS, type FoundationKind, type InfraSpec, type LoadBalancerSpec, type SecurityRule, type SecuritySource, type StorageSpec, renderInfraVars, validateInfraSpec };
@@ -0,0 +1,131 @@
1
+ /** Where a security-group ingress rule may allow traffic from. */
2
+ type SecuritySource = 'self' | 'alb' | 'admin';
3
+ /** One security-group ingress rule: a port and who may reach it. */
4
+ interface SecurityRule {
5
+ /** Destination port to open on the compute nodes. */
6
+ port: number;
7
+ /** IP protocol. Default 'tcp'. */
8
+ protocol?: 'tcp' | 'udp';
9
+ /** Origins allowed to reach `port`. At least one. */
10
+ sources: SecuritySource[];
11
+ /** Human-readable description (surfaced on the rule). */
12
+ description?: string;
13
+ }
14
+ /** Front-door ALB spec. Omit entirely for forwarder-only / headless tools. */
15
+ interface LoadBalancerSpec {
16
+ /** Port the tool's web UI listens on behind the ALB (e.g. Splunk Web 8000). */
17
+ targetPort: number;
18
+ /** ALB→instance protocol. Default 'HTTP' (TLS terminates at the ALB). */
19
+ targetProtocol?: 'HTTP' | 'HTTPS';
20
+ /** Health-check path on the target (e.g. '/en-US/account/login'). */
21
+ healthCheckPath: string;
22
+ /** Health-check success matcher. Default '200-399'. */
23
+ healthCheckMatcher?: string;
24
+ /** Health-check protocol. Default = targetProtocol. */
25
+ healthCheckProtocol?: 'HTTP' | 'HTTPS';
26
+ /** Compute kinds that sit behind the ALB (e.g. ['search-head','standalone']). */
27
+ targetKinds: string[];
28
+ /** Public HTTPS listener port. Default 443. */
29
+ listenerPort?: number;
30
+ }
31
+ /** An object-storage bucket the tool needs (e.g. Splunk SmartStore, frozen). */
32
+ interface StorageSpec {
33
+ /** Logical suffix for the bucket name (e.g. 'smartstore'). */
34
+ name: string;
35
+ }
36
+ /** Optional Cognito/OIDC MFA enforced at the ALB, in front of the tool's UI. */
37
+ interface AlbAuthSpec {
38
+ enabled: boolean;
39
+ userPoolArn?: string;
40
+ userPoolClientId?: string;
41
+ userPoolDomain?: string;
42
+ }
43
+ /**
44
+ * The complete declarative infra spec for one app. Everything tool-shaped is a
45
+ * value here; the generic module reads these via tfvars.
46
+ */
47
+ interface InfraSpec {
48
+ /**
49
+ * Explicit compute kinds. When omitted, ANY plan item whose `kind` is not a
50
+ * generic FOUNDATION kind (see FOUNDATION_KINDS) is treated as compute — so an
51
+ * app's roles become compute automatically without listing them here.
52
+ */
53
+ computeKinds?: string[];
54
+ /** SG ingress rules: the tool's ports and who may reach each. */
55
+ securityRules: SecurityRule[];
56
+ /** ALB front-door. Omit for tools with no web UI. */
57
+ loadBalancer?: LoadBalancerSpec;
58
+ /**
59
+ * kind → DNS label prefix for per-node function FQDNs
60
+ * (e.g. { indexer: 'idx', 'search-head': 'sh', 'cluster-manager': 'mgmt' }).
61
+ * A kind absent from the map falls back to the kind string itself.
62
+ */
63
+ dnsPrefixes: Record<string, string>;
64
+ /** Object-storage buckets to create. Omit if the tool needs none. */
65
+ storage?: StorageSpec[];
66
+ /**
67
+ * Attach a WAFv2 web ACL (AWS-managed rule sets + IP rate limit) to the ALB.
68
+ * Defaults to true when `loadBalancer` is set; ignored otherwise.
69
+ */
70
+ waf?: boolean;
71
+ /** Optional ALB Cognito MFA. */
72
+ albAuth?: AlbAuthSpec;
73
+ /**
74
+ * Path, relative to the app's `infra/` dir, to the bring-up entrypoint the
75
+ * generic worker runs after `tofu apply` succeeds (config management +
76
+ * readiness gate). ALL tool-specific configuration lives behind this — e.g.
77
+ * './bringup/ansible/site.yml' for Splunk. Omit for tools that are ready at
78
+ * boot (no post-provision configuration).
79
+ */
80
+ bringup?: string;
81
+ }
82
+ /**
83
+ * Generic foundation kinds the module realizes as shared infra (not compute).
84
+ * Any plan `kind` NOT in this set is compute. Kept in sync with the module's
85
+ * `var.foundation_kinds` default.
86
+ */
87
+ declare const FOUNDATION_KINDS: readonly ["network", "storage", "secrets", "tls", "load-balancer", "dns", "license-file", "hec"];
88
+ type FoundationKind = (typeof FOUNDATION_KINDS)[number];
89
+
90
+ /** ALB spec as the module expects it (snake_case). */
91
+ interface AwsLoadBalancerVars {
92
+ target_port: number;
93
+ target_protocol: 'HTTP' | 'HTTPS';
94
+ health_check_path: string;
95
+ health_check_matcher: string;
96
+ health_check_protocol: string;
97
+ target_kinds: string[];
98
+ listener_port: number;
99
+ }
100
+ /** One rendered SG ingress rule (snake_case). */
101
+ interface AwsSecurityRuleVars {
102
+ port: number;
103
+ protocol: 'tcp' | 'udp';
104
+ sources: string[];
105
+ description: string;
106
+ }
107
+ /** Cognito MFA vars (snake_case), always present so `var.alb_auth.enabled` resolves. */
108
+ interface AwsAlbAuthVars {
109
+ enabled: boolean;
110
+ user_pool_arn: string;
111
+ user_pool_client_id: string;
112
+ user_pool_domain: string;
113
+ }
114
+ /** The spec-derived subset of the generic AWS module's variables. */
115
+ interface AwsInfraVars {
116
+ compute_kinds: string[];
117
+ security_rules: AwsSecurityRuleVars[];
118
+ load_balancer: AwsLoadBalancerVars | null;
119
+ dns_prefixes: Record<string, string>;
120
+ waf_enabled: boolean;
121
+ alb_auth: AwsAlbAuthVars;
122
+ }
123
+ /**
124
+ * Render the spec-derived tfvars for the generic AWS module. Pure and
125
+ * deterministic — no I/O, no clock, no randomness.
126
+ */
127
+ declare function renderInfraVars(spec: InfraSpec): AwsInfraVars;
128
+
129
+ declare function validateInfraSpec(spec: InfraSpec): string[];
130
+
131
+ export { type AlbAuthSpec, type AwsAlbAuthVars, type AwsInfraVars, type AwsLoadBalancerVars, type AwsSecurityRuleVars, FOUNDATION_KINDS, type FoundationKind, type InfraSpec, type LoadBalancerSpec, type SecurityRule, type SecuritySource, type StorageSpec, renderInfraVars, validateInfraSpec };
@@ -0,0 +1,109 @@
1
+ // src/opentofu/spec.ts
2
+ var FOUNDATION_KINDS = [
3
+ "network",
4
+ "storage",
5
+ "secrets",
6
+ "tls",
7
+ "load-balancer",
8
+ "dns",
9
+ "license-file",
10
+ "hec"
11
+ ];
12
+
13
+ // src/opentofu/render.ts
14
+ function renderInfraVars(spec) {
15
+ const security_rules = (spec.securityRules ?? []).map((r) => ({
16
+ port: r.port,
17
+ protocol: r.protocol ?? "tcp",
18
+ sources: [...r.sources],
19
+ description: r.description ?? ""
20
+ }));
21
+ const load_balancer = spec.loadBalancer ? {
22
+ target_port: spec.loadBalancer.targetPort,
23
+ target_protocol: spec.loadBalancer.targetProtocol ?? "HTTP",
24
+ health_check_path: spec.loadBalancer.healthCheckPath,
25
+ health_check_matcher: spec.loadBalancer.healthCheckMatcher ?? "200-399",
26
+ health_check_protocol: spec.loadBalancer.healthCheckProtocol ?? "",
27
+ target_kinds: [...spec.loadBalancer.targetKinds],
28
+ listener_port: spec.loadBalancer.listenerPort ?? 443
29
+ } : null;
30
+ const waf_enabled = spec.waf ?? Boolean(spec.loadBalancer);
31
+ const alb_auth = {
32
+ enabled: spec.albAuth?.enabled ?? false,
33
+ user_pool_arn: spec.albAuth?.userPoolArn ?? "",
34
+ user_pool_client_id: spec.albAuth?.userPoolClientId ?? "",
35
+ user_pool_domain: spec.albAuth?.userPoolDomain ?? ""
36
+ };
37
+ return {
38
+ compute_kinds: spec.computeKinds ? [...spec.computeKinds] : [],
39
+ security_rules,
40
+ load_balancer,
41
+ dns_prefixes: { ...spec.dnsPrefixes },
42
+ waf_enabled,
43
+ alb_auth
44
+ };
45
+ }
46
+
47
+ // src/opentofu/validate.ts
48
+ function validateInfraSpec(spec) {
49
+ const errors = [];
50
+ if (!Array.isArray(spec.securityRules) || spec.securityRules.length === 0) {
51
+ errors.push("securityRules must contain at least one rule.");
52
+ }
53
+ const validSources = /* @__PURE__ */ new Set(["self", "alb", "admin"]);
54
+ const usesAlbSource = (spec.securityRules ?? []).some(
55
+ (r) => (r.sources ?? []).includes("alb")
56
+ );
57
+ for (const r of spec.securityRules ?? []) {
58
+ if (typeof r.port !== "number" || r.port < 1 || r.port > 65535) {
59
+ errors.push(`securityRules: invalid port ${JSON.stringify(r.port)}.`);
60
+ }
61
+ if (!Array.isArray(r.sources) || r.sources.length === 0) {
62
+ errors.push(`securityRules[port ${r.port}]: sources must be non-empty.`);
63
+ }
64
+ for (const s of r.sources ?? []) {
65
+ if (!validSources.has(s)) {
66
+ errors.push(`securityRules[port ${r.port}]: unknown source "${s}" (expected self|alb|admin).`);
67
+ }
68
+ }
69
+ }
70
+ if (usesAlbSource && !spec.loadBalancer) {
71
+ errors.push('a securityRules entry uses source "alb" but no loadBalancer is declared.');
72
+ }
73
+ if (spec.loadBalancer) {
74
+ const lb = spec.loadBalancer;
75
+ if (typeof lb.targetPort !== "number") {
76
+ errors.push("loadBalancer.targetPort is required.");
77
+ }
78
+ if (!lb.healthCheckPath || !lb.healthCheckPath.startsWith("/")) {
79
+ errors.push('loadBalancer.healthCheckPath must be an absolute path (start with "/").');
80
+ }
81
+ if (!Array.isArray(lb.targetKinds) || lb.targetKinds.length === 0) {
82
+ errors.push("loadBalancer.targetKinds must name at least one compute kind.");
83
+ }
84
+ const targetPortOpen = (spec.securityRules ?? []).some(
85
+ (r) => r.port === lb.targetPort && (r.sources ?? []).includes("alb")
86
+ );
87
+ if (!targetPortOpen) {
88
+ errors.push(
89
+ `loadBalancer.targetPort ${lb.targetPort} is not opened to source "alb" in securityRules \u2014 the ALB can't reach the web tier.`
90
+ );
91
+ }
92
+ }
93
+ if (spec.albAuth?.enabled) {
94
+ const a = spec.albAuth;
95
+ if (!a.userPoolArn || !a.userPoolClientId || !a.userPoolDomain) {
96
+ errors.push("albAuth.enabled requires userPoolArn, userPoolClientId and userPoolDomain.");
97
+ }
98
+ if (!spec.loadBalancer) {
99
+ errors.push("albAuth is set but there is no loadBalancer to attach it to.");
100
+ }
101
+ }
102
+ return errors;
103
+ }
104
+ export {
105
+ FOUNDATION_KINDS,
106
+ renderInfraVars,
107
+ validateInfraSpec
108
+ };
109
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/opentofu/spec.ts","../../src/opentofu/render.ts","../../src/opentofu/validate.ts"],"sourcesContent":["// =============================================================================\n// InfraSpec — the declarative, tool-agnostic infrastructure contract.\n//\n// An app ships one of these in `apps/<app>/infra/spec.ts`. The SDK renders it\n// into the generic OpenTofu module's tfvars (see ./render). NOTHING in the\n// module is tool-specific: an app supplies all of its shape (roles, ports,\n// front-door, DNS, bring-up) as DATA here, so the same modules provision Splunk,\n// Security Onion, or any other BYOI tool without a line of per-tool HCL.\n//\n// Pairs with sdk/opentofu/modules/<cloud> (the generic HCL) and the platform\n// render+apply worker, which invokes the app's `bringup` entrypoint after apply.\n// =============================================================================\n\n/** Where a security-group ingress rule may allow traffic from. */\nexport type SecuritySource =\n | 'self' // peer nodes in this stack (the node SG referencing itself)\n | 'alb' // the public ALB security group (only valid when loadBalancer is set)\n | 'admin'; // the Veltrix control-plane / admin CIDR (var.admin_cidr)\n\n/** One security-group ingress rule: a port and who may reach it. */\nexport interface SecurityRule {\n /** Destination port to open on the compute nodes. */\n port: number;\n /** IP protocol. Default 'tcp'. */\n protocol?: 'tcp' | 'udp';\n /** Origins allowed to reach `port`. At least one. */\n sources: SecuritySource[];\n /** Human-readable description (surfaced on the rule). */\n description?: string;\n}\n\n/** Front-door ALB spec. Omit entirely for forwarder-only / headless tools. */\nexport interface LoadBalancerSpec {\n /** Port the tool's web UI listens on behind the ALB (e.g. Splunk Web 8000). */\n targetPort: number;\n /** ALB→instance protocol. Default 'HTTP' (TLS terminates at the ALB). */\n targetProtocol?: 'HTTP' | 'HTTPS';\n /** Health-check path on the target (e.g. '/en-US/account/login'). */\n healthCheckPath: string;\n /** Health-check success matcher. Default '200-399'. */\n healthCheckMatcher?: string;\n /** Health-check protocol. Default = targetProtocol. */\n healthCheckProtocol?: 'HTTP' | 'HTTPS';\n /** Compute kinds that sit behind the ALB (e.g. ['search-head','standalone']). */\n targetKinds: string[];\n /** Public HTTPS listener port. Default 443. */\n listenerPort?: number;\n}\n\n/** An object-storage bucket the tool needs (e.g. Splunk SmartStore, frozen). */\nexport interface StorageSpec {\n /** Logical suffix for the bucket name (e.g. 'smartstore'). */\n name: string;\n}\n\n/** Optional Cognito/OIDC MFA enforced at the ALB, in front of the tool's UI. */\nexport interface AlbAuthSpec {\n enabled: boolean;\n userPoolArn?: string;\n userPoolClientId?: string;\n userPoolDomain?: string;\n}\n\n/**\n * The complete declarative infra spec for one app. Everything tool-shaped is a\n * value here; the generic module reads these via tfvars.\n */\nexport interface InfraSpec {\n /**\n * Explicit compute kinds. When omitted, ANY plan item whose `kind` is not a\n * generic FOUNDATION kind (see FOUNDATION_KINDS) is treated as compute — so an\n * app's roles become compute automatically without listing them here.\n */\n computeKinds?: string[];\n\n /** SG ingress rules: the tool's ports and who may reach each. */\n securityRules: SecurityRule[];\n\n /** ALB front-door. Omit for tools with no web UI. */\n loadBalancer?: LoadBalancerSpec;\n\n /**\n * kind → DNS label prefix for per-node function FQDNs\n * (e.g. { indexer: 'idx', 'search-head': 'sh', 'cluster-manager': 'mgmt' }).\n * A kind absent from the map falls back to the kind string itself.\n */\n dnsPrefixes: Record<string, string>;\n\n /** Object-storage buckets to create. Omit if the tool needs none. */\n storage?: StorageSpec[];\n\n /**\n * Attach a WAFv2 web ACL (AWS-managed rule sets + IP rate limit) to the ALB.\n * Defaults to true when `loadBalancer` is set; ignored otherwise.\n */\n waf?: boolean;\n\n /** Optional ALB Cognito MFA. */\n albAuth?: AlbAuthSpec;\n\n /**\n * Path, relative to the app's `infra/` dir, to the bring-up entrypoint the\n * generic worker runs after `tofu apply` succeeds (config management +\n * readiness gate). ALL tool-specific configuration lives behind this — e.g.\n * './bringup/ansible/site.yml' for Splunk. Omit for tools that are ready at\n * boot (no post-provision configuration).\n */\n bringup?: string;\n}\n\n/**\n * Generic foundation kinds the module realizes as shared infra (not compute).\n * Any plan `kind` NOT in this set is compute. Kept in sync with the module's\n * `var.foundation_kinds` default.\n */\nexport const FOUNDATION_KINDS = [\n 'network',\n 'storage',\n 'secrets',\n 'tls',\n 'load-balancer',\n 'dns',\n 'license-file',\n 'hec',\n] as const;\n\nexport type FoundationKind = (typeof FOUNDATION_KINDS)[number];\n","// =============================================================================\n// render — turn an app's declarative InfraSpec into the generic OpenTofu\n// module's variable values (tfvars).\n//\n// The platform render+apply worker builds the BASE vars (app_id, customer_id,\n// plan, subnet_cidr, tags, region, ...) from the DeployRequest, and merges these\n// SPEC-derived vars on top. Field names are snake_case to match the HCL module\n// variables 1:1. Defaults mirror the module's `optional(...)` defaults, applied\n// here so the emitted tfvars are explicit and self-describing.\n// =============================================================================\n\nimport type { InfraSpec } from './spec';\n\n/** ALB spec as the module expects it (snake_case). */\nexport interface AwsLoadBalancerVars {\n target_port: number;\n target_protocol: 'HTTP' | 'HTTPS';\n health_check_path: string;\n health_check_matcher: string;\n health_check_protocol: string; // \"\" => module falls back to target_protocol\n target_kinds: string[];\n listener_port: number;\n}\n\n/** One rendered SG ingress rule (snake_case). */\nexport interface AwsSecurityRuleVars {\n port: number;\n protocol: 'tcp' | 'udp';\n sources: string[];\n description: string;\n}\n\n/** Cognito MFA vars (snake_case), always present so `var.alb_auth.enabled` resolves. */\nexport interface AwsAlbAuthVars {\n enabled: boolean;\n user_pool_arn: string;\n user_pool_client_id: string;\n user_pool_domain: string;\n}\n\n/** The spec-derived subset of the generic AWS module's variables. */\nexport interface AwsInfraVars {\n compute_kinds: string[];\n security_rules: AwsSecurityRuleVars[];\n load_balancer: AwsLoadBalancerVars | null;\n dns_prefixes: Record<string, string>;\n waf_enabled: boolean;\n alb_auth: AwsAlbAuthVars;\n}\n\n/**\n * Render the spec-derived tfvars for the generic AWS module. Pure and\n * deterministic — no I/O, no clock, no randomness.\n */\nexport function renderInfraVars(spec: InfraSpec): AwsInfraVars {\n const security_rules: AwsSecurityRuleVars[] = (spec.securityRules ?? []).map((r) => ({\n port: r.port,\n protocol: r.protocol ?? 'tcp',\n sources: [...r.sources],\n description: r.description ?? '',\n }));\n\n const load_balancer: AwsLoadBalancerVars | null = spec.loadBalancer\n ? {\n target_port: spec.loadBalancer.targetPort,\n target_protocol: spec.loadBalancer.targetProtocol ?? 'HTTP',\n health_check_path: spec.loadBalancer.healthCheckPath,\n health_check_matcher: spec.loadBalancer.healthCheckMatcher ?? '200-399',\n health_check_protocol: spec.loadBalancer.healthCheckProtocol ?? '',\n target_kinds: [...spec.loadBalancer.targetKinds],\n listener_port: spec.loadBalancer.listenerPort ?? 443,\n }\n : null;\n\n // WAF defaults ON when there is a load balancer, OFF otherwise (the module\n // ignores it without an ALB, but keep the emitted value honest).\n const waf_enabled = spec.waf ?? Boolean(spec.loadBalancer);\n\n const alb_auth: AwsAlbAuthVars = {\n enabled: spec.albAuth?.enabled ?? false,\n user_pool_arn: spec.albAuth?.userPoolArn ?? '',\n user_pool_client_id: spec.albAuth?.userPoolClientId ?? '',\n user_pool_domain: spec.albAuth?.userPoolDomain ?? '',\n };\n\n return {\n compute_kinds: spec.computeKinds ? [...spec.computeKinds] : [],\n security_rules,\n load_balancer,\n dns_prefixes: { ...spec.dnsPrefixes },\n waf_enabled,\n alb_auth,\n };\n}\n","// =============================================================================\n// validateInfraSpec — cheap, dependency-free checks that catch the InfraSpec\n// mistakes the OpenTofu module can't (or would only surface at apply time).\n// Returns a list of human-readable errors; empty === valid.\n// =============================================================================\n\nimport type { InfraSpec } from './spec';\n\nexport function validateInfraSpec(spec: InfraSpec): string[] {\n const errors: string[] = [];\n\n if (!Array.isArray(spec.securityRules) || spec.securityRules.length === 0) {\n errors.push('securityRules must contain at least one rule.');\n }\n\n const validSources = new Set(['self', 'alb', 'admin']);\n const usesAlbSource = (spec.securityRules ?? []).some((r) =>\n (r.sources ?? []).includes('alb'),\n );\n\n for (const r of spec.securityRules ?? []) {\n if (typeof r.port !== 'number' || r.port < 1 || r.port > 65535) {\n errors.push(`securityRules: invalid port ${JSON.stringify(r.port)}.`);\n }\n if (!Array.isArray(r.sources) || r.sources.length === 0) {\n errors.push(`securityRules[port ${r.port}]: sources must be non-empty.`);\n }\n for (const s of r.sources ?? []) {\n if (!validSources.has(s)) {\n errors.push(`securityRules[port ${r.port}]: unknown source \"${s}\" (expected self|alb|admin).`);\n }\n }\n }\n\n // \"alb\" sources are meaningless without a load balancer.\n if (usesAlbSource && !spec.loadBalancer) {\n errors.push('a securityRules entry uses source \"alb\" but no loadBalancer is declared.');\n }\n\n if (spec.loadBalancer) {\n const lb = spec.loadBalancer;\n if (typeof lb.targetPort !== 'number') {\n errors.push('loadBalancer.targetPort is required.');\n }\n if (!lb.healthCheckPath || !lb.healthCheckPath.startsWith('/')) {\n errors.push('loadBalancer.healthCheckPath must be an absolute path (start with \"/\").');\n }\n if (!Array.isArray(lb.targetKinds) || lb.targetKinds.length === 0) {\n errors.push('loadBalancer.targetKinds must name at least one compute kind.');\n }\n // The web tier must be reachable from the ALB.\n const targetPortOpen = (spec.securityRules ?? []).some(\n (r) => r.port === lb.targetPort && (r.sources ?? []).includes('alb'),\n );\n if (!targetPortOpen) {\n errors.push(\n `loadBalancer.targetPort ${lb.targetPort} is not opened to source \"alb\" in securityRules — the ALB can't reach the web tier.`,\n );\n }\n }\n\n if (spec.albAuth?.enabled) {\n const a = spec.albAuth;\n if (!a.userPoolArn || !a.userPoolClientId || !a.userPoolDomain) {\n errors.push('albAuth.enabled requires userPoolArn, userPoolClientId and userPoolDomain.');\n }\n if (!spec.loadBalancer) {\n errors.push('albAuth is set but there is no loadBalancer to attach it to.');\n }\n }\n\n return errors;\n}\n"],"mappings":"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","names":[]}