@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-2024 - Restate Software, Inc., Restate GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
package/README.md CHANGED
@@ -1 +1,175 @@
1
- # @restatedev/restate-sdk-tunnel — placeholder, not yet released
1
+ # @restatedev/restate-sdk-tunnel
2
+
3
+ Serve a Restate SDK deployment over an **outbound** connection to Restate
4
+ Cloud's tunnel — no inbound HTTP listener, no public ingress into your
5
+ network. A drop-in alternative to binding a listener with `restate.serve(...)`
6
+ for deployments running in private networks.
7
+
8
+ ```ts
9
+ import * as restate from "@restatedev/restate-sdk";
10
+ import { connectTunnel } from "@restatedev/restate-sdk-tunnel";
11
+
12
+ const greeter = restate.service({
13
+ name: "greeter",
14
+ handlers: { greet: async (_ctx, name: string) => `Hello ${name}!` },
15
+ });
16
+
17
+ const connection = connectTunnel({
18
+ region: "us", // Restate Cloud region (tunnel servers discovered via DNS)
19
+ environmentId: "env_...", // your environment ID (env_ prefix included)
20
+ authToken: process.env.RESTATE_AUTH_TOKEN!, // Cloud API key with the Full role
21
+ signingPublicKey: "publickeyv1_...", // your environment's request-identity key
22
+ tunnelName: "greeter-v1", // the deployment's identity: unique per deployment, shared by its replicas
23
+ services: [greeter],
24
+ });
25
+
26
+ await connection.ready;
27
+ console.log(`register me: ${connection.deploymentUrl}`);
28
+ ```
29
+
30
+ Once connected, register the deployment against Restate Cloud at
31
+ `connection.deploymentUrl` —
32
+ `<proxyUrl>/http/in-process/9080/`, e.g.:
33
+
34
+ ```
35
+ restate dep register https://tunnel.us.restate.cloud:9080/<unprefixed-env-id>/greeter-v1/http/in-process/9080/
36
+ └──────────────── connection.proxyUrl ─────────────────┘└── constant ──┘
37
+ ```
38
+
39
+ Two things to know about the URL's anatomy:
40
+
41
+ - **`tunnelName` is the deployment's identity** — the tunnel server keys
42
+ connections by `<environment>/<tunnelName>` and load-balances every
43
+ proxied invocation across the connections registered under that key.
44
+ Give each distinct deployment its own `tunnelName`; let replicas of the
45
+ _same_ deployment share one (that's the HA/load-balancing path).
46
+ - **The `/http/in-process/9080/` destination is a constant** — the
47
+ standalone tunnel client is a forwarder that dials this segment on the
48
+ far side; an in-process tunnel terminates in this very process, so the
49
+ destination is never dialed and plays no role. It only needs to be
50
+ stable, because Restate identifies deployments by their full URI.
51
+
52
+ ## Zero config on Kubernetes (restate-operator)
53
+
54
+ The identity options and `region` fall back to `RESTATE_INPROC_*`
55
+ environment variables when not given (option > environment > throw):
56
+
57
+ | Option | Environment variable |
58
+ | ------------------ | ------------------------------------------------------------- |
59
+ | `tunnelName` | `RESTATE_INPROC_TUNNEL_NAME` |
60
+ | `environmentId` | `RESTATE_INPROC_ENVIRONMENT_ID` |
61
+ | `region` | `RESTATE_INPROC_CLOUD_REGION` |
62
+ | `signingPublicKey` | `RESTATE_INPROC_SIGNING_PUBLIC_KEY` |
63
+ | `authToken` | the file named by `RESTATE_INPROC_AUTH_TOKEN_FILE` (see below) |
64
+
65
+ The [restate-operator](https://github.com/restatedev/restate-operator)
66
+ injects the first four into the pods of a `tunnelMode: in-process`
67
+ RestateDeployment — with a per-revision `tunnelName` — and registers the
68
+ matching tunnel URL for every revision automatically. There, a complete
69
+ configuration is:
70
+
71
+ ```ts
72
+ connectTunnel({ services: [greeter] });
73
+ ```
74
+
75
+ plus a mounted API-key Secret: credentials are never injected, so mount one
76
+ yourself and set `RESTATE_INPROC_AUTH_TOKEN_FILE` to its path. The file is
77
+ **re-read on every reconnect**, so a rotated Secret is picked up without a
78
+ restart (a transient read failure during rotation is treated as a retryable
79
+ connection failure, not a crash). Mount the Secret as a whole volume rather
80
+ than via `subPath` — Kubernetes does not update `subPath` mounts in place, so
81
+ a rotated token would never reach the file.
82
+
83
+ ## How it works
84
+
85
+ `connectTunnel` is the in-process analog of Restate Cloud's standalone
86
+ [tunnel client](https://github.com/restatedev/restate-cloud-tunnel-client):
87
+
88
+ 1. **Dial out** to the tunnel servers (region-based DNS discovery, or
89
+ explicit `tunnelServers`) — **one connection per resolved server**, like
90
+ the standalone client: SRV discovery expands every target to all of its
91
+ addresses, the set is re-resolved periodically, and connections are
92
+ started/torn down as servers appear/vanish. TLS with **ALPN `h2`** —
93
+ the same offer the standalone Rust client makes; the negotiation must
94
+ succeed (see the server-version note below).
95
+ 2. **Role-flip:** Restate Cloud drives HTTP/2 as the _client_ over the
96
+ connection we dialed; the deployment becomes the HTTP/2 _server_ on it.
97
+ 3. **Handshake:** the cloud opens `GET /_/start-tunnel`; we answer with the
98
+ environment credentials and receive the tunnel confirmation (including
99
+ the public `proxy-url`) as HTTP/2 trailers.
100
+ 4. **Serve:** each invocation arrives as one HTTP/2 stream. The tunnel's
101
+ `/<scheme>/<host>/<port>` destination prefix is stripped and the request
102
+ is handed to the SDK's own endpoint handler (full-duplex streaming —
103
+ `BIDI_STREAM`), exactly as if it had arrived on a local listener.
104
+ 5. **Verify:** every forwarded request carries Restate's request-identity
105
+ JWT (`x-restate-jwt-v1`). Verification is delegated to the SDK against
106
+ `signingPublicKey`, so only requests signed by _your_ environment are
107
+ served.
108
+ 6. **Reconnect** on disconnect with jittered exponential backoff.
109
+ Authorization failures (`unauthorized`, `bad-tunnel-name`) are **fatal**
110
+ — they surface on `connection.error` / `connection.ready` instead of
111
+ hammering the auth path.
112
+ 7. **Drain gracefully:** the engine advertises `supports-drain`. When
113
+ Restate Cloud rolls a tunnel node it sends `/_/drain-tunnel`; the engine
114
+ immediately dials a replacement while the old connection keeps serving
115
+ its in-flight invocations (up to `drainGraceMs`, default 120s) — zero
116
+ dropped requests across cloud rollovers.
117
+
118
+ ## API
119
+
120
+ ```ts
121
+ function connectTunnel(options: ConnectTunnelOptions): TunnelConnection;
122
+
123
+ interface TunnelConnection {
124
+ close(): Promise<void>; // stop reconnecting + close
125
+ readonly ready: Promise<void>; // first successful handshake (rejects on fatal)
126
+ readonly connectionCount: number;
127
+ readonly tunnelName: string | undefined; // learned from the handshake
128
+ readonly proxyUrl: string | undefined; // proxy base for this tunnel
129
+ readonly deploymentUrl: string | undefined; // ready-made registration URL
130
+ readonly tunnelUrl: string | undefined;
131
+ readonly error: Error | undefined; // set on fatal (no more reconnects)
132
+ }
133
+ ```
134
+
135
+ Key options (see `ConnectTunnelOptions` for the full surface and defaults):
136
+
137
+ | Option | Meaning |
138
+ | ----------------------------------------------- | --------------------------------------------------------------------------- |
139
+ | `region` / `tunnelServersSrv` / `tunnelServers` | Tunnel server discovery — exactly one; one connection per resolved server |
140
+ | `resolveIntervalMs` | SRV re-resolution cadence (30s; Node hides DNS TTLs) |
141
+ | `environmentId` | `env_...` — the environment to tunnel to |
142
+ | `authToken` | Cloud API key (`key_...`, Full role) presented in the handshake |
143
+ | `signingPublicKey` | `publickeyv1_...` — request-identity verification (required) |
144
+ | `tunnelName` | The deployment's identity — unique per deployment, shared by its replicas |
145
+ | `services` | Same shape `restate.serve` accepts |
146
+ | `tls` | Default on (system trust, ALPN `h2`); object form for CA/mTLS |
147
+ | `connectTimeoutMs` | TCP+TLS dial deadline (5s, mirrors the standalone client) |
148
+ | `reconnectInitialMs/MaxMs/Factor` | Jittered exponential backoff (10ms → 120s, reset after a stable connection) |
149
+ | `supportsDrain` / `drainGraceMs` | Graceful-drain handover on cloud rollovers (on, 120s grace) |
150
+ | `pingIntervalMs/TimeoutMs/MaxMissed` | Liveness watchdog (75s cadence) |
151
+ | `maxConcurrentStreams` etc. | HTTP/2 tuning for high-concurrency serving |
152
+
153
+ ## Install
154
+
155
+ ```bash
156
+ npm install @restatedev/restate-sdk-tunnel @restatedev/restate-sdk
157
+ ```
158
+
159
+ `@restatedev/restate-sdk` is a peer dependency.
160
+
161
+ ## Scope and status
162
+
163
+ - Serves the deployment **in-process** — this package does not proxy to a
164
+ separate local service, and does not expose the standalone tunnel client's
165
+ "remote proxy" (local ingress/admin forwarding) feature.
166
+ - Multi-homed like the standalone client: one connection per resolved
167
+ tunnel server (per IP for SRV discovery), each with its own reconnect
168
+ loop; the server set is reconciled as DNS changes. A fatal handshake on
169
+ any connection (`unauthorized`, `bad-tunnel-name`) stops the whole
170
+ tunnel — the credentials are shared.
171
+ - **Requires a tunnel server with standard-h2 control traffic** (ALPN `h2`
172
+ advertised on the tunnel listener, `:authority` on control requests).
173
+ Older tunnel servers complete the TLS handshake without a negotiated
174
+ protocol and are rejected by this client with a clear log message.
175
+ - Node-only (uses `node:tls`, `node:http2`, `node:dns`). Node ≥ 22.