@sudobility/screenwriter_types 0.1.1 → 0.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.
@@ -1,452 +0,0 @@
1
- import { z } from 'zod';
2
- import { cursorQuerySchema } from '../api/pagination.js';
3
- /**
4
- * The asset library (slice B11; spec 05 §6.13, §8.6, §14.1, spec 11 §3 and §4.5). Assets are workspace files with metadata;
5
- * bytes never pass through the API (presigned multipart PUT to R2). This file holds the vocabularies, the DTOs and the
6
- * role-to-target table the server enforces (`ROLE_NOT_ALLOWED_FOR_TARGET`).
7
- */
8
- // ─── vocabularies ────────────────────────────────────────────────────────────
9
- export const ASSET_KINDS = [
10
- 'image',
11
- 'video',
12
- 'audio',
13
- 'model3d',
14
- 'document',
15
- 'archive',
16
- 'data',
17
- ];
18
- export const assetKindSchema = z.enum(ASSET_KINDS);
19
- export const ASSET_ROLES = [
20
- // launch
21
- 'reference',
22
- 'headshot',
23
- 'photo',
24
- 'logo',
25
- 'beat_image',
26
- 'inline_image',
27
- 'panel_reference',
28
- 'location_scout',
29
- 'mood_board',
30
- 'attachment',
31
- // future production (valid now, unused by launch UI)
32
- 'turnaround',
33
- 'expression_sheet',
34
- 'costume_design',
35
- 'set_concept',
36
- 'set_plan',
37
- 'storyboard_frame',
38
- 'animatic',
39
- 'previz',
40
- 'rendered_video',
41
- 'rendered_shot',
42
- 'dialogue_audio',
43
- 'voice_sample',
44
- 'voice_profile',
45
- 'music_cue',
46
- 'sound_effect',
47
- 'lip_sync',
48
- 'captions',
49
- 'model3d',
50
- 'model3d_rig',
51
- 'texture',
52
- 'lora',
53
- 'timeline',
54
- ];
55
- export const assetRoleSchema = z.enum(ASSET_ROLES);
56
- export const ASSET_ROLE_GROUPS = {
57
- reference: 'reference',
58
- headshot: 'reference',
59
- photo: 'reference',
60
- logo: 'other',
61
- beat_image: 'other',
62
- inline_image: 'other',
63
- panel_reference: 'storyboard',
64
- location_scout: 'reference',
65
- mood_board: 'reference',
66
- attachment: 'other',
67
- turnaround: 'design',
68
- expression_sheet: 'design',
69
- costume_design: 'design',
70
- set_concept: 'design',
71
- set_plan: 'design',
72
- storyboard_frame: 'storyboard',
73
- animatic: 'video',
74
- previz: 'video',
75
- rendered_video: 'video',
76
- rendered_shot: 'video',
77
- dialogue_audio: 'audio',
78
- voice_sample: 'audio',
79
- voice_profile: 'audio',
80
- music_cue: 'audio',
81
- sound_effect: 'audio',
82
- lip_sync: 'video',
83
- captions: 'other',
84
- model3d: '3d',
85
- model3d_rig: '3d',
86
- texture: '3d',
87
- lora: 'other',
88
- timeline: 'other',
89
- };
90
- export const ASSET_TARGET_KINDS = [
91
- 'document',
92
- 'scene',
93
- 'element',
94
- 'shot',
95
- 'entity',
96
- 'beat',
97
- 'title_page',
98
- 'snapshot',
99
- ];
100
- export const assetTargetKindSchema = z.enum(ASSET_TARGET_KINDS);
101
- /**
102
- * Which target kinds a role may sit on (spec 05 §6.13: "enforces spec 11's role-to-target table"; spec 11 §3.7 lists the
103
- * roles but not the table, so this is the decision made in B11). A role not listed here may go on any target.
104
- */
105
- export const ASSET_ROLE_TARGETS = {
106
- headshot: ['entity'],
107
- turnaround: ['entity'],
108
- expression_sheet: ['entity'],
109
- costume_design: ['entity'],
110
- voice_sample: ['entity'],
111
- voice_profile: ['entity'],
112
- set_concept: ['entity', 'scene'],
113
- set_plan: ['entity', 'scene'],
114
- location_scout: ['entity', 'scene'],
115
- beat_image: ['beat'],
116
- inline_image: ['element'],
117
- panel_reference: ['element', 'scene', 'shot'],
118
- logo: ['title_page', 'document'],
119
- storyboard_frame: ['shot', 'scene'],
120
- animatic: ['shot', 'scene', 'document'],
121
- previz: ['shot', 'scene', 'document'],
122
- rendered_video: ['shot', 'scene', 'document'],
123
- rendered_shot: ['shot'],
124
- dialogue_audio: ['element', 'shot', 'scene'],
125
- music_cue: ['scene', 'shot', 'document'],
126
- sound_effect: ['scene', 'shot', 'element'],
127
- lip_sync: ['shot', 'element'],
128
- captions: ['scene', 'shot', 'document'],
129
- timeline: ['document', 'scene'],
130
- };
131
- /** Roles that only fit an entity of one kind (checked against `proj_entities.kind`). */
132
- export const ASSET_ROLE_ENTITY_KINDS = {
133
- headshot: ['character'],
134
- turnaround: ['character'],
135
- expression_sheet: ['character'],
136
- costume_design: ['character'],
137
- voice_sample: ['character'],
138
- voice_profile: ['character'],
139
- set_concept: ['location'],
140
- set_plan: ['location'],
141
- location_scout: ['location'],
142
- };
143
- export const ASSET_ORIGINS = [
144
- 'upload',
145
- 'import',
146
- 'generated',
147
- 'external_tool',
148
- ];
149
- export const ASSET_VERSION_STATUSES = [
150
- 'uploading',
151
- 'processing',
152
- 'ready',
153
- 'failed',
154
- ];
155
- export const STALENESS_VALUES = [
156
- 'fresh',
157
- 'stale',
158
- 'target_deleted',
159
- 'unknown',
160
- ];
161
- /** Multipart parts are this big (the last one smaller); an upload of at most one part is a single presigned PUT. */
162
- export const ASSET_PART_SIZE_BYTES = 8 * 1024 * 1024;
163
- /** How many part PUTs the client helper runs at once. */
164
- export const ASSET_UPLOAD_CONCURRENCY = 3;
165
- /** Presigned part URLs and the upload itself live this long (seconds); a resume re-signs. */
166
- export const ASSET_UPLOAD_TTL_S = 3600;
167
- /** Presigned GET lifetime for `GET /assets/:aid/versions/:vid/url` (seconds). */
168
- export const ASSET_URL_TTL_S = 300;
169
- /** Largest original per kind (spec 11 §3.2). */
170
- export const ASSET_MAX_BYTES = {
171
- image: 100 * 1024 ** 2,
172
- video: 10 * 1024 ** 3,
173
- audio: 2 * 1024 ** 3,
174
- model3d: 5 * 1024 ** 3,
175
- document: 200 * 1024 ** 2,
176
- archive: 10 * 1024 ** 3,
177
- data: 50 * 1024 ** 2,
178
- };
179
- /** Extension -> kind and the MIME types accepted for it (spec 11 §3.2). The extension must be listed and the magic bytes must agree. */
180
- export const ASSET_EXTENSIONS = {
181
- '.png': { kind: 'image', mimes: ['image/png'] },
182
- '.jpg': { kind: 'image', mimes: ['image/jpeg'] },
183
- '.jpeg': { kind: 'image', mimes: ['image/jpeg'] },
184
- '.webp': { kind: 'image', mimes: ['image/webp'] },
185
- '.avif': { kind: 'image', mimes: ['image/avif'] },
186
- '.gif': { kind: 'image', mimes: ['image/gif'] },
187
- '.heic': { kind: 'image', mimes: ['image/heic'] },
188
- '.heif': { kind: 'image', mimes: ['image/heif'] },
189
- '.svg': { kind: 'image', mimes: ['image/svg+xml'] },
190
- '.tif': { kind: 'image', mimes: ['image/tiff'] },
191
- '.tiff': { kind: 'image', mimes: ['image/tiff'] },
192
- '.exr': { kind: 'image', mimes: ['image/x-exr'] },
193
- '.psd': { kind: 'image', mimes: ['image/vnd.adobe.photoshop'] },
194
- '.mp4': { kind: 'video', mimes: ['video/mp4'] },
195
- '.mov': { kind: 'video', mimes: ['video/quicktime'] },
196
- '.webm': { kind: 'video', mimes: ['video/webm'] },
197
- '.mkv': { kind: 'video', mimes: ['video/x-matroska'] },
198
- '.wav': { kind: 'audio', mimes: ['audio/wav', 'audio/x-wav', 'audio/wave'] },
199
- '.aif': { kind: 'audio', mimes: ['audio/x-aiff', 'audio/aiff'] },
200
- '.aiff': { kind: 'audio', mimes: ['audio/x-aiff', 'audio/aiff'] },
201
- '.flac': { kind: 'audio', mimes: ['audio/flac'] },
202
- '.mp3': { kind: 'audio', mimes: ['audio/mpeg'] },
203
- '.aac': { kind: 'audio', mimes: ['audio/aac'] },
204
- '.m4a': { kind: 'audio', mimes: ['audio/mp4', 'audio/x-m4a'] },
205
- '.ogg': { kind: 'audio', mimes: ['audio/ogg'] },
206
- '.opus': { kind: 'audio', mimes: ['audio/ogg', 'audio/opus'] },
207
- '.glb': { kind: 'model3d', mimes: ['model/gltf-binary'] },
208
- '.gltf': { kind: 'model3d', mimes: ['model/gltf+json'] },
209
- '.usdz': { kind: 'model3d', mimes: ['model/vnd.usdz+zip'] },
210
- '.blend': {
211
- kind: 'model3d',
212
- mimes: ['application/x-blender', 'application/octet-stream'],
213
- },
214
- '.fbx': {
215
- kind: 'model3d',
216
- mimes: ['model/vnd.fbx', 'application/octet-stream'],
217
- },
218
- '.obj': { kind: 'model3d', mimes: ['model/obj', 'text/plain'] },
219
- '.stl': {
220
- kind: 'model3d',
221
- mimes: ['model/stl', 'application/sla', 'application/octet-stream'],
222
- },
223
- '.usda': { kind: 'model3d', mimes: ['model/vnd.usda', 'text/plain'] },
224
- '.usdc': {
225
- kind: 'model3d',
226
- mimes: ['model/vnd.usda', 'application/octet-stream'],
227
- },
228
- '.pdf': { kind: 'document', mimes: ['application/pdf'] },
229
- '.txt': { kind: 'document', mimes: ['text/plain'] },
230
- '.md': { kind: 'document', mimes: ['text/markdown'] },
231
- '.docx': {
232
- kind: 'document',
233
- mimes: [
234
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
235
- ],
236
- },
237
- '.zip': {
238
- kind: 'archive',
239
- mimes: ['application/zip', 'application/x-zip-compressed'],
240
- },
241
- '.json': { kind: 'data', mimes: ['application/json'] },
242
- '.csv': { kind: 'data', mimes: ['text/csv'] },
243
- '.srt': { kind: 'data', mimes: ['application/x-subrip', 'text/plain'] },
244
- '.vtt': { kind: 'data', mimes: ['text/vtt'] },
245
- '.otio': {
246
- kind: 'data',
247
- mimes: ['application/otio+json', 'application/json'],
248
- },
249
- '.fcpxml': {
250
- kind: 'data',
251
- mimes: ['application/x-fcpxml', 'application/xml', 'text/xml'],
252
- },
253
- '.edl': { kind: 'data', mimes: ['application/edl', 'text/plain'] },
254
- };
255
- /** `.ext` of a file name, lower-cased ('' when it has none). */
256
- export const assetExtension = (filename) => {
257
- const m = /(\.[A-Za-z0-9]{1,10})$/.exec(filename);
258
- return m ? m[1].toLowerCase() : '';
259
- };
260
- /** Client-side pre-check that mirrors the server's: null when the file could be accepted, else why not. */
261
- export function checkAssetFile(filename, mimeType, sizeBytes, kind) {
262
- const spec = ASSET_EXTENSIONS[assetExtension(filename)];
263
- if (!spec)
264
- return 'type';
265
- if (spec.kind !== kind)
266
- return 'kind';
267
- if (!spec.mimes.includes(mimeType.toLowerCase().split(';')[0].trim()))
268
- return 'mime';
269
- if (sizeBytes > ASSET_MAX_BYTES[kind])
270
- return 'size';
271
- return null;
272
- }
273
- // ─── rights and provenance (spec 11 §3.6, §3.8) ──────────────────────────────
274
- export const assetRightsSchema = z.object({
275
- ownership: z
276
- .enum(['owned', 'licensed', 'public_domain', 'generated', 'unknown'])
277
- .default('unknown'),
278
- license: z.string().max(500).optional(),
279
- attribution: z.string().max(500).optional(),
280
- sourceUrl: z.string().max(2000).optional(),
281
- restrictions: z.string().max(1000).optional(),
282
- expiresAt: z.string().optional(),
283
- containsLikenessOf: z.array(z.string().max(200)).max(50).optional(),
284
- consentAssetId: z.string().optional(),
285
- aiTrainingAllowed: z.boolean().default(false),
286
- });
287
- export const assetSourceRefSchema = z.object({
288
- documentId: z.string(),
289
- snapshotId: z.string().optional(),
290
- targetKind: z.enum([
291
- 'document',
292
- 'scene',
293
- 'element',
294
- 'shot',
295
- 'entity',
296
- 'beat',
297
- 'title_page',
298
- ]),
299
- targetId: z.string(),
300
- sourceHash: z.string(),
301
- });
302
- /** What a caller may send as `provenance` (the server fills `upload` and `import` itself). */
303
- export const assetProvenanceInputSchema = z.discriminatedUnion('origin', [
304
- z.object({
305
- origin: z.literal('external_tool'),
306
- tool: z.string().min(1).max(100),
307
- toolVersion: z.string().max(100).optional(),
308
- description: z.string().max(2000).optional(),
309
- sourceRefs: z.array(assetSourceRefSchema).max(100).default([]),
310
- createdVia: z.enum(['mcp', 'api', 'app']).default('api'),
311
- }),
312
- z.object({
313
- origin: z.literal('generated'),
314
- jobId: z.string().optional(),
315
- adapter: z.string().min(1).max(100),
316
- provider: z.string().min(1).max(100),
317
- model: z.string().max(200).optional(),
318
- prompt: z.string().max(20000).optional(),
319
- negativePrompt: z.string().max(20000).optional(),
320
- parameters: z.record(z.string(), z.unknown()).default({}),
321
- inputs: z
322
- .array(z.object({
323
- assetVersionId: z.string().optional(),
324
- packetHash: z.string().optional(),
325
- role: z.string(),
326
- }))
327
- .max(100)
328
- .default([]),
329
- sourceRefs: z.array(assetSourceRefSchema).max(100).default([]),
330
- costCredits: z.number().optional(),
331
- providerJobId: z.string().optional(),
332
- createdVia: z.enum(['mcp', 'api', 'app']).default('api'),
333
- }),
334
- ]);
335
- // ─── upload flow (spec 05 §6.13) ─────────────────────────────────────────────
336
- const sha256HexSchema = z
337
- .string()
338
- .regex(/^[0-9a-fA-F]{64}$/, 'sha256Hex must be 64 hex characters');
339
- export const assetUploadRequestSchema = z.object({
340
- /** Client-minted `asset_...` id for offline staging; accepted when unused (`ID_CONFLICT` otherwise). */
341
- assetId: z
342
- .string()
343
- .regex(/^asset_[0-9A-Za-z]{10,40}$/)
344
- .optional(),
345
- filename: z.string().min(1).max(500),
346
- mimeType: z.string().min(1).max(200),
347
- sizeBytes: z.number().int().min(1),
348
- sha256Hex: sha256HexSchema.optional(),
349
- kind: assetKindSchema,
350
- title: z.string().trim().min(1).max(200).optional(),
351
- /** A new version of an existing asset: the version it derives from. */
352
- parentVersionId: z.string().min(1).optional(),
353
- provenance: assetProvenanceInputSchema.optional(),
354
- rights: assetRightsSchema.partial().optional(),
355
- });
356
- export const assetPartsRequestSchema = z.object({
357
- partNumbers: z.array(z.number().int().min(1).max(10000)).min(1).max(100),
358
- });
359
- export const assetCompleteSchema = z.object({
360
- parts: z
361
- .array(z.object({
362
- partNumber: z.number().int().min(1),
363
- etag: z.string().min(1).max(200),
364
- }))
365
- .max(10000),
366
- sha256Hex: sha256HexSchema.optional(),
367
- });
368
- // ─── library routes ──────────────────────────────────────────────────────────
369
- const linkedToSchema = z
370
- .string()
371
- .regex(/^(entity|scene|shot|element):[A-Za-z0-9_-]+$/, 'linkedTo is <entity|scene|shot|element>:<id>');
372
- export const assetListQuerySchema = cursorQuerySchema.extend({
373
- workspaceId: z.string().min(1).optional(),
374
- documentId: z.string().min(1).optional(),
375
- linkedTo: linkedToSchema.optional(),
376
- role: assetRoleSchema.optional(),
377
- kind: assetKindSchema.optional(),
378
- q: z.string().trim().min(1).max(200).optional(),
379
- origin: z.enum(ASSET_ORIGINS).optional(),
380
- });
381
- export const assetUpdateSchema = z
382
- .object({
383
- title: z.string().trim().min(1).max(200).optional(),
384
- description: z.string().max(5000).optional(),
385
- rights: assetRightsSchema.partial().optional(),
386
- })
387
- .refine((v) => Object.keys(v).length > 0, 'Nothing to update');
388
- export const ASSET_URL_VARIANTS = [
389
- 'original',
390
- 'thumb_256',
391
- 'thumb_512',
392
- 'preview_1600',
393
- 'poster',
394
- 'proxy_720p',
395
- 'proxy_1080p',
396
- 'preview',
397
- 'waveform',
398
- 'sprite',
399
- ];
400
- export const assetUrlQuerySchema = z.object({
401
- variant: z
402
- .string()
403
- .regex(/^[a-z0-9_]{1,40}$/)
404
- .default('original'),
405
- });
406
- // ─── links ───────────────────────────────────────────────────────────────────
407
- export const assetLinkCreateSchema = z.object({
408
- assetId: z.string().min(1),
409
- pinnedVersionId: z.string().min(1).optional(),
410
- /** Null/omitted = a workspace-level link: the target must then be `{kind: 'document', id: <the workspace id>}`. */
411
- documentId: z.string().min(1).nullable().optional(),
412
- target: z.object({
413
- kind: assetTargetKindSchema,
414
- id: z.string().min(1).max(200),
415
- }),
416
- role: assetRoleSchema,
417
- sortOrder: z.number().int().min(0).max(1_000_000).optional(),
418
- /** Version-prefixed content hash of the target when the asset was derived from it (spec 11 §4.2). */
419
- sourceHash: z.string().min(1).max(200).optional(),
420
- sourceSnapshotId: z.string().min(1).optional(),
421
- note: z.string().max(2000).optional(),
422
- });
423
- export const assetLinkUpdateSchema = z
424
- .object({
425
- role: assetRoleSchema.optional(),
426
- sortOrder: z.number().int().min(0).max(1_000_000).optional(),
427
- /** `null` unpins (follow the current version). */
428
- pinnedVersionId: z.string().min(1).nullable().optional(),
429
- note: z.string().max(2000).optional(),
430
- })
431
- .refine((v) => Object.keys(v).length > 0, 'Nothing to update');
432
- const boolQuery = z.enum(['true', 'false']).transform((v) => v === 'true');
433
- export const documentAssetLinksQuerySchema = z.object({
434
- targetKind: assetTargetKindSchema.optional(),
435
- targetId: z.string().min(1).optional(),
436
- role: assetRoleSchema.optional(),
437
- withStaleness: boolQuery.optional(),
438
- });
439
- // ─── staleness (spec 11 §4.5) ────────────────────────────────────────────────
440
- export const STALENESS_TARGET_KINDS = [
441
- 'scene',
442
- 'element',
443
- 'shot',
444
- 'entity',
445
- ];
446
- export const documentStalenessQuerySchema = z.object({
447
- targetKind: z.enum(STALENESS_TARGET_KINDS).optional(),
448
- });
449
- // ─── the `asset.derive` job (a documented stub: see the API's CLAUDE.md) ─────
450
- export const assetDeriveInputSchema = z.object({
451
- assetVersionId: z.string().min(1),
452
- });
@@ -1,211 +0,0 @@
1
- /**
2
- * Collaboration DTOs (spec 05 §6.15 notes/mentions/chat/activity, §6.16 notifications, §6.24 devices/push,
3
- * §6.26 `writingReminder` prefs; slice B12). Tables: `note_mentions`, `document_chat_messages`, `activity_events`,
4
- * `notifications`, `devices`.
5
- */
6
- import { z } from 'zod';
7
- import { cursorQuerySchema } from '../api/pagination.js';
8
- // ─── Notification kinds and prefs (§6.16, §6.26) ────────────────────────────
9
- export const NOTIFICATION_KINDS = [
10
- 'mention',
11
- 'note_reply',
12
- 'note_on_my_document',
13
- 'snapshot_comment',
14
- 'chat_mention',
15
- 'invitation',
16
- 'share_granted',
17
- 'snapshot_opened_by_collaborator',
18
- 'job_finished',
19
- 'job_failed',
20
- 'ai_suggestions_ready',
21
- 'ai_refunded',
22
- 'export_ready',
23
- 'watermark_batch_sent',
24
- 'writing_reminder',
25
- 'credits_low',
26
- 'purchase_completed',
27
- 'account_deletion_scheduled',
28
- ];
29
- export const NOTIFICATION_EMAIL_MODES = ['immediate', 'digest', 'off'];
30
- /** Kinds that default to an immediate email (the rest default to `digest` or `off`; see `defaultNotificationKindPrefs`). */
31
- const DEFAULT_IMMEDIATE_EMAIL = new Set([
32
- 'mention',
33
- 'note_reply',
34
- 'chat_mention',
35
- 'invitation',
36
- 'share_granted',
37
- 'job_failed',
38
- 'export_ready',
39
- 'watermark_batch_sent',
40
- 'credits_low',
41
- 'purchase_completed',
42
- 'account_deletion_scheduled',
43
- ]);
44
- const DEFAULT_DIGEST_EMAIL = new Set([
45
- 'snapshot_comment',
46
- 'note_on_my_document',
47
- 'ai_suggestions_ready',
48
- 'ai_refunded',
49
- 'snapshot_opened_by_collaborator',
50
- ]);
51
- /** Kinds a person is not plausibly interrupted for, off by default: no email, no push. */
52
- const DEFAULT_QUIET = new Set([
53
- 'job_finished',
54
- 'writing_reminder',
55
- ]);
56
- /** Every kind gets a sane default; a person may override any of `inApp`/`push`/`email` per kind. */
57
- export function defaultNotificationKindPrefs(kind) {
58
- const email = DEFAULT_IMMEDIATE_EMAIL.has(kind)
59
- ? 'immediate'
60
- : DEFAULT_DIGEST_EMAIL.has(kind)
61
- ? 'digest'
62
- : 'off';
63
- return { inApp: true, push: !DEFAULT_QUIET.has(kind), email };
64
- }
65
- export function defaultNotificationPrefs() {
66
- return {
67
- kinds: Object.fromEntries(NOTIFICATION_KINDS.map((k) => [k, defaultNotificationKindPrefs(k)])),
68
- writingReminderTime: null,
69
- };
70
- }
71
- const notificationKindPrefsPatchSchema = z.object({
72
- inApp: z.boolean().optional(),
73
- push: z.boolean().optional(),
74
- email: z.enum(NOTIFICATION_EMAIL_MODES).optional(),
75
- });
76
- const HHMM_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
77
- /** `PUT /me/notification-prefs`: a sparse per-kind patch (only the kinds/fields given change) plus the reminder time. */
78
- export const notificationPrefsSchema = z.object({
79
- kinds: z
80
- .record(z.string(), notificationKindPrefsPatchSchema)
81
- .refine((obj) => Object.keys(obj).every((k) => NOTIFICATION_KINDS.includes(k)), {
82
- message: 'unknown notification kind',
83
- })
84
- .optional(),
85
- writingReminderTime: z
86
- .string()
87
- .regex(HHMM_RE, 'writingReminderTime must be HH:MM (24h, local)')
88
- .nullable()
89
- .optional(),
90
- });
91
- /** `{...current, ...update}` per kind (a patch's fields win); `writingReminderTime` replaces whole. */
92
- export function mergeNotificationPrefs(current, update) {
93
- const kinds = { ...current.kinds };
94
- for (const [k, patch] of Object.entries(update.kinds ?? {})) {
95
- const kind = k;
96
- kinds[kind] = {
97
- ...(kinds[kind] ?? defaultNotificationKindPrefs(kind)),
98
- ...patch,
99
- };
100
- }
101
- return {
102
- kinds,
103
- writingReminderTime: update.writingReminderTime === undefined
104
- ? current.writingReminderTime
105
- : update.writingReminderTime,
106
- };
107
- }
108
- const boolQuery = z.enum(['true', 'false']).transform((v) => v === 'true');
109
- export const notificationsQuerySchema = cursorQuerySchema.extend({
110
- unread: boolQuery.optional(),
111
- });
112
- export const notificationsReadSchema = z
113
- .object({
114
- ids: z.array(z.string().min(1)).max(200).optional(),
115
- all: z.literal(true).optional(),
116
- before: z.iso.datetime({ offset: true }).optional(),
117
- })
118
- .refine((v) => v.ids !== undefined || v.all !== undefined || v.before !== undefined, {
119
- message: 'one of ids, all or before is required',
120
- });
121
- // ─── Chat (§6.15, §8.7 `document_chat_messages`) ────────────────────────────
122
- export const CHAT_BODY_MAX = 4000;
123
- export const CHAT_EDIT_WINDOW_MINUTES = 15;
124
- /** Y.Doc notes use plain `mentions: string[]` (user ids, no offset/length: writing_core's simplification); chat mirrors it in the DTO for consistency with the spec's shape, offset/length optional. */
125
- export const mentionRefSchema = z.object({
126
- userId: z.string().min(1),
127
- offset: z.number().int().min(0).optional(),
128
- length: z.number().int().min(1).optional(),
129
- });
130
- export const chatMessageCreateSchema = z.object({
131
- clientMessageId: z
132
- .string()
133
- .min(8)
134
- .max(64)
135
- .regex(/^msg_[0-9A-Za-z]+$/, 'clientMessageId must be a msg_ prefixed id'),
136
- body: z.string().trim().min(1).max(CHAT_BODY_MAX),
137
- mentions: z.array(mentionRefSchema).max(50).optional(),
138
- });
139
- export const chatMessageUpdateSchema = z.object({
140
- body: z.string().trim().min(1).max(CHAT_BODY_MAX),
141
- });
142
- export const chatQuerySchema = cursorQuerySchema.extend({
143
- before: z.iso.datetime({ offset: true }).optional(),
144
- });
145
- // ─── Activity (§6.15, §8.7 `activity_events`) ───────────────────────────────
146
- export const ACTIVITY_KINDS = [
147
- 'document.created',
148
- 'document.renamed',
149
- 'document.moved',
150
- 'document.trashed',
151
- 'document.restored',
152
- 'session.edited',
153
- 'snapshot.created',
154
- 'snapshot.opened',
155
- 'snapshot.forked',
156
- 'version.restored',
157
- 'template.applied',
158
- 'import.over',
159
- 'revision.setCreated',
160
- 'locks.changed',
161
- 'changes.decided',
162
- 'ai.jobCompleted',
163
- 'ai.suggestionsDecided',
164
- 'share.granted',
165
- 'share.linkCreated',
166
- 'member.joined',
167
- 'member.left',
168
- 'mcp.commandsApplied',
169
- ];
170
- export const ACTOR_KINDS = ['user', 'mcp', 'api', 'ai', 'system'];
171
- const csvList = (v) => typeof v === 'string'
172
- ? v
173
- .split(',')
174
- .map((s) => s.trim())
175
- .filter(Boolean)
176
- : v;
177
- export const activityQuerySchema = cursorQuerySchema.extend({
178
- kinds: z.preprocess(csvList, z.array(z.string())).optional(),
179
- });
180
- export const workspaceActivityQuerySchema = cursorQuerySchema.extend({
181
- projectId: z.string().min(1).optional(),
182
- });
183
- // ─── Devices, push tokens (§6.24) ────────────────────────────────────────────
184
- export const DEVICE_PLATFORMS = [
185
- 'web',
186
- 'ios',
187
- 'ipados',
188
- 'android',
189
- 'macos',
190
- 'windows',
191
- ];
192
- export const PUSH_PLATFORMS = ['apns', 'fcm', 'wns', 'webpush'];
193
- export const deviceCreateSchema = z.object({
194
- id: z
195
- .string()
196
- .min(8)
197
- .max(64)
198
- .regex(/^dev_[0-9A-Za-z]+$/, 'id must be a dev_ prefixed id'),
199
- installId: z.string().min(1).max(128),
200
- displayName: z.string().trim().min(1).max(80),
201
- platform: z.enum(DEVICE_PLATFORMS),
202
- appVersion: z.string().min(1).max(40),
203
- });
204
- export const deviceUpdateSchema = z.object({
205
- displayName: z.string().trim().min(1).max(80),
206
- });
207
- export const pushTokenSetSchema = z.object({
208
- platform: z.enum(PUSH_PLATFORMS),
209
- token: z.string().min(1).max(4096),
210
- environment: z.enum(['production', 'sandbox']).optional(),
211
- });
@@ -1,20 +0,0 @@
1
- import { z } from 'zod';
2
- export const COMMAND_BATCH_MAX = 500;
3
- export const commandBatchRequestSchema = z.object({
4
- commands: z
5
- .array(z.record(z.string(), z.unknown()))
6
- .min(1)
7
- .max(COMMAND_BATCH_MAX),
8
- baseEpoch: z.number().int().min(0),
9
- expectedHashes: z.record(z.string(), z.string()).optional(),
10
- dryRun: z.boolean().optional(),
11
- });
12
- // ---- Scene and element reads (spec 05 6.7.1, live document) ----
13
- export const SCENE_BATCH_MAX = 20;
14
- export const ELEMENT_BATCH_MAX = 500;
15
- export const elementsBatchRequestSchema = z.object({
16
- elementIds: z.array(z.string().min(1)).min(1).max(ELEMENT_BATCH_MAX),
17
- });
18
- export const scenesBatchRequestSchema = z.object({
19
- sceneIds: z.array(z.string().min(1)).min(1).max(SCENE_BATCH_MAX),
20
- });
@@ -1,7 +0,0 @@
1
- import { z } from 'zod';
2
- export const CONSUMABLE_SOURCES = ['web', 'apple', 'google', 'free'];
3
- /** `POST /purchases/handoff` (spec 05 §6.21): lets a platform with no in-app purchase (Windows) send the user to the web to buy. */
4
- export const purchaseHandoffRequestSchema = z.object({
5
- productId: z.string().optional(),
6
- returnTo: z.string().min(1).max(2000),
7
- });