@macula-io/ts 0.16.0 → 0.18.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/README.md +109 -263
- package/dist/binding.d.ts +47 -45
- package/dist/binding.js +8 -125
- package/dist/binding.js.map +1 -1
- package/dist/index.d.ts +4 -9
- package/dist/index.js +5 -8
- package/dist/index.js.map +1 -1
- package/dist/key.d.ts +31 -0
- package/dist/key.js +72 -0
- package/dist/key.js.map +1 -0
- package/dist/pool.d.ts +167 -131
- package/dist/pool.js +200 -581
- package/dist/pool.js.map +1 -1
- package/dist/stream.d.ts +63 -0
- package/dist/stream.js +93 -0
- package/dist/stream.js.map +1 -0
- package/dist/wire.d.ts +53 -0
- package/dist/wire.js +79 -0
- package/dist/wire.js.map +1 -0
- package/package.json +5 -4
- package/prebuilds/darwin-arm64/@macula-io+ts.node +0 -0
- package/prebuilds/darwin-x64/@macula-io+ts.node +0 -0
- package/prebuilds/linux-arm64/@macula-io+ts.node +0 -0
- package/prebuilds/linux-x64/@macula-io+ts.node +0 -0
- package/prebuilds/win32-x64/@macula-io+ts.node +0 -0
package/README.md
CHANGED
|
@@ -19,28 +19,21 @@
|
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
> **Status, 2026-09-
|
|
23
|
-
>
|
|
24
|
-
>
|
|
25
|
-
>
|
|
26
|
-
>
|
|
27
|
-
> (
|
|
28
|
-
>
|
|
29
|
-
> policy gating don't exist yet — see [What's explicitly not yet
|
|
30
|
-
> implemented](#whats-explicitly-not-yet-implemented). Published to npm
|
|
31
|
-
> as `@macula-io/ts` with zero install-time scripts — see
|
|
32
|
-
> [Packaging](#packaging-genuinely-zero-install-time-scripts). Full
|
|
33
|
-
> development history, including every bug found and fixed along the
|
|
34
|
-
> way, lives in [CHANGELOG.md](CHANGELOG.md), not here.
|
|
22
|
+
> **Status, 2026-09-24:** on the **macula 12** wire (post-quantum: ML-DSA-87
|
|
23
|
+
> identities, ML-KEM hybrid key exchange, signed requests), over macula-go's
|
|
24
|
+
> pool. Calls and streams by direct dial, serving, publish/subscribe and the
|
|
25
|
+
> DHT are tested against in-process macula 12 stations on every `npm test`.
|
|
26
|
+
> Content transfer and UCAN-gated calls are not here yet; see [Not yet
|
|
27
|
+
> implemented](#not-yet-implemented). Releases before 0.18.0 speak the retired
|
|
28
|
+
> 10.x wire and cannot reach the current fleet.
|
|
35
29
|
|
|
36
30
|
## What is this?
|
|
37
31
|
|
|
38
|
-
A TypeScript SDK for the Macula mesh
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
reimplementation — see below for why.
|
|
32
|
+
A TypeScript SDK for the Macula mesh: a node's key, a pool of links to
|
|
33
|
+
stations it pins by node_id, calls and streams that reach a provider by direct
|
|
34
|
+
dial, serving procedures, publish/subscribe and the DHT, from Node.js. Built as
|
|
35
|
+
an FFI binding over [macula-go](https://github.com/macula-io/macula-go) rather
|
|
36
|
+
than a native reimplementation; see below for why.
|
|
44
37
|
|
|
45
38
|
## Why FFI over macula-go, not a native TypeScript reimplementation
|
|
46
39
|
|
|
@@ -66,7 +59,7 @@ that's actually usable for this today:
|
|
|
66
59
|
|
|
67
60
|
macula-go, macula-rust, macula-dotnet, and macula-php have all already
|
|
68
61
|
proven this protocol works and are actively maintained. Rather than
|
|
69
|
-
reimplement QUIC
|
|
62
|
+
reimplement QUIC, post-quantum TLS, deterministic CBOR and signed frames a fifth time in a
|
|
70
63
|
language with no mature QUIC story of its own, macula-ts reuses macula-go's
|
|
71
64
|
already-proven implementation through FFI — the same tradeoff
|
|
72
65
|
[macula-php](https://github.com/macula-io/macula-php) already made
|
|
@@ -89,264 +82,117 @@ convention).
|
|
|
89
82
|
|
|
90
83
|
## Quick start
|
|
91
84
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
await
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
console.log("call response:", response);
|
|
126
|
-
|
|
127
|
-
await stop();
|
|
128
|
-
await provider.close(providerId);
|
|
129
|
-
await caller.close(callerId);
|
|
130
|
-
providerId.dispose();
|
|
131
|
-
callerId.dispose();
|
|
132
|
-
console.log("OK");
|
|
85
|
+
```bash
|
|
86
|
+
npm install @macula-io/ts
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
A node needs a station to link to, **pinned by its node_id**, and the key of
|
|
90
|
+
each realm it trusts, which the realm publishes. Its own key is created on
|
|
91
|
+
first use and kept in a file readable by its owner only.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { NodeKey, Pool, StreamMode } from "@macula-io/ts";
|
|
95
|
+
|
|
96
|
+
const key = await NodeKey.loadOrCreate("node.key");
|
|
97
|
+
const pool = await Pool.connect(key, [{ host: "2600:3c0e::2000:c2ff:fed0:f20b", port: 4433, nodeId: stationId }], {
|
|
98
|
+
realmTrust: [{ realm, key: realmKeyHex }],
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// A call reaches a provider by direct dial: its advertisement from the DHT,
|
|
102
|
+
// trusted only when the realm key authorizes it, and its station dialed.
|
|
103
|
+
const answer = await pool.call(realm, "mcl-echo/echo", "hello");
|
|
104
|
+
|
|
105
|
+
// Publish and subscribe; topics name a kind of fact, ids go in the payload.
|
|
106
|
+
const sub = await pool.subscribe(realm, "acme/demo/greeting_sent_v1", (e) => console.log(e.payload));
|
|
107
|
+
await pool.publish(realm, "acme/demo/greeting_sent_v1", { text: "hi" });
|
|
108
|
+
|
|
109
|
+
// Serve in this node's own namespace, ~<node_id>/ring: no org, no realm key.
|
|
110
|
+
const served = await pool.serve(realm, pool.ownProcedure("ring"), (r) => ({ answered: r.caller }));
|
|
111
|
+
|
|
112
|
+
// Streams: a server stream's chunks arrive until its end.
|
|
113
|
+
const stream = await pool.openStream(realm, "mcl-tube/watch", StreamMode.Server);
|
|
114
|
+
for await (const event of stream) if (event.kind === "end") break;
|
|
115
|
+
await stream.free();
|
|
116
|
+
|
|
117
|
+
await pool.close();
|
|
133
118
|
```
|
|
134
119
|
|
|
120
|
+
Runnable versions are in [`examples/`](examples).
|
|
121
|
+
|
|
122
|
+
### Coming from 0.17 and earlier
|
|
123
|
+
|
|
124
|
+
Everything moved to the macula 12 wire, and the API with it. There is no
|
|
125
|
+
compatibility layer.
|
|
126
|
+
|
|
127
|
+
- **New identities.** A macula 12 node_id derives from an ML-DSA-87 key (or the
|
|
128
|
+
LAMPS composite in `pq_hybrid`), so no Ed25519 identity carries over.
|
|
129
|
+
`NodeKey.loadOrCreate(path)` makes a new key file; your old seed files are
|
|
130
|
+
left untouched. **Re-join your realms and re-trust your agents**: anything
|
|
131
|
+
that named your old node_id (trust lists, petnames, realm memberships) must
|
|
132
|
+
be redone with the new one.
|
|
133
|
+
- `Identity` is now `NodeKey`; `Session` and `Pool` are one `Pool`, whose seeds
|
|
134
|
+
carry the station's `nodeId` and whose `realmTrust` pins realm keys;
|
|
135
|
+
`callDirect` is simply `call`; `resolveDirect` is `providers`.
|
|
136
|
+
- Serving an org procedure needs the realm's org directory and the org's
|
|
137
|
+
delegation to your node in the DHT: a realm admits orgs through a human.
|
|
138
|
+
|
|
135
139
|
## Architecture
|
|
136
140
|
|
|
137
141
|
```
|
|
138
|
-
src
|
|
142
|
+
src/ (TypeScript API) ── addon/binding.cc (N-API) ── cabi/ (Go, C archive) ── macula-go pool
|
|
139
143
|
```
|
|
140
144
|
|
|
141
|
-
`cabi/`
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
locate or `dlopen` at runtime, no separate shared library to ship
|
|
148
|
-
alongside it. `src/binding.ts` loads that addon via
|
|
149
|
-
[`node-gyp-build`](https://github.com/prebuild/node-gyp-build) (a
|
|
150
|
-
zero-dependency runtime loader) and re-exports its typed functions;
|
|
151
|
-
`src/identity.ts` (and everything built on top of it) is the actual
|
|
152
|
-
public TypeScript API, never touching the addon directly.
|
|
153
|
-
|
|
154
|
-
**Memory ownership**, copied from macula-php's `cabi/` rather than
|
|
155
|
-
reinvented: every opaque Go value (an identity keypair, a session, or an
|
|
156
|
-
inbound "pending call" awaiting a `serve()` handler's reply) crosses the
|
|
157
|
-
boundary as a `uintptr_t` from `runtime/cgo.Handle`. Hold it, pass it
|
|
158
|
-
back for every operation on that value, and free it exactly once
|
|
159
|
-
(`Identity#dispose()` on the TS side; a pending-call handle is freed
|
|
160
|
-
automatically by whichever of `macula_pending_call_reply_result`/`_error`
|
|
161
|
-
answers it). Fixed-length fields (a 32-byte NodeID or seed) are written
|
|
162
|
-
directly into a caller-supplied output buffer. Every exported function
|
|
163
|
-
resolves handles through a `recover()`-guarded lookup, never a raw
|
|
164
|
-
`cgo.Handle(h)` — `cgo.Handle`'s own `.Value()`/`.Delete()` panic, not
|
|
165
|
-
return an error, on a handle this process never issued or already freed.
|
|
166
|
-
|
|
167
|
-
**RPC payloads cross this boundary as JSON text**, not another handle —
|
|
168
|
-
`cabi/wirevalue.go` converts to/from macula-go's `cbor.Value`, ported from
|
|
169
|
-
[macula-cli](https://github.com/macula-io/macula-cli)'s
|
|
170
|
-
`internal/wirevalue` package (already proven against the same no-bool
|
|
171
|
-
rule) rather than reinvented, plus a reserved `{"$bytes": base64}` object
|
|
172
|
-
for bytes (see the RPC caller role below). `Session.serve()`
|
|
173
|
-
cannot hand a Go closure across the FFI boundary the way `ServeOneCall`
|
|
174
|
-
expects, since the actual answer has to come from arbitrary, possibly-
|
|
175
|
-
async TypeScript — so `cabi/serve.go` splits that one blocking Go call
|
|
176
|
-
into three cgo exports instead (wait-for-call, read the pending call's
|
|
177
|
-
procedure/payload, reply), the same split
|
|
178
|
-
[macula-php](https://github.com/macula-io/macula-php)'s `cabi/serve.go`
|
|
179
|
-
already proved for the identical problem.
|
|
180
|
-
|
|
181
|
-
Development history (every bug found while building this, and how it was
|
|
182
|
-
fixed) is in [CHANGELOG.md](CHANGELOG.md).
|
|
145
|
+
`cabi/` exports C functions over macula-go's `pool` (and `stationlink`
|
|
146
|
+
streams). Every Go value crosses as a `runtime/cgo.Handle`; payloads cross as
|
|
147
|
+
JSON with no booleans and bytes as `{"$bytes": "<base64>"}` going in. Every call
|
|
148
|
+
that does network I/O runs on a worker thread (`Napi::AsyncWorker`) and returns
|
|
149
|
+
a Promise; events, served calls and served streams reach JavaScript through a
|
|
150
|
+
`ThreadSafeFunction`.
|
|
183
151
|
|
|
184
152
|
## What's implemented
|
|
185
153
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
`{"$bytes": "AQID"}` is the bytes `01 02 03`. Any other value under
|
|
207
|
-
that sole key is an error, an object with more keys stays a map, and a
|
|
208
|
-
plain string is always text. Bytes come back as `"0x"`-prefixed hex by
|
|
209
|
-
default; `opts.bytes: "tagged"` returns them in the same `$bytes` form,
|
|
210
|
-
so a returned id can be passed straight back. A BOLT#4 ERROR frame (e.g.
|
|
211
|
-
`unknown_next_peer`) rejects with a `MaculaCallError` carrying the
|
|
212
|
-
numeric `code`, `bolt4Name`, `retryable`, and `detail`. `opts.realm` (a
|
|
213
|
-
64-character hex string) scopes the call to a realm other than the
|
|
214
|
-
all-zero default; `callWithUcan()`/`publish()`/`subscribe()` take the
|
|
215
|
-
identical option.
|
|
216
|
-
- **RPC, provider role** — `session.serve(procedure, handler, opts?)` advertises
|
|
217
|
-
`procedure` and answers inbound CALLs against it forever, invoking
|
|
218
|
-
`handler(payload)` for each (sync or async; `opts.bytes` as for
|
|
219
|
-
`call()`). Resolves with an async
|
|
220
|
-
`stop()` that unadvertises and waits for the current poll tick to
|
|
221
|
-
finish. Only one `serve()` per `Session` at a time, and `call()`/
|
|
222
|
-
`serve()` refuse to run concurrently on the same `Session` — both read
|
|
223
|
-
frames off one shared control stream; open a second `Session` for the
|
|
224
|
-
other role.
|
|
225
|
-
- **DHT** — `session.findRecordsByType(recordType)`,
|
|
226
|
-
`session.findRecords(key)`, `session.findRecord(key)`, and
|
|
227
|
-
`session.putProcedureAdvertisement(procedure, servingStation, opts?)`/
|
|
228
|
-
`session.putContentAnnouncement(mcid, endpoint, ttlMs?)` — typed
|
|
229
|
-
builders wrapping macula-go's own `dht.NewProcedureAdvertisement`/
|
|
230
|
-
`NewContentAnnouncement` (signed via `dht.Sign`, stored via
|
|
231
|
-
`dht.PutRecord`). There is deliberately no generic
|
|
232
|
-
`putRecord(type, arbitraryPayload)` — a procedure_advertisement/
|
|
233
|
-
content_announcement payload carries raw pubkey/MCID fields that must
|
|
234
|
-
be actual CBOR byte strings, which only the typed builders guarantee.
|
|
235
|
-
- **Pubsub** — `session.publish(topic, payload, opts?)` (fire-and-forget,
|
|
236
|
-
no ack on the wire) and `session.subscribe(topic, handler, opts?)`
|
|
237
|
-
(`opts.bytes` as for `call()`).
|
|
238
|
-
`subscribe()` resolves with an async `stop()` that sends UNSUBSCRIBE
|
|
239
|
-
and does not resolve until the underlying reader goroutine has
|
|
240
|
-
genuinely exited. Only one `subscribe()` (and no active `serve()`) per
|
|
241
|
-
`Session` at a time — `publish()` itself is exempt, since it only ever
|
|
242
|
-
writes, so a `Session` can safely `publish()` on the same topic it's
|
|
243
|
-
`subscribe()`d to.
|
|
244
|
-
- **Content transfer** — `session.putContent(data, name?)` /
|
|
245
|
-
`session.getContent(mcid)`, sent on their own dedicated QUIC stream
|
|
246
|
-
(not the shared control stream, so they run safely alongside an active
|
|
247
|
-
`serve()`/`subscribe()`). Data above 256 KiB is chunked and reassembled
|
|
248
|
-
automatically. `mcid` crosses the boundary as a lowercase hex string.
|
|
249
|
-
**This is a one-time TRANSFER mechanism, not durable object storage** —
|
|
250
|
-
a station may forget content after serving it, and there is no
|
|
251
|
-
list/delete operation.
|
|
252
|
-
- **UCAN** — `Ucan.mint(issuer, audience, capabilities?, opts?)` (a
|
|
253
|
-
JWT-shaped, EdDSA-signed capability token, UCAN spec `"0.10.0"`) and
|
|
254
|
-
`Ucan.decode(token)` (parses claims WITHOUT verifying signature or
|
|
255
|
-
expiry — `Ucan#isExpired` mirrors macula-go's own semantics). Both are
|
|
256
|
-
pure local operations, no network I/O. `issuer` is written as
|
|
257
|
-
`did:macula:<hex NodeID>` and `audience` as the audience NodeID in
|
|
258
|
-
lowercase hex. `session.callWithUcan(procedure, payload, ucanToken,
|
|
259
|
-
opts?)` attaches a token to an outgoing CALL, for invoking a procedure
|
|
260
|
-
gated behind a provider-side `ucan.Policy.Required` policy. A gated
|
|
261
|
-
provider accepts a token only from the caller its `aud` names, so mint
|
|
262
|
-
it for the identity that will present it; `callWithUcan` attaches
|
|
263
|
-
whatever token it is given. This SDK does **not**
|
|
264
|
-
expose `ucan.Verify` or `ucan.Policy` — only minting, inspecting, and
|
|
265
|
-
attaching a token are implemented; enforcing one is provider-side, out
|
|
266
|
-
of scope here.
|
|
267
|
-
- **Direct-dial** — `session.resolveDirect(procedure, opts?)`,
|
|
268
|
-
`session.callDirect(procedure, payload, opts?)`,
|
|
269
|
-
`session.callDirectWithUcan(procedure, payload, ucanToken, opts?)`
|
|
270
|
-
(caller side) and `session.advertiseDirect(procedure, opts?)` plus a
|
|
271
|
-
standalone `keepAdvertisedDirect(session, procedure, opts?)` helper
|
|
272
|
-
(provider side). Resolves a signed `procedure_advertisement` DHT record
|
|
273
|
-
to its serving station's own signed `station_endpoint`, then dials that
|
|
274
|
-
station directly in one hop instead of depending on advertise-gossip
|
|
275
|
-
having reached whichever station the caller happens to already be
|
|
276
|
-
connected to. Every advertisement that verifies is a candidate:
|
|
277
|
-
`resolveDirect()` asks the DHT again until one's station endpoint
|
|
278
|
-
resolves, within `opts.deadlineMs` (10 s when unset), and `callDirect()`
|
|
279
|
-
moves on to the next candidate when a dial fails, within its own
|
|
280
|
-
`deadlineMs`. Trust is enforced at the application layer: the freshly
|
|
281
|
-
connected peer's HELLO-proven identity is checked against the exact
|
|
282
|
-
pubkey the signed DHT chain resolved. `advertiseDirect()` issues both a
|
|
283
|
-
plain ADVERTISE and the signed DHT record on the same call — both are
|
|
284
|
-
required for `resolveDirect()`+`callDirect()` to actually reach a live
|
|
285
|
-
route. `resolveDirect`/`callDirect`/`callDirectWithUcan`/
|
|
286
|
-
`advertiseDirect` share the same same-Session exclusivity guard as
|
|
287
|
-
`call()`/the DHT methods; a long-lived provider that also serves the
|
|
288
|
-
same procedure needs a separate `Session` (and identity — this fleet
|
|
289
|
-
enforces one connection per identity) to keep re-advertising on, which
|
|
290
|
-
is why `keepAdvertisedDirect()` is a standalone function rather than a
|
|
291
|
-
`Session` method.
|
|
292
|
-
|
|
293
|
-
- **Pool** — `Pool.connect(seeds, controlIdentity, opts)` holds live
|
|
294
|
-
connections to every configured seed concurrently (not
|
|
295
|
-
dial-one-then-fallback-on-failure), each independently monitored and
|
|
296
|
-
respawned with backoff on disconnect; `publish()`/`call()` fan out
|
|
297
|
-
over live links, `subscribe()` re-establishes automatically on
|
|
298
|
-
reconnect; `call()` and `subscribe()` take the same `bytes` option. Ports `macula/src/client/macula_client.erl`'s pool design;
|
|
299
|
-
see `pool.ts`'s own module doc for why it's a set of role-scoped
|
|
300
|
-
`Session`s per seed rather than one, given the single-reader
|
|
301
|
-
constraint below.
|
|
302
|
-
|
|
303
|
-
Every item above is live-verified against the real production fleet
|
|
304
|
-
(`station-de-frankfurt.macula.io`), including negative/error paths and,
|
|
305
|
-
where applicable, the actual packaged npm tarball rather than only the
|
|
306
|
-
dev build — see [CHANGELOG.md](CHANGELOG.md) for the specific
|
|
307
|
-
assertions, bugs found, and fixes for each.
|
|
308
|
-
|
|
309
|
-
## What's explicitly not yet implemented
|
|
310
|
-
|
|
311
|
-
Streaming RPC, streaming/content direct-dial (`OpenStreamDirect`,
|
|
312
|
-
`PutDirect`/`GetDirect` — plain `Session.call`/`serve` direct-dial is
|
|
313
|
-
implemented, see above), cert-chain-authorized direct-dial
|
|
314
|
-
(`ResolveWithCertChain`/`CallWithCertChain`/`AdvertiseDirectWithCertChain`
|
|
315
|
-
— opt-in even in macula-go itself), provider-side UCAN policy gating
|
|
316
|
-
(`ucan.Policy`/`ServeOneCallGated` — this SDK can mint/attach a token but
|
|
317
|
-
not enforce one on a served procedure), per-realm `serve`/`advertise`
|
|
318
|
-
(these two still only ever use the all-zero realm — `call`/`callWithUcan`/
|
|
319
|
-
`publish`/`subscribe`, and DHT's `putProcedureAdvertisement`, all DO now
|
|
320
|
-
take an optional realm), a generic "put any DHT record type with an
|
|
321
|
-
arbitrary payload" function (see above for why), a `station_endpoint`
|
|
322
|
-
record builder (macula-go has none either — stations publish those
|
|
323
|
-
themselves, not clients), and `Pinned`/`Insecure` trust modes (`WebPKI`
|
|
324
|
-
only so far). Multiple concurrent `subscribe()` topics on one `Session`
|
|
325
|
-
still isn't supported at the `Session` level itself — one `subscribe()`
|
|
326
|
-
(like one `serve()`) per `Session` at a time; open a second `Session`
|
|
327
|
-
for a second topic (`Pool`, above, does exactly this internally to give
|
|
328
|
-
each tracked topic its own session). Each of these is a separate, later
|
|
329
|
-
slice of work built on top of a working `Session`.
|
|
154
|
+
| Primitive | Caller | Provider | Notes |
|
|
155
|
+
|---|---|---|---|
|
|
156
|
+
| Node keys (`NodeKey`) | ✅ | ✅ | `pq_hybrid` (the fleet's) or `pq_pure`; key files readable by the owner only |
|
|
157
|
+
| Pool of station links (`Pool.connect`) | ✅ | ✅ | Seeds pinned by node_id; realm keys pinned; links redialed with subscriptions and served procedures replayed |
|
|
158
|
+
| Calls by direct dial (`call`, `providers`) | ✅ | ✅ | `serve`: a thrown error goes back as `handler_error`; errors arrive as `ProviderError` / `RelayError` |
|
|
159
|
+
| A node's own namespace (`ownProcedure`) | ✅ | ✅ | `~<node_id>/<name>`: served and called with no org and no realm key; the node's signature authorizes it |
|
|
160
|
+
| Streams (`openStream`, `serveStream`) | ✅ | ✅ | Server, client and bidi; a QUIC stream per session, released on every path |
|
|
161
|
+
| Publish/subscribe | ✅ | ✅ | Signed publications, delivered once across links |
|
|
162
|
+
| DHT (`findRecord`, `findRecords`, `findRecordsByType`, `putRecord`) | ✅ | — | Records verified before they are handed on |
|
|
163
|
+
|
|
164
|
+
## Not yet implemented
|
|
165
|
+
|
|
166
|
+
- **Content transfer.** In macula 12 a station keeps no content; the node
|
|
167
|
+
that shares it serves it. That protocol is being defined in macula
|
|
168
|
+
(macula#35) and comes here with macula-go.
|
|
169
|
+
- **UCAN-gated calls and serving.** macula 12 uses post-quantum UCANs
|
|
170
|
+
(macula-go#2). Calls carry no token yet, and a gated procedure cannot be
|
|
171
|
+
served.
|
|
172
|
+
- **Serving without an org.** Self-named procedures (`~<node id>/<name>`),
|
|
173
|
+
decided for macula 12, are not in macula or the stations yet.
|
|
330
174
|
|
|
331
175
|
## Testing
|
|
332
176
|
|
|
333
177
|
```bash
|
|
334
|
-
|
|
335
|
-
npm run test:live #
|
|
178
|
+
npm test # builds build/teststation, then the offline suite
|
|
179
|
+
npm run test:live # one live station, see below
|
|
336
180
|
```
|
|
337
181
|
|
|
338
|
-
`
|
|
339
|
-
`
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
`
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
182
|
+
`npm test` runs `src/pool.test.ts` against `cabi/cmd/teststation`, a helper
|
|
183
|
+
that runs two in-process macula 12 stations (macula-go's `teststation`) sharing
|
|
184
|
+
a DHT, with a test realm that admits the test's provider nodes. It exercises
|
|
185
|
+
keys, calls by direct dial and their errors, providers, server and client
|
|
186
|
+
streams (and that no stream is left unreleased), pubsub and the DHT, through
|
|
187
|
+
the real addon. No network is needed.
|
|
188
|
+
|
|
189
|
+
`src/fleet.live.test.ts` runs against one real station and is not part of
|
|
190
|
+
`npm test`. It needs `MACULA_TS_LIVE_SEED` (host:port), `MACULA_TS_LIVE_STATION_ID`
|
|
191
|
+
(the station's node_id), `MACULA_TS_LIVE_REALM` and `MACULA_TS_LIVE_REALM_KEY`;
|
|
192
|
+
an unset one fails the run naming it. It reads the DHT, calls `mcl-echo/echo`
|
|
193
|
+
by direct dial and hears its own publication.
|
|
194
|
+
`.github/workflows/live.yml` runs it when dispatched by hand, on the committed
|
|
195
|
+
linux-x64 prebuild.
|
|
350
196
|
|
|
351
197
|
## Packaging: genuinely zero install-time scripts
|
|
352
198
|
|
|
@@ -394,7 +240,7 @@ npm run build:go # builds cabi/build/libmacula.a -- must run BEFORE
|
|
|
394
240
|
npm install # builds the native addon (via the implicit node-gyp
|
|
395
241
|
# rebuild above) and installs JS deps
|
|
396
242
|
npm run typecheck
|
|
397
|
-
npm test
|
|
243
|
+
npm test # builds build/teststation (Go) first
|
|
398
244
|
npm run build:prebuilds # regenerate prebuilds/ after touching addon/ or cabi/ -- commit the result
|
|
399
245
|
npm run build # local dev build: addon + tsc
|
|
400
246
|
```
|
package/dist/binding.d.ts
CHANGED
|
@@ -1,47 +1,49 @@
|
|
|
1
|
-
|
|
1
|
+
/** An opaque Go value's handle (runtime/cgo.Handle), as the addon returns it. */
|
|
2
|
+
export type Handle = bigint;
|
|
3
|
+
/** What a listener (a subscription, a served procedure, a served stream
|
|
4
|
+
* procedure) is handed, on the event loop: an event or closed notice with its
|
|
5
|
+
* JSON, or a request with the pending call's or stream's handle. */
|
|
6
|
+
export interface Delivery {
|
|
7
|
+
readonly kind: "event" | "closed" | "request";
|
|
8
|
+
readonly json: string;
|
|
9
|
+
readonly handle: Handle;
|
|
10
|
+
}
|
|
11
|
+
export type Listener = (delivery: Delivery) => void;
|
|
2
12
|
export declare const native: {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}) => void, bytesMode: number): Promise<bigint>;
|
|
40
|
-
sessionSubscribeStop(subscriptionHandle: Handle): Promise<void>;
|
|
41
|
-
contentPut(sessionHandle: Handle, identityHandle: Handle, data: Uint8Array, name: string): Promise<string>;
|
|
42
|
-
contentGet(sessionHandle: Handle, identityHandle: Handle, mcidHex: string): Promise<Uint8Array | null>;
|
|
43
|
-
directdialResolve(sessionHandle: Handle, identityHandle: Handle, realm: Uint8Array | undefined, procedure: string, timeoutMs: number): Promise<string>;
|
|
44
|
-
directdialCall(sessionHandle: Handle, identityHandle: Handle, procedure: string, realm: Uint8Array | undefined, payloadJson: string, timeoutMs: number, bytesMode: number): Promise<string>;
|
|
45
|
-
directdialCallWithUcan(sessionHandle: Handle, identityHandle: Handle, procedure: string, realm: Uint8Array | undefined, payloadJson: string, timeoutMs: number, ucanToken: string, bytesMode: number): Promise<string>;
|
|
46
|
-
directdialAdvertise(sessionHandle: Handle, identityHandle: Handle, realm: Uint8Array | undefined, procedure: string, ttlMs: number): Promise<void>;
|
|
13
|
+
keyGenerate(profile: string): Promise<Handle>;
|
|
14
|
+
keyLoad(path: string, profile: string): Promise<Handle>;
|
|
15
|
+
keySave(key: Handle, path: string): Promise<void>;
|
|
16
|
+
keyNodeId(key: Handle): Uint8Array;
|
|
17
|
+
keyPublicKey(key: Handle): Uint8Array;
|
|
18
|
+
keyProfile(key: Handle): string;
|
|
19
|
+
keySign(key: Handle, data: Uint8Array): Promise<Uint8Array>;
|
|
20
|
+
keyFree(key: Handle): void;
|
|
21
|
+
poolConnect(key: Handle, seedsJson: string, optionsJson: string): Promise<Handle>;
|
|
22
|
+
poolClose(pool: Handle): Promise<void>;
|
|
23
|
+
poolNodeId(pool: Handle): Uint8Array;
|
|
24
|
+
poolStatus(pool: Handle): string;
|
|
25
|
+
poolCall(pool: Handle, realm: Uint8Array, procedure: string, payloadJson: string, provider: Uint8Array | null, timeoutMs: number, bytesMode: number): Promise<string>;
|
|
26
|
+
poolProviders(pool: Handle, realm: Uint8Array, procedure: string, timeoutMs: number): Promise<string>;
|
|
27
|
+
poolPublish(pool: Handle, realm: Uint8Array, topic: string, payloadJson: string, ttlMs: number): Promise<void>;
|
|
28
|
+
poolSubscribe(pool: Handle, realm: Uint8Array, topic: string, bytesMode: number, listener: Listener): Promise<Handle>;
|
|
29
|
+
subscriptionStop(subscription: Handle): Promise<void>;
|
|
30
|
+
poolFindRecord(pool: Handle, key: Uint8Array, timeoutMs: number, bytesMode: number): Promise<string>;
|
|
31
|
+
poolFindRecords(pool: Handle, key: Uint8Array, timeoutMs: number, bytesMode: number): Promise<string>;
|
|
32
|
+
poolFindRecordsByType(pool: Handle, type: number, timeoutMs: number, bytesMode: number): Promise<string>;
|
|
33
|
+
poolPutRecord(pool: Handle, wire: Uint8Array, timeoutMs: number): Promise<void>;
|
|
34
|
+
poolServe(pool: Handle, realm: Uint8Array, procedure: string, bytesMode: number, listener: Listener): Promise<Handle>;
|
|
35
|
+
poolServeStream(pool: Handle, realm: Uint8Array, procedure: string, mode: number, bytesMode: number, listener: Listener): Promise<Handle>;
|
|
36
|
+
pendingReply(pending: Handle, resultJson: string): void;
|
|
37
|
+
pendingError(pending: Handle, message: string): void;
|
|
38
|
+
servedStop(served: Handle): Promise<void>;
|
|
39
|
+
poolOpenStream(pool: Handle, realm: Uint8Array, procedure: string, mode: number, payloadJson: string, provider: Uint8Array | null, deadlineMs: number, timeoutMs: number): Promise<Handle>;
|
|
40
|
+
streamSendBytes(stream: Handle, data: Uint8Array): Promise<void>;
|
|
41
|
+
streamSendJson(stream: Handle, valueJson: string): Promise<void>;
|
|
42
|
+
streamCloseSend(stream: Handle): Promise<void>;
|
|
43
|
+
streamClose(stream: Handle): Promise<void>;
|
|
44
|
+
streamReply(stream: Handle, payloadJson: string): Promise<void>;
|
|
45
|
+
streamAbort(stream: Handle, code: string, message: string): Promise<void>;
|
|
46
|
+
streamRecv(stream: Handle, timeoutMs: number, bytesMode: number): Promise<string>;
|
|
47
|
+
streamRequest(stream: Handle, bytesMode: number): string;
|
|
48
|
+
streamFree(stream: Handle): Promise<void>;
|
|
47
49
|
};
|