@wtfalch/foundry 0.54.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plan/diff.js CHANGED
@@ -18,7 +18,7 @@ import { hasWildcardGate, planGates } from './gates.js';
18
18
  import { appHostname, tunnelDnsHostnames } from './hostnames.js';
19
19
  import { APP_GONE_KIND } from './live.js';
20
20
  import { lockedHostnames } from './locked-hostnames.js';
21
- import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, agoraIssuerHostFor, clientOrganizationsState, companyLoginTunnelHost, customersClientOrganizations, inAppApps, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, pendingRemovalHosts, } from './login-host.js';
21
+ import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, agoraIssuerHostFor, clientOrganizationsState, companyLoginTunnelHost, customersClientOrganizations, inAppApps, loginBindingState, loginHostClientIds, loginHostServesAgora, loginHostnameFor, ownAuthApplied, ownCustomersAppState, pendingRemovalHosts, } from './login-host.js';
22
22
  import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, } from './mail-copy.js';
23
23
  import { mailStoreSwitchChange } from './mail-store.js';
24
24
  import { planMcp } from './mcp.js';
@@ -1394,8 +1394,13 @@ function planOwnLoginHost(org, recordedIds, gates) {
1394
1394
  const none = { step: null, blocked: null };
1395
1395
  const mapOwed = clientOrganizationsOwed(org, recordedIds, true);
1396
1396
  // Nothing wanted and nothing owed.
1397
- if (!inAppSignIn(org) && !recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] && !mapOwed)
1397
+ // A customers entry's app owes its base URI (the company's login host) until the marker names it.
1398
+ const customersOwed = Boolean(recordedIds[SIGN_IN_LOGIN_APP_SCOPE_KEY]) &&
1399
+ ownCustomersAppState(org, recordedIds) !==
1400
+ (recordedIds[OWN_CUSTOMERS_BASE_URI_SCOPE_KEY] ?? '');
1401
+ if (!inAppSignIn(org) && !recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] && !mapOwed && !customersOwed) {
1398
1402
  return none;
1403
+ }
1399
1404
  // The company's own sign-in is reported on its own line (`coolify:own-auth`).
1400
1405
  if (!ownAuthApplied(org, recordedIds))
1401
1406
  return none;
@@ -1411,6 +1416,7 @@ function planOwnLoginHost(org, recordedIds, gates) {
1411
1416
  if ((recordedIds[SIGN_IN_TRUSTED_SCOPE_KEY] ?? '') === targets.trusted &&
1412
1417
  (recordedIds[SIGN_IN_IN_APP_SCOPE_KEY] ?? '') === targets.active &&
1413
1418
  !mapOwed &&
1419
+ !customersOwed &&
1414
1420
  pendingRemovalHosts(recordedIds).length === 0) {
1415
1421
  return { step: null, blocked: waiting };
1416
1422
  }
@@ -37,7 +37,7 @@ import { GATE_CLAIM_NAME, gateHostnamesScopeKey, gateIdRecordKey, isBehindGate }
37
37
  import { IMAGE_HEALTHCHECK_SETTINGS, commandHealthCheckSettings, healthCheckSettings, wgetProbe, } from './health-check-settings.js';
38
38
  import { PROXY_LABEL_TOKEN_REF } from './held-key-refs.js';
39
39
  import { appHostname, cmsStudioHostnameFor, coolifyTunnelDomains, entryHostnames, siteHostnameFor, tunnelDnsHostnames, tunnelIngressHostnames, } from './hostnames.js';
40
- import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, clientOrganizationIds, clientOrganizationsState, customersClientOrganizations, customersEntryKeys, loginBindingState, loginHostClientIds, ownAuthApplied, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
40
+ import { LOGIN_BINDING_SCOPE_KEY, LOGIN_CLIENT_ORGS_SCOPE_KEY, LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, LOGIN_HOST_SHARED_SCOPE_KEY, OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, clientOrganizationIds, clientOrganizationsState, customersClientOrganizations, customersEntryKeys, loginBindingState, loginHostClientIds, ownAuthApplied, pendingRemovalHosts, pendingRemovalState, } from './login-host.js';
41
41
  import { MAIL_COPY_COMPLETED_SCOPE_KEY, MAIL_COPY_DONE_SCOPE_KEY, parseMailCopyCompleted, runMailCopyStep, } from './mail-copy.js';
42
42
  import { OWN_MAIL_SMTP_INTERNAL_SCOPE_KEY, OWN_MAIL_SMTP_PORT_SCOPE_KEY, isOwnMailSmtpPort, normalizeMailTarget, } from './mail-relay.js';
43
43
  import { applyAccessTokenLifetime, applyMcpAccess, applyMcpProject } from './mcp.js';
@@ -2268,6 +2268,17 @@ export class FoundryStepExecutor {
2268
2268
  customersAppIds.add(appId);
2269
2269
  }
2270
2270
  }
2271
+ // A company's own instance: a customers entry's app signs in on the company's login host. Without a base
2272
+ // URI of its own it falls back to the v1 login once in-app sign-in turns `loginV2.required` off.
2273
+ const ownCustomersAppIds = new Set();
2274
+ if (own) {
2275
+ for (const key of customersKeys) {
2276
+ const appId = recorded[`zitadel:app:${key}`];
2277
+ if (appId)
2278
+ ownCustomersAppIds.add(appId);
2279
+ }
2280
+ }
2281
+ const ownCustomersSet = [];
2271
2282
  if (projectId) {
2272
2283
  const listed = await this.ports.zitadel.listApps(zitadelOrgId, projectId);
2273
2284
  listingComplete = listed.complete;
@@ -2277,6 +2288,11 @@ export class FoundryStepExecutor {
2277
2288
  await setOidcAppLoginBaseUri(this.ports.zitadel, zitadelOrgId, projectId, app.id, sharedBaseUri);
2278
2289
  continue;
2279
2290
  }
2291
+ if (ownCustomersAppIds.has(app.id)) {
2292
+ await setOidcAppLoginBaseUri(this.ports.zitadel, zitadelOrgId, projectId, app.id, `https://${host}/`);
2293
+ ownCustomersSet.push(app.id);
2294
+ continue;
2295
+ }
2280
2296
  if (!boundAppIds.has(app.id))
2281
2297
  continue;
2282
2298
  // A company's own instance has the login host as its default; only the in-app apps move.
@@ -2317,9 +2333,11 @@ export class FoundryStepExecutor {
2317
2333
  for (const app of liveApps) {
2318
2334
  const uri = customersAppIds.has(app.id) && sharedBaseUri
2319
2335
  ? sharedBaseUri
2320
- : boundAppIds.has(app.id)
2321
- ? baseUriOf(app.id)
2322
- : app.loginBaseUri;
2336
+ : ownCustomersAppIds.has(app.id)
2337
+ ? `https://${host}/`
2338
+ : boundAppIds.has(app.id)
2339
+ ? baseUriOf(app.id)
2340
+ : app.loginBaseUri;
2323
2341
  if (typeof uri === 'string')
2324
2342
  currentLoginBaseUris.add(normalizedLoginBaseUri(uri));
2325
2343
  }
@@ -2413,6 +2431,10 @@ export class FoundryStepExecutor {
2413
2431
  if (orgsWritable) {
2414
2432
  context.recordScopeId(LOGIN_CLIENT_ORGS_SCOPE_KEY, clientOrganizationsState(wantedOrgs));
2415
2433
  }
2434
+ if (own) {
2435
+ // Written last: a failed base URI write leaves it behind and the step is planned again.
2436
+ context.recordScopeId(OWN_CUSTOMERS_BASE_URI_SCOPE_KEY, ownCustomersSet.sort().join(','));
2437
+ }
2416
2438
  context.recordScopeId(LOGIN_HOST_PENDING_REMOVAL_SCOPE_KEY, pendingRemovalState(stillPending));
2417
2439
  return { resourceId: host };
2418
2440
  }
@@ -184,6 +184,14 @@ export declare const LOGIN_CLIENT_ORGS_SCOPE_KEY = "login_client_orgs";
184
184
  * recorded. Staff clients are not here.
185
185
  */
186
186
  export declare function customersClientOrganizations(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): Record<string, string>;
187
+ /**
188
+ * Scope id holding the ZITADEL app ids of an own org's customers entries whose Login V2 base URI was set to the
189
+ * company's login host (sorted, joined by `,`). On the company's own instance an app with no base URI of its own
190
+ * falls back to the v1 login once in-app sign-in turns `loginV2.required` off. Absent or empty means none.
191
+ */
192
+ export declare const OWN_CUSTOMERS_BASE_URI_SCOPE_KEY = "own_customers_login_base_uri";
193
+ /** The recorded ZITADEL app ids of the org's customers entries, sorted and joined by `,` ('' when none). */
194
+ export declare function ownCustomersAppState(org: DeclaredOrg, recorded: Readonly<Record<string, string>>): string;
187
195
  /** {@link LOGIN_CLIENT_ORGS_SCOPE_KEY}'s value for a map. */
188
196
  export declare function clientOrganizationsState(map: Readonly<Record<string, string>>): string;
189
197
  /** The client ids a recorded {@link LOGIN_CLIENT_ORGS_SCOPE_KEY} value holds. */
@@ -211,6 +211,20 @@ export function customersClientOrganizations(org, recorded) {
211
211
  }
212
212
  return map;
213
213
  }
214
+ /**
215
+ * Scope id holding the ZITADEL app ids of an own org's customers entries whose Login V2 base URI was set to the
216
+ * company's login host (sorted, joined by `,`). On the company's own instance an app with no base URI of its own
217
+ * falls back to the v1 login once in-app sign-in turns `loginV2.required` off. Absent or empty means none.
218
+ */
219
+ export const OWN_CUSTOMERS_BASE_URI_SCOPE_KEY = 'own_customers_login_base_uri';
220
+ /** The recorded ZITADEL app ids of the org's customers entries, sorted and joined by `,` ('' when none). */
221
+ export function ownCustomersAppState(org, recorded) {
222
+ return [...customersEntryKeys(org)]
223
+ .map((key) => recorded[`zitadel:app:${key}`])
224
+ .filter((id) => Boolean(id))
225
+ .sort()
226
+ .join(',');
227
+ }
214
228
  /** {@link LOGIN_CLIENT_ORGS_SCOPE_KEY}'s value for a map. */
215
229
  export function clientOrganizationsState(map) {
216
230
  return Object.entries(map)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
5
5
  "repository": {
6
6
  "type": "git",