@toon-protocol/core 3.2.1 → 3.4.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.
- package/dist/chunk-K72H36Y4.js +591 -0
- package/dist/chunk-K72H36Y4.js.map +1 -0
- package/dist/index.d.ts +108 -7
- package/dist/index.js +165 -24
- package/dist/index.js.map +1 -1
- package/dist/wire/index.d.ts +638 -0
- package/dist/wire/index.js +115 -0
- package/dist/wire/index.js.map +1 -0
- package/package.json +7 -1
|
@@ -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 };
|