@o3co/create-auth-provider 0.15.0 → 0.17.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 (92) hide show
  1. package/README.ja.md +139 -48
  2. package/README.md +138 -47
  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 +69 -24
  12. package/templates/standalone/Dockerfile +7 -3
  13. package/templates/standalone/README.ja.md +677 -184
  14. package/templates/standalone/README.md +699 -238
  15. package/templates/standalone/config/application.conf +203 -325
  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 +399 -0
  22. package/templates/standalone/docker-compose.mailpit.yml +36 -0
  23. package/templates/standalone/docker-compose.production.yml +58 -30
  24. package/templates/standalone/docker-compose.yml +13 -3
  25. package/templates/standalone/gitignore +4 -2
  26. package/templates/standalone/package.json +9 -6
  27. package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +65 -32
  28. package/templates/standalone/src/__tests__/adapters.test.mts +358 -0
  29. package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +1178 -0
  30. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +256 -0
  31. package/templates/standalone/src/__tests__/all-modules-composition.test.mts +1119 -0
  32. package/templates/standalone/src/__tests__/audit-sink.test.mts +78 -76
  33. package/templates/standalone/src/__tests__/configPath.test.mts +13 -11
  34. package/templates/standalone/src/__tests__/core-federations.test.mts +226 -0
  35. package/templates/standalone/src/__tests__/core-sections.test.mts +190 -0
  36. package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +9 -14
  37. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +672 -271
  38. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +480 -78
  39. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +102 -66
  40. package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +135 -0
  41. package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +49 -18
  42. package/templates/standalone/src/__tests__/google-session.fixture.mts +315 -0
  43. package/templates/standalone/src/__tests__/host-routes.test.mts +134 -0
  44. package/templates/standalone/src/__tests__/library-references.fixture.mts +183 -0
  45. package/templates/standalone/src/__tests__/listen.test.mts +121 -0
  46. package/templates/standalone/src/__tests__/logger-projection.test.mts +77 -0
  47. package/templates/standalone/src/__tests__/mail-sender.test.mts +134 -0
  48. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +328 -0
  49. package/templates/standalone/src/__tests__/metrics.test.mts +17 -7
  50. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +123 -0
  51. package/templates/standalone/src/__tests__/mfa-switch.test.mts +780 -0
  52. package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +52 -46
  53. package/templates/standalone/src/__tests__/own-modules.test.mts +1457 -0
  54. package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +183 -0
  55. package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +135 -0
  56. package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +112 -81
  57. package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +25 -31
  58. package/templates/standalone/src/__tests__/replica-safety.test.mts +166 -145
  59. package/templates/standalone/src/__tests__/router-error-answers.test.mts +146 -0
  60. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +275 -95
  61. package/templates/standalone/src/__tests__/sections.test.mts +480 -0
  62. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +67 -55
  63. package/templates/standalone/src/__tests__/session-requirements.test.mts +299 -0
  64. package/templates/standalone/src/__tests__/shutdown.test.mts +319 -115
  65. package/templates/standalone/src/__tests__/smoke.test.mts +179 -233
  66. package/templates/standalone/src/__tests__/store-credential.test.mts +317 -0
  67. package/templates/standalone/src/__tests__/supertest-loopback.test.mts +182 -11
  68. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +108 -95
  69. package/templates/standalone/src/__tests__/two-phase-config.test.mts +438 -0
  70. package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +390 -0
  71. package/templates/standalone/src/adapters.mts +149 -0
  72. package/templates/standalone/src/app.mts +71 -115
  73. package/templates/standalone/src/buildModules.mts +229 -251
  74. package/templates/standalone/src/configPath.mts +422 -29
  75. package/templates/standalone/src/listen.mts +60 -0
  76. package/templates/standalone/src/logger.mts +31 -57
  77. package/templates/standalone/src/metrics.mts +24 -47
  78. package/templates/standalone/src/mfaSwitch.mts +262 -0
  79. package/templates/standalone/src/modules.mts +480 -464
  80. package/templates/standalone/src/rootRenames.mts +79 -0
  81. package/templates/standalone/src/routes.mts +89 -0
  82. package/templates/standalone/src/sections.mts +366 -0
  83. package/templates/standalone/src/shutdown.mts +54 -126
  84. package/templates/standalone/tests/index.test.js +2 -2
  85. package/templates/standalone/tests/package-lock.json +1615 -2223
  86. package/templates/standalone/tests/package.json +2 -2
  87. package/templates/standalone/tests/vitest.config.mjs +26 -0
  88. package/templates/standalone/vitest.config.mts +8 -7
  89. package/templates/standalone/vitest.supertest-loopback.mts +97 -69
  90. package/templates/versions.json +18 -15
  91. package/templates/standalone/src/__tests__/terminal-error.test.mts +0 -96
  92. package/templates/standalone/src/terminalError.mts +0 -60
@@ -14,61 +14,50 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { fileURLToPath } from "node:url";
17
- import {
18
- type AppConfig,
19
- AppConfigSchema,
20
- createApp,
21
- createHealthcheckRouter,
22
- createReadinessRouter,
23
- } from "@o3co/auth-provider-core";
24
- import { parseFile } from "@o3co/ts.hocon";
25
- import { validate } from "@o3co/ts.hocon/zod";
17
+ import { createApp } from "@o3co/auth-provider-core";
26
18
  import express from "express";
27
19
  import helmet from "helmet";
28
20
  import { buildModules } from "./buildModules.mjs";
29
- import { resolveConfigPaths, resolveLibraryReferenceConfPath } from "./configPath.mjs";
21
+ import {
22
+ readLogging,
23
+ readOwnLayers,
24
+ readSwitches,
25
+ resolveConfigPaths,
26
+ resolveForBoot,
27
+ type Switches,
28
+ } from "./configPath.mjs";
29
+ import { listen } from "./listen.mjs";
30
30
  import { createAppLogger } from "./logger.mjs";
31
31
  import { createMetrics } from "./metrics.mjs";
32
- import { cleanupAllowanceFor, installGracefulShutdown } from "./shutdown.mjs";
33
- import { createTerminalErrorHandler } from "./terminalError.mjs";
32
+ import { mountRoutes } from "./routes.mjs";
33
+ import { installGracefulShutdown } from "./shutdown.mjs";
34
34
 
35
- // Step 1: Load and validate application config (HOCON → Zod schema).
35
+ // Step 1: the configuration, in two phases (`configPath.mts`; template
36
+ // README, "Environment-specific config overlay").
36
37
  // ENV = CONFIG_ENV || NODE_ENV || "development"; missing {ENV}.conf is fatal.
37
- //
38
- // 3-tier HOCON precedence (highest → lowest):
39
- // 1. {env}.conf — environment-specific overrides (e.g. production.conf)
40
- // 2. application.conf — template-level consumer delta
41
- // 3. reference.conf — library defaults shipped in @o3co/auth-provider-core
42
38
  const env = process.env.CONFIG_ENV || process.env.NODE_ENV || "development";
43
39
  const configDir = new URL("../config/", import.meta.url);
44
40
  const configDirPath = fileURLToPath(configDir);
45
41
  const { applicationConfPath, envConfPath } = resolveConfigPaths(configDirPath, env);
46
- const libraryReferencePath = resolveLibraryReferenceConfPath();
47
- const config: AppConfig = validate(
48
- parseFile(envConfPath)
49
- .withFallback(parseFile(applicationConfPath))
50
- .withFallback(parseFile(libraryReferencePath)),
51
- AppConfigSchema,
52
- );
42
+ // Read once, so both phases read the same thing.
43
+ const own = readOwnLayers([envConfPath, applicationConfPath]);
44
+ // Phase one: what the template reads before it knows its modules, only for
45
+ // those choices — its own `adapters` and MFA switch (`mfaMode`, MFA_MODE),
46
+ // and the switches core's reader parses — and for what the composition
47
+ // expects of session admission: the configuration's list, with `mfa` added
48
+ // when the MFA switch installs MFA.
49
+ const switches: Switches = readSwitches(own);
53
50
 
54
- // The logger is built from config so its level is operator-controlled, and it
55
- // is wired into `bootstrapComponents` so every module that declares
56
- // `optional: ["logger"]` receives this one rather than falling back to its own
57
- // default. Without that slot filled, configuring a level here would change
58
- // nothing about what the modules emit.
59
- const logger = createAppLogger(config);
51
+ // From the `logging` module's section, so the level holds from the first line;
52
+ // wired into `bootstrapComponents` so every module logs through it (template
53
+ // README, "Logging").
54
+ const logger = createAppLogger(readLogging(own));
60
55
 
61
56
  await (async (): Promise<void> => {
62
57
  // Step 2: Create the Express app and apply base security middleware.
58
+ // `trust proxy` is set from the `http` module's `httpSettings` once boot
59
+ // has it (step 3), before a request can arrive.
63
60
  const app = express();
64
- // `false` | `true` | a hop count | a list of IPs / CIDR ranges / named
65
- // ranges — the schema hands Express exactly the shapes `trust proxy`
66
- // already understands, validated at boot so a typo'd range fails here
67
- // rather than becoming a rule that silently never matches (#292). Prefer
68
- // naming the proxy: `true` believes a forwarded client address from anyone
69
- // who can reach this process, and `req.ip` is what every IP-keyed rate
70
- // limit is bucketed on.
71
- app.set("trust proxy", config.http.trustProxy);
72
61
  app.use(
73
62
  helmet({
74
63
  contentSecurityPolicy: {
@@ -86,94 +75,61 @@ await (async (): Promise<void> => {
86
75
  const metrics = createMetrics();
87
76
  app.use(metrics.middleware);
88
77
 
89
- // Step 3: Boot the auth pipeline.
90
- // D-5: express-session middleware is now wired by `sessionStoreModule`
91
- // inside the boot planner. Mount order is enforced by declarationIndex
92
- // tie-breaking — sessionStoreModule MUST come first in `buildModules(...)`
93
- // so the middleware initialises `req.session` for every downstream route.
94
- // The connect-redis client's lifetime is owned by the planner via
95
- // `BuilderContext.lifecycle` and drained on `handle.dispose()`.
96
- // bootstrapComponents carries only host-
97
- // environment values (config + pathResolver per A2-γ §4 worked example);
98
- // every other component flows through composition-root-local modules.
99
- // `buildModules` is the single source of truth for the module list — it
100
- // gates federation modules on `config.federations.<name>.enabled`, which
101
- // the standalone scaffold defaults to false.
78
+ // Step 3: boot the auth pipeline. `buildModules` is the single source of
79
+ // truth for the module list and its order; `bootstrapComponents` carries
80
+ // only host-environment values, every other component comes from the
81
+ // modules. `environment` is the name the config was selected by: the Redis
82
+ // federation-token store's plaintext guard reads it as well as NODE_ENV,
83
+ // so `CONFIG_ENV=production` is production to the guard too.
102
84
  //
103
- // `environment` is the name the config above was selected by (#473): the
104
- // Redis federation-token store's plaintext guard reads it in addition to
105
- // NODE_ENV, so `CONFIG_ENV=production` is production to the guard too.
85
+ // Phase two: `createApp` parses the configuration as resolved once, with
86
+ // every loaded module's schema (`configPath.mts`). From here on the
87
+ // template reads the parsed configuration.
88
+ const modules = buildModules(switches, { environment: env, logger });
106
89
  const handle = await createApp({
107
- modules: buildModules(config, { environment: env }),
90
+ modules,
108
91
  bootstrapComponents: {
109
- config,
92
+ config: resolveForBoot(own, modules, switches),
110
93
  pathResolver: import.meta.resolve,
111
94
  logger,
112
95
  },
113
96
  });
97
+ // The `http` module's settings: core's slot and the host's own.
98
+ const { httpSettings, httpHostSettings } = handle.components;
99
+ if (httpSettings === undefined || httpHostSettings === undefined) {
100
+ throw new Error("createApp booted without the http module's settings");
101
+ }
102
+ // `false` | `true` | a hop count | a list of IPs / CIDR ranges / named
103
+ // ranges: the shapes `trust proxy` understands, validated at boot so a
104
+ // typo'd range fails there rather than silently never matching. Prefer
105
+ // naming the proxy: `true` believes a forwarded client address from anyone
106
+ // who can reach this process, and every IP-keyed rate limit buckets on
107
+ // `req.ip`.
108
+ app.set("trust proxy", httpSettings.trustProxy);
114
109
 
115
- // Step 4: Wire host-level routes before the auth router so they remain
116
- // reachable even when the auth pipeline is degraded — which is exactly when
117
- // an operator needs an answer from them.
118
- //
119
- // Liveness: the process is up and its event loop is turning. Deliberately
120
- // static — restarting the process would not bring Redis back, so a Redis
121
- // outage must not read as "this container is broken, kill it".
122
- app.use(createHealthcheckRouter(express));
123
-
124
- // Readiness: can this replica serve right now? Redis backs sessions,
125
- // authorization codes and refresh-token families in the deployable
126
- // defaults, so a replica that has lost it answers 503 here and should be
127
- // taken out of rotation. Probes are contributed by the builders that own
128
- // each connection; a memory-only deployment registers none and is always
129
- // ready.
130
- app.use(
131
- createReadinessRouter(express, {
132
- probes: handle.readinessProbes,
133
- timeoutMs: config.http.readinessTimeoutMs,
134
- logger,
135
- }),
136
- );
137
-
138
- // Prometheus scrape endpoint. Same reasoning as the probes above: it has to
139
- // keep answering while the auth pipeline is degraded, because that is when
140
- // the series matter.
141
- app.use(
142
- metrics.route(express, {
143
- probes: handle.readinessProbes,
144
- probeTimeoutMs: config.http.readinessTimeoutMs,
145
- }),
146
- );
147
-
148
- // Step 5: Mount the composed auth router and start the HTTP server.
149
- app.use(handle.router);
150
- // Terminal error handler LAST (#293 item 8): Express routes an error only
151
- // to handlers registered after the route that threw it. See
152
- // `terminalError.mts` for what it catches and why.
153
- app.use(createTerminalErrorHandler(logger));
154
- const server = app.listen(config.http.port, (): void => {
155
- logger.info(`Server is running on http://localhost:${config.http.port}`);
110
+ // Step 4: the host routes (liveness, readiness, metrics), then the composed
111
+ // auth router, then the terminal error handler; `routes.mts` says why in
112
+ // that order.
113
+ mountRoutes(app, {
114
+ router: handle.router,
115
+ probes: handle.readinessProbes,
116
+ readinessTimeoutMs: httpHostSettings.readinessTimeoutMs,
117
+ metrics,
118
+ logger,
156
119
  });
120
+ // Step 5: start the HTTP server. A port that cannot be bound fails boot
121
+ // with that error (`listen.mts`).
122
+ const server = await listen(app, httpHostSettings.port, logger);
157
123
 
158
- // Step 6: Graceful shutdown — handle.dispose() runs reverse-topological
159
- // per-component cleanup (per A2-β §8.1) plus D-5 LifecycleRegistrar drain
160
- // (Redis clients, interval timers).
161
- //
162
- // #290: in the template rather than a dependency, because "does SIGTERM
163
- // wait for in-flight requests, and for how long?" has to be answerable
164
- // from the code you deploy. See `shutdown.mts` for the stated guarantees
165
- // and for the deadline the previous implementation did not have. Size
166
- // `drainTimeoutMs` below your orchestrator's kill grace period.
167
- //
168
- // #593 slice 7: with federation grants on, cleanup gets the longest refresh
169
- // tail the configured budgets allow plus a margin — 45 s under the shipped
170
- // ones — rather than inheriting the drain's ten: the dispose is what waits
171
- // for a rotated credential's write. The compose files size
172
- // `stop_grace_period` to cover the drain, this and an exit margin for the
173
- // shipped budgets; a raised budget raises both.
124
+ // Step 6: graceful shutdown (`shutdown.mts`). Size `drainTimeoutMs` below
125
+ // your orchestrator's kill grace period. Cleanup gets at least the
126
+ // allowance the modules registered with their cleanups (45 s with
127
+ // federation grants on under the shipped budgets) rather than only the
128
+ // drain's ten; the template README, "Shutdown guarantees", sizes the grace
129
+ // period for it.
174
130
  installGracefulShutdown(server, {
175
131
  logger,
176
132
  cleanup: () => handle.dispose(),
177
- ...cleanupAllowanceFor(config),
133
+ cleanupAllowanceMs: () => handle.cleanupAllowanceMs,
178
134
  });
179
135
  })();