@sealkeeper/schema 0.0.1 → 0.4.0
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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +14 -1
- package/dist/agent-id.d.ts +13 -0
- package/dist/agent-id.js +29 -0
- package/dist/agent-name.d.ts +9 -0
- package/dist/agent-name.js +58 -0
- package/dist/api.d.ts +1207 -0
- package/dist/api.js +624 -0
- package/dist/base64url.d.ts +4 -0
- package/dist/base64url.js +43 -0
- package/dist/credential.d.ts +392 -0
- package/dist/credential.js +287 -0
- package/dist/db/agent-renames.d.ts +126 -0
- package/dist/db/agent-renames.js +20 -0
- package/dist/db/agents.d.ts +145 -0
- package/dist/db/agents.js +40 -0
- package/dist/db/client.d.ts +1893 -0
- package/dist/db/client.js +34 -0
- package/dist/db/credentials.d.ts +143 -0
- package/dist/db/credentials.js +16 -0
- package/dist/db/deleted-operators.d.ts +75 -0
- package/dist/db/deleted-operators.js +13 -0
- package/dist/db/events.d.ts +160 -0
- package/dist/db/events.js +34 -0
- package/dist/db/feed-items.d.ts +109 -0
- package/dist/db/feed-items.js +16 -0
- package/dist/db/index.d.ts +56 -0
- package/dist/db/index.js +23 -0
- package/dist/db/migrate.d.ts +1 -0
- package/dist/db/migrate.js +30 -0
- package/dist/db/migrator.d.ts +2 -0
- package/dist/db/migrator.js +20 -0
- package/dist/db/operators.d.ts +109 -0
- package/dist/db/operators.js +11 -0
- package/dist/db/quota-counters.d.ts +109 -0
- package/dist/db/quota-counters.js +17 -0
- package/dist/db/ratings.d.ts +160 -0
- package/dist/db/ratings.js +27 -0
- package/dist/db/scores.d.ts +160 -0
- package/dist/db/scores.js +15 -0
- package/dist/db/standing.d.ts +228 -0
- package/dist/db/standing.js +29 -0
- package/dist/db/task-outcomes.d.ts +126 -0
- package/dist/db/task-outcomes.js +18 -0
- package/dist/db/tasks.d.ts +251 -0
- package/dist/db/tasks.js +49 -0
- package/dist/db/timestamps.d.ts +4 -0
- package/dist/db/timestamps.js +24 -0
- package/dist/db/url.d.ts +12 -0
- package/dist/db/url.js +25 -0
- package/dist/dimensions.d.ts +21 -0
- package/dist/dimensions.js +15 -0
- package/dist/envelope.d.ts +16 -0
- package/dist/envelope.js +70 -0
- package/dist/events.d.ts +153 -0
- package/dist/events.js +98 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/seal-conformance.d.ts +14 -0
- package/dist/seal-conformance.js +176 -0
- package/dist/standing.d.ts +46 -0
- package/dist/standing.js +79 -0
- package/dist/tasks.d.ts +49 -0
- package/dist/tasks.js +102 -0
- package/dist/top-dimensions.d.ts +9 -0
- package/dist/top-dimensions.js +13 -0
- 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>;
|
package/dist/standing.js
ADDED
|
@@ -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
|
+
});
|
package/dist/tasks.d.ts
ADDED
|
@@ -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,102 @@
|
|
|
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
|
+
export function redactJsonSchema(schema) {
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
78
|
+
if (SHAPE_DATA.has(key)) {
|
|
79
|
+
out[key] = Array.isArray(value) ? [...value] : value;
|
|
80
|
+
}
|
|
81
|
+
else if (SHAPE_ONE.has(key)) {
|
|
82
|
+
out[key] = redactSubschema(value);
|
|
83
|
+
}
|
|
84
|
+
else if (key === 'prefixItems' && Array.isArray(value)) {
|
|
85
|
+
out[key] = value.map(redactSubschema);
|
|
86
|
+
}
|
|
87
|
+
else if (key === 'properties' && isJsonObject(value)) {
|
|
88
|
+
out[key] = Object.fromEntries(Object.entries(value).map(([name, sub]) => [
|
|
89
|
+
name,
|
|
90
|
+
redactSubschema(sub),
|
|
91
|
+
]));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
// What GET /v1/tasks and GET /v1/tasks/:id show. Hash and counterparty specs
|
|
97
|
+
// are already safe to show, a sha256 is one way.
|
|
98
|
+
export function publicVerification(spec) {
|
|
99
|
+
if (spec.kind !== 'schema')
|
|
100
|
+
return spec;
|
|
101
|
+
return { kind: 'schema', jsonSchema: redactJsonSchema(spec.jsonSchema) };
|
|
102
|
+
}
|
|
@@ -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
|
|
4
|
-
"
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Zod schemas, event taxonomy and signing helpers for Vouched.",
|
|
5
6
|
"license": "Apache-2.0",
|
|
6
|
-
"homepage": "https://sealkeeper.run",
|
|
7
|
-
"
|
|
8
|
-
|
|
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
|
}
|