@prosopo/api-express-router 3.1.98 → 3.2.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 (48) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +7 -9
  2. package/.turbo/turbo-build$colon$tsc.log +21 -24
  3. package/.turbo/turbo-build.log +7 -12
  4. package/CHANGELOG.md +54 -0
  5. package/LICENSE +201 -0
  6. package/dist/cjs/index.cjs +2 -0
  7. package/dist/cjs/middlewares/authMiddleware.cjs +25 -14
  8. package/dist/cjs/middlewares/jwtReplayGuard.cjs +28 -0
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +2 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/middlewares/authMiddleware.d.ts +7 -1
  14. package/dist/middlewares/authMiddleware.d.ts.map +1 -1
  15. package/dist/middlewares/authMiddleware.js +26 -15
  16. package/dist/middlewares/authMiddleware.js.map +1 -1
  17. package/dist/middlewares/jwtReplayGuard.d.ts +5 -0
  18. package/dist/middlewares/jwtReplayGuard.d.ts.map +1 -0
  19. package/dist/middlewares/jwtReplayGuard.js +28 -0
  20. package/dist/middlewares/jwtReplayGuard.js.map +1 -0
  21. package/dist/tests/apiExpressRouter.test-d.js +1 -1
  22. package/dist/tests/apiExpressRouter.test-d.js.map +1 -1
  23. package/dist/tests/unit/middlewares/authMiddleware.unit.test.js +94 -8
  24. package/dist/tests/unit/middlewares/authMiddleware.unit.test.js.map +1 -1
  25. package/dist/tests/unit/middlewares/jwtReplayGuard.unit.test.d.ts +2 -0
  26. package/dist/tests/unit/middlewares/jwtReplayGuard.unit.test.d.ts.map +1 -0
  27. package/dist/tests/unit/middlewares/jwtReplayGuard.unit.test.js +36 -0
  28. package/dist/tests/unit/middlewares/jwtReplayGuard.unit.test.js.map +1 -0
  29. package/package.json +62 -59
  30. package/src/apiExpressRouterFactory.ts +0 -68
  31. package/src/endpointAdapter/apiExpressDefaultEndpointAdapter.ts +0 -92
  32. package/src/endpointAdapter/apiExpressEndpointAdapter.ts +0 -28
  33. package/src/errorHandler.ts +0 -33
  34. package/src/index.ts +0 -37
  35. package/src/middlewares/authMiddleware.ts +0 -93
  36. package/src/middlewares/requestLoggerMiddleware.ts +0 -104
  37. package/src/tests/apiExpressRouter.test-d.ts +0 -190
  38. package/src/tests/unit/apiExpressDefaultEndpointAdapter.unit.test.ts +0 -414
  39. package/src/tests/unit/apiExpressRouterFactory.unit.test.ts +0 -215
  40. package/src/tests/unit/errorHandler.unit.test.ts +0 -190
  41. package/src/tests/unit/index.unit.test.ts +0 -95
  42. package/src/tests/unit/middlewares/authMiddleware.unit.test.ts +0 -342
  43. package/src/tests/unit/middlewares/requestLoggerMiddleware.unit.test.ts +0 -368
  44. package/src/tests/unit/testDoubles.ts +0 -39
  45. package/tsconfig.cjs.json +0 -37
  46. package/tsconfig.json +0 -44
  47. package/tsconfig.tsbuildinfo +0 -1
  48. package/tsconfig.types.json +0 -9
@@ -1,190 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import { ProsopoApiError, ProsopoEnvError } from "@prosopo/common";
16
- import { loadI18next } from "@prosopo/locale";
17
- import type { NextFunction, Request, Response } from "express";
18
- import { describe, expect, it, vi } from "vitest";
19
- import { ZodError } from "zod";
20
- import { handleErrors } from "../../errorHandler.js";
21
-
22
- describe("handleErrors", async () => {
23
- const i18n = await loadI18next(true);
24
- await i18n.changeLanguage("en");
25
-
26
- it("should handle ProsopoApiError", async () => {
27
- const mockRequest = { i18n } as unknown as Request;
28
- const mockResponse = {
29
- writeHead: vi.fn().mockReturnThis(),
30
- set: vi.fn().mockReturnThis(),
31
- status: vi.fn().mockReturnThis(),
32
- send: vi.fn(),
33
- end: vi.fn(),
34
- } as unknown as Response;
35
- const mockNext = vi.fn() as unknown as NextFunction;
36
-
37
- const error = new ProsopoApiError("CONTRACT.INVALID_DATA_FORMAT", {
38
- context: { code: 400 },
39
- i18n,
40
- });
41
- console.log(error);
42
-
43
- handleErrors(error, mockRequest, mockResponse, mockNext);
44
-
45
- expect(mockResponse.set).toHaveBeenCalledWith(
46
- "content-type",
47
- "application/json",
48
- );
49
- expect(mockResponse.send).toHaveBeenCalledWith({
50
- error: {
51
- code: 400,
52
- key: "CONTRACT.INVALID_DATA_FORMAT",
53
- message: "Invalid data format",
54
- },
55
- });
56
- expect(mockResponse.status).toHaveBeenCalledWith(400);
57
- expect(mockResponse.end).toHaveBeenCalled();
58
- });
59
-
60
- it("should not return SyntaxError", async () => {
61
- const mockRequest = { i18n } as unknown as Request;
62
- const mockResponse = {
63
- writeHead: vi.fn().mockReturnThis(),
64
- set: vi.fn().mockReturnThis(),
65
- status: vi.fn().mockReturnThis(),
66
- send: vi.fn(),
67
- end: vi.fn(),
68
- } as unknown as Response;
69
- const mockNext = vi.fn() as unknown as NextFunction;
70
-
71
- const message = "Unknown API error";
72
- const error = new SyntaxError(message);
73
-
74
- handleErrors(error, mockRequest, mockResponse, mockNext);
75
-
76
- expect(mockResponse.set).toHaveBeenCalledWith(
77
- "content-type",
78
- "application/json",
79
- );
80
- expect(mockResponse.status).toHaveBeenCalledWith(400);
81
- expect(mockResponse.send).toHaveBeenCalledWith({
82
- error: {
83
- message,
84
- key: "API.UNKNOWN",
85
- code: 400,
86
- },
87
- });
88
- expect(mockResponse.end).toHaveBeenCalled();
89
- });
90
-
91
- it("should handle ZodError", () => {
92
- const mockRequest = { i18n } as unknown as Request;
93
- const mockResponse = {
94
- writeHead: vi.fn().mockReturnThis(),
95
- set: vi.fn().mockReturnThis(),
96
- status: vi.fn().mockReturnThis(),
97
- send: vi.fn(),
98
- end: vi.fn(),
99
- } as unknown as Response;
100
- const mockNext = vi.fn() as unknown as NextFunction;
101
-
102
- const zodError = {
103
- code: "custom" as const,
104
- message: "Invalid input",
105
- path: ["some", "variable"],
106
- };
107
- const error = new ZodError([zodError]);
108
-
109
- handleErrors(error, mockRequest, mockResponse, mockNext);
110
-
111
- expect(mockResponse.set).toHaveBeenCalledWith(
112
- "content-type",
113
- "application/json",
114
- );
115
- expect(mockResponse.status).toHaveBeenCalledWith(400);
116
- expect(mockResponse.send).toHaveBeenCalledWith({
117
- error: { code: 400, key: "API.INVALID_BODY", message: [zodError] },
118
- });
119
- expect(mockResponse.end).toHaveBeenCalled();
120
- });
121
-
122
- it("should unwrap nested ProsopoBaseError", async () => {
123
- const mockRequest = { i18n } as unknown as Request;
124
- const mockResponse = {
125
- writeHead: vi.fn().mockReturnThis(),
126
- set: vi.fn().mockReturnThis(),
127
- status: vi.fn().mockReturnThis(),
128
- send: vi.fn(),
129
- end: vi.fn(),
130
- } as unknown as Response;
131
- const mockNext = vi.fn() as unknown as NextFunction;
132
-
133
- const envError = new ProsopoEnvError("GENERAL.ENVIRONMENT_NOT_READY", {
134
- i18n,
135
- });
136
- const apiError = new ProsopoApiError(envError);
137
-
138
- handleErrors(apiError, mockRequest, mockResponse, mockNext);
139
-
140
- expect(mockResponse.set).toHaveBeenCalledWith(
141
- "content-type",
142
- "application/json",
143
- );
144
- expect(mockResponse.status).toHaveBeenCalledWith(500);
145
- expect(mockResponse.send).toHaveBeenCalledWith({
146
- error: {
147
- code: 500,
148
- key: "GENERAL.ENVIRONMENT_NOT_READY",
149
- message: "Environment not ready",
150
- },
151
- });
152
- expect(mockResponse.end).toHaveBeenCalled();
153
- });
154
-
155
- it("should unwrap nested ProsopoBaseErrors but not an Error that is nested inside them", async () => {
156
- const mockRequest = { i18n } as unknown as Request;
157
- const mockResponse = {
158
- writeHead: vi.fn().mockReturnThis(),
159
- set: vi.fn().mockReturnThis(),
160
- status: vi.fn().mockReturnThis(),
161
- send: vi.fn(),
162
- end: vi.fn(),
163
- } as unknown as Response;
164
- const mockNext = vi.fn() as unknown as NextFunction;
165
- const code = 400;
166
- const key = "API.UNKNOWN";
167
-
168
- const error = new Error("Some error");
169
- const apiError = new ProsopoApiError(key, {
170
- context: { code, error },
171
- i18n,
172
- });
173
-
174
- handleErrors(apiError, mockRequest, mockResponse, mockNext);
175
-
176
- expect(mockResponse.set).toHaveBeenCalledWith(
177
- "content-type",
178
- "application/json",
179
- );
180
- expect(mockResponse.status).toHaveBeenCalledWith(code);
181
- expect(mockResponse.send).toHaveBeenCalledWith({
182
- error: {
183
- code,
184
- key,
185
- message: "Unknown API error",
186
- },
187
- });
188
- expect(mockResponse.end).toHaveBeenCalled();
189
- });
190
- });
@@ -1,95 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import type { ApiRoutes, ApiRoutesProvider } from "@prosopo/api-route";
16
- import { LogLevel } from "@prosopo/logger";
17
- import type { NextFunction, Request, Response } from "express";
18
- import { describe, expect, test, vi } from "vitest";
19
- import {
20
- apiExpressRouterFactory,
21
- authMiddleware,
22
- createApiExpressDefaultEndpointAdapter,
23
- handleErrors,
24
- requestLoggerMiddleware,
25
- } from "../../index.js";
26
-
27
- /**
28
- * The barrel is what every consuming package imports, so the shape of it is
29
- * part of the contract — a missing re-export or a changed default breaks
30
- * callers without touching any of the implementations.
31
- */
32
-
33
- describe("createApiExpressDefaultEndpointAdapter", () => {
34
- test("builds an adapter exposing handleRequest", () => {
35
- const adapter = createApiExpressDefaultEndpointAdapter(LogLevel.enum.info);
36
- expect(typeof adapter.handleRequest).toBe("function");
37
- });
38
-
39
- test("builds a fresh adapter per call", () => {
40
- expect(createApiExpressDefaultEndpointAdapter(LogLevel.enum.info)).not.toBe(
41
- createApiExpressDefaultEndpointAdapter(LogLevel.enum.info),
42
- );
43
- });
44
-
45
- test("defaults the error status code so callers may omit it", () => {
46
- expect(() =>
47
- createApiExpressDefaultEndpointAdapter(LogLevel.enum.debug),
48
- ).not.toThrow();
49
- });
50
-
51
- test("accepts an explicit error status code", () => {
52
- expect(() =>
53
- createApiExpressDefaultEndpointAdapter(LogLevel.enum.debug, 503),
54
- ).not.toThrow();
55
- });
56
- });
57
-
58
- describe("apiExpressRouterFactory", () => {
59
- test("is exported as a single shared instance", () => {
60
- // It holds no per-router state, so one instance is enough — but callers
61
- // import the value rather than the class, so it must stay a singleton.
62
- expect(typeof apiExpressRouterFactory.createRouter).toBe("function");
63
- });
64
-
65
- test("builds a router from a provider", () => {
66
- const provider: ApiRoutesProvider = {
67
- getRoutes: (): ApiRoutes => ({}),
68
- };
69
- const router = apiExpressRouterFactory.createRouter(provider, {
70
- handleRequest: vi.fn<
71
- (
72
- endpoint: never,
73
- request: Request,
74
- response: Response,
75
- next: NextFunction,
76
- ) => Promise<void>
77
- >(async () => undefined),
78
- });
79
- expect(typeof router).toBe("function");
80
- });
81
- });
82
-
83
- describe("the re-exported middleware", () => {
84
- test("exposes the error handler", () => {
85
- expect(typeof handleErrors).toBe("function");
86
- });
87
-
88
- test("exposes the auth middleware", () => {
89
- expect(typeof authMiddleware).toBe("function");
90
- });
91
-
92
- test("exposes the request logger middleware", () => {
93
- expect(typeof requestLoggerMiddleware).toBe("function");
94
- });
95
- });
@@ -1,342 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import type { IncomingHttpHeaders } from "node:http";
16
- import { ProsopoApiError } from "@prosopo/common";
17
- import type { Logger } from "@prosopo/logger";
18
- import type { KeyringPair } from "@prosopo/types";
19
- import type { JWT } from "@prosopo/util-crypto";
20
- import type { Request, Response } from "express";
21
- import { beforeEach, describe, expect, test, vi } from "vitest";
22
- import {
23
- authMiddleware,
24
- verifySignature,
25
- } from "../../../middlewares/authMiddleware.js";
26
- import { type NextCapture, captureNext } from "../testDoubles.js";
27
-
28
- /**
29
- * Everything this middleware protects is behind a single decision: call next()
30
- * or answer 401. It must never fall through to the handler when a token is
31
- * absent, malformed, or verified by neither key — including when verification
32
- * itself throws.
33
- */
34
-
35
- type Verify = (jwt: JWT) => { isValid: boolean };
36
-
37
- /** A pair that only implements the parts the middleware actually calls. */
38
- const pairThat = (verify: Verify): KeyringPair =>
39
- ({
40
- jwtVerify: vi.fn<Verify>(verify),
41
- address: "5Test",
42
- publicKey: new Uint8Array([1, 2, 3]),
43
- verify: vi.fn<
44
- (message: string, signature: Uint8Array, key: Uint8Array) => boolean
45
- >(() => true),
46
- }) as unknown as KeyringPair;
47
-
48
- const accepts = (): KeyringPair => pairThat(() => ({ isValid: true }));
49
- const rejects = (): KeyringPair => pairThat(() => ({ isValid: false }));
50
- const explodes = (): KeyringPair =>
51
- pairThat(() => {
52
- throw new Error("verifier unavailable");
53
- });
54
-
55
- interface Harness {
56
- req: Request;
57
- res: Response;
58
- next: NextCapture;
59
- status: ReturnType<typeof vi.fn<(code: number) => Response>>;
60
- json: ReturnType<typeof vi.fn<(body: unknown) => Response>>;
61
- logged: ReturnType<typeof vi.fn<(entry: () => unknown) => void>>;
62
- }
63
-
64
- const build = (headers: IncomingHttpHeaders = {}): Harness => {
65
- const json = vi.fn<(body: unknown) => Response>();
66
- const status = vi.fn<(code: number) => Response>();
67
- const res = { status, json } as unknown as Response;
68
- status.mockReturnValue(res);
69
-
70
- const logged = vi.fn<(entry: () => unknown) => void>();
71
- const req = {
72
- headers,
73
- logger: { error: logged } as unknown as Logger,
74
- } as unknown as Request;
75
-
76
- return { req, res, next: captureNext(), status, json, logged };
77
- };
78
-
79
- const bearer = (token: string): IncomingHttpHeaders => ({
80
- authorization: `Bearer ${token}`,
81
- });
82
-
83
- let harness: Harness;
84
-
85
- beforeEach(() => {
86
- harness = build(bearer("token-1"));
87
- });
88
-
89
- const run = async (
90
- pair: KeyringPair | undefined,
91
- authAccount?: KeyringPair | undefined,
92
- ): Promise<void> => {
93
- await authMiddleware(pair, authAccount)(
94
- harness.req,
95
- harness.res,
96
- harness.next.fn,
97
- );
98
- };
99
-
100
- describe("a token one of the keys accepts", () => {
101
- test("passes the request through on the auth account", async () => {
102
- await run(rejects(), accepts());
103
- expect(harness.next.calls).toHaveLength(1);
104
- expect(harness.status).not.toHaveBeenCalled();
105
- });
106
-
107
- test("passes the request through on the provider pair", async () => {
108
- await run(accepts(), rejects());
109
- expect(harness.next.calls).toHaveLength(1);
110
- });
111
-
112
- test("does not consult the provider pair once the auth account accepts", async () => {
113
- // Short-circuiting matters: the second verify is a signature check and
114
- // there is no reason to pay for it twice.
115
- const pair = accepts();
116
- await run(pair, accepts());
117
- expect(pair.jwtVerify).not.toHaveBeenCalled();
118
- });
119
-
120
- test("works when only the provider pair is configured", async () => {
121
- await run(accepts(), undefined);
122
- expect(harness.next.calls).toHaveLength(1);
123
- });
124
-
125
- test("works when only the auth account is configured", async () => {
126
- await run(undefined, accepts());
127
- expect(harness.next.calls).toHaveLength(1);
128
- });
129
-
130
- test("hands the verifier the token without its Bearer prefix", async () => {
131
- const pair = accepts();
132
- await run(pair, undefined);
133
- expect(pair.jwtVerify).toHaveBeenCalledWith("token-1");
134
- });
135
- });
136
-
137
- describe("a token neither key accepts", () => {
138
- beforeEach(async () => {
139
- await run(rejects(), rejects());
140
- });
141
-
142
- test("is answered 401 and never reaches the handler", () => {
143
- expect(harness.status).toHaveBeenCalledWith(401);
144
- expect(harness.next.calls).toHaveLength(0);
145
- });
146
-
147
- test("is answered with an error body carrying the 401 code", () => {
148
- const body = harness.json.mock.calls[0]?.[0] as { error: ProsopoApiError };
149
- expect(body.error.context?.code).toBe(401);
150
- });
151
- });
152
-
153
- describe("a request with no usable credentials", () => {
154
- test("no configured keys at all is rejected, not allowed through", async () => {
155
- // A misconfigured provider must fail closed.
156
- await run(undefined, undefined);
157
- expect(harness.status).toHaveBeenCalledWith(401);
158
- expect(harness.next.calls).toHaveLength(0);
159
- });
160
-
161
- test("a missing Authorization header is rejected", async () => {
162
- harness = build({});
163
- await run(accepts(), accepts());
164
- expect(harness.status).toHaveBeenCalledWith(401);
165
- expect(harness.next.calls).toHaveLength(0);
166
- });
167
-
168
- test("an Authorization header that is not a string is rejected", async () => {
169
- // Node hands back an array for a repeated header; verifying that would
170
- // throw deeper in, so it is refused here.
171
- harness = build({
172
- authorization: ["Bearer a", "Bearer b"] as unknown as string,
173
- });
174
- await run(accepts(), accepts());
175
- expect(harness.status).toHaveBeenCalledWith(401);
176
- });
177
-
178
- test("an empty Authorization header is rejected", async () => {
179
- harness = build({ authorization: "" });
180
- await run(accepts(), accepts());
181
- expect(harness.status).toHaveBeenCalledWith(401);
182
- });
183
-
184
- test("a Bearer prefix with no token after it is rejected", async () => {
185
- harness = build({ authorization: "Bearer " });
186
- await run(accepts(), accepts());
187
- expect(harness.status).toHaveBeenCalledWith(401);
188
- expect(harness.next.calls).toHaveLength(0);
189
- });
190
-
191
- test("the verifier is never reached for a malformed header", async () => {
192
- harness = build({});
193
- const pair = accepts();
194
- await run(pair, undefined);
195
- expect(pair.jwtVerify).not.toHaveBeenCalled();
196
- });
197
-
198
- test("a header rejection is logged", async () => {
199
- harness = build({});
200
- await run(accepts(), undefined);
201
- expect(harness.logged).toHaveBeenCalledTimes(1);
202
- });
203
- });
204
-
205
- describe("header handling quirks worth pinning down", () => {
206
- test("a bare token with no Bearer prefix is accepted as the token", async () => {
207
- // The prefix is stripped rather than required, so a client that omits it
208
- // still authenticates. Recorded so a change here is deliberate.
209
- harness = build({ authorization: "token-1" });
210
- const pair = accepts();
211
- await run(pair, undefined);
212
- expect(pair.jwtVerify).toHaveBeenCalledWith("token-1");
213
- expect(harness.next.calls).toHaveLength(1);
214
- });
215
-
216
- test("a capitalised Authorization key is read as well as the lowercase one", async () => {
217
- // Node normalises real inbound header names to lowercase, so only the
218
- // second lookup fires in production — but both are honoured, which is
219
- // what lets a hand-built request object work in a test or a shim.
220
- harness = build({ Authorization: "Bearer token-1" } as IncomingHttpHeaders);
221
- const pair = accepts();
222
- await run(pair, undefined);
223
- expect(pair.jwtVerify).toHaveBeenCalledWith("token-1");
224
- expect(harness.next.calls).toHaveLength(1);
225
- });
226
-
227
- test("a Bearer prefix appearing inside the token is also stripped", async () => {
228
- // replace() is unanchored, so it removes the first occurrence wherever it
229
- // sits rather than only at the front.
230
- harness = build({ authorization: "abcBearer xyz" });
231
- const pair = accepts();
232
- await run(pair, undefined);
233
- expect(pair.jwtVerify).toHaveBeenCalledWith("abcxyz");
234
- });
235
- });
236
-
237
- describe("a verifier that throws", () => {
238
- test("is answered 401 rather than crashing the request", async () => {
239
- // A key backend being unavailable must not turn into a 500 or an
240
- // unhandled rejection.
241
- await run(explodes(), undefined);
242
- expect(harness.status).toHaveBeenCalledWith(401);
243
- expect(harness.next.calls).toHaveLength(0);
244
- });
245
-
246
- test("is logged against the request logger", async () => {
247
- await run(explodes(), undefined);
248
- expect(harness.logged).toHaveBeenCalledTimes(1);
249
- const entry = harness.logged.mock.calls[0]?.[0] as () => {
250
- msg: string;
251
- err: unknown;
252
- };
253
- expect(entry().msg).toBe("Auth Middleware Error");
254
- });
255
-
256
- test("a throwing auth account does not fall through to the provider pair", async () => {
257
- // The throw escapes the whole chain, so a working second key cannot
258
- // rescue the request.
259
- const pair = accepts();
260
- await run(pair, explodes());
261
- expect(pair.jwtVerify).not.toHaveBeenCalled();
262
- expect(harness.next.calls).toHaveLength(0);
263
- });
264
-
265
- test("does not resolve before it has answered", async () => {
266
- const middleware = authMiddleware(explodes(), undefined);
267
- await middleware(harness.req, harness.res, harness.next.fn);
268
- expect(harness.json).toHaveBeenCalledTimes(1);
269
- });
270
- });
271
-
272
- describe("verifySignature", () => {
273
- const message = "sign me";
274
- const signature = "0x0102";
275
-
276
- test("returns quietly when the pair verifies the signature", () => {
277
- const pair = pairThat(() => ({ isValid: true }));
278
- expect(() => verifySignature(signature, message, pair)).not.toThrow();
279
- });
280
-
281
- test("passes the decoded signature bytes to the pair", () => {
282
- const pair = pairThat(() => ({ isValid: true }));
283
- verifySignature(signature, message, pair);
284
- expect(pair.verify).toHaveBeenCalledWith(
285
- message,
286
- new Uint8Array([1, 2]),
287
- pair.publicKey,
288
- );
289
- });
290
-
291
- test("throws a 401 error when verification fails", () => {
292
- const pair = pairThat(() => ({ isValid: true }));
293
- vi.mocked(pair.verify).mockReturnValue(false);
294
- try {
295
- verifySignature(signature, message, pair);
296
- expect.unreachable("verifySignature should have thrown");
297
- } catch (error) {
298
- expect(error).toBeInstanceOf(ProsopoApiError);
299
- expect((error as ProsopoApiError).context?.code).toBe(401);
300
- }
301
- });
302
-
303
- test("reports which account and message failed", () => {
304
- // Without the account in the context a failure is untraceable across the
305
- // several keys a provider holds.
306
- const pair = pairThat(() => ({ isValid: true }));
307
- vi.mocked(pair.verify).mockReturnValue(false);
308
- try {
309
- verifySignature(signature, message, pair);
310
- expect.unreachable("verifySignature should have thrown");
311
- } catch (error) {
312
- const context = (error as ProsopoApiError).context;
313
- expect(context?.account).toBe("5Test");
314
- expect(context?.message).toBe(message);
315
- expect(context?.signature).toBe(signature);
316
- }
317
- });
318
-
319
- test("a non-hex signature is decoded to garbage rather than rejected", () => {
320
- // hexToU8a is lenient: it silently produces bytes for input that is not
321
- // hex at all. Nothing here validates the shape, so a malformed signature
322
- // reaches the pair and fails there instead of being refused up front.
323
- const pair = pairThat(() => ({ isValid: true }));
324
- vi.mocked(pair.verify).mockReturnValue(false);
325
- expect(() => verifySignature("not hex", message, pair)).toThrow(
326
- ProsopoApiError,
327
- );
328
- expect(pair.verify).toHaveBeenCalledTimes(1);
329
- });
330
-
331
- test("an empty message is still passed through to the pair", () => {
332
- // Nothing guards against it, so an empty message verifies or fails purely
333
- // on the pair's answer rather than being rejected up front.
334
- const pair = pairThat(() => ({ isValid: true }));
335
- verifySignature(signature, "", pair);
336
- expect(pair.verify).toHaveBeenCalledWith(
337
- "",
338
- new Uint8Array([1, 2]),
339
- pair.publicKey,
340
- );
341
- });
342
- });