@yargote/artifacts-contracts 0.1.0-beta.6 → 0.1.0-beta.7

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.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  `@yargote/artifacts-contracts` contains strict domain schemas, types, `ArtifactError`, snapshot serialization, and pure retained-edit helpers. It has no database, ZIP, server, or CLI dependency. Node 24 or later is required.
4
4
 
5
5
  ```sh
6
- npm install @yargote/artifacts-contracts@0.1.0-beta.6
6
+ npm install @yargote/artifacts-contracts@0.1.0-beta.7
7
7
  ```
8
8
 
9
9
  The root export contains domain schemas and types. `/workflow` contains `prepareArtifactEdit`, revision comparison, and compact projections. `/serialization` contains canonical JSON and SHA-256. `/http` contains HTTP response schemas, identity, canonical resource validation, and route URL helpers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yargote/artifacts-contracts",
3
- "version": "0.1.0-beta.6",
3
+ "version": "0.1.0-beta.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Shared schemas and contracts for Artifacts clients.",
package/src/http.d.ts CHANGED
@@ -33,6 +33,7 @@ export declare const revisionSummarySchema: z.ZodObject<{
33
33
  text: "text";
34
34
  html: "html";
35
35
  markdown: "markdown";
36
+ design: "design";
36
37
  }>;
37
38
  utf8Bytes: z.ZodNumber;
38
39
  }, z.core.$strict>;
@@ -74,6 +75,7 @@ export declare const compactArtifactReadSchema: z.ZodObject<{
74
75
  text: "text";
75
76
  html: "html";
76
77
  markdown: "markdown";
78
+ design: "design";
77
79
  }>;
78
80
  utf8Bytes: z.ZodNumber;
79
81
  }, z.core.$strict>;
@@ -146,6 +148,7 @@ export declare const historyResultSchema: z.ZodObject<{
146
148
  text: "text";
147
149
  html: "html";
148
150
  markdown: "markdown";
151
+ design: "design";
149
152
  }>;
150
153
  text: z.ZodString;
151
154
  }, z.core.$strict>;
@@ -181,6 +184,7 @@ export declare const summaryHistoryResultSchema: z.ZodObject<{
181
184
  text: "text";
182
185
  html: "html";
183
186
  markdown: "markdown";
187
+ design: "design";
184
188
  }>;
185
189
  utf8Bytes: z.ZodNumber;
186
190
  }, z.core.$strict>;
package/src/http.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { z } from 'zod';
2
- import { artifactReadSchema, publicationSchema, revisionSchema, snapshotSchema } from "./index.js";
2
+ import { artifactReadSchema, contentKinds, publicationSchema, revisionSchema, snapshotSchema } from "./index.js";
3
3
  const identifier = z.string().min(1).max(128);
4
4
  const digest = z.string().regex(/^[a-f0-9]{64}$/);
5
5
  export const identitySchema = z.strictObject({ userId: identifier, sessionId: identifier, clientId: identifier, scopes: z.array(z.string()) });
6
6
  export const attachmentSummarySchema = z.strictObject({ path: z.string(), mediaType: z.string(), encoding: z.enum(['utf8', 'base64']), decodedBytes: z.number().int().nonnegative(), sha256: digest });
7
7
  export const revisionSummarySchema = revisionSchema.omit({ snapshot: true }).extend({
8
8
  metadata: snapshotSchema.shape.metadata.pick({ title: true, description: true }),
9
- content: z.strictObject({ kind: snapshotSchema.shape.content.shape.kind, utf8Bytes: z.number().int().nonnegative() }),
9
+ content: z.strictObject({ kind: z.enum(contentKinds), utf8Bytes: z.number().int().nonnegative() }),
10
10
  files: z.array(attachmentSummarySchema).max(32),
11
11
  });
12
12
  export const compactArtifactReadSchema = artifactReadSchema.omit({ revision: true }).extend({ revision: revisionSummarySchema });
package/src/index.d.ts CHANGED
@@ -1,4 +1,28 @@
1
1
  import { z } from 'zod';
2
+ export declare const contentKinds: readonly ["html", "markdown", "text", "design"];
3
+ export declare const canvasIndexSchema: z.ZodObject<{
4
+ v: z.ZodLiteral<1>;
5
+ boards: z.ZodPipe<z.ZodUnknown, z.ZodRecord<z.ZodString, z.ZodObject<{
6
+ x: z.ZodNumber;
7
+ y: z.ZodNumber;
8
+ w: z.ZodNumber;
9
+ h: z.ZodNumber;
10
+ title: z.ZodOptional<z.ZodString>;
11
+ page: z.ZodOptional<z.ZodString>;
12
+ }, z.core.$strict>>>;
13
+ order: z.ZodPipe<z.ZodArray<z.ZodUnknown>, z.ZodArray<z.ZodString>>;
14
+ notes: z.ZodOptional<z.ZodPipe<z.ZodUnknown, z.ZodRecord<z.ZodString, z.ZodObject<{
15
+ x: z.ZodNumber;
16
+ y: z.ZodNumber;
17
+ text: z.ZodString;
18
+ w: z.ZodOptional<z.ZodNumber>;
19
+ page: z.ZodOptional<z.ZodString>;
20
+ }, z.core.$strict>>>>;
21
+ pages: z.ZodOptional<z.ZodPipe<z.ZodArray<z.ZodUnknown>, z.ZodArray<z.ZodObject<{
22
+ id: z.ZodString;
23
+ name: z.ZodString;
24
+ }, z.core.$strict>>>>;
25
+ }, z.core.$strict>;
2
26
  export declare const snapshotSchema: z.ZodObject<{
3
27
  metadata: z.ZodObject<{
4
28
  title: z.ZodString;
@@ -10,6 +34,7 @@ export declare const snapshotSchema: z.ZodObject<{
10
34
  text: "text";
11
35
  html: "html";
12
36
  markdown: "markdown";
37
+ design: "design";
13
38
  }>;
14
39
  text: z.ZodString;
15
40
  }, z.core.$strict>;
@@ -23,6 +48,16 @@ export declare const snapshotSchema: z.ZodObject<{
23
48
  data: z.ZodString;
24
49
  }, z.core.$strict>>;
25
50
  }, z.core.$strict>;
51
+ export type CanvasIndex = z.infer<typeof canvasIndexSchema>;
52
+ export type CanvasBoard = CanvasIndex['boards'][string] & {
53
+ path: string;
54
+ html: string;
55
+ };
56
+ export type Canvas = {
57
+ index: CanvasIndex;
58
+ boards: CanvasBoard[];
59
+ };
60
+ export declare function parseCanvas(snapshot: Pick<Snapshot, 'content' | 'files'>): Canvas;
26
61
  export declare const projectNameSchema: z.ZodString;
27
62
  export declare const createSchema: z.ZodObject<{
28
63
  requestId: z.ZodString;
@@ -37,6 +72,7 @@ export declare const createSchema: z.ZodObject<{
37
72
  text: "text";
38
73
  html: "html";
39
74
  markdown: "markdown";
75
+ design: "design";
40
76
  }>;
41
77
  text: z.ZodString;
42
78
  }, z.core.$strict>;
@@ -79,6 +115,7 @@ export declare const updateSchema: z.ZodObject<{
79
115
  text: "text";
80
116
  html: "html";
81
117
  markdown: "markdown";
118
+ design: "design";
82
119
  }>;
83
120
  text: z.ZodString;
84
121
  }, z.core.$strict>;
@@ -324,6 +361,7 @@ export declare const revisionSchema: z.ZodObject<{
324
361
  text: "text";
325
362
  html: "html";
326
363
  markdown: "markdown";
364
+ design: "design";
327
365
  }>;
328
366
  text: z.ZodString;
329
367
  }, z.core.$strict>;
@@ -367,6 +405,7 @@ export declare const artifactReadSchema: z.ZodObject<{
367
405
  text: "text";
368
406
  html: "html";
369
407
  markdown: "markdown";
408
+ design: "design";
370
409
  }>;
371
410
  text: z.ZodString;
372
411
  }, z.core.$strict>;
@@ -405,6 +444,7 @@ export declare const sharedReadSchema: z.ZodObject<{
405
444
  text: "text";
406
445
  html: "html";
407
446
  markdown: "markdown";
447
+ design: "design";
408
448
  }>;
409
449
  text: z.ZodString;
410
450
  }, z.core.$strict>;
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  const identifier = z.string().min(1).max(128);
3
3
  const requestId = z.string().min(8).max(128);
4
+ export const contentKinds = ['html', 'markdown', 'text', 'design'];
4
5
  const fileSchema = z.strictObject({
5
6
  path: z.string().min(1).max(240).refine((path) => path === path.normalize('NFC') && !/[:\\%\x00-\x1f\x7f]/.test(path) && path.split('/').every((part) => part !== '' && part !== '.' && part !== '..'), 'Use a normalized relative file path'),
6
7
  mediaType: z.string().max(128).regex(/^[a-zA-Z0-9!#$&^_.+-]+\/[a-zA-Z0-9!#$&^_.+-]+$/),
@@ -10,9 +11,63 @@ const fileSchema = z.strictObject({
10
11
  if (file.encoding === 'base64' && Buffer.from(file.data, 'base64').toString('base64') !== file.data)
11
12
  context.addIssue({ code: 'custom', path: ['data'], message: 'Use canonical padded base64' });
12
13
  });
14
+ const canvasId = z.string().regex(/^[A-Za-z0-9_-]{1,40}$/, 'Use 1 to 40 letters, digits, underscores, or hyphens').refine((id) => id !== '__proto__', 'Use an id other than __proto__');
15
+ const boardPath = z.string().regex(/^boards\/[A-Za-z0-9_][A-Za-z0-9_.-]*\.html$/, 'Use a board path such as boards/Home.html');
16
+ const coordinate = z.number().min(-1_000_000).max(1_000_000);
17
+ const extent = z.number().min(40).max(8000);
18
+ const label = z.string().min(1).max(256);
19
+ // An oversized count or a bad key aborts before any entry is parsed, so an oversized collection reports one issue.
20
+ // Keys are read from the raw object because a zod record skips a __proto__ key without checking it.
21
+ function keyed(key, value, max, noun) {
22
+ return z.unknown().superRefine((input, context) => {
23
+ if (typeof input !== 'object' || input === null || Array.isArray(input))
24
+ return;
25
+ const keys = Object.keys(input);
26
+ if (keys.length > max) {
27
+ context.addIssue({ code: 'too_big', origin: 'object', maximum: max, inclusive: true, input, continue: false, message: `A canvas holds at most ${max} ${noun}` });
28
+ return;
29
+ }
30
+ for (const name of keys) {
31
+ const result = key.safeParse(name);
32
+ if (!result.success)
33
+ context.addIssue({ code: 'invalid_key', origin: 'record', issues: result.error.issues, input: name, path: [name], continue: false, message: result.error.issues[0].message });
34
+ }
35
+ }).pipe(z.record(z.string(), value));
36
+ }
37
+ export const canvasIndexSchema = z.strictObject({
38
+ v: z.literal(1),
39
+ boards: keyed(boardPath, z.strictObject({ x: coordinate, y: coordinate, w: extent, h: extent, title: label.optional(), page: canvasId.optional() }), 32, 'boards'),
40
+ order: z.array(z.unknown()).min(1, 'A canvas needs at least one board').max(32, { message: 'A canvas holds at most 32 boards', abort: true }).pipe(z.array(boardPath)),
41
+ notes: keyed(canvasId, z.strictObject({ x: coordinate, y: coordinate, text: z.string(), w: extent.optional(), page: canvasId.optional() }), 200, 'notes').optional(),
42
+ pages: z.array(z.unknown()).max(32, { message: 'A canvas holds at most 32 pages', abort: true }).pipe(z.array(z.strictObject({ id: canvasId, name: label }))).optional(),
43
+ }).superRefine((index, context) => {
44
+ const ordered = new Set();
45
+ index.order.forEach((path, position) => {
46
+ if (!Object.hasOwn(index.boards, path))
47
+ context.addIssue({ code: 'custom', path: ['order', position], message: `Canvas order names ${path}, which is not a board` });
48
+ else if (ordered.has(path))
49
+ context.addIssue({ code: 'custom', path: ['order', position], message: `Canvas order lists ${path} more than once` });
50
+ ordered.add(path);
51
+ });
52
+ for (const path of Object.keys(index.boards))
53
+ if (!ordered.has(path))
54
+ context.addIssue({ code: 'custom', path: ['order'], message: `Canvas order omits board ${path}` });
55
+ const pages = new Set();
56
+ index.pages?.forEach(({ id }, position) => {
57
+ if (pages.has(id))
58
+ context.addIssue({ code: 'custom', path: ['pages', position, 'id'], message: `Canvas page ${id} is declared more than once` });
59
+ pages.add(id);
60
+ });
61
+ for (const [path, board] of Object.entries(index.boards))
62
+ if (board.page !== undefined && !pages.has(board.page))
63
+ context.addIssue({ code: 'custom', path: ['boards', path, 'page'], message: `Canvas board ${path} names unknown page ${board.page}` });
64
+ for (const [id, note] of Object.entries(index.notes ?? {}))
65
+ if (note.page !== undefined && !pages.has(note.page))
66
+ context.addIssue({ code: 'custom', path: ['notes', id, 'page'], message: `Canvas note ${id} names unknown page ${note.page}` });
67
+ });
13
68
  export const snapshotSchema = z.strictObject({
14
69
  metadata: z.strictObject({ title: z.string().min(1).max(256), description: z.string().max(8192), custom: z.json().optional() }),
15
- content: z.strictObject({ kind: z.enum(['html', 'markdown', 'text']), text: z.string().max(1_048_576).refine((text) => text.isWellFormed(), 'Content must contain well-formed Unicode') }),
70
+ content: z.strictObject({ kind: z.enum(contentKinds), text: z.string().max(1_048_576).refine((text) => text.isWellFormed(), 'Content must contain well-formed Unicode') }),
16
71
  files: z.array(fileSchema).max(32),
17
72
  }).superRefine((snapshot, context) => {
18
73
  const paths = snapshot.files.map((file) => file.path);
@@ -21,7 +76,50 @@ export const snapshotSchema = z.strictObject({
21
76
  const bytes = Buffer.byteLength(JSON.stringify(snapshot.metadata)) + Buffer.byteLength(snapshot.content.text) + snapshot.files.reduce((total, file) => total + Buffer.byteLength(file.path) + Buffer.byteLength(file.mediaType) + decodeFile(file).length, 0);
22
77
  if (bytes > 1_048_576)
23
78
  context.addIssue({ code: 'custom', message: 'A revision must fit within 1 MiB of decoded content and metadata' });
79
+ // A request body can reach 8 MiB, so the index of an oversized design is not parsed.
80
+ if (snapshot.content.kind !== 'design' || bytes > 1_048_576)
81
+ return;
82
+ const canvas = readCanvas(snapshot);
83
+ if (!canvas.success)
84
+ for (const issue of canvas.issues)
85
+ context.addIssue({ ...issue });
24
86
  });
87
+ function readCanvas({ content, files }) {
88
+ let value;
89
+ try {
90
+ value = JSON.parse(content.text);
91
+ }
92
+ catch {
93
+ return { success: false, issues: [{ code: 'custom', path: ['content', 'text'], message: 'Design content must be a canvas index in JSON' }] };
94
+ }
95
+ const index = canvasIndexSchema.safeParse(value);
96
+ if (!index.success)
97
+ return { success: false, issues: index.error.issues.map((issue) => ({ ...issue, path: ['content', 'text', ...issue.path] })) };
98
+ const positions = new Map(files.map((file, position) => [file.path, position]));
99
+ const boards = [];
100
+ const issues = [];
101
+ for (const path of index.data.order) {
102
+ const position = positions.get(path);
103
+ if (position === undefined) {
104
+ issues.push({ code: 'custom', path: ['content', 'text', 'boards', path], message: `Canvas board ${path} has no matching file` });
105
+ continue;
106
+ }
107
+ const file = files[position];
108
+ if (file.mediaType !== 'text/html' || file.encoding !== 'utf8')
109
+ issues.push({ code: 'custom', path: ['files', position], message: `Canvas board ${path} must be a text/html file with utf8 encoding` });
110
+ else
111
+ boards.push({ ...index.data.boards[path], path, html: file.data });
112
+ }
113
+ return issues.length ? { success: false, issues } : { success: true, data: { index: index.data, boards } };
114
+ }
115
+ export function parseCanvas(snapshot) {
116
+ if (snapshot.content.kind !== 'design')
117
+ throw new TypeError('Only a design snapshot has a canvas');
118
+ const canvas = readCanvas(snapshot);
119
+ if (!canvas.success)
120
+ throw new z.ZodError(canvas.issues);
121
+ return canvas.data;
122
+ }
25
123
  export const projectNameSchema = z.string().normalize('NFC').trim().min(1).max(80).refine((name) => name.isWellFormed() && !/\p{Cc}/u.test(name), 'Use a project name without control characters');
26
124
  const response = z.enum(['full', 'compact']).optional();
27
125
  export const createSchema = z.strictObject({ requestId, snapshot: snapshotSchema, project: projectNameSchema.optional(), response });