@rine-network/core 0.5.2 → 0.6.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.
@@ -133,6 +133,20 @@ export interface WebhookDeliveryRead {
133
133
  last_error?: string;
134
134
  created_at: string;
135
135
  }
136
+ export interface HookRead {
137
+ hook_name: string;
138
+ hostname: string;
139
+ active: boolean;
140
+ created_at: string;
141
+ control_ws_url: string;
142
+ }
143
+ export interface HookCreated extends HookRead {
144
+ agent_id: string;
145
+ }
146
+ export interface HookListResponse {
147
+ items: HookRead[];
148
+ total: number;
149
+ }
136
150
  export interface VoteResponse {
137
151
  request_id: string;
138
152
  your_vote: string;
@@ -0,0 +1,43 @@
1
+ import { Client } from "acme-client";
2
+ import type { HttpClient } from "../http.js";
3
+ import { type AcmeOptions } from "./cert-cache.js";
4
+ import type { IssueCertDeps } from "./issue.js";
5
+ export { waitForTxtPropagation } from "./propagation.js";
6
+ /**
7
+ * Tag an LE rate-limit failure so backoff.ts classifies it as TERMINAL. Matches
8
+ * the stable LE stem `too many certificates` so the modern parenthetical-count
9
+ * wording (`too many certificates (5) already issued ...`) is still caught —
10
+ * see backoff.ts isTerminalAcmeError, which uses the same stems.
11
+ */
12
+ export declare function tagLeRateLimit<E extends Error>(err: E): E;
13
+ /**
14
+ * POST to the backend dns-challenge endpoint, honoring a 429 + Retry-After
15
+ * (the server-side rate-limit backstop, REQ-CERT-07). The body carries ONLY
16
+ * {action, value?} — never the FQDN, never key material.
17
+ */
18
+ export declare function makeDnsPost(http: HttpClient, agentId: string, agentHeaders?: Record<string, string>): IssueCertDeps["dnsPost"];
19
+ /**
20
+ * Build the production ACME deps. The acme-client `Client` is the order driver;
21
+ * a per-issuance closure holds the order/challenge between newOrder→validate→
22
+ * download so the orchestrator's step boundaries map onto the RFC 8555 flow.
23
+ */
24
+ export declare function makeAcmeClientDeps(client: Client): Pick<IssueCertDeps, "acme">;
25
+ /**
26
+ * Load (or mint + persist 0600) the ONE shared ACME account key for this relay,
27
+ * then build an `acme-client` Client against the selected LE directory. One LE
28
+ * account is reused across all of the relay's hooks (REQ-CERT-12), so repeated
29
+ * issuances don't churn accounts. The account key never leaves the box.
30
+ */
31
+ export declare function buildAcmeClient(configDir: string, options: AcmeOptions): Promise<Client>;
32
+ /**
33
+ * Assemble the full production `IssueCertDeps` the relay (Track E) injects into
34
+ * `ensureCert` / `revokeAndWipeCache`. Pure wiring: the ACME client drives the
35
+ * order, the HttpClient brokers the TXT, node:dns confirms propagation.
36
+ */
37
+ export declare function buildIssueCertDeps(args: {
38
+ configDir: string;
39
+ options: AcmeOptions;
40
+ http: HttpClient;
41
+ agentId: string;
42
+ agentHeaders?: Record<string, string>;
43
+ }): Promise<IssueCertDeps>;
@@ -0,0 +1,22 @@
1
+ import type { CertMeta } from "./cert-cache.js";
2
+ /**
3
+ * Full-jitter exponential backoff: a uniform random delay in `[0, ceiling]`
4
+ * where `ceiling = base * factor^(n-1)`, capped at 1h. Full jitter (not just a
5
+ * jittered fixed delay) is what de-correlates a fleet of crash-looping relays.
6
+ * `n` is the 1-based consecutive-failure count (n<=0 is treated as the first).
7
+ */
8
+ export declare function nextBackoffDelayMs(consecutiveFailures: number): number;
9
+ /**
10
+ * A cert is due for renewal when `not_after - now <= 30 days` (boundary
11
+ * inclusive). An already-expired cert is due (and forces a cold issuance).
12
+ */
13
+ export declare function isRenewalDue(meta: CertMeta, nowMs: number): boolean;
14
+ /**
15
+ * TERMINAL = an LE rate-limit ceiling that no amount of retrying can clear in
16
+ * the near term (the duplicate-certificate limit: 5 identical FQDN sets / 7
17
+ * days, and the per-account failed-validation limit). On a terminal error the
18
+ * relay stops attempting and surfaces it rather than burning the LE budget.
19
+ * Ordinary transient failures (timeouts, propagation, provider 502) are NOT
20
+ * terminal — they retry on the backoff schedule.
21
+ */
22
+ export declare function isTerminalAcmeError(err: unknown): boolean;
@@ -0,0 +1,52 @@
1
+ /** ACME directory selection. `RINE_ACME_STAGING=1` env is also honored. */
2
+ export type AcmeOptions = {
3
+ staging?: boolean;
4
+ };
5
+ /**
6
+ * Resolve the ACME directory URL. Production by default; staging when either
7
+ * `opts.staging` is set or `RINE_ACME_STAGING=1` is in the environment, so dev
8
+ * never burns the production LE rate limits. Shares the single `isStaging`
9
+ * decision with the cache-path split so a cert's directory_url and its cache
10
+ * subtree are always derived from the same flag.
11
+ */
12
+ export declare function acmeDirectoryUrl(opts: AcmeOptions): string;
13
+ /**
14
+ * Order/renewal metadata persisted alongside the cert. `consecutive_failures`
15
+ * is the persisted ACME-storm backoff counter (REQ-CERT-13) — it survives a
16
+ * crash-loop so a buggy relay can never reset its own backoff to zero on boot.
17
+ */
18
+ export interface CertMeta {
19
+ directory_url: string;
20
+ not_after: string;
21
+ /**
22
+ * The cert handle, kept as a local cache label only (historical field name —
23
+ * there is no DNS delegation anymore; the TXT lives directly at
24
+ * `_acme-challenge.<cert-domain>.hook.rine.network`). Drives nothing security-
25
+ * relevant; it only labels the cache and seeds the never-hit poll fallback.
26
+ */
27
+ delegation_id: string;
28
+ last_attempt_ts: number;
29
+ consecutive_failures: number;
30
+ }
31
+ export interface CachedCert {
32
+ certPem: string;
33
+ keyPem: string;
34
+ meta: CertMeta;
35
+ }
36
+ /** `${configDir}/funnel` (prod) or `${configDir}/funnel/staging` (staging). */
37
+ export declare function funnelCacheDir(configDir: string, opts: AcmeOptions): string;
38
+ /** The per-handle cache dir under the (staging-segregated) funnel root. */
39
+ export declare function handleCacheDir(configDir: string, handle: string, opts: AcmeOptions): string;
40
+ /** One LE account key per relay, shared across handles, at the funnel root. */
41
+ export declare function accountKeyPath(configDir: string, opts: AcmeOptions): string;
42
+ export declare function saveMeta(configDir: string, handle: string, opts: AcmeOptions, meta: CertMeta): void;
43
+ export declare function loadMeta(configDir: string, handle: string, opts: AcmeOptions): CertMeta | null;
44
+ export declare function saveCert(configDir: string, handle: string, opts: AcmeOptions, cert: CachedCert): void;
45
+ /**
46
+ * Returns the cached cert ONLY if cert+key+meta are all present AND the meta's
47
+ * directory_url matches the requested mode (staging cert never served in prod).
48
+ * Any partial/corrupt cache reads back as `null` — never a half-cert.
49
+ */
50
+ export declare function loadCachedCert(configDir: string, handle: string, opts: AcmeOptions): CachedCert | null;
51
+ /** Scoped wipe of one handle's cache dir (REQ-CERT-16). Other handles untouched. */
52
+ export declare function wipeHandleCache(configDir: string, handle: string, opts: AcmeOptions): void;
@@ -0,0 +1,45 @@
1
+ import { type AcmeOptions } from "./cert-cache.js";
2
+ import { type IssueCertDeps, type IssuedCert } from "./issue.js";
3
+ export type { AcmeOptions, CachedCert, CertMeta } from "./cert-cache.js";
4
+ export { accountKeyPath, acmeDirectoryUrl, funnelCacheDir, handleCacheDir, loadCachedCert, loadMeta, saveCert, saveMeta, wipeHandleCache, } from "./cert-cache.js";
5
+ export { isRenewalDue, isTerminalAcmeError, nextBackoffDelayMs, } from "./backoff.js";
6
+ export { type KeypairAndCsr, generateCertKeypairAndCsr, hookFqdn, } from "./csr.js";
7
+ export { type IssueCertDeps, type IssuedCert, issueCert } from "./issue.js";
8
+ export interface EnsureCertResult extends IssuedCert {
9
+ ready: boolean;
10
+ fromCache: boolean;
11
+ }
12
+ /**
13
+ * Cold-start readiness gate (REQ-CERT-32) + cache reuse + persisted backoff
14
+ * (REQ-CERT-13). A valid, fresh cached cert opens the gate immediately with NO
15
+ * ACME round-trip. Otherwise issue, persist atomically, and reset the failure
16
+ * counter on success. On failure the persisted `consecutive_failures` is bumped
17
+ * (read from the prior meta so a crash-loop never resets it to zero) and the
18
+ * error re-thrown — a failed cold start installs NOTHING (gate stays closed).
19
+ */
20
+ export declare function ensureCert(args: {
21
+ handle: string;
22
+ agentId: string;
23
+ /**
24
+ * The cert handle (the relay passes its own `<handle>`). Persisted as
25
+ * `meta.delegation_id` purely as a local cache label and used to build the
26
+ * never-hit propagation-poll fallback FQDN — the server always returns the
27
+ * authoritative `_acme-challenge.<cert-domain>.hook.rine.network` directly.
28
+ */
29
+ delegationId: string;
30
+ configDir: string;
31
+ options: AcmeOptions;
32
+ deps: IssueCertDeps;
33
+ }): Promise<EnsureCertResult>;
34
+ /**
35
+ * Graceful teardown (REQ-CERT-16): best-effort ACME revoke (relay-local — the
36
+ * server holds no key and cannot revoke, REQ-CERT-15), then a SCOPED wipe of
37
+ * just this handle's cache dir. A revoke failure (offline) is swallowed; the
38
+ * local wipe is the meaningful local effect and always runs.
39
+ */
40
+ export declare function revokeAndWipeCache(args: {
41
+ handle: string;
42
+ configDir: string;
43
+ options: AcmeOptions;
44
+ deps: IssueCertDeps;
45
+ }): Promise<void>;
@@ -0,0 +1,19 @@
1
+ export { acmeDirectoryUrl } from "./cert-cache.js";
2
+ /** The publicly-trusted hostname this relay terminates TLS for. */
3
+ export declare function hookFqdn(handle: string): string;
4
+ export interface KeypairAndCsr {
5
+ /** Cert private key (PKCS#8 PEM) — LOCAL storage only, never transmitted. */
6
+ keyPem: string;
7
+ /** CSR (PEM) — the only artifact sent to LE; carries the public key only. */
8
+ csrPem: string;
9
+ commonName: string;
10
+ subjectAltNames: string[];
11
+ keyAlgorithm: "EC";
12
+ keyCurve: string;
13
+ }
14
+ /**
15
+ * Generate a fresh EC P-256 keypair (node:crypto) and a CSR for
16
+ * `<handle>.hook.rine.network` whose CN and single SAN are that FQDN. A fresh,
17
+ * unique key is minted on every call — no key reuse across issuances/renewals.
18
+ */
19
+ export declare function generateCertKeypairAndCsr(handle: string): Promise<KeypairAndCsr>;
@@ -0,0 +1,6 @@
1
+ export * from "./cert.js";
2
+ export * from "./acme.js";
3
+ export * from "./tunnel.js";
4
+ export * from "./tls-listener.js";
5
+ export * from "./relay-adapter.js";
6
+ export * from "./secret-store.js";
@@ -0,0 +1,74 @@
1
+ import type { CertMeta } from "./cert-cache.js";
2
+ /** Injectable externals. ALL are mocked in unit tests — never hits LE/DNS/net. */
3
+ export interface IssueCertDeps {
4
+ acme: {
5
+ /** Place an ACME order; returns the DNS-01 token + the challenge types LE offered. */
6
+ newOrder(args: {
7
+ handle: string;
8
+ directoryUrl: string;
9
+ }): Promise<{
10
+ dnsChallengeToken: string;
11
+ challengeTypesOffered: string[];
12
+ }>;
13
+ /** Tell LE to validate the challenge (only after DNS has propagated). */
14
+ validate(args: {
15
+ handle: string;
16
+ }): Promise<void>;
17
+ /** Finalize the order with the CSR and download the issued chain. */
18
+ finalizeAndDownload(args: {
19
+ handle: string;
20
+ csrPem: string;
21
+ }): Promise<{
22
+ certPem: string;
23
+ }>;
24
+ /** Relay-local revocation using the relay's own cert+account key. */
25
+ revoke(args: {
26
+ certPem: string;
27
+ directoryUrl: string;
28
+ }): Promise<void>;
29
+ };
30
+ /**
31
+ * POST {action:"set",value} | {action:"clear"} to the dns-challenge endpoint.
32
+ * The `set` response carries `{status,fqdn}` where `fqdn` is
33
+ * `_acme-challenge.<cert-domain>.hook.rine.network` — the relative TXT rrset
34
+ * the backend wrote DIRECTLY in the single `rine.network` zone (no CNAME, no
35
+ * `acme.` delegation subzone). It is the source of truth for the propagation
36
+ * poll (the relay never derives it).
37
+ */
38
+ dnsPost(action: "set" | "clear", value?: string): Promise<DnsChallengeResult>;
39
+ /** Bounded poll of the authoritative DNS at `fqdn` until the TXT propagates. */
40
+ waitForPropagation(args: {
41
+ fqdn: string;
42
+ }): Promise<void>;
43
+ now(): number;
44
+ }
45
+ /** The dns-challenge endpoint response (REQ-CERT-08): `set` echoes the `fqdn`. */
46
+ export interface DnsChallengeResult {
47
+ status?: string;
48
+ fqdn?: string;
49
+ }
50
+ export interface IssuedCert {
51
+ certPem: string;
52
+ keyPem: string;
53
+ meta: CertMeta;
54
+ }
55
+ /**
56
+ * Run one ACME DNS-01 issuance. Sequence (REQ-CERT-10):
57
+ * newOrder -> dns:set -> waitForPropagation -> validate -> download -> dns:clear
58
+ * The TXT is cleared in a `finally` so a FAILED order still cleans up its record
59
+ * (no lingering challenge). Propagation is awaited BEFORE asking LE to validate
60
+ * so a slow zone never burns a validation attempt.
61
+ */
62
+ export declare function issueCert(args: {
63
+ handle: string;
64
+ agentId: string;
65
+ /**
66
+ * The cert handle — the relay passes its own `<handle>` here. Used only as a
67
+ * local cache label (persisted as `meta.delegation_id`) and to build the
68
+ * NEVER-HIT propagation-poll fallback FQDN below; the server always echoes the
69
+ * authoritative `fqdn` in its `set` response, so this fallback is dead in prod.
70
+ */
71
+ delegationId: string;
72
+ directoryUrl: string;
73
+ deps: IssueCertDeps;
74
+ }): Promise<IssuedCert>;
@@ -0,0 +1,40 @@
1
+ import { type Frame } from "./mux-codec.js";
2
+ /** Per-conn in-flight cap before the relay pauses reading from the broker. */
3
+ export declare const PER_CONN_INFLIGHT_CAP: number;
4
+ /** A local byte pipe (the TLS-terminating listener conn) the relay drives. */
5
+ export interface LocalPipe {
6
+ write(bytes: Uint8Array): void;
7
+ onData(cb: (bytes: Uint8Array) => void): void;
8
+ close(): void;
9
+ pause?(): void;
10
+ resume?(): void;
11
+ }
12
+ /** The minimal WS surface the driver needs (real WebSocket or a fake in tests). */
13
+ export interface MuxWs {
14
+ send(data: Uint8Array): void;
15
+ onBinary(cb: (data: Uint8Array) => void): void;
16
+ onClose?(cb: () => void): void;
17
+ close(): void;
18
+ }
19
+ export interface MuxClientArgs {
20
+ ws: MuxWs;
21
+ openLocalPipe(frame: Frame): LocalPipe;
22
+ maxFrameBytes?: number;
23
+ }
24
+ export interface MuxClient {
25
+ start(): void;
26
+ handleFrame(frame: Frame): void;
27
+ }
28
+ /**
29
+ * Build a mux client over `ws`. `start()` wires the WS binary + close callbacks;
30
+ * `handleFrame` is exposed so a caller (and the unit tests) can drive a single
31
+ * decoded frame directly.
32
+ */
33
+ export declare function createMuxClient(args: MuxClientArgs): MuxClient;
34
+ /**
35
+ * Reconnect backoff progression, identical to stream.ts: double, capped at 30s.
36
+ * `1 → 2 → 4 → 8 → 16 → 30 → 30`.
37
+ */
38
+ export declare function nextReconnectBackoffSeconds(backoffSeconds: number): number;
39
+ /** Full jitter over the current backoff window — identical to stream.ts. */
40
+ export declare function jitteredDelayMs(backoffSeconds: number): number;
@@ -0,0 +1,37 @@
1
+ /** Fixed mux header size: type(1) + conn_id(4) + len(4). */
2
+ export declare const HEADER_LEN = 9;
3
+ /** conn_id 0 is reserved for the control plane (BIND and keepalive). */
4
+ export declare const CONTROL_CONN_ID = 0;
5
+ /** Frame type discriminants. WINDOW (0x07) is intentionally absent. */
6
+ export declare const FrameType: {
7
+ readonly OPEN: 1;
8
+ readonly DATA: 2;
9
+ readonly CLOSE: 3;
10
+ readonly RESET: 4;
11
+ readonly BIND: 16;
12
+ readonly BIND_ACK: 17;
13
+ readonly BIND_NAK: 18;
14
+ };
15
+ export type FrameType = (typeof FrameType)[keyof typeof FrameType];
16
+ export interface Frame {
17
+ frameType: FrameType;
18
+ connId: number;
19
+ payload: Uint8Array;
20
+ }
21
+ /** A framing/protocol fault. The broker maps these to WS close codes (1002/1009). */
22
+ export declare class FrameError extends Error {
23
+ constructor(message: string);
24
+ }
25
+ /**
26
+ * Encode a frame into one WS-binary message body. Rejects an over-`maxFrameBytes`
27
+ * payload and any type the codec does not know (so the relay can never put a
28
+ * WINDOW/0x07 frame on the wire) before allocating.
29
+ */
30
+ export declare function encodeFrame(frame: Frame, maxFrameBytes: number): Uint8Array;
31
+ /**
32
+ * Decode one WS-binary message body into a Frame, enforcing every REQ-TUN-05
33
+ * rule: unknown/reserved type (incl. WINDOW 0x07), short header, declared-len
34
+ * mismatch, conn_id=0 on a data frame, non-zero conn_id on a control frame, and
35
+ * an over-`maxFrameBytes` payload.
36
+ */
37
+ export declare function decodeFrame(buf: Uint8Array, maxFrameBytes: number): Frame;
@@ -0,0 +1,11 @@
1
+ export declare function sleep(ms: number): Promise<void>;
2
+ /**
3
+ * Poll the authoritative DNS for the challenge TXT at the server-returned
4
+ * `_acme-challenge.<cert-domain>.hook.rine.network` until it resolves (bounded,
5
+ * ≤120s) before telling LE to validate, so a slow zone never burns a validation
6
+ * attempt. The FQDN is the one the backend wrote the TXT under (the
7
+ * dns-challenge `set` response's `fqdn`) — never derived from a relay-side guess.
8
+ */
9
+ export declare function waitForTxtPropagation(args: {
10
+ fqdn: string;
11
+ }): Promise<void>;
@@ -0,0 +1,27 @@
1
+ import type { HttpClient } from "../http.js";
2
+ export declare const WEBHOOK_MESSAGE_TYPE = "rine.v1.webhook";
3
+ /** A single inbound webhook request as the local TLS listener parsed it. */
4
+ export interface WebhookRequest {
5
+ headers: Record<string, string>;
6
+ rawBody: Uint8Array;
7
+ }
8
+ export interface RelayAdapterDeps {
9
+ configDir: string;
10
+ agentId: string;
11
+ hookName: string;
12
+ secret: string;
13
+ client: Pick<HttpClient, "get" | "post">;
14
+ /** Lifecycle sink (stream.ts shape). NEVER receives body/secret/signature. */
15
+ emitLifecycle?(ev: Record<string, unknown>): void;
16
+ }
17
+ export interface RelayResult {
18
+ verified: boolean;
19
+ relayed: boolean;
20
+ }
21
+ /**
22
+ * Verify → encrypt → self-send one webhook. Returns the outcome; never throws out
23
+ * (the caller's tunnel must survive a single bad request). A verify failure drops
24
+ * the conn with no post and no encrypt; a post/key-fetch failure drops the conn
25
+ * after verify with `relayed:false`.
26
+ */
27
+ export declare function handleWebhookRequest(deps: RelayAdapterDeps, req: WebhookRequest): Promise<RelayResult>;
@@ -0,0 +1,16 @@
1
+ export declare function isValidHookName(name: string): boolean;
2
+ /** Generate a 32-byte HMAC secret as 64 hex chars (client-side only). */
3
+ export declare function generateHookSecret(): string;
4
+ /** Directory holding one agent's funnel secrets. */
5
+ export declare function funnelSecretDir(configDir: string, agentId: string): string;
6
+ /** Absolute path of a hook's secret file. */
7
+ export declare function hookSecretPath(configDir: string, agentId: string, name: string): string;
8
+ /**
9
+ * Persist a hook secret atomically with file mode 0600 in a 0700 dir. Returns the
10
+ * path it wrote so callers can tell the operator where the secret lives.
11
+ */
12
+ export declare function saveHookSecret(configDir: string, agentId: string, name: string, secret: string): string;
13
+ /** Read a hook secret, or undefined if it is not stored locally. */
14
+ export declare function loadHookSecret(configDir: string, agentId: string, name: string): string | undefined;
15
+ /** Remove a hook secret (no error if it is already absent). */
16
+ export declare function deleteHookSecret(configDir: string, agentId: string, name: string): void;
@@ -0,0 +1,32 @@
1
+ import { type TLSSocket } from "node:tls";
2
+ import type { LocalPipe } from "./mux-client.js";
3
+ import type { Frame } from "./mux-codec.js";
4
+ import type { WebhookRequest } from "./relay-adapter.js";
5
+ export interface TlsListener {
6
+ port: number;
7
+ close(): void;
8
+ /** A LocalPipe factory for the mux client — one loopback conn per mux OPEN. */
9
+ openLocalPipe(frame: Frame): LocalPipe;
10
+ }
11
+ export interface TlsListenerArgs {
12
+ port: number;
13
+ certPem: string;
14
+ keyPem: string;
15
+ /** Handle one decoded request; resolves when the relay is done with it. */
16
+ onRequest(req: WebhookRequest): Promise<void>;
17
+ }
18
+ /**
19
+ * Stand up the loopback TLS listener. Each accepted socket is one webhook
20
+ * delivery: parse the HTTP request head + body, hand the RAW body to `onRequest`,
21
+ * then write a minimal 204/400 response and close (GitHub only needs a 2xx).
22
+ */
23
+ export declare function startTlsListener(args: TlsListenerArgs): Promise<TlsListener>;
24
+ /**
25
+ * Buffer one HTTP request off the TLS socket and dispatch as soon as the full
26
+ * body has arrived. A real HTTP/1.1 client (GitHub) sends the request then BLOCKS
27
+ * on the response WITHOUT half-closing, so triggering on the socket `"end"` (EOF)
28
+ * deadlocks until the client's own timeout. Instead we dispatch the moment we have
29
+ * the head plus a `Content-Length`-worth of body; `"end"` stays a fallback for the
30
+ * no-body / chunked / EOF-terminated cases. `handled` guards single dispatch.
31
+ */
32
+ export declare function handleConnection(socket: TLSSocket, onRequest: TlsListenerArgs["onRequest"]): void;
@@ -0,0 +1,46 @@
1
+ import { type LocalPipe } from "./mux-client.js";
2
+ import { type Frame } from "./mux-codec.js";
3
+ export * from "./mux-codec.js";
4
+ export * from "./mux-client.js";
5
+ /** Negotiated per-tunnel caps the broker returns in BIND_ACK. */
6
+ export interface TunnelCaps {
7
+ max_conns: number;
8
+ per_conn_inflight_bytes: number;
9
+ max_frame_bytes: number;
10
+ heartbeat_secs: number;
11
+ }
12
+ /**
13
+ * Why the control WS closed. `clean` (WS close 1000/1001 or a bare TCP drop with
14
+ * no abnormal code) maps to an orderly broker close — the relay reconnects
15
+ * immediately (REQ-TUN-14). Any other / abnormal code (1002/1009/1011/…) is an
16
+ * error and gets the jittered backoff.
17
+ */
18
+ export interface TunnelClose {
19
+ code: number;
20
+ clean: boolean;
21
+ }
22
+ /** A bind rejection (BIND_NAK / ownership failure). `revoked` stops the relay. */
23
+ export declare class TunnelBindError extends Error {
24
+ readonly revoked = true;
25
+ constructor(reason: string);
26
+ }
27
+ export interface ConnectTunnelArgs {
28
+ controlWsUrl: string;
29
+ token: string;
30
+ agentId: string;
31
+ hostname: string;
32
+ openLocalPipe(frame: Frame): LocalPipe;
33
+ }
34
+ export interface TunnelHandle {
35
+ caps: TunnelCaps;
36
+ close(): void;
37
+ /** The callback receives the classified close so the relay can pick its reconnect policy. */
38
+ onClose(cb: (close: TunnelClose) => void): void;
39
+ }
40
+ /**
41
+ * Open the control WS (Bearer at the upgrade — Node 24's global WebSocket takes
42
+ * a `{protocols, headers}` options object), perform the BIND handshake, and wire
43
+ * the mux driver. The returned handle resolves once BIND_ACK arrives; a BIND_NAK
44
+ * rejects with a TunnelBindError (the relay treats it as revoked and stops).
45
+ */
46
+ export declare function connectTunnel(args: ConnectTunnelArgs): Promise<TunnelHandle>;
@@ -9,6 +9,7 @@ export * from "./timelock.js";
9
9
  export * from "./sender-key-ops.js";
10
10
  export * from "./group-invite-ops.js";
11
11
  export * from "./crypto/index.js";
12
+ export * from "./funnel/index.js";
12
13
  export * from "./mls-ops.js";
13
14
  export { externalJoinMlsGroup } from "./mls-ops.js";
14
15
  export { performRegistration, performAgentCreation, validateSlug } from "./onboard.js";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rine-network/core",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Core library for rine.network — crypto, HTTP, config, agent resolution",
5
5
  "author": "mmmbs <mmmbs@proton.me>",
6
6
  "license": "EUPL-1.2",
@@ -29,6 +29,7 @@
29
29
  "@noble/curves": "^2.0.1",
30
30
  "@noble/hashes": "^2.0.1",
31
31
  "@noble/post-quantum": "^0.6.1",
32
+ "acme-client": "^5.4.0",
32
33
  "ts-mls": "^2.0.0-rc.13"
33
34
  },
34
35
  "devDependencies": {