@deveye/types 0.15.1 → 0.16.0

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 (73) hide show
  1. package/package.json +9 -6
  2. package/src/domain/device.ts +4 -29
  3. package/src/domain/featureRegistry.ts +40 -108
  4. package/src/domain/home.ts +40 -101
  5. package/src/domain/live.ts +36 -87
  6. package/src/domain/metrics.ts +10 -14
  7. package/src/domain/notifications.ts +39 -110
  8. package/src/domain/project.ts +3 -162
  9. package/src/domain/report.ts +46 -95
  10. package/src/domain/secrecy.ts +3 -5
  11. package/src/domain/sharing.ts +33 -70
  12. package/src/domain/syncProtocol.ts +5 -12
  13. package/src/domain/user.ts +8 -14
  14. package/src/domain/workspace.ts +0 -3
  15. package/src/domain/workspaceRole.ts +34 -82
  16. package/src/features/agent.ts +306 -0
  17. package/src/features/live.ts +17 -37
  18. package/src/features/notify.ts +19 -57
  19. package/src/features/registry.ts +7 -44
  20. package/src/features/secrecy.ts +4 -9
  21. package/src/features/sharing.ts +12 -26
  22. package/src/features/user.ts +6 -11
  23. package/src/features/workspace.ts +6 -11
  24. package/src/http/auth.ts +6 -13
  25. package/src/http/device.ts +13 -17
  26. package/src/http/status.ts +6 -10
  27. package/src/index.ts +29 -932
  28. package/src/protocol/agent.ts +40 -70
  29. package/src/protocol/envelope.ts +3 -10
  30. package/src/sdk/client-ambient.d.ts +244 -22
  31. package/src/sdk/client.ts +277 -17
  32. package/src/sdk/devb.ts +120 -0
  33. package/src/sdk/manifest.test.ts +96 -0
  34. package/src/sdk/manifest.ts +160 -25
  35. package/src/sdk/providers.ts +310 -5
  36. package/src/sdk/server.ts +483 -19
  37. package/src/sdk/testing.test.ts +62 -0
  38. package/src/sdk/testing.ts +416 -40
  39. package/src/utils/version.ts +5 -8
  40. package/src/domain/audience.ts +0 -549
  41. package/src/domain/backup.ts +0 -355
  42. package/src/domain/credential.ts +0 -55
  43. package/src/domain/database.ts +0 -467
  44. package/src/domain/deploy.ts +0 -231
  45. package/src/domain/finance.ts +0 -477
  46. package/src/domain/git.ts +0 -419
  47. package/src/domain/mail.ts +0 -394
  48. package/src/domain/note.ts +0 -202
  49. package/src/domain/password.ts +0 -36
  50. package/src/domain/projectBoard.ts +0 -130
  51. package/src/domain/projectChat.ts +0 -46
  52. package/src/domain/projectHistory.ts +0 -82
  53. package/src/domain/projectLink.ts +0 -87
  54. package/src/domain/projectPlan.ts +0 -68
  55. package/src/domain/sentinel.ts +0 -623
  56. package/src/domain/uptime.ts +0 -216
  57. package/src/features/audience.ts +0 -275
  58. package/src/features/backup.ts +0 -230
  59. package/src/features/database.ts +0 -461
  60. package/src/features/deploy.ts +0 -245
  61. package/src/features/device.ts +0 -292
  62. package/src/features/deviceFiles.ts +0 -83
  63. package/src/features/deviceLogs.ts +0 -36
  64. package/src/features/deviceTerminal.ts +0 -57
  65. package/src/features/finance.ts +0 -360
  66. package/src/features/git.ts +0 -368
  67. package/src/features/mail.ts +0 -374
  68. package/src/features/metrics.ts +0 -185
  69. package/src/features/note.ts +0 -189
  70. package/src/features/password.ts +0 -67
  71. package/src/features/project.ts +0 -709
  72. package/src/features/sentinel.ts +0 -233
  73. package/src/features/uptime.ts +0 -186
@@ -1,394 +0,0 @@
1
- import { z } from 'zod';
2
-
3
- /**
4
- * Mail: user-configured IMAP/SMTP mailboxes, read and sent through DevEye.
5
- *
6
- * Storage split (see `Docs/SECURITY_MODEL.md`): `securityTier`/`authMethod` and
7
- * everything the server needs to *plan* a sync or pick a cipher (enabled,
8
- * timestamps) live in clear columns. Everything identifying — display name,
9
- * address, credentials, message envelopes — is encrypted, but **which** cipher
10
- * (`ctx.secure` vs `ctx.secure.open`) is chosen per account by `securityTier`,
11
- * not fixed per feature like Uptime/Password are. An "open" account can be
12
- * synced/sent in the background (e.g. wired to Uptime alerts); a "guarded" one
13
- * only ever decrypts during a live, unlocked session. Message **bodies and
14
- * attachments are never persisted** — only envelopes (subject/from/date/flags)
15
- * are cached for fast listing; the body is fetched live from IMAP on open.
16
- */
17
-
18
- export const MAIL_DISPLAY_NAME_MAX_LENGTH = 80;
19
- export const MAIL_EMAIL_MAX_LENGTH = 320;
20
- export const MAIL_HOST_MAX_LENGTH = 255;
21
- export const MAIL_SUBJECT_MAX_LENGTH = 998; // RFC 5322 line-length ceiling
22
- export const MAIL_SNIPPET_MAX_LENGTH = 280;
23
-
24
- /** Upper bound on a `mail.messageSearch` query — a needle, not a document. */
25
- export const MAIL_SEARCH_QUERY_MAX_LENGTH = 128;
26
-
27
- export const MAIL_SYNC_INTERVAL_MIN_MINUTES = 1;
28
- export const MAIL_SYNC_INTERVAL_MAX_MINUTES = 180;
29
- export const MAIL_SYNC_INTERVAL_DEFAULT_MINUTES = 10;
30
-
31
- /**
32
- * Taille d'une page de messages — et, par voie de conséquence, de la fenêtre que
33
- * la synchro relit à chaque passage pour y réconcilier drapeaux et disparus.
34
- *
35
- * Les deux sont le même nombre à dessein : la relève de fond n'avance pas
36
- * seulement le haut de la boîte, elle garde honnête exactement ce que l'écran
37
- * affiche sans défiler. Au-delà, la dérive existe toujours mais ne se voit
38
- * qu'après un défilement, et se répare au changement de dossier.
39
- */
40
- export const MAIL_MESSAGE_PAGE_SIZE = 50;
41
-
42
- export const mailSecurityTierSchema = z.enum(['open', 'guarded']);
43
- export type MailSecurityTier = z.infer<typeof mailSecurityTierSchema>;
44
-
45
- /**
46
- * État de la dernière opération tentée sur une boîte, quelle qu'en soit
47
- * l'origine — relève de fond ou commande de l'utilisateur.
48
- *
49
- * Trois familles d'échec plutôt qu'un booléen, parce qu'elles n'appellent pas la
50
- * même chose : `auth` se répare en reconnectant le compte, `unreachable` se
51
- * répare tout seul quand le réseau revient, `error` demande de lire le message.
52
- * Le libellé exact reste dans `lastSyncError` — chiffré, lui, car il peut citer
53
- * un hôte ou une adresse.
54
- */
55
- export const mailAccountStatusSchema = z.enum(['ok', 'auth', 'unreachable', 'error']);
56
- export type MailAccountStatus = z.infer<typeof mailAccountStatusSchema>;
57
-
58
- export const mailAuthMethodSchema = z.enum(['password', 'oauth_google', 'oauth_microsoft']);
59
- export type MailAuthMethod = z.infer<typeof mailAuthMethodSchema>;
60
-
61
- export const mailOAuthProviderSchema = z.enum(['google', 'microsoft']);
62
- export type MailOAuthProvider = z.infer<typeof mailOAuthProviderSchema>;
63
-
64
- export const mailFolderSpecialUseSchema = z.enum([
65
- 'inbox',
66
- 'sent',
67
- 'drafts',
68
- 'trash',
69
- 'junk',
70
- 'archive',
71
- 'other'
72
- ]);
73
- export type MailFolderSpecialUse = z.infer<typeof mailFolderSpecialUseSchema>;
74
-
75
- export const mailProxyKindSchema = z.enum(['socks5', 'http']);
76
- export type MailProxyKind = z.infer<typeof mailProxyKindSchema>;
77
-
78
- /** Optional manual proxy for a single account — DevEye never provides one itself. */
79
- export const mailProxySchema = z.object({
80
- kind: mailProxyKindSchema,
81
- host: z.string().min(1).max(MAIL_HOST_MAX_LENGTH),
82
- port: z.number().int().min(1).max(65535),
83
- username: z.string().max(255).nullable(),
84
- password: z.string().max(255).nullable()
85
- });
86
- export type MailProxy = z.infer<typeof mailProxySchema>;
87
-
88
- const serverEndpointSchema = z.object({
89
- host: z.string().min(1).max(MAIL_HOST_MAX_LENGTH),
90
- port: z.number().int().min(1).max(65535),
91
- username: z.string().min(1).max(MAIL_EMAIL_MAX_LENGTH),
92
- password: z.string().min(1)
93
- });
94
-
95
- /**
96
- * What the user submits to create/edit a **password**-auth account. OAuth
97
- * accounts are never drafted this way — they're created by the
98
- * `mail.oauthStart` → provider redirect → callback flow, which never puts a
99
- * secret through this schema.
100
- */
101
- export const mailAccountDraftSchema = z.object({
102
- displayName: z.string().min(1).max(MAIL_DISPLAY_NAME_MAX_LENGTH),
103
- emailAddress: z.string().email().max(MAIL_EMAIL_MAX_LENGTH),
104
- securityTier: mailSecurityTierSchema,
105
- imap: serverEndpointSchema,
106
- smtp: serverEndpointSchema,
107
- proxy: mailProxySchema.nullable()
108
- });
109
- export type MailAccountDraft = z.infer<typeof mailAccountDraftSchema>;
110
-
111
- /**
112
- * Editing an existing password account, as opposed to creating one. Same shape
113
- * as a draft with two "leave it alone" affordances the add form has no use for:
114
- * a blank username/password keeps the stored one, and an omitted `proxy` keeps
115
- * the stored proxy. Both exist because the account DTO deliberately never
116
- * echoes a secret back, so the form has nothing to prefill — without them,
117
- * renaming a mailbox would mean retyping both sets of server credentials, and
118
- * saving anything at all would silently drop a configured proxy.
119
- */
120
- export const mailAccountEditSchema = mailAccountDraftSchema.extend({
121
- imap: serverEndpointSchema.extend({
122
- username: z.string().max(MAIL_EMAIL_MAX_LENGTH),
123
- password: z.string()
124
- }),
125
- smtp: serverEndpointSchema.extend({
126
- username: z.string().max(MAIL_EMAIL_MAX_LENGTH),
127
- password: z.string()
128
- }),
129
- proxy: mailProxySchema.nullable().optional()
130
- });
131
- export type MailAccountEdit = z.infer<typeof mailAccountEditSchema>;
132
-
133
- /**
134
- * Client-facing account DTO. Never carries a secret — not the IMAP/SMTP
135
- * password, not the OAuth tokens, not the proxy credentials. Editing a
136
- * password-auth account re-submits a full new `MailAccountDraft`; there is no
137
- * "reveal secret" endpoint, matching Password's discipline of never re-serving
138
- * a stored secret except through the one gated read path it actually needs.
139
- */
140
- export const mailAccountSchema = z.object({
141
- id: z.number().int().positive(),
142
- sortOrder: z.number().int().nonnegative(),
143
- displayName: z.string(),
144
- emailAddress: z.string(),
145
- securityTier: mailSecurityTierSchema,
146
- authMethod: mailAuthMethodSchema,
147
- imapHost: z.string(),
148
- imapPort: z.number().int(),
149
- smtpHost: z.string(),
150
- smtpPort: z.number().int(),
151
- proxyConfigured: z.boolean(),
152
- enabled: z.boolean(),
153
- lastSyncAt: z.number().int().nonnegative().nullable(),
154
- lastSyncError: z.string().nullable(),
155
- /**
156
- * État de la dernière opération, persistant jusqu'à ce qu'une réussite le
157
- * lève. C'est ce qui permet d'annoncer une boîte en panne dès l'ouverture,
158
- * sans attendre qu'un geste de l'utilisateur reproduise l'échec.
159
- */
160
- status: mailAccountStatusSchema,
161
- /** Quand l'échec courant a été constaté. `null` tant que tout va bien. */
162
- lastErrorAt: z.number().int().nonnegative().nullable(),
163
- /** True when an OAuth refresh failed and the user must reconnect. */
164
- needsReauth: z.boolean(),
165
- /**
166
- * How often the background loop re-checks this mailbox. Per account, not
167
- * per user: an archive box has no reason to be polled as hard as a work
168
- * one. Ignored for "guarded" accounts, which are never background-synced.
169
- * Real precision is floored by the server's own tick
170
- * (`MAIL_SYNC_TICK_SECONDS`), so a value below that just means "every tick".
171
- */
172
- syncIntervalMinutes: z
173
- .number()
174
- .int()
175
- .min(MAIL_SYNC_INTERVAL_MIN_MINUTES)
176
- .max(MAIL_SYNC_INTERVAL_MAX_MINUTES),
177
- /** A background sync tick is currently running for this account (in-memory, never persisted). */
178
- syncing: z.boolean(),
179
- /**
180
- * Folder-level fraction (0-1) of the in-progress sync — the finest
181
- * granularity available without instrumenting the IMAP fetch itself.
182
- * `null` while `syncing` means "just started, folder count not known yet";
183
- * always `null` when not syncing.
184
- */
185
- syncProgress: z.number().min(0).max(1).nullable(),
186
- created: z.number().int().nonnegative()
187
- });
188
- export type MailAccount = z.infer<typeof mailAccountSchema>;
189
-
190
- export const mailFolderSchema = z.object({
191
- id: z.number().int().positive(),
192
- accountId: z.number().int().positive(),
193
- name: z.string(),
194
- specialUse: mailFolderSpecialUseSchema,
195
- sortOrder: z.number().int().nonnegative(),
196
- unreadCount: z.number().int().nonnegative(),
197
- totalCount: z.number().int().nonnegative()
198
- });
199
- export type MailFolder = z.infer<typeof mailFolderSchema>;
200
-
201
- export const mailAddressSchema = z.object({
202
- name: z.string().nullable(),
203
- address: z.string()
204
- });
205
- export type MailAddress = z.infer<typeof mailAddressSchema>;
206
-
207
- export const mailFlagsSchema = z.object({
208
- seen: z.boolean(),
209
- flagged: z.boolean(),
210
- answered: z.boolean(),
211
- draft: z.boolean()
212
- });
213
- export type MailFlags = z.infer<typeof mailFlagsSchema>;
214
-
215
- export const mailAttachmentSchema = z.object({
216
- id: z.string(),
217
- filename: z.string(),
218
- mimeType: z.string(),
219
- size: z.number().int().nonnegative()
220
- });
221
- export type MailAttachment = z.infer<typeof mailAttachmentSchema>;
222
-
223
- /** Why a link was flagged — shown as a warning chip, never blocks the click. */
224
- export const mailLinkWarningReasonSchema = z.enum([
225
- 'text-href-mismatch',
226
- 'lookalike-domain',
227
- 'unsafe-scheme'
228
- ]);
229
- export type MailLinkWarningReason = z.infer<typeof mailLinkWarningReasonSchema>;
230
-
231
- export const mailSuspiciousLinkSchema = z.object({
232
- text: z.string(),
233
- href: z.string(),
234
- reason: mailLinkWarningReasonSchema
235
- });
236
- export type MailSuspiciousLink = z.infer<typeof mailSuspiciousLinkSchema>;
237
-
238
- /**
239
- * One header line as the server sent it, in receipt order. Kept raw and
240
- * unabridged — a `Received:` chain, an SPF/DKIM/DMARC verdict or a mailing-list
241
- * id is exactly the sort of thing you need when tracing a message, and any
242
- * summary would be the wrong one for some question.
243
- */
244
- export const mailHeaderSchema = z.object({ name: z.string(), value: z.string() });
245
- export type MailHeader = z.infer<typeof mailHeaderSchema>;
246
-
247
- /**
248
- * Page cursor for `mail.messageList`. Opaque to the client: hand back the one
249
- * from the previous page.
250
- *
251
- * A composite of `(date, id)` rather than a bare row id, because the list is
252
- * ordered by date and row ids are *insertion* order — which is not the same
253
- * thing. Backfill and remote search both write older messages into the cache
254
- * after newer ones, so an id-ordered list put freshly-fetched old mail at the
255
- * top of the mailbox. `id` is only the tiebreak between two identical dates,
256
- * so a page boundary can never repeat or skip a row.
257
- */
258
- export const mailMessageCursorSchema = z.object({
259
- date: z.number().int().nonnegative(),
260
- id: z.number().int().positive()
261
- });
262
- export type MailMessageCursor = z.infer<typeof mailMessageCursorSchema>;
263
-
264
- /** List-row shape: the cached envelope, nothing that requires a live IMAP fetch. */
265
- export const mailMessageSummarySchema = z.object({
266
- id: z.number().int().positive(),
267
- accountId: z.number().int().positive(),
268
- folderId: z.number().int().positive(),
269
- uid: z.number().int().positive(),
270
- subject: z.string(),
271
- from: mailAddressSchema.nullable(),
272
- to: z.array(mailAddressSchema),
273
- date: z.number().int().nonnegative(),
274
- flags: mailFlagsSchema,
275
- hasAttachments: z.boolean(),
276
- snippet: z.string()
277
- });
278
- export type MailMessageSummary = z.infer<typeof mailMessageSummarySchema>;
279
-
280
- /**
281
- * Full message. Fetched live from IMAP on `mail.messageGet` — never cached —
282
- * with `bodyHtml` already sanitized server-side (scripts/handlers/tracking CSS
283
- * stripped, remote images blocked unless `allowRemoteImages` was requested).
284
- */
285
- export const mailMessageSchema = mailMessageSummarySchema.extend({
286
- bodyHtml: z.string().nullable(),
287
- bodyText: z.string().nullable(),
288
- attachments: z.array(mailAttachmentSchema),
289
- remoteImagesBlocked: z.boolean(),
290
- /** Distinct hostnames of the remote images that were blocked, for a "trust these" picker. */
291
- blockedImageSources: z.array(z.string()),
292
- suspiciousLinks: z.array(mailSuspiciousLinkSchema),
293
- /** Every header line as received, in order. Never persisted — read live with the body. */
294
- headers: z.array(mailHeaderSchema),
295
- /** Size of the raw RFC822 source, in bytes. */
296
- sizeBytes: z.number().int().nonnegative(),
297
- /** IMAP path of the containing mailbox, which with `uid` identifies the message server-side. */
298
- folderPath: z.string()
299
- });
300
- export type MailMessage = z.infer<typeof mailMessageSchema>;
301
-
302
- export const mailBodyRenderModeSchema = z.enum(['embedded', 'raw']);
303
- export type MailBodyRenderMode = z.infer<typeof mailBodyRenderModeSchema>;
304
-
305
- export const mailSettingsSchema = z.object({
306
- /** Off by default everywhere — opt-in per scope decision, never pushed on the user. */
307
- externalScanEnabledDefault: z.boolean(),
308
- /** Hostnames whose remote images auto-load without a per-message prompt. */
309
- trustedImageDomains: z.array(z.string()),
310
- /**
311
- * `embedded` (default): the sanitized body renders inline, styled by DevEye
312
- * (no `<style>`/inline styles — see `src/mail/sanitize.ts`). `raw`: rendered
313
- * in an isolated sandboxed iframe on a white background with the message's
314
- * own styling preserved, for when the embedded look mangles a message.
315
- */
316
- bodyRenderMode: mailBodyRenderModeSchema
317
- });
318
- export type MailSettings = z.infer<typeof mailSettingsSchema>;
319
-
320
- /** Database row shapes (server-only). Mirror the columns exactly. */
321
- export interface MailAccountRow {
322
- id: number;
323
- user_id: number;
324
- workspace_id: number;
325
- sort_order: number;
326
- /** Encrypted (tier-dependent). */
327
- display_name_enc: string;
328
- /** Encrypted (tier-dependent). */
329
- email_address_enc: string;
330
- security_tier: MailSecurityTier;
331
- auth_method: MailAuthMethod;
332
- enabled: number;
333
- /** Background-sync cadence for this mailbox alone. */
334
- sync_interval_seconds: number;
335
- last_sync_at: number | null;
336
- /** Encrypted (tier-dependent), or null after a clean sync. */
337
- last_sync_error_enc: string | null;
338
- /**
339
- * En clair, à côté de `enabled` et `security_tier` : l'état doit être
340
- * lisible sans clé — par un ordonnanceur qui trie, par une requête de
341
- * diagnostic, et par l'interface d'un compte dont le message d'erreur,
342
- * lui, ne se déchiffre pas.
343
- */
344
- last_sync_status: MailAccountStatus;
345
- last_error_at: number | null;
346
- /**
347
- * Encrypted (tier-dependent) JSON blob — the only place secrets live:
348
- * `{ imap: {host,port,username,password}, smtp: {...}, proxy? }` for
349
- * `password` auth, or `{ provider, accessToken, refreshToken, expiresAt,
350
- * scope }` for OAuth. Never decrypted into the client-facing DTO.
351
- */
352
- credentials_enc: string;
353
- created: number;
354
- }
355
-
356
- export interface MailFolderRow {
357
- id: number;
358
- account_id: number;
359
- /** Clear: the IMAP mailbox path, required to address the mailbox during sync. */
360
- imap_path: string;
361
- /** Encrypted (account's tier) display name. */
362
- name_enc: string;
363
- special_use: MailFolderSpecialUse;
364
- sort_order: number;
365
- /** IMAP UIDVALIDITY — a change invalidates every cached message in this folder. */
366
- uid_validity: number | null;
367
- /** High-water mark: forward (incremental) sync fetches strictly after this. */
368
- last_seen_uid: number | null;
369
- /** Low-water mark: backward (backfill) sync fetches strictly before this. NULL until the first sync/backfill sets it. */
370
- first_seen_uid: number | null;
371
- unread_count: number;
372
- total_count: number;
373
- }
374
-
375
- export interface MailMessageRow {
376
- id: number;
377
- folder_id: number;
378
- uid: number;
379
- /** Encrypted (account's tier) `{ subject, from, to, snippet }`. Body is never stored. */
380
- envelope_enc: string;
381
- date: number;
382
- seen: number;
383
- flagged: number;
384
- answered: number;
385
- has_attachments: number;
386
- }
387
-
388
- export interface MailSettingsRow {
389
- workspace_id: number;
390
- external_scan_enabled_default: number;
391
- /** JSON-encoded string array, or null when empty. Not encrypted — hostnames, not secrets. */
392
- trusted_image_domains: string | null;
393
- body_render_mode: MailBodyRenderMode;
394
- }
@@ -1,202 +0,0 @@
1
- import { z } from 'zod';
2
-
3
- /**
4
- * A note's body is a modular list of typed blocks rather than free text. This
5
- * keeps the editor "à la Apple" (one clean surface) while still supporting
6
- * paragraphs and checklists in any order:
7
- * - `text` → a paragraph.
8
- * - `check` → a checklist item, with its own `done` state.
9
- *
10
- * - `bullet` → a bulleted (unordered) list item.
11
- * - `number` → a numbered (ordered) list item; its visible index is derived
12
- * from its position in the run of consecutive `number` blocks (not stored).
13
- * - `heading` → a section title, `level` 1 (largest) to 5 (smallest).
14
- * - `divider` → a horizontal separator; carries no text.
15
- *
16
- * Inline emphasis (bold `**`, italic `*`, underline `__`, strikethrough `~~`)
17
- * is kept as markdown markers inside a block's `text`, not as separate blocks.
18
- *
19
- * The shape is intentionally small and forward-compatible: adding a new block
20
- * kind later (heading, divider, …) only widens this union.
21
- */
22
- /**
23
- * Named colour palette shared by the two colouring features:
24
- * - inline text colour, carried as `{c:name}…{/c}` markers inside a block's
25
- * `text` (so it lives in the freeform string, not the schema);
26
- * - the block-level `color` below, which tints a marker (bullet dot, ordinal,
27
- * checkbox, divider rule).
28
- *
29
- * A *named* palette (not a free hex) keeps the stored value tied to a theme
30
- * token (`--note-<name>`), so colours stay coherent with the app's design and
31
- * adapt if the palette is retuned. Widen this enum to add a colour.
32
- */
33
- export const noteColorSchema = z.enum(['red', 'orange', 'yellow', 'green', 'blue', 'purple']);
34
-
35
- export type NoteColor = z.infer<typeof noteColorSchema>;
36
-
37
- export const noteTextBlockSchema = z.object({
38
- type: z.literal('text'),
39
- text: z.string()
40
- });
41
-
42
- export const noteCheckBlockSchema = z.object({
43
- type: z.literal('check'),
44
- text: z.string(),
45
- done: z.boolean(),
46
- /** Optional tint for the checkbox marker (theme token `--note-<color>`). */
47
- color: noteColorSchema.optional()
48
- });
49
-
50
- export const noteBulletBlockSchema = z.object({
51
- type: z.literal('bullet'),
52
- text: z.string(),
53
- /** Optional tint for the bullet dot (theme token `--note-<color>`). */
54
- color: noteColorSchema.optional()
55
- });
56
-
57
- export const noteNumberBlockSchema = z.object({
58
- type: z.literal('number'),
59
- text: z.string(),
60
- /** Optional tint for the ordinal marker (theme token `--note-<color>`). */
61
- color: noteColorSchema.optional()
62
- });
63
-
64
- export const noteHeadingBlockSchema = z.object({
65
- type: z.literal('heading'),
66
- text: z.string(),
67
- /** Heading level, 1 (largest) to 5 (smallest). */
68
- level: z.number().int().min(1).max(5)
69
- });
70
-
71
- export const noteDividerBlockSchema = z.object({
72
- type: z.literal('divider'),
73
- /** Optional tint for the horizontal rule (theme token `--note-<color>`). */
74
- color: noteColorSchema.optional()
75
- });
76
-
77
- export const noteBlockSchema = z.discriminatedUnion('type', [
78
- noteTextBlockSchema,
79
- noteCheckBlockSchema,
80
- noteBulletBlockSchema,
81
- noteNumberBlockSchema,
82
- noteHeadingBlockSchema,
83
- noteDividerBlockSchema
84
- ]);
85
-
86
- export type NoteTextBlock = z.infer<typeof noteTextBlockSchema>;
87
- export type NoteCheckBlock = z.infer<typeof noteCheckBlockSchema>;
88
- export type NoteBulletBlock = z.infer<typeof noteBulletBlockSchema>;
89
- export type NoteNumberBlock = z.infer<typeof noteNumberBlockSchema>;
90
- export type NoteHeadingBlock = z.infer<typeof noteHeadingBlockSchema>;
91
- export type NoteDividerBlock = z.infer<typeof noteDividerBlockSchema>;
92
- export type NoteBlock = z.infer<typeof noteBlockSchema>;
93
-
94
- /** Upper bounds, enforced both client- and server-side, to keep rows sane. */
95
- export const NOTE_TITLE_MAX_LENGTH = 200;
96
- export const NOTE_FOLDER_MAX_LENGTH = 80;
97
- export const NOTE_BLOCK_TEXT_MAX_LENGTH = 5_000;
98
- export const NOTE_MAX_BLOCKS = 500;
99
-
100
- /**
101
- * A folder is a first-class, server-persisted bucket. Its `name` is stored
102
- * encrypted server-side (with the open key, so the folder tree is readable
103
- * without a password); only the linkage (`folderId` on notes) is kept in clear.
104
- * `id` is stable across renames, so notes reference it directly.
105
- */
106
- export const noteFolderSchema = z.object({
107
- id: z.number().int().positive(),
108
- name: z.string().max(NOTE_FOLDER_MAX_LENGTH),
109
- /** Rank within the user's folders; lower comes first. Manually reorderable. */
110
- sortOrder: z.number().int().nonnegative()
111
- });
112
-
113
- export type NoteFolder = z.infer<typeof noteFolderSchema>;
114
-
115
- /**
116
- * A full note as exchanged with the client. `folderId` references a
117
- * {@link noteFolderSchema}; `null` means "no folder" (Sans dossier).
118
- *
119
- * A **private** note is encrypted with the password-wrapped DEK, so reading or
120
- * writing it requires the session to be unlocked; a regular note is encrypted
121
- * with the user's open key, which the server can always resolve — that's what
122
- * lets the feature open without any prompt.
123
- */
124
- export const noteSchema = z.object({
125
- id: z.number().int().nonnegative(),
126
- title: z.string().max(NOTE_TITLE_MAX_LENGTH),
127
- folderId: z.number().int().positive().nullable(),
128
- blocks: z.array(noteBlockSchema).max(NOTE_MAX_BLOCKS),
129
- /** Rank within its folder; lower comes first. Only drag & drop changes it. */
130
- sortOrder: z.number().int().nonnegative(),
131
- /** True when the note is encrypted with the password-protected key. */
132
- private: z.boolean(),
133
- /** Epoch seconds; set by the server, surfaced for sorting/display. */
134
- updated: z.number().int().nonnegative(),
135
- /** Epoch seconds the note was first created. */
136
- created: z.number().int().nonnegative()
137
- });
138
-
139
- export type Note = z.infer<typeof noteSchema>;
140
-
141
- /**
142
- * Lightweight list variant. A private note listed while the session is locked
143
- * comes back **masked** — metadata only, no `title`/`preview` — so the UI can
144
- * render a padlock placeholder without the body ever being decrypted.
145
- */
146
- export const noteSummarySchema = z.object({
147
- id: z.number().int().nonnegative(),
148
- title: z.string(),
149
- folderId: z.number().int().positive().nullable(),
150
- /** Rank within its folder; lower comes first. Only drag & drop changes it. */
151
- sortOrder: z.number().int().nonnegative(),
152
- /** Present only when the body is readable; absent for masked notes. */
153
- preview: z.string().optional(),
154
- /** Total checklist items / how many are done — for an at-a-glance summary. */
155
- checkTotal: z.number().int().nonnegative(),
156
- checkDone: z.number().int().nonnegative(),
157
- /** True when the note is encrypted with the password-protected key. */
158
- private: z.boolean(),
159
- /** True when the body stayed encrypted for this response (private + locked). */
160
- masked: z.boolean(),
161
- /** Epoch seconds the note was archived, or `null` while it is active. */
162
- archivedAt: z.number().int().nonnegative().nullable(),
163
- updated: z.number().int().nonnegative(),
164
- /** Epoch seconds the note was first created. */
165
- created: z.number().int().nonnegative()
166
- });
167
-
168
- export type NoteSummary = z.infer<typeof noteSummarySchema>;
169
-
170
- export interface NoteRow {
171
- id: number;
172
- user_id: number;
173
- workspace_id: number | null;
174
- folder_id: number | null;
175
- /**
176
- * Encrypted JSON payload (title + blocks), keyed by the private DEK when
177
- * `is_private`, by the user's open DEK otherwise.
178
- */
179
- content: string;
180
- /** Manual rank within its folder; lower comes first. */
181
- sort_order: number;
182
- /** 1 when the body is encrypted with the password-protected key. */
183
- is_private: number;
184
- /**
185
- * Epoch seconds the note was archived; `NULL` while active. Deleting a note
186
- * archives it — only an already-archived note can be destroyed for good.
187
- */
188
- archived_at: number | null;
189
- updated: number;
190
- created: number;
191
- }
192
-
193
- export interface NoteFolderRow {
194
- id: number;
195
- user_id: number;
196
- workspace_id: number | null;
197
- /** Encrypted JSON payload (`{ name }`). */
198
- content: string;
199
- /** Manual rank within the user's folders; lower comes first. */
200
- sort_order: number;
201
- created: number;
202
- }
@@ -1,36 +0,0 @@
1
- import { z } from 'zod';
2
-
3
- export const passwordStatusSchema = z.enum(['active', 'inactive', 'none']);
4
- export type PasswordStatus = z.infer<typeof passwordStatusSchema>;
5
-
6
- export const passwordEntrySchema = z.object({
7
- id: z.number().int().nonnegative(),
8
- category: z.string(),
9
- service: z.string(),
10
- email: z.string(),
11
- password: z.string(),
12
- status: passwordStatusSchema
13
- });
14
-
15
- export type PasswordEntry = z.infer<typeof passwordEntrySchema>;
16
-
17
- /**
18
- * Variant without the secret payload, used when listing entries before unlock.
19
- * `hasPassword` tells the UI whether a (still hidden) secret actually exists, so
20
- * a genuinely empty password renders as a blank cell instead of fake dots with
21
- * reveal/copy controls that would yield nothing.
22
- */
23
- export const passwordEntryMaskedSchema = passwordEntrySchema.extend({
24
- password: z.literal(''),
25
- hasPassword: z.boolean()
26
- });
27
-
28
- export type PasswordEntryMasked = z.infer<typeof passwordEntryMaskedSchema>;
29
-
30
- export interface PasswordRow {
31
- id: number;
32
- user_id: number;
33
- workspace_id: number | null;
34
- content: string;
35
- date: number;
36
- }