@wtfalch/foundry 0.16.5 → 0.16.7

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.
package/dist/index.d.ts CHANGED
@@ -23,6 +23,8 @@ export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
23
23
  export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, PlatformOrgPortSource, AgoraHostPort, AgoraHostValues, LoginHostBinding, LoginHostBindingsPort, } from './plan/index.js';
24
24
  export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
25
25
  export { HttpJobsSchedulerClient } from './plan/index.js';
26
+ export type { ZitadelListAppsRawApp } from './plan/index.js';
27
+ export { adaptZitadelListApps } from './plan/index.js';
26
28
  export type { MailIdentityApp, MailIdentityAudienceCheck } from './plan/index.js';
27
29
  export { checkMailIdentityAudience } from './plan/index.js';
28
30
  export type { MailAllowlistApp } from './plan/index.js';
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export { DEFAULT_ORG_PLATFORM_READINESS, ORG_PLATFORM_KEY_REFS, agoraHostValuesF
10
10
  export { PlatformOrgBlockedError, PlatformOrgPorts } from './plan/index.js';
11
11
  export { FoundryStepExecutor, NotBuiltError, healthUrls, loginHostnameFor } from './plan/index.js';
12
12
  export { HttpJobsSchedulerClient } from './plan/index.js';
13
+ export { adaptZitadelListApps } from './plan/index.js';
13
14
  export { checkMailIdentityAudience } from './plan/index.js';
14
15
  export { checkMailAppsAllowlist } from './plan/index.js';
15
16
  export { FoundryPlatformStepExecutor, ProvidersPlatformCloudflareReader, ProvidersPlatformCoolifyReader, createZitadelAdminMinter, createZitadelProjectRoles, PLATFORM_AUTH_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ADMIN_KEY_DESCRIPTION, } from './plan/index.js';
@@ -35,4 +35,6 @@ export type { MailAllowlistApp } from './mail-identity-allowlist.js';
35
35
  export { checkMailAppsAllowlist } from './mail-identity-allowlist.js';
36
36
  export type { CoolifyPort, CloudflarePort, ZitadelPort, CallsSfuPort, JobsSchedulerPort, FilesOrganisationPort, AiOrganisationPort, FoundryStepExecutorPorts, PlatformOrgPortSource, } from './executor.js';
37
37
  export { FoundryStepExecutor, NotBuiltError, healthUrls } from './executor.js';
38
+ export type { ZitadelListAppsRawApp } from './executor.js';
39
+ export { adaptZitadelListApps } from './executor.js';
38
40
  export { HttpJobsSchedulerClient } from './jobs-scheduler-http.js';
@@ -18,4 +18,5 @@ export { loginHostnameFor } from './login-host.js';
18
18
  export { checkMailIdentityAudience } from './mail-identity.js';
19
19
  export { checkMailAppsAllowlist } from './mail-identity-allowlist.js';
20
20
  export { FoundryStepExecutor, NotBuiltError, healthUrls } from './executor.js';
21
+ export { adaptZitadelListApps } from './executor.js';
21
22
  export { HttpJobsSchedulerClient } from './jobs-scheduler-http.js';
@@ -466,6 +466,13 @@ export declare const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = "ai_platform_migrat
466
466
  * that throws) is repaired by the next one.
467
467
  */
468
468
  export declare const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = "ai_platform_deploy_pending";
469
+ /**
470
+ * The Coolify deployment queued for the host (its `deployment_uuid`, or
471
+ * `untracked` when the deploy answered none), set when that deploy is queued
472
+ * and cleared once it finished. The mint waits for it, so a retry after a
473
+ * failed wait still waits instead of minting against the old container.
474
+ */
475
+ export declare const PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY = "ai_platform_runner_deployment";
469
476
  /**
470
477
  * #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
471
478
  * dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
@@ -523,6 +523,13 @@ export const PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY = 'ai_platform_migrate_blocke
523
523
  * that throws) is repaired by the next one.
524
524
  */
525
525
  export const PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY = 'ai_platform_deploy_pending';
526
+ /**
527
+ * The Coolify deployment queued for the host (its `deployment_uuid`, or
528
+ * `untracked` when the deploy answered none), set when that deploy is queued
529
+ * and cleared once it finished. The mint waits for it, so a retry after a
530
+ * failed wait still waits instead of minting against the old container.
531
+ */
532
+ export const PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY = 'ai_platform_runner_deployment';
526
533
  /**
527
534
  * #171: the resource key `planFiles` plans under and `FoundryPlatformStepExecutor`
528
535
  * dispatches on, the {@link PLATFORM_AUTH_KEY} shape for the files host.
@@ -1042,6 +1049,10 @@ function planSfu(platform) {
1042
1049
  * the broker's own token as part of running this step, and a step that is
1043
1050
  * never planned never runs. So this still plans the step -- surfacing
1044
1051
  * `executorBlocked` in `blocked` for visibility, not to stop planning.
1052
+ * Exception (the auth-broker loop): once everything else is applied and
1053
+ * `otherInputsMissing` is non-empty, nothing this step can do changes the
1054
+ * missing inputs, so it is reported blocked and not planned -- else every
1055
+ * `driveApply` round replans it until `maxRounds`.
1045
1056
  *
1046
1057
  * #211 round 6: round 3's `!brokerApplied` guard assumed "no executor" and
1047
1058
  * "broker not yet created" always coincide. They don't: a broker recorded on
@@ -1152,10 +1163,25 @@ function planAuth(platform, recordedIds, executorBlocked, executorMissing, authD
1152
1163
  // #211 round 3: a blocked reason with no broker yet created also means the
1153
1164
  // executor is unusable (no auth dependencies) -- nothing to plan.
1154
1165
  // A blocked reason with the broker already created means only its deploy
1155
- // is held back; plan the step anyway so it can keep retrying and, once
1156
- // #209/#210 land, deploy on the very next apply.
1166
+ // is held back; plan the step anyway, so it can mint what it can (#209) and,
1167
+ // once #210 lands, deploy on the very next apply.
1157
1168
  if (executorBlocked && !brokerApplied)
1158
1169
  return { step: null, blocked: executorBlocked };
1170
+ // Loop fix: everything but the broker deploy is applied and that deploy
1171
+ // waits on an input (`otherInputsMissing`, never the token this step mints)
1172
+ // that another run supplies -- re-applying changes nothing, so the step
1173
+ // planned and applied every `driveApply` round, with dozens of Coolify
1174
+ // calls each, until `maxRounds`. Report it blocked and plan nothing; the
1175
+ // next plan re-reads the live inputs, so it plans the step once they exist.
1176
+ // The login client is not required here: `provisionPlatformAuth` mints it
1177
+ // only once those same inputs exist, so it is never recorded before then.
1178
+ if (executorBlocked &&
1179
+ otherInputsMissing.length > 0 &&
1180
+ recordedIds[key] &&
1181
+ connectorApplied &&
1182
+ recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS) {
1183
+ return { step: null, blocked: executorBlocked };
1184
+ }
1159
1185
  return {
1160
1186
  step: {
1161
1187
  action: {
@@ -6,7 +6,7 @@ import { redactSecrets } from '../calls-sfu-deploy-support.js';
6
6
  import { ensureCloudflaredConnector } from './cloudflared-connector.js';
7
7
  import { ACCESS_POLICY_NAME, getDatabaseStarted, isNotFoundError, isRunningOrStarting, } from './executor.js';
8
8
  import { coolifyDomains } from './hostnames.js';
9
- import { AUTH_ZITADEL_RUN_OPTIONS, 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_AUTH_ZITADEL_RUN_OPTIONS_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, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, parseBrokerDeployRecord, parseLoginClientRecord, platformMailMxHost, } from './platform-diff.js';
9
+ import { AUTH_ZITADEL_RUN_OPTIONS, 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_RUNNER_DEPLOYMENT_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_AUTH_ZITADEL_RUN_OPTIONS_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, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, parseBrokerDeployRecord, parseLoginClientRecord, platformMailMxHost, } from './platform-diff.js';
10
10
  import { applyCallsSfuPlatformStep } from './platform-sfu.js';
11
11
  import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, } from './zitadel-system-api.js';
12
12
  /** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
@@ -3147,16 +3147,18 @@ const PLATFORM_AI_WORKER_SCOPE_KEY = 'coolify_platform_ai_worker_app';
3147
3147
  * Written only when absent, never overwritten: an operator's own model list
3148
3148
  * or worker bounds survive every apply. Values are `.env.example`'s, and
3149
3149
  * `AI_MODELS=[]` is the README's "honest configuration for a host with no
3150
- * approved text model".
3150
+ * approved text model". The runner needs `AI_WORKER_TIMEOUT_MS` too: ai's
3151
+ * `runtimeHost()` (`apps/host/src/lib/authz/ai-host.ts`) requires it, and
3152
+ * every route that checks readiness fails without it.
3151
3153
  */
3152
3154
  const PLATFORM_AI_RUNNER_DEFAULTS = [
3153
3155
  { key: 'AI_MODELS', value: '[]' },
3154
3156
  { key: 'AI_FORGE_BINDINGS', value: '[]' },
3157
+ { key: 'AI_WORKER_TIMEOUT_MS', value: '60000' },
3155
3158
  ];
3156
3159
  const PLATFORM_AI_WORKER_DEFAULTS = [
3157
3160
  ...PLATFORM_AI_RUNNER_DEFAULTS,
3158
3161
  { key: 'AI_WORKER_BATCH', value: '10' },
3159
- { key: 'AI_WORKER_TIMEOUT_MS', value: '60000' },
3160
3162
  ];
3161
3163
  /**
3162
3164
  * #172: makes `coolify:app` keyed {@link PLATFORM_AI_KEY} true -- the ai host
@@ -3402,8 +3404,12 @@ async function provisionPlatformAi(context, deps) {
3402
3404
  }
3403
3405
  await coolify.raw('POST', `/applications/${migrate.appId}/stop`);
3404
3406
  }
3405
- if (deployRunner)
3406
- await coolify.deploy(runner.appId);
3407
+ // `deploy` only queues a build that takes minutes, so the id of the host's
3408
+ // deployment is kept for the mint below to wait on.
3409
+ const runnerDeployment = deployRunner ? await coolify.deploy(runner.appId) : undefined;
3410
+ if (deployRunner) {
3411
+ await record(PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY, queuedDeploymentUuid(runnerDeployment) ?? 'untracked');
3412
+ }
3407
3413
  if (deployWorker)
3408
3414
  await coolify.deploy(worker.appId);
3409
3415
  await record(PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, '');
@@ -3422,6 +3428,25 @@ async function provisionPlatformAi(context, deps) {
3422
3428
  if (lost) {
3423
3429
  throw new Error(`platform:ai: AI management key ${lost} was minted but never delivered; revoke it and remove the recorded key ${PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY} to mint a new one`);
3424
3430
  }
3431
+ // The host answers 503 (or nothing) until the deployment that carries its
3432
+ // management settings has finished.
3433
+ const queued = context.recordedIds()[PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY];
3434
+ if (queued) {
3435
+ try {
3436
+ await waitForAiDeployment(coolify, runner.appId, queued, runtime, sleep);
3437
+ }
3438
+ catch (error) {
3439
+ // Only a build still running is worth waiting on again. A deployment
3440
+ // that failed, was cancelled or cannot be read is not: mark the host
3441
+ // pending so the next apply queues a new one instead of polling it.
3442
+ if (!(error instanceof AiDeploymentTimeoutError)) {
3443
+ await record(PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, '1');
3444
+ await record(PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY, '');
3445
+ }
3446
+ throw error;
3447
+ }
3448
+ await record(PLATFORM_AI_RUNNER_DEPLOYMENT_SCOPE_KEY, '');
3449
+ }
3425
3450
  // #201: a test injects its own minter; without one, build the real one
3426
3451
  // over ai's own POST /v1/platform/keys, signed with the key pinned above.
3427
3452
  const minter = deps.managementKeyMinter ??
@@ -3459,8 +3484,9 @@ async function provisionPlatformAi(context, deps) {
3459
3484
  if (error instanceof AiManagementKeyUntrustedError) {
3460
3485
  throw new Error(`platform:ai: ${error.message}`);
3461
3486
  }
3462
- // The cause is dropped in case it echoes a credential.
3463
- throw new Error('platform:ai: the AI management key could not be minted (the minter failed; its error is not shown). Nothing was recorded; apply again');
3487
+ // The cause is dropped in case it echoes a credential; only the HTTP
3488
+ // status, or a network error's name and code, say why.
3489
+ throw new Error(`platform:ai: the AI management key could not be minted (${describeMintFailure(error)}). Nothing was recorded; apply again`);
3464
3490
  }
3465
3491
  const { credentialId, organisationId, secret } = minted;
3466
3492
  await record(PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, credentialId);
@@ -3549,6 +3575,70 @@ class MigrationNotHealthyError extends Error {
3549
3575
  this.lastStatus = lastStatus;
3550
3576
  }
3551
3577
  }
3578
+ /** The `deployment_uuid` in the `{deployments:[...]}` / array answer of Coolify's deploy call, if any. */
3579
+ function queuedDeploymentUuid(result) {
3580
+ const rows = Array.isArray(result)
3581
+ ? result
3582
+ : result?.deployments;
3583
+ const uuid = Array.isArray(rows)
3584
+ ? rows[0]?.deployment_uuid
3585
+ : undefined;
3586
+ return typeof uuid === 'string' && uuid ? uuid : undefined;
3587
+ }
3588
+ /**
3589
+ * Waits until the host's queued deployment has `finished`; throws on
3590
+ * `failed`/`cancelled-by-user` or once `migrateTimeoutMs` has passed. With no
3591
+ * deployment id (`untracked`) the only signal left is the app's own status,
3592
+ * which an old container can already satisfy, so that is best effort.
3593
+ */
3594
+ /** The host's deployment was still running when the wait gave up. */
3595
+ class AiDeploymentTimeoutError extends Error {
3596
+ }
3597
+ async function waitForAiDeployment(coolify, appId, deploymentUuid, runtime, sleep) {
3598
+ const now = runtime.now ?? Date.now;
3599
+ const timeout = runtime.migrateTimeoutMs ?? 600_000;
3600
+ const interval = runtime.migrateIntervalMs ?? 5_000;
3601
+ const deadline = now() + timeout;
3602
+ let status;
3603
+ for (;;) {
3604
+ await sleep(interval);
3605
+ if (deploymentUuid === 'untracked') {
3606
+ status = (await coolify.getApplication(appId)).status;
3607
+ if (status?.startsWith('running:healthy'))
3608
+ return;
3609
+ }
3610
+ else {
3611
+ const found = (await coolify.raw('GET', `/deployments/${deploymentUuid}`));
3612
+ status = typeof found?.status === 'string' ? found.status : undefined;
3613
+ if (status === 'finished')
3614
+ return;
3615
+ if (status === 'failed' || status === 'cancelled-by-user') {
3616
+ throw new Error(`platform:ai: the host's deployment for ${appId} ended ${status}; the AI management key was not minted. Check its build log in Coolify, then apply again`);
3617
+ }
3618
+ }
3619
+ if (now() >= deadline) {
3620
+ throw new AiDeploymentTimeoutError(`platform:ai: the host's deployment for ${appId} did not finish within ${Math.round(timeout / 1000)}s (last status ${status ?? 'unknown'}); the AI management key was not minted. Apply again`);
3621
+ }
3622
+ }
3623
+ }
3624
+ /**
3625
+ * Why a mint failed, safe to show: the status ai answered (the minter's own
3626
+ * fixed message), or a network error's `name` and `cause.code`. Never a
3627
+ * `.message` or body, which could echo a credential.
3628
+ */
3629
+ function describeMintFailure(error) {
3630
+ const status = /^ai's platform\/keys route answered (\d{3})$/.exec(error instanceof Error ? error.message : '')?.[1];
3631
+ if (status)
3632
+ return `ai answered HTTP ${status}`;
3633
+ if (error instanceof Error) {
3634
+ const code = error.cause?.code;
3635
+ // A plain `Error` is the minter's own (a malformed body), not the network.
3636
+ if (typeof code === 'string' || error.name !== 'Error') {
3637
+ return `network error ${error.name}${typeof code === 'string' ? ` ${code}` : ''}`;
3638
+ }
3639
+ }
3640
+ return 'the minter failed; its error is not shown';
3641
+ }
3552
3642
  /**
3553
3643
  * Polls the app until Coolify reports it stopped (`exited`/`stopped`), so a
3554
3644
  * healthy status read afterwards belongs to a deployment queued after this.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.16.5",
3
+ "version": "0.16.7",
4
4
  "description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
5
5
  "repository": {
6
6
  "type": "git",