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

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 (39) hide show
  1. package/README.ja.md +3 -1
  2. package/README.md +5 -2
  3. package/package.json +1 -1
  4. package/templates/standalone/.env.example +7 -2
  5. package/templates/standalone/README.ja.md +14 -10
  6. package/templates/standalone/README.md +26 -20
  7. package/templates/standalone/config/application.conf +1 -0
  8. package/templates/standalone/config/clients.yaml.example +8 -0
  9. package/templates/standalone/config/development.conf +2 -2
  10. package/templates/standalone/config/mailpit.conf +10 -5
  11. package/templates/standalone/config/reference.conf +9 -3
  12. package/templates/standalone/docker-compose.mailpit.yml +3 -0
  13. package/templates/standalone/package.json +1 -1
  14. package/templates/standalone/src/__tests__/adapters.test.mts +12 -6
  15. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +13 -5
  16. package/templates/standalone/src/__tests__/config-notices.test.mts +4 -1
  17. package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +48 -0
  18. package/templates/standalone/src/__tests__/core-federations.test.mts +9 -6
  19. package/templates/standalone/src/__tests__/core-sections.test.mts +7 -5
  20. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +89 -86
  21. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +4 -0
  22. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +81 -14
  23. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +8 -4
  24. package/templates/standalone/src/__tests__/mfa-switch.test.mts +79 -21
  25. package/templates/standalone/src/__tests__/own-modules.test.mts +38 -12
  26. package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +214 -0
  27. package/templates/standalone/src/__tests__/replica-safety.test.mts +4 -0
  28. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +10 -3
  29. package/templates/standalone/src/__tests__/sections.test.mts +24 -0
  30. package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +4 -0
  31. package/templates/standalone/src/__tests__/startup-log-lines.test.mts +148 -0
  32. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +8 -4
  33. package/templates/standalone/src/__tests__/two-phase-config.test.mts +2 -1
  34. package/templates/standalone/src/adapters.mts +2 -2
  35. package/templates/standalone/src/mfaSwitch.mts +6 -7
  36. package/templates/standalone/src/modules.mts +12 -6
  37. package/templates/standalone/src/rootRenames.mts +12 -13
  38. package/templates/standalone/src/sections.mts +12 -2
  39. package/templates/versions.json +18 -18
@@ -22,9 +22,8 @@
22
22
  * bundles are listed whatever the map says (`federation-types.test.mts`
23
23
  * boots them). A variable the
24
24
  * template bound before, `FEDERATIONS_<NAME>_<KEY>`, set alone or beside its
25
- * new name at a different value is refused before any module is chosen,
26
- * naming the new variable and path and never a value; the two at one value
27
- * are accepted. The map written at the top level refuses boot, naming its
25
+ * new name, at any value, is refused before any module is chosen, naming the
26
+ * new variable and path and never a value. The map written at the top level refuses boot, naming its
28
27
  * paths under `core.federations`.
29
28
  */
30
29
 
@@ -206,9 +205,13 @@ describe("a variable the template bound before, FEDERATIONS_<NAME>_<KEY>", () =>
206
205
  );
207
206
 
208
207
  it.each(RENAMED)(
209
- "core.federations.%s.%s: %s beside its new name at the same value is read",
210
- (name, key, old, variable, value) => {
211
- expect(String(federationsOf({ [old]: value, [variable]: value })[name]?.[key])).toBe(value);
208
+ "core.federations.%s.%s: %s beside its new name at the same value is refused all the same",
209
+ (_name, _key, old, variable, value) => {
210
+ const err = refusal({ [old]: value, [variable]: value });
211
+ expect(err).toMatchObject({
212
+ reason: "environment-variable-renamed",
213
+ details: { renamed: [{ from: old, state: "different" }] },
214
+ });
212
215
  },
213
216
  );
214
217
  });
@@ -20,8 +20,8 @@
20
20
  * environment, phase one's switches, then the layers over every loaded
21
21
  * package's `reference.conf` handed to boot. A path they moved from, written
22
22
  * in the operator's own layer, refuses boot naming the new one;
23
- * `DEPLOYMENT_MODE`, renamed `CORE_DEPLOYMENT_MODE`, refuses boot unless the
24
- * new name carries the same value.
23
+ * `DEPLOYMENT_MODE`, renamed `CORE_DEPLOYMENT_MODE`, refuses boot while it is
24
+ * set, whatever the new name holds.
25
25
  */
26
26
 
27
27
  import { BootError } from "@o3co/auth-provider-core";
@@ -176,10 +176,12 @@ describe("DEPLOYMENT_MODE, renamed CORE_DEPLOYMENT_MODE, through the template's
176
176
  expect(err.message).not.toContain("new-mode-c81a");
177
177
  });
178
178
 
179
- it("set beside the new name at the same value: boots with that mode", async () => {
180
- const composition = await boot({ DEPLOYMENT_MODE: "single", CORE_DEPLOYMENT_MODE: "single" });
179
+ it("set beside the new name at the same value: refused all the same", async () => {
180
+ const err = await refused({ DEPLOYMENT_MODE: "single", CORE_DEPLOYMENT_MODE: "single" });
181
181
 
182
- expect(composition.handle.components.deploymentMode).toBe("single");
182
+ expect(err.details).toMatchObject({
183
+ renamed: [{ from: "DEPLOYMENT_MODE", to: "CORE_DEPLOYMENT_MODE", state: "different" }],
184
+ });
183
185
  });
184
186
 
185
187
  it("unset, with the new name set: boots with its mode", async () => {
@@ -44,7 +44,12 @@ import {
44
44
  resolveLayers,
45
45
  type Switches,
46
46
  } from "../configPath.mjs";
47
- import { httpModule, keyStoreModule, templateReference } from "../modules.mjs";
47
+ import {
48
+ httpModule,
49
+ keyStoreModule,
50
+ standaloneRedisClientsModule,
51
+ templateReference,
52
+ } from "../modules.mjs";
48
53
 
49
54
  /**
50
55
  * Boots the shipped config with EVERY documented override supplied the way an
@@ -195,6 +200,7 @@ const DOCUMENTED_ENV: Readonly<Record<string, string>> = {
195
200
  REDIS_REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT: "3",
196
201
  REDIS_CLIENTS_URL: "redis://redis:6379",
197
202
  REDIS_CLIENTS_PASSWORD: "rt-family-password",
203
+ REDIS_CLIENTS_ASSUME_NO_EVICTION: "true",
198
204
  // The federation token store's Redis branch.
199
205
  REDIS_FEDERATION_TOKEN_STORE_KEY_PREFIX: "ft:",
200
206
  REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_MODE: "required",
@@ -311,139 +317,139 @@ const DELIBERATELY_UNSET: Readonly<Record<string, string>> = {
311
317
  OAUTH_AUTHORIZE_ALLOW_UNMARKED_CLIENTS:
312
318
  "the variable of a removed key, and only captured — any value fails boot",
313
319
  OAUTH_ACCESS_TOKEN_EXPIRES_IN:
314
- "renamed OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN, and only captured — set alone, or to another value, it fails boot",
320
+ "renamed OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN, and only captured — set at any value, beside its new name or not, it fails boot",
315
321
  DEPLOYMENT_MODE:
316
- "renamed CORE_DEPLOYMENT_MODE, and only captured — set alone, or to another value, it fails boot",
322
+ "renamed CORE_DEPLOYMENT_MODE, and only captured — set at any value, beside its new name or not, it fails boot",
317
323
  MEMORY_RATE_LIMITER_MAX_BUCKETS:
318
- "renamed CORE_RATE_LIMITER_MEMORY_MAX_BUCKETS, and only captured — set alone, or to another value, it fails boot",
324
+ "renamed CORE_RATE_LIMITER_MEMORY_MAX_BUCKETS, and only captured — set at any value, beside its new name or not, it fails boot",
319
325
  RATE_LIMIT_FAIL_MODE:
320
- "renamed REDIS_RATE_LIMITER_FAIL_MODE, and only captured — set alone, or to another value, it fails boot",
326
+ "renamed REDIS_RATE_LIMITER_FAIL_MODE, and only captured — set at any value, beside its new name or not, it fails boot",
321
327
  REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX:
322
- "renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX, and only captured — set alone, or to another value, it fails boot",
328
+ "renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX, and only captured — set at any value, beside its new name or not, it fails boot",
323
329
  REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT:
324
- "renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT, and only captured — set alone, or to another value, it fails boot",
330
+ "renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT, and only captured — set at any value, beside its new name or not, it fails boot",
325
331
  FEDERATION_GRANTS_ENCRYPTION_MODE:
326
- "renamed REDIS_FEDERATION_GRANT_STORE_ENCRYPTION_MODE, and only captured — set alone, or to another value, it fails boot",
332
+ "renamed REDIS_FEDERATION_GRANT_STORE_ENCRYPTION_MODE, and only captured — set at any value, beside its new name or not, it fails boot",
327
333
  OAUTH_TOKEN_BINDING_DISPATCH_POLICY:
328
- "renamed CORE_TOKEN_BINDING_DISPATCH_POLICY, and only captured — set alone, or to another value, it fails boot",
334
+ "renamed CORE_TOKEN_BINDING_DISPATCH_POLICY, and only captured — set at any value, beside its new name or not, it fails boot",
329
335
  OAUTH_TOKEN_BINDING_BIND_CONFIDENTIAL_CLIENT_REFRESH_TOKENS:
330
- "renamed CORE_TOKEN_BINDING_BIND_CONFIDENTIAL_CLIENT_REFRESH_TOKENS, and only captured — set alone, or to another value, it fails boot",
336
+ "renamed CORE_TOKEN_BINDING_BIND_CONFIDENTIAL_CLIENT_REFRESH_TOKENS, and only captured — set at any value, beside its new name or not, it fails boot",
331
337
  OAUTH_GRANTS_SESSION_ENABLED:
332
- "renamed OAUTH_SESSION_ENABLED, and only captured — set alone, or to another value, it fails boot",
338
+ "renamed OAUTH_SESSION_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
333
339
  OAUTH_GRANTS_AUTHORIZATION_CODE_ENABLED:
334
- "renamed OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED, and only captured — set alone, or to another value, it fails boot",
340
+ "renamed OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
335
341
  OAUTH_GRANTS_REFRESH_TOKEN_ENABLED:
336
- "renamed OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_ENABLED, and only captured — set alone, or to another value, it fails boot",
342
+ "renamed OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
337
343
  OAUTH_REFRESH_TOKEN_UNKNOWN_FAMILY_POLICY:
338
- "renamed OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_UNKNOWN_FAMILY_POLICY, and only captured — set alone, or to another value, it fails boot",
344
+ "renamed OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_UNKNOWN_FAMILY_POLICY, and only captured — set at any value, beside its new name or not, it fails boot",
339
345
  OAUTH_GRANTS_CLIENT_CREDENTIALS_ENABLED:
340
- "renamed OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED, and only captured — set alone, or to another value, it fails boot",
346
+ "renamed OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
341
347
  OAUTH_GRANTS_JWT_BEARER_ENABLED:
342
- "renamed OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED, and only captured — set alone, or to another value, it fails boot",
348
+ "renamed OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
343
349
  ENDPOINTS_CONSENT_URL:
344
- "renamed OAUTH_CONSENT_PAGE_URL, and only captured — set alone, or to another value, it fails boot",
350
+ "renamed OAUTH_CONSENT_PAGE_URL, and only captured — set at any value, beside its new name or not, it fails boot",
345
351
  OAUTH_CIMD_ENABLED:
346
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ENABLED, and only captured — set alone, or to another value, it fails boot",
352
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
347
353
  OAUTH_CIMD_ALLOWED_SCOPES:
348
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_SCOPES, and only captured — set alone, or to another value, it fails boot",
354
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_SCOPES, and only captured — set at any value, beside its new name or not, it fails boot",
349
355
  OAUTH_CIMD_ALLOWED_AUDIENCES:
350
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_AUDIENCES, and only captured — set alone, or to another value, it fails boot",
356
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_AUDIENCES, and only captured — set at any value, beside its new name or not, it fails boot",
351
357
  OAUTH_CIMD_ALLOWED_HOSTS:
352
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_HOSTS, and only captured — set alone, or to another value, it fails boot",
358
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_HOSTS, and only captured — set at any value, beside its new name or not, it fails boot",
353
359
  OAUTH_CIMD_DENIED_HOSTS:
354
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_DENIED_HOSTS, and only captured — set alone, or to another value, it fails boot",
360
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_DENIED_HOSTS, and only captured — set at any value, beside its new name or not, it fails boot",
355
361
  OAUTH_CIMD_MAX_BYTES:
356
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_BYTES, and only captured — set alone, or to another value, it fails boot",
362
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_BYTES, and only captured — set at any value, beside its new name or not, it fails boot",
357
363
  OAUTH_CIMD_TIMEOUT_MS:
358
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_TIMEOUT_MS, and only captured — set alone, or to another value, it fails boot",
364
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_TIMEOUT_MS, and only captured — set at any value, beside its new name or not, it fails boot",
359
365
  OAUTH_CIMD_CACHE_MAX_AGE_MS:
360
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_CACHE_MAX_AGE_MS, and only captured — set alone, or to another value, it fails boot",
366
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_CACHE_MAX_AGE_MS, and only captured — set at any value, beside its new name or not, it fails boot",
361
367
  OAUTH_CIMD_MAX_CACHE_ENTRIES:
362
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CACHE_ENTRIES, and only captured — set alone, or to another value, it fails boot",
368
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CACHE_ENTRIES, and only captured — set at any value, beside its new name or not, it fails boot",
363
369
  OAUTH_CIMD_STALE_IF_ERROR_MS:
364
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_STALE_IF_ERROR_MS, and only captured — set alone, or to another value, it fails boot",
370
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_STALE_IF_ERROR_MS, and only captured — set at any value, beside its new name or not, it fails boot",
365
371
  OAUTH_CIMD_NEGATIVE_CACHE_MS:
366
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_NEGATIVE_CACHE_MS, and only captured — set alone, or to another value, it fails boot",
372
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_NEGATIVE_CACHE_MS, and only captured — set at any value, beside its new name or not, it fails boot",
367
373
  OAUTH_CIMD_MAX_CONCURRENT_FETCHES:
368
- "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CONCURRENT_FETCHES, and only captured — set alone, or to another value, it fails boot",
374
+ "renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CONCURRENT_FETCHES, and only captured — set at any value, beside its new name or not, it fails boot",
369
375
  OAUTH_GRANTS_AUTHORIZATION_CODE_PKCE_REQUIRE_S256:
370
376
  "the authorization-code grant's pkce block was removed, and this is only captured — set at all, it fails boot",
371
377
  ENDPOINTS_LOGIN_URL:
372
- "renamed SESSION_LOGIN_PAGE_URL, and only captured — set alone, or to another value, it fails boot",
378
+ "renamed SESSION_LOGIN_PAGE_URL, and only captured — set at any value, beside its new name or not, it fails boot",
373
379
  SESSION_SECRET:
374
- "renamed SESSION_STORE_SECRET, and only captured — set alone, or to another value, it fails boot",
380
+ "renamed SESSION_STORE_SECRET, and only captured — set at any value, beside its new name or not, it fails boot",
375
381
  SESSION_NAME:
376
- "renamed SESSION_STORE_NAME, and only captured — set alone, or to another value, it fails boot",
382
+ "renamed SESSION_STORE_NAME, and only captured — set at any value, beside its new name or not, it fails boot",
377
383
  SESSION_MAX_AGE:
378
- "renamed SESSION_STORE_MAX_AGE, and only captured — set alone, or to another value, it fails boot",
384
+ "renamed SESSION_STORE_MAX_AGE, and only captured — set at any value, beside its new name or not, it fails boot",
379
385
  SESSION_SECURE:
380
- "renamed SESSION_STORE_SECURE, and only captured — set alone, or to another value, it fails boot",
386
+ "renamed SESSION_STORE_SECURE, and only captured — set at any value, beside its new name or not, it fails boot",
381
387
  SESSION_SAME_SITE:
382
- "renamed SESSION_STORE_SAME_SITE, and only captured — set alone, or to another value, it fails boot",
388
+ "renamed SESSION_STORE_SAME_SITE, and only captured — set at any value, beside its new name or not, it fails boot",
383
389
  SESSION_DOMAIN:
384
- "renamed SESSION_STORE_DOMAIN, and only captured — set alone, or to another value, it fails boot",
390
+ "renamed SESSION_STORE_DOMAIN, and only captured — set at any value, beside its new name or not, it fails boot",
385
391
  SESSION_STORAGE_TYPE:
386
- "renamed SESSION_STORE_STORAGE_TYPE, and only captured — set alone, or to another value, it fails boot",
392
+ "renamed SESSION_STORE_STORAGE_TYPE, and only captured — set at any value, beside its new name or not, it fails boot",
387
393
  SESSION_STORAGE_REDIS_URL:
388
- "renamed SESSION_STORE_STORAGE_REDIS_URL, and only captured — set alone, or to another value, it fails boot",
394
+ "renamed SESSION_STORE_STORAGE_REDIS_URL, and only captured — set at any value, beside its new name or not, it fails boot",
389
395
  SESSION_STORAGE_REDIS_PASSWORD:
390
- "renamed SESSION_STORE_STORAGE_REDIS_PASSWORD, and only captured — set alone, or to another value, it fails boot",
396
+ "renamed SESSION_STORE_STORAGE_REDIS_PASSWORD, and only captured — set at any value, beside its new name or not, it fails boot",
391
397
  LOG_LEVEL:
392
- "renamed LOGGING_LEVEL, and only captured — set alone, or to another value, it fails boot",
398
+ "renamed LOGGING_LEVEL, and only captured — set at any value, beside its new name or not, it fails boot",
393
399
  CORS_ALLOWED_ORIGINS:
394
- "renamed HTTP_CORS_ALLOWED_ORIGINS, and only captured — set alone, or to another value, it fails boot",
400
+ "renamed HTTP_CORS_ALLOWED_ORIGINS, and only captured — set at any value, beside its new name or not, it fails boot",
395
401
  OAUTH_JWT_SIGNING_KEY_PROVIDER:
396
- "renamed KEY_STORE_PROVIDER, and only captured — set alone, or to another value, it fails boot",
402
+ "renamed KEY_STORE_PROVIDER, and only captured — set at any value, beside its new name or not, it fails boot",
397
403
  OAUTH_JWT_ALGORITHM:
398
- "renamed KEY_STORE_LOCAL_ALGORITHM, and only captured — set alone, or to another value, it fails boot",
404
+ "renamed KEY_STORE_LOCAL_ALGORITHM, and only captured — set at any value, beside its new name or not, it fails boot",
399
405
  OAUTH_JWT_KID:
400
- "renamed KEY_STORE_LOCAL_KID, and only captured — set alone, or to another value, it fails boot",
406
+ "renamed KEY_STORE_LOCAL_KID, and only captured — set at any value, beside its new name or not, it fails boot",
401
407
  OAUTH_JWT_SECRET:
402
- "renamed KEY_STORE_LOCAL_SECRET, and only captured — set alone, or to another value, it fails boot",
408
+ "renamed KEY_STORE_LOCAL_SECRET, and only captured — set at any value, beside its new name or not, it fails boot",
403
409
  OAUTH_JWT_PRIVATE_KEY_PATH:
404
- "renamed KEY_STORE_LOCAL_PRIVATE_KEY_PATH, and only captured — set alone, or to another value, it fails boot",
410
+ "renamed KEY_STORE_LOCAL_PRIVATE_KEY_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
405
411
  OAUTH_JWT_PUBLIC_KEY_PATH:
406
- "renamed KEY_STORE_LOCAL_PUBLIC_KEY_PATH, and only captured — set alone, or to another value, it fails boot",
412
+ "renamed KEY_STORE_LOCAL_PUBLIC_KEY_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
407
413
  OAUTH_JWT_PRIVATE_KEY:
408
- "renamed KEY_STORE_LOCAL_PRIVATE_KEY, and only captured — set alone, or to another value, it fails boot",
414
+ "renamed KEY_STORE_LOCAL_PRIVATE_KEY, and only captured — set at any value, beside its new name or not, it fails boot",
409
415
  OAUTH_JWT_PUBLIC_KEY:
410
- "renamed KEY_STORE_LOCAL_PUBLIC_KEY, and only captured — set alone, or to another value, it fails boot",
416
+ "renamed KEY_STORE_LOCAL_PUBLIC_KEY, and only captured — set at any value, beside its new name or not, it fails boot",
411
417
  REFRESH_TOKEN_FAMILY_STORE_REDIS_URL:
412
- "renamed REDIS_CLIENTS_URL, and only captured — set alone, or to another value, it fails boot",
418
+ "renamed REDIS_CLIENTS_URL, and only captured — set at any value, beside its new name or not, it fails boot",
413
419
  REFRESH_TOKEN_FAMILY_STORE_REDIS_PASSWORD:
414
- "renamed REDIS_CLIENTS_PASSWORD, and only captured — set alone, or to another value, it fails boot",
420
+ "renamed REDIS_CLIENTS_PASSWORD, and only captured — set at any value, beside its new name or not, it fails boot",
415
421
  CLIENT_PATH:
416
- "renamed REPOSITORIES_CLIENT_YAML_PATH, and only captured — set alone, or to another value, it fails boot",
422
+ "renamed REPOSITORIES_CLIENT_YAML_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
417
423
  CLIENT_USER_PATH:
418
- "renamed REPOSITORIES_USER_YAML_PATH, and only captured — set alone, or to another value, it fails boot",
424
+ "renamed REPOSITORIES_USER_YAML_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
419
425
  CLIENT_USER_AUTHENTICATE_URL:
420
- "renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_URL, and only captured — set alone, or to another value, it fails boot",
426
+ "renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_URL, and only captured — set at any value, beside its new name or not, it fails boot",
421
427
  CLIENT_USER_AUTHENTICATE_BY_TOKEN_URL:
422
- "renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_BY_TOKEN_URL, and only captured — set alone, or to another value, it fails boot",
428
+ "renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_BY_TOKEN_URL, and only captured — set at any value, beside its new name or not, it fails boot",
423
429
  CLIENT_USER_LINK_FEDERATED_IDENTITY_URL:
424
- "renamed REPOSITORIES_USER_HTTP_LINK_FEDERATED_IDENTITY_URL, and only captured — set alone, or to another value, it fails boot",
430
+ "renamed REPOSITORIES_USER_HTTP_LINK_FEDERATED_IDENTITY_URL, and only captured — set at any value, beside its new name or not, it fails boot",
425
431
  CLIENT_USER_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL:
426
- "renamed REPOSITORIES_USER_HTTP_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL, and only captured — set alone, or to another value, it fails boot",
432
+ "renamed REPOSITORIES_USER_HTTP_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL, and only captured — set at any value, beside its new name or not, it fails boot",
427
433
  CLIENT_USER_BEARER_TOKEN:
428
- "renamed REPOSITORIES_USER_HTTP_BEARER_TOKEN, and only captured — set alone, or to another value, it fails boot",
434
+ "renamed REPOSITORIES_USER_HTTP_BEARER_TOKEN, and only captured — set at any value, beside its new name or not, it fails boot",
429
435
  CLIENT_USER_TIMEOUT:
430
- "renamed REPOSITORIES_USER_HTTP_TIMEOUT, and only captured — set alone, or to another value, it fails boot",
436
+ "renamed REPOSITORIES_USER_HTTP_TIMEOUT, and only captured — set at any value, beside its new name or not, it fails boot",
431
437
  CLIENT_USER_MAX_RESPONSE_BYTES:
432
- "renamed REPOSITORIES_USER_HTTP_MAX_RESPONSE_BYTES, and only captured — set alone, or to another value, it fails boot",
438
+ "renamed REPOSITORIES_USER_HTTP_MAX_RESPONSE_BYTES, and only captured — set at any value, beside its new name or not, it fails boot",
433
439
  CLIENT_CODE_DEFAULT_EXPIRES_IN:
434
- "renamed REDIS_CODE_REPOSITORY_DEFAULT_EXPIRES_IN (the Redis code repository) and STANDALONE_IN_MEMORY_CODE_REPOSITORY_DEFAULT_EXPIRES_IN (the in-process one), and only captured — set alone, or to another value, it fails boot",
440
+ "renamed REDIS_CODE_REPOSITORY_DEFAULT_EXPIRES_IN (the Redis code repository) and STANDALONE_IN_MEMORY_CODE_REPOSITORY_DEFAULT_EXPIRES_IN (the in-process one), and only captured — set at any value, beside its new name or not, it fails boot",
435
441
  CLIENT_CODE_KEY_PREFIX:
436
- "renamed REDIS_CODE_REPOSITORY_KEY_PREFIX, and only captured — set alone, or to another value, it fails boot",
442
+ "renamed REDIS_CODE_REPOSITORY_KEY_PREFIX, and only captured — set at any value, beside its new name or not, it fails boot",
437
443
  CLIENT_CODE_ENDPOINT_URI:
438
444
  "removed: the Redis code repository uses the shared redis-clients connection; only captured — set at all, it fails boot",
439
445
  CLIENT_CODE_PASSWORD:
440
446
  "removed: the Redis code repository uses the shared redis-clients connection; only captured — set at all, it fails boot",
441
447
  ENDPOINTS_MFA_URL:
442
- "renamed MFA_PAGE_URL, and only captured — set alone, or to another value, it fails boot where MFA is installed",
448
+ "renamed MFA_PAGE_URL, and only captured — set at any value, beside its new name or not, it fails boot where MFA is installed",
443
449
  MFA_TOTP_ENABLED:
444
- "renamed MFA_TOTP_FACTOR_ENABLED, and only captured — set alone, or to another value, it fails boot where MFA is installed",
450
+ "renamed MFA_TOTP_FACTOR_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot where MFA is installed",
445
451
  MFA_TOTP_ISSUER:
446
- "renamed MFA_TOTP_FACTOR_ISSUER, and only captured — set alone, or to another value, it fails boot where MFA is installed",
452
+ "renamed MFA_TOTP_FACTOR_ISSUER, and only captured — set at any value, beside its new name or not, it fails boot where MFA is installed",
447
453
  };
448
454
 
449
455
  /**
@@ -480,11 +486,11 @@ function commentedFederationBindings(): string {
480
486
  }
481
487
 
482
488
  /**
483
- * The provider environment `o3co/auth`'s `tests/docker-compose.yml` sets,
484
- * transcribed: each renamed variable under its old and its new name, the two
485
- * at one value. The umbrella E2E boots the shipped template with exactly this,
486
- * so a parse failure here is a red umbrella build that this repository can see
487
- * first. `SESSION_STORE_SECURE=false` is the one it cannot run without: the
489
+ * The provider environment `o3co/auth`'s `tests/docker-compose.yml` is to
490
+ * set, transcribed: each renamed variable under its new name alone, since an
491
+ * old name set refuses boot beside its new one too. The umbrella E2E boots
492
+ * the shipped template with this, so a parse failure here is a red umbrella
493
+ * build that this repository can see first. `SESSION_STORE_SECURE=false` is the one it cannot run without: the
488
494
  * suite speaks plain HTTP.
489
495
  *
490
496
  * The two `FEDERATION_TOKEN_STORE` lines are required: the federation token
@@ -493,37 +499,23 @@ function commentedFederationBindings(): string {
493
499
  * Redis store is selected with its encryption key.
494
500
  */
495
501
  const UMBRELLA_E2E_ENV: Readonly<Record<string, string>> = {
496
- OAUTH_JWT_ALGORITHM: "HS256",
497
502
  KEY_STORE_LOCAL_ALGORITHM: "HS256",
498
- OAUTH_JWT_SECRET: "e2e-shared-hs256-secret.at-least-32-bytes.ok",
499
503
  KEY_STORE_LOCAL_SECRET: "e2e-shared-hs256-secret.at-least-32-bytes.ok",
500
504
  OAUTH_JWT_ISSUER: "https://auth.e2e.test",
501
- SESSION_SECRET: "lO0QH09fuKSGuViZ9myJbH3jsgai99A2GpC3RYRuy6Y=",
502
505
  SESSION_STORE_SECRET: "lO0QH09fuKSGuViZ9myJbH3jsgai99A2GpC3RYRuy6Y=",
503
- SESSION_SECURE: "false",
504
506
  SESSION_STORE_SECURE: "false",
505
- SESSION_NAME: "auth.session",
506
507
  SESSION_STORE_NAME: "auth.session",
507
- DEPLOYMENT_MODE: "multi",
508
508
  CORE_DEPLOYMENT_MODE: "multi",
509
- REFRESH_TOKEN_FAMILY_STORE_REDIS_URL: "redis://redis:6379",
510
509
  REDIS_CLIENTS_URL: "redis://redis:6379",
511
- SESSION_STORAGE_REDIS_URL: "redis://redis:6379",
512
510
  SESSION_STORE_STORAGE_REDIS_URL: "redis://redis:6379",
513
- USER_SESSION_STORES_ADAPTER: "redis",
514
511
  ADAPTERS_USER_SESSION_STORES: "redis",
515
- RATE_LIMITER_ADAPTER: "redis",
516
512
  ADAPTERS_RATE_LIMITER: "redis",
517
- OAUTH_CODE_ADAPTER: "redis",
518
513
  ADAPTERS_CODE_REPOSITORY: "redis",
519
- FEDERATION_TOKEN_STORE_TYPE: "redis",
520
514
  ADAPTERS_FEDERATION_TOKEN_STORE: "redis",
521
515
  REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_KEY: "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=",
522
- CLIENT_USER_TYPE: "yaml",
523
516
  ADAPTERS_USER_REPOSITORY: "yaml",
524
517
  OAUTH_RESOURCE_INDICATOR_ENABLED: "true",
525
518
  OAUTH_REQUIRE_EMAIL_VERIFIED: "true",
526
- OAUTH_GRANTS_SESSION_ENABLED: "true",
527
519
  OAUTH_SESSION_ENABLED: "true",
528
520
  MFA_MODE: "off",
529
521
  };
@@ -782,6 +774,11 @@ describe("the shipped config boots with every documented override supplied as a
782
774
  // A leftover string here would be read as "on" by a truthiness check
783
775
  // and as "off" by `=== true`, for a feature whose whole default is off.
784
776
  expect(federationGrantsSection(config).enabled).toBe(true);
777
+ expect(
778
+ standaloneRedisClientsModule.section?.schema.parse(
779
+ (config as Record<string, unknown>)["redis-clients"],
780
+ ),
781
+ ).toMatchObject({ assumeNoEviction: true });
785
782
  });
786
783
 
787
784
  it("turns every non-boolean override into its declared type", async () => {
@@ -1245,12 +1242,18 @@ describe("the shipped config boots with every documented override supplied as a
1245
1242
  ).rejects.toMatchObject({ reason: "environment-variable-renamed" });
1246
1243
  });
1247
1244
 
1248
- it("boots with OAUTH_ACCESS_TOKEN_EXPIRES_IN beside its new name set to the same value, reading the new name", async () => {
1249
- const config = await bootParsed({
1245
+ it("refuses OAUTH_ACCESS_TOKEN_EXPIRES_IN beside its new name set to the same value", async () => {
1246
+ const booting = bootParsed({
1250
1247
  ...DOCUMENTED_ENV,
1251
1248
  OAUTH_ACCESS_TOKEN_EXPIRES_IN:
1252
1249
  DOCUMENTED_ENV.OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN as string,
1253
1250
  });
1251
+ await expect(booting).rejects.toMatchObject({ reason: "environment-variable-renamed" });
1252
+ await expect(booting).rejects.toThrow(/unset OAUTH_ACCESS_TOKEN_EXPIRES_IN/);
1253
+ });
1254
+
1255
+ it("boots with OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN alone, reading it", async () => {
1256
+ const config = await bootParsed(DOCUMENTED_ENV);
1254
1257
  expect(oauthSection(config).accessToken).toEqual({
1255
1258
  defaultExpiresIn: 900,
1256
1259
  maxExpiresIn: 7200,
@@ -85,6 +85,10 @@ vi.mock("ioredis", () => {
85
85
  quit: async () => "OK",
86
86
  disconnect: () => undefined,
87
87
  ping: async () => "PONG",
88
+ // The Redis stores' eviction gate reads the policy from INFO memory: a
89
+ // default server's.
90
+ info: async (section: string) =>
91
+ section === "memory" ? "# Memory\r\nmaxmemory_policy:noeviction\r\n" : null,
88
92
  };
89
93
  const makeMockRedis = (): object =>
90
94
  new Proxy(
@@ -15,11 +15,12 @@
15
15
  */
16
16
 
17
17
  /**
18
- * The development compose run, `docker-compose.yml`, with MFA on as the
19
- * template ships it, and the Mailpit overlay, `docker-compose.mailpit.yml`: a
20
- * development run whose mail goes through the SMTP sender to Mailpit. It is an
21
- * overlay on `docker-compose.yml`, so a plain `docker compose up` starts no
22
- * Mailpit, and the production file names none. The compose files are read as
18
+ * The development compose run, `docker-compose.yml`, with MFA as the
19
+ * configuration's switch says (on as the template ships it), and the Mailpit
20
+ * overlay, `docker-compose.mailpit.yml`: a development run whose mail goes
21
+ * through the SMTP sender to Mailpit. It is an overlay on
22
+ * `docker-compose.yml`, so a plain `docker compose up` starts no Mailpit, and
23
+ * the production file names none. The compose files are read as
23
24
  * the fixed-shape files they are, and the process each run describes is booted
24
25
  * from the shipped files, as `app.mts` boots them, under the environment the
25
26
  * run sets: `.env.example`'s lines, then each compose file's `environment`, the
@@ -44,6 +45,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
44
45
  import { buildModules } from "#/buildModules.mjs";
45
46
  import { readOwnLayers, readSwitches, resolveConfigPaths, resolveForBoot } from "#/configPath.mjs";
46
47
  import { createRecordingLogger } from "./all-modules-composition.fixture.mjs";
48
+ import { configuredMfaMode } from "./configured-mfa-mode.fixture.mjs";
47
49
 
48
50
  vi.mock("redis", () => ({
49
51
  createClient: vi.fn(() => ({
@@ -74,6 +76,10 @@ vi.mock("ioredis", () => {
74
76
  quit: async () => "OK",
75
77
  disconnect: () => undefined,
76
78
  ping: async () => "PONG",
79
+ // The Redis stores' eviction gate reads the policy from INFO memory: a
80
+ // default server's.
81
+ info: async (section: string) =>
82
+ section === "memory" ? "# Memory\r\nmaxmemory_policy:noeviction\r\n" : null,
77
83
  };
78
84
  const makeMockRedis = (): object =>
79
85
  new Proxy(
@@ -245,14 +251,15 @@ describe("the process each development run describes", () => {
245
251
  };
246
252
  }
247
253
 
248
- it("boots docker-compose.yml's run with MFA required: development, the development mail sender, the MFA stores on its Redis", async () => {
254
+ it("boots docker-compose.yml's run with MFA as the configuration's switch says: development, the development mail sender, the MFA stores on its Redis", async () => {
249
255
  const env = containerEnv(false);
250
256
  // `app.mts`: CONFIG_ENV, then NODE_ENV, then development; the run sets neither.
251
257
  expect(env.CONFIG_ENV ?? env.NODE_ENV).toBeUndefined();
252
258
  const { applicationConfPath, envConfPath } = resolveConfigPaths(configDir, "development");
253
259
  const own = readOwnLayers([envConfPath, applicationConfPath], { env });
254
260
  const switches = readSwitches(own);
255
- expect(switches.mfaMode).toBe("required");
261
+ const mode = configuredMfaMode("development");
262
+ expect(switches.mfaMode).toBe(mode);
256
263
  // A hot reload restarts the process on every save, and Redis keeps the
257
264
  // factors, and the sessions beside them, across it.
258
265
  expect(switches.adapters).toMatchObject({
@@ -277,7 +284,7 @@ describe("the process each development run describes", () => {
277
284
  "standard-development",
278
285
  );
279
286
  expect((handle.components.config as { mfa?: { mode?: unknown } } | undefined)?.mfa?.mode).toBe(
280
- "required",
287
+ mode === "off" ? undefined : mode,
281
288
  );
282
289
  });
283
290
 
@@ -289,18 +296,27 @@ describe("the process each development run describes", () => {
289
296
  expect(existsSync(resolveConfigPaths(configDir, configEnv as string).envConfPath)).toBe(true);
290
297
  });
291
298
 
292
- it("boots with MFA on and the SMTP sender in the mailSender slot, relaying to Mailpit", async () => {
293
- const env = containerEnv();
299
+ /** The key the operator writes into `.env` for the Mailpit run (`openssl rand -base64 32`). */
300
+ const OWN_MFA_KEY = Buffer.alloc(32, 7).toString("base64");
301
+
302
+ /** The Mailpit run's modules and the configuration boot is handed, under `env`. */
303
+ function mailpitRun(env: Record<string, string>) {
294
304
  const configEnv = env.CONFIG_ENV as string;
295
305
  const { applicationConfPath, envConfPath } = resolveConfigPaths(configDir, configEnv);
296
306
  const own = readOwnLayers([envConfPath, applicationConfPath], { env });
297
307
  const switches = readSwitches(own);
298
- expect(switches.mfaMode).toBe("required");
299
-
300
308
  const modules = buildModules(switches, {
301
309
  environment: configEnv,
302
310
  refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
303
311
  });
312
+ return { switches, modules, config: () => resolveForBoot(own, modules, switches) };
313
+ }
314
+
315
+ it("boots with the SMTP sender relaying to Mailpit: in the mailSender slot, with the operator's MFA key, while MFA is on (the configuration's switch); unread, with no mfa section, while it is off", async () => {
316
+ const env: Record<string, string> = { ...containerEnv(), MFA_ENCRYPTION_KEY: OWN_MFA_KEY };
317
+ const { switches, modules, config } = mailpitRun(env);
318
+ const mode = configuredMfaMode(env.CONFIG_ENV as string);
319
+ expect(switches.mfaMode).toBe(mode);
304
320
  const names = modules.map((module) => module.name);
305
321
  expect(names).toContain(standardSmtpMailSenderModule.name);
306
322
  expect(names).not.toContain(
@@ -310,17 +326,68 @@ describe("the process each development run describes", () => {
310
326
  const handle = await createApp({
311
327
  modules,
312
328
  bootstrapComponents: {
313
- config: resolveForBoot(own, modules, switches),
329
+ config: config(),
314
330
  pathResolver: (s: string) => s,
315
331
  logger: createRecordingLogger(),
316
332
  },
317
333
  });
318
334
  handles.push(handle);
319
- expect((handle.components.mailSender as MailSender | undefined)?.kind).toBe("standard-smtp");
335
+ expect((handle.components.mailSender as MailSender | undefined)?.kind).toBe(
336
+ mode === "off" ? undefined : "standard-smtp",
337
+ );
338
+ const mfa = (
339
+ handle.components.config as {
340
+ mfa?: { mode?: unknown; encryptionKeys?: { key?: unknown }[] };
341
+ }
342
+ ).mfa;
343
+ if (mode === "off") {
344
+ expect(mfa).toBeUndefined();
345
+ } else {
346
+ expect(mfa?.mode).toBe(mode);
347
+ expect(mfa?.encryptionKeys?.map((entry) => entry.key)).toEqual([OWN_MFA_KEY]);
348
+ }
320
349
  expect(
321
350
  (handle.components.config as Record<string, unknown> | undefined)?.[
322
351
  "standard-smtp-mail-sender"
323
352
  ],
324
353
  ).toMatchObject({ host: "localhost", port: 1025, secure: "none" });
325
354
  });
355
+
356
+ it("does not carry the development sample key: with MFA on, without MFA_ENCRYPTION_KEY the run is refused; with it off, nothing reads a key", async () => {
357
+ const env = containerEnv();
358
+ expect(env.MFA_ENCRYPTION_KEY).toBeUndefined();
359
+ const { modules, config } = mailpitRun(env);
360
+ if (configuredMfaMode(env.CONFIG_ENV as string) === "off") {
361
+ const handle = await createApp({
362
+ modules,
363
+ bootstrapComponents: {
364
+ config: config(),
365
+ pathResolver: (s: string) => s,
366
+ logger: createRecordingLogger(),
367
+ },
368
+ });
369
+ handles.push(handle);
370
+ expect(handle.components.config).not.toHaveProperty("mfa");
371
+ return;
372
+ }
373
+ const refused = await (async () => {
374
+ try {
375
+ handles.push(
376
+ await createApp({
377
+ modules,
378
+ bootstrapComponents: {
379
+ config: config(),
380
+ pathResolver: (s: string) => s,
381
+ logger: createRecordingLogger(),
382
+ },
383
+ }),
384
+ );
385
+ } catch (error) {
386
+ return error;
387
+ }
388
+ return undefined;
389
+ })();
390
+ expect(refused).toBeInstanceOf(Error);
391
+ expect(String((refused as Error).message)).toMatch(/mfa\.encryptionKeys/);
392
+ });
326
393
  });
@@ -17,10 +17,10 @@
17
17
  /**
18
18
  * The template's MFA switch under the name production, where the MFA stores
19
19
  * must be shared: both on Redis, as a production deployment selects them.
20
- * ioredis is a stand-in, as in `all-modules-composition.multi.test.mts`:
21
- * nothing here issues a command, and the stores' durability check names what
22
- * it could not read and goes on. `packages/redis` runs the stores against a
23
- * real server.
20
+ * ioredis is a stand-in, as in `all-modules-composition.multi.test.mts`: it
21
+ * answers `INFO memory` as a default server does, so the stores' eviction gate
22
+ * passes, and their persistence check names what it could not read and goes
23
+ * on. `packages/redis` runs the stores against a real server.
24
24
  */
25
25
 
26
26
  import { BootError } from "@o3co/auth-provider-core";
@@ -36,6 +36,10 @@ vi.mock("ioredis", () => {
36
36
  quit: async () => "OK",
37
37
  disconnect: () => undefined,
38
38
  ping: async () => "PONG",
39
+ // The Redis stores' eviction gate reads the policy from INFO memory: a
40
+ // default server's.
41
+ info: async (section: string) =>
42
+ section === "memory" ? "# Memory\r\nmaxmemory_policy:noeviction\r\n" : null,
39
43
  };
40
44
  const makeMockRedis = (): object =>
41
45
  new Proxy(