@o3co/create-auth-provider 0.14.0 → 0.16.0
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 +93 -47
- package/README.md +92 -46
- package/package.json +1 -1
- package/templates/standalone/.env.example +34 -0
- package/templates/standalone/Dockerfile +1 -1
- package/templates/standalone/README.ja.md +565 -130
- package/templates/standalone/README.md +356 -92
- package/templates/standalone/config/application.conf +77 -2
- package/templates/standalone/docker-compose.production.yml +7 -0
- package/templates/standalone/docker-compose.yml +7 -0
- package/templates/standalone/gitignore +3 -2
- package/templates/standalone/package.json +3 -2
- package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +49 -15
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +1103 -0
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +241 -0
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +1100 -0
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +40 -5
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +445 -0
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +446 -0
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +133 -0
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +85 -0
- package/templates/standalone/src/__tests__/google-session.fixture.mts +304 -0
- package/templates/standalone/src/__tests__/host-routes.test.mts +141 -0
- package/templates/standalone/src/__tests__/listen.test.mts +128 -0
- package/templates/standalone/src/__tests__/logger-projection.test.mts +77 -0
- package/templates/standalone/src/__tests__/metrics.test.mts +20 -5
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +187 -0
- package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +139 -0
- package/templates/standalone/src/__tests__/replica-safety.test.mts +13 -3
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +149 -0
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +14 -0
- package/templates/standalone/src/__tests__/shutdown.test.mts +253 -22
- package/templates/standalone/src/__tests__/smoke.test.mts +39 -20
- package/templates/standalone/src/__tests__/store-credential.test.mts +276 -0
- package/templates/standalone/src/app.mts +30 -51
- package/templates/standalone/src/buildModules.mts +94 -12
- package/templates/standalone/src/listen.mts +66 -0
- package/templates/standalone/src/logger.mts +7 -4
- package/templates/standalone/src/modules.mts +118 -12
- package/templates/standalone/src/routes.mts +95 -0
- package/templates/standalone/src/shutdown.mts +90 -18
- package/templates/standalone/vitest.supertest-loopback.mts +22 -3
- package/templates/versions.json +15 -14
- package/templates/standalone/src/__tests__/terminal-error.test.mts +0 -96
- package/templates/standalone/src/terminalError.mts +0 -60
|
@@ -0,0 +1,241 @@
|
|
|
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, `deployment.mode = "multi"`,
|
|
20
|
+
* every module the template can turn on switched on together.
|
|
21
|
+
*
|
|
22
|
+
* What it holds: the replica-safety declarations hold with every module on —
|
|
23
|
+
* the all-Redis composition boots with nothing declaring replica-unsafe state,
|
|
24
|
+
* and each store switched back to memory is refused at boot, naming the
|
|
25
|
+
* module — and the Redis adapters change nothing a client can see: the same
|
|
26
|
+
* routes, the same discovery document as on one replica.
|
|
27
|
+
*
|
|
28
|
+
* ioredis, node-redis and connect-redis are stand-ins, as in
|
|
29
|
+
* `replica-safety.test.mts`: what is under test is the composition and the
|
|
30
|
+
* boot's verdict, and nothing here issues a command. The template has no
|
|
31
|
+
* Redis server to run against — `packages/redis` runs its adapters against
|
|
32
|
+
* one.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { type Module, replicaUnsafeReason } from "@o3co/auth-provider-core";
|
|
36
|
+
import * as redisPackage from "@o3co/auth-provider-redis";
|
|
37
|
+
import request from "supertest";
|
|
38
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
39
|
+
import { standaloneRedisClientsModule } from "#/modules.mjs";
|
|
40
|
+
import {
|
|
41
|
+
type Composition,
|
|
42
|
+
compose,
|
|
43
|
+
composedModules,
|
|
44
|
+
MULTI_ENV,
|
|
45
|
+
resolveConfig,
|
|
46
|
+
} from "./all-modules-composition.fixture.mjs";
|
|
47
|
+
|
|
48
|
+
vi.mock("redis", () => ({
|
|
49
|
+
createClient: vi.fn(() => ({
|
|
50
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
51
|
+
quit: vi.fn().mockResolvedValue(undefined),
|
|
52
|
+
ping: vi.fn().mockResolvedValue("PONG"),
|
|
53
|
+
on: vi.fn(),
|
|
54
|
+
})),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
vi.mock("connect-redis", async () => {
|
|
58
|
+
const { EventEmitter } = await import("node:events");
|
|
59
|
+
// express-session subscribes to store events, so the stand-in is an emitter.
|
|
60
|
+
return {
|
|
61
|
+
RedisStore: class MockRedisStore extends EventEmitter {
|
|
62
|
+
constructor(_opts: { client: unknown }) {
|
|
63
|
+
super();
|
|
64
|
+
}
|
|
65
|
+
get(): unknown {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
set(): void {}
|
|
69
|
+
destroy(): void {}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
vi.mock("ioredis", () => {
|
|
75
|
+
// Constructible, quits cleanly, answers the readiness probe's `ping`;
|
|
76
|
+
// every other command resolves to nothing.
|
|
77
|
+
const explicit: Record<string, unknown> = {
|
|
78
|
+
on: () => undefined,
|
|
79
|
+
quit: async () => "OK",
|
|
80
|
+
disconnect: () => undefined,
|
|
81
|
+
ping: async () => "PONG",
|
|
82
|
+
};
|
|
83
|
+
const makeMockRedis = (): object =>
|
|
84
|
+
new Proxy(
|
|
85
|
+
{},
|
|
86
|
+
{
|
|
87
|
+
get(_target, prop) {
|
|
88
|
+
// A function-valued `then` would make the instance a thenable.
|
|
89
|
+
if (typeof prop !== "string" || prop === "then") return undefined;
|
|
90
|
+
if (prop === "duplicate") return makeMockRedis;
|
|
91
|
+
if (prop in explicit) return explicit[prop];
|
|
92
|
+
return async () => null;
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
function MockRedis(): object {
|
|
97
|
+
return makeMockRedis();
|
|
98
|
+
}
|
|
99
|
+
return { Redis: MockRedis, default: MockRedis };
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
let current: Composition | undefined;
|
|
103
|
+
|
|
104
|
+
afterEach(async () => {
|
|
105
|
+
await current?.handle.dispose();
|
|
106
|
+
current = undefined;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const MULTI = { env: MULTI_ENV, shippedRefreshTokenFamilyStore: true } as const;
|
|
110
|
+
|
|
111
|
+
/** What `buildModules` lists with every switch on and every shared store on Redis. */
|
|
112
|
+
const ALL_ON_REDIS_MODULES = [
|
|
113
|
+
"sessionStoreModule",
|
|
114
|
+
"federation-grant-background",
|
|
115
|
+
"federation-grants",
|
|
116
|
+
"oauth",
|
|
117
|
+
"oauth-session",
|
|
118
|
+
"oauth-authorization",
|
|
119
|
+
"jwks",
|
|
120
|
+
"session",
|
|
121
|
+
"federation:google",
|
|
122
|
+
"standalone:google-federation-config",
|
|
123
|
+
"standalone:oidc-federation-config",
|
|
124
|
+
"federation:oidc:oidc",
|
|
125
|
+
"standalone:key-store",
|
|
126
|
+
"test:repositories",
|
|
127
|
+
"standalone:audit-sink",
|
|
128
|
+
"standalone:redis-clients",
|
|
129
|
+
"redis-federation-token-store",
|
|
130
|
+
"redis-federation-grant-store",
|
|
131
|
+
"redis-federation-grant-intent-store",
|
|
132
|
+
"redisSessionStores",
|
|
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, 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
|
+
/**
|
|
170
|
+
* Each store's switch back to memory, alone, with every module on. The
|
|
171
|
+
* boot names the module that declared the state it would fork.
|
|
172
|
+
*/
|
|
173
|
+
const MEMORY_SWITCHES: ReadonlyArray<readonly [variable: string, module: string]> = [
|
|
174
|
+
["SESSION_STORAGE_TYPE", "sessionStoreModule"],
|
|
175
|
+
["USER_SESSION_STORES_ADAPTER", "standalone:in-memory-session-stores"],
|
|
176
|
+
["OAUTH_CODE_ADAPTER", "standalone:in-memory-code-repository"],
|
|
177
|
+
["FEDERATION_TOKEN_STORE_TYPE", "standalone:in-memory-federation-token-store"],
|
|
178
|
+
["RATE_LIMITER_ADAPTER", "core-rate-limiter-memory"],
|
|
179
|
+
["ACCESS_TOKEN_DENYLIST_ADAPTER", "core-access-token-denylist-memory"],
|
|
180
|
+
["REPLAY_SEEN_SET_ADAPTER", "core-replay-seen-set-memory"],
|
|
181
|
+
["CONSENT_STORE_ADAPTER", "core-consent-store-memory"],
|
|
182
|
+
["FEDERATION_GRANT_STORE_ADAPTER", "core-federation-grant-store-memory"],
|
|
183
|
+
["FEDERATION_GRANT_INTENT_STORE_ADAPTER", "core-federation-grant-intent-store-memory"],
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
it.each(MEMORY_SWITCHES)("%s=memory is refused at boot, naming %s", async (variable, module) => {
|
|
187
|
+
await expect(
|
|
188
|
+
compose({ ...MULTI, env: { ...MULTI_ENV, [variable]: "memory" } }),
|
|
189
|
+
).rejects.toMatchObject({
|
|
190
|
+
name: "BootError",
|
|
191
|
+
reason: "replica-unsafe-adapter",
|
|
192
|
+
details: { modules: [module] },
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("names every memory store together when every switch is memory", async () => {
|
|
197
|
+
const env = { ...MULTI_ENV };
|
|
198
|
+
for (const [variable] of MEMORY_SWITCHES) env[variable] = "memory";
|
|
199
|
+
await expect(compose({ ...MULTI, env })).rejects.toMatchObject({
|
|
200
|
+
reason: "replica-unsafe-adapter",
|
|
201
|
+
details: {
|
|
202
|
+
modules: expect.arrayContaining(MEMORY_SWITCHES.map(([, module]) => module)),
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("the shared Redis socket can back every store the Redis package ships", () => {
|
|
209
|
+
/**
|
|
210
|
+
* Every store module `@o3co/auth-provider-redis` exports, the two built per
|
|
211
|
+
* composition root included. This template composes some; a deployment adds
|
|
212
|
+
* the others to the manifest with the modules that read them (the device
|
|
213
|
+
* grant's code store, WebAuthn's challenge store), and each requires its
|
|
214
|
+
* client from this module. One it does not provide is a boot refused with
|
|
215
|
+
* `missing-required-component` — the #439 shape, which `modules.mts`
|
|
216
|
+
* guards against for the device-code store by name.
|
|
217
|
+
*/
|
|
218
|
+
const storeModules: Module[] = Object.entries(redisPackage).flatMap(([name, value]) => {
|
|
219
|
+
if (name.endsWith("ModuleFor") && typeof value === "function") {
|
|
220
|
+
return [(value as (options: object) => Module)({})];
|
|
221
|
+
}
|
|
222
|
+
if (name.endsWith("Module") && typeof value === "object" && value !== null) {
|
|
223
|
+
return [value as Module];
|
|
224
|
+
}
|
|
225
|
+
return [];
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it.each(storeModules.map((m) => [m.name, m] as const))(
|
|
229
|
+
"%s: its client slot is provided by standalone:redis-clients",
|
|
230
|
+
(_name, module) => {
|
|
231
|
+
const provided = Object.keys(standaloneRedisClientsModule.provides ?? {});
|
|
232
|
+
const clients = (module.requires ?? []).filter((slot) => String(slot).endsWith("Client"));
|
|
233
|
+
expect(clients.length).toBeGreaterThan(0);
|
|
234
|
+
for (const slot of clients) expect(provided, slot).toContain(slot);
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
it("finds the store modules to check", () => {
|
|
239
|
+
expect(storeModules.length).toBeGreaterThanOrEqual(12);
|
|
240
|
+
});
|
|
241
|
+
});
|