@floegence/flowersec-core 2.5.3 → 3.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 (119) hide show
  1. package/README.md +46 -18
  2. package/dist/browser/connectSessionV3.d.ts +13 -0
  3. package/dist/browser/connectSessionV3.js +75 -0
  4. package/dist/browser/index.d.ts +3 -2
  5. package/dist/browser/index.js +2 -1
  6. package/dist/browser/v2.d.ts +3 -0
  7. package/dist/browser/v2.js +2 -0
  8. package/dist/facade.d.ts +15 -8
  9. package/dist/facade.js +5 -4
  10. package/dist/framing/jsonframe.d.ts +1 -1
  11. package/dist/framing/jsonframe.js +3 -1
  12. package/dist/interop/proxyServerPeer.js +1 -1
  13. package/dist/interop/serverParityPeer.js +8 -1
  14. package/dist/node/acceptor.d.ts +10 -0
  15. package/dist/node/acceptor.js +53 -7
  16. package/dist/node/acceptorV3.d.ts +57 -0
  17. package/dist/node/acceptorV3.js +227 -0
  18. package/dist/node/connectSessionV3.d.ts +21 -0
  19. package/dist/node/connectSessionV3.js +73 -0
  20. package/dist/node/index.d.ts +15 -10
  21. package/dist/node/index.js +6 -5
  22. package/dist/node/nativeTransportAddon.d.ts +39 -1
  23. package/dist/node/nativeTransportAddon.js +29 -5
  24. package/dist/node/rawQuicAdapterV3.d.ts +10 -0
  25. package/dist/node/rawQuicAdapterV3.js +76 -0
  26. package/dist/node/rawQuicServerV3.d.ts +24 -0
  27. package/dist/node/rawQuicServerV3.js +34 -0
  28. package/dist/node/runtimeAuthorizationV3.d.ts +29 -0
  29. package/dist/node/runtimeAuthorizationV3.js +126 -0
  30. package/dist/node/tunnelRuntimeV3.d.ts +57 -0
  31. package/dist/node/tunnelRuntimeV3.js +857 -0
  32. package/dist/node/v2.d.ts +11 -0
  33. package/dist/node/v2.js +6 -0
  34. package/dist/node/webSocketServerV3.d.ts +27 -0
  35. package/dist/node/webSocketServerV3.js +219 -0
  36. package/dist/proxy/runtime.js +39 -6
  37. package/dist/proxy/serviceWorker.d.ts +2 -0
  38. package/dist/proxy/serviceWorker.js +63 -7
  39. package/dist/proxy/windowBridge.d.ts +1 -0
  40. package/dist/proxy/windowBridge.js +93 -33
  41. package/dist/proxy/wsPatch.d.ts +1 -0
  42. package/dist/proxy/wsPatch.js +39 -6
  43. package/dist/public/streamHandlers.d.ts +3 -0
  44. package/dist/public/streamHandlers.js +6 -1
  45. package/dist/rpc/client.js +9 -7
  46. package/dist/rpc/server.js +44 -20
  47. package/dist/rpc/validate.d.ts +1 -0
  48. package/dist/rpc/validate.js +18 -6
  49. package/dist/v2/index.d.ts +14 -0
  50. package/dist/v2/index.js +7 -0
  51. package/dist/v2/protocol.js +23 -32
  52. package/dist/v2/publicSession.js +4 -0
  53. package/dist/v2/session.d.ts +5 -0
  54. package/dist/v2/session.js +94 -35
  55. package/dist/v3/admissionError.d.ts +4 -0
  56. package/dist/v3/admissionError.js +8 -0
  57. package/dist/v3/artifact.d.ts +145 -0
  58. package/dist/v3/artifact.js +1162 -0
  59. package/dist/v3/artifactLease.d.ts +17 -0
  60. package/dist/v3/artifactLease.js +109 -0
  61. package/dist/v3/browserRuntime.d.ts +37 -0
  62. package/dist/v3/browserRuntime.js +197 -0
  63. package/dist/v3/browserSessionRuntime.d.ts +2 -0
  64. package/dist/v3/browserSessionRuntime.js +21 -0
  65. package/dist/v3/capability.d.ts +32 -0
  66. package/dist/v3/capability.js +345 -0
  67. package/dist/v3/carrier.d.ts +84 -0
  68. package/dist/v3/carrier.js +411 -0
  69. package/dist/v3/connectionController.d.ts +87 -0
  70. package/dist/v3/connectionController.js +805 -0
  71. package/dist/v3/contract.d.ts +65 -0
  72. package/dist/v3/contract.js +3 -0
  73. package/dist/v3/controller.d.ts +43 -0
  74. package/dist/v3/controller.js +303 -0
  75. package/dist/v3/handshake.d.ts +81 -0
  76. package/dist/v3/handshake.js +471 -0
  77. package/dist/v3/jcs.d.ts +6 -0
  78. package/dist/v3/jcs.js +69 -0
  79. package/dist/v3/jsonPreflight.d.ts +1 -0
  80. package/dist/v3/jsonPreflight.js +251 -0
  81. package/dist/v3/nodeRuntime.d.ts +40 -0
  82. package/dist/v3/nodeRuntime.js +315 -0
  83. package/dist/v3/nodeSessionRuntime.d.ts +2 -0
  84. package/dist/v3/nodeSessionRuntime.js +13 -0
  85. package/dist/v3/protocol.d.ts +99 -0
  86. package/dist/v3/protocol.js +773 -0
  87. package/dist/v3/publicApi.d.ts +12 -0
  88. package/dist/v3/publicApi.js +46 -0
  89. package/dist/v3/publicSession.d.ts +1 -0
  90. package/dist/v3/publicSession.js +281 -0
  91. package/dist/v3/retryDisposition.d.ts +16 -0
  92. package/dist/v3/retryDisposition.js +30 -0
  93. package/dist/v3/runtimeAdapters.d.ts +9 -0
  94. package/dist/v3/runtimeAdapters.js +109 -0
  95. package/dist/v3/security.d.ts +34 -0
  96. package/dist/v3/security.js +100 -0
  97. package/dist/v3/serverAdmission.d.ts +38 -0
  98. package/dist/v3/serverAdmission.js +223 -0
  99. package/dist/v3/session.d.ts +318 -0
  100. package/dist/v3/session.js +2289 -0
  101. package/dist/v3/sessionConnector.d.ts +37 -0
  102. package/dist/v3/sessionConnector.js +440 -0
  103. package/dist/v3/streamLifetimeLedger.d.ts +31 -0
  104. package/dist/v3/streamLifetimeLedger.js +131 -0
  105. package/dist/v3/streamMetadata.d.ts +1 -0
  106. package/dist/v3/streamMetadata.js +7 -0
  107. package/dist/v3/transportConstants.d.ts +56 -0
  108. package/dist/v3/transportConstants.js +59 -0
  109. package/dist/v3/unreliableMessage.d.ts +44 -0
  110. package/dist/v3/unreliableMessage.js +284 -0
  111. package/dist/v3/webSocketCarrier.d.ts +31 -0
  112. package/dist/v3/webSocketCarrier.js +370 -0
  113. package/dist/v3/webTransportCarrier.d.ts +27 -0
  114. package/dist/v3/webTransportCarrier.js +534 -0
  115. package/dist/yamux/stream.d.ts +3 -0
  116. package/dist/yamux/stream.js +17 -4
  117. package/package.json +4 -4
  118. package/sbom/cyclonedx.json +6 -6
  119. package/sbom/spdx.json +11 -11
@@ -39,12 +39,17 @@ export class MessagePortByteStream {
39
39
  if (this.terminalError !== undefined)
40
40
  throw this.terminalError;
41
41
  return await new Promise((resolve, reject) => {
42
- const waiter = { resolve, reject };
42
+ const cleanup = () => options.signal?.removeEventListener("abort", onAbort);
43
+ const waiter = {
44
+ resolve: (value) => { cleanup(); resolve(value); },
45
+ reject: (error) => { cleanup(); reject(error); },
46
+ };
43
47
  const onAbort = () => {
44
48
  const index = this.readWaiters.indexOf(waiter);
45
- if (index >= 0)
46
- this.readWaiters.splice(index, 1);
47
- reject(new SessionError("canceled"));
49
+ if (index < 0)
50
+ return;
51
+ this.readWaiters.splice(index, 1);
52
+ waiter.reject(new SessionError("canceled"));
48
53
  };
49
54
  options.signal?.addEventListener("abort", onAbort, { once: true });
50
55
  this.readWaiters.push(waiter);
@@ -66,17 +71,19 @@ export class MessagePortByteStream {
66
71
  const copy = data.slice();
67
72
  this.buffered += copy.length;
68
73
  await new Promise((resolve, reject) => {
74
+ const onAbort = () => this.settleWrite(id, new SessionError("canceled"));
69
75
  this.writeWaiters.set(id, {
70
- resolve: () => { this.buffered -= copy.length; resolve(); },
71
- reject: (error) => { this.buffered -= copy.length; reject(error); },
76
+ bytes: copy.length,
77
+ ...(options.signal === undefined ? {} : { signal: options.signal, onAbort }),
78
+ resolve,
79
+ reject,
72
80
  });
81
+ options.signal?.addEventListener("abort", onAbort, { once: true });
73
82
  try {
74
83
  this.port.postMessage({ type: "chunk", id, data: copy.buffer }, [copy.buffer]);
75
84
  }
76
85
  catch {
77
- this.writeWaiters.delete(id);
78
- this.buffered -= copy.length;
79
- reject(new SessionError("operation_failed"));
86
+ this.settleWrite(id, new SessionError("operation_failed"));
80
87
  }
81
88
  });
82
89
  return data.length;
@@ -130,11 +137,7 @@ export class MessagePortByteStream {
130
137
  return;
131
138
  }
132
139
  if (message.type === "ack" && Number.isSafeInteger(message.id)) {
133
- const waiter = this.writeWaiters.get(message.id);
134
- if (waiter !== undefined) {
135
- this.writeWaiters.delete(message.id);
136
- waiter.resolve();
137
- }
140
+ this.settleWrite(message.id);
138
141
  return;
139
142
  }
140
143
  if (message.type === "end") {
@@ -159,9 +162,8 @@ export class MessagePortByteStream {
159
162
  this.readBuffered = 0;
160
163
  for (const waiter of this.readWaiters.splice(0))
161
164
  waiter.resolve(null);
162
- for (const waiter of this.writeWaiters.values())
163
- waiter.reject(new SessionError("closed"));
164
- this.writeWaiters.clear();
165
+ for (const id of [...this.writeWaiters.keys()])
166
+ this.settleWrite(id, new SessionError("closed"));
165
167
  this.port.close();
166
168
  }
167
169
  fail(error) {
@@ -173,11 +175,22 @@ export class MessagePortByteStream {
173
175
  this.readBuffered = 0;
174
176
  for (const waiter of this.readWaiters.splice(0))
175
177
  waiter.reject(error);
176
- for (const waiter of this.writeWaiters.values())
177
- waiter.reject(error);
178
- this.writeWaiters.clear();
178
+ for (const id of [...this.writeWaiters.keys()])
179
+ this.settleWrite(id, error);
179
180
  this.port.close();
180
181
  }
182
+ settleWrite(id, error) {
183
+ const waiter = this.writeWaiters.get(id);
184
+ if (waiter === undefined)
185
+ return;
186
+ this.writeWaiters.delete(id);
187
+ waiter.signal?.removeEventListener("abort", waiter.onAbort);
188
+ this.buffered = Math.max(0, this.buffered - waiter.bytes);
189
+ if (error === undefined)
190
+ waiter.resolve();
191
+ else
192
+ waiter.reject(error);
193
+ }
181
194
  }
182
195
  function bridgeLimits(maxWsFrameBytes, maxWsBufferedAmountBytes) {
183
196
  const wsFrame = maxWsFrameBytes ?? SDK_DEFAULTS.proxy.maxWsFrameBytes;
@@ -279,33 +292,65 @@ export function registerProxyAppWindow(options) {
279
292
  dispose: () => { disposed = true; },
280
293
  });
281
294
  }
282
- async function bridgeStreams(runtimeStream, port) {
295
+ async function bridgeStreams(runtimeStream, port, signal) {
283
296
  const bridge = new MessagePortByteStream(port);
297
+ const controller = new AbortController();
298
+ let resetTask;
299
+ const resetBoth = () => {
300
+ resetTask ??= Promise.allSettled([
301
+ Promise.resolve().then(async () => await runtimeStream.reset()),
302
+ Promise.resolve().then(async () => await bridge.reset()),
303
+ ]);
304
+ return resetTask;
305
+ };
306
+ const abort = () => {
307
+ controller.abort(signal?.reason ?? new SessionError("canceled"));
308
+ void resetBoth();
309
+ };
310
+ if (signal?.aborted === true)
311
+ abort();
312
+ else
313
+ signal?.addEventListener("abort", abort, { once: true });
284
314
  const left = (async () => {
285
315
  while (true) {
286
- const chunk = await runtimeStream.read();
316
+ const chunk = await runtimeStream.read({ signal: controller.signal });
287
317
  if (chunk === null) {
288
318
  await bridge.closeWrite();
289
319
  return;
290
320
  }
291
- await bridge.write(chunk);
321
+ await bridge.write(chunk, { signal: controller.signal });
292
322
  }
293
- })();
323
+ })().catch((error) => { controller.abort(error); void resetBoth(); throw error; });
294
324
  const right = (async () => {
295
325
  while (true) {
296
- const chunk = await bridge.read();
326
+ const chunk = await bridge.read({ signal: controller.signal });
297
327
  if (chunk === null) {
298
328
  await runtimeStream.closeWrite();
299
329
  return;
300
330
  }
301
331
  let offset = 0;
302
332
  while (offset < chunk.length)
303
- offset += await runtimeStream.write(chunk.subarray(offset));
333
+ offset += await runtimeStream.write(chunk.subarray(offset), { signal: controller.signal });
304
334
  }
305
- })();
306
- await Promise.all([left, right]);
307
- await runtimeStream.close();
308
- await bridge.close();
335
+ })().catch((error) => { controller.abort(error); void resetBoth(); throw error; });
336
+ try {
337
+ const settled = await Promise.allSettled([left, right]);
338
+ const failure = settled.find((result) => result.status === "rejected");
339
+ if (failure !== undefined) {
340
+ await resetBoth();
341
+ throw failure.reason;
342
+ }
343
+ try {
344
+ await Promise.all([runtimeStream.close(), bridge.close()]);
345
+ }
346
+ catch (error) {
347
+ await resetBoth();
348
+ throw error;
349
+ }
350
+ }
351
+ finally {
352
+ signal?.removeEventListener("abort", abort);
353
+ }
309
354
  }
310
355
  export function registerProxyControllerWindow(options) {
311
356
  const target = options.targetWindow ?? globalThis.window;
@@ -315,6 +360,7 @@ export function registerProxyControllerWindow(options) {
315
360
  }
316
361
  const nonce = capability(options.capabilityNonce);
317
362
  let disposed = false;
363
+ const active = new Set();
318
364
  const onMessage = (event) => {
319
365
  if (disposed || !allowed.has(event.origin) || (options.expectedSource !== undefined && event.source !== options.expectedSource))
320
366
  return;
@@ -331,6 +377,7 @@ export function registerProxyControllerWindow(options) {
331
377
  void (async () => {
332
378
  let canceled = false;
333
379
  const openController = new AbortController();
380
+ active.add(openController);
334
381
  port.onmessage = (message) => {
335
382
  if (message.data?.type === "reset" ||
336
383
  message.data?.type === "close") {
@@ -349,17 +396,30 @@ export function registerProxyControllerWindow(options) {
349
396
  return;
350
397
  }
351
398
  port.postMessage({ type: WEBSOCKET_ACK_MESSAGE, ok: true, protocol: opened.protocol });
352
- await bridgeStreams(opened.stream, port);
399
+ await bridgeStreams(opened.stream, port, openController.signal);
353
400
  }
354
401
  catch {
355
- port.postMessage({ type: WEBSOCKET_ACK_MESSAGE, ok: false });
402
+ try {
403
+ port.postMessage({ type: WEBSOCKET_ACK_MESSAGE, ok: false });
404
+ }
405
+ catch { /* Port is already closed. */ }
356
406
  port.close();
357
407
  }
408
+ finally {
409
+ active.delete(openController);
410
+ }
358
411
  })();
359
412
  }
360
413
  };
361
414
  target.addEventListener("message", onMessage);
362
- return Object.freeze({ dispose: () => { disposed = true; target.removeEventListener("message", onMessage); } });
415
+ return Object.freeze({
416
+ dispose: () => {
417
+ disposed = true;
418
+ target.removeEventListener("message", onMessage);
419
+ for (const controller of active)
420
+ controller.abort(new SessionError("closed"));
421
+ },
422
+ });
363
423
  }
364
424
  export async function registerProxyAppWindowWithServiceWorkerControl(options) {
365
425
  await registerServiceWorkerAndEnsureControl({
@@ -4,6 +4,7 @@ export type WebSocketPatchOptions = Readonly<{
4
4
  shouldProxy?: (url: URL) => boolean;
5
5
  maxWsFrameBytes?: number;
6
6
  maxWsBufferedAmountBytes?: number;
7
+ closeHandshakeTimeoutMs?: number;
7
8
  }>;
8
9
  export declare function installWebSocketPatch(options: WebSocketPatchOptions): Readonly<{
9
10
  uninstall(): void;
@@ -64,6 +64,7 @@ export function installWebSocketPatch(options) {
64
64
  const runtimeLimits = options.runtime.limits;
65
65
  const maxFrameBytes = limit("maxWsFrameBytes", options.maxWsFrameBytes, runtimeLimits.maxWsFrameBytes ?? 1024 * 1024);
66
66
  const maxBufferedBytes = limit("maxWsBufferedAmountBytes", options.maxWsBufferedAmountBytes, runtimeLimits.maxWsBufferedAmountBytes ?? 4 * 1024 * 1024);
67
+ const closeHandshakeTimeoutMs = limit("closeHandshakeTimeoutMs", options.closeHandshakeTimeoutMs, 5_000);
67
68
  const shouldProxy = options.shouldProxy ?? ((url) => {
68
69
  const location = globalThis.location;
69
70
  if (location?.hostname === undefined || location.hostname === "")
@@ -95,6 +96,7 @@ export function installWebSocketPatch(options) {
95
96
  abort = new AbortController();
96
97
  stream;
97
98
  writes = Promise.resolve();
99
+ closeTimer;
98
100
  constructor(input, protocols) {
99
101
  const url = new URL(String(input), globalThis.location?.href);
100
102
  if (!shouldProxy(url))
@@ -152,12 +154,17 @@ export function installWebSocketPatch(options) {
152
154
  const reasonBytes = encoder.encode(reason);
153
155
  if (reasonBytes.length > 123)
154
156
  throw new DOMException("WebSocket close reason is too long", "SyntaxError");
157
+ if (this.readyState === ProxyWebSocket.CONNECTING || this.stream === undefined) {
158
+ this.fail();
159
+ return;
160
+ }
155
161
  this.readyState = ProxyWebSocket.CLOSING;
156
162
  const payload = code === undefined ? new Uint8Array() : new Uint8Array([...u16be(code), ...reasonBytes]);
157
- this.writes = this.writes.then(async () => {
158
- if (this.stream !== undefined)
159
- await writeFrame(this.stream, 8, payload, maxFrameBytes);
160
- }).catch(() => undefined).finally(() => this.abort.abort());
163
+ const stream = this.stream;
164
+ this.closeTimer = setTimeout(() => this.fail(), closeHandshakeTimeoutMs);
165
+ this.writes = this.writes
166
+ .then(async () => await writeFrame(stream, 8, payload, maxFrameBytes))
167
+ .catch(() => this.fail());
161
168
  }
162
169
  async connect(url, protocols) {
163
170
  try {
@@ -183,10 +190,19 @@ export function installWebSocketPatch(options) {
183
190
  this.writes = this.writes.then(async () => await writeFrame(stream, 10, frame.payload, maxFrameBytes)).catch(() => this.fail());
184
191
  }
185
192
  else if (frame.opcode === 8) {
193
+ if (frame.payload.length === 1)
194
+ throw new Error("invalid WebSocket close frame");
186
195
  const code = frame.payload.length >= 2 ? readU16(frame.payload) : 1000;
187
196
  const reason = frame.payload.length > 2 ? decoder.decode(frame.payload.subarray(2)) : "";
188
- this.readyState = ProxyWebSocket.CLOSED;
189
- this.emit("close", new CloseEvent("close", { code, reason, wasClean: true }));
197
+ const peerInitiated = this.readyState === ProxyWebSocket.OPEN;
198
+ this.readyState = ProxyWebSocket.CLOSING;
199
+ if (this.closeTimer !== undefined)
200
+ clearTimeout(this.closeTimer);
201
+ if (peerInitiated) {
202
+ this.writes = this.writes.then(async () => await writeFrame(stream, 8, frame.payload, maxFrameBytes));
203
+ }
204
+ await this.writes;
205
+ this.completeClose(code, reason);
190
206
  return;
191
207
  }
192
208
  else if (frame.opcode === 1) {
@@ -211,12 +227,29 @@ export function installWebSocketPatch(options) {
211
227
  fail() {
212
228
  if (this.readyState === ProxyWebSocket.CLOSED)
213
229
  return;
230
+ if (this.closeTimer !== undefined)
231
+ clearTimeout(this.closeTimer);
214
232
  this.readyState = ProxyWebSocket.CLOSED;
215
233
  this.bufferedAmount = 0;
216
234
  this.emit("error", new Event("error"));
217
235
  this.emit("close", new CloseEvent("close", { code: 1006, reason: "proxy WebSocket failed", wasClean: false }));
236
+ const stream = this.stream;
237
+ this.stream = undefined;
238
+ this.abort.abort();
239
+ void stream?.reset().catch(() => undefined);
240
+ }
241
+ completeClose(code, reason) {
242
+ if (this.readyState === ProxyWebSocket.CLOSED)
243
+ return;
244
+ if (this.closeTimer !== undefined)
245
+ clearTimeout(this.closeTimer);
246
+ this.readyState = ProxyWebSocket.CLOSED;
247
+ this.bufferedAmount = 0;
248
+ this.emit("close", new CloseEvent("close", { code, reason, wasClean: true }));
249
+ const stream = this.stream;
218
250
  this.stream = undefined;
219
251
  this.abort.abort();
252
+ void stream?.close().catch(() => stream.reset().catch(() => undefined));
220
253
  }
221
254
  }
222
255
  globalThis.WebSocket = ProxyWebSocket;
@@ -18,3 +18,6 @@ export declare class StreamHandlers {
18
18
  handleStream(kind: string, handler: StreamHandler): void;
19
19
  serve(session: Session, options?: OperationOptions): Promise<void>;
20
20
  }
21
+ /** Explicit-compatibility application-stream handlers. */
22
+ export declare class LegacyStreamHandlers extends StreamHandlers {
23
+ }
@@ -11,6 +11,7 @@ export class HandlerRegistrationError extends Error {
11
11
  }
12
12
  }
13
13
  const streamHandlerStates = new WeakMap();
14
+ const RESERVED_RPC_KINDS = new Set(["flowersec.rpc.v2", "flowersec.rpc.v3"]);
14
15
  /** Carrier-neutral application-stream handlers for any established Session. */
15
16
  export class StreamHandlers {
16
17
  constructor(options = {}) {
@@ -22,6 +23,7 @@ export class StreamHandlers {
22
23
  }
23
24
  streamHandlerStates.set(this, {
24
25
  maxConcurrentStreams: maximum,
26
+ reservedRPCKinds: RESERVED_RPC_KINDS,
25
27
  streams: new Map(),
26
28
  frozen: false,
27
29
  });
@@ -36,6 +38,9 @@ export class StreamHandlers {
36
38
  await serveFrozenStreamHandlers(freezeStreamHandlers(this), session, options, async () => await session.close());
37
39
  }
38
40
  }
41
+ /** Explicit-compatibility application-stream handlers. */
42
+ export class LegacyStreamHandlers extends StreamHandlers {
43
+ }
39
44
  function mutableStreamHandlerState(handlers) {
40
45
  const state = streamHandlerStates.get(handlers);
41
46
  if (state === undefined)
@@ -50,7 +55,7 @@ function registerIntoState(state, entries) {
50
55
  const pending = new Set();
51
56
  for (const [kind, handler] of entries) {
52
57
  if (!validApplicationStreamKind(kind) ||
53
- kind === "flowersec.rpc.v2" ||
58
+ state.reservedRPCKinds.has(kind) ||
54
59
  typeof handler !== "function") {
55
60
  throw new HandlerRegistrationError("invalid_handler");
56
61
  }
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
2
- import { assertRpcEnvelope } from "./validate.js";
2
+ import { assertRpcEnvelope, assertRpcTypeId } from "./validate.js";
3
3
  // Guard against precision loss when encoding request IDs as numbers.
4
4
  const MAX_SAFE_REQUEST_ID = BigInt(Number.MAX_SAFE_INTEGER);
5
5
  // RpcClient sends request/response envelopes and dispatches notifications.
@@ -25,12 +25,13 @@ export class RpcClient {
25
25
  async call(typeId, payload, signal) {
26
26
  if (this.closed)
27
27
  throw new Error("rpc client closed");
28
+ const validatedTypeId = assertRpcTypeId(typeId);
28
29
  if (this.nextId > MAX_SAFE_REQUEST_ID)
29
30
  throw new Error("request id overflow");
30
31
  const requestId = this.nextId;
31
32
  this.nextId += 1n;
32
33
  const env = {
33
- type_id: typeId >>> 0,
34
+ type_id: validatedTypeId,
34
35
  request_id: Number(requestId),
35
36
  response_to: 0,
36
37
  payload
@@ -39,7 +40,7 @@ export class RpcClient {
39
40
  this.pending.set(requestId, { resolve, reject });
40
41
  });
41
42
  try {
42
- await writeJsonFrame(this.write, env);
43
+ await writeJsonFrame(this.write, env, DEFAULT_MAX_JSON_FRAME_BYTES);
43
44
  }
44
45
  catch (e) {
45
46
  this.pending.delete(requestId);
@@ -71,7 +72,7 @@ export class RpcClient {
71
72
  }
72
73
  // onNotify registers a handler for incoming notifications.
73
74
  onNotify(typeId, handler) {
74
- const tid = typeId >>> 0;
75
+ const tid = assertRpcTypeId(typeId);
75
76
  const set = this.notifyHandlers.get(tid) ?? new Set();
76
77
  set.add(handler);
77
78
  this.notifyHandlers.set(tid, set);
@@ -86,13 +87,14 @@ export class RpcClient {
86
87
  async notify(typeId, payload) {
87
88
  if (this.closed)
88
89
  throw new Error("rpc client closed");
90
+ const validatedTypeId = assertRpcTypeId(typeId);
89
91
  const env = {
90
- type_id: typeId >>> 0,
92
+ type_id: validatedTypeId,
91
93
  request_id: 0,
92
94
  response_to: 0,
93
95
  payload
94
96
  };
95
- await writeJsonFrame(this.write, env);
97
+ await writeJsonFrame(this.write, env, DEFAULT_MAX_JSON_FRAME_BYTES);
96
98
  }
97
99
  async readLoop() {
98
100
  try {
@@ -101,7 +103,7 @@ export class RpcClient {
101
103
  if (v.response_to === 0) {
102
104
  // Notification: response_to=0 and request_id=0.
103
105
  if (v.request_id === 0) {
104
- const set = this.notifyHandlers.get(v.type_id >>> 0);
106
+ const set = this.notifyHandlers.get(v.type_id);
105
107
  if (set != null) {
106
108
  for (const h of set) {
107
109
  try {
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_MAX_JSON_FRAME_BYTES, readJsonFrame, writeJsonFrame } from "../framing/jsonframe.js";
2
- import { assertRpcEnvelope, assertRpcError } from "./validate.js";
2
+ import { assertRpcEnvelope, assertRpcError, assertRpcTypeId } from "./validate.js";
3
3
  import { SDK_DEFAULTS } from "../defaults.js";
4
4
  const DEFAULT_RPC_SERVER_OPTIONS = Object.freeze({
5
5
  maxConcurrentRequests: SDK_DEFAULTS.rpc.maxConcurrentRequests,
@@ -10,13 +10,13 @@ export class RpcRouter {
10
10
  handlers = new Map();
11
11
  notifyHandlers = new Map();
12
12
  register(typeId, handler) {
13
- this.handlers.set(typeId >>> 0, handler);
13
+ this.handlers.set(assertRpcTypeId(typeId), handler);
14
14
  }
15
15
  handler(typeId) {
16
- return this.handlers.get(typeId >>> 0);
16
+ return this.handlers.get(assertRpcTypeId(typeId));
17
17
  }
18
18
  onNotify(typeId, handler) {
19
- const normalized = typeId >>> 0;
19
+ const normalized = assertRpcTypeId(typeId);
20
20
  const handlers = this.notifyHandlers.get(normalized) ?? new Set();
21
21
  handlers.add(handler);
22
22
  this.notifyHandlers.set(normalized, handlers);
@@ -27,7 +27,7 @@ export class RpcRouter {
27
27
  };
28
28
  }
29
29
  async dispatchNotification(typeId, payload) {
30
- const normalized = typeId >>> 0;
30
+ const normalized = assertRpcTypeId(typeId);
31
31
  const requestHandler = this.handlers.get(normalized);
32
32
  if (requestHandler !== undefined)
33
33
  await requestHandler(payload);
@@ -76,7 +76,7 @@ export class RpcServer {
76
76
  if (this.closed)
77
77
  throw new Error("rpc server closed");
78
78
  await this.writeEnvelope({
79
- type_id: typeId >>> 0,
79
+ type_id: assertRpcTypeId(typeId),
80
80
  request_id: 0,
81
81
  response_to: 0,
82
82
  payload,
@@ -93,6 +93,7 @@ export class RpcServer {
93
93
  return await new Promise(() => undefined);
94
94
  }));
95
95
  let failure;
96
+ const aborted = abortPromise(signal);
96
97
  try {
97
98
  while (!this.closed) {
98
99
  if (signal?.aborted)
@@ -101,6 +102,7 @@ export class RpcServer {
101
102
  readJsonFrame(this.transport.readExactly, DEFAULT_MAX_JSON_FRAME_BYTES),
102
103
  this.terminalSignal.then((error) => { throw error; }),
103
104
  workerFailure,
105
+ ...(aborted === undefined ? [] : [aborted.promise]),
104
106
  ]);
105
107
  const v = assertRpcEnvelope(next);
106
108
  if (v.response_to !== 0)
@@ -126,6 +128,9 @@ export class RpcServer {
126
128
  failure = err;
127
129
  this.terminalError = err;
128
130
  }
131
+ finally {
132
+ aborted?.cleanup();
133
+ }
129
134
  let closeError;
130
135
  try {
131
136
  this.close(failure ?? this.terminalError ?? new Error("rpc server closed"));
@@ -173,19 +178,23 @@ export class RpcServer {
173
178
  if (work == null)
174
179
  return;
175
180
  const v = work.envelope;
176
- const h = this.router.handler(v.type_id);
177
- let out;
178
- if (h == null)
179
- out = { payload: null, error: { code: 404, message: "handler not found" } };
180
- else {
181
- try {
182
- out = await h(v.payload);
183
- }
184
- catch {
185
- out = { payload: null, error: { code: 500, message: "internal error" } };
186
- }
187
- }
188
181
  try {
182
+ const h = this.router.handler(v.type_id);
183
+ let out;
184
+ if (h == null)
185
+ out = { payload: null, error: { code: 404, message: "handler not found" } };
186
+ else {
187
+ const outcome = await Promise.race([
188
+ Promise.resolve().then(() => h(v.payload)).then((value) => ({ kind: "completed", value }), () => ({
189
+ kind: "completed",
190
+ value: { payload: null, error: { code: 500, message: "internal error" } },
191
+ })),
192
+ this.terminalSignal.then(() => ({ kind: "terminated" })),
193
+ ]);
194
+ if (outcome.kind === "terminated")
195
+ return;
196
+ out = outcome.value;
197
+ }
189
198
  if (this.closed)
190
199
  return;
191
200
  await this.writeResponse(v, out);
@@ -201,7 +210,12 @@ export class RpcServer {
201
210
  if (work == null)
202
211
  return;
203
212
  const v = work.envelope;
204
- await this.router.dispatchNotification(v.type_id, v.payload);
213
+ const completed = await Promise.race([
214
+ Promise.resolve().then(() => this.router.dispatchNotification(v.type_id, v.payload)).then(() => true),
215
+ this.terminalSignal.then(() => false),
216
+ ]);
217
+ if (!completed)
218
+ return;
205
219
  }
206
220
  }
207
221
  async nextWork(queue, waiters) {
@@ -236,7 +250,7 @@ export class RpcServer {
236
250
  await this.writeEnvelope(resp);
237
251
  }
238
252
  async writeEnvelope(envelope) {
239
- const write = this.writeChain.then(() => writeJsonFrame(this.transport.write, envelope));
253
+ const write = this.writeChain.then(() => writeJsonFrame(this.transport.write, envelope, DEFAULT_MAX_JSON_FRAME_BYTES));
240
254
  this.writeChain = write;
241
255
  try {
242
256
  await write;
@@ -247,6 +261,16 @@ export class RpcServer {
247
261
  }
248
262
  }
249
263
  }
264
+ function abortPromise(signal) {
265
+ if (signal === undefined || signal.aborted)
266
+ return undefined;
267
+ let onAbort;
268
+ const promise = new Promise((_resolve, reject) => {
269
+ onAbort = () => reject(signal.reason ?? new Error("aborted"));
270
+ signal.addEventListener("abort", onAbort, { once: true });
271
+ });
272
+ return { promise, cleanup: () => signal.removeEventListener("abort", onAbort) };
273
+ }
250
274
  function positiveInteger(value, name) {
251
275
  if (!Number.isSafeInteger(value) || value <= 0)
252
276
  throw new RangeError(`${name} must be a positive integer`);
@@ -1,3 +1,4 @@
1
1
  import type { RpcEnvelope, RpcError } from "./wire.js";
2
2
  export declare function assertRpcEnvelope(v: unknown): RpcEnvelope;
3
+ export declare function assertRpcTypeId(value: unknown): number;
3
4
  export declare function assertRpcError(value: unknown): RpcError;
@@ -6,21 +6,33 @@ const strictDecoder = new TextDecoder("utf-8", { fatal: true });
6
6
  // The wire format is JSON, so JS numbers are used. For u64 we enforce the safe integer range
7
7
  // to avoid silent precision loss on request/response correlation.
8
8
  export function assertRpcEnvelope(v) {
9
- if (typeof v !== "object" || v == null)
9
+ if (typeof v !== "object" || v == null || Array.isArray(v))
10
10
  throw new Error("bad rpc envelope");
11
11
  const o = v;
12
- if (!isSafeU32Number(o.type_id))
13
- throw new Error("bad rpc envelope: type_id");
12
+ const keys = Object.keys(o);
13
+ if (keys.some((key) => key !== "type_id" && key !== "request_id" && key !== "response_to" && key !== "payload" && key !== "error")) {
14
+ throw new Error("bad rpc envelope: shape");
15
+ }
16
+ if (!Object.prototype.hasOwnProperty.call(o, "payload"))
17
+ throw new Error("bad rpc envelope: payload");
18
+ assertRpcTypeId(o.type_id);
14
19
  if (!isSafeU64Number(o.request_id))
15
20
  throw new Error("bad rpc envelope: request_id");
16
21
  if (!isSafeU64Number(o.response_to))
17
22
  throw new Error("bad rpc envelope: response_to");
18
- // payload: unknown (JSON)
19
- if (o.error != null) {
23
+ if (o.request_id !== 0 && o.response_to !== 0)
24
+ throw new Error("bad rpc envelope: request/response shape");
25
+ if (o.error != null && o.response_to === 0)
26
+ throw new Error("bad rpc envelope: error shape");
27
+ if (o.error != null)
20
28
  assertRpcError(o.error);
21
- }
22
29
  return o;
23
30
  }
31
+ export function assertRpcTypeId(value) {
32
+ if (!isSafeU32Number(value) || value === 0)
33
+ throw new RangeError("RPC typeId must be a non-zero u32 integer");
34
+ return value;
35
+ }
24
36
  export function assertRpcError(value) {
25
37
  if (typeof value !== "object" || value == null)
26
38
  throw new Error("bad rpc envelope: error");
@@ -0,0 +1,14 @@
1
+ export type { ByteStream, IncomingStream, JsonObject, JsonPrimitive, JsonValue, OperationOptions, RpcPeer, RpcResult, SessionErrorCode, StreamOpenOptions, UnreliableMessageChannel, UnreliableMessageSendOptions, UnreliableMessageSendResult, SessionTermination, Session, } from "../public/contract.js";
2
+ export { SessionError, UnreliableMessageError } from "../public/contract.js";
3
+ export { createStreamMetadata, StreamMetadataError } from "../public/streamMetadata.js";
4
+ export type { StreamMetadata } from "../public/streamMetadata.js";
5
+ export { HandlerRegistrationError, LegacyStreamHandlers as StreamHandlers, } from "../public/streamHandlers.js";
6
+ export type { StreamHandler, StreamHandlerOptions, } from "../public/streamHandlers.js";
7
+ export { ArtifactLeaseError, createArtifactLease, } from "../public/artifactLease.js";
8
+ export type { ArtifactLease } from "../public/artifactLease.js";
9
+ export { Artifact, ArtifactError, parseArtifact } from "../public/artifact.js";
10
+ export type { ArtifactErrorCode } from "../public/artifact.js";
11
+ export type { ConnectErrorCode } from "../public/connectError.js";
12
+ export { ConnectError } from "../public/connectError.js";
13
+ export { ConnectionControllerError } from "../connectionController.js";
14
+ export type { ArtifactSource, ArtifactSourceResult, ConnectionController, ConnectionControllerFailure, ConnectionControllerOptions, ConnectionSnapshot, ConnectionState, RetryDisposition, } from "../connectionController.js";
@@ -0,0 +1,7 @@
1
+ export { SessionError, UnreliableMessageError } from "../public/contract.js";
2
+ export { createStreamMetadata, StreamMetadataError } from "../public/streamMetadata.js";
3
+ export { HandlerRegistrationError, LegacyStreamHandlers as StreamHandlers, } from "../public/streamHandlers.js";
4
+ export { ArtifactLeaseError, createArtifactLease, } from "../public/artifactLease.js";
5
+ export { Artifact, ArtifactError, parseArtifact } from "../public/artifact.js";
6
+ export { ConnectError } from "../public/connectError.js";
7
+ export { ConnectionControllerError } from "../connectionController.js";