@karpeleslab/teamclaude 1.0.7 → 1.0.8

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.
@@ -0,0 +1,314 @@
1
+ // HPACK header compression (RFC 7541) — pure JS, no deps.
2
+ //
3
+ // Ported clean-room from the compcol Rust implementation (itself transcribed
4
+ // from RFC 7541's appendices). Used by the MITM proxy to decode/re-encode the
5
+ // HTTP/2 header block so it can rewrite only the `authorization` field while
6
+ // leaving everything else intact. Names/values are Buffers (byte-exact).
7
+ //
8
+ // State note: an HPACK codec is stateful across header blocks (the dynamic
9
+ // table evolves), so one HpackDecoder/HpackEncoder instance is kept per
10
+ // direction per connection.
11
+
12
+ // ── Huffman code table (RFC 7541 Appendix B): [code, bitLength] by symbol ──
13
+ // Index = symbol value; index 256 = EOS.
14
+ const CODES = [
15
+ [0x1ff8,13],[0x7fffd8,23],[0xfffffe2,28],[0xfffffe3,28],[0xfffffe4,28],[0xfffffe5,28],[0xfffffe6,28],[0xfffffe7,28],
16
+ [0xfffffe8,28],[0xffffea,24],[0x3ffffffc,30],[0xfffffe9,28],[0xfffffea,28],[0x3ffffffd,30],[0xfffffeb,28],[0xfffffec,28],
17
+ [0xfffffed,28],[0xfffffee,28],[0xfffffef,28],[0xffffff0,28],[0xffffff1,28],[0xffffff2,28],[0x3ffffffe,30],[0xffffff3,28],
18
+ [0xffffff4,28],[0xffffff5,28],[0xffffff6,28],[0xffffff7,28],[0xffffff8,28],[0xffffff9,28],[0xffffffa,28],[0xffffffb,28],
19
+ [0x14,6],[0x3f8,10],[0x3f9,10],[0xffa,12],[0x1ff9,13],[0x15,6],[0xf8,8],[0x7fa,11],
20
+ [0x3fa,10],[0x3fb,10],[0xf9,8],[0x7fb,11],[0xfa,8],[0x16,6],[0x17,6],[0x18,6],
21
+ [0x0,5],[0x1,5],[0x2,5],[0x19,6],[0x1a,6],[0x1b,6],[0x1c,6],[0x1d,6],
22
+ [0x1e,6],[0x1f,6],[0x5c,7],[0xfb,8],[0x7ffc,15],[0x20,6],[0xffb,12],[0x3fc,10],
23
+ [0x1ffa,13],[0x21,6],[0x5d,7],[0x5e,7],[0x5f,7],[0x60,7],[0x61,7],[0x62,7],
24
+ [0x63,7],[0x64,7],[0x65,7],[0x66,7],[0x67,7],[0x68,7],[0x69,7],[0x6a,7],
25
+ [0x6b,7],[0x6c,7],[0x6d,7],[0x6e,7],[0x6f,7],[0x70,7],[0x71,7],[0x72,7],
26
+ [0xfc,8],[0x73,7],[0xfd,8],[0x1ffb,13],[0x7fff0,19],[0x1ffc,13],[0x3ffc,14],[0x22,6],
27
+ [0x7ffd,15],[0x3,5],[0x23,6],[0x4,5],[0x24,6],[0x5,5],[0x25,6],[0x26,6],
28
+ [0x27,6],[0x6,5],[0x74,7],[0x75,7],[0x28,6],[0x29,6],[0x2a,6],[0x7,5],
29
+ [0x2b,6],[0x76,7],[0x2c,6],[0x8,5],[0x9,5],[0x2d,6],[0x77,7],[0x78,7],
30
+ [0x79,7],[0x7a,7],[0x7b,7],[0x7ffe,15],[0x7fc,11],[0x3ffd,14],[0x1ffd,13],[0xffffffc,28],
31
+ [0xfffe6,20],[0x3fffd2,22],[0xfffe7,20],[0xfffe8,20],[0x3fffd3,22],[0x3fffd4,22],[0x3fffd5,22],[0x7fffd9,23],
32
+ [0x3fffd6,22],[0x7fffda,23],[0x7fffdb,23],[0x7fffdc,23],[0x7fffdd,23],[0x7fffde,23],[0xffffeb,24],[0x7fffdf,23],
33
+ [0xffffec,24],[0xffffed,24],[0x3fffd7,22],[0x7fffe0,23],[0xffffee,24],[0x7fffe1,23],[0x7fffe2,23],[0x7fffe3,23],
34
+ [0x7fffe4,23],[0x1fffdc,21],[0x3fffd8,22],[0x7fffe5,23],[0x3fffd9,22],[0x7fffe6,23],[0x7fffe7,23],[0xffffef,24],
35
+ [0x3fffda,22],[0x1fffdd,21],[0xfffe9,20],[0x3fffdb,22],[0x3fffdc,22],[0x7fffe8,23],[0x7fffe9,23],[0x1fffde,21],
36
+ [0x7fffea,23],[0x3fffdd,22],[0x3fffde,22],[0xfffff0,24],[0x1fffdf,21],[0x3fffdf,22],[0x7fffeb,23],[0x7fffec,23],
37
+ [0x1fffe0,21],[0x1fffe1,21],[0x3fffe0,22],[0x1fffe2,21],[0x7fffed,23],[0x3fffe1,22],[0x7fffee,23],[0x7fffef,23],
38
+ [0xfffea,20],[0x3fffe2,22],[0x3fffe3,22],[0x3fffe4,22],[0x7ffff0,23],[0x3fffe5,22],[0x3fffe6,22],[0x7ffff1,23],
39
+ [0x3ffffe0,26],[0x3ffffe1,26],[0xfffeb,20],[0x7fff1,19],[0x3fffe7,22],[0x7ffff2,23],[0x3fffe8,22],[0x1ffffec,25],
40
+ [0x3ffffe2,26],[0x3ffffe3,26],[0x3ffffe4,26],[0x7ffffde,27],[0x7ffffdf,27],[0x3ffffe5,26],[0xfffff1,24],[0x1ffffed,25],
41
+ [0x7fff2,19],[0x1fffe3,21],[0x3ffffe6,26],[0x7ffffe0,27],[0x7ffffe1,27],[0x3ffffe7,26],[0x7ffffe2,27],[0xfffff2,24],
42
+ [0x1fffe4,21],[0x1fffe5,21],[0x3ffffe8,26],[0x3ffffe9,26],[0xffffffd,28],[0x7ffffe3,27],[0x7ffffe4,27],[0x7ffffe5,27],
43
+ [0xfffec,20],[0xfffff3,24],[0xfffed,20],[0x1fffe6,21],[0x3fffe9,22],[0x1fffe7,21],[0x1fffe8,21],[0x7ffff3,23],
44
+ [0x3fffea,22],[0x3fffeb,22],[0x1ffffee,25],[0x1ffffef,25],[0xfffff4,24],[0xfffff5,24],[0x3ffffea,26],[0x7ffff4,23],
45
+ [0x3ffffeb,26],[0x7ffffe6,27],[0x3ffffec,26],[0x3ffffed,26],[0x7ffffe7,27],[0x7ffffe8,27],[0x7ffffe9,27],[0x7ffffea,27],
46
+ [0x7ffffeb,27],[0xffffffe,28],[0x7ffffec,27],[0x7ffffed,27],[0x7ffffee,27],[0x7ffffef,27],[0x7fffff0,27],[0x3ffffee,26],
47
+ [0x3fffffff,30],
48
+ ];
49
+ const EOS = 256;
50
+
51
+ class HpackError extends Error {}
52
+
53
+ // ── Huffman decode trie (built once) ──────────────────────────────────────
54
+ const trie = { c0: [-1], c1: [-1], leaf: [-1] }; // node 0 = root
55
+ (function buildTrie() {
56
+ for (let sym = 0; sym < CODES.length; sym++) {
57
+ const [code, len] = CODES[sym];
58
+ let node = 0;
59
+ for (let i = len - 1; i >= 0; i--) {
60
+ const bit = (code >>> i) & 1;
61
+ const arr = bit ? trie.c1 : trie.c0;
62
+ let next = arr[node];
63
+ if (next === -1) {
64
+ next = trie.leaf.length;
65
+ trie.c0.push(-1); trie.c1.push(-1); trie.leaf.push(-1);
66
+ arr[node] = next;
67
+ }
68
+ node = next;
69
+ }
70
+ trie.leaf[node] = sym;
71
+ }
72
+ })();
73
+
74
+ export function huffmanEncodedLen(buf) {
75
+ let bits = 0;
76
+ for (const b of buf) bits += CODES[b][1];
77
+ return Math.ceil(bits / 8);
78
+ }
79
+
80
+ export function huffmanEncode(buf) {
81
+ const out = [];
82
+ let acc = 0n, nbits = 0n;
83
+ for (const b of buf) {
84
+ const [code, len] = CODES[b];
85
+ acc = (acc << BigInt(len)) | BigInt(code);
86
+ nbits += BigInt(len);
87
+ while (nbits >= 8n) {
88
+ nbits -= 8n;
89
+ out.push(Number((acc >> nbits) & 0xffn));
90
+ }
91
+ }
92
+ if (nbits > 0n) {
93
+ const pad = 8n - nbits;
94
+ out.push(Number(((acc << pad) | ((1n << pad) - 1n)) & 0xffn));
95
+ }
96
+ return Buffer.from(out);
97
+ }
98
+
99
+ export function huffmanDecode(buf) {
100
+ const out = [];
101
+ let node = 0, depth = 0, allOnes = true;
102
+ for (const byte of buf) {
103
+ for (let i = 7; i >= 0; i--) {
104
+ const bit = (byte >> i) & 1;
105
+ node = bit ? trie.c1[node] : trie.c0[node];
106
+ depth++; allOnes = allOnes && bit === 1;
107
+ const sym = trie.leaf[node];
108
+ if (sym >= 0) {
109
+ if (sym === EOS) throw new HpackError('EOS symbol in input');
110
+ out.push(sym);
111
+ node = 0; depth = 0; allOnes = true;
112
+ }
113
+ }
114
+ }
115
+ if (depth >= 8 || !allOnes) throw new HpackError('bad Huffman padding');
116
+ return Buffer.from(out);
117
+ }
118
+
119
+ // ── integer codec (RFC 7541 §5.1) ─────────────────────────────────────────
120
+ export function encodeInt(out, value, n, flags) {
121
+ const maxPrefix = (1 << n) - 1;
122
+ if (value < maxPrefix) { out.push(flags | value); return; }
123
+ out.push(flags | maxPrefix);
124
+ let v = value - maxPrefix;
125
+ while (v >= 128) { out.push((v & 0x7f) | 0x80); v = Math.floor(v / 128); }
126
+ out.push(v);
127
+ }
128
+
129
+ export function decodeInt(buf, pos, n) {
130
+ const maxPrefix = (1 << n) - 1;
131
+ if (pos >= buf.length) throw new HpackError('truncated integer');
132
+ let value = buf[pos] & maxPrefix;
133
+ let p = pos + 1;
134
+ if (value < maxPrefix) return [value, p];
135
+ let shift = 0;
136
+ for (;;) {
137
+ if (p >= buf.length) throw new HpackError('truncated integer');
138
+ const b = buf[p++];
139
+ if (shift >= 53) throw new HpackError('integer overflow');
140
+ value += (b & 0x7f) * 2 ** shift;
141
+ if (!(b & 0x80)) break;
142
+ shift += 7;
143
+ }
144
+ return [value, p];
145
+ }
146
+
147
+ // ── static table (RFC 7541 Appendix A) ─────────────────────────────────────
148
+ const STATIC_TABLE = [
149
+ [':authority', ''], [':method', 'GET'], [':method', 'POST'], [':path', '/'],
150
+ [':path', '/index.html'], [':scheme', 'http'], [':scheme', 'https'], [':status', '200'],
151
+ [':status', '204'], [':status', '206'], [':status', '304'], [':status', '400'],
152
+ [':status', '404'], [':status', '500'], ['accept-charset', ''], ['accept-encoding', 'gzip, deflate'],
153
+ ['accept-language', ''], ['accept-ranges', ''], ['accept', ''], ['access-control-allow-origin', ''],
154
+ ['age', ''], ['allow', ''], ['authorization', ''], ['cache-control', ''],
155
+ ['content-disposition', ''], ['content-encoding', ''], ['content-language', ''], ['content-length', ''],
156
+ ['content-location', ''], ['content-range', ''], ['content-type', ''], ['cookie', ''],
157
+ ['date', ''], ['etag', ''], ['expect', ''], ['expires', ''],
158
+ ['from', ''], ['host', ''], ['if-match', ''], ['if-modified-since', ''],
159
+ ['if-none-match', ''], ['if-range', ''], ['if-unmodified-since', ''], ['last-modified', ''],
160
+ ['link', ''], ['location', ''], ['max-forwards', ''], ['proxy-authenticate', ''],
161
+ ['proxy-authorization', ''], ['range', ''], ['referer', ''], ['refresh', ''],
162
+ ['retry-after', ''], ['server', ''], ['set-cookie', ''], ['strict-transport-security', ''],
163
+ ['transfer-encoding', ''], ['user-agent', ''], ['vary', ''], ['via', ''],
164
+ ['www-authenticate', ''],
165
+ ].map(([n, v]) => [Buffer.from(n), Buffer.from(v)]);
166
+ const STATIC_LEN = STATIC_TABLE.length;
167
+ const ENTRY_OVERHEAD = 32;
168
+
169
+ class DynamicTable {
170
+ constructor(maxSize) { this.entries = []; this.size = 0; this.maxSize = maxSize; }
171
+ setMaxSize(m) { this.maxSize = m; this.#evict(0); }
172
+ #entrySize(n, v) { return n.length + v.length + ENTRY_OVERHEAD; }
173
+ #evict(incoming) {
174
+ while (this.size + incoming > this.maxSize && this.entries.length) {
175
+ const [n, v] = this.entries.pop();
176
+ this.size -= this.#entrySize(n, v);
177
+ }
178
+ }
179
+ insert(name, value) {
180
+ const need = this.#entrySize(name, value);
181
+ this.#evict(need);
182
+ if (need <= this.maxSize) { this.entries.unshift([name, value]); this.size += need; }
183
+ }
184
+ get(index) {
185
+ if (index === 0) return null;
186
+ if (index <= STATIC_LEN) return STATIC_TABLE[index - 1];
187
+ const e = this.entries[index - STATIC_LEN - 1];
188
+ return e || null;
189
+ }
190
+ find(name, value) {
191
+ let nameOnly = null;
192
+ for (let i = 0; i < STATIC_TABLE.length; i++) {
193
+ const [n, v] = STATIC_TABLE[i];
194
+ if (n.equals(name)) {
195
+ if (v.equals(value)) return { index: i + 1, valueMatched: true };
196
+ if (nameOnly === null) nameOnly = i + 1;
197
+ }
198
+ }
199
+ for (let pos = 0; pos < this.entries.length; pos++) {
200
+ const [n, v] = this.entries[pos];
201
+ if (n.equals(name)) {
202
+ const index = STATIC_LEN + 1 + pos;
203
+ if (v.equals(value)) return { index, valueMatched: true };
204
+ if (nameOnly === null) nameOnly = index;
205
+ }
206
+ }
207
+ return nameOnly === null ? null : { index: nameOnly, valueMatched: false };
208
+ }
209
+ }
210
+
211
+ export const DEFAULT_TABLE_SIZE = 4096;
212
+
213
+ function readString(block, pos) {
214
+ if (pos >= block.length) throw new HpackError('truncated string');
215
+ const huff = (block[pos] & 0x80) !== 0;
216
+ const [len, p] = decodeInt(block, pos, 7);
217
+ const end = p + len;
218
+ if (end > block.length) throw new HpackError('truncated string');
219
+ const raw = block.subarray(p, end);
220
+ return [huff ? huffmanDecode(raw) : Buffer.from(raw), end];
221
+ }
222
+
223
+ // A decoded/encodable field. name/value are Buffers; sensitive = never-indexed.
224
+ export class HpackDecoder {
225
+ constructor(maxSize = DEFAULT_TABLE_SIZE) { this.table = new DynamicTable(maxSize); this.sizeLimit = maxSize; }
226
+ decode(block) {
227
+ const fields = [];
228
+ let pos = 0;
229
+ while (pos < block.length) {
230
+ const b = block[pos];
231
+ if (b & 0x80) { // §6.1 indexed
232
+ const [idx, np] = decodeInt(block, pos, 7); pos = np;
233
+ if (idx === 0) throw new HpackError('index 0');
234
+ const e = this.table.get(idx); if (!e) throw new HpackError('bad index');
235
+ fields.push({ name: e[0], value: e[1], sensitive: false });
236
+ } else if (b & 0x40) { // §6.2.1 literal w/ incremental indexing
237
+ const [name, value, np] = this.#readLiteral(block, pos, 6); pos = np;
238
+ this.table.insert(name, value);
239
+ fields.push({ name, value, sensitive: false });
240
+ } else if (b & 0x20) { // §6.3 dynamic table size update
241
+ const [newMax, np] = decodeInt(block, pos, 5); pos = np;
242
+ if (newMax > this.sizeLimit) throw new HpackError('size update over limit');
243
+ this.table.setMaxSize(newMax);
244
+ } else { // §6.2.2 / §6.2.3 (4-bit prefix; 0x10 = never indexed)
245
+ const sensitive = (b & 0x10) !== 0;
246
+ const [name, value, np] = this.#readLiteral(block, pos, 4); pos = np;
247
+ fields.push({ name, value, sensitive });
248
+ }
249
+ }
250
+ return fields;
251
+ }
252
+ #readLiteral(block, pos, prefix) {
253
+ const [idx, p0] = decodeInt(block, pos, prefix);
254
+ let p = p0, name;
255
+ if (idx === 0) { const [n, np] = readString(block, p); name = n; p = np; }
256
+ else { const e = this.table.get(idx); if (!e) throw new HpackError('bad name index'); name = e[0]; }
257
+ const [value, np] = readString(block, p);
258
+ return [name, value, np];
259
+ }
260
+ }
261
+
262
+ export class HpackEncoder {
263
+ constructor(maxSize = DEFAULT_TABLE_SIZE) {
264
+ this.table = new DynamicTable(maxSize);
265
+ this.useHuffman = true;
266
+ this.pendingSizeUpdate = maxSize === DEFAULT_TABLE_SIZE ? null : maxSize;
267
+ // When false, never insert into / reference the dynamic table — emit every
268
+ // field as a literal (full static matches still use the static index). This
269
+ // makes the encoder independent of the peer's SETTINGS_HEADER_TABLE_SIZE,
270
+ // which the MITM relay relies on (it doesn't track the upstream's table).
271
+ this.dynamicIndexing = true;
272
+ }
273
+ encode(fields) {
274
+ const out = [];
275
+ if (this.pendingSizeUpdate !== null) { encodeInt(out, this.pendingSizeUpdate, 5, 0x20); this.pendingSizeUpdate = null; }
276
+ for (const f of fields) this.#field(out, f);
277
+ return Buffer.from(out);
278
+ }
279
+ #field(out, f) {
280
+ const name = Buffer.isBuffer(f.name) ? f.name : Buffer.from(f.name);
281
+ const value = Buffer.isBuffer(f.value) ? f.value : Buffer.from(f.value);
282
+ if (f.sensitive) {
283
+ const m = this.table.find(name, value);
284
+ const nameIdx = (m && this.table.get(m.index)[0].equals(name)) ? m.index : null;
285
+ this.#literal(out, 0x10, 4, nameIdx, name, value);
286
+ return;
287
+ }
288
+ const m = this.table.find(name, value);
289
+ if (m && m.valueMatched) { encodeInt(out, m.index, 7, 0x80); return; } // indexed (static when no indexing)
290
+ if (this.dynamicIndexing) {
291
+ this.#literal(out, 0x40, 6, m ? m.index : null, name, value); // literal w/ incremental indexing
292
+ this.table.insert(name, value);
293
+ } else {
294
+ this.#literal(out, 0x00, 4, m ? m.index : null, name, value); // literal without indexing; no insert
295
+ }
296
+ }
297
+ #literal(out, pattern, prefix, nameIdx, name, value) {
298
+ if (nameIdx !== null) encodeInt(out, nameIdx, prefix, pattern);
299
+ else { encodeInt(out, 0, prefix, pattern); this.#string(out, name); }
300
+ this.#string(out, value);
301
+ }
302
+ #string(out, s) {
303
+ if (this.useHuffman && huffmanEncodedLen(s) < s.length) {
304
+ const coded = huffmanEncode(s);
305
+ encodeInt(out, coded.length, 7, 0x80);
306
+ for (const b of coded) out.push(b);
307
+ } else {
308
+ encodeInt(out, s.length, 7, 0x00);
309
+ for (const b of s) out.push(b);
310
+ }
311
+ }
312
+ }
313
+
314
+ export { HpackError };