@wtfalch/foundry 0.60.0 → 0.62.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/apps/cloudflare.d.ts +173 -0
- package/dist/apps/cloudflare.js +17 -0
- package/dist/apps/deploy.d.ts +116 -0
- package/dist/apps/deploy.js +791 -0
- package/dist/apps/github.d.ts +47 -0
- package/dist/apps/github.js +201 -0
- package/dist/apps/index.d.ts +9 -0
- package/dist/apps/index.js +9 -0
- package/dist/apps/migrations.d.ts +110 -0
- package/dist/apps/migrations.js +458 -0
- package/dist/audit/connector.d.ts +43 -0
- package/dist/audit/connector.js +22 -0
- package/dist/audit/evidence.d.ts +49 -0
- package/dist/audit/evidence.js +56 -0
- package/dist/audit/index.d.ts +9 -0
- package/dist/audit/index.js +9 -0
- package/dist/audit/migrations.d.ts +5 -0
- package/dist/audit/migrations.js +5 -0
- package/dist/audit/registry.d.ts +49 -0
- package/dist/audit/registry.js +44 -0
- package/dist/audit/seal.d.ts +49 -0
- package/dist/audit/seal.js +131 -0
- package/dist/audit/sealer-role.d.ts +33 -0
- package/dist/audit/sealer-role.js +56 -0
- package/dist/audit/signer.d.ts +40 -0
- package/dist/audit/signer.js +78 -0
- package/dist/audit/tick.d.ts +36 -0
- package/dist/audit/tick.js +87 -0
- package/dist/catalogue/copy-en.d.ts +1 -0
- package/dist/catalogue/copy-en.js +129 -0
- package/dist/catalogue/index.d.ts +9 -0
- package/dist/catalogue/index.js +21 -0
- package/dist/catalogue/integrations.d.ts +4 -0
- package/dist/catalogue/integrations.js +104 -0
- package/dist/catalogue/systems.d.ts +7 -0
- package/dist/catalogue/systems.js +31 -0
- package/dist/catalogue/types.d.ts +110 -0
- package/dist/catalogue/types.js +1 -0
- package/dist/entry-rules.d.ts +53 -0
- package/dist/entry-rules.js +71 -0
- package/dist/hosted-label.d.ts +13 -0
- package/dist/hosted-label.js +81 -0
- package/dist/index.d.ts +14 -5
- package/dist/index.js +7 -3
- package/dist/migrations/0062_hosting_github.sql +6 -0
- package/dist/migrations/0063_boule_apps.sql +7 -0
- package/dist/migrations/0064_requested_changes_boule_apps.sql +6 -0
- package/dist/migrations/0065_native_build_runs.sql +32 -0
- package/dist/migrations/0066_org_kosmos.sql +10 -0
- package/dist/modules/ai.d.ts +3 -0
- package/dist/modules/ai.js +64 -0
- package/dist/modules/assistants.d.ts +3 -0
- package/dist/modules/assistants.js +72 -0
- package/dist/modules/audit-ledger.d.ts +29 -0
- package/dist/modules/audit-ledger.js +49 -0
- package/dist/modules/audit.d.ts +7 -0
- package/dist/modules/audit.js +44 -0
- package/dist/modules/database.d.ts +23 -0
- package/dist/modules/database.js +189 -0
- package/dist/modules/email.d.ts +3 -0
- package/dist/modules/email.js +18 -0
- package/dist/modules/entry-parts.d.ts +23 -0
- package/dist/modules/entry-parts.js +63 -0
- package/dist/modules/files.d.ts +3 -0
- package/dist/modules/files.js +64 -0
- package/dist/modules/index.d.ts +74 -0
- package/dist/modules/index.js +774 -0
- package/dist/modules/jobs.d.ts +3 -0
- package/dist/modules/jobs.js +23 -0
- package/dist/modules/organisations.d.ts +3 -0
- package/dist/modules/organisations.js +20 -0
- package/dist/modules/outputs.d.ts +9 -0
- package/dist/modules/outputs.js +38 -0
- package/dist/modules/sign-in.d.ts +3 -0
- package/dist/modules/sign-in.js +197 -0
- package/dist/modules/signed-records.d.ts +3 -0
- package/dist/modules/signed-records.js +32 -0
- package/dist/modules/types.d.ts +194 -0
- package/dist/modules/types.js +1 -0
- package/dist/modules/workspaces.d.ts +3 -0
- package/dist/modules/workspaces.js +75 -0
- package/dist/native-build.d.ts +70 -0
- package/dist/native-build.js +92 -0
- package/dist/org.d.ts +184 -24
- package/dist/org.js +444 -49
- package/dist/plan/agora.d.ts +1 -1
- package/dist/plan/agora.js +4 -0
- package/dist/plan/apps-db-route.d.ts +90 -0
- package/dist/plan/apps-db-route.js +168 -0
- package/dist/plan/apps-zones.d.ts +56 -0
- package/dist/plan/apps-zones.js +118 -0
- package/dist/plan/archon-blue-green.js +11 -1
- package/dist/plan/blue-green-apply.d.ts +37 -1
- package/dist/plan/blue-green-apply.js +55 -15
- package/dist/plan/blue-green.d.ts +60 -8
- package/dist/plan/blue-green.js +115 -10
- package/dist/plan/boule.d.ts +23 -1
- package/dist/plan/boule.js +35 -2
- package/dist/plan/broker-entries.d.ts +28 -3
- package/dist/plan/broker-entries.js +74 -3
- package/dist/plan/broker-registrar.d.ts +168 -0
- package/dist/plan/broker-registrar.js +440 -0
- package/dist/plan/broker-workspaces-database.d.ts +48 -0
- package/dist/plan/broker-workspaces-database.js +87 -0
- package/dist/plan/deploy-health.js +9 -2
- package/dist/plan/desktop-callback.js +15 -7
- package/dist/plan/diff.d.ts +8 -2
- package/dist/plan/diff.js +552 -104
- package/dist/plan/edge-access.d.ts +6 -0
- package/dist/plan/edge-access.js +1 -0
- package/dist/plan/edge-dns.d.ts +6 -0
- package/dist/plan/edge-dns.js +9 -7
- package/dist/plan/entry-ai-key.d.ts +1 -1
- package/dist/plan/entry-ai-key.js +2 -2
- package/dist/plan/entry-audit.d.ts +58 -0
- package/dist/plan/entry-audit.js +122 -0
- package/dist/plan/entry-customer-grants.d.ts +75 -0
- package/dist/plan/entry-customer-grants.js +154 -0
- package/dist/plan/entry-edge-key.d.ts +26 -1
- package/dist/plan/entry-edge-key.js +42 -3
- package/dist/plan/entry-jobs.d.ts +64 -0
- package/dist/plan/entry-jobs.js +92 -0
- package/dist/plan/entry-repo-key-apply.d.ts +55 -0
- package/dist/plan/entry-repo-key-apply.js +185 -0
- package/dist/plan/entry-repo-key.d.ts +92 -0
- package/dist/plan/entry-repo-key.js +235 -0
- package/dist/plan/entry-workspaces.d.ts +55 -0
- package/dist/plan/entry-workspaces.js +90 -0
- package/dist/plan/executor.d.ts +231 -19
- package/dist/plan/executor.js +1648 -129
- package/dist/plan/gates.d.ts +1 -1
- package/dist/plan/gates.js +62 -9
- package/dist/plan/github-deploy-keys.d.ts +78 -0
- package/dist/plan/github-deploy-keys.js +305 -0
- package/dist/plan/github-source.d.ts +53 -0
- package/dist/plan/github-source.js +140 -0
- package/dist/plan/held-key-refs.d.ts +12 -0
- package/dist/plan/held-key-refs.js +12 -0
- package/dist/plan/hostnames.d.ts +9 -0
- package/dist/plan/hostnames.js +28 -1
- package/dist/plan/index.d.ts +9 -0
- package/dist/plan/index.js +6 -0
- package/dist/plan/jobs-scheduler-http.js +3 -0
- package/dist/plan/keys-registrar-http.d.ts +32 -0
- package/dist/plan/keys-registrar-http.js +105 -0
- package/dist/plan/keys-registrar.d.ts +67 -0
- package/dist/plan/keys-registrar.js +103 -0
- package/dist/plan/kosmos.d.ts +56 -0
- package/dist/plan/kosmos.js +84 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +2 -2
- package/dist/plan/locked-hostnames.js +7 -4
- package/dist/plan/login-host.js +11 -5
- package/dist/plan/machine-paths.d.ts +36 -0
- package/dist/plan/machine-paths.js +137 -0
- package/dist/plan/mcp.d.ts +1 -1
- package/dist/plan/mcp.js +49 -17
- package/dist/plan/org-apps-admin.d.ts +108 -0
- package/dist/plan/org-apps-admin.js +434 -0
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +15 -2
- package/dist/plan/org-postgres-migrate.js +1 -0
- package/dist/plan/own-diff.d.ts +23 -0
- package/dist/plan/own-diff.js +53 -0
- package/dist/plan/own-hostnames.d.ts +2 -0
- package/dist/plan/own-hostnames.js +1 -0
- package/dist/plan/own-jobs-route.d.ts +47 -0
- package/dist/plan/own-jobs-route.js +91 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/own-services.d.ts +4 -0
- package/dist/plan/own-services.js +4 -0
- package/dist/plan/plan-apply.d.ts +39 -0
- package/dist/plan/plan-apply.js +163 -9
- package/dist/plan/platform-diff.d.ts +2 -2
- package/dist/plan/platform-diff.js +7 -2
- package/dist/plan/platform-org.d.ts +30 -0
- package/dist/plan/platform-org.js +12 -1
- package/dist/plan/platform-providers.d.ts +2 -0
- package/dist/plan/platform-providers.js +13 -2
- package/dist/plan/post-logout-uris.d.ts +1 -1
- package/dist/plan/post-logout-uris.js +12 -3
- package/dist/plan/private-pages.d.ts +26 -0
- package/dist/plan/private-pages.js +39 -0
- package/dist/plan/providers-readers.js +20 -1
- package/dist/plan/proxy-labels.d.ts +12 -1
- package/dist/plan/proxy-labels.js +32 -2
- package/dist/plan/release-host.d.ts +6 -2
- package/dist/plan/release-host.js +36 -8
- package/dist/plan/stage-env.d.ts +19 -1
- package/dist/plan/stage-env.js +25 -1
- package/dist/plan/stage-guards.d.ts +20 -0
- package/dist/plan/stage-guards.js +24 -0
- package/dist/plan/stage-isolation.d.ts +24 -0
- package/dist/plan/stage-isolation.js +29 -1
- package/dist/plan/worker-app-database.d.ts +46 -42
- package/dist/plan/worker-app-database.js +150 -153
- package/dist/plan/worker-app.d.ts +5 -65
- package/dist/plan/worker-app.js +28 -105
- package/dist/plan/worker.d.ts +2 -2
- package/dist/plan/worker.js +2 -1
- package/dist/plan/workspace-client.d.ts +25 -0
- package/dist/plan/workspace-client.js +48 -0
- package/dist/plan/workspace-sign-in.d.ts +55 -0
- package/dist/plan/workspace-sign-in.js +273 -0
- package/dist/plan/workspace-worker.d.ts +4 -0
- package/dist/plan/workspace-worker.js +13 -0
- package/dist/plan/workspace.d.ts +101 -0
- package/dist/plan/workspace.js +305 -0
- package/dist/release/decide.d.ts +30 -1
- package/dist/release/decide.js +73 -11
- package/dist/release/health.d.ts +8 -3
- package/dist/release/health.js +58 -5
- package/dist/release/migration-compatibility.d.ts +66 -0
- package/dist/release/migration-compatibility.js +340 -0
- package/dist/release/run.d.ts +2 -5
- package/dist/release/run.js +4 -0
- package/dist/release/state.d.ts +3 -1
- package/dist/release/state.js +6 -0
- package/dist/release/types.d.ts +20 -0
- package/dist/requested-changes.d.ts +11 -2
- package/dist/requested-changes.js +13 -1
- package/dist/service-labels.d.ts +4 -0
- package/dist/service-labels.js +11 -0
- package/dist/slug.d.ts +2 -0
- package/dist/slug.js +20 -7
- package/dist/store/postgres.js +25 -2
- package/dist/store/schema.d.ts +73 -1
- package/dist/store/schema.js +10 -0
- package/dist/worker-app-config.d.ts +1 -11
- package/dist/worker-app-config.js +4 -17
- package/dist/worker-app-migrations.d.ts +105 -0
- package/dist/worker-app-migrations.js +166 -0
- package/dist/worker-app-upload.d.ts +103 -0
- package/dist/worker-app-upload.js +128 -0
- package/package.json +19 -2
package/dist/plan/worker-app.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { APPS_DOMAIN } from '../org.js';
|
|
3
|
+
import { WORKER_APP_KEYS_SECRET, WORKER_APP_KEYS_URL_VAR, workerAppHasBuild, workerAppHostId, } from '../worker-app-config.js';
|
|
4
|
+
import { WORKER_APP_DNS_RECORD, pendingWorkerAppMigrations, strayContainerImage, withCallTimeout, workerAppRegistryPrefix, workerAppShape, workerAppUpload, } from '../worker-app-upload.js';
|
|
3
5
|
import { entryAuthEnv } from './broker-entries.js';
|
|
4
6
|
import { workerAppHostname } from './hostnames.js';
|
|
7
|
+
import { workspaceWorkerParent } from './workspace-worker.js';
|
|
5
8
|
import { appStepKey } from './platform-diff.js';
|
|
6
9
|
import { remoteDatabaseKeys } from './remote-database.js';
|
|
7
10
|
import { applyWorkerAppDatabase, deleteWorkerAppDatabaseRoute, deleteWorkerAppHyperdrive, workerAppDbRouteMarkerScopeKey, workerAppHyperdriveScopeKey, workerAppMigrationsScopeKey, } from './worker-app-database.js';
|
|
8
|
-
|
|
11
|
+
export { WORKER_APP_DNS_RECORD, withCallTimeout, workerAppRegistryPrefix };
|
|
9
12
|
/**
|
|
10
13
|
* ADR 0002: a hosting entry that declares `workerApp` runs as one Cloudflare
|
|
11
14
|
* Worker with Durable Objects and Containers on the platform account. One step
|
|
@@ -60,14 +63,11 @@ export const workerAppTraceScopeKeys = (entryKey) => [
|
|
|
60
63
|
export const workerAppResourceKey = (stepKey) => `cloudflare:worker-app:${stepKey}`;
|
|
61
64
|
export const WORKER_APP_ORG_HASH_LENGTH = 8;
|
|
62
65
|
/** `${8 hex of the org id's sha256}-${entry key}-app`. */
|
|
63
|
-
export const workerAppName = (org, entry) =>
|
|
66
|
+
export const workerAppName = (org, entry) => workspaceWorkerParent(entry) !== null
|
|
67
|
+
? `${org.slug}-${workspaceWorkerParent(entry)}-work`
|
|
68
|
+
: `${createHash('sha256').update(org.id).digest('hex').slice(0, WORKER_APP_ORG_HASH_LENGTH)}-${entry.key}-app`;
|
|
64
69
|
/** The zone that holds the routes of an org with no domain of its own. */
|
|
65
70
|
export const WORKER_APP_OIKOS_ZONE = 'oikos.host';
|
|
66
|
-
/** The record a Worker route needs on the org's own zone: Cloudflare's documented dummy, proxied. */
|
|
67
|
-
export const WORKER_APP_DNS_RECORD = {
|
|
68
|
-
type: 'AAAA',
|
|
69
|
-
content: '100::',
|
|
70
|
-
};
|
|
71
71
|
/** True once the Worker app holds the broker key (`authEnv` delivered it). */
|
|
72
72
|
export const brokerKeyDelivered = (recorded, entryKey) => Boolean(parseJson(recorded[workerAppSecretsScopeKey(entryKey)], {})
|
|
73
73
|
.AUTH_APP_KEY);
|
|
@@ -107,7 +107,11 @@ export function custodyRenewalDue(recorded, entryKey, now) {
|
|
|
107
107
|
}
|
|
108
108
|
const sortedEntries = (record) => Object.entries(record ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
109
109
|
/** The zone apex the entry's route lives in. */
|
|
110
|
-
export const workerAppApex = (org) =>
|
|
110
|
+
export const workerAppApex = (org, entry) => entry && workspaceWorkerParent(entry) !== null
|
|
111
|
+
? APPS_DOMAIN
|
|
112
|
+
: org.hosting.usesDomain && org.domain
|
|
113
|
+
? org.domain
|
|
114
|
+
: WORKER_APP_OIKOS_ZONE;
|
|
111
115
|
/** True when the want has a secret that must be held (a generated one, the broker key, a custody credential). */
|
|
112
116
|
const holdsGenerated = (want) => want.secrets.length > 0;
|
|
113
117
|
/**
|
|
@@ -160,27 +164,19 @@ export function wantedWorkerApp(org, entry, recorded, input) {
|
|
|
160
164
|
}
|
|
161
165
|
if (missing.length > 0)
|
|
162
166
|
return { missing };
|
|
167
|
+
const shape = workerAppShape(app);
|
|
163
168
|
return {
|
|
164
169
|
want: {
|
|
165
170
|
name: workerAppName(org, entry),
|
|
166
|
-
artifactUrl:
|
|
167
|
-
artifactSha256:
|
|
168
|
-
compatibilityDate:
|
|
169
|
-
compatibilityFlags:
|
|
171
|
+
artifactUrl: shape.artifactUrl,
|
|
172
|
+
artifactSha256: shape.artifactSha256,
|
|
173
|
+
compatibilityDate: shape.compatibilityDate,
|
|
174
|
+
compatibilityFlags: shape.compatibilityFlags,
|
|
170
175
|
host: workerAppHostname(org, entry),
|
|
171
|
-
apex: workerAppApex(org),
|
|
172
|
-
classes:
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
migrations: app.durableObjects.migrations,
|
|
176
|
-
containers: (app.containers ?? [])
|
|
177
|
-
.map((c) => ({
|
|
178
|
-
className: c.className,
|
|
179
|
-
image: `${c.image.repository}@${c.image.digest}`,
|
|
180
|
-
instanceType: c.instanceType,
|
|
181
|
-
maxInstances: c.maxInstances,
|
|
182
|
-
}))
|
|
183
|
-
.sort((a, b) => a.className.localeCompare(b.className)),
|
|
176
|
+
apex: workerAppApex(org, entry),
|
|
177
|
+
classes: shape.classes,
|
|
178
|
+
migrations: shape.migrations,
|
|
179
|
+
containers: shape.containers,
|
|
184
180
|
vars: sortedEntries(vars),
|
|
185
181
|
...(app.database
|
|
186
182
|
? {
|
|
@@ -223,8 +219,6 @@ export function migrationPrefixError(applied, declared) {
|
|
|
223
219
|
}
|
|
224
220
|
/** The unapplied migrations after the last tag in `applied`. */
|
|
225
221
|
export const unappliedMigrations = (declared, applied) => declared.slice(applied.length);
|
|
226
|
-
/** The registry prefix a container image of this platform must start with. */
|
|
227
|
-
export const workerAppRegistryPrefix = (accountId) => `registry.cloudflare.com/${accountId}/`;
|
|
228
222
|
/** Every entry key with a deployed or half-deployed Worker app recorded. */
|
|
229
223
|
export function liveWorkerApps(orgId, recordedIds) {
|
|
230
224
|
const mainPrefix = `cloudflare:worker-app:${orgId}:`;
|
|
@@ -328,9 +322,7 @@ export function planWorkerApps(org, recordedIds, input) {
|
|
|
328
322
|
continue;
|
|
329
323
|
}
|
|
330
324
|
const { want } = wanted;
|
|
331
|
-
const stray = (app
|
|
332
|
-
!c.image.repository.startsWith(workerAppRegistryPrefix(input.registryAccountId)) ||
|
|
333
|
-
c.image.repository.split('/').some((part) => /^\.+$/.test(part)));
|
|
325
|
+
const stray = strayContainerImage(app, input.registryAccountId);
|
|
334
326
|
if (stray) {
|
|
335
327
|
block(input.registryAccountId === undefined
|
|
336
328
|
? 'the host supplies no Cloudflare account id for the container registry'
|
|
@@ -400,31 +392,6 @@ export function planWorkerApps(org, recordedIds, input) {
|
|
|
400
392
|
/** An outside call that has not answered in this long fails the step; the next apply repairs it. */
|
|
401
393
|
export const WORKER_APP_CALL_TIMEOUT_MS = 120_000;
|
|
402
394
|
export const WORKER_APP_KEYS_DELIVERY_ATTEMPTS = 3;
|
|
403
|
-
/**
|
|
404
|
-
* Races `task` against a timer. An error names the call and edge's error code, never the cause.
|
|
405
|
-
*/
|
|
406
|
-
export function withCallTimeout(ms, label, task) {
|
|
407
|
-
return new Promise((resolve, reject) => {
|
|
408
|
-
const timer = setTimeout(() => reject(new Error(`${label}: no answer within ${ms} ms`)), ms);
|
|
409
|
-
task().then((value) => {
|
|
410
|
-
clearTimeout(timer);
|
|
411
|
-
resolve(value);
|
|
412
|
-
}, (error) => {
|
|
413
|
-
clearTimeout(timer);
|
|
414
|
-
// The cause is dropped: a provider error can echo a request body. The
|
|
415
|
-
// HTTP status stays, because callers tell "already gone" by it. So does
|
|
416
|
-
// edge's error code (a fixed lower-case enum, never a secret), to say why.
|
|
417
|
-
const { status, code } = (error ?? {});
|
|
418
|
-
const known = typeof code === 'string' && /^[a-z_]{1,40}$/.test(code) ? code : undefined;
|
|
419
|
-
const failure = new Error(known ? `${label} failed (${known})` : `${label} failed`);
|
|
420
|
-
if (typeof status === 'number')
|
|
421
|
-
failure.status = status;
|
|
422
|
-
if (known)
|
|
423
|
-
failure.code = known;
|
|
424
|
-
reject(failure);
|
|
425
|
-
});
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
395
|
const isGone = (error) => {
|
|
429
396
|
const status = error?.status;
|
|
430
397
|
return typeof status === 'number' && status === 404;
|
|
@@ -481,58 +448,14 @@ export async function applyWorkerApp(deps, org, action, context) {
|
|
|
481
448
|
const recordedTags = parseJson(recorded0[workerAppTagsScopeKey(entryKey)], []);
|
|
482
449
|
// The live tag is the fact; the record can be behind it after a crash between upload and record.
|
|
483
450
|
const liveTag = await call('read the migration tag', () => workers.liveMigrationTag(want.name));
|
|
484
|
-
|
|
485
|
-
if (
|
|
486
|
-
|
|
487
|
-
else {
|
|
488
|
-
const index = declaredTags.indexOf(liveTag);
|
|
489
|
-
if (index < 0) {
|
|
490
|
-
throw new Error(`${at}: the live Worker holds migration '${liveTag}', which is not in the declared list; migrations are append-only and foundry will not guess`);
|
|
491
|
-
}
|
|
492
|
-
appliedCount = index + 1;
|
|
493
|
-
}
|
|
451
|
+
const pending = pendingWorkerAppMigrations(want.migrations, liveTag);
|
|
452
|
+
if (!pending.ok)
|
|
453
|
+
throw new Error(`${at}: ${pending.reason}`);
|
|
494
454
|
const recordedError = migrationPrefixError(recordedTags, declaredTags);
|
|
495
455
|
if (recordedError)
|
|
496
456
|
throw new Error(`${at}: ${recordedError}`);
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
? {
|
|
500
|
-
...(liveTag !== undefined ? { oldTag: liveTag } : {}),
|
|
501
|
-
newTag: pending[pending.length - 1]?.tag,
|
|
502
|
-
steps: pending.map((m) => ({
|
|
503
|
-
...(m.newSqliteClasses ? { newSqliteClasses: m.newSqliteClasses } : {}),
|
|
504
|
-
...(m.deletedClasses ? { deletedClasses: m.deletedClasses } : {}),
|
|
505
|
-
...(m.renamedClasses ? { renamedClasses: m.renamedClasses } : {}),
|
|
506
|
-
})),
|
|
507
|
-
}
|
|
508
|
-
: undefined;
|
|
509
|
-
await call('upload the script', () => workers.uploadWorkerApp({
|
|
510
|
-
name: want.name,
|
|
511
|
-
artifactUrl: want.artifactUrl,
|
|
512
|
-
artifactSha256: want.artifactSha256,
|
|
513
|
-
compatibilityDate: want.compatibilityDate,
|
|
514
|
-
compatibilityFlags: want.compatibilityFlags,
|
|
515
|
-
bindings: [
|
|
516
|
-
...want.classes.map((c) => ({
|
|
517
|
-
type: 'durable_object_namespace',
|
|
518
|
-
name: c.binding,
|
|
519
|
-
class_name: c.className,
|
|
520
|
-
})),
|
|
521
|
-
...want.vars.map(([name, text]) => ({
|
|
522
|
-
type: 'plain_text',
|
|
523
|
-
name,
|
|
524
|
-
text,
|
|
525
|
-
})),
|
|
526
|
-
...(hyperdriveId
|
|
527
|
-
? [{ type: 'hyperdrive', name: WORKER_APP_HYPERDRIVE_BINDING, id: hyperdriveId }]
|
|
528
|
-
: []),
|
|
529
|
-
],
|
|
530
|
-
observability: { enabled: true, head_sampling_rate: 1 },
|
|
531
|
-
...(migrations ? { migrations } : {}),
|
|
532
|
-
...(want.containers.length > 0
|
|
533
|
-
? { containers: want.containers.map((c) => c.className) }
|
|
534
|
-
: {}),
|
|
535
|
-
}));
|
|
457
|
+
const migrations = pending.migrations;
|
|
458
|
+
await call('upload the script', () => workers.uploadWorkerApp(workerAppUpload(want.name, want, want.vars, migrations, hyperdriveId)));
|
|
536
459
|
context.recordScopeId(workerAppTagsScopeKey(entryKey), JSON.stringify(declaredTags));
|
|
537
460
|
await context.persistScopeIds();
|
|
538
461
|
// A Worker that no longer declares a database no longer binds its config: delete it now.
|
package/dist/plan/worker.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DeclaredOrg, HostingEntry, WorkerConfig } from '../org.js';
|
|
2
|
+
import { WORKER_COMPATIBILITY_DATE } from '../worker-app-upload.js';
|
|
2
3
|
import type { DraftStep } from './diff.js';
|
|
3
4
|
/**
|
|
4
5
|
* F8 (#318): one plain Cloudflare Worker per hosting entry that declares
|
|
@@ -91,7 +92,7 @@ export declare const workerName: (org: DeclaredOrg, entry: HostingEntry) => stri
|
|
|
91
92
|
* like {@link RATE_LIMIT_FREE_PLAN_PERIOD_SECONDS} is a fixed Cloudflare
|
|
92
93
|
* Free plan value rather than something an org declares.
|
|
93
94
|
*/
|
|
94
|
-
export
|
|
95
|
+
export { WORKER_COMPATIBILITY_DATE };
|
|
95
96
|
export type WorkerMode = 'apply' | 'retire';
|
|
96
97
|
export interface WorkerWant {
|
|
97
98
|
name: string;
|
|
@@ -131,4 +132,3 @@ export declare function planWorkers(org: DeclaredOrg, recordedIds: Readonly<Reco
|
|
|
131
132
|
steps: DraftStep[];
|
|
132
133
|
blocked: Blocked;
|
|
133
134
|
};
|
|
134
|
-
export {};
|
package/dist/plan/worker.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { WORKER_COMPATIBILITY_DATE } from '../worker-app-upload.js';
|
|
2
3
|
import { entryHostnames } from './hostnames.js';
|
|
3
4
|
import { lockedHostnames } from './locked-hostnames.js';
|
|
4
5
|
import { appStepKey } from './platform-diff.js';
|
|
@@ -123,7 +124,7 @@ export const workerName = (org, entry) =>
|
|
|
123
124
|
* like {@link RATE_LIMIT_FREE_PLAN_PERIOD_SECONDS} is a fixed Cloudflare
|
|
124
125
|
* Free plan value rather than something an org declares.
|
|
125
126
|
*/
|
|
126
|
-
export
|
|
127
|
+
export { WORKER_COMPATIBILITY_DATE };
|
|
127
128
|
/**
|
|
128
129
|
* With a wildcard route (`*.<domain>/*`) the Worker would also capture every
|
|
129
130
|
* hostname foundry provisions on the domain, so each gets a no-Worker route
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DeclaredOrg, HostingEntry } from '../org.js';
|
|
2
|
+
/** The name of an entry's workspace client and of the key its workspace broker entry is held under. */
|
|
3
|
+
export declare const workspaceClientKey: (entryKey: string) => string;
|
|
4
|
+
/** The company's ZITADEL organisation for the customers of its workspaces. The space keeps it apart from every slug. */
|
|
5
|
+
export declare const testCustomersOrgName: (slug: string) => string;
|
|
6
|
+
/** The recorded id of that organisation (the step's `system:kind:key`). */
|
|
7
|
+
export declare const testCustomersOrgKey: (orgId: string) => string;
|
|
8
|
+
/** `'1'` or `'0'`: whether the test customers organisation was last left open to self-registration. */
|
|
9
|
+
export declare const TEST_CUSTOMERS_SIGN_UP_SCOPE_KEY = "test_customers_sign_up";
|
|
10
|
+
/** `'email-code'` once the emailed-code metadata was written to the test customers organisation. */
|
|
11
|
+
export declare const TEST_CUSTOMERS_SIGN_IN_SCOPE_KEY = "test_customers_sign_in";
|
|
12
|
+
/** The server entries whose workspace has its own sign-in: a workspace and `uses.auth`. */
|
|
13
|
+
export declare function workspaceAuthEntries(org: DeclaredOrg): HostingEntry[];
|
|
14
|
+
/** The entry whose workspace client is named `clientKey`, or `undefined`. */
|
|
15
|
+
export declare const workspaceAuthEntryOf: (org: DeclaredOrg, clientKey: string) => HostingEntry | undefined;
|
|
16
|
+
/** The entries whose workspace signs in the test customers organisation. */
|
|
17
|
+
export declare const testCustomersEntries: (org: DeclaredOrg) => HostingEntry[];
|
|
18
|
+
/** The addresses a workspace client signs in at: the workspace host. */
|
|
19
|
+
export declare const workspaceClientHosts: (org: DeclaredOrg, entry: HostingEntry) => string[];
|
|
20
|
+
/**
|
|
21
|
+
* `org` (planned without its dark entries) plus the sign-in-only shape of the dark entries whose workspace has
|
|
22
|
+
* sign-in, so the login host's lists of customers clients match what the executor, which reads the org as
|
|
23
|
+
* declared, writes. The copies have no address and no Worker.
|
|
24
|
+
*/
|
|
25
|
+
export declare function withDarkWorkspaceSignIn(org: DeclaredOrg, declared: DeclaredOrg): DeclaredOrg;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { workspaceHost } from './hostnames.js';
|
|
2
|
+
/**
|
|
3
|
+
* What the sign-in of a server workspace is called, shared by the planners that must agree on it. A workspace of
|
|
4
|
+
* an entry with `uses.auth` signs in with its own ZITADEL client named `<entry key>-work`, and its customers
|
|
5
|
+
* sign in a separate "<slug> test customers" organisation, never the live customers one.
|
|
6
|
+
*/
|
|
7
|
+
/** The suffix of the client, broker entry and held key that belong to an entry's workspace. */
|
|
8
|
+
const WORKSPACE_CLIENT_SUFFIX = '-work';
|
|
9
|
+
/** The name of an entry's workspace client and of the key its workspace broker entry is held under. */
|
|
10
|
+
export const workspaceClientKey = (entryKey) => `${entryKey}${WORKSPACE_CLIENT_SUFFIX}`;
|
|
11
|
+
/** The company's ZITADEL organisation for the customers of its workspaces. The space keeps it apart from every slug. */
|
|
12
|
+
export const testCustomersOrgName = (slug) => `${slug} test customers`;
|
|
13
|
+
/** The recorded id of that organisation (the step's `system:kind:key`). */
|
|
14
|
+
export const testCustomersOrgKey = (orgId) => `zitadel:customers-test:${orgId}`;
|
|
15
|
+
/** `'1'` or `'0'`: whether the test customers organisation was last left open to self-registration. */
|
|
16
|
+
export const TEST_CUSTOMERS_SIGN_UP_SCOPE_KEY = 'test_customers_sign_up';
|
|
17
|
+
/** `'email-code'` once the emailed-code metadata was written to the test customers organisation. */
|
|
18
|
+
export const TEST_CUSTOMERS_SIGN_IN_SCOPE_KEY = 'test_customers_sign_in';
|
|
19
|
+
/** The server entries whose workspace has its own sign-in: a workspace and `uses.auth`. */
|
|
20
|
+
export function workspaceAuthEntries(org) {
|
|
21
|
+
if (!org.hosting.enabled)
|
|
22
|
+
return [];
|
|
23
|
+
return org.hosting.entries.filter((entry) => entry.workspace !== undefined &&
|
|
24
|
+
entry.workerApp === undefined &&
|
|
25
|
+
entry.uses?.auth !== undefined);
|
|
26
|
+
}
|
|
27
|
+
/** The entry whose workspace client is named `clientKey`, or `undefined`. */
|
|
28
|
+
export const workspaceAuthEntryOf = (org, clientKey) => workspaceAuthEntries(org).find((entry) => workspaceClientKey(entry.key) === clientKey);
|
|
29
|
+
/** The entries whose workspace signs in the test customers organisation. */
|
|
30
|
+
export const testCustomersEntries = (org) => workspaceAuthEntries(org).filter((entry) => entry.uses?.auth?.users === 'customers');
|
|
31
|
+
/** The addresses a workspace client signs in at: the workspace host. */
|
|
32
|
+
export const workspaceClientHosts = (org, entry) => [
|
|
33
|
+
workspaceHost(org.slug, entry.key),
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* `org` (planned without its dark entries) plus the sign-in-only shape of the dark entries whose workspace has
|
|
37
|
+
* sign-in, so the login host's lists of customers clients match what the executor, which reads the org as
|
|
38
|
+
* declared, writes. The copies have no address and no Worker.
|
|
39
|
+
*/
|
|
40
|
+
export function withDarkWorkspaceSignIn(org, declared) {
|
|
41
|
+
const present = new Set(org.hosting.entries.map((entry) => entry.key));
|
|
42
|
+
const dark = workspaceAuthEntries(declared)
|
|
43
|
+
.filter((entry) => !present.has(entry.key))
|
|
44
|
+
.map((entry) => ({ ...entry, hostnames: [], gates: [] }));
|
|
45
|
+
if (dark.length === 0)
|
|
46
|
+
return org;
|
|
47
|
+
return { ...org, hosting: { ...org.hosting, entries: [...org.hosting.entries, ...dark] } };
|
|
48
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { DeclaredOrg, HostingEntry } from '../org.js';
|
|
2
|
+
import type { UnmanagedItem } from '../plan.js';
|
|
3
|
+
import { type BrokerEntryInput, type BrokerEntryWant } from './broker-entries.js';
|
|
4
|
+
import type { DraftStep } from './diff.js';
|
|
5
|
+
import type { LiveResource } from './live.js';
|
|
6
|
+
/** The step `coolify:workspace-broker-entry` and its action key, for the planner and the executor to share. */
|
|
7
|
+
export declare const WORKSPACE_BROKER_ENTRY_KIND = "workspace-broker-entry";
|
|
8
|
+
/**
|
|
9
|
+
* The MCP resource of a workspace with `uses.auth.mcp`: its own endpoint on the workspace host, its own audience
|
|
10
|
+
* project in the test customers organisation (recorded by `zitadel:mcp-project` under the client's name), and the
|
|
11
|
+
* onboarding page on the workspace host. `null` without `mcp` or until the project is recorded.
|
|
12
|
+
*/
|
|
13
|
+
export declare function workspaceMcp(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>): {
|
|
14
|
+
resource: string;
|
|
15
|
+
projectId: string;
|
|
16
|
+
loginHost: string;
|
|
17
|
+
onboardingUrl?: string;
|
|
18
|
+
} | null;
|
|
19
|
+
/**
|
|
20
|
+
* What the app's env says about who signs in at the workspace, as recorded so far, or `null` until the
|
|
21
|
+
* organisation, client and issuer are all known. Non-secret values only.
|
|
22
|
+
*/
|
|
23
|
+
export declare function workspaceAuthEnv(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>, issuer: string | null): Record<string, string> | null;
|
|
24
|
+
/**
|
|
25
|
+
* The client ids of the company's live apps: every recorded client id except the workspaces' own. A stage that
|
|
26
|
+
* holds one of them could sign in as the live app.
|
|
27
|
+
*/
|
|
28
|
+
export declare function liveClientIds(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): string[];
|
|
29
|
+
/** What `ws.<key>.signin` records: the sign-in env above as JSON, or `null` while it is not all known. */
|
|
30
|
+
export declare function workspaceSignInMarker(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>, issuer: string | null): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* The test customers organisation, planned once an entry's workspace signs in customers. Sign-up and the emailed
|
|
33
|
+
* code follow what the company declares for its live customers (`customers.signUp`, the entries' `signIn`), so
|
|
34
|
+
* testers meet the same sign-in the customers will. Idempotent on the recorded id and the two markers.
|
|
35
|
+
*/
|
|
36
|
+
export declare function planTestCustomers(org: DeclaredOrg, live: readonly LiveResource[], claimed: Set<string>, recordedIds: Readonly<Record<string, string>>, metadataWired: boolean): {
|
|
37
|
+
step: DraftStep | null;
|
|
38
|
+
blocked: UnmanagedItem[];
|
|
39
|
+
};
|
|
40
|
+
/** One ZITADEL web client `<key>-work` per workspace with sign-in, planned once; its redirect addresses name the workspace host. */
|
|
41
|
+
export declare function planWorkspaceClients(org: DeclaredOrg, byKey: ReadonlyMap<string, LiveResource>, claimed: Set<string>): DraftStep[];
|
|
42
|
+
/** What a workspace broker entry must say, without its keys, or what is still missing. */
|
|
43
|
+
export declare function wantedWorkspaceBrokerEntry(org: DeclaredOrg, entry: HostingEntry, recorded: Readonly<Record<string, string>>): {
|
|
44
|
+
want: BrokerEntryWant;
|
|
45
|
+
} | {
|
|
46
|
+
missing: BrokerEntryInput[];
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* One broker `AUTH_APPS` entry per workspace with sign-in, written after the workspace step has put the stage's
|
|
50
|
+
* `AUTH_APP_KEY` in its env. Idempotent on the recorded marker of what the last write said.
|
|
51
|
+
*/
|
|
52
|
+
export declare function planWorkspaceBrokerEntries(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, plannedSteps: readonly DraftStep[], brokerWired: boolean, brokerWorkspacesDatabase: boolean, stageReady: (entry: HostingEntry) => boolean): {
|
|
53
|
+
steps: DraftStep[];
|
|
54
|
+
blocked: UnmanagedItem[];
|
|
55
|
+
};
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, entryClientIdScopeKey, } from './agora.js';
|
|
2
|
+
import { BOULE_CLIENT_ID_SCOPE_KEY } from './boule.js';
|
|
3
|
+
import { brokerEntryAppliedScopeKey, entryMcpProjectScopeKey, } from './broker-entries.js';
|
|
4
|
+
import { BROKER_WORKSPACES_DATABASE_ENV, workspacesDatabaseRef, workspacesUrl, } from './entry-workspaces.js';
|
|
5
|
+
import { workspaceHost } from './hostnames.js';
|
|
6
|
+
import { loginHostnameFor } from './login-host.js';
|
|
7
|
+
import { appStepKey } from './platform-diff.js';
|
|
8
|
+
import { TEST_CUSTOMERS_SIGN_IN_SCOPE_KEY, TEST_CUSTOMERS_SIGN_UP_SCOPE_KEY, testCustomersEntries, testCustomersOrgKey, workspaceAuthEntries, workspaceClientKey, } from './workspace-client.js';
|
|
9
|
+
/**
|
|
10
|
+
* The sign-in of a server workspace (`HostingEntry.workspace` with `uses.auth`), so the team and invited testers
|
|
11
|
+
* can test the whole app, customer sign-in and customer workspaces included, before it is live. It is built from
|
|
12
|
+
* the same pieces as the live entry's sign-in and shares none of its secrets:
|
|
13
|
+
* - a company with any such entry gets a "<slug> test customers" organisation (`planTestCustomers`), never the
|
|
14
|
+
* live customers one;
|
|
15
|
+
* - the entry gets its own ZITADEL client `<key>-work` at the workspace host (`planWorkspaceClients`);
|
|
16
|
+
* - the stage app gets its own `AUTH_*` values, cookie secret, app key and, with `uses.auth.workspaces`, its own
|
|
17
|
+
* workspaces key;
|
|
18
|
+
* - the broker gets its own entry `<slug>-<key>-work` (`planWorkspaceBrokerEntries`), whose key and workspaces
|
|
19
|
+
* key hash are the stage's, so it has its own product tenant in the broker.
|
|
20
|
+
*/
|
|
21
|
+
const resourceKey = (system, kind, key) => `${system}:${kind}:${key}`;
|
|
22
|
+
/** The step `coolify:workspace-broker-entry` and its action key, for the planner and the executor to share. */
|
|
23
|
+
export const WORKSPACE_BROKER_ENTRY_KIND = 'workspace-broker-entry';
|
|
24
|
+
/**
|
|
25
|
+
* The MCP resource of a workspace with `uses.auth.mcp`: its own endpoint on the workspace host, its own audience
|
|
26
|
+
* project in the test customers organisation (recorded by `zitadel:mcp-project` under the client's name), and the
|
|
27
|
+
* onboarding page on the workspace host. `null` without `mcp` or until the project is recorded.
|
|
28
|
+
*/
|
|
29
|
+
export function workspaceMcp(org, entry, recorded) {
|
|
30
|
+
const mcp = entry.uses?.auth?.mcp;
|
|
31
|
+
const projectId = recorded[entryMcpProjectScopeKey(workspaceClientKey(entry.key))];
|
|
32
|
+
if (!mcp || !projectId)
|
|
33
|
+
return null;
|
|
34
|
+
const host = workspaceHost(org.slug, entry.key);
|
|
35
|
+
return {
|
|
36
|
+
resource: `https://${host}${mcp.path}`,
|
|
37
|
+
projectId,
|
|
38
|
+
loginHost: loginHostnameFor(org),
|
|
39
|
+
...(mcp.onboardingPath ? { onboardingUrl: `https://${host}${mcp.onboardingPath}` } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* What the app's env says about who signs in at the workspace, as recorded so far, or `null` until the
|
|
44
|
+
* organisation, client and issuer are all known. Non-secret values only.
|
|
45
|
+
*/
|
|
46
|
+
export function workspaceAuthEnv(org, entry, recorded, issuer) {
|
|
47
|
+
const auth = entry.uses?.auth;
|
|
48
|
+
if (!auth)
|
|
49
|
+
return null;
|
|
50
|
+
const organizationId = auth.users === 'customers' ? recorded[testCustomersOrgKey(org.id)] : recorded.zitadel_org;
|
|
51
|
+
const clientId = recorded[entryClientIdScopeKey(workspaceClientKey(entry.key))];
|
|
52
|
+
if (!organizationId || !clientId || !issuer)
|
|
53
|
+
return null;
|
|
54
|
+
const mcp = workspaceMcp(org, entry, recorded);
|
|
55
|
+
if (auth.mcp && !mcp)
|
|
56
|
+
return null;
|
|
57
|
+
return {
|
|
58
|
+
AUTH_ORGANIZATION_ID: organizationId,
|
|
59
|
+
AUTH_CLIENT_ID: clientId,
|
|
60
|
+
AUTH_ISSUER: issuer,
|
|
61
|
+
...(auth.workspaces ? { WORKSPACES_URL: workspacesUrl(org) } : {}),
|
|
62
|
+
...(mcp
|
|
63
|
+
? {
|
|
64
|
+
MCP_AUDIENCE: mcp.projectId,
|
|
65
|
+
MCP_RESOURCE: mcp.resource,
|
|
66
|
+
MCP_ISSUER: `https://${mcp.loginHost}`,
|
|
67
|
+
}
|
|
68
|
+
: {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const SHARED_CLIENT_KEYS = new Set([
|
|
72
|
+
APP_CLIENT_ID_SCOPE_KEY,
|
|
73
|
+
CMS_CLIENT_ID_SCOPE_KEY,
|
|
74
|
+
AGORA_CLIENT_ID_SCOPE_KEY,
|
|
75
|
+
BOULE_CLIENT_ID_SCOPE_KEY,
|
|
76
|
+
]);
|
|
77
|
+
/**
|
|
78
|
+
* The client ids of the company's live apps: every recorded client id except the workspaces' own. A stage that
|
|
79
|
+
* holds one of them could sign in as the live app.
|
|
80
|
+
*/
|
|
81
|
+
export function liveClientIds(org, recorded) {
|
|
82
|
+
const own = new Set(workspaceAuthEntries(org).map((e) => entryClientIdScopeKey(workspaceClientKey(e.key))));
|
|
83
|
+
return Object.entries(recorded)
|
|
84
|
+
.filter(([key, id]) => Boolean(id) &&
|
|
85
|
+
(SHARED_CLIENT_KEYS.has(key) ||
|
|
86
|
+
(key.startsWith(ENTRY_CLIENT_ID_SCOPE_PREFIX) && !own.has(key))))
|
|
87
|
+
.map(([, id]) => id)
|
|
88
|
+
.sort();
|
|
89
|
+
}
|
|
90
|
+
/** What `ws.<key>.signin` records: the sign-in env above as JSON, or `null` while it is not all known. */
|
|
91
|
+
export function workspaceSignInMarker(org, entry, recorded, issuer) {
|
|
92
|
+
const env = workspaceAuthEnv(org, entry, recorded, issuer);
|
|
93
|
+
return env ? JSON.stringify(env) : null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The test customers organisation, planned once an entry's workspace signs in customers. Sign-up and the emailed
|
|
97
|
+
* code follow what the company declares for its live customers (`customers.signUp`, the entries' `signIn`), so
|
|
98
|
+
* testers meet the same sign-in the customers will. Idempotent on the recorded id and the two markers.
|
|
99
|
+
*/
|
|
100
|
+
export function planTestCustomers(org, live, claimed, recordedIds, metadataWired) {
|
|
101
|
+
const entries = testCustomersEntries(org);
|
|
102
|
+
const wanted = entries.length > 0;
|
|
103
|
+
const signUp = org.customers?.signUp === true;
|
|
104
|
+
const wantedSignIn = entries.some((e) => e.uses?.auth?.signIn === 'email-code')
|
|
105
|
+
? 'email-code'
|
|
106
|
+
: '';
|
|
107
|
+
const blocked = [];
|
|
108
|
+
const orgId = recordedIds[testCustomersOrgKey(org.id)];
|
|
109
|
+
const metadataBlocked = () => ({
|
|
110
|
+
description: `${org.slug}'s test customers organisation sign-in metadata is blocked, not planned: the host's ZITADEL client cannot set or remove organisation metadata`,
|
|
111
|
+
key: `zitadel:customers-test-signin:${org.id}`,
|
|
112
|
+
});
|
|
113
|
+
const draft = (desired, description) => ({
|
|
114
|
+
step: {
|
|
115
|
+
action: { system: 'zitadel', kind: 'customers-test', key: org.id, desired },
|
|
116
|
+
step: { description, destroys: false, migrates: false },
|
|
117
|
+
},
|
|
118
|
+
blocked,
|
|
119
|
+
});
|
|
120
|
+
if (!orgId) {
|
|
121
|
+
if (!wanted)
|
|
122
|
+
return { step: null, blocked };
|
|
123
|
+
const withSignIn = wantedSignIn === 'email-code' && metadataWired;
|
|
124
|
+
if (wantedSignIn === 'email-code' && !metadataWired)
|
|
125
|
+
blocked.push(metadataBlocked());
|
|
126
|
+
return draft({ signUp, ...(withSignIn ? { signIn: wantedSignIn } : {}) }, `provision a ZITADEL org for ${org.slug}'s test customers, with self-registration ${signUp ? 'on' : 'off'}${withSignIn ? ' and sign-in by emailed code' : ''}`);
|
|
127
|
+
}
|
|
128
|
+
for (const r of live) {
|
|
129
|
+
if (r.system === 'zitadel' && r.kind === 'project_grant' && r.config.grantedOrgId === orgId) {
|
|
130
|
+
claimed.add(resourceKey(r.system, r.kind, r.key));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const appliedSignUp = recordedIds[TEST_CUSTOMERS_SIGN_UP_SCOPE_KEY] === '1' ? '1' : '0';
|
|
134
|
+
const wantedSignUp = wanted && signUp ? '1' : '0';
|
|
135
|
+
const appliedSignIn = recordedIds[TEST_CUSTOMERS_SIGN_IN_SCOPE_KEY] === 'email-code' ? 'email-code' : '';
|
|
136
|
+
const signInDiffers = appliedSignIn !== (wanted ? wantedSignIn : '');
|
|
137
|
+
if (signInDiffers && !metadataWired)
|
|
138
|
+
blocked.push(metadataBlocked());
|
|
139
|
+
const writeSignIn = signInDiffers && metadataWired;
|
|
140
|
+
if (appliedSignUp === wantedSignUp && !writeSignIn)
|
|
141
|
+
return { step: null, blocked };
|
|
142
|
+
return draft({
|
|
143
|
+
signUp: wantedSignUp === '1',
|
|
144
|
+
...(writeSignIn ? { signIn: wanted ? wantedSignIn : '' } : {}),
|
|
145
|
+
}, appliedSignUp === wantedSignUp
|
|
146
|
+
? `${wantedSignIn ? 'set' : 'remove'} the emailed-code sign-in metadata of ${org.slug}'s test customers`
|
|
147
|
+
: `turn self-registration ${wantedSignUp === '1' ? 'on' : 'off'} for ${org.slug}'s test customers`);
|
|
148
|
+
}
|
|
149
|
+
/** One ZITADEL web client `<key>-work` per workspace with sign-in, planned once; its redirect addresses name the workspace host. */
|
|
150
|
+
export function planWorkspaceClients(org, byKey, claimed) {
|
|
151
|
+
const steps = [];
|
|
152
|
+
for (const entry of workspaceAuthEntries(org)) {
|
|
153
|
+
const client = workspaceClientKey(entry.key);
|
|
154
|
+
const key = resourceKey('zitadel', 'app', client);
|
|
155
|
+
if (byKey.has(key)) {
|
|
156
|
+
claimed.add(key);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
steps.push({
|
|
160
|
+
action: { system: 'zitadel', kind: 'app', key: client, desired: { appType: 'web' } },
|
|
161
|
+
step: {
|
|
162
|
+
description: `provision the workspace sign-in client '${client}' for ${org.slug}`,
|
|
163
|
+
destroys: false,
|
|
164
|
+
migrates: false,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return steps;
|
|
169
|
+
}
|
|
170
|
+
/** What a workspace broker entry must say, without its keys, or what is still missing. */
|
|
171
|
+
export function wantedWorkspaceBrokerEntry(org, entry, recorded) {
|
|
172
|
+
const auth = entry.uses?.auth;
|
|
173
|
+
if (!auth)
|
|
174
|
+
return { missing: [] };
|
|
175
|
+
const client = workspaceClientKey(entry.key);
|
|
176
|
+
const organizationId = auth.users === 'customers' ? recorded[testCustomersOrgKey(org.id)] : recorded.zitadel_org;
|
|
177
|
+
const clientId = recorded[entryClientIdScopeKey(client)];
|
|
178
|
+
const mailDomain = recorded.mail_domain;
|
|
179
|
+
const missing = [];
|
|
180
|
+
if (!organizationId) {
|
|
181
|
+
missing.push(auth.users === 'customers'
|
|
182
|
+
? { what: 'the test customers organisation id', producedBy: ['zitadel', 'customers-test'] }
|
|
183
|
+
: { what: "the org's ZITADEL org id", producedBy: ['zitadel', 'issuer'] });
|
|
184
|
+
}
|
|
185
|
+
if (!clientId)
|
|
186
|
+
missing.push({ what: 'its workspace sign-in client id', producedBy: ['zitadel', 'app'] });
|
|
187
|
+
if (!mailDomain)
|
|
188
|
+
missing.push({ what: 'the org mail domain', producedBy: ['coolify', 'mail'] });
|
|
189
|
+
const mcp = workspaceMcp(org, entry, recorded);
|
|
190
|
+
if (auth.mcp && !mcp) {
|
|
191
|
+
missing.push({ what: 'its MCP audience project id', producedBy: ['zitadel', 'mcp-project'] });
|
|
192
|
+
}
|
|
193
|
+
if (!organizationId || !clientId || !mailDomain || (auth.mcp && !mcp))
|
|
194
|
+
return { missing };
|
|
195
|
+
const host = workspaceHost(org.slug, entry.key);
|
|
196
|
+
return {
|
|
197
|
+
want: {
|
|
198
|
+
name: `${org.slug}-${client}`,
|
|
199
|
+
organizationId,
|
|
200
|
+
clientId,
|
|
201
|
+
origins: [`https://${host}`],
|
|
202
|
+
from: `${org.mail.noReplyLocalPart || 'noreply'}@${mailDomain}`,
|
|
203
|
+
registration: auth.registration === 'open',
|
|
204
|
+
replaces: null,
|
|
205
|
+
...(mcp ? { mcp } : {}),
|
|
206
|
+
...(auth.workspaces
|
|
207
|
+
? {
|
|
208
|
+
workspaces: {
|
|
209
|
+
roles: [...auth.workspaces.roles],
|
|
210
|
+
inviteUrl: `https://${host}${auth.workspaces.invitePath}`,
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
: {}),
|
|
214
|
+
...(auth.signIn ? { emailLink: auth.signIn === 'email-code' } : {}),
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* One broker `AUTH_APPS` entry per workspace with sign-in, written after the workspace step has put the stage's
|
|
220
|
+
* `AUTH_APP_KEY` in its env. Idempotent on the recorded marker of what the last write said.
|
|
221
|
+
*/
|
|
222
|
+
export function planWorkspaceBrokerEntries(org, recordedIds, plannedSteps, brokerWired, brokerWorkspacesDatabase, stageReady) {
|
|
223
|
+
const steps = [];
|
|
224
|
+
const blocked = [];
|
|
225
|
+
for (const entry of workspaceAuthEntries(org)) {
|
|
226
|
+
const auth = entry.uses?.auth;
|
|
227
|
+
if (!auth)
|
|
228
|
+
continue;
|
|
229
|
+
const at = `${org.slug}'s ${entry.key} workspace broker entry`;
|
|
230
|
+
const key = `coolify:workspace-broker-entry:${entry.key}`;
|
|
231
|
+
const block = (why) => blocked.push({ description: `${at} is blocked, not planned: ${why}`, key });
|
|
232
|
+
if (!brokerWired) {
|
|
233
|
+
block('the host has not wired the broker port');
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (auth.workspaces && !brokerWorkspacesDatabase) {
|
|
237
|
+
block(`the broker has no ${BROKER_WORKSPACES_DATABASE_ENV}; hold the connection string of its workspaces database at ${workspacesDatabaseRef(org)}, then apply again`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (!stageReady(entry)) {
|
|
241
|
+
block("the workspace's app has no sign-in key yet");
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const wanted = wantedWorkspaceBrokerEntry(org, entry, recordedIds);
|
|
245
|
+
if ('want' in wanted) {
|
|
246
|
+
if (recordedIds[brokerEntryAppliedScopeKey(workspaceClientKey(entry.key))] ===
|
|
247
|
+
JSON.stringify(wanted.want)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const unplanned = wanted.missing.filter(({ producedBy: [system, kind] }) => !plannedSteps.some((p) => p.action.system === system && p.action.kind === kind));
|
|
253
|
+
if (unplanned.length > 0) {
|
|
254
|
+
block(`${unplanned.map((m) => m.what).join(', ')} not recorded yet`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
steps.push({
|
|
259
|
+
action: {
|
|
260
|
+
system: 'coolify',
|
|
261
|
+
kind: WORKSPACE_BROKER_ENTRY_KIND,
|
|
262
|
+
key: appStepKey(org, entry.key),
|
|
263
|
+
desired: {},
|
|
264
|
+
},
|
|
265
|
+
step: {
|
|
266
|
+
description: `write ${at} beside the others`,
|
|
267
|
+
destroys: false,
|
|
268
|
+
migrates: false,
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return { steps, blocked };
|
|
273
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { HostingEntry } from '../org.js';
|
|
2
|
+
export declare function registerWorkspaceWorker(copy: HostingEntry, parentKey: string): void;
|
|
3
|
+
/** The key of the entry a workspace Worker copy belongs to, or `null` for a declared entry. */
|
|
4
|
+
export declare function workspaceWorkerParent(entry: HostingEntry): string | null;
|