@wtfalch/foundry 0.8.0 → 0.9.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.
@@ -13,7 +13,7 @@ export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, Confirmatio
13
13
  export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './platform-providers.js';
14
14
  export type { PlatformAuthDeps, PlatformAuthRuntime, PlatformFilesDeps, PlatformFilesRuntime, } from './platform-providers.js';
15
15
  export type { PlatformMailDeps, PlatformMailAdminPort, PlatformMailRuntime, } from './platform-providers.js';
16
- export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, } from './platform-providers.js';
16
+ export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
17
17
  export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
18
18
  export type { AiManagementKeyMinter, PlatformAiDeps, PlatformAiRuntime, } from './platform-providers.js';
19
19
  export { createZitadelAdminMinter } from './zitadel-machine-user.js';
@@ -6,7 +6,7 @@ export { PlatformOrgBlockedError, PlatformOrgPorts } from './platform-org-ports.
6
6
  export { FoundryPlanApply, DeclaredOrgNotFoundError, StalePlanError, ConfirmationRequiredError, NotAuthorizedError, NotATestOrgError, DeclaredPlatformNotFoundError, PlatformStalePlanError, PlatformConfirmationRequiredError, planNeedsConfirmation, } from './plan-apply.js';
7
7
  export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './platform-providers.js';
8
8
  // #171: how a host signs files management requests with the delivered key.
9
- export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, } from './platform-providers.js';
9
+ export { FILES_INTROSPECTION_SECRET_REF, FILES_MANAGEMENT_ISSUER, FILES_MANAGEMENT_KEY_ID, FILES_MANAGEMENT_KEY_REF, PLATFORM_AUTH_INSTANCE_BEARER_REF, } from './platform-providers.js';
10
10
  export { AI_MANAGEMENT_ISSUER, AI_MANAGEMENT_KEY_ID, AI_SIGNING_KEY_REF, AiManagementKeyExistsError, createAiManagementKeyMinter, PLATFORM_AI_MANAGEMENT_KEY_REF, PLATFORM_AI_MANAGEMENT_PERMISSIONS, } from './platform-providers.js';
11
11
  export { createZitadelAdminMinter } from './zitadel-machine-user.js';
12
12
  export { MissingCallsSfuConnectionError, applyCallsSfuPlatformStep } from './platform-sfu.js';
@@ -633,13 +633,12 @@ function stepField(action) {
633
633
  *
634
634
  * Three sources, in this order:
635
635
  * - fields no planner step exists for yet, so no apply can make them true:
636
- * SSO, guests and solo when turned on, an adopted app in the `repo` form
637
- * (#7's `#15` comment: "declared but refused by `apply`"), and calls or
638
- * hosting on a domain the org has not recorded (#145: calls on the org's
639
- * own domain now has a planner step, the same way hosting's own domain
640
- * does, so it only fails for that one missing-domain reason, not
641
- * unconditionally). `bespokeAppRepo` is not one of these: `coolify:app`
642
- * reads it (#240), so it converges with that step like any other field;
636
+ * SSO, guests and solo when turned on, and calls or hosting on a domain
637
+ * the org has not recorded (#145: calls on the org's own domain now has a
638
+ * planner step, the same way hosting's own domain does, so it only fails
639
+ * for that one missing-domain reason, not unconditionally). An app repo is
640
+ * not one of these: `coolify:app` reads the `app` entry's `repo`, so it
641
+ * converges with that step like any other field;
643
642
  * - chat's reverse re-key, which the planner surfaces but which is not built
644
643
  * (#7 amendment 3), so it counts as failed even when no step threw;
645
644
  * - on a failed apply, the field of every planned step from the one that
@@ -663,8 +662,6 @@ function failedFields(org, detail, completed, serviceIds) {
663
662
  failed.add('guests.enabled');
664
663
  if (org.solo.enabled)
665
664
  failed.add('solo.enabled');
666
- if (org.adoptedApp?.form === 'repo')
667
- failed.add('adoptedApp');
668
665
  // `planCallsService`/`planTunnel`/`planZone` all share this same domain
669
666
  // requirement (#145): a declared `usesDomain: true` with no recorded
670
667
  // domain plans no step for any of them either.
@@ -1,4 +1,4 @@
1
- import type { DeclaredOrg, OrgId } from '../org.js';
1
+ import type { DeclaredOrg, HostingEntry, OrgId } from '../org.js';
2
2
  import { type Plan, type UnmanagedItem, type VersionChangeItem } from '../plan.js';
3
3
  import type { DeclaredPlatform } from '../platform.js';
4
4
  import type { PlannedStep } from './diff.js';
@@ -118,6 +118,10 @@ export declare const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = "archon_applied_
118
118
  export declare const PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY = "archon_gate_closed";
119
119
  export declare const PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY = "zitadel_platform_admin_user_id";
120
120
  export declare const PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY = "zitadel_platform_admin_user_undelivered";
121
+ /** #283: the created instance's id, recorded once its owner token is delivered. */
122
+ export declare const PLATFORM_AUTH_INSTANCE_SCOPE_KEY = "zitadel_platform_instance_id";
123
+ /** #283: an instance that may exist without a delivered owner token; blocks creating another. */
124
+ export declare const PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY = "zitadel_platform_instance_undelivered";
121
125
  /**
122
126
  * #216 (building on #209/#215): the `IAM_LOGIN_CLIENT` machine user minted
123
127
  * for the login app and the broker, mirroring
@@ -312,24 +316,32 @@ export declare function platformMailMxHost(apex: string): string;
312
316
  export declare const ORG_MAIL_APPLIED_APEX_SCOPE_KEY = "mail_applied_apex";
313
317
  export declare const ORG_MAIL_APPLIED_HOST_SCOPE_KEY = "mail_applied_host";
314
318
  /**
315
- * #243: the org's `hosting.appEnv` values the last `coolify:app` apply pushed,
316
- * recorded as one JSON object (env name to value; ids and a domain, never a
317
- * secret). `planApp` plans the step again when {@link resolveAppEnv} differs
318
- * from it, so a changed map reaches an app that is already live.
319
+ * The key of an org's `app` hosting entry in a `coolify:app` or
320
+ * `cloudflare:access` step and in the ids recorded for it:
321
+ * `<orgId>:<entry key>`, so a second entry cannot collide with the first.
319
322
  */
320
- export declare const APP_ENV_APPLIED_SCOPE_KEY = "app_env_applied";
323
+ export declare function appStepKey(org: DeclaredOrg): string;
324
+ /**
325
+ * #243: the org's `app` entry's `env` values the last `coolify:app` apply
326
+ * pushed, recorded as one JSON object (env name to value; ids and a domain,
327
+ * never a secret). `planApp` plans the step again when {@link resolveAppEnv}
328
+ * differs from it, so a changed map reaches an app that is already live. The
329
+ * `.app` suffix is the entry key; a scope key holds no `:`, so
330
+ * `storeRecordedIds` still filters it out.
331
+ */
332
+ export declare const APP_ENV_APPLIED_SCOPE_KEY = "app_env_applied.app";
321
333
  /**
322
334
  * #239: set to '1' once an app has been restarted after its database URLs
323
335
  * were pushed. Not a URL. Without it a crash between the push and the restart
324
336
  * would leave a stale container that no plan notices.
325
337
  */
326
- export declare const DB_ENV_APPLIED_SCOPE_KEY = "db_env_applied";
338
+ export declare const DB_ENV_APPLIED_SCOPE_KEY = "db_env_applied.app";
327
339
  /**
328
- * The `hosting.appEnv` entries whose source has a recorded value, in env-name
329
- * order, and the env names whose source has none yet. Shared by `planApp` and
330
- * the executor's `pushAppEnv` so both read the same recorded ids.
340
+ * The entry's `env` values that have a value, in env-name order, and the env
341
+ * names whose source has none yet. Shared by `planApp` and the executor's
342
+ * `pushAppEnv` so both read the same recorded ids.
331
343
  */
332
- export declare function resolveAppEnv(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): {
344
+ export declare function resolveAppEnv(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>): {
333
345
  values: Record<string, string>;
334
346
  unresolved: Array<[string, string]>;
335
347
  };
@@ -1,4 +1,5 @@
1
1
  import { VERSIONS } from '../bootstrap/versions.js';
2
+ import { APP_ENTRY_KEY } from '../org.js';
2
3
  import { VERSION_CHANGE_KIND, } from '../plan.js';
3
4
  /**
4
5
  * The `Plan` (#19's public contract) a {@link PlatformPlanDetail} carries,
@@ -136,6 +137,10 @@ export const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = 'archon_applied_broker_a
136
137
  export const PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY = 'archon_gate_closed';
137
138
  export const PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY = 'zitadel_platform_admin_user_id';
138
139
  export const PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY = 'zitadel_platform_admin_user_undelivered';
140
+ /** #283: the created instance's id, recorded once its owner token is delivered. */
141
+ export const PLATFORM_AUTH_INSTANCE_SCOPE_KEY = 'zitadel_platform_instance_id';
142
+ /** #283: an instance that may exist without a delivered owner token; blocks creating another. */
143
+ export const PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY = 'zitadel_platform_instance_undelivered';
139
144
  /**
140
145
  * #216 (building on #209/#215): the `IAM_LOGIN_CLIENT` machine user minted
141
146
  * for the login app and the broker, mirroring
@@ -343,24 +348,34 @@ export function platformMailMxHost(apex) {
343
348
  export const ORG_MAIL_APPLIED_APEX_SCOPE_KEY = 'mail_applied_apex';
344
349
  export const ORG_MAIL_APPLIED_HOST_SCOPE_KEY = 'mail_applied_host';
345
350
  /**
346
- * #243: the org's `hosting.appEnv` values the last `coolify:app` apply pushed,
347
- * recorded as one JSON object (env name to value; ids and a domain, never a
348
- * secret). `planApp` plans the step again when {@link resolveAppEnv} differs
349
- * from it, so a changed map reaches an app that is already live.
351
+ * The key of an org's `app` hosting entry in a `coolify:app` or
352
+ * `cloudflare:access` step and in the ids recorded for it:
353
+ * `<orgId>:<entry key>`, so a second entry cannot collide with the first.
350
354
  */
351
- export const APP_ENV_APPLIED_SCOPE_KEY = 'app_env_applied';
355
+ export function appStepKey(org) {
356
+ return `${org.id}:${APP_ENTRY_KEY}`;
357
+ }
358
+ /**
359
+ * #243: the org's `app` entry's `env` values the last `coolify:app` apply
360
+ * pushed, recorded as one JSON object (env name to value; ids and a domain,
361
+ * never a secret). `planApp` plans the step again when {@link resolveAppEnv}
362
+ * differs from it, so a changed map reaches an app that is already live. The
363
+ * `.app` suffix is the entry key; a scope key holds no `:`, so
364
+ * `storeRecordedIds` still filters it out.
365
+ */
366
+ export const APP_ENV_APPLIED_SCOPE_KEY = `app_env_applied.${APP_ENTRY_KEY}`;
352
367
  /**
353
368
  * #239: set to '1' once an app has been restarted after its database URLs
354
369
  * were pushed. Not a URL. Without it a crash between the push and the restart
355
370
  * would leave a stale container that no plan notices.
356
371
  */
357
- export const DB_ENV_APPLIED_SCOPE_KEY = 'db_env_applied';
372
+ export const DB_ENV_APPLIED_SCOPE_KEY = `db_env_applied.${APP_ENTRY_KEY}`;
358
373
  /**
359
- * The `hosting.appEnv` entries whose source has a recorded value, in env-name
360
- * order, and the env names whose source has none yet. Shared by `planApp` and
361
- * the executor's `pushAppEnv` so both read the same recorded ids.
374
+ * The entry's `env` values that have a value, in env-name order, and the env
375
+ * names whose source has none yet. Shared by `planApp` and the executor's
376
+ * `pushAppEnv` so both read the same recorded ids.
362
377
  */
363
- export function resolveAppEnv(org, recorded) {
378
+ export function resolveAppEnv(org, entry, recorded) {
364
379
  const sources = {
365
380
  filesOrganisationId: recorded[`files:organisation:${org.id}`],
366
381
  aiOrganisationId: recorded[`ai:organisation:${org.id}`],
@@ -368,12 +383,12 @@ export function resolveAppEnv(org, recorded) {
368
383
  };
369
384
  const values = {};
370
385
  const unresolved = [];
371
- for (const [name, source] of Object.entries(org.hosting.appEnv).sort(([a], [b]) => a < b ? -1 : 1)) {
372
- const value = sources[source];
386
+ for (const [name, declared] of Object.entries(entry.env).sort(([a], [b]) => (a < b ? -1 : 1))) {
387
+ const value = typeof declared === 'string' ? declared : sources[declared.from];
373
388
  if (value)
374
389
  values[name] = value;
375
- else
376
- unresolved.push([name, source]);
390
+ else if (typeof declared !== 'string')
391
+ unresolved.push([name, declared.from]);
377
392
  }
378
393
  return { values, unresolved };
379
394
  }
@@ -999,6 +1014,16 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
999
1014
  blocked: `the platform's sign-in is blocked, not planned: the admin key of ZITADEL machine user ${undelivered} was minted but never delivered and cannot be shown again; delete that user in ZITADEL and remove the recorded key ${PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY} to mint a new one`,
1000
1015
  };
1001
1016
  }
1017
+ // #283: an instance may exist whose owner token never reached the host.
1018
+ const instanceLost = recordedIds[PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY];
1019
+ if (instanceLost &&
1020
+ !recordedIds[PLATFORM_AUTH_INSTANCE_SCOPE_KEY] &&
1021
+ !recordedIds[PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY]) {
1022
+ return {
1023
+ step: null,
1024
+ blocked: `the platform's sign-in is blocked, not planned: a ZITADEL instance (${instanceLost}) may exist without a delivered owner token; delete it with the system API (DELETE /system/v1/instances/{id}) or redeploy the ZITADEL database, then remove the recorded key ${PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY}`,
1025
+ };
1026
+ }
1002
1027
  // #211 round 6: the executor itself is unusable on this host -- nothing to
1003
1028
  // plan, whatever the broker's recorded state says.
1004
1029
  if (executorMissing)
@@ -223,6 +223,13 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
223
223
  private executeSfu;
224
224
  private coolifyJobs;
225
225
  }
226
+ /**
227
+ * #283: the ref the new instance's IAM_OWNER machine token (a PAT) is
228
+ * delivered under, once. A host reads it back to build `zitadelForInstance`
229
+ * (see {@link provisionPlatformAuth}'s doc comment). Distinct from the admin
230
+ * key delivered under `zitadel:platform:admin`.
231
+ */
232
+ export declare const PLATFORM_AUTH_INSTANCE_BEARER_REF = "zitadel:platform:instanceBearer";
226
233
  /**
227
234
  * The fixed description Stalwart's admin API key is minted under
228
235
  * (`StalwartClient.createApiKey`'s own signature returns only the secret,
@@ -4,8 +4,9 @@ import { VERSIONS, floatingTagBlocked } from '../bootstrap/versions.js';
4
4
  import { redactSecrets } from '../calls-sfu-deploy-support.js';
5
5
  import { ensureCloudflaredConnector } from './cloudflared-connector.js';
6
6
  import { ACCESS_POLICY_NAME, isNotFoundError, isRunningOrStarting } from './executor.js';
7
- import { JOBS_SCHEDULER_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, parseBrokerDeployRecord, parseLoginClientRecord, platformMailMxHost, } from './platform-diff.js';
7
+ import { JOBS_SCHEDULER_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, parseBrokerDeployRecord, parseLoginClientRecord, platformMailMxHost, } from './platform-diff.js';
8
8
  import { applyCallsSfuPlatformStep } from './platform-sfu.js';
9
+ import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, } from './zitadel-system-api.js';
9
10
  /** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
10
11
  function apexZoneKey(apex) {
11
12
  return `cloudflare:zone:${apex}`;
@@ -495,7 +496,7 @@ const AUTH_BROKER_PORT = '3000';
495
496
  * falls through to the login app's own ingress rule below (login v2 answers
496
497
  * everything else, including the root).
497
498
  */
498
- const AUTH_ZITADEL_PATH_PATTERN = '^/(oidc|oauth|saml|management|admin|system|auth|assets|ui/console|\\.well-known)(/|$)';
499
+ const AUTH_ZITADEL_PATH_PATTERN = '^/(oidc|oauth|saml|management|admin|system|auth|assets|debug/healthz|ui/console|\\.well-known)(/|$)';
499
500
  /**
500
501
  * `@wtfalch/auth`'s own `brokerUrl` default, `<issuer>/api`
501
502
  * (`packages/auth/src/config.ts`) -- `deploy/proxy/Caddyfile`'s own
@@ -515,6 +516,14 @@ const PLATFORM_AUTH_ADMIN_USER_NAME = 'foundry-admin';
515
516
  /** #216 (building on #209/#215): the narrower `IAM_LOGIN_CLIENT` machine user for the login app and the broker. */
516
517
  const PLATFORM_AUTH_LOGIN_CLIENT_USER_NAME = 'foundry-login-client';
517
518
  const PLATFORM_AUTH_ORG_NAME = 'foundry';
519
+ /**
520
+ * #283: the ref the new instance's IAM_OWNER machine token (a PAT) is
521
+ * delivered under, once. A host reads it back to build `zitadelForInstance`
522
+ * (see {@link provisionPlatformAuth}'s doc comment). Distinct from the admin
523
+ * key delivered under `zitadel:platform:admin`.
524
+ */
525
+ export const PLATFORM_AUTH_INSTANCE_BEARER_REF = 'zitadel:platform:instanceBearer';
526
+ const PLATFORM_AUTH_BEARER_USER_NAME = 'foundry-instance-owner';
518
527
  /**
519
528
  * Retries `action` up to `attempts` times, sleeping increasingly between
520
529
  * tries -- the same shape the admin machine user's own delivery retry (just
@@ -758,8 +767,24 @@ async function liveLoginClientTokenHash(coolify, appId) {
758
767
  * first), so it will not start (`parseApps` throws on an empty list) until
759
768
  * then either. Safe to run again: the next apply that registers an app
760
769
  * redeploys the broker with a non-empty list.
761
- * - Whoever supplies `deps.zitadel` (or `zitadelForInstance`) must hold a
762
- * bearer token for the new instance, which foundry cannot read from it.
770
+ * - ~~Whoever supplies `deps.zitadel` (or `zitadelForInstance`) must hold a
771
+ * bearer token for the new instance, which foundry cannot read from it.~~
772
+ * Closed by #283: see "The instance bearer" below.
773
+ *
774
+ * The instance bearer (#283): the container creates no instance of its own
775
+ * (`ZITADEL_FIRSTINSTANCE_SKIP`). foundry generates an RSA keypair, puts the
776
+ * public half in `ZITADEL_SYSTEMAPIUSERS` (a system API user with
777
+ * SYSTEM_OWNER only), and once the container answers calls ZITADEL's system
778
+ * API `CreateInstance` with a machine owner (IAM_OWNER of the new instance)
779
+ * signed by the private half, which lives in memory for that run only. The
780
+ * owner's PAT comes back once and is handed to
781
+ * {@link PlatformAuthDeps.deliverSecret} under
782
+ * {@link PLATFORM_AUTH_INSTANCE_BEARER_REF}, before anything else uses it. A
783
+ * host builds `zitadelForInstance` as `(url) => createZitadelAdminMinter({
784
+ * url, token: <the secret delivered under that ref> })` plus its own org
785
+ * calls; `zitadelForInstance` is first called after the delivery. No human
786
+ * admin is created: a person who needs console access is added by the host
787
+ * through that bearer.
763
788
  *
764
789
  * #197: a `cloudflared` connector for this tunnel is run
765
790
  * ({@link ensureCloudflaredConnector}), so `hostname` answers.
@@ -899,7 +924,19 @@ async function provisionPlatformAuth(context, deps) {
899
924
  // same rule `executor.ts`'s `ensureAuthCookieSecret` follows for a value
900
925
  // this package mints itself rather than receiving from a provider.
901
926
  const masterkey = await ensureMasterkey(deps.coolify, zitadelAppId, databaseInUse);
902
- const adminPassword = await ensureGeneratedEnv(deps.coolify, zitadelAppId, 'ZITADEL_ADMIN_PASSWORD', generateAdminPassword);
927
+ // #283: the instance is created once, with the system API, by a platform
928
+ // whose admin machine user is not recorded yet (one that already has it was
929
+ // built from a first instance and keeps it). The keypair is regenerated on
930
+ // any run that still has to create it, so the private half never needs to
931
+ // be held anywhere between runs.
932
+ const lostInstance = recorded[PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY];
933
+ if (lostInstance &&
934
+ !recorded[PLATFORM_AUTH_INSTANCE_SCOPE_KEY] &&
935
+ !recorded[PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY]) {
936
+ throw new Error(instanceWithoutBearerMessage(hostname, `instance ${lostInstance}`));
937
+ }
938
+ const needsInstance = !recorded[PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY] && !recorded[PLATFORM_AUTH_INSTANCE_SCOPE_KEY];
939
+ const systemKeys = needsInstance ? generateSystemApiKeypair() : undefined;
903
940
  const zitadelEnvs = [
904
941
  { key: 'ZITADEL_PORT', value: AUTH_ZITADEL_PORT },
905
942
  { key: 'ZITADEL_EXTERNALDOMAIN', value: hostname },
@@ -907,8 +944,7 @@ async function provisionPlatformAuth(context, deps) {
907
944
  { key: 'ZITADEL_EXTERNALSECURE', value: 'true' },
908
945
  { key: 'ZITADEL_TLS_ENABLED', value: 'false' },
909
946
  { key: 'ZITADEL_DATABASE_POSTGRES_DSN', value: db.internal_db_url },
910
- { key: 'ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME', value: 'zitadel-admin' },
911
- { key: 'ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORDCHANGEREQUIRED', value: 'false' },
947
+ { key: 'ZITADEL_FIRSTINSTANCE_SKIP', value: 'true' },
912
948
  { key: 'ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED', value: 'true' },
913
949
  { key: 'ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_BASEURI', value: `https://${hostname}/` },
914
950
  { key: 'ZITADEL_OIDC_DEFAULTLOGINURLV2', value: `https://${hostname}/login?authRequest=` },
@@ -921,8 +957,12 @@ async function provisionPlatformAuth(context, deps) {
921
957
  ];
922
958
  if (masterkey)
923
959
  zitadelEnvs.push({ key: 'ZITADEL_MASTERKEY', value: masterkey });
924
- if (adminPassword)
925
- zitadelEnvs.push({ key: 'ZITADEL_ADMIN_PASSWORD', value: adminPassword });
960
+ if (systemKeys) {
961
+ zitadelEnvs.push({
962
+ key: 'ZITADEL_SYSTEMAPIUSERS',
963
+ value: systemApiUsersEnv(systemKeys.publicKey),
964
+ });
965
+ }
926
966
  const loginEnvs = [
927
967
  {
928
968
  key: 'ZITADEL_API_URL',
@@ -1051,6 +1091,50 @@ async function provisionPlatformAuth(context, deps) {
1051
1091
  // a caller that reaches the step anyway.
1052
1092
  throw new Error(`platform:auth: the admin key of ZITADEL machine user ${lost} was minted but never delivered; delete that user and the recorded key ${PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY} to mint a new one`);
1053
1093
  }
1094
+ if (systemKeys) {
1095
+ // ZITADEL answers /debug/healthz without an instance; the openid
1096
+ // document below only exists once the instance does.
1097
+ await waitForInstance(`https://${hostname}/debug/healthz`, runtime, sleep);
1098
+ // Sent exactly once. Anything that may have reached ZITADEL leaves a
1099
+ // marker first, so the next run refuses instead of creating again.
1100
+ let created;
1101
+ try {
1102
+ created = await createZitadelInstance({
1103
+ customDomain: hostname,
1104
+ privateKey: systemKeys.privateKey,
1105
+ instanceName: PLATFORM_AUTH_ORG_NAME,
1106
+ firstOrgName: PLATFORM_AUTH_ORG_NAME,
1107
+ owner: { userName: PLATFORM_AUTH_BEARER_USER_NAME, name: 'Foundry (instance owner)' },
1108
+ }, { fetch: runtime.fetch, now: runtime.now ?? Date.now });
1109
+ }
1110
+ catch (error) {
1111
+ if (error instanceof CreateInstanceError && error.kind !== 'rejected') {
1112
+ await record(PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, 'unknown');
1113
+ }
1114
+ throw error;
1115
+ }
1116
+ const { instanceId, pat } = created;
1117
+ // Recorded before the PAT leaves this function, like the admin key below.
1118
+ await record(PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, instanceId);
1119
+ const attempts = runtime.deliverAttempts ?? 3;
1120
+ for (let attempt = 1;; attempt += 1) {
1121
+ try {
1122
+ await deps.deliverSecret(PLATFORM_AUTH_INSTANCE_BEARER_REF, pat);
1123
+ break;
1124
+ }
1125
+ catch (error) {
1126
+ if (attempt >= attempts) {
1127
+ const reason = redactSecrets(error instanceof Error ? error.message : String(error), [
1128
+ pat,
1129
+ ]);
1130
+ throw new Error(`platform:auth: the owner token of ZITADEL instance ${instanceId} was created but could not be delivered after ${attempts} tries (${reason}); ${instanceWithoutBearerMessage(hostname, `instance ${instanceId}`)}`);
1131
+ }
1132
+ await sleep(1000 * attempt);
1133
+ }
1134
+ }
1135
+ // The marker stays recorded but is ignored once this is set.
1136
+ await record(PLATFORM_AUTH_INSTANCE_SCOPE_KEY, instanceId);
1137
+ }
1054
1138
  // Minting needs a running instance behind the tunnel: the first apply
1055
1139
  // gets here right after the deploy.
1056
1140
  await waitForInstance(`https://${hostname}/.well-known/openid-configuration`, runtime, sleep);
@@ -3642,10 +3726,6 @@ async function ensureMasterkey(coolify, appId, databaseInUse) {
3642
3726
  }
3643
3727
  return randomBytes(24).toString('base64');
3644
3728
  }
3645
- /** `docs/deploy.md`: "ZITADEL_ADMIN_PASSWORD: 8+, upper, lower, digit, symbol". */
3646
- function generateAdminPassword() {
3647
- return `${randomBytes(18).toString('base64')}Aa1!`;
3648
- }
3649
3729
  /**
3650
3730
  * #171: how the host signs a files management request with the key this step
3651
3731
  * delivers under {@link FILES_MANAGEMENT_KEY_REF} (files'
@@ -0,0 +1,64 @@
1
+ /**
2
+ * #283: foundry's way into a brand-new ZITADEL instance. ZITADEL's system API
3
+ * (`POST /system/v1/instances/_create`, permission `system.instance.write`,
4
+ * https://github.com/zitadel/zitadel/blob/main/proto/zitadel/system.proto)
5
+ * creates the instance with a machine owner (IAM_OWNER of that instance) and
6
+ * returns that owner's personal access token (`pat`) once, inline. foundry
7
+ * authenticates to the system API with a short-lived RS256 JWT signed by a
8
+ * keypair it generates: the public half goes into the container as a system
9
+ * API user, the private half stays in memory for one run. The calls follow
10
+ * ZITADEL's source and proto and have not been run against a live instance.
11
+ */
12
+ /** The system API user's name; it is the JWT's `iss` and `sub`. */
13
+ export declare const SYSTEM_API_USER_NAME = "foundry";
14
+ /** A fresh RSA keypair: the public key (PEM) for ZITADEL, the private key (PKCS#8 PEM) for signing. */
15
+ export declare function generateSystemApiKeypair(): {
16
+ publicKey: string;
17
+ privateKey: string;
18
+ };
19
+ /**
20
+ * The value of `ZITADEL_SYSTEMAPIUSERS`: JSON in ZITADEL's `SystemAPIUsers`
21
+ * shape (`cmd/defaults.yaml`), `KeyData` being the base64 of the public key
22
+ * PEM. With no `Memberships` ZITADEL grants exactly System/SYSTEM_OWNER.
23
+ */
24
+ export declare function systemApiUsersEnv(publicKeyPem: string): string;
25
+ /** A 1-minute RS256 JWT for the system API: `iss` = `sub` = the user name, `aud` = the instance URL. */
26
+ export declare function signSystemApiJwt(privateKeyPem: string, audience: string, nowMs: number): string;
27
+ /**
28
+ * How a create failed. `rejected`: ZITADEL refused it for good before doing
29
+ * anything (bad key, bad request), so nothing exists. `exists`: an instance
30
+ * for the domain already exists. `uncertain`: the request may have reached
31
+ * ZITADEL (timeout, network error, 5xx, an unreadable answer), so an instance
32
+ * may exist. The caller records a marker for the last two.
33
+ */
34
+ export declare class CreateInstanceError extends Error {
35
+ readonly kind: 'rejected' | 'exists' | 'uncertain';
36
+ constructor(kind: 'rejected' | 'exists' | 'uncertain', message: string);
37
+ }
38
+ /** What a person does when an instance exists without a delivered bearer. Nothing is deleted automatically. */
39
+ export declare function instanceWithoutBearerMessage(hostname: string, detail: string): string;
40
+ export interface CreateInstanceRuntime {
41
+ fetch?: typeof fetch;
42
+ now: () => number;
43
+ /** Fetch timeout for the one create call (default 30s). */
44
+ timeoutMs?: number;
45
+ }
46
+ /**
47
+ * Creates the platform's ZITADEL instance on `customDomain` with one machine
48
+ * owner and returns its id and the owner's PAT. Sends exactly one request,
49
+ * never retries, and signs a fresh 60-second JWT for it. The PAT is returned
50
+ * to the caller only, never logged or put in an error.
51
+ */
52
+ export declare function createZitadelInstance(input: {
53
+ customDomain: string;
54
+ privateKey: string;
55
+ instanceName: string;
56
+ firstOrgName: string;
57
+ owner: {
58
+ userName: string;
59
+ name: string;
60
+ };
61
+ }, runtime: CreateInstanceRuntime): Promise<{
62
+ instanceId: string;
63
+ pat: string;
64
+ }>;
@@ -0,0 +1,125 @@
1
+ import { createSign, generateKeyPairSync } from 'node:crypto';
2
+ /**
3
+ * #283: foundry's way into a brand-new ZITADEL instance. ZITADEL's system API
4
+ * (`POST /system/v1/instances/_create`, permission `system.instance.write`,
5
+ * https://github.com/zitadel/zitadel/blob/main/proto/zitadel/system.proto)
6
+ * creates the instance with a machine owner (IAM_OWNER of that instance) and
7
+ * returns that owner's personal access token (`pat`) once, inline. foundry
8
+ * authenticates to the system API with a short-lived RS256 JWT signed by a
9
+ * keypair it generates: the public half goes into the container as a system
10
+ * API user, the private half stays in memory for one run. The calls follow
11
+ * ZITADEL's source and proto and have not been run against a live instance.
12
+ */
13
+ /** The system API user's name; it is the JWT's `iss` and `sub`. */
14
+ export const SYSTEM_API_USER_NAME = 'foundry';
15
+ /** A fresh RSA keypair: the public key (PEM) for ZITADEL, the private key (PKCS#8 PEM) for signing. */
16
+ export function generateSystemApiKeypair() {
17
+ return generateKeyPairSync('rsa', {
18
+ modulusLength: 2048,
19
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
20
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
21
+ });
22
+ }
23
+ /**
24
+ * The value of `ZITADEL_SYSTEMAPIUSERS`: JSON in ZITADEL's `SystemAPIUsers`
25
+ * shape (`cmd/defaults.yaml`), `KeyData` being the base64 of the public key
26
+ * PEM. With no `Memberships` ZITADEL grants exactly System/SYSTEM_OWNER.
27
+ */
28
+ export function systemApiUsersEnv(publicKeyPem) {
29
+ return JSON.stringify({
30
+ [SYSTEM_API_USER_NAME]: { KeyData: Buffer.from(publicKeyPem).toString('base64') },
31
+ });
32
+ }
33
+ const b64url = (input) => Buffer.from(input).toString('base64url');
34
+ /** A 1-minute RS256 JWT for the system API: `iss` = `sub` = the user name, `aud` = the instance URL. */
35
+ export function signSystemApiJwt(privateKeyPem, audience, nowMs) {
36
+ const iat = Math.floor(nowMs / 1000);
37
+ const head = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
38
+ const body = b64url(JSON.stringify({
39
+ iss: SYSTEM_API_USER_NAME,
40
+ sub: SYSTEM_API_USER_NAME,
41
+ aud: audience,
42
+ iat,
43
+ exp: iat + 60,
44
+ }));
45
+ const signature = createSign('RSA-SHA256').update(`${head}.${body}`).sign(privateKeyPem);
46
+ return `${head}.${body}.${b64url(signature)}`;
47
+ }
48
+ /**
49
+ * How a create failed. `rejected`: ZITADEL refused it for good before doing
50
+ * anything (bad key, bad request), so nothing exists. `exists`: an instance
51
+ * for the domain already exists. `uncertain`: the request may have reached
52
+ * ZITADEL (timeout, network error, 5xx, an unreadable answer), so an instance
53
+ * may exist. The caller records a marker for the last two.
54
+ */
55
+ export class CreateInstanceError extends Error {
56
+ kind;
57
+ constructor(kind, message) {
58
+ super(message);
59
+ this.kind = kind;
60
+ this.name = 'CreateInstanceError';
61
+ }
62
+ }
63
+ /** What a person does when an instance exists without a delivered bearer. Nothing is deleted automatically. */
64
+ export function instanceWithoutBearerMessage(hostname, detail) {
65
+ return `platform:auth: a ZITADEL instance may already exist for ${hostname} without a delivered bearer (${detail}). To recover, delete the instance with the system API (DELETE /system/v1/instances/{id}) or redeploy the ZITADEL database, remove the recorded key zitadel_platform_instance_undelivered, then apply again`;
66
+ }
67
+ /**
68
+ * Creates the platform's ZITADEL instance on `customDomain` with one machine
69
+ * owner and returns its id and the owner's PAT. Sends exactly one request,
70
+ * never retries, and signs a fresh 60-second JWT for it. The PAT is returned
71
+ * to the caller only, never logged or put in an error.
72
+ */
73
+ export async function createZitadelInstance(input, runtime) {
74
+ const fetchImpl = runtime.fetch ?? globalThis.fetch;
75
+ const url = `https://${input.customDomain}`;
76
+ let response;
77
+ try {
78
+ response = await fetchImpl(`${url}/system/v1/instances/_create`, {
79
+ method: 'POST',
80
+ signal: AbortSignal.timeout(runtime.timeoutMs ?? 30_000),
81
+ headers: {
82
+ authorization: `Bearer ${signSystemApiJwt(input.privateKey, url, runtime.now())}`,
83
+ 'content-type': 'application/json',
84
+ },
85
+ body: JSON.stringify({
86
+ instanceName: input.instanceName,
87
+ firstOrgName: input.firstOrgName,
88
+ customDomain: input.customDomain,
89
+ machine: {
90
+ userName: input.owner.userName,
91
+ name: input.owner.name,
92
+ personalAccessToken: {
93
+ expirationDate: new Date(runtime.now() + 10 * 365 * 86_400_000).toISOString(),
94
+ },
95
+ },
96
+ }),
97
+ });
98
+ }
99
+ catch {
100
+ throw new CreateInstanceError('uncertain', instanceWithoutBearerMessage(input.customDomain, 'the create request timed out or failed'));
101
+ }
102
+ if (response.status === 401) {
103
+ throw new CreateInstanceError('rejected', 'platform:auth: system API key rejected (HTTP 401); nothing was created');
104
+ }
105
+ if (response.status === 409) {
106
+ throw new CreateInstanceError('exists', instanceWithoutBearerMessage(input.customDomain, 'ZITADEL answered 409, already exists'));
107
+ }
108
+ if (!response.ok) {
109
+ // A 4xx is a refusal before anything was created; a 5xx may not be.
110
+ throw new CreateInstanceError(response.status < 500 ? 'rejected' : 'uncertain', response.status < 500
111
+ ? `platform:auth: ZITADEL refused to create the instance (HTTP ${response.status}); nothing was created`
112
+ : instanceWithoutBearerMessage(input.customDomain, `ZITADEL answered HTTP ${response.status}`));
113
+ }
114
+ let body = {};
115
+ try {
116
+ body = (await response.json());
117
+ }
118
+ catch {
119
+ // Handled below.
120
+ }
121
+ if (!body.instanceId || !body.pat) {
122
+ throw new CreateInstanceError('uncertain', instanceWithoutBearerMessage(input.customDomain, 'the answer had no instance id or token'));
123
+ }
124
+ return { instanceId: body.instanceId, pat: body.pat };
125
+ }
@@ -4,7 +4,7 @@ import type { OrgId } from './org.js';
4
4
  * {@link DeclaredPlatform}. `enabled` turns the service's own provisioning
5
5
  * on; `hostname` is where it is served (default `<service>.<apex>`,
6
6
  * {@link defaultPlatformHostname}); `gitRef` is the release it runs, the
7
- * same "declared, not discovered" shape `HostingConfig.gitBranch` uses for
7
+ * same "declared, not discovered" shape `HostingEntry.branch` uses for
8
8
  * an org's own app (`org.ts`).
9
9
  */
10
10
  export interface PlatformService {
@@ -79,13 +79,9 @@ function rowToOrg(row) {
79
79
  hosting: {
80
80
  enabled: row.hostingEnabled,
81
81
  usesDomain: row.hostingUsesDomain,
82
- database: row.hostingDatabase,
83
82
  tunnel: row.hostingTunnel,
84
- gitBranch: row.hostingGitBranch,
85
- buildPack: row.hostingBuildPack,
86
83
  issuerApps: row.hostingIssuerApps,
87
- accessEmails: row.hostingAccessEmails,
88
- appEnv: row.hostingAppEnv,
84
+ entries: row.hostingEntries,
89
85
  },
90
86
  sso: { enabled: row.ssoEnabled },
91
87
  guests: { enabled: row.guestsEnabled },
@@ -122,13 +118,9 @@ function orgToRow(org) {
122
118
  siteUsesDomain: org.site.usesDomain,
123
119
  hostingEnabled: org.hosting.enabled,
124
120
  hostingUsesDomain: org.hosting.usesDomain,
125
- hostingDatabase: org.hosting.database,
126
121
  hostingTunnel: org.hosting.tunnel,
127
- hostingGitBranch: org.hosting.gitBranch,
128
- hostingBuildPack: org.hosting.buildPack,
129
122
  hostingIssuerApps: org.hosting.issuerApps,
130
- hostingAccessEmails: org.hosting.accessEmails,
131
- hostingAppEnv: org.hosting.appEnv ?? {},
123
+ hostingEntries: org.hosting.entries,
132
124
  ssoEnabled: org.sso.enabled,
133
125
  guestsEnabled: org.guests.enabled,
134
126
  soloEnabled: org.solo.enabled,