@dcasegr/skein 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +522 -2
- package/bin/skein-host.js +22 -0
- package/bin/skein.js +8 -0
- package/dist/client/admin-cli.d.ts +10 -0
- package/dist/client/admin-cli.js +370 -0
- package/dist/client/admin.d.ts +139 -0
- package/dist/client/admin.js +256 -0
- package/dist/client/bundle.d.ts +9 -0
- package/dist/client/bundle.js +34 -0
- package/dist/client/cli.d.ts +44 -0
- package/dist/client/cli.js +306 -0
- package/dist/client/client.d.ts +161 -0
- package/dist/client/client.js +237 -0
- package/dist/client/config.d.ts +18 -0
- package/dist/client/config.js +31 -0
- package/dist/client/conversation.d.ts +62 -0
- package/dist/client/conversation.js +113 -0
- package/dist/client/raw.d.ts +94 -0
- package/dist/client/raw.js +154 -0
- package/dist/client/target.d.ts +34 -0
- package/dist/client/target.js +158 -0
- package/dist/dev/cli.d.ts +8 -0
- package/dist/dev/cli.js +248 -0
- package/dist/dev/scan.d.ts +15 -0
- package/dist/dev/scan.js +173 -0
- package/dist/envelope-cbor.d.ts +110 -0
- package/dist/envelope-cbor.js +160 -0
- package/dist/envelope.d.ts +38 -0
- package/dist/envelope.js +101 -0
- package/dist/host/arc.d.ts +131 -0
- package/dist/host/arc.js +425 -0
- package/dist/host/billing.d.ts +102 -0
- package/dist/host/billing.js +167 -0
- package/dist/host/boot.d.ts +246 -0
- package/dist/host/boot.js +643 -0
- package/dist/host/cli.d.ts +57 -0
- package/dist/host/cli.js +1253 -0
- package/dist/host/clock.d.ts +3 -0
- package/dist/host/clock.js +8 -0
- package/dist/host/control.d.ts +45 -0
- package/dist/host/control.js +170 -0
- package/dist/host/cron.d.ts +99 -0
- package/dist/host/cron.js +196 -0
- package/dist/host/deliver.d.ts +53 -0
- package/dist/host/deliver.js +251 -0
- package/dist/host/deploy.d.ts +29 -0
- package/dist/host/deploy.js +55 -0
- package/dist/host/door-answer.d.ts +24 -0
- package/dist/host/door-answer.js +21 -0
- package/dist/host/events.d.ts +89 -0
- package/dist/host/events.js +222 -0
- package/dist/host/fake-arcade.d.ts +40 -0
- package/dist/host/fake-arcade.js +149 -0
- package/dist/host/fake-discovery.d.ts +4 -0
- package/dist/host/fake-discovery.js +58 -0
- package/dist/host/feeds.d.ts +127 -0
- package/dist/host/feeds.js +422 -0
- package/dist/host/frontdoor.d.ts +92 -0
- package/dist/host/frontdoor.js +172 -0
- package/dist/host/genesis.d.ts +259 -0
- package/dist/host/genesis.js +353 -0
- package/dist/host/handles.d.ts +40 -0
- package/dist/host/handles.js +88 -0
- package/dist/host/hostenv.d.ts +30 -0
- package/dist/host/hostenv.js +83 -0
- package/dist/host/image-chain.d.ts +127 -0
- package/dist/host/image-chain.js +440 -0
- package/dist/host/install.d.ts +22 -0
- package/dist/host/install.js +139 -0
- package/dist/host/instances.d.ts +227 -0
- package/dist/host/instances.js +430 -0
- package/dist/host/kernel.d.ts +246 -0
- package/dist/host/kernel.js +603 -0
- package/dist/host/liveness.d.ts +65 -0
- package/dist/host/liveness.js +155 -0
- package/dist/host/manifest.d.ts +160 -0
- package/dist/host/manifest.js +597 -0
- package/dist/host/p2p.d.ts +337 -0
- package/dist/host/p2p.js +1002 -0
- package/dist/host/packet.d.ts +121 -0
- package/dist/host/packet.js +562 -0
- package/dist/host/plan.d.ts +154 -0
- package/dist/host/plan.js +418 -0
- package/dist/host/providers.d.ts +238 -0
- package/dist/host/providers.js +565 -0
- package/dist/host/roster.d.ts +72 -0
- package/dist/host/roster.js +172 -0
- package/dist/host/router.d.ts +773 -0
- package/dist/host/router.js +2389 -0
- package/dist/host/signer.d.ts +49 -0
- package/dist/host/signer.js +167 -0
- package/dist/host/store-sqlite.d.ts +39 -0
- package/dist/host/store-sqlite.js +170 -0
- package/dist/host/stores.d.ts +43 -0
- package/dist/host/stores.js +342 -0
- package/dist/host/sync.d.ts +121 -0
- package/dist/host/sync.js +467 -0
- package/dist/host/testbeef.d.ts +21 -0
- package/dist/host/testbeef.js +37 -0
- package/dist/host/testhost.d.ts +149 -0
- package/dist/host/testhost.js +211 -0
- package/dist/host/vcdiff.d.ts +9 -0
- package/dist/host/vcdiff.js +210 -0
- package/dist/host/wasm-kernel.d.ts +87 -0
- package/dist/host/wasm-kernel.js +296 -0
- package/dist/host/wasm-worker.d.ts +1 -0
- package/dist/host/wasm-worker.js +186 -0
- package/dist/peers/cron.d.ts +33 -0
- package/dist/peers/cron.js +75 -0
- package/dist/peers/infer-main.d.ts +2 -0
- package/dist/peers/infer-main.js +74 -0
- package/dist/peers/infer.d.ts +163 -0
- package/dist/peers/infer.js +367 -0
- package/dist/runtime/beef.d.ts +28 -0
- package/dist/runtime/beef.js +107 -0
- package/dist/runtime/bitcoin.d.ts +36 -0
- package/dist/runtime/bitcoin.js +141 -0
- package/dist/runtime/cid.d.ts +14 -0
- package/dist/runtime/cid.js +51 -0
- package/dist/runtime/dispatch.d.ts +92 -0
- package/dist/runtime/dispatch.js +142 -0
- package/dist/runtime/envelope.d.ts +60 -0
- package/dist/runtime/envelope.js +101 -0
- package/dist/runtime/heads.d.ts +25 -0
- package/dist/runtime/heads.js +37 -0
- package/dist/runtime/identity.d.ts +28 -0
- package/dist/runtime/identity.js +67 -0
- package/dist/runtime/index-store.d.ts +152 -0
- package/dist/runtime/index-store.js +715 -0
- package/dist/runtime/log.d.ts +62 -0
- package/dist/runtime/log.js +104 -0
- package/dist/runtime/programs.d.ts +30 -0
- package/dist/runtime/programs.js +61 -0
- package/dist/runtime/records.d.ts +135 -0
- package/dist/runtime/records.js +149 -0
- package/dist/runtime/sqlite.d.ts +33 -0
- package/dist/runtime/sqlite.js +604 -0
- package/dist/runtime/store.d.ts +141 -0
- package/dist/runtime/store.js +24 -0
- package/dist/runtime/syscalls.d.ts +9 -0
- package/dist/runtime/syscalls.js +41 -0
- package/dist/runtime/tree.d.ts +60 -0
- package/dist/runtime/tree.js +196 -0
- package/dist/runtime/types.d.ts +100 -0
- package/dist/runtime/types.js +7 -0
- package/dist/testapps.d.ts +101 -0
- package/dist/testapps.js +275 -0
- package/dist/testkit.d.ts +25 -0
- package/dist/testkit.js +48 -0
- package/dist/wallet.d.ts +35 -0
- package/dist/wallet.js +87 -0
- package/images/README.md +68 -0
- package/images/default/README.md +38 -0
- package/images/default/bin/frontdoor.cid +1 -0
- package/images/default/bin/frontdoor.json +5 -0
- package/images/default/bin/messagebox.cid +1 -0
- package/images/default/bin/messagebox.json +8 -0
- package/images/default/bin/wallet.cid +1 -0
- package/images/default/bin/wallet.json +14 -0
- package/images/default/etc/config.json +1 -0
- package/images/default/etc/dispatch.json +82 -0
- package/images/host/etc/apps.json +8 -0
- package/kernel-zig/README.md +1247 -0
- package/kernel-zig/web/client.js +65 -0
- package/kernel-zig/web/kernel.d.ts +59 -0
- package/kernel-zig/web/kernel.js +329 -0
- package/kernel-zig/web/store.js +67 -0
- package/kernel-zig/web/worker.js +79 -0
- package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
- package/package.json +77 -4
- package/wasm/README.md +35 -0
- package/wasm/frontdoor.wasm +0 -0
- package/wasm/messagebox.wasm +0 -0
- package/wasm/resolve.wasm +0 -0
- package/wasm/wallet.wasm +0 -0
- package/wasm/wire-probe.wasm +0 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
// The host's providers (#70): recipients with identities of their own, which
|
|
2
|
+
// carry an instance's messages out. A step emits a message (the kernel's
|
|
3
|
+
// `emit`; unsigned since #126 step 4: the host carries what its own kernel
|
|
4
|
+
// hands it); once its step is committed the kernel hands it here (the
|
|
5
|
+
// serve frame `emit`: {message, body, transport, address, place?}; `place`,
|
|
6
|
+
// #148: an event's place in the log, beside the record) and this module
|
|
7
|
+
// delivers it by the address book's transport:
|
|
8
|
+
//
|
|
9
|
+
// local the provider named `address` on this host — handed over directly;
|
|
10
|
+
// `self` (#79): a message the instance sent itself (one of its apps to
|
|
11
|
+
// another: the wallet or an overlay to the chain app) — appended
|
|
12
|
+
// back into the same instance
|
|
13
|
+
// as a `local` request, as it is (unsigned: the front door finds the
|
|
14
|
+
// record in the instance's store, its own emit)
|
|
15
|
+
// libp2p the instance's libp2p node: `topic:<name>` publishes the
|
|
16
|
+
// package, a peer ID gets it as one frame on /skein/message/1.0.0;
|
|
17
|
+
// the libp2p provider answers {replyTo, seqno, recipients} | {replyTo, sent: true}
|
|
18
|
+
// event (#65, #119, #126) not a message: an event (`address` its name).
|
|
19
|
+
// `broadcast` — {kind: "broadcast", tx: <cid>, beef?}, the
|
|
20
|
+
// transaction's bytes as `body` — to the host's broadcaster (arc.ts: a
|
|
21
|
+
// durable queue, retries, one Arcade session; none: dropped with a line);
|
|
22
|
+
// `subscribe`/`unsubscribe`/`beacon`/`unbeacon` to the libp2p node,
|
|
23
|
+
// `liveness`/`unliveness` (#138) to its liveness tool (liveness.ts);
|
|
24
|
+
// `deadline`/`fetch` are intentions (#126): the host signs the request
|
|
25
|
+
// for one with the instance's key and gives it to the service it is
|
|
26
|
+
// wired to (`route`: its waker, its HTTP proxy), whose signed answer
|
|
27
|
+
// names the event and carries the request (intention, below);
|
|
28
|
+
// `payment` (#130) — the kernel's pay step's, the host's own: its
|
|
29
|
+
// billing (billing.ts, Router.payment) keeps and broadcasts it;
|
|
30
|
+
// and every event, whatever its name, to the event streams (#148,
|
|
31
|
+
// events.ts: a page's subscription to its app's events) too
|
|
32
|
+
//
|
|
33
|
+
// `deliver` (#157) — a `mailbox` recipient's message, from its
|
|
34
|
+
// delivery thread: the host carries the BRC-103/104 exchange
|
|
35
|
+
// (deliver.ts) and admits the server's signed answer, which stands on
|
|
36
|
+
// its own at the door (`deliver` below)
|
|
37
|
+
//
|
|
38
|
+
// Every answer is a signed message from the provider to the instance — the
|
|
39
|
+
// record an `emit` makes, {kind: "mail", op: "put", sender, recipient, box,
|
|
40
|
+
// body, nonce}, with its `signature` (#126 step 4: a `local` package has no
|
|
41
|
+
// session of the instance's to prove its sender), signed under [2, "metanet handles envelope"], key
|
|
42
|
+
// "send", counterparty anyone, its body {replyTo: <the message>, …} — appended
|
|
43
|
+
// as a `local` request ({kind: "message", message, body}): the instance's
|
|
44
|
+
// front door checks it and routes it by `replyTo` to the thread awaiting
|
|
45
|
+
// that message. The host verifies nothing of what it carries in; a provider
|
|
46
|
+
// checks that a message it is asked to act on is for it (the instance's
|
|
47
|
+
// message comes from the instance's own kernel: no signature, #126 step 4).
|
|
48
|
+
//
|
|
49
|
+
// The stock providers (box → body → answer body; every answer has `replyTo`,
|
|
50
|
+
// and a failure is {replyTo, error}):
|
|
51
|
+
//
|
|
52
|
+
// fetch a `fetch` intention's signed request (#126) → {replyTo: <the event>,
|
|
53
|
+
// request, status, headers, body}; a message fetch {method, url,
|
|
54
|
+
// headers?, body?, timeoutMs?, maxBytes?} (a module before skein-sdk
|
|
55
|
+
// 0.7.0) → {status, headers, body}: the HTTP proxy. A URL of this host's own
|
|
56
|
+
// is answered in process (no socket); any other goes out when the host
|
|
57
|
+
// allows it (SKEIN_HTTP=fetch, or the router's `http` option). `body`
|
|
58
|
+
// is bytes either way. `maxBytes` (#91): a response body over it is
|
|
59
|
+
// not carried in — the answer is {error} (SKEIN_HTTP=fetch stops
|
|
60
|
+
// reading at the limit)
|
|
61
|
+
// waker a `deadline` intention's signed request (#126: a step's deadline, a
|
|
62
|
+
// shell's sleep) → at `at`: {replyTo: <the event>, request, at}; a
|
|
63
|
+
// wake-me message wake {at: ms} (a kernel before #126) → {at}
|
|
64
|
+
// cron cron {fn: "tick", every | at, box, body?, name} → {name, next};
|
|
65
|
+
// then each tick, a message of its own (no `replyTo`) in `box`:
|
|
66
|
+
// {...body, kind: body.kind ?? "cron", name, due}
|
|
67
|
+
// cron {fn: "stop", name} → {name, stopped} (#69, cron.ts)
|
|
68
|
+
// libp2p publish {topic, body} → {seqno: bytes(8), recipients}
|
|
69
|
+
// dial {peer, protocol} → {stream}; then each frame read from the
|
|
70
|
+
// stream, in box `frame`: {stream, body}, and
|
|
71
|
+
// its end: {stream, closed: true, error?}
|
|
72
|
+
// send {stream, body} → {}
|
|
73
|
+
// close {stream} → {}
|
|
74
|
+
// status takes no messages (#65): it speaks first. Each status of a transaction
|
|
75
|
+
// an instance holds (but a proof: that is an event, arc.ts) is a message
|
|
76
|
+
// from it in box `chain/status` (the chain app's, #128), `subject` the
|
|
77
|
+
// transaction's CID, body {kind: "status", txid, txStatus, blockHash?,
|
|
78
|
+
// blockHeight?, extraInfo?}; an instance admits it only if it has a row from
|
|
79
|
+
// this key on `chain/status`
|
|
80
|
+
// manager the instance manager (#89, #90): it creates, starts and stops this
|
|
81
|
+
// host's instances. It takes messages from the host skein only (the
|
|
82
|
+
// operator's instance, `skein-host init`): its entry is in the host
|
|
83
|
+
// skein's address book alone, and a message from any other instance
|
|
84
|
+
// or key is not acted on and not answered (a line in the host's log).
|
|
85
|
+
// create {handle, owner, image?, claim} → {handle, identity: bytes(33), url}:
|
|
86
|
+
// the identity derived, the instance booted from the image
|
|
87
|
+
// (`default`, the only one so far), `claim` — the owner's own
|
|
88
|
+
// signed claim {message, body}, naming no recipient (#127) —
|
|
89
|
+
// forwarded into it, then — claimed — its hostname published
|
|
90
|
+
// and it started (Router.createInstance). Refused (an answer
|
|
91
|
+
// {error}): a bad or taken handle, a bad owner key, another
|
|
92
|
+
// image, no claim, another key's claim, a refused claim
|
|
93
|
+
// start {handle} → {handle, started: true, url}: published and started
|
|
94
|
+
// stop {handle} → {handle, stopped: true}: unpublished and stopped (off its
|
|
95
|
+
// libp2p node too: its beacons stop, skein-amm#4)
|
|
96
|
+
// create {handle, owner, image: "mailbox", domain?} (#113): a mailbox
|
|
97
|
+
// instance for `owner` (the front door and the messagebox,
|
|
98
|
+
// keeping its mail), published at once; the same owner and
|
|
99
|
+
// handle again: the same answer (it exists). `domain` (both
|
|
100
|
+
// images): the handle's domain, recorded with the row
|
|
101
|
+
// (`list` later.) It signs no claim (#127): it forwards the owner's,
|
|
102
|
+
// as signed — but on a one-key host (#160, #162) a create with no
|
|
103
|
+
// claim is claimed by the host's own key, its owner
|
|
104
|
+
// create {…, config?} (#162): `config` the new instance's apps' config
|
|
105
|
+
// at birth ({<app>: {…}}, merged as an install's --config);
|
|
106
|
+
// `image` the default image, `mailbox`, or an image by name
|
|
107
|
+
// ($SKEIN_HOME/images/<name>, SKEIN_IMAGES_DIR)
|
|
108
|
+
// create {…, beef?} (skein-amm#4): an Atomic BEEF handed into the new
|
|
109
|
+
// instance's receive row /skein/overlay/beef/1.0.0 once it is up
|
|
110
|
+
// (as a catch-up's fetched BEEF, from the host's own peer); the
|
|
111
|
+
// answer gains delivered: {verdict, reason?} (turned away at the
|
|
112
|
+
// door: reported, not retried, the instance stays created)
|
|
113
|
+
// Also for the instances SKEIN_MANAGER_FOR names (#162): the manager in
|
|
114
|
+
// their address book at birth, their messages taken as the host skein's
|
|
115
|
+
// certifier the host's BRC-169 certifier (#100, #113): the host skein's alone,
|
|
116
|
+
// as the manager. Its key is the certifier key, the manifest's
|
|
117
|
+
// metanet.trust.publicKey (the master's child, key ID `certifier`).
|
|
118
|
+
// issue {handle, domain, subject: bytes(33), serialNumber, issuance?}
|
|
119
|
+
// → {certificate, holder: {certificate, keyringForSubject}, serialNumber,
|
|
120
|
+
// issuance?}: the handle certificate for handle@domain → subject under
|
|
121
|
+
// that serial number, signed — plaintext fields (what resolve answers)
|
|
122
|
+
// and the subject's copy with encrypted fields and its keyring (what a
|
|
123
|
+
// wallet's acquireCertificate takes) — handles.ts. It records nothing:
|
|
124
|
+
// the host skein's onboarding app records the issue
|
|
125
|
+
// billing (#130) the host's billing key: it takes no messages and is in no address
|
|
126
|
+
// book; it signs the host's ticks to the instances whose host row names it
|
|
127
|
+
// (billing.ts) — the key an owner grants (`skein host`)
|
|
128
|
+
//
|
|
129
|
+
// How this host obtains the providers' keys is its own business (signer.ts:
|
|
130
|
+
// children of its master secret); the instance knows them from its address
|
|
131
|
+
// book (the genesis seeds them: `addressBook`, transport `local`, address the
|
|
132
|
+
// provider's name — no role, #126).
|
|
133
|
+
import { randomBytes } from "node:crypto";
|
|
134
|
+
import { ProtoWallet } from "@bsv/sdk";
|
|
135
|
+
import * as dagCbor from "@ipld/dag-cbor";
|
|
136
|
+
import { encode } from "../runtime/cid.js";
|
|
137
|
+
/** The signing a provider's answer and an intention's request get (BRC-169 §7.2, on skein's mail record); an emit is unsigned (#126 step 4). */
|
|
138
|
+
export const MESSAGE_PROTOCOL = [2, "metanet handles envelope"];
|
|
139
|
+
export const MESSAGE_KEY_ID = "send";
|
|
140
|
+
/** The stream protocol a signed message travels on to a libp2p recipient. */
|
|
141
|
+
export const MESSAGE_STREAM = "/skein/message/1.0.0";
|
|
142
|
+
/** The stock providers' names (each one's address on this host: `local`, <name>). */
|
|
143
|
+
export const PROVIDERS = ["fetch", "waker", "cron", "libp2p", "status", "manager", "certifier", "billing"];
|
|
144
|
+
/** The providers only the host skein's address book names (#90, #113): they act for it alone. */
|
|
145
|
+
export const HOST_SKEIN_PROVIDERS = ["manager", "certifier"];
|
|
146
|
+
/** Keys no address book names (#130: the host's billing key signs its ticks; it is not reached). */
|
|
147
|
+
export const UNADDRESSED = ["billing"];
|
|
148
|
+
const SERVICE = { fetch: { box: "fetch", provider: "fetch" }, deadline: { box: "wake", provider: "waker" } };
|
|
149
|
+
/** The events the libp2p node's wiring takes (`topicEvent`): subscriptions (#119), beacons (#126), liveness (#138), an overlay's wants (#112, #150). */
|
|
150
|
+
const TOPIC_EVENTS = ["subscribe", "unsubscribe", "beacon", "unbeacon", "liveness", "unliveness", "want", "unwant"];
|
|
151
|
+
const hex = (b) => Buffer.from(b).toString("hex");
|
|
152
|
+
/** The fetch provider's refusal of a response over the request's `maxBytes` (#91). */
|
|
153
|
+
export const tooLarge = (max) => `the response is larger than maxBytes (${max} bytes): not carried in`;
|
|
154
|
+
export class Providers {
|
|
155
|
+
o;
|
|
156
|
+
wallets = new Map();
|
|
157
|
+
keys = new Map();
|
|
158
|
+
/** Messages taken (handle + CID), so a message handed over again (a kernel's start) is acted on once. */
|
|
159
|
+
taken = new Set();
|
|
160
|
+
/** The waker's timers, by handle + the wake-me's CID. */
|
|
161
|
+
timers = new Map();
|
|
162
|
+
/** Messages being carried now, and the waker's answers being appended. */
|
|
163
|
+
inflight = new Set();
|
|
164
|
+
stopped = false;
|
|
165
|
+
constructor(o) { this.o = o; }
|
|
166
|
+
say(source, line) { this.o.log?.(source, line); }
|
|
167
|
+
/** A provider's identity key (hex). */
|
|
168
|
+
key(name) {
|
|
169
|
+
let k = this.keys.get(name);
|
|
170
|
+
if (!k)
|
|
171
|
+
this.keys.set(name, (k = this.o.keyOf(name).toPublicKey().toString()));
|
|
172
|
+
return k;
|
|
173
|
+
}
|
|
174
|
+
wallet(name) {
|
|
175
|
+
let w = this.wallets.get(name);
|
|
176
|
+
if (!w)
|
|
177
|
+
this.wallets.set(name, (w = new ProtoWallet(this.o.keyOf(name))));
|
|
178
|
+
return w;
|
|
179
|
+
}
|
|
180
|
+
/** The address book entries a genesis seeds for this host's providers: key, transport `local`, address the name (#126: no role). */
|
|
181
|
+
entries(names = PROVIDERS.filter((n) => !HOST_SKEIN_PROVIDERS.includes(n) && !UNADDRESSED.includes(n))) {
|
|
182
|
+
return names.map((n) => ({ key: Uint8Array.from(Buffer.from(this.key(n), "hex")), transport: "local", address: n }));
|
|
183
|
+
}
|
|
184
|
+
/** Stop the timers; nothing more is carried. */
|
|
185
|
+
stop() {
|
|
186
|
+
this.stopped = true;
|
|
187
|
+
for (const t of this.timers.values())
|
|
188
|
+
clearTimeout(t.timer);
|
|
189
|
+
this.timers.clear();
|
|
190
|
+
}
|
|
191
|
+
/** A message (or an event, #65) from `handle` to carry out (the kernel's `emit` notice). */
|
|
192
|
+
deliver(handle, out) {
|
|
193
|
+
if (this.stopped)
|
|
194
|
+
return;
|
|
195
|
+
if (out.transport === "event")
|
|
196
|
+
return this.event(handle, out);
|
|
197
|
+
const id = encode(out.message).cid;
|
|
198
|
+
const k = `${handle} ${id}`;
|
|
199
|
+
if (this.taken.has(k))
|
|
200
|
+
return;
|
|
201
|
+
this.taken.add(k);
|
|
202
|
+
if (this.taken.size > 100_000)
|
|
203
|
+
this.taken.delete(this.taken.values().next().value);
|
|
204
|
+
const p = this.carry(handle, id, out).catch((e) => this.say(handle, `provider: message ${id.toString().slice(-8)}: ${e.message}`));
|
|
205
|
+
this.inflight.add(p);
|
|
206
|
+
void p.finally(() => this.inflight.delete(p));
|
|
207
|
+
}
|
|
208
|
+
track(p) {
|
|
209
|
+
this.inflight.add(p);
|
|
210
|
+
void p.finally(() => this.inflight.delete(p));
|
|
211
|
+
}
|
|
212
|
+
/** Whether anything is being carried now (a waker's timer is not: it is waiting). */
|
|
213
|
+
busy() { return this.inflight.size > 0; }
|
|
214
|
+
/** Until what is being carried now is done (tests, the corpus: Router.settled). */
|
|
215
|
+
async idle() {
|
|
216
|
+
while (this.inflight.size)
|
|
217
|
+
await Promise.all([...this.inflight]);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* An event out (#65, #119): addressed to no one, acted on by its name —
|
|
221
|
+
* `broadcast` → the broadcaster (Arcade); `subscribe` / `unsubscribe` → the
|
|
222
|
+
* libp2p node (the topics it subscribes: p2p.ts; delivery is the kernel's, by the subscription); any other name: the application host's `appEvent` when it has one (#164), else a log
|
|
223
|
+
* line, nothing else (the step is not told). A broadcast is acted on every
|
|
224
|
+
* time it is handed over (a kernel's start hands over again what a waiting
|
|
225
|
+
* thread awaits: the queue takes a transaction once); any other event is
|
|
226
|
+
* handed over once.
|
|
227
|
+
*/
|
|
228
|
+
event(handle, out) {
|
|
229
|
+
const m = out.message;
|
|
230
|
+
const p = out.place;
|
|
231
|
+
if (p && m.kind === "event" && typeof m.event === "string")
|
|
232
|
+
this.o.emitted?.(handle, m, { n: Number(p.n), seq: Number(p.seq), i: Number(p.i), thread: p.thread }); // #148
|
|
233
|
+
if (out.address === "broadcast" && m.kind === "broadcast") {
|
|
234
|
+
if (!this.o.broadcast) {
|
|
235
|
+
this.say(handle, "broadcast: this host has no Arcade (SKEIN_ARC_URL): dropped");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.o.broadcast(handle, out.body, m.beef instanceof Uint8Array ? m.beef : undefined);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if ((out.address === "deadline" || out.address === "fetch") && m.kind === "event" && m.event === out.address) {
|
|
242
|
+
const k = `${handle} ${encode(m).cid}`;
|
|
243
|
+
if (this.taken.has(k))
|
|
244
|
+
return; // handed over again (a kernel's start): kept already
|
|
245
|
+
this.taken.add(k);
|
|
246
|
+
const p = this.intention(handle, out.address, m).catch((e) => { this.taken.delete(k); this.say(handle, `provider: ${out.address}: ${e.message}`); });
|
|
247
|
+
this.track(p);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (out.address === "deliver" && m.kind === "event" && m.event === "deliver") {
|
|
251
|
+
// #157: a mailbox delivery the host carries; its answer an entry.
|
|
252
|
+
if (!this.o.deliver) {
|
|
253
|
+
this.say(handle, "deliver: this host carries no deliveries: ignored");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const id = encode(m).cid;
|
|
257
|
+
const k = `${handle} ${id}`;
|
|
258
|
+
if (this.taken.has(k))
|
|
259
|
+
return;
|
|
260
|
+
this.taken.add(k);
|
|
261
|
+
// No answer (or a failure): forgotten, so the instance's next start (its re-offer) carries it again — once per start.
|
|
262
|
+
this.track(this.o.deliver(handle, id, m).then((ok) => { if (!ok)
|
|
263
|
+
this.taken.delete(k); }, (e) => { this.taken.delete(k); this.say(handle, `provider: deliver: ${e.message}`); }));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (out.address === "payment" && m.kind === "event" && m.event === "payment") {
|
|
267
|
+
// #130: the payee is the host: its billing keeps it (and broadcasts it).
|
|
268
|
+
if (!this.o.payment) {
|
|
269
|
+
this.say(handle, "payment: this host bills no one: ignored");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
this.o.payment(handle, m);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (TOPIC_EVENTS.includes(out.address) && m.kind === "event" && m.event === out.address) {
|
|
276
|
+
if (!this.o.topicEvent) {
|
|
277
|
+
this.say(handle, `${out.address}: this host has no libp2p node: ignored`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
this.o.topicEvent(handle, m);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (this.o.appEvent && m.kind === "event" && m.event === out.address) {
|
|
284
|
+
// #164: the application host's (e.g. the Open Exchange's): it acts on the names it knows.
|
|
285
|
+
try {
|
|
286
|
+
this.o.appEvent(handle, m);
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
this.say(handle, `provider: an event ${out.address}: the host's wiring: ${e.message}`);
|
|
290
|
+
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.say(handle, `provider: an event ${out.address} this host has no wiring for: ignored`);
|
|
294
|
+
}
|
|
295
|
+
async carry(handle, id, out) {
|
|
296
|
+
if (out.transport === "libp2p")
|
|
297
|
+
return await this.libp2pRecipient(handle, id, out);
|
|
298
|
+
if (out.transport !== "local")
|
|
299
|
+
throw new Error(`no transport ${out.transport} here`);
|
|
300
|
+
// #79: the loopback — the instance's message to itself, appended back as it is (its front door checks it).
|
|
301
|
+
if (out.address === "self") {
|
|
302
|
+
if (hex(out.message.sender) !== hex(out.message.recipient))
|
|
303
|
+
throw new Error("a loopback message is the instance's to itself");
|
|
304
|
+
await this.o.append(handle, { kind: "message", message: out.message, body: out.body });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const name = out.address;
|
|
308
|
+
if (!PROVIDERS.includes(name))
|
|
309
|
+
throw new Error(`no provider ${out.address} on this host`);
|
|
310
|
+
if (hex(out.message.recipient) !== this.key(name))
|
|
311
|
+
throw new Error(`the message is not for the ${name} provider`);
|
|
312
|
+
if (HOST_SKEIN_PROVIDERS.includes(name)) {
|
|
313
|
+
// #90, #113: the host skein's alone — any other sender's message is not acted on, and not answered.
|
|
314
|
+
const host = this.o.manager?.from();
|
|
315
|
+
const may = (h) => !!h && handle === h.handle && hex(out.message.sender) === h.identity;
|
|
316
|
+
// #162: the manager also for the instances SKEIN_MANAGER_FOR names; the certifier for the host skein alone.
|
|
317
|
+
if (!may(host) && !(name === "manager" && (this.o.manager?.alsoFor?.() ?? []).some(may)))
|
|
318
|
+
throw new Error(`the ${name === "manager" ? "instance manager" : name} takes messages from the host skein only: not acted on`);
|
|
319
|
+
}
|
|
320
|
+
let body;
|
|
321
|
+
try {
|
|
322
|
+
body = dagCbor.decode(out.body);
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return await this.answer(handle, name, out.message, id, { error: "the body is not dag-cbor" });
|
|
326
|
+
}
|
|
327
|
+
const box = out.message.box;
|
|
328
|
+
try {
|
|
329
|
+
switch (name) {
|
|
330
|
+
case "fetch": return await this.fetch(handle, out.message, id, box, body);
|
|
331
|
+
case "waker": return this.wake(handle, out.message, id, box, body);
|
|
332
|
+
case "libp2p": return await this.p2p(handle, out.message, id, box, body);
|
|
333
|
+
case "cron": return await this.cron(handle, out.message, id, box, body);
|
|
334
|
+
case "status": return await this.answer(handle, "status", out.message, id, { error: "the status provider takes no messages: it sends statuses to the instances that subscribe to it (box \"chain/status\")" });
|
|
335
|
+
case "manager": return await this.answer(handle, "manager", out.message, id, await this.o.manager.request(box, body));
|
|
336
|
+
case "certifier": {
|
|
337
|
+
if (!this.o.certifier)
|
|
338
|
+
return await this.answer(handle, "certifier", out.message, id, { error: "this host has no certifier" });
|
|
339
|
+
return await this.answer(handle, "certifier", out.message, id, await this.o.certifier(box, body));
|
|
340
|
+
}
|
|
341
|
+
case "billing": return await this.answer(handle, "billing", out.message, id, { error: "the host's billing key takes no messages (#130: it signs the host's ticks)" });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch (e) {
|
|
345
|
+
await this.answer(handle, name, out.message, id, { error: e.message });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** A signed message from provider `name` to the instance that sent `to`, in `to`'s box: {replyTo, …answer}, appended. */
|
|
349
|
+
async answer(handle, name, to, id, answer, box = to.box) {
|
|
350
|
+
await this.send(handle, name, to.sender, box, { replyTo: id, ...answer }, to.subject);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* A signed message from provider `name` to `recipient` (the instance
|
|
354
|
+
* `handle`), in `box`, its body `body` — an answer, a tick (#69), a status
|
|
355
|
+
* (#65) — appended there as a `local` request (what the append answers:
|
|
356
|
+
* the entry). Its own message (two like bodies are two messages:
|
|
357
|
+
* `nonce`), about `subject` if given.
|
|
358
|
+
*/
|
|
359
|
+
async send(handle, name, recipient, box, body, subject) {
|
|
360
|
+
if (this.stopped)
|
|
361
|
+
return undefined;
|
|
362
|
+
const bytes = dagCbor.encode(body);
|
|
363
|
+
const unsigned = {
|
|
364
|
+
kind: "mail", op: "put", sender: Uint8Array.from(Buffer.from(this.key(name), "hex")), recipient, box, body: encode(dagCbor.decode(bytes)).cid,
|
|
365
|
+
nonce: Uint8Array.from(randomBytes(16)), ...(subject ? { subject } : {}),
|
|
366
|
+
};
|
|
367
|
+
const { signature } = await this.wallet(name).createSignature({ protocolID: MESSAGE_PROTOCOL, keyID: MESSAGE_KEY_ID, counterparty: "anyone", data: [...dagCbor.encode(unsigned)] });
|
|
368
|
+
const message = { ...unsigned, signature: Uint8Array.from(signature) };
|
|
369
|
+
return await this.o.append(handle, { kind: "message", message, body: bytes });
|
|
370
|
+
}
|
|
371
|
+
// ---------------------------------------------------------------- fetch: the HTTP proxy
|
|
372
|
+
/** A message to the fetch provider (a module on an SDK before #126: fetch was a message to it). */
|
|
373
|
+
async fetch(handle, m, id, box, b) {
|
|
374
|
+
if (box !== "fetch")
|
|
375
|
+
return await this.answer(handle, "fetch", m, id, { error: `the fetch provider takes box "fetch", not ${box}` });
|
|
376
|
+
await this.answer(handle, "fetch", m, id, await this.proxy(handle, b));
|
|
377
|
+
}
|
|
378
|
+
/** The HTTP proxy (#70, #126): one request {method, url, headers?, body?, timeoutMs?, maxBytes?} → {status, headers, body} | {error}. */
|
|
379
|
+
async proxy(handle, b) {
|
|
380
|
+
if (typeof b.method !== "string" || typeof b.url !== "string")
|
|
381
|
+
return { error: "fetch wants {method, url, headers?, body?, timeoutMs?, maxBytes?}" };
|
|
382
|
+
const num = (x) => typeof x === "number" ? x : typeof x === "bigint" ? Number(x) : undefined;
|
|
383
|
+
const maxBytes = num(b.maxBytes) !== undefined && num(b.maxBytes) >= 0 ? num(b.maxBytes) : undefined;
|
|
384
|
+
const timeoutMs = num(b.timeoutMs);
|
|
385
|
+
const headers = {};
|
|
386
|
+
if (b.headers && typeof b.headers === "object")
|
|
387
|
+
for (const [k, v] of Object.entries(b.headers))
|
|
388
|
+
if (typeof v === "string")
|
|
389
|
+
headers[k.toLowerCase()] = v;
|
|
390
|
+
let r;
|
|
391
|
+
try {
|
|
392
|
+
r = await this.o.fetch({ method: b.method, url: b.url, headers, ...(b.body instanceof Uint8Array ? { body: b.body } : {}), ...(timeoutMs !== undefined ? { timeoutMs } : {}), ...(maxBytes !== undefined ? { maxBytes } : {}) }, handle);
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
return { error: e.message };
|
|
396
|
+
}
|
|
397
|
+
if (maxBytes !== undefined && r.body.length > maxBytes)
|
|
398
|
+
return { error: tooLarge(maxBytes) };
|
|
399
|
+
return { status: r.status, headers: r.headers, body: r.body };
|
|
400
|
+
}
|
|
401
|
+
// ---------------------------------------------------------------- the waker
|
|
402
|
+
/**
|
|
403
|
+
* An intention a step recorded (#126: a `deadline`, a `fetch` event), handed
|
|
404
|
+
* over once its step is committed: the request is the instance's, signed by
|
|
405
|
+
* its wallet — the mail record {kind: "mail", op: "put", sender: <the
|
|
406
|
+
* instance>, recipient: <the service's key>, box, body: <the event's CID>,
|
|
407
|
+
* nonce, signature} — sent where this host is wired (`route`: its own waker
|
|
408
|
+
* or HTTP proxy, `local`). The service's answer is a signed message from its
|
|
409
|
+
* key whose body names the event (`replyTo`) and carries that request
|
|
410
|
+
* (`request`), appended as a `local` request: the front door checks its
|
|
411
|
+
* signature, the kernel the request's (scheduler.zig intentionAnswer), and
|
|
412
|
+
* the thread awaiting the event steps — `woke` at a deadline, `reply` with a
|
|
413
|
+
* fetch's response ({status, headers, body} | {error}).
|
|
414
|
+
*/
|
|
415
|
+
async intention(handle, name, ev) {
|
|
416
|
+
const r = this.o.route?.[name] ?? { transport: "local", address: SERVICE[name].provider };
|
|
417
|
+
if (r.transport !== "local")
|
|
418
|
+
throw new Error(`no transport ${String(r.transport)} wired for ${name}`);
|
|
419
|
+
if (!PROVIDERS.includes(r.address))
|
|
420
|
+
throw new Error(`no provider ${r.address} on this host`);
|
|
421
|
+
const id = encode(ev).cid;
|
|
422
|
+
const identity = await this.o.identity(handle);
|
|
423
|
+
if (!identity)
|
|
424
|
+
throw new Error("the instance's identity is not known");
|
|
425
|
+
const unsigned = {
|
|
426
|
+
kind: "mail", op: "put", sender: identity, recipient: Uint8Array.from(Buffer.from(this.key(r.address), "hex")), box: SERVICE[name].box, body: id, nonce: Uint8Array.from(randomBytes(16)),
|
|
427
|
+
};
|
|
428
|
+
const request = { ...unsigned, signature: await this.o.sign(handle, dagCbor.encode(unsigned)) };
|
|
429
|
+
const reply = (answer) => this.send(handle, r.address, identity, SERVICE[name].box, { replyTo: id, request, ...answer });
|
|
430
|
+
if (name === "deadline") {
|
|
431
|
+
const at = typeof ev.at === "number" ? ev.at : typeof ev.at === "bigint" ? Number(ev.at) : NaN;
|
|
432
|
+
if (!Number.isFinite(at))
|
|
433
|
+
throw new Error("a deadline with no `at`");
|
|
434
|
+
this.timer(handle, id, at, () => this.track(reply({ at }).catch((e) => this.say(handle, `waker: ${e.message}`))));
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
await reply(await this.proxy(handle, ev));
|
|
438
|
+
}
|
|
439
|
+
/** Run `fire` at `at` (by the clock `now`), once per (handle, id). */
|
|
440
|
+
timer(handle, id, at, fire) {
|
|
441
|
+
const k = `${handle} ${id}`;
|
|
442
|
+
if (this.timers.has(k))
|
|
443
|
+
return;
|
|
444
|
+
// A timer's longest wait is about 24.8 days; a later wake waits in steps.
|
|
445
|
+
const arm = () => this.timers.set(k, { at, handle, fire: go, timer: setTimeout(go, Math.min(2 ** 31 - 1, Math.max(0, at - this.o.now()) + 1)) });
|
|
446
|
+
const go = () => {
|
|
447
|
+
this.timers.delete(k);
|
|
448
|
+
if (this.o.now() < at) {
|
|
449
|
+
arm();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
fire();
|
|
453
|
+
};
|
|
454
|
+
arm();
|
|
455
|
+
}
|
|
456
|
+
/** A wake-me message (a kernel before #126: a deadline was a message to the waker), kept for such a log's waiting threads. */
|
|
457
|
+
wake(handle, m, id, box, b) {
|
|
458
|
+
if (box !== "wake" || typeof b.at !== "number") {
|
|
459
|
+
void this.answer(handle, "waker", m, id, { error: "the waker takes {at: ms} in box \"wake\"" });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
this.timer(handle, id, b.at, () => this.track(this.answer(handle, "waker", m, id, { at: b.at }).catch((e) => this.say(handle, `waker: ${e.message}`))));
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Answer now every wake-me that is due by the clock (`now`): what the
|
|
466
|
+
* timers do in real time, for a clock that is not (a script clock: tests,
|
|
467
|
+
* the corpus). The answers are being appended when it returns (idle()).
|
|
468
|
+
*/
|
|
469
|
+
wakeDue() {
|
|
470
|
+
const now = this.o.now();
|
|
471
|
+
for (const [k, t] of [...this.timers])
|
|
472
|
+
if (t.at <= now) {
|
|
473
|
+
clearTimeout(t.timer);
|
|
474
|
+
this.timers.delete(k);
|
|
475
|
+
t.fire();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/** The earliest wake the waker owes `handle` (ms), if any (the host page). */
|
|
479
|
+
nextWake(handle) {
|
|
480
|
+
let next;
|
|
481
|
+
for (const t of this.timers.values())
|
|
482
|
+
if (t.handle === handle && (next === undefined || t.at < next))
|
|
483
|
+
next = t.at;
|
|
484
|
+
return next;
|
|
485
|
+
}
|
|
486
|
+
// ---------------------------------------------------------------- libp2p
|
|
487
|
+
async p2p(handle, m, id, box, b) {
|
|
488
|
+
const p = this.o.p2p;
|
|
489
|
+
if (!p)
|
|
490
|
+
return await this.answer(handle, "libp2p", m, id, { error: "this host runs no libp2p" });
|
|
491
|
+
switch (box) {
|
|
492
|
+
case "publish": {
|
|
493
|
+
if (typeof b.topic !== "string" || !(b.body instanceof Uint8Array))
|
|
494
|
+
return await this.answer(handle, "libp2p", m, id, { error: "publish wants {topic, body}" });
|
|
495
|
+
const r = await p.publish(handle, b.topic, b.body);
|
|
496
|
+
return await this.answer(handle, "libp2p", m, id, { seqno: r.seqno, recipients: r.recipients });
|
|
497
|
+
}
|
|
498
|
+
case "dial": {
|
|
499
|
+
if (typeof b.peer !== "string" || typeof b.protocol !== "string")
|
|
500
|
+
return await this.answer(handle, "libp2p", m, id, { error: "dial wants {peer, protocol}" });
|
|
501
|
+
// Each frame, and the end, is a message in box `frame` answering the dial: the thread awaits the dial's CID.
|
|
502
|
+
let stream = -1;
|
|
503
|
+
const queue = [];
|
|
504
|
+
let ready = false;
|
|
505
|
+
const frames = (f) => {
|
|
506
|
+
const x = { ...("body" in f ? { body: f.body } : { closed: true, ...(f.error ? { error: f.error } : {}) }) };
|
|
507
|
+
if (!ready) {
|
|
508
|
+
queue.push(x);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
this.track(this.answer(handle, "libp2p", m, id, { stream, ...x }, "frame").catch((e) => this.say(handle, `libp2p: frame: ${e.message}`)));
|
|
512
|
+
};
|
|
513
|
+
stream = await p.dial(handle, b.peer, b.protocol, frames);
|
|
514
|
+
await this.answer(handle, "libp2p", m, id, { stream });
|
|
515
|
+
ready = true;
|
|
516
|
+
for (const x of queue)
|
|
517
|
+
await this.answer(handle, "libp2p", m, id, { stream, ...x }, "frame");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
case "send": {
|
|
521
|
+
if (typeof b.stream !== "number" || !(b.body instanceof Uint8Array))
|
|
522
|
+
return await this.answer(handle, "libp2p", m, id, { error: "send wants {stream, body}" });
|
|
523
|
+
await p.send(handle, b.stream, b.body);
|
|
524
|
+
return await this.answer(handle, "libp2p", m, id, {});
|
|
525
|
+
}
|
|
526
|
+
case "close": {
|
|
527
|
+
if (typeof b.stream !== "number")
|
|
528
|
+
return await this.answer(handle, "libp2p", m, id, { error: "close wants {stream}" });
|
|
529
|
+
await p.close(handle, b.stream);
|
|
530
|
+
return await this.answer(handle, "libp2p", m, id, {});
|
|
531
|
+
}
|
|
532
|
+
default:
|
|
533
|
+
return await this.answer(handle, "libp2p", m, id, { error: `the libp2p provider takes publish, dial, send, close: not ${box}` });
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/** A message to a libp2p recipient: its package published on the topic, or written to the peer on MESSAGE_STREAM. */
|
|
537
|
+
async libp2pRecipient(handle, id, out) {
|
|
538
|
+
const p = this.o.p2p;
|
|
539
|
+
const pkg = dagCbor.encode({ message: out.message, body: out.body });
|
|
540
|
+
const fail = (error) => this.answer(handle, "libp2p", out.message, id, { error }, "sent");
|
|
541
|
+
if (!p)
|
|
542
|
+
return await fail("this host runs no libp2p");
|
|
543
|
+
try {
|
|
544
|
+
if (out.address.startsWith("topic:")) {
|
|
545
|
+
const r = await p.publish(handle, out.address.slice("topic:".length), pkg);
|
|
546
|
+
return await this.answer(handle, "libp2p", out.message, id, { seqno: r.seqno, recipients: r.recipients }, "sent");
|
|
547
|
+
}
|
|
548
|
+
const s = await p.dial(handle, out.address, MESSAGE_STREAM, () => { });
|
|
549
|
+
await p.send(handle, s, pkg);
|
|
550
|
+
await p.close(handle, s);
|
|
551
|
+
await this.answer(handle, "libp2p", out.message, id, { sent: true }, "sent");
|
|
552
|
+
}
|
|
553
|
+
catch (e) {
|
|
554
|
+
await fail(e.message);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
// ---------------------------------------------------------------- cron (#69)
|
|
558
|
+
async cron(handle, m, id, box, b) {
|
|
559
|
+
if (!this.o.cron)
|
|
560
|
+
return await this.answer(handle, "cron", m, id, { error: "this host keeps no schedules" });
|
|
561
|
+
if (box !== "cron")
|
|
562
|
+
return await this.answer(handle, "cron", m, id, { error: `the cron provider takes box "cron", not ${box}` });
|
|
563
|
+
await this.answer(handle, "cron", m, id, this.o.cron(handle, hex(m.sender), id.toString(), b));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type Server } from "node:http";
|
|
2
|
+
import { type TreeBlocks } from "../runtime/tree.ts";
|
|
3
|
+
import { type InstanceRow } from "./instances.ts";
|
|
4
|
+
export interface RosterEntry {
|
|
5
|
+
handle: string;
|
|
6
|
+
domain: string;
|
|
7
|
+
identity: string;
|
|
8
|
+
displayName: string;
|
|
9
|
+
description: string;
|
|
10
|
+
emoji?: string;
|
|
11
|
+
avatar?: string;
|
|
12
|
+
status: "live" | "idle";
|
|
13
|
+
}
|
|
14
|
+
export type IdentityFields = Pick<RosterEntry, "displayName" | "description" | "emoji" | "avatar">;
|
|
15
|
+
/** IDENTITY.md's `- Key: value` lines (first of each wins); missing ones are "" (name, description) or absent. */
|
|
16
|
+
export declare function parseIdentity(text: string): IdentityFields;
|
|
17
|
+
/** The row's entry; `blocks` holds its store, if there is one to read. A tree not (yet) in the store reads as no IDENTITY.md. */
|
|
18
|
+
export declare function rosterEntry(row: InstanceRow, blocks: TreeBlocks | undefined, live: boolean): Promise<RosterEntry>;
|
|
19
|
+
/**
|
|
20
|
+
* IDENTITY.md's fields in the row's deployed tree: empty ones if the tree has
|
|
21
|
+
* none, undefined if there is no tree or `blocks` does not have it (yet).
|
|
22
|
+
*/
|
|
23
|
+
export declare function deployedIdentity(row: InstanceRow, blocks: TreeBlocks | undefined): Promise<IdentityFields | undefined>;
|
|
24
|
+
/** The heading of every ROSTER.md. */
|
|
25
|
+
export declare const ROSTER_HEADING = "## Colleagues";
|
|
26
|
+
/**
|
|
27
|
+
* `row`'s ROSTER.md: the rows it knows among `rows` (in their order; itself
|
|
28
|
+
* never; `knows` naming no row is skipped), one line each —
|
|
29
|
+
* `- @handle@domain — Name: description` (no name: the handle; no
|
|
30
|
+
* description: no ": …"). Undefined when it knows nobody: no file.
|
|
31
|
+
*/
|
|
32
|
+
export declare function rosterFor(row: InstanceRow, rows: InstanceRow[], fields: (row: InstanceRow) => Promise<IdentityFields>): Promise<string | undefined>;
|
|
33
|
+
/**
|
|
34
|
+
* The roster of `rows` (the caller passes the enabled ones). `open` gives a
|
|
35
|
+
* row's store to read, or undefined, and how to let go of it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function roster(rows: InstanceRow[], open: (row: InstanceRow) => Promise<{
|
|
38
|
+
blocks?: TreeBlocks;
|
|
39
|
+
close?(): Promise<void> | void;
|
|
40
|
+
}>, live: (row: InstanceRow) => boolean): Promise<RosterEntry[]>;
|
|
41
|
+
/** One instance as the operator page shows it. */
|
|
42
|
+
export interface HostRow {
|
|
43
|
+
handle: string;
|
|
44
|
+
domain: string;
|
|
45
|
+
identity: string;
|
|
46
|
+
status: "live" | "idle" | "not run";
|
|
47
|
+
store: string;
|
|
48
|
+
tree: string;
|
|
49
|
+
pid?: number;
|
|
50
|
+
restarts: number;
|
|
51
|
+
/** The instance's origin: a skein from the default image serves the management site there (#92). */
|
|
52
|
+
origin?: string;
|
|
53
|
+
/** The instance's next wake (ms), if it has one: the waker's earliest, or the cron provider's next tick (#69). */
|
|
54
|
+
wake?: number;
|
|
55
|
+
}
|
|
56
|
+
/** The router's side of the page (#40): where it is, each instance's origin, the mailbox instances. */
|
|
57
|
+
export interface HostInfo {
|
|
58
|
+
router?: string;
|
|
59
|
+
originOf?(handle: string): string;
|
|
60
|
+
mailboxes?: Array<{
|
|
61
|
+
handle: string;
|
|
62
|
+
domain: string;
|
|
63
|
+
owner?: string | null;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
/** The host's operator page (#23): read-only, one row per enabled instance, a link to each one's origin. */
|
|
67
|
+
export declare function hostPage(rows: HostRow[], info?: HostInfo): string;
|
|
68
|
+
/**
|
|
69
|
+
* GET /roster.json → get(), CORS *; GET / → page(), if given. Resolves once
|
|
70
|
+
* listening (port 0: any; see server.address()).
|
|
71
|
+
*/
|
|
72
|
+
export declare function serveRoster(port: number, get: () => Promise<RosterEntry[]>, host?: string, page?: () => Promise<string>): Promise<Server>;
|