@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.
- package/dist/index.js +1004 -4
- package/dist/src/api-types.d.ts +14 -0
- package/dist/src/funnel/acme.d.ts +43 -0
- package/dist/src/funnel/backoff.d.ts +22 -0
- package/dist/src/funnel/cert-cache.d.ts +52 -0
- package/dist/src/funnel/cert.d.ts +45 -0
- package/dist/src/funnel/csr.d.ts +19 -0
- package/dist/src/funnel/index.d.ts +6 -0
- package/dist/src/funnel/issue.d.ts +74 -0
- package/dist/src/funnel/mux-client.d.ts +40 -0
- package/dist/src/funnel/mux-codec.d.ts +37 -0
- package/dist/src/funnel/propagation.d.ts +11 -0
- package/dist/src/funnel/relay-adapter.d.ts +27 -0
- package/dist/src/funnel/secret-store.d.ts +16 -0
- package/dist/src/funnel/tls-listener.d.ts +32 -0
- package/dist/src/funnel/tunnel.d.ts +46 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/test/funnel/cert.test.d.ts +1 -0
- package/dist/test/funnel/no-body-log-structural.test.d.ts +1 -0
- package/dist/test/funnel/relay-adapter.test.d.ts +1 -0
- package/dist/test/funnel/tls-listener.test.d.ts +1 -0
- package/dist/test/funnel/tunnel.test.d.ts +1 -0
- package/dist/test/setup.d.ts +1 -0
- package/package.json +2 -1
package/dist/src/api-types.d.ts
CHANGED
|
@@ -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,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>;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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.
|
|
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": {
|