@o3co/create-auth-provider 0.16.0 → 0.17.0-rc.1
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 -26
- package/templates/standalone/Dockerfile +6 -2
- package/templates/standalone/README.ja.md +293 -162
- package/templates/standalone/README.md +533 -199
- package/templates/standalone/config/application.conf +196 -341
- 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 +399 -0
- package/templates/standalone/docker-compose.mailpit.yml +36 -0
- package/templates/standalone/docker-compose.production.yml +58 -30
- 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__/access-token-lifetime-alias.test.mts +19 -20
- package/templates/standalone/src/__tests__/adapters.test.mts +358 -0
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +216 -141
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +48 -33
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +77 -58
- package/templates/standalone/src/__tests__/audit-sink.test.mts +78 -76
- package/templates/standalone/src/__tests__/configPath.test.mts +13 -11
- package/templates/standalone/src/__tests__/core-federations.test.mts +226 -0
- package/templates/standalone/src/__tests__/core-sections.test.mts +190 -0
- package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +9 -14
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +656 -269
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +425 -84
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +91 -70
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +7 -5
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +22 -19
- package/templates/standalone/src/__tests__/google-session.fixture.mts +53 -42
- package/templates/standalone/src/__tests__/host-routes.test.mts +4 -11
- package/templates/standalone/src/__tests__/library-references.fixture.mts +183 -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 +328 -0
- package/templates/standalone/src/__tests__/metrics.test.mts +4 -9
- package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +123 -0
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +780 -0
- package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +52 -46
- package/templates/standalone/src/__tests__/own-modules.test.mts +1457 -0
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +7 -11
- 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 +112 -81
- package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +25 -31
- package/templates/standalone/src/__tests__/replica-safety.test.mts +153 -142
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +4 -7
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +275 -95
- package/templates/standalone/src/__tests__/sections.test.mts +480 -0
- package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +67 -55
- package/templates/standalone/src/__tests__/session-requirements.test.mts +299 -0
- package/templates/standalone/src/__tests__/shutdown.test.mts +163 -96
- package/templates/standalone/src/__tests__/smoke.test.mts +179 -252
- package/templates/standalone/src/__tests__/store-credential.test.mts +91 -50
- package/templates/standalone/src/__tests__/supertest-loopback.test.mts +182 -11
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +108 -95
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +438 -0
- package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +390 -0
- package/templates/standalone/src/adapters.mts +149 -0
- package/templates/standalone/src/app.mts +64 -79
- package/templates/standalone/src/buildModules.mts +212 -251
- package/templates/standalone/src/configPath.mts +422 -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 +262 -0
- package/templates/standalone/src/modules.mts +458 -476
- package/templates/standalone/src/rootRenames.mts +79 -0
- package/templates/standalone/src/routes.mts +8 -14
- package/templates/standalone/src/sections.mts +366 -0
- package/templates/standalone/src/shutdown.mts +45 -123
- package/templates/standalone/tests/index.test.js +2 -2
- 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
|
@@ -15,94 +15,116 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
-
import { type AppConfig, AppConfigSchema } from "@o3co/auth-provider-core";
|
|
18
|
+
import { type AppConfig, AppConfigSchema, coreReference } from "@o3co/auth-provider-core";
|
|
19
19
|
import { parseFile } from "@o3co/ts.hocon";
|
|
20
20
|
import { validate } from "@o3co/ts.hocon/zod";
|
|
21
21
|
import { describe, expect, it } from "vitest";
|
|
22
|
+
import { readAdapters } from "../adapters.mjs";
|
|
22
23
|
import { buildModules } from "../buildModules.mjs";
|
|
23
|
-
import { resolveConfigPaths,
|
|
24
|
+
import { resolveConfigPaths, type Switches } from "../configPath.mjs";
|
|
25
|
+
import { readMfaSwitch } from "../mfaSwitch.mjs";
|
|
26
|
+
import { templateReference } from "../modules.mjs";
|
|
24
27
|
|
|
25
28
|
// config/ is two levels above this test file:
|
|
26
29
|
// src/__tests__/ → src/ → standalone/ → config/
|
|
27
30
|
const configDir = fileURLToPath(new URL("../../config", import.meta.url));
|
|
28
31
|
|
|
29
32
|
// Provide required secrets so AppConfigSchema parse succeeds. These are
|
|
30
|
-
// test-only values — no real keys are embedded here.
|
|
33
|
+
// test-only values — no real keys are embedded here. SESSION_STORE_SECRET
|
|
31
34
|
// carries a 256-bit entropy floor, so these clear it (the '.' characters keep
|
|
32
35
|
// them outside the base64 alphabet, so the UTF-8 length is what counts).
|
|
33
36
|
const testEnv = {
|
|
34
|
-
|
|
37
|
+
KEY_STORE_LOCAL_SECRET: "test-secret-three-tier.at-least-32-bytes.ok",
|
|
35
38
|
OAUTH_JWT_ISSUER: "https://auth.test",
|
|
36
|
-
|
|
39
|
+
SESSION_STORE_SECRET: "test-session-secret-three-tier.at-least-32-bytes.ok",
|
|
37
40
|
};
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The three tiers under `env`, parsed with core's schema, beside the
|
|
44
|
+
* composition root's `adapters` and `mfaMode` phase one reads from the
|
|
45
|
+
* template's own layers.
|
|
46
|
+
*/
|
|
47
|
+
function buildResolvedConfig(env: string, extraEnv: Record<string, string> = {}): Switches {
|
|
40
48
|
const { applicationConfPath, envConfPath } = resolveConfigPaths(configDir, env);
|
|
41
|
-
const libraryReferencePath =
|
|
49
|
+
const libraryReferencePath = fileURLToPath(coreReference());
|
|
42
50
|
const resolvedEnv = { ...testEnv, ...extraEnv };
|
|
43
|
-
|
|
44
|
-
parseFile(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
const own = parseFile(envConfPath, { env: resolvedEnv })
|
|
52
|
+
.withFallback(parseFile(applicationConfPath, { env: resolvedEnv }))
|
|
53
|
+
.withFallback(parseFile(fileURLToPath(templateReference()), { env: resolvedEnv }));
|
|
54
|
+
return {
|
|
55
|
+
...validate(
|
|
56
|
+
own.withFallback(parseFile(libraryReferencePath, { env: resolvedEnv })),
|
|
57
|
+
AppConfigSchema,
|
|
58
|
+
),
|
|
59
|
+
adapters: readAdapters(own.toObject() as Record<string, unknown>, resolvedEnv),
|
|
60
|
+
mfaMode: readMfaSwitch(own.toObject() as Record<string, unknown>, resolvedEnv),
|
|
61
|
+
storeTransport: undefined,
|
|
62
|
+
};
|
|
49
63
|
}
|
|
50
64
|
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// shape (string from env substitution, boolean from literal), so the cast
|
|
55
|
-
// is intentional and well-bounded.
|
|
65
|
+
// The oauth-authorization module's section, which core mirrors for the one
|
|
66
|
+
// key read before the modules are chosen (`grants`), kept as written: the
|
|
67
|
+
// runtime shape is a string from env substitution, a boolean from a literal.
|
|
56
68
|
type GrantEntry = { enabled?: unknown };
|
|
57
69
|
const grants = (config: AppConfig) =>
|
|
58
|
-
(config
|
|
70
|
+
((config["oauth-authorization"] as { grants?: unknown } | undefined)?.grants as
|
|
71
|
+
| Record<string, GrantEntry | undefined>
|
|
72
|
+
| undefined) ?? {};
|
|
59
73
|
|
|
60
74
|
describe("three-tier HOCON resolution (env → application.conf → reference.conf)", () => {
|
|
61
75
|
it("template application.conf wins over reference.conf for grant.enabled", () => {
|
|
62
76
|
const config = buildResolvedConfig("development");
|
|
63
|
-
// Template's application.conf sets
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
expect(grants(config).authorization_code?.enabled).toBe(true);
|
|
77
|
+
// Template's application.conf sets authorizationCode.enabled = true,
|
|
78
|
+
// so the resolved value is true whatever reference.conf says.
|
|
79
|
+
expect(grants(config).authorizationCode?.enabled).toBe(true);
|
|
67
80
|
});
|
|
68
81
|
|
|
69
82
|
it("reference.conf default reaches resolved config when template omits the key", () => {
|
|
70
83
|
const config = buildResolvedConfig("development");
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
expect(config.oauth.
|
|
84
|
+
// nonce.maxLength is library-owned in both layers (template doesn't
|
|
85
|
+
// override it). Verifies precedence falls through.
|
|
86
|
+
expect(config.oauth.nonce?.maxLength).toBe(256);
|
|
74
87
|
});
|
|
75
88
|
|
|
76
89
|
it("env var at template layer can disable a template-enabled grant (precedence: env-override line must be repeated)", () => {
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// Note: ts.hocon substitutes env vars as strings, and `oauth.grants` is
|
|
83
|
-
// `z.object({}).passthrough()` — an open tree each grant module's own
|
|
84
|
-
// `configSchema` validates — so no shape-walking coercion reaches this
|
|
85
|
-
// leaf and the string survives to the resolved config. #288 unified the
|
|
86
|
-
// coercion for every DECLARED boolean in the schema and deliberately
|
|
87
|
-
// left this one alone: every consumer of a grant's `enabled`
|
|
88
|
-
// (`GrantRegistry.isEnabled`, `oauthAuthorization`, `oauthSession`)
|
|
89
|
-
// already reads `true` and `"true"` as enabled and everything else,
|
|
90
|
-
// `"false"` included, as not-enabled.
|
|
90
|
+
// The env-override line is repeated at the template layer alongside
|
|
91
|
+
// `enabled = true`; without it, the package reference's substitution is
|
|
92
|
+
// shadowed by the template's literal `true`. The value stays a string:
|
|
93
|
+
// core mirrors the section as written, and the module reads the switch
|
|
94
|
+
// with its own schema, `"false"` as off.
|
|
91
95
|
const config = buildResolvedConfig("development", {
|
|
92
|
-
|
|
96
|
+
OAUTH_AUTHORIZATION_GRANTS_AUTHORIZATION_CODE_ENABLED: "false",
|
|
93
97
|
});
|
|
94
|
-
expect(grants(config).
|
|
98
|
+
expect(grants(config).authorizationCode?.enabled).toBe("false");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("reads the MFA switch, mfaMode, as required where MFA_MODE is unset, and installs no MFA module under MFA_MODE=off", () => {
|
|
102
|
+
// The template's reference.conf ships the switch required and binds MFA_MODE.
|
|
103
|
+
const mfaNames = (config: Switches) =>
|
|
104
|
+
buildModules(config)
|
|
105
|
+
.map((m) => m.name)
|
|
106
|
+
.filter((name) => /mfa/i.test(name));
|
|
107
|
+
const shipped = buildResolvedConfig("development");
|
|
108
|
+
expect(shipped.mfaMode).toBe("required");
|
|
109
|
+
expect(mfaNames(shipped)).toContain("mfa");
|
|
110
|
+
const off = buildResolvedConfig("development", { MFA_MODE: "off" });
|
|
111
|
+
expect(off.mfaMode).toBe("off");
|
|
112
|
+
expect(mfaNames(off)).toEqual([]);
|
|
113
|
+
const optional = buildResolvedConfig("development", { MFA_MODE: "optional" });
|
|
114
|
+
expect(optional.mfaMode).toBe("optional");
|
|
115
|
+
expect(mfaNames(optional)).toContain("mfa");
|
|
95
116
|
});
|
|
96
117
|
|
|
97
|
-
it("reference.conf
|
|
118
|
+
it("core's reference.conf ships no rateLimit.failMode: it is the Redis limiter's own key", () => {
|
|
98
119
|
const config = buildResolvedConfig("development");
|
|
99
|
-
expect(config
|
|
120
|
+
expect(config).not.toHaveProperty("rateLimit.failMode");
|
|
100
121
|
});
|
|
101
122
|
|
|
102
|
-
it("
|
|
103
|
-
//
|
|
123
|
+
it("template application.conf ships clientCredentials.enabled off", () => {
|
|
124
|
+
// The template writes every grant switch it reads before boot, so the
|
|
125
|
+
// value phase one reads is the one boot parses.
|
|
104
126
|
const config = buildResolvedConfig("development");
|
|
105
|
-
expect(grants(config).
|
|
127
|
+
expect(grants(config).clientCredentials?.enabled).toBe(false);
|
|
106
128
|
});
|
|
107
129
|
|
|
108
130
|
it("reference.conf default for oauth.resourceIndicator.enabled is false", () => {
|
|
@@ -124,17 +146,13 @@ describe("three-tier HOCON resolution (env → application.conf → reference.co
|
|
|
124
146
|
expect(config.oauth.resourceIndicator?.enabled).toBe(true);
|
|
125
147
|
});
|
|
126
148
|
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// looking effective. These assertions run the real three-tier merge, so they
|
|
133
|
-
// fail if either layer stops carrying the key.
|
|
134
|
-
describe("#287: the audit sink as the shipped artifact resolves it", () => {
|
|
135
|
-
it("resolves audit.sink.type to the template's logger sink with nothing set", () => {
|
|
149
|
+
// The selection is the composition root's own, `adapters.auditSink`,
|
|
150
|
+
// which phase one reads over the template's own layers. These assertions
|
|
151
|
+
// run the real merge, so they fail if a layer stops carrying the key.
|
|
152
|
+
describe("the audit sink as the shipped artifact resolves it", () => {
|
|
153
|
+
it("resolves adapters.auditSink to the template's logger sink with nothing set", () => {
|
|
136
154
|
const config = buildResolvedConfig("production");
|
|
137
|
-
expect(config.
|
|
155
|
+
expect(config.adapters.auditSink).toBe("logger");
|
|
138
156
|
});
|
|
139
157
|
|
|
140
158
|
it("wires exactly one auditSink provider for that resolved config", () => {
|
|
@@ -144,36 +162,30 @@ describe("three-tier HOCON resolution (env → application.conf → reference.co
|
|
|
144
162
|
});
|
|
145
163
|
|
|
146
164
|
it("lets an operator select core's built-in console sink by env var", () => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// same precedence rule the grant tests above pin.
|
|
150
|
-
const config = buildResolvedConfig("production", { AUDIT_SINK_TYPE: "console" });
|
|
151
|
-
expect(config.audit?.sink.type).toBe("console");
|
|
165
|
+
const config = buildResolvedConfig("production", { ADAPTERS_AUDIT_SINK: "console" });
|
|
166
|
+
expect(config.adapters.auditSink).toBe("console");
|
|
152
167
|
});
|
|
153
168
|
|
|
154
|
-
it("reference.conf
|
|
155
|
-
// A
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
AppConfigSchema,
|
|
169
|
+
it("the template's reference.conf alone selects a sink, not a drop", () => {
|
|
170
|
+
// A composition resolved against the template's reference alone
|
|
171
|
+
// still lands on a sink: an application.conf that forgets the
|
|
172
|
+
// selection must not thereby lose its audit trail.
|
|
173
|
+
const referenceOnly = parseFile(fileURLToPath(templateReference()), { env: {} });
|
|
174
|
+
expect(readAdapters(referenceOnly.toObject() as Record<string, unknown>, {}).auditSink).toBe(
|
|
175
|
+
"logger",
|
|
162
176
|
);
|
|
163
|
-
expect(referenceOnly.audit?.sink.type).toBe("console");
|
|
164
177
|
});
|
|
165
178
|
});
|
|
166
179
|
|
|
167
|
-
//
|
|
168
|
-
// application.conf, `
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
it("resolves accessTokenDenylist.adapter to redis with nothing set", () => {
|
|
180
|
+
// The shape the umbrella E2E (o3co/auth) boots: the shipped
|
|
181
|
+
// application.conf, `CORE_DEPLOYMENT_MODE=multi`, and one shared ioredis socket.
|
|
182
|
+
// A memory denylist under `multi` is refused by the replica-safety guard,
|
|
183
|
+
// and NO denylist by core's denylist boot guard, so the template has to
|
|
184
|
+
// land on "redis" without the deployment naming it.
|
|
185
|
+
describe("access-token revocation as the shipped artifact resolves it", () => {
|
|
186
|
+
it("resolves adapters.accessTokenDenylist to redis with nothing set", () => {
|
|
175
187
|
const config = buildResolvedConfig("production");
|
|
176
|
-
expect(config.accessTokenDenylist
|
|
188
|
+
expect(config.adapters.accessTokenDenylist).toBe("redis");
|
|
177
189
|
});
|
|
178
190
|
|
|
179
191
|
it("resolves oauth.revocation.accessToken to denylist with nothing set", () => {
|
|
@@ -189,22 +201,23 @@ describe("three-tier HOCON resolution (env → application.conf → reference.co
|
|
|
189
201
|
|
|
190
202
|
it("leaves nothing replica-unsafe under the umbrella E2E's environment", async () => {
|
|
191
203
|
// The environment `o3co/auth`'s tests/docker-compose.yml sets. Under
|
|
192
|
-
// `
|
|
193
|
-
// every in-memory shared store, so the denylist
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
// — which no name list in core covers — count too.
|
|
204
|
+
// `CORE_DEPLOYMENT_MODE=multi` the replica-safety guard fails boot naming
|
|
205
|
+
// every in-memory shared store, so the denylist has to come out as
|
|
206
|
+
// the Redis one from the template's own config: the compose file
|
|
207
|
+
// names no denylist variable. Asked of each manifest, as the guard
|
|
208
|
+
// does, so a rename cannot quietly invalidate it and the template's
|
|
209
|
+
// own memory modules count too.
|
|
199
210
|
const { replicaUnsafeReason } = await import("@o3co/auth-provider-core");
|
|
200
211
|
const config = buildResolvedConfig("production", {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
|
|
212
|
+
ADAPTERS_USER_SESSION_STORES: "redis",
|
|
213
|
+
ADAPTERS_RATE_LIMITER: "redis",
|
|
214
|
+
ADAPTERS_CODE_REPOSITORY: "redis",
|
|
215
|
+
// The federation token store defaults to memory, and the memory
|
|
216
|
+
// module declares itself replica-unsafe.
|
|
217
|
+
ADAPTERS_FEDERATION_TOKEN_STORE: "redis",
|
|
207
218
|
REDIS_FEDERATION_TOKEN_STORE_ENCRYPTION_KEY: "BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=",
|
|
219
|
+
// The umbrella sets MFA off.
|
|
220
|
+
MFA_MODE: "off",
|
|
208
221
|
});
|
|
209
222
|
const modules = buildModules(config);
|
|
210
223
|
expect(modules.map((m) => m.name)).toContain("redis-access-token-denylist");
|
|
@@ -215,9 +228,9 @@ describe("three-tier HOCON resolution (env → application.conf → reference.co
|
|
|
215
228
|
|
|
216
229
|
it("lets a single-instance deployment opt down to memory by env var", () => {
|
|
217
230
|
const config = buildResolvedConfig("production", {
|
|
218
|
-
|
|
231
|
+
ADAPTERS_ACCESS_TOKEN_DENYLIST: "memory",
|
|
219
232
|
});
|
|
220
|
-
expect(config.accessTokenDenylist
|
|
233
|
+
expect(config.adapters.accessTokenDenylist).toBe("memory");
|
|
221
234
|
const names = buildModules(config).map((m) => m.name);
|
|
222
235
|
expect(names).toContain("core-access-token-denylist-memory");
|
|
223
236
|
});
|