@macula-io/ts 0.13.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.
@@ -0,0 +1,55 @@
1
+ import type { JsonValue } from "./rpc.js";
2
+ /** Options for Session.publish(). */
3
+ export interface PublishOptions {
4
+ /** How many milliseconds from now this event should live -- a
5
+ * DURATION, not a timestamp (matches every other ttlMs in this SDK,
6
+ * e.g. dht.ts's DHT_DEFAULT_TTL_MS) -- PublishSpec's own `ttl_ms`
7
+ * (frame/pubsub.go) is computed from this. Omitted means no TTL, not
8
+ * zero or an invented default -- macula-go's Publish has no fallback
9
+ * of its own for this field, unlike the DHT puts' TTL. */
10
+ ttlMs?: number;
11
+ /** The realm this PUBLISH is scoped to, as a 64-character hex string
12
+ * (32 bytes) -- see session.ts's CallOptions.realm for the full
13
+ * convention this shares. Omitted means the all-zero realm. Realm is
14
+ * an exact-match routing key: a subscribe() only ever receives this
15
+ * event if its own realm matches exactly, not a prefix or default
16
+ * fallback. */
17
+ realm?: string;
18
+ }
19
+ /** Options for Session.subscribe(). */
20
+ export interface SubscribeOptions {
21
+ /** The realm this SUBSCRIBE listens on -- see PublishOptions.realm's
22
+ * own doc for the format and exact-match semantics. Omitted means the
23
+ * all-zero realm. Must match the realm a publisher actually used, or
24
+ * nothing published under a different realm is ever delivered here. */
25
+ realm?: string;
26
+ /** Called at most once, only if this subscription's background
27
+ * reader exits on its own -- the underlying session/connection died,
28
+ * or some other transport error ended the read loop -- rather than
29
+ * via the stop() subscribe() returned being called. See
30
+ * Session.subscribe()'s own doc for the full story (a real bug this
31
+ * SDK had and fixed: without this signal, such a subscription went
32
+ * silent forever and left the Session unable to close cleanly).
33
+ * Optional: even without a handler here, the subscription still
34
+ * tears itself down automatically and correctly the moment this
35
+ * happens -- this is purely a notification hook for a caller who
36
+ * wants to know why events stopped arriving. */
37
+ onClosed?: (error: Error) => void;
38
+ }
39
+ /** One delivered EVENT -- macula-go's frame.EventInfo, minus `Realm`
40
+ * (the delivering EVENT's own realm is not surfaced back per-event here
41
+ * -- a caller already knows it, since subscribe()'s own `realm` option
42
+ * is what selected which realm's events reach this handler at all) and
43
+ * `DeliveredVia` (a routing/telemetry detail, not part of this SDK's
44
+ * scope yet). `payload`
45
+ * follows rpc.ts's JsonValue rules exactly like a CALL payload does --
46
+ * no boolean, embedded bytes as "0x"-prefixed hex (see cabi/wirevalue.go).
47
+ * `publisher` is the raw 32-byte Ed25519 public key of whoever published
48
+ * this event -- NOT verified against the frame's own signature by this
49
+ * SDK, matching findRecord/findRecords/findRecordsByType's identical
50
+ * "does not verify" posture (dht.ts). */
51
+ export interface PubsubEvent {
52
+ readonly payload: JsonValue;
53
+ readonly publisher: Uint8Array;
54
+ readonly seq: number;
55
+ }
package/dist/pubsub.js ADDED
@@ -0,0 +1,8 @@
1
+ // Pubsub shapes -- macula-go's connection.Session.Publish/Subscribe/
2
+ // Unsubscribe and frame.EventInfo (frame/pubsub.go, connection/
3
+ // connection.go, connection/subscriber.go). Session (session.ts) is the
4
+ // actual FFI entry point (publish()/subscribe()), matching call()/
5
+ // serve()/the DHT methods' own shape -- this file holds the shapes that
6
+ // side needs, the same split rpc.ts/dht.ts already have.
7
+ export {};
8
+ //# sourceMappingURL=pubsub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pubsub.js","sourceRoot":"","sources":["../src/pubsub.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,gEAAgE;AAChE,wEAAwE;AACxE,mEAAmE;AACnE,wEAAwE;AACxE,yDAAyD"}
package/dist/rpc.d.ts ADDED
@@ -0,0 +1,69 @@
1
+ /** A CALL/RESULT payload, restricted to what macula's wire CBOR can
2
+ * actually represent (cbor.Value's Kind enum: UInt/NegInt/Bytes/Text/
3
+ * List/Map/Null/Float -- no bool, no undefined). There is deliberately
4
+ * no `boolean` in this union: encode true/false as 1/0 yourself, the
5
+ * same rule this project's macula MCP server and macula-cli's own
6
+ * wirevalue package both enforce -- a JS boolean silently reaching the
7
+ * wire is exactly the mistake this type exists to make impossible at
8
+ * compile time.
9
+ *
10
+ * Bytes have no native JSON shape either: represent them as a
11
+ * "0x"-prefixed hex string, cabi's own convention (ported from
12
+ * macula-cli's wirevalue package, see cabi/wirevalue.go) -- a payload
13
+ * or reply containing raw bytes round-trips through call()/serve()
14
+ * unchanged as long as both sides agree on that convention. */
15
+ export type JsonValue = string | number | null | JsonValue[] | {
16
+ [key: string]: JsonValue;
17
+ };
18
+ /** The BOLT#4 fields a failed CALL carries -- see bolt4/bolt4.go's own
19
+ * 17-code table (UnknownNextPeer, TemporaryRelayFailure, Unauthorized,
20
+ * ...). `retryable` is bolt4.Code.IsRetryable()'s verdict, computed
21
+ * Go-side from `code` (cabi/rpc.go), not re-derived here. */
22
+ export interface Bolt4ErrorInfo {
23
+ readonly code: number;
24
+ readonly name: string;
25
+ readonly retryable: boolean;
26
+ readonly detail: string | null;
27
+ }
28
+ /** Thrown by Session.call() when the provider (or a relay in between)
29
+ * answered with a real BOLT#4 ERROR frame instead of a RESULT -- e.g.
30
+ * calling a procedure nobody has advertised comes back
31
+ * unknown_next_peer; a provider handler that threw comes back
32
+ * unknown_error (macula-go's connection/serve.go maps every handler
33
+ * error to that one code, matching macula_station_link.erl's own
34
+ * handle_inbound_call/2); a provider handler that panicked (recovered)
35
+ * comes back temporary_relay_failure. Distinct from a plain
36
+ * Error/rejection out of call() itself, which means this CALL never
37
+ * got a wire-level answer at all (a local timeout, a dead connection,
38
+ * a payload macula's CBOR can't represent). */
39
+ export declare class MaculaCallError extends Error {
40
+ readonly code: number;
41
+ readonly bolt4Name: string;
42
+ readonly retryable: boolean;
43
+ readonly detail: string | null;
44
+ constructor(info: Bolt4ErrorInfo);
45
+ }
46
+ /** The JSON envelope cabi/rpc.go's macula_session_call returns --
47
+ * internal to the FFI boundary, not part of the public API. Kept in
48
+ * sync BY HAND with cabi/rpc.go's callEnvelope/callEnvelopeError Go
49
+ * structs; there is no shared schema generating either side. */
50
+ export type CallEnvelope = {
51
+ ok: true;
52
+ payload: JsonValue;
53
+ } | {
54
+ ok: false;
55
+ bolt4: Bolt4ErrorInfo;
56
+ };
57
+ /** How long Session.call() waits for a RESULT/ERROR before giving up,
58
+ * in milliseconds -- also becomes the wire's own `deadline_ms` (now +
59
+ * this), matching macula-go's own examples/quickstart/main.go, which
60
+ * derives both from one duration rather than treating the local wait
61
+ * and the wire deadline as independent numbers. */
62
+ export declare const DEFAULT_CALL_TIMEOUT_MS = 30000;
63
+ /** How long one Session.serve() poll tick blocks waiting for the next
64
+ * inbound CALL before checking whether stop() was requested --
65
+ * mirrors macula-go's own servePollInterval (connection/serve_loop.go),
66
+ * the exact tick length its ServeForever uses internally for the same
67
+ * "poll with a bounded per-tick wait, check for cancellation between
68
+ * ticks" shape. */
69
+ export declare const SERVE_POLL_MS = 2000;
package/dist/rpc.js ADDED
@@ -0,0 +1,47 @@
1
+ // Unary RPC -- the caller and provider roles built on top of a
2
+ // handshaked Session's control stream, via macula-go's own
3
+ // Session.Call (caller) and Session.Advertise + Session.ServeOneCall
4
+ // (provider) -- see connection/connection.go and connection/serve.go.
5
+ // The actual FFI plumbing lives on Session (session.ts); this file
6
+ // holds the shapes both directions share: the JSON-only payload model
7
+ // and the structured BOLT#4 error macula-go's own bolt4 package
8
+ // defines (bolt4/bolt4.go, 17 codes).
9
+ /** Thrown by Session.call() when the provider (or a relay in between)
10
+ * answered with a real BOLT#4 ERROR frame instead of a RESULT -- e.g.
11
+ * calling a procedure nobody has advertised comes back
12
+ * unknown_next_peer; a provider handler that threw comes back
13
+ * unknown_error (macula-go's connection/serve.go maps every handler
14
+ * error to that one code, matching macula_station_link.erl's own
15
+ * handle_inbound_call/2); a provider handler that panicked (recovered)
16
+ * comes back temporary_relay_failure. Distinct from a plain
17
+ * Error/rejection out of call() itself, which means this CALL never
18
+ * got a wire-level answer at all (a local timeout, a dead connection,
19
+ * a payload macula's CBOR can't represent). */
20
+ export class MaculaCallError extends Error {
21
+ code;
22
+ bolt4Name;
23
+ retryable;
24
+ detail;
25
+ constructor(info) {
26
+ super(`macula-ts: CALL failed: ${info.name} (bolt4 code ${info.code})${info.detail ? `: ${info.detail}` : ""}`);
27
+ this.name = "MaculaCallError";
28
+ this.code = info.code;
29
+ this.bolt4Name = info.name;
30
+ this.retryable = info.retryable;
31
+ this.detail = info.detail;
32
+ }
33
+ }
34
+ /** How long Session.call() waits for a RESULT/ERROR before giving up,
35
+ * in milliseconds -- also becomes the wire's own `deadline_ms` (now +
36
+ * this), matching macula-go's own examples/quickstart/main.go, which
37
+ * derives both from one duration rather than treating the local wait
38
+ * and the wire deadline as independent numbers. */
39
+ export const DEFAULT_CALL_TIMEOUT_MS = 30_000;
40
+ /** How long one Session.serve() poll tick blocks waiting for the next
41
+ * inbound CALL before checking whether stop() was requested --
42
+ * mirrors macula-go's own servePollInterval (connection/serve_loop.go),
43
+ * the exact tick length its ServeForever uses internally for the same
44
+ * "poll with a bounded per-tick wait, check for cancellation between
45
+ * ticks" shape. */
46
+ export const SERVE_POLL_MS = 2_000;
47
+ //# sourceMappingURL=rpc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc.js","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,2DAA2D;AAC3D,qEAAqE;AACrE,sEAAsE;AACtE,mEAAmE;AACnE,sEAAsE;AACtE,gEAAgE;AAChE,sCAAsC;AA6BtC;;;;;;;;;;+CAU+C;AAC/C,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAC/B,IAAI,CAAS;IACb,SAAS,CAAS;IAClB,SAAS,CAAU;IACnB,MAAM,CAAgB;IAE/B,YAAY,IAAoB;QAC9B,KAAK,CAAC,2BAA2B,IAAI,CAAC,IAAI,gBAAgB,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChH,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;QAC9B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC5B,CAAC;CACF;AAQD;;;;mDAImD;AACnD,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAE9C;;;;;mBAKmB;AACnB,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAC"}
@@ -0,0 +1,453 @@
1
+ import { DhtRecordType, type DhtRecord } from "./dht.js";
2
+ import type { AdvertiseDirectOptions, DirectDialTarget } from "./directdial.js";
3
+ import { Identity } from "./identity.js";
4
+ import type { PublishOptions, PubsubEvent, SubscribeOptions } from "./pubsub.js";
5
+ import { type JsonValue } from "./rpc.js";
6
+ import { Ucan } from "./ucan.js";
7
+ /** Options for Session.call()/callWithUcan(). */
8
+ export interface CallOptions {
9
+ /** How long to wait for a RESULT/ERROR before giving up, in
10
+ * milliseconds. Also becomes the wire's own `deadline_ms` (now +
11
+ * this) -- see rpc.ts's DEFAULT_CALL_TIMEOUT_MS for why both share
12
+ * one number. */
13
+ deadlineMs?: number;
14
+ /** The realm this CALL is scoped to, as a 64-character lowercase (or
15
+ * uppercase, case-insensitive) hex string -- 32 bytes, the same
16
+ * hex-string convention DhtRecord's own key/version/signature fields
17
+ * already use (dht.ts), not native.*'s raw-byte Uint8Array (this
18
+ * class converts internally -- see realmBytesFromHex below). Omitted
19
+ * means the all-zero realm, the same default every mesh operation in
20
+ * this SDK used exclusively before this option existed, and what
21
+ * macula-go's own realm32OrZero (cabi/main.go) falls back to when no
22
+ * realm pointer is given.
23
+ *
24
+ * Must match whatever realm the target procedure is actually served
25
+ * under, or this CALL comes back `unknown_next_peer` even for a
26
+ * procedure genuinely advertised elsewhere -- realm is an exact-match
27
+ * routing key, not a hierarchy or a default-realm fallback. This
28
+ * class's own `serve()` always advertises under the all-zero realm
29
+ * (a separate, later gap -- see README.md's "What's explicitly not
30
+ * yet implemented"); calling with a non-zero realm only reaches a
31
+ * provider serving under that same realm through some other means. */
32
+ realm?: string;
33
+ }
34
+ export declare class Session {
35
+ #private;
36
+ private constructor();
37
+ /** Dials host:port and completes the full CONNECT/HELLO handshake
38
+ * against a real macula-station, via macula-go's connection.Connect
39
+ * (WebPKI trust -- standard CA-bundle validation, matching what the
40
+ * production fleet actually presents; Pinned/Insecure trust modes
41
+ * aren't exposed yet).
42
+ *
43
+ * This is real network I/O -- a QUIC dial plus a signed round trip,
44
+ * bounded by macula-go's own ~30s handshake timeout -- so it's async
45
+ * on both sides of the FFI boundary (see addon/binding.cc's
46
+ * ConnectWorker): awaiting this never blocks Node's event loop.
47
+ *
48
+ * `identity` must stay non-disposed for the life of the returned
49
+ * Session -- close() needs it again to sign GOODBYE. */
50
+ static connect(host: string, port: number, identity: Identity): Promise<Session>;
51
+ /** The address this session's underlying QUIC connection is with. */
52
+ get remoteAddr(): string;
53
+ /** The station's HELLO-verified 32-byte NodeID (Ed25519 public key)
54
+ * -- proof, beyond "connect() didn't throw", that this is a real,
55
+ * application-layer-verified session and not just a QUIC/TLS
56
+ * handshake: frame.Verify already checked this NodeID's signature
57
+ * over the HELLO frame inside connect(), this just surfaces it. */
58
+ get stationNodeId(): Uint8Array;
59
+ /** Sends a signed GOODBYE and closes the connection. Safe to call
60
+ * more than once -- a second call is a no-op, matching Identity's
61
+ * dispose() convention. `identity` must be the same (non-disposed)
62
+ * identity used to open this session; Close needs it to sign
63
+ * GOODBYE. Like connect(), this is real network I/O (a drain sleep
64
+ * plus a write) and runs off the main thread on the native side.
65
+ *
66
+ * Stops an active subscribe() FIRST, if there is one, sending its
67
+ * UNSUBSCRIBE over the still-open connection before that connection
68
+ * goes away -- unlike every other resource this SDK hands out (an
69
+ * Identity, an unstopped serve() loop), an unstopped subscription
70
+ * left dangling here does not just leak memory: its background reader
71
+ * goroutine holds a live Napi::ThreadSafeFunction, which deliberately
72
+ * keeps Node's event loop alive on its own (see subscribe()'s own doc
73
+ * -- a program that does nothing but subscribe() and wait needs
74
+ * exactly this to stay alive for events to arrive at all). Verified
75
+ * live: closing a Session out from under an active subscription
76
+ * without this hung the process forever, not merely leaked a handle
77
+ * -- close() closing it first is what makes "forgot to call the
78
+ * returned stop()" fail safe instead of fail hung. */
79
+ close(identity: Identity, reason?: string): Promise<void>;
80
+ /** Caller role: sends a signed CALL for `procedure` and waits for the
81
+ * matching RESULT or ERROR (macula-go's connection.Session.Call).
82
+ * `payload` is JSON, converted to a cbor.Value on the Go side
83
+ * (cabi/wirevalue.go) -- see rpc.ts's JsonValue for the wire's own
84
+ * restrictions (no booleans, bytes as hex strings).
85
+ *
86
+ * Resolves with the RESULT's payload on success. Rejects with a
87
+ * MaculaCallError (rpc.ts) when a real BOLT#4 ERROR frame came back
88
+ * instead -- e.g. `unknown_next_peer` for a procedure nobody has
89
+ * advertised -- carrying its code/name/retryable triple rather than
90
+ * a generic message; rejects with a plain Error for everything that
91
+ * isn't a wire-level answer at all (a local timeout, a dead session,
92
+ * a payload the wire can't represent).
93
+ *
94
+ * Real network I/O -- a signed frame out and a wait for the reply,
95
+ * up to opts.deadlineMs -- so this runs off the main thread on the
96
+ * native side, like connect()/close(). Do not call this
97
+ * concurrently with an active serve() on the SAME Session: both read
98
+ * frames off the one shared control stream, and macula-go's own
99
+ * ServeOneCall/Call docs both warn that mixing roles on one
100
+ * connection races (an unrelated frame arriving first is discarded,
101
+ * not queued) -- open a second Session for the other role instead,
102
+ * exactly what this SDK's own live test does. */
103
+ call(procedure: string, payload: JsonValue, opts?: CallOptions): Promise<JsonValue>;
104
+ /** Caller role: call(), attaching `ucanToken` to the outgoing CALL
105
+ * (macula-go's `connection.Session.CallWithUCAN`) -- for invoking a
106
+ * procedure a provider has gated behind a `ucan.Policy.Required`
107
+ * policy on its own side (this SDK does not implement that provider
108
+ * side itself -- see ucan.ts's own module doc). `ucanToken` may be a
109
+ * `Ucan` (as returned by `Ucan.mint()`/`Ucan.decode()` -- its `.token`
110
+ * is attached) or a raw token string directly.
111
+ *
112
+ * Same resolve/reject shape as `call()` in every other respect: the
113
+ * RESULT's payload on success, a `MaculaCallError` for a real BOLT#4
114
+ * ERROR frame (e.g. `unauthorized` for a token that fails the
115
+ * provider's policy check, or `unknown_next_peer` for a procedure
116
+ * nobody has advertised), a plain `Error` for anything that never got
117
+ * a wire-level answer at all.
118
+ *
119
+ * Deliberately attaches whatever token bytes it is given with NO local
120
+ * check relating this Session's own identity to `ucanToken`'s `aud`
121
+ * claim -- see ucan.ts's own module doc for why: macula's UCAN gate is
122
+ * a bearer-token check (signature + expiry against the token's
123
+ * issuer), and the real wire-level gate never looks at the caller's
124
+ * identity against `aud` either. A client-side guard here would both
125
+ * reject configurations the mesh accepts fine and misrepresent a
126
+ * security property that isn't actually enforced.
127
+ *
128
+ * Real network I/O, off the main thread on the native side, and
129
+ * subject to the identical same-Session exclusivity rule as `call()`
130
+ * (`#requireHandleNotServing`) -- both end up on the same shared
131
+ * control stream. */
132
+ callWithUcan(procedure: string, payload: JsonValue, ucanToken: string | Ucan, opts?: CallOptions): Promise<JsonValue>;
133
+ /** Provider role: advertises `procedure` (macula-go's
134
+ * connection.Session.Advertise) and answers inbound CALLs against it
135
+ * forever, invoking `handler` for each one (payload in, reply or
136
+ * thrown error out -- `handler` may be async), until the returned
137
+ * stop function is called. A thrown/rejected `handler` becomes a
138
+ * BOLT#4 UnknownError reply carrying the thrown value's message as
139
+ * detail (matching macula-go's connection/serve.go, which maps every
140
+ * handler error to that one code); a `handler` that panics on the Go
141
+ * side instead (not reachable from here -- there is no Go code
142
+ * between this and the JS handler) would map to
143
+ * TemporaryRelayFailure, per that same file.
144
+ *
145
+ * Only one serve() registration is allowed per Session at a time --
146
+ * see call()'s own doc on why mixing roles (or two server loops) on
147
+ * one connection is unsafe, not just inadvisable; open a second
148
+ * Session for a second procedure instead of trying to serve two
149
+ * procedures off one.
150
+ *
151
+ * The returned stop function is async: it unadvertises the
152
+ * procedure (a real network write) and waits for the current poll
153
+ * tick to finish (up to rpc.ts's SERVE_POLL_MS) before resolving --
154
+ * there is no way to interrupt a Go-side wait already in flight, the
155
+ * same bounded-latency shape macula-go's own ServeForever has
156
+ * internally. */
157
+ serve(procedure: string, handler: (payload: JsonValue) => JsonValue | Promise<JsonValue>): Promise<() => Promise<void>>;
158
+ /** DHT: returns every record of `recordType` currently visible from
159
+ * the station this Session is connected to (macula-go's
160
+ * dht.FindRecordsByType, via a signed CALL to `_dht.find_records_by_type`
161
+ * under the DHT's own reserved realm -- threaded internally, this
162
+ * method never touches a realm itself). Coverage depends on that
163
+ * station's own view of the mesh, not a global guarantee. Neither
164
+ * this nor findRecord/findRecords verifies a returned record's
165
+ * signature or checks its expiry -- see DhtRecord's own doc (dht.ts).
166
+ *
167
+ * Real network I/O, off the main thread on the native side, exactly
168
+ * like call() -- and subject to the same same-Session exclusivity
169
+ * rule as call() (see #requireHandleNotServing's own doc): do not
170
+ * call this while serve() is active on the same Session. */
171
+ findRecordsByType(recordType: DhtRecordType | number): Promise<DhtRecord[]>;
172
+ /** DHT: returns every record stored at `key` -- the full
173
+ * signer-deduped multiset at that storage key (macula-go's
174
+ * dht.FindRecords), e.g. every procedure_advertisement for one
175
+ * procedure, not just the first one found. `key` must be exactly 32
176
+ * bytes -- see dht/record.go's ProcedureKey/StationEndpointKey/
177
+ * ContentKey (macula-go) for how those are derived from the thing
178
+ * being looked up. Same I/O and exclusivity notes as
179
+ * findRecordsByType(). */
180
+ findRecords(key: Uint8Array): Promise<DhtRecord[]>;
181
+ /** DHT: returns ONE record by storage key (macula-go's
182
+ * dht.FindRecord). Resolves `null` when none exists -- mirrors
183
+ * macula-go's own dht.ErrNotFound, translated to a value instead of a
184
+ * thrown error since "not found" is an expected, routine outcome
185
+ * here, not exceptional. Same I/O and exclusivity notes as
186
+ * findRecordsByType(). */
187
+ findRecord(key: Uint8Array): Promise<DhtRecord | null>;
188
+ /** DHT: builds the realm-qualified discovery URI (macula-go's
189
+ * dht.DiscoveryURI), then builds (dht.NewProcedureAdvertisement),
190
+ * signs, and stores a procedure_advertisement naming this Session's
191
+ * own Identity as `procedure`'s advertiser and `servingStation` (32
192
+ * bytes -- a station's NodeID, typically this Session's own
193
+ * `stationNodeId`) as the station that serves it.
194
+ *
195
+ * `realm` should be the SAME realm `procedure` is (or will be) served
196
+ * under via `serve()` -- defaults to the all-zero realm, matching
197
+ * `call()`'s own default (see CallOptions.realm's own doc: `call()`/
198
+ * `callWithUcan()`/`publish()`/`subscribe()` now all take an optional
199
+ * realm; `serve()`/`advertise` remain all-zero-realm-only for this
200
+ * slice, unchanged). This method
201
+ * builds the qualified URI itself (dht.DiscoveryURI) rather than
202
+ * taking a pre-qualified string, since NewProcedureAdvertisement's own
203
+ * doc is explicit that "the advertiser and the resolver must derive
204
+ * the identical URI or the DHT storage key will not agree" -- a
205
+ * caller-supplied pre-qualified string invites exactly that class of
206
+ * bug (verified directly: an earlier draft of this SDK's own live
207
+ * test got this wrong by hand-qualifying the URI itself, and its
208
+ * following findRecord() came back not-found until this method built
209
+ * the URI internally instead).
210
+ *
211
+ * Wraps macula-go's REAL constructor rather than a generic
212
+ * JSON-payload builder deliberately -- see cabi/dht.go's own doc on
213
+ * why: two of this record type's payload fields (advertiser_node,
214
+ * serving_station) are raw 32-byte pubkeys that must be actual CBOR
215
+ * byte strings for a real resolver to read, and this SDK's generic
216
+ * JSON<->cbor.Value conversion (rpc.ts's JsonValue, wirevalue.go) has
217
+ * no way to produce those going IN -- only OUT, as "0x"-prefixed hex
218
+ * (see DhtRecord's own doc). `ttlMs` defaults to DHT_DEFAULT_TTL_MS
219
+ * (48h). Resolves with the signed record actually stored. Same I/O
220
+ * and exclusivity notes as findRecordsByType(). */
221
+ putProcedureAdvertisement(procedure: string, servingStation: Uint8Array, opts?: {
222
+ realm?: Uint8Array;
223
+ ttlMs?: number;
224
+ }): Promise<DhtRecord>;
225
+ /** DHT: builds (macula-go's dht.NewContentAnnouncement), signs, and
226
+ * stores a content_announcement naming this Session's own Identity as
227
+ * `mcid`'s (34 bytes) announcer, reachable at `endpoint` (a dialable
228
+ * seed URL, e.g. "https://host:4433" -- NOT a station_endpoint's
229
+ * split host/port). Same reasoning as putProcedureAdvertisement()'s
230
+ * own doc for wrapping macula-go's real constructor instead of a
231
+ * generic JSON-payload builder (announcer_node/mcid are the same kind
232
+ * of raw-byte field). `ttlMs` defaults to DHT_DEFAULT_TTL_MS (48h).
233
+ * Same I/O and exclusivity notes as findRecordsByType(). */
234
+ putContentAnnouncement(mcid: Uint8Array, endpoint: string, ttlMs?: number): Promise<DhtRecord>;
235
+ /** Direct-dial (caller side): finds `procedure`'s currently-advertised
236
+ * serving station and its dialable host/port (macula-go's
237
+ * `directdial.Resolve`), via this Session used only to query the DHT
238
+ * -- it does not need to be connected to the station that ends up
239
+ * serving `procedure`. Retries past DHT propagation lag internally (up
240
+ * to ~5s, macula-go's own fixed schedule, not configurable here); a
241
+ * `procedure` nobody ever called `advertiseDirect()` for rejects
242
+ * cleanly after that window (a plain `Error` wrapping macula-go's
243
+ * `ErrProcedureNotAdvertised`), never a hang.
244
+ *
245
+ * `opts.realm` must match whatever realm `procedure` was
246
+ * `advertiseDirect()`d under, or the discovery URI the two sides
247
+ * derive disagrees and this rejects the same way as if nothing was
248
+ * ever advertised at all -- see `AdvertiseDirectOptions.realm`'s own
249
+ * doc (directdial.ts). `callDirect()`/`callDirectWithUcan()` call this
250
+ * internally; it's exposed on its own for callers that just want the
251
+ * resolved station/host/port without also dialing and calling it (e.g.
252
+ * diagnostics).
253
+ *
254
+ * Real network I/O, off the main thread on the native side, subject to
255
+ * the same same-Session exclusivity rule as `call()`/the DHT methods
256
+ * (`#requireHandleNotServing`). */
257
+ resolveDirect(procedure: string, opts?: {
258
+ realm?: string;
259
+ }): Promise<DirectDialTarget>;
260
+ /** Direct-dial (caller side): resolves `procedure`'s provider (via
261
+ * `resolveDirect()`, through this Session) and calls it there, in one
262
+ * hop, in a SEPARATE connection macula-go opens, application-layer-pins
263
+ * against the resolved station identity, and closes again, entirely
264
+ * internally (macula-go's `directdial.Call`) -- this Session's own
265
+ * connection is never touched beyond the DHT lookup. The provider must
266
+ * have `advertiseDirect()`d `procedure` (or the Erlang/Rust/Go
267
+ * equivalent) -- a plain `serve()`-side `Advertise` alone publishes no
268
+ * discoverable DHT record, and this rejects with `ErrProcedureNotAdvertised`.
269
+ *
270
+ * Same resolve/reject shape as `call()`: the RESULT's payload on
271
+ * success, a `MaculaCallError` (rpc.ts) for a real BOLT#4 ERROR frame
272
+ * from the resolved provider, a plain `Error` for a resolve failure, a
273
+ * dial failure, an identity-trust violation (the dialed peer proved a
274
+ * DIFFERENT identity than the DHT chain resolved), or anything else
275
+ * that never got a wire-level answer at all.
276
+ *
277
+ * Real network I/O (DHT lookups, a fresh QUIC dial, then the CALL
278
+ * itself) -- off the main thread on the native side, subject to the
279
+ * same same-Session exclusivity rule as `call()` for THIS Session's own
280
+ * DHT-querying use (the separate dialed connection this opens
281
+ * internally is not this Session and is never exposed as one). */
282
+ callDirect(procedure: string, payload: JsonValue, opts?: CallOptions): Promise<JsonValue>;
283
+ /** Direct-dial (caller side): `callDirect()`, attaching `ucanToken` to
284
+ * the outgoing CALL (macula-go's `directdial.CallWithUCAN`) -- for
285
+ * reaching a direct-dial-advertised procedure a provider has gated
286
+ * behind a `ucan.Policy.Required` policy. Every hecate-om capability is
287
+ * advertised via `advertiseDirect()` specifically so it's reachable
288
+ * ONLY this way -- plain `callDirect()` cannot resolve or attach a
289
+ * token to it. Same `ucanToken` shape, no-audience-check, and
290
+ * resolve/reject conventions as `callWithUcan()` (see both that
291
+ * method's and ucan.ts's own doc for why: macula's UCAN gate is a
292
+ * bearer-token check, not an audience match).
293
+ *
294
+ * Same I/O and exclusivity notes as `callDirect()`. */
295
+ callDirectWithUcan(procedure: string, payload: JsonValue, ucanToken: string | Ucan, opts?: CallOptions): Promise<JsonValue>;
296
+ /** Direct-dial (provider side): publishes `procedure` as
297
+ * direct-dial-reachable at THIS Session's own currently-connected
298
+ * station (macula-go's `directdial.AdvertiseDirect`) -- a plain
299
+ * ADVERTISE (so an inbound CALL routed here via the DHT-resolved path
300
+ * still has something to route to -- a real bug macula-go fixed live
301
+ * 2026-08-30: skipping this let resolve+dial complete cleanly against a
302
+ * station with nothing registered to answer, `ServeOneCall` never
303
+ * seeing it) plus a signed `procedure_advertisement` DHT record naming
304
+ * this Session's own station.
305
+ *
306
+ * Unlike `serve()`'s own internal advertise (still all-zero-realm-only
307
+ * in this SDK -- see `serve()`'s own doc), this method threads
308
+ * `opts.realm` all the way through, matching `directdial.AdvertiseDirect`
309
+ * itself: `resolveDirect()`/`callDirect()`/`callDirectWithUcan()` only
310
+ * ever reach a procedure `advertiseDirect()`d under the EXACT SAME
311
+ * realm.
312
+ *
313
+ * A station's registration for a procedure does not survive the
314
+ * connection that sent it being replaced -- a long-lived provider needs
315
+ * to call this again on its own schedule; see `keepAdvertisedDirect()`
316
+ * (directdial.ts) for that loop, built on top of this method rather
317
+ * than duplicating macula-go's own `KeepAdvertisedDirect` here.
318
+ *
319
+ * **Must NOT be called on a Session that is also actively
320
+ * `serve()`-ing** -- enforced by the same `#requireHandleNotServing`
321
+ * guard `call()`/the DHT methods use, for the identical reason: this
322
+ * method's own `PutRecord` CALL reads a RESULT off the same shared
323
+ * control stream `serve()`'s poll loop is also reading, and the two
324
+ * would race (matches `directdial.AdvertiseDirect`'s own doc). A
325
+ * provider that also serves `procedure` needs a SEPARATE Session (and
326
+ * identity -- this fleet enforces one connection per identity, kicking
327
+ * whichever connects second) to call this on.
328
+ *
329
+ * Real network I/O (a fire-and-forget ADVERTISE write plus a signed
330
+ * PutRecord CALL) -- off the main thread on the native side. */
331
+ advertiseDirect(procedure: string, opts?: AdvertiseDirectOptions): Promise<void>;
332
+ /** Pubsub: sends a signed PUBLISH for `topic` (macula-go's
333
+ * connection.Session.Publish, which also attaches the end-to-end
334
+ * publisher_sig a relayed EVENT needs to survive beyond one hop --
335
+ * see that method's own doc, not reimplemented here). `payload`
336
+ * follows the same JsonValue rules as call()'s payload (no boolean,
337
+ * embedded bytes as "0x"-prefixed hex). Fire-and-forget: Publish's
338
+ * own doc is explicit that no reply is expected on the wire, so the
339
+ * returned Promise resolving only means this Session's own frame was
340
+ * encoded, signed, and sent -- never that any subscriber received it
341
+ * (macula-go's own live test for this, TestLivePubSubRoundTrip,
342
+ * observes a subscriber's own publish arriving back at it rather than
343
+ * asserting it as a hard guarantee, for the same reason).
344
+ *
345
+ * Deliberately NOT guarded by the same-Session exclusivity rule
346
+ * call()/serve()/subscribe()/the DHT methods share (see
347
+ * #requireHandleNotServing's own doc) -- publish() only ever writes,
348
+ * never reads off the shared control stream, so it does not race a
349
+ * concurrent serve()/subscribe() the way those do, and can run safely
350
+ * on the SAME Session a subscribe() of its own is active on -- exactly
351
+ * what a subscriber publishing to (and receiving) its own topic needs.
352
+ *
353
+ * `opts.realm` (see CallOptions.realm's own doc for the hex-string
354
+ * format and exact-match semantics) scopes which realm this EVENT is
355
+ * published under -- omitted means the all-zero realm, this SDK's
356
+ * sole default before this option existed. A subscribe() only
357
+ * receives this event if its own realm matches exactly.
358
+ *
359
+ * Real network I/O (one signed frame write) -- runs off the main
360
+ * thread on the native side, like every other network-touching method
361
+ * here. */
362
+ publish(topic: string, payload: JsonValue, opts?: PublishOptions): Promise<void>;
363
+ /** Pubsub: sends a signed SUBSCRIBE for `topic`, then delivers every
364
+ * inbound EVENT for it to `handler` -- macula-go's own
365
+ * connection.Session.RunSubscriber (connection/subscriber.go) drives
366
+ * the actual read loop on the Go side, in a background goroutine, NOT
367
+ * reimplemented on top of a hand-rolled poll here (see cabi/pubsub.go's
368
+ * own doc for why RunSubscriber specifically, over the lower-level
369
+ * RecvEvent). Delivery is Go-driven, not JS-driven: unlike serve()'s
370
+ * poll loop, nothing on this side calls into the native layer
371
+ * repeatedly to ask "did anything arrive yet" -- the addon calls INTO
372
+ * this handler asynchronously, via a Napi::ThreadSafeFunction wired to
373
+ * that background goroutine, whenever an EVENT actually shows up.
374
+ *
375
+ * Only one subscribe() (and no active serve()) is allowed per Session
376
+ * at a time -- same reasoning as serve()'s own one-at-a-time rule
377
+ * (#requireHandleNotServing's own doc): the background reader and any
378
+ * other read off this Session's shared control stream would race.
379
+ * Open a second Session for a second topic (or to serve/call
380
+ * concurrently) instead.
381
+ *
382
+ * Resolves with an async stop() function once the initial SUBSCRIBE
383
+ * has been sent and the background reader has started. stop() sends
384
+ * the matching UNSUBSCRIBE and does not resolve until the Go-side
385
+ * reader goroutine has genuinely exited -- calling it and awaiting the
386
+ * result is the actual guarantee that no further `handler` call can
387
+ * happen afterward, not just that one was requested.
388
+ *
389
+ * Real network I/O (the initial SUBSCRIBE send, and stop()'s
390
+ * UNSUBSCRIBE) -- both run off the main thread on the native side.
391
+ *
392
+ * `opts.realm` (see CallOptions.realm's own doc for the hex-string
393
+ * format and exact-match semantics) scopes which realm this SUBSCRIBE
394
+ * listens on -- omitted means the all-zero realm, this SDK's sole
395
+ * default before this option existed. Only an EVENT published under
396
+ * the SAME realm is ever delivered to `handler`.
397
+ *
398
+ * If the underlying connection dies (or any other transport error
399
+ * ends the background reader) rather than the returned stop() being
400
+ * called, this subscription tears itself down automatically -- the
401
+ * native handle is released and this Session is left closable and
402
+ * reusable for a fresh subscribe()/serve()/call() -- and, if
403
+ * provided, `opts.onClosed` is called once with the error. Verified
404
+ * live that, without this, such a subscription went silent forever
405
+ * (no further events, no error) and left this Session's handle
406
+ * permanently open even after close(). */
407
+ subscribe(topic: string, handler: (evt: PubsubEvent) => void, opts?: SubscribeOptions): Promise<() => Promise<void>>;
408
+ /** Content transfer: stores `data` (macula-go's content.Put, on this
409
+ * Session's own fresh dedicated QUIC stream -- Session.
410
+ * OpenDedicatedStream on the Go side, NOT the shared control stream
411
+ * call()/serve()/the DHT methods/subscribe() all read from), chunking
412
+ * automatically above manifest.DefaultChunkSize (256 KiB) and
413
+ * returning the hex-encoded mcid it's now addressable by. `name` is
414
+ * used ONLY on the chunked path (attached to the resulting manifest)
415
+ * -- a single-block put ignores it entirely, matching content.Put's
416
+ * own documented behavior; leave it unset for small blobs.
417
+ *
418
+ * NOT durable object storage -- see content.ts's own module doc: a
419
+ * station may forget this content later, and there is no list/delete
420
+ * operation. Treat this as "hand these bytes to a peer once."
421
+ *
422
+ * Because Put opens its own dedicated stream instead of reading the
423
+ * shared control stream, this is, unlike call()/serve()/the DHT
424
+ * methods/subscribe(), never subject to Session's same-Session
425
+ * exclusivity guard (#requireHandleNotServing) -- it can run
426
+ * concurrently with an active serve()/subscribe() (or another
427
+ * putContent()/getContent()) on the same Session without racing.
428
+ *
429
+ * Real network I/O (one or more signed CALLs on the new stream) --
430
+ * runs off the main thread on the native side, like every other
431
+ * network-touching method here. */
432
+ putContent(data: Uint8Array, name?: string): Promise<{
433
+ mcid: string;
434
+ }>;
435
+ /** Content transfer: fetches and verifies (macula-go's content.Get,
436
+ * on its own fresh dedicated QUIC stream, same reasoning as
437
+ * putContent() -- including content.Get's own client-side hash
438
+ * re-check against `mcid`: a station may only be relaying content it
439
+ * doesn't itself store, so its answer is never trusted blindly) the
440
+ * content addressed by `mcid` (the hex string putContent() returned).
441
+ *
442
+ * Rejects with ContentNotFoundError (content.ts) specifically when
443
+ * the station reports it doesn't know this mcid -- an expected,
444
+ * routine outcome for a one-time transfer mechanism with no
445
+ * durability guarantee, not a transport failure; every other failure
446
+ * (a bad session, a malformed mcid, a real transport error) rejects
447
+ * with a plain Error instead.
448
+ *
449
+ * Same dedicated-stream, no-exclusivity-guard reasoning as
450
+ * putContent() -- safe alongside an active serve()/subscribe() on the
451
+ * same Session. Real network I/O, runs off the main thread. */
452
+ getContent(mcid: string): Promise<Uint8Array>;
453
+ }