@o3co/create-auth-provider 0.17.0-rc.4 → 0.18.0-rc.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.
Files changed (110) hide show
  1. package/README.md +2 -198
  2. package/main.js +4 -0
  3. package/package.json +8 -30
  4. package/README.ja.md +0 -200
  5. package/dist/cli.d.mts +0 -3
  6. package/dist/cli.d.mts.map +0 -1
  7. package/dist/cli.mjs +0 -18
  8. package/dist/index.d.mts +0 -48
  9. package/dist/index.d.mts.map +0 -1
  10. package/dist/index.mjs +0 -325
  11. package/dist/internal/template-filter.d.mts +0 -14
  12. package/dist/internal/template-filter.d.mts.map +0 -1
  13. package/dist/internal/template-filter.mjs +0 -42
  14. package/templates/standalone/.dockerignore +0 -23
  15. package/templates/standalone/.env.example +0 -152
  16. package/templates/standalone/Dockerfile +0 -125
  17. package/templates/standalone/Makefile +0 -12
  18. package/templates/standalone/README.ja.md +0 -913
  19. package/templates/standalone/README.md +0 -1537
  20. package/templates/standalone/config/application.conf +0 -351
  21. package/templates/standalone/config/clients.yaml +0 -4
  22. package/templates/standalone/config/clients.yaml.example +0 -118
  23. package/templates/standalone/config/development.conf +0 -17
  24. package/templates/standalone/config/mailpit.conf +0 -13
  25. package/templates/standalone/config/production.conf +0 -15
  26. package/templates/standalone/config/reference.conf +0 -424
  27. package/templates/standalone/docker-compose.mailpit.yml +0 -39
  28. package/templates/standalone/docker-compose.production.yml +0 -229
  29. package/templates/standalone/docker-compose.test.yml +0 -16
  30. package/templates/standalone/docker-compose.yml +0 -67
  31. package/templates/standalone/gitignore +0 -23
  32. package/templates/standalone/package.json +0 -55
  33. package/templates/standalone/src/__tests__/adapters.test.mts +0 -425
  34. package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +0 -87
  35. package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +0 -1192
  36. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +0 -285
  37. package/templates/standalone/src/__tests__/all-modules-composition.test.mts +0 -1154
  38. package/templates/standalone/src/__tests__/audit-sink.test.mts +0 -346
  39. package/templates/standalone/src/__tests__/config-notices.test.mts +0 -157
  40. package/templates/standalone/src/__tests__/configPath.test.mts +0 -69
  41. package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +0 -48
  42. package/templates/standalone/src/__tests__/core-federations.test.mts +0 -252
  43. package/templates/standalone/src/__tests__/core-sections.test.mts +0 -192
  44. package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +0 -95
  45. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +0 -1290
  46. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +0 -784
  47. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +0 -461
  48. package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +0 -141
  49. package/templates/standalone/src/__tests__/federation-types.test.mts +0 -239
  50. package/templates/standalone/src/__tests__/google-session.fixture.mts +0 -308
  51. package/templates/standalone/src/__tests__/host-routes.test.mts +0 -134
  52. package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +0 -29
  53. package/templates/standalone/src/__tests__/library-references.fixture.mts +0 -188
  54. package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +0 -216
  55. package/templates/standalone/src/__tests__/listen.test.mts +0 -121
  56. package/templates/standalone/src/__tests__/logger-projection.test.mts +0 -77
  57. package/templates/standalone/src/__tests__/logout-session-cookie.test.mts +0 -113
  58. package/templates/standalone/src/__tests__/mail-sender.test.mts +0 -134
  59. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +0 -393
  60. package/templates/standalone/src/__tests__/metrics.test.mts +0 -253
  61. package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +0 -122
  62. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +0 -127
  63. package/templates/standalone/src/__tests__/mfa-switch.test.mts +0 -883
  64. package/templates/standalone/src/__tests__/own-modules.test.mts +0 -1512
  65. package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +0 -183
  66. package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +0 -214
  67. package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +0 -135
  68. package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +0 -214
  69. package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +0 -249
  70. package/templates/standalone/src/__tests__/replica-safety.test.mts +0 -654
  71. package/templates/standalone/src/__tests__/repositories-logger.test.mts +0 -119
  72. package/templates/standalone/src/__tests__/router-error-answers.test.mts +0 -146
  73. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +0 -610
  74. package/templates/standalone/src/__tests__/sections.test.mts +0 -504
  75. package/templates/standalone/src/__tests__/session-lifecycle.test.mts +0 -215
  76. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +0 -296
  77. package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +0 -130
  78. package/templates/standalone/src/__tests__/session-requirements.test.mts +0 -382
  79. package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +0 -274
  80. package/templates/standalone/src/__tests__/shutdown.test.mts +0 -622
  81. package/templates/standalone/src/__tests__/smoke.test.mts +0 -634
  82. package/templates/standalone/src/__tests__/startup-log-lines.test.mts +0 -148
  83. package/templates/standalone/src/__tests__/store-credential.test.mts +0 -312
  84. package/templates/standalone/src/__tests__/supertest-loopback.test.mts +0 -296
  85. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +0 -265
  86. package/templates/standalone/src/__tests__/two-phase-config.test.mts +0 -605
  87. package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +0 -382
  88. package/templates/standalone/src/adapters.mts +0 -162
  89. package/templates/standalone/src/app.mts +0 -136
  90. package/templates/standalone/src/bootRefusal.mts +0 -117
  91. package/templates/standalone/src/buildModules.mts +0 -358
  92. package/templates/standalone/src/configPath.mts +0 -487
  93. package/templates/standalone/src/listen.mts +0 -60
  94. package/templates/standalone/src/logger.mts +0 -80
  95. package/templates/standalone/src/metrics.mts +0 -167
  96. package/templates/standalone/src/mfaSwitch.mts +0 -284
  97. package/templates/standalone/src/modules.mts +0 -705
  98. package/templates/standalone/src/rootRenames.mts +0 -95
  99. package/templates/standalone/src/routes.mts +0 -89
  100. package/templates/standalone/src/sections.mts +0 -377
  101. package/templates/standalone/src/shutdown.mts +0 -213
  102. package/templates/standalone/tests/Makefile +0 -5
  103. package/templates/standalone/tests/index.test.js +0 -283
  104. package/templates/standalone/tests/package-lock.json +0 -1617
  105. package/templates/standalone/tests/package.json +0 -16
  106. package/templates/standalone/tests/vitest.config.mjs +0 -26
  107. package/templates/standalone/tsconfig.json +0 -31
  108. package/templates/standalone/vitest.config.mts +0 -31
  109. package/templates/standalone/vitest.supertest-loopback.mts +0 -278
  110. package/templates/versions.json +0 -20
@@ -1,382 +0,0 @@
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
- * What an upstream IdP asserted about its own login counts only for a
19
- * federation configured with `core.federations.<name>.trustUpstreamAmr = true`
20
- * (ADR 2026-09-25-multi-factor-authentication), end to end: a federated login
21
- * through the session routes, `/oauth/authorize` with `acr_values`, and
22
- * `/oauth/token`, on the standalone as a deployment composes it
23
- * (`buildModules`), with a federation whose IdP asserts `mfa` and `hwk`.
24
- * By default they are kept in `authentication.upstreamAmr` and an `acr` entry
25
- * only they could meet is withheld; trusted, they are recorded beside `fed`.
26
- * The session routes and `oauth` read the switch separately (neither package
27
- * depends on the other); this is where the two meet.
28
- */
29
-
30
- import { createHash, randomBytes } from "node:crypto";
31
- import { fileURLToPath } from "node:url";
32
- import {
33
- createApp,
34
- createKeyStoreFactory,
35
- defineModule,
36
- type FederationProvider,
37
- federationsOf,
38
- InMemoryClientRepository,
39
- InMemoryUserRepository,
40
- memoryRefreshTokenFamilyStoreModule,
41
- registerBuiltinKeyStores,
42
- type UserSessionStore,
43
- } from "@o3co/auth-provider-core";
44
- import { federationTypeForTests } from "@o3co/auth-provider-core/testing";
45
- import type { OAuthSection } from "@o3co/auth-provider-oauth";
46
- import { parseFile } from "@o3co/ts.hocon";
47
- import express from "express";
48
- import request from "supertest";
49
- import { afterEach, describe, expect, it } from "vitest";
50
- import { buildModules } from "#/buildModules.mjs";
51
- import { resolveConfigPaths } from "#/configPath.mjs";
52
- import { templateReference } from "../modules.mjs";
53
- import {
54
- type BothPhases,
55
- bothPhasesOf,
56
- capturedRenames,
57
- libraryLayers,
58
- } from "./library-references.fixture.mjs";
59
-
60
- const configDir = fileURLToPath(new URL("../../config", import.meta.url));
61
-
62
- const ISSUER = "https://auth.test";
63
- const CLIENT_ID = "rp";
64
- const CLIENT_SECRET = "rp-secret-value";
65
- const BASIC = `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64")}`;
66
- const REDIRECT_URI = "https://rp.example/callback";
67
- const FEDERATION = "partner";
68
- const CALLBACK_URL = `${ISSUER}/session/oauth/federation/${FEDERATION}/callback`;
69
- /** What the partner IdP asserts about its own login. */
70
- const UPSTREAM_AMR = ["mfa", "hwk"];
71
- const MFA_ACR = "urn:example:mfa";
72
- const FED_ACR = "urn:example:fed";
73
-
74
- const ENV: Readonly<Record<string, string>> = {
75
- // This provider verifies no second factor of its own here: only upstream's.
76
- MFA_MODE: "off",
77
- KEY_STORE_LOCAL_ALGORITHM: "HS256",
78
- KEY_STORE_LOCAL_SECRET: "upstream-amr.acceptance.at-least-32-bytes",
79
- OAUTH_JWT_ISSUER: ISSUER,
80
- SESSION_STORE_SECRET: "upstream-amr.acceptance-session.at-least-32-bytes",
81
- SESSION_STORE_SECURE: "false",
82
- SESSION_STORE_NAME: "auth.session",
83
- SESSION_STORE_STORAGE_TYPE: "memory",
84
- ADAPTERS_USER_REPOSITORY: "yaml",
85
- REDIS_CLIENTS_URL: "redis://redis.test:6379",
86
- ADAPTERS_USER_SESSION_STORES: "memory",
87
- ADAPTERS_RATE_LIMITER: "memory",
88
- ADAPTERS_CODE_REPOSITORY: "memory",
89
- ADAPTERS_ACCESS_TOKEN_DENYLIST: "memory",
90
- ADAPTERS_REPLAY_SEEN_SET: "memory",
91
- ADAPTERS_FEDERATION_TOKEN_STORE: "memory",
92
- ADAPTERS_CONSENT_STORE: "none",
93
- };
94
-
95
- /** The shipped configuration, with the partner federation and an acr table beside it. */
96
- function resolveConfig(trustUpstreamAmr: boolean | undefined): BothPhases {
97
- const { applicationConfPath, envConfPath } = resolveConfigPaths(configDir, "production");
98
- const layers = parseFile(envConfPath, { env: ENV })
99
- .withFallback(parseFile(applicationConfPath, { env: ENV }))
100
- .withFallback(parseFile(fileURLToPath(templateReference()), { env: ENV }))
101
- .withFallback(libraryLayers(ENV));
102
- const shipped = bothPhasesOf(layers, ENV);
103
- return {
104
- ...shipped,
105
- core: {
106
- ...shipped.core,
107
- federations: {
108
- ...federationsOf(shipped),
109
- [FEDERATION]: {
110
- enabled: true,
111
- type: FEDERATION,
112
- callbackURL: CALLBACK_URL,
113
- ...(trustUpstreamAmr === undefined ? {} : { trustUpstreamAmr }),
114
- },
115
- },
116
- },
117
- oauth: {
118
- ...(shipped.oauth as OAuthSection),
119
- authorize: {
120
- ...(shipped.oauth as OAuthSection).authorize,
121
- acrValues: { [MFA_ACR]: ["mfa"], [FED_ACR]: ["fed"] },
122
- },
123
- },
124
- // What the resolution captured of the renamed variables.
125
- "renamed-variables": capturedRenames(ENV),
126
- } as BothPhases;
127
- }
128
-
129
- /** An IdP that authenticates `ext-1` and says it did so with `mfa` and `hwk`. */
130
- const partner: FederationProvider = {
131
- name: FEDERATION,
132
- scope: ["openid"],
133
- buildAuthorizationUrl: ({ state }) => {
134
- const url = new URL("https://idp.partner.example/authorize");
135
- url.searchParams.set("state", state);
136
- return url;
137
- },
138
- exchangeCode: async () => ({
139
- issuer: "https://idp.partner.example",
140
- sub: "ext-1",
141
- accessToken: "upstream-at",
142
- expiresAt: null,
143
- amr: UPSTREAM_AMR,
144
- }),
145
- };
146
-
147
- /**
148
- * The partner federation's type: the `partner` entry is the IdP above, with a
149
- * redirect policy that accepts every redirect and resolves the callback to `/`.
150
- */
151
- const partnerFederationModule = federationTypeForTests(FEDERATION, { provider: () => partner });
152
-
153
- const testRepositoriesModule = defineModule({
154
- name: "test:repositories",
155
- provides: {
156
- clientRepository: () =>
157
- new InMemoryClientRepository(
158
- new Map([
159
- [
160
- CLIENT_ID,
161
- {
162
- tokenEndpointAuthMethod: "client_secret_basic",
163
- clientSecret: CLIENT_SECRET,
164
- allowedRedirectUris: [REDIRECT_URI],
165
- allowedScopes: ["openid"],
166
- allowedAudiences: [],
167
- allowedGrantTypes: ["authorization_code"],
168
- firstParty: true,
169
- backchannelLogoutSessionRequired: false,
170
- frontchannelLogoutSessionRequired: false,
171
- allowedAzpForFederationToken: false,
172
- },
173
- ],
174
- ]),
175
- ),
176
- // The partner's `ext-1` is alice's linked identity.
177
- userRepository: () =>
178
- new InMemoryUserRepository(
179
- new Map([["alice", { id: "u-alice", password: "unused", token: `${FEDERATION}:ext-1` }]]),
180
- ),
181
- } as never,
182
- });
183
-
184
- const testKeyStoreModule = defineModule({
185
- name: "test:key-store",
186
- requires: ["config"] as const,
187
- provides: {
188
- keyStore: async ({ config: c }) => {
189
- const factory = createKeyStoreFactory();
190
- registerBuiltinKeyStores(factory);
191
- return factory.create({
192
- type: "local",
193
- ...((c as { "key-store"?: { local?: object } })["key-store"]?.local ?? {}),
194
- });
195
- },
196
- },
197
- });
198
-
199
- /** A JWT's claims, unverified: the deployment signed it, and the test reads what it said. */
200
- const decodeJwt = (jwt: string): Record<string, unknown> =>
201
- JSON.parse(Buffer.from(jwt.split(".")[1] ?? "", "base64url").toString("utf8")) as Record<
202
- string,
203
- unknown
204
- >;
205
-
206
- const cookiesOf = (res: request.Response): string[] =>
207
- (res.headers["set-cookie"] as unknown as string[] | undefined) ?? [];
208
-
209
- interface Deployment {
210
- readonly app: express.Express;
211
- readonly userSessionStore: UserSessionStore;
212
- readonly dispose: () => Promise<void>;
213
- }
214
-
215
- async function boot(trustUpstreamAmr: boolean | undefined): Promise<Deployment> {
216
- const config = resolveConfig(trustUpstreamAmr);
217
- const handle = await createApp({
218
- modules: [
219
- ...buildModules(config, {
220
- keyStoreModule: testKeyStoreModule,
221
- repositoriesModule: testRepositoriesModule,
222
- refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
223
- }),
224
- partnerFederationModule,
225
- ],
226
- bootstrapComponents: { config, pathResolver: (s) => s },
227
- });
228
- const app = express();
229
- app.use(handle.router);
230
- return {
231
- app,
232
- userSessionStore: (handle.components as { userSessionStore: UserSessionStore })
233
- .userSessionStore,
234
- dispose: () => handle.dispose(),
235
- };
236
- }
237
-
238
- /** Signs alice in through the partner federation; the browser's cookies after the callback. */
239
- async function signInThroughPartner(app: express.Express): Promise<string[]> {
240
- const start = await request(app).get(`/session/oauth/federation/${FEDERATION}`);
241
- expect(start.status).toBe(302);
242
- const state = new URL(start.headers.location as string).searchParams.get("state");
243
- expect(state).toBeTruthy();
244
- const callback = await request(app)
245
- .get(`/session/oauth/federation/${FEDERATION}/callback`)
246
- .query({ state, code: "upstream-code" })
247
- .set("Cookie", cookiesOf(start));
248
- expect(callback.status).toBe(302);
249
- return cookiesOf(callback);
250
- }
251
-
252
- /** `/oauth/authorize` for the signed-in browser: the redirect back to the client, as parameters. */
253
- async function authorize(
254
- app: express.Express,
255
- cookies: readonly string[],
256
- acrValues: string,
257
- ): Promise<{ readonly params: URLSearchParams; readonly verifier: string }> {
258
- const verifier = randomBytes(32).toString("base64url");
259
- const res = await request(app)
260
- .get("/oauth/authorize")
261
- .query({
262
- response_type: "code",
263
- client_id: CLIENT_ID,
264
- redirect_uri: REDIRECT_URI,
265
- scope: "openid",
266
- state: "rp-state",
267
- code_challenge: createHash("sha256").update(verifier).digest("base64url"),
268
- code_challenge_method: "S256",
269
- acr_values: acrValues,
270
- })
271
- .set("Cookie", [...cookies]);
272
- expect(res.status).toBe(302);
273
- const location = new URL(res.headers.location as string);
274
- expect(`${location.origin}${location.pathname}`).toBe(REDIRECT_URI);
275
- return { params: location.searchParams, verifier };
276
- }
277
-
278
- /** The tokens `/oauth/token` mints for `code`, decoded. */
279
- async function tokensFor(
280
- app: express.Express,
281
- code: string,
282
- verifier: string,
283
- ): Promise<{
284
- readonly idToken: Record<string, unknown>;
285
- readonly accessToken: Record<string, unknown>;
286
- }> {
287
- const res = await request(app)
288
- .post("/oauth/token")
289
- .set("Authorization", BASIC)
290
- .type("form")
291
- .send({
292
- grant_type: "authorization_code",
293
- code,
294
- redirect_uri: REDIRECT_URI,
295
- code_verifier: verifier,
296
- });
297
- expect(res.status).toBe(200);
298
- return {
299
- idToken: decodeJwt(res.body.id_token as string),
300
- accessToken: decodeJwt(res.body.access_token as string),
301
- };
302
- }
303
-
304
- describe("an upstream IdP's amr counts only for a federation that trusts it", () => {
305
- let deployment: Deployment | undefined;
306
-
307
- afterEach(async () => {
308
- await deployment?.dispose();
309
- deployment = undefined;
310
- });
311
-
312
- it("by default: kept apart in the session, withheld from discovery, unmet at /authorize, on no token", async () => {
313
- deployment = await boot(undefined);
314
- const { app, userSessionStore } = deployment;
315
-
316
- const discovery = await request(app).get("/.well-known/openid-configuration");
317
- // The entry only the IdP's `mfa` could meet is dropped, since that `mfa`
318
- // is not recorded: no deployment advertises what it cannot meet.
319
- expect(discovery.body.acr_values_supported).toEqual([FED_ACR]);
320
-
321
- const cookies = await signInThroughPartner(app);
322
-
323
- const unmet = await authorize(app, cookies, MFA_ACR);
324
- expect(unmet.params.get("error")).toBe("unmet_authentication_requirements");
325
- expect(unmet.params.get("code")).toBeNull();
326
-
327
- const { params, verifier } = await authorize(app, cookies, FED_ACR);
328
- const code = params.get("code");
329
- expect(code).toBeTruthy();
330
- const { idToken, accessToken } = await tokensFor(app, code as string, verifier);
331
- expect(idToken.acr).toBe(FED_ACR);
332
- expect(idToken.amr).toEqual(["fed"]);
333
- expect(accessToken.amr).toEqual(["fed"]);
334
-
335
- // The session keeps what the IdP said, for the record.
336
- const session = await userSessionStore.get(idToken.sid as string);
337
- expect(session?.amr).toEqual(["fed"]);
338
- expect(session?.authentication).toStrictEqual({
339
- primary: "fed",
340
- federation: FEDERATION,
341
- upstreamAmr: UPSTREAM_AMR,
342
- mfaAt: undefined,
343
- // The partner reports no `auth_time`, and the federation does not let
344
- // its callback stand for one (`callbackMeetsFreshness`, false by default).
345
- upstreamAuthTime: null,
346
- });
347
- });
348
-
349
- it("trusted: recorded beside fed, advertised, met at /authorize and stamped", async () => {
350
- deployment = await boot(true);
351
- const { app, userSessionStore } = deployment;
352
-
353
- const discovery = await request(app).get("/.well-known/openid-configuration");
354
- expect(discovery.body.acr_values_supported).toEqual([MFA_ACR, FED_ACR]);
355
-
356
- const cookies = await signInThroughPartner(app);
357
- const { params, verifier } = await authorize(app, cookies, MFA_ACR);
358
- const code = params.get("code");
359
- expect(code).toBeTruthy();
360
- const { idToken, accessToken } = await tokensFor(app, code as string, verifier);
361
- expect(idToken.acr).toBe(MFA_ACR);
362
- expect(idToken.amr).toEqual([...UPSTREAM_AMR, "fed"]);
363
- expect(accessToken.amr).toEqual([...UPSTREAM_AMR, "fed"]);
364
-
365
- const session = await userSessionStore.get(idToken.sid as string);
366
- expect(session?.authentication).toStrictEqual({
367
- primary: "fed",
368
- federation: FEDERATION,
369
- upstreamAmr: undefined,
370
- mfaAt: undefined,
371
- // The partner reports no `auth_time`, and the federation does not let
372
- // its callback stand for one (`callbackMeetsFreshness`, false by default).
373
- upstreamAuthTime: null,
374
- });
375
- });
376
-
377
- it("says false explicitly as it says nothing: kept apart", async () => {
378
- deployment = await boot(false);
379
- const discovery = await request(deployment.app).get("/.well-known/openid-configuration");
380
- expect(discovery.body.acr_values_supported).toEqual([FED_ACR]);
381
- });
382
- });
@@ -1,162 +0,0 @@
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 composition root's own section, `adapters`: which adapter fills each
19
- * slot. Phase one reads it alone, before the modules are chosen, with the
20
- * template's own schema; boot is never handed it. The paths the selections
21
- * moved from, and the variables renamed with them, are refused here, before
22
- * any module is chosen, in the words and under the reasons boot refuses a
23
- * module's with (`config-path-relocated`, `environment-variable-renamed`): a
24
- * renamed variable set, alone or beside its new name at any value, is
25
- * refused, naming the variables and never a value.
26
- */
27
-
28
- import { configRefused, issuesAt, pathsRelocated } from "./bootRefusal.mjs";
29
- import { refuseRenamedVariables } from "./rootRenames.mjs";
30
- import { type Adapters, adaptersSchema } from "./sections.mjs";
31
-
32
- /** The composition root's own section. No module may be named after it. */
33
- export const ADAPTERS_SECTION = "adapters";
34
-
35
- /** Each selection's key under `adapters`, and the variable its path is bound to. */
36
- const VARIABLES: Readonly<Record<keyof Adapters, string>> = {
37
- rateLimiter: "ADAPTERS_RATE_LIMITER",
38
- attemptCounter: "ADAPTERS_ATTEMPT_COUNTER",
39
- userSessionStores: "ADAPTERS_USER_SESSION_STORES",
40
- accessTokenDenylist: "ADAPTERS_ACCESS_TOKEN_DENYLIST",
41
- replaySeenSet: "ADAPTERS_REPLAY_SEEN_SET",
42
- consentStore: "ADAPTERS_CONSENT_STORE",
43
- federationTokenStore: "ADAPTERS_FEDERATION_TOKEN_STORE",
44
- federationGrantStore: "ADAPTERS_FEDERATION_GRANT_STORE",
45
- federationGrantIntentStore: "ADAPTERS_FEDERATION_GRANT_INTENT_STORE",
46
- mfaFactorStore: "ADAPTERS_MFA_FACTOR_STORE",
47
- mfaTransactionStore: "ADAPTERS_MFA_TRANSACTION_STORE",
48
- codeRepository: "ADAPTERS_CODE_REPOSITORY",
49
- clientRepository: "ADAPTERS_CLIENT_REPOSITORY",
50
- userRepository: "ADAPTERS_USER_REPOSITORY",
51
- auditSink: "ADAPTERS_AUDIT_SINK",
52
- };
53
-
54
- /** Each selection's old path, and its key under `adapters`. */
55
- const MOVED: readonly (readonly [string, keyof Adapters])[] = [
56
- ["rateLimiter.adapter", "rateLimiter"],
57
- ["userSessionStores.adapter", "userSessionStores"],
58
- ["accessTokenDenylist.adapter", "accessTokenDenylist"],
59
- ["replaySeenSet.adapter", "replaySeenSet"],
60
- ["consentStore.adapter", "consentStore"],
61
- ["federationTokenStore.type", "federationTokenStore"],
62
- ["federationGrantStore.adapter", "federationGrantStore"],
63
- ["federationGrantIntentStore.adapter", "federationGrantIntentStore"],
64
- ["mfaFactorStore.adapter", "mfaFactorStore"],
65
- ["mfaTransactionStore.adapter", "mfaTransactionStore"],
66
- ["oauth.code.adapter", "codeRepository"],
67
- ["repositories.code.type", "codeRepository"],
68
- ["repositories.client.type", "clientRepository"],
69
- ["repositories.user.type", "userRepository"],
70
- ["audit.sink.type", "auditSink"],
71
- ];
72
-
73
- /** Each variable renamed with a selection, and the selection's key under `adapters`. */
74
- const RENAMED: readonly (readonly [string, keyof Adapters])[] = [
75
- ["RATE_LIMITER_ADAPTER", "rateLimiter"],
76
- ["USER_SESSION_STORES_ADAPTER", "userSessionStores"],
77
- ["ACCESS_TOKEN_DENYLIST_ADAPTER", "accessTokenDenylist"],
78
- ["REPLAY_SEEN_SET_ADAPTER", "replaySeenSet"],
79
- ["CONSENT_STORE_ADAPTER", "consentStore"],
80
- ["FEDERATION_TOKEN_STORE_TYPE", "federationTokenStore"],
81
- ["FEDERATION_GRANT_STORE_ADAPTER", "federationGrantStore"],
82
- ["FEDERATION_GRANT_INTENT_STORE_ADAPTER", "federationGrantIntentStore"],
83
- ["MFA_FACTOR_STORE_ADAPTER", "mfaFactorStore"],
84
- ["MFA_TRANSACTION_STORE_ADAPTER", "mfaTransactionStore"],
85
- ["OAUTH_CODE_ADAPTER", "codeRepository"],
86
- ["CLIENT_CODE_TYPE", "codeRepository"],
87
- ["CLIENT_TYPE", "clientRepository"],
88
- ["CLIENT_USER_TYPE", "userRepository"],
89
- ["AUDIT_SINK_TYPE", "auditSink"],
90
- ];
91
-
92
- /** The value at a dotted `path` of `config`, read as own properties; `undefined` when absent. */
93
- function valueAt(config: unknown, path: string): unknown {
94
- let cursor: unknown = config;
95
- for (const key of path.split(".")) {
96
- if (typeof cursor !== "object" || cursor === null || !Object.hasOwn(cursor, key)) {
97
- return undefined;
98
- }
99
- cursor = (cursor as Record<string, unknown>)[key];
100
- }
101
- return cursor;
102
- }
103
-
104
- /**
105
- * `adapters` from `resolved` — the template's own layers over its
106
- * `config/reference.conf` — under `env`, the environment they were
107
- * substituted with, parsed with the template's schema. Refuses, each with a
108
- * `BootError` (`bootRefusal.mts`), a selection still written at the path it
109
- * moved from, naming its new path and variable (`config-path-relocated`);
110
- * then a variable renamed with one (`refuseRenamedVariables`); then a value
111
- * or a key the schema refuses, naming its path under `adapters`
112
- * (`config-validation-failed`).
113
- */
114
- export function readAdapters(
115
- resolved: Readonly<Record<string, unknown>>,
116
- env: Readonly<Record<string, string>>,
117
- ): Adapters {
118
- const moved = MOVED.filter(([from]) => valueAt(resolved, from) !== undefined).map(
119
- ([from, key]) => ({
120
- module: ADAPTERS_SECTION,
121
- from,
122
- to: `${ADAPTERS_SECTION}.${key}`,
123
- environmentVariable: VARIABLES[key],
124
- }),
125
- );
126
- if (moved.length > 0) {
127
- throw pathsRelocated(
128
- `Configuration sets ${moved.length} path(s) that moved: ${moved
129
- .map(
130
- ({ from, to, environmentVariable }) =>
131
- `${from} has moved to ${to}; see the upgrade guide (docs/upgrading-from-v0.16.0.md). Write it there (environment variable ${environmentVariable}) and remove this field from your config (or unset the environment variable that sets it).`,
132
- )
133
- .join(" ")}`,
134
- moved,
135
- );
136
- }
137
- refuseRenamedVariables(
138
- env,
139
- RENAMED.map(([from, key]) => ({
140
- module: ADAPTERS_SECTION,
141
- from,
142
- to: VARIABLES[key],
143
- path: `${ADAPTERS_SECTION}.${key}`,
144
- })),
145
- );
146
- const result = adaptersSchema.safeParse(resolved[ADAPTERS_SECTION]);
147
- if (!result.success) {
148
- const issues = issuesAt([ADAPTERS_SECTION], result.error.issues);
149
- throw configRefused(
150
- `Config validation failed — ${issues.length} issue(s) found: ${issues
151
- .map((issue) => {
152
- const unknown =
153
- issue.code === "unrecognized_keys" ? ` ${JSON.stringify(issue.keys)}` : "";
154
- return `${issue.path.map(String).join(".")}: ${issue.message}${unknown}`;
155
- })
156
- .join("; ")}`,
157
- issues,
158
- [{ module: ADAPTERS_SECTION, schemaPath: ADAPTERS_SECTION }],
159
- );
160
- }
161
- return result.data;
162
- }
@@ -1,136 +0,0 @@
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
- import { fileURLToPath } from "node:url";
17
- import { createApp } from "@o3co/auth-provider-core";
18
- import express from "express";
19
- import helmet from "helmet";
20
- import { buildModules } from "./buildModules.mjs";
21
- import {
22
- configDefaultsFor,
23
- readLogging,
24
- readOwnLayers,
25
- readSwitches,
26
- resolveConfigPaths,
27
- resolveForBoot,
28
- type Switches,
29
- } from "./configPath.mjs";
30
- import { listen } from "./listen.mjs";
31
- import { createAppLogger } from "./logger.mjs";
32
- import { createMetrics } from "./metrics.mjs";
33
- import { mountRoutes } from "./routes.mjs";
34
- import { installGracefulShutdown } from "./shutdown.mjs";
35
-
36
- // Step 1: the configuration, in two phases (`configPath.mts`; template
37
- // README, "Environment-specific config overlay").
38
- // ENV = CONFIG_ENV || NODE_ENV || "development"; missing {ENV}.conf is fatal.
39
- const env = process.env.CONFIG_ENV || process.env.NODE_ENV || "development";
40
- const configDir = new URL("../config/", import.meta.url);
41
- const configDirPath = fileURLToPath(configDir);
42
- const { applicationConfPath, envConfPath } = resolveConfigPaths(configDirPath, env);
43
- // Read once, so both phases read the same thing.
44
- const own = readOwnLayers([envConfPath, applicationConfPath]);
45
- // Phase one: what the template reads before it knows its modules, only for
46
- // those choices — its own `adapters` and MFA switch (`mfaMode`, MFA_MODE),
47
- // and whether federation grants are installed.
48
- const switches: Switches = readSwitches(own);
49
-
50
- // From the `logging` module's section, so the level holds from the first line;
51
- // wired into `bootstrapComponents` so every module logs through it (template
52
- // README, "Logging").
53
- const logger = createAppLogger(readLogging(own));
54
-
55
- await (async (): Promise<void> => {
56
- // Step 2: Create the Express app and apply base security middleware.
57
- // `trust proxy` is set from the `http` module's `httpSettings` once boot
58
- // has it (step 3), before a request can arrive.
59
- const app = express();
60
- app.use(
61
- helmet({
62
- contentSecurityPolicy: {
63
- directives: {
64
- defaultSrc: ["'none'"],
65
- frameAncestors: ["'none'"],
66
- },
67
- },
68
- }),
69
- );
70
-
71
- // Timing middleware goes ahead of everything so the histogram covers the
72
- // whole stack — including responses produced by middleware that short-circuits
73
- // before any route runs, such as the rate limiter's 429s.
74
- const metrics = createMetrics();
75
- app.use(metrics.middleware);
76
-
77
- // Step 3: boot the auth pipeline. `buildModules` is the single source of
78
- // truth for the module list and its order; `bootstrapComponents` carries
79
- // only host-environment values, every other component comes from the
80
- // modules. `environment` is the name the config was selected by: the Redis
81
- // federation-token store's plaintext guard reads it as well as NODE_ENV,
82
- // so `CONFIG_ENV=production` is production to the guard too.
83
- //
84
- // Phase two: `createApp` parses the configuration as resolved once, with
85
- // every loaded module's schema (`configPath.mts`), and names each section
86
- // nothing loaded reads by the configuration's defaults. From here on the
87
- // template reads the parsed configuration.
88
- const modules = buildModules(switches, { environment: env });
89
- const handle = await createApp({
90
- modules,
91
- bootstrapComponents: {
92
- config: resolveForBoot(own, modules, switches),
93
- configDefaults: configDefaultsFor(modules),
94
- pathResolver: import.meta.resolve,
95
- logger,
96
- },
97
- });
98
- // The `http` module's settings: core's slot and the host's own.
99
- const { httpSettings, httpHostSettings } = handle.components;
100
- if (httpSettings === undefined || httpHostSettings === undefined) {
101
- throw new Error("createApp booted without the http module's settings");
102
- }
103
- // `false` | `true` | a hop count | a list of IPs / CIDR ranges / named
104
- // ranges: the shapes `trust proxy` understands, validated at boot so a
105
- // typo'd range fails there rather than silently never matching. Prefer
106
- // naming the proxy: `true` believes a forwarded client address from anyone
107
- // who can reach this process, and every IP-keyed rate limit buckets on
108
- // `req.ip`.
109
- app.set("trust proxy", httpSettings.trustProxy);
110
-
111
- // Step 4: the host routes (liveness, readiness, metrics), then the composed
112
- // auth router, then the terminal error handler; `routes.mts` says why in
113
- // that order.
114
- mountRoutes(app, {
115
- router: handle.router,
116
- probes: handle.readinessProbes,
117
- readinessTimeoutMs: httpHostSettings.readinessTimeoutMs,
118
- metrics,
119
- logger,
120
- });
121
- // Step 5: start the HTTP server. A port that cannot be bound fails boot
122
- // with that error (`listen.mts`).
123
- const server = await listen(app, httpHostSettings.port, logger);
124
-
125
- // Step 6: graceful shutdown (`shutdown.mts`). Size `drainTimeoutMs` below
126
- // your orchestrator's kill grace period. Cleanup gets at least the
127
- // allowance the modules registered with their cleanups (45 s with
128
- // federation grants on under the shipped budgets) rather than only the
129
- // drain's ten; the template README, "Shutdown guarantees", sizes the grace
130
- // period for it.
131
- installGracefulShutdown(server, {
132
- logger,
133
- cleanup: () => handle.dispose(),
134
- cleanupAllowanceMs: () => handle.cleanupAllowanceMs,
135
- });
136
- })();