@yargote/artifacts-contracts 0.1.0-beta.6 → 0.1.0-beta.8
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 +5 -1
- package/package.json +1 -1
- package/src/http.d.ts +12 -0
- package/src/http.js +16 -3
- package/src/index.d.ts +56 -2
- package/src/index.js +108 -4
- package/src/workflow.js +2 -2
package/README.md
CHANGED
|
@@ -3,11 +3,15 @@
|
|
|
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
|
+
npm install @yargote/artifacts-contracts@0.1.0-beta.8
|
|
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.
|
|
10
10
|
|
|
11
|
+
`protectedResourceMetadataSchema` accepts standard protected-resource metadata and an optional `artifacts_rest_resource`. The relation names the exact absolute canonical REST OAuth resource URL. It may name the resource itself or one distinct REST resource. Metadata for the distinct resource may omit the relation or name itself, but cannot name another resource.
|
|
12
|
+
|
|
13
|
+
`slugSchema` validates page slugs such as `concepts/rag` without normalizing them. `readSchema` takes exactly one of `artifactId` or a `{ project, slug }` locator, and `readByIdSchema` takes only an ID. `ArtifactRead` and the HTTP summary schema include `slug`, and `ArtifactErrorCode` includes `slug_conflict`.
|
|
14
|
+
|
|
11
15
|
The existing `@artifacts/artifacts` root, `/contracts`, and `/workflow` exports re-export these implementations. Store and REST packages share one `ArtifactError` runtime.
|
|
12
16
|
|
|
13
17
|
This package is MIT licensed. The license applies to this package and does not license the private Artifacts application repository.
|
package/package.json
CHANGED
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>;
|
|
@@ -51,6 +52,7 @@ export declare const compactArtifactReadSchema: z.ZodObject<{
|
|
|
51
52
|
ownerId: z.ZodString;
|
|
52
53
|
headRevisionId: z.ZodString;
|
|
53
54
|
project: z.ZodNullable<z.ZodString>;
|
|
55
|
+
slug: z.ZodNullable<z.ZodString>;
|
|
54
56
|
artifactId: z.ZodString;
|
|
55
57
|
publication: z.ZodNullable<z.ZodObject<{
|
|
56
58
|
revisionId: z.ZodString;
|
|
@@ -74,6 +76,7 @@ export declare const compactArtifactReadSchema: z.ZodObject<{
|
|
|
74
76
|
text: "text";
|
|
75
77
|
html: "html";
|
|
76
78
|
markdown: "markdown";
|
|
79
|
+
design: "design";
|
|
77
80
|
}>;
|
|
78
81
|
utf8Bytes: z.ZodNumber;
|
|
79
82
|
}, z.core.$strict>;
|
|
@@ -95,6 +98,7 @@ export declare const artifactSummarySchema: z.ZodObject<{
|
|
|
95
98
|
headRevisionId: z.ZodString;
|
|
96
99
|
createdAt: z.ZodISODateTime;
|
|
97
100
|
project: z.ZodNullable<z.ZodString>;
|
|
101
|
+
slug: z.ZodNullable<z.ZodString>;
|
|
98
102
|
publication: z.ZodNullable<z.ZodObject<{
|
|
99
103
|
revisionId: z.ZodString;
|
|
100
104
|
version: z.ZodNumber;
|
|
@@ -108,6 +112,7 @@ export declare const listResultSchema: z.ZodObject<{
|
|
|
108
112
|
headRevisionId: z.ZodString;
|
|
109
113
|
createdAt: z.ZodISODateTime;
|
|
110
114
|
project: z.ZodNullable<z.ZodString>;
|
|
115
|
+
slug: z.ZodNullable<z.ZodString>;
|
|
111
116
|
publication: z.ZodNullable<z.ZodObject<{
|
|
112
117
|
revisionId: z.ZodString;
|
|
113
118
|
version: z.ZodNumber;
|
|
@@ -146,6 +151,7 @@ export declare const historyResultSchema: z.ZodObject<{
|
|
|
146
151
|
text: "text";
|
|
147
152
|
html: "html";
|
|
148
153
|
markdown: "markdown";
|
|
154
|
+
design: "design";
|
|
149
155
|
}>;
|
|
150
156
|
text: z.ZodString;
|
|
151
157
|
}, z.core.$strict>;
|
|
@@ -181,6 +187,7 @@ export declare const summaryHistoryResultSchema: z.ZodObject<{
|
|
|
181
187
|
text: "text";
|
|
182
188
|
html: "html";
|
|
183
189
|
markdown: "markdown";
|
|
190
|
+
design: "design";
|
|
184
191
|
}>;
|
|
185
192
|
utf8Bytes: z.ZodNumber;
|
|
186
193
|
}, z.core.$strict>;
|
|
@@ -204,5 +211,10 @@ export declare const httpErrorSchema: z.ZodObject<{
|
|
|
204
211
|
}, z.core.$strict>;
|
|
205
212
|
}, z.core.$strict>;
|
|
206
213
|
export declare function canonicalResource(value: string): string;
|
|
214
|
+
export declare const protectedResourceMetadataSchema: z.ZodObject<{
|
|
215
|
+
resource: z.ZodString;
|
|
216
|
+
authorization_servers: z.ZodArray<z.ZodString>;
|
|
217
|
+
artifacts_rest_resource: z.ZodOptional<z.ZodString>;
|
|
218
|
+
}, z.core.$strip>;
|
|
207
219
|
export declare function protectedResourceMetadataUrl(resource: string): URL;
|
|
208
220
|
export declare function restBase(resource: string): string;
|
package/src/http.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
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:
|
|
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 });
|
|
13
|
-
export const artifactSummarySchema = z.strictObject({ artifactId: identifier, title: z.string(), headRevisionId: identifier, createdAt: z.iso.datetime(), project: z.string().nullable(), publication: publicationSchema.nullable() });
|
|
13
|
+
export const artifactSummarySchema = z.strictObject({ artifactId: identifier, title: z.string(), headRevisionId: identifier, createdAt: z.iso.datetime(), project: z.string().nullable(), slug: z.string().nullable(), publication: publicationSchema.nullable() });
|
|
14
14
|
export const listResultSchema = z.strictObject({ items: z.array(artifactSummarySchema).max(100), nextCursor: identifier.nullable() });
|
|
15
15
|
export const projectSummarySchema = z.strictObject({ name: z.string(), artifactCount: z.number().int().positive() });
|
|
16
16
|
export const projectsResultSchema = z.strictObject({ items: z.array(projectSummarySchema) });
|
|
@@ -26,6 +26,19 @@ export function canonicalResource(value) {
|
|
|
26
26
|
throw new TypeError('Use the exact canonical resource URL without a trailing slash');
|
|
27
27
|
return value;
|
|
28
28
|
}
|
|
29
|
+
export const protectedResourceMetadataSchema = z.object({
|
|
30
|
+
resource: z.string(),
|
|
31
|
+
authorization_servers: z.array(z.string()).min(1),
|
|
32
|
+
artifacts_rest_resource: z.string().refine((value) => {
|
|
33
|
+
try {
|
|
34
|
+
canonicalResource(value);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}).optional(),
|
|
41
|
+
});
|
|
29
42
|
export function protectedResourceMetadataUrl(resource) {
|
|
30
43
|
const url = new URL(canonicalResource(resource));
|
|
31
44
|
return new URL(`/.well-known/oauth-protected-resource${url.pathname === '/' ? '' : url.pathname}`, url.origin);
|
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,7 +48,18 @@ 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;
|
|
62
|
+
export declare const slugSchema: z.ZodString;
|
|
27
63
|
export declare const createSchema: z.ZodObject<{
|
|
28
64
|
requestId: z.ZodString;
|
|
29
65
|
snapshot: z.ZodObject<{
|
|
@@ -37,6 +73,7 @@ export declare const createSchema: z.ZodObject<{
|
|
|
37
73
|
text: "text";
|
|
38
74
|
html: "html";
|
|
39
75
|
markdown: "markdown";
|
|
76
|
+
design: "design";
|
|
40
77
|
}>;
|
|
41
78
|
text: z.ZodString;
|
|
42
79
|
}, z.core.$strict>;
|
|
@@ -51,12 +88,13 @@ export declare const createSchema: z.ZodObject<{
|
|
|
51
88
|
}, z.core.$strict>>;
|
|
52
89
|
}, z.core.$strict>;
|
|
53
90
|
project: z.ZodOptional<z.ZodString>;
|
|
91
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
54
92
|
response: z.ZodOptional<z.ZodEnum<{
|
|
55
93
|
full: "full";
|
|
56
94
|
compact: "compact";
|
|
57
95
|
}>>;
|
|
58
96
|
}, z.core.$strict>;
|
|
59
|
-
export declare const
|
|
97
|
+
export declare const readByIdSchema: z.ZodObject<{
|
|
60
98
|
artifactId: z.ZodString;
|
|
61
99
|
revisionId: z.ZodOptional<z.ZodString>;
|
|
62
100
|
response: z.ZodOptional<z.ZodEnum<{
|
|
@@ -64,6 +102,16 @@ export declare const readSchema: z.ZodObject<{
|
|
|
64
102
|
compact: "compact";
|
|
65
103
|
}>>;
|
|
66
104
|
}, z.core.$strict>;
|
|
105
|
+
export declare const readSchema: z.ZodObject<{
|
|
106
|
+
artifactId: z.ZodOptional<z.ZodString>;
|
|
107
|
+
project: z.ZodOptional<z.ZodString>;
|
|
108
|
+
slug: z.ZodOptional<z.ZodString>;
|
|
109
|
+
revisionId: z.ZodOptional<z.ZodString>;
|
|
110
|
+
response: z.ZodOptional<z.ZodEnum<{
|
|
111
|
+
full: "full";
|
|
112
|
+
compact: "compact";
|
|
113
|
+
}>>;
|
|
114
|
+
}, z.core.$strict>;
|
|
67
115
|
export declare const updateSchema: z.ZodObject<{
|
|
68
116
|
requestId: z.ZodString;
|
|
69
117
|
artifactId: z.ZodString;
|
|
@@ -79,6 +127,7 @@ export declare const updateSchema: z.ZodObject<{
|
|
|
79
127
|
text: "text";
|
|
80
128
|
html: "html";
|
|
81
129
|
markdown: "markdown";
|
|
130
|
+
design: "design";
|
|
82
131
|
}>;
|
|
83
132
|
text: z.ZodString;
|
|
84
133
|
}, z.core.$strict>;
|
|
@@ -267,6 +316,7 @@ export type ArtifactSummary = {
|
|
|
267
316
|
headRevisionId: string;
|
|
268
317
|
createdAt: string;
|
|
269
318
|
project: string | null;
|
|
319
|
+
slug: string | null;
|
|
270
320
|
publication: Publication | null;
|
|
271
321
|
};
|
|
272
322
|
export type ProjectSummary = {
|
|
@@ -290,7 +340,7 @@ export type Archive = {
|
|
|
290
340
|
base64: string;
|
|
291
341
|
sha256: string;
|
|
292
342
|
};
|
|
293
|
-
export type ArtifactErrorCode = 'forbidden' | 'not_found' | 'conflict' | 'request_conflict' | 'invalid_grant';
|
|
343
|
+
export type ArtifactErrorCode = 'forbidden' | 'not_found' | 'conflict' | 'request_conflict' | 'slug_conflict' | 'invalid_grant';
|
|
294
344
|
export declare class ArtifactError extends Error {
|
|
295
345
|
readonly code: ArtifactErrorCode;
|
|
296
346
|
readonly details: Record<string, unknown>;
|
|
@@ -324,6 +374,7 @@ export declare const revisionSchema: z.ZodObject<{
|
|
|
324
374
|
text: "text";
|
|
325
375
|
html: "html";
|
|
326
376
|
markdown: "markdown";
|
|
377
|
+
design: "design";
|
|
327
378
|
}>;
|
|
328
379
|
text: z.ZodString;
|
|
329
380
|
}, z.core.$strict>;
|
|
@@ -343,6 +394,7 @@ export declare const artifactReadSchema: z.ZodObject<{
|
|
|
343
394
|
ownerId: z.ZodString;
|
|
344
395
|
headRevisionId: z.ZodString;
|
|
345
396
|
project: z.ZodNullable<z.ZodString>;
|
|
397
|
+
slug: z.ZodNullable<z.ZodString>;
|
|
346
398
|
publication: z.ZodNullable<z.ZodObject<{
|
|
347
399
|
revisionId: z.ZodString;
|
|
348
400
|
version: z.ZodNumber;
|
|
@@ -367,6 +419,7 @@ export declare const artifactReadSchema: z.ZodObject<{
|
|
|
367
419
|
text: "text";
|
|
368
420
|
html: "html";
|
|
369
421
|
markdown: "markdown";
|
|
422
|
+
design: "design";
|
|
370
423
|
}>;
|
|
371
424
|
text: z.ZodString;
|
|
372
425
|
}, z.core.$strict>;
|
|
@@ -405,6 +458,7 @@ export declare const sharedReadSchema: z.ZodObject<{
|
|
|
405
458
|
text: "text";
|
|
406
459
|
html: "html";
|
|
407
460
|
markdown: "markdown";
|
|
461
|
+
design: "design";
|
|
408
462
|
}>;
|
|
409
463
|
text: z.ZodString;
|
|
410
464
|
}, 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(
|
|
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,11 +76,60 @@ 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
|
-
|
|
28
|
-
export const
|
|
125
|
+
// The .md check aborts so a file-style name gets the suffix hint instead of the generic pattern message.
|
|
126
|
+
export const slugSchema = z.string().min(1).max(160)
|
|
127
|
+
.refine((slug) => !slug.endsWith('.md'), { message: 'Remove the .md suffix: a slug names a page, not a file', abort: true })
|
|
128
|
+
.regex(/^[a-z0-9]+(-[a-z0-9]+)*(\/[a-z0-9]+(-[a-z0-9]+)*)*$/, 'Use lowercase kebab-case segments separated by /, such as concepts/rag');
|
|
129
|
+
export const createSchema = z.strictObject({ requestId, snapshot: snapshotSchema, project: projectNameSchema.optional(), slug: slugSchema.optional(), response });
|
|
130
|
+
export const readByIdSchema = z.strictObject({ artifactId: identifier, revisionId: identifier.optional(), response });
|
|
131
|
+
export const readSchema = z.strictObject({ artifactId: identifier.optional(), project: projectNameSchema.optional(), slug: slugSchema.optional(), revisionId: identifier.optional(), response })
|
|
132
|
+
.refine((input) => (input.artifactId === undefined) !== (input.slug === undefined) && (input.project === undefined || input.slug !== undefined), 'Specify exactly one of artifactId or slug; project applies only with slug');
|
|
29
133
|
export const updateSchema = z.strictObject({ requestId, artifactId: identifier, expectedRevisionId: identifier, snapshot: snapshotSchema, response });
|
|
30
134
|
export const publishSchema = z.strictObject({ requestId, artifactId: identifier, revisionId: identifier, expectedPublicationVersion: z.number().int().nonnegative() });
|
|
31
135
|
export const deleteSchema = z.strictObject({ requestId, artifactId: identifier, expectedRevisionId: identifier, expectedPublicationVersion: z.number().int().nonnegative() });
|
|
@@ -69,6 +173,6 @@ export function decodeFile(file) {
|
|
|
69
173
|
}
|
|
70
174
|
export const publicationSchema = z.strictObject({ revisionId: identifier, version: z.number().int().positive(), url: z.url() });
|
|
71
175
|
export const revisionSchema = z.strictObject({ id: identifier, parentRevisionId: identifier.nullable(), sequence: z.number().int().positive(), authorUserId: identifier, authorClientId: identifier, createdAt: z.iso.datetime(), sha256: z.string().regex(/^[a-f0-9]{64}$/), snapshot: snapshotSchema });
|
|
72
|
-
export const artifactReadSchema = z.strictObject({ artifactId: identifier, ownerId: identifier, headRevisionId: identifier, project: z.string().nullable(), publication: publicationSchema.nullable(), revision: revisionSchema });
|
|
176
|
+
export const artifactReadSchema = z.strictObject({ artifactId: identifier, ownerId: identifier, headRevisionId: identifier, project: z.string().nullable(), slug: z.string().nullable(), publication: publicationSchema.nullable(), revision: revisionSchema });
|
|
73
177
|
export const sharedReadSchema = z.strictObject({ artifactId: identifier, ownerName: z.string().default('Usuario de Artifacts'), publication: publicationSchema, revision: revisionSchema.pick({ id: true, createdAt: true, sha256: true, snapshot: true }) });
|
|
74
178
|
export const publishResultSchema = publicationSchema.extend({ artifactId: identifier });
|
package/src/workflow.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { ArtifactError, artifactReadSchema, createSchema, decodeFile,
|
|
2
|
+
import { ArtifactError, artifactReadSchema, createSchema, decodeFile, readByIdSchema, snapshotSchema, updateSchema } from "./index.js";
|
|
3
3
|
import { canonicalJson, sha256 } from "./serialization.js";
|
|
4
4
|
async function summarizeFiles(files) {
|
|
5
5
|
return Promise.all([...files].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0).map(async ({ path, mediaType, encoding, data }) => {
|
|
@@ -57,7 +57,7 @@ export async function compareRevisions(before, after) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
const prepareEditSchema = z.strictObject({
|
|
60
|
-
artifactId:
|
|
60
|
+
artifactId: readByIdSchema.shape.artifactId,
|
|
61
61
|
requestId: createSchema.shape.requestId,
|
|
62
62
|
changes: z.strictObject({
|
|
63
63
|
metadata: snapshotSchema.shape.metadata.partial().optional(),
|