@sealkeeper/schema 0.4.4 → 0.4.6
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/dist/agent-name.d.ts +1 -1
- package/dist/agent-name.js +4 -4
- package/dist/api.d.ts +625 -33
- package/dist/api.js +262 -57
- package/dist/badge.d.ts +1 -0
- package/dist/badge.js +7 -0
- package/dist/client-address.d.ts +6 -0
- package/dist/client-address.js +81 -0
- package/dist/db/agents.d.ts +155 -0
- package/dist/db/agents.js +38 -0
- package/dist/db/client.d.ts +431 -0
- package/dist/db/client.js +24 -1
- package/dist/db/events.js +7 -0
- package/dist/db/index.d.ts +16 -0
- package/dist/db/index.js +4 -0
- package/dist/db/migrate.js +5 -1
- package/dist/db/migrator.d.ts +5 -1
- package/dist/db/migrator.js +18 -2
- package/dist/db/operator-identities.d.ts +211 -0
- package/dist/db/operator-identities.js +49 -0
- package/dist/db/operator-level-grants.d.ts +109 -0
- package/dist/db/operator-level-grants.js +30 -0
- package/dist/db/operator-slugs.d.ts +92 -0
- package/dist/db/operator-slugs.js +25 -0
- package/dist/db/operators.d.ts +119 -0
- package/dist/db/operators.js +30 -2
- package/dist/db/standing.d.ts +1 -0
- package/dist/db/standing.js +2 -0
- package/dist/db/task-claim-failures.d.ts +143 -0
- package/dist/db/task-claim-failures.js +37 -0
- package/dist/db/tasks.d.ts +60 -0
- package/dist/db/tasks.js +19 -1
- package/dist/envelope.d.ts +3 -0
- package/dist/envelope.js +16 -11
- package/dist/goal.d.ts +62 -1
- package/dist/goal.js +70 -11
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/json-shape.d.ts +9 -0
- package/dist/json-shape.js +70 -0
- package/dist/moderation.d.ts +29 -0
- package/dist/moderation.js +547 -0
- package/dist/operator-domains.d.ts +104 -0
- package/dist/operator-domains.js +85 -0
- package/dist/policy.d.ts +4 -0
- package/dist/policy.js +10 -0
- package/dist/runtime.d.ts +17 -0
- package/dist/runtime.js +61 -0
- package/dist/seal-conformance.js +2 -0
- package/dist/standing.d.ts +27 -8
- package/dist/standing.js +59 -22
- package/dist/tasks.d.ts +25 -1
- package/dist/tasks.js +43 -9
- package/package.json +1 -1
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Moderation of the names an operator chooses, the operator slug and the
|
|
3
|
+
// operator display name. The API runs these checks on every write and is
|
|
4
|
+
// the only enforcement. The CLI and the web run the same checks for early
|
|
5
|
+
// feedback. A name another operator already holds is the API's call, since
|
|
6
|
+
// only the database knows (409 name_taken).
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Shapes
|
|
9
|
+
export const OPERATOR_SLUG_MIN = 1;
|
|
10
|
+
// 39, like a GitHub login. A 39 character slug, a slash and a 39 character
|
|
11
|
+
// agent name make a 79 character handle, the AgentHandle cap that published
|
|
12
|
+
// CLIs bundle.
|
|
13
|
+
export const OPERATOR_SLUG_MAX = 39;
|
|
14
|
+
export const OPERATOR_DISPLAY_NAME_MIN = 1;
|
|
15
|
+
export const OPERATOR_DISPLAY_NAME_MAX = 64;
|
|
16
|
+
const SLUG = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
|
|
17
|
+
// The operator's slug, the first half of every handle, as in alice/claude-code.
|
|
18
|
+
// Lowercase letters, digits and single hyphens, starting and ending with a
|
|
19
|
+
// letter or digit. Unique across SealKeeper. Reserved words and protected
|
|
20
|
+
// names are refused by moderateSlug, not by this shape.
|
|
21
|
+
export const OperatorSlug = z
|
|
22
|
+
.string()
|
|
23
|
+
.min(OPERATOR_SLUG_MIN)
|
|
24
|
+
.max(OPERATOR_SLUG_MAX)
|
|
25
|
+
.regex(SLUG, 'Use lowercase letters, digits and hyphens, starting and ending with a letter or digit')
|
|
26
|
+
.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
|
+
const LETTER_OR_DIGIT = /[\p{L}\p{N}]/u;
|
|
32
|
+
// The operator's display name. Free text, no control or format characters,
|
|
33
|
+
// no space at either end, and at least one letter or digit, so its
|
|
34
|
+
// normalised key is never empty. Protected names are refused by
|
|
35
|
+
// moderateDisplayName.
|
|
36
|
+
export const OperatorDisplayName = z
|
|
37
|
+
.string()
|
|
38
|
+
.min(OPERATOR_DISPLAY_NAME_MIN)
|
|
39
|
+
.max(OPERATOR_DISPLAY_NAME_MAX)
|
|
40
|
+
.refine((name) => !HIDDEN.test(name), 'No control characters')
|
|
41
|
+
.refine((name) => name === name.trim(), 'No space at the start or end')
|
|
42
|
+
.refine((name) => LETTER_OR_DIGIT.test(name), 'Use at least one letter or digit');
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Normalisation
|
|
45
|
+
// Lookalikes, folded to the Latin letter they read as. Each row is a Latin
|
|
46
|
+
// letter and the letters of other scripts that read as it, in the order
|
|
47
|
+
// Greek, Cyrillic, Armenian, Cherokee, Canadian syllabics, Lisu, Latin,
|
|
48
|
+
// Coptic, Tifinagh. The letters come from Unicode's confusables.txt, one
|
|
49
|
+
// letter to one Latin letter, for these scripts only, plus the Latin small
|
|
50
|
+
// capitals and a few Latin letters NFKD does not split into a base and a
|
|
51
|
+
// mark. Coptic alfa and eie (Ⲁ, Ⲉ) are added by eye, they are not in the
|
|
52
|
+
// list. Letters NFKC or
|
|
53
|
+
// NFKD turn into another letter (the lunate sigma, the rho symbol, ё) are
|
|
54
|
+
// left out, since they never reach the tables.
|
|
55
|
+
function byLetter(rows) {
|
|
56
|
+
const table = {};
|
|
57
|
+
for (const [latin, lookalikes] of Object.entries(rows)) {
|
|
58
|
+
for (const ch of lookalikes) {
|
|
59
|
+
if (ch in table)
|
|
60
|
+
throw new Error(`${ch} is in two lookalike rows`);
|
|
61
|
+
table[ch] = latin;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return table;
|
|
65
|
+
}
|
|
66
|
+
// Capitals, and letters of scripts without case, that read as a Latin
|
|
67
|
+
// capital. Folded before a letter is lowercased, since lowercasing first
|
|
68
|
+
// would turn Greek capital Nu into nu, which reads as v, and a Cherokee
|
|
69
|
+
// capital into a Cherokee small letter. So capital Nu is n here and small
|
|
70
|
+
// nu is v in SMALL_LOOKALIKES.
|
|
71
|
+
const CAPITAL_LOOKALIKES = byLetter({
|
|
72
|
+
a: 'ΑАᎪᗅꓮⲀ',
|
|
73
|
+
b: 'ΒВЬᏏᏴᗷꓐⲂ',
|
|
74
|
+
c: 'СᏟꓚⲤ',
|
|
75
|
+
d: 'ᎠᏧᗞᗪꓓ',
|
|
76
|
+
e: 'ΕЕᎬꓰⲈⴹ',
|
|
77
|
+
f: 'Ϝᖴꓝ',
|
|
78
|
+
g: 'ԌᏀᏳꓖ',
|
|
79
|
+
h: 'ΗНҺᎻᏂᕼꓧⲎ',
|
|
80
|
+
i: 'ΙІᎥ',
|
|
81
|
+
j: 'ͿЈᎫᒍꓙ',
|
|
82
|
+
k: 'ΚКᏦꓗⲔ',
|
|
83
|
+
l: 'ӀᏞᒪꓡꓲǀⲒⳐⵏ',
|
|
84
|
+
m: 'ΜϺМᎷᗰꓟⲘ',
|
|
85
|
+
n: 'Ν𑪾ꓠⲚ',
|
|
86
|
+
o: 'ΟОՕꓳⲞⵔ',
|
|
87
|
+
p: 'ΡРᏢᑭꓑⲢⳎ',
|
|
88
|
+
q: 'Ԛⵕ',
|
|
89
|
+
r: 'ᎡᏒᖇꓣ',
|
|
90
|
+
s: 'ЅՏᏕᏚꓢ',
|
|
91
|
+
t: 'ΤТᎢꓔⲦ',
|
|
92
|
+
u: 'Սᑌꓴ',
|
|
93
|
+
v: 'ѴᏙᐯꓦ',
|
|
94
|
+
w: 'ԜᎳᏔꓪ',
|
|
95
|
+
x: 'ΧХꓫⲬⵝ',
|
|
96
|
+
y: 'ΥУҮᎩᎽꓬ',
|
|
97
|
+
z: 'ΖᏃ𑪼ꓜ',
|
|
98
|
+
});
|
|
99
|
+
// Small letters that read as a Latin letter. Cherokee small letters are the
|
|
100
|
+
// Cherokee capitals above, smaller, so they are added from that table.
|
|
101
|
+
const SMALL_LOOKALIKES = byLetter({
|
|
102
|
+
a: 'αаɑᴀⲁ',
|
|
103
|
+
b: 'βвᑲᖯʙⲃ',
|
|
104
|
+
c: 'сᲃᴄⲥ',
|
|
105
|
+
d: 'ԁᑯꓒđᴅ',
|
|
106
|
+
e: 'εеҽᴇⲉ',
|
|
107
|
+
f: 'քꜰ',
|
|
108
|
+
g: 'ցɡɢ',
|
|
109
|
+
h: 'һнհħʜⲏ',
|
|
110
|
+
i: 'ιіꙇւıɪɩⲓ',
|
|
111
|
+
j: 'ϳјյȷᴊ',
|
|
112
|
+
k: 'κкᴋⲕ',
|
|
113
|
+
l: 'ӏłʟⳑ',
|
|
114
|
+
m: 'мᴍ',
|
|
115
|
+
n: 'ηпոռɴⲛ',
|
|
116
|
+
o: 'οоᲂօøᴏⲟϭ',
|
|
117
|
+
p: 'ρϸрᴘⲣⳏ',
|
|
118
|
+
q: 'ԛգզꞯ',
|
|
119
|
+
r: 'ᴦгʀⲅ',
|
|
120
|
+
s: 'ѕꜱ',
|
|
121
|
+
t: 'τтŧᴛⲧ',
|
|
122
|
+
u: 'μυսᴜ',
|
|
123
|
+
v: 'νѵᴠ',
|
|
124
|
+
w: 'ωшԝѡաᴡꟺⲽ',
|
|
125
|
+
x: 'χхᕁᕽⲭ',
|
|
126
|
+
y: 'уүʏⲩ',
|
|
127
|
+
z: 'ᴢ',
|
|
128
|
+
ss: 'ß',
|
|
129
|
+
ae: 'æ',
|
|
130
|
+
oe: 'œ',
|
|
131
|
+
});
|
|
132
|
+
for (const [ch, latin] of Object.entries(CAPITAL_LOOKALIKES)) {
|
|
133
|
+
const small = ch.toLowerCase();
|
|
134
|
+
if (small !== ch && /\p{Script=Cherokee}/u.test(ch)) {
|
|
135
|
+
SMALL_LOOKALIKES[small] = latin;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Digits and symbols used as letters.
|
|
139
|
+
const DIGIT_SWAPS = {
|
|
140
|
+
'0': 'o',
|
|
141
|
+
'1': 'l',
|
|
142
|
+
'3': 'e',
|
|
143
|
+
'4': 'a',
|
|
144
|
+
'5': 's',
|
|
145
|
+
'7': 't',
|
|
146
|
+
'8': 'b',
|
|
147
|
+
'@': 'a',
|
|
148
|
+
$: 's',
|
|
149
|
+
'|': 'l',
|
|
150
|
+
'!': 'l',
|
|
151
|
+
};
|
|
152
|
+
// One letter folded. A capital by the capital it looks like, else the
|
|
153
|
+
// letter lowercased and folded by the small letter it looks like.
|
|
154
|
+
function foldLetter(ch) {
|
|
155
|
+
const capital = CAPITAL_LOOKALIKES[ch];
|
|
156
|
+
if (capital !== undefined)
|
|
157
|
+
return capital;
|
|
158
|
+
const small = ch.toLowerCase();
|
|
159
|
+
return SMALL_LOOKALIKES[small] ?? DIGIT_SWAPS[small] ?? small;
|
|
160
|
+
}
|
|
161
|
+
// i and l look alike in many fonts (GoogIe with a capital I), so both fold
|
|
162
|
+
// to l. Run after the lookalikes, so a Cyrillic і, a Greek ι or a dotless
|
|
163
|
+
// ı, which fold to i there, fold on to l as well.
|
|
164
|
+
const I_AS_L = /i/g;
|
|
165
|
+
const MARKS = /\p{M}/gu;
|
|
166
|
+
const NOT_LETTER_OR_DIGIT = /[^\p{L}\p{N}]/gu;
|
|
167
|
+
// The form two names are compared in. NFKC, accents dropped, lookalikes of
|
|
168
|
+
// other scripts and digit swaps folded by shape and the rest lowercased,
|
|
169
|
+
// everything but letters and digits dropped, then letter pairs that read
|
|
170
|
+
// as one letter (rn as m, vv as w) folded. Open-AI, OPENAI and 0penai all
|
|
171
|
+
// give the same key, and so does OPENAI with a Greek capital Omicron.
|
|
172
|
+
// Letters of scripts with no Latin lookalike are kept, so a display name
|
|
173
|
+
// in another script still has a key of its own.
|
|
174
|
+
export function normaliseName(text) {
|
|
175
|
+
const folded = Array.from(text.normalize('NFKC').normalize('NFKD').replace(MARKS, ''), foldLetter)
|
|
176
|
+
.join('')
|
|
177
|
+
.replace(I_AS_L, 'l');
|
|
178
|
+
return folded
|
|
179
|
+
.replace(NOT_LETTER_OR_DIGIT, '')
|
|
180
|
+
.replace(/rn/g, 'm')
|
|
181
|
+
.replace(/vv/g, 'w');
|
|
182
|
+
}
|
|
183
|
+
// The display_name_key column. Unique across operators, so two display
|
|
184
|
+
// names that read the same cannot both be held.
|
|
185
|
+
export const displayNameKey = normaliseName;
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Protected names
|
|
188
|
+
// The top AI and technology companies and products. A slug or display name
|
|
189
|
+
// that normalises to one of these, with or without a common suffix, is
|
|
190
|
+
// refused. Written as the name reads. The list protects the distinctive
|
|
191
|
+
// form only, so Stability AI protects stabilityai (and stability-ai-labs)
|
|
192
|
+
// but not the plain word stability. Products that are also common first
|
|
193
|
+
// names (Devin, Alexa, Siri) are left out, since a login such as devin
|
|
194
|
+
// would otherwise lose its slug. Claude is the one exception.
|
|
195
|
+
export const PROTECTED_NAMES = [
|
|
196
|
+
// SealKeeper itself
|
|
197
|
+
'SealKeeper',
|
|
198
|
+
'Vouched',
|
|
199
|
+
// AI labs and model makers
|
|
200
|
+
'Anthropic',
|
|
201
|
+
'Claude',
|
|
202
|
+
'OpenAI',
|
|
203
|
+
'ChatGPT',
|
|
204
|
+
'GPT',
|
|
205
|
+
'Codex',
|
|
206
|
+
'DALL-E',
|
|
207
|
+
'Google',
|
|
208
|
+
'Alphabet',
|
|
209
|
+
'DeepMind',
|
|
210
|
+
'Gemini',
|
|
211
|
+
'Microsoft',
|
|
212
|
+
'Copilot',
|
|
213
|
+
'Bing',
|
|
214
|
+
'Azure',
|
|
215
|
+
'Meta',
|
|
216
|
+
'Facebook',
|
|
217
|
+
'Instagram',
|
|
218
|
+
'WhatsApp',
|
|
219
|
+
'Llama',
|
|
220
|
+
'xAI',
|
|
221
|
+
'Grok',
|
|
222
|
+
'Twitter',
|
|
223
|
+
'Mistral',
|
|
224
|
+
'Le Chat',
|
|
225
|
+
'Cohere',
|
|
226
|
+
'DeepSeek',
|
|
227
|
+
'Qwen',
|
|
228
|
+
'Alibaba',
|
|
229
|
+
'Zhipu',
|
|
230
|
+
'Baidu',
|
|
231
|
+
'ByteDance',
|
|
232
|
+
'TikTok',
|
|
233
|
+
'Tencent',
|
|
234
|
+
'Perplexity',
|
|
235
|
+
'Character AI',
|
|
236
|
+
'Stability AI',
|
|
237
|
+
'Midjourney',
|
|
238
|
+
'ElevenLabs',
|
|
239
|
+
'Hugging Face',
|
|
240
|
+
'Scale AI',
|
|
241
|
+
'AI21',
|
|
242
|
+
'Nous Research',
|
|
243
|
+
'Together AI',
|
|
244
|
+
'Groq',
|
|
245
|
+
'Cerebras',
|
|
246
|
+
'Ollama',
|
|
247
|
+
'LangChain',
|
|
248
|
+
'LlamaIndex',
|
|
249
|
+
'CrewAI',
|
|
250
|
+
'AutoGPT',
|
|
251
|
+
'Mastra',
|
|
252
|
+
'OpenClaw',
|
|
253
|
+
// Coding agents and developer tools
|
|
254
|
+
'Cursor',
|
|
255
|
+
'Anysphere',
|
|
256
|
+
'Windsurf',
|
|
257
|
+
'Codeium',
|
|
258
|
+
'Replit',
|
|
259
|
+
'Lovable',
|
|
260
|
+
'StackBlitz',
|
|
261
|
+
'Vercel',
|
|
262
|
+
'Sourcegraph',
|
|
263
|
+
'Tabnine',
|
|
264
|
+
'JetBrains',
|
|
265
|
+
'GitHub',
|
|
266
|
+
'GitLab',
|
|
267
|
+
'Bitbucket',
|
|
268
|
+
'Atlassian',
|
|
269
|
+
'Notion',
|
|
270
|
+
'Slack',
|
|
271
|
+
'Discord',
|
|
272
|
+
'Figma',
|
|
273
|
+
'Stripe',
|
|
274
|
+
'Supabase',
|
|
275
|
+
'Cloudflare',
|
|
276
|
+
'Netlify',
|
|
277
|
+
'Docker',
|
|
278
|
+
'npm',
|
|
279
|
+
// Platforms and hardware
|
|
280
|
+
'Apple',
|
|
281
|
+
'Amazon',
|
|
282
|
+
'AWS',
|
|
283
|
+
'Nvidia',
|
|
284
|
+
'AMD',
|
|
285
|
+
'Intel',
|
|
286
|
+
'IBM',
|
|
287
|
+
'Oracle',
|
|
288
|
+
'Salesforce',
|
|
289
|
+
'Samsung',
|
|
290
|
+
'Adobe',
|
|
291
|
+
'Netflix',
|
|
292
|
+
'Uber',
|
|
293
|
+
'Palantir',
|
|
294
|
+
'Databricks',
|
|
295
|
+
'Snowflake',
|
|
296
|
+
'Zapier',
|
|
297
|
+
];
|
|
298
|
+
// Suffixes an impersonator adds to a protected name. Stripped one at a time
|
|
299
|
+
// from the end, and the rest compared again.
|
|
300
|
+
export const PROTECTED_SUFFIXES = [
|
|
301
|
+
'inc',
|
|
302
|
+
'labs',
|
|
303
|
+
'ai',
|
|
304
|
+
'hq',
|
|
305
|
+
'official',
|
|
306
|
+
'team',
|
|
307
|
+
];
|
|
308
|
+
// Prefixes an impersonator puts in front, as in the-real-openai. Stripped
|
|
309
|
+
// from the start the same way.
|
|
310
|
+
export const PROTECTED_PREFIXES = [
|
|
311
|
+
'official',
|
|
312
|
+
'real',
|
|
313
|
+
'the',
|
|
314
|
+
'team',
|
|
315
|
+
];
|
|
316
|
+
const PROTECTED_KEYS = new Map(PROTECTED_NAMES.map((name) => [normaliseName(name), name]));
|
|
317
|
+
const SUFFIX_KEYS = PROTECTED_SUFFIXES.map(normaliseName);
|
|
318
|
+
const PREFIX_KEYS = PROTECTED_PREFIXES.map(normaliseName);
|
|
319
|
+
// Every form of a key a protected name could hide in. The key itself, then
|
|
320
|
+
// the key with every combination of prefixes and suffixes stripped. Never
|
|
321
|
+
// empty.
|
|
322
|
+
function keyForms(key) {
|
|
323
|
+
const forms = new Set();
|
|
324
|
+
const queue = [key];
|
|
325
|
+
while (queue.length > 0) {
|
|
326
|
+
const form = queue.pop();
|
|
327
|
+
if (form === '' || forms.has(form))
|
|
328
|
+
continue;
|
|
329
|
+
forms.add(form);
|
|
330
|
+
for (const suffix of SUFFIX_KEYS) {
|
|
331
|
+
if (form.length > suffix.length && form.endsWith(suffix)) {
|
|
332
|
+
queue.push(form.slice(0, -suffix.length));
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
for (const prefix of PREFIX_KEYS) {
|
|
336
|
+
if (form.length > prefix.length && form.startsWith(prefix)) {
|
|
337
|
+
queue.push(form.slice(prefix.length));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return [...forms];
|
|
342
|
+
}
|
|
343
|
+
// The protected name a text impersonates, as written in PROTECTED_NAMES, or
|
|
344
|
+
// null when it impersonates none.
|
|
345
|
+
export function protectedNameOf(text) {
|
|
346
|
+
for (const form of keyForms(normaliseName(text))) {
|
|
347
|
+
const name = PROTECTED_KEYS.get(form);
|
|
348
|
+
if (name !== undefined)
|
|
349
|
+
return name;
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// Reserved words
|
|
355
|
+
// Route and product words a slug would collide with or read as part of the
|
|
356
|
+
// site. Slugs only, a display name is never part of a URL.
|
|
357
|
+
export const RESERVED_WORDS = [
|
|
358
|
+
'about',
|
|
359
|
+
'account',
|
|
360
|
+
'accounts',
|
|
361
|
+
'admin',
|
|
362
|
+
'administrator',
|
|
363
|
+
'agent',
|
|
364
|
+
'agents',
|
|
365
|
+
'anonymous',
|
|
366
|
+
'api',
|
|
367
|
+
'app',
|
|
368
|
+
'assets',
|
|
369
|
+
'auth',
|
|
370
|
+
'avatar',
|
|
371
|
+
'avatars',
|
|
372
|
+
'badge',
|
|
373
|
+
'badges',
|
|
374
|
+
'blog',
|
|
375
|
+
'callback',
|
|
376
|
+
'check',
|
|
377
|
+
'cli',
|
|
378
|
+
'dashboard',
|
|
379
|
+
'directory',
|
|
380
|
+
'docs',
|
|
381
|
+
'feed',
|
|
382
|
+
'help',
|
|
383
|
+
'home',
|
|
384
|
+
'join',
|
|
385
|
+
'leaderboard',
|
|
386
|
+
'legal',
|
|
387
|
+
'login',
|
|
388
|
+
'logout',
|
|
389
|
+
'me',
|
|
390
|
+
'moderator',
|
|
391
|
+
'new',
|
|
392
|
+
'null',
|
|
393
|
+
'oauth',
|
|
394
|
+
'official',
|
|
395
|
+
'operator',
|
|
396
|
+
'operators',
|
|
397
|
+
'privacy',
|
|
398
|
+
'profile',
|
|
399
|
+
'prove',
|
|
400
|
+
'register',
|
|
401
|
+
'root',
|
|
402
|
+
'search',
|
|
403
|
+
'seal',
|
|
404
|
+
'seals',
|
|
405
|
+
'sealkeeper',
|
|
406
|
+
'security',
|
|
407
|
+
'settings',
|
|
408
|
+
'signin',
|
|
409
|
+
'signout',
|
|
410
|
+
'signup',
|
|
411
|
+
'staff',
|
|
412
|
+
'static',
|
|
413
|
+
'status',
|
|
414
|
+
'support',
|
|
415
|
+
'system',
|
|
416
|
+
'task',
|
|
417
|
+
'tasks',
|
|
418
|
+
'terms',
|
|
419
|
+
'undefined',
|
|
420
|
+
'v1',
|
|
421
|
+
'verify',
|
|
422
|
+
'vouched',
|
|
423
|
+
'well-known',
|
|
424
|
+
'what-is-shared',
|
|
425
|
+
'www',
|
|
426
|
+
];
|
|
427
|
+
// Compared in normalised form, so adm1n and a-p-i are refused as well.
|
|
428
|
+
const RESERVED_KEYS = new Set(RESERVED_WORDS.map(normaliseName));
|
|
429
|
+
export function isReservedWord(text) {
|
|
430
|
+
return RESERVED_KEYS.has(normaliseName(text));
|
|
431
|
+
}
|
|
432
|
+
function protectedRefusal(text) {
|
|
433
|
+
const name = protectedNameOf(text);
|
|
434
|
+
return name === null
|
|
435
|
+
? null
|
|
436
|
+
: {
|
|
437
|
+
code: 'name_protected',
|
|
438
|
+
message: `${text} reads as ${name}, which is a protected name. Pick another.`,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
// The refusal for an operator slug, or null when it may be used. The shape
|
|
442
|
+
// is OperatorSlug's job, this checks reserved words and protected names.
|
|
443
|
+
export function moderateSlug(slug) {
|
|
444
|
+
if (isReservedWord(slug)) {
|
|
445
|
+
return {
|
|
446
|
+
code: 'name_reserved',
|
|
447
|
+
message: `${slug} is reserved by SealKeeper. Pick another.`,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return protectedRefusal(slug);
|
|
451
|
+
}
|
|
452
|
+
// Scripts a name in Latin letters may mix with, the mixes Unicode's highly
|
|
453
|
+
// restrictive profile (UTS 39) allows. Han, Hiragana, Katakana, Hangul and
|
|
454
|
+
// Bopomofo, plus Common and Inherited, which hold letters every script
|
|
455
|
+
// shares. Any other script mixed with Latin is how a lookalike hides (Nvidia
|
|
456
|
+
// with a Greek capital Nu, Google with Armenian o, OpenAI with a Coptic O),
|
|
457
|
+
// so it is refused outright, whatever the tables above fold. A script not
|
|
458
|
+
// listed here is refused by default, so a new lookalike script needs no row.
|
|
459
|
+
// A name written in any one script alone is fine.
|
|
460
|
+
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;
|
|
461
|
+
// Names for the refusal message, for the scripts with the most Latin
|
|
462
|
+
// lookalikes. Any other script is "letters of another script".
|
|
463
|
+
const SCRIPT_NAMES = [
|
|
464
|
+
['Greek letters', /\p{Script=Greek}/u],
|
|
465
|
+
['Cyrillic letters', /\p{Script=Cyrillic}/u],
|
|
466
|
+
['Armenian letters', /\p{Script=Armenian}/u],
|
|
467
|
+
['Cherokee letters', /\p{Script=Cherokee}/u],
|
|
468
|
+
['Lisu letters', /\p{Script=Lisu}/u],
|
|
469
|
+
['Canadian syllabics', /\p{Script=Canadian_Aboriginal}/u],
|
|
470
|
+
['Coptic letters', /\p{Script=Coptic}/u],
|
|
471
|
+
['Tifinagh letters', /\p{Script=Tifinagh}/u],
|
|
472
|
+
];
|
|
473
|
+
const LATIN = /\p{Script=Latin}/u;
|
|
474
|
+
const LETTER = /\p{L}/u;
|
|
475
|
+
// The micro sign and the ohm sign. NFKC turns them into Greek mu and
|
|
476
|
+
// Omega, and the ohm sign is Greek already, so they are dropped before the
|
|
477
|
+
// script check. 10kΩ Audio is a unit, not a Greek letter.
|
|
478
|
+
const UNIT_SIGNS = /[\u00B5\u2126]/gu;
|
|
479
|
+
// The script a text mixes with Latin letters, as the refusal names it, or
|
|
480
|
+
// null when it mixes none it may not. Read after NFKC, so fullwidth and
|
|
481
|
+
// styled Latin letters count as Latin.
|
|
482
|
+
export function mixedScriptOf(text) {
|
|
483
|
+
const letters = Array.from(text.replace(UNIT_SIGNS, '').normalize('NFKC')).filter((ch) => LETTER.test(ch));
|
|
484
|
+
if (!letters.some((ch) => LATIN.test(ch)))
|
|
485
|
+
return null;
|
|
486
|
+
const other = letters.find((ch) => !MIXES_WITH_LATIN.test(ch));
|
|
487
|
+
if (other === undefined)
|
|
488
|
+
return null;
|
|
489
|
+
const named = SCRIPT_NAMES.find(([, pattern]) => pattern.test(other));
|
|
490
|
+
return named ? named[0] : 'letters of another script';
|
|
491
|
+
}
|
|
492
|
+
// The refusal for an operator display name, or null when it may be used.
|
|
493
|
+
// The shape is OperatorDisplayName's job. Protected names first, then Latin
|
|
494
|
+
// mixed with a script it may not mix with. Whether another operator holds
|
|
495
|
+
// the same key is the API's check against display_name_key. Slugs need no
|
|
496
|
+
// script check, OperatorSlug allows a to z, digits and hyphens only.
|
|
497
|
+
export function moderateDisplayName(name) {
|
|
498
|
+
const refusal = protectedRefusal(name);
|
|
499
|
+
if (refusal)
|
|
500
|
+
return refusal;
|
|
501
|
+
const script = mixedScriptOf(name);
|
|
502
|
+
return script === null
|
|
503
|
+
? null
|
|
504
|
+
: {
|
|
505
|
+
code: 'name_mixed_script',
|
|
506
|
+
message: `${name} mixes Latin letters with ${script}, so it can pass for another name. Use one script.`,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
// ---------------------------------------------------------------------------
|
|
510
|
+
// Default slugs
|
|
511
|
+
// A login as a slug. Lowercase, every run of characters other than a to z
|
|
512
|
+
// and 0 to 9 made one hyphen, hyphens trimmed from the ends, since an old
|
|
513
|
+
// GitHub login can end in one. A login is not always letters, digits and
|
|
514
|
+
// hyphens (an Enterprise Managed User login ends in _shortcode), and a
|
|
515
|
+
// slug must be. The same rule as sk_base in migration 0025. The API's
|
|
516
|
+
// default slug for a new operator starts from it.
|
|
517
|
+
export function slugBase(login) {
|
|
518
|
+
const base = login
|
|
519
|
+
.toLowerCase()
|
|
520
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
521
|
+
.replace(/^-|-$/g, '')
|
|
522
|
+
.slice(0, OPERATOR_SLUG_MAX)
|
|
523
|
+
.replace(/-$/, '');
|
|
524
|
+
return base === '' ? 'operator' : base;
|
|
525
|
+
}
|
|
526
|
+
// base with -n on the end, cut so the whole stays within the slug limit.
|
|
527
|
+
export const suffixedSlug = (base, n) => `${base.slice(0, OPERATOR_SLUG_MAX - `-${n}`.length).replace(/-+$/, '')}-${n}`;
|
|
528
|
+
// The reason the slug is the login's slug with a -2, -3 and so on, or null
|
|
529
|
+
// when it is not. A slug the operator chose, or one left behind by a GitHub
|
|
530
|
+
// login change, is not of that form and gives null. The caller also checks
|
|
531
|
+
// that the slug never changed, since an operator can pick a suffixed slug.
|
|
532
|
+
// reserved and protected are read from today's lists, taken is the rest.
|
|
533
|
+
export function slugSuffixReason(login, slug) {
|
|
534
|
+
const base = slugBase(login);
|
|
535
|
+
const n = /-([0-9]+)$/.exec(slug)?.[1];
|
|
536
|
+
if (n === undefined || slug === base)
|
|
537
|
+
return null;
|
|
538
|
+
const k = Number(n);
|
|
539
|
+
if (k < 2 || suffixedSlug(base, k) !== slug)
|
|
540
|
+
return null;
|
|
541
|
+
const refusal = moderateSlug(base);
|
|
542
|
+
if (refusal?.code === 'name_reserved')
|
|
543
|
+
return 'reserved';
|
|
544
|
+
if (refusal?.code === 'name_protected')
|
|
545
|
+
return 'protected';
|
|
546
|
+
return 'taken';
|
|
547
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const DNS_RECORD_LABEL = "_sealkeeper";
|
|
3
|
+
export declare const DNS_RECORD_PREFIX = "sealkeeper-verify=";
|
|
4
|
+
export declare const DNS_IDENTITY_KIND = "https://sealkeeper.run/seal/identity/dns";
|
|
5
|
+
export declare const DNS_IDENTITY_PROVIDER = "https://sealkeeper.run";
|
|
6
|
+
export declare const OPERATOR_VERIFICATION: {
|
|
7
|
+
readonly graceDays: 14;
|
|
8
|
+
readonly recheckHours: 24;
|
|
9
|
+
readonly maxDomains: 5;
|
|
10
|
+
};
|
|
11
|
+
export declare const DomainName: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodString>;
|
|
12
|
+
export type DomainName = z.infer<typeof DomainName>;
|
|
13
|
+
export declare const AddDomainRequest: z.ZodObject<{
|
|
14
|
+
domain: z.ZodPipe<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>, z.ZodString>;
|
|
15
|
+
}, z.core.$strict>;
|
|
16
|
+
export type AddDomainRequest = z.infer<typeof AddDomainRequest>;
|
|
17
|
+
export declare const DomainParams: z.ZodObject<{
|
|
18
|
+
id: z.ZodUUID;
|
|
19
|
+
}, z.core.$strict>;
|
|
20
|
+
export type DomainParams = z.infer<typeof DomainParams>;
|
|
21
|
+
export declare const DomainStatus: z.ZodEnum<{
|
|
22
|
+
lapsed: "lapsed";
|
|
23
|
+
lapsing: "lapsing";
|
|
24
|
+
pending: "pending";
|
|
25
|
+
verified: "verified";
|
|
26
|
+
}>;
|
|
27
|
+
export type DomainStatus = z.infer<typeof DomainStatus>;
|
|
28
|
+
export declare const OperatorDomain: z.ZodObject<{
|
|
29
|
+
id: z.ZodUUID;
|
|
30
|
+
domain: z.ZodString;
|
|
31
|
+
record: z.ZodObject<{
|
|
32
|
+
name: z.ZodString;
|
|
33
|
+
value: z.ZodString;
|
|
34
|
+
}, z.core.$strict>;
|
|
35
|
+
status: z.ZodEnum<{
|
|
36
|
+
lapsed: "lapsed";
|
|
37
|
+
lapsing: "lapsing";
|
|
38
|
+
pending: "pending";
|
|
39
|
+
verified: "verified";
|
|
40
|
+
}>;
|
|
41
|
+
verifiedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
42
|
+
lastCheckedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
43
|
+
lapsedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
44
|
+
graceEndsAt: z.ZodNullable<z.ZodISODateTime>;
|
|
45
|
+
}, z.core.$strict>;
|
|
46
|
+
export type OperatorDomain = z.infer<typeof OperatorDomain>;
|
|
47
|
+
export declare const DomainsResponse: z.ZodObject<{
|
|
48
|
+
domains: z.ZodArray<z.ZodObject<{
|
|
49
|
+
id: z.ZodUUID;
|
|
50
|
+
domain: z.ZodString;
|
|
51
|
+
record: z.ZodObject<{
|
|
52
|
+
name: z.ZodString;
|
|
53
|
+
value: z.ZodString;
|
|
54
|
+
}, z.core.$strict>;
|
|
55
|
+
status: z.ZodEnum<{
|
|
56
|
+
lapsed: "lapsed";
|
|
57
|
+
lapsing: "lapsing";
|
|
58
|
+
pending: "pending";
|
|
59
|
+
verified: "verified";
|
|
60
|
+
}>;
|
|
61
|
+
verifiedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
62
|
+
lastCheckedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
63
|
+
lapsedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
64
|
+
graceEndsAt: z.ZodNullable<z.ZodISODateTime>;
|
|
65
|
+
}, z.core.$strict>>;
|
|
66
|
+
graceDays: z.ZodInt;
|
|
67
|
+
}, z.core.$strict>;
|
|
68
|
+
export type DomainsResponse = z.infer<typeof DomainsResponse>;
|
|
69
|
+
export declare const DomainLookup: z.ZodEnum<{
|
|
70
|
+
failed: "failed";
|
|
71
|
+
found: "found";
|
|
72
|
+
missing: "missing";
|
|
73
|
+
}>;
|
|
74
|
+
export type DomainLookup = z.infer<typeof DomainLookup>;
|
|
75
|
+
export declare const DomainCheckResponse: z.ZodObject<{
|
|
76
|
+
domain: z.ZodObject<{
|
|
77
|
+
id: z.ZodUUID;
|
|
78
|
+
domain: z.ZodString;
|
|
79
|
+
record: z.ZodObject<{
|
|
80
|
+
name: z.ZodString;
|
|
81
|
+
value: z.ZodString;
|
|
82
|
+
}, z.core.$strict>;
|
|
83
|
+
status: z.ZodEnum<{
|
|
84
|
+
lapsed: "lapsed";
|
|
85
|
+
lapsing: "lapsing";
|
|
86
|
+
pending: "pending";
|
|
87
|
+
verified: "verified";
|
|
88
|
+
}>;
|
|
89
|
+
verifiedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
90
|
+
lastCheckedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
91
|
+
lapsedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
92
|
+
graceEndsAt: z.ZodNullable<z.ZodISODateTime>;
|
|
93
|
+
}, z.core.$strict>;
|
|
94
|
+
lookup: z.ZodEnum<{
|
|
95
|
+
failed: "failed";
|
|
96
|
+
found: "found";
|
|
97
|
+
missing: "missing";
|
|
98
|
+
}>;
|
|
99
|
+
}, z.core.$strict>;
|
|
100
|
+
export type DomainCheckResponse = z.infer<typeof DomainCheckResponse>;
|
|
101
|
+
export declare const domainRecord: (domain: string, token: string) => {
|
|
102
|
+
name: string;
|
|
103
|
+
value: string;
|
|
104
|
+
};
|