@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,262 +0,0 @@
1
- import { z } from 'zod';
2
- /** Spec 03 §2. Subprotocol string is `fadewright-sync.v1`. */
3
- export const SYNC_PROTOCOL_VERSION = 1;
4
- export const SYNC_SUBPROTOCOL = 'fadewright-sync.v1';
5
- export const SYNC_PATH = '/api/v1/sync';
6
- export const SYNC_HEARTBEAT_MS = 25_000;
7
- export const SYNC_MAX_FRAME_BYTES = 1_048_576;
8
- export const SYNC_AUTH_TIMEOUT_MS = 5_000;
9
- /** Numeric message-type codes (spec 03 §2.3). Frame = channelId, messageType, payload. */
10
- export const SYNC_MESSAGE_TYPES = {
11
- auth: 0,
12
- authOk: 1,
13
- authError: 2,
14
- reauth: 3,
15
- ping: 4,
16
- pong: 4,
17
- subscribe: 10,
18
- subscribed: 11,
19
- subscribeError: 12,
20
- unsubscribe: 13,
21
- syncStep1: 20,
22
- syncStep2: 21,
23
- update: 22,
24
- ack: 23,
25
- reject: 24,
26
- awareness: 30,
27
- epochChanged: 40,
28
- roleChanged: 41,
29
- documentDeleted: 42,
30
- normalized: 43,
31
- serverShutdown: 50,
32
- };
33
- /** Close codes (spec 03 §2.6). */
34
- export const SYNC_CLOSE_CODES = {
35
- NORMAL: 1000,
36
- GOING_AWAY: 1001,
37
- MALFORMED_FRAME: 4400,
38
- AUTH_TIMEOUT_OR_INVALID: 4401,
39
- ACCOUNT_DISABLED_OR_DEVICE_REVOKED: 4403,
40
- PROTOCOL_UNSUPPORTED: 4406,
41
- HEARTBEAT_TIMEOUT: 4408,
42
- /** Document epoch changed (snapshot open / version restore): resubscribe at the new epoch. */
43
- EPOCH_CHANGED: 4409,
44
- FRAME_TOO_LARGE: 4413,
45
- RATE_LIMITED: 4429,
46
- };
47
- // ─── Vocabularies ───────────────────────────────────────────────────────────
48
- export const SYNC_AUTH_SCHEMES = ['firebase', 'apikey', 'link'];
49
- export const SYNC_AUTH_ERROR_CODES = [
50
- 'UNAUTHENTICATED',
51
- 'ACCOUNT_DISABLED',
52
- 'DEVICE_REVOKED',
53
- 'LINK_EXPIRED',
54
- 'CLIENT_TOO_OLD',
55
- 'SERVER_TOO_OLD',
56
- ];
57
- export const SYNC_SUBSCRIBE_ERROR_CODES = [
58
- 'NOT_FOUND',
59
- 'FORBIDDEN',
60
- 'DOCUMENT_DELETED',
61
- 'EPOCH_MISMATCH',
62
- 'DOCUMENT_LOCKED',
63
- 'CLIENT_TOO_OLD',
64
- ];
65
- export const SYNC_REJECT_CODES = [
66
- 'FORBIDDEN',
67
- 'EPOCH_MISMATCH',
68
- 'UPDATE_INVALID',
69
- 'PAYLOAD_TOO_LARGE',
70
- 'SCHEMA_VIOLATION',
71
- ];
72
- export const SYNC_SUBSCRIBE_MODES = ['edit', 'view'];
73
- export const SYNC_EPOCH_REASONS = [
74
- 'snapshot-open',
75
- 'import-over',
76
- 'version-restore',
77
- 'schema-migration',
78
- ];
79
- const roleOrNone = z.enum([
80
- 'owner',
81
- 'admin',
82
- 'writer',
83
- 'commenter',
84
- 'viewer',
85
- 'none',
86
- ]);
87
- const bytes = z.instanceof(Uint8Array);
88
- const uint = z.number().int().min(0);
89
- // ─── Payload schemas (JSON UTF-8 payloads) ──────────────────────────────────
90
- export const authPayloadSchema = z.object({
91
- scheme: z.enum(SYNC_AUTH_SCHEMES),
92
- token: z.string(),
93
- deviceId: z.string(),
94
- installId: z.string(),
95
- clientVersion: z.string(),
96
- schemaVersion: uint,
97
- /** Unlock-session token (`POST /documents/:did/unlock-session`) for opening locked documents. */
98
- unlockToken: z.string().optional(),
99
- });
100
- export const authOkPayloadSchema = z.object({
101
- userId: z.string(),
102
- sessionId: z.string(),
103
- serverSchemaVersion: uint,
104
- heartbeatMs: uint,
105
- maxFrameBytes: uint,
106
- });
107
- export const authErrorPayloadSchema = z.object({
108
- code: z.enum(SYNC_AUTH_ERROR_CODES),
109
- message: z.string(),
110
- });
111
- export const reauthPayloadSchema = z.object({ token: z.string() });
112
- export const pingPayloadSchema = z.object({ t: z.number() });
113
- export const subscribePayloadSchema = z.object({
114
- channelId: uint.min(1),
115
- documentId: z.string(),
116
- epoch: uint,
117
- /** base64 state vector */
118
- stateVector: z.string().optional(),
119
- mode: z.enum(SYNC_SUBSCRIBE_MODES),
120
- snapshotId: z.string().optional(),
121
- });
122
- export const subscribedPayloadSchema = z.object({
123
- documentId: z.string(),
124
- role: roleOrNone,
125
- epoch: uint,
126
- markPermissions: z.unknown(),
127
- schemaVersion: uint,
128
- });
129
- export const subscribeErrorPayloadSchema = z.object({
130
- code: z.enum(SYNC_SUBSCRIBE_ERROR_CODES),
131
- detail: z.unknown().optional(),
132
- });
133
- export const unsubscribePayloadSchema = z.object({ channelId: uint.min(1) });
134
- export const ackPayloadSchema = z.object({
135
- clientSeq: uint,
136
- serverSeq: uint,
137
- });
138
- export const rejectPayloadSchema = z.object({
139
- clientSeq: uint,
140
- code: z.enum(SYNC_REJECT_CODES),
141
- detail: z.unknown().optional(),
142
- });
143
- export const epochChangedPayloadSchema = z.object({
144
- newEpoch: uint,
145
- reason: z.enum(SYNC_EPOCH_REASONS),
146
- byUserId: z.string(),
147
- snapshotId: z.string().optional(),
148
- });
149
- export const roleChangedPayloadSchema = z.object({
150
- role: roleOrNone,
151
- markPermissions: z.unknown(),
152
- });
153
- export const documentDeletedPayloadSchema = z.object({
154
- byUserId: z.string(),
155
- recoverableUntil: z.string(),
156
- });
157
- export const normalizedPayloadSchema = z.object({
158
- serverSeq: uint,
159
- contentHashes: z.record(z.string(), z.string()),
160
- });
161
- export const serverShutdownPayloadSchema = z.object({
162
- reconnectAfterMs: uint,
163
- });
164
- // ─── Frame schemas: decoded `{channelId, type, payload}` ────────────────────
165
- const ctl = z.literal(0);
166
- const chan = uint.min(1);
167
- export const syncFrameSchema = z.discriminatedUnion('type', [
168
- z.object({
169
- channelId: ctl,
170
- type: z.literal('auth'),
171
- payload: authPayloadSchema,
172
- }),
173
- z.object({
174
- channelId: ctl,
175
- type: z.literal('authOk'),
176
- payload: authOkPayloadSchema,
177
- }),
178
- z.object({
179
- channelId: ctl,
180
- type: z.literal('authError'),
181
- payload: authErrorPayloadSchema,
182
- }),
183
- z.object({
184
- channelId: ctl,
185
- type: z.literal('reauth'),
186
- payload: reauthPayloadSchema,
187
- }),
188
- z.object({
189
- channelId: ctl,
190
- type: z.literal('ping'),
191
- payload: pingPayloadSchema,
192
- }),
193
- z.object({
194
- channelId: ctl,
195
- type: z.literal('pong'),
196
- payload: pingPayloadSchema,
197
- }),
198
- z.object({
199
- channelId: ctl,
200
- type: z.literal('subscribe'),
201
- payload: subscribePayloadSchema,
202
- }),
203
- z.object({
204
- channelId: chan,
205
- type: z.literal('subscribed'),
206
- payload: subscribedPayloadSchema,
207
- }),
208
- z.object({
209
- channelId: chan,
210
- type: z.literal('subscribeError'),
211
- payload: subscribeErrorPayloadSchema,
212
- }),
213
- z.object({
214
- channelId: ctl,
215
- type: z.literal('unsubscribe'),
216
- payload: unsubscribePayloadSchema,
217
- }),
218
- z.object({ channelId: chan, type: z.literal('syncStep1'), payload: bytes }),
219
- z.object({ channelId: chan, type: z.literal('syncStep2'), payload: bytes }),
220
- z.object({
221
- channelId: chan,
222
- type: z.literal('update'),
223
- payload: z.object({ epoch: uint, clientSeq: uint, update: bytes }),
224
- }),
225
- z.object({
226
- channelId: chan,
227
- type: z.literal('ack'),
228
- payload: ackPayloadSchema,
229
- }),
230
- z.object({
231
- channelId: chan,
232
- type: z.literal('reject'),
233
- payload: rejectPayloadSchema,
234
- }),
235
- z.object({ channelId: chan, type: z.literal('awareness'), payload: bytes }),
236
- z.object({
237
- channelId: chan,
238
- type: z.literal('epochChanged'),
239
- payload: epochChangedPayloadSchema,
240
- }),
241
- z.object({
242
- channelId: chan,
243
- type: z.literal('roleChanged'),
244
- payload: roleChangedPayloadSchema,
245
- }),
246
- z.object({
247
- channelId: chan,
248
- type: z.literal('documentDeleted'),
249
- payload: documentDeletedPayloadSchema,
250
- }),
251
- z.object({
252
- channelId: chan,
253
- type: z.literal('normalized'),
254
- payload: normalizedPayloadSchema,
255
- }),
256
- z.object({
257
- channelId: ctl,
258
- type: z.literal('serverShutdown'),
259
- payload: serverShutdownPayloadSchema,
260
- }),
261
- ]);
262
- export * from './permissions.js';
@@ -1,11 +0,0 @@
1
- /** Top-level Y.Doc keys a commenter may change (notes and replies, R6). */
2
- export const COMMENTER_WRITABLE_KEYS = ['notes'];
3
- /** Inline marks a commenter may add or remove inside element text: note anchors `n:<NoteId>` and the highlighter. */
4
- export const isCommenterMark = (name) => name === 'hl' || name.startsWith('n:');
5
- export function markPermissionsFor(role) {
6
- const edit = role === 'owner' || role === 'admin' || role === 'writer';
7
- const comment = edit || role === 'commenter';
8
- return { edit, comment, highlight: comment };
9
- }
10
- /** Roles that may send `update` frames at all (a commenter's are filtered by the matrix). */
11
- export const canSendUpdates = (role) => role !== 'viewer';
@@ -1,70 +0,0 @@
1
- import { z } from 'zod';
2
- /** `GET /templates` query: user templates always; `workspaceId` adds that workspace's; `scope` narrows. */
3
- export const templateListQuerySchema = z.object({
4
- category: z.string().optional(),
5
- workspaceId: z.string().optional(),
6
- scope: z.enum(['builtin', 'user', 'workspace']).optional(),
7
- });
8
- /** `GET /templates/:tid?version=`: an older version of a user/workspace template (immutable). */
9
- export const templateGetQuerySchema = z.object({
10
- version: z.coerce.number().int().min(1).optional(),
11
- });
12
- // ─── B14: template lifecycle (spec 05 §6.8) ─────────────────────────────────
13
- /** The body is validated server-side with writing_core's `TemplateJSON` schema (`TEMPLATE_INVALID` with the issues). */
14
- const templateBody = z.custom((v) => typeof v === 'object' && v !== null && !Array.isArray(v), {
15
- message: 'template must be an object',
16
- });
17
- export const templateCreateSchema = z
18
- .object({
19
- scope: z.enum(['user', 'workspace']),
20
- workspaceId: z.string().min(1).optional(),
21
- template: templateBody,
22
- })
23
- .refine((v) => v.scope !== 'workspace' || !!v.workspaceId, {
24
- message: 'workspaceId is required for a workspace template',
25
- path: ['workspaceId'],
26
- });
27
- export const templateVersionCreateSchema = z.object({ template: templateBody });
28
- export const templateUpdateSchema = z.object({
29
- name: z.string().trim().min(1).max(120).optional(),
30
- description: z.string().max(2000).nullable().optional(),
31
- category: z.string().min(1).max(40).optional(),
32
- });
33
- /** Template file formats: Fadewright's own JSON and the two foreign ones spec 04 §4.3 names. */
34
- export const TEMPLATE_FILE_FORMATS = [
35
- 'fwtemplate',
36
- 'fadein-template',
37
- 'fdxt',
38
- ];
39
- /** `{ "format": "fadewright-template", "formatVersion": 1, "template": <TemplateJSON> }` (spec 04 §4.3.1). */
40
- export const FWTEMPLATE_FORMAT = 'fadewright-template';
41
- export const FWTEMPLATE_FORMAT_VERSION = 1;
42
- /**
43
- * `POST /templates/import`. Spec 05's `importId` flow is `templateImportByIdSchema` (B16); this body form (the file inline, like
44
- * `documents/import`) keeps working. The result is the created template.
45
- */
46
- export const templateImportSchema = z
47
- .object({
48
- scope: z.enum(['user', 'workspace']),
49
- workspaceId: z.string().min(1).optional(),
50
- filename: z.string().min(1).max(255),
51
- contentB64: z.string().min(1),
52
- })
53
- .refine((v) => v.scope !== 'workspace' || !!v.workspaceId, {
54
- message: 'workspaceId is required for a workspace template',
55
- path: ['workspaceId'],
56
- });
57
- /**
58
- * The upload flow (B16): `POST /imports {templateTarget, filename, sizeBytes, sha256Hex}`, PUT the file, then
59
- * `POST /templates/import {importId}`. Scope and workspace come from the upload, never from this request.
60
- */
61
- export const templateImportByIdSchema = z.object({
62
- importId: z.string().min(1),
63
- });
64
- /** `GET /templates/:tid/export?format=` (only `fwtemplate` is built). */
65
- export const templateExportQuerySchema = z.object({
66
- format: z
67
- .enum(['fwtemplate', 'json', 'fadein-template', 'fdxt'])
68
- .default('fwtemplate'),
69
- version: z.coerce.number().int().min(1).optional(),
70
- });
@@ -1,102 +0,0 @@
1
- import { z } from 'zod';
2
- export const WORKSPACE_KINDS = ['personal', 'team'];
3
- /** Ordered by authority (spec 05 §5.2). */
4
- export const ROLES = [
5
- 'owner',
6
- 'admin',
7
- 'writer',
8
- 'commenter',
9
- 'viewer',
10
- ];
11
- export const workspaceKindSchema = z.enum(WORKSPACE_KINDS);
12
- export const roleSchema = z.enum(ROLES);
13
- // ─── Permissions (spec 05 §5.2) ─────────────────────────────────────────────
14
- /**
15
- * Every permission with the LOWEST role that holds it. The matrix is monotonic (a higher role has
16
- * everything a lower one has), so `ROLE_PERMISSIONS` is derived from this table.
17
- * Notes on the two permissions with a condition beyond the role:
18
- * - `share.manage` for a writer only applies to documents/projects they created (checked by the API).
19
- * - `project.trash` / `project.restore` for a writer only applies to projects they created.
20
- */
21
- export const PERMISSION_MIN_ROLE = {
22
- 'workspace.read': 'viewer',
23
- 'workspace.update': 'admin',
24
- 'workspace.aiToggle': 'owner',
25
- 'workspace.audit': 'admin',
26
- 'workspace.delete': 'owner',
27
- 'workspace.transfer': 'owner',
28
- 'workspace.leave': 'viewer',
29
- 'members.read': 'viewer',
30
- 'members.invite': 'admin',
31
- 'members.updateRole': 'admin',
32
- 'members.remove': 'admin',
33
- 'project.create': 'writer',
34
- 'project.read': 'viewer',
35
- 'project.update': 'writer',
36
- 'project.trash': 'writer',
37
- 'project.restore': 'writer',
38
- 'project.purge': 'admin',
39
- 'document.create': 'writer',
40
- 'document.read': 'viewer',
41
- 'document.export': 'viewer',
42
- 'document.edit': 'writer',
43
- 'document.comment': 'commenter',
44
- 'document.chat': 'commenter',
45
- 'document.activity': 'viewer',
46
- 'document.trash': 'writer',
47
- 'document.restore': 'writer',
48
- 'document.move': 'writer',
49
- 'document.lock': 'writer',
50
- 'document.aiExclude': 'writer',
51
- 'snapshot.create': 'writer',
52
- 'snapshot.open': 'writer',
53
- 'snapshot.fork': 'writer',
54
- 'snapshot.cherryPick': 'writer',
55
- 'snapshot.appendNote': 'writer',
56
- 'snapshot.read': 'viewer',
57
- 'snapshot.compare': 'viewer',
58
- 'snapshot.hideForSelf': 'viewer',
59
- 'version.restore': 'writer',
60
- 'version.restoreAsCopy': 'writer',
61
- 'share.manage': 'writer',
62
- 'assets.upload': 'writer',
63
- 'assets.link': 'writer',
64
- 'assets.read': 'viewer',
65
- 'ai.run': 'writer',
66
- 'ai.review': 'commenter',
67
- 'jobs.media': 'writer',
68
- 'templates.manage': 'writer',
69
- 'reports.run': 'viewer',
70
- 'apiKeys.use': 'viewer',
71
- };
72
- export const PERMISSIONS = Object.keys(PERMISSION_MIN_ROLE);
73
- /** 0 = owner ... 4 = viewer; a smaller number is more authority. */
74
- const RANK = {
75
- owner: 0,
76
- admin: 1,
77
- writer: 2,
78
- commenter: 3,
79
- viewer: 4,
80
- };
81
- /** True when `a` has at least the authority of `b`. */
82
- export const roleAtLeast = (a, b) => RANK[a] <= RANK[b];
83
- /** The higher-authority of the given roles (nulls ignored); null when there are none. */
84
- export function maxRole(...roles) {
85
- let best = null;
86
- for (const r of roles)
87
- if (r && (best === null || RANK[r] < RANK[best]))
88
- best = r;
89
- return best;
90
- }
91
- /** The lower-authority of two roles (used to cap an API key by its scope). */
92
- export const minRole = (a, b) => (RANK[a] >= RANK[b] ? a : b);
93
- export const ROLE_PERMISSIONS = Object.fromEntries(ROLES.map((role) => [
94
- role,
95
- PERMISSIONS.filter((p) => roleAtLeast(role, PERMISSION_MIN_ROLE[p])),
96
- ]));
97
- /** The only check routes call. */
98
- export const hasPermission = (role, permission) => roleAtLeast(role, PERMISSION_MIN_ROLE[permission]);
99
- /** Lowest role that holds `permission` (goes in a 403's `details.requiredRole`). */
100
- export const requiredRoleFor = (permission) => PERMISSION_MIN_ROLE[permission];
101
- /** Roles a share grant or a project/document invitation may carry: `owner` is never granted by share. */
102
- export const SHARE_ROLES = ['admin', 'writer', 'commenter', 'viewer'];
@@ -1,88 +0,0 @@
1
- const T = '2026-01-01T00:00:00.000Z';
2
- export function makeWorkspace(over = {}) {
3
- return {
4
- id: 'ws_test',
5
- kind: 'personal',
6
- name: 'Personal',
7
- avatarAssetId: null,
8
- defaultTemplateId: null,
9
- defaultLanguage: 'en',
10
- aiEnabled: true,
11
- allowPublicLinks: true,
12
- createdAt: T,
13
- updatedAt: T,
14
- ...over,
15
- };
16
- }
17
- export function makeProject(over = {}) {
18
- return {
19
- id: 'prj_test',
20
- workspaceId: 'ws_test',
21
- name: 'Untitled Project',
22
- description: null,
23
- color: null,
24
- coverAssetId: null,
25
- kind: 'feature',
26
- logline: null,
27
- documentCount: 0,
28
- createdAt: T,
29
- updatedAt: T,
30
- trashedAt: null,
31
- role: 'owner',
32
- folders: [],
33
- documents: [],
34
- ...over,
35
- };
36
- }
37
- export function makeDocumentMeta(over = {}) {
38
- return {
39
- id: 'doc_test',
40
- projectId: 'prj_test',
41
- workspaceId: 'ws_test',
42
- folderId: null,
43
- kind: 'script',
44
- title: 'Untitled Screenplay',
45
- logline: null,
46
- color: null,
47
- labels: [],
48
- season: null,
49
- episode: null,
50
- position: 0,
51
- language: 'en',
52
- templateId: null,
53
- templateVersion: null,
54
- epoch: 0,
55
- excludeFromAi: false,
56
- locked: false,
57
- pageCount: 0,
58
- sceneCount: 0,
59
- wordCount: 0,
60
- parentSnapshotId: null,
61
- createdBy: null,
62
- createdAt: T,
63
- updatedAt: T,
64
- lastEditedAt: null,
65
- trashedAt: null,
66
- ...over,
67
- };
68
- }
69
- export function makeSnapshotSummary(over = {}) {
70
- return {
71
- id: 'snap_test',
72
- documentId: 'doc_test',
73
- parentId: null,
74
- name: 'First draft',
75
- note: null,
76
- kind: 'manual',
77
- autoReason: null,
78
- stats: { pages: 0, scenes: 0, words: 0 },
79
- createdBy: null,
80
- createdOnDevice: null,
81
- createdAt: T,
82
- receivedAt: T,
83
- forkedDocumentIds: [],
84
- hiddenForMe: false,
85
- noteCount: 0,
86
- ...over,
87
- };
88
- }
@@ -1,107 +0,0 @@
1
- import { z } from 'zod';
2
- import { cursorQuerySchema } from '../api/pagination.js';
3
- /** Spec 03 §10.2 (canonical, kebab-case). */
4
- export const SNAPSHOT_AUTO_REASONS = [
5
- 'pre-ai-polish',
6
- 'pre-import',
7
- 'pre-template',
8
- 'pre-open',
9
- 'pre-restore',
10
- 'pre-cherry-pick-bulk',
11
- 'pre-clear-revisions',
12
- 'pre-delete-revision-set',
13
- 'pre-unlock-all-pages',
14
- 'pre-bulk-change-decision',
15
- 'pre-account-transfer',
16
- 'offline-edits',
17
- 'access-revoked',
18
- ];
19
- /** The only reasons a client may send itself. */
20
- export const CLIENT_AUTO_REASONS = ['offline-edits', 'access-revoked'];
21
- export const SNAPSHOT_KINDS = ['manual', 'auto'];
22
- export const VERSION_REASONS = [
23
- 'interval',
24
- 'session-end',
25
- 'pre-epoch',
26
- 'pre-restore',
27
- 'daily',
28
- 'reconnect',
29
- ];
30
- export const snapshotCreateSchema = z.object({
31
- /** Idempotency key, unique per document. */
32
- clientSnapshotId: z.string().min(1).max(64).optional(),
33
- name: z.string().trim().min(1).max(120),
34
- note: z.string().max(2000).optional(),
35
- kind: z.enum(SNAPSHOT_KINDS).optional(),
36
- autoReason: z.enum(SNAPSHOT_AUTO_REASONS).optional(),
37
- sourceEpoch: z.number().int().min(0).optional(),
38
- assumedParentId: z.string().nullable().optional(),
39
- createdAt: z.string().optional(),
40
- createdOnDevice: z.string().max(200).optional(),
41
- /**
42
- * Client-provided state for offline creation (spec 03 §10.2, spec 05 §6.11): exactly what the writer saw, uploaded through
43
- * `POST /uploads/state`. With it, `sourceEpoch` may be older than the live epoch (an offline-edits snapshot after an epoch bump).
44
- */
45
- state: z.object({ uploadKey: z.string().min(1).max(128) }).optional(),
46
- });
47
- export const snapshotOpenSchema = z.object({
48
- confirm: z.literal(true),
49
- dryRun: z.boolean().optional(),
50
- });
51
- export const snapshotForkSchema = z.object({
52
- title: z.string().trim().min(1).max(200),
53
- targetProjectId: z.string().optional(),
54
- });
55
- export const sourceRefSchema = z.discriminatedUnion('kind', [
56
- z.object({ kind: z.literal('live'), documentId: z.string() }),
57
- z.object({ kind: z.literal('snapshot'), snapshotId: z.string() }),
58
- z.object({
59
- kind: z.literal('version'),
60
- documentId: z.string(),
61
- versionId: z.string(),
62
- }),
63
- ]);
64
- // ─── B15: restore-as-copy, prefs, notes, comments, compare, history, presence ────────────────────────────────────
65
- /** `POST /documents/:did/versions/:vid/restore-as-copy` (spec 05 §6.10). Response: `DocumentMeta`. */
66
- export const versionRestoreAsCopySchema = z.object({
67
- title: z.string().trim().min(1).max(200),
68
- targetProjectId: z.string().optional(),
69
- });
70
- /** `PUT /snapshots/:sid/prefs`: per-user hide; the snapshot row never changes. */
71
- export const snapshotPrefsSchema = z.object({ hidden: z.boolean() });
72
- export const SNAPSHOT_NOTES_MAX = 200;
73
- export const snapshotNoteCreateSchema = z.object({
74
- body: z.string().trim().min(1).max(2000),
75
- });
76
- export const snapshotCommentAnchorSchema = z.object({
77
- elementId: z.string().min(1),
78
- offset: z.number().int().min(0),
79
- length: z.number().int().min(0),
80
- });
81
- export const snapshotCommentCreateSchema = z.object({
82
- anchor: snapshotCommentAnchorSchema,
83
- body: z.string().trim().min(1).max(10_000),
84
- /** User ids mentioned in the body (notifications are B12). */
85
- mentions: z.array(z.string().min(1)).max(50).optional(),
86
- });
87
- export const snapshotCommentResolveSchema = z.object({ resolved: z.boolean() });
88
- export const snapshotCommentsQuerySchema = cursorQuerySchema;
89
- // Compare (spec 03 §11.1)
90
- export const COMPARE_GRANULARITIES = ['scene', 'element', 'word'];
91
- export const TRACKED_CHANGE_MODES = [
92
- 'accept-all',
93
- 'reject-all',
94
- 'as-marked',
95
- ];
96
- export const compareRequestSchema = z.object({
97
- base: sourceRefSchema,
98
- target: sourceRefSchema,
99
- granularity: z.enum(COMPARE_GRANULARITIES).default('element'),
100
- ignoreFormatting: z.boolean().default(false),
101
- ignoreRevisionMarks: z.boolean().default(false),
102
- ignoreTrackedChanges: z.enum(TRACKED_CHANGE_MODES).default('as-marked'),
103
- });
104
- /** Above this many combined pages a compare is refused (spec 05 returns a `doc.compare` job; no runner exists, so 413 `COMPARE_TOO_LARGE`). */
105
- export const COMPARE_MAX_PAGES = 400;
106
- /** `GET /documents/:did/elements/:elementId/history`: newest first (spec 03 §14). */
107
- export const elementHistoryQuerySchema = cursorQuerySchema;