@byollm/protocol 0.1.0-alpha.10 → 0.1.0-alpha.101

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.
@@ -0,0 +1,511 @@
1
+ import { z } from 'zod';
2
+
3
+ /**
4
+ * A party's public keys, as they travel — extracted from `keys.ts` for B018c.
5
+ *
6
+ * ## Why it lives on its own
7
+ *
8
+ * The browser end of a console session needs this SCHEMA at runtime, and
9
+ * `keys.ts` is irreducibly node-only: `generateKeyPairSync`,
10
+ * `createPrivateKey`, `createHash`. So every route to the console types
11
+ * dragged the node world into a bundle meant for a tab.
12
+ *
13
+ * Nothing about the shape changed. `keys.ts` re-exports it, so every existing
14
+ * import still resolves and the wire is untouched — this is a file move, and
15
+ * the tests that were passing before it are the proof.
16
+ *
17
+ * Pure zod: no crypto, no node, nothing to make unportable later.
18
+ */
19
+ declare const PublicIdentity: z.ZodObject<{
20
+ identity: z.ZodString;
21
+ encryption: z.ZodString;
22
+ encryptionSig: z.ZodString;
23
+ }, z.core.$strict>;
24
+ type PublicIdentity = z.infer<typeof PublicIdentity>;
25
+
26
+ /**
27
+ * Device and site keys — byollm_009 §3.
28
+ *
29
+ * **Two keypairs per party, and the split is load-bearing.** An Ed25519
30
+ * *identity* key signs; an X25519 *encryption* key receives sealed envelopes.
31
+ * The encryption key is signed by the identity key, and **the identity key is
32
+ * what gets pinned**. So "who sent this" and "who can read this" are answered
33
+ * by different keys — which is what lets an encryption key rotate without
34
+ * re-establishing trust, and what byollm_009 §6's signed-then-sealed envelope
35
+ * depends on.
36
+ *
37
+ * **No new dependency.** byollm_009 §2 says established primitives only, via
38
+ * libsodium. Everything *this* module needs — Ed25519 signing, X25519 key
39
+ * generation — Node provides natively, and using it costs nothing and adds no
40
+ * install weight to a daemon that must land fast on a stranger's laptop.
41
+ *
42
+ * libsodium becomes necessary at envelope v2, where sealing does. That is a
43
+ * real dependency decision and it belongs in the change that needs it: a
44
+ * sealed box is a specific reviewed construction, and rebuilding it out of
45
+ * Node primitives is exactly the "novel construction" §2 rules out. Deferring
46
+ * the dependency is not the same as deferring the rule.
47
+ */
48
+ /** A public identity, as it travels on the wire. All values base64url. */
49
+ /** Private key material, as stored on disk. Never leaves the machine. */
50
+ declare const StoredKeys: z.ZodObject<{
51
+ version: z.ZodLiteral<1>;
52
+ identityPublic: z.ZodString;
53
+ identityPrivate: z.ZodString;
54
+ encryptionPublic: z.ZodString;
55
+ encryptionPrivate: z.ZodString;
56
+ encryptionSig: z.ZodString;
57
+ createdAt: z.ZodNumber;
58
+ }, z.core.$strict>;
59
+ type StoredKeys = z.infer<typeof StoredKeys>;
60
+ /** Domain separator, so a signature over an encryption key cannot be
61
+ * replayed as a signature over anything else. */
62
+ /**
63
+ * What an encryption key's signature covers.
64
+ *
65
+ * Exported because a rotation is a real event this protocol has to be able to
66
+ * *test* — a record whose encryption key moved under an identity that signed
67
+ * the move is the one case pinning must refuse loudly, and building one
68
+ * outside this file otherwise means re-typing this string, which is how two
69
+ * copies of a constant start disagreeing.
70
+ */
71
+ declare const ENCRYPTION_KEY_CONTEXT = "byollm/v1/encryption-key";
72
+ /** Generate a fresh pair of keypairs and bind them together. */
73
+ declare function generateKeys(now: number): StoredKeys;
74
+ /** The public half, for the wire. */
75
+ declare function publicIdentityOf(keys: StoredKeys): PublicIdentity;
76
+ /**
77
+ * Check that an encryption key really belongs to the identity presenting it.
78
+ *
79
+ * Called on everything received, including from an upstream we otherwise
80
+ * trust — the point of pinning the identity is that nothing else needs to be
81
+ * trusted, and that only holds if this is checked every time rather than at
82
+ * first sight.
83
+ */
84
+ declare function verifyPublicIdentity(identity: PublicIdentity): boolean;
85
+ /** Sign arbitrary bytes with an identity key. */
86
+ /**
87
+ * Sign bytes with an identity key.
88
+ *
89
+ * Takes only the private half it uses. A signer that demanded a whole
90
+ * {@link StoredKeys} would make every caller hold an encryption keypair for a
91
+ * job that has no encryption in it — and the control plane, which signs
92
+ * rosters and opens nothing, would be generating and storing secret material
93
+ * it can never need. Every existing caller passes a full `StoredKeys`, which
94
+ * satisfies this.
95
+ */
96
+ declare function signWith(keys: Pick<StoredKeys, "identityPrivate">, data: Uint8Array): string;
97
+ /** Verify bytes against a raw Ed25519 public key. */
98
+ declare function verifyWith(identityPublic: string, data: Uint8Array, signature: string): boolean;
99
+ /**
100
+ * A fingerprint a human can compare out loud.
101
+ *
102
+ * 120 bits of SHA-256 over the raw identity key, as six groups of four. Long
103
+ * enough that grinding a colliding key is not worth anyone's afternoon, short
104
+ * enough to read down a phone line — which is the whole point. A fingerprint
105
+ * nobody can be bothered to compare provides no security at all, so
106
+ * legibility is a security property here, not a nicety.
107
+ *
108
+ * Formatted with a `BYOLLM-` prefix so a pasted fingerprint is recognisable
109
+ * out of context, in a support thread or a screenshot.
110
+ */
111
+ declare function fingerprint(identityPublic: string): string;
112
+ /** The short id used in envelopes and provenance. Stable, and comparable. */
113
+ declare const keyId: (identityPublic: string) => string;
114
+
115
+ declare function cryptoReady(): Promise<void>;
116
+ /**
117
+ * How long a sealed payload is worth keeping, from creation.
118
+ *
119
+ * Bound into every envelope and recomputed when one is opened, so it lives
120
+ * here rather than in the two places that need it. Two copies of a value the
121
+ * signature depends on is the same bug as two clock readings: it works until
122
+ * they disagree, and then nothing can be opened.
123
+ *
124
+ * Not a job's TTL. That answers how long the *work* is worth doing, belongs
125
+ * to the app and the store, and may legitimately differ per deployment.
126
+ */
127
+ declare const ENVELOPE_MAX_AGE_MS: number;
128
+ /** Which leg an envelope belongs to. Bound into the signature. */
129
+ declare const EnvelopeDirection: z.ZodEnum<{
130
+ payload: "payload";
131
+ result: "result";
132
+ }>;
133
+ type EnvelopeDirection = z.infer<typeof EnvelopeDirection>;
134
+ declare const SealedEnvelope: z.ZodObject<{
135
+ ciphertext: z.ZodString;
136
+ recipientKeyId: z.ZodString;
137
+ senderKeyId: z.ZodString;
138
+ direction: z.ZodEnum<{
139
+ payload: "payload";
140
+ result: "result";
141
+ }>;
142
+ deadlineAt: z.ZodNumber;
143
+ }, z.core.$strict>;
144
+ type SealedEnvelope = z.infer<typeof SealedEnvelope>;
145
+ /** Everything the signature covers besides the plaintext itself. */
146
+ interface EnvelopeContext {
147
+ readonly jobId: string;
148
+ readonly senderKeyId: string;
149
+ readonly recipientKeyId: string;
150
+ readonly deadlineAt: number;
151
+ readonly direction: EnvelopeDirection;
152
+ }
153
+ /** Seal a plaintext to a recipient, signed by the sender's identity. */
154
+ declare function seal(input: {
155
+ plaintext: string;
156
+ senderKeys: StoredKeys;
157
+ recipientEncryptionPublic: string;
158
+ context: EnvelopeContext;
159
+ }): Promise<SealedEnvelope>;
160
+ /** Why an envelope was refused. Never distinguished to a remote caller. */
161
+ type EnvelopeFailure = "not-for-us" | "unopenable" | "malformed" | "bad-signature" | "context-mismatch";
162
+ type OpenResult = {
163
+ readonly ok: true;
164
+ readonly plaintext: string;
165
+ } | {
166
+ readonly ok: false;
167
+ readonly reason: EnvelopeFailure;
168
+ };
169
+ /**
170
+ * Open an envelope and verify it came from the pinned sender.
171
+ *
172
+ * Every failure returns rather than throws: this runs on input from the
173
+ * network, and a crash here is a denial of service on the delivery path.
174
+ *
175
+ * The context is checked against the signature, not merely read from the
176
+ * envelope. An envelope carries its own claims about who sent it and to
177
+ * whom — believing those would authenticate the attacker's assertion rather
178
+ * than the sender's key.
179
+ */
180
+ declare function open(input: {
181
+ envelope: SealedEnvelope;
182
+ recipientKeys: StoredKeys;
183
+ senderIdentityPublic: string;
184
+ /** The deadline is taken from the envelope and checked against its signature. */
185
+ expected: Omit<EnvelopeContext, "deadlineAt">;
186
+ }): Promise<OpenResult>;
187
+
188
+ /**
189
+ * The terminal stream a hosted box's console runs over — byollm_018 §"Console
190
+ * redesign: brokered E2E replaces SSH-first (RULED, Todd 2026-09-03)".
191
+ *
192
+ * The ruling's item 3 is the whole reason this file exists: *"The terminal
193
+ * stream is E2E encrypted browser↔agent with the sealed-channel pattern jobs
194
+ * use; the hub routes ciphertext."* So the bytes are carried by
195
+ * {@link seal}/{@link open} exactly as a job's payload is, and what is defined
196
+ * here is the thing a sealed envelope does not carry: **what a frame means,
197
+ * and where it sits in the stream.**
198
+ *
199
+ * ## Why an ordering rule is protocol and not an implementation detail
200
+ *
201
+ * Sealing makes the hub unable to READ the stream. It does nothing to make
202
+ * the hub unable to reshape it. A router that holds ciphertext can drop a
203
+ * frame, deliver two out of order, or send the same frame twice, and every
204
+ * envelope still opens, still verifies against the pinned identity, and still
205
+ * looks perfect to the receiver — because each envelope is authenticated on
206
+ * its own and says nothing about the ones around it.
207
+ *
208
+ * On a terminal that is not a theoretical loss of tidiness. A dropped
209
+ * keystroke is a different command; two frames swapped is a different command;
210
+ * a replayed frame is a command run twice. The console carries vendor
211
+ * sign-in flows (byollm_018 item 5), so "a different command" can mean an auth
212
+ * code going somewhere it was not typed.
213
+ *
214
+ * Hence {@link consoleOrder}: a sequence inside the ciphertext, checked by
215
+ * both ends. Inside, because a counter the hub could see is a counter the hub
216
+ * could rewrite.
217
+ *
218
+ * ## One definition, both ends, and there are three ends
219
+ *
220
+ * The browser, the box's agent, and the hub broker all handle these frames,
221
+ * and the first two must agree byte for byte or the stream silently corrupts.
222
+ * The hub must NOT be able to agree — it only ever sees the sealed envelope.
223
+ * This module is the single definition the two real ends share; neither
224
+ * retypes it.
225
+ *
226
+ * ## The pinned box key is the PAIRING key (CCB's call, 09-16)
227
+ *
228
+ * The ruling says "box key pinned at first session", and the backlog left one
229
+ * delta open: whether the identity the owner already approved at pairing can
230
+ * serve as that pinned key. It can, and it should, for three reasons.
231
+ *
232
+ * 1. **The pairing key is the only one a human ever chose.** `PairStartRequest`
233
+ * carries the device's {@link PublicIdentity} and says why: *"Pairing is
234
+ * where the two parties learn each other's identities, because it is the
235
+ * one moment a human is already deciding to trust: the approval click. A
236
+ * key exchanged anywhere else would be a key nobody chose."* A fresh
237
+ * trust-on-first-use pin at the first console session is precisely a key
238
+ * nobody chose — it trusts whoever answers first, which on a brokered
239
+ * channel is whoever the broker points at.
240
+ * 2. **Two pins of one box can disagree, and silently.** A console pin
241
+ * separate from the pairing pin is a second record of the same fact, which
242
+ * is this project's most-repeated defect. When they diverge, nothing reads
243
+ * both, so nothing notices.
244
+ * 3. **Alarm-on-change comes free from the split already in `keys.ts`.** The
245
+ * Ed25519 identity is the pinned half and the X25519 encryption key is
246
+ * signed by it, so {@link verifyPublicIdentity} lets the box rotate its
247
+ * encryption key without a new ceremony while an IDENTITY change fails to
248
+ * verify and alarms — which is what the ruling asks for, and it is already
249
+ * written.
250
+ *
251
+ * **What this does not claim.** Pinning the pairing key does not make the hub
252
+ * unable to lie about which key that was; the hub is the one telling the
253
+ * browser. The ruling is explicit that we build the operator-ACCOUNTABLE
254
+ * version, not the operator-incapable one, and item 4's session feed is where
255
+ * that accountability is paid. What the pin buys is that a CHANGE is
256
+ * detectable, and that the key being pinned is one an owner approved rather
257
+ * than one a broker supplied.
258
+ */
259
+ /** Domain tag for the plaintext inside a console envelope. */
260
+ declare const CONSOLE_FRAME_VERSION = "byollm/v1/console";
261
+ /**
262
+ * The largest `data` payload one frame may carry, before base64.
263
+ *
264
+ * Far below {@link MAX_ENVELOPE_BYTES}, and deliberately: an envelope cap
265
+ * stops a memory attack, while this stops a latency one. A console is
266
+ * interactive, so a peer that batches a megabyte into one frame has made the
267
+ * stream unusable without ever exceeding a limit.
268
+ */
269
+ declare const CONSOLE_MAX_DATA_BYTES: number;
270
+ /**
271
+ * Encode one frame's payload. Canonical, and the only encoder either end may
272
+ * use — see {@link decodeConsoleData} for what retyping it cost.
273
+ */
274
+ declare function encodeConsoleData(bytes: Uint8Array): string;
275
+ /**
276
+ * Decode one frame's payload, accepting either base64 alphabet.
277
+ *
278
+ * The two alphabets disagree on three characters — `+`, `/`, and the `=`
279
+ * padding — and a box that encoded with Node's standard base64 produced
280
+ * frames that sealed, opened, verified against the pinned identity, and
281
+ * ordered correctly, and then decoded to nothing in a strict base64url
282
+ * reader. Nothing faulted, because a decode that returns nothing is not a
283
+ * fault anywhere in this file. What an operator saw was a console that ate
284
+ * every keystroke and roughly two output chunks in three: one typed character
285
+ * is one byte, which always pads, while a longer chunk survives exactly when
286
+ * its length is a multiple of three and its bytes happen to avoid `+` and `/`.
287
+ *
288
+ * So the tolerance here is deliberate, not lax. A box is software on someone
289
+ * else's machine, and a browser that accepted only the canonical spelling
290
+ * would fix the console for whoever upgraded and for nobody else. Anything
291
+ * that is neither spelling still returns `undefined`, and {@link ConsoleFrame}
292
+ * rejects it at the schema so it fails loudly rather than vanishing.
293
+ */
294
+ declare function decodeConsoleData(text: string): Uint8Array | undefined;
295
+ /**
296
+ * The bytes of a payload that has already passed {@link ConsoleFrame}.
297
+ *
298
+ * Total on purpose. The schema refuses a payload that cannot be read, so both
299
+ * ends would otherwise carry a fallback arm that no input can reach — and an
300
+ * arm nothing can reach is an arm nothing can test. The guarantee lives here,
301
+ * once, where a test can hold it to both answers.
302
+ */
303
+ declare function consoleDataBytes(data: string): Uint8Array;
304
+ declare const ConsoleHello: z.ZodObject<{
305
+ v: z.ZodLiteral<"byollm/v1/console">;
306
+ kind: z.ZodLiteral<"hello">;
307
+ seq: z.ZodLiteral<1>;
308
+ browser: z.ZodObject<{
309
+ identity: z.ZodString;
310
+ encryption: z.ZodString;
311
+ encryptionSig: z.ZodString;
312
+ }, z.core.$strict>;
313
+ cols: z.ZodNumber;
314
+ rows: z.ZodNumber;
315
+ }, z.core.$strict>;
316
+ type ConsoleHello = z.infer<typeof ConsoleHello>;
317
+ /**
318
+ * Two schemas rather than one with a `kind: z.enum([...])`, because the union
319
+ * below discriminates on `kind` and a discriminator that is itself a set is
320
+ * how a union quietly stops discriminating.
321
+ */
322
+ declare const ConsoleStdin: z.ZodObject<{
323
+ v: z.ZodLiteral<"byollm/v1/console">;
324
+ kind: z.ZodLiteral<"stdin">;
325
+ seq: z.ZodNumber;
326
+ data: z.ZodString;
327
+ }, z.core.$strict>;
328
+ type ConsoleStdin = z.infer<typeof ConsoleStdin>;
329
+ declare const ConsoleStdout: z.ZodObject<{
330
+ v: z.ZodLiteral<"byollm/v1/console">;
331
+ kind: z.ZodLiteral<"stdout">;
332
+ seq: z.ZodNumber;
333
+ data: z.ZodString;
334
+ }, z.core.$strict>;
335
+ type ConsoleStdout = z.infer<typeof ConsoleStdout>;
336
+ declare const ConsoleResize: z.ZodObject<{
337
+ v: z.ZodLiteral<"byollm/v1/console">;
338
+ kind: z.ZodLiteral<"resize">;
339
+ seq: z.ZodNumber;
340
+ cols: z.ZodNumber;
341
+ rows: z.ZodNumber;
342
+ }, z.core.$strict>;
343
+ type ConsoleResize = z.infer<typeof ConsoleResize>;
344
+ declare const ConsoleBye: z.ZodObject<{
345
+ v: z.ZodLiteral<"byollm/v1/console">;
346
+ kind: z.ZodLiteral<"bye">;
347
+ seq: z.ZodNumber;
348
+ reason: z.ZodString;
349
+ }, z.core.$strict>;
350
+ type ConsoleBye = z.infer<typeof ConsoleBye>;
351
+ declare const ConsoleFrame: z.ZodDiscriminatedUnion<[z.ZodObject<{
352
+ v: z.ZodLiteral<"byollm/v1/console">;
353
+ kind: z.ZodLiteral<"hello">;
354
+ seq: z.ZodLiteral<1>;
355
+ browser: z.ZodObject<{
356
+ identity: z.ZodString;
357
+ encryption: z.ZodString;
358
+ encryptionSig: z.ZodString;
359
+ }, z.core.$strict>;
360
+ cols: z.ZodNumber;
361
+ rows: z.ZodNumber;
362
+ }, z.core.$strict>, z.ZodObject<{
363
+ v: z.ZodLiteral<"byollm/v1/console">;
364
+ kind: z.ZodLiteral<"stdin">;
365
+ seq: z.ZodNumber;
366
+ data: z.ZodString;
367
+ }, z.core.$strict>, z.ZodObject<{
368
+ v: z.ZodLiteral<"byollm/v1/console">;
369
+ kind: z.ZodLiteral<"stdout">;
370
+ seq: z.ZodNumber;
371
+ data: z.ZodString;
372
+ }, z.core.$strict>, z.ZodObject<{
373
+ v: z.ZodLiteral<"byollm/v1/console">;
374
+ kind: z.ZodLiteral<"resize">;
375
+ seq: z.ZodNumber;
376
+ cols: z.ZodNumber;
377
+ rows: z.ZodNumber;
378
+ }, z.core.$strict>, z.ZodObject<{
379
+ v: z.ZodLiteral<"byollm/v1/console">;
380
+ kind: z.ZodLiteral<"bye">;
381
+ seq: z.ZodNumber;
382
+ reason: z.ZodString;
383
+ }, z.core.$strict>], "kind">;
384
+ type ConsoleFrame = z.infer<typeof ConsoleFrame>;
385
+ /**
386
+ * The envelope context for a console frame.
387
+ *
388
+ * Exported so that neither end computes it: the session id goes in `jobId`
389
+ * and the direction is derived, and if the two ends disagreed about either,
390
+ * every envelope would fail to open with `not-for-us` — a failure that looks
391
+ * exactly like an attack.
392
+ */
393
+ declare function consoleEnvelope(input: {
394
+ sessionId: string;
395
+ from: "browser" | "box";
396
+ senderKeyId: string;
397
+ recipientKeyId: string;
398
+ deadlineAt: number;
399
+ }): EnvelopeContext;
400
+ /** Why a frame was refused. Each one is a distinct thing a router can do. */
401
+ type ConsoleOrderFault =
402
+ /** Seq went backwards or repeated — the router sent it twice. */
403
+ "replayed"
404
+ /** Seq skipped — the router dropped what was between. */
405
+ | "gap"
406
+ /** A `hello` that was not the first frame, or a first frame that was not `hello`. */
407
+ | "out-of-turn"
408
+ /** A `stdin` from the box, or a `stdout` from the browser. */
409
+ | "wrong-way"
410
+ /** Anything after `bye`. The stream is over. */
411
+ | "closed";
412
+ type ConsoleOrderResult = {
413
+ readonly ok: true;
414
+ } | {
415
+ readonly ok: false;
416
+ readonly fault: ConsoleOrderFault;
417
+ };
418
+ /**
419
+ * One direction's ordering rule, which both ends run over what they receive.
420
+ *
421
+ * **Strictly +1, not merely increasing.** Increasing would catch replay and
422
+ * reorder while letting a DROP through silently, and a dropped frame on a
423
+ * terminal is a truncated command that still runs. There is no benign gap
424
+ * here to tolerate: the transport underneath is an ordered, reliable stream,
425
+ * so a gap means something between the ends removed a frame.
426
+ *
427
+ * Every fault is fatal to the session by design. A console has no
428
+ * resynchronisation story that is safe — you cannot ask "what did I miss" of
429
+ * a party that may be the one who took it.
430
+ */
431
+ /**
432
+ * One direction's receiver-side ordering state.
433
+ *
434
+ * Named as an interface rather than inferred, so the box's agent and the
435
+ * browser hold the SAME type — an inferred one would let the two ends drift
436
+ * apart without a compiler ever objecting.
437
+ */
438
+ interface ConsoleOrder {
439
+ /** The last sequence accepted. 0 before anything has been. */
440
+ readonly seen: number;
441
+ accept(frame: ConsoleFrame): ConsoleOrderResult;
442
+ }
443
+ declare function consoleOrder(from: "browser" | "box"): ConsoleOrder;
444
+
445
+ /**
446
+ * The envelope's FORMAT, with no primitives and no platform — B018c step 3.
447
+ *
448
+ * ## Why this file exists
449
+ *
450
+ * The browser is one of the two ends a console stream is encrypted between,
451
+ * and it cannot run `envelope.ts`: the sealing is WASM and portable, but the
452
+ * inner signature goes through `node:crypto`'s Ed25519. So the browser needs
453
+ * its own primitives.
454
+ *
455
+ * What it must NOT have is its own *format*. Two implementations of "which
456
+ * bytes get signed" is the defect class this codebase spends most of its
457
+ * checks on, and here the two copies would diverge silently — a mismatched
458
+ * signature is indistinguishable from an attack, so the first symptom would
459
+ * be a console that refuses to open and a log line saying `bad-signature`.
460
+ *
461
+ * Everything here is therefore pure: no `node:` imports, no `Buffer`, no
462
+ * `btoa`. `envelope-is-portable.test.ts` asserts that mechanically, because
463
+ * "somebody will import Buffer for convenience one day" is a prediction, and
464
+ * this project's rule is that a prediction ships with the test that catches it.
465
+ *
466
+ * ## Why base64url is written out by hand
467
+ *
468
+ * `Buffer` is Node's and `btoa` takes a binary string, so neither is both
469
+ * portable and pleasant. Twenty lines of table lookup is: it agrees with
470
+ * `Buffer` on every byte value, which the test proves rather than assumes.
471
+ */
472
+ /** Unpadded base64url, the encoding every key and signature on this wire uses. */
473
+ declare function toBase64Url(bytes: Uint8Array): string;
474
+ /** The inverse. Returns undefined rather than throwing — this parses input. */
475
+ declare function fromBase64Url(text: string): Uint8Array | undefined;
476
+ /** Domain tag for the bytes an envelope's signature covers. */
477
+ declare const ENVELOPE_BODY_VERSION = "byollm/v1/envelope";
478
+ /** What an envelope's signature is over — the fields, in this order. */
479
+ interface EnvelopeBodyContext {
480
+ readonly jobId: string;
481
+ readonly senderKeyId: string;
482
+ readonly recipientKeyId: string;
483
+ readonly deadlineAt: number;
484
+ readonly direction: string;
485
+ }
486
+ /**
487
+ * The exact bytes an envelope's signature covers.
488
+ *
489
+ * **Field order is part of the format**, because `JSON.stringify` emits keys
490
+ * in insertion order and the verifier hashes bytes, not meaning. Reordering
491
+ * these lines is a wire change that no type would catch and every signature
492
+ * would fail — which is why they are written out rather than spread from an
493
+ * object somebody could reshape.
494
+ */
495
+ declare function envelopeSignedBody(context: EnvelopeBodyContext, plaintext: string): Uint8Array;
496
+ /** The signed body and its signature, as they travel inside the sealed box. */
497
+ declare function encodeEnvelopeInner(body: Uint8Array, signature: string): string;
498
+ interface EnvelopeInner {
499
+ readonly body: Uint8Array;
500
+ readonly signature: string;
501
+ }
502
+ /**
503
+ * Read the inner object back.
504
+ *
505
+ * Returns undefined for anything malformed rather than throwing, because
506
+ * every byte reaching this has come out of a decryption and is therefore
507
+ * input — `malformed` is one of the outcomes `open()` already distinguishes.
508
+ */
509
+ declare function decodeEnvelopeInner(text: string): EnvelopeInner | undefined;
510
+
511
+ export { envelopeSignedBody as A, fingerprint as B, CONSOLE_FRAME_VERSION as C, fromBase64Url as D, ENCRYPTION_KEY_CONTEXT as E, generateKeys as F, keyId as G, open as H, publicIdentityOf as I, seal as J, signWith as K, toBase64Url as L, verifyPublicIdentity as M, verifyWith as N, type OpenResult as O, PublicIdentity as P, StoredKeys as S, CONSOLE_MAX_DATA_BYTES as a, ConsoleBye as b, ConsoleFrame as c, ConsoleHello as d, type ConsoleOrder as e, type ConsoleOrderFault as f, type ConsoleOrderResult as g, ConsoleResize as h, ConsoleStdin as i, ConsoleStdout as j, ENVELOPE_BODY_VERSION as k, ENVELOPE_MAX_AGE_MS as l, type EnvelopeBodyContext as m, type EnvelopeContext as n, EnvelopeDirection as o, type EnvelopeFailure as p, type EnvelopeInner as q, SealedEnvelope as r, consoleDataBytes as s, consoleEnvelope as t, consoleOrder as u, cryptoReady as v, decodeConsoleData as w, decodeEnvelopeInner as x, encodeConsoleData as y, encodeEnvelopeInner as z };
@@ -0,0 +1,2 @@
1
+ export { C as CONSOLE_FRAME_VERSION, a as CONSOLE_MAX_DATA_BYTES, b as ConsoleBye, c as ConsoleFrame, d as ConsoleHello, e as ConsoleOrder, f as ConsoleOrderFault, g as ConsoleOrderResult, h as ConsoleResize, i as ConsoleStdin, j as ConsoleStdout, k as ENVELOPE_BODY_VERSION, m as EnvelopeBodyContext, q as EnvelopeInner, P as PublicIdentity, s as consoleDataBytes, t as consoleEnvelope, u as consoleOrder, w as decodeConsoleData, x as decodeEnvelopeInner, y as encodeConsoleData, z as encodeEnvelopeInner, A as envelopeSignedBody, D as fromBase64Url, L as toBase64Url } from './portable-C6rfiCXi.js';
2
+ import 'zod';
@@ -0,0 +1,45 @@
1
+ import {
2
+ CONSOLE_FRAME_VERSION,
3
+ CONSOLE_MAX_DATA_BYTES,
4
+ ConsoleBye,
5
+ ConsoleFrame,
6
+ ConsoleHello,
7
+ ConsoleResize,
8
+ ConsoleStdin,
9
+ ConsoleStdout,
10
+ ENVELOPE_BODY_VERSION,
11
+ PublicIdentity,
12
+ consoleDataBytes,
13
+ consoleEnvelope,
14
+ consoleOrder,
15
+ decodeConsoleData,
16
+ decodeEnvelopeInner,
17
+ encodeConsoleData,
18
+ encodeEnvelopeInner,
19
+ envelopeSignedBody,
20
+ fromBase64Url,
21
+ toBase64Url
22
+ } from "./chunk-J3HTAGMX.js";
23
+ export {
24
+ CONSOLE_FRAME_VERSION,
25
+ CONSOLE_MAX_DATA_BYTES,
26
+ ConsoleBye,
27
+ ConsoleFrame,
28
+ ConsoleHello,
29
+ ConsoleResize,
30
+ ConsoleStdin,
31
+ ConsoleStdout,
32
+ ENVELOPE_BODY_VERSION,
33
+ PublicIdentity,
34
+ consoleDataBytes,
35
+ consoleEnvelope,
36
+ consoleOrder,
37
+ decodeConsoleData,
38
+ decodeEnvelopeInner,
39
+ encodeConsoleData,
40
+ encodeEnvelopeInner,
41
+ envelopeSignedBody,
42
+ fromBase64Url,
43
+ toBase64Url
44
+ };
45
+ //# sourceMappingURL=portable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byollm/protocol",
3
- "version": "0.1.0-alpha.10",
3
+ "version": "0.1.0-alpha.101",
4
4
  "description": "The BYOLLM wire contract — types, zod schemas, and the audience rules both the daemon and server enforce. ALPHA: under active development.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,13 +8,19 @@
8
8
  ".": {
9
9
  "types": "./dist/index.d.ts",
10
10
  "import": "./dist/index.js"
11
+ },
12
+ "./portable": {
13
+ "types": "./dist/portable.d.ts",
14
+ "import": "./dist/portable.js"
11
15
  }
12
16
  },
13
17
  "main": "./dist/index.js",
14
18
  "types": "./dist/index.d.ts",
15
19
  "files": [
16
20
  "dist",
17
- "README.md"
21
+ "README.md",
22
+ "ABOUT.md",
23
+ "ABOUT-SHORT.md"
18
24
  ],
19
25
  "engines": {
20
26
  "node": ">=22.14"