deepspace 0.26.0 → 0.26.1

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 (52) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/{chunk-LPXWXF6R.js → chunk-YBXIUDZJ.js} +2 -2
  3. package/dist/{chunk-LPXWXF6R.js.map → chunk-YBXIUDZJ.js.map} +1 -1
  4. package/dist/cli.js +80 -45
  5. package/dist/cli.js.map +1 -1
  6. package/dist/documentation-client-core.js +1 -1
  7. package/dist/documentation-default-renderer.cjs +1 -1
  8. package/dist/documentation-react.js +1 -1
  9. package/dist/documentation-runtime.js +1 -1
  10. package/dist/documentation-server-core.js +1 -1
  11. package/dist/documentation.js +1 -1
  12. package/dist/documentation.js.map +1 -1
  13. package/dist/index.d.ts +26 -434
  14. package/dist/index.js +99 -864
  15. package/dist/index.js.map +1 -1
  16. package/dist/schema.d.ts +4 -41
  17. package/dist/schema.js +2 -242
  18. package/dist/schema.js.map +1 -1
  19. package/dist/server.d.ts +6 -162
  20. package/dist/server.js +13 -281
  21. package/dist/server.js.map +1 -1
  22. package/dist/worker.d.ts +4 -160
  23. package/dist/worker.js +15 -281
  24. package/dist/worker.js.map +1 -1
  25. package/features/messaging/feature.json +3 -43
  26. package/features/messaging/src/ChatPage.tsx +12 -16
  27. package/features/messaging/src/components/chat/ChatHeader.tsx +5 -13
  28. package/features/messaging/src/components/chat/MessageItem.tsx +7 -9
  29. package/features/messaging/src/components/chat/MessageList.tsx +57 -40
  30. package/features/messaging/src/components/chat/ThreadPanel.tsx +25 -11
  31. package/features/messaging/src/components/chat/UserProfilePopover.tsx +10 -29
  32. package/features/messaging/src/hooks/useChatChannel.ts +18 -23
  33. package/features/messaging/src/messaging-schema.ts +0 -2
  34. package/features/search-bar/feature.json +9 -20
  35. package/features/search-bar/src/InlineSearch.tsx +101 -0
  36. package/features/search-bar/src/__tests__/search-model.test.tsx +38 -0
  37. package/features/search-bar/src/search-model.ts +2 -149
  38. package/package.json +1 -1
  39. package/features/messaging/src/ChatMultiPage.tsx +0 -298
  40. package/features/messaging/src/components/chat-multi/AddMemberModal.tsx +0 -188
  41. package/features/messaging/src/components/chat-multi/BrowseGroupsModal.tsx +0 -163
  42. package/features/messaging/src/components/chat-multi/ChannelListItem.tsx +0 -147
  43. package/features/messaging/src/components/chat-multi/ChannelSettingsPanel.tsx +0 -344
  44. package/features/messaging/src/components/chat-multi/ChannelSidebar.tsx +0 -460
  45. package/features/messaging/src/components/chat-multi/CreateGroupModal.tsx +0 -190
  46. package/features/messaging/src/components/chat-multi/InvitationList.tsx +0 -149
  47. package/features/messaging/src/components/chat-multi/NewMessageModal.tsx +0 -159
  48. package/features/messaging/src/hooks/useMultiChannel.ts +0 -142
  49. package/features/search-bar/src/SearchOverlay.tsx +0 -566
  50. package/features/search-bar/src/__tests__/search-model.test.ts +0 -40
  51. package/features/sidebar/feature.json +0 -19
  52. package/features/sidebar/src/AppSidebar.tsx +0 -558
package/dist/schema.d.ts CHANGED
@@ -123,7 +123,9 @@ declare const AI_MESSAGES_SCHEMA: CollectionSchema;
123
123
  * Messaging Schemas
124
124
  *
125
125
  * Pre-built collection schemas for messaging functionality.
126
- * Any app can import these to add channels, messages, reactions, etc.
126
+ * These schemas intentionally model public channels only. RecordRoom row
127
+ * permissions cannot make a channel private by themselves; advertising DMs
128
+ * or private groups here would expose their records to every room member.
127
129
  *
128
130
  * @example
129
131
  * ```typescript
@@ -136,45 +138,6 @@ declare const CHANNELS_SCHEMA: CollectionSchema;
136
138
  declare const MESSAGES_SCHEMA: CollectionSchema;
137
139
  declare const REACTIONS_SCHEMA: CollectionSchema;
138
140
  declare const CHANNEL_MEMBERS_SCHEMA: CollectionSchema;
139
- declare const CHANNEL_INVITATIONS_SCHEMA: CollectionSchema;
140
141
  declare const READ_RECEIPTS_SCHEMA: CollectionSchema;
141
142
 
142
- /**
143
- * Shared conversation schemas for RecordRoom-based conversations.
144
- *
145
- * These define the collections inside a per-conversation RecordRoom DO.
146
- * Used by apps that have messaging/conversation features (slack-clone,
147
- * helpdesk, mail, reddit-clone, etc.).
148
- *
149
- * Each conversation gets its own RecordRoom DO keyed by `conv:{id}`.
150
- */
151
-
152
- declare const CONVERSATION_SCHEMAS: CollectionSchema[];
153
- /**
154
- * Voting schemas for Reddit-style apps.
155
- * Add these alongside CONVERSATION_SCHEMAS for apps that need voting.
156
- */
157
- declare const VOTING_SCHEMAS: CollectionSchema[];
158
-
159
- /**
160
- * Directory DO Schemas
161
- *
162
- * Purpose-built collections for the `dir:{appName}` global DO type.
163
- * This is the cross-app-visible directory layer — any app can subscribe
164
- * to another app's directory in real-time via SHARED_CONNECTIONS.
165
- *
166
- * Five collections covering all standard communication/social patterns:
167
- * - conversations: channels, DMs, email threads, support chats
168
- * - conversation_state: per-user metadata (read cursor, stars, labels, folders)
169
- * - communities: groups, forums, boards, projects
170
- * - memberships: user membership in communities
171
- * - posts: feed items, tweets, Q&A questions, announcements
172
- *
173
- * Domain-specific data (tickets, CRM, procurement) stays in app DOs.
174
- * Message content stays in conv:{id} DOs (CONVERSATION_SCHEMAS).
175
- */
176
-
177
- /** All directory schemas for the `dir:{appName}` global DO type. */
178
- declare const DIRECTORY_SCHEMAS: CollectionSchema[];
179
-
180
- export { AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA, BASE_USERS_SCHEMA, CHANNELS_SCHEMA, CHANNEL_INVITATIONS_SCHEMA, CHANNEL_MEMBERS_SCHEMA, CONVERSATION_SCHEMAS, type CollectionSchema, type ColumnDefinition, type ColumnInterpretation, DIRECTORY_SCHEMAS, MESSAGES_SCHEMA, type PermissionLevel, REACTIONS_SCHEMA, READ_RECEIPTS_SCHEMA, type RolePermissions, USERS_COLUMNS, VOTING_SCHEMAS };
143
+ export { AI_CHATS_SCHEMA, AI_MESSAGES_SCHEMA, BASE_USERS_SCHEMA, CHANNELS_SCHEMA, CHANNEL_MEMBERS_SCHEMA, type CollectionSchema, type ColumnDefinition, type ColumnInterpretation, MESSAGES_SCHEMA, type PermissionLevel, REACTIONS_SCHEMA, READ_RECEIPTS_SCHEMA, type RolePermissions, USERS_COLUMNS };
package/dist/schema.js CHANGED
@@ -65,7 +65,7 @@ var CHANNELS_SCHEMA = {
65
65
  {
66
66
  name: "type",
67
67
  storage: "text",
68
- interpretation: { kind: "select", options: ["public", "private", "dm"] }
68
+ interpretation: { kind: "select", options: ["public"] }
69
69
  },
70
70
  {
71
71
  name: "createdBy",
@@ -141,44 +141,6 @@ var CHANNEL_MEMBERS_SCHEMA = {
141
141
  viewer: { read: true, create: false, update: false, delete: false }
142
142
  }
143
143
  };
144
- var CHANNEL_INVITATIONS_SCHEMA = {
145
- name: "channel-invitations",
146
- columns: [
147
- {
148
- name: "channelId",
149
- storage: "text",
150
- interpretation: "plain",
151
- required: true,
152
- immutable: true
153
- },
154
- {
155
- name: "invitedUserId",
156
- storage: "text",
157
- interpretation: "plain",
158
- required: true,
159
- immutable: true
160
- },
161
- {
162
- name: "invitedBy",
163
- storage: "text",
164
- interpretation: "plain",
165
- userBound: true,
166
- immutable: true
167
- },
168
- {
169
- name: "status",
170
- storage: "text",
171
- interpretation: { kind: "select", options: ["pending", "accepted", "declined"] },
172
- required: true
173
- }
174
- ],
175
- uniqueOn: ["channelId", "invitedUserId"],
176
- permissions: {
177
- admin: { read: true, create: true, update: true, delete: true },
178
- member: { read: true, create: true, update: true, delete: false },
179
- viewer: { read: true, create: false, update: false, delete: false }
180
- }
181
- };
182
144
  var READ_RECEIPTS_SCHEMA = {
183
145
  name: "read-receipts",
184
146
  columns: [
@@ -194,217 +156,15 @@ var READ_RECEIPTS_SCHEMA = {
194
156
  viewer: { read: false, create: false, update: false, delete: false }
195
157
  }
196
158
  };
197
-
198
- // src/server/schemas/conversation.ts
199
- var CONVERSATION_SCHEMAS = [
200
- {
201
- name: "conv_messages",
202
- columns: [
203
- { name: "Content", storage: "text", interpretation: "plain" },
204
- { name: "AuthorId", storage: "text", interpretation: "plain" },
205
- { name: "ParentId", storage: "text", interpretation: "plain" },
206
- { name: "Edited", storage: "number", interpretation: "boolean" },
207
- { name: "MessageType", storage: "text", interpretation: "plain" },
208
- { name: "Metadata", storage: "text", interpretation: "json" }
209
- ],
210
- permissions: {
211
- viewer: { read: true, create: false, update: false, delete: false },
212
- member: { read: true, create: true, update: "own", delete: "own" },
213
- admin: { read: true, create: true, update: true, delete: true }
214
- },
215
- ownerField: "AuthorId"
216
- },
217
- {
218
- name: "conv_reactions",
219
- columns: [
220
- { name: "MessageId", storage: "text", interpretation: "plain" },
221
- { name: "Emoji", storage: "text", interpretation: "plain" },
222
- { name: "UserId", storage: "text", interpretation: "plain" }
223
- ],
224
- uniqueOn: ["MessageId", "Emoji", "UserId"],
225
- permissions: {
226
- viewer: { read: true, create: false, update: false, delete: false },
227
- member: { read: true, create: true, update: false, delete: "own" },
228
- admin: { read: true, create: true, update: false, delete: true }
229
- },
230
- ownerField: "UserId"
231
- },
232
- {
233
- name: "conv_members",
234
- columns: [
235
- { name: "UserId", storage: "text", interpretation: "plain" },
236
- { name: "UserName", storage: "text", interpretation: "plain" },
237
- { name: "Role", storage: "text", interpretation: "plain" }
238
- ],
239
- uniqueOn: ["UserId"],
240
- permissions: {
241
- viewer: { read: true, create: false, update: false, delete: false },
242
- member: { read: true, create: true, update: "own", delete: false },
243
- admin: { read: true, create: true, update: true, delete: true }
244
- },
245
- ownerField: "UserId"
246
- },
247
- {
248
- name: "conv_read_cursors",
249
- columns: [
250
- { name: "UserId", storage: "text", interpretation: "plain" },
251
- { name: "LastReadAt", storage: "text", interpretation: "plain" }
252
- ],
253
- uniqueOn: ["UserId"],
254
- permissions: {
255
- viewer: { read: true, create: false, update: false, delete: false },
256
- member: { read: true, create: true, update: "own", delete: false },
257
- admin: { read: true, create: true, update: true, delete: true }
258
- },
259
- ownerField: "UserId"
260
- }
261
- ];
262
- var VOTING_SCHEMAS = [
263
- {
264
- name: "conv_votes",
265
- columns: [
266
- { name: "TargetId", storage: "text", interpretation: "plain" },
267
- { name: "UserId", storage: "text", interpretation: "plain" },
268
- { name: "Direction", storage: "number", interpretation: "plain" }
269
- ],
270
- uniqueOn: ["TargetId", "UserId"],
271
- permissions: {
272
- viewer: { read: true, create: false, update: false, delete: false },
273
- member: { read: true, create: true, update: "own", delete: "own" },
274
- admin: { read: true, create: true, update: true, delete: true }
275
- },
276
- ownerField: "UserId"
277
- }
278
- ];
279
-
280
- // src/server/schemas/directory.ts
281
- var conversationsSchema = {
282
- name: "conversations",
283
- columns: [
284
- { name: "Name", storage: "text", interpretation: "plain" },
285
- { name: "Description", storage: "text", interpretation: "plain" },
286
- { name: "Type", storage: "text", interpretation: { kind: "select", options: ["public", "private", "dm", "group", "email", "outbound-email", "support", "announcement"] } },
287
- { name: "Visibility", storage: "text", interpretation: { kind: "select", options: ["public", "private", "restricted"] } },
288
- { name: "CreatedBy", storage: "text", interpretation: "plain" },
289
- { name: "ParticipantHash", storage: "text", interpretation: "plain" },
290
- { name: "ParticipantIds", storage: "text", interpretation: "json" },
291
- { name: "Status", storage: "text", interpretation: { kind: "select", options: ["active", "archived", "resolved", "closed"] } },
292
- { name: "AssigneeId", storage: "text", interpretation: "plain" },
293
- { name: "LinkedRef", storage: "text", interpretation: "json" },
294
- { name: "LastMessageAt", storage: "text", interpretation: { kind: "datetime" } },
295
- { name: "LastMessagePreview", storage: "text", interpretation: "plain" },
296
- { name: "LastMessageAuthor", storage: "text", interpretation: "plain" },
297
- { name: "MessageCount", storage: "number", interpretation: "plain" }
298
- ],
299
- collaboratorsField: "ParticipantIds",
300
- visibilityField: "Visibility",
301
- permissions: {
302
- viewer: { read: "shared", create: false, update: false, delete: false },
303
- member: { read: "shared", create: true, update: true, delete: "own" },
304
- admin: { read: true, create: true, update: true, delete: true }
305
- },
306
- ownerField: "CreatedBy"
307
- };
308
- var conversationStateSchema = {
309
- name: "conversation_state",
310
- columns: [
311
- { name: "ConversationId", storage: "text", interpretation: "plain" },
312
- { name: "UserId", storage: "text", interpretation: "plain" },
313
- { name: "LastReadAt", storage: "text", interpretation: { kind: "datetime" } },
314
- { name: "LastReadMessageCount", storage: "number", interpretation: "plain" },
315
- { name: "Starred", storage: "number", interpretation: "boolean" },
316
- { name: "Archived", storage: "number", interpretation: "boolean" },
317
- { name: "Trashed", storage: "number", interpretation: "boolean" },
318
- { name: "Labels", storage: "text", interpretation: "json" },
319
- { name: "Folder", storage: "text", interpretation: "plain" }
320
- ],
321
- uniqueOn: ["ConversationId", "UserId"],
322
- ownerField: "UserId",
323
- permissions: {
324
- viewer: { read: false, create: false, update: false, delete: false },
325
- member: { read: "own", create: true, update: "own", delete: false },
326
- admin: { read: true, create: true, update: true, delete: true }
327
- }
328
- };
329
- var communitiesSchema = {
330
- name: "communities",
331
- columns: [
332
- { name: "Name", storage: "text", interpretation: "plain" },
333
- { name: "Description", storage: "text", interpretation: "plain" },
334
- { name: "CreatedBy", storage: "text", interpretation: "plain" },
335
- { name: "Type", storage: "text", interpretation: { kind: "select", options: ["community", "forum", "board", "project", "server"] } },
336
- { name: "Visibility", storage: "text", interpretation: { kind: "select", options: ["public", "private", "restricted"] } },
337
- { name: "MemberCount", storage: "number", interpretation: "plain" },
338
- { name: "Rules", storage: "text", interpretation: "json" },
339
- { name: "IconUrl", storage: "text", interpretation: "plain" },
340
- { name: "CoverUrl", storage: "text", interpretation: "plain" }
341
- ],
342
- uniqueOn: ["Name"],
343
- permissions: {
344
- viewer: { read: true, create: false, update: false, delete: false },
345
- member: { read: true, create: true, update: "own", delete: false },
346
- admin: { read: true, create: true, update: true, delete: true }
347
- },
348
- ownerField: "CreatedBy"
349
- };
350
- var membershipsSchema = {
351
- name: "memberships",
352
- columns: [
353
- { name: "CommunityId", storage: "text", interpretation: "plain" },
354
- { name: "UserId", storage: "text", interpretation: "plain" },
355
- { name: "UserName", storage: "text", interpretation: "plain" },
356
- { name: "Role", storage: "text", interpretation: { kind: "select", options: ["member", "moderator", "admin"] } },
357
- { name: "JoinedAt", storage: "text", interpretation: { kind: "datetime" } }
358
- ],
359
- uniqueOn: ["CommunityId", "UserId"],
360
- permissions: {
361
- viewer: { read: true, create: false, update: false, delete: false },
362
- member: { read: true, create: true, update: "own", delete: "own" },
363
- admin: { read: true, create: true, update: true, delete: true }
364
- },
365
- ownerField: "UserId"
366
- };
367
- var postsSchema = {
368
- name: "posts",
369
- columns: [
370
- { name: "Title", storage: "text", interpretation: "plain" },
371
- { name: "Content", storage: "text", interpretation: "plain" },
372
- { name: "AuthorId", storage: "text", interpretation: "plain" },
373
- { name: "Type", storage: "text", interpretation: { kind: "select", options: ["post", "question", "answer", "tweet", "announcement", "link"] } },
374
- { name: "CommunityId", storage: "text", interpretation: "plain" },
375
- { name: "ParentId", storage: "text", interpretation: "plain" },
376
- { name: "ConversationId", storage: "text", interpretation: "plain" },
377
- { name: "Status", storage: "text", interpretation: { kind: "select", options: ["draft", "published", "pinned", "archived", "accepted"] } },
378
- { name: "Tags", storage: "text", interpretation: "json" },
379
- { name: "LinkUrl", storage: "text", interpretation: "plain" }
380
- ],
381
- permissions: {
382
- viewer: { read: true, create: false, update: false, delete: false },
383
- member: { read: true, create: true, update: "own", delete: "own" },
384
- admin: { read: true, create: true, update: true, delete: true }
385
- },
386
- ownerField: "AuthorId"
387
- };
388
- var DIRECTORY_SCHEMAS = [
389
- conversationsSchema,
390
- conversationStateSchema,
391
- communitiesSchema,
392
- membershipsSchema,
393
- postsSchema
394
- ];
395
159
  export {
396
160
  AI_CHATS_SCHEMA,
397
161
  AI_MESSAGES_SCHEMA,
398
162
  BASE_USERS_SCHEMA,
399
163
  CHANNELS_SCHEMA,
400
- CHANNEL_INVITATIONS_SCHEMA,
401
164
  CHANNEL_MEMBERS_SCHEMA,
402
- CONVERSATION_SCHEMAS,
403
- DIRECTORY_SCHEMAS,
404
165
  MESSAGES_SCHEMA,
405
166
  REACTIONS_SCHEMA,
406
167
  READ_RECEIPTS_SCHEMA,
407
- USERS_COLUMNS,
408
- VOTING_SCHEMAS
168
+ USERS_COLUMNS
409
169
  };
410
170
  //# sourceMappingURL=schema.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server/schemas/registry.ts","../src/server/schemas/ai-chat.ts","../src/server/schemas/messaging.ts","../src/server/schemas/conversation.ts","../src/server/schemas/directory.ts"],"sourcesContent":["/**\n * Collection Schema Definitions & Validation\n *\n * All collections use typed SQL columns. No document-mode / fields-based storage.\n */\n\nimport type {\n CollectionSchema,\n ColumnDefinition,\n ColumnInterpretation,\n PermissionLevel,\n RolePermissions,\n} from '../../shared/types'\n\nexport type {\n CollectionSchema,\n ColumnDefinition,\n ColumnInterpretation,\n PermissionLevel,\n RolePermissions,\n} from '../../shared/types'\n\n// ============================================================================\n// Column Definitions\n// ============================================================================\n\nexport interface ResolvedColumn {\n id: string\n name: string\n storage: 'number' | 'text'\n interpretation: ColumnInterpretation\n expression?: string\n readonly: boolean\n userBound?: boolean\n immutable?: boolean\n required?: boolean\n default?: unknown\n timestampTrigger?: { field: string; value?: unknown }\n}\n\n// ============================================================================\n// Column Helpers\n// ============================================================================\n\nexport function collectionTableName(name: string): string {\n return `c_${name.replace(/[^a-zA-Z0-9_]/g, '_')}`\n}\n\nexport function columnId(name: string): string {\n return `col_${name.toLowerCase().replace(/[^a-z0-9]/g, '_')}`\n}\n\nexport function resolveColumn(col: ColumnDefinition): ResolvedColumn {\n const interp: ColumnInterpretation =\n typeof col.interpretation === 'string'\n ? ({ kind: col.interpretation } as ColumnInterpretation)\n : col.interpretation\n return {\n id: col.id ?? columnId(col.name),\n name: col.name,\n storage: col.storage,\n interpretation: interp,\n expression: col.expression,\n readonly: !!col.expression,\n userBound: col.userBound,\n immutable: col.immutable,\n required: col.required,\n default: col.default,\n timestampTrigger: col.timestampTrigger,\n }\n}\n\nexport function rowToData(\n row: Record<string, unknown>,\n columns: ResolvedColumn[],\n): Record<string, unknown> {\n const data: Record<string, unknown> = {}\n for (const col of columns) {\n if (row[col.id] !== undefined && row[col.id] !== null) {\n let val = row[col.id]\n if (col.interpretation.kind === 'json' && typeof val === 'string') {\n try {\n val = JSON.parse(val as string)\n } catch {\n /* keep as string */\n }\n }\n data[col.name] = val\n }\n }\n return data\n}\n\nexport function dataToColumnValues(\n data: Record<string, unknown>,\n columns: ResolvedColumn[],\n): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n for (const col of columns) {\n if (col.expression || col.readonly) continue\n // Record mutations use public schema names only. Accepting physical column\n // ids here lets a second key bypass name-based RBAC/userBound checks.\n const val = data[col.name]\n if (val !== undefined) {\n values[col.id] = coerceValue(val, col.storage, col.interpretation)\n }\n }\n return values\n}\n\nexport function coerceValue(\n value: unknown,\n storage: 'number' | 'text',\n interpretation: ColumnInterpretation,\n): unknown {\n if (value === null || value === undefined) return null\n // An empty string is a value on a text column, not an absence. Folding it\n // into NULL destroyed it on the way in — `rowToData` then dropped the key on\n // the way out — so a field written as `''` read back `undefined` and a\n // `column.default = ''` never materialized at all (it is applied at create\n // and was coerced away one line later). A `number` column has nothing to\n // store for `''`, so there it stays NULL.\n if (value === '') return storage === 'text' ? '' : null\n\n if (storage === 'text') {\n if (interpretation.kind === 'json') {\n if (typeof value === 'string') return value\n try {\n return JSON.stringify(value)\n } catch {\n return null\n }\n }\n return typeof value === 'string' ? value : String(value)\n }\n\n if (typeof value === 'number') return isFinite(value) ? value : null\n if (typeof value === 'boolean') return value ? 1 : 0\n if (typeof value !== 'string') return String(value)\n\n const s = value.trim()\n\n if (interpretation.kind === 'boolean') {\n const lower = s.toLowerCase()\n if (lower === 'true' || lower === '1' || lower === 'yes') return 1\n if (lower === 'false' || lower === '0' || lower === 'no') return 0\n return s\n }\n\n if (interpretation.kind === 'percent') {\n const m = s.match(/^(-?\\d+(?:\\.\\d+)?)\\s*%$/)\n if (m) return parseFloat(m[1]) / 100\n }\n\n if (interpretation.kind === 'currency') {\n const cleaned = s.replace(/[^0-9.-]/g, '')\n const n = parseFloat(cleaned)\n if (!isNaN(n)) return n\n return s\n }\n\n if (interpretation.kind === 'date' || interpretation.kind === 'datetime') {\n const d = new Date(s)\n if (!isNaN(d.getTime())) return epochSeconds(d)\n return s\n }\n\n const cleaned = s.replace(/[$€£¥,\\s]/g, '')\n const n = parseFloat(cleaned)\n if (!isNaN(n) && isFinite(n)) return n\n\n return s\n}\n\n/** Canonical numeric representation for date/datetime storage and triggers. */\nexport function epochSeconds(date: Date = new Date()): number {\n return Math.floor(date.getTime() / 1000)\n}\n\nexport function buildTableSelect(collectionName: string, columns: ResolvedColumn[]): string {\n const tbl = collectionTableName(collectionName)\n const parts: string[] = ['_row_id', '_created_by', '_created_at', '_updated_at']\n for (const col of columns) {\n if (col.expression) {\n parts.push(`(${col.expression}) AS \"${col.id}\"`)\n } else {\n parts.push(`\"${col.id}\"`)\n }\n }\n return `SELECT ${parts.join(', ')} FROM \"${tbl}\"`\n}\n\nexport interface User {\n id: string\n email: string\n name: string\n imageUrl?: string\n role: string\n createdAt: string\n lastSeenAt: string\n}\n\nexport interface StoredRecord {\n collection: string\n recordId: string\n data: Record<string, unknown>\n createdBy: string\n createdAt: string\n updatedAt: string\n}\n\n// ============================================================================\n// Permission Checking\n// ============================================================================\n\nexport interface PermissionContext {\n isTeamMember: (teamId: string, userId: string) => boolean\n}\n\nexport const noopPermissionContext: PermissionContext = {\n isTeamMember: () => false,\n}\n\nexport function getRolePermissions(schema: CollectionSchema, role: string): RolePermissions {\n if (schema.permissions[role]) return schema.permissions[role]\n if (schema.permissions['*']) return schema.permissions['*']\n return { read: false, create: false, update: false, delete: false }\n}\n\nexport function isOwner(\n schema: CollectionSchema,\n record: { data: Record<string, unknown>; createdBy: string },\n userId: string,\n): boolean {\n if (schema.ownerField) return record.data[schema.ownerField] === userId\n return record.createdBy === userId\n}\n\nfunction isCollaborator(\n schema: CollectionSchema,\n record: { data: Record<string, unknown> },\n userId: string,\n): boolean {\n if (!schema.collaboratorsField) return false\n const collab = record.data[schema.collaboratorsField]\n if (typeof collab === 'string') {\n try {\n return (JSON.parse(collab) as string[]).includes(userId)\n } catch {\n return false\n }\n }\n if (Array.isArray(collab)) return collab.includes(userId)\n return false\n}\n\nfunction passesVisibilityCheck(\n schema: CollectionSchema,\n record: { data: Record<string, unknown> },\n): boolean {\n if (!schema.visibilityField) return false\n if (typeof schema.visibilityField === 'string') {\n return record.data[schema.visibilityField] === 'public'\n }\n return record.data[schema.visibilityField.field] === schema.visibilityField.value\n}\n\nfunction checkPermissionLevel(\n level: PermissionLevel,\n schema: CollectionSchema,\n record: { data: Record<string, unknown>; createdBy: string; recordId?: string },\n userId: string,\n ctx: PermissionContext = noopPermissionContext,\n): boolean {\n if (level === true) return true\n if (level === false) return false\n if (level === 'own') return isOwner(schema, record, userId)\n if (level === 'unclaimed-or-own') {\n if (schema.ownerField) {\n const ownerVal = record.data[schema.ownerField]\n if (!ownerVal || ownerVal === '') return true\n }\n return isOwner(schema, record, userId)\n }\n if (level === 'collaborator') {\n return isOwner(schema, record, userId) || isCollaborator(schema, record, userId)\n }\n if (level === 'team' || level === 'access') {\n if (isOwner(schema, record, userId)) return true\n if (isCollaborator(schema, record, userId)) return true\n if (schema.teamField) {\n const teamId = record.data[schema.teamField]\n if (typeof teamId === 'string' && ctx.isTeamMember(teamId, userId)) return true\n }\n return false\n }\n if (level === 'published') {\n return isOwner(schema, record, userId) || passesVisibilityCheck(schema, record)\n }\n if (level === 'shared') {\n return (\n isOwner(schema, record, userId) ||\n isCollaborator(schema, record, userId) ||\n passesVisibilityCheck(schema, record)\n )\n }\n return false\n}\n\nexport function canRead(\n schema: CollectionSchema,\n role: string,\n record: { data: Record<string, unknown>; createdBy: string; recordId?: string },\n userId: string,\n ctx: PermissionContext = noopPermissionContext,\n): boolean {\n return checkPermissionLevel(getRolePermissions(schema, role).read, schema, record, userId, ctx)\n}\n\nexport function canCreate(schema: CollectionSchema, role: string): boolean {\n return getRolePermissions(schema, role).create === true\n}\n\nexport function canUpdate(\n schema: CollectionSchema,\n role: string,\n record: { data: Record<string, unknown>; createdBy: string; recordId?: string },\n userId: string,\n ctx: PermissionContext = noopPermissionContext,\n): boolean {\n return checkPermissionLevel(getRolePermissions(schema, role).update, schema, record, userId, ctx)\n}\n\nexport function canDelete(\n schema: CollectionSchema,\n role: string,\n record: { data: Record<string, unknown>; createdBy: string; recordId?: string },\n userId: string,\n ctx: PermissionContext = noopPermissionContext,\n): boolean {\n return checkPermissionLevel(getRolePermissions(schema, role).delete, schema, record, userId, ctx)\n}\n\n/** Check if a caller-supplied field write violates writableFields restrictions. */\nexport function checkFieldPermissions(\n schema: CollectionSchema,\n role: string,\n newData: Record<string, unknown>,\n existingData?: Record<string, unknown>,\n): string | null {\n const perms = getRolePermissions(schema, role)\n if (!perms.writableFields) return null // no restrictions\n\n const allowedFields = new Set(perms.writableFields)\n for (const key of Object.keys(newData)) {\n if (existingData && newData[key] === existingData[key]) continue\n if (!allowedFields.has(key)) {\n return `Role '${role}' cannot modify field '${key}'`\n }\n }\n return null\n}\n\n/**\n * `unclaimed-or-own` lets a client claim an empty owner field, but never lets\n * them choose another user's id. Check the post-write record so the invariant\n * covers both creation and updates while still allowing an owner to unclaim.\n */\nexport function checkUnclaimedOwnerTransition(\n schema: CollectionSchema,\n role: string,\n nextData: Record<string, unknown>,\n userId: string,\n): string | null {\n const permissions = getRolePermissions(schema, role)\n if (permissions.update !== 'unclaimed-or-own' || !schema.ownerField) return null\n\n const nextOwner = nextData[schema.ownerField]\n if (nextOwner === undefined || nextOwner === null || nextOwner === '' || nextOwner === userId) {\n return null\n }\n\n return `Role '${role}' can set '${schema.ownerField}' only to their own user id or leave it unclaimed`\n}\n\n// ============================================================================\n// System-Managed Columns (users collection)\n// ============================================================================\n\n/**\n * Columns the system *assigns* — from the identity provider at registration,\n * or from an admin action. A caller who sends one of these has chosen a value\n * and means it, so a record write that would change one is refused loudly\n * rather than silently dropped.\n */\nexport const SYSTEM_ASSIGNED_COLUMNS = new Set(['email', 'name', 'imageUrl', 'role'])\n\n/**\n * Columns the system *maintains* on its own schedule, without telling anyone.\n * `registerUser` bumps `lastSeenAt` on every WS connect and `handleUserUpdate`\n * rewrites it on every 60s presence heartbeat; neither broadcasts, so every\n * client's copy goes stale silently and there is no heal path short of a\n * refetch. Read-modify-write echo (`put(id, { ...rec.data, field })`) is the\n * SDK's own house style, so refusing these would refuse writes over a value the\n * caller never chose and cannot keep current. They are preserved silently.\n */\nexport const SYSTEM_MAINTAINED_COLUMNS = new Set(['createdAt', 'lastSeenAt'])\n\n/** Every `users` column owned by the system rather than by record writes. */\nexport const SYSTEM_MANAGED_COLUMNS = new Set([\n ...SYSTEM_ASSIGNED_COLUMNS,\n ...SYSTEM_MAINTAINED_COLUMNS,\n])\n\n/** Standard user columns. Apps spread these into their users schema. */\nexport const USERS_COLUMNS: ColumnDefinition[] = [\n { name: 'email', storage: 'text', interpretation: 'plain' },\n { name: 'name', storage: 'text', interpretation: 'plain' },\n { name: 'imageUrl', storage: 'text', interpretation: 'plain' },\n { name: 'role', storage: 'text', interpretation: 'plain' },\n { name: 'createdAt', storage: 'text', interpretation: { kind: 'datetime' } },\n { name: 'lastSeenAt', storage: 'text', interpretation: { kind: 'datetime' } },\n]\n\nexport const BASE_USERS_SCHEMA: CollectionSchema = {\n name: 'users',\n columns: [...USERS_COLUMNS],\n permissions: {\n '*': { read: 'own', create: false, update: false, delete: false },\n admin: { read: true, create: false, update: true, delete: true },\n },\n}\n\n// ============================================================================\n// Schema Registry\n// ============================================================================\n\n/**\n * Lint a CollectionSchema for declarations that look like they should\n * enforce something but don't, due to interactions between top-level\n * fields (visibilityField, ownerField) and per-role permission levels.\n *\n * The SDK has historically silently accepted schemas that imply more\n * enforcement than they actually deliver — e.g., `visibilityField` set\n * but every role's `read: true` means \"anyone can read everything\"\n * regardless of `visibility`. Warn loudly at registration so app\n * authors notice before shipping a privacy bug.\n *\n * Returns an array of warning messages. Empty = clean.\n */\nexport function lintSchema(schema: CollectionSchema): string[] {\n const warnings: string[] = []\n const VISIBILITY_AWARE: ReadonlyArray<PermissionLevel> = ['published', 'shared']\n\n // Roles that grant unconditional read despite visibilityField being declared.\n if (schema.visibilityField) {\n const offendingRoles = Object.entries(schema.permissions)\n .filter(([, perms]) => perms.read === true)\n .map(([role]) => role)\n if (offendingRoles.length > 0) {\n const usesAware = Object.values(schema.permissions).some(\n (p) => typeof p.read === 'string' && VISIBILITY_AWARE.includes(p.read as PermissionLevel),\n )\n if (!usesAware) {\n warnings.push(\n `[${schema.name}] visibilityField is declared but no role uses read: 'published' or 'shared'. ` +\n `Roles with read: true (${offendingRoles.join(', ')}) will see every row regardless of visibility. ` +\n `Change those to read: 'published' (owner OR public) or 'shared' (owner OR collaborator OR public) ` +\n `to actually enforce the filter, or remove visibilityField if you don't intend to gate reads by it.`,\n )\n }\n }\n }\n\n // An ordinary client role that can create rows must not be able to claim\n // someone else's ownership. Server-written collections legitimately carry\n // a recipient/subject id instead: their non-privileged roles all have\n // create:false, while owner/admin writes are already fully trusted.\n if (schema.ownerField) {\n const col = schema.columns.find((c) => c.name === schema.ownerField)\n const spoofableRoles = Object.entries(schema.permissions)\n .filter(\n ([role, permissions]) =>\n role !== 'owner' &&\n role !== 'admin' &&\n permissions.create === true &&\n permissions.update !== 'unclaimed-or-own',\n )\n .map(([role]) => role)\n if (col && !col.userBound && spoofableRoles.length > 0) {\n warnings.push(\n `[${schema.name}] ownerField is '${schema.ownerField}' but that column is not marked userBound: true. ` +\n `Client role(s) ${spoofableRoles.join(', ')} can create a row with someone else's id in this field, bypassing 'own' permission checks. ` +\n `Add userBound: true (and ideally immutable: true) to the column.`,\n )\n }\n }\n\n // userBound columns that aren't text storage — userBound sets the\n // field to the caller's userId (a string), so non-text storage is a\n // schema mistake that will surface as a coercion error at write time.\n for (const col of schema.columns) {\n if (col.userBound && col.storage !== 'text') {\n warnings.push(\n `[${schema.name}] column '${col.name}' is userBound but storage is '${col.storage}'. ` +\n `userBound stamps the user id (a string); use storage: 'text'.`,\n )\n }\n }\n\n return warnings\n}\n\n/**\n * Lint a whole schema set: every per-schema warning, plus the rules that\n * need to see the set. The `'team'` permission level resolves membership from\n * a `team_members` collection (`teamId`, `userId`, `status`); without one it\n * denies everything, silently — a mistake that presents as a sync bug, not\n * as a schema error, so name it here.\n */\nexport function lintSchemas(schemas: CollectionSchema[]): string[] {\n return [...schemas.flatMap((schema) => lintSchema(schema)), ...lintSchemaSet(schemas)]\n}\n\n/** The rules that need to see the whole schema set (per-schema rules live in\n * `lintSchema`; `lintSchemas` composes both). */\nexport function lintSchemaSet(schemas: CollectionSchema[]): string[] {\n const warnings: string[] = []\n if (!schemas.some((schema) => schema.name === 'team_members')) {\n for (const schema of schemas) {\n const teamRoles = Object.entries(schema.permissions)\n .filter(([, p]) => [p.read, p.update, p.delete].includes('team'))\n .map(([role]) => role)\n if (teamRoles.length > 0) {\n warnings.push(\n `[${schema.name}] role(s) ${teamRoles.join(', ')} use the 'team' permission level, but no ` +\n `'team_members' collection is registered. 'team' resolves membership from team_members ` +\n `(teamId, userId, status), so without it those roles are denied everything — silently. ` +\n `Register a team_members schema or use another level.`,\n )\n }\n }\n }\n return warnings\n}\n\nexport class SchemaRegistry {\n private trusted: Map<string, CollectionSchema> = new Map()\n\n constructor(schemas: CollectionSchema[] = []) {\n // registerTrusted runs the per-schema lint; only the set-level rules are\n // this loop's to add.\n for (const w of lintSchemaSet(schemas)) console.warn(`[schema-lint] ${w}`)\n for (const schema of schemas) this.registerTrusted(schema)\n }\n\n registerTrusted(schema: CollectionSchema): void {\n for (const w of lintSchema(schema)) {\n console.warn(`[schema-lint] ${w}`)\n }\n this.trusted.set(schema.name, schema)\n }\n\n get(name: string): CollectionSchema | undefined {\n return this.trusted.get(name)\n }\n\n has(name: string): boolean {\n return this.trusted.has(name)\n }\n\n all(): CollectionSchema[] {\n return Array.from(this.trusted.values())\n }\n\n names(): string[] {\n return Array.from(this.trusted.keys())\n }\n}\n\n// ============================================================================\n// Permission Analysis\n// ============================================================================\n\nexport type PermissionSource = 'explicit' | 'wildcard' | 'default-deny'\n\nexport interface ResolvedPermission {\n level: PermissionLevel | boolean\n source: PermissionSource\n}\n\nexport interface CollectionPermissionSummary {\n collection: string\n ownerField?: string\n collaboratorsField?: string\n teamField?: string\n columns: ColumnDefinition[]\n permissions: Record<\n string,\n {\n read: ResolvedPermission\n create: ResolvedPermission\n update: ResolvedPermission\n delete: ResolvedPermission\n writableFields?: string[]\n }\n >\n}\n\nexport interface PermissionAnalysis {\n roles: string[]\n collections: CollectionPermissionSummary[]\n}\n\nexport function analyzePermissions(schemas: CollectionSchema[]): PermissionAnalysis {\n const roleSet = new Set<string>()\n for (const schema of schemas) {\n for (const role of Object.keys(schema.permissions)) roleSet.add(role)\n }\n roleSet.delete('*')\n const roles = Array.from(roleSet)\n\n const collections: CollectionPermissionSummary[] = schemas.map((schema) => {\n const permissions: CollectionPermissionSummary['permissions'] = {}\n for (const role of roles) {\n let source: PermissionSource\n let perms: RolePermissions\n if (schema.permissions[role]) {\n source = 'explicit'\n perms = schema.permissions[role]\n } else if (schema.permissions['*']) {\n source = 'wildcard'\n perms = schema.permissions['*']\n } else {\n source = 'default-deny'\n perms = { read: false, create: false, update: false, delete: false }\n }\n permissions[role] = {\n read: { level: perms.read, source },\n create: { level: perms.create, source },\n update: { level: perms.update, source },\n delete: { level: perms.delete, source },\n writableFields: perms.writableFields,\n }\n }\n return {\n collection: schema.name,\n ownerField: schema.ownerField,\n collaboratorsField: schema.collaboratorsField,\n teamField: schema.teamField,\n columns: schema.columns,\n permissions,\n }\n })\n\n return { roles, collections }\n}\n","/**\n * AI Chat Schemas\n *\n * Pre-built collection schemas for DO-backed AI chat history.\n * The worker is the only writer; the client reads via `useQuery`.\n */\n\nimport type { CollectionSchema } from './registry'\n\n// member.create is false on both collections: writes flow exclusively through\n// the worker's /api/ai/* routes, which use X-App-Action to bypass RBAC.\n// Allowing direct WS creates would let a user PUT fake `role: 'assistant'`\n// rows into their own chats and have the next turn's loadMessages feed them\n// to the LLM as if they were real assistant outputs.\nexport const AI_CHATS_SCHEMA: CollectionSchema = {\n name: 'ai-chats',\n columns: [\n { name: 'userId', storage: 'text', interpretation: 'plain', userBound: true, immutable: true, required: true },\n { name: 'title', storage: 'text', interpretation: 'plain' },\n { name: 'model', storage: 'text', interpretation: 'plain' },\n { name: 'compactedSummary', storage: 'text', interpretation: 'plain' },\n { name: 'compactedThroughId', storage: 'text', interpretation: 'plain' },\n ],\n ownerField: 'userId',\n permissions: {\n admin: { read: true, create: true, update: true, delete: true },\n member: { read: 'own', create: false, update: 'own', delete: 'own' },\n viewer: { read: 'own', create: false, update: false, delete: false },\n },\n}\n\nexport const AI_MESSAGES_SCHEMA: CollectionSchema = {\n name: 'ai-messages',\n columns: [\n { name: 'chatId', storage: 'text', interpretation: 'plain', immutable: true, required: true },\n { name: 'userId', storage: 'text', interpretation: 'plain', userBound: true, immutable: true, required: true },\n { name: 'role', storage: 'text', interpretation: 'plain', required: true },\n { name: 'content', storage: 'text', interpretation: 'plain' },\n { name: 'parts', storage: 'text', interpretation: { kind: 'json' } },\n ],\n ownerField: 'userId',\n permissions: {\n admin: { read: true, create: true, update: true, delete: true },\n member: { read: 'own', create: false, update: 'own', delete: 'own' },\n viewer: { read: 'own', create: false, update: false, delete: false },\n },\n}\n","/**\n * Messaging Schemas\n *\n * Pre-built collection schemas for messaging functionality.\n * Any app can import these to add channels, messages, reactions, etc.\n *\n * @example\n * ```typescript\n * import { CHANNELS_SCHEMA, MESSAGES_SCHEMA, REACTIONS_SCHEMA } from 'deepspace/worker'\n * export const schemas = [usersSchema, CHANNELS_SCHEMA, MESSAGES_SCHEMA, REACTIONS_SCHEMA]\n * ```\n */\n\nimport type { CollectionSchema } from './registry'\n\nexport const CHANNELS_SCHEMA: CollectionSchema = {\n name: 'channels',\n columns: [\n { name: 'name', storage: 'text', interpretation: 'plain' },\n { name: 'description', storage: 'text', interpretation: 'plain' },\n {\n name: 'type',\n storage: 'text',\n interpretation: { kind: 'select', options: ['public', 'private', 'dm'] },\n },\n {\n name: 'createdBy',\n storage: 'text',\n interpretation: 'plain',\n userBound: true,\n immutable: true,\n },\n { name: 'archived', storage: 'number', interpretation: { kind: 'boolean' } },\n ],\n ownerField: 'createdBy',\n permissions: {\n admin: { read: true, create: true, update: true, delete: true },\n member: { read: true, create: true, update: 'own', delete: false },\n viewer: { read: true, create: false, update: false, delete: false },\n },\n}\n\nexport const MESSAGES_SCHEMA: CollectionSchema = {\n name: 'messages',\n columns: [\n { name: 'channelId', storage: 'text', interpretation: 'plain' },\n { name: 'content', storage: 'text', interpretation: 'plain' },\n {\n name: 'authorId',\n storage: 'text',\n interpretation: 'plain',\n userBound: true,\n immutable: true,\n },\n { name: 'parentMessageId', storage: 'text', interpretation: 'plain' },\n { name: 'edited', storage: 'number', interpretation: { kind: 'boolean' } },\n { name: 'editedAt', storage: 'text', interpretation: { kind: 'datetime' } },\n // Soft-delete flag. Slack-style messaging keeps the row + parent IDs\n // intact (so threads remain coherent) and renders a tombstone instead.\n // Hard-delete via remove() still works if an app wants it.\n { name: 'deleted', storage: 'number', interpretation: { kind: 'boolean' } },\n ],\n ownerField: 'authorId',\n permissions: {\n admin: { read: true, create: true, update: true, delete: true },\n member: { read: true, create: true, update: 'own', delete: 'own' },\n viewer: { read: true, create: false, update: false, delete: false },\n },\n}\n\nexport const REACTIONS_SCHEMA: CollectionSchema = {\n name: 'reactions',\n columns: [\n { name: 'messageId', storage: 'text', interpretation: 'plain' },\n { name: 'channelId', storage: 'text', interpretation: 'plain' },\n { name: 'emoji', storage: 'text', interpretation: 'plain' },\n { name: 'userId', storage: 'text', interpretation: 'plain', userBound: true, immutable: true },\n ],\n ownerField: 'userId',\n uniqueOn: ['messageId', 'emoji', 'userId'],\n permissions: {\n admin: { read: true, create: true, update: false, delete: true },\n member: { read: true, create: true, update: false, delete: 'own' },\n viewer: { read: true, create: false, update: false, delete: false },\n },\n}\n\nexport const CHANNEL_MEMBERS_SCHEMA: CollectionSchema = {\n name: 'channel-members',\n columns: [\n { name: 'channelId', storage: 'text', interpretation: 'plain' },\n { name: 'userId', storage: 'text', interpretation: 'plain', userBound: true, immutable: true },\n { name: 'joinedAt', storage: 'text', interpretation: { kind: 'datetime' } },\n ],\n ownerField: 'userId',\n uniqueOn: ['channelId', 'userId'],\n permissions: {\n admin: { read: true, create: true, update: false, delete: true },\n member: { read: true, create: true, update: false, delete: 'own' },\n viewer: { read: true, create: false, update: false, delete: false },\n },\n}\n\nexport const CHANNEL_INVITATIONS_SCHEMA: CollectionSchema = {\n name: 'channel-invitations',\n columns: [\n {\n name: 'channelId',\n storage: 'text',\n interpretation: 'plain',\n required: true,\n immutable: true,\n },\n {\n name: 'invitedUserId',\n storage: 'text',\n interpretation: 'plain',\n required: true,\n immutable: true,\n },\n {\n name: 'invitedBy',\n storage: 'text',\n interpretation: 'plain',\n userBound: true,\n immutable: true,\n },\n {\n name: 'status',\n storage: 'text',\n interpretation: { kind: 'select', options: ['pending', 'accepted', 'declined'] },\n required: true,\n },\n ],\n uniqueOn: ['channelId', 'invitedUserId'],\n permissions: {\n admin: { read: true, create: true, update: true, delete: true },\n member: { read: true, create: true, update: true, delete: false },\n viewer: { read: true, create: false, update: false, delete: false },\n },\n}\n\nexport const READ_RECEIPTS_SCHEMA: CollectionSchema = {\n name: 'read-receipts',\n columns: [\n { name: 'channelId', storage: 'text', interpretation: 'plain' },\n { name: 'userId', storage: 'text', interpretation: 'plain', userBound: true, immutable: true },\n { name: 'lastReadAt', storage: 'text', interpretation: { kind: 'datetime' } },\n ],\n ownerField: 'userId',\n uniqueOn: ['channelId', 'userId'],\n permissions: {\n admin: { read: true, create: true, update: true, delete: true },\n member: { read: true, create: true, update: 'own', delete: 'own' },\n viewer: { read: false, create: false, update: false, delete: false },\n },\n}\n","/**\n * Shared conversation schemas for RecordRoom-based conversations.\n *\n * These define the collections inside a per-conversation RecordRoom DO.\n * Used by apps that have messaging/conversation features (slack-clone,\n * helpdesk, mail, reddit-clone, etc.).\n *\n * Each conversation gets its own RecordRoom DO keyed by `conv:{id}`.\n */\n\nimport type { CollectionSchema } from './registry'\n\n// Re-export data interfaces from types (single source of truth)\nexport type {\n ConvMessageData,\n ConvReactionData,\n ConvMemberData,\n ConvReadCursorData,\n ConvVoteData,\n} from '../../shared/types'\n\n// ============================================================================\n// Schemas\n// ============================================================================\n\nexport const CONVERSATION_SCHEMAS: CollectionSchema[] = [\n {\n name: 'conv_messages',\n columns: [\n { name: 'Content', storage: 'text', interpretation: 'plain' },\n { name: 'AuthorId', storage: 'text', interpretation: 'plain' },\n { name: 'ParentId', storage: 'text', interpretation: 'plain' },\n { name: 'Edited', storage: 'number', interpretation: 'boolean' },\n { name: 'MessageType', storage: 'text', interpretation: 'plain' },\n { name: 'Metadata', storage: 'text', interpretation: 'json' },\n ],\n permissions: {\n viewer: { read: true, create: false, update: false, delete: false },\n member: { read: true, create: true, update: 'own', delete: 'own' },\n admin: { read: true, create: true, update: true, delete: true },\n },\n ownerField: 'AuthorId',\n },\n {\n name: 'conv_reactions',\n columns: [\n { name: 'MessageId', storage: 'text', interpretation: 'plain' },\n { name: 'Emoji', storage: 'text', interpretation: 'plain' },\n { name: 'UserId', storage: 'text', interpretation: 'plain' },\n ],\n uniqueOn: ['MessageId', 'Emoji', 'UserId'],\n permissions: {\n viewer: { read: true, create: false, update: false, delete: false },\n member: { read: true, create: true, update: false, delete: 'own' },\n admin: { read: true, create: true, update: false, delete: true },\n },\n ownerField: 'UserId',\n },\n {\n name: 'conv_members',\n columns: [\n { name: 'UserId', storage: 'text', interpretation: 'plain' },\n { name: 'UserName', storage: 'text', interpretation: 'plain' },\n { name: 'Role', storage: 'text', interpretation: 'plain' },\n ],\n uniqueOn: ['UserId'],\n permissions: {\n viewer: { read: true, create: false, update: false, delete: false },\n member: { read: true, create: true, update: 'own', delete: false },\n admin: { read: true, create: true, update: true, delete: true },\n },\n ownerField: 'UserId',\n },\n {\n name: 'conv_read_cursors',\n columns: [\n { name: 'UserId', storage: 'text', interpretation: 'plain' },\n { name: 'LastReadAt', storage: 'text', interpretation: 'plain' },\n ],\n uniqueOn: ['UserId'],\n permissions: {\n viewer: { read: true, create: false, update: false, delete: false },\n member: { read: true, create: true, update: 'own', delete: false },\n admin: { read: true, create: true, update: true, delete: true },\n },\n ownerField: 'UserId',\n },\n]\n\n/**\n * Voting schemas for Reddit-style apps.\n * Add these alongside CONVERSATION_SCHEMAS for apps that need voting.\n */\nexport const VOTING_SCHEMAS: CollectionSchema[] = [\n {\n name: 'conv_votes',\n columns: [\n { name: 'TargetId', storage: 'text', interpretation: 'plain' },\n { name: 'UserId', storage: 'text', interpretation: 'plain' },\n { name: 'Direction', storage: 'number', interpretation: 'plain' },\n ],\n uniqueOn: ['TargetId', 'UserId'],\n permissions: {\n viewer: { read: true, create: false, update: false, delete: false },\n member: { read: true, create: true, update: 'own', delete: 'own' },\n admin: { read: true, create: true, update: true, delete: true },\n },\n ownerField: 'UserId',\n },\n]\n","/**\n * Directory DO Schemas\n *\n * Purpose-built collections for the `dir:{appName}` global DO type.\n * This is the cross-app-visible directory layer — any app can subscribe\n * to another app's directory in real-time via SHARED_CONNECTIONS.\n *\n * Five collections covering all standard communication/social patterns:\n * - conversations: channels, DMs, email threads, support chats\n * - conversation_state: per-user metadata (read cursor, stars, labels, folders)\n * - communities: groups, forums, boards, projects\n * - memberships: user membership in communities\n * - posts: feed items, tweets, Q&A questions, announcements\n *\n * Domain-specific data (tickets, CRM, procurement) stays in app DOs.\n * Message content stays in conv:{id} DOs (CONVERSATION_SCHEMAS).\n */\n\nimport type { CollectionSchema } from './registry'\n\n// Re-export data interfaces from types (single source of truth)\nexport type {\n DirectoryConversationData,\n ConversationStateData,\n DirectoryCommunityData,\n DirectoryMembershipData,\n DirectoryPostData,\n} from '../../shared/types'\n\n// ============================================================================\n// Schemas\n// ============================================================================\n\nconst conversationsSchema: CollectionSchema = {\n name: 'conversations',\n columns: [\n { name: 'Name', storage: 'text', interpretation: 'plain' },\n { name: 'Description', storage: 'text', interpretation: 'plain' },\n { name: 'Type', storage: 'text', interpretation: { kind: 'select', options: ['public', 'private', 'dm', 'group', 'email', 'outbound-email', 'support', 'announcement'] } },\n { name: 'Visibility', storage: 'text', interpretation: { kind: 'select', options: ['public', 'private', 'restricted'] } },\n { name: 'CreatedBy', storage: 'text', interpretation: 'plain' },\n { name: 'ParticipantHash', storage: 'text', interpretation: 'plain' },\n { name: 'ParticipantIds', storage: 'text', interpretation: 'json' },\n { name: 'Status', storage: 'text', interpretation: { kind: 'select', options: ['active', 'archived', 'resolved', 'closed'] } },\n { name: 'AssigneeId', storage: 'text', interpretation: 'plain' },\n { name: 'LinkedRef', storage: 'text', interpretation: 'json' },\n { name: 'LastMessageAt', storage: 'text', interpretation: { kind: 'datetime' } },\n { name: 'LastMessagePreview', storage: 'text', interpretation: 'plain' },\n { name: 'LastMessageAuthor', storage: 'text', interpretation: 'plain' },\n { name: 'MessageCount', storage: 'number', interpretation: 'plain' },\n ],\n collaboratorsField: 'ParticipantIds',\n visibilityField: 'Visibility',\n permissions: {\n viewer: { read: 'shared', create: false, update: false, delete: false },\n member: { read: 'shared', create: true, update: true, delete: 'own' },\n admin: { read: true, create: true, update: true, delete: true },\n },\n ownerField: 'CreatedBy',\n}\n\nconst conversationStateSchema: CollectionSchema = {\n name: 'conversation_state',\n columns: [\n { name: 'ConversationId', storage: 'text', interpretation: 'plain' },\n { name: 'UserId', storage: 'text', interpretation: 'plain' },\n { name: 'LastReadAt', storage: 'text', interpretation: { kind: 'datetime' } },\n { name: 'LastReadMessageCount', storage: 'number', interpretation: 'plain' },\n { name: 'Starred', storage: 'number', interpretation: 'boolean' },\n { name: 'Archived', storage: 'number', interpretation: 'boolean' },\n { name: 'Trashed', storage: 'number', interpretation: 'boolean' },\n { name: 'Labels', storage: 'text', interpretation: 'json' },\n { name: 'Folder', storage: 'text', interpretation: 'plain' },\n ],\n uniqueOn: ['ConversationId', 'UserId'],\n ownerField: 'UserId',\n permissions: {\n viewer: { read: false, create: false, update: false, delete: false },\n member: { read: 'own', create: true, update: 'own', delete: false },\n admin: { read: true, create: true, update: true, delete: true },\n },\n}\n\nconst communitiesSchema: CollectionSchema = {\n name: 'communities',\n columns: [\n { name: 'Name', storage: 'text', interpretation: 'plain' },\n { name: 'Description', storage: 'text', interpretation: 'plain' },\n { name: 'CreatedBy', storage: 'text', interpretation: 'plain' },\n { name: 'Type', storage: 'text', interpretation: { kind: 'select', options: ['community', 'forum', 'board', 'project', 'server'] } },\n { name: 'Visibility', storage: 'text', interpretation: { kind: 'select', options: ['public', 'private', 'restricted'] } },\n { name: 'MemberCount', storage: 'number', interpretation: 'plain' },\n { name: 'Rules', storage: 'text', interpretation: 'json' },\n { name: 'IconUrl', storage: 'text', interpretation: 'plain' },\n { name: 'CoverUrl', storage: 'text', interpretation: 'plain' },\n ],\n uniqueOn: ['Name'],\n permissions: {\n viewer: { read: true, create: false, update: false, delete: false },\n member: { read: true, create: true, update: 'own', delete: false },\n admin: { read: true, create: true, update: true, delete: true },\n },\n ownerField: 'CreatedBy',\n}\n\nconst membershipsSchema: CollectionSchema = {\n name: 'memberships',\n columns: [\n { name: 'CommunityId', storage: 'text', interpretation: 'plain' },\n { name: 'UserId', storage: 'text', interpretation: 'plain' },\n { name: 'UserName', storage: 'text', interpretation: 'plain' },\n { name: 'Role', storage: 'text', interpretation: { kind: 'select', options: ['member', 'moderator', 'admin'] } },\n { name: 'JoinedAt', storage: 'text', interpretation: { kind: 'datetime' } },\n ],\n uniqueOn: ['CommunityId', 'UserId'],\n permissions: {\n viewer: { read: true, create: false, update: false, delete: false },\n member: { read: true, create: true, update: 'own', delete: 'own' },\n admin: { read: true, create: true, update: true, delete: true },\n },\n ownerField: 'UserId',\n}\n\nconst postsSchema: CollectionSchema = {\n name: 'posts',\n columns: [\n { name: 'Title', storage: 'text', interpretation: 'plain' },\n { name: 'Content', storage: 'text', interpretation: 'plain' },\n { name: 'AuthorId', storage: 'text', interpretation: 'plain' },\n { name: 'Type', storage: 'text', interpretation: { kind: 'select', options: ['post', 'question', 'answer', 'tweet', 'announcement', 'link'] } },\n { name: 'CommunityId', storage: 'text', interpretation: 'plain' },\n { name: 'ParentId', storage: 'text', interpretation: 'plain' },\n { name: 'ConversationId', storage: 'text', interpretation: 'plain' },\n { name: 'Status', storage: 'text', interpretation: { kind: 'select', options: ['draft', 'published', 'pinned', 'archived', 'accepted'] } },\n { name: 'Tags', storage: 'text', interpretation: 'json' },\n { name: 'LinkUrl', storage: 'text', interpretation: 'plain' },\n ],\n permissions: {\n viewer: { read: true, create: false, update: false, delete: false },\n member: { read: true, create: true, update: 'own', delete: 'own' },\n admin: { read: true, create: true, update: true, delete: true },\n },\n ownerField: 'AuthorId',\n}\n\n// ============================================================================\n// Exports\n// ============================================================================\n\n/** All directory schemas for the `dir:{appName}` global DO type. */\nexport const DIRECTORY_SCHEMAS: CollectionSchema[] = [\n conversationsSchema,\n conversationStateSchema,\n communitiesSchema,\n membershipsSchema,\n postsSchema,\n]\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/server/schemas/registry.ts","../src/server/schemas/ai-chat.ts","../src/server/schemas/messaging.ts"],"sourcesContent":["/**\n * Collection Schema Definitions & Validation\n *\n * All collections use typed SQL columns. No document-mode / fields-based storage.\n */\n\nimport type {\n CollectionSchema,\n ColumnDefinition,\n ColumnInterpretation,\n PermissionLevel,\n RolePermissions,\n} from '../../shared/types'\n\nexport type {\n CollectionSchema,\n ColumnDefinition,\n ColumnInterpretation,\n PermissionLevel,\n RolePermissions,\n} from '../../shared/types'\n\n// ============================================================================\n// Column Definitions\n// ============================================================================\n\nexport interface ResolvedColumn {\n id: string\n name: string\n storage: 'number' | 'text'\n interpretation: ColumnInterpretation\n expression?: string\n readonly: boolean\n userBound?: boolean\n immutable?: boolean\n required?: boolean\n default?: unknown\n timestampTrigger?: { field: string; value?: unknown }\n}\n\n// ============================================================================\n// Column Helpers\n// ============================================================================\n\nexport function collectionTableName(name: string): string {\n return `c_${name.replace(/[^a-zA-Z0-9_]/g, '_')}`\n}\n\nexport function columnId(name: string): string {\n return `col_${name.toLowerCase().replace(/[^a-z0-9]/g, '_')}`\n}\n\nexport function resolveColumn(col: ColumnDefinition): ResolvedColumn {\n const interp: ColumnInterpretation =\n typeof col.interpretation === 'string'\n ? ({ kind: col.interpretation } as ColumnInterpretation)\n : col.interpretation\n return {\n id: col.id ?? columnId(col.name),\n name: col.name,\n storage: col.storage,\n interpretation: interp,\n expression: col.expression,\n readonly: !!col.expression,\n userBound: col.userBound,\n immutable: col.immutable,\n required: col.required,\n default: col.default,\n timestampTrigger: col.timestampTrigger,\n }\n}\n\nexport function rowToData(\n row: Record<string, unknown>,\n columns: ResolvedColumn[],\n): Record<string, unknown> {\n const data: Record<string, unknown> = {}\n for (const col of columns) {\n if (row[col.id] !== undefined && row[col.id] !== null) {\n let val = row[col.id]\n if (col.interpretation.kind === 'json' && typeof val === 'string') {\n try {\n val = JSON.parse(val as string)\n } catch {\n /* keep as string */\n }\n }\n data[col.name] = val\n }\n }\n return data\n}\n\nexport function dataToColumnValues(\n data: Record<string, unknown>,\n columns: ResolvedColumn[],\n): Record<string, unknown> {\n const values: Record<string, unknown> = {}\n for (const col of columns) {\n if (col.expression || col.readonly) continue\n // Record mutations use public schema names only. Accepting physical column\n // ids here lets a second key bypass name-based RBAC/userBound checks.\n const val = data[col.name]\n if (val !== undefined) {\n values[col.id] = coerceValue(val, col.storage, col.interpretation)\n }\n }\n return values\n}\n\nexport function coerceValue(\n value: unknown,\n storage: 'number' | 'text',\n interpretation: ColumnInterpretation,\n): unknown {\n if (value === null || value === undefined) return null\n // An empty string is a value on a text column, not an absence. Folding it\n // into NULL destroyed it on the way in — `rowToData` then dropped the key on\n // the way out — so a field written as `''` read back `undefined` and a\n // `column.default = ''` never materialized at all (it is applied at create\n // and was coerced away one line later). A `number` column has nothing to\n // store for `''`, so there it stays NULL.\n if (value === '') return storage === 'text' ? '' : null\n\n if (storage === 'text') {\n if (interpretation.kind === 'json') {\n if (typeof value === 'string') return value\n try {\n return JSON.stringify(value)\n } catch {\n return null\n }\n }\n return typeof value === 'string' ? value : String(value)\n }\n\n if (typeof value === 'number') return isFinite(value) ? value : null\n if (typeof value === 'boolean') return value ? 1 : 0\n if (typeof value !== 'string') return String(value)\n\n const s = value.trim()\n\n if (interpretation.kind === 'boolean') {\n const lower = s.toLowerCase()\n if (lower === 'true' || lower === '1' || lower === 'yes') return 1\n if (lower === 'false' || lower === '0' || lower === 'no') return 0\n return s\n }\n\n if (interpretation.kind === 'percent') {\n const m = s.match(/^(-?\\d+(?:\\.\\d+)?)\\s*%$/)\n if (m) return parseFloat(m[1]) / 100\n }\n\n if (interpretation.kind === 'currency') {\n const cleaned = s.replace(/[^0-9.-]/g, '')\n const n = parseFloat(cleaned)\n if (!isNaN(n)) return n\n return s\n }\n\n if (interpretation.kind === 'date' || interpretation.kind === 'datetime') {\n const d = new Date(s)\n if (!isNaN(d.getTime())) return epochSeconds(d)\n return s\n }\n\n const cleaned = s.replace(/[$€£¥,\\s]/g, '')\n const n = parseFloat(cleaned)\n if (!isNaN(n) && isFinite(n)) return n\n\n return s\n}\n\n/** Canonical numeric representation for date/datetime storage and triggers. */\nexport function epochSeconds(date: Date = new Date()): number {\n return Math.floor(date.getTime() / 1000)\n}\n\nexport function buildTableSelect(collectionName: string, columns: ResolvedColumn[]): string {\n const tbl = collectionTableName(collectionName)\n const parts: string[] = ['_row_id', '_created_by', '_created_at', '_updated_at']\n for (const col of columns) {\n if (col.expression) {\n parts.push(`(${col.expression}) AS \"${col.id}\"`)\n } else {\n parts.push(`\"${col.id}\"`)\n }\n }\n return `SELECT ${parts.join(', ')} FROM \"${tbl}\"`\n}\n\nexport interface User {\n id: string\n email: string\n name: string\n imageUrl?: string\n role: string\n createdAt: string\n lastSeenAt: string\n}\n\nexport interface StoredRecord {\n collection: string\n recordId: string\n data: Record<string, unknown>\n createdBy: string\n createdAt: string\n updatedAt: string\n}\n\n// ============================================================================\n// Permission Checking\n// ============================================================================\n\nexport interface PermissionContext {\n isTeamMember: (teamId: string, userId: string) => boolean\n}\n\nexport const noopPermissionContext: PermissionContext = {\n isTeamMember: () => false,\n}\n\nexport function getRolePermissions(schema: CollectionSchema, role: string): RolePermissions {\n if (schema.permissions[role]) return schema.permissions[role]\n if (schema.permissions['*']) return schema.permissions['*']\n return { read: false, create: false, update: false, delete: false }\n}\n\nexport function isOwner(\n schema: CollectionSchema,\n record: { data: Record<string, unknown>; createdBy: string },\n userId: string,\n): boolean {\n if (schema.ownerField) return record.data[schema.ownerField] === userId\n return record.createdBy === userId\n}\n\nfunction isCollaborator(\n schema: CollectionSchema,\n record: { data: Record<string, unknown> },\n userId: string,\n): boolean {\n if (!schema.collaboratorsField) return false\n const collab = record.data[schema.collaboratorsField]\n if (typeof collab === 'string') {\n try {\n return (JSON.parse(collab) as string[]).includes(userId)\n } catch {\n return false\n }\n }\n if (Array.isArray(collab)) return collab.includes(userId)\n return false\n}\n\nfunction passesVisibilityCheck(\n schema: CollectionSchema,\n record: { data: Record<string, unknown> },\n): boolean {\n if (!schema.visibilityField) return false\n if (typeof schema.visibilityField === 'string') {\n return record.data[schema.visibilityField] === 'public'\n }\n return record.data[schema.visibilityField.field] === schema.visibilityField.value\n}\n\nfunction checkPermissionLevel(\n level: PermissionLevel,\n schema: CollectionSchema,\n record: { data: Record<string, unknown>; createdBy: string; recordId?: string },\n userId: string,\n ctx: PermissionContext = noopPermissionContext,\n): boolean {\n if (level === true) return true\n if (level === false) return false\n if (level === 'own') return isOwner(schema, record, userId)\n if (level === 'unclaimed-or-own') {\n if (schema.ownerField) {\n const ownerVal = record.data[schema.ownerField]\n if (!ownerVal || ownerVal === '') return true\n }\n return isOwner(schema, record, userId)\n }\n if (level === 'collaborator') {\n return isOwner(schema, record, userId) || isCollaborator(schema, record, userId)\n }\n if (level === 'team' || level === 'access') {\n if (isOwner(schema, record, userId)) return true\n if (isCollaborator(schema, record, userId)) return true\n if (schema.teamField) {\n const teamId = record.data[schema.teamField]\n if (typeof teamId === 'string' && ctx.isTeamMember(teamId, userId)) return true\n }\n return false\n }\n if (level === 'published') {\n return isOwner(schema, record, userId) || passesVisibilityCheck(schema, record)\n }\n if (level === 'shared') {\n return (\n isOwner(schema, record, userId) ||\n isCollaborator(schema, record, userId) ||\n passesVisibilityCheck(schema, record)\n )\n }\n return false\n}\n\nexport function canRead(\n schema: CollectionSchema,\n role: string,\n record: { data: Record<string, unknown>; createdBy: string; recordId?: string },\n userId: string,\n ctx: PermissionContext = noopPermissionContext,\n): boolean {\n return checkPermissionLevel(getRolePermissions(schema, role).read, schema, record, userId, ctx)\n}\n\nexport function canCreate(schema: CollectionSchema, role: string): boolean {\n return getRolePermissions(schema, role).create === true\n}\n\nexport function canUpdate(\n schema: CollectionSchema,\n role: string,\n record: { data: Record<string, unknown>; createdBy: string; recordId?: string },\n userId: string,\n ctx: PermissionContext = noopPermissionContext,\n): boolean {\n return checkPermissionLevel(getRolePermissions(schema, role).update, schema, record, userId, ctx)\n}\n\nexport function canDelete(\n schema: CollectionSchema,\n role: string,\n record: { data: Record<string, unknown>; createdBy: string; recordId?: string },\n userId: string,\n ctx: PermissionContext = noopPermissionContext,\n): boolean {\n return checkPermissionLevel(getRolePermissions(schema, role).delete, schema, record, userId, ctx)\n}\n\n/** Check if a caller-supplied field write violates writableFields restrictions. */\nexport function checkFieldPermissions(\n schema: CollectionSchema,\n role: string,\n newData: Record<string, unknown>,\n existingData?: Record<string, unknown>,\n): string | null {\n const perms = getRolePermissions(schema, role)\n if (!perms.writableFields) return null // no restrictions\n\n const allowedFields = new Set(perms.writableFields)\n for (const key of Object.keys(newData)) {\n if (existingData && newData[key] === existingData[key]) continue\n if (!allowedFields.has(key)) {\n return `Role '${role}' cannot modify field '${key}'`\n }\n }\n return null\n}\n\n/**\n * `unclaimed-or-own` lets a client claim an empty owner field, but never lets\n * them choose another user's id. Check the post-write record so the invariant\n * covers both creation and updates while still allowing an owner to unclaim.\n */\nexport function checkUnclaimedOwnerTransition(\n schema: CollectionSchema,\n role: string,\n nextData: Record<string, unknown>,\n userId: string,\n): string | null {\n const permissions = getRolePermissions(schema, role)\n if (permissions.update !== 'unclaimed-or-own' || !schema.ownerField) return null\n\n const nextOwner = nextData[schema.ownerField]\n if (nextOwner === undefined || nextOwner === null || nextOwner === '' || nextOwner === userId) {\n return null\n }\n\n return `Role '${role}' can set '${schema.ownerField}' only to their own user id or leave it unclaimed`\n}\n\n// ============================================================================\n// System-Managed Columns (users collection)\n// ============================================================================\n\n/**\n * Columns the system *assigns* — from the identity provider at registration,\n * or from an admin action. A caller who sends one of these has chosen a value\n * and means it, so a record write that would change one is refused loudly\n * rather than silently dropped.\n */\nexport const SYSTEM_ASSIGNED_COLUMNS = new Set(['email', 'name', 'imageUrl', 'role'])\n\n/**\n * Columns the system *maintains* on its own schedule, without telling anyone.\n * `registerUser` bumps `lastSeenAt` on every WS connect and `handleUserUpdate`\n * rewrites it on every 60s presence heartbeat; neither broadcasts, so every\n * client's copy goes stale silently and there is no heal path short of a\n * refetch. Read-modify-write echo (`put(id, { ...rec.data, field })`) is the\n * SDK's own house style, so refusing these would refuse writes over a value the\n * caller never chose and cannot keep current. They are preserved silently.\n */\nexport const SYSTEM_MAINTAINED_COLUMNS = new Set(['createdAt', 'lastSeenAt'])\n\n/** Every `users` column owned by the system rather than by record writes. */\nexport const SYSTEM_MANAGED_COLUMNS = new Set([\n ...SYSTEM_ASSIGNED_COLUMNS,\n ...SYSTEM_MAINTAINED_COLUMNS,\n])\n\n/** Standard user columns. Apps spread these into their users schema. */\nexport const USERS_COLUMNS: ColumnDefinition[] = [\n { name: 'email', storage: 'text', interpretation: 'plain' },\n { name: 'name', storage: 'text', interpretation: 'plain' },\n { name: 'imageUrl', storage: 'text', interpretation: 'plain' },\n { name: 'role', storage: 'text', interpretation: 'plain' },\n { name: 'createdAt', storage: 'text', interpretation: { kind: 'datetime' } },\n { name: 'lastSeenAt', storage: 'text', interpretation: { kind: 'datetime' } },\n]\n\nexport const BASE_USERS_SCHEMA: CollectionSchema = {\n name: 'users',\n columns: [...USERS_COLUMNS],\n permissions: {\n '*': { read: 'own', create: false, update: false, delete: false },\n admin: { read: true, create: false, update: true, delete: true },\n },\n}\n\n// ============================================================================\n// Schema Registry\n// ============================================================================\n\n/**\n * Lint a CollectionSchema for declarations that look like they should\n * enforce something but don't, due to interactions between top-level\n * fields (visibilityField, ownerField) and per-role permission levels.\n *\n * The SDK has historically silently accepted schemas that imply more\n * enforcement than they actually deliver — e.g., `visibilityField` set\n * but every role's `read: true` means \"anyone can read everything\"\n * regardless of `visibility`. Warn loudly at registration so app\n * authors notice before shipping a privacy bug.\n *\n * Returns an array of warning messages. Empty = clean.\n */\nexport function lintSchema(schema: CollectionSchema): string[] {\n const warnings: string[] = []\n const VISIBILITY_AWARE: ReadonlyArray<PermissionLevel> = ['published', 'shared']\n\n // Roles that grant unconditional read despite visibilityField being declared.\n if (schema.visibilityField) {\n const offendingRoles = Object.entries(schema.permissions)\n .filter(([, perms]) => perms.read === true)\n .map(([role]) => role)\n if (offendingRoles.length > 0) {\n const usesAware = Object.values(schema.permissions).some(\n (p) => typeof p.read === 'string' && VISIBILITY_AWARE.includes(p.read as PermissionLevel),\n )\n if (!usesAware) {\n warnings.push(\n `[${schema.name}] visibilityField is declared but no role uses read: 'published' or 'shared'. ` +\n `Roles with read: true (${offendingRoles.join(', ')}) will see every row regardless of visibility. ` +\n `Change those to read: 'published' (owner OR public) or 'shared' (owner OR collaborator OR public) ` +\n `to actually enforce the filter, or remove visibilityField if you don't intend to gate reads by it.`,\n )\n }\n }\n }\n\n // An ordinary client role that can create rows must not be able to claim\n // someone else's ownership. Server-written collections legitimately carry\n // a recipient/subject id instead: their non-privileged roles all have\n // create:false, while owner/admin writes are already fully trusted.\n if (schema.ownerField) {\n const col = schema.columns.find((c) => c.name === schema.ownerField)\n const spoofableRoles = Object.entries(schema.permissions)\n .filter(\n ([role, permissions]) =>\n role !== 'owner' &&\n role !== 'admin' &&\n permissions.create === true &&\n permissions.update !== 'unclaimed-or-own',\n )\n .map(([role]) => role)\n if (col && !col.userBound && spoofableRoles.length > 0) {\n warnings.push(\n `[${schema.name}] ownerField is '${schema.ownerField}' but that column is not marked userBound: true. ` +\n `Client role(s) ${spoofableRoles.join(', ')} can create a row with someone else's id in this field, bypassing 'own' permission checks. ` +\n `Add userBound: true (and ideally immutable: true) to the column.`,\n )\n }\n }\n\n // userBound columns that aren't text storage — userBound sets the\n // field to the caller's userId (a string), so non-text storage is a\n // schema mistake that will surface as a coercion error at write time.\n for (const col of schema.columns) {\n if (col.userBound && col.storage !== 'text') {\n warnings.push(\n `[${schema.name}] column '${col.name}' is userBound but storage is '${col.storage}'. ` +\n `userBound stamps the user id (a string); use storage: 'text'.`,\n )\n }\n }\n\n return warnings\n}\n\n/**\n * Lint a whole schema set: every per-schema warning, plus the rules that\n * need to see the set. The `'team'` permission level resolves membership from\n * a `team_members` collection (`teamId`, `userId`, `status`); without one it\n * denies everything, silently — a mistake that presents as a sync bug, not\n * as a schema error, so name it here.\n */\nexport function lintSchemas(schemas: CollectionSchema[]): string[] {\n return [...schemas.flatMap((schema) => lintSchema(schema)), ...lintSchemaSet(schemas)]\n}\n\n/** The rules that need to see the whole schema set (per-schema rules live in\n * `lintSchema`; `lintSchemas` composes both). */\nexport function lintSchemaSet(schemas: CollectionSchema[]): string[] {\n const warnings: string[] = []\n if (!schemas.some((schema) => schema.name === 'team_members')) {\n for (const schema of schemas) {\n const teamRoles = Object.entries(schema.permissions)\n .filter(([, p]) => [p.read, p.update, p.delete].includes('team'))\n .map(([role]) => role)\n if (teamRoles.length > 0) {\n warnings.push(\n `[${schema.name}] role(s) ${teamRoles.join(', ')} use the 'team' permission level, but no ` +\n `'team_members' collection is registered. 'team' resolves membership from team_members ` +\n `(teamId, userId, status), so without it those roles are denied everything — silently. ` +\n `Register a team_members schema or use another level.`,\n )\n }\n }\n }\n return warnings\n}\n\nexport class SchemaRegistry {\n private trusted: Map<string, CollectionSchema> = new Map()\n\n constructor(schemas: CollectionSchema[] = []) {\n // registerTrusted runs the per-schema lint; only the set-level rules are\n // this loop's to add.\n for (const w of lintSchemaSet(schemas)) console.warn(`[schema-lint] ${w}`)\n for (const schema of schemas) this.registerTrusted(schema)\n }\n\n registerTrusted(schema: CollectionSchema): void {\n for (const w of lintSchema(schema)) {\n console.warn(`[schema-lint] ${w}`)\n }\n this.trusted.set(schema.name, schema)\n }\n\n get(name: string): CollectionSchema | undefined {\n return this.trusted.get(name)\n }\n\n has(name: string): boolean {\n return this.trusted.has(name)\n }\n\n all(): CollectionSchema[] {\n return Array.from(this.trusted.values())\n }\n\n names(): string[] {\n return Array.from(this.trusted.keys())\n }\n}\n\n// ============================================================================\n// Permission Analysis\n// ============================================================================\n\nexport type PermissionSource = 'explicit' | 'wildcard' | 'default-deny'\n\nexport interface ResolvedPermission {\n level: PermissionLevel | boolean\n source: PermissionSource\n}\n\nexport interface CollectionPermissionSummary {\n collection: string\n ownerField?: string\n collaboratorsField?: string\n teamField?: string\n columns: ColumnDefinition[]\n permissions: Record<\n string,\n {\n read: ResolvedPermission\n create: ResolvedPermission\n update: ResolvedPermission\n delete: ResolvedPermission\n writableFields?: string[]\n }\n >\n}\n\nexport interface PermissionAnalysis {\n roles: string[]\n collections: CollectionPermissionSummary[]\n}\n\nexport function analyzePermissions(schemas: CollectionSchema[]): PermissionAnalysis {\n const roleSet = new Set<string>()\n for (const schema of schemas) {\n for (const role of Object.keys(schema.permissions)) roleSet.add(role)\n }\n roleSet.delete('*')\n const roles = Array.from(roleSet)\n\n const collections: CollectionPermissionSummary[] = schemas.map((schema) => {\n const permissions: CollectionPermissionSummary['permissions'] = {}\n for (const role of roles) {\n let source: PermissionSource\n let perms: RolePermissions\n if (schema.permissions[role]) {\n source = 'explicit'\n perms = schema.permissions[role]\n } else if (schema.permissions['*']) {\n source = 'wildcard'\n perms = schema.permissions['*']\n } else {\n source = 'default-deny'\n perms = { read: false, create: false, update: false, delete: false }\n }\n permissions[role] = {\n read: { level: perms.read, source },\n create: { level: perms.create, source },\n update: { level: perms.update, source },\n delete: { level: perms.delete, source },\n writableFields: perms.writableFields,\n }\n }\n return {\n collection: schema.name,\n ownerField: schema.ownerField,\n collaboratorsField: schema.collaboratorsField,\n teamField: schema.teamField,\n columns: schema.columns,\n permissions,\n }\n })\n\n return { roles, collections }\n}\n","/**\n * AI Chat Schemas\n *\n * Pre-built collection schemas for DO-backed AI chat history.\n * The worker is the only writer; the client reads via `useQuery`.\n */\n\nimport type { CollectionSchema } from './registry'\n\n// member.create is false on both collections: writes flow exclusively through\n// the worker's /api/ai/* routes, which use X-App-Action to bypass RBAC.\n// Allowing direct WS creates would let a user PUT fake `role: 'assistant'`\n// rows into their own chats and have the next turn's loadMessages feed them\n// to the LLM as if they were real assistant outputs.\nexport const AI_CHATS_SCHEMA: CollectionSchema = {\n name: 'ai-chats',\n columns: [\n { name: 'userId', storage: 'text', interpretation: 'plain', userBound: true, immutable: true, required: true },\n { name: 'title', storage: 'text', interpretation: 'plain' },\n { name: 'model', storage: 'text', interpretation: 'plain' },\n { name: 'compactedSummary', storage: 'text', interpretation: 'plain' },\n { name: 'compactedThroughId', storage: 'text', interpretation: 'plain' },\n ],\n ownerField: 'userId',\n permissions: {\n admin: { read: true, create: true, update: true, delete: true },\n member: { read: 'own', create: false, update: 'own', delete: 'own' },\n viewer: { read: 'own', create: false, update: false, delete: false },\n },\n}\n\nexport const AI_MESSAGES_SCHEMA: CollectionSchema = {\n name: 'ai-messages',\n columns: [\n { name: 'chatId', storage: 'text', interpretation: 'plain', immutable: true, required: true },\n { name: 'userId', storage: 'text', interpretation: 'plain', userBound: true, immutable: true, required: true },\n { name: 'role', storage: 'text', interpretation: 'plain', required: true },\n { name: 'content', storage: 'text', interpretation: 'plain' },\n { name: 'parts', storage: 'text', interpretation: { kind: 'json' } },\n ],\n ownerField: 'userId',\n permissions: {\n admin: { read: true, create: true, update: true, delete: true },\n member: { read: 'own', create: false, update: 'own', delete: 'own' },\n viewer: { read: 'own', create: false, update: false, delete: false },\n },\n}\n","/**\n * Messaging Schemas\n *\n * Pre-built collection schemas for messaging functionality.\n * These schemas intentionally model public channels only. RecordRoom row\n * permissions cannot make a channel private by themselves; advertising DMs\n * or private groups here would expose their records to every room member.\n *\n * @example\n * ```typescript\n * import { CHANNELS_SCHEMA, MESSAGES_SCHEMA, REACTIONS_SCHEMA } from 'deepspace/worker'\n * export const schemas = [usersSchema, CHANNELS_SCHEMA, MESSAGES_SCHEMA, REACTIONS_SCHEMA]\n * ```\n */\n\nimport type { CollectionSchema } from './registry'\n\nexport const CHANNELS_SCHEMA: CollectionSchema = {\n name: 'channels',\n columns: [\n { name: 'name', storage: 'text', interpretation: 'plain' },\n { name: 'description', storage: 'text', interpretation: 'plain' },\n {\n name: 'type',\n storage: 'text',\n interpretation: { kind: 'select', options: ['public'] },\n },\n {\n name: 'createdBy',\n storage: 'text',\n interpretation: 'plain',\n userBound: true,\n immutable: true,\n },\n { name: 'archived', storage: 'number', interpretation: { kind: 'boolean' } },\n ],\n ownerField: 'createdBy',\n permissions: {\n admin: { read: true, create: true, update: true, delete: true },\n member: { read: true, create: true, update: 'own', delete: false },\n viewer: { read: true, create: false, update: false, delete: false },\n },\n}\n\nexport const MESSAGES_SCHEMA: CollectionSchema = {\n name: 'messages',\n columns: [\n { name: 'channelId', storage: 'text', interpretation: 'plain' },\n { name: 'content', storage: 'text', interpretation: 'plain' },\n {\n name: 'authorId',\n storage: 'text',\n interpretation: 'plain',\n userBound: true,\n immutable: true,\n },\n { name: 'parentMessageId', storage: 'text', interpretation: 'plain' },\n { name: 'edited', storage: 'number', interpretation: { kind: 'boolean' } },\n { name: 'editedAt', storage: 'text', interpretation: { kind: 'datetime' } },\n // Soft-delete flag. Slack-style messaging keeps the row + parent IDs\n // intact (so threads remain coherent) and renders a tombstone instead.\n // Hard-delete via remove() still works if an app wants it.\n { name: 'deleted', storage: 'number', interpretation: { kind: 'boolean' } },\n ],\n ownerField: 'authorId',\n permissions: {\n admin: { read: true, create: true, update: true, delete: true },\n member: { read: true, create: true, update: 'own', delete: 'own' },\n viewer: { read: true, create: false, update: false, delete: false },\n },\n}\n\nexport const REACTIONS_SCHEMA: CollectionSchema = {\n name: 'reactions',\n columns: [\n { name: 'messageId', storage: 'text', interpretation: 'plain' },\n { name: 'channelId', storage: 'text', interpretation: 'plain' },\n { name: 'emoji', storage: 'text', interpretation: 'plain' },\n { name: 'userId', storage: 'text', interpretation: 'plain', userBound: true, immutable: true },\n ],\n ownerField: 'userId',\n uniqueOn: ['messageId', 'emoji', 'userId'],\n permissions: {\n admin: { read: true, create: true, update: false, delete: true },\n member: { read: true, create: true, update: false, delete: 'own' },\n viewer: { read: true, create: false, update: false, delete: false },\n },\n}\n\nexport const CHANNEL_MEMBERS_SCHEMA: CollectionSchema = {\n name: 'channel-members',\n columns: [\n { name: 'channelId', storage: 'text', interpretation: 'plain' },\n { name: 'userId', storage: 'text', interpretation: 'plain', userBound: true, immutable: true },\n { name: 'joinedAt', storage: 'text', interpretation: { kind: 'datetime' } },\n ],\n ownerField: 'userId',\n uniqueOn: ['channelId', 'userId'],\n permissions: {\n admin: { read: true, create: true, update: false, delete: true },\n member: { read: true, create: true, update: false, delete: 'own' },\n viewer: { read: true, create: false, update: false, delete: false },\n },\n}\n\nexport const READ_RECEIPTS_SCHEMA: CollectionSchema = {\n name: 'read-receipts',\n columns: [\n { name: 'channelId', storage: 'text', interpretation: 'plain' },\n { name: 'userId', storage: 'text', interpretation: 'plain', userBound: true, immutable: true },\n { name: 'lastReadAt', storage: 'text', interpretation: { kind: 'datetime' } },\n ],\n ownerField: 'userId',\n uniqueOn: ['channelId', 'userId'],\n permissions: {\n admin: { read: true, create: true, update: true, delete: true },\n member: { read: true, create: true, update: 'own', delete: 'own' },\n viewer: { read: false, create: false, update: false, delete: false },\n },\n}\n"],"mappings":"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","names":[]}