@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,516 +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 schemas of the template's own modules' sections, each strict: what
19
- * `logging`, `http` (with its CORS list), `key-store` and `redis-clients`
20
- * accept, and how each refuses what it cannot use, before any module reads it.
21
- */
22
-
23
- import { MAX_KID_LENGTH } from "@o3co/auth-provider-core";
24
- import { describe, expect, it } from "vitest";
25
- import {
26
- httpSectionSchema,
27
- inMemoryCodeRepositorySectionSchema,
28
- keyStoreSectionSchema,
29
- loggingSectionSchema,
30
- redisClientsSectionSchema,
31
- } from "#/sections.mjs";
32
-
33
- /** The shipped `http` section, with `overrides` laid over it. */
34
- const http = (overrides: Record<string, unknown> = {}) => ({
35
- port: 3000,
36
- trustProxy: false,
37
- readinessTimeoutMs: 1000,
38
- cors: { allowedOrigins: [] },
39
- ...overrides,
40
- });
41
-
42
- const parsedTrustProxy = (trustProxy: unknown) => httpSectionSchema.safeParse(http({ trustProxy }));
43
-
44
- const issuePaths = (result: { success: boolean; error?: { issues: { path: PropertyKey[] }[] } }) =>
45
- result.success ? [] : (result.error?.issues ?? []).map((issue) => issue.path.join("."));
46
-
47
- describe("logging", () => {
48
- it.each(["trace", "debug", "info", "warn", "error", "fatal", "silent"])(
49
- "accepts the level %s",
50
- (level) => {
51
- expect(loggingSectionSchema.parse({ level })).toEqual({ level });
52
- },
53
- );
54
-
55
- it("refuses a level it does not know", () => {
56
- expect(issuePaths(loggingSectionSchema.safeParse({ level: "verbose" }))).toEqual(["level"]);
57
- });
58
-
59
- it("refuses a key it does not declare", () => {
60
- expect(loggingSectionSchema.safeParse({ level: "info", format: "json" }).success).toBe(false);
61
- });
62
- });
63
-
64
- describe("http.trustProxy — boolean", () => {
65
- it.each([false, true])("accepts the boolean %s unchanged", (value) => {
66
- const result = parsedTrustProxy(value);
67
- expect(result.success).toBe(true);
68
- if (result.success) expect(result.data.trustProxy).toBe(value);
69
- });
70
-
71
- it.each([
72
- ["true", true],
73
- ["false", false],
74
- ])("reads the variable's string %s as a boolean", (raw, expected) => {
75
- const result = parsedTrustProxy(raw);
76
- expect(result.success).toBe(true);
77
- if (result.success) expect(result.data.trustProxy).toBe(expected);
78
- });
79
-
80
- it("reads an exported-but-empty variable as `false`, trusting nothing", () => {
81
- const result = parsedTrustProxy("");
82
- expect(result.success).toBe(true);
83
- if (result.success) expect(result.data.trustProxy).toBe(false);
84
- });
85
- });
86
-
87
- describe("http.trustProxy — hop count", () => {
88
- it.each([0, 1, 3])("accepts the hop count %s", (value) => {
89
- const result = parsedTrustProxy(value);
90
- expect(result.success).toBe(true);
91
- if (result.success) expect(result.data.trustProxy).toBe(value);
92
- });
93
-
94
- it("reads a numeric string as a hop count, not as a one-entry address list", () => {
95
- const result = parsedTrustProxy("2");
96
- expect(result.success).toBe(true);
97
- if (result.success) expect(result.data.trustProxy).toBe(2);
98
- });
99
-
100
- it.each([
101
- ["a negative hop count", -1],
102
- ["a fractional hop count", 1.5],
103
- ["a hop count above the ceiling", 100_000],
104
- ])("refuses %s", (_what, value) => {
105
- expect(parsedTrustProxy(value).success).toBe(false);
106
- });
107
- });
108
-
109
- describe("http.trustProxy — address list", () => {
110
- it.each([
111
- [["10.0.0.7", "2001:db8::1"]],
112
- [["10.0.0.0/8", "fc00::/7"]],
113
- [["loopback"]],
114
- [["linklocal"]],
115
- [["uniquelocal"]],
116
- ])("accepts %j", (list) => {
117
- const result = parsedTrustProxy(list);
118
- expect(result.success).toBe(true);
119
- if (result.success) expect(result.data.trustProxy).toEqual(list);
120
- });
121
-
122
- it("splits a comma-separated string into entries, trimming each", () => {
123
- const result = parsedTrustProxy("10.0.0.0/8, 192.168.0.0/16");
124
- expect(result.success).toBe(true);
125
- if (result.success) expect(result.data.trustProxy).toEqual(["10.0.0.0/8", "192.168.0.0/16"]);
126
- });
127
-
128
- it("reads a single entry as a one-entry list", () => {
129
- const result = parsedTrustProxy("loopback");
130
- expect(result.success).toBe(true);
131
- if (result.success) expect(result.data.trustProxy).toEqual(["loopback"]);
132
- });
133
-
134
- it("refuses an empty list", () => {
135
- expect(parsedTrustProxy([]).success).toBe(false);
136
- });
137
-
138
- it("refuses a hostname, naming its index", () => {
139
- expect(issuePaths(parsedTrustProxy(["proxy.internal"]))).toContain("trustProxy.0");
140
- });
141
-
142
- it("names the offending index of a longer list", () => {
143
- expect(issuePaths(parsedTrustProxy(["loopback", "10.0.0.0/8", "10.0.0.0/33"]))).toContain(
144
- "trustProxy.2",
145
- );
146
- });
147
-
148
- it("refuses dotted-netmask notation, naming the prefix-length form", () => {
149
- const result = parsedTrustProxy(["10.0.0.0/255.0.0.0"]);
150
- expect(result.success).toBe(false);
151
- if (!result.success) {
152
- expect(result.error.issues.some((issue) => /prefix length/i.test(issue.message))).toBe(true);
153
- }
154
- });
155
-
156
- it.each([[[42]], [null], [{}]])("refuses %j", (value) => {
157
- expect(parsedTrustProxy(value).success).toBe(false);
158
- });
159
-
160
- it("is required", () => {
161
- const { trustProxy: _dropped, ...rest } = http();
162
- expect(issuePaths(httpSectionSchema.safeParse(rest))).toContain("trustProxy");
163
- });
164
- });
165
-
166
- describe("http.cors.allowedOrigins", () => {
167
- const parsed = (allowedOrigins: unknown) =>
168
- httpSectionSchema.safeParse(http({ cors: { allowedOrigins } }));
169
- const messagesOf = (result: ReturnType<typeof parsed>): string =>
170
- result.success ? "" : result.error.issues.map((issue) => issue.message).join("\n");
171
-
172
- it("accepts the empty list, CORS off", () => {
173
- const result = parsed([]);
174
- expect(result.success).toBe(true);
175
- if (result.success) expect(result.data.cors.allowedOrigins).toEqual([]);
176
- });
177
-
178
- it.each([
179
- "https://app.example.com",
180
- "https://app.example.com:8443",
181
- "http://localhost:5173",
182
- "http://127.0.0.1:5173",
183
- "http://[::1]:5173",
184
- ])("accepts the serialized origin %s", (origin) => {
185
- expect(parsed([origin]).success).toBe(true);
186
- });
187
-
188
- it.each([
189
- ["a trailing slash", "https://app.example.com/"],
190
- ["an explicit default port", "https://app.example.com:443"],
191
- ["an uppercase host", "https://APP.example.com"],
192
- ["a path", "https://app.example.com/callback"],
193
- ["a query", "https://app.example.com?x=1"],
194
- ["a fragment", "https://app.example.com#f"],
195
- ["userinfo", "https://user:pass@app.example.com"],
196
- ["a wildcard", "https://*.example.com"],
197
- ["a bare wildcard", "*"],
198
- ["plaintext off loopback", "http://app.example.com"],
199
- ["something unparsable", "not-an-origin"],
200
- ["the literal null origin", "null"],
201
- ["a custom app scheme with no tuple origin", "com.example.app://x"],
202
- ])("refuses %s", (_label, origin) => {
203
- expect(parsed([origin]).success).toBe(false);
204
- });
205
-
206
- it("names the key and the index of an entry that could never match", () => {
207
- const result = parsed(["https://ok.example.com", "https://app.example.com/"]);
208
- expect(messagesOf(result)).toContain("http.cors.allowedOrigins[1]");
209
- expect(issuePaths(result)).toEqual(["cors.allowedOrigins.1"]);
210
- });
211
-
212
- it("suggests the serialized form an entry was probably meant to be", () => {
213
- expect(messagesOf(parsed(["https://app.example.com/"]))).toContain('"https://app.example.com"');
214
- });
215
-
216
- it("says a wildcard matches nothing, rather than only refusing it", () => {
217
- expect(messagesOf(parsed(["https://*.example.com"]))).toMatch(/exact string equality/);
218
- });
219
-
220
- it("splits the variable's comma-separated string, trimming each entry and dropping a trailing comma", () => {
221
- const result = parsed(" https://app.example.com , http://localhost:5173 ,");
222
- expect(result.success).toBe(true);
223
- if (result.success) {
224
- expect(result.data.cors.allowedOrigins).toEqual([
225
- "https://app.example.com",
226
- "http://localhost:5173",
227
- ]);
228
- }
229
- });
230
-
231
- it("reads an exported-but-empty variable, and null, as no origins", () => {
232
- for (const value of ["", null]) {
233
- const result = parsed(value);
234
- expect(result.success).toBe(true);
235
- if (result.success) expect(result.data.cors.allowedOrigins).toEqual([]);
236
- }
237
- });
238
-
239
- it("checks every entry the string carried", () => {
240
- expect(parsed("https://app.example.com,https://*.example.com").success).toBe(false);
241
- });
242
-
243
- it.each([
244
- ["a number", 42],
245
- ["an object", { origin: "https://app.example.com" }],
246
- ["a boolean", true],
247
- ])("refuses %s, naming the key, both spellings and the new variable", (_label, value) => {
248
- const result = parsed(value);
249
- expect(issuePaths(result)).toEqual(["cors.allowedOrigins"]);
250
- expect(messagesOf(result)).toMatch(/list of origins/);
251
- expect(messagesOf(result)).toMatch(/comma-separated string/);
252
- expect(messagesOf(result)).toContain("HTTP_CORS_ALLOWED_ORIGINS");
253
- });
254
- });
255
-
256
- describe("http — the rest of the section", () => {
257
- it("reads the port and the readiness deadline from the variables' strings", () => {
258
- expect(
259
- httpSectionSchema.parse(http({ port: "8080", readinessTimeoutMs: "1500" })),
260
- ).toMatchObject({ port: 8080, readinessTimeoutMs: 1500 });
261
- });
262
-
263
- it.each([
264
- ["a blank variable", ""],
265
- ["zero", 0],
266
- ["a negative deadline", -1],
267
- ["a fractional deadline", 1.5],
268
- ["a deadline beyond Node's timer range", 2_147_483_648],
269
- ])("refuses %s as the readiness deadline", (_what, readinessTimeoutMs) => {
270
- expect(issuePaths(httpSectionSchema.safeParse(http({ readinessTimeoutMs })))).toEqual([
271
- "readinessTimeoutMs",
272
- ]);
273
- });
274
-
275
- describe("the readiness deadline is read in decimal digits", () => {
276
- const MESSAGE = "must be a whole number from 1 to 2147483647, in decimal digits";
277
- const messagesAt = (readinessTimeoutMs: unknown) =>
278
- (httpSectionSchema.safeParse(http({ readinessTimeoutMs })).error?.issues ?? [])
279
- .filter((issue) => issue.path.join(".") === "readinessTimeoutMs")
280
- .map((issue) => issue.message);
281
-
282
- it.each([
283
- ["0x10"],
284
- ["1e3"],
285
- ["5.0"],
286
- ["+5"],
287
- [true],
288
- [""],
289
- [" "],
290
- ["Infinity"],
291
- [0],
292
- [2_147_483_648],
293
- ])("refuses %j, naming the range", (readinessTimeoutMs) => {
294
- expect(messagesAt(readinessTimeoutMs)).toEqual([MESSAGE]);
295
- });
296
-
297
- it.each([[60], ["60"], [" 60 "]])("reads %j as 60", (readinessTimeoutMs) => {
298
- expect(httpSectionSchema.parse(http({ readinessTimeoutMs })).readinessTimeoutMs).toBe(60);
299
- });
300
- });
301
-
302
- it.each([
303
- [8080, 8080],
304
- [" 8080 ", 8080],
305
- [65_535, 65_535],
306
- ])("reads the port %j as %j", (port, read) => {
307
- expect(httpSectionSchema.parse(http({ port })).port).toBe(read);
308
- });
309
-
310
- it.each([0, "0"])("keeps an explicit %j as the port, the OS choosing a free one", (port) => {
311
- expect(httpSectionSchema.parse(http({ port })).port).toBe(0);
312
- });
313
-
314
- it.each([
315
- ["the empty string an exported-but-empty HTTP_PORT carries", ""],
316
- ["a blank variable", " "],
317
- ["hexadecimal", "0x50"],
318
- ["an exponent", "8e3"],
319
- ["a sign", "+80"],
320
- ["a negative number", -1],
321
- ["a fraction", 80.5],
322
- ["a number above 65535", 65_536],
323
- ["a number above 65535, as the variable's string", "65536"],
324
- ["null", null],
325
- ["true", true],
326
- ])("refuses %s as the port, naming http.port and HTTP_PORT", (_what, port) => {
327
- const result = httpSectionSchema.safeParse(http({ port }));
328
- expect(issuePaths(result)).toEqual(["port"]);
329
- const message = result.error?.issues.map((issue) => issue.message).join("\n");
330
- expect(message).toContain("http.port");
331
- expect(message).toContain("HTTP_PORT");
332
- });
333
-
334
- it.each([
335
- ["a key the section does not declare", http({ host: "0.0.0.0" })],
336
- ["a key cors does not declare", http({ cors: { allowedOrigins: [], credentials: true } })],
337
- ])("refuses %s", (_what, section) => {
338
- expect(httpSectionSchema.safeParse(section).success).toBe(false);
339
- });
340
- });
341
-
342
- /** A `local` block with every field an HS256 one takes, `overrides` laid over it. */
343
- const hs256 = (overrides: Record<string, unknown> = {}) => ({
344
- algorithm: "HS256",
345
- kid: "v0",
346
- secret: "s3cret",
347
- previousSecrets: [],
348
- ...overrides,
349
- });
350
-
351
- describe("key-store", () => {
352
- it("accepts provider local with its local block", () => {
353
- expect(keyStoreSectionSchema.parse({ provider: "local", local: hs256() })).toEqual({
354
- provider: "local",
355
- local: hs256(),
356
- });
357
- });
358
-
359
- it("requires provider", () => {
360
- expect(issuePaths(keyStoreSectionSchema.safeParse({ local: hs256() }))).toContain("provider");
361
- });
362
-
363
- it("refuses a block the section does not declare", () => {
364
- expect(
365
- keyStoreSectionSchema.safeParse({ provider: "kms", kms: { keyArn: "arn:aws:..." } }).success,
366
- ).toBe(false);
367
- });
368
-
369
- it("leaves the key material to the key store: HS256 without a secret parses", () => {
370
- expect(
371
- keyStoreSectionSchema.safeParse({
372
- provider: "local",
373
- local: { algorithm: "HS256", kid: "v0", previousSecrets: [] },
374
- }).success,
375
- ).toBe(true);
376
- });
377
-
378
- it("accepts HS256 rotation through previousSecrets, and refuses previousKeys under HS256", () => {
379
- const previous = { kid: "v-old", expiresAt: "2099-12-31T00:00:00Z" };
380
- expect(
381
- keyStoreSectionSchema.safeParse({
382
- provider: "local",
383
- local: hs256({ previousSecrets: [{ ...previous, secret: "old-secret" }] }),
384
- }).success,
385
- ).toBe(true);
386
- expect(
387
- keyStoreSectionSchema.safeParse({
388
- provider: "local",
389
- local: hs256({ previousKeys: [{ ...previous, publicKey: "...pem..." }] }),
390
- }).success,
391
- ).toBe(false);
392
- });
393
-
394
- it("accepts an asymmetric block carrying the secret the reference binds for every algorithm", () => {
395
- expect(
396
- keyStoreSectionSchema.safeParse({
397
- provider: "local",
398
- local: { algorithm: "EdDSA", kid: "v0", privateKey: "p", publicKey: "q", secret: "s" },
399
- }).success,
400
- ).toBe(true);
401
- });
402
-
403
- it("refuses a key an asymmetric block does not declare", () => {
404
- expect(
405
- keyStoreSectionSchema.safeParse({
406
- provider: "local",
407
- local: { algorithm: "EdDSA", kid: "v0", privateKeyPth: "/keys/p.pem" },
408
- }).success,
409
- ).toBe(false);
410
- });
411
-
412
- it("refuses an algorithm it does not know", () => {
413
- expect(
414
- keyStoreSectionSchema.safeParse({ provider: "local", local: hs256({ algorithm: "none" }) })
415
- .success,
416
- ).toBe(false);
417
- });
418
-
419
- describe.each([
420
- ["longer than MAX_KID_LENGTH", "k".repeat(MAX_KID_LENGTH + 1)],
421
- ["empty", ""],
422
- ["carrying a line feed", "v1\nv2"],
423
- ["not a string", 7],
424
- ])("a kid %s", (_label, kid) => {
425
- const parse = (local: Record<string, unknown>) =>
426
- keyStoreSectionSchema.safeParse({ provider: "local", local }).success;
427
- const ed25519 = { algorithm: "EdDSA", kid: "v0", privateKey: "p", publicKey: "q" };
428
- const at = "2030-01-01T00:00:00Z";
429
-
430
- it("is refused as the current kid, and in previousSecrets and previousKeys", () => {
431
- expect(parse(hs256({ kid }))).toBe(false);
432
- expect(parse(hs256({ previousSecrets: [{ kid, secret: "s", expiresAt: at }] }))).toBe(false);
433
- expect(parse({ ...ed25519, kid })).toBe(false);
434
- expect(parse({ ...ed25519, previousKeys: [{ kid, publicKey: "q", expiresAt: at }] })).toBe(
435
- false,
436
- );
437
- });
438
- });
439
- });
440
-
441
- describe("redis-clients", () => {
442
- it("accepts a URL, and a password beside it", () => {
443
- expect(
444
- redisClientsSectionSchema.parse({
445
- url: "redis://r:6379",
446
- password: "p",
447
- assumeNoEviction: false,
448
- }),
449
- ).toEqual({
450
- url: "redis://r:6379",
451
- password: "p",
452
- assumeNoEviction: false,
453
- });
454
- expect(
455
- redisClientsSectionSchema.parse({ url: "redis://r:6379", assumeNoEviction: false }),
456
- ).toEqual({
457
- url: "redis://r:6379",
458
- assumeNoEviction: false,
459
- });
460
- });
461
-
462
- it.each([
463
- [true, true],
464
- ["true", true],
465
- ["1", true],
466
- ["false", false],
467
- ["", false],
468
- ])("reads assumeNoEviction %j as %s", (given, read) => {
469
- expect(
470
- redisClientsSectionSchema.parse({ url: "redis://r:6379", assumeNoEviction: given })
471
- .assumeNoEviction,
472
- ).toBe(read);
473
- });
474
-
475
- it("refuses an assumeNoEviction it cannot read as a boolean, naming the key", () => {
476
- const parsed = redisClientsSectionSchema.safeParse({
477
- url: "redis://r:6379",
478
- assumeNoEviction: "yes",
479
- });
480
- expect(parsed.success).toBe(false);
481
- expect(parsed.error?.issues[0]?.path).toEqual(["assumeNoEviction"]);
482
- });
483
-
484
- it.each([
485
- ["no URL", {}],
486
- ["a null URL", { url: null }],
487
- [
488
- "a key the section does not declare",
489
- { url: "redis://r:6379", db: 2, assumeNoEviction: false },
490
- ],
491
- ["no assumeNoEviction, which the template's reference.conf ships", { url: "redis://r:6379" }],
492
- ])("refuses %s", (_what, section) => {
493
- expect(redisClientsSectionSchema.safeParse(section).success).toBe(false);
494
- });
495
- });
496
-
497
- describe("the in-process code repository", () => {
498
- const MESSAGE = "must be a whole number of at least 1, in decimal digits";
499
- const messagesAt = (defaultExpiresIn: unknown) =>
500
- (inMemoryCodeRepositorySectionSchema.safeParse({ defaultExpiresIn }).error?.issues ?? [])
501
- .filter((issue) => issue.path.join(".") === "defaultExpiresIn")
502
- .map((issue) => issue.message);
503
-
504
- it.each([["0x10"], ["1e3"], ["5.0"], ["+5"], [true], [""], [" "], ["Infinity"], [0], ["0"]])(
505
- "refuses %j as defaultExpiresIn, naming the range",
506
- (defaultExpiresIn) => {
507
- expect(messagesAt(defaultExpiresIn)).toEqual([MESSAGE]);
508
- },
509
- );
510
-
511
- it.each([[60], ["60"], [" 60 "]])("reads %j as defaultExpiresIn 60", (defaultExpiresIn) => {
512
- expect(inMemoryCodeRepositorySectionSchema.parse({ defaultExpiresIn }).defaultExpiresIn).toBe(
513
- 60,
514
- );
515
- });
516
- });