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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.ja.md +66 -21
  2. package/README.md +76 -31
  3. package/dist/index.d.mts +33 -10
  4. package/dist/index.d.mts.map +1 -1
  5. package/dist/index.mjs +118 -59
  6. package/dist/internal/template-filter.d.mts +7 -10
  7. package/dist/internal/template-filter.d.mts.map +1 -1
  8. package/dist/internal/template-filter.mjs +7 -10
  9. package/package.json +6 -4
  10. package/templates/standalone/.dockerignore +15 -2
  11. package/templates/standalone/.env.example +59 -29
  12. package/templates/standalone/Dockerfile +6 -2
  13. package/templates/standalone/README.ja.md +324 -169
  14. package/templates/standalone/README.md +618 -208
  15. package/templates/standalone/config/application.conf +211 -352
  16. package/templates/standalone/config/clients.yaml +2 -0
  17. package/templates/standalone/config/clients.yaml.example +4 -0
  18. package/templates/standalone/config/development.conf +13 -4
  19. package/templates/standalone/config/mailpit.conf +8 -0
  20. package/templates/standalone/config/production.conf +5 -6
  21. package/templates/standalone/config/reference.conf +418 -0
  22. package/templates/standalone/docker-compose.mailpit.yml +36 -0
  23. package/templates/standalone/docker-compose.production.yml +63 -32
  24. package/templates/standalone/docker-compose.yml +13 -3
  25. package/templates/standalone/gitignore +1 -0
  26. package/templates/standalone/package.json +9 -6
  27. package/templates/standalone/src/__tests__/adapters.test.mts +419 -0
  28. package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +87 -0
  29. package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +287 -198
  30. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +71 -35
  31. package/templates/standalone/src/__tests__/all-modules-composition.test.mts +148 -94
  32. package/templates/standalone/src/__tests__/audit-sink.test.mts +84 -81
  33. package/templates/standalone/src/__tests__/config-notices.test.mts +154 -0
  34. package/templates/standalone/src/__tests__/configPath.test.mts +13 -11
  35. package/templates/standalone/src/__tests__/core-federations.test.mts +249 -0
  36. package/templates/standalone/src/__tests__/core-sections.test.mts +190 -0
  37. package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +10 -15
  38. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +970 -288
  39. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +421 -86
  40. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +87 -72
  41. package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +16 -8
  42. package/templates/standalone/src/__tests__/federation-types.test.mts +239 -0
  43. package/templates/standalone/src/__tests__/google-session.fixture.mts +97 -93
  44. package/templates/standalone/src/__tests__/host-routes.test.mts +4 -11
  45. package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +1 -1
  46. package/templates/standalone/src/__tests__/library-references.fixture.mts +188 -0
  47. package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +216 -0
  48. package/templates/standalone/src/__tests__/listen.test.mts +6 -13
  49. package/templates/standalone/src/__tests__/logger-projection.test.mts +1 -1
  50. package/templates/standalone/src/__tests__/mail-sender.test.mts +134 -0
  51. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +326 -0
  52. package/templates/standalone/src/__tests__/metrics.test.mts +4 -9
  53. package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +122 -0
  54. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +123 -0
  55. package/templates/standalone/src/__tests__/mfa-switch.test.mts +825 -0
  56. package/templates/standalone/src/__tests__/own-modules.test.mts +1486 -0
  57. package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +10 -14
  58. package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +1 -5
  59. package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +116 -81
  60. package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +25 -31
  61. package/templates/standalone/src/__tests__/replica-safety.test.mts +265 -146
  62. package/templates/standalone/src/__tests__/router-error-answers.test.mts +4 -7
  63. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +278 -103
  64. package/templates/standalone/src/__tests__/sections.test.mts +480 -0
  65. package/templates/standalone/src/__tests__/session-lifecycle.test.mts +215 -0
  66. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +69 -61
  67. package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +130 -0
  68. package/templates/standalone/src/__tests__/session-requirements.test.mts +382 -0
  69. package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +270 -0
  70. package/templates/standalone/src/__tests__/shutdown.test.mts +163 -96
  71. package/templates/standalone/src/__tests__/smoke.test.mts +176 -258
  72. package/templates/standalone/src/__tests__/store-credential.test.mts +88 -52
  73. package/templates/standalone/src/__tests__/supertest-loopback.test.mts +182 -11
  74. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +131 -102
  75. package/templates/standalone/src/__tests__/two-phase-config.test.mts +604 -0
  76. package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +382 -0
  77. package/templates/standalone/src/adapters.mts +162 -0
  78. package/templates/standalone/src/app.mts +65 -79
  79. package/templates/standalone/src/bootRefusal.mts +117 -0
  80. package/templates/standalone/src/buildModules.mts +220 -309
  81. package/templates/standalone/src/configPath.mts +449 -29
  82. package/templates/standalone/src/listen.mts +12 -18
  83. package/templates/standalone/src/logger.mts +31 -60
  84. package/templates/standalone/src/metrics.mts +24 -47
  85. package/templates/standalone/src/mfaSwitch.mts +285 -0
  86. package/templates/standalone/src/modules.mts +423 -629
  87. package/templates/standalone/src/rootRenames.mts +96 -0
  88. package/templates/standalone/src/routes.mts +8 -14
  89. package/templates/standalone/src/sections.mts +367 -0
  90. package/templates/standalone/src/shutdown.mts +45 -123
  91. package/templates/standalone/tests/index.test.js +6 -5
  92. package/templates/standalone/tests/package-lock.json +1615 -2223
  93. package/templates/standalone/tests/package.json +2 -2
  94. package/templates/standalone/tests/vitest.config.mjs +26 -0
  95. package/templates/standalone/vitest.config.mts +8 -7
  96. package/templates/standalone/vitest.supertest-loopback.mts +97 -69
  97. package/templates/versions.json +18 -15
  98. package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +0 -135
  99. package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +0 -199
  100. package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +0 -138
@@ -15,94 +15,127 @@
15
15
  */
16
16
 
17
17
  import { fileURLToPath } from "node:url";
18
- import { type AppConfig, AppConfigSchema } from "@o3co/auth-provider-core";
18
+ import type { AppConfig } from "@o3co/auth-provider-core";
19
+ import { type OAuthSection, oauthEndpointsModule } from "@o3co/auth-provider-oauth";
19
20
  import { parseFile } from "@o3co/ts.hocon";
20
- import { validate } from "@o3co/ts.hocon/zod";
21
21
  import { describe, expect, it } from "vitest";
22
+ import { readAdapters } from "../adapters.mjs";
22
23
  import { buildModules } from "../buildModules.mjs";
23
- import { resolveConfigPaths, resolveLibraryReferenceConfPath } from "../configPath.mjs";
24
+ import {
25
+ readOwnLayers,
26
+ readSwitches,
27
+ resolveConfigPaths,
28
+ resolveForBoot,
29
+ type Switches,
30
+ } from "../configPath.mjs";
31
+ import { templateReference } from "../modules.mjs";
32
+ import { type BothPhases, withSwitches } from "./library-references.fixture.mjs";
24
33
 
25
34
  // config/ is two levels above this test file:
26
35
  // src/__tests__/ → src/ → standalone/ → config/
27
36
  const configDir = fileURLToPath(new URL("../../config", import.meta.url));
28
37
 
29
- // Provide required secrets so AppConfigSchema parse succeeds. These are
30
- // test-only values — no real keys are embedded here. #282: SESSION_SECRET
38
+ // Provide the secrets the template's files require. These are
39
+ // test-only values — no real keys are embedded here. SESSION_STORE_SECRET
31
40
  // carries a 256-bit entropy floor, so these clear it (the '.' characters keep
32
41
  // them outside the base64 alphabet, so the UTF-8 length is what counts).
33
42
  const testEnv = {
34
- OAUTH_JWT_SECRET: "test-secret-three-tier.at-least-32-bytes.ok",
43
+ KEY_STORE_LOCAL_SECRET: "test-secret-three-tier.at-least-32-bytes.ok",
35
44
  OAUTH_JWT_ISSUER: "https://auth.test",
36
- SESSION_SECRET: "test-session-secret-three-tier.at-least-32-bytes.ok",
45
+ SESSION_STORE_SECRET: "test-session-secret-three-tier.at-least-32-bytes.ok",
37
46
  };
38
47
 
39
- function buildResolvedConfig(env: string, extraEnv: Record<string, string> = {}): AppConfig {
48
+ /**
49
+ * The three tiers under `env`, as `app.mts` reads them: what phase one reads
50
+ * of the template's own layers, laid over what phase two hands boot — the
51
+ * own layers over the reference of every package the chosen modules come
52
+ * from, resolved and unparsed.
53
+ */
54
+ function buildResolvedConfig(env: string, extraEnv: Record<string, string> = {}): BothPhases {
40
55
  const { applicationConfPath, envConfPath } = resolveConfigPaths(configDir, env);
41
- const libraryReferencePath = resolveLibraryReferenceConfPath();
42
- const resolvedEnv = { ...testEnv, ...extraEnv };
43
- return validate(
44
- parseFile(envConfPath, { env: resolvedEnv })
45
- .withFallback(parseFile(applicationConfPath, { env: resolvedEnv }))
46
- .withFallback(parseFile(libraryReferencePath, { env: resolvedEnv })),
47
- AppConfigSchema,
56
+ const own = readOwnLayers([envConfPath, applicationConfPath], {
57
+ env: { ...testEnv, ...extraEnv },
58
+ });
59
+ const switches = readSwitches(own);
60
+ return withSwitches(
61
+ resolveForBoot(own, buildModules(switches), switches) as unknown as Record<string, unknown>,
62
+ switches,
48
63
  );
49
64
  }
50
65
 
51
- // `oauth.grants` is declared as `z.object({}).passthrough()` in the schema,
52
- // so the inferred TS type is `{}` per entry and accessing `.enabled` doesn't
53
- // compile under `tsc --strict`. Tests assert against the resolved runtime
54
- // shape (string from env substitution, boolean from literal), so the cast
55
- // is intentional and well-bounded.
66
+ /** `oauth.resourceIndicator` as the section's owner, the oauth module, parses it at boot. */
67
+ function parsedResourceIndicator(config: AppConfig): { readonly enabled?: unknown } | undefined {
68
+ const schema = oauthEndpointsModule.section?.schema;
69
+ if (schema === undefined) throw new Error("the oauth module declares no section");
70
+ return (schema.parse(config.oauth) as { resourceIndicator?: { enabled?: unknown } })
71
+ .resourceIndicator;
72
+ }
73
+
74
+ // The oauth-authorization module's section as resolved, unparsed: the
75
+ // runtime shape is a string from env substitution, a boolean from a literal.
56
76
  type GrantEntry = { enabled?: unknown };
57
77
  const grants = (config: AppConfig) =>
58
- (config.oauth.grants as unknown as Record<string, GrantEntry | undefined>) ?? {};
78
+ ((config["oauth-authorization"] as { grants?: unknown } | undefined)?.grants as
79
+ | Record<string, GrantEntry | undefined>
80
+ | undefined) ?? {};
81
+
82
+ /** `oauth {}` as the resolution leaves it, read as the shape the oauth module parses. */
83
+ const oauthOf = (config: AppConfig): OAuthSection => config.oauth as OAuthSection;
59
84
 
60
85
  describe("three-tier HOCON resolution (env → application.conf → reference.conf)", () => {
61
86
  it("template application.conf wins over reference.conf for grant.enabled", () => {
62
87
  const config = buildResolvedConfig("development");
63
- // Template's application.conf sets authorization_code.enabled = true.
64
- // This test asserts the resolved value — it remains true whether
65
- // reference.conf says false (current secure-default) or true (legacy).
66
- expect(grants(config).authorization_code?.enabled).toBe(true);
88
+ // Template's application.conf sets authorizationCode.enabled = true,
89
+ // so the resolved value is true whatever reference.conf says.
90
+ expect(grants(config).authorizationCode?.enabled).toBe(true);
67
91
  });
68
92
 
69
93
  it("reference.conf default reaches resolved config when template omits the key", () => {
70
94
  const config = buildResolvedConfig("development");
71
- // tokenExchange.maxActorChainDepth is library-owned in both layers
72
- // (template doesn't override it). Verifies precedence falls through.
73
- expect(config.oauth.tokenExchange?.maxActorChainDepth).toBe(3);
95
+ // nonce.maxLength is library-owned in both layers (template doesn't
96
+ // override it). Verifies precedence falls through.
97
+ expect(oauthOf(config).nonce?.maxLength).toBe(256);
74
98
  });
75
99
 
76
100
  it("env var at template layer can disable a template-enabled grant (precedence: env-override line must be repeated)", () => {
77
- // Template enables authorization_code via application.conf. The env-override
78
- // line is repeated at the template layer alongside `enabled = true`,
79
- // so OAUTH_GRANTS_AUTHORIZATION_CODE_ENABLED=false reaches the resolved value.
80
- // Without the repeated env line, the substitution at reference.conf is
81
- // shadowed by the template's literal `true`.
82
- // Note: ts.hocon substitutes env vars as strings, and `oauth.grants` is
83
- // `z.object({}).passthrough()` — an open tree each grant module's own
84
- // `configSchema` validates — so no shape-walking coercion reaches this
85
- // leaf and the string survives to the resolved config. #288 unified the
86
- // coercion for every DECLARED boolean in the schema and deliberately
87
- // left this one alone: every consumer of a grant's `enabled`
88
- // (`GrantRegistry.isEnabled`, `oauthAuthorization`, `oauthSession`)
89
- // already reads `true` and `"true"` as enabled and everything else,
90
- // `"false"` included, as not-enabled.
101
+ // The env-override line is repeated at the template layer alongside
102
+ // `enabled = true`; without it, the package reference's substitution is
103
+ // shadowed by the template's literal `true`. The value stays a string
104
+ // until boot: the module reads the switch with its own schema, `"false"`
105
+ // as off.
91
106
  const config = buildResolvedConfig("development", {
92
- OAUTH_GRANTS_AUTHORIZATION_CODE_ENABLED: "false",
107
+ OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED: "false",
93
108
  });
94
- expect(grants(config).authorization_code?.enabled).toBe("false");
109
+ expect(grants(config).authorizationCode?.enabled).toBe("false");
110
+ });
111
+
112
+ it("reads the MFA switch, mfaMode, as required where MFA_MODE is unset, and installs no MFA module under MFA_MODE=off", () => {
113
+ // The template's reference.conf ships the switch required and binds MFA_MODE.
114
+ const mfaNames = (config: Switches) =>
115
+ buildModules(config)
116
+ .map((m) => m.name)
117
+ .filter((name) => /mfa/i.test(name));
118
+ const shipped = buildResolvedConfig("development");
119
+ expect(shipped.mfaMode).toBe("required");
120
+ expect(mfaNames(shipped)).toContain("mfa");
121
+ const off = buildResolvedConfig("development", { MFA_MODE: "off" });
122
+ expect(off.mfaMode).toBe("off");
123
+ expect(mfaNames(off)).toEqual([]);
124
+ const optional = buildResolvedConfig("development", { MFA_MODE: "optional" });
125
+ expect(optional.mfaMode).toBe("optional");
126
+ expect(mfaNames(optional)).toContain("mfa");
95
127
  });
96
128
 
97
- it("reference.conf default for rateLimit.failMode is 'closed'", () => {
129
+ it("core's reference.conf ships no rateLimit.failMode: it is the Redis limiter's own key", () => {
98
130
  const config = buildResolvedConfig("development");
99
- expect(config.rateLimit?.failMode).toBe("closed");
131
+ expect(config).not.toHaveProperty("rateLimit.failMode");
100
132
  });
101
133
 
102
- it("reference.conf default for client_credentials.enabled is false", () => {
103
- // Template doesn't enable client_credentials. Reference default propagates.
134
+ it("template application.conf ships clientCredentials.enabled off", () => {
135
+ // The template writes every grant switch it reads before boot, so the
136
+ // value phase one reads is the one boot parses.
104
137
  const config = buildResolvedConfig("development");
105
- expect(grants(config).client_credentials?.enabled).toBe(false);
138
+ expect(grants(config).clientCredentials?.enabled).toBe(false);
106
139
  });
107
140
 
108
141
  it("reference.conf default for oauth.resourceIndicator.enabled is false", () => {
@@ -110,7 +143,8 @@ describe("three-tier HOCON resolution (env → application.conf → reference.co
110
143
  // schema coercion (coerceBooleanFromEnv) can produce a boolean value.
111
144
  // Without the HOCON block the field resolves to undefined.
112
145
  const config = buildResolvedConfig("development");
113
- expect(config.oauth.resourceIndicator?.enabled).toBe(false);
146
+ expect(oauthOf(config).resourceIndicator?.enabled).toBe(false);
147
+ expect(parsedResourceIndicator(config)?.enabled).toBe(false);
114
148
  });
115
149
 
116
150
  it("env var OAUTH_RESOURCE_INDICATOR_ENABLED=true reaches resolved config and coerces to boolean true", () => {
@@ -121,20 +155,17 @@ describe("three-tier HOCON resolution (env → application.conf → reference.co
121
155
  const config = buildResolvedConfig("development", {
122
156
  OAUTH_RESOURCE_INDICATOR_ENABLED: "true",
123
157
  });
124
- expect(config.oauth.resourceIndicator?.enabled).toBe(true);
158
+ expect(oauthOf(config).resourceIndicator?.enabled).toBe("true");
159
+ expect(parsedResourceIndicator(config)?.enabled).toBe(true);
125
160
  });
126
161
 
127
- // #287 — the audit trail as the shipped artifact resolves it. The section is
128
- // declared in core's schema for a load-bearing reason: `AppConfigSchema` is
129
- // a plain `z.object` and strips what it does not declare, so an undeclared
130
- // `audit` block would vanish between `parseFile` and `buildModules` and the
131
- // sink selector would read `undefined` while the config sat in the file
132
- // looking effective. These assertions run the real three-tier merge, so they
133
- // fail if either layer stops carrying the key.
134
- describe("#287: the audit sink as the shipped artifact resolves it", () => {
135
- it("resolves audit.sink.type to the template's logger sink with nothing set", () => {
162
+ // The selection is the composition root's own, `adapters.auditSink`,
163
+ // which phase one reads over the template's own layers. These assertions
164
+ // run the real merge, so they fail if a layer stops carrying the key.
165
+ describe("the audit sink as the shipped artifact resolves it", () => {
166
+ it("resolves adapters.auditSink to the template's logger sink with nothing set", () => {
136
167
  const config = buildResolvedConfig("production");
137
- expect(config.audit?.sink.type).toBe("logger");
168
+ expect(config.adapters.auditSink).toBe("logger");
138
169
  });
139
170
 
140
171
  it("wires exactly one auditSink provider for that resolved config", () => {
@@ -144,41 +175,35 @@ describe("three-tier HOCON resolution (env → application.conf → reference.co
144
175
  });
145
176
 
146
177
  it("lets an operator select core's built-in console sink by env var", () => {
147
- // The env-override line has to be repeated at the template layer or
148
- // the literal `"logger"` shadows reference.conf's substitution — the
149
- // same precedence rule the grant tests above pin.
150
- const config = buildResolvedConfig("production", { AUDIT_SINK_TYPE: "console" });
151
- expect(config.audit?.sink.type).toBe("console");
178
+ const config = buildResolvedConfig("production", { ADAPTERS_AUDIT_SINK: "console" });
179
+ expect(config.adapters.auditSink).toBe("console");
152
180
  });
153
181
 
154
- it("reference.conf's own default is a sink, not a drop", () => {
155
- // A consumer that resolves against reference.conf alone still lands on
156
- // a sink. #304's sink policy is stdout JSON, never "none", and the
157
- // library layer has to hold that on its own — a composition root that
158
- // forgets to override it must not thereby lose its audit trail.
159
- const referenceOnly = validate(
160
- parseFile(resolveLibraryReferenceConfPath(), { env: testEnv }),
161
- AppConfigSchema,
182
+ it("the template's reference.conf alone selects a sink, not a drop", () => {
183
+ // A composition resolved against the template's reference alone
184
+ // still lands on a sink: an application.conf that forgets the
185
+ // selection must not thereby lose its audit trail.
186
+ const referenceOnly = parseFile(fileURLToPath(templateReference()), { env: {} });
187
+ expect(readAdapters(referenceOnly.toObject() as Record<string, unknown>, {}).auditSink).toBe(
188
+ "logger",
162
189
  );
163
- expect(referenceOnly.audit?.sink.type).toBe("console");
164
190
  });
165
191
  });
166
192
 
167
- // #277 — this is the shape the umbrella E2E (o3co/auth) boots: the shipped
168
- // application.conf, `DEPLOYMENT_MODE=multi`, and one shared ioredis socket.
169
- // The assertions below are what keep that stack booting: a memory denylist
170
- // under `multi` is refused by the replica-safety guard, and NO denylist is
171
- // refused by the #277 guard, so the template has to land on "redis" without
172
- // the deployment naming it.
173
- describe("#277: access-token revocation as the shipped artifact resolves it", () => {
174
- it("resolves accessTokenDenylist.adapter to redis with nothing set", () => {
193
+ // The shape the umbrella E2E (o3co/auth) boots: the shipped
194
+ // application.conf, `CORE_DEPLOYMENT_MODE=multi`, and one shared ioredis socket.
195
+ // A memory denylist under `multi` is refused by the replica-safety guard,
196
+ // and NO denylist by core's denylist boot guard, so the template has to
197
+ // land on "redis" without the deployment naming it.
198
+ describe("access-token revocation as the shipped artifact resolves it", () => {
199
+ it("resolves adapters.accessTokenDenylist to redis with nothing set", () => {
175
200
  const config = buildResolvedConfig("production");
176
- expect(config.accessTokenDenylist?.adapter).toBe("redis");
201
+ expect(config.adapters.accessTokenDenylist).toBe("redis");
177
202
  });
178
203
 
179
204
  it("resolves oauth.revocation.accessToken to denylist with nothing set", () => {
180
205
  const config = buildResolvedConfig("production");
181
- expect(config.oauth.revocation?.accessToken).toBe("denylist");
206
+ expect(oauthOf(config).revocation?.accessToken).toBe("denylist");
182
207
  });
183
208
 
184
209
  it("picks the replica-safe denylist module for that resolved config", () => {
@@ -189,35 +214,39 @@ describe("three-tier HOCON resolution (env → application.conf → reference.co
189
214
 
190
215
  it("leaves nothing replica-unsafe under the umbrella E2E's environment", async () => {
191
216
  // The environment `o3co/auth`'s tests/docker-compose.yml sets. Under
192
- // `DEPLOYMENT_MODE=multi` the replica-safety guard fails boot naming
193
- // every in-memory shared store, so the denylist this change adds has to
194
- // come out of that environment as the Redis one — from the template's
195
- // own config, since the compose file names no denylist variable.
196
- // Asked of each manifest, the way the guard does (#455), so a rename
197
- // cannot quietly invalidate it and the template's own memory modules
198
- // — which no name list in core covers — count too.
217
+ // `CORE_DEPLOYMENT_MODE=multi` the replica-safety guard fails boot naming
218
+ // every in-memory shared store, so the denylist has to come out as
219
+ // the Redis one from the template's own config: the compose file
220
+ // names no denylist variable. Asked of each manifest, as the guard
221
+ // does, so a rename cannot quietly invalidate it and the template's
222
+ // own memory modules count too.
199
223
  const { replicaUnsafeReason } = await import("@o3co/auth-provider-core");
200
224
  const config = buildResolvedConfig("production", {
201
- USER_SESSION_STORES_ADAPTER: "redis",
202
- RATE_LIMITER_ADAPTER: "redis",
203
- OAUTH_CODE_ADAPTER: "redis",
204
- // #456: the federation token store defaults to memory, and the
205
- // memory module declares itself replica-unsafe (#455).
206
- FEDERATION_TOKEN_STORE_TYPE: "redis",
225
+ ADAPTERS_USER_SESSION_STORES: "redis",
226
+ ADAPTERS_RATE_LIMITER: "redis",
227
+ ADAPTERS_CODE_REPOSITORY: "redis",
228
+ // The federation token store defaults to memory, and the memory
229
+ // module declares itself replica-unsafe.
230
+ ADAPTERS_FEDERATION_TOKEN_STORE: "redis",
207
231
  REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_KEY: "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=",
232
+ // The umbrella sets MFA off.
233
+ MFA_MODE: "off",
208
234
  });
209
235
  const modules = buildModules(config);
210
236
  expect(modules.map((m) => m.name)).toContain("redis-access-token-denylist");
237
+ // Each module's section at its name: a declaration made from the
238
+ // section is answered for it.
239
+ const sections = config as unknown as Record<string, unknown>;
211
240
  for (const module of modules) {
212
- expect(replicaUnsafeReason(module), module.name).toBeUndefined();
241
+ expect(replicaUnsafeReason(module, sections[module.name]), module.name).toBeUndefined();
213
242
  }
214
243
  });
215
244
 
216
245
  it("lets a single-instance deployment opt down to memory by env var", () => {
217
246
  const config = buildResolvedConfig("production", {
218
- ACCESS_TOKEN_DENYLIST_ADAPTER: "memory",
247
+ ADAPTERS_ACCESS_TOKEN_DENYLIST: "memory",
219
248
  });
220
- expect(config.accessTokenDenylist?.adapter).toBe("memory");
249
+ expect(config.adapters.accessTokenDenylist).toBe("memory");
221
250
  const names = buildModules(config).map((m) => m.name);
222
251
  expect(names).toContain("core-access-token-denylist-memory");
223
252
  });
@@ -226,7 +255,7 @@ describe("three-tier HOCON resolution (env → application.conf → reference.co
226
255
  const config = buildResolvedConfig("production", {
227
256
  OAUTH_REVOCATION_ACCESS_TOKEN: "unsupported",
228
257
  });
229
- expect(config.oauth.revocation?.accessToken).toBe("unsupported");
258
+ expect(oauthOf(config).revocation?.accessToken).toBe("unsupported");
230
259
  });
231
260
  });
232
261
  });