@o3co/create-auth-provider 0.16.0 → 0.17.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +66 -21
- package/README.md +76 -31
- package/dist/index.d.mts +33 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +118 -59
- package/dist/internal/template-filter.d.mts +7 -10
- package/dist/internal/template-filter.d.mts.map +1 -1
- package/dist/internal/template-filter.mjs +7 -10
- package/package.json +6 -4
- package/templates/standalone/.dockerignore +15 -2
- package/templates/standalone/.env.example +59 -29
- package/templates/standalone/Dockerfile +6 -2
- package/templates/standalone/README.ja.md +324 -169
- package/templates/standalone/README.md +618 -208
- package/templates/standalone/config/application.conf +211 -352
- package/templates/standalone/config/clients.yaml +2 -0
- package/templates/standalone/config/clients.yaml.example +4 -0
- package/templates/standalone/config/development.conf +13 -4
- package/templates/standalone/config/mailpit.conf +8 -0
- package/templates/standalone/config/production.conf +5 -6
- package/templates/standalone/config/reference.conf +418 -0
- package/templates/standalone/docker-compose.mailpit.yml +36 -0
- package/templates/standalone/docker-compose.production.yml +63 -32
- package/templates/standalone/docker-compose.yml +13 -3
- package/templates/standalone/gitignore +1 -0
- package/templates/standalone/package.json +9 -6
- package/templates/standalone/src/__tests__/adapters.test.mts +419 -0
- package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +87 -0
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +287 -198
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +71 -35
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +148 -94
- package/templates/standalone/src/__tests__/audit-sink.test.mts +84 -81
- package/templates/standalone/src/__tests__/config-notices.test.mts +154 -0
- package/templates/standalone/src/__tests__/configPath.test.mts +13 -11
- package/templates/standalone/src/__tests__/core-federations.test.mts +249 -0
- package/templates/standalone/src/__tests__/core-sections.test.mts +190 -0
- package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +10 -15
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +970 -288
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +421 -86
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +87 -72
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +16 -8
- package/templates/standalone/src/__tests__/federation-types.test.mts +239 -0
- package/templates/standalone/src/__tests__/google-session.fixture.mts +97 -93
- package/templates/standalone/src/__tests__/host-routes.test.mts +4 -11
- package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +1 -1
- package/templates/standalone/src/__tests__/library-references.fixture.mts +188 -0
- package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +216 -0
- package/templates/standalone/src/__tests__/listen.test.mts +6 -13
- package/templates/standalone/src/__tests__/logger-projection.test.mts +1 -1
- package/templates/standalone/src/__tests__/mail-sender.test.mts +134 -0
- package/templates/standalone/src/__tests__/mailpit-compose.test.mts +326 -0
- package/templates/standalone/src/__tests__/metrics.test.mts +4 -9
- package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +122 -0
- package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +123 -0
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +825 -0
- package/templates/standalone/src/__tests__/own-modules.test.mts +1486 -0
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +10 -14
- package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +1 -5
- package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +116 -81
- package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +25 -31
- package/templates/standalone/src/__tests__/replica-safety.test.mts +265 -146
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +4 -7
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +278 -103
- package/templates/standalone/src/__tests__/sections.test.mts +480 -0
- package/templates/standalone/src/__tests__/session-lifecycle.test.mts +215 -0
- package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +69 -61
- package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +130 -0
- package/templates/standalone/src/__tests__/session-requirements.test.mts +382 -0
- package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +270 -0
- package/templates/standalone/src/__tests__/shutdown.test.mts +163 -96
- package/templates/standalone/src/__tests__/smoke.test.mts +176 -258
- package/templates/standalone/src/__tests__/store-credential.test.mts +88 -52
- package/templates/standalone/src/__tests__/supertest-loopback.test.mts +182 -11
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +131 -102
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +604 -0
- package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +382 -0
- package/templates/standalone/src/adapters.mts +162 -0
- package/templates/standalone/src/app.mts +65 -79
- package/templates/standalone/src/bootRefusal.mts +117 -0
- package/templates/standalone/src/buildModules.mts +220 -309
- package/templates/standalone/src/configPath.mts +449 -29
- package/templates/standalone/src/listen.mts +12 -18
- package/templates/standalone/src/logger.mts +31 -60
- package/templates/standalone/src/metrics.mts +24 -47
- package/templates/standalone/src/mfaSwitch.mts +285 -0
- package/templates/standalone/src/modules.mts +423 -629
- package/templates/standalone/src/rootRenames.mts +96 -0
- package/templates/standalone/src/routes.mts +8 -14
- package/templates/standalone/src/sections.mts +367 -0
- package/templates/standalone/src/shutdown.mts +45 -123
- package/templates/standalone/tests/index.test.js +6 -5
- package/templates/standalone/tests/package-lock.json +1615 -2223
- package/templates/standalone/tests/package.json +2 -2
- package/templates/standalone/tests/vitest.config.mjs +26 -0
- package/templates/standalone/vitest.config.mts +8 -7
- package/templates/standalone/vitest.supertest-loopback.mts +97 -69
- package/templates/versions.json +18 -15
- package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +0 -135
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +0 -199
- package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +0 -138
|
@@ -15,63 +15,56 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* The standalone's own store modules under `core.deployment.mode = "multi"`,
|
|
19
|
+
* booted the way an operator reaches them: from the shipped HOCON with one
|
|
20
|
+
* environment variable flipped.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
22
|
+
* - Every memory branch must be refused by name, and the all-Redis
|
|
23
|
+
* environment must boot. The replica-safety guard reads each module's
|
|
24
|
+
* manifest declaration, which covers the template's own in-memory modules
|
|
25
|
+
* and express-session's store (`sessionStoreModule`, which declares from
|
|
26
|
+
* its own section).
|
|
27
|
+
* - `federationTokenStore.type = "redis"` mounts
|
|
28
|
+
* `redisFederationTokenStoreModule` off the shared ioredis socket.
|
|
29
|
+
* - The Redis federation store's `allow-plaintext` guard reads the
|
|
30
|
+
* environment the config was selected by (`CONFIG_ENV || NODE_ENV`, passed
|
|
31
|
+
* through `buildModules`) and refuses under `core.deployment.mode = "multi"` in
|
|
32
|
+
* every environment.
|
|
30
33
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* the
|
|
34
|
-
*
|
|
35
|
-
* below is the test the README sentence never had.
|
|
36
|
-
*
|
|
37
|
-
* #474: express-session's own store was the one memory store the guard could
|
|
38
|
-
* not see — `SESSION_STORAGE_TYPE=memory` is config, and `sessionStoreModule`'s
|
|
39
|
-
* static manifest carried no declaration. `buildModules` now builds it from
|
|
40
|
-
* the config (`sessionStoreModuleFor`), so it is refused by name like the rest.
|
|
41
|
-
*
|
|
42
|
-
* #473: the Redis federation store's `allow-plaintext` guard keyed off
|
|
43
|
-
* `NODE_ENV` alone. It now reads the environment the config was selected by
|
|
44
|
-
* (`CONFIG_ENV || NODE_ENV`, passed through `buildModules`) and refuses under
|
|
45
|
-
* `deployment.mode = "multi"` in every environment.
|
|
46
|
-
*
|
|
47
|
-
* ioredis is mocked, as in `device-code-store-client-module.test.mts`: the
|
|
48
|
-
* point is composition and the boot planner's stage-1 verdict, not Redis.
|
|
49
|
-
* Nothing here issues a command. node-redis and connect-redis — the session
|
|
50
|
-
* store's own connection — are mocked for the same reason, since #474 puts
|
|
51
|
-
* `SESSION_STORAGE_TYPE=redis` in the baseline.
|
|
34
|
+
* ioredis, node-redis and connect-redis are mocked, as in
|
|
35
|
+
* `device-code-store-client-module.test.mts`: the point is composition and
|
|
36
|
+
* the boot planner's stage-1 verdict, not Redis. Nothing here issues a
|
|
37
|
+
* command.
|
|
52
38
|
*/
|
|
53
39
|
|
|
54
40
|
import { fileURLToPath } from "node:url";
|
|
55
41
|
import {
|
|
56
|
-
type AppConfig,
|
|
57
|
-
AppConfigSchema,
|
|
58
42
|
createApp,
|
|
59
43
|
createKeyStoreFactory,
|
|
60
44
|
defineModule,
|
|
61
45
|
InMemoryClientRepository,
|
|
62
46
|
InMemoryUserRepository,
|
|
47
|
+
type Module,
|
|
63
48
|
memoryRefreshTokenFamilyStoreModule,
|
|
64
49
|
registerBuiltinKeyStores,
|
|
65
50
|
replicaUnsafeReason,
|
|
66
51
|
} from "@o3co/auth-provider-core";
|
|
52
|
+
import { sessionStoreModule } from "@o3co/auth-provider-session";
|
|
53
|
+
import { standardSmtpMailSenderConfigForTests } from "@o3co/auth-provider-standard/testing";
|
|
67
54
|
import { parseFile } from "@o3co/ts.hocon";
|
|
68
|
-
import { validate } from "@o3co/ts.hocon/zod";
|
|
69
55
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
70
56
|
import { buildModules } from "../buildModules.mjs";
|
|
71
|
-
import { resolveConfigPaths
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
57
|
+
import { resolveConfigPaths } from "../configPath.mjs";
|
|
58
|
+
import { templateReference } from "../modules.mjs";
|
|
59
|
+
import {
|
|
60
|
+
type BothPhases,
|
|
61
|
+
bothPhasesOf,
|
|
62
|
+
capturedRenames,
|
|
63
|
+
libraryLayers,
|
|
64
|
+
} from "./library-references.fixture.mjs";
|
|
65
|
+
|
|
66
|
+
// The redis session-store builder, which the baseline selects, dynamically
|
|
67
|
+
// imports these; mock them so no socket opens.
|
|
75
68
|
vi.mock("redis", () => ({
|
|
76
69
|
createClient: vi.fn(() => ({
|
|
77
70
|
connect: vi.fn().mockResolvedValue(undefined),
|
|
@@ -141,41 +134,47 @@ const ENCRYPTION_KEY = Buffer.alloc(32, 7).toString("base64");
|
|
|
141
134
|
|
|
142
135
|
/**
|
|
143
136
|
* The umbrella E2E's shape (`o3co/auth` `tests/docker-compose.yml`), with
|
|
144
|
-
* every shared store on Redis
|
|
145
|
-
*
|
|
137
|
+
* every shared store on Redis, express-session's own included, and MFA off
|
|
138
|
+
* as the umbrella sets it. Each case below flips one variable off this.
|
|
146
139
|
*/
|
|
147
140
|
const ALL_REDIS_ENV: Readonly<Record<string, string>> = {
|
|
148
|
-
|
|
149
|
-
|
|
141
|
+
MFA_MODE: "off",
|
|
142
|
+
KEY_STORE_LOCAL_ALGORITHM: "HS256",
|
|
143
|
+
KEY_STORE_LOCAL_SECRET: "replica-safety-test-secret.at-least-32-bytes.ok",
|
|
150
144
|
OAUTH_JWT_ISSUER: "https://auth.test",
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
145
|
+
SESSION_STORE_SECRET: "replica-safety-session-secret.at-least-32-bytes.ok",
|
|
146
|
+
SESSION_STORE_SECURE: "false",
|
|
147
|
+
SESSION_STORE_NAME: "auth.session",
|
|
148
|
+
SESSION_STORE_STORAGE_TYPE: "redis",
|
|
149
|
+
SESSION_STORE_STORAGE_REDIS_URL: "redis://redis.test:6379",
|
|
150
|
+
ADAPTERS_USER_REPOSITORY: "yaml",
|
|
151
|
+
CORE_DEPLOYMENT_MODE: "multi",
|
|
152
|
+
REDIS_CLIENTS_URL: "redis://redis.test:6379",
|
|
153
|
+
ADAPTERS_USER_SESSION_STORES: "redis",
|
|
154
|
+
ADAPTERS_RATE_LIMITER: "redis",
|
|
155
|
+
// The login's attempt counter, shared: per process, `multi` refuses the login.
|
|
156
|
+
ADAPTERS_ATTEMPT_COUNTER: "redis",
|
|
157
|
+
ADAPTERS_CODE_REPOSITORY: "redis",
|
|
158
|
+
ADAPTERS_ACCESS_TOKEN_DENYLIST: "redis",
|
|
159
|
+
ADAPTERS_REPLAY_SEEN_SET: "redis",
|
|
160
|
+
ADAPTERS_FEDERATION_TOKEN_STORE: "redis",
|
|
165
161
|
REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_KEY: ENCRYPTION_KEY,
|
|
166
|
-
//
|
|
162
|
+
// The consent step for clients that are not first-party, on the
|
|
167
163
|
// shared store — the one switch value `multi` accepts besides `none`.
|
|
168
|
-
|
|
164
|
+
ADAPTERS_CONSENT_STORE: "redis",
|
|
169
165
|
};
|
|
170
166
|
|
|
171
|
-
function resolveConfig(env: Record<string, string>):
|
|
167
|
+
function resolveConfig(env: Record<string, string>): BothPhases {
|
|
172
168
|
const { applicationConfPath, envConfPath } = resolveConfigPaths(configDir, "production");
|
|
173
|
-
|
|
174
|
-
parseFile(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
169
|
+
const layers = parseFile(envConfPath, { env })
|
|
170
|
+
.withFallback(parseFile(applicationConfPath, { env }))
|
|
171
|
+
.withFallback(parseFile(fileURLToPath(templateReference()), { env }))
|
|
172
|
+
.withFallback(libraryLayers(env));
|
|
173
|
+
return {
|
|
174
|
+
...bothPhasesOf(layers, env),
|
|
175
|
+
// What the resolution captured of the renamed variables.
|
|
176
|
+
"renamed-variables": capturedRenames(env),
|
|
177
|
+
} as BothPhases;
|
|
179
178
|
}
|
|
180
179
|
|
|
181
180
|
/** Drops a variable, so the HOCON default takes over. */
|
|
@@ -204,20 +203,27 @@ const testKeyStoreModule = defineModule({
|
|
|
204
203
|
registerBuiltinKeyStores(factory);
|
|
205
204
|
return factory.create({
|
|
206
205
|
type: "local",
|
|
207
|
-
...((c as
|
|
206
|
+
...((c as { "key-store"?: { local?: object } })["key-store"]?.local ?? {}),
|
|
208
207
|
});
|
|
209
208
|
},
|
|
210
209
|
},
|
|
211
210
|
});
|
|
212
211
|
|
|
213
|
-
const modulesFor = (config:
|
|
212
|
+
const modulesFor = (config: BothPhases, environment?: string) =>
|
|
214
213
|
buildModules(config, {
|
|
215
214
|
keyStoreModule: testKeyStoreModule,
|
|
216
215
|
repositoriesModule: testRepositoriesModule,
|
|
217
216
|
...(environment === undefined ? {} : { environment }),
|
|
218
217
|
});
|
|
219
218
|
|
|
220
|
-
|
|
219
|
+
/**
|
|
220
|
+
* `module`'s section in `config`, for a declaration made from it: each module
|
|
221
|
+
* here that has one sits at its name.
|
|
222
|
+
*/
|
|
223
|
+
const sectionOf = (config: BothPhases, module: Module): unknown =>
|
|
224
|
+
(config as unknown as Record<string, unknown>)[module.name];
|
|
225
|
+
|
|
226
|
+
const boot = (config: BothPhases, environment?: string) =>
|
|
221
227
|
createApp({
|
|
222
228
|
modules: modulesFor(config, environment),
|
|
223
229
|
bootstrapComponents: { config, pathResolver: (s) => s },
|
|
@@ -229,7 +235,7 @@ const messageChain = (err: unknown): string => {
|
|
|
229
235
|
return `${e.message ?? ""} ${e.cause?.message ?? ""}`;
|
|
230
236
|
};
|
|
231
237
|
|
|
232
|
-
describe('
|
|
238
|
+
describe('the standalone\'s memory modules are refused under core.deployment.mode = "multi"', () => {
|
|
233
239
|
let handleRef: Awaited<ReturnType<typeof boot>> | undefined;
|
|
234
240
|
|
|
235
241
|
afterEach(async () => {
|
|
@@ -238,21 +244,20 @@ describe('#455: the standalone\'s memory modules are refused under deployment.mo
|
|
|
238
244
|
});
|
|
239
245
|
|
|
240
246
|
const cases: ReadonlyArray<readonly [variable: string, module: string]> = [
|
|
241
|
-
// The template's own modules
|
|
242
|
-
["
|
|
243
|
-
["
|
|
244
|
-
["
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
["
|
|
248
|
-
//
|
|
249
|
-
["
|
|
250
|
-
// Core's, selected by the same kind of switch.
|
|
251
|
-
|
|
252
|
-
["
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
["REPLAY_SEEN_SET_ADAPTER", "core-replay-seen-set-memory"],
|
|
247
|
+
// The template's own modules.
|
|
248
|
+
["ADAPTERS_USER_SESSION_STORES", "standalone-in-memory-session-stores"],
|
|
249
|
+
["ADAPTERS_CODE_REPOSITORY", "standalone-in-memory-code-repository"],
|
|
250
|
+
["ADAPTERS_FEDERATION_TOKEN_STORE", "standalone-in-memory-federation-token-store"],
|
|
251
|
+
// express-session's own store. Not a module of this template: it
|
|
252
|
+
// declares from its own section, which boot parses.
|
|
253
|
+
["SESSION_STORE_STORAGE_TYPE", "session-store"],
|
|
254
|
+
// The consent store, wired only when the switch says so.
|
|
255
|
+
["ADAPTERS_CONSENT_STORE", "core-consent-store-memory"],
|
|
256
|
+
// Core's, selected by the same kind of switch.
|
|
257
|
+
["ADAPTERS_RATE_LIMITER", "core-rate-limiter-memory"],
|
|
258
|
+
["ADAPTERS_ACCESS_TOKEN_DENYLIST", "core-access-token-denylist-memory"],
|
|
259
|
+
// The jti single-use record behind private_key_jwt client auth.
|
|
260
|
+
["ADAPTERS_REPLAY_SEEN_SET", "core-replay-seen-set-memory"],
|
|
256
261
|
];
|
|
257
262
|
|
|
258
263
|
for (const [variable, module] of cases) {
|
|
@@ -269,14 +274,14 @@ describe('#455: the standalone\'s memory modules are refused under deployment.mo
|
|
|
269
274
|
it("names every memory module together when every switch is memory", async () => {
|
|
270
275
|
const config = resolveConfig({
|
|
271
276
|
...ALL_REDIS_ENV,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
ADAPTERS_USER_SESSION_STORES: "memory",
|
|
278
|
+
ADAPTERS_CODE_REPOSITORY: "memory",
|
|
279
|
+
ADAPTERS_FEDERATION_TOKEN_STORE: "memory",
|
|
280
|
+
SESSION_STORE_STORAGE_TYPE: "memory",
|
|
281
|
+
ADAPTERS_RATE_LIMITER: "memory",
|
|
282
|
+
ADAPTERS_ACCESS_TOKEN_DENYLIST: "memory",
|
|
283
|
+
ADAPTERS_REPLAY_SEEN_SET: "memory",
|
|
284
|
+
ADAPTERS_CONSENT_STORE: "memory",
|
|
280
285
|
});
|
|
281
286
|
await expect(boot(config)).rejects.toMatchObject({
|
|
282
287
|
reason: "replica-unsafe-adapter",
|
|
@@ -287,21 +292,21 @@ describe('#455: the standalone\'s memory modules are refused under deployment.mo
|
|
|
287
292
|
});
|
|
288
293
|
|
|
289
294
|
it("each standalone memory module declares its consequence on its own manifest", () => {
|
|
290
|
-
// The declaration is what the guard reads
|
|
295
|
+
// The declaration is what the guard reads, so it has to be on
|
|
291
296
|
// the module and it has to say what breaks — the guard quotes it.
|
|
292
297
|
const config = resolveConfig({
|
|
293
298
|
...ALL_REDIS_ENV,
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
299
|
+
ADAPTERS_USER_SESSION_STORES: "memory",
|
|
300
|
+
ADAPTERS_CODE_REPOSITORY: "memory",
|
|
301
|
+
ADAPTERS_FEDERATION_TOKEN_STORE: "memory",
|
|
297
302
|
});
|
|
298
303
|
const standaloneMemoryModules = modulesFor(config).filter((m) =>
|
|
299
|
-
m.name.startsWith("standalone
|
|
304
|
+
m.name.startsWith("standalone-in-memory-"),
|
|
300
305
|
);
|
|
301
306
|
expect(standaloneMemoryModules.map((m) => m.name).sort()).toEqual([
|
|
302
|
-
"standalone
|
|
303
|
-
"standalone
|
|
304
|
-
"standalone
|
|
307
|
+
"standalone-in-memory-code-repository",
|
|
308
|
+
"standalone-in-memory-federation-token-store",
|
|
309
|
+
"standalone-in-memory-session-stores",
|
|
305
310
|
]);
|
|
306
311
|
for (const m of standaloneMemoryModules) {
|
|
307
312
|
expect(replicaUnsafeReason(m), m.name).toBeDefined();
|
|
@@ -312,14 +317,116 @@ describe('#455: the standalone\'s memory modules are refused under deployment.mo
|
|
|
312
317
|
it("boots the all-Redis environment, with nothing declaring replica-unsafe state", async () => {
|
|
313
318
|
const config = resolveConfig(ALL_REDIS_ENV);
|
|
314
319
|
for (const m of modulesFor(config)) {
|
|
315
|
-
expect(replicaUnsafeReason(m), m.name).toBeUndefined();
|
|
320
|
+
expect(replicaUnsafeReason(m, sectionOf(config, m)), m.name).toBeUndefined();
|
|
316
321
|
}
|
|
317
322
|
handleRef = await boot(config);
|
|
318
323
|
expect(handleRef).toBeDefined();
|
|
319
324
|
});
|
|
320
325
|
});
|
|
321
326
|
|
|
322
|
-
describe('
|
|
327
|
+
describe("express-session's store declares its replica safety from its own section", () => {
|
|
328
|
+
it("is the session package's sessionStoreModule, listed first", () => {
|
|
329
|
+
expect(modulesFor(resolveConfig(ALL_REDIS_ENV))[0]).toBe(sessionStoreModule);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
/** Each way the storage type is set, laid over the all-Redis environment. */
|
|
333
|
+
const STORAGE: ReadonlyArray<readonly [string, Record<string, string>]> = [
|
|
334
|
+
[
|
|
335
|
+
"SESSION_STORE_STORAGE_TYPE=memory",
|
|
336
|
+
{ ...ALL_REDIS_ENV, SESSION_STORE_STORAGE_TYPE: "memory" },
|
|
337
|
+
],
|
|
338
|
+
["SESSION_STORE_STORAGE_TYPE=redis", ALL_REDIS_ENV],
|
|
339
|
+
[
|
|
340
|
+
"no SESSION_STORE_STORAGE_TYPE, the reference's default",
|
|
341
|
+
without(ALL_REDIS_ENV, "SESSION_STORE_STORAGE_TYPE"),
|
|
342
|
+
],
|
|
343
|
+
[
|
|
344
|
+
"the variable it was renamed from, SESSION_STORAGE_TYPE=memory",
|
|
345
|
+
{ ...without(ALL_REDIS_ENV, "SESSION_STORE_STORAGE_TYPE"), SESSION_STORAGE_TYPE: "memory" },
|
|
346
|
+
],
|
|
347
|
+
];
|
|
348
|
+
const MODES: ReadonlyArray<
|
|
349
|
+
readonly [string, (env: Record<string, string>) => Record<string, string>]
|
|
350
|
+
> = [
|
|
351
|
+
["multi", (env) => ({ ...env, CORE_DEPLOYMENT_MODE: "multi" })],
|
|
352
|
+
["single", (env) => ({ ...env, CORE_DEPLOYMENT_MODE: "single" })],
|
|
353
|
+
["unset", (env) => without(env, "CORE_DEPLOYMENT_MODE")],
|
|
354
|
+
];
|
|
355
|
+
|
|
356
|
+
/** What a boot of `modules` settles as: the refusal, or the replica-safety warnings it logged. */
|
|
357
|
+
async function outcomeOf(config: BothPhases, modules: readonly Module[]): Promise<unknown> {
|
|
358
|
+
const warn = vi.fn();
|
|
359
|
+
const logger = {
|
|
360
|
+
warn,
|
|
361
|
+
info: vi.fn(),
|
|
362
|
+
error: vi.fn(),
|
|
363
|
+
debug: vi.fn(),
|
|
364
|
+
trace: vi.fn(),
|
|
365
|
+
fatal: vi.fn(),
|
|
366
|
+
child: vi.fn(),
|
|
367
|
+
};
|
|
368
|
+
try {
|
|
369
|
+
const handle = await createApp({
|
|
370
|
+
modules: [...modules],
|
|
371
|
+
bootstrapComponents: { config, logger, pathResolver: (s: string) => s } as never,
|
|
372
|
+
});
|
|
373
|
+
await handle.dispose();
|
|
374
|
+
} catch (err) {
|
|
375
|
+
const e = err as { reason?: string; message?: string; details?: unknown };
|
|
376
|
+
return { refused: { reason: e.reason, message: e.message, details: e.details } };
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
warned: warn.mock.calls.filter(([, event]) => event === "replica_unsafe_adapters"),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (const [storage, env] of STORAGE) {
|
|
384
|
+
for (const [mode, withMode] of MODES) {
|
|
385
|
+
it(`${storage} under ${mode}`, async () => {
|
|
386
|
+
const config = resolveConfig(withMode(env));
|
|
387
|
+
const modules = modulesFor(config);
|
|
388
|
+
expect(modules[0]).toBe(sessionStoreModule);
|
|
389
|
+
const outcome = await outcomeOf(config, modules);
|
|
390
|
+
if (
|
|
391
|
+
storage === "SESSION_STORE_STORAGE_TYPE=redis" ||
|
|
392
|
+
storage === "no SESSION_STORE_STORAGE_TYPE, the reference's default"
|
|
393
|
+
) {
|
|
394
|
+
// A shared store: nothing is refused or warned about.
|
|
395
|
+
expect(outcome).toEqual({ warned: [] });
|
|
396
|
+
}
|
|
397
|
+
if (storage === "SESSION_STORE_STORAGE_TYPE=memory") {
|
|
398
|
+
const named = { modules: ["session-store"] };
|
|
399
|
+
expect(outcome).toEqual(
|
|
400
|
+
mode === "multi"
|
|
401
|
+
? {
|
|
402
|
+
refused: expect.objectContaining({
|
|
403
|
+
reason: "replica-unsafe-adapter",
|
|
404
|
+
message: expect.stringContaining(
|
|
405
|
+
"session-store: the express-session store forks per replica",
|
|
406
|
+
),
|
|
407
|
+
details: expect.objectContaining(named),
|
|
408
|
+
}),
|
|
409
|
+
}
|
|
410
|
+
: {
|
|
411
|
+
warned:
|
|
412
|
+
mode === "unset"
|
|
413
|
+
? [[expect.objectContaining(named), "replica_unsafe_adapters"]]
|
|
414
|
+
: [],
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
if (storage === "the variable it was renamed from, SESSION_STORAGE_TYPE=memory") {
|
|
419
|
+
// Refused as renamed before the replica-safety guard reads anything.
|
|
420
|
+
expect(outcome).toEqual({
|
|
421
|
+
refused: expect.objectContaining({ reason: "environment-variable-renamed" }),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
describe('adapters.federationTokenStore = "redis" in the standalone', () => {
|
|
323
430
|
let handleRef: Awaited<ReturnType<typeof boot>> | undefined;
|
|
324
431
|
|
|
325
432
|
afterEach(async () => {
|
|
@@ -327,13 +434,11 @@ describe('#456: federationTokenStore.type = "redis" in the standalone', () => {
|
|
|
327
434
|
handleRef = undefined;
|
|
328
435
|
});
|
|
329
436
|
|
|
330
|
-
it("reaches
|
|
331
|
-
|
|
332
|
-
// switch had to be declared before an env var could select anything.
|
|
333
|
-
expect(resolveConfig(ALL_REDIS_ENV).federationTokenStore?.type).toBe("redis");
|
|
437
|
+
it("reaches phase one's adapters from ADAPTERS_FEDERATION_TOKEN_STORE", () => {
|
|
438
|
+
expect(resolveConfig(ALL_REDIS_ENV).adapters.federationTokenStore).toBe("redis");
|
|
334
439
|
expect(
|
|
335
|
-
resolveConfig(without(ALL_REDIS_ENV, "
|
|
336
|
-
|
|
440
|
+
resolveConfig(without(ALL_REDIS_ENV, "ADAPTERS_FEDERATION_TOKEN_STORE")).adapters
|
|
441
|
+
.federationTokenStore,
|
|
337
442
|
).toBe("memory");
|
|
338
443
|
});
|
|
339
444
|
|
|
@@ -341,8 +446,8 @@ describe('#456: federationTokenStore.type = "redis" in the standalone', () => {
|
|
|
341
446
|
const modules = modulesFor(resolveConfig(ALL_REDIS_ENV));
|
|
342
447
|
const names = modules.map((m) => m.name);
|
|
343
448
|
expect(names).toContain("redis-federation-token-store");
|
|
344
|
-
expect(names).toContain("
|
|
345
|
-
expect(names).not.toContain("standalone
|
|
449
|
+
expect(names).toContain("redis-clients");
|
|
450
|
+
expect(names).not.toContain("standalone-in-memory-federation-token-store");
|
|
346
451
|
// Exactly one provider for the slot: both modules provide it, so
|
|
347
452
|
// selecting both would be a boot-time slot collision.
|
|
348
453
|
const providers = modules.filter((m) =>
|
|
@@ -355,18 +460,18 @@ describe('#456: federationTokenStore.type = "redis" in the standalone', () => {
|
|
|
355
460
|
// Every other store on memory, single replica: the Redis federation
|
|
356
461
|
// store still needs `federationTokenStoreClient`, which only the shared
|
|
357
462
|
// clients module provides. Without this the branch fails stage 1 with
|
|
358
|
-
// `missing-required-component
|
|
463
|
+
// `missing-required-component`.
|
|
359
464
|
const config = resolveConfig({
|
|
360
465
|
...ALL_REDIS_ENV,
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
466
|
+
CORE_DEPLOYMENT_MODE: "single",
|
|
467
|
+
ADAPTERS_USER_SESSION_STORES: "memory",
|
|
468
|
+
ADAPTERS_CODE_REPOSITORY: "memory",
|
|
469
|
+
ADAPTERS_RATE_LIMITER: "memory",
|
|
470
|
+
ADAPTERS_ACCESS_TOKEN_DENYLIST: "memory",
|
|
366
471
|
});
|
|
367
472
|
const names = modulesFor(config).map((m) => m.name);
|
|
368
473
|
expect(names).toContain("redis-federation-token-store");
|
|
369
|
-
expect(names).toContain("
|
|
474
|
+
expect(names).toContain("redis-clients");
|
|
370
475
|
});
|
|
371
476
|
|
|
372
477
|
it("boots, and the resolved store is the Redis adapter", async () => {
|
|
@@ -388,20 +493,20 @@ describe('#456: federationTokenStore.type = "redis" in the standalone', () => {
|
|
|
388
493
|
});
|
|
389
494
|
});
|
|
390
495
|
|
|
391
|
-
it("keeps the memory branch
|
|
496
|
+
it("keeps the memory branch: the default, in single mode, resolves the memory adapter", async () => {
|
|
392
497
|
const config = resolveConfig({
|
|
393
|
-
...without(ALL_REDIS_ENV, "
|
|
394
|
-
|
|
498
|
+
...without(ALL_REDIS_ENV, "ADAPTERS_FEDERATION_TOKEN_STORE"),
|
|
499
|
+
CORE_DEPLOYMENT_MODE: "single",
|
|
395
500
|
});
|
|
396
501
|
const names = modulesFor(config).map((m) => m.name);
|
|
397
|
-
expect(names).toContain("standalone
|
|
502
|
+
expect(names).toContain("standalone-in-memory-federation-token-store");
|
|
398
503
|
expect(names).not.toContain("redis-federation-token-store");
|
|
399
504
|
handleRef = await boot(config);
|
|
400
505
|
expect(handleRef.components.federationTokenStore?.kind).toBe("memory");
|
|
401
506
|
});
|
|
402
507
|
});
|
|
403
508
|
|
|
404
|
-
describe('
|
|
509
|
+
describe('consentStore.adapter = "redis" in the standalone', () => {
|
|
405
510
|
let handleRef: Awaited<ReturnType<typeof boot>> | undefined;
|
|
406
511
|
|
|
407
512
|
afterEach(async () => {
|
|
@@ -428,16 +533,16 @@ describe('#561: consentStore.adapter = "redis" in the standalone', () => {
|
|
|
428
533
|
it("pulls the shared clients module in for the consent stores alone", () => {
|
|
429
534
|
// Every other store on memory, single replica: the consent module still
|
|
430
535
|
// needs its two client slots, which only the shared clients module
|
|
431
|
-
// provides
|
|
536
|
+
// provides.
|
|
432
537
|
const config = resolveConfig({
|
|
433
538
|
...ALL_REDIS_ENV,
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
539
|
+
CORE_DEPLOYMENT_MODE: "single",
|
|
540
|
+
ADAPTERS_USER_SESSION_STORES: "memory",
|
|
541
|
+
ADAPTERS_CODE_REPOSITORY: "memory",
|
|
542
|
+
ADAPTERS_RATE_LIMITER: "memory",
|
|
543
|
+
ADAPTERS_ACCESS_TOKEN_DENYLIST: "memory",
|
|
544
|
+
ADAPTERS_REPLAY_SEEN_SET: "memory",
|
|
545
|
+
ADAPTERS_FEDERATION_TOKEN_STORE: "memory",
|
|
441
546
|
});
|
|
442
547
|
const names = buildModules(config, {
|
|
443
548
|
keyStoreModule: testKeyStoreModule,
|
|
@@ -445,17 +550,17 @@ describe('#561: consentStore.adapter = "redis" in the standalone', () => {
|
|
|
445
550
|
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
446
551
|
}).map((m) => m.name);
|
|
447
552
|
expect(names).toContain("redis-consent-store");
|
|
448
|
-
expect(names).toContain("
|
|
553
|
+
expect(names).toContain("redis-clients");
|
|
449
554
|
});
|
|
450
555
|
|
|
451
|
-
it("boots under
|
|
556
|
+
it("boots under CORE_DEPLOYMENT_MODE=multi with both slots resolved to the Redis adapters", async () => {
|
|
452
557
|
handleRef = await boot(resolveConfig(ALL_REDIS_ENV));
|
|
453
558
|
expect(handleRef.components.consentStore?.kind).toBe("redis");
|
|
454
559
|
expect(handleRef.components.pendingConsentStore?.kind).toBe("redis");
|
|
455
560
|
});
|
|
456
561
|
});
|
|
457
562
|
|
|
458
|
-
describe("
|
|
563
|
+
describe("the Redis federation store's plaintext guard, booted from the shipped config", () => {
|
|
459
564
|
let handleRef: Awaited<ReturnType<typeof boot>> | undefined;
|
|
460
565
|
let warnSpy: ReturnType<typeof vi.spyOn>;
|
|
461
566
|
let errorSpy: ReturnType<typeof vi.spyOn>;
|
|
@@ -486,28 +591,36 @@ describe("#473: the Redis federation store's plaintext guard, booted from the sh
|
|
|
486
591
|
REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_MODE: "allow-plaintext",
|
|
487
592
|
};
|
|
488
593
|
|
|
489
|
-
it('is refused under deployment.mode = "multi" in
|
|
490
|
-
// The umbrella shape with plaintext
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
|
|
494
|
-
|
|
594
|
+
it('is refused under core.deployment.mode = "multi" in an environment that is not production', async () => {
|
|
595
|
+
// The umbrella shape with plaintext. A multi-replica deployment is
|
|
596
|
+
// never a development box. A development configuration under multi is
|
|
597
|
+
// refused by the development mail sender first, so the environment here
|
|
598
|
+
// is another name that is not production, with the SMTP sender's section.
|
|
599
|
+
await expect(
|
|
600
|
+
boot({ ...resolveConfig(PLAINTEXT_ENV), ...standardSmtpMailSenderConfigForTests() }, "test"),
|
|
601
|
+
).rejects.toSatisfy((err: unknown) =>
|
|
602
|
+
/allow-plaintext[\s\S]*core\.deployment\.mode is "multi"/.test(messageChain(err)),
|
|
495
603
|
);
|
|
496
604
|
expect(errorSpy).not.toHaveBeenCalled();
|
|
497
605
|
});
|
|
498
606
|
|
|
499
607
|
it("is refused when the config was selected by CONFIG_ENV=production, whatever NODE_ENV says", async () => {
|
|
500
608
|
// `app.mts` selects `production.conf` by `CONFIG_ENV || NODE_ENV` and
|
|
501
|
-
// passes that name through `buildModules`; the guard
|
|
502
|
-
// NODE_ENV alone
|
|
503
|
-
|
|
609
|
+
// passes that name through `buildModules`; the guard reads that name,
|
|
610
|
+
// not NODE_ENV alone.
|
|
611
|
+
// Under production the template installs the SMTP sender's module, whose
|
|
612
|
+
// section the package's builder carries.
|
|
613
|
+
const config = {
|
|
614
|
+
...resolveConfig({ ...PLAINTEXT_ENV, CORE_DEPLOYMENT_MODE: "single" }),
|
|
615
|
+
...standardSmtpMailSenderConfigForTests(),
|
|
616
|
+
};
|
|
504
617
|
await expect(boot(config, "production")).rejects.toSatisfy((err: unknown) =>
|
|
505
|
-
/the environment is "production"/.test(messageChain(err)),
|
|
618
|
+
/allow-plaintext[\s\S]*the environment is "production"/.test(messageChain(err)),
|
|
506
619
|
);
|
|
507
620
|
});
|
|
508
621
|
|
|
509
622
|
it("boots with the plaintext warning in a development environment on a single replica", async () => {
|
|
510
|
-
const config = resolveConfig({ ...PLAINTEXT_ENV,
|
|
623
|
+
const config = resolveConfig({ ...PLAINTEXT_ENV, CORE_DEPLOYMENT_MODE: "single" });
|
|
511
624
|
handleRef = await boot(config, "development");
|
|
512
625
|
expect(handleRef.components.federationTokenStore?.kind).toBe("redis");
|
|
513
626
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
@@ -518,7 +631,13 @@ describe("#473: the Redis federation store's plaintext guard, booted from the sh
|
|
|
518
631
|
|
|
519
632
|
it("keeps the FEDERATION_TOKENS_ALLOW_INSECURE=1 escape hatch under multi, logged at error", async () => {
|
|
520
633
|
process.env.FEDERATION_TOKENS_ALLOW_INSECURE = "1";
|
|
521
|
-
|
|
634
|
+
// Under multi the development mail sender refuses a development
|
|
635
|
+
// configuration, so the environment here is another name that is not
|
|
636
|
+
// production, with the SMTP sender's section its module reads.
|
|
637
|
+
handleRef = await boot(
|
|
638
|
+
{ ...resolveConfig(PLAINTEXT_ENV), ...standardSmtpMailSenderConfigForTests() },
|
|
639
|
+
"test",
|
|
640
|
+
);
|
|
522
641
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
523
642
|
expect.objectContaining({
|
|
524
643
|
store: "federation-tokens",
|
|
@@ -19,13 +19,10 @@
|
|
|
19
19
|
* with no terminal handler of the template's after it — the router alone, as
|
|
20
20
|
* a composition root that copies nothing of `app.mts` mounts it.
|
|
21
21
|
*
|
|
22
|
-
* The
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* other host answered with Express's final handler — an HTML page, with the
|
|
27
|
-
* stack outside production, where V8's JSON error quotes the body it could
|
|
28
|
-
* not parse. The router core returns now answers them itself.
|
|
22
|
+
* The router core returns answers a body parser's refusal and any error a
|
|
23
|
+
* route lets through itself. Left to Express's final handler, a host would
|
|
24
|
+
* answer with an HTML page carrying the stack outside production, where V8's
|
|
25
|
+
* JSON error quotes the body it could not parse.
|
|
29
26
|
*/
|
|
30
27
|
|
|
31
28
|
import { defineModule } from "@o3co/auth-provider-core";
|