@fitzzero/quickdraw-core 5.1.1 → 5.1.2

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 (107) hide show
  1. package/README.md +533 -180
  2. package/dist/{admin-DKwkIh5i.d.ts → admin-DWWrT1h6.d.ts} +2 -2
  3. package/dist/{adminSchemas-2DUA_hZ8.d.ts → adminSchemas-D4bFKsrb.d.ts} +6 -4
  4. package/dist/{auth-BhWqdyb-.d.ts → auth-DXiSJZaf.d.ts} +42 -16
  5. package/dist/{caller-CXTFswBO.d.ts → caller-BT-Mv4bD.d.ts} +117 -22
  6. package/dist/{chunk-RT6OILOI.js → chunk-4D2DOGAW.js} +2 -14
  7. package/dist/chunk-4D2DOGAW.js.map +1 -0
  8. package/dist/chunk-5CGO426C.js +15 -0
  9. package/dist/chunk-5CGO426C.js.map +1 -0
  10. package/dist/{chunk-RZTP4IEW.js → chunk-6K5BY47X.js} +4594 -3481
  11. package/dist/chunk-6K5BY47X.js.map +1 -0
  12. package/dist/{chunk-VSQBHPWH.js → chunk-753VJIUP.js} +13 -15
  13. package/dist/chunk-753VJIUP.js.map +1 -0
  14. package/dist/{chunk-ZIVPHYRG.js → chunk-7QCUX4TK.js} +4 -4
  15. package/dist/chunk-7QCUX4TK.js.map +1 -0
  16. package/dist/chunk-AK23XSWD.js +16 -0
  17. package/dist/chunk-AK23XSWD.js.map +1 -0
  18. package/dist/{chunk-SHEGLZEP.js → chunk-AONAFOYU.js} +14 -5
  19. package/dist/{chunk-SHEGLZEP.js.map → chunk-AONAFOYU.js.map} +1 -1
  20. package/dist/chunk-DKE6QO26.js +136 -0
  21. package/dist/chunk-DKE6QO26.js.map +1 -0
  22. package/dist/{chunk-H23KEFBA.js → chunk-DUBU3BB3.js} +30 -1
  23. package/dist/chunk-DUBU3BB3.js.map +1 -0
  24. package/dist/{chunk-CDKG4TFA.js → chunk-GKH3LJ7N.js} +1 -1
  25. package/dist/chunk-GKH3LJ7N.js.map +1 -0
  26. package/dist/{chunk-OECZPRPW.js → chunk-GSIPUNJL.js} +49 -9
  27. package/dist/chunk-GSIPUNJL.js.map +1 -0
  28. package/dist/{chunk-DD4SKCNO.js → chunk-LCC7HNFP.js} +12 -1
  29. package/dist/chunk-LCC7HNFP.js.map +1 -0
  30. package/dist/{chunk-2PWMUVC7.js → chunk-MBH6IDSM.js} +480 -12
  31. package/dist/chunk-MBH6IDSM.js.map +1 -0
  32. package/dist/{chunk-QCHKWIRS.js → chunk-NMXYENH4.js} +10 -10
  33. package/dist/{chunk-QCHKWIRS.js.map → chunk-NMXYENH4.js.map} +1 -1
  34. package/dist/chunk-NO4M6PO5.js +101 -0
  35. package/dist/chunk-NO4M6PO5.js.map +1 -0
  36. package/dist/{chunk-TBMKJ26R.js → chunk-NW6U45HX.js} +3 -1
  37. package/dist/chunk-NW6U45HX.js.map +1 -0
  38. package/dist/{chunk-REX5YPHX.js → chunk-RJ5I2VO3.js} +74 -19
  39. package/dist/chunk-RJ5I2VO3.js.map +1 -0
  40. package/dist/{chunk-QEKVYG5F.js → chunk-RL4VKLBX.js} +1 -1
  41. package/dist/{chunk-QEKVYG5F.js.map → chunk-RL4VKLBX.js.map} +1 -1
  42. package/dist/chunk-XH2IZVLT.js +62 -0
  43. package/dist/chunk-XH2IZVLT.js.map +1 -0
  44. package/dist/{chunk-4DIQHASK.js → chunk-Z5ILULF2.js} +26 -7
  45. package/dist/chunk-Z5ILULF2.js.map +1 -0
  46. package/dist/cli/quickdraw-docs.js +5 -3
  47. package/dist/cli/quickdraw-docs.js.map +1 -1
  48. package/dist/client/index.d.ts +30 -18
  49. package/dist/client/index.js +8 -6
  50. package/dist/client/index.js.map +1 -1
  51. package/dist/{clientTypes-Bc-G8n-U.d.ts → clientTypes-hWcPUVS9.d.ts} +3 -3
  52. package/dist/{connection-Cr1BU07W.d.ts → connection-CgijdGpT.d.ts} +8 -4
  53. package/dist/{createServer-CP9eDHZZ.d.ts → createServer-Cp-Wk500.d.ts} +36 -14
  54. package/dist/{createTestApp-UKVGEmOf.d.ts → createTestApp-BB_PkqQ7.d.ts} +16 -10
  55. package/dist/{envelope-Cp3YcVxC.d.ts → envelope-DzWyIXjj.d.ts} +18 -4
  56. package/dist/{http-Djj2Inuu.d.ts → http-DotQ5bj8.d.ts} +68 -17
  57. package/dist/index.d.ts +7 -7
  58. package/dist/index.js +37 -13
  59. package/dist/index.js.map +1 -1
  60. package/dist/{infer-Teo1xpsd.d.ts → infer-BmoZsL9a.d.ts} +43 -10
  61. package/dist/logger-Yz93KzdV.d.ts +30 -0
  62. package/dist/{metrics-BdBu5o8G.d.ts → metrics-DW80Exve.d.ts} +2 -2
  63. package/dist/origins-kvpxnVcW.d.ts +8 -0
  64. package/dist/prisma/index.d.ts +5 -5
  65. package/dist/prisma/index.js +130 -67
  66. package/dist/prisma/index.js.map +1 -1
  67. package/dist/{rateLimit-ZMIZJUZF.js → rateLimit-5RKBFLUR.js} +2 -2
  68. package/dist/server/auth/index.d.ts +82 -33
  69. package/dist/server/auth/index.js +273 -268
  70. package/dist/server/auth/index.js.map +1 -1
  71. package/dist/server/express/index.d.ts +9 -2
  72. package/dist/server/express/index.js +1 -1
  73. package/dist/server/index.d.ts +147 -67
  74. package/dist/server/index.js +520 -381
  75. package/dist/server/index.js.map +1 -1
  76. package/dist/server/mcp/index.d.ts +63 -17
  77. package/dist/server/mcp/index.js +256 -54
  78. package/dist/server/mcp/index.js.map +1 -1
  79. package/dist/server/otel.d.ts +3 -3
  80. package/dist/{sharing-DVa8W-yR.d.ts → sharing-UDLn1049.d.ts} +3 -3
  81. package/dist/testing/client.d.ts +14 -14
  82. package/dist/testing/client.js +9 -7
  83. package/dist/testing/client.js.map +1 -1
  84. package/dist/testing/index.d.ts +12 -12
  85. package/dist/testing/index.js +21 -12
  86. package/dist/testing/index.js.map +1 -1
  87. package/dist/testing/mock.d.ts +5 -5
  88. package/dist/testing/mock.js +8 -6
  89. package/dist/{types-Cu0vmx7c.d.ts → types-CYliTP65.d.ts} +1 -1
  90. package/dist/utils/index.d.ts +1 -1
  91. package/package.json +1 -1
  92. package/dist/chunk-2PWMUVC7.js.map +0 -1
  93. package/dist/chunk-4DIQHASK.js.map +0 -1
  94. package/dist/chunk-CDKG4TFA.js.map +0 -1
  95. package/dist/chunk-DD4SKCNO.js.map +0 -1
  96. package/dist/chunk-H23KEFBA.js.map +0 -1
  97. package/dist/chunk-OECZPRPW.js.map +0 -1
  98. package/dist/chunk-REX5YPHX.js.map +0 -1
  99. package/dist/chunk-RT6OILOI.js.map +0 -1
  100. package/dist/chunk-RZTP4IEW.js.map +0 -1
  101. package/dist/chunk-TBMKJ26R.js.map +0 -1
  102. package/dist/chunk-VSQBHPWH.js.map +0 -1
  103. package/dist/chunk-YSWH776Y.js +0 -13
  104. package/dist/chunk-YSWH776Y.js.map +0 -1
  105. package/dist/chunk-ZIVPHYRG.js.map +0 -1
  106. package/dist/logger-BY28acW-.d.ts +0 -16
  107. /package/dist/{rateLimit-ZMIZJUZF.js.map → rateLimit-5RKBFLUR.js.map} +0 -0
@@ -1,6 +1,10 @@
1
+ import {
2
+ findNestedWrites
3
+ } from "../chunk-XH2IZVLT.js";
1
4
  import {
2
5
  CHANNEL_ABUSE_MULTIPLIER,
3
6
  CHANNEL_ABUSE_WINDOW_MS,
7
+ DEFAULT_BULK_THRESHOLD,
4
8
  DEFAULT_LIMITS,
5
9
  DEFAULT_STALL_INTERVAL_MS,
6
10
  DEFAULT_STALL_MAX_MS,
@@ -16,7 +20,6 @@ import {
16
20
  RESUME_MAX_DELTAS,
17
21
  STALL_RESOLUTION_MS,
18
22
  STREAM_MAX_SCOPES,
19
- SUBSCRIBER_LEVELS,
20
23
  accessFormProblem,
21
24
  afterCursor,
22
25
  allowedScopes,
@@ -24,9 +27,8 @@ import {
24
27
  anyOfPolicies,
25
28
  applyRateLimitMiddleware,
26
29
  checkName,
27
- checkWhenDefined,
28
- checksRowsItself,
29
- compileProjections,
30
+ claimsOption,
31
+ compileCollections,
30
32
  createBasicAccessEngine,
31
33
  createDispatcher,
32
34
  createHttpRouter,
@@ -36,32 +38,20 @@ import {
36
38
  custom,
37
39
  decodeCursor,
38
40
  definePolicy,
41
+ deploymentOf,
39
42
  encodeCursor,
40
- handlerProblem,
41
- inputKeys,
42
43
  isAccessPolicy,
43
44
  isCustomAccess,
44
- isKitHandler,
45
- isRowChecked,
46
- levelsAtLeast,
47
45
  levelsById,
48
- markGrantsEditor,
49
- maxLevel,
50
- meetsLevel,
51
46
  members,
52
47
  membersWhere,
53
48
  membershipOf,
54
- narrowKinds,
55
49
  orderByOf,
56
- projectRow,
57
- projectedOutput,
58
- registerRuntime,
59
50
  resolver,
51
+ rowlessProblem,
60
52
  selectWith,
61
- serviceGrant,
62
- strip,
63
53
  watchAdapterClients
64
- } from "../chunk-RZTP4IEW.js";
54
+ } from "../chunk-6K5BY47X.js";
65
55
  import {
66
56
  DevWarningError,
67
57
  N_PLUS_ONE_STATEMENTS,
@@ -69,273 +59,93 @@ import {
69
59
  currentRev,
70
60
  formatDevWarning,
71
61
  runBeforeAnyDispatcher
72
- } from "../chunk-4DIQHASK.js";
62
+ } from "../chunk-Z5ILULF2.js";
73
63
  import {
74
64
  crud,
75
65
  crudSpecOf,
76
66
  sharing,
77
67
  sharingSpecOf
78
- } from "../chunk-QCHKWIRS.js";
79
- import "../chunk-QEKVYG5F.js";
68
+ } from "../chunk-NMXYENH4.js";
69
+ import "../chunk-RL4VKLBX.js";
80
70
  import {
71
+ SUBSCRIBER_LEVELS,
81
72
  abortError,
82
73
  callerGrantsOf,
74
+ checkWhenDefined,
75
+ checkWhenServed,
76
+ checksRowsItself,
77
+ compileProjections,
83
78
  createCaller,
79
+ dependsOnCaller,
80
+ handlerProblem,
81
+ isCallerDependent,
84
82
  kitRuntimeOf,
83
+ levelsAtLeast,
84
+ markGrantsEditor,
85
+ maxLevel,
86
+ meetsLevel,
87
+ narrowKinds,
85
88
  outputSchemaOf,
86
- schemaOutputOf
87
- } from "../chunk-2PWMUVC7.js";
88
- import "../chunk-DD4SKCNO.js";
89
+ projectRow,
90
+ projectedOutput,
91
+ registerRuntime,
92
+ schemaOutputOf,
93
+ serviceGrant,
94
+ strip
95
+ } from "../chunk-MBH6IDSM.js";
96
+ import "../chunk-LCC7HNFP.js";
89
97
  import {
90
98
  recordSocketSession,
91
99
  toCallReply,
92
100
  unreadable
93
- } from "../chunk-REX5YPHX.js";
101
+ } from "../chunk-RJ5I2VO3.js";
94
102
  import {
95
103
  ANY_FIELD,
96
104
  modelKey,
97
105
  storageOf
98
- } from "../chunk-H23KEFBA.js";
106
+ } from "../chunk-DUBU3BB3.js";
99
107
  import {
100
108
  ADMIN_FIELD_TYPES,
101
109
  ADMIN_NEVER_WRITABLE,
110
+ MAX_CURSOR_LENGTH,
102
111
  admin,
103
112
  adminSpecOf,
104
113
  search,
105
114
  searchSpecOf
106
- } from "../chunk-CDKG4TFA.js";
115
+ } from "../chunk-GKH3LJ7N.js";
107
116
  import {
108
- ACCESS_LEVELS,
117
+ MAX_ENTITY_IDS_PER_SOCKET,
118
+ MAX_SCOPES_PER_SOCKET,
119
+ MAX_TOPICS_PER_SOCKET,
109
120
  entityRoom,
110
- isAccessLevel,
111
- reservedRoomPrefix
112
- } from "../chunk-VSQBHPWH.js";
121
+ reservedRoomPrefix,
122
+ socketIdShaped
123
+ } from "../chunk-753VJIUP.js";
113
124
  import {
114
125
  CHANNEL_DEFAULT_RATE,
115
- DEFAULT_COLLECTION_LIMIT,
116
- DEFAULT_COLLECTION_MAX_LIMIT,
117
126
  isScopedStream
118
- } from "../chunk-RT6OILOI.js";
127
+ } from "../chunk-5CGO426C.js";
128
+ import "../chunk-4D2DOGAW.js";
129
+ import {
130
+ ACCESS_LEVELS,
131
+ isAccessLevel
132
+ } from "../chunk-AK23XSWD.js";
119
133
  import "../chunk-47AEXVDM.js";
120
134
  import "../chunk-Z2LZ7IZ7.js";
121
135
  import {
122
- consoleLogger
123
- } from "../chunk-YSWH776Y.js";
136
+ consoleLogger,
137
+ errorText
138
+ } from "../chunk-DKE6QO26.js";
124
139
  import {
125
140
  QuickdrawError
126
141
  } from "../chunk-4YO5H5ON.js";
127
142
 
128
- // src/server/access/rowless.ts
129
- function checksNoRow(form) {
130
- if (form === "public" || form === "authenticated") {
131
- return true;
132
- }
133
- return typeof form === "object" && !isCustomAccess(form) && form.entry === void 0 && form.scope === void 0 && form.service !== void 0 && form.service !== "Admin";
134
- }
135
- function described(form) {
136
- if (form === "public") {
137
- return { code: '"public"', who: "anyone, signed in or not," };
138
- }
139
- if (form === "authenticated") {
140
- return { code: '"authenticated"', who: "every signed-in user" };
141
- }
142
- const level = typeof form === "object" && !isCustomAccess(form) ? form.service : void 0;
143
- return {
144
- code: `{ service: "${String(level)}" }`,
145
- who: `everyone with a service-wide ${String(level)} grant`
146
- };
147
- }
148
- function suggestion(method) {
149
- const { access } = method;
150
- if (typeof access === "object" && !isCustomAccess(access) && access.service !== void 0) {
151
- return `{ service: "${access.service}", entry: "${access.service}" }`;
152
- }
153
- return `{ entry: "${method.kind === "query" ? "Read" : "Moderate"}" }`;
154
- }
155
- function rowlessProblem(method) {
156
- const { access, handler } = method;
157
- if (method.rowless || !checksNoRow(access) || isRowChecked(handler)) {
158
- return void 0;
159
- }
160
- if (inputKeys(method.input)?.includes("id") !== true) {
161
- return void 0;
162
- }
163
- const optOut = isKitHandler(handler) ? `name it in the kit's rowless option (rowless: ["${method.name}"])` : "set rowless: true on the method";
164
- const form = described(access);
165
- return `method "${method.name}" takes a row id (its input has id), but its access ${form.code} checks no row: on a service with an access policy, that lets ${form.who} reach any row by its id. Give it ${suggestion(method)} so the policy decides, or, if every caller its access admits may reach any row, ${optOut}`;
166
- }
167
-
168
- // src/server/collections/define.ts
169
- var DEFAULT_BULK_THRESHOLD = 200;
170
- var OPTION_KEYS = /* @__PURE__ */ new Set(["anchor", "scopeAccess", "bulkThreshold"]);
171
- function isRecord(value) {
172
- return typeof value === "object" && value !== null && !Array.isArray(value);
173
- }
174
- function isContract(value) {
175
- return isRecord(value) && typeof value.name === "string" && value.name.length > 0 && Object.isFrozen(value);
176
- }
177
- function checkOption(owner2, option, fail5) {
178
- if (!isRecord(option)) {
179
- fail5(`${owner2} must be { anchor: contract } or { scopeAccess: "self" }`);
180
- }
181
- const unknownKey = Object.keys(option).find((key) => !OPTION_KEYS.has(key));
182
- if (unknownKey !== void 0) {
183
- fail5(
184
- `${owner2} has an unknown option "${unknownKey}"; the options are anchor, scopeAccess and bulkThreshold`
185
- );
186
- }
187
- const { anchor, scopeAccess, bulkThreshold = DEFAULT_BULK_THRESHOLD } = option;
188
- if (anchor === void 0 === (scopeAccess === void 0)) {
189
- fail5(
190
- `${owner2} needs exactly one of anchor (the contract whose rows its scope values are ids of) and scopeAccess: "self"`
191
- );
192
- }
193
- if (anchor !== void 0 && !isContract(anchor)) {
194
- fail5(`${owner2}: anchor must be a contract from defineContract`);
195
- }
196
- if (scopeAccess !== void 0 && scopeAccess !== "self") {
197
- fail5(
198
- `${owner2}: scopeAccess must be "self", for a scope whose value is the subscriber's user id`
199
- );
200
- }
201
- if (typeof bulkThreshold !== "number" || !Number.isSafeInteger(bulkThreshold) || bulkThreshold < 1) {
202
- fail5(`${owner2}: bulkThreshold must be a positive whole number`);
203
- }
204
- return { anchor, bulkThreshold };
205
- }
206
- function checkIndex(name, def, item, access, fail5) {
207
- const { index, views } = def;
208
- if (index === void 0) {
209
- if (views !== void 0) {
210
- fail5(`collection "${name}" declares views but no index; views read index rows`);
211
- }
212
- return void 0;
213
- }
214
- if (!Array.isArray(index) || !index.every((field) => typeof field === "string")) {
215
- fail5(`collection "${name}": index must be a list of item field names`);
216
- }
217
- const owner2 = `collection "${name}": index field`;
218
- const hidden = item.tiers.hidden(access);
219
- for (const [position, field] of index.entries()) {
220
- if (field === "id") {
221
- fail5(`${owner2} "id" is not needed; every index row starts with the member's id`);
222
- }
223
- if (!item.keys.includes(field)) {
224
- fail5(
225
- `${owner2} "${field}" is not a key of its item "${item.name}"; index fields are item fields`
226
- );
227
- }
228
- if (hidden.has(field)) {
229
- fail5(
230
- `${owner2} "${field}" is reserved by the contract's fields for a level above the collection's access (${access}), so its items never carry it`
231
- );
232
- }
233
- if (index.indexOf(field) !== position) {
234
- fail5(`${owner2} "${field}" is named twice`);
235
- }
236
- }
237
- checkIndexOrder(name, def.order, index, fail5);
238
- return Object.freeze([...index]);
239
- }
240
- function checkOrderTiers(name, order, fields, access, fail5) {
241
- for (const [column] of order) {
242
- const required = Object.hasOwn(fields, column) ? fields[column] : void 0;
243
- if (required !== void 0 && !meetsLevel(access, required)) {
244
- fail5(
245
- `collection "${name}": order column "${column}" is reserved by the contract's fields for ${required}, above the collection's access (${access}); a page's cursor and order would tell its subscribers what it holds`
246
- );
247
- }
248
- }
249
- }
250
- function checkIndexOrder(name, order, index, fail5) {
251
- const missing2 = order.map(([column]) => column).find((c) => c !== "id" && !index.includes(c));
252
- if (missing2 !== void 0) {
253
- fail5(
254
- `collection "${name}": order column "${missing2}" is not an index field; a client keeps the index in order by it`
255
- );
256
- }
257
- }
258
- function accessOf(name, def, anchor, fail5) {
259
- if (anchor !== void 0) {
260
- return def.access ?? "Read";
261
- }
262
- if (def.access !== void 0 && meetsLevel(def.access, "Moderate")) {
263
- fail5(
264
- `collection "${name}" has scopeAccess "self", which is authorized by the subscriber's user id and not by a level, so its access may not be above Read (it declares ${def.access})`
265
- );
266
- }
267
- return "Read";
268
- }
269
- function scopeOf(def) {
270
- const { scope } = def;
271
- if (typeof scope === "string") {
272
- return Object.freeze({ kind: "column", column: scope });
273
- }
274
- return Object.freeze({
275
- kind: "via",
276
- model: scope.model,
277
- entry: scope.entry,
278
- scope: scope.scope,
279
- refreshEntry: scope.refreshEntry === true
280
- });
281
- }
282
- function compileOne([name, def], contract, projections, option, fail5) {
283
- if (option === void 0) {
284
- fail5(
285
- `collection "${name}" needs its scope's access: collections: { ${name}: { anchor: contract } }, or { scopeAccess: "self" } for a scope that is the subscriber's user id`
286
- );
287
- }
288
- const { anchor, bulkThreshold } = checkOption(`collections.${name}`, option, fail5);
289
- const item = projections.get(def.item);
290
- if (item === void 0) {
291
- fail5(`collection "${name}": item "${def.item}" is not a projection of the contract`);
292
- }
293
- const access = accessOf(name, def, anchor, fail5);
294
- checkOrderTiers(name, def.order, contract.fields, access, fail5);
295
- return Object.freeze({
296
- name,
297
- scope: scopeOf(def),
298
- item,
299
- where: Object.freeze({ ...def.where }),
300
- order: def.order,
301
- limit: def.limit ?? DEFAULT_COLLECTION_LIMIT,
302
- maxLimit: def.maxLimit ?? DEFAULT_COLLECTION_MAX_LIMIT,
303
- access,
304
- anchor,
305
- bulkThreshold,
306
- index: checkIndex(name, def, item, access, fail5)
307
- });
308
- }
309
- function compileCollections(contract, projections, model, option, fail5) {
310
- if (option !== void 0 && !isRecord(option)) {
311
- fail5(
312
- 'collections must map each collection of the contract to { anchor } or { scopeAccess: "self" }'
313
- );
314
- }
315
- const options = option ?? {};
316
- const unknownName = Object.keys(options).find(
317
- (name) => !Object.hasOwn(contract.collections, name)
318
- );
319
- if (unknownName !== void 0) {
320
- fail5(`collections names "${unknownName}", which is not a collection of the contract`);
321
- }
322
- const declared = Object.entries(contract.collections);
323
- if (declared.length > 0 && model === void 0) {
324
- fail5("collections need model: their items are rows of the service's model");
325
- }
326
- const compiled = /* @__PURE__ */ new Map();
327
- for (const [name, def] of declared) {
328
- compiled.set(name, compileOne([name, def], contract, projections, options[name], fail5));
329
- }
330
- return compiled;
331
- }
332
-
333
143
  // src/server/realtime/define.ts
334
- function isRecord2(value) {
144
+ function isRecord(value) {
335
145
  return typeof value === "object" && value !== null && !Array.isArray(value);
336
146
  }
337
147
  function compileSelector(selector) {
338
- const read = typeof selector === "function" ? selector : (payload) => isRecord2(payload) ? payload[selector] : void 0;
148
+ const read = typeof selector === "function" ? selector : (payload) => isRecord(payload) ? payload[selector] : void 0;
339
149
  return (payload) => {
340
150
  try {
341
151
  const value = read(payload);
@@ -352,7 +162,7 @@ function compileRoom(room) {
352
162
  const computed = compileSelector(room);
353
163
  return (payload) => {
354
164
  const name = computed(payload);
355
- return name === void 0 || reservedRoomPrefix(name) !== void 0 ? void 0 : name;
165
+ return name === void 0 || reservedRoomPrefix(name) !== void 0 || socketIdShaped(name) ? void 0 : name;
356
166
  };
357
167
  }
358
168
  function compileRequires(def) {
@@ -380,8 +190,8 @@ function checkChannelAccess(owner2, access, fail5) {
380
190
  if (access === void 0 || access === "authenticated") {
381
191
  return "authenticated";
382
192
  }
383
- const keys = isRecord2(access) ? Object.keys(access) : [];
384
- if (!isRecord2(access) || keys.length !== 1 || !isAccessLevel(access.service)) {
193
+ const keys = isRecord(access) ? Object.keys(access) : [];
194
+ if (!isRecord(access) || keys.length !== 1 || !isAccessLevel(access.service)) {
385
195
  fail5(`${owner2}: access must be "authenticated" or { service: level }`);
386
196
  }
387
197
  return Object.freeze({ service: access.service });
@@ -389,7 +199,7 @@ function checkChannelAccess(owner2, access, fail5) {
389
199
  function checkChannel(service, name, def, entry, kinds, fail5) {
390
200
  const owner2 = `channel "${name}"`;
391
201
  const implementation = typeof entry === "function" ? { handler: entry } : entry;
392
- if (!isRecord2(implementation) || typeof implementation.handler !== "function") {
202
+ if (!isRecord(implementation) || typeof implementation.handler !== "function") {
393
203
  fail5(`${owner2} must be a handler function or { access, handler }`);
394
204
  }
395
205
  const unknownKey = Object.keys(implementation).find(
@@ -408,6 +218,7 @@ function checkChannel(service, name, def, entry, kinds, fail5) {
408
218
  burst: def.burst ?? Math.max(1, ratePerSecond * 2),
409
219
  requires: compileRequires(def),
410
220
  access: checkChannelAccess(owner2, implementation.access, fail5),
221
+ accessDeclared: implementation.access !== void 0,
411
222
  kinds,
412
223
  handler: implementation.handler
413
224
  });
@@ -417,7 +228,7 @@ function compileChannels(contract, value, kinds, fail5) {
417
228
  if (value === void 0 && declared.length === 0) {
418
229
  return /* @__PURE__ */ new Map();
419
230
  }
420
- if (!isRecord2(value)) {
231
+ if (!isRecord(value)) {
421
232
  fail5("channels must be an object with one implementation per contract channel");
422
233
  }
423
234
  const missing2 = declared.filter(([name]) => !Object.hasOwn(value, name)).map(([name]) => name);
@@ -474,7 +285,7 @@ function checkStreamOptions(name, def, entry, fail5) {
474
285
  if (entry === void 0) {
475
286
  return { computeSeed: void 0, validate: "always" };
476
287
  }
477
- if (!isRecord2(entry)) {
288
+ if (!isRecord(entry)) {
478
289
  fail5(`${owner2} must be an object: { seed?, validate? }`);
479
290
  }
480
291
  const unknownKey = Object.keys(entry).find((key) => !STREAM_OPTION_KEYS.includes(key));
@@ -522,7 +333,7 @@ function checkStream(name, def, needs, options, fail5) {
522
333
  });
523
334
  }
524
335
  function compileStreams(contract, needs, value, fail5) {
525
- if (value !== void 0 && !isRecord2(value)) {
336
+ if (value !== void 0 && !isRecord(value)) {
526
337
  fail5(
527
338
  "streams must be an object of options per contract stream: { <stream>: { seed?, validate? } }"
528
339
  );
@@ -541,14 +352,14 @@ function compileStreams(contract, needs, value, fail5) {
541
352
  }
542
353
 
543
354
  // src/server/serviceData.ts
544
- function isRecord3(value) {
355
+ function isRecord2(value) {
545
356
  return typeof value === "object" && value !== null && !Array.isArray(value);
546
357
  }
547
358
  function isName(value) {
548
359
  return typeof value === "string" && value.length > 0;
549
360
  }
550
- function isContract2(value) {
551
- return isRecord3(value) && isName(value.name) && Object.isFrozen(value);
361
+ function isContract(value) {
362
+ return isRecord2(value) && isName(value.name) && Object.isFrozen(value);
552
363
  }
553
364
  function idsIn(value) {
554
365
  if (isName(value)) {
@@ -558,7 +369,7 @@ function idsIn(value) {
558
369
  }
559
370
  function checkLink(index, entry, fail5) {
560
371
  const owner2 = `affects[${index}]`;
561
- if (!isRecord3(entry) || !isContract2(entry.service)) {
372
+ if (!isRecord2(entry) || !isContract(entry.service)) {
562
373
  fail5(`${owner2} must be { service: contract, id }`);
563
374
  }
564
375
  const { id, columns } = entry;
@@ -613,7 +424,7 @@ function checkWatchAccess(value, fail5) {
613
424
  if (value === "public" || value === "authenticated") {
614
425
  return value;
615
426
  }
616
- if (!isRecord3(value) || Object.keys(value).length !== 1 || !isAccessLevel(value.service)) {
427
+ if (!isRecord2(value) || Object.keys(value).length !== 1 || !isAccessLevel(value.service)) {
617
428
  fail5('watchAccess must be "public", "authenticated" or { service: level }');
618
429
  }
619
430
  return Object.freeze({ service: value.service });
@@ -635,6 +446,9 @@ function checkMethodKinds(owner2, entry, data, fail5) {
635
446
  (message) => fail5(`${owner2}: ${message}`)
636
447
  );
637
448
  }
449
+ function checkMethodClaims(owner2, entry, fail5) {
450
+ return claimsOption(owner2, entry, fail5);
451
+ }
638
452
  function checkServiceData(definition, appKinds, fail5) {
639
453
  const { model, access, versionColumn } = definition;
640
454
  if (model !== void 0 && !isName(model)) {
@@ -703,9 +517,10 @@ var METHOD_KEYS = /* @__PURE__ */ new Set([
703
517
  "timeoutMs",
704
518
  "version",
705
519
  "rowless",
706
- "kinds"
520
+ "kinds",
521
+ "claims"
707
522
  ]);
708
- function isRecord4(value) {
523
+ function isRecord3(value) {
709
524
  return typeof value === "object" && value !== null && !Array.isArray(value);
710
525
  }
711
526
  function checkKeys(value, allowed, owner2, fail5) {
@@ -720,7 +535,7 @@ function isDuration(value, max) {
720
535
  return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= max;
721
536
  }
722
537
  function checkContract(contract) {
723
- const valid = isRecord4(contract) && typeof contract.name === "string" && contract.name.length > 0 && isRecord4(contract.methods) && Object.isFrozen(contract);
538
+ const valid = isRecord3(contract) && typeof contract.name === "string" && contract.name.length > 0 && isRecord3(contract.methods) && Object.isFrozen(contract);
724
539
  if (!valid) {
725
540
  throw new TypeError("defineService: the first argument must be a contract from defineContract");
726
541
  }
@@ -731,11 +546,16 @@ function checkShare(owner2, entry, fail5) {
731
546
  if (share !== void 0 && share !== "caller" && share !== "all") {
732
547
  fail5(`${owner2}: share must be "caller" or "all"`);
733
548
  }
734
- if (share === "all" && isRecord4(entry.access) && entry.access.kind === "custom") {
549
+ if (share === "all" && isRecord3(entry.access) && entry.access.kind === "custom") {
735
550
  fail5(
736
551
  `${owner2} has custom access, so it cannot share: "all"; its result may depend on who asks`
737
552
  );
738
553
  }
554
+ if (share === "all" && typeof entry.handler === "function" && isCallerDependent(entry.handler)) {
555
+ fail5(
556
+ `${owner2} is a kit method whose result depends on who asks, so it cannot share: "all"; share: "caller" runs one principal's identical calls once`
557
+ );
558
+ }
739
559
  if (ttlMs !== void 0 && (share === void 0 || !isDuration(ttlMs, MAX_TIMEOUT_MS))) {
740
560
  fail5(`${owner2}: ttlMs needs share and must be a positive number of milliseconds`);
741
561
  }
@@ -756,7 +576,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
756
576
  if (def === void 0) {
757
577
  fail5(`"${name}" is not a method of the contract`);
758
578
  }
759
- if (!isRecord4(entry)) {
579
+ if (!isRecord3(entry)) {
760
580
  fail5(`${owner2} must be { access, handler }`);
761
581
  }
762
582
  checkKeys(entry, METHOD_KEYS, owner2, fail5);
@@ -777,6 +597,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
777
597
  }
778
598
  checkQueryOptions(owner2, entry, def.kind, fail5);
779
599
  const kinds = checkMethodKinds(owner2, entry, data, fail5);
600
+ const claims = checkMethodClaims(owner2, entry, fail5);
780
601
  const projection = projectedOutput(def.output, projections);
781
602
  return Object.freeze({
782
603
  name,
@@ -787,6 +608,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
787
608
  schemaOutput: projection === void 0 ? schemaOutputOf(def.output) : void 0,
788
609
  access: entry.access,
789
610
  kinds,
611
+ claims,
790
612
  handler: entry.handler,
791
613
  share: entry.share,
792
614
  ttlMs: entry.ttlMs,
@@ -796,7 +618,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
796
618
  });
797
619
  }
798
620
  function checkMethods(contract, projections, data, value, fail5) {
799
- if (!isRecord4(value)) {
621
+ if (!isRecord3(value)) {
800
622
  fail5("methods must be an object with one implementation per contract method");
801
623
  }
802
624
  const missing2 = Object.keys(contract.methods).filter((name) => !Object.hasOwn(value, name));
@@ -818,7 +640,7 @@ function checkWatches(contract, collections, data, fail5) {
818
640
  if (def.kind !== "query") {
819
641
  fail5(`method "${name}" is a mutation; only a query can watch`);
820
642
  }
821
- if (watch === "service" || isRecord4(watch) && Object.hasOwn(watch, "service")) {
643
+ if (watch === "service" || isRecord3(watch) && Object.hasOwn(watch, "service")) {
822
644
  if (data.watchAccess === void 0) {
823
645
  fail5(
824
646
  `method "${name}" watches the service's topic, which is closed without watchAccess: declare who may watch it ("public", "authenticated" or { service: level })`
@@ -829,7 +651,7 @@ function checkWatches(contract, collections, data, fail5) {
829
651
  }
830
652
  continue;
831
653
  }
832
- if (!isRecord4(watch) || typeof watch.scope !== "function") {
654
+ if (!isRecord3(watch) || typeof watch.scope !== "function") {
833
655
  fail5(`method "${name}": watch needs a scope function, which finds the scope from the input`);
834
656
  }
835
657
  if (typeof watch.collection !== "string" || !collections.has(watch.collection)) {
@@ -857,12 +679,12 @@ function checkRowForms(methods, data, fail5) {
857
679
  }
858
680
  }
859
681
  }
860
- function checkRowless(methods, data, fail5) {
682
+ function checkRowless(contract, methods, data, fail5) {
861
683
  if (data.access === void 0) {
862
684
  return;
863
685
  }
864
686
  for (const method of Object.values(methods)) {
865
- const problem = rowlessProblem(method);
687
+ const problem = rowlessProblem(method, contract.name);
866
688
  if (problem !== void 0) {
867
689
  fail5(problem);
868
690
  }
@@ -898,7 +720,7 @@ function buildService(runtime, contract, definition) {
898
720
  const fail5 = (message) => {
899
721
  throw new TypeError(`defineService("${checked2.name}"): ${message}`);
900
722
  };
901
- if (!isRecord4(definition)) {
723
+ if (!isRecord3(definition)) {
902
724
  fail5("the definition must be an object");
903
725
  }
904
726
  checkKeys(definition, DEFINITION_KEYS, "the definition", fail5);
@@ -916,12 +738,13 @@ function buildService(runtime, contract, definition) {
916
738
  projections,
917
739
  data.model,
918
740
  definition.collections,
919
- fail5
741
+ fail5,
742
+ data.access
920
743
  );
921
744
  checkWatches(checked2, collections, data, fail5);
922
745
  const methods = checkMethods(checked2, projections, data, definition.methods, fail5);
923
746
  checkRowForms(methods, data, fail5);
924
- checkRowless(methods, data, fail5);
747
+ checkRowless(checked2, methods, data, fail5);
925
748
  const service = Object.freeze({
926
749
  name: checked2.name,
927
750
  contract: checked2,
@@ -1060,7 +883,7 @@ var COLUMNS = /* @__PURE__ */ new WeakMap();
1060
883
  function jsonAclColumnsOf(policy) {
1061
884
  return COLUMNS.get(policy);
1062
885
  }
1063
- function isRecord5(value) {
886
+ function isRecord4(value) {
1064
887
  return typeof value === "object" && value !== null && !Array.isArray(value);
1065
888
  }
1066
889
  function aclLevel(acl, userId) {
@@ -1069,7 +892,7 @@ function aclLevel(acl, userId) {
1069
892
  }
1070
893
  let level = null;
1071
894
  for (const entry of acl) {
1072
- if (isRecord5(entry) && entry.userId === userId) {
895
+ if (isRecord4(entry) && entry.userId === userId) {
1073
896
  level = maxLevel(level, entry.level);
1074
897
  }
1075
898
  }
@@ -1115,11 +938,11 @@ function jsonAcl(field, options = {}) {
1115
938
  }
1116
939
 
1117
940
  // src/server/access/policies/inherit.ts
1118
- function isContract3(value) {
941
+ function isContract2(value) {
1119
942
  return typeof value === "object" && value !== null && typeof value.name === "string" && Object.isFrozen(value);
1120
943
  }
1121
944
  function inherit(options) {
1122
- if (typeof options !== "object" || options === null || !isContract3(options.from)) {
945
+ if (typeof options !== "object" || options === null || !isContract2(options.from)) {
1123
946
  throw new TypeError("inherit({ from, via }): from must be the parent service's contract");
1124
947
  }
1125
948
  const { from } = options;
@@ -1202,6 +1025,10 @@ function rowLevel(form, fallback) {
1202
1025
  }
1203
1026
  return form.entry ?? form.scope ?? fallback;
1204
1027
  }
1028
+ function bulkFallback(counterpart) {
1029
+ const level = counterpart === void 0 ? "Moderate" : rowLevel(counterpart, "Moderate");
1030
+ return meetsLevel(level, "Moderate") ? level : "Moderate";
1031
+ }
1205
1032
  function unchecked(call, form, use) {
1206
1033
  return call.runtime.service.access === void 0 || use === "read" && form === "public";
1207
1034
  }
@@ -1228,8 +1055,8 @@ function grantRowLevel(call, form) {
1228
1055
  function grantReaches(call, form, level) {
1229
1056
  return meetsLevel(grantRowLevel(call, form), level);
1230
1057
  }
1231
- async function rowsWhere(call, form, level) {
1232
- if (unchecked(call, form, "read") || grantReaches(call, form, level)) {
1058
+ async function rowsWhere(call, form, level, use = "read") {
1059
+ if (unchecked(call, form, use) || grantReaches(call, form, level)) {
1233
1060
  return void 0;
1234
1061
  }
1235
1062
  if (call.principal === null) {
@@ -1291,29 +1118,85 @@ function guardedColumns(service) {
1291
1118
  add(collection.scope.column, collection.anchor.name);
1292
1119
  }
1293
1120
  }
1294
- const moving = new Set([...anchors.values()].map(({ column }) => column));
1295
- const locked = (reads?.columns ?? []).filter((column) => column !== "id" && !moving.has(column));
1121
+ const parents = new Set((reads?.parents ?? []).map(({ via }) => via));
1122
+ const locked = (reads?.columns ?? []).filter((column) => column !== "id" && !parents.has(column));
1296
1123
  return { locked: new Set(locked), anchors: [...anchors.values()] };
1297
1124
  }
1125
+ var MEMBERSHIPS = /* @__PURE__ */ new WeakMap();
1126
+ function membershipColumnsOf(services, model) {
1127
+ let byModel = MEMBERSHIPS.get(services);
1128
+ if (byModel === void 0) {
1129
+ byModel = /* @__PURE__ */ new Map();
1130
+ MEMBERSHIPS.set(services, byModel);
1131
+ }
1132
+ const key = modelKey(model);
1133
+ const kept = byModel.get(key);
1134
+ if (kept !== void 0) {
1135
+ return kept;
1136
+ }
1137
+ const columns = /* @__PURE__ */ new Map();
1138
+ for (const service of services.values()) {
1139
+ for (const read of service.access?.reads.memberships ?? []) {
1140
+ if (modelKey(read.model) !== key) {
1141
+ continue;
1142
+ }
1143
+ for (const column of [read.entry, read.user, read.level]) {
1144
+ if (!columns.has(column)) {
1145
+ columns.set(column, service.name);
1146
+ }
1147
+ }
1148
+ }
1149
+ }
1150
+ byModel.set(key, columns);
1151
+ return columns;
1152
+ }
1298
1153
  function forbidden(message) {
1299
1154
  return new QuickdrawError("FORBIDDEN", message);
1300
1155
  }
1301
1156
  function bypasses(service, principal) {
1302
1157
  return principal !== null && service.adminBypass && serviceGrant(principal, service.name) === "Admin";
1303
1158
  }
1304
- async function checkWritableColumns(runtime, principal, data, level) {
1305
- const { service, access } = runtime;
1159
+ function namedColumns(data) {
1160
+ return new Set(Object.keys(data).filter((column) => data[column] !== void 0));
1161
+ }
1162
+ function membershipNamed(runtime, columns) {
1163
+ const { service, services } = runtime;
1164
+ if (service.model === void 0) {
1165
+ return void 0;
1166
+ }
1167
+ const memberships = membershipColumnsOf(services, service.model);
1168
+ const column = columns.find((named) => memberships.has(named));
1169
+ return column === void 0 ? void 0 : [column, memberships.get(column) ?? service.name];
1170
+ }
1171
+ function checkLockedColumns(runtime, principal, data, options = {}) {
1172
+ const { service } = runtime;
1306
1173
  if (bypasses(service, principal)) {
1307
1174
  return;
1308
1175
  }
1309
- const { locked, anchors } = guardedColumns(service);
1310
- const named = new Set(Object.keys(data).filter((column) => data[column] !== void 0));
1311
- const readOnly = [...named].find((column) => locked.has(column));
1176
+ const { locked } = guardedColumns(service);
1177
+ const named = [...namedColumns(data)];
1178
+ const readOnly = named.find((column) => locked.has(column));
1179
+ const creating = options.creating === true;
1312
1180
  if (readOnly !== void 0) {
1313
1181
  throw forbidden(
1314
- `"${readOnly}" decides who may reach rows of ${service.name}: only a service-wide Admin may change it`
1182
+ creating ? `"${readOnly}" decides who may reach rows of ${service.name}: only a service-wide Admin may send it; set it in the kit's prepare` : `"${readOnly}" decides who may reach rows of ${service.name}: only a service-wide Admin may change it`
1183
+ );
1184
+ }
1185
+ const membership = membershipNamed(runtime, named);
1186
+ if (membership !== void 0) {
1187
+ const [column, reader] = membership;
1188
+ throw forbidden(
1189
+ creating ? `"${column}" holds a membership in ${reader}'s policy: only a service-wide Admin may send it through a kit; add members with the sharing kit` : `"${column}" holds a membership in ${reader}'s policy: only a service-wide Admin may change it through a kit; change memberships with the sharing kit`
1315
1190
  );
1316
1191
  }
1192
+ }
1193
+ async function checkAnchorColumns(runtime, principal, data, level, options = {}) {
1194
+ const { service, access } = runtime;
1195
+ if (bypasses(service, principal)) {
1196
+ return;
1197
+ }
1198
+ const { anchors } = guardedColumns(service);
1199
+ const named = namedColumns(data);
1317
1200
  for (const { column, anchor } of anchors) {
1318
1201
  const target = data[column];
1319
1202
  if (!named.has(column) || target === null) {
@@ -1323,11 +1206,75 @@ async function checkWritableColumns(runtime, principal, data, level) {
1323
1206
  const resolved = valid ? await access.resolve(anchor, principal, [target], { grants: false }) : void 0;
1324
1207
  if (resolved === void 0 || !meetsLevel(resolved.levels.get(target), level)) {
1325
1208
  throw forbidden(
1326
- `Moving a row of ${service.name} by "${column}" needs ${level} on the ${anchor} row it moves into`
1209
+ options.creating === true ? `Creating a row of ${service.name} with "${column}" needs ${level} on the ${anchor} row it names` : `Moving a row of ${service.name} by "${column}" needs ${level} on the ${anchor} row it moves into`
1327
1210
  );
1328
1211
  }
1329
1212
  }
1330
1213
  }
1214
+ var relations = /* @__PURE__ */ new WeakMap();
1215
+ function relationKeysOf(service) {
1216
+ let keys = relations.get(service);
1217
+ if (keys === void 0) {
1218
+ const found = /* @__PURE__ */ new Set();
1219
+ for (const projection of service.projections.values()) {
1220
+ for (const [key, value] of Object.entries(projection.select)) {
1221
+ if (typeof value === "object" && value !== null) {
1222
+ found.add(key);
1223
+ }
1224
+ }
1225
+ }
1226
+ keys = found;
1227
+ relations.set(service, keys);
1228
+ }
1229
+ return keys;
1230
+ }
1231
+ function isPlainObject(value) {
1232
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1233
+ return false;
1234
+ }
1235
+ const prototype = Object.getPrototypeOf(value);
1236
+ return prototype === Object.prototype || prototype === null;
1237
+ }
1238
+ function columnValueIssues(service, data, path = []) {
1239
+ const relationKeys = relationKeysOf(service);
1240
+ const nested = new Map(
1241
+ findNestedWrites({ ...data }).map(({ field, operation }) => [field, operation])
1242
+ );
1243
+ const issues = [];
1244
+ for (const [key, value] of Object.entries(data)) {
1245
+ if (value === void 0) {
1246
+ continue;
1247
+ }
1248
+ const operation = nested.get(key) ?? (isPlainObject(value) && Array.isArray(value.set) ? "set" : void 0);
1249
+ if (relationKeys.has(key)) {
1250
+ issues.push({
1251
+ path: [...path, key],
1252
+ message: `"${key}" is a relation of ${service.name}'s model: kits write column values only; set its foreign key column instead`
1253
+ });
1254
+ } else if (operation !== void 0) {
1255
+ issues.push({
1256
+ path: [...path, key],
1257
+ message: `"${key}" holds a nested write ({ ${operation} }), which is not tracked: kits write column values only`
1258
+ });
1259
+ }
1260
+ }
1261
+ return issues;
1262
+ }
1263
+ function checkColumnValues(service, data, path = []) {
1264
+ const issues = columnValueIssues(service, data, path);
1265
+ if (issues.length > 0) {
1266
+ throw new QuickdrawError(
1267
+ "VALIDATION",
1268
+ "The data holds a relation or a nested write, which kits do not write",
1269
+ { issues }
1270
+ );
1271
+ }
1272
+ }
1273
+ async function checkWritableColumns(runtime, principal, data, level, path = []) {
1274
+ checkColumnValues(runtime.service, data, path);
1275
+ checkLockedColumns(runtime, principal, data);
1276
+ await checkAnchorColumns(runtime, principal, data, level);
1277
+ }
1331
1278
 
1332
1279
  // src/server/kits/transactions.ts
1333
1280
  function isWriteConflict(error) {
@@ -1447,13 +1394,13 @@ function updateHandler(form) {
1447
1394
  }
1448
1395
 
1449
1396
  // src/server/kits/crud/bulk.ts
1450
- async function writeAllowed(call, db, form, ids, write) {
1397
+ async function writeAllowed(call, db, [form, level], ids, write) {
1451
1398
  const unique = uniqueIds(ids);
1452
1399
  if (unique.length === 0) {
1453
1400
  return { count: 0 };
1454
1401
  }
1455
1402
  return await inTransaction(db, async (tx) => {
1456
- const allowed = await allowedIds(call, form, unique, rowLevel(form, "Moderate"), "write");
1403
+ const allowed = await allowedIds(call, form, unique, level, "write");
1457
1404
  if (allowed.length === 0) {
1458
1405
  return { count: 0 };
1459
1406
  }
@@ -1461,7 +1408,8 @@ async function writeAllowed(call, db, form, ids, write) {
1461
1408
  return { count };
1462
1409
  });
1463
1410
  }
1464
- function bulkUpdateHandler(form) {
1411
+ function bulkUpdateHandler(form, fallback) {
1412
+ const level = rowLevel(form, fallback);
1465
1413
  const handler = async ({ input, ctx, db }) => {
1466
1414
  const call = crudCall(ctx, db);
1467
1415
  const { ids, data } = input;
@@ -1469,24 +1417,25 @@ function bulkUpdateHandler(form) {
1469
1417
  if (Object.keys(patch).length === 0) {
1470
1418
  return { count: 0 };
1471
1419
  }
1472
- await checkWritableColumns(call.runtime, call.principal, patch, rowLevel(form, "Moderate"));
1420
+ await checkWritableColumns(call.runtime, call.principal, patch, level, ["data"]);
1473
1421
  return await writeAllowed(
1474
1422
  call,
1475
1423
  db,
1476
- form,
1424
+ [form, level],
1477
1425
  ids,
1478
1426
  (table, allowed) => table.updateMany({ where: { id: { in: [...allowed] } }, data: patch })
1479
1427
  );
1480
1428
  };
1481
1429
  return handler;
1482
1430
  }
1483
- function bulkDeleteHandler(form) {
1431
+ function bulkDeleteHandler(form, fallback) {
1432
+ const level = rowLevel(form, fallback);
1484
1433
  const handler = async ({ input, ctx, db }) => {
1485
1434
  const call = crudCall(ctx, db);
1486
1435
  return await writeAllowed(
1487
1436
  call,
1488
1437
  db,
1489
- form,
1438
+ [form, level],
1490
1439
  input.ids,
1491
1440
  (table, allowed) => table.deleteMany({ where: { id: { in: [...allowed] } } })
1492
1441
  );
@@ -1495,16 +1444,40 @@ function bulkDeleteHandler(form) {
1495
1444
  }
1496
1445
 
1497
1446
  // src/server/kits/crud/create.ts
1498
- function createHandler(prepare) {
1447
+ function isRecord5(value) {
1448
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1449
+ }
1450
+ function isPlainObject2(value) {
1451
+ if (!isRecord5(value)) {
1452
+ return false;
1453
+ }
1454
+ const prototype = Object.getPrototypeOf(value);
1455
+ return prototype === Object.prototype || prototype === null;
1456
+ }
1457
+ function createHandler(form, prepare) {
1499
1458
  const handler = async ({ input, ctx, db }) => {
1500
1459
  const call = crudCall(ctx, db);
1460
+ const { service } = call.runtime;
1461
+ const sent = isRecord5(input) ? input : {};
1462
+ checkColumnValues(service, sent);
1463
+ checkLockedColumns(call.runtime, call.principal, sent, { creating: true });
1501
1464
  const data = prepare === void 0 ? input : await checked(() => prepare(input, ctx, db));
1502
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
1465
+ if (!isPlainObject2(data)) {
1466
+ throw new QuickdrawError(
1467
+ "INTERNAL",
1468
+ `The read/write kit's prepare for ${service.name} must return the new row's data`
1469
+ );
1470
+ }
1471
+ const issues = columnValueIssues(service, data);
1472
+ if (issues.length > 0) {
1503
1473
  throw new QuickdrawError(
1504
1474
  "INTERNAL",
1505
- `The read/write kit's prepare for ${call.runtime.service.name} must return the new row's data`
1475
+ `The read/write kit's prepare for ${service.name} must return column values: ${issues.map((issue) => issue.message).join("; ")}`
1506
1476
  );
1507
1477
  }
1478
+ await checkAnchorColumns(call.runtime, call.principal, data, rowLevel(form, "Moderate"), {
1479
+ creating: true
1480
+ });
1508
1481
  const { select } = projectionOf(call, "entity");
1509
1482
  return await call.table.create({ data, select });
1510
1483
  };
@@ -1602,20 +1575,33 @@ function signatureOf(order) {
1602
1575
  function signed(order) {
1603
1576
  return [[SIGNATURE, "asc"], ...order];
1604
1577
  }
1578
+ var ROW = "row ";
1579
+ var ROW_CURSOR = [
1580
+ [SIGNATURE, "asc"],
1581
+ ["id", "asc"]
1582
+ ];
1605
1583
  function cursorAfter(order, row) {
1606
- return encodeCursor(signed(order), { ...row, [SIGNATURE]: signatureOf(order) });
1584
+ const cursor = encodeCursor(signed(order), { ...row, [SIGNATURE]: signatureOf(order) });
1585
+ return cursor.length <= MAX_CURSOR_LENGTH ? cursor : encodeCursor(ROW_CURSOR, { [SIGNATURE]: `${ROW}${signatureOf(order)}`, id: row.id });
1607
1586
  }
1608
- function cursorValues(order, cursor) {
1609
- let values;
1610
- try {
1611
- values = decodeCursor(signed(order), cursor);
1612
- } catch {
1613
- throw unreadable(NOT_A_CURSOR, ["cursor"]);
1587
+ function readCursor(order, cursor) {
1588
+ const signature = signatureOf(order);
1589
+ const decoded = (shape) => {
1590
+ try {
1591
+ return decodeCursor(shape, cursor);
1592
+ } catch {
1593
+ return void 0;
1594
+ }
1595
+ };
1596
+ const values = decoded(signed(order));
1597
+ if (values?.[0] === signature) {
1598
+ return { values: values.slice(1) };
1614
1599
  }
1615
- if (values[0] !== signatureOf(order)) {
1616
- throw unreadable(NOT_A_CURSOR, ["cursor"]);
1600
+ const named = decoded(ROW_CURSOR);
1601
+ if (named?.[0] === `${ROW}${signature}` && typeof named[1] === "string") {
1602
+ return { row: named[1] };
1617
1603
  }
1618
- return values.slice(1);
1604
+ throw unreadable(NOT_A_CURSOR, ["cursor"]);
1619
1605
  }
1620
1606
  async function nullableColumns(read, values) {
1621
1607
  const { storage, model, order } = read;
@@ -1652,8 +1638,27 @@ function refusal(error, read) {
1652
1638
  refused.cause = error;
1653
1639
  return refused;
1654
1640
  }
1641
+ async function startValues(read, cursor) {
1642
+ const held = readCursor(read.order, cursor);
1643
+ if ("values" in held) {
1644
+ return held.values;
1645
+ }
1646
+ let row;
1647
+ try {
1648
+ row = await read.table.findFirst({
1649
+ where: { AND: [read.where, { id: held.row }] },
1650
+ select: Object.fromEntries(read.order.map(([column]) => [column, true]))
1651
+ });
1652
+ } catch (error) {
1653
+ throw refusal(error, read);
1654
+ }
1655
+ if (row === null) {
1656
+ throw unreadable(NOT_A_CURSOR, ["cursor"]);
1657
+ }
1658
+ return read.order.map(([column]) => row[column]);
1659
+ }
1655
1660
  async function readPage(read) {
1656
- const values = read.cursor === void 0 ? void 0 : cursorValues(read.order, read.cursor);
1661
+ const values = read.cursor === void 0 ? void 0 : await startValues(read, read.cursor);
1657
1662
  const nullable = await nullableColumns(read, values);
1658
1663
  const where = values === void 0 ? read.where : { AND: [read.where, afterCursor(read.order, values, nullable)] };
1659
1664
  let rows;
@@ -1762,14 +1767,17 @@ async function nextOrdinal(db, model, where = {}, options = {}) {
1762
1767
  function invalid(message, path) {
1763
1768
  return new QuickdrawError("VALIDATION", message, { issues: [{ path: [...path], message }] });
1764
1769
  }
1765
- async function readNamed(table, spec, move, model) {
1770
+ async function readNamed(table, [spec, writable], move, model) {
1766
1771
  const select = Object.fromEntries(["id", spec.column, ...spec.within].map((key) => [key, true]));
1767
- const ids = [move.id, move.beforeId, move.afterId].filter((id) => id !== void 0);
1768
- const rows = await table.findMany({ where: { id: { in: ids } }, select });
1772
+ const ids = {
1773
+ id: { in: [move.id, move.beforeId, move.afterId].filter((id) => id !== void 0) }
1774
+ };
1775
+ const where = writable === void 0 ? ids : { AND: [ids, writable] };
1776
+ const rows = writable === "none" ? [] : await table.findMany({ where, select });
1769
1777
  const byId = new Map(rows.map((row) => [row.id, row]));
1770
1778
  const moved = byId.get(move.id);
1771
1779
  if (moved === void 0) {
1772
- throw new QuickdrawError("NOT_FOUND", `No such ${model}`);
1780
+ throw writable === void 0 ? new QuickdrawError("NOT_FOUND", `No such ${model}`) : new QuickdrawError("FORBIDDEN", "Insufficient permissions");
1773
1781
  }
1774
1782
  const neighbor = (id, key) => {
1775
1783
  const row = id === void 0 ? void 0 : byId.get(id);
@@ -1785,9 +1793,10 @@ async function readNamed(table, spec, move, model) {
1785
1793
  after: neighbor(move.afterId, "afterId")
1786
1794
  };
1787
1795
  }
1788
- function listOf(spec, moved) {
1796
+ function listOf(spec, moved, writable) {
1789
1797
  const scope = Object.fromEntries(spec.within.map((column) => [column, moved[column] ?? null]));
1790
- return { AND: [scope, { id: { not: moved.id } }] };
1798
+ const mine = writable === void 0 ? [] : [writable === "none" ? { id: { in: [] } } : writable];
1799
+ return { AND: [scope, { id: { not: moved.id } }, ...mine] };
1791
1800
  }
1792
1801
  async function adjacent(table, spec, list, row, side) {
1793
1802
  const { column } = spec;
@@ -1805,10 +1814,10 @@ async function adjacent(table, spec, list, row, side) {
1805
1814
  select: { id: true, [column]: true }
1806
1815
  });
1807
1816
  }
1808
- async function bounds(table, spec, named) {
1817
+ async function bounds(table, spec, named, writable) {
1809
1818
  const { column } = spec;
1810
1819
  const { before, after } = named;
1811
- const list = listOf(spec, named.moved);
1820
+ const list = listOf(spec, named.moved, writable);
1812
1821
  const low = before ?? (after === void 0 ? null : await adjacent(table, spec, list, after, "previous"));
1813
1822
  const high = after ?? (before === void 0 ? null : await adjacent(table, spec, list, before, "next"));
1814
1823
  const lowValue = low === null ? null : low[column];
@@ -1822,14 +1831,21 @@ async function bounds(table, spec, named) {
1822
1831
  }
1823
1832
  return [lowValue, highValue];
1824
1833
  }
1825
- async function renumber(table, spec, named, move) {
1834
+ async function renumber(table, [spec, writable], named, move) {
1826
1835
  const { column } = spec;
1827
1836
  const rows = await table.findMany({
1828
- where: listOf(spec, named.moved),
1837
+ where: listOf(spec, named.moved, writable),
1829
1838
  select: { id: true, [column]: true },
1830
1839
  orderBy: [{ [column]: "asc" }, { id: "asc" }]
1831
1840
  });
1832
1841
  const order = rows.map((row) => row.id);
1842
+ const neighbor = move.beforeId ?? move.afterId;
1843
+ if (neighbor !== void 0 && !order.includes(neighbor)) {
1844
+ throw new QuickdrawError(
1845
+ "INTERNAL",
1846
+ "A reorder's neighbor is missing from the list it renumbers"
1847
+ );
1848
+ }
1833
1849
  const at = move.beforeId === void 0 ? Math.max(order.indexOf(move.afterId), 0) : order.indexOf(move.beforeId) + 1;
1834
1850
  order.splice(at, 0, move.id);
1835
1851
  const current = new Map(rows.map((row) => [row.id, row[column]]));
@@ -1848,29 +1864,48 @@ var RENUMBER_BASE_MS = 5e3;
1848
1864
  var RENUMBER_MS_PER_ROW = 10;
1849
1865
  var OWNER = "The read/write kit's reorder";
1850
1866
  var CONFLICT = "Another change to this list ran at the same time; try again";
1851
- async function moveIn(tx, { call, spec, move, select }, mayRenumber) {
1867
+ async function moveIn(tx, { call, spec, move, writable, select }, mayRenumber) {
1852
1868
  const table = delegateOf(tx, call.model);
1853
- const named = await readNamed(table, spec, move, call.model);
1854
- const range = await bounds(table, spec, named);
1869
+ const named = await readNamed(table, [spec, writable], move, call.model);
1870
+ const range = await bounds(table, spec, named, writable);
1855
1871
  const between = range === void 0 ? void 0 : ordinalBetween(range[0] ?? void 0, range[1] ?? void 0);
1856
1872
  if (between === void 0 && !mayRenumber) {
1857
- return { list: listOf(spec, named.moved) };
1873
+ return { list: listOf(spec, named.moved, writable) };
1858
1874
  }
1859
- const value = between ?? await renumber(table, spec, named, move);
1875
+ const value = between ?? await renumber(table, [spec, writable], named, move);
1860
1876
  return {
1861
1877
  row: await table.update({ where: { id: move.id }, data: { [spec.column]: value }, select })
1862
1878
  };
1863
1879
  }
1880
+ async function checkNamed(call, [form, level], move) {
1881
+ const named = [move.beforeId, move.afterId].filter((id) => id !== void 0);
1882
+ const ids = uniqueIds([move.id, ...named]);
1883
+ const writable = new Set(await allowedIds(call, form, ids, level, "write"));
1884
+ if (!writable.has(move.id)) {
1885
+ throw new QuickdrawError("FORBIDDEN", "Insufficient permissions");
1886
+ }
1887
+ for (const [id, key] of [
1888
+ [move.beforeId, "beforeId"],
1889
+ [move.afterId, "afterId"]
1890
+ ]) {
1891
+ if (id !== void 0 && !writable.has(id)) {
1892
+ throw new QuickdrawError("NOT_FOUND", `No such ${call.model} in this list as ${key}`);
1893
+ }
1894
+ }
1895
+ }
1864
1896
  function reorderHandler(spec, form) {
1897
+ const level = rowLevel(form, "Moderate");
1865
1898
  const handler = async ({ input, ctx, db }) => {
1866
1899
  const call = crudCall(ctx, db);
1900
+ const asked = input;
1901
+ await checkNamed(call, [form, level], asked);
1867
1902
  const move = {
1868
1903
  call,
1869
1904
  spec,
1870
- move: input,
1905
+ move: asked,
1906
+ writable: await rowsWhere(call, form, level, "write"),
1871
1907
  select: projectionOf(call, "entity").select
1872
1908
  };
1873
- await checkRowWrite(call, form, move.move.id);
1874
1909
  const how = { owner: OWNER, conflict: CONFLICT };
1875
1910
  const first = await inKitTransaction(db, async (tx) => await moveIn(tx, move, false), how);
1876
1911
  if ("row" in first) {
@@ -1901,7 +1936,7 @@ function handlerOf(context) {
1901
1936
  case "list":
1902
1937
  return listHandler({ spec, form, projection: context.projection });
1903
1938
  case "create":
1904
- return createHandler(context.prepare);
1939
+ return createHandler(form, context.prepare);
1905
1940
  case "update":
1906
1941
  return updateHandler(form);
1907
1942
  case "delete":
@@ -1909,9 +1944,9 @@ function handlerOf(context) {
1909
1944
  case "reorder":
1910
1945
  return reorderHandler(spec, form);
1911
1946
  case "bulkUpdate":
1912
- return bulkUpdateHandler(form);
1947
+ return bulkUpdateHandler(form, context.bulkLevel);
1913
1948
  default:
1914
- return bulkDeleteHandler(form);
1949
+ return bulkDeleteHandler(form, context.bulkLevel);
1915
1950
  }
1916
1951
  }
1917
1952
 
@@ -1989,7 +2024,14 @@ var CHECKS_ROWS = /* @__PURE__ */ new Set([
1989
2024
  "delete",
1990
2025
  "reorder"
1991
2026
  ]);
1992
- var OPTION_KEYS2 = ["access", "prepare", "rowless"];
2027
+ var CALLER_DEPENDENT = /* @__PURE__ */ new Set(["list", "getMany"]);
2028
+ var COUNTERPARTS = { bulkDelete: "delete", bulkUpdate: "update" };
2029
+ function bulkLevelOf(kit, access, spec) {
2030
+ const counterpart = spec.method === "bulkDelete" || spec.method === "bulkUpdate" ? COUNTERPARTS[spec.method] : void 0;
2031
+ const found = kit.find(([, other]) => other.method === counterpart);
2032
+ return bulkFallback(found === void 0 ? void 0 : access[found[0]]);
2033
+ }
2034
+ var OPTION_KEYS = ["access", "prepare", "rowless"];
1993
2035
  function serviceProblem(service, contract, [name, spec], form) {
1994
2036
  if (service.contract !== contract) {
1995
2037
  return `its read/write kit handlers were made for another contract; pass ${service.name}'s own contract to crud.handlers`;
@@ -2004,9 +2046,9 @@ function handlers(contract, options) {
2004
2046
  if (!isRecord6(options)) {
2005
2047
  fail("options must be { access, prepare?, rowless? }");
2006
2048
  }
2007
- const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS2.includes(key));
2049
+ const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS.includes(key));
2008
2050
  if (unknownKey !== void 0) {
2009
- fail(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS2.join(", ")}`);
2051
+ fail(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS.join(", ")}`);
2010
2052
  }
2011
2053
  const names = kit.map(([name]) => name);
2012
2054
  const access = checkAccess(options.access, names);
@@ -2016,11 +2058,15 @@ function handlers(contract, options) {
2016
2058
  for (const [name, spec] of kit) {
2017
2059
  const form = access[name];
2018
2060
  const projection = spec.method === "list" ? itemProjection(contract, spec) : "entity";
2019
- const handler = handlerOf({ spec, form, projection, prepare });
2061
+ const bulkLevel = bulkLevelOf(kit, access, spec);
2062
+ const handler = handlerOf({ spec, form, projection, prepare, bulkLevel });
2020
2063
  checkWhenDefined(handler, (service) => serviceProblem(service, contract, [name, spec], form));
2021
2064
  if (CHECKS_ROWS.has(spec.method)) {
2022
2065
  checksRowsItself(handler);
2023
2066
  }
2067
+ if (CALLER_DEPENDENT.has(spec.method)) {
2068
+ dependsOnCaller(handler);
2069
+ }
2024
2070
  entries[name] = kitEntry(name, form, handler, rowless);
2025
2071
  }
2026
2072
  return Object.freeze(entries);
@@ -2106,9 +2152,7 @@ async function scopeWhere(run) {
2106
2152
  return await membersWhere(reader, collection, run.query.scope, collection.maxLimit) ?? "none";
2107
2153
  }
2108
2154
  function searchOrder(run) {
2109
- const { scope } = run.context.spec;
2110
- const collection = scope === void 0 ? void 0 : run.call.runtime.service.collections.get(scope);
2111
- return collection?.order ?? ID_ORDER;
2155
+ return scopedCollection(run)?.order ?? ID_ORDER;
2112
2156
  }
2113
2157
  async function strategyIds(run, ids) {
2114
2158
  const found = await checked(() => ids(run.query.q, run.ctx, { limit: run.query.limit }));
@@ -2272,14 +2316,14 @@ function checkStrategy(strategy) {
2272
2316
  }
2273
2317
  return strategy;
2274
2318
  }
2275
- var OPTION_KEYS3 = ["access", "strategy", "method"];
2319
+ var OPTION_KEYS2 = ["access", "strategy", "method"];
2276
2320
  function checkOptions(options, kit) {
2277
2321
  if (!isRecord8(options)) {
2278
2322
  fail2("options must be { access, strategy?, method? }");
2279
2323
  }
2280
- const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS3.includes(key));
2324
+ const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS2.includes(key));
2281
2325
  if (unknownKey !== void 0) {
2282
- fail2(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS3.join(", ")}`);
2326
+ fail2(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS2.join(", ")}`);
2283
2327
  }
2284
2328
  const problem = accessFormProblem(options.access);
2285
2329
  if (problem !== void 0) {
@@ -2344,6 +2388,7 @@ function handlers2(contract, options) {
2344
2388
  checkScope(contract, name, spec, projection);
2345
2389
  const handler = searchHandler({ spec, form, projection, strategy });
2346
2390
  checkWhenDefined(handler, (service) => serviceProblem2(service, contract, name, form));
2391
+ dependsOnCaller(handler);
2347
2392
  entries[name] = Object.freeze({ access: form, handler, share: "caller" });
2348
2393
  }
2349
2394
  return Object.freeze(entries);
@@ -2618,6 +2663,14 @@ function capGrant(own, given, model) {
2618
2663
  );
2619
2664
  }
2620
2665
  }
2666
+ function capTarget(own, current, model) {
2667
+ if (current !== null && !meetsLevel(own, current)) {
2668
+ throw new QuickdrawError(
2669
+ "FORBIDDEN",
2670
+ `That user has ${current} on this ${model}, above your own level on it (${own ?? "none"}); you cannot change or take away their access`
2671
+ );
2672
+ }
2673
+ }
2621
2674
 
2622
2675
  // src/server/kits/sharing/context.ts
2623
2676
  async function resolveTarget(call, context, given) {
@@ -2692,15 +2745,17 @@ async function checkLastAdmin(call, tx, listed, change) {
2692
2745
  async function changeList(call, context, change) {
2693
2746
  const columns = aclColumnsOf(call.runtime.service);
2694
2747
  return await inSerializable(call.db, async (tx) => {
2695
- capGrant(await checkCaller(call, context.form, change.id), change.level, call.model);
2748
+ const own = await checkCaller(call, context.form, change.id);
2749
+ capGrant(own, change.level, call.model);
2696
2750
  const table = tableOf(tx, call.model);
2697
2751
  const listed = await readList(table, columns, change.id, call.model);
2698
2752
  const problem = refusal2(listed, change, call.model);
2699
2753
  if (problem !== void 0) {
2700
2754
  throw problem;
2701
2755
  }
2702
- await checkLastAdmin(call, tx, listed, change);
2703
2756
  const before = levelIn(listed.entries, change.userId);
2757
+ capTarget(own, before, call.model);
2758
+ await checkLastAdmin(call, tx, listed, change);
2704
2759
  if (before === change.level && entriesOf(listed.entries, change.userId) === 1) {
2705
2760
  return sharesOf(listed.entries);
2706
2761
  }
@@ -2852,15 +2907,16 @@ async function invite(call, context, target, role) {
2852
2907
  async function end(call, context, target, kind) {
2853
2908
  const read = membershipTableOf(call.runtime.service);
2854
2909
  return await inSerializable(call.db, async (tx) => {
2855
- if (kind === "remove") {
2856
- await checkCaller(call, context.form, target.entryId);
2857
- }
2910
+ const own = kind === "remove" ? await checkCaller(call, context.form, target.entryId) : void 0;
2858
2911
  const table = tableOf(tx, read.model);
2859
2912
  const rows = await rowsOf(table, read, target);
2860
2913
  if (rows.length === 0) {
2861
2914
  throw kind === "leave" ? new QuickdrawError("FORBIDDEN", `You are not a member of this ${call.model}`) : new QuickdrawError("NOT_FOUND", `That user is not a member of this ${call.model}`);
2862
2915
  }
2863
2916
  const current = currentRole(read, rows);
2917
+ if (own !== void 0) {
2918
+ capTarget(own, current.level, call.model);
2919
+ }
2864
2920
  if (current.level === "Admin" && !await anotherAdmin(call, tx, read, target)) {
2865
2921
  throw lastAdmin(call.model);
2866
2922
  }
@@ -2883,6 +2939,7 @@ async function changeRole(call, context, target, role) {
2883
2939
  throw new QuickdrawError("NOT_FOUND", `That user is not a member of this ${call.model}`);
2884
2940
  }
2885
2941
  const current = currentRole(read, rows);
2942
+ capTarget(own, current.level, call.model);
2886
2943
  if (rows.length === 1 && current.role === role) {
2887
2944
  return memberOf(read, target.userId, role);
2888
2945
  }
@@ -3071,6 +3128,9 @@ function handlers3(contract, ...rest) {
3071
3128
  const form = access[name] ?? DEFAULT_ACCESS[spec.method];
3072
3129
  const handler = handlerOf2(spec.method, Object.freeze({ ...hooks, form }));
3073
3130
  checkWhenDefined(handler, (service) => serviceProblem3(service, contract, spec.mode));
3131
+ if (spec.method === "listShares" || spec.method === "listMembers") {
3132
+ dependsOnCaller(handler);
3133
+ }
3074
3134
  entries[name] = kitEntry(name, form, handler, rowless);
3075
3135
  }
3076
3136
  return Object.freeze(entries);
@@ -3123,6 +3183,13 @@ var DEFAULT_FIELDS = Object.freeze({
3123
3183
  showInTable: false
3124
3184
  }
3125
3185
  });
3186
+ function grantNamesOf(column) {
3187
+ return column === void 0 || ADMIN_GRANT_FIELDS.includes(column) ? ADMIN_GRANT_FIELDS : [...ADMIN_GRANT_FIELDS, column];
3188
+ }
3189
+ function grantsColumnFor(model, source) {
3190
+ const same = model !== void 0 && source !== void 0;
3191
+ return same && modelKey(source.model) === modelKey(model) ? source.column : void 0;
3192
+ }
3126
3193
  function isRecord10(value) {
3127
3194
  return typeof value === "object" && value !== null && !Array.isArray(value);
3128
3195
  }
@@ -3142,23 +3209,19 @@ function checkDisplayName(displayName, serviceName, fail5) {
3142
3209
  }
3143
3210
  return displayName;
3144
3211
  }
3145
- function checkGrants(grants, names, fail5) {
3212
+ function checkGrants(grants, [names, grantNames], fail5) {
3146
3213
  if (grants === void 0 || grants === false) {
3147
3214
  return /* @__PURE__ */ new Set();
3148
3215
  }
3149
3216
  if (grants !== true) {
3150
3217
  fail5("grants must be true or false");
3151
3218
  }
3152
- const fields = ADMIN_GRANT_FIELDS.filter((name) => names.includes(name));
3153
- if (fields.length === 0) {
3154
- fail5(
3155
- `grants: the entity has no ${ADMIN_GRANT_FIELDS.join(" or ")} field for the kit to show and write`
3156
- );
3157
- }
3158
- return new Set(fields);
3219
+ return new Set(grantNames.filter((name) => names.includes(name)));
3159
3220
  }
3160
- function checkHidden(hiddenFields, names, grants, fail5) {
3161
- const defaults = ADMIN_HIDDEN_FIELDS.filter((name) => !grants.has(name));
3221
+ function checkHidden(hiddenFields, [names, grantNames], grants, fail5) {
3222
+ const defaults = [.../* @__PURE__ */ new Set([...ADMIN_HIDDEN_FIELDS, ...grantNames])].filter(
3223
+ (name) => !grants.has(name)
3224
+ );
3162
3225
  if (hiddenFields === void 0) {
3163
3226
  return new Set(defaults);
3164
3227
  }
@@ -3293,8 +3356,9 @@ function frozenConfig(config) {
3293
3356
  }
3294
3357
  function adminFieldsOf(serviceName, spec, options, fail5) {
3295
3358
  const names = spec.fields.map((field) => field.name);
3296
- const grants = checkGrants(options.grants, names, fail5);
3297
- const hidden = checkHidden(options.hiddenFields, names, grants, fail5);
3359
+ const grantNames = grantNamesOf(options.grantsColumn);
3360
+ const grants = checkGrants(options.grants, [names, grantNames], fail5);
3361
+ const hidden = checkHidden(options.hiddenFields, [names, grantNames], grants, fail5);
3298
3362
  const listed = [...spec.filter, ...spec.sort].find((name) => hidden.has(name));
3299
3363
  if (listed !== void 0) {
3300
3364
  fail5(`"${listed}" is hidden, so adminList may not filter or sort on it`);
@@ -3322,10 +3386,26 @@ function adminFieldsOf(serviceName, spec, options, fail5) {
3322
3386
  fields: Object.freeze(fields)
3323
3387
  });
3324
3388
  const readOnly = new Set(fields.filter((field) => !field.editable).map((field) => field.name));
3325
- return Object.freeze({ meta, hidden, readOnly, grants });
3389
+ const named = /* @__PURE__ */ new Set([
3390
+ ...editable ?? [],
3391
+ ...Object.entries(overrides).filter(([, override]) => override.editable === true).map(([name]) => name)
3392
+ ]);
3393
+ return Object.freeze({ meta, hidden, readOnly, grants, named });
3394
+ }
3395
+ function withReadOnlyFields(meta, readOnly) {
3396
+ if (!meta.fields.some((field) => readOnly.has(field.name) && field.editable)) {
3397
+ return meta;
3398
+ }
3399
+ const fields = meta.fields.map(
3400
+ (field) => readOnly.has(field.name) && field.editable ? frozenConfig({ ...field, editable: false }) : field
3401
+ );
3402
+ return Object.freeze({ ...meta, fields: Object.freeze(fields) });
3326
3403
  }
3327
3404
 
3328
3405
  // src/server/kits/admin/runtime.ts
3406
+ function grantsColumnOf(runtime) {
3407
+ return grantsColumnFor(runtime.service.model, runtime.grantsSource);
3408
+ }
3329
3409
  function tableOf2(db, model) {
3330
3410
  const name = modelKey(model);
3331
3411
  const isClient = (typeof db === "object" || typeof db === "function") && db !== null;
@@ -3339,7 +3419,7 @@ function tableOf2(db, model) {
3339
3419
  }
3340
3420
  return delegate;
3341
3421
  }
3342
- function adminCall(ctx, db, fields) {
3422
+ function adminCall(ctx, db, fieldsFor) {
3343
3423
  const runtime = kitRuntimeOf(ctx);
3344
3424
  const model = runtime?.service.model;
3345
3425
  const projection = runtime?.service.projections.get("entity");
@@ -3349,6 +3429,7 @@ function adminCall(ctx, db, fields) {
3349
3429
  "The admin kit's handlers run through a dispatcher, in a service with a model and an entity"
3350
3430
  );
3351
3431
  }
3432
+ const fields = fieldsFor(grantsColumnOf(runtime));
3352
3433
  const { principal } = ctx;
3353
3434
  const level = principal === null ? null : serviceGrant(principal, runtime.service.name) ?? null;
3354
3435
  const tiered = projection.tiers.hidden(level);
@@ -3360,7 +3441,8 @@ function adminCall(ctx, db, fields) {
3360
3441
  principal,
3361
3442
  projection,
3362
3443
  unseen,
3363
- omitted: /* @__PURE__ */ new Set([...unseen, ...fields.hidden])
3444
+ omitted: /* @__PURE__ */ new Set([...unseen, ...fields.hidden]),
3445
+ fields
3364
3446
  };
3365
3447
  }
3366
3448
  function rowOut(call, row) {
@@ -3384,7 +3466,7 @@ function checkSeen(call, names) {
3384
3466
  var NO_NULLABLE = /* @__PURE__ */ new Set();
3385
3467
  function adminListHandler(context) {
3386
3468
  const handler = async ({ input, ctx, db }) => {
3387
- const call = adminCall(ctx, db, context.fields);
3469
+ const call = adminCall(ctx, db, context.fieldsFor);
3388
3470
  const query = input;
3389
3471
  checkSeen(call, namedFields(query));
3390
3472
  const order = listOrder(defaultSorts(context.spec.sort, call.unseen), query.sort);
@@ -3435,14 +3517,14 @@ function subscribersOf(call, id) {
3435
3517
  }
3436
3518
  function adminSubscribersHandler(context) {
3437
3519
  const handler = ({ input, ctx, db }) => {
3438
- const call = adminCall(ctx, db, context.fields);
3520
+ const call = adminCall(ctx, db, context.fieldsFor);
3439
3521
  return Promise.resolve(subscribersOf(call, input.id));
3440
3522
  };
3441
3523
  return handler;
3442
3524
  }
3443
3525
  function adminReemitHandler(context) {
3444
3526
  const handler = async ({ input, ctx, db }) => {
3445
- const call = adminCall(ctx, db, context.fields);
3527
+ const call = adminCall(ctx, db, context.fieldsFor);
3446
3528
  const { id } = input;
3447
3529
  requireRow(
3448
3530
  await call.table.findUnique({ where: { id }, select: { id: true } }),
@@ -3456,7 +3538,7 @@ function adminReemitHandler(context) {
3456
3538
 
3457
3539
  // src/server/kits/admin/writes.ts
3458
3540
  async function checkWrite(call, context, data) {
3459
- const { hidden, readOnly } = context.fields;
3541
+ const { hidden, readOnly } = call.fields;
3460
3542
  const names = Object.keys(data);
3461
3543
  const issues = names.filter((name) => hidden.has(name) || readOnly.has(name)).map((name) => ({
3462
3544
  path: ["data", name],
@@ -3472,7 +3554,8 @@ async function checkWrite(call, context, data) {
3472
3554
  call.runtime,
3473
3555
  call.principal,
3474
3556
  data,
3475
- rowLevel(context.form, "Moderate")
3557
+ rowLevel(context.form, "Moderate"),
3558
+ ["data"]
3476
3559
  );
3477
3560
  }
3478
3561
  function isValueRefusal(error) {
@@ -3516,7 +3599,7 @@ function committed(context, args, write) {
3516
3599
  category: "quickdraw.admin",
3517
3600
  method: write.method,
3518
3601
  id: write.id,
3519
- error: error instanceof Error ? error.message : String(error)
3602
+ error: errorText(error)
3520
3603
  });
3521
3604
  };
3522
3605
  try {
@@ -3561,7 +3644,7 @@ async function written(context, call, args, method, perform) {
3561
3644
  }
3562
3645
  function adminGetHandler(context) {
3563
3646
  const handler = async ({ input, ctx, db }) => {
3564
- const call = adminCall(ctx, db, context.fields);
3647
+ const call = adminCall(ctx, db, context.fieldsFor);
3565
3648
  return await readRow(call, input.id);
3566
3649
  };
3567
3650
  return handler;
@@ -3571,7 +3654,7 @@ function idOf(row) {
3571
3654
  }
3572
3655
  function adminCreateHandler(context) {
3573
3656
  const handler = async (args) => {
3574
- const call = adminCall(args.ctx, args.db, context.fields);
3657
+ const call = adminCall(args.ctx, args.db, context.fieldsFor);
3575
3658
  const { data } = args.input;
3576
3659
  await checkWrite(call, context, data);
3577
3660
  return await written(context, call, args, "adminCreate", async (table) => {
@@ -3583,7 +3666,7 @@ function adminCreateHandler(context) {
3583
3666
  }
3584
3667
  function adminUpdateHandler(context) {
3585
3668
  const handler = async (args) => {
3586
- const call = adminCall(args.ctx, args.db, context.fields);
3669
+ const call = adminCall(args.ctx, args.db, context.fieldsFor);
3587
3670
  const { id, data } = args.input;
3588
3671
  await checkWrite(call, context, data);
3589
3672
  if (Object.keys(data).length === 0) {
@@ -3600,7 +3683,7 @@ function adminUpdateHandler(context) {
3600
3683
  }
3601
3684
  function adminDeleteHandler(context) {
3602
3685
  const handler = async (args) => {
3603
- const call = adminCall(args.ctx, args.db, context.fields);
3686
+ const call = adminCall(args.ctx, args.db, context.fieldsFor);
3604
3687
  const { id } = args.input;
3605
3688
  return await written(context, call, args, "adminDelete", async (table, withBefore) => {
3606
3689
  const select = withBefore ? call.projection.select : { id: true };
@@ -3623,8 +3706,23 @@ var MAKERS2 = Object.freeze({
3623
3706
  adminReemit: adminReemitHandler
3624
3707
  });
3625
3708
  function adminMetaHandler(context) {
3626
- const { meta } = context.fields;
3627
- const handler = (_args) => Promise.resolve(meta);
3709
+ const byService = /* @__PURE__ */ new WeakMap();
3710
+ const handler = (args) => {
3711
+ const runtime = kitRuntimeOf(args.ctx);
3712
+ if (runtime === void 0) {
3713
+ return Promise.resolve(context.fieldsFor(void 0).meta);
3714
+ }
3715
+ const { service } = runtime;
3716
+ const column = grantsColumnFor(service.model, runtime.grantsSource);
3717
+ const answers = byService.get(service) ?? /* @__PURE__ */ new Map();
3718
+ byService.set(service, answers);
3719
+ let answer = answers.get(column ?? "");
3720
+ if (answer === void 0) {
3721
+ answer = withReadOnlyFields(context.fieldsFor(column).meta, relationKeysOf(service));
3722
+ answers.set(column ?? "", answer);
3723
+ }
3724
+ return Promise.resolve(answer);
3725
+ };
3628
3726
  return handler;
3629
3727
  }
3630
3728
  function handlerOf3(context) {
@@ -3633,7 +3731,7 @@ function handlerOf3(context) {
3633
3731
  }
3634
3732
 
3635
3733
  // src/server/kits/admin/handlers.ts
3636
- var OPTION_KEYS4 = [
3734
+ var OPTION_KEYS3 = [
3637
3735
  "access",
3638
3736
  "displayName",
3639
3737
  "hiddenFields",
@@ -3681,9 +3779,9 @@ function checkOptions3(options) {
3681
3779
  "options must be { access?, displayName?, hiddenFields?, editable?, fieldOverrides?, rowless?, grants?, onWrite?, onCommitted? }"
3682
3780
  );
3683
3781
  }
3684
- const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS4.includes(key));
3782
+ const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS3.includes(key));
3685
3783
  if (unknownKey !== void 0) {
3686
- fail4(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS4.join(", ")}`);
3784
+ fail4(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS3.join(", ")}`);
3687
3785
  }
3688
3786
  return options;
3689
3787
  }
@@ -3719,7 +3817,7 @@ function contractEditable(contract, specs) {
3719
3817
  }
3720
3818
  return [...lists.values()][0];
3721
3819
  }
3722
- function fieldsOf(contract, kit, options) {
3820
+ function fieldsOf(contract, kit, options, grantsColumn) {
3723
3821
  const specs = kit.map(([, spec2]) => spec2);
3724
3822
  const [first] = specs;
3725
3823
  const spec = {
@@ -3736,16 +3834,47 @@ function fieldsOf(contract, kit, options) {
3736
3834
  hiddenFields: options.hiddenFields,
3737
3835
  editable: options.editable,
3738
3836
  fieldOverrides: options.fieldOverrides,
3739
- grants: options.grants
3837
+ grants: options.grants,
3838
+ ...grantsColumn === void 0 ? {} : { grantsColumn }
3740
3839
  },
3741
3840
  fail4
3742
3841
  );
3743
3842
  }
3744
- function serviceProblem4(service, contract) {
3843
+ function fieldsByColumn(contract, kit, options) {
3844
+ const byColumn = /* @__PURE__ */ new Map();
3845
+ return (grantsColumn) => {
3846
+ let fields = byColumn.get(grantsColumn ?? "");
3847
+ if (fields === void 0) {
3848
+ fields = fieldsOf(contract, kit, options, grantsColumn);
3849
+ byColumn.set(grantsColumn ?? "", fields);
3850
+ }
3851
+ return fields;
3852
+ };
3853
+ }
3854
+ function servedProblemOf(fieldsFor, grants, { service, grantsSource }) {
3855
+ const column = grantsColumnFor(service.model, grantsSource);
3856
+ let fields;
3857
+ try {
3858
+ fields = fieldsFor(column);
3859
+ } catch (error) {
3860
+ return error instanceof Error ? error.message : String(error);
3861
+ }
3862
+ if (grants && fields.grants.size === 0) {
3863
+ const named = column === void 0 ? `auth.serviceAccessSource names no column of model "${service.model ?? ""}"` : `the entity has no field "${column}", the column auth.serviceAccessSource names`;
3864
+ return `grants: true needs a grants field: the entity has no serviceAccess or service_access field, and ${named}`;
3865
+ }
3866
+ return void 0;
3867
+ }
3868
+ function serviceProblem4(service, contract, fields) {
3745
3869
  if (service.contract !== contract) {
3746
3870
  return `its admin kit handlers were made for another contract; pass ${service.name}'s own contract to admin.handlers`;
3747
3871
  }
3748
- return service.model === void 0 ? "the admin kit reads and writes the service's rows: declare its model" : void 0;
3872
+ if (service.model === void 0) {
3873
+ return "the admin kit reads and writes the service's rows: declare its model";
3874
+ }
3875
+ const relations2 = relationKeysOf(service);
3876
+ const relation = [...fields.named].find((name) => relations2.has(name));
3877
+ return relation === void 0 ? void 0 : `"${relation}" is a relation of ${service.name}'s model (a projection selects it with a nested select): the admin kit writes column values only; leave it out of editable and fieldOverrides`;
3749
3878
  }
3750
3879
  function handlers4(contract, options) {
3751
3880
  const kit = kitMethods4(contract);
@@ -3753,7 +3882,8 @@ function handlers4(contract, options) {
3753
3882
  const names = kit.map(([name]) => name);
3754
3883
  const access = checkAccess3(checked2.access, names);
3755
3884
  const rowless = rowlessMethods(checked2.rowless, names, fail4);
3756
- const fields = fieldsOf(contract, kit, checked2);
3885
+ const fieldsFor = fieldsByColumn(contract, kit, checked2);
3886
+ const fields = fieldsFor(void 0);
3757
3887
  const { onWrite, onCommitted } = checked2;
3758
3888
  if (onWrite !== void 0 && typeof onWrite !== "function") {
3759
3889
  fail4("onWrite must be a function of (write, ctx, db)");
@@ -3768,11 +3898,18 @@ function handlers4(contract, options) {
3768
3898
  const entries = {};
3769
3899
  for (const [name, spec] of kit) {
3770
3900
  const form = access[name] ?? ADMIN_DEFAULT_ACCESS;
3771
- const handler = handlerOf3({ spec, fields, form, ...hooks });
3772
- checkWhenDefined(handler, (service) => serviceProblem4(service, contract));
3901
+ const handler = handlerOf3({ spec, fieldsFor, form, ...hooks });
3902
+ checkWhenDefined(handler, (service) => serviceProblem4(service, contract, fields));
3903
+ checkWhenServed(
3904
+ handler,
3905
+ (served) => servedProblemOf(fieldsFor, checked2.grants === true, served)
3906
+ );
3773
3907
  if (checked2.grants === true) {
3774
3908
  markGrantsEditor(handler);
3775
3909
  }
3910
+ if (spec.method === "adminList" || spec.method === "adminGet") {
3911
+ dependsOnCaller(handler);
3912
+ }
3776
3913
  entries[name] = kitEntry(name, form, handler, rowless);
3777
3914
  }
3778
3915
  return Object.freeze(entries);
@@ -3816,7 +3953,8 @@ function reportErrors(client, logger, role) {
3816
3953
  }
3817
3954
  async function setupRedisAdapter(io, options = {}) {
3818
3955
  const logger = options.logger?.child({ service: "RedisAdapter" }) ?? consoleLogger.child({ service: "RedisAdapter" });
3819
- const { host = "localhost", port = 6379, password, db = 0, keyPrefix = "socket.io" } = options;
3956
+ const { host = "localhost", port = 6379, password, db = 0 } = options;
3957
+ const key = options.keyPrefix ?? deploymentOf(io);
3820
3958
  try {
3821
3959
  const peers = await loadRedisPeers();
3822
3960
  const createAdapter = peers.createAdapter;
@@ -3830,9 +3968,7 @@ async function setupRedisAdapter(io, options = {}) {
3830
3968
  reportErrors(pubClient, logger, "publish");
3831
3969
  reportErrors(subClient, logger, "subscribe");
3832
3970
  await Promise.all([pubClient.connect(), subClient.connect()]);
3833
- io.adapter(
3834
- createAdapter(pubClient, subClient, { key: keyPrefix })
3835
- );
3971
+ io.adapter(createAdapter(pubClient, subClient, { key }));
3836
3972
  watchAdapterClients(io, logger);
3837
3973
  logger.info(`Redis adapter connected to ${host}:${port}`);
3838
3974
  const cleanup = async () => {
@@ -3977,8 +4113,11 @@ export {
3977
4113
  DevWarningError,
3978
4114
  INDEX_MAX_ROWS,
3979
4115
  MAX_APP_ROOMS,
4116
+ MAX_ENTITY_IDS_PER_SOCKET,
3980
4117
  MAX_ITEM_IDS,
4118
+ MAX_SCOPES_PER_SOCKET,
3981
4119
  MAX_STREAMS_PER_SOCKET,
4120
+ MAX_TOPICS_PER_SOCKET,
3982
4121
  MIN_STALL_INTERVAL_MS,
3983
4122
  N_PLUS_ONE_STATEMENTS,
3984
4123
  ORDINAL_STEP,