repoburg 1.3.255 → 1.3.257

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/daemon/dist/index.js +61 -3
  2. package/daemon/dist/staticServing.js +2 -2
  3. package/daemon/dist/tabtape/api/follow.js +174 -0
  4. package/daemon/dist/tabtape/api/http.js +221 -0
  5. package/daemon/dist/tabtape/api/record.js +149 -0
  6. package/daemon/dist/tabtape/api/router.js +519 -0
  7. package/daemon/dist/tabtape/api/router.spec.js +604 -0
  8. package/daemon/dist/tabtape/api/static.js +70 -0
  9. package/daemon/dist/tabtape/api/ws.js +166 -0
  10. package/daemon/dist/tabtape/buffer/buffer.js +347 -0
  11. package/daemon/dist/tabtape/buffer/buffer.spec.js +137 -0
  12. package/daemon/dist/tabtape/config.js +108 -0
  13. package/daemon/dist/tabtape/e2e-server.js +96 -0
  14. package/daemon/dist/tabtape/errors.js +77 -0
  15. package/daemon/dist/tabtape/export/export.js +331 -0
  16. package/daemon/dist/tabtape/export/export.spec.js +262 -0
  17. package/daemon/dist/tabtape/har/gostd.js +395 -0
  18. package/daemon/dist/tabtape/har/gostd.spec.js +139 -0
  19. package/daemon/dist/tabtape/har/har.js +582 -0
  20. package/daemon/dist/tabtape/har/har.spec.js +325 -0
  21. package/daemon/dist/tabtape/index.js +134 -0
  22. package/daemon/dist/tabtape/index.spec.js +104 -0
  23. package/daemon/dist/tabtape/lock.js +67 -0
  24. package/daemon/dist/tabtape/log.js +32 -0
  25. package/daemon/dist/tabtape/mcp/definitions.js +362 -0
  26. package/daemon/dist/tabtape/mcp/gofmt.js +227 -0
  27. package/daemon/dist/tabtape/mcp/params.js +345 -0
  28. package/daemon/dist/tabtape/mcp/params.spec.js +172 -0
  29. package/daemon/dist/tabtape/mcp/recordLink.js +122 -0
  30. package/daemon/dist/tabtape/mcp/server.js +411 -0
  31. package/daemon/dist/tabtape/mcp/server.spec.js +722 -0
  32. package/daemon/dist/tabtape/mcp/tools.js +1085 -0
  33. package/daemon/dist/tabtape/query/format.js +195 -0
  34. package/daemon/dist/tabtape/query/network.js +133 -0
  35. package/daemon/dist/tabtape/query/perf.js +93 -0
  36. package/daemon/dist/tabtape/query/query.spec.js +233 -0
  37. package/daemon/dist/tabtape/query/reader.js +602 -0
  38. package/daemon/dist/tabtape/query/types.js +274 -0
  39. package/daemon/dist/tabtape/redact.js +37 -0
  40. package/daemon/dist/tabtape/repro/lang.js +95 -0
  41. package/daemon/dist/tabtape/repro/repro.js +422 -0
  42. package/daemon/dist/tabtape/repro/repro.spec.js +210 -0
  43. package/daemon/dist/tabtape/sink.js +24 -0
  44. package/daemon/dist/tabtape/store/record.js +99 -0
  45. package/daemon/dist/tabtape/store/segment.js +267 -0
  46. package/daemon/dist/tabtape/store/store.js +846 -0
  47. package/daemon/dist/tabtape/store/store.spec.js +485 -0
  48. package/daemon/dist/tabtape/testing/querytest.js +119 -0
  49. package/daemon/dist/tabtape/testing/tmp.js +33 -0
  50. package/package.json +3 -2
  51. package/tabtape/extension/dist/background.js +8 -0
  52. package/tabtape/extension/dist/background.js.map +7 -0
  53. package/tabtape/extension/dist/bridge.js +3 -0
  54. package/tabtape/extension/dist/bridge.js.map +7 -0
  55. package/tabtape/extension/dist/icons/128.png +0 -0
  56. package/tabtape/extension/dist/icons/16.png +0 -0
  57. package/tabtape/extension/dist/icons/32.png +0 -0
  58. package/tabtape/extension/dist/icons/48.png +0 -0
  59. package/tabtape/extension/dist/manifest.json +28 -0
  60. package/tabtape/extension/dist/popup.css +62 -0
  61. package/tabtape/extension/dist/popup.html +34 -0
  62. package/tabtape/extension/dist/popup.js +4 -0
  63. package/tabtape/extension/dist/popup.js.map +7 -0
  64. package/tabtape/extension/dist/recorder.js +133 -0
  65. package/tabtape/extension/dist/recorder.js.map +1 -0
  66. package/tabtape/extension/dist/state.js +2 -0
  67. package/tabtape/extension/dist/state.js.map +1 -0
  68. package/tabtape/playwright/mcp.mjs +58 -0
  69. package/tabtape/playwright/tabtape.d.mts +37 -0
  70. package/tabtape/playwright/tabtape.mjs +146 -0
  71. package/tabtape/ui/dist/app/assets/index-Canv3Ljk.css +1 -0
  72. package/tabtape/ui/dist/app/assets/index-iWNSSrAE.js +145 -0
  73. package/tabtape/ui/dist/app/index.html +14 -0
  74. package/tabtape/ui/dist/export/viewer.css +2 -0
  75. package/tabtape/ui/dist/export/viewer.js +143 -0
@@ -0,0 +1,262 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const crypto_1 = __importDefault(require("crypto"));
40
+ const zlib_1 = __importDefault(require("zlib"));
41
+ const errors_1 = require("../errors");
42
+ const reader_1 = require("../query/reader");
43
+ const redact_1 = require("../redact");
44
+ const sink_1 = require("../sink");
45
+ const record_1 = require("../store/record");
46
+ const qt = __importStar(require("../testing/querytest"));
47
+ const tmp_1 = require("../testing/tmp");
48
+ const export_1 = require("./export");
49
+ const t0 = qt.T0;
50
+ const assets = { js: "document.title='</script><script>alert(1)';", css: 'body{color:red}</style><b>' };
51
+ afterEach(async () => {
52
+ await qt.closeTestStores();
53
+ (0, tmp_1.cleanupTempDirs)();
54
+ });
55
+ async function view(title = 'App </title>') {
56
+ const st = qt.testStore();
57
+ qt.start(st, 's1', t0, 'http://app.test/', title);
58
+ qt.append(st, 's1', qt.rrMeta(t0, 'http://app.test/', 800, 600), qt.rrFull(t0), qt.net(t0 + 1000, {
59
+ kind: 'fetch',
60
+ method: 'POST',
61
+ url: 'http://app.test/login',
62
+ reqHeaders: { Authorization: 'Bearer test-token', Accept: '*/*' },
63
+ reqBody: '{"password":"********"}',
64
+ resBody: '{"ok":true}',
65
+ status: 200,
66
+ }), qt.consoleRec(t0 + 2000, { level: 'log', source: 'console', text: 'hello {…}', args: ['hello', { secret: 1 }] }), qt.ws(t0 + 2500, 'ws-1', 'recv', '{"chat":"hi"}'), qt.rrMeta(t0 + 30000, 'http://app.test/', 800, 600), qt.rrFull(t0 + 30000), qt.consoleRec(t0 + 31000, { level: 'log', source: 'console', text: 'after checkout' }), qt.consoleRec(t0 + 60000, { level: 'log', source: 'console', text: 'late' }), qt.rrInc(t0 + 60000));
67
+ return new reader_1.Reader(st).view('s1');
68
+ }
69
+ const DATA_RE = /<script id="tabtape-data" type="application\/octet-stream">([\s\S]*?)<\/script>/;
70
+ const META_RE = /<script id="tabtape-meta" type="application\/json">([\s\S]*?)<\/script>/;
71
+ /** The embedded recording, decoded like the viewer does: base64, then gunzip. */
72
+ function recording(page) {
73
+ const m = DATA_RE.exec(page);
74
+ if (!m)
75
+ throw new Error(`no data script in:\n${page}`);
76
+ return zlib_1.default.gunzipSync(Buffer.from(m[1], 'base64')).toString('utf8');
77
+ }
78
+ function meta(page) {
79
+ const m = META_RE.exec(page);
80
+ if (!m)
81
+ throw new Error('no meta script');
82
+ return JSON.parse(m[1]);
83
+ }
84
+ /** Writes the file with the options the Go tests used; checks the reported size. */
85
+ async function render(o = {}, v) {
86
+ const out = (0, sink_1.collect)();
87
+ const n = await (0, export_1.writeExport)(out.sink, v ?? (await view()), assets, {
88
+ ...o,
89
+ redact: new redact_1.RedactHeaders(),
90
+ version: '0.2.0',
91
+ now: () => t0 + 100000,
92
+ });
93
+ const bytes = out.bytes();
94
+ expect(n).toBe(bytes.length);
95
+ const page = bytes.toString('utf8');
96
+ const lines = recording(page)
97
+ .trim()
98
+ .split('\n')
99
+ .map((l) => JSON.parse(l));
100
+ return { page, lines, meta: meta(page) };
101
+ }
102
+ function find(lines, type) {
103
+ return lines.filter((l) => l.type === type).map((l) => l.data);
104
+ }
105
+ /** "\uXXXX" as JSON writes it. */
106
+ const u = (hex) => String.fromCharCode(92) + 'u' + hex;
107
+ async function storeText(st, id) {
108
+ const out = (0, sink_1.collect)();
109
+ await st.writeSession(id, out.sink);
110
+ return out.bytes().toString('utf8');
111
+ }
112
+ describe('writeExport', () => {
113
+ it('hides secrets and escapes the frame by default', async () => {
114
+ const { page, lines, meta: m } = await render();
115
+ expect(lines.length).toBe(10);
116
+ const req = find(lines, 'network')[0];
117
+ expect(req.reqHeaders.Authorization).toBe(redact_1.REDACTED);
118
+ expect(req.reqBody).toBeDefined();
119
+ expect(page).not.toContain('</script><script>alert');
120
+ expect(page).not.toContain('</style><b>');
121
+ expect(page).toContain('<title>App &lt;/title&gt; · tabtape recording</title>');
122
+ expect([m.id, m.version, m.redactedHeaders.length]).toEqual(['s1', '0.2.0', 5]);
123
+ });
124
+ it('strips bodies, console arguments and WebSocket data on request', async () => {
125
+ const { lines, meta: m } = await render({ secrets: true, stripBodies: true, stripConsoleArgs: true });
126
+ const req = find(lines, 'network')[0];
127
+ expect(req.reqHeaders.Authorization).toBe('Bearer test-token'); // secrets keep the header
128
+ expect('reqBody' in req).toBe(false);
129
+ expect(req.reqBodyNote).toBe('removed from this export');
130
+ expect(req.resBody).toBeUndefined();
131
+ for (const c of find(lines, 'console'))
132
+ expect('args' in c).toBe(false);
133
+ const ws = find(lines, 'ws')[0];
134
+ expect([ws.data, ws.dataRemoved, ws.conn]).toEqual([undefined, true, 'ws-1']);
135
+ expect([m.bodiesRemoved, m.consoleArgsRemoved, 'redactedHeaders' in m]).toEqual([true, true, false]);
136
+ });
137
+ it('keeps WebSocket connections that are open during the range', async () => {
138
+ const st = qt.testStore();
139
+ qt.append(st, 's1', qt.rrMeta(t0, 'http://app.test/', 800, 600), qt.rrFull(t0), qt.net(t0 + 1000, { id: 'ws-1', kind: 'websocket', method: 'GET', url: 'ws://app.test/live', status: 101, closedAt: t0 + 50000 }), qt.net(t0 + 2000, { id: 'ws-2', kind: 'websocket', method: 'GET', url: 'ws://app.test/short', status: 101, closedAt: t0 + 3000 }), qt.rrMeta(t0 + 30000, 'http://app.test/', 800, 600), qt.rrFull(t0 + 30000), qt.ws(t0 + 41000, 'ws-1', 'recv', '{"price":1}'), qt.rrInc(t0 + 60000));
140
+ const out = (0, sink_1.collect)();
141
+ await (0, export_1.writeExport)(out.sink, await new reader_1.Reader(st).view('s1'), assets, { from: 40000, to: 45000, redact: new redact_1.RedactHeaders(), version: '' });
142
+ const body = recording(out.bytes().toString('utf8'));
143
+ // The connection open during the range stays (ws-1); the closed one goes (ws-2).
144
+ expect(body).toContain('"id":"ws-1"');
145
+ expect(body).not.toContain('"id":"ws-2"');
146
+ expect(body).toContain('price');
147
+ });
148
+ it('starts a range at the snapshot before it', async () => {
149
+ const { lines, meta: m } = await render({ from: 40000, to: 50000 });
150
+ expect(find(lines, 'console').map((c) => c.text)).toEqual(['after checkout']);
151
+ const rr = find(lines, 'rrweb');
152
+ expect([rr.length, rr[0].type]).toEqual([2, reader_1.RR_META]);
153
+ expect(m.range).toEqual({ from: 40000, to: 50000 });
154
+ });
155
+ it('needs the viewer', async () => {
156
+ const opts = { redact: new redact_1.RedactHeaders(), version: '' };
157
+ await expect((0, export_1.writeExport)((0, sink_1.collect)().sink, await view(), { js: '', css: '' }, opts)).rejects.toBeInstanceOf(errors_1.NoViewerError);
158
+ await expect((0, export_1.writeExport)((0, sink_1.collect)().sink, await view(), { js: Buffer.alloc(0), css: 'x' }, opts)).rejects.toBeInstanceOf(errors_1.NoViewerError);
159
+ });
160
+ it('writes meta fields and escapes them like Go', async () => {
161
+ const ls = String.fromCharCode(0x2028);
162
+ const title = `</script><b> & "q" ${ls}`;
163
+ const out = (0, sink_1.collect)();
164
+ const v = await view(title);
165
+ await (0, export_1.writeExport)(out.sink, v, assets, { redact: new redact_1.RedactHeaders(''), version: '1.0"<x>', now: () => 42 });
166
+ const page = out.bytes().toString('utf8');
167
+ expect(page).toContain(`"title":"${u('003c')}/script${u('003e')}${u('003c')}b${u('003e')} ${u('0026')} \\"q\\" ${u('2028')}"`);
168
+ expect(page).toContain('<meta name="generator" content="tabtape 1.0&#34;&lt;x&gt;">');
169
+ expect(page).toContain('<title>&lt;/script&gt;&lt;b&gt; &amp; &#34;q&#34; ');
170
+ // Field order and omitted fields as with Go's json.Marshal: no redacted
171
+ // headers to list, no range, nothing removed.
172
+ expect(Object.keys(meta(page))).toEqual(['id', 'title', 'url', 'startedAt', 'userAgent', 'exportedAt', 'version']);
173
+ expect(meta(page)).toMatchObject({ id: 's1', title, url: 'http://app.test/', startedAt: t0, userAgent: qt.UA, exportedAt: 42 });
174
+ });
175
+ it('escapes closing tags in the assets in any case and keeps their bytes', async () => {
176
+ const invalidUTF8 = Buffer.from([0xff, 0xfe]);
177
+ const js = Buffer.concat([invalidUTF8, Buffer.from('a</SCRIPT b</Script c</scriptx d</ſcript e</scrip')]);
178
+ const out = (0, sink_1.collect)();
179
+ await (0, export_1.writeExport)(out.sink, await view(), { js, css: 'x</STYLE y</style' }, { redact: new redact_1.RedactHeaders(), version: '' });
180
+ const page = out.bytes();
181
+ const want = Buffer.concat([invalidUTF8, Buffer.from('a<\\/SCRIPT b<\\/Script c<\\/scriptx d<\\/ſcript e</scrip')]);
182
+ expect(page.includes(Buffer.concat([Buffer.from('<script>'), want, Buffer.from('</script>\n</body>\n</html>\n')]))).toBe(true);
183
+ expect(page.toString('utf8')).toContain('<style>x<\\/STYLE y<\\/style</style>');
184
+ });
185
+ it('re-encodes edited lines with the envelope in order', async () => {
186
+ const st = qt.testStore();
187
+ const rec = {
188
+ ts: t0,
189
+ type: 'network',
190
+ data: { kind: 'fetch', method: 'GET', url: 'http://app.test/a', reqHeaders: { Authorization: 'x', 'X-Null': null }, resHeaders: 'odd' },
191
+ extra: 1,
192
+ };
193
+ st.append('s1', [(0, record_1.parseRecord)(JSON.stringify(rec))]);
194
+ const out = (0, sink_1.collect)();
195
+ await (0, export_1.writeExport)(out.sink, await new reader_1.Reader(st).view('s1'), assets, { redact: new redact_1.RedactHeaders(), version: '' });
196
+ // Like Go: unknown envelope fields are dropped, a null header becomes "",
197
+ // and headers that are not a map of strings are left alone.
198
+ expect(recording(out.bytes().toString('utf8'))).toBe(`{"sid":"s1","ts":${t0},"type":"network","data":{"kind":"fetch","method":"GET","url":"http://app.test/a",` +
199
+ `"reqHeaders":{"Authorization":"${redact_1.REDACTED}","X-Null":""},"resHeaders":"odd"}}\n`);
200
+ });
201
+ it('streams large sessions in chunks, waiting for the sink', async () => {
202
+ const st = bigSession();
203
+ const parts = [];
204
+ let busy = false;
205
+ let overlapped = false;
206
+ const sink = (chunk) => {
207
+ if (busy)
208
+ overlapped = true;
209
+ parts.push(Buffer.from(chunk));
210
+ busy = true;
211
+ return new Promise((resolve) => setImmediate(() => {
212
+ busy = false;
213
+ resolve();
214
+ }));
215
+ };
216
+ const n = await (0, export_1.writeExport)(sink, await new reader_1.Reader(st).view('big'), assets, { secrets: true, redact: new redact_1.RedactHeaders(), version: '' });
217
+ const page = Buffer.concat(parts);
218
+ expect(overlapped).toBe(false);
219
+ expect(n).toBe(page.length);
220
+ expect(parts.length).toBeGreaterThan(10);
221
+ // Lines that need no change are kept byte for byte, in file order.
222
+ expect(recording(page.toString('utf8'))).toBe(await storeText(st, 'big'));
223
+ });
224
+ it('reports the same size to a counting sink', async () => {
225
+ const v = await view();
226
+ const opts = { redact: new redact_1.RedactHeaders(), version: '0.2.0', now: () => t0 };
227
+ const out = (0, sink_1.collect)();
228
+ const c = (0, sink_1.counter)();
229
+ const n = await (0, export_1.writeExport)(out.sink, v, assets, opts);
230
+ expect(await (0, export_1.writeExport)(c.sink, v, assets, opts)).toBe(n);
231
+ expect([c.count(), out.bytes().length]).toEqual([n, n]);
232
+ });
233
+ it('passes sink errors through', async () => {
234
+ const opts = { redact: new redact_1.RedactHeaders(), version: '' };
235
+ const boom = new Error('client went away');
236
+ const failOn = (call) => {
237
+ let calls = 0;
238
+ return () => (++calls === call ? Promise.reject(boom) : undefined);
239
+ };
240
+ const throwing = () => {
241
+ throw boom;
242
+ };
243
+ const big = await new reader_1.Reader(bigSession()).view('big');
244
+ await expect((0, export_1.writeExport)(failOn(6), big, assets, opts)).rejects.toBe(boom); // while lines are compressed
245
+ await expect((0, export_1.writeExport)(failOn(4), await view(), assets, opts)).rejects.toBe(boom); // at the end of the data
246
+ await expect((0, export_1.writeExport)(throwing, await view(), assets, opts)).rejects.toBe(boom);
247
+ });
248
+ });
249
+ /** Some 1 MB of hardly compressible console lines, and one line bigger than a batch. */
250
+ function bigSession() {
251
+ const st = qt.testStore();
252
+ qt.start(st, 'big', t0, 'http://app.test/', 'Big');
253
+ qt.append(st, 'big', qt.rrMeta(t0, 'http://app.test/', 800, 600), qt.rrFull(t0));
254
+ for (let i = 0; i < 600; i += 100) {
255
+ const recs = [];
256
+ for (let j = 0; j < 100; j++)
257
+ recs.push(qt.consoleRec(t0 + i + j + 1, { level: 'log', text: crypto_1.default.randomBytes(600).toString('hex') }));
258
+ qt.append(st, 'big', ...recs);
259
+ }
260
+ qt.append(st, 'big', qt.consoleRec(t0 + 10000, { level: 'log', text: crypto_1.default.randomBytes(200000).toString('base64') }));
261
+ return st;
262
+ }
@@ -0,0 +1,395 @@
1
+ "use strict";
2
+ /**
3
+ * Equivalents of the Go standard library functions the Go server's HAR
4
+ * conversion relied on (Go 1.25: net/http cookie parsing, mime.ParseMediaType,
5
+ * net/url parsing and unescaping, path.Ext, strings.TrimSpace), so that HAR
6
+ * files keep coming out the same. Only the parts the conversion uses are
7
+ * implemented; validity rules follow Go's, including its quirks.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.trimSpace = trimSpace;
11
+ exports.compareStrings = compareStrings;
12
+ exports.parseCookie = parseCookie;
13
+ exports.parseSetCookie = parseSetCookie;
14
+ exports.mediaType = mediaType;
15
+ exports.queryUnescape = queryUnescape;
16
+ exports.parseURL = parseURL;
17
+ exports.pathExt = pathExt;
18
+ /** A 128-entry ASCII membership table. */
19
+ function asciiTable(chars) {
20
+ const t = new Uint8Array(128);
21
+ for (let i = 0; i < chars.length; i++)
22
+ t[chars.charCodeAt(i)] = 1;
23
+ return t;
24
+ }
25
+ const ALNUM = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
26
+ /** RFC 7230 token characters: Go's httpguts token table, and mime's in Go 1.25 (older versions also took "{" and "}"). */
27
+ const TOKEN = asciiTable(ALNUM + "!#$%&'*+-.^_`|~");
28
+ const isTokenChar = (c) => c < 128 && TOKEN[c] === 1;
29
+ function isToken(s) {
30
+ if (s === '')
31
+ return false;
32
+ for (let i = 0; i < s.length; i++)
33
+ if (!isTokenChar(s.charCodeAt(i)))
34
+ return false;
35
+ return true;
36
+ }
37
+ /** unicode.IsSpace (Unicode White_Space): unlike String.prototype.trim, U+0085 counts and U+FEFF does not. */
38
+ function isSpace(c) {
39
+ return ((c >= 0x09 && c <= 0x0d) ||
40
+ c === 0x20 ||
41
+ c === 0x85 ||
42
+ c === 0xa0 ||
43
+ c === 0x1680 ||
44
+ (c >= 0x2000 && c <= 0x200a) ||
45
+ c === 0x2028 ||
46
+ c === 0x2029 ||
47
+ c === 0x202f ||
48
+ c === 0x205f ||
49
+ c === 0x3000);
50
+ }
51
+ /** strings.TrimSpace. */
52
+ function trimSpace(s) {
53
+ let i = 0;
54
+ let j = s.length;
55
+ while (i < j && isSpace(s.charCodeAt(i)))
56
+ i++;
57
+ while (j > i && isSpace(s.charCodeAt(j - 1)))
58
+ j--;
59
+ return s.slice(i, j);
60
+ }
61
+ function trimLeftSpace(s) {
62
+ let i = 0;
63
+ while (i < s.length && isSpace(s.charCodeAt(i)))
64
+ i++;
65
+ return s.slice(i);
66
+ }
67
+ /** textproto.TrimString: trims ASCII space, tab, CR and LF only. */
68
+ function trimASCII(s) {
69
+ const ws = (c) => c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d;
70
+ let i = 0;
71
+ let j = s.length;
72
+ while (i < j && ws(s.charCodeAt(i)))
73
+ i++;
74
+ while (j > i && ws(s.charCodeAt(j - 1)))
75
+ j--;
76
+ return s.slice(i, j);
77
+ }
78
+ /** Go's string order: by UTF-8 bytes (UTF-16 order differs past U+FFFF). */
79
+ function compareStrings(a, b) {
80
+ if (a === b)
81
+ return 0;
82
+ return Buffer.compare(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));
83
+ }
84
+ // ---------- cookies (net/http) ----------
85
+ /** parseCookieValue: strips one pair of double quotes; null if a byte is not allowed in a cookie value. */
86
+ function cookieValue(raw) {
87
+ if (raw.length > 1 && raw[0] === '"' && raw[raw.length - 1] === '"')
88
+ raw = raw.slice(1, -1);
89
+ for (let i = 0; i < raw.length; i++) {
90
+ const c = raw.charCodeAt(i);
91
+ if (c < 0x20 || c >= 0x7f || c === 0x22 /* " */ || c === 0x3b /* ; */ || c === 0x5c /* \ */)
92
+ return null;
93
+ }
94
+ return raw;
95
+ }
96
+ /** http.ParseCookie: the cookies of a Cookie header, or null when any part is malformed. */
97
+ function parseCookie(line) {
98
+ const parts = trimASCII(line).split(';');
99
+ if (parts.length === 1 && parts[0] === '')
100
+ return null;
101
+ const out = [];
102
+ for (const part of parts) {
103
+ const s = trimASCII(part);
104
+ const eq = s.indexOf('=');
105
+ if (eq < 0)
106
+ return null;
107
+ const name = s.slice(0, eq); // not trimmed, unlike in ParseSetCookie
108
+ if (!isToken(name))
109
+ return null;
110
+ const value = cookieValue(s.slice(eq + 1));
111
+ if (value === null)
112
+ return null;
113
+ out.push({ name, value });
114
+ }
115
+ return out;
116
+ }
117
+ /** http.ParseSetCookie, name and value only (its attributes never make it fail); null on a syntax error. */
118
+ function parseSetCookie(line) {
119
+ const parts = trimASCII(line).split(';');
120
+ if (parts.length === 1 && parts[0] === '')
121
+ return null;
122
+ const first = trimASCII(parts[0]);
123
+ const eq = first.indexOf('=');
124
+ if (eq < 0)
125
+ return null;
126
+ const name = trimASCII(first.slice(0, eq));
127
+ if (!isToken(name))
128
+ return null;
129
+ const value = cookieValue(first.slice(eq + 1));
130
+ return value === null ? null : { name, value };
131
+ }
132
+ // ---------- media types (mime) ----------
133
+ const TSPECIALS = '()<>@,;:\\"/[]?=';
134
+ function consumeToken(v) {
135
+ let i = 0;
136
+ while (i < v.length && isTokenChar(v.charCodeAt(i)))
137
+ i++;
138
+ return [v.slice(0, i), v.slice(i)];
139
+ }
140
+ /** A token or a quoted string; ["", v] on failure. */
141
+ function consumeValue(v) {
142
+ if (v === '')
143
+ return ['', ''];
144
+ if (v[0] !== '"')
145
+ return consumeToken(v);
146
+ let out = '';
147
+ for (let i = 1; i < v.length; i++) {
148
+ const c = v[i];
149
+ if (c === '"')
150
+ return [out, v.slice(i + 1)];
151
+ // A backslash only escapes tspecials (MSIE sends file paths unescaped).
152
+ if (c === '\\' && i + 1 < v.length && TSPECIALS.indexOf(v[i + 1]) >= 0) {
153
+ out += v[i + 1];
154
+ i++;
155
+ continue;
156
+ }
157
+ if (c === '\r' || c === '\n')
158
+ return ['', v];
159
+ out += c;
160
+ }
161
+ return ['', v]; // no closing quote
162
+ }
163
+ function consumeMediaParam(v) {
164
+ let rest = trimLeftSpace(v);
165
+ if (rest[0] !== ';')
166
+ return null;
167
+ rest = trimLeftSpace(rest.slice(1));
168
+ let key;
169
+ [key, rest] = consumeToken(rest);
170
+ key = key.toLowerCase();
171
+ if (key === '')
172
+ return null;
173
+ rest = trimLeftSpace(rest);
174
+ if (rest[0] !== '=')
175
+ return null;
176
+ rest = trimLeftSpace(rest.slice(1));
177
+ const [value, after] = consumeValue(rest);
178
+ if (value === '' && after === rest)
179
+ return null;
180
+ return { key, value, rest: after };
181
+ }
182
+ /** checkMediaTypeDisposition: "type" or "type/subtype". */
183
+ function validMediaType(s) {
184
+ const [typ, rest] = consumeToken(s);
185
+ if (typ === '')
186
+ return false;
187
+ if (rest === '')
188
+ return true;
189
+ if (rest[0] !== '/')
190
+ return false;
191
+ const [subtype, after] = consumeToken(rest.slice(1));
192
+ return subtype !== '' && after === '';
193
+ }
194
+ /**
195
+ * The media type mime.ParseMediaType returns, lowercase and without
196
+ * parameters; null where it returns an error: a malformed type or parameter,
197
+ * or a parameter repeated with another value.
198
+ */
199
+ function mediaType(v) {
200
+ const semi = v.indexOf(';');
201
+ const base = semi < 0 ? v : v.slice(0, semi);
202
+ const mt = trimSpace(base.toLowerCase());
203
+ if (!validMediaType(mt))
204
+ return null;
205
+ const params = new Map();
206
+ // RFC 2231 parts ("name*", "name*0") are kept apart per base name.
207
+ const continuation = new Map();
208
+ let rest = v.slice(base.length);
209
+ while (rest.length > 0) {
210
+ rest = trimLeftSpace(rest);
211
+ if (rest.length === 0)
212
+ break;
213
+ const p = consumeMediaParam(rest);
214
+ if (!p) {
215
+ if (trimSpace(rest) === ';')
216
+ break; // trailing semicolons are not an error
217
+ return null;
218
+ }
219
+ let pmap = params;
220
+ const star = p.key.indexOf('*');
221
+ if (star >= 0) {
222
+ const baseName = p.key.slice(0, star);
223
+ let m = continuation.get(baseName);
224
+ if (!m)
225
+ continuation.set(baseName, (m = new Map()));
226
+ pmap = m;
227
+ }
228
+ const prev = pmap.get(p.key);
229
+ if (prev !== undefined && prev !== p.value)
230
+ return null; // duplicate parameter name
231
+ pmap.set(p.key, p.value);
232
+ rest = p.rest;
233
+ }
234
+ return mt;
235
+ }
236
+ // ---------- URLs (net/url, path) ----------
237
+ const isHex = (c) => (c >= 0x30 && c <= 0x39) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66);
238
+ const unhex = (c) => (c <= 0x39 ? c - 0x30 : (c | 0x20) - 0x61 + 10);
239
+ /** Whether s[i] starts a well-formed "%XX" escape. */
240
+ const goodEscape = (s, i) => i + 2 < s.length && isHex(s.charCodeAt(i + 1)) && isHex(s.charCodeAt(i + 2));
241
+ /**
242
+ * Go's unescape for queries (plusIsSpace), paths and fragments: decodes
243
+ * "%XX" escapes byte-wise; null when a "%" is not followed by two hex digits.
244
+ * Decoded bytes that are not UTF-8 become U+FFFD.
245
+ */
246
+ function unescape(s, plusIsSpace) {
247
+ let decode = false;
248
+ for (let i = 0; i < s.length; i++) {
249
+ const c = s.charCodeAt(i);
250
+ if (c === 0x25) {
251
+ if (!goodEscape(s, i))
252
+ return null;
253
+ decode = true;
254
+ i += 2;
255
+ }
256
+ else if (c === 0x2b && plusIsSpace) {
257
+ decode = true;
258
+ }
259
+ }
260
+ if (!decode)
261
+ return s;
262
+ const b = Buffer.from(s, 'utf8'); // "%", "+" and hex digits are single bytes
263
+ const out = Buffer.allocUnsafe(b.length);
264
+ let n = 0;
265
+ for (let i = 0; i < b.length; i++) {
266
+ const c = b[i];
267
+ if (c === 0x25) {
268
+ out[n++] = (unhex(b[i + 1]) << 4) | unhex(b[i + 2]);
269
+ i += 2;
270
+ }
271
+ else {
272
+ out[n++] = c === 0x2b && plusIsSpace ? 0x20 : c;
273
+ }
274
+ }
275
+ return out.toString('utf8', 0, n);
276
+ }
277
+ /** url.QueryUnescape: "%XX" escapes, and "+" is a space; null on a malformed escape. */
278
+ function queryUnescape(s) {
279
+ return unescape(s, true);
280
+ }
281
+ /** ASCII characters a host may contain unescaped (url.shouldEscape in host mode). */
282
+ const HOST = asciiTable(ALNUM + "!$&'()*+,;=:[]<>\"-_.~");
283
+ const hostChar = (c) => c < 128 && HOST[c] === 1;
284
+ /** Go's unescape in host (or IPv6 zone) mode, as a check: ASCII may not be escaped (but %25), nor written unless allowed. */
285
+ function validHostEscapes(s, zone) {
286
+ for (let i = 0; i < s.length; i++) {
287
+ const c = s.charCodeAt(i);
288
+ if (c === 0x25) {
289
+ if (!goodEscape(s, i))
290
+ return false;
291
+ const hi = unhex(s.charCodeAt(i + 1));
292
+ const v = (hi << 4) | unhex(s.charCodeAt(i + 2));
293
+ if (v !== 0x25) {
294
+ if (!zone && hi < 8)
295
+ return false;
296
+ if (zone && v !== 0x20 && !hostChar(v))
297
+ return false;
298
+ }
299
+ i += 2;
300
+ }
301
+ else if (c < 0x80 && !hostChar(c)) {
302
+ return false;
303
+ }
304
+ }
305
+ return true;
306
+ }
307
+ const validOptionalPort = (port) => /^(:[0-9]*)?$/.test(port);
308
+ /** parseHost: host[:port], with "[v6]" literals and their %25 zones. */
309
+ function validHost(host) {
310
+ if (host[0] === '[') {
311
+ const i = host.lastIndexOf(']');
312
+ if (i < 0)
313
+ return false; // missing ']' in host
314
+ if (!validOptionalPort(host.slice(i + 1)))
315
+ return false;
316
+ const zone = host.slice(0, i).indexOf('%25');
317
+ if (zone >= 0) {
318
+ return validHostEscapes(host.slice(0, zone), false) && validHostEscapes(host.slice(zone, i), true) && validHostEscapes(host.slice(i), false);
319
+ }
320
+ }
321
+ else {
322
+ const i = host.lastIndexOf(':');
323
+ if (i >= 0 && !validOptionalPort(host.slice(i)))
324
+ return false;
325
+ }
326
+ return validHostEscapes(host, false);
327
+ }
328
+ const USERINFO = /^[A-Za-z0-9\-._:~!$&'()*+,;=%@]*$/;
329
+ /** parseAuthority: [userinfo@]host[:port]. */
330
+ function validAuthority(authority) {
331
+ const at = authority.lastIndexOf('@');
332
+ if (!validHost(at < 0 ? authority : authority.slice(at + 1)))
333
+ return false;
334
+ if (at < 0)
335
+ return true;
336
+ const userinfo = authority.slice(0, at);
337
+ return USERINFO.test(userinfo) && unescape(userinfo, false) !== null;
338
+ }
339
+ const CTL = /[\x00-\x1f\x7f]/;
340
+ const SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:/;
341
+ /**
342
+ * url.Parse: null where Go fails (a control character, ":" without a
343
+ * scheme, a colon in the first segment of a relative path, a malformed host,
344
+ * port or user info, or a malformed escape in the path or fragment).
345
+ */
346
+ function parseURL(raw) {
347
+ const hash = raw.indexOf('#');
348
+ const parts = parseNoFragment(hash < 0 ? raw : raw.slice(0, hash));
349
+ if (parts === null)
350
+ return null;
351
+ if (hash >= 0 && unescape(raw.slice(hash + 1), false) === null)
352
+ return null;
353
+ return parts;
354
+ }
355
+ function parseNoFragment(raw) {
356
+ if (CTL.test(raw))
357
+ return null; // invalid control character in URL
358
+ if (raw === '*')
359
+ return { scheme: '', rawQuery: '', path: '*' };
360
+ if (raw[0] === ':')
361
+ return null; // missing protocol scheme
362
+ const m = SCHEME.exec(raw);
363
+ const scheme = m ? m[0].slice(0, -1).toLowerCase() : '';
364
+ let rest = m ? raw.slice(m[0].length) : raw;
365
+ let rawQuery = '';
366
+ const q = rest.indexOf('?');
367
+ if (q >= 0) {
368
+ rawQuery = rest.slice(q + 1);
369
+ rest = rest.slice(0, q);
370
+ }
371
+ if (rest[0] !== '/') {
372
+ if (scheme !== '')
373
+ return { scheme, rawQuery, path: '' }; // opaque: a rootless path
374
+ const slash = rest.indexOf('/');
375
+ if ((slash < 0 ? rest : rest.slice(0, slash)).indexOf(':') >= 0)
376
+ return null; // first path segment in URL cannot contain colon
377
+ }
378
+ if ((scheme !== '' || !rest.startsWith('///')) && rest.startsWith('//')) {
379
+ const slash = rest.indexOf('/', 2);
380
+ const authority = slash < 0 ? rest.slice(2) : rest.slice(2, slash);
381
+ rest = slash < 0 ? '' : rest.slice(slash);
382
+ if (!validAuthority(authority))
383
+ return null;
384
+ }
385
+ const path = unescape(rest, false);
386
+ return path === null ? null : { scheme, rawQuery, path };
387
+ }
388
+ /** path.Ext: the suffix from the last "." of the final element, or "". */
389
+ function pathExt(p) {
390
+ for (let i = p.length - 1; i >= 0 && p[i] !== '/'; i--) {
391
+ if (p[i] === '.')
392
+ return p.slice(i);
393
+ }
394
+ return '';
395
+ }