@wtfalch/foundry 0.41.1 → 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.
Files changed (38) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.js +1 -1
  3. package/dist/migrations/0054_org_signin_in_app.sql +6 -0
  4. package/dist/org.d.ts +17 -0
  5. package/dist/org.js +14 -0
  6. package/dist/plan/agora.d.ts +4 -1
  7. package/dist/plan/agora.js +16 -2
  8. package/dist/plan/boule.d.ts +5 -2
  9. package/dist/plan/boule.js +14 -2
  10. package/dist/plan/coolify-previews.d.ts +10 -0
  11. package/dist/plan/coolify-previews.js +14 -0
  12. package/dist/plan/diff.js +92 -9
  13. package/dist/plan/edge-ratelimits.d.ts +5 -0
  14. package/dist/plan/edge-ratelimits.js +16 -6
  15. package/dist/plan/executor.d.ts +45 -0
  16. package/dist/plan/executor.js +283 -22
  17. package/dist/plan/login-host.d.ts +23 -2
  18. package/dist/plan/login-host.js +30 -5
  19. package/dist/plan/own-diff.d.ts +1 -1
  20. package/dist/plan/own-diff.js +9 -2
  21. package/dist/plan/own-sfu.d.ts +2 -0
  22. package/dist/plan/own-sfu.js +3 -0
  23. package/dist/plan/platform-diff.js +4 -0
  24. package/dist/plan/platform-org.d.ts +6 -0
  25. package/dist/plan/platform-org.js +5 -0
  26. package/dist/plan/platform-providers.d.ts +16 -1
  27. package/dist/plan/platform-providers.js +255 -8
  28. package/dist/plan/providers-readers.js +6 -2
  29. package/dist/plan/sign-in-archon.d.ts +46 -0
  30. package/dist/plan/sign-in-archon.js +71 -0
  31. package/dist/plan/sign-in-bindings.d.ts +16 -0
  32. package/dist/plan/sign-in-bindings.js +44 -0
  33. package/dist/plan/sign-in.d.ts +135 -0
  34. package/dist/plan/sign-in.js +172 -0
  35. package/dist/store/postgres.js +7 -1
  36. package/dist/store/schema.d.ts +17 -0
  37. package/dist/store/schema.js +2 -0
  38. 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
- const loginClientNeedsMint = !freshLoginClientRecord ||
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 the login app and broker are down until a later apply mints and delivers a fresh one`
2078
- : `${label}: the login-client token of ZITADEL machine user ${loginClientUserId} could not be delivered after ${deliverAttempts} tries; this is its first token, so the login app and broker still have none until a later apply mints and delivers one`);
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
- if ((stale.key === 'FILES_MANAGEMENT_KEY' || staleUptime || staleOperator || staleLink) &&
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>;