@prosopo/api-express-router 3.2.0 → 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.
- package/.turbo/turbo-build$colon$cjs.log +2 -5
- package/.turbo/turbo-build$colon$tsc.log +21 -24
- package/.turbo/turbo-build.log +3 -9
- package/CHANGELOG.md +19 -0
- package/LICENSE +201 -0
- package/package.json +62 -59
- package/src/apiExpressRouterFactory.ts +0 -68
- package/src/endpointAdapter/apiExpressDefaultEndpointAdapter.ts +0 -92
- package/src/endpointAdapter/apiExpressEndpointAdapter.ts +0 -28
- package/src/errorHandler.ts +0 -33
- package/src/index.ts +0 -38
- package/src/middlewares/authMiddleware.ts +0 -150
- package/src/middlewares/jwtReplayGuard.ts +0 -56
- package/src/middlewares/requestLoggerMiddleware.ts +0 -104
- package/src/tests/apiExpressRouter.test-d.ts +0 -196
- package/src/tests/unit/apiExpressDefaultEndpointAdapter.unit.test.ts +0 -414
- package/src/tests/unit/apiExpressRouterFactory.unit.test.ts +0 -215
- package/src/tests/unit/errorHandler.unit.test.ts +0 -190
- package/src/tests/unit/index.unit.test.ts +0 -95
- package/src/tests/unit/middlewares/authMiddleware.unit.test.ts +0 -449
- package/src/tests/unit/middlewares/jwtReplayGuard.unit.test.ts +0 -53
- package/src/tests/unit/middlewares/requestLoggerMiddleware.unit.test.ts +0 -368
- package/src/tests/unit/testDoubles.ts +0 -39
- package/tsconfig.cjs.json +0 -37
- package/tsconfig.json +0 -44
- package/tsconfig.tsbuildinfo +0 -1
- package/tsconfig.types.json +0 -9
|
@@ -1,28 +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 { ApiEndpoint } from "@prosopo/api-route";
|
|
16
|
-
import type { NextFunction, Request, Response } from "express";
|
|
17
|
-
import type { ZodType } from "zod";
|
|
18
|
-
|
|
19
|
-
interface ApiExpressEndpointAdapter {
|
|
20
|
-
handleRequest(
|
|
21
|
-
endpoint: ApiEndpoint<ZodType | undefined>,
|
|
22
|
-
request: Request,
|
|
23
|
-
response: Response,
|
|
24
|
-
next: NextFunction,
|
|
25
|
-
): Promise<void>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type { ApiExpressEndpointAdapter };
|
package/src/errorHandler.ts
DELETED
|
@@ -1,33 +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
|
-
// We need the unused params to make express recognise this function as an error handler
|
|
16
|
-
|
|
17
|
-
import { type ProsopoApiError, unwrapError } from "@prosopo/common";
|
|
18
|
-
import type { NextFunction, Request, Response } from "express";
|
|
19
|
-
import type { ZodError } from "zod";
|
|
20
|
-
|
|
21
|
-
export const handleErrors = (
|
|
22
|
-
err: ProsopoApiError | SyntaxError | ZodError,
|
|
23
|
-
request: Request,
|
|
24
|
-
response: Response,
|
|
25
|
-
next: NextFunction,
|
|
26
|
-
) => {
|
|
27
|
-
const { code, statusMessage, jsonError } = unwrapError(err, request.i18n);
|
|
28
|
-
response.statusMessage = statusMessage;
|
|
29
|
-
response.set("content-type", "application/json");
|
|
30
|
-
response.status(code);
|
|
31
|
-
response.send({ error: jsonError });
|
|
32
|
-
response.end();
|
|
33
|
-
};
|
package/src/index.ts
DELETED
|
@@ -1,38 +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 { LogLevel } from "@prosopo/logger";
|
|
16
|
-
import { ApiExpressRouterFactory } from "./apiExpressRouterFactory.js";
|
|
17
|
-
import { ApiExpressDefaultEndpointAdapter } from "./endpointAdapter/apiExpressDefaultEndpointAdapter.js";
|
|
18
|
-
import type { ApiExpressEndpointAdapter } from "./endpointAdapter/apiExpressEndpointAdapter.js";
|
|
19
|
-
|
|
20
|
-
const apiExpressRouterFactory = new ApiExpressRouterFactory();
|
|
21
|
-
|
|
22
|
-
const createApiExpressDefaultEndpointAdapter = (
|
|
23
|
-
logLevel: LogLevel,
|
|
24
|
-
errorStatusCode = 500,
|
|
25
|
-
): ApiExpressEndpointAdapter => {
|
|
26
|
-
return new ApiExpressDefaultEndpointAdapter(logLevel, errorStatusCode);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export {
|
|
30
|
-
apiExpressRouterFactory,
|
|
31
|
-
createApiExpressDefaultEndpointAdapter,
|
|
32
|
-
type ApiExpressEndpointAdapter,
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export * from "./errorHandler.js";
|
|
36
|
-
export * from "./middlewares/authMiddleware.js";
|
|
37
|
-
export * from "./middlewares/jwtReplayGuard.js";
|
|
38
|
-
export * from "./middlewares/requestLoggerMiddleware.js";
|
|
@@ -1,150 +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 { hexToU8a } from "@polkadot/util";
|
|
16
|
-
import { ProsopoApiError, ProsopoBaseError } from "@prosopo/common";
|
|
17
|
-
import { type TranslationKey, isTranslationKey } from "@prosopo/locale";
|
|
18
|
-
import type { ApiJsonError, KeyringPair } from "@prosopo/types";
|
|
19
|
-
import type {
|
|
20
|
-
JWT,
|
|
21
|
-
JWTVerifyOptions,
|
|
22
|
-
JWTVerifyResult,
|
|
23
|
-
} from "@prosopo/util-crypto";
|
|
24
|
-
import type { NextFunction, Request, Response } from "express";
|
|
25
|
-
import type { JwtReplayGuard } from "./jwtReplayGuard.js";
|
|
26
|
-
|
|
27
|
-
export type AuthMiddlewareOptions = {
|
|
28
|
-
/** Claim checks (audience, maximum lifetime) applied to every token. */
|
|
29
|
-
verify?: JWTVerifyOptions;
|
|
30
|
-
/**
|
|
31
|
-
* Makes a token that carries a `jti` single-use. Tokens without `jti` are
|
|
32
|
-
* unaffected, so existing issuers keep working until they add one.
|
|
33
|
-
*/
|
|
34
|
-
replayGuard?: JwtReplayGuard;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const verifyWith = (
|
|
38
|
-
key: KeyringPair | undefined,
|
|
39
|
-
jwt: JWT,
|
|
40
|
-
options: AuthMiddlewareOptions,
|
|
41
|
-
): JWTVerifyResult | undefined => {
|
|
42
|
-
if (!key) return undefined;
|
|
43
|
-
const result = key.jwtVerify(jwt, options.verify);
|
|
44
|
-
return result.isValid ? result : undefined;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const isReplay = (
|
|
48
|
-
result: JWTVerifyResult,
|
|
49
|
-
options: AuthMiddlewareOptions,
|
|
50
|
-
): boolean => {
|
|
51
|
-
const payload = result.payload;
|
|
52
|
-
if (!options.replayGuard || !payload || typeof payload.jti !== "string") {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
return !options.replayGuard.claim(
|
|
56
|
-
`${payload.sub}:${payload.jti}`,
|
|
57
|
-
payload.exp,
|
|
58
|
-
);
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export const authMiddleware = (
|
|
62
|
-
pair: KeyringPair | undefined,
|
|
63
|
-
authAccount?: KeyringPair | undefined,
|
|
64
|
-
options: AuthMiddlewareOptions = {},
|
|
65
|
-
) => {
|
|
66
|
-
return async (req: Request, res: Response, next: NextFunction) => {
|
|
67
|
-
try {
|
|
68
|
-
const jwt = extractJWT(req);
|
|
69
|
-
|
|
70
|
-
const verified =
|
|
71
|
-
verifyWith(authAccount, jwt, options) ?? verifyWith(pair, jwt, options);
|
|
72
|
-
|
|
73
|
-
if (verified && !isReplay(verified, options)) {
|
|
74
|
-
next();
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
unauthorized(req, res, "API.UNAUTHORIZED");
|
|
79
|
-
return;
|
|
80
|
-
} catch (err) {
|
|
81
|
-
req.logger.error(() => ({ err, msg: "Auth Middleware Error" }));
|
|
82
|
-
// The thrown value can carry request context, stack or config, so
|
|
83
|
-
// only its translation key goes back to the caller.
|
|
84
|
-
unauthorized(req, res, translationKeyOf(err));
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const translationKeyOf = (err: unknown): TranslationKey =>
|
|
91
|
-
err instanceof ProsopoBaseError &&
|
|
92
|
-
err.translationKey !== undefined &&
|
|
93
|
-
isTranslationKey(err.translationKey)
|
|
94
|
-
? err.translationKey
|
|
95
|
-
: "API.UNAUTHORIZED";
|
|
96
|
-
|
|
97
|
-
const unauthorized = (
|
|
98
|
-
req: Request,
|
|
99
|
-
res: Response,
|
|
100
|
-
key: TranslationKey,
|
|
101
|
-
): void => {
|
|
102
|
-
const body: { error: ApiJsonError } = {
|
|
103
|
-
error: {
|
|
104
|
-
code: 401,
|
|
105
|
-
key,
|
|
106
|
-
message: req.i18n ? req.i18n.t(key) : key,
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
res.status(401).json(body);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const extractJWT = (req: Request) => {
|
|
113
|
-
const authHeader = req.headers.Authorization || req.headers.authorization;
|
|
114
|
-
|
|
115
|
-
if (!authHeader || typeof authHeader !== "string") {
|
|
116
|
-
throw new ProsopoApiError("GENERAL.MISSING_AUTH_HEADER", {
|
|
117
|
-
context: { error: "Missing Authorization header", code: 401 },
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const jwt = authHeader.replace("Bearer ", "");
|
|
122
|
-
|
|
123
|
-
if (!jwt) {
|
|
124
|
-
throw new ProsopoApiError("GENERAL.INVALID_JWT", {
|
|
125
|
-
context: { error: "Missing JWT", code: 400 },
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return jwt as JWT;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
export const verifySignature = (
|
|
133
|
-
signature: string,
|
|
134
|
-
message: string,
|
|
135
|
-
pair: KeyringPair,
|
|
136
|
-
) => {
|
|
137
|
-
const u8Sig = hexToU8a(signature);
|
|
138
|
-
|
|
139
|
-
if (!pair.verify(message, u8Sig, pair.publicKey)) {
|
|
140
|
-
throw new ProsopoApiError("GENERAL.INVALID_SIGNATURE", {
|
|
141
|
-
context: {
|
|
142
|
-
error: "Signature verification failed",
|
|
143
|
-
code: 401,
|
|
144
|
-
account: pair.address,
|
|
145
|
-
message,
|
|
146
|
-
signature,
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
};
|
|
@@ -1,56 +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
|
-
export interface JwtReplayGuard {
|
|
16
|
-
/**
|
|
17
|
-
* Record a token id. Returns false when the id was already recorded and has
|
|
18
|
-
* not yet expired, i.e. the token is being replayed.
|
|
19
|
-
*/
|
|
20
|
-
claim(id: string, expiresAtSeconds: number): boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Per-process set of token ids seen until their expiry. Only tokens signed by
|
|
25
|
-
* an admin key reach it, so the size cap guards against a misbehaving issuer
|
|
26
|
-
* rather than an attacker; when full the oldest entry is dropped.
|
|
27
|
-
*/
|
|
28
|
-
export const createMemoryJwtReplayGuard = (
|
|
29
|
-
maxEntries = 10_000,
|
|
30
|
-
nowSeconds: () => number = () => Date.now() / 1000,
|
|
31
|
-
): JwtReplayGuard => {
|
|
32
|
-
const seen = new Map<string, number>();
|
|
33
|
-
|
|
34
|
-
const sweep = (now: number): void => {
|
|
35
|
-
for (const [id, exp] of seen) {
|
|
36
|
-
if (exp < now) seen.delete(id);
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
claim(id: string, expiresAtSeconds: number): boolean {
|
|
42
|
-
const now = nowSeconds();
|
|
43
|
-
const previous = seen.get(id);
|
|
44
|
-
if (previous !== undefined && previous >= now) return false;
|
|
45
|
-
if (seen.size >= maxEntries) {
|
|
46
|
-
sweep(now);
|
|
47
|
-
if (seen.size >= maxEntries) {
|
|
48
|
-
const oldest = seen.keys().next();
|
|
49
|
-
if (!oldest.done) seen.delete(oldest.value);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
seen.set(id, expiresAtSeconds);
|
|
53
|
-
return true;
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
};
|
|
@@ -1,104 +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 { ProviderEnvironment } from "@prosopo/env";
|
|
16
|
-
import { getLogger, parseLogLevel } from "@prosopo/logger";
|
|
17
|
-
import type { NextFunction, Request, Response } from "express";
|
|
18
|
-
import { v4 as uuidv4 } from "uuid";
|
|
19
|
-
|
|
20
|
-
const getHeaderValue = (
|
|
21
|
-
req: Request,
|
|
22
|
-
headerName: string,
|
|
23
|
-
): string | undefined => {
|
|
24
|
-
if (headerName in req.headers && req.headers[headerName]) {
|
|
25
|
-
return req.headers[headerName].toString();
|
|
26
|
-
}
|
|
27
|
-
return undefined;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const HEALTH_CHECK_PATHS = new Set(["/healthz", "/health", "/readyz"]);
|
|
31
|
-
|
|
32
|
-
export function requestLoggerMiddleware(env: ProviderEnvironment) {
|
|
33
|
-
return (req: Request, res: Response, next: NextFunction) => {
|
|
34
|
-
// Honour an inbound `x-request-id` (Caddy/upstream proxy assigns one and
|
|
35
|
-
// we want the same value to link Caddy access logs to Node app logs).
|
|
36
|
-
// Fall back to `e-<uuid>` when the upstream didn't set one.
|
|
37
|
-
const requestId =
|
|
38
|
-
(req.headers["x-request-id"] as string) || `e-${uuidv4()}`;
|
|
39
|
-
const user = getHeaderValue(req, "prosopo-user");
|
|
40
|
-
const siteKey = getHeaderValue(req, "prosopo-site-key");
|
|
41
|
-
const sessionId = req.body?.sessionId ? req.body.sessionId : undefined;
|
|
42
|
-
|
|
43
|
-
const logger = getLogger(
|
|
44
|
-
parseLogLevel(env.config.logLevel),
|
|
45
|
-
"provider:request",
|
|
46
|
-
).with({
|
|
47
|
-
requestId,
|
|
48
|
-
...(user ? { user } : {}),
|
|
49
|
-
...(siteKey ? { siteKey } : {}),
|
|
50
|
-
...(sessionId ? { sessionId } : {}),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
req.logger = logger;
|
|
54
|
-
req.requestId = requestId;
|
|
55
|
-
|
|
56
|
-
// Mirror the requestId back to the client so downstream systems can
|
|
57
|
-
// correlate their own logs to ours.
|
|
58
|
-
res.setHeader("x-request-id", requestId);
|
|
59
|
-
|
|
60
|
-
// Skip request/response envelope logging for high-volume health probes
|
|
61
|
-
// so they don't drown out useful captcha-endpoint entries.
|
|
62
|
-
if (HEALTH_CHECK_PATHS.has(req.path)) {
|
|
63
|
-
next();
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Emit a single request-received line so every endpoint has an entry
|
|
68
|
-
// log in OpenObserve even when the handler itself doesn't log. Kept at
|
|
69
|
-
// info level; drop to debug if the volume becomes a problem.
|
|
70
|
-
const startNs = process.hrtime.bigint();
|
|
71
|
-
logger.info(() => ({
|
|
72
|
-
msg: "Request received",
|
|
73
|
-
data: {
|
|
74
|
-
method: req.method,
|
|
75
|
-
path: req.path,
|
|
76
|
-
},
|
|
77
|
-
}));
|
|
78
|
-
|
|
79
|
-
// Log a response-finished line. `finish` fires when the response was
|
|
80
|
-
// sent successfully; `close` fires when the client disconnects before
|
|
81
|
-
// the response was fully sent. Guard against both firing (once() would
|
|
82
|
-
// only cover a single event source).
|
|
83
|
-
let finished = false;
|
|
84
|
-
const emitFinish = (outcome: "finish" | "close") => {
|
|
85
|
-
if (finished) return;
|
|
86
|
-
finished = true;
|
|
87
|
-
const durationMs = Number(process.hrtime.bigint() - startNs) / 1e6;
|
|
88
|
-
logger.info(() => ({
|
|
89
|
-
msg: "Response sent",
|
|
90
|
-
data: {
|
|
91
|
-
method: req.method,
|
|
92
|
-
path: req.path,
|
|
93
|
-
status: res.statusCode,
|
|
94
|
-
durationMs,
|
|
95
|
-
outcome,
|
|
96
|
-
},
|
|
97
|
-
}));
|
|
98
|
-
};
|
|
99
|
-
res.on("finish", () => emitFinish("finish"));
|
|
100
|
-
res.on("close", () => emitFinish("close"));
|
|
101
|
-
|
|
102
|
-
next();
|
|
103
|
-
};
|
|
104
|
-
}
|
|
@@ -1,196 +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 { ApiEndpoint, ApiRoutesProvider } from "@prosopo/api-route";
|
|
16
|
-
import type { LogLevel } from "@prosopo/logger";
|
|
17
|
-
import type { KeyringPair } from "@prosopo/types";
|
|
18
|
-
import type {
|
|
19
|
-
NextFunction,
|
|
20
|
-
Request,
|
|
21
|
-
RequestHandler,
|
|
22
|
-
Response,
|
|
23
|
-
Router,
|
|
24
|
-
} from "express";
|
|
25
|
-
import { assertType, describe, expectTypeOf, test } from "vitest";
|
|
26
|
-
import type { ZodType } from "zod";
|
|
27
|
-
import type * as barrel from "../index.js";
|
|
28
|
-
import {
|
|
29
|
-
type ApiExpressEndpointAdapter,
|
|
30
|
-
type AuthMiddlewareOptions,
|
|
31
|
-
apiExpressRouterFactory,
|
|
32
|
-
authMiddleware,
|
|
33
|
-
createApiExpressDefaultEndpointAdapter,
|
|
34
|
-
handleErrors,
|
|
35
|
-
requestLoggerMiddleware,
|
|
36
|
-
verifySignature,
|
|
37
|
-
} from "../index.js";
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* These pin the package's public type surface. Consumers wire routers and
|
|
41
|
-
* middleware from this barrel alone, so a widened parameter or a dropped export
|
|
42
|
-
* is a break they would only find at their own call sites.
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
describe("the barrel's shape", () => {
|
|
46
|
-
test("exports exactly the intended names", () => {
|
|
47
|
-
expectTypeOf<keyof typeof barrel>().toEqualTypeOf<
|
|
48
|
-
| "apiExpressRouterFactory"
|
|
49
|
-
| "createApiExpressDefaultEndpointAdapter"
|
|
50
|
-
| "handleErrors"
|
|
51
|
-
| "authMiddleware"
|
|
52
|
-
| "createMemoryJwtReplayGuard"
|
|
53
|
-
| "verifySignature"
|
|
54
|
-
| "requestLoggerMiddleware"
|
|
55
|
-
>();
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("createApiExpressDefaultEndpointAdapter", () => {
|
|
60
|
-
test("takes a log level and an optional status code", () => {
|
|
61
|
-
expectTypeOf(
|
|
62
|
-
createApiExpressDefaultEndpointAdapter,
|
|
63
|
-
).parameters.toEqualTypeOf<
|
|
64
|
-
[logLevel: LogLevel, errorStatusCode?: number]
|
|
65
|
-
>();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("returns the interface, not the concrete class", () => {
|
|
69
|
-
// Callers depend on the interface so an alternative adapter can be
|
|
70
|
-
// substituted without a cast.
|
|
71
|
-
expectTypeOf(
|
|
72
|
-
createApiExpressDefaultEndpointAdapter,
|
|
73
|
-
).returns.toEqualTypeOf<ApiExpressEndpointAdapter>();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("rejects a log level that is not one of the known levels", () => {
|
|
77
|
-
// @ts-expect-error - an arbitrary string is not a LogLevel
|
|
78
|
-
createApiExpressDefaultEndpointAdapter("chatty");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("rejects a non-numeric status code", () => {
|
|
82
|
-
// @ts-expect-error - the status code is a number
|
|
83
|
-
createApiExpressDefaultEndpointAdapter("info", "500");
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe("the router factory", () => {
|
|
88
|
-
test("takes a routes provider and an adapter, and returns a Router", () => {
|
|
89
|
-
expectTypeOf(apiExpressRouterFactory.createRouter).toEqualTypeOf<
|
|
90
|
-
(
|
|
91
|
-
apiRoutesProvider: ApiRoutesProvider,
|
|
92
|
-
apiExpressEndpointAdapter: ApiExpressEndpointAdapter,
|
|
93
|
-
) => Router
|
|
94
|
-
>();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("rejects a bare routes object in place of a provider", () => {
|
|
98
|
-
const adapter: ApiExpressEndpointAdapter = {
|
|
99
|
-
handleRequest: async () => undefined,
|
|
100
|
-
};
|
|
101
|
-
// @ts-expect-error - the factory wants a provider, not the routes it returns
|
|
102
|
-
apiExpressRouterFactory.createRouter({}, adapter);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe("the endpoint adapter interface", () => {
|
|
107
|
-
test("takes the endpoint and the express trio and resolves to void", () => {
|
|
108
|
-
expectTypeOf<ApiExpressEndpointAdapter["handleRequest"]>().toEqualTypeOf<
|
|
109
|
-
(
|
|
110
|
-
endpoint: ApiEndpoint<ZodType | undefined>,
|
|
111
|
-
request: Request,
|
|
112
|
-
response: Response,
|
|
113
|
-
next: NextFunction,
|
|
114
|
-
) => Promise<void>
|
|
115
|
-
>();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test("is satisfied by any object with a matching handleRequest", () => {
|
|
119
|
-
// Structural, deliberately: a consumer can supply its own adapter
|
|
120
|
-
// without importing or extending the default class.
|
|
121
|
-
assertType<ApiExpressEndpointAdapter>({
|
|
122
|
-
handleRequest: async (): Promise<void> => undefined,
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe("the middleware", () => {
|
|
128
|
-
test("authMiddleware takes an optional pair, auth account and options", () => {
|
|
129
|
-
expectTypeOf(authMiddleware).parameters.toEqualTypeOf<
|
|
130
|
-
[
|
|
131
|
-
pair: KeyringPair | undefined,
|
|
132
|
-
authAccount?: KeyringPair | undefined,
|
|
133
|
-
options?: AuthMiddlewareOptions,
|
|
134
|
-
]
|
|
135
|
-
>();
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("authMiddleware returns an async express handler", () => {
|
|
139
|
-
expectTypeOf(authMiddleware).returns.toEqualTypeOf<
|
|
140
|
-
(req: Request, res: Response, next: NextFunction) => Promise<void>
|
|
141
|
-
>();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("authMiddleware still requires the first argument to be passed", () => {
|
|
145
|
-
// It is `KeyringPair | undefined` rather than optional, so a provider
|
|
146
|
-
// with no key has to say so explicitly instead of forgetting to wire it.
|
|
147
|
-
// @ts-expect-error - the first parameter is required
|
|
148
|
-
authMiddleware();
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test("requestLoggerMiddleware returns a synchronous express handler", () => {
|
|
152
|
-
expectTypeOf(requestLoggerMiddleware).returns.toEqualTypeOf<
|
|
153
|
-
(req: Request, res: Response, next: NextFunction) => void
|
|
154
|
-
>();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("the handler each factory builds is usable as express middleware", () => {
|
|
158
|
-
// The exports are factories, so it is what they return — not the
|
|
159
|
-
// factories themselves — that gets mounted with app.use().
|
|
160
|
-
assertType<RequestHandler>(
|
|
161
|
-
requestLoggerMiddleware(
|
|
162
|
-
{} as Parameters<typeof requestLoggerMiddleware>[0],
|
|
163
|
-
),
|
|
164
|
-
);
|
|
165
|
-
assertType<RequestHandler>(authMiddleware(undefined));
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test("verifySignature takes a signature, a message and a pair, and returns void", () => {
|
|
169
|
-
expectTypeOf(verifySignature).parameters.toEqualTypeOf<
|
|
170
|
-
[signature: string, message: string, pair: KeyringPair]
|
|
171
|
-
>();
|
|
172
|
-
expectTypeOf(verifySignature).returns.toEqualTypeOf<void>();
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test("verifySignature rejects a byte-array signature", () => {
|
|
176
|
-
// The hex string is decoded internally; passing bytes would be decoded
|
|
177
|
-
// a second time and silently verify the wrong value.
|
|
178
|
-
// @ts-expect-error - the signature is a hex string
|
|
179
|
-
verifySignature(new Uint8Array([1]), "msg", {} as KeyringPair);
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe("the error handler", () => {
|
|
184
|
-
test("takes an error first, which is what makes express treat it as one", () => {
|
|
185
|
-
// A three-parameter handler would be mounted as ordinary middleware and
|
|
186
|
-
// never see an error at all.
|
|
187
|
-
expectTypeOf(handleErrors).parameters.toMatchTypeOf<
|
|
188
|
-
[unknown, Request, Response, NextFunction]
|
|
189
|
-
>();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("rejects being mounted as an ordinary handler", () => {
|
|
193
|
-
// @ts-expect-error - the first parameter is an error, not a request
|
|
194
|
-
assertType<RequestHandler>(handleErrors);
|
|
195
|
-
});
|
|
196
|
-
});
|