@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
@@ -7,10 +7,10 @@
7
7
  "test": "vitest run"
8
8
  },
9
9
  "dependencies": {
10
- "axios": "^1.6.0",
10
+ "axios": "^1.18.0",
11
11
  "jsonwebtoken": "^9.0.3"
12
12
  },
13
13
  "devDependencies": {
14
- "vitest": "^1.0.0"
14
+ "vitest": "^5.0.2"
15
15
  }
16
16
  }
@@ -0,0 +1,26 @@
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
+ // This directory is its own npm project, run against a live provider. Its own
18
+ // config fixes what this project collects; no config of the template's around
19
+ // it decides that, whichever vitest the lockfile carries.
20
+ import { defineConfig } from "vitest/config";
21
+
22
+ export default defineConfig({
23
+ test: {
24
+ include: ["*.test.js"],
25
+ },
26
+ });
@@ -3,7 +3,7 @@ import { defineConfig } from "vitest/config";
3
3
  export default defineConfig({
4
4
  test: {
5
5
  include: ["src/**/__tests__/**/*.test.mts"],
6
- // #357: the workspace-wide deadline floor, repeated as literals rather
6
+ // The workspace-wide deadline floor, repeated as literals rather
7
7
  // than imported from vitest.shared.mts — this template is copied
8
8
  // verbatim into scaffolded projects, where that file does not exist.
9
9
  // Rationale lives there: under a loaded parallel run, transform+import
@@ -11,7 +11,7 @@ export default defineConfig({
11
11
  // 20s.
12
12
  testTimeout: 20_000,
13
13
  hookTimeout: 20_000,
14
- // #512: run the @o3co/auth-provider-* packages through vitest instead of
14
+ // Run the @o3co/auth-provider-* packages through vitest instead of
15
15
  // letting Node load them natively. In a scaffolded project they are
16
16
  // installed from npm under node_modules, which vitest externalizes by
17
17
  // default — and an externalized package's own `import "ioredis"` /
@@ -20,11 +20,12 @@ export default defineConfig({
20
20
  // monorepo the same packages are symlinks to source and are inlined
21
21
  // anyway; this makes the two layouts behave the same.
22
22
  server: { deps: { inline: [/@o3co\/auth-provider-/] } },
23
- // #556: the server `request(app)` starts listens on 127.0.0.1, the
24
- // address supertest dials. Unpatched, macOS can give it a port another
25
- // process holds on 127.0.0.1, and the request hangs on that process until
26
- // the test timeout. Rationale and mechanics in the file itself. The
27
- // Dockerfile `test` stage copies it; scaffold-assets.test.mts checks that.
23
+ // The supertest loopback guard: the server `request(app)` starts
24
+ // listens on 127.0.0.1, the address supertest dials. Unpatched, macOS
25
+ // can give it a port another process holds on 127.0.0.1, and the
26
+ // request hangs on that process until the test timeout. Rationale and
27
+ // mechanics in the file itself. The Dockerfile `test` stage copies it;
28
+ // scaffold-assets.test.mts checks that.
28
29
  setupFiles: ["./vitest.supertest-loopback.mts"],
29
30
  },
30
31
  });
@@ -15,50 +15,47 @@
15
15
  */
16
16
 
17
17
  /**
18
- * vitest setup file (#556): the server supertest starts listens on the
19
- * address supertest dials.
18
+ * vitest setup file: the server supertest starts listens on the address
19
+ * supertest dials.
20
20
  *
21
21
  * `request(app)` / `request.agent(app)` start a server with `app.listen(0)`
22
22
  * and send the request to `127.0.0.1:<port>`. A hostless `listen` binds the
23
- * dual-stack wildcard `[::]:P`, and on macOS (BSD `SO_REUSEADDR` semantics —
23
+ * dual-stack wildcard `[::]:P`, and on macOS (BSD `SO_REUSEADDR` semantics;
24
24
  * libuv sets it on every TCP listener) the kernel will choose a `P` that
25
25
  * another process already holds as `127.0.0.1:P`. The more specific socket
26
- * wins, so the test's request is delivered to that process. When it accepts
27
- * and never answers — an editor helper, in the case that was caught — the
28
- * request hangs until the test timeout, in whichever supertest test drew the
29
- * port, and passes on the next run. Linux refuses the conflicting bind
30
- * (EADDRINUSE), so CI does not show it.
26
+ * wins, so the request goes to that process; if it accepts and never answers,
27
+ * the test hangs until its timeout and passes on the next run. Linux refuses
28
+ * the conflicting bind (EADDRINUSE), so CI does not show it.
31
29
  *
32
30
  * Binding `127.0.0.1` explicitly removes both halves: the kernel will not
33
31
  * give out a `127.0.0.1:P` someone else holds, and against a wildcard holder
34
32
  * our specific socket is the one that receives. A specific-host `listen` is
35
- * asynchronous (it goes through `dns.lookup`), and supertest reads the port
33
+ * asynchronous (it goes through `dns.lookup`) and supertest reads the port
36
34
  * synchronously in its constructor, so the URL is completed when the request
37
35
  * is sent (`end`, which `then` and `expect(..., fn)` go through). A server the
38
36
  * test started itself is never touched.
39
37
  *
40
- * Which version of supertest closes the server it started, and when, has
41
- * changed under this file once (7.3.0): 7.2 closed `_server` before asserting;
42
- * 7.3 tracks the servers it started itself in a private registry and closes
43
- * only those, so a server this file bound is left listening. The patched
44
- * `end` therefore closes the server this request started, before the test's
45
- * callback, whenever supertest has not — the same order either way.
38
+ * supertest does not close a server bound here (7.3 counts requests only on
39
+ * servers in its own registry; 7.2 closed after the response of the request
40
+ * that bound it, resetting an agent's other requests on the shared server).
41
+ * So this file counts every request on such a server from `end`, and the last
42
+ * to settle closes the server before its own callback, the order supertest
43
+ * keeps. A request built before that close and sent after it binds the server
44
+ * again.
46
45
  *
47
46
  * supertest is loaded from the package that owns the running test file (the
48
47
  * nearest `package.json` above it), so the patched class is the one that test
49
- * imports, whatever directory vitest was started from. A package that does
50
- * not declare supertest is left alone. Everything else this file cannot do —
51
- * no test path, a declared supertest that does not load, a supertest whose
52
- * internals no longer look like this — throws, so the guard is never silently
53
- * off. That load-time check sees only the two methods' presence; a supertest
54
- * that moved the `listen` out of `serverAddress` would pass it and leave the
55
- * guard off. The behavioural tests catch that shape of change: this
56
- * template's `supertest-loopback.test.mts` and its twin in the session
57
- * package assert the address the server actually bound.
48
+ * imports. A package that does not declare supertest is left alone. Anything
49
+ * else this file cannot do (no test path, a declared supertest that does not
50
+ * load, internals that no longer look like this) throws, so the guard is
51
+ * never silently off. That load-time check sees only the two methods'
52
+ * presence; a supertest that moved the `listen` out of `serverAddress` would
53
+ * pass it, which `supertest-loopback.test.mts` here and its twin in the
54
+ * session package catch by asserting the address the server actually bound.
58
55
  *
59
- * This file is part of the project template and ships with every scaffold.
60
- * The auth.provider workspace loads this same file for its packages (see
61
- * `WORKSPACE_TEST_SETUP` in its `vitest.shared.mts`).
56
+ * Part of the project template, shipped with every scaffold; the
57
+ * auth.provider workspace loads it for its packages too
58
+ * (`WORKSPACE_TEST_SETUP` in its `vitest.shared.mts`).
62
59
  */
63
60
 
64
61
  import { once } from "node:events";
@@ -73,17 +70,28 @@ const LOOPBACK = "127.0.0.1";
73
70
  const PATCHED = Symbol.for("o3co.auth.provider.supertest-loopback");
74
71
  const PENDING = Symbol("o3co.auth.provider.supertest-loopback.pending");
75
72
 
73
+ /**
74
+ * A server this file bound, from its `listen` until the last request on it
75
+ * settles. It is the server's current bind for as long as `bound` maps the
76
+ * server to it; a request built on one that no longer is — closed by its last
77
+ * request, or failed — binds again when it is sent.
78
+ */
79
+ type Bound = {
80
+ /** Settles on `listening`, rejects on a listen `error`. */
81
+ readonly ready: Promise<unknown>;
82
+ /** The requests sent over it that have not settled. */
83
+ pending: number;
84
+ };
85
+
76
86
  type Pending = {
77
87
  readonly server: Server;
78
88
  readonly protocol: string;
79
89
  readonly path: string;
80
- /** Settles on `listening`, rejects on a listen `error`. */
81
- readonly ready: Promise<unknown>;
90
+ readonly bound: Bound;
82
91
  };
83
92
 
84
93
  type TestInstance = {
85
94
  url: string;
86
- _server?: Server;
87
95
  [PENDING]?: Pending;
88
96
  };
89
97
 
@@ -126,7 +134,7 @@ function supertestFor(testPath: string): TestPrototype | undefined {
126
134
  supertest = createRequire(manifestPath)("supertest");
127
135
  } catch (cause) {
128
136
  throw new Error(
129
- `vitest.supertest-loopback.mts: ${manifestPath} declares supertest, but it cannot be loaded from there; the #556 loopback guard would be off.`,
137
+ `vitest.supertest-loopback.mts: ${manifestPath} declares supertest, but it cannot be loaded from there; the supertest loopback guard would be off.`,
130
138
  { cause },
131
139
  );
132
140
  }
@@ -134,19 +142,46 @@ function supertestFor(testPath: string): TestPrototype | undefined {
134
142
  if (typeof proto?.serverAddress !== "function" || typeof proto.end !== "function") {
135
143
  throw new Error(
136
144
  "vitest.supertest-loopback.mts: supertest no longer exposes Test.prototype.serverAddress/end; " +
137
- "re-check the #556 loopback guard against the installed supertest before removing it.",
145
+ "re-check the supertest loopback guard against the installed supertest before removing it.",
138
146
  );
139
147
  }
140
148
  return proto;
141
149
  }
142
150
 
143
151
  /**
144
- * The bind in progress per server. A second request on a server whose bind has
145
- * not finished (an agent's requests sent together share one server) waits on
146
- * the same bind instead of calling `listen` twice — and, like unpatched
147
- * supertest when it finds a server already listening, does not own it.
152
+ * The servers this file bound, each until the last request on it closes it.
153
+ * A request on a server whose bind has not finished (an agent's requests sent
154
+ * together share one server) waits on that bind instead of calling `listen`
155
+ * twice, and one sent while the server is serving others joins their count.
148
156
  */
149
- const binding = new WeakMap<Server, Promise<unknown>>();
157
+ const bound = new WeakMap<Server, Bound>();
158
+
159
+ /** The server's bind in progress or in service, or a new one. */
160
+ function share(server: Server): Bound {
161
+ const current = bound.get(server);
162
+ if (current) return current;
163
+ server.listen(0, LOOPBACK);
164
+ // Subscribed now, not in `end`: a listen error emitted before the test
165
+ // sends the request must still reach it, not crash the worker as an
166
+ // unhandled 'error' event.
167
+ const fresh: Bound = { ready: once(server, "listening"), pending: 0 };
168
+ bound.set(server, fresh);
169
+ // Once it has failed, the bind is left to the requests already waiting on
170
+ // it, each of which fails with its error, and is no longer the server's:
171
+ // the next request sent binds again, one built on the failed bind included.
172
+ // Forgotten on the error event itself, and not on the rejection it becomes
173
+ // — that settles a few microtasks later, and a request built in between
174
+ // would join the failed bind.
175
+ const failed = (): void => {
176
+ if (bound.get(server) === fresh) bound.delete(server);
177
+ };
178
+ server.once("error", failed);
179
+ fresh.ready.then(
180
+ () => server.removeListener("error", failed),
181
+ () => {},
182
+ );
183
+ return fresh;
184
+ }
150
185
 
151
186
  /**
152
187
  * A throw from the test's own callback, raised where it cannot propagate to
@@ -165,27 +200,14 @@ function patch(proto: TestPrototype): void {
165
200
  const originalEnd = proto.end;
166
201
 
167
202
  proto.serverAddress = function serverAddress(app, path) {
168
- // Already listening: the test owns the server and chose its address.
169
- if (app.address()) return originalServerAddress.call(this, app, path);
203
+ // Listening, and not bound here: the test owns the server and chose its
204
+ // address.
205
+ if (!bound.has(app) && app.address()) return originalServerAddress.call(this, app, path);
170
206
 
207
+ // supertest is never told about this bind — neither 7.2's `_server` nor
208
+ // 7.3's registry — so neither closes the server; the patched `end` does.
171
209
  const protocol = app instanceof TlsServer ? "https" : "http";
172
- let ready = binding.get(app);
173
- if (!ready) {
174
- // supertest 7.2 closes `_server` after the response; 7.3 does not,
175
- // and the callback in the patched `end` below closes it instead.
176
- this._server = app.listen(0, LOOPBACK);
177
- // Subscribed now, not in `end`: a listen error emitted before the test
178
- // sends the request must still reach it, not crash the worker as an
179
- // unhandled 'error' event.
180
- const bound = once(app, "listening");
181
- binding.set(app, bound);
182
- bound.then(
183
- () => binding.delete(app),
184
- () => binding.delete(app),
185
- );
186
- ready = bound;
187
- }
188
- this[PENDING] = { server: app, protocol, path, ready };
210
+ this[PENDING] = { server: app, protocol, path, bound: share(app) };
189
211
  // No port yet; `end` fills it in before anything is sent.
190
212
  return `${protocol}://${LOOPBACK}${path}`;
191
213
  };
@@ -195,17 +217,24 @@ function patch(proto: TestPrototype): void {
195
217
  if (!pending) return originalEnd.call(this, fn);
196
218
  this[PENDING] = undefined;
197
219
 
198
- const { server, protocol, path, ready } = pending;
220
+ const { server, protocol, path } = pending;
221
+ // Built on a bind that is no longer the server's — closed by its last
222
+ // request, or failed — and sent now: bind it again, or join the bind
223
+ // another request has made since.
224
+ const shared = bound.get(server) === pending.bound ? pending.bound : share(server);
225
+ shared.pending += 1;
199
226
  let called = false;
200
227
  const callback = (err: unknown, res?: unknown) => {
201
228
  called = true;
202
- // supertest 7.2 has closed the server it started by now, before the
203
- // callback; 7.3 closes only servers in its own private registry, which
204
- // this bind is not in. Close what this request started, if it is
205
- // still open, before the callback — the order 7.2 kept.
206
- if (this._server === server && server.listening) {
207
- server.close(() => fn?.(err, res));
208
- return;
229
+ // The last request on the server closes it, before its own callback —
230
+ // the order supertest keeps. A bind that failed has nothing to close.
231
+ shared.pending -= 1;
232
+ if (shared.pending === 0 && bound.get(server) === shared) {
233
+ bound.delete(server);
234
+ if (server.listening) {
235
+ server.close(() => fn?.(err, res));
236
+ return;
237
+ }
209
238
  }
210
239
  fn?.(err, res);
211
240
  };
@@ -218,16 +247,15 @@ function patch(proto: TestPrototype): void {
218
247
  // this chain is one nobody awaits.
219
248
  const fail = (err: unknown) => {
220
249
  if (called) return rethrowOutsideThisChain(err);
221
- // The request never went out, so supertest will not close the server
222
- // this request started.
223
- if (this._server?.listening) this._server.close();
250
+ // The request never went out; it still leaves the count, and closes
251
+ // the server if it was the last one on it.
224
252
  try {
225
253
  callback(err);
226
254
  } catch (thrown) {
227
255
  rethrowOutsideThisChain(thrown);
228
256
  }
229
257
  };
230
- ready
258
+ shared.ready
231
259
  .then(() => {
232
260
  const { port } = server.address() as { port: number };
233
261
  this.url = `${protocol}://${LOOPBACK}:${port}${path}`;
@@ -243,7 +271,7 @@ function patch(proto: TestPrototype): void {
243
271
  const { testPath } = expect.getState();
244
272
  if (!testPath) {
245
273
  throw new Error(
246
- "vitest.supertest-loopback.mts: vitest did not say which test file this setup runs for, so the #556 loopback guard cannot find supertest.",
274
+ "vitest.supertest-loopback.mts: vitest did not say which test file this setup runs for, so the supertest loopback guard cannot find supertest.",
247
275
  );
248
276
  }
249
277
  const proto = supertestFor(testPath);
@@ -1,17 +1,20 @@
1
1
  {
2
- "@o3co/auth-provider-core": "0.15.0",
3
- "@o3co/auth-provider-device-grant": "0.15.0",
4
- "@o3co/auth-provider-dpop": "0.15.0",
5
- "@o3co/auth-provider-mtls": "0.15.0",
6
- "@o3co/auth-provider-federation-apple": "0.15.0",
7
- "@o3co/auth-provider-federation-google": "0.15.0",
8
- "@o3co/auth-provider-federation-github": "0.15.0",
9
- "@o3co/auth-provider-federation-grants": "0.15.0",
10
- "@o3co/auth-provider-federation-oidc": "0.15.0",
11
- "@o3co/auth-provider-oauth": "0.15.0",
12
- "@o3co/auth-provider-oauth-token-exchange": "0.15.0",
13
- "@o3co/auth-provider-session": "0.15.0",
14
- "@o3co/auth-provider-foundation": "0.15.0",
15
- "@o3co/auth-provider-redis": "0.15.0",
16
- "@o3co/auth-provider-webauthn": "0.15.0"
2
+ "@o3co/auth-provider-core": "0.17.0-rc.1",
3
+ "@o3co/auth-provider-device-grant": "0.17.0-rc.1",
4
+ "@o3co/auth-provider-dpop": "0.17.0-rc.1",
5
+ "@o3co/auth-provider-mtls": "0.17.0-rc.1",
6
+ "@o3co/auth-provider-federation-apple": "0.17.0-rc.1",
7
+ "@o3co/auth-provider-federation-google": "0.17.0-rc.1",
8
+ "@o3co/auth-provider-federation-github": "0.17.0-rc.1",
9
+ "@o3co/auth-provider-federation-grants": "0.17.0-rc.1",
10
+ "@o3co/auth-provider-federation-oidc": "0.17.0-rc.1",
11
+ "@o3co/auth-provider-oauth": "0.17.0-rc.1",
12
+ "@o3co/auth-provider-oauth-token-exchange": "0.17.0-rc.1",
13
+ "@o3co/auth-provider-session": "0.17.0-rc.1",
14
+ "@o3co/auth-provider-foundation": "0.17.0-rc.1",
15
+ "@o3co/auth-provider-mfa": "0.17.0-rc.1",
16
+ "@o3co/auth-provider-redis": "0.17.0-rc.1",
17
+ "@o3co/auth-provider-standard": "0.17.0-rc.1",
18
+ "@o3co/auth-provider-test-kit": "0.17.0-rc.1",
19
+ "@o3co/auth-provider-webauthn": "0.17.0-rc.1"
17
20
  }
@@ -1,96 +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
- import type { Logger } from "@o3co/auth-provider-core";
17
- import express from "express";
18
- import request from "supertest";
19
- import { describe, expect, it, vi } from "vitest";
20
- import { createTerminalErrorHandler } from "../terminalError.mjs";
21
-
22
- const makeLogger = () => {
23
- const error = vi.fn();
24
- const logger = {
25
- error,
26
- warn: vi.fn(),
27
- info: vi.fn(),
28
- debug: vi.fn(),
29
- } as unknown as Logger;
30
- return { logger, error };
31
- };
32
-
33
- const makeApp = (logger: Logger) => {
34
- const app = express();
35
- app.use(express.json());
36
- app.post("/echo", (_req, res) => {
37
- res.status(200).json({ ok: true });
38
- });
39
- app.get("/boom", () => {
40
- throw new Error("route exploded");
41
- });
42
- app.use(createTerminalErrorHandler(logger));
43
- return app;
44
- };
45
-
46
- describe("terminal error handler (#293 item 8)", () => {
47
- it("answers malformed JSON with the client's 400 in the shared envelope, not Express's HTML page", async () => {
48
- const { logger, error } = makeLogger();
49
- const res = await request(makeApp(logger))
50
- .post("/echo")
51
- .set("Content-Type", "application/json")
52
- .send("{not json");
53
-
54
- expect(res.status).toBe(400);
55
- expect(res.headers["content-type"]).toContain("application/json");
56
- expect(res.body.error).toBe("invalid_request");
57
- // The client's malformed body is not a server error worth an error line.
58
- expect(error).not.toHaveBeenCalled();
59
- });
60
-
61
- it("keeps the body parser's own status for an over-limit body (413)", async () => {
62
- const { logger } = makeLogger();
63
- const app = express();
64
- app.use(express.json({ limit: 16 }));
65
- app.post("/echo", (_req, res) => {
66
- res.status(200).json({ ok: true });
67
- });
68
- app.use(createTerminalErrorHandler(logger));
69
-
70
- const res = await request(app)
71
- .post("/echo")
72
- .set("Content-Type", "application/json")
73
- .send(JSON.stringify({ pad: "x".repeat(64) }));
74
-
75
- expect(res.status).toBe(413);
76
- expect(res.body.error).toBe("invalid_request");
77
- expect(res.body.error_description).toBe("request body too large");
78
- });
79
-
80
- it("answers an unexpected route throw with 500 server_error and logs it structurally", async () => {
81
- const { logger, error } = makeLogger();
82
- const res = await request(makeApp(logger)).get("/boom");
83
-
84
- expect(res.status).toBe(500);
85
- expect(res.body).toEqual({
86
- error: "server_error",
87
- error_description: "Internal server error",
88
- });
89
- // No stack trace on the wire — the envelope is the whole body.
90
- expect(JSON.stringify(res.body)).not.toContain("route exploded");
91
- expect(error).toHaveBeenCalledWith(
92
- expect.objectContaining({ endpoint: "/boom" }),
93
- "unhandled_request_error",
94
- );
95
- });
96
- });
@@ -1,60 +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
- import { errorEnvelope, type Logger } from "@o3co/auth-provider-core";
17
- import type { ErrorRequestHandler } from "express";
18
-
19
- /**
20
- * Terminal Express error handler (#293 item 8).
21
- *
22
- * Anything a route threw past its own try/catch — and every body-parser
23
- * rejection, which fires before any route runs — used to fall through to
24
- * Express's default handler: an HTML 500 outside the structured-log pipeline,
25
- * carrying a stack trace outside production. A client that parses only the
26
- * RFC 6749 §5.2 JSON envelope this surface answers with everywhere else has
27
- * no way to read that page.
28
- *
29
- * Body-parser failures (malformed JSON/form, over-limit, bad charset) carry
30
- * their own 4xx `status` and are the client's fault: keep the status, wrap it
31
- * in the shared envelope, and do not log them as server errors. Everything
32
- * else is logged with the request path and answered `500 server_error`.
33
- *
34
- * Mount it AFTER every route (`app.use(handle.router)` included) — Express
35
- * routes errors only to handlers registered later.
36
- */
37
- export const createTerminalErrorHandler = (logger: Logger): ErrorRequestHandler => {
38
- return (err, req, res, next) => {
39
- // A failure after the response started is not ours to rewrite; handing
40
- // it back lets Express close the connection.
41
- if (res.headersSent) {
42
- next(err);
43
- return;
44
- }
45
- const status = (err as { status?: unknown; statusCode?: unknown } | null) ?? {};
46
- const httpStatus =
47
- typeof status.status === "number"
48
- ? status.status
49
- : typeof status.statusCode === "number"
50
- ? status.statusCode
51
- : undefined;
52
- if (httpStatus !== undefined && httpStatus >= 400 && httpStatus < 500) {
53
- const description = httpStatus === 413 ? "request body too large" : "malformed request body";
54
- res.status(httpStatus).json(errorEnvelope("invalid_request", description));
55
- return;
56
- }
57
- logger.error({ err, endpoint: req.path }, "unhandled_request_error");
58
- res.status(500).json(errorEnvelope("server_error", "Internal server error"));
59
- };
60
- };