capnweb 0.9.1 → 0.10.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.
@@ -126,6 +126,13 @@ type Provider<T> = MaybeCallableProvider<T> & (T extends ReadonlyArray<unknown>
126
126
  * ```
127
127
  */
128
128
  type EncodingLevel = "string" | "jsonCompatible" | "jsonCompatibleWithBytes" | "structuredClonable";
129
+ interface RpcLimits {
130
+ maxBigIntDigits: number;
131
+ maxDepth: number;
132
+ maxMessageSize: number;
133
+ }
134
+ declare const DEFAULT_MAX_DEPTH = 256;
135
+ declare const DEFAULT_LIMITS: RpcLimits;
129
136
  /**
130
137
  * Serialize a value, using Cap'n Web's underlying serialization. This won't be able to serialize
131
138
  * RPC stubs, but it will support basic data types.
@@ -227,6 +234,16 @@ type RpcSessionOptions = {
227
234
  * to serialize the error with the stack omitted.
228
235
  */
229
236
  onSendError?: (error: Error) => Error | void;
237
+ /**
238
+ * Overrides for the resource limits enforced while deserializing messages from the peer. Any
239
+ * field left unset falls back to `DEFAULT_LIMITS`. These guard against resource-exhaustion
240
+ * attacks from untrusted peers; see `RpcLimits` for the meaning and defaults of each field.
241
+ *
242
+ * Limits are a purely local, receiver-side decision -- the protocol has no negotiation step, so
243
+ * the peer never learns these values. A message that exceeds a limit is rejected, aborting the
244
+ * session.
245
+ */
246
+ limits?: Partial<RpcLimits>;
230
247
  };
231
248
  //#endregion
232
249
  //#region src/websocket.d.ts
@@ -373,7 +390,7 @@ declare let newMessagePortRpcSession: <T extends RpcCompatible<T> = Empty>(port:
373
390
  * credentials as parameters and returns the authorized API), then cross-origin requests should
374
391
  * be safe.
375
392
  */
376
- declare function newWorkersRpcResponse(request: Request, localMain: any): Promise<Response>;
393
+ declare function newWorkersRpcResponse(request: Request, localMain: any, options?: RpcSessionOptions): Promise<Response>;
377
394
  //#endregion
378
- export { type AnyRpcTransport, type EncodingLevel, type RpcCompatible, RpcPromise, RpcSession, type RpcSessionOptions, RpcStub, RpcTarget, type RpcTransport, type RpcTransportWithCustomEncoding, WebSocketTransport, deserialize, newHttpBatchRpcResponse, newHttpBatchRpcSession, newMessagePortRpcSession, newWebSocketRpcSession, newWorkersRpcResponse, newWorkersWebSocketRpcResponse, nodeHttpBatchRpcResponse, serialize };
395
+ export { type AnyRpcTransport, DEFAULT_LIMITS, DEFAULT_MAX_DEPTH, type EncodingLevel, type RpcCompatible, type RpcLimits, RpcPromise, RpcSession, type RpcSessionOptions, RpcStub, RpcTarget, type RpcTransport, type RpcTransportWithCustomEncoding, WebSocketTransport, deserialize, newHttpBatchRpcResponse, newHttpBatchRpcSession, newMessagePortRpcSession, newWebSocketRpcSession, newWorkersRpcResponse, newWorkersWebSocketRpcResponse, nodeHttpBatchRpcResponse, serialize };
379
396
  //# sourceMappingURL=index-workers.d.cts.map
@@ -126,6 +126,13 @@ type Provider<T> = MaybeCallableProvider<T> & (T extends ReadonlyArray<unknown>
126
126
  * ```
127
127
  */
128
128
  type EncodingLevel = "string" | "jsonCompatible" | "jsonCompatibleWithBytes" | "structuredClonable";
129
+ interface RpcLimits {
130
+ maxBigIntDigits: number;
131
+ maxDepth: number;
132
+ maxMessageSize: number;
133
+ }
134
+ declare const DEFAULT_MAX_DEPTH = 256;
135
+ declare const DEFAULT_LIMITS: RpcLimits;
129
136
  /**
130
137
  * Serialize a value, using Cap'n Web's underlying serialization. This won't be able to serialize
131
138
  * RPC stubs, but it will support basic data types.
@@ -227,6 +234,16 @@ type RpcSessionOptions = {
227
234
  * to serialize the error with the stack omitted.
228
235
  */
229
236
  onSendError?: (error: Error) => Error | void;
237
+ /**
238
+ * Overrides for the resource limits enforced while deserializing messages from the peer. Any
239
+ * field left unset falls back to `DEFAULT_LIMITS`. These guard against resource-exhaustion
240
+ * attacks from untrusted peers; see `RpcLimits` for the meaning and defaults of each field.
241
+ *
242
+ * Limits are a purely local, receiver-side decision -- the protocol has no negotiation step, so
243
+ * the peer never learns these values. A message that exceeds a limit is rejected, aborting the
244
+ * session.
245
+ */
246
+ limits?: Partial<RpcLimits>;
230
247
  };
231
248
  //#endregion
232
249
  //#region src/websocket.d.ts
@@ -373,7 +390,7 @@ declare let newMessagePortRpcSession: <T extends RpcCompatible<T> = Empty>(port:
373
390
  * credentials as parameters and returns the authorized API), then cross-origin requests should
374
391
  * be safe.
375
392
  */
376
- declare function newWorkersRpcResponse(request: Request, localMain: any): Promise<Response>;
393
+ declare function newWorkersRpcResponse(request: Request, localMain: any, options?: RpcSessionOptions): Promise<Response>;
377
394
  //#endregion
378
- export { type AnyRpcTransport, type EncodingLevel, type RpcCompatible, RpcPromise, RpcSession, type RpcSessionOptions, RpcStub, RpcTarget, type RpcTransport, type RpcTransportWithCustomEncoding, WebSocketTransport, deserialize, newHttpBatchRpcResponse, newHttpBatchRpcSession, newMessagePortRpcSession, newWebSocketRpcSession, newWorkersRpcResponse, newWorkersWebSocketRpcResponse, nodeHttpBatchRpcResponse, serialize };
395
+ export { type AnyRpcTransport, DEFAULT_LIMITS, DEFAULT_MAX_DEPTH, type EncodingLevel, type RpcCompatible, type RpcLimits, RpcPromise, RpcSession, type RpcSessionOptions, RpcStub, RpcTarget, type RpcTransport, type RpcTransportWithCustomEncoding, WebSocketTransport, deserialize, newHttpBatchRpcResponse, newHttpBatchRpcSession, newMessagePortRpcSession, newWebSocketRpcSession, newWorkersRpcResponse, newWorkersWebSocketRpcResponse, nodeHttpBatchRpcResponse, serialize };
379
396
  //# sourceMappingURL=index-workers.d.ts.map
@@ -935,6 +935,12 @@ var PromiseStubHook = class PromiseStubHook extends StubHook {
935
935
 
936
936
  //#endregion
937
937
  //#region src/serialize.ts
938
+ const DEFAULT_MAX_DEPTH = 256;
939
+ const DEFAULT_LIMITS = {
940
+ maxBigIntDigits: 16384,
941
+ maxDepth: 256,
942
+ maxMessageSize: 32 * 1024 * 1024
943
+ };
938
944
  var NullExporter = class {
939
945
  exportStub(stub) {
940
946
  throw new Error("Cannot serialize RPC stubs without an RPC session.");
@@ -954,7 +960,7 @@ async function streamToBlob(stream, type) {
954
960
  let b = await new Response(stream).blob();
955
961
  return b.type === type ? b : b.slice(0, b.size, type);
956
962
  }
957
- const ERROR_TYPES = {
963
+ const ERROR_TYPES = Object.assign(Object.create(null), {
958
964
  Error,
959
965
  EvalError,
960
966
  RangeError,
@@ -963,7 +969,7 @@ const ERROR_TYPES = {
963
969
  TypeError,
964
970
  URIError,
965
971
  AggregateError
966
- };
972
+ });
967
973
  var Devaluator = class Devaluator {
968
974
  exporter;
969
975
  source;
@@ -986,7 +992,7 @@ var Devaluator = class Devaluator {
986
992
  }
987
993
  exports;
988
994
  devaluateImpl(value, parent, depth) {
989
- if (depth >= 64) throw new Error("Serialization exceeded maximum allowed depth. (Does the message contain cycles?)");
995
+ if (depth >= 256) throw new Error("Serialization exceeded maximum allowed depth. (Does the message contain cycles?)");
990
996
  switch (typeForRpc(value)) {
991
997
  case "unsupported": {
992
998
  let msg;
@@ -1220,6 +1226,9 @@ var NullImporter = class {
1220
1226
  getPipeReadable(exportId) {
1221
1227
  throw new Error("Cannot retrieve pipe readable without an RPC session.");
1222
1228
  }
1229
+ getLimits() {
1230
+ return DEFAULT_LIMITS;
1231
+ }
1223
1232
  };
1224
1233
  const NULL_IMPORTER = new NullImporter();
1225
1234
  function fixBrokenRequestBody(request, body) {
@@ -1237,16 +1246,21 @@ function streamToBlobPromise(stream, type) {
1237
1246
  var Evaluator = class Evaluator {
1238
1247
  importer;
1239
1248
  encodingLevel;
1249
+ limits;
1240
1250
  constructor(importer, encodingLevel = "string") {
1241
1251
  this.importer = importer;
1242
1252
  this.encodingLevel = encodingLevel;
1253
+ this.limits = importer.getLimits();
1243
1254
  }
1244
1255
  hooks = [];
1245
1256
  promises = [];
1246
1257
  evaluate(value) {
1258
+ return this.evaluateWithDepth(value, 0);
1259
+ }
1260
+ evaluateWithDepth(value, depth) {
1247
1261
  let payload = RpcPayload.forEvaluate(this.hooks, this.promises);
1248
1262
  try {
1249
- payload.value = this.evaluateImpl(value, payload, "value");
1263
+ payload.value = this.evaluateImpl(value, payload, "value", depth);
1250
1264
  return payload;
1251
1265
  } catch (err) {
1252
1266
  payload.dispose();
@@ -1256,18 +1270,25 @@ var Evaluator = class Evaluator {
1256
1270
  evaluateCopy(value) {
1257
1271
  return this.evaluate(structuredClone(value));
1258
1272
  }
1259
- evaluateImpl(value, parent, property) {
1273
+ evaluateImpl(value, parent, property, depth) {
1274
+ let maxDepth = this.limits.maxDepth;
1275
+ if (depth >= maxDepth) throw new TypeError(`Deserialization exceeded maximum allowed message depth of ${maxDepth}.`);
1260
1276
  if (this.encodingLevel === "structuredClonable") {
1261
1277
  if (value instanceof Date || typeof value === "bigint") return value;
1262
1278
  }
1263
1279
  if (value instanceof Array) {
1264
1280
  if (value.length == 1 && value[0] instanceof Array) {
1265
1281
  let result = value[0];
1266
- for (let i = 0; i < result.length; i++) result[i] = this.evaluateImpl(result[i], result, i);
1282
+ for (let i = 0; i < result.length; i++) result[i] = this.evaluateImpl(result[i], result, i, depth + 1);
1267
1283
  return result;
1268
1284
  } else switch (value[0]) {
1269
1285
  case "bigint":
1270
- if (typeof value[1] == "string") return BigInt(value[1]);
1286
+ if (typeof value[1] == "string") {
1287
+ let digits = value[1];
1288
+ let maxBigIntDigits = this.limits.maxBigIntDigits;
1289
+ if (digits.length > maxBigIntDigits) throw new TypeError(`Deserialized bigint exceeds maximum length of ${maxBigIntDigits} digits.`);
1290
+ return BigInt(digits);
1291
+ }
1271
1292
  break;
1272
1293
  case "date":
1273
1294
  if (value[1] === null) return /* @__PURE__ */ new Date(NaN);
@@ -1297,7 +1318,11 @@ var Evaluator = class Evaluator {
1297
1318
  let propsObj = props;
1298
1319
  for (let key of Object.keys(propsObj)) {
1299
1320
  if (key === "name" || key === "message" || key === "stack") continue;
1300
- anyResult[key] = this.evaluateImpl(propsObj[key], result, key);
1321
+ if (key in Object.prototype || key === "toJSON") {
1322
+ this.evaluateImpl(propsObj[key], result, key, depth + 1);
1323
+ continue;
1324
+ }
1325
+ anyResult[key] = this.evaluateImpl(propsObj[key], result, key, depth + 1);
1301
1326
  }
1302
1327
  }
1303
1328
  return result;
@@ -1318,11 +1343,11 @@ var Evaluator = class Evaluator {
1318
1343
  let init = value[2];
1319
1344
  if (typeof init !== "object" || init === null) break;
1320
1345
  if (init.body) {
1321
- init.body = this.evaluateImpl(init.body, init, "body");
1346
+ init.body = this.evaluateImpl(init.body, init, "body", depth + 1);
1322
1347
  if (init.body === null || typeof init.body === "string" || init.body instanceof Uint8Array || init.body instanceof ReadableStream) {} else throw new TypeError("Request body must be of type ReadableStream.");
1323
1348
  }
1324
1349
  if (init.signal) {
1325
- init.signal = this.evaluateImpl(init.signal, init, "signal");
1350
+ init.signal = this.evaluateImpl(init.signal, init, "signal", depth + 1);
1326
1351
  if (!(init.signal instanceof AbortSignal)) throw new TypeError("Request siganl must be of type AbortSignal.");
1327
1352
  }
1328
1353
  if (init.headers && !(init.headers instanceof Array)) throw new TypeError("Request headers must be serialized as an array of pairs.");
@@ -1339,7 +1364,7 @@ var Evaluator = class Evaluator {
1339
1364
  }
1340
1365
  case "response": {
1341
1366
  if (value.length !== 3) break;
1342
- let body = this.evaluateImpl(value[1], parent, property);
1367
+ let body = this.evaluateImpl(value[1], parent, property, depth + 1);
1343
1368
  if (body === null || typeof body === "string" || body instanceof Uint8Array || body instanceof ReadableStream) {} else throw new TypeError("Response body must be of type ReadableStream.");
1344
1369
  let init = value[2];
1345
1370
  if (typeof init !== "object" || init === null) break;
@@ -1350,7 +1375,7 @@ var Evaluator = class Evaluator {
1350
1375
  case "blob": {
1351
1376
  if (value.length !== 3 || typeof value[1] !== "string") break;
1352
1377
  let contentType = value[1];
1353
- let content = this.evaluateImpl(value[2], parent, property);
1378
+ let content = this.evaluateImpl(value[2], parent, property, depth + 1);
1354
1379
  if (!(content instanceof ReadableStream)) throw new TypeError("Blob content must be serialized as a ReadableStream.");
1355
1380
  let promise = streamToBlobPromise(content, contentType);
1356
1381
  this.promises.push({
@@ -1391,7 +1416,7 @@ var Evaluator = class Evaluator {
1391
1416
  if (value.length == 3) return addStub(hook.get(path));
1392
1417
  let args = value[3];
1393
1418
  if (!(args instanceof Array)) break;
1394
- args = new Evaluator(this.importer).evaluate([args]);
1419
+ args = new Evaluator(this.importer).evaluateWithDepth([args], depth);
1395
1420
  return addStub(hook.call(path, args));
1396
1421
  }
1397
1422
  case "remap": {
@@ -1457,9 +1482,9 @@ var Evaluator = class Evaluator {
1457
1482
  } else if (value instanceof Object) {
1458
1483
  let result = value;
1459
1484
  for (let key in result) if (key in Object.prototype || key === "toJSON") {
1460
- this.evaluateImpl(result[key], result, key);
1485
+ this.evaluateImpl(result[key], result, key, depth + 1);
1461
1486
  delete result[key];
1462
- } else result[key] = this.evaluateImpl(result[key], result, key);
1487
+ } else result[key] = this.evaluateImpl(result[key], result, key, depth + 1);
1463
1488
  return result;
1464
1489
  } else return value;
1465
1490
  }
@@ -1530,6 +1555,10 @@ var ImportTableEntry = class {
1530
1555
  resolution;
1531
1556
  onBrokenRegistrations;
1532
1557
  resolve(resolution) {
1558
+ if (this.resolution) {
1559
+ resolution.dispose();
1560
+ return;
1561
+ }
1533
1562
  if (this.localRefcount == 0) {
1534
1563
  resolution.dispose();
1535
1564
  return;
@@ -1682,6 +1711,7 @@ var RpcSessionImpl = class {
1682
1711
  pullCount = 0;
1683
1712
  onBrokenCallbacks = [];
1684
1713
  encodingLevel;
1714
+ limits;
1685
1715
  constructor(transport, mainHook, options) {
1686
1716
  this.transport = transport;
1687
1717
  this.options = options;
@@ -1694,6 +1724,10 @@ var RpcSessionImpl = class {
1694
1724
  }
1695
1725
  }
1696
1726
  this.encodingLevel = level;
1727
+ this.limits = {
1728
+ ...DEFAULT_LIMITS,
1729
+ ...options.limits
1730
+ };
1697
1731
  this.exports.push({
1698
1732
  hook: mainHook,
1699
1733
  refcount: 1
@@ -1836,6 +1870,9 @@ var RpcSessionImpl = class {
1836
1870
  entry.pipeReadable = void 0;
1837
1871
  return readable;
1838
1872
  }
1873
+ getLimits() {
1874
+ return this.limits;
1875
+ }
1839
1876
  createPipe(readable, readableHook) {
1840
1877
  if (this.abortReason) throw this.abortReason;
1841
1878
  this.send(["pipe"]);
@@ -1995,6 +2032,7 @@ var RpcSessionImpl = class {
1995
2032
  } finally {
1996
2033
  if (this.cancelReadLoop === readCanceled.reject) this.cancelReadLoop = void 0;
1997
2034
  }
2035
+ if (this.encodingLevel === "string" && raw.length > this.limits.maxMessageSize) throw new TypeError(`Incoming message exceeds maximum size of ${this.limits.maxMessageSize} UTF-16 code units.`);
1998
2036
  if (this.abortReason) break;
1999
2037
  let msg = this.encodingLevel === "string" ? JSON.parse(raw) : raw;
2000
2038
  if (msg instanceof Array) switch (msg[0]) {
@@ -2069,7 +2107,7 @@ var RpcSessionImpl = class {
2069
2107
  case "abort": {
2070
2108
  let payload = new Evaluator(this, this.encodingLevel).evaluate(msg[1]);
2071
2109
  payload.dispose();
2072
- this.abort(payload, false);
2110
+ this.abort(payload.value, false);
2073
2111
  break;
2074
2112
  }
2075
2113
  }
@@ -2195,6 +2233,8 @@ var WebSocketTransport = class {
2195
2233
  let message;
2196
2234
  if (reason instanceof Error) message = reason.message;
2197
2235
  else message = `${reason}`;
2236
+ let reasonBytes = new TextEncoder().encode(message);
2237
+ if (reasonBytes.length > 123) message = new TextDecoder().decode(reasonBytes.subarray(0, 123), { stream: true });
2198
2238
  this.#webSocket.close(3e3, message);
2199
2239
  if (!this.#error) this.#error = reason;
2200
2240
  }
@@ -2580,6 +2620,9 @@ var MapApplicator = class {
2580
2620
  getPipeReadable(exportId) {
2581
2621
  throw new Error("A mapper function cannot use pipe readables.");
2582
2622
  }
2623
+ getLimits() {
2624
+ return DEFAULT_LIMITS;
2625
+ }
2583
2626
  };
2584
2627
  function applyMapToElement(input, parent, owner, captures, instructions) {
2585
2628
  let mapper = new MapApplicator(captures, new PayloadStubHook(RpcPayload.deepCopyFrom(input, parent, owner)));
@@ -2918,15 +2961,15 @@ let newMessagePortRpcSession = newMessagePortRpcSession$1;
2918
2961
  * credentials as parameters and returns the authorized API), then cross-origin requests should
2919
2962
  * be safe.
2920
2963
  */
2921
- async function newWorkersRpcResponse(request, localMain) {
2964
+ async function newWorkersRpcResponse(request, localMain, options) {
2922
2965
  if (request.method === "POST") {
2923
- let response = await newHttpBatchRpcResponse(request, localMain);
2966
+ let response = await newHttpBatchRpcResponse(request, localMain, options);
2924
2967
  response.headers.set("Access-Control-Allow-Origin", "*");
2925
2968
  return response;
2926
- } else if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") return newWorkersWebSocketRpcResponse(request, localMain);
2969
+ } else if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") return newWorkersWebSocketRpcResponse(request, localMain, options);
2927
2970
  else return new Response("This endpoint only accepts POST or WebSocket requests.", { status: 400 });
2928
2971
  }
2929
2972
 
2930
2973
  //#endregion
2931
- export { RpcPromise, RpcSession, RpcStub, RpcTarget, WebSocketTransport, deserialize, newHttpBatchRpcResponse, newHttpBatchRpcSession, newMessagePortRpcSession, newWebSocketRpcSession, newWorkersRpcResponse, newWorkersWebSocketRpcResponse, nodeHttpBatchRpcResponse, serialize };
2974
+ export { DEFAULT_LIMITS, DEFAULT_MAX_DEPTH, RpcPromise, RpcSession, RpcStub, RpcTarget, WebSocketTransport, deserialize, newHttpBatchRpcResponse, newHttpBatchRpcSession, newMessagePortRpcSession, newWebSocketRpcSession, newWorkersRpcResponse, newWorkersWebSocketRpcResponse, nodeHttpBatchRpcResponse, serialize };
2932
2975
  //# sourceMappingURL=index-workers.js.map