@restatedev/restate-sdk-tunnel 0.0.0-dev → 1.15.0-rc.3
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/LICENSE +21 -0
- package/README.md +175 -1
- package/dist/index.cjs +955 -0
- package/dist/index.d.cts +288 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +288 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +927 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -9
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["url: URL","port","targets","targets: Target[]","base: tls.ConnectionOptions","authToken: string","target: Target","slotSignal: AbortSignal","deps: ConnectionDeps","entry: DrainingConnection","initialMs: number","factor: number","maxMs: number","onAbort!: () => void","fatalError: Error | undefined","lastInfo: HandshakeInfo | undefined","readyResolve!: () => void","readyReject!: (err: Error) => void","connectionDeps: ConnectionDeps","slot: Slot","targets: Target[]"],"sources":["../src/targets.ts","../src/options.ts","../src/handshake.ts","../src/forwarded.ts","../src/connection.ts","../src/draining.ts","../src/backoff.ts","../src/util.ts","../src/connect.ts"],"sourcesContent":["/*\n * Copyright (c) 2023-2026 - Restate Software, Inc., Restate GmbH\n *\n * This file is part of the Restate SDK for Node.js/TypeScript,\n * which is released under the MIT license.\n *\n * You can find a copy of the license in file LICENSE in the root\n * directory of this repository or package, or at\n * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE\n */\n\n// Tunnel-server discovery: explicit addresses or region-based DNS SRV.\n\nimport * as dns from \"node:dns\";\n\n/** A dialable tunnel server. */\nexport interface Target {\n host: string;\n port: number;\n /**\n * TLS SNI / verification name. For SRV-discovered targets this is the SRV\n * QUERY name (`tunnel.<region>.restate.cloud` — what the cloud's cert\n * covers), regardless of which per-record host is dialed; for explicit\n * addresses it is the configured host.\n */\n servername: string;\n /**\n * Per-target plaintext override: set when an explicit `http://` URL was\n * given. `undefined` means \"follow the global `tls` option\".\n */\n plaintext?: boolean;\n}\n\n/**\n * Parse one explicit tunnel-server address: `\"host:port\"`, or a URL whose\n * scheme picks TLS (`https`) / plaintext (`http`) for that server.\n * Throws on a malformed address.\n */\nexport function parseServerAddress(address: string): Target {\n if (address.includes(\"://\")) {\n let url: URL;\n try {\n url = new URL(address);\n } catch {\n throw new Error(\n `tunnel: invalid tunnel server URL ${JSON.stringify(address)}`\n );\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new Error(\n `tunnel: unsupported tunnel server scheme ${JSON.stringify(url.protocol)} (use http or https)`\n );\n }\n if (url.pathname !== \"/\" || url.search !== \"\") {\n throw new Error(\n `tunnel: tunnel server URL must not have a path or query: ${JSON.stringify(address)}`\n );\n }\n const port =\n url.port !== \"\" ? Number(url.port) : url.protocol === \"https:\" ? 443 : 80;\n return {\n host: url.hostname,\n port,\n servername: url.hostname,\n plaintext: url.protocol === \"http:\",\n };\n }\n // \"host:port\" — split on the LAST colon so IPv6-ish hosts survive.\n const idx = address.lastIndexOf(\":\");\n if (idx <= 0 || idx === address.length - 1) {\n throw new Error(\n `tunnel: invalid tunnel server address ${JSON.stringify(address)} (expected \"host:port\" or a URL)`\n );\n }\n const host = address.slice(0, idx);\n const port = Number(address.slice(idx + 1));\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(\n `tunnel: invalid port in tunnel server address ${JSON.stringify(address)}`\n );\n }\n return { host, port, servername: host };\n}\n\n/**\n * Resolve the current set of tunnel servers. Called fresh per connection\n * attempt, so DNS changes are picked up across redials.\n *\n * - Explicit `tunnelServers`: parsed as-is (no DNS here — the dial resolves\n * the hostname).\n * - `srvName` (region-derived or given directly): a DNS SRV lookup, each\n * record expanded to ALL of its addresses (priority asc, weight desc).\n *\n * Error taxonomy (mirrors the Rust resolver): a NEGATIVE answer for an SRV\n * target (the name genuinely has no address — ENOTFOUND/ENODATA) removes\n * that target, and an all-negative answer yields an EMPTY list (the\n * supervisor then reconciles everything away, like Rust's empty set). A\n * TRANSPORT error (EAI_AGAIN, timeouts, SERVFAIL) THROWS instead — the\n * supervisor must keep the existing connections serving and retry, not\n * tear down healthy slots over a resolver blip.\n */\nexport async function resolveTargets(spec: {\n srvName?: string;\n tunnelServers?: string[];\n}): Promise<Target[]> {\n if (spec.tunnelServers !== undefined) {\n const targets = spec.tunnelServers.map(parseServerAddress);\n if (targets.length === 0) {\n throw new Error(\"tunnel: tunnelServers is empty\");\n }\n return targets;\n }\n const srvName = spec.srvName!;\n const records = await dns.promises.resolveSrv(srvName);\n records.sort((a, b) => a.priority - b.priority || b.weight - a.weight);\n // Expand each SRV target to its addresses: the tunnel connects to EVERY\n // resolved address (one connection per IP), exactly like the Rust client,\n // which flat-maps SRV targets through A/AAAA lookups into per-IP URIs.\n // Lookups run concurrently (Rust uses FuturesUnordered) so one slow\n // resolver doesn't serialize the rest. SNI / certificate verification\n // uses the SRV QUERY name (the cloud's cert covers the SRV name, not\n // per-node hostnames) — mirroring the Rust FixedServerNameResolver.\n const lookups = await Promise.allSettled(\n records.map((r) => dns.promises.lookup(r.name, { all: true }))\n );\n const targets: Target[] = [];\n const seen = new Set<string>();\n for (let i = 0; i < records.length; i++) {\n const r = records[i]!;\n const result = lookups[i]!;\n if (result.status === \"rejected\") {\n const code = (result.reason as NodeJS.ErrnoException | undefined)?.code;\n if (code === \"ENOTFOUND\" || code === \"ENODATA\") {\n continue; // negative answer: this SRV target genuinely has no address\n }\n // Transport error — fail the whole resolution so the supervisor\n // keeps existing slots and retries.\n throw result.reason;\n }\n for (const a of result.value) {\n const key = `${a.address}:${r.port}`;\n if (seen.has(key)) continue;\n seen.add(key);\n targets.push({ host: a.address, port: r.port, servername: srvName });\n }\n }\n return targets;\n}\n\n/** Stable identity of a target — the unit of one tunnel connection. */\nexport function targetKey(t: Target): string {\n return `${t.host}:${t.port}`;\n}\n","/*\n * Copyright (c) 2023-2026 - Restate Software, Inc., Restate GmbH\n *\n * This file is part of the Restate SDK for Node.js/TypeScript,\n * which is released under the MIT license.\n *\n * You can find a copy of the license in file LICENSE in the root\n * directory of this repository or package, or at\n * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE\n */\n\n// Option validation and TLS construction.\n\nimport * as fs from \"node:fs\";\nimport type * as tls from \"node:tls\";\nimport type { ConnectTunnelOptions, TunnelTlsOptions } from \"./types.js\";\nimport { parseServerAddress } from \"./targets.js\";\n\n// The environment variables options fall back to when not given explicitly\n// (option > environment > throw). They form the contract with the\n// restate-operator, which injects the first four into the pods of a\n// `tunnelMode: in-process` RestateDeployment; AUTH_TOKEN_FILE is reserved\n// for the user's own Secret mount — credentials are never injected.\nexport const TUNNEL_NAME_ENV = \"RESTATE_INPROC_TUNNEL_NAME\";\nexport const ENVIRONMENT_ID_ENV = \"RESTATE_INPROC_ENVIRONMENT_ID\";\nexport const CLOUD_REGION_ENV = \"RESTATE_INPROC_CLOUD_REGION\";\nexport const SIGNING_PUBLIC_KEY_ENV = \"RESTATE_INPROC_SIGNING_PUBLIC_KEY\";\nexport const AUTH_TOKEN_FILE_ENV = \"RESTATE_INPROC_AUTH_TOKEN_FILE\";\n\nexport interface ResolvedOptions {\n /** The SRV name to discover tunnel servers from (region-derived or given). */\n srvName?: string;\n tunnelServers?: string[];\n environmentId: string;\n /**\n * Returns the bearer token for the handshake. Called once per connection\n * attempt: a file-sourced token (AUTH_TOKEN_FILE_ENV) is re-read on every\n * redial so rotations are picked up without a restart. May throw (e.g.\n * the file is briefly unreadable mid-rotation) — callers treat that as a\n * retryable connection failure.\n */\n authToken: () => string;\n signingPublicKey: string;\n tunnelName: string;\n bidirectional: boolean;\n resolveIntervalMs: number;\n supportsDrain: boolean;\n drainGraceMs: number;\n connectTimeoutMs: number;\n handshakeTimeoutMs: number;\n reconnectInitialMs: number;\n reconnectMaxMs: number;\n reconnectFactor: number;\n pingIntervalMs: number;\n pingTimeoutMs: number;\n pingMaxMissed: number;\n maxConcurrentStreams: number;\n connectionWindowSize: number;\n maxSessionMemory: number;\n tls: boolean | TunnelTlsOptions;\n logger: (message: string) => void;\n}\n\n/** An env var set to the empty string is treated as unset. */\nfunction fromEnv(name: string): string | undefined {\n const value = process.env[name];\n return value === undefined || value === \"\" ? undefined : value;\n}\n\n/** Resolve option > environment > throw. */\nfunction requireConfigured(\n value: string | undefined,\n name: string,\n envName: string\n): string {\n const resolved =\n value !== undefined && value !== \"\" ? value : fromEnv(envName);\n if (resolved === undefined) {\n throw new Error(\n `tunnel: ${name} is required (pass the option or set ${envName})`\n );\n }\n return resolved;\n}\n\n/**\n * Both credentials travel as HTTP header values in the handshake. Node\n * silently strips header-illegal characters, which would surface as a\n * baffling `unauthorized` from the server — reject them loudly instead.\n */\nfunction requireHeaderSafe(value: string, what: string): string {\n if (!/^[\\x21-\\x7e]+$/.test(value)) {\n throw new Error(\n `tunnel: ${what} contains characters that cannot travel in an HTTP header (whitespace or non-printable)`\n );\n }\n return value;\n}\n\nfunction resolveAuthToken(option: string | undefined): () => string {\n if (option !== undefined && option !== \"\") {\n requireHeaderSafe(option, \"authToken\");\n return () => option;\n }\n const tokenFile = fromEnv(AUTH_TOKEN_FILE_ENV);\n if (tokenFile === undefined) {\n throw new Error(\n `tunnel: authToken is required (pass the option or set ${AUTH_TOKEN_FILE_ENV})`\n );\n }\n const readToken = () => {\n // Guard before reading: this runs synchronously on the redial path, so a\n // FIFO (blocks forever) or an unbounded device file (reads forever) would\n // freeze the event loop — and with it every other live connection.\n const stat = fs.statSync(tokenFile);\n if (!stat.isFile()) {\n throw new Error(\n `tunnel: auth token file ${tokenFile} is not a regular file`\n );\n }\n if (stat.size > 64 * 1024) {\n throw new Error(\n `tunnel: auth token file ${tokenFile} is implausibly large for a token (${stat.size} bytes)`\n );\n }\n // Trimmed because mounted secrets routinely carry a trailing newline.\n const token = fs.readFileSync(tokenFile, \"utf8\").trim();\n if (token === \"\") {\n throw new Error(`tunnel: auth token file ${tokenFile} is empty`);\n }\n return requireHeaderSafe(token, `auth token file ${tokenFile}`);\n };\n // A bad path or token must throw at configuration time like every other\n // misconfiguration, not look like a transient failure mid-redial.\n readToken();\n return readToken;\n}\n\nfunction positive(\n value: number | undefined,\n fallback: number,\n name: string\n): number {\n if (value === undefined) return fallback;\n if (!Number.isFinite(value) || value <= 0) {\n throw new Error(`tunnel: ${name} must be a positive number`);\n }\n return value;\n}\n\n/**\n * Validate user options and apply defaults. Throws on misconfiguration.\n * Each identity/discovery option falls back to its RESTATE_INPROC_* env var\n * (option > environment > throw), so a pod the restate-operator configured\n * for `tunnelMode: in-process` needs no explicit configuration beyond the\n * auth token.\n */\nexport function resolveOptions(options: ConnectTunnelOptions): ResolvedOptions {\n const hasSrv =\n options.tunnelServersSrv !== undefined && options.tunnelServersSrv !== \"\";\n const hasServers =\n options.tunnelServers !== undefined && options.tunnelServers.length > 0;\n // The env var only fills the gap when NO discovery option was given — an\n // explicit tunnelServersSrv/tunnelServers wins over an injected region, and\n // an explicitly-given-but-empty tunnelServers stays a loud config error\n // rather than silently yielding to the environment.\n let region = options.region;\n if (\n (region === undefined || region === \"\") &&\n !hasSrv &&\n options.tunnelServers === undefined\n ) {\n region = fromEnv(CLOUD_REGION_ENV);\n }\n const hasRegion = region !== undefined && region !== \"\";\n const discoveryCount =\n Number(hasRegion) + Number(hasSrv) + Number(hasServers);\n if (discoveryCount === 0) {\n throw new Error(\n `tunnel: specify one of \\`region\\`, \\`tunnelServersSrv\\` or \\`tunnelServers\\` (or set ${CLOUD_REGION_ENV})`\n );\n }\n if (discoveryCount > 1) {\n throw new Error(\n \"tunnel: specify exactly one of `region`, `tunnelServersSrv` or `tunnelServers`\"\n );\n }\n if (hasRegion && !/^[a-z0-9-]+$/.test(region!)) {\n throw new Error(`tunnel: invalid region ${JSON.stringify(region)}`);\n }\n if (hasSrv && !/^[A-Za-z0-9._-]+$/.test(options.tunnelServersSrv!)) {\n throw new Error(\n `tunnel: invalid tunnelServersSrv ${JSON.stringify(options.tunnelServersSrv)}`\n );\n }\n // Parse explicit servers eagerly: a config typo must throw here, like\n // every other misconfiguration (the Rust client parses URIs at startup).\n // Left to the supervisor it would look like a transient resolution\n // failure and retry forever without ever connecting.\n if (hasServers) {\n for (const address of options.tunnelServers!) parseServerAddress(address);\n }\n\n const environmentId = requireConfigured(\n options.environmentId,\n \"environmentId\",\n ENVIRONMENT_ID_ENV\n );\n if (!/^env_[A-Za-z0-9_-]+$/.test(environmentId)) {\n throw new Error(\n \"tunnel: environmentId must be `env_` followed by alphanumerics (e.g. env_201k0yd4...)\"\n );\n }\n const authToken = resolveAuthToken(options.authToken);\n const signingPublicKey = requireConfigured(\n options.signingPublicKey,\n \"signingPublicKey\",\n SIGNING_PUBLIC_KEY_ENV\n );\n if (!signingPublicKey.startsWith(\"publickeyv1_\")) {\n throw new Error(\n \"tunnel: signingPublicKey must be a request-identity public key (publickeyv1_...)\"\n );\n }\n const tunnelName = requireConfigured(\n options.tunnelName,\n \"tunnelName\",\n TUNNEL_NAME_ENV\n );\n if (!/^[A-Za-z0-9._-]+$/.test(tunnelName)) {\n throw new Error(\n `tunnel: invalid tunnelName ${JSON.stringify(tunnelName)} — use letters, digits, '.', '_' or '-'`\n );\n }\n const pingIntervalMs = positive(\n options.pingIntervalMs,\n 75_000,\n \"pingIntervalMs\"\n );\n const pingTimeoutMs = positive(\n options.pingTimeoutMs,\n 10_000,\n \"pingTimeoutMs\"\n );\n\n return {\n srvName: hasRegion\n ? srvNameForRegion(region!)\n : hasSrv\n ? options.tunnelServersSrv\n : undefined,\n tunnelServers: hasServers ? options.tunnelServers : undefined,\n environmentId,\n authToken,\n signingPublicKey,\n tunnelName,\n bidirectional: options.bidirectional ?? true,\n resolveIntervalMs: positive(\n options.resolveIntervalMs,\n 30_000,\n \"resolveIntervalMs\"\n ),\n supportsDrain: options.supportsDrain ?? true,\n drainGraceMs: positive(options.drainGraceMs, 120_000, \"drainGraceMs\"),\n connectTimeoutMs: positive(\n options.connectTimeoutMs,\n 5_000,\n \"connectTimeoutMs\"\n ),\n handshakeTimeoutMs: positive(\n options.handshakeTimeoutMs,\n 5_000,\n \"handshakeTimeoutMs\"\n ),\n reconnectInitialMs: positive(\n options.reconnectInitialMs,\n 10,\n \"reconnectInitialMs\"\n ),\n reconnectMaxMs: positive(options.reconnectMaxMs, 120_000, \"reconnectMaxMs\"),\n reconnectFactor: positive(options.reconnectFactor, 2, \"reconnectFactor\"),\n pingIntervalMs,\n pingTimeoutMs,\n pingMaxMissed: positive(options.pingMaxMissed, 2, \"pingMaxMissed\"),\n maxConcurrentStreams: positive(\n options.maxConcurrentStreams,\n 4096,\n \"maxConcurrentStreams\"\n ),\n connectionWindowSize: positive(\n options.connectionWindowSize,\n 16 * 1024 * 1024,\n \"connectionWindowSize\"\n ),\n maxSessionMemory: positive(\n options.maxSessionMemory,\n 256,\n \"maxSessionMemory\"\n ),\n tls: options.tls ?? true,\n logger: options.logger ?? (() => {}),\n };\n}\n\n/**\n * Build the `tls.connect` options for a tunnel target, or `undefined` for a\n * plaintext connection.\n *\n * Always offers ALPN `[\"h2\"]` — the same offer every Rust tunnel client\n * makes — and the connection layer requires the negotiation to succeed:\n * Node's http2 will only run a server session over a TLS socket whose ALPN\n * negotiated `h2`. Tunnel servers advertise it since the standard-h2\n * control-traffic change; older servers (which cleared their ALPN list)\n * cannot serve this client.\n */\nexport function buildTlsConnectOptions(\n tlsOption: boolean | TunnelTlsOptions,\n servername: string\n): tls.ConnectionOptions | undefined {\n if (tlsOption === false) return undefined;\n const base: tls.ConnectionOptions = { servername, ALPNProtocols: [\"h2\"] };\n if (tlsOption === true) return base;\n return {\n ...base,\n ...(tlsOption.servername !== undefined && {\n servername: tlsOption.servername,\n }),\n ...(tlsOption.ca !== undefined && { ca: tlsOption.ca }),\n ...(tlsOption.cert !== undefined && { cert: tlsOption.cert }),\n ...(tlsOption.key !== undefined && { key: tlsOption.key }),\n ...(tlsOption.rejectUnauthorized !== undefined && {\n rejectUnauthorized: tlsOption.rejectUnauthorized,\n }),\n };\n}\n\n/** The DNS SRV name for region-based tunnel-server discovery. */\nexport function srvNameForRegion(region: string): string {\n return `tunnel.${region}.restate.cloud`;\n}\n","/*\n * Copyright (c) 2023-2026 - Restate Software, Inc., Restate GmbH\n *\n * This file is part of the Restate SDK for Node.js/TypeScript,\n * which is released under the MIT license.\n *\n * You can find a copy of the license in file LICENSE in the root\n * directory of this repository or package, or at\n * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE\n */\n\n// The /_/start-tunnel handshake.\n// =============================================================================\n//\n// The tunnel server (the HTTP/2 client on the role-flipped connection)\n// opens its FIRST stream as `GET /_/start-tunnel`, with a request body that\n// stays open and later delivers HTTP/2 TRAILERS. The exchange:\n//\n// 1. We answer immediately: `200` whose RESPONSE HEADERS carry our\n// credentials — `authorization: Bearer <token>`,\n// `environment-id: env_<id>`, `tunnel-name: <name>`, and\n// `supports-drain: true` when the drain handover is enabled (the\n// default — see the /_/drain-tunnel handling in connect.ts).\n// 2. The server validates the credentials, then completes the handshake\n// by sending TRAILERS on its still-open request body:\n// `tunnel-status: ok | unauthorized | bad-tunnel-name | too-many-tunnels`\n// plus, on ok: `proxy-url`, `tunnel-url`, `tunnel-name`.\n//\n// Node gotcha (PoC-verified): the high-level Http2ServerRequest \"trailers\"\n// event does NOT fire. Trailers must be read from the raw stream —\n// `req.stream.on(\"trailers\", ...)` — or from `req.trailers` after \"end\".\n// The body must be drained for either to fire.\n//\n// Outcome taxonomy (drives the reconnect policy in connect.ts):\n// - fatal: unauthorized, bad-tunnel-name, or a tunnel-name echo\n// mismatch. Configuration errors — redialing cannot fix\n// them, and hammering the auth path is harmful.\n// - retryable: too-many-tunnels (often a previous instance still\n// draining), timeout, malformed/missing trailers, stream\n// errors, and unknown statuses (forward compatibility).\n\nimport type * as http2 from \"node:http2\";\n\n/** What the server tells us about the established tunnel. */\nexport interface HandshakeInfo {\n tunnelName: string;\n proxyUrl: string;\n tunnelUrl: string;\n}\n\nexport type HandshakeOutcome =\n | { kind: \"ok\"; info: HandshakeInfo }\n | { kind: \"fatal\"; reason: string }\n | { kind: \"retryable\"; reason: string };\n\nexport interface HandshakeCredentials {\n authToken: string;\n environmentId: string;\n tunnelName: string;\n /**\n * Advertise `supports-drain: true`. Only set this when the engine\n * actually implements the `/_/drain-tunnel` handover — advertising it\n * obliges us to open a replacement connection on drain.\n */\n supportsDrain: boolean;\n}\n\nexport const START_TUNNEL_PATH = \"/_/start-tunnel\";\n\n/** Handshake deadline — mirrors the tunnel server's own 5s timeout. */\nexport const HANDSHAKE_TIMEOUT_MS = 5_000;\n\n/**\n * Run the receiver side of the /_/start-tunnel exchange on its stream.\n * Resolves with an outcome; never rejects.\n */\nexport function performHandshake(\n req: http2.Http2ServerRequest,\n res: http2.Http2ServerResponse,\n creds: HandshakeCredentials,\n timeoutMs: number = HANDSHAKE_TIMEOUT_MS\n): Promise<HandshakeOutcome> {\n return new Promise((resolve) => {\n let settled = false;\n const finish = (outcome: HandshakeOutcome) => {\n if (settled) return;\n settled = true;\n clearTimeout(deadline);\n resolve(outcome);\n };\n\n const deadline = setTimeout(() => {\n finish({\n kind: \"retryable\",\n reason: `handshake trailers not received within ${timeoutMs}ms`,\n });\n req.stream.destroy();\n }, timeoutMs);\n deadline.unref();\n\n const onTrailers = (trailers: http2.IncomingHttpHeaders) => {\n const status = trailers[\"tunnel-status\"];\n if (status !== \"ok\") {\n if (status === \"unauthorized\" || status === \"bad-tunnel-name\") {\n finish({ kind: \"fatal\", reason: `tunnel-status: ${String(status)}` });\n } else {\n finish({\n kind: \"retryable\",\n reason: `tunnel-status: ${String(status ?? \"<missing>\")}`,\n });\n }\n return;\n }\n const tunnelName = trailers[\"tunnel-name\"];\n const proxyUrl = trailers[\"proxy-url\"];\n const tunnelUrl = trailers[\"tunnel-url\"];\n if (\n typeof tunnelName !== \"string\" ||\n typeof proxyUrl !== \"string\" ||\n typeof tunnelUrl !== \"string\"\n ) {\n finish({\n kind: \"retryable\",\n reason: \"handshake ok but proxy-url/tunnel-url/tunnel-name missing\",\n });\n return;\n }\n if (tunnelName !== creds.tunnelName) {\n // We requested a specific name; the server must echo it. A different\n // name means our registration URL would not route here.\n finish({\n kind: \"fatal\",\n reason: `tunnel-name mismatch: requested ${JSON.stringify(creds.tunnelName)}, got ${JSON.stringify(tunnelName)}`,\n });\n return;\n }\n finish({ kind: \"ok\", info: { tunnelName, proxyUrl, tunnelUrl } });\n };\n\n // PoC-verified: only the raw stream's \"trailers\" event fires; also read\n // req.trailers after \"end\" as a belt-and-braces fallback.\n req.stream.on(\"trailers\", onTrailers);\n req.on(\"end\", () => {\n if (!settled && req.trailers && Object.keys(req.trailers).length > 0) {\n onTrailers(req.trailers);\n }\n });\n req.on(\"error\", (err) => {\n finish({\n kind: \"retryable\",\n reason: `handshake stream error: ${err.message}`,\n });\n });\n req.stream.on(\"close\", () => {\n finish({\n kind: \"retryable\",\n reason: \"handshake stream closed before trailers\",\n });\n });\n // Drain the (empty) body so \"end\"/\"trailers\" can fire.\n req.resume();\n\n // Answer with our credentials. The request side stays open for trailers.\n res.writeHead(200, {\n authorization: `Bearer ${creds.authToken}`,\n \"environment-id\": creds.environmentId,\n \"tunnel-name\": creds.tunnelName,\n ...(creds.supportsDrain && { \"supports-drain\": \"true\" }),\n });\n res.end();\n });\n}\n","/*\n * Copyright (c) 2023-2026 - Restate Software, Inc., Restate GmbH\n *\n * This file is part of the Restate SDK for Node.js/TypeScript,\n * which is released under the MIT license.\n *\n * You can find a copy of the license in file LICENSE in the root\n * directory of this repository or package, or at\n * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE\n */\n\n// Forwarded-path handling. Pure — no I/O.\n\n/**\n * Strip the tunnel's forwarded prefix `/<scheme>/<host>/<port>` and return\n * the tail — the path the SDK should see.\n *\n * A forwarded invocation arrives down the tunnel with its destination\n * encoded in the path (`/http/my-service.ns.svc.cluster.local/9080/invoke/...`);\n * the cloud proxy has already stripped the `/<env>/<tunnel>` rendezvous\n * prefix. For an in-process SDK deployment the scheme/host/port are\n * vestigial (the receiver *is* the service), so we drop exactly those three\n * segments and keep the tail (`/discover`, `/invoke/<svc>/<handler>`, …).\n *\n * The tail is passed through without re-encoding: the SDK verifies each\n * request's identity JWT against the signed service-relative path (its\n * routing and verification tolerate extra path *prefixes*, but re-encoding,\n * normalization or case folding of the tail itself would break the match).\n * The query string is preserved (it is not part of `aud`).\n *\n * Returns `null` if the path isn't a forwarded `/<scheme>/<host>/<port>/...`\n * path.\n */\nexport function forwardedTail(rawUrl: string): string | null {\n const qIdx = rawUrl.indexOf(\"?\");\n const path = qIdx === -1 ? rawUrl : rawUrl.slice(0, qIdx);\n const query = qIdx === -1 ? \"\" : rawUrl.slice(qIdx);\n const seg = path.split(\"/\"); // [\"\", scheme, host, port, ...tail]\n // The port segment must be numeric — that's what distinguishes a real\n // forwarded prefix from an unprefixed SDK path that happens to have three\n // segments (e.g. `/invoke/Svc/handler` must NOT parse as scheme=invoke,\n // host=Svc, port=handler and dispatch `/` to the SDK).\n if (\n seg.length < 4 ||\n seg[1] === \"\" ||\n seg[2] === \"\" ||\n !/^\\d+$/.test(seg[3]!)\n ) {\n return null;\n }\n const tail = \"/\" + seg.slice(4).join(\"/\");\n return query ? tail + query : tail;\n}\n","/*\n * Copyright (c) 2023-2026 - Restate Software, Inc., Restate GmbH\n *\n * This file is part of the Restate SDK for Node.js/TypeScript,\n * which is released under the MIT license.\n *\n * You can find a copy of the license in file LICENSE in the root\n * directory of this repository or package, or at\n * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE\n */\n\n// A single tunnel connection attempt.\n// =============================================================================\n//\n// One dial → serve → end cycle against one tunnel server:\n//\n// dial TCP → TLS (ALPN must negotiate h2; the tunnel server advertises\n// it) → role-flip: we become the HTTP/2 *server* on the socket we dialed →\n// the cloud (h2 client) opens `GET /_/start-tunnel` → handshake.ts →\n// on `tunnel-status: ok`, serve: each forwarded invocation is one h2\n// stream; strip `/<scheme>/<host>/<port>` and hand it to the SDK's\n// endpoint handler. The SDK verifies each request's identity JWT against\n// the stripped path (aud is signed service-relative), so this package\n// does zero crypto.\n//\n// Lifecycle invariants:\n//\n// C1. `settle(outcome)` runs EXACTLY ONCE per attempt: it clears every\n// timer, detaches the slot-abort listener, releases the socket, and\n// resolves `run()`. Every exit path funnels through it.\n// C2. The connection is torn down on settle — with ONE exception: a\n// drain handover (`detachForDrain`) hands the still-serving session\n// to the DrainingRegistry instead, so in-flight invocations finish\n// while the slot dials a replacement.\n// C3. The handshake gate: forwarded streams (and drains) that arrive\n// before the handshake outcome's microtask has run PARK on the\n// handshake promise instead of being rejected — the cloud fires\n// parked work the instant the tunnel registers, routinely\n// coalescing it with the ok-trailers in one TCP flush.\n\nimport * as net from \"node:net\";\nimport * as tls from \"node:tls\";\nimport * as http2 from \"node:http2\";\n\nimport type { ResolvedOptions } from \"./options.js\";\nimport { buildTlsConnectOptions } from \"./options.js\";\nimport type { Target } from \"./targets.js\";\nimport {\n performHandshake,\n START_TUNNEL_PATH,\n type HandshakeInfo,\n} from \"./handshake.js\";\nimport { forwardedTail } from \"./forwarded.js\";\nimport type { DrainingRegistry } from \"./draining.js\";\n\n/** Why a connection ended — drives the slot's reconnect policy. */\nexport type ConnectionOutcome =\n | { kind: \"served\"; uptimeMs: number } // handshake ok'd, served, then closed → redial\n | { kind: \"drained\"; uptimeMs: number } // server asked us to rotate → redial promptly\n | { kind: \"retryable\"; reason: string } // redial with backoff\n | { kind: \"fatal\"; reason: string }; // stop the tunnel, surface an error\n\n/** The Node request handler produced by the SDK's createEndpointHandler. */\ntype SdkHandler = ReturnType<\n typeof import(\"@restatedev/restate-sdk\").createEndpointHandler\n>;\n\n/** What a connection attempt needs from the engine. */\nexport interface ConnectionDeps {\n opts: ResolvedOptions;\n /** Built once by the engine; stateless per call, shared across streams. */\n sdkHandler: SdkHandler;\n /** Takes ownership of a detached session on drain handover. */\n draining: DrainingRegistry;\n /** Engine-level socket registry, so close() can destroy in-flight dials. */\n activeSockets: Set<net.Socket>;\n /** Called once per successful handshake (count, learned info, ready). */\n onEstablished: (info: HandshakeInfo) => void;\n}\n\n/**\n * Run one connection attempt. Resolves (never rejects) with the outcome\n * when the connection ends; `slotSignal` aborts the attempt at any phase.\n */\nexport function runConnection(\n target: Target,\n slotSignal: AbortSignal,\n deps: ConnectionDeps\n): Promise<ConnectionOutcome> {\n // Resolved once per attempt, before dialing: a file-sourced token is\n // re-read on every redial so rotations are picked up, and a read failure\n // (e.g. mid-rotation) is a retryable outcome rather than a crash.\n let authToken: string;\n try {\n authToken = deps.opts.authToken();\n } catch (err) {\n return Promise.resolve({\n kind: \"retryable\",\n reason: `auth token unavailable: ${err instanceof Error ? err.message : String(err)}`,\n });\n }\n return new ConnectionAttempt(target, slotSignal, deps, authToken).run();\n}\n\nclass ConnectionAttempt {\n private settled = false;\n /** Set when the handshake confirms ok — the connection's serve epoch. */\n private openedAt: number | undefined;\n private serving = false;\n /** Assigned when /_/start-tunnel arrives; the gate streams park on (C3). */\n private handshakePromise: Promise<{ ok: boolean }> | undefined;\n /** Drain handover requested — settle detaches instead of destroying (C2). */\n private detachForDrain = false;\n\n private readonly socket: net.Socket;\n private session: http2.Http2Session | undefined;\n private readonly connectTimer: NodeJS.Timeout;\n private firstRequestTimer: NodeJS.Timeout | undefined;\n private watchdog: NodeJS.Timeout | undefined;\n\n private resolveRun!: (outcome: ConnectionOutcome) => void;\n private readonly plaintext: boolean;\n private readonly log: (message: string) => void;\n\n constructor(\n private readonly target: Target,\n private readonly slotSignal: AbortSignal,\n private readonly deps: ConnectionDeps,\n private readonly authToken: string\n ) {\n this.log = deps.opts.logger;\n this.plaintext = target.plaintext ?? deps.opts.tls === false;\n\n // close() (or this slot's removal) may race any phase of this attempt\n // (dial, TLS, handshake, serving) — the abort listener tears the\n // attempt down deterministically wherever it is.\n slotSignal.addEventListener(\"abort\", this.onStop, { once: true });\n\n const tlsOptions = this.plaintext\n ? undefined\n : buildTlsConnectOptions(deps.opts.tls, target.servername);\n this.socket = this.plaintext\n ? net.connect({ host: target.host, port: target.port })\n : tls.connect({ host: target.host, port: target.port, ...tlsOptions });\n deps.activeSockets.add(this.socket);\n\n // Bound the TCP connect AND the TLS handshake: a peer that accepts the\n // SYN but never completes TLS would otherwise stall this attempt\n // forever (the handshake timer below is only armed once connected).\n // Mirrors the Rust client's connect_timeout (5s default).\n this.connectTimer = setTimeout(() => {\n this.settle({\n kind: \"retryable\",\n reason: `connect timeout after ${deps.opts.connectTimeoutMs}ms`,\n });\n }, deps.opts.connectTimeoutMs);\n this.connectTimer.unref();\n }\n\n run(): Promise<ConnectionOutcome> {\n return new Promise((resolve) => {\n this.resolveRun = resolve;\n this.socket.on(\"error\", (err: Error) => {\n this.settle({\n kind: \"retryable\",\n reason: `socket error: ${err.message}`,\n });\n });\n this.socket.on(\"close\", () => {\n this.settle(\n this.endOutcome(\"connection closed before handshake completed\")\n );\n });\n this.socket.once(this.plaintext ? \"connect\" : \"secureConnect\", () =>\n this.onConnected()\n );\n });\n }\n\n // ---- lifecycle ----\n\n private readonly onStop = () =>\n this.settle({ kind: \"retryable\", reason: \"tunnel closed\" });\n\n private uptimeMs(): number {\n return this.openedAt === undefined ? 0 : Date.now() - this.openedAt;\n }\n\n /** The end-of-connection outcome: \"served\" once established, else retryable. */\n private endOutcome(reason: string): ConnectionOutcome {\n return this.openedAt !== undefined\n ? { kind: \"served\", uptimeMs: this.uptimeMs() }\n : { kind: \"retryable\", reason };\n }\n\n /** C1: the single exit point. C2: drain handover detaches instead. */\n private settle(outcome: ConnectionOutcome): void {\n if (this.settled) return;\n this.settled = true;\n if (this.watchdog !== undefined) clearInterval(this.watchdog);\n if (this.firstRequestTimer !== undefined)\n clearTimeout(this.firstRequestTimer);\n clearTimeout(this.connectTimer);\n this.slotSignal.removeEventListener(\"abort\", this.onStop);\n if (\n this.detachForDrain &&\n this.session !== undefined &&\n !this.session.destroyed\n ) {\n // Drain handover: the session keeps serving its in-flight streams\n // under the registry's grace window (the cloud stops routing new\n // work to it).\n this.deps.draining.add(\n this.session,\n this.socket,\n this.deps.opts.drainGraceMs\n );\n } else {\n this.session?.destroy();\n this.socket.destroy();\n }\n this.deps.activeSockets.delete(this.socket);\n this.resolveRun(outcome);\n }\n\n // ---- connected: role-flip and serve ----\n\n private onConnected(): void {\n clearTimeout(this.connectTimer);\n this.socket.setNoDelay(true);\n this.log(\n `tunnel: connected to ${this.target.host}:${this.target.port}, starting handshake`\n );\n\n // The tunnel server advertises ALPN h2 and the dial offers it; Node's\n // http2 requires the negotiation to have succeeded before it will run a\n // server session over a TLS socket. A server that doesn't negotiate is\n // too old for this client (see the README's server-version note).\n if (!this.plaintext) {\n const alpn = (this.socket as tls.TLSSocket).alpnProtocol;\n if (alpn !== \"h2\") {\n this.settle({\n kind: \"retryable\",\n reason:\n \"tunnel server did not negotiate h2 ALPN — it predates standard-h2 control traffic and cannot serve this client\",\n });\n return;\n }\n }\n const stream = this.socket;\n\n const h2 = http2.createServer(\n {\n maxSessionMemory: this.deps.opts.maxSessionMemory,\n settings: {\n maxConcurrentStreams: this.deps.opts.maxConcurrentStreams,\n initialWindowSize: 1024 * 1024,\n maxFrameSize: 65536,\n },\n },\n (req, res) => this.handleRequest(req, res)\n );\n\n h2.on(\"session\", (s) => {\n this.session = s;\n try {\n // Raise the per-connection flow-control window (Node defaults to\n // 64 KiB, throttling aggregate throughput across streams).\n (\n s as unknown as { setLocalWindowSize?: (n: number) => void }\n ).setLocalWindowSize?.(this.deps.opts.connectionWindowSize);\n } catch {\n // Older Node — per-stream windows still apply.\n }\n s.on(\"close\", () => {\n this.settle(\n this.endOutcome(\"session closed before handshake completed\")\n );\n });\n s.on(\"error\", (err: Error) => {\n this.settle(this.endOutcome(`session error: ${err.message}`));\n });\n });\n h2.on(\"sessionError\", (err: Error) => {\n this.settle(this.endOutcome(`session error: ${err.message}`));\n });\n\n // The server must open /_/start-tunnel promptly; a peer that never\n // does is not a tunnel server.\n this.firstRequestTimer = setTimeout(() => {\n if (this.handshakePromise === undefined) {\n this.settle({\n kind: \"retryable\",\n reason: \"server never initiated /_/start-tunnel\",\n });\n }\n }, this.deps.opts.handshakeTimeoutMs);\n this.firstRequestTimer.unref();\n\n h2.emit(\"connection\", stream);\n }\n\n // ---- request routing ----\n\n private handleRequest(\n req: http2.Http2ServerRequest,\n res: http2.Http2ServerResponse\n ): void {\n const rawPath = (req.url ?? \"\").split(\"?\")[0];\n\n if (\n this.handshakePromise === undefined &&\n req.method === \"GET\" &&\n rawPath === START_TUNNEL_PATH\n ) {\n this.startHandshake(req, res);\n return;\n }\n\n // Control paths arrive UNPREFIXED (they are cloud-originated, not\n // forwarded invocations) — intercept on the raw path, before any\n // prefix stripping. Distinct from the SDK's own `/health`, which\n // arrives prefixed and flows through dispatch below.\n if (rawPath === \"/_/health\") {\n res.writeHead(200);\n res.end();\n return;\n }\n if (rawPath === \"/_/drain-tunnel\") {\n this.handleDrainRequest(res);\n return;\n }\n\n if (this.serving) {\n this.dispatchForwarded(req, res);\n return;\n }\n if (this.handshakePromise === undefined) {\n // A forwarded stream before /_/start-tunnel was even opened —\n // not a tunnel server speaking the protocol.\n res.writeHead(503);\n res.end(\"tunnel: not ready\");\n return;\n }\n this.parkOnGate(req, res);\n }\n\n /** First stream: run the handshake; its outcome opens the gate (C3). */\n private startHandshake(\n req: http2.Http2ServerRequest,\n res: http2.Http2ServerResponse\n ): void {\n if (this.firstRequestTimer !== undefined)\n clearTimeout(this.firstRequestTimer);\n this.handshakePromise = performHandshake(\n req,\n res,\n {\n authToken: this.authToken,\n environmentId: this.deps.opts.environmentId,\n tunnelName: this.deps.opts.tunnelName,\n supportsDrain: this.deps.opts.supportsDrain,\n },\n this.deps.opts.handshakeTimeoutMs\n ).then((outcome) => {\n if (this.settled) return { ok: false };\n if (outcome.kind === \"ok\") {\n this.openedAt = Date.now();\n this.serving = true;\n this.log(\n `tunnel: established (name=${outcome.info.tunnelName}, proxy=${outcome.info.proxyUrl})`\n );\n this.deps.onEstablished(outcome.info);\n this.startWatchdog();\n return { ok: true };\n }\n this.settle(outcome);\n return { ok: false };\n });\n }\n\n /** Strip the destination prefix and hand the stream to the SDK. */\n private dispatchForwarded(\n req: http2.Http2ServerRequest,\n res: http2.Http2ServerResponse\n ): void {\n const tail = forwardedTail(req.url ?? \"\");\n if (tail === null) {\n res.writeHead(400);\n res.end(\"tunnel: malformed forwarded path\");\n return;\n }\n req.url = tail;\n this.deps.sdkHandler(req, res);\n }\n\n /**\n * C3: a stream that raced the handshake parks on its outcome (bounded by\n * handshakeTimeoutMs) rather than rejecting work the cloud sent the\n * moment it registered the tunnel.\n */\n private parkOnGate(\n req: http2.Http2ServerRequest,\n res: http2.Http2ServerResponse\n ): void {\n void this.handshakePromise!.then(({ ok }) => {\n if (this.settled || res.stream.destroyed) return;\n try {\n if (ok) {\n this.dispatchForwarded(req, res);\n } else {\n res.writeHead(503);\n res.end(\"tunnel: not ready\");\n }\n } catch {\n // The session may be tearing down under us.\n }\n });\n }\n\n // ---- drain ----\n\n private handleDrainRequest(res: http2.Http2ServerResponse): void {\n res.writeHead(200);\n res.end();\n if (!this.deps.opts.supportsDrain) {\n // Not advertised, so unexpected — acknowledge and let the server\n // close on us; the slot's redial loop re-establishes.\n this.log(\n \"tunnel: received /_/drain-tunnel (drain not advertised) — acknowledging\"\n );\n return;\n }\n if (this.serving) {\n this.beginDrain();\n } else if (this.handshakePromise !== undefined) {\n // Drain coalesced with the ok-trailers (the server drains tunnels\n // the moment it shuts down, including ones it just registered): the\n // same gate race as forwarded streams — park the drain on the\n // handshake outcome instead of silently dropping it.\n void this.handshakePromise.then(({ ok }) => {\n if (ok) this.beginDrain();\n });\n }\n // Before /_/start-tunnel was even opened: not a tunnel server\n // speaking the protocol — ack-and-ignore.\n }\n\n /** Handover: detach (C2) and settle so the slot dials a replacement. */\n private beginDrain(): void {\n if (this.settled || this.detachForDrain) return;\n this.log(\"tunnel: drain requested — opening a replacement connection\");\n this.detachForDrain = true;\n this.settle({ kind: \"drained\", uptimeMs: this.uptimeMs() });\n }\n\n // ---- liveness ----\n\n /**\n * Periodic h2 PING; consecutive misses mean the connection is half-open\n * (the OS may never surface it) — kill and redial. Started once serving.\n */\n private startWatchdog(): void {\n let missed = 0;\n this.watchdog = setInterval(() => {\n const s = this.session;\n if (s === undefined || s.destroyed) return;\n let acked = false;\n try {\n s.ping((err) => {\n if (err === null) {\n acked = true;\n missed = 0;\n }\n });\n } catch {\n return;\n }\n const t = setTimeout(() => {\n if (acked || s.destroyed) return;\n missed++;\n if (missed >= this.deps.opts.pingMaxMissed) {\n this.log(`tunnel: ${missed} consecutive pings missed — reconnecting`);\n this.settle({ kind: \"served\", uptimeMs: this.uptimeMs() });\n }\n }, this.deps.opts.pingTimeoutMs);\n t.unref();\n }, this.deps.opts.pingIntervalMs);\n this.watchdog.unref();\n }\n}\n","/*\n * Copyright (c) 2023-2026 - Restate Software, Inc., Restate GmbH\n *\n * This file is part of the Restate SDK for Node.js/TypeScript,\n * which is released under the MIT license.\n *\n * You can find a copy of the license in file LICENSE in the root\n * directory of this repository or package, or at\n * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE\n */\n\n// The draining registry — graceful-drain handover ownership.\n//\n// When the cloud asks a connection to drain (`/_/drain-tunnel`), the\n// connection is \"detached\": its attempt settles (so the slot dials a\n// replacement) WITHOUT destroying the session, which keeps serving its\n// in-flight invocations. This registry owns those detached sessions:\n// each is bounded by a grace timer, removes itself when the session ends\n// naturally, and is destroyed unconditionally on engine teardown — a\n// fatal or close() must never leave a detached session serving (and\n// pinning the process) for the rest of its grace window.\n\nimport type * as http2 from \"node:http2\";\nimport type * as net from \"node:net\";\n\ninterface DrainingConnection {\n session: http2.Http2Session;\n socket: net.Socket;\n timer: NodeJS.Timeout;\n}\n\nexport class DrainingRegistry {\n private readonly entries = new Set<DrainingConnection>();\n\n /**\n * Take ownership of a detached (draining) connection: let it serve its\n * in-flight streams for up to `graceMs`, then tear it down. The entry\n * removes itself if the session ends earlier on its own.\n */\n add(session: http2.Http2Session, socket: net.Socket, graceMs: number): void {\n const entry: DrainingConnection = {\n session,\n socket,\n timer: setTimeout(() => {\n this.entries.delete(entry);\n session.destroy();\n socket.destroy();\n }, graceMs),\n };\n // unref'd: a draining session must not keep the process alive past\n // engine teardown (destroyAll covers the explicit paths).\n entry.timer.unref();\n this.entries.add(entry);\n session.on(\"close\", () => {\n clearTimeout(entry.timer);\n this.entries.delete(entry);\n socket.destroy();\n });\n }\n\n /** Tear down every draining connection. Idempotent. */\n destroyAll(): void {\n for (const entry of this.entries) {\n clearTimeout(entry.timer);\n entry.session.destroy();\n entry.socket.destroy();\n }\n this.entries.clear();\n }\n}\n","/*\n * Copyright (c) 2023-2026 - Restate Software, Inc., Restate GmbH\n *\n * This file is part of the Restate SDK for Node.js/TypeScript,\n * which is released under the MIT license.\n *\n * You can find a copy of the license in file LICENSE in the root\n * directory of this repository or package, or at\n * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE\n */\n\n// Reconnect backoff policy.\n\n/**\n * Backoff resets only when a served connection stayed up at least this long\n * (mirrors the Rust client's 5s \"opened\" guard). Without it, a server that\n * authorizes the handshake but immediately drops the connection would be\n * redialed at the backoff floor forever — a full TLS+h2+auth round trip\n * every ~10ms.\n */\nexport const MIN_UPTIME_FOR_BACKOFF_RESET_MS = 5_000;\n\n/**\n * Jittered exponential backoff: each `next()` returns the current delay\n * with ±50% jitter and advances the schedule toward `maxMs`; `reset()`\n * returns to the floor. Jitter keeps multi-homed slots from redialing in\n * lockstep after a fleet-wide blip (thundering herd).\n */\nexport class Backoff {\n private currentMs: number;\n\n constructor(\n private readonly initialMs: number,\n private readonly factor: number,\n private readonly maxMs: number\n ) {\n this.currentMs = initialMs;\n }\n\n next(): number {\n const d = this.currentMs;\n this.currentMs = Math.min(this.currentMs * this.factor, this.maxMs);\n return d * (0.5 + Math.random());\n }\n\n reset(): void {\n this.currentMs = this.initialMs;\n }\n}\n","/*\n * Copyright (c) 2023-2026 - Restate Software, Inc., Restate GmbH\n *\n * This file is part of the Restate SDK for Node.js/TypeScript,\n * which is released under the MIT license.\n *\n * You can find a copy of the license in file LICENSE in the root\n * directory of this repository or package, or at\n * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE\n */\n\n// Small abort-aware async utilities shared by the engine.\n\n/** Sleep that wakes early (resolving) when the signal aborts. */\nexport function delay(ms: number, signal: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal.aborted) {\n resolve();\n return;\n }\n const t = setTimeout(() => {\n signal.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(t);\n resolve();\n };\n signal.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\n/**\n * Race a promise against a signal: resolves `null` the moment the signal\n * aborts, otherwise passes the promise's result through (rejections\n * propagate). The abort listener is removed when the race settles, so\n * repeated calls against a long-lived signal don't accumulate listeners.\n */\nexport async function raceAbortable<T>(\n promise: Promise<T>,\n signal: AbortSignal\n): Promise<T | null> {\n if (signal.aborted) return null;\n let onAbort!: () => void;\n const aborted = new Promise<null>((resolve) => {\n onAbort = () => resolve(null);\n signal.addEventListener(\"abort\", onAbort, { once: true });\n });\n try {\n return await Promise.race([promise, aborted]);\n } finally {\n signal.removeEventListener(\"abort\", onAbort);\n }\n}\n","/*\n * Copyright (c) 2023-2026 - Restate Software, Inc., Restate GmbH\n *\n * This file is part of the Restate SDK for Node.js/TypeScript,\n * which is released under the MIT license.\n *\n * You can find a copy of the license in file LICENSE in the root\n * directory of this repository or package, or at\n * https://github.com/restatedev/sdk-typescript/blob/main/LICENSE\n */\n\n// The tunnel engine.\n// =============================================================================\n//\n// connectTunnel() serves a Restate SDK deployment over OUTBOUND connections\n// to Restate Cloud's tunnel servers — no inbound listener. The pieces:\n//\n// connection.ts — one dial → role-flip → handshake → serve cycle\n// handshake.ts — the /_/start-tunnel credentials/trailers exchange\n// forwarded.ts — the /<scheme>/<host>/<port> destination-prefix strip\n// targets.ts — server discovery (SRV per-IP expansion / explicit list)\n// draining.ts — graceful-drain handover ownership\n// backoff.ts — jittered exponential reconnect policy\n//\n// This module owns the engine: the SLOT SUPERVISOR (multi-homing — like the\n// Rust client, one connection per resolved tunnel server, reconciled as DNS\n// changes; K is not configurable, it IS the resolved set), per-slot\n// reconnect loops with fatal-vs-retryable classification, and the public\n// TunnelConnection handle.\n//\n// Engine invariants:\n//\n// E1. A FATAL outcome (unauthorized / bad-tunnel-name / name mismatch) on\n// ANY slot stops the WHOLE tunnel — the credentials are shared, so\n// every other slot would hit the same wall. Fatal wakes the\n// supervisor, aborts every slot, tears down draining sessions, and\n// surfaces on `error`/`ready` instead of retry-looping the auth path.\n// E2. Backoff resets only after a connection held for\n// MIN_UPTIME_FOR_BACKOFF_RESET_MS; a drain only skips the backoff\n// sleep under the same guard (drain-spam must compound like any\n// handshake-ok-then-die cycle).\n// E3. Teardown is prompt everywhere: close()/fatal abort in-flight dials\n// via per-slot signals, race the (un-abortable) DNS work instead of\n// awaiting it, and never wait out a sleep or a drain grace window.\n\nimport type * as net from \"node:net\";\nimport { createEndpointHandler } from \"@restatedev/restate-sdk\";\n\nimport type { ConnectTunnelOptions, TunnelConnection } from \"./types.js\";\nimport { resolveOptions } from \"./options.js\";\nimport { resolveTargets, targetKey, type Target } from \"./targets.js\";\nimport type { HandshakeInfo } from \"./handshake.js\";\nimport { runConnection, type ConnectionDeps } from \"./connection.js\";\nimport { DrainingRegistry } from \"./draining.js\";\nimport { Backoff, MIN_UPTIME_FOR_BACKOFF_RESET_MS } from \"./backoff.js\";\nimport { delay, raceAbortable } from \"./util.js\";\n\n/** A running per-server connection loop. */\ninterface Slot {\n ctl: AbortController;\n done: Promise<void>;\n}\n\n/**\n * Connect this deployment to a Restate Cloud tunnel and serve `services`\n * over it. Returns immediately; connection management runs in the\n * background until `close()` (or the `signal`) stops it. See\n * {@link TunnelConnection.ready} to await the first successful handshake.\n */\nexport function connectTunnel(options: ConnectTunnelOptions): TunnelConnection {\n const opts = resolveOptions(options);\n const log = opts.logger;\n\n // Built once, shared across connections and streams (it is stateless per\n // call). identityKeys delegates per-request JWT verification to the SDK —\n // it checks `aud` against the post-strip `req.url` pathname.\n const sdkHandler = createEndpointHandler({\n services: options.services,\n bidirectional: opts.bidirectional,\n identityKeys: [opts.signingPublicKey],\n });\n\n // ---- engine state ----\n\n let stopped = false;\n let fatalError: Error | undefined;\n let connectionCount = 0;\n let lastInfo: HandshakeInfo | undefined;\n const activeSockets = new Set<net.Socket>();\n const draining = new DrainingRegistry();\n const slots = new Map<string, Slot>();\n\n // Aborted by close(); cascades into every slot.\n const stopController = new AbortController();\n // Wakes the supervisor out of its sleeps promptly on close() AND on a\n // fatal (so teardown doesn't wait out resolveIntervalMs) — E3.\n const supervisorWake = new AbortController();\n stopController.signal.addEventListener(\n \"abort\",\n () => supervisorWake.abort(),\n { once: true }\n );\n\n let readyResolve!: () => void;\n let readyReject!: (err: Error) => void;\n const ready = new Promise<void>((resolve, reject) => {\n readyResolve = resolve;\n readyReject = reject;\n });\n // A caller may never await `ready`; don't turn a fatal handshake into an\n // unhandled rejection.\n ready.catch(() => {});\n\n // Anchor the event loop: between a session closing and the next redial\n // timer there may be no pending I/O, and a bare awaited promise does not\n // keep Node alive.\n const keepAlive = setInterval(() => {}, 0x7fffffff);\n\n const connectionDeps: ConnectionDeps = {\n opts,\n sdkHandler,\n draining,\n activeSockets,\n onEstablished: (info) => {\n connectionCount++;\n lastInfo = info;\n readyResolve();\n },\n };\n\n // ---- slots: one tunnel connection per resolved server ----\n\n const stopAllSlots = () => {\n for (const slot of slots.values()) slot.ctl.abort();\n supervisorWake.abort();\n };\n\n /** The per-server loop: dial → serve → classify outcome → backoff → redial. */\n const runSlot = async (target: Target, ctl: AbortController) => {\n const backoff = new Backoff(\n opts.reconnectInitialMs,\n opts.reconnectFactor,\n opts.reconnectMaxMs\n );\n\n while (!stopped && !ctl.signal.aborted && fatalError === undefined) {\n const outcome = await runConnection(target, ctl.signal, connectionDeps);\n if (stopped || ctl.signal.aborted) break;\n if (outcome.kind === \"fatal\") {\n // E1: shared credentials — stop everything.\n fatalError = new Error(`tunnel: ${outcome.reason}`);\n log(`tunnel: FATAL — ${outcome.reason}; stopping all connections`);\n readyReject(fatalError);\n stopAllSlots();\n break;\n }\n if (outcome.kind === \"served\" || outcome.kind === \"drained\") {\n // E2: only a connection that actually held resets the backoff.\n const heldLongEnough =\n outcome.uptimeMs >= MIN_UPTIME_FOR_BACKOFF_RESET_MS;\n if (heldLongEnough) backoff.reset();\n if (outcome.kind === \"drained\" && heldLongEnough) {\n // A stable connection was asked to rotate and the server is\n // holding the old one open for us — replace it NOW.\n log(\"tunnel: draining — reconnecting immediately\");\n continue;\n }\n log(\n outcome.kind === \"drained\"\n ? \"tunnel: drained shortly after connecting — reconnecting with backoff\"\n : \"tunnel: connection ended — reconnecting\"\n );\n } else {\n log(`tunnel: ${outcome.reason} — reconnecting`);\n }\n await delay(backoff.next(), ctl.signal);\n }\n };\n\n const startSlot = (key: string, target: Target) => {\n const ctl = new AbortController();\n // Chain to the global stop so close() cascades; self-detaching.\n stopController.signal.addEventListener(\"abort\", () => ctl.abort(), {\n once: true,\n signal: ctl.signal,\n });\n const slot: Slot = { ctl, done: Promise.resolve() };\n slot.done = runSlot(target, ctl).finally(() => {\n // Guarded: this key may have vanished and re-appeared, in which case\n // a NEWER slot owns it — don't delete someone else's registration.\n if (slots.get(key) === slot) slots.delete(key);\n });\n slots.set(key, slot);\n };\n\n // ---- the supervisor: resolve the server set, reconcile slots, repeat ----\n //\n // For SRV discovery the set is re-resolved every resolveIntervalMs; an\n // explicit tunnelServers set is fixed and resolved once (like the Rust\n // client's fixed_uri_stream).\n\n const loopDone = (async () => {\n while (!stopped && fatalError === undefined) {\n let targets: Target[];\n try {\n // E3: race the (un-abortable) DNS work against the wake signal so\n // close()/fatal don't block on a slow resolver — a late result is\n // discarded by the stopped/fatal check below.\n const resolution = resolveTargets(opts);\n resolution.catch(() => {}); // a late rejection must not be unhandled\n const raced = await raceAbortable(resolution, supervisorWake.signal);\n if (raced === null) break; // woken: stopped or fatal\n targets = raced;\n } catch (err) {\n // Keep whatever slots exist serving; retry the resolution later\n // (the Rust client does the same on SRV failures).\n log(\n `tunnel: target resolution failed: ${err instanceof Error ? err.message : String(err)} — retrying`\n );\n await delay(\n Math.min(5_000, opts.resolveIntervalMs),\n supervisorWake.signal\n );\n continue;\n }\n if (stopped || fatalError !== undefined) break;\n\n const desired = new Map(targets.map((t) => [targetKey(t), t] as const));\n for (const [key, target] of desired) {\n if (!slots.has(key)) {\n log(`tunnel: starting connection to ${key}`);\n startSlot(key, target);\n }\n }\n for (const [key, slot] of slots) {\n if (!desired.has(key)) {\n log(`tunnel: ${key} no longer resolves — tearing down`);\n slot.ctl.abort();\n }\n }\n\n if (opts.srvName === undefined) break; // explicit servers: fixed set\n await delay(opts.resolveIntervalMs, supervisorWake.signal);\n }\n // Slots still in the map are live; evicted ones have already settled.\n // No slot can start after the supervisor loop exits.\n await Promise.all([...slots.values()].map((s) => s.done));\n draining.destroyAll();\n clearInterval(keepAlive);\n // If the tunnel never established (closed or stopped before the first\n // ok handshake), settle `ready` so awaiting callers don't hang. No-op\n // if it already resolved/rejected.\n readyReject(\n fatalError ?? new Error(\"tunnel: closed before the first handshake\")\n );\n })();\n\n // ---- the public handle ----\n\n const close = async (): Promise<void> => {\n if (!stopped) {\n stopped = true;\n stopController.abort();\n stopAllSlots();\n for (const socket of activeSockets) socket.destroy();\n activeSockets.clear();\n draining.destroyAll();\n clearInterval(keepAlive);\n }\n await loopDone;\n };\n\n if (options.signal?.aborted) {\n // An already-aborted signal means \"don't run\" — stop before dialing.\n void close();\n } else {\n options.signal?.addEventListener(\"abort\", () => void close(), {\n once: true,\n });\n }\n\n return {\n close,\n get connectionCount() {\n return connectionCount;\n },\n get tunnelName() {\n return lastInfo?.tunnelName;\n },\n get proxyUrl() {\n return lastInfo?.proxyUrl;\n },\n get tunnelUrl() {\n return lastInfo?.tunnelUrl;\n },\n get deploymentUrl() {\n if (lastInfo === undefined) return undefined;\n // Public clusters may advertise the proxy without a port; the proxy\n // listens on 9080. The destination (`/http/in-process/9080/`) is a\n // constant — an in-process tunnel is never dialed, so the server\n // routes purely by the tunnelName earlier in the path.\n try {\n const proxy = new URL(lastInfo.proxyUrl);\n if (proxy.port === \"\") proxy.port = \"9080\";\n const base = proxy.toString().replace(/\\/$/, \"\");\n return `${base}/http/in-process/9080/`;\n } catch {\n return `${lastInfo.proxyUrl}/http/in-process/9080/`;\n }\n },\n get error() {\n return fatalError;\n },\n ready,\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@restatedev/restate-sdk-tunnel",
|
|
3
|
+
"version": "1.15.0-rc.3",
|
|
4
|
+
"description": "Reverse-tunnel client for Restate Cloud — serve a Restate SDK deployment over an outbound tunnel connection, with no inbound HTTP listener",
|
|
5
|
+
"author": "Restate Developers",
|
|
6
|
+
"email": "code@restate.dev",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/restatedev/sdk-typescript#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/restatedev/sdk-typescript.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/restatedev/sdk-typescript/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.cts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"require": "./dist/index.cjs"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@restatedev/restate-sdk": "1.15.0-rc.3"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@restatedev/restate-sdk": "^1.15.0-rc.2"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"test": "turbo run _test --filter={.}...",
|
|
43
|
+
"_test": "vitest run",
|
|
44
|
+
"build": "turbo run _build --filter={.}...",
|
|
45
|
+
"_build": "tsc --noEmit && tsdown",
|
|
46
|
+
"dev": "tsc --noEmit --watch",
|
|
47
|
+
"clean": "rm -rf dist *.tsbuildinfo .turbo",
|
|
48
|
+
"check:types": "turbo run _check:types --filter={.}...",
|
|
49
|
+
"_check:types": "tsc --noEmit --project tsconfig.build.json",
|
|
50
|
+
"lint": "eslint .",
|
|
51
|
+
"check:exports": "turbo run _check:exports --filter={.}...",
|
|
52
|
+
"_check:exports": "attw --pack .",
|
|
53
|
+
"check:api": "turbo run _check:api --filter={.}...",
|
|
54
|
+
"_check:api": "api-extractor run --local"
|
|
10
55
|
}
|
|
56
|
+
}
|