@o3co/create-auth-provider 0.16.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 (90) 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 -26
  12. package/templates/standalone/Dockerfile +6 -2
  13. package/templates/standalone/README.ja.md +293 -162
  14. package/templates/standalone/README.md +533 -199
  15. package/templates/standalone/config/application.conf +196 -341
  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 +1 -0
  26. package/templates/standalone/package.json +9 -6
  27. package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +19 -20
  28. package/templates/standalone/src/__tests__/adapters.test.mts +358 -0
  29. package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +216 -141
  30. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +48 -33
  31. package/templates/standalone/src/__tests__/all-modules-composition.test.mts +77 -58
  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 +656 -269
  38. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +425 -84
  39. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +91 -70
  40. package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +7 -5
  41. package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +22 -19
  42. package/templates/standalone/src/__tests__/google-session.fixture.mts +53 -42
  43. package/templates/standalone/src/__tests__/host-routes.test.mts +4 -11
  44. package/templates/standalone/src/__tests__/library-references.fixture.mts +183 -0
  45. package/templates/standalone/src/__tests__/listen.test.mts +6 -13
  46. package/templates/standalone/src/__tests__/logger-projection.test.mts +1 -1
  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 +4 -9
  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 +7 -11
  55. package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +1 -5
  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 +153 -142
  59. package/templates/standalone/src/__tests__/router-error-answers.test.mts +4 -7
  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 +163 -96
  65. package/templates/standalone/src/__tests__/smoke.test.mts +179 -252
  66. package/templates/standalone/src/__tests__/store-credential.test.mts +91 -50
  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 +64 -79
  73. package/templates/standalone/src/buildModules.mts +212 -251
  74. package/templates/standalone/src/configPath.mts +422 -29
  75. package/templates/standalone/src/listen.mts +12 -18
  76. package/templates/standalone/src/logger.mts +31 -60
  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 +458 -476
  80. package/templates/standalone/src/rootRenames.mts +79 -0
  81. package/templates/standalone/src/routes.mts +8 -14
  82. package/templates/standalone/src/sections.mts +366 -0
  83. package/templates/standalone/src/shutdown.mts +45 -123
  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
@@ -14,56 +14,50 @@
14
14
  * limitations under the License.
15
15
  */
16
16
  import { fileURLToPath } from "node:url";
17
- import { type AppConfig, AppConfigSchema, createApp } from "@o3co/auth-provider-core";
18
- import { parseFile } from "@o3co/ts.hocon";
19
- import { validate } from "@o3co/ts.hocon/zod";
17
+ import { createApp } from "@o3co/auth-provider-core";
20
18
  import express from "express";
21
19
  import helmet from "helmet";
22
20
  import { buildModules } from "./buildModules.mjs";
23
- 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";
24
29
  import { listen } from "./listen.mjs";
25
30
  import { createAppLogger } from "./logger.mjs";
26
31
  import { createMetrics } from "./metrics.mjs";
27
32
  import { mountRoutes } from "./routes.mjs";
28
- import { cleanupAllowanceFor, installGracefulShutdown } from "./shutdown.mjs";
33
+ import { installGracefulShutdown } from "./shutdown.mjs";
29
34
 
30
- // 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").
31
37
  // ENV = CONFIG_ENV || NODE_ENV || "development"; missing {ENV}.conf is fatal.
32
- //
33
- // 3-tier HOCON precedence (highest → lowest):
34
- // 1. {env}.conf — environment-specific overrides (e.g. production.conf)
35
- // 2. application.conf — template-level consumer delta
36
- // 3. reference.conf — library defaults shipped in @o3co/auth-provider-core
37
38
  const env = process.env.CONFIG_ENV || process.env.NODE_ENV || "development";
38
39
  const configDir = new URL("../config/", import.meta.url);
39
40
  const configDirPath = fileURLToPath(configDir);
40
41
  const { applicationConfPath, envConfPath } = resolveConfigPaths(configDirPath, env);
41
- const libraryReferencePath = resolveLibraryReferenceConfPath();
42
- const config: AppConfig = validate(
43
- parseFile(envConfPath)
44
- .withFallback(parseFile(applicationConfPath))
45
- .withFallback(parseFile(libraryReferencePath)),
46
- AppConfigSchema,
47
- );
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);
48
50
 
49
- // The logger is built from config so its level is operator-controlled, and it
50
- // is wired into `bootstrapComponents` so every module that declares
51
- // `optional: ["logger"]` receives this one rather than falling back to its own
52
- // default. Without that slot filled, configuring a level here would change
53
- // nothing about what the modules emit.
54
- 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));
55
55
 
56
56
  await (async (): Promise<void> => {
57
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.
58
60
  const app = express();
59
- // `false` | `true` | a hop count | a list of IPs / CIDR ranges / named
60
- // ranges — the schema hands Express exactly the shapes `trust proxy`
61
- // already understands, validated at boot so a typo'd range fails here
62
- // rather than becoming a rule that silently never matches (#292). Prefer
63
- // naming the proxy: `true` believes a forwarded client address from anyone
64
- // who can reach this process, and `req.ip` is what every IP-keyed rate
65
- // limit is bucketed on.
66
- app.set("trust proxy", config.http.trustProxy);
67
61
  app.use(
68
62
  helmet({
69
63
  contentSecurityPolicy: {
@@ -81,70 +75,61 @@ await (async (): Promise<void> => {
81
75
  const metrics = createMetrics();
82
76
  app.use(metrics.middleware);
83
77
 
84
- // Step 3: Boot the auth pipeline.
85
- // D-5: express-session middleware is now wired by `sessionStoreModule`
86
- // inside the boot planner. Mount order is enforced by declarationIndex
87
- // tie-breaking — sessionStoreModule MUST come first in `buildModules(...)`
88
- // so the middleware initialises `req.session` for every downstream route.
89
- // The connect-redis client's lifetime is owned by the planner via
90
- // `BuilderContext.lifecycle` and drained on `handle.dispose()`.
91
- // bootstrapComponents carries only host-
92
- // environment values (config + pathResolver per A2-γ §4 worked example);
93
- // every other component flows through composition-root-local modules.
94
- // `buildModules` is the single source of truth for the module list — it
95
- // gates federation modules on `config.federations.<name>.enabled`, which
96
- // 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.
97
84
  //
98
- // `environment` is the name the config above was selected by (#473): the
99
- // Redis federation-token store's plaintext guard reads it in addition to
100
- // NODE_ENV, so `CONFIG_ENV=production` is production to the guard too.
101
- // `logger` is where the composition's own notices go (a deprecated config
102
- // key), the same logger every module gets through the slot below.
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 });
103
89
  const handle = await createApp({
104
- modules: buildModules(config, { environment: env, logger }),
90
+ modules,
105
91
  bootstrapComponents: {
106
- config,
92
+ config: resolveForBoot(own, modules, switches),
107
93
  pathResolver: import.meta.resolve,
108
94
  logger,
109
95
  },
110
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);
111
109
 
112
- // Step 4: the host-level routes (liveness, readiness, metrics) ahead of the
113
- // auth router so they remain reachable even when the auth pipeline is
114
- // degraded — which is exactly when an operator needs an answer from them —
115
- // then the composed auth router, then the terminal error handler. See
116
- // `routes.mts` for what each is and why the order is what it is.
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.
117
113
  mountRoutes(app, {
118
114
  router: handle.router,
119
115
  probes: handle.readinessProbes,
120
- readinessTimeoutMs: config.http.readinessTimeoutMs,
116
+ readinessTimeoutMs: httpHostSettings.readinessTimeoutMs,
121
117
  metrics,
122
118
  logger,
123
119
  });
124
- // Step 5: start the HTTP server.
125
- // Resolves once the socket is bound (`server_listening`); a port that
126
- // cannot be bound rejects, and boot fails with that error (`listen.mts`).
127
- const server = await listen(app, config.http.port, logger);
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);
128
123
 
129
- // Step 6: Graceful shutdown — handle.dispose() runs reverse-topological
130
- // per-component cleanup (per A2-β §8.1) plus D-5 LifecycleRegistrar drain
131
- // (Redis clients, interval timers).
132
- //
133
- // #290: in the template rather than a dependency, because "does SIGTERM
134
- // wait for in-flight requests, and for how long?" has to be answerable
135
- // from the code you deploy. See `shutdown.mts` for the stated guarantees
136
- // and for the deadline the previous implementation did not have. Size
137
- // `drainTimeoutMs` below your orchestrator's kill grace period.
138
- //
139
- // #593 slice 7: with federation grants on, cleanup gets the longest refresh
140
- // tail the configured budgets allow plus a margin — 45 s under the shipped
141
- // ones — rather than inheriting the drain's ten: the dispose is what waits
142
- // for a rotated credential's write. The compose files size
143
- // `stop_grace_period` to cover the drain, this and an exit margin for the
144
- // 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.
145
130
  installGracefulShutdown(server, {
146
131
  logger,
147
132
  cleanup: () => handle.dispose(),
148
- ...cleanupAllowanceFor(config),
133
+ cleanupAllowanceMs: () => handle.cleanupAllowanceMs,
149
134
  });
150
135
  })();