@fitzzero/quickdraw-core 5.1.1 → 5.1.3

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 (152) hide show
  1. package/README.md +1308 -219
  2. package/dist/{admin-DKwkIh5i.d.ts → admin-CS7JB_UH.d.ts} +2 -2
  3. package/dist/{adminSchemas-2DUA_hZ8.d.ts → adminSchemas-29cGgrg5.d.ts} +6 -4
  4. package/dist/{auth-BhWqdyb-.d.ts → auth-DtdYjrZ9.d.ts} +51 -21
  5. package/dist/{caller-CXTFswBO.d.ts → caller-BTwGl7vm.d.ts} +296 -33
  6. package/dist/chunk-2FDSKLBO.js +82 -0
  7. package/dist/chunk-2FDSKLBO.js.map +1 -0
  8. package/dist/{chunk-RT6OILOI.js → chunk-4D2DOGAW.js} +2 -14
  9. package/dist/chunk-4D2DOGAW.js.map +1 -0
  10. package/dist/{chunk-OECZPRPW.js → chunk-64I6MYBC.js} +162 -775
  11. package/dist/chunk-64I6MYBC.js.map +1 -0
  12. package/dist/chunk-7473HK24.js +17 -0
  13. package/dist/chunk-7473HK24.js.map +1 -0
  14. package/dist/chunk-7Y5ROPXL.js +33 -0
  15. package/dist/chunk-7Y5ROPXL.js.map +1 -0
  16. package/dist/chunk-A2RBQD66.js +7 -0
  17. package/dist/chunk-A2RBQD66.js.map +1 -0
  18. package/dist/chunk-AK23XSWD.js +16 -0
  19. package/dist/chunk-AK23XSWD.js.map +1 -0
  20. package/dist/chunk-ASAMUGHG.js +651 -0
  21. package/dist/chunk-ASAMUGHG.js.map +1 -0
  22. package/dist/{chunk-B2P7AZ25.js → chunk-CYE4UBOB.js} +6 -4
  23. package/dist/chunk-CYE4UBOB.js.map +1 -0
  24. package/dist/chunk-DKE6QO26.js +136 -0
  25. package/dist/chunk-DKE6QO26.js.map +1 -0
  26. package/dist/chunk-DNP65TD5.js +832 -0
  27. package/dist/chunk-DNP65TD5.js.map +1 -0
  28. package/dist/{chunk-H23KEFBA.js → chunk-DUBU3BB3.js} +30 -1
  29. package/dist/chunk-DUBU3BB3.js.map +1 -0
  30. package/dist/chunk-GYLWEJHW.js +19 -0
  31. package/dist/chunk-GYLWEJHW.js.map +1 -0
  32. package/dist/{chunk-SHEGLZEP.js → chunk-I3JCFS7H.js} +34 -13
  33. package/dist/chunk-I3JCFS7H.js.map +1 -0
  34. package/dist/{chunk-X5CWPX7H.js → chunk-IIQ3WMJK.js} +13 -2
  35. package/dist/chunk-IIQ3WMJK.js.map +1 -0
  36. package/dist/{chunk-RZTP4IEW.js → chunk-JVTONQFI.js} +6339 -4226
  37. package/dist/chunk-JVTONQFI.js.map +1 -0
  38. package/dist/{chunk-QEKVYG5F.js → chunk-KKQANRLG.js} +2 -2
  39. package/dist/chunk-KKQANRLG.js.map +1 -0
  40. package/dist/{chunk-CDKG4TFA.js → chunk-MPQAT55F.js} +38 -205
  41. package/dist/chunk-MPQAT55F.js.map +1 -0
  42. package/dist/chunk-NO4M6PO5.js +101 -0
  43. package/dist/chunk-NO4M6PO5.js.map +1 -0
  44. package/dist/{chunk-TBMKJ26R.js → chunk-NW6U45HX.js} +3 -1
  45. package/dist/chunk-NW6U45HX.js.map +1 -0
  46. package/dist/chunk-NX2V6SIL.js +202 -0
  47. package/dist/chunk-NX2V6SIL.js.map +1 -0
  48. package/dist/{chunk-ZIVPHYRG.js → chunk-NZHA4TZD.js} +72 -175
  49. package/dist/chunk-NZHA4TZD.js.map +1 -0
  50. package/dist/{chunk-2PWMUVC7.js → chunk-OCZJ4EYV.js} +518 -27
  51. package/dist/chunk-OCZJ4EYV.js.map +1 -0
  52. package/dist/{chunk-REX5YPHX.js → chunk-PHCURHC7.js} +76 -21
  53. package/dist/chunk-PHCURHC7.js.map +1 -0
  54. package/dist/{chunk-4YO5H5ON.js → chunk-Q4OGQM5I.js} +4 -10
  55. package/dist/chunk-Q4OGQM5I.js.map +1 -0
  56. package/dist/chunk-QSU47XEX.js +13 -0
  57. package/dist/chunk-QSU47XEX.js.map +1 -0
  58. package/dist/{chunk-DD4SKCNO.js → chunk-RZXJTDH5.js} +13 -2
  59. package/dist/chunk-RZXJTDH5.js.map +1 -0
  60. package/dist/{chunk-Z2LZ7IZ7.js → chunk-SN3OZVEQ.js} +5 -31
  61. package/dist/chunk-SN3OZVEQ.js.map +1 -0
  62. package/dist/{chunk-4DIQHASK.js → chunk-SQ5VUG77.js} +28 -8
  63. package/dist/chunk-SQ5VUG77.js.map +1 -0
  64. package/dist/chunk-STH5DLQP.js +1192 -0
  65. package/dist/chunk-STH5DLQP.js.map +1 -0
  66. package/dist/{chunk-QCHKWIRS.js → chunk-VZFJTKIW.js} +13 -13
  67. package/dist/{chunk-QCHKWIRS.js.map → chunk-VZFJTKIW.js.map} +1 -1
  68. package/dist/{chunk-VSQBHPWH.js → chunk-X4ZU4QH5.js} +54 -22
  69. package/dist/chunk-X4ZU4QH5.js.map +1 -0
  70. package/dist/chunk-XD7SRDUY.js +188 -0
  71. package/dist/chunk-XD7SRDUY.js.map +1 -0
  72. package/dist/chunk-XH2IZVLT.js +62 -0
  73. package/dist/chunk-XH2IZVLT.js.map +1 -0
  74. package/dist/chunk-Z7I6SONO.js +825 -0
  75. package/dist/chunk-Z7I6SONO.js.map +1 -0
  76. package/dist/cli/quickdraw-docs.js +132 -112
  77. package/dist/cli/quickdraw-docs.js.map +1 -1
  78. package/dist/client/core.d.ts +387 -0
  79. package/dist/client/core.js +109 -0
  80. package/dist/client/index.d.ts +52 -203
  81. package/dist/client/index.js +59 -218
  82. package/dist/client/index.js.map +1 -1
  83. package/dist/{clientTypes-Bc-G8n-U.d.ts → clientTypes-CUUNJwWs.d.ts} +3 -3
  84. package/dist/{connection-Cr1BU07W.d.ts → connection-2EeQ_Et0.d.ts} +22 -11
  85. package/dist/{createServer-CP9eDHZZ.d.ts → createServer-Bx5HOcYK.d.ts} +67 -17
  86. package/dist/{createTestApp-UKVGEmOf.d.ts → createTestApp-DAMANT79.d.ts} +46 -13
  87. package/dist/{envelope-Cp3YcVxC.d.ts → envelope-BMBaxppV.d.ts} +216 -10
  88. package/dist/{http-Djj2Inuu.d.ts → http-YzLAar3w.d.ts} +76 -60
  89. package/dist/index.d.ts +8 -7
  90. package/dist/index.js +82 -25
  91. package/dist/index.js.map +1 -1
  92. package/dist/{infer-Teo1xpsd.d.ts → infer-Br62cnyW.d.ts} +82 -11
  93. package/dist/logger-Yz93KzdV.d.ts +30 -0
  94. package/dist/{metrics-BdBu5o8G.d.ts → metrics-CTVpw0gu.d.ts} +2 -2
  95. package/dist/origins-kvpxnVcW.d.ts +8 -0
  96. package/dist/parser.js +2 -1
  97. package/dist/prisma/index.d.ts +14 -7
  98. package/dist/prisma/index.js +279 -70
  99. package/dist/prisma/index.js.map +1 -1
  100. package/dist/{rateLimit-ZMIZJUZF.js → rateLimit-5RKBFLUR.js} +2 -2
  101. package/dist/rateLimit-5RKBFLUR.js.map +1 -0
  102. package/dist/remote-N7OUAGOW.js +15 -0
  103. package/dist/remote-N7OUAGOW.js.map +1 -0
  104. package/dist/server/auth/index.d.ts +82 -33
  105. package/dist/server/auth/index.js +277 -270
  106. package/dist/server/auth/index.js.map +1 -1
  107. package/dist/server/express/index.d.ts +9 -2
  108. package/dist/server/express/index.js +1 -1
  109. package/dist/server/index.d.ts +268 -84
  110. package/dist/server/index.js +664 -418
  111. package/dist/server/index.js.map +1 -1
  112. package/dist/server/mcp/index.d.ts +89 -27
  113. package/dist/server/mcp/index.js +281 -62
  114. package/dist/server/mcp/index.js.map +1 -1
  115. package/dist/server/otel.d.ts +3 -3
  116. package/dist/{sharing-DVa8W-yR.d.ts → sharing-BrsmlDv_.d.ts} +3 -3
  117. package/dist/testing/client.d.ts +14 -14
  118. package/dist/testing/client.js +19 -10
  119. package/dist/testing/client.js.map +1 -1
  120. package/dist/testing/index.d.ts +50 -13
  121. package/dist/testing/index.js +113 -21
  122. package/dist/testing/index.js.map +1 -1
  123. package/dist/testing/mock.d.ts +5 -5
  124. package/dist/testing/mock.js +17 -9
  125. package/dist/tracked-BR5b2O-U.d.ts +43 -0
  126. package/dist/{types-Cu0vmx7c.d.ts → types-CLRCQGnO.d.ts} +4 -3
  127. package/dist/types-tMagHbjy.d.ts +151 -0
  128. package/dist/utils/index.d.ts +16 -4
  129. package/dist/utils/index.js +6 -3
  130. package/package.json +5 -1
  131. package/dist/chunk-2PWMUVC7.js.map +0 -1
  132. package/dist/chunk-4DIQHASK.js.map +0 -1
  133. package/dist/chunk-4YO5H5ON.js.map +0 -1
  134. package/dist/chunk-B2P7AZ25.js.map +0 -1
  135. package/dist/chunk-CDKG4TFA.js.map +0 -1
  136. package/dist/chunk-DD4SKCNO.js.map +0 -1
  137. package/dist/chunk-H23KEFBA.js.map +0 -1
  138. package/dist/chunk-OECZPRPW.js.map +0 -1
  139. package/dist/chunk-QEKVYG5F.js.map +0 -1
  140. package/dist/chunk-REX5YPHX.js.map +0 -1
  141. package/dist/chunk-RT6OILOI.js.map +0 -1
  142. package/dist/chunk-RZTP4IEW.js.map +0 -1
  143. package/dist/chunk-SHEGLZEP.js.map +0 -1
  144. package/dist/chunk-TBMKJ26R.js.map +0 -1
  145. package/dist/chunk-VSQBHPWH.js.map +0 -1
  146. package/dist/chunk-X5CWPX7H.js.map +0 -1
  147. package/dist/chunk-YSWH776Y.js +0 -13
  148. package/dist/chunk-YSWH776Y.js.map +0 -1
  149. package/dist/chunk-Z2LZ7IZ7.js.map +0 -1
  150. package/dist/chunk-ZIVPHYRG.js.map +0 -1
  151. package/dist/logger-BY28acW-.d.ts +0 -16
  152. /package/dist/{rateLimit-ZMIZJUZF.js.map → client/core.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,21 @@ 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,
52
+ seedRoomHosts,
60
53
  selectWith,
61
- serviceGrant,
62
- strip,
63
54
  watchAdapterClients
64
- } from "../chunk-RZTP4IEW.js";
55
+ } from "../chunk-JVTONQFI.js";
65
56
  import {
66
57
  DevWarningError,
67
58
  N_PLUS_ONE_STATEMENTS,
@@ -69,273 +60,102 @@ import {
69
60
  currentRev,
70
61
  formatDevWarning,
71
62
  runBeforeAnyDispatcher
72
- } from "../chunk-4DIQHASK.js";
63
+ } from "../chunk-SQ5VUG77.js";
73
64
  import {
74
65
  crud,
75
66
  crudSpecOf,
76
67
  sharing,
77
68
  sharingSpecOf
78
- } from "../chunk-QCHKWIRS.js";
79
- import "../chunk-QEKVYG5F.js";
69
+ } from "../chunk-VZFJTKIW.js";
70
+ import "../chunk-KKQANRLG.js";
80
71
  import {
72
+ SUBSCRIBER_LEVELS,
81
73
  abortError,
82
74
  callerGrantsOf,
75
+ checkWhenDefined,
76
+ checkWhenServed,
77
+ checksRowsItself,
78
+ compileProjections,
83
79
  createCaller,
80
+ dependsOnCaller,
81
+ handlerProblem,
82
+ isCallerDependent,
84
83
  kitRuntimeOf,
84
+ levelsAtLeast,
85
+ markGrantsEditor,
86
+ maxLevel,
87
+ meetsLevel,
88
+ narrowKinds,
85
89
  outputSchemaOf,
86
- schemaOutputOf
87
- } from "../chunk-2PWMUVC7.js";
88
- import "../chunk-DD4SKCNO.js";
90
+ projectRow,
91
+ projectedOutput,
92
+ registerRuntime,
93
+ schemaOutputOf,
94
+ serviceGrant,
95
+ strip
96
+ } from "../chunk-OCZJ4EYV.js";
97
+ import "../chunk-RZXJTDH5.js";
89
98
  import {
90
99
  recordSocketSession,
91
100
  toCallReply,
92
101
  unreadable
93
- } from "../chunk-REX5YPHX.js";
102
+ } from "../chunk-PHCURHC7.js";
94
103
  import {
95
104
  ANY_FIELD,
96
105
  modelKey,
97
106
  storageOf
98
- } from "../chunk-H23KEFBA.js";
107
+ } from "../chunk-DUBU3BB3.js";
108
+ import {
109
+ search,
110
+ searchSpecOf
111
+ } from "../chunk-XD7SRDUY.js";
112
+ import "../chunk-4D2DOGAW.js";
113
+ import {
114
+ ACCESS_LEVELS,
115
+ isAccessLevel
116
+ } from "../chunk-AK23XSWD.js";
117
+ import {
118
+ consoleLogger,
119
+ errorText
120
+ } from "../chunk-DKE6QO26.js";
121
+ import "../chunk-7473HK24.js";
122
+ import "../chunk-2FDSKLBO.js";
99
123
  import {
100
124
  ADMIN_FIELD_TYPES,
101
125
  ADMIN_NEVER_WRITABLE,
126
+ MAX_CURSOR_LENGTH,
102
127
  admin,
103
128
  adminSpecOf,
104
- search,
105
- searchSpecOf
106
- } from "../chunk-CDKG4TFA.js";
129
+ writtenData
130
+ } from "../chunk-MPQAT55F.js";
131
+ import "../chunk-A2RBQD66.js";
107
132
  import {
108
- ACCESS_LEVELS,
133
+ MAX_ENTITY_IDS_PER_SOCKET,
134
+ MAX_SCOPES_PER_SOCKET,
135
+ MAX_TOPICS_PER_SOCKET,
109
136
  entityRoom,
110
- isAccessLevel,
111
- reservedRoomPrefix
112
- } from "../chunk-VSQBHPWH.js";
137
+ reservedRoomPrefix,
138
+ socketIdShaped
139
+ } from "../chunk-X4ZU4QH5.js";
113
140
  import {
114
141
  CHANNEL_DEFAULT_RATE,
115
- DEFAULT_COLLECTION_LIMIT,
116
- DEFAULT_COLLECTION_MAX_LIMIT,
142
+ isHostServed,
117
143
  isScopedStream
118
- } from "../chunk-RT6OILOI.js";
144
+ } from "../chunk-GYLWEJHW.js";
119
145
  import "../chunk-47AEXVDM.js";
120
- import "../chunk-Z2LZ7IZ7.js";
121
- import {
122
- consoleLogger
123
- } from "../chunk-YSWH776Y.js";
146
+ import "../chunk-SN3OZVEQ.js";
147
+ import "../chunk-7Y5ROPXL.js";
124
148
  import {
125
149
  QuickdrawError
126
- } from "../chunk-4YO5H5ON.js";
127
-
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
- }
150
+ } from "../chunk-Q4OGQM5I.js";
151
+ import "../chunk-QSU47XEX.js";
332
152
 
333
153
  // src/server/realtime/define.ts
334
- function isRecord2(value) {
154
+ function isRecord(value) {
335
155
  return typeof value === "object" && value !== null && !Array.isArray(value);
336
156
  }
337
157
  function compileSelector(selector) {
338
- const read = typeof selector === "function" ? selector : (payload) => isRecord2(payload) ? payload[selector] : void 0;
158
+ const read = typeof selector === "function" ? selector : (payload) => isRecord(payload) ? payload[selector] : void 0;
339
159
  return (payload) => {
340
160
  try {
341
161
  const value = read(payload);
@@ -352,7 +172,7 @@ function compileRoom(room) {
352
172
  const computed = compileSelector(room);
353
173
  return (payload) => {
354
174
  const name = computed(payload);
355
- return name === void 0 || reservedRoomPrefix(name) !== void 0 ? void 0 : name;
175
+ return name === void 0 || reservedRoomPrefix(name) !== void 0 || socketIdShaped(name) ? void 0 : name;
356
176
  };
357
177
  }
358
178
  function compileRequires(def) {
@@ -380,8 +200,8 @@ function checkChannelAccess(owner2, access, fail5) {
380
200
  if (access === void 0 || access === "authenticated") {
381
201
  return "authenticated";
382
202
  }
383
- const keys = isRecord2(access) ? Object.keys(access) : [];
384
- if (!isRecord2(access) || keys.length !== 1 || !isAccessLevel(access.service)) {
203
+ const keys = isRecord(access) ? Object.keys(access) : [];
204
+ if (!isRecord(access) || keys.length !== 1 || !isAccessLevel(access.service)) {
385
205
  fail5(`${owner2}: access must be "authenticated" or { service: level }`);
386
206
  }
387
207
  return Object.freeze({ service: access.service });
@@ -389,7 +209,7 @@ function checkChannelAccess(owner2, access, fail5) {
389
209
  function checkChannel(service, name, def, entry, kinds, fail5) {
390
210
  const owner2 = `channel "${name}"`;
391
211
  const implementation = typeof entry === "function" ? { handler: entry } : entry;
392
- if (!isRecord2(implementation) || typeof implementation.handler !== "function") {
212
+ if (!isRecord(implementation) || typeof implementation.handler !== "function") {
393
213
  fail5(`${owner2} must be a handler function or { access, handler }`);
394
214
  }
395
215
  const unknownKey = Object.keys(implementation).find(
@@ -408,16 +228,17 @@ function checkChannel(service, name, def, entry, kinds, fail5) {
408
228
  burst: def.burst ?? Math.max(1, ratePerSecond * 2),
409
229
  requires: compileRequires(def),
410
230
  access: checkChannelAccess(owner2, implementation.access, fail5),
231
+ accessDeclared: implementation.access !== void 0,
411
232
  kinds,
412
233
  handler: implementation.handler
413
234
  });
414
235
  }
415
236
  function compileChannels(contract, value, kinds, fail5) {
416
- const declared = Object.entries(contract.channels);
237
+ const declared = Object.entries(contract.channels).filter(([, def]) => !isHostServed(def));
417
238
  if (value === void 0 && declared.length === 0) {
418
239
  return /* @__PURE__ */ new Map();
419
240
  }
420
- if (!isRecord2(value)) {
241
+ if (!isRecord(value)) {
421
242
  fail5("channels must be an object with one implementation per contract channel");
422
243
  }
423
244
  const missing2 = declared.filter(([name]) => !Object.hasOwn(value, name)).map(([name]) => name);
@@ -431,6 +252,11 @@ function compileChannels(contract, value, kinds, fail5) {
431
252
  if (def === void 0) {
432
253
  fail5(`"${name}" is not a channel of the contract`);
433
254
  }
255
+ if (isHostServed(def)) {
256
+ fail5(
257
+ `channel "${name}" is served by the room's host (serve: "host"): its messages never reach the server, so it takes no handler`
258
+ );
259
+ }
434
260
  channels.set(name, checkChannel(contract.name, name, def, entry, admitted, fail5));
435
261
  }
436
262
  return channels;
@@ -474,7 +300,7 @@ function checkStreamOptions(name, def, entry, fail5) {
474
300
  if (entry === void 0) {
475
301
  return { computeSeed: void 0, validate: "always" };
476
302
  }
477
- if (!isRecord2(entry)) {
303
+ if (!isRecord(entry)) {
478
304
  fail5(`${owner2} must be an object: { seed?, validate? }`);
479
305
  }
480
306
  const unknownKey = Object.keys(entry).find((key) => !STREAM_OPTION_KEYS.includes(key));
@@ -499,6 +325,11 @@ function checkStreamOptions(name, def, entry, fail5) {
499
325
  return { computeSeed: seed, validate };
500
326
  }
501
327
  function checkStream(name, def, needs, options, fail5) {
328
+ if (isHostServed(def) && options !== void 0) {
329
+ fail5(
330
+ `streams.${name}: the stream is served by the room's host (serve: "host"), whose client pushes its items and keeps its seed: the server takes no options for it`
331
+ );
332
+ }
502
333
  const access = engineForm(def.access);
503
334
  if (typeof access === "object" && "entry" in access && !needs.hasPolicy) {
504
335
  fail5(
@@ -518,11 +349,12 @@ function checkStream(name, def, needs, options, fail5) {
518
349
  ...checkStreamOptions(name, def, options, fail5),
519
350
  volatile: def.volatile === true,
520
351
  access,
521
- room: roomAccess(def.access)
352
+ room: roomAccess(def.access),
353
+ host: isHostServed(def)
522
354
  });
523
355
  }
524
356
  function compileStreams(contract, needs, value, fail5) {
525
- if (value !== void 0 && !isRecord2(value)) {
357
+ if (value !== void 0 && !isRecord(value)) {
526
358
  fail5(
527
359
  "streams must be an object of options per contract stream: { <stream>: { seed?, validate? } }"
528
360
  );
@@ -541,14 +373,14 @@ function compileStreams(contract, needs, value, fail5) {
541
373
  }
542
374
 
543
375
  // src/server/serviceData.ts
544
- function isRecord3(value) {
376
+ function isRecord2(value) {
545
377
  return typeof value === "object" && value !== null && !Array.isArray(value);
546
378
  }
547
379
  function isName(value) {
548
380
  return typeof value === "string" && value.length > 0;
549
381
  }
550
- function isContract2(value) {
551
- return isRecord3(value) && isName(value.name) && Object.isFrozen(value);
382
+ function isContract(value) {
383
+ return isRecord2(value) && isName(value.name) && Object.isFrozen(value);
552
384
  }
553
385
  function idsIn(value) {
554
386
  if (isName(value)) {
@@ -558,7 +390,7 @@ function idsIn(value) {
558
390
  }
559
391
  function checkLink(index, entry, fail5) {
560
392
  const owner2 = `affects[${index}]`;
561
- if (!isRecord3(entry) || !isContract2(entry.service)) {
393
+ if (!isRecord2(entry) || !isContract(entry.service)) {
562
394
  fail5(`${owner2} must be { service: contract, id }`);
563
395
  }
564
396
  const { id, columns } = entry;
@@ -613,7 +445,7 @@ function checkWatchAccess(value, fail5) {
613
445
  if (value === "public" || value === "authenticated") {
614
446
  return value;
615
447
  }
616
- if (!isRecord3(value) || Object.keys(value).length !== 1 || !isAccessLevel(value.service)) {
448
+ if (!isRecord2(value) || Object.keys(value).length !== 1 || !isAccessLevel(value.service)) {
617
449
  fail5('watchAccess must be "public", "authenticated" or { service: level }');
618
450
  }
619
451
  return Object.freeze({ service: value.service });
@@ -635,6 +467,9 @@ function checkMethodKinds(owner2, entry, data, fail5) {
635
467
  (message) => fail5(`${owner2}: ${message}`)
636
468
  );
637
469
  }
470
+ function checkMethodClaims(owner2, entry, fail5) {
471
+ return claimsOption(owner2, entry, fail5);
472
+ }
638
473
  function checkServiceData(definition, appKinds, fail5) {
639
474
  const { model, access, versionColumn } = definition;
640
475
  if (model !== void 0 && !isName(model)) {
@@ -692,6 +527,7 @@ var DEFINITION_KEYS = /* @__PURE__ */ new Set([
692
527
  "channels",
693
528
  "streams",
694
529
  "onRoomLeave",
530
+ "onHostLeave",
695
531
  "adminBypass",
696
532
  "kinds"
697
533
  ]);
@@ -703,9 +539,10 @@ var METHOD_KEYS = /* @__PURE__ */ new Set([
703
539
  "timeoutMs",
704
540
  "version",
705
541
  "rowless",
706
- "kinds"
542
+ "kinds",
543
+ "claims"
707
544
  ]);
708
- function isRecord4(value) {
545
+ function isRecord3(value) {
709
546
  return typeof value === "object" && value !== null && !Array.isArray(value);
710
547
  }
711
548
  function checkKeys(value, allowed, owner2, fail5) {
@@ -720,7 +557,7 @@ function isDuration(value, max) {
720
557
  return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= max;
721
558
  }
722
559
  function checkContract(contract) {
723
- const valid = isRecord4(contract) && typeof contract.name === "string" && contract.name.length > 0 && isRecord4(contract.methods) && Object.isFrozen(contract);
560
+ const valid = isRecord3(contract) && typeof contract.name === "string" && contract.name.length > 0 && isRecord3(contract.methods) && Object.isFrozen(contract);
724
561
  if (!valid) {
725
562
  throw new TypeError("defineService: the first argument must be a contract from defineContract");
726
563
  }
@@ -731,11 +568,16 @@ function checkShare(owner2, entry, fail5) {
731
568
  if (share !== void 0 && share !== "caller" && share !== "all") {
732
569
  fail5(`${owner2}: share must be "caller" or "all"`);
733
570
  }
734
- if (share === "all" && isRecord4(entry.access) && entry.access.kind === "custom") {
571
+ if (share === "all" && isRecord3(entry.access) && entry.access.kind === "custom") {
735
572
  fail5(
736
573
  `${owner2} has custom access, so it cannot share: "all"; its result may depend on who asks`
737
574
  );
738
575
  }
576
+ if (share === "all" && typeof entry.handler === "function" && isCallerDependent(entry.handler)) {
577
+ fail5(
578
+ `${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`
579
+ );
580
+ }
739
581
  if (ttlMs !== void 0 && (share === void 0 || !isDuration(ttlMs, MAX_TIMEOUT_MS))) {
740
582
  fail5(`${owner2}: ttlMs needs share and must be a positive number of milliseconds`);
741
583
  }
@@ -756,7 +598,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
756
598
  if (def === void 0) {
757
599
  fail5(`"${name}" is not a method of the contract`);
758
600
  }
759
- if (!isRecord4(entry)) {
601
+ if (!isRecord3(entry)) {
760
602
  fail5(`${owner2} must be { access, handler }`);
761
603
  }
762
604
  checkKeys(entry, METHOD_KEYS, owner2, fail5);
@@ -777,6 +619,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
777
619
  }
778
620
  checkQueryOptions(owner2, entry, def.kind, fail5);
779
621
  const kinds = checkMethodKinds(owner2, entry, data, fail5);
622
+ const claims = checkMethodClaims(owner2, entry, fail5);
780
623
  const projection = projectedOutput(def.output, projections);
781
624
  return Object.freeze({
782
625
  name,
@@ -787,6 +630,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
787
630
  schemaOutput: projection === void 0 ? schemaOutputOf(def.output) : void 0,
788
631
  access: entry.access,
789
632
  kinds,
633
+ claims,
790
634
  handler: entry.handler,
791
635
  share: entry.share,
792
636
  ttlMs: entry.ttlMs,
@@ -796,7 +640,7 @@ function checkMethod(contract, projections, data, name, entry, fail5) {
796
640
  });
797
641
  }
798
642
  function checkMethods(contract, projections, data, value, fail5) {
799
- if (!isRecord4(value)) {
643
+ if (!isRecord3(value)) {
800
644
  fail5("methods must be an object with one implementation per contract method");
801
645
  }
802
646
  const missing2 = Object.keys(contract.methods).filter((name) => !Object.hasOwn(value, name));
@@ -818,7 +662,7 @@ function checkWatches(contract, collections, data, fail5) {
818
662
  if (def.kind !== "query") {
819
663
  fail5(`method "${name}" is a mutation; only a query can watch`);
820
664
  }
821
- if (watch === "service" || isRecord4(watch) && Object.hasOwn(watch, "service")) {
665
+ if (watch === "service" || isRecord3(watch) && Object.hasOwn(watch, "service")) {
822
666
  if (data.watchAccess === void 0) {
823
667
  fail5(
824
668
  `method "${name}" watches the service's topic, which is closed without watchAccess: declare who may watch it ("public", "authenticated" or { service: level })`
@@ -829,7 +673,7 @@ function checkWatches(contract, collections, data, fail5) {
829
673
  }
830
674
  continue;
831
675
  }
832
- if (!isRecord4(watch) || typeof watch.scope !== "function") {
676
+ if (!isRecord3(watch) || typeof watch.scope !== "function") {
833
677
  fail5(`method "${name}": watch needs a scope function, which finds the scope from the input`);
834
678
  }
835
679
  if (typeof watch.collection !== "string" || !collections.has(watch.collection)) {
@@ -857,12 +701,12 @@ function checkRowForms(methods, data, fail5) {
857
701
  }
858
702
  }
859
703
  }
860
- function checkRowless(methods, data, fail5) {
704
+ function checkRowless(contract, methods, data, fail5) {
861
705
  if (data.access === void 0) {
862
706
  return;
863
707
  }
864
708
  for (const method of Object.values(methods)) {
865
- const problem = rowlessProblem(method);
709
+ const problem = rowlessProblem(method, contract.name);
866
710
  if (problem !== void 0) {
867
711
  fail5(problem);
868
712
  }
@@ -898,17 +742,20 @@ function buildService(runtime, contract, definition) {
898
742
  const fail5 = (message) => {
899
743
  throw new TypeError(`defineService("${checked2.name}"): ${message}`);
900
744
  };
901
- if (!isRecord4(definition)) {
745
+ if (!isRecord3(definition)) {
902
746
  fail5("the definition must be an object");
903
747
  }
904
748
  checkKeys(definition, DEFINITION_KEYS, "the definition", fail5);
905
- const { adminBypass = true, onRoomLeave } = definition;
749
+ const { adminBypass = true, onRoomLeave, onHostLeave } = definition;
906
750
  if (typeof adminBypass !== "boolean") {
907
751
  fail5("adminBypass must be a boolean");
908
752
  }
909
753
  if (onRoomLeave !== void 0 && typeof onRoomLeave !== "function") {
910
754
  fail5("onRoomLeave must be a function of the leave and a run context");
911
755
  }
756
+ if (onHostLeave !== void 0 && typeof onHostLeave !== "function") {
757
+ fail5("onHostLeave must be a function of the leave and a run context");
758
+ }
912
759
  const data = checkServiceData(definition, runtime.kinds, fail5);
913
760
  const projections = compileProjections(checked2, definition.project, fail5);
914
761
  const collections = compileCollections(
@@ -916,12 +763,13 @@ function buildService(runtime, contract, definition) {
916
763
  projections,
917
764
  data.model,
918
765
  definition.collections,
919
- fail5
766
+ fail5,
767
+ data.access
920
768
  );
921
769
  checkWatches(checked2, collections, data, fail5);
922
770
  const methods = checkMethods(checked2, projections, data, definition.methods, fail5);
923
771
  checkRowForms(methods, data, fail5);
924
- checkRowless(methods, data, fail5);
772
+ checkRowless(checked2, methods, data, fail5);
925
773
  const service = Object.freeze({
926
774
  name: checked2.name,
927
775
  contract: checked2,
@@ -937,7 +785,8 @@ function buildService(runtime, contract, definition) {
937
785
  definition.streams,
938
786
  fail5
939
787
  ),
940
- onRoomLeave
788
+ onRoomLeave,
789
+ onHostLeave
941
790
  });
942
791
  checkHandlers(service, fail5);
943
792
  registerRuntime(service, runtime);
@@ -978,6 +827,10 @@ function streamOf(current, contract, name) {
978
827
  },
979
828
  pushMany(...args) {
980
829
  handle().pushMany(...args);
830
+ },
831
+ // Async, so every refusal (no dispatcher, a host-served stream, a bad scope) rejects.
832
+ async subscribers(...args) {
833
+ return await handle().subscribers(...args);
981
834
  }
982
835
  });
983
836
  }
@@ -1060,7 +913,7 @@ var COLUMNS = /* @__PURE__ */ new WeakMap();
1060
913
  function jsonAclColumnsOf(policy) {
1061
914
  return COLUMNS.get(policy);
1062
915
  }
1063
- function isRecord5(value) {
916
+ function isRecord4(value) {
1064
917
  return typeof value === "object" && value !== null && !Array.isArray(value);
1065
918
  }
1066
919
  function aclLevel(acl, userId) {
@@ -1069,7 +922,7 @@ function aclLevel(acl, userId) {
1069
922
  }
1070
923
  let level = null;
1071
924
  for (const entry of acl) {
1072
- if (isRecord5(entry) && entry.userId === userId) {
925
+ if (isRecord4(entry) && entry.userId === userId) {
1073
926
  level = maxLevel(level, entry.level);
1074
927
  }
1075
928
  }
@@ -1115,11 +968,11 @@ function jsonAcl(field, options = {}) {
1115
968
  }
1116
969
 
1117
970
  // src/server/access/policies/inherit.ts
1118
- function isContract3(value) {
971
+ function isContract2(value) {
1119
972
  return typeof value === "object" && value !== null && typeof value.name === "string" && Object.isFrozen(value);
1120
973
  }
1121
974
  function inherit(options) {
1122
- if (typeof options !== "object" || options === null || !isContract3(options.from)) {
975
+ if (typeof options !== "object" || options === null || !isContract2(options.from)) {
1123
976
  throw new TypeError("inherit({ from, via }): from must be the parent service's contract");
1124
977
  }
1125
978
  const { from } = options;
@@ -1202,6 +1055,10 @@ function rowLevel(form, fallback) {
1202
1055
  }
1203
1056
  return form.entry ?? form.scope ?? fallback;
1204
1057
  }
1058
+ function bulkFallback(counterpart) {
1059
+ const level = counterpart === void 0 ? "Moderate" : rowLevel(counterpart, "Moderate");
1060
+ return meetsLevel(level, "Moderate") ? level : "Moderate";
1061
+ }
1205
1062
  function unchecked(call, form, use) {
1206
1063
  return call.runtime.service.access === void 0 || use === "read" && form === "public";
1207
1064
  }
@@ -1228,8 +1085,8 @@ function grantRowLevel(call, form) {
1228
1085
  function grantReaches(call, form, level) {
1229
1086
  return meetsLevel(grantRowLevel(call, form), level);
1230
1087
  }
1231
- async function rowsWhere(call, form, level) {
1232
- if (unchecked(call, form, "read") || grantReaches(call, form, level)) {
1088
+ async function rowsWhere(call, form, level, use = "read") {
1089
+ if (unchecked(call, form, use) || grantReaches(call, form, level)) {
1233
1090
  return void 0;
1234
1091
  }
1235
1092
  if (call.principal === null) {
@@ -1291,29 +1148,85 @@ function guardedColumns(service) {
1291
1148
  add(collection.scope.column, collection.anchor.name);
1292
1149
  }
1293
1150
  }
1294
- const moving = new Set([...anchors.values()].map(({ column }) => column));
1295
- const locked = (reads?.columns ?? []).filter((column) => column !== "id" && !moving.has(column));
1151
+ const parents = new Set((reads?.parents ?? []).map(({ via }) => via));
1152
+ const locked = (reads?.columns ?? []).filter((column) => column !== "id" && !parents.has(column));
1296
1153
  return { locked: new Set(locked), anchors: [...anchors.values()] };
1297
1154
  }
1155
+ var MEMBERSHIPS = /* @__PURE__ */ new WeakMap();
1156
+ function membershipColumnsOf(services, model) {
1157
+ let byModel = MEMBERSHIPS.get(services);
1158
+ if (byModel === void 0) {
1159
+ byModel = /* @__PURE__ */ new Map();
1160
+ MEMBERSHIPS.set(services, byModel);
1161
+ }
1162
+ const key = modelKey(model);
1163
+ const kept = byModel.get(key);
1164
+ if (kept !== void 0) {
1165
+ return kept;
1166
+ }
1167
+ const columns = /* @__PURE__ */ new Map();
1168
+ for (const service of services.values()) {
1169
+ for (const read of service.access?.reads.memberships ?? []) {
1170
+ if (modelKey(read.model) !== key) {
1171
+ continue;
1172
+ }
1173
+ for (const column of [read.entry, read.user, read.level]) {
1174
+ if (!columns.has(column)) {
1175
+ columns.set(column, service.name);
1176
+ }
1177
+ }
1178
+ }
1179
+ }
1180
+ byModel.set(key, columns);
1181
+ return columns;
1182
+ }
1298
1183
  function forbidden(message) {
1299
1184
  return new QuickdrawError("FORBIDDEN", message);
1300
1185
  }
1301
1186
  function bypasses(service, principal) {
1302
1187
  return principal !== null && service.adminBypass && serviceGrant(principal, service.name) === "Admin";
1303
1188
  }
1304
- async function checkWritableColumns(runtime, principal, data, level) {
1305
- const { service, access } = runtime;
1189
+ function namedColumns(data) {
1190
+ return new Set(Object.keys(data).filter((column) => data[column] !== void 0));
1191
+ }
1192
+ function membershipNamed(runtime, columns) {
1193
+ const { service, services } = runtime;
1194
+ if (service.model === void 0) {
1195
+ return void 0;
1196
+ }
1197
+ const memberships = membershipColumnsOf(services, service.model);
1198
+ const column = columns.find((named) => memberships.has(named));
1199
+ return column === void 0 ? void 0 : [column, memberships.get(column) ?? service.name];
1200
+ }
1201
+ function checkLockedColumns(runtime, principal, data, options = {}) {
1202
+ const { service } = runtime;
1306
1203
  if (bypasses(service, principal)) {
1307
1204
  return;
1308
1205
  }
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));
1206
+ const { locked } = guardedColumns(service);
1207
+ const named = [...namedColumns(data)];
1208
+ const readOnly = named.find((column) => locked.has(column));
1209
+ const creating = options.creating === true;
1312
1210
  if (readOnly !== void 0) {
1313
1211
  throw forbidden(
1314
- `"${readOnly}" decides who may reach rows of ${service.name}: only a service-wide Admin may change it`
1212
+ 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`
1315
1213
  );
1316
1214
  }
1215
+ const membership = membershipNamed(runtime, named);
1216
+ if (membership !== void 0) {
1217
+ const [column, reader] = membership;
1218
+ throw forbidden(
1219
+ 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`
1220
+ );
1221
+ }
1222
+ }
1223
+ async function checkAnchorColumns(runtime, principal, data, level, options = {}) {
1224
+ const { service, access } = runtime;
1225
+ if (bypasses(service, principal)) {
1226
+ return;
1227
+ }
1228
+ const { anchors } = guardedColumns(service);
1229
+ const named = namedColumns(data);
1317
1230
  for (const { column, anchor } of anchors) {
1318
1231
  const target = data[column];
1319
1232
  if (!named.has(column) || target === null) {
@@ -1323,11 +1236,75 @@ async function checkWritableColumns(runtime, principal, data, level) {
1323
1236
  const resolved = valid ? await access.resolve(anchor, principal, [target], { grants: false }) : void 0;
1324
1237
  if (resolved === void 0 || !meetsLevel(resolved.levels.get(target), level)) {
1325
1238
  throw forbidden(
1326
- `Moving a row of ${service.name} by "${column}" needs ${level} on the ${anchor} row it moves into`
1239
+ 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
1240
  );
1328
1241
  }
1329
1242
  }
1330
1243
  }
1244
+ var relations = /* @__PURE__ */ new WeakMap();
1245
+ function relationKeysOf(service) {
1246
+ let keys = relations.get(service);
1247
+ if (keys === void 0) {
1248
+ const found = /* @__PURE__ */ new Set();
1249
+ for (const projection of service.projections.values()) {
1250
+ for (const [key, value] of Object.entries(projection.select)) {
1251
+ if (typeof value === "object" && value !== null) {
1252
+ found.add(key);
1253
+ }
1254
+ }
1255
+ }
1256
+ keys = found;
1257
+ relations.set(service, keys);
1258
+ }
1259
+ return keys;
1260
+ }
1261
+ function isPlainObject(value) {
1262
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1263
+ return false;
1264
+ }
1265
+ const prototype = Object.getPrototypeOf(value);
1266
+ return prototype === Object.prototype || prototype === null;
1267
+ }
1268
+ function columnValueIssues(service, data, path = []) {
1269
+ const relationKeys = relationKeysOf(service);
1270
+ const nested = new Map(
1271
+ findNestedWrites({ ...data }).map(({ field, operation }) => [field, operation])
1272
+ );
1273
+ const issues = [];
1274
+ for (const [key, value] of Object.entries(data)) {
1275
+ if (value === void 0) {
1276
+ continue;
1277
+ }
1278
+ const operation = nested.get(key) ?? (isPlainObject(value) && Array.isArray(value.set) ? "set" : void 0);
1279
+ if (relationKeys.has(key)) {
1280
+ issues.push({
1281
+ path: [...path, key],
1282
+ message: `"${key}" is a relation of ${service.name}'s model: kits write column values only; set its foreign key column instead`
1283
+ });
1284
+ } else if (operation !== void 0) {
1285
+ issues.push({
1286
+ path: [...path, key],
1287
+ message: `"${key}" holds a nested write ({ ${operation} }), which is not tracked: kits write column values only`
1288
+ });
1289
+ }
1290
+ }
1291
+ return issues;
1292
+ }
1293
+ function checkColumnValues(service, data, path = []) {
1294
+ const issues = columnValueIssues(service, data, path);
1295
+ if (issues.length > 0) {
1296
+ throw new QuickdrawError(
1297
+ "VALIDATION",
1298
+ "The data holds a relation or a nested write, which kits do not write",
1299
+ { issues }
1300
+ );
1301
+ }
1302
+ }
1303
+ async function checkWritableColumns(runtime, principal, data, level, path = []) {
1304
+ checkColumnValues(runtime.service, data, path);
1305
+ checkLockedColumns(runtime, principal, data);
1306
+ await checkAnchorColumns(runtime, principal, data, level);
1307
+ }
1331
1308
 
1332
1309
  // src/server/kits/transactions.ts
1333
1310
  function isWriteConflict(error) {
@@ -1447,13 +1424,13 @@ function updateHandler(form) {
1447
1424
  }
1448
1425
 
1449
1426
  // src/server/kits/crud/bulk.ts
1450
- async function writeAllowed(call, db, form, ids, write) {
1427
+ async function writeAllowed(call, db, [form, level], ids, write) {
1451
1428
  const unique = uniqueIds(ids);
1452
1429
  if (unique.length === 0) {
1453
1430
  return { count: 0 };
1454
1431
  }
1455
1432
  return await inTransaction(db, async (tx) => {
1456
- const allowed = await allowedIds(call, form, unique, rowLevel(form, "Moderate"), "write");
1433
+ const allowed = await allowedIds(call, form, unique, level, "write");
1457
1434
  if (allowed.length === 0) {
1458
1435
  return { count: 0 };
1459
1436
  }
@@ -1461,7 +1438,8 @@ async function writeAllowed(call, db, form, ids, write) {
1461
1438
  return { count };
1462
1439
  });
1463
1440
  }
1464
- function bulkUpdateHandler(form) {
1441
+ function bulkUpdateHandler(form, fallback) {
1442
+ const level = rowLevel(form, fallback);
1465
1443
  const handler = async ({ input, ctx, db }) => {
1466
1444
  const call = crudCall(ctx, db);
1467
1445
  const { ids, data } = input;
@@ -1469,24 +1447,25 @@ function bulkUpdateHandler(form) {
1469
1447
  if (Object.keys(patch).length === 0) {
1470
1448
  return { count: 0 };
1471
1449
  }
1472
- await checkWritableColumns(call.runtime, call.principal, patch, rowLevel(form, "Moderate"));
1450
+ await checkWritableColumns(call.runtime, call.principal, patch, level, ["data"]);
1473
1451
  return await writeAllowed(
1474
1452
  call,
1475
1453
  db,
1476
- form,
1454
+ [form, level],
1477
1455
  ids,
1478
1456
  (table, allowed) => table.updateMany({ where: { id: { in: [...allowed] } }, data: patch })
1479
1457
  );
1480
1458
  };
1481
1459
  return handler;
1482
1460
  }
1483
- function bulkDeleteHandler(form) {
1461
+ function bulkDeleteHandler(form, fallback) {
1462
+ const level = rowLevel(form, fallback);
1484
1463
  const handler = async ({ input, ctx, db }) => {
1485
1464
  const call = crudCall(ctx, db);
1486
1465
  return await writeAllowed(
1487
1466
  call,
1488
1467
  db,
1489
- form,
1468
+ [form, level],
1490
1469
  input.ids,
1491
1470
  (table, allowed) => table.deleteMany({ where: { id: { in: [...allowed] } } })
1492
1471
  );
@@ -1495,16 +1474,40 @@ function bulkDeleteHandler(form) {
1495
1474
  }
1496
1475
 
1497
1476
  // src/server/kits/crud/create.ts
1498
- function createHandler(prepare) {
1477
+ function isRecord5(value) {
1478
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1479
+ }
1480
+ function isPlainObject2(value) {
1481
+ if (!isRecord5(value)) {
1482
+ return false;
1483
+ }
1484
+ const prototype = Object.getPrototypeOf(value);
1485
+ return prototype === Object.prototype || prototype === null;
1486
+ }
1487
+ function createHandler(form, prepare) {
1499
1488
  const handler = async ({ input, ctx, db }) => {
1500
1489
  const call = crudCall(ctx, db);
1490
+ const { service } = call.runtime;
1491
+ const sent = isRecord5(input) ? input : {};
1492
+ checkColumnValues(service, sent);
1493
+ checkLockedColumns(call.runtime, call.principal, sent, { creating: true });
1501
1494
  const data = prepare === void 0 ? input : await checked(() => prepare(input, ctx, db));
1502
- if (typeof data !== "object" || data === null || Array.isArray(data)) {
1495
+ if (!isPlainObject2(data)) {
1503
1496
  throw new QuickdrawError(
1504
1497
  "INTERNAL",
1505
- `The read/write kit's prepare for ${call.runtime.service.name} must return the new row's data`
1498
+ `The read/write kit's prepare for ${service.name} must return the new row's data`
1506
1499
  );
1507
1500
  }
1501
+ const issues = columnValueIssues(service, data);
1502
+ if (issues.length > 0) {
1503
+ throw new QuickdrawError(
1504
+ "INTERNAL",
1505
+ `The read/write kit's prepare for ${service.name} must return column values: ${issues.map((issue) => issue.message).join("; ")}`
1506
+ );
1507
+ }
1508
+ await checkAnchorColumns(call.runtime, call.principal, data, rowLevel(form, "Moderate"), {
1509
+ creating: true
1510
+ });
1508
1511
  const { select } = projectionOf(call, "entity");
1509
1512
  return await call.table.create({ data, select });
1510
1513
  };
@@ -1602,20 +1605,33 @@ function signatureOf(order) {
1602
1605
  function signed(order) {
1603
1606
  return [[SIGNATURE, "asc"], ...order];
1604
1607
  }
1608
+ var ROW = "row ";
1609
+ var ROW_CURSOR = [
1610
+ [SIGNATURE, "asc"],
1611
+ ["id", "asc"]
1612
+ ];
1605
1613
  function cursorAfter(order, row) {
1606
- return encodeCursor(signed(order), { ...row, [SIGNATURE]: signatureOf(order) });
1614
+ const cursor = encodeCursor(signed(order), { ...row, [SIGNATURE]: signatureOf(order) });
1615
+ return cursor.length <= MAX_CURSOR_LENGTH ? cursor : encodeCursor(ROW_CURSOR, { [SIGNATURE]: `${ROW}${signatureOf(order)}`, id: row.id });
1607
1616
  }
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"]);
1617
+ function readCursor(order, cursor) {
1618
+ const signature = signatureOf(order);
1619
+ const decoded = (shape) => {
1620
+ try {
1621
+ return decodeCursor(shape, cursor);
1622
+ } catch {
1623
+ return void 0;
1624
+ }
1625
+ };
1626
+ const values = decoded(signed(order));
1627
+ if (values?.[0] === signature) {
1628
+ return { values: values.slice(1) };
1614
1629
  }
1615
- if (values[0] !== signatureOf(order)) {
1616
- throw unreadable(NOT_A_CURSOR, ["cursor"]);
1630
+ const named = decoded(ROW_CURSOR);
1631
+ if (named?.[0] === `${ROW}${signature}` && typeof named[1] === "string") {
1632
+ return { row: named[1] };
1617
1633
  }
1618
- return values.slice(1);
1634
+ throw unreadable(NOT_A_CURSOR, ["cursor"]);
1619
1635
  }
1620
1636
  async function nullableColumns(read, values) {
1621
1637
  const { storage, model, order } = read;
@@ -1652,8 +1668,27 @@ function refusal(error, read) {
1652
1668
  refused.cause = error;
1653
1669
  return refused;
1654
1670
  }
1671
+ async function startValues(read, cursor) {
1672
+ const held = readCursor(read.order, cursor);
1673
+ if ("values" in held) {
1674
+ return held.values;
1675
+ }
1676
+ let row;
1677
+ try {
1678
+ row = await read.table.findFirst({
1679
+ where: { AND: [read.where, { id: held.row }] },
1680
+ select: Object.fromEntries(read.order.map(([column]) => [column, true]))
1681
+ });
1682
+ } catch (error) {
1683
+ throw refusal(error, read);
1684
+ }
1685
+ if (row === null) {
1686
+ throw unreadable(NOT_A_CURSOR, ["cursor"]);
1687
+ }
1688
+ return read.order.map(([column]) => row[column]);
1689
+ }
1655
1690
  async function readPage(read) {
1656
- const values = read.cursor === void 0 ? void 0 : cursorValues(read.order, read.cursor);
1691
+ const values = read.cursor === void 0 ? void 0 : await startValues(read, read.cursor);
1657
1692
  const nullable = await nullableColumns(read, values);
1658
1693
  const where = values === void 0 ? read.where : { AND: [read.where, afterCursor(read.order, values, nullable)] };
1659
1694
  let rows;
@@ -1762,14 +1797,17 @@ async function nextOrdinal(db, model, where = {}, options = {}) {
1762
1797
  function invalid(message, path) {
1763
1798
  return new QuickdrawError("VALIDATION", message, { issues: [{ path: [...path], message }] });
1764
1799
  }
1765
- async function readNamed(table, spec, move, model) {
1800
+ async function readNamed(table, [spec, writable], move, model) {
1766
1801
  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 });
1802
+ const ids = {
1803
+ id: { in: [move.id, move.beforeId, move.afterId].filter((id) => id !== void 0) }
1804
+ };
1805
+ const where = writable === void 0 ? ids : { AND: [ids, writable] };
1806
+ const rows = writable === "none" ? [] : await table.findMany({ where, select });
1769
1807
  const byId = new Map(rows.map((row) => [row.id, row]));
1770
1808
  const moved = byId.get(move.id);
1771
1809
  if (moved === void 0) {
1772
- throw new QuickdrawError("NOT_FOUND", `No such ${model}`);
1810
+ throw writable === void 0 ? new QuickdrawError("NOT_FOUND", `No such ${model}`) : new QuickdrawError("FORBIDDEN", "Insufficient permissions");
1773
1811
  }
1774
1812
  const neighbor = (id, key) => {
1775
1813
  const row = id === void 0 ? void 0 : byId.get(id);
@@ -1785,9 +1823,10 @@ async function readNamed(table, spec, move, model) {
1785
1823
  after: neighbor(move.afterId, "afterId")
1786
1824
  };
1787
1825
  }
1788
- function listOf(spec, moved) {
1826
+ function listOf(spec, moved, writable) {
1789
1827
  const scope = Object.fromEntries(spec.within.map((column) => [column, moved[column] ?? null]));
1790
- return { AND: [scope, { id: { not: moved.id } }] };
1828
+ const mine = writable === void 0 ? [] : [writable === "none" ? { id: { in: [] } } : writable];
1829
+ return { AND: [scope, { id: { not: moved.id } }, ...mine] };
1791
1830
  }
1792
1831
  async function adjacent(table, spec, list, row, side) {
1793
1832
  const { column } = spec;
@@ -1805,10 +1844,10 @@ async function adjacent(table, spec, list, row, side) {
1805
1844
  select: { id: true, [column]: true }
1806
1845
  });
1807
1846
  }
1808
- async function bounds(table, spec, named) {
1847
+ async function bounds(table, spec, named, writable) {
1809
1848
  const { column } = spec;
1810
1849
  const { before, after } = named;
1811
- const list = listOf(spec, named.moved);
1850
+ const list = listOf(spec, named.moved, writable);
1812
1851
  const low = before ?? (after === void 0 ? null : await adjacent(table, spec, list, after, "previous"));
1813
1852
  const high = after ?? (before === void 0 ? null : await adjacent(table, spec, list, before, "next"));
1814
1853
  const lowValue = low === null ? null : low[column];
@@ -1822,14 +1861,21 @@ async function bounds(table, spec, named) {
1822
1861
  }
1823
1862
  return [lowValue, highValue];
1824
1863
  }
1825
- async function renumber(table, spec, named, move) {
1864
+ async function renumber(table, [spec, writable], named, move) {
1826
1865
  const { column } = spec;
1827
1866
  const rows = await table.findMany({
1828
- where: listOf(spec, named.moved),
1867
+ where: listOf(spec, named.moved, writable),
1829
1868
  select: { id: true, [column]: true },
1830
1869
  orderBy: [{ [column]: "asc" }, { id: "asc" }]
1831
1870
  });
1832
1871
  const order = rows.map((row) => row.id);
1872
+ const neighbor = move.beforeId ?? move.afterId;
1873
+ if (neighbor !== void 0 && !order.includes(neighbor)) {
1874
+ throw new QuickdrawError(
1875
+ "INTERNAL",
1876
+ "A reorder's neighbor is missing from the list it renumbers"
1877
+ );
1878
+ }
1833
1879
  const at = move.beforeId === void 0 ? Math.max(order.indexOf(move.afterId), 0) : order.indexOf(move.beforeId) + 1;
1834
1880
  order.splice(at, 0, move.id);
1835
1881
  const current = new Map(rows.map((row) => [row.id, row[column]]));
@@ -1848,29 +1894,48 @@ var RENUMBER_BASE_MS = 5e3;
1848
1894
  var RENUMBER_MS_PER_ROW = 10;
1849
1895
  var OWNER = "The read/write kit's reorder";
1850
1896
  var CONFLICT = "Another change to this list ran at the same time; try again";
1851
- async function moveIn(tx, { call, spec, move, select }, mayRenumber) {
1897
+ async function moveIn(tx, { call, spec, move, writable, select }, mayRenumber) {
1852
1898
  const table = delegateOf(tx, call.model);
1853
- const named = await readNamed(table, spec, move, call.model);
1854
- const range = await bounds(table, spec, named);
1899
+ const named = await readNamed(table, [spec, writable], move, call.model);
1900
+ const range = await bounds(table, spec, named, writable);
1855
1901
  const between = range === void 0 ? void 0 : ordinalBetween(range[0] ?? void 0, range[1] ?? void 0);
1856
1902
  if (between === void 0 && !mayRenumber) {
1857
- return { list: listOf(spec, named.moved) };
1903
+ return { list: listOf(spec, named.moved, writable) };
1858
1904
  }
1859
- const value = between ?? await renumber(table, spec, named, move);
1905
+ const value = between ?? await renumber(table, [spec, writable], named, move);
1860
1906
  return {
1861
1907
  row: await table.update({ where: { id: move.id }, data: { [spec.column]: value }, select })
1862
1908
  };
1863
1909
  }
1910
+ async function checkNamed(call, [form, level], move) {
1911
+ const named = [move.beforeId, move.afterId].filter((id) => id !== void 0);
1912
+ const ids = uniqueIds([move.id, ...named]);
1913
+ const writable = new Set(await allowedIds(call, form, ids, level, "write"));
1914
+ if (!writable.has(move.id)) {
1915
+ throw new QuickdrawError("FORBIDDEN", "Insufficient permissions");
1916
+ }
1917
+ for (const [id, key] of [
1918
+ [move.beforeId, "beforeId"],
1919
+ [move.afterId, "afterId"]
1920
+ ]) {
1921
+ if (id !== void 0 && !writable.has(id)) {
1922
+ throw new QuickdrawError("NOT_FOUND", `No such ${call.model} in this list as ${key}`);
1923
+ }
1924
+ }
1925
+ }
1864
1926
  function reorderHandler(spec, form) {
1927
+ const level = rowLevel(form, "Moderate");
1865
1928
  const handler = async ({ input, ctx, db }) => {
1866
1929
  const call = crudCall(ctx, db);
1930
+ const asked = input;
1931
+ await checkNamed(call, [form, level], asked);
1867
1932
  const move = {
1868
1933
  call,
1869
1934
  spec,
1870
- move: input,
1935
+ move: asked,
1936
+ writable: await rowsWhere(call, form, level, "write"),
1871
1937
  select: projectionOf(call, "entity").select
1872
1938
  };
1873
- await checkRowWrite(call, form, move.move.id);
1874
1939
  const how = { owner: OWNER, conflict: CONFLICT };
1875
1940
  const first = await inKitTransaction(db, async (tx) => await moveIn(tx, move, false), how);
1876
1941
  if ("row" in first) {
@@ -1901,7 +1966,7 @@ function handlerOf(context) {
1901
1966
  case "list":
1902
1967
  return listHandler({ spec, form, projection: context.projection });
1903
1968
  case "create":
1904
- return createHandler(context.prepare);
1969
+ return createHandler(form, context.prepare);
1905
1970
  case "update":
1906
1971
  return updateHandler(form);
1907
1972
  case "delete":
@@ -1909,9 +1974,9 @@ function handlerOf(context) {
1909
1974
  case "reorder":
1910
1975
  return reorderHandler(spec, form);
1911
1976
  case "bulkUpdate":
1912
- return bulkUpdateHandler(form);
1977
+ return bulkUpdateHandler(form, context.bulkLevel);
1913
1978
  default:
1914
- return bulkDeleteHandler(form);
1979
+ return bulkDeleteHandler(form, context.bulkLevel);
1915
1980
  }
1916
1981
  }
1917
1982
 
@@ -1989,7 +2054,14 @@ var CHECKS_ROWS = /* @__PURE__ */ new Set([
1989
2054
  "delete",
1990
2055
  "reorder"
1991
2056
  ]);
1992
- var OPTION_KEYS2 = ["access", "prepare", "rowless"];
2057
+ var CALLER_DEPENDENT = /* @__PURE__ */ new Set(["list", "getMany"]);
2058
+ var COUNTERPARTS = { bulkDelete: "delete", bulkUpdate: "update" };
2059
+ function bulkLevelOf(kit, access, spec) {
2060
+ const counterpart = spec.method === "bulkDelete" || spec.method === "bulkUpdate" ? COUNTERPARTS[spec.method] : void 0;
2061
+ const found = kit.find(([, other]) => other.method === counterpart);
2062
+ return bulkFallback(found === void 0 ? void 0 : access[found[0]]);
2063
+ }
2064
+ var OPTION_KEYS = ["access", "prepare", "rowless"];
1993
2065
  function serviceProblem(service, contract, [name, spec], form) {
1994
2066
  if (service.contract !== contract) {
1995
2067
  return `its read/write kit handlers were made for another contract; pass ${service.name}'s own contract to crud.handlers`;
@@ -2004,9 +2076,9 @@ function handlers(contract, options) {
2004
2076
  if (!isRecord6(options)) {
2005
2077
  fail("options must be { access, prepare?, rowless? }");
2006
2078
  }
2007
- const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS2.includes(key));
2079
+ const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS.includes(key));
2008
2080
  if (unknownKey !== void 0) {
2009
- fail(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS2.join(", ")}`);
2081
+ fail(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS.join(", ")}`);
2010
2082
  }
2011
2083
  const names = kit.map(([name]) => name);
2012
2084
  const access = checkAccess(options.access, names);
@@ -2016,11 +2088,15 @@ function handlers(contract, options) {
2016
2088
  for (const [name, spec] of kit) {
2017
2089
  const form = access[name];
2018
2090
  const projection = spec.method === "list" ? itemProjection(contract, spec) : "entity";
2019
- const handler = handlerOf({ spec, form, projection, prepare });
2091
+ const bulkLevel = bulkLevelOf(kit, access, spec);
2092
+ const handler = handlerOf({ spec, form, projection, prepare, bulkLevel });
2020
2093
  checkWhenDefined(handler, (service) => serviceProblem(service, contract, [name, spec], form));
2021
2094
  if (CHECKS_ROWS.has(spec.method)) {
2022
2095
  checksRowsItself(handler);
2023
2096
  }
2097
+ if (CALLER_DEPENDENT.has(spec.method)) {
2098
+ dependsOnCaller(handler);
2099
+ }
2024
2100
  entries[name] = kitEntry(name, form, handler, rowless);
2025
2101
  }
2026
2102
  return Object.freeze(entries);
@@ -2087,7 +2163,8 @@ async function authorizeScope(run, collection, scope) {
2087
2163
  const rule = {
2088
2164
  service: runtime.service,
2089
2165
  anchor: collection.anchor?.name,
2090
- access: collection.access
2166
+ access: collection.access,
2167
+ requires: collection.requires
2091
2168
  };
2092
2169
  const allowed = await allowedScopes(runtime.access, rule, principal, [scope]);
2093
2170
  if (!allowed.has(scope)) {
@@ -2106,9 +2183,7 @@ async function scopeWhere(run) {
2106
2183
  return await membersWhere(reader, collection, run.query.scope, collection.maxLimit) ?? "none";
2107
2184
  }
2108
2185
  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;
2186
+ return scopedCollection(run)?.order ?? ID_ORDER;
2112
2187
  }
2113
2188
  async function strategyIds(run, ids) {
2114
2189
  const found = await checked(() => ids(run.query.q, run.ctx, { limit: run.query.limit }));
@@ -2272,14 +2347,14 @@ function checkStrategy(strategy) {
2272
2347
  }
2273
2348
  return strategy;
2274
2349
  }
2275
- var OPTION_KEYS3 = ["access", "strategy", "method"];
2350
+ var OPTION_KEYS2 = ["access", "strategy", "method"];
2276
2351
  function checkOptions(options, kit) {
2277
2352
  if (!isRecord8(options)) {
2278
2353
  fail2("options must be { access, strategy?, method? }");
2279
2354
  }
2280
- const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS3.includes(key));
2355
+ const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS2.includes(key));
2281
2356
  if (unknownKey !== void 0) {
2282
- fail2(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS3.join(", ")}`);
2357
+ fail2(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS2.join(", ")}`);
2283
2358
  }
2284
2359
  const problem = accessFormProblem(options.access);
2285
2360
  if (problem !== void 0) {
@@ -2344,6 +2419,7 @@ function handlers2(contract, options) {
2344
2419
  checkScope(contract, name, spec, projection);
2345
2420
  const handler = searchHandler({ spec, form, projection, strategy });
2346
2421
  checkWhenDefined(handler, (service) => serviceProblem2(service, contract, name, form));
2422
+ dependsOnCaller(handler);
2347
2423
  entries[name] = Object.freeze({ access: form, handler, share: "caller" });
2348
2424
  }
2349
2425
  return Object.freeze(entries);
@@ -2618,6 +2694,14 @@ function capGrant(own, given, model) {
2618
2694
  );
2619
2695
  }
2620
2696
  }
2697
+ function capTarget(own, current, model) {
2698
+ if (current !== null && !meetsLevel(own, current)) {
2699
+ throw new QuickdrawError(
2700
+ "FORBIDDEN",
2701
+ `That user has ${current} on this ${model}, above your own level on it (${own ?? "none"}); you cannot change or take away their access`
2702
+ );
2703
+ }
2704
+ }
2621
2705
 
2622
2706
  // src/server/kits/sharing/context.ts
2623
2707
  async function resolveTarget(call, context, given) {
@@ -2692,15 +2776,17 @@ async function checkLastAdmin(call, tx, listed, change) {
2692
2776
  async function changeList(call, context, change) {
2693
2777
  const columns = aclColumnsOf(call.runtime.service);
2694
2778
  return await inSerializable(call.db, async (tx) => {
2695
- capGrant(await checkCaller(call, context.form, change.id), change.level, call.model);
2779
+ const own = await checkCaller(call, context.form, change.id);
2780
+ capGrant(own, change.level, call.model);
2696
2781
  const table = tableOf(tx, call.model);
2697
2782
  const listed = await readList(table, columns, change.id, call.model);
2698
2783
  const problem = refusal2(listed, change, call.model);
2699
2784
  if (problem !== void 0) {
2700
2785
  throw problem;
2701
2786
  }
2702
- await checkLastAdmin(call, tx, listed, change);
2703
2787
  const before = levelIn(listed.entries, change.userId);
2788
+ capTarget(own, before, call.model);
2789
+ await checkLastAdmin(call, tx, listed, change);
2704
2790
  if (before === change.level && entriesOf(listed.entries, change.userId) === 1) {
2705
2791
  return sharesOf(listed.entries);
2706
2792
  }
@@ -2852,15 +2938,16 @@ async function invite(call, context, target, role) {
2852
2938
  async function end(call, context, target, kind) {
2853
2939
  const read = membershipTableOf(call.runtime.service);
2854
2940
  return await inSerializable(call.db, async (tx) => {
2855
- if (kind === "remove") {
2856
- await checkCaller(call, context.form, target.entryId);
2857
- }
2941
+ const own = kind === "remove" ? await checkCaller(call, context.form, target.entryId) : void 0;
2858
2942
  const table = tableOf(tx, read.model);
2859
2943
  const rows = await rowsOf(table, read, target);
2860
2944
  if (rows.length === 0) {
2861
2945
  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
2946
  }
2863
2947
  const current = currentRole(read, rows);
2948
+ if (own !== void 0) {
2949
+ capTarget(own, current.level, call.model);
2950
+ }
2864
2951
  if (current.level === "Admin" && !await anotherAdmin(call, tx, read, target)) {
2865
2952
  throw lastAdmin(call.model);
2866
2953
  }
@@ -2883,6 +2970,7 @@ async function changeRole(call, context, target, role) {
2883
2970
  throw new QuickdrawError("NOT_FOUND", `That user is not a member of this ${call.model}`);
2884
2971
  }
2885
2972
  const current = currentRole(read, rows);
2973
+ capTarget(own, current.level, call.model);
2886
2974
  if (rows.length === 1 && current.role === role) {
2887
2975
  return memberOf(read, target.userId, role);
2888
2976
  }
@@ -3071,6 +3159,9 @@ function handlers3(contract, ...rest) {
3071
3159
  const form = access[name] ?? DEFAULT_ACCESS[spec.method];
3072
3160
  const handler = handlerOf2(spec.method, Object.freeze({ ...hooks, form }));
3073
3161
  checkWhenDefined(handler, (service) => serviceProblem3(service, contract, spec.mode));
3162
+ if (spec.method === "listShares" || spec.method === "listMembers") {
3163
+ dependsOnCaller(handler);
3164
+ }
3074
3165
  entries[name] = kitEntry(name, form, handler, rowless);
3075
3166
  }
3076
3167
  return Object.freeze(entries);
@@ -3123,6 +3214,13 @@ var DEFAULT_FIELDS = Object.freeze({
3123
3214
  showInTable: false
3124
3215
  }
3125
3216
  });
3217
+ function grantNamesOf(column) {
3218
+ return column === void 0 || ADMIN_GRANT_FIELDS.includes(column) ? ADMIN_GRANT_FIELDS : [...ADMIN_GRANT_FIELDS, column];
3219
+ }
3220
+ function grantsColumnFor(model, source) {
3221
+ const same = model !== void 0 && source !== void 0;
3222
+ return same && modelKey(source.model) === modelKey(model) ? source.column : void 0;
3223
+ }
3126
3224
  function isRecord10(value) {
3127
3225
  return typeof value === "object" && value !== null && !Array.isArray(value);
3128
3226
  }
@@ -3142,23 +3240,19 @@ function checkDisplayName(displayName, serviceName, fail5) {
3142
3240
  }
3143
3241
  return displayName;
3144
3242
  }
3145
- function checkGrants(grants, names, fail5) {
3243
+ function checkGrants(grants, [names, grantNames], fail5) {
3146
3244
  if (grants === void 0 || grants === false) {
3147
3245
  return /* @__PURE__ */ new Set();
3148
3246
  }
3149
3247
  if (grants !== true) {
3150
3248
  fail5("grants must be true or false");
3151
3249
  }
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);
3250
+ return new Set(grantNames.filter((name) => names.includes(name)));
3159
3251
  }
3160
- function checkHidden(hiddenFields, names, grants, fail5) {
3161
- const defaults = ADMIN_HIDDEN_FIELDS.filter((name) => !grants.has(name));
3252
+ function checkHidden(hiddenFields, [names, grantNames], grants, fail5) {
3253
+ const defaults = [.../* @__PURE__ */ new Set([...ADMIN_HIDDEN_FIELDS, ...grantNames])].filter(
3254
+ (name) => !grants.has(name)
3255
+ );
3162
3256
  if (hiddenFields === void 0) {
3163
3257
  return new Set(defaults);
3164
3258
  }
@@ -3293,8 +3387,9 @@ function frozenConfig(config) {
3293
3387
  }
3294
3388
  function adminFieldsOf(serviceName, spec, options, fail5) {
3295
3389
  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);
3390
+ const grantNames = grantNamesOf(options.grantsColumn);
3391
+ const grants = checkGrants(options.grants, [names, grantNames], fail5);
3392
+ const hidden = checkHidden(options.hiddenFields, [names, grantNames], grants, fail5);
3298
3393
  const listed = [...spec.filter, ...spec.sort].find((name) => hidden.has(name));
3299
3394
  if (listed !== void 0) {
3300
3395
  fail5(`"${listed}" is hidden, so adminList may not filter or sort on it`);
@@ -3322,10 +3417,26 @@ function adminFieldsOf(serviceName, spec, options, fail5) {
3322
3417
  fields: Object.freeze(fields)
3323
3418
  });
3324
3419
  const readOnly = new Set(fields.filter((field) => !field.editable).map((field) => field.name));
3325
- return Object.freeze({ meta, hidden, readOnly, grants });
3420
+ const named = /* @__PURE__ */ new Set([
3421
+ ...editable ?? [],
3422
+ ...Object.entries(overrides).filter(([, override]) => override.editable === true).map(([name]) => name)
3423
+ ]);
3424
+ return Object.freeze({ meta, hidden, readOnly, grants, named });
3425
+ }
3426
+ function withReadOnlyFields(meta, readOnly) {
3427
+ if (!meta.fields.some((field) => readOnly.has(field.name) && field.editable)) {
3428
+ return meta;
3429
+ }
3430
+ const fields = meta.fields.map(
3431
+ (field) => readOnly.has(field.name) && field.editable ? frozenConfig({ ...field, editable: false }) : field
3432
+ );
3433
+ return Object.freeze({ ...meta, fields: Object.freeze(fields) });
3326
3434
  }
3327
3435
 
3328
3436
  // src/server/kits/admin/runtime.ts
3437
+ function grantsColumnOf(runtime) {
3438
+ return grantsColumnFor(runtime.service.model, runtime.grantsSource);
3439
+ }
3329
3440
  function tableOf2(db, model) {
3330
3441
  const name = modelKey(model);
3331
3442
  const isClient = (typeof db === "object" || typeof db === "function") && db !== null;
@@ -3339,7 +3450,7 @@ function tableOf2(db, model) {
3339
3450
  }
3340
3451
  return delegate;
3341
3452
  }
3342
- function adminCall(ctx, db, fields) {
3453
+ function adminCall(ctx, db, fieldsFor) {
3343
3454
  const runtime = kitRuntimeOf(ctx);
3344
3455
  const model = runtime?.service.model;
3345
3456
  const projection = runtime?.service.projections.get("entity");
@@ -3349,6 +3460,7 @@ function adminCall(ctx, db, fields) {
3349
3460
  "The admin kit's handlers run through a dispatcher, in a service with a model and an entity"
3350
3461
  );
3351
3462
  }
3463
+ const fields = fieldsFor(grantsColumnOf(runtime));
3352
3464
  const { principal } = ctx;
3353
3465
  const level = principal === null ? null : serviceGrant(principal, runtime.service.name) ?? null;
3354
3466
  const tiered = projection.tiers.hidden(level);
@@ -3360,7 +3472,8 @@ function adminCall(ctx, db, fields) {
3360
3472
  principal,
3361
3473
  projection,
3362
3474
  unseen,
3363
- omitted: /* @__PURE__ */ new Set([...unseen, ...fields.hidden])
3475
+ omitted: /* @__PURE__ */ new Set([...unseen, ...fields.hidden]),
3476
+ fields
3364
3477
  };
3365
3478
  }
3366
3479
  function rowOut(call, row) {
@@ -3384,7 +3497,7 @@ function checkSeen(call, names) {
3384
3497
  var NO_NULLABLE = /* @__PURE__ */ new Set();
3385
3498
  function adminListHandler(context) {
3386
3499
  const handler = async ({ input, ctx, db }) => {
3387
- const call = adminCall(ctx, db, context.fields);
3500
+ const call = adminCall(ctx, db, context.fieldsFor);
3388
3501
  const query = input;
3389
3502
  checkSeen(call, namedFields(query));
3390
3503
  const order = listOrder(defaultSorts(context.spec.sort, call.unseen), query.sort);
@@ -3435,14 +3548,14 @@ function subscribersOf(call, id) {
3435
3548
  }
3436
3549
  function adminSubscribersHandler(context) {
3437
3550
  const handler = ({ input, ctx, db }) => {
3438
- const call = adminCall(ctx, db, context.fields);
3551
+ const call = adminCall(ctx, db, context.fieldsFor);
3439
3552
  return Promise.resolve(subscribersOf(call, input.id));
3440
3553
  };
3441
3554
  return handler;
3442
3555
  }
3443
3556
  function adminReemitHandler(context) {
3444
3557
  const handler = async ({ input, ctx, db }) => {
3445
- const call = adminCall(ctx, db, context.fields);
3558
+ const call = adminCall(ctx, db, context.fieldsFor);
3446
3559
  const { id } = input;
3447
3560
  requireRow(
3448
3561
  await call.table.findUnique({ where: { id }, select: { id: true } }),
@@ -3456,7 +3569,7 @@ function adminReemitHandler(context) {
3456
3569
 
3457
3570
  // src/server/kits/admin/writes.ts
3458
3571
  async function checkWrite(call, context, data) {
3459
- const { hidden, readOnly } = context.fields;
3572
+ const { hidden, readOnly } = call.fields;
3460
3573
  const names = Object.keys(data);
3461
3574
  const issues = names.filter((name) => hidden.has(name) || readOnly.has(name)).map((name) => ({
3462
3575
  path: ["data", name],
@@ -3472,9 +3585,49 @@ async function checkWrite(call, context, data) {
3472
3585
  call.runtime,
3473
3586
  call.principal,
3474
3587
  data,
3475
- rowLevel(context.form, "Moderate")
3588
+ rowLevel(context.form, "Moderate"),
3589
+ ["data"]
3476
3590
  );
3477
3591
  }
3592
+ function preparedIssues(call, columns) {
3593
+ const never = Object.keys(columns).filter((name) => ADMIN_NEVER_WRITABLE.includes(name)).map((name) => ({
3594
+ path: ["data", name],
3595
+ message: `"${name}" is not writable; the database sets it`
3596
+ }));
3597
+ return [...never, ...columnValueIssues(call.runtime.service, columns, ["data"])];
3598
+ }
3599
+ async function prepareWrite(call, context, write, ctx, db) {
3600
+ const { prepare } = context;
3601
+ const data = write.method === "adminDelete" ? {} : write.data;
3602
+ if (prepare === void 0) {
3603
+ return data;
3604
+ }
3605
+ const returned = await checked(() => prepare(write, ctx, db));
3606
+ if (write.method === "adminDelete" || returned === void 0) {
3607
+ return data;
3608
+ }
3609
+ const { name } = call.runtime.service;
3610
+ if (!isPlainObject(returned)) {
3611
+ throw new QuickdrawError(
3612
+ "INTERNAL",
3613
+ `The admin kit's prepare for ${name} must return column values, or nothing`
3614
+ );
3615
+ }
3616
+ const columns = writtenData(returned);
3617
+ const issues = preparedIssues(call, columns);
3618
+ if (issues.length > 0) {
3619
+ throw new QuickdrawError(
3620
+ "VALIDATION",
3621
+ `The admin kit's prepare for ${name} returned data the kit does not write`,
3622
+ { issues }
3623
+ );
3624
+ }
3625
+ const level = rowLevel(context.form, "Moderate");
3626
+ await checkAnchorColumns(call.runtime, call.principal, columns, level, {
3627
+ creating: write.method === "adminCreate"
3628
+ });
3629
+ return { ...data, ...columns };
3630
+ }
3478
3631
  function isValueRefusal(error) {
3479
3632
  if (!(error instanceof Error)) {
3480
3633
  return false;
@@ -3516,7 +3669,7 @@ function committed(context, args, write) {
3516
3669
  category: "quickdraw.admin",
3517
3670
  method: write.method,
3518
3671
  id: write.id,
3519
- error: error instanceof Error ? error.message : String(error)
3672
+ error: errorText(error)
3520
3673
  });
3521
3674
  };
3522
3675
  try {
@@ -3525,43 +3678,71 @@ function committed(context, args, write) {
3525
3678
  failed(error);
3526
3679
  }
3527
3680
  }
3528
- async function written(context, call, args, method, perform) {
3529
- const { onWrite, onCommitted } = context;
3530
- const attempt = async (table, before) => {
3681
+ async function rowBefore(context, call, table, write) {
3682
+ const readsAsItDeletes = write.method === "adminDelete" && context.prepare === void 0;
3683
+ if (write.method === "adminCreate" || readsAsItDeletes) {
3684
+ return void 0;
3685
+ }
3686
+ const row = await table.findUnique({ where: { id: write.id }, select: call.projection.select });
3687
+ return requireRow(row, `No such ${call.model}`);
3688
+ }
3689
+ function prepareCall(write, before) {
3690
+ if (write.method === "adminCreate") {
3691
+ return Object.freeze({ method: write.method, data: Object.freeze({ ...write.data }) });
3692
+ }
3693
+ if (write.method === "adminUpdate") {
3694
+ const data = Object.freeze({ ...write.data });
3695
+ return Object.freeze({ method: write.method, id: write.id, data, before });
3696
+ }
3697
+ return Object.freeze({ method: write.method, id: write.id, before });
3698
+ }
3699
+ async function written(context, call, args, write, perform) {
3700
+ const { prepare, onWrite, onCommitted } = context;
3701
+ const data = write.method === "adminDelete" ? {} : write.data;
3702
+ const attempt = async (table, values, before) => {
3531
3703
  try {
3532
- return await perform(table, before);
3704
+ return await perform({ table, values, before });
3533
3705
  } catch (error) {
3534
3706
  throw writeRefusal(error);
3535
3707
  }
3536
3708
  };
3537
- if (onWrite === void 0 && onCommitted === void 0) {
3538
- return (await attempt(call.table, false)).reply;
3709
+ if (prepare === void 0 && onWrite === void 0 && onCommitted === void 0) {
3710
+ return (await attempt(call.table, data, false)).reply;
3539
3711
  }
3540
- let write;
3712
+ const ctx = args.ctx;
3713
+ let heard;
3541
3714
  const reply = await inKitTransaction(
3542
3715
  args.db,
3543
3716
  async (tx) => {
3544
- const done = await attempt(tableOf2(tx, call.model), method !== "adminCreate");
3717
+ const table = tableOf2(tx, call.model);
3545
3718
  const whole = (row) => row ? rowWhole(call, row) : null;
3546
- write = {
3547
- method,
3719
+ const first = await rowBefore(context, call, table, write);
3720
+ const before = first === void 0 ? void 0 : whole(first);
3721
+ const values = await prepareWrite(call, context, prepareCall(write, before), ctx, tx);
3722
+ const done = await attempt(
3723
+ table,
3724
+ values,
3725
+ write.method === "adminDelete" && first === void 0
3726
+ );
3727
+ heard = {
3728
+ method: write.method,
3548
3729
  id: done.id,
3549
- ...method === "adminCreate" ? {} : { before: whole(done.before) },
3730
+ ...write.method === "adminCreate" ? {} : { before: before ?? whole(done.before) },
3550
3731
  after: whole(done.after)
3551
3732
  };
3552
- await onWrite?.(write, args.ctx, tx);
3733
+ await onWrite?.(heard, ctx, tx);
3553
3734
  return done.reply;
3554
3735
  },
3555
- { owner: "The admin kit's writes with onWrite or onCommitted" }
3736
+ { owner: "The admin kit's writes with prepare, onWrite or onCommitted" }
3556
3737
  );
3557
- if (write !== void 0) {
3558
- committed(context, args, write);
3738
+ if (heard !== void 0) {
3739
+ committed(context, args, heard);
3559
3740
  }
3560
3741
  return reply;
3561
3742
  }
3562
3743
  function adminGetHandler(context) {
3563
3744
  const handler = async ({ input, ctx, db }) => {
3564
- const call = adminCall(ctx, db, context.fields);
3745
+ const call = adminCall(ctx, db, context.fieldsFor);
3565
3746
  return await readRow(call, input.id);
3566
3747
  };
3567
3748
  return handler;
@@ -3571,11 +3752,13 @@ function idOf(row) {
3571
3752
  }
3572
3753
  function adminCreateHandler(context) {
3573
3754
  const handler = async (args) => {
3574
- const call = adminCall(args.ctx, args.db, context.fields);
3755
+ const call = adminCall(args.ctx, args.db, context.fieldsFor);
3575
3756
  const { data } = args.input;
3576
3757
  await checkWrite(call, context, data);
3577
- return await written(context, call, args, "adminCreate", async (table) => {
3578
- const after = await table.create({ data, select: call.projection.select });
3758
+ const { select } = call.projection;
3759
+ const write = { method: "adminCreate", data };
3760
+ return await written(context, call, args, write, async ({ table, values }) => {
3761
+ const after = await table.create({ data: values, select });
3579
3762
  return { id: idOf(after), after, reply: rowOut(call, after) };
3580
3763
  });
3581
3764
  };
@@ -3583,29 +3766,30 @@ function adminCreateHandler(context) {
3583
3766
  }
3584
3767
  function adminUpdateHandler(context) {
3585
3768
  const handler = async (args) => {
3586
- const call = adminCall(args.ctx, args.db, context.fields);
3769
+ const call = adminCall(args.ctx, args.db, context.fieldsFor);
3587
3770
  const { id, data } = args.input;
3588
3771
  await checkWrite(call, context, data);
3589
3772
  if (Object.keys(data).length === 0) {
3590
3773
  return await readRow(call, id);
3591
3774
  }
3592
3775
  const { select } = call.projection;
3593
- return await written(context, call, args, "adminUpdate", async (table, withBefore) => {
3594
- const before = withBefore ? await table.findUnique({ where: { id }, select }) : void 0;
3595
- const after = await table.update({ where: { id }, data, select });
3596
- return { id, before, after, reply: rowOut(call, after) };
3776
+ const write = { method: "adminUpdate", id, data };
3777
+ return await written(context, call, args, write, async ({ table, values }) => {
3778
+ const after = await table.update({ where: { id }, data: values, select });
3779
+ return { id, after, reply: rowOut(call, after) };
3597
3780
  });
3598
3781
  };
3599
3782
  return handler;
3600
3783
  }
3601
3784
  function adminDeleteHandler(context) {
3602
3785
  const handler = async (args) => {
3603
- const call = adminCall(args.ctx, args.db, context.fields);
3786
+ const call = adminCall(args.ctx, args.db, context.fieldsFor);
3604
3787
  const { id } = args.input;
3605
- return await written(context, call, args, "adminDelete", async (table, withBefore) => {
3606
- const select = withBefore ? call.projection.select : { id: true };
3607
- const before = await table.delete({ where: { id }, select });
3608
- return { id, ...withBefore ? { before } : {}, after: null, reply: null };
3788
+ const write = { method: "adminDelete", id };
3789
+ return await written(context, call, args, write, async ({ table, before }) => {
3790
+ const select = before ? call.projection.select : { id: true };
3791
+ const row = await table.delete({ where: { id }, select });
3792
+ return { id, ...before ? { before: row } : {}, after: null, reply: null };
3609
3793
  });
3610
3794
  };
3611
3795
  return handler;
@@ -3623,8 +3807,23 @@ var MAKERS2 = Object.freeze({
3623
3807
  adminReemit: adminReemitHandler
3624
3808
  });
3625
3809
  function adminMetaHandler(context) {
3626
- const { meta } = context.fields;
3627
- const handler = (_args) => Promise.resolve(meta);
3810
+ const byService = /* @__PURE__ */ new WeakMap();
3811
+ const handler = (args) => {
3812
+ const runtime = kitRuntimeOf(args.ctx);
3813
+ if (runtime === void 0) {
3814
+ return Promise.resolve(context.fieldsFor(void 0).meta);
3815
+ }
3816
+ const { service } = runtime;
3817
+ const column = grantsColumnFor(service.model, runtime.grantsSource);
3818
+ const answers = byService.get(service) ?? /* @__PURE__ */ new Map();
3819
+ byService.set(service, answers);
3820
+ let answer = answers.get(column ?? "");
3821
+ if (answer === void 0) {
3822
+ answer = withReadOnlyFields(context.fieldsFor(column).meta, relationKeysOf(service));
3823
+ answers.set(column ?? "", answer);
3824
+ }
3825
+ return Promise.resolve(answer);
3826
+ };
3628
3827
  return handler;
3629
3828
  }
3630
3829
  function handlerOf3(context) {
@@ -3633,7 +3832,7 @@ function handlerOf3(context) {
3633
3832
  }
3634
3833
 
3635
3834
  // src/server/kits/admin/handlers.ts
3636
- var OPTION_KEYS4 = [
3835
+ var OPTION_KEYS3 = [
3637
3836
  "access",
3638
3837
  "displayName",
3639
3838
  "hiddenFields",
@@ -3641,6 +3840,7 @@ var OPTION_KEYS4 = [
3641
3840
  "fieldOverrides",
3642
3841
  "rowless",
3643
3842
  "grants",
3843
+ "prepare",
3644
3844
  "onWrite",
3645
3845
  "onCommitted"
3646
3846
  ];
@@ -3678,12 +3878,12 @@ function checkOptions3(options) {
3678
3878
  }
3679
3879
  if (!isRecord11(options)) {
3680
3880
  fail4(
3681
- "options must be { access?, displayName?, hiddenFields?, editable?, fieldOverrides?, rowless?, grants?, onWrite?, onCommitted? }"
3881
+ "options must be { access?, displayName?, hiddenFields?, editable?, fieldOverrides?, rowless?, grants?, prepare?, onWrite?, onCommitted? }"
3682
3882
  );
3683
3883
  }
3684
- const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS4.includes(key));
3884
+ const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS3.includes(key));
3685
3885
  if (unknownKey !== void 0) {
3686
- fail4(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS4.join(", ")}`);
3886
+ fail4(`options has an unknown key "${unknownKey}"; the options are ${OPTION_KEYS3.join(", ")}`);
3687
3887
  }
3688
3888
  return options;
3689
3889
  }
@@ -3719,7 +3919,7 @@ function contractEditable(contract, specs) {
3719
3919
  }
3720
3920
  return [...lists.values()][0];
3721
3921
  }
3722
- function fieldsOf(contract, kit, options) {
3922
+ function fieldsOf(contract, kit, options, grantsColumn) {
3723
3923
  const specs = kit.map(([, spec2]) => spec2);
3724
3924
  const [first] = specs;
3725
3925
  const spec = {
@@ -3736,16 +3936,47 @@ function fieldsOf(contract, kit, options) {
3736
3936
  hiddenFields: options.hiddenFields,
3737
3937
  editable: options.editable,
3738
3938
  fieldOverrides: options.fieldOverrides,
3739
- grants: options.grants
3939
+ grants: options.grants,
3940
+ ...grantsColumn === void 0 ? {} : { grantsColumn }
3740
3941
  },
3741
3942
  fail4
3742
3943
  );
3743
3944
  }
3744
- function serviceProblem4(service, contract) {
3945
+ function fieldsByColumn(contract, kit, options) {
3946
+ const byColumn = /* @__PURE__ */ new Map();
3947
+ return (grantsColumn) => {
3948
+ let fields = byColumn.get(grantsColumn ?? "");
3949
+ if (fields === void 0) {
3950
+ fields = fieldsOf(contract, kit, options, grantsColumn);
3951
+ byColumn.set(grantsColumn ?? "", fields);
3952
+ }
3953
+ return fields;
3954
+ };
3955
+ }
3956
+ function servedProblemOf(fieldsFor, grants, { service, grantsSource }) {
3957
+ const column = grantsColumnFor(service.model, grantsSource);
3958
+ let fields;
3959
+ try {
3960
+ fields = fieldsFor(column);
3961
+ } catch (error) {
3962
+ return error instanceof Error ? error.message : String(error);
3963
+ }
3964
+ if (grants && fields.grants.size === 0) {
3965
+ 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`;
3966
+ return `grants: true needs a grants field: the entity has no serviceAccess or service_access field, and ${named}`;
3967
+ }
3968
+ return void 0;
3969
+ }
3970
+ function serviceProblem4(service, contract, fields) {
3745
3971
  if (service.contract !== contract) {
3746
3972
  return `its admin kit handlers were made for another contract; pass ${service.name}'s own contract to admin.handlers`;
3747
3973
  }
3748
- return service.model === void 0 ? "the admin kit reads and writes the service's rows: declare its model" : void 0;
3974
+ if (service.model === void 0) {
3975
+ return "the admin kit reads and writes the service's rows: declare its model";
3976
+ }
3977
+ const relations2 = relationKeysOf(service);
3978
+ const relation = [...fields.named].find((name) => relations2.has(name));
3979
+ 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
3980
  }
3750
3981
  function handlers4(contract, options) {
3751
3982
  const kit = kitMethods4(contract);
@@ -3753,8 +3984,12 @@ function handlers4(contract, options) {
3753
3984
  const names = kit.map(([name]) => name);
3754
3985
  const access = checkAccess3(checked2.access, names);
3755
3986
  const rowless = rowlessMethods(checked2.rowless, names, fail4);
3756
- const fields = fieldsOf(contract, kit, checked2);
3757
- const { onWrite, onCommitted } = checked2;
3987
+ const fieldsFor = fieldsByColumn(contract, kit, checked2);
3988
+ const fields = fieldsFor(void 0);
3989
+ const { prepare, onWrite, onCommitted } = checked2;
3990
+ if (prepare !== void 0 && typeof prepare !== "function") {
3991
+ fail4("prepare must be a function of (call, ctx, db)");
3992
+ }
3758
3993
  if (onWrite !== void 0 && typeof onWrite !== "function") {
3759
3994
  fail4("onWrite must be a function of (write, ctx, db)");
3760
3995
  }
@@ -3762,17 +3997,25 @@ function handlers4(contract, options) {
3762
3997
  fail4("onCommitted must be a function of (write, ctx)");
3763
3998
  }
3764
3999
  const hooks = {
4000
+ prepare,
3765
4001
  onWrite,
3766
4002
  onCommitted
3767
4003
  };
3768
4004
  const entries = {};
3769
4005
  for (const [name, spec] of kit) {
3770
4006
  const form = access[name] ?? ADMIN_DEFAULT_ACCESS;
3771
- const handler = handlerOf3({ spec, fields, form, ...hooks });
3772
- checkWhenDefined(handler, (service) => serviceProblem4(service, contract));
4007
+ const handler = handlerOf3({ spec, fieldsFor, form, ...hooks });
4008
+ checkWhenDefined(handler, (service) => serviceProblem4(service, contract, fields));
4009
+ checkWhenServed(
4010
+ handler,
4011
+ (served) => servedProblemOf(fieldsFor, checked2.grants === true, served)
4012
+ );
3773
4013
  if (checked2.grants === true) {
3774
4014
  markGrantsEditor(handler);
3775
4015
  }
4016
+ if (spec.method === "adminList" || spec.method === "adminGet") {
4017
+ dependsOnCaller(handler);
4018
+ }
3776
4019
  entries[name] = kitEntry(name, form, handler, rowless);
3777
4020
  }
3778
4021
  return Object.freeze(entries);
@@ -3816,7 +4059,8 @@ function reportErrors(client, logger, role) {
3816
4059
  }
3817
4060
  async function setupRedisAdapter(io, options = {}) {
3818
4061
  const logger = options.logger?.child({ service: "RedisAdapter" }) ?? consoleLogger.child({ service: "RedisAdapter" });
3819
- const { host = "localhost", port = 6379, password, db = 0, keyPrefix = "socket.io" } = options;
4062
+ const { host = "localhost", port = 6379, password, db = 0 } = options;
4063
+ const key = options.keyPrefix ?? deploymentOf(io);
3820
4064
  try {
3821
4065
  const peers = await loadRedisPeers();
3822
4066
  const createAdapter = peers.createAdapter;
@@ -3830,10 +4074,9 @@ async function setupRedisAdapter(io, options = {}) {
3830
4074
  reportErrors(pubClient, logger, "publish");
3831
4075
  reportErrors(subClient, logger, "subscribe");
3832
4076
  await Promise.all([pubClient.connect(), subClient.connect()]);
3833
- io.adapter(
3834
- createAdapter(pubClient, subClient, { key: keyPrefix })
3835
- );
4077
+ io.adapter(createAdapter(pubClient, subClient, { key }));
3836
4078
  watchAdapterClients(io, logger);
4079
+ seedRoomHosts(io);
3837
4080
  logger.info(`Redis adapter connected to ${host}:${port}`);
3838
4081
  const cleanup = async () => {
3839
4082
  try {
@@ -3977,8 +4220,11 @@ export {
3977
4220
  DevWarningError,
3978
4221
  INDEX_MAX_ROWS,
3979
4222
  MAX_APP_ROOMS,
4223
+ MAX_ENTITY_IDS_PER_SOCKET,
3980
4224
  MAX_ITEM_IDS,
4225
+ MAX_SCOPES_PER_SOCKET,
3981
4226
  MAX_STREAMS_PER_SOCKET,
4227
+ MAX_TOPICS_PER_SOCKET,
3982
4228
  MIN_STALL_INTERVAL_MS,
3983
4229
  N_PLUS_ONE_STATEMENTS,
3984
4230
  ORDINAL_STEP,