@fortytwoservices/ai-universe-setup 1.0.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 (94) hide show
  1. package/assets/app.bicep +145 -0
  2. package/assets/docker-compose.yml +50 -0
  3. package/assets/main.bicep +233 -0
  4. package/assets/modules/auditSecret.bicep +28 -0
  5. package/assets/modules/backendApp.bicep +107 -0
  6. package/assets/modules/containerApp.bicep +99 -0
  7. package/assets/modules/containerAppsEnv.bicep +43 -0
  8. package/assets/modules/controlGalaxyApp.bicep +129 -0
  9. package/assets/modules/entraClientSecret.bicep +31 -0
  10. package/assets/modules/innovationJob.bicep +91 -0
  11. package/assets/modules/insights.bicep +29 -0
  12. package/assets/modules/keyVault.bicep +42 -0
  13. package/assets/modules/platformIdentity.bicep +69 -0
  14. package/assets/modules/postgres.bicep +116 -0
  15. package/assets/modules/registry.bicep +38 -0
  16. package/assets/modules/storage.bicep +30 -0
  17. package/bin/setup.mjs +7 -0
  18. package/dist/azure/bicep.d.ts +9 -0
  19. package/dist/azure/bicep.d.ts.map +1 -0
  20. package/dist/azure/bicep.js +57 -0
  21. package/dist/azure/bicep.js.map +1 -0
  22. package/dist/azure/entra.d.ts +7 -0
  23. package/dist/azure/entra.d.ts.map +1 -0
  24. package/dist/azure/entra.js +45 -0
  25. package/dist/azure/entra.js.map +1 -0
  26. package/dist/azure/github.d.ts +5 -0
  27. package/dist/azure/github.d.ts.map +1 -0
  28. package/dist/azure/github.js +28 -0
  29. package/dist/azure/github.js.map +1 -0
  30. package/dist/azure/images.d.ts +15 -0
  31. package/dist/azure/images.d.ts.map +1 -0
  32. package/dist/azure/images.js +53 -0
  33. package/dist/azure/images.js.map +1 -0
  34. package/dist/azure/prerequisites.d.ts +8 -0
  35. package/dist/azure/prerequisites.d.ts.map +1 -0
  36. package/dist/azure/prerequisites.js +56 -0
  37. package/dist/azure/prerequisites.js.map +1 -0
  38. package/dist/azure/prompts.d.ts +10 -0
  39. package/dist/azure/prompts.d.ts.map +1 -0
  40. package/dist/azure/prompts.js +88 -0
  41. package/dist/azure/prompts.js.map +1 -0
  42. package/dist/azure/resource-group.d.ts +2 -0
  43. package/dist/azure/resource-group.d.ts.map +1 -0
  44. package/dist/azure/resource-group.js +8 -0
  45. package/dist/azure/resource-group.js.map +1 -0
  46. package/dist/azure/run.d.ts +2 -0
  47. package/dist/azure/run.d.ts.map +1 -0
  48. package/dist/azure/run.js +87 -0
  49. package/dist/azure/run.js.map +1 -0
  50. package/dist/azure/summary.d.ts +3 -0
  51. package/dist/azure/summary.d.ts.map +1 -0
  52. package/dist/azure/summary.js +13 -0
  53. package/dist/azure/summary.js.map +1 -0
  54. package/dist/constants.d.ts +13 -0
  55. package/dist/constants.d.ts.map +1 -0
  56. package/dist/constants.js +21 -0
  57. package/dist/constants.js.map +1 -0
  58. package/dist/index.d.ts +2 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +17 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/local/compose.d.ts +5 -0
  63. package/dist/local/compose.d.ts.map +1 -0
  64. package/dist/local/compose.js +40 -0
  65. package/dist/local/compose.js.map +1 -0
  66. package/dist/local/prerequisites.d.ts +3 -0
  67. package/dist/local/prerequisites.d.ts.map +1 -0
  68. package/dist/local/prerequisites.js +60 -0
  69. package/dist/local/prerequisites.js.map +1 -0
  70. package/dist/local/prompts.d.ts +6 -0
  71. package/dist/local/prompts.d.ts.map +1 -0
  72. package/dist/local/prompts.js +23 -0
  73. package/dist/local/prompts.js.map +1 -0
  74. package/dist/local/run.d.ts +2 -0
  75. package/dist/local/run.d.ts.map +1 -0
  76. package/dist/local/run.js +50 -0
  77. package/dist/local/run.js.map +1 -0
  78. package/dist/local/summary.d.ts +2 -0
  79. package/dist/local/summary.d.ts.map +1 -0
  80. package/dist/local/summary.js +16 -0
  81. package/dist/local/summary.js.map +1 -0
  82. package/dist/logo.d.ts +2 -0
  83. package/dist/logo.d.ts.map +1 -0
  84. package/dist/logo.js +8 -0
  85. package/dist/logo.js.map +1 -0
  86. package/dist/ui.d.ts +7 -0
  87. package/dist/ui.d.ts.map +1 -0
  88. package/dist/ui.js +16 -0
  89. package/dist/ui.js.map +1 -0
  90. package/dist/utils.d.ts +10 -0
  91. package/dist/utils.d.ts.map +1 -0
  92. package/dist/utils.js +45 -0
  93. package/dist/utils.js.map +1 -0
  94. package/package.json +49 -0
@@ -0,0 +1,43 @@
1
+ // The shared Container Apps managed environment. The backend, the control
2
+ // galaxy, and the innovation job all run inside this one environment so they
3
+ // share a log destination and can address each other over the internal
4
+ // environment domain when wired that way.
5
+
6
+ @description('Short, kebab-case portal name.')
7
+ param portalName string
8
+
9
+ @description('Azure region.')
10
+ param location string
11
+
12
+ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
13
+ name: '${portalName}-cae-logs'
14
+ location: location
15
+ properties: {
16
+ sku: { name: 'PerGB2018' }
17
+ retentionInDays: 30
18
+ workspaceCapping: { dailyQuotaGb: 2 }
19
+ }
20
+ }
21
+
22
+ resource environment 'Microsoft.App/managedEnvironments@2024-03-01' = {
23
+ name: '${portalName}-cae'
24
+ location: location
25
+ properties: {
26
+ appLogsConfiguration: {
27
+ destination: 'log-analytics'
28
+ logAnalyticsConfiguration: {
29
+ customerId: logAnalytics.properties.customerId
30
+ sharedKey: logAnalytics.listKeys().primarySharedKey
31
+ }
32
+ }
33
+ workloadProfiles: [
34
+ {
35
+ name: 'Consumption'
36
+ workloadProfileType: 'Consumption'
37
+ }
38
+ ]
39
+ }
40
+ }
41
+
42
+ output environmentId string = environment.id
43
+ output defaultDomain string = environment.properties.defaultDomain
@@ -0,0 +1,129 @@
1
+ // Control galaxy (admin portal) as a Container App. It serves the admin SPA
2
+ // and runs the BFF proxy that forwards the operator's Entra token to the
3
+ // backend control-plane API (shared-audience passthrough). It keeps its own
4
+ // portal-local database for the galaxy-local domains (connectors, branding,
5
+ // onboarding, users, settings); everything platform/AI-governance-related is
6
+ // owned by the backend and reached over BACKEND_BASE_URL.
7
+ //
8
+ // External ingress: operators reach the admin UI directly. The admin SPA
9
+ // acquires a token for the shared admin audience and the BFF forwards it to
10
+ // the backend; both validate the same audience.
11
+
12
+ @description('Short, kebab-case portal name.')
13
+ param portalName string
14
+
15
+ @description('Azure region.')
16
+ param location string
17
+
18
+ @description('Container Apps managed environment id (containerAppsEnv.bicep).')
19
+ param environmentId string
20
+
21
+ @description('User-assigned managed identity resource id (platformIdentity.bicep).')
22
+ param identityId string
23
+
24
+ @description('ACR login server (e.g. acmeacr.azurecr.io).')
25
+ param acrLoginServer string
26
+
27
+ @description('Fully qualified control-galaxy image reference, including tag.')
28
+ param controlGalaxyImage string
29
+
30
+ @description('Backend base URL the BFF forwards admin/config calls to (https://<backend fqdn>).')
31
+ param backendBaseUrl string
32
+
33
+ @description('Key Vault URI (https://<vault>.vault.azure.net/).')
34
+ param keyVaultUri string
35
+
36
+ @description('Key Vault secret name holding the control-galaxy database connection string.')
37
+ param databaseSecretName string
38
+
39
+ @description('Key Vault secret name holding the audit HMAC signing key.')
40
+ param auditSecretName string
41
+
42
+ @description('Key Vault secret name holding the Entra app-registration client secret (connector OBO).')
43
+ param entraClientSecretName string
44
+
45
+ @description('Application Insights connection string.')
46
+ param insightsConnectionString string
47
+
48
+ @description('Entra tenant id for token validation.')
49
+ param entraTenantId string
50
+
51
+ @description('Admin/config-plane Entra app registration client id (shared audience with the backend).')
52
+ param entraAdminClientId string
53
+
54
+ @description('Portal organization DID.')
55
+ param portalOrgDid string
56
+
57
+ @description('Portal organization display name.')
58
+ param portalOrgName string
59
+
60
+ @description('CPU cores.')
61
+ param cpuCores string = '0.5'
62
+
63
+ @description('Memory.')
64
+ param memorySize string = '1Gi'
65
+
66
+ // Domain-specific secrets[] and env[]. These literal arrays stay here (rather
67
+ // than in containerApp.bicep) so the infra contract test can parse the env
68
+ // contract from this module. The shared containerApps scaffolding lives in
69
+ // containerApp.bicep.
70
+ var controlGalaxySecrets = [
71
+ {
72
+ name: 'database-url'
73
+ keyVaultUrl: '${keyVaultUri}secrets/${databaseSecretName}'
74
+ identity: identityId
75
+ }
76
+ {
77
+ name: 'audit-signing-key'
78
+ keyVaultUrl: '${keyVaultUri}secrets/${auditSecretName}'
79
+ identity: identityId
80
+ }
81
+ {
82
+ name: 'entra-client-secret'
83
+ keyVaultUrl: '${keyVaultUri}secrets/${entraClientSecretName}'
84
+ identity: identityId
85
+ }
86
+ ]
87
+
88
+ var controlGalaxyEnv = [
89
+ { name: 'NODE_ENV', value: 'production' }
90
+ { name: 'HOST', value: '0.0.0.0' }
91
+ { name: 'PORT', value: '5174' }
92
+ { name: 'BACKEND_BASE_URL', value: backendBaseUrl }
93
+ { name: 'DATABASE_URL', secretRef: 'database-url' }
94
+ { name: 'AUDIT_SIGNING_KEY', secretRef: 'audit-signing-key' }
95
+ { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: insightsConnectionString }
96
+ { name: 'KEY_VAULT_URI', value: keyVaultUri }
97
+ // Foundry OBO (server/agents/external/foundry-deps.ts) reads
98
+ // AZURE_KEY_VAULT_URL; other code reads KEY_VAULT_URI. Keep both.
99
+ { name: 'AZURE_KEY_VAULT_URL', value: keyVaultUri }
100
+ { name: 'ENTRA_TENANT_ID', value: entraTenantId }
101
+ { name: 'ENTRA_CLIENT_ID', value: entraAdminClientId }
102
+ // Connector OBO (server/index.ts) reads ENTRA_CLIENT_SECRET.
103
+ { name: 'ENTRA_CLIENT_SECRET', secretRef: 'entra-client-secret' }
104
+ { name: 'BACKEND_API_AUDIENCE', value: 'api://${entraAdminClientId}' }
105
+ { name: 'PORTAL_ORG_DID', value: portalOrgDid }
106
+ { name: 'PORTAL_ORG_NAME', value: portalOrgName }
107
+ ]
108
+
109
+ module controlGalaxy 'containerApp.bicep' = {
110
+ name: 'controlGalaxyContainerApp'
111
+ params: {
112
+ portalName: portalName
113
+ appSuffix: 'control-galaxy'
114
+ location: location
115
+ environmentId: environmentId
116
+ identityId: identityId
117
+ acrLoginServer: acrLoginServer
118
+ image: controlGalaxyImage
119
+ port: 5174
120
+ maxReplicas: 2
121
+ cpuCores: cpuCores
122
+ memorySize: memorySize
123
+ secrets: controlGalaxySecrets
124
+ env: controlGalaxyEnv
125
+ }
126
+ }
127
+
128
+ output appName string = controlGalaxy.outputs.appName
129
+ output fqdn string = controlGalaxy.outputs.fqdn
@@ -0,0 +1,31 @@
1
+ // Entra app-registration client secret for connector OBO (on-behalf-of) flows.
2
+ // apps/control-galaxy/server/index.ts reads process.env.ENTRA_CLIENT_SECRET to
3
+ // exchange the operator token for a connector access token. Without it, every
4
+ // enabled connector fails at its first OBO call. Written here (not in
5
+ // keyVault.bicep) so the @secure() value stays inside a module that references
6
+ // the vault by name param, keeping it out of the orchestration deployment graph
7
+ // as an output. Mirrors auditSecret.bicep.
8
+
9
+ @description('Name of the existing Key Vault to write the secret into.')
10
+ param keyVaultName string
11
+
12
+ @secure()
13
+ @description('Entra app-registration client secret value.')
14
+ param entraClientSecret string
15
+
16
+ @description('Secret name.')
17
+ param secretName string = 'entra-client-secret'
18
+
19
+ resource vault 'Microsoft.KeyVault/vaults@2024-04-01-preview' existing = {
20
+ name: keyVaultName
21
+ }
22
+
23
+ resource secret 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' = {
24
+ parent: vault
25
+ name: secretName
26
+ properties: {
27
+ value: entraClientSecret
28
+ }
29
+ }
30
+
31
+ output secretName string = secret.name
@@ -0,0 +1,91 @@
1
+ // The Innovation Implementer as a Container Apps Job, on the SHARED
2
+ // environment and SHARED platform identity (no longer its own env/logs/identity).
3
+ //
4
+ // When an admin approves an innovation proposal, the control galaxy triggers
5
+ // this short-lived job. The agent reads the proposal, generates code following
6
+ // the galaxy's CLAUDE.md + brain/wisdom/ conventions, opens a PR, then dies.
7
+ // Jobs scale to zero with per-second billing; each run is a fresh container so
8
+ // state cannot leak between proposals.
9
+ //
10
+ // Credentials (Anthropic key, GitHub PAT, callback HMAC) are read from Key
11
+ // Vault at job start over the shared user-assigned identity. No long-lived
12
+ // secret is baked into the image.
13
+
14
+ @description('Short name prefix (matches main.bicep).')
15
+ param portalName string
16
+
17
+ @description('Azure region.')
18
+ param location string
19
+
20
+ @description('Shared Container Apps managed environment id.')
21
+ param environmentId string
22
+
23
+ @description('Shared user-assigned managed identity resource id (already granted Key Vault Secrets User + AcrPull).')
24
+ param identityId string
25
+
26
+ @description('Identity client id, surfaced to the container so it can authenticate to Azure as the managed identity.')
27
+ param identityClientId string
28
+
29
+ @description('Key Vault name the job reads its run secrets from.')
30
+ param keyVaultName string
31
+
32
+ @description('Container image for the implementer agent.')
33
+ param implementerImage string = 'ghcr.io/fortytwoservices/ai-universe-innovation-implementer:v1'
34
+
35
+ @description('CPU cores. Decimal allowed.')
36
+ param cpuCores string = '1.0'
37
+
38
+ @description('Memory.')
39
+ param memorySize string = '2Gi'
40
+
41
+ @description('Replica timeout in seconds. Hard upper bound on agent runtime.')
42
+ param replicaTimeoutSeconds int = 1800
43
+
44
+ resource keyVault 'Microsoft.KeyVault/vaults@2024-04-01-preview' existing = {
45
+ name: keyVaultName
46
+ }
47
+
48
+ resource implementerJob 'Microsoft.App/jobs@2024-03-01' = {
49
+ name: '${portalName}-innovation-implementer'
50
+ location: location
51
+ identity: {
52
+ type: 'UserAssigned'
53
+ userAssignedIdentities: {
54
+ '${identityId}': {}
55
+ }
56
+ }
57
+ properties: {
58
+ environmentId: environmentId
59
+ workloadProfileName: 'Consumption'
60
+ configuration: {
61
+ triggerType: 'Manual'
62
+ replicaTimeout: replicaTimeoutSeconds
63
+ replicaRetryLimit: 1
64
+ manualTriggerConfig: {
65
+ replicaCompletionCount: 1
66
+ parallelism: 1
67
+ }
68
+ secrets: []
69
+ }
70
+ template: {
71
+ containers: [
72
+ {
73
+ name: 'implementer'
74
+ image: implementerImage
75
+ resources: {
76
+ cpu: json(cpuCores)
77
+ memory: memorySize
78
+ }
79
+ env: [
80
+ { name: 'KEY_VAULT_NAME', value: keyVault.name }
81
+ { name: 'KEY_VAULT_URI', value: keyVault.properties.vaultUri }
82
+ { name: 'PORTAL_NAME', value: portalName }
83
+ { name: 'AZURE_CLIENT_ID', value: identityClientId }
84
+ ]
85
+ }
86
+ ]
87
+ }
88
+ }
89
+ }
90
+
91
+ output jobName string = implementerJob.name
@@ -0,0 +1,29 @@
1
+ // Application Insights resource (Log Analytics workspace-backed, the modern variant).
2
+
3
+ @description('Short name prefix for the portal.')
4
+ param portalName string
5
+
6
+ @description('Azure region.')
7
+ param location string
8
+
9
+ resource logWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
10
+ name: '${portalName}-logs'
11
+ location: location
12
+ properties: {
13
+ sku: { name: 'PerGB2018' }
14
+ retentionInDays: 30
15
+ }
16
+ }
17
+
18
+ resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
19
+ name: '${portalName}-insights'
20
+ location: location
21
+ kind: 'web'
22
+ properties: {
23
+ Application_Type: 'web'
24
+ WorkspaceResourceId: logWorkspace.id
25
+ IngestionMode: 'LogAnalytics'
26
+ }
27
+ }
28
+
29
+ output connectionString string = appInsights.properties.ConnectionString
@@ -0,0 +1,42 @@
1
+ // Key Vault — owned and provisioned by this module. Domain-specific
2
+ // secret writes (postgres connection strings, audit HMAC signing key,
3
+ // app-reg secrets) live in the modules that own those domains and
4
+ // reference this vault by name, so secret values never travel through
5
+ // this orchestration layer as outputs. RBAC-only authorization model;
6
+ // the shared platform managed identity gets `Key Vault Secrets User`
7
+ // in platformIdentity.bicep.
8
+
9
+ @description('Short name prefix for the portal.')
10
+ param portalName string
11
+
12
+ @description('Azure region.')
13
+ param location string
14
+
15
+ @description('Entra ID tenant for RBAC.')
16
+ param tenantId string
17
+
18
+ // Collision-resistant, deterministic vault name. A plain take('${portalName}-kv', 24)
19
+ // drops the '-kv' suffix for long portalNames (~22 chars) and can collide across
20
+ // resource groups. Appending a stable uniqueString(resourceGroup().id) segment
21
+ // keeps the name unique and reconstructible. uniqueString is deterministic per RG,
22
+ // so app.bicep rebuilds the SAME name as an existing reference. Keep both name
23
+ // expressions (here and in app.bicep) in lock-step. Max length 24:
24
+ // take(portalName, 9) + '-kv-' (4) + take(unique, 6) = up to 19 chars.
25
+ var vaultName = '${take(portalName, 9)}-kv-${take(uniqueString(resourceGroup().id), 6)}'
26
+
27
+ resource vault 'Microsoft.KeyVault/vaults@2024-04-01-preview' = {
28
+ name: vaultName
29
+ location: location
30
+ properties: {
31
+ tenantId: tenantId
32
+ sku: { family: 'A', name: 'standard' }
33
+ enableRbacAuthorization: true
34
+ enableSoftDelete: true
35
+ softDeleteRetentionInDays: 7
36
+ enablePurgeProtection: true
37
+ publicNetworkAccess: 'Enabled'
38
+ }
39
+ }
40
+
41
+ output vaultName string = vault.name
42
+ output vaultUri string = vault.properties.vaultUri
@@ -0,0 +1,69 @@
1
+ // One user-assigned managed identity shared by the platform Container Apps
2
+ // (backend, control galaxy) and the innovation job. Using a user-assigned
3
+ // identity avoids the chicken-and-egg of system-assigned identities needing
4
+ // AcrPull before the first image pull: the identity and its role assignments
5
+ // exist before any app references it.
6
+ //
7
+ // Grants:
8
+ // - AcrPull on the customer registry (pull images with no registry secret)
9
+ // - Key Vault Secrets Officer on the vault. Read is needed to resolve
10
+ // DATABASE_URL, HMAC keys, and app-reg secrets as Container Apps secret
11
+ // references. WRITE is needed because the control galaxy persists provider
12
+ // secrets at runtime: apps/control-galaxy/server/config/router.ts calls
13
+ // client.setSecret(...). Secrets User is read-only and would 403 on that
14
+ // write, silently dropping provider secrets. Officer includes read access.
15
+
16
+ @description('Short, kebab-case portal name.')
17
+ param portalName string
18
+
19
+ @description('Azure region.')
20
+ param location string
21
+
22
+ @description('Registry name (from registry.bicep) to grant AcrPull on.')
23
+ param registryName string
24
+
25
+ @description('Key Vault name (from keyVault.bicep) to grant Secrets User on.')
26
+ param keyVaultName string
27
+
28
+ resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
29
+ name: '${portalName}-platform-id'
30
+ location: location
31
+ }
32
+
33
+ resource registry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' existing = {
34
+ name: registryName
35
+ }
36
+
37
+ resource keyVault 'Microsoft.KeyVault/vaults@2024-04-01-preview' existing = {
38
+ name: keyVaultName
39
+ }
40
+
41
+ // Built-in role definition ids.
42
+ var acrPullRoleId = '7f951dda-4ed3-4680-a7ca-43fe172d538d'
43
+ // Key Vault Secrets Officer (read + write). Write is required so the control
44
+ // galaxy can persist provider secrets via setSecret(...) at runtime.
45
+ var keyVaultSecretsOfficerRoleId = 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7'
46
+
47
+ resource acrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
48
+ scope: registry
49
+ name: guid(registry.id, identity.id, acrPullRoleId)
50
+ properties: {
51
+ principalId: identity.properties.principalId
52
+ principalType: 'ServicePrincipal'
53
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', acrPullRoleId)
54
+ }
55
+ }
56
+
57
+ resource kvSecretsOfficer 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
58
+ scope: keyVault
59
+ name: guid(keyVault.id, identity.id, keyVaultSecretsOfficerRoleId)
60
+ properties: {
61
+ principalId: identity.properties.principalId
62
+ principalType: 'ServicePrincipal'
63
+ roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', keyVaultSecretsOfficerRoleId)
64
+ }
65
+ }
66
+
67
+ output identityId string = identity.id
68
+ output clientId string = identity.properties.clientId
69
+ output principalId string = identity.properties.principalId
@@ -0,0 +1,116 @@
1
+ // PostgreSQL Flexible Server. One server hosts two databases on its own
2
+ // schema: `backend` (the headless kernel: providers, models, policies, agents,
3
+ // galaxy registry, audit, spend) and `control_galaxy` (the admin portal's
4
+ // galaxy-local domains: connectors, branding, onboarding, users, settings).
5
+ // v1 ships with public-access + Azure-services allowlist for simplicity.
6
+ // Production should switch to private endpoint.
7
+ //
8
+ // SECURITY: the admin password never crosses a module boundary as an output.
9
+ // The module writes each runtime connection string directly into the existing
10
+ // Key Vault, keeping the secret value inside @secure() context for its entire
11
+ // lifetime. Outputs expose only the FQDN and the secret names.
12
+ //
13
+ // AAD-only auth is the recommended next step (flexibleServers/administrators
14
+ // with the platform managed identity) and removes the password entirely.
15
+
16
+ @description('Short name prefix for the portal.')
17
+ param portalName string
18
+
19
+ @description('Azure region.')
20
+ param location string
21
+
22
+ @description('Administrator login name.')
23
+ param administratorLogin string
24
+
25
+ @secure()
26
+ @description('Administrator password. Written straight to Key Vault, never surfaced as a module output.')
27
+ param administratorPassword string
28
+
29
+ @description('Compute SKU.')
30
+ param sku string
31
+
32
+ @description('Storage in GB.')
33
+ param storageGb int
34
+
35
+ @description('Postgres major version.')
36
+ param version string = '16'
37
+
38
+ @description('Name of the existing Key Vault to write the connection-string secrets into. Must be in the same resource group.')
39
+ param keyVaultName string
40
+
41
+ // Each entry becomes a database on the server and a connection-string secret
42
+ // in the vault. Backend and control galaxy keep separate schemas.
43
+ var databases = [
44
+ { name: 'backend', secret: 'postgres-connection-string-backend' }
45
+ { name: 'control_galaxy', secret: 'postgres-connection-string-control' }
46
+ ]
47
+
48
+ var serverName = '${portalName}-pg'
49
+
50
+ resource server 'Microsoft.DBforPostgreSQL/flexibleServers@2024-03-01-preview' = {
51
+ name: serverName
52
+ location: location
53
+ sku: {
54
+ name: sku
55
+ tier: startsWith(sku, 'Standard_B') ? 'Burstable' : 'GeneralPurpose'
56
+ }
57
+ properties: {
58
+ version: version
59
+ administratorLogin: administratorLogin
60
+ administratorLoginPassword: administratorPassword
61
+ storage: {
62
+ storageSizeGB: storageGb
63
+ autoGrow: 'Enabled'
64
+ }
65
+ backup: {
66
+ backupRetentionDays: 7
67
+ geoRedundantBackup: 'Disabled'
68
+ }
69
+ highAvailability: { mode: 'Disabled' }
70
+ network: {
71
+ publicNetworkAccess: 'Enabled'
72
+ }
73
+ }
74
+ }
75
+
76
+ resource allowAzureServices 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2024-03-01-preview' = {
77
+ parent: server
78
+ name: 'AllowAllAzureIps'
79
+ properties: {
80
+ startIpAddress: '0.0.0.0'
81
+ endIpAddress: '0.0.0.0'
82
+ }
83
+ }
84
+
85
+ resource pgDatabases 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2024-03-01-preview' = [
86
+ for db in databases: {
87
+ parent: server
88
+ name: db.name
89
+ properties: {
90
+ charset: 'UTF8'
91
+ collation: 'en_US.utf8'
92
+ }
93
+ }
94
+ ]
95
+
96
+ // Existing-vault reference + secret write. The vault is provisioned by
97
+ // modules/keyVault.bicep before this module runs (main.bicep wires the order).
98
+ // The secret values are interpolated INSIDE this module's @secure() scope,
99
+ // never crossing into an output.
100
+ resource vault 'Microsoft.KeyVault/vaults@2024-04-01-preview' existing = {
101
+ name: keyVaultName
102
+ }
103
+
104
+ resource connectionStringSecrets 'Microsoft.KeyVault/vaults/secrets@2024-04-01-preview' = [
105
+ for db in databases: {
106
+ parent: vault
107
+ name: db.secret
108
+ properties: {
109
+ value: 'postgresql://${administratorLogin}:${administratorPassword}@${server.properties.fullyQualifiedDomainName}:5432/${db.name}?sslmode=require'
110
+ }
111
+ }
112
+ ]
113
+
114
+ output fqdn string = server.properties.fullyQualifiedDomainName
115
+ output backendConnectionSecretName string = databases[0].secret
116
+ output controlConnectionSecretName string = databases[1].secret
@@ -0,0 +1,38 @@
1
+ // Azure Container Registry — the per-customer registry the platform images
2
+ // are pulled from at runtime. Fortytwo CI builds and pushes the backend (and
3
+ // control-galaxy) images to a central Fortytwo ACR; at deploy time those
4
+ // images are imported into this customer-owned registry (`az acr import`) so
5
+ // the Container Apps pull them over a managed identity with no stored secret.
6
+ //
7
+ // Admin user is disabled on purpose: pulls are identity-based (AcrPull granted
8
+ // in platformIdentity.bicep), never username/password.
9
+
10
+ @description('Short, kebab-case portal name. Used as a prefix; ACR names are alphanumeric only so the prefix is sanitized.')
11
+ @minLength(2)
12
+ param portalName string
13
+
14
+ @description('Azure region.')
15
+ param location string
16
+
17
+ @description('Registry SKU. Standard is the floor for production (webhooks, larger storage); Premium adds geo-replication + private link.')
18
+ @allowed([
19
+ 'Standard'
20
+ 'Premium'
21
+ ])
22
+ param sku string = 'Standard'
23
+
24
+ // ACR names: 5-50 chars, alphanumeric only, globally unique. Strip dashes.
25
+ var registryName = take('${replace(portalName, '-', '')}acr', 50)
26
+
27
+ resource registry 'Microsoft.ContainerRegistry/registries@2023-11-01-preview' = {
28
+ name: registryName
29
+ location: location
30
+ sku: { name: sku }
31
+ properties: {
32
+ adminUserEnabled: false
33
+ publicNetworkAccess: 'Enabled'
34
+ }
35
+ }
36
+
37
+ output registryName string = registry.name
38
+ output loginServer string = registry.properties.loginServer
@@ -0,0 +1,30 @@
1
+ // Storage account for future blob uploads (attachments, exports, audit-log archives).
2
+
3
+ @description('Short name prefix for the portal.')
4
+ param portalName string
5
+
6
+ @description('Azure region.')
7
+ param location string
8
+
9
+ // Collision-resistant, deterministic storage account name. The old
10
+ // take(...'${portalName}storage'..., 24) truncated the 'storage' suffix for long
11
+ // portalNames and could collide (account names are globally unique). Storage
12
+ // account names are 3-24 chars, lowercase alphanumeric only; uniqueString already
13
+ // returns lowercase alphanumeric. take(clean, 11) + 'st' (2) + take(unique, 11) = up to 24.
14
+ var portalNameClean = toLower(replace(portalName, '-', ''))
15
+ var accountName = '${take(portalNameClean, 11)}st${take(uniqueString(resourceGroup().id), 11)}'
16
+
17
+ resource storage 'Microsoft.Storage/storageAccounts@2024-01-01' = {
18
+ name: accountName
19
+ location: location
20
+ sku: { name: 'Standard_LRS' }
21
+ kind: 'StorageV2'
22
+ properties: {
23
+ minimumTlsVersion: 'TLS1_2'
24
+ supportsHttpsTrafficOnly: true
25
+ allowBlobPublicAccess: false
26
+ defaultToOAuthAuthentication: true
27
+ }
28
+ }
29
+
30
+ output accountName string = storage.name
package/bin/setup.mjs ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { runSetup } from '../dist/index.js';
3
+
4
+ runSetup(process.argv).catch((err) => {
5
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,9 @@
1
+ import type { EntraConfig } from './entra.js';
2
+ import type { AzureConfig } from './prompts.js';
3
+ export type BicepOutputs = {
4
+ backendUrl: string;
5
+ controlGalaxyUrl: string;
6
+ };
7
+ export declare function parseBicepOutputs(raw: string): BicepOutputs;
8
+ export declare function deployBicep(config: AzureConfig, entra: EntraConfig, githubInstallationId: string): Promise<BicepOutputs>;
9
+ //# sourceMappingURL=bicep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bicep.d.ts","sourceRoot":"","sources":["../../src/azure/bicep.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAKhD,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAWF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAc3D;AAED,wBAAsB,WAAW,CAC/B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,WAAW,EAClB,oBAAoB,EAAE,MAAM,GAC3B,OAAO,CAAC,YAAY,CAAC,CAqCvB"}