@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,256 @@
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 all-modules composition of `all-modules-composition.test.mts` on more
19
+ * than one replica: every shared store on Redis, `core.deployment.mode = "multi"`,
20
+ * every module the template can turn on switched on together.
21
+ *
22
+ * With every module on, the all-Redis composition boots with nothing declaring
23
+ * replica-unsafe state, and each store switched back to memory is refused at
24
+ * boot, naming the module. The Redis adapters change nothing a client can
25
+ * see: the same routes, the same discovery document as on one replica.
26
+ *
27
+ * ioredis, node-redis and connect-redis are stand-ins, as in
28
+ * `replica-safety.test.mts`: nothing here issues a command. `packages/redis`
29
+ * runs its adapters against a real server.
30
+ */
31
+
32
+ import { type Module, replicaUnsafeReason } from "@o3co/auth-provider-core";
33
+ import * as redisPackage from "@o3co/auth-provider-redis";
34
+ import request from "supertest";
35
+ import { afterEach, describe, expect, it, vi } from "vitest";
36
+ import { standaloneRedisClientsModule } from "#/modules.mjs";
37
+ import {
38
+ type Composition,
39
+ compose,
40
+ composedModules,
41
+ MULTI_ENV,
42
+ resolveConfig,
43
+ } from "./all-modules-composition.fixture.mjs";
44
+
45
+ vi.mock("redis", () => ({
46
+ createClient: vi.fn(() => ({
47
+ connect: vi.fn().mockResolvedValue(undefined),
48
+ quit: vi.fn().mockResolvedValue(undefined),
49
+ ping: vi.fn().mockResolvedValue("PONG"),
50
+ on: vi.fn(),
51
+ })),
52
+ }));
53
+
54
+ vi.mock("connect-redis", async () => {
55
+ const { EventEmitter } = await import("node:events");
56
+ // express-session subscribes to store events, so the stand-in is an emitter.
57
+ return {
58
+ RedisStore: class MockRedisStore extends EventEmitter {
59
+ constructor(_opts: { client: unknown }) {
60
+ super();
61
+ }
62
+ get(): unknown {
63
+ return undefined;
64
+ }
65
+ set(): void {}
66
+ destroy(): void {}
67
+ },
68
+ };
69
+ });
70
+
71
+ vi.mock("ioredis", () => {
72
+ // Constructible, quits cleanly, answers the readiness probe's `ping`;
73
+ // every other command resolves to nothing.
74
+ const explicit: Record<string, unknown> = {
75
+ on: () => undefined,
76
+ quit: async () => "OK",
77
+ disconnect: () => undefined,
78
+ ping: async () => "PONG",
79
+ };
80
+ const makeMockRedis = (): object =>
81
+ new Proxy(
82
+ {},
83
+ {
84
+ get(_target, prop) {
85
+ // A function-valued `then` would make the instance a thenable.
86
+ if (typeof prop !== "string" || prop === "then") return undefined;
87
+ if (prop === "duplicate") return makeMockRedis;
88
+ if (prop in explicit) return explicit[prop];
89
+ return async () => null;
90
+ },
91
+ },
92
+ );
93
+ function MockRedis(): object {
94
+ return makeMockRedis();
95
+ }
96
+ return { Redis: MockRedis, default: MockRedis };
97
+ });
98
+
99
+ let current: Composition | undefined;
100
+
101
+ afterEach(async () => {
102
+ await current?.handle.dispose();
103
+ current = undefined;
104
+ });
105
+
106
+ const MULTI = { env: MULTI_ENV, shippedRefreshTokenFamilyStore: true } as const;
107
+
108
+ /** What `buildModules` lists with every switch on and every shared store on Redis. */
109
+ const ALL_ON_REDIS_MODULES = [
110
+ "session-store",
111
+ "federation-grant-background",
112
+ "federation-grants",
113
+ "oauth",
114
+ "oauth-session",
115
+ "oauth-authorization",
116
+ "jwks",
117
+ "session",
118
+ "federation-google",
119
+ "google-federation-config",
120
+ "oidc-federation-config",
121
+ "federation-oidc-oidc",
122
+ "logging",
123
+ "http",
124
+ "key-store",
125
+ "test:repositories",
126
+ "audit-sink",
127
+ "standard-smtp-mail-sender",
128
+ "redis-clients",
129
+ "redis-federation-token-store",
130
+ "redis-federation-grant-store",
131
+ "redis-federation-grant-intent-store",
132
+ "redis-session-stores",
133
+ "redis-rate-limiter",
134
+ "redis-code-repository",
135
+ "redis-access-token-denylist",
136
+ "redis-replay-seen-set",
137
+ "redis-consent-store",
138
+ "redis-refresh-token-family-store",
139
+ "core-default-refresh-token-family-rotation",
140
+ "core-default-refresh-token-family-revocation",
141
+ "subject-revocation-service",
142
+ ];
143
+
144
+ describe('every module on, every shared store on Redis, core.deployment.mode = "multi"', () => {
145
+ it("lists every module, none declaring replica-unsafe state, and boots", async () => {
146
+ const modules = composedModules(resolveConfig(MULTI_ENV), MULTI);
147
+ expect(modules.map((m) => m.name)).toEqual(ALL_ON_REDIS_MODULES);
148
+ for (const module of modules) expect(replicaUnsafeReason(module), module.name).toBeUndefined();
149
+ current = await compose(MULTI);
150
+ expect(current.handle.readinessProbes.map((p) => p.name)).toContain("redis");
151
+ });
152
+
153
+ it("mounts the same routes and serves the same discovery document as one replica on memory", async () => {
154
+ current = await compose();
155
+ const single = {
156
+ routes: current.handle.routes.map((r) => r.contribution.id),
157
+ discovery: (await request(current.app).get("/.well-known/openid-configuration")).body,
158
+ };
159
+ await current.handle.dispose();
160
+ current = undefined;
161
+
162
+ current = await compose(MULTI);
163
+ expect(current.handle.routes.map((r) => r.contribution.id)).toEqual(single.routes);
164
+ const discovery = await request(current.app).get("/.well-known/openid-configuration");
165
+ expect(discovery.status).toBe(200);
166
+ expect(discovery.body).toEqual(single.discovery);
167
+ });
168
+
169
+ it("boots as multi on CORE_DEPLOYMENT_MODE=multi alone", async () => {
170
+ expect(MULTI_ENV).toMatchObject({ CORE_DEPLOYMENT_MODE: "multi" });
171
+ expect(MULTI_ENV).not.toHaveProperty("DEPLOYMENT_MODE");
172
+ current = await compose(MULTI);
173
+ expect(current.handle.components.deploymentMode).toBe("multi");
174
+ });
175
+
176
+ it("boots as multi on DEPLOYMENT_MODE=multi beside CORE_DEPLOYMENT_MODE=multi, as the umbrella sets them", async () => {
177
+ current = await compose({
178
+ ...MULTI,
179
+ env: { ...MULTI_ENV, DEPLOYMENT_MODE: "multi", CORE_DEPLOYMENT_MODE: "multi" },
180
+ });
181
+ expect(current.handle.components.deploymentMode).toBe("multi");
182
+ });
183
+
184
+ /**
185
+ * Each store's switch back to memory, alone, with every module on. The
186
+ * boot names the module that declared the state it would fork.
187
+ */
188
+ const MEMORY_SWITCHES: ReadonlyArray<readonly [variable: string, module: string]> = [
189
+ ["SESSION_STORE_STORAGE_TYPE", "session-store"],
190
+ ["ADAPTERS_USER_SESSION_STORES", "standalone-in-memory-session-stores"],
191
+ ["ADAPTERS_CODE_REPOSITORY", "standalone-in-memory-code-repository"],
192
+ ["ADAPTERS_FEDERATION_TOKEN_STORE", "standalone-in-memory-federation-token-store"],
193
+ ["ADAPTERS_RATE_LIMITER", "core-rate-limiter-memory"],
194
+ ["ADAPTERS_ACCESS_TOKEN_DENYLIST", "core-access-token-denylist-memory"],
195
+ ["ADAPTERS_REPLAY_SEEN_SET", "core-replay-seen-set-memory"],
196
+ ["ADAPTERS_CONSENT_STORE", "core-consent-store-memory"],
197
+ ["ADAPTERS_FEDERATION_GRANT_STORE", "core-federation-grant-store-memory"],
198
+ ["ADAPTERS_FEDERATION_GRANT_INTENT_STORE", "core-federation-grant-intent-store-memory"],
199
+ ];
200
+
201
+ it.each(MEMORY_SWITCHES)("%s=memory is refused at boot, naming %s", async (variable, module) => {
202
+ await expect(
203
+ compose({ ...MULTI, env: { ...MULTI_ENV, [variable]: "memory" } }),
204
+ ).rejects.toMatchObject({
205
+ name: "BootError",
206
+ reason: "replica-unsafe-adapter",
207
+ details: { modules: [module] },
208
+ });
209
+ });
210
+
211
+ it("names every memory store together when every switch is memory", async () => {
212
+ const env = { ...MULTI_ENV };
213
+ for (const [variable] of MEMORY_SWITCHES) env[variable] = "memory";
214
+ await expect(compose({ ...MULTI, env })).rejects.toMatchObject({
215
+ reason: "replica-unsafe-adapter",
216
+ details: {
217
+ modules: expect.arrayContaining(MEMORY_SWITCHES.map(([, module]) => module)),
218
+ },
219
+ });
220
+ });
221
+ });
222
+
223
+ describe("the shared Redis socket can back every store the Redis package ships", () => {
224
+ /**
225
+ * Every store module `@o3co/auth-provider-redis` exports, the two built per
226
+ * composition root included. This template composes some; a deployment adds
227
+ * the others to the manifest with the modules that read them (the device
228
+ * grant's code store, WebAuthn's challenge store), and each requires its
229
+ * client from this module. One it does not provide is a boot refused with
230
+ * `missing-required-component`, which `modules.mts` guards against for
231
+ * the device-code store by name.
232
+ */
233
+ const storeModules: Module[] = Object.entries(redisPackage).flatMap(([name, value]) => {
234
+ if (name.endsWith("ModuleFor") && typeof value === "function") {
235
+ return [(value as (options: object) => Module)({})];
236
+ }
237
+ if (name.endsWith("Module") && typeof value === "object" && value !== null) {
238
+ return [value as Module];
239
+ }
240
+ return [];
241
+ });
242
+
243
+ it.each(storeModules.map((m) => [m.name, m] as const))(
244
+ "%s: its client slot is provided by redis-clients",
245
+ (_name, module) => {
246
+ const provided = Object.keys(standaloneRedisClientsModule.provides ?? {});
247
+ const clients = (module.requires ?? []).filter((slot) => String(slot).endsWith("Client"));
248
+ expect(clients.length).toBeGreaterThan(0);
249
+ for (const slot of clients) expect(provided, slot).toContain(slot);
250
+ },
251
+ );
252
+
253
+ it("finds the store modules to check", () => {
254
+ expect(storeModules.length).toBeGreaterThanOrEqual(12);
255
+ });
256
+ });