@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.
- package/dist/agent-name.js +5 -1
- package/dist/api.d.ts +143 -2
- package/dist/api.js +103 -23
- package/dist/conformance.d.ts +0 -1
- package/dist/conformance.js +3 -4
- package/dist/credential.d.ts +1 -0
- package/dist/credential.js +8 -3
- package/dist/duel-next.js +4 -1
- package/dist/envelope.d.ts +41 -1
- package/dist/envelope.js +71 -4
- package/dist/game.js +15 -11
- package/dist/handshake-conformance.d.ts +1 -0
- package/dist/handshake-conformance.js +41 -0
- package/dist/handshake.d.ts +15 -2
- package/dist/handshake.js +63 -14
- package/dist/policy.d.ts +1 -1
- package/dist/policy.js +1 -1
- package/dist/seal-conformance.d.ts +1 -0
- package/dist/seal-conformance.js +23 -1
- package/dist/seal-verify.d.ts +1 -1
- package/dist/seal-verify.js +7 -1
- package/dist/standing.js +7 -6
- package/dist/task-templates.d.ts +2 -22
- package/dist/task-templates.js +94 -469
- package/package.json +1 -1
- package/dist/template-conformance.d.ts +0 -10
- package/dist/template-conformance.js +0 -298
package/dist/api.js
CHANGED
|
@@ -9,13 +9,14 @@ import { Jws } from './envelope.js';
|
|
|
9
9
|
import { StoredVersion, Version } from './events.js';
|
|
10
10
|
import { AgentFingerprint, Fingerprint } from './fingerprint.js';
|
|
11
11
|
import { CHALLENGE_TASKS, ChallengeState, DuelOrigin, DuelResult, DuelSeekState, DuelState, GAME, GAME_CAP_MAX, GameBadgeKind, GameCap, IsoWeek, KeeperRank, } from './game.js';
|
|
12
|
-
import { HANDSHAKE_MAX_CHARS, HandshakeAgainst, HandshakeNonce, HandshakeRefusal, HandshakeResult, } from './handshake.js';
|
|
12
|
+
import { HANDSHAKE_MAX_CHARS, HandshakeAgainst, HandshakeAudience, HandshakeNonce, HandshakeProof, HandshakeRefusal, HandshakeResult, } from './handshake.js';
|
|
13
13
|
import { boundedJsonObject } from './json-shape.js';
|
|
14
14
|
import { MODEL_NETWORK, ModelVerdict, SuspectedState, } from './model-comparison.js';
|
|
15
15
|
import { ModelName } from './model-name.js';
|
|
16
16
|
import { OPERATOR_DISPLAY_NAME_MAX, OPERATOR_SLUG_MAX, OperatorDisplayName, OperatorSlug, } from './moderation.js';
|
|
17
17
|
import { Runtime } from './runtime.js';
|
|
18
18
|
import { DAY_MS, Level, Standing, TRUST_SCORE } from './standing.js';
|
|
19
|
+
import { TASK_TEMPLATE_IDS, TEMPLATE_MAX_INPUT_CHARS, } from './task-templates.js';
|
|
19
20
|
import { CheckMethod, Disclosure, PublicVerificationSpec, Sha256Hex, ShownVerificationSpec, STORED_TASK_CATEGORIES, StoredTaskCategory, TASK_CATEGORIES, TASK_DIFFICULTIES, TaskCategory, TaskDifficulty, TaskInputRef, TaskOutcome, TaskOutputShape, TaskSize, TaskState, VerificationSpec, } from './tasks.js';
|
|
20
21
|
export const MAX_EVENTS_PER_BATCH = 500;
|
|
21
22
|
// The window an event's occurred_at must fall in for the API to take it, at
|
|
@@ -25,6 +26,12 @@ export const MAX_EVENTS_PER_BATCH = 500;
|
|
|
25
26
|
// the two cannot drift.
|
|
26
27
|
export const EVENT_MAX_AGE_DAYS = 7;
|
|
27
28
|
export const EVENT_MAX_FUTURE_SKEW_SEC = 300;
|
|
29
|
+
// How long the API keeps an event, in days by its received_at (VOU-641).
|
|
30
|
+
// The scoring window (180 days) plus a margin, so every read of the window
|
|
31
|
+
// finds its events. A sweep on the scoring schedule deletes older ones,
|
|
32
|
+
// except each agent's newest usage event, which names its model
|
|
33
|
+
// (apps/api/src/retention.ts). The privacy page reads it.
|
|
34
|
+
export const EVENT_KEEP_DAYS = 200;
|
|
28
35
|
export const MAX_ENVELOPE_CHARS = 4096;
|
|
29
36
|
export const MAX_TASK_SPEC_BYTES = 16384;
|
|
30
37
|
// Depth and key caps on a spec, checked before its bytes (VOU-214). The top
|
|
@@ -68,7 +75,9 @@ export const SignedTaskRequest = z.strictObject({
|
|
|
68
75
|
// and its agent is unknown. gameEnabled is the operator's game switch at
|
|
69
76
|
// init (VOU-469, D-GAME-2), optional too, and absent means off. Both are
|
|
70
77
|
// read on a new agent only. Registering a key that is already registered
|
|
71
|
-
// changes nothing, runtime and game switch included.
|
|
78
|
+
// changes nothing, runtime and game switch included. issuedAt (VOU-637)
|
|
79
|
+
// bounds how long a captured envelope could be sent again, checked by the
|
|
80
|
+
// API when present. Optional, since CLI 0.5.0 and older send none.
|
|
72
81
|
export const RegisterAgentRequest = z.strictObject({
|
|
73
82
|
publicKey: AgentId,
|
|
74
83
|
githubToken: z.string().min(1).max(256),
|
|
@@ -76,6 +85,7 @@ export const RegisterAgentRequest = z.strictObject({
|
|
|
76
85
|
version: Version,
|
|
77
86
|
runtime: Runtime.optional(),
|
|
78
87
|
gameEnabled: z.boolean().optional(),
|
|
88
|
+
issuedAt: Timestamp.optional(),
|
|
79
89
|
});
|
|
80
90
|
// A name as the API sends it. Every stored name is an AgentName, this stays
|
|
81
91
|
// loose so a client never refuses an answer over a name.
|
|
@@ -325,7 +335,7 @@ export const GameSettingsResponse = z.strictObject({
|
|
|
325
335
|
});
|
|
326
336
|
// GET /v1/game/categories (VOU-474), the categories a duel or a weekly
|
|
327
337
|
// challenge can be in, in the order of TASK_CATEGORIES. A category is
|
|
328
|
-
// duelable when
|
|
338
|
+
// duelable when one of the API's game templates is in it, whose tasks
|
|
329
339
|
// count as seed tasks (GAME_TEMPLATES in apps/api/src/game/tasks.ts).
|
|
330
340
|
// Wrapped in an object, so a later field can be added beside the list.
|
|
331
341
|
export const GameCategoriesResponse = z.strictObject({
|
|
@@ -702,23 +712,49 @@ export const AdoptTaskRequest = z.strictObject({
|
|
|
702
712
|
expiresAt: Timestamp.optional(),
|
|
703
713
|
origin: z.enum(['template', 'routine']).optional(),
|
|
704
714
|
});
|
|
715
|
+
/*
|
|
716
|
+
* A template post (VOU-640), the third payload POST /v1/tasks takes. The
|
|
717
|
+
* agent names a template and, for one that takes it, the operator's input,
|
|
718
|
+
* and the API makes the spec, works out the digest or schema itself and
|
|
719
|
+
* posts it as the agent's own template task, so the solver never ships in
|
|
720
|
+
* the public CLI. The answer to the post carries the task the API made.
|
|
721
|
+
* input is the operator's text as cleanTemplateInput gives it, at most
|
|
722
|
+
* TEMPLATE_MAX_INPUT_CHARS, and the API runs the template's rules on it
|
|
723
|
+
* again. taskId, expiresAt and assignee work as in PostTaskRequest, origin
|
|
724
|
+
* as in AdoptTaskRequest, template when absent. The category, size and
|
|
725
|
+
* difficulty are the template's, so the payload has none of them. A CLI
|
|
726
|
+
* from before it posts the made task as a PostTaskRequest with its own
|
|
727
|
+
* digest, which the API still checks against its solver (refuseUnsolvable).
|
|
728
|
+
*/
|
|
729
|
+
export const TemplatePostRequest = z.strictObject({
|
|
730
|
+
taskId: z.uuid(),
|
|
731
|
+
template: z.enum(TASK_TEMPLATE_IDS),
|
|
732
|
+
input: z.string().min(1).max(TEMPLATE_MAX_INPUT_CHARS).optional(),
|
|
733
|
+
expiresAt: Timestamp.optional(),
|
|
734
|
+
assignee: AgentRef.optional(),
|
|
735
|
+
origin: z.enum(['template', 'routine']).optional(),
|
|
736
|
+
});
|
|
705
737
|
// A task post always has a task type, a spec and a verification, and an
|
|
706
|
-
// adoption
|
|
738
|
+
// adoption and a template post have none of them. A template post names
|
|
739
|
+
// its template.
|
|
707
740
|
const ADOPTION_ABSENT = ['taskType', 'spec', 'verification'];
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
741
|
+
const isObject = (value) => typeof value === 'object' && value !== null;
|
|
742
|
+
const isAdoption = (value) => isObject(value) && ADOPTION_ABSENT.every((key) => !Object.hasOwn(value, key));
|
|
743
|
+
const isTemplatePost = (value) => isAdoption(value) && Object.hasOwn(value, 'template');
|
|
711
744
|
/*
|
|
712
745
|
* The payload POST /v1/tasks parses. One with none of taskType, spec and
|
|
713
|
-
* verification is
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
746
|
+
* verification is a template post when it names a template and parses as
|
|
747
|
+
* TemplatePostRequest, else an adoption and parses as AdoptTaskRequest, and
|
|
748
|
+
* any other parses as PostTaskRequest, so each failure names its own fields
|
|
749
|
+
* rather than a union's. A transform that picks the schema, shown once. The
|
|
750
|
+
* issues of the schema it picked are copied onto this one, paths and all.
|
|
717
751
|
*/
|
|
718
752
|
export const PostTaskPayload = z.unknown().transform((value, ctx) => {
|
|
719
|
-
const parsed =
|
|
720
|
-
?
|
|
721
|
-
:
|
|
753
|
+
const parsed = isTemplatePost(value)
|
|
754
|
+
? TemplatePostRequest.safeParse(value)
|
|
755
|
+
: isAdoption(value)
|
|
756
|
+
? AdoptTaskRequest.safeParse(value)
|
|
757
|
+
: PostTaskRequest.safeParse(value);
|
|
722
758
|
if (parsed.success)
|
|
723
759
|
return parsed.data;
|
|
724
760
|
for (const issue of parsed.error.issues) {
|
|
@@ -848,9 +884,12 @@ export const SubmitTaskRequest = z.strictObject({
|
|
|
848
884
|
// POST /v1/tasks/:id/release, the claimant giving its claim back
|
|
849
885
|
// (VOU-572). Named for its task like claim and submit, checked against the
|
|
850
886
|
// path. A captured envelope cannot end a later claim, since the release
|
|
851
|
-
// bars its agent from the task.
|
|
887
|
+
// bars its agent from the task. issuedAt (VOU-637) bounds how long a
|
|
888
|
+
// captured envelope could be sent again, checked by the API when present.
|
|
889
|
+
// Optional, since CLI 0.5.0 and older send none.
|
|
852
890
|
export const ReleaseTaskRequest = z.strictObject({
|
|
853
891
|
taskId: z.uuid(),
|
|
892
|
+
issuedAt: Timestamp.optional(),
|
|
854
893
|
});
|
|
855
894
|
// Named for its task like claim and submit, checked against the path.
|
|
856
895
|
// origin is the reporting side's, TaskOrigin above.
|
|
@@ -1024,6 +1063,16 @@ export const ListDuelsResponse = z.strictObject({
|
|
|
1024
1063
|
duels: z.array(DuelResponse),
|
|
1025
1064
|
nextCursor: z.string().nullable(),
|
|
1026
1065
|
});
|
|
1066
|
+
// GET /v1/agents/:id/duels and GET /v1/agents/:slug/:name/duels
|
|
1067
|
+
// (VOU-486), the public list of one agent's duels that started, active
|
|
1068
|
+
// and finished, newest first on (created_at, id), paged with the task
|
|
1069
|
+
// list's cursor. An invite, answered or not, is never in it.
|
|
1070
|
+
export const AgentDuelsQuery = z.strictObject({
|
|
1071
|
+
limit: Limit,
|
|
1072
|
+
cursor: TasksCursorParam.optional(),
|
|
1073
|
+
});
|
|
1074
|
+
// Each duel as GET /v1/duels/:id answers it, no side with a taskId.
|
|
1075
|
+
export const AgentDuelsResponse = ListDuelsResponse;
|
|
1027
1076
|
// A seek as its agent sees it. duelId is the duel a match started.
|
|
1028
1077
|
export const DuelSeekView = z.strictObject({
|
|
1029
1078
|
id: z.uuid(),
|
|
@@ -1063,7 +1112,8 @@ export const ChallengeTaskView = z.strictObject({
|
|
|
1063
1112
|
});
|
|
1064
1113
|
// The current week's challenge as its agent sees it. entered says whether
|
|
1065
1114
|
// the agent has an entry, and tasks are its own, empty without one. rank
|
|
1066
|
-
// is its live place, null until it has submitted
|
|
1115
|
+
// is its live place, null until it has submitted and while another entry
|
|
1116
|
+
// of its operator is ahead of it (VOU-644).
|
|
1067
1117
|
export const CurrentChallengeResponse = z.strictObject({
|
|
1068
1118
|
isoWeek: IsoWeek,
|
|
1069
1119
|
category: TaskCategory,
|
|
@@ -1087,8 +1137,9 @@ export const ChallengeBoardRow = z.strictObject({
|
|
|
1087
1137
|
serverMs: z.int().min(0),
|
|
1088
1138
|
});
|
|
1089
1139
|
// The week's board, live while it is open and its final ranks once it
|
|
1090
|
-
// closed.
|
|
1091
|
-
// at most
|
|
1140
|
+
// closed. One row per operator, its best entry (VOU-644). entrants counts
|
|
1141
|
+
// the places, the operators with a ranked entry, at most
|
|
1142
|
+
// GAME.challengeRankMax.
|
|
1092
1143
|
export const ChallengeBoardResponse = z.strictObject({
|
|
1093
1144
|
isoWeek: IsoWeek,
|
|
1094
1145
|
category: TaskCategory,
|
|
@@ -1100,7 +1151,8 @@ export const ChallengeBoardResponse = z.strictObject({
|
|
|
1100
1151
|
// One of the operator's agents with an entry in the week, its rank and its
|
|
1101
1152
|
// tasks as CurrentChallengeResponse shows them to the agent. rank is the
|
|
1102
1153
|
// live place while the week is open and the final one once it closed, null
|
|
1103
|
-
// for an entry with no submission
|
|
1154
|
+
// for an entry with no submission and for one behind another entry of the
|
|
1155
|
+
// same operator, which takes no place (VOU-644).
|
|
1104
1156
|
export const MyChallengeEntry = z.strictObject({
|
|
1105
1157
|
agentId: AgentId,
|
|
1106
1158
|
rank: z.int().min(1).nullable(),
|
|
@@ -2293,6 +2345,12 @@ const feedItemOf = (kind) => z.strictObject({
|
|
|
2293
2345
|
// read, so a renamed agent's old items link to where it lives today.
|
|
2294
2346
|
operator: OperatorRef,
|
|
2295
2347
|
handle: AgentHandle,
|
|
2348
|
+
// The other side of a duel item (duel_invited, duel_started,
|
|
2349
|
+
// duel_finished), the payload's opponent as it is now, looked up when
|
|
2350
|
+
// the item is read like handle and never stored in the payload, so the
|
|
2351
|
+
// web links the opponent's current profile (VOU-486). Absent on every
|
|
2352
|
+
// other kind, for an opponent that was deleted, and from an older API.
|
|
2353
|
+
opponentHandle: AgentHandle.optional(),
|
|
2296
2354
|
// What the agent runs in and the level of its current version, as they
|
|
2297
2355
|
// are now, for the row's name line. Optional so the web reads an older
|
|
2298
2356
|
// API's answer. level is left out until the agent has been scored.
|
|
@@ -2454,7 +2512,8 @@ export const SealCheckResponse = CheckResponse.extend({
|
|
|
2454
2512
|
// GET /v1/agents/:id/seal, its alias /credential and the handle route, when
|
|
2455
2513
|
// the agent's current version has been dormant for 90 days or more and the
|
|
2456
2514
|
// last scoring run marked it no_seal. No SEAL is issued until the next run
|
|
2457
|
-
// after an
|
|
2515
|
+
// after new task work, never after an event alone. dormant_days is counted
|
|
2516
|
+
// at the answer.
|
|
2458
2517
|
export const SealWithheldResponse = z.strictObject({
|
|
2459
2518
|
error: z.strictObject({
|
|
2460
2519
|
code: z.literal('no_seal'),
|
|
@@ -2478,16 +2537,27 @@ export const SealHeldResponse = z.strictObject({
|
|
|
2478
2537
|
// string itself has no tighter limit here. handshake is the agent's signed
|
|
2479
2538
|
// handshake (VB-6, ./handshake.ts), checked beside the SEAL. nonce is the
|
|
2480
2539
|
// one the caller handed the agent out of band, and needs a handshake.
|
|
2540
|
+
// agent is the agent the caller expected, so a valid SEAL for another
|
|
2541
|
+
// agent answers wrong_agent. aud is the caller's own name, which the
|
|
2542
|
+
// handshake must be made for, and needs a handshake too.
|
|
2481
2543
|
export const SealVerifyRequest = z
|
|
2482
2544
|
.strictObject({
|
|
2483
2545
|
seal: z.string(),
|
|
2546
|
+
agent: AgentId.optional(),
|
|
2484
2547
|
handshake: z.string().max(HANDSHAKE_MAX_CHARS).optional(),
|
|
2485
2548
|
nonce: HandshakeNonce.optional(),
|
|
2549
|
+
aud: HandshakeAudience.optional(),
|
|
2486
2550
|
})
|
|
2487
2551
|
.refine((b) => b.nonce === undefined || b.handshake !== undefined, {
|
|
2488
2552
|
message: 'nonce needs a handshake',
|
|
2489
2553
|
path: ['nonce'],
|
|
2554
|
+
})
|
|
2555
|
+
.refine((b) => b.aud === undefined || b.handshake !== undefined, {
|
|
2556
|
+
message: 'aud needs a handshake',
|
|
2557
|
+
path: ['aud'],
|
|
2490
2558
|
});
|
|
2559
|
+
// Why a SEAL is broken, the steps of verifySeal. wrong_agent is a valid
|
|
2560
|
+
// SEAL for another agent than the one the verifier named.
|
|
2491
2561
|
export const SealBrokenReason = z.enum([
|
|
2492
2562
|
'malformed',
|
|
2493
2563
|
'unknown_kid',
|
|
@@ -2496,23 +2566,28 @@ export const SealBrokenReason = z.enum([
|
|
|
2496
2566
|
'unsupported_version',
|
|
2497
2567
|
'expired',
|
|
2498
2568
|
'not_yet_valid',
|
|
2569
|
+
'wrong_agent',
|
|
2499
2570
|
]);
|
|
2500
2571
|
// The handshake beside a valid SEAL. Refused with its reason, or its
|
|
2501
2572
|
// result and what it was compared with, the SEAL's own fingerprint or, for
|
|
2502
2573
|
// a SEAL before version 3, the issuer's current record.
|
|
2574
|
+
// proof says what it shows about who presents it (HandshakeProof).
|
|
2503
2575
|
export const HandshakeAnswer = z.discriminatedUnion('valid', [
|
|
2504
2576
|
z.strictObject({
|
|
2505
2577
|
valid: z.literal(true),
|
|
2506
2578
|
result: HandshakeResult,
|
|
2507
2579
|
against: HandshakeAgainst,
|
|
2580
|
+
proof: HandshakeProof,
|
|
2508
2581
|
}),
|
|
2509
2582
|
z.strictObject({ valid: z.literal(false), reason: HandshakeRefusal }),
|
|
2510
2583
|
]);
|
|
2511
2584
|
// Always 200 for a SEAL, good or broken. expiresIn is whole seconds left.
|
|
2512
|
-
// payload is version 1, 2 or
|
|
2585
|
+
// payload is version 1, 2, 3 or 4. handshake is there only when the request
|
|
2513
2586
|
// carried one and the SEAL is valid. A broken SEAL names no agent to check
|
|
2514
|
-
// it against.
|
|
2515
|
-
|
|
2587
|
+
// it against. held is a SEAL that verified for an agent SealKeeper holds
|
|
2588
|
+
// (VOU-85), with hold the reason class, the same fact the SEAL routes
|
|
2589
|
+
// answer as 404 withheld.
|
|
2590
|
+
export const SealVerifyResponse = z.union([
|
|
2516
2591
|
z.strictObject({
|
|
2517
2592
|
valid: z.literal(true),
|
|
2518
2593
|
payload: SealPayload,
|
|
@@ -2520,6 +2595,11 @@ export const SealVerifyResponse = z.discriminatedUnion('valid', [
|
|
|
2520
2595
|
handshake: HandshakeAnswer.optional(),
|
|
2521
2596
|
}),
|
|
2522
2597
|
z.strictObject({ valid: z.literal(false), reason: SealBrokenReason }),
|
|
2598
|
+
z.strictObject({
|
|
2599
|
+
valid: z.literal(false),
|
|
2600
|
+
reason: z.literal('held'),
|
|
2601
|
+
hold: HoldReason,
|
|
2602
|
+
}),
|
|
2523
2603
|
]);
|
|
2524
2604
|
export const ScoreRunResponse = z.strictObject({
|
|
2525
2605
|
scored: z.int().min(0),
|
package/dist/conformance.d.ts
CHANGED
package/dist/conformance.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// The ./conformance entry. Fixtures other verifiers
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// The ./conformance entry. Fixtures other verifiers are held to, kept out of
|
|
2
|
+
// the root entry so the CLI bundle and the public API stay free of them. The
|
|
3
|
+
// template vectors live in the API with the solver they test (VOU-640).
|
|
4
4
|
export * from './fingerprint-conformance.js';
|
|
5
5
|
export * from './handshake-conformance.js';
|
|
6
6
|
export * from './seal-conformance.js';
|
|
7
|
-
export * from './template-conformance.js';
|
package/dist/credential.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare function acceptedIssuer(iss: unknown, nowSeconds: number): boolea
|
|
|
8
8
|
export declare const WELL_KNOWN_PATH = "/.well-known/seal.json";
|
|
9
9
|
export declare const LEGACY_WELL_KNOWN_PATH = "/.well-known/vouched.json";
|
|
10
10
|
export declare const WELL_KNOWN_URL = "https://sealkeeper.run/.well-known/seal.json";
|
|
11
|
+
export declare const WELL_KNOWN_MAX_AGE_SECONDS = 300;
|
|
11
12
|
export declare const SEAL_EXTENSION_URI = "https://sealkeeper.run/ext/seal/v1";
|
|
12
13
|
export declare const LEGACY_SEAL_EXTENSION_URI = "https://vouched.run/ext/seal/v1";
|
|
13
14
|
export declare const CREDENTIAL_EXTENSION_URI = "https://vouched.run/ext/credential/v1";
|
package/dist/credential.js
CHANGED
|
@@ -31,6 +31,11 @@ export function acceptedIssuer(iss, nowSeconds) {
|
|
|
31
31
|
export const WELL_KNOWN_PATH = '/.well-known/seal.json';
|
|
32
32
|
export const LEGACY_WELL_KNOWN_PATH = '/.well-known/vouched.json';
|
|
33
33
|
export const WELL_KNOWN_URL = `https://${CREDENTIAL_ISSUER}${WELL_KNOWN_PATH}`;
|
|
34
|
+
// How long the keys document may be cached, its Cache-Control max-age in
|
|
35
|
+
// seconds. The API serves it with this and the CLI fetches it again once
|
|
36
|
+
// its copy is older, so a key withdrawn from the list stops verifying
|
|
37
|
+
// within five minutes (VOU-645).
|
|
38
|
+
export const WELL_KNOWN_MAX_AGE_SECONDS = 300;
|
|
34
39
|
// The card extension a SEAL travels under. The two old URIs are kept beside
|
|
35
40
|
// it for one release, VOU-77 drops them.
|
|
36
41
|
export const SEAL_EXTENSION_URI = 'https://sealkeeper.run/ext/seal/v1';
|
|
@@ -213,9 +218,9 @@ const MAX_TTL_MESSAGE = 'exp must be at most 24 hours after iat';
|
|
|
213
218
|
* working. VOU-77 drops it.
|
|
214
219
|
*
|
|
215
220
|
* counts are the standard's eight, over the 180 day window. last_active is
|
|
216
|
-
* Unix seconds of the agent's last activity, its newest
|
|
217
|
-
*
|
|
218
|
-
*
|
|
221
|
+
* Unix seconds of the agent's last activity, its newest task work the API
|
|
222
|
+
* recorded and never an event, null when it has none, and dormant_days
|
|
223
|
+
* whole days since then at issue, null with it.
|
|
219
224
|
*/
|
|
220
225
|
const v1Payload = (iss, scores) => z
|
|
221
226
|
.strictObject({
|
package/dist/duel-next.js
CHANGED
|
@@ -34,7 +34,10 @@ export const DUEL_LIST_MAX = 10;
|
|
|
34
34
|
* routine is true when a routine run sends the call (VOU-598). Nobody is
|
|
35
35
|
* there to say yes, so the route leaves a game that is off as it is and
|
|
36
36
|
* refuses with 403 game_disabled where it would turn it on, and makes no
|
|
37
|
-
* post offer, so the day's offer stays for the user's own run.
|
|
37
|
+
* post offer, so the day's offer stays for the user's own run. With no
|
|
38
|
+
* form it never stops at the invites waiting for a person (VOU-644), so
|
|
39
|
+
* an invite from an operator off the routine's allowlist cannot hold the
|
|
40
|
+
* routine's own seek back. It only
|
|
38
41
|
* narrows what the call may do, so an agent gains nothing by setting it.
|
|
39
42
|
*
|
|
40
43
|
* fingerprint is the agent's, as on a claim, kept with each claim the
|
package/dist/envelope.d.ts
CHANGED
|
@@ -22,9 +22,49 @@ export declare function audienceOf(apiUrl: string): string;
|
|
|
22
22
|
export declare function withAudience<T extends object>(payload: T, aud: string): T & {
|
|
23
23
|
aud: string;
|
|
24
24
|
};
|
|
25
|
-
export declare
|
|
25
|
+
export declare const REQUEST_PURPOSES: readonly ['agent.register', 'agent.update', 'agent.delete', 'agent.status', 'agent.run', 'routine.next', 'challenge.next', 'duel.next', 'task.post', 'task.claim', 'task.submit', 'task.release', 'task.outcome', 'task.submission', 'task.open', 'rating.post', 'game.status', 'game.settings', 'challenge.current', 'challenge.enter', 'duel.seek', 'duel.cancel_seek', 'duel.challenge', 'duel.mine', 'duel.inbox', 'duel.rematch', 'duel.accept', 'duel.decline'];
|
|
26
|
+
export declare const RequestPurpose: z.ZodEnum<{
|
|
27
|
+
"agent.delete": "agent.delete";
|
|
28
|
+
"agent.register": "agent.register";
|
|
29
|
+
"agent.run": "agent.run";
|
|
30
|
+
"agent.status": "agent.status";
|
|
31
|
+
"agent.update": "agent.update";
|
|
32
|
+
"challenge.current": "challenge.current";
|
|
33
|
+
"challenge.enter": "challenge.enter";
|
|
34
|
+
"challenge.next": "challenge.next";
|
|
35
|
+
"duel.accept": "duel.accept";
|
|
36
|
+
"duel.cancel_seek": "duel.cancel_seek";
|
|
37
|
+
"duel.challenge": "duel.challenge";
|
|
38
|
+
"duel.decline": "duel.decline";
|
|
39
|
+
"duel.inbox": "duel.inbox";
|
|
40
|
+
"duel.mine": "duel.mine";
|
|
41
|
+
"duel.next": "duel.next";
|
|
42
|
+
"duel.rematch": "duel.rematch";
|
|
43
|
+
"duel.seek": "duel.seek";
|
|
44
|
+
"game.settings": "game.settings";
|
|
45
|
+
"game.status": "game.status";
|
|
46
|
+
"rating.post": "rating.post";
|
|
47
|
+
"routine.next": "routine.next";
|
|
48
|
+
"task.claim": "task.claim";
|
|
49
|
+
"task.open": "task.open";
|
|
50
|
+
"task.outcome": "task.outcome";
|
|
51
|
+
"task.post": "task.post";
|
|
52
|
+
"task.release": "task.release";
|
|
53
|
+
"task.submission": "task.submission";
|
|
54
|
+
"task.submit": "task.submit";
|
|
55
|
+
}>;
|
|
56
|
+
export type RequestPurpose = z.infer<typeof RequestPurpose>;
|
|
57
|
+
export declare function withPurpose<T extends object>(payload: T, purpose: RequestPurpose): T & {
|
|
58
|
+
purpose: RequestPurpose;
|
|
59
|
+
};
|
|
60
|
+
export declare function signRequest(payload: object, privateKey: Uint8Array, kid: string, aud: string, purpose?: RequestPurpose): Promise<string>;
|
|
26
61
|
export type AudienceCheck = {
|
|
27
62
|
result: 'match' | 'missing' | 'wrong';
|
|
28
63
|
payload: unknown;
|
|
29
64
|
};
|
|
30
65
|
export declare function readAudience(payload: unknown, accepted: readonly string[]): AudienceCheck;
|
|
66
|
+
export type PurposeCheck = {
|
|
67
|
+
result: 'match' | 'missing' | 'wrong';
|
|
68
|
+
payload: unknown;
|
|
69
|
+
};
|
|
70
|
+
export declare function readPurpose(payload: unknown, expected: RequestPurpose): PurposeCheck;
|
package/dist/envelope.js
CHANGED
|
@@ -123,10 +123,63 @@ export function withAudience(payload, aud) {
|
|
|
123
123
|
}
|
|
124
124
|
return { ...payload, aud };
|
|
125
125
|
}
|
|
126
|
-
//
|
|
127
|
-
// the
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
// Signed request purpose (VOU-637). Every agent-signed request also names
|
|
127
|
+
// the one route it is for with `purpose`, a top level key inside the signed
|
|
128
|
+
// payload beside `aud`, so an envelope signed for one route is refused on
|
|
129
|
+
// every other. Without it a status envelope, payload { issuedAt }, would
|
|
130
|
+
// pass the strict schema of the delete route too. The list is fixed here and
|
|
131
|
+
// read by the CLI and the API alike. Events, the sync's fingerprint
|
|
132
|
+
// declaration, handshakes and SEALs carry no purpose.
|
|
133
|
+
export const REQUEST_PURPOSES = [
|
|
134
|
+
'agent.register',
|
|
135
|
+
'agent.update',
|
|
136
|
+
'agent.delete',
|
|
137
|
+
'agent.status',
|
|
138
|
+
'agent.run',
|
|
139
|
+
'routine.next',
|
|
140
|
+
'challenge.next',
|
|
141
|
+
'duel.next',
|
|
142
|
+
'task.post',
|
|
143
|
+
'task.claim',
|
|
144
|
+
'task.submit',
|
|
145
|
+
'task.release',
|
|
146
|
+
'task.outcome',
|
|
147
|
+
'task.submission',
|
|
148
|
+
'task.open',
|
|
149
|
+
'rating.post',
|
|
150
|
+
'game.status',
|
|
151
|
+
'game.settings',
|
|
152
|
+
'challenge.current',
|
|
153
|
+
'challenge.enter',
|
|
154
|
+
'duel.seek',
|
|
155
|
+
'duel.cancel_seek',
|
|
156
|
+
'duel.challenge',
|
|
157
|
+
'duel.mine',
|
|
158
|
+
'duel.inbox',
|
|
159
|
+
'duel.rematch',
|
|
160
|
+
'duel.accept',
|
|
161
|
+
'duel.decline',
|
|
162
|
+
];
|
|
163
|
+
export const RequestPurpose = z.enum(REQUEST_PURPOSES);
|
|
164
|
+
// A copy of the payload with `purpose` added. Never mutates the input.
|
|
165
|
+
export function withPurpose(payload, purpose) {
|
|
166
|
+
if (!RequestPurpose.safeParse(purpose).success) {
|
|
167
|
+
throw new EnvelopeError('invalid_input', 'purpose is not a known request');
|
|
168
|
+
}
|
|
169
|
+
if (Array.isArray(payload)) {
|
|
170
|
+
throw new EnvelopeError('invalid_input', 'A signed request payload must be an object');
|
|
171
|
+
}
|
|
172
|
+
if (Object.hasOwn(payload, 'purpose')) {
|
|
173
|
+
throw new EnvelopeError('invalid_input', 'Payload already has a purpose');
|
|
174
|
+
}
|
|
175
|
+
return { ...payload, purpose };
|
|
176
|
+
}
|
|
177
|
+
// How every agent-signed request is signed. The payload gains `aud` and,
|
|
178
|
+
// for a request to a route, `purpose` inside the signed bytes. Events go
|
|
179
|
+
// through here with no purpose. `sign` stays for SEALs, which carry `iss`.
|
|
180
|
+
export async function signRequest(payload, privateKey, kid, aud, purpose) {
|
|
181
|
+
const bound = withAudience(payload, aud);
|
|
182
|
+
return sign(purpose === undefined ? bound : withPurpose(bound, purpose), privateKey, kid);
|
|
130
183
|
}
|
|
131
184
|
// Read `aud` off a verified payload. Call only after `verify`. The compare
|
|
132
185
|
// is exact, the received value is never normalised. A payload that is not a
|
|
@@ -143,3 +196,17 @@ export function readAudience(payload, accepted) {
|
|
|
143
196
|
const match = typeof aud === 'string' && accepted.includes(aud);
|
|
144
197
|
return { result: match ? 'match' : 'wrong', payload: rest };
|
|
145
198
|
}
|
|
199
|
+
// Read `purpose` off a verified payload, after `readAudience`. The compare
|
|
200
|
+
// is exact against the one purpose the route expects. A payload that is not
|
|
201
|
+
// a plain object comes back unchanged as missing, so the request schema
|
|
202
|
+
// still rejects it.
|
|
203
|
+
export function readPurpose(payload, expected) {
|
|
204
|
+
if (typeof payload !== 'object' ||
|
|
205
|
+
payload === null ||
|
|
206
|
+
Array.isArray(payload) ||
|
|
207
|
+
!Object.hasOwn(payload, 'purpose')) {
|
|
208
|
+
return { result: 'missing', payload };
|
|
209
|
+
}
|
|
210
|
+
const { purpose, ...rest } = payload;
|
|
211
|
+
return { result: purpose === expected ? 'match' : 'wrong', payload: rest };
|
|
212
|
+
}
|
package/dist/game.js
CHANGED
|
@@ -182,23 +182,27 @@ export const GAME = {
|
|
|
182
182
|
// an entry's correct count never rewards a guess after a wrong answer,
|
|
183
183
|
// and a wrong one ends the claim as the failed submit cap ends one.
|
|
184
184
|
challengeSubmits: 1,
|
|
185
|
-
//
|
|
186
|
-
// and
|
|
187
|
-
// whole. TRUST_SCORE.rankMax bounds a
|
|
185
|
+
// A live rank and the live board read at most this many entries of the
|
|
186
|
+
// week in rank order, and an entry past them has no place, so a read
|
|
187
|
+
// never walks a week's entrants whole. TRUST_SCORE.rankMax bounds a
|
|
188
|
+
// Trust rank the same way.
|
|
188
189
|
challengeRankMax: 10_000,
|
|
189
|
-
// The top places of a weekly challenge (VOU-479). A
|
|
190
|
-
//
|
|
191
|
-
//
|
|
190
|
+
// The top places of a weekly challenge (VOU-479). A place is one
|
|
191
|
+
// operator's best entry (VOU-644). A submit that first takes an entry
|
|
192
|
+
// into them writes a challenge_top10 item, once per operator and week,
|
|
193
|
+
// and at the close they earn the top_10 badge.
|
|
192
194
|
challengeTopPlaces: 10,
|
|
193
|
-
// The
|
|
194
|
-
//
|
|
195
|
-
//
|
|
195
|
+
// The places a week needs at its close for its top places to earn
|
|
196
|
+
// top_10, and for its first place to earn winner, so a near empty week
|
|
197
|
+
// hands out no badge for showing up. A place is one operator's, so these
|
|
198
|
+
// count distinct operators with a ranked entry (VOU-644), and one
|
|
199
|
+
// operator's agents never meet them alone.
|
|
196
200
|
challengeTopEntrants: 20,
|
|
197
201
|
challengeWinnerEntrants: 5,
|
|
198
202
|
// The correct answers an entry needs for winner, top_10 and the
|
|
199
203
|
// challenge_top10 item (D-GAME-11). Any submit ranks an entry, so
|
|
200
|
-
// without it
|
|
201
|
-
//
|
|
204
|
+
// without it five operators' agents with a wrong answer each would fill
|
|
205
|
+
// a week and one of them would win on no correct answer.
|
|
202
206
|
challengePlaceCorrect: 1,
|
|
203
207
|
// The places the challenge_closed item names.
|
|
204
208
|
challengePodium: 3,
|
|
@@ -12,6 +12,9 @@ const HOUR = 3600;
|
|
|
12
12
|
const FINGERPRINT = 'unFXnGMKWNOP4f_NnbearJn0prLlxoGia2LaZcqnzy8';
|
|
13
13
|
const OTHER_FINGERPRINT = 'BHAfx6dALmCdt3aXz-g6iAjLLGDurK95DZm15ZjIVZg';
|
|
14
14
|
const NONCE = 'check-7f3a';
|
|
15
|
+
// The verifier the handshake is made for, and another.
|
|
16
|
+
const AUD = 'https://verifier.example';
|
|
17
|
+
const OTHER_AUD = 'https://relay.example';
|
|
15
18
|
const header = (value) => base64urlEncode(utf8Encode(JSON.stringify(value)));
|
|
16
19
|
// Signs the header and payload text as they are, so a case can carry a
|
|
17
20
|
// header or payload sign() would never write.
|
|
@@ -178,6 +181,43 @@ export async function handshakeConformanceCases() {
|
|
|
178
181
|
'nonce_mismatch',
|
|
179
182
|
{ nonce: 'check-7f3a' },
|
|
180
183
|
],
|
|
184
|
+
// aud binds a handshake to the verifier that asked for it, so one made
|
|
185
|
+
// for another verifier and relayed is refused.
|
|
186
|
+
[
|
|
187
|
+
'made for the verifier that asked',
|
|
188
|
+
byAgent({ ...base, nonce: NONCE, aud: AUD }),
|
|
189
|
+
'matches',
|
|
190
|
+
{ nonce: NONCE, aud: AUD },
|
|
191
|
+
],
|
|
192
|
+
[
|
|
193
|
+
'made for another verifier and relayed',
|
|
194
|
+
byAgent({ ...base, nonce: NONCE, aud: OTHER_AUD }),
|
|
195
|
+
'wrong_verifier',
|
|
196
|
+
{ nonce: NONCE, aud: AUD },
|
|
197
|
+
],
|
|
198
|
+
[
|
|
199
|
+
'no aud when the verifier gave its name',
|
|
200
|
+
byAgent({ ...base, nonce: NONCE }),
|
|
201
|
+
'wrong_verifier',
|
|
202
|
+
{ nonce: NONCE, aud: AUD },
|
|
203
|
+
],
|
|
204
|
+
[
|
|
205
|
+
'an aud the verifier did not ask for',
|
|
206
|
+
byAgent({ ...base, nonce: NONCE, aud: AUD }),
|
|
207
|
+
'matches',
|
|
208
|
+
{ nonce: NONCE },
|
|
209
|
+
],
|
|
210
|
+
[
|
|
211
|
+
'a wrong nonce is named before a wrong verifier',
|
|
212
|
+
byAgent({ ...base, nonce: 'check-0000', aud: OTHER_AUD }),
|
|
213
|
+
'nonce_mismatch',
|
|
214
|
+
{ nonce: NONCE, aud: AUD },
|
|
215
|
+
],
|
|
216
|
+
[
|
|
217
|
+
'a 65 character aud',
|
|
218
|
+
byAgent({ ...base, aud: 'a'.repeat(65) }),
|
|
219
|
+
'malformed',
|
|
220
|
+
],
|
|
181
221
|
// Verified with the SEAL's sub key before the header is read, so a
|
|
182
222
|
// handshake another agent signed for itself fails the signature.
|
|
183
223
|
[
|
|
@@ -268,6 +308,7 @@ export async function handshakeConformanceCases() {
|
|
|
268
308
|
jws: await jws,
|
|
269
309
|
expected,
|
|
270
310
|
...(opts?.nonce === undefined ? {} : { nonce: opts.nonce }),
|
|
311
|
+
...(opts?.aud === undefined ? {} : { aud: opts.aud }),
|
|
271
312
|
...(opts?.nowSeconds === undefined
|
|
272
313
|
? {}
|
|
273
314
|
: { nowSeconds: opts.nowSeconds }),
|