@aifabrix/builder 2.57.0 → 2.59.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 (106) hide show
  1. package/lib/agent-kit/codex-app-server.js +44 -0
  2. package/lib/agent-kit/init.js +4 -17
  3. package/lib/agent-kit/session-report.js +15 -10
  4. package/lib/agent-kit/setup-auth.js +131 -0
  5. package/lib/agent-kit/setup-context.js +118 -0
  6. package/lib/agent-kit/setup-github.js +70 -0
  7. package/lib/agent-kit/setup-managed.js +79 -0
  8. package/lib/agent-kit/setup-process.js +42 -0
  9. package/lib/agent-kit/setup-session.js +53 -0
  10. package/lib/agent-kit/setup-state.js +52 -0
  11. package/lib/agent-kit/setup-workspace.js +113 -0
  12. package/lib/agent-kit/setup.js +84 -0
  13. package/lib/agent-kit/start.js +4 -3
  14. package/lib/agent-kit/tmux-sessions.js +20 -3
  15. package/lib/api/dev.api.js +1 -1
  16. package/lib/api/miso-health.api.js +26 -0
  17. package/lib/api/types/dev.types.js +3 -3
  18. package/lib/app/deploy.js +0 -102
  19. package/lib/app/push.js +98 -41
  20. package/lib/build/standard-docker-build.js +13 -11
  21. package/lib/cli/index.js +4 -0
  22. package/lib/cli/setup-app.js +16 -5
  23. package/lib/cli/setup-dev-path-commands.js +53 -2
  24. package/lib/cli/setup-dev.js +7 -4
  25. package/lib/cli/setup-environment.js +17 -4
  26. package/lib/cli/setup-help.js +86 -0
  27. package/lib/cli/setup-onboarding.js +37 -0
  28. package/lib/cli/setup-utility-repair.js +7 -5
  29. package/lib/commands/agent-kit-help.js +21 -7
  30. package/lib/commands/agent-kit-sessions.js +126 -0
  31. package/lib/commands/agent-kit-setup.js +44 -0
  32. package/lib/commands/agent-kit.js +5 -79
  33. package/lib/commands/dev-init.js +21 -23
  34. package/lib/commands/dev-refresh.js +2 -2
  35. package/lib/commands/dev-registry.js +155 -0
  36. package/lib/commands/dev-show-display.js +10 -2
  37. package/lib/commands/governance-verify-external.js +12 -5
  38. package/lib/commands/repair-datasource.js +9 -9
  39. package/lib/commands/repair.js +45 -8
  40. package/lib/commands/setup-image-refresh.js +26 -0
  41. package/lib/commands/upload-qualification-client.js +56 -11
  42. package/lib/commands/upload-qualification-poll.js +51 -22
  43. package/lib/commands/wizard-dataplane.js +16 -52
  44. package/lib/core/config-attach-extensions.js +4 -1
  45. package/lib/datasource/datasource-validate-summary.js +8 -5
  46. package/lib/datasource/sync-defaults.js +53 -0
  47. package/lib/deployment/installation/azure-infra-stage.js +50 -9
  48. package/lib/deployment/installation/azure-preflight.js +63 -7
  49. package/lib/deployment/installation/azure-readiness.js +59 -7
  50. package/lib/deployment/installation/environment-auth.js +60 -20
  51. package/lib/deployment/installation/environment-stage.js +9 -5
  52. package/lib/deployment/installation/infra-catalog.js +57 -5
  53. package/lib/deployment/installation/infra-manifest.js +22 -2
  54. package/lib/deployment/installation/infra-projection.js +14 -3
  55. package/lib/deployment/installation/registry-auth-mode.js +121 -0
  56. package/lib/deployment/installation/stage-input.js +21 -6
  57. package/lib/deployment/marketplace-environment.js +26 -1
  58. package/lib/generator/builders.js +8 -9
  59. package/lib/generator/index.js +23 -12
  60. package/lib/integration-definition/apply.js +2 -2
  61. package/lib/internal/fs-real-sync.js +4 -1
  62. package/lib/programmatic/bearer-auth.js +3 -62
  63. package/lib/programmatic/builder-api-startup.js +51 -0
  64. package/lib/programmatic/builder-miso-runtime.js +167 -0
  65. package/lib/programmatic/openapi-descriptions.js +3 -3
  66. package/lib/programmatic/openapi-schema-descriptions-core.js +3 -3
  67. package/lib/programmatic/openapi-spec.js +2 -1
  68. package/lib/programmatic/route-handler-map.js +3 -2
  69. package/lib/schema/environment-deploy-request.schema.json +1 -1
  70. package/lib/schema/external-datasource.schema.json +20 -5
  71. package/lib/schema/infra-parameter.schema.json +1 -1
  72. package/lib/schema/infra.parameter.yaml +2 -1
  73. package/lib/schema/infrastructure-schema.json +200 -3
  74. package/lib/schema/installation-environment.schema.json +1 -1
  75. package/lib/schema/wizard-config.schema.json +1 -1
  76. package/lib/utils/app-register-config.js +2 -4
  77. package/lib/utils/build-resolve-image.js +2 -21
  78. package/lib/utils/cli-utils.js +8 -1
  79. package/lib/utils/config-paths.js +24 -29
  80. package/lib/utils/config-registry-preference.js +113 -0
  81. package/lib/utils/dev-init-ssh-merge.js +5 -4
  82. package/lib/utils/dev-user-groups.js +2 -1
  83. package/lib/utils/docker-build.js +53 -19
  84. package/lib/utils/docker-daemon-tls-ca.js +2 -2
  85. package/lib/utils/env-map.js +23 -6
  86. package/lib/utils/error-formatters/validation-errors.js +25 -5
  87. package/lib/utils/help-builder.js +4 -3
  88. package/lib/utils/image-tags.js +88 -0
  89. package/lib/utils/promote-dev-docker-image.js +6 -0
  90. package/lib/utils/registry-credentials.js +236 -0
  91. package/lib/utils/resolve-docker-image-ref.js +78 -9
  92. package/lib/utils/secrets-helpers.js +9 -1
  93. package/lib/utils/token-manager.js +29 -36
  94. package/lib/utils/variable-transformer.js +2 -4
  95. package/package.json +4 -2
  96. package/templates/agent-kit/instructions/AGENTKIT.md +39 -2
  97. package/templates/agent-kit/instructions/root.AGENTS.md +40 -1
  98. package/templates/agent-kit/skills/shared/hosts.md +23 -5
  99. package/templates/agent-kit/workspace/README.md +14 -1
  100. package/templates/applications/builder-api/application.yaml +1 -1
  101. package/templates/applications/dataplane/application.yaml +1 -1
  102. package/templates/applications/dataplane/env.template +1 -1
  103. package/templates/applications/miso-controller/application.yaml +1 -1
  104. package/templates/applications/miso-controller/env.template +1 -1
  105. package/templates/external-system/external-datasource.yaml.hbs +7 -6
  106. package/templates/marketplace/main.json +440 -82
@@ -5,7 +5,7 @@
5
5
  "_generator": {
6
6
  "name": "bicep",
7
7
  "version": "0.47.16.16243",
8
- "templateHash": "12207364571048202485"
8
+ "templateHash": "15679444522742934591"
9
9
  }
10
10
  },
11
11
  "parameters": {
@@ -23,6 +23,19 @@
23
23
  "description": "Azure region for deployment. Default is the managed resource group location; portal may override via createUiDefinition."
24
24
  }
25
25
  },
26
+ "capacityProfile": {
27
+ "type": "string",
28
+ "defaultValue": "s",
29
+ "allowedValues": [
30
+ "s",
31
+ "m",
32
+ "l",
33
+ "xl"
34
+ ],
35
+ "metadata": {
36
+ "description": "Baseline capacity of the Miso/Keycloak foundation: s (B3), m (P2v3), l (P3v3), xl (P3mv3). One compute instance for every profile. Select the purchased capacity independently of licenseEdition; this does not provision workload environment topology."
37
+ }
38
+ },
26
39
  "tags": {
27
40
  "type": "object",
28
41
  "defaultValue": {
@@ -82,7 +95,11 @@
82
95
  "deploymentEnvironment": {
83
96
  "type": "string",
84
97
  "defaultValue": "evaluation",
85
- "allowedValues": ["prod", "nonprod", "evaluation"],
98
+ "allowedValues": [
99
+ "prod",
100
+ "nonprod",
101
+ "evaluation"
102
+ ],
86
103
  "metadata": {
87
104
  "description": "Deployment environment used for app runtime defaults. MP UI hardcodes evaluation for now."
88
105
  }
@@ -133,7 +150,14 @@
133
150
  "type": "bool",
134
151
  "defaultValue": true,
135
152
  "metadata": {
136
- "description": "When false, skip ARM role assignments (CI/CD redeploy without roleAssignments/write). RBAC must already exist."
153
+ "description": "When false, skip ARM role assignments in THIS resource group (CI/CD redeploy without roleAssignments/write). RBAC must already exist."
154
+ }
155
+ },
156
+ "deployRegistryRoleAssignment": {
157
+ "type": "bool",
158
+ "defaultValue": "[parameters('deployRoleAssignments')]",
159
+ "metadata": {
160
+ "description": "When false, skip the AcrPull grant on the container registry. That grant lives in the registry's own resource group, which for a customer installation is in the vendor's tenant, where Azure RBAC cannot reach. An installation deployed with registry credentials sets this false and keeps the grants in its own resource group, which it does own."
137
161
  }
138
162
  },
139
163
  "moriBaseUrl": {
@@ -143,6 +167,20 @@
143
167
  "description": "Mori base URL for controller tenant activation and Entra handoff (no trailing slash)"
144
168
  }
145
169
  },
170
+ "moriLicensePublicKey": {
171
+ "type": "string",
172
+ "defaultValue": "",
173
+ "metadata": {
174
+ "description": "Mori licence issuer public key in SPKI PEM form. Empty leaves the installation unpinned, and Miso then refuses a licence rather than trusting a key carried inside it."
175
+ }
176
+ },
177
+ "moriLicenseIssuer": {
178
+ "type": "string",
179
+ "defaultValue": "mori-controller",
180
+ "metadata": {
181
+ "description": "Expected `iss` claim on a Mori licence."
182
+ }
183
+ },
146
184
  "moriCallbackApiKey": {
147
185
  "type": "securestring",
148
186
  "defaultValue": "",
@@ -202,7 +240,10 @@
202
240
  "onboardingMode": {
203
241
  "type": "string",
204
242
  "defaultValue": "customerOwned",
205
- "allowedValues": ["customerOwned", "saasDirect"],
243
+ "allowedValues": [
244
+ "customerOwned",
245
+ "saasDirect"
246
+ ],
206
247
  "metadata": {
207
248
  "description": "Tenant onboarding mode for Miso (ONBOARDING_MODE). customerOwned = portal/Marketplace wizard; saasDirect = Builder SaaS installation. Not exposed in createUiDefinition."
208
249
  }
@@ -210,15 +251,44 @@
210
251
  },
211
252
  "variables": {
212
253
  "infrastructureName": "[if(not(empty(parameters('infrastructureNameOverride'))), parameters('infrastructureNameOverride'), resourceGroup().name)]",
254
+ "capacityProfiles": {
255
+ "s": {
256
+ "computeSku": "B3",
257
+ "postgresSku": "Standard_B2ms",
258
+ "postgresTier": "Burstable",
259
+ "postgresStorageGB": 64
260
+ },
261
+ "m": {
262
+ "computeSku": "P2v3",
263
+ "postgresSku": "Standard_D2ds_v5",
264
+ "postgresTier": "GeneralPurpose",
265
+ "postgresStorageGB": 128
266
+ },
267
+ "l": {
268
+ "computeSku": "P3v3",
269
+ "postgresSku": "Standard_D4ds_v5",
270
+ "postgresTier": "GeneralPurpose",
271
+ "postgresStorageGB": 256
272
+ },
273
+ "xl": {
274
+ "computeSku": "P3mv3",
275
+ "postgresSku": "Standard_D8ds_v5",
276
+ "postgresTier": "GeneralPurpose",
277
+ "postgresStorageGB": 512
278
+ }
279
+ },
280
+ "selectedCapacity": "[variables('capacityProfiles')[parameters('capacityProfile')]]",
213
281
  "deploymentLocation": "[parameters('location')]",
214
282
  "appInsightsLocation": "[if(equals(variables('deploymentLocation'), 'norwaywest'), 'norwayeast', variables('deploymentLocation'))]",
215
283
  "openAiLocationKnown": "[contains(createArray('australiaeast', 'brazilsouth', 'canadaeast', 'eastus', 'eastus2', 'francecentral', 'japaneast', 'koreacentral', 'northcentralus', 'norwayeast', 'polandcentral', 'southafricanorth', 'southcentralus', 'southindia', 'swedencentral', 'switzerlandnorth', 'uksouth', 'westeurope', 'westus', 'westus3'), variables('deploymentLocation'))]",
216
- "openAiLocation": "[if(variables('openAiLocationKnown'), variables('deploymentLocation'), 'swedencentral')]",
284
+ "createAzureOpenAI": "[and(and(parameters('enableAzureOpenAI'), variables('openAiLocationKnown')), not(variables('hostedInstallation')))]",
217
285
  "subnetBase": "[add(20, mod(length(variables('infrastructureName')), 230))]",
218
286
  "registryPasswordConfigured": "[not(empty(parameters('acrAdminPassword')))]",
219
287
  "acrRegistryName": "[toLower(last(split(parameters('acrResourceId'), '/')))]",
220
288
  "effectiveAcrLoginServer": "[if(empty(parameters('acrLoginServer')), format('{0}{1}', variables('acrRegistryName'), environment().suffixes.acrLoginServer), parameters('acrLoginServer'))]",
221
289
  "registryUsername": "[if(variables('registryPasswordConfigured'), if(empty(parameters('acrAdminUsername')), variables('acrRegistryName'), parameters('acrAdminUsername')), '')]",
290
+ "hostedInstallation": "[equals(parameters('onboardingMode'), 'saasDirect')]",
291
+ "effectiveEntraIdTenantId": "[if(variables('hostedInstallation'), '', parameters('entraIdTenantId'))]",
222
292
  "keycloakContainerImage": "[format('{0}/aifabrix/keycloak:{1}', variables('effectiveAcrLoginServer'), parameters('keycloakImageTag'))]",
223
293
  "misoControllerContainerImage": "[format('{0}/aifabrix/miso-controller:{1}', variables('effectiveAcrLoginServer'), parameters('misoImageTag'))]",
224
294
  "keycloakVdirPublic": "/auth",
@@ -369,7 +439,9 @@
369
439
  "tags": "[parameters('tags')]",
370
440
  "properties": {
371
441
  "addressSpace": {
372
- "addressPrefixes": ["[parameters('vnetAddressSpace')]"]
442
+ "addressPrefixes": [
443
+ "[parameters('vnetAddressSpace')]"
444
+ ]
373
445
  },
374
446
  "subnets": [
375
447
  {
@@ -721,7 +793,9 @@
721
793
  "value": "[reference(resourceId('Microsoft.Resources/deployments', 'network'), '2025-04-01').outputs.keyVaultPrivateDnsZoneId.value]"
722
794
  },
723
795
  "allowedIPs": {
724
- "value": ["*"]
796
+ "value": [
797
+ "*"
798
+ ]
725
799
  },
726
800
  "currentUserObjectId": {
727
801
  "value": ""
@@ -801,7 +875,10 @@
801
875
  "sku": {
802
876
  "type": "string",
803
877
  "defaultValue": "standard",
804
- "allowedValues": ["standard", "premium"],
878
+ "allowedValues": [
879
+ "standard",
880
+ "premium"
881
+ ],
805
882
  "metadata": {
806
883
  "description": "Key Vault SKU (standard or premium)"
807
884
  }
@@ -823,7 +900,11 @@
823
900
  "tenantId": "[subscription().tenantId]",
824
901
  "objectId": "[parameters('deploymentIdentityObjectIds')[copyIndex('deploymentIdentityPolicies')]]",
825
902
  "permissions": {
826
- "secrets": ["get", "list", "set"],
903
+ "secrets": [
904
+ "get",
905
+ "list",
906
+ "set"
907
+ ],
827
908
  "keys": [],
828
909
  "certificates": [],
829
910
  "storage": []
@@ -878,7 +959,9 @@
878
959
  "properties": {
879
960
  "accessPolicies": "[variables('currentUserPolicy')]"
880
961
  },
881
- "dependsOn": ["[resourceId('Microsoft.KeyVault/vaults', parameters('name'))]"]
962
+ "dependsOn": [
963
+ "[resourceId('Microsoft.KeyVault/vaults', parameters('name'))]"
964
+ ]
882
965
  },
883
966
  {
884
967
  "condition": "[and(not(empty(variables('deploymentIdentityPolicies'))), not(variables('shouldGrantCurrentUserPolicy')))]",
@@ -888,7 +971,9 @@
888
971
  "properties": {
889
972
  "accessPolicies": "[variables('deploymentIdentityPolicies')]"
890
973
  },
891
- "dependsOn": ["[resourceId('Microsoft.KeyVault/vaults', parameters('name'))]"]
974
+ "dependsOn": [
975
+ "[resourceId('Microsoft.KeyVault/vaults', parameters('name'))]"
976
+ ]
892
977
  },
893
978
  {
894
979
  "condition": "[and(variables('shouldGrantCurrentUserPolicy'), not(empty(variables('deploymentIdentityPolicies'))))]",
@@ -898,7 +983,9 @@
898
983
  "properties": {
899
984
  "accessPolicies": "[concat(variables('currentUserPolicy'), variables('deploymentIdentityPolicies'))]"
900
985
  },
901
- "dependsOn": ["[resourceId('Microsoft.KeyVault/vaults', parameters('name'))]"]
986
+ "dependsOn": [
987
+ "[resourceId('Microsoft.KeyVault/vaults', parameters('name'))]"
988
+ ]
902
989
  },
903
990
  {
904
991
  "type": "Microsoft.Network/privateEndpoints",
@@ -916,12 +1003,16 @@
916
1003
  "name": "kv-pls",
917
1004
  "properties": {
918
1005
  "privateLinkServiceId": "[resourceId('Microsoft.KeyVault/vaults', parameters('name'))]",
919
- "groupIds": ["vault"]
1006
+ "groupIds": [
1007
+ "vault"
1008
+ ]
920
1009
  }
921
1010
  }
922
1011
  ]
923
1012
  },
924
- "dependsOn": ["[resourceId('Microsoft.KeyVault/vaults', parameters('name'))]"]
1013
+ "dependsOn": [
1014
+ "[resourceId('Microsoft.KeyVault/vaults', parameters('name'))]"
1015
+ ]
925
1016
  },
926
1017
  {
927
1018
  "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
@@ -973,7 +1064,7 @@
973
1064
  ]
974
1065
  },
975
1066
  {
976
- "condition": "[parameters('enableAzureOpenAI')]",
1067
+ "condition": "[variables('createAzureOpenAI')]",
977
1068
  "type": "Microsoft.Resources/deployments",
978
1069
  "apiVersion": "2025-04-01",
979
1070
  "name": "azureOpenAI",
@@ -987,7 +1078,7 @@
987
1078
  "value": "[variables('infrastructureName')]"
988
1079
  },
989
1080
  "location": {
990
- "value": "[variables('openAiLocation')]"
1081
+ "value": "[variables('deploymentLocation')]"
991
1082
  },
992
1083
  "tags": {
993
1084
  "value": "[parameters('tags')]"
@@ -1087,7 +1178,9 @@
1087
1178
  }
1088
1179
  }
1089
1180
  },
1090
- "dependsOn": ["[resourceId('Microsoft.Resources/deployments', 'keyVault')]"]
1181
+ "dependsOn": [
1182
+ "[resourceId('Microsoft.Resources/deployments', 'keyVault')]"
1183
+ ]
1091
1184
  },
1092
1185
  {
1093
1186
  "type": "Microsoft.Resources/deployments",
@@ -1151,7 +1244,12 @@
1151
1244
  "sku": {
1152
1245
  "type": "string",
1153
1246
  "defaultValue": "Standard_LRS",
1154
- "allowedValues": ["Standard_LRS", "Standard_GRS", "Standard_RAGRS", "Premium_LRS"],
1247
+ "allowedValues": [
1248
+ "Standard_LRS",
1249
+ "Standard_GRS",
1250
+ "Standard_RAGRS",
1251
+ "Premium_LRS"
1252
+ ],
1155
1253
  "metadata": {
1156
1254
  "description": "Storage account SKU (e.g., Standard_LRS, Standard_GRS, Premium_LRS)"
1157
1255
  }
@@ -1255,7 +1353,9 @@
1255
1353
  "apiVersion": "2024-01-01",
1256
1354
  "name": "[format('{0}/{1}', parameters('name'), 'default')]",
1257
1355
  "properties": {},
1258
- "dependsOn": ["[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]"]
1356
+ "dependsOn": [
1357
+ "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]"
1358
+ ]
1259
1359
  },
1260
1360
  {
1261
1361
  "type": "Microsoft.Storage/storageAccounts/blobServices/containers",
@@ -1273,7 +1373,9 @@
1273
1373
  "type": "Microsoft.Storage/storageAccounts/fileServices",
1274
1374
  "apiVersion": "2021-04-01",
1275
1375
  "name": "[format('{0}/{1}', parameters('name'), 'default')]",
1276
- "dependsOn": ["[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]"]
1376
+ "dependsOn": [
1377
+ "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]"
1378
+ ]
1277
1379
  },
1278
1380
  {
1279
1381
  "type": "Microsoft.Storage/storageAccounts/fileServices/shares",
@@ -1302,12 +1404,16 @@
1302
1404
  "name": "blob-pls",
1303
1405
  "properties": {
1304
1406
  "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]",
1305
- "groupIds": ["blob"]
1407
+ "groupIds": [
1408
+ "blob"
1409
+ ]
1306
1410
  }
1307
1411
  }
1308
1412
  ]
1309
1413
  },
1310
- "dependsOn": ["[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]"]
1414
+ "dependsOn": [
1415
+ "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]"
1416
+ ]
1311
1417
  },
1312
1418
  {
1313
1419
  "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
@@ -1343,12 +1449,16 @@
1343
1449
  "name": "file-pls",
1344
1450
  "properties": {
1345
1451
  "privateLinkServiceId": "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]",
1346
- "groupIds": ["file"]
1452
+ "groupIds": [
1453
+ "file"
1454
+ ]
1347
1455
  }
1348
1456
  }
1349
1457
  ]
1350
1458
  },
1351
- "dependsOn": ["[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]"]
1459
+ "dependsOn": [
1460
+ "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]"
1461
+ ]
1352
1462
  },
1353
1463
  {
1354
1464
  "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups",
@@ -1375,7 +1485,9 @@
1375
1485
  "properties": {
1376
1486
  "value": "[listKeys(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '2024-01-01').keys[0].value]"
1377
1487
  },
1378
- "dependsOn": ["[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]"]
1488
+ "dependsOn": [
1489
+ "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]"
1490
+ ]
1379
1491
  }
1380
1492
  ],
1381
1493
  "outputs": {
@@ -1432,10 +1544,10 @@
1432
1544
  "value": "[format('{0}-miso-asp', variables('infrastructureName'))]"
1433
1545
  },
1434
1546
  "skuDisplay": {
1435
- "value": "B2"
1547
+ "value": "[variables('selectedCapacity').computeSku]"
1436
1548
  },
1437
1549
  "capacity": {
1438
- "value": 2
1550
+ "value": 1
1439
1551
  },
1440
1552
  "tags": {
1441
1553
  "value": "[parameters('tags')]"
@@ -1448,7 +1560,7 @@
1448
1560
  "_generator": {
1449
1561
  "name": "bicep",
1450
1562
  "version": "0.47.16.16243",
1451
- "templateHash": "4988795866576200937"
1563
+ "templateHash": "11613691335280989070"
1452
1564
  }
1453
1565
  },
1454
1566
  "parameters": {
@@ -1471,15 +1583,10 @@
1471
1583
  "B1",
1472
1584
  "B2",
1473
1585
  "B3",
1474
- "S1",
1475
- "S2",
1476
- "S3",
1477
1586
  "P1v3",
1478
1587
  "P2v3",
1479
1588
  "P3v3",
1480
- "EP1",
1481
- "EP2",
1482
- "EP3"
1589
+ "P3mv3"
1483
1590
  ],
1484
1591
  "metadata": {
1485
1592
  "description": "Select a Linux-compatible App Service Plan SKU with details"
@@ -1507,15 +1614,10 @@
1507
1614
  "B1": "Basic",
1508
1615
  "B2": "Basic",
1509
1616
  "B3": "Basic",
1510
- "S1": "Standard",
1511
- "S2": "Standard",
1512
- "S3": "Standard",
1513
1617
  "P1v3": "PremiumV3",
1514
1618
  "P2v3": "PremiumV3",
1515
1619
  "P3v3": "PremiumV3",
1516
- "EP1": "ElasticPremium",
1517
- "EP2": "ElasticPremium",
1518
- "EP3": "ElasticPremium"
1620
+ "P3mv3": "PremiumV3"
1519
1621
  }
1520
1622
  },
1521
1623
  "resources": [
@@ -1727,7 +1829,9 @@
1727
1829
  }
1728
1830
  }
1729
1831
  },
1730
- "dependsOn": ["[resourceId('Microsoft.Resources/deployments', 'keyVault')]"]
1832
+ "dependsOn": [
1833
+ "[resourceId('Microsoft.Resources/deployments', 'keyVault')]"
1834
+ ]
1731
1835
  },
1732
1836
  {
1733
1837
  "type": "Microsoft.Resources/deployments",
@@ -1755,13 +1859,13 @@
1755
1859
  "value": "15"
1756
1860
  },
1757
1861
  "skuName": {
1758
- "value": "Standard_B2ms"
1862
+ "value": "[variables('selectedCapacity').postgresSku]"
1759
1863
  },
1760
1864
  "skuTier": {
1761
- "value": "Burstable"
1865
+ "value": "[variables('selectedCapacity').postgresTier]"
1762
1866
  },
1763
1867
  "storageSizeGB": {
1764
- "value": 64
1868
+ "value": "[variables('selectedCapacity').postgresStorageGB]"
1765
1869
  },
1766
1870
  "subnetId": {
1767
1871
  "value": "[reference(resourceId('Microsoft.Resources/deployments', 'network'), '2025-04-01').outputs.postgresSubnetId.value]"
@@ -1851,7 +1955,11 @@
1851
1955
  "skuTier": {
1852
1956
  "type": "string",
1853
1957
  "defaultValue": "Burstable",
1854
- "allowedValues": ["Burstable", "GeneralPurpose", "MemoryOptimized"],
1958
+ "allowedValues": [
1959
+ "Burstable",
1960
+ "GeneralPurpose",
1961
+ "MemoryOptimized"
1962
+ ],
1855
1963
  "metadata": {
1856
1964
  "description": "SKU tier for PostgreSQL Flexible Server"
1857
1965
  }
@@ -2190,7 +2298,7 @@
2190
2298
  "_generator": {
2191
2299
  "name": "bicep",
2192
2300
  "version": "0.47.16.16243",
2193
- "templateHash": "6101318472045983896"
2301
+ "templateHash": "10929433789276981779"
2194
2302
  }
2195
2303
  },
2196
2304
  "parameters": {
@@ -2379,7 +2487,7 @@
2379
2487
  "virtualNetworkSubnetId": "[if(not(empty(parameters('vnetSubnetId'))), parameters('vnetSubnetId'), null())]",
2380
2488
  "siteConfig": {
2381
2489
  "linuxFxVersion": "[format('DOCKER|{0}', parameters('image'))]",
2382
- "acrUseManagedIdentityCreds": true,
2490
+ "acrUseManagedIdentityCreds": "[not(variables('registryPasswordConfigured'))]",
2383
2491
  "appCommandLine": "start --optimized=false",
2384
2492
  "healthCheckPath": "[variables('keycloakHealthCheckPath')]",
2385
2493
  "alwaysOn": true,
@@ -2418,7 +2526,9 @@
2418
2526
  "enabled": true
2419
2527
  }
2420
2528
  },
2421
- "dependsOn": ["[resourceId('Microsoft.Web/sites', parameters('name'))]"]
2529
+ "dependsOn": [
2530
+ "[resourceId('Microsoft.Web/sites', parameters('name'))]"
2531
+ ]
2422
2532
  }
2423
2533
  ],
2424
2534
  "outputs": {
@@ -2491,6 +2601,21 @@
2491
2601
  },
2492
2602
  "encryptionKey": {
2493
2603
  "value": "[reference(resourceId('Microsoft.Resources/deployments', 'bootstrapSecrets'), '2025-04-01').outputs.encryptionKey.value]"
2604
+ },
2605
+ "entraCallbackStateSecret": {
2606
+ "value": "[parameters('entraCallbackStateSecret')]"
2607
+ },
2608
+ "entraIdClientSecret": {
2609
+ "value": "[parameters('entraIdClientSecret')]"
2610
+ },
2611
+ "moriLicensePublicKey": {
2612
+ "value": "[parameters('moriLicensePublicKey')]"
2613
+ },
2614
+ "moriBaseUrl": {
2615
+ "value": "[parameters('moriBaseUrl')]"
2616
+ },
2617
+ "moriCallbackApiKey": {
2618
+ "value": "[parameters('moriCallbackApiKey')]"
2494
2619
  }
2495
2620
  },
2496
2621
  "template": {
@@ -2500,7 +2625,7 @@
2500
2625
  "_generator": {
2501
2626
  "name": "bicep",
2502
2627
  "version": "0.47.16.16243",
2503
- "templateHash": "9394845529752045254"
2628
+ "templateHash": "9634353332764264230"
2504
2629
  }
2505
2630
  },
2506
2631
  "parameters": {
@@ -2568,9 +2693,46 @@
2568
2693
  "metadata": {
2569
2694
  "description": "MISO Client Secret (auto-generated if not provided)"
2570
2695
  }
2696
+ },
2697
+ "entraCallbackStateSecret": {
2698
+ "type": "securestring",
2699
+ "defaultValue": "",
2700
+ "metadata": {
2701
+ "description": "Shared HMAC secret for Entra consent OAuth state (ENTRA_CALLBACK_STATE_SECRET). Generated when empty."
2702
+ }
2703
+ },
2704
+ "entraIdClientSecret": {
2705
+ "type": "securestring",
2706
+ "defaultValue": "",
2707
+ "metadata": {
2708
+ "description": "Vendor Entra app client secret (ENTRA_ID_CLIENT_SECRET). Placeholder when empty — replace for live Entra flows."
2709
+ }
2710
+ },
2711
+ "moriLicensePublicKey": {
2712
+ "type": "string",
2713
+ "defaultValue": "",
2714
+ "metadata": {
2715
+ "description": "Mori licence issuer public key (SPKI PEM). Empty leaves the installation unpinned."
2716
+ }
2717
+ },
2718
+ "moriBaseUrl": {
2719
+ "type": "string",
2720
+ "defaultValue": "",
2721
+ "metadata": {
2722
+ "description": "Mori base URL, stored so App Service can reference it instead of holding a literal."
2723
+ }
2724
+ },
2725
+ "moriCallbackApiKey": {
2726
+ "type": "securestring",
2727
+ "defaultValue": "",
2728
+ "metadata": {
2729
+ "description": "Mori callback API key, shared by the Miso Controller and Mori."
2730
+ }
2571
2731
  }
2572
2732
  },
2573
2733
  "variables": {
2734
+ "resolvedEntraCallbackStateSecret": "[if(not(empty(parameters('entraCallbackStateSecret'))), parameters('entraCallbackStateSecret'), format('ecs_{0}{1}', uniqueString(resourceGroup().id, parameters('prefix'), 'entra-callback-state-v1'), uniqueString(parameters('prefix'), 'entra-cb-b')))]",
2735
+ "resolvedEntraIdClientSecret": "[if(not(empty(parameters('entraIdClientSecret'))), parameters('entraIdClientSecret'), format('PLACEHOLDER_ENTRA_CLIENT_SECRET_MUST_BE_REPLACED_{0}', uniqueString(resourceGroup().id, parameters('prefix'))))]",
2574
2736
  "misoWebAppOrigin": "[if(endsWith(parameters('misoWebAppUrl'), '/'), substring(parameters('misoWebAppUrl'), 0, max(0, sub(length(parameters('misoWebAppUrl')), 1))), parameters('misoWebAppUrl'))]",
2575
2737
  "misoVdirSegment": "[if(or(equals(parameters('misoVdirPublic'), ''), equals(parameters('misoVdirPublic'), '/')), '', if(startsWith(parameters('misoVdirPublic'), '/'), parameters('misoVdirPublic'), format('/{0}', parameters('misoVdirPublic'))))]",
2576
2738
  "misoWebServerPublicUrl": "[if(empty(variables('misoVdirSegment')), variables('misoWebAppOrigin'), format('{0}{1}', variables('misoWebAppOrigin'), variables('misoVdirSegment')))]",
@@ -2660,6 +2822,49 @@
2660
2822
  "properties": {
2661
2823
  "value": "[if(not(empty(parameters('misoClientSecret'))), parameters('misoClientSecret'), format('PLACEHOLDER_CLIENT_SECRET_MUST_BE_REPLACED_{0}', uniqueString(resourceGroup().id, parameters('prefix'))))]"
2662
2824
  }
2825
+ },
2826
+ {
2827
+ "type": "Microsoft.KeyVault/vaults/secrets",
2828
+ "apiVersion": "2023-07-01",
2829
+ "name": "[format('{0}/{1}', parameters('keyVaultName'), 'entra-callback-state-secret')]",
2830
+ "properties": {
2831
+ "value": "[variables('resolvedEntraCallbackStateSecret')]"
2832
+ }
2833
+ },
2834
+ {
2835
+ "type": "Microsoft.KeyVault/vaults/secrets",
2836
+ "apiVersion": "2023-07-01",
2837
+ "name": "[format('{0}/{1}', parameters('keyVaultName'), 'entra-client-secretKeyVault')]",
2838
+ "properties": {
2839
+ "value": "[variables('resolvedEntraIdClientSecret')]"
2840
+ }
2841
+ },
2842
+ {
2843
+ "condition": "[not(empty(parameters('moriLicensePublicKey')))]",
2844
+ "type": "Microsoft.KeyVault/vaults/secrets",
2845
+ "apiVersion": "2023-07-01",
2846
+ "name": "[format('{0}/{1}', parameters('keyVaultName'), 'mori-license-public-keyKeyVault')]",
2847
+ "properties": {
2848
+ "value": "[parameters('moriLicensePublicKey')]"
2849
+ }
2850
+ },
2851
+ {
2852
+ "condition": "[not(empty(parameters('moriBaseUrl')))]",
2853
+ "type": "Microsoft.KeyVault/vaults/secrets",
2854
+ "apiVersion": "2023-07-01",
2855
+ "name": "[format('{0}/{1}', parameters('keyVaultName'), 'mori-base-urlKeyVault')]",
2856
+ "properties": {
2857
+ "value": "[parameters('moriBaseUrl')]"
2858
+ }
2859
+ },
2860
+ {
2861
+ "condition": "[not(empty(parameters('moriCallbackApiKey')))]",
2862
+ "type": "Microsoft.KeyVault/vaults/secrets",
2863
+ "apiVersion": "2023-07-01",
2864
+ "name": "[format('{0}/{1}', parameters('keyVaultName'), 'mori-callback-api-keyKeyVault')]",
2865
+ "properties": {
2866
+ "value": "[parameters('moriCallbackApiKey')]"
2867
+ }
2663
2868
  }
2664
2869
  ]
2665
2870
  }
@@ -2710,6 +2915,12 @@
2710
2915
  "keycloakWebAppUrl": {
2711
2916
  "value": "[reference(resourceId('Microsoft.Resources/deployments', 'keycloakWebApp'), '2025-04-01').outputs.webAppUrl.value]"
2712
2917
  },
2918
+ "keycloakVdirPublic": {
2919
+ "value": "[variables('keycloakVdirPublic')]"
2920
+ },
2921
+ "misoVdirPublic": {
2922
+ "value": "/miso"
2923
+ },
2713
2924
  "keycloakRealm": {
2714
2925
  "value": "aifabrix"
2715
2926
  },
@@ -2755,20 +2966,20 @@
2755
2966
  "moriBaseUrl": {
2756
2967
  "value": "[parameters('moriBaseUrl')]"
2757
2968
  },
2969
+ "moriLicensePublicKey": {
2970
+ "value": "[parameters('moriLicensePublicKey')]"
2971
+ },
2972
+ "moriLicenseIssuer": {
2973
+ "value": "[parameters('moriLicenseIssuer')]"
2974
+ },
2758
2975
  "moriCallbackApiKey": {
2759
2976
  "value": "[parameters('moriCallbackApiKey')]"
2760
2977
  },
2761
- "entraCallbackStateSecret": {
2762
- "value": "[parameters('entraCallbackStateSecret')]"
2763
- },
2764
2978
  "entraIdClientId": {
2765
2979
  "value": "[parameters('entraIdClientId')]"
2766
2980
  },
2767
- "entraIdClientSecret": {
2768
- "value": "[parameters('entraIdClientSecret')]"
2769
- },
2770
2981
  "entraIdTenantId": {
2771
- "value": "[parameters('entraIdTenantId')]"
2982
+ "value": "[variables('effectiveEntraIdTenantId')]"
2772
2983
  },
2773
2984
  "licenseEdition": {
2774
2985
  "value": "[parameters('licenseEdition')]"
@@ -2790,7 +3001,7 @@
2790
3001
  "_generator": {
2791
3002
  "name": "bicep",
2792
3003
  "version": "0.47.16.16243",
2793
- "templateHash": "3326510661393380434"
3004
+ "templateHash": "2563187238594401937"
2794
3005
  }
2795
3006
  },
2796
3007
  "parameters": {
@@ -2943,6 +3154,20 @@
2943
3154
  "description": "MISO controller web app URL (for MISO_WEB_SERVER_URL)"
2944
3155
  }
2945
3156
  },
3157
+ "keycloakVdirPublic": {
3158
+ "type": "string",
3159
+ "defaultValue": "/auth",
3160
+ "metadata": {
3161
+ "description": "Public virtual-directory path for Keycloak (Front Door /auth/*). Builds KEYCLOAK_SERVER_URL."
3162
+ }
3163
+ },
3164
+ "misoVdirPublic": {
3165
+ "type": "string",
3166
+ "defaultValue": "/miso",
3167
+ "metadata": {
3168
+ "description": "Public virtual-directory path for the controller. Emitted as MISO_RELATIVE_PATH."
3169
+ }
3170
+ },
2946
3171
  "enablePostgresPublicAccess": {
2947
3172
  "type": "bool",
2948
3173
  "defaultValue": false,
@@ -2953,7 +3178,11 @@
2953
3178
  "deploymentEnvironment": {
2954
3179
  "type": "string",
2955
3180
  "defaultValue": "nonprod",
2956
- "allowedValues": ["prod", "nonprod", "evaluation"],
3181
+ "allowedValues": [
3182
+ "prod",
3183
+ "nonprod",
3184
+ "evaluation"
3185
+ ],
2957
3186
  "metadata": {
2958
3187
  "description": "Deployment environment. Use prod for production; non-prod disables API rate limiting by default"
2959
3188
  }
@@ -2979,25 +3208,25 @@
2979
3208
  "description": "Optional shared secret for Mori → controller callback, supplied through the Miso installation/application contract."
2980
3209
  }
2981
3210
  },
2982
- "entraCallbackStateSecret": {
2983
- "type": "securestring",
3211
+ "moriLicensePublicKey": {
3212
+ "type": "string",
2984
3213
  "defaultValue": "",
2985
3214
  "metadata": {
2986
- "description": "Optional shared HMAC secret for Entra consent OAuth state, supplied through the Miso installation contract."
3215
+ "description": "Mori licence issuer public key (SPKI PEM). Empty leaves this installation unpinned."
2987
3216
  }
2988
3217
  },
2989
- "entraIdClientId": {
3218
+ "moriLicenseIssuer": {
2990
3219
  "type": "string",
2991
- "defaultValue": "481251f2-a318-47ba-9ad8-40c36f790df1",
3220
+ "defaultValue": "mori-controller",
2992
3221
  "metadata": {
2993
- "description": "Vendor multi-tenant Entra app client ID."
3222
+ "description": "Expected `iss` claim on a Mori licence."
2994
3223
  }
2995
3224
  },
2996
- "entraIdClientSecret": {
2997
- "type": "securestring",
2998
- "defaultValue": "",
3225
+ "entraIdClientId": {
3226
+ "type": "string",
3227
+ "defaultValue": "481251f2-a318-47ba-9ad8-40c36f790df1",
2999
3228
  "metadata": {
3000
- "description": "Optional vendor Entra app client secret, supplied through the Miso installation contract."
3229
+ "description": "Vendor multi-tenant Entra app client ID."
3001
3230
  }
3002
3231
  },
3003
3232
  "entraIdTenantId": {
@@ -3017,7 +3246,10 @@
3017
3246
  "onboardingMode": {
3018
3247
  "type": "string",
3019
3248
  "defaultValue": "customerOwned",
3020
- "allowedValues": ["customerOwned", "saasDirect"],
3249
+ "allowedValues": [
3250
+ "customerOwned",
3251
+ "saasDirect"
3252
+ ],
3021
3253
  "metadata": {
3022
3254
  "description": "Tenant onboarding mode (ONBOARDING_MODE)."
3023
3255
  }
@@ -3035,10 +3267,27 @@
3035
3267
  "metadata": {
3036
3268
  "description": "Mori license JWT for controller. Empty uses Mori subscription validation. DEVELOPMENT is ignored when DEPLOYMENT=azure."
3037
3269
  }
3270
+ },
3271
+ "bootstrapAppKeys": {
3272
+ "type": "string",
3273
+ "defaultValue": "dataplane,builder-api,mori-controller",
3274
+ "metadata": {
3275
+ "description": "Workload applications an installer may provision at install time (BOOTSTRAP_APPKEYS)."
3276
+ }
3038
3277
  }
3039
3278
  },
3040
3279
  "variables": {
3041
3280
  "shouldDisableRateLimit": "[if(equals(parameters('deploymentEnvironment'), 'prod'), 'false', 'true')]",
3281
+ "misoOriginUrl": "[if(not(empty(parameters('misoControllerUrl'))), parameters('misoControllerUrl'), format('https://{0}.azurewebsites.net', parameters('name')))]",
3282
+ "misoWebAppOrigin": "[if(endsWith(variables('misoOriginUrl'), '/'), substring(variables('misoOriginUrl'), 0, max(0, sub(length(variables('misoOriginUrl')), 1))), variables('misoOriginUrl'))]",
3283
+ "misoVdirSegment": "[if(or(equals(parameters('misoVdirPublic'), ''), equals(parameters('misoVdirPublic'), '/')), '', if(startsWith(parameters('misoVdirPublic'), '/'), parameters('misoVdirPublic'), format('/{0}', parameters('misoVdirPublic'))))]",
3284
+ "misoPublicUrl": "[if(empty(variables('misoVdirSegment')), variables('misoWebAppOrigin'), format('{0}{1}', variables('misoWebAppOrigin'), variables('misoVdirSegment')))]",
3285
+ "misoInternalUrl": "[if(empty(variables('misoVdirSegment')), variables('misoWebAppOrigin'), format('{0}{1}', variables('misoWebAppOrigin'), variables('misoVdirSegment')))]",
3286
+ "misoVdirValue": "[if(empty(variables('misoVdirSegment')), '/', variables('misoVdirSegment'))]",
3287
+ "keycloakWebAppOrigin": "[if(endsWith(parameters('keycloakWebAppUrl'), '/'), substring(parameters('keycloakWebAppUrl'), 0, max(0, sub(length(parameters('keycloakWebAppUrl')), 1))), parameters('keycloakWebAppUrl'))]",
3288
+ "keycloakVdirSegment": "[if(or(equals(parameters('keycloakVdirPublic'), ''), equals(parameters('keycloakVdirPublic'), '/')), '', if(startsWith(parameters('keycloakVdirPublic'), '/'), parameters('keycloakVdirPublic'), format('/{0}', parameters('keycloakVdirPublic'))))]",
3289
+ "keycloakPublicUrl": "[if(empty(variables('keycloakVdirSegment')), variables('keycloakWebAppOrigin'), format('{0}{1}', variables('keycloakWebAppOrigin'), variables('keycloakVdirSegment')))]",
3290
+ "keycloakInternalUrl": "[variables('keycloakPublicUrl')]",
3042
3291
  "databaseUrlAppSettingValue": "[if(not(empty(parameters('databaseUrlSecretUriWithVersion'))), format('@Microsoft.KeyVault(SecretUri={0})', parameters('databaseUrlSecretUriWithVersion')), format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-databases-0-urlKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix')))]",
3043
3292
  "databaseLogUrlAppSettingValue": "[if(not(empty(parameters('databaseLogUrlSecretUriWithVersion'))), format('@Microsoft.KeyVault(SecretUri={0})', parameters('databaseLogUrlSecretUriWithVersion')), format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-databases-1-urlKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix')))]",
3044
3293
  "moriBaseNormalized": "[if(and(greater(length(parameters('moriBaseUrl')), 1), endsWith(parameters('moriBaseUrl'), '/')), substring(parameters('moriBaseUrl'), 0, max(0, sub(length(parameters('moriBaseUrl')), 1))), parameters('moriBaseUrl'))]",
@@ -3083,10 +3332,10 @@
3083
3332
  "virtualNetworkSubnetId": "[if(not(empty(parameters('vnetSubnetId'))), parameters('vnetSubnetId'), null())]",
3084
3333
  "siteConfig": {
3085
3334
  "linuxFxVersion": "[format('DOCKER|{0}', parameters('image'))]",
3086
- "acrUseManagedIdentityCreds": true,
3335
+ "acrUseManagedIdentityCreds": "[not(parameters('registryPasswordConfigured'))]",
3087
3336
  "healthCheckPath": "/health",
3088
3337
  "alwaysOn": true,
3089
- "appSettings": "[concat(createArray(createObject('name', 'WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'value', 'false'), createObject('name', 'WEBSITES_CONTAINER_START_TIME_LIMIT', 'value', '1800'), createObject('name', 'DATABASE_URL', 'value', variables('databaseUrlAppSettingValue')), createObject('name', 'JWT_SECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-secrets-jwtKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'NODE_ENV', 'value', 'production'), createObject('name', 'LOCATION', 'value', parameters('location')), createObject('name', 'PORT', 'value', '3000'), createObject('name', 'CORS_ORIGINS', 'value', format('https://{0}.azurewebsites.net', parameters('name'))), createObject('name', 'ALLOWED_ORIGINS', 'value', format('http://localhost:*,http://127.0.0.1:*,https://{0}.azurewebsites.net', parameters('name'))), createObject('name', 'WEBSITE_VNET_ROUTE_ALL', 'value', if(parameters('enablePostgresPublicAccess'), '0', '1')), createObject('name', 'WEBSITE_DNS_SERVER', 'value', '168.63.129.16'), createObject('name', 'DATABASELOG_URL', 'value', variables('databaseLogUrlAppSettingValue')), createObject('name', 'KEY_VAULT_ENABLED', 'value', 'true'), createObject('name', 'SECURITY_STORAGE_MODE', 'value', 'keyvault'), createObject('name', 'KEY_VAULT_URL', 'value', format('https://{0}.vault.azure.net/', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'ENCRYPTION_KEY', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-encryption-keyKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'AUTO_CREATE_TABLES', 'value', if(parameters('autoCreateTables'), 'true', 'false')), createObject('name', 'KEYCLOAK_REALM', 'value', parameters('keycloakRealm')), createObject('name', 'KEYCLOAK_CLIENT_ID', 'value', parameters('keycloakClientId')), createObject('name', 'KEYCLOAK_ADMIN_USERNAME', 'value', parameters('keycloakAdminUsername')), createObject('name', 'KEYCLOAK_ADMIN_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-keycloak-admin-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'ONBOARDING_ADMIN_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-onboarding-admin-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'KEYCLOAK_SERVER_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=keycloak-web-server-url)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'KEYCLOAK_INTERNAL_SERVER_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=keycloak-internal-server-url)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'KEYCLOAK_TOKEN_TIMEOUT', 'value', '5000'), createObject('name', 'KEYCLOAK_VERIFY_AUDIENCE', 'value', if(parameters('keycloakVerifyAudience'), 'true', 'false')), createObject('name', 'DISABLE_RATE_LIMIT', 'value', variables('shouldDisableRateLimit')), createObject('name', 'SKIP_DB_REDEPLOY', 'value', 'true'), createObject('name', 'MISO_WEB_SERVER_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-web-server-url)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MISO_CONTROLLER_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-internal-server-urlKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MISO_RELATIVE_PATH', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-vdir-public)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'CONTROLLER_BASE_URL', 'value', if(not(empty(parameters('misoControllerUrl'))), parameters('misoControllerUrl'), format('https://{0}.azurewebsites.net', parameters('name')))), createObject('name', 'AZURE_SUBSCRIPTION_ID', 'value', subscription().subscriptionId), createObject('name', 'TENANT_ID', 'value', subscription().tenantId), createObject('name', 'AZURE_RESOURCE_GROUP_NAME', 'value', parameters('resourceGroupName')), createObject('name', 'INFRASTRUCTURE_NAME', 'value', parameters('prefix')), createObject('name', 'ENTRA_ID_CLIENT_ID', 'value', parameters('entraIdClientId')), createObject('name', 'ENTRA_ID_CLIENT_SECRET', 'value', parameters('entraIdClientSecret')), createObject('name', 'ENTRA_ID_TENANT_ID', 'value', parameters('entraIdTenantId')), createObject('name', 'MISO_CLIENTID', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-miso-clientIdKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'MISO_CLIENTSECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-client-secretKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MORI_BASE_URL', 'value', variables('moriBaseNormalized')), createObject('name', 'MORI_CALLBACK_API_KEY', 'value', parameters('moriCallbackApiKey')), createObject('name', 'ENTRA_GRAPH_DELEGATED_REDIRECT_URI', 'value', variables('moriEntraGraphDelegatedRedirectUri')), createObject('name', 'ENTRA_ID_CENTRAL_CALLBACK_URI', 'value', variables('moriEntraCentralCallbackUri')), createObject('name', 'ENTRA_CALLBACK_STATE_SECRET', 'value', parameters('entraCallbackStateSecret')), createObject('name', 'DOCKER_ENABLE_CI', 'value', 'true'), createObject('name', 'DOCKER_REGISTRY_SERVER_URL', 'value', if(not(empty(parameters('acrLoginServer'))), format('https://{0}', parameters('acrLoginServer')), '')), createObject('name', 'ONBOARDING_CREATE_PIPELINE_CREDENTIALS', 'value', 'true'), createObject('name', 'LICENSE_JWT', 'value', parameters('licenseJwt')), createObject('name', 'LICENSE_EDITION', 'value', parameters('licenseEdition')), createObject('name', 'ONBOARDING_MODE', 'value', parameters('onboardingMode'))), variables('registryCredentialAppSettings'))]",
3338
+ "appSettings": "[concat(createArray(createObject('name', 'WEBSITES_ENABLE_APP_SERVICE_STORAGE', 'value', 'false'), createObject('name', 'WEBSITES_CONTAINER_START_TIME_LIMIT', 'value', '1800'), createObject('name', 'DATABASE_URL', 'value', variables('databaseUrlAppSettingValue')), createObject('name', 'JWT_SECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-secrets-jwtKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'NODE_ENV', 'value', 'production'), createObject('name', 'LOCATION', 'value', parameters('location')), createObject('name', 'PORT', 'value', '3000'), createObject('name', 'ALLOWED_ORIGINS', 'value', format('http://localhost:*,http://127.0.0.1:*,https://{0}.azurewebsites.net', parameters('name'))), createObject('name', 'WEBSITE_VNET_ROUTE_ALL', 'value', if(parameters('enablePostgresPublicAccess'), '0', '1')), createObject('name', 'WEBSITE_DNS_SERVER', 'value', '168.63.129.16'), createObject('name', 'DATABASELOG_URL', 'value', variables('databaseLogUrlAppSettingValue')), createObject('name', 'KEY_VAULT_ENABLED', 'value', 'true'), createObject('name', 'SECURITY_STORAGE_MODE', 'value', 'keyvault'), createObject('name', 'KEY_VAULT_URL', 'value', format('https://{0}.vault.azure.net/', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'ENCRYPTION_KEY', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-encryption-keyKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'AUTO_CREATE_TABLES', 'value', if(parameters('autoCreateTables'), 'true', 'false')), createObject('name', 'KEYCLOAK_REALM', 'value', parameters('keycloakRealm')), createObject('name', 'KEYCLOAK_CLIENT_ID', 'value', parameters('keycloakClientId')), createObject('name', 'KEYCLOAK_ADMIN_USERNAME', 'value', parameters('keycloakAdminUsername')), createObject('name', 'KEYCLOAK_ADMIN_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-keycloak-admin-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'ONBOARDING_ADMIN_PASSWORD', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-onboarding-admin-password)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'KEYCLOAK_SERVER_URL', 'value', variables('keycloakPublicUrl')), createObject('name', 'KEYCLOAK_INTERNAL_SERVER_URL', 'value', variables('keycloakInternalUrl')), createObject('name', 'KEYCLOAK_TOKEN_TIMEOUT', 'value', '5000'), createObject('name', 'KEYCLOAK_VERIFY_AUDIENCE', 'value', if(parameters('keycloakVerifyAudience'), 'true', 'false')), createObject('name', 'DISABLE_RATE_LIMIT', 'value', variables('shouldDisableRateLimit')), createObject('name', 'SKIP_DB_REDEPLOY', 'value', 'true'), createObject('name', 'MISO_WEB_SERVER_URL', 'value', variables('misoPublicUrl')), createObject('name', 'MISO_CONTROLLER_URL', 'value', variables('misoInternalUrl')), createObject('name', 'MISO_RELATIVE_PATH', 'value', variables('misoVdirValue')), createObject('name', 'CONTROLLER_BASE_URL', 'value', if(not(empty(parameters('misoControllerUrl'))), parameters('misoControllerUrl'), format('https://{0}.azurewebsites.net', parameters('name')))), createObject('name', 'AZURE_SUBSCRIPTION_ID', 'value', subscription().subscriptionId), createObject('name', 'TENANT_ID', 'value', subscription().tenantId), createObject('name', 'AZURE_RESOURCE_GROUP_NAME', 'value', parameters('resourceGroupName')), createObject('name', 'INFRASTRUCTURE_NAME', 'value', parameters('prefix')), createObject('name', 'ENTRA_ID_CLIENT_ID', 'value', parameters('entraIdClientId')), createObject('name', 'ENTRA_ID_CLIENT_SECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=entra-client-secretKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'ENTRA_ID_TENANT_ID', 'value', parameters('entraIdTenantId')), createObject('name', 'MISO_CLIENTID', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1}-miso-clientIdKeyVault)', last(split(parameters('keyVaultId'), '/')), parameters('prefix'))), createObject('name', 'MISO_CLIENTSECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=miso-controller-client-secretKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MORI_BASE_URL', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=mori-base-urlKeyVault)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'MORI_LICENSE_PUBLIC_KEY', 'value', if(empty(parameters('moriLicensePublicKey')), '', format('@Microsoft.KeyVault(VaultName={0};SecretName=mori-license-public-keyKeyVault)', last(split(parameters('keyVaultId'), '/'))))), createObject('name', 'MORI_LICENSE_ISSUER', 'value', parameters('moriLicenseIssuer')), createObject('name', 'MORI_CALLBACK_API_KEY', 'value', if(empty(parameters('moriCallbackApiKey')), '', format('@Microsoft.KeyVault(VaultName={0};SecretName=mori-callback-api-keyKeyVault)', last(split(parameters('keyVaultId'), '/'))))), createObject('name', 'ENTRA_GRAPH_DELEGATED_REDIRECT_URI', 'value', variables('moriEntraGraphDelegatedRedirectUri')), createObject('name', 'ENTRA_ID_CENTRAL_CALLBACK_URI', 'value', variables('moriEntraCentralCallbackUri')), createObject('name', 'ENTRA_CALLBACK_STATE_SECRET', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName=entra-callback-state-secret)', last(split(parameters('keyVaultId'), '/')))), createObject('name', 'DOCKER_ENABLE_CI', 'value', 'true'), createObject('name', 'DOCKER_REGISTRY_SERVER_URL', 'value', if(not(empty(parameters('acrLoginServer'))), format('https://{0}', parameters('acrLoginServer')), '')), createObject('name', 'ONBOARDING_CREATE_PIPELINE_CREDENTIALS', 'value', 'true'), createObject('name', 'BOOTSTRAP_APPKEYS', 'value', parameters('bootstrapAppKeys')), createObject('name', 'LICENSE_JWT', 'value', parameters('licenseJwt')), createObject('name', 'LICENSE_EDITION', 'value', parameters('licenseEdition')), createObject('name', 'ONBOARDING_MODE', 'value', parameters('onboardingMode'))), variables('registryCredentialAppSettings'))]",
3090
3339
  "vnetRouteAllEnabled": "[not(parameters('enablePostgresPublicAccess'))]"
3091
3340
  },
3092
3341
  "vnetRouteAllEnabled": "[not(parameters('enablePostgresPublicAccess'))]"
@@ -3116,7 +3365,9 @@
3116
3365
  "enabled": true
3117
3366
  }
3118
3367
  },
3119
- "dependsOn": ["[resourceId('Microsoft.Web/sites', parameters('name'))]"]
3368
+ "dependsOn": [
3369
+ "[resourceId('Microsoft.Web/sites', parameters('name'))]"
3370
+ ]
3120
3371
  },
3121
3372
  {
3122
3373
  "type": "Microsoft.Web/sites/config",
@@ -3131,7 +3382,9 @@
3131
3382
  "mountPath": "/data"
3132
3383
  }
3133
3384
  },
3134
- "dependsOn": ["[resourceId('Microsoft.Web/sites', parameters('name'))]"]
3385
+ "dependsOn": [
3386
+ "[resourceId('Microsoft.Web/sites', parameters('name'))]"
3387
+ ]
3135
3388
  }
3136
3389
  ],
3137
3390
  "outputs": {
@@ -3232,7 +3485,11 @@
3232
3485
  "tenantId": "[subscription().tenantId]",
3233
3486
  "objectId": "[parameters('webAppPrincipalIds')[copyIndex('accessPolicies')]]",
3234
3487
  "permissions": {
3235
- "secrets": ["get", "list", "set"],
3488
+ "secrets": [
3489
+ "get",
3490
+ "list",
3491
+ "set"
3492
+ ],
3236
3493
  "keys": [],
3237
3494
  "certificates": [],
3238
3495
  "storage": []
@@ -3278,7 +3535,7 @@
3278
3535
  "_generator": {
3279
3536
  "name": "bicep",
3280
3537
  "version": "0.47.16.16243",
3281
- "templateHash": "16977137676176830260"
3538
+ "templateHash": "8140687320474179352"
3282
3539
  }
3283
3540
  },
3284
3541
  "parameters": {
@@ -3304,7 +3561,8 @@
3304
3561
  "variables": {
3305
3562
  "readySetting": {
3306
3563
  "AIFABRIX_KV_ACCESS_READY": "[parameters('readyStamp')]"
3307
- }
3564
+ },
3565
+ "misoOnboardingSetting": "[variables('readySetting')]"
3308
3566
  },
3309
3567
  "resources": [
3310
3568
  {
@@ -3373,7 +3631,7 @@
3373
3631
  "value": "[parameters('misoWebAppName')]"
3374
3632
  },
3375
3633
  "appSettings": {
3376
- "value": "[union(list(resourceId('Microsoft.Web/sites/config', parameters('misoWebAppName'), 'appsettings'), '2022-03-01').properties, variables('readySetting'))]"
3634
+ "value": "[union(list(resourceId('Microsoft.Web/sites/config', parameters('misoWebAppName'), 'appsettings'), '2022-03-01').properties, variables('misoOnboardingSetting'))]"
3377
3635
  }
3378
3636
  },
3379
3637
  "template": {
@@ -3460,6 +3718,12 @@
3460
3718
  },
3461
3719
  "encryptionKey": {
3462
3720
  "value": "[reference(resourceId('Microsoft.Resources/deployments', 'bootstrapSecrets'), '2025-04-01').outputs.encryptionKey.value]"
3721
+ },
3722
+ "entraCallbackStateSecret": {
3723
+ "value": "[parameters('entraCallbackStateSecret')]"
3724
+ },
3725
+ "entraIdClientSecret": {
3726
+ "value": "[parameters('entraIdClientSecret')]"
3463
3727
  }
3464
3728
  },
3465
3729
  "template": {
@@ -3469,7 +3733,7 @@
3469
3733
  "_generator": {
3470
3734
  "name": "bicep",
3471
3735
  "version": "0.47.16.16243",
3472
- "templateHash": "9394845529752045254"
3736
+ "templateHash": "9634353332764264230"
3473
3737
  }
3474
3738
  },
3475
3739
  "parameters": {
@@ -3537,9 +3801,46 @@
3537
3801
  "metadata": {
3538
3802
  "description": "MISO Client Secret (auto-generated if not provided)"
3539
3803
  }
3804
+ },
3805
+ "entraCallbackStateSecret": {
3806
+ "type": "securestring",
3807
+ "defaultValue": "",
3808
+ "metadata": {
3809
+ "description": "Shared HMAC secret for Entra consent OAuth state (ENTRA_CALLBACK_STATE_SECRET). Generated when empty."
3810
+ }
3811
+ },
3812
+ "entraIdClientSecret": {
3813
+ "type": "securestring",
3814
+ "defaultValue": "",
3815
+ "metadata": {
3816
+ "description": "Vendor Entra app client secret (ENTRA_ID_CLIENT_SECRET). Placeholder when empty — replace for live Entra flows."
3817
+ }
3818
+ },
3819
+ "moriLicensePublicKey": {
3820
+ "type": "string",
3821
+ "defaultValue": "",
3822
+ "metadata": {
3823
+ "description": "Mori licence issuer public key (SPKI PEM). Empty leaves the installation unpinned."
3824
+ }
3825
+ },
3826
+ "moriBaseUrl": {
3827
+ "type": "string",
3828
+ "defaultValue": "",
3829
+ "metadata": {
3830
+ "description": "Mori base URL, stored so App Service can reference it instead of holding a literal."
3831
+ }
3832
+ },
3833
+ "moriCallbackApiKey": {
3834
+ "type": "securestring",
3835
+ "defaultValue": "",
3836
+ "metadata": {
3837
+ "description": "Mori callback API key, shared by the Miso Controller and Mori."
3838
+ }
3540
3839
  }
3541
3840
  },
3542
3841
  "variables": {
3842
+ "resolvedEntraCallbackStateSecret": "[if(not(empty(parameters('entraCallbackStateSecret'))), parameters('entraCallbackStateSecret'), format('ecs_{0}{1}', uniqueString(resourceGroup().id, parameters('prefix'), 'entra-callback-state-v1'), uniqueString(parameters('prefix'), 'entra-cb-b')))]",
3843
+ "resolvedEntraIdClientSecret": "[if(not(empty(parameters('entraIdClientSecret'))), parameters('entraIdClientSecret'), format('PLACEHOLDER_ENTRA_CLIENT_SECRET_MUST_BE_REPLACED_{0}', uniqueString(resourceGroup().id, parameters('prefix'))))]",
3543
3844
  "misoWebAppOrigin": "[if(endsWith(parameters('misoWebAppUrl'), '/'), substring(parameters('misoWebAppUrl'), 0, max(0, sub(length(parameters('misoWebAppUrl')), 1))), parameters('misoWebAppUrl'))]",
3544
3845
  "misoVdirSegment": "[if(or(equals(parameters('misoVdirPublic'), ''), equals(parameters('misoVdirPublic'), '/')), '', if(startsWith(parameters('misoVdirPublic'), '/'), parameters('misoVdirPublic'), format('/{0}', parameters('misoVdirPublic'))))]",
3545
3846
  "misoWebServerPublicUrl": "[if(empty(variables('misoVdirSegment')), variables('misoWebAppOrigin'), format('{0}{1}', variables('misoWebAppOrigin'), variables('misoVdirSegment')))]",
@@ -3629,6 +3930,49 @@
3629
3930
  "properties": {
3630
3931
  "value": "[if(not(empty(parameters('misoClientSecret'))), parameters('misoClientSecret'), format('PLACEHOLDER_CLIENT_SECRET_MUST_BE_REPLACED_{0}', uniqueString(resourceGroup().id, parameters('prefix'))))]"
3631
3932
  }
3933
+ },
3934
+ {
3935
+ "type": "Microsoft.KeyVault/vaults/secrets",
3936
+ "apiVersion": "2023-07-01",
3937
+ "name": "[format('{0}/{1}', parameters('keyVaultName'), 'entra-callback-state-secret')]",
3938
+ "properties": {
3939
+ "value": "[variables('resolvedEntraCallbackStateSecret')]"
3940
+ }
3941
+ },
3942
+ {
3943
+ "type": "Microsoft.KeyVault/vaults/secrets",
3944
+ "apiVersion": "2023-07-01",
3945
+ "name": "[format('{0}/{1}', parameters('keyVaultName'), 'entra-client-secretKeyVault')]",
3946
+ "properties": {
3947
+ "value": "[variables('resolvedEntraIdClientSecret')]"
3948
+ }
3949
+ },
3950
+ {
3951
+ "condition": "[not(empty(parameters('moriLicensePublicKey')))]",
3952
+ "type": "Microsoft.KeyVault/vaults/secrets",
3953
+ "apiVersion": "2023-07-01",
3954
+ "name": "[format('{0}/{1}', parameters('keyVaultName'), 'mori-license-public-keyKeyVault')]",
3955
+ "properties": {
3956
+ "value": "[parameters('moriLicensePublicKey')]"
3957
+ }
3958
+ },
3959
+ {
3960
+ "condition": "[not(empty(parameters('moriBaseUrl')))]",
3961
+ "type": "Microsoft.KeyVault/vaults/secrets",
3962
+ "apiVersion": "2023-07-01",
3963
+ "name": "[format('{0}/{1}', parameters('keyVaultName'), 'mori-base-urlKeyVault')]",
3964
+ "properties": {
3965
+ "value": "[parameters('moriBaseUrl')]"
3966
+ }
3967
+ },
3968
+ {
3969
+ "condition": "[not(empty(parameters('moriCallbackApiKey')))]",
3970
+ "type": "Microsoft.KeyVault/vaults/secrets",
3971
+ "apiVersion": "2023-07-01",
3972
+ "name": "[format('{0}/{1}', parameters('keyVaultName'), 'mori-callback-api-keyKeyVault')]",
3973
+ "properties": {
3974
+ "value": "[parameters('moriCallbackApiKey')]"
3975
+ }
3632
3976
  }
3633
3977
  ]
3634
3978
  }
@@ -3772,7 +4116,7 @@
3772
4116
  ]
3773
4117
  },
3774
4118
  {
3775
- "condition": "[parameters('deployRoleAssignments')]",
4119
+ "condition": "[parameters('deployRegistryRoleAssignment')]",
3776
4120
  "type": "Microsoft.Resources/deployments",
3777
4121
  "apiVersion": "2025-04-01",
3778
4122
  "name": "acrPullRoleAssignment",
@@ -3801,7 +4145,7 @@
3801
4145
  "_generator": {
3802
4146
  "name": "bicep",
3803
4147
  "version": "0.47.16.16243",
3804
- "templateHash": "1882530004456299732"
4148
+ "templateHash": "14966069055732756709"
3805
4149
  }
3806
4150
  },
3807
4151
  "parameters": {
@@ -3821,7 +4165,7 @@
3821
4165
  }
3822
4166
  },
3823
4167
  "variables": {
3824
- "acrPullRoleDefinitionId": "7f951dda-4ed3-4680-a7ca-43b17298f9ac"
4168
+ "acrPullRoleDefinitionId": "7f951dda-4ed3-4680-a7ca-43fe172d538d"
3825
4169
  },
3826
4170
  "resources": [
3827
4171
  {
@@ -4082,6 +4426,20 @@
4082
4426
  "portalUrl": {
4083
4427
  "type": "string",
4084
4428
  "value": "[format('https://portal.azure.com/#@{0}/resource/subscriptions/{1}/resourceGroups/{2}', subscription().tenantId, subscription().subscriptionId, resourceGroup().name)]"
4429
+ },
4430
+ "azureOpenAiCreated": {
4431
+ "type": "string",
4432
+ "metadata": {
4433
+ "description": "`true` when Phase-1 created the Azure OpenAI account in the installation region."
4434
+ },
4435
+ "value": "[if(variables('createAzureOpenAI'), 'true', 'false')]"
4436
+ },
4437
+ "azureOpenAiSkipReason": {
4438
+ "type": "string",
4439
+ "metadata": {
4440
+ "description": "Empty when the account was created; otherwise why it was not."
4441
+ },
4442
+ "value": "[if(variables('createAzureOpenAI'), '', if(parameters('enableAzureOpenAI'), format('Azure OpenAI is not offered in {0}, and it is not relocated to another region automatically. The installation is complete without it: create the account in a supported region yourself, or supply an existing endpoint as an external provider.', variables('deploymentLocation')), 'Azure OpenAI creation was disabled for this installation (enableAzureOpenAI=false).'))]"
4085
4443
  }
4086
4444
  }
4087
- }
4445
+ }