@fitzzero/quickdraw-core 5.0.1 → 5.1.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 (60) hide show
  1. package/README.md +303 -32
  2. package/dist/{admin-CBlTY0do.d.ts → admin-DKwkIh5i.d.ts} +2 -2
  3. package/dist/{adminSchemas-zji11V74.d.ts → adminSchemas-2DUA_hZ8.d.ts} +1 -1
  4. package/dist/{auth-CMyRkfjZ.d.ts → auth-BhWqdyb-.d.ts} +2 -2
  5. package/dist/{caller-BqCYnpmC.d.ts → caller-CXTFswBO.d.ts} +27 -5
  6. package/dist/{chunk-2XJFNVHW.js → chunk-2PWMUVC7.js} +1 -1
  7. package/dist/{chunk-2XJFNVHW.js.map → chunk-2PWMUVC7.js.map} +1 -1
  8. package/dist/{chunk-3JNMVB2V.js → chunk-2UELNHDD.js} +2 -2
  9. package/dist/{chunk-HQ6WIRE2.js → chunk-3674EIRT.js} +4 -4
  10. package/dist/{chunk-NS5PUHIZ.js → chunk-A6DJBGOG.js} +130 -23
  11. package/dist/chunk-A6DJBGOG.js.map +1 -0
  12. package/dist/{chunk-ZACRRYWL.js → chunk-JZBCLNDU.js} +4 -4
  13. package/dist/{chunk-O2PWC2YW.js → chunk-KNDDCVQJ.js} +3 -3
  14. package/dist/{chunk-5YRQ26LF.js → chunk-RT6OILOI.js} +1 -1
  15. package/dist/chunk-RT6OILOI.js.map +1 -0
  16. package/dist/{chunk-WVIRXYI4.js → chunk-VJTFXHOK.js} +2 -2
  17. package/dist/{chunk-WVIRXYI4.js.map → chunk-VJTFXHOK.js.map} +1 -1
  18. package/dist/chunk-XNLWBZIG.js +9 -0
  19. package/dist/chunk-XNLWBZIG.js.map +1 -0
  20. package/dist/cli/quickdraw-docs.js +241 -27
  21. package/dist/cli/quickdraw-docs.js.map +1 -1
  22. package/dist/client/index.d.ts +7 -7
  23. package/dist/client/index.js +4 -4
  24. package/dist/{clientTypes-DuZW_8mb.d.ts → clientTypes-Bc-G8n-U.d.ts} +3 -3
  25. package/dist/{connection-DuN8GvcD.d.ts → connection-Cr1BU07W.d.ts} +2 -2
  26. package/dist/{createServer-CGsJlxM8.d.ts → createServer-CP9eDHZZ.d.ts} +30 -9
  27. package/dist/{createTestApp-Cg2CZa1g.d.ts → createTestApp-UKVGEmOf.d.ts} +6 -6
  28. package/dist/{envelope-nf5eGB-a.d.ts → envelope-Cp3YcVxC.d.ts} +1 -1
  29. package/dist/{http-D3fSFQ57.d.ts → http-Djj2Inuu.d.ts} +6 -6
  30. package/dist/index.d.ts +6 -6
  31. package/dist/index.js +26 -6
  32. package/dist/index.js.map +1 -1
  33. package/dist/{infer-Dw21kmLw.d.ts → infer-Teo1xpsd.d.ts} +17 -0
  34. package/dist/{metrics-C0WvB7-I.d.ts → metrics-BdBu5o8G.d.ts} +2 -2
  35. package/dist/prisma/index.d.ts +4 -4
  36. package/dist/server/auth/index.d.ts +3 -3
  37. package/dist/server/index.d.ts +47 -26
  38. package/dist/server/index.js +62 -36
  39. package/dist/server/index.js.map +1 -1
  40. package/dist/server/mcp/index.d.ts +51 -11
  41. package/dist/server/mcp/index.js +129 -8
  42. package/dist/server/mcp/index.js.map +1 -1
  43. package/dist/server/otel.d.ts +3 -3
  44. package/dist/{sharing-CoreezAq.d.ts → sharing-DVa8W-yR.d.ts} +3 -3
  45. package/dist/testing/client.d.ts +13 -13
  46. package/dist/testing/client.js +5 -5
  47. package/dist/testing/index.d.ts +156 -12
  48. package/dist/testing/index.js +1036 -21
  49. package/dist/testing/index.js.map +1 -1
  50. package/dist/testing/mock.d.ts +5 -5
  51. package/dist/testing/mock.js +4 -4
  52. package/dist/{types-Bwg3kIEc.d.ts → types-Cu0vmx7c.d.ts} +8 -2
  53. package/dist/utils/index.d.ts +1 -1
  54. package/package.json +1 -1
  55. package/dist/chunk-5YRQ26LF.js.map +0 -1
  56. package/dist/chunk-NS5PUHIZ.js.map +0 -1
  57. /package/dist/{chunk-3JNMVB2V.js.map → chunk-2UELNHDD.js.map} +0 -0
  58. /package/dist/{chunk-HQ6WIRE2.js.map → chunk-3674EIRT.js.map} +0 -0
  59. /package/dist/{chunk-ZACRRYWL.js.map → chunk-JZBCLNDU.js.map} +0 -0
  60. /package/dist/{chunk-O2PWC2YW.js.map → chunk-KNDDCVQJ.js.map} +0 -0
@@ -30,7 +30,7 @@ import {
30
30
  toQuickdrawError,
31
31
  toWireIssues,
32
32
  withSignal
33
- } from "./chunk-2XJFNVHW.js";
33
+ } from "./chunk-2PWMUVC7.js";
34
34
  import {
35
35
  createRecorder,
36
36
  describeError,
@@ -77,7 +77,7 @@ import {
77
77
  streamRoom,
78
78
  topicRoom,
79
79
  userRoom
80
- } from "./chunk-WVIRXYI4.js";
80
+ } from "./chunk-VJTFXHOK.js";
81
81
  import {
82
82
  hasJsonSchema
83
83
  } from "./chunk-47AEXVDM.js";
@@ -1547,8 +1547,11 @@ function receive(deps, state, frame) {
1547
1547
  return;
1548
1548
  }
1549
1549
  const { principal } = state.socket.data;
1550
- const parsed = principal === null ? void 0 : parse(deps, channel, payload);
1551
- if (principal === null || parsed === void 0) {
1550
+ if (principal === null || channel.kinds?.has(principal.kind ?? "") === false) {
1551
+ return;
1552
+ }
1553
+ const parsed = parse(deps, channel, payload);
1554
+ if (parsed === void 0) {
1552
1555
  return;
1553
1556
  }
1554
1557
  const room = allowed(channel, state.socket, principal, parsed.value);
@@ -2107,6 +2110,44 @@ var StreamIndex = class {
2107
2110
  }
2108
2111
  };
2109
2112
 
2113
+ // src/server/access/kinds.ts
2114
+ function isKindList(value) {
2115
+ return Array.isArray(value) && value.length > 0 && value.every((kind) => typeof kind === "string" && kind.length > 0);
2116
+ }
2117
+ function narrowKinds(value, above, fail5) {
2118
+ if (value === void 0) {
2119
+ return above?.kinds;
2120
+ }
2121
+ if (!isKindList(value)) {
2122
+ fail5(
2123
+ 'kinds must be a non-empty list of principal kinds (strings), such as ["user"]; leave it out to admit every kind'
2124
+ );
2125
+ }
2126
+ if (above?.kinds !== void 0) {
2127
+ const { kinds, by } = above;
2128
+ const wider = value.find((kind) => !kinds.includes(kind));
2129
+ if (wider !== void 0) {
2130
+ fail5(
2131
+ `kinds may only narrow the kinds ${by} admits (${kinds.join(", ")}), and "${wider}" is not one of them`
2132
+ );
2133
+ }
2134
+ }
2135
+ return Object.freeze([...new Set(value)]);
2136
+ }
2137
+ function checkKind(kinds, principal, what) {
2138
+ if (kinds === void 0 || principal === null) {
2139
+ return;
2140
+ }
2141
+ const { kind } = principal;
2142
+ if (typeof kind === "string" && kinds.includes(kind)) {
2143
+ return;
2144
+ }
2145
+ throw new QuickdrawError(
2146
+ "FORBIDDEN",
2147
+ kind === void 0 ? `${what} is not open to a principal without a kind` : `${what} is not open to principals of kind "${String(kind)}"`
2148
+ );
2149
+ }
2150
+
2110
2151
  // src/server/realtime/streamTargets.ts
2111
2152
  function isName(value) {
2112
2153
  return typeof value === "string" && value.length > 0;
@@ -2153,6 +2194,7 @@ function inStreamRoom(socket, access, scope) {
2153
2194
  return room !== void 0 && joined !== void 0 && Object.hasOwn(joined, room);
2154
2195
  }
2155
2196
  async function authorizeStream(hub, socket, target2) {
2197
+ checkKind(target2.service.kinds, socket.data.principal, target2.service.name);
2156
2198
  const { room } = target2.stream;
2157
2199
  if (room !== void 0) {
2158
2200
  if (!inStreamRoom(socket, room, target2.scope)) {
@@ -4369,6 +4411,8 @@ function mayWatchService(service, principal) {
4369
4411
  return meetsLevel(serviceGrant(principal, service.name), form.service);
4370
4412
  }
4371
4413
  async function authorizeWatch(hub, principal, target2) {
4414
+ const service = target2.kind === "service" ? target2.service : target2.collection.service;
4415
+ checkKind(service.kinds, principal, service.name);
4372
4416
  if (target2.kind === "service" && target2.service.watchAccess === void 0) {
4373
4417
  throw new QuickdrawError(
4374
4418
  "FORBIDDEN",
@@ -4646,6 +4690,7 @@ function collectionSubscriptions(hub) {
4646
4690
  answerEvent(socket, context, CLIENT_EVENTS.collectionSub, async (frame) => {
4647
4691
  const request = readSubscribe(frame);
4648
4692
  const { collection } = servedCollection(hub, socket, request);
4693
+ checkKind(collection.service.kinds, socket.data.principal, collection.service.name);
4649
4694
  return await subscribeScope(hub, socket, collection, request);
4650
4695
  });
4651
4696
  answerEvent(socket, context, CLIENT_EVENTS.collectionItems, async (frame) => {
@@ -5297,13 +5342,18 @@ function createRealtime(hub) {
5297
5342
  rooms.disconnected(socket);
5298
5343
  });
5299
5344
  },
5345
+ legacyExtension(socket, context) {
5346
+ onDisconnect(socket, context, () => {
5347
+ rooms.disconnected(socket);
5348
+ });
5349
+ },
5300
5350
  presence,
5301
5351
  rooms: rooms.server,
5302
5352
  onRoomLeave: (hooks, run2) => {
5303
5353
  rooms.onLeave(hooks, run2);
5304
5354
  },
5305
5355
  roomsFor(transport, connectionId, share) {
5306
- const socket = transport === "socket" && connectionId !== void 0 ? hub.io?.sockets.sockets.get(connectionId) : void 0;
5356
+ const socket = (transport === "socket" || transport === "legacy") && connectionId !== void 0 ? hub.io?.sockets.sockets.get(connectionId) : void 0;
5307
5357
  const own = socket === void 0 ? rooms.detached : rooms.of(socket);
5308
5358
  return share === void 0 ? own : unjoinable(own, share);
5309
5359
  },
@@ -5909,6 +5959,7 @@ async function onSubscribe(hub, socket, frame) {
5909
5959
  if (socket.data.principal === null) {
5910
5960
  throw new QuickdrawError("UNAUTHENTICATED", "Authentication required");
5911
5961
  }
5962
+ checkKind(target2.service.kinds, socket.data.principal, target2.service.name);
5912
5963
  if (hub.storage === void 0) {
5913
5964
  throw new QuickdrawError(
5914
5965
  "INTERNAL",
@@ -6155,6 +6206,7 @@ function createLive(options) {
6155
6206
  topics.extension(...args);
6156
6207
  realtime.extension(...args);
6157
6208
  },
6209
+ legacyExtension: realtime.legacyExtension,
6158
6210
  realtime,
6159
6211
  revisions: hub.revisions,
6160
6212
  attach(io, probe, cluster, broadcasts) {
@@ -7343,7 +7395,7 @@ function contextFor(settings, request, requestId, target2, signal, dispatch) {
7343
7395
  log,
7344
7396
  requestId,
7345
7397
  transport: request.transport,
7346
- ...request.transport === "socket" && request.connectionId !== void 0 ? { socketId: request.connectionId } : {},
7398
+ ...(request.transport === "socket" || request.transport === "legacy") && request.connectionId !== void 0 ? { socketId: request.connectionId } : {},
7347
7399
  ...request.mcp === void 0 ? {} : { mcp: request.mcp },
7348
7400
  touch: settings.touch,
7349
7401
  rooms: realtime.roomsFor(request.transport, request.connectionId, target2.method.share),
@@ -7391,6 +7443,7 @@ async function proceed(pipeline, call, target2) {
7391
7443
  const { request } = call;
7392
7444
  const label = `${target2.service.name}.${target2.method.name}`;
7393
7445
  const input = await stage(call, parseInput(target2.method.input, request.input, label));
7446
+ checkKind(target2.method.kinds, request.principal, label);
7394
7447
  const ctx = contextFor(settings, request, call.requestId, target2, call.signal, pipeline.dispatch);
7395
7448
  const access = {
7396
7449
  service: target2.service,
@@ -8278,17 +8331,58 @@ function toLegacyReply(result) {
8278
8331
  return { success: true, data: result.notModified === true ? void 0 : result.data };
8279
8332
  }
8280
8333
  var LEGACY_INTERNAL = Object.freeze(legacyFailure("INTERNAL", "Internal error"));
8281
- function targetOf5(event, registry) {
8334
+ function resolveLegacyWire(option, registered) {
8335
+ if (option === void 0 || option === false) {
8336
+ return void 0;
8337
+ }
8338
+ if (option === true) {
8339
+ return { aliases: /* @__PURE__ */ new Map() };
8340
+ }
8341
+ if (typeof option !== "object" || option === null || Array.isArray(option)) {
8342
+ throw new TypeError("createServer: legacyWire must be a boolean or { aliases }");
8343
+ }
8344
+ for (const key of Object.keys(option)) {
8345
+ if (key !== "aliases") {
8346
+ throw new TypeError(`createServer: legacyWire has an unknown option "${key}"`);
8347
+ }
8348
+ }
8349
+ const { aliases = {} } = option;
8350
+ if (typeof aliases !== "object" || aliases === null || Array.isArray(aliases)) {
8351
+ throw new TypeError("createServer: legacyWire.aliases must map old service names to new ones");
8352
+ }
8353
+ const services = new Set(registered.map((service) => service.name));
8354
+ const resolved = /* @__PURE__ */ new Map();
8355
+ for (const [alias, target2] of Object.entries(aliases)) {
8356
+ const entry = `legacyWire.aliases[${JSON.stringify(alias)}]`;
8357
+ if (alias === "" || alias.includes(":")) {
8358
+ throw new TypeError(`createServer: ${entry} must be a service name without ":"`);
8359
+ }
8360
+ if (services.has(alias)) {
8361
+ throw new TypeError(
8362
+ `createServer: ${entry} is a registered service's own name, so it cannot be an alias`
8363
+ );
8364
+ }
8365
+ if (typeof target2 !== "string" || !services.has(target2)) {
8366
+ throw new TypeError(
8367
+ `createServer: ${entry} names ${JSON.stringify(target2)}, which is not a registered service`
8368
+ );
8369
+ }
8370
+ resolved.set(alias, target2);
8371
+ }
8372
+ return { aliases: resolved };
8373
+ }
8374
+ function targetOf5(event, registry, aliases) {
8282
8375
  if (typeof event !== "string") {
8283
8376
  return void 0;
8284
8377
  }
8285
8378
  const colon = event.indexOf(":");
8286
- const service = event.slice(0, colon);
8379
+ const called = event.slice(0, colon);
8287
8380
  const method = event.slice(colon + 1);
8288
- if (colon <= 0 || registry.find(service, method) === void 0) {
8381
+ const service = aliases.get(called);
8382
+ if (colon <= 0 || registry.find(service ?? called, method) === void 0) {
8289
8383
  return void 0;
8290
8384
  }
8291
- return { service, method };
8385
+ return service === void 0 ? { service: called, method } : { service, method, alias: called };
8292
8386
  }
8293
8387
  function principalKind(principal) {
8294
8388
  if (principal === null) {
@@ -8298,15 +8392,18 @@ function principalKind(principal) {
8298
8392
  }
8299
8393
  function noteCaller(context, seen, target2, principal) {
8300
8394
  const kind = principalKind(principal);
8301
- const key = `${target2.service}\0${target2.method}\0${kind}`;
8395
+ const { service, method, alias } = target2;
8396
+ const key = `${alias ?? service}\0${method}\0${kind}`;
8302
8397
  if (seen.has(key)) {
8303
8398
  return;
8304
8399
  }
8305
8400
  seen.add(key);
8306
- context.logger.warn(
8307
- `A 4.x client called ${target2.service}.${target2.method} through the legacy shim`,
8308
- { category: "quickdraw.legacy", ...target2, principalKind: kind }
8309
- );
8401
+ const as = alias === void 0 ? "" : ` as ${alias}.${method}`;
8402
+ context.logger.warn(`A 4.x client called ${service}.${method}${as} through the legacy shim`, {
8403
+ category: "quickdraw.legacy",
8404
+ ...target2,
8405
+ principalKind: kind
8406
+ });
8310
8407
  }
8311
8408
  function serve2(socket, context, calls, call) {
8312
8409
  const controller = new AbortController();
@@ -8332,19 +8429,20 @@ function serve2(socket, context, calls, call) {
8332
8429
  respond: (result) => acknowledge(context.meter, call.ack, toLegacyReply(result), LEGACY_INTERNAL, report)
8333
8430
  }).then(release3, release3);
8334
8431
  }
8335
- function attachLegacyShim(socket, context, seen) {
8432
+ function attachLegacyShim(socket, context, seen, wire) {
8336
8433
  const calls = /* @__PURE__ */ new Set();
8337
8434
  socket.use((packet, next) => {
8338
8435
  const [event, ...args] = packet;
8339
8436
  const ack = args.at(-1);
8340
- const target2 = targetOf5(event, context.dispatcher.registry);
8437
+ const target2 = targetOf5(event, context.dispatcher.registry, wire.aliases);
8341
8438
  if (target2 === void 0 || typeof ack !== "function") {
8342
8439
  next();
8343
8440
  return;
8344
8441
  }
8345
8442
  noteCaller(context, seen, target2, socket.data.principal);
8346
8443
  const input = args.length > 1 ? args[0] : void 0;
8347
- serve2(socket, context, calls, { ...target2, input, ack });
8444
+ const { service, method } = target2;
8445
+ serve2(socket, context, calls, { service, method, input, ack });
8348
8446
  });
8349
8447
  socket.on("disconnect", () => {
8350
8448
  for (const controller of calls) {
@@ -8806,7 +8904,10 @@ function onConnection(settings) {
8806
8904
  void socket.join(userRoom(principal.userId));
8807
8905
  }
8808
8906
  if (socket.data.protocol === "legacy") {
8809
- attachLegacyShim(socket, settings, settings.legacyCallers);
8907
+ attachLegacyShim(socket, settings, settings.legacyCallers, settings.legacyWire);
8908
+ for (const extension of settings.legacyExtensions) {
8909
+ extension(socket, settings);
8910
+ }
8810
8911
  return;
8811
8912
  }
8812
8913
  attachCallListeners(socket, settings);
@@ -8862,7 +8963,7 @@ function createSocketServer(httpServer, settings) {
8862
8963
  settings.live?.attach(io, probe, settings.cluster, broadcasts);
8863
8964
  listenForGrants(io, settings.live, settings.logger);
8864
8965
  listenForDisconnects(io);
8865
- io.use(protocolMiddleware(settings.legacyWire, context));
8966
+ io.use(protocolMiddleware(settings.legacyWire !== void 0, context));
8866
8967
  io.use(authMiddleware(settings.resolvePrincipal, context));
8867
8968
  if (settings.rateLimit !== false) {
8868
8969
  applySocketRateLimit(io, settings.rateLimit, { ...context, loops: settings.loops });
@@ -8873,7 +8974,10 @@ function createSocketServer(httpServer, settings) {
8873
8974
  ...context,
8874
8975
  hello: helloFrame(settings),
8875
8976
  extensions: settings.live === void 0 ? settings.extensions : [...settings.extensions, settings.live.extension],
8876
- legacyCallers: /* @__PURE__ */ new Set()
8977
+ legacyExtensions: settings.live === void 0 ? [] : [settings.live.legacyExtension],
8978
+ legacyCallers: /* @__PURE__ */ new Set(),
8979
+ // A server that refuses 4.x clients has no 4.x socket to read it.
8980
+ legacyWire: settings.legacyWire ?? { aliases: /* @__PURE__ */ new Map() }
8877
8981
  })
8878
8982
  );
8879
8983
  return {
@@ -8962,6 +9066,7 @@ function warnUnsourcedGrants(services, sourced, logger) {
8962
9066
  }
8963
9067
  function createServer(options) {
8964
9068
  checkOptions(options);
9069
+ const legacyWire = resolveLegacyWire(options.legacyWire, options.services);
8965
9070
  const logger = options.logger ?? consoleLogger;
8966
9071
  warnUnsourcedGrants(options.services, options.auth?.serviceAccessSource !== void 0, logger);
8967
9072
  const watchdog = prepareWatchdog(options, options.stallWatchdog);
@@ -8982,7 +9087,7 @@ function createServer(options) {
8982
9087
  resolvePrincipal,
8983
9088
  loadServiceAccess: options.auth?.loadServiceAccess,
8984
9089
  binary: options.binary === true,
8985
- legacyWire: options.legacyWire === true,
9090
+ legacyWire,
8986
9091
  cors: options.cors,
8987
9092
  socket: options.socket,
8988
9093
  rateLimit: options.rateLimit ?? {},
@@ -9039,6 +9144,7 @@ export {
9039
9144
  custom,
9040
9145
  isCustomAccess,
9041
9146
  accessFormProblem,
9147
+ accessIds,
9042
9148
  meetsLevel,
9043
9149
  maxLevel,
9044
9150
  levelsAtLeast,
@@ -9070,6 +9176,7 @@ export {
9070
9176
  MAX_ITEM_IDS,
9071
9177
  selectWith,
9072
9178
  INDEX_MAX_ROWS,
9179
+ narrowKinds,
9073
9180
  allowedScopes,
9074
9181
  membersWhere,
9075
9182
  CHANNEL_ABUSE_WINDOW_MS,
@@ -9099,4 +9206,4 @@ export {
9099
9206
  watchAdapterClients,
9100
9207
  createServer
9101
9208
  };
9102
- //# sourceMappingURL=chunk-NS5PUHIZ.js.map
9209
+ //# sourceMappingURL=chunk-A6DJBGOG.js.map