@wtfalch/foundry 0.41.2 → 0.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/migrations/0054_org_signin_in_app.sql +6 -0
- package/dist/org.d.ts +17 -0
- package/dist/org.js +14 -0
- package/dist/plan/agora.d.ts +4 -1
- package/dist/plan/agora.js +16 -2
- package/dist/plan/boule.d.ts +5 -2
- package/dist/plan/boule.js +14 -2
- package/dist/plan/coolify-previews.d.ts +10 -0
- package/dist/plan/coolify-previews.js +14 -0
- package/dist/plan/diff.js +92 -9
- package/dist/plan/edge-ratelimits.d.ts +5 -0
- package/dist/plan/edge-ratelimits.js +16 -6
- package/dist/plan/executor.d.ts +45 -0
- package/dist/plan/executor.js +283 -22
- package/dist/plan/login-host.d.ts +23 -2
- package/dist/plan/login-host.js +30 -5
- package/dist/plan/own-diff.js +6 -0
- package/dist/plan/platform-diff.js +4 -0
- package/dist/plan/platform-org.d.ts +6 -0
- package/dist/plan/platform-org.js +5 -0
- package/dist/plan/platform-providers.d.ts +16 -1
- package/dist/plan/platform-providers.js +255 -8
- package/dist/plan/providers-readers.js +6 -2
- package/dist/plan/sign-in-archon.d.ts +46 -0
- package/dist/plan/sign-in-archon.js +71 -0
- package/dist/plan/sign-in-bindings.d.ts +16 -0
- package/dist/plan/sign-in-bindings.js +44 -0
- package/dist/plan/sign-in.d.ts +135 -0
- package/dist/plan/sign-in.js +172 -0
- package/dist/store/postgres.js +7 -1
- package/dist/store/schema.d.ts +17 -0
- package/dist/store/schema.js +2 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import { ownerMailboxes } from '../org.js';
|
|
|
7
7
|
import { BOULE_BRIDGE_SECRET_ENV, BOULE_PEER_PASSWORD_ENV, BOULE_PEER_ROLE, bouleHostnameFor, } from './boule.js';
|
|
8
8
|
import { recordBuiltCommit } from './build-record.js';
|
|
9
9
|
import { ensureCloudflaredConnector } from './cloudflared-connector.js';
|
|
10
|
+
import { previewsOn } from './coolify-previews.js';
|
|
10
11
|
import { waitForDeploymentFinished, waitForHealthyVersion, waitForNewDeployment, } from './deploy-health.js';
|
|
11
12
|
import { placedOnPlatform } from './destinations.js';
|
|
12
13
|
import { ACCESS_POLICY_NAME, coolifyProjectMark, getDatabaseStarted, isNotFoundError, isRunningOrStarting, } from './executor.js';
|
|
@@ -22,7 +23,10 @@ import { OIKOS_INGRESS_BLOCKED, PLATFORM_OIKOS_INGRESS_KEY, oikosIngressApplied,
|
|
|
22
23
|
import { recordOwnDnsHostname } from './own-dns.js';
|
|
23
24
|
import { platformNetwork, platformTarget, scopeFromPlatform, } from './own-services.js';
|
|
24
25
|
import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_AUTH_DOMAIN_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ACCESS_PROVIDER_UNDELIVERED_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_APPLIED_GITREF_SCOPE_KEY, PLATFORM_AI_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_AI_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AI_DEPLOY_PENDING_SCOPE_KEY, PLATFORM_AI_INTERNAL_URL_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_MANAGEMENT_KEY_CONFLICT_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_AI_MANAGEMENT_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_MIGRATE_BLOCKED_SCOPE_KEY, PLATFORM_AI_ORGANISATION_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_SET_SCOPE_KEY, PLATFORM_AI_SIGNING_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_AUTH_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FILES_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_ROLLING_UPDATE_SCOPE_KEY, PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY, PLATFORM_ARCHON_CONNECTOR_APP_SCOPE_KEY, PLATFORM_ARCHON_COOLIFY_READ_TOKEN_REF, PLATFORM_ARCHON_DEPLOYMENT_SCOPE_KEY, PLATFORM_ARCHON_ENVS_DEPLOYED_SCOPE_KEY, PLATFORM_ARCHON_GATE_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ID_TOKEN_USERINFO_APPLIED_SCOPE_KEY, PLATFORM_ARCHON_INTEGRATIONS_KEY_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_KEY, PLATFORM_ARCHON_MASTER_CLOUDFLARE_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_TOKEN_REF, PLATFORM_ARCHON_MASTER_COOLIFY_URL_REF, PLATFORM_ARCHON_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_ARCHON_ORG_DOMAIN_SCOPE_KEY, PLATFORM_ARCHON_ORG_POLICY_SCOPE_KEY, PLATFORM_ARCHON_ORG_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_ARCHON_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_ARCHON_OWNER_INVITE_SCOPE_KEY, PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY, PLATFORM_ARCHON_UPTIME_TOKEN_SYNCED_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_SCOPE_KEY, PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_CONNECTOR_APP_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_SCOPE_KEY, PLATFORM_AUTH_INSTANCE_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_ORG_SCOPE_KEY, PLATFORM_AUTH_OWNER_INVITE_SCOPE_KEY, PLATFORM_AUTH_REGISTRATION_CLOSED_SCOPE_KEY, PLATFORM_AUTH_RESERVED_ADMIN_USERNAME_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_APPLIED_IMAGE_TAG_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY, PLATFORM_BACKUP_DATABASE_SCOPE_KEYS, PLATFORM_BACKUP_SERVICE_KEYS, PLATFORM_EDGE_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_EDGE_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_EDGE_CLOUDFLARE_TOKEN_REF, PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_EDGE_KEY, PLATFORM_EDGE_KEY_REF, PLATFORM_EDGE_PLATFORM_KEY_SET_SCOPE_KEY, PLATFORM_EDGE_PLATFORM_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_SCOPE_KEY, PLATFORM_FILES_INTROSPECTION_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_FILES_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_SCOPE_KEY, PLATFORM_FILES_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_HC_ACCESS_SCOPE_KEY, PLATFORM_HC_API_KEY_REF, PLATFORM_HC_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_HC_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_HC_KEY, PLATFORM_HEARTBEATS_APPLIED_SCOPE_KEY, PLATFORM_HEARTBEATS_HOSTED_API_KEY_REF, PLATFORM_INTEGRATIONS_APPLIED_GITREF_SCOPE_KEY, PLATFORM_INTEGRATIONS_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY, PLATFORM_INTEGRATIONS_KEY_ID_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_PREVIOUS_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_REQUESTED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_ROTATION_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_SET_SCOPE_KEY, PLATFORM_INTEGRATIONS_KEY_UNDELIVERED_SCOPE_KEY, PLATFORM_INTEGRATIONS_RUNNER_KEYRING_DEPLOYED_SCOPE_KEY, PLATFORM_MAIL_ADMIN_KEY_SCOPE_KEY, PLATFORM_MAIL_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_MAIL_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_MAIL_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_MAIL_CONNECTOR_APP_SCOPE_KEY, PLATFORM_MAIL_HOST_PORTS_DIRECT, PLATFORM_MAIL_HOST_PORTS_SCOPE_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_RELAY_KEY, PLATFORM_MAIL_ROUTE_SCOPE_KEY, PLATFORM_MAIL_ROUTE_WANTED, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_BLOCKED_SCOPE_KEY, PLATFORM_OWNER_ORG_DOMAIN_SCOPE_KEY, PLATFORM_OWNER_ORG_SCOPE_KEY, PLATFORM_SFU_APPLIED_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_CHECK_NAME, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_ZITADEL_ORG_SCOPE_KEY, archonFilesMarker, archonFirstOwnerLink, archonOrgDomain, archonOrgName, desiredHeartbeatCheckNames, heartbeatCheckUrlRef, parseBrokerDeployRecord, parseLoginClientRecord, platformBackupHeartbeatCheckName, platformMailHostPorts, platformMailMxHost, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeRecorded, uptimeVarsDigest, } from './platform-diff.js';
|
|
26
|
+
import { PLATFORM_ARCHON_IN_APP_SCOPE_KEY, archonInAppBack, archonInAppBind, archonInAppHost, archonInAppPoint, archonInAppUnbind, } from './sign-in-archon.js';
|
|
27
|
+
import { LOGIN_HOST_BINDINGS_ENV } from './sign-in-bindings.js';
|
|
25
28
|
import { ensureSignInOrg, inviteOwnerWithLink, signInOrgDomain } from './sign-in-org.js';
|
|
29
|
+
import { SIGN_IN_BASE_PATH, SIGN_IN_BUILD_ARG_ENV, SIGN_IN_LOGIN_APP_SCOPE_KEY, SIGN_IN_UPSTREAM_ENV, inAppOnSharedLogin, signInLoginNames, signInUpstream, } from './sign-in.js';
|
|
26
30
|
import { SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY } from './signin-db-tls.js';
|
|
27
31
|
import { SIGNIN_DB_APP_SCOPE_KEY, SIGNIN_DB_TLS_SCOPE_KEY, dockerfileAppCreator, ensureSigninDbCaFile, provisionSigninDb, waitHealthy, withCallTimeout, } from './signin-db.js';
|
|
28
32
|
export { PLATFORM_ZITADEL_ORG_SCOPE_KEY };
|
|
@@ -594,8 +598,9 @@ export class FoundryPlatformStepExecutor {
|
|
|
594
598
|
}
|
|
595
599
|
if (platform.auth.enabled && this.auth) {
|
|
596
600
|
const loginClientDrifted = await authLoginClientDrifted(platform, this.auth, recordedIds);
|
|
597
|
-
if (loginClientDrifted)
|
|
601
|
+
if (loginClientDrifted || (await this.signInLoginDrifted(this.auth, recordedIds))) {
|
|
598
602
|
result.auth = true;
|
|
603
|
+
}
|
|
599
604
|
}
|
|
600
605
|
// #380: once the runner app is recorded, re-check its live env against
|
|
601
606
|
// every setting `ensureStarted()` requires -- the gap that left the
|
|
@@ -653,13 +658,45 @@ export class FoundryPlatformStepExecutor {
|
|
|
653
658
|
await coolify.deploy(runnerAppId);
|
|
654
659
|
return { [PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY]: normalHosts(wanted).join(',') };
|
|
655
660
|
}
|
|
661
|
+
/** True while a company signs in on its apps' own addresses through the platform's login. */
|
|
662
|
+
async signInLoginWanted() {
|
|
663
|
+
return (await this.declaredOrgStore.list()).some(inAppOnSharedLogin);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* In-app sign-in: the login app that serves `/sign-in` is wanted and either not recorded, gone from
|
|
667
|
+
* Coolify, or holding a host table other than the login's. The step then plans again and repairs it.
|
|
668
|
+
* A Coolify read that fails counts as not drifted, with one fixed log line (the error can carry values).
|
|
669
|
+
*/
|
|
670
|
+
async signInLoginDrifted(auth, recordedIds) {
|
|
671
|
+
if (!(await this.signInLoginWanted()))
|
|
672
|
+
return false;
|
|
673
|
+
const signInId = recordedIds[SIGN_IN_LOGIN_APP_SCOPE_KEY];
|
|
674
|
+
const loginId = recordedIds[PLATFORM_AUTH_LOGIN_SCOPE_KEY];
|
|
675
|
+
if (!signInId)
|
|
676
|
+
return true;
|
|
677
|
+
if (!loginId)
|
|
678
|
+
return false;
|
|
679
|
+
try {
|
|
680
|
+
const [login, signIn] = await Promise.all([
|
|
681
|
+
currentEnvValue(auth.coolify, loginId, LOGIN_HOST_BINDINGS_ENV),
|
|
682
|
+
currentEnvValue(auth.coolify, signInId, LOGIN_HOST_BINDINGS_ENV),
|
|
683
|
+
]);
|
|
684
|
+
return (login ?? '') !== (signIn ?? '');
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
if (isNotFoundError(error))
|
|
688
|
+
return true;
|
|
689
|
+
console.error('platform:auth: could not read the login apps to compare their host tables; treating the in-app login as in step');
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
656
693
|
async execute(action, context) {
|
|
657
694
|
if (action.system === 'coolify' && action.kind === 'app' && action.key === PLATFORM_AUTH_KEY) {
|
|
658
695
|
if (!this.auth) {
|
|
659
696
|
throw new Error('platform:auth needs FoundryPlatformStepExecutor to be constructed with its auth dependencies (#168)');
|
|
660
697
|
}
|
|
661
698
|
const company = await this.declaredOrgStore.get(context.platform.ownerOrgId);
|
|
662
|
-
return provisionPlatformAuth(context, this.limitedOnPlatform(this.auth, LIMITS.platformAuth), company);
|
|
699
|
+
return provisionPlatformAuth(context, this.limitedOnPlatform(this.auth, LIMITS.platformAuth), company, await this.signInLoginWanted());
|
|
663
700
|
}
|
|
664
701
|
if (action.system === 'coolify' && action.kind === 'app' && action.key === PLATFORM_FILES_KEY) {
|
|
665
702
|
if (!this.files) {
|
|
@@ -919,6 +956,7 @@ export async function provisionJobsHost(scope, deps, target, input) {
|
|
|
919
956
|
const server = await resolvePlatformServer(coolify);
|
|
920
957
|
const githubApp = await coolify.requireGithubApp();
|
|
921
958
|
const created = await coolify.createGithubAppApplication({
|
|
959
|
+
is_preview_deployments_enabled: false,
|
|
922
960
|
project_uuid: projectUuid,
|
|
923
961
|
server_uuid: server.uuid,
|
|
924
962
|
environment_name: environment.name,
|
|
@@ -1552,6 +1590,7 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1552
1590
|
const server = await resolvePlatformServer(deps.coolify);
|
|
1553
1591
|
const githubApp = await deps.coolify.requireGithubApp();
|
|
1554
1592
|
return deps.coolify.createGithubAppApplication({
|
|
1593
|
+
is_preview_deployments_enabled: false,
|
|
1555
1594
|
project_uuid: projectUuid,
|
|
1556
1595
|
server_uuid: server.uuid,
|
|
1557
1596
|
environment_name: environment.name,
|
|
@@ -1571,11 +1610,46 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1571
1610
|
custom_internal_name: target.internal.authLogin,
|
|
1572
1611
|
...IMAGE_HEALTHCHECK_SETTINGS,
|
|
1573
1612
|
});
|
|
1613
|
+
// ---- the in-app login app (`/sign-in`) ------------------------------------
|
|
1614
|
+
// A Next.js base path is fixed at build, so the login that serves `/sign-in` is a second app built from
|
|
1615
|
+
// the same Dockerfile with the base path as a build-time env. It is recorded as applied only at the end,
|
|
1616
|
+
// once it is deployed and holds the login token.
|
|
1617
|
+
const signInNames = input.signInLogin ? signInLoginNames(target) : null;
|
|
1618
|
+
const signInApp = signInNames
|
|
1619
|
+
? await findOrCreateSignInLoginApp(deps.coolify, {
|
|
1620
|
+
name: signInNames.name,
|
|
1621
|
+
environment,
|
|
1622
|
+
label,
|
|
1623
|
+
create: async () => {
|
|
1624
|
+
const server = await resolvePlatformServer(deps.coolify);
|
|
1625
|
+
const githubApp = await deps.coolify.requireGithubApp();
|
|
1626
|
+
return deps.coolify.createGithubAppApplication({
|
|
1627
|
+
project_uuid: projectUuid,
|
|
1628
|
+
server_uuid: server.uuid,
|
|
1629
|
+
environment_name: environment.name,
|
|
1630
|
+
environment_uuid: environment.uuid,
|
|
1631
|
+
github_app_uuid: githubApp.uuid,
|
|
1632
|
+
git_repository: AUTH_REPO,
|
|
1633
|
+
git_branch: gitRef,
|
|
1634
|
+
build_pack: 'dockerfile',
|
|
1635
|
+
name: signInNames.name,
|
|
1636
|
+
dockerfile_location: AUTH_LOGIN_DOCKERFILE,
|
|
1637
|
+
});
|
|
1638
|
+
},
|
|
1639
|
+
})
|
|
1640
|
+
: null;
|
|
1641
|
+
if (signInApp && signInNames) {
|
|
1642
|
+
await deps.coolify.updateApplication(signInApp.appId, {
|
|
1643
|
+
custom_internal_name: signInNames.internal,
|
|
1644
|
+
...IMAGE_HEALTHCHECK_SETTINGS,
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1574
1647
|
// ---- the broker app (#204) -------------------------------------------------
|
|
1575
1648
|
const brokerApp = await findOrCreatePlatformApp(deps.coolify, target.names.authBroker, environment, async () => {
|
|
1576
1649
|
const server = await resolvePlatformServer(deps.coolify);
|
|
1577
1650
|
const githubApp = await deps.coolify.requireGithubApp();
|
|
1578
1651
|
return deps.coolify.createGithubAppApplication({
|
|
1652
|
+
is_preview_deployments_enabled: false,
|
|
1579
1653
|
project_uuid: projectUuid,
|
|
1580
1654
|
server_uuid: server.uuid,
|
|
1581
1655
|
environment_name: environment.name,
|
|
@@ -1709,6 +1783,19 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1709
1783
|
await deps.coolify.bulkUpsertEnvs(zitadelAppId, zitadelEnvs);
|
|
1710
1784
|
await deps.coolify.bulkUpsertEnvs(loginAppId, loginEnvs);
|
|
1711
1785
|
await deps.coolify.bulkUpsertEnvs(brokerAppId, brokerEnvs);
|
|
1786
|
+
// The in-app login: the login's env, the base path as a build argument, and the host table.
|
|
1787
|
+
let signInEnvsChanged = false;
|
|
1788
|
+
if (signInApp && input.signInLogin) {
|
|
1789
|
+
const bindings = input.signInLogin.bindings ??
|
|
1790
|
+
(await currentEnvValue(deps.coolify, loginAppId, LOGIN_HOST_BINDINGS_ENV));
|
|
1791
|
+
const signInEnvs = [
|
|
1792
|
+
...loginEnvs,
|
|
1793
|
+
{ key: SIGN_IN_BUILD_ARG_ENV, value: SIGN_IN_BASE_PATH, buildTime: true },
|
|
1794
|
+
...(bindings ? [{ key: LOGIN_HOST_BINDINGS_ENV, value: bindings }] : []),
|
|
1795
|
+
];
|
|
1796
|
+
signInEnvsChanged = await envsDiffer(deps.coolify, signInApp.appId, signInEnvs);
|
|
1797
|
+
await deps.coolify.bulkUpsertEnvs(signInApp.appId, signInEnvs);
|
|
1798
|
+
}
|
|
1712
1799
|
// Deploy an app that was just created, is not running (a throw after the
|
|
1713
1800
|
// create left it never deployed, or it stopped), or whose envs changed.
|
|
1714
1801
|
// A move that started ZITADEL on the TLS database and never saw it healthy there (#746): start it
|
|
@@ -1732,6 +1819,9 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
1732
1819
|
if (loginApp.justCreated || !loginApp.running || loginEnvsChanged) {
|
|
1733
1820
|
await deps.coolify.deploy(loginAppId);
|
|
1734
1821
|
}
|
|
1822
|
+
if (signInApp && (signInApp.justCreated || !signInApp.running || signInEnvsChanged)) {
|
|
1823
|
+
await deps.coolify.deploy(signInApp.appId);
|
|
1824
|
+
}
|
|
1735
1825
|
// #211: the broker's own `server.ts` refuses to start without
|
|
1736
1826
|
// `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` and `LETTERMINT_API_TOKEN` --
|
|
1737
1827
|
// deploying it without them only produces a container that crash-loops
|
|
@@ -2010,8 +2100,33 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2010
2100
|
const loginClientOtherInputsMissing = brokerMissingInputs.filter((m) => m !== 'ZITADEL_SERVICE_USER_TOKEN');
|
|
2011
2101
|
const freshRecordedIds = scope.recordedIds();
|
|
2012
2102
|
const freshLoginClientRecord = parseLoginClientRecord(freshRecordedIds[PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY]);
|
|
2013
|
-
|
|
2103
|
+
let loginClientNeedsMint = !freshLoginClientRecord ||
|
|
2014
2104
|
(await authLoginClientDriftedAt({ hostname, bearerRef: target.refs.instanceBearer }, deps, freshRecordedIds));
|
|
2105
|
+
// The in-app login app needs the same token. When the login already holds a live one, it is copied
|
|
2106
|
+
// (no new mint, so the login is not restarted) if the in-app login holds none or another one -- a re-mint
|
|
2107
|
+
// while the app was not delivered to leaves it a revoked token. When the login's cannot be read, the mint
|
|
2108
|
+
// below delivers it. The tokens are compared, never logged.
|
|
2109
|
+
if (signInApp && !loginClientNeedsMint) {
|
|
2110
|
+
const signInHeld = await currentEnvValue(deps.coolify, signInApp.appId, SERVICE_USER_TOKEN_ENV);
|
|
2111
|
+
const held = await currentEnvValue(deps.coolify, loginAppId, SERVICE_USER_TOKEN_ENV);
|
|
2112
|
+
if (held && held !== signInHeld) {
|
|
2113
|
+
runtime.onSecret?.(held);
|
|
2114
|
+
const attempts = runtime.deliverAttempts ?? 3;
|
|
2115
|
+
try {
|
|
2116
|
+
await retryPlatformAuthCall(attempts, sleep, () => deps.coolify.bulkUpsertEnvs(signInApp.appId, [
|
|
2117
|
+
{ key: SERVICE_USER_TOKEN_ENV, value: held },
|
|
2118
|
+
]));
|
|
2119
|
+
await retryPlatformAuthCall(attempts, sleep, () => deps.coolify.deploy(signInApp.appId, true));
|
|
2120
|
+
}
|
|
2121
|
+
catch {
|
|
2122
|
+
// Never echo the cause: Coolify's validation error can include the rejected value (the token).
|
|
2123
|
+
throw new Error(`${label}: the login token could not be copied to ${signInNames?.name} after ${attempts} tries`);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
else if (!held && !signInHeld) {
|
|
2127
|
+
loginClientNeedsMint = true;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2015
2130
|
if (loginClientNeedsMint && loginClientOtherInputsMissing.length === 0) {
|
|
2016
2131
|
// An own org never mints a second time on top of a token that may be live:
|
|
2017
2132
|
// the platform's re-mint revokes the old one, which would take the org's
|
|
@@ -2040,6 +2155,9 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2040
2155
|
// comment above gives.
|
|
2041
2156
|
await record(PLATFORM_AUTH_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, loginClientUserId);
|
|
2042
2157
|
const deliverAttempts = runtime.deliverAttempts ?? 3;
|
|
2158
|
+
const receivers = signInApp
|
|
2159
|
+
? `the login app, the in-app login app (${signInNames?.name}) and broker`
|
|
2160
|
+
: 'the login app and broker';
|
|
2043
2161
|
try {
|
|
2044
2162
|
await retryPlatformAuthCall(deliverAttempts, sleep, () => deps.coolify.bulkUpsertEnvs(loginAppId, [
|
|
2045
2163
|
{ key: 'ZITADEL_SERVICE_USER_TOKEN', value: loginClientToken },
|
|
@@ -2047,6 +2165,11 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2047
2165
|
await retryPlatformAuthCall(deliverAttempts, sleep, () => deps.coolify.bulkUpsertEnvs(brokerAppId, [
|
|
2048
2166
|
{ key: 'ZITADEL_SERVICE_USER_TOKEN', value: loginClientToken },
|
|
2049
2167
|
]));
|
|
2168
|
+
if (signInApp) {
|
|
2169
|
+
await retryPlatformAuthCall(deliverAttempts, sleep, () => deps.coolify.bulkUpsertEnvs(signInApp.appId, [
|
|
2170
|
+
{ key: SERVICE_USER_TOKEN_ENV, value: loginClientToken },
|
|
2171
|
+
]));
|
|
2172
|
+
}
|
|
2050
2173
|
// Forced: an unforced deploy answers 200 "skipped" when one for the
|
|
2051
2174
|
// same commit is already queued -- likely here, since this function's
|
|
2052
2175
|
// own deploys above may just have queued one for either app. An
|
|
@@ -2055,6 +2178,9 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2055
2178
|
// (#216 follow-up review, item 2).
|
|
2056
2179
|
await retryPlatformAuthCall(deliverAttempts, sleep, () => deps.coolify.deploy(loginAppId, true));
|
|
2057
2180
|
await retryPlatformAuthCall(deliverAttempts, sleep, () => deps.coolify.deploy(brokerAppId, true));
|
|
2181
|
+
if (signInApp) {
|
|
2182
|
+
await retryPlatformAuthCall(deliverAttempts, sleep, () => deps.coolify.deploy(signInApp.appId, true));
|
|
2183
|
+
}
|
|
2058
2184
|
}
|
|
2059
2185
|
catch {
|
|
2060
2186
|
// Never echo the underlying error: Coolify's validation error can
|
|
@@ -2074,8 +2200,8 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2074
2200
|
// saying "its prior token was already revoked" there would describe a
|
|
2075
2201
|
// revoke that never happened, so the message only says so when it did.
|
|
2076
2202
|
throw new Error(revokedPriorPat
|
|
2077
|
-
? `${label}: the login-client token of ZITADEL machine user ${loginClientUserId} could not be delivered after ${deliverAttempts} tries; its prior token was already revoked, so
|
|
2078
|
-
: `${label}: the login-client token of ZITADEL machine user ${loginClientUserId} could not be delivered after ${deliverAttempts} tries; this is its first token, so
|
|
2203
|
+
? `${label}: the login-client token of ZITADEL machine user ${loginClientUserId} could not be delivered after ${deliverAttempts} tries; its prior token was already revoked, so ${receivers} are down until a later apply mints and delivers a fresh one`
|
|
2204
|
+
: `${label}: the login-client token of ZITADEL machine user ${loginClientUserId} could not be delivered after ${deliverAttempts} tries; this is its first token, so ${receivers} still have none until a later apply mints and delivers one`);
|
|
2079
2205
|
}
|
|
2080
2206
|
// #216 round 4/5: records what was actually delivered -- the user id,
|
|
2081
2207
|
// the exact PAT id, and (round 5) a hash of the exact token -- so the
|
|
@@ -2085,6 +2211,11 @@ export async function provisionAuthStack(scope, deps, target, input) {
|
|
|
2085
2211
|
// own doc comment, and `authLoginClientDrifted`'s, for why.
|
|
2086
2212
|
await record(PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, loginClientRecordValue(loginClientUserId, loginClientPatId, loginClientToken));
|
|
2087
2213
|
}
|
|
2214
|
+
// The in-app login app is ready once it is created, deployed (above) and holds the token: only then is
|
|
2215
|
+
// it recorded, so the login-host step plans no app host for a login that cannot serve it yet.
|
|
2216
|
+
if (signInApp && (await currentEnvValue(deps.coolify, signInApp.appId, SERVICE_USER_TOKEN_ENV))) {
|
|
2217
|
+
await record(SIGN_IN_LOGIN_APP_SCOPE_KEY, signInApp.appId);
|
|
2218
|
+
}
|
|
2088
2219
|
// ---- the owner company's own sign-in org ----------------------------------
|
|
2089
2220
|
// Archon signs people in from this org; the `foundry` org holds none. The
|
|
2090
2221
|
// company executor finds the same org by name (its slug) and makes no second.
|
|
@@ -2206,7 +2337,7 @@ const platformAuthProject = {
|
|
|
2206
2337
|
},
|
|
2207
2338
|
};
|
|
2208
2339
|
/** The platform's sign-in: {@link provisionAuthStack} with {@link platformTarget} and the platform's own inputs. */
|
|
2209
|
-
async function provisionPlatformAuth(context, deps, ownerCompany = null) {
|
|
2340
|
+
async function provisionPlatformAuth(context, deps, ownerCompany = null, signInLogin = false) {
|
|
2210
2341
|
const { platform } = context;
|
|
2211
2342
|
const scope = scopeFromPlatform(context);
|
|
2212
2343
|
return provisionAuthStack(scope, deps, platformTarget(), {
|
|
@@ -2218,6 +2349,8 @@ async function provisionPlatformAuth(context, deps, ownerCompany = null) {
|
|
|
2218
2349
|
smtpSender: `noreply@${platform.apex}`,
|
|
2219
2350
|
defaultRedirectUri: platform.archon.enabled ? `https://${platform.archon.hostname}` : null,
|
|
2220
2351
|
ownerCompany,
|
|
2352
|
+
// In-app sign-in: the second login app copies the login's host table (the host's port writes that).
|
|
2353
|
+
...(signInLogin ? { signInLogin: { bindings: null } } : {}),
|
|
2221
2354
|
});
|
|
2222
2355
|
}
|
|
2223
2356
|
/**
|
|
@@ -3814,6 +3947,7 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
3814
3947
|
const server = await resolvePlatformServer(deps.coolify);
|
|
3815
3948
|
const githubApp = await deps.coolify.requireGithubApp();
|
|
3816
3949
|
return deps.coolify.createGithubAppApplication({
|
|
3950
|
+
is_preview_deployments_enabled: false,
|
|
3817
3951
|
project_uuid: projectUuid,
|
|
3818
3952
|
server_uuid: server.uuid,
|
|
3819
3953
|
environment_name: environment.name,
|
|
@@ -3850,6 +3984,8 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
3850
3984
|
await deps.coolify.updateApplication(appId, {
|
|
3851
3985
|
custom_network_aliases: PLATFORM_ARCHON_INTERNAL_NAME,
|
|
3852
3986
|
is_consistent_container_name_enabled: false,
|
|
3987
|
+
// #788: previews off; `archonRollingUpdateDrifted` plans the step again if they come back on.
|
|
3988
|
+
is_preview_deployments_enabled: false,
|
|
3853
3989
|
// #543: the quartet below is the shared `healthCheckSettings` helper
|
|
3854
3990
|
// every other app foundry creates now also goes through -- Archon's own
|
|
3855
3991
|
// fields, unchanged in value, just no longer hand-rolled here.
|
|
@@ -3920,6 +4056,60 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
3920
4056
|
await record(PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY, created.clientId);
|
|
3921
4057
|
clientId = created.clientId;
|
|
3922
4058
|
}
|
|
4059
|
+
// ---- in-app sign-in -----------------------------------------------------------
|
|
4060
|
+
// Archon signs people in on its own address, through the owner company's in-app login app. Wanted when
|
|
4061
|
+
// the company has the switch on and that login app is applied. In this order: trusted and bound, then
|
|
4062
|
+
// the env (`SIGNIN_UPSTREAM`) written and deployed, then the client's base URI, so the proxy is never
|
|
4063
|
+
// on for an address the login cannot serve and the base URI never points at an app that does not proxy.
|
|
4064
|
+
// Given up in the reverse order: the base URI back first, then the env, then the binding and the domain.
|
|
4065
|
+
const inAppWanted = Boolean(ownerSignIn.signInInApp && ownerSignIn.signInLoginAppId && ownerCompany);
|
|
4066
|
+
const archonAppZitadelId = context.recordedIds()[PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY];
|
|
4067
|
+
const inAppOwed = archonInAppHost(context.recordedIds()) !== '';
|
|
4068
|
+
const inAppCalls = () => {
|
|
4069
|
+
const { ensureTrustedDomain, deleteTrustedDomain, updateOidcAppLoginBaseUri, allowPerAppLoginBaseUri, } = zitadel;
|
|
4070
|
+
if (!ensureTrustedDomain || !deleteTrustedDomain || !updateOidcAppLoginBaseUri) {
|
|
4071
|
+
throw new Error("platform:archon: the owner company's ZITADEL port lacks ensureTrustedDomain, deleteTrustedDomain or updateOidcAppLoginBaseUri, so Archon cannot sign in on its own address");
|
|
4072
|
+
}
|
|
4073
|
+
return {
|
|
4074
|
+
ensureTrustedDomain: ensureTrustedDomain.bind(zitadel),
|
|
4075
|
+
deleteTrustedDomain: deleteTrustedDomain.bind(zitadel),
|
|
4076
|
+
updateOidcAppLoginBaseUri: updateOidcAppLoginBaseUri.bind(zitadel),
|
|
4077
|
+
...(allowPerAppLoginBaseUri
|
|
4078
|
+
? { allowPerAppLoginBaseUri: allowPerAppLoginBaseUri.bind(zitadel) }
|
|
4079
|
+
: {}),
|
|
4080
|
+
};
|
|
4081
|
+
};
|
|
4082
|
+
const inAppTarget = {
|
|
4083
|
+
coolify: deps.coolify,
|
|
4084
|
+
loginHost: ownerSignIn.hostname,
|
|
4085
|
+
host: hostname,
|
|
4086
|
+
organisationId: authOrgId,
|
|
4087
|
+
projectId: project.id,
|
|
4088
|
+
appId: archonAppZitadelId ?? '',
|
|
4089
|
+
};
|
|
4090
|
+
if (inAppWanted && ownerSignIn.signInLoginAppId) {
|
|
4091
|
+
if (!archonAppZitadelId) {
|
|
4092
|
+
throw new Error("platform:archon: Archon's sign-in client is not recorded yet");
|
|
4093
|
+
}
|
|
4094
|
+
// A port that cannot allow a per-app base URI stops the step before anything is written.
|
|
4095
|
+
if (!zitadel.allowPerAppLoginBaseUri) {
|
|
4096
|
+
throw new Error("platform:archon: the owner company's ZITADEL port cannot allow a per-app login base URI, so Archon cannot sign in on its own address");
|
|
4097
|
+
}
|
|
4098
|
+
await archonInAppBind({
|
|
4099
|
+
...inAppTarget,
|
|
4100
|
+
zitadel: inAppCalls(),
|
|
4101
|
+
loginAppId: ownerSignIn.signInLoginAppId,
|
|
4102
|
+
clientId,
|
|
4103
|
+
});
|
|
4104
|
+
}
|
|
4105
|
+
else if (inAppOwed) {
|
|
4106
|
+
// Given up: the client goes back to the company's login host before the app stops proxying.
|
|
4107
|
+
await archonInAppBack({
|
|
4108
|
+
...inAppTarget,
|
|
4109
|
+
host: archonInAppHost(context.recordedIds()),
|
|
4110
|
+
zitadel: inAppCalls(),
|
|
4111
|
+
});
|
|
4112
|
+
}
|
|
3923
4113
|
// ---- env ------------------------------------------------------------------
|
|
3924
4114
|
const envs = [
|
|
3925
4115
|
{ key: 'APP_URL', value: `https://${hostname}` },
|
|
@@ -3929,6 +4119,8 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
3929
4119
|
{ key: 'AUTH_CLIENT_ID', value: clientId },
|
|
3930
4120
|
...(firstOwnerLink ? [{ key: 'ARCHON_FIRST_OWNER_LINK', value: firstOwnerLink }] : []),
|
|
3931
4121
|
];
|
|
4122
|
+
if (inAppWanted)
|
|
4123
|
+
envs.push({ key: SIGN_IN_UPSTREAM_ENV, value: signInUpstream(ownerCompany ?? undefined) });
|
|
3932
4124
|
// #265: an owner URL or reporting site already on the app is kept as it is.
|
|
3933
4125
|
// The owner URL must name the adopted database's host, or the app's two
|
|
3934
4126
|
// database URLs would point at different databases.
|
|
@@ -4103,7 +4295,13 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
4103
4295
|
const staleOperator = stale.key === 'AUTHZ_BOOTSTRAP_OPERATOR';
|
|
4104
4296
|
// The link is written only while both ids are known; one that is no longer wanted goes.
|
|
4105
4297
|
const staleLink = !firstOwnerLink && stale.key === 'ARCHON_FIRST_OWNER_LINK';
|
|
4106
|
-
|
|
4298
|
+
// In-app sign-in switched off: the proxy to the login goes.
|
|
4299
|
+
const staleSignIn = !inAppWanted && stale.key === SIGN_IN_UPSTREAM_ENV;
|
|
4300
|
+
if ((stale.key === 'FILES_MANAGEMENT_KEY' ||
|
|
4301
|
+
staleUptime ||
|
|
4302
|
+
staleOperator ||
|
|
4303
|
+
staleLink ||
|
|
4304
|
+
staleSignIn) &&
|
|
4107
4305
|
stale.uuid) {
|
|
4108
4306
|
await deps.coolify.deleteEnv(appId, stale.uuid);
|
|
4109
4307
|
staleKeyDeleted = true;
|
|
@@ -4363,6 +4561,25 @@ async function provisionPlatformArchon(context, deps, ownerCompany = null) {
|
|
|
4363
4561
|
if (archonDeployedNow && !selfRedeploy) {
|
|
4364
4562
|
await waitForHealthyVersion(appId, { kind: 'ready', url: `https://${hostname}/api/health` }, runtime, sleep, 'platform:archon', `archon stays at ${appliedGitRef ?? 'no recorded ref'} on ${recorded[PLATFORM_ARCHON_APPLIED_HOSTNAME_SCOPE_KEY] ?? 'no recorded hostname'}`);
|
|
4365
4563
|
}
|
|
4564
|
+
// In-app sign-in, last half. The env is deployed and Archon answers health, or no deploy was owed (the
|
|
4565
|
+
// running env is the one written), so Archon now proxies `/sign-in/*` (on) or no longer does (off).
|
|
4566
|
+
// A host that deploys itself has not confirmed that yet: the next run does the rest, and until then the
|
|
4567
|
+
// marker stays as it was, so the step is planned again. Recorded last of its own calls.
|
|
4568
|
+
if (!archonDeployedNow || !selfRedeploy) {
|
|
4569
|
+
if (inAppWanted) {
|
|
4570
|
+
await archonInAppPoint({ ...inAppTarget, zitadel: inAppCalls() });
|
|
4571
|
+
await record(PLATFORM_ARCHON_IN_APP_SCOPE_KEY, `${hostname}=${clientId}`);
|
|
4572
|
+
}
|
|
4573
|
+
else if (inAppOwed) {
|
|
4574
|
+
await archonInAppUnbind({
|
|
4575
|
+
...inAppTarget,
|
|
4576
|
+
host: archonInAppHost(context.recordedIds()),
|
|
4577
|
+
zitadel: inAppCalls(),
|
|
4578
|
+
...(ownerSignIn.signInLoginAppId ? { loginAppId: ownerSignIn.signInLoginAppId } : {}),
|
|
4579
|
+
});
|
|
4580
|
+
await record(PLATFORM_ARCHON_IN_APP_SCOPE_KEY, '');
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4366
4583
|
// #398: the deployment finished and answers health, so Archon now runs with
|
|
4367
4584
|
// the key id its env carries. `provisionPlatformIntegrations` drops the
|
|
4368
4585
|
// previous public key only on this marker.
|
|
@@ -5158,7 +5375,8 @@ async function archonRollingUpdateDrifted(coolify, appId) {
|
|
|
5158
5375
|
const app = await coolify.getApplication(appId);
|
|
5159
5376
|
return (app.settings?.is_consistent_container_name_enabled === true ||
|
|
5160
5377
|
app.custom_network_aliases !== PLATFORM_ARCHON_INTERNAL_NAME ||
|
|
5161
|
-
app.health_check_enabled !== true
|
|
5378
|
+
app.health_check_enabled !== true ||
|
|
5379
|
+
previewsOn(app));
|
|
5162
5380
|
}
|
|
5163
5381
|
catch {
|
|
5164
5382
|
return false;
|
|
@@ -5770,6 +5988,7 @@ async function provisionPlatformAi(context, deps) {
|
|
|
5770
5988
|
const server = await resolvePlatformServer(coolify);
|
|
5771
5989
|
const githubApp = await coolify.requireGithubApp();
|
|
5772
5990
|
return coolify.createGithubAppApplication({
|
|
5991
|
+
is_preview_deployments_enabled: false,
|
|
5773
5992
|
project_uuid: projectUuid,
|
|
5774
5993
|
server_uuid: server.uuid,
|
|
5775
5994
|
environment_name: environment.name,
|
|
@@ -6314,6 +6533,31 @@ export async function findOrCreatePlatformApp(coolify, name, environment, create
|
|
|
6314
6533
|
const created = await create();
|
|
6315
6534
|
return { appId: created.uuid, justCreated: true, running: false };
|
|
6316
6535
|
}
|
|
6536
|
+
const SERVICE_USER_TOKEN_ENV = 'ZITADEL_SERVICE_USER_TOKEN';
|
|
6537
|
+
/**
|
|
6538
|
+
* {@link findOrCreatePlatformApp} for the in-app login app, which adopts only an app that is its own: one
|
|
6539
|
+
* of that name outside this environment, or built from another repository, is refused and left as it is.
|
|
6540
|
+
* The check reads the application list directly: a lookup by name through the limits wrapper would patch
|
|
6541
|
+
* the stranger's resource limits before this could refuse it.
|
|
6542
|
+
*/
|
|
6543
|
+
async function findOrCreateSignInLoginApp(coolify, input) {
|
|
6544
|
+
const { name, environment, label } = input;
|
|
6545
|
+
const listed = await coolify.raw('GET', '/applications');
|
|
6546
|
+
if (!Array.isArray(listed)) {
|
|
6547
|
+
throw new Error(`${label}: Coolify's application list could not be read, so ${name} is not touched`);
|
|
6548
|
+
}
|
|
6549
|
+
for (const app of listed) {
|
|
6550
|
+
if (app.name !== name)
|
|
6551
|
+
continue;
|
|
6552
|
+
if (app.environment_id !== environment.id) {
|
|
6553
|
+
throw new Error(`${label}: an application named ${name} exists outside the ${environment.name} environment of the sign-in project; foundry does not adopt it`);
|
|
6554
|
+
}
|
|
6555
|
+
if (app.git_repository !== undefined && !app.git_repository.endsWith(AUTH_REPO)) {
|
|
6556
|
+
throw new Error(`${label}: the application ${name} is built from ${app.git_repository}, not ${AUTH_REPO}; foundry does not adopt it`);
|
|
6557
|
+
}
|
|
6558
|
+
}
|
|
6559
|
+
return findOrCreatePlatformApp(coolify, name, environment, input.create);
|
|
6560
|
+
}
|
|
6317
6561
|
/**
|
|
6318
6562
|
* #211: names of the envs the platform's sign-in broker (`wtfalch/auth`'s
|
|
6319
6563
|
* `broker/server.ts`) refuses to start without -- `ZITADEL_SERVICE_USER_TOKEN`/
|
|
@@ -6790,6 +7034,7 @@ export async function provisionFilesHost(scope, deps, target, input) {
|
|
|
6790
7034
|
const server = await resolvePlatformServer(deps.coolify);
|
|
6791
7035
|
const githubApp = await deps.coolify.requireGithubApp();
|
|
6792
7036
|
return deps.coolify.createGithubAppApplication({
|
|
7037
|
+
is_preview_deployments_enabled: false,
|
|
6793
7038
|
project_uuid: projectUuid,
|
|
6794
7039
|
server_uuid: server.uuid,
|
|
6795
7040
|
environment_name: environment.name,
|
|
@@ -7183,6 +7428,7 @@ async function provisionPlatformEdge(context, deps) {
|
|
|
7183
7428
|
const app = await findOrCreatePlatformApp(deps.coolify, EDGE_APP_NAME, environment, async () => {
|
|
7184
7429
|
const githubApp = await deps.coolify.requireGithubApp();
|
|
7185
7430
|
return deps.coolify.createGithubAppApplication({
|
|
7431
|
+
is_preview_deployments_enabled: false,
|
|
7186
7432
|
project_uuid: projectUuid,
|
|
7187
7433
|
server_uuid: server.uuid,
|
|
7188
7434
|
environment_name: environment.name,
|
|
@@ -8082,6 +8328,7 @@ async function provisionPlatformIntegrations(action, context, deps) {
|
|
|
8082
8328
|
const server = await resolvePlatformServer(coolify);
|
|
8083
8329
|
const githubApp = await coolify.requireGithubApp();
|
|
8084
8330
|
return coolify.createGithubAppApplication({
|
|
8331
|
+
is_preview_deployments_enabled: false,
|
|
8085
8332
|
project_uuid: projectUuid,
|
|
8086
8333
|
server_uuid: server.uuid,
|
|
8087
8334
|
environment_name: environment.name,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ZitadelError } from '@wtfalch/providers/zitadel';
|
|
2
2
|
import { APP_ENTRY_KEY } from '../org.js';
|
|
3
3
|
import { BOULE_MAIL_TOKEN_ENV, BOULE_MANAGED_ENV_KEYS } from './boule.js';
|
|
4
|
+
import { previewsOn } from './coolify-previews.js';
|
|
5
|
+
import { SIGN_IN_UPSTREAM_ENV } from './sign-in.js';
|
|
4
6
|
/**
|
|
5
7
|
* A {@link ScopeResolver} backed by `AppliedState.serviceIds` (`../state.ts`)
|
|
6
8
|
* — the one place foundry records a provider's own id for an org today,
|
|
@@ -169,9 +171,10 @@ async function withHealthCheckConfig(client, resource, recorded) {
|
|
|
169
171
|
// above notes for `ports_exposes` -- `getApplication` returns the raw
|
|
170
172
|
// Coolify API response regardless.
|
|
171
173
|
const healthCheckEnabled = app.health_check_enabled;
|
|
174
|
+
// #788: the same read says whether pull-request previews are on.
|
|
172
175
|
return {
|
|
173
176
|
...resource,
|
|
174
|
-
config: { ...resource.config, healthCheckEnabled },
|
|
177
|
+
config: { ...resource.config, healthCheckEnabled, previewsEnabled: previewsOn(app) },
|
|
175
178
|
};
|
|
176
179
|
}
|
|
177
180
|
catch {
|
|
@@ -243,8 +246,9 @@ async function withAgoraEnvConfig(client, resource, recorded) {
|
|
|
243
246
|
]);
|
|
244
247
|
const env = {};
|
|
245
248
|
for (const e of envs) {
|
|
246
|
-
if (!e.is_preview && e.key.startsWith('AGORA_'))
|
|
249
|
+
if (!e.is_preview && (e.key.startsWith('AGORA_') || e.key === SIGN_IN_UPSTREAM_ENV)) {
|
|
247
250
|
env[e.key] = e.value;
|
|
251
|
+
}
|
|
248
252
|
}
|
|
249
253
|
// `@wtfalch/providers`' `CoolifyApplication` type does not declare
|
|
250
254
|
// `ports_exposes` (as of 0.7.3), but `getApplication` returns the raw
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { CoolifyPort, ZitadelPort } from './executor.js';
|
|
2
|
+
/**
|
|
3
|
+
* Archon in-app sign-in: Archon signs people in on its own address under `/sign-in`, through the owner
|
|
4
|
+
* company's in-app login app. Its sign-in client lives in the owner company's ZITADEL, in Archon's own
|
|
5
|
+
* organisation, so this is the platform's step and not the company's login-host step.
|
|
6
|
+
*/
|
|
7
|
+
/** Recorded `<host>=<clientId>` once Archon's address is trusted, bound, proxied by the app and pointed at; `''` once given up again, in the reverse order. */
|
|
8
|
+
export declare const PLATFORM_ARCHON_IN_APP_SCOPE_KEY = "platform_archon_in_app";
|
|
9
|
+
/** The host a recorded {@link PLATFORM_ARCHON_IN_APP_SCOPE_KEY} names; `''` when none. */
|
|
10
|
+
export declare function archonInAppHost(recorded: Readonly<Record<string, string>>): string;
|
|
11
|
+
/** The ZITADEL calls Archon's in-app sign-in makes, on the owner company's own instance. */
|
|
12
|
+
export type ArchonInAppZitadel = Pick<ZitadelPort, 'ensureTrustedDomain' | 'deleteTrustedDomain' | 'updateOidcAppLoginBaseUri'> & Partial<Pick<ZitadelPort, 'allowPerAppLoginBaseUri'>>;
|
|
13
|
+
interface ArchonInAppInput {
|
|
14
|
+
zitadel: ArchonInAppZitadel;
|
|
15
|
+
coolify: Pick<CoolifyPort, 'listEnvs' | 'bulkUpsertEnvs' | 'deploy'>;
|
|
16
|
+
/** The owner company's in-app login app, whose host table holds Archon's binding. Giving the address up without it leaves no table to edit. */
|
|
17
|
+
loginAppId?: string;
|
|
18
|
+
/** The owner company's login host, the instance's own: Archon's default base URI. */
|
|
19
|
+
loginHost: string;
|
|
20
|
+
/** Archon's address. */
|
|
21
|
+
host: string;
|
|
22
|
+
/** Archon's ZITADEL organisation, project and sign-in client (app id and client id). */
|
|
23
|
+
organisationId: string;
|
|
24
|
+
projectId: string;
|
|
25
|
+
appId: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Archon's address becomes one the login serves, in two halves with the app's own env between them
|
|
29
|
+
* (`SIGNIN_UPSTREAM`, written and deployed by the step): first trusted in the company's ZITADEL and bound
|
|
30
|
+
* to Archon's organisation and client in the in-app login's host table; only after the app proxies
|
|
31
|
+
* `/sign-in/*`, the client's Login V2 base URI is set to the address ({@link archonInAppPoint}).
|
|
32
|
+
*/
|
|
33
|
+
export declare function archonInAppBind(input: ArchonInAppInput & {
|
|
34
|
+
clientId: string;
|
|
35
|
+
loginAppId: string;
|
|
36
|
+
}): Promise<void>;
|
|
37
|
+
/** Sets the client's Login V2 base URI to Archon's address. Only after the app proxies `/sign-in/*`. */
|
|
38
|
+
export declare function archonInAppPoint(input: ArchonInAppInput): Promise<void>;
|
|
39
|
+
/** First half of giving the address up: the client goes back to the company's login host. */
|
|
40
|
+
export declare function archonInAppBack(input: ArchonInAppInput): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Last half: the binding and the trusted domain are removed, after the app stopped proxying. A domain or
|
|
43
|
+
* binding already gone counts as done.
|
|
44
|
+
*/
|
|
45
|
+
export declare function archonInAppUnbind(input: ArchonInAppInput): Promise<void>;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { isNotFoundError, setOidcAppLoginBaseUri } from './executor.js';
|
|
2
|
+
import { editSignInBindings } from './sign-in-bindings.js';
|
|
3
|
+
import { SIGN_IN_BASE_PATH } from './sign-in.js';
|
|
4
|
+
/**
|
|
5
|
+
* Archon in-app sign-in: Archon signs people in on its own address under `/sign-in`, through the owner
|
|
6
|
+
* company's in-app login app. Its sign-in client lives in the owner company's ZITADEL, in Archon's own
|
|
7
|
+
* organisation, so this is the platform's step and not the company's login-host step.
|
|
8
|
+
*/
|
|
9
|
+
/** Recorded `<host>=<clientId>` once Archon's address is trusted, bound, proxied by the app and pointed at; `''` once given up again, in the reverse order. */
|
|
10
|
+
export const PLATFORM_ARCHON_IN_APP_SCOPE_KEY = 'platform_archon_in_app';
|
|
11
|
+
/** The host a recorded {@link PLATFORM_ARCHON_IN_APP_SCOPE_KEY} names; `''` when none. */
|
|
12
|
+
export function archonInAppHost(recorded) {
|
|
13
|
+
return (recorded[PLATFORM_ARCHON_IN_APP_SCOPE_KEY] ?? '').split('=')[0] ?? '';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Archon's address becomes one the login serves, in two halves with the app's own env between them
|
|
17
|
+
* (`SIGNIN_UPSTREAM`, written and deployed by the step): first trusted in the company's ZITADEL and bound
|
|
18
|
+
* to Archon's organisation and client in the in-app login's host table; only after the app proxies
|
|
19
|
+
* `/sign-in/*`, the client's Login V2 base URI is set to the address ({@link archonInAppPoint}).
|
|
20
|
+
*/
|
|
21
|
+
export async function archonInAppBind(input) {
|
|
22
|
+
const { zitadel, host } = input;
|
|
23
|
+
await zitadel.ensureTrustedDomain(host);
|
|
24
|
+
await editSignInBindings(input.coolify, {
|
|
25
|
+
appId: input.loginAppId,
|
|
26
|
+
instanceHost: input.loginHost,
|
|
27
|
+
upsert: [
|
|
28
|
+
{
|
|
29
|
+
host,
|
|
30
|
+
organizationId: input.organisationId,
|
|
31
|
+
clientIds: [input.clientId],
|
|
32
|
+
apps: { [input.clientId]: 'archon' },
|
|
33
|
+
basePath: SIGN_IN_BASE_PATH,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** Sets the client's Login V2 base URI to Archon's address. Only after the app proxies `/sign-in/*`. */
|
|
39
|
+
export async function archonInAppPoint(input) {
|
|
40
|
+
const { zitadel, host } = input;
|
|
41
|
+
if (!zitadel.allowPerAppLoginBaseUri) {
|
|
42
|
+
throw new Error("platform:archon: the owner company's ZITADEL port cannot allow a per-app login base URI, so Archon cannot sign in on its own address");
|
|
43
|
+
}
|
|
44
|
+
await zitadel.allowPerAppLoginBaseUri(`https://${input.loginHost}/`);
|
|
45
|
+
await setOidcAppLoginBaseUri(zitadel, input.organisationId, input.projectId, input.appId, `https://${host}${SIGN_IN_BASE_PATH}/`);
|
|
46
|
+
}
|
|
47
|
+
/** First half of giving the address up: the client goes back to the company's login host. */
|
|
48
|
+
export async function archonInAppBack(input) {
|
|
49
|
+
await setOidcAppLoginBaseUri(input.zitadel, input.organisationId, input.projectId, input.appId, `https://${input.loginHost}/`);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Last half: the binding and the trusted domain are removed, after the app stopped proxying. A domain or
|
|
53
|
+
* binding already gone counts as done.
|
|
54
|
+
*/
|
|
55
|
+
export async function archonInAppUnbind(input) {
|
|
56
|
+
const { zitadel, host } = input;
|
|
57
|
+
if (input.loginAppId) {
|
|
58
|
+
await editSignInBindings(input.coolify, {
|
|
59
|
+
appId: input.loginAppId,
|
|
60
|
+
instanceHost: input.loginHost,
|
|
61
|
+
remove: [host],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await zitadel.deleteTrustedDomain(host);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (!isNotFoundError(error))
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CoolifyPort } from './executor.js';
|
|
2
|
+
import type { LoginHostBinding } from './login-host.js';
|
|
3
|
+
/** The login's host table env, and the document it holds (`auth/docs/company-login-hosts.md`). */
|
|
4
|
+
export declare const LOGIN_HOST_BINDINGS_ENV = "LOGIN_HOST_BINDINGS";
|
|
5
|
+
/**
|
|
6
|
+
* Adds, replaces and removes hosts in the host table of one in-app login app, then redeploys it when the
|
|
7
|
+
* document changed. Hosts not named stay as they are, so two steps that each own some hosts (the
|
|
8
|
+
* company's login-host step, Archon's step) never undo each other. Entries are sorted by host and the
|
|
9
|
+
* document is written compact, so a repeat writes the same text and deploys nothing.
|
|
10
|
+
*/
|
|
11
|
+
export declare function editSignInBindings(coolify: Pick<CoolifyPort, 'listEnvs' | 'bulkUpsertEnvs' | 'deploy'>, input: {
|
|
12
|
+
appId: string;
|
|
13
|
+
instanceHost: string;
|
|
14
|
+
upsert?: readonly LoginHostBinding[];
|
|
15
|
+
remove?: readonly string[];
|
|
16
|
+
}): Promise<void>;
|