repoburg 1.3.254 → 1.3.256
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/agent-pane/frontend/dist/assets/{index-hYkepxdY.js → index-BzDC6DcX.js} +86 -86
- package/agent-pane/frontend/dist/index.html +1 -1
- package/daemon/dist/index.js +61 -3
- package/daemon/dist/staticServing.js +2 -2
- package/daemon/dist/tabtape/api/follow.js +174 -0
- package/daemon/dist/tabtape/api/http.js +221 -0
- package/daemon/dist/tabtape/api/record.js +149 -0
- package/daemon/dist/tabtape/api/router.js +519 -0
- package/daemon/dist/tabtape/api/router.spec.js +604 -0
- package/daemon/dist/tabtape/api/static.js +70 -0
- package/daemon/dist/tabtape/api/ws.js +166 -0
- package/daemon/dist/tabtape/buffer/buffer.js +347 -0
- package/daemon/dist/tabtape/buffer/buffer.spec.js +137 -0
- package/daemon/dist/tabtape/config.js +108 -0
- package/daemon/dist/tabtape/e2e-server.js +96 -0
- package/daemon/dist/tabtape/errors.js +77 -0
- package/daemon/dist/tabtape/export/export.js +331 -0
- package/daemon/dist/tabtape/export/export.spec.js +262 -0
- package/daemon/dist/tabtape/har/gostd.js +395 -0
- package/daemon/dist/tabtape/har/gostd.spec.js +139 -0
- package/daemon/dist/tabtape/har/har.js +582 -0
- package/daemon/dist/tabtape/har/har.spec.js +325 -0
- package/daemon/dist/tabtape/index.js +134 -0
- package/daemon/dist/tabtape/index.spec.js +104 -0
- package/daemon/dist/tabtape/lock.js +67 -0
- package/daemon/dist/tabtape/log.js +32 -0
- package/daemon/dist/tabtape/mcp/definitions.js +362 -0
- package/daemon/dist/tabtape/mcp/gofmt.js +227 -0
- package/daemon/dist/tabtape/mcp/params.js +345 -0
- package/daemon/dist/tabtape/mcp/params.spec.js +172 -0
- package/daemon/dist/tabtape/mcp/recordLink.js +122 -0
- package/daemon/dist/tabtape/mcp/server.js +411 -0
- package/daemon/dist/tabtape/mcp/server.spec.js +722 -0
- package/daemon/dist/tabtape/mcp/tools.js +1085 -0
- package/daemon/dist/tabtape/query/format.js +195 -0
- package/daemon/dist/tabtape/query/network.js +133 -0
- package/daemon/dist/tabtape/query/perf.js +93 -0
- package/daemon/dist/tabtape/query/query.spec.js +233 -0
- package/daemon/dist/tabtape/query/reader.js +602 -0
- package/daemon/dist/tabtape/query/types.js +274 -0
- package/daemon/dist/tabtape/redact.js +37 -0
- package/daemon/dist/tabtape/repro/lang.js +95 -0
- package/daemon/dist/tabtape/repro/repro.js +422 -0
- package/daemon/dist/tabtape/repro/repro.spec.js +210 -0
- package/daemon/dist/tabtape/sink.js +24 -0
- package/daemon/dist/tabtape/store/record.js +99 -0
- package/daemon/dist/tabtape/store/segment.js +267 -0
- package/daemon/dist/tabtape/store/store.js +846 -0
- package/daemon/dist/tabtape/store/store.spec.js +485 -0
- package/daemon/dist/tabtape/testing/querytest.js +119 -0
- package/daemon/dist/tabtape/testing/tmp.js +33 -0
- package/package.json +3 -2
- package/tabtape/extension/dist/background.js +8 -0
- package/tabtape/extension/dist/background.js.map +7 -0
- package/tabtape/extension/dist/bridge.js +3 -0
- package/tabtape/extension/dist/bridge.js.map +7 -0
- package/tabtape/extension/dist/icons/128.png +0 -0
- package/tabtape/extension/dist/icons/16.png +0 -0
- package/tabtape/extension/dist/icons/32.png +0 -0
- package/tabtape/extension/dist/icons/48.png +0 -0
- package/tabtape/extension/dist/manifest.json +28 -0
- package/tabtape/extension/dist/popup.css +62 -0
- package/tabtape/extension/dist/popup.html +34 -0
- package/tabtape/extension/dist/popup.js +4 -0
- package/tabtape/extension/dist/popup.js.map +7 -0
- package/tabtape/extension/dist/recorder.js +133 -0
- package/tabtape/extension/dist/recorder.js.map +1 -0
- package/tabtape/extension/dist/state.js +2 -0
- package/tabtape/extension/dist/state.js.map +1 -0
- package/tabtape/playwright/mcp.mjs +58 -0
- package/tabtape/playwright/tabtape.d.mts +37 -0
- package/tabtape/playwright/tabtape.mjs +146 -0
- package/tabtape/ui/dist/app/assets/index-Canv3Ljk.css +1 -0
- package/tabtape/ui/dist/app/assets/index-iWNSSrAE.js +145 -0
- package/tabtape/ui/dist/app/index.html +14 -0
- package/tabtape/ui/dist/export/viewer.css +2 -0
- package/tabtape/ui/dist/export/viewer.js +143 -0
- package/visual-editor-proxy/dist/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TabtapeMcp = exports.INSTRUCTIONS = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Exposes recorded sessions to Claude Code (and other MCP clients) over MCP
|
|
9
|
+
* Streamable HTTP at /tabtape/mcp. Every tool is read-only.
|
|
10
|
+
*
|
|
11
|
+
* The server is stateless: each HTTP request gets a fresh SDK Server and
|
|
12
|
+
* transport, answers with plain JSON, and lists sessions as they are at that
|
|
13
|
+
* moment. Before a message reaches the SDK, requests go through the checks
|
|
14
|
+
* of the Go server and of the Go SDK's stateless handler (go-sdk v1.8.0), so
|
|
15
|
+
* HTTP-level answers match the Go server's.
|
|
16
|
+
*/
|
|
17
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
18
|
+
const net_1 = __importDefault(require("net"));
|
|
19
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
20
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
|
21
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
22
|
+
const errors_1 = require("../errors");
|
|
23
|
+
const log_1 = require("../log");
|
|
24
|
+
const definitions_1 = require("./definitions");
|
|
25
|
+
const gofmt_1 = require("./gofmt");
|
|
26
|
+
const tools_1 = require("./tools");
|
|
27
|
+
exports.INSTRUCTIONS = `tabtape records browser sessions locally: DOM replay, clicks, page views, form changes, network requests, console output, the app's own events (tabtape.track) and user id (tabtape.identify), Web Vitals and long freezes, and Redux / Zustand store actions.
|
|
28
|
+
Use these tools to answer questions about what happened in a recording. Times are offsets from the start of the replay (mm:ss.mmm).
|
|
29
|
+
Every session and line carries a replay link; include the relevant links in answers so the user can watch the moment.
|
|
30
|
+
Start with list_sessions or get_session_summary (session_id "latest" is the most recent recording), then drill down with get_network, get_request, get_console and get_context_at.
|
|
31
|
+
Request and response bodies are only returned by get_request or with include_bodies=true. Sensitive headers are shown as [redacted].
|
|
32
|
+
Recordings made in deep mode also have the real request headers and cookies, the timing split, why a request failed (CORS, blocked cookies, mixed content) and WebSocket connections: get_network with kind=websocket lists them, get_request on a connection pages its messages.
|
|
33
|
+
get_context_at ends with the last store actions before the moment and the state paths they changed.
|
|
34
|
+
To record a browser you drive yourself (Playwright MCP, a Playwright script), get_record_link returns links that start and stop a recording in it; open the start link before the check and the stop link after it.`;
|
|
35
|
+
const RESOURCE_PREFIX = 'tabtape://session/';
|
|
36
|
+
const SESSION_TEMPLATE = {
|
|
37
|
+
name: 'session',
|
|
38
|
+
title: 'tabtape recording',
|
|
39
|
+
description: 'Summary of a recorded session: pages, clicks, changes, errors and failed requests with replay links.',
|
|
40
|
+
mimeType: 'text/markdown',
|
|
41
|
+
uriTemplate: 'tabtape://session/{id}',
|
|
42
|
+
};
|
|
43
|
+
const DEBUG_PROMPT = {
|
|
44
|
+
name: 'debug_latest',
|
|
45
|
+
title: 'Debug the latest tabtape recording',
|
|
46
|
+
description: 'Find the first error in the most recent recording, what the user did before it, and the likely root cause.',
|
|
47
|
+
};
|
|
48
|
+
const DEBUG_TEXT = `Debug my latest tabtape recording.
|
|
49
|
+
|
|
50
|
+
1. Call get_session_summary with session_id "latest" to see what happened.
|
|
51
|
+
2. Look at failed requests (get_network with status "failed" and include_bodies true) and console errors (get_console with level "error").
|
|
52
|
+
3. For the first error, call get_context_at with its id to see what the user did right before it.
|
|
53
|
+
4. Explain the most likely root cause in a few sentences, quote the evidence (request, response body, error message) and include the replay links of the key moments.
|
|
54
|
+
If the recording has no errors, say so and describe what the user did.`;
|
|
55
|
+
/** Page size of list results, as in the Go SDK. */
|
|
56
|
+
const PAGE_SIZE = 1000;
|
|
57
|
+
/** The Go SDK's limit on a request body. */
|
|
58
|
+
const MAX_BODY_BYTES = 4 << 20;
|
|
59
|
+
/** JSON-RPC invalid params; the Go SDK also uses it for unknown resources (SEP-2164). */
|
|
60
|
+
const INVALID_PARAMS = -32602;
|
|
61
|
+
/** An error the SDK sends as is: code, message and data. */
|
|
62
|
+
class RpcError extends Error {
|
|
63
|
+
constructor(code, message, data) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.code = code;
|
|
66
|
+
this.data = data;
|
|
67
|
+
this.name = 'RpcError';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const resourceNotFound = (uri) => new RpcError(INVALID_PARAMS, 'Resource not found', { uri });
|
|
71
|
+
class TabtapeMcp {
|
|
72
|
+
constructor(st, rd, opts) {
|
|
73
|
+
this.st = st;
|
|
74
|
+
this.opts = opts;
|
|
75
|
+
this.log = opts.logger ?? (0, log_1.consoleLogger)();
|
|
76
|
+
this.tools = new tools_1.Tools(st, rd, {
|
|
77
|
+
baseURL: opts.baseURL,
|
|
78
|
+
redact: opts.redact,
|
|
79
|
+
showSecrets: opts.showSecrets,
|
|
80
|
+
recordToken: opts.recordToken,
|
|
81
|
+
timeZone: opts.timeZone,
|
|
82
|
+
now: opts.now ?? Date.now,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Serves one MCP Streamable HTTP request (POST/GET/DELETE). The request
|
|
87
|
+
* body has NOT been read or parsed. Browser requests (they carry Origin)
|
|
88
|
+
* are rejected, and the optional token is checked, before anything else.
|
|
89
|
+
* Resolves once the response is sent.
|
|
90
|
+
*/
|
|
91
|
+
async handle(req, res) {
|
|
92
|
+
try {
|
|
93
|
+
if (header(req, 'origin') !== '') {
|
|
94
|
+
textError(res, 403, 'browser requests are not allowed');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (this.opts.token !== '' && !this.authorized(req)) {
|
|
98
|
+
res.setHeader('WWW-Authenticate', 'Bearer realm="tabtape"');
|
|
99
|
+
textError(res, 401, 'missing or wrong bearer token');
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const body = await this.admit(req, res);
|
|
103
|
+
if (body === undefined)
|
|
104
|
+
return;
|
|
105
|
+
await this.serve(req, res, body);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
this.log.error('mcp request failed', { err });
|
|
109
|
+
if (!res.headersSent)
|
|
110
|
+
textError(res, 500, 'internal server error');
|
|
111
|
+
else
|
|
112
|
+
res.end();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
authorized(req) {
|
|
116
|
+
const auth = header(req, 'authorization');
|
|
117
|
+
if (!auth.startsWith('Bearer '))
|
|
118
|
+
return false;
|
|
119
|
+
const got = Buffer.from(auth.slice('Bearer '.length));
|
|
120
|
+
const want = Buffer.from(this.opts.token);
|
|
121
|
+
return got.length === want.length && crypto_1.default.timingSafeEqual(got, want);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The Go SDK's stateless handler up to the first message: it answers
|
|
125
|
+
* everything but a well-formed POST, and returns that POST's parsed body.
|
|
126
|
+
*/
|
|
127
|
+
async admit(req, res) {
|
|
128
|
+
// DNS rebinding protection for a server on a loopback address.
|
|
129
|
+
const host = header(req, 'host');
|
|
130
|
+
if (isLoopback(req.socket.localAddress ?? '') && !isLoopback(host)) {
|
|
131
|
+
textError(res, 403, `Forbidden: invalid Host header ${(0, gofmt_1.goQuote)(host)}`);
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const version = header(req, 'mcp-protocol-version');
|
|
135
|
+
if (version !== '' && !types_js_1.SUPPORTED_PROTOCOL_VERSIONS.includes(version) && version < '2026-07-28') {
|
|
136
|
+
textError(res, 400, `Bad Request: Unsupported protocol version (supported versions: ${types_js_1.SUPPORTED_PROTOCOL_VERSIONS.join(',')})`);
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
// Stateless: there is no session to stream to (GET) or to end (DELETE).
|
|
140
|
+
if (req.method !== 'POST') {
|
|
141
|
+
res.setHeader('Allow', 'POST');
|
|
142
|
+
textError(res, 405, 'Method Not Allowed');
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
if (baseMediaType(header(req, 'content-type')) !== 'application/json') {
|
|
146
|
+
textError(res, 415, "Content-Type must be 'application/json'");
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
if (!acceptsJSONAndStream(header(req, 'accept'))) {
|
|
150
|
+
textError(res, 400, "Accept must contain both 'application/json' and 'text/event-stream'");
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
let raw;
|
|
154
|
+
try {
|
|
155
|
+
raw = await readBody(req, MAX_BODY_BYTES);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
textError(res, 400, `failed to read body: ${(0, errors_1.errorMessage)(err)}`);
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
if (raw === null) {
|
|
162
|
+
res.setHeader('Connection', 'close');
|
|
163
|
+
textError(res, 413, `request body exceeds ${MAX_BODY_BYTES} bytes`);
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
if (req.headers['last-event-id'] !== undefined) {
|
|
167
|
+
textError(res, 400, "can't send Last-Event-ID for POST request");
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
if (raw.length === 0) {
|
|
171
|
+
textError(res, 400, 'POST requires a non-empty body');
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
let body;
|
|
175
|
+
try {
|
|
176
|
+
body = JSON.parse(raw.toString('utf8'));
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
textError(res, 400, `malformed payload: ${(0, errors_1.errorMessage)(err)}`);
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
if (Array.isArray(body)) {
|
|
183
|
+
if (body.length === 0) {
|
|
184
|
+
textError(res, 400, 'malformed payload: empty batch');
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
const v = version || '2025-03-26';
|
|
188
|
+
if (v >= '2025-06-18') {
|
|
189
|
+
textError(res, 400, `JSON-RPC batching is not supported in 2025-06-18 and later (request version: ${v})`);
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// The SDK transport checks these headers again, without wildcards or parameters.
|
|
194
|
+
req.headers.accept = 'application/json, text/event-stream';
|
|
195
|
+
req.headers['content-type'] = 'application/json';
|
|
196
|
+
return body;
|
|
197
|
+
}
|
|
198
|
+
/** Answers one POST with a fresh server and transport. */
|
|
199
|
+
async serve(req, res, body) {
|
|
200
|
+
const server = this.newServer();
|
|
201
|
+
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({ sessionIdGenerator: undefined, enableJsonResponse: true });
|
|
202
|
+
try {
|
|
203
|
+
await server.connect(transport);
|
|
204
|
+
await transport.handleRequest(req, res, body);
|
|
205
|
+
await responseDone(res); // handleRequest returns before the handlers answer
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
await server.close().catch(() => undefined);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
newServer() {
|
|
212
|
+
const server = new index_js_1.Server({ name: 'tabtape', title: 'tabtape session recorder', version: this.opts.version }, {
|
|
213
|
+
capabilities: { tools: { listChanged: true }, resources: { listChanged: true }, prompts: { listChanged: true } },
|
|
214
|
+
instructions: exports.INSTRUCTIONS,
|
|
215
|
+
});
|
|
216
|
+
// Invalid messages and clients that went away; the client gets its answer.
|
|
217
|
+
server.onerror = (err) => this.log.debug('mcp', { err });
|
|
218
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: definitions_1.TOOL_LIST }));
|
|
219
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
220
|
+
const { name, arguments: args } = request.params;
|
|
221
|
+
if (!(0, definitions_1.hasTool)(name))
|
|
222
|
+
throw new RpcError(INVALID_PARAMS, `unknown tool ${(0, gofmt_1.goQuote)(name)}`);
|
|
223
|
+
return (0, definitions_1.callTool)(this.tools, name, args);
|
|
224
|
+
});
|
|
225
|
+
server.setRequestHandler(types_js_1.ListResourcesRequestSchema, async (request) => this.listResources(request.params?.cursor));
|
|
226
|
+
server.setRequestHandler(types_js_1.ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [SESSION_TEMPLATE] }));
|
|
227
|
+
server.setRequestHandler(types_js_1.ReadResourceRequestSchema, async (request) => this.readResource(request.params.uri));
|
|
228
|
+
server.setRequestHandler(types_js_1.ListPromptsRequestSchema, async () => ({ prompts: [DEBUG_PROMPT] }));
|
|
229
|
+
server.setRequestHandler(types_js_1.GetPromptRequestSchema, async (request) => this.getPrompt(request.params.name));
|
|
230
|
+
return server;
|
|
231
|
+
}
|
|
232
|
+
/** Every session as a resource, so Claude Code can offer them for @-mentions; by URI, in pages. */
|
|
233
|
+
listResources(cursor) {
|
|
234
|
+
const all = this.st.list().map((sess) => ({
|
|
235
|
+
uri: RESOURCE_PREFIX + sess.id,
|
|
236
|
+
name: sess.id,
|
|
237
|
+
title: this.tools.resourceTitle(sess),
|
|
238
|
+
description: 'tabtape recording of ' + (sess.url || sess.id),
|
|
239
|
+
mimeType: 'text/markdown',
|
|
240
|
+
}));
|
|
241
|
+
all.sort((a, b) => (a.uri < b.uri ? -1 : a.uri > b.uri ? 1 : 0));
|
|
242
|
+
let start = 0;
|
|
243
|
+
if (cursor) {
|
|
244
|
+
const after = Buffer.from(cursor, 'base64url').toString('utf8');
|
|
245
|
+
if (!after.startsWith(RESOURCE_PREFIX))
|
|
246
|
+
throw new RpcError(INVALID_PARAMS, 'invalid params');
|
|
247
|
+
start = all.findIndex((r) => r.uri > after);
|
|
248
|
+
if (start < 0)
|
|
249
|
+
start = all.length;
|
|
250
|
+
}
|
|
251
|
+
const page = all.slice(start, start + PAGE_SIZE);
|
|
252
|
+
const result = { resources: page };
|
|
253
|
+
if (start + PAGE_SIZE < all.length)
|
|
254
|
+
result.nextCursor = Buffer.from(page[page.length - 1].uri, 'utf8').toString('base64url');
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
async readResource(uri) {
|
|
258
|
+
const id = uri.startsWith(RESOURCE_PREFIX) ? uri.slice(RESOURCE_PREFIX.length) : '';
|
|
259
|
+
if (id === '')
|
|
260
|
+
throw resourceNotFound(uri);
|
|
261
|
+
let sessionID;
|
|
262
|
+
try {
|
|
263
|
+
sessionID = this.tools.resolve(id).id;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
throw resourceNotFound(uri);
|
|
267
|
+
}
|
|
268
|
+
const { text } = await this.tools.summary(sessionID, '');
|
|
269
|
+
return { contents: [{ uri, mimeType: 'text/markdown', text }] };
|
|
270
|
+
}
|
|
271
|
+
getPrompt(name) {
|
|
272
|
+
if (name !== DEBUG_PROMPT.name)
|
|
273
|
+
throw new RpcError(INVALID_PARAMS, `unknown prompt ${(0, gofmt_1.goQuote)(name)}`);
|
|
274
|
+
return {
|
|
275
|
+
description: 'Debug the latest tabtape recording',
|
|
276
|
+
messages: [{ role: 'user', content: { type: 'text', text: DEBUG_TEXT } }],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
exports.TabtapeMcp = TabtapeMcp;
|
|
281
|
+
function header(req, name) {
|
|
282
|
+
const v = req.headers[name];
|
|
283
|
+
return (Array.isArray(v) ? v[0] : v) ?? '';
|
|
284
|
+
}
|
|
285
|
+
/** The Go SDK's reading of Accept: media types without parameters, any case, wildcards. */
|
|
286
|
+
function acceptsJSONAndStream(accept) {
|
|
287
|
+
let json = false;
|
|
288
|
+
let stream = false;
|
|
289
|
+
for (const part of accept.split(',')) {
|
|
290
|
+
switch ((0, gofmt_1.goTrimSpace)(part.split(';')[0]).toLowerCase()) {
|
|
291
|
+
case 'application/json':
|
|
292
|
+
case 'application/*':
|
|
293
|
+
json = true;
|
|
294
|
+
break;
|
|
295
|
+
case 'text/event-stream':
|
|
296
|
+
case 'text/*':
|
|
297
|
+
stream = true;
|
|
298
|
+
break;
|
|
299
|
+
case '*/*':
|
|
300
|
+
json = true;
|
|
301
|
+
stream = true;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return json && stream;
|
|
306
|
+
}
|
|
307
|
+
const TOKEN = "[!#$%&'*+.^_`|~0-9A-Za-z-]+";
|
|
308
|
+
const MEDIA_TYPE_RE = new RegExp(`^${TOKEN}(?:/${TOKEN})?$`);
|
|
309
|
+
const PARAM_RE = new RegExp(`^;\\s*${TOKEN}\\s*=\\s*(?:${TOKEN}|"(?:[^"\\\\]|\\\\.)*")\\s*`);
|
|
310
|
+
/** Go's mime.ParseMediaType as the Go SDK uses it: the lowercase media type, or "" when the value is malformed. */
|
|
311
|
+
function baseMediaType(value) {
|
|
312
|
+
const semi = value.indexOf(';');
|
|
313
|
+
const base = (0, gofmt_1.goTrimSpace)((semi < 0 ? value : value.slice(0, semi)).toLowerCase());
|
|
314
|
+
if (!MEDIA_TYPE_RE.test(base))
|
|
315
|
+
return '';
|
|
316
|
+
let rest = semi < 0 ? '' : value.slice(semi);
|
|
317
|
+
while (rest !== '') {
|
|
318
|
+
if (rest.trim() === ';')
|
|
319
|
+
break; // a trailing semicolon is fine
|
|
320
|
+
const m = PARAM_RE.exec(rest);
|
|
321
|
+
if (!m)
|
|
322
|
+
return '';
|
|
323
|
+
rest = rest.slice(m[0].length);
|
|
324
|
+
}
|
|
325
|
+
return base;
|
|
326
|
+
}
|
|
327
|
+
/** Go's net.SplitHostPort host part, or null when addr has no port. */
|
|
328
|
+
function splitHost(addr) {
|
|
329
|
+
if (addr.startsWith('[')) {
|
|
330
|
+
const end = addr.indexOf(']');
|
|
331
|
+
if (end < 0 || addr[end + 1] !== ':')
|
|
332
|
+
return null;
|
|
333
|
+
return addr.slice(1, end);
|
|
334
|
+
}
|
|
335
|
+
const colon = addr.lastIndexOf(':');
|
|
336
|
+
if (colon < 0 || addr.slice(0, colon).includes(':'))
|
|
337
|
+
return null;
|
|
338
|
+
return addr.slice(0, colon);
|
|
339
|
+
}
|
|
340
|
+
const IPV4_RE = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
|
|
341
|
+
/** The Go SDK's util.IsLoopback: "localhost" or a loopback IP, with or without a port. */
|
|
342
|
+
function isLoopback(addr) {
|
|
343
|
+
const host = splitHost(addr) ?? addr.replace(/^[[\]]+|[[\]]+$/g, '');
|
|
344
|
+
if (host === 'localhost')
|
|
345
|
+
return true;
|
|
346
|
+
if (IPV4_RE.test(host))
|
|
347
|
+
return host.startsWith('127.');
|
|
348
|
+
const ip = host.replace(/%.*$/, ''); // zone
|
|
349
|
+
if (!net_1.default.isIPv6(ip))
|
|
350
|
+
return false;
|
|
351
|
+
let normal;
|
|
352
|
+
try {
|
|
353
|
+
normal = new URL(`http://[${ip}]/`).hostname;
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
return normal === '[::1]' || /^\[::ffff:7f[0-9a-f]{2}:[0-9a-f]{1,4}\]$/.test(normal);
|
|
359
|
+
}
|
|
360
|
+
/** The body, or null when it is longer than limit. */
|
|
361
|
+
function readBody(req, limit) {
|
|
362
|
+
return new Promise((resolve, reject) => {
|
|
363
|
+
const chunks = [];
|
|
364
|
+
let size = 0;
|
|
365
|
+
let settled = false;
|
|
366
|
+
req.on('data', (chunk) => {
|
|
367
|
+
if (settled)
|
|
368
|
+
return;
|
|
369
|
+
size += chunk.length;
|
|
370
|
+
if (size > limit) {
|
|
371
|
+
settled = true;
|
|
372
|
+
resolve(null); // the rest is drained and dropped
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
chunks.push(chunk);
|
|
376
|
+
});
|
|
377
|
+
req.on('end', () => {
|
|
378
|
+
if (settled)
|
|
379
|
+
return;
|
|
380
|
+
settled = true;
|
|
381
|
+
resolve(Buffer.concat(chunks, size));
|
|
382
|
+
});
|
|
383
|
+
req.on('error', (err) => {
|
|
384
|
+
if (settled)
|
|
385
|
+
return;
|
|
386
|
+
settled = true;
|
|
387
|
+
reject(err);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
/** Go's http.Error. */
|
|
392
|
+
function textError(res, status, msg) {
|
|
393
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
394
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
395
|
+
res.writeHead(status);
|
|
396
|
+
res.end(msg + '\n');
|
|
397
|
+
}
|
|
398
|
+
/** Resolves when the response is sent or the client went away. */
|
|
399
|
+
function responseDone(res) {
|
|
400
|
+
if (res.writableFinished || res.destroyed)
|
|
401
|
+
return Promise.resolve();
|
|
402
|
+
return new Promise((resolve) => {
|
|
403
|
+
const done = () => {
|
|
404
|
+
res.off('finish', done);
|
|
405
|
+
res.off('close', done);
|
|
406
|
+
resolve();
|
|
407
|
+
};
|
|
408
|
+
res.on('finish', done);
|
|
409
|
+
res.on('close', done);
|
|
410
|
+
});
|
|
411
|
+
}
|