@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.
- package/README.ja.md +139 -48
- package/README.md +138 -47
- package/dist/index.d.mts +33 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +118 -59
- package/dist/internal/template-filter.d.mts +7 -10
- package/dist/internal/template-filter.d.mts.map +1 -1
- package/dist/internal/template-filter.mjs +7 -10
- package/package.json +6 -4
- package/templates/standalone/.dockerignore +15 -2
- package/templates/standalone/.env.example +69 -24
- package/templates/standalone/Dockerfile +7 -3
- package/templates/standalone/README.ja.md +677 -184
- package/templates/standalone/README.md +699 -238
- package/templates/standalone/config/application.conf +203 -325
- package/templates/standalone/config/clients.yaml +2 -0
- package/templates/standalone/config/clients.yaml.example +4 -0
- package/templates/standalone/config/development.conf +13 -4
- package/templates/standalone/config/mailpit.conf +8 -0
- package/templates/standalone/config/production.conf +5 -6
- package/templates/standalone/config/reference.conf +399 -0
- package/templates/standalone/docker-compose.mailpit.yml +36 -0
- package/templates/standalone/docker-compose.production.yml +58 -30
- package/templates/standalone/docker-compose.yml +13 -3
- package/templates/standalone/gitignore +4 -2
- package/templates/standalone/package.json +9 -6
- package/templates/standalone/src/__tests__/access-token-lifetime-alias.test.mts +65 -32
- package/templates/standalone/src/__tests__/adapters.test.mts +358 -0
- package/templates/standalone/src/__tests__/all-modules-composition.fixture.mts +1178 -0
- package/templates/standalone/src/__tests__/all-modules-composition.multi.test.mts +256 -0
- package/templates/standalone/src/__tests__/all-modules-composition.test.mts +1119 -0
- package/templates/standalone/src/__tests__/audit-sink.test.mts +78 -76
- package/templates/standalone/src/__tests__/configPath.test.mts +13 -11
- package/templates/standalone/src/__tests__/core-federations.test.mts +226 -0
- package/templates/standalone/src/__tests__/core-sections.test.mts +190 -0
- package/templates/standalone/src/__tests__/device-code-store-client-module.test.mts +9 -14
- package/templates/standalone/src/__tests__/documented-env-overrides.test.mts +672 -271
- package/templates/standalone/src/__tests__/federation-grants-composition.test.mts +480 -78
- package/templates/standalone/src/__tests__/federation-grants-survive-logout.test.mts +102 -66
- package/templates/standalone/src/__tests__/federation-token-refresh-outage.test.mts +135 -0
- package/templates/standalone/src/__tests__/google-federation-config-module.test.mts +49 -18
- package/templates/standalone/src/__tests__/google-session.fixture.mts +315 -0
- package/templates/standalone/src/__tests__/host-routes.test.mts +134 -0
- package/templates/standalone/src/__tests__/library-references.fixture.mts +183 -0
- package/templates/standalone/src/__tests__/listen.test.mts +121 -0
- package/templates/standalone/src/__tests__/logger-projection.test.mts +77 -0
- package/templates/standalone/src/__tests__/mail-sender.test.mts +134 -0
- package/templates/standalone/src/__tests__/mailpit-compose.test.mts +328 -0
- package/templates/standalone/src/__tests__/metrics.test.mts +17 -7
- package/templates/standalone/src/__tests__/mfa-switch.redis.test.mts +123 -0
- package/templates/standalone/src/__tests__/mfa-switch.test.mts +780 -0
- package/templates/standalone/src/__tests__/oidc-federation-config-module.test.mts +52 -46
- package/templates/standalone/src/__tests__/own-modules.test.mts +1457 -0
- package/templates/standalone/src/__tests__/post-logout-redirect.test.mts +183 -0
- package/templates/standalone/src/__tests__/redis-connection-error-log.test.mts +135 -0
- package/templates/standalone/src/__tests__/redis-rate-limiter-config-survives.test.mts +112 -81
- package/templates/standalone/src/__tests__/refresh-token-family-client-module.test.mts +25 -31
- package/templates/standalone/src/__tests__/replica-safety.test.mts +166 -145
- package/templates/standalone/src/__tests__/router-error-answers.test.mts +146 -0
- package/templates/standalone/src/__tests__/scaffold-assets.test.mts +275 -95
- package/templates/standalone/src/__tests__/sections.test.mts +480 -0
- package/templates/standalone/src/__tests__/session-logout-invalidates-token.test.mts +67 -55
- package/templates/standalone/src/__tests__/session-requirements.test.mts +299 -0
- package/templates/standalone/src/__tests__/shutdown.test.mts +319 -115
- package/templates/standalone/src/__tests__/smoke.test.mts +179 -233
- package/templates/standalone/src/__tests__/store-credential.test.mts +317 -0
- package/templates/standalone/src/__tests__/supertest-loopback.test.mts +182 -11
- package/templates/standalone/src/__tests__/three-tier-resolution.test.mts +108 -95
- package/templates/standalone/src/__tests__/two-phase-config.test.mts +438 -0
- package/templates/standalone/src/__tests__/upstream-amr.acceptance.test.mts +390 -0
- package/templates/standalone/src/adapters.mts +149 -0
- package/templates/standalone/src/app.mts +71 -115
- package/templates/standalone/src/buildModules.mts +229 -251
- package/templates/standalone/src/configPath.mts +422 -29
- package/templates/standalone/src/listen.mts +60 -0
- package/templates/standalone/src/logger.mts +31 -57
- package/templates/standalone/src/metrics.mts +24 -47
- package/templates/standalone/src/mfaSwitch.mts +262 -0
- package/templates/standalone/src/modules.mts +480 -464
- package/templates/standalone/src/rootRenames.mts +79 -0
- package/templates/standalone/src/routes.mts +89 -0
- package/templates/standalone/src/sections.mts +366 -0
- package/templates/standalone/src/shutdown.mts +54 -126
- package/templates/standalone/tests/index.test.js +2 -2
- package/templates/standalone/tests/package-lock.json +1615 -2223
- package/templates/standalone/tests/package.json +2 -2
- package/templates/standalone/tests/vitest.config.mjs +26 -0
- package/templates/standalone/vitest.config.mts +8 -7
- package/templates/standalone/vitest.supertest-loopback.mts +97 -69
- package/templates/versions.json +18 -15
- package/templates/standalone/src/__tests__/terminal-error.test.mts +0 -96
- package/templates/standalone/src/terminalError.mts +0 -60
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
24
|
-
// address supertest dials. Unpatched, macOS
|
|
25
|
-
// process holds on 127.0.0.1, and the
|
|
26
|
-
//
|
|
27
|
-
// Dockerfile `test` stage copies it;
|
|
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
|
|
19
|
-
*
|
|
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
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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`)
|
|
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
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
-
*
|
|
60
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
145
|
-
* not finished (an agent's requests sent
|
|
146
|
-
*
|
|
147
|
-
*
|
|
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
|
|
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
|
-
//
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
server.
|
|
208
|
-
|
|
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
|
|
222
|
-
//
|
|
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
|
|
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);
|
package/templates/versions.json
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
{
|
|
2
|
-
"@o3co/auth-provider-core": "0.
|
|
3
|
-
"@o3co/auth-provider-device-grant": "0.
|
|
4
|
-
"@o3co/auth-provider-dpop": "0.
|
|
5
|
-
"@o3co/auth-provider-mtls": "0.
|
|
6
|
-
"@o3co/auth-provider-federation-apple": "0.
|
|
7
|
-
"@o3co/auth-provider-federation-google": "0.
|
|
8
|
-
"@o3co/auth-provider-federation-github": "0.
|
|
9
|
-
"@o3co/auth-provider-federation-grants": "0.
|
|
10
|
-
"@o3co/auth-provider-federation-oidc": "0.
|
|
11
|
-
"@o3co/auth-provider-oauth": "0.
|
|
12
|
-
"@o3co/auth-provider-oauth-token-exchange": "0.
|
|
13
|
-
"@o3co/auth-provider-session": "0.
|
|
14
|
-
"@o3co/auth-provider-foundation": "0.
|
|
15
|
-
"@o3co/auth-provider-
|
|
16
|
-
"@o3co/auth-provider-
|
|
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
|
-
};
|