@wtfalch/foundry 0.16.12 → 0.16.13

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.
@@ -108,7 +108,7 @@ export function guardStepDeps(executor, redactor, deliverSecret, heldKeys) {
108
108
  // #201: ai's own default minter needs heldKeys too, to re-sign a mint
109
109
  // request against its signing key when an earlier run pinned it but
110
110
  // crashed before the management key itself was minted and delivered.
111
- if (key === 'archon' || key === 'ai' || 'heldKeys' in copy)
111
+ if (key === 'archon' || key === 'ai' || key === 'auth' || 'heldKeys' in copy)
112
112
  copy.heldKeys = heldKeys;
113
113
  out[key] = copy;
114
114
  }
@@ -1021,7 +1021,7 @@ function planSfu(platform) {
1021
1021
  * #211 round 2: a recorded broker id is not the same as a *deployed* broker
1022
1022
  * ({@link PLATFORM_AUTH_BROKER_DEPLOYED_SCOPE_KEY}) -- `provisionPlatformAuth`
1023
1023
  * creates and records the app before it has `ZITADEL_SERVICE_USER_TOKEN`/
1024
- * `RESEND_API_TOKEN` to deploy it with (#209, #210). So this still plans the
1024
+ * `LETTERMINT_API_TOKEN` to deploy it with (#209, #210). So this still plans the
1025
1025
  * step again while the broker sits undeployed: `executorBlocked` (from
1026
1026
  * `authBrokerBlockedReason`) reports it blocked, by name, for as long as the
1027
1027
  * inputs stay missing, and the moment they arrive the same replanned step
@@ -28,6 +28,19 @@ export interface PlatformAuthDeps {
28
28
  */
29
29
  zitadelForInstance?: (issuerUrl: string) => PlatformAuthDeps['zitadel'];
30
30
  deliverSecret: FoundryStepExecutorPorts['deliverSecret'];
31
+ /**
32
+ * #210: reads back the broker's Lettermint mail key, held under
33
+ * {@link PLATFORM_AUTH_BROKER_MAIL_KEY_REF}. Never minted here -- unlike
34
+ * the login-client token above, foundry has no Lettermint account of its
35
+ * own to mint one from, so this only ever reads back a value a human (or
36
+ * Archon's own onboarding) put there directly once Lettermint issued it.
37
+ * Absent, or with nothing held under that ref yet, `LETTERMINT_API_TOKEN`
38
+ * stays a gap `missingBrokerInputs` reports by name, the same "blocked,
39
+ * not failing" every other unmet broker input already follows.
40
+ */
41
+ heldKeys?: {
42
+ resolve(ref: string): Promise<string | null>;
43
+ };
31
44
  /** Clock, sleep and fetch for the readiness wait and delivery retries; real ones by default. */
32
45
  runtime?: PlatformAuthRuntime;
33
46
  }
@@ -234,6 +247,15 @@ export declare class FoundryPlatformStepExecutor implements PlatformStepExecutor
234
247
  * key delivered under `zitadel:platform:admin`.
235
248
  */
236
249
  export declare const PLATFORM_AUTH_INSTANCE_BEARER_REF = "zitadel:platform:instanceBearer";
250
+ /**
251
+ * #210: the ref the broker's Lettermint mail key is held under. Unlike every
252
+ * other `_REF` in this file, nothing here ever delivers a value under it --
253
+ * {@link PlatformAuthDeps.heldKeys} only reads it back, once a human (or
254
+ * Archon's own onboarding) puts it there directly with the key Lettermint
255
+ * issued, on one allowed to send from every app's domain
256
+ * (`wtfalch/auth/docs/deploy.md`).
257
+ */
258
+ export declare const PLATFORM_AUTH_BROKER_MAIL_KEY_REF = "lettermint:platform:apiToken";
237
259
  /**
238
260
  * The fixed description Stalwart's admin API key is minted under
239
261
  * (`StalwartClient.createApiKey`'s own signature returns only the secret,
@@ -591,13 +613,14 @@ export declare function createAiManagementKeyMinter(options: {
591
613
  }): AiManagementKeyMinter;
592
614
  /**
593
615
  * #211: names of the envs the platform's sign-in broker (`wtfalch/auth`'s
594
- * `broker/server.ts`) refuses to start without, that `provisionPlatformAuth`
595
- * itself has no way to supply yet -- `ZITADEL_SERVICE_USER_TOKEN`/`_FILE`
596
- * (#209 mints and pushes this once it lands) and `RESEND_API_TOKEN` (#210,
597
- * blocked on `wtfalch/auth#138`). Read from the broker's own live env, not a
598
- * recorded scope id: whichever of #209/#210 lands first (or a human, setting
599
- * either directly on the app) is enough to clear that one input, in either
600
- * order, with no coupling to this step's own execution.
616
+ * `broker/server.ts`) refuses to start without -- `ZITADEL_SERVICE_USER_TOKEN`/
617
+ * `_FILE` (#209 mints and pushes this once it lands) and `LETTERMINT_API_TOKEN`
618
+ * (#210: `wtfalch/auth#138` moved the broker's account mail onto
619
+ * `@wtfalch/mailer`'s Lettermint transport, so this is the env name the
620
+ * broker actually reads now, not `RESEND_API_TOKEN`). Read from the broker's
621
+ * own live env, not a recorded scope id: whichever of #209/#210 lands first
622
+ * (or a human, setting either directly on the app) is enough to clear that
623
+ * one input, in either order, with no coupling to this step's own execution.
601
624
  *
602
625
  * #211 round 3: `AUTH_APPS` is a third required input -- the broker's own
603
626
  * `parseApps` refuses to start with no `AUTH_APPS` set or an empty array
@@ -577,6 +577,15 @@ const PLATFORM_AUTH_ORG_NAME = 'foundry';
577
577
  */
578
578
  export const PLATFORM_AUTH_INSTANCE_BEARER_REF = 'zitadel:platform:instanceBearer';
579
579
  const PLATFORM_AUTH_BEARER_USER_NAME = 'foundry-instance-owner';
580
+ /**
581
+ * #210: the ref the broker's Lettermint mail key is held under. Unlike every
582
+ * other `_REF` in this file, nothing here ever delivers a value under it --
583
+ * {@link PlatformAuthDeps.heldKeys} only reads it back, once a human (or
584
+ * Archon's own onboarding) puts it there directly with the key Lettermint
585
+ * issued, on one allowed to send from every app's domain
586
+ * (`wtfalch/auth/docs/deploy.md`).
587
+ */
588
+ export const PLATFORM_AUTH_BROKER_MAIL_KEY_REF = 'lettermint:platform:apiToken';
580
589
  /**
581
590
  * Retries `action` up to `attempts` times, sleeping increasingly between
582
591
  * tries -- the same shape the admin machine user's own delivery retry (just
@@ -811,10 +820,15 @@ async function liveLoginClientTokenHash(coolify, appId) {
811
820
  * `IAM_LOGIN_CLIENT` machine user (never the admin one, which is
812
821
  * `IAM_OWNER`) and delivers its token to both apps, once the broker's
813
822
  * other required inputs are already met.
814
- * - The broker never receives `RESEND_API_TOKEN` either, so it will not start
815
- * on `AUTH_APPS` alone. `wtfalch/auth#138` moves the broker's account mail
816
- * onto held keys and `@wtfalch/mailer`; until that lands there is no held
817
- * key to read here, so this stays a plain gap rather than a wired value.
823
+ * - ~~The broker never receives `RESEND_API_TOKEN` either, so it will not
824
+ * start on `AUTH_APPS` alone.~~ Closed by #210, once `wtfalch/auth#138`
825
+ * moved the broker's account mail onto `@wtfalch/mailer`'s Lettermint
826
+ * transport: this function now reads the key back from
827
+ * {@link PlatformAuthDeps.heldKeys} under
828
+ * {@link PLATFORM_AUTH_BROKER_MAIL_KEY_REF} and pushes it as
829
+ * `LETTERMINT_API_TOKEN`, once a human (or Archon's own onboarding) has
830
+ * put one there. Still a gap until then, the same "blocked, not failing"
831
+ * way an unset `AUTH_APPS` already is.
818
832
  * - `AUTH_APPS` starts unset: the broker has nothing to serve until a
819
833
  * platform app registers with it ({@link provisionPlatformArchon} is the
820
834
  * first), so it will not start (`parseApps` throws on an empty list) until
@@ -1043,14 +1057,24 @@ async function provisionPlatformAuth(context, deps) {
1043
1057
  { key: 'OTEL_NODE_RESOURCE_DETECTORS', value: 'none' },
1044
1058
  { key: 'API_CACHE_CONFIG', value: '{"defaultMinutes":2,"longMinutes":5}' },
1045
1059
  ];
1046
- // `AUTH_APPS`, `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` and `RESEND_API_TOKEN`
1047
- // are not pushed here -- this function's own doc comment, "Known gaps".
1060
+ // #210: read back now, not inside the deploy-decision block below -- this
1061
+ // push has to land before `missingBrokerInputs` is read a few lines down,
1062
+ // the same way `envsDiffer`/`bulkUpsertEnvs` below already do for every
1063
+ // other broker env, so a single apply that finds the key already held can
1064
+ // also see `LETTERMINT_API_TOKEN` as met and deploy in that same run.
1065
+ const heldMailKey = await deps.heldKeys?.resolve(PLATFORM_AUTH_BROKER_MAIL_KEY_REF);
1066
+ // `AUTH_APPS` and `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` are not pushed here
1067
+ // -- this function's own doc comment, "Known gaps". `LETTERMINT_API_TOKEN`
1068
+ // is, once held (#210): pushing it costs nothing even while the broker
1069
+ // still cannot start on the other two, and the deploy decision below stays
1070
+ // gated on `missingBrokerInputs` regardless.
1048
1071
  const brokerEnvs = [
1049
1072
  {
1050
1073
  key: 'ZITADEL_API_URL',
1051
1074
  value: `http://${PLATFORM_AUTH_ZITADEL_INTERNAL_NAME}:${AUTH_ZITADEL_PORT}`,
1052
1075
  },
1053
1076
  { key: 'ZITADEL_DOMAIN', value: hostname },
1077
+ ...(heldMailKey ? [{ key: 'LETTERMINT_API_TOKEN', value: heldMailKey }] : []),
1054
1078
  ];
1055
1079
  // Compared before the write: an env change only reaches a running
1056
1080
  // container on its next deploy (`diff.ts`'s `coolifyCalls` follows the
@@ -1074,11 +1098,11 @@ async function provisionPlatformAuth(context, deps) {
1074
1098
  await deps.coolify.deploy(loginAppId);
1075
1099
  }
1076
1100
  // #211: the broker's own `server.ts` refuses to start without
1077
- // `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` and `RESEND_API_TOKEN` -- deploying
1078
- // it without them only produces a container that crash-loops behind
1079
- // `/api`. The app is still created and recorded above either way, so #209
1080
- // and #210 (or a human, directly) have something to push the env onto;
1081
- // only the deploy waits.
1101
+ // `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` and `LETTERMINT_API_TOKEN` --
1102
+ // deploying it without them only produces a container that crash-loops
1103
+ // behind `/api`. The app is still created and recorded above either way,
1104
+ // so #209 and #210 (or a human, directly) have something to push the env
1105
+ // onto; only the deploy waits.
1082
1106
  // #211 round 5: `brokerDeployNeeded` is its own reason to deploy (not only
1083
1107
  // alongside justCreated/running/envsChanged, none of which this function's
1084
1108
  // own `brokerEnvs` write above ever touches `AUTH_APPS`): it is what
@@ -1262,7 +1286,7 @@ async function provisionPlatformAuth(context, deps) {
1262
1286
  // envs, then redeploys both.
1263
1287
  //
1264
1288
  // Deferred, not blocked, while the broker's other required inputs
1265
- // (`RESEND_API_TOKEN`, `AUTH_APPS`) are still missing (composing with
1289
+ // (`LETTERMINT_API_TOKEN`, `AUTH_APPS`) are still missing (composing with
1266
1290
  // #211's own gating): minting and delivering here would only redeploy a
1267
1291
  // broker that still refuses to start on the other missing input, spending
1268
1292
  // a mint and two deploys on nothing this apply can fix. `blockedSteps`'s
@@ -1967,12 +1991,12 @@ async function mailBlockedReason(platform, mail, recordedIds) {
1967
1991
  }
1968
1992
  /**
1969
1993
  * #211: the broker cannot start without `ZITADEL_SERVICE_USER_TOKEN`/`_FILE`,
1970
- * `RESEND_API_TOKEN` and `AUTH_APPS`. `provisionPlatformAuth` already skips
1971
- * deploying the broker while any is missing ({@link missingBrokerInputs});
1972
- * this is the same check surfaced through `blockedSteps`, so a plan says
1973
- * why, by name, rather than the broker simply sitting undeployed with no
1974
- * explanation. `null` while there is no broker app id recorded yet (nothing
1975
- * to check) or every input is present.
1994
+ * `LETTERMINT_API_TOKEN` and `AUTH_APPS`. `provisionPlatformAuth` already
1995
+ * skips deploying the broker while any is missing
1996
+ * ({@link missingBrokerInputs}); this is the same check surfaced through
1997
+ * `blockedSteps`, so a plan says why, by name, rather than the broker simply
1998
+ * sitting undeployed with no explanation. `null` while there is no broker
1999
+ * app id recorded yet (nothing to check) or every input is present.
1976
2000
  *
1977
2001
  * #211 round 3: wraps the live read the way {@link mailBlockedReason} wraps
1978
2002
  * its own Coolify call -- a transient `listEnvs` failure must report a
@@ -1986,6 +2010,21 @@ async function mailBlockedReason(platform, mail, recordedIds) {
1986
2010
  * go fix an entry, not merely wait for one of #209/#210/Archon's own
1987
2011
  * registration to supply one), and reporting it identically hid which one
1988
2012
  * was true.
2013
+ *
2014
+ * #210: `LETTERMINT_API_TOKEN` joins `ZITADEL_SERVICE_USER_TOKEN` in being
2015
+ * excluded from `otherInputsMissing` -- but only while `auth.heldKeys`
2016
+ * actually resolves {@link PLATFORM_AUTH_BROKER_MAIL_KEY_REF} right now.
2017
+ * `planAuth`'s own "loop fix" stops planning this step the moment every
2018
+ * `otherInputsMissing` entry looks like something only another run can
2019
+ * supply; excluding `LETTERMINT_API_TOKEN` unconditionally (the way
2020
+ * `ZITADEL_SERVICE_USER_TOKEN` is, since foundry can always eventually mint
2021
+ * that one) would keep the step planned, and `provisionPlatformAuth`
2022
+ * running its full body, forever on a host that never wires `heldKeys` or
2023
+ * whose operator has not put a Lettermint key there yet -- exactly the
2024
+ * wasteful-replan bug #211 round 5 fixed for the broker deploy itself. Once
2025
+ * the key *is* held, excluding it lets this run's `provisionPlatformAuth`
2026
+ * push it (see that function's own `heldMailKey` read) and clears the gap
2027
+ * for good.
1989
2028
  */
1990
2029
  async function authBrokerBlockedReason(auth, recordedIds) {
1991
2030
  const brokerAppId = recordedIds[PLATFORM_AUTH_BROKER_SCOPE_KEY];
@@ -1993,9 +2032,11 @@ async function authBrokerBlockedReason(auth, recordedIds) {
1993
2032
  return { reason: null, otherInputsMissing: [] };
1994
2033
  let missing;
1995
2034
  let authAppsRaw;
2035
+ let heldMailKey;
1996
2036
  try {
1997
2037
  missing = await missingBrokerInputs(auth.coolify, brokerAppId);
1998
2038
  authAppsRaw = await currentEnvValue(auth.coolify, brokerAppId, 'AUTH_APPS');
2039
+ heldMailKey = (await auth.heldKeys?.resolve(PLATFORM_AUTH_BROKER_MAIL_KEY_REF)) ?? null;
1999
2040
  }
2000
2041
  catch {
2001
2042
  return {
@@ -2007,13 +2048,19 @@ async function authBrokerBlockedReason(auth, recordedIds) {
2007
2048
  }
2008
2049
  // #216 round 5: what `provisionPlatformAuth`'s own `loginClientOtherInputsMissing`
2009
2050
  // gate is -- see `planAuth`'s own doc comment for why it needs this too.
2010
- const otherInputsMissing = missing.filter((m) => m !== 'ZITADEL_SERVICE_USER_TOKEN');
2051
+ // #210: and `LETTERMINT_API_TOKEN`, once held -- see this function's own
2052
+ // doc comment.
2053
+ const otherInputsMissing = missing.filter((m) => m !== 'ZITADEL_SERVICE_USER_TOKEN' && !(m === 'LETTERMINT_API_TOKEN' && heldMailKey));
2011
2054
  if (missing.length === 0)
2012
2055
  return { reason: null, otherInputsMissing };
2056
+ // #210: say where the operator seals the Lettermint key, not only its env name.
2057
+ const named = (names) => names.map((m) => m === 'LETTERMINT_API_TOKEN'
2058
+ ? `LETTERMINT_API_TOKEN (seal it under the held-key ref ${PLATFORM_AUTH_BROKER_MAIL_KEY_REF})`
2059
+ : m);
2013
2060
  if (authAppsProblem(authAppsRaw, brokerAppId) === 'malformed') {
2014
2061
  const otherMissing = missing.filter((m) => m !== 'AUTH_APPS');
2015
2062
  const otherSuffix = otherMissing.length > 0
2016
- ? `, and it has no ${otherMissing.join(' and no ')} yet (#209, #210)`
2063
+ ? `, and it has no ${named(otherMissing).join(' and no ')} yet (#209, #210)`
2017
2064
  : '';
2018
2065
  return {
2019
2066
  reason: `the platform's sign-in broker ${brokerAppId} is blocked, not deployed: its AUTH_APPS is malformed${otherSuffix}`,
@@ -2024,7 +2071,7 @@ async function authBrokerBlockedReason(auth, recordedIds) {
2024
2071
  // those issues when one of them is actually among what's missing.
2025
2072
  const suffix = missing.some((m) => m !== 'AUTH_APPS') ? ' (#209, #210)' : '';
2026
2073
  return {
2027
- reason: `the platform's sign-in broker is created but not deployed, blocked: it has no ${missing.join(' and no ')} yet${suffix}`,
2074
+ reason: `the platform's sign-in broker is created but not deployed, blocked: it has no ${named(missing).join(' and no ')} yet${suffix}`,
2028
2075
  otherInputsMissing,
2029
2076
  };
2030
2077
  }
@@ -2197,8 +2244,8 @@ export const PLATFORM_ARCHON_CLIENT_ID_SCOPE_KEY = 'zitadel_platform_archon_clie
2197
2244
  * `provisionPlatformAuth` guarantees by the time this step is reachable (its
2198
2245
  * own resourceId records only once the broker app exists too). The broker
2199
2246
  * will not actually be able to serve a request with this key until
2200
- * `provisionPlatformAuth`'s own "Known gaps" (`ZITADEL_SERVICE_USER_TOKEN`,
2201
- * `RESEND_API_TOKEN`) are closed; wiring the key here is still correct and
2247
+ * `provisionPlatformAuth`'s own `ZITADEL_SERVICE_USER_TOKEN` is minted and
2248
+ * `LETTERMINT_API_TOKEN` is held; wiring the key here is still correct and
2202
2249
  * needed so the two apps agree the moment the broker can run.
2203
2250
  *
2204
2251
  * Known gaps, left for review:
@@ -2486,8 +2533,8 @@ async function provisionPlatformArchon(context, deps) {
2486
2533
  await deps.coolify.bulkUpsertEnvs(brokerAppId, desiredAuthApps);
2487
2534
  }
2488
2535
  // #211 round 2: deploying the broker before its own `server.ts` can start
2489
- // (missing `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` or `RESEND_API_TOKEN`, #209,
2490
- // #210) only produces a container that crash-loops behind `/api` -- the
2536
+ // (missing `ZITADEL_SERVICE_USER_TOKEN`/`_FILE` or `LETTERMINT_API_TOKEN`,
2537
+ // #209, #210) only produces a container that crash-loops behind `/api` -- the
2491
2538
  // same rule `provisionPlatformAuth` follows for its own deploy of this
2492
2539
  // app. The env is still written either way, so the broker serves the new
2493
2540
  // registration the moment it is next deployed, by either step.
@@ -3987,13 +4034,14 @@ async function findOrCreatePlatformApp(coolify, name, environment, create) {
3987
4034
  }
3988
4035
  /**
3989
4036
  * #211: names of the envs the platform's sign-in broker (`wtfalch/auth`'s
3990
- * `broker/server.ts`) refuses to start without, that `provisionPlatformAuth`
3991
- * itself has no way to supply yet -- `ZITADEL_SERVICE_USER_TOKEN`/`_FILE`
3992
- * (#209 mints and pushes this once it lands) and `RESEND_API_TOKEN` (#210,
3993
- * blocked on `wtfalch/auth#138`). Read from the broker's own live env, not a
3994
- * recorded scope id: whichever of #209/#210 lands first (or a human, setting
3995
- * either directly on the app) is enough to clear that one input, in either
3996
- * order, with no coupling to this step's own execution.
4037
+ * `broker/server.ts`) refuses to start without -- `ZITADEL_SERVICE_USER_TOKEN`/
4038
+ * `_FILE` (#209 mints and pushes this once it lands) and `LETTERMINT_API_TOKEN`
4039
+ * (#210: `wtfalch/auth#138` moved the broker's account mail onto
4040
+ * `@wtfalch/mailer`'s Lettermint transport, so this is the env name the
4041
+ * broker actually reads now, not `RESEND_API_TOKEN`). Read from the broker's
4042
+ * own live env, not a recorded scope id: whichever of #209/#210 lands first
4043
+ * (or a human, setting either directly on the app) is enough to clear that
4044
+ * one input, in either order, with no coupling to this step's own execution.
3997
4045
  *
3998
4046
  * #211 round 3: `AUTH_APPS` is a third required input -- the broker's own
3999
4047
  * `parseApps` refuses to start with no `AUTH_APPS` set or an empty array
@@ -4011,8 +4059,8 @@ export async function missingBrokerInputs(coolify, brokerAppId) {
4011
4059
  if (!has('ZITADEL_SERVICE_USER_TOKEN') && !has('ZITADEL_SERVICE_USER_TOKEN_FILE')) {
4012
4060
  missing.push('ZITADEL_SERVICE_USER_TOKEN');
4013
4061
  }
4014
- if (!has('RESEND_API_TOKEN'))
4015
- missing.push('RESEND_API_TOKEN');
4062
+ if (!has('LETTERMINT_API_TOKEN'))
4063
+ missing.push('LETTERMINT_API_TOKEN');
4016
4064
  if (authAppsProblem(value('AUTH_APPS'), brokerAppId) !== null)
4017
4065
  missing.push('AUTH_APPS');
4018
4066
  return missing;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.16.12",
3
+ "version": "0.16.13",
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",