@wtfalch/foundry 0.57.0 → 0.59.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.
@@ -11,15 +11,27 @@ export declare function desktopCallbackUri(host: string): string;
11
11
  /** The URIs to add to an app's current list: current first, then each wanted one it lacks. */
12
12
  export declare function unionRedirectUris(current: readonly string[], wanted: readonly string[]): string[];
13
13
  /**
14
- * The ZITADEL app id of company app `appKey` once its desktop callback was
15
- * registered. `planDesktopCallbacks` names an app while its recorded id
16
- * differs, so an app made before the callback existed, or made again under a
17
- * new id, is repaired once.
14
+ * The redirect URIs a company web app signs in with: `/auth/callback` and the
15
+ * desktop callback on each of its hosts. Agora's own client holds only its
16
+ * `/callback`. `zitadelApp` creates the app with this list, and
17
+ * `zitadelDesktopCallback` adds what an existing app lacks.
18
+ */
19
+ export declare function signInRedirectUris(org: DeclaredOrg, appKey: string): string[];
20
+ /**
21
+ * The marker value for an app: its ZITADEL id and the URIs it must hold, in a
22
+ * fixed order. A changed hostname or a re-created app gives a different
23
+ * value, so the next plan names the app again.
24
+ */
25
+ export declare function signInCallbackAppliedValue(appId: string, uris: readonly string[]): string;
26
+ /**
27
+ * Marker of company app `appKey`: `signInCallbackAppliedValue` once its
28
+ * sign-in callbacks were registered. A marker holding only the app id (the
29
+ * desktop-only form of #339) differs, so such an app is repaired once.
18
30
  */
19
31
  export declare function zitadelAppDesktopCallbackAppliedScopeKey(appKey: string): string;
20
32
  /**
21
33
  * One `zitadel:desktop-callback` step names every live web sign-in app whose
22
- * recorded id differs from its marker. Plans nothing when the host's ZITADEL
34
+ * marker differs from the one its recorded id and wanted URIs give. Plans nothing when the host's ZITADEL
23
35
  * client lacks the two redirect URI calls (`wired`), before the org and
24
36
  * project are recorded, or while the platform's sign-in is not ready.
25
37
  */
@@ -1,3 +1,6 @@
1
+ import { agoraHostnameFor } from './agora.js';
2
+ import { bouleHostnameFor } from './boule.js';
3
+ import { appHostname, cmsStudioHostnameFor, entryHostnames } from './hostnames.js';
1
4
  /**
2
5
  * #339: the desktop app signs in through the company's web app, so every
3
6
  * `@wtfalch/auth` web client foundry makes registers this path beside
@@ -12,19 +15,48 @@ export function unionRedirectUris(current, wanted) {
12
15
  return [...new Set([...current, ...wanted])];
13
16
  }
14
17
  /**
15
- * The ZITADEL app id of company app `appKey` once its desktop callback was
16
- * registered. `planDesktopCallbacks` names an app while its recorded id
17
- * differs, so an app made before the callback existed, or made again under a
18
- * new id, is repaired once.
18
+ * The redirect URIs a company web app signs in with: `/auth/callback` and the
19
+ * desktop callback on each of its hosts. Agora's own client holds only its
20
+ * `/callback`. `zitadelApp` creates the app with this list, and
21
+ * `zitadelDesktopCallback` adds what an existing app lacks.
22
+ */
23
+ export function signInRedirectUris(org, appKey) {
24
+ if (appKey === 'agora')
25
+ return [`https://${agoraHostnameFor(org)}/callback`];
26
+ const authEntry = org.hosting.entries.find((e) => e.key === appKey && e.uses?.auth);
27
+ const hosts = authEntry
28
+ ? entryHostnames(org, authEntry)
29
+ : [
30
+ appKey === 'cms' && org.site.enabled
31
+ ? cmsStudioHostnameFor(org)
32
+ : appKey === 'boule'
33
+ ? bouleHostnameFor(org)
34
+ : appHostname(org),
35
+ ];
36
+ return [
37
+ ...hosts.map((host) => `https://${host}/auth/callback`),
38
+ ...hosts.map(desktopCallbackUri),
39
+ ];
40
+ }
41
+ /**
42
+ * The marker value for an app: its ZITADEL id and the URIs it must hold, in a
43
+ * fixed order. A changed hostname or a re-created app gives a different
44
+ * value, so the next plan names the app again.
45
+ */
46
+ export function signInCallbackAppliedValue(appId, uris) {
47
+ return `${appId} ${[...uris].sort().join(' ')}`;
48
+ }
49
+ /**
50
+ * Marker of company app `appKey`: `signInCallbackAppliedValue` once its
51
+ * sign-in callbacks were registered. A marker holding only the app id (the
52
+ * desktop-only form of #339) differs, so such an app is repaired once.
19
53
  */
20
54
  export function zitadelAppDesktopCallbackAppliedScopeKey(appKey) {
21
55
  return `zitadel_app_desktop_callback_applied.${appKey}`;
22
56
  }
23
- /** The fixed Agora client keeps its own `/callback`; it is not built on `@wtfalch/auth`. */
24
- const NO_DESKTOP_CALLBACK_APPS = new Set(['agora']);
25
57
  /**
26
58
  * One `zitadel:desktop-callback` step names every live web sign-in app whose
27
- * recorded id differs from its marker. Plans nothing when the host's ZITADEL
59
+ * marker differs from the one its recorded id and wanted URIs give. Plans nothing when the host's ZITADEL
28
60
  * client lacks the two redirect URI calls (`wired`), before the org and
29
61
  * project are recorded, or while the platform's sign-in is not ready.
30
62
  */
@@ -32,10 +64,7 @@ export function planDesktopCallbacks(org, live, recordedIds, plannedSteps, authR
32
64
  if (!wired || !authReady || !recordedIds.zitadel_org || !recordedIds.zitadel_project)
33
65
  return null;
34
66
  const apps = live
35
- .filter((r) => r.system === 'zitadel' &&
36
- r.kind === 'app' &&
37
- r.config.appType === 'web' &&
38
- !NO_DESKTOP_CALLBACK_APPS.has(r.key))
67
+ .filter((r) => r.system === 'zitadel' && r.kind === 'app' && r.config.appType === 'web')
39
68
  .map((r) => r.key)
40
69
  // An app this plan recreates gets its marker when it is made.
41
70
  .filter((appKey) => !plannedSteps.some((s) => s.action.system === 'zitadel' &&
@@ -44,14 +73,16 @@ export function planDesktopCallbacks(org, live, recordedIds, plannedSteps, authR
44
73
  s.action.desired.recreate === true))
45
74
  .filter((appKey) => {
46
75
  const appId = recordedIds[`zitadel:app:${appKey}`];
47
- return (Boolean(appId) && recordedIds[zitadelAppDesktopCallbackAppliedScopeKey(appKey)] !== appId);
76
+ return (Boolean(appId) &&
77
+ recordedIds[zitadelAppDesktopCallbackAppliedScopeKey(appKey)] !==
78
+ signInCallbackAppliedValue(appId, signInRedirectUris(org, appKey)));
48
79
  });
49
80
  if (apps.length === 0)
50
81
  return null;
51
82
  return {
52
83
  action: { system: 'zitadel', kind: 'desktop-callback', key: org.id, desired: { apps } },
53
84
  step: {
54
- description: `register the desktop sign-in callback for ${org.slug}'s sign-in apps (${apps.join(', ')})`,
85
+ description: `register the sign-in callbacks for ${org.slug}'s sign-in apps (${apps.join(', ')})`,
55
86
  destroys: false,
56
87
  migrates: false,
57
88
  },
@@ -2212,20 +2212,16 @@ export declare class FoundryStepExecutor implements StepExecutor, StepDestroyer
2212
2212
  */
2213
2213
  private zitadelPostLogoutUris;
2214
2214
  /**
2215
- * #339: registers `/auth/desktop/callback` on each live web sign-in app the
2216
- * planner named, keeping every redirect URI the app already holds (the
2217
- * union of the current list and the wanted one; a write only when the
2218
- * union is larger). An app's marker (its ZITADEL app id) is recorded and
2215
+ * #339: registers the sign-in callbacks (`signInRedirectUris`: `/auth/callback`
2216
+ * and `/auth/desktop/callback` per host, Agora's `/callback`) on each live web
2217
+ * sign-in app the planner named, keeping every redirect URI the app already
2218
+ * holds (the union of the current list and the wanted ones; a write only when
2219
+ * the union is larger). An app's marker (its app id and wanted URIs) is recorded and
2219
2220
  * persisted only after its read and write resolved, so a failure leaves no
2220
2221
  * marker and the next plan names the app again. The error names ids only,
2221
2222
  * never the cause.
2222
2223
  */
2223
2224
  private zitadelDesktopCallback;
2224
- /**
2225
- * The hostnames a company web app signs in at: the same rule `zitadelApp`
2226
- * uses for the redirect URIs it creates the app with.
2227
- */
2228
- private signInHostnames;
2229
2225
  /**
2230
2226
  * `plan/diff.ts`'s `planLoginHost` (#267): adds the org's own company
2231
2227
  * login host (`action.desired.host`) to the ZITADEL instance's trusted
@@ -22,7 +22,7 @@ import { previewsOn } from './coolify-previews.js';
22
22
  import { CORE_FILES_COLLECTION_NAME, FILES_CORE_COLLECTION_SCOPE_KEY, ZITADEL_FILES_PROJECT_SCOPE_KEY, } from './core-files.js';
23
23
  import { waitForHealthyVersion, waitForNewDeployment, } from './deploy-health.js';
24
24
  import { DESKTOP_AUTH_APP_SCOPE_KEY, DESKTOP_AUTH_DEPLOYED_SCOPE_KEY, DESKTOP_AUTH_DOCKERFILE, DESKTOP_AUTH_HOME, DESKTOP_AUTH_ORIGIN_SCOPE_KEY, DESKTOP_AUTH_READY_PATH, DESKTOP_AUTH_READY_SCOPE_KEY, DESKTOP_AUTH_REDIRECT_SCOPE_KEY, DESKTOP_AUTH_REVISION_SCOPE_KEY, desktopAuthCallbackUri, desktopAuthEnv, desktopAuthInputs, desktopAuthRedirectUnsupported, desktopAuthRevision, } from './desktop-auth.js';
25
- import { desktopCallbackUri, unionRedirectUris, zitadelAppDesktopCallbackAppliedScopeKey, } from './desktop-callback.js';
25
+ import { signInCallbackAppliedValue, signInRedirectUris, unionRedirectUris, zitadelAppDesktopCallbackAppliedScopeKey, } from './desktop-callback.js';
26
26
  import { PLATFORM_NETWORK, companyNetworkName, ensureDestination, findDestination, onPlatformNetwork, placed, placedByScope, } from './destinations.js';
27
27
  import { DEFAULT_AI_LIMIT_MICROS } from './diff.js';
28
28
  import { createEdgeAccess, isNotFound as isEdgeNotFound, } from './edge-access.js';
@@ -1967,10 +1967,11 @@ export class FoundryStepExecutor {
1967
1967
  return {};
1968
1968
  }
1969
1969
  /**
1970
- * #339: registers `/auth/desktop/callback` on each live web sign-in app the
1971
- * planner named, keeping every redirect URI the app already holds (the
1972
- * union of the current list and the wanted one; a write only when the
1973
- * union is larger). An app's marker (its ZITADEL app id) is recorded and
1970
+ * #339: registers the sign-in callbacks (`signInRedirectUris`: `/auth/callback`
1971
+ * and `/auth/desktop/callback` per host, Agora's `/callback`) on each live web
1972
+ * sign-in app the planner named, keeping every redirect URI the app already
1973
+ * holds (the union of the current list and the wanted ones; a write only when
1974
+ * the union is larger). An app's marker (its app id and wanted URIs) is recorded and
1974
1975
  * persisted only after its read and write resolved, so a failure leaves no
1975
1976
  * marker and the next plan names the app again. The error names ids only,
1976
1977
  * never the cause.
@@ -1991,7 +1992,7 @@ export class FoundryStepExecutor {
1991
1992
  const appId = recorded[`zitadel:app:${appKey}`];
1992
1993
  if (!appId)
1993
1994
  continue;
1994
- const wanted = this.signInHostnames(org, appKey).map(desktopCallbackUri);
1995
+ const wanted = signInRedirectUris(org, appKey);
1995
1996
  try {
1996
1997
  const current = await read(zitadelOrgId, projectId, appId);
1997
1998
  const union = unionRedirectUris(current, wanted);
@@ -1999,27 +2000,13 @@ export class FoundryStepExecutor {
1999
2000
  await write(zitadelOrgId, projectId, appId, union);
2000
2001
  }
2001
2002
  catch {
2002
- throw new Error(`zitadel:desktop-callback for ${org.slug}: registering the desktop callback failed for app '${appKey}' (${appId})`);
2003
+ throw new Error(`zitadel:desktop-callback for ${org.slug}: registering the sign-in callbacks failed for app '${appKey}' (${appId})`);
2003
2004
  }
2004
- context.recordScopeId(zitadelAppDesktopCallbackAppliedScopeKey(appKey), appId);
2005
+ context.recordScopeId(zitadelAppDesktopCallbackAppliedScopeKey(appKey), signInCallbackAppliedValue(appId, wanted));
2005
2006
  await context.persistScopeIds();
2006
2007
  }
2007
2008
  return {};
2008
2009
  }
2009
- /**
2010
- * The hostnames a company web app signs in at: the same rule `zitadelApp`
2011
- * uses for the redirect URIs it creates the app with.
2012
- */
2013
- signInHostnames(org, appKey) {
2014
- const authEntry = org.hosting.entries.find((e) => e.key === appKey && e.uses?.auth);
2015
- if (appKey === 'cms' && org.site.enabled)
2016
- return [cmsHostnameFor(org)];
2017
- if (appKey === 'boule')
2018
- return [bouleHostnameFor(org)];
2019
- if (authEntry)
2020
- return entryHostnames(org, authEntry);
2021
- return [appHostname(org)];
2022
- }
2023
2010
  /**
2024
2011
  * `plan/diff.ts`'s `planLoginHost` (#267): adds the org's own company
2025
2012
  * login host (`action.desired.host`) to the ZITADEL instance's trusted
@@ -2701,55 +2688,24 @@ export class FoundryStepExecutor {
2701
2688
  await this.ports.deliverSecret(`zitadel:app:${org.slug}:${action.key}`, created.clientSecret);
2702
2689
  return { resourceId: created.appId };
2703
2690
  }
2704
- // #139: the fixed `cms` client (`diff.ts`'s `planCmsClient`) signs
2705
- // people into the cms studio at `cmsHostnameFor(org)` -- `cms.<domain>`,
2706
- // or `<slug>-cms.oikos.host` for a company without a domain -- not the
2707
- // org's own app hostname. #164: the fixed `agora` client (`diff.ts`'s
2708
- // `planAgoraClient`) is the same shape, always -- there is no
2709
- // `org.site.enabled`-style guard to check, since Agora is always on.
2710
2691
  // The Agora client stays `appType: 'web'`: providers' `createOidcApp`
2711
2692
  // makes every non-native app a public client (authorization code + PKCE,
2712
2693
  // `OIDC_AUTH_METHOD_TYPE_NONE`, no secret), which is what Agora's browser
2713
2694
  // SPA needs. Providers cannot create ZITADEL's `USER_AGENT` app kind.
2714
- // #161: the fixed `boule` client signs people into the org's Boule at its
2715
- // own hostname, on the standard `/auth/callback` path (Boule is built on
2716
- // `@wtfalch/auth` like every other web app here).
2717
- const hostname = isCmsClient
2718
- ? cmsHostnameFor(org)
2719
- : isAgoraClient
2720
- ? agoraHostnameFor(org)
2721
- : isBouleClient
2722
- ? bouleHostnameFor(org)
2723
- : appHostname(org);
2724
2695
  // Native apps sign in via device code, with no redirect of their own
2725
2696
  // (auth/scripts/provision.mjs:475-497, prior art cited in the decisions
2726
2697
  // file) -- empty arrays rather than a web app's callback/logout URLs.
2727
- // #164: Agora's own callback path is `/callback`, not `/auth/callback`
2728
- // (agora/apps/agora/README.md, "Deploying an Agora": "The issuer's OAuth
2729
- // client needs `https://<agora address>/callback` as a redirect URI") --
2730
- // Agora is not built on `@wtfalch/auth`'s `NextAuth` mount the way every
2731
- // other web app this file provisions is.
2732
- const redirectUri = isAgoraClient
2733
- ? `https://${hostname}/callback`
2734
- : `https://${hostname}/auth/callback`;
2735
- // An entry with `uses.auth` signs in at its own exact hostnames, one pair
2736
- // of addresses each; a `*` adds none (`entryHostnames`).
2737
- const hosts = authEntry ? entryHostnames(org, authEntry) : null;
2698
+ // `signInRedirectUris` holds each web app's hosts and callback paths (the
2699
+ // cms, Agora, Boule and entry hostnames; Agora's `/callback`).
2738
2700
  const postLogoutUris = appType === 'native' ? [] : companyAppPostLogoutUris(org, action.key);
2739
- // #339: every `@wtfalch/auth` web client also takes the desktop callback (Agora's own client does not).
2740
- const desktopCallbacks = appType === 'web' && !isAgoraClient ? (hosts ?? [hostname]).map(desktopCallbackUri) : [];
2701
+ // The same rule the repair step uses (`signInRedirectUris`).
2702
+ const signInUris = appType === 'web' ? signInRedirectUris(org, action.key) : [];
2741
2703
  const created = await this.ports.zitadel.createOidcApp(zitadelOrgId, projectId, {
2742
2704
  name: action.key,
2743
2705
  appType,
2744
2706
  redirectUris: appType === 'native'
2745
2707
  ? []
2746
- : hosts
2747
- ? [
2748
- ...hosts.map((host) => `https://${host}/auth/callback`),
2749
- ...desktopCallbacks,
2750
- ...(authEntry?.uses?.auth?.mcp ? [mcpCallbackUri(org)] : []),
2751
- ]
2752
- : [redirectUri, ...desktopCallbacks],
2708
+ : [...signInUris, ...(authEntry?.uses?.auth?.mcp ? [mcpCallbackUri(org)] : [])],
2753
2709
  postLogoutRedirectUris: postLogoutUris,
2754
2710
  ...(authEntry?.uses?.auth?.mcp ? { accessTokenType: 'jwt' } : {}),
2755
2711
  });
@@ -2769,8 +2725,8 @@ export class FoundryStepExecutor {
2769
2725
  if (appType === 'web') {
2770
2726
  context.recordScopeId(zitadelAppPostLogoutAppliedScopeKey(action.key), postLogoutAppliedValue(created.appId, postLogoutUris));
2771
2727
  }
2772
- if (desktopCallbacks.length > 0) {
2773
- context.recordScopeId(zitadelAppDesktopCallbackAppliedScopeKey(action.key), created.appId);
2728
+ if (appType === 'web') {
2729
+ context.recordScopeId(zitadelAppDesktopCallbackAppliedScopeKey(action.key), signInCallbackAppliedValue(created.appId, signInUris));
2774
2730
  }
2775
2731
  return { resourceId: created.appId };
2776
2732
  }
@@ -107,6 +107,11 @@ export interface WorkerAppUpload {
107
107
  migrations?: WorkerAppMigrations;
108
108
  /** Durable Object classes the upload marks container-enabled; absent when the entry declares no container. */
109
109
  containers?: string[];
110
+ /** Workers Logs: always on, every request sampled. The adapter sends it as the upload metadata's `observability`. */
111
+ observability: {
112
+ enabled: true;
113
+ head_sampling_rate: 1;
114
+ };
110
115
  }
111
116
  export interface WorkerAppContainer {
112
117
  className: string;
@@ -526,6 +526,7 @@ export async function applyWorkerApp(deps, org, action, context) {
526
526
  ? [{ type: 'hyperdrive', name: WORKER_APP_HYPERDRIVE_BINDING, id: hyperdriveId }]
527
527
  : []),
528
528
  ],
529
+ observability: { enabled: true, head_sampling_rate: 1 },
529
530
  ...(migrations ? { migrations } : {}),
530
531
  ...(want.containers.length > 0
531
532
  ? { containers: want.containers.map((c) => c.className) }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.57.0",
3
+ "version": "0.59.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",