@o3co/create-auth-provider 0.17.0 → 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 (114) 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 -912
  19. package/templates/standalone/README.md +0 -1536
  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 -124
  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 -427
  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 -56
  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 -1188
  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 -355
  39. package/templates/standalone/src/__tests__/backchannel-peer.fixture.mts +0 -64
  40. package/templates/standalone/src/__tests__/config-notices.test.mts +0 -157
  41. package/templates/standalone/src/__tests__/configPath.test.mts +0 -69
  42. package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +0 -48
  43. package/templates/standalone/src/__tests__/core-federations.test.mts +0 -252
  44. package/templates/standalone/src/__tests__/core-sections.test.mts +0 -192
  45. package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +0 -95
  46. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +0 -1291
  47. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +0 -787
  48. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +0 -464
  49. package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +0 -141
  50. package/templates/standalone/src/__tests__/federation-types.test.mts +0 -239
  51. package/templates/standalone/src/__tests__/frontchannel-logout-csp.test.mts +0 -168
  52. package/templates/standalone/src/__tests__/google-session.fixture.mts +0 -318
  53. package/templates/standalone/src/__tests__/host-routes.test.mts +0 -134
  54. package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +0 -29
  55. package/templates/standalone/src/__tests__/library-references.fixture.mts +0 -188
  56. package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +0 -217
  57. package/templates/standalone/src/__tests__/listen.test.mts +0 -121
  58. package/templates/standalone/src/__tests__/logger-projection.test.mts +0 -77
  59. package/templates/standalone/src/__tests__/logout-session-cookie.test.mts +0 -113
  60. package/templates/standalone/src/__tests__/mail-sender.test.mts +0 -134
  61. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +0 -393
  62. package/templates/standalone/src/__tests__/metrics.test.mts +0 -253
  63. package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +0 -122
  64. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +0 -127
  65. package/templates/standalone/src/__tests__/mfa-switch.test.mts +0 -883
  66. package/templates/standalone/src/__tests__/own-modules.test.mts +0 -1480
  67. package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +0 -183
  68. package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +0 -283
  69. package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +0 -135
  70. package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +0 -214
  71. package/templates/standalone/src/__tests__/redis-stand-in.fixture.mts +0 -98
  72. package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +0 -249
  73. package/templates/standalone/src/__tests__/replica-safety.test.mts +0 -657
  74. package/templates/standalone/src/__tests__/repositories-logger.test.mts +0 -119
  75. package/templates/standalone/src/__tests__/router-error-answers.test.mts +0 -146
  76. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +0 -610
  77. package/templates/standalone/src/__tests__/sections.test.mts +0 -516
  78. package/templates/standalone/src/__tests__/session-lifecycle.test.mts +0 -213
  79. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +0 -305
  80. package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +0 -130
  81. package/templates/standalone/src/__tests__/session-requirements.test.mts +0 -382
  82. package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +0 -274
  83. package/templates/standalone/src/__tests__/shutdown.test.mts +0 -622
  84. package/templates/standalone/src/__tests__/smoke.test.mts +0 -642
  85. package/templates/standalone/src/__tests__/startup-log-lines.test.mts +0 -164
  86. package/templates/standalone/src/__tests__/store-credential.test.mts +0 -314
  87. package/templates/standalone/src/__tests__/supertest-loopback.test.mts +0 -296
  88. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +0 -265
  89. package/templates/standalone/src/__tests__/two-phase-config.test.mts +0 -605
  90. package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +0 -385
  91. package/templates/standalone/src/adapters.mts +0 -162
  92. package/templates/standalone/src/app.mts +0 -127
  93. package/templates/standalone/src/bootRefusal.mts +0 -117
  94. package/templates/standalone/src/buildModules.mts +0 -358
  95. package/templates/standalone/src/configPath.mts +0 -487
  96. package/templates/standalone/src/listen.mts +0 -60
  97. package/templates/standalone/src/logger.mts +0 -80
  98. package/templates/standalone/src/metrics.mts +0 -167
  99. package/templates/standalone/src/mfaSwitch.mts +0 -284
  100. package/templates/standalone/src/modules.mts +0 -705
  101. package/templates/standalone/src/rootRenames.mts +0 -95
  102. package/templates/standalone/src/routes.mts +0 -89
  103. package/templates/standalone/src/sections.mts +0 -378
  104. package/templates/standalone/src/securityHeaders.mts +0 -36
  105. package/templates/standalone/src/shutdown.mts +0 -213
  106. package/templates/standalone/tests/Makefile +0 -5
  107. package/templates/standalone/tests/index.test.js +0 -283
  108. package/templates/standalone/tests/package-lock.json +0 -1617
  109. package/templates/standalone/tests/package.json +0 -16
  110. package/templates/standalone/tests/vitest.config.mjs +0 -26
  111. package/templates/standalone/tsconfig.json +0 -31
  112. package/templates/standalone/vitest.config.mts +0 -31
  113. package/templates/standalone/vitest.supertest-loopback.mts +0 -278
  114. package/templates/versions.json +0 -20
@@ -1,318 +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
- * 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.
23
- *
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.
28
- *
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.
32
- */
33
-
34
- import { createHmac } from "node:crypto";
35
- import { fileURLToPath } from "node:url";
36
- import {
37
- createApp,
38
- createKeyStoreFactory,
39
- defineModule,
40
- type FederationTokenStore,
41
- type FederationTokens,
42
- InMemoryClientRepository,
43
- InMemoryUserRepository,
44
- memoryRefreshTokenFamilyStoreModule,
45
- registerBuiltinKeyStores,
46
- type SessionLifecycle,
47
- type UserSessionStore,
48
- } from "@o3co/auth-provider-core";
49
- import { googleFederationTypeModule } from "@o3co/auth-provider-federation-google";
50
- import { parseFile } from "@o3co/ts.hocon";
51
- import express from "express";
52
- import request from "supertest";
53
- import { expect } from "vitest";
54
- import { buildModules } from "#/buildModules.mjs";
55
- import { resolveConfigPaths } from "#/configPath.mjs";
56
- import { templateReference } from "../modules.mjs";
57
- import { keyStoreSectionSchema } from "../sections.mjs";
58
- import {
59
- type BothPhases,
60
- bothPhasesOf,
61
- capturedRenames,
62
- libraryLayers,
63
- } from "./library-references.fixture.mjs";
64
-
65
- const configDir = fileURLToPath(new URL("../../config", import.meta.url));
66
-
67
- export const ISSUER = "https://auth.test";
68
- const JWT_SECRET = "google-session.fixture.at-least-32-bytes.ok";
69
-
70
- /** The one client: first-party, allowed federation tokens, one post-logout URI registered. */
71
- export const CLIENT_ID = "rp";
72
- const CLIENT_SECRET = "rp-secret-value";
73
- const BASIC = `Basic ${Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString("base64")}`;
74
- export const REGISTERED_POST_LOGOUT_REDIRECT_URI = "https://rp.example/signed-out";
75
-
76
- const USERNAME = "alice";
77
- const PASSWORD = "correct-horse-battery-staple";
78
- const SUB = "u-alice";
79
-
80
- /** The Google client the adapter is configured with. */
81
- export const GOOGLE_CLIENT_ID = "google-client";
82
- const GOOGLE_CLIENT_SECRET = "google-secret";
83
- const GOOGLE_CALLBACK = `${ISSUER}/session/oauth/federation/google/callback`;
84
-
85
- const ENV: Readonly<Record<string, string>> = {
86
- // A session signed in through Google alone, no second factor.
87
- MFA_MODE: "off",
88
- KEY_STORE_LOCAL_ALGORITHM: "HS256",
89
- KEY_STORE_LOCAL_SECRET: JWT_SECRET,
90
- OAUTH_JWT_ISSUER: ISSUER,
91
- SESSION_STORE_SECRET: "google-session.fixture-session.at-least-32-bytes.ok",
92
- SESSION_STORE_SECURE: "false",
93
- SESSION_STORE_NAME: "auth.session",
94
- SESSION_STORE_STORAGE_TYPE: "memory",
95
- ADAPTERS_USER_REPOSITORY: "yaml",
96
- REDIS_CLIENTS_URL: "redis://redis.test:6379",
97
- ADAPTERS_USER_SESSION_STORES: "memory",
98
- ADAPTERS_RATE_LIMITER: "memory",
99
- ADAPTERS_CODE_REPOSITORY: "memory",
100
- ADAPTERS_ACCESS_TOKEN_DENYLIST: "memory",
101
- ADAPTERS_REPLAY_SEEN_SET: "memory",
102
- ADAPTERS_FEDERATION_TOKEN_STORE: "memory",
103
- ADAPTERS_CONSENT_STORE: "none",
104
- };
105
-
106
- /**
107
- * Google switched on the way an operator does, through its environment
108
- * variables, and handled by the Google federation type the template lists.
109
- * `"shipped"` is that alone. Otherwise, beside it: `endSessionEndpoint`, a key
110
- * of the entry with no environment form, written into the entry as an
111
- * operator's own layer would; and `fetch`, the one the type module is built
112
- * with in place of the global one.
113
- */
114
- export type GoogleWiring =
115
- | "shipped"
116
- | { readonly endSessionEndpoint?: string; readonly fetch?: typeof fetch };
117
-
118
- const GOOGLE_ENV: Readonly<Record<string, string>> = {
119
- CORE_FEDERATIONS_GOOGLE_ENABLED: "true",
120
- CORE_FEDERATIONS_GOOGLE_CLIENT_ID: GOOGLE_CLIENT_ID,
121
- CORE_FEDERATIONS_GOOGLE_CLIENT_SECRET: GOOGLE_CLIENT_SECRET,
122
- CORE_FEDERATIONS_GOOGLE_CALLBACK_URL: GOOGLE_CALLBACK,
123
- };
124
-
125
- function resolveConfig(google: GoogleWiring): BothPhases {
126
- const env: Record<string, string> = { ...ENV, ...GOOGLE_ENV };
127
- const { applicationConfPath, envConfPath } = resolveConfigPaths(configDir, "production");
128
- const layers = parseFile(envConfPath, { env })
129
- .withFallback(parseFile(applicationConfPath, { env }))
130
- .withFallback(parseFile(fileURLToPath(templateReference()), { env }))
131
- .withFallback(libraryLayers(env));
132
- const config = bothPhasesOf(layers, env);
133
- const endSessionEndpoint = google === "shipped" ? undefined : google.endSessionEndpoint;
134
- const federations = config.core?.federations;
135
- return {
136
- ...config,
137
- ...(endSessionEndpoint === undefined
138
- ? {}
139
- : {
140
- core: {
141
- ...config.core,
142
- federations: {
143
- ...federations,
144
- google: { ...federations?.google, endSessionEndpoint },
145
- },
146
- },
147
- }),
148
- // What the resolution captured of the renamed variables.
149
- "renamed-variables": capturedRenames(env),
150
- } as BothPhases;
151
- }
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: [],
165
- allowedScopes: ["openid", "profile"],
166
- allowedAudiences: [],
167
- allowedGrantTypes: ["session"],
168
- postLogoutRedirectUris: [REGISTERED_POST_LOGOUT_REDIRECT_URI],
169
- backchannelLogoutSessionRequired: true,
170
- frontchannelLogoutSessionRequired: true,
171
- allowedAzpForFederationToken: true,
172
- },
173
- ],
174
- ]),
175
- ),
176
- userRepository: () =>
177
- new InMemoryUserRepository(
178
- new Map([[USERNAME, { id: SUB, password: PASSWORD, email: "alice@example.com" }]]),
179
- ),
180
- } as never,
181
- });
182
-
183
- // The template's `key-store` section, read by its own schema, without the
184
- // section's relocation bridge.
185
- const testKeyStoreModule = defineModule({
186
- name: "key-store",
187
- section: { schema: keyStoreSectionSchema },
188
- provides: {
189
- keyStore: async ({ section }) => {
190
- const factory = createKeyStoreFactory();
191
- registerBuiltinKeyStores(factory);
192
- return factory.create({
193
- type: "local",
194
- ...(section.local ?? {}),
195
- });
196
- },
197
- },
198
- });
199
-
200
- const base64url = (value: unknown): string =>
201
- Buffer.from(JSON.stringify(value)).toString("base64url");
202
-
203
- /** A JWT signed with the deployment's own HS256 key: what its keystore verifies. */
204
- function signed(typ: string, claims: Record<string, unknown>): string {
205
- const header = base64url({ alg: "HS256", kid: "v0", typ });
206
- const now = Math.floor(Date.now() / 1000);
207
- const payload = base64url({ iss: ISSUER, sub: SUB, iat: now, exp: now + 3600, ...claims });
208
- const signature = createHmac("sha256", JWT_SECRET)
209
- .update(`${header}.${payload}`)
210
- .digest("base64url");
211
- return `${header}.${payload}.${signature}`;
212
- }
213
-
214
- /**
215
- * A fresh id_token for the session, issued to the client: what an RP — or
216
- * anyone holding an id_token of their own — presents as `id_token_hint`.
217
- */
218
- export const idTokenHint = (sid: string): string => signed("JWT", { aud: CLIENT_ID, sid });
219
-
220
- /**
221
- * An access token for the session as the authorization-code grant issues it
222
- * to the client, for the client itself: `aud` and `azp`, `sid` and a
223
- * refresh-token `family_id`. The `session` grant's token names no family, and
224
- * the federation token route asks for one.
225
- */
226
- export const accessTokenWithFamily = (sid: string): string =>
227
- signed("at+jwt", {
228
- aud: CLIENT_ID,
229
- azp: CLIENT_ID,
230
- sid,
231
- family_id: "family-1",
232
- scope: "openid",
233
- });
234
-
235
- const claimsOf = (jwt: string): Record<string, unknown> =>
236
- JSON.parse(Buffer.from(jwt.split(".")[1] ?? "", "base64url").toString("utf8")) as Record<
237
- string,
238
- unknown
239
- >;
240
-
241
- export interface GoogleSession {
242
- readonly app: express.Express;
243
- readonly sid: string;
244
- /** The access token the `session` grant minted for the session; it carries `azp`. */
245
- readonly accessToken: string;
246
- readonly userSessionStore: UserSessionStore;
247
- readonly sessionLifecycle: SessionLifecycle;
248
- readonly federationTokenStore: FederationTokenStore;
249
- readonly dispose: () => Promise<void>;
250
- }
251
-
252
- /**
253
- * Boots the deployment, signs alice in with her password, and links Google to
254
- * her session as a federated sign-in would: the token store holds `tokens`,
255
- * and Google joins the session through its lifecycle.
256
- */
257
- export async function signInLinkedToGoogle(options: {
258
- readonly google: GoogleWiring;
259
- readonly tokens: FederationTokens;
260
- }): Promise<GoogleSession> {
261
- const { google, tokens } = options;
262
- const config = resolveConfig(google);
263
- const upstreamFetch = google === "shipped" ? undefined : google.fetch;
264
- const googleType = googleFederationTypeModule().name;
265
- const handle = await createApp({
266
- modules: buildModules(config, {
267
- keyStoreModule: testKeyStoreModule,
268
- repositoriesModule: testRepositoriesModule,
269
- refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
270
- }).map((module) =>
271
- module.name === googleType && upstreamFetch !== undefined
272
- ? googleFederationTypeModule({ fetch: upstreamFetch })
273
- : module,
274
- ),
275
- bootstrapComponents: { config, pathResolver: (s) => s },
276
- });
277
- try {
278
- const components = handle.components as {
279
- userSessionStore: UserSessionStore;
280
- sessionLifecycle: SessionLifecycle;
281
- federationTokenStore: FederationTokenStore;
282
- };
283
- const app = express();
284
- app.use(handle.router);
285
-
286
- const csrfRes = await request(app).get("/session/csrf");
287
- expect(csrfRes.status).toBe(200);
288
- const loginRes = await request(app)
289
- .post("/session/login")
290
- .set("Cookie", csrfRes.headers["set-cookie"] as unknown as string[])
291
- .set(csrfRes.body.header_name as string, csrfRes.body.csrf_token as string)
292
- .type("form")
293
- .send({ username: USERNAME, password: PASSWORD });
294
- expect(loginRes.status).toBe(200);
295
-
296
- const tokenRes = await request(app)
297
- .post("/oauth/token")
298
- .set("Authorization", BASIC)
299
- .set("Cookie", loginRes.headers["set-cookie"] as unknown as string[])
300
- .type("form")
301
- .send({ grant_type: "session" });
302
- expect(tokenRes.status).toBe(200);
303
- const accessToken = tokenRes.body.access_token as string;
304
- const claims = claimsOf(accessToken);
305
- expect(claims.azp).toBe(CLIENT_ID);
306
- const sid = claims.sid as string;
307
- expect(typeof sid).toBe("string");
308
-
309
- await components.federationTokenStore.attach(sid, "google", tokens);
310
- expect(await components.sessionLifecycle.join(sid, { federation: "google" })).toEqual({
311
- outcome: "joined",
312
- });
313
- return { app, sid, accessToken, ...components, dispose: () => handle.dispose() };
314
- } catch (error) {
315
- await handle.dispose();
316
- throw error;
317
- }
318
- }
@@ -1,134 +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 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.
22
- */
23
-
24
- import type { Logger, ReadinessProbe } from "@o3co/auth-provider-core";
25
- import express, { Router } from "express";
26
- import request from "supertest";
27
- import { describe, expect, it, vi } from "vitest";
28
- import { createMetrics } from "#/metrics.mjs";
29
- import { mountRoutes } from "#/routes.mjs";
30
-
31
- const spyLogger = () => {
32
- const logger = {
33
- trace: vi.fn(),
34
- debug: vi.fn(),
35
- info: vi.fn(),
36
- warn: vi.fn(),
37
- error: vi.fn(),
38
- fatal: vi.fn(),
39
- child: () => logger,
40
- };
41
- return logger;
42
- };
43
-
44
- /** A probe that breaks its own contract: reading its `name` throws. */
45
- const probeWhoseNameThrows = (thrown: Error): ReadinessProbe =>
46
- ({
47
- get name(): string {
48
- throw thrown;
49
- },
50
- check: async () => undefined,
51
- }) as ReadinessProbe;
52
-
53
- const host = (probes: readonly ReadinessProbe[]) => {
54
- const logger = spyLogger();
55
- const app = express();
56
- mountRoutes(app, {
57
- router: Router(),
58
- probes,
59
- readinessTimeoutMs: 1_000,
60
- metrics: createMetrics(),
61
- logger: logger as unknown as Logger,
62
- });
63
- return { app, logger };
64
- };
65
-
66
- /** Every call on a level other than `error`. */
67
- const otherLevels = (logger: ReturnType<typeof spyLogger>) =>
68
- [logger.trace, logger.debug, logger.info, logger.warn, logger.fatal].flatMap(
69
- (level) => level.mock.calls,
70
- );
71
-
72
- describe("the host's routes, mounted as the process mounts them", () => {
73
- it("/_healthcheck answers 200, and nothing is logged", async () => {
74
- const { app, logger } = host([]);
75
- const res = await request(app).get("/_healthcheck");
76
- expect(res.status).toBe(200);
77
- expect(res.body).toEqual({ status: "ok" });
78
- expect(logger.error).not.toHaveBeenCalled();
79
- expect(otherLevels(logger)).toEqual([]);
80
- });
81
-
82
- it.each(["/readyz", "/metrics"])(
83
- "%s: an error it lets through is 500 server_error in the envelope, no-store, logged once with its projection",
84
- async (path) => {
85
- const { app, logger } = host([
86
- probeWhoseNameThrows(new Error("probe registry at internal-host-marker is gone")),
87
- ]);
88
-
89
- const res = await request(app).get(path);
90
-
91
- expect(res.status).toBe(500);
92
- expect(res.headers["content-type"]).toMatch(/^application\/json/);
93
- expect(res.headers["cache-control"]).toBe("no-store");
94
- expect(res.body).toEqual({ error: "server_error", error_description: "unexpected_error" });
95
- expect(res.text).not.toContain("internal-host-marker");
96
- expect(logger.error).toHaveBeenCalledTimes(1);
97
- const [fields, event, ...rest] = logger.error.mock.calls[0] as unknown[];
98
- expect(event).toBe("unhandled_request_error");
99
- expect(rest).toEqual([]);
100
- expect(fields).toEqual({
101
- endpoint: path,
102
- err: expect.objectContaining({
103
- name: "Error",
104
- detail: "probe registry at internal-host-marker is gone",
105
- }),
106
- });
107
- expect((fields as { err: unknown }).err).not.toBeInstanceOf(Error);
108
- expect(otherLevels(logger)).toEqual([]);
109
- },
110
- );
111
-
112
- it("takes a 4xx status an error did not mark as the client's for the server's: 500, logged", async () => {
113
- // A store's or a library's error can carry an HTTP status of its own —
114
- // the answer it got — which is not this server's answer to the client.
115
- const { app, logger } = host([
116
- probeWhoseNameThrows(
117
- Object.assign(new Error("the probe registry refused this deployment"), { status: 403 }),
118
- ),
119
- ]);
120
-
121
- const res = await request(app).get("/readyz");
122
-
123
- expect(res.status).toBe(500);
124
- expect(res.body).toEqual({ error: "server_error", error_description: "unexpected_error" });
125
- expect(logger.error).toHaveBeenCalledTimes(1);
126
- expect(logger.error).toHaveBeenCalledWith(
127
- {
128
- endpoint: "/readyz",
129
- err: expect.objectContaining({ name: "Error", status: 403 }),
130
- },
131
- "unhandled_request_error",
132
- );
133
- });
134
- });
@@ -1,29 +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
- import { existsSync, readFileSync } from "node:fs";
18
- import { fileURLToPath } from "node:url";
19
- import { describe, expect, it } from "vitest";
20
-
21
- describe("library reference.conf subpath resolution", () => {
22
- it("resolves @o3co/auth-provider-core/reference.conf to a readable file", () => {
23
- const url = import.meta.resolve("@o3co/auth-provider-core/reference.conf");
24
- const path = fileURLToPath(url);
25
- expect(existsSync(path)).toBe(true);
26
- const content = readFileSync(path, "utf-8");
27
- expect(content).toMatch(/^core\s*\{/m); // sanity: HOCON content, core's own section
28
- });
29
- });
@@ -1,188 +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 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
- }