@wtfalch/foundry 0.65.0 → 0.67.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 +5 -0
- package/dist/apps/deploy.js +33 -1
- package/dist/apps/migrations.d.ts +3 -0
- package/dist/apps/migrations.js +32 -7
- package/dist/catalogue/copy-en.js +5 -0
- package/dist/catalogue/integrations.js +5 -0
- package/dist/entry-rules.d.ts +11 -0
- package/dist/entry-rules.js +70 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.js +5 -2
- package/dist/migrations/0069_release_events.sql +16 -0
- package/dist/modules/analytics.d.ts +8 -0
- package/dist/modules/analytics.js +28 -0
- package/dist/modules/database.js +22 -3
- package/dist/modules/index.js +2 -0
- package/dist/native-build.d.ts +16 -12
- package/dist/native-build.js +16 -12
- package/dist/org.d.ts +9 -1
- package/dist/org.js +4 -1
- package/dist/plan/analytics-boule.d.ts +17 -0
- package/dist/plan/analytics-boule.js +43 -0
- package/dist/plan/analytics.d.ts +58 -0
- package/dist/plan/analytics.js +89 -0
- package/dist/plan/boule.js +2 -0
- package/dist/plan/company-backups.js +2 -0
- package/dist/plan/diff.d.ts +4 -0
- package/dist/plan/diff.js +103 -3
- package/dist/plan/entry-analytics-key.d.ts +35 -0
- package/dist/plan/entry-analytics-key.js +134 -0
- package/dist/plan/executor.d.ts +79 -6
- package/dist/plan/executor.js +379 -19
- package/dist/plan/hostnames.js +4 -0
- package/dist/plan/index.d.ts +2 -0
- package/dist/plan/index.js +1 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.js +3 -0
- package/dist/plan/login-host.d.ts +8 -0
- package/dist/plan/login-host.js +8 -0
- package/dist/plan/org-names.d.ts +3 -3
- package/dist/plan/org-names.js +3 -0
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/plan-apply.d.ts +12 -0
- package/dist/plan/plan-apply.js +40 -1
- package/dist/plan/platform-diff.d.ts +17 -1
- package/dist/plan/platform-diff.js +40 -2
- package/dist/plan/platform-org.d.ts +6 -0
- package/dist/plan/platform-providers.d.ts +8 -6
- package/dist/plan/platform-providers.js +56 -10
- package/dist/plan/providers-readers.js +16 -1
- package/dist/plan/release-host.d.ts +30 -1
- package/dist/plan/release-host.js +84 -41
- package/dist/plan/release-series-reader.d.ts +26 -0
- package/dist/plan/release-series-reader.js +40 -0
- package/dist/plan/sign-in-archon.d.ts +1 -1
- package/dist/plan/sign-in-archon.js +2 -2
- package/dist/plan/stage-services.d.ts +3 -1
- package/dist/plan/stage-services.js +2 -1
- package/dist/plan/uptime-worker.js +53 -3
- package/dist/plan/worker-app-database.d.ts +12 -1
- package/dist/plan/worker-app-database.js +13 -0
- package/dist/plan/worker-app.d.ts +27 -2
- package/dist/plan/worker-app.js +52 -7
- package/dist/plan/workspace.d.ts +2 -2
- package/dist/plan/workspace.js +3 -3
- package/dist/release/decide.d.ts +2 -0
- package/dist/release/decide.js +67 -10
- package/dist/release/events.d.ts +13 -0
- package/dist/release/events.js +67 -0
- package/dist/release/health.d.ts +5 -0
- package/dist/release/health.js +3 -3
- package/dist/release/run.d.ts +7 -2
- package/dist/release/run.js +8 -0
- package/dist/release/series.d.ts +58 -0
- package/dist/release/series.js +65 -0
- package/dist/release/state.d.ts +58 -1
- package/dist/release/state.js +45 -0
- package/dist/release/types.d.ts +44 -0
- package/dist/slug.d.ts +1 -1
- package/dist/slug.js +5 -2
- package/dist/store/postgres.js +29 -3
- package/dist/store/schema.d.ts +112 -0
- package/dist/store/schema.js +9 -0
- package/dist/worker-app-config.d.ts +22 -1
- package/dist/worker-app-config.js +73 -15
- package/dist/worker-app-migrations.d.ts +6 -2
- package/dist/worker-app-migrations.js +1 -0
- package/package.json +1 -1
package/dist/plan/executor.js
CHANGED
|
@@ -5,10 +5,12 @@ import { EdgeApiError } from '@wtfalch/edge';
|
|
|
5
5
|
import { coolifyImageFor } from '../bootstrap/versions.js';
|
|
6
6
|
import { redactSecrets } from '../calls-sfu-deploy-support.js';
|
|
7
7
|
import { resolveCallsSfuUrls } from '../calls-sfu.js';
|
|
8
|
-
import {
|
|
8
|
+
import { NATIVE_BUILD_KEY_ENV } from '../native-build.js';
|
|
9
9
|
import { AI_ENTRY_PERMISSIONS, APPS_DOMAIN, APP_ENTRY_KEY, FILES_ENTRY_PERMISSIONS, FULL_COMMIT_SHA, ORG_POSTGRES_SERVER_SCOPE_KEY, ORG_SERVICE_SCHEMAS, appsOn, companyGithubFor, filesCollectionEnvName, heldEnvRef, isLiveEntry, isMainEntry, kosmosOn, mainEntry, ownerMailboxRecorded, ownerMailboxes, ownsServices, sharedSecretRef, } from '../org.js';
|
|
10
10
|
import { adoptedKeysUnreadable, parseAdoptSnapshot } from './adopt.js';
|
|
11
11
|
import { AGORA_ACTIVATED_ENV_SCOPE_KEY, AGORA_APP_PORTS_EXPOSES, AGORA_CLIENT_ID_SCOPE_KEY, AGORA_DEPLOYED_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CALLS_INTROSPECTION_SCOPE_KEY, CALLS_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CALLS_SERVICE_DEPLOYED_SCOPE_KEY, CALLS_SERVICE_NAME_SCOPE_KEY, CALLS_SERVICE_PORT, CALLS_SERVICE_TOKEN_ENV_SCOPE_KEY, CHAT_CALLS_APPLIED_SCOPE_KEY, CHAT_CALLS_TOKEN_SCOPE_KEY, CHAT_CALLS_TOKEN_UNDELIVERED_SCOPE_KEY, CHAT_INTROSPECTION_SCOPE_KEY, CHAT_INTROSPECTION_UNDELIVERED_SCOPE_KEY, CHAT_SERVICE_URL_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, DESKTOP_AUTH_PORT, ENTRY_CLIENT_ID_SCOPE_PREFIX, ZITADEL_MESSAGING_PROJECT_SCOPE_KEY, ZITADEL_ORG_SCOPE_KEY, agoraEnvDrift, agoraEnvRevision, agoraHostnameFor, callsIntrospectionAppName, callsServiceHasName, callsServiceInternalName, callsTokenEnvMarker, callsTokenEnvMovedMessage, chatCallsApplied, chatHasUpstreams, chatIntrospectionAppName, desiredAgoraEnv, desktopAuthInternalName, entryClientIdScopeKey, } from './agora.js';
|
|
12
|
+
import { ANALYTICS_BOULE_APPLIED_SCOPE_KEY, ANALYTICS_BOULE_ENV_NAMES, analyticsBouleMarker, } from './analytics-boule.js';
|
|
13
|
+
import { ANALYTICS_DATABASE_SCOPE_KEY, ANALYTICS_DEPLOYED_SCOPE_KEY, ANALYTICS_OPERATOR_KEY_ENV, analyticsEnvDrift, analyticsHostnameFor, analyticsUrlFor, desiredAnalyticsEnv, } from './analytics.js';
|
|
12
14
|
import { APPS_DATABASE_ADMIN_URL_ENV, applyAppsDbRoute, retireAppsDbRoute, } from './apps-db-route.js';
|
|
13
15
|
import { APPS_ZONES_APPLIED_SCOPE_KEY, applyAppsZones, retireAppsZones } from './apps-zones.js';
|
|
14
16
|
import { applyBlueGreenConnectors, applyBlueGreenServer, applyBlueGreenWeights, applyWorkspaceServer, } from './blue-green-apply.js';
|
|
@@ -38,6 +40,7 @@ import { createEdgeRateLimits, } from './edge-ratelimits.js';
|
|
|
38
40
|
import { createEdgeTunnels } from './edge-tunnels.js';
|
|
39
41
|
import { createEdgeZones } from './edge-zones.js';
|
|
40
42
|
import { AI_ENTRY_KEY_DAYS, AI_ENTRY_RENEW_WINDOW_DAYS, entryAiAppliedScopeKey, entryAiClampedScopeKey, entryAiExpiresScopeKey, entryAiPendingExpiresScopeKey, entryAiRetiredScopeKey, entryAiUndeliveredScopeKey, entryAiUnrevokedScopeKey, entryAiWebhookPendingScopeKey, entryAiWrittenScopeKey, parseIdList, } from './entry-ai-key.js';
|
|
43
|
+
import { ENTRY_ANALYTICS_KEY_DELIVERY_ATTEMPTS, entryAnalyticsAppliedScopeKey, entryAnalyticsRetiredScopeKey, entryAnalyticsUndeliveredScopeKey, stageAnalyticsKeyName, } from './entry-analytics-key.js';
|
|
41
44
|
import { applyEntryAudit } from './entry-audit.js';
|
|
42
45
|
import { CUSTOMER_GRANTS_CREDENTIAL_ENV, CUSTOMER_GRANTS_ENV, CUSTOMER_GRANTS_WRITTEN_SCOPE_KEY, customerGrantsEntries, customerGrantsJson, customerGrantsMintedScopeKey, customerGrantsRef, customerGrantsToDrop, ensureCustomerGrantsCredential, wantedCustomerGrants, } from './entry-customer-grants.js';
|
|
43
46
|
import { BOULE_EDGE_ENTRY_KEY, EDGE_ENTRY_KEY_DAYS, EDGE_RESERVED_LABELS_APPLIED_SCOPE_KEY, EDGE_SLUG_APPLIED_SCOPE_KEY, ENTRY_EDGE_KEY_DELIVERY_ATTEMPTS, bouleEdgeKeyWanted, edgeKeyAppRecordKey, edgeKeyDomain, entryEdgeMintedAtScopeKey, entryEdgeRetiredScopeKey, entryEdgeUndeliveredScopeKey, } from './entry-edge-key.js';
|
|
@@ -52,7 +55,7 @@ import { PROXY_LABEL_TOKEN_REF } from './held-key-refs.js';
|
|
|
52
55
|
import { appHostname, cmsStudioHostnameFor, coolifyTunnelDomains, entryHostnames, siteHostnameFor, tunnelDnsHostnames, tunnelIngressHostnames, workspaceHost, } from './hostnames.js';
|
|
53
56
|
import { KEYS_REGISTRAR_DELIVERY_ATTEMPTS, KEYS_REGISTRAR_KEY_DAYS, KEYS_REGISTRAR_KEY_ENV, KEYS_REGISTRAR_MINTED_AT_SCOPE_KEY, KEYS_REGISTRAR_MINT_STARTED_SCOPE_KEY, KEYS_REGISTRAR_SCOPE_KEY, KEYS_REGISTRAR_UNDELIVERED_SCOPE_KEY, KEYS_REGISTRAR_URL_APPLIED_SCOPE_KEY, KEYS_REGISTRAR_URL_ENV, } from './keys-registrar.js';
|
|
54
57
|
import { KOSMOS_CLIENT_ID_SCOPE_KEY, KOSMOS_DATABASE_SCOPE_KEY, KOSMOS_DEPLOYED_SCOPE_KEY, desiredKosmosEnv, kosmosEnvDrift, kosmosHostnameFor, } from './kosmos.js';
|
|
55
|
-
import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, clientOrganizationIds, clientOrganizationsState, customersClientOrganizations, customersEntryKeys, loginBindingState, loginHostClientIds, ownAuthApplied, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
|
|
58
|
+
import { LOGIN_BASE_URI_SWEEP_SCOPE_KEY, LOGIN_BASE_URI_SWEEP_VERSION, LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, clientOrganizationIds, clientOrganizationsState, customersClientOrganizations, customersEntryKeys, loginBindingState, loginHostClientIds, ownAuthApplied, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
|
|
56
59
|
import { machineBypassScopeKey } from './machine-paths.js';
|
|
57
60
|
import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
|
|
58
61
|
import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_SCOPE_KEY, isOwnMailSmtpPort, normalizeMailTarget, } from './mail-relay.js';
|
|
@@ -404,6 +407,10 @@ export const BOULE_PORT = 3000;
|
|
|
404
407
|
export const KOSMOS_REPO = 'wtfalch/kosmos';
|
|
405
408
|
export const KOSMOS_APP_DOCKERFILE = '/Dockerfile';
|
|
406
409
|
export const KOSMOS_PORT = 3000;
|
|
410
|
+
/** The company's analytics backend (https://github.com/wtfalch/analytics): root Dockerfile, serves on 3000, migrates in its own start script. */
|
|
411
|
+
export const ANALYTICS_REPO = 'wtfalch/analytics';
|
|
412
|
+
export const ANALYTICS_APP_DOCKERFILE = '/Dockerfile';
|
|
413
|
+
export const ANALYTICS_PORT = 3000;
|
|
407
414
|
/**
|
|
408
415
|
* Every app foundry deploys keeps its own logs across a redeploy, so a
|
|
409
416
|
* database outage does not also erase the only other copy of the evidence
|
|
@@ -969,6 +976,8 @@ function limitProfileOf(action) {
|
|
|
969
976
|
return LIMITS.boule;
|
|
970
977
|
case 'coolify:kosmos':
|
|
971
978
|
return LIMITS.kosmos;
|
|
979
|
+
case 'coolify:analytics':
|
|
980
|
+
return LIMITS.analytics;
|
|
972
981
|
case 'coolify:broker-registrar':
|
|
973
982
|
return LIMITS.brokerEntry;
|
|
974
983
|
case 'coolify:keys-registrar':
|
|
@@ -1404,6 +1413,12 @@ export class FoundryStepExecutor {
|
|
|
1404
1413
|
return this.bouleBrokerEntry(org, context);
|
|
1405
1414
|
case 'coolify:kosmos':
|
|
1406
1415
|
return this.coolifyKosmos(action, org, context);
|
|
1416
|
+
case 'coolify:analytics':
|
|
1417
|
+
return this.coolifyAnalytics(action, org, context);
|
|
1418
|
+
case 'coolify:analytics-boule':
|
|
1419
|
+
return this.analyticsBoule(action, org, context);
|
|
1420
|
+
case 'analytics:key':
|
|
1421
|
+
return this.analyticsEntryKey(action, org, context);
|
|
1407
1422
|
case 'coolify:broker-registrar':
|
|
1408
1423
|
return this.brokerRegistrar(org, context);
|
|
1409
1424
|
case 'coolify:keys-registrar':
|
|
@@ -1755,6 +1770,7 @@ export class FoundryStepExecutor {
|
|
|
1755
1770
|
: {}),
|
|
1756
1771
|
zones: this.zonePort(),
|
|
1757
1772
|
dns: this.dnsPort(),
|
|
1773
|
+
secret: (value) => void this.handledSecrets.add(value),
|
|
1758
1774
|
...(this.ports.heldKeys ? { heldKeys: this.ports.heldKeys } : {}),
|
|
1759
1775
|
};
|
|
1760
1776
|
}
|
|
@@ -2354,6 +2370,7 @@ export class FoundryStepExecutor {
|
|
|
2354
2370
|
? loginHostClientIds(org, recorded, activePairs.map((pair) => pair.clientId).filter((id) => !movedIds.has(id)))
|
|
2355
2371
|
: clientIds;
|
|
2356
2372
|
const cookieDomain = inAppCookieDomain(action.desired.cookieDomain);
|
|
2373
|
+
let swept = false;
|
|
2357
2374
|
const writeCompanyBinding = async (ids) => {
|
|
2358
2375
|
if (ids.length > 0 || activePairs.length === 0) {
|
|
2359
2376
|
await bindings.upsertBinding({ host, organizationId: zitadelOrgId, clientIds: ids });
|
|
@@ -2428,8 +2445,10 @@ export class FoundryStepExecutor {
|
|
|
2428
2445
|
await writeClientOrgs();
|
|
2429
2446
|
// One write for every upsert above (one redeploy of the login), before any app is repointed.
|
|
2430
2447
|
await bindings.flush?.();
|
|
2431
|
-
|
|
2432
|
-
|
|
2448
|
+
// In-app sign-in recorded or wanted: `required` is off, so every app needs a base URI (marker below).
|
|
2449
|
+
if (own && (activeHosts.size > 0 || recorded[SIGN_IN_IN_APP_SCOPE_KEY])) {
|
|
2450
|
+
await allowPerAppLoginBaseUris(this.ports.zitadel, `https://${host}/`);
|
|
2451
|
+
swept = Boolean(this.ports.zitadel.allowPerAppLoginBaseUri);
|
|
2433
2452
|
}
|
|
2434
2453
|
}
|
|
2435
2454
|
// The base URI each app's sign-in starts from: its own address under `/sign-in/` in-app, else the login host.
|
|
@@ -2643,10 +2662,23 @@ export class FoundryStepExecutor {
|
|
|
2643
2662
|
if (own) {
|
|
2644
2663
|
// Written last: a failed base URI write leaves it behind and the step is planned again.
|
|
2645
2664
|
context.recordScopeId(OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, ownCustomersSet.sort().join(','));
|
|
2665
|
+
if (swept)
|
|
2666
|
+
context.recordScopeId(LOGIN_BASE_URI_SWEEP_SCOPE_KEY, LOGIN_BASE_URI_SWEEP_VERSION);
|
|
2646
2667
|
}
|
|
2647
2668
|
context.recordScopeId(LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, pendingRemovalState(stillPending));
|
|
2648
2669
|
return { resourceId: host };
|
|
2649
2670
|
}
|
|
2671
|
+
/**
|
|
2672
|
+
* `{ loginBaseUri }` for an app made in the org's ZITADEL: the instance's own login host (a company's own
|
|
2673
|
+
* `auth.` host, else the shared one). Nothing when the shared host is not known. See
|
|
2674
|
+
* {@link FoundryStepExecutorPorts}' `createOidcApp` `loginBaseUri`.
|
|
2675
|
+
*/
|
|
2676
|
+
async instanceLoginBaseUriFor(org) {
|
|
2677
|
+
if (ownsServices(org))
|
|
2678
|
+
return { loginBaseUri: `https://${ownHostnames(org).auth}/` };
|
|
2679
|
+
const shared = await this.ports.platformOrg?.authHostname?.();
|
|
2680
|
+
return shared ? { loginBaseUri: `https://${shared}/` } : {};
|
|
2681
|
+
}
|
|
2650
2682
|
/**
|
|
2651
2683
|
* The host table of a company's own in-app login app as a bindings port that collects its edits and
|
|
2652
2684
|
* writes them once on `flush` (one redeploy, none when nothing changed). Used for a company that runs
|
|
@@ -2931,6 +2963,7 @@ export class FoundryStepExecutor {
|
|
|
2931
2963
|
: [...signInUris, ...(authEntry?.uses?.auth?.mcp ? [mcpCallbackUri(org)] : [])],
|
|
2932
2964
|
postLogoutRedirectUris: postLogoutUris,
|
|
2933
2965
|
...(authEntry?.uses?.auth?.mcp ? { accessTokenType: 'jwt' } : {}),
|
|
2966
|
+
...(await this.instanceLoginBaseUriFor(org)),
|
|
2934
2967
|
});
|
|
2935
2968
|
if (appType === 'web') {
|
|
2936
2969
|
// `coolifyApp`'s own `pushAppEnv` reads `APP_CLIENT_ID_SCOPE_KEY` to
|
|
@@ -3813,8 +3846,34 @@ export class FoundryStepExecutor {
|
|
|
3813
3846
|
: { id: registered.id, name: projectName };
|
|
3814
3847
|
},
|
|
3815
3848
|
};
|
|
3849
|
+
const analytics = {
|
|
3850
|
+
held: held('analytics'),
|
|
3851
|
+
available: async () => canHold &&
|
|
3852
|
+
Boolean(this.ports.analytics) &&
|
|
3853
|
+
Boolean(context.recordedIds()[`coolify:analytics:${org.id}`]) &&
|
|
3854
|
+
entry?.uses?.analytics === true,
|
|
3855
|
+
create: async () => {
|
|
3856
|
+
const port = this.ports.analytics;
|
|
3857
|
+
if (!port)
|
|
3858
|
+
throw new Error('no analytics port');
|
|
3859
|
+
const operatorKey = await this.analyticsOperatorKey(org, context);
|
|
3860
|
+
const name = stageAnalyticsKeyName(org, subject.id);
|
|
3861
|
+
const minted = await port.mintSiteKey({
|
|
3862
|
+
baseUrl: analyticsUrlFor(org),
|
|
3863
|
+
operatorKey,
|
|
3864
|
+
name,
|
|
3865
|
+
environment: 'stage',
|
|
3866
|
+
});
|
|
3867
|
+
return {
|
|
3868
|
+
id: minted.id,
|
|
3869
|
+
name,
|
|
3870
|
+
keyIds: [minted.id],
|
|
3871
|
+
secrets: [{ env: 'ANALYTICS_SITE_KEY', value: minted.secret }],
|
|
3872
|
+
};
|
|
3873
|
+
},
|
|
3874
|
+
};
|
|
3816
3875
|
const prodJobsProjectId = context.recordedIds()[`jobs:registration:${org.id}`];
|
|
3817
|
-
return { files, ai, jobs, ...(prodJobsProjectId ? { prodJobsProjectId } : {}) };
|
|
3876
|
+
return { files, ai, jobs, analytics, ...(prodJobsProjectId ? { prodJobsProjectId } : {}) };
|
|
3818
3877
|
}
|
|
3819
3878
|
/**
|
|
3820
3879
|
* Stage's own sign-in (`plan/stage-signin.ts`): a client named `<production client>-stage` on production's
|
|
@@ -3869,6 +3928,7 @@ export class FoundryStepExecutor {
|
|
|
3869
3928
|
appType: 'web',
|
|
3870
3929
|
redirectUris: spec.redirectUris,
|
|
3871
3930
|
postLogoutRedirectUris: spec.postLogoutRedirectUris,
|
|
3931
|
+
...(await this.instanceLoginBaseUriFor(org)),
|
|
3872
3932
|
});
|
|
3873
3933
|
},
|
|
3874
3934
|
};
|
|
@@ -3921,6 +3981,7 @@ export class FoundryStepExecutor {
|
|
|
3921
3981
|
appType: 'web',
|
|
3922
3982
|
redirectUris: spec.redirectUris,
|
|
3923
3983
|
postLogoutRedirectUris: spec.postLogoutRedirectUris,
|
|
3984
|
+
...(await this.instanceLoginBaseUriFor(org)),
|
|
3924
3985
|
});
|
|
3925
3986
|
},
|
|
3926
3987
|
writeBrokerEntry: async (entryWant, appKey) => {
|
|
@@ -5203,13 +5264,28 @@ export class FoundryStepExecutor {
|
|
|
5203
5264
|
return already ? null : randomBytes(bytes).toString('base64url');
|
|
5204
5265
|
}
|
|
5205
5266
|
/**
|
|
5206
|
-
*
|
|
5207
|
-
*
|
|
5208
|
-
*
|
|
5209
|
-
*
|
|
5267
|
+
* foundry#987: nothing reads `NATIVE_BUILD_KEY` any more -- Boule calls Archon's org build
|
|
5268
|
+
* route (`/api/orgs/{orgId}/native/builds`) with its own org credential instead
|
|
5269
|
+
* (`BOULE_ARCHON_CREDENTIAL_ENV`, archon#706, boule#432). This takes the env row away from
|
|
5270
|
+
* every Boule that still has one from #970/#971; `NATIVE_BUILD_KEY_ENV` and
|
|
5271
|
+
* `NATIVE_BUILD_KEY_BYTES` are deprecated and go in the next release (the two-release removal
|
|
5272
|
+
* rule), once no live Boule holds the row any more.
|
|
5210
5273
|
*/
|
|
5211
|
-
async
|
|
5212
|
-
|
|
5274
|
+
async dropNativeBuildKeyEnv(appId, liveRows) {
|
|
5275
|
+
let changed = false;
|
|
5276
|
+
for (const row of liveRows) {
|
|
5277
|
+
if (row.key !== NATIVE_BUILD_KEY_ENV || !row.uuid)
|
|
5278
|
+
continue;
|
|
5279
|
+
try {
|
|
5280
|
+
await this.ports.coolify.deleteEnv(appId, row.uuid);
|
|
5281
|
+
changed = true;
|
|
5282
|
+
}
|
|
5283
|
+
catch (error) {
|
|
5284
|
+
if (!isNotFoundError(error))
|
|
5285
|
+
throw error;
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
return changed;
|
|
5213
5289
|
}
|
|
5214
5290
|
/**
|
|
5215
5291
|
* `null` once the app already has one -- `AUTH_COOKIE_SECRET` is generated
|
|
@@ -7703,12 +7779,10 @@ export class FoundryStepExecutor {
|
|
|
7703
7779
|
this.handledSecrets.add(ownerSeedSecret);
|
|
7704
7780
|
envs.push({ key: BOULE_OWNER_SEED_SECRET_ENV, value: ownerSeedSecret });
|
|
7705
7781
|
}
|
|
7706
|
-
//
|
|
7707
|
-
|
|
7708
|
-
if (
|
|
7709
|
-
|
|
7710
|
-
envs.push({ key: NATIVE_BUILD_KEY_ENV, value: nativeBuildKey });
|
|
7711
|
-
}
|
|
7782
|
+
// foundry#987: Boule calls Archon's org build route with its own org credential now, so
|
|
7783
|
+
// nothing reads this any more -- take the row away from every Boule that still has one.
|
|
7784
|
+
if (await this.dropNativeBuildKeyEnv(app.appId, liveRows))
|
|
7785
|
+
envChanged = true;
|
|
7712
7786
|
// Held, minted once (C3). Compared with the live value so a changed one restarts the app.
|
|
7713
7787
|
const credentialEnvs = [];
|
|
7714
7788
|
let archonPushedAt = null;
|
|
@@ -7769,7 +7843,6 @@ export class FoundryStepExecutor {
|
|
|
7769
7843
|
peerPassword,
|
|
7770
7844
|
bridgeSecret,
|
|
7771
7845
|
ownerSeedSecret,
|
|
7772
|
-
nativeBuildKey,
|
|
7773
7846
|
...credentialEnvs.map((credential) => credential.value),
|
|
7774
7847
|
...grants.secrets,
|
|
7775
7848
|
].filter((v) => v !== null);
|
|
@@ -7809,7 +7882,6 @@ export class FoundryStepExecutor {
|
|
|
7809
7882
|
peerPassword !== null ||
|
|
7810
7883
|
bridgeSecret !== null ||
|
|
7811
7884
|
ownerSeedSecret !== null ||
|
|
7812
|
-
nativeBuildKey !== null ||
|
|
7813
7885
|
secretsNotRestarted ||
|
|
7814
7886
|
registrarUnstarted ||
|
|
7815
7887
|
dbPush.changed ||
|
|
@@ -7828,6 +7900,254 @@ export class FoundryStepExecutor {
|
|
|
7828
7900
|
this.recordSignInEnvLive(context, 'boule', bouleHostnameFor(org), env);
|
|
7829
7901
|
return { resourceId: app.appId };
|
|
7830
7902
|
}
|
|
7903
|
+
// ---- analytics backend ----------------------------------------------------
|
|
7904
|
+
/**
|
|
7905
|
+
* `plan/diff.ts`'s `planAnalyticsBackend`: the company's analytics backend as one more Coolify application
|
|
7906
|
+
* from https://github.com/wtfalch/analytics's root Dockerfile ({@link ANALYTICS_REPO}), with a database of its
|
|
7907
|
+
* own, shaped like {@link coolifyKosmos}. Its start script migrates as the owner role and then serves as the
|
|
7908
|
+
* runtime role, so `pushDatabaseUrl` gives it both URLs. It needs no sign-in client: it reads only
|
|
7909
|
+
* `AUTH_ORGANIZATION_ID` (the tenant id derives from it), `APP_URL` and the operator key.
|
|
7910
|
+
*
|
|
7911
|
+
* `ANALYTICS_OPERATOR_KEY` is made once here (`mintIfAbsent`) and written only to this app's env. No outside
|
|
7912
|
+
* service shows it once, so no undelivered marker is needed: a failed write leaves nothing live to mint twice.
|
|
7913
|
+
* Steps that call the backend's operator API read it back from this env.
|
|
7914
|
+
*
|
|
7915
|
+
* A brand new app is deployed once ({@link ANALYTICS_DEPLOYED_SCOPE_KEY}), as for Boule (#504).
|
|
7916
|
+
*/
|
|
7917
|
+
async coolifyAnalytics(_action, org, context) {
|
|
7918
|
+
const { env, missing } = desiredAnalyticsEnv(org, context.recordedIds());
|
|
7919
|
+
if (missing.length > 0) {
|
|
7920
|
+
throw new Error(`the analytics backend for ${org.slug} cannot be configured: missing ${missing.join(', ')}. Nothing was written.`);
|
|
7921
|
+
}
|
|
7922
|
+
const projectUuid = await this.ensureCoolifyProject(org, context);
|
|
7923
|
+
const { environment } = await this.ports.coolify.findOrCreateEnvironment(projectUuid, 'production');
|
|
7924
|
+
const name = `${org.slug}-analytics`;
|
|
7925
|
+
const dbName = `${org.slug}-analytics-db`.replace(/-/g, '_');
|
|
7926
|
+
const dbId = await this.ensureCallsDatabase(org, context, projectUuid, environment, `${org.slug}-analytics-db`, dbName, ANALYTICS_DATABASE_SCOPE_KEY);
|
|
7927
|
+
context.recordScopeId(ANALYTICS_DATABASE_SCOPE_KEY, dbId);
|
|
7928
|
+
const server = await this.resolveServer(org, context);
|
|
7929
|
+
const githubApp = await this.ports.coolify.requireGithubApp();
|
|
7930
|
+
const app = await this.findOrCreateCallsApp(name, environment.id, () => this.ports.coolify.createGithubAppApplication({
|
|
7931
|
+
is_preview_deployments_enabled: false,
|
|
7932
|
+
project_uuid: projectUuid,
|
|
7933
|
+
server_uuid: server.uuid,
|
|
7934
|
+
environment_name: environment.name,
|
|
7935
|
+
environment_uuid: environment.uuid,
|
|
7936
|
+
github_app_uuid: githubApp.uuid,
|
|
7937
|
+
git_repository: ANALYTICS_REPO,
|
|
7938
|
+
git_branch: 'main',
|
|
7939
|
+
build_pack: 'dockerfile',
|
|
7940
|
+
name,
|
|
7941
|
+
domains: coolifyTunnelDomains([analyticsHostnameFor(org)]),
|
|
7942
|
+
dockerfile_location: ANALYTICS_APP_DOCKERFILE,
|
|
7943
|
+
}));
|
|
7944
|
+
await this.ensureLogVolume(app.appId, org, app.existed);
|
|
7945
|
+
await this.ports.coolify.updateApplication(app.appId, {
|
|
7946
|
+
...healthCheckSettings('/api/health', ANALYTICS_PORT),
|
|
7947
|
+
...(await this.routedLabels(name, analyticsHostnameFor(org), ANALYTICS_PORT)),
|
|
7948
|
+
});
|
|
7949
|
+
const liveEnv = Object.fromEntries((await this.ports.coolify.listEnvs(app.appId))
|
|
7950
|
+
.filter((e) => !e.is_preview)
|
|
7951
|
+
.map((e) => [e.key, e.value]));
|
|
7952
|
+
const envChanged = analyticsEnvDrift(env, liveEnv).length > 0;
|
|
7953
|
+
const envs = Object.entries(env).map(([key, value]) => ({ key, value }));
|
|
7954
|
+
const operatorKey = await this.mintIfAbsent(app.appId, ANALYTICS_OPERATOR_KEY_ENV, 32);
|
|
7955
|
+
if (operatorKey) {
|
|
7956
|
+
this.handledSecrets.add(operatorKey);
|
|
7957
|
+
envs.push({ key: ANALYTICS_OPERATOR_KEY_ENV, value: operatorKey });
|
|
7958
|
+
}
|
|
7959
|
+
try {
|
|
7960
|
+
await this.ports.coolify.bulkUpsertEnvs(app.appId, envs);
|
|
7961
|
+
}
|
|
7962
|
+
catch (error) {
|
|
7963
|
+
if (!operatorKey)
|
|
7964
|
+
throw error;
|
|
7965
|
+
throw new Error(redactSecrets(error instanceof Error ? error.message : String(error), [operatorKey]));
|
|
7966
|
+
}
|
|
7967
|
+
const dbPush = await this.pushDatabaseUrl(app.appId, dbId, dbName);
|
|
7968
|
+
if (!dbPush.pushed) {
|
|
7969
|
+
throw new Error(`the analytics backend for ${org.slug}: its database has no internal_db_url yet -- retry this apply once Coolify reports one`);
|
|
7970
|
+
}
|
|
7971
|
+
if (!context.recordedIds()[ANALYTICS_DEPLOYED_SCOPE_KEY]) {
|
|
7972
|
+
const runtime = this.ports.runtime ?? {};
|
|
7973
|
+
const built = await waitForNewDeployment(this.ports.coolify, app.appId, publicHealthCheck(org, context.recordedIds(), analyticsHostnameFor(org), '/api/health'), runtime, runtime.sleep ?? defaultSleep, 'coolify:analytics', `${org.slug}'s analytics backend stays at its previous deploy`);
|
|
7974
|
+
await recordBuiltCommit(this.ports.coolify, { appId: app.appId, deploymentUuid: built.deploymentUuid, name: 'analytics' }, (key, value) => context.recordScopeId(key, value));
|
|
7975
|
+
context.recordScopeId(ANALYTICS_DEPLOYED_SCOPE_KEY, '1');
|
|
7976
|
+
}
|
|
7977
|
+
else if (app.existed && (envChanged || operatorKey !== null || dbPush.changed)) {
|
|
7978
|
+
await this.ports.coolify.restart(app.appId);
|
|
7979
|
+
}
|
|
7980
|
+
return { resourceId: app.appId };
|
|
7981
|
+
}
|
|
7982
|
+
/** The backend's operator key, read from its own env. Fixed text when the backend or the key is not there. */
|
|
7983
|
+
async analyticsOperatorKey(org, context) {
|
|
7984
|
+
const appId = context.recordedIds()[`coolify:analytics:${org.id}`];
|
|
7985
|
+
if (!appId)
|
|
7986
|
+
throw new Error(`the analytics backend for ${org.slug} is not recorded yet`);
|
|
7987
|
+
const rows = await this.ports.coolify.listEnvs(appId);
|
|
7988
|
+
const key = rows.find((e) => e.key === ANALYTICS_OPERATOR_KEY_ENV && !e.is_preview)?.value;
|
|
7989
|
+
if (!key)
|
|
7990
|
+
throw new Error(`the analytics backend for ${org.slug} has no operator key yet`);
|
|
7991
|
+
this.handledSecrets.add(key);
|
|
7992
|
+
return key;
|
|
7993
|
+
}
|
|
7994
|
+
/**
|
|
7995
|
+
* C2 `coolify:analytics-boule` (`plan/analytics-boule.ts`): the backend's address and operator key into
|
|
7996
|
+
* Boule's env, then Boule restarts. `analytics_boule_applied` is recorded last. Retire takes both names away.
|
|
7997
|
+
*/
|
|
7998
|
+
async analyticsBoule(action, org, context) {
|
|
7999
|
+
const at = `coolify:analytics-boule for ${org.slug}`;
|
|
8000
|
+
const recorded = context.recordedIds();
|
|
8001
|
+
const bouleId = recorded[`coolify:boule:${org.id}`];
|
|
8002
|
+
if (!bouleId)
|
|
8003
|
+
throw new Error(`${at}: Boule is not recorded yet`);
|
|
8004
|
+
if (action.desired.mode === 'retire') {
|
|
8005
|
+
const rows = await this.ports.coolify.listEnvs(bouleId);
|
|
8006
|
+
for (const name of ANALYTICS_BOULE_ENV_NAMES) {
|
|
8007
|
+
const row = rows.find((e) => e.key === name && !e.is_preview);
|
|
8008
|
+
if (row?.uuid) {
|
|
8009
|
+
try {
|
|
8010
|
+
await this.ports.coolify.deleteEnv(bouleId, row.uuid);
|
|
8011
|
+
}
|
|
8012
|
+
catch (error) {
|
|
8013
|
+
if (!isNotFoundError(error))
|
|
8014
|
+
throw error;
|
|
8015
|
+
}
|
|
8016
|
+
}
|
|
8017
|
+
}
|
|
8018
|
+
await this.ports.coolify.restart(bouleId);
|
|
8019
|
+
context.recordScopeId(ANALYTICS_BOULE_APPLIED_SCOPE_KEY, '');
|
|
8020
|
+
return {};
|
|
8021
|
+
}
|
|
8022
|
+
if (!appsOn(org))
|
|
8023
|
+
throw new Error(`${at}: the company's Boule runs no apps (org.boule.apps)`);
|
|
8024
|
+
const backendId = recorded[`coolify:analytics:${org.id}`];
|
|
8025
|
+
if (!backendId)
|
|
8026
|
+
throw new Error(`${at}: the analytics backend is not recorded yet`);
|
|
8027
|
+
const operatorKey = await this.analyticsOperatorKey(org, context);
|
|
8028
|
+
try {
|
|
8029
|
+
await this.ports.coolify.bulkUpsertEnvs(bouleId, [
|
|
8030
|
+
{ key: 'ANALYTICS_URL', value: analyticsUrlFor(org), literal: true },
|
|
8031
|
+
{ key: ANALYTICS_OPERATOR_KEY_ENV, value: operatorKey, literal: true },
|
|
8032
|
+
]);
|
|
8033
|
+
await this.ports.coolify.restart(bouleId);
|
|
8034
|
+
}
|
|
8035
|
+
catch (error) {
|
|
8036
|
+
throw new Error(redactSecrets(error instanceof Error ? error.message : String(error), [operatorKey]));
|
|
8037
|
+
}
|
|
8038
|
+
context.recordScopeId(ANALYTICS_BOULE_APPLIED_SCOPE_KEY, analyticsBouleMarker(org, backendId));
|
|
8039
|
+
return {};
|
|
8040
|
+
}
|
|
8041
|
+
/**
|
|
8042
|
+
* `plan/entry-analytics-key.ts`: one server entry's site key (CLAUDE.md "Platform steps" rule 1). Mint: the
|
|
8043
|
+
* key's id is recorded as undelivered and persisted before anything is written; the env write is tried 3
|
|
8044
|
+
* times with the same secret; the applied address and the cleared marker come last; every try failing leaves
|
|
8045
|
+
* the marker, and the error names the id only. `url` rewrites the address. Retire revokes the key and the
|
|
8046
|
+
* env names. The operator key is read from the backend's env and never recorded.
|
|
8047
|
+
*/
|
|
8048
|
+
async analyticsEntryKey(action, org, context) {
|
|
8049
|
+
const entryKey = action.key.slice(org.id.length + 1);
|
|
8050
|
+
const at = `analytics:key for ${org.slug} entry '${entryKey}'`;
|
|
8051
|
+
const analytics = this.ports.analytics;
|
|
8052
|
+
if (!analytics)
|
|
8053
|
+
throw new Error(`${at}: this executor was built without an analytics port`);
|
|
8054
|
+
const desired = action.desired;
|
|
8055
|
+
const recorded = context.recordedIds();
|
|
8056
|
+
const appId = recorded[`coolify:app:${action.key}`];
|
|
8057
|
+
const baseUrl = analyticsUrlFor(org);
|
|
8058
|
+
const tolerate = async (call, gone) => {
|
|
8059
|
+
try {
|
|
8060
|
+
return await call();
|
|
8061
|
+
}
|
|
8062
|
+
catch (error) {
|
|
8063
|
+
if (!isNotFoundError(error))
|
|
8064
|
+
throw error;
|
|
8065
|
+
return gone;
|
|
8066
|
+
}
|
|
8067
|
+
};
|
|
8068
|
+
if (desired.mode === 'retire') {
|
|
8069
|
+
const mainId = recorded[`analytics:key:${action.key}`];
|
|
8070
|
+
const undelivered = recorded[entryAnalyticsUndeliveredScopeKey(entryKey)];
|
|
8071
|
+
const ids = [...new Set([mainId, undelivered].filter((id) => Boolean(id)))];
|
|
8072
|
+
if (ids.length > 0) {
|
|
8073
|
+
const operatorKey = await this.analyticsOperatorKey(org, context);
|
|
8074
|
+
for (const keyId of ids) {
|
|
8075
|
+
await tolerate(() => analytics.revokeSiteKey({ baseUrl, operatorKey, keyId }), undefined);
|
|
8076
|
+
}
|
|
8077
|
+
}
|
|
8078
|
+
if (appId) {
|
|
8079
|
+
const rows = await tolerate(() => this.ports.coolify.listEnvs(appId), []);
|
|
8080
|
+
let removed = false;
|
|
8081
|
+
for (const name of ['ANALYTICS_URL', 'ANALYTICS_SITE_KEY']) {
|
|
8082
|
+
const row = rows.find((e) => e.key === name && !e.is_preview);
|
|
8083
|
+
if (row?.uuid) {
|
|
8084
|
+
const uuid = row.uuid;
|
|
8085
|
+
await tolerate(() => this.ports.coolify.deleteEnv(appId, uuid), undefined);
|
|
8086
|
+
removed = true;
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
8089
|
+
if (removed)
|
|
8090
|
+
await tolerate(() => this.ports.coolify.restart(appId), undefined);
|
|
8091
|
+
}
|
|
8092
|
+
context.recordScopeId(entryAnalyticsUndeliveredScopeKey(entryKey), '');
|
|
8093
|
+
context.recordScopeId(entryAnalyticsAppliedScopeKey(entryKey), '');
|
|
8094
|
+
if (mainId ?? undelivered) {
|
|
8095
|
+
context.recordScopeId(entryAnalyticsRetiredScopeKey(entryKey), (mainId ?? undelivered));
|
|
8096
|
+
}
|
|
8097
|
+
return {};
|
|
8098
|
+
}
|
|
8099
|
+
if (!org.hosting.entries.some((e) => e.key === entryKey && e.uses?.analytics)) {
|
|
8100
|
+
throw new Error(`${at}: the entry no longer declares uses.analytics`);
|
|
8101
|
+
}
|
|
8102
|
+
if (!appId)
|
|
8103
|
+
throw new Error(`${at}: the entry's app is not recorded yet`);
|
|
8104
|
+
if (desired.mode === 'url') {
|
|
8105
|
+
await this.ports.coolify.bulkUpsertEnvs(appId, [
|
|
8106
|
+
{ key: 'ANALYTICS_URL', value: baseUrl, literal: true },
|
|
8107
|
+
]);
|
|
8108
|
+
await this.ports.coolify.restart(appId);
|
|
8109
|
+
context.recordScopeId(entryAnalyticsAppliedScopeKey(entryKey), baseUrl);
|
|
8110
|
+
return {};
|
|
8111
|
+
}
|
|
8112
|
+
if (recorded[entryAnalyticsUndeliveredScopeKey(entryKey)]) {
|
|
8113
|
+
throw new Error(`${at}: a key was minted and never delivered; it is not minted again`);
|
|
8114
|
+
}
|
|
8115
|
+
const operatorKey = await this.analyticsOperatorKey(org, context);
|
|
8116
|
+
let minted;
|
|
8117
|
+
try {
|
|
8118
|
+
minted = await analytics.mintSiteKey({
|
|
8119
|
+
baseUrl,
|
|
8120
|
+
operatorKey,
|
|
8121
|
+
name: desired.name,
|
|
8122
|
+
environment: 'production',
|
|
8123
|
+
});
|
|
8124
|
+
}
|
|
8125
|
+
catch (error) {
|
|
8126
|
+
const status = error?.status;
|
|
8127
|
+
throw new Error(`${at}: the backend did not mint a key${typeof status === 'number' ? ` (status ${status})` : ''}`);
|
|
8128
|
+
}
|
|
8129
|
+
this.handledSecrets.add(minted.secret);
|
|
8130
|
+
context.recordScopeId(entryAnalyticsUndeliveredScopeKey(entryKey), minted.id);
|
|
8131
|
+
await context.persistScopeIds();
|
|
8132
|
+
const rows = [
|
|
8133
|
+
{ key: 'ANALYTICS_URL', value: baseUrl, literal: true },
|
|
8134
|
+
{ key: 'ANALYTICS_SITE_KEY', value: minted.secret, literal: true },
|
|
8135
|
+
];
|
|
8136
|
+
let lastError;
|
|
8137
|
+
for (let attempt = 1; attempt <= ENTRY_ANALYTICS_KEY_DELIVERY_ATTEMPTS; attempt++) {
|
|
8138
|
+
try {
|
|
8139
|
+
await this.ports.coolify.bulkUpsertEnvs(appId, rows);
|
|
8140
|
+
await this.ports.coolify.restart(appId);
|
|
8141
|
+
context.recordScopeId(entryAnalyticsAppliedScopeKey(entryKey), baseUrl);
|
|
8142
|
+
context.recordScopeId(entryAnalyticsUndeliveredScopeKey(entryKey), '');
|
|
8143
|
+
return { resourceId: minted.id };
|
|
8144
|
+
}
|
|
8145
|
+
catch (error) {
|
|
8146
|
+
lastError = error;
|
|
8147
|
+
}
|
|
8148
|
+
}
|
|
8149
|
+
throw new Error(`${at}: key ${minted.id} was minted but could not be written to the app's env after ${ENTRY_ANALYTICS_KEY_DELIVERY_ATTEMPTS} attempts (${lastError instanceof Error ? lastError.name : 'error'})`);
|
|
8150
|
+
}
|
|
7831
8151
|
// ---- kosmos (foundry#949) --------------------------------------------------
|
|
7832
8152
|
/**
|
|
7833
8153
|
* `plan/diff.ts`'s `planKosmos`: provisions the org's own kosmos
|
|
@@ -11448,6 +11768,46 @@ export function isNoChangesError(error) {
|
|
|
11448
11768
|
* {@link ZitadelPort.updateOidcAppLoginBaseUri} that counts an app already holding the wanted base URI as
|
|
11449
11769
|
* done: ZITADEL answers that write with "No changes" and a repeat apply must not fail on it.
|
|
11450
11770
|
*/
|
|
11771
|
+
/**
|
|
11772
|
+
* Turns the instance feature `loginV2.required` off so each app's own base URI counts, and makes sure no
|
|
11773
|
+
* app can then land on ZITADEL's v1 login: with `required` off an app without a base URI of its own
|
|
11774
|
+
* (ZITADEL's own console app, an app foundry did not make, an app made later) falls back to v1, and
|
|
11775
|
+
* ZITADEL v4.17.3 gives a `required` instance's base URI to every app but ignores an app's own
|
|
11776
|
+
* (`internal/query/oidc_client.go:88-92`), so the only way is a base URI on every app. Every live OIDC
|
|
11777
|
+
* app of every organisation and project gets `instanceBaseUri` when it has none; an app that has one
|
|
11778
|
+
* keeps it. The apps are set first, while `required` still makes the instance ignore them, so no sign-in
|
|
11779
|
+
* passes through a gap. Without a way to list every app, or with a listing that may be cut short, it
|
|
11780
|
+
* throws before the feature is touched.
|
|
11781
|
+
*/
|
|
11782
|
+
export async function allowPerAppLoginBaseUris(zitadel, instanceBaseUri) {
|
|
11783
|
+
if (!zitadel.allowPerAppLoginBaseUri)
|
|
11784
|
+
return;
|
|
11785
|
+
const { listOrgs, listProjects } = zitadel;
|
|
11786
|
+
if (!listOrgs || !listProjects) {
|
|
11787
|
+
throw new Error('the ZITADEL port cannot list every organisation and project, so apps without a login base URI would fall back to the v1 login');
|
|
11788
|
+
}
|
|
11789
|
+
for (const org of await listOrgs()) {
|
|
11790
|
+
if (org.state && org.state !== 'ORG_STATE_ACTIVE')
|
|
11791
|
+
continue;
|
|
11792
|
+
for (const project of await listProjects(org.id)) {
|
|
11793
|
+
if (project.state && project.state !== 'PROJECT_STATE_ACTIVE')
|
|
11794
|
+
continue;
|
|
11795
|
+
const listed = await zitadel.listApps(org.id, project.id);
|
|
11796
|
+
if (!listed.complete) {
|
|
11797
|
+
throw new Error(`the app listing of project ${project.id} may be cut short, so apps without a login base URI could be missed`);
|
|
11798
|
+
}
|
|
11799
|
+
for (const app of listed.apps) {
|
|
11800
|
+
if (app.state === 'APP_STATE_REMOVED' || app.appType === 'api')
|
|
11801
|
+
continue;
|
|
11802
|
+
// `undefined` is "not read": set it, the write is the same either way.
|
|
11803
|
+
if (typeof app.loginBaseUri === 'string' && app.loginBaseUri !== '')
|
|
11804
|
+
continue;
|
|
11805
|
+
await setOidcAppLoginBaseUri(zitadel, org.id, project.id, app.id, instanceBaseUri);
|
|
11806
|
+
}
|
|
11807
|
+
}
|
|
11808
|
+
}
|
|
11809
|
+
await zitadel.allowPerAppLoginBaseUri(instanceBaseUri);
|
|
11810
|
+
}
|
|
11451
11811
|
export async function setOidcAppLoginBaseUri(zitadel, orgId, projectId, appId, loginBaseUri) {
|
|
11452
11812
|
try {
|
|
11453
11813
|
await zitadel.updateOidcAppLoginBaseUri(orgId, projectId, appId, loginBaseUri);
|
package/dist/plan/hostnames.js
CHANGED
|
@@ -2,6 +2,7 @@ import { INBOX_HOST_PREFIX, WORKSPACE_HOST_PREFIX } from '../hosted-label.js';
|
|
|
2
2
|
import { APPS_DOMAIN, isMainEntry, mainEntry } from '../org.js';
|
|
3
3
|
import { workerAppHostnameOf } from '../worker-app-config.js';
|
|
4
4
|
import { agoraHostnameFor } from './agora.js';
|
|
5
|
+
import { analyticsHostnameFor, analyticsWanted } from './analytics.js';
|
|
5
6
|
import { bouleHostnameFor } from './boule.js';
|
|
6
7
|
import { workspaceWorkerParent } from './workspace-worker.js';
|
|
7
8
|
/**
|
|
@@ -154,6 +155,9 @@ export function tunnelIngressHostnames(org, loginHost = null) {
|
|
|
154
155
|
hostnames.push(agoraHostnameFor(org));
|
|
155
156
|
// #161: and Boule moves onto `boule.<domain>` the same way.
|
|
156
157
|
hostnames.push(bouleHostnameFor(org));
|
|
158
|
+
// The analytics backend's route, only while something uses it.
|
|
159
|
+
if (analyticsWanted(org))
|
|
160
|
+
hostnames.push(analyticsHostnameFor(org));
|
|
157
161
|
}
|
|
158
162
|
const share = filesShareHostname(org);
|
|
159
163
|
if (share)
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type { KeysRegistrarPort } from './keys-registrar.js';
|
|
2
|
+
export type { AnalyticsPort } from './analytics.js';
|
|
3
|
+
export { httpAnalytics } from './analytics.js';
|
|
2
4
|
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, } from './live.js';
|
|
3
5
|
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
4
6
|
export type { StepAction, PlannedStep, PlanDetail, OrgPostgresPlan } from './diff.js';
|
package/dist/plan/index.js
CHANGED
package/dist/plan/live.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export interface LiveResource {
|
|
|
42
42
|
* see that function's own doc comment for why "always planned" broke
|
|
43
43
|
* `bootstrapPlatform`.
|
|
44
44
|
*/
|
|
45
|
-
system: 'zitadel' | 'coolify' | 'cloudflare' | 'files' | 'ai' | 'cms' | 'jobs' | 'sfu' | 'edge' | 'heartbeats' | 'github' | 'audit';
|
|
45
|
+
system: 'zitadel' | 'coolify' | 'cloudflare' | 'files' | 'ai' | 'cms' | 'jobs' | 'sfu' | 'edge' | 'heartbeats' | 'github' | 'audit' | 'analytics';
|
|
46
46
|
kind: string;
|
|
47
47
|
key: string;
|
|
48
48
|
config: Record<string, unknown>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { kosmosOn, ownsServices } from '../org.js';
|
|
2
2
|
import { OWN_SERVICE_LABELS } from '../service-labels.js';
|
|
3
3
|
import { agoraHostnameFor } from './agora.js';
|
|
4
|
+
import { analyticsHostnameFor } from './analytics.js';
|
|
4
5
|
import { bouleHostnameFor } from './boule.js';
|
|
5
6
|
import { cmsStudioHostnameFor, siteHostnameFor } from './hostnames.js';
|
|
6
7
|
import { kosmosHostnameFor } from './kosmos.js';
|
|
@@ -32,6 +33,8 @@ export function lockedHostnames(org, platformHostnames) {
|
|
|
32
33
|
}
|
|
33
34
|
locked.add(agoraHostnameFor(org));
|
|
34
35
|
locked.add(bouleHostnameFor(org));
|
|
36
|
+
// Reserved always, planned only while something uses analytics (`analyticsWanted`).
|
|
37
|
+
locked.add(analyticsHostnameFor(org));
|
|
35
38
|
if (kosmosOn(org))
|
|
36
39
|
locked.add(kosmosHostnameFor(org));
|
|
37
40
|
if (org.site.enabled)
|
|
@@ -190,6 +190,14 @@ export declare function customersClientOrganizations(org: DeclaredOrg, recorded:
|
|
|
190
190
|
* falls back to the v1 login once in-app sign-in turns `loginV2.required` off. Absent or empty means none.
|
|
191
191
|
*/
|
|
192
192
|
export declare const OWN_CUSTOMERS_BASE_URI_SCOPE_KEY = "own_customers_login_base_uri";
|
|
193
|
+
/**
|
|
194
|
+
* Scope id recording that the instance's apps were all given a Login V2 base URI ({@link LOGIN_BASE_URI_SWEEP_VERSION}),
|
|
195
|
+
* on a company's own instance and on the platform's (Archon's step). A value that is missing or older plans the step
|
|
196
|
+
* again while in-app sign-in has `loginV2.required` off, so an instance applied before the sweep gets it too.
|
|
197
|
+
*/
|
|
198
|
+
export declare const LOGIN_BASE_URI_SWEEP_SCOPE_KEY = "login_base_uri_sweep";
|
|
199
|
+
/** The value {@link LOGIN_BASE_URI_SWEEP_SCOPE_KEY} holds once the sweep ran. Raise it to run the sweep again everywhere. */
|
|
200
|
+
export declare const LOGIN_BASE_URI_SWEEP_VERSION = "1";
|
|
193
201
|
/** The recorded ZITADEL app ids of the org's customers entries, sorted and joined by `,` ('' when none). */
|
|
194
202
|
export declare function ownCustomersAppState(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): string;
|
|
195
203
|
/** {@link LOGIN_CLIENT_ORGS_SCOPE_KEY}'s value for a map. */
|
package/dist/plan/login-host.js
CHANGED
|
@@ -223,6 +223,14 @@ export function customersClientOrganizations(org, recorded) {
|
|
|
223
223
|
* falls back to the v1 login once in-app sign-in turns `loginV2.required` off. Absent or empty means none.
|
|
224
224
|
*/
|
|
225
225
|
export const OWN_CUSTOMERS_BASE_URI_SCOPE_KEY = 'own_customers_login_base_uri';
|
|
226
|
+
/**
|
|
227
|
+
* Scope id recording that the instance's apps were all given a Login V2 base URI ({@link LOGIN_BASE_URI_SWEEP_VERSION}),
|
|
228
|
+
* on a company's own instance and on the platform's (Archon's step). A value that is missing or older plans the step
|
|
229
|
+
* again while in-app sign-in has `loginV2.required` off, so an instance applied before the sweep gets it too.
|
|
230
|
+
*/
|
|
231
|
+
export const LOGIN_BASE_URI_SWEEP_SCOPE_KEY = 'login_base_uri_sweep';
|
|
232
|
+
/** The value {@link LOGIN_BASE_URI_SWEEP_SCOPE_KEY} holds once the sweep ran. Raise it to run the sweep again everywhere. */
|
|
233
|
+
export const LOGIN_BASE_URI_SWEEP_VERSION = '1';
|
|
226
234
|
/** The recorded ZITADEL app ids of the org's customers entries, sorted and joined by `,` ('' when none). */
|
|
227
235
|
export function ownCustomersAppState(org, recorded) {
|
|
228
236
|
return [...customersEntryKeys(org)]
|