@wtfalch/foundry 0.23.0 → 0.24.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 +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/migrations/0048_hosting_gate_limit.sql +7 -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 +34 -1
- package/dist/plan/edge-access.js +30 -1
- package/dist/plan/executor.d.ts +36 -16
- 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/plan-apply.d.ts +2 -0
- package/dist/plan/plan-apply.js +8 -1
- package/dist/plan/platform-diff.d.ts +14 -0
- package/dist/plan/platform-diff.js +78 -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 +24 -0
- package/dist/plan/platform-providers.js +110 -2
- package/dist/store/postgres.js +27 -5
- package/dist/store/schema.d.ts +17 -0
- package/dist/store/schema.js +2 -0
- package/package.json +2 -2
|
@@ -197,6 +197,12 @@ export interface FoundryPlatformStepExecutorOptions {
|
|
|
197
197
|
* heartbeat checks reported as blocked rather than planned.
|
|
198
198
|
*/
|
|
199
199
|
heartbeats?: PlatformHeartbeatsDeps;
|
|
200
|
+
/**
|
|
201
|
+
* #604: dependencies for the `cloudflare:access-provider` step -- optional
|
|
202
|
+
* for the same reason as `auth`; edge and sign-in applied with this unset
|
|
203
|
+
* gets the step reported as blocked.
|
|
204
|
+
*/
|
|
205
|
+
accessProvider?: PlatformAccessProviderDeps;
|
|
200
206
|
}
|
|
201
207
|
/**
|
|
202
208
|
* The real {@link PlatformStepExecutor} for the two steps built so far:
|
|
@@ -279,6 +285,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
279
285
|
private readonly backup?;
|
|
280
286
|
private readonly hc?;
|
|
281
287
|
private readonly heartbeats?;
|
|
288
|
+
private readonly accessProvider?;
|
|
282
289
|
constructor(cloudflare: Pick<CloudflarePort, 'findZoneByApex' | 'createZone'>, declaredOrgStore: Pick<DeclaredOrgStore, 'findOrgIdForZone' | 'list' | 'get'>, options?: FoundryPlatformStepExecutorOptions);
|
|
283
290
|
blockedSteps(platform: DeclaredPlatform, recordedIds?: Readonly<Record<string, string>>): Promise<{
|
|
284
291
|
auth?: string;
|
|
@@ -290,6 +297,7 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
|
|
|
290
297
|
edge?: string;
|
|
291
298
|
archon?: string;
|
|
292
299
|
mail?: string;
|
|
300
|
+
accessProvider?: string;
|
|
293
301
|
backupExecutorMissing?: boolean;
|
|
294
302
|
hc?: string;
|
|
295
303
|
heartbeatsExecutorMissing?: boolean;
|
|
@@ -959,6 +967,22 @@ export interface PlatformEdgeDeps {
|
|
|
959
967
|
/** Clock, sleep and fetch for the readiness wait and delivery retries; real ones by default. */
|
|
960
968
|
runtime?: PlatformEdgeRuntime;
|
|
961
969
|
}
|
|
970
|
+
/** The Access calls the provider step makes. */
|
|
971
|
+
export type PlatformAccessProviderPort = Pick<AccessPort, 'readiness' | 'ensureProvider'>;
|
|
972
|
+
/** #604: dependencies for {@link provisionPlatformAccessProvider}. */
|
|
973
|
+
export interface PlatformAccessProviderDeps {
|
|
974
|
+
/** Edge's Access calls, at its recorded address (see {@link PlatformAccessFactory}). */
|
|
975
|
+
access: (edgeUrl: string) => PlatformAccessProviderPort | Promise<PlatformAccessProviderPort>;
|
|
976
|
+
/** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
|
|
977
|
+
zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'listApps' | 'deleteApp'> & Required<Pick<ZitadelPort, 'createOidcAppWithSecret'>>;
|
|
978
|
+
/** {@link PlatformArchonDeps.zitadelForInstance}: the ZITADEL client for the sign-in at `issuerUrl`. */
|
|
979
|
+
zitadelForInstance?: (issuerUrl: string) => PlatformAccessProviderDeps['zitadel'];
|
|
980
|
+
/** Sleep and delivery tries for the one call that carries the secret; real ones by default. */
|
|
981
|
+
runtime?: {
|
|
982
|
+
sleep?: (ms: number) => Promise<void>;
|
|
983
|
+
deliverAttempts?: number;
|
|
984
|
+
};
|
|
985
|
+
}
|
|
962
986
|
export interface PlatformHcRuntime {
|
|
963
987
|
fetch?: typeof fetch;
|
|
964
988
|
sleep?: (ms: number) => Promise<void>;
|
|
@@ -6,9 +6,10 @@ 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_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
13
|
import { applyCallsSfuPlatformStep } from './platform-sfu.js';
|
|
13
14
|
import { CreateInstanceError, createZitadelInstance, generateSystemApiKeypair, instanceWithoutBearerMessage, systemApiUsersEnv, } from './zitadel-system-api.js';
|
|
14
15
|
/** The key `runPlatformSteps` records the apex zone's id under (`system:kind:key`). */
|
|
@@ -155,6 +156,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
155
156
|
backup;
|
|
156
157
|
hc;
|
|
157
158
|
heartbeats;
|
|
159
|
+
accessProvider;
|
|
158
160
|
constructor(cloudflare, declaredOrgStore, options = {}) {
|
|
159
161
|
this.cloudflare = cloudflare;
|
|
160
162
|
this.declaredOrgStore = declaredOrgStore;
|
|
@@ -172,6 +174,7 @@ export class FoundryPlatformStepExecutor {
|
|
|
172
174
|
this.backup = options.backup;
|
|
173
175
|
this.hc = options.hc;
|
|
174
176
|
this.heartbeats = options.heartbeats;
|
|
177
|
+
this.accessProvider = options.accessProvider;
|
|
175
178
|
}
|
|
176
179
|
async blockedSteps(platform, recordedIds = {}) {
|
|
177
180
|
const blocked = {};
|
|
@@ -323,6 +326,26 @@ export class FoundryPlatformStepExecutor {
|
|
|
323
326
|
if (!apiKey)
|
|
324
327
|
blocked.heartbeatsApiKeyMissing = true;
|
|
325
328
|
}
|
|
329
|
+
// #604: only once edge and sign-in are applied (the planner reports
|
|
330
|
+
// that case itself) and no provider is recorded yet.
|
|
331
|
+
if (platform.edge.enabled &&
|
|
332
|
+
platform.auth.enabled &&
|
|
333
|
+
!recordedIds[ACCESS_PROVIDER_RECORD_KEY] &&
|
|
334
|
+
recordedIds[PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY]) {
|
|
335
|
+
if (!this.accessProvider?.zitadel.createOidcAppWithSecret) {
|
|
336
|
+
blocked.accessProvider =
|
|
337
|
+
'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)';
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
try {
|
|
341
|
+
await (await platformAccess(this.accessProvider, recordedIds, 'platform:access')).readiness();
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
blocked.accessProvider =
|
|
345
|
+
'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)';
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
326
349
|
// Not a block: tells the planner to order Archon's step last (`PlanPlatformOptions.blocked.archonRunsInsideApp`).
|
|
327
350
|
if (this.archon?.runsInsideApp) {
|
|
328
351
|
blocked.archonRunsInsideApp = true;
|
|
@@ -490,6 +513,12 @@ export class FoundryPlatformStepExecutor {
|
|
|
490
513
|
}
|
|
491
514
|
return provisionPlatformHc(context, this.hc);
|
|
492
515
|
}
|
|
516
|
+
if (action.system === 'cloudflare' && action.kind === 'access-provider') {
|
|
517
|
+
if (!this.accessProvider) {
|
|
518
|
+
throw new Error('cloudflare:access-provider needs FoundryPlatformStepExecutor to be constructed with its Access provider dependencies (#604)');
|
|
519
|
+
}
|
|
520
|
+
return provisionPlatformAccessProvider(context, this.accessProvider);
|
|
521
|
+
}
|
|
493
522
|
if (action.system === 'heartbeats' && action.kind === 'reconcile') {
|
|
494
523
|
if (!this.heartbeats) {
|
|
495
524
|
throw new Error('heartbeats:reconcile needs FoundryPlatformStepExecutor to be constructed with its heartbeats dependencies (#540)');
|
|
@@ -5668,6 +5697,85 @@ async function provisionPlatformEdge(context, deps) {
|
|
|
5668
5697
|
await record(PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, gitRef);
|
|
5669
5698
|
return { resourceId: appId };
|
|
5670
5699
|
}
|
|
5700
|
+
/** The ZITADEL project and client the platform's Access provider signs in through (#604). */
|
|
5701
|
+
const PLATFORM_ACCESS_ZITADEL_PROJECT = 'cloudflare-access';
|
|
5702
|
+
const PLATFORM_ACCESS_ZITADEL_APP = 'cloudflare-access';
|
|
5703
|
+
/** The name edge finds the Cloudflare Access provider by. */
|
|
5704
|
+
const PLATFORM_ACCESS_PROVIDER_NAME = 'foundry';
|
|
5705
|
+
/**
|
|
5706
|
+
* #604: makes the platform's ZITADEL a sign-in provider in Cloudflare Access.
|
|
5707
|
+
* Reads the Access team address from edge, makes a ZITADEL client whose
|
|
5708
|
+
* redirect is that address's callback, and hands its id and secret to edge's
|
|
5709
|
+
* `ensureProvider`, which is the delivery. The client's secret exists once
|
|
5710
|
+
* (CLAUDE.md rules 1 and 6): its app id is recorded and persisted before the
|
|
5711
|
+
* secret leaves, the guard {@link ACCESS_PROVIDER_RECORD_KEY} only after edge
|
|
5712
|
+
* returned the provider id, and a recorded undelivered id stops a second
|
|
5713
|
+
* mint. The secret reaches only `ensureProvider`: no error, log or record
|
|
5714
|
+
* holds it. A client app by this name with no recorded id is one whose create
|
|
5715
|
+
* response was lost before anything saw its secret, so it is deleted and made
|
|
5716
|
+
* again.
|
|
5717
|
+
*/
|
|
5718
|
+
async function provisionPlatformAccessProvider(context, deps) {
|
|
5719
|
+
const { platform } = context;
|
|
5720
|
+
const recorded = context.recordedIds();
|
|
5721
|
+
const sleep = deps.runtime?.sleep ?? defaultSleep;
|
|
5722
|
+
const attempts = deps.runtime?.deliverAttempts ?? 3;
|
|
5723
|
+
const label = 'platform:access-provider';
|
|
5724
|
+
if (recorded[ACCESS_PROVIDER_RECORD_KEY])
|
|
5725
|
+
return {};
|
|
5726
|
+
const lost = recorded[PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY];
|
|
5727
|
+
if (lost) {
|
|
5728
|
+
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`);
|
|
5729
|
+
}
|
|
5730
|
+
const access = await platformAccess(deps, recorded, label);
|
|
5731
|
+
let authDomain;
|
|
5732
|
+
try {
|
|
5733
|
+
({ authDomain } = await access.readiness());
|
|
5734
|
+
}
|
|
5735
|
+
catch {
|
|
5736
|
+
throw new Error(`${label}: edge could not confirm that Cloudflare Access is ready`);
|
|
5737
|
+
}
|
|
5738
|
+
const issuer = `https://${platform.auth.hostname}`;
|
|
5739
|
+
const zitadel = deps.zitadelForInstance?.(issuer) ?? deps.zitadel;
|
|
5740
|
+
const { org } = await zitadel.findOrCreateOrg(PLATFORM_AUTH_ORG_NAME);
|
|
5741
|
+
const { project } = await zitadel.findOrCreateProject(org.id, PLATFORM_ACCESS_ZITADEL_PROJECT);
|
|
5742
|
+
const orphan = (await zitadel.listApps(org.id, project.id)).apps.find((a) => a.name === PLATFORM_ACCESS_ZITADEL_APP && a.state !== 'APP_STATE_REMOVED');
|
|
5743
|
+
if (orphan) {
|
|
5744
|
+
try {
|
|
5745
|
+
await zitadel.deleteApp(org.id, project.id, orphan.id);
|
|
5746
|
+
}
|
|
5747
|
+
catch (error) {
|
|
5748
|
+
if (!isNotFoundError(error))
|
|
5749
|
+
throw error;
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
const client = await zitadel.createOidcAppWithSecret(org.id, project.id, {
|
|
5753
|
+
name: PLATFORM_ACCESS_ZITADEL_APP,
|
|
5754
|
+
redirectUris: [`https://${authDomain}/cdn-cgi/access/callback`],
|
|
5755
|
+
postLogoutRedirectUris: [`https://${authDomain}/`],
|
|
5756
|
+
});
|
|
5757
|
+
context.recordScopeId(PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, client.appId);
|
|
5758
|
+
await context.persist?.();
|
|
5759
|
+
let providerId = '';
|
|
5760
|
+
await deliverWithRetry(async (_ref, secret) => {
|
|
5761
|
+
providerId = (await access.ensureProvider({
|
|
5762
|
+
name: PLATFORM_ACCESS_PROVIDER_NAME,
|
|
5763
|
+
clientId: client.clientId,
|
|
5764
|
+
clientSecret: secret,
|
|
5765
|
+
authUrl: `${issuer}/oauth/v2/authorize`,
|
|
5766
|
+
tokenUrl: `${issuer}/oauth/v2/token`,
|
|
5767
|
+
certsUrl: `${issuer}/oauth/v2/keys`,
|
|
5768
|
+
scopes: ['openid', 'email', 'profile', 'urn:zitadel:iam:user:resourceowner'],
|
|
5769
|
+
claims: [GATE_CLAIM_NAME],
|
|
5770
|
+
})).id;
|
|
5771
|
+
}, ACCESS_PROVIDER_RECORD_KEY, client.clientSecret, attempts, sleep, `the ZITADEL client ${client.appId}'s secret`, label);
|
|
5772
|
+
// Last: the guard says the secret reached edge.
|
|
5773
|
+
context.recordScopeId(PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, platform.auth.hostname);
|
|
5774
|
+
context.recordScopeId(PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, authDomain);
|
|
5775
|
+
context.recordScopeId(ACCESS_PROVIDER_RECORD_KEY, providerId);
|
|
5776
|
+
await context.persist?.();
|
|
5777
|
+
return {};
|
|
5778
|
+
}
|
|
5671
5779
|
/** `healthchecks/healthchecks` on Docker Hub -- #540's own "third-party, not this estate's own repo" image. */
|
|
5672
5780
|
const HC_IMAGE = 'healthchecks/healthchecks';
|
|
5673
5781
|
const PLATFORM_HC_PROJECT = 'platform';
|
|
@@ -6022,7 +6130,7 @@ label = 'platform:files') {
|
|
|
6022
6130
|
}
|
|
6023
6131
|
catch {
|
|
6024
6132
|
if (attempt >= attempts) {
|
|
6025
|
-
throw new Error(`${label}: ${what} could not be delivered after ${attempts} tries; the next plan reports it as
|
|
6133
|
+
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
6134
|
}
|
|
6027
6135
|
await sleep(1000 * attempt);
|
|
6028
6136
|
}
|
package/dist/store/postgres.js
CHANGED
|
@@ -79,6 +79,30 @@ export class DeclaredOrgError extends Error {
|
|
|
79
79
|
this.name = 'DeclaredOrgError';
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Reads one stored entry. `hostnames` defaults as migration 0030 set it, and
|
|
84
|
+
* `gates` to none. An entry stored before gates existed with a non-empty
|
|
85
|
+
* `accessEmails` was gated: read it as one gate on its first non-'*' hostname
|
|
86
|
+
* (as stored), or on '*' when that is its only hostname, so the read fails
|
|
87
|
+
* closed and never drops a declared gate. With no hostname it is not served
|
|
88
|
+
* and reads no gates. The emails are dropped (people are chosen
|
|
89
|
+
* elsewhere now), and the next write stores the new shape without them. An
|
|
90
|
+
* entry that already has `gates` keeps them. A converted company over
|
|
91
|
+
* `gateLimit` is not an error here; validation reports it on the next validate.
|
|
92
|
+
*/
|
|
93
|
+
function storedEntryToEntry(stored, usesDomain) {
|
|
94
|
+
const { accessEmails, ...rest } = stored;
|
|
95
|
+
const hostnames = stored.hostnames ?? (stored.key === APP_ENTRY_KEY && usesDomain ? ['@'] : []);
|
|
96
|
+
let gates = stored.gates;
|
|
97
|
+
if (gates === undefined) {
|
|
98
|
+
const first = hostnames.find((h) => h !== '*') ?? hostnames[0];
|
|
99
|
+
gates =
|
|
100
|
+
accessEmails !== undefined && accessEmails.length > 0 && first !== undefined
|
|
101
|
+
? [{ key: stored.key, hostnames: [first] }]
|
|
102
|
+
: [];
|
|
103
|
+
}
|
|
104
|
+
return { ...rest, hostnames, gates };
|
|
105
|
+
}
|
|
82
106
|
function rowToOrg(row) {
|
|
83
107
|
return {
|
|
84
108
|
id: row.id,
|
|
@@ -117,11 +141,8 @@ function rowToOrg(row) {
|
|
|
117
141
|
usesDomain: row.hostingUsesDomain,
|
|
118
142
|
tunnel: row.hostingTunnel,
|
|
119
143
|
issuerApps: row.hostingIssuerApps,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
...entry,
|
|
123
|
-
hostnames: entry.hostnames ?? (entry.key === APP_ENTRY_KEY && row.hostingUsesDomain ? ['@'] : []),
|
|
124
|
-
})),
|
|
144
|
+
entries: row.hostingEntries.map((entry) => storedEntryToEntry(entry, row.hostingUsesDomain)),
|
|
145
|
+
gateLimit: row.hostingGateLimit,
|
|
125
146
|
},
|
|
126
147
|
sso: { enabled: row.ssoEnabled },
|
|
127
148
|
guests: { enabled: row.guestsEnabled },
|
|
@@ -163,6 +184,7 @@ function orgToRow(org) {
|
|
|
163
184
|
hostingTunnel: org.hosting.tunnel,
|
|
164
185
|
hostingIssuerApps: org.hosting.issuerApps,
|
|
165
186
|
hostingEntries: org.hosting.entries,
|
|
187
|
+
hostingGateLimit: org.hosting.gateLimit,
|
|
166
188
|
ssoEnabled: org.sso.enabled,
|
|
167
189
|
guestsEnabled: org.guests.enabled,
|
|
168
190
|
soloEnabled: org.solo.enabled,
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -533,6 +533,23 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
533
533
|
}, {}, {
|
|
534
534
|
$type: HostingEntry[];
|
|
535
535
|
}>;
|
|
536
|
+
hostingGateLimit: import("drizzle-orm/pg-core").PgColumn<{
|
|
537
|
+
name: "hosting_gate_limit";
|
|
538
|
+
tableName: "foundry_declared_orgs";
|
|
539
|
+
dataType: "number";
|
|
540
|
+
columnType: "PgInteger";
|
|
541
|
+
data: number;
|
|
542
|
+
driverParam: string | number;
|
|
543
|
+
notNull: true;
|
|
544
|
+
hasDefault: true;
|
|
545
|
+
isPrimaryKey: false;
|
|
546
|
+
isAutoincrement: false;
|
|
547
|
+
hasRuntimeDefault: false;
|
|
548
|
+
enumValues: undefined;
|
|
549
|
+
baseColumn: never;
|
|
550
|
+
identity: undefined;
|
|
551
|
+
generated: undefined;
|
|
552
|
+
}, {}, {}>;
|
|
536
553
|
ssoEnabled: import("drizzle-orm/pg-core").PgColumn<{
|
|
537
554
|
name: "sso_enabled";
|
|
538
555
|
tableName: "foundry_declared_orgs";
|
package/dist/store/schema.js
CHANGED
|
@@ -59,6 +59,8 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
|
|
|
59
59
|
// hosting_access_emails and hosting_app_env columns, which stay for an older
|
|
60
60
|
// reader and are no longer written.
|
|
61
61
|
hostingEntries: jsonb('hosting_entries').notNull().default([]).$type(),
|
|
62
|
+
// HostingConfig.gateLimit (org.ts, #603); migrations/0048. Default 3 is DEFAULT_GATE_LIMIT.
|
|
63
|
+
hostingGateLimit: integer('hosting_gate_limit').notNull().default(3),
|
|
62
64
|
ssoEnabled: boolean('sso_enabled').notNull().default(false),
|
|
63
65
|
guestsEnabled: boolean('guests_enabled').notNull().default(false),
|
|
64
66
|
soloEnabled: boolean('solo_enabled').notNull().default(false),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/foundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
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",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@wtfalch/audit": "0.6.0",
|
|
45
45
|
"@wtfalch/auth": "0.16.0",
|
|
46
|
-
"@wtfalch/edge": "0.
|
|
46
|
+
"@wtfalch/edge": "0.11.0",
|
|
47
47
|
"@wtfalch/email": "^0.17.0",
|
|
48
48
|
"@wtfalch/providers": "0.7.5",
|
|
49
49
|
"ssh2": "1.17.0",
|