@o3co/create-auth-provider 0.17.0-rc.1 → 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.
- package/README.ja.md +3 -1
- package/README.md +5 -2
- package/package.json +1 -1
- package/templates/standalone/.env.example +7 -5
- package/templates/standalone/README.ja.md +53 -25
- package/templates/standalone/README.md +140 -58
- package/templates/standalone/config/application.conf +41 -36
- package/templates/standalone/config/clients.yaml.example +8 -0
- package/templates/standalone/config/development.conf +2 -2
- package/templates/standalone/config/mailpit.conf +10 -5
- package/templates/standalone/config/reference.conf +30 -5
- package/templates/standalone/docker-compose.mailpit.yml +3 -0
- package/templates/standalone/docker-compose.production.yml +5 -2
- package/templates/standalone/package.json +1 -1
- package/templates/standalone/src/__tests__/adapters.test.mts +92 -25
- package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +87 -0
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +112 -98
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +40 -11
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +86 -51
- package/templates/standalone/src/__tests__/audit-sink.test.mts +10 -9
- package/templates/standalone/src/__tests__/config-notices.test.mts +157 -0
- package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +48 -0
- package/templates/standalone/src/__tests__/core-federations.test.mts +57 -31
- package/templates/standalone/src/__tests__/core-sections.test.mts +7 -5
- package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +1 -1
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +434 -136
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +19 -21
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +9 -15
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +10 -4
- package/templates/standalone/src/__tests__/federation-types.test.mts +239 -0
- package/templates/standalone/src/__tests__/google-session.fixture.mts +60 -67
- package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +1 -1
- package/templates/standalone/src/__tests__/library-references.fixture.mts +51 -46
- package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +216 -0
- package/templates/standalone/src/__tests__/mailpit-compose.test.mts +81 -16
- package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +122 -0
- package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +8 -4
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +149 -46
- package/templates/standalone/src/__tests__/own-modules.test.mts +78 -23
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +3 -3
- package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +214 -0
- package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +8 -4
- package/templates/standalone/src/__tests__/replica-safety.test.mts +131 -19
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +25 -23
- package/templates/standalone/src/__tests__/sections.test.mts +24 -0
- package/templates/standalone/src/__tests__/session-lifecycle.test.mts +215 -0
- package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +3 -7
- package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +130 -0
- package/templates/standalone/src/__tests__/session-requirements.test.mts +114 -31
- package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +274 -0
- package/templates/standalone/src/__tests__/smoke.test.mts +19 -28
- package/templates/standalone/src/__tests__/startup-log-lines.test.mts +148 -0
- package/templates/standalone/src/__tests__/store-credential.test.mts +8 -13
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +58 -38
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +243 -76
- package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +23 -31
- package/templates/standalone/src/adapters.mts +31 -18
- package/templates/standalone/src/app.mts +6 -5
- package/templates/standalone/src/bootRefusal.mts +117 -0
- package/templates/standalone/src/buildModules.mts +46 -96
- package/templates/standalone/src/configPath.mts +180 -153
- package/templates/standalone/src/mfaSwitch.mts +49 -27
- package/templates/standalone/src/modules.mts +37 -219
- package/templates/standalone/src/rootRenames.mts +38 -22
- package/templates/standalone/src/sections.mts +13 -2
- package/templates/standalone/tests/index.test.js +4 -3
- package/templates/versions.json +18 -18
- package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +0 -134
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +0 -202
- package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +0 -144
|
@@ -20,12 +20,18 @@ import { join } from "node:path";
|
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import {
|
|
22
22
|
type AppConfig,
|
|
23
|
+
BootError,
|
|
23
24
|
createApp,
|
|
25
|
+
defineModule,
|
|
24
26
|
type Module,
|
|
25
27
|
moduleReferences,
|
|
26
28
|
resolveAccessTokenLifetime,
|
|
27
29
|
} from "@o3co/auth-provider-core";
|
|
28
|
-
import {
|
|
30
|
+
import { createFakeIdp, type FakeIdp } from "@o3co/auth-provider-core/testing";
|
|
31
|
+
import { googleFederationTypeModule } from "@o3co/auth-provider-federation-google";
|
|
32
|
+
import { federationGrantsModule } from "@o3co/auth-provider-federation-grants";
|
|
33
|
+
import { oidcFederationTypeModule } from "@o3co/auth-provider-federation-oidc";
|
|
34
|
+
import { type OAuthSection, oauthEndpointsModule } from "@o3co/auth-provider-oauth";
|
|
29
35
|
import { sessionModule, sessionStoreModule } from "@o3co/auth-provider-session";
|
|
30
36
|
import { describe, expect, it } from "vitest";
|
|
31
37
|
import { buildModules } from "../buildModules.mjs";
|
|
@@ -35,16 +41,22 @@ import {
|
|
|
35
41
|
readSwitches,
|
|
36
42
|
resolveConfigPaths,
|
|
37
43
|
resolveForBoot,
|
|
44
|
+
resolveLayers,
|
|
38
45
|
type Switches,
|
|
39
46
|
} from "../configPath.mjs";
|
|
40
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
httpModule,
|
|
49
|
+
keyStoreModule,
|
|
50
|
+
standaloneRedisClientsModule,
|
|
51
|
+
templateReference,
|
|
52
|
+
} from "../modules.mjs";
|
|
41
53
|
|
|
42
54
|
/**
|
|
43
55
|
* Boots the shipped config with EVERY documented override supplied the way an
|
|
44
56
|
* operator supplies one: as a string.
|
|
45
57
|
*
|
|
46
|
-
* HOCON substitutes `${?VAR}` as a string, always. `app.mts` reads its
|
|
47
|
-
* with
|
|
58
|
+
* HOCON substitutes `${?VAR}` as a string, always. `app.mts` reads its own
|
|
59
|
+
* switches with the template's schema and hands `createApp` what it resolved,
|
|
48
60
|
* which boot parses once with plain Zod, so every schema leaf must read the
|
|
49
61
|
* string itself. This suite reads through that path: `readSwitches` for what
|
|
50
62
|
* is read before boot, `resolveForBoot` and `createApp` for the configuration
|
|
@@ -94,11 +106,7 @@ const DOCUMENTED_ENV: Readonly<Record<string, string>> = {
|
|
|
94
106
|
// --- oauth tokens / policy ----------------------------------------
|
|
95
107
|
OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN: "900",
|
|
96
108
|
OAUTH_ACCESS_TOKEN_MAX_EXPIRES_IN: "7200",
|
|
97
|
-
// Deprecated alias of the default. Exported alongside the new variable on
|
|
98
|
-
// purpose, with a different value: the new variable must win.
|
|
99
|
-
OAUTH_ACCESS_TOKEN_EXPIRES_IN: "3600",
|
|
100
109
|
OAUTH_REFRESH_TOKEN_EXPIRES_IN: "86400",
|
|
101
|
-
OAUTH_REFRESH_TOKEN_UNKNOWN_FAMILY_POLICY: "reject",
|
|
102
110
|
OAUTH_OIDC_MODE: "dual",
|
|
103
111
|
OAUTH_REVOCATION_ACCESS_TOKEN: "denylist",
|
|
104
112
|
OAUTH_REVOCATION_SUBJECT: "unsupported",
|
|
@@ -131,6 +139,7 @@ const DOCUMENTED_ENV: Readonly<Record<string, string>> = {
|
|
|
131
139
|
OAUTH_SESSION_ENABLED: "false",
|
|
132
140
|
OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED: "true",
|
|
133
141
|
OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_ENABLED: "true",
|
|
142
|
+
OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_UNKNOWN_FAMILY_POLICY: "reject",
|
|
134
143
|
OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED: "true",
|
|
135
144
|
OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED: "false",
|
|
136
145
|
|
|
@@ -156,6 +165,7 @@ const DOCUMENTED_ENV: Readonly<Record<string, string>> = {
|
|
|
156
165
|
// Which adapter fills each slot: the composition root's own section,
|
|
157
166
|
// read before the modules are chosen.
|
|
158
167
|
ADAPTERS_RATE_LIMITER: "redis",
|
|
168
|
+
ADAPTERS_ATTEMPT_COUNTER: "redis",
|
|
159
169
|
ADAPTERS_USER_SESSION_STORES: "redis",
|
|
160
170
|
ADAPTERS_ACCESS_TOKEN_DENYLIST: "redis",
|
|
161
171
|
// The replay seen-set behind private_key_jwt client authentication.
|
|
@@ -190,6 +200,7 @@ const DOCUMENTED_ENV: Readonly<Record<string, string>> = {
|
|
|
190
200
|
REDIS_REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT: "3",
|
|
191
201
|
REDIS_CLIENTS_URL: "redis://redis:6379",
|
|
192
202
|
REDIS_CLIENTS_PASSWORD: "rt-family-password",
|
|
203
|
+
REDIS_CLIENTS_ASSUME_NO_EVICTION: "true",
|
|
193
204
|
// The federation token store's Redis branch.
|
|
194
205
|
REDIS_FEDERATION_TOKEN_STORE_KEY_PREFIX: "ft:",
|
|
195
206
|
REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_MODE: "required",
|
|
@@ -304,145 +315,182 @@ const DOCUMENTED_ENV: Readonly<Record<string, string>> = {
|
|
|
304
315
|
*/
|
|
305
316
|
const DELIBERATELY_UNSET: Readonly<Record<string, string>> = {
|
|
306
317
|
OAUTH_AUTHORIZE_ALLOW_UNMARKED_CLIENTS:
|
|
307
|
-
"
|
|
318
|
+
"the variable of a removed key, and only captured — any value fails boot",
|
|
319
|
+
OAUTH_ACCESS_TOKEN_EXPIRES_IN:
|
|
320
|
+
"renamed OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
308
321
|
DEPLOYMENT_MODE:
|
|
309
|
-
"renamed CORE_DEPLOYMENT_MODE, and only captured — set
|
|
322
|
+
"renamed CORE_DEPLOYMENT_MODE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
310
323
|
MEMORY_RATE_LIMITER_MAX_BUCKETS:
|
|
311
|
-
"renamed CORE_RATE_LIMITER_MEMORY_MAX_BUCKETS, and only captured — set
|
|
324
|
+
"renamed CORE_RATE_LIMITER_MEMORY_MAX_BUCKETS, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
312
325
|
RATE_LIMIT_FAIL_MODE:
|
|
313
|
-
"renamed REDIS_RATE_LIMITER_FAIL_MODE, and only captured — set
|
|
326
|
+
"renamed REDIS_RATE_LIMITER_FAIL_MODE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
314
327
|
REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX:
|
|
315
|
-
"renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_KEY_PREFIX, and only captured — set
|
|
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",
|
|
316
329
|
REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT:
|
|
317
|
-
"renamed REDIS_REFRESH_TOKEN_FAMILY_STORE_CAS_RETRY_LIMIT, and only captured — set
|
|
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",
|
|
318
331
|
FEDERATION_GRANTS_ENCRYPTION_MODE:
|
|
319
|
-
"renamed REDIS_FEDERATION_GRANT_STORE_ENCRYPTION_MODE, and only captured — set
|
|
332
|
+
"renamed REDIS_FEDERATION_GRANT_STORE_ENCRYPTION_MODE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
320
333
|
OAUTH_TOKEN_BINDING_DISPATCH_POLICY:
|
|
321
|
-
"renamed CORE_TOKEN_BINDING_DISPATCH_POLICY, and only captured — set
|
|
334
|
+
"renamed CORE_TOKEN_BINDING_DISPATCH_POLICY, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
322
335
|
OAUTH_TOKEN_BINDING_BIND_CONFIDENTIAL_CLIENT_REFRESH_TOKENS:
|
|
323
|
-
"renamed CORE_TOKEN_BINDING_BIND_CONFIDENTIAL_CLIENT_REFRESH_TOKENS, and only captured — set
|
|
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",
|
|
324
337
|
OAUTH_GRANTS_SESSION_ENABLED:
|
|
325
|
-
"renamed OAUTH_SESSION_ENABLED, and only captured — set
|
|
338
|
+
"renamed OAUTH_SESSION_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
326
339
|
OAUTH_GRANTS_AUTHORIZATION_CODE_ENABLED:
|
|
327
|
-
"renamed OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED, and only captured — set
|
|
340
|
+
"renamed OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
328
341
|
OAUTH_GRANTS_REFRESH_TOKEN_ENABLED:
|
|
329
|
-
"renamed OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_ENABLED, and only captured — set
|
|
342
|
+
"renamed OAUTH_AUTHORIZATION_GRANTS_REFRESH_TOKEN_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
343
|
+
OAUTH_REFRESH_TOKEN_UNKNOWN_FAMILY_POLICY:
|
|
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",
|
|
330
345
|
OAUTH_GRANTS_CLIENT_CREDENTIALS_ENABLED:
|
|
331
|
-
"renamed OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED, and only captured — set
|
|
346
|
+
"renamed OAUTH_AUTHORIZATION_GRANTS_CLIENT_CREDENTIALS_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
332
347
|
OAUTH_GRANTS_JWT_BEARER_ENABLED:
|
|
333
|
-
"renamed OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED, and only captured — set
|
|
348
|
+
"renamed OAUTH_AUTHORIZATION_GRANTS_JWT_BEARER_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
334
349
|
ENDPOINTS_CONSENT_URL:
|
|
335
|
-
"renamed OAUTH_CONSENT_PAGE_URL, and only captured — set
|
|
350
|
+
"renamed OAUTH_CONSENT_PAGE_URL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
336
351
|
OAUTH_CIMD_ENABLED:
|
|
337
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ENABLED, and only captured — set
|
|
352
|
+
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ENABLED, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
338
353
|
OAUTH_CIMD_ALLOWED_SCOPES:
|
|
339
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_SCOPES, and only captured — set
|
|
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",
|
|
340
355
|
OAUTH_CIMD_ALLOWED_AUDIENCES:
|
|
341
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_AUDIENCES, and only captured — set
|
|
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",
|
|
342
357
|
OAUTH_CIMD_ALLOWED_HOSTS:
|
|
343
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_ALLOWED_HOSTS, and only captured — set
|
|
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",
|
|
344
359
|
OAUTH_CIMD_DENIED_HOSTS:
|
|
345
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_DENIED_HOSTS, and only captured — set
|
|
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",
|
|
346
361
|
OAUTH_CIMD_MAX_BYTES:
|
|
347
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_BYTES, and only captured — set
|
|
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",
|
|
348
363
|
OAUTH_CIMD_TIMEOUT_MS:
|
|
349
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_TIMEOUT_MS, and only captured — set
|
|
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",
|
|
350
365
|
OAUTH_CIMD_CACHE_MAX_AGE_MS:
|
|
351
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_CACHE_MAX_AGE_MS, and only captured — set
|
|
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",
|
|
352
367
|
OAUTH_CIMD_MAX_CACHE_ENTRIES:
|
|
353
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CACHE_ENTRIES, and only captured — set
|
|
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",
|
|
354
369
|
OAUTH_CIMD_STALE_IF_ERROR_MS:
|
|
355
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_STALE_IF_ERROR_MS, and only captured — set
|
|
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",
|
|
356
371
|
OAUTH_CIMD_NEGATIVE_CACHE_MS:
|
|
357
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_NEGATIVE_CACHE_MS, and only captured — set
|
|
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",
|
|
358
373
|
OAUTH_CIMD_MAX_CONCURRENT_FETCHES:
|
|
359
|
-
"renamed OAUTH_CLIENT_ID_METADATA_DOCUMENTS_MAX_CONCURRENT_FETCHES, and only captured — set
|
|
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",
|
|
360
375
|
OAUTH_GRANTS_AUTHORIZATION_CODE_PKCE_REQUIRE_S256:
|
|
361
376
|
"the authorization-code grant's pkce block was removed, and this is only captured — set at all, it fails boot",
|
|
362
377
|
ENDPOINTS_LOGIN_URL:
|
|
363
|
-
"renamed SESSION_LOGIN_PAGE_URL, and only captured — set
|
|
378
|
+
"renamed SESSION_LOGIN_PAGE_URL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
364
379
|
SESSION_SECRET:
|
|
365
|
-
"renamed SESSION_STORE_SECRET, and only captured — set
|
|
380
|
+
"renamed SESSION_STORE_SECRET, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
366
381
|
SESSION_NAME:
|
|
367
|
-
"renamed SESSION_STORE_NAME, and only captured — set
|
|
382
|
+
"renamed SESSION_STORE_NAME, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
368
383
|
SESSION_MAX_AGE:
|
|
369
|
-
"renamed SESSION_STORE_MAX_AGE, and only captured — set
|
|
384
|
+
"renamed SESSION_STORE_MAX_AGE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
370
385
|
SESSION_SECURE:
|
|
371
|
-
"renamed SESSION_STORE_SECURE, and only captured — set
|
|
386
|
+
"renamed SESSION_STORE_SECURE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
372
387
|
SESSION_SAME_SITE:
|
|
373
|
-
"renamed SESSION_STORE_SAME_SITE, and only captured — set
|
|
388
|
+
"renamed SESSION_STORE_SAME_SITE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
374
389
|
SESSION_DOMAIN:
|
|
375
|
-
"renamed SESSION_STORE_DOMAIN, and only captured — set
|
|
390
|
+
"renamed SESSION_STORE_DOMAIN, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
376
391
|
SESSION_STORAGE_TYPE:
|
|
377
|
-
"renamed SESSION_STORE_STORAGE_TYPE, and only captured — set
|
|
392
|
+
"renamed SESSION_STORE_STORAGE_TYPE, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
378
393
|
SESSION_STORAGE_REDIS_URL:
|
|
379
|
-
"renamed SESSION_STORE_STORAGE_REDIS_URL, and only captured — set
|
|
394
|
+
"renamed SESSION_STORE_STORAGE_REDIS_URL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
380
395
|
SESSION_STORAGE_REDIS_PASSWORD:
|
|
381
|
-
"renamed SESSION_STORE_STORAGE_REDIS_PASSWORD, and only captured — set
|
|
396
|
+
"renamed SESSION_STORE_STORAGE_REDIS_PASSWORD, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
382
397
|
LOG_LEVEL:
|
|
383
|
-
"renamed LOGGING_LEVEL, and only captured — set
|
|
398
|
+
"renamed LOGGING_LEVEL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
384
399
|
CORS_ALLOWED_ORIGINS:
|
|
385
|
-
"renamed HTTP_CORS_ALLOWED_ORIGINS, and only captured — set
|
|
400
|
+
"renamed HTTP_CORS_ALLOWED_ORIGINS, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
386
401
|
OAUTH_JWT_SIGNING_KEY_PROVIDER:
|
|
387
|
-
"renamed KEY_STORE_PROVIDER, and only captured — set
|
|
402
|
+
"renamed KEY_STORE_PROVIDER, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
388
403
|
OAUTH_JWT_ALGORITHM:
|
|
389
|
-
"renamed KEY_STORE_LOCAL_ALGORITHM, and only captured — set
|
|
404
|
+
"renamed KEY_STORE_LOCAL_ALGORITHM, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
390
405
|
OAUTH_JWT_KID:
|
|
391
|
-
"renamed KEY_STORE_LOCAL_KID, and only captured — set
|
|
406
|
+
"renamed KEY_STORE_LOCAL_KID, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
392
407
|
OAUTH_JWT_SECRET:
|
|
393
|
-
"renamed KEY_STORE_LOCAL_SECRET, and only captured — set
|
|
408
|
+
"renamed KEY_STORE_LOCAL_SECRET, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
394
409
|
OAUTH_JWT_PRIVATE_KEY_PATH:
|
|
395
|
-
"renamed KEY_STORE_LOCAL_PRIVATE_KEY_PATH, and only captured — set
|
|
410
|
+
"renamed KEY_STORE_LOCAL_PRIVATE_KEY_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
396
411
|
OAUTH_JWT_PUBLIC_KEY_PATH:
|
|
397
|
-
"renamed KEY_STORE_LOCAL_PUBLIC_KEY_PATH, and only captured — set
|
|
412
|
+
"renamed KEY_STORE_LOCAL_PUBLIC_KEY_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
398
413
|
OAUTH_JWT_PRIVATE_KEY:
|
|
399
|
-
"renamed KEY_STORE_LOCAL_PRIVATE_KEY, and only captured — set
|
|
414
|
+
"renamed KEY_STORE_LOCAL_PRIVATE_KEY, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
400
415
|
OAUTH_JWT_PUBLIC_KEY:
|
|
401
|
-
"renamed KEY_STORE_LOCAL_PUBLIC_KEY, and only captured — set
|
|
416
|
+
"renamed KEY_STORE_LOCAL_PUBLIC_KEY, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
402
417
|
REFRESH_TOKEN_FAMILY_STORE_REDIS_URL:
|
|
403
|
-
"renamed REDIS_CLIENTS_URL, and only captured — set
|
|
418
|
+
"renamed REDIS_CLIENTS_URL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
404
419
|
REFRESH_TOKEN_FAMILY_STORE_REDIS_PASSWORD:
|
|
405
|
-
"renamed REDIS_CLIENTS_PASSWORD, and only captured — set
|
|
420
|
+
"renamed REDIS_CLIENTS_PASSWORD, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
406
421
|
CLIENT_PATH:
|
|
407
|
-
"renamed REPOSITORIES_CLIENT_YAML_PATH, and only captured — set
|
|
422
|
+
"renamed REPOSITORIES_CLIENT_YAML_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
408
423
|
CLIENT_USER_PATH:
|
|
409
|
-
"renamed REPOSITORIES_USER_YAML_PATH, and only captured — set
|
|
424
|
+
"renamed REPOSITORIES_USER_YAML_PATH, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
410
425
|
CLIENT_USER_AUTHENTICATE_URL:
|
|
411
|
-
"renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_URL, and only captured — set
|
|
426
|
+
"renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_URL, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
412
427
|
CLIENT_USER_AUTHENTICATE_BY_TOKEN_URL:
|
|
413
|
-
"renamed REPOSITORIES_USER_HTTP_AUTHENTICATE_BY_TOKEN_URL, and only captured — set
|
|
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",
|
|
414
429
|
CLIENT_USER_LINK_FEDERATED_IDENTITY_URL:
|
|
415
|
-
"renamed REPOSITORIES_USER_HTTP_LINK_FEDERATED_IDENTITY_URL, and only captured — set
|
|
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",
|
|
416
431
|
CLIENT_USER_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL:
|
|
417
|
-
"renamed REPOSITORIES_USER_HTTP_FIND_SUBJECT_BY_FEDERATED_IDENTITY_URL, and only captured — set
|
|
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",
|
|
418
433
|
CLIENT_USER_BEARER_TOKEN:
|
|
419
|
-
"renamed REPOSITORIES_USER_HTTP_BEARER_TOKEN, and only captured — set
|
|
434
|
+
"renamed REPOSITORIES_USER_HTTP_BEARER_TOKEN, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
420
435
|
CLIENT_USER_TIMEOUT:
|
|
421
|
-
"renamed REPOSITORIES_USER_HTTP_TIMEOUT, and only captured — set
|
|
436
|
+
"renamed REPOSITORIES_USER_HTTP_TIMEOUT, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
422
437
|
CLIENT_USER_MAX_RESPONSE_BYTES:
|
|
423
|
-
"renamed REPOSITORIES_USER_HTTP_MAX_RESPONSE_BYTES, and only captured — set
|
|
438
|
+
"renamed REPOSITORIES_USER_HTTP_MAX_RESPONSE_BYTES, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
424
439
|
CLIENT_CODE_DEFAULT_EXPIRES_IN:
|
|
425
|
-
"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
|
|
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",
|
|
426
441
|
CLIENT_CODE_KEY_PREFIX:
|
|
427
|
-
"renamed REDIS_CODE_REPOSITORY_KEY_PREFIX, and only captured — set
|
|
442
|
+
"renamed REDIS_CODE_REPOSITORY_KEY_PREFIX, and only captured — set at any value, beside its new name or not, it fails boot",
|
|
428
443
|
CLIENT_CODE_ENDPOINT_URI:
|
|
429
444
|
"removed: the Redis code repository uses the shared redis-clients connection; only captured — set at all, it fails boot",
|
|
430
445
|
CLIENT_CODE_PASSWORD:
|
|
431
446
|
"removed: the Redis code repository uses the shared redis-clients connection; only captured — set at all, it fails boot",
|
|
432
447
|
ENDPOINTS_MFA_URL:
|
|
433
|
-
"renamed MFA_PAGE_URL, and only captured — set
|
|
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",
|
|
434
449
|
MFA_TOTP_ENABLED:
|
|
435
|
-
"renamed MFA_TOTP_FACTOR_ENABLED, and only captured — set
|
|
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",
|
|
436
451
|
MFA_TOTP_ISSUER:
|
|
437
|
-
"renamed MFA_TOTP_FACTOR_ISSUER, and only captured — set
|
|
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",
|
|
438
453
|
};
|
|
439
454
|
|
|
440
455
|
/**
|
|
441
|
-
* The
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
456
|
+
* The federation variables `config/application.conf` documents on lines it
|
|
457
|
+
* ships commented out — a key an operator binds by uncommenting it — as the
|
|
458
|
+
* string an operator would supply. They are not live substitutions, so they
|
|
459
|
+
* are not in `DOCUMENTED_ENV`; the federation cases below bind them as their
|
|
460
|
+
* comments write them (`commentedFederationBindings`).
|
|
461
|
+
*/
|
|
462
|
+
const COMMENTED_FEDERATION_ENV: Readonly<Record<string, string>> = {
|
|
463
|
+
CORE_FEDERATIONS_GOOGLE_SESSION_DOMAIN: ".example.com",
|
|
464
|
+
CORE_FEDERATIONS_GOOGLE_AUTH_CALLBACK_URL: "https://app.example.com/auth/callback",
|
|
465
|
+
CORE_FEDERATIONS_GOOGLE_CLIENT_URL: "https://app.example.com/",
|
|
466
|
+
CORE_FEDERATIONS_GOOGLE_REQUIRE_AUTHORIZATION_RESPONSE_ISS: "false",
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Each `# <key> = ${?CORE_FEDERATIONS_<NAME>_…}` line of the shipped
|
|
471
|
+
* `config/application.conf`, uncommented as an operator's layer above it:
|
|
472
|
+
* `core.federations.<name>.<key> = ${?…}`.
|
|
473
|
+
*/
|
|
474
|
+
function commentedFederationBindings(): string {
|
|
475
|
+
const { applicationConfPath } = resolveConfigPaths(configDir, "production");
|
|
476
|
+
return readFileSync(applicationConfPath, "utf8")
|
|
477
|
+
.split("\n")
|
|
478
|
+
.flatMap((line) => {
|
|
479
|
+
const match =
|
|
480
|
+
/^\s*#\s*([A-Za-z]+) = \$\{\?(CORE_FEDERATIONS_([A-Z0-9]+)_[A-Z0-9_]+)\}\s*$/.exec(line);
|
|
481
|
+
if (match === null) return [];
|
|
482
|
+
const [, key, variable, name] = match as unknown as [string, string, string, string];
|
|
483
|
+
return [`core.federations.${name.toLowerCase()}.${key} = \${?${variable}}`];
|
|
484
|
+
})
|
|
485
|
+
.join("\n");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
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
|
|
446
494
|
* suite speaks plain HTTP.
|
|
447
495
|
*
|
|
448
496
|
* The two `FEDERATION_TOKEN_STORE` lines are required: the federation token
|
|
@@ -451,37 +499,23 @@ const DELIBERATELY_UNSET: Readonly<Record<string, string>> = {
|
|
|
451
499
|
* Redis store is selected with its encryption key.
|
|
452
500
|
*/
|
|
453
501
|
const UMBRELLA_E2E_ENV: Readonly<Record<string, string>> = {
|
|
454
|
-
OAUTH_JWT_ALGORITHM: "HS256",
|
|
455
502
|
KEY_STORE_LOCAL_ALGORITHM: "HS256",
|
|
456
|
-
OAUTH_JWT_SECRET: "e2e-shared-hs256-secret.at-least-32-bytes.ok",
|
|
457
503
|
KEY_STORE_LOCAL_SECRET: "e2e-shared-hs256-secret.at-least-32-bytes.ok",
|
|
458
504
|
OAUTH_JWT_ISSUER: "https://auth.e2e.test",
|
|
459
|
-
SESSION_SECRET: "lO0QH09fuKSGuViZ9myJbH3jsgai99A2GpC3RYRuy6Y=",
|
|
460
505
|
SESSION_STORE_SECRET: "lO0QH09fuKSGuViZ9myJbH3jsgai99A2GpC3RYRuy6Y=",
|
|
461
|
-
SESSION_SECURE: "false",
|
|
462
506
|
SESSION_STORE_SECURE: "false",
|
|
463
|
-
SESSION_NAME: "auth.session",
|
|
464
507
|
SESSION_STORE_NAME: "auth.session",
|
|
465
|
-
DEPLOYMENT_MODE: "multi",
|
|
466
508
|
CORE_DEPLOYMENT_MODE: "multi",
|
|
467
|
-
REFRESH_TOKEN_FAMILY_STORE_REDIS_URL: "redis://redis:6379",
|
|
468
509
|
REDIS_CLIENTS_URL: "redis://redis:6379",
|
|
469
|
-
SESSION_STORAGE_REDIS_URL: "redis://redis:6379",
|
|
470
510
|
SESSION_STORE_STORAGE_REDIS_URL: "redis://redis:6379",
|
|
471
|
-
USER_SESSION_STORES_ADAPTER: "redis",
|
|
472
511
|
ADAPTERS_USER_SESSION_STORES: "redis",
|
|
473
|
-
RATE_LIMITER_ADAPTER: "redis",
|
|
474
512
|
ADAPTERS_RATE_LIMITER: "redis",
|
|
475
|
-
OAUTH_CODE_ADAPTER: "redis",
|
|
476
513
|
ADAPTERS_CODE_REPOSITORY: "redis",
|
|
477
|
-
FEDERATION_TOKEN_STORE_TYPE: "redis",
|
|
478
514
|
ADAPTERS_FEDERATION_TOKEN_STORE: "redis",
|
|
479
515
|
REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_KEY: "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=",
|
|
480
|
-
CLIENT_USER_TYPE: "yaml",
|
|
481
516
|
ADAPTERS_USER_REPOSITORY: "yaml",
|
|
482
517
|
OAUTH_RESOURCE_INDICATOR_ENABLED: "true",
|
|
483
518
|
OAUTH_REQUIRE_EMAIL_VERIFIED: "true",
|
|
484
|
-
OAUTH_GRANTS_SESSION_ENABLED: "true",
|
|
485
519
|
OAUTH_SESSION_ENABLED: "true",
|
|
486
520
|
MFA_MODE: "off",
|
|
487
521
|
};
|
|
@@ -507,34 +541,106 @@ function readShippedSwitches(env: Record<string, string>, configEnv = "productio
|
|
|
507
541
|
const FEDERATION_STORES = Object.fromEntries(
|
|
508
542
|
[
|
|
509
543
|
"userSessionStore",
|
|
510
|
-
"
|
|
511
|
-
"sessionFamilyIndex",
|
|
512
|
-
"sessionFederationIndex",
|
|
544
|
+
"sessionLifecycle",
|
|
513
545
|
"federationTokenStore",
|
|
514
546
|
"refreshTokenFamilyRevocation",
|
|
515
547
|
].map((key) => [key, {}]),
|
|
516
548
|
);
|
|
517
549
|
|
|
550
|
+
/** What core handed a federation type for one entry: its callback, and its own keys as the type's schema parsed them. */
|
|
551
|
+
interface Dispatched {
|
|
552
|
+
readonly callbackURL: string;
|
|
553
|
+
readonly entry: Readonly<Record<string, unknown>>;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** A federation type's declaration, as far as this suite reads it. */
|
|
557
|
+
interface TypeDeclaration {
|
|
558
|
+
readonly factory: (deps: unknown, instance: { name: string } & Dispatched) => unknown;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* `module`, a federation type module, with each type's factory recording what
|
|
563
|
+
* core handed it into `dispatched`, by the entry's name, before building the
|
|
564
|
+
* provider as the module does. The schema, the parse and the provider are the
|
|
565
|
+
* module's own.
|
|
566
|
+
*/
|
|
567
|
+
function recordingDispatch(module: Module, dispatched: Map<string, Dispatched>): Module {
|
|
568
|
+
const contributes = module.contributes as {
|
|
569
|
+
readonly federationTypes: Readonly<Record<string, TypeDeclaration>>;
|
|
570
|
+
};
|
|
571
|
+
const federationTypes = Object.fromEntries(
|
|
572
|
+
Object.entries(contributes.federationTypes).map(([type, declaration]) => [
|
|
573
|
+
type,
|
|
574
|
+
{
|
|
575
|
+
...declaration,
|
|
576
|
+
factory: (deps: unknown, instance: { name: string } & Dispatched) => {
|
|
577
|
+
dispatched.set(instance.name, {
|
|
578
|
+
callbackURL: instance.callbackURL,
|
|
579
|
+
entry: instance.entry,
|
|
580
|
+
});
|
|
581
|
+
return declaration.factory(deps, instance);
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
]),
|
|
585
|
+
);
|
|
586
|
+
return { ...module, contributes: { ...contributes, federationTypes } } as Module;
|
|
587
|
+
}
|
|
588
|
+
|
|
518
589
|
/**
|
|
519
|
-
* The
|
|
520
|
-
*
|
|
521
|
-
*
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
590
|
+
* The upstream the documented OIDC federation's issuer names. Its type
|
|
591
|
+
* discovers the issuer at boot, so the module is handed this fake's `fetch`
|
|
592
|
+
* rather than the network. Made once per file.
|
|
593
|
+
*/
|
|
594
|
+
let oidcUpstream: Promise<FakeIdp> | undefined;
|
|
595
|
+
const documentedOidcUpstream = (): Promise<FakeIdp> => {
|
|
596
|
+
oidcUpstream ??= createFakeIdp({
|
|
597
|
+
issuer: DOCUMENTED_ENV.CORE_FEDERATIONS_OIDC_ISSUER as string,
|
|
598
|
+
discovery: true,
|
|
599
|
+
clientId: DOCUMENTED_ENV.CORE_FEDERATIONS_OIDC_CLIENT_ID as string,
|
|
600
|
+
});
|
|
601
|
+
return oidcUpstream;
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* The shipped layers under `env`, as `app.mts` hands them to boot, booted
|
|
606
|
+
* with the federation types the template bundles: phase one for what the
|
|
607
|
+
* composition expects of session admission, phase two resolved over the
|
|
608
|
+
* reference of every package the template's modules come from (the
|
|
609
|
+
* template's own and core's) and parsed once by `createApp` — no bridge on
|
|
610
|
+
* the way. Core dispatches each enabled federation to its type, which parses
|
|
611
|
+
* the entry with its own schema; `dispatched` is what each type was handed.
|
|
612
|
+
* No other module is loaded unless `modules` names one: the parse is what this
|
|
525
613
|
* suite asks about, and each key it reads is one core's schema declares, or
|
|
526
614
|
* the section of a module it loads.
|
|
527
615
|
*/
|
|
528
|
-
|
|
616
|
+
/**
|
|
617
|
+
* The oauth module's section, read without its factories: the template always
|
|
618
|
+
* loads the module, and core refuses keys of `oauth {}` where no loaded
|
|
619
|
+
* module's section is `oauth`. What refuses `oauth {}` here is the module's
|
|
620
|
+
* own section — its schema, its removed keys and variables.
|
|
621
|
+
*/
|
|
622
|
+
const oauthSectionReader = defineModule({
|
|
623
|
+
name: oauthEndpointsModule.name,
|
|
624
|
+
...(oauthEndpointsModule.section === undefined ? {} : { section: oauthEndpointsModule.section }),
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
async function bootDispatched(
|
|
529
628
|
env: Record<string, string>,
|
|
530
629
|
configEnv = "production",
|
|
531
630
|
operatorLayer?: string,
|
|
532
631
|
modules: readonly Module[] = [],
|
|
533
|
-
): Promise<AppConfig> {
|
|
632
|
+
): Promise<{ readonly parsed: AppConfig; readonly dispatched: ReadonlyMap<string, Dispatched> }> {
|
|
534
633
|
const own = readOwnLayers(ownFiles(configEnv, operatorLayer), { env });
|
|
535
634
|
const switches = readSwitches(own);
|
|
635
|
+
const dispatched = new Map<string, Dispatched>();
|
|
636
|
+
const upstream = await documentedOidcUpstream();
|
|
536
637
|
const handle = await createApp({
|
|
537
|
-
modules: [
|
|
638
|
+
modules: [
|
|
639
|
+
recordingDispatch(googleFederationTypeModule(), dispatched),
|
|
640
|
+
recordingDispatch(oidcFederationTypeModule({ fetch: upstream.fetch }), dispatched),
|
|
641
|
+
oauthSectionReader,
|
|
642
|
+
...modules,
|
|
643
|
+
],
|
|
538
644
|
bootstrapComponents: {
|
|
539
645
|
config: resolveForBoot(own, buildModules(switches, { environment: configEnv }), switches),
|
|
540
646
|
pathResolver: (s: string) => s,
|
|
@@ -544,7 +650,17 @@ async function bootParsed(
|
|
|
544
650
|
const parsed = handle.components.config;
|
|
545
651
|
await handle.dispose();
|
|
546
652
|
if (parsed === undefined) throw new Error("createApp booted without the parsed configuration");
|
|
547
|
-
return parsed;
|
|
653
|
+
return { parsed, dispatched };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** The configuration boot parsed, as {@link bootDispatched} boots it. */
|
|
657
|
+
async function bootParsed(
|
|
658
|
+
env: Record<string, string>,
|
|
659
|
+
configEnv = "production",
|
|
660
|
+
operatorLayer?: string,
|
|
661
|
+
modules: readonly Module[] = [],
|
|
662
|
+
): Promise<AppConfig> {
|
|
663
|
+
return (await bootDispatched(env, configEnv, operatorLayer, modules)).parsed;
|
|
548
664
|
}
|
|
549
665
|
|
|
550
666
|
/** `session-store {}` as the session store's module parses it. */
|
|
@@ -560,6 +676,13 @@ function sessionStoreSection(config: AppConfig): {
|
|
|
560
676
|
>;
|
|
561
677
|
}
|
|
562
678
|
|
|
679
|
+
/** `federation-grants {}` as the federation-grants module parses it. */
|
|
680
|
+
function federationGrantsSection(config: AppConfig): { readonly enabled?: boolean } {
|
|
681
|
+
const schema = federationGrantsModule.section?.schema;
|
|
682
|
+
if (schema === undefined) throw new Error("the federation-grants module declares no section");
|
|
683
|
+
return schema.parse(config["federation-grants"]) as ReturnType<typeof federationGrantsSection>;
|
|
684
|
+
}
|
|
685
|
+
|
|
563
686
|
/** `session {}` as the session module parses it. */
|
|
564
687
|
function sessionSection(config: AppConfig): { readonly csrf?: { readonly ttlSeconds: number } } {
|
|
565
688
|
const schema = sessionModule.section?.schema;
|
|
@@ -578,13 +701,10 @@ function httpSectionOf(config: AppConfig): {
|
|
|
578
701
|
}
|
|
579
702
|
|
|
580
703
|
/** `oauth {}` as the oauth module's own schema parses it. */
|
|
581
|
-
function oauthSection(config: AppConfig): {
|
|
582
|
-
|
|
583
|
-
readonly clientIdMetadataDocuments?: Readonly<Record<string, unknown>>;
|
|
584
|
-
} {
|
|
585
|
-
const schema = oauthModule({ config }).section?.schema;
|
|
704
|
+
function oauthSection(config: AppConfig): OAuthSection {
|
|
705
|
+
const schema = oauthEndpointsModule.section?.schema;
|
|
586
706
|
if (schema === undefined) throw new Error("the oauth module declares no section");
|
|
587
|
-
return schema.parse(config.oauth) as
|
|
707
|
+
return schema.parse(config.oauth) as OAuthSection;
|
|
588
708
|
}
|
|
589
709
|
|
|
590
710
|
/** Every `${?VAR}` in a HOCON layer, ignoring commented-out lines. */
|
|
@@ -646,14 +766,19 @@ describe("the shipped config boots with every documented override supplied as a
|
|
|
646
766
|
// Each of these arrives from HOCON as a string. A leftover string is
|
|
647
767
|
// not a cosmetic defect: `=== true` is how the runtime reads them.
|
|
648
768
|
expect(sessionStoreSection(config).secure).toBe(false);
|
|
649
|
-
expect(config.
|
|
650
|
-
expect(config.
|
|
651
|
-
expect(config.
|
|
769
|
+
expect(oauthSection(config).jwt.legacyTypAccept).toBe(true);
|
|
770
|
+
expect(oauthSection(config).requireEmailVerified).toBe(true);
|
|
771
|
+
expect(oauthSection(config).resourceIndicator?.enabled).toBe(true);
|
|
652
772
|
expect(config.core?.federations?.google?.enabled).toBe(true);
|
|
653
773
|
expect(config.core?.federations?.oidc?.enabled).toBe(true);
|
|
654
774
|
// A leftover string here would be read as "on" by a truthiness check
|
|
655
775
|
// and as "off" by `=== true`, for a feature whose whole default is off.
|
|
656
|
-
expect(config
|
|
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 });
|
|
657
782
|
});
|
|
658
783
|
|
|
659
784
|
it("turns every non-boolean override into its declared type", async () => {
|
|
@@ -665,17 +790,18 @@ describe("the shipped config boots with every documented override supplied as a
|
|
|
665
790
|
expect(http.readinessTimeoutMs).toBe(1500);
|
|
666
791
|
expect(http.trustProxy).toEqual(["10.0.0.0/8", "loopback"]);
|
|
667
792
|
expect(config.core?.federations?.google?.accessType).toBe("online");
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
793
|
+
expect(resolveAccessTokenLifetime({ oauth: oauthSection(config) })).toEqual({
|
|
794
|
+
defaultExpiresIn: 900,
|
|
795
|
+
maxExpiresIn: 7200,
|
|
796
|
+
});
|
|
797
|
+
expect(oauthSection(config).accessToken).toEqual({
|
|
671
798
|
defaultExpiresIn: 900,
|
|
672
799
|
maxExpiresIn: 7200,
|
|
673
800
|
});
|
|
674
|
-
expect(config.
|
|
675
|
-
expect(config.oauth.refreshToken.expiresIn).toBe(86400);
|
|
801
|
+
expect(oauthSection(config).refreshToken.expiresIn).toBe(86400);
|
|
676
802
|
expect(sessionStoreSection(config).maxAge).toBe(3600000);
|
|
677
803
|
expect(sessionSection(config).csrf?.ttlSeconds).toBe(7200);
|
|
678
|
-
expect(config.
|
|
804
|
+
expect(oauthSection(config).nonce?.maxLength).toBe(256);
|
|
679
805
|
expect(readShippedSwitches(DOCUMENTED_ENV).adapters.consentStore).toBe("redis");
|
|
680
806
|
// A Redis store's section, which its module (not loaded here) parses.
|
|
681
807
|
const sections = config as unknown as Record<string, { keyPrefix?: unknown } | undefined>;
|
|
@@ -711,6 +837,133 @@ describe("the shipped config boots with every documented override supplied as a
|
|
|
711
837
|
expect(http.cors.allowedOrigins).toEqual(["https://app.example.com", "http://localhost:5173"]);
|
|
712
838
|
});
|
|
713
839
|
|
|
840
|
+
describe("the federations' variables, read by each federation's type", () => {
|
|
841
|
+
it("hands each enabled federation's type the entry its documented variables set", async () => {
|
|
842
|
+
const { dispatched } = await bootDispatched(DOCUMENTED_ENV);
|
|
843
|
+
expect(Object.fromEntries(dispatched)).toEqual({
|
|
844
|
+
google: {
|
|
845
|
+
callbackURL: DOCUMENTED_ENV.CORE_FEDERATIONS_GOOGLE_CALLBACK_URL,
|
|
846
|
+
entry: {
|
|
847
|
+
clientId: DOCUMENTED_ENV.CORE_FEDERATIONS_GOOGLE_CLIENT_ID,
|
|
848
|
+
clientSecret: DOCUMENTED_ENV.CORE_FEDERATIONS_GOOGLE_CLIENT_SECRET,
|
|
849
|
+
redirectAllowlist: [],
|
|
850
|
+
accessType: "online",
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
oidc: {
|
|
854
|
+
callbackURL: DOCUMENTED_ENV.CORE_FEDERATIONS_OIDC_CALLBACK_URL,
|
|
855
|
+
entry: {
|
|
856
|
+
issuer: DOCUMENTED_ENV.CORE_FEDERATIONS_OIDC_ISSUER,
|
|
857
|
+
clientId: DOCUMENTED_ENV.CORE_FEDERATIONS_OIDC_CLIENT_ID,
|
|
858
|
+
clientSecret: DOCUMENTED_ENV.CORE_FEDERATIONS_OIDC_CLIENT_SECRET,
|
|
859
|
+
scopes: ["openid", "profile", "email"],
|
|
860
|
+
redirectAllowlist: [],
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it("reads the Google keys the configuration documents commented out, bound as their comments write them", async () => {
|
|
867
|
+
const { dispatched } = await bootDispatched(
|
|
868
|
+
{ ...DOCUMENTED_ENV, ...COMMENTED_FEDERATION_ENV },
|
|
869
|
+
"production",
|
|
870
|
+
commentedFederationBindings(),
|
|
871
|
+
);
|
|
872
|
+
expect(dispatched.get("google")?.entry).toEqual({
|
|
873
|
+
clientId: DOCUMENTED_ENV.CORE_FEDERATIONS_GOOGLE_CLIENT_ID,
|
|
874
|
+
clientSecret: DOCUMENTED_ENV.CORE_FEDERATIONS_GOOGLE_CLIENT_SECRET,
|
|
875
|
+
redirectAllowlist: [],
|
|
876
|
+
accessType: "online",
|
|
877
|
+
sessionDomain: COMMENTED_FEDERATION_ENV.CORE_FEDERATIONS_GOOGLE_SESSION_DOMAIN,
|
|
878
|
+
authCallbackUrl: COMMENTED_FEDERATION_ENV.CORE_FEDERATIONS_GOOGLE_AUTH_CALLBACK_URL,
|
|
879
|
+
clientUrl: COMMENTED_FEDERATION_ENV.CORE_FEDERATIONS_GOOGLE_CLIENT_URL,
|
|
880
|
+
requireAuthorizationResponseIss: false,
|
|
881
|
+
});
|
|
882
|
+
// `toEqual` passes a key present as `undefined`: no variable binds
|
|
883
|
+
// `endSessionEndpoint`, so the entry must not carry the key at all.
|
|
884
|
+
expect(dispatched.get("google")?.entry).not.toHaveProperty("endSessionEndpoint");
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
it("hands the Google type no key the documented variables leave unset, not even as undefined", async () => {
|
|
888
|
+
const { dispatched } = await bootDispatched(DOCUMENTED_ENV);
|
|
889
|
+
const entry = dispatched.get("google")?.entry;
|
|
890
|
+
expect(entry).toBeDefined();
|
|
891
|
+
for (const key of [
|
|
892
|
+
"sessionDomain",
|
|
893
|
+
"authCallbackUrl",
|
|
894
|
+
"clientUrl",
|
|
895
|
+
"requireAuthorizationResponseIss",
|
|
896
|
+
"endSessionEndpoint",
|
|
897
|
+
]) {
|
|
898
|
+
expect(entry, key).not.toHaveProperty(key);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
it("binds every variable the configuration documents commented out", () => {
|
|
903
|
+
expect(
|
|
904
|
+
commentedFederationBindings()
|
|
905
|
+
.match(/\$\{\?[A-Z0-9_]+\}/g)
|
|
906
|
+
?.sort(),
|
|
907
|
+
).toEqual(
|
|
908
|
+
Object.keys(COMMENTED_FEDERATION_ENV)
|
|
909
|
+
.map((name) => `\${?${name}}`)
|
|
910
|
+
.sort(),
|
|
911
|
+
);
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
/** The entry the Google type was handed under `variables`, the commented keys bound. */
|
|
915
|
+
const googleEntryUnder = async (variables: Record<string, string>) =>
|
|
916
|
+
(
|
|
917
|
+
await bootDispatched(
|
|
918
|
+
{ ...DOCUMENTED_ENV, ...variables },
|
|
919
|
+
"production",
|
|
920
|
+
commentedFederationBindings(),
|
|
921
|
+
)
|
|
922
|
+
).dispatched.get("google")?.entry;
|
|
923
|
+
|
|
924
|
+
for (const [supplied, expected] of [
|
|
925
|
+
["true", true],
|
|
926
|
+
["TRUE", true],
|
|
927
|
+
["1", true],
|
|
928
|
+
["false", false],
|
|
929
|
+
["False", false],
|
|
930
|
+
[" false ", false],
|
|
931
|
+
["0", false],
|
|
932
|
+
] as const) {
|
|
933
|
+
it(`CORE_FEDERATIONS_GOOGLE_REQUIRE_AUTHORIZATION_RESPONSE_ISS=${JSON.stringify(supplied)} reads as ${expected}`, async () => {
|
|
934
|
+
const entry = await googleEntryUnder({
|
|
935
|
+
CORE_FEDERATIONS_GOOGLE_REQUIRE_AUTHORIZATION_RESPONSE_ISS: supplied,
|
|
936
|
+
});
|
|
937
|
+
expect(entry?.requireAuthorizationResponseIss).toBe(expected);
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
it("refuses CORE_FEDERATIONS_GOOGLE_REQUIRE_AUTHORIZATION_RESPONSE_ISS exported empty or misspelt, naming the key, rather than turning the check off", async () => {
|
|
942
|
+
for (const supplied of ["", "no", "off", "ture"]) {
|
|
943
|
+
await expect(
|
|
944
|
+
googleEntryUnder({
|
|
945
|
+
CORE_FEDERATIONS_GOOGLE_REQUIRE_AUTHORIZATION_RESPONSE_ISS: supplied,
|
|
946
|
+
}),
|
|
947
|
+
supplied,
|
|
948
|
+
).rejects.toThrow(/core\.federations\.google\.requireAuthorizationResponseIss/);
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it("reads CORE_FEDERATIONS_GOOGLE_ACCESS_TYPE as offline or online, exactly", async () => {
|
|
953
|
+
for (const supplied of ["offline", "online"]) {
|
|
954
|
+
expect(
|
|
955
|
+
(await googleEntryUnder({ CORE_FEDERATIONS_GOOGLE_ACCESS_TYPE: supplied }))?.accessType,
|
|
956
|
+
).toBe(supplied);
|
|
957
|
+
}
|
|
958
|
+
for (const supplied of ["", "Online", "offine", "true"]) {
|
|
959
|
+
await expect(
|
|
960
|
+
googleEntryUnder({ CORE_FEDERATIONS_GOOGLE_ACCESS_TYPE: supplied }),
|
|
961
|
+
supplied,
|
|
962
|
+
).rejects.toThrow(/core\.federations\.google\.accessType/);
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
|
|
714
967
|
describe("HTTP_CORS_ALLOWED_ORIGINS", () => {
|
|
715
968
|
it("reads an exported-but-empty variable as no origins, not as an error", async () => {
|
|
716
969
|
// The .env / compose / ConfigMap shape. "CORS off" is what both the
|
|
@@ -802,8 +1055,8 @@ describe("the shipped config boots with every documented override supplied as a
|
|
|
802
1055
|
it("parses the environment the umbrella E2E boots, with SESSION_STORE_SECURE=false as a string", async () => {
|
|
803
1056
|
const config = await bootParsed(UMBRELLA_E2E_ENV);
|
|
804
1057
|
expect(sessionStoreSection(config).secure).toBe(false);
|
|
805
|
-
expect(config.
|
|
806
|
-
expect(config.
|
|
1058
|
+
expect(oauthSection(config).requireEmailVerified).toBe(true);
|
|
1059
|
+
expect(oauthSection(config).resourceIndicator?.enabled).toBe(true);
|
|
807
1060
|
expect(config.core?.deployment?.mode).toBe("multi");
|
|
808
1061
|
});
|
|
809
1062
|
|
|
@@ -814,8 +1067,14 @@ describe("the shipped config boots with every documented override supplied as a
|
|
|
814
1067
|
// list covers core's modules only, and cannot see the template's own
|
|
815
1068
|
// memory modules.
|
|
816
1069
|
const { replicaUnsafeReason } = await import("@o3co/auth-provider-core");
|
|
817
|
-
|
|
818
|
-
|
|
1070
|
+
// Each module's section at its name: a declaration made from the
|
|
1071
|
+
// section is answered for it.
|
|
1072
|
+
const own = readOwnLayers(ownFiles("production"), { env: UMBRELLA_E2E_ENV });
|
|
1073
|
+
const switches = readSwitches(own);
|
|
1074
|
+
const modules = buildModules(switches);
|
|
1075
|
+
const sections = resolveForBoot(own, modules, switches) as unknown as Record<string, unknown>;
|
|
1076
|
+
for (const module of modules) {
|
|
1077
|
+
expect(replicaUnsafeReason(module, sections[module.name]), module.name).toBeUndefined();
|
|
819
1078
|
}
|
|
820
1079
|
});
|
|
821
1080
|
|
|
@@ -852,7 +1111,7 @@ describe("the shipped config boots with every documented override supplied as a
|
|
|
852
1111
|
...DOCUMENTED_ENV,
|
|
853
1112
|
OAUTH_JWT_LEGACY_TYP_ACCEPT: supplied,
|
|
854
1113
|
});
|
|
855
|
-
expect(config.
|
|
1114
|
+
expect(oauthSection(config).jwt.legacyTypAccept).toBe(expected);
|
|
856
1115
|
});
|
|
857
1116
|
}
|
|
858
1117
|
|
|
@@ -916,13 +1175,13 @@ describe("the shipped config boots with every documented override supplied as a
|
|
|
916
1175
|
});
|
|
917
1176
|
|
|
918
1177
|
describe("variables whose documented behaviour is to fail boot", () => {
|
|
919
|
-
it("refuses OAUTH_AUTHORIZE_ALLOW_UNMARKED_CLIENTS, naming allowUnmarkedClients", async () => {
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
).rejects.toThrow(/allowUnmarkedClients/);
|
|
1178
|
+
it("refuses OAUTH_AUTHORIZE_ALLOW_UNMARKED_CLIENTS as the variable of a removed key, naming allowUnmarkedClients", async () => {
|
|
1179
|
+
const booting = bootParsed({
|
|
1180
|
+
...DOCUMENTED_ENV,
|
|
1181
|
+
OAUTH_AUTHORIZE_ALLOW_UNMARKED_CLIENTS: "true",
|
|
1182
|
+
});
|
|
1183
|
+
await expect(booting).rejects.toMatchObject({ reason: "environment-variable-renamed" });
|
|
1184
|
+
await expect(booting).rejects.toThrow(/oauth\.authorize\.allowUnmarkedClients/);
|
|
926
1185
|
});
|
|
927
1186
|
|
|
928
1187
|
it("refuses an access-token default above the max, naming both keys", async () => {
|
|
@@ -938,30 +1197,69 @@ describe("the shipped config boots with every documented override supplied as a
|
|
|
938
1197
|
for (const name of [
|
|
939
1198
|
"OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN",
|
|
940
1199
|
"OAUTH_ACCESS_TOKEN_MAX_EXPIRES_IN",
|
|
941
|
-
"OAUTH_ACCESS_TOKEN_EXPIRES_IN",
|
|
942
1200
|
]) {
|
|
943
1201
|
it(`refuses an empty ${name} rather than minting already-expired tokens`, async () => {
|
|
944
|
-
await expect(bootParsed({ ...DOCUMENTED_ENV, [name]: "" })).rejects.
|
|
1202
|
+
await expect(bootParsed({ ...DOCUMENTED_ENV, [name]: "" })).rejects.toMatchObject({
|
|
1203
|
+
reason: "config-validation-failed",
|
|
1204
|
+
});
|
|
945
1205
|
});
|
|
946
1206
|
}
|
|
947
1207
|
|
|
948
1208
|
it("refuses MFA_MODE that is none of the three before boot, naming mfaMode", async () => {
|
|
949
1209
|
const env = { ...DOCUMENTED_ENV, MFA_MODE: "on" };
|
|
950
|
-
expect(() => readShippedSwitches(env)).toThrow(
|
|
1210
|
+
expect(() => readShippedSwitches(env)).toThrow(BootError);
|
|
951
1211
|
await expect(bootParsed(env)).rejects.toThrow(/mfaMode/);
|
|
952
1212
|
});
|
|
953
1213
|
|
|
954
1214
|
for (const mode of ["optional", "required"] as const) {
|
|
955
1215
|
it(`reads MFA_MODE=${mode} as the switch that installs MFA, expecting mfa`, () => {
|
|
956
|
-
const
|
|
1216
|
+
const own = readOwnLayers(ownFiles("production"), {
|
|
1217
|
+
env: { ...DOCUMENTED_ENV, MFA_MODE: mode },
|
|
1218
|
+
});
|
|
1219
|
+
const switches = readSwitches(own);
|
|
957
1220
|
expect(switches.mfaMode).toBe(mode);
|
|
958
|
-
|
|
1221
|
+
const shipped = (resolveLayers(own, []).core as { sessionRequirements?: unknown })
|
|
1222
|
+
.sessionRequirements;
|
|
1223
|
+
expect(expectedSessionRequirements(shipped, switches.mfaMode)).toEqual({
|
|
959
1224
|
expected: ["mfa"],
|
|
960
1225
|
secondFactorAuthority: "mfa",
|
|
961
1226
|
});
|
|
962
1227
|
});
|
|
963
1228
|
}
|
|
964
1229
|
|
|
1230
|
+
it("refuses OAUTH_ACCESS_TOKEN_EXPIRES_IN set alone, naming OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN", async () => {
|
|
1231
|
+
const { OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN: _new, ...env } = DOCUMENTED_ENV;
|
|
1232
|
+
const booting = bootParsed({ ...env, OAUTH_ACCESS_TOKEN_EXPIRES_IN: "900" });
|
|
1233
|
+
await expect(booting).rejects.toMatchObject({ reason: "environment-variable-renamed" });
|
|
1234
|
+
await expect(booting).rejects.toThrow(
|
|
1235
|
+
/OAUTH_ACCESS_TOKEN_EXPIRES_IN was renamed OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN/,
|
|
1236
|
+
);
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
it("refuses OAUTH_ACCESS_TOKEN_EXPIRES_IN beside its new name set to another value", async () => {
|
|
1240
|
+
await expect(
|
|
1241
|
+
bootParsed({ ...DOCUMENTED_ENV, OAUTH_ACCESS_TOKEN_EXPIRES_IN: "3600" }),
|
|
1242
|
+
).rejects.toMatchObject({ reason: "environment-variable-renamed" });
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
it("refuses OAUTH_ACCESS_TOKEN_EXPIRES_IN beside its new name set to the same value", async () => {
|
|
1246
|
+
const booting = bootParsed({
|
|
1247
|
+
...DOCUMENTED_ENV,
|
|
1248
|
+
OAUTH_ACCESS_TOKEN_EXPIRES_IN:
|
|
1249
|
+
DOCUMENTED_ENV.OAUTH_ACCESS_TOKEN_DEFAULT_EXPIRES_IN as string,
|
|
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);
|
|
1257
|
+
expect(oauthSection(config).accessToken).toEqual({
|
|
1258
|
+
defaultExpiresIn: 900,
|
|
1259
|
+
maxExpiresIn: 7200,
|
|
1260
|
+
});
|
|
1261
|
+
});
|
|
1262
|
+
|
|
965
1263
|
it("refuses DEPLOYMENT_MODE set alone, naming CORE_DEPLOYMENT_MODE", async () => {
|
|
966
1264
|
const { CORE_DEPLOYMENT_MODE: _new, ...env } = DOCUMENTED_ENV;
|
|
967
1265
|
await expect(bootParsed({ ...env, DEPLOYMENT_MODE: "multi" })).rejects.toThrow(
|