@toon-protocol/core 3.3.0 → 3.5.0

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,638 @@
1
+ /**
2
+ * Canonical OER length primitives (RFC-0030), ported byte-for-byte from
3
+ * `connector_domain::oer` so the Rust and TypeScript fleets agree on the wire.
4
+ *
5
+ * "Canonical" is the whole point (ADR 0023): every value has exactly ONE
6
+ * encoding, and a decoder that quietly accepts a second spelling of the same
7
+ * value is a decoder two implementations can disagree through. So a
8
+ * determinant is refused unless the bytes it introduces are exactly what
9
+ * {@link encodeVarUint} would have produced:
10
+ *
11
+ * - `0x81 0x03` — a long form for `3`, which `0x03` already encodes — is
12
+ * {@link OerErrorKind.NonCanonicalLength}, not `3`.
13
+ * - `0x80` — a zero-byte long form aliasing `0x00` — is
14
+ * {@link OerErrorKind.NonCanonicalLength}, not `0`.
15
+ * - `0x89 …` — nine length bytes, which cannot fit a `u64` without silently
16
+ * discarding the high-order ones — is
17
+ * {@link OerErrorKind.LengthDeterminantOverflow}, not a truncated read.
18
+ *
19
+ * Values are `bigint` because the wire type is `u64` and `number` cannot hold
20
+ * one; the caller narrows.
21
+ *
22
+ * Pure: no I/O, no keys, no clock.
23
+ */
24
+ /** The three, and only three, ways a length determinant can be refused. */
25
+ declare enum OerErrorKind {
26
+ /** The buffer ends before the field it declares does. */
27
+ BufferUnderflow = "buffer_underflow",
28
+ /** A determinant that is not the minimal encoding of its own value. */
29
+ NonCanonicalLength = "non_canonical_length",
30
+ /** A determinant wider than eight bytes (cannot be a `u64`). */
31
+ LengthDeterminantOverflow = "length_determinant_overflow"
32
+ }
33
+ /** A refusal from the length primitives, carrying which of the three it is. */
34
+ declare class OerError extends Error {
35
+ readonly kind: OerErrorKind;
36
+ constructor(kind: OerErrorKind);
37
+ }
38
+ /** Encode a VarUInt: `0..=127` as one byte, `128+` as a length-prefixed BE value. */
39
+ declare function encodeVarUint(value: bigint): Uint8Array;
40
+ /** A decoded field: its value, and how many bytes it consumed. */
41
+ interface Decoded<T> {
42
+ value: T;
43
+ consumed: number;
44
+ }
45
+ /**
46
+ * Decode a VarUInt at `offset`, refusing any non-canonical spelling.
47
+ *
48
+ * @throws {OerError} and nothing else — arbitrary bytes never produce a
49
+ * `RangeError`, a `TypeError`, or a plausible-but-wrong value.
50
+ */
51
+ declare function decodeVarUint(buf: Uint8Array, offset: number): Decoded<bigint>;
52
+ /** Encode a VarOctetString: a VarUInt length prefix followed by the bytes. */
53
+ declare function encodeVarOctetString(data: Uint8Array): Uint8Array;
54
+ /**
55
+ * Decode a VarOctetString at `offset`.
56
+ *
57
+ * @throws {OerError} and nothing else.
58
+ */
59
+ declare function decodeVarOctetString(buf: Uint8Array, offset: number): Decoded<Uint8Array>;
60
+
61
+ /**
62
+ * The structured envelope (ADR 0018): what a packet carries between a
63
+ * connector and the app behind it, encoded with the same OER primitives an ILP
64
+ * packet already uses rather than as HTTP text.
65
+ *
66
+ * A faithful port of `connector_domain::envelope`
67
+ * (`crates/connector-domain/src/envelope.rs`) — a method, target, ordered
68
+ * headers and body going in; a two-byte big-endian status, headers and body
69
+ * coming back. `vectors/wire-vectors.json` is the contract this is checked
70
+ * against, and `wire-vectors.test.ts` is where that check happens; the prose
71
+ * here describes the same thing but the bytes decide.
72
+ *
73
+ * ─── Why not HTTP text ─────────────────────────────────────────────────────
74
+ * The hand-rolled HTTP text this replaces — building latin1 HTTP/1.1 request
75
+ * text, then parsing a status line and scanning for `\r\n\r\n` — is how such
76
+ * code acquires leniencies nobody chose: a missing header/body separator
77
+ * yielding an empty body, blank header lines skipped, spaces tolerated inside
78
+ * the target. A structured encoding has exactly one representation per message
79
+ * and nothing to smuggle.
80
+ *
81
+ * ─── Headers are a list, not a map ─────────────────────────────────────────
82
+ * A count prefix then `(name, value)` pairs in order. Both header ORDER and
83
+ * DUPLICATE NAMES are meaningful on this wire and both survive a round trip; a
84
+ * `Record<string, string>` would silently destroy each.
85
+ *
86
+ * Pure: no keys, no I/O, no clock. `@noble/*` is not involved.
87
+ */
88
+ /**
89
+ * Every distinguishable way an envelope can fail to decode. These strings are
90
+ * the vector file's `expected_error` values verbatim — the same names
91
+ * `EnvelopeError` uses in `connector-domain` — so a replay compares reasons,
92
+ * not just "it threw".
93
+ *
94
+ * There is deliberately no catch-all: a decoder with an `Invalid` bucket is a
95
+ * decoder that can be lenient without anyone noticing.
96
+ */
97
+ declare enum EnvelopeErrorKind {
98
+ /** The buffer ends before the envelope does. */
99
+ BufferUnderflow = "buffer_underflow",
100
+ /** A length determinant that is not the minimal encoding of its value. */
101
+ NonCanonicalLength = "non_canonical_length",
102
+ /** A length determinant wider than eight bytes. */
103
+ LengthDeterminantOverflow = "length_determinant_overflow",
104
+ /** The type byte is not the one the requested direction requires. */
105
+ InvalidType = "invalid_type",
106
+ /** A field that must be UTF-8 is not. */
107
+ InvalidUtf8 = "invalid_utf8",
108
+ /** Bytes remain after a fully decoded envelope. */
109
+ TrailingBytes = "trailing_bytes"
110
+ }
111
+ /**
112
+ * A refusal to decode. `kind` is the machine-readable reason; `field` names
113
+ * the offending field for {@link EnvelopeErrorKind.InvalidUtf8}, matching
114
+ * `EnvelopeError::InvalidUtf8(&'static str)` on the Rust side.
115
+ */
116
+ declare class EnvelopeError extends Error {
117
+ readonly kind: EnvelopeErrorKind;
118
+ readonly field?: string | undefined;
119
+ constructor(kind: EnvelopeErrorKind, field?: string | undefined);
120
+ }
121
+ /** One `(name, value)` header, in wire order. Duplicates are legal. */
122
+ type EnvelopeHeader = readonly [name: string, value: string];
123
+ /** The request a connector is to make of the app behind a terminated route. */
124
+ interface EnvelopeRequest {
125
+ method: string;
126
+ target: string;
127
+ headers: readonly EnvelopeHeader[];
128
+ body: Uint8Array;
129
+ }
130
+ /** The app's response back to the connector. */
131
+ interface EnvelopeResponse {
132
+ /** HTTP status, two bytes big-endian on the wire (`0..=65535`). */
133
+ status: number;
134
+ headers: readonly EnvelopeHeader[];
135
+ body: Uint8Array;
136
+ }
137
+ /** Either direction, tagged — the shape the vector file's `decoded` carries. */
138
+ type Envelope = ({
139
+ direction: 'request';
140
+ } & EnvelopeRequest) | ({
141
+ direction: 'response';
142
+ } & EnvelopeResponse);
143
+ /** Encode a request envelope. The inverse of {@link decodeEnvelopeRequest}. */
144
+ declare function encodeEnvelopeRequest(request: EnvelopeRequest): Uint8Array;
145
+ /**
146
+ * Decode a request envelope, consuming the WHOLE buffer.
147
+ *
148
+ * @throws {EnvelopeError} and nothing else, for any input whatsoever.
149
+ */
150
+ declare function decodeEnvelopeRequest(buf: Uint8Array): EnvelopeRequest;
151
+ /** Encode a response envelope. The inverse of {@link decodeEnvelopeResponse}. */
152
+ declare function encodeEnvelopeResponse(response: EnvelopeResponse): Uint8Array;
153
+ /**
154
+ * Decode a response envelope, consuming the WHOLE buffer.
155
+ *
156
+ * @throws {EnvelopeError} and nothing else, for any input whatsoever.
157
+ */
158
+ declare function decodeEnvelopeResponse(buf: Uint8Array): EnvelopeResponse;
159
+ /** Encode either direction from its tagged form. */
160
+ declare function encodeEnvelope(envelope: Envelope): Uint8Array;
161
+ /**
162
+ * Decode as the named direction. The direction is an INPUT, never guessed from
163
+ * the type byte: a caller that asked for a request and got a response has been
164
+ * handed the wrong bytes, and `invalid_type` is the honest answer.
165
+ */
166
+ declare function decodeEnvelope(buf: Uint8Array, direction: 'request' | 'response'): Envelope;
167
+
168
+ /**
169
+ * The gift wrap (ADR 0018) and the fulfilment a shared secret derives (ADR
170
+ * 0019): the seal around the {@link ./envelope.js | envelope}, so a packet
171
+ * payload is readable only by the connector that terminates it and the
172
+ * preimage that pays for it needs no app participation.
173
+ *
174
+ * A faithful port of `connector_signer::giftwrap`
175
+ * (`crates/connector-signer/src/giftwrap.rs` on toon-protocol/connector `main`)
176
+ * and of `connector_domain::condition::derive_condition`
177
+ * (`crates/connector-domain/src/condition.rs:27`). `vectors/wire-vectors.json`'s
178
+ * `giftwrap` and `fulfilment` sections are the contract, replayed byte-for-byte
179
+ * in `wire-vectors.test.ts`; the prose here describes the same thing but the
180
+ * bytes decide.
181
+ *
182
+ * ─── The mechanism, as read off the Rust ────────────────────────────────────
183
+ * Recorded here because it cannot be inferred from the vector file's field
184
+ * names, and because the acceptance criterion for toon-client#449 asks for it
185
+ * in writing. Every line below is cited to
186
+ * `crates/connector-signer/src/giftwrap.rs` unless stated otherwise.
187
+ *
188
+ * - **Curve and ECDH.** The receiver's identity key is secp256k1; the value a
189
+ * real connector reports from `GET /ilp/identity` is its 65-byte
190
+ * uncompressed form (`0x04 ‖ X ‖ Y`). The sender ECDHs a fresh, per-packet
191
+ * ephemeral key against it and uses the **raw X-coordinate** of the
192
+ * resulting point — `crypto.rs`'s `ecdh_x_coordinate` does
193
+ * `tweak_mul_assign` then `serialize_compressed()[1..]`, deliberately *not*
194
+ * `libsecp256k1::SharedSecret`'s digest-mixing variant. So: not hashed, and
195
+ * not the 33-byte compressed point.
196
+ * - **Key derivation.** `HKDF-SHA256`, **no salt** (`Hkdf::new(None, ikm)`,
197
+ * i.e. HKDF-Extract with an all-zero salt), expanded to 32 bytes under one
198
+ * of three fixed ASCII `info` strings — `giftwrap.rs`'s `hkdf_key`:
199
+ * - `toon-giftwrap-request` — the request AEAD key, over the ECDH X.
200
+ * - `toon-giftwrap-response` — the response AEAD key, over the shared secret.
201
+ * - `toon-giftwrap-fulfillment` — the fulfilment, over the shared secret.
202
+ * (American spelling on the wire; the vector *section* is `fulfilment`.)
203
+ * These three strings are the only domain separation between two AEAD keys
204
+ * and a payment preimage all derived from the same input, so one wrong
205
+ * character silently yields bytes that decrypt nothing and satisfy no
206
+ * condition.
207
+ * - **AEAD.** ChaCha20-Poly1305 (RFC 8439), 12-byte nonce, **no** additional
208
+ * authenticated data.
209
+ * - **Request framing.** `0x01 ‖ ephemeral_public(65) ‖ nonce(12) ‖
210
+ * ciphertext`, where the encrypted plaintext is `shared_secret(32) ‖
211
+ * encoded_envelope`. The secret rides *inside* the sealed request, which is
212
+ * what lets the response be sealed with no second key exchange.
213
+ * - **Response framing.** `0x02 ‖ nonce(12) ‖ ciphertext`, the plaintext being
214
+ * just the encoded envelope. Sealed directly with the request's shared
215
+ * secret.
216
+ * - **Fulfilment and condition.** `fulfilment = HKDF-SHA256(shared_secret,
217
+ * "toon-giftwrap-fulfillment")`; the condition a sender mints is
218
+ * `sha256(fulfilment)` — `condition.rs`'s `derive_condition`. A sender knows
219
+ * the secret before it seals, so it can mint the condition first; the
220
+ * terminating connector recovers the secret by opening the wrap and derives
221
+ * the preimage without asking the app anything.
222
+ * - **Telling a sealed reject from a plaintext one.** The leading type byte,
223
+ * and only that (`looks_like_sealed_response`). A reject raised short of the
224
+ * termination shares no secret with the sender and carries empty `data`; an
225
+ * empty `data` is never sealed, so a sealed reject is positive proof that
226
+ * the destination itself said no.
227
+ *
228
+ * ─── Two failure modes, two types ───────────────────────────────────────────
229
+ * A wrap that cannot be OPENED throws {@link GiftWrapError}. A wrap that opens
230
+ * cleanly but whose recovered plaintext is not a valid envelope throws
231
+ * `EnvelopeError`, from `envelope.ts` — a different class entirely, raised
232
+ * above this module rather than by it. The Rust keeps the same separation
233
+ * (`GiftWrapError` vs `connector_domain::EnvelopeError`) for the same reason:
234
+ * "I could not read this" and "I read this and it was malformed" are different
235
+ * facts about a peer, and collapsing them loses the distinction at every call
236
+ * site.
237
+ *
238
+ * ─── Callers ────────────────────────────────────────────────────────────────
239
+ * In this package, `BootstrapService.announceViaIlp` (toon#143) is the sending
240
+ * direction: it seals to the connector that TERMINATES the destination and
241
+ * lets that connector derive the fulfilment (ADR 0019), via
242
+ * `wire/sealed-exchange.ts`. The receiving direction — a node that is itself
243
+ * the BTP destination, opening a request addressed to its own
244
+ * {@link giftWrapPublicKey} and sealing the answer back — is served by
245
+ * {@link openRequest}/{@link sealResponse} and lives in the consumers of this
246
+ * subpath rather than here.
247
+ */
248
+ /** ChaCha20-Poly1305 nonce length. */
249
+ declare const GIFTWRAP_NONCE_LENGTH = 12;
250
+ /** The shared secret carried inside a sealed request, and the fulfilment. */
251
+ declare const GIFTWRAP_SECRET_LENGTH = 32;
252
+ /** The uncompressed secp256k1 public key a sealed request carries. */
253
+ declare const GIFTWRAP_PUBLIC_KEY_LENGTH = 65;
254
+ /** `TYPE_GIFTWRAP_REQUEST` — the leading byte of a sealed request. */
255
+ declare const GIFTWRAP_TYPE_REQUEST = 1;
256
+ /** `TYPE_GIFTWRAP_RESPONSE` — the leading byte of a sealed response. */
257
+ declare const GIFTWRAP_TYPE_RESPONSE = 2;
258
+ /**
259
+ * Every distinguishable way a wrap can fail to open — the four variants of
260
+ * `GiftWrapError` in `giftwrap.rs:57-71`, named the same way. There is
261
+ * deliberately no catch-all: this module must never be able to fail "somehow".
262
+ *
263
+ * None of these ever carries decrypted plaintext, matching the Rust's own
264
+ * guarantee.
265
+ */
266
+ declare enum GiftWrapErrorKind {
267
+ /** The leading byte is not the one this direction requires. */
268
+ InvalidType = "invalid_type",
269
+ /** The bytes end before the framing does. */
270
+ Truncated = "truncated",
271
+ /** An ephemeral or receiver public key that is not a usable curve point. */
272
+ InvalidKey = "invalid_key",
273
+ /** The AEAD refused: wrong key, wrong nonce, or a tampered ciphertext. */
274
+ OpenFailed = "open_failed"
275
+ }
276
+ /**
277
+ * A refusal to open a wrap. `kind` is the machine-readable reason;
278
+ * `expectedType` names the type byte that was required, for
279
+ * {@link GiftWrapErrorKind.InvalidType} — mirroring
280
+ * `GiftWrapError::InvalidType(u8)`.
281
+ */
282
+ declare class GiftWrapError extends Error {
283
+ readonly kind: GiftWrapErrorKind;
284
+ readonly expectedType?: number | undefined;
285
+ constructor(kind: GiftWrapErrorKind, expectedType?: number | undefined);
286
+ }
287
+ /**
288
+ * What sealing a request produces: the bytes to carry as `Prepare.data`, and
289
+ * the shared secret that both derives this packet's fulfilment and opens the
290
+ * answer. The sender must keep the secret — it is the only thing that opens
291
+ * the response, and the only thing that proves the fulfilment it is paid
292
+ * against was earned rather than guessed.
293
+ */
294
+ interface SealedRequest {
295
+ /** `0x01 ‖ ephemeral_public(65) ‖ nonce(12) ‖ ciphertext`. */
296
+ readonly wrapped: Uint8Array;
297
+ /** The 32 random bytes sealed inside `wrapped`. */
298
+ readonly sharedSecret: Uint8Array;
299
+ }
300
+ /** What opening a sealed request recovers. */
301
+ interface OpenedRequest {
302
+ /** The encoded envelope — feed to `decodeEnvelopeRequest`. */
303
+ readonly envelopeBytes: Uint8Array;
304
+ /** The 32-byte secret the sender chose, for the fulfilment and the answer. */
305
+ readonly sharedSecret: Uint8Array;
306
+ }
307
+ /**
308
+ * A key agreement that never hands out secret key material — the TypeScript
309
+ * counterpart of `giftwrap.rs`'s dependency on `Signer::ecdh`, and the reason
310
+ * {@link openRequest} takes this rather than a raw private key: a remote or
311
+ * hardware-backed identity can open a wrap without its key leaving its own
312
+ * boundary.
313
+ *
314
+ * Returns the raw 32-byte ECDH X-coordinate, or throws.
315
+ */
316
+ interface GiftWrapEcdh {
317
+ ecdh(ephemeralPublicKey: Uint8Array): Uint8Array;
318
+ }
319
+ /**
320
+ * The ordinary local case of {@link GiftWrapEcdh}: a 32-byte secp256k1 secret
321
+ * key held in this process. `openRequest` accepts a bare key too and wraps it
322
+ * in this.
323
+ */
324
+ declare function localGiftWrapEcdh(secretKey: Uint8Array): GiftWrapEcdh;
325
+ /**
326
+ * The 65-byte uncompressed secp256k1 public key a `secretKey` opens gift
327
+ * wraps under — the counterpart {@link openRequest}/{@link localGiftWrapEcdh}
328
+ * derive their ECDH from, and the same format a real connector reports from
329
+ * `GET /ilp/identity` (`ConnectorIdentity.publicKey`). A caller publishes
330
+ * this as a `kind:31990` advertisement's `seal_pubkey` tag (toon-meta#266
331
+ * §3.1) so a buyer can seal a job's PREPARE `data` to it directly, without a
332
+ * `GET /identity` this client cannot serve (ADR 0022).
333
+ */
334
+ declare function giftWrapPublicKey(secretKey: Uint8Array): Uint8Array;
335
+ /**
336
+ * The deterministic core of {@link sealRequest}, parameterized on every value
337
+ * a real seal draws at random — the counterpart of the Rust's
338
+ * `seal_request_with_randomness`, which that crate gates behind a `test-util`
339
+ * feature for a reason worth repeating: **reusing a (key, nonce) pair under
340
+ * ChaCha20-Poly1305 is catastrophic**, so caller-supplied randomness is a
341
+ * footgun, not a convenience. Use {@link sealRequest} unless you are replaying
342
+ * a fixed vector.
343
+ *
344
+ * `receiverPublicKey` may be the 65-byte uncompressed form a connector's
345
+ * `GET /ilp/identity` reports, or the 33-byte compressed form; the ECDH result
346
+ * is the same point either way. The ephemeral key is always emitted
347
+ * uncompressed, because that is what the framing pins.
348
+ */
349
+ declare function sealRequestWithRandomness(plaintext: Uint8Array, receiverPublicKey: Uint8Array, ephemeralSecretKey: Uint8Array, sharedSecret: Uint8Array, nonce: Uint8Array): Uint8Array;
350
+ /**
351
+ * Seal `plaintext` (an encoded envelope) to `receiverPublicKey`. A fresh
352
+ * ephemeral key pair, shared secret and nonce are drawn for this call alone,
353
+ * so no two sealed requests — even to the same receiver, even with the same
354
+ * plaintext — share any of the three.
355
+ *
356
+ * Returns the wire bytes and the shared secret. Mint the packet's execution
357
+ * condition as {@link deriveCondition}({@link deriveFulfillment}(secret))
358
+ * before sending: the terminating connector recovers the same secret by
359
+ * opening this wrap, so the preimage it is paid against is one it derived, not
360
+ * one anybody handed it.
361
+ */
362
+ declare function sealRequest(plaintext: Uint8Array, receiverPublicKey: Uint8Array): SealedRequest;
363
+ /**
364
+ * Open a sealed request addressed to `identity`, recovering the encoded
365
+ * envelope and the shared secret carried alongside it.
366
+ *
367
+ * `identity` is either a 32-byte secp256k1 secret key or a {@link GiftWrapEcdh}
368
+ * that never exposes one. A node that is itself the BTP destination is the
369
+ * party that calls this for real. It also makes the seal testable from both
370
+ * ends — and the vectors' "opening it with the fixture's secret key must
371
+ * recover the envelope and the secret exactly" a check this repo can actually
372
+ * run.
373
+ */
374
+ declare function openRequest(bytes: Uint8Array, identity: Uint8Array | GiftWrapEcdh): OpenedRequest;
375
+ /**
376
+ * The deterministic core of {@link sealResponse}, parameterized on the nonce.
377
+ * Same footgun, same warning as {@link sealRequestWithRandomness}: a nonce
378
+ * reused under one shared secret destroys the confidentiality of both
379
+ * responses sealed with it.
380
+ */
381
+ declare function sealResponseWithRandomness(sharedSecret: Uint8Array, plaintext: Uint8Array, nonce: Uint8Array): Uint8Array;
382
+ /**
383
+ * Seal `plaintext` — an encoded response envelope, or a reject's diagnostic
384
+ * bytes — with the request's own `sharedSecret`. No second key exchange: the
385
+ * secret is bidirectional by construction, which is also why a sealed reject
386
+ * can only have come from the termination.
387
+ */
388
+ declare function sealResponse(sharedSecret: Uint8Array, plaintext: Uint8Array): Uint8Array;
389
+ /**
390
+ * Open a sealed response with the shared secret {@link sealRequest} returned
391
+ * for the request it answers. No signer, no key exchange, no identity key.
392
+ */
393
+ declare function openResponse(sharedSecret: Uint8Array, bytes: Uint8Array): Uint8Array;
394
+ /**
395
+ * Whether `bytes` is shaped like a sealed response — the leading type byte and
396
+ * nothing else, exactly as `looks_like_sealed_response` does it. This is how a
397
+ * sender tells a reject raised BY the destination from one raised on the way
398
+ * to it, without holding the secret to try opening it.
399
+ *
400
+ * Empty `data`, which every reject raised short of a termination carries, is
401
+ * never sealed.
402
+ */
403
+ declare function looksLikeSealedResponse(bytes: Uint8Array): boolean;
404
+ /**
405
+ * The fulfilment a request's shared secret derives (ADR 0019):
406
+ * `HKDF-SHA256(shared_secret, "toon-giftwrap-fulfillment")`, 32 bytes.
407
+ * Domain-separated by its own `info` string from both AEAD keys the same
408
+ * secret also derives, so it can never collide with either.
409
+ */
410
+ declare function deriveFulfillment(sharedSecret: Uint8Array): Uint8Array;
411
+ /**
412
+ * The condition a `fulfillment` satisfies: `sha256(fulfillment)` —
413
+ * `connector_domain::condition::derive_condition`. Hashing only ever runs in
414
+ * this direction; there is deliberately no function here that goes from a
415
+ * condition back to a fulfilment.
416
+ *
417
+ * The hash is the same one `wire/condition.ts` already uses; what ADR 0019
418
+ * changes is where the preimage comes from. `mintExecutionCondition` there
419
+ * draws a random preimage the sender must then carry to the FULFILL itself;
420
+ * here the preimage is DERIVED from the secret sealed inside the packet, so
421
+ * the terminating connector can produce it without the sender or the app
422
+ * handing it over. Checking a returned fulfilment against a condition is
423
+ * unchanged either way — use `fulfillmentMatchesCondition` from
424
+ * `wire/condition.ts` for that rather than a second copy of it here.
425
+ */
426
+ declare function deriveCondition(fulfillment: Uint8Array): Uint8Array;
427
+
428
+ /**
429
+ * Sender-chosen ILP execution conditions (toon-client#350, rolling-swap
430
+ * prerequisite — toon-meta#145 §3 R1/R2).
431
+ *
432
+ * Contract (normative: connector `docs/local-delivery-fulfillment-contract.md`,
433
+ * connector#309):
434
+ * - An ABSENT or ALL-ZERO (32×0x00) `executionCondition` is the LEGACY
435
+ * class: no hashlock, no verification — today's publish/upload writes.
436
+ * - Any NON-ZERO condition is SENDER-CHOSEN: the sender mints a fresh
437
+ * random 32-byte preimage `P` per packet, sets `C = sha256(P)` on the
438
+ * PREPARE, and on FULFILL MUST verify that the returned fulfillment
439
+ * hashes back to `C` — a missing/malformed/mismatching fulfillment is a
440
+ * hard failure (the packet is counted failed, never silently accepted).
441
+ *
442
+ * Isomorphic: @noble/hashes only (no Buffer, no node:crypto).
443
+ */
444
+ /** Exact byte length of an ILP execution condition / fulfillment preimage. */
445
+ declare const CONDITION_LENGTH = 32;
446
+ /** A freshly minted per-packet hashlock: `condition = sha256(preimage)`. */
447
+ interface ExecutionConditionPair {
448
+ /** Random 32-byte preimage `P` — reveal is the commit act; never reuse. */
449
+ preimage: Uint8Array;
450
+ /** `C = sha256(P)` — goes on the PREPARE's `executionCondition`. */
451
+ condition: Uint8Array;
452
+ }
453
+ /**
454
+ * Mint a fresh random 32-byte preimage and its sha256 condition (spec R1).
455
+ * Mint one PER PACKET: a reused preimage lets any observer of packet *i*
456
+ * fulfill packet *i+1* without the sender's consent.
457
+ */
458
+ declare function mintExecutionCondition(): ExecutionConditionPair;
459
+ /**
460
+ * True when `condition` selects the LEGACY (unverified) class: absent or
461
+ * all-zero. The OER wire treats absent and 32×0x00 identically.
462
+ */
463
+ declare function isZeroCondition(condition: Uint8Array | undefined): boolean;
464
+ /**
465
+ * Validate a caller-supplied execution condition before it goes on the wire.
466
+ * @throws {Error} When the condition is not exactly 32 bytes — the OER
467
+ * serializer would otherwise silently zero-fill it, downgrading a
468
+ * sender-chosen packet to the legacy unverified class.
469
+ */
470
+ declare function assertValidCondition(condition: Uint8Array): void;
471
+ /**
472
+ * True iff `fulfillment` is exactly 32 bytes and `sha256(fulfillment)`
473
+ * equals `condition` — the sender-side FULFILL check (spec R6's mirror).
474
+ * Fail-closed: an undefined/short/long fulfillment never matches.
475
+ */
476
+ declare function fulfillmentMatchesCondition(fulfillment: Uint8Array | undefined, condition: Uint8Array): boolean;
477
+
478
+ /**
479
+ * One sealed request/answer exchange with a terminating connector (ADR 0018,
480
+ * ADR 0019, ADR 0020) — the whole shape of a packet's payload, from the
481
+ * envelope going out to the answer coming back, behind two functions.
482
+ *
483
+ * A sender's obligations on this wire are not independent of one another, and
484
+ * getting any of them separately wrong is silent:
485
+ *
486
+ * - The `data` is a gift wrap addressed to the connector that TERMINATES the
487
+ * route, around an OER envelope — never HTTP text, never a bare payload.
488
+ * - The execution condition is `sha256` of the fulfilment DERIVED from the
489
+ * very secret that wrap carries. A random condition would never be
490
+ * fulfilled; an all-zero one is refused outright by the connector
491
+ * (`connector-domain`'s `condition_is_present`), which is why the publish
492
+ * path could not work against it at all before this module existed.
493
+ * - The answer is sealed with that same secret, so opening it requires having
494
+ * kept it from the seal.
495
+ *
496
+ * {@link sealExchange} produces all three from one call so they cannot drift
497
+ * apart, and {@link readExchangeOutcome} is the only thing that reads the
498
+ * answer. The secret is the token joining them.
499
+ *
500
+ * ─── Why the outcome is a tagged union ─────────────────────────────────────
501
+ * ADR 0018 gives a sender something it never had: a REJECT sealed with the
502
+ * request's own secret can ONLY have come from the termination, because only
503
+ * the termination could recover that secret. A reject raised short of it is
504
+ * necessarily plaintext — an intermediate hop shares no secret and cannot
505
+ * seal. So "the destination said no" and "somebody on the path said no" are
506
+ * now genuinely different facts, and flattening both into one error string
507
+ * would throw away the distinction the seal was introduced to create.
508
+ *
509
+ * ADR 0020 draws the other line this union encodes: an HTTP status inside a
510
+ * response envelope is envelope CONTENT, not a packet outcome. A 404 arrives
511
+ * on a FULFILL and value moved. {@link ExchangeOutcome.Answered} therefore
512
+ * carries any status at all, and it is the caller's business — not this
513
+ * module's — whether a given status means its own operation succeeded.
514
+ *
515
+ * Pure: no I/O, no clock, no network. Randomness only through
516
+ * {@link sealRequest}.
517
+ */
518
+
519
+ /**
520
+ * A sealed request, ready to become a PREPARE: what goes in `data`, what goes
521
+ * on `executionCondition`, and the secret needed to read the answer.
522
+ *
523
+ * The three are produced together and belong together. Sending `data` under a
524
+ * condition minted any other way produces a packet no honest connector can
525
+ * fulfil; opening the answer with any other secret fails.
526
+ */
527
+ interface SealedExchange {
528
+ /** The gift wrap to carry as the PREPARE's `data`. */
529
+ readonly data: Uint8Array;
530
+ /**
531
+ * The PREPARE's `executionCondition`: `sha256(deriveFulfillment(secret))`.
532
+ * Derived, never random and never caller-supplied — the terminating
533
+ * connector recovers the same secret and derives the same fulfilment
534
+ * without the app participating (ADR 0019).
535
+ */
536
+ readonly condition: Uint8Array;
537
+ /**
538
+ * The 32-byte secret sealed inside the wrap. Keep it until the answer is
539
+ * read; it is the only thing that opens the response. Never send it
540
+ * anywhere else, and never reuse it for a second packet.
541
+ */
542
+ readonly sharedSecret: Uint8Array;
543
+ /**
544
+ * The fulfilment the terminating connector will return. Held so a caller
545
+ * can check the FULFILL preimage it actually got, rather than only that it
546
+ * hashes to the condition.
547
+ */
548
+ readonly fulfillment: Uint8Array;
549
+ }
550
+ /**
551
+ * How an exchange ended. Three outcomes, deliberately not two: a refusal's
552
+ * ORIGIN is the fact ADR 0018 makes knowable for the first time.
553
+ */
554
+ type ExchangeOutcome =
555
+ /**
556
+ * The termination answered. `response` is whatever it said, at whatever
557
+ * status — a non-2xx here is a real answer that was paid for (ADR 0020),
558
+ * not a failure of the packet.
559
+ */
560
+ {
561
+ readonly kind: 'answered';
562
+ readonly response: EnvelopeResponse;
563
+ }
564
+ /**
565
+ * The DESTINATION refused, provably: the reject's `data` was sealed with
566
+ * this exchange's own secret, which only the terminating connector could
567
+ * hold. `detail` is whatever it sealed alongside (often empty).
568
+ */
569
+ | {
570
+ readonly kind: 'destination-refused';
571
+ readonly code: string;
572
+ readonly message: string;
573
+ readonly detail: Uint8Array;
574
+ }
575
+ /**
576
+ * Somebody ON THE PATH refused — no route, expiry, a ceiling, a rejected
577
+ * claim — short of the termination, so nothing was sealed. Unauthenticated
578
+ * by construction: any hop can produce this, and it should be read as a
579
+ * hint, not a verdict from the destination.
580
+ */
581
+ | {
582
+ readonly kind: 'path-refused';
583
+ readonly code: string;
584
+ readonly message: string;
585
+ };
586
+ /**
587
+ * Why an answer that arrived could not be read. Distinct from an
588
+ * {@link ExchangeOutcome}: these are not the destination's answer at all, but
589
+ * bytes that failed to be one.
590
+ */
591
+ type SealedResponseErrorKind =
592
+ /** A FULFILL whose `data` is absent, or does not begin with the sealed-response type byte. */
593
+ 'not-sealed'
594
+ /** The wrap is shaped right but does not open under this exchange's secret. */
595
+ | 'unopenable'
596
+ /** It opened, and what came out is not a response envelope. */
597
+ | 'malformed-envelope';
598
+ /** A refusal to read an answer. Never carries recovered plaintext. */
599
+ declare class SealedResponseError extends Error {
600
+ readonly kind: SealedResponseErrorKind;
601
+ readonly cause?: Error | undefined;
602
+ constructor(kind: SealedResponseErrorKind, message: string, cause?: Error | undefined);
603
+ }
604
+ /**
605
+ * Seal `request` to `connectorPublicKey` — the identity of the connector that
606
+ * TERMINATES the destination, as read from its own client edge
607
+ * (`ConnectorEdgeClient`). Sealing to any other key is a confidentiality
608
+ * failure that presents as an undeliverable packet.
609
+ *
610
+ * @param request the envelope to seal.
611
+ * @param connectorPublicKey the terminating connector's 65-byte uncompressed
612
+ * secp256k1 identity key.
613
+ */
614
+ declare function sealExchange(request: EnvelopeRequest, connectorPublicKey: Uint8Array): SealedExchange;
615
+ /**
616
+ * Read what came back.
617
+ *
618
+ * `accepted` distinguishes a FULFILL from a REJECT; `data` is the packet's
619
+ * payload bytes, absent when it carried none. Both come straight off the
620
+ * transport's result, so this stays testable without a socket.
621
+ *
622
+ * A FULFILL must be sealed and must decode: value moved, so bytes that are
623
+ * neither are a broken counterparty, and this throws rather than inventing an
624
+ * outcome. A REJECT is classified instead of thrown on — an unopenable one is
625
+ * simply a path reject that happened to carry bytes.
626
+ *
627
+ * @throws {SealedResponseError} for a FULFILL that is not a readable sealed
628
+ * response envelope.
629
+ */
630
+ declare function readExchangeOutcome(result: {
631
+ accepted: boolean;
632
+ code?: string;
633
+ message?: string;
634
+ }, data: Uint8Array | undefined, sharedSecret: Uint8Array): ExchangeOutcome;
635
+ /** Read a response header, case-insensitively; the first match wins. */
636
+ declare function envelopeHeader(response: EnvelopeResponse, name: string): string | undefined;
637
+
638
+ export { CONDITION_LENGTH, type Decoded, type Envelope, EnvelopeError, EnvelopeErrorKind, type EnvelopeHeader, type EnvelopeRequest, type EnvelopeResponse, type ExchangeOutcome, type ExecutionConditionPair, GIFTWRAP_NONCE_LENGTH, GIFTWRAP_PUBLIC_KEY_LENGTH, GIFTWRAP_SECRET_LENGTH, GIFTWRAP_TYPE_REQUEST, GIFTWRAP_TYPE_RESPONSE, type GiftWrapEcdh, GiftWrapError, GiftWrapErrorKind, OerError, OerErrorKind, type OpenedRequest, type SealedExchange, type SealedRequest, SealedResponseError, type SealedResponseErrorKind, assertValidCondition, decodeEnvelope, decodeEnvelopeRequest, decodeEnvelopeResponse, decodeVarOctetString, decodeVarUint, deriveCondition, deriveFulfillment, encodeEnvelope, encodeEnvelopeRequest, encodeEnvelopeResponse, encodeVarOctetString, encodeVarUint, envelopeHeader, fulfillmentMatchesCondition, giftWrapPublicKey, isZeroCondition, localGiftWrapEcdh, looksLikeSealedResponse, mintExecutionCondition, openRequest, openResponse, readExchangeOutcome, sealExchange, sealRequest, sealRequestWithRandomness, sealResponse, sealResponseWithRandomness };