@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
@@ -41,7 +41,7 @@ function buildApp(probes: Parameters<ReturnType<typeof createMetrics>["route"]>[
41
41
  * Starts `app` on 127.0.0.1, the address the raw-socket tests dial. A hostless
42
42
  * `listen(0)` binds `[::]`, and on macOS the port it gets can be one another
43
43
  * process holds on 127.0.0.1 — the request then reaches that process and can
44
- * hang until the test times out (#556).
44
+ * hang until the test times out.
45
45
  */
46
46
  function listenOnLoopback(app: express.Express): Promise<ReturnType<express.Express["listen"]>> {
47
47
  return new Promise((resolve) => {
@@ -137,14 +137,22 @@ describe("GET /metrics", () => {
137
137
  it("counts a request whose client disconnects before the response finishes", async () => {
138
138
  // `close` fires without `finish` when a client or proxy gives up
139
139
  // mid-handler, and those are disproportionately the slow and failing
140
- // requests RED metrics exist to surface.
140
+ // requests RED metrics exist to surface. Sequenced on the server's own
141
+ // events, never on a fixed wait: the abort goes out once the handler
142
+ // holds the request, and /metrics is read once the response has closed.
141
143
  const metrics = createMetrics();
142
144
  const app = express();
143
145
  app.use(metrics.middleware);
144
146
  app.use(metrics.route(express, { probes: [], probeTimeoutMs: 100 }));
147
+ const handling = Promise.withResolvers<void>();
148
+ const closed = Promise.withResolvers<void>();
145
149
  app.get("/slow", (_req, res) => {
146
- // Never responds; the test aborts the socket instead.
147
- void res;
150
+ // Never responds; the test aborts the socket instead. This listener is
151
+ // added after the metrics middleware's, and a response emits `close`
152
+ // to its listeners in the order they were added, so once it runs the
153
+ // request has been observed.
154
+ res.once("close", () => closed.resolve());
155
+ handling.resolve();
148
156
  });
149
157
 
150
158
  const server = await listenOnLoopback(app);
@@ -154,17 +162,19 @@ describe("GET /metrics", () => {
154
162
  const pending = fetch(`http://127.0.0.1:${port}/slow`, {
155
163
  signal: controller.signal,
156
164
  }).catch(() => undefined);
157
- await new Promise((resolve) => setTimeout(resolve, 30));
165
+ await handling.promise;
158
166
  controller.abort();
159
167
  await pending;
160
- await new Promise((resolve) => setTimeout(resolve, 30));
168
+ await closed.promise;
161
169
 
162
170
  const res = await request(app).get("/metrics");
163
171
  const slow = res.text
164
172
  .split("\n")
165
173
  .filter((l) => l.startsWith("http_request_duration_seconds_count"))
166
174
  .filter((l) => l.includes('route="/slow"'));
167
- expect(slow.length).toBeGreaterThan(0);
175
+ // One request, counted once, under the route it had reached.
176
+ expect(slow).toHaveLength(1);
177
+ expect(slow[0]?.trim().endsWith(" 1")).toBe(true);
168
178
  } finally {
169
179
  server.close();
170
180
  }
@@ -0,0 +1,123 @@
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 template's MFA switch under the name production, where the MFA stores
19
+ * must be shared: both on Redis, as a production deployment selects them.
20
+ * ioredis is a stand-in, as in `all-modules-composition.multi.test.mts`:
21
+ * nothing here issues a command, and the stores' durability check names what
22
+ * it could not read and goes on. `packages/redis` runs the stores against a
23
+ * real server.
24
+ */
25
+
26
+ import { BootError } from "@o3co/auth-provider-core";
27
+ import { MFA_DEVELOPMENT_SAMPLE_KEY } from "@o3co/auth-provider-mfa";
28
+ import { afterEach, describe, expect, it, vi } from "vitest";
29
+ import { type Composition, compose, SINGLE_ENV } from "./all-modules-composition.fixture.mjs";
30
+
31
+ vi.mock("ioredis", () => {
32
+ // Constructible, quits cleanly, answers the readiness probe's `ping`;
33
+ // every other command resolves to nothing.
34
+ const explicit: Record<string, unknown> = {
35
+ on: () => undefined,
36
+ quit: async () => "OK",
37
+ disconnect: () => undefined,
38
+ ping: async () => "PONG",
39
+ };
40
+ const makeMockRedis = (): object =>
41
+ new Proxy(
42
+ {},
43
+ {
44
+ get(_target, prop) {
45
+ // A function-valued `then` would make the instance a thenable.
46
+ if (typeof prop !== "string" || prop === "then") return undefined;
47
+ if (prop === "duplicate") return makeMockRedis;
48
+ if (prop in explicit) return explicit[prop];
49
+ return async () => null;
50
+ },
51
+ },
52
+ );
53
+ function MockRedis(): object {
54
+ return makeMockRedis();
55
+ }
56
+ return { Redis: MockRedis, default: MockRedis };
57
+ });
58
+
59
+ let current: Composition | undefined;
60
+
61
+ afterEach(async () => {
62
+ await current?.handle.dispose();
63
+ current = undefined;
64
+ });
65
+
66
+ /** MFA required in production, both MFA stores on Redis, a key of the deployment's own and an SMTP relay. */
67
+ const PRODUCTION_ENV: Readonly<Record<string, string>> = {
68
+ ...SINGLE_ENV,
69
+ MFA_MODE: "required",
70
+ ADAPTERS_MFA_FACTOR_STORE: "redis",
71
+ ADAPTERS_MFA_TRANSACTION_STORE: "redis",
72
+ REDIS_CLIENTS_URL: "redis://redis.test:6379",
73
+ MFA_ENCRYPTION_KEY: Buffer.alloc(32, 9).toString("base64"),
74
+ STANDARD_SMTP_MAIL_SENDER_HOST: "smtp.auth.test",
75
+ STANDARD_SMTP_MAIL_SENDER_FROM: "auth@auth.test",
76
+ };
77
+
78
+ /** An error's message with every cause's under it. */
79
+ const textOf = (err: unknown): string => {
80
+ const parts: string[] = [];
81
+ for (let at: unknown = err; at instanceof Error; at = at.cause) parts.push(at.message);
82
+ return parts.join(" <- ");
83
+ };
84
+
85
+ const refusal = (composing: Promise<Composition>): Promise<unknown> =>
86
+ composing.then(
87
+ (composition) => {
88
+ current = composition;
89
+ return undefined;
90
+ },
91
+ (caught: unknown) => caught,
92
+ );
93
+
94
+ describe("MFA_MODE=required under the name production, the MFA stores on Redis", () => {
95
+ it("boots, the requirement registered as the second-factor authority", async () => {
96
+ current = await compose({ env: PRODUCTION_ENV, environment: "production" });
97
+ const names = current.modules.map((m) => m.name);
98
+ expect(names).toContain("redis-mfa-factor-store");
99
+ expect(names).toContain("redis-mfa-transaction-store");
100
+ expect(
101
+ current.handle.components.sessionRequirementResolver?.get("mfa")?.secondFactorAuthority,
102
+ ).toBe(true);
103
+ });
104
+
105
+ it("refuses the development sample key, naming MFA_ENCRYPTION_KEY and quoting no key", async () => {
106
+ const err = await refusal(
107
+ compose({
108
+ env: { ...PRODUCTION_ENV, MFA_ENCRYPTION_KEY: MFA_DEVELOPMENT_SAMPLE_KEY },
109
+ environment: "production",
110
+ }),
111
+ );
112
+ expect(err).toBeInstanceOf(BootError);
113
+ expect(textOf(err)).toContain("MFA_ENCRYPTION_KEY");
114
+ expect(textOf(err)).not.toContain(MFA_DEVELOPMENT_SAMPLE_KEY);
115
+ });
116
+
117
+ it("refuses without an SMTP relay, naming STANDARD_SMTP_MAIL_SENDER_HOST", async () => {
118
+ const { STANDARD_SMTP_MAIL_SENDER_HOST: _host, ...env } = PRODUCTION_ENV;
119
+ const err = await refusal(compose({ env, environment: "production" }));
120
+ expect(err).toBeInstanceOf(BootError);
121
+ expect(textOf(err)).toContain("STANDARD_SMTP_MAIL_SENDER_HOST");
122
+ });
123
+ });