@o3co/create-auth-provider 0.17.0 → 0.18.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.md +2 -198
- package/main.js +4 -0
- package/package.json +8 -30
- package/README.ja.md +0 -200
- package/dist/cli.d.mts +0 -3
- package/dist/cli.d.mts.map +0 -1
- package/dist/cli.mjs +0 -18
- package/dist/index.d.mts +0 -48
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -325
- package/dist/internal/template-filter.d.mts +0 -14
- package/dist/internal/template-filter.d.mts.map +0 -1
- package/dist/internal/template-filter.mjs +0 -42
- package/templates/standalone/.dockerignore +0 -23
- package/templates/standalone/.env.example +0 -152
- package/templates/standalone/Dockerfile +0 -125
- package/templates/standalone/Makefile +0 -12
- package/templates/standalone/README.ja.md +0 -912
- package/templates/standalone/README.md +0 -1536
- package/templates/standalone/config/application.conf +0 -351
- package/templates/standalone/config/clients.yaml +0 -4
- package/templates/standalone/config/clients.yaml.example +0 -124
- package/templates/standalone/config/development.conf +0 -17
- package/templates/standalone/config/mailpit.conf +0 -13
- package/templates/standalone/config/production.conf +0 -15
- package/templates/standalone/config/reference.conf +0 -427
- package/templates/standalone/docker-compose.mailpit.yml +0 -39
- package/templates/standalone/docker-compose.production.yml +0 -229
- package/templates/standalone/docker-compose.test.yml +0 -16
- package/templates/standalone/docker-compose.yml +0 -67
- package/templates/standalone/gitignore +0 -23
- package/templates/standalone/package.json +0 -56
- package/templates/standalone/src/__tests__/adapters.test.mts +0 -425
- package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +0 -87
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +0 -1188
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +0 -285
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +0 -1154
- package/templates/standalone/src/__tests__/audit-sink.test.mts +0 -355
- package/templates/standalone/src/__tests__/backchannel-peer.fixture.mts +0 -64
- package/templates/standalone/src/__tests__/config-notices.test.mts +0 -157
- package/templates/standalone/src/__tests__/configPath.test.mts +0 -69
- package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +0 -48
- package/templates/standalone/src/__tests__/core-federations.test.mts +0 -252
- package/templates/standalone/src/__tests__/core-sections.test.mts +0 -192
- package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +0 -95
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +0 -1291
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +0 -787
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +0 -464
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +0 -141
- package/templates/standalone/src/__tests__/federation-types.test.mts +0 -239
- package/templates/standalone/src/__tests__/frontchannel-logout-csp.test.mts +0 -168
- package/templates/standalone/src/__tests__/google-session.fixture.mts +0 -318
- package/templates/standalone/src/__tests__/host-routes.test.mts +0 -134
- package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +0 -29
- package/templates/standalone/src/__tests__/library-references.fixture.mts +0 -188
- package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +0 -217
- package/templates/standalone/src/__tests__/listen.test.mts +0 -121
- package/templates/standalone/src/__tests__/logger-projection.test.mts +0 -77
- package/templates/standalone/src/__tests__/logout-session-cookie.test.mts +0 -113
- package/templates/standalone/src/__tests__/mail-sender.test.mts +0 -134
- package/templates/standalone/src/__tests__/mailpit-compose.test.mts +0 -393
- package/templates/standalone/src/__tests__/metrics.test.mts +0 -253
- package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +0 -122
- package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +0 -127
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +0 -883
- package/templates/standalone/src/__tests__/own-modules.test.mts +0 -1480
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +0 -183
- package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +0 -283
- package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +0 -135
- package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +0 -214
- package/templates/standalone/src/__tests__/redis-stand-in.fixture.mts +0 -98
- package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +0 -249
- package/templates/standalone/src/__tests__/replica-safety.test.mts +0 -657
- package/templates/standalone/src/__tests__/repositories-logger.test.mts +0 -119
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +0 -146
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +0 -610
- package/templates/standalone/src/__tests__/sections.test.mts +0 -516
- package/templates/standalone/src/__tests__/session-lifecycle.test.mts +0 -213
- package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +0 -305
- package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +0 -130
- package/templates/standalone/src/__tests__/session-requirements.test.mts +0 -382
- package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +0 -274
- package/templates/standalone/src/__tests__/shutdown.test.mts +0 -622
- package/templates/standalone/src/__tests__/smoke.test.mts +0 -642
- package/templates/standalone/src/__tests__/startup-log-lines.test.mts +0 -164
- package/templates/standalone/src/__tests__/store-credential.test.mts +0 -314
- package/templates/standalone/src/__tests__/supertest-loopback.test.mts +0 -296
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +0 -265
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +0 -605
- package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +0 -385
- package/templates/standalone/src/adapters.mts +0 -162
- package/templates/standalone/src/app.mts +0 -127
- package/templates/standalone/src/bootRefusal.mts +0 -117
- package/templates/standalone/src/buildModules.mts +0 -358
- package/templates/standalone/src/configPath.mts +0 -487
- package/templates/standalone/src/listen.mts +0 -60
- package/templates/standalone/src/logger.mts +0 -80
- package/templates/standalone/src/metrics.mts +0 -167
- package/templates/standalone/src/mfaSwitch.mts +0 -284
- package/templates/standalone/src/modules.mts +0 -705
- package/templates/standalone/src/rootRenames.mts +0 -95
- package/templates/standalone/src/routes.mts +0 -89
- package/templates/standalone/src/sections.mts +0 -378
- package/templates/standalone/src/securityHeaders.mts +0 -36
- package/templates/standalone/src/shutdown.mts +0 -213
- package/templates/standalone/tests/Makefile +0 -5
- package/templates/standalone/tests/index.test.js +0 -283
- package/templates/standalone/tests/package-lock.json +0 -1617
- package/templates/standalone/tests/package.json +0 -16
- package/templates/standalone/tests/vitest.config.mjs +0 -26
- package/templates/standalone/tsconfig.json +0 -31
- package/templates/standalone/vitest.config.mts +0 -31
- package/templates/standalone/vitest.supertest-loopback.mts +0 -278
- package/templates/versions.json +0 -20
|
@@ -1,48 +0,0 @@
|
|
|
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 MFA switch, `mfaMode`, as this project's own configuration files write
|
|
19
|
-
* it with `MFA_MODE` unset: `{environment}.conf` over `application.conf` over
|
|
20
|
-
* the template's `config/reference.conf`. The template ships it `required`;
|
|
21
|
-
* a project `create-auth-provider --no-mfa` scaffolds writes it `off` at the
|
|
22
|
-
* end of its `application.conf`. A suite whose subject depends on the switch
|
|
23
|
-
* asserts what MFA on (`required`, as the template ships it) or off (as
|
|
24
|
-
* `--no-mfa` writes it) implies, so the suite that ships with a scaffold holds
|
|
25
|
-
* in either project. A project whose files write another mode, such as
|
|
26
|
-
* `optional`, is not what these suites claim to cover.
|
|
27
|
-
*
|
|
28
|
-
* Read with the HOCON parser alone, not through phase one (`readSwitches`),
|
|
29
|
-
* so that a suite about phase one can state what phase one should answer.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import { join } from "node:path";
|
|
33
|
-
import { fileURLToPath } from "node:url";
|
|
34
|
-
import { type Config, empty, parseFile } from "@o3co/ts.hocon";
|
|
35
|
-
import { type MfaSwitch, mfaSwitchSchema } from "#/sections.mjs";
|
|
36
|
-
|
|
37
|
-
const configDir = fileURLToPath(new URL("../../config", import.meta.url));
|
|
38
|
-
|
|
39
|
-
/** `mfaMode` as the files selected by `environment` write it, with no variable set. */
|
|
40
|
-
export function configuredMfaMode(environment: string): MfaSwitch {
|
|
41
|
-
const resolved = [`${environment}.conf`, "application.conf", "reference.conf"]
|
|
42
|
-
.reduce<Config>(
|
|
43
|
-
(layered, file) => layered.withFallback(parseFile(join(configDir, file), { env: {} })),
|
|
44
|
-
empty(),
|
|
45
|
-
)
|
|
46
|
-
.toObject() as Record<string, unknown>;
|
|
47
|
-
return mfaSwitchSchema.parse(resolved.mfaMode);
|
|
48
|
-
}
|
|
@@ -1,252 +0,0 @@
|
|
|
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 federations the template ships, in core's own section:
|
|
19
|
-
* `core.federations.google` and `core.federations.oidc`, each naming its
|
|
20
|
-
* type and bound to the variables named after its paths
|
|
21
|
-
* (`CORE_FEDERATIONS_<NAME>_<KEY>`). The federation types the template
|
|
22
|
-
* bundles are listed whatever the map says (`federation-types.test.mts`
|
|
23
|
-
* boots them). A variable the
|
|
24
|
-
* template bound before, `FEDERATIONS_<NAME>_<KEY>`, set alone or beside its
|
|
25
|
-
* new name, at any value, is refused before any module is chosen, naming the
|
|
26
|
-
* new variable and path and never a value. The map written at the top level refuses boot, naming its
|
|
27
|
-
* paths under `core.federations`.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
31
|
-
import { tmpdir } from "node:os";
|
|
32
|
-
import { join } from "node:path";
|
|
33
|
-
import { fileURLToPath } from "node:url";
|
|
34
|
-
import { BootError, createApp } from "@o3co/auth-provider-core";
|
|
35
|
-
import { afterAll, describe, expect, it } from "vitest";
|
|
36
|
-
import { buildModules } from "#/buildModules.mjs";
|
|
37
|
-
import { readOwnLayers, readSwitches, resolveConfigPaths, resolveForBoot } from "#/configPath.mjs";
|
|
38
|
-
|
|
39
|
-
const configDir = fileURLToPath(new URL("../../config", import.meta.url));
|
|
40
|
-
|
|
41
|
-
/** The directories the operator layers are written to, removed after the suite. */
|
|
42
|
-
const operatorDirs: string[] = [];
|
|
43
|
-
afterAll(() => {
|
|
44
|
-
for (const dir of operatorDirs) rmSync(dir, { recursive: true, force: true });
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
/** The template's own files for the production environment, under an operator's layer when given. */
|
|
48
|
-
function ownFiles(hocon?: string): string[] {
|
|
49
|
-
const { applicationConfPath, envConfPath } = resolveConfigPaths(configDir, "production");
|
|
50
|
-
if (hocon === undefined) return [envConfPath, applicationConfPath];
|
|
51
|
-
const dir = mkdtempSync(join(tmpdir(), "core-federations-"));
|
|
52
|
-
operatorDirs.push(dir);
|
|
53
|
-
const file = join(dir, "operator.conf");
|
|
54
|
-
writeFileSync(file, hocon);
|
|
55
|
-
return [file, envConfPath, applicationConfPath];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** MFA, on by default, switched off: these are about the federations. */
|
|
59
|
-
const MFA_OFF: Readonly<Record<string, string>> = { MFA_MODE: "off" };
|
|
60
|
-
|
|
61
|
-
/** Phase one under `env` and an operator's `hocon`. */
|
|
62
|
-
const switchesFrom = (env: Record<string, string> = {}, hocon?: string) =>
|
|
63
|
-
readSwitches(readOwnLayers(ownFiles(hocon), { env: { ...MFA_OFF, ...env } }));
|
|
64
|
-
|
|
65
|
-
/** What phase one refuses under `env`. */
|
|
66
|
-
function refusal(env: Record<string, string>): Error {
|
|
67
|
-
try {
|
|
68
|
-
switchesFrom(env);
|
|
69
|
-
} catch (err) {
|
|
70
|
-
return err as Error;
|
|
71
|
-
}
|
|
72
|
-
throw new Error("phase one read the switches");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** The federations the composition hands boot under `env`, by name: phase one, then phase two. */
|
|
76
|
-
function federationsOf(env: Record<string, string> = {}) {
|
|
77
|
-
const own = readOwnLayers(ownFiles(), { env: { ...MFA_OFF, ...env } });
|
|
78
|
-
const switches = readSwitches(own);
|
|
79
|
-
const modules = buildModules(switches, { environment: "production" });
|
|
80
|
-
const resolved = resolveForBoot(own, modules, switches) as unknown as {
|
|
81
|
-
readonly core?: { readonly federations?: Record<string, Record<string, unknown>> };
|
|
82
|
-
};
|
|
83
|
-
return resolved.core?.federations ?? {};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** The modules phase one chooses under `env`, by name. */
|
|
87
|
-
const moduleNames = (env: Record<string, string>) =>
|
|
88
|
-
buildModules(switchesFrom(env), { environment: "production" }).map((module) => module.name);
|
|
89
|
-
|
|
90
|
-
/** Each variable the template binds: its key under the federation, the old name and the new. */
|
|
91
|
-
const RENAMED = [
|
|
92
|
-
["google", "enabled", "FEDERATIONS_GOOGLE_ENABLED", "CORE_FEDERATIONS_GOOGLE_ENABLED", "true"],
|
|
93
|
-
[
|
|
94
|
-
"google",
|
|
95
|
-
"clientId",
|
|
96
|
-
"FEDERATIONS_GOOGLE_CLIENT_ID",
|
|
97
|
-
"CORE_FEDERATIONS_GOOGLE_CLIENT_ID",
|
|
98
|
-
"g-id",
|
|
99
|
-
],
|
|
100
|
-
[
|
|
101
|
-
"google",
|
|
102
|
-
"clientSecret",
|
|
103
|
-
"FEDERATIONS_GOOGLE_CLIENT_SECRET",
|
|
104
|
-
"CORE_FEDERATIONS_GOOGLE_CLIENT_SECRET",
|
|
105
|
-
"g-secret",
|
|
106
|
-
],
|
|
107
|
-
[
|
|
108
|
-
"google",
|
|
109
|
-
"callbackURL",
|
|
110
|
-
"FEDERATIONS_GOOGLE_CALLBACK_URL",
|
|
111
|
-
"CORE_FEDERATIONS_GOOGLE_CALLBACK_URL",
|
|
112
|
-
"https://auth.test/session/oauth/federation/google/callback",
|
|
113
|
-
],
|
|
114
|
-
[
|
|
115
|
-
"google",
|
|
116
|
-
"accessType",
|
|
117
|
-
"FEDERATIONS_GOOGLE_ACCESS_TYPE",
|
|
118
|
-
"CORE_FEDERATIONS_GOOGLE_ACCESS_TYPE",
|
|
119
|
-
"online",
|
|
120
|
-
],
|
|
121
|
-
["oidc", "enabled", "FEDERATIONS_OIDC_ENABLED", "CORE_FEDERATIONS_OIDC_ENABLED", "true"],
|
|
122
|
-
["oidc", "issuer", "FEDERATIONS_OIDC_ISSUER", "CORE_FEDERATIONS_OIDC_ISSUER", "https://idp.test"],
|
|
123
|
-
["oidc", "clientId", "FEDERATIONS_OIDC_CLIENT_ID", "CORE_FEDERATIONS_OIDC_CLIENT_ID", "o-id"],
|
|
124
|
-
[
|
|
125
|
-
"oidc",
|
|
126
|
-
"clientSecret",
|
|
127
|
-
"FEDERATIONS_OIDC_CLIENT_SECRET",
|
|
128
|
-
"CORE_FEDERATIONS_OIDC_CLIENT_SECRET",
|
|
129
|
-
"o-secret",
|
|
130
|
-
],
|
|
131
|
-
[
|
|
132
|
-
"oidc",
|
|
133
|
-
"callbackURL",
|
|
134
|
-
"FEDERATIONS_OIDC_CALLBACK_URL",
|
|
135
|
-
"CORE_FEDERATIONS_OIDC_CALLBACK_URL",
|
|
136
|
-
"https://auth.test/session/oauth/federation/oidc/callback",
|
|
137
|
-
],
|
|
138
|
-
] as const;
|
|
139
|
-
|
|
140
|
-
describe("the federations the template ships, under core.federations", () => {
|
|
141
|
-
it("ships google and oidc disabled, each of the type of its name", () => {
|
|
142
|
-
const federations = federationsOf();
|
|
143
|
-
expect(federations.google).toMatchObject({ enabled: false, type: "google" });
|
|
144
|
-
expect(federations.oidc).toMatchObject({ enabled: false, type: "oidc" });
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it.each(RENAMED)(
|
|
148
|
-
"reads core.federations.%s.%s from %s's new name",
|
|
149
|
-
(name, key, _old, variable, value) => {
|
|
150
|
-
expect(String(federationsOf({ [variable]: value })[name]?.[key])).toBe(value);
|
|
151
|
-
},
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
it("lists the same federation modules whether CORE_FEDERATIONS_GOOGLE_ENABLED is true or not", () => {
|
|
155
|
-
const federationModules = (env: Record<string, string>) =>
|
|
156
|
-
moduleNames(env).filter((name) => name === "federation-oidc" || name.endsWith("-type"));
|
|
157
|
-
expect(federationModules({ CORE_FEDERATIONS_GOOGLE_ENABLED: "true" })).toEqual([
|
|
158
|
-
"federation-google-type",
|
|
159
|
-
"federation-oidc",
|
|
160
|
-
]);
|
|
161
|
-
expect(federationModules({})).toEqual(
|
|
162
|
-
federationModules({ CORE_FEDERATIONS_GOOGLE_ENABLED: "true" }),
|
|
163
|
-
);
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe("a variable the template bound before, FEDERATIONS_<NAME>_<KEY>", () => {
|
|
168
|
-
it.each(RENAMED)(
|
|
169
|
-
"core.federations.%s.%s: %s set alone is refused before any module is chosen, naming %s",
|
|
170
|
-
(name, key, old, variable, value) => {
|
|
171
|
-
const err = refusal({ [old]: value });
|
|
172
|
-
expect(err).toBeInstanceOf(BootError);
|
|
173
|
-
expect(err.message).toContain(`${old} was renamed ${variable}`);
|
|
174
|
-
expect(err.message).toContain(`core.federations.${name}.${key}`);
|
|
175
|
-
expect(err).toMatchObject({
|
|
176
|
-
reason: "environment-variable-renamed",
|
|
177
|
-
details: {
|
|
178
|
-
reason: "environment-variable-renamed",
|
|
179
|
-
renamed: [
|
|
180
|
-
{
|
|
181
|
-
module: "core",
|
|
182
|
-
from: old,
|
|
183
|
-
to: variable,
|
|
184
|
-
path: `core.federations.${name}.${key}`,
|
|
185
|
-
state: "unset",
|
|
186
|
-
},
|
|
187
|
-
],
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
},
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
it.each(RENAMED)(
|
|
194
|
-
"core.federations.%s.%s: %s beside its new name at a different value is refused, quoting neither",
|
|
195
|
-
(_name, _key, old, variable) => {
|
|
196
|
-
const err = refusal({ [old]: "old-value-5e2d", [variable]: "new-value-c81a" });
|
|
197
|
-
expect(err).toMatchObject({
|
|
198
|
-
reason: "environment-variable-renamed",
|
|
199
|
-
details: { renamed: [{ from: old, state: "different" }] },
|
|
200
|
-
});
|
|
201
|
-
expect(err.message).toContain(old);
|
|
202
|
-
expect(err.message).not.toContain("old-value-5e2d");
|
|
203
|
-
expect(err.message).not.toContain("new-value-c81a");
|
|
204
|
-
},
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
it.each(RENAMED)(
|
|
208
|
-
"core.federations.%s.%s: %s beside its new name at the same value is refused all the same",
|
|
209
|
-
(_name, _key, old, variable, value) => {
|
|
210
|
-
const err = refusal({ [old]: value, [variable]: value });
|
|
211
|
-
expect(err).toMatchObject({
|
|
212
|
-
reason: "environment-variable-renamed",
|
|
213
|
-
details: { renamed: [{ from: old, state: "different" }] },
|
|
214
|
-
});
|
|
215
|
-
},
|
|
216
|
-
);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
describe("the map written at the top level", () => {
|
|
220
|
-
it("refuses boot, naming its paths under core.federations", async () => {
|
|
221
|
-
const own = readOwnLayers(ownFiles("federations.okta.enabled = false\n"), {
|
|
222
|
-
env: {
|
|
223
|
-
...MFA_OFF,
|
|
224
|
-
KEY_STORE_LOCAL_SECRET: "core-federations-secret.at-least-32-bytes.ok",
|
|
225
|
-
OAUTH_JWT_ISSUER: "https://auth.test",
|
|
226
|
-
SESSION_STORE_SECRET: "core-federations-session.at-least-32-bytes.ok",
|
|
227
|
-
},
|
|
228
|
-
});
|
|
229
|
-
const switches = readSwitches(own);
|
|
230
|
-
const modules = buildModules(switches, { environment: "production" });
|
|
231
|
-
await expect(
|
|
232
|
-
createApp({
|
|
233
|
-
modules,
|
|
234
|
-
bootstrapComponents: {
|
|
235
|
-
config: resolveForBoot(own, modules, switches),
|
|
236
|
-
pathResolver: (s: string) => s,
|
|
237
|
-
} as never,
|
|
238
|
-
}),
|
|
239
|
-
).rejects.toMatchObject({
|
|
240
|
-
reason: "config-path-relocated",
|
|
241
|
-
details: {
|
|
242
|
-
relocated: [
|
|
243
|
-
expect.objectContaining({
|
|
244
|
-
from: "federations.okta.enabled",
|
|
245
|
-
to: "core.federations.okta.enabled",
|
|
246
|
-
environmentVariable: "CORE_FEDERATIONS_OKTA_ENABLED",
|
|
247
|
-
}),
|
|
248
|
-
],
|
|
249
|
-
},
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
});
|
|
@@ -1,192 +0,0 @@
|
|
|
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
|
-
* Core's own settings under `core {}` and the JWKS module's under `jwks {}`,
|
|
19
|
-
* through the template's own reading: its layers read once under one
|
|
20
|
-
* environment, phase one's switches, then the layers over every loaded
|
|
21
|
-
* package's `reference.conf` handed to boot. A path they moved from, written
|
|
22
|
-
* in the operator's own layer, refuses boot naming the new one;
|
|
23
|
-
* `DEPLOYMENT_MODE`, renamed `CORE_DEPLOYMENT_MODE`, refuses boot while it is
|
|
24
|
-
* set, whatever the new name holds.
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { BootError } from "@o3co/auth-provider-core";
|
|
28
|
-
import request from "supertest";
|
|
29
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
30
|
-
import {
|
|
31
|
-
type ComposeOptions,
|
|
32
|
-
type Composition,
|
|
33
|
-
compose,
|
|
34
|
-
ISSUER,
|
|
35
|
-
SINGLE_ENV,
|
|
36
|
-
} from "./all-modules-composition.fixture.mjs";
|
|
37
|
-
|
|
38
|
-
/** The fixture's single-replica environment, with neither name of the replica count set. */
|
|
39
|
-
const BASE: Readonly<Record<string, string>> = Object.fromEntries(
|
|
40
|
-
Object.entries(SINGLE_ENV).filter(
|
|
41
|
-
([name]) => name !== "DEPLOYMENT_MODE" && name !== "CORE_DEPLOYMENT_MODE",
|
|
42
|
-
),
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
let current: Composition | undefined;
|
|
46
|
-
|
|
47
|
-
afterEach(async () => {
|
|
48
|
-
await current?.handle.dispose();
|
|
49
|
-
current = undefined;
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
/** Boots under `env` beside `BASE`, and remembers the composition so `afterEach` disposes it. */
|
|
53
|
-
async function boot(env: Record<string, string>, more: ComposeOptions = {}): Promise<Composition> {
|
|
54
|
-
current = await compose({ env: { ...BASE, ...env }, ...more });
|
|
55
|
-
return current;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** What boot refused with. */
|
|
59
|
-
async function refused(env: Record<string, string>, more: ComposeOptions = {}): Promise<BootError> {
|
|
60
|
-
try {
|
|
61
|
-
await boot(env, more);
|
|
62
|
-
} catch (err) {
|
|
63
|
-
if (err instanceof BootError) return err;
|
|
64
|
-
throw err;
|
|
65
|
-
}
|
|
66
|
-
throw new Error("the composition booted");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
describe("core's own settings and the JWKS module's, as the template reads them", () => {
|
|
70
|
-
it("boots the shipped configuration: core.sessionRequirements.expected is [], the replica count what CORE_DEPLOYMENT_MODE sets", async () => {
|
|
71
|
-
const composition = await boot({ CORE_DEPLOYMENT_MODE: "single" });
|
|
72
|
-
|
|
73
|
-
const config = composition.config as unknown as Record<string, unknown>;
|
|
74
|
-
expect(config.core).toMatchObject({ sessionRequirements: { expected: [] } });
|
|
75
|
-
expect(config).not.toHaveProperty("sessionRequirements");
|
|
76
|
-
expect(config).not.toHaveProperty("deployment");
|
|
77
|
-
expect(composition.handle.components.deploymentMode).toBe("single");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("serves the JWKS at the path JWKS_PATH sets, for the max-age JWKS_CACHE_MAX_AGE sets, and advertises it as jwks_uri", async () => {
|
|
81
|
-
const composition = await boot({
|
|
82
|
-
CORE_DEPLOYMENT_MODE: "single",
|
|
83
|
-
JWKS_PATH: "/keys/jwks.json",
|
|
84
|
-
JWKS_CACHE_MAX_AGE: "600",
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const jwks = await request(composition.app).get("/keys/jwks.json");
|
|
88
|
-
expect(jwks.status).toBe(200);
|
|
89
|
-
expect(jwks.headers["cache-control"]).toBe("public, max-age=600");
|
|
90
|
-
const discovery = await request(composition.app).get("/.well-known/openid-configuration");
|
|
91
|
-
expect(discovery.body.jwks_uri).toBe(`${ISSUER}/keys/jwks.json`);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe("a path core's or the JWKS module's settings moved from, written in the operator's own layer", () => {
|
|
96
|
-
it.each([
|
|
97
|
-
[
|
|
98
|
-
'deployment.mode = "single"',
|
|
99
|
-
"core",
|
|
100
|
-
"deployment.mode",
|
|
101
|
-
"core.deployment.mode",
|
|
102
|
-
"CORE_DEPLOYMENT_MODE",
|
|
103
|
-
],
|
|
104
|
-
[
|
|
105
|
-
"sessionRequirements.expected = []",
|
|
106
|
-
"core",
|
|
107
|
-
"sessionRequirements.expected",
|
|
108
|
-
"core.sessionRequirements.expected",
|
|
109
|
-
undefined,
|
|
110
|
-
],
|
|
111
|
-
[
|
|
112
|
-
'oauth.jwt.jwksPath = "/keys/jwks.json"',
|
|
113
|
-
"jwks",
|
|
114
|
-
"oauth.jwt.jwksPath",
|
|
115
|
-
"jwks.path",
|
|
116
|
-
"JWKS_PATH",
|
|
117
|
-
],
|
|
118
|
-
[
|
|
119
|
-
"oauth.jwt.jwksCacheMaxAge = 60",
|
|
120
|
-
"jwks",
|
|
121
|
-
"oauth.jwt.jwksCacheMaxAge",
|
|
122
|
-
"jwks.cacheMaxAge",
|
|
123
|
-
"JWKS_CACHE_MAX_AGE",
|
|
124
|
-
],
|
|
125
|
-
])(
|
|
126
|
-
"%s: refused, naming %s's new path and the variable that binds it, if any",
|
|
127
|
-
async (hocon, module, from, to, variable) => {
|
|
128
|
-
const err = await refused(
|
|
129
|
-
{ CORE_DEPLOYMENT_MODE: "single" },
|
|
130
|
-
{ operatorHocon: `${hocon}\n` },
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
expect(err.details).toEqual({
|
|
134
|
-
reason: "config-path-relocated",
|
|
135
|
-
relocated: [
|
|
136
|
-
{
|
|
137
|
-
module,
|
|
138
|
-
from,
|
|
139
|
-
to,
|
|
140
|
-
...(variable === undefined ? {} : { environmentVariable: variable }),
|
|
141
|
-
},
|
|
142
|
-
],
|
|
143
|
-
});
|
|
144
|
-
},
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe("DEPLOYMENT_MODE, renamed CORE_DEPLOYMENT_MODE, through the template's reading", () => {
|
|
149
|
-
it("set alone: refused, naming the new variable and core.deployment.mode", async () => {
|
|
150
|
-
const err = await refused({ DEPLOYMENT_MODE: "single" });
|
|
151
|
-
|
|
152
|
-
expect(err.details).toEqual({
|
|
153
|
-
reason: "environment-variable-renamed",
|
|
154
|
-
renamed: [
|
|
155
|
-
{
|
|
156
|
-
module: "core",
|
|
157
|
-
from: "DEPLOYMENT_MODE",
|
|
158
|
-
to: "CORE_DEPLOYMENT_MODE",
|
|
159
|
-
path: "core.deployment.mode",
|
|
160
|
-
state: "unset",
|
|
161
|
-
},
|
|
162
|
-
],
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("set beside the new name at a different value: refused, naming neither value", async () => {
|
|
167
|
-
const err = await refused({
|
|
168
|
-
DEPLOYMENT_MODE: "old-mode-5e2d",
|
|
169
|
-
CORE_DEPLOYMENT_MODE: "new-mode-c81a",
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
expect(err.details).toMatchObject({
|
|
173
|
-
renamed: [{ from: "DEPLOYMENT_MODE", to: "CORE_DEPLOYMENT_MODE", state: "different" }],
|
|
174
|
-
});
|
|
175
|
-
expect(err.message).not.toContain("old-mode-5e2d");
|
|
176
|
-
expect(err.message).not.toContain("new-mode-c81a");
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it("set beside the new name at the same value: refused all the same", async () => {
|
|
180
|
-
const err = await refused({ DEPLOYMENT_MODE: "single", CORE_DEPLOYMENT_MODE: "single" });
|
|
181
|
-
|
|
182
|
-
expect(err.details).toMatchObject({
|
|
183
|
-
renamed: [{ from: "DEPLOYMENT_MODE", to: "CORE_DEPLOYMENT_MODE", state: "different" }],
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("unset, with the new name set: boots with its mode", async () => {
|
|
188
|
-
const composition = await boot({ CORE_DEPLOYMENT_MODE: "single" });
|
|
189
|
-
|
|
190
|
-
expect(composition.handle.components.deploymentMode).toBe("single");
|
|
191
|
-
});
|
|
192
|
-
});
|
|
@@ -1,95 +0,0 @@
|
|
|
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
|
-
* `standaloneRedisClientsModule` provides `deviceCodeStoreClient`.
|
|
19
|
-
*
|
|
20
|
-
* The template does not mount the device grant, so nothing in `buildModules`
|
|
21
|
-
* selects `redisDeviceCodeStoreModule` and the smoke test's client-slot
|
|
22
|
-
* invariant cannot see it. This pins the slot directly: a deployment that adds
|
|
23
|
-
* `deviceAuthorizationGrantModule` to this manifest and picks the Redis store must not hit
|
|
24
|
-
* `missing-required-component` for a client slot the shared clients module
|
|
25
|
-
* does not provide.
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
import type { LifecycleRegistrar } from "@o3co/auth-provider-core";
|
|
29
|
-
import { redisDeviceCodeStoreModule } from "@o3co/auth-provider-redis";
|
|
30
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
31
|
-
|
|
32
|
-
const redisCtorCalls: Array<{ url: string }> = [];
|
|
33
|
-
|
|
34
|
-
vi.mock("ioredis", () => {
|
|
35
|
-
class MockRedis {
|
|
36
|
-
on = vi.fn();
|
|
37
|
-
quit = vi.fn(async () => "OK" as const);
|
|
38
|
-
ping = vi.fn(async () => "PONG" as const);
|
|
39
|
-
eval = vi.fn();
|
|
40
|
-
evalsha = vi.fn();
|
|
41
|
-
duplicate(): MockRedis {
|
|
42
|
-
return new MockRedis("redis://duplicate.local");
|
|
43
|
-
}
|
|
44
|
-
constructor(url: string) {
|
|
45
|
-
redisCtorCalls.push({ url });
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return { Redis: MockRedis, default: MockRedis };
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const importModule = async () => await import("../modules.mjs");
|
|
52
|
-
|
|
53
|
-
/** The module's own section, `redis-clients`, as boot hands it over. */
|
|
54
|
-
const baseSection = { url: "redis://example.com:6379" };
|
|
55
|
-
|
|
56
|
-
describe("standaloneRedisClientsModule.deviceCodeStoreClient", () => {
|
|
57
|
-
beforeEach(() => {
|
|
58
|
-
redisCtorCalls.length = 0;
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
afterEach(() => {
|
|
62
|
-
vi.restoreAllMocks();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("provides every *Client slot redisDeviceCodeStoreModule requires", async () => {
|
|
66
|
-
const { standaloneRedisClientsModule } = await importModule();
|
|
67
|
-
const provided = new Set(Object.keys(standaloneRedisClientsModule.provides ?? {}));
|
|
68
|
-
const required = [...(redisDeviceCodeStoreModule.requires ?? [])].filter((key) =>
|
|
69
|
-
key.endsWith("Client"),
|
|
70
|
-
);
|
|
71
|
-
expect(required).toEqual(["deviceCodeStoreClient"]);
|
|
72
|
-
expect(required.filter((key) => !provided.has(key))).toEqual([]);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("resolves the client off the one shared socket, not a second connection", async () => {
|
|
76
|
-
const { standaloneRedisClientsModule } = await importModule();
|
|
77
|
-
const provides = (
|
|
78
|
-
standaloneRedisClientsModule as unknown as {
|
|
79
|
-
provides: Record<string, (deps: Record<string, unknown>) => Promise<unknown>>;
|
|
80
|
-
}
|
|
81
|
-
).provides;
|
|
82
|
-
const lifecycleRegistrar: LifecycleRegistrar = { register: () => {} };
|
|
83
|
-
|
|
84
|
-
const client = (await provides.deviceCodeStoreClient?.({
|
|
85
|
-
section: { ...baseSection },
|
|
86
|
-
lifecycleRegistrar,
|
|
87
|
-
})) as Record<string, unknown>;
|
|
88
|
-
await provides.rateLimiterClient?.({ section: { ...baseSection }, lifecycleRegistrar });
|
|
89
|
-
|
|
90
|
-
expect(redisCtorCalls).toHaveLength(1);
|
|
91
|
-
for (const method of ["create", "findPending", "decide", "poll", "remove"]) {
|
|
92
|
-
expect(typeof client[method], `deviceCodeStoreClient.${method}`).toBe("function");
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
});
|