capnweb 0.9.0 → 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.
package/README.md CHANGED
@@ -405,6 +405,8 @@ If anything happens to the stub that would cause all further method calls and pr
405
405
 
406
406
  * Cap'n Web's pipelining can make it easy for a malicious client to enqueue a large amount of work to occur on a server. To mitigate this, we recommend implementing rate limits on expensive operations. If using Cloudflare Workers, you may also consider configuring [per-request CPU limits](https://developers.cloudflare.com/workers/wrangler/configuration/#limits) to be lower than the default 30s. Note that in stateless Workers (i.e. not Durable Objects), the system considers an entire WebSocket session to be one "request" for CPU limits purposes.
407
407
 
408
+ * Cap'n Web applies receiver-side resource limits before expensive message processing, including a maximum incoming message size before `JSON.parse`. If your app is exposed to untrusted peers, also configure native transport or socket payload limits where available, such as `ws`'s `maxPayload`, Bun's `maxPayloadLength`, or the runtime's built-in WebSocket cap. Cap'n Web's own check runs after `RpcTransport.receive()` has returned a complete message string, so transport-level limits are still the first line of defense against buffering very large frames.
409
+
408
410
  * Cap'n Web currently does not provide any runtime type checking. When using TypeScript, keep in mind that types are checked only at compile time. A malicious client can send types you did not expect, and this could cause you application to behave in unexpected ways. For example, MongoDB uses special property names to express queries; placing attacker-provided values directly into queries can result in query injection vulnerabilities (similar to SQL injection). Of course, JSON has always had the same problem, and there exists tooling to solve it. You might consider using a runtime type-checking framework like Zod to check your inputs. In the future, we hope to explore auto-generating type-checking code based on TypeScript types.
409
411
 
410
412
  ## Setting up a session
@@ -931,6 +931,12 @@ var PromiseStubHook = class PromiseStubHook extends StubHook {
931
931
 
932
932
  //#endregion
933
933
  //#region src/serialize.ts
934
+ const DEFAULT_MAX_DEPTH = 256;
935
+ const DEFAULT_LIMITS = {
936
+ maxBigIntDigits: 16384,
937
+ maxDepth: 256,
938
+ maxMessageSize: 32 * 1024 * 1024
939
+ };
934
940
  var NullExporter = class {
935
941
  exportStub(stub) {
936
942
  throw new Error("Cannot serialize RPC stubs without an RPC session.");
@@ -950,7 +956,7 @@ async function streamToBlob(stream, type) {
950
956
  let b = await new Response(stream).blob();
951
957
  return b.type === type ? b : b.slice(0, b.size, type);
952
958
  }
953
- const ERROR_TYPES = {
959
+ const ERROR_TYPES = Object.assign(Object.create(null), {
954
960
  Error,
955
961
  EvalError,
956
962
  RangeError,
@@ -959,7 +965,7 @@ const ERROR_TYPES = {
959
965
  TypeError,
960
966
  URIError,
961
967
  AggregateError
962
- };
968
+ });
963
969
  var Devaluator = class Devaluator {
964
970
  exporter;
965
971
  source;
@@ -982,7 +988,7 @@ var Devaluator = class Devaluator {
982
988
  }
983
989
  exports;
984
990
  devaluateImpl(value, parent, depth) {
985
- if (depth >= 64) throw new Error("Serialization exceeded maximum allowed depth. (Does the message contain cycles?)");
991
+ if (depth >= 256) throw new Error("Serialization exceeded maximum allowed depth. (Does the message contain cycles?)");
986
992
  switch (typeForRpc(value)) {
987
993
  case "unsupported": {
988
994
  let msg;
@@ -1216,6 +1222,9 @@ var NullImporter = class {
1216
1222
  getPipeReadable(exportId) {
1217
1223
  throw new Error("Cannot retrieve pipe readable without an RPC session.");
1218
1224
  }
1225
+ getLimits() {
1226
+ return DEFAULT_LIMITS;
1227
+ }
1219
1228
  };
1220
1229
  const NULL_IMPORTER = new NullImporter();
1221
1230
  function fixBrokenRequestBody(request, body) {
@@ -1233,16 +1242,21 @@ function streamToBlobPromise(stream, type) {
1233
1242
  var Evaluator = class Evaluator {
1234
1243
  importer;
1235
1244
  encodingLevel;
1245
+ limits;
1236
1246
  constructor(importer, encodingLevel = "string") {
1237
1247
  this.importer = importer;
1238
1248
  this.encodingLevel = encodingLevel;
1249
+ this.limits = importer.getLimits();
1239
1250
  }
1240
1251
  hooks = [];
1241
1252
  promises = [];
1242
1253
  evaluate(value) {
1254
+ return this.evaluateWithDepth(value, 0);
1255
+ }
1256
+ evaluateWithDepth(value, depth) {
1243
1257
  let payload = RpcPayload.forEvaluate(this.hooks, this.promises);
1244
1258
  try {
1245
- payload.value = this.evaluateImpl(value, payload, "value");
1259
+ payload.value = this.evaluateImpl(value, payload, "value", depth);
1246
1260
  return payload;
1247
1261
  } catch (err) {
1248
1262
  payload.dispose();
@@ -1252,18 +1266,25 @@ var Evaluator = class Evaluator {
1252
1266
  evaluateCopy(value) {
1253
1267
  return this.evaluate(structuredClone(value));
1254
1268
  }
1255
- evaluateImpl(value, parent, property) {
1269
+ evaluateImpl(value, parent, property, depth) {
1270
+ let maxDepth = this.limits.maxDepth;
1271
+ if (depth >= maxDepth) throw new TypeError(`Deserialization exceeded maximum allowed message depth of ${maxDepth}.`);
1256
1272
  if (this.encodingLevel === "structuredClonable") {
1257
1273
  if (value instanceof Date || typeof value === "bigint") return value;
1258
1274
  }
1259
1275
  if (value instanceof Array) {
1260
1276
  if (value.length == 1 && value[0] instanceof Array) {
1261
1277
  let result = value[0];
1262
- for (let i = 0; i < result.length; i++) result[i] = this.evaluateImpl(result[i], result, i);
1278
+ for (let i = 0; i < result.length; i++) result[i] = this.evaluateImpl(result[i], result, i, depth + 1);
1263
1279
  return result;
1264
1280
  } else switch (value[0]) {
1265
1281
  case "bigint":
1266
- if (typeof value[1] == "string") return BigInt(value[1]);
1282
+ if (typeof value[1] == "string") {
1283
+ let digits = value[1];
1284
+ let maxBigIntDigits = this.limits.maxBigIntDigits;
1285
+ if (digits.length > maxBigIntDigits) throw new TypeError(`Deserialized bigint exceeds maximum length of ${maxBigIntDigits} digits.`);
1286
+ return BigInt(digits);
1287
+ }
1267
1288
  break;
1268
1289
  case "date":
1269
1290
  if (value[1] === null) return /* @__PURE__ */ new Date(NaN);
@@ -1293,7 +1314,11 @@ var Evaluator = class Evaluator {
1293
1314
  let propsObj = props;
1294
1315
  for (let key of Object.keys(propsObj)) {
1295
1316
  if (key === "name" || key === "message" || key === "stack") continue;
1296
- anyResult[key] = this.evaluateImpl(propsObj[key], result, key);
1317
+ if (key in Object.prototype || key === "toJSON") {
1318
+ this.evaluateImpl(propsObj[key], result, key, depth + 1);
1319
+ continue;
1320
+ }
1321
+ anyResult[key] = this.evaluateImpl(propsObj[key], result, key, depth + 1);
1297
1322
  }
1298
1323
  }
1299
1324
  return result;
@@ -1314,11 +1339,11 @@ var Evaluator = class Evaluator {
1314
1339
  let init = value[2];
1315
1340
  if (typeof init !== "object" || init === null) break;
1316
1341
  if (init.body) {
1317
- init.body = this.evaluateImpl(init.body, init, "body");
1342
+ init.body = this.evaluateImpl(init.body, init, "body", depth + 1);
1318
1343
  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.");
1319
1344
  }
1320
1345
  if (init.signal) {
1321
- init.signal = this.evaluateImpl(init.signal, init, "signal");
1346
+ init.signal = this.evaluateImpl(init.signal, init, "signal", depth + 1);
1322
1347
  if (!(init.signal instanceof AbortSignal)) throw new TypeError("Request siganl must be of type AbortSignal.");
1323
1348
  }
1324
1349
  if (init.headers && !(init.headers instanceof Array)) throw new TypeError("Request headers must be serialized as an array of pairs.");
@@ -1335,7 +1360,7 @@ var Evaluator = class Evaluator {
1335
1360
  }
1336
1361
  case "response": {
1337
1362
  if (value.length !== 3) break;
1338
- let body = this.evaluateImpl(value[1], parent, property);
1363
+ let body = this.evaluateImpl(value[1], parent, property, depth + 1);
1339
1364
  if (body === null || typeof body === "string" || body instanceof Uint8Array || body instanceof ReadableStream) {} else throw new TypeError("Response body must be of type ReadableStream.");
1340
1365
  let init = value[2];
1341
1366
  if (typeof init !== "object" || init === null) break;
@@ -1346,7 +1371,7 @@ var Evaluator = class Evaluator {
1346
1371
  case "blob": {
1347
1372
  if (value.length !== 3 || typeof value[1] !== "string") break;
1348
1373
  let contentType = value[1];
1349
- let content = this.evaluateImpl(value[2], parent, property);
1374
+ let content = this.evaluateImpl(value[2], parent, property, depth + 1);
1350
1375
  if (!(content instanceof ReadableStream)) throw new TypeError("Blob content must be serialized as a ReadableStream.");
1351
1376
  let promise = streamToBlobPromise(content, contentType);
1352
1377
  this.promises.push({
@@ -1387,7 +1412,7 @@ var Evaluator = class Evaluator {
1387
1412
  if (value.length == 3) return addStub(hook.get(path));
1388
1413
  let args = value[3];
1389
1414
  if (!(args instanceof Array)) break;
1390
- args = new Evaluator(this.importer).evaluate([args]);
1415
+ args = new Evaluator(this.importer).evaluateWithDepth([args], depth);
1391
1416
  return addStub(hook.call(path, args));
1392
1417
  }
1393
1418
  case "remap": {
@@ -1453,9 +1478,9 @@ var Evaluator = class Evaluator {
1453
1478
  } else if (value instanceof Object) {
1454
1479
  let result = value;
1455
1480
  for (let key in result) if (key in Object.prototype || key === "toJSON") {
1456
- this.evaluateImpl(result[key], result, key);
1481
+ this.evaluateImpl(result[key], result, key, depth + 1);
1457
1482
  delete result[key];
1458
- } else result[key] = this.evaluateImpl(result[key], result, key);
1483
+ } else result[key] = this.evaluateImpl(result[key], result, key, depth + 1);
1459
1484
  return result;
1460
1485
  } else return value;
1461
1486
  }
@@ -1526,6 +1551,10 @@ var ImportTableEntry = class {
1526
1551
  resolution;
1527
1552
  onBrokenRegistrations;
1528
1553
  resolve(resolution) {
1554
+ if (this.resolution) {
1555
+ resolution.dispose();
1556
+ return;
1557
+ }
1529
1558
  if (this.localRefcount == 0) {
1530
1559
  resolution.dispose();
1531
1560
  return;
@@ -1678,6 +1707,7 @@ var RpcSessionImpl = class {
1678
1707
  pullCount = 0;
1679
1708
  onBrokenCallbacks = [];
1680
1709
  encodingLevel;
1710
+ limits;
1681
1711
  constructor(transport, mainHook, options) {
1682
1712
  this.transport = transport;
1683
1713
  this.options = options;
@@ -1690,6 +1720,10 @@ var RpcSessionImpl = class {
1690
1720
  }
1691
1721
  }
1692
1722
  this.encodingLevel = level;
1723
+ this.limits = {
1724
+ ...DEFAULT_LIMITS,
1725
+ ...options.limits
1726
+ };
1693
1727
  this.exports.push({
1694
1728
  hook: mainHook,
1695
1729
  refcount: 1
@@ -1832,6 +1866,9 @@ var RpcSessionImpl = class {
1832
1866
  entry.pipeReadable = void 0;
1833
1867
  return readable;
1834
1868
  }
1869
+ getLimits() {
1870
+ return this.limits;
1871
+ }
1835
1872
  createPipe(readable, readableHook) {
1836
1873
  if (this.abortReason) throw this.abortReason;
1837
1874
  this.send(["pipe"]);
@@ -1991,6 +2028,7 @@ var RpcSessionImpl = class {
1991
2028
  } finally {
1992
2029
  if (this.cancelReadLoop === readCanceled.reject) this.cancelReadLoop = void 0;
1993
2030
  }
2031
+ 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.`);
1994
2032
  if (this.abortReason) break;
1995
2033
  let msg = this.encodingLevel === "string" ? JSON.parse(raw) : raw;
1996
2034
  if (msg instanceof Array) switch (msg[0]) {
@@ -2065,7 +2103,7 @@ var RpcSessionImpl = class {
2065
2103
  case "abort": {
2066
2104
  let payload = new Evaluator(this, this.encodingLevel).evaluate(msg[1]);
2067
2105
  payload.dispose();
2068
- this.abort(payload, false);
2106
+ this.abort(payload.value, false);
2069
2107
  break;
2070
2108
  }
2071
2109
  }
@@ -2191,6 +2229,8 @@ var WebSocketTransport = class {
2191
2229
  let message;
2192
2230
  if (reason instanceof Error) message = reason.message;
2193
2231
  else message = `${reason}`;
2232
+ let reasonBytes = new TextEncoder().encode(message);
2233
+ if (reasonBytes.length > 123) message = new TextDecoder().decode(reasonBytes.subarray(0, 123), { stream: true });
2194
2234
  this.#webSocket.close(3e3, message);
2195
2235
  if (!this.#error) this.#error = reason;
2196
2236
  }
@@ -2307,7 +2347,11 @@ async function newHttpBatchRpcResponse(request, localMain, options) {
2307
2347
  * @param options Optional RPC session options. You can also pass headers to set on the response.
2308
2348
  */
2309
2349
  async function nodeHttpBatchRpcResponse(request, response, localMain, options) {
2310
- if (request.method !== "POST") response.writeHead(405, "This endpoint only accepts POST requests.");
2350
+ if (request.method !== "POST") {
2351
+ response.writeHead(405, "This endpoint only accepts POST requests.", options?.headers);
2352
+ response.end();
2353
+ return;
2354
+ }
2311
2355
  let body = await new Promise((resolve, reject) => {
2312
2356
  let chunks = [];
2313
2357
  request.on("data", (chunk) => {
@@ -2572,6 +2616,9 @@ var MapApplicator = class {
2572
2616
  getPipeReadable(exportId) {
2573
2617
  throw new Error("A mapper function cannot use pipe readables.");
2574
2618
  }
2619
+ getLimits() {
2620
+ return DEFAULT_LIMITS;
2621
+ }
2575
2622
  };
2576
2623
  function applyMapToElement(input, parent, owner, captures, instructions) {
2577
2624
  let mapper = new MapApplicator(captures, new PayloadStubHook(RpcPayload.deepCopyFrom(input, parent, owner)));
@@ -2910,12 +2957,12 @@ let newMessagePortRpcSession = newMessagePortRpcSession$1;
2910
2957
  * credentials as parameters and returns the authorized API), then cross-origin requests should
2911
2958
  * be safe.
2912
2959
  */
2913
- async function newWorkersRpcResponse(request, localMain) {
2960
+ async function newWorkersRpcResponse(request, localMain, options) {
2914
2961
  if (request.method === "POST") {
2915
- let response = await newHttpBatchRpcResponse(request, localMain);
2962
+ let response = await newHttpBatchRpcResponse(request, localMain, options);
2916
2963
  response.headers.set("Access-Control-Allow-Origin", "*");
2917
2964
  return response;
2918
- } else if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") return newWorkersWebSocketRpcResponse(request, localMain);
2965
+ } else if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") return newWorkersWebSocketRpcResponse(request, localMain, options);
2919
2966
  else return new Response("This endpoint only accepts POST or WebSocket requests.", { status: 400 });
2920
2967
  }
2921
2968
 
@@ -3037,6 +3084,8 @@ let newBunWebSocketRpcSession = newBunWebSocketRpcSession$1;
3037
3084
 
3038
3085
  //#endregion
3039
3086
  exports.BunWebSocketTransport = BunWebSocketTransport;
3087
+ exports.DEFAULT_LIMITS = DEFAULT_LIMITS;
3088
+ exports.DEFAULT_MAX_DEPTH = DEFAULT_MAX_DEPTH;
3040
3089
  exports.RpcPromise = RpcPromise;
3041
3090
  exports.RpcSession = RpcSession;
3042
3091
  exports.RpcStub = RpcStub;