@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.
Files changed (114) hide show
  1. package/README.md +2 -198
  2. package/main.js +4 -0
  3. package/package.json +8 -30
  4. package/README.ja.md +0 -200
  5. package/dist/cli.d.mts +0 -3
  6. package/dist/cli.d.mts.map +0 -1
  7. package/dist/cli.mjs +0 -18
  8. package/dist/index.d.mts +0 -48
  9. package/dist/index.d.mts.map +0 -1
  10. package/dist/index.mjs +0 -325
  11. package/dist/internal/template-filter.d.mts +0 -14
  12. package/dist/internal/template-filter.d.mts.map +0 -1
  13. package/dist/internal/template-filter.mjs +0 -42
  14. package/templates/standalone/.dockerignore +0 -23
  15. package/templates/standalone/.env.example +0 -152
  16. package/templates/standalone/Dockerfile +0 -125
  17. package/templates/standalone/Makefile +0 -12
  18. package/templates/standalone/README.ja.md +0 -912
  19. package/templates/standalone/README.md +0 -1536
  20. package/templates/standalone/config/application.conf +0 -351
  21. package/templates/standalone/config/clients.yaml +0 -4
  22. package/templates/standalone/config/clients.yaml.example +0 -124
  23. package/templates/standalone/config/development.conf +0 -17
  24. package/templates/standalone/config/mailpit.conf +0 -13
  25. package/templates/standalone/config/production.conf +0 -15
  26. package/templates/standalone/config/reference.conf +0 -427
  27. package/templates/standalone/docker-compose.mailpit.yml +0 -39
  28. package/templates/standalone/docker-compose.production.yml +0 -229
  29. package/templates/standalone/docker-compose.test.yml +0 -16
  30. package/templates/standalone/docker-compose.yml +0 -67
  31. package/templates/standalone/gitignore +0 -23
  32. package/templates/standalone/package.json +0 -56
  33. package/templates/standalone/src/__tests__/adapters.test.mts +0 -425
  34. package/templates/standalone/src/__tests__/admission-lifecycle.test.mts +0 -87
  35. package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +0 -1188
  36. package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +0 -285
  37. package/templates/standalone/src/__tests__/all-modules-composition.test.mts +0 -1154
  38. package/templates/standalone/src/__tests__/audit-sink.test.mts +0 -355
  39. package/templates/standalone/src/__tests__/backchannel-peer.fixture.mts +0 -64
  40. package/templates/standalone/src/__tests__/config-notices.test.mts +0 -157
  41. package/templates/standalone/src/__tests__/configPath.test.mts +0 -69
  42. package/templates/standalone/src/__tests__/configured-mfa-mode.fixture.mts +0 -48
  43. package/templates/standalone/src/__tests__/core-federations.test.mts +0 -252
  44. package/templates/standalone/src/__tests__/core-sections.test.mts +0 -192
  45. package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +0 -95
  46. package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +0 -1291
  47. package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +0 -787
  48. package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +0 -464
  49. package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +0 -141
  50. package/templates/standalone/src/__tests__/federation-types.test.mts +0 -239
  51. package/templates/standalone/src/__tests__/frontchannel-logout-csp.test.mts +0 -168
  52. package/templates/standalone/src/__tests__/google-session.fixture.mts +0 -318
  53. package/templates/standalone/src/__tests__/host-routes.test.mts +0 -134
  54. package/templates/standalone/src/__tests__/library-reference-resolution.smoke.test.mts +0 -29
  55. package/templates/standalone/src/__tests__/library-references.fixture.mts +0 -188
  56. package/templates/standalone/src/__tests__/lifecycle-pending-close.test.mts +0 -217
  57. package/templates/standalone/src/__tests__/listen.test.mts +0 -121
  58. package/templates/standalone/src/__tests__/logger-projection.test.mts +0 -77
  59. package/templates/standalone/src/__tests__/logout-session-cookie.test.mts +0 -113
  60. package/templates/standalone/src/__tests__/mail-sender.test.mts +0 -134
  61. package/templates/standalone/src/__tests__/mailpit-compose.test.mts +0 -393
  62. package/templates/standalone/src/__tests__/metrics.test.mts +0 -253
  63. package/templates/standalone/src/__tests__/mfa-routes-budget.test.mts +0 -122
  64. package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +0 -127
  65. package/templates/standalone/src/__tests__/mfa-switch.test.mts +0 -883
  66. package/templates/standalone/src/__tests__/own-modules.test.mts +0 -1480
  67. package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +0 -183
  68. package/templates/standalone/src/__tests__/redis-assume-noeviction.test.mts +0 -283
  69. package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +0 -135
  70. package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +0 -214
  71. package/templates/standalone/src/__tests__/redis-stand-in.fixture.mts +0 -98
  72. package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +0 -249
  73. package/templates/standalone/src/__tests__/replica-safety.test.mts +0 -657
  74. package/templates/standalone/src/__tests__/repositories-logger.test.mts +0 -119
  75. package/templates/standalone/src/__tests__/router-error-answers.test.mts +0 -146
  76. package/templates/standalone/src/__tests__/scaffold-assets.test.mts +0 -610
  77. package/templates/standalone/src/__tests__/sections.test.mts +0 -516
  78. package/templates/standalone/src/__tests__/session-lifecycle.test.mts +0 -213
  79. package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +0 -305
  80. package/templates/standalone/src/__tests__/session-logout-lifecycle.test.mts +0 -130
  81. package/templates/standalone/src/__tests__/session-requirements.test.mts +0 -382
  82. package/templates/standalone/src/__tests__/shipped-config-notices.test.mts +0 -274
  83. package/templates/standalone/src/__tests__/shutdown.test.mts +0 -622
  84. package/templates/standalone/src/__tests__/smoke.test.mts +0 -642
  85. package/templates/standalone/src/__tests__/startup-log-lines.test.mts +0 -164
  86. package/templates/standalone/src/__tests__/store-credential.test.mts +0 -314
  87. package/templates/standalone/src/__tests__/supertest-loopback.test.mts +0 -296
  88. package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +0 -265
  89. package/templates/standalone/src/__tests__/two-phase-config.test.mts +0 -605
  90. package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +0 -385
  91. package/templates/standalone/src/adapters.mts +0 -162
  92. package/templates/standalone/src/app.mts +0 -127
  93. package/templates/standalone/src/bootRefusal.mts +0 -117
  94. package/templates/standalone/src/buildModules.mts +0 -358
  95. package/templates/standalone/src/configPath.mts +0 -487
  96. package/templates/standalone/src/listen.mts +0 -60
  97. package/templates/standalone/src/logger.mts +0 -80
  98. package/templates/standalone/src/metrics.mts +0 -167
  99. package/templates/standalone/src/mfaSwitch.mts +0 -284
  100. package/templates/standalone/src/modules.mts +0 -705
  101. package/templates/standalone/src/rootRenames.mts +0 -95
  102. package/templates/standalone/src/routes.mts +0 -89
  103. package/templates/standalone/src/sections.mts +0 -378
  104. package/templates/standalone/src/securityHeaders.mts +0 -36
  105. package/templates/standalone/src/shutdown.mts +0 -213
  106. package/templates/standalone/tests/Makefile +0 -5
  107. package/templates/standalone/tests/index.test.js +0 -283
  108. package/templates/standalone/tests/package-lock.json +0 -1617
  109. package/templates/standalone/tests/package.json +0 -16
  110. package/templates/standalone/tests/vitest.config.mjs +0 -26
  111. package/templates/standalone/tsconfig.json +0 -31
  112. package/templates/standalone/vitest.config.mts +0 -31
  113. package/templates/standalone/vitest.supertest-loopback.mts +0 -278
  114. package/templates/versions.json +0 -20
@@ -1,425 +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 composition root's own section, `adapters`: which adapter fills each
19
- * slot. Phase one reads it alone, with the template's own strict schema, over
20
- * the template's own layers and its `reference.conf`, before it chooses its
21
- * modules; boot is not handed it. A path a selection moved from, or a
22
- * variable renamed with it, refuses before any module is chosen, naming the
23
- * new path and variable; an old variable beside its new name at the same
24
- * value is accepted.
25
- */
26
-
27
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
28
- import { tmpdir } from "node:os";
29
- import { join } from "node:path";
30
- import { fileURLToPath } from "node:url";
31
- import { BootError, defineModule } from "@o3co/auth-provider-core";
32
- import { afterAll, describe, expect, it } from "vitest";
33
- import { z } from "zod";
34
- import { buildModules } from "#/buildModules.mjs";
35
- import { readOwnLayers, readSwitches, resolveConfigPaths, resolveForBoot } from "#/configPath.mjs";
36
-
37
- const configDir = fileURLToPath(new URL("../../config", import.meta.url));
38
-
39
- /** The directories the operator layers are written to, removed after the suite. */
40
- const operatorDirs: string[] = [];
41
- afterAll(() => {
42
- for (const dir of operatorDirs) rmSync(dir, { recursive: true, force: true });
43
- });
44
-
45
- /** The template's own files for the production environment, under an operator's layer when given. */
46
- function ownFiles(hocon?: string): string[] {
47
- const { applicationConfPath, envConfPath } = resolveConfigPaths(configDir, "production");
48
- if (hocon === undefined) return [envConfPath, applicationConfPath];
49
- const dir = mkdtempSync(join(tmpdir(), "adapters-"));
50
- operatorDirs.push(dir);
51
- const file = join(dir, "operator.conf");
52
- writeFileSync(file, hocon);
53
- return [file, envConfPath, applicationConfPath];
54
- }
55
-
56
- /** MFA, on by default, switched off where modules are built: these are about the other adapters. */
57
- const MFA_OFF: Readonly<Record<string, string>> = { MFA_MODE: "off" };
58
-
59
- /** Phase one's reading of `adapters`, under `env` and an operator's `hocon`. */
60
- const adaptersFrom = (env: Record<string, string> = {}, hocon?: string) =>
61
- readSwitches(readOwnLayers(ownFiles(hocon), { env })).adapters;
62
-
63
- /** What phase one refuses under `env` and an operator's `hocon`: a `BootError`. */
64
- function refusal(env: Record<string, string> = {}, hocon?: string): BootError {
65
- try {
66
- adaptersFrom(env, hocon);
67
- } catch (err) {
68
- expect(err).toBeInstanceOf(BootError);
69
- return err as BootError;
70
- }
71
- throw new Error("phase one read the adapters");
72
- }
73
-
74
- /** Each selection: its key, the variable that sets it, the path and variable it moved from, a value it takes. */
75
- const SELECTIONS = [
76
- ["rateLimiter", "RATE_LIMITER", "rateLimiter.adapter", "RATE_LIMITER_ADAPTER", "redis"],
77
- [
78
- "userSessionStores",
79
- "USER_SESSION_STORES",
80
- "userSessionStores.adapter",
81
- "USER_SESSION_STORES_ADAPTER",
82
- "redis",
83
- ],
84
- [
85
- "accessTokenDenylist",
86
- "ACCESS_TOKEN_DENYLIST",
87
- "accessTokenDenylist.adapter",
88
- "ACCESS_TOKEN_DENYLIST_ADAPTER",
89
- "memory",
90
- ],
91
- [
92
- "replaySeenSet",
93
- "REPLAY_SEEN_SET",
94
- "replaySeenSet.adapter",
95
- "REPLAY_SEEN_SET_ADAPTER",
96
- "memory",
97
- ],
98
- ["consentStore", "CONSENT_STORE", "consentStore.adapter", "CONSENT_STORE_ADAPTER", "memory"],
99
- [
100
- "federationTokenStore",
101
- "FEDERATION_TOKEN_STORE",
102
- "federationTokenStore.type",
103
- "FEDERATION_TOKEN_STORE_TYPE",
104
- "redis",
105
- ],
106
- [
107
- "federationGrantStore",
108
- "FEDERATION_GRANT_STORE",
109
- "federationGrantStore.adapter",
110
- "FEDERATION_GRANT_STORE_ADAPTER",
111
- "memory",
112
- ],
113
- [
114
- "federationGrantIntentStore",
115
- "FEDERATION_GRANT_INTENT_STORE",
116
- "federationGrantIntentStore.adapter",
117
- "FEDERATION_GRANT_INTENT_STORE_ADAPTER",
118
- "memory",
119
- ],
120
- [
121
- "mfaFactorStore",
122
- "MFA_FACTOR_STORE",
123
- "mfaFactorStore.adapter",
124
- "MFA_FACTOR_STORE_ADAPTER",
125
- "store",
126
- ],
127
- [
128
- "mfaTransactionStore",
129
- "MFA_TRANSACTION_STORE",
130
- "mfaTransactionStore.adapter",
131
- "MFA_TRANSACTION_STORE_ADAPTER",
132
- "redis",
133
- ],
134
- ["codeRepository", "CODE_REPOSITORY", "oauth.code.adapter", "OAUTH_CODE_ADAPTER", "memory"],
135
- ["codeRepository", "CODE_REPOSITORY", "repositories.code.type", "CLIENT_CODE_TYPE", "memory"],
136
- ["clientRepository", "CLIENT_REPOSITORY", "repositories.client.type", "CLIENT_TYPE", "yaml"],
137
- ["userRepository", "USER_REPOSITORY", "repositories.user.type", "CLIENT_USER_TYPE", "yaml"],
138
- ["auditSink", "AUDIT_SINK", "audit.sink.type", "AUDIT_SINK_TYPE", "console"],
139
- ] as const;
140
-
141
- describe("the shipped selections", () => {
142
- it("reads every selection from the template's reference.conf, at the values the template ships", () => {
143
- expect(adaptersFrom()).toEqual({
144
- rateLimiter: "memory",
145
- attemptCounter: "memory",
146
- userSessionStores: "memory",
147
- accessTokenDenylist: "redis",
148
- replaySeenSet: "redis",
149
- consentStore: "none",
150
- federationTokenStore: "memory",
151
- federationGrantStore: "none",
152
- federationGrantIntentStore: "none",
153
- mfaFactorStore: "memory",
154
- mfaTransactionStore: "memory",
155
- codeRepository: "redis",
156
- clientRepository: "yaml",
157
- userRepository: "http",
158
- auditSink: "logger",
159
- });
160
- });
161
-
162
- it.each(SELECTIONS)(
163
- "reads adapters.%s from ADAPTERS_%s",
164
- (key, name, _oldPath, _oldName, value) => {
165
- expect(adaptersFrom({ [`ADAPTERS_${name}`]: value })[key]).toBe(value);
166
- },
167
- );
168
-
169
- it("reads adapters.attemptCounter from ADAPTERS_ATTEMPT_COUNTER", () => {
170
- expect(adaptersFrom({ ADAPTERS_ATTEMPT_COUNTER: "redis" }).attemptCounter).toBe("redis");
171
- expect(refusal({ ADAPTERS_ATTEMPT_COUNTER: "memcached" }).message).toMatch(
172
- /adapters\.attemptCounter/,
173
- );
174
- });
175
-
176
- it("reads a selection an operator writes in HOCON", () => {
177
- expect(adaptersFrom({}, 'adapters.rateLimiter = "redis"\n').rateLimiter).toBe("redis");
178
- });
179
-
180
- it("refuses a value its schema does not know, naming the key", () => {
181
- const err = refusal({ ADAPTERS_RATE_LIMITER: "memcached" });
182
- expect(err.message).toMatch(/adapters\.rateLimiter/);
183
- expect(err.reason).toBe("config-validation-failed");
184
- expect(err.details).toMatchObject({
185
- issues: [{ path: ["adapters", "rateLimiter"] }],
186
- modules: [{ module: "adapters", schemaPath: "adapters" }],
187
- });
188
- });
189
-
190
- it("refuses the Store for the MFA transaction store, which holds verification state, and accepts it for the factor store", () => {
191
- expect(refusal({ ADAPTERS_MFA_TRANSACTION_STORE: "store" }).message).toMatch(
192
- /adapters\.mfaTransactionStore/,
193
- );
194
- expect(adaptersFrom({ ADAPTERS_MFA_FACTOR_STORE: "store" }).mfaFactorStore).toBe("store");
195
- });
196
-
197
- it.each([
198
- ["ADAPTERS_CLIENT_REPOSITORY", "clientRepository"],
199
- ["ADAPTERS_USER_REPOSITORY", "userRepository"],
200
- ] as const)("reads static, core's alias of yaml, from %s", (variable, key) => {
201
- expect(adaptersFrom({ [variable]: "static" })[key]).toBe("static");
202
- });
203
-
204
- it("refuses a key the section does not declare, naming it", () => {
205
- expect(refusal({}, 'adapters.sessionStore = "redis"\n').message).toMatch(/sessionStore/);
206
- });
207
- });
208
-
209
- describe("a path a selection moved from, written in the operator's own layer", () => {
210
- it.each(SELECTIONS)(
211
- "adapters.%s: refused where it was, %s's old path, naming the new path and ADAPTERS_%s",
212
- (key, name, oldPath, _oldName, value) => {
213
- const err = refusal({}, `${oldPath} = "${value}"\n`);
214
- expect(err.message).toContain(
215
- `${oldPath} has moved to adapters.${key}; see the upgrade guide (docs/upgrading-from-v0.16.0.md).`,
216
- );
217
- expect(err.message).toContain(`ADAPTERS_${name}`);
218
- expect(err.reason).toBe("config-path-relocated");
219
- expect(err.details).toEqual({
220
- reason: "config-path-relocated",
221
- relocated: [
222
- {
223
- module: "adapters",
224
- from: oldPath,
225
- to: `adapters.${key}`,
226
- environmentVariable: `ADAPTERS_${name}`,
227
- },
228
- ],
229
- });
230
- },
231
- );
232
-
233
- it("names every old path it finds, in one refusal", () => {
234
- const err = refusal(
235
- {},
236
- 'rateLimiter.adapter = "redis"\nconsentStore.adapter = "memory"\naudit.sink.type = "console"\n',
237
- );
238
- for (const path of ["rateLimiter.adapter", "consentStore.adapter", "audit.sink.type"]) {
239
- expect(err.message).toContain(path);
240
- }
241
- expect(err.details).toMatchObject({
242
- relocated: [
243
- { from: "rateLimiter.adapter" },
244
- { from: "consentStore.adapter" },
245
- { from: "audit.sink.type" },
246
- ],
247
- });
248
- });
249
- });
250
-
251
- describe("a variable renamed with a selection", () => {
252
- it.each(SELECTIONS)(
253
- "adapters.%s: %s's old variable set alone is refused, naming ADAPTERS_%s",
254
- (key, name, _oldPath, oldName, value) => {
255
- const err = refusal({ [oldName]: value });
256
- expect(err.message).toContain(
257
- `${oldName} was renamed ADAPTERS_${name}, the variable adapters.${key} is bound to; see the upgrade guide (docs/upgrading-from-v0.16.0.md).`,
258
- );
259
- expect(err.reason).toBe("environment-variable-renamed");
260
- expect(err.details).toEqual({
261
- reason: "environment-variable-renamed",
262
- renamed: [
263
- {
264
- module: "adapters",
265
- from: oldName,
266
- to: `ADAPTERS_${name}`,
267
- path: `adapters.${key}`,
268
- state: "unset",
269
- },
270
- ],
271
- });
272
- },
273
- );
274
-
275
- it.each(SELECTIONS)(
276
- "adapters.%s: the old variable beside ADAPTERS_%s at a different value is refused, quoting neither",
277
- (_key, name, _oldPath, oldName) => {
278
- const err = refusal({ [oldName]: "old-value-5e2d", [`ADAPTERS_${name}`]: "new-value-c81a" });
279
- expect(err.message).toContain(oldName);
280
- expect(err.message).not.toContain("old-value-5e2d");
281
- expect(err.message).not.toContain("new-value-c81a");
282
- expect(err.details).toMatchObject({ renamed: [{ from: oldName, state: "different" }] });
283
- expect(JSON.stringify(err.details)).not.toMatch(/old-value-5e2d|new-value-c81a/);
284
- },
285
- );
286
-
287
- it.each(SELECTIONS)(
288
- "adapters.%s: the old variable beside ADAPTERS_%s at the same value is refused all the same",
289
- (_key, name, _oldPath, oldName, value) => {
290
- const err = refusal({ [oldName]: value, [`ADAPTERS_${name}`]: value });
291
- expect(err.reason).toBe("environment-variable-renamed");
292
- expect(err.message).toContain(`unset ${oldName}`);
293
- expect(err.details).toMatchObject({ renamed: [{ from: oldName, state: "different" }] });
294
- },
295
- );
296
- });
297
-
298
- describe("the modules phase one chooses by the selections", () => {
299
- const names = (env: Record<string, string>) =>
300
- buildModules(readSwitches(readOwnLayers(ownFiles(), { env: { ...MFA_OFF, ...env } })), {
301
- environment: "production",
302
- }).map((module) => module.name);
303
-
304
- it.each([
305
- ["ADAPTERS_RATE_LIMITER", "redis", "redis-rate-limiter", "core-rate-limiter-memory"],
306
- ["ADAPTERS_RATE_LIMITER", "memory", "core-rate-limiter-memory", "redis-rate-limiter"],
307
- [
308
- "ADAPTERS_CODE_REPOSITORY",
309
- "memory",
310
- "standalone-in-memory-code-repository",
311
- "redis-code-repository",
312
- ],
313
- [
314
- "ADAPTERS_CODE_REPOSITORY",
315
- "redis",
316
- "redis-code-repository",
317
- "standalone-in-memory-code-repository",
318
- ],
319
- [
320
- "ADAPTERS_USER_SESSION_STORES",
321
- "redis",
322
- "redis-session-stores",
323
- "standalone-in-memory-session-stores",
324
- ],
325
- ["ADAPTERS_CONSENT_STORE", "memory", "core-consent-store-memory", "redis-consent-store"],
326
- ])("%s=%s installs %s, not %s", (variable, value, installed, absent) => {
327
- const modules = names({ [variable]: value });
328
- expect(modules).toContain(installed);
329
- expect(modules).not.toContain(absent);
330
- });
331
-
332
- it("ADAPTERS_ATTEMPT_COUNTER=redis installs redis-attempt-counter over the shared Redis socket; memory installs no counter", () => {
333
- const onRedis = names({ ADAPTERS_ATTEMPT_COUNTER: "redis" });
334
- expect(onRedis).toContain("redis-attempt-counter");
335
- expect(onRedis).toContain("redis-clients");
336
- expect(names({ ADAPTERS_ATTEMPT_COUNTER: "memory" })).not.toContain("redis-attempt-counter");
337
- });
338
- });
339
-
340
- describe("the federation-grant stores", () => {
341
- const namesWith = (env: Record<string, string>) =>
342
- buildModules(
343
- readSwitches(
344
- readOwnLayers(ownFiles(), {
345
- env: { ...MFA_OFF, FEDERATION_GRANTS_ENABLED: "true", ...env },
346
- }),
347
- ),
348
- { environment: "production" },
349
- ).map((module) => module.name);
350
- const STORES = [
351
- "core-federation-grant-store-memory",
352
- "redis-federation-grant-store",
353
- "core-federation-grant-intent-store-memory",
354
- "redis-federation-grant-intent-store",
355
- ];
356
-
357
- it("default to none: with the feature on, neither store is installed until one is selected", () => {
358
- expect(namesWith({}).filter((name) => STORES.includes(name))).toEqual([]);
359
- });
360
-
361
- it("read none from ADAPTERS_FEDERATION_GRANT_STORE and ADAPTERS_FEDERATION_GRANT_INTENT_STORE", () => {
362
- expect(
363
- adaptersFrom({
364
- ADAPTERS_FEDERATION_GRANT_STORE: "none",
365
- ADAPTERS_FEDERATION_GRANT_INTENT_STORE: "none",
366
- }),
367
- ).toMatchObject({ federationGrantStore: "none", federationGrantIntentStore: "none" });
368
- });
369
-
370
- it.each([
371
- ["memory", "core-federation-grant-store-memory", "core-federation-grant-intent-store-memory"],
372
- ["redis", "redis-federation-grant-store", "redis-federation-grant-intent-store"],
373
- ])("install the %s stores once selected", (value, grants, intents) => {
374
- const names = namesWith({
375
- ADAPTERS_FEDERATION_GRANT_STORE: value,
376
- ADAPTERS_FEDERATION_GRANT_INTENT_STORE: value,
377
- });
378
- expect(names).toContain(grants);
379
- expect(names).toContain(intents);
380
- });
381
- });
382
-
383
- describe("boot and the section", () => {
384
- it("hands boot no adapters section: the composition root consumed it", () => {
385
- const own = readOwnLayers(ownFiles(), { env: MFA_OFF });
386
- const switches = readSwitches(own);
387
- const resolved = resolveForBoot(
388
- own,
389
- buildModules(switches, { environment: "production" }),
390
- switches,
391
- );
392
- expect(resolved).not.toHaveProperty("adapters");
393
- });
394
-
395
- it.each([
396
- ["named adapters", "adapters"],
397
- ["named mfaMode", "mfaMode"],
398
- ] as const)("refuses a module %s: its section would never reach boot", (_label, name) => {
399
- const own = readOwnLayers(ownFiles(), { env: MFA_OFF });
400
- const switches = readSwitches(own);
401
- const mine = defineModule({
402
- name,
403
- section: { schema: z.object({}).passthrough().optional() },
404
- });
405
- const run = () =>
406
- resolveForBoot(
407
- own,
408
- [...buildModules(switches, { environment: "production" }), mine],
409
- switches,
410
- );
411
- expect(run).toThrow(new RegExp(`"${name}".*composition root's own section`));
412
- const err = (() => {
413
- try {
414
- run();
415
- } catch (thrown) {
416
- return thrown;
417
- }
418
- })();
419
- expect(err).toBeInstanceOf(BootError);
420
- expect(err).toMatchObject({
421
- reason: "module-section-path-invalid",
422
- details: { module: name, at: name },
423
- });
424
- });
425
- });
@@ -1,87 +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 oauth package's consumers of session admission read the session
19
- * lifecycle's record the template's session stores provide: a session whose
20
- * record is closing is refused at `/authorize`, the code exchange and the
21
- * refresh grant, while its user session is still there.
22
- */
23
-
24
- import type { SessionLifecycleStore, UserSessionStore } from "@o3co/auth-provider-core";
25
- import request from "supertest";
26
- import { describe, expect, it } from "vitest";
27
- import {
28
- authorize,
29
- basic,
30
- codeFrom,
31
- compose,
32
- login,
33
- redeem,
34
- WEB,
35
- } from "./all-modules-composition.fixture.mjs";
36
-
37
- /** A JWT's claims, unverified. */
38
- const claimsOf = (token: string): Record<string, unknown> =>
39
- JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
40
-
41
- describe("a session whose lifecycle record is closing", () => {
42
- it("is refused at /authorize, the code exchange and the refresh grant", async () => {
43
- const { app, handle } = await compose();
44
- try {
45
- const components = handle.components as Record<string, unknown>;
46
- const store = components.sessionLifecycleStore as SessionLifecycleStore;
47
- const sessions = components.userSessionStore as UserSessionStore;
48
- const { cookies } = await login(app);
49
- const tokens = (await redeem(app, codeFrom(await authorize(app, cookies)))).body as Record<
50
- string,
51
- string
52
- >;
53
- const sid = String(claimsOf(tokens.id_token ?? "").sid);
54
- const pendingCode = codeFrom(await authorize(app, cookies));
55
-
56
- // The session's close commits, its work pending: the user session stays.
57
- const session = await sessions.get(sid);
58
- if (session === null) throw new Error("the login wrote no session");
59
- expect((await store.open(sid, session.sub, session.expiresAt)).outcome).toBe("opened");
60
- const closing = await store.beginClose(sid, {
61
- cause: "rp_logout",
62
- steps: ["held_open"],
63
- perParticipant: [],
64
- retainMs: 0,
65
- });
66
- expect(closing.outcome).toBe("closing");
67
- expect(await sessions.get(sid)).not.toBeNull();
68
-
69
- const again = await authorize(app, cookies);
70
- // Sent to log in again, with no code.
71
- expect(again.status).toBe(302);
72
- expect(String(again.headers.location)).not.toMatch(/[?&]code=/);
73
-
74
- expect((await redeem(app, pendingCode)).body).toMatchObject({ error: "invalid_grant" });
75
-
76
- const refreshed = await request(app)
77
- .post("/oauth/token")
78
- .set("Authorization", basic(WEB))
79
- .type("form")
80
- .send({ grant_type: "refresh_token", refresh_token: tokens.refresh_token });
81
- expect(refreshed.status).toBe(400);
82
- expect(refreshed.body).toMatchObject({ error: "invalid_grant" });
83
- } finally {
84
- await handle.dispose();
85
- }
86
- });
87
- });