@sealkeeper/schema 0.4.3 → 0.4.5

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 +744 -27
  4. package/dist/api.js +232 -43
  5. package/dist/badge.d.ts +1 -0
  6. package/dist/badge.js +7 -0
  7. package/dist/credential.d.ts +186 -1
  8. package/dist/credential.js +55 -8
  9. package/dist/db/agents.d.ts +70 -0
  10. package/dist/db/agents.js +18 -0
  11. package/dist/db/client.d.ts +398 -0
  12. package/dist/db/client.js +2 -0
  13. package/dist/db/events.js +7 -0
  14. package/dist/db/index.d.ts +17 -1
  15. package/dist/db/index.js +5 -1
  16. package/dist/db/migrate.js +5 -1
  17. package/dist/db/migrator.d.ts +3 -1
  18. package/dist/db/migrator.js +8 -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 +36 -0
  28. package/dist/db/standing.js +5 -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/task-outcomes.d.ts +19 -0
  32. package/dist/db/task-outcomes.js +8 -1
  33. package/dist/db/tasks.d.ts +79 -0
  34. package/dist/db/tasks.js +19 -1
  35. package/dist/envelope.d.ts +3 -0
  36. package/dist/envelope.js +16 -11
  37. package/dist/goal.d.ts +149 -0
  38. package/dist/goal.js +171 -0
  39. package/dist/index.d.ts +6 -0
  40. package/dist/index.js +6 -0
  41. package/dist/moderation.d.ts +28 -0
  42. package/dist/moderation.js +450 -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 +32 -1
  50. package/dist/standing.d.ts +68 -0
  51. package/dist/standing.js +132 -6
  52. package/dist/tasks.d.ts +23 -1
  53. package/dist/tasks.js +29 -5
  54. package/package.json +1 -1
@@ -0,0 +1,450 @@
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
+ // 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
104
+ '0': 'o',
105
+ '1': 'l',
106
+ '3': 'e',
107
+ '4': 'a',
108
+ '5': 's',
109
+ '7': 't',
110
+ '8': 'b',
111
+ '@': 'a',
112
+ $: 's',
113
+ '|': 'l',
114
+ '!': 'l',
115
+ };
116
+ // 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.
119
+ const I_AS_L = /i/g;
120
+ const MARKS = /\p{M}/gu;
121
+ 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.
128
+ export function normaliseName(text) {
129
+ const folded = Array.from(text.normalize('NFKC').toLowerCase().normalize('NFKD').replace(MARKS, ''), (ch) => HOMOGLYPHS[ch] ?? ch)
130
+ .join('')
131
+ .replace(I_AS_L, 'l');
132
+ return folded
133
+ .replace(NOT_LETTER_OR_DIGIT, '')
134
+ .replace(/rn/g, 'm')
135
+ .replace(/vv/g, 'w');
136
+ }
137
+ // The display_name_key column. Unique across operators, so two display
138
+ // names that read the same cannot both be held.
139
+ export const displayNameKey = normaliseName;
140
+ // ---------------------------------------------------------------------------
141
+ // Protected names
142
+ // The top AI and technology companies and products. A slug or display name
143
+ // that normalises to one of these, with or without a common suffix, is
144
+ // refused. Written as the name reads. The list protects the distinctive
145
+ // form only, so Stability AI protects stabilityai (and stability-ai-labs)
146
+ // but not the plain word stability. Products that are also common first
147
+ // names (Devin, Alexa, Siri) are left out, since a login such as devin
148
+ // would otherwise lose its slug. Claude is the one exception.
149
+ export const PROTECTED_NAMES = [
150
+ // SealKeeper itself
151
+ 'SealKeeper',
152
+ 'Vouched',
153
+ // AI labs and model makers
154
+ 'Anthropic',
155
+ 'Claude',
156
+ 'OpenAI',
157
+ 'ChatGPT',
158
+ 'GPT',
159
+ 'Codex',
160
+ 'DALL-E',
161
+ 'Google',
162
+ 'Alphabet',
163
+ 'DeepMind',
164
+ 'Gemini',
165
+ 'Microsoft',
166
+ 'Copilot',
167
+ 'Bing',
168
+ 'Azure',
169
+ 'Meta',
170
+ 'Facebook',
171
+ 'Instagram',
172
+ 'WhatsApp',
173
+ 'Llama',
174
+ 'xAI',
175
+ 'Grok',
176
+ 'Twitter',
177
+ 'Mistral',
178
+ 'Le Chat',
179
+ 'Cohere',
180
+ 'DeepSeek',
181
+ 'Qwen',
182
+ 'Alibaba',
183
+ 'Zhipu',
184
+ 'Baidu',
185
+ 'ByteDance',
186
+ 'TikTok',
187
+ 'Tencent',
188
+ 'Perplexity',
189
+ 'Character AI',
190
+ 'Stability AI',
191
+ 'Midjourney',
192
+ 'ElevenLabs',
193
+ 'Hugging Face',
194
+ 'Scale AI',
195
+ 'AI21',
196
+ 'Nous Research',
197
+ 'Together AI',
198
+ 'Groq',
199
+ 'Cerebras',
200
+ 'Ollama',
201
+ 'LangChain',
202
+ 'LlamaIndex',
203
+ 'CrewAI',
204
+ 'AutoGPT',
205
+ 'Mastra',
206
+ 'OpenClaw',
207
+ // Coding agents and developer tools
208
+ 'Cursor',
209
+ 'Anysphere',
210
+ 'Windsurf',
211
+ 'Codeium',
212
+ 'Replit',
213
+ 'Lovable',
214
+ 'StackBlitz',
215
+ 'Vercel',
216
+ 'Sourcegraph',
217
+ 'Tabnine',
218
+ 'JetBrains',
219
+ 'GitHub',
220
+ 'GitLab',
221
+ 'Bitbucket',
222
+ 'Atlassian',
223
+ 'Notion',
224
+ 'Slack',
225
+ 'Discord',
226
+ 'Figma',
227
+ 'Stripe',
228
+ 'Supabase',
229
+ 'Cloudflare',
230
+ 'Netlify',
231
+ 'Docker',
232
+ 'npm',
233
+ // Platforms and hardware
234
+ 'Apple',
235
+ 'Amazon',
236
+ 'AWS',
237
+ 'Nvidia',
238
+ 'AMD',
239
+ 'Intel',
240
+ 'IBM',
241
+ 'Oracle',
242
+ 'Salesforce',
243
+ 'Samsung',
244
+ 'Adobe',
245
+ 'Netflix',
246
+ 'Uber',
247
+ 'Palantir',
248
+ 'Databricks',
249
+ 'Snowflake',
250
+ 'Zapier',
251
+ ];
252
+ // Suffixes an impersonator adds to a protected name. Stripped one at a time
253
+ // from the end, and the rest compared again.
254
+ export const PROTECTED_SUFFIXES = [
255
+ 'inc',
256
+ 'labs',
257
+ 'ai',
258
+ 'hq',
259
+ 'official',
260
+ 'team',
261
+ ];
262
+ // Prefixes an impersonator puts in front, as in the-real-openai. Stripped
263
+ // from the start the same way.
264
+ export const PROTECTED_PREFIXES = [
265
+ 'official',
266
+ 'real',
267
+ 'the',
268
+ 'team',
269
+ ];
270
+ const PROTECTED_KEYS = new Map(PROTECTED_NAMES.map((name) => [normaliseName(name), name]));
271
+ const SUFFIX_KEYS = PROTECTED_SUFFIXES.map(normaliseName);
272
+ const PREFIX_KEYS = PROTECTED_PREFIXES.map(normaliseName);
273
+ // Every form of a key a protected name could hide in. The key itself, then
274
+ // the key with every combination of prefixes and suffixes stripped. Never
275
+ // empty.
276
+ function keyForms(key) {
277
+ const forms = new Set();
278
+ const queue = [key];
279
+ while (queue.length > 0) {
280
+ const form = queue.pop();
281
+ if (form === '' || forms.has(form))
282
+ continue;
283
+ forms.add(form);
284
+ for (const suffix of SUFFIX_KEYS) {
285
+ if (form.length > suffix.length && form.endsWith(suffix)) {
286
+ queue.push(form.slice(0, -suffix.length));
287
+ }
288
+ }
289
+ for (const prefix of PREFIX_KEYS) {
290
+ if (form.length > prefix.length && form.startsWith(prefix)) {
291
+ queue.push(form.slice(prefix.length));
292
+ }
293
+ }
294
+ }
295
+ return [...forms];
296
+ }
297
+ // The protected name a text impersonates, as written in PROTECTED_NAMES, or
298
+ // null when it impersonates none.
299
+ export function protectedNameOf(text) {
300
+ for (const form of keyForms(normaliseName(text))) {
301
+ const name = PROTECTED_KEYS.get(form);
302
+ if (name !== undefined)
303
+ return name;
304
+ }
305
+ return null;
306
+ }
307
+ // ---------------------------------------------------------------------------
308
+ // Reserved words
309
+ // Route and product words a slug would collide with or read as part of the
310
+ // site. Slugs only, a display name is never part of a URL.
311
+ export const RESERVED_WORDS = [
312
+ 'about',
313
+ 'account',
314
+ 'accounts',
315
+ 'admin',
316
+ 'administrator',
317
+ 'agent',
318
+ 'agents',
319
+ 'anonymous',
320
+ 'api',
321
+ 'app',
322
+ 'assets',
323
+ 'auth',
324
+ 'avatar',
325
+ 'avatars',
326
+ 'badge',
327
+ 'badges',
328
+ 'blog',
329
+ 'callback',
330
+ 'check',
331
+ 'cli',
332
+ 'dashboard',
333
+ 'directory',
334
+ 'docs',
335
+ 'feed',
336
+ 'help',
337
+ 'home',
338
+ 'join',
339
+ 'leaderboard',
340
+ 'legal',
341
+ 'login',
342
+ 'logout',
343
+ 'me',
344
+ 'moderator',
345
+ 'new',
346
+ 'null',
347
+ 'oauth',
348
+ 'official',
349
+ 'operator',
350
+ 'operators',
351
+ 'privacy',
352
+ 'profile',
353
+ 'prove',
354
+ 'register',
355
+ 'root',
356
+ 'search',
357
+ 'seal',
358
+ 'seals',
359
+ 'sealkeeper',
360
+ 'security',
361
+ 'settings',
362
+ 'signin',
363
+ 'signout',
364
+ 'signup',
365
+ 'staff',
366
+ 'static',
367
+ 'status',
368
+ 'support',
369
+ 'system',
370
+ 'task',
371
+ 'tasks',
372
+ 'terms',
373
+ 'undefined',
374
+ 'v1',
375
+ 'verify',
376
+ 'vouched',
377
+ 'well-known',
378
+ 'what-is-shared',
379
+ 'www',
380
+ ];
381
+ // Compared in normalised form, so adm1n and a-p-i are refused as well.
382
+ const RESERVED_KEYS = new Set(RESERVED_WORDS.map(normaliseName));
383
+ export function isReservedWord(text) {
384
+ return RESERVED_KEYS.has(normaliseName(text));
385
+ }
386
+ function protectedRefusal(text) {
387
+ const name = protectedNameOf(text);
388
+ return name === null
389
+ ? null
390
+ : {
391
+ code: 'name_protected',
392
+ message: `${text} reads as ${name}, which is a protected name. Pick another.`,
393
+ };
394
+ }
395
+ // The refusal for an operator slug, or null when it may be used. The shape
396
+ // is OperatorSlug's job, this checks reserved words and protected names.
397
+ export function moderateSlug(slug) {
398
+ if (isReservedWord(slug)) {
399
+ return {
400
+ code: 'name_reserved',
401
+ message: `${slug} is reserved by SealKeeper. Pick another.`,
402
+ };
403
+ }
404
+ return protectedRefusal(slug);
405
+ }
406
+ // 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.
409
+ export function moderateDisplayName(name) {
410
+ return protectedRefusal(name);
411
+ }
412
+ // ---------------------------------------------------------------------------
413
+ // Default slugs
414
+ // A login as a slug. Lowercase, every run of characters other than a to z
415
+ // and 0 to 9 made one hyphen, hyphens trimmed from the ends, since an old
416
+ // GitHub login can end in one. A login is not always letters, digits and
417
+ // hyphens (an Enterprise Managed User login ends in _shortcode), and a
418
+ // slug must be. The same rule as sk_base in migration 0025. The API's
419
+ // default slug for a new operator starts from it.
420
+ export function slugBase(login) {
421
+ const base = login
422
+ .toLowerCase()
423
+ .replace(/[^a-z0-9]+/g, '-')
424
+ .replace(/^-|-$/g, '')
425
+ .slice(0, OPERATOR_SLUG_MAX)
426
+ .replace(/-$/, '');
427
+ return base === '' ? 'operator' : base;
428
+ }
429
+ // base with -n on the end, cut so the whole stays within the slug limit.
430
+ export const suffixedSlug = (base, n) => `${base.slice(0, OPERATOR_SLUG_MAX - `-${n}`.length).replace(/-+$/, '')}-${n}`;
431
+ // The reason the slug is the login's slug with a -2, -3 and so on, or null
432
+ // when it is not. A slug the operator chose, or one left behind by a GitHub
433
+ // login change, is not of that form and gives null. The caller also checks
434
+ // that the slug never changed, since an operator can pick a suffixed slug.
435
+ // reserved and protected are read from today's lists, taken is the rest.
436
+ export function slugSuffixReason(login, slug) {
437
+ const base = slugBase(login);
438
+ const n = /-([0-9]+)$/.exec(slug)?.[1];
439
+ if (n === undefined || slug === base)
440
+ return null;
441
+ const k = Number(n);
442
+ if (k < 2 || suffixedSlug(base, k) !== slug)
443
+ return null;
444
+ const refusal = moderateSlug(base);
445
+ if (refusal?.code === 'name_reserved')
446
+ return 'reserved';
447
+ if (refusal?.code === 'name_protected')
448
+ return 'protected';
449
+ return 'taken';
450
+ }
@@ -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
+ };
@@ -0,0 +1,85 @@
1
+ import { z } from 'zod';
2
+ // Operator verification by DNS TXT (VOU-185), gold's operator_verified at
3
+ // launch. The operator adds a domain on the account page, puts a TXT
4
+ // record at DNS_RECORD_LABEL.<domain> holding DNS_RECORD_PREFIX<token>, and
5
+ // SealKeeper checks it. The API's routes live under /v1/me/domains.
6
+ // The record name is this label, a dot and the domain.
7
+ export const DNS_RECORD_LABEL = '_sealkeeper';
8
+ // The record value is this prefix and the token.
9
+ export const DNS_RECORD_PREFIX = 'sealkeeper-verify=';
10
+ // The kind and provider of a DNS identity in a SEAL, SEAL standard section
11
+ // 4a. kind is a URL, since dns is none of the four named kinds. SealKeeper
12
+ // checked the record, so it is the provider.
13
+ export const DNS_IDENTITY_KIND = 'https://sealkeeper.run/seal/identity/dns';
14
+ export const DNS_IDENTITY_PROVIDER = 'https://sealkeeper.run';
15
+ // How a verified domain lapses (26 September 2026, the grace period still
16
+ // open). SealKeeper checks every verified domain again about every
17
+ // recheckHours hours. A check that misses the record starts graceDays days
18
+ // of grace, in which the operator stays verified and the goal warns, and
19
+ // operator_verified goes false when it ends. A check that finds the record
20
+ // again ends the grace. maxDomains is how many domains one operator may
21
+ // add. The scoring job and the API read these, so they live here.
22
+ export const OPERATOR_VERIFICATION = {
23
+ graceDays: 14,
24
+ recheckHours: 24,
25
+ maxDomains: 5,
26
+ };
27
+ // A host name in ASCII, lower case, at least two labels, the last starting
28
+ // with a letter so an IP address never passes. An IDN goes in as its
29
+ // xn-- form. A trailing dot is dropped first.
30
+ const HOST = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
31
+ export const DomainName = z
32
+ .string()
33
+ .trim()
34
+ .toLowerCase()
35
+ .max(254)
36
+ .transform((d) => d.replace(/\.$/, ''))
37
+ .pipe(z.string().regex(HOST, 'a domain such as example.com'));
38
+ // POST /v1/me/domains.
39
+ export const AddDomainRequest = z.strictObject({ domain: DomainName });
40
+ // POST /v1/me/domains/:id/check and DELETE /v1/me/domains/:id.
41
+ export const DomainParams = z.strictObject({ id: z.uuid() });
42
+ // pending, added and never found. verified, found at the last check.
43
+ // lapsing, missed since lapsedAt and still verified until graceEndsAt.
44
+ // lapsed, missed for the whole grace period and no longer verified.
45
+ export const DomainStatus = z.enum([
46
+ 'pending',
47
+ 'verified',
48
+ 'lapsing',
49
+ 'lapsed',
50
+ ]);
51
+ const Timestamp = z.iso.datetime();
52
+ // One domain as its operator sees it, with the record to add. Private to
53
+ // the signed in operator.
54
+ export const OperatorDomain = z.strictObject({
55
+ id: z.uuid(),
56
+ domain: z.string().min(1).max(253),
57
+ record: z.strictObject({
58
+ name: z.string().min(1).max(300),
59
+ value: z.string().min(1).max(300),
60
+ }),
61
+ status: DomainStatus,
62
+ verifiedAt: Timestamp.nullable(),
63
+ lastCheckedAt: Timestamp.nullable(),
64
+ lapsedAt: Timestamp.nullable(),
65
+ graceEndsAt: Timestamp.nullable(),
66
+ });
67
+ // GET /v1/me/domains and the answer of POST /v1/me/domains.
68
+ export const DomainsResponse = z.strictObject({
69
+ domains: z.array(OperatorDomain),
70
+ graceDays: z.int().positive(),
71
+ });
72
+ // What one lookup of the record found. found, the token is there. missing,
73
+ // the name has no TXT record with it. failed, the lookup itself did not
74
+ // answer, so nothing is known.
75
+ export const DomainLookup = z.enum(['found', 'missing', 'failed']);
76
+ // POST /v1/me/domains/:id/check.
77
+ export const DomainCheckResponse = z.strictObject({
78
+ domain: OperatorDomain,
79
+ lookup: DomainLookup,
80
+ });
81
+ // The TXT record for a domain and a token.
82
+ export const domainRecord = (domain, token) => ({
83
+ name: `${DNS_RECORD_LABEL}.${domain}`,
84
+ value: `${DNS_RECORD_PREFIX}${token}`,
85
+ });
@@ -0,0 +1,4 @@
1
+ export declare const POLICY_VERSIONS: {
2
+ readonly terms: '2026-09-27';
3
+ readonly privacy: '2026-09-27';
4
+ };
package/dist/policy.js ADDED
@@ -0,0 +1,10 @@
1
+ // The versions of the terms of service and the privacy policy, as the
2
+ // "Last updated" date each page shows, YYYY-MM-DD. The single source of
3
+ // truth. The web renders both pages' dates from here and the API stamps them
4
+ // on an operator when it creates the row and on an agent when it registers,
5
+ // as what was accepted (VOU-121). The client never sends them. Change a date
6
+ // here when the matching page changes.
7
+ export const POLICY_VERSIONS = {
8
+ terms: '2026-09-27',
9
+ privacy: '2026-09-27',
10
+ };
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ export declare const RUNTIMES: readonly ['claude-code', 'codex', 'cursor', 'gemini-cli', 'openclaw', 'mastra', 'other', 'unknown'];
3
+ export declare const Runtime: z.ZodEnum<{
4
+ "claude-code": "claude-code";
5
+ codex: "codex";
6
+ cursor: "cursor";
7
+ "gemini-cli": "gemini-cli";
8
+ mastra: "mastra";
9
+ openclaw: "openclaw";
10
+ other: "other";
11
+ unknown: "unknown";
12
+ }>;
13
+ export type Runtime = z.infer<typeof Runtime>;
14
+ export declare const DEFAULT_RUNTIME: Runtime;
15
+ export declare const RUNTIME_LABELS: Readonly<Record<Runtime, string>>;
16
+ export declare const RUNTIME_NAMES: ReadonlySet<string>;
17
+ export declare function isRuntimeName(name: string): boolean;