@byollm/protocol 0.1.0-alpha.81 → 0.1.0-alpha.83

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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  > [!WARNING]
2
- > **Alpha (`0.1.0-alpha.81`) — under active development. Don't use this yet.**
2
+ > **Alpha (`0.1.0-alpha.83`) — under active development. Don't use this yet.**
3
3
  >
4
4
  > Install it deliberately: `npm install @byollm/protocol@alpha`.
5
5
  >
package/dist/index.d.ts CHANGED
@@ -1,5 +1,96 @@
1
1
  import { z } from 'zod';
2
2
 
3
+ /**
4
+ * Who may be told about a new version — B053.
5
+ *
6
+ * `HeartbeatResponse.updateTo` is a new field on a `.strict()` schema, and
7
+ * strict means a daemon built before the field does not ignore it: it rejects
8
+ * the entire heartbeat. Send it to everybody and the message carrying the
9
+ * update is the message that takes offline the machines it was meant to
10
+ * update — the fleet-wide version of the failure this codebase keeps meeting
11
+ * one surface at a time.
12
+ *
13
+ * No handshake is needed to avoid that. `HeartbeatRequest.daemonVersion` is
14
+ * already on the wire, so the sender can simply decline to say anything a
15
+ * given listener cannot hear. The rule lives here, next to the field it
16
+ * governs, because a rule of this kind in a runbook is a rule that holds
17
+ * until the next person deploys.
18
+ *
19
+ * ## The other direction, deliberately not taken
20
+ *
21
+ * The tidier-looking design is a capability list on the request — the daemon
22
+ * says what it understands. It has a hole this one does not: that field is
23
+ * also new, the hub's schema is also strict, and an upgraded daemon sending
24
+ * it to a hub that has not deployed yet is refused outright. It makes the
25
+ * daemon's upgrade depend on the hub's, in a system where the daemon is the
26
+ * side we do not control the timing of.
27
+ *
28
+ * Reading a field that already exists has no such ordering problem: old
29
+ * daemons are never sent the new field, new daemons are, and neither needs
30
+ * the other to have moved first.
31
+ */
32
+ /**
33
+ * The first daemon version whose schema has `updateTo` in it.
34
+ *
35
+ * Raising this is safe and lowering it is not, which is worth knowing before
36
+ * anybody tidies it: too high means some daemons miss an update they could
37
+ * have taken, and too low means their heartbeats are refused.
38
+ */
39
+ declare const UPDATE_OFFER_SINCE = "0.1.0-alpha.83";
40
+ /**
41
+ * Semver ordering, only as far as this needs it.
42
+ *
43
+ * Numeric prerelease parts compare as numbers, which is the whole reason not
44
+ * to compare these as strings: `alpha.9` sorts after `alpha.83`
45
+ * lexicographically, and that mistake here means every daemon between .10 and
46
+ * .82 is treated as too old to hear about an update — or worse, in the other
47
+ * direction, sent a field it cannot parse.
48
+ */
49
+ declare function compareVersions(a: string, b: string): number | undefined;
50
+ /**
51
+ * May this daemon be told about a new version?
52
+ *
53
+ * **A version this cannot parse is a no.** Unreadable is not permission: the
54
+ * consequence of guessing wrong in that direction is the daemon's heartbeat
55
+ * being refused, which is worse than it missing one update cycle. The same
56
+ * rule the rest of this protocol applies to unreadable answers, on the one
57
+ * field where getting it wrong is fleet-shaped.
58
+ */
59
+ declare function mayOfferUpdate(daemonVersion: string): boolean;
60
+ /**
61
+ * The oldest daemon a hub will serve — B052, the floor.
62
+ *
63
+ * The updater's backstop and its opposite number. The updater moves machines
64
+ * that opted in; the floor is what moves the ones that did not, and it is the
65
+ * only mechanism that works on a daemon which is not listening for offers.
66
+ *
67
+ * Raising it is a deliberate act with a spec note, never automatic — a floor
68
+ * that followed `latest` would refuse every machine that had not updated in
69
+ * the last hour, which is the outage version of hygiene.
70
+ */
71
+ interface FloorRefusal {
72
+ readonly error: "daemon-below-floor";
73
+ readonly message: string;
74
+ readonly floor: string;
75
+ }
76
+ /**
77
+ * Is this daemon too old to serve?
78
+ *
79
+ * **An unreadable version is NOT refused**, and that is the deliberate
80
+ * asymmetry with {@link mayOfferUpdate}, which treats an unreadable version
81
+ * as "do not offer". The two point the same way once you ask what the
82
+ * mistake costs: there, guessing yes sends a field that breaks the
83
+ * heartbeat; here, guessing yes takes a working machine out of service over
84
+ * a string it could not parse. Both decline to act when they cannot tell,
85
+ * and declining to act means opposite booleans.
86
+ */
87
+ declare function checkDaemonFloor(input: {
88
+ readonly daemonVersion: string;
89
+ readonly floor: string;
90
+ /** How somebody fixes it. The floor is useless without the remedy. */
91
+ readonly upgradeCommand: string;
92
+ }): FloorRefusal | null;
93
+
3
94
  /** The full description — five sections, plus why it matters. */
4
95
  declare const ABOUT = "# About BYOLLM\n\n**What BYOLLM is**\n\nBYOLLM lets you use your own AI on websites. You install one small program on\nyour computer. Then, websites that support BYOLLM can use the AI you already\nhave \u2014 a free model running on your machine, or an AI service you already pay\nfor \u2014 instead of the website paying for AI and passing the cost to you.\n\n**Why it matters**\n\nFor you:\n\n- Your favorite model, everywhere you go.\n- New models the moment you get them \u2013 not when a site gets around to adding\n them.\n- Encrypted end-to-end. Your prompts go to your own device; byollm.cloud can't\n read them.\n- Sites never learn which model you use, and your subscriptions are never\n shared.\n- Pay less. Sites that don't pay for AI can charge you less \u2013 or nothing.\n\nFor sites and developers:\n\n- Zero AI bills. Your users bring their own compute.\n- No floating money \u2013 you don't pay LLM bills up front and hope to collect\n later, and you never ask people to prepay just to try you.\n- Free trials that cost you nothing to offer.\n- Ship the AI features you kept private for fear of the API bill.\n- One small integration. Your users choose the models.\n\n**Your device**\n\nThe `byollm` program runs on your computer. It knows which AI services you have\nset up: free open-source models on your machine, metered services you pay per\nuse, or your own subscriptions like Claude Pro/Max. When a website you have\nenabled sends work, your device runs it with the service you chose. Your\nprompts are encrypted end-to-end to your own device. byollm.cloud passes them\nalong and cannot read them.\n\n**Sites**\n\nA website that wants to use BYOLLM says what it needs \u2014 \"writing help,\" \"chat,\"\nand so on. When you connect the site, you pick which of your services answers\neach one. The site never learns which model you use. You can turn a site off at\nany time, and it stops getting your work.\n\n**Teams (optional)**\n\nA team lets you share what runs on your devices with people you name \u2014 the free\nopen-source models on your machine, or a metered service with a spending limit\nyou set. Your subscription accounts (like Claude Pro/Max) are never shared with\nanyone. That is a rule, not a setting.\n\n**byollm.cloud (or your own relay)**\n\nMany sites, many devices, many people. byollm.cloud keeps track of who has\nallowed what and sends each job to the right device. It never sees your\nprompts. If you would rather run this part yourself, the relay is open source \u2014\nyou can run your own instead of using byollm.cloud.";
5
96
  /**
@@ -594,8 +685,8 @@ declare function payloadTextLength(kinded: KindedPayload): number;
594
685
  * ```
595
686
  */
596
687
  declare const JobState: z.ZodEnum<{
597
- ok: "ok";
598
688
  error: "error";
689
+ ok: "ok";
599
690
  expired: "expired";
600
691
  queued: "queued";
601
692
  claimed: "claimed";
@@ -846,8 +937,8 @@ type SealedOutcome = z.infer<typeof SealedOutcome>;
846
937
  declare const DeliveredResult: z.ZodObject<{
847
938
  jobId: z.ZodString;
848
939
  state: z.ZodEnum<{
849
- ok: "ok";
850
940
  error: "error";
941
+ ok: "ok";
851
942
  expired: "expired";
852
943
  queued: "queued";
853
944
  claimed: "claimed";
@@ -2535,6 +2626,7 @@ declare const HeartbeatResponse: z.ZodObject<{
2535
2626
  }, z.core.$strict>>;
2536
2627
  serverTime: z.ZodNumber;
2537
2628
  awaitingConsent: z.ZodArray<z.ZodString>;
2629
+ updateTo: z.ZodOptional<z.ZodString>;
2538
2630
  }, z.core.$strict>;
2539
2631
  type HeartbeatResponse = z.infer<typeof HeartbeatResponse>;
2540
2632
  /**
@@ -2550,8 +2642,8 @@ type HeartbeatResponse = z.infer<typeof HeartbeatResponse>;
2550
2642
  * and collapsing them would make the relay guess.
2551
2643
  */
2552
2644
  declare const ResultDisposition: z.ZodEnum<{
2553
- ok: "ok";
2554
2645
  error: "error";
2646
+ ok: "ok";
2555
2647
  canceled: "canceled";
2556
2648
  }>;
2557
2649
  type ResultDisposition = z.infer<typeof ResultDisposition>;
@@ -2571,8 +2663,8 @@ declare const ResultRequest: z.ZodObject<{
2571
2663
  deadlineAt: z.ZodNumber;
2572
2664
  }, z.core.$strict>;
2573
2665
  disposition: z.ZodEnum<{
2574
- ok: "ok";
2575
2666
  error: "error";
2667
+ ok: "ok";
2576
2668
  canceled: "canceled";
2577
2669
  }>;
2578
2670
  }, z.core.$strict>;
@@ -2612,6 +2704,7 @@ type ReleaseResponse = z.infer<typeof ReleaseResponse>;
2612
2704
  * with no response at all.
2613
2705
  */
2614
2706
  declare const WireErrorCode: z.ZodEnum<{
2707
+ "daemon-below-floor": "daemon-below-floor";
2615
2708
  "unsupported-protocol-version": "unsupported-protocol-version";
2616
2709
  revoked: "revoked";
2617
2710
  "bad-request": "bad-request";
@@ -2627,6 +2720,7 @@ declare const WireErrorCode: z.ZodEnum<{
2627
2720
  type WireErrorCode = z.infer<typeof WireErrorCode>;
2628
2721
  declare const WireError: z.ZodObject<{
2629
2722
  error: z.ZodEnum<{
2723
+ "daemon-below-floor": "daemon-below-floor";
2630
2724
  "unsupported-protocol-version": "unsupported-protocol-version";
2631
2725
  revoked: "revoked";
2632
2726
  "bad-request": "bad-request";
@@ -2642,6 +2736,7 @@ declare const WireError: z.ZodObject<{
2642
2736
  message: z.ZodString;
2643
2737
  supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
2644
2738
  minimum: z.ZodOptional<z.ZodString>;
2739
+ floor: z.ZodOptional<z.ZodString>;
2645
2740
  retryAfter: z.ZodOptional<z.ZodNumber>;
2646
2741
  serverTime: z.ZodOptional<z.ZodNumber>;
2647
2742
  maxSkewMs: z.ZodOptional<z.ZodNumber>;
@@ -2670,4 +2765,4 @@ declare const FetchResponse: z.ZodObject<{
2670
2765
  }, z.core.$strict>;
2671
2766
  type FetchResponse = z.infer<typeof FetchResponse>;
2672
2767
 
2673
- export { ABOUT, ABOUT_SHORT, ABOUT_SHORT_LEDE, ABOUT_SHORT_TAIL, AUDIENCES, Audience, BACKENDS, BACKEND_CLASSES, BACKEND_IDS, BackendClass, BackendCost, type BackendDescriptor, type BackendId, BackendIdSchema, CLOCK_ATTRIBUTION_MS, CLOCK_SKEW_WARN_MS, Capability, CapabilityMatrix, ChatMessage, ChatPayload, ClaimRequest, ClaimResponse, ClaimedJob, ClaimedStub, DeliveredResult, ENCRYPTION_KEY_CONTEXT, ENDPOINTS, ENVELOPE_MAX_AGE_MS, ERROR_STATUS, type Endpoint, type EnvelopeContext, EnvelopeDirection, type EnvelopeFailure, FetchRequest, FetchResponse, GRANT_CONTEXT, GRANT_MAX_AGE_MS, GRANT_SIGNED_FIELDS, GeneratePayload, type GrantClaims, GrantRef, type GrantRefusal, HeartbeatRequest, HeartbeatResponse, JOB_KINDS, JobKind, JobOutcome, JobPayload, JobRefused, JobResultCanceled, JobResultError, JobResultOk, JobState, JobStub, KindedPayload, Lease, MAX_CLOCK_SKEW_MS, MAX_ENVELOPE_BYTES, MAX_PURPOSES, MAX_SUCCESSION_CHAIN, MIN_PROTOCOL_VERSION, MUSTS, MUST_IDS, Manifest, type MatchDaemon, type MatchJob, MatchRefusal, type MatchResult, type Must, type MustEnforcer, type MustId, type MustVerification, type MustVerifiedBy, OFFER_SCOPES, OfferScope, type OpenResult, PAYLOAD_LIMITS, PROTOCOL_PREFIX, PROTOCOL_VERSION, PairPollRequest, PairPollResponse, PairRequest, PairStartRequest, PairStartResponse, type PayloadFor, PublicIdentity, Purpose, REFUSAL_MESSAGES, RESERVED_PURPOSE, RETIREMENT_WINDOW_MS, RefusalReason, ReleaseRequest, ReleaseResponse, RequestSignature, ResultDisposition, ResultProvenance, ResultRequest, ResultResponse, RunMetadata, SIZE_CLASSES, SIZE_CLASS_LIMITS, SUCCESSION_CONTEXT, SUPPORTED_PROTOCOL_VERSIONS, SealedEnvelope, SealedOutcome, type SignatureFailure, SignedGrant, SizeClass, type SpendConsent, StoredKeys, Succession, type SuccessionFailure, type SuccessionWalk, TERMINAL_STATES, type VersionRefusal, WireError, WireErrorCode, WithheldKind, backendDescriptor, backendName, canTransition, canonicalRequest, checkProtocolVersion, classifyCost, cryptoReady, declaredVersion, effectiveOfferScope, envelopeBytes, fingerprint, generateKeys, grantStatement, isBackendId, isCloudTaggedModel, isJobKind, isLocalHost, isTerminal, keyId, kindsOf, matchAudience, mustsVerifiedBy, open, payloadTextLength, provenanceFor, publicIdentityOf, resolveCost, seal, signGrant, signRequest, signSiteRequest, signSuccession, signWith, singlePurposeManifest, sizeClassCeiling, sizeClassOf, successionStatement, verifyGrant, verifyLink, verifyPublicIdentity, verifyRequest, verifySiteRequest, verifyWith, walkSuccession };
2768
+ export { ABOUT, ABOUT_SHORT, ABOUT_SHORT_LEDE, ABOUT_SHORT_TAIL, AUDIENCES, Audience, BACKENDS, BACKEND_CLASSES, BACKEND_IDS, BackendClass, BackendCost, type BackendDescriptor, type BackendId, BackendIdSchema, CLOCK_ATTRIBUTION_MS, CLOCK_SKEW_WARN_MS, Capability, CapabilityMatrix, ChatMessage, ChatPayload, ClaimRequest, ClaimResponse, ClaimedJob, ClaimedStub, DeliveredResult, ENCRYPTION_KEY_CONTEXT, ENDPOINTS, ENVELOPE_MAX_AGE_MS, ERROR_STATUS, type Endpoint, type EnvelopeContext, EnvelopeDirection, type EnvelopeFailure, FetchRequest, FetchResponse, type FloorRefusal, GRANT_CONTEXT, GRANT_MAX_AGE_MS, GRANT_SIGNED_FIELDS, GeneratePayload, type GrantClaims, GrantRef, type GrantRefusal, HeartbeatRequest, HeartbeatResponse, JOB_KINDS, JobKind, JobOutcome, JobPayload, JobRefused, JobResultCanceled, JobResultError, JobResultOk, JobState, JobStub, KindedPayload, Lease, MAX_CLOCK_SKEW_MS, MAX_ENVELOPE_BYTES, MAX_PURPOSES, MAX_SUCCESSION_CHAIN, MIN_PROTOCOL_VERSION, MUSTS, MUST_IDS, Manifest, type MatchDaemon, type MatchJob, MatchRefusal, type MatchResult, type Must, type MustEnforcer, type MustId, type MustVerification, type MustVerifiedBy, OFFER_SCOPES, OfferScope, type OpenResult, PAYLOAD_LIMITS, PROTOCOL_PREFIX, PROTOCOL_VERSION, PairPollRequest, PairPollResponse, PairRequest, PairStartRequest, PairStartResponse, type PayloadFor, PublicIdentity, Purpose, REFUSAL_MESSAGES, RESERVED_PURPOSE, RETIREMENT_WINDOW_MS, RefusalReason, ReleaseRequest, ReleaseResponse, RequestSignature, ResultDisposition, ResultProvenance, ResultRequest, ResultResponse, RunMetadata, SIZE_CLASSES, SIZE_CLASS_LIMITS, SUCCESSION_CONTEXT, SUPPORTED_PROTOCOL_VERSIONS, SealedEnvelope, SealedOutcome, type SignatureFailure, SignedGrant, SizeClass, type SpendConsent, StoredKeys, Succession, type SuccessionFailure, type SuccessionWalk, TERMINAL_STATES, UPDATE_OFFER_SINCE, type VersionRefusal, WireError, WireErrorCode, WithheldKind, backendDescriptor, backendName, canTransition, canonicalRequest, checkDaemonFloor, checkProtocolVersion, classifyCost, compareVersions, cryptoReady, declaredVersion, effectiveOfferScope, envelopeBytes, fingerprint, generateKeys, grantStatement, isBackendId, isCloudTaggedModel, isJobKind, isLocalHost, isTerminal, keyId, kindsOf, matchAudience, mayOfferUpdate, mustsVerifiedBy, open, payloadTextLength, provenanceFor, publicIdentityOf, resolveCost, seal, signGrant, signRequest, signSiteRequest, signSuccession, signWith, singlePurposeManifest, sizeClassCeiling, sizeClassOf, successionStatement, verifyGrant, verifyLink, verifyPublicIdentity, verifyRequest, verifySiteRequest, verifyWith, walkSuccession };
package/dist/index.js CHANGED
@@ -1,3 +1,52 @@
1
+ // src/update-offer.ts
2
+ var UPDATE_OFFER_SINCE = "0.1.0-alpha.83";
3
+ function parse(version) {
4
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
5
+ version
6
+ );
7
+ if (match === null) return void 0;
8
+ return {
9
+ release: [Number(match[1]), Number(match[2]), Number(match[3])],
10
+ pre: match[4] === void 0 ? [] : match[4].split(".").map((part) => /^\d+$/.test(part) ? Number(part) : part)
11
+ };
12
+ }
13
+ function compareVersions(a, b) {
14
+ const left = parse(a);
15
+ const right = parse(b);
16
+ if (left === void 0 || right === void 0) return void 0;
17
+ for (let i = 0; i < 3; i += 1) {
18
+ const diff = (left.release[i] ?? 0) - (right.release[i] ?? 0);
19
+ if (diff !== 0) return diff < 0 ? -1 : 1;
20
+ }
21
+ if (left.pre.length === 0 && right.pre.length > 0) return 1;
22
+ if (left.pre.length > 0 && right.pre.length === 0) return -1;
23
+ for (let i = 0; i < Math.max(left.pre.length, right.pre.length); i += 1) {
24
+ const l = left.pre[i];
25
+ const r = right.pre[i];
26
+ if (l === void 0) return -1;
27
+ if (r === void 0) return 1;
28
+ if (l === r) continue;
29
+ if (typeof l === "number" && typeof r === "number") return l < r ? -1 : 1;
30
+ if (typeof l === "number") return -1;
31
+ if (typeof r === "number") return 1;
32
+ return l < r ? -1 : 1;
33
+ }
34
+ return 0;
35
+ }
36
+ function mayOfferUpdate(daemonVersion) {
37
+ const order = compareVersions(daemonVersion, UPDATE_OFFER_SINCE);
38
+ return order !== void 0 && order >= 0;
39
+ }
40
+ function checkDaemonFloor(input) {
41
+ const order = compareVersions(input.daemonVersion, input.floor);
42
+ if (order === void 0 || order >= 0) return null;
43
+ return {
44
+ error: "daemon-below-floor",
45
+ message: `byollm ${input.daemonVersion} is below the supported floor (${input.floor}). Run \`${input.upgradeCommand}\`, then \`byollm start\`.`,
46
+ floor: input.floor
47
+ };
48
+ }
49
+
1
50
  // src/about.ts
2
51
  var ABOUT = `# About BYOLLM
3
52
 
@@ -2108,7 +2157,30 @@ var HeartbeatResponse = z11.object({
2108
2157
  * operator stopped it" — one word with two subjects on two halves of one
2109
2158
  * exchange is a confusion nobody untangles from a log.
2110
2159
  */
2111
- awaitingConsent: z11.array(z11.string().min(1))
2160
+ awaitingConsent: z11.array(z11.string().min(1)),
2161
+ /**
2162
+ * A version this daemon should move itself to — B053.
2163
+ *
2164
+ * The channel the auto-updater reads, and it is the channel the daemon
2165
+ * already polls rather than a new phone-home, which is what the ruling
2166
+ * asked for (016 §Auto-update).
2167
+ *
2168
+ * **The hub may not send this to every daemon.** This schema is
2169
+ * `.strict()`, so a daemon built before the field exists does not ignore
2170
+ * it — it rejects the whole heartbeat and stops working. Which would mean
2171
+ * the message carrying the update is the message that breaks the machines
2172
+ * it was meant to update.
2173
+ *
2174
+ * That is decidable without any new handshake, because the request
2175
+ * already carries `daemonVersion`. {@link mayOfferUpdate} is the rule,
2176
+ * kept here as code rather than as a paragraph in a runbook, so both
2177
+ * sides read the same one.
2178
+ *
2179
+ * Exact versions only, never a tag: the daemon refuses anything else, and
2180
+ * a fleet resolving one tag at different minutes is a fleet on different
2181
+ * builds reporting one number.
2182
+ */
2183
+ updateTo: z11.string().min(1).optional()
2112
2184
  }).strict();
2113
2185
  var ResultDisposition = z11.enum(["ok", "error", "canceled"]);
2114
2186
  var ResultRequest = z11.object({
@@ -2220,6 +2292,20 @@ var ReleaseResponse = z11.object({
2220
2292
  var WireErrorCode = z11.enum([
2221
2293
  "bad-request",
2222
2294
  "unsupported-protocol-version",
2295
+ /**
2296
+ * The daemon is older than this hub will serve — B052.
2297
+ *
2298
+ * Distinct from `unsupported-protocol-version`, which is about the
2299
+ * contract; this is about the build. A daemon can speak protocol 1
2300
+ * perfectly and still be old enough that we would rather move it than keep
2301
+ * carrying it — and the two need different remedies in the message, since
2302
+ * one is "your daemon and this server disagree" and the other is "yours
2303
+ * works, and it is time".
2304
+ *
2305
+ * The floor is the backstop to the auto-updater (B053), and the only lever
2306
+ * that reaches a machine which never opted into offers.
2307
+ */
2308
+ "daemon-below-floor",
2223
2309
  // "We do not know who you are." Exactly 401, and only that — cloud_008
2224
2310
  // §1.4d.
2225
2311
  "unauthorized",
@@ -2285,6 +2371,16 @@ var WireError = z11.object({
2285
2371
  */
2286
2372
  supported: z11.array(z11.string().min(1)).optional(),
2287
2373
  minimum: z11.string().min(1).optional(),
2374
+ /**
2375
+ * The oldest daemon this hub serves, on `daemon-below-floor` — B052.
2376
+ *
2377
+ * Carried for the same reason `supported` and `minimum` are: a refusal
2378
+ * that cannot be branched on is a refusal a client can only print. The
2379
+ * message already names the floor for a person; this names it for the
2380
+ * code, so a surface can say "you are two versions under" without
2381
+ * parsing English.
2382
+ */
2383
+ floor: z11.string().min(1).optional(),
2288
2384
  /** Seconds; mirrors Retry-After for `rate-limited` and `server-error`. */
2289
2385
  retryAfter: z11.number().int().nonnegative().optional(),
2290
2386
  /**
@@ -2313,6 +2409,19 @@ var WireError = z11.object({
2313
2409
  message: `${error.error} must not carry serverTime or maxSkewMs`
2314
2410
  });
2315
2411
  }
2412
+ const floored = error.error === "daemon-below-floor";
2413
+ if (floored && error.floor === void 0) {
2414
+ ctx.addIssue({
2415
+ code: "custom",
2416
+ message: "daemon-below-floor must carry floor"
2417
+ });
2418
+ }
2419
+ if (!floored && error.floor !== void 0) {
2420
+ ctx.addIssue({
2421
+ code: "custom",
2422
+ message: `${error.error} must not carry floor`
2423
+ });
2424
+ }
2316
2425
  const version = error.error === "unsupported-protocol-version";
2317
2426
  const versionFields = error.supported !== void 0 || error.minimum !== void 0;
2318
2427
  if (version && !versionFields) {
@@ -2331,6 +2440,20 @@ var WireError = z11.object({
2331
2440
  var ERROR_STATUS = Object.freeze({
2332
2441
  "bad-request": 400,
2333
2442
  "unsupported-protocol-version": 400,
2443
+ /**
2444
+ * 426 Upgrade Required — B052, and it is the one status that says this.
2445
+ *
2446
+ * Not 403, which this daemon reads as a permission problem and which
2447
+ * sits beside `revoked` in every log. Not 400, which reads as a
2448
+ * malformed request; the request was perfect and the sender is old.
2449
+ *
2450
+ * It also fails safely on a daemon that predates the code: 426 is not in
2451
+ * that switch, so it lands on the 4xx default — `rejected`, which is
2452
+ * "never retried: the request is wrong, and repeating it stays wrong".
2453
+ * Vaguer than the remedy, and the right behaviour, which is what a
2454
+ * fallback has to be.
2455
+ */
2456
+ "daemon-below-floor": 426,
2334
2457
  unauthorized: 401,
2335
2458
  forbidden: 403,
2336
2459
  revoked: 403,
@@ -2472,6 +2595,7 @@ export {
2472
2595
  StoredKeys,
2473
2596
  Succession,
2474
2597
  TERMINAL_STATES,
2598
+ UPDATE_OFFER_SINCE,
2475
2599
  WireError,
2476
2600
  WireErrorCode,
2477
2601
  WithheldKind,
@@ -2479,8 +2603,10 @@ export {
2479
2603
  backendName,
2480
2604
  canTransition,
2481
2605
  canonicalRequest,
2606
+ checkDaemonFloor,
2482
2607
  checkProtocolVersion,
2483
2608
  classifyCost,
2609
+ compareVersions,
2484
2610
  cryptoReady,
2485
2611
  declaredVersion,
2486
2612
  effectiveOfferScope,
@@ -2496,6 +2622,7 @@ export {
2496
2622
  keyId,
2497
2623
  kindsOf,
2498
2624
  matchAudience,
2625
+ mayOfferUpdate,
2499
2626
  mustsVerifiedBy,
2500
2627
  open,
2501
2628
  payloadTextLength,