@sealkeeper/schema 0.4.1 → 0.4.2

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/api.d.ts CHANGED
@@ -330,6 +330,65 @@ export declare const TaskOutcomeRequest: z.ZodObject<{
330
330
  evidenceHash: z.ZodOptional<z.ZodString>;
331
331
  }, z.core.$strict>;
332
332
  export type TaskOutcomeRequest = z.infer<typeof TaskOutcomeRequest>;
333
+ export declare const TASK_READ_MAX_AGE_SEC = 300;
334
+ export declare const TaskSubmissionRequest: z.ZodObject<{
335
+ taskId: z.ZodUUID;
336
+ issuedAt: z.ZodISODateTime;
337
+ }, z.core.$strict>;
338
+ export type TaskSubmissionRequest = z.infer<typeof TaskSubmissionRequest>;
339
+ export declare const TaskReports: z.ZodObject<{
340
+ poster: z.ZodNullable<z.ZodEnum<{
341
+ failure: "failure";
342
+ success: "success";
343
+ }>>;
344
+ claimant: z.ZodNullable<z.ZodEnum<{
345
+ failure: "failure";
346
+ success: "success";
347
+ }>>;
348
+ }, z.core.$strict>;
349
+ export type TaskReports = z.infer<typeof TaskReports>;
350
+ export declare const TaskSubmissionResponse: z.ZodObject<{
351
+ task: z.ZodObject<{
352
+ id: z.ZodUUID;
353
+ posterAgentId: z.ZodString;
354
+ claimantAgentId: z.ZodNullable<z.ZodString>;
355
+ taskType: z.ZodString;
356
+ spec: z.ZodRecord<z.ZodString, z.ZodUnknown>;
357
+ verification: z.ZodDiscriminatedUnion<[z.ZodObject<{
358
+ kind: z.ZodLiteral<"hash">;
359
+ sha256: z.ZodString;
360
+ }, z.core.$strict>, z.ZodObject<{
361
+ kind: z.ZodLiteral<"schema">;
362
+ jsonSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
363
+ }, z.core.$strict>, z.ZodObject<{
364
+ kind: z.ZodLiteral<"counterparty">;
365
+ }, z.core.$strict>], "kind">;
366
+ state: z.ZodEnum<{
367
+ claimed: "claimed";
368
+ expired: "expired";
369
+ open: "open";
370
+ submitted: "submitted";
371
+ verified: "verified";
372
+ }>;
373
+ postedAt: z.ZodISODateTime;
374
+ claimedAt: z.ZodNullable<z.ZodISODateTime>;
375
+ submittedAt: z.ZodNullable<z.ZodISODateTime>;
376
+ verifiedAt: z.ZodNullable<z.ZodISODateTime>;
377
+ expiresAt: z.ZodISODateTime;
378
+ submission: z.ZodOptional<z.ZodString>;
379
+ }, z.core.$strict>;
380
+ reports: z.ZodObject<{
381
+ poster: z.ZodNullable<z.ZodEnum<{
382
+ failure: "failure";
383
+ success: "success";
384
+ }>>;
385
+ claimant: z.ZodNullable<z.ZodEnum<{
386
+ failure: "failure";
387
+ success: "success";
388
+ }>>;
389
+ }, z.core.$strict>;
390
+ }, z.core.$strict>;
391
+ export type TaskSubmissionResponse = z.infer<typeof TaskSubmissionResponse>;
333
392
  export declare const ListTasksQuery: z.ZodObject<{
334
393
  state: z.ZodDefault<z.ZodEnum<{
335
394
  claimed: "claimed";
@@ -374,6 +433,117 @@ export declare const ListTasksResponse: z.ZodObject<{
374
433
  }, z.core.$strict>>;
375
434
  }, z.core.$strict>;
376
435
  export type ListTasksResponse = z.infer<typeof ListTasksResponse>;
436
+ export type TasksCursor = {
437
+ atMicros: string;
438
+ id: string;
439
+ };
440
+ export declare function encodeTasksCursor(cursor: TasksCursor): string;
441
+ export declare function decodeTasksCursor(raw: string): TasksCursor | null;
442
+ export declare const TaskBoardQuery: z.ZodObject<{
443
+ taskType: z.ZodOptional<z.ZodString>;
444
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
445
+ cursor: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<TasksCursor, string>>>;
446
+ }, z.core.$strict>;
447
+ export type TaskBoardQuery = z.infer<typeof TaskBoardQuery>;
448
+ export declare const AgentTaskRole: z.ZodEnum<{
449
+ claimant: "claimant";
450
+ poster: "poster";
451
+ }>;
452
+ export type AgentTaskRole = z.infer<typeof AgentTaskRole>;
453
+ export declare const AgentTasksQuery: z.ZodObject<{
454
+ role: z.ZodEnum<{
455
+ claimant: "claimant";
456
+ poster: "poster";
457
+ }>;
458
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
459
+ cursor: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<TasksCursor, string>>>;
460
+ }, z.core.$strict>;
461
+ export type AgentTasksQuery = z.infer<typeof AgentTasksQuery>;
462
+ export declare const TaskParty: z.ZodObject<{
463
+ id: z.ZodString;
464
+ handle: z.ZodString;
465
+ operatedBySealKeeper: z.ZodBoolean;
466
+ }, z.core.$strict>;
467
+ export type TaskParty = z.infer<typeof TaskParty>;
468
+ export declare const TaskView: z.ZodObject<{
469
+ id: z.ZodUUID;
470
+ taskType: z.ZodString;
471
+ spec: z.ZodRecord<z.ZodString, z.ZodUnknown>;
472
+ verification: z.ZodDiscriminatedUnion<[z.ZodObject<{
473
+ kind: z.ZodLiteral<"hash">;
474
+ sha256: z.ZodString;
475
+ }, z.core.$strict>, z.ZodObject<{
476
+ kind: z.ZodLiteral<"schema">;
477
+ jsonSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
478
+ }, z.core.$strict>, z.ZodObject<{
479
+ kind: z.ZodLiteral<"counterparty">;
480
+ }, z.core.$strict>], "kind">;
481
+ state: z.ZodEnum<{
482
+ claimed: "claimed";
483
+ expired: "expired";
484
+ open: "open";
485
+ submitted: "submitted";
486
+ verified: "verified";
487
+ }>;
488
+ poster: z.ZodObject<{
489
+ id: z.ZodString;
490
+ handle: z.ZodString;
491
+ operatedBySealKeeper: z.ZodBoolean;
492
+ }, z.core.$strict>;
493
+ claimant: z.ZodNullable<z.ZodObject<{
494
+ id: z.ZodString;
495
+ handle: z.ZodString;
496
+ operatedBySealKeeper: z.ZodBoolean;
497
+ }, z.core.$strict>>;
498
+ countsForTrust: z.ZodNullable<z.ZodBoolean>;
499
+ postedAt: z.ZodISODateTime;
500
+ claimedAt: z.ZodNullable<z.ZodISODateTime>;
501
+ submittedAt: z.ZodNullable<z.ZodISODateTime>;
502
+ verifiedAt: z.ZodNullable<z.ZodISODateTime>;
503
+ expiresAt: z.ZodISODateTime;
504
+ }, z.core.$strict>;
505
+ export type TaskView = z.infer<typeof TaskView>;
506
+ export declare const TaskViewsResponse: z.ZodObject<{
507
+ tasks: z.ZodArray<z.ZodObject<{
508
+ id: z.ZodUUID;
509
+ taskType: z.ZodString;
510
+ spec: z.ZodRecord<z.ZodString, z.ZodUnknown>;
511
+ verification: z.ZodDiscriminatedUnion<[z.ZodObject<{
512
+ kind: z.ZodLiteral<"hash">;
513
+ sha256: z.ZodString;
514
+ }, z.core.$strict>, z.ZodObject<{
515
+ kind: z.ZodLiteral<"schema">;
516
+ jsonSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
517
+ }, z.core.$strict>, z.ZodObject<{
518
+ kind: z.ZodLiteral<"counterparty">;
519
+ }, z.core.$strict>], "kind">;
520
+ state: z.ZodEnum<{
521
+ claimed: "claimed";
522
+ expired: "expired";
523
+ open: "open";
524
+ submitted: "submitted";
525
+ verified: "verified";
526
+ }>;
527
+ poster: z.ZodObject<{
528
+ id: z.ZodString;
529
+ handle: z.ZodString;
530
+ operatedBySealKeeper: z.ZodBoolean;
531
+ }, z.core.$strict>;
532
+ claimant: z.ZodNullable<z.ZodObject<{
533
+ id: z.ZodString;
534
+ handle: z.ZodString;
535
+ operatedBySealKeeper: z.ZodBoolean;
536
+ }, z.core.$strict>>;
537
+ countsForTrust: z.ZodNullable<z.ZodBoolean>;
538
+ postedAt: z.ZodISODateTime;
539
+ claimedAt: z.ZodNullable<z.ZodISODateTime>;
540
+ submittedAt: z.ZodNullable<z.ZodISODateTime>;
541
+ verifiedAt: z.ZodNullable<z.ZodISODateTime>;
542
+ expiresAt: z.ZodISODateTime;
543
+ }, z.core.$strict>>;
544
+ nextCursor: z.ZodNullable<z.ZodString>;
545
+ }, z.core.$strict>;
546
+ export type TaskViewsResponse = z.infer<typeof TaskViewsResponse>;
377
547
  export declare const SignedRatingRequest: z.ZodObject<{
378
548
  envelope: z.ZodString;
379
549
  }, z.core.$strict>;
@@ -703,6 +873,8 @@ export declare const LeaderboardResponse: z.ZodObject<{
703
873
  export type LeaderboardResponse = z.infer<typeof LeaderboardResponse>;
704
874
  export declare const FeedKind: z.ZodEnum<{
705
875
  flag: "flag";
876
+ level_change: "level_change";
877
+ milestone: "milestone";
706
878
  registration: "registration";
707
879
  score_change: "score_change";
708
880
  task_verified: "task_verified";
@@ -713,6 +885,12 @@ export declare const FeedFlagCode: z.ZodEnum<{
713
885
  outcome_disagreement: "outcome_disagreement";
714
886
  }>;
715
887
  export type FeedFlagCode = z.infer<typeof FeedFlagCode>;
888
+ export declare const FeedMilestone: z.ZodEnum<{
889
+ first_seal: "first_seal";
890
+ verified_100: "verified_100";
891
+ verified_1000: "verified_1000";
892
+ }>;
893
+ export type FeedMilestone = z.infer<typeof FeedMilestone>;
716
894
  export declare const FeedPayloads: {
717
895
  registration: z.ZodObject<{
718
896
  agentId: z.ZodString;
@@ -752,6 +930,39 @@ export declare const FeedPayloads: {
752
930
  old: z.ZodString;
753
931
  new: z.ZodString;
754
932
  }, z.core.$strict>;
933
+ level_change: z.ZodObject<{
934
+ agentId: z.ZodString;
935
+ name: z.ZodString;
936
+ version: z.ZodString;
937
+ old: z.ZodEnum<{
938
+ bronze: "bronze";
939
+ gold: "gold";
940
+ none: "none";
941
+ silver: "silver";
942
+ }>;
943
+ new: z.ZodEnum<{
944
+ bronze: "bronze";
945
+ gold: "gold";
946
+ none: "none";
947
+ silver: "silver";
948
+ }>;
949
+ }, z.core.$strict>;
950
+ milestone: z.ZodObject<{
951
+ agentId: z.ZodString;
952
+ name: z.ZodString;
953
+ version: z.ZodString;
954
+ milestone: z.ZodEnum<{
955
+ first_seal: "first_seal";
956
+ verified_100: "verified_100";
957
+ verified_1000: "verified_1000";
958
+ }>;
959
+ level: z.ZodEnum<{
960
+ bronze: "bronze";
961
+ gold: "gold";
962
+ none: "none";
963
+ silver: "silver";
964
+ }>;
965
+ }, z.core.$strict>;
755
966
  };
756
967
  export type FeedPayload<K extends FeedKind> = z.infer<(typeof FeedPayloads)[K]>;
757
968
  export declare const FeedItem: z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -837,6 +1048,57 @@ export declare const FeedItem: z.ZodDiscriminatedUnion<[z.ZodObject<{
837
1048
  new: z.ZodString;
838
1049
  }, z.core.$strict>;
839
1050
  createdAt: z.ZodISODateTime;
1051
+ }, z.core.$strict>, z.ZodObject<{
1052
+ id: z.ZodInt;
1053
+ kind: z.ZodLiteral<"level_change">;
1054
+ agentId: z.ZodNullable<z.ZodString>;
1055
+ operator: z.ZodObject<{
1056
+ login: z.ZodString;
1057
+ }, z.core.$strict>;
1058
+ handle: z.ZodString;
1059
+ payload: z.ZodObject<{
1060
+ agentId: z.ZodString;
1061
+ name: z.ZodString;
1062
+ version: z.ZodString;
1063
+ old: z.ZodEnum<{
1064
+ bronze: "bronze";
1065
+ gold: "gold";
1066
+ none: "none";
1067
+ silver: "silver";
1068
+ }>;
1069
+ new: z.ZodEnum<{
1070
+ bronze: "bronze";
1071
+ gold: "gold";
1072
+ none: "none";
1073
+ silver: "silver";
1074
+ }>;
1075
+ }, z.core.$strict>;
1076
+ createdAt: z.ZodISODateTime;
1077
+ }, z.core.$strict>, z.ZodObject<{
1078
+ id: z.ZodInt;
1079
+ kind: z.ZodLiteral<"milestone">;
1080
+ agentId: z.ZodNullable<z.ZodString>;
1081
+ operator: z.ZodObject<{
1082
+ login: z.ZodString;
1083
+ }, z.core.$strict>;
1084
+ handle: z.ZodString;
1085
+ payload: z.ZodObject<{
1086
+ agentId: z.ZodString;
1087
+ name: z.ZodString;
1088
+ version: z.ZodString;
1089
+ milestone: z.ZodEnum<{
1090
+ first_seal: "first_seal";
1091
+ verified_100: "verified_100";
1092
+ verified_1000: "verified_1000";
1093
+ }>;
1094
+ level: z.ZodEnum<{
1095
+ bronze: "bronze";
1096
+ gold: "gold";
1097
+ none: "none";
1098
+ silver: "silver";
1099
+ }>;
1100
+ }, z.core.$strict>;
1101
+ createdAt: z.ZodISODateTime;
840
1102
  }, z.core.$strict>], "kind">;
841
1103
  export type FeedItem = z.infer<typeof FeedItem>;
842
1104
  export declare const FEED_RECENT_MAX = 200;
@@ -928,6 +1190,57 @@ export declare const FeedRecentResponse: z.ZodObject<{
928
1190
  new: z.ZodString;
929
1191
  }, z.core.$strict>;
930
1192
  createdAt: z.ZodISODateTime;
1193
+ }, z.core.$strict>, z.ZodObject<{
1194
+ id: z.ZodInt;
1195
+ kind: z.ZodLiteral<"level_change">;
1196
+ agentId: z.ZodNullable<z.ZodString>;
1197
+ operator: z.ZodObject<{
1198
+ login: z.ZodString;
1199
+ }, z.core.$strict>;
1200
+ handle: z.ZodString;
1201
+ payload: z.ZodObject<{
1202
+ agentId: z.ZodString;
1203
+ name: z.ZodString;
1204
+ version: z.ZodString;
1205
+ old: z.ZodEnum<{
1206
+ bronze: "bronze";
1207
+ gold: "gold";
1208
+ none: "none";
1209
+ silver: "silver";
1210
+ }>;
1211
+ new: z.ZodEnum<{
1212
+ bronze: "bronze";
1213
+ gold: "gold";
1214
+ none: "none";
1215
+ silver: "silver";
1216
+ }>;
1217
+ }, z.core.$strict>;
1218
+ createdAt: z.ZodISODateTime;
1219
+ }, z.core.$strict>, z.ZodObject<{
1220
+ id: z.ZodInt;
1221
+ kind: z.ZodLiteral<"milestone">;
1222
+ agentId: z.ZodNullable<z.ZodString>;
1223
+ operator: z.ZodObject<{
1224
+ login: z.ZodString;
1225
+ }, z.core.$strict>;
1226
+ handle: z.ZodString;
1227
+ payload: z.ZodObject<{
1228
+ agentId: z.ZodString;
1229
+ name: z.ZodString;
1230
+ version: z.ZodString;
1231
+ milestone: z.ZodEnum<{
1232
+ first_seal: "first_seal";
1233
+ verified_100: "verified_100";
1234
+ verified_1000: "verified_1000";
1235
+ }>;
1236
+ level: z.ZodEnum<{
1237
+ bronze: "bronze";
1238
+ gold: "gold";
1239
+ none: "none";
1240
+ silver: "silver";
1241
+ }>;
1242
+ }, z.core.$strict>;
1243
+ createdAt: z.ZodISODateTime;
931
1244
  }, z.core.$strict>], "kind">>;
932
1245
  }, z.core.$strict>;
933
1246
  export type FeedRecentResponse = z.infer<typeof FeedRecentResponse>;
package/dist/api.js CHANGED
@@ -169,34 +169,46 @@ export const AgentRenamedResponse = z.strictObject({
169
169
  // ties broken by id, paged with an opaque cursor.
170
170
  // A GitHub login. Letters, digits and hyphens, at most 39 characters.
171
171
  export const GithubLogin = z.string().regex(/^[A-Za-z0-9-]{1,39}$/);
172
- const CURSOR_TEXT = /^(\d{1,18})\.([A-Za-z0-9_-]{43})$/;
173
- export function encodeAgentsCursor(cursor) {
174
- return base64urlEncode(utf8Encode(`${cursor.createdAtMicros}.${cursor.id}`));
175
- }
176
- // Null for anything that is not a cursor this API made.
177
- export function decodeAgentsCursor(raw) {
178
- let text;
179
- try {
180
- text = utf8Decode(base64urlDecode(raw));
181
- }
182
- catch {
183
- return null;
184
- }
185
- const m = CURSOR_TEXT.exec(text);
186
- if (!m?.[1] || !m[2] || !AgentId.safeParse(m[2]).success)
187
- return null;
188
- return { createdAtMicros: m[1], id: m[2] };
172
+ function keysetCursor(id, isId = () => true) {
173
+ const text = new RegExp(`^(\\d{1,18})\\.(${id.source})$`);
174
+ const encode = (c) => base64urlEncode(utf8Encode(`${c.micros}.${c.id}`));
175
+ // Null for anything that is not a cursor this API made.
176
+ const decode = (raw) => {
177
+ let decoded;
178
+ try {
179
+ decoded = utf8Decode(base64urlDecode(raw));
180
+ }
181
+ catch {
182
+ return null;
183
+ }
184
+ const m = text.exec(decoded);
185
+ if (!m?.[1] || !m[2] || !isId(m[2]))
186
+ return null;
187
+ return { micros: m[1], id: m[2] };
188
+ };
189
+ return { encode, decode };
189
190
  }
190
- export const AgentsCursorParam = z
191
+ // A query parameter that holds a cursor, 400 for one the API did not make.
192
+ const cursorParam = (decode) => z
191
193
  .string()
192
194
  .max(128)
193
195
  .transform((raw, ctx) => {
194
- const cursor = decodeAgentsCursor(raw);
196
+ const cursor = decode(raw);
195
197
  if (cursor)
196
198
  return cursor;
197
199
  ctx.addIssue({ code: 'custom', message: 'Invalid cursor' });
198
200
  return z.NEVER;
199
201
  });
202
+ const agentsKeyset = keysetCursor(/[A-Za-z0-9_-]{43}/, (id) => AgentId.safeParse(id).success);
203
+ export function encodeAgentsCursor(cursor) {
204
+ return agentsKeyset.encode({ micros: cursor.createdAtMicros, id: cursor.id });
205
+ }
206
+ // Null for anything that is not a cursor this API made.
207
+ export function decodeAgentsCursor(raw) {
208
+ const c = agentsKeyset.decode(raw);
209
+ return c && { createdAtMicros: c.micros, id: c.id };
210
+ }
211
+ export const AgentsCursorParam = cursorParam(decodeAgentsCursor);
200
212
  export const AgentsListQuery = z.strictObject({
201
213
  limit: Limit,
202
214
  cursor: AgentsCursorParam.optional(),
@@ -303,6 +315,26 @@ export const TaskOutcomeRequest = z.strictObject({
303
315
  outcome: TaskOutcome,
304
316
  evidenceHash: Sha256Hex.optional(),
305
317
  });
318
+ // POST /v1/tasks/:id/submission, the poster's signed read of its task with
319
+ // the submission and both outcome reports. The public GET leaves the
320
+ // submission out. issuedAt bounds how long a captured envelope could be sent
321
+ // again, the same window as rename.
322
+ export const TASK_READ_MAX_AGE_SEC = RENAME_MAX_AGE_SEC;
323
+ export const TaskSubmissionRequest = z.strictObject({
324
+ taskId: z.uuid(),
325
+ issuedAt: Timestamp,
326
+ });
327
+ // The outcome each side has reported, null until it reports. A new report
328
+ // replaces the old one, so this is the current one.
329
+ export const TaskReports = z.strictObject({
330
+ poster: TaskOutcome.nullable(),
331
+ claimant: TaskOutcome.nullable(),
332
+ });
333
+ // task.submission is absent until the claimant submits.
334
+ export const TaskSubmissionResponse = z.strictObject({
335
+ task: TaskResponse,
336
+ reports: TaskReports,
337
+ });
306
338
  export const ListTasksQuery = z.strictObject({
307
339
  state: TaskState.default('open'),
308
340
  taskType: TaskType.optional(),
@@ -311,6 +343,63 @@ export const ListTasksQuery = z.strictObject({
311
343
  export const ListTasksResponse = z.strictObject({
312
344
  tasks: z.array(TaskResponse),
313
345
  });
346
+ const tasksKeyset = keysetCursor(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/);
347
+ export function encodeTasksCursor(cursor) {
348
+ return tasksKeyset.encode({ micros: cursor.atMicros, id: cursor.id });
349
+ }
350
+ // Null for anything that is not a cursor this API made.
351
+ export function decodeTasksCursor(raw) {
352
+ const c = tasksKeyset.decode(raw);
353
+ return c && { atMicros: c.micros, id: c.id };
354
+ }
355
+ const TasksCursorParam = cursorParam(decodeTasksCursor);
356
+ // GET /v1/tasks/board. Open tasks, newest first.
357
+ export const TaskBoardQuery = z.strictObject({
358
+ taskType: TaskType.optional(),
359
+ limit: Limit,
360
+ cursor: TasksCursorParam.optional(),
361
+ });
362
+ // GET /v1/agents/:id/tasks. claimant is the tasks the agent completed,
363
+ // verified, newest verified first. poster is every task it posted, newest
364
+ // posted first.
365
+ export const AgentTaskRole = z.enum(['claimant', 'poster']);
366
+ export const AgentTasksQuery = z.strictObject({
367
+ role: AgentTaskRole,
368
+ limit: Limit,
369
+ cursor: TasksCursorParam.optional(),
370
+ });
371
+ // One side of a task, as it is now.
372
+ export const TaskParty = z.strictObject({
373
+ id: AgentId,
374
+ handle: AgentHandle,
375
+ // True for an agent SealKeeper runs itself, the seed agent.
376
+ operatedBySealKeeper: z.boolean(),
377
+ });
378
+ export const TaskView = z.strictObject({
379
+ id: z.uuid(),
380
+ taskType: TaskType,
381
+ spec: z.record(z.string(), z.unknown()),
382
+ // The public spec, never the full schema.
383
+ verification: VerificationSpec,
384
+ state: TaskState,
385
+ poster: TaskParty,
386
+ // Null while open, and after the claimant agent was deleted.
387
+ claimant: TaskParty.nullable(),
388
+ // The scoring rule. True when the poster is the seed agent or an agent of
389
+ // another operator than the claimant, false when both sides share an
390
+ // operator. Null until verified, and after the claimant was deleted.
391
+ countsForTrust: z.boolean().nullable(),
392
+ postedAt: Timestamp,
393
+ claimedAt: Timestamp.nullable(),
394
+ submittedAt: Timestamp.nullable(),
395
+ verifiedAt: Timestamp.nullable(),
396
+ expiresAt: Timestamp,
397
+ });
398
+ // nextCursor is null on the last page.
399
+ export const TaskViewsResponse = z.strictObject({
400
+ tasks: z.array(TaskView),
401
+ nextCursor: z.string().nullable(),
402
+ });
314
403
  // The body of a signed rating. A rating payload is small, so it takes the
315
404
  // event envelope cap.
316
405
  export const SignedRatingRequest = z.strictObject({
@@ -429,12 +518,22 @@ export const LeaderboardResponse = z.strictObject({
429
518
  export const FeedKind = z.enum([
430
519
  'registration',
431
520
  'task_verified',
521
+ // No longer written since VOU-125. Kept so old rows still parse and render.
432
522
  'score_change',
433
523
  'flag',
434
524
  'version_change',
525
+ 'level_change',
526
+ 'milestone',
435
527
  ]);
436
528
  // The one flag code today. Two sides of a counterparty task disagreed.
437
529
  export const FeedFlagCode = z.enum(['outcome_disagreement']);
530
+ // Milestones an agent passes once. Its first SEAL level above none, then
531
+ // 100 and 1,000 verified tasks.
532
+ export const FeedMilestone = z.enum([
533
+ 'first_seal',
534
+ 'verified_100',
535
+ 'verified_1000',
536
+ ]);
438
537
  // name is the agent's name when the item was written. The item's handle
439
538
  // is the agent as it is now.
440
539
  const FeedAgent = {
@@ -468,6 +567,21 @@ export const FeedPayloads = {
468
567
  old: Version,
469
568
  new: Version,
470
569
  }),
570
+ // The scoring run moved the level of the agent's current version, up or
571
+ // down. The first move above none is a first_seal milestone instead.
572
+ level_change: z.strictObject({
573
+ ...FeedAgent,
574
+ version: Version,
575
+ old: Level,
576
+ new: Level,
577
+ }),
578
+ // level is the current version's level when the milestone was passed.
579
+ milestone: z.strictObject({
580
+ ...FeedAgent,
581
+ version: Version,
582
+ milestone: FeedMilestone,
583
+ level: Level,
584
+ }),
471
585
  };
472
586
  const feedItemOf = (kind) => z.strictObject({
473
587
  // The SSE event id. Ascending in insert order.
@@ -487,6 +601,8 @@ export const FeedItem = z.discriminatedUnion('kind', [
487
601
  feedItemOf('score_change'),
488
602
  feedItemOf('flag'),
489
603
  feedItemOf('version_change'),
604
+ feedItemOf('level_change'),
605
+ feedItemOf('milestone'),
490
606
  ]);
491
607
  export const FEED_RECENT_MAX = 200;
492
608
  export const FeedRecentQuery = z.strictObject({
@@ -0,0 +1,75 @@
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
+ }>;
@@ -0,0 +1,16 @@
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] })]);
@@ -138,6 +138,57 @@ export declare const agents: import("drizzle-orm/pg-core").PgTableWithColumns<{
138
138
  identity: undefined;
139
139
  generated: undefined;
140
140
  }, {}, {}>;
141
+ dirtySince: import("drizzle-orm/pg-core").PgColumn<{
142
+ name: "dirty_since";
143
+ tableName: "agents";
144
+ dataType: "date";
145
+ columnType: "PgTimestamp";
146
+ data: Date;
147
+ driverParam: string;
148
+ notNull: false;
149
+ hasDefault: true;
150
+ isPrimaryKey: false;
151
+ isAutoincrement: false;
152
+ hasRuntimeDefault: false;
153
+ enumValues: undefined;
154
+ baseColumn: never;
155
+ identity: undefined;
156
+ generated: undefined;
157
+ }, {}, {}>;
158
+ dirtyGen: import("drizzle-orm/pg-core").PgColumn<{
159
+ name: "dirty_gen";
160
+ tableName: "agents";
161
+ dataType: "number";
162
+ columnType: "PgBigInt53";
163
+ data: number;
164
+ driverParam: string | number;
165
+ notNull: true;
166
+ hasDefault: true;
167
+ isPrimaryKey: false;
168
+ isAutoincrement: false;
169
+ hasRuntimeDefault: false;
170
+ enumValues: undefined;
171
+ baseColumn: never;
172
+ identity: undefined;
173
+ generated: undefined;
174
+ }, {}, {}>;
175
+ recomputeAt: import("drizzle-orm/pg-core").PgColumn<{
176
+ name: "recompute_at";
177
+ tableName: "agents";
178
+ dataType: "date";
179
+ columnType: "PgTimestamp";
180
+ data: Date;
181
+ driverParam: string;
182
+ notNull: false;
183
+ hasDefault: false;
184
+ isPrimaryKey: false;
185
+ isAutoincrement: false;
186
+ hasRuntimeDefault: false;
187
+ enumValues: undefined;
188
+ baseColumn: never;
189
+ identity: undefined;
190
+ generated: undefined;
191
+ }, {}, {}>;
141
192
  };
142
193
  dialect: 'pg';
143
194
  }>;
package/dist/db/agents.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { sql } from 'drizzle-orm';
2
- import { check, jsonb, pgTable, text, timestamp, uniqueIndex, uuid, } from 'drizzle-orm/pg-core';
2
+ import { bigint, check, index, jsonb, pgTable, text, timestamp, uniqueIndex, uuid, } from 'drizzle-orm/pg-core';
3
3
  import { operators } from './operators.js';
4
4
  import { timestamps } from './timestamps.js';
5
5
  // How a Drizzle table is declared.
@@ -31,10 +31,31 @@ export const agents = pgTable('agents', {
31
31
  // sent again cannot move the version back.
32
32
  versionIssuedAt: timestamp('version_issued_at', { withTimezone: true }),
33
33
  card: jsonb('card'),
34
+ // When the agent was first marked dirty since the scoring run last
35
+ // cleared it. Every write that can move the agent's standing marks it,
36
+ // in the same transaction as that write, but a mark sets this only when
37
+ // it is null, so an agent that keeps being marked keeps its place in the
38
+ // run's queue. defaultNow, so a new agent starts dirty and migration
39
+ // 0018 backfilled every existing agent. Not a row timestamp, the scoring
40
+ // run owns it.
41
+ dirtySince: timestamp('dirty_since', { withTimezone: true }).defaultNow(),
42
+ // Bumped by every mark. The run reads it when it selects the agent and
43
+ // clears dirty_since only where it is unchanged, so a mark that commits
44
+ // mid run survives (migration 0019).
45
+ dirtyGen: bigint('dirty_gen', { mode: 'number' }).notNull().default(0),
46
+ // The earliest future moment time alone can change this agent's
47
+ // standing, a dormancy rung or evidence leaving the window. Written by
48
+ // the scoring run only, null when nothing is due.
49
+ recomputeAt: timestamp('recompute_at', { withTimezone: true }),
34
50
  ...timestamps,
35
51
  }, (t) => [
36
52
  check('agents_id_length', sql `length(${t.id}) = 43`),
37
53
  // The handle login/name points at one agent. Names are lowercase, so a
38
54
  // plain unique index is enough.
39
55
  uniqueIndex('agents_operator_name_unique').on(t.operatorId, t.name),
56
+ // The scoring run selects agents that are dirty or due.
57
+ index('agents_recompute_at_idx').on(t.recomputeAt),
58
+ index('agents_dirty_since_idx')
59
+ .on(t.dirtySince)
60
+ .where(sql `${t.dirtySince} is not null`),
40
61
  ]);
@@ -252,6 +252,57 @@ export declare function createDb(url: string, options?: DbOptions): import("driz
252
252
  identity: undefined;
253
253
  generated: undefined;
254
254
  }, {}, {}>;
255
+ dirtySince: import("drizzle-orm/pg-core").PgColumn<{
256
+ name: "dirty_since";
257
+ tableName: "agents";
258
+ dataType: "date";
259
+ columnType: "PgTimestamp";
260
+ data: Date;
261
+ driverParam: string;
262
+ notNull: false;
263
+ hasDefault: true;
264
+ isPrimaryKey: false;
265
+ isAutoincrement: false;
266
+ hasRuntimeDefault: false;
267
+ enumValues: undefined;
268
+ baseColumn: never;
269
+ identity: undefined;
270
+ generated: undefined;
271
+ }, {}, {}>;
272
+ dirtyGen: import("drizzle-orm/pg-core").PgColumn<{
273
+ name: "dirty_gen";
274
+ tableName: "agents";
275
+ dataType: "number";
276
+ columnType: "PgBigInt53";
277
+ data: number;
278
+ driverParam: string | number;
279
+ notNull: true;
280
+ hasDefault: true;
281
+ isPrimaryKey: false;
282
+ isAutoincrement: false;
283
+ hasRuntimeDefault: false;
284
+ enumValues: undefined;
285
+ baseColumn: never;
286
+ identity: undefined;
287
+ generated: undefined;
288
+ }, {}, {}>;
289
+ recomputeAt: import("drizzle-orm/pg-core").PgColumn<{
290
+ name: "recompute_at";
291
+ tableName: "agents";
292
+ dataType: "date";
293
+ columnType: "PgTimestamp";
294
+ data: Date;
295
+ driverParam: string;
296
+ notNull: false;
297
+ hasDefault: false;
298
+ isPrimaryKey: false;
299
+ isAutoincrement: false;
300
+ hasRuntimeDefault: false;
301
+ enumValues: undefined;
302
+ baseColumn: never;
303
+ identity: undefined;
304
+ generated: undefined;
305
+ }, {}, {}>;
255
306
  };
256
307
  dialect: 'pg';
257
308
  }>;
@@ -1814,6 +1865,81 @@ export declare function createDb(url: string, options?: DbOptions): import("driz
1814
1865
  };
1815
1866
  dialect: 'pg';
1816
1867
  }>;
1868
+ agentMilestones: import("drizzle-orm/pg-core").PgTableWithColumns<{
1869
+ name: "agent_milestones";
1870
+ schema: undefined;
1871
+ columns: {
1872
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
1873
+ name: "created_at";
1874
+ tableName: "agent_milestones";
1875
+ dataType: "date";
1876
+ columnType: "PgTimestamp";
1877
+ data: Date;
1878
+ driverParam: string;
1879
+ notNull: true;
1880
+ hasDefault: true;
1881
+ isPrimaryKey: false;
1882
+ isAutoincrement: false;
1883
+ hasRuntimeDefault: false;
1884
+ enumValues: undefined;
1885
+ baseColumn: never;
1886
+ identity: undefined;
1887
+ generated: undefined;
1888
+ }, {}, {}>;
1889
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
1890
+ name: "updated_at";
1891
+ tableName: "agent_milestones";
1892
+ dataType: "date";
1893
+ columnType: "PgTimestamp";
1894
+ data: Date;
1895
+ driverParam: string;
1896
+ notNull: true;
1897
+ hasDefault: true;
1898
+ isPrimaryKey: false;
1899
+ isAutoincrement: false;
1900
+ hasRuntimeDefault: false;
1901
+ enumValues: undefined;
1902
+ baseColumn: never;
1903
+ identity: undefined;
1904
+ generated: undefined;
1905
+ }, {}, {}>;
1906
+ agentId: import("drizzle-orm/pg-core").PgColumn<{
1907
+ name: "agent_id";
1908
+ tableName: "agent_milestones";
1909
+ dataType: "string";
1910
+ columnType: "PgText";
1911
+ data: string;
1912
+ driverParam: string;
1913
+ notNull: true;
1914
+ hasDefault: false;
1915
+ isPrimaryKey: false;
1916
+ isAutoincrement: false;
1917
+ hasRuntimeDefault: false;
1918
+ enumValues: [string, ...string[]];
1919
+ baseColumn: never;
1920
+ identity: undefined;
1921
+ generated: undefined;
1922
+ }, {}, {}>;
1923
+ milestone: import("drizzle-orm/pg-core").PgColumn<{
1924
+ name: "milestone";
1925
+ tableName: "agent_milestones";
1926
+ dataType: "string";
1927
+ columnType: "PgText";
1928
+ data: string;
1929
+ driverParam: string;
1930
+ notNull: true;
1931
+ hasDefault: false;
1932
+ isPrimaryKey: false;
1933
+ isAutoincrement: false;
1934
+ hasRuntimeDefault: false;
1935
+ enumValues: [string, ...string[]];
1936
+ baseColumn: never;
1937
+ identity: undefined;
1938
+ generated: undefined;
1939
+ }, {}, {}>;
1940
+ };
1941
+ dialect: 'pg';
1942
+ }>;
1817
1943
  deletedOperators: import("drizzle-orm/pg-core").PgTableWithColumns<{
1818
1944
  name: "deleted_operators";
1819
1945
  schema: undefined;
package/dist/db/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { drizzle } from 'drizzle-orm/postgres-js';
2
2
  import postgres from 'postgres';
3
+ import { agentMilestones } from './agent-milestones.js';
3
4
  import { agentRenames } from './agent-renames.js';
4
5
  import { agents } from './agents.js';
5
6
  import { credentials } from './credentials.js';
@@ -27,6 +28,7 @@ const schema = {
27
28
  quotaCounters,
28
29
  feedItems,
29
30
  agentRenames,
31
+ agentMilestones,
30
32
  deletedOperators,
31
33
  };
32
34
  export function createDb(url, options = {}) {
@@ -13,4 +13,9 @@ export const feedItems = pgTable('feed_items', {
13
13
  agentId: text('agent_id').references(() => agents.id),
14
14
  payload: jsonb('payload').notNull(),
15
15
  ...timestamps,
16
- }, (t) => [index('feed_items_created_at_idx').on(t.createdAt)]);
16
+ }, (t) => [
17
+ index('feed_items_created_at_idx').on(t.createdAt),
18
+ // Milestones are written once per agent, and deleting an agent removes
19
+ // its items. Both look up by agent and kind.
20
+ index('feed_items_agent_id_kind_idx').on(t.agentId, t.kind),
21
+ ]);
@@ -1,3 +1,4 @@
1
+ import type { agentMilestones } from './agent-milestones.js';
1
2
  import type { agentRenames } from './agent-renames.js';
2
3
  import type { credentials } from './credentials.js';
3
4
  import type { deletedOperators } from './deleted-operators.js';
@@ -10,8 +11,9 @@ import type { scores } from './scores.js';
10
11
  import type { standing } from './standing.js';
11
12
  import type { taskOutcomes } from './task-outcomes.js';
12
13
  import type { tasks } from './tasks.js';
13
- export { and, asc, desc, eq, gt, inArray, isNotNull, isNull, lt, lte, max, ne, notInArray, or, sql, } from 'drizzle-orm';
14
+ export { and, asc, desc, eq, gt, inArray, isNotNull, isNull, lt, lte, max, ne, notInArray, or, type SQL, sql, } from 'drizzle-orm';
14
15
  export { DrizzleQueryError } from 'drizzle-orm/errors';
16
+ export { agentMilestones } from './agent-milestones.js';
15
17
  export { agentRenames } from './agent-renames.js';
16
18
  export type { Agent, NewAgent } from './agents.js';
17
19
  export { agents } from './agents.js';
@@ -52,5 +54,7 @@ export type AgentRename = typeof agentRenames.$inferSelect;
52
54
  export type NewAgentRename = typeof agentRenames.$inferInsert;
53
55
  export type FeedItem = typeof feedItems.$inferSelect;
54
56
  export type NewFeedItem = typeof feedItems.$inferInsert;
57
+ export type AgentMilestone = typeof agentMilestones.$inferSelect;
58
+ export type NewAgentMilestone = typeof agentMilestones.$inferInsert;
55
59
  export type DeletedOperator = typeof deletedOperators.$inferSelect;
56
60
  export type NewDeletedOperator = typeof deletedOperators.$inferInsert;
package/dist/db/index.js CHANGED
@@ -4,6 +4,7 @@ export { and, asc, desc, eq, gt, inArray, isNotNull, isNull, lt, lte, max, ne, n
4
4
  // What every failed query throws. Its message holds the SQL and every bound
5
5
  // param, so the API error log must never print it.
6
6
  export { DrizzleQueryError } from 'drizzle-orm/errors';
7
+ export { agentMilestones } from './agent-milestones.js';
7
8
  export { agentRenames } from './agent-renames.js';
8
9
  export { agents } from './agents.js';
9
10
  export { createDb } from './client.js';
@@ -14,3 +14,14 @@ export declare function verify(jws: string, publicKey: Uint8Array): Promise<{
14
14
  header: JwsHeader;
15
15
  payload: unknown;
16
16
  }>;
17
+ export declare const Audience: z.ZodString;
18
+ export declare function audienceOf(apiUrl: string): string;
19
+ export declare function withAudience<T extends object>(payload: T, aud: string): T & {
20
+ aud: string;
21
+ };
22
+ export declare function signRequest(payload: object, privateKey: Uint8Array, kid: string, aud: string): Promise<string>;
23
+ export type AudienceCheck = {
24
+ result: 'match' | 'missing' | 'wrong';
25
+ payload: unknown;
26
+ };
27
+ export declare function readAudience(payload: unknown, accepted: readonly string[]): AudienceCheck;
package/dist/envelope.js CHANGED
@@ -68,3 +68,73 @@ export async function verify(jws, publicKey) {
68
68
  const header = parseHeader(parts.header);
69
69
  return { header, payload: parseJson(parts.payload, 'payload') };
70
70
  }
71
+ // Signed request audience (VOU-111). Every agent-signed request names the
72
+ // API it is meant for with `aud`, a top level key inside the signed payload,
73
+ // so a request captured for one API cannot be replayed to another. SEALs
74
+ // carry `iss` instead and do not use this.
75
+ function isHttpOrigin(v) {
76
+ try {
77
+ const url = new URL(v);
78
+ return ((url.protocol === 'http:' || url.protocol === 'https:') &&
79
+ url.origin === v);
80
+ }
81
+ catch {
82
+ return false;
83
+ }
84
+ }
85
+ // An origin with no trailing slash, no path, a lowercase host and no default
86
+ // port, such as https://api.sealkeeper.run.
87
+ export const Audience = z
88
+ .string()
89
+ .max(256)
90
+ .refine(isHttpOrigin, 'aud must be an origin such as https://api.sealkeeper.run');
91
+ // The audience for an API base URL, its origin.
92
+ export function audienceOf(apiUrl) {
93
+ let url;
94
+ try {
95
+ url = new URL(apiUrl);
96
+ }
97
+ catch {
98
+ throw new EnvelopeError('API URL is not a URL');
99
+ }
100
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
101
+ throw new EnvelopeError('API URL must be http or https');
102
+ }
103
+ if (url.origin === 'null') {
104
+ throw new EnvelopeError('API URL has no origin');
105
+ }
106
+ return url.origin;
107
+ }
108
+ // A copy of the payload with `aud` added. Never mutates the input.
109
+ export function withAudience(payload, aud) {
110
+ if (!Audience.safeParse(aud).success) {
111
+ throw new EnvelopeError('aud must be an origin');
112
+ }
113
+ if (Array.isArray(payload)) {
114
+ throw new EnvelopeError('A signed request payload must be an object');
115
+ }
116
+ if (Object.hasOwn(payload, 'aud')) {
117
+ throw new EnvelopeError('Payload already has an aud');
118
+ }
119
+ return { ...payload, aud };
120
+ }
121
+ // How every agent-signed request is signed. The payload gains `aud` inside
122
+ // the signed bytes. `sign` stays for SEALs, which carry `iss`.
123
+ export async function signRequest(payload, privateKey, kid, aud) {
124
+ return sign(withAudience(payload, aud), privateKey, kid);
125
+ }
126
+ // Read `aud` off a verified payload. Call only after `verify`. The compare
127
+ // is exact, the received value is never normalised. A payload that is not a
128
+ // plain object comes back unchanged as missing, so the request schema still
129
+ // rejects it.
130
+ export function readAudience(payload, accepted) {
131
+ if (typeof payload !== 'object' ||
132
+ payload === null ||
133
+ Array.isArray(payload) ||
134
+ !Object.hasOwn(payload, 'aud')) {
135
+ return { result: 'missing', payload };
136
+ }
137
+ const { aud, ...rest } = payload;
138
+ const match = typeof aud === 'string' && accepted.includes(aud);
139
+ return { result: match ? 'match' : 'wrong', payload: rest };
140
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealkeeper/schema",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "private": false,
5
5
  "description": "Zod schemas, event taxonomy and signing helpers for SealKeeper.",
6
6
  "license": "Apache-2.0",