@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.
Files changed (54) hide show
  1. package/dist/agent-name.d.ts +1 -1
  2. package/dist/agent-name.js +4 -4
  3. package/dist/api.d.ts +625 -33
  4. package/dist/api.js +262 -57
  5. package/dist/badge.d.ts +1 -0
  6. package/dist/badge.js +7 -0
  7. package/dist/client-address.d.ts +6 -0
  8. package/dist/client-address.js +81 -0
  9. package/dist/db/agents.d.ts +155 -0
  10. package/dist/db/agents.js +38 -0
  11. package/dist/db/client.d.ts +431 -0
  12. package/dist/db/client.js +24 -1
  13. package/dist/db/events.js +7 -0
  14. package/dist/db/index.d.ts +16 -0
  15. package/dist/db/index.js +4 -0
  16. package/dist/db/migrate.js +5 -1
  17. package/dist/db/migrator.d.ts +5 -1
  18. package/dist/db/migrator.js +18 -2
  19. package/dist/db/operator-identities.d.ts +211 -0
  20. package/dist/db/operator-identities.js +49 -0
  21. package/dist/db/operator-level-grants.d.ts +109 -0
  22. package/dist/db/operator-level-grants.js +30 -0
  23. package/dist/db/operator-slugs.d.ts +92 -0
  24. package/dist/db/operator-slugs.js +25 -0
  25. package/dist/db/operators.d.ts +119 -0
  26. package/dist/db/operators.js +30 -2
  27. package/dist/db/standing.d.ts +1 -0
  28. package/dist/db/standing.js +2 -0
  29. package/dist/db/task-claim-failures.d.ts +143 -0
  30. package/dist/db/task-claim-failures.js +37 -0
  31. package/dist/db/tasks.d.ts +60 -0
  32. package/dist/db/tasks.js +19 -1
  33. package/dist/envelope.d.ts +3 -0
  34. package/dist/envelope.js +16 -11
  35. package/dist/goal.d.ts +62 -1
  36. package/dist/goal.js +70 -11
  37. package/dist/index.d.ts +7 -0
  38. package/dist/index.js +7 -0
  39. package/dist/json-shape.d.ts +9 -0
  40. package/dist/json-shape.js +70 -0
  41. package/dist/moderation.d.ts +29 -0
  42. package/dist/moderation.js +547 -0
  43. package/dist/operator-domains.d.ts +104 -0
  44. package/dist/operator-domains.js +85 -0
  45. package/dist/policy.d.ts +4 -0
  46. package/dist/policy.js +10 -0
  47. package/dist/runtime.d.ts +17 -0
  48. package/dist/runtime.js +61 -0
  49. package/dist/seal-conformance.js +2 -0
  50. package/dist/standing.d.ts +27 -8
  51. package/dist/standing.js +59 -22
  52. package/dist/tasks.d.ts +25 -1
  53. package/dist/tasks.js +43 -9
  54. 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
+ };