@sealkeeper/schema 0.0.1 → 0.4.1

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 (68) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +14 -1
  4. package/dist/agent-id.d.ts +13 -0
  5. package/dist/agent-id.js +29 -0
  6. package/dist/agent-name.d.ts +9 -0
  7. package/dist/agent-name.js +58 -0
  8. package/dist/api.d.ts +1212 -0
  9. package/dist/api.js +633 -0
  10. package/dist/base64url.d.ts +4 -0
  11. package/dist/base64url.js +43 -0
  12. package/dist/credential.d.ts +392 -0
  13. package/dist/credential.js +287 -0
  14. package/dist/db/agent-renames.d.ts +126 -0
  15. package/dist/db/agent-renames.js +20 -0
  16. package/dist/db/agents.d.ts +145 -0
  17. package/dist/db/agents.js +40 -0
  18. package/dist/db/client.d.ts +1896 -0
  19. package/dist/db/client.js +36 -0
  20. package/dist/db/credentials.d.ts +143 -0
  21. package/dist/db/credentials.js +16 -0
  22. package/dist/db/deleted-operators.d.ts +75 -0
  23. package/dist/db/deleted-operators.js +13 -0
  24. package/dist/db/events.d.ts +160 -0
  25. package/dist/db/events.js +34 -0
  26. package/dist/db/feed-items.d.ts +109 -0
  27. package/dist/db/feed-items.js +16 -0
  28. package/dist/db/index.d.ts +56 -0
  29. package/dist/db/index.js +23 -0
  30. package/dist/db/migrate.d.ts +1 -0
  31. package/dist/db/migrate.js +30 -0
  32. package/dist/db/migrator.d.ts +2 -0
  33. package/dist/db/migrator.js +20 -0
  34. package/dist/db/operators.d.ts +109 -0
  35. package/dist/db/operators.js +11 -0
  36. package/dist/db/quota-counters.d.ts +109 -0
  37. package/dist/db/quota-counters.js +17 -0
  38. package/dist/db/ratings.d.ts +160 -0
  39. package/dist/db/ratings.js +27 -0
  40. package/dist/db/scores.d.ts +160 -0
  41. package/dist/db/scores.js +15 -0
  42. package/dist/db/standing.d.ts +228 -0
  43. package/dist/db/standing.js +29 -0
  44. package/dist/db/task-outcomes.d.ts +126 -0
  45. package/dist/db/task-outcomes.js +18 -0
  46. package/dist/db/tasks.d.ts +251 -0
  47. package/dist/db/tasks.js +49 -0
  48. package/dist/db/timestamps.d.ts +4 -0
  49. package/dist/db/timestamps.js +24 -0
  50. package/dist/db/url.d.ts +12 -0
  51. package/dist/db/url.js +25 -0
  52. package/dist/dimensions.d.ts +21 -0
  53. package/dist/dimensions.js +15 -0
  54. package/dist/envelope.d.ts +16 -0
  55. package/dist/envelope.js +70 -0
  56. package/dist/events.d.ts +153 -0
  57. package/dist/events.js +98 -0
  58. package/dist/index.d.ts +12 -0
  59. package/dist/index.js +12 -0
  60. package/dist/seal-conformance.d.ts +14 -0
  61. package/dist/seal-conformance.js +176 -0
  62. package/dist/standing.d.ts +46 -0
  63. package/dist/standing.js +79 -0
  64. package/dist/tasks.d.ts +49 -0
  65. package/dist/tasks.js +132 -0
  66. package/dist/top-dimensions.d.ts +9 -0
  67. package/dist/top-dimensions.js +13 -0
  68. package/package.json +69 -5
@@ -0,0 +1,176 @@
1
+ import { signAsync } from '@noble/ed25519';
2
+ import { generateKeypair } from './agent-id.js';
3
+ import { base64urlEncode, utf8Encode } from './base64url.js';
4
+ import { CREDENTIAL_ISSUER, LEGACY_ISSUER_UNTIL, LEGACY_ISSUERS, SEAL_CLOCK_SKEW_SECONDS, SEAL_MAX_TTL_SECONDS, SEAL_VERSION, } from './credential.js';
5
+ import { sign } from './envelope.js';
6
+ const KID = 'vouched-conformance-1';
7
+ // 1 October 2026, after LEGACY_UNTIL, so no case leans on the legacy shape,
8
+ // and before LEGACY_ISSUER_UNTIL, so a legacy issuer is still accepted.
9
+ const NOW = 1_790_812_800;
10
+ const LEGACY_ISSUER = LEGACY_ISSUERS[0];
11
+ const HOUR = 3600;
12
+ const header = (kid) => base64urlEncode(utf8Encode(JSON.stringify({ alg: 'EdDSA', kid })));
13
+ // Signs payload bytes as they are, so a case can carry a payload that is not
14
+ // JSON. sign() in envelope.ts always writes JSON.
15
+ async function signRaw(body, privateKey, kid) {
16
+ const input = `${header(kid)}.${base64urlEncode(utf8Encode(body))}`;
17
+ const signature = await signAsync(utf8Encode(input), privateKey);
18
+ return `${input}.${base64urlEncode(signature)}`;
19
+ }
20
+ export async function sealConformanceCases() {
21
+ const pair = await generateKeypair();
22
+ const other = await generateKeypair();
23
+ const base = {
24
+ iss: CREDENTIAL_ISSUER,
25
+ sub: pair.agentId,
26
+ ver: SEAL_VERSION,
27
+ iat: NOW - HOUR,
28
+ exp: NOW + 23 * HOUR,
29
+ agent_version: '1.0.0',
30
+ version: '1.0.0',
31
+ level: 'bronze',
32
+ scores: { reliability: 0.9, safety: null },
33
+ counts: {
34
+ events: 40,
35
+ history_days: 4,
36
+ verified_tasks: 26,
37
+ seed_tasks: 25,
38
+ server_checked_tasks: 1,
39
+ confirmed_tasks: 0,
40
+ distinct_operators: 1,
41
+ safety_incidents_90d: 0,
42
+ },
43
+ operator: { verified: false },
44
+ identity: [],
45
+ last_active: NOW - HOUR,
46
+ dormant_days: 0,
47
+ };
48
+ const signed = (payload) => sign(payload, pair.privateKey, KID);
49
+ const valid = await signed(base);
50
+ const [h, , s] = valid.split('.');
51
+ const altered = `${h}.${base64urlEncode(utf8Encode(JSON.stringify({ ...base, level: 'gold' })))}.${s}`;
52
+ const cases = [
53
+ ['a valid SEAL', valid, 'valid'],
54
+ [
55
+ 'iat ahead of now within the clock skew',
56
+ signed({
57
+ ...base,
58
+ iat: NOW + SEAL_CLOCK_SKEW_SECONDS,
59
+ exp: NOW + SEAL_CLOCK_SKEW_SECONDS + 23 * HOUR,
60
+ }),
61
+ 'valid',
62
+ ],
63
+ [
64
+ 'expired',
65
+ signed({ ...base, iat: NOW - 25 * HOUR, exp: NOW - HOUR }),
66
+ 'expired',
67
+ ],
68
+ [
69
+ 'expiring this second',
70
+ signed({ ...base, iat: NOW - 24 * HOUR, exp: NOW }),
71
+ 'expired',
72
+ ],
73
+ [
74
+ 'iat in the future past the clock skew',
75
+ signed({
76
+ ...base,
77
+ iat: NOW + SEAL_CLOCK_SKEW_SECONDS + 1,
78
+ exp: NOW + SEAL_CLOCK_SKEW_SECONDS + 1 + 23 * HOUR,
79
+ }),
80
+ 'not_yet_valid',
81
+ ],
82
+ ['an unknown ver', signed({ ...base, ver: 2 }), 'unsupported_version'],
83
+ [
84
+ 'no ver after the legacy cutoff',
85
+ signed({ ...base, ver: undefined }),
86
+ 'unsupported_version',
87
+ ],
88
+ [
89
+ 'the wrong shape, a level not in the standard',
90
+ signed({ ...base, level: 'platinum' }),
91
+ 'malformed',
92
+ ],
93
+ [
94
+ 'a score above 1',
95
+ signed({ ...base, scores: { reliability: 1.2 } }),
96
+ 'malformed',
97
+ ],
98
+ [
99
+ 'a negative count',
100
+ signed({
101
+ ...base,
102
+ counts: { ...base.counts, events: -1 },
103
+ }),
104
+ 'malformed',
105
+ ],
106
+ [
107
+ 'exp more than 24 hours after iat',
108
+ signed({ ...base, exp: base.iat + SEAL_MAX_TTL_SECONDS + 1 }),
109
+ 'malformed',
110
+ ],
111
+ [
112
+ 'a signed payload that is not JSON',
113
+ signRaw('not json', pair.privateKey, KID),
114
+ 'malformed',
115
+ ],
116
+ [
117
+ 'the wrong issuer, full payload',
118
+ signed({ ...base, iss: 'other.example' }),
119
+ 'wrong_issuer',
120
+ ],
121
+ [
122
+ 'the wrong issuer, a payload in no SEAL shape',
123
+ signed({ iss: 'other.example', sub: 'someone' }),
124
+ 'wrong_issuer',
125
+ ],
126
+ [
127
+ 'a legacy issuer before LEGACY_ISSUER_UNTIL',
128
+ signed({ ...base, iss: LEGACY_ISSUER }),
129
+ 'valid',
130
+ ],
131
+ [
132
+ 'a legacy issuer from LEGACY_ISSUER_UNTIL on',
133
+ signed({
134
+ ...base,
135
+ iss: LEGACY_ISSUER,
136
+ iat: LEGACY_ISSUER_UNTIL - HOUR,
137
+ exp: LEGACY_ISSUER_UNTIL + 23 * HOUR,
138
+ last_active: LEGACY_ISSUER_UNTIL - HOUR,
139
+ }),
140
+ 'wrong_issuer',
141
+ LEGACY_ISSUER_UNTIL,
142
+ ],
143
+ ['not a JWS', 'not.a.seal', 'malformed'],
144
+ [
145
+ 'a kid no published key has',
146
+ sign(base, other.privateKey, 'retired-1'),
147
+ 'unknown_kid',
148
+ ],
149
+ ['a payload altered after signing', altered, 'bad_signature'],
150
+ [
151
+ 'signed by another key under the published kid',
152
+ sign(base, other.privateKey, KID),
153
+ 'bad_signature',
154
+ ],
155
+ ];
156
+ return {
157
+ nowSeconds: NOW,
158
+ wellKnown: {
159
+ keys: [
160
+ {
161
+ kid: KID,
162
+ kty: 'OKP',
163
+ crv: 'Ed25519',
164
+ alg: 'EdDSA',
165
+ x: base64urlEncode(pair.publicKey),
166
+ },
167
+ ],
168
+ },
169
+ cases: await Promise.all(cases.map(async ([name, jws, expected, nowSeconds]) => ({
170
+ name,
171
+ jws: await jws,
172
+ expected,
173
+ ...(nowSeconds === undefined ? {} : { nowSeconds }),
174
+ }))),
175
+ };
176
+ }
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+ export declare const Level: z.ZodEnum<{
3
+ bronze: "bronze";
4
+ gold: "gold";
5
+ none: "none";
6
+ silver: "silver";
7
+ }>;
8
+ export type Level = z.infer<typeof Level>;
9
+ export declare const LEVEL_RANK: Record<Level, number>;
10
+ export declare const StandingCounts: z.ZodObject<{
11
+ events: z.ZodInt;
12
+ history_days: z.ZodInt;
13
+ verified_tasks: z.ZodInt;
14
+ seed_tasks: z.ZodInt;
15
+ server_checked_tasks: z.ZodInt;
16
+ confirmed_tasks: z.ZodInt;
17
+ distinct_operators: z.ZodInt;
18
+ safety_incidents_90d: z.ZodInt;
19
+ }, z.core.$strict>;
20
+ export type StandingCounts = z.infer<typeof StandingCounts>;
21
+ export declare const DAY_MS = 86400000;
22
+ export declare const DORMANCY: {
23
+ readonly quietDays: 14;
24
+ readonly dropOneDays: 30;
25
+ readonly dropTwoDays: 60;
26
+ readonly noneDays: 90;
27
+ };
28
+ export declare function dormantDays(lastActive: Date | null, now: Date): number | null;
29
+ export declare const isQuiet: (days: number | null) => boolean;
30
+ export declare const Standing: z.ZodObject<{
31
+ counts: z.ZodObject<{
32
+ events: z.ZodInt;
33
+ history_days: z.ZodInt;
34
+ verified_tasks: z.ZodInt;
35
+ seed_tasks: z.ZodInt;
36
+ server_checked_tasks: z.ZodInt;
37
+ confirmed_tasks: z.ZodInt;
38
+ distinct_operators: z.ZodInt;
39
+ safety_incidents_90d: z.ZodInt;
40
+ }, z.core.$strict>;
41
+ history_days: z.ZodInt;
42
+ last_active: z.ZodNullable<z.ZodISODateTime>;
43
+ dormant_days: z.ZodNullable<z.ZodInt>;
44
+ quiet: z.ZodBoolean;
45
+ }, z.core.$strict>;
46
+ export type Standing = z.infer<typeof Standing>;
@@ -0,0 +1,79 @@
1
+ import { z } from 'zod';
2
+ // Standing levels from the SEAL standard, section 5, lowest first. none is
3
+ // below bronze. Every scored agent version has one.
4
+ export const Level = z.enum(['none', 'bronze', 'silver', 'gold']);
5
+ // Higher is better. The leaderboard sorts on it before the score.
6
+ export const LEVEL_RANK = {
7
+ none: 0,
8
+ bronze: 1,
9
+ silver: 2,
10
+ gold: 3,
11
+ };
12
+ const Count = z.int().min(0);
13
+ // The evidence counts of the SEAL standard, section 4, over the scoring
14
+ // window (180 days) for one agent version. The names are the standard's, so
15
+ // they carry over to the SEAL unchanged.
16
+ //
17
+ // events signed events accepted in the window
18
+ // history_days distinct UTC days with an accepted event, the
19
+ // agent's across all its versions
20
+ // verified_tasks seed_tasks + server_checked_tasks + confirmed_tasks
21
+ // seed_tasks verified tasks the seed agent posted
22
+ // server_checked_tasks hash or schema tasks from another operator's agent
23
+ // confirmed_tasks counterparty tasks from another operator's agent,
24
+ // both sides reported and the reports agree
25
+ // distinct_operators operators other than the agent's own behind its
26
+ // server checked and confirmed tasks
27
+ // safety_incidents_90d incident events in the last 90 days
28
+ //
29
+ // verified_tasks is always the sum of the three kinds, for a new version's
30
+ // inherited counts too, and a SEAL whose counts break that does not parse.
31
+ export const StandingCounts = z
32
+ .strictObject({
33
+ events: Count,
34
+ history_days: Count,
35
+ verified_tasks: Count,
36
+ seed_tasks: Count,
37
+ server_checked_tasks: Count,
38
+ confirmed_tasks: Count,
39
+ distinct_operators: Count,
40
+ safety_incidents_90d: Count,
41
+ })
42
+ .refine((c) => c.verified_tasks ===
43
+ c.seed_tasks + c.server_checked_tasks + c.confirmed_tasks, {
44
+ message: 'verified_tasks must equal seed_tasks + server_checked_tasks + confirmed_tasks',
45
+ path: ['verified_tasks'],
46
+ });
47
+ export const DAY_MS = 86_400_000;
48
+ // The dormancy ladder of the SEAL standard, section 5, in whole days since
49
+ // the agent's last accepted event. At quietDays the badge shows quiet and
50
+ // the level stays. At dropOneDays and dropTwoDays the level drops one step
51
+ // each. At noneDays the level is none and no SEAL is issued. The scoring
52
+ // job applies it and the CLI explains it, so it lives here.
53
+ export const DORMANCY = {
54
+ quietDays: 14,
55
+ dropOneDays: 30,
56
+ dropTwoDays: 60,
57
+ noneDays: 90,
58
+ };
59
+ // Whole days from lastActive to now, never below zero. null when the agent
60
+ // has sent no accepted event. The one place dormancy is counted.
61
+ export function dormantDays(lastActive, now) {
62
+ if (lastActive === null)
63
+ return null;
64
+ return Math.max(0, Math.floor((now.getTime() - lastActive.getTime()) / DAY_MS));
65
+ }
66
+ // True at quietDays or more. Never active is not quiet.
67
+ export const isQuiet = (days) => days !== null && days >= DORMANCY.quietDays;
68
+ // What the agent answers carry beside level, read from the standing table
69
+ // for the current version. last_active is the newest accepted event of the
70
+ // agent on any version, null when it has sent none. dormant_days is whole
71
+ // days since last_active at the time of the answer, null with it. quiet is
72
+ // true when dormant_days is at DORMANCY.quietDays or more.
73
+ export const Standing = z.strictObject({
74
+ counts: StandingCounts,
75
+ history_days: Count,
76
+ last_active: z.iso.datetime().nullable(),
77
+ dormant_days: Count.nullable(),
78
+ quiet: z.boolean(),
79
+ });
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ export declare const Sha256Hex: z.ZodString;
3
+ export declare const JSON_SCHEMA_MAX_BYTES = 16384;
4
+ export declare const HashVerification: z.ZodObject<{
5
+ kind: z.ZodLiteral<"hash">;
6
+ sha256: z.ZodString;
7
+ }, z.core.$strict>;
8
+ export declare const SchemaVerification: z.ZodObject<{
9
+ kind: z.ZodLiteral<"schema">;
10
+ jsonSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
11
+ }, z.core.$strict>;
12
+ export declare const CounterpartyVerification: z.ZodObject<{
13
+ kind: z.ZodLiteral<"counterparty">;
14
+ }, z.core.$strict>;
15
+ export declare const VerificationSpec: z.ZodDiscriminatedUnion<[z.ZodObject<{
16
+ kind: z.ZodLiteral<"hash">;
17
+ sha256: z.ZodString;
18
+ }, z.core.$strict>, z.ZodObject<{
19
+ kind: z.ZodLiteral<"schema">;
20
+ jsonSchema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
21
+ }, z.core.$strict>, z.ZodObject<{
22
+ kind: z.ZodLiteral<"counterparty">;
23
+ }, z.core.$strict>], "kind">;
24
+ export type VerificationSpec = z.infer<typeof VerificationSpec>;
25
+ export declare const TaskOutcome: z.ZodEnum<{
26
+ failure: "failure";
27
+ success: "success";
28
+ }>;
29
+ export type TaskOutcome = z.infer<typeof TaskOutcome>;
30
+ export declare const TaskState: z.ZodEnum<{
31
+ claimed: "claimed";
32
+ expired: "expired";
33
+ open: "open";
34
+ submitted: "submitted";
35
+ verified: "verified";
36
+ }>;
37
+ export type TaskState = z.infer<typeof TaskState>;
38
+ type Timestamp = Date | string;
39
+ export type TaskTimestamps = {
40
+ claimedAt: Timestamp | null;
41
+ submittedAt: Timestamp | null;
42
+ verifiedAt: Timestamp | null;
43
+ expiresAt: Timestamp;
44
+ };
45
+ export declare function taskState(task: TaskTimestamps, now?: Date): TaskState;
46
+ type JsonObject = Record<string, unknown>;
47
+ export declare function redactJsonSchema(schema: JsonObject): JsonObject;
48
+ export declare function publicVerification(spec: VerificationSpec): VerificationSpec;
49
+ export {};
package/dist/tasks.js ADDED
@@ -0,0 +1,132 @@
1
+ import { z } from 'zod';
2
+ import { utf8Encode } from './base64url.js';
3
+ export const Sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
4
+ export const JSON_SCHEMA_MAX_BYTES = 16384;
5
+ export const HashVerification = z.strictObject({
6
+ kind: z.literal('hash'),
7
+ sha256: Sha256Hex,
8
+ });
9
+ export const SchemaVerification = z.strictObject({
10
+ kind: z.literal('schema'),
11
+ jsonSchema: z
12
+ .record(z.string(), z.unknown())
13
+ .refine((schema) => utf8Encode(JSON.stringify(schema)).length <= JSON_SCHEMA_MAX_BYTES, `jsonSchema must be at most ${JSON_SCHEMA_MAX_BYTES} bytes`),
14
+ });
15
+ export const CounterpartyVerification = z.strictObject({
16
+ kind: z.literal('counterparty'),
17
+ });
18
+ export const VerificationSpec = z.discriminatedUnion('kind', [
19
+ HashVerification,
20
+ SchemaVerification,
21
+ CounterpartyVerification,
22
+ ]);
23
+ export const TaskOutcome = z.enum(['success', 'failure']);
24
+ export const TaskState = z.enum([
25
+ 'open',
26
+ 'claimed',
27
+ 'submitted',
28
+ 'verified',
29
+ 'expired',
30
+ ]);
31
+ export function taskState(task, now = new Date()) {
32
+ if (task.verifiedAt !== null)
33
+ return 'verified';
34
+ if (new Date(task.expiresAt).getTime() <= now.getTime())
35
+ return 'expired';
36
+ if (task.submittedAt !== null)
37
+ return 'submitted';
38
+ if (task.claimedAt !== null)
39
+ return 'claimed';
40
+ return 'open';
41
+ }
42
+ /*
43
+ * The public view of a task's JSON schema. A schema task can pin its expected
44
+ * values with const, enum or pattern, so the stored schema may contain the
45
+ * answer. Reads show this redacted copy and submit verifies against the full
46
+ * one, which never leaves the server.
47
+ *
48
+ * An allowlist, not a denylist. Only keywords that describe the shape are
49
+ * kept, so a keyword added later cannot leak by default. Subschemas under
50
+ * properties, items, prefixItems and additionalProperties are redacted the
51
+ * same way. Everything else (const, enum, pattern, examples, default, anyOf,
52
+ * $ref, $defs, description and the rest) is dropped.
53
+ */
54
+ const SHAPE_DATA = new Set([
55
+ 'type',
56
+ 'required',
57
+ 'minimum',
58
+ 'maximum',
59
+ 'exclusiveMinimum',
60
+ 'exclusiveMaximum',
61
+ 'minLength',
62
+ 'maxLength',
63
+ 'minItems',
64
+ 'maxItems',
65
+ ]);
66
+ const SHAPE_ONE = new Set(['items', 'additionalProperties']);
67
+ const isJsonObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
68
+ // A boolean is a valid subschema and says nothing about the answer, so it
69
+ // stays. Anything else that is not an object becomes the empty schema.
70
+ function redactSubschema(node) {
71
+ if (typeof node === 'boolean')
72
+ return node;
73
+ return isJsonObject(node) ? redactJsonSchema(node) : {};
74
+ }
75
+ const BOUNDS = [
76
+ 'minimum',
77
+ 'maximum',
78
+ 'exclusiveMinimum',
79
+ 'exclusiveMaximum',
80
+ ];
81
+ const finite = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
82
+ // True when the numeric bounds leave room for one value only, such as
83
+ // minimum 42 with maximum 42, or an integer above 41 and below 43. Those
84
+ // pin the answer as surely as const, so the redacted copy drops all four
85
+ // bounds (VOU-107). Not exported, so the schema package's exports stay
86
+ // the ones on npm.
87
+ function boundsPinOneValue(schema) {
88
+ const [min, max, xmin, xmax] = BOUNDS.map((k) => finite(schema[k]));
89
+ if (min !== undefined && min === max)
90
+ return true;
91
+ // Null never meets a numeric bound, so a nullable integer leaks the
92
+ // same one value as a plain integer.
93
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
94
+ const numeric = types.filter((t) => t !== 'null');
95
+ if (numeric.length === 0 || !numeric.every((t) => t === 'integer')) {
96
+ return false;
97
+ }
98
+ const lo = Math.max(min === undefined ? -Infinity : Math.ceil(min), xmin === undefined ? -Infinity : Math.floor(xmin) + 1);
99
+ const hi = Math.min(max === undefined ? Infinity : Math.floor(max), xmax === undefined ? Infinity : Math.ceil(xmax) - 1);
100
+ return lo === hi;
101
+ }
102
+ export function redactJsonSchema(schema) {
103
+ const pinned = boundsPinOneValue(schema);
104
+ const out = {};
105
+ for (const [key, value] of Object.entries(schema)) {
106
+ if (pinned && BOUNDS.includes(key))
107
+ continue;
108
+ if (SHAPE_DATA.has(key)) {
109
+ out[key] = Array.isArray(value) ? [...value] : value;
110
+ }
111
+ else if (SHAPE_ONE.has(key)) {
112
+ out[key] = redactSubschema(value);
113
+ }
114
+ else if (key === 'prefixItems' && Array.isArray(value)) {
115
+ out[key] = value.map(redactSubschema);
116
+ }
117
+ else if (key === 'properties' && isJsonObject(value)) {
118
+ out[key] = Object.fromEntries(Object.entries(value).map(([name, sub]) => [
119
+ name,
120
+ redactSubschema(sub),
121
+ ]));
122
+ }
123
+ }
124
+ return out;
125
+ }
126
+ // What GET /v1/tasks and GET /v1/tasks/:id show. Hash and counterparty specs
127
+ // are already safe to show, a sha256 is one way.
128
+ export function publicVerification(spec) {
129
+ if (spec.kind !== 'schema')
130
+ return spec;
131
+ return { kind: 'schema', jsonSchema: redactJsonSchema(spec.jsonSchema) };
132
+ }
@@ -0,0 +1,9 @@
1
+ import { BaseDimension } from './dimensions.js';
2
+ export type TopDimension = {
3
+ dimension: BaseDimension;
4
+ value: number;
5
+ };
6
+ export declare function topBaseDimensions(scores: readonly {
7
+ dimension: string;
8
+ value: number | null;
9
+ }[], count: number): TopDimension[];
@@ -0,0 +1,13 @@
1
+ import { BaseDimension } from './dimensions.js';
2
+ // Base dimensions with a value, highest first. Ties keep the fixed order of
3
+ // BaseDimension. Callers that want trust only filter with isTrustDimension
4
+ // first.
5
+ export function topBaseDimensions(scores, count) {
6
+ const ranked = [];
7
+ for (const d of BaseDimension.options) {
8
+ const value = scores.find((s) => s.dimension === d)?.value;
9
+ if (typeof value === 'number')
10
+ ranked.push({ dimension: d, value });
11
+ }
12
+ return ranked.sort((a, b) => b.value - a.value).slice(0, count);
13
+ }
package/package.json CHANGED
@@ -1,9 +1,73 @@
1
1
  {
2
2
  "name": "@sealkeeper/schema",
3
- "version": "0.0.1",
4
- "description": "Sealkeeper shared schema. Placeholder until the first release.",
3
+ "version": "0.4.1",
4
+ "private": false,
5
+ "description": "Zod schemas, event taxonomy and signing helpers for SealKeeper.",
5
6
  "license": "Apache-2.0",
6
- "homepage": "https://sealkeeper.run",
7
- "publishConfig": { "access": "public" },
8
- "files": ["README.md"]
7
+ "homepage": "https://sealkeeper.run/seal/standard",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/sealkeeper-dev/sealkeeper.git",
11
+ "directory": "packages/schema"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/sealkeeper-dev/cli/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "default": "./dist/index.js"
24
+ },
25
+ "./conformance": {
26
+ "types": "./dist/seal-conformance.d.ts",
27
+ "default": "./dist/seal-conformance.js"
28
+ },
29
+ "./db": {
30
+ "types": "./dist/db/index.d.ts",
31
+ "default": "./dist/db/index.js"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE",
38
+ "NOTICE"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json",
42
+ "typecheck": "tsc -p tsconfig.json --noEmit",
43
+ "prepublishOnly": "rm -rf dist && pnpm run build",
44
+ "test": "tsc -p tsconfig.json && vitest run",
45
+ "db:generate": "drizzle-kit generate",
46
+ "db:migrate": "drizzle-kit migrate",
47
+ "db:migrate:prod": "node dist/db/migrate.js"
48
+ },
49
+ "dependencies": {
50
+ "@noble/ed25519": "^3.2.0",
51
+ "zod": "^4.6.5"
52
+ },
53
+ "peerDependencies": {
54
+ "drizzle-orm": "^0.45.3",
55
+ "postgres": "^3.4.9"
56
+ },
57
+ "peerDependenciesMeta": {
58
+ "drizzle-orm": {
59
+ "optional": true
60
+ },
61
+ "postgres": {
62
+ "optional": true
63
+ }
64
+ },
65
+ "devDependencies": {
66
+ "@types/node": "^24.13.6",
67
+ "drizzle-kit": "^0.31.11",
68
+ "drizzle-orm": "^0.45.3",
69
+ "postgres": "^3.4.9",
70
+ "typescript": "^7.0.2",
71
+ "vitest": "^5.0.1"
72
+ }
9
73
  }