@wtfalch/foundry 0.23.0 → 0.25.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.
- package/dist/bootstrap/declare.js +6 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/migrations/0048_hosting_gate_limit.sql +7 -0
- package/dist/migrations/0049_platform_uptime.sql +11 -0
- package/dist/org.d.ts +27 -23
- package/dist/org.js +73 -29
- package/dist/plan/diff.js +36 -28
- package/dist/plan/edge-access.d.ts +48 -1
- package/dist/plan/edge-access.js +51 -2
- package/dist/plan/edge-workers.d.ts +16 -1
- package/dist/plan/edge-workers.js +8 -0
- package/dist/plan/executor.d.ts +50 -17
- package/dist/plan/executor.js +93 -47
- package/dist/plan/gates.d.ts +42 -0
- package/dist/plan/gates.js +126 -0
- package/dist/plan/health-targets.js +3 -4
- package/dist/plan/index.d.ts +2 -1
- package/dist/plan/plan-apply.d.ts +6 -0
- package/dist/plan/plan-apply.js +8 -1
- package/dist/plan/platform-diff.d.ts +102 -0
- package/dist/plan/platform-diff.js +288 -0
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-org.js +4 -0
- package/dist/plan/platform-providers.d.ts +47 -0
- package/dist/plan/platform-providers.js +271 -14
- package/dist/plan/uptime-worker.d.ts +50 -0
- package/dist/plan/uptime-worker.js +229 -0
- package/dist/platform.d.ts +18 -0
- package/dist/platform.js +14 -0
- package/dist/store/postgres.js +31 -5
- package/dist/store/schema.d.ts +68 -0
- package/dist/store/schema.js +6 -0
- package/package.json +2 -2
|
@@ -6,10 +6,12 @@ import { redactSecrets } from '../calls-sfu-deploy-support.js';
|
|
|
6
6
|
import { ensureCloudflaredConnector } from './cloudflared-connector.js';
|
|
7
7
|
import { waitForDeploymentFinished, waitForHealthyVersion, waitForNewDeployment, } from './deploy-health.js';
|
|
8
8
|
import { ACCESS_POLICY_NAME, getDatabaseStarted, isNotFoundError, isRunningOrStarting, } from './executor.js';
|
|
9
|
+
import { ACCESS_PROVIDER_RECORD_KEY, GATE_CLAIM_NAME } from './gates.js';
|
|
9
10
|
import { healthCheckSettings } from './health-check-settings.js';
|
|
10
11
|
import { coolifyDomains, coolifyTunnelDomains, filesShareHostname } from './hostnames.js';
|
|
11
|
-
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, 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_APPLIED_OPERATOR_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_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_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_SMTP_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_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, 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_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_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, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, archonBootstrapOperator, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailMxHost, } from './platform-diff.js';
|
|
12
|
+
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_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_APPLIED_OPERATOR_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, 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_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_SMTP_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_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, 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_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_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, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, archonBootstrapOperator, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailMxHost, uptimeAccessTokenConflict, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
12
13
|
import { applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
14
|
+
import { heldUptimeValues, provisionPlatformUptime, } from './uptime-worker.js';
|
|
13
15
|
import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, } from './zitadel-system-api.js';
|
|
14
16
|
/** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
|
|
15
17
|
function apexZoneKey(apex) {
|
|
@@ -155,6 +157,8 @@ export class FoundryPlatformStepExecutor {
|
|
|
155
157
|
backup;
|
|
156
158
|
hc;
|
|
157
159
|
heartbeats;
|
|
160
|
+
uptime;
|
|
161
|
+
accessProvider;
|
|
158
162
|
constructor(cloudflare, declaredOrgStore, options = {}) {
|
|
159
163
|
this.cloudflare = cloudflare;
|
|
160
164
|
this.declaredOrgStore = declaredOrgStore;
|
|
@@ -172,6 +176,8 @@ export class FoundryPlatformStepExecutor {
|
|
|
172
176
|
this.backup = options.backup;
|
|
173
177
|
this.hc = options.hc;
|
|
174
178
|
this.heartbeats = options.heartbeats;
|
|
179
|
+
this.uptime = options.uptime;
|
|
180
|
+
this.accessProvider = options.accessProvider;
|
|
175
181
|
}
|
|
176
182
|
async blockedSteps(platform, recordedIds = {}) {
|
|
177
183
|
const blocked = {};
|
|
@@ -323,6 +329,27 @@ export class FoundryPlatformStepExecutor {
|
|
|
323
329
|
if (!apiKey)
|
|
324
330
|
blocked.heartbeatsApiKeyMissing = true;
|
|
325
331
|
}
|
|
332
|
+
await this.uptimeBlocked(platform, recordedIds, blocked);
|
|
333
|
+
// #604: only once edge and sign-in are applied (the planner reports
|
|
334
|
+
// that case itself) and no provider is recorded yet.
|
|
335
|
+
if (platform.edge.enabled &&
|
|
336
|
+
platform.auth.enabled &&
|
|
337
|
+
!recordedIds[ACCESS_PROVIDER_RECORD_KEY] &&
|
|
338
|
+
recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY]) {
|
|
339
|
+
if (!this.accessProvider?.zitadel.createOidcAppWithSecret) {
|
|
340
|
+
blocked.accessProvider =
|
|
341
|
+
'the Access sign-in provider is blocked, not planned: the host built the platform executor without its Access provider dependencies (a ZITADEL client that can make a client with a secret)';
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
try {
|
|
345
|
+
await (await platformAccess(this.accessProvider, recordedIds, 'platform:access')).readiness();
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
blocked.accessProvider =
|
|
349
|
+
'the Access sign-in provider is blocked, not planned: edge could not confirm that Cloudflare Access is ready (its Cloudflare token needs every Access right)';
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
326
353
|
// Not a block: tells the planner to order Archon's step last (`PlanPlatformOptions.blocked.archonRunsInsideApp`).
|
|
327
354
|
if (this.archon?.runsInsideApp) {
|
|
328
355
|
blocked.archonRunsInsideApp = true;
|
|
@@ -337,6 +364,69 @@ export class FoundryPlatformStepExecutor {
|
|
|
337
364
|
}
|
|
338
365
|
return blocked;
|
|
339
366
|
}
|
|
367
|
+
/**
|
|
368
|
+
* foundry#542: why the uptime Worker cannot run yet, which optional secrets
|
|
369
|
+
* are held, and whether the minted values are held. A Worker token or Access
|
|
370
|
+
* pair that is gone is not a block here: the step repairs it. The planner
|
|
371
|
+
* checks the pin and Archon first, so none of the reads here happens before
|
|
372
|
+
* they hold.
|
|
373
|
+
*/
|
|
374
|
+
async uptimeBlocked(platform, recordedIds, blocked) {
|
|
375
|
+
const deployed = Boolean(recordedIds[`cloudflare:worker:${PLATFORM_UPTIME_KEY}`]) &&
|
|
376
|
+
!recordedIds[PLATFORM_UPTIME_REMOVED_SCOPE_KEY];
|
|
377
|
+
if (!platform.uptime.enabled && !uptimeRecorded(recordedIds))
|
|
378
|
+
return;
|
|
379
|
+
const deps = this.uptime;
|
|
380
|
+
if (!deps) {
|
|
381
|
+
blocked.uptime =
|
|
382
|
+
'the uptime Worker is blocked, not planned: the host built the platform executor without its uptime dependencies';
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (!platform.uptime.enabled)
|
|
386
|
+
return;
|
|
387
|
+
blocked.uptimeSecretsHeld = Object.keys(await heldUptimeValues(deps));
|
|
388
|
+
const accessTokenId = recordedIds[PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY];
|
|
389
|
+
const accessHeld = Boolean((await deps.heldKeys.resolve(PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF)) &&
|
|
390
|
+
(await deps.heldKeys.resolve(PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF)));
|
|
391
|
+
blocked.uptimeHeld = {
|
|
392
|
+
token: Boolean(await deps.heldKeys.resolve(PLATFORM_UPTIME_TOKEN_REF)),
|
|
393
|
+
access: accessHeld,
|
|
394
|
+
vars: uptimeVarsDigest(deps.vars),
|
|
395
|
+
};
|
|
396
|
+
const edgeUrl = recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY];
|
|
397
|
+
// A token of the name with no id recorded cannot be minted over, and
|
|
398
|
+
// foundry does not remove what it did not make. With an id recorded the
|
|
399
|
+
// step removes that token through edge and mints again.
|
|
400
|
+
if (!accessTokenId &&
|
|
401
|
+
!recordedIds[PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY] &&
|
|
402
|
+
edgeUrl &&
|
|
403
|
+
platform.uptime.tag) {
|
|
404
|
+
try {
|
|
405
|
+
const access = await deps.access(edgeUrl);
|
|
406
|
+
if (await access.findServiceTokenByName(PLATFORM_UPTIME_ACCESS_TOKEN_NAME)) {
|
|
407
|
+
blocked.uptime = uptimeAccessTokenConflict();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
blocked.uptime =
|
|
413
|
+
'the uptime Worker is blocked, not planned: edge could not be asked whether the Access service token exists';
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (!deployed && edgeUrl && platform.uptime.tag) {
|
|
418
|
+
try {
|
|
419
|
+
const workers = await deps.workers(edgeUrl);
|
|
420
|
+
if (await workers.scriptExists(PLATFORM_UPTIME_SCRIPT_NAME)) {
|
|
421
|
+
blocked.uptime = `the uptime Worker is blocked, not planned: a Worker named ${PLATFORM_UPTIME_SCRIPT_NAME} already exists at Cloudflare and foundry did not make it; remove it by hand`;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
blocked.uptime =
|
|
426
|
+
'the uptime Worker is blocked, not planned: edge could not be asked whether the Worker exists';
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
340
430
|
/**
|
|
341
431
|
* Whether Archon's live Access policy lets through anyone but the declared
|
|
342
432
|
* emails, or (#211 round 4) the broker it last registered with no longer
|
|
@@ -349,7 +439,9 @@ export class FoundryPlatformStepExecutor {
|
|
|
349
439
|
if (platform.archon.enabled &&
|
|
350
440
|
this.archon &&
|
|
351
441
|
recordedIds[`coolify:app:${PLATFORM_ARCHON_KEY}`]) {
|
|
352
|
-
const accessDrifted = await archonAccessDrifted(this.archon, recordedIds, platform.archon.hostname, this.archon.accessEmails
|
|
442
|
+
const accessDrifted = await archonAccessDrifted(this.archon, recordedIds, platform.archon.hostname, this.archon.accessEmails, platform.uptime.enabled
|
|
443
|
+
? recordedIds[PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY]
|
|
444
|
+
: undefined);
|
|
353
445
|
const brokerDrifted = !accessDrifted && (await archonBrokerRegistrationDrifted(this.archon.coolify, recordedIds));
|
|
354
446
|
const bootstrapOperatorDrifted = !accessDrifted &&
|
|
355
447
|
!brokerDrifted &&
|
|
@@ -490,12 +582,26 @@ export class FoundryPlatformStepExecutor {
|
|
|
490
582
|
}
|
|
491
583
|
return provisionPlatformHc(context, this.hc);
|
|
492
584
|
}
|
|
585
|
+
if (action.system === 'cloudflare' && action.kind === 'access-provider') {
|
|
586
|
+
if (!this.accessProvider) {
|
|
587
|
+
throw new Error('cloudflare:access-provider needs FoundryPlatformStepExecutor to be constructed with its Access provider dependencies (#604)');
|
|
588
|
+
}
|
|
589
|
+
return provisionPlatformAccessProvider(context, this.accessProvider);
|
|
590
|
+
}
|
|
493
591
|
if (action.system === 'heartbeats' && action.kind === 'reconcile') {
|
|
494
592
|
if (!this.heartbeats) {
|
|
495
593
|
throw new Error('heartbeats:reconcile needs FoundryPlatformStepExecutor to be constructed with its heartbeats dependencies (#540)');
|
|
496
594
|
}
|
|
497
595
|
return provisionPlatformHeartbeats(context, this.heartbeats);
|
|
498
596
|
}
|
|
597
|
+
if (action.system === 'cloudflare' &&
|
|
598
|
+
action.kind === 'worker' &&
|
|
599
|
+
action.key === PLATFORM_UPTIME_KEY) {
|
|
600
|
+
if (!this.uptime) {
|
|
601
|
+
throw new Error('platform:uptime needs FoundryPlatformStepExecutor to be constructed with its uptime dependencies (foundry#542)');
|
|
602
|
+
}
|
|
603
|
+
return provisionPlatformUptime(context, this.uptime);
|
|
604
|
+
}
|
|
499
605
|
if (action.system === 'cloudflare' && action.kind === 'zone') {
|
|
500
606
|
return this.cloudflareZone(action, context);
|
|
501
607
|
}
|
|
@@ -2915,6 +3021,15 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
2915
3021
|
envs.push({ key: 'COOLIFY_URL', value: coolifyUrl }, { key: 'COOLIFY_READ_TOKEN', value: coolifyReadToken });
|
|
2916
3022
|
}
|
|
2917
3023
|
}
|
|
3024
|
+
// foundry#542: the token the uptime Worker sends, once the Worker step has
|
|
3025
|
+
// minted and delivered it. Absent until then; that step's guard makes
|
|
3026
|
+
// `planArchon` plan this one again.
|
|
3027
|
+
const uptimeTokenId = platform.uptime.enabled
|
|
3028
|
+
? recorded[PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY]
|
|
3029
|
+
: undefined;
|
|
3030
|
+
const uptimeToken = uptimeTokenId ? await deps.heldKeys.resolve(PLATFORM_UPTIME_TOKEN_REF) : null;
|
|
3031
|
+
if (uptimeToken)
|
|
3032
|
+
envs.push({ key: 'UPTIME_TOKEN', value: uptimeToken });
|
|
2918
3033
|
const changed = await envsDiffer(deps.coolify, appId, envs);
|
|
2919
3034
|
// #467: this run's intended env set, hashed before anything below writes
|
|
2920
3035
|
// it -- compared against {@link PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY}
|
|
@@ -2943,7 +3058,9 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
2943
3058
|
// what either hash names (#467).
|
|
2944
3059
|
let staleKeyDeleted = false;
|
|
2945
3060
|
for (const stale of await deps.coolify.listEnvs(appId)) {
|
|
2946
|
-
|
|
3061
|
+
// foundry#542: a disabled Worker's token leaves Archon's env too.
|
|
3062
|
+
const staleUptime = !platform.uptime.enabled && stale.key === 'UPTIME_TOKEN';
|
|
3063
|
+
if ((stale.key === 'FILES_MANAGEMENT_KEY' || staleUptime) && stale.uuid) {
|
|
2947
3064
|
await deps.coolify.deleteEnv(appId, stale.uuid);
|
|
2948
3065
|
staleKeyDeleted = true;
|
|
2949
3066
|
}
|
|
@@ -3035,7 +3152,10 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
3035
3152
|
const tunnels = await platformTunnels(deps, context.recordedIds(), 'platform:archon');
|
|
3036
3153
|
const access = await platformAccess(deps, context.recordedIds(), 'platform:archon');
|
|
3037
3154
|
let gateClosed = Boolean(recorded[PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY]);
|
|
3038
|
-
const
|
|
3155
|
+
const uptimeAccessTokenId = platform.uptime.enabled
|
|
3156
|
+
? recorded[PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY]
|
|
3157
|
+
: undefined;
|
|
3158
|
+
const accessAppId = await ensureArchonAccess(access, hostname, deps.accessEmails, uptimeAccessTokenId, async () => {
|
|
3039
3159
|
// The Access app is about to be rebuilt: answer 404 for every hostname
|
|
3040
3160
|
// meanwhile, so it is never served without the gate. The marker is
|
|
3041
3161
|
// persisted first: a failure before the real ingress is written back
|
|
@@ -3177,6 +3297,18 @@ async function provisionPlatformArchon(context, deps) {
|
|
|
3177
3297
|
if (keyUnsynced && integrationsKeyId && (!selfRedeploy || deployState === 'finished')) {
|
|
3178
3298
|
await record(PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, integrationsKeyId);
|
|
3179
3299
|
}
|
|
3300
|
+
// foundry#542: the same confirmation for the uptime token, and for its removal.
|
|
3301
|
+
if (!platform.uptime.enabled &&
|
|
3302
|
+
recorded[PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY] &&
|
|
3303
|
+
(!selfRedeploy || deployState === 'finished')) {
|
|
3304
|
+
await record(PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, '');
|
|
3305
|
+
}
|
|
3306
|
+
if (uptimeToken &&
|
|
3307
|
+
uptimeTokenId &&
|
|
3308
|
+
recorded[PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY] !== uptimeTokenId &&
|
|
3309
|
+
(!selfRedeploy || deployState === 'finished')) {
|
|
3310
|
+
await record(PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, uptimeTokenId);
|
|
3311
|
+
}
|
|
3180
3312
|
// #475: confirmed only now, after the health check above has passed on a
|
|
3181
3313
|
// container that was deployed with the alias patch in place -- the same
|
|
3182
3314
|
// confirm-after-health-check timing as the two records just above.
|
|
@@ -3496,14 +3628,18 @@ async function putArchonMasterKeys(heldKeys, masterKeys) {
|
|
|
3496
3628
|
await heldKeys.put?.(ref, value);
|
|
3497
3629
|
}
|
|
3498
3630
|
}
|
|
3631
|
+
/** The policy that lets the uptime Worker's Access service token through Archon's app, beside {@link ACCESS_POLICY_NAME}. */
|
|
3632
|
+
const PLATFORM_ARCHON_UPTIME_POLICY_NAME = 'foundry: uptime service token';
|
|
3499
3633
|
/**
|
|
3500
3634
|
* `executor.ts`'s `cloudflareAccess`, for the platform's Archon: reuses only
|
|
3501
3635
|
* an Access app named {@link PLATFORM_ARCHON_ACCESS_APP_NAME}, refuses to
|
|
3502
3636
|
* adopt any other Access app already holding the hostname, and refuses when
|
|
3503
3637
|
* its own app carries a policy foundry did not create, since that policy
|
|
3504
|
-
* could let others in.
|
|
3638
|
+
* could let others in. foundry#542: with `uptimeTokenId` the app also carries
|
|
3639
|
+
* a `non_identity` policy for the uptime Worker's service token; a missing one
|
|
3640
|
+
* is added in place, one for another token rebuilds the app.
|
|
3505
3641
|
*/
|
|
3506
|
-
async function ensureArchonAccess(cf, domain, emails, closeGate) {
|
|
3642
|
+
async function ensureArchonAccess(cf, domain, emails, uptimeTokenId, closeGate) {
|
|
3507
3643
|
const onDomain = (await cf.listAccessApps()).filter((a) => a.domain === domain);
|
|
3508
3644
|
const foreign = onDomain.find((a) => a.name !== PLATFORM_ARCHON_ACCESS_APP_NAME);
|
|
3509
3645
|
if (foreign) {
|
|
@@ -3516,22 +3652,31 @@ async function ensureArchonAccess(cf, domain, emails, closeGate) {
|
|
|
3516
3652
|
type: 'self_hosted',
|
|
3517
3653
|
});
|
|
3518
3654
|
await cf.createAccessPolicy(created.id, archonAccessPolicy(emails));
|
|
3655
|
+
if (uptimeTokenId) {
|
|
3656
|
+
await cf.createAccessPolicy(created.id, archonUptimePolicy(uptimeTokenId));
|
|
3657
|
+
}
|
|
3519
3658
|
return created;
|
|
3520
3659
|
};
|
|
3521
3660
|
const existing = onDomain[0];
|
|
3522
3661
|
if (!existing)
|
|
3523
3662
|
return (await create()).id;
|
|
3524
3663
|
const policies = await cf.listAccessPolicies(existing.id);
|
|
3525
|
-
const other = policies.find((p) => p.name !== ACCESS_POLICY_NAME);
|
|
3664
|
+
const other = policies.find((p) => p.name !== ACCESS_POLICY_NAME && p.name !== PLATFORM_ARCHON_UPTIME_POLICY_NAME);
|
|
3526
3665
|
if (other) {
|
|
3527
3666
|
throw new Error(`platform:archon: Access app ${existing.id} has policy '${other.name}' that foundry did not create`);
|
|
3528
3667
|
}
|
|
3529
|
-
|
|
3530
|
-
|
|
3668
|
+
const emailPolicies = policies.filter((p) => p.name === ACCESS_POLICY_NAME);
|
|
3669
|
+
const tokenPolicies = policies.filter((p) => p.name === PLATFORM_ARCHON_UPTIME_POLICY_NAME);
|
|
3670
|
+
const staleToken = tokenPolicies.some((p) => !uptimePolicyIntact(p, uptimeTokenId));
|
|
3671
|
+
if (emailPolicies.every((p) => sameEmails(p.include, emails)) && !staleToken) {
|
|
3672
|
+
if (emailPolicies.length === 0) {
|
|
3673
|
+
await cf.createAccessPolicy(existing.id, archonAccessPolicy(emails));
|
|
3674
|
+
}
|
|
3675
|
+
if (uptimeTokenId && tokenPolicies.length === 0) {
|
|
3676
|
+
await cf.createAccessPolicy(existing.id, archonUptimePolicy(uptimeTokenId));
|
|
3677
|
+
}
|
|
3531
3678
|
return existing.id;
|
|
3532
3679
|
}
|
|
3533
|
-
if (policies.every((p) => sameEmails(p.include, emails)))
|
|
3534
|
-
return existing.id;
|
|
3535
3680
|
// No call updates an app's policy: rebuild the app, hostname closed meanwhile.
|
|
3536
3681
|
await closeGate();
|
|
3537
3682
|
await cf.deleteAccessApp(existing.id);
|
|
@@ -3544,6 +3689,30 @@ function archonAccessPolicy(emails) {
|
|
|
3544
3689
|
include: emails.map((email) => ({ email: { email } })),
|
|
3545
3690
|
};
|
|
3546
3691
|
}
|
|
3692
|
+
function archonUptimePolicy(tokenId) {
|
|
3693
|
+
return {
|
|
3694
|
+
name: PLATFORM_ARCHON_UPTIME_POLICY_NAME,
|
|
3695
|
+
decision: 'non_identity',
|
|
3696
|
+
include: [{ service_token: { token_id: tokenId } }],
|
|
3697
|
+
};
|
|
3698
|
+
}
|
|
3699
|
+
/**
|
|
3700
|
+
* Whether the uptime policy is what foundry makes: decision `non_identity` and
|
|
3701
|
+
* exactly one include, a service token -- the recorded `tokenId` when there is
|
|
3702
|
+
* one, any token when none is recorded (the Worker is off or not applied yet).
|
|
3703
|
+
*/
|
|
3704
|
+
function uptimePolicyIntact(policy, tokenId) {
|
|
3705
|
+
if (policy.decision !== 'non_identity')
|
|
3706
|
+
return false;
|
|
3707
|
+
const include = policy.include;
|
|
3708
|
+
if (!Array.isArray(include) || include.length !== 1)
|
|
3709
|
+
return false;
|
|
3710
|
+
const entry = include[0];
|
|
3711
|
+
if (Object.keys(entry ?? {}).length !== 1)
|
|
3712
|
+
return false;
|
|
3713
|
+
const live = entry?.service_token?.token_id;
|
|
3714
|
+
return typeof live === 'string' && live !== '' && (tokenId === undefined || live === tokenId);
|
|
3715
|
+
}
|
|
3547
3716
|
/** Whether an Access policy's include list is exactly `emails` (case-insensitive) and nothing else. */
|
|
3548
3717
|
function sameEmails(include, emails) {
|
|
3549
3718
|
if (!Array.isArray(include))
|
|
@@ -3563,14 +3732,23 @@ function sameEmails(include, emails) {
|
|
|
3563
3732
|
* exactly the declared emails. A failed read is not drift: the next plan reads
|
|
3564
3733
|
* again.
|
|
3565
3734
|
*/
|
|
3566
|
-
async function archonAccessDrifted(deps, recorded, domain, emails) {
|
|
3735
|
+
async function archonAccessDrifted(deps, recorded, domain, emails, uptimeTokenId) {
|
|
3567
3736
|
try {
|
|
3568
3737
|
const cf = await platformAccess(deps, recorded, 'platform:archon');
|
|
3569
3738
|
const app = (await cf.listAccessApps()).find((a) => a.domain === domain && a.name === PLATFORM_ARCHON_ACCESS_APP_NAME);
|
|
3570
3739
|
if (!app)
|
|
3571
3740
|
return true;
|
|
3572
3741
|
const policies = await cf.listAccessPolicies(app.id);
|
|
3573
|
-
|
|
3742
|
+
const emailPolicies = policies.filter((p) => p.name === ACCESS_POLICY_NAME);
|
|
3743
|
+
const tokenPolicies = policies.filter((p) => p.name === PLATFORM_ARCHON_UPTIME_POLICY_NAME);
|
|
3744
|
+
if (policies.length !== emailPolicies.length + tokenPolicies.length)
|
|
3745
|
+
return true;
|
|
3746
|
+
if (emailPolicies.length === 0 || !emailPolicies.every((p) => sameEmails(p.include, emails))) {
|
|
3747
|
+
return true;
|
|
3748
|
+
}
|
|
3749
|
+
if (!tokenPolicies.every((p) => uptimePolicyIntact(p, uptimeTokenId)))
|
|
3750
|
+
return true;
|
|
3751
|
+
return uptimeTokenId !== undefined && tokenPolicies.length === 0;
|
|
3574
3752
|
}
|
|
3575
3753
|
catch {
|
|
3576
3754
|
return false;
|
|
@@ -5668,6 +5846,85 @@ async function provisionPlatformEdge(context, deps) {
|
|
|
5668
5846
|
await record(PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, gitRef);
|
|
5669
5847
|
return { resourceId: appId };
|
|
5670
5848
|
}
|
|
5849
|
+
/** The ZITADEL project and client the platform's Access provider signs in through (#604). */
|
|
5850
|
+
const PLATFORM_ACCESS_ZITADEL_PROJECT = 'cloudflare-access';
|
|
5851
|
+
const PLATFORM_ACCESS_ZITADEL_APP = 'cloudflare-access';
|
|
5852
|
+
/** The name edge finds the Cloudflare Access provider by. */
|
|
5853
|
+
const PLATFORM_ACCESS_PROVIDER_NAME = 'foundry';
|
|
5854
|
+
/**
|
|
5855
|
+
* #604: makes the platform's ZITADEL a sign-in provider in Cloudflare Access.
|
|
5856
|
+
* Reads the Access team address from edge, makes a ZITADEL client whose
|
|
5857
|
+
* redirect is that address's callback, and hands its id and secret to edge's
|
|
5858
|
+
* `ensureProvider`, which is the delivery. The client's secret exists once
|
|
5859
|
+
* (CLAUDE.md rules 1 and 6): its app id is recorded and persisted before the
|
|
5860
|
+
* secret leaves, the guard {@link ACCESS_PROVIDER_RECORD_KEY} only after edge
|
|
5861
|
+
* returned the provider id, and a recorded undelivered id stops a second
|
|
5862
|
+
* mint. The secret reaches only `ensureProvider`: no error, log or record
|
|
5863
|
+
* holds it. A client app by this name with no recorded id is one whose create
|
|
5864
|
+
* response was lost before anything saw its secret, so it is deleted and made
|
|
5865
|
+
* again.
|
|
5866
|
+
*/
|
|
5867
|
+
async function provisionPlatformAccessProvider(context, deps) {
|
|
5868
|
+
const { platform } = context;
|
|
5869
|
+
const recorded = context.recordedIds();
|
|
5870
|
+
const sleep = deps.runtime?.sleep ?? defaultSleep;
|
|
5871
|
+
const attempts = deps.runtime?.deliverAttempts ?? 3;
|
|
5872
|
+
const label = 'platform:access-provider';
|
|
5873
|
+
if (recorded[ACCESS_PROVIDER_RECORD_KEY])
|
|
5874
|
+
return {};
|
|
5875
|
+
const lost = recorded[PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY];
|
|
5876
|
+
if (lost) {
|
|
5877
|
+
throw new Error(`${label}: the ZITADEL client ${lost} was made and its secret may or may not have reached edge; delete it and remove the recorded keys ${PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY} and ${ACCESS_PROVIDER_RECORD_KEY}, then apply again to make both anew`);
|
|
5878
|
+
}
|
|
5879
|
+
const access = await platformAccess(deps, recorded, label);
|
|
5880
|
+
let authDomain;
|
|
5881
|
+
try {
|
|
5882
|
+
({ authDomain } = await access.readiness());
|
|
5883
|
+
}
|
|
5884
|
+
catch {
|
|
5885
|
+
throw new Error(`${label}: edge could not confirm that Cloudflare Access is ready`);
|
|
5886
|
+
}
|
|
5887
|
+
const issuer = `https://${platform.auth.hostname}`;
|
|
5888
|
+
const zitadel = deps.zitadelForInstance?.(issuer) ?? deps.zitadel;
|
|
5889
|
+
const { org } = await zitadel.findOrCreateOrg(PLATFORM_AUTH_ORG_NAME);
|
|
5890
|
+
const { project } = await zitadel.findOrCreateProject(org.id, PLATFORM_ACCESS_ZITADEL_PROJECT);
|
|
5891
|
+
const orphan = (await zitadel.listApps(org.id, project.id)).apps.find((a) => a.name === PLATFORM_ACCESS_ZITADEL_APP && a.state !== 'APP_STATE_REMOVED');
|
|
5892
|
+
if (orphan) {
|
|
5893
|
+
try {
|
|
5894
|
+
await zitadel.deleteApp(org.id, project.id, orphan.id);
|
|
5895
|
+
}
|
|
5896
|
+
catch (error) {
|
|
5897
|
+
if (!isNotFoundError(error))
|
|
5898
|
+
throw error;
|
|
5899
|
+
}
|
|
5900
|
+
}
|
|
5901
|
+
const client = await zitadel.createOidcAppWithSecret(org.id, project.id, {
|
|
5902
|
+
name: PLATFORM_ACCESS_ZITADEL_APP,
|
|
5903
|
+
redirectUris: [`https://${authDomain}/cdn-cgi/access/callback`],
|
|
5904
|
+
postLogoutRedirectUris: [`https://${authDomain}/`],
|
|
5905
|
+
});
|
|
5906
|
+
context.recordScopeId(PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, client.appId);
|
|
5907
|
+
await context.persist?.();
|
|
5908
|
+
let providerId = '';
|
|
5909
|
+
await deliverWithRetry(async (_ref, secret) => {
|
|
5910
|
+
providerId = (await access.ensureProvider({
|
|
5911
|
+
name: PLATFORM_ACCESS_PROVIDER_NAME,
|
|
5912
|
+
clientId: client.clientId,
|
|
5913
|
+
clientSecret: secret,
|
|
5914
|
+
authUrl: `${issuer}/oauth/v2/authorize`,
|
|
5915
|
+
tokenUrl: `${issuer}/oauth/v2/token`,
|
|
5916
|
+
certsUrl: `${issuer}/oauth/v2/keys`,
|
|
5917
|
+
scopes: ['openid', 'email', 'profile', 'urn:zitadel:iam:user:resourceowner'],
|
|
5918
|
+
claims: [GATE_CLAIM_NAME],
|
|
5919
|
+
})).id;
|
|
5920
|
+
}, ACCESS_PROVIDER_RECORD_KEY, client.clientSecret, attempts, sleep, `the ZITADEL client ${client.appId}'s secret`, label);
|
|
5921
|
+
// Last: the guard says the secret reached edge.
|
|
5922
|
+
context.recordScopeId(PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, platform.auth.hostname);
|
|
5923
|
+
context.recordScopeId(PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, authDomain);
|
|
5924
|
+
context.recordScopeId(ACCESS_PROVIDER_RECORD_KEY, providerId);
|
|
5925
|
+
await context.persist?.();
|
|
5926
|
+
return {};
|
|
5927
|
+
}
|
|
5671
5928
|
/** `healthchecks/healthchecks` on Docker Hub -- #540's own "third-party, not this estate's own repo" image. */
|
|
5672
5929
|
const HC_IMAGE = 'healthchecks/healthchecks';
|
|
5673
5930
|
const PLATFORM_HC_PROJECT = 'platform';
|
|
@@ -6022,7 +6279,7 @@ label = 'platform:files') {
|
|
|
6022
6279
|
}
|
|
6023
6280
|
catch {
|
|
6024
6281
|
if (attempt >= attempts) {
|
|
6025
|
-
throw new Error(`${label}: ${what} could not be delivered after ${attempts} tries; the next plan reports it as
|
|
6282
|
+
throw new Error(`${label}: ${what} could not be delivered after ${attempts} tries; the next plan reports it as blocked and mints no new one`);
|
|
6026
6283
|
}
|
|
6027
6284
|
await sleep(1000 * attempt);
|
|
6028
6285
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AccessPort } from './edge-access.js';
|
|
2
|
+
import type { PlatformWorkersPort } from './edge-workers.js';
|
|
3
|
+
import type { PlatformStepExecutionContext } from './plan-apply.js';
|
|
4
|
+
/** The optional plain text variables the Worker reads, supplied by the host (not secrets). */
|
|
5
|
+
export declare const UPTIME_OPTIONAL_VARS: readonly ["CHAT_HOMESERVER_URL", "CHAT_ROOM_ID", "ALERT_EMAIL_FROM", "ALERT_EMAIL_TO"];
|
|
6
|
+
/** Builds the Workers port at edge's recorded address, the {@link import('./platform-providers.js').PlatformDnsFactory} way. */
|
|
7
|
+
export type PlatformWorkersFactory = (edgeUrl: string) => PlatformWorkersPort | Promise<PlatformWorkersPort>;
|
|
8
|
+
/** The Access calls the uptime step makes: it mints, finds and removes its one service token. */
|
|
9
|
+
export type PlatformUptimeAccessPort = Pick<AccessPort, 'createServiceToken' | 'removeServiceToken' | 'findServiceTokenByName'>;
|
|
10
|
+
/** Builds the Access port at edge's recorded address, the {@link PlatformWorkersFactory} way. */
|
|
11
|
+
export type PlatformUptimeAccessFactory = (edgeUrl: string) => PlatformUptimeAccessPort | Promise<PlatformUptimeAccessPort>;
|
|
12
|
+
/**
|
|
13
|
+
* Dependencies of the uptime Worker step. `deliverSecret` and `heldKeys` are
|
|
14
|
+
* the host's: the minted token goes only to `deliverSecret`, and every secret
|
|
15
|
+
* is read back only to be handed to edge, which never returns one.
|
|
16
|
+
*/
|
|
17
|
+
export interface PlatformUptimeDeps {
|
|
18
|
+
workers: PlatformWorkersFactory;
|
|
19
|
+
/** The Access service token the Worker passes Archon's gate with, minted through edge. */
|
|
20
|
+
access: PlatformUptimeAccessFactory;
|
|
21
|
+
/**
|
|
22
|
+
* Holds `secret` under `ref`. An empty `secret` clears it, which is how the
|
|
23
|
+
* step forgets the Access pair when the Worker is removed.
|
|
24
|
+
*/
|
|
25
|
+
deliverSecret: (ref: string, secret: string) => Promise<void>;
|
|
26
|
+
heldKeys: {
|
|
27
|
+
resolve(ref: string): Promise<string | null>;
|
|
28
|
+
};
|
|
29
|
+
/** Host config for the Worker's optional plain text variables; an absent one is left unset. */
|
|
30
|
+
vars?: Partial<Record<(typeof UPTIME_OPTIONAL_VARS)[number], string>>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* What is held now: the optional secrets, and `UPTIME_HEARTBEAT_URL` (a ping
|
|
34
|
+
* URL that works as a capability, so a secret too), name to value. Callers
|
|
35
|
+
* that report or record take the names only.
|
|
36
|
+
*/
|
|
37
|
+
export declare function heldUptimeValues(deps: Pick<PlatformUptimeDeps, 'heldKeys'>): Promise<Record<string, string>>;
|
|
38
|
+
/**
|
|
39
|
+
* foundry#542: makes the uptime Worker true, in an order where every failure
|
|
40
|
+
* is repaired by the next apply and none needs a hand edit. Upload the pinned
|
|
41
|
+
* Archon release (the host fetches and verifies it) and record the script at
|
|
42
|
+
* once, so a later failure is a half-made Worker foundry owns. Then hold the
|
|
43
|
+
* Worker token and the Access pair (minted once each, each guard recorded as
|
|
44
|
+
* soon as its value is held), set every held secret on the Worker, and last
|
|
45
|
+
* the cron trigger, so the Worker never runs a round without its token. The
|
|
46
|
+
* applied values are recorded only when all of it succeeded.
|
|
47
|
+
*/
|
|
48
|
+
export declare function provisionPlatformUptime(context: PlatformStepExecutionContext, deps: PlatformUptimeDeps): Promise<{
|
|
49
|
+
resourceId?: string;
|
|
50
|
+
}>;
|