@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.
@@ -1,6 +1,7 @@
1
1
  import { ownsServices } from '../org.js';
2
- import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, } from './agora.js';
3
- import { BOULE_CLIENT_ID_SCOPE_KEY } from './boule.js';
2
+ import { AGORA_CLIENT_ID_SCOPE_KEY, APP_CLIENT_ID_SCOPE_KEY, CMS_CLIENT_ID_SCOPE_KEY, ENTRY_CLIENT_ID_SCOPE_PREFIX, agoraHostnameFor, } from './agora.js';
3
+ import { BOULE_CLIENT_ID_SCOPE_KEY, bouleHostnameFor } from './boule.js';
4
+ import { SIGN_IN_IN_APP_SCOPE_KEY, inAppSignIn, inAppStatePairs, } from './sign-in.js';
4
5
  /**
5
6
  * #267: what both the planner (`diff.ts`) and the executor (`executor.ts`)
6
7
  * need to agree on for an org's own company login host -- the hostname and
@@ -120,7 +121,10 @@ export function loginHostServesAgora(host, org, recorded) {
120
121
  if (!bound?.startsWith(`${host} `))
121
122
  return false;
122
123
  const clientId = recorded[AGORA_CLIENT_ID_SCOPE_KEY];
124
+ // Agora's client moves to its own host's binding once that host signs in on the app's address.
125
+ const movedToAppHost = inAppStatePairs(recorded[SIGN_IN_IN_APP_SCOPE_KEY]).some((pair) => pair.clientId === clientId);
123
126
  return (!clientId ||
127
+ movedToAppHost ||
124
128
  bound
125
129
  .slice(host.length + 1)
126
130
  .split(',')
@@ -132,6 +136,23 @@ export function loginHostServesAgora(host, org, recorded) {
132
136
  * instance, so `destroy` must not remove it.
133
137
  */
134
138
  export const LOGIN_HOST_SHARED_SCOPE_KEY = 'zitadel_login_host_shared';
139
+ /**
140
+ * The app hosts that sign in on their own address when the company has `signIn.inApp`: Agora's and
141
+ * Boule's, each only once its client id is recorded. Empty with the switch off. Archon (owner company
142
+ * only) is not here: the platform's Archon step owns its host (`sign-in-archon.ts`).
143
+ */
144
+ export function inAppApps(org, recorded) {
145
+ if (!inAppSignIn(org))
146
+ return [];
147
+ const apps = [];
148
+ const agora = recorded[AGORA_CLIENT_ID_SCOPE_KEY];
149
+ if (agora)
150
+ apps.push({ app: 'agora', host: agoraHostnameFor(org), clientId: agora });
151
+ const boule = recorded[BOULE_CLIENT_ID_SCOPE_KEY];
152
+ if (boule)
153
+ apps.push({ app: 'boule', host: bouleHostnameFor(org), clientId: boule });
154
+ return apps.sort((a, b) => (a.host < b.host ? -1 : 1));
155
+ }
135
156
  /**
136
157
  * Scope id holding what the binding was last written with
137
158
  * ({@link loginBindingState}). Client ids are not secrets. The planner
@@ -149,8 +170,12 @@ export const LOGIN_BINDING_SCOPE_KEY = 'login_host_binding';
149
170
  export function customersEntryKeys(org) {
150
171
  return new Set(org.hosting.entries.filter((e) => e.uses?.auth?.users === 'customers').map((e) => e.key));
151
172
  }
152
- /** The org's recorded app client ids (hosted app, each entry's own bar a customers entry, cms, agora, boule), deduplicated and sorted. */
153
- export function loginHostClientIds(org, recorded) {
173
+ /**
174
+ * The org's recorded app client ids (hosted app, each entry's own bar a customers entry, cms, agora, boule),
175
+ * deduplicated and sorted, bar `except`: the login refuses a client id held by two bindings, so the ids of the
176
+ * app hosts that have their own binding (in-app sign-in) are not in the company's.
177
+ */
178
+ export function loginHostClientIds(org, recorded, except = []) {
154
179
  const customers = customersEntryKeys(org);
155
180
  const ids = [
156
181
  recorded[APP_CLIENT_ID_SCOPE_KEY],
@@ -162,7 +187,7 @@ export function loginHostClientIds(org, recorded) {
162
187
  !customers.has(key.slice(ENTRY_CLIENT_ID_SCOPE_PREFIX.length)))
163
188
  .map(([, id]) => id),
164
189
  ].filter((id) => Boolean(id));
165
- return [...new Set(ids)].sort();
190
+ return [...new Set(ids)].filter((id) => !except.includes(id)).sort();
166
191
  }
167
192
  /** The host plus its sorted client ids, as recorded under {@link LOGIN_BINDING_SCOPE_KEY}. */
168
193
  export function loginBindingState(host, clientIds) {
@@ -13,6 +13,7 @@ import { OWN_SFU_APPLIED_HOSTNAME_SCOPE_KEY, OWN_SFU_APPLIED_NODE_IP_SCOPE_KEY,
13
13
  import { AUTH_ZITADEL_RUN_OPTIONS, JOBS_DEPLOY_PENDING_SCOPE_KEY, JOBS_INTERNAL_URL_SCOPE_KEY, PLATFORM_AUTH_ADMIN_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_ADMIN_USER_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_LOGIN_CLIENT_UNDELIVERED_SCOPE_KEY, PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY, PLATFORM_AUTH_SMTP_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_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_MANAGEMENT_KEY_SCOPE_KEY, PLATFORM_FILES_MANAGEMENT_UNDELIVERED_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_INTERNAL_URL_SCOPE_KEY, platformMailMxHost, } from './platform-diff.js';
14
14
  import { OWN_FILES_RUNNER_SCOPE_KEY } from './platform-org.js';
15
15
  import { JOBS_ADMIN_TOKEN_UNDELIVERED_SCOPE_KEY, JOBS_APPLIED_GIT_REF_SCOPE_KEY, PLATFORM_AUTH_DB_SCOPE_KEY, PLATFORM_AUTH_ZITADEL_SCOPE_KEY, PLATFORM_FILES_SHARE_DOMAINS_SCOPE_KEY, filesShareDomains, } from './platform-providers.js';
16
+ import { SIGN_IN_LOGIN_APP_SCOPE_KEY, inAppSignIn } from './sign-in.js';
16
17
  import { signinDbBackupMissing } from './signin-db-backup.js';
17
18
  import { SIGNIN_DB_ZITADEL_PENDING_SCOPE_KEY, signinDbCertDue } from './signin-db-tls.js';
18
19
  import { SIGNIN_DB_TLS_SCOPE_KEY } from './signin-db.js';
@@ -162,6 +163,11 @@ export function planOwnAuth(options) {
162
163
  recordedIds[PLATFORM_AUTH_SMTP_SCOPE_KEY] &&
163
164
  recordedIds[PLATFORM_AUTH_ZITADEL_RUN_OPTIONS_SCOPE_KEY] === AUTH_ZITADEL_RUN_OPTIONS &&
164
165
  (Boolean(recordedIds[PLATFORM_AUTH_LOGIN_CLIENT_USER_SCOPE_KEY]) || !brokerHoldsAnEntry) &&
166
+ // In-app sign-in: the second login app is recorded once it is deployed and holds the token, which
167
+ // needs the login client; until the broker holds an entry that cannot be, so the step waits.
168
+ (!inAppSignIn(org) ||
169
+ Boolean(recordedIds[SIGN_IN_LOGIN_APP_SCOPE_KEY]) ||
170
+ !brokerHoldsAnEntry) &&
165
171
  !signinDbCertDue(recordedIds, options.now ?? Date.now()) &&
166
172
  !signinDbBackupMissing(recordedIds) &&
167
173
  // ZITADEL was started on the TLS database and never seen healthy there: the step starts it again.
@@ -11,6 +11,7 @@ import { PLATFORM_MAIL_RELAY_KEY, planMailRelay } from './mail-relay.js';
11
11
  import { MAIL_DATABASE_SCOPE_KEY, mailStoreIsLegacy, mailStoreSwitchChange, mailStoreSwitchDescription, } from './mail-store.js';
12
12
  import { planOikosIngress } from './oikos-ingress.js';
13
13
  import { callsSfuAppliedFingerprint } from './platform-sfu.js';
14
+ import { archonInAppHost } from './sign-in-archon.js';
14
15
  import { signInDomainBlockedText } from './sign-in-org.js';
15
16
  import { withStepSystem } from './step-system.js';
16
17
  /**
@@ -3055,6 +3056,9 @@ function planArchon(platform, recordedIds, executorBlocked, ownerSignIn, ownerFi
3055
3056
  recordedIds[PLATFORM_ARCHON_APPLIED_CI_ORG_SCOPE_KEY] === ownerSignIn.organisationId &&
3056
3057
  (recordedIds[PLATFORM_ARCHON_APPLIED_FIRST_OWNER_LINK_SCOPE_KEY] ?? '') ===
3057
3058
  archonFirstOwnerLink(recordedIds, ownerSignIn) &&
3059
+ // In-app sign-in: Archon's own address is the sign-in address only once the owner company has the
3060
+ // switch on and its in-app login is applied, and goes back to the company's login host when it does not.
3061
+ archonInAppHost(recordedIds) === (ownerSignIn.signInInApp ? platform.archon.hostname : '') &&
3058
3062
  recordedIds[PLATFORM_ARCHON_APP_KEY_ORG_SCOPE_KEY] === archonOrgId &&
3059
3063
  !recordedIds[PLATFORM_ARCHON_RETIRED_CLIENT_SCOPE_KEY] &&
3060
3064
  Boolean(recordedIds[PLATFORM_ARCHON_ACCESS_PIN_SCOPE_KEY]) &&
@@ -102,6 +102,8 @@ export interface OrgPlatformReadiness {
102
102
  * platform's, not as strangers. Absent when none is recorded.
103
103
  */
104
104
  platformZitadelIds?: readonly string[];
105
+ /** The second login app that serves `/sign-in` for companies with `signIn.inApp` is applied. Absent reads as false. */
106
+ signInLoginReady?: boolean;
105
107
  /** The platform's own sign-in (#168), which every org's own ZITADEL org lives inside. */
106
108
  authReady: boolean;
107
109
  /**
@@ -310,6 +312,10 @@ export interface OwnerSignIn {
310
312
  * is declared, absent while the owner step has recorded nothing.
311
313
  */
312
314
  firstOwnerUserId?: string | null;
315
+ /** The company's in-app login app (`<slug>-auth-login-sign-in`), once it is applied. Absent before. */
316
+ signInLoginAppId?: string;
317
+ /** True when the company has `signIn.inApp` and that login app is applied: Archon signs in on its own address. */
318
+ signInInApp?: true;
313
319
  }
314
320
  /** The owner company's own files host, as Archon's `FILES_*` env reads it. */
315
321
  export interface OwnerFiles {
@@ -7,6 +7,7 @@ import { oikosIngressApplied } from './oikos-ingress.js';
7
7
  import { ownHostnames } from './own-hostnames.js';
8
8
  import { OWN_SERVICES_APPLIED_SCOPE_KEY, orgTarget } from './own-services.js';
9
9
  import { JOBS_INTERNAL_URL_SCOPE_KEY, JOBS_SCHEDULER_KEY, PLATFORM_ACCESS_ZITADEL_APP_SCOPE_KEY, PLATFORM_ACCESS_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AI_KEY, PLATFORM_AI_TUNNEL_SCOPE_KEY, PLATFORM_ARCHON_APPLIED_BROKER_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_APP_SCOPE_KEY, PLATFORM_ARCHON_ZITADEL_PROJECT_SCOPE_KEY, PLATFORM_AUTH_BROKER_SCOPE_KEY, PLATFORM_AUTH_KEY, PLATFORM_EDGE_KEY, PLATFORM_FILES_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_FILES_INTERNAL_URL_SCOPE_KEY, PLATFORM_FILES_KEY, PLATFORM_MAIL_INTERNAL_URL_SCOPE_KEY, PLATFORM_MAIL_KEY, PLATFORM_MAIL_ZITADEL_PROJECT_SCOPE_KEY, platformMailMxHost, } from './platform-diff.js';
10
+ import { SIGN_IN_LOGIN_APP_SCOPE_KEY, inAppSignIn } from './sign-in.js';
10
11
  /**
11
12
  * #443: the no-domain company mail apex -- `<slug>.oikos.host`, the same
12
13
  * fixed suffix `agora.ts`'s `agoraHostnameFor`, `boule.ts`'s
@@ -159,6 +160,7 @@ edgePortPresent = false) {
159
160
  ...(coolifyAppApplied(platformRecordedIds, PLATFORM_AUTH_KEY)
160
161
  ? { platformAuthRecordedHost: platform.auth.hostname }
161
162
  : {}),
163
+ ...(platformRecordedIds[SIGN_IN_LOGIN_APP_SCOPE_KEY] ? { signInLoginReady: true } : {}),
162
164
  authReady,
163
165
  authIssuer: authReady ? `https://${platform.auth.hostname}` : null,
164
166
  jobsReady: jobsApplied && jobsUrl !== undefined && missingKeys.jobs === null,
@@ -314,6 +316,7 @@ export function ownerSignInFrom(owner, recorded) {
314
316
  if (!applied || applied !== ownHostnames(owner).auth)
315
317
  return null;
316
318
  const hostname = applied;
319
+ const signInLoginAppId = recorded[SIGN_IN_LOGIN_APP_SCOPE_KEY];
317
320
  return {
318
321
  issuerUrl: `https://${applied}`,
319
322
  organisationId,
@@ -321,6 +324,8 @@ export function ownerSignInFrom(owner, recorded) {
321
324
  bearerRef: orgTarget(owner).refs.instanceBearer,
322
325
  hostname,
323
326
  firstOwnerUserId: platformOwnerUserIdFrom(owner, recorded),
327
+ ...(signInLoginAppId ? { signInLoginAppId } : {}),
328
+ ...(signInLoginAppId && inAppSignIn(owner) ? { signInInApp: true } : {}),
324
329
  };
325
330
  }
326
331
  /**
@@ -408,6 +408,14 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
408
408
  * (or the runner was never applied).
409
409
  */
410
410
  refreshFilesShare(_platform: DeclaredPlatform, recordedIds: Readonly<Record<string, string>>): Promise<Record<string, string> | null>;
411
+ /** True while a company signs in on its apps' own addresses through the platform's login. */
412
+ private signInLoginWanted;
413
+ /**
414
+ * In-app sign-in: the login app that serves `/sign-in` is wanted and either not recorded, gone from
415
+ * Coolify, or holding a host table other than the login's. The step then plans again and repairs it.
416
+ * A Coolify read that fails counts as not drifted, with one fixed log line (the error can carry values).
417
+ */
418
+ private signInLoginDrifted;
411
419
  execute(action: StepAction, context: PlatformStepExecutionContext): Promise<{
412
420
  resourceId?: string;
413
421
  } | undefined>;
@@ -510,6 +518,13 @@ export interface AuthStackInput {
510
518
  defaultRedirectUri: string | null;
511
519
  /** The company whose owner is invited into its own sign-in org; null for a company's own stack. */
512
520
  ownerCompany: DeclaredOrg | null;
521
+ /**
522
+ * In-app sign-in: a second login app serves `/sign-in` next to the login app. Absent: none is made.
523
+ * `bindings` is its `LOGIN_HOST_BINDINGS` document; `null` copies the login app's live one.
524
+ */
525
+ signInLogin?: {
526
+ bindings: string | null;
527
+ };
513
528
  }
514
529
  /**
515
530
  * #168: makes `coolify:app` keyed {@link PLATFORM_AUTH_KEY} true -- a Coolify
@@ -890,7 +905,7 @@ export interface PlatformArchonDeps {
890
905
  access: PlatformAccessFactory;
891
906
  cloudflare: Pick<CloudflarePort, 'findZoneByApex'>;
892
907
  /** Must target the platform's own ZITADEL instance, or `zitadelForInstance` must be set. */
893
- zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp' | 'updateOidcAppIdTokenUserinfoAssertion'>;
908
+ zitadel: Pick<ZitadelPort, 'findOrCreateOrg' | 'findOrCreateProject' | 'createOidcApp' | 'listApps' | 'deleteApp' | 'updateOidcAppIdTokenUserinfoAssertion'> & Partial<Pick<ZitadelPort, 'ensureTrustedDomain' | 'deleteTrustedDomain' | 'updateOidcAppLoginBaseUri' | 'allowPerAppLoginBaseUri'>>;
894
909
  /**
895
910
  * {@link PlatformAuthDeps.zitadelForInstance}, for Archon's sign-in client:
896
911
  * the owner company's own ZITADEL, by its issuer and the held-key reference of