@o3co/create-auth-provider 0.16.0 → 0.17.0-rc.2

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 (100) hide show
  1. package/README.ja.md +66 -21
  2. package/README.md +76 -31
  3. package/dist/index.d.mts +33 -10
  4. package/dist/index.d.mts.map +1 -1
  5. package/dist/index.mjs +118 -59
  6. package/dist/internal/template-filter.d.mts +7 -10
  7. package/dist/internal/template-filter.d.mts.map +1 -1
  8. package/dist/internal/template-filter.mjs +7 -10
  9. package/package.json +6 -4
  10. package/templates/standalone/.dockerignore +15 -2
  11. package/templates/standalone/.env.example +59 -29
  12. package/templates/standalone/Dockerfile +6 -2
  13. package/templates/standalone/README.ja.md +324 -169
  14. package/templates/standalone/README.md +618 -208
  15. package/templates/standalone/config/application.conf +211 -352
  16. package/templates/standalone/config/clients.yaml +2 -0
  17. package/templates/standalone/config/clients.yaml.example +4 -0
  18. package/templates/standalone/config/development.conf +13 -4
  19. package/templates/standalone/config/mailpit.conf +8 -0
  20. package/templates/standalone/config/production.conf +5 -6
  21. package/templates/standalone/config/reference.conf +418 -0
  22. package/templates/standalone/docker-compose.mailpit.yml +36 -0
  23. package/templates/standalone/docker-compose.production.yml +63 -32
  24. package/templates/standalone/docker-compose.yml +13 -3
  25. package/templates/standalone/gitignore +1 -0
  26. package/templates/standalone/package.json +9 -6
  27. package/templates/standalone/src/__tests__/adapters.test.mts +419 -0
  28. package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +87 -0
  29. package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +287 -198
  30. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +71 -35
  31. package/templates/standalone/src/__tests__/all-modules-composition.test.mts +148 -94
  32. package/templates/standalone/src/__tests__/audit-sink.test.mts +84 -81
  33. package/templates/standalone/src/__tests__/config-notices.test.mts +154 -0
  34. package/templates/standalone/src/__tests__/configPath.test.mts +13 -11
  35. package/templates/standalone/src/__tests__/core-federations.test.mts +249 -0
  36. package/templates/standalone/src/__tests__/core-sections.test.mts +190 -0
  37. package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +10 -15
  38. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +970 -288
  39. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +421 -86
  40. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +87 -72
  41. package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +16 -8
  42. package/templates/standalone/src/__tests__/federation-types.test.mts +239 -0
  43. package/templates/standalone/src/__tests__/google-session.fixture.mts +97 -93
  44. package/templates/standalone/src/__tests__/host-routes.test.mts +4 -11
  45. package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +1 -1
  46. package/templates/standalone/src/__tests__/library-references.fixture.mts +188 -0
  47. package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +216 -0
  48. package/templates/standalone/src/__tests__/listen.test.mts +6 -13
  49. package/templates/standalone/src/__tests__/logger-projection.test.mts +1 -1
  50. package/templates/standalone/src/__tests__/mail-sender.test.mts +134 -0
  51. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +326 -0
  52. package/templates/standalone/src/__tests__/metrics.test.mts +4 -9
  53. package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +122 -0
  54. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +123 -0
  55. package/templates/standalone/src/__tests__/mfa-switch.test.mts +825 -0
  56. package/templates/standalone/src/__tests__/own-modules.test.mts +1486 -0
  57. package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +10 -14
  58. package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +1 -5
  59. package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +116 -81
  60. package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +25 -31
  61. package/templates/standalone/src/__tests__/replica-safety.test.mts +265 -146
  62. package/templates/standalone/src/__tests__/router-error-answers.test.mts +4 -7
  63. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +278 -103
  64. package/templates/standalone/src/__tests__/sections.test.mts +480 -0
  65. package/templates/standalone/src/__tests__/session-lifecycle.test.mts +215 -0
  66. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +69 -61
  67. package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +130 -0
  68. package/templates/standalone/src/__tests__/session-requirements.test.mts +382 -0
  69. package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +270 -0
  70. package/templates/standalone/src/__tests__/shutdown.test.mts +163 -96
  71. package/templates/standalone/src/__tests__/smoke.test.mts +176 -258
  72. package/templates/standalone/src/__tests__/store-credential.test.mts +88 -52
  73. package/templates/standalone/src/__tests__/supertest-loopback.test.mts +182 -11
  74. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +131 -102
  75. package/templates/standalone/src/__tests__/two-phase-config.test.mts +604 -0
  76. package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +382 -0
  77. package/templates/standalone/src/adapters.mts +162 -0
  78. package/templates/standalone/src/app.mts +65 -79
  79. package/templates/standalone/src/bootRefusal.mts +117 -0
  80. package/templates/standalone/src/buildModules.mts +220 -309
  81. package/templates/standalone/src/configPath.mts +449 -29
  82. package/templates/standalone/src/listen.mts +12 -18
  83. package/templates/standalone/src/logger.mts +31 -60
  84. package/templates/standalone/src/metrics.mts +24 -47
  85. package/templates/standalone/src/mfaSwitch.mts +285 -0
  86. package/templates/standalone/src/modules.mts +423 -629
  87. package/templates/standalone/src/rootRenames.mts +96 -0
  88. package/templates/standalone/src/routes.mts +8 -14
  89. package/templates/standalone/src/sections.mts +367 -0
  90. package/templates/standalone/src/shutdown.mts +45 -123
  91. package/templates/standalone/tests/index.test.js +6 -5
  92. package/templates/standalone/tests/package-lock.json +1615 -2223
  93. package/templates/standalone/tests/package.json +2 -2
  94. package/templates/standalone/tests/vitest.config.mjs +26 -0
  95. package/templates/standalone/vitest.config.mts +8 -7
  96. package/templates/standalone/vitest.supertest-loopback.mts +97 -69
  97. package/templates/versions.json +18 -15
  98. package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +0 -135
  99. package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +0 -199
  100. package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +0 -138
@@ -15,28 +15,25 @@
15
15
  */
16
16
 
17
17
  /**
18
- * A browser session with Google linked, on the standalone as a deployment
19
- * composes it: `oauthModule`, the session module and the real Google adapter,
20
- * booted through `createApp` under the shipped configuration.
18
+ * A browser session with Google linked, on the standalone composed as a
19
+ * deployment does: `oauthEndpointsModule`, the session module and the real Google
20
+ * adapter — the Google federation type `buildModules` lists, handed the
21
+ * shipped `core.federations.google` entry — booted through `createApp` under
22
+ * the shipped configuration.
21
23
  *
22
- * The routes that act on a session's federation — the two logout routes and
23
- * `POST /oauth/federation/:name/token` — live in `@o3co/auth-provider-oauth`,
24
- * which depends on no adapter, so its own tests stand one in. What they hand
25
- * the adapter, and what they make of what its library raises, can only be
26
- * seen here.
24
+ * The two logout routes and `POST /oauth/federation/:name/token` live in
25
+ * `@o3co/auth-provider-oauth`, which depends on no adapter; only here can a
26
+ * test see what they hand the real adapter and what they make of what its
27
+ * library raises.
27
28
  *
28
- * Not `all-modules-composition.fixture.mts`: that one switches every module
29
- * on and signs with a key it keeps to itself. The tests here present an
30
- * `id_token_hint` and an access token of their own — a caller's, with the
31
- * claims the case needs — so the deployment signs with an HS256 secret this
32
- * file holds, and nothing but Google is federated.
29
+ * Unlike `all-modules-composition.fixture.mts`, the deployment signs with an
30
+ * HS256 secret this file holds, so tests can present an `id_token_hint` and
31
+ * access token of their own; nothing but Google is federated.
33
32
  */
34
33
 
35
34
  import { createHmac } from "node:crypto";
36
35
  import { fileURLToPath } from "node:url";
37
36
  import {
38
- type AppConfig,
39
- AppConfigSchema,
40
37
  createApp,
41
38
  createKeyStoreFactory,
42
39
  defineModule,
@@ -46,20 +43,23 @@ import {
46
43
  InMemoryUserRepository,
47
44
  memoryRefreshTokenFamilyStoreModule,
48
45
  registerBuiltinKeyStores,
49
- type SessionFederationIndex,
46
+ type SessionLifecycle,
50
47
  type UserSessionStore,
51
48
  } from "@o3co/auth-provider-core";
52
- import {
53
- type GoogleProviderConfig,
54
- googleFederationModule,
55
- } from "@o3co/auth-provider-federation-google";
49
+ import { googleFederationTypeModule } from "@o3co/auth-provider-federation-google";
56
50
  import { parseFile } from "@o3co/ts.hocon";
57
- import { validate } from "@o3co/ts.hocon/zod";
58
51
  import express from "express";
59
52
  import request from "supertest";
60
53
  import { expect } from "vitest";
61
54
  import { buildModules } from "#/buildModules.mjs";
62
- import { resolveConfigPaths, resolveLibraryReferenceConfPath } from "#/configPath.mjs";
55
+ import { resolveConfigPaths } from "#/configPath.mjs";
56
+ import { templateReference } from "../modules.mjs";
57
+ import {
58
+ type BothPhases,
59
+ bothPhasesOf,
60
+ capturedRenames,
61
+ libraryLayers,
62
+ } from "./library-references.fixture.mjs";
63
63
 
64
64
  const configDir = fileURLToPath(new URL("../../config", import.meta.url));
65
65
 
@@ -82,53 +82,71 @@ const GOOGLE_CLIENT_SECRET = "google-secret";
82
82
  const GOOGLE_CALLBACK = `${ISSUER}/session/oauth/federation/google/callback`;
83
83
 
84
84
  const ENV: Readonly<Record<string, string>> = {
85
- OAUTH_JWT_ALGORITHM: "HS256",
86
- OAUTH_JWT_SECRET: JWT_SECRET,
85
+ // A session signed in through Google alone, no second factor.
86
+ MFA_MODE: "off",
87
+ KEY_STORE_LOCAL_ALGORITHM: "HS256",
88
+ KEY_STORE_LOCAL_SECRET: JWT_SECRET,
87
89
  OAUTH_JWT_ISSUER: ISSUER,
88
- SESSION_SECRET: "google-session.fixture-session.at-least-32-bytes.ok",
89
- SESSION_SECURE: "false",
90
- SESSION_NAME: "auth.session",
91
- SESSION_STORAGE_TYPE: "memory",
92
- CLIENT_USER_TYPE: "yaml",
93
- REFRESH_TOKEN_FAMILY_STORE_REDIS_URL: "redis://redis.test:6379",
94
- USER_SESSION_STORES_ADAPTER: "memory",
95
- RATE_LIMITER_ADAPTER: "memory",
96
- OAUTH_CODE_ADAPTER: "memory",
97
- ACCESS_TOKEN_DENYLIST_ADAPTER: "memory",
98
- REPLAY_SEEN_SET_ADAPTER: "memory",
99
- FEDERATION_TOKEN_STORE_TYPE: "memory",
100
- CONSENT_STORE_ADAPTER: "none",
90
+ SESSION_STORE_SECRET: "google-session.fixture-session.at-least-32-bytes.ok",
91
+ SESSION_STORE_SECURE: "false",
92
+ SESSION_STORE_NAME: "auth.session",
93
+ SESSION_STORE_STORAGE_TYPE: "memory",
94
+ ADAPTERS_USER_REPOSITORY: "yaml",
95
+ REDIS_CLIENTS_URL: "redis://redis.test:6379",
96
+ ADAPTERS_USER_SESSION_STORES: "memory",
97
+ ADAPTERS_RATE_LIMITER: "memory",
98
+ ADAPTERS_CODE_REPOSITORY: "memory",
99
+ ADAPTERS_ACCESS_TOKEN_DENYLIST: "memory",
100
+ ADAPTERS_REPLAY_SEEN_SET: "memory",
101
+ ADAPTERS_FEDERATION_TOKEN_STORE: "memory",
102
+ ADAPTERS_CONSENT_STORE: "none",
101
103
  };
102
104
 
103
105
  /**
104
- * `"shipped"`: Google switched on the way an operator does, through its
105
- * environment variables and the template's own configuration bridge — which
106
- * has no setting for an end-session endpoint or a `fetch`. Otherwise the
107
- * adapter's configuration beyond its credentials, composed with
108
- * `googleFederationModule` directly.
106
+ * Google switched on the way an operator does, through its environment
107
+ * variables, and handled by the Google federation type the template lists.
108
+ * `"shipped"` is that alone. Otherwise, beside it: `endSessionEndpoint`, a key
109
+ * of the entry with no environment form, written into the entry as an
110
+ * operator's own layer would; and `fetch`, the one the type module is built
111
+ * with in place of the global one.
109
112
  */
110
113
  export type GoogleWiring =
111
114
  | "shipped"
112
- | Omit<GoogleProviderConfig, "clientId" | "clientSecret" | "callbackURL">;
115
+ | { readonly endSessionEndpoint?: string; readonly fetch?: typeof fetch };
116
+
117
+ const GOOGLE_ENV: Readonly<Record<string, string>> = {
118
+ CORE_FEDERATIONS_GOOGLE_ENABLED: "true",
119
+ CORE_FEDERATIONS_GOOGLE_CLIENT_ID: GOOGLE_CLIENT_ID,
120
+ CORE_FEDERATIONS_GOOGLE_CLIENT_SECRET: GOOGLE_CLIENT_SECRET,
121
+ CORE_FEDERATIONS_GOOGLE_CALLBACK_URL: GOOGLE_CALLBACK,
122
+ };
113
123
 
114
- function resolveConfig(google: GoogleWiring): AppConfig {
115
- const env: Record<string, string> =
116
- google === "shipped"
117
- ? {
118
- ...ENV,
119
- FEDERATIONS_GOOGLE_ENABLED: "true",
120
- FEDERATIONS_GOOGLE_CLIENT_ID: GOOGLE_CLIENT_ID,
121
- FEDERATIONS_GOOGLE_CLIENT_SECRET: GOOGLE_CLIENT_SECRET,
122
- FEDERATIONS_GOOGLE_CALLBACK_URL: GOOGLE_CALLBACK,
123
- }
124
- : { ...ENV };
124
+ function resolveConfig(google: GoogleWiring): BothPhases {
125
+ const env: Record<string, string> = { ...ENV, ...GOOGLE_ENV };
125
126
  const { applicationConfPath, envConfPath } = resolveConfigPaths(configDir, "production");
126
- return validate(
127
- parseFile(envConfPath, { env })
128
- .withFallback(parseFile(applicationConfPath, { env }))
129
- .withFallback(parseFile(resolveLibraryReferenceConfPath(), { env })),
130
- AppConfigSchema,
131
- ) as AppConfig;
127
+ const layers = parseFile(envConfPath, { env })
128
+ .withFallback(parseFile(applicationConfPath, { env }))
129
+ .withFallback(parseFile(fileURLToPath(templateReference()), { env }))
130
+ .withFallback(libraryLayers(env));
131
+ const config = bothPhasesOf(layers, env);
132
+ const endSessionEndpoint = google === "shipped" ? undefined : google.endSessionEndpoint;
133
+ const federations = config.core?.federations;
134
+ return {
135
+ ...config,
136
+ ...(endSessionEndpoint === undefined
137
+ ? {}
138
+ : {
139
+ core: {
140
+ ...config.core,
141
+ federations: {
142
+ ...federations,
143
+ google: { ...federations?.google, endSessionEndpoint },
144
+ },
145
+ },
146
+ }),
147
+ // What the resolution captured of the renamed variables.
148
+ "renamed-variables": capturedRenames(env),
149
+ } as BothPhases;
132
150
  }
133
151
 
134
152
  const testRepositoriesModule = defineModule({
@@ -170,7 +188,7 @@ const testKeyStoreModule = defineModule({
170
188
  registerBuiltinKeyStores(factory);
171
189
  return factory.create({
172
190
  type: "local",
173
- ...((c as AppConfig).oauth.jwt.signingKey.local ?? {}),
191
+ ...((c as { "key-store"?: { local?: object } })["key-store"]?.local ?? {}),
174
192
  });
175
193
  },
176
194
  },
@@ -216,15 +234,15 @@ export interface GoogleSession {
216
234
  /** The access token the `session` grant minted for the session; it carries `azp`. */
217
235
  readonly accessToken: string;
218
236
  readonly userSessionStore: UserSessionStore;
219
- readonly sessionFederationIndex: SessionFederationIndex;
237
+ readonly sessionLifecycle: SessionLifecycle;
220
238
  readonly federationTokenStore: FederationTokenStore;
221
239
  readonly dispose: () => Promise<void>;
222
240
  }
223
241
 
224
242
  /**
225
243
  * Boots the deployment, signs alice in with her password, and links Google to
226
- * her session as a federated sign-in would: the session's federation index
227
- * names it and the token store holds `tokens`.
244
+ * her session as a federated sign-in would: the token store holds `tokens`,
245
+ * and Google joins the session through its lifecycle.
228
246
  */
229
247
  export async function signInLinkedToGoogle(options: {
230
248
  readonly google: GoogleWiring;
@@ -232,38 +250,24 @@ export async function signInLinkedToGoogle(options: {
232
250
  }): Promise<GoogleSession> {
233
251
  const { google, tokens } = options;
234
252
  const config = resolveConfig(google);
235
- const googleModules =
236
- google === "shipped"
237
- ? []
238
- : [
239
- googleFederationModule,
240
- defineModule({
241
- name: "test:google-federation-config",
242
- provides: {
243
- googleFederationConfig: (): GoogleProviderConfig => ({
244
- clientId: GOOGLE_CLIENT_ID,
245
- clientSecret: GOOGLE_CLIENT_SECRET,
246
- callbackURL: GOOGLE_CALLBACK,
247
- ...google,
248
- }),
249
- },
250
- }),
251
- ];
253
+ const upstreamFetch = google === "shipped" ? undefined : google.fetch;
254
+ const googleType = googleFederationTypeModule().name;
252
255
  const handle = await createApp({
253
- modules: [
254
- ...buildModules(config, {
255
- keyStoreModule: testKeyStoreModule,
256
- repositoriesModule: testRepositoriesModule,
257
- refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
258
- }),
259
- ...googleModules,
260
- ],
256
+ modules: buildModules(config, {
257
+ keyStoreModule: testKeyStoreModule,
258
+ repositoriesModule: testRepositoriesModule,
259
+ refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
260
+ }).map((module) =>
261
+ module.name === googleType && upstreamFetch !== undefined
262
+ ? googleFederationTypeModule({ fetch: upstreamFetch })
263
+ : module,
264
+ ),
261
265
  bootstrapComponents: { config, pathResolver: (s) => s },
262
266
  });
263
267
  try {
264
268
  const components = handle.components as {
265
269
  userSessionStore: UserSessionStore;
266
- sessionFederationIndex: SessionFederationIndex;
270
+ sessionLifecycle: SessionLifecycle;
267
271
  federationTokenStore: FederationTokenStore;
268
272
  };
269
273
  const app = express();
@@ -292,10 +296,10 @@ export async function signInLinkedToGoogle(options: {
292
296
  const sid = claims.sid as string;
293
297
  expect(typeof sid).toBe("string");
294
298
 
295
- const session = await components.userSessionStore.get(sid);
296
- if (session === null) throw new Error("fixture: the sign-in left no session");
297
- await components.sessionFederationIndex.addFederation(sid, "google", session.expiresAt);
298
299
  await components.federationTokenStore.attach(sid, "google", tokens);
300
+ expect(await components.sessionLifecycle.join(sid, { federation: "google" })).toEqual({
301
+ outcome: "joined",
302
+ });
299
303
  return { app, sid, accessToken, ...components, dispose: () => handle.dispose() };
300
304
  } catch (error) {
301
305
  await handle.dispose();
@@ -15,17 +15,10 @@
15
15
  */
16
16
 
17
17
  /**
18
- * The host's own routes — liveness, readiness, the metrics scrape — mounted
19
- * as the process mounts them (`mountRoutes`), and what an error one of them
20
- * lets through is answered with.
21
- *
22
- * The composed router answers its own errors with core's terminal handler.
23
- * The host's routes sat in front of a handler of the template's own, which
24
- * answered in other words ("Internal server error"), took any 4xx `status`
25
- * for the client's mistake and answered it as one, handed an error on to
26
- * Express once the headers were out (whose final handler prints the stack),
27
- * and logged the path as it arrived. Core's handler is now the one after
28
- * them too, so a deployment answers every route's error the same way.
18
+ * The host's own routes (liveness, readiness, the metrics scrape), mounted as
19
+ * the process mounts them (`mountRoutes`), and what an error one of them lets
20
+ * through is answered with: core's terminal handler, the same one behind the
21
+ * composed router, so every route's error is answered the same way.
29
22
  */
30
23
 
31
24
  import type { Logger, ReadinessProbe } from "@o3co/auth-provider-core";
@@ -24,6 +24,6 @@ describe("library reference.conf subpath resolution", () => {
24
24
  const path = fileURLToPath(url);
25
25
  expect(existsSync(path)).toBe(true);
26
26
  const content = readFileSync(path, "utf-8");
27
- expect(content).toMatch(/oauth\s*\{/); // sanity: HOCON content
27
+ expect(content).toMatch(/^core\s*\{/m); // sanity: HOCON content, core's own section
28
28
  });
29
29
  });
@@ -0,0 +1,188 @@
1
+ /*
2
+ * Copyright 2026 1o1 Co. Ltd.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ /**
18
+ * The libraries' layers beneath the template's own files, for a test that
19
+ * resolves its configuration by hand: the `reference.conf` of every package
20
+ * the template composes a module from, core's last, as `app.mts` layers them;
21
+ * the resolution handed to both phases at once; and what that resolution
22
+ * captures of the renamed variables.
23
+ */
24
+
25
+ import { fileURLToPath } from "node:url";
26
+ import {
27
+ type AppConfig,
28
+ memoryRateLimiterModule,
29
+ moduleReferences,
30
+ } from "@o3co/auth-provider-core";
31
+ import { CORE_RELOCATIONS, renamedVariableCaptures } from "@o3co/auth-provider-core/testing";
32
+ import { federationGrantsModules } from "@o3co/auth-provider-federation-grants";
33
+ import {
34
+ oauthAuthorizationGrantsModule,
35
+ oauthEndpointsModule,
36
+ oauthSessionGrantModule,
37
+ } from "@o3co/auth-provider-oauth";
38
+ import {
39
+ redisCodeRepositoryModule,
40
+ redisFederationGrantStoreModule,
41
+ redisRateLimiterModule,
42
+ redisRefreshTokenFamilyStoreModule,
43
+ } from "@o3co/auth-provider-redis";
44
+ import { sessionModule, sessionStoreModule } from "@o3co/auth-provider-session";
45
+ import { type Config, empty, parseFile } from "@o3co/ts.hocon";
46
+ import { readAdapters } from "../adapters.mjs";
47
+ import { readSwitches, type Switches } from "../configPath.mjs";
48
+ import {
49
+ httpModule,
50
+ inMemoryCodeRepositoryModule,
51
+ keyStoreModule,
52
+ loggingModule,
53
+ repositoriesModuleFor,
54
+ standaloneRedisClientsModule,
55
+ templateReference,
56
+ } from "../modules.mjs";
57
+ import { type Adapters, isPlainSection } from "../sections.mjs";
58
+
59
+ /** The oauth package's modules: none reads the configuration it is handed. */
60
+ const OAUTH_MODULES = [
61
+ oauthEndpointsModule,
62
+ oauthSessionGrantModule,
63
+ oauthAuthorizationGrantsModule,
64
+ ];
65
+
66
+ /** The modules the template composes that declare a renamed variable. */
67
+ const RENAMING_MODULES = [
68
+ memoryRateLimiterModule,
69
+ redisRateLimiterModule,
70
+ redisRefreshTokenFamilyStoreModule,
71
+ redisFederationGrantStoreModule,
72
+ ...OAUTH_MODULES,
73
+ sessionModule,
74
+ sessionStoreModule,
75
+ loggingModule,
76
+ httpModule,
77
+ keyStoreModule,
78
+ standaloneRedisClientsModule,
79
+ repositoriesModuleFor({ client: "yaml", user: "yaml" }),
80
+ inMemoryCodeRepositoryModule,
81
+ ];
82
+
83
+ /** Every package reference the template's modules declare, core's last. */
84
+ const LIBRARY_REFERENCES: readonly URL[] = moduleReferences([
85
+ redisRateLimiterModule,
86
+ ...federationGrantsModules,
87
+ ...OAUTH_MODULES,
88
+ sessionModule,
89
+ sessionStoreModule,
90
+ ]);
91
+
92
+ /** The libraries' references resolved under `env`, in the order `app.mts` layers them. */
93
+ export function libraryLayers(env: Readonly<Record<string, string>>): Config {
94
+ return LIBRARY_REFERENCES.reduce<Config>(
95
+ (layered, reference) =>
96
+ layered.withFallback(parseFile(fileURLToPath(reference), { env: { ...env } })),
97
+ empty(),
98
+ );
99
+ }
100
+
101
+ /** Phase one's switches laid over the configuration boot parses. */
102
+ export type BothPhases = Switches & AppConfig;
103
+
104
+ /**
105
+ * `switches` laid over `resolved`, the configuration boot parses, as a test
106
+ * that hands one object to both phases does: the composition root's own keys
107
+ * beside the sections, and `federation-grants.enabled` written as phase one
108
+ * decided it into the section as resolved. Boot is so handed phase one's
109
+ * boolean, not the value as written, unlike `app.mts`; boot's refusal of a
110
+ * written value phase one installs the modules for is pinned in
111
+ * `two-phase-config.test.mts`.
112
+ */
113
+ export function withSwitches(
114
+ resolved: Readonly<Record<string, unknown>>,
115
+ switches: Switches,
116
+ ): BothPhases {
117
+ const grants = resolved["federation-grants"];
118
+ return {
119
+ ...resolved,
120
+ ...switches,
121
+ "federation-grants": {
122
+ ...(isPlainSection(grants) ? grants : {}),
123
+ ...switches["federation-grants"],
124
+ },
125
+ } as unknown as BothPhases;
126
+ }
127
+
128
+ /**
129
+ * A resolution — `layers`, the template's `config/reference.conf` among them,
130
+ * under `env` — as a test that resolves by hand hands it to both phases at
131
+ * once: the configuration as resolved and unparsed, which boot parses as
132
+ * `app.mts` hands it on, with what phase one reads of it (`readSwitches`)
133
+ * laid over it — the composition root's `adapters` and `mfaMode`, the Store
134
+ * transport settings, and `federation-grants.enabled` as phase one decides
135
+ * it.
136
+ */
137
+ export function bothPhasesOf(layers: Config, env: Readonly<Record<string, string>>): BothPhases {
138
+ return withSwitches(
139
+ layers.toObject() as Record<string, unknown>,
140
+ readSwitches({ config: layers, env }),
141
+ );
142
+ }
143
+
144
+ /**
145
+ * The composition root's `adapters` a resolution holds — `layers`, the
146
+ * template's `config/reference.conf` among them, under `env` — as phase one
147
+ * reads them.
148
+ */
149
+ export function adaptersOf(layers: Config, env: Readonly<Record<string, string>>): Adapters {
150
+ return readAdapters(layers.toObject() as Record<string, unknown>, env);
151
+ }
152
+
153
+ /** The adapters the template ships, as its `config/reference.conf` sets them with no environment. */
154
+ export function shippedAdapters(): Adapters {
155
+ return adaptersOf(parseFile(fileURLToPath(templateReference()), { env: {} }), {});
156
+ }
157
+
158
+ /** The shipped adapters with every store the template can hold in process there: no Redis connection needed. */
159
+ export function inProcessAdapters(): Adapters {
160
+ return {
161
+ ...shippedAdapters(),
162
+ rateLimiter: "memory",
163
+ userSessionStores: "memory",
164
+ accessTokenDenylist: "memory",
165
+ replaySeenSet: "memory",
166
+ federationTokenStore: "memory",
167
+ federationGrantStore: "memory",
168
+ federationGrantIntentStore: "memory",
169
+ mfaFactorStore: "memory",
170
+ mfaTransactionStore: "memory",
171
+ codeRepository: "memory",
172
+ };
173
+ }
174
+
175
+ /**
176
+ * The `renamed-variables` section a resolution under `env` holds: what every
177
+ * module above captures, and the Redis code repository's, which the
178
+ * in-process one's excludes from one composition (both declare
179
+ * CLIENT_CODE_DEFAULT_EXPIRES_IN renamed, each to its own new name).
180
+ */
181
+ export function capturedRenames(
182
+ env: Readonly<Record<string, string | undefined>>,
183
+ ): Record<string, string | null> {
184
+ return {
185
+ ...renamedVariableCaptures({ modules: [redisCodeRepositoryModule], env }),
186
+ ...renamedVariableCaptures({ modules: RENAMING_MODULES, core: CORE_RELOCATIONS, env }),
187
+ };
188
+ }