@byollm/protocol 0.1.0-alpha.92 → 0.1.0-alpha.94
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 +1 -1
- package/dist/index.d.ts +290 -1
- package/dist/index.js +217 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -2972,4 +2972,293 @@ declare const MUST_IDS: readonly ("PAIR_ONE_USER" | "PAIR_INTERACTIVE" | "PAIR_C
|
|
|
2972
2972
|
/** Every MUST verified a particular way. */
|
|
2973
2973
|
declare function mustsVerifiedBy(kind: MustVerification): MustId[];
|
|
2974
2974
|
|
|
2975
|
-
|
|
2975
|
+
/**
|
|
2976
|
+
* The terminal stream a hosted box's console runs over — byollm_018 §"Console
|
|
2977
|
+
* redesign: brokered E2E replaces SSH-first (RULED, Todd 2026-09-03)".
|
|
2978
|
+
*
|
|
2979
|
+
* The ruling's item 3 is the whole reason this file exists: *"The terminal
|
|
2980
|
+
* stream is E2E encrypted browser↔agent with the sealed-channel pattern jobs
|
|
2981
|
+
* use; the hub routes ciphertext."* So the bytes are carried by
|
|
2982
|
+
* {@link seal}/{@link open} exactly as a job's payload is, and what is defined
|
|
2983
|
+
* here is the thing a sealed envelope does not carry: **what a frame means,
|
|
2984
|
+
* and where it sits in the stream.**
|
|
2985
|
+
*
|
|
2986
|
+
* ## Why an ordering rule is protocol and not an implementation detail
|
|
2987
|
+
*
|
|
2988
|
+
* Sealing makes the hub unable to READ the stream. It does nothing to make
|
|
2989
|
+
* the hub unable to reshape it. A router that holds ciphertext can drop a
|
|
2990
|
+
* frame, deliver two out of order, or send the same frame twice, and every
|
|
2991
|
+
* envelope still opens, still verifies against the pinned identity, and still
|
|
2992
|
+
* looks perfect to the receiver — because each envelope is authenticated on
|
|
2993
|
+
* its own and says nothing about the ones around it.
|
|
2994
|
+
*
|
|
2995
|
+
* On a terminal that is not a theoretical loss of tidiness. A dropped
|
|
2996
|
+
* keystroke is a different command; two frames swapped is a different command;
|
|
2997
|
+
* a replayed frame is a command run twice. The console carries vendor
|
|
2998
|
+
* sign-in flows (byollm_018 item 5), so "a different command" can mean an auth
|
|
2999
|
+
* code going somewhere it was not typed.
|
|
3000
|
+
*
|
|
3001
|
+
* Hence {@link consoleOrder}: a sequence inside the ciphertext, checked by
|
|
3002
|
+
* both ends. Inside, because a counter the hub could see is a counter the hub
|
|
3003
|
+
* could rewrite.
|
|
3004
|
+
*
|
|
3005
|
+
* ## One definition, both ends, and there are three ends
|
|
3006
|
+
*
|
|
3007
|
+
* The browser, the box's agent, and the hub broker all handle these frames,
|
|
3008
|
+
* and the first two must agree byte for byte or the stream silently corrupts.
|
|
3009
|
+
* The hub must NOT be able to agree — it only ever sees the sealed envelope.
|
|
3010
|
+
* This module is the single definition the two real ends share; neither
|
|
3011
|
+
* retypes it.
|
|
3012
|
+
*
|
|
3013
|
+
* ## The pinned box key is the PAIRING key (CCB's call, 09-16)
|
|
3014
|
+
*
|
|
3015
|
+
* The ruling says "box key pinned at first session", and the backlog left one
|
|
3016
|
+
* delta open: whether the identity the owner already approved at pairing can
|
|
3017
|
+
* serve as that pinned key. It can, and it should, for three reasons.
|
|
3018
|
+
*
|
|
3019
|
+
* 1. **The pairing key is the only one a human ever chose.** `PairStartRequest`
|
|
3020
|
+
* carries the device's {@link PublicIdentity} and says why: *"Pairing is
|
|
3021
|
+
* where the two parties learn each other's identities, because it is the
|
|
3022
|
+
* one moment a human is already deciding to trust: the approval click. A
|
|
3023
|
+
* key exchanged anywhere else would be a key nobody chose."* A fresh
|
|
3024
|
+
* trust-on-first-use pin at the first console session is precisely a key
|
|
3025
|
+
* nobody chose — it trusts whoever answers first, which on a brokered
|
|
3026
|
+
* channel is whoever the broker points at.
|
|
3027
|
+
* 2. **Two pins of one box can disagree, and silently.** A console pin
|
|
3028
|
+
* separate from the pairing pin is a second record of the same fact, which
|
|
3029
|
+
* is this project's most-repeated defect. When they diverge, nothing reads
|
|
3030
|
+
* both, so nothing notices.
|
|
3031
|
+
* 3. **Alarm-on-change comes free from the split already in `keys.ts`.** The
|
|
3032
|
+
* Ed25519 identity is the pinned half and the X25519 encryption key is
|
|
3033
|
+
* signed by it, so {@link verifyPublicIdentity} lets the box rotate its
|
|
3034
|
+
* encryption key without a new ceremony while an IDENTITY change fails to
|
|
3035
|
+
* verify and alarms — which is what the ruling asks for, and it is already
|
|
3036
|
+
* written.
|
|
3037
|
+
*
|
|
3038
|
+
* **What this does not claim.** Pinning the pairing key does not make the hub
|
|
3039
|
+
* unable to lie about which key that was; the hub is the one telling the
|
|
3040
|
+
* browser. The ruling is explicit that we build the operator-ACCOUNTABLE
|
|
3041
|
+
* version, not the operator-incapable one, and item 4's session feed is where
|
|
3042
|
+
* that accountability is paid. What the pin buys is that a CHANGE is
|
|
3043
|
+
* detectable, and that the key being pinned is one an owner approved rather
|
|
3044
|
+
* than one a broker supplied.
|
|
3045
|
+
*/
|
|
3046
|
+
/** Domain tag for the plaintext inside a console envelope. */
|
|
3047
|
+
declare const CONSOLE_FRAME_VERSION = "byollm/v1/console";
|
|
3048
|
+
/**
|
|
3049
|
+
* The largest `data` payload one frame may carry, before base64.
|
|
3050
|
+
*
|
|
3051
|
+
* Far below {@link MAX_ENVELOPE_BYTES}, and deliberately: an envelope cap
|
|
3052
|
+
* stops a memory attack, while this stops a latency one. A console is
|
|
3053
|
+
* interactive, so a peer that batches a megabyte into one frame has made the
|
|
3054
|
+
* stream unusable without ever exceeding a limit.
|
|
3055
|
+
*/
|
|
3056
|
+
declare const CONSOLE_MAX_DATA_BYTES: number;
|
|
3057
|
+
declare const ConsoleHello: z.ZodObject<{
|
|
3058
|
+
v: z.ZodLiteral<"byollm/v1/console">;
|
|
3059
|
+
kind: z.ZodLiteral<"hello">;
|
|
3060
|
+
seq: z.ZodLiteral<1>;
|
|
3061
|
+
browser: z.ZodObject<{
|
|
3062
|
+
identity: z.ZodString;
|
|
3063
|
+
encryption: z.ZodString;
|
|
3064
|
+
encryptionSig: z.ZodString;
|
|
3065
|
+
}, z.core.$strict>;
|
|
3066
|
+
cols: z.ZodNumber;
|
|
3067
|
+
rows: z.ZodNumber;
|
|
3068
|
+
}, z.core.$strict>;
|
|
3069
|
+
type ConsoleHello = z.infer<typeof ConsoleHello>;
|
|
3070
|
+
/**
|
|
3071
|
+
* Two schemas rather than one with a `kind: z.enum([...])`, because the union
|
|
3072
|
+
* below discriminates on `kind` and a discriminator that is itself a set is
|
|
3073
|
+
* how a union quietly stops discriminating.
|
|
3074
|
+
*/
|
|
3075
|
+
declare const ConsoleStdin: z.ZodObject<{
|
|
3076
|
+
v: z.ZodLiteral<"byollm/v1/console">;
|
|
3077
|
+
kind: z.ZodLiteral<"stdin">;
|
|
3078
|
+
seq: z.ZodNumber;
|
|
3079
|
+
data: z.ZodString;
|
|
3080
|
+
}, z.core.$strict>;
|
|
3081
|
+
type ConsoleStdin = z.infer<typeof ConsoleStdin>;
|
|
3082
|
+
declare const ConsoleStdout: z.ZodObject<{
|
|
3083
|
+
v: z.ZodLiteral<"byollm/v1/console">;
|
|
3084
|
+
kind: z.ZodLiteral<"stdout">;
|
|
3085
|
+
seq: z.ZodNumber;
|
|
3086
|
+
data: z.ZodString;
|
|
3087
|
+
}, z.core.$strict>;
|
|
3088
|
+
type ConsoleStdout = z.infer<typeof ConsoleStdout>;
|
|
3089
|
+
declare const ConsoleResize: z.ZodObject<{
|
|
3090
|
+
v: z.ZodLiteral<"byollm/v1/console">;
|
|
3091
|
+
kind: z.ZodLiteral<"resize">;
|
|
3092
|
+
seq: z.ZodNumber;
|
|
3093
|
+
cols: z.ZodNumber;
|
|
3094
|
+
rows: z.ZodNumber;
|
|
3095
|
+
}, z.core.$strict>;
|
|
3096
|
+
type ConsoleResize = z.infer<typeof ConsoleResize>;
|
|
3097
|
+
declare const ConsoleBye: z.ZodObject<{
|
|
3098
|
+
v: z.ZodLiteral<"byollm/v1/console">;
|
|
3099
|
+
kind: z.ZodLiteral<"bye">;
|
|
3100
|
+
seq: z.ZodNumber;
|
|
3101
|
+
reason: z.ZodString;
|
|
3102
|
+
}, z.core.$strict>;
|
|
3103
|
+
type ConsoleBye = z.infer<typeof ConsoleBye>;
|
|
3104
|
+
declare const ConsoleFrame: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
3105
|
+
v: z.ZodLiteral<"byollm/v1/console">;
|
|
3106
|
+
kind: z.ZodLiteral<"hello">;
|
|
3107
|
+
seq: z.ZodLiteral<1>;
|
|
3108
|
+
browser: z.ZodObject<{
|
|
3109
|
+
identity: z.ZodString;
|
|
3110
|
+
encryption: z.ZodString;
|
|
3111
|
+
encryptionSig: z.ZodString;
|
|
3112
|
+
}, z.core.$strict>;
|
|
3113
|
+
cols: z.ZodNumber;
|
|
3114
|
+
rows: z.ZodNumber;
|
|
3115
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3116
|
+
v: z.ZodLiteral<"byollm/v1/console">;
|
|
3117
|
+
kind: z.ZodLiteral<"stdin">;
|
|
3118
|
+
seq: z.ZodNumber;
|
|
3119
|
+
data: z.ZodString;
|
|
3120
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3121
|
+
v: z.ZodLiteral<"byollm/v1/console">;
|
|
3122
|
+
kind: z.ZodLiteral<"stdout">;
|
|
3123
|
+
seq: z.ZodNumber;
|
|
3124
|
+
data: z.ZodString;
|
|
3125
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3126
|
+
v: z.ZodLiteral<"byollm/v1/console">;
|
|
3127
|
+
kind: z.ZodLiteral<"resize">;
|
|
3128
|
+
seq: z.ZodNumber;
|
|
3129
|
+
cols: z.ZodNumber;
|
|
3130
|
+
rows: z.ZodNumber;
|
|
3131
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3132
|
+
v: z.ZodLiteral<"byollm/v1/console">;
|
|
3133
|
+
kind: z.ZodLiteral<"bye">;
|
|
3134
|
+
seq: z.ZodNumber;
|
|
3135
|
+
reason: z.ZodString;
|
|
3136
|
+
}, z.core.$strict>], "kind">;
|
|
3137
|
+
type ConsoleFrame = z.infer<typeof ConsoleFrame>;
|
|
3138
|
+
/**
|
|
3139
|
+
* The envelope context for a console frame.
|
|
3140
|
+
*
|
|
3141
|
+
* Exported so that neither end computes it: the session id goes in `jobId`
|
|
3142
|
+
* and the direction is derived, and if the two ends disagreed about either,
|
|
3143
|
+
* every envelope would fail to open with `not-for-us` — a failure that looks
|
|
3144
|
+
* exactly like an attack.
|
|
3145
|
+
*/
|
|
3146
|
+
declare function consoleEnvelope(input: {
|
|
3147
|
+
sessionId: string;
|
|
3148
|
+
from: "browser" | "box";
|
|
3149
|
+
senderKeyId: string;
|
|
3150
|
+
recipientKeyId: string;
|
|
3151
|
+
deadlineAt: number;
|
|
3152
|
+
}): EnvelopeContext;
|
|
3153
|
+
/** Why a frame was refused. Each one is a distinct thing a router can do. */
|
|
3154
|
+
type ConsoleOrderFault =
|
|
3155
|
+
/** Seq went backwards or repeated — the router sent it twice. */
|
|
3156
|
+
"replayed"
|
|
3157
|
+
/** Seq skipped — the router dropped what was between. */
|
|
3158
|
+
| "gap"
|
|
3159
|
+
/** A `hello` that was not the first frame, or a first frame that was not `hello`. */
|
|
3160
|
+
| "out-of-turn"
|
|
3161
|
+
/** A `stdin` from the box, or a `stdout` from the browser. */
|
|
3162
|
+
| "wrong-way"
|
|
3163
|
+
/** Anything after `bye`. The stream is over. */
|
|
3164
|
+
| "closed";
|
|
3165
|
+
type ConsoleOrderResult = {
|
|
3166
|
+
readonly ok: true;
|
|
3167
|
+
} | {
|
|
3168
|
+
readonly ok: false;
|
|
3169
|
+
readonly fault: ConsoleOrderFault;
|
|
3170
|
+
};
|
|
3171
|
+
/**
|
|
3172
|
+
* One direction's ordering rule, which both ends run over what they receive.
|
|
3173
|
+
*
|
|
3174
|
+
* **Strictly +1, not merely increasing.** Increasing would catch replay and
|
|
3175
|
+
* reorder while letting a DROP through silently, and a dropped frame on a
|
|
3176
|
+
* terminal is a truncated command that still runs. There is no benign gap
|
|
3177
|
+
* here to tolerate: the transport underneath is an ordered, reliable stream,
|
|
3178
|
+
* so a gap means something between the ends removed a frame.
|
|
3179
|
+
*
|
|
3180
|
+
* Every fault is fatal to the session by design. A console has no
|
|
3181
|
+
* resynchronisation story that is safe — you cannot ask "what did I miss" of
|
|
3182
|
+
* a party that may be the one who took it.
|
|
3183
|
+
*/
|
|
3184
|
+
/**
|
|
3185
|
+
* One direction's receiver-side ordering state.
|
|
3186
|
+
*
|
|
3187
|
+
* Named as an interface rather than inferred, so the box's agent and the
|
|
3188
|
+
* browser hold the SAME type — an inferred one would let the two ends drift
|
|
3189
|
+
* apart without a compiler ever objecting.
|
|
3190
|
+
*/
|
|
3191
|
+
interface ConsoleOrder {
|
|
3192
|
+
/** The last sequence accepted. 0 before anything has been. */
|
|
3193
|
+
readonly seen: number;
|
|
3194
|
+
accept(frame: ConsoleFrame): ConsoleOrderResult;
|
|
3195
|
+
}
|
|
3196
|
+
declare function consoleOrder(from: "browser" | "box"): ConsoleOrder;
|
|
3197
|
+
|
|
3198
|
+
/**
|
|
3199
|
+
* The envelope's FORMAT, with no primitives and no platform — B018c step 3.
|
|
3200
|
+
*
|
|
3201
|
+
* ## Why this file exists
|
|
3202
|
+
*
|
|
3203
|
+
* The browser is one of the two ends a console stream is encrypted between,
|
|
3204
|
+
* and it cannot run `envelope.ts`: the sealing is WASM and portable, but the
|
|
3205
|
+
* inner signature goes through `node:crypto`'s Ed25519. So the browser needs
|
|
3206
|
+
* its own primitives.
|
|
3207
|
+
*
|
|
3208
|
+
* What it must NOT have is its own *format*. Two implementations of "which
|
|
3209
|
+
* bytes get signed" is the defect class this codebase spends most of its
|
|
3210
|
+
* checks on, and here the two copies would diverge silently — a mismatched
|
|
3211
|
+
* signature is indistinguishable from an attack, so the first symptom would
|
|
3212
|
+
* be a console that refuses to open and a log line saying `bad-signature`.
|
|
3213
|
+
*
|
|
3214
|
+
* Everything here is therefore pure: no `node:` imports, no `Buffer`, no
|
|
3215
|
+
* `btoa`. `envelope-is-portable.test.ts` asserts that mechanically, because
|
|
3216
|
+
* "somebody will import Buffer for convenience one day" is a prediction, and
|
|
3217
|
+
* this project's rule is that a prediction ships with the test that catches it.
|
|
3218
|
+
*
|
|
3219
|
+
* ## Why base64url is written out by hand
|
|
3220
|
+
*
|
|
3221
|
+
* `Buffer` is Node's and `btoa` takes a binary string, so neither is both
|
|
3222
|
+
* portable and pleasant. Twenty lines of table lookup is: it agrees with
|
|
3223
|
+
* `Buffer` on every byte value, which the test proves rather than assumes.
|
|
3224
|
+
*/
|
|
3225
|
+
/** Unpadded base64url, the encoding every key and signature on this wire uses. */
|
|
3226
|
+
declare function toBase64Url(bytes: Uint8Array): string;
|
|
3227
|
+
/** The inverse. Returns undefined rather than throwing — this parses input. */
|
|
3228
|
+
declare function fromBase64Url(text: string): Uint8Array | undefined;
|
|
3229
|
+
/** Domain tag for the bytes an envelope's signature covers. */
|
|
3230
|
+
declare const ENVELOPE_BODY_VERSION = "byollm/v1/envelope";
|
|
3231
|
+
/** What an envelope's signature is over — the fields, in this order. */
|
|
3232
|
+
interface EnvelopeBodyContext {
|
|
3233
|
+
readonly jobId: string;
|
|
3234
|
+
readonly senderKeyId: string;
|
|
3235
|
+
readonly recipientKeyId: string;
|
|
3236
|
+
readonly deadlineAt: number;
|
|
3237
|
+
readonly direction: string;
|
|
3238
|
+
}
|
|
3239
|
+
/**
|
|
3240
|
+
* The exact bytes an envelope's signature covers.
|
|
3241
|
+
*
|
|
3242
|
+
* **Field order is part of the format**, because `JSON.stringify` emits keys
|
|
3243
|
+
* in insertion order and the verifier hashes bytes, not meaning. Reordering
|
|
3244
|
+
* these lines is a wire change that no type would catch and every signature
|
|
3245
|
+
* would fail — which is why they are written out rather than spread from an
|
|
3246
|
+
* object somebody could reshape.
|
|
3247
|
+
*/
|
|
3248
|
+
declare function envelopeSignedBody(context: EnvelopeBodyContext, plaintext: string): Uint8Array;
|
|
3249
|
+
/** The signed body and its signature, as they travel inside the sealed box. */
|
|
3250
|
+
declare function encodeEnvelopeInner(body: Uint8Array, signature: string): string;
|
|
3251
|
+
interface EnvelopeInner {
|
|
3252
|
+
readonly body: Uint8Array;
|
|
3253
|
+
readonly signature: string;
|
|
3254
|
+
}
|
|
3255
|
+
/**
|
|
3256
|
+
* Read the inner object back.
|
|
3257
|
+
*
|
|
3258
|
+
* Returns undefined for anything malformed rather than throwing, because
|
|
3259
|
+
* every byte reaching this has come out of a decryption and is therefore
|
|
3260
|
+
* input — `malformed` is one of the outcomes `open()` already distinguishes.
|
|
3261
|
+
*/
|
|
3262
|
+
declare function decodeEnvelopeInner(text: string): EnvelopeInner | undefined;
|
|
3263
|
+
|
|
3264
|
+
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, CONSOLE_FRAME_VERSION, CONSOLE_MAX_DATA_BYTES, Capability, CapabilityMatrix, ChatMessage, ChatPayload, ClaimRequest, ClaimResponse, ClaimedJob, ClaimedStub, ConsoleBye, ConsoleFrame, ConsoleHello, type ConsoleOrder, type ConsoleOrderFault, type ConsoleOrderResult, ConsoleResize, ConsoleStdin, ConsoleStdout, DeliveredResult, ENCRYPTION_KEY_CONTEXT, ENDPOINTS, ENVELOPE_BODY_VERSION, ENVELOPE_MAX_AGE_MS, ERROR_STATUS, type Endpoint, type EnvelopeBodyContext, type EnvelopeContext, EnvelopeDirection, type EnvelopeFailure, type EnvelopeInner, 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, REFUSAL_TEXT, 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, type StopReason, StopReasonSchema, StoredKeys, Succession, type SuccessionFailure, type SuccessionWalk, TERMINAL_STATES, UPDATE_OFFER_SINCE, UPGRADE_COMMAND, type VersionRefusal, WireError, WireErrorCode, WithheldKind, backendDescriptor, backendName, canTransition, canonicalRequest, checkDaemonFloor, checkProtocolVersion, classifyCost, compareVersions, consoleEnvelope, consoleOrder, cryptoReady, declaredVersion, decodeEnvelopeInner, describeBytes, effectiveOfferScope, encodeEnvelopeInner, envelopeBytes, envelopeSignedBody, fingerprint, fromBase64Url, generateKeys, grantStatement, isBackendId, isCloudTaggedModel, isJobKind, isLocalHost, isTerminal, keyId, kindsOf, matchAudience, mayOfferUpdate, mentionsWireField, mustsVerifiedBy, open, payloadTextLength, provenanceFor, publicIdentityOf, resolveCost, seal, signGrant, signRequest, signSiteRequest, signSuccession, signWith, singlePurposeManifest, sizeClassCeiling, sizeClassOf, successionStatement, toBase64Url, tooLargeMessage, updateOfferFor, verifyGrant, verifyLink, verifyPublicIdentity, verifyRequest, verifySiteRequest, verifyWith, walkSuccession, withoutComments };
|
package/dist/index.js
CHANGED
|
@@ -89,16 +89,16 @@ function verifyPublicIdentity(identity) {
|
|
|
89
89
|
return false;
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
function signWith(keys,
|
|
93
|
-
return sign(null,
|
|
92
|
+
function signWith(keys, data2) {
|
|
93
|
+
return sign(null, data2, importPrivate(keys.identityPrivate)).toString(
|
|
94
94
|
"base64url"
|
|
95
95
|
);
|
|
96
96
|
}
|
|
97
|
-
function verifyWith(identityPublic,
|
|
97
|
+
function verifyWith(identityPublic, data2, signature) {
|
|
98
98
|
try {
|
|
99
99
|
return verify(
|
|
100
100
|
null,
|
|
101
|
-
|
|
101
|
+
data2,
|
|
102
102
|
importPublic(identityPublic, "Ed25519"),
|
|
103
103
|
Buffer.from(signature, "base64url")
|
|
104
104
|
);
|
|
@@ -1163,6 +1163,85 @@ var ClaimedStub = JobStub.extend({
|
|
|
1163
1163
|
import { createPrivateKey as createPrivateKey2, createPublicKey as createPublicKey2 } from "crypto";
|
|
1164
1164
|
import sodium from "libsodium-wrappers";
|
|
1165
1165
|
import { z as z8 } from "zod";
|
|
1166
|
+
|
|
1167
|
+
// src/envelope-format.ts
|
|
1168
|
+
var ALPHABET2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
1169
|
+
var CHARS = ALPHABET2.split("");
|
|
1170
|
+
var VALUES = /* @__PURE__ */ (() => {
|
|
1171
|
+
const table = new Uint8Array(128).fill(255);
|
|
1172
|
+
for (let at = 0; at < ALPHABET2.length; at += 1) {
|
|
1173
|
+
table[ALPHABET2.charCodeAt(at)] = at;
|
|
1174
|
+
}
|
|
1175
|
+
return table;
|
|
1176
|
+
})();
|
|
1177
|
+
function toBase64Url(bytes) {
|
|
1178
|
+
let out = "";
|
|
1179
|
+
for (let at = 0; at < bytes.length; at += 3) {
|
|
1180
|
+
const a = bytes[at] ?? 0;
|
|
1181
|
+
const b = bytes[at + 1];
|
|
1182
|
+
const c = bytes[at + 2];
|
|
1183
|
+
const word = a << 16 | (b ?? 0) << 8 | (c ?? 0);
|
|
1184
|
+
out += CHARS[word >> 18 & 63] ?? "";
|
|
1185
|
+
out += CHARS[word >> 12 & 63] ?? "";
|
|
1186
|
+
if (b !== void 0) out += CHARS[word >> 6 & 63] ?? "";
|
|
1187
|
+
if (c !== void 0) out += CHARS[word & 63] ?? "";
|
|
1188
|
+
}
|
|
1189
|
+
return out;
|
|
1190
|
+
}
|
|
1191
|
+
function fromBase64Url(text) {
|
|
1192
|
+
const length = text.length;
|
|
1193
|
+
if (length % 4 === 1) return void 0;
|
|
1194
|
+
const bytes = new Uint8Array(Math.floor(length * 3 / 4));
|
|
1195
|
+
let written = 0;
|
|
1196
|
+
let word = 0;
|
|
1197
|
+
let bits = 0;
|
|
1198
|
+
for (let at = 0; at < length; at += 1) {
|
|
1199
|
+
const code = text.charCodeAt(at);
|
|
1200
|
+
const value = code < 128 ? VALUES[code] : 255;
|
|
1201
|
+
if (value === void 0 || value === 255) return void 0;
|
|
1202
|
+
word = word << 6 | value;
|
|
1203
|
+
bits += 6;
|
|
1204
|
+
if (bits >= 8) {
|
|
1205
|
+
bits -= 8;
|
|
1206
|
+
bytes[written] = word >> bits & 255;
|
|
1207
|
+
written += 1;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return bytes.subarray(0, written);
|
|
1211
|
+
}
|
|
1212
|
+
var ENVELOPE_BODY_VERSION = "byollm/v1/envelope";
|
|
1213
|
+
function envelopeSignedBody(context, plaintext) {
|
|
1214
|
+
return new TextEncoder().encode(
|
|
1215
|
+
JSON.stringify({
|
|
1216
|
+
v: ENVELOPE_BODY_VERSION,
|
|
1217
|
+
jobId: context.jobId,
|
|
1218
|
+
senderKeyId: context.senderKeyId,
|
|
1219
|
+
recipientKeyId: context.recipientKeyId,
|
|
1220
|
+
deadlineAt: context.deadlineAt,
|
|
1221
|
+
direction: context.direction,
|
|
1222
|
+
plaintext
|
|
1223
|
+
})
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
function encodeEnvelopeInner(body, signature) {
|
|
1227
|
+
return JSON.stringify({ body: toBase64Url(body), signature });
|
|
1228
|
+
}
|
|
1229
|
+
function decodeEnvelopeInner(text) {
|
|
1230
|
+
let parsed;
|
|
1231
|
+
try {
|
|
1232
|
+
parsed = JSON.parse(text);
|
|
1233
|
+
} catch {
|
|
1234
|
+
return void 0;
|
|
1235
|
+
}
|
|
1236
|
+
if (typeof parsed.body !== "string" || typeof parsed.signature !== "string") {
|
|
1237
|
+
return void 0;
|
|
1238
|
+
}
|
|
1239
|
+
const body = fromBase64Url(parsed.body);
|
|
1240
|
+
if (body === void 0) return void 0;
|
|
1241
|
+
return { body, signature: parsed.signature };
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// src/envelope.ts
|
|
1166
1245
|
var readied;
|
|
1167
1246
|
async function cryptoReady() {
|
|
1168
1247
|
readied ??= sodium.ready;
|
|
@@ -1192,20 +1271,7 @@ var SealedEnvelope = z8.object({
|
|
|
1192
1271
|
*/
|
|
1193
1272
|
deadlineAt: z8.number().int().positive()
|
|
1194
1273
|
}).strict();
|
|
1195
|
-
|
|
1196
|
-
return Buffer.from(
|
|
1197
|
-
JSON.stringify({
|
|
1198
|
-
v: "byollm/v1/envelope",
|
|
1199
|
-
jobId: context.jobId,
|
|
1200
|
-
senderKeyId: context.senderKeyId,
|
|
1201
|
-
recipientKeyId: context.recipientKeyId,
|
|
1202
|
-
deadlineAt: context.deadlineAt,
|
|
1203
|
-
direction: context.direction,
|
|
1204
|
-
plaintext
|
|
1205
|
-
}),
|
|
1206
|
-
"utf8"
|
|
1207
|
-
);
|
|
1208
|
-
}
|
|
1274
|
+
var signedBody = (context, plaintext) => envelopeSignedBody(context, plaintext);
|
|
1209
1275
|
var rawX25519 = (key, part) => {
|
|
1210
1276
|
const jwk = key.export({ format: "jwk" });
|
|
1211
1277
|
const value = part === "x" ? jwk.x : jwk.d;
|
|
@@ -1216,7 +1282,7 @@ async function seal(input) {
|
|
|
1216
1282
|
await cryptoReady();
|
|
1217
1283
|
const body = signedBody(input.context, input.plaintext);
|
|
1218
1284
|
const signature = signWith(input.senderKeys, body);
|
|
1219
|
-
const inner =
|
|
1285
|
+
const inner = encodeEnvelopeInner(body, signature);
|
|
1220
1286
|
const recipient = new Uint8Array(
|
|
1221
1287
|
Buffer.from(input.recipientEncryptionPublic, "base64url")
|
|
1222
1288
|
);
|
|
@@ -1255,22 +1321,15 @@ async function open(input) {
|
|
|
1255
1321
|
} catch {
|
|
1256
1322
|
return { ok: false, reason: "unopenable" };
|
|
1257
1323
|
}
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
} catch {
|
|
1262
|
-
return { ok: false, reason: "malformed" };
|
|
1263
|
-
}
|
|
1264
|
-
if (typeof parsed.body !== "string" || typeof parsed.signature !== "string") {
|
|
1265
|
-
return { ok: false, reason: "malformed" };
|
|
1266
|
-
}
|
|
1267
|
-
const body = Buffer.from(parsed.body, "base64url");
|
|
1324
|
+
const parsed = decodeEnvelopeInner(inner);
|
|
1325
|
+
if (parsed === void 0) return { ok: false, reason: "malformed" };
|
|
1326
|
+
const body = parsed.body;
|
|
1268
1327
|
if (!verifyWith(input.senderIdentityPublic, body, parsed.signature)) {
|
|
1269
1328
|
return { ok: false, reason: "bad-signature" };
|
|
1270
1329
|
}
|
|
1271
1330
|
let claims;
|
|
1272
1331
|
try {
|
|
1273
|
-
claims = JSON.parse(
|
|
1332
|
+
claims = JSON.parse(new TextDecoder().decode(body));
|
|
1274
1333
|
} catch {
|
|
1275
1334
|
return { ok: false, reason: "malformed" };
|
|
1276
1335
|
}
|
|
@@ -2663,6 +2722,118 @@ var MUST_IDS = Object.freeze(Object.keys(MUSTS));
|
|
|
2663
2722
|
function mustsVerifiedBy(kind) {
|
|
2664
2723
|
return MUST_IDS.filter((id) => kindsOf(MUSTS[id]).includes(kind));
|
|
2665
2724
|
}
|
|
2725
|
+
|
|
2726
|
+
// src/console.ts
|
|
2727
|
+
import { z as z12 } from "zod";
|
|
2728
|
+
var CONSOLE_FRAME_VERSION = "byollm/v1/console";
|
|
2729
|
+
var CONSOLE_MAX_DATA_BYTES = 64 * 1024;
|
|
2730
|
+
var data = z12.string().max(Math.ceil(CONSOLE_MAX_DATA_BYTES * 4 / 3) + 4).describe("base64");
|
|
2731
|
+
var cols = z12.number().int().min(1).max(1e4);
|
|
2732
|
+
var rows = z12.number().int().min(1).max(1e4);
|
|
2733
|
+
var seq = z12.number().int().positive();
|
|
2734
|
+
var ConsoleHello = z12.object({
|
|
2735
|
+
v: z12.literal(CONSOLE_FRAME_VERSION),
|
|
2736
|
+
kind: z12.literal("hello"),
|
|
2737
|
+
seq: z12.literal(1),
|
|
2738
|
+
/**
|
|
2739
|
+
* The browser's EPHEMERAL identity for this session, and the reason hello
|
|
2740
|
+
* exists as its own frame.
|
|
2741
|
+
*
|
|
2742
|
+
* A browser has no long-term key — it is a tab, not a machine — so it
|
|
2743
|
+
* generates one per session and the box seals its output to it. That key
|
|
2744
|
+
* has to reach the box without the hub being able to substitute one of its
|
|
2745
|
+
* own, which is the classic broker attack. So it travels SEALED TO THE
|
|
2746
|
+
* BOX'S PINNED IDENTITY like every other frame: the hub cannot read it and
|
|
2747
|
+
* cannot replace it, because replacing it means producing a sealed box
|
|
2748
|
+
* the box's key opens and the browser's key signed.
|
|
2749
|
+
*
|
|
2750
|
+
* The box does not pin this. It cannot — it is new every session, and
|
|
2751
|
+
* nothing authorises it cryptographically. What authorises the browser is
|
|
2752
|
+
* the hub's per-session grant (ruling item 2); what this key does is keep
|
|
2753
|
+
* the hub from READING what it authorised.
|
|
2754
|
+
*/
|
|
2755
|
+
browser: PublicIdentity,
|
|
2756
|
+
cols,
|
|
2757
|
+
rows
|
|
2758
|
+
}).strict();
|
|
2759
|
+
var ConsoleStdin = z12.object({
|
|
2760
|
+
v: z12.literal(CONSOLE_FRAME_VERSION),
|
|
2761
|
+
/** Browser→box only. A `stdin` arriving from the box is `wrong-way`. */
|
|
2762
|
+
kind: z12.literal("stdin"),
|
|
2763
|
+
seq,
|
|
2764
|
+
data
|
|
2765
|
+
}).strict();
|
|
2766
|
+
var ConsoleStdout = z12.object({
|
|
2767
|
+
v: z12.literal(CONSOLE_FRAME_VERSION),
|
|
2768
|
+
/** Box→browser only. */
|
|
2769
|
+
kind: z12.literal("stdout"),
|
|
2770
|
+
seq,
|
|
2771
|
+
data
|
|
2772
|
+
}).strict();
|
|
2773
|
+
var ConsoleResize = z12.object({
|
|
2774
|
+
v: z12.literal(CONSOLE_FRAME_VERSION),
|
|
2775
|
+
kind: z12.literal("resize"),
|
|
2776
|
+
seq,
|
|
2777
|
+
cols,
|
|
2778
|
+
rows
|
|
2779
|
+
}).strict();
|
|
2780
|
+
var ConsoleBye = z12.object({
|
|
2781
|
+
v: z12.literal(CONSOLE_FRAME_VERSION),
|
|
2782
|
+
kind: z12.literal("bye"),
|
|
2783
|
+
seq,
|
|
2784
|
+
/** Shown to the owner in the session feed, so it is a sentence. */
|
|
2785
|
+
reason: z12.string().min(1).max(200)
|
|
2786
|
+
}).strict();
|
|
2787
|
+
var ConsoleFrame = z12.discriminatedUnion("kind", [
|
|
2788
|
+
ConsoleHello,
|
|
2789
|
+
ConsoleStdin,
|
|
2790
|
+
ConsoleStdout,
|
|
2791
|
+
ConsoleResize,
|
|
2792
|
+
ConsoleBye
|
|
2793
|
+
]);
|
|
2794
|
+
function consoleEnvelope(input) {
|
|
2795
|
+
return {
|
|
2796
|
+
jobId: input.sessionId,
|
|
2797
|
+
senderKeyId: input.senderKeyId,
|
|
2798
|
+
recipientKeyId: input.recipientKeyId,
|
|
2799
|
+
deadlineAt: input.deadlineAt,
|
|
2800
|
+
direction: input.from === "browser" ? "payload" : "result"
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
function consoleOrder(from) {
|
|
2804
|
+
const maySend = {
|
|
2805
|
+
// The browser drives: it opens, it types, it sets the size, it leaves.
|
|
2806
|
+
browser: ["hello", "stdin", "resize", "bye"],
|
|
2807
|
+
// The box answers, and can end a session. Nothing else.
|
|
2808
|
+
box: ["stdout", "bye"]
|
|
2809
|
+
};
|
|
2810
|
+
const allowed = maySend[from];
|
|
2811
|
+
const opensWithHello = from === "browser";
|
|
2812
|
+
let last = 0;
|
|
2813
|
+
let closed = false;
|
|
2814
|
+
return {
|
|
2815
|
+
/** The last sequence accepted. 0 before anything has been. */
|
|
2816
|
+
get seen() {
|
|
2817
|
+
return last;
|
|
2818
|
+
},
|
|
2819
|
+
accept(frame) {
|
|
2820
|
+
if (closed) return { ok: false, fault: "closed" };
|
|
2821
|
+
if (!allowed.includes(frame.kind)) {
|
|
2822
|
+
return { ok: false, fault: "wrong-way" };
|
|
2823
|
+
}
|
|
2824
|
+
const isFirst = last === 0;
|
|
2825
|
+
const wantsHello = isFirst && opensWithHello;
|
|
2826
|
+
if (wantsHello !== (frame.kind === "hello")) {
|
|
2827
|
+
return { ok: false, fault: "out-of-turn" };
|
|
2828
|
+
}
|
|
2829
|
+
if (frame.seq <= last) return { ok: false, fault: "replayed" };
|
|
2830
|
+
if (frame.seq !== last + 1) return { ok: false, fault: "gap" };
|
|
2831
|
+
last = frame.seq;
|
|
2832
|
+
if (frame.kind === "bye") closed = true;
|
|
2833
|
+
return { ok: true };
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
}
|
|
2666
2837
|
export {
|
|
2667
2838
|
ABOUT,
|
|
2668
2839
|
ABOUT_SHORT,
|
|
@@ -2678,6 +2849,8 @@ export {
|
|
|
2678
2849
|
BackendIdSchema,
|
|
2679
2850
|
CLOCK_ATTRIBUTION_MS,
|
|
2680
2851
|
CLOCK_SKEW_WARN_MS,
|
|
2852
|
+
CONSOLE_FRAME_VERSION,
|
|
2853
|
+
CONSOLE_MAX_DATA_BYTES,
|
|
2681
2854
|
Capability,
|
|
2682
2855
|
CapabilityMatrix,
|
|
2683
2856
|
ChatMessage,
|
|
@@ -2686,9 +2859,16 @@ export {
|
|
|
2686
2859
|
ClaimResponse,
|
|
2687
2860
|
ClaimedJob,
|
|
2688
2861
|
ClaimedStub,
|
|
2862
|
+
ConsoleBye,
|
|
2863
|
+
ConsoleFrame,
|
|
2864
|
+
ConsoleHello,
|
|
2865
|
+
ConsoleResize,
|
|
2866
|
+
ConsoleStdin,
|
|
2867
|
+
ConsoleStdout,
|
|
2689
2868
|
DeliveredResult,
|
|
2690
2869
|
ENCRYPTION_KEY_CONTEXT,
|
|
2691
2870
|
ENDPOINTS,
|
|
2871
|
+
ENVELOPE_BODY_VERSION,
|
|
2692
2872
|
ENVELOPE_MAX_AGE_MS,
|
|
2693
2873
|
ERROR_STATUS,
|
|
2694
2874
|
EnvelopeDirection,
|
|
@@ -2772,12 +2952,18 @@ export {
|
|
|
2772
2952
|
checkProtocolVersion,
|
|
2773
2953
|
classifyCost,
|
|
2774
2954
|
compareVersions,
|
|
2955
|
+
consoleEnvelope,
|
|
2956
|
+
consoleOrder,
|
|
2775
2957
|
cryptoReady,
|
|
2776
2958
|
declaredVersion,
|
|
2959
|
+
decodeEnvelopeInner,
|
|
2777
2960
|
describeBytes,
|
|
2778
2961
|
effectiveOfferScope,
|
|
2962
|
+
encodeEnvelopeInner,
|
|
2779
2963
|
envelopeBytes,
|
|
2964
|
+
envelopeSignedBody,
|
|
2780
2965
|
fingerprint,
|
|
2966
|
+
fromBase64Url,
|
|
2781
2967
|
generateKeys,
|
|
2782
2968
|
grantStatement,
|
|
2783
2969
|
isBackendId,
|
|
@@ -2806,6 +2992,7 @@ export {
|
|
|
2806
2992
|
sizeClassCeiling,
|
|
2807
2993
|
sizeClassOf,
|
|
2808
2994
|
successionStatement,
|
|
2995
|
+
toBase64Url,
|
|
2809
2996
|
tooLargeMessage,
|
|
2810
2997
|
updateOfferFor,
|
|
2811
2998
|
verifyGrant,
|