@ketch.to/schema 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,505 @@
1
+ import { type Card, type CardPatch, type NewCard, type NewWebhook, type OutcomeInput, type WebhookPayload } from "./types.js";
2
+ export declare const cardSchema: {
3
+ $schema: string;
4
+ $id: string;
5
+ title: string;
6
+ description: string;
7
+ $defs: {
8
+ Duration: {
9
+ type: string;
10
+ description: string;
11
+ pattern: string;
12
+ examples: string[];
13
+ };
14
+ CardId: {
15
+ type: string;
16
+ pattern: string;
17
+ description: string;
18
+ };
19
+ ActionId: {
20
+ type: string;
21
+ pattern: string;
22
+ description: string;
23
+ };
24
+ ActionStyle: {
25
+ type: string;
26
+ enum: string[];
27
+ description: string;
28
+ };
29
+ Action: {
30
+ type: string;
31
+ additionalProperties: boolean;
32
+ required: string[];
33
+ properties: {
34
+ id: {
35
+ $ref: string;
36
+ };
37
+ label: {
38
+ type: string;
39
+ minLength: number;
40
+ maxLength: number;
41
+ };
42
+ style: {
43
+ $ref: string;
44
+ default: string;
45
+ };
46
+ input: {
47
+ type: string;
48
+ enum: string[];
49
+ description: string;
50
+ };
51
+ snooze_for: {
52
+ $ref: string;
53
+ description: string;
54
+ };
55
+ };
56
+ };
57
+ Actions: {
58
+ type: string;
59
+ minItems: number;
60
+ maxItems: number;
61
+ items: {
62
+ $ref: string;
63
+ };
64
+ };
65
+ Source: {
66
+ type: string;
67
+ additionalProperties: boolean;
68
+ properties: {
69
+ app: {
70
+ type: string;
71
+ minLength: number;
72
+ maxLength: number;
73
+ description: string;
74
+ };
75
+ context: {
76
+ type: string;
77
+ minLength: number;
78
+ maxLength: number;
79
+ };
80
+ };
81
+ };
82
+ Input: {
83
+ type: string;
84
+ additionalProperties: boolean;
85
+ properties: {
86
+ placeholder: {
87
+ type: string;
88
+ maxLength: number;
89
+ };
90
+ spawns: {
91
+ type: string;
92
+ default: boolean;
93
+ description: string;
94
+ };
95
+ };
96
+ };
97
+ Recurrence: {
98
+ type: string;
99
+ additionalProperties: boolean;
100
+ required: string[];
101
+ properties: {
102
+ every: {
103
+ $ref: string;
104
+ };
105
+ };
106
+ };
107
+ Kind: {
108
+ type: string;
109
+ enum: string[];
110
+ description: string;
111
+ };
112
+ Priority: {
113
+ type: string;
114
+ minimum: number;
115
+ maximum: number;
116
+ default: number;
117
+ description: string;
118
+ };
119
+ Link: {
120
+ type: string;
121
+ pattern: string;
122
+ maxLength: number;
123
+ description: string;
124
+ };
125
+ Meta: {
126
+ type: string;
127
+ description: string;
128
+ };
129
+ CardFields: {
130
+ type: string;
131
+ properties: {
132
+ title: {
133
+ type: string;
134
+ minLength: number;
135
+ maxLength: number;
136
+ };
137
+ body: {
138
+ type: string;
139
+ maxLength: number;
140
+ description: string;
141
+ };
142
+ source: {
143
+ $ref: string;
144
+ };
145
+ link: {
146
+ $ref: string;
147
+ };
148
+ priority: {
149
+ $ref: string;
150
+ };
151
+ kind: {
152
+ $ref: string;
153
+ default: string;
154
+ };
155
+ actions: {
156
+ $ref: string;
157
+ };
158
+ input: {
159
+ $ref: string;
160
+ };
161
+ recurrence: {
162
+ $ref: string;
163
+ };
164
+ show_after: {
165
+ type: string[];
166
+ format: string;
167
+ description: string;
168
+ };
169
+ expires_at: {
170
+ type: string[];
171
+ format: string;
172
+ description: string;
173
+ };
174
+ meta: {
175
+ $ref: string;
176
+ };
177
+ };
178
+ };
179
+ NewCard: {
180
+ description: string;
181
+ allOf: {
182
+ $ref: string;
183
+ }[];
184
+ type: string;
185
+ additionalProperties: boolean;
186
+ required: string[];
187
+ properties: {
188
+ title: boolean;
189
+ body: boolean;
190
+ source: boolean;
191
+ link: boolean;
192
+ priority: boolean;
193
+ kind: boolean;
194
+ actions: boolean;
195
+ input: boolean;
196
+ recurrence: boolean;
197
+ show_after: boolean;
198
+ expires_at: boolean;
199
+ meta: boolean;
200
+ };
201
+ };
202
+ CardPatch: {
203
+ description: string;
204
+ type: string;
205
+ additionalProperties: boolean;
206
+ minProperties: number;
207
+ properties: {
208
+ title: {
209
+ type: string;
210
+ minLength: number;
211
+ maxLength: number;
212
+ };
213
+ body: {
214
+ type: string[];
215
+ maxLength: number;
216
+ };
217
+ actions: {
218
+ $ref: string;
219
+ };
220
+ show_after: {
221
+ type: string[];
222
+ format: string;
223
+ };
224
+ priority: {
225
+ $ref: string;
226
+ };
227
+ link: {
228
+ anyOf: ({
229
+ $ref: string;
230
+ type?: undefined;
231
+ } | {
232
+ type: string;
233
+ $ref?: undefined;
234
+ })[];
235
+ };
236
+ expires_at: {
237
+ type: string[];
238
+ format: string;
239
+ };
240
+ meta: {
241
+ $ref: string;
242
+ };
243
+ };
244
+ };
245
+ Status: {
246
+ type: string;
247
+ enum: string[];
248
+ description: string;
249
+ };
250
+ Outcome: {
251
+ type: string;
252
+ additionalProperties: boolean;
253
+ required: string[];
254
+ properties: {
255
+ action: {
256
+ $ref: string;
257
+ };
258
+ text: {
259
+ type: string[];
260
+ maxLength: number;
261
+ };
262
+ at: {
263
+ type: string;
264
+ format: string;
265
+ };
266
+ snoozed_until: {
267
+ type: string[];
268
+ format: string;
269
+ };
270
+ };
271
+ };
272
+ OutcomeInput: {
273
+ description: string;
274
+ type: string;
275
+ additionalProperties: boolean;
276
+ required: string[];
277
+ properties: {
278
+ action: {
279
+ $ref: string;
280
+ };
281
+ text: {
282
+ type: string[];
283
+ maxLength: number;
284
+ };
285
+ snoozed_until: {
286
+ type: string[];
287
+ format: string;
288
+ description: string;
289
+ };
290
+ spawn: {
291
+ type: string;
292
+ maxItems: number;
293
+ items: {
294
+ type: string;
295
+ minLength: number;
296
+ maxLength: number;
297
+ };
298
+ description: string;
299
+ };
300
+ };
301
+ };
302
+ Card: {
303
+ description: string;
304
+ allOf: {
305
+ $ref: string;
306
+ }[];
307
+ type: string;
308
+ additionalProperties: boolean;
309
+ required: string[];
310
+ properties: {
311
+ id: {
312
+ $ref: string;
313
+ };
314
+ title: boolean;
315
+ body: boolean;
316
+ link: boolean;
317
+ priority: boolean;
318
+ kind: boolean;
319
+ actions: boolean;
320
+ input: boolean;
321
+ recurrence: boolean;
322
+ show_after: boolean;
323
+ expires_at: boolean;
324
+ meta: boolean;
325
+ source: {
326
+ type: string;
327
+ additionalProperties: boolean;
328
+ required: string[];
329
+ properties: {
330
+ app: {
331
+ type: string;
332
+ minLength: number;
333
+ maxLength: number;
334
+ };
335
+ context: {
336
+ type: string;
337
+ minLength: number;
338
+ maxLength: number;
339
+ };
340
+ };
341
+ };
342
+ status: {
343
+ $ref: string;
344
+ };
345
+ parent_id: {
346
+ anyOf: ({
347
+ $ref: string;
348
+ type?: undefined;
349
+ } | {
350
+ type: string;
351
+ $ref?: undefined;
352
+ })[];
353
+ description: string;
354
+ };
355
+ outcome: {
356
+ anyOf: ({
357
+ $ref: string;
358
+ type?: undefined;
359
+ } | {
360
+ type: string;
361
+ $ref?: undefined;
362
+ })[];
363
+ description: string;
364
+ };
365
+ created_at: {
366
+ type: string;
367
+ format: string;
368
+ };
369
+ updated_at: {
370
+ type: string;
371
+ format: string;
372
+ };
373
+ retired_at: {
374
+ type: string[];
375
+ format: string;
376
+ };
377
+ };
378
+ };
379
+ WebhookEvent: {
380
+ type: string;
381
+ enum: string[];
382
+ };
383
+ NewWebhook: {
384
+ type: string;
385
+ additionalProperties: boolean;
386
+ required: string[];
387
+ properties: {
388
+ url: {
389
+ type: string;
390
+ pattern: string;
391
+ maxLength: number;
392
+ };
393
+ secret: {
394
+ type: string;
395
+ minLength: number;
396
+ maxLength: number;
397
+ };
398
+ events: {
399
+ type: string;
400
+ minItems: number;
401
+ uniqueItems: boolean;
402
+ items: {
403
+ $ref: string;
404
+ };
405
+ };
406
+ };
407
+ };
408
+ Webhook: {
409
+ type: string;
410
+ additionalProperties: boolean;
411
+ required: string[];
412
+ properties: {
413
+ id: {
414
+ type: string;
415
+ pattern: string;
416
+ };
417
+ url: {
418
+ type: string;
419
+ };
420
+ events: {
421
+ type: string;
422
+ items: {
423
+ $ref: string;
424
+ };
425
+ };
426
+ created_at: {
427
+ type: string;
428
+ format: string;
429
+ };
430
+ };
431
+ };
432
+ WebhookPayload: {
433
+ description: string;
434
+ type: string;
435
+ additionalProperties: boolean;
436
+ required: string[];
437
+ properties: {
438
+ id: {
439
+ type: string;
440
+ description: string;
441
+ };
442
+ event: {
443
+ $ref: string;
444
+ };
445
+ at: {
446
+ type: string;
447
+ format: string;
448
+ };
449
+ card: {
450
+ $ref: string;
451
+ };
452
+ outcome: {
453
+ $ref: string;
454
+ };
455
+ parent: {
456
+ $ref: string;
457
+ };
458
+ };
459
+ oneOf: ({
460
+ properties: {
461
+ event: {
462
+ const: string;
463
+ };
464
+ parent: boolean;
465
+ outcome?: undefined;
466
+ };
467
+ required: string[];
468
+ } | {
469
+ properties: {
470
+ event: {
471
+ const: string;
472
+ };
473
+ outcome: boolean;
474
+ parent?: undefined;
475
+ };
476
+ required: string[];
477
+ })[];
478
+ };
479
+ };
480
+ };
481
+ export interface ValidationError {
482
+ /** JSON pointer-ish path, e.g. "/actions/1/id". "" for the root. */
483
+ path: string;
484
+ message: string;
485
+ }
486
+ export type ValidationResult<T> = {
487
+ ok: true;
488
+ value: T;
489
+ } | {
490
+ ok: false;
491
+ errors: ValidationError[];
492
+ };
493
+ /** Validate a producer payload for POST /cards. Does not apply kind defaults; see withKindDefaults. */
494
+ export declare function validateNewCard(input: unknown): ValidationResult<NewCard>;
495
+ /** Validate a full server-shaped card (what GET returns, what webhooks carry). */
496
+ export declare function validateCard(input: unknown): ValidationResult<Card>;
497
+ export declare function validateCardPatch(input: unknown): ValidationResult<CardPatch>;
498
+ /**
499
+ * Validate an outcome against the card it's for: the action must be one of the
500
+ * card's actions or a reserved id; snooze-style actions need snoozed_until;
501
+ * required-input actions need text; spawn needs a spawning input.
502
+ */
503
+ export declare function validateOutcomeInput(input: unknown, card?: Pick<Card, "actions" | "input">): ValidationResult<OutcomeInput>;
504
+ export declare function validateNewWebhook(input: unknown): ValidationResult<NewWebhook>;
505
+ export declare function validateWebhookPayload(input: unknown): ValidationResult<WebhookPayload>;
@@ -0,0 +1,92 @@
1
+ import { Ajv2020 } from "ajv/dist/2020.js";
2
+ import * as addFormatsModule from "ajv-formats";
3
+ const addFormats = (addFormatsModule.default ?? addFormatsModule);
4
+ import schema from "../card.schema.json" with { type: "json" };
5
+ import { RESERVED_ACTION_IDS } from "./types.js";
6
+ export const cardSchema = schema;
7
+ const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false, allowUnionTypes: true });
8
+ addFormats(ajv);
9
+ ajv.addSchema(schema);
10
+ function compile(def) {
11
+ return ajv.compile({ $ref: `${schema.$id}#/$defs/${def}` });
12
+ }
13
+ const validators = {
14
+ NewCard: compile("NewCard"),
15
+ Card: compile("Card"),
16
+ CardPatch: compile("CardPatch"),
17
+ OutcomeInput: compile("OutcomeInput"),
18
+ NewWebhook: compile("NewWebhook"),
19
+ WebhookPayload: compile("WebhookPayload"),
20
+ };
21
+ function toErrors(errs) {
22
+ return (errs ?? []).map((e) => ({
23
+ path: e.instancePath,
24
+ message: e.keyword === "additionalProperties" ? `unknown property "${e.params.additionalProperty}"` : e.message ?? e.keyword,
25
+ }));
26
+ }
27
+ /** Semantic checks the JSON Schema can't express. */
28
+ function checkActions(actions, path) {
29
+ if (!Array.isArray(actions))
30
+ return [];
31
+ const errors = [];
32
+ const seen = new Set();
33
+ actions.forEach((a, i) => {
34
+ if (a.id && seen.has(a.id))
35
+ errors.push({ path: `${path}/${i}/id`, message: `duplicate action id "${a.id}"` });
36
+ if (a.id)
37
+ seen.add(a.id);
38
+ if (a.snooze_for !== undefined && a.style !== "snooze") {
39
+ errors.push({ path: `${path}/${i}/snooze_for`, message: 'snooze_for is only allowed when style is "snooze"' });
40
+ }
41
+ });
42
+ return errors;
43
+ }
44
+ function run(v, input, extra = () => []) {
45
+ if (!v(input))
46
+ return { ok: false, errors: toErrors(v.errors) };
47
+ const errors = extra(input);
48
+ return errors.length ? { ok: false, errors } : { ok: true, value: input };
49
+ }
50
+ /** Validate a producer payload for POST /cards. Does not apply kind defaults; see withKindDefaults. */
51
+ export function validateNewCard(input) {
52
+ return run(validators.NewCard, input, (c) => checkActions(c.actions, "/actions"));
53
+ }
54
+ /** Validate a full server-shaped card (what GET returns, what webhooks carry). */
55
+ export function validateCard(input) {
56
+ return run(validators.Card, input, (c) => checkActions(c.actions, "/actions"));
57
+ }
58
+ export function validateCardPatch(input) {
59
+ return run(validators.CardPatch, input, (c) => checkActions(c.actions, "/actions"));
60
+ }
61
+ /**
62
+ * Validate an outcome against the card it's for: the action must be one of the
63
+ * card's actions or a reserved id; snooze-style actions need snoozed_until;
64
+ * required-input actions need text; spawn needs a spawning input.
65
+ */
66
+ export function validateOutcomeInput(input, card) {
67
+ return run(validators.OutcomeInput, input, (o) => {
68
+ if (!card)
69
+ return [];
70
+ const errors = [];
71
+ const action = card.actions.find((a) => a.id === o.action);
72
+ const reserved = RESERVED_ACTION_IDS.includes(o.action);
73
+ if (!action && !reserved)
74
+ errors.push({ path: "/action", message: `"${o.action}" is not an action on this card` });
75
+ const isSnooze = o.action === "snooze" || action?.style === "snooze";
76
+ if (isSnooze && !o.snoozed_until)
77
+ errors.push({ path: "/snoozed_until", message: "required for snooze actions" });
78
+ if (!isSnooze && o.snoozed_until)
79
+ errors.push({ path: "/snoozed_until", message: "only allowed for snooze actions" });
80
+ if (action?.input === "required" && !o.text?.trim())
81
+ errors.push({ path: "/text", message: `action "${o.action}" requires text` });
82
+ if (o.spawn?.length && !card.input?.spawns)
83
+ errors.push({ path: "/spawn", message: "card has no spawning input" });
84
+ return errors;
85
+ });
86
+ }
87
+ export function validateNewWebhook(input) {
88
+ return run(validators.NewWebhook, input);
89
+ }
90
+ export function validateWebhookPayload(input) {
91
+ return run(validators.WebhookPayload, input);
92
+ }
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@ketch.to/schema",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Ketch card JSON Schema, TypeScript types, validators and fixtures",
5
+ "type": "module",
6
+ "main": "./dist/src/index.js",
7
+ "types": "./dist/src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/src/index.d.ts",
11
+ "import": "./dist/src/index.js"
12
+ },
13
+ "./card.schema.json": "./card.schema.json",
14
+ "./fixtures": {
15
+ "types": "./dist/fixtures/index.d.ts",
16
+ "import": "./dist/fixtures/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist/src",
21
+ "dist/fixtures",
22
+ "card.schema.json",
23
+ "README.md"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "typecheck": "tsc -p tsconfig.json --noEmit",
28
+ "test": "vitest run",
29
+ "gen:swift": "tsx scripts/gen-swift.ts && tsx scripts/export-fixtures.ts",
30
+ "prepublishOnly": "npm run build && npm test"
31
+ },
32
+ "dependencies": {
33
+ "ajv": "^8.17.1",
34
+ "ajv-formats": "^3.0.1"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "homepage": "https://ketch.to",
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "keywords": [
44
+ "ketch",
45
+ "json-schema",
46
+ "cards",
47
+ "agents"
48
+ ]
49
+ }