@yargote/artifacts-contracts 0.0.0-stage → 0.1.0-beta.10

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yerson Argote
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,21 @@
1
- # Temporary Holding Version
1
+ # Artifacts contracts
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+
5
+ ```sh
6
+ npm install @yargote/artifacts-contracts@0.1.0-beta.10
7
+ ```
8
+
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
+
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. `moveSchema` takes `project`, `slug`, or both, each optional and nullable. `moveResultSchema` is unchanged. `ArtifactRead` and the HTTP summary schema include `slug`, and `ArtifactErrorCode` includes `slug_conflict`.
14
+
15
+ `searchSchema` takes a trimmed `query` of 1 to 256 characters, optional `project`, `limit` from 1 to 25 with default 10, and an opaque decimal `cursor`. `searchResultSchema` holds `items` of `searchHitSchema`, `{ artifactId, project, slug, title, snippet, rank }`, and a nullable `nextCursor`.
16
+
17
+ The existing `@artifacts/artifacts` root, `/contracts`, and `/workflow` exports re-export these implementations. Store and REST packages share one `ArtifactError` runtime.
18
+
19
+ This package is MIT licensed. The license applies to this package and does not license the private Artifacts application repository.
20
+
21
+ `shareSchema` accepts optional `notify` and `requireNew: true` without defaults. `shareResultSchema` remains the strict five-field grant shape used by lists. `shareCommandResultSchema` adds an optional immutable notification disposition for commands. Plain and explicit-false requests return the original grant shape. An absent flag and an explicit false remain distinct receipt arguments.
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@yargote/artifacts-contracts",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-beta.10",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Shared schemas and contracts for Artifacts clients.",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/yersonargotev/artifacts.git",
11
+ "directory": "packages/contracts"
12
+ },
13
+ "homepage": "https://github.com/yersonargotev",
14
+ "bugs": {
15
+ "url": "https://github.com/yersonargotev"
16
+ },
17
+ "engines": {
18
+ "node": ">=24"
19
+ },
20
+ "exports": {
21
+ ".": "./src/index.js",
22
+ "./workflow": "./src/workflow.js",
23
+ "./serialization": "./src/serialization.js",
24
+ "./http": "./src/http.js"
25
+ },
26
+ "files": [
27
+ "src",
28
+ "README.md",
29
+ "LICENSE"
30
+ ],
31
+ "dependencies": {
32
+ "zod": "4.6.5"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "tag": "beta",
37
+ "provenance": false
38
+ }
39
+ }
package/src/http.d.ts ADDED
@@ -0,0 +1,220 @@
1
+ import { z } from 'zod';
2
+ export declare const identitySchema: z.ZodObject<{
3
+ userId: z.ZodString;
4
+ sessionId: z.ZodString;
5
+ clientId: z.ZodString;
6
+ scopes: z.ZodArray<z.ZodString>;
7
+ }, z.core.$strict>;
8
+ export type Identity = z.infer<typeof identitySchema>;
9
+ export declare const attachmentSummarySchema: z.ZodObject<{
10
+ path: z.ZodString;
11
+ mediaType: z.ZodString;
12
+ encoding: z.ZodEnum<{
13
+ utf8: "utf8";
14
+ base64: "base64";
15
+ }>;
16
+ decodedBytes: z.ZodNumber;
17
+ sha256: z.ZodString;
18
+ }, z.core.$strict>;
19
+ export declare const revisionSummarySchema: z.ZodObject<{
20
+ id: z.ZodString;
21
+ createdAt: z.ZodISODateTime;
22
+ parentRevisionId: z.ZodNullable<z.ZodString>;
23
+ sequence: z.ZodNumber;
24
+ authorUserId: z.ZodString;
25
+ authorClientId: z.ZodString;
26
+ sha256: z.ZodString;
27
+ metadata: z.ZodObject<{
28
+ description: z.ZodString;
29
+ title: z.ZodString;
30
+ }, z.core.$strict>;
31
+ content: z.ZodObject<{
32
+ kind: z.ZodEnum<{
33
+ text: "text";
34
+ html: "html";
35
+ markdown: "markdown";
36
+ design: "design";
37
+ }>;
38
+ utf8Bytes: z.ZodNumber;
39
+ }, z.core.$strict>;
40
+ files: z.ZodArray<z.ZodObject<{
41
+ path: z.ZodString;
42
+ mediaType: z.ZodString;
43
+ encoding: z.ZodEnum<{
44
+ utf8: "utf8";
45
+ base64: "base64";
46
+ }>;
47
+ decodedBytes: z.ZodNumber;
48
+ sha256: z.ZodString;
49
+ }, z.core.$strict>>;
50
+ }, z.core.$strict>;
51
+ export declare const compactArtifactReadSchema: z.ZodObject<{
52
+ ownerId: z.ZodString;
53
+ headRevisionId: z.ZodString;
54
+ project: z.ZodNullable<z.ZodString>;
55
+ slug: z.ZodNullable<z.ZodString>;
56
+ artifactId: z.ZodString;
57
+ publication: z.ZodNullable<z.ZodObject<{
58
+ revisionId: z.ZodString;
59
+ version: z.ZodNumber;
60
+ url: z.ZodURL;
61
+ }, z.core.$strict>>;
62
+ revision: z.ZodObject<{
63
+ id: z.ZodString;
64
+ createdAt: z.ZodISODateTime;
65
+ parentRevisionId: z.ZodNullable<z.ZodString>;
66
+ sequence: z.ZodNumber;
67
+ authorUserId: z.ZodString;
68
+ authorClientId: z.ZodString;
69
+ sha256: z.ZodString;
70
+ metadata: z.ZodObject<{
71
+ description: z.ZodString;
72
+ title: z.ZodString;
73
+ }, z.core.$strict>;
74
+ content: z.ZodObject<{
75
+ kind: z.ZodEnum<{
76
+ text: "text";
77
+ html: "html";
78
+ markdown: "markdown";
79
+ design: "design";
80
+ }>;
81
+ utf8Bytes: z.ZodNumber;
82
+ }, z.core.$strict>;
83
+ files: z.ZodArray<z.ZodObject<{
84
+ path: z.ZodString;
85
+ mediaType: z.ZodString;
86
+ encoding: z.ZodEnum<{
87
+ utf8: "utf8";
88
+ base64: "base64";
89
+ }>;
90
+ decodedBytes: z.ZodNumber;
91
+ sha256: z.ZodString;
92
+ }, z.core.$strict>>;
93
+ }, z.core.$strict>;
94
+ }, z.core.$strict>;
95
+ export declare const artifactSummarySchema: z.ZodObject<{
96
+ artifactId: z.ZodString;
97
+ title: z.ZodString;
98
+ headRevisionId: z.ZodString;
99
+ createdAt: z.ZodISODateTime;
100
+ project: z.ZodNullable<z.ZodString>;
101
+ slug: z.ZodNullable<z.ZodString>;
102
+ publication: z.ZodNullable<z.ZodObject<{
103
+ revisionId: z.ZodString;
104
+ version: z.ZodNumber;
105
+ url: z.ZodURL;
106
+ }, z.core.$strict>>;
107
+ }, z.core.$strict>;
108
+ export declare const listResultSchema: z.ZodObject<{
109
+ items: z.ZodArray<z.ZodObject<{
110
+ artifactId: z.ZodString;
111
+ title: z.ZodString;
112
+ headRevisionId: z.ZodString;
113
+ createdAt: z.ZodISODateTime;
114
+ project: z.ZodNullable<z.ZodString>;
115
+ slug: z.ZodNullable<z.ZodString>;
116
+ publication: z.ZodNullable<z.ZodObject<{
117
+ revisionId: z.ZodString;
118
+ version: z.ZodNumber;
119
+ url: z.ZodURL;
120
+ }, z.core.$strict>>;
121
+ }, z.core.$strict>>;
122
+ nextCursor: z.ZodNullable<z.ZodString>;
123
+ }, z.core.$strict>;
124
+ export declare const projectSummarySchema: z.ZodObject<{
125
+ name: z.ZodString;
126
+ artifactCount: z.ZodNumber;
127
+ }, z.core.$strict>;
128
+ export declare const projectsResultSchema: z.ZodObject<{
129
+ items: z.ZodArray<z.ZodObject<{
130
+ name: z.ZodString;
131
+ artifactCount: z.ZodNumber;
132
+ }, z.core.$strict>>;
133
+ }, z.core.$strict>;
134
+ export declare const historyResultSchema: z.ZodObject<{
135
+ items: z.ZodArray<z.ZodObject<{
136
+ id: z.ZodString;
137
+ parentRevisionId: z.ZodNullable<z.ZodString>;
138
+ sequence: z.ZodNumber;
139
+ authorUserId: z.ZodString;
140
+ authorClientId: z.ZodString;
141
+ createdAt: z.ZodISODateTime;
142
+ sha256: z.ZodString;
143
+ snapshot: z.ZodObject<{
144
+ metadata: z.ZodObject<{
145
+ title: z.ZodString;
146
+ description: z.ZodString;
147
+ custom: z.ZodOptional<z.ZodJSONSchema>;
148
+ }, z.core.$strict>;
149
+ content: z.ZodObject<{
150
+ kind: z.ZodEnum<{
151
+ text: "text";
152
+ html: "html";
153
+ markdown: "markdown";
154
+ design: "design";
155
+ }>;
156
+ text: z.ZodString;
157
+ }, z.core.$strict>;
158
+ files: z.ZodArray<z.ZodObject<{
159
+ path: z.ZodString;
160
+ mediaType: z.ZodString;
161
+ encoding: z.ZodEnum<{
162
+ utf8: "utf8";
163
+ base64: "base64";
164
+ }>;
165
+ data: z.ZodString;
166
+ }, z.core.$strict>>;
167
+ }, z.core.$strict>;
168
+ }, z.core.$strict>>;
169
+ nextBeforeSequence: z.ZodNullable<z.ZodNumber>;
170
+ }, z.core.$strict>;
171
+ export declare const summaryHistoryResultSchema: z.ZodObject<{
172
+ nextBeforeSequence: z.ZodNullable<z.ZodNumber>;
173
+ items: z.ZodArray<z.ZodObject<{
174
+ id: z.ZodString;
175
+ createdAt: z.ZodISODateTime;
176
+ parentRevisionId: z.ZodNullable<z.ZodString>;
177
+ sequence: z.ZodNumber;
178
+ authorUserId: z.ZodString;
179
+ authorClientId: z.ZodString;
180
+ sha256: z.ZodString;
181
+ metadata: z.ZodObject<{
182
+ description: z.ZodString;
183
+ title: z.ZodString;
184
+ }, z.core.$strict>;
185
+ content: z.ZodObject<{
186
+ kind: z.ZodEnum<{
187
+ text: "text";
188
+ html: "html";
189
+ markdown: "markdown";
190
+ design: "design";
191
+ }>;
192
+ utf8Bytes: z.ZodNumber;
193
+ }, z.core.$strict>;
194
+ files: z.ZodArray<z.ZodObject<{
195
+ path: z.ZodString;
196
+ mediaType: z.ZodString;
197
+ encoding: z.ZodEnum<{
198
+ utf8: "utf8";
199
+ base64: "base64";
200
+ }>;
201
+ decodedBytes: z.ZodNumber;
202
+ sha256: z.ZodString;
203
+ }, z.core.$strict>>;
204
+ }, z.core.$strict>>;
205
+ }, z.core.$strict>;
206
+ export declare const httpErrorSchema: z.ZodObject<{
207
+ error: z.ZodObject<{
208
+ code: z.ZodString;
209
+ message: z.ZodString;
210
+ details: z.ZodRecord<z.ZodString, z.ZodUnknown>;
211
+ }, z.core.$strict>;
212
+ }, z.core.$strict>;
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>;
219
+ export declare function protectedResourceMetadataUrl(resource: string): URL;
220
+ export declare function restBase(resource: string): string;
package/src/http.js ADDED
@@ -0,0 +1,48 @@
1
+ import { z } from 'zod';
2
+ import { artifactReadSchema, contentKinds, publicationSchema, revisionSchema, snapshotSchema } from "./index.js";
3
+ const identifier = z.string().min(1).max(128);
4
+ const digest = z.string().regex(/^[a-f0-9]{64}$/);
5
+ export const identitySchema = z.strictObject({ userId: identifier, sessionId: identifier, clientId: identifier, scopes: z.array(z.string()) });
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
+ export const revisionSummarySchema = revisionSchema.omit({ snapshot: true }).extend({
8
+ metadata: snapshotSchema.shape.metadata.pick({ title: true, description: true }),
9
+ content: z.strictObject({ kind: z.enum(contentKinds), utf8Bytes: z.number().int().nonnegative() }),
10
+ files: z.array(attachmentSummarySchema).max(32),
11
+ });
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(), slug: z.string().nullable(), publication: publicationSchema.nullable() });
14
+ export const listResultSchema = z.strictObject({ items: z.array(artifactSummarySchema).max(100), nextCursor: identifier.nullable() });
15
+ export const projectSummarySchema = z.strictObject({ name: z.string(), artifactCount: z.number().int().positive() });
16
+ export const projectsResultSchema = z.strictObject({ items: z.array(projectSummarySchema) });
17
+ export const historyResultSchema = z.strictObject({ items: z.array(revisionSchema).max(10), nextBeforeSequence: z.number().int().positive().nullable() });
18
+ export const summaryHistoryResultSchema = historyResultSchema.extend({ items: z.array(revisionSummarySchema).max(10) });
19
+ export const httpErrorSchema = z.strictObject({ error: z.strictObject({ code: z.string().min(1), message: z.string(), details: z.record(z.string(), z.unknown()) }) });
20
+ export function canonicalResource(value) {
21
+ const url = new URL(value);
22
+ const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
23
+ if (url.username || url.password || url.search || url.hash || (url.protocol !== 'https:' && !(loopback && url.protocol === 'http:')))
24
+ throw new TypeError('The server requires HTTPS, or HTTP on an exact loopback host, without credentials, query, or fragment');
25
+ if ((url.href !== value && url.href.replace(/\/$/, '') !== value) || (url.pathname !== '/' && url.pathname.endsWith('/')))
26
+ throw new TypeError('Use the exact canonical resource URL without a trailing slash');
27
+ return value;
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
+ });
42
+ export function protectedResourceMetadataUrl(resource) {
43
+ const url = new URL(canonicalResource(resource));
44
+ return new URL(`/.well-known/oauth-protected-resource${url.pathname === '/' ? '' : url.pathname}`, url.origin);
45
+ }
46
+ export function restBase(resource) {
47
+ return `${canonicalResource(resource).replace(/\/$/, '')}/v1`;
48
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,510 @@
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>;
26
+ export declare const snapshotSchema: z.ZodObject<{
27
+ metadata: z.ZodObject<{
28
+ title: z.ZodString;
29
+ description: z.ZodString;
30
+ custom: z.ZodOptional<z.ZodJSONSchema>;
31
+ }, z.core.$strict>;
32
+ content: z.ZodObject<{
33
+ kind: z.ZodEnum<{
34
+ text: "text";
35
+ html: "html";
36
+ markdown: "markdown";
37
+ design: "design";
38
+ }>;
39
+ text: z.ZodString;
40
+ }, z.core.$strict>;
41
+ files: z.ZodArray<z.ZodObject<{
42
+ path: z.ZodString;
43
+ mediaType: z.ZodString;
44
+ encoding: z.ZodEnum<{
45
+ utf8: "utf8";
46
+ base64: "base64";
47
+ }>;
48
+ data: z.ZodString;
49
+ }, z.core.$strict>>;
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;
61
+ export declare const projectNameSchema: z.ZodString;
62
+ export declare const slugSchema: z.ZodString;
63
+ export declare const createSchema: z.ZodObject<{
64
+ requestId: z.ZodString;
65
+ snapshot: z.ZodObject<{
66
+ metadata: z.ZodObject<{
67
+ title: z.ZodString;
68
+ description: z.ZodString;
69
+ custom: z.ZodOptional<z.ZodJSONSchema>;
70
+ }, z.core.$strict>;
71
+ content: z.ZodObject<{
72
+ kind: z.ZodEnum<{
73
+ text: "text";
74
+ html: "html";
75
+ markdown: "markdown";
76
+ design: "design";
77
+ }>;
78
+ text: z.ZodString;
79
+ }, z.core.$strict>;
80
+ files: z.ZodArray<z.ZodObject<{
81
+ path: z.ZodString;
82
+ mediaType: z.ZodString;
83
+ encoding: z.ZodEnum<{
84
+ utf8: "utf8";
85
+ base64: "base64";
86
+ }>;
87
+ data: z.ZodString;
88
+ }, z.core.$strict>>;
89
+ }, z.core.$strict>;
90
+ project: z.ZodOptional<z.ZodString>;
91
+ slug: z.ZodOptional<z.ZodString>;
92
+ response: z.ZodOptional<z.ZodEnum<{
93
+ full: "full";
94
+ compact: "compact";
95
+ }>>;
96
+ }, z.core.$strict>;
97
+ export declare const readByIdSchema: z.ZodObject<{
98
+ artifactId: z.ZodString;
99
+ revisionId: z.ZodOptional<z.ZodString>;
100
+ response: z.ZodOptional<z.ZodEnum<{
101
+ full: "full";
102
+ compact: "compact";
103
+ }>>;
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>;
115
+ export declare const updateSchema: z.ZodObject<{
116
+ requestId: z.ZodString;
117
+ artifactId: z.ZodString;
118
+ expectedRevisionId: z.ZodString;
119
+ snapshot: z.ZodObject<{
120
+ metadata: z.ZodObject<{
121
+ title: z.ZodString;
122
+ description: z.ZodString;
123
+ custom: z.ZodOptional<z.ZodJSONSchema>;
124
+ }, z.core.$strict>;
125
+ content: z.ZodObject<{
126
+ kind: z.ZodEnum<{
127
+ text: "text";
128
+ html: "html";
129
+ markdown: "markdown";
130
+ design: "design";
131
+ }>;
132
+ text: z.ZodString;
133
+ }, z.core.$strict>;
134
+ files: z.ZodArray<z.ZodObject<{
135
+ path: z.ZodString;
136
+ mediaType: z.ZodString;
137
+ encoding: z.ZodEnum<{
138
+ utf8: "utf8";
139
+ base64: "base64";
140
+ }>;
141
+ data: z.ZodString;
142
+ }, z.core.$strict>>;
143
+ }, z.core.$strict>;
144
+ response: z.ZodOptional<z.ZodEnum<{
145
+ full: "full";
146
+ compact: "compact";
147
+ }>>;
148
+ }, z.core.$strict>;
149
+ export declare const publishSchema: z.ZodObject<{
150
+ requestId: z.ZodString;
151
+ artifactId: z.ZodString;
152
+ revisionId: z.ZodString;
153
+ expectedPublicationVersion: z.ZodNumber;
154
+ }, z.core.$strict>;
155
+ export declare const deleteSchema: z.ZodObject<{
156
+ requestId: z.ZodString;
157
+ artifactId: z.ZodString;
158
+ expectedRevisionId: z.ZodString;
159
+ expectedPublicationVersion: z.ZodNumber;
160
+ }, z.core.$strict>;
161
+ export declare const deleteResultSchema: z.ZodObject<{
162
+ artifactId: z.ZodString;
163
+ }, z.core.$strict>;
164
+ export type DeleteInput = z.infer<typeof deleteSchema>;
165
+ export type DeleteResult = z.infer<typeof deleteResultSchema>;
166
+ export declare const moveSchema: z.ZodObject<{
167
+ requestId: z.ZodString;
168
+ artifactId: z.ZodString;
169
+ project: z.ZodOptional<z.ZodNullable<z.ZodString>>;
170
+ slug: z.ZodOptional<z.ZodNullable<z.ZodString>>;
171
+ }, z.core.$strict>;
172
+ export declare const moveResultSchema: z.ZodObject<{
173
+ artifactId: z.ZodString;
174
+ project: z.ZodNullable<z.ZodString>;
175
+ }, z.core.$strict>;
176
+ export type MoveResult = z.infer<typeof moveResultSchema>;
177
+ export declare const shareSchema: z.ZodObject<{
178
+ requestId: z.ZodString;
179
+ artifactId: z.ZodString;
180
+ email: z.ZodPipe<z.ZodString, z.ZodEmail>;
181
+ notify: z.ZodOptional<z.ZodBoolean>;
182
+ requireNew: z.ZodOptional<z.ZodLiteral<true>>;
183
+ }, z.core.$strict>;
184
+ export declare const unshareSchema: z.ZodObject<{
185
+ requestId: z.ZodString;
186
+ artifactId: z.ZodString;
187
+ shareId: z.ZodString;
188
+ }, z.core.$strict>;
189
+ export declare const listSharesSchema: z.ZodObject<{
190
+ artifactId: z.ZodString;
191
+ }, z.core.$strict>;
192
+ export declare const shareResultSchema: z.ZodObject<{
193
+ shareId: z.ZodString;
194
+ artifactId: z.ZodString;
195
+ kind: z.ZodLiteral<"email">;
196
+ value: z.ZodEmail;
197
+ createdAt: z.ZodISODateTime;
198
+ }, z.core.$strict>;
199
+ export declare const notificationSchema: z.ZodUnion<readonly [z.ZodObject<{
200
+ status: z.ZodLiteral<"requested">;
201
+ }, z.core.$strict>, z.ZodObject<{
202
+ status: z.ZodLiteral<"skipped">;
203
+ reason: z.ZodLiteral<"disabled">;
204
+ }, z.core.$strict>, z.ZodObject<{
205
+ status: z.ZodLiteral<"skipped">;
206
+ reason: z.ZodLiteral<"rate_limited">;
207
+ retryAt: z.ZodISODateTime;
208
+ }, z.core.$strict>]>;
209
+ export declare const shareCommandResultSchema: z.ZodObject<{
210
+ shareId: z.ZodString;
211
+ artifactId: z.ZodString;
212
+ kind: z.ZodLiteral<"email">;
213
+ value: z.ZodEmail;
214
+ createdAt: z.ZodISODateTime;
215
+ notification: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
216
+ status: z.ZodLiteral<"requested">;
217
+ }, z.core.$strict>, z.ZodObject<{
218
+ status: z.ZodLiteral<"skipped">;
219
+ reason: z.ZodLiteral<"disabled">;
220
+ }, z.core.$strict>, z.ZodObject<{
221
+ status: z.ZodLiteral<"skipped">;
222
+ reason: z.ZodLiteral<"rate_limited">;
223
+ retryAt: z.ZodISODateTime;
224
+ }, z.core.$strict>]>>;
225
+ }, z.core.$strict>;
226
+ export declare const unshareResultSchema: z.ZodObject<{
227
+ artifactId: z.ZodString;
228
+ shareId: z.ZodString;
229
+ }, z.core.$strict>;
230
+ export type Share = z.infer<typeof shareResultSchema>;
231
+ export type ShareResult = z.infer<typeof shareCommandResultSchema>;
232
+ export type Notification = z.infer<typeof notificationSchema>;
233
+ export type UnshareResult = z.infer<typeof unshareResultSchema>;
234
+ export declare const exportSchema: z.ZodObject<{
235
+ artifactId: z.ZodString;
236
+ revisionId: z.ZodString;
237
+ }, z.core.$strict>;
238
+ export declare const listSchema: z.ZodObject<{
239
+ limit: z.ZodDefault<z.ZodNumber>;
240
+ cursor: z.ZodOptional<z.ZodString>;
241
+ project: z.ZodOptional<z.ZodString>;
242
+ }, z.core.$strict>;
243
+ export declare const searchSchema: z.ZodObject<{
244
+ query: z.ZodString;
245
+ project: z.ZodOptional<z.ZodString>;
246
+ limit: z.ZodDefault<z.ZodNumber>;
247
+ cursor: z.ZodOptional<z.ZodString>;
248
+ }, z.core.$strict>;
249
+ export declare const searchHitSchema: z.ZodObject<{
250
+ artifactId: z.ZodString;
251
+ project: z.ZodNullable<z.ZodString>;
252
+ slug: z.ZodNullable<z.ZodString>;
253
+ title: z.ZodString;
254
+ snippet: z.ZodString;
255
+ rank: z.ZodNumber;
256
+ }, z.core.$strict>;
257
+ export declare const searchResultSchema: z.ZodObject<{
258
+ items: z.ZodArray<z.ZodObject<{
259
+ artifactId: z.ZodString;
260
+ project: z.ZodNullable<z.ZodString>;
261
+ slug: z.ZodNullable<z.ZodString>;
262
+ title: z.ZodString;
263
+ snippet: z.ZodString;
264
+ rank: z.ZodNumber;
265
+ }, z.core.$strict>>;
266
+ nextCursor: z.ZodNullable<z.ZodString>;
267
+ }, z.core.$strict>;
268
+ export type SearchHit = z.infer<typeof searchHitSchema>;
269
+ export type SearchResult = z.infer<typeof searchResultSchema>;
270
+ export declare const libraryQuerySchema: z.ZodObject<{
271
+ q: z.ZodDefault<z.ZodString>;
272
+ status: z.ZodDefault<z.ZodEnum<{
273
+ all: "all";
274
+ published: "published";
275
+ unpublished: "unpublished";
276
+ pending: "pending";
277
+ }>>;
278
+ project: z.ZodOptional<z.ZodString>;
279
+ cursor: z.ZodOptional<z.ZodString>;
280
+ }, z.core.$strict>;
281
+ export declare const historySchema: z.ZodObject<{
282
+ artifactId: z.ZodString;
283
+ limit: z.ZodDefault<z.ZodNumber>;
284
+ beforeSequence: z.ZodOptional<z.ZodNumber>;
285
+ response: z.ZodOptional<z.ZodEnum<{
286
+ full: "full";
287
+ summary: "summary";
288
+ }>>;
289
+ }, z.core.$strict>;
290
+ export declare const issueGrantSchema: z.ZodObject<{
291
+ artifactId: z.ZodString;
292
+ state: z.ZodString;
293
+ }, z.core.$strict>;
294
+ export declare const consumeGrantSchema: z.ZodObject<{
295
+ artifactId: z.ZodString;
296
+ state: z.ZodString;
297
+ code: z.ZodString;
298
+ }, z.core.$strict>;
299
+ export declare const viewerSchema: z.ZodObject<{
300
+ artifactId: z.ZodString;
301
+ userId: z.ZodString;
302
+ sessionId: z.ZodString;
303
+ expiresAt: z.ZodNumber;
304
+ }, z.core.$strict>;
305
+ export type Snapshot = z.infer<typeof snapshotSchema>;
306
+ export type ReadonlyJsonValue = string | number | boolean | null | {
307
+ readonly [key: string]: ReadonlyJsonValue;
308
+ } | readonly ReadonlyJsonValue[];
309
+ export type UpdateInput = {
310
+ readonly requestId: string;
311
+ readonly artifactId: string;
312
+ readonly expectedRevisionId: string;
313
+ readonly snapshot: {
314
+ readonly metadata: {
315
+ readonly title: string;
316
+ readonly description: string;
317
+ readonly custom?: ReadonlyJsonValue;
318
+ };
319
+ readonly content: Readonly<Snapshot['content']>;
320
+ readonly files: readonly Readonly<Snapshot['files'][number]>[];
321
+ };
322
+ readonly response?: 'full' | 'compact';
323
+ };
324
+ export type Actor = Readonly<{
325
+ userId: string;
326
+ sessionId: string;
327
+ clientId: string;
328
+ scopes: readonly string[];
329
+ }>;
330
+ export type Viewer = z.infer<typeof viewerSchema>;
331
+ export type Publication = z.infer<typeof publicationSchema>;
332
+ export type Revision = z.infer<typeof revisionSchema>;
333
+ export type ArtifactRead = z.infer<typeof artifactReadSchema>;
334
+ export type SharedRead = z.infer<typeof sharedReadSchema>;
335
+ export type SharedArtifactSummary = Readonly<{
336
+ artifactId: string;
337
+ title: string;
338
+ ownerName: string;
339
+ url: string;
340
+ }>;
341
+ export type ArtifactSummary = {
342
+ artifactId: string;
343
+ title: string;
344
+ headRevisionId: string;
345
+ createdAt: string;
346
+ project: string | null;
347
+ slug: string | null;
348
+ publication: Publication | null;
349
+ };
350
+ export type ProjectSummary = {
351
+ name: string;
352
+ artifactCount: number;
353
+ };
354
+ export type ArtifactLibraryQuery = z.infer<typeof libraryQuerySchema>;
355
+ export type ArtifactLibraryPage = {
356
+ items: ArtifactSummary[];
357
+ nextCursor: string | null;
358
+ counts: {
359
+ all: number;
360
+ published: number;
361
+ unpublished: number;
362
+ pending: number;
363
+ };
364
+ };
365
+ export type Archive = {
366
+ filename: string;
367
+ mediaType: 'application/zip';
368
+ base64: string;
369
+ sha256: string;
370
+ };
371
+ export type ArtifactErrorCode = 'forbidden' | 'not_found' | 'conflict' | 'request_conflict' | 'slug_conflict' | 'invalid_grant';
372
+ export declare class ArtifactError extends Error {
373
+ readonly code: ArtifactErrorCode;
374
+ readonly details: Record<string, unknown>;
375
+ constructor(code: ArtifactErrorCode, message: string, details?: Record<string, unknown>);
376
+ }
377
+ export declare function decodeFile(file: {
378
+ encoding: 'utf8' | 'base64';
379
+ data: string;
380
+ }): Uint8Array;
381
+ export declare const publicationSchema: z.ZodObject<{
382
+ revisionId: z.ZodString;
383
+ version: z.ZodNumber;
384
+ url: z.ZodURL;
385
+ }, z.core.$strict>;
386
+ export declare const revisionSchema: z.ZodObject<{
387
+ id: z.ZodString;
388
+ parentRevisionId: z.ZodNullable<z.ZodString>;
389
+ sequence: z.ZodNumber;
390
+ authorUserId: z.ZodString;
391
+ authorClientId: z.ZodString;
392
+ createdAt: z.ZodISODateTime;
393
+ sha256: z.ZodString;
394
+ snapshot: z.ZodObject<{
395
+ metadata: z.ZodObject<{
396
+ title: z.ZodString;
397
+ description: z.ZodString;
398
+ custom: z.ZodOptional<z.ZodJSONSchema>;
399
+ }, z.core.$strict>;
400
+ content: z.ZodObject<{
401
+ kind: z.ZodEnum<{
402
+ text: "text";
403
+ html: "html";
404
+ markdown: "markdown";
405
+ design: "design";
406
+ }>;
407
+ text: z.ZodString;
408
+ }, z.core.$strict>;
409
+ files: z.ZodArray<z.ZodObject<{
410
+ path: z.ZodString;
411
+ mediaType: z.ZodString;
412
+ encoding: z.ZodEnum<{
413
+ utf8: "utf8";
414
+ base64: "base64";
415
+ }>;
416
+ data: z.ZodString;
417
+ }, z.core.$strict>>;
418
+ }, z.core.$strict>;
419
+ }, z.core.$strict>;
420
+ export declare const artifactReadSchema: z.ZodObject<{
421
+ artifactId: z.ZodString;
422
+ ownerId: z.ZodString;
423
+ headRevisionId: z.ZodString;
424
+ project: z.ZodNullable<z.ZodString>;
425
+ slug: z.ZodNullable<z.ZodString>;
426
+ publication: z.ZodNullable<z.ZodObject<{
427
+ revisionId: z.ZodString;
428
+ version: z.ZodNumber;
429
+ url: z.ZodURL;
430
+ }, z.core.$strict>>;
431
+ revision: z.ZodObject<{
432
+ id: z.ZodString;
433
+ parentRevisionId: z.ZodNullable<z.ZodString>;
434
+ sequence: z.ZodNumber;
435
+ authorUserId: z.ZodString;
436
+ authorClientId: z.ZodString;
437
+ createdAt: z.ZodISODateTime;
438
+ sha256: z.ZodString;
439
+ snapshot: z.ZodObject<{
440
+ metadata: z.ZodObject<{
441
+ title: z.ZodString;
442
+ description: z.ZodString;
443
+ custom: z.ZodOptional<z.ZodJSONSchema>;
444
+ }, z.core.$strict>;
445
+ content: z.ZodObject<{
446
+ kind: z.ZodEnum<{
447
+ text: "text";
448
+ html: "html";
449
+ markdown: "markdown";
450
+ design: "design";
451
+ }>;
452
+ text: z.ZodString;
453
+ }, z.core.$strict>;
454
+ files: z.ZodArray<z.ZodObject<{
455
+ path: z.ZodString;
456
+ mediaType: z.ZodString;
457
+ encoding: z.ZodEnum<{
458
+ utf8: "utf8";
459
+ base64: "base64";
460
+ }>;
461
+ data: z.ZodString;
462
+ }, z.core.$strict>>;
463
+ }, z.core.$strict>;
464
+ }, z.core.$strict>;
465
+ }, z.core.$strict>;
466
+ export declare const sharedReadSchema: z.ZodObject<{
467
+ artifactId: z.ZodString;
468
+ ownerName: z.ZodDefault<z.ZodString>;
469
+ publication: z.ZodObject<{
470
+ revisionId: z.ZodString;
471
+ version: z.ZodNumber;
472
+ url: z.ZodURL;
473
+ }, z.core.$strict>;
474
+ revision: z.ZodObject<{
475
+ id: z.ZodString;
476
+ createdAt: z.ZodISODateTime;
477
+ sha256: z.ZodString;
478
+ snapshot: z.ZodObject<{
479
+ metadata: z.ZodObject<{
480
+ title: z.ZodString;
481
+ description: z.ZodString;
482
+ custom: z.ZodOptional<z.ZodJSONSchema>;
483
+ }, z.core.$strict>;
484
+ content: z.ZodObject<{
485
+ kind: z.ZodEnum<{
486
+ text: "text";
487
+ html: "html";
488
+ markdown: "markdown";
489
+ design: "design";
490
+ }>;
491
+ text: z.ZodString;
492
+ }, z.core.$strict>;
493
+ files: z.ZodArray<z.ZodObject<{
494
+ path: z.ZodString;
495
+ mediaType: z.ZodString;
496
+ encoding: z.ZodEnum<{
497
+ utf8: "utf8";
498
+ base64: "base64";
499
+ }>;
500
+ data: z.ZodString;
501
+ }, z.core.$strict>>;
502
+ }, z.core.$strict>;
503
+ }, z.core.$strict>;
504
+ }, z.core.$strict>;
505
+ export declare const publishResultSchema: z.ZodObject<{
506
+ revisionId: z.ZodString;
507
+ version: z.ZodNumber;
508
+ url: z.ZodURL;
509
+ artifactId: z.ZodString;
510
+ }, z.core.$strict>;
package/src/index.js ADDED
@@ -0,0 +1,187 @@
1
+ import { z } from 'zod';
2
+ const identifier = z.string().min(1).max(128);
3
+ const requestId = z.string().min(8).max(128);
4
+ export const contentKinds = ['html', 'markdown', 'text', 'design'];
5
+ const fileSchema = z.strictObject({
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'),
7
+ mediaType: z.string().max(128).regex(/^[a-zA-Z0-9!#$&^_.+-]+\/[a-zA-Z0-9!#$&^_.+-]+$/),
8
+ encoding: z.enum(['utf8', 'base64']),
9
+ data: z.string().max(1_398_104).refine((data) => data.isWellFormed(), 'File text must contain well-formed Unicode'),
10
+ }).superRefine((file, context) => {
11
+ if (file.encoding === 'base64' && Buffer.from(file.data, 'base64').toString('base64') !== file.data)
12
+ context.addIssue({ code: 'custom', path: ['data'], message: 'Use canonical padded base64' });
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
+ });
68
+ export const snapshotSchema = z.strictObject({
69
+ metadata: z.strictObject({ title: z.string().min(1).max(256), description: z.string().max(8192), custom: z.json().optional() }),
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') }),
71
+ files: z.array(fileSchema).max(32),
72
+ }).superRefine((snapshot, context) => {
73
+ const paths = snapshot.files.map((file) => file.path);
74
+ if (paths.some((path, index) => paths.some((other, otherIndex) => index !== otherIndex && (path === other || other.startsWith(`${path}/`)))))
75
+ context.addIssue({ code: 'custom', path: ['files'], message: 'File paths must be unique and cannot contain another file' });
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);
77
+ if (bytes > 1_048_576)
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 });
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
+ }
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');
124
+ const response = z.enum(['full', 'compact']).optional();
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');
133
+ export const updateSchema = z.strictObject({ requestId, artifactId: identifier, expectedRevisionId: identifier, snapshot: snapshotSchema, response });
134
+ export const publishSchema = z.strictObject({ requestId, artifactId: identifier, revisionId: identifier, expectedPublicationVersion: z.number().int().nonnegative() });
135
+ export const deleteSchema = z.strictObject({ requestId, artifactId: identifier, expectedRevisionId: identifier, expectedPublicationVersion: z.number().int().nonnegative() });
136
+ export const deleteResultSchema = z.strictObject({ artifactId: identifier });
137
+ export const moveSchema = z.strictObject({ requestId, artifactId: identifier, project: projectNameSchema.nullable().optional(), slug: slugSchema.nullable().optional() })
138
+ .refine((input) => input.project !== undefined || input.slug !== undefined, 'Specify project, slug, or both; null clears either');
139
+ export const moveResultSchema = z.strictObject({ artifactId: identifier, project: z.string().nullable() });
140
+ export const shareSchema = z.strictObject({ requestId, artifactId: identifier, email: z.string().trim().toLowerCase().pipe(z.email().max(254)).meta({ format: 'email', description: 'Email address, trimmed and lowercased; at most 254 characters after trimming' }), notify: z.boolean().optional(), requireNew: z.literal(true).optional() });
141
+ export const unshareSchema = z.strictObject({ requestId, artifactId: identifier, shareId: identifier });
142
+ export const listSharesSchema = z.strictObject({ artifactId: identifier });
143
+ export const shareResultSchema = z.strictObject({ shareId: identifier, artifactId: identifier, kind: z.literal('email'), value: z.email(), createdAt: z.iso.datetime() });
144
+ export const notificationSchema = z.union([
145
+ z.strictObject({ status: z.literal('requested') }),
146
+ z.strictObject({ status: z.literal('skipped'), reason: z.literal('disabled') }),
147
+ z.strictObject({ status: z.literal('skipped'), reason: z.literal('rate_limited'), retryAt: z.iso.datetime() }),
148
+ ]);
149
+ export const shareCommandResultSchema = shareResultSchema.extend({ notification: notificationSchema.optional() });
150
+ export const unshareResultSchema = z.strictObject({ artifactId: identifier, shareId: identifier });
151
+ export const exportSchema = z.strictObject({ artifactId: identifier, revisionId: identifier });
152
+ export const listSchema = z.strictObject({ limit: z.number().int().min(1).max(100).default(25), cursor: identifier.optional(), project: projectNameSchema.optional() });
153
+ export const searchSchema = z.strictObject({
154
+ query: z.string().trim().min(1).max(256),
155
+ project: projectNameSchema.optional(),
156
+ limit: z.number().int().min(1).max(25).default(10),
157
+ cursor: z.string().regex(/^\d{1,6}$/, 'Use the nextCursor from a previous search').optional(),
158
+ });
159
+ export const searchHitSchema = z.strictObject({ artifactId: identifier, project: z.string().nullable(), slug: z.string().nullable(), title: z.string(), snippet: z.string(), rank: z.number() });
160
+ export const searchResultSchema = z.strictObject({ items: z.array(searchHitSchema).max(25), nextCursor: z.string().nullable() });
161
+ export const libraryQuerySchema = z.strictObject({
162
+ q: z.string().trim().max(256).default(''),
163
+ status: z.enum(['all', 'published', 'unpublished', 'pending']).default('all'),
164
+ project: projectNameSchema.optional(),
165
+ cursor: identifier.optional(),
166
+ });
167
+ export const historySchema = z.strictObject({ artifactId: identifier, limit: z.number().int().min(1).max(10).default(5), beforeSequence: z.number().int().positive().optional(), response: z.enum(['full', 'summary']).optional() });
168
+ export const issueGrantSchema = z.strictObject({ artifactId: identifier, state: z.string().min(16).max(256) });
169
+ export const consumeGrantSchema = issueGrantSchema.extend({ code: z.string().min(32).max(128) });
170
+ export const viewerSchema = z.strictObject({ artifactId: identifier, userId: identifier, sessionId: identifier, expiresAt: z.number().int().positive() });
171
+ export class ArtifactError extends Error {
172
+ code;
173
+ details;
174
+ constructor(code, message, details = {}) {
175
+ super(message);
176
+ this.code = code;
177
+ this.details = details;
178
+ }
179
+ }
180
+ export function decodeFile(file) {
181
+ return Buffer.from(file.data, file.encoding === 'utf8' ? 'utf8' : 'base64');
182
+ }
183
+ export const publicationSchema = z.strictObject({ revisionId: identifier, version: z.number().int().positive(), url: z.url() });
184
+ 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 });
185
+ export const artifactReadSchema = z.strictObject({ artifactId: identifier, ownerId: identifier, headRevisionId: identifier, project: z.string().nullable(), slug: z.string().nullable(), publication: publicationSchema.nullable(), revision: revisionSchema });
186
+ 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 }) });
187
+ export const publishResultSchema = publicationSchema.extend({ artifactId: identifier });
@@ -0,0 +1,2 @@
1
+ export declare function canonicalJson(value: unknown): string;
2
+ export declare function sha256(value: string | Uint8Array): Promise<string>;
@@ -0,0 +1,11 @@
1
+ export function canonicalJson(value) {
2
+ return JSON.stringify(value, (_key, item) => {
3
+ if (item && typeof item === 'object' && !Array.isArray(item))
4
+ return Object.fromEntries(Object.entries(item).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
5
+ return item;
6
+ });
7
+ }
8
+ export async function sha256(value) {
9
+ const bytes = typeof value === 'string' ? new TextEncoder().encode(value) : new Uint8Array(value);
10
+ return Buffer.from(await crypto.subtle.digest('SHA-256', bytes)).toString('hex');
11
+ }
@@ -0,0 +1,75 @@
1
+ import { type ArtifactRead, type Revision, type Snapshot, type UpdateInput } from './index.ts';
2
+ export type { UpdateInput } from './index.ts';
3
+ export type AttachmentSummary = {
4
+ path: string;
5
+ mediaType: string;
6
+ encoding: 'utf8' | 'base64';
7
+ decodedBytes: number;
8
+ sha256: string;
9
+ };
10
+ export type RevisionSummary = Omit<Revision, 'snapshot'> & {
11
+ metadata: Pick<Snapshot['metadata'], 'title' | 'description'>;
12
+ content: {
13
+ kind: Snapshot['content']['kind'];
14
+ utf8Bytes: number;
15
+ };
16
+ files: AttachmentSummary[];
17
+ };
18
+ export type CompactArtifactRead = Omit<ArtifactRead, 'revision'> & {
19
+ revision: RevisionSummary;
20
+ };
21
+ export type MetadataChange = {
22
+ field: 'title' | 'description';
23
+ before: string;
24
+ after: string;
25
+ } | {
26
+ field: 'custom';
27
+ before?: Snapshot['metadata']['custom'];
28
+ after?: Snapshot['metadata']['custom'];
29
+ };
30
+ export type AttachmentChange = {
31
+ path: string;
32
+ change: 'added';
33
+ before: null;
34
+ after: AttachmentSummary;
35
+ } | {
36
+ path: string;
37
+ change: 'removed';
38
+ before: AttachmentSummary;
39
+ after: null;
40
+ } | {
41
+ path: string;
42
+ change: 'changed';
43
+ before: AttachmentSummary;
44
+ after: AttachmentSummary;
45
+ };
46
+ export type RevisionComparison = {
47
+ beforeRevisionId: string;
48
+ afterRevisionId: string;
49
+ metadata: MetadataChange[];
50
+ content: {
51
+ before: Snapshot['content'];
52
+ after: Snapshot['content'];
53
+ kindChanged: boolean;
54
+ textChanged: boolean;
55
+ };
56
+ files: AttachmentChange[];
57
+ };
58
+ export declare function summarizeRevision(revision: Revision): Promise<RevisionSummary>;
59
+ export declare function compactArtifactRead(document: ArtifactRead): Promise<CompactArtifactRead>;
60
+ export declare function compareRevisions(before: Revision, after: Revision): Promise<RevisionComparison>;
61
+ export type ArtifactEditClient = {
62
+ read(input: {
63
+ artifactId: string;
64
+ }): Promise<ArtifactRead>;
65
+ };
66
+ export type PrepareArtifactEditInput = {
67
+ artifactId: string;
68
+ requestId: string;
69
+ changes: {
70
+ metadata?: Partial<Snapshot['metadata']>;
71
+ content?: Partial<Snapshot['content']>;
72
+ files?: Snapshot['files'];
73
+ };
74
+ };
75
+ export declare function prepareArtifactEdit(client: ArtifactEditClient, value: PrepareArtifactEditInput): Promise<UpdateInput>;
@@ -0,0 +1,97 @@
1
+ import { z } from 'zod';
2
+ import { ArtifactError, artifactReadSchema, createSchema, decodeFile, readByIdSchema, snapshotSchema, updateSchema } from "./index.js";
3
+ import { canonicalJson, sha256 } from "./serialization.js";
4
+ async function summarizeFiles(files) {
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 }) => {
6
+ const bytes = decodeFile({ encoding, data });
7
+ return { path, mediaType, encoding, decodedBytes: bytes.length, sha256: await sha256(bytes) };
8
+ }));
9
+ }
10
+ export async function summarizeRevision(revision) {
11
+ const { snapshot, ...identity } = revision;
12
+ return {
13
+ ...identity,
14
+ metadata: { title: snapshot.metadata.title, description: snapshot.metadata.description },
15
+ content: { kind: snapshot.content.kind, utf8Bytes: Buffer.byteLength(snapshot.content.text) },
16
+ files: await summarizeFiles(snapshot.files),
17
+ };
18
+ }
19
+ export async function compactArtifactRead(document) {
20
+ const { revision, ...identity } = document;
21
+ return { ...identity, revision: await summarizeRevision(revision) };
22
+ }
23
+ export async function compareRevisions(before, after) {
24
+ const metadata = [];
25
+ for (const field of ['title', 'description']) {
26
+ if (before.snapshot.metadata[field] !== after.snapshot.metadata[field])
27
+ metadata.push({ field, before: before.snapshot.metadata[field], after: after.snapshot.metadata[field] });
28
+ }
29
+ if (canonicalJson(before.snapshot.metadata.custom) !== canonicalJson(after.snapshot.metadata.custom)) {
30
+ metadata.push({ field: 'custom', before: structuredClone(before.snapshot.metadata.custom), after: structuredClone(after.snapshot.metadata.custom) });
31
+ }
32
+ const [beforeFiles, afterFiles] = await Promise.all([summarizeFiles(before.snapshot.files), summarizeFiles(after.snapshot.files)]);
33
+ const beforeByPath = new Map(beforeFiles.map((file) => [file.path, file]));
34
+ const afterByPath = new Map(afterFiles.map((file) => [file.path, file]));
35
+ const files = [];
36
+ for (const path of [...new Set([...beforeByPath.keys(), ...afterByPath.keys()])].sort()) {
37
+ const previous = beforeByPath.get(path);
38
+ const next = afterByPath.get(path);
39
+ if (!previous && next)
40
+ files.push({ path, change: 'added', before: null, after: next });
41
+ else if (previous && !next)
42
+ files.push({ path, change: 'removed', before: previous, after: null });
43
+ else if (previous && next && (previous.sha256 !== next.sha256 || previous.mediaType !== next.mediaType || previous.encoding !== next.encoding))
44
+ files.push({ path, change: 'changed', before: previous, after: next });
45
+ }
46
+ return {
47
+ beforeRevisionId: before.id,
48
+ afterRevisionId: after.id,
49
+ metadata,
50
+ content: {
51
+ before: { ...before.snapshot.content },
52
+ after: { ...after.snapshot.content },
53
+ kindChanged: before.snapshot.content.kind !== after.snapshot.content.kind,
54
+ textChanged: before.snapshot.content.text !== after.snapshot.content.text,
55
+ },
56
+ files,
57
+ };
58
+ }
59
+ const prepareEditSchema = z.strictObject({
60
+ artifactId: readByIdSchema.shape.artifactId,
61
+ requestId: createSchema.shape.requestId,
62
+ changes: z.strictObject({
63
+ metadata: snapshotSchema.shape.metadata.partial().optional(),
64
+ content: snapshotSchema.shape.content.partial().optional(),
65
+ files: snapshotSchema.shape.files.optional(),
66
+ }),
67
+ });
68
+ function freezeJson(value) {
69
+ if (value && typeof value === 'object') {
70
+ for (const child of Object.values(value))
71
+ freezeJson(child);
72
+ Object.freeze(value);
73
+ }
74
+ return value;
75
+ }
76
+ export async function prepareArtifactEdit(client, value) {
77
+ const input = prepareEditSchema.parse(structuredClone(value));
78
+ const current = artifactReadSchema.parse(structuredClone(await client.read({ artifactId: input.artifactId })));
79
+ if (current.artifactId !== input.artifactId)
80
+ throw new TypeError('Artifact reader returned a different artifact');
81
+ if (current.revision.id !== current.headRevisionId)
82
+ throw new ArtifactError('conflict', 'Artifact reader did not return the current head', { currentRevisionId: current.headRevisionId });
83
+ const snapshot = current.revision.snapshot;
84
+ const metadata = { ...snapshot.metadata, ...input.changes.metadata };
85
+ if (Object.hasOwn(input.changes.metadata ?? {}, 'custom') && input.changes.metadata?.custom === undefined)
86
+ delete metadata.custom;
87
+ return freezeJson(updateSchema.parse({
88
+ artifactId: input.artifactId,
89
+ requestId: input.requestId,
90
+ expectedRevisionId: current.headRevisionId,
91
+ snapshot: {
92
+ metadata,
93
+ content: { ...snapshot.content, ...input.changes.content },
94
+ files: input.changes.files ?? snapshot.files,
95
+ },
96
+ }));
97
+ }