@sealkeeper/schema 0.5.0 → 0.5.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.
@@ -5,12 +5,15 @@ export declare const HANDSHAKE_MAX_AGE_SECONDS: number;
5
5
  export declare function handshakeWindowProblem(iat: number, hasNonce: boolean, nowSeconds: number): 'stale' | null;
6
6
  export declare const HANDSHAKE_NONCE_MAX = 64;
7
7
  export declare const HandshakeNonce: z.ZodString;
8
+ export declare const HANDSHAKE_AUD_MAX = 64;
9
+ export declare const HandshakeAudience: z.ZodString;
8
10
  export declare const HANDSHAKE_MAX_CHARS = 2048;
9
11
  export declare const HandshakePayload: z.ZodObject<{
10
12
  sub: z.ZodString;
11
13
  fingerprint: z.ZodString;
12
14
  at: z.ZodInt;
13
15
  nonce: z.ZodOptional<z.ZodString>;
16
+ aud: z.ZodOptional<z.ZodString>;
14
17
  iat: z.ZodInt;
15
18
  }, z.core.$strict>;
16
19
  export type HandshakePayload = z.infer<typeof HandshakePayload>;
@@ -20,6 +23,7 @@ export declare const HandshakeRefusal: z.ZodEnum<{
20
23
  nonce_mismatch: "nonce_mismatch";
21
24
  stale: "stale";
22
25
  wrong_agent: "wrong_agent";
26
+ wrong_verifier: "wrong_verifier";
23
27
  }>;
24
28
  export type HandshakeRefusal = z.infer<typeof HandshakeRefusal>;
25
29
  export declare const HandshakeResult: z.ZodEnum<{
@@ -33,6 +37,13 @@ export declare const HandshakeAgainst: z.ZodEnum<{
33
37
  seal: "seal";
34
38
  }>;
35
39
  export type HandshakeAgainst = z.infer<typeof HandshakeAgainst>;
40
+ export declare const HandshakeProof: z.ZodEnum<{
41
+ nonce: "nonce";
42
+ none: "none";
43
+ verifier: "verifier";
44
+ }>;
45
+ export type HandshakeProof = z.infer<typeof HandshakeProof>;
46
+ export declare const handshakeProof: (nonce: string | undefined, aud: string | undefined) => HandshakeProof;
36
47
  export declare const HANDSHAKE_RECORD_NOTE = "compared with the issuer's current record, the SEAL carries no fingerprint until version 3";
37
48
  export type HandshakeVerification = {
38
49
  ok: true;
@@ -45,12 +56,13 @@ export type HandshakeFingerprint = {
45
56
  hash: string;
46
57
  captured_at: number;
47
58
  };
48
- export declare function signHandshake(privateKey: Uint8Array, agentId: string, fingerprint: HandshakeFingerprint, nowSeconds: number, nonce?: string): Promise<string>;
49
- export declare function verifyHandshake(compact: string, sub: string, nowSeconds: number, nonce?: string): Promise<HandshakeVerification>;
59
+ export declare function signHandshake(privateKey: Uint8Array, agentId: string, fingerprint: HandshakeFingerprint, nowSeconds: number, nonce?: string, aud?: string): Promise<string>;
60
+ export declare function verifyHandshake(compact: string, sub: string, nowSeconds: number, nonce?: string, aud?: string): Promise<HandshakeVerification>;
50
61
  export type HandshakeCheck = {
51
62
  ok: true;
52
63
  result: HandshakeResult;
53
64
  against: HandshakeAgainst;
65
+ proof: HandshakeProof;
54
66
  payload: HandshakePayload;
55
67
  } | {
56
68
  ok: false;
@@ -61,6 +73,7 @@ export type HandshakeCheckOptions = {
61
73
  seal: SealPayload;
62
74
  nowSeconds: number;
63
75
  nonce?: string;
76
+ aud?: string;
64
77
  record: (sub: string) => Promise<string | null>;
65
78
  };
66
79
  export declare function checkHandshake(options: HandshakeCheckOptions): Promise<HandshakeCheck>;
package/dist/handshake.js CHANGED
@@ -15,8 +15,19 @@ import { FINGERPRINT_MAX_CAPTURED_AT, Sha256Base64url } from './fingerprint.js';
15
15
  * anything in the JWS is read. Then the header, whose kid must be the sub,
16
16
  * and the payload, whose sub must be the sub too. That iat is inside its window
17
17
  * (handshakeWindowProblem). That the nonce is the one the verifier asked
18
- * for, when it asked for one. A handshake that fails any check is refused,
19
- * never read as Changed.
18
+ * for, when it asked for one. That aud is the verifier's own name, when it
19
+ * gave one. A handshake that fails any check is refused, never read as
20
+ * Changed.
21
+ *
22
+ * What a handshake that verified shows (HandshakeProof). Without a nonce it
23
+ * shows nothing about who presents it, since a copy in a card replays for
24
+ * as long as the SEAL beside it lives. With the verifier's nonce it shows
25
+ * the key holder signed after the nonce was made, to whoever holds that
26
+ * nonce, so a presenter could have relayed the nonce to the real agent and
27
+ * its answer back. With the nonce and the verifier's own name in aud it
28
+ * shows the key holder answered this verifier, so a handshake relayed from
29
+ * one made for another verifier is refused. A handshake without aud keeps
30
+ * verifying for a verifier that names none.
20
31
  *
21
32
  * checkHandshake then compares the signed fingerprint hash with the one the
22
33
  * SEAL carries, version 3 on. A SEAL before version 3 carries none, so the
@@ -55,18 +66,30 @@ export const HANDSHAKE_NONCE_MAX = 64;
55
66
  export const HandshakeNonce = z
56
67
  .string()
57
68
  .regex(/^[\x20-\x7e]{1,64}$/, 'A nonce is 1 to 64 printable ASCII characters');
69
+ // The verifier a handshake is made for, its aud, is a name the verifier
70
+ // picks and hands the agent with its nonce, such as its URL or its own
71
+ // agent id. The same rule as a nonce, 1 to 64 printable ASCII characters.
72
+ export const HANDSHAKE_AUD_MAX = 64;
73
+ export const HandshakeAudience = z
74
+ .string()
75
+ .regex(/^[\x20-\x7e]{1,64}$/, 'A verifier name is 1 to 64 printable ASCII characters');
58
76
  // A handshake is a few hundred characters. Anything longer is not one.
59
77
  export const HANDSHAKE_MAX_CHARS = 2048;
60
78
  const Seconds = z.int().min(0).max(FINGERPRINT_MAX_CAPTURED_AT);
61
79
  // sub is the agent id and the key that signed. fingerprint is the agent's
62
80
  // fingerprint hash (fingerprintHash) and at when the CLI captured it, Unix
63
- // seconds, never after iat. iat is when the agent signed, Unix seconds.
81
+ // seconds, never after iat. aud is the verifier the handshake is made for,
82
+ // when the agent was given one. iat is when the agent signed, Unix seconds.
83
+ // A verifier on a schema before aud reads a handshake that carries it as
84
+ // malformed, since the payload is closed, so an agent adds aud only for a
85
+ // verifier that asked for it.
64
86
  export const HandshakePayload = z
65
87
  .strictObject({
66
88
  sub: AgentId,
67
89
  fingerprint: Sha256Base64url,
68
90
  at: Seconds,
69
91
  nonce: HandshakeNonce.optional(),
92
+ aud: HandshakeAudience.optional(),
70
93
  iat: Seconds,
71
94
  })
72
95
  .refine((h) => h.at <= h.iat, 'at must not be after iat');
@@ -78,7 +101,8 @@ export const HandshakePayload = z
78
101
  * wrong_agent is a verified handshake whose kid or payload sub is not the
79
102
  * SEAL's sub. stale is an iat outside its window, 300 seconds either way with
80
103
  * a nonce, up to 24 hours old without. nonce_mismatch is a handshake without the nonce the
81
- * verifier asked for.
104
+ * verifier asked for. wrong_verifier is a handshake whose aud is not the
105
+ * name the verifier gave, or that carries none when the verifier gave one.
82
106
  */
83
107
  export const HandshakeRefusal = z.enum([
84
108
  'malformed',
@@ -86,6 +110,7 @@ export const HandshakeRefusal = z.enum([
86
110
  'bad_signature',
87
111
  'stale',
88
112
  'nonce_mismatch',
113
+ 'wrong_verifier',
89
114
  ]);
90
115
  // What the comparison found. matches and changed compare the signed hash
91
116
  // with the SEAL's or the record's. no_fingerprint is a valid handshake with
@@ -95,20 +120,34 @@ export const HandshakeResult = z.enum(['matches', 'changed', 'no_fingerprint']);
95
120
  // What the hash was compared with. seal is the SEAL's own fingerprint,
96
121
  // version 3 on. record is the issuer's current record, for a SEAL before.
97
122
  export const HandshakeAgainst = z.enum(['seal', 'record']);
123
+ /*
124
+ * What a handshake that verified shows about who presents it, see the top
125
+ * of this file. none, the verifier asked for no nonce, so the handshake
126
+ * does not show the presenter holds the key. nonce, the key holder signed
127
+ * over the verifier's nonce, which shows key possession to whoever holds
128
+ * the nonce and no more. verifier, the nonce and the verifier's own name
129
+ * both, so the key holder answered this verifier.
130
+ */
131
+ export const HandshakeProof = z.enum(['none', 'nonce', 'verifier']);
132
+ // The proof of a handshake that verified, from what the verifier asked for.
133
+ // verifyHandshake has already refused one that lacks either.
134
+ export const handshakeProof = (nonce, aud) => nonce === undefined ? 'none' : aud === undefined ? 'nonce' : 'verifier';
98
135
  // The one line a verifier shows when it compared with the record.
99
136
  export const HANDSHAKE_RECORD_NOTE = "compared with the issuer's current record, the SEAL carries no fingerprint until version 3";
100
137
  /*
101
138
  * Signs a handshake for agentId over its fingerprint, at nowSeconds, with
102
- * an optional nonce. The only place a handshake is signed. Throws
103
- * EnvelopeError invalid_input when the payload is not in the shape, such as
104
- * a nonce that is too long or a capture after now.
139
+ * an optional nonce and an optional verifier name, aud. The only place a
140
+ * handshake is signed. Throws EnvelopeError invalid_input when the payload
141
+ * is not in the shape, such as a nonce that is too long or a capture after
142
+ * now.
105
143
  */
106
- export async function signHandshake(privateKey, agentId, fingerprint, nowSeconds, nonce) {
144
+ export async function signHandshake(privateKey, agentId, fingerprint, nowSeconds, nonce, aud) {
107
145
  const parsed = HandshakePayload.safeParse({
108
146
  sub: agentId,
109
147
  fingerprint: fingerprint.hash,
110
148
  at: fingerprint.captured_at,
111
149
  ...(nonce === undefined ? {} : { nonce }),
150
+ ...(aud === undefined ? {} : { aud }),
112
151
  iat: Math.floor(nowSeconds),
113
152
  });
114
153
  if (!parsed.success) {
@@ -119,11 +158,12 @@ export async function signHandshake(privateKey, agentId, fingerprint, nowSeconds
119
158
  /*
120
159
  * Verifies a handshake for the agent sub, the SEAL's sub, at nowSeconds.
121
160
  * nonce is the one the verifier handed the agent, or undefined when it
122
- * asked for none, in which case a nonce in the handshake is ignored.
123
- * Surrounding whitespace is dropped first, as a pasted handshake often has
124
- * it. Verify first, parse second.
161
+ * asked for none, in which case a nonce in the handshake is ignored. aud is
162
+ * the verifier's own name, or undefined when it gives none, in which case
163
+ * an aud in the handshake is ignored. Surrounding whitespace is dropped
164
+ * first, as a pasted handshake often has it. Verify first, parse second.
125
165
  */
126
- export async function verifyHandshake(compact, sub, nowSeconds, nonce) {
166
+ export async function verifyHandshake(compact, sub, nowSeconds, nonce, aud) {
127
167
  const jws = compact.trim();
128
168
  if (jws.length > HANDSHAKE_MAX_CHARS) {
129
169
  return { ok: false, reason: 'malformed' };
@@ -161,6 +201,9 @@ export async function verifyHandshake(compact, sub, nowSeconds, nonce) {
161
201
  if (nonce !== undefined && payload.nonce !== nonce) {
162
202
  return { ok: false, reason: 'nonce_mismatch' };
163
203
  }
204
+ if (aud !== undefined && payload.aud !== aud) {
205
+ return { ok: false, reason: 'wrong_verifier' };
206
+ }
164
207
  return { ok: true, payload };
165
208
  }
166
209
  /*
@@ -171,7 +214,7 @@ export async function verifyHandshake(compact, sub, nowSeconds, nonce) {
171
214
  */
172
215
  export async function checkHandshake(options) {
173
216
  const { seal } = options;
174
- const v = await verifyHandshake(options.handshake, seal.sub, options.nowSeconds, options.nonce);
217
+ const v = await verifyHandshake(options.handshake, seal.sub, options.nowSeconds, options.nonce, options.aud);
175
218
  if (!v.ok)
176
219
  return v;
177
220
  // Version 3 and 4 carry their own fingerprint.
@@ -184,5 +227,11 @@ export async function checkHandshake(options) {
184
227
  : expected === v.payload.fingerprint
185
228
  ? 'matches'
186
229
  : 'changed';
187
- return { ok: true, result, against, payload: v.payload };
230
+ return {
231
+ ok: true,
232
+ result,
233
+ against,
234
+ proof: handshakeProof(options.nonce, options.aud),
235
+ payload: v.payload,
236
+ };
188
237
  }
package/dist/policy.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export declare const POLICY_VERSIONS: {
2
2
  readonly terms: '2026-09-27';
3
- readonly privacy: '2026-10-04';
3
+ readonly privacy: '2026-10-06';
4
4
  };
package/dist/policy.js CHANGED
@@ -6,5 +6,5 @@
6
6
  // here when the matching page changes.
7
7
  export const POLICY_VERSIONS = {
8
8
  terms: '2026-09-27',
9
- privacy: '2026-10-04',
9
+ privacy: '2026-10-06',
10
10
  };
@@ -5,6 +5,7 @@ export type SealConformanceCase = {
5
5
  jws: string;
6
6
  expected: 'valid' | SealBrokenReason;
7
7
  nowSeconds?: number;
8
+ agent?: string;
8
9
  };
9
10
  export type SealConformance = {
10
11
  nowSeconds: number;
@@ -99,6 +99,27 @@ export async function sealConformanceCases() {
99
99
  const altered = `${h}.${base64urlEncode(utf8Encode(JSON.stringify({ ...base, level: 'gold' })))}.${s}`;
100
100
  const cases = [
101
101
  ['a valid SEAL', valid, 'valid'],
102
+ [
103
+ 'a valid SEAL for the agent expected',
104
+ valid,
105
+ 'valid',
106
+ undefined,
107
+ pair.agentId,
108
+ ],
109
+ [
110
+ 'a valid SEAL for another agent than expected',
111
+ valid,
112
+ 'wrong_agent',
113
+ undefined,
114
+ other.agentId,
115
+ ],
116
+ [
117
+ 'an expired SEAL for another agent than expected',
118
+ signed({ ...base, iat: NOW - 25 * HOUR, exp: NOW - HOUR }),
119
+ 'expired',
120
+ undefined,
121
+ other.agentId,
122
+ ],
102
123
  [
103
124
  'iat ahead of now within the clock skew',
104
125
  signed({
@@ -458,11 +479,12 @@ export async function sealConformanceCases() {
458
479
  },
459
480
  ],
460
481
  },
461
- cases: await Promise.all(cases.map(async ([name, jws, expected, nowSeconds]) => ({
482
+ cases: await Promise.all(cases.map(async ([name, jws, expected, nowSeconds, agent]) => ({
462
483
  name,
463
484
  jws: await jws,
464
485
  expected,
465
486
  ...(nowSeconds === undefined ? {} : { nowSeconds }),
487
+ ...(agent === undefined ? {} : { agent }),
466
488
  }))),
467
489
  };
468
490
  }
@@ -18,4 +18,4 @@ export type SealProblem = {
18
18
  payload?: SealPayload;
19
19
  };
20
20
  export type SealVerification = SealVerified | SealProblem;
21
- export declare function verifySeal(keys: readonly SealKey[], compact: string, nowSeconds: number): Promise<SealVerification>;
21
+ export declare function verifySeal(keys: readonly SealKey[], compact: string, nowSeconds: number, agent?: string): Promise<SealVerification>;
@@ -19,11 +19,14 @@ import { decodeHeader, EnvelopeError, verify } from './envelope.js';
19
19
  * 5. exp, then iat, as the standard reads "Check exp is in the future and
20
20
  * iat is not". An iat more than SEAL_CLOCK_SKEW_SECONDS ahead is
21
21
  * not_yet_valid.
22
+ * 6. sub, when the caller names the agent it expected. A valid SEAL for
23
+ * another agent is wrong_agent, so a copied SEAL is refused. Without
24
+ * agent this check is the caller's, as the standard says.
22
25
  *
23
26
  * nowSeconds is the verifier's clock in Unix seconds, passed in so tests
24
27
  * can pin it.
25
28
  */
26
- export async function verifySeal(keys, compact, nowSeconds) {
29
+ export async function verifySeal(keys, compact, nowSeconds, agent) {
27
30
  const jws = compact.trim();
28
31
  let kid;
29
32
  try {
@@ -59,5 +62,8 @@ export async function verifySeal(keys, compact, nowSeconds) {
59
62
  const early = sealIatProblem(payload.iat, nowSeconds);
60
63
  if (early)
61
64
  return { ok: false, reason: early, kid, signed, payload };
65
+ if (agent !== undefined && payload.sub !== agent) {
66
+ return { ok: false, reason: 'wrong_agent', kid, signed, payload };
67
+ }
62
68
  return { ok: true, kid, payload, expiresIn };
63
69
  }
package/dist/standing.js CHANGED
@@ -218,8 +218,8 @@ export const COUNTED_EVIDENCE = {
218
218
  export const POSTER_RESPONSE_HOURS = 48;
219
219
  export const DAY_MS = 86_400_000;
220
220
  // The dormancy ladder of the SEAL standard, section 5, in whole days since
221
- // the agent was last active, its newest accepted event or its newest task
222
- // work the API recorded (lastTaskWork in the API). At quietDays the level stays and the
221
+ // the agent was last active, its newest task work the API recorded
222
+ // (lastTaskWork in the API), never an event (VOU-642). At quietDays the level stays and the
223
223
  // agent is quiet, which the agent answers carry (Standing.quiet, isQuiet)
224
224
  // and the badge and profile show. At dropOneDays and dropTwoDays the level
225
225
  // drops one step each. At noneDays the level is none and the scoring run
@@ -416,7 +416,8 @@ export const OPERATOR_SILVER_CAP = {
416
416
  days: 30,
417
417
  };
418
418
  // Whole days from lastActive to now, never below zero. null when the agent
419
- // has sent no accepted event. The one place dormancy is counted.
419
+ // has no task work, since last active reads task work alone. The one place
420
+ // dormancy is counted.
420
421
  export function dormantDays(lastActive, now) {
421
422
  if (lastActive === null)
422
423
  return null;
@@ -426,9 +427,9 @@ export function dormantDays(lastActive, now) {
426
427
  export const isQuiet = (days) => days !== null && days >= DORMANCY.quietDays;
427
428
  // What the agent answers carry beside level, read from the standing table
428
429
  // for the current version. counts are the raw facts and counted what the
429
- // level read, both with the posted counts (POST-3) beside the SEAL's. last_active is the agent's newest accepted event
430
- // on any version or its newest task work the API recorded, whichever is
431
- // newer, null when it has neither. dormant_days is whole
430
+ // level read, both with the posted counts (POST-3) beside the SEAL's.
431
+ // last_active is the agent's newest task work the API recorded, never an
432
+ // event (VOU-642), null when it has none. dormant_days is whole
432
433
  // days since last_active at the time of the answer, null with it. quiet is
433
434
  // true when dormant_days is at DORMANCY.quietDays or more.
434
435
  export const Standing = z.strictObject({
@@ -8,29 +8,9 @@ export type TemplateSpec = {
8
8
  input: string;
9
9
  output: string;
10
10
  };
11
- export type TemplateDraftVerification = {
12
- kind: 'hash';
13
- } | {
14
- kind: 'schema';
15
- jsonSchema: Record<string, unknown>;
16
- } | {
17
- kind: 'counterparty';
18
- };
19
- export type TemplateDraft = {
20
- taskType: TaskTemplateId;
21
- spec: TemplateSpec;
22
- verification: TemplateDraftVerification;
23
- };
24
- export type TemplateDraw = (lo: number, hi: number) => number;
25
11
  export declare class TemplateInputError extends Error {
26
12
  name: string;
27
13
  }
28
- export type TemplateSolveErrorCode = 'unknown_template' | 'no_server_check' | 'unknown_rule' | 'bad_input';
29
- export declare class TemplateSolveError extends Error {
30
- readonly code: TemplateSolveErrorCode;
31
- name: string;
32
- constructor(code: TemplateSolveErrorCode, message: string);
33
- }
34
14
  export type TaskTemplate = {
35
15
  id: TaskTemplateId;
36
16
  kind: TemplateKind;
@@ -38,11 +18,11 @@ export type TaskTemplate = {
38
18
  category: TaskCategory;
39
19
  size: TaskSize;
40
20
  difficulty: TaskDifficulty;
41
- make(input: string | undefined, draw: TemplateDraw): TemplateDraft;
42
21
  };
43
22
  export declare const TEMPLATE_MAX_INPUT_CHARS = 8000;
44
23
  export declare const SUMMARISE_MIN_INPUT_WORDS = 40;
24
+ export declare const QUESTION_MIN_INPUT_WORDS = 3;
45
25
  export declare const TASK_TEMPLATES: readonly TaskTemplate[];
46
26
  export declare const CONFIRM_CANDIDATE_ONE_IN = 4;
47
27
  export declare function taskTemplateById(id: string): TaskTemplate | undefined;
48
- export declare function solveTemplate(taskType: string, spec: unknown): string;
28
+ export declare function cleanTemplateInput(id: TaskTemplateId, input: string | undefined): string | undefined;