albert-heijn-mcp 1.3.1 → 1.4.0
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.md +15 -7
- package/dist/server/http.js +23 -42
- package/dist/server/http.js.map +1 -1
- package/dist/server/oauth.js +375 -0
- package/dist/server/oauth.js.map +1 -0
- package/dist/server/request.js +22 -0
- package/dist/server/request.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -137,14 +137,16 @@ Where the settings live differs per client; see its documentation. Clients with
|
|
|
137
137
|
|
|
138
138
|
### Remote clients (Streamable HTTP)
|
|
139
139
|
|
|
140
|
-
Web apps such as ChatGPT and Claude.ai only connect to servers on the internet. Set one up first ([Deploying to a server](#deploying-to-a-server)). The endpoint is `https://your-server/mcp`.
|
|
140
|
+
Web apps such as ChatGPT and Claude.ai only connect to servers on the internet. Set one up first ([Deploying to a server](#deploying-to-a-server)). The endpoint is `https://your-server/mcp`.
|
|
141
141
|
|
|
142
|
-
|
|
142
|
+
Clients that support OAuth log in to the server themselves: add the endpoint with OAuth (or automatic) authentication, and the client opens a login page on your server. Enter your `AH_MCP_TOKEN` there once; the client then gets its own tokens and renews them. Clients without OAuth send `AH_MCP_TOKEN` as an `Authorization: Bearer YOUR_TOKEN` header, or in the URL as `https://your-server/mcp?token=YOUR_TOKEN`.
|
|
143
143
|
|
|
144
|
-
**
|
|
144
|
+
**ChatGPT**: needs Developer mode (Plus, Pro, Business, Enterprise and Education). Open Settings → advanced settings, turn on Developer mode, and create a connector with the endpoint. Set authentication to **OAuth**.
|
|
145
|
+
|
|
146
|
+
**Claude.ai**: Settings → Connectors → Add custom connector, then paste the endpoint and choose Connect.
|
|
145
147
|
|
|
146
148
|
> [!IMPORTANT]
|
|
147
|
-
>
|
|
149
|
+
> Anyone with `AH_MCP_TOKEN` can use your Albert Heijn account. Use a long random value (`openssl rand -hex 32`). Changing it also logs out every OAuth client. A token in a URL can end up in proxy logs, so prefer OAuth or the header.
|
|
148
150
|
|
|
149
151
|
## Configuration
|
|
150
152
|
|
|
@@ -156,8 +158,8 @@ Settings are environment variables. They can also go in a `.env` file in the wor
|
|
|
156
158
|
| `AH_TOKENS_PATH` | [per OS](#logging-in) | Where to store login tokens. |
|
|
157
159
|
| `AH_MCP_HOST` | `127.0.0.1` | Interface the HTTP server listens on. Keep the default behind a reverse proxy. |
|
|
158
160
|
| `AH_MCP_PORT` | `3000` | HTTP server port. |
|
|
159
|
-
| `AH_MCP_BASE_URL` | `http://localhost:3000` | Public URL of the HTTP server. Set it on a server: for a non-local URL
|
|
160
|
-
| `AH_MCP_TOKEN` | — | Secret
|
|
161
|
+
| `AH_MCP_BASE_URL` | `http://localhost:3000` | Public URL of the HTTP server. Set it on a server: OAuth clients are sent to this URL to log in, and for a non-local URL the localhost-only `Host` check is turned off so a reverse proxy can forward requests. |
|
|
162
|
+
| `AH_MCP_TOKEN` | — | Secret for the HTTP transport, which doesn't start without it. Clients send it as `Authorization: Bearer …` or `?token=…`, or enter it on the OAuth login page. |
|
|
161
163
|
| `AH_LOG_FILE` | — | Also append logs to this file. Logs always go to stderr. |
|
|
162
164
|
|
|
163
165
|
Command-line flags:
|
|
@@ -166,7 +168,7 @@ Command-line flags:
|
|
|
166
168
|
node dist/index.js [--transport stdio|streamable-http] [--remote] [--version] [--help]
|
|
167
169
|
```
|
|
168
170
|
|
|
169
|
-
`stdio` (the default) is for local clients; `streamable-http` serves MCP at `/mcp`.
|
|
171
|
+
`stdio` (the default) is for local clients; `streamable-http` serves MCP at `/mcp`, with OAuth login at `/authorize`.
|
|
170
172
|
|
|
171
173
|
## Deploying to a server
|
|
172
174
|
|
|
@@ -369,6 +371,12 @@ AH accepts order changes only once an order exists. Choose a delivery slot in th
|
|
|
369
371
|
Choosing a slot moved your list into the order. Use `ah_get_cart` and `ah_update_cart_item` until the order is delivered or cancelled.
|
|
370
372
|
</details>
|
|
371
373
|
|
|
374
|
+
<details>
|
|
375
|
+
<summary><b>OAuth login opens at localhost, or the client can't reach it</b></summary>
|
|
376
|
+
|
|
377
|
+
Set `AH_MCP_BASE_URL` to the server's public `https://` URL and restart it. Clients are sent there to log in.
|
|
378
|
+
</details>
|
|
379
|
+
|
|
372
380
|
<details>
|
|
373
381
|
<summary><b>Port 3000 is in use</b></summary>
|
|
374
382
|
|
package/dist/server/http.js
CHANGED
|
@@ -1,39 +1,14 @@
|
|
|
1
|
-
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
1
|
import { createServer } from "node:http";
|
|
3
2
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
3
|
import { log } from "../log.js";
|
|
4
|
+
import { OAuth } from "./oauth.js";
|
|
5
|
+
import { isLoopbackHost, readBody } from "./request.js";
|
|
5
6
|
const MCP_PATH = "/mcp";
|
|
6
7
|
const MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
7
|
-
function isLoopbackHost(host) {
|
|
8
|
-
const h = host.replace(/^\[|\]$/g, "");
|
|
9
|
-
return h === "localhost" || h === "::1" || /^127\./.test(h);
|
|
10
|
-
}
|
|
11
8
|
/** Reports whether url's host is localhost or loopback; an unparsable url is not. */
|
|
12
9
|
function isLocalUrl(url) {
|
|
13
10
|
return URL.canParse(url) && isLoopbackHost(new URL(url).hostname);
|
|
14
11
|
}
|
|
15
|
-
const sha256 = (s) => createHash("sha256").update(s).digest();
|
|
16
|
-
/** Checks "Authorization: Bearer <token>" or "?token=<token>" in constant time. */
|
|
17
|
-
function hasToken(req, token) {
|
|
18
|
-
// Hashing makes the lengths equal, so the comparison doesn't reveal the token's length either.
|
|
19
|
-
const expected = sha256(token);
|
|
20
|
-
const matches = (candidate) => Boolean(candidate) && timingSafeEqual(sha256(candidate), expected);
|
|
21
|
-
const auth = req.headers.authorization;
|
|
22
|
-
if (auth?.startsWith("Bearer ") && matches(auth.slice("Bearer ".length)))
|
|
23
|
-
return true;
|
|
24
|
-
return matches(new URL(req.url ?? "/", "http://localhost").searchParams.get("token"));
|
|
25
|
-
}
|
|
26
|
-
async function readJsonBody(req) {
|
|
27
|
-
const chunks = [];
|
|
28
|
-
let size = 0;
|
|
29
|
-
for await (const chunk of req) {
|
|
30
|
-
size += chunk.length;
|
|
31
|
-
if (size > MAX_BODY_BYTES)
|
|
32
|
-
throw new Error("request body too large");
|
|
33
|
-
chunks.push(chunk);
|
|
34
|
-
}
|
|
35
|
-
return chunks.length ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : undefined;
|
|
36
|
-
}
|
|
37
12
|
/** Serves MCP at /mcp, stateless: each request gets a new server. Resolves once listening. */
|
|
38
13
|
export async function serveHttp(newServer, opts) {
|
|
39
14
|
// Anyone who reaches the server acts as the logged-in AH account, and a
|
|
@@ -47,29 +22,35 @@ export async function serveHttp(newServer, opts) {
|
|
|
47
22
|
const local = isLocalUrl(opts.baseUrl) && isLoopbackHost(opts.host);
|
|
48
23
|
// Clients omit the port for port 80; a local proxy may listen on another port (the base URL's).
|
|
49
24
|
const allowedHosts = local
|
|
50
|
-
? [
|
|
51
|
-
...
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
]),
|
|
55
|
-
]
|
|
25
|
+
? new Set([
|
|
26
|
+
...["localhost", "127.0.0.1", "[::1]"].flatMap((h) => (opts.port === 80 ? [h, `${h}:80`] : [`${h}:${opts.port}`])),
|
|
27
|
+
new URL(opts.baseUrl).host,
|
|
28
|
+
])
|
|
56
29
|
: undefined;
|
|
30
|
+
// Web clients such as ChatGPT log in with OAuth; others send the token itself.
|
|
31
|
+
const oauth = new OAuth(opts.baseUrl, MCP_PATH, token);
|
|
57
32
|
const handle = async (req, res) => {
|
|
58
|
-
|
|
33
|
+
// Checked here rather than in the MCP transport, so it covers the OAuth routes too.
|
|
34
|
+
if (allowedHosts && !allowedHosts.has(req.headers.host ?? "")) {
|
|
35
|
+
res.writeHead(403).end("Forbidden");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
39
|
+
if (await oauth.handle(req, res, url))
|
|
40
|
+
return;
|
|
41
|
+
if (url.pathname !== MCP_PATH) {
|
|
59
42
|
res.writeHead(404).end("Not Found");
|
|
60
43
|
return;
|
|
61
44
|
}
|
|
62
|
-
|
|
63
|
-
|
|
45
|
+
const challenge = oauth.authenticate(req, url);
|
|
46
|
+
if (challenge) {
|
|
47
|
+
res.writeHead(401, { "WWW-Authenticate": challenge }).end("Unauthorized");
|
|
64
48
|
return;
|
|
65
49
|
}
|
|
66
|
-
const
|
|
50
|
+
const raw = req.method === "POST" ? await readBody(req, MAX_BODY_BYTES) : "";
|
|
51
|
+
const body = raw ? JSON.parse(raw) : undefined;
|
|
67
52
|
const server = newServer();
|
|
68
|
-
const transport = new StreamableHTTPServerTransport({
|
|
69
|
-
sessionIdGenerator: undefined,
|
|
70
|
-
enableDnsRebindingProtection: local,
|
|
71
|
-
allowedHosts,
|
|
72
|
-
});
|
|
53
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
73
54
|
res.on("close", () => {
|
|
74
55
|
void transport.close();
|
|
75
56
|
void server.close();
|
package/dist/server/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/server/http.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/server/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { setTimeout } from "node:timers/promises";
|
|
3
|
+
import { log } from "../log.js";
|
|
4
|
+
import { isLoopbackHost, readBody, sameSecret, sha256 } from "./request.js";
|
|
5
|
+
// OAuth 2.1 for clients that connect over HTTP, e.g. ChatGPT and Claude.ai: the
|
|
6
|
+
// server is its own authorization server, and the owner logs in with AH_MCP_TOKEN.
|
|
7
|
+
//
|
|
8
|
+
// Nothing is stored. Client IDs, login codes and tokens are signed with a key
|
|
9
|
+
// derived from AH_MCP_TOKEN, so they survive restarts and changing the token
|
|
10
|
+
// revokes them all. Login codes are single-use within this process. Clients
|
|
11
|
+
// without OAuth send AH_MCP_TOKEN itself.
|
|
12
|
+
const CODE_TTL = 60;
|
|
13
|
+
const ACCESS_TTL = 60 * 60;
|
|
14
|
+
const REFRESH_TTL = 30 * 24 * 60 * 60;
|
|
15
|
+
const MAX_FORM_BYTES = 64 * 1024;
|
|
16
|
+
/**
|
|
17
|
+
* Delay before answering a wrong token. It slows guessing without locking anyone
|
|
18
|
+
* out: a lockout would let anyone who can reach the server block the owner's login.
|
|
19
|
+
*/
|
|
20
|
+
const FAILED_LOGIN_DELAY_MS = 1000;
|
|
21
|
+
class OAuthError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
description;
|
|
24
|
+
status;
|
|
25
|
+
constructor(code, description, status = 400) {
|
|
26
|
+
super(`${code}: ${description}`);
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.description = description;
|
|
29
|
+
this.status = status;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const now = () => Math.floor(Date.now() / 1000);
|
|
33
|
+
const b64url = (b) => Buffer.from(b).toString("base64url");
|
|
34
|
+
const clientHash = (clientId) => b64url(sha256(clientId)).slice(0, 22);
|
|
35
|
+
/** The OAuth routes (handle) and the check for MCP requests (authenticate). */
|
|
36
|
+
export class OAuth {
|
|
37
|
+
#key;
|
|
38
|
+
#tokenHash;
|
|
39
|
+
#issuer;
|
|
40
|
+
#resource;
|
|
41
|
+
#resourceMetadataPath;
|
|
42
|
+
#challenge;
|
|
43
|
+
/** JSON bodies of the two metadata documents, which never change. */
|
|
44
|
+
#resourceMetadata;
|
|
45
|
+
#serverMetadata;
|
|
46
|
+
#usedCodes = new Map();
|
|
47
|
+
/** baseUrl is the public URL of the server; MCP is served at mcpPath under it. */
|
|
48
|
+
constructor(baseUrl, mcpPath, token) {
|
|
49
|
+
this.#issuer = baseUrl.replace(/\/+$/, "");
|
|
50
|
+
this.#resource = this.#issuer + mcpPath;
|
|
51
|
+
this.#tokenHash = sha256(token);
|
|
52
|
+
this.#key = createHmac("sha256", token).update("albert-heijn-mcp oauth v1").digest();
|
|
53
|
+
this.#resourceMetadataPath = `/.well-known/oauth-protected-resource${new URL(this.#resource).pathname}`;
|
|
54
|
+
this.#challenge = `Bearer resource_metadata="${this.#issuer}${this.#resourceMetadataPath}"`;
|
|
55
|
+
this.#resourceMetadata = JSON.stringify({
|
|
56
|
+
resource: this.#resource,
|
|
57
|
+
authorization_servers: [this.#issuer],
|
|
58
|
+
bearer_methods_supported: ["header"],
|
|
59
|
+
});
|
|
60
|
+
this.#serverMetadata = JSON.stringify({
|
|
61
|
+
issuer: this.#issuer,
|
|
62
|
+
authorization_endpoint: `${this.#issuer}/authorize`,
|
|
63
|
+
token_endpoint: `${this.#issuer}/token`,
|
|
64
|
+
registration_endpoint: `${this.#issuer}/register`,
|
|
65
|
+
response_types_supported: ["code"],
|
|
66
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
67
|
+
code_challenge_methods_supported: ["S256"],
|
|
68
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
69
|
+
authorization_response_iss_parameter_supported: true,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Checks an MCP request: AH_MCP_TOKEN or an access token as "Authorization: Bearer …",
|
|
74
|
+
* or AH_MCP_TOKEN as "?token=…". Returns the WWW-Authenticate header for a 401, or undefined if allowed.
|
|
75
|
+
*/
|
|
76
|
+
authenticate(req, url) {
|
|
77
|
+
const auth = req.headers.authorization;
|
|
78
|
+
const bearer = auth?.startsWith("Bearer ") ? auth.slice("Bearer ".length) : undefined;
|
|
79
|
+
if (bearer && (sameSecret(bearer, this.#tokenHash) || this.#verify("access", bearer)))
|
|
80
|
+
return undefined;
|
|
81
|
+
const query = url.searchParams.get("token");
|
|
82
|
+
if (query && sameSecret(query, this.#tokenHash))
|
|
83
|
+
return undefined;
|
|
84
|
+
return bearer === undefined ? this.#challenge : `${this.#challenge}, error="invalid_token"`;
|
|
85
|
+
}
|
|
86
|
+
/** Serves the OAuth routes; returns false for other paths. */
|
|
87
|
+
async handle(req, res, url) {
|
|
88
|
+
const route = this.#route(url.pathname);
|
|
89
|
+
if (!route)
|
|
90
|
+
return false;
|
|
91
|
+
// Browser-based clients call these from another origin; none use cookies.
|
|
92
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
93
|
+
if (req.method === "OPTIONS") {
|
|
94
|
+
res
|
|
95
|
+
.writeHead(204, {
|
|
96
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
97
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, MCP-Protocol-Version",
|
|
98
|
+
"Access-Control-Max-Age": "86400",
|
|
99
|
+
})
|
|
100
|
+
.end();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (!route.methods.includes(req.method ?? "")) {
|
|
104
|
+
res.writeHead(405, { Allow: route.methods.join(", ") }).end("Method Not Allowed");
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await route.serve(req, res, url);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
if (!(err instanceof OAuthError))
|
|
112
|
+
throw err;
|
|
113
|
+
sendJson(res, err.status, { error: err.code, error_description: err.description });
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
#route(path) {
|
|
118
|
+
switch (path) {
|
|
119
|
+
case "/.well-known/oauth-protected-resource":
|
|
120
|
+
case this.#resourceMetadataPath:
|
|
121
|
+
return { methods: ["GET"], serve: (_req, res) => sendJson(res, 200, this.#resourceMetadata) };
|
|
122
|
+
case "/.well-known/oauth-authorization-server":
|
|
123
|
+
return { methods: ["GET"], serve: (_req, res) => sendJson(res, 200, this.#serverMetadata) };
|
|
124
|
+
case "/register":
|
|
125
|
+
return { methods: ["POST"], serve: (req, res) => this.#register(req, res) };
|
|
126
|
+
case "/authorize":
|
|
127
|
+
return { methods: ["GET", "POST"], serve: (req, res, url) => this.#authorize(req, res, url) };
|
|
128
|
+
case "/token":
|
|
129
|
+
return { methods: ["POST"], serve: (req, res) => this.#tokenEndpoint(req, res) };
|
|
130
|
+
default:
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Dynamic client registration (RFC 7591): the client ID is the signed registration. */
|
|
135
|
+
async #register(req, res) {
|
|
136
|
+
let meta;
|
|
137
|
+
try {
|
|
138
|
+
meta = (JSON.parse(await readOAuthBody(req)) ?? {});
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (err instanceof OAuthError)
|
|
142
|
+
throw err;
|
|
143
|
+
throw new OAuthError("invalid_client_metadata", "the body must be JSON");
|
|
144
|
+
}
|
|
145
|
+
const uris = meta.redirect_uris;
|
|
146
|
+
if (!Array.isArray(uris) || uris.length === 0 || uris.length > 10) {
|
|
147
|
+
throw new OAuthError("invalid_redirect_uri", "redirect_uris must list 1 to 10 URIs");
|
|
148
|
+
}
|
|
149
|
+
if (!uris.every((uri) => typeof uri === "string" && isAllowedRedirect(uri))) {
|
|
150
|
+
throw new OAuthError("invalid_redirect_uri", "redirect URIs must be https, http on localhost, or an app scheme");
|
|
151
|
+
}
|
|
152
|
+
const name = typeof meta.client_name === "string" ? meta.client_name.slice(0, 100) : undefined;
|
|
153
|
+
sendJson(res, 201, {
|
|
154
|
+
client_id: this.#sign("client", { redirectUris: uris, name }),
|
|
155
|
+
client_id_issued_at: now(),
|
|
156
|
+
redirect_uris: uris,
|
|
157
|
+
client_name: name,
|
|
158
|
+
token_endpoint_auth_method: "none",
|
|
159
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
160
|
+
response_types: ["code"],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/** GET shows the login page; POST checks the password and redirects back with a code. */
|
|
164
|
+
async #authorize(req, res, url) {
|
|
165
|
+
const params = req.method === "POST" ? new URLSearchParams(await readOAuthBody(req)) : url.searchParams;
|
|
166
|
+
const clientId = params.get("client_id") ?? "";
|
|
167
|
+
const redirectUri = params.get("redirect_uri") ?? "";
|
|
168
|
+
const client = this.#verify("client", clientId);
|
|
169
|
+
// Without a known client and redirect URI, errors can't go back to the client.
|
|
170
|
+
if (!client || !client.redirectUris.includes(redirectUri)) {
|
|
171
|
+
sendPage(res, 400, errorPage("This login link is not valid. Start connecting again from your app."));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const back = (query) => {
|
|
175
|
+
const url = new URL(redirectUri);
|
|
176
|
+
for (const [k, v] of Object.entries({ ...query, iss: this.#issuer }))
|
|
177
|
+
url.searchParams.set(k, v);
|
|
178
|
+
const state = params.get("state");
|
|
179
|
+
if (state)
|
|
180
|
+
url.searchParams.set("state", state);
|
|
181
|
+
res.writeHead(302, { Location: url.href, "Cache-Control": "no-store" }).end();
|
|
182
|
+
};
|
|
183
|
+
const challenge = params.get("code_challenge") ?? "";
|
|
184
|
+
if (params.get("response_type") !== "code")
|
|
185
|
+
return back({ error: "unsupported_response_type" });
|
|
186
|
+
if (params.get("code_challenge_method") !== "S256" || !/^[A-Za-z0-9_-]{43}$/.test(challenge)) {
|
|
187
|
+
return back({ error: "invalid_request", error_description: "PKCE with S256 is required" });
|
|
188
|
+
}
|
|
189
|
+
if (!this.#isOurResource(params.get("resource")))
|
|
190
|
+
return back({ error: "invalid_target" });
|
|
191
|
+
const showLogin = (status, error) => sendPage(res, status, loginPage(this.#issuer, params, client, redirectUri, error));
|
|
192
|
+
if (req.method !== "POST")
|
|
193
|
+
return showLogin(200);
|
|
194
|
+
if (params.get("action") === "deny")
|
|
195
|
+
return back({ error: "access_denied" });
|
|
196
|
+
if (!sameSecret(params.get("password") ?? "", this.#tokenHash)) {
|
|
197
|
+
await setTimeout(FAILED_LOGIN_DELAY_MS);
|
|
198
|
+
log.warn("OAuth login failed", { client: client.name ?? "unnamed" });
|
|
199
|
+
return showLogin(401, "That's not the right token.");
|
|
200
|
+
}
|
|
201
|
+
const code = {
|
|
202
|
+
client: clientHash(clientId),
|
|
203
|
+
redirectUri,
|
|
204
|
+
challenge,
|
|
205
|
+
jti: b64url(randomBytes(12)),
|
|
206
|
+
exp: now() + CODE_TTL,
|
|
207
|
+
};
|
|
208
|
+
log.info("OAuth login", { client: client.name ?? "unnamed" });
|
|
209
|
+
back({ code: this.#sign("code", code) });
|
|
210
|
+
}
|
|
211
|
+
async #tokenEndpoint(req, res) {
|
|
212
|
+
const params = new URLSearchParams(await readOAuthBody(req));
|
|
213
|
+
const clientId = params.get("client_id") ?? "";
|
|
214
|
+
if (!this.#verify("client", clientId))
|
|
215
|
+
throw new OAuthError("invalid_client", "unknown client", 401);
|
|
216
|
+
if (!this.#isOurResource(params.get("resource")))
|
|
217
|
+
throw new OAuthError("invalid_target", "unknown resource");
|
|
218
|
+
const client = clientHash(clientId);
|
|
219
|
+
switch (params.get("grant_type")) {
|
|
220
|
+
case "authorization_code": {
|
|
221
|
+
const code = this.#verify("code", params.get("code") ?? "");
|
|
222
|
+
if (!code || code.client !== client || code.redirectUri !== params.get("redirect_uri")) {
|
|
223
|
+
throw new OAuthError("invalid_grant", "the code is invalid or expired");
|
|
224
|
+
}
|
|
225
|
+
const verifier = params.get("code_verifier") ?? "";
|
|
226
|
+
if (!/^[A-Za-z0-9._~-]{43,128}$/.test(verifier) || b64url(sha256(verifier)) !== code.challenge) {
|
|
227
|
+
throw new OAuthError("invalid_grant", "the code verifier does not match");
|
|
228
|
+
}
|
|
229
|
+
if (!this.#useCode(code))
|
|
230
|
+
throw new OAuthError("invalid_grant", "the code was already used");
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
case "refresh_token": {
|
|
234
|
+
const refresh = this.#verify("refresh", params.get("refresh_token") ?? "");
|
|
235
|
+
if (!refresh || refresh.client !== client)
|
|
236
|
+
throw new OAuthError("invalid_grant", "the refresh token is invalid or expired");
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
default:
|
|
240
|
+
throw new OAuthError("unsupported_grant_type", "use authorization_code or refresh_token");
|
|
241
|
+
}
|
|
242
|
+
const t = now();
|
|
243
|
+
sendJson(res, 200, {
|
|
244
|
+
access_token: this.#sign("access", { client, exp: t + ACCESS_TTL }),
|
|
245
|
+
token_type: "Bearer",
|
|
246
|
+
expires_in: ACCESS_TTL,
|
|
247
|
+
refresh_token: this.#sign("refresh", { client, exp: t + REFRESH_TTL }),
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
/** Clients may name the MCP endpoint or the server as the resource (RFC 8707), or omit it. */
|
|
251
|
+
#isOurResource(resource) {
|
|
252
|
+
if (!resource)
|
|
253
|
+
return true;
|
|
254
|
+
return [this.#resource, this.#issuer].includes(resource.replace(/\/+$/, ""));
|
|
255
|
+
}
|
|
256
|
+
#useCode(code) {
|
|
257
|
+
const t = now();
|
|
258
|
+
for (const [jti, exp] of this.#usedCodes)
|
|
259
|
+
if (exp < t)
|
|
260
|
+
this.#usedCodes.delete(jti);
|
|
261
|
+
if (this.#usedCodes.has(code.jti))
|
|
262
|
+
return false;
|
|
263
|
+
this.#usedCodes.set(code.jti, code.exp);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
/** kind.payload.signature; the kind is signed too, so one kind can't pass for another. */
|
|
267
|
+
#sign(kind, claims) {
|
|
268
|
+
const payload = b64url(JSON.stringify(claims));
|
|
269
|
+
return `${payload}.${this.#mac(kind, payload)}`;
|
|
270
|
+
}
|
|
271
|
+
#verify(kind, value) {
|
|
272
|
+
const [payload, mac, extra] = value.split(".");
|
|
273
|
+
if (!payload || !mac || extra !== undefined)
|
|
274
|
+
return undefined;
|
|
275
|
+
const expected = Buffer.from(this.#mac(kind, payload));
|
|
276
|
+
const given = Buffer.from(mac);
|
|
277
|
+
if (given.length !== expected.length || !timingSafeEqual(given, expected))
|
|
278
|
+
return undefined;
|
|
279
|
+
try {
|
|
280
|
+
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
281
|
+
if (claims.exp !== undefined && claims.exp < now())
|
|
282
|
+
return undefined;
|
|
283
|
+
return claims;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
#mac(kind, payload) {
|
|
290
|
+
return createHmac("sha256", this.#key).update(`${kind}.${payload}`).digest("base64url");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/** https anywhere, http only on loopback, or an app's own scheme (e.g. cursor://); never script or data URLs. */
|
|
294
|
+
function isAllowedRedirect(uri) {
|
|
295
|
+
if (!URL.canParse(uri))
|
|
296
|
+
return false;
|
|
297
|
+
const url = new URL(uri);
|
|
298
|
+
if (url.hash)
|
|
299
|
+
return false;
|
|
300
|
+
if (url.protocol === "https:")
|
|
301
|
+
return true;
|
|
302
|
+
if (url.protocol === "http:")
|
|
303
|
+
return isLoopbackHost(url.hostname);
|
|
304
|
+
return !["javascript:", "data:", "file:", "vbscript:", "blob:", "about:", "ftp:", "ws:", "wss:"].includes(url.protocol);
|
|
305
|
+
}
|
|
306
|
+
async function readOAuthBody(req) {
|
|
307
|
+
try {
|
|
308
|
+
return await readBody(req, MAX_FORM_BYTES);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
if (err instanceof Error && err.message === "request body too large")
|
|
312
|
+
throw new OAuthError("invalid_request", err.message, 413);
|
|
313
|
+
throw err;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/** body is an object to serialise, or JSON already serialised. */
|
|
317
|
+
function sendJson(res, status, body) {
|
|
318
|
+
res
|
|
319
|
+
.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" })
|
|
320
|
+
.end(typeof body === "string" ? body : JSON.stringify(body));
|
|
321
|
+
}
|
|
322
|
+
function sendPage(res, status, html) {
|
|
323
|
+
res
|
|
324
|
+
.writeHead(status, {
|
|
325
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
326
|
+
"Cache-Control": "no-store",
|
|
327
|
+
// No scripts, no framing (clickjacking), and the login link stays out of Referer headers.
|
|
328
|
+
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; img-src data:; frame-ancestors 'none'",
|
|
329
|
+
"X-Frame-Options": "DENY",
|
|
330
|
+
"Referrer-Policy": "no-referrer",
|
|
331
|
+
})
|
|
332
|
+
.end(html);
|
|
333
|
+
}
|
|
334
|
+
const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
335
|
+
/** Request parameters carried through the login form. */
|
|
336
|
+
const FORWARDED = ["response_type", "client_id", "redirect_uri", "code_challenge", "code_challenge_method", "state", "scope", "resource"];
|
|
337
|
+
function loginPage(issuer, params, client, redirectUri, error) {
|
|
338
|
+
const hidden = FORWARDED.flatMap((k) => {
|
|
339
|
+
const v = params.get(k);
|
|
340
|
+
return v === null ? [] : [`<input type="hidden" name="${k}" value="${escapeHtml(v)}">`];
|
|
341
|
+
}).join("");
|
|
342
|
+
const app = escapeHtml(client.name ?? "An app");
|
|
343
|
+
const target = escapeHtml(new URL(redirectUri).host || new URL(redirectUri).protocol);
|
|
344
|
+
return page("Connect to Albert Heijn", `<h1>Connect to Albert Heijn</h1>
|
|
345
|
+
<p><strong>${app}</strong> wants to use this albert-heijn-mcp server, with access to your Albert Heijn account. You'll be sent back to <strong>${target}</strong>.</p>
|
|
346
|
+
${error ? `<p class="error" role="alert">${escapeHtml(error)}</p>` : ""}
|
|
347
|
+
<form method="post" action="${escapeHtml(issuer)}/authorize">${hidden}
|
|
348
|
+
<label for="password">Server token (AH_MCP_TOKEN)</label>
|
|
349
|
+
<input id="password" name="password" type="password" autocomplete="current-password" required autofocus>
|
|
350
|
+
<div class="actions"><button type="submit" name="action" value="allow">Connect</button>
|
|
351
|
+
<button type="submit" name="action" value="deny" class="secondary" formnovalidate>Cancel</button></div>
|
|
352
|
+
</form>`);
|
|
353
|
+
}
|
|
354
|
+
function errorPage(message) {
|
|
355
|
+
return page("Can't connect", `<h1>Can't connect</h1><p>${escapeHtml(message)}</p>`);
|
|
356
|
+
}
|
|
357
|
+
function page(title, body) {
|
|
358
|
+
return `<!doctype html>
|
|
359
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
360
|
+
<title>${escapeHtml(title)}</title>
|
|
361
|
+
<style>
|
|
362
|
+
:root{color-scheme:light dark;--bg:#f4f6f8;--card:#fff;--fg:#1a1a1a;--muted:#5b6470;--accent:#00ade6;--border:#d6dbe0;--err:#c62828}
|
|
363
|
+
@media (prefers-color-scheme:dark){:root{--bg:#121417;--card:#1c1f24;--fg:#eceff1;--muted:#9aa3ad;--border:#343a42;--err:#ef6c6c}}
|
|
364
|
+
body{margin:0;min-height:100vh;display:grid;place-items:center;background:var(--bg);color:var(--fg);font:16px/1.5 system-ui,sans-serif}
|
|
365
|
+
main{box-sizing:border-box;width:min(420px,100% - 32px);background:var(--card);border:1px solid var(--border);border-radius:12px;padding:28px}
|
|
366
|
+
h1{font-size:1.3rem;margin:0 0 12px}p{color:var(--muted);margin:0 0 16px}strong{color:var(--fg)}
|
|
367
|
+
label{display:block;font-weight:600;margin-bottom:6px}
|
|
368
|
+
input[type=password]{box-sizing:border-box;width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:8px;background:transparent;color:inherit;font:inherit}
|
|
369
|
+
.actions{display:flex;gap:8px;margin-top:16px}
|
|
370
|
+
button{flex:1;padding:10px;border:0;border-radius:8px;background:var(--accent);color:#fff;font:inherit;font-weight:600;cursor:pointer}
|
|
371
|
+
button.secondary{background:transparent;color:var(--fg);border:1px solid var(--border)}
|
|
372
|
+
.error{color:var(--err);font-weight:600}
|
|
373
|
+
</style></head><body><main>${body}</main></body></html>`;
|
|
374
|
+
}
|
|
375
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/server/oauth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEvE,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAE5E,gFAAgF;AAChF,mFAAmF;AACnF,EAAE;AACF,8EAA8E;AAC9E,6EAA6E;AAC7E,4EAA4E;AAC5E,0CAA0C;AAE1C,MAAM,QAAQ,GAAG,EAAE,CAAC;AACpB,MAAM,UAAU,GAAG,EAAE,GAAG,EAAE,CAAC;AAC3B,MAAM,WAAW,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AACtC,MAAM,cAAc,GAAG,EAAE,GAAG,IAAI,CAAC;AACjC;;;GAGG;AACH,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAsBnC,MAAM,UAAW,SAAQ,KAAK;IACnB,IAAI,CAAS;IACb,WAAW,CAAS;IACpB,MAAM,CAAS;IAExB,YAAY,IAAY,EAAE,WAAmB,EAAE,MAAM,GAAG,GAAG;QACzD,KAAK,CAAC,GAAG,IAAI,KAAK,WAAW,EAAE,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED,MAAM,GAAG,GAAG,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;AAChD,MAAM,MAAM,GAAG,CAAC,CAAkB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC5E,MAAM,UAAU,GAAG,CAAC,QAAgB,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AAE/E,+EAA+E;AAC/E,MAAM,OAAO,KAAK;IACP,IAAI,CAAS;IACb,UAAU,CAAS;IACnB,OAAO,CAAS;IAChB,SAAS,CAAS;IAClB,qBAAqB,CAAS;IAC9B,UAAU,CAAS;IAC5B,qEAAqE;IAC5D,iBAAiB,CAAS;IAC1B,eAAe,CAAS;IACxB,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEhD,kFAAkF;IAClF,YAAY,OAAe,EAAE,OAAe,EAAE,KAAa;QACzD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC3C,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACxC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,2BAA2B,CAAC,CAAC,MAAM,EAAE,CAAC;QACrF,IAAI,CAAC,qBAAqB,GAAG,wCAAwC,IAAI,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,CAAC;QACxG,IAAI,CAAC,UAAU,GAAG,6BAA6B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,qBAAqB,GAAG,CAAC;QAC5F,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,SAAS,CAAC;YACtC,QAAQ,EAAE,IAAI,CAAC,SAAS;YACxB,qBAAqB,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;YACrC,wBAAwB,EAAE,CAAC,QAAQ,CAAC;SACrC,CAAC,CAAC;QACH,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC;YACpC,MAAM,EAAE,IAAI,CAAC,OAAO;YACpB,sBAAsB,EAAE,GAAG,IAAI,CAAC,OAAO,YAAY;YACnD,cAAc,EAAE,GAAG,IAAI,CAAC,OAAO,QAAQ;YACvC,qBAAqB,EAAE,GAAG,IAAI,CAAC,OAAO,WAAW;YACjD,wBAAwB,EAAE,CAAC,MAAM,CAAC;YAClC,qBAAqB,EAAE,CAAC,oBAAoB,EAAE,eAAe,CAAC;YAC9D,gCAAgC,EAAE,CAAC,MAAM,CAAC;YAC1C,qCAAqC,EAAE,CAAC,MAAM,CAAC;YAC/C,8CAA8C,EAAE,IAAI;SACrD,CAAC,CAAC;IACL,CAAC;IAED;;;OAGG;IACH,YAAY,CAAC,GAAoB,EAAE,GAAQ;QACzC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;QACvC,MAAM,MAAM,GAAG,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACtF,IAAI,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,OAAO,CAAc,QAAQ,EAAE,MAAM,CAAC,CAAC;YAAE,OAAO,SAAS,CAAC;QACrH,MAAM,KAAK,GAAG,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC5C,IAAI,KAAK,IAAI,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,CAAC;YAAE,OAAO,SAAS,CAAC;QAClE,OAAO,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,UAAU,yBAAyB,CAAC;IAC9F,CAAC;IAED,8DAA8D;IAC9D,KAAK,CAAC,MAAM,CAAC,GAAoB,EAAE,GAAmB,EAAE,GAAQ;QAC9D,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAC;QACzB,0EAA0E;QAC1E,GAAG,CAAC,SAAS,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC;QAClD,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC7B,GAAG;iBACA,SAAS,CAAC,GAAG,EAAE;gBACd,8BAA8B,EAAE,oBAAoB;gBACpD,8BAA8B,EAAE,mDAAmD;gBACnF,wBAAwB,EAAE,OAAO;aAClC,CAAC;iBACD,GAAG,EAAE,CAAC;YACT,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,EAAE,CAAC;YAC9C,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;YAClF,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QACnC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,CAAC,GAAG,YAAY,UAAU,CAAC;gBAAE,MAAM,GAAG,CAAC;YAC5C,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,EAAE,iBAAiB,EAAE,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC;QACrF,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,CAAC,IAAY;QACjB,QAAQ,IAAI,EAAE,CAAC;YACb,KAAK,uCAAuC,CAAC;YAC7C,KAAK,IAAI,CAAC,qBAAqB;gBAC7B,OAAO,EAAE,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,iBAAiB,CAAC,EAAE,CAAC;YAChG,KAAK,yCAAyC;gBAC5C,OAAO,EAAE,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC;YAC9F,KAAK,WAAW;gBACd,OAAO,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;YAC9E,KAAK,YAAY;gBACf,OAAO,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;YAChG,KAAK,QAAQ;gBACX,OAAO,EAAE,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;YACnF;gBACE,OAAO,SAAS,CAAC;QACrB,CAAC;IACH,CAAC;IAED,wFAAwF;IACxF,KAAK,CAAC,SAAS,CAAC,GAAoB,EAAE,GAAmB;QACvD,IAAI,IAAwD,CAAC;QAC7D,IAAI,CAAC;YACH,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAgB,CAAC;QACrE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,GAAG,YAAY,UAAU;gBAAE,MAAM,GAAG,CAAC;YACzC,MAAM,IAAI,UAAU,CAAC,yBAAyB,EAAE,uBAAuB,CAAC,CAAC;QAC3E,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,CAAC;QAChC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,EAAE,EAAE,CAAC;YAClE,MAAM,IAAI,UAAU,CAAC,sBAAsB,EAAE,sCAAsC,CAAC,CAAC;QACvF,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,GAAG,KAAK,QAAQ,IAAI,iBAAiB,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YAC5E,MAAM,IAAI,UAAU,CAAC,sBAAsB,EAAE,kEAAkE,CAAC,CAAC;QACnH,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC/F,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE;YACjB,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,IAAI,EAAyB,CAAC;YACpF,mBAAmB,EAAE,GAAG,EAAE;YAC1B,aAAa,EAAE,IAAI;YACnB,WAAW,EAAE,IAAI;YACjB,0BAA0B,EAAE,MAAM;YAClC,WAAW,EAAE,CAAC,oBAAoB,EAAE,eAAe,CAAC;YACpD,cAAc,EAAE,CAAC,MAAM,CAAC;SACzB,CAAC,CAAC;IACL,CAAC;IAED,yFAAyF;IACzF,KAAK,CAAC,UAAU,CAAC,GAAoB,EAAE,GAAmB,EAAE,GAAQ;QAClE,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,eAAe,CAAC,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC;QACxG,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QAC/C,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QACrD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAe,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC9D,+EAA+E;QAC/E,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;YAC1D,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,CAAC,qEAAqE,CAAC,CAAC,CAAC;YACrG,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,KAA6B,EAAE,EAAE;YAC7C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC;YACjC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;gBAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACjG,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAClC,IAAI,KAAK;gBAAE,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YAChD,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,IAAI,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;QAChF,CAAC,CAAC;QACF,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC;QACrD,IAAI,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC,CAAC;QAChG,IAAI,MAAM,CAAC,GAAG,CAAC,uBAAuB,CAAC,KAAK,MAAM,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;YAC7F,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,4BAA4B,EAAE,CAAC,CAAC;QAC7F,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;QAE3F,MAAM,SAAS,GAAG,CAAC,MAAc,EAAE,KAAc,EAAE,EAAE,CACnD,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC,CAAC;QACrF,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM;YAAE,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC;QACjD,IAAI,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC;QAC7E,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/D,MAAM,UAAU,CAAC,qBAAqB,CAAC,CAAC;YACxC,GAAG,CAAC,IAAI,CAAC,oBAAoB,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,IAAI,SAAS,EAAE,CAAC,CAAC;YACrE,OAAO,SAAS,CAAC,GAAG,EAAE,6BAA6B,CAAC,CAAC;QACvD,CAAC;QACD,MAAM,IAAI,GAAe;YACvB,MAAM,EAAE,UAAU,CAAC,QAAQ,CAAC;YAC5B,WAAW;YACX,SAAS;YACT,GAAG,EAAE,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;YAC5B,GAAG,EAAE,GAAG,EAAE,GAAG,QAAQ;SACtB,CAAC;QACF,GAAG,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,IAAI,SAAS,EAAE,CAAC,CAAC;QAC9D,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,GAAoB,EAAE,GAAmB;QAC5D,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,MAAM,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7D,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC;QAC/C,IAAI,CAAC,IAAI,CAAC,OAAO,CAAe,QAAQ,EAAE,QAAQ,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,gBAAgB,EAAE,gBAAgB,EAAE,GAAG,CAAC,CAAC;QACnH,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;YAAE,MAAM,IAAI,UAAU,CAAC,gBAAgB,EAAE,kBAAkB,CAAC,CAAC;QAC7G,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QAEpC,QAAQ,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;YACjC,KAAK,oBAAoB,EAAE,CAAC;gBAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAa,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;gBACxE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,MAAM,IAAI,IAAI,CAAC,WAAW,KAAK,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;oBACvF,MAAM,IAAI,UAAU,CAAC,eAAe,EAAE,gCAAgC,CAAC,CAAC;gBAC1E,CAAC;gBACD,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC;gBACnD,IAAI,CAAC,2BAA2B,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;oBAC/F,MAAM,IAAI,UAAU,CAAC,eAAe,EAAE,kCAAkC,CAAC,CAAC;gBAC5E,CAAC;gBACD,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;oBAAE,MAAM,IAAI,UAAU,CAAC,eAAe,EAAE,2BAA2B,CAAC,CAAC;gBAC7F,MAAM;YACR,CAAC;YACD,KAAK,eAAe,EAAE,CAAC;gBACrB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAc,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC;gBACxF,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM;oBAAE,MAAM,IAAI,UAAU,CAAC,eAAe,EAAE,yCAAyC,CAAC,CAAC;gBAC5H,MAAM;YACR,CAAC;YACD;gBACE,MAAM,IAAI,UAAU,CAAC,wBAAwB,EAAE,yCAAyC,CAAC,CAAC;QAC9F,CAAC;QACD,MAAM,CAAC,GAAG,GAAG,EAAE,CAAC;QAChB,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE;YACjB,YAAY,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,UAAU,EAAwB,CAAC;YACzF,UAAU,EAAE,QAAQ;YACpB,UAAU,EAAE,UAAU;YACtB,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,WAAW,EAAwB,CAAC;SAC7F,CAAC,CAAC;IACL,CAAC;IAED,8FAA8F;IAC9F,cAAc,CAAC,QAAuB;QACpC,IAAI,CAAC,QAAQ;YAAE,OAAO,IAAI,CAAC;QAC3B,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC;IAC/E,CAAC;IAED,QAAQ,CAAC,IAAgB;QACvB,MAAM,CAAC,GAAG,GAAG,EAAE,CAAC;QAChB,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,UAAU;YAAE,IAAI,GAAG,GAAG,CAAC;gBAAE,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACnF,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAChD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;QACxC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,0FAA0F;IAC1F,KAAK,CAAC,IAAU,EAAE,MAAc;QAC9B,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/C,OAAO,GAAG,OAAO,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC;IAClD,CAAC;IAED,OAAO,CAAmB,IAAU,EAAE,KAAa;QACjD,MAAM,CAAC,OAAO,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/C,IAAI,CAAC,OAAO,IAAI,CAAC,GAAG,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QAC9D,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;QACvD,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC;YAAE,OAAO,SAAS,CAAC;QAC5F,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAyB,CAAC;YACtG,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,IAAI,MAAM,CAAC,GAAG,GAAG,GAAG,EAAE;gBAAE,OAAO,SAAS,CAAC;YACrE,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IAED,IAAI,CAAC,IAAU,EAAE,OAAe;QAC9B,OAAO,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC1F,CAAC;CACF;AAED,iHAAiH;AACjH,SAAS,iBAAiB,CAAC,GAAW;IACpC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACrC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACzB,IAAI,GAAG,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IAC3B,IAAI,GAAG,CAAC,QAAQ,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC3C,IAAI,GAAG,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAClE,OAAO,CAAC,CAAC,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AAC1H,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,GAAoB;IAC/C,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAC7C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,OAAO,KAAK,wBAAwB;YAAE,MAAM,IAAI,UAAU,CAAC,iBAAiB,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QAChI,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC;AAED,kEAAkE;AAClE,SAAS,QAAQ,CAAC,GAAmB,EAAE,MAAc,EAAE,IAAqB;IAC1E,GAAG;SACA,SAAS,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,eAAe,EAAE,UAAU,EAAE,CAAC;SACtF,GAAG,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;AACjE,CAAC;AAED,SAAS,QAAQ,CAAC,GAAmB,EAAE,MAAc,EAAE,IAAY;IACjE,GAAG;SACA,SAAS,CAAC,MAAM,EAAE;QACjB,cAAc,EAAE,0BAA0B;QAC1C,eAAe,EAAE,UAAU;QAC3B,0FAA0F;QAC1F,yBAAyB,EAAE,sFAAsF;QACjH,iBAAiB,EAAE,MAAM;QACzB,iBAAiB,EAAE,aAAa;KACjC,CAAC;SACD,GAAG,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,CAAS,EAAE,EAAE,CAC/B,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;AAE9G,yDAAyD;AACzD,MAAM,SAAS,GAAG,CAAC,eAAe,EAAE,WAAW,EAAE,cAAc,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;AAE1I,SAAS,SAAS,CAAC,MAAc,EAAE,MAAuB,EAAE,MAAoB,EAAE,WAAmB,EAAE,KAAc;IACnH,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE;QACrC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACxB,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,8BAA8B,CAAC,YAAY,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACZ,MAAM,GAAG,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,IAAI,QAAQ,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC,IAAI,IAAI,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,CAAC;IACtF,OAAO,IAAI,CACT,yBAAyB,EACzB;aACS,GAAG,iIAAiI,MAAM;EACrJ,KAAK,CAAC,CAAC,CAAC,iCAAiC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;8BACzC,UAAU,CAAC,MAAM,CAAC,eAAe,MAAM;;;;;QAK7D,CACL,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,OAAe;IAChC,OAAO,IAAI,CAAC,eAAe,EAAE,4BAA4B,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;AACtF,CAAC;AAED,SAAS,IAAI,CAAC,KAAa,EAAE,IAAY;IACvC,OAAO;;SAEA,UAAU,CAAC,KAAK,CAAC;;;;;;;;;;;;;6BAaG,IAAI,uBAAuB,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
export function isLoopbackHost(host) {
|
|
3
|
+
const h = host.replace(/^\[|\]$/g, "");
|
|
4
|
+
return h === "localhost" || h === "::1" || /^127\./.test(h);
|
|
5
|
+
}
|
|
6
|
+
export const sha256 = (s) => createHash("sha256").update(s).digest();
|
|
7
|
+
/** Compares a secret in constant time; hashing makes the lengths equal, so it doesn't reveal the length either. */
|
|
8
|
+
export function sameSecret(candidate, expectedHash) {
|
|
9
|
+
return timingSafeEqual(sha256(candidate), expectedHash);
|
|
10
|
+
}
|
|
11
|
+
export async function readBody(req, maxBytes) {
|
|
12
|
+
const chunks = [];
|
|
13
|
+
let size = 0;
|
|
14
|
+
for await (const chunk of req) {
|
|
15
|
+
size += chunk.length;
|
|
16
|
+
if (size > maxBytes)
|
|
17
|
+
throw new Error("request body too large");
|
|
18
|
+
chunks.push(chunk);
|
|
19
|
+
}
|
|
20
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.js","sourceRoot":"","sources":["../../src/server/request.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAG1D,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IACvC,OAAO,CAAC,KAAK,WAAW,IAAI,CAAC,KAAK,KAAK,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;AAE7E,mHAAmH;AACnH,MAAM,UAAU,UAAU,CAAC,SAAiB,EAAE,YAAoB;IAChE,OAAO,eAAe,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,YAAY,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAoB,EAAE,QAAgB;IACnE,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QAC9B,IAAI,IAAK,KAAgB,CAAC,MAAM,CAAC;QACjC,IAAI,IAAI,GAAG,QAAQ;YAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;QAC/D,MAAM,CAAC,IAAI,CAAC,KAAe,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC"}
|
package/package.json
CHANGED