@fitzzero/quickdraw-core 5.0.0 → 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 (68) hide show
  1. package/README.md +343 -49
  2. package/dist/{admin-BiU4LRZS.d.ts → admin-DKwkIh5i.d.ts} +25 -10
  3. package/dist/{adminSchemas-k5a1Ov7b.d.ts → adminSchemas-2DUA_hZ8.d.ts} +15 -9
  4. package/dist/{auth-CMyRkfjZ.d.ts → auth-BhWqdyb-.d.ts} +2 -2
  5. package/dist/{caller-DHzK3oiI.d.ts → caller-CXTFswBO.d.ts} +28 -6
  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-5IC5VZ7T.js → chunk-2UELNHDD.js} +3 -3
  9. package/dist/{chunk-XFDOGTMC.js → chunk-3674EIRT.js} +4 -4
  10. package/dist/{chunk-DNCAI3KY.js → chunk-4DIQHASK.js} +1 -1
  11. package/dist/chunk-4DIQHASK.js.map +1 -0
  12. package/dist/{chunk-LLLYILEP.js → chunk-A6DJBGOG.js} +411 -43
  13. package/dist/chunk-A6DJBGOG.js.map +1 -0
  14. package/dist/{chunk-B2UQ27GH.js → chunk-CDKG4TFA.js} +25 -5
  15. package/dist/{chunk-B2UQ27GH.js.map → chunk-CDKG4TFA.js.map} +1 -1
  16. package/dist/{chunk-IRGAGRVG.js → chunk-JZBCLNDU.js} +4 -4
  17. package/dist/{chunk-XUSN5WKU.js → chunk-KNDDCVQJ.js} +4 -4
  18. package/dist/{chunk-5YRQ26LF.js → chunk-RT6OILOI.js} +1 -1
  19. package/dist/chunk-RT6OILOI.js.map +1 -0
  20. package/dist/{chunk-7QIY2DDX.js → chunk-VJTFXHOK.js} +2 -2
  21. package/dist/{chunk-7QIY2DDX.js.map → chunk-VJTFXHOK.js.map} +1 -1
  22. package/dist/chunk-XNLWBZIG.js +9 -0
  23. package/dist/chunk-XNLWBZIG.js.map +1 -0
  24. package/dist/cli/quickdraw-docs.js +241 -27
  25. package/dist/cli/quickdraw-docs.js.map +1 -1
  26. package/dist/client/index.d.ts +7 -7
  27. package/dist/client/index.js +5 -5
  28. package/dist/{clientTypes-B3o_99Xq.d.ts → clientTypes-Bc-G8n-U.d.ts} +3 -3
  29. package/dist/{connection-DuN8GvcD.d.ts → connection-Cr1BU07W.d.ts} +2 -2
  30. package/dist/{createServer-DUacdfKl.d.ts → createServer-CP9eDHZZ.d.ts} +30 -9
  31. package/dist/{createTestApp-DFLXoTKm.d.ts → createTestApp-UKVGEmOf.d.ts} +6 -6
  32. package/dist/{envelope-nf5eGB-a.d.ts → envelope-Cp3YcVxC.d.ts} +1 -1
  33. package/dist/{http--uoooIH5.d.ts → http-Djj2Inuu.d.ts} +6 -6
  34. package/dist/index.d.ts +6 -6
  35. package/dist/index.js +27 -7
  36. package/dist/index.js.map +1 -1
  37. package/dist/{infer-Dw21kmLw.d.ts → infer-Teo1xpsd.d.ts} +17 -0
  38. package/dist/{metrics-C0WvB7-I.d.ts → metrics-BdBu5o8G.d.ts} +2 -2
  39. package/dist/prisma/index.d.ts +4 -4
  40. package/dist/prisma/index.js +1 -1
  41. package/dist/server/auth/index.d.ts +14 -5
  42. package/dist/server/auth/index.js +0 -5
  43. package/dist/server/auth/index.js.map +1 -1
  44. package/dist/server/index.d.ts +120 -33
  45. package/dist/server/index.js +146 -193
  46. package/dist/server/index.js.map +1 -1
  47. package/dist/server/mcp/index.d.ts +51 -11
  48. package/dist/server/mcp/index.js +129 -8
  49. package/dist/server/mcp/index.js.map +1 -1
  50. package/dist/server/otel.d.ts +3 -3
  51. package/dist/{sharing-n4ss-ONM.d.ts → sharing-DVa8W-yR.d.ts} +3 -3
  52. package/dist/testing/client.d.ts +13 -13
  53. package/dist/testing/client.js +6 -6
  54. package/dist/testing/index.d.ts +156 -12
  55. package/dist/testing/index.js +1037 -22
  56. package/dist/testing/index.js.map +1 -1
  57. package/dist/testing/mock.d.ts +5 -5
  58. package/dist/testing/mock.js +5 -5
  59. package/dist/{types-Bwg3kIEc.d.ts → types-Cu0vmx7c.d.ts} +8 -2
  60. package/dist/utils/index.d.ts +1 -1
  61. package/package.json +1 -1
  62. package/dist/chunk-5YRQ26LF.js.map +0 -1
  63. package/dist/chunk-DNCAI3KY.js.map +0 -1
  64. package/dist/chunk-LLLYILEP.js.map +0 -1
  65. /package/dist/{chunk-5IC5VZ7T.js.map → chunk-2UELNHDD.js.map} +0 -0
  66. /package/dist/{chunk-XFDOGTMC.js.map → chunk-3674EIRT.js.map} +0 -0
  67. /package/dist/{chunk-IRGAGRVG.js.map → chunk-JZBCLNDU.js.map} +0 -0
  68. /package/dist/{chunk-XUSN5WKU.js.map → chunk-KNDDCVQJ.js.map} +0 -0
@@ -10,7 +10,7 @@ import {
10
10
  quietly,
11
11
  strictWarningsOf,
12
12
  versionTime
13
- } from "./chunk-DNCAI3KY.js";
13
+ } from "./chunk-4DIQHASK.js";
14
14
  import {
15
15
  isCallEnvelope,
16
16
  isCancel
@@ -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-7QIY2DDX.js";
80
+ } from "./chunk-VJTFXHOK.js";
81
81
  import {
82
82
  hasJsonSchema
83
83
  } from "./chunk-47AEXVDM.js";
@@ -424,9 +424,9 @@ function addBranchKeys(node, root, keys, seen) {
424
424
  }
425
425
  }
426
426
  for (const keyword of BRANCHES) {
427
- const members = target2[keyword];
428
- if (Array.isArray(members)) {
429
- for (const member of members) {
427
+ const members2 = target2[keyword];
428
+ if (Array.isArray(members2)) {
429
+ for (const member of members2) {
430
430
  addBranchKeys(member, root, keys, seen);
431
431
  }
432
432
  }
@@ -673,12 +673,12 @@ async function membersAmong(storage, collection, scope, ids) {
673
673
  }
674
674
  async function readItemsById(storage, collection, scope, ids, rev) {
675
675
  const wanted = [...new Set(ids)];
676
- const members = wanted.length === 0 ? void 0 : await membersAmong(storage, collection, scope, wanted);
677
- if (members === void 0) {
676
+ const members2 = wanted.length === 0 ? void 0 : await membersAmong(storage, collection, scope, wanted);
677
+ if (members2 === void 0) {
678
678
  return { rev, items: [] };
679
679
  }
680
680
  const rows = await storage.findMany(collection.model, {
681
- where: members,
681
+ where: members2,
682
682
  select: collection.item.select
683
683
  });
684
684
  const found = new Map(rows.flatMap((row) => typeof row.id === "string" ? [[row.id, row]] : []));
@@ -792,10 +792,10 @@ function indexRowFrom(collection, item, row, rev) {
792
792
  const id = typeof row.id === "string" ? row.id : "";
793
793
  return [id, version ?? rev, ...(collection.index ?? []).map((field) => values[field] ?? null)];
794
794
  }
795
- async function readIndex(storage, collection, members, nullable, rev, max = INDEX_MAX_ROWS) {
795
+ async function readIndex(storage, collection, members2, nullable, rev, max = INDEX_MAX_ROWS) {
796
796
  const fields = collection.index ?? [];
797
797
  const rows = await storage.findMany(collection.model, {
798
- where: members,
798
+ where: members2,
799
799
  select: indexSelect(collection, fields),
800
800
  orderBy: orderByOf(collection.order, nullable),
801
801
  take: max + 1
@@ -1056,8 +1056,17 @@ function bindingOf(state, contract) {
1056
1056
  }
1057
1057
  return binding;
1058
1058
  }
1059
+ function sameRead(a, b) {
1060
+ const columns = ["model", "entry", "user", "level"].every((key) => a[key] === b[key]);
1061
+ if (!columns || a.levels === void 0 || b.levels === void 0) {
1062
+ return columns && a.levels === b.levels;
1063
+ }
1064
+ const mapped = b.levels;
1065
+ const roles = Object.entries(a.levels);
1066
+ return roles.length === Object.keys(mapped).length && roles.every(([role, level]) => Object.hasOwn(mapped, role) && mapped[role] === level);
1067
+ }
1059
1068
  function membershipScope(scope, binding, read) {
1060
- const declared = binding.memberships.get(read);
1069
+ const declared = binding.memberships.get(read) ?? [...binding.memberships].find(([known]) => sameRead(known, read))?.[1];
1061
1070
  if (declared !== void 0) {
1062
1071
  return [scope.cached, declared];
1063
1072
  }
@@ -1538,8 +1547,11 @@ function receive(deps, state, frame) {
1538
1547
  return;
1539
1548
  }
1540
1549
  const { principal } = state.socket.data;
1541
- const parsed = principal === null ? void 0 : parse(deps, channel, payload);
1542
- 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) {
1543
1555
  return;
1544
1556
  }
1545
1557
  const room = allowed(channel, state.socket, principal, parsed.value);
@@ -2098,6 +2110,44 @@ var StreamIndex = class {
2098
2110
  }
2099
2111
  };
2100
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
+
2101
2151
  // src/server/realtime/streamTargets.ts
2102
2152
  function isName(value) {
2103
2153
  return typeof value === "string" && value.length > 0;
@@ -2144,6 +2194,7 @@ function inStreamRoom(socket, access, scope) {
2144
2194
  return room !== void 0 && joined !== void 0 && Object.hasOwn(joined, room);
2145
2195
  }
2146
2196
  async function authorizeStream(hub, socket, target2) {
2197
+ checkKind(target2.service.kinds, socket.data.principal, target2.service.name);
2147
2198
  const { room } = target2.stream;
2148
2199
  if (room !== void 0) {
2149
2200
  if (!inStreamRoom(socket, room, target2.scope)) {
@@ -2336,6 +2387,220 @@ function createBasicAccessEngine(options = {}) {
2336
2387
  });
2337
2388
  }
2338
2389
 
2390
+ // src/server/access/policies/anyOf.ts
2391
+ function unionReads(policies) {
2392
+ const memberships = /* @__PURE__ */ new Set();
2393
+ for (const policy of policies) {
2394
+ for (const read of policy.reads.memberships) {
2395
+ memberships.add(read);
2396
+ }
2397
+ }
2398
+ return {
2399
+ columns: [...new Set(policies.flatMap((policy) => policy.reads.columns))],
2400
+ memberships: [...memberships],
2401
+ inherits: [...new Set(policies.flatMap((policy) => policy.reads.inherits))],
2402
+ parents: policies.flatMap((policy) => policy.reads.parents ?? []),
2403
+ storage: policies.some((policy) => policy.reads.storage)
2404
+ };
2405
+ }
2406
+ var MEMBERS = /* @__PURE__ */ new WeakMap();
2407
+ function anyOfPolicies(policy) {
2408
+ return MEMBERS.get(policy);
2409
+ }
2410
+ function anyOf(...policies) {
2411
+ if (policies.length === 0 || !policies.every(isAccessPolicy)) {
2412
+ throw new TypeError("anyOf(...policies): pass one or more access policies");
2413
+ }
2414
+ const all = Object.freeze([...policies]);
2415
+ const combined = definePolicy({
2416
+ kind: "anyOf",
2417
+ policies: all,
2418
+ reads: unionReads(all),
2419
+ async levelsFor(principal, ids, tools) {
2420
+ const each = await Promise.all(all.map((policy) => policy.levelsFor(principal, ids, tools)));
2421
+ const levels = /* @__PURE__ */ new Map();
2422
+ for (const id of ids) {
2423
+ levels.set(
2424
+ id,
2425
+ each.reduce((level, granted) => maxLevel(level, granted.get(id)), null)
2426
+ );
2427
+ }
2428
+ return levels;
2429
+ },
2430
+ async accessWhere(principal, level, tools) {
2431
+ const filters = await Promise.all(
2432
+ all.map((policy) => policy.accessWhere(principal, level, tools))
2433
+ );
2434
+ const matching = filters.filter((filter) => filter !== "none");
2435
+ if (matching.some((filter) => Object.keys(filter).length === 0)) {
2436
+ return {};
2437
+ }
2438
+ if (matching.length <= 1) {
2439
+ return matching[0] ?? "none";
2440
+ }
2441
+ return { OR: matching };
2442
+ }
2443
+ });
2444
+ MEMBERS.set(combined, all);
2445
+ return combined;
2446
+ }
2447
+
2448
+ // src/server/access/policies/members.ts
2449
+ function membershipOf(policy) {
2450
+ return policy.kind === "members" ? policy.reads.memberships[0] : void 0;
2451
+ }
2452
+ function checkLevels(owner, levels) {
2453
+ if (levels === void 0) {
2454
+ return void 0;
2455
+ }
2456
+ const valid = typeof levels === "object" && levels !== null && !Array.isArray(levels) && Object.values(levels).every(isAccessLevel);
2457
+ if (!valid) {
2458
+ throw new TypeError(
2459
+ `${owner}: levels must map stored roles to access levels, as in { editor: "Moderate" }`
2460
+ );
2461
+ }
2462
+ return Object.freeze({ ...levels });
2463
+ }
2464
+ function membershipRead(options, owner) {
2465
+ const { model, entry, user, level, levels } = options;
2466
+ return Object.freeze({
2467
+ model: checkName(owner("model"), "model", model),
2468
+ entry: checkName(owner("entry"), "entry", entry),
2469
+ user: checkName(owner("user"), "user", user),
2470
+ level: checkName(owner("level"), "level", level),
2471
+ levels: checkLevels(owner("levels"), levels)
2472
+ });
2473
+ }
2474
+ function rolesAtLeast(read, level) {
2475
+ const enough = new Set(levelsAtLeast(level));
2476
+ if (read.levels === void 0) {
2477
+ return [...enough];
2478
+ }
2479
+ return Object.entries(read.levels).filter(([, mapped]) => enough.has(mapped)).map(([role]) => role);
2480
+ }
2481
+ function members(options) {
2482
+ if (typeof options !== "object" || options === null) {
2483
+ throw new TypeError("members(options): options must be { model, entry, user, level }");
2484
+ }
2485
+ const read = membershipRead(options, (option) => `members({ ${option} })`);
2486
+ return definePolicy({
2487
+ kind: "members",
2488
+ membership: read,
2489
+ reads: { columns: [], memberships: [read], inherits: [], storage: true },
2490
+ levelsFor: (principal, ids, tools) => tools.memberships(read, principal.userId, ids),
2491
+ async accessWhere(principal, level, tools) {
2492
+ const roles = rolesAtLeast(read, level);
2493
+ if (roles.length === 0) {
2494
+ return "none";
2495
+ }
2496
+ const rows = await tools.storage.findMany(read.model, {
2497
+ where: { [read.user]: principal.userId, [read.level]: { in: roles } },
2498
+ select: { [read.entry]: true }
2499
+ });
2500
+ const entries = rows.map((row) => row[read.entry]);
2501
+ return idFilter(entries.filter((entry) => typeof entry === "string"));
2502
+ }
2503
+ });
2504
+ }
2505
+
2506
+ // src/server/access/policies/resolver.ts
2507
+ var UNDECLARED = /* @__PURE__ */ new WeakSet();
2508
+ function declaresNoReads(policy) {
2509
+ return UNDECLARED.has(policy);
2510
+ }
2511
+ function noReads() {
2512
+ return { columns: [], memberships: [], inherits: [], storage: false };
2513
+ }
2514
+ var READ_KEYS = ["columns", "memberships"];
2515
+ function readsProblem(message) {
2516
+ throw new TypeError(`resolver({ reads }): ${message}`);
2517
+ }
2518
+ function checkColumns(columns) {
2519
+ if (columns === void 0) {
2520
+ return [];
2521
+ }
2522
+ const valid = Array.isArray(columns) && columns.every((column) => typeof column === "string" && column.length > 0);
2523
+ return valid ? Object.freeze([...new Set(columns)]) : readsProblem("columns must be a list of column names");
2524
+ }
2525
+ function checkMemberships(memberships) {
2526
+ if (memberships === void 0) {
2527
+ return [];
2528
+ }
2529
+ const valid = Array.isArray(memberships) && memberships.every((each) => typeof each === "object" && each !== null && !Array.isArray(each));
2530
+ if (!valid) {
2531
+ readsProblem("memberships must be a list of { model, entry, user, level, levels? }");
2532
+ }
2533
+ return Object.freeze(
2534
+ memberships.map(
2535
+ (each) => membershipRead(each, () => "resolver({ reads: { memberships } })")
2536
+ )
2537
+ );
2538
+ }
2539
+ function checkReads(reads) {
2540
+ if (reads === void 0) {
2541
+ return void 0;
2542
+ }
2543
+ if (reads === "none") {
2544
+ return noReads();
2545
+ }
2546
+ if (typeof reads !== "object" || reads === null || Array.isArray(reads)) {
2547
+ readsProblem('reads must be { columns?, memberships? } or "none"');
2548
+ }
2549
+ const given = reads;
2550
+ const unknownKey = Object.keys(given).find((key) => !READ_KEYS.includes(key));
2551
+ if (unknownKey !== void 0) {
2552
+ readsProblem(`reads has an unknown key "${unknownKey}"; the keys are columns and memberships`);
2553
+ }
2554
+ const columns = checkColumns(given.columns);
2555
+ const memberships = checkMemberships(given.memberships);
2556
+ if (columns.length === 0 && memberships.length === 0) {
2557
+ readsProblem('name a column or a membership table, or pass "none"');
2558
+ }
2559
+ return { columns, memberships, inherits: [], storage: true };
2560
+ }
2561
+ function levelIn(levels, id) {
2562
+ let level;
2563
+ if (levels instanceof Map) {
2564
+ level = levels.get(id);
2565
+ } else if (Object.hasOwn(levels, id)) {
2566
+ level = levels[id];
2567
+ }
2568
+ return isAccessLevel(level) ? level : null;
2569
+ }
2570
+ function resolver(options) {
2571
+ const valid = typeof options === "object" && options !== null && typeof options.levelsFor === "function" && (options.where === void 0 || typeof options.where === "function");
2572
+ if (!valid) {
2573
+ throw new TypeError(
2574
+ "resolver({ levelsFor, where?, reads? }): levelsFor and where must be functions"
2575
+ );
2576
+ }
2577
+ const { levelsFor, where: where2 } = options;
2578
+ const reads = checkReads(options.reads);
2579
+ const policy = definePolicy({
2580
+ kind: "resolver",
2581
+ reads: reads ?? noReads(),
2582
+ async levelsFor(principal, ids, tools) {
2583
+ const levels = await levelsFor(principal, ids, tools);
2584
+ if (typeof levels !== "object" || levels === null) {
2585
+ return levelsById(ids, () => null);
2586
+ }
2587
+ return levelsById(ids, (id) => levelIn(levels, id));
2588
+ },
2589
+ async accessWhere(principal, level, tools) {
2590
+ if (where2 === void 0) {
2591
+ return "none";
2592
+ }
2593
+ const filter = await where2(principal, level, tools);
2594
+ const isFilter = typeof filter === "object" && filter !== null && !Array.isArray(filter);
2595
+ return isFilter ? filter : "none";
2596
+ }
2597
+ });
2598
+ if (reads === void 0) {
2599
+ UNDECLARED.add(policy);
2600
+ }
2601
+ return policy;
2602
+ }
2603
+
2339
2604
  // src/server/collections/scopes.ts
2340
2605
  function serviceOf2(anchor) {
2341
2606
  return anchor.slice(0, anchor.indexOf("\0"));
@@ -4146,6 +4411,8 @@ function mayWatchService(service, principal) {
4146
4411
  return meetsLevel(serviceGrant(principal, service.name), form.service);
4147
4412
  }
4148
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);
4149
4416
  if (target2.kind === "service" && target2.service.watchAccess === void 0) {
4150
4417
  throw new QuickdrawError(
4151
4418
  "FORBIDDEN",
@@ -4211,9 +4478,9 @@ async function nullableColumns(storage, collection, values) {
4211
4478
  return nullable;
4212
4479
  }
4213
4480
  async function readRows4(storage, collection, read) {
4214
- const { members, values, nullable, limit } = read;
4481
+ const { members: members2, values, nullable, limit } = read;
4215
4482
  const { order } = collection;
4216
- const where2 = values === void 0 ? members : { AND: [members, afterCursor(order, values, nullable)] };
4483
+ const where2 = values === void 0 ? members2 : { AND: [members2, afterCursor(order, values, nullable)] };
4217
4484
  const [rows, total] = await Promise.allSettled([
4218
4485
  storage.findMany(collection.model, {
4219
4486
  where: where2,
@@ -4224,7 +4491,7 @@ async function readRows4(storage, collection, read) {
4224
4491
  orderBy: orderByOf(order, nullable),
4225
4492
  take: limit + 1
4226
4493
  }),
4227
- storage.count(collection.model, { where: members })
4494
+ storage.count(collection.model, { where: members2 })
4228
4495
  ]);
4229
4496
  if (total.status === "rejected") {
4230
4497
  throw total.reason;
@@ -4244,15 +4511,15 @@ async function readPage(storage, collection, request, rev) {
4244
4511
  const { order } = collection;
4245
4512
  const values = request.cursor === void 0 ? void 0 : decodeCursor(order, request.cursor);
4246
4513
  const nullable = await nullableColumns(storage, collection, values);
4247
- const members = await membersWhere(storage, collection, request.scope);
4514
+ const members2 = await membersWhere(storage, collection, request.scope);
4248
4515
  const page = { ok: true, rev, limit, ...clamped ? { clamped: true } : {} };
4249
4516
  const indexed = values === void 0 && collection.index !== void 0;
4250
- if (members === void 0) {
4517
+ if (members2 === void 0) {
4251
4518
  return { ...page, items: [], total: 0, cursor: null, ...indexed ? { index: [] } : {} };
4252
4519
  }
4253
4520
  const [{ rows, total }, index] = await Promise.all([
4254
- readRows4(storage, collection, { members, values, nullable, limit }),
4255
- indexed ? readIndex(storage, collection, members, nullable, rev) : void 0
4521
+ readRows4(storage, collection, { members: members2, values, nullable, limit }),
4522
+ indexed ? readIndex(storage, collection, members2, nullable, rev) : void 0
4256
4523
  ]);
4257
4524
  const shown = rows.slice(0, limit);
4258
4525
  const last = shown.at(-1);
@@ -4423,6 +4690,7 @@ function collectionSubscriptions(hub) {
4423
4690
  answerEvent(socket, context, CLIENT_EVENTS.collectionSub, async (frame) => {
4424
4691
  const request = readSubscribe(frame);
4425
4692
  const { collection } = servedCollection(hub, socket, request);
4693
+ checkKind(collection.service.kinds, socket.data.principal, collection.service.name);
4426
4694
  return await subscribeScope(hub, socket, collection, request);
4427
4695
  });
4428
4696
  answerEvent(socket, context, CLIENT_EVENTS.collectionItems, async (frame) => {
@@ -5074,13 +5342,18 @@ function createRealtime(hub) {
5074
5342
  rooms.disconnected(socket);
5075
5343
  });
5076
5344
  },
5345
+ legacyExtension(socket, context) {
5346
+ onDisconnect(socket, context, () => {
5347
+ rooms.disconnected(socket);
5348
+ });
5349
+ },
5077
5350
  presence,
5078
5351
  rooms: rooms.server,
5079
5352
  onRoomLeave: (hooks, run2) => {
5080
5353
  rooms.onLeave(hooks, run2);
5081
5354
  },
5082
5355
  roomsFor(transport, connectionId, share) {
5083
- 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;
5084
5357
  const own = socket === void 0 ? rooms.detached : rooms.of(socket);
5085
5358
  return share === void 0 ? own : unjoinable(own, share);
5086
5359
  },
@@ -5686,6 +5959,7 @@ async function onSubscribe(hub, socket, frame) {
5686
5959
  if (socket.data.principal === null) {
5687
5960
  throw new QuickdrawError("UNAUTHENTICATED", "Authentication required");
5688
5961
  }
5962
+ checkKind(target2.service.kinds, socket.data.principal, target2.service.name);
5689
5963
  if (hub.storage === void 0) {
5690
5964
  throw new QuickdrawError(
5691
5965
  "INTERNAL",
@@ -5932,6 +6206,7 @@ function createLive(options) {
5932
6206
  topics.extension(...args);
5933
6207
  realtime.extension(...args);
5934
6208
  },
6209
+ legacyExtension: realtime.legacyExtension,
5935
6210
  realtime,
5936
6211
  revisions: hub.revisions,
5937
6212
  attach(io, probe, cluster, broadcasts) {
@@ -6477,11 +6752,41 @@ function createPolicyEngine(options) {
6477
6752
  });
6478
6753
  }
6479
6754
 
6755
+ // src/server/access/resolverReads.ts
6756
+ function hasUndeclaredResolver(policy) {
6757
+ return declaresNoReads(policy) || (anyOfPolicies(policy)?.some(hasUndeclaredResolver) ?? false);
6758
+ }
6759
+ function resolverReadsWarnings(service) {
6760
+ const { access, model } = service;
6761
+ if (access === void 0 || model === void 0 || !hasUndeclaredResolver(access)) {
6762
+ return [];
6763
+ }
6764
+ return [
6765
+ {
6766
+ kind: "resolver-without-reads",
6767
+ service: service.name,
6768
+ message: `${service.name}: its access policy uses a resolver that declares no reads, so no tracked write re-checks the levels it gives: a live row, collection scope or change topic it authorized stays open after the user loses access. Declare what the levels depend on, resolver({ levelsFor, reads: { columns, memberships } }) (columns of the ${model} model, membership tables as members() takes them), or reads: "none" when nothing a tracked write changes can change them`,
6769
+ meta: { model }
6770
+ }
6771
+ ];
6772
+ }
6773
+ function warnResolverReads(registry, warnings) {
6774
+ if (!warnings.enabled) {
6775
+ return;
6776
+ }
6777
+ for (const service of registry.services.values()) {
6778
+ for (const warning of resolverReadsWarnings(service)) {
6779
+ warnings.warn(warning);
6780
+ }
6781
+ }
6782
+ }
6783
+
6480
6784
  // src/server/pipeline/accessSettings.ts
6481
6785
  function isAccessEngine(value) {
6482
6786
  return typeof value === "object" && value !== null && typeof value.authorize === "function";
6483
6787
  }
6484
- function resolveAccess2(access, registry, storage, logger) {
6788
+ function resolveAccess2(access, registry, storage, report) {
6789
+ const { logger, warnings } = report;
6485
6790
  if (access !== void 0 && (typeof access !== "object" || access === null)) {
6486
6791
  throw new TypeError("createDispatcher: access must be an access engine or { cacheMs }");
6487
6792
  }
@@ -6493,6 +6798,7 @@ function resolveAccess2(access, registry, storage, logger) {
6493
6798
  logger,
6494
6799
  cacheMs: accessOptions.cacheMs
6495
6800
  });
6801
+ warnResolverReads(registry, warnings);
6496
6802
  return { access: engine ?? createBasicAccessEngine({ rows: policies.rows }), policies };
6497
6803
  }
6498
6804
 
@@ -6626,7 +6932,10 @@ function resolveSettings(options, registry, db, accessSinks = []) {
6626
6932
  const warnings = createDevWarnings({ logger, development, strict: strictWarningsOf(options) });
6627
6933
  warnTieredOutputs(registry, warnings);
6628
6934
  const storage = storageFor(options, db);
6629
- const { access, policies } = resolveAccess2(options.access, registry, storage, logger);
6935
+ const { access, policies } = resolveAccess2(options.access, registry, storage, {
6936
+ logger,
6937
+ warnings
6938
+ });
6630
6939
  const outputValidation = options.outputValidation ?? development;
6631
6940
  const live = createLive({
6632
6941
  registry,
@@ -7086,7 +7395,7 @@ function contextFor(settings, request, requestId, target2, signal, dispatch) {
7086
7395
  log,
7087
7396
  requestId,
7088
7397
  transport: request.transport,
7089
- ...request.transport === "socket" && request.connectionId !== void 0 ? { socketId: request.connectionId } : {},
7398
+ ...(request.transport === "socket" || request.transport === "legacy") && request.connectionId !== void 0 ? { socketId: request.connectionId } : {},
7090
7399
  ...request.mcp === void 0 ? {} : { mcp: request.mcp },
7091
7400
  touch: settings.touch,
7092
7401
  rooms: realtime.roomsFor(request.transport, request.connectionId, target2.method.share),
@@ -7134,6 +7443,7 @@ async function proceed(pipeline, call, target2) {
7134
7443
  const { request } = call;
7135
7444
  const label = `${target2.service.name}.${target2.method.name}`;
7136
7445
  const input = await stage(call, parseInput(target2.method.input, request.input, label));
7446
+ checkKind(target2.method.kinds, request.principal, label);
7137
7447
  const ctx = contextFor(settings, request, call.requestId, target2, call.signal, pipeline.dispatch);
7138
7448
  const access = {
7139
7449
  service: target2.service,
@@ -8021,17 +8331,58 @@ function toLegacyReply(result) {
8021
8331
  return { success: true, data: result.notModified === true ? void 0 : result.data };
8022
8332
  }
8023
8333
  var LEGACY_INTERNAL = Object.freeze(legacyFailure("INTERNAL", "Internal error"));
8024
- 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) {
8025
8375
  if (typeof event !== "string") {
8026
8376
  return void 0;
8027
8377
  }
8028
8378
  const colon = event.indexOf(":");
8029
- const service = event.slice(0, colon);
8379
+ const called = event.slice(0, colon);
8030
8380
  const method = event.slice(colon + 1);
8031
- 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) {
8032
8383
  return void 0;
8033
8384
  }
8034
- return { service, method };
8385
+ return service === void 0 ? { service: called, method } : { service, method, alias: called };
8035
8386
  }
8036
8387
  function principalKind(principal) {
8037
8388
  if (principal === null) {
@@ -8041,15 +8392,18 @@ function principalKind(principal) {
8041
8392
  }
8042
8393
  function noteCaller(context, seen, target2, principal) {
8043
8394
  const kind = principalKind(principal);
8044
- const key = `${target2.service}\0${target2.method}\0${kind}`;
8395
+ const { service, method, alias } = target2;
8396
+ const key = `${alias ?? service}\0${method}\0${kind}`;
8045
8397
  if (seen.has(key)) {
8046
8398
  return;
8047
8399
  }
8048
8400
  seen.add(key);
8049
- context.logger.warn(
8050
- `A 4.x client called ${target2.service}.${target2.method} through the legacy shim`,
8051
- { category: "quickdraw.legacy", ...target2, principalKind: kind }
8052
- );
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
+ });
8053
8407
  }
8054
8408
  function serve2(socket, context, calls, call) {
8055
8409
  const controller = new AbortController();
@@ -8075,19 +8429,20 @@ function serve2(socket, context, calls, call) {
8075
8429
  respond: (result) => acknowledge(context.meter, call.ack, toLegacyReply(result), LEGACY_INTERNAL, report)
8076
8430
  }).then(release3, release3);
8077
8431
  }
8078
- function attachLegacyShim(socket, context, seen) {
8432
+ function attachLegacyShim(socket, context, seen, wire) {
8079
8433
  const calls = /* @__PURE__ */ new Set();
8080
8434
  socket.use((packet, next) => {
8081
8435
  const [event, ...args] = packet;
8082
8436
  const ack = args.at(-1);
8083
- const target2 = targetOf5(event, context.dispatcher.registry);
8437
+ const target2 = targetOf5(event, context.dispatcher.registry, wire.aliases);
8084
8438
  if (target2 === void 0 || typeof ack !== "function") {
8085
8439
  next();
8086
8440
  return;
8087
8441
  }
8088
8442
  noteCaller(context, seen, target2, socket.data.principal);
8089
8443
  const input = args.length > 1 ? args[0] : void 0;
8090
- serve2(socket, context, calls, { ...target2, input, ack });
8444
+ const { service, method } = target2;
8445
+ serve2(socket, context, calls, { service, method, input, ack });
8091
8446
  });
8092
8447
  socket.on("disconnect", () => {
8093
8448
  for (const controller of calls) {
@@ -8549,7 +8904,10 @@ function onConnection(settings) {
8549
8904
  void socket.join(userRoom(principal.userId));
8550
8905
  }
8551
8906
  if (socket.data.protocol === "legacy") {
8552
- 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
+ }
8553
8911
  return;
8554
8912
  }
8555
8913
  attachCallListeners(socket, settings);
@@ -8605,7 +8963,7 @@ function createSocketServer(httpServer, settings) {
8605
8963
  settings.live?.attach(io, probe, settings.cluster, broadcasts);
8606
8964
  listenForGrants(io, settings.live, settings.logger);
8607
8965
  listenForDisconnects(io);
8608
- io.use(protocolMiddleware(settings.legacyWire, context));
8966
+ io.use(protocolMiddleware(settings.legacyWire !== void 0, context));
8609
8967
  io.use(authMiddleware(settings.resolvePrincipal, context));
8610
8968
  if (settings.rateLimit !== false) {
8611
8969
  applySocketRateLimit(io, settings.rateLimit, { ...context, loops: settings.loops });
@@ -8616,7 +8974,10 @@ function createSocketServer(httpServer, settings) {
8616
8974
  ...context,
8617
8975
  hello: helloFrame(settings),
8618
8976
  extensions: settings.live === void 0 ? settings.extensions : [...settings.extensions, settings.live.extension],
8619
- 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() }
8620
8981
  })
8621
8982
  );
8622
8983
  return {
@@ -8705,6 +9066,7 @@ function warnUnsourcedGrants(services, sourced, logger) {
8705
9066
  }
8706
9067
  function createServer(options) {
8707
9068
  checkOptions(options);
9069
+ const legacyWire = resolveLegacyWire(options.legacyWire, options.services);
8708
9070
  const logger = options.logger ?? consoleLogger;
8709
9071
  warnUnsourcedGrants(options.services, options.auth?.serviceAccessSource !== void 0, logger);
8710
9072
  const watchdog = prepareWatchdog(options, options.stallWatchdog);
@@ -8725,7 +9087,7 @@ function createServer(options) {
8725
9087
  resolvePrincipal,
8726
9088
  loadServiceAccess: options.auth?.loadServiceAccess,
8727
9089
  binary: options.binary === true,
8728
- legacyWire: options.legacyWire === true,
9090
+ legacyWire,
8729
9091
  cors: options.cors,
8730
9092
  socket: options.socket,
8731
9093
  rateLimit: options.rateLimit ?? {},
@@ -8782,6 +9144,7 @@ export {
8782
9144
  custom,
8783
9145
  isCustomAccess,
8784
9146
  accessFormProblem,
9147
+ accessIds,
8785
9148
  meetsLevel,
8786
9149
  maxLevel,
8787
9150
  levelsAtLeast,
@@ -8806,7 +9169,6 @@ export {
8806
9169
  isAccessPolicy,
8807
9170
  checkName,
8808
9171
  levelsById,
8809
- idFilter,
8810
9172
  encodeCursor,
8811
9173
  decodeCursor,
8812
9174
  orderByOf,
@@ -8814,6 +9176,7 @@ export {
8814
9176
  MAX_ITEM_IDS,
8815
9177
  selectWith,
8816
9178
  INDEX_MAX_ROWS,
9179
+ narrowKinds,
8817
9180
  allowedScopes,
8818
9181
  membersWhere,
8819
9182
  CHANNEL_ABUSE_WINDOW_MS,
@@ -8823,6 +9186,11 @@ export {
8823
9186
  STREAM_MAX_SCOPES,
8824
9187
  MAX_STREAMS_PER_SOCKET,
8825
9188
  createBasicAccessEngine,
9189
+ anyOfPolicies,
9190
+ anyOf,
9191
+ membershipOf,
9192
+ members,
9193
+ resolver,
8826
9194
  DEFAULT_LIMITS,
8827
9195
  MAX_TIMEOUT_MS,
8828
9196
  createDispatcher,
@@ -8838,4 +9206,4 @@ export {
8838
9206
  watchAdapterClients,
8839
9207
  createServer
8840
9208
  };
8841
- //# sourceMappingURL=chunk-LLLYILEP.js.map
9209
+ //# sourceMappingURL=chunk-A6DJBGOG.js.map