@sealkeeper/schema 0.4.1 → 0.4.3

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.
@@ -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
  }>;
@@ -504,6 +555,57 @@ export declare function createDb(url: string, options?: DbOptions): import("driz
504
555
  identity: undefined;
505
556
  generated: undefined;
506
557
  }, {}, {}>;
558
+ assigneeAgentId: import("drizzle-orm/pg-core").PgColumn<{
559
+ name: "assignee_agent_id";
560
+ tableName: "tasks";
561
+ dataType: "string";
562
+ columnType: "PgText";
563
+ data: string;
564
+ driverParam: string;
565
+ notNull: false;
566
+ hasDefault: false;
567
+ isPrimaryKey: false;
568
+ isAutoincrement: false;
569
+ hasRuntimeDefault: false;
570
+ enumValues: [string, ...string[]];
571
+ baseColumn: never;
572
+ identity: undefined;
573
+ generated: undefined;
574
+ }, {}, {}>;
575
+ pairSettledAt: import("drizzle-orm/pg-core").PgColumn<{
576
+ name: "pair_settled_at";
577
+ tableName: "tasks";
578
+ dataType: "date";
579
+ columnType: "PgTimestamp";
580
+ data: Date;
581
+ driverParam: string;
582
+ notNull: false;
583
+ hasDefault: false;
584
+ isPrimaryKey: false;
585
+ isAutoincrement: false;
586
+ hasRuntimeDefault: false;
587
+ enumValues: undefined;
588
+ baseColumn: never;
589
+ identity: undefined;
590
+ generated: undefined;
591
+ }, {}, {}>;
592
+ overPairCap: import("drizzle-orm/pg-core").PgColumn<{
593
+ name: "over_pair_cap";
594
+ tableName: "tasks";
595
+ dataType: "boolean";
596
+ columnType: "PgBoolean";
597
+ data: boolean;
598
+ driverParam: boolean;
599
+ notNull: true;
600
+ hasDefault: true;
601
+ isPrimaryKey: false;
602
+ isAutoincrement: false;
603
+ hasRuntimeDefault: false;
604
+ enumValues: undefined;
605
+ baseColumn: never;
606
+ identity: undefined;
607
+ generated: undefined;
608
+ }, {}, {}>;
507
609
  taskType: import("drizzle-orm/pg-core").PgColumn<{
508
610
  name: "task_type";
509
611
  tableName: "tasks";
@@ -1814,6 +1916,81 @@ export declare function createDb(url: string, options?: DbOptions): import("driz
1814
1916
  };
1815
1917
  dialect: 'pg';
1816
1918
  }>;
1919
+ agentMilestones: import("drizzle-orm/pg-core").PgTableWithColumns<{
1920
+ name: "agent_milestones";
1921
+ schema: undefined;
1922
+ columns: {
1923
+ createdAt: import("drizzle-orm/pg-core").PgColumn<{
1924
+ name: "created_at";
1925
+ tableName: "agent_milestones";
1926
+ dataType: "date";
1927
+ columnType: "PgTimestamp";
1928
+ data: Date;
1929
+ driverParam: string;
1930
+ notNull: true;
1931
+ hasDefault: true;
1932
+ isPrimaryKey: false;
1933
+ isAutoincrement: false;
1934
+ hasRuntimeDefault: false;
1935
+ enumValues: undefined;
1936
+ baseColumn: never;
1937
+ identity: undefined;
1938
+ generated: undefined;
1939
+ }, {}, {}>;
1940
+ updatedAt: import("drizzle-orm/pg-core").PgColumn<{
1941
+ name: "updated_at";
1942
+ tableName: "agent_milestones";
1943
+ dataType: "date";
1944
+ columnType: "PgTimestamp";
1945
+ data: Date;
1946
+ driverParam: string;
1947
+ notNull: true;
1948
+ hasDefault: true;
1949
+ isPrimaryKey: false;
1950
+ isAutoincrement: false;
1951
+ hasRuntimeDefault: false;
1952
+ enumValues: undefined;
1953
+ baseColumn: never;
1954
+ identity: undefined;
1955
+ generated: undefined;
1956
+ }, {}, {}>;
1957
+ agentId: import("drizzle-orm/pg-core").PgColumn<{
1958
+ name: "agent_id";
1959
+ tableName: "agent_milestones";
1960
+ dataType: "string";
1961
+ columnType: "PgText";
1962
+ data: string;
1963
+ driverParam: string;
1964
+ notNull: true;
1965
+ hasDefault: false;
1966
+ isPrimaryKey: false;
1967
+ isAutoincrement: false;
1968
+ hasRuntimeDefault: false;
1969
+ enumValues: [string, ...string[]];
1970
+ baseColumn: never;
1971
+ identity: undefined;
1972
+ generated: undefined;
1973
+ }, {}, {}>;
1974
+ milestone: import("drizzle-orm/pg-core").PgColumn<{
1975
+ name: "milestone";
1976
+ tableName: "agent_milestones";
1977
+ dataType: "string";
1978
+ columnType: "PgText";
1979
+ data: string;
1980
+ driverParam: string;
1981
+ notNull: true;
1982
+ hasDefault: false;
1983
+ isPrimaryKey: false;
1984
+ isAutoincrement: false;
1985
+ hasRuntimeDefault: false;
1986
+ enumValues: [string, ...string[]];
1987
+ baseColumn: never;
1988
+ identity: undefined;
1989
+ generated: undefined;
1990
+ }, {}, {}>;
1991
+ };
1992
+ dialect: 'pg';
1993
+ }>;
1817
1994
  deletedOperators: import("drizzle-orm/pg-core").PgTableWithColumns<{
1818
1995
  name: "deleted_operators";
1819
1996
  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';
@@ -87,6 +87,57 @@ export declare const tasks: import("drizzle-orm/pg-core").PgTableWithColumns<{
87
87
  identity: undefined;
88
88
  generated: undefined;
89
89
  }, {}, {}>;
90
+ assigneeAgentId: import("drizzle-orm/pg-core").PgColumn<{
91
+ name: "assignee_agent_id";
92
+ tableName: "tasks";
93
+ dataType: "string";
94
+ columnType: "PgText";
95
+ data: string;
96
+ driverParam: string;
97
+ notNull: false;
98
+ hasDefault: false;
99
+ isPrimaryKey: false;
100
+ isAutoincrement: false;
101
+ hasRuntimeDefault: false;
102
+ enumValues: [string, ...string[]];
103
+ baseColumn: never;
104
+ identity: undefined;
105
+ generated: undefined;
106
+ }, {}, {}>;
107
+ pairSettledAt: import("drizzle-orm/pg-core").PgColumn<{
108
+ name: "pair_settled_at";
109
+ tableName: "tasks";
110
+ dataType: "date";
111
+ columnType: "PgTimestamp";
112
+ data: Date;
113
+ driverParam: string;
114
+ notNull: false;
115
+ hasDefault: false;
116
+ isPrimaryKey: false;
117
+ isAutoincrement: false;
118
+ hasRuntimeDefault: false;
119
+ enumValues: undefined;
120
+ baseColumn: never;
121
+ identity: undefined;
122
+ generated: undefined;
123
+ }, {}, {}>;
124
+ overPairCap: import("drizzle-orm/pg-core").PgColumn<{
125
+ name: "over_pair_cap";
126
+ tableName: "tasks";
127
+ dataType: "boolean";
128
+ columnType: "PgBoolean";
129
+ data: boolean;
130
+ driverParam: boolean;
131
+ notNull: true;
132
+ hasDefault: true;
133
+ isPrimaryKey: false;
134
+ isAutoincrement: false;
135
+ hasRuntimeDefault: false;
136
+ enumValues: undefined;
137
+ baseColumn: never;
138
+ identity: undefined;
139
+ generated: undefined;
140
+ }, {}, {}>;
90
141
  taskType: import("drizzle-orm/pg-core").PgColumn<{
91
142
  name: "task_type";
92
143
  tableName: "tasks";
package/dist/db/tasks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { sql } from 'drizzle-orm';
2
- import { index, jsonb, pgTable, text, timestamp, uuid, } from 'drizzle-orm/pg-core';
2
+ import { boolean, index, jsonb, pgTable, text, timestamp, uuid, } from 'drizzle-orm/pg-core';
3
3
  import { agents } from './agents.js';
4
4
  import { timestamps } from './timestamps.js';
5
5
  export const tasks = pgTable('tasks', {
@@ -8,6 +8,21 @@ export const tasks = pgTable('tasks', {
8
8
  .notNull()
9
9
  .references(() => agents.id),
10
10
  claimantAgentId: text('claimant_agent_id').references(() => agents.id),
11
+ // The one agent an addressed task is for, null for an open task. Only it
12
+ // can claim the task, and the open pool leaves it out. Set at post and
13
+ // never changed, except that deleting the assignee agent deletes its
14
+ // unverified addressed tasks and nulls this on its verified ones, whose
15
+ // claimant it was (deleteAgents in apps/api/src/account.ts).
16
+ assigneeAgentId: text('assignee_agent_id').references(() => agents.id),
17
+ // When a claimed addressed task settled for its operator pair's cap,
18
+ // verified_at when it verified, expires_at when it expired unverified.
19
+ // Null for an open task and until it settles. Set once, with
20
+ // over_pair_cap, by apps/api/src/pair-cap.ts.
21
+ pairSettledAt: timestamp('pair_settled_at', { withTimezone: true }),
22
+ // True for a settled addressed task that came past its operator pair's
23
+ // cap, so it weighs nothing as evidence, success or failure. False for
24
+ // every other task.
25
+ overPairCap: boolean('over_pair_cap').notNull().default(false),
11
26
  taskType: text('task_type').notNull(),
12
27
  spec: jsonb('spec').notNull(),
13
28
  verification: jsonb('verification').notNull(),
@@ -36,10 +51,28 @@ export const tasks = pgTable('tasks', {
36
51
  index('tasks_poster_agent_id_idx').on(t.posterAgentId),
37
52
  // The claim cap per claimant, activity counts, scoring and evidence.
38
53
  index('tasks_claimant_agent_id_idx').on(t.claimantAgentId),
39
- // GET /v1/tasks?state=open, oldest first.
54
+ // GET /v1/tasks?state=open and the board, oldest or newest first. The
55
+ // open pool filters out addressed tasks on top of it.
40
56
  index('tasks_open_posted_at_idx')
41
57
  .on(t.postedAt, t.id)
42
58
  .where(sql `${t.claimedAt} is null and ${t.verifiedAt} is null`),
59
+ // Open tasks addressed to one agent, oldest first. GET /v1/tasks?assignee=
60
+ // and the per assignee caps.
61
+ index('tasks_open_assignee_posted_at_idx')
62
+ .on(t.assigneeAgentId, t.postedAt, t.id)
63
+ .where(sql `${t.assigneeAgentId} is not null and ${t.claimedAt} is null and ${t.verifiedAt} is null`),
64
+ // Addressed tasks by assignee and settle time. The pair cap counts an
65
+ // assignee's settled tasks over the last days, and a deletion finds the
66
+ // tasks addressed to it.
67
+ index('tasks_assignee_settled_at_idx')
68
+ .on(t.assigneeAgentId, t.pairSettledAt)
69
+ .where(sql `${t.assigneeAgentId} is not null`),
70
+ // Claimed addressed tasks not yet settled for the pair cap, by settle
71
+ // time. The pair cap walks and the scoring run's pass read it. Small,
72
+ // since a task leaves it once it is decided.
73
+ index('tasks_addressed_pending_idx')
74
+ .on(sql `least(${t.verifiedAt}, ${t.expiresAt})`)
75
+ .where(sql `${t.assigneeAgentId} is not null and ${t.claimedAt} is not null and ${t.pairSettledAt} is null`),
43
76
  // GET /v1/tasks?state=claimed, oldest first.
44
77
  index('tasks_claimed_posted_at_idx')
45
78
  .on(t.postedAt, t.id)
@@ -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.3",
4
4
  "private": false,
5
5
  "description": "Zod schemas, event taxonomy and signing helpers for SealKeeper.",
6
6
  "license": "Apache-2.0",