@byollm/protocol 0.1.0-alpha.5 → 0.1.0-alpha.51

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/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { z as z2 } from "zod";
3
3
 
4
4
  // src/backends.ts
5
+ import { isIP } from "net";
5
6
  import { z } from "zod";
6
7
  var BackendClass = z.enum(["http", "process"]);
7
8
  var BackendCost = z.enum(["free", "metered", "subscription"]);
@@ -162,6 +163,24 @@ var BACKENDS = Object.freeze({
162
163
  class: "process",
163
164
  cost: "subscription",
164
165
  adversarialCorpus: "process"
166
+ }),
167
+ /**
168
+ * OpenAI's Codex CLI, on a ChatGPT plan — byollm_016 stage 3.
169
+ *
170
+ * `subscription`, so `SUBSCRIPTION_SELF_LOCK` pins it to its owner's own
171
+ * work whatever the config says. That is load-bearing here in a way it is
172
+ * not for `claude-cli`: Codex is an *agent*, and its default feature set
173
+ * includes a shell tool, browser control and computer use. The daemon
174
+ * disables every one of them, verified against the shipped binary rather
175
+ * than assumed — see `codex-cli.ts` — but the self-lock is the floor under
176
+ * that verification rather than a duplicate of it.
177
+ */
178
+ "codex-cli": backend({
179
+ id: "codex-cli",
180
+ label: "Codex CLI (your ChatGPT plan)",
181
+ class: "process",
182
+ cost: "subscription",
183
+ adversarialCorpus: "process"
165
184
  })
166
185
  });
167
186
  var BACKEND_IDS = Object.freeze(Object.keys(BACKENDS));
@@ -177,17 +196,24 @@ function backendDescriptor(id) {
177
196
  function isLocalHost(hostname) {
178
197
  const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
179
198
  if (host === "localhost" || host.endsWith(".localhost")) return true;
180
- if (host === "::1") return true;
199
+ const version = isIP(host);
200
+ if (version === 0) return false;
201
+ if (version === 6) {
202
+ if (host === "::1") return true;
203
+ return /^f[cd]/.test(host);
204
+ }
181
205
  if (host.startsWith("127.")) return true;
182
206
  if (host.startsWith("10.")) return true;
183
207
  if (host.startsWith("192.168.")) return true;
184
- if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return true;
185
- if (/^f[cd]/.test(host)) return true;
186
- return false;
208
+ return /^172\.(1[6-9]|2\d|3[01])\./.test(host);
209
+ }
210
+ function isCloudTaggedModel(model) {
211
+ return /:[^:]*cloud$/.test(model);
187
212
  }
188
- function resolveCost(id, baseUrl) {
213
+ function resolveCost(id, baseUrl, model) {
189
214
  const declared = BACKENDS[id].cost;
190
215
  if (declared !== null) return declared;
216
+ if (model !== void 0 && isCloudTaggedModel(model)) return "metered";
191
217
  if (baseUrl === void 0) return "metered";
192
218
  try {
193
219
  return isLocalHost(new URL(baseUrl).hostname) ? "free" : "metered";
@@ -197,8 +223,8 @@ function resolveCost(id, baseUrl) {
197
223
  }
198
224
 
199
225
  // src/audience.ts
200
- var Audience = z2.enum(["self", "named", "public"]);
201
- var OfferScope = z2.enum(["self", "named", "public"]);
226
+ var Audience = z2.enum(["private", "team", "public"]);
227
+ var OfferScope = z2.enum(["private", "team", "public"]);
202
228
  var AUDIENCES = Object.freeze(Audience.options);
203
229
  var OFFER_SCOPES = Object.freeze(OfferScope.options);
204
230
  var MatchRefusal = z2.enum([
@@ -222,16 +248,16 @@ var MatchRefusal = z2.enum([
222
248
  var ALLOWED = Object.freeze({ ok: true });
223
249
  var refuse = (refusal) => Object.freeze({ ok: false, refusal });
224
250
  function effectiveOfferScope(configured, cost, spend) {
225
- if (cost === "subscription") return "self";
226
- if (cost === "metered" && spend?.acknowledged !== true) return "self";
251
+ if (cost === "subscription") return "private";
252
+ if (cost === "metered" && spend?.acknowledged !== true) return "private";
227
253
  return configured;
228
254
  }
229
255
  function matchAudience(job, daemon) {
230
256
  const sameOwner = job.owner === daemon.owner;
231
- if (job.audience === "self" && !sameOwner) {
257
+ if (job.audience === "private" && !sameOwner) {
232
258
  return refuse("audience-self-other-owner");
233
259
  }
234
- if (job.audience === "named" && !sameOwner && job.audienceAllow !== void 0 && !job.audienceAllow.includes(daemon.owner)) {
260
+ if (job.audience === "team" && !sameOwner && job.audienceAllow !== void 0 && !job.audienceAllow.includes(daemon.owner)) {
235
261
  return refuse("not-in-server-allowlist");
236
262
  }
237
263
  const scope = effectiveOfferScope(
@@ -254,19 +280,19 @@ function matchAudience(job, daemon) {
254
280
  }
255
281
  }
256
282
  switch (scope) {
257
- case "self":
283
+ case "private":
258
284
  return refuse("offer-scope-too-narrow");
259
- case "named":
285
+ case "team":
260
286
  return daemon.locallyAllows(job.owner) ? ALLOWED : refuse("not-locally-allowed");
261
287
  case "public":
262
288
  return ALLOWED;
263
289
  }
264
290
  }
265
291
  var REFUSAL_MESSAGES = Object.freeze({
266
- "no-capability": "no backend on this machine is configured and healthy for that job kind",
267
- "audience-self-other-owner": "the job is private to its owner and this machine is paired to someone else",
268
- "not-locally-allowed": "the job's owner is not on this machine's allowlist (byollm allow <server> <user>)",
269
- "not-in-server-allowlist": "the app restricted this job to named runners and this machine is not one of them",
292
+ "no-capability": "no backend on this device is configured and healthy for that job kind",
293
+ "audience-self-other-owner": "the job is private to its owner and this device is paired to someone else",
294
+ "not-locally-allowed": "the job's owner is not on this device's allowlist (byollm allow <server> <user>)",
295
+ "not-in-server-allowlist": "the app restricted this job to named runners and this device is not one of them",
270
296
  "offer-scope-too-narrow": "this backend is offered to its owner only (byollm offer <backend> named|public to widen)",
271
297
  "subscription-self-lock": "subscription-backed models run their owner's work only \u2014 this is a protocol rule, not a setting",
272
298
  "metered-no-spend-consent": "this backend bills its owner per token, and they have not agreed to spend it on other people's work",
@@ -290,11 +316,21 @@ var ChatMessage = z3.object({
290
316
  var GeneratePayload = z3.object({
291
317
  prompt: z3.string().min(1).max(PAYLOAD_LIMITS.maxTextChars),
292
318
  system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
293
- }).strict();
319
+ }).strict().refine(
320
+ (payload) => payload.prompt.length + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
321
+ {
322
+ message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
323
+ }
324
+ );
294
325
  var ChatPayload = z3.object({
295
326
  messages: z3.array(ChatMessage).min(1).max(PAYLOAD_LIMITS.maxMessages),
296
327
  system: z3.string().max(PAYLOAD_LIMITS.maxTextChars).optional()
297
- }).strict();
328
+ }).strict().refine(
329
+ (payload) => payload.messages.reduce((sum, m) => sum + m.content.length, 0) + (payload.system?.length ?? 0) <= PAYLOAD_LIMITS.maxTotalChars,
330
+ {
331
+ message: `payload exceeds ${String(PAYLOAD_LIMITS.maxTotalChars)} characters`
332
+ }
333
+ );
298
334
  var JobKind = z3.enum(["llm.generate", "llm.chat"]);
299
335
  var JOB_KINDS = Object.freeze(JobKind.options);
300
336
  var KindedPayload = z3.discriminatedUnion("kind", [
@@ -378,8 +414,33 @@ var ClaimedJob = z4.object({
378
414
  audience: Audience,
379
415
  /** The app's id for the user who enqueued it. */
380
416
  owner: z4.string().min(1),
381
- /** Runner owners the app restricted a `named` job to, if any. */
382
- audienceAllow: z4.array(z4.string().min(1)).optional(),
417
+ /**
418
+ * Which site's job — V1-3.
419
+ *
420
+ * The stub has always carried it; the opened job did not, so everything
421
+ * downstream of the payload — the ingress line above all — recorded a job
422
+ * id that belongs to a site without saying which. Two sites can choose
423
+ * the same id, and the meter is the product.
424
+ *
425
+ * Optional so a caller assembling a job by hand is not forced to invent
426
+ * one, and so this reads as what it is: a fact about where the work came
427
+ * from, not a second copy of the routing key.
428
+ */
429
+ site: z4.string().min(1).optional(),
430
+ /**
431
+ * Which service the site named, if it named one — byollm_016 Phase B.
432
+ *
433
+ * The same shape `site` documents above, and added for the same reason:
434
+ * the stub carries it, the opened job did not, and everything downstream
435
+ * of the payload lost it. Here that loss was not cosmetic — the daemon
436
+ * picks the backend from this, so a job that selected a non-default
437
+ * service would have been served by the default instead, which is the
438
+ * substitution `NO_PAYLOAD_ROUTING` forbids.
439
+ *
440
+ * Optional, because absent means "the owner's default" and that is every
441
+ * job written before selection existed.
442
+ */
443
+ service: z4.string().min(1).optional(),
383
444
  lease: Lease
384
445
  }).strict();
385
446
  var ResultProvenance = z4.object({
@@ -406,9 +467,16 @@ function provenanceFor(input) {
406
467
  runnerOwner: input.runnerOwner,
407
468
  backendClass: input.backendClass,
408
469
  model: input.model,
409
- untrusted: input.audience !== "self"
470
+ untrusted: input.audience !== "private"
410
471
  };
411
472
  }
473
+ var RunMetadata = z4.object({
474
+ /** Which model actually served it. */
475
+ model: z4.string().min(1),
476
+ backendClass: BackendClass,
477
+ /** Wall-clock milliseconds the backend call took. */
478
+ durationMs: z4.number().int().nonnegative()
479
+ }).strict();
412
480
  var JobResultOk = z4.object({
413
481
  outcome: z4.literal("ok"),
414
482
  text: z4.string(),
@@ -430,11 +498,88 @@ var JobOutcome = z4.discriminatedUnion("outcome", [
430
498
  JobResultError,
431
499
  JobResultCanceled
432
500
  ]);
501
+ var RefusalReason = z4.enum([
502
+ /**
503
+ * A selection this requester cannot be served — byollm_016 Phase B.
504
+ *
505
+ * **One value for two causes, and the collapse is the security property.**
506
+ * A named service may be unknown to this owner, or known and not offered to
507
+ * this requester. Those are different facts and the requester may learn
508
+ * neither, because telling them apart turns refusal wording into an
509
+ * inventory oracle: probe names, sort the answers, and enumerate a device
510
+ * you were never offered. The finer cause lives owner-side, where the person
511
+ * reading it already owns the machine — see {@link SelectionFailure}.
512
+ *
513
+ * The first draft of this enum had both causes on the wire with a comment
514
+ * claiming they disclosed identically. They did not; the comment described a
515
+ * property the code lacked, which is the more dangerous half of that mistake.
516
+ */
517
+ "select-unavailable",
518
+ /**
519
+ * Two or more services answer this kind and the owner has named no default,
520
+ * so the kind is withheld. Nobody may pick on the owner's behalf — the wrong
521
+ * guess is the metered one.
522
+ *
523
+ * Not collapsed into the value above, and the reason is that a kind is not
524
+ * probeable. There are two kinds; a requester asking about one is not
525
+ * enumerating a namespace, and learns nothing they could not learn by
526
+ * looking at what the device advertises. It is also already what a roster
527
+ * member sees on the devices page — `awaitingDefault` carries exactly this,
528
+ * by kind, for exactly this reason.
529
+ */
530
+ "default-ambiguity",
531
+ /**
532
+ * A default exists and this requester can never use it — byollm_016's
533
+ * defaults-meet-audiences corner.
534
+ *
535
+ * The specimen: an owner's default for `llm.chat` is their Claude
536
+ * subscription, self-locked by `SUBSCRIPTION_SELF_LOCK`. A team member's
537
+ * unselected job resolves to it and can never be served by it. That must be
538
+ * a refusal on the spot, not a wait that expires an hour later looking like
539
+ * nobody was online.
540
+ *
541
+ * Bounded like the value above and probeable for the same reason it is not:
542
+ * the requester named nothing, so there is no name space to walk.
543
+ */
544
+ "default-unusable"
545
+ ]);
546
+ var JobRefused = z4.object({
547
+ outcome: z4.literal("refused"),
548
+ reason: RefusalReason,
549
+ /** Plain words for a human reading a log, never parsed. */
550
+ message: z4.string().min(1)
551
+ }).strict();
552
+ var REFUSAL_TEXT = Object.freeze({
553
+ "select-unavailable": "that service is not available to you on this device",
554
+ "default-ambiguity": "this device serves that kind from more than one service and its owner has not chosen which",
555
+ "default-unusable": "this device's default for that kind cannot run work for you"
556
+ });
557
+ var REFUSED_SELECTION = Object.freeze({
558
+ outcome: "refused",
559
+ reason: "select-unavailable",
560
+ message: REFUSAL_TEXT["select-unavailable"]
561
+ });
562
+ var SealedOutcome = z4.object({ outcome: JobOutcome, ran: RunMetadata }).strict();
433
563
  var DeliveredResult = z4.object({
434
564
  jobId: z4.string().min(1),
435
565
  state: JobState,
436
566
  outcome: JobOutcome.optional(),
437
- provenance: ResultProvenance.optional()
567
+ provenance: ResultProvenance.optional(),
568
+ /**
569
+ * Present, and always `true`, when this did not come from a runner —
570
+ * {@link MUSTS.FALLBACK_LABELED}.
571
+ *
572
+ * The app's own `onNoRunner` value produced it: a hosted model, a cached
573
+ * answer, an apology. It never travels on the wire, because nothing on
574
+ * the wire produced it; it exists so that a result which did not come
575
+ * from the user's own compute cannot be reported as though it did.
576
+ *
577
+ * A literal rather than a boolean, so `fallback: false` is not a
578
+ * spelling anybody can reach for. The absence of this field means a
579
+ * runner ran the job, and the *server* stamps it — an app cannot supply
580
+ * a substitute that hides what it is.
581
+ */
582
+ fallback: z4.literal(true).optional()
438
583
  }).strict();
439
584
  var SizeClass = z4.enum(["small", "medium", "large", "unbounded"]);
440
585
  var SIZE_CLASS_LIMITS = Object.freeze({
@@ -456,8 +601,75 @@ var JobStub = z4.object({
456
601
  kind: JobKind,
457
602
  /** The app's id for the user who enqueued it. */
458
603
  owner: z4.string().min(1),
604
+ /**
605
+ * Which site this job belongs to — byollm_009 Amendment A §A.3.
606
+ *
607
+ * **The site's identity key id**, not an id somebody assigned it. §6 has
608
+ * listed `site` since this spec was frozen; the schema never carried it,
609
+ * which is the drift the amendment closes.
610
+ *
611
+ * A key id rather than an opaque handle for one reason above the others:
612
+ * it makes the stub *self-describing* instead of a pointer into somebody
613
+ * else's table. A daemon holds this key id already, from pinning, so it
614
+ * can check `stub.site` against the payload envelope's `senderKeyId`
615
+ * without a lookup and without trusting the party that routed it. An
616
+ * opaque id can only be believed.
617
+ *
618
+ * It also avoids inventing a second namespace for a thing that has a
619
+ * canonical one — the shape of finding 41 (two owner namespaces compared
620
+ * for equality) and of finding fourteen before it.
621
+ *
622
+ * Rotation is a designed transition rather than a cost: a site publishes a
623
+ * new identity signed by the outgoing one, both are valid through an
624
+ * overlap window, and a daemon re-keys its own map by verifying that
625
+ * signature against the key it already pinned (§A.3.1).
626
+ */
627
+ site: z4.string().min(1),
459
628
  audience: Audience,
460
- audienceAllow: z4.array(z4.string().min(1)).optional(),
629
+ // `audienceAllow` is **not** here, and its absence is the enforcement —
630
+ // cloud_008 §0.2.
631
+ //
632
+ // It was a list of the people who may run a job, travelling to every
633
+ // routing party on every `named` job. byollm_001 Rev 1 §B settled who
634
+ // decides that long before this schema existed: *the daemon's own list
635
+ // decides, not the server's*, and `allowlist.predicateFor(origin)` is the
636
+ // enforcement in both lanes. So this was a second answer to a question the
637
+ // daemon already owned — able only to agree, in which case it was
638
+ // redundant, or to disagree, in which case nothing said which wins.
639
+ //
640
+ // The rule it leaves behind, which decides the next field too: **a class
641
+ // the router acts on may travel; membership never does.** `audience` stays
642
+ // for exactly that reason — the relay narrows on it. A roster does not
643
+ // travel, so `ROSTER_NOT_DISCLOSED` holds here by absence, which is the
644
+ // strongest way for a MUST to hold.
645
+ //
646
+ // The site keeps its own copy on `JobRecord` and still filters candidates
647
+ // with it before offering. That is server-internal, where the party
648
+ // holding the list authored it.
649
+ /**
650
+ * Which of the owner's services should answer — byollm_016 Phase B.
651
+ *
652
+ * **A selection from a menu, never a demand.** The owner advertises named
653
+ * services; a site may name one of them, and that is the entire power the
654
+ * field grants. It carries no model, no base URL, no flags — the daemon
655
+ * resolves the name against its own config and nothing else, so what
656
+ * actually runs is still decided exclusively by the person who owns the
657
+ * hardware. A name that is not on that owner's menu is refused
658
+ * (`select-unadvertised`), never silently substituted, because a
659
+ * substitution is how "select" would quietly become "whatever we had".
660
+ *
661
+ * Absent means "the owner's default for this kind", which is the only
662
+ * behaviour Phase A had.
663
+ *
664
+ * It travels because the router matches on it, under the rule the absent
665
+ * `audienceAllow` above establishes: *a class the router acts on may
666
+ * travel; membership never does.* This is a class.
667
+ *
668
+ * It is a **stub** field and never a payload field, which is the line
669
+ * `NO_PAYLOAD_ROUTING` draws: the prompt cannot reach it, so no amount of
670
+ * user text can influence what runs.
671
+ */
672
+ service: z4.string().min(1).optional(),
461
673
  sizeClass: SizeClass,
462
674
  /** Reserved for byollm_006. False until streaming exists. */
463
675
  streaming: z4.boolean(),
@@ -748,6 +960,21 @@ function signRequest(keys, input) {
748
960
  signature: signWith(keys, canonicalRequest(input))
749
961
  };
750
962
  }
963
+ function signSiteRequest(keys, input) {
964
+ return signRequest(keys, {
965
+ endpoint: siteEndpoint(input.endpoint),
966
+ runnerId: input.siteId,
967
+ issuedAt: input.issuedAt,
968
+ body: input.body
969
+ });
970
+ }
971
+ function verifySiteRequest(input) {
972
+ return verifyRequest({
973
+ ...input,
974
+ endpoint: siteEndpoint(input.endpoint)
975
+ });
976
+ }
977
+ var siteEndpoint = (endpoint) => `site/${endpoint}`;
751
978
  function verifyRequest(input) {
752
979
  const skew = input.maxSkewMs ?? MAX_CLOCK_SKEW_MS;
753
980
  if (Math.abs(input.now - input.signature.issuedAt) > skew) return "stale";
@@ -764,7 +991,69 @@ function verifyRequest(input) {
764
991
  return ok ? null : "bad-signature";
765
992
  }
766
993
 
994
+ // src/succession.ts
995
+ import { z as z8 } from "zod";
996
+ var SUCCESSION_CONTEXT = "byollm/v1/site-succession";
997
+ var RETIREMENT_WINDOW_MS = 7 * 24 * 60 * 60 * 1e3;
998
+ var MAX_SUCCESSION_CHAIN = 64;
999
+ var Succession = z8.object({
1000
+ /**
1001
+ * The predecessor's public identity — K1, in full.
1002
+ *
1003
+ * The whole identity rather than the key id, because a daemon meeting a
1004
+ * chain it has not seen before has to *verify* each link, and a key id is
1005
+ * a fingerprint: enough to compare, never enough to check a signature.
1006
+ */
1007
+ identity: PublicIdentity,
1008
+ /** K1's signature over the statement naming K1 and its successor. */
1009
+ signature: z8.string().min(1)
1010
+ }).strict();
1011
+ function successionStatement(fromKeyId, toKeyId) {
1012
+ return Buffer.from(`${SUCCESSION_CONTEXT}:${fromKeyId}:${toKeyId}`);
1013
+ }
1014
+ function signSuccession(previous, next) {
1015
+ return {
1016
+ identity: {
1017
+ identity: previous.identityPublic,
1018
+ encryption: previous.encryptionPublic,
1019
+ encryptionSig: previous.encryptionSig
1020
+ },
1021
+ signature: signWith(
1022
+ previous,
1023
+ successionStatement(keyId(previous.identityPublic), keyId(next.identity))
1024
+ )
1025
+ };
1026
+ }
1027
+ function verifyLink(link, toKeyId) {
1028
+ if (!verifyPublicIdentity(link.identity)) return false;
1029
+ return verifyWith(
1030
+ link.identity.identity,
1031
+ successionStatement(keyId(link.identity.identity), toKeyId),
1032
+ link.signature
1033
+ );
1034
+ }
1035
+ function walkSuccession(input) {
1036
+ const { current, chain, approved } = input;
1037
+ if (chain.length === 0) return { path: [current], failure: "no-chain" };
1038
+ if (chain.length > MAX_SUCCESSION_CHAIN)
1039
+ return { path: [current], failure: "too-long" };
1040
+ const steps = [...chain].reverse();
1041
+ const path = [current];
1042
+ let succeeding = current;
1043
+ for (const link of steps) {
1044
+ if (!verifyLink(link, succeeding)) return { path, failure: "broken-link" };
1045
+ const previous = keyId(link.identity.identity);
1046
+ path.unshift(previous);
1047
+ if (approved(previous)) return { path, from: previous };
1048
+ succeeding = previous;
1049
+ }
1050
+ return { path, failure: "unknown-origin" };
1051
+ }
1052
+
767
1053
  // src/musts.ts
1054
+ function kindsOf(must2) {
1055
+ return typeof must2.verifiedBy === "string" ? [must2.verifiedBy] : must2.verifiedBy;
1056
+ }
768
1057
  var must = (m) => Object.freeze(m);
769
1058
  var MUSTS = Object.freeze({
770
1059
  // ---- Pairing and identity -------------------------------------------
@@ -797,6 +1086,48 @@ var MUSTS = Object.freeze({
797
1086
  verifiedBy: "conformance",
798
1087
  source: "byollm_009 \xA74"
799
1088
  }),
1089
+ SITE_KEY_BY_STUB: must({
1090
+ id: "SITE_KEY_BY_STUB",
1091
+ statement: "A daemon MUST verify a job's payload against the pinned key of the site the stub names, and MUST refuse a job naming a site it has not pinned. It MUST NOT fall back to another pinned key, and MUST refuse an envelope whose declared sender disagrees with the stub's site.",
1092
+ enforcedBy: "daemon",
1093
+ // Adversarial, and the reason is the finding that produced it: the
1094
+ // honest paths pass with every site check deleted, because `open`
1095
+ // refuses a signature from the wrong key anyway. What distinguishes an
1096
+ // enforced rule from a coincidence here is a hostile pairing of stub and
1097
+ // envelope, which no conformance client would ever send.
1098
+ verifiedBy: "adversarial",
1099
+ source: "byollm_009 \xA7A.3"
1100
+ }),
1101
+ SITES_LOCALLY_APPROVED: must({
1102
+ id: "SITES_LOCALLY_APPROVED",
1103
+ statement: "A daemon MUST NOT run work for a site it has not approved on the machine itself. An upstream may propose a site set; a site the daemon has never approved MUST be offered to its owner and served nothing until they approve it. A key that has changed for an already-approved id MUST be refused for the life of the pairing, including after that id has left the set and returned. A **verified succession** is not a changed key: a new key id carrying a signature, by a key this daemon has already approved, over a statement naming both key ids MUST be accepted without a new local approval \u2014 provided the control plane projects the same successor \u2014 and MUST be announced rather than applied silently.",
1104
+ enforcedBy: "daemon",
1105
+ // Two kinds, and the second is the one that matters — V1-1.
1106
+ //
1107
+ // `construction`: the daemon cannot serve a site that is not in its
1108
+ // pinned map, and admission refuses before a payload is fetched, so the
1109
+ // ordinary path cannot reach a site nobody approved.
1110
+ //
1111
+ // `adversarial`: the property that survives is about a *sequence* —
1112
+ // remove the id, re-offer it under a different key — which no honest
1113
+ // upstream sends and which the fence above does not see. That was the
1114
+ // bypass: the pin was deleted with the id, so the comparison had nothing
1115
+ // to compare against and the substitution arrived as a stranger.
1116
+ // **Not `conformance`, and that is a live gap rather than a judgement.**
1117
+ // Amendment C's succession clause is a rule about two implementations
1118
+ // agreeing, which is what a conformance check is for — but rotating a
1119
+ // site's key is not something `ConformanceTarget` can express, and adding
1120
+ // an optional hook that most targets omit would produce a check reporting
1121
+ // success for a reason unrelated to the property it claims. That is this
1122
+ // project's most-repeated bug, and it is not worth reintroducing for a
1123
+ // stronger-sounding word in a table. The rotation path is verified by
1124
+ // `site-rotation.test.ts` (both directions, against the shipped runner)
1125
+ // and `relay/test/rotation.test.ts` (both planes, against the reference
1126
+ // relay); the missing piece is a second *independent* implementation to
1127
+ // check them against, and there is not one yet.
1128
+ verifiedBy: ["construction", "adversarial"],
1129
+ source: "byollm_009 \xA7B.2, Amendment C"
1130
+ }),
800
1131
  KEYS_EXCHANGED_AT_CONSENT: must({
801
1132
  id: "KEYS_EXCHANGED_AT_CONSENT",
802
1133
  statement: "Pairing MUST exchange both parties' public identities; each side MUST verify that the encryption key is signed by the identity presenting it, and MUST pin the identity. Keys MUST NOT be delivered before approval.",
@@ -984,12 +1315,12 @@ var MUSTS = Object.freeze({
984
1315
  verifiedBy: "conformance",
985
1316
  source: "byollm_001 \xA7Endpoints.4"
986
1317
  }),
987
- RESULT_PROVENANCE: must({
988
- id: "RESULT_PROVENANCE",
989
- statement: "A result from a non-'self' job MUST carry its provenance (audience and runner) to the delivery seam so an app never treats volunteer output as first-party.",
1318
+ PROVENANCE_NAMES_DEVICE: must({
1319
+ id: "PROVENANCE_NAMES_DEVICE",
1320
+ statement: "A result MUST carry the claiming device's key id and its relationship to the requester, to the delivery seam, so an app never treats volunteer output as first-party. The key id MUST be the device the upstream granted the lease to, and a result whose signature does not verify against that device MUST be refused rather than recorded.",
990
1321
  enforcedBy: "server",
991
1322
  verifiedBy: "conformance",
992
- source: "byollm_003 Rev 1 \xA7Return-trip"
1323
+ source: "byollm_009 \xA711"
993
1324
  }),
994
1325
  // ---- The trust surface -------------------------------------------------
995
1326
  INGRESS_LOGGED_BEFORE_EXECUTION: must({
@@ -1007,12 +1338,33 @@ var MUSTS = Object.freeze({
1007
1338
  verifiedBy: "adversarial",
1008
1339
  source: "byollm_004 \xA72"
1009
1340
  }),
1341
+ /**
1342
+ * Amended for byollm_016 Phase B, and the amendment is deliberately narrow.
1343
+ *
1344
+ * A site may now name a **service** on the stub. The temptation is to read
1345
+ * that as a crack in this law, so the statement below says exactly where the
1346
+ * line is: a name selects from a menu the owner published, and resolves to a
1347
+ * model, backend, base URL and flags **only** through that owner's own
1348
+ * config. The site supplies a key; the owner supplies every value it maps
1349
+ * to. A name the owner does not advertise is refused rather than
1350
+ * substituted, because substitution is how "you may pick from my list" turns
1351
+ * into "you may ask for anything and get something".
1352
+ *
1353
+ * Two properties keep it from drifting into "sites demand models":
1354
+ *
1355
+ * 1. **Nothing the site sends is ever a value.** No model string, no URL,
1356
+ * no flag crosses the wire — only a key that means nothing off this
1357
+ * owner's machine.
1358
+ * 2. **It is a stub field, never a payload field.** The prompt cannot
1359
+ * reach it. That is unchanged and is the sentence the second clause
1360
+ * below still enforces verbatim.
1361
+ */
1010
1362
  NO_PAYLOAD_ROUTING: must({
1011
1363
  id: "NO_PAYLOAD_ROUTING",
1012
- statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them.",
1364
+ statement: "Model, backend, base URL, and flags MUST come from owner config only; a payload MUST NOT influence any of them. A stub MAY name a service the owner advertises, which selects among that owner's own config entries and MUST NOT introduce any value the owner did not write; an unadvertised name MUST be refused, never substituted.",
1013
1365
  enforcedBy: "daemon",
1014
1366
  verifiedBy: "adversarial",
1015
- source: "byollm_004 \xA72"
1367
+ source: "byollm_004 \xA72, amended byollm_016 \xA7Phase B"
1016
1368
  }),
1017
1369
  STRIPPED_CHILD_ENV: must({
1018
1370
  id: "STRIPPED_CHILD_ENV",
@@ -1041,26 +1393,117 @@ var MUSTS = Object.freeze({
1041
1393
  enforcedBy: "daemon",
1042
1394
  verifiedBy: "adversarial",
1043
1395
  source: "byollm_004 \xA74"
1396
+ }),
1397
+ REVOCATION_IMMEDIATE: must({
1398
+ id: "REVOCATION_IMMEDIATE",
1399
+ statement: "Revocation MUST take effect at the upstream at once \u2014 a revoked runner MUST NOT be granted further work from the moment the record changes \u2014 and MUST reach the daemon by its next heartbeat.",
1400
+ // Both, and stated as one sentence with two obligations rather than
1401
+ // folded into REVOCATION_HONORED. That one binds the *daemon*: a revoked
1402
+ // daemon stops claiming and abandons in-flight work. This binds the
1403
+ // *upstream*. byollm_009 §5 is explicit that the pair is the point — "a
1404
+ // revocation enforced at one end survives a compromise of that end" — and
1405
+ // one entry covering both would make a compromised daemon look compliant.
1406
+ enforcedBy: "both",
1407
+ verifiedBy: "conformance",
1408
+ source: "byollm_009 \xA711"
1409
+ }),
1410
+ CONSENT_BEFORE_ROUTE: must({
1411
+ id: "CONSENT_BEFORE_ROUTE",
1412
+ statement: "An upstream MUST NOT route a job to a device without a record binding that user, that site and that scope. There MUST be no discovery path by which a device receives work it was never granted.",
1413
+ enforcedBy: "server",
1414
+ verifiedBy: "conformance",
1415
+ source: "byollm_009 \xA711"
1416
+ }),
1417
+ ROSTER_NOT_DISCLOSED: must({
1418
+ id: "ROSTER_NOT_DISCLOSED",
1419
+ statement: "A site MUST NOT learn the membership of a group whose compute it uses, and MUST NOT publish membership to a routing party. No wire message may carry a list of who may run a job.",
1420
+ // Checkable since cloud_008 §0.2 took `audienceAllow` off the stub: the
1421
+ // property now holds by *absence*, and absence is exactly what a strict
1422
+ // schema and a serialised stub can be asked about. Before that it was a
1423
+ // sentence — and one this project cited in code comments, tests and two
1424
+ // specs as though it were enforced data, which is why it is worth
1425
+ // stating precisely rather than generously.
1426
+ enforcedBy: "both",
1427
+ verifiedBy: "conformance",
1428
+ source: "byollm_009 \xA711"
1429
+ }),
1430
+ EFFECTIVE_OFFER_ONLY: must({
1431
+ id: "EFFECTIVE_OFFER_ONLY",
1432
+ statement: "A daemon MUST declare effective offers only. An upstream MUST NOT receive raw config, allowlists, or capacity the owner has not shared, and MUST act on the declared offer rather than on what was asked for.",
1433
+ enforcedBy: "both",
1434
+ verifiedBy: "conformance",
1435
+ source: "byollm_009 \xA711"
1436
+ }),
1437
+ FALLBACK_LABELED: must({
1438
+ id: "FALLBACK_LABELED",
1439
+ statement: "Work served by anything other than the user's own compute MUST be labelled as such wherever it is reported, and MUST NOT be silently substituted.",
1440
+ // `construction` today, and deliberately not `conformance`. Nothing on
1441
+ // the wire yet distinguishes a fallback from any other community job —
1442
+ // the ledger that would give it a surface is unbuilt — so a check would
1443
+ // have to assert something it cannot observe. Promoted the day that
1444
+ // surface exists. Marking it `conformance` now would put "verified"
1445
+ // beside a property no third party can see, which is the one thing the
1446
+ // kinds exist to prevent.
1447
+ enforcedBy: "both",
1448
+ verifiedBy: "construction",
1449
+ source: "byollm_009 \xA711"
1450
+ }),
1451
+ RELAY_BLIND: must({
1452
+ id: "RELAY_BLIND",
1453
+ statement: "A relay MUST NOT hold any key capable of decrypting a payload, a result, or a delta frame.",
1454
+ // Operator: a third party can read the relay's types and see there is
1455
+ // nowhere to put such a key, but the kit certifies a *server* and cannot
1456
+ // reach inside somebody's deployment to prove what it holds.
1457
+ enforcedBy: "server",
1458
+ verifiedBy: "operator",
1459
+ source: "byollm_009 \xA711"
1460
+ }),
1461
+ SHARED_COMPUTE_DISCLOSED: must({
1462
+ id: "SHARED_COMPUTE_DISCLOSED",
1463
+ statement: "Before a user's work first runs on compute they do not own, they MUST be told in plain language that the machine's owner can see it.",
1464
+ // Operator, and cloud_008 §0.3 is why the classification now comes with a
1465
+ // standing answer rather than a standing question. The screen is not
1466
+ // wire-observable, but the *string the server composes* is, and it is
1467
+ // now unit-tested with the two false sentences forbidden by name. The
1468
+ // kind stays `operator` because a third-party site can still render
1469
+ // whatever it likes; what changed is that the part inside our own
1470
+ // boundary stopped depending on somebody remembering to audit it.
1471
+ enforcedBy: "server",
1472
+ verifiedBy: "operator",
1473
+ source: "byollm_009 \xA711"
1044
1474
  })
1045
1475
  });
1476
+ var RETIRED_MUSTS = Object.freeze({
1477
+ RESULT_PROVENANCE: {
1478
+ supersededBy: "PROVENANCE_NAMES_DEVICE",
1479
+ note: "Strengthened, not renamed: attribution is now by proof of possession \u2014 the result's signature must verify against the device the upstream granted the lease to \u2014 rather than by a provenance label travelling beside it. byollm_009 \xA711 states the stronger form."
1480
+ }
1481
+ });
1046
1482
  var MUST_IDS = Object.freeze(Object.keys(MUSTS));
1047
1483
  function mustsVerifiedBy(kind) {
1048
- return MUST_IDS.filter((id) => MUSTS[id].verifiedBy === kind);
1484
+ return MUST_IDS.filter((id) => kindsOf(MUSTS[id]).includes(kind));
1049
1485
  }
1050
1486
 
1051
1487
  // src/wire.ts
1052
- import { z as z8 } from "zod";
1488
+ import { z as z9 } from "zod";
1053
1489
  var PROTOCOL_VERSION = "0";
1054
1490
  var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze([
1055
1491
  PROTOCOL_VERSION
1056
1492
  ]);
1057
1493
  var MIN_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0] ?? PROTOCOL_VERSION;
1494
+ function declaredVersion(input) {
1495
+ const { body, query } = input;
1496
+ if (typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion")) {
1497
+ return body.protocolVersion;
1498
+ }
1499
+ return query?.get("protocolVersion") ?? void 0;
1500
+ }
1058
1501
  function checkProtocolVersion(body) {
1059
1502
  const declared = typeof body === "object" && body !== null && Object.hasOwn(body, "protocolVersion") ? body.protocolVersion : void 0;
1060
1503
  if (typeof declared !== "string" || declared.length === 0) {
1061
1504
  return {
1062
1505
  error: "unsupported-protocol-version",
1063
- message: "this request declared no protocol version. Upgrade the daemon: `npm i -g byollm@alpha`.",
1506
+ message: `this request declared no protocol version. Upgrade the daemon: \`${UPGRADE_COMMAND}\`.`,
1064
1507
  supported: SUPPORTED_PROTOCOL_VERSIONS,
1065
1508
  minimum: MIN_PROTOCOL_VERSION
1066
1509
  };
@@ -1068,13 +1511,14 @@ function checkProtocolVersion(body) {
1068
1511
  if (!SUPPORTED_PROTOCOL_VERSIONS.includes(declared)) {
1069
1512
  return {
1070
1513
  error: "unsupported-protocol-version",
1071
- message: `this server speaks protocol ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")} and the daemon asked for ${declared}. ` + (declared < MIN_PROTOCOL_VERSION ? "Upgrade the daemon: `npm i -g byollm@alpha`." : "This daemon is newer than the server; the server needs upgrading."),
1514
+ message: `this server speaks protocol ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")} and the daemon asked for ${declared}. ` + (declared < MIN_PROTOCOL_VERSION ? `Upgrade the daemon: \`${UPGRADE_COMMAND}\`.` : "This daemon is newer than the server; the server needs upgrading."),
1072
1515
  supported: SUPPORTED_PROTOCOL_VERSIONS,
1073
1516
  minimum: MIN_PROTOCOL_VERSION
1074
1517
  };
1075
1518
  }
1076
1519
  return null;
1077
1520
  }
1521
+ var UPGRADE_COMMAND = "npm i -g byollm@latest";
1078
1522
  var PROTOCOL_PREFIX = "/byollm";
1079
1523
  var ENDPOINTS = Object.freeze([
1080
1524
  "pair",
@@ -1084,22 +1528,46 @@ var ENDPOINTS = Object.freeze([
1084
1528
  "result",
1085
1529
  "release"
1086
1530
  ]);
1087
- var Capability = z8.object({
1531
+ var Capability = z9.object({
1088
1532
  kind: JobKind,
1533
+ /**
1534
+ * The owner's name for the service answering this kind — byollm_016.
1535
+ *
1536
+ * A device advertises *which* of its services serves a kind, not merely
1537
+ * that something does. Phase B lets a job select by this name; until then
1538
+ * it is what a device page shows and what a default is chosen between.
1539
+ */
1540
+ service: z9.string().min(1),
1541
+ /**
1542
+ * Whether this row is the default for its kind.
1543
+ *
1544
+ * Stated rather than inferred from being the only row, which is true in
1545
+ * Phase A and stops being true the moment Phase B advertises every
1546
+ * selectable service per kind. A consumer that learned "default means
1547
+ * alone" would have to unlearn it, and the ones that did not would be
1548
+ * quietly wrong. One field now, no second shape later.
1549
+ */
1550
+ isDefault: z9.boolean(),
1089
1551
  backendId: BackendIdSchema,
1090
1552
  backendClass: BackendClass,
1091
- model: z8.string().min(1),
1553
+ model: z9.string().min(1),
1092
1554
  offerScope: OfferScope
1093
1555
  }).strict();
1094
- var CapabilityMatrix = z8.array(Capability);
1095
- var PairStartRequest = z8.object({
1096
- protocolVersion: z8.literal(PROTOCOL_VERSION),
1097
- action: z8.literal("start"),
1098
- daemon: z8.object({
1099
- version: z8.string().min(1),
1556
+ var CapabilityMatrix = z9.array(Capability);
1557
+ var WithheldKind = z9.object({
1558
+ kind: JobKind,
1559
+ claimants: z9.array(
1560
+ z9.object({ service: z9.string().min(1), offer: OfferScope }).strict()
1561
+ ).min(2)
1562
+ }).strict();
1563
+ var PairStartRequest = z9.object({
1564
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1565
+ action: z9.literal("start"),
1566
+ daemon: z9.object({
1567
+ version: z9.string().min(1),
1100
1568
  /** Shown in the app's runner list so a user can tell their machines apart. */
1101
- label: z8.string().min(1).max(120),
1102
- platform: z8.enum(["darwin", "linux", "win32"])
1569
+ label: z9.string().min(1).max(120),
1570
+ platform: z9.enum(["darwin", "linux", "win32"])
1103
1571
  }),
1104
1572
  /**
1105
1573
  * This machine's public keys (byollm_009 §5).
@@ -1111,111 +1579,259 @@ var PairStartRequest = z8.object({
1111
1579
  device: PublicIdentity,
1112
1580
  capabilities: CapabilityMatrix
1113
1581
  }).strict();
1114
- var PairStartResponse = z8.object({
1582
+ var PairStartResponse = z9.object({
1115
1583
  /** Secret the daemon polls with. Never shown to the user. */
1116
- deviceCode: z8.string().min(20),
1584
+ deviceCode: z9.string().min(20),
1117
1585
  /** Short code the user reads and confirms in the browser. */
1118
- userCode: z8.string().min(4).max(16),
1586
+ userCode: z9.string().min(4).max(16),
1119
1587
  /** Where the user approves. Must be on the server's own origin. */
1120
- verificationUrl: z8.url(),
1588
+ verificationUrl: z9.url(),
1121
1589
  /** Epoch ms after which the code is dead ({@link MUSTS.PAIR_CODE_EXPIRES}). */
1122
- expiresAt: z8.number().int().positive(),
1590
+ expiresAt: z9.number().int().positive(),
1123
1591
  /** How often the daemon may poll. */
1124
- pollIntervalMs: z8.number().int().min(500).max(6e4)
1592
+ pollIntervalMs: z9.number().int().min(500).max(6e4)
1125
1593
  }).strict();
1126
- var PairPollRequest = z8.object({
1127
- protocolVersion: z8.literal(PROTOCOL_VERSION),
1128
- action: z8.literal("poll"),
1129
- deviceCode: z8.string().min(20)
1594
+ var PairPollRequest = z9.object({
1595
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1596
+ action: z9.literal("poll"),
1597
+ deviceCode: z9.string().min(20)
1130
1598
  }).strict();
1131
- var PairPollResponse = z8.discriminatedUnion("status", [
1132
- z8.object({ status: z8.literal("pending") }).strict(),
1133
- z8.object({ status: z8.literal("denied") }).strict(),
1134
- z8.object({ status: z8.literal("expired") }).strict(),
1135
- z8.object({
1136
- status: z8.literal("approved"),
1137
- /** Bearer token for every later call. Scoped to exactly one user. */
1138
- runnerToken: z8.string().min(20),
1139
- runnerId: z8.string().min(1),
1599
+ var PairPollResponse = z9.discriminatedUnion("status", [
1600
+ z9.object({ status: z9.literal("pending") }).strict(),
1601
+ z9.object({ status: z9.literal("denied") }).strict(),
1602
+ z9.object({ status: z9.literal("expired") }).strict(),
1603
+ z9.object({
1604
+ status: z9.literal("approved"),
1605
+ // `runnerToken` is gone cloud_008 §2.4, finding 37.
1606
+ //
1607
+ // It was minted here, hashed into `RunnerRecord.tokenHash`, written to
1608
+ // the daemon's pairings file, and then **never sent, never looked up
1609
+ // and never compared**. `getRunnerByTokenHash` existed on both stores
1610
+ // and was called by nothing but a test asserting it returns null.
1611
+ //
1612
+ // Not merely dead wire, which is what `audienceAllow` and
1613
+ // `HeartbeatResponse.leases` were. This was a *secret*: minted,
1614
+ // transmitted, and written to two disks at rest, for nothing. A
1615
+ // credential with no purpose is a liability rather than clutter,
1616
+ // because the only thing it can ever do is leak.
1617
+ //
1618
+ // `REQUESTS_SIGNED_NOT_BEARER` was already the rule and was already
1619
+ // enforced — every authenticated call is signed by the device's pinned
1620
+ // identity key. This removes the thing the MUST is named after.
1621
+ runnerId: z9.string().min(1),
1140
1622
  /** The app's id for the approving user — this daemon's owner forever. */
1141
- owner: z8.string().min(1),
1623
+ owner: z9.string().min(1),
1142
1624
  /** Display name for the trust UI, if the app offers one. */
1143
- ownerLabel: z8.string().optional(),
1625
+ ownerLabel: z9.string().optional(),
1144
1626
  /**
1145
- * The site's public keys, for the daemon to pin (byollm_009 §5).
1627
+ * The sites this pairing covers, for the daemon to pin (byollm_009 §5),
1628
+ * keyed by each site's identity key id — cloud_009 §5.
1146
1629
  *
1147
- * Returned only on approval a pending or denied poll learns nothing,
1630
+ * Returned only on approval: a pending or denied poll learns nothing,
1148
1631
  * so an unapproved code cannot be used to enumerate a site's keys.
1632
+ *
1633
+ * **One pairing per upstream, not one per site.** A user who connects a
1634
+ * site on a web dashboard has no reason to go back to a laptop and run
1635
+ * a command, so which sites a pairing covers is a projection of consent
1636
+ * — refreshed on the heartbeat — rather than something frozen at
1637
+ * pairing. A direct site answers with exactly one entry, which is the
1638
+ * same shape and not a special case.
1639
+ *
1640
+ * Keyed by the id `stub.site` carries (Amendment A §A.3), so the
1641
+ * runner's lookup is a map read rather than a join across two
1642
+ * namespaces.
1149
1643
  */
1150
- site: PublicIdentity
1644
+ sites: z9.record(z9.string().min(1), PublicIdentity)
1151
1645
  }).strict()
1152
1646
  ]);
1153
- var PairRequest = z8.discriminatedUnion("action", [
1647
+ var PairRequest = z9.discriminatedUnion("action", [
1154
1648
  PairStartRequest,
1155
1649
  PairPollRequest
1156
1650
  ]);
1157
- var ClaimRequest = z8.object({
1158
- protocolVersion: z8.literal(PROTOCOL_VERSION),
1159
- runnerId: z8.string().min(1),
1651
+ var ClaimRequest = z9.object({
1652
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1653
+ runnerId: z9.string().min(1),
1160
1654
  /** Re-sent on every claim so a server never matches against a stale matrix. */
1161
1655
  capabilities: CapabilityMatrix,
1162
1656
  /** Upper bound on jobs to return; the server may return fewer. */
1163
- max: z8.number().int().min(1).max(64)
1657
+ max: z9.number().int().min(1).max(64)
1164
1658
  }).strict();
1165
- var ClaimResponse = z8.object({
1659
+ var ClaimResponse = z9.object({
1166
1660
  /**
1167
1661
  * Stubs, not jobs. The payload arrives from `fetch`, sealed to whichever
1168
1662
  * device claimed — see {@link JobStub} for the exhaustive metadata list.
1169
1663
  */
1170
- jobs: z8.array(ClaimedStub),
1664
+ jobs: z9.array(ClaimedStub),
1171
1665
  /** Lease duration granted, so the daemon knows its renewal deadline. */
1172
- leaseMs: z8.number().int().positive()
1666
+ leaseMs: z9.number().int().positive()
1173
1667
  }).strict();
1174
- var HeartbeatRequest = z8.object({
1175
- protocolVersion: z8.literal(PROTOCOL_VERSION),
1176
- runnerId: z8.string().min(1),
1177
- daemonVersion: z8.string().min(1),
1668
+ var HeartbeatRequest = z9.object({
1669
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1670
+ runnerId: z9.string().min(1),
1671
+ daemonVersion: z9.string().min(1),
1178
1672
  capabilities: CapabilityMatrix,
1673
+ /**
1674
+ * Kinds this device is withholding, and why it can be said.
1675
+ *
1676
+ * Optional so a daemon that has nothing withheld sends nothing, and so an
1677
+ * older daemon against a newer hub is simply a device with no withheld
1678
+ * kinds rather than a parse failure.
1679
+ */
1680
+ withheld: z9.array(WithheldKind).default([]),
1179
1681
  /**
1180
1682
  * Leases this daemon believes it holds; the server renews exactly these.
1181
1683
  *
1182
1684
  * Lease ids rather than job ids, so a replayed heartbeat cannot renew a
1183
1685
  * grant the runner no longer holds — see {@link Lease.id}.
1184
1686
  */
1185
- activeLeases: z8.array(
1186
- z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
1687
+ activeLeases: z9.array(
1688
+ z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) })
1187
1689
  ),
1188
1690
  /** True while the owner has the daemon paused; the server stops offering work. */
1189
- paused: z8.boolean()
1691
+ paused: z9.boolean()
1190
1692
  }).strict();
1191
- var HeartbeatResponse = z8.object({
1192
- /** Once true, the daemon stops claiming and abandons in-flight work. */
1193
- revoked: z8.boolean(),
1693
+ var HeartbeatResponse = z9.object({
1694
+ /**
1695
+ * The sites this daemon may serve, right now — cloud_008 finding 59.
1696
+ *
1697
+ * Revocation used to be a boolean, and it was device-wide: the daemon
1698
+ * plane refused every call when the (owner, hub-site) consent was gone,
1699
+ * heartbeat answered `revoked: true` with `lost: all`, and the daemon
1700
+ * dropped its whole pairing by origin. Under a hub that is one site's
1701
+ * revocation ending a machine's relationship with every other site it
1702
+ * served — the amplification finding 48 warned about, arriving through
1703
+ * the one field nobody thought of as tenancy.
1704
+ *
1705
+ * So the answer is the set. A site that leaves it is revoked *for that
1706
+ * site*: the daemon drops that pin and keeps the rest. An empty set is
1707
+ * what "revoked" used to mean, and the daemon can see that for itself
1708
+ * rather than being told a second time — two fields for one fact is how
1709
+ * they drift.
1710
+ */
1711
+ sites: z9.record(z9.string().min(1), PublicIdentity),
1712
+ /**
1713
+ * How a site's current key traces back to one this daemon already holds —
1714
+ * byollm_009 Amendment C.
1715
+ *
1716
+ * Keyed by the same id as `sites`, and **additive on purpose**: `sites`
1717
+ * remains the one statement of which key is current, and this says only
1718
+ * how that key got there. Two fields for one fact is how they drift; this
1719
+ * is two facts, and the second is evidence about the first.
1720
+ *
1721
+ * Optional because a site that has never rotated has no chain, which is
1722
+ * every site today. A daemon that receives one for an id it already holds
1723
+ * ignores it: the pin it has is the pin it approved.
1724
+ *
1725
+ * §12 carries what this adds to the metadata surface — a site's rotation
1726
+ * history is public by construction, because a daemon that cannot read it
1727
+ * cannot verify it.
1728
+ */
1729
+ successions: z9.record(
1730
+ z9.string().min(1),
1731
+ z9.object({
1732
+ /** Oldest last, as the projection carries it. */
1733
+ succeeds: z9.array(Succession).max(MAX_SUCCESSION_CHAIN),
1734
+ /**
1735
+ * Until when the superseded key may still sign work — epoch ms.
1736
+ *
1737
+ * The daemon holds its own clock against this, for the reason it
1738
+ * holds its own allowlist: a projection that could extend the
1739
+ * window indefinitely would be a two-key site forever, decided by
1740
+ * the party this design does not trust.
1741
+ */
1742
+ retiringUntil: z9.number().int().positive().optional()
1743
+ }).strict()
1744
+ ).optional(),
1194
1745
  /**
1195
1746
  * Per-job cancel (byollm_001 Rev 1 §C). The daemon aborts these jobs'
1196
1747
  * in-flight backend calls and reports them `canceled`.
1748
+ *
1749
+ * **The grant, not the id** — V1-3. Job ids are chosen per site, so two
1750
+ * sites may pick the same one, and a bare id told a daemon holding both
1751
+ * to abort whichever it happened to have filed under that name. The lease
1752
+ * is the unique grant and the daemon already keys its work by it; this is
1753
+ * the same shape `activeLeases` sends in the other direction.
1197
1754
  */
1198
- cancel: z8.array(z8.string().min(1)),
1199
- /** Jobs whose leases were renewed, with their new expiry. */
1200
- leases: z8.array(
1201
- z8.object({
1202
- jobId: z8.string().min(1),
1203
- expiresAt: z8.number().int().positive()
1204
- }).strict()
1755
+ cancel: z9.array(
1756
+ z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict()
1205
1757
  ),
1758
+ // `leases` is deliberately absent — cloud_008 §1.4b, finding 16.
1759
+ //
1760
+ // It carried "these leases were renewed, and here is the new expiry", and
1761
+ // **no daemon ever read it.** A mutation returning an empty list while
1762
+ // renewing correctly survived every test, which is what made it visible.
1763
+ //
1764
+ // It is neither a class nor membership, so Amendment A's rule does not
1765
+ // decide it — the older test does: nothing reads it, so it is dead wire.
1766
+ // §6's exhaustiveness is a commitment about what an upstream can see, and
1767
+ // it applies to every message rather than only to the stub.
1768
+ //
1769
+ // `lost` is the actionable signal and always was: a daemon stops work on
1770
+ // a lease it no longer holds. "Renewed" was the same question answered a
1771
+ // second time, and a second answer can only agree or contradict.
1772
+ //
1773
+ // Renewal itself is untouched — the upstream still extends the grants a
1774
+ // heartbeat names, which is what §0.6 fixed. What ended is telling the
1775
+ // daemon about it in a field it ignored. If an upstream ever needs to
1776
+ // push lease decisions, that is a new field with a reader, added on
1777
+ // purpose.
1206
1778
  /**
1207
1779
  * Jobs the daemon thinks it holds but the server has reassigned or
1208
1780
  * expired. The daemon must stop work on these and not report results.
1781
+ *
1782
+ * Named by grant rather than by id, for V1-3's reason: a bare id is
1783
+ * ambiguous across sites, and "the lease you no longer hold" is exactly
1784
+ * what this field means anyway.
1209
1785
  */
1210
- lost: z8.array(z8.string().min(1)),
1786
+ lost: z9.array(
1787
+ z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) }).strict()
1788
+ ),
1211
1789
  /** Server clock, so a daemon with a skewed clock still honors leases. */
1212
- serverTime: z8.number().int().positive()
1790
+ serverTime: z9.number().int().positive(),
1791
+ /**
1792
+ * Sites whose disclosure the user must read again before work moves —
1793
+ * cloud_008 finding 48, named rather than counted.
1794
+ *
1795
+ * A **subset of `sites`**, deliberately: a paused site keeps its pin, so
1796
+ * re-consenting never costs a re-pair. The daemon can say which site is
1797
+ * waiting and the user can go and read it, which is the difference
1798
+ * between a machine that is quietly idle and one that says why.
1799
+ *
1800
+ * Not `revoked`, which is a human ending a relationship, and not
1801
+ * `paused`, which on the request side already means "this daemon's
1802
+ * operator stopped it" — one word with two subjects on two halves of one
1803
+ * exchange is a confusion nobody untangles from a log.
1804
+ */
1805
+ awaitingConsent: z9.array(z9.string().min(1))
1213
1806
  }).strict();
1214
- var ResultDisposition = z8.enum(["ok", "error", "canceled"]);
1215
- var ResultRequest = z8.object({
1216
- protocolVersion: z8.literal(PROTOCOL_VERSION),
1217
- runnerId: z8.string().min(1),
1218
- jobId: z8.string().min(1),
1807
+ var ResultDisposition = z9.enum(["ok", "error", "canceled"]);
1808
+ var ResultRequest = z9.object({
1809
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1810
+ runnerId: z9.string().min(1),
1811
+ jobId: z9.string().min(1),
1812
+ /**
1813
+ * The grant this result was produced under — cloud_008 §1.4a.
1814
+ *
1815
+ * `fetch` has always named its lease, with the reasoning written beside
1816
+ * it: a request that names only the job would be answerable for whatever
1817
+ * lease exists when it arrives. **The operation that writes the result did
1818
+ * not**, on either plane, and checked only the runner id — which survives
1819
+ * a claim-release-reclaim cycle, so a device whose grant had been swept
1820
+ * and reissued could still land a result for a job it no longer held.
1821
+ *
1822
+ * Found by tracing a mutation that survived in §0.6: the lease lapsed, the
1823
+ * sweep requeued, the daemon re-claimed under a new grant, and the
1824
+ * original run finished and posted anyway. The relay marked the job done
1825
+ * with a result the site cannot open — it verifies the envelope against
1826
+ * the *current* holder's device, so the crypto contains the substitution —
1827
+ * and then refused the real holder's result as a replay. A lost job, in
1828
+ * silence.
1829
+ *
1830
+ * `LEASE_HONORED` is a statement about a lease *instance*. That was
1831
+ * learned once already, when a replayed release yanked a later grant, and
1832
+ * it applies here for the same reason.
1833
+ */
1834
+ leaseId: z9.string().min(1),
1219
1835
  /**
1220
1836
  * The outcome, sealed to the site and signed by the device.
1221
1837
  *
@@ -1231,26 +1847,48 @@ var ResultRequest = z8.object({
1231
1847
  * fact: believing it unverified would let a daemon mark a job `ok` while
1232
1848
  * sealing an error, and only the app would ever find out.
1233
1849
  */
1234
- disposition: ResultDisposition,
1235
- /** Which model actually served it, for the result's provenance. */
1236
- model: z8.string().min(1),
1237
- backendClass: BackendClass,
1238
- /** Wall-clock milliseconds the backend call took. */
1239
- durationMs: z8.number().int().nonnegative()
1850
+ disposition: ResultDisposition
1851
+ // `model`, `backendClass` and `durationMs` are **inside the envelope**
1852
+ // cloud_008 §2.5. See {@link RunMetadata}.
1853
+ //
1854
+ // They were here, in the clear, and that was two problems wearing one
1855
+ // coat. On the direct plane the site recorded unauthenticated fields
1856
+ // beside an authenticated answer: a daemon could seal one result and
1857
+ // declare a different model, and only the unsigned half would reach the
1858
+ // app. Through a relay they reached a third party that acts on none of
1859
+ // them — `model` in particular being the sort of detail Amendment A's
1860
+ // rule keeps off the wire.
1861
+ //
1862
+ // `disposition` stays, and the difference is the test: a relay *routes*
1863
+ // on it, so it is a class a routing party consumes. Nobody between the
1864
+ // two ends consumes these.
1240
1865
  }).strict();
1241
- var ResultResponse = z8.object({
1866
+ var ResultResponse = z9.object({
1242
1867
  /**
1243
- * False when the submission lost an idempotency race or the lease was
1244
- * already gone the daemon should discard, not retry
1245
- * ({@link MUSTS.RESULT_IDEMPOTENT}).
1868
+ * False when this submission wrote nothing the daemon should discard,
1869
+ * not retry ({@link MUSTS.RESULT_IDEMPOTENT}).
1246
1870
  */
1247
- accepted: z8.boolean(),
1871
+ accepted: z9.boolean(),
1872
+ /**
1873
+ * True when this device had already recorded this job's result.
1874
+ *
1875
+ * The difference between "already recorded" and "you no longer hold this"
1876
+ * — cloud_008 §3.6. A daemon whose acknowledgment was lost is in the first
1877
+ * case and needs to hear it: its answer is safely on disk. Reporting a
1878
+ * stale lease instead invents a worry about a result that is already
1879
+ * stored, and sends its owner looking for a routing problem.
1880
+ *
1881
+ * Set only for the device that finished the job. A different device gets
1882
+ * the same refusal it would get for a job that is *not* terminal, so a job
1883
+ * id cannot be used as a terminality probe.
1884
+ */
1885
+ duplicate: z9.boolean().optional(),
1248
1886
  /** The job's state after this submission. */
1249
- state: z8.string().min(1)
1887
+ state: z9.string().min(1)
1250
1888
  }).strict();
1251
- var ReleaseRequest = z8.object({
1252
- protocolVersion: z8.literal(PROTOCOL_VERSION),
1253
- runnerId: z8.string().min(1),
1889
+ var ReleaseRequest = z9.object({
1890
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
1891
+ runnerId: z9.string().min(1),
1254
1892
  /**
1255
1893
  * Which leases to release — the grant, not just the job.
1256
1894
  *
@@ -1258,8 +1896,8 @@ var ReleaseRequest = z8.object({
1258
1896
  * moment it arrives, which for a replayed request is not the lease the
1259
1897
  * daemon meant. See {@link Lease.id}.
1260
1898
  */
1261
- leases: z8.array(
1262
- z8.object({ jobId: z8.string().min(1), leaseId: z8.string().min(1) })
1899
+ leases: z9.array(
1900
+ z9.object({ jobId: z9.string().min(1), leaseId: z9.string().min(1) })
1263
1901
  ),
1264
1902
  /**
1265
1903
  * Why, so the app's runner list can say something true.
@@ -1270,39 +1908,152 @@ var ReleaseRequest = z8.object({
1270
1908
  * stop offering that job to that runner, or the pair would spin between
1271
1909
  * claim and release forever.
1272
1910
  */
1273
- reason: z8.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
1911
+ reason: z9.enum(["shutdown", "pause", "revoked", "backend-down", "refused"])
1274
1912
  }).strict();
1275
- var ReleaseResponse = z8.object({
1276
- released: z8.array(z8.string().min(1))
1913
+ var ReleaseResponse = z9.object({
1914
+ released: z9.array(z9.string().min(1))
1277
1915
  }).strict();
1278
- var WireErrorCode = z8.enum([
1916
+ var WireErrorCode = z9.enum([
1279
1917
  "bad-request",
1280
1918
  "unsupported-protocol-version",
1919
+ // "We do not know who you are." Exactly 401, and only that — cloud_008
1920
+ // §1.4d.
1281
1921
  "unauthorized",
1922
+ /**
1923
+ * "We know exactly who you are, and the answer is no." Exactly 403.
1924
+ *
1925
+ * Five refusals across both planes served 403 with `unauthorized`, whose
1926
+ * table entry is 401: a revoked device, a site claiming another site's
1927
+ * stub, a job you do not hold, a device belonging to another owner, a
1928
+ * relay that does not route for you. Every one of them is an *identified*
1929
+ * caller being refused.
1930
+ *
1931
+ * Collapsing the two loses a distinction that matters everywhere it is
1932
+ * read: a revoked daemon would look like an unsigned one in every log and
1933
+ * every client branch, and "check your keys" is the wrong advice for both
1934
+ * of them in opposite directions.
1935
+ */
1936
+ "forbidden",
1282
1937
  "revoked",
1283
1938
  "not-found",
1939
+ // Claimed, but the site has not sealed the payload yet — cloud_008 §1.4.
1940
+ //
1941
+ // A daemon must retry rather than abandon: the job is legitimately still
1942
+ // its own until the lease or the awaiting-payload clock says otherwise.
1943
+ // That is why it cannot be `not-found` or `server-error`, and why it was
1944
+ // the protocol gap that produced a bare 409 in the first place.
1945
+ "not-ready",
1946
+ /**
1947
+ * The job is over, and this call is about a job — V1-6, and the code the
1948
+ * site plane has been serving without one (V1-13).
1949
+ *
1950
+ * Distinct from `not-found`, which says "no such job", and from
1951
+ * `not-ready`, which says "not yet, keep asking". This one says "yes, and
1952
+ * it finished" — so a daemon must stop rather than retry, and a replayed
1953
+ * request must not be able to reopen it.
1954
+ */
1955
+ "too-late",
1956
+ // The caller's clock is too far from ours to judge a signature's freshness.
1957
+ //
1958
+ // Split out from `unauthorized` because the remedy is completely different
1959
+ // and only the server can tell them apart: a bad signature means the key is
1960
+ // wrong, this means the machine's time is wrong. A daemon reporting it as a
1961
+ // generic rejection sends its owner looking at their network.
1962
+ "clock-skew",
1284
1963
  "rate-limited",
1285
1964
  "server-error"
1286
1965
  ]);
1287
- var WireError = z8.object({
1966
+ var WireError = z9.object({
1288
1967
  error: WireErrorCode,
1289
- message: z8.string().min(1),
1968
+ message: z9.string().min(1),
1969
+ /**
1970
+ * What this server speaks, on `unsupported-protocol-version` — §B.4.
1971
+ *
1972
+ * The refusal has carried these since the version handshake existed and
1973
+ * the enumeration did not model them, so the one error that exists to be
1974
+ * *acted on* was the one that failed to parse as a wire error. Found by
1975
+ * the relay's own suite the day the relay started sending it: a refusal
1976
+ * outside the enumerated shape is a refusal a client cannot branch on,
1977
+ * which is the whole reason §1.4 enumerates them.
1978
+ *
1979
+ * Modelled the way `clock-skew`'s two fields already are — code-specific
1980
+ * extras, refused on any other code by the refinement below.
1981
+ */
1982
+ supported: z9.array(z9.string().min(1)).optional(),
1983
+ minimum: z9.string().min(1).optional(),
1290
1984
  /** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
1291
- retryAfter: z8.number().int().nonnegative().optional()
1292
- }).strict();
1985
+ retryAfter: z9.number().int().nonnegative().optional(),
1986
+ /**
1987
+ * The server's clock, and the window it allows. `clock-skew` only.
1988
+ *
1989
+ * So the far side can say *how far off* rather than *that something is
1990
+ * wrong* — the difference between "adjust your clock by four minutes" and
1991
+ * "something is wrong with your connection". Not a disclosure: the
1992
+ * heartbeat response returns the same value, and so does every `Date`
1993
+ * header.
1994
+ */
1995
+ serverTime: z9.number().int().positive().optional(),
1996
+ maxSkewMs: z9.number().int().positive().optional()
1997
+ }).strict().superRefine((error, ctx) => {
1998
+ const skew = error.error === "clock-skew";
1999
+ const carried = error.serverTime !== void 0 || error.maxSkewMs !== void 0;
2000
+ if (skew && !carried) {
2001
+ ctx.addIssue({
2002
+ code: "custom",
2003
+ message: "clock-skew must carry serverTime and maxSkewMs"
2004
+ });
2005
+ }
2006
+ if (!skew && carried) {
2007
+ ctx.addIssue({
2008
+ code: "custom",
2009
+ message: `${error.error} must not carry serverTime or maxSkewMs`
2010
+ });
2011
+ }
2012
+ const version = error.error === "unsupported-protocol-version";
2013
+ const versionFields = error.supported !== void 0 || error.minimum !== void 0;
2014
+ if (version && !versionFields) {
2015
+ ctx.addIssue({
2016
+ code: "custom",
2017
+ message: "unsupported-protocol-version must carry supported and minimum"
2018
+ });
2019
+ }
2020
+ if (!version && versionFields) {
2021
+ ctx.addIssue({
2022
+ code: "custom",
2023
+ message: `${error.error} must not carry supported or minimum`
2024
+ });
2025
+ }
2026
+ });
1293
2027
  var ERROR_STATUS = Object.freeze({
1294
2028
  "bad-request": 400,
1295
2029
  "unsupported-protocol-version": 400,
1296
2030
  unauthorized: 401,
2031
+ forbidden: 403,
1297
2032
  revoked: 403,
1298
2033
  "not-found": 404,
2034
+ // 409, not 404: the job exists and is yours, it is simply not ready.
2035
+ "not-ready": 409,
2036
+ // The same 409 as `not-ready` and the opposite instruction: that one says
2037
+ // keep asking, this one says stop. The status is the class of the
2038
+ // problem — a request that does not fit the resource's state — and the
2039
+ // code is what a caller acts on.
2040
+ "too-late": 409,
2041
+ // 401 alongside `unauthorized`, because that is what it is — the
2042
+ // signature could not be judged. The code is what carries the remedy.
2043
+ "clock-skew": 401,
1299
2044
  "rate-limited": 429,
1300
2045
  "server-error": 500
1301
2046
  });
1302
- var FetchRequest = z8.object({
1303
- protocolVersion: z8.string().min(1),
1304
- runnerId: z8.string().min(1),
1305
- jobId: z8.string().min(1),
2047
+ var FetchRequest = z9.object({
2048
+ // `literal`, like every other request — V1-17. This one said
2049
+ // `string().min(1)`, so a daemon speaking a version this server does not
2050
+ // know got past the handshake on the one endpoint that hands over a
2051
+ // sealed payload. The version check exists so that a mismatch is a named
2052
+ // refusal rather than a schema failure three fields later; here it was
2053
+ // neither.
2054
+ protocolVersion: z9.literal(PROTOCOL_VERSION),
2055
+ runnerId: z9.string().min(1),
2056
+ jobId: z9.string().min(1),
1306
2057
  /**
1307
2058
  * The grant this daemon holds.
1308
2059
  *
@@ -1310,9 +2061,9 @@ var FetchRequest = z8.object({
1310
2061
  * only the job would be answerable for whatever lease exists when it
1311
2062
  * arrives ({@link Lease.id}).
1312
2063
  */
1313
- leaseId: z8.string().min(1)
2064
+ leaseId: z9.string().min(1)
1314
2065
  }).strict();
1315
- var FetchResponse = z8.object({
2066
+ var FetchResponse = z9.object({
1316
2067
  /**
1317
2068
  * The work, sealed to the device that claimed it — byollm_009 §6.
1318
2069
  *
@@ -1340,6 +2091,7 @@ export {
1340
2091
  ClaimedJob,
1341
2092
  ClaimedStub,
1342
2093
  DeliveredResult,
2094
+ ENCRYPTION_KEY_CONTEXT,
1343
2095
  ENDPOINTS,
1344
2096
  ENVELOPE_MAX_AGE_MS,
1345
2097
  ERROR_STATUS,
@@ -1353,6 +2105,7 @@ export {
1353
2105
  JobKind,
1354
2106
  JobOutcome,
1355
2107
  JobPayload,
2108
+ JobRefused,
1356
2109
  JobResultCanceled,
1357
2110
  JobResultError,
1358
2111
  JobResultOk,
@@ -1361,6 +2114,7 @@ export {
1361
2114
  KindedPayload,
1362
2115
  Lease,
1363
2116
  MAX_CLOCK_SKEW_MS,
2117
+ MAX_SUCCESSION_CHAIN,
1364
2118
  MIN_PROTOCOL_VERSION,
1365
2119
  MUSTS,
1366
2120
  MUST_IDS,
@@ -1377,6 +2131,8 @@ export {
1377
2131
  PairStartResponse,
1378
2132
  PublicIdentity,
1379
2133
  REFUSAL_MESSAGES,
2134
+ RETIREMENT_WINDOW_MS,
2135
+ RefusalReason,
1380
2136
  ReleaseRequest,
1381
2137
  ReleaseResponse,
1382
2138
  RequestSignature,
@@ -1384,27 +2140,35 @@ export {
1384
2140
  ResultProvenance,
1385
2141
  ResultRequest,
1386
2142
  ResultResponse,
2143
+ RunMetadata,
1387
2144
  SIZE_CLASS_LIMITS,
2145
+ SUCCESSION_CONTEXT,
1388
2146
  SUPPORTED_PROTOCOL_VERSIONS,
1389
2147
  SealedEnvelope,
2148
+ SealedOutcome,
1390
2149
  SizeClass,
1391
2150
  StoredKeys,
2151
+ Succession,
1392
2152
  TERMINAL_STATES,
1393
2153
  WireError,
1394
2154
  WireErrorCode,
2155
+ WithheldKind,
1395
2156
  backendDescriptor,
1396
2157
  canTransition,
1397
2158
  canonicalRequest,
1398
2159
  checkProtocolVersion,
1399
2160
  cryptoReady,
2161
+ declaredVersion,
1400
2162
  effectiveOfferScope,
1401
2163
  fingerprint,
1402
2164
  generateKeys,
1403
2165
  isBackendId,
2166
+ isCloudTaggedModel,
1404
2167
  isJobKind,
1405
2168
  isLocalHost,
1406
2169
  isTerminal,
1407
2170
  keyId,
2171
+ kindsOf,
1408
2172
  matchAudience,
1409
2173
  mustsVerifiedBy,
1410
2174
  open,
@@ -1414,11 +2178,17 @@ export {
1414
2178
  resolveCost,
1415
2179
  seal,
1416
2180
  signRequest,
2181
+ signSiteRequest,
2182
+ signSuccession,
1417
2183
  signWith,
1418
2184
  sizeClassCeiling,
1419
2185
  sizeClassOf,
2186
+ successionStatement,
2187
+ verifyLink,
1420
2188
  verifyPublicIdentity,
1421
2189
  verifyRequest,
1422
- verifyWith
2190
+ verifySiteRequest,
2191
+ verifyWith,
2192
+ walkSuccession
1423
2193
  };
1424
2194
  //# sourceMappingURL=index.js.map