@sealkeeper/schema 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +2 -2
  2. package/dist/api.d.ts +30 -38
  3. package/dist/api.js +87 -42
  4. package/dist/client-address.d.ts +6 -0
  5. package/dist/client-address.js +81 -0
  6. package/dist/credential.d.ts +1 -66
  7. package/dist/credential.js +25 -63
  8. package/dist/events.d.ts +1 -0
  9. package/dist/events.js +13 -1
  10. package/dist/goal.js +2 -2
  11. package/dist/hidden.d.ts +1 -0
  12. package/dist/hidden.js +5 -0
  13. package/dist/index.d.ts +3 -1
  14. package/dist/index.js +3 -1
  15. package/dist/json-shape.d.ts +9 -0
  16. package/dist/json-shape.js +70 -0
  17. package/dist/moderation.d.ts +2 -1
  18. package/dist/moderation.js +169 -75
  19. package/dist/seal-conformance.js +2 -2
  20. package/dist/seal-verify.d.ts +21 -0
  21. package/dist/seal-verify.js +63 -0
  22. package/dist/standing.js +1 -1
  23. package/dist/tasks.d.ts +2 -0
  24. package/dist/tasks.js +14 -4
  25. package/package.json +2 -6
  26. package/dist/db/agent-milestones.d.ts +0 -75
  27. package/dist/db/agent-milestones.js +0 -16
  28. package/dist/db/agent-renames.d.ts +0 -126
  29. package/dist/db/agent-renames.js +0 -20
  30. package/dist/db/agents.d.ts +0 -266
  31. package/dist/db/agents.js +0 -79
  32. package/dist/db/client.d.ts +0 -2471
  33. package/dist/db/client.js +0 -40
  34. package/dist/db/credentials.d.ts +0 -143
  35. package/dist/db/credentials.js +0 -16
  36. package/dist/db/deleted-operators.d.ts +0 -75
  37. package/dist/db/deleted-operators.js +0 -13
  38. package/dist/db/events.d.ts +0 -160
  39. package/dist/db/events.js +0 -41
  40. package/dist/db/feed-items.d.ts +0 -109
  41. package/dist/db/feed-items.js +0 -21
  42. package/dist/db/index.d.ts +0 -76
  43. package/dist/db/index.js +0 -28
  44. package/dist/db/migrate.d.ts +0 -1
  45. package/dist/db/migrate.js +0 -34
  46. package/dist/db/migrator.d.ts +0 -4
  47. package/dist/db/migrator.js +0 -26
  48. package/dist/db/operator-identities.d.ts +0 -211
  49. package/dist/db/operator-identities.js +0 -49
  50. package/dist/db/operator-level-grants.d.ts +0 -109
  51. package/dist/db/operator-level-grants.js +0 -30
  52. package/dist/db/operator-slugs.d.ts +0 -92
  53. package/dist/db/operator-slugs.js +0 -25
  54. package/dist/db/operators.d.ts +0 -228
  55. package/dist/db/operators.js +0 -39
  56. package/dist/db/quota-counters.d.ts +0 -109
  57. package/dist/db/quota-counters.js +0 -17
  58. package/dist/db/ratings.d.ts +0 -160
  59. package/dist/db/ratings.js +0 -27
  60. package/dist/db/scores.d.ts +0 -160
  61. package/dist/db/scores.js +0 -15
  62. package/dist/db/standing.d.ts +0 -264
  63. package/dist/db/standing.js +0 -34
  64. package/dist/db/task-claim-failures.d.ts +0 -143
  65. package/dist/db/task-claim-failures.js +0 -37
  66. package/dist/db/task-outcomes.d.ts +0 -145
  67. package/dist/db/task-outcomes.js +0 -25
  68. package/dist/db/tasks.d.ts +0 -381
  69. package/dist/db/tasks.js +0 -100
  70. package/dist/db/timestamps.d.ts +0 -4
  71. package/dist/db/timestamps.js +0 -24
  72. package/dist/db/url.d.ts +0 -12
  73. package/dist/db/url.js +0 -25
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { HIDDEN } from './hidden.js';
2
3
  // Moderation of the names an operator chooses, the operator slug and the
3
4
  // operator display name. The API runs these checks on every write and is
4
5
  // the only enforcement. The CLI and the web run the same checks for early
@@ -24,10 +25,6 @@ export const OperatorSlug = z
24
25
  .max(OPERATOR_SLUG_MAX)
25
26
  .regex(SLUG, 'Use lowercase letters, digits and hyphens, starting and ending with a letter or digit')
26
27
  .refine((slug) => !slug.includes('--'), 'No double hyphen');
27
- // Control, format, private use, unassigned and lone surrogate characters
28
- // (bidi overrides, zero width joiners and the like), and the line and
29
- // paragraph separators, which can hide, reorder or break what a reader sees.
30
- const HIDDEN = /[\p{C}\p{Zl}\p{Zp}]/u;
31
28
  const LETTER_OR_DIGIT = /[\p{L}\p{N}]/u;
32
29
  // The operator's display name. Free text, no control or format characters,
33
30
  // no space at either end, and at least one letter or digit, so its
@@ -42,65 +39,101 @@ export const OperatorDisplayName = z
42
39
  .refine((name) => LETTER_OR_DIGIT.test(name), 'Use at least one letter or digit');
43
40
  // ---------------------------------------------------------------------------
44
41
  // Normalisation
45
- // Letters from other scripts that read as a Latin letter, and a few Latin
46
- // letters NFKD does not split into a base and a mark.
47
- const HOMOGLYPHS = {
48
- // Cyrillic
49
- а: 'a',
50
- в: 'b',
51
- с: 'c',
52
- ԁ: 'd',
53
- е: 'e',
54
- ё: 'e',
55
- һ: 'h',
56
- н: 'h',
57
- і: 'i',
58
- ї: 'i',
59
- ј: 'j',
60
- к: 'k',
61
- ӏ: 'l',
62
- м: 'm',
63
- п: 'n',
64
- о: 'o',
65
- р: 'p',
66
- ԛ: 'q',
67
- г: 'r',
68
- ѕ: 's',
69
- т: 't',
70
- у: 'y',
71
- ш: 'w',
72
- ԝ: 'w',
73
- х: 'x',
74
- // Greek
75
- α: 'a',
76
- β: 'b',
77
- ϲ: 'c',
78
- ε: 'e',
79
- η: 'n',
80
- ι: 'i',
81
- κ: 'k',
82
- μ: 'u',
83
- ν: 'v',
84
- ο: 'o',
85
- ρ: 'p',
86
- τ: 't',
87
- υ: 'u',
88
- χ: 'x',
89
- ω: 'w',
90
- // Latin letters without a decomposition
91
- ı: 'i',
92
- ȷ: 'j',
93
- ł: 'l',
94
- ø: 'o',
95
- đ: 'd',
96
- ħ: 'h',
97
- ŧ: 't',
98
- ß: 'ss',
99
- æ: 'ae',
100
- œ: 'oe',
101
- ɡ: 'g',
102
- ɑ: 'a',
103
- // Digits and symbols used as letters
42
+ // Lookalikes, folded to the Latin letter they read as. Each row is a Latin
43
+ // letter and the letters of other scripts that read as it, in the order
44
+ // Greek, Cyrillic, Armenian, Cherokee, Canadian syllabics, Lisu, Latin,
45
+ // Coptic, Tifinagh. The letters come from Unicode's confusables.txt, one
46
+ // letter to one Latin letter, for these scripts only, plus the Latin small
47
+ // capitals and a few Latin letters NFKD does not split into a base and a
48
+ // mark. Coptic alfa and eie (Ⲁ, Ⲉ) are added by eye, they are not in the
49
+ // list. Letters NFKC or
50
+ // NFKD turn into another letter (the lunate sigma, the rho symbol, ё) are
51
+ // left out, since they never reach the tables.
52
+ function byLetter(rows) {
53
+ const table = {};
54
+ for (const [latin, lookalikes] of Object.entries(rows)) {
55
+ for (const ch of lookalikes) {
56
+ if (ch in table)
57
+ throw new Error(`${ch} is in two lookalike rows`);
58
+ table[ch] = latin;
59
+ }
60
+ }
61
+ return table;
62
+ }
63
+ // Capitals, and letters of scripts without case, that read as a Latin
64
+ // capital. Folded before a letter is lowercased, since lowercasing first
65
+ // would turn Greek capital Nu into nu, which reads as v, and a Cherokee
66
+ // capital into a Cherokee small letter. So capital Nu is n here and small
67
+ // nu is v in SMALL_LOOKALIKES.
68
+ const CAPITAL_LOOKALIKES = byLetter({
69
+ a: 'ΑАᎪᗅꓮⲀ',
70
+ b: 'ΒВЬᏏᏴᗷꓐⲂ',
71
+ c: 'СᏟꓚⲤ',
72
+ d: 'ᎠᏧᗞᗪꓓ',
73
+ e: 'ΕЕᎬꓰⲈⴹ',
74
+ f: 'Ϝᖴꓝ',
75
+ g: 'ԌᏀᏳꓖ',
76
+ h: 'ΗНҺᎻᏂᕼꓧⲎ',
77
+ i: 'ΙІᎥ',
78
+ j: 'ͿЈᎫᒍꓙ',
79
+ k: 'ΚКᏦꓗⲔ',
80
+ l: 'ӀᏞᒪꓡꓲǀⲒⳐⵏ',
81
+ m: 'ΜϺМᎷᗰꓟⲘ',
82
+ n: 'Ν𑪾ꓠⲚ',
83
+ o: 'ΟОՕꓳⲞⵔ',
84
+ p: 'ΡРᏢᑭꓑⲢⳎ',
85
+ q: 'Ԛⵕ',
86
+ r: 'ᎡᏒᖇꓣ',
87
+ s: 'ЅՏᏕᏚꓢ',
88
+ t: 'ΤТᎢꓔⲦ',
89
+ u: 'Սᑌꓴ',
90
+ v: 'ѴᏙᐯꓦ',
91
+ w: 'ԜᎳᏔꓪ',
92
+ x: 'ΧХꓫⲬⵝ',
93
+ y: 'ΥУҮᎩᎽꓬ',
94
+ z: 'ΖᏃ𑪼ꓜ',
95
+ });
96
+ // Small letters that read as a Latin letter. Cherokee small letters are the
97
+ // Cherokee capitals above, smaller, so they are added from that table.
98
+ const SMALL_LOOKALIKES = byLetter({
99
+ a: 'αаɑᴀⲁ',
100
+ b: 'βвᑲᖯʙⲃ',
101
+ c: 'сᲃᴄⲥ',
102
+ d: 'ԁᑯꓒđᴅ',
103
+ e: 'εеҽᴇⲉ',
104
+ f: 'քꜰ',
105
+ g: 'ցɡɢ',
106
+ h: 'һнհħʜⲏ',
107
+ i: 'ιіꙇւıɪɩⲓ',
108
+ j: 'ϳјյȷᴊ',
109
+ k: 'κкᴋⲕ',
110
+ l: 'ӏłʟⳑ',
111
+ m: 'мᴍ',
112
+ n: 'ηпոռɴⲛ',
113
+ o: 'οоᲂօøᴏⲟϭ',
114
+ p: 'ρϸрᴘⲣⳏ',
115
+ q: 'ԛգզꞯ',
116
+ r: 'ᴦгʀⲅ',
117
+ s: 'ѕꜱ',
118
+ t: 'τтŧᴛⲧ',
119
+ u: 'μυսᴜ',
120
+ v: 'νѵᴠ',
121
+ w: 'ωшԝѡաᴡꟺⲽ',
122
+ x: 'χхᕁᕽⲭ',
123
+ y: 'уүʏⲩ',
124
+ z: 'ᴢ',
125
+ ss: 'ß',
126
+ ae: 'æ',
127
+ oe: 'œ',
128
+ });
129
+ for (const [ch, latin] of Object.entries(CAPITAL_LOOKALIKES)) {
130
+ const small = ch.toLowerCase();
131
+ if (small !== ch && /\p{Script=Cherokee}/u.test(ch)) {
132
+ SMALL_LOOKALIKES[small] = latin;
133
+ }
134
+ }
135
+ // Digits and symbols used as letters.
136
+ const DIGIT_SWAPS = {
104
137
  '0': 'o',
105
138
  '1': 'l',
106
139
  '3': 'e',
@@ -113,20 +146,30 @@ const HOMOGLYPHS = {
113
146
  '|': 'l',
114
147
  '!': 'l',
115
148
  };
149
+ // One letter folded. A capital by the capital it looks like, else the
150
+ // letter lowercased and folded by the small letter it looks like.
151
+ function foldLetter(ch) {
152
+ const capital = CAPITAL_LOOKALIKES[ch];
153
+ if (capital !== undefined)
154
+ return capital;
155
+ const small = ch.toLowerCase();
156
+ return SMALL_LOOKALIKES[small] ?? DIGIT_SWAPS[small] ?? small;
157
+ }
116
158
  // i and l look alike in many fonts (GoogIe with a capital I), so both fold
117
- // to l. Run after HOMOGLYPHS, so a Cyrillic і, a Greek ι or a dotless ı,
118
- // which fold to i there, fold on to l as well.
159
+ // to l. Run after the lookalikes, so a Cyrillic і, a Greek ι or a dotless
160
+ // ı, which fold to i there, fold on to l as well.
119
161
  const I_AS_L = /i/g;
120
162
  const MARKS = /\p{M}/gu;
121
163
  const NOT_LETTER_OR_DIGIT = /[^\p{L}\p{N}]/gu;
122
- // The form two names are compared in. NFKC, lowercase, accents dropped,
123
- // homoglyphs and digit swaps folded, everything but letters and digits
124
- // dropped, then letter pairs that read as one letter (rn as m, vv as w)
125
- // folded. Open-AI, OPENAI and 0penai all give the same key. Letters of
126
- // scripts with no Latin lookalike are kept, so a display name in another
127
- // script still has a key of its own.
164
+ // The form two names are compared in. NFKC, accents dropped, lookalikes of
165
+ // other scripts and digit swaps folded by shape and the rest lowercased,
166
+ // everything but letters and digits dropped, then letter pairs that read
167
+ // as one letter (rn as m, vv as w) folded. Open-AI, OPENAI and 0penai all
168
+ // give the same key, and so does OPENAI with a Greek capital Omicron.
169
+ // Letters of scripts with no Latin lookalike are kept, so a display name
170
+ // in another script still has a key of its own.
128
171
  export function normaliseName(text) {
129
- const folded = Array.from(text.normalize('NFKC').toLowerCase().normalize('NFKD').replace(MARKS, ''), (ch) => HOMOGLYPHS[ch] ?? ch)
172
+ const folded = Array.from(text.normalize('NFKC').normalize('NFKD').replace(MARKS, ''), foldLetter)
130
173
  .join('')
131
174
  .replace(I_AS_L, 'l');
132
175
  return folded
@@ -403,11 +446,62 @@ export function moderateSlug(slug) {
403
446
  }
404
447
  return protectedRefusal(slug);
405
448
  }
449
+ // Scripts a name in Latin letters may mix with, the mixes Unicode's highly
450
+ // restrictive profile (UTS 39) allows. Han, Hiragana, Katakana, Hangul and
451
+ // Bopomofo, plus Common and Inherited, which hold letters every script
452
+ // shares. Any other script mixed with Latin is how a lookalike hides (Nvidia
453
+ // with a Greek capital Nu, Google with Armenian o, OpenAI with a Coptic O),
454
+ // so it is refused outright, whatever the tables above fold. A script not
455
+ // listed here is refused by default, so a new lookalike script needs no row.
456
+ // A name written in any one script alone is fine.
457
+ const MIXES_WITH_LATIN = /[\p{Script=Latin}\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Bopomofo}\p{Script=Common}\p{Script=Inherited}]/u;
458
+ // Names for the refusal message, for the scripts with the most Latin
459
+ // lookalikes. Any other script is "letters of another script".
460
+ const SCRIPT_NAMES = [
461
+ ['Greek letters', /\p{Script=Greek}/u],
462
+ ['Cyrillic letters', /\p{Script=Cyrillic}/u],
463
+ ['Armenian letters', /\p{Script=Armenian}/u],
464
+ ['Cherokee letters', /\p{Script=Cherokee}/u],
465
+ ['Lisu letters', /\p{Script=Lisu}/u],
466
+ ['Canadian syllabics', /\p{Script=Canadian_Aboriginal}/u],
467
+ ['Coptic letters', /\p{Script=Coptic}/u],
468
+ ['Tifinagh letters', /\p{Script=Tifinagh}/u],
469
+ ];
470
+ const LATIN = /\p{Script=Latin}/u;
471
+ const LETTER = /\p{L}/u;
472
+ // The micro sign and the ohm sign. NFKC turns them into Greek mu and
473
+ // Omega, and the ohm sign is Greek already, so they are dropped before the
474
+ // script check. 10kΩ Audio is a unit, not a Greek letter.
475
+ const UNIT_SIGNS = /[\u00B5\u2126]/gu;
476
+ // The script a text mixes with Latin letters, as the refusal names it, or
477
+ // null when it mixes none it may not. Read after NFKC, so fullwidth and
478
+ // styled Latin letters count as Latin.
479
+ export function mixedScriptOf(text) {
480
+ const letters = Array.from(text.replace(UNIT_SIGNS, '').normalize('NFKC')).filter((ch) => LETTER.test(ch));
481
+ if (!letters.some((ch) => LATIN.test(ch)))
482
+ return null;
483
+ const other = letters.find((ch) => !MIXES_WITH_LATIN.test(ch));
484
+ if (other === undefined)
485
+ return null;
486
+ const named = SCRIPT_NAMES.find(([, pattern]) => pattern.test(other));
487
+ return named ? named[0] : 'letters of another script';
488
+ }
406
489
  // The refusal for an operator display name, or null when it may be used.
407
- // The shape is OperatorDisplayName's job. Whether another operator holds
408
- // the same key is the API's check against display_name_key.
490
+ // The shape is OperatorDisplayName's job. Protected names first, then Latin
491
+ // mixed with a script it may not mix with. Whether another operator holds
492
+ // the same key is the API's check against display_name_key. Slugs need no
493
+ // script check, OperatorSlug allows a to z, digits and hyphens only.
409
494
  export function moderateDisplayName(name) {
410
- return protectedRefusal(name);
495
+ const refusal = protectedRefusal(name);
496
+ if (refusal)
497
+ return refusal;
498
+ const script = mixedScriptOf(name);
499
+ return script === null
500
+ ? null
501
+ : {
502
+ code: 'name_mixed_script',
503
+ message: `${name} mixes Latin letters with ${script}, so it can pass for another name. Use one script.`,
504
+ };
411
505
  }
412
506
  // ---------------------------------------------------------------------------
413
507
  // Default slugs
@@ -4,8 +4,8 @@ import { base64urlEncode, utf8Encode } from './base64url.js';
4
4
  import { CREDENTIAL_ISSUER, LEGACY_ISSUER_UNTIL, LEGACY_ISSUERS, SEAL_CLOCK_SKEW_SECONDS, SEAL_MAX_TTL_SECONDS, SEAL_VERSION, } from './credential.js';
5
5
  import { sign } from './envelope.js';
6
6
  const KID = 'vouched-conformance-1';
7
- // 1 October 2026, after LEGACY_UNTIL, so no case leans on the legacy shape,
8
- // and before LEGACY_ISSUER_UNTIL, so a legacy issuer is still accepted.
7
+ // 1 October 2026, before LEGACY_ISSUER_UNTIL, so a legacy issuer is still
8
+ // accepted.
9
9
  const NOW = 1_790_812_800;
10
10
  const LEGACY_ISSUER = LEGACY_ISSUERS[0];
11
11
  const HOUR = 3600;
@@ -0,0 +1,21 @@
1
+ import type { SealBrokenReason } from './api.js';
2
+ import { type SealPayload } from './credential.js';
3
+ export type SealKey = {
4
+ kid: string;
5
+ x: string;
6
+ };
7
+ export type SealVerified = {
8
+ ok: true;
9
+ kid: string;
10
+ payload: SealPayload;
11
+ expiresIn: number;
12
+ };
13
+ export type SealProblem = {
14
+ ok: false;
15
+ reason: SealBrokenReason;
16
+ kid: string | null;
17
+ signed?: unknown;
18
+ payload?: SealPayload;
19
+ };
20
+ export type SealVerification = SealVerified | SealProblem;
21
+ export declare function verifySeal(keys: readonly SealKey[], compact: string, nowSeconds: number): Promise<SealVerification>;
@@ -0,0 +1,63 @@
1
+ import { base64urlDecode } from './base64url.js';
2
+ import { parseSealPayload, sealIatProblem, } from './credential.js';
3
+ import { decodeHeader, EnvelopeError, verify } from './envelope.js';
4
+ /*
5
+ * Verifies a SEAL against the issuer's published keys, the steps of the
6
+ * standard (section 3) in order. The API's POST /v1/seal/verify, the web's
7
+ * verify tool and badge and `sealkeeper seal verify` all call this, and the
8
+ * SEAL conformance cases hold it to the standard's answers.
9
+ *
10
+ * 1. The header, which names the kid. Surrounding whitespace is dropped
11
+ * first, as a pasted SEAL often has it.
12
+ * 2. The key the kid names, among keys.
13
+ * 3. The signature over the exact header.payload bytes, and only then the
14
+ * payload as JSON. A signature that checks out over a payload that is
15
+ * not JSON is malformed, not bad_signature.
16
+ * 4. The issuer, then ver, then the shape for that ver, through
17
+ * parseSealPayload. So a SEAL from another issuer is wrong_issuer
18
+ * whatever its ver or shape.
19
+ * 5. exp, then iat, as the standard reads "Check exp is in the future and
20
+ * iat is not". An iat more than SEAL_CLOCK_SKEW_SECONDS ahead is
21
+ * not_yet_valid.
22
+ *
23
+ * nowSeconds is the verifier's clock in Unix seconds, passed in so tests
24
+ * can pin it.
25
+ */
26
+ export async function verifySeal(keys, compact, nowSeconds) {
27
+ const jws = compact.trim();
28
+ let kid;
29
+ try {
30
+ kid = decodeHeader(jws).kid;
31
+ }
32
+ catch {
33
+ return { ok: false, reason: 'malformed', kid: null };
34
+ }
35
+ const key = keys.find((k) => k.kid === kid);
36
+ if (!key)
37
+ return { ok: false, reason: 'unknown_kid', kid };
38
+ let signed;
39
+ try {
40
+ signed = (await verify(jws, base64urlDecode(key.x))).payload;
41
+ }
42
+ catch (error) {
43
+ return {
44
+ ok: false,
45
+ reason: error instanceof EnvelopeError && error.code === 'bad_signature'
46
+ ? 'bad_signature'
47
+ : 'malformed',
48
+ kid,
49
+ };
50
+ }
51
+ const parsed = parseSealPayload(signed, nowSeconds);
52
+ if (!parsed.ok)
53
+ return { ok: false, reason: parsed.reason, kid, signed };
54
+ const payload = parsed.payload;
55
+ const expiresIn = Math.floor(payload.exp - nowSeconds);
56
+ if (expiresIn <= 0) {
57
+ return { ok: false, reason: 'expired', kid, signed, payload };
58
+ }
59
+ const early = sealIatProblem(payload.iat, nowSeconds);
60
+ if (early)
61
+ return { ok: false, reason: early, kid, signed, payload };
62
+ return { ok: true, kid, payload, expiresIn };
63
+ }
package/dist/standing.js CHANGED
@@ -118,7 +118,7 @@ export const DORMANCY = {
118
118
  // (SCORING.levels in the API) and the CLI explains them, so they live here.
119
119
  // Task counts are counted evidence (CountedCounts), recalibrated on 26
120
120
  // September 2026 (VOU-139) from the simulation in
121
- // apps/api/src/scoring/simulation.ts. Seed tasks count in verifiedTasks at
121
+ // apps/api/src/test/simulation.ts. Seed tasks count in verifiedTasks at
122
122
  // every level (VOU-172, 26 September 2026), so an agent that does seed
123
123
  // tasks every day reaches silver on them alone. Silver dropped its
124
124
  // checked or confirmed, other operator and confirmed task clauses then,
package/dist/tasks.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  export declare const Sha256Hex: z.ZodString;
3
3
  export declare const JSON_SCHEMA_MAX_BYTES = 16384;
4
+ export declare const JSON_SCHEMA_MAX_DEPTH = 20;
5
+ export declare const JSON_SCHEMA_MAX_KEYS = 500;
4
6
  export declare const HashVerification: z.ZodObject<{
5
7
  kind: z.ZodLiteral<"hash">;
6
8
  sha256: z.ZodString;
package/dist/tasks.js CHANGED
@@ -1,16 +1,26 @@
1
1
  import { z } from 'zod';
2
- import { utf8Encode } from './base64url.js';
2
+ import { boundedJsonObject } from './json-shape.js';
3
3
  export const Sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
4
4
  export const JSON_SCHEMA_MAX_BYTES = 16384;
5
+ // Depth and key caps on a jsonSchema, checked before its bytes (VOU-214).
6
+ // The depth counts JSON levels, the top level object is 1. The API's schema
7
+ // check allows 8 schema levels (MAX_SCHEMA_DEPTH in apps/api), and a
8
+ // subschema under properties, allOf or an items list sits two JSON levels
9
+ // below its parent, so an accepted schema reaches about 17. 20 refuses none
10
+ // of them and still stops a deep bracket bomb.
11
+ export const JSON_SCHEMA_MAX_DEPTH = 20;
12
+ export const JSON_SCHEMA_MAX_KEYS = 500;
5
13
  export const HashVerification = z.strictObject({
6
14
  kind: z.literal('hash'),
7
15
  sha256: Sha256Hex,
8
16
  });
9
17
  export const SchemaVerification = z.strictObject({
10
18
  kind: z.literal('schema'),
11
- jsonSchema: z
12
- .record(z.string(), z.unknown())
13
- .refine((schema) => utf8Encode(JSON.stringify(schema)).length <= JSON_SCHEMA_MAX_BYTES, `jsonSchema must be at most ${JSON_SCHEMA_MAX_BYTES} bytes`),
19
+ jsonSchema: boundedJsonObject('jsonSchema', {
20
+ maxDepth: JSON_SCHEMA_MAX_DEPTH,
21
+ maxKeys: JSON_SCHEMA_MAX_KEYS,
22
+ maxBytes: JSON_SCHEMA_MAX_BYTES,
23
+ }),
14
24
  });
15
25
  export const CounterpartyVerification = z.strictObject({
16
26
  kind: z.literal('counterparty'),
package/package.json CHANGED
@@ -1,15 +1,10 @@
1
1
  {
2
2
  "name": "@sealkeeper/schema",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "private": false,
5
5
  "description": "Zod schemas, event taxonomy and signing helpers for SealKeeper.",
6
6
  "license": "Apache-2.0",
7
7
  "homepage": "https://sealkeeper.run/seal/standard",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/sealkeeper-dev/sealkeeper.git",
11
- "directory": "packages/schema"
12
- },
13
8
  "bugs": {
14
9
  "url": "https://github.com/sealkeeper-dev/cli/issues"
15
10
  },
@@ -33,6 +28,7 @@
33
28
  },
34
29
  "files": [
35
30
  "dist",
31
+ "!dist/db",
36
32
  "README.md",
37
33
  "LICENSE",
38
34
  "NOTICE"
@@ -1,75 +0,0 @@
1
- export declare const agentMilestones: import("drizzle-orm/pg-core").PgTableWithColumns<{
2
- name: "agent_milestones";
3
- schema: undefined;
4
- columns: {
5
- createdAt: import("drizzle-orm/pg-core").PgColumn<{
6
- name: "created_at";
7
- tableName: "agent_milestones";
8
- dataType: "date";
9
- columnType: "PgTimestamp";
10
- data: Date;
11
- driverParam: string;
12
- notNull: true;
13
- hasDefault: true;
14
- isPrimaryKey: false;
15
- isAutoincrement: false;
16
- hasRuntimeDefault: false;
17
- enumValues: undefined;
18
- baseColumn: never;
19
- identity: undefined;
20
- generated: undefined;
21
- }, {}, {}>;
22
- updatedAt: import("drizzle-orm/pg-core").PgColumn<{
23
- name: "updated_at";
24
- tableName: "agent_milestones";
25
- dataType: "date";
26
- columnType: "PgTimestamp";
27
- data: Date;
28
- driverParam: string;
29
- notNull: true;
30
- hasDefault: true;
31
- isPrimaryKey: false;
32
- isAutoincrement: false;
33
- hasRuntimeDefault: false;
34
- enumValues: undefined;
35
- baseColumn: never;
36
- identity: undefined;
37
- generated: undefined;
38
- }, {}, {}>;
39
- agentId: import("drizzle-orm/pg-core").PgColumn<{
40
- name: "agent_id";
41
- tableName: "agent_milestones";
42
- dataType: "string";
43
- columnType: "PgText";
44
- data: string;
45
- driverParam: string;
46
- notNull: true;
47
- hasDefault: false;
48
- isPrimaryKey: false;
49
- isAutoincrement: false;
50
- hasRuntimeDefault: false;
51
- enumValues: [string, ...string[]];
52
- baseColumn: never;
53
- identity: undefined;
54
- generated: undefined;
55
- }, {}, {}>;
56
- milestone: import("drizzle-orm/pg-core").PgColumn<{
57
- name: "milestone";
58
- tableName: "agent_milestones";
59
- dataType: "string";
60
- columnType: "PgText";
61
- data: string;
62
- driverParam: string;
63
- notNull: true;
64
- hasDefault: false;
65
- isPrimaryKey: false;
66
- isAutoincrement: false;
67
- hasRuntimeDefault: false;
68
- enumValues: [string, ...string[]];
69
- baseColumn: never;
70
- identity: undefined;
71
- generated: undefined;
72
- }, {}, {}>;
73
- };
74
- dialect: 'pg';
75
- }>;
@@ -1,16 +0,0 @@
1
- import { pgTable, primaryKey, text } from 'drizzle-orm/pg-core';
2
- import { agents } from './agents.js';
3
- import { timestamps } from './timestamps.js';
4
- // The feed milestones an agent has reached, one row per (agent, milestone).
5
- // The scoring run writes a milestone to the feed only when it has no row
6
- // here, so each is written once per agent, ever. Migration 0019 seeded it
7
- // from standing, so an agent that already stood above none, or already
8
- // passed a verified task count, before milestones existed is not told it
9
- // did so for the first time.
10
- export const agentMilestones = pgTable('agent_milestones', {
11
- agentId: text('agent_id')
12
- .notNull()
13
- .references(() => agents.id),
14
- milestone: text('milestone').notNull(),
15
- ...timestamps,
16
- }, (t) => [primaryKey({ columns: [t.agentId, t.milestone] })]);