@prosopo/api-express-router 3.1.50 → 3.1.52
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/.turbo/turbo-build$colon$cjs.log +14 -12
- package/.turbo/turbo-build$colon$tsc.log +21 -21
- package/.turbo/turbo-build.log +16 -13
- package/CHANGELOG.md +23 -0
- package/dist/_virtual/_rolldown/runtime.js +3 -0
- package/dist/apiExpressRouterFactory.js +17 -27
- package/dist/cjs/apiExpressRouterFactory.cjs +18 -28
- package/dist/cjs/endpointAdapter/apiExpressDefaultEndpointAdapter.cjs +29 -35
- package/dist/cjs/errorHandler.cjs +10 -10
- package/dist/cjs/index.cjs +14 -13
- package/dist/cjs/middlewares/authMiddleware.cjs +54 -56
- package/dist/cjs/middlewares/requestLoggerMiddleware.cjs +57 -59
- package/dist/endpointAdapter/apiExpressDefaultEndpointAdapter.js +26 -33
- package/dist/errorHandler.js +10 -10
- package/dist/index.js +8 -12
- package/dist/middlewares/authMiddleware.js +54 -57
- package/dist/middlewares/requestLoggerMiddleware.js +56 -58
- package/dist/tests/apiExpressRouter.test-d.d.ts +2 -0
- package/dist/tests/apiExpressRouter.test-d.d.ts.map +1 -0
- package/dist/tests/apiExpressRouter.test-d.js +76 -0
- package/dist/tests/apiExpressRouter.test-d.js.map +1 -0
- package/dist/tests/unit/apiExpressDefaultEndpointAdapter.unit.test.d.ts +2 -0
- package/dist/tests/unit/apiExpressDefaultEndpointAdapter.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/apiExpressDefaultEndpointAdapter.unit.test.js +197 -0
- package/dist/tests/unit/apiExpressDefaultEndpointAdapter.unit.test.js.map +1 -0
- package/dist/tests/unit/apiExpressRouterFactory.unit.test.d.ts +2 -0
- package/dist/tests/unit/apiExpressRouterFactory.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/apiExpressRouterFactory.unit.test.js +108 -0
- package/dist/tests/unit/apiExpressRouterFactory.unit.test.js.map +1 -0
- package/dist/tests/unit/index.unit.test.d.ts +2 -0
- package/dist/tests/unit/index.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/index.unit.test.js +44 -0
- package/dist/tests/unit/index.unit.test.js.map +1 -0
- package/dist/tests/unit/middlewares/authMiddleware.unit.test.js +210 -136
- package/dist/tests/unit/middlewares/authMiddleware.unit.test.js.map +1 -1
- package/dist/tests/unit/middlewares/requestLoggerMiddleware.unit.test.d.ts +2 -0
- package/dist/tests/unit/middlewares/requestLoggerMiddleware.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/middlewares/requestLoggerMiddleware.unit.test.js +242 -0
- package/dist/tests/unit/middlewares/requestLoggerMiddleware.unit.test.js.map +1 -0
- package/dist/tests/unit/testDoubles.d.ts +8 -0
- package/dist/tests/unit/testDoubles.d.ts.map +1 -0
- package/dist/tests/unit/testDoubles.js +11 -0
- package/dist/tests/unit/testDoubles.js.map +1 -0
- package/package.json +10 -10
- package/src/tests/apiExpressRouter.test-d.ts +190 -0
- package/src/tests/unit/apiExpressDefaultEndpointAdapter.unit.test.ts +395 -0
- package/src/tests/unit/apiExpressRouterFactory.unit.test.ts +215 -0
- package/src/tests/unit/index.unit.test.ts +95 -0
- package/src/tests/unit/middlewares/authMiddleware.unit.test.ts +318 -159
- package/src/tests/unit/middlewares/requestLoggerMiddleware.unit.test.ts +368 -0
- package/src/tests/unit/testDoubles.ts +39 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -12,172 +12,331 @@
|
|
|
12
12
|
// See the License for the specific language governing permissions and
|
|
13
13
|
// limitations under the License.
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
15
|
+
import type { IncomingHttpHeaders } from "node:http";
|
|
16
|
+
import { ProsopoApiError } from "@prosopo/common";
|
|
17
|
+
import type { Logger } from "@prosopo/logger";
|
|
18
18
|
import type { KeyringPair } from "@prosopo/types";
|
|
19
|
-
import type {
|
|
20
|
-
import type {
|
|
21
|
-
import { describe, expect,
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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}`,
|
|
45
81
|
});
|
|
46
82
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
);
|
|
57
98
|
};
|
|
58
99
|
|
|
59
|
-
describe("
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
} as unknown as Request;
|
|
78
|
-
|
|
79
|
-
const mockRes = {
|
|
80
|
-
status: vi.fn().mockReturnThis(),
|
|
81
|
-
json: vi.fn(),
|
|
82
|
-
} as unknown as Response;
|
|
83
|
-
|
|
84
|
-
const mockNext = vi.fn() as unknown as NextFunction;
|
|
85
|
-
|
|
86
|
-
vi.mocked(isHex).mockReturnValue(true);
|
|
87
|
-
vi.mocked(hexToU8a).mockReturnValue(new Uint8Array());
|
|
88
|
-
vi.mocked(mockPair.jwtVerify).mockReturnValue({
|
|
89
|
-
isValid: true,
|
|
90
|
-
} as unknown as JWTVerifyResult);
|
|
91
|
-
|
|
92
|
-
const middleware = authMiddleware(mockEnv.pair, mockEnv.authAccount);
|
|
93
|
-
await middleware(mockReq, mockRes, mockNext);
|
|
94
|
-
|
|
95
|
-
expect(mockNext).toHaveBeenCalled();
|
|
96
|
-
expect(mockRes.status).not.toHaveBeenCalled();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("should return 401 if jwt is invalid", async () => {
|
|
100
|
-
const mockLogger = {
|
|
101
|
-
debug: vi.fn().mockImplementation(loggerOuter.debug.bind(loggerOuter)),
|
|
102
|
-
log: vi.fn().mockImplementation(loggerOuter.log.bind(loggerOuter)),
|
|
103
|
-
info: vi.fn().mockImplementation(loggerOuter.info.bind(loggerOuter)),
|
|
104
|
-
error: vi.fn().mockImplementation(loggerOuter.error.bind(loggerOuter)),
|
|
105
|
-
trace: vi.fn().mockImplementation(loggerOuter.trace.bind(loggerOuter)),
|
|
106
|
-
fatal: vi.fn().mockImplementation(loggerOuter.fatal.bind(loggerOuter)),
|
|
107
|
-
warn: vi.fn().mockImplementation(loggerOuter.warn.bind(loggerOuter)),
|
|
108
|
-
} as unknown as Logger;
|
|
109
|
-
const mockReq = {
|
|
110
|
-
url: "/v1/prosopo/provider/captcha/image",
|
|
111
|
-
originalUrl: "/v1/prosopo/provider/captcha/image",
|
|
112
|
-
headers: {
|
|
113
|
-
Authorization: "Bearer mockToken",
|
|
114
|
-
},
|
|
115
|
-
logger: mockLogger,
|
|
116
|
-
i18n: vi.fn().mockReturnValue({
|
|
117
|
-
t: (key: string) => key,
|
|
118
|
-
}),
|
|
119
|
-
} as unknown as Request;
|
|
120
|
-
|
|
121
|
-
const mockRes = {
|
|
122
|
-
status: vi.fn().mockReturnThis(),
|
|
123
|
-
json: vi.fn(),
|
|
124
|
-
} as unknown as Response;
|
|
125
|
-
|
|
126
|
-
const mockNext = vi.fn() as unknown as NextFunction;
|
|
127
|
-
|
|
128
|
-
vi.mocked(isHex).mockReturnValue(true);
|
|
129
|
-
vi.mocked(hexToU8a).mockReturnValue(new Uint8Array());
|
|
130
|
-
vi.mocked(mockPair.jwtVerify).mockReturnValue({
|
|
131
|
-
isValid: false,
|
|
132
|
-
} as unknown as JWTVerifyResult);
|
|
133
|
-
|
|
134
|
-
const middleware = authMiddleware(mockEnv.pair, mockEnv.authAccount);
|
|
135
|
-
await middleware(mockReq, mockRes, mockNext);
|
|
136
|
-
|
|
137
|
-
expect(mockNext).not.toHaveBeenCalled();
|
|
138
|
-
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
139
|
-
expect(mockRes.json).toHaveBeenCalledWith({
|
|
140
|
-
error: new ProsopoEnvError("API.UNAUTHORIZED", {
|
|
141
|
-
context: { i18n: mockReq.i18n, code: 401 },
|
|
142
|
-
}),
|
|
143
|
-
});
|
|
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();
|
|
144
118
|
});
|
|
145
119
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
expect(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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,
|
|
181
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
|
+
);
|
|
182
341
|
});
|
|
183
342
|
});
|