@dcasegr/skein 0.2.9 → 0.3.3

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.
@@ -388,6 +388,23 @@ export declare class P2PHost {
388
388
  * frame written, one frame read back, the stream closed. → the answer's bytes.
389
389
  */
390
390
  request(handle: string, peer: string, protocol: string, body: Uint8Array, timeoutMs?: number): Promise<Uint8Array>;
391
+ /**
392
+ * One request to a peer and its answer, for host code (the open-exchange host's swap relay,
393
+ * 2026-11-06): from `handle`'s node, a connection to `peer` (a peer ID, or a multiaddr with
394
+ * /p2p/<id>), a stream on `protocol`, ONE frame written, ONE frame read back, the stream closed →
395
+ * the answer's bytes and the peer the connection proved (Noise: its peer ID's multihash). Nothing
396
+ * is logged anywhere. No timer of its own: the stream is live until the peer answers or the
397
+ * connection breaks. A break throws, its message saying which: the dial refused, the protocol not
398
+ * served, the write failed, the stream closed without an answer. `signal` is the caller's only
399
+ * bound: once it fires, a dial in progress is cancelled, a request not yet written is never
400
+ * written, an open stream is closed, and the call throws "the call was cancelled".
401
+ */
402
+ call(handle: string, peer: string, protocol: string, body: Uint8Array, o?: {
403
+ signal?: AbortSignal;
404
+ }): Promise<{
405
+ from: Uint8Array;
406
+ body: Uint8Array;
407
+ }>;
391
408
  /** The protocols `handle`'s node handles inbound now (the libp2p node's own registry: its rows', the host's, /skein/message/1.0.0). */
392
409
  handledProtocols(handle: string): string[];
393
410
  /** The addresses `handle`'s node knows for `peer` (its peer store), as multiaddr strings. */
package/dist/host/p2p.js CHANGED
@@ -1082,6 +1082,49 @@ export class P2PHost {
1082
1082
  throw e;
1083
1083
  }
1084
1084
  }
1085
+ /**
1086
+ * One request to a peer and its answer, for host code (the open-exchange host's swap relay,
1087
+ * 2026-11-06): from `handle`'s node, a connection to `peer` (a peer ID, or a multiaddr with
1088
+ * /p2p/<id>), a stream on `protocol`, ONE frame written, ONE frame read back, the stream closed →
1089
+ * the answer's bytes and the peer the connection proved (Noise: its peer ID's multihash). Nothing
1090
+ * is logged anywhere. No timer of its own: the stream is live until the peer answers or the
1091
+ * connection breaks. A break throws, its message saying which: the dial refused, the protocol not
1092
+ * served, the write failed, the stream closed without an answer. `signal` is the caller's only
1093
+ * bound: once it fires, a dial in progress is cancelled, a request not yet written is never
1094
+ * written, an open stream is closed, and the call throws "the call was cancelled".
1095
+ */
1096
+ async call(handle, peer, protocol, body, o = {}) {
1097
+ const n = this.nodeOf(handle);
1098
+ const signal = o.signal;
1099
+ const cancelled = () => new Error(`the call was cancelled${signal?.reason instanceof Error ? `: ${signal.reason.message}` : typeof signal?.reason === "string" ? `: ${signal.reason}` : ""}`);
1100
+ if (signal?.aborted)
1101
+ throw cancelled();
1102
+ let target;
1103
+ try {
1104
+ target = peer.startsWith("/") ? multiaddr(peer) : peerIdFromString(peer);
1105
+ }
1106
+ catch (e) {
1107
+ throw new Error(`not a peer: ${peer}: ${e.message}`);
1108
+ }
1109
+ const conn = await n.node.dial(target, signal ? { signal } : {}).catch((e) => { throw signal?.aborted ? cancelled() : new Error(`the dial was refused: ${e.message}`); });
1110
+ if (signal?.aborted)
1111
+ throw cancelled();
1112
+ const stream = await conn.newStream(protocol, signal ? { signal } : {}).catch((e) => { throw signal?.aborted ? cancelled() : new Error(`the peer does not serve ${protocol}: ${e.message}`); });
1113
+ const lp = lpStream(stream);
1114
+ try {
1115
+ // Never written once the signal has fired.
1116
+ if (signal?.aborted)
1117
+ throw cancelled();
1118
+ await lp.write(body, signal ? { signal } : {}).catch((e) => { throw signal?.aborted ? cancelled() : new Error(`the write failed: ${e.message}`); });
1119
+ const answer = await lp.read(signal ? { signal } : {}).catch((e) => { throw signal?.aborted ? cancelled() : new Error(`the stream closed without an answer: ${e.message}`); });
1120
+ await stream.close().catch(() => { });
1121
+ return { from: conn.remotePeer.toMultihash().bytes, body: answer.subarray() };
1122
+ }
1123
+ catch (e) {
1124
+ stream.abort(e);
1125
+ throw e;
1126
+ }
1127
+ }
1085
1128
  /** The protocols `handle`'s node handles inbound now (the libp2p node's own registry: its rows', the host's, /skein/message/1.0.0). */
1086
1129
  handledProtocols(handle) { return this.byHandle.get(handle)?.node.getProtocols() ?? []; }
1087
1130
  /** The addresses `handle`'s node knows for `peer` (its peer store), as multiaddr strings. */
@@ -172,7 +172,8 @@ export interface RouterOptions {
172
172
  */
173
173
  appEvent?(handle: string, record: Record<string, unknown>): void;
174
174
  /**
175
- * An application host's reads (shruggr/open-exchange 0.11.0, the public read API): a GET for an
175
+ * An application host's requests (shruggr/open-exchange 0.11.0, the public read API; every method
176
+ * since 0.3.2, so the hook checks `req.method` itself): a request for an
176
177
  * instance (its hostname, `/@<handle>/`, or the root instance's hostname), asked AFTER the host's own
177
178
  * reads there (the instance card, `/<app>/.live/<topic>`) and BEFORE the request is forwarded to the
178
179
  * instance's program. `route` is the path at the instance (`/amm/pools`); `undefined`: not the host's,
@@ -205,8 +206,13 @@ export interface RouterRequest {
205
206
  method: string;
206
207
  url: string;
207
208
  headers: Record<string, string>;
208
- body: Uint8Array; /** The instance whose `http` this is (none for a socket's). */
209
- from?: string;
209
+ body: Uint8Array;
210
+ /** The instance whose `http` this is (none for a socket's). */ from?: string;
211
+ /**
212
+ * 0.3.2: on a socket's request (Router.handler), aborted when the client's connection closes
213
+ * before the answer is written; never fired after the answer. In process: none.
214
+ */
215
+ signal?: AbortSignal;
210
216
  }
211
217
  export interface RouterResponse {
212
218
  status: number;
@@ -416,6 +422,24 @@ export declare class Router {
416
422
  messageLocal(handle: string, messageBytes: Uint8Array, body: Uint8Array): Promise<CID>;
417
423
  /** Sign `data` with `handle`'s signer: [2, "metanet handles envelope"] / `send` / anyone — an intention request's signature (providers.ts), a beacon's beat (p2p.ts beaconFrame). */
418
424
  private signAs;
425
+ /**
426
+ * A BRC-169 message signed by `handle`'s key through this host's key holder, outside any step (as
427
+ * the host signs an intention's request, providers.ts): the mail record {kind: "mail", op: "put",
428
+ * sender: the instance's identity, recipient?, box, body: <the body's CID>, nonce, signature}
429
+ * ([2, "metanet handles envelope"], key "send", counterparty anyone, over the record's dag-cbor
430
+ * without `signature`) and the body's dag-cbor bytes. `recipient` an identity key (33 bytes or
431
+ * hex); `nonce` 16 bytes, fresh when absent. For host code (the open-exchange host's swap relay
432
+ * signs its package to a validator with the token instance's key, 2026-11-06). Nothing is logged.
433
+ */
434
+ signMessage(handle: string, m: {
435
+ recipient?: Uint8Array | string;
436
+ box: string;
437
+ body: Uint8Array;
438
+ nonce?: Uint8Array;
439
+ }): Promise<{
440
+ message: MailRecord;
441
+ body: Uint8Array;
442
+ }>;
419
443
  /**
420
444
  * An inbound libp2p message or stream frame (#51, #68): appended as
421
445
  * received (the message with its signature; the frame), and the verdict
@@ -423,7 +447,9 @@ export declare class Router {
423
447
  * on, once it comes to rest (#66). The front door verifies, routes, and
424
448
  * admits what an accept carries (the message's `p2p` event first); a
425
449
  * redelivered message it accepted before is `ignore`. Its thread failing,
426
- * or not answering within the wait, is `ignore` (no penalty). A frame a
450
+ * or (a topic message) not answering within the wait, is `ignore` (no
451
+ * penalty); a stream frame's answer is waited for until its thread rests,
452
+ * however long (the stream stays open; nothing late is dropped). A frame a
427
453
  * route's filter answers at the door (a read, #143: nothing written) is
428
454
  * answered with the filter's body (#154: the overlay's sync protocol).
429
455
  */
@@ -113,6 +113,8 @@ import { createServer } from "node:http";
113
113
  import { join } from "node:path";
114
114
  import { KeyDeriver, P2PKH, PrivateKey, ProtoWallet, Transaction } from "@bsv/sdk";
115
115
  import * as dagCbor from "@ipld/dag-cbor";
116
+ import { randomBytes } from "node:crypto";
117
+ import { encode as encodeBlock } from "../runtime/cid.js";
116
118
  import { CID } from "multiformats/cid";
117
119
  import { rootIdentity } from "../runtime/identity.js";
118
120
  import { DEFAULTS, short, stampMs } from "../runtime/log.js";
@@ -749,6 +751,38 @@ export class Router {
749
751
  const { signature } = await (await this.o.walletFor(row)).createSignature({ protocolID: MESSAGE_PROTOCOL, keyID: MESSAGE_KEY_ID, counterparty: "anyone", data: [...data] });
750
752
  return Uint8Array.from(signature);
751
753
  }
754
+ /**
755
+ * A BRC-169 message signed by `handle`'s key through this host's key holder, outside any step (as
756
+ * the host signs an intention's request, providers.ts): the mail record {kind: "mail", op: "put",
757
+ * sender: the instance's identity, recipient?, box, body: <the body's CID>, nonce, signature}
758
+ * ([2, "metanet handles envelope"], key "send", counterparty anyone, over the record's dag-cbor
759
+ * without `signature`) and the body's dag-cbor bytes. `recipient` an identity key (33 bytes or
760
+ * hex); `nonce` 16 bytes, fresh when absent. For host code (the open-exchange host's swap relay
761
+ * signs its package to a validator with the token instance's key, 2026-11-06). Nothing is logged.
762
+ */
763
+ async signMessage(handle, m) {
764
+ const id = this.o.db.get(handle)?.identity;
765
+ if (!id)
766
+ throw new Error(`no instance ${handle} with an identity here`);
767
+ let value;
768
+ try {
769
+ value = dagCbor.decode(m.body);
770
+ }
771
+ catch {
772
+ throw new Error("the body is not dag-cbor");
773
+ }
774
+ const block = encodeBlock(value);
775
+ if (Buffer.compare(Buffer.from(block.bytes), Buffer.from(m.body)) !== 0)
776
+ throw new Error("the body is not canonical dag-cbor");
777
+ if (m.nonce && m.nonce.length !== 16)
778
+ throw new Error("nonce: want 16 bytes");
779
+ const recipient = m.recipient === undefined ? undefined : typeof m.recipient === "string" ? keyBytes(m.recipient) : m.recipient;
780
+ const unsigned = {
781
+ kind: "mail", op: "put", sender: keyBytes(id), ...(recipient ? { recipient } : {}),
782
+ box: m.box, body: block.cid, nonce: m.nonce ?? Uint8Array.from(randomBytes(16)),
783
+ };
784
+ return { message: { ...unsigned, signature: await this.signAs(handle, dagCbor.encode(unsigned)) }, body: m.body };
785
+ }
752
786
  /**
753
787
  * An inbound libp2p message or stream frame (#51, #68): appended as
754
788
  * received (the message with its signature; the frame), and the verdict
@@ -756,7 +790,9 @@ export class Router {
756
790
  * on, once it comes to rest (#66). The front door verifies, routes, and
757
791
  * admits what an accept carries (the message's `p2p` event first); a
758
792
  * redelivered message it accepted before is `ignore`. Its thread failing,
759
- * or not answering within the wait, is `ignore` (no penalty). A frame a
793
+ * or (a topic message) not answering within the wait, is `ignore` (no
794
+ * penalty); a stream frame's answer is waited for until its thread rests,
795
+ * however long (the stream stays open; nothing late is dropped). A frame a
760
796
  * route's filter answers at the door (a read, #143: nothing written) is
761
797
  * answered with the filter's body (#154: the overlay's sync protocol).
762
798
  */
@@ -802,7 +838,12 @@ export class Router {
802
838
  this.say(handle, `${source} from ${from}: ${verdict} (turned away at the door: ${reason})`);
803
839
  return { verdict, reason };
804
840
  }
805
- const a = await l.kernel.answer(entry, this.o.answerWaitMs ?? ANSWER_WAIT_MS);
841
+ let a = await l.kernel.answer(entry, this.o.answerWaitMs ?? ANSWER_WAIT_MS);
842
+ // A stream frame's answer is written whenever its thread rests (the swap relay's call, 2026-11-06):
843
+ // the stream is open the whole time, so the wait is waited again, never answered "ignore" and the
844
+ // late answer dropped. A topic message keeps the bound (GossipSub waits on its verdict).
845
+ while (call.topic === undefined && a.state !== "finished" && a.state !== "errored" && !this.stopped)
846
+ a = await (await this.hydrate(handle)).kernel.answer(entry, this.o.answerWaitMs ?? ANSWER_WAIT_MS);
806
847
  this.settle(l);
807
848
  if (a.state !== "finished") {
808
849
  const why = a.state === "errored" ? `the front door failed: ${a.error}` : `not answered in time (${a.state})`;
@@ -2235,8 +2276,8 @@ export class Router {
2235
2276
  // #148: a stream, served on a socket only (handler).
2236
2277
  if (req.method === "GET" && EVENTS_ROUTE.test(x.route))
2237
2278
  return json(400, { status: "error", code: "ERR_BAD_REQUEST", description: "an event stream (text/event-stream) is served on a connection of its own, not in process" });
2238
- // An application host's own reads (appRead): before the instance's program; undefined falls through.
2239
- if (req.method === "GET" && this.o.appRead) {
2279
+ // An application host's own requests (appRead, every method since 0.3.2): before the instance's program; undefined falls through.
2280
+ if (this.o.appRead) {
2240
2281
  const a = await this.o.appRead(x.handle, x.route, url, req);
2241
2282
  if (a)
2242
2283
  return a;
@@ -2489,11 +2530,16 @@ export class Router {
2489
2530
  return;
2490
2531
  }
2491
2532
  }
2533
+ // 0.3.2: the client's connection closing before the answer is written (the response's `close`
2534
+ // with nothing ended: the request's own `close` also fires once its body is read) aborts req.signal.
2535
+ const gone = new AbortController();
2536
+ res.on("close", () => { if (!res.writableEnded)
2537
+ gone.abort(new Error("the client closed the connection")); });
2492
2538
  const chunks = [];
2493
2539
  req.on("data", (c) => chunks.push(c));
2494
2540
  req.on("end", () => {
2495
2541
  const url = `http://${req.headers.host ?? `127.0.0.1:${this.port}`}${req.url ?? "/"}`;
2496
- this.dispatch({ method: req.method ?? "GET", url, headers: headerMap(req.headers), body: new Uint8Array(Buffer.concat(chunks)) })
2542
+ this.dispatch({ method: req.method ?? "GET", url, headers: headerMap(req.headers), body: new Uint8Array(Buffer.concat(chunks)), signal: gone.signal })
2497
2543
  .then((r) => {
2498
2544
  if (!r.stream) {
2499
2545
  res.writeHead(r.status, { ...cors, ...r.headers, "content-length": String(r.body.length) });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcasegr/skein",
3
- "version": "0.2.9",
3
+ "version": "0.3.3",
4
4
  "description": "A deterministic WASI machine over a content-addressed graph: the node host (it loads the wasm kernel, kernel-zig built for the web target), the client, its peers, and dev tools.",
5
5
  "type": "module",
6
6
  "bin": {