@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.
- package/dist/host/p2p.d.ts +17 -0
- package/dist/host/p2p.js +43 -0
- package/dist/host/router.d.ts +30 -4
- package/dist/host/router.js +51 -5
- package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
- package/package.json +1 -1
package/dist/host/p2p.d.ts
CHANGED
|
@@ -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. */
|
package/dist/host/router.d.ts
CHANGED
|
@@ -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
|
|
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;
|
|
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
|
|
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
|
*/
|
package/dist/host/router.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
2239
|
-
if (
|
|
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) });
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcasegr/skein",
|
|
3
|
-
"version": "0.
|
|
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": {
|