@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.
- package/README.md +2 -2
- package/dist/api.d.ts +30 -38
- package/dist/api.js +87 -42
- package/dist/client-address.d.ts +6 -0
- package/dist/client-address.js +81 -0
- package/dist/credential.d.ts +1 -66
- package/dist/credential.js +25 -63
- package/dist/events.d.ts +1 -0
- package/dist/events.js +13 -1
- package/dist/goal.js +2 -2
- package/dist/hidden.d.ts +1 -0
- package/dist/hidden.js +5 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/json-shape.d.ts +9 -0
- package/dist/json-shape.js +70 -0
- package/dist/moderation.d.ts +2 -1
- package/dist/moderation.js +169 -75
- package/dist/seal-conformance.js +2 -2
- package/dist/seal-verify.d.ts +21 -0
- package/dist/seal-verify.js +63 -0
- package/dist/standing.js +1 -1
- package/dist/tasks.d.ts +2 -0
- package/dist/tasks.js +14 -4
- package/package.json +2 -6
- package/dist/db/agent-milestones.d.ts +0 -75
- package/dist/db/agent-milestones.js +0 -16
- package/dist/db/agent-renames.d.ts +0 -126
- package/dist/db/agent-renames.js +0 -20
- package/dist/db/agents.d.ts +0 -266
- package/dist/db/agents.js +0 -79
- package/dist/db/client.d.ts +0 -2471
- package/dist/db/client.js +0 -40
- package/dist/db/credentials.d.ts +0 -143
- package/dist/db/credentials.js +0 -16
- package/dist/db/deleted-operators.d.ts +0 -75
- package/dist/db/deleted-operators.js +0 -13
- package/dist/db/events.d.ts +0 -160
- package/dist/db/events.js +0 -41
- package/dist/db/feed-items.d.ts +0 -109
- package/dist/db/feed-items.js +0 -21
- package/dist/db/index.d.ts +0 -76
- package/dist/db/index.js +0 -28
- package/dist/db/migrate.d.ts +0 -1
- package/dist/db/migrate.js +0 -34
- package/dist/db/migrator.d.ts +0 -4
- package/dist/db/migrator.js +0 -26
- package/dist/db/operator-identities.d.ts +0 -211
- package/dist/db/operator-identities.js +0 -49
- package/dist/db/operator-level-grants.d.ts +0 -109
- package/dist/db/operator-level-grants.js +0 -30
- package/dist/db/operator-slugs.d.ts +0 -92
- package/dist/db/operator-slugs.js +0 -25
- package/dist/db/operators.d.ts +0 -228
- package/dist/db/operators.js +0 -39
- package/dist/db/quota-counters.d.ts +0 -109
- package/dist/db/quota-counters.js +0 -17
- package/dist/db/ratings.d.ts +0 -160
- package/dist/db/ratings.js +0 -27
- package/dist/db/scores.d.ts +0 -160
- package/dist/db/scores.js +0 -15
- package/dist/db/standing.d.ts +0 -264
- package/dist/db/standing.js +0 -34
- package/dist/db/task-claim-failures.d.ts +0 -143
- package/dist/db/task-claim-failures.js +0 -37
- package/dist/db/task-outcomes.d.ts +0 -145
- package/dist/db/task-outcomes.js +0 -25
- package/dist/db/tasks.d.ts +0 -381
- package/dist/db/tasks.js +0 -100
- package/dist/db/timestamps.d.ts +0 -4
- package/dist/db/timestamps.js +0 -24
- package/dist/db/url.d.ts +0 -12
- package/dist/db/url.js +0 -25
package/dist/moderation.js
CHANGED
|
@@ -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
|
-
//
|
|
46
|
-
// letters
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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,
|
|
123
|
-
//
|
|
124
|
-
// dropped, then letter pairs that read
|
|
125
|
-
// folded. Open-AI, OPENAI and 0penai all
|
|
126
|
-
//
|
|
127
|
-
//
|
|
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').
|
|
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.
|
|
408
|
-
//
|
|
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
|
-
|
|
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
|
package/dist/seal-conformance.js
CHANGED
|
@@ -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,
|
|
8
|
-
//
|
|
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/
|
|
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 {
|
|
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:
|
|
12
|
-
|
|
13
|
-
|
|
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.
|
|
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] })]);
|