staff-mcp 1.2.0 → 2.0.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.
Files changed (133) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +178 -49
  3. package/README_zh.md +176 -47
  4. package/dist/src/data/HttpDataSession.d.ts +22 -0
  5. package/dist/src/data/HttpDataSession.js +925 -0
  6. package/dist/src/data/HttpDataSession.js.map +1 -0
  7. package/dist/src/data/node.d.ts +4 -0
  8. package/dist/src/data/node.js +93 -0
  9. package/dist/src/data/node.js.map +1 -0
  10. package/dist/src/data/types.d.ts +54 -0
  11. package/dist/src/data/types.js +2 -0
  12. package/dist/src/data/types.js.map +1 -0
  13. package/dist/src/data-stdio-docker.d.ts +10 -0
  14. package/dist/src/data-stdio-docker.js +140 -0
  15. package/dist/src/data-stdio-docker.js.map +1 -0
  16. package/dist/src/docker-proxy.d.ts +6 -1
  17. package/dist/src/docker-proxy.js +324 -147
  18. package/dist/src/docker-proxy.js.map +1 -1
  19. package/dist/src/extensions/apps.d.ts +12 -0
  20. package/dist/src/extensions/apps.js +85 -0
  21. package/dist/src/extensions/apps.js.map +1 -0
  22. package/dist/src/extensions/config.d.ts +27 -0
  23. package/dist/src/extensions/config.js +91 -0
  24. package/dist/src/extensions/config.js.map +1 -0
  25. package/dist/src/extensions/protocol.d.ts +35 -0
  26. package/dist/src/extensions/protocol.js +47 -0
  27. package/dist/src/extensions/protocol.js.map +1 -0
  28. package/dist/src/extensions/runtime.d.ts +16 -0
  29. package/dist/src/extensions/runtime.js +266 -0
  30. package/dist/src/extensions/runtime.js.map +1 -0
  31. package/dist/src/file-manager/extension.d.ts +21 -0
  32. package/dist/src/file-manager/extension.js +514 -0
  33. package/dist/src/file-manager/extension.js.map +1 -0
  34. package/dist/src/file-manager/file-core.d.ts +128 -0
  35. package/dist/src/file-manager/file-core.js +750 -0
  36. package/dist/src/file-manager/file-core.js.map +1 -0
  37. package/dist/src/file-manager/transfers.d.ts +423 -0
  38. package/dist/src/file-manager/transfers.js +540 -0
  39. package/dist/src/file-manager/transfers.js.map +1 -0
  40. package/dist/src/file-manager/ui/index.html +81 -0
  41. package/dist/src/index.js +289 -128
  42. package/dist/src/index.js.map +1 -1
  43. package/dist/src/lsp/manager.d.ts +2 -0
  44. package/dist/src/lsp/manager.js +22 -10
  45. package/dist/src/lsp/manager.js.map +1 -1
  46. package/dist/src/reverse/SseTransport.d.ts +34 -0
  47. package/dist/src/reverse/SseTransport.js +215 -0
  48. package/dist/src/reverse/SseTransport.js.map +1 -0
  49. package/dist/src/reverse/http.d.ts +7 -0
  50. package/dist/src/reverse/http.js +85 -0
  51. package/dist/src/reverse/http.js.map +1 -0
  52. package/dist/src/server.d.ts +4 -2
  53. package/dist/src/server.js +80 -18
  54. package/dist/src/server.js.map +1 -1
  55. package/dist/src/terminal/extension.d.ts +4 -0
  56. package/dist/src/terminal/extension.js +108 -0
  57. package/dist/src/terminal/extension.js.map +1 -0
  58. package/dist/src/terminal/pty.d.ts +14 -0
  59. package/dist/src/terminal/pty.js +121 -0
  60. package/dist/src/terminal/pty.js.map +1 -0
  61. package/dist/src/terminal/session.d.ts +136 -0
  62. package/dist/src/terminal/session.js +422 -0
  63. package/dist/src/terminal/session.js.map +1 -0
  64. package/dist/src/terminal/ui/index.html +183 -0
  65. package/dist/src/tools/file-tools.d.ts +2 -1
  66. package/dist/src/tools/file-tools.js +19 -7
  67. package/dist/src/tools/file-tools.js.map +1 -1
  68. package/dist/src/tools/mcp-client-tools.js +244 -104
  69. package/dist/src/tools/mcp-client-tools.js.map +1 -1
  70. package/dist/src/tools/shell-tools.js +1 -1
  71. package/dist/src/tools/shell-tools.js.map +1 -1
  72. package/dist/src/tools/skills.js +26 -1
  73. package/dist/src/tools/skills.js.map +1 -1
  74. package/dist/src/tools/system-tools.d.ts +9 -3
  75. package/dist/src/tools/system-tools.js +31 -48
  76. package/dist/src/tools/system-tools.js.map +1 -1
  77. package/dist/src/transports/http.d.ts +16 -2
  78. package/dist/src/transports/http.js +261 -59
  79. package/dist/src/transports/http.js.map +1 -1
  80. package/dist/src/transports/reverse.d.ts +23 -19
  81. package/dist/src/transports/reverse.js +130 -74
  82. package/dist/src/transports/reverse.js.map +1 -1
  83. package/dist/src/transports/stdio.d.ts +6 -2
  84. package/dist/src/transports/stdio.js +131 -19
  85. package/dist/src/transports/stdio.js.map +1 -1
  86. package/dist/src/utils/proxy.d.ts +3 -3
  87. package/dist/src/utils/proxy.js +84 -62
  88. package/dist/src/utils/proxy.js.map +1 -1
  89. package/dist/src/utils/server-activity.d.ts +6 -0
  90. package/dist/src/utils/server-activity.js +46 -0
  91. package/dist/src/utils/server-activity.js.map +1 -0
  92. package/dist/src/utils/server-lifecycle.d.ts +5 -0
  93. package/dist/src/utils/server-lifecycle.js +51 -0
  94. package/dist/src/utils/server-lifecycle.js.map +1 -0
  95. package/docs/file-manager.md +179 -0
  96. package/docs/terminal.md +66 -0
  97. package/package.json +40 -19
  98. package/dist/src/transports/fte-lifecycle.d.ts +0 -13
  99. package/dist/src/transports/fte-lifecycle.js +0 -76
  100. package/dist/src/transports/fte-lifecycle.js.map +0 -1
  101. package/dist/tests/docker-proxy.test.d.ts +0 -1
  102. package/dist/tests/docker-proxy.test.js +0 -395
  103. package/dist/tests/docker-proxy.test.js.map +0 -1
  104. package/dist/tests/file-write-queue.test.d.ts +0 -1
  105. package/dist/tests/file-write-queue.test.js +0 -105
  106. package/dist/tests/file-write-queue.test.js.map +0 -1
  107. package/dist/tests/fte-lifecycle.test.d.ts +0 -1
  108. package/dist/tests/fte-lifecycle.test.js +0 -118
  109. package/dist/tests/fte-lifecycle.test.js.map +0 -1
  110. package/dist/tests/lsp-client.test.d.ts +0 -1
  111. package/dist/tests/lsp-client.test.js +0 -52
  112. package/dist/tests/lsp-client.test.js.map +0 -1
  113. package/dist/tests/package-info.test.d.ts +0 -1
  114. package/dist/tests/package-info.test.js +0 -54
  115. package/dist/tests/package-info.test.js.map +0 -1
  116. package/dist/tests/proxy-no-proxy.test.d.ts +0 -1
  117. package/dist/tests/proxy-no-proxy.test.js +0 -156
  118. package/dist/tests/proxy-no-proxy.test.js.map +0 -1
  119. package/dist/tests/proxy.test.d.ts +0 -1
  120. package/dist/tests/proxy.test.js +0 -89
  121. package/dist/tests/proxy.test.js.map +0 -1
  122. package/dist/tests/reverse-reconnect.test.d.ts +0 -1
  123. package/dist/tests/reverse-reconnect.test.js +0 -89
  124. package/dist/tests/reverse-reconnect.test.js.map +0 -1
  125. package/dist/tests/security.test.d.ts +0 -1
  126. package/dist/tests/security.test.js +0 -21
  127. package/dist/tests/security.test.js.map +0 -1
  128. package/dist/tests/shell-tools.test.d.ts +0 -1
  129. package/dist/tests/shell-tools.test.js +0 -113
  130. package/dist/tests/shell-tools.test.js.map +0 -1
  131. package/dist/tests/test_files/test.d.ts +0 -1
  132. package/dist/tests/test_files/test.js +0 -4
  133. package/dist/tests/test_files/test.js.map +0 -1
@@ -0,0 +1,925 @@
1
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { setImmediate as yieldImmediate } from 'node:timers/promises';
3
+ export const DATA_OPEN_METHOD = 'cc.qqxing/data/v1/open';
4
+ const CHUNK = 64 * 1024;
5
+ const WINDOW = 256 * 1024;
6
+ const HEADERS = { 'Cache-Control': 'no-store' };
7
+ function deferred() {
8
+ let resolve;
9
+ let reject;
10
+ const promise = new Promise((yes, no) => {
11
+ resolve = yes;
12
+ reject = no;
13
+ });
14
+ void promise.catch(() => { });
15
+ return { promise, resolve, reject };
16
+ }
17
+ function check(value, message) {
18
+ if (!value)
19
+ throw new Error(message);
20
+ }
21
+ function errorOf(error) {
22
+ return error instanceof Error ? error : new Error(String(error));
23
+ }
24
+ function validPurpose(value) {
25
+ return typeof value === 'string' && value.length > 0 && value.length <= 256;
26
+ }
27
+ function validOpen(value, supportsPurpose) {
28
+ const open = value;
29
+ check(open &&
30
+ validPurpose(open.purpose) &&
31
+ typeof open.resourceId === 'string' &&
32
+ open.resourceId.length > 0 &&
33
+ open.resourceId.length <= 256 &&
34
+ ['host-to-remote', 'remote-to-host', 'duplex'].includes(open.direction), 'Invalid data channel metadata');
35
+ check(supportsPurpose(open.purpose), 'Unsupported data channel purpose');
36
+ return {
37
+ purpose: open.purpose,
38
+ resourceId: open.resourceId,
39
+ direction: open.direction,
40
+ };
41
+ }
42
+ function sameOpen(left, right) {
43
+ return (left.purpose === right.purpose &&
44
+ left.resourceId === right.resourceId &&
45
+ left.direction === right.direction);
46
+ }
47
+ function endpointUrl(value) {
48
+ const url = new URL(value);
49
+ check(['http:', 'https:'].includes(url.protocol) &&
50
+ !url.username &&
51
+ !url.password &&
52
+ !url.search &&
53
+ !url.hash &&
54
+ url.pathname !== '/' &&
55
+ !url.pathname.endsWith('/'), 'Invalid data endpoint');
56
+ check(url.href === value, 'Data endpoint must be canonical');
57
+ return url;
58
+ }
59
+ /** One logical channel; HTTP legs never invoke a second business accept. */
60
+ class HttpChannel {
61
+ link;
62
+ options;
63
+ serving;
64
+ release;
65
+ completion = deferred();
66
+ receiverReady = deferred();
67
+ changed = deferred();
68
+ connected = deferred();
69
+ closed = this.completion.promise;
70
+ abort = new AbortController();
71
+ outgoing;
72
+ acknowledgments;
73
+ output;
74
+ ackOutput;
75
+ receiver;
76
+ writing;
77
+ reading;
78
+ legs = new Set();
79
+ sent = 0;
80
+ consumed = 0;
81
+ received = 0;
82
+ ackSequence = 0;
83
+ ackPending = false;
84
+ ackFinished = false;
85
+ ackWanted;
86
+ ackSending;
87
+ receiveSequence = 0;
88
+ ending = false;
89
+ outputEnded = false;
90
+ inputEnded = false;
91
+ writingNow = false;
92
+ writeFinalized = false;
93
+ ackDemand = deferred();
94
+ consuming = false;
95
+ disposed = false;
96
+ activated = false;
97
+ transportReady = false;
98
+ failure;
99
+ lastActivity = performance.now();
100
+ ticketDeadline;
101
+ openTimer;
102
+ idleTimer;
103
+ upload;
104
+ acknowledgmentsRead;
105
+ bytesSinceYield = 0;
106
+ constructor(link, options, serving, release) {
107
+ this.link = link;
108
+ this.options = options;
109
+ this.serving = serving;
110
+ this.release = release;
111
+ this.ticketDeadline = performance.now() + link.expiresInMs;
112
+ const host = options.role === 'host';
113
+ this.writing =
114
+ link.open.direction === 'duplex' ||
115
+ link.open.direction ===
116
+ (host ? 'host-to-remote' : 'remote-to-host');
117
+ this.reading = link.open.direction === 'duplex' || !this.writing;
118
+ this.outputEnded = !this.writing;
119
+ this.writeFinalized = !this.writing;
120
+ this.inputEnded = !this.reading;
121
+ this.outgoing = new ReadableStream({
122
+ start: (controller) => {
123
+ this.output = controller;
124
+ },
125
+ cancel: () => this.close('HTTP data reader cancelled'),
126
+ }, { highWaterMark: 0 });
127
+ this.acknowledgments = new ReadableStream({
128
+ start: (controller) => {
129
+ this.ackOutput = controller;
130
+ },
131
+ pull: () => {
132
+ this.ackDemand.resolve();
133
+ },
134
+ cancel: () => {
135
+ if (!this.ackFinished)
136
+ this.close('HTTP acknowledgment reader cancelled');
137
+ },
138
+ }, { highWaterMark: 0 });
139
+ this.openTimer = setTimeout(() => this.close('HTTP data legs timed out'), options.openTimeoutMs ?? 15_000);
140
+ const idle = options.idleTimeoutMs ?? 60_000;
141
+ this.idleTimer = setInterval(() => {
142
+ if (this.pendingConsumption() &&
143
+ performance.now() - this.lastActivity >= idle)
144
+ this.close('HTTP data consumption timed out');
145
+ }, Math.min(idle, 1_000));
146
+ this.openTimer.unref?.();
147
+ this.idleTimer.unref?.();
148
+ }
149
+ setReceiver(receiver) {
150
+ if (this.disposed) {
151
+ receiver.close(this.failure);
152
+ return;
153
+ }
154
+ this.receiver = receiver;
155
+ this.receiverReady.resolve(receiver);
156
+ }
157
+ assertAlive() {
158
+ if (this.disposed)
159
+ throw this.failure ?? new Error('Data channel already closed');
160
+ }
161
+ pendingConsumption() {
162
+ return (this.sent > this.consumed ||
163
+ (this.ending && !this.writeFinalized) ||
164
+ this.consuming ||
165
+ this.ackPending ||
166
+ this.ackSending !== undefined);
167
+ }
168
+ startConsumption() {
169
+ // A healthy idle PTY is governed by its business lease, not an extra
170
+ // data-plane lease. Start a fresh progress deadline when work appears.
171
+ if (!this.pendingConsumption())
172
+ this.lastActivity = performance.now();
173
+ }
174
+ pulse() {
175
+ this.lastActivity = performance.now();
176
+ this.changed.resolve();
177
+ this.changed = deferred();
178
+ }
179
+ async wait(promise) {
180
+ this.assertAlive();
181
+ // A cancellable waiter, not Promise.race(closed): the latter retains one
182
+ // closed.then callback per chunk until a potentially hours-long channel ends.
183
+ return new Promise((resolve, reject) => {
184
+ const signal = this.abort.signal;
185
+ const cancel = () => reject(this.failure ?? new Error('Data channel closed'));
186
+ signal.addEventListener('abort', cancel, { once: true });
187
+ promise.then((value) => {
188
+ signal.removeEventListener('abort', cancel);
189
+ resolve(value);
190
+ }, (error) => {
191
+ signal.removeEventListener('abort', cancel);
192
+ reject(error);
193
+ });
194
+ if (signal.aborted)
195
+ cancel();
196
+ });
197
+ }
198
+ attachSignal(signal) {
199
+ if (!signal)
200
+ return;
201
+ const cancel = () => this.close('Data channel aborted');
202
+ if (signal.aborted)
203
+ cancel();
204
+ else {
205
+ signal.addEventListener('abort', cancel, { once: true });
206
+ void this.closed
207
+ .finally(() => signal.removeEventListener('abort', cancel))
208
+ .catch(() => { });
209
+ }
210
+ }
211
+ claim(leg) {
212
+ this.assertAlive();
213
+ check(!this.legs.has(leg), 'Data ticket already used');
214
+ check(leg === 'ready' || (leg === 'get' ? this.writing : this.reading), 'Wrong data direction');
215
+ this.legs.add(leg);
216
+ // A real metadata response proves ticket admission without requiring a
217
+ // first data/ACK chunk (Next does not flush streaming GET headers early).
218
+ // PUT may be lazy until its first byte/EOF; reserve it with ready, then
219
+ // enforce its own single-use claim and the consumption idle deadline.
220
+ const ready = this.legs.has('ready') &&
221
+ (!this.writing || this.legs.has('get')) &&
222
+ (!this.reading || this.legs.has('ack'));
223
+ if (ready) {
224
+ this.transportReady = true;
225
+ if (this.activated)
226
+ clearTimeout(this.openTimer);
227
+ this.connected.resolve();
228
+ }
229
+ this.pulse();
230
+ }
231
+ ticketExpired() {
232
+ // The single-use ready claim redeems the ticket; it cannot renew it.
233
+ // A reserved lazy PUT can attach later while the logical channel lives.
234
+ return (!this.legs.has('ready') && performance.now() >= this.ticketDeadline);
235
+ }
236
+ async waitConnected() {
237
+ await this.wait(this.connected.promise);
238
+ }
239
+ activate() {
240
+ this.assertAlive();
241
+ this.activated = true;
242
+ if (this.transportReady)
243
+ clearTimeout(this.openTimer);
244
+ this.finish();
245
+ }
246
+ clientConnected() {
247
+ this.transportReady = true;
248
+ if (this.activated)
249
+ clearTimeout(this.openTimer);
250
+ this.connected.resolve();
251
+ this.pulse();
252
+ }
253
+ async fairness(bytes) {
254
+ this.bytesSinceYield += bytes;
255
+ if (this.bytesSinceYield >= WINDOW) {
256
+ this.bytesSinceYield %= WINDOW;
257
+ await this.wait(yieldImmediate());
258
+ }
259
+ }
260
+ async write(bytes) {
261
+ this.assertAlive();
262
+ check(this.writing && !this.ending, 'Data channel is not writable');
263
+ // No unbounded write queue retaining arbitrary caller buffers.
264
+ check(!this.writingNow, 'Concurrent data writes are not supported');
265
+ this.writingNow = true;
266
+ try {
267
+ await this.wait(this.connected.promise);
268
+ for (let offset = 0; offset < bytes.byteLength; offset += CHUNK) {
269
+ this.assertAlive();
270
+ const length = Math.min(CHUNK, bytes.byteLength - offset);
271
+ while (this.sent - this.consumed + length > WINDOW) {
272
+ await this.wait(this.changed.promise);
273
+ }
274
+ // Buffer.slice aliases memory, so always make an owned Uint8Array copy.
275
+ const chunk = new Uint8Array(length);
276
+ chunk.set(bytes.subarray(offset, offset + length));
277
+ this.startConsumption();
278
+ this.sent += chunk.byteLength;
279
+ check(Number.isSafeInteger(this.sent), 'Data offset overflow');
280
+ this.output.enqueue(chunk);
281
+ // Copy/queue admission only; credit advances exclusively on receiver consumption ACK.
282
+ await this.fairness(chunk.byteLength);
283
+ }
284
+ }
285
+ catch (error) {
286
+ this.fail(errorOf(error));
287
+ throw error;
288
+ }
289
+ finally {
290
+ this.writingNow = false;
291
+ }
292
+ }
293
+ async end() {
294
+ if (this.outputEnded)
295
+ return;
296
+ this.assertAlive();
297
+ check(this.writing && !this.ending && !this.writingNow, 'Invalid data end');
298
+ this.startConsumption();
299
+ this.ending = true;
300
+ try {
301
+ await this.wait(this.connected.promise);
302
+ // EOF is emitted immediately, never await a final socket drain before end.
303
+ this.output.close();
304
+ while (!this.outputEnded)
305
+ await this.wait(this.changed.promise);
306
+ if (this.upload)
307
+ await this.upload;
308
+ if (this.acknowledgmentsRead)
309
+ await this.acknowledgmentsRead;
310
+ this.writeFinalized = true;
311
+ this.finish();
312
+ }
313
+ catch (error) {
314
+ this.fail(errorOf(error));
315
+ throw error;
316
+ }
317
+ }
318
+ beginAck() {
319
+ this.assertAlive();
320
+ check(!this.ackPending, 'Concurrent consumption acknowledgment');
321
+ this.startConsumption();
322
+ this.ackPending = true;
323
+ }
324
+ endAck() {
325
+ this.ackPending = false;
326
+ }
327
+ applyAck(raw) {
328
+ this.assertAlive();
329
+ const ack = raw;
330
+ check(ack &&
331
+ Number.isSafeInteger(ack.sequence) &&
332
+ ack.sequence === this.ackSequence + 1 &&
333
+ Number.isSafeInteger(ack.offset) &&
334
+ ack.offset >= this.consumed &&
335
+ ack.offset <= this.sent &&
336
+ typeof ack.ended === 'boolean' &&
337
+ !this.outputEnded &&
338
+ (ack.ended
339
+ ? this.ending && ack.offset === this.sent
340
+ : ack.offset > this.consumed), 'Invalid or replayed consumption acknowledgment');
341
+ this.ackSequence = ack.sequence;
342
+ this.consumed = ack.offset;
343
+ if (ack.ended)
344
+ this.outputEnded = true;
345
+ this.pulse();
346
+ // end() owns final completion, after its HTTP PUT success (if any).
347
+ }
348
+ queueAck(ended) {
349
+ // One in-flight HTTP operation plus one latest cumulative offset, never
350
+ // a per-chunk request queue. Receiver consumption does NOT await an RTT.
351
+ this.ackWanted = { offset: this.received, ended };
352
+ this.startAckPump();
353
+ }
354
+ startAckPump() {
355
+ if (this.ackSending || !this.ackWanted)
356
+ return;
357
+ this.startConsumption();
358
+ const sending = this.pumpAcks();
359
+ this.ackSending = sending;
360
+ void sending.then(() => {
361
+ this.ackSending = undefined;
362
+ if (!this.disposed)
363
+ this.startAckPump();
364
+ }, (error) => this.fail(errorOf(error)));
365
+ }
366
+ async pumpAcks() {
367
+ while (this.ackWanted) {
368
+ const latest = this.ackWanted;
369
+ this.ackWanted = undefined;
370
+ const ack = { sequence: ++this.receiveSequence, ...latest };
371
+ if (this.serving) {
372
+ await this.wait(this.ackDemand.promise);
373
+ this.ackDemand = deferred();
374
+ if (ack.ended)
375
+ this.ackFinished = true;
376
+ this.ackOutput.enqueue(new TextEncoder().encode(JSON.stringify(ack) + '\n'));
377
+ if (ack.ended)
378
+ this.ackOutput.close();
379
+ }
380
+ else {
381
+ const response = await this.request('ack', 'POST', JSON.stringify(ack));
382
+ await response.body?.cancel();
383
+ }
384
+ }
385
+ }
386
+ async finalAck() {
387
+ this.queueAck(true);
388
+ while (this.ackSending)
389
+ await this.wait(this.ackSending);
390
+ }
391
+ async receive(stream) {
392
+ const reader = stream?.getReader();
393
+ check(reader, 'Missing raw HTTP body');
394
+ try {
395
+ const receiver = await this.wait(this.receiverReady.promise);
396
+ while (true) {
397
+ const result = await this.wait(reader.read());
398
+ if (result.done)
399
+ break;
400
+ for (let offset = 0; offset < result.value.byteLength; offset += CHUNK) {
401
+ const bytes = result.value.subarray(offset, offset + CHUNK);
402
+ this.startConsumption();
403
+ this.consuming = true;
404
+ await this.wait(Promise.resolve().then(() => {
405
+ this.assertAlive();
406
+ return receiver.data(bytes);
407
+ }));
408
+ this.assertAlive();
409
+ this.consuming = false;
410
+ this.received += bytes.byteLength;
411
+ check(Number.isSafeInteger(this.received), 'Data offset overflow');
412
+ this.queueAck(false);
413
+ this.pulse();
414
+ await this.fairness(bytes.byteLength);
415
+ }
416
+ }
417
+ this.startConsumption();
418
+ this.consuming = true;
419
+ await this.wait(Promise.resolve().then(() => {
420
+ this.assertAlive();
421
+ return receiver.end();
422
+ }));
423
+ await this.finalAck();
424
+ this.consuming = false;
425
+ this.inputEnded = true;
426
+ this.pulse();
427
+ this.finish();
428
+ }
429
+ catch (error) {
430
+ this.fail(errorOf(error));
431
+ throw error;
432
+ }
433
+ finally {
434
+ this.consuming = false;
435
+ // reader.cancel releases a blocked network read on failure.
436
+ if (this.failure)
437
+ void reader.cancel(this.failure).catch(() => { });
438
+ reader.releaseLock();
439
+ }
440
+ }
441
+ async request(leg, method, body) {
442
+ this.assertAlive();
443
+ const init = {
444
+ method,
445
+ redirect: 'error',
446
+ cache: 'no-store',
447
+ signal: this.abort.signal,
448
+ headers: {
449
+ Authorization: `Bearer ${this.link.token}`,
450
+ 'X-Data-Binding': this.link.binding,
451
+ 'Content-Type': typeof body === 'string'
452
+ ? 'application/json'
453
+ : 'application/octet-stream',
454
+ 'Accept-Encoding': 'identity',
455
+ },
456
+ };
457
+ if (body !== undefined) {
458
+ init.body = body;
459
+ if (typeof body !== 'string')
460
+ init.duplex = 'half';
461
+ }
462
+ const response = await this.wait(this.options.fetch(`${this.link.endpoint}/${this.link.channel}/${leg}`, init));
463
+ const encoding = response.headers.get('content-encoding');
464
+ if (!response.ok || (encoding && encoding !== 'identity')) {
465
+ await response.body?.cancel();
466
+ throw new Error('HTTP data request failed');
467
+ }
468
+ return response;
469
+ }
470
+ async readAcks(stream) {
471
+ const reader = stream?.getReader();
472
+ check(reader, 'Missing HTTP consumption acknowledgments');
473
+ let pending = '';
474
+ const decoder = new TextDecoder('utf-8', { fatal: true });
475
+ try {
476
+ while (true) {
477
+ const next = await this.wait(reader.read());
478
+ if (next.done) {
479
+ pending += decoder.decode();
480
+ break;
481
+ }
482
+ check(!this.outputEnded || next.value.byteLength === 0, 'Data after final acknowledgment');
483
+ pending += decoder.decode(next.value, { stream: true });
484
+ let index;
485
+ while ((index = pending.indexOf('\n')) >= 0) {
486
+ check(index <= 256, 'HTTP acknowledgment record limit');
487
+ this.applyAck(JSON.parse(pending.slice(0, index)));
488
+ pending = pending.slice(index + 1);
489
+ if (this.outputEnded) {
490
+ check(pending.length === 0, 'Data after final acknowledgment');
491
+ // Read the HTTP EOF instead of cancelling a valid final ACK leg.
492
+ // Cancelling here can abort the peer's still-open duplex channel.
493
+ }
494
+ }
495
+ check(pending.length <= 256, 'HTTP acknowledgment record limit');
496
+ }
497
+ check(pending.length === 0 && this.outputEnded, 'Truncated HTTP acknowledgments');
498
+ }
499
+ finally {
500
+ if (this.failure)
501
+ void reader.cancel(this.failure).catch(() => { });
502
+ reader.releaseLock();
503
+ }
504
+ }
505
+ async connect() {
506
+ try {
507
+ if (this.writing) {
508
+ this.acknowledgmentsRead = this.request('ack', 'GET').then((response) => this.readAcks(response.body));
509
+ void this.acknowledgmentsRead.catch((error) => this.fail(errorOf(error)));
510
+ this.upload = this.request('put', 'PUT', this.outgoing).then(async (response) => {
511
+ await response.body?.cancel();
512
+ check(this.ending, 'Premature HTTP upload completion');
513
+ });
514
+ void this.upload.catch((error) => this.fail(errorOf(error)));
515
+ }
516
+ if (this.reading) {
517
+ void this.request('get', 'GET')
518
+ .then((response) => this.receive(response.body))
519
+ .catch((error) => this.fail(errorOf(error)));
520
+ }
521
+ // No payload/prelude or invented consumption credit. Unlike raw GET
522
+ // headers, this empty authenticated response is independent of bytes
523
+ // and EOF; it also rejects stale/replayed links before business accept.
524
+ const ready = await this.request('ready', 'POST');
525
+ await ready.body?.cancel();
526
+ this.clientConnected();
527
+ }
528
+ catch (error) {
529
+ this.fail(errorOf(error));
530
+ throw error;
531
+ }
532
+ }
533
+ finish() {
534
+ if (!this.activated ||
535
+ !this.inputEnded ||
536
+ !this.outputEnded ||
537
+ !this.writeFinalized ||
538
+ this.disposed)
539
+ return;
540
+ this.cleanup();
541
+ this.completion.resolve();
542
+ try {
543
+ this.receiver?.close();
544
+ }
545
+ catch {
546
+ /* Completion cannot be undone by cleanup. */
547
+ }
548
+ }
549
+ cleanup() {
550
+ this.disposed = true;
551
+ this.ackWanted = undefined;
552
+ clearTimeout(this.openTimer);
553
+ clearInterval(this.idleTimer);
554
+ this.release();
555
+ }
556
+ fail(error) {
557
+ if (this.disposed)
558
+ return;
559
+ this.failure = error;
560
+ this.cleanup();
561
+ this.abort.abort(error);
562
+ this.receiverReady.reject(error);
563
+ this.connected.reject(error);
564
+ this.changed.reject(error);
565
+ this.ackDemand.reject(error);
566
+ try {
567
+ this.output.error(error);
568
+ }
569
+ catch {
570
+ /* Already ended. */
571
+ }
572
+ try {
573
+ this.ackOutput.error(error);
574
+ }
575
+ catch {
576
+ /* Already ended. */
577
+ }
578
+ this.completion.reject(error);
579
+ try {
580
+ this.receiver?.close(error);
581
+ }
582
+ catch {
583
+ /* Preserve original failure. */
584
+ }
585
+ }
586
+ close(code = 'Data channel cancelled') {
587
+ if (this.disposed)
588
+ return;
589
+ // Best effort only; no retry or replay. Local cancellation is immediate.
590
+ if (!this.serving) {
591
+ const signal = AbortSignal.timeout(1_000);
592
+ void this.options
593
+ .fetch(`${this.link.endpoint}/${this.link.channel}/close`, {
594
+ method: 'DELETE',
595
+ redirect: 'error',
596
+ signal,
597
+ headers: {
598
+ Authorization: `Bearer ${this.link.token}`,
599
+ 'X-Data-Binding': this.link.binding,
600
+ },
601
+ })
602
+ .then((response) => response.body?.cancel())
603
+ .catch(() => { });
604
+ }
605
+ this.fail(new Error(code));
606
+ }
607
+ }
608
+ /** Connection-scoped HTTP binary data plane; no SDK, business classes or listeners. */
609
+ export class HttpDataSession {
610
+ options;
611
+ channels = new Map();
612
+ stopped = false;
613
+ pendingOpens = new Set();
614
+ usedLinks = new Map();
615
+ endpoint;
616
+ maximum;
617
+ constructor(options) {
618
+ this.options = options;
619
+ check(typeof options.fetch === 'function' &&
620
+ typeof options.validateEndpoint === 'function', 'HTTP client and endpoint policy are required');
621
+ check(typeof options.binding === 'string' &&
622
+ /^[A-Za-z0-9_-]{16,256}$/.test(options.binding), 'Invalid connection data binding');
623
+ check(typeof options.supportsPurpose === 'function', 'Purpose policy is required');
624
+ this.maximum = options.maxChannels ?? 4;
625
+ check(Number.isInteger(this.maximum) &&
626
+ this.maximum > 0 &&
627
+ this.maximum <= 32, 'Invalid data channel limit');
628
+ for (const timeout of [
629
+ options.openTimeoutMs ?? 15_000,
630
+ options.idleTimeoutMs ?? 60_000,
631
+ ]) {
632
+ check(Number.isSafeInteger(timeout) &&
633
+ timeout > 0 &&
634
+ timeout <= 300_000, 'Invalid data timeout');
635
+ }
636
+ if (options.endpoint)
637
+ this.endpoint = endpointUrl(options.endpoint);
638
+ }
639
+ hasActivity() {
640
+ return this.pendingOpens.size > 0 || this.channels.size > 0;
641
+ }
642
+ supports(purpose, direction) {
643
+ return (!this.stopped &&
644
+ validPurpose(purpose) &&
645
+ this.options.supportsPurpose(purpose) &&
646
+ ['host-to-remote', 'remote-to-host', 'duplex'].includes(direction));
647
+ }
648
+ admit() {
649
+ check(!this.stopped, 'Data session closed');
650
+ check(this.channels.size + this.pendingOpens.size < this.maximum, 'Data channel limit exceeded');
651
+ }
652
+ validateLink(raw, open) {
653
+ const link = raw;
654
+ check(link &&
655
+ link.version === 1 &&
656
+ typeof link.channel === 'string' &&
657
+ /^[a-f0-9]{48}$/.test(link.channel) &&
658
+ typeof link.token === 'string' &&
659
+ /^[a-f0-9]{64}$/.test(link.token) &&
660
+ link.binding === this.options.binding &&
661
+ Number.isSafeInteger(link.expiresInMs) &&
662
+ link.expiresInMs > 0 &&
663
+ link.expiresInMs <= (this.options.openTimeoutMs ?? 15_000) &&
664
+ sameOpen(validOpen(link.open, this.options.supportsPurpose), open), 'Invalid data link');
665
+ this.options.validateEndpoint(endpointUrl(link.endpoint));
666
+ return { ...link, open };
667
+ }
668
+ create(open, supplied) {
669
+ open = validOpen(open, this.options.supportsPurpose);
670
+ this.admit();
671
+ check(supplied || this.endpoint, 'No local HTTP data endpoint');
672
+ const link = supplied ?? {
673
+ version: 1,
674
+ endpoint: this.endpoint.href,
675
+ channel: randomBytes(24).toString('hex'),
676
+ token: randomBytes(32).toString('hex'),
677
+ binding: this.options.binding,
678
+ open,
679
+ expiresInMs: this.options.openTimeoutMs ?? 15_000,
680
+ };
681
+ check(!this.channels.has(link.channel), 'Data channel already accepted');
682
+ if (supplied) {
683
+ const now = performance.now();
684
+ for (const [id, expires] of this.usedLinks) {
685
+ if (expires <= now)
686
+ this.usedLinks.delete(id);
687
+ }
688
+ check(!this.usedLinks.has(link.channel), 'Data link already used');
689
+ check(this.usedLinks.size < 256, 'Pending data link replay limit');
690
+ this.usedLinks.set(link.channel, now + link.expiresInMs);
691
+ }
692
+ const channel = new HttpChannel(link, this.options, !supplied, () => this.channels.delete(link.channel));
693
+ this.channels.set(link.channel, channel);
694
+ return channel;
695
+ }
696
+ async openChannel(open, receiver, link, signal) {
697
+ check(this.options.role === 'host', 'Only the host opens data channels');
698
+ open = validOpen(open, this.options.supportsPurpose);
699
+ let channel;
700
+ try {
701
+ if (link) {
702
+ channel = this.create(open, this.validateLink(link, open));
703
+ channel.setReceiver(receiver);
704
+ channel.attachSignal(signal);
705
+ await channel.connect();
706
+ }
707
+ else if (this.endpoint) {
708
+ check(this.options.requestOpen, 'Missing data/open control client');
709
+ channel = this.create(open);
710
+ channel.setReceiver(receiver);
711
+ channel.attachSignal(signal);
712
+ await channel.wait(this.options.requestOpen({
713
+ open,
714
+ binding: this.options.binding,
715
+ link: channel.link,
716
+ }, channel.abort.signal));
717
+ }
718
+ else {
719
+ check(this.options.requestOpen, 'Missing data/open control client');
720
+ // Reserve a bounded slot BEFORE awaiting RPC, including stalled metadata calls.
721
+ this.admit();
722
+ const pending = new AbortController();
723
+ const deadline = setTimeout(() => pending.abort(), this.options.openTimeoutMs ?? 15_000);
724
+ deadline.unref?.();
725
+ const combined = AbortSignal.any([
726
+ pending.signal,
727
+ ...(signal ? [signal] : []),
728
+ ]);
729
+ this.pendingOpens.add(pending);
730
+ let result;
731
+ try {
732
+ result = await new Promise((resolve, reject) => {
733
+ const abort = () => reject(new Error('Data/open cancelled or timed out'));
734
+ combined.addEventListener('abort', abort, {
735
+ once: true,
736
+ });
737
+ if (combined.aborted)
738
+ abort();
739
+ else
740
+ void this.options.requestOpen({ open, binding: this.options.binding }, combined).then(resolve, reject);
741
+ });
742
+ }
743
+ finally {
744
+ clearTimeout(deadline);
745
+ pending.abort();
746
+ this.pendingOpens.delete(pending);
747
+ }
748
+ check(result.link, 'Data/open did not return a link');
749
+ channel = this.create(open, this.validateLink(result.link, open));
750
+ channel.setReceiver(receiver);
751
+ channel.attachSignal(signal);
752
+ await channel.connect();
753
+ }
754
+ await channel.waitConnected();
755
+ channel.activate();
756
+ return channel;
757
+ }
758
+ catch (error) {
759
+ if (channel)
760
+ channel.fail(errorOf(error));
761
+ else
762
+ receiver.close(errorOf(error));
763
+ throw error;
764
+ }
765
+ }
766
+ async acceptOpen(raw) {
767
+ check(this.options.role === 'remote' && this.options.accept, 'Data accept is unavailable');
768
+ const params = raw;
769
+ check(params && params.binding === this.options.binding, 'Data/open connection binding mismatch');
770
+ const open = validOpen(params.open, this.options.supportsPurpose);
771
+ const link = params.link
772
+ ? this.validateLink(params.link, open)
773
+ : undefined;
774
+ const channel = this.create(open, link);
775
+ try {
776
+ // A reverse descriptor must first claim live, single-use HTTP legs.
777
+ // Replaying a completed/expired descriptor must not re-enter business accept.
778
+ if (link)
779
+ await channel.connect();
780
+ const accepting = Promise.resolve().then(() => this.options.accept(open, channel));
781
+ const installed = accepting.then((receiver) => channel.setReceiver(receiver));
782
+ await channel.wait(installed);
783
+ channel.activate();
784
+ return link ? {} : { link: channel.link };
785
+ }
786
+ catch (error) {
787
+ channel.fail(errorOf(error));
788
+ throw error;
789
+ }
790
+ }
791
+ async handleRequest(request) {
792
+ if (!this.endpoint)
793
+ return undefined;
794
+ const url = new URL(request.url);
795
+ const prefix = this.endpoint.pathname + '/';
796
+ if (!url.pathname.startsWith(prefix))
797
+ return undefined;
798
+ if (request.headers.has('origin') || url.search || url.hash)
799
+ return new Response(null, { status: 403, headers: HEADERS });
800
+ const match = /^([a-f0-9]{48})\/(get|put|ack|ready|close)$/.exec(url.pathname.slice(prefix.length));
801
+ const channel = match && this.channels.get(match[1]);
802
+ const token = Buffer.from(request.headers.get('authorization') ?? '');
803
+ const expected = Buffer.from(channel ? `Bearer ${channel.link.token}` : '');
804
+ if (!channel ||
805
+ !channel.serving ||
806
+ token.length !== expected.length ||
807
+ !timingSafeEqual(token, expected) ||
808
+ request.headers.get('x-data-binding') !== this.options.binding) {
809
+ return new Response(null, { status: 401, headers: HEADERS });
810
+ }
811
+ const leg = match[2];
812
+ if ((leg === 'ready' && request.method !== 'POST') ||
813
+ (leg === 'get' && request.method !== 'GET') ||
814
+ (leg === 'put' && request.method !== 'PUT') ||
815
+ (leg === 'ack' && !['GET', 'POST'].includes(request.method)) ||
816
+ (leg === 'close' && request.method !== 'DELETE')) {
817
+ return new Response(null, { status: 405, headers: HEADERS });
818
+ }
819
+ // Expiry gates initial leg admission, not a healthy long-running transfer.
820
+ if (leg !== 'close' &&
821
+ !(leg === 'ack' && request.method === 'POST') &&
822
+ channel.ticketExpired()) {
823
+ channel.close('Data ticket expired');
824
+ return new Response(null, { status: 410, headers: HEADERS });
825
+ }
826
+ const cancel = () => channel.close('HTTP data request aborted');
827
+ request.signal.addEventListener('abort', cancel, { once: true });
828
+ if (!(leg === 'close' ||
829
+ leg === 'ready' ||
830
+ (leg === 'ack' && request.method === 'POST'))) {
831
+ void channel.closed
832
+ .finally(() => request.signal.removeEventListener('abort', cancel))
833
+ .catch(() => { });
834
+ }
835
+ if (request.signal.aborted)
836
+ cancel();
837
+ const transient = leg === 'close' ||
838
+ leg === 'ready' ||
839
+ (leg === 'ack' && request.method === 'POST');
840
+ try {
841
+ if (leg === 'close') {
842
+ channel.close();
843
+ return new Response(null, { status: 204, headers: HEADERS });
844
+ }
845
+ if (leg === 'ready') {
846
+ check(!request.headers.has('transfer-encoding') &&
847
+ (!request.headers.has('content-length') ||
848
+ request.headers.get('content-length') === '0'), 'Data ready must have no body');
849
+ channel.claim(leg);
850
+ await channel.waitConnected();
851
+ return new Response(null, { status: 204, headers: HEADERS });
852
+ }
853
+ if (leg === 'ack' && request.method === 'POST') {
854
+ channel.beginAck();
855
+ const text = await readSmallBody(request, channel);
856
+ channel.applyAck(JSON.parse(text));
857
+ return new Response(null, { status: 204, headers: HEADERS });
858
+ }
859
+ channel.claim(leg);
860
+ if (leg === 'get')
861
+ return new Response(channel.outgoing, {
862
+ headers: {
863
+ ...HEADERS,
864
+ 'Content-Type': 'application/octet-stream',
865
+ },
866
+ });
867
+ if (leg === 'ack')
868
+ return new Response(channel.acknowledgments, {
869
+ headers: {
870
+ ...HEADERS,
871
+ 'Content-Type': 'application/x-ndjson',
872
+ },
873
+ });
874
+ check(!request.headers.get('content-encoding') ||
875
+ request.headers.get('content-encoding') === 'identity', 'Encoded data body is forbidden');
876
+ await channel.receive(request.body);
877
+ return new Response(null, { status: 204, headers: HEADERS });
878
+ }
879
+ catch (error) {
880
+ channel.fail(errorOf(error));
881
+ return new Response(null, { status: 409, headers: HEADERS });
882
+ }
883
+ finally {
884
+ if (transient) {
885
+ if (leg === 'ack')
886
+ channel.endAck();
887
+ request.signal.removeEventListener('abort', cancel);
888
+ }
889
+ }
890
+ }
891
+ close(reason = 'Data connection closed') {
892
+ if (this.stopped)
893
+ return;
894
+ this.stopped = true;
895
+ for (const pending of this.pendingOpens)
896
+ pending.abort();
897
+ this.pendingOpens.clear();
898
+ for (const channel of this.channels.values())
899
+ channel.close(reason);
900
+ this.channels.clear();
901
+ this.usedLinks.clear();
902
+ }
903
+ }
904
+ async function readSmallBody(request, channel) {
905
+ const reader = request.body?.getReader();
906
+ check(reader, 'Missing acknowledgment');
907
+ const chunks = [];
908
+ let size = 0;
909
+ try {
910
+ while (true) {
911
+ const next = await channel.wait(reader.read());
912
+ if (next.done)
913
+ break;
914
+ size += next.value.byteLength;
915
+ check(size <= 256, 'Acknowledgment metadata limit');
916
+ chunks.push(next.value);
917
+ }
918
+ return Buffer.concat(chunks).toString('utf8');
919
+ }
920
+ finally {
921
+ void reader.cancel().catch(() => { });
922
+ reader.releaseLock();
923
+ }
924
+ }
925
+ //# sourceMappingURL=HttpDataSession.js.map