@wtfalch/foundry 0.16.14 → 0.16.16

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.
@@ -127,6 +127,17 @@ export const PLATFORM_ARCHON_KEY = 'platform:archon';
127
127
  */
128
128
  export const PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY = 'archon_applied_hostname';
129
129
  export const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = 'archon_applied_git_ref';
130
+ /**
131
+ * #398: the integrations management key id Archon runs with, recorded by
132
+ * `provisionPlatformArchon` only after its deployment finished and its health
133
+ * check passed with that id in its env. `provisionPlatformIntegrations`'s
134
+ * rotation step reads this before dropping a previous key id from the
135
+ * runner's `MANAGEMENT_PUBLIC_KEYS`: the old key stays pinned until this
136
+ * matches the new id, so a request signed by an Archon container still
137
+ * running the old key is never refused. If Archon's deploy fails this stays
138
+ * unrecorded and the old key stays pinned.
139
+ */
140
+ export const PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY = 'archon_integrations_key_synced';
130
141
  /**
131
142
  * #211 round 4: the broker app id Archon last registered its entry with.
132
143
  * `provisionPlatformArchon` reads the broker to register with from
@@ -138,6 +149,22 @@ export const PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY = 'archon_applied_git_ref
138
149
  * changed hostname or `gitRef` does.
139
150
  */
140
151
  export const PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY = 'archon_applied_broker_app';
152
+ /**
153
+ * The `AUTHZ_BOOTSTRAP_OPERATOR` the last Archon apply set (`none` when it set
154
+ * none). `planArchon` plans the step again when the owner's id recorded by
155
+ * `platform:auth` differs from it, so Archon picks the owner up once auth
156
+ * has recorded it.
157
+ */
158
+ export const PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY = 'archon_applied_bootstrap_operator';
159
+ /**
160
+ * The owner's ZITADEL user id Archon takes as its first operator, from
161
+ * {@link PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY}. Undefined when none is
162
+ * recorded or the recorded value is `none` (no owner declared).
163
+ */
164
+ export function archonBootstrapOperator(recorded) {
165
+ const id = recorded[PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY];
166
+ return id && id !== 'none' ? id : undefined;
167
+ }
141
168
  /**
142
169
  * #175: set to '1' before the step points the tunnel at `http_status:404`
143
170
  * (Access app rebuild), cleared to '' once the real ingress is written back.
@@ -448,8 +475,14 @@ export function appliedAppDomains(org, recorded, entryKey) {
448
475
  * The entry's `env` values that have a value, in env-name order, and the env
449
476
  * names whose source has none yet. Shared by `planApp` and the executor's
450
477
  * `pushAppEnv` so both read the same recorded ids.
478
+ *
479
+ * A secret source is never a value here. `secrets` names each one that can be
480
+ * filled (`available`), `unresolvedSecrets` each that cannot, and `marker`
481
+ * is `values` plus a `secret:<source>` label per available secret: the JSON
482
+ * `coolifyApp` records as applied, so a declared change replans and no secret
483
+ * is written into it.
451
484
  */
452
- export function resolveAppEnv(org, entry, recorded) {
485
+ export function resolveAppEnv(org, entry, recorded, available = { platformMailKey: false, sharedSecret: false }) {
453
486
  const sources = {
454
487
  filesOrganisationId: recorded[`files:organisation:${org.id}`],
455
488
  aiOrganisationId: recorded[`ai:organisation:${org.id}`],
@@ -457,14 +490,38 @@ export function resolveAppEnv(org, entry, recorded) {
457
490
  };
458
491
  const values = {};
459
492
  const unresolved = [];
493
+ const secrets = [];
494
+ const unresolvedSecrets = [];
495
+ const marker = {};
460
496
  for (const [name, declared] of Object.entries(entry.env).sort(([a], [b]) => (a < b ? -1 : 1))) {
497
+ if (typeof declared !== 'string' && declared.from === 'platformMailKey') {
498
+ if (available.platformMailKey) {
499
+ secrets.push({ name, source: 'platformMailKey' });
500
+ marker[name] = 'secret:platformMailKey';
501
+ }
502
+ else
503
+ unresolvedSecrets.push([name, 'platformMailKey']);
504
+ continue;
505
+ }
506
+ if (typeof declared !== 'string' && declared.from === 'sharedSecret') {
507
+ const label = `sharedSecret ${declared.name}`;
508
+ if (available.sharedSecret) {
509
+ secrets.push({ name, source: declared });
510
+ marker[name] = `secret:sharedSecret:${declared.name}`;
511
+ }
512
+ else
513
+ unresolvedSecrets.push([name, label]);
514
+ continue;
515
+ }
461
516
  const value = typeof declared === 'string' ? declared : sources[declared.from];
462
- if (value)
517
+ if (value) {
463
518
  values[name] = value;
519
+ marker[name] = value;
520
+ }
464
521
  else if (typeof declared !== 'string')
465
522
  unresolved.push([name, declared.from]);
466
523
  }
467
- return { values, unresolved };
524
+ return { values, unresolved, secrets, unresolvedSecrets, marker };
468
525
  }
469
526
  /**
470
527
  * #172: the resource key `planAi` plans under and `FoundryPlatformStepExecutor`
@@ -535,13 +592,6 @@ export const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = 'ai_platform_migrate_blocke
535
592
  * that throws) is repaired by the next one.
536
593
  */
537
594
  export const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = 'ai_platform_deploy_pending';
538
- /**
539
- * The Coolify deployment queued for the host (its `deployment_uuid`, or
540
- * `untracked` when the deploy answered none), set when that deploy is queued
541
- * and cleared once it finished. The mint waits for it, so a retry after a
542
- * failed wait still waits instead of minting against the old container.
543
- */
544
- export const PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY = 'ai_platform_runner_deployment';
545
595
  /**
546
596
  * #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
547
597
  * dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
@@ -715,6 +765,17 @@ export function planPlatform(platform, live, options = {}) {
715
765
  unmanaged.push({ description: ai.blocked, key: 'ai' });
716
766
  if (archon.blocked)
717
767
  unmanaged.push({ description: archon.blocked, key: 'archon' });
768
+ // #398: the old integrations key stays pinned until Archon holds the new
769
+ // id; name that when Archon is what is stopping it.
770
+ if (archon.blocked &&
771
+ recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY] &&
772
+ recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY] !==
773
+ recordedIds[PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY]) {
774
+ unmanaged.push({
775
+ description: `the previous integrations management key ${recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY]} stays pinned on the runner until Archon is redeployed with the new key, and Archon is blocked (see above)`,
776
+ key: 'integrations',
777
+ });
778
+ }
718
779
  unmanaged.push(...backups.blocked);
719
780
  return { plannedSteps, unmanaged, versionChanges };
720
781
  }
@@ -1344,6 +1405,52 @@ export const PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY = 'integrations_management_
1344
1405
  /** The hostname and gitRef the last successful apply put live; a difference from the declared ones plans the step again. */
1345
1406
  export const PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY = 'integrations_applied_hostname';
1346
1407
  export const PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY = 'integrations_applied_gitref';
1408
+ /**
1409
+ * #398: the key id currently pinned on the runner as the active entry of
1410
+ * `MANAGEMENT_PUBLIC_KEYS` -- `foundry-1` on the first mint, and whatever a
1411
+ * rotation minted next after that. `collectArchonServiceInputs` reads this
1412
+ * (falling back to the fixed `INTEGRATIONS_MANAGEMENT_KEY_ID` constant for a
1413
+ * platform applied before this key existed) for Archon's own
1414
+ * `INTEGRATIONS_MANAGEMENT_KEY_ID` env, so a rotated id reaches it without
1415
+ * Archon's step needing to know anything happened on the integrations side.
1416
+ */
1417
+ export const PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY = 'integrations_management_key_id';
1418
+ /**
1419
+ * #398: the key id the runner was last deployed with in its
1420
+ * `MANAGEMENT_PUBLIC_KEYS`, recorded by `provisionPlatformIntegrations` only
1421
+ * after the runner's deployment finished. Archon is not moved to a new key id
1422
+ * until this equals the active id, so Archon never signs with a key the
1423
+ * running runner does not yet hold.
1424
+ */
1425
+ export const PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY = 'integrations_runner_keyring_deployed';
1426
+ /**
1427
+ * #398: the same two-key discipline as
1428
+ * {@link PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY}, for a rotation's
1429
+ * new pair: recorded the moment it is minted, before delivery, and cleared
1430
+ * by nothing -- {@link PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY} moving past it
1431
+ * is what later runs read as "delivered and pinned", the same way the
1432
+ * once-only mint's guard is a second key rather than removing the first.
1433
+ * An id here that {@link PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY} has not
1434
+ * caught up to means the pair may be lost or half-applied; the step reports
1435
+ * it and mints no further pair.
1436
+ */
1437
+ export const PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY = 'integrations_management_key_rotation_undelivered';
1438
+ /**
1439
+ * #398: the previous key id, still pinned in `MANAGEMENT_PUBLIC_KEYS` beside
1440
+ * the new one, pending removal -- set the moment the new pair is pinned
1441
+ * beside it, and cleared (to `''`, the {@link PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY}
1442
+ * convention for "was set, now isn't") once the previous id is actually
1443
+ * removed from the keyring. Its presence is the overlap window: both keys
1444
+ * verify a request while it holds.
1445
+ */
1446
+ export const PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY = 'integrations_management_key_rotation_previous';
1447
+ /**
1448
+ * #398: how a rotation is requested. A person records any non-empty value
1449
+ * under this scope key; `planIntegrations` then plans the step again and
1450
+ * `provisionPlatformIntegrations` mints the new pair, clearing the key (to
1451
+ * `''`) as soon as the mint is recorded, so one request is one rotation.
1452
+ */
1453
+ export const PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY = 'integrations_management_key_rotation_requested';
1347
1454
  /**
1348
1455
  * #174: the integrations host (https://github.com/wtfalch/integrations) -- its
1349
1456
  * migrate job, web app and queue worker, their shared database, and its
@@ -1360,6 +1467,13 @@ export const PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY = 'integrations_appl
1360
1467
  * Blocked, not planned: an undelivered key, and an executor that cannot run
1361
1468
  * the step (no integrations dependencies, or no `@wtfalch/keys` values for
1362
1469
  * the host).
1470
+ *
1471
+ * #398: a rotation (see {@link PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY})
1472
+ * or one still pending removal of the previous key
1473
+ * ({@link PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY}) plans the
1474
+ * step again even when `applied`. A rotation whose new pair is minted but
1475
+ * not confirmed delivered is blocked, never planned: the step would throw
1476
+ * on every apply.
1363
1477
  */
1364
1478
  function planIntegrations(platform, recordedIds, executorBlocked) {
1365
1479
  if (!platform.integrations.enabled)
@@ -1368,7 +1482,41 @@ function planIntegrations(platform, recordedIds, executorBlocked) {
1368
1482
  const applied = Boolean(recordedIds[resourceKey('coolify', 'app', PLATFORM_INTEGRATIONS_KEY)]) &&
1369
1483
  recordedIds[PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY] === hostname &&
1370
1484
  recordedIds[PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY] === gitRef;
1371
- if (applied)
1485
+ const keyId = recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY];
1486
+ const previous = recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY];
1487
+ const keySet = Boolean(recordedIds[PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY]);
1488
+ // #398: a rotation key minted but not confirmed delivered and pinned blocks
1489
+ // the step whatever else is recorded: the executor throws on it, and
1490
+ // Archon's env refuses to pair the held key with an older id.
1491
+ const rotationUndelivered = recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY];
1492
+ if (rotationUndelivered && rotationUndelivered !== keyId) {
1493
+ return {
1494
+ step: null,
1495
+ blocked: `the integrations host is blocked, not planned: rotation key ${rotationUndelivered} was minted but not confirmed delivered and pinned; check the held key and the runner's MANAGEMENT_PUBLIC_KEYS, then remove the recorded key ${PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY} and set ${PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY} again to mint a new one`,
1496
+ };
1497
+ }
1498
+ // A request counts only once the first key is minted and no earlier
1499
+ // rotation is waiting to drop its previous key.
1500
+ const requested = Boolean(recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY]) &&
1501
+ keySet &&
1502
+ !previous;
1503
+ const requestIgnored = Boolean(recordedIds[PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY]) && !requested;
1504
+ // The previous key is dropped once Archon holds the new id, or at once when
1505
+ // Archon is not enabled (nothing else holds the old key). While Archon has
1506
+ // not synced there is nothing for this step to do.
1507
+ // Mirrors the executor's drop guards: the previous id is not the active
1508
+ // one, and the last delivered mint is the active one.
1509
+ const dropReady = Boolean(previous) &&
1510
+ previous !== keyId &&
1511
+ rotationUndelivered === keyId &&
1512
+ (!platform.archon.enabled ||
1513
+ recordedIds[PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY] === keyId);
1514
+ // The runner has not been deployed with the active key id yet (a deploy
1515
+ // failed after the key was pinned): plan the step to deploy it.
1516
+ const runnerStale = Boolean(keyId) &&
1517
+ recordedIds[PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY] !== keyId;
1518
+ const rotating = requested || dropReady || runnerStale;
1519
+ if (applied && !rotating)
1372
1520
  return { step: null, blocked: null };
1373
1521
  const undelivered = recordedIds[PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY];
1374
1522
  if (undelivered && !recordedIds[PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY]) {
@@ -1388,7 +1536,9 @@ function planIntegrations(platform, recordedIds, executorBlocked) {
1388
1536
  desired: { hostname, gitRef },
1389
1537
  },
1390
1538
  step: {
1391
- description: `provision the integrations host at ${hostname}`,
1539
+ description: `provision the integrations host at ${hostname}${requestIgnored
1540
+ ? `; the recorded rotation request ${PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY} is ignored and cleared (no key minted yet, or the previous key ${previous ?? ''} is still pending removal); set it again afterwards`
1541
+ : ''}`,
1392
1542
  destroys: false,
1393
1543
  migrates: false,
1394
1544
  },
@@ -1613,7 +1763,20 @@ function planArchon(platform, recordedIds, executorBlocked, authReady, drifted)
1613
1763
  Boolean(recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY]) &&
1614
1764
  recordedIds[PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY] ===
1615
1765
  recordedIds[PLATFORM_AUTH_BROKER_SCOPE_KEY] &&
1616
- !recordedIds[PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY];
1766
+ (recordedIds[PLATFORM_ARCHON_APPLIED_OPERATOR_SCOPE_KEY] ?? 'none') ===
1767
+ (archonBootstrapOperator(recordedIds) ?? 'none') &&
1768
+ !recordedIds[PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY] &&
1769
+ // #398: a rotated integrations key id Archon's env was not yet written
1770
+ // with (see PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY). Absent
1771
+ // before the first rotation on a platform applied before #398.
1772
+ // Only while integrations is enabled, and only once the runner is deployed
1773
+ // with that id (PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY).
1774
+ (!platform.integrations.enabled ||
1775
+ !recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY] ||
1776
+ recordedIds[PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY] !==
1777
+ recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY] ||
1778
+ recordedIds[PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY] ===
1779
+ recordedIds[PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY]);
1617
1780
  if (applied && !drifted)
1618
1781
  return { step: null, blocked: null };
1619
1782
  if (!platform.auth.enabled) {
@@ -68,6 +68,13 @@ export interface OrgPlatformReadiness {
68
68
  ai: string | null;
69
69
  jobs: string | null;
70
70
  };
71
+ /**
72
+ * The platform's Lettermint key (`PLATFORM_AUTH_BROKER_MAIL_KEY_REF`) is held,
73
+ * so an entry env from `platformMailKey` can be filled. Absent reads as not held.
74
+ */
75
+ mailKeyHeld?: boolean;
76
+ /** The host's held keys can `put`, so an entry env from `sharedSecret` can be filled. Absent reads as false. */
77
+ canHoldSecrets?: boolean;
71
78
  /**
72
79
  * True when the host wired no held-keys reader, so no key could be checked.
73
80
  * A missing key is then reported as "no reader given" instead of "not held".
@@ -91,6 +98,8 @@ export declare const ORG_PLATFORM_KEY_REFS: {
91
98
  /** The one method of Archon's held keys this package reads. A value is returned to the caller and never logged, recorded or put in a plan. */
92
99
  export interface HeldKeyResolver {
93
100
  resolve(ref: string): Promise<string | null | undefined>;
101
+ /** Holds a value under `ref`; only the org steps' shared secrets need it. */
102
+ put?(ref: string, value: string): Promise<void>;
94
103
  }
95
104
  /**
96
105
  * Which of {@link ORG_PLATFORM_KEY_REFS} `heldKeys` can resolve right now. A
@@ -1,4 +1,5 @@
1
1
  import type { CloudflareClient } from '@wtfalch/providers/cloudflare';
2
+ import type { DeclaredOrg } from '../org.js';
2
3
  import type { DeclaredPlatform } from '../platform.js';
3
4
  import type { DeclaredOrgStore, PlatformStore } from '../store.js';
4
5
  import type { StepAction } from './diff.js';
@@ -243,6 +244,13 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
243
244
  backupStorage?: boolean;
244
245
  backupStorageBlocked?: string;
245
246
  }>;
247
+ /**
248
+ * Gives the files runner a changed share-domain set and nothing else: its
249
+ * Coolify `domains`, `FILES_PUBLIC_HOSTS` and one redeploy of the runner.
250
+ * Returns the scope ids to record, or `null` when nothing changed
251
+ * (or the runner was never applied).
252
+ */
253
+ refreshFilesShare(_platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>): Promise<Record<string, string> | null>;
246
254
  execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
247
255
  resourceId?: string;
248
256
  } | undefined>;
@@ -259,6 +267,14 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
259
267
  private executeSfu;
260
268
  private coolifyJobs;
261
269
  }
270
+ /**
271
+ * Every ZITADEL OIDC/management/console/asset path -- `deploy/proxy/Caddyfile`'s
272
+ * own explicit list, restated here as one regex since the tunnel routes by a
273
+ * single `path` per ingress rule rather than a set. Anything not matching
274
+ * falls through to the login app's own ingress rule below (login v2 answers
275
+ * everything else, including the root).
276
+ */
277
+ export declare const AUTH_ZITADEL_PATH_PATTERN = "^/(oidc|oauth|saml|management|admin|system|auth|assets|debug/healthz|ui/console|\\.well-known|v2|v2beta|zitadel\\.[A-Za-z0-9.]+)(/|$)";
262
278
  /**
263
279
  * #283: the ref the new instance's IAM_OWNER machine token (a PAT) is
264
280
  * delivered under, once. A host reads it back to build `zitadelForInstance`
@@ -349,7 +365,7 @@ export declare function defaultStalwartForInstance(input: {
349
365
  * own shape.
350
366
  */
351
367
  export interface PlatformMailDeps {
352
- coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy'>;
368
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'createDockerImageApplication' | 'createDockerfileApplication' | 'raw' | 'listStorages' | 'createPersistentStorage' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait'>;
353
369
  cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord'>;
354
370
  /**
355
371
  * Must target the platform's own ZITADEL instance, or `zitadelForInstance`
@@ -393,7 +409,7 @@ export interface PlatformMailRuntime {
393
409
  * {@link PlatformStepExecutionContext}, bundled like {@link PlatformAuthDeps}.
394
410
  */
395
411
  export interface PlatformArchonDeps {
396
- coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy'>;
412
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'createDockerfileApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'raw' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait'>;
397
413
  cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'findTunnelByName' | 'createTunnel' | 'putTunnelConfig' | 'getTunnelToken' | 'findDnsRecord' | 'createDnsRecord' | 'updateDnsRecord' | 'listAccessApps' | 'createAccessApp' | 'listAccessPolicies' | 'createAccessPolicy' | 'deleteAccessApp'>;
398
414
  /** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
399
415
  zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp'>;
@@ -439,6 +455,17 @@ export interface PlatformArchonDeps {
439
455
  coolifyToken: string;
440
456
  cloudflareParentToken: string;
441
457
  };
458
+ /** Clock, sleep and fetch for the post-deploy health wait (#252); real ones by default. */
459
+ runtime?: PlatformArchonRuntime;
460
+ }
461
+ export interface PlatformArchonRuntime {
462
+ fetch?: typeof fetch;
463
+ sleep?: (ms: number) => Promise<void>;
464
+ now?: () => number;
465
+ /** How long to wait for `/api/health` to answer the new deploy (default 5 minutes). */
466
+ readyTimeoutMs?: number;
467
+ /** Pause between readiness polls (default 5 seconds). */
468
+ readyIntervalMs?: number;
442
469
  }
443
470
  /**
444
471
  * The name of foundry's own Cloudflare Access app in front of Archon. Same
@@ -554,7 +581,7 @@ export declare class AiManagementKeyUntrustedError extends Error {
554
581
  }
555
582
  /** #172: everything {@link provisionPlatformAi} needs beyond {@link PlatformStepExecutionContext}. */
556
583
  export interface PlatformAiDeps {
557
- coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
584
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'findApplicationByName' | 'getApplication' | 'requireGithubApp' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
558
585
  deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
559
586
  /**
560
587
  * Reads back a secret this step delivered on an earlier apply -- needed
@@ -682,7 +709,7 @@ export declare function brokerDeployRecordValue(brokerAppId: string, authAppsRaw
682
709
  * {@link PlatformAuthDeps}.
683
710
  */
684
711
  export interface PlatformFilesDeps {
685
- coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'raw'>;
712
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'updateApplication' | 'deploy' | 'deployAndWait' | 'raw'>;
686
713
  /**
687
714
  * Must target the platform's own sign-in (the instance `platform:auth`
688
715
  * built), or {@link zitadelForInstance} must be set.
@@ -791,7 +818,7 @@ export declare const FILES_INTROSPECTION_SECRET_REF = "zitadel:platform:files:in
791
818
  * {@link FoundryPlatformStepExecutor} keeps compiling with it omitted.
792
819
  */
793
820
  export interface PlatformIntegrationsDeps {
794
- coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'updateApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'deploy' | 'raw'>;
821
+ coolify: Pick<CoolifyPort, 'findProjectByName' | 'createProject' | 'findOrCreateEnvironment' | 'listServers' | 'requireGithubApp' | 'findApplicationByName' | 'getApplication' | 'createGithubAppApplication' | 'updateApplication' | 'listDatabases' | 'createPostgresDatabase' | 'getDatabase' | 'bulkUpsertEnvs' | 'listEnvs' | 'deploy' | 'deployAndWait' | 'raw'>;
795
822
  /** Hands the management private key to the host, once (`FoundryStepExecutorPorts.deliverSecret`'s contract). */
796
823
  deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
797
824
  /**
@@ -890,4 +917,18 @@ export declare class ProvidersPlatformCoolifyReader implements PlatformCoolifyRe
890
917
  constructor(client: Pick<CoolifyPort, 'getApplication' | 'getDatabase' | 'listDatabaseBackups'>, platformStore: Pick<PlatformStore, 'getServiceIds'>);
891
918
  listResources(): Promise<LiveResource[]>;
892
919
  }
920
+ /** Scope id (no `:`): the share domains the runner's Coolify `domains` was last set to, comma-joined. */
921
+ export declare const PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY = "platform_files_share_domains";
922
+ /**
923
+ * The runner's `FILES_PUBLIC_HOSTS`: the host-supplied `publicHosts` first, in
924
+ * their given order, then the share domains sorted, with no host twice.
925
+ * Empty when there is neither.
926
+ */
927
+ export declare function filesPublicHostsValue(publicHosts: string | undefined, shareDomains: readonly string[] | undefined): string;
928
+ /**
929
+ * The share domain of every org that has a domain and an entry declaring
930
+ * `uses.files.shareHost`: `share.<domain>`. The platform files step derives it
931
+ * from the declared orgs, for its apply and its drift check alike.
932
+ */
933
+ export declare function filesShareDomains(orgs: readonly DeclaredOrg[]): string[];
893
934
  export {};