@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,28 +15,25 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* A browser session with Google linked, on the standalone as a
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* A browser session with Google linked, on the standalone composed as a
|
|
19
|
+
* deployment does: `oauthEndpointsModule`, the session module and the real Google
|
|
20
|
+
* adapter — the Google federation type `buildModules` lists, handed the
|
|
21
|
+
* shipped `core.federations.google` entry — booted through `createApp` under
|
|
22
|
+
* the shipped configuration.
|
|
21
23
|
*
|
|
22
|
-
* The
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* seen here.
|
|
24
|
+
* The two logout routes and `POST /oauth/federation/:name/token` live in
|
|
25
|
+
* `@o3co/auth-provider-oauth`, which depends on no adapter; only here can a
|
|
26
|
+
* test see what they hand the real adapter and what they make of what its
|
|
27
|
+
* library raises.
|
|
27
28
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* claims the case needs — so the deployment signs with an HS256 secret this
|
|
32
|
-
* file holds, and nothing but Google is federated.
|
|
29
|
+
* Unlike `all-modules-composition.fixture.mts`, the deployment signs with an
|
|
30
|
+
* HS256 secret this file holds, so tests can present an `id_token_hint` and
|
|
31
|
+
* access token of their own; nothing but Google is federated.
|
|
33
32
|
*/
|
|
34
33
|
|
|
35
34
|
import { createHmac } from "node:crypto";
|
|
36
35
|
import { fileURLToPath } from "node:url";
|
|
37
36
|
import {
|
|
38
|
-
type AppConfig,
|
|
39
|
-
AppConfigSchema,
|
|
40
37
|
createApp,
|
|
41
38
|
createKeyStoreFactory,
|
|
42
39
|
defineModule,
|
|
@@ -46,20 +43,23 @@ import {
|
|
|
46
43
|
InMemoryUserRepository,
|
|
47
44
|
memoryRefreshTokenFamilyStoreModule,
|
|
48
45
|
registerBuiltinKeyStores,
|
|
49
|
-
type
|
|
46
|
+
type SessionLifecycle,
|
|
50
47
|
type UserSessionStore,
|
|
51
48
|
} from "@o3co/auth-provider-core";
|
|
52
|
-
import {
|
|
53
|
-
type GoogleProviderConfig,
|
|
54
|
-
googleFederationModule,
|
|
55
|
-
} from "@o3co/auth-provider-federation-google";
|
|
49
|
+
import { googleFederationTypeModule } from "@o3co/auth-provider-federation-google";
|
|
56
50
|
import { parseFile } from "@o3co/ts.hocon";
|
|
57
|
-
import { validate } from "@o3co/ts.hocon/zod";
|
|
58
51
|
import express from "express";
|
|
59
52
|
import request from "supertest";
|
|
60
53
|
import { expect } from "vitest";
|
|
61
54
|
import { buildModules } from "#/buildModules.mjs";
|
|
62
|
-
import { resolveConfigPaths
|
|
55
|
+
import { resolveConfigPaths } from "#/configPath.mjs";
|
|
56
|
+
import { templateReference } from "../modules.mjs";
|
|
57
|
+
import {
|
|
58
|
+
type BothPhases,
|
|
59
|
+
bothPhasesOf,
|
|
60
|
+
capturedRenames,
|
|
61
|
+
libraryLayers,
|
|
62
|
+
} from "./library-references.fixture.mjs";
|
|
63
63
|
|
|
64
64
|
const configDir = fileURLToPath(new URL("../../config", import.meta.url));
|
|
65
65
|
|
|
@@ -82,53 +82,71 @@ const GOOGLE_CLIENT_SECRET = "google-secret";
|
|
|
82
82
|
const GOOGLE_CALLBACK = `${ISSUER}/session/oauth/federation/google/callback`;
|
|
83
83
|
|
|
84
84
|
const ENV: Readonly<Record<string, string>> = {
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// A session signed in through Google alone, no second factor.
|
|
86
|
+
MFA_MODE: "off",
|
|
87
|
+
KEY_STORE_LOCAL_ALGORITHM: "HS256",
|
|
88
|
+
KEY_STORE_LOCAL_SECRET: JWT_SECRET,
|
|
87
89
|
OAUTH_JWT_ISSUER: ISSUER,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
90
|
+
SESSION_STORE_SECRET: "google-session.fixture-session.at-least-32-bytes.ok",
|
|
91
|
+
SESSION_STORE_SECURE: "false",
|
|
92
|
+
SESSION_STORE_NAME: "auth.session",
|
|
93
|
+
SESSION_STORE_STORAGE_TYPE: "memory",
|
|
94
|
+
ADAPTERS_USER_REPOSITORY: "yaml",
|
|
95
|
+
REDIS_CLIENTS_URL: "redis://redis.test:6379",
|
|
96
|
+
ADAPTERS_USER_SESSION_STORES: "memory",
|
|
97
|
+
ADAPTERS_RATE_LIMITER: "memory",
|
|
98
|
+
ADAPTERS_CODE_REPOSITORY: "memory",
|
|
99
|
+
ADAPTERS_ACCESS_TOKEN_DENYLIST: "memory",
|
|
100
|
+
ADAPTERS_REPLAY_SEEN_SET: "memory",
|
|
101
|
+
ADAPTERS_FEDERATION_TOKEN_STORE: "memory",
|
|
102
|
+
ADAPTERS_CONSENT_STORE: "none",
|
|
101
103
|
};
|
|
102
104
|
|
|
103
105
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* `
|
|
106
|
+
* Google switched on the way an operator does, through its environment
|
|
107
|
+
* variables, and handled by the Google federation type the template lists.
|
|
108
|
+
* `"shipped"` is that alone. Otherwise, beside it: `endSessionEndpoint`, a key
|
|
109
|
+
* of the entry with no environment form, written into the entry as an
|
|
110
|
+
* operator's own layer would; and `fetch`, the one the type module is built
|
|
111
|
+
* with in place of the global one.
|
|
109
112
|
*/
|
|
110
113
|
export type GoogleWiring =
|
|
111
114
|
| "shipped"
|
|
112
|
-
|
|
|
115
|
+
| { readonly endSessionEndpoint?: string; readonly fetch?: typeof fetch };
|
|
116
|
+
|
|
117
|
+
const GOOGLE_ENV: Readonly<Record<string, string>> = {
|
|
118
|
+
CORE_FEDERATIONS_GOOGLE_ENABLED: "true",
|
|
119
|
+
CORE_FEDERATIONS_GOOGLE_CLIENT_ID: GOOGLE_CLIENT_ID,
|
|
120
|
+
CORE_FEDERATIONS_GOOGLE_CLIENT_SECRET: GOOGLE_CLIENT_SECRET,
|
|
121
|
+
CORE_FEDERATIONS_GOOGLE_CALLBACK_URL: GOOGLE_CALLBACK,
|
|
122
|
+
};
|
|
113
123
|
|
|
114
|
-
function resolveConfig(google: GoogleWiring):
|
|
115
|
-
const env: Record<string, string> =
|
|
116
|
-
google === "shipped"
|
|
117
|
-
? {
|
|
118
|
-
...ENV,
|
|
119
|
-
FEDERATIONS_GOOGLE_ENABLED: "true",
|
|
120
|
-
FEDERATIONS_GOOGLE_CLIENT_ID: GOOGLE_CLIENT_ID,
|
|
121
|
-
FEDERATIONS_GOOGLE_CLIENT_SECRET: GOOGLE_CLIENT_SECRET,
|
|
122
|
-
FEDERATIONS_GOOGLE_CALLBACK_URL: GOOGLE_CALLBACK,
|
|
123
|
-
}
|
|
124
|
-
: { ...ENV };
|
|
124
|
+
function resolveConfig(google: GoogleWiring): BothPhases {
|
|
125
|
+
const env: Record<string, string> = { ...ENV, ...GOOGLE_ENV };
|
|
125
126
|
const { applicationConfPath, envConfPath } = resolveConfigPaths(configDir, "production");
|
|
126
|
-
|
|
127
|
-
parseFile(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
const layers = parseFile(envConfPath, { env })
|
|
128
|
+
.withFallback(parseFile(applicationConfPath, { env }))
|
|
129
|
+
.withFallback(parseFile(fileURLToPath(templateReference()), { env }))
|
|
130
|
+
.withFallback(libraryLayers(env));
|
|
131
|
+
const config = bothPhasesOf(layers, env);
|
|
132
|
+
const endSessionEndpoint = google === "shipped" ? undefined : google.endSessionEndpoint;
|
|
133
|
+
const federations = config.core?.federations;
|
|
134
|
+
return {
|
|
135
|
+
...config,
|
|
136
|
+
...(endSessionEndpoint === undefined
|
|
137
|
+
? {}
|
|
138
|
+
: {
|
|
139
|
+
core: {
|
|
140
|
+
...config.core,
|
|
141
|
+
federations: {
|
|
142
|
+
...federations,
|
|
143
|
+
google: { ...federations?.google, endSessionEndpoint },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}),
|
|
147
|
+
// What the resolution captured of the renamed variables.
|
|
148
|
+
"renamed-variables": capturedRenames(env),
|
|
149
|
+
} as BothPhases;
|
|
132
150
|
}
|
|
133
151
|
|
|
134
152
|
const testRepositoriesModule = defineModule({
|
|
@@ -170,7 +188,7 @@ const testKeyStoreModule = defineModule({
|
|
|
170
188
|
registerBuiltinKeyStores(factory);
|
|
171
189
|
return factory.create({
|
|
172
190
|
type: "local",
|
|
173
|
-
...((c as
|
|
191
|
+
...((c as { "key-store"?: { local?: object } })["key-store"]?.local ?? {}),
|
|
174
192
|
});
|
|
175
193
|
},
|
|
176
194
|
},
|
|
@@ -216,15 +234,15 @@ export interface GoogleSession {
|
|
|
216
234
|
/** The access token the `session` grant minted for the session; it carries `azp`. */
|
|
217
235
|
readonly accessToken: string;
|
|
218
236
|
readonly userSessionStore: UserSessionStore;
|
|
219
|
-
readonly
|
|
237
|
+
readonly sessionLifecycle: SessionLifecycle;
|
|
220
238
|
readonly federationTokenStore: FederationTokenStore;
|
|
221
239
|
readonly dispose: () => Promise<void>;
|
|
222
240
|
}
|
|
223
241
|
|
|
224
242
|
/**
|
|
225
243
|
* Boots the deployment, signs alice in with her password, and links Google to
|
|
226
|
-
* her session as a federated sign-in would: the
|
|
227
|
-
*
|
|
244
|
+
* her session as a federated sign-in would: the token store holds `tokens`,
|
|
245
|
+
* and Google joins the session through its lifecycle.
|
|
228
246
|
*/
|
|
229
247
|
export async function signInLinkedToGoogle(options: {
|
|
230
248
|
readonly google: GoogleWiring;
|
|
@@ -232,38 +250,24 @@ export async function signInLinkedToGoogle(options: {
|
|
|
232
250
|
}): Promise<GoogleSession> {
|
|
233
251
|
const { google, tokens } = options;
|
|
234
252
|
const config = resolveConfig(google);
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
? []
|
|
238
|
-
: [
|
|
239
|
-
googleFederationModule,
|
|
240
|
-
defineModule({
|
|
241
|
-
name: "test:google-federation-config",
|
|
242
|
-
provides: {
|
|
243
|
-
googleFederationConfig: (): GoogleProviderConfig => ({
|
|
244
|
-
clientId: GOOGLE_CLIENT_ID,
|
|
245
|
-
clientSecret: GOOGLE_CLIENT_SECRET,
|
|
246
|
-
callbackURL: GOOGLE_CALLBACK,
|
|
247
|
-
...google,
|
|
248
|
-
}),
|
|
249
|
-
},
|
|
250
|
-
}),
|
|
251
|
-
];
|
|
253
|
+
const upstreamFetch = google === "shipped" ? undefined : google.fetch;
|
|
254
|
+
const googleType = googleFederationTypeModule().name;
|
|
252
255
|
const handle = await createApp({
|
|
253
|
-
modules:
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
256
|
+
modules: buildModules(config, {
|
|
257
|
+
keyStoreModule: testKeyStoreModule,
|
|
258
|
+
repositoriesModule: testRepositoriesModule,
|
|
259
|
+
refreshTokenFamilyModules: [memoryRefreshTokenFamilyStoreModule],
|
|
260
|
+
}).map((module) =>
|
|
261
|
+
module.name === googleType && upstreamFetch !== undefined
|
|
262
|
+
? googleFederationTypeModule({ fetch: upstreamFetch })
|
|
263
|
+
: module,
|
|
264
|
+
),
|
|
261
265
|
bootstrapComponents: { config, pathResolver: (s) => s },
|
|
262
266
|
});
|
|
263
267
|
try {
|
|
264
268
|
const components = handle.components as {
|
|
265
269
|
userSessionStore: UserSessionStore;
|
|
266
|
-
|
|
270
|
+
sessionLifecycle: SessionLifecycle;
|
|
267
271
|
federationTokenStore: FederationTokenStore;
|
|
268
272
|
};
|
|
269
273
|
const app = express();
|
|
@@ -292,10 +296,10 @@ export async function signInLinkedToGoogle(options: {
|
|
|
292
296
|
const sid = claims.sid as string;
|
|
293
297
|
expect(typeof sid).toBe("string");
|
|
294
298
|
|
|
295
|
-
const session = await components.userSessionStore.get(sid);
|
|
296
|
-
if (session === null) throw new Error("fixture: the sign-in left no session");
|
|
297
|
-
await components.sessionFederationIndex.addFederation(sid, "google", session.expiresAt);
|
|
298
299
|
await components.federationTokenStore.attach(sid, "google", tokens);
|
|
300
|
+
expect(await components.sessionLifecycle.join(sid, { federation: "google" })).toEqual({
|
|
301
|
+
outcome: "joined",
|
|
302
|
+
});
|
|
299
303
|
return { app, sid, accessToken, ...components, dispose: () => handle.dispose() };
|
|
300
304
|
} catch (error) {
|
|
301
305
|
await handle.dispose();
|
|
@@ -15,17 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* The host's own routes
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* The composed router answers its own errors with core's terminal handler.
|
|
23
|
-
* The host's routes sat in front of a handler of the template's own, which
|
|
24
|
-
* answered in other words ("Internal server error"), took any 4xx `status`
|
|
25
|
-
* for the client's mistake and answered it as one, handed an error on to
|
|
26
|
-
* Express once the headers were out (whose final handler prints the stack),
|
|
27
|
-
* and logged the path as it arrived. Core's handler is now the one after
|
|
28
|
-
* them too, so a deployment answers every route's error the same way.
|
|
18
|
+
* The host's own routes (liveness, readiness, the metrics scrape), mounted as
|
|
19
|
+
* the process mounts them (`mountRoutes`), and what an error one of them lets
|
|
20
|
+
* through is answered with: core's terminal handler, the same one behind the
|
|
21
|
+
* composed router, so every route's error is answered the same way.
|
|
29
22
|
*/
|
|
30
23
|
|
|
31
24
|
import type { Logger, ReadinessProbe } from "@o3co/auth-provider-core";
|
|
@@ -24,6 +24,6 @@ describe("library reference.conf subpath resolution", () => {
|
|
|
24
24
|
const path = fileURLToPath(url);
|
|
25
25
|
expect(existsSync(path)).toBe(true);
|
|
26
26
|
const content = readFileSync(path, "utf-8");
|
|
27
|
-
expect(content).toMatch(
|
|
27
|
+
expect(content).toMatch(/^core\s*\{/m); // sanity: HOCON content, core's own section
|
|
28
28
|
});
|
|
29
29
|
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026 1o1 Co. Ltd.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The libraries' layers beneath the template's own files, for a test that
|
|
19
|
+
* resolves its configuration by hand: the `reference.conf` of every package
|
|
20
|
+
* the template composes a module from, core's last, as `app.mts` layers them;
|
|
21
|
+
* the resolution handed to both phases at once; and what that resolution
|
|
22
|
+
* captures of the renamed variables.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
import {
|
|
27
|
+
type AppConfig,
|
|
28
|
+
memoryRateLimiterModule,
|
|
29
|
+
moduleReferences,
|
|
30
|
+
} from "@o3co/auth-provider-core";
|
|
31
|
+
import { CORE_RELOCATIONS, renamedVariableCaptures } from "@o3co/auth-provider-core/testing";
|
|
32
|
+
import { federationGrantsModules } from "@o3co/auth-provider-federation-grants";
|
|
33
|
+
import {
|
|
34
|
+
oauthAuthorizationGrantsModule,
|
|
35
|
+
oauthEndpointsModule,
|
|
36
|
+
oauthSessionGrantModule,
|
|
37
|
+
} from "@o3co/auth-provider-oauth";
|
|
38
|
+
import {
|
|
39
|
+
redisCodeRepositoryModule,
|
|
40
|
+
redisFederationGrantStoreModule,
|
|
41
|
+
redisRateLimiterModule,
|
|
42
|
+
redisRefreshTokenFamilyStoreModule,
|
|
43
|
+
} from "@o3co/auth-provider-redis";
|
|
44
|
+
import { sessionModule, sessionStoreModule } from "@o3co/auth-provider-session";
|
|
45
|
+
import { type Config, empty, parseFile } from "@o3co/ts.hocon";
|
|
46
|
+
import { readAdapters } from "../adapters.mjs";
|
|
47
|
+
import { readSwitches, type Switches } from "../configPath.mjs";
|
|
48
|
+
import {
|
|
49
|
+
httpModule,
|
|
50
|
+
inMemoryCodeRepositoryModule,
|
|
51
|
+
keyStoreModule,
|
|
52
|
+
loggingModule,
|
|
53
|
+
repositoriesModuleFor,
|
|
54
|
+
standaloneRedisClientsModule,
|
|
55
|
+
templateReference,
|
|
56
|
+
} from "../modules.mjs";
|
|
57
|
+
import { type Adapters, isPlainSection } from "../sections.mjs";
|
|
58
|
+
|
|
59
|
+
/** The oauth package's modules: none reads the configuration it is handed. */
|
|
60
|
+
const OAUTH_MODULES = [
|
|
61
|
+
oauthEndpointsModule,
|
|
62
|
+
oauthSessionGrantModule,
|
|
63
|
+
oauthAuthorizationGrantsModule,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
/** The modules the template composes that declare a renamed variable. */
|
|
67
|
+
const RENAMING_MODULES = [
|
|
68
|
+
memoryRateLimiterModule,
|
|
69
|
+
redisRateLimiterModule,
|
|
70
|
+
redisRefreshTokenFamilyStoreModule,
|
|
71
|
+
redisFederationGrantStoreModule,
|
|
72
|
+
...OAUTH_MODULES,
|
|
73
|
+
sessionModule,
|
|
74
|
+
sessionStoreModule,
|
|
75
|
+
loggingModule,
|
|
76
|
+
httpModule,
|
|
77
|
+
keyStoreModule,
|
|
78
|
+
standaloneRedisClientsModule,
|
|
79
|
+
repositoriesModuleFor({ client: "yaml", user: "yaml" }),
|
|
80
|
+
inMemoryCodeRepositoryModule,
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
/** Every package reference the template's modules declare, core's last. */
|
|
84
|
+
const LIBRARY_REFERENCES: readonly URL[] = moduleReferences([
|
|
85
|
+
redisRateLimiterModule,
|
|
86
|
+
...federationGrantsModules,
|
|
87
|
+
...OAUTH_MODULES,
|
|
88
|
+
sessionModule,
|
|
89
|
+
sessionStoreModule,
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
/** The libraries' references resolved under `env`, in the order `app.mts` layers them. */
|
|
93
|
+
export function libraryLayers(env: Readonly<Record<string, string>>): Config {
|
|
94
|
+
return LIBRARY_REFERENCES.reduce<Config>(
|
|
95
|
+
(layered, reference) =>
|
|
96
|
+
layered.withFallback(parseFile(fileURLToPath(reference), { env: { ...env } })),
|
|
97
|
+
empty(),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Phase one's switches laid over the configuration boot parses. */
|
|
102
|
+
export type BothPhases = Switches & AppConfig;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* `switches` laid over `resolved`, the configuration boot parses, as a test
|
|
106
|
+
* that hands one object to both phases does: the composition root's own keys
|
|
107
|
+
* beside the sections, and `federation-grants.enabled` written as phase one
|
|
108
|
+
* decided it into the section as resolved. Boot is so handed phase one's
|
|
109
|
+
* boolean, not the value as written, unlike `app.mts`; boot's refusal of a
|
|
110
|
+
* written value phase one installs the modules for is pinned in
|
|
111
|
+
* `two-phase-config.test.mts`.
|
|
112
|
+
*/
|
|
113
|
+
export function withSwitches(
|
|
114
|
+
resolved: Readonly<Record<string, unknown>>,
|
|
115
|
+
switches: Switches,
|
|
116
|
+
): BothPhases {
|
|
117
|
+
const grants = resolved["federation-grants"];
|
|
118
|
+
return {
|
|
119
|
+
...resolved,
|
|
120
|
+
...switches,
|
|
121
|
+
"federation-grants": {
|
|
122
|
+
...(isPlainSection(grants) ? grants : {}),
|
|
123
|
+
...switches["federation-grants"],
|
|
124
|
+
},
|
|
125
|
+
} as unknown as BothPhases;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A resolution — `layers`, the template's `config/reference.conf` among them,
|
|
130
|
+
* under `env` — as a test that resolves by hand hands it to both phases at
|
|
131
|
+
* once: the configuration as resolved and unparsed, which boot parses as
|
|
132
|
+
* `app.mts` hands it on, with what phase one reads of it (`readSwitches`)
|
|
133
|
+
* laid over it — the composition root's `adapters` and `mfaMode`, the Store
|
|
134
|
+
* transport settings, and `federation-grants.enabled` as phase one decides
|
|
135
|
+
* it.
|
|
136
|
+
*/
|
|
137
|
+
export function bothPhasesOf(layers: Config, env: Readonly<Record<string, string>>): BothPhases {
|
|
138
|
+
return withSwitches(
|
|
139
|
+
layers.toObject() as Record<string, unknown>,
|
|
140
|
+
readSwitches({ config: layers, env }),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The composition root's `adapters` a resolution holds — `layers`, the
|
|
146
|
+
* template's `config/reference.conf` among them, under `env` — as phase one
|
|
147
|
+
* reads them.
|
|
148
|
+
*/
|
|
149
|
+
export function adaptersOf(layers: Config, env: Readonly<Record<string, string>>): Adapters {
|
|
150
|
+
return readAdapters(layers.toObject() as Record<string, unknown>, env);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The adapters the template ships, as its `config/reference.conf` sets them with no environment. */
|
|
154
|
+
export function shippedAdapters(): Adapters {
|
|
155
|
+
return adaptersOf(parseFile(fileURLToPath(templateReference()), { env: {} }), {});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** The shipped adapters with every store the template can hold in process there: no Redis connection needed. */
|
|
159
|
+
export function inProcessAdapters(): Adapters {
|
|
160
|
+
return {
|
|
161
|
+
...shippedAdapters(),
|
|
162
|
+
rateLimiter: "memory",
|
|
163
|
+
userSessionStores: "memory",
|
|
164
|
+
accessTokenDenylist: "memory",
|
|
165
|
+
replaySeenSet: "memory",
|
|
166
|
+
federationTokenStore: "memory",
|
|
167
|
+
federationGrantStore: "memory",
|
|
168
|
+
federationGrantIntentStore: "memory",
|
|
169
|
+
mfaFactorStore: "memory",
|
|
170
|
+
mfaTransactionStore: "memory",
|
|
171
|
+
codeRepository: "memory",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The `renamed-variables` section a resolution under `env` holds: what every
|
|
177
|
+
* module above captures, and the Redis code repository's, which the
|
|
178
|
+
* in-process one's excludes from one composition (both declare
|
|
179
|
+
* CLIENT_CODE_DEFAULT_EXPIRES_IN renamed, each to its own new name).
|
|
180
|
+
*/
|
|
181
|
+
export function capturedRenames(
|
|
182
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
183
|
+
): Record<string, string | null> {
|
|
184
|
+
return {
|
|
185
|
+
...renamedVariableCaptures({ modules: [redisCodeRepositoryModule], env }),
|
|
186
|
+
...renamedVariableCaptures({ modules: RENAMING_MODULES, core: CORE_RELOCATIONS, env }),
|
|
187
|
+
};
|
|
188
|
+
}
|