@o3co/create-auth-provider 0.15.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.
Files changed (92) hide show
  1. package/README.ja.md +139 -48
  2. package/README.md +138 -47
  3. package/dist/index.d.mts +33 -10
  4. package/dist/index.d.mts.map +1 -1
  5. package/dist/index.mjs +118 -59
  6. package/dist/internal/template-filter.d.mts +7 -10
  7. package/dist/internal/template-filter.d.mts.map +1 -1
  8. package/dist/internal/template-filter.mjs +7 -10
  9. package/package.json +6 -4
  10. package/templates/standalone/.dockerignore +15 -2
  11. package/templates/standalone/.env.example +69 -24
  12. package/templates/standalone/Dockerfile +7 -3
  13. package/templates/standalone/README.ja.md +677 -184
  14. package/templates/standalone/README.md +699 -238
  15. package/templates/standalone/config/application.conf +203 -325
  16. package/templates/standalone/config/clients.yaml +2 -0
  17. package/templates/standalone/config/clients.yaml.example +4 -0
  18. package/templates/standalone/config/development.conf +13 -4
  19. package/templates/standalone/config/mailpit.conf +8 -0
  20. package/templates/standalone/config/production.conf +5 -6
  21. package/templates/standalone/config/reference.conf +399 -0
  22. package/templates/standalone/docker-compose.mailpit.yml +36 -0
  23. package/templates/standalone/docker-compose.production.yml +58 -30
  24. package/templates/standalone/docker-compose.yml +13 -3
  25. package/templates/standalone/gitignore +4 -2
  26. package/templates/standalone/package.json +9 -6
  27. package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +65 -32
  28. package/templates/standalone/src/__tests__/adapters.test.mts +358 -0
  29. package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +1178 -0
  30. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +256 -0
  31. package/templates/standalone/src/__tests__/all-modules-composition.test.mts +1119 -0
  32. package/templates/standalone/src/__tests__/audit-sink.test.mts +78 -76
  33. package/templates/standalone/src/__tests__/configPath.test.mts +13 -11
  34. package/templates/standalone/src/__tests__/core-federations.test.mts +226 -0
  35. package/templates/standalone/src/__tests__/core-sections.test.mts +190 -0
  36. package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +9 -14
  37. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +672 -271
  38. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +480 -78
  39. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +102 -66
  40. package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +135 -0
  41. package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +49 -18
  42. package/templates/standalone/src/__tests__/google-session.fixture.mts +315 -0
  43. package/templates/standalone/src/__tests__/host-routes.test.mts +134 -0
  44. package/templates/standalone/src/__tests__/library-references.fixture.mts +183 -0
  45. package/templates/standalone/src/__tests__/listen.test.mts +121 -0
  46. package/templates/standalone/src/__tests__/logger-projection.test.mts +77 -0
  47. package/templates/standalone/src/__tests__/mail-sender.test.mts +134 -0
  48. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +328 -0
  49. package/templates/standalone/src/__tests__/metrics.test.mts +17 -7
  50. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +123 -0
  51. package/templates/standalone/src/__tests__/mfa-switch.test.mts +780 -0
  52. package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +52 -46
  53. package/templates/standalone/src/__tests__/own-modules.test.mts +1457 -0
  54. package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +183 -0
  55. package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +135 -0
  56. package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +112 -81
  57. package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +25 -31
  58. package/templates/standalone/src/__tests__/replica-safety.test.mts +166 -145
  59. package/templates/standalone/src/__tests__/router-error-answers.test.mts +146 -0
  60. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +275 -95
  61. package/templates/standalone/src/__tests__/sections.test.mts +480 -0
  62. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +67 -55
  63. package/templates/standalone/src/__tests__/session-requirements.test.mts +299 -0
  64. package/templates/standalone/src/__tests__/shutdown.test.mts +319 -115
  65. package/templates/standalone/src/__tests__/smoke.test.mts +179 -233
  66. package/templates/standalone/src/__tests__/store-credential.test.mts +317 -0
  67. package/templates/standalone/src/__tests__/supertest-loopback.test.mts +182 -11
  68. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +108 -95
  69. package/templates/standalone/src/__tests__/two-phase-config.test.mts +438 -0
  70. package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +390 -0
  71. package/templates/standalone/src/adapters.mts +149 -0
  72. package/templates/standalone/src/app.mts +71 -115
  73. package/templates/standalone/src/buildModules.mts +229 -251
  74. package/templates/standalone/src/configPath.mts +422 -29
  75. package/templates/standalone/src/listen.mts +60 -0
  76. package/templates/standalone/src/logger.mts +31 -57
  77. package/templates/standalone/src/metrics.mts +24 -47
  78. package/templates/standalone/src/mfaSwitch.mts +262 -0
  79. package/templates/standalone/src/modules.mts +480 -464
  80. package/templates/standalone/src/rootRenames.mts +79 -0
  81. package/templates/standalone/src/routes.mts +89 -0
  82. package/templates/standalone/src/sections.mts +366 -0
  83. package/templates/standalone/src/shutdown.mts +54 -126
  84. package/templates/standalone/tests/index.test.js +2 -2
  85. package/templates/standalone/tests/package-lock.json +1615 -2223
  86. package/templates/standalone/tests/package.json +2 -2
  87. package/templates/standalone/tests/vitest.config.mjs +26 -0
  88. package/templates/standalone/vitest.config.mts +8 -7
  89. package/templates/standalone/vitest.supertest-loopback.mts +97 -69
  90. package/templates/versions.json +18 -15
  91. package/templates/standalone/src/__tests__/terminal-error.test.mts +0 -96
  92. package/templates/standalone/src/terminalError.mts +0 -60
@@ -0,0 +1,190 @@
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 unless the
24
+ * new name carries the same value.
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: boots with that mode", async () => {
180
+ const composition = await boot({ DEPLOYMENT_MODE: "single", CORE_DEPLOYMENT_MODE: "single" });
181
+
182
+ expect(composition.handle.components.deploymentMode).toBe("single");
183
+ });
184
+
185
+ it("unset, with the new name set: boots with its mode", async () => {
186
+ const composition = await boot({ CORE_DEPLOYMENT_MODE: "single" });
187
+
188
+ expect(composition.handle.components.deploymentMode).toBe("single");
189
+ });
190
+ });
@@ -15,16 +15,14 @@
15
15
  */
16
16
 
17
17
  /**
18
- * `standaloneRedisClientsModule` provides `deviceCodeStoreClient` (#433).
18
+ * `standaloneRedisClientsModule` provides `deviceCodeStoreClient`.
19
19
  *
20
20
  * The template does not mount the device grant, so nothing in `buildModules`
21
- * selects `redisDeviceCodeStoreModule` today and the smoke test's client-slot
21
+ * selects `redisDeviceCodeStoreModule` and the smoke test's client-slot
22
22
  * invariant cannot see it. This pins the slot directly: a deployment that adds
23
23
  * `deviceGrantModule` to this manifest and picks the Redis store must not hit
24
- * the `missing-required-component` boot failure that #439 already paid for
25
- * once with the subject-level slots — a Redis-branch module whose required
26
- * client slot the shared clients module did not provide, caught only by the
27
- * umbrella e2e.
24
+ * `missing-required-component` for a client slot the shared clients module
25
+ * does not provide.
28
26
  */
29
27
 
30
28
  import type { LifecycleRegistrar } from "@o3co/auth-provider-core";
@@ -52,13 +50,10 @@ vi.mock("ioredis", () => {
52
50
 
53
51
  const importModule = async () => await import("../modules.mjs");
54
52
 
55
- const baseConfig = {
56
- refreshTokenFamilyStore: {
57
- redis: { url: "redis://example.com:6379" },
58
- },
59
- };
53
+ /** The module's own section, `redis-clients`, as boot hands it over. */
54
+ const baseSection = { url: "redis://example.com:6379" };
60
55
 
61
- describe("#433 / standaloneRedisClientsModule.deviceCodeStoreClient", () => {
56
+ describe("standaloneRedisClientsModule.deviceCodeStoreClient", () => {
62
57
  beforeEach(() => {
63
58
  redisCtorCalls.length = 0;
64
59
  });
@@ -87,10 +82,10 @@ describe("#433 / standaloneRedisClientsModule.deviceCodeStoreClient", () => {
87
82
  const lifecycleRegistrar: LifecycleRegistrar = { register: () => {} };
88
83
 
89
84
  const client = (await provides.deviceCodeStoreClient?.({
90
- config: { ...baseConfig },
85
+ section: { ...baseSection },
91
86
  lifecycleRegistrar,
92
87
  })) as Record<string, unknown>;
93
- await provides.rateLimiterClient?.({ config: { ...baseConfig }, lifecycleRegistrar });
88
+ await provides.rateLimiterClient?.({ section: { ...baseSection }, lifecycleRegistrar });
94
89
 
95
90
  expect(redisCtorCalls).toHaveLength(1);
96
91
  for (const method of ["create", "findPending", "decide", "poll", "remove"]) {