@elizaos/plugin-tailscale 2.0.0-beta.1

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/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # @elizaos/plugin-tailscale
2
+
3
+ Tunnel plugin for elizaOS. Exposes a local port through one of two interchangeable
4
+ Tailscale-backed implementations:
5
+
6
+ - **Local backend (`LocalTailscaleService`)** — drives the locally-installed
7
+ `tailscale` CLI (`tailscale serve` for tailnet-internal HTTPS, `tailscale
8
+ funnel` for public Internet exposure). The user must already be authenticated
9
+ to a tailnet.
10
+ - **Cloud backend (`CloudTailscaleService`)** — calls
11
+ `POST /v1/apis/tunnels/tailscale/auth-key` on Eliza Cloud to mint a scoped
12
+ ephemeral auth key for the configured `tag:eliza-tunnel` ACL, then runs
13
+ `tailscale up --auth-key=...` followed by `tailscale serve`/`funnel` against
14
+ the local port. The cloud holds the OAuth client credentials and Tailnet
15
+ identity.
16
+
17
+ Both backends register under `serviceType = "tunnel"` and implement the same
18
+ `ITunnelService` shape, so consumers always go through
19
+ `runtime.getService("tunnel")` and never reach for backend-specific APIs.
20
+
21
+ > **Mutually exclusive with `@elizaos/plugin-ngrok`.** Both plugins register
22
+ > under `serviceType = "tunnel"`. Enable only one at a time.
23
+
24
+ ## Backend selection
25
+
26
+ The plugin reads `TAILSCALE_BACKEND` from runtime settings:
27
+
28
+ | Value | Behavior |
29
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
+ | `local` | Always register `LocalTailscaleService`. |
31
+ | `cloud` | Always register `CloudTailscaleService`. |
32
+ | `auto` (default) | Register `CloudTailscaleService` when Eliza Cloud is connected (`ELIZAOS_CLOUD_API_KEY` set + `ELIZAOS_CLOUD_ENABLED=true`); otherwise fall back to `LocalTailscaleService`. |
33
+
34
+ `isCloudConnected` from `@elizaos/cloud-routing` is the source of truth.
35
+
36
+ ## Settings
37
+
38
+ | Key | Default | Notes |
39
+ | ----------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
40
+ | `TAILSCALE_BACKEND` | `auto` | `local` / `cloud` / `auto`. |
41
+ | `TAILSCALE_AUTH_KEY` | — | Optional pre-minted auth key for the local backend. Most users authenticate via `tailscale up` once and never set this. |
42
+ | `TAILSCALE_TAGS` | `tag:eliza-tunnel` | Comma-separated list of ACL tags applied to the cloud-minted ephemeral key. |
43
+ | `TAILSCALE_FUNNEL` | `false` | When truthy, use `tailscale funnel` (public Internet) instead of `tailscale serve` (tailnet-only). |
44
+ | `TAILSCALE_DEFAULT_PORT` | `3000` | Used when no port is extracted from the user message. |
45
+ | `TAILSCALE_AUTH_KEY_EXPIRY_SECONDS` | `3600` | Expiry hint passed to the cloud auth-key minter. |
46
+ | `ELIZAOS_CLOUD_API_KEY` | — | Required for the cloud backend. |
47
+ | `ELIZAOS_CLOUD_BASE_URL` | `https://www.elizacloud.ai/api/v1` | Cloud base URL override. |
48
+ | `ELIZAOS_CLOUD_ENABLED` | `false` | Required (truthy) for `auto` mode to pick the cloud backend. |
49
+
50
+ ## Actions
51
+
52
+ - `START_TAILSCALE` (similes: `START_TUNNEL`, `OPEN_TUNNEL`, `CREATE_TUNNEL`, `TAILSCALE_UP`)
53
+ - `STOP_TAILSCALE` (similes: `STOP_TUNNEL`, `CLOSE_TUNNEL`, `TAILSCALE_DOWN`)
54
+ - `GET_TAILSCALE_STATUS` (similes: `TAILSCALE_STATUS`, `CHECK_TUNNEL`, `TUNNEL_INFO`)
55
+
56
+ All three resolve the active backend through `runtime.getService("tunnel")`, so
57
+ they behave identically across local and cloud modes.
58
+
59
+ ## Cloud backend wire format
60
+
61
+ `POST /v1/apis/tunnels/tailscale/auth-key`:
62
+
63
+ ```json
64
+ { "tags": ["tag:eliza-tunnel"], "expirySeconds": 3600 }
65
+ ```
66
+
67
+ Response:
68
+
69
+ ```json
70
+ {
71
+ "authKey": "tskey-auth-...",
72
+ "tailnet": "https://headscale.elizacloud.ai",
73
+ "loginServer": "https://headscale.elizacloud.ai",
74
+ "hostname": "eliza-org-session",
75
+ "magicDnsName": "eliza-agent-1234.tunnel.elizacloud.ai"
76
+ }
77
+ ```
78
+
79
+ The plugin then runs locally, in this order:
80
+
81
+ ```bash
82
+ tailscale up --auth-key=<authKey> --login-server=<loginServer> --hostname=<hostname>
83
+ tailscale serve --bg --https=443 localhost:<port> # or `tailscale funnel <port>`
84
+ ```
85
+
86
+ ## Development
87
+
88
+ ```bash
89
+ bun install
90
+ bun run typecheck
91
+ bun run lint
92
+ bun run test
93
+ ```
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,147 @@
1
+ import { IAgentRuntime, ConnectorAccountProvider, Service, Plugin } from '@elizaos/core';
2
+ import { ITunnelService, TunnelStatus } from '@elizaos/plugin-tunnel';
3
+ export { ITunnelService, TunnelProvider, TunnelStatus } from '@elizaos/plugin-tunnel';
4
+
5
+ declare const DEFAULT_TAILSCALE_ACCOUNT_ID = "default";
6
+ interface TailscaleAccountConfig {
7
+ accountId: string;
8
+ authKey?: string;
9
+ tags?: string | string[];
10
+ funnel?: string | boolean;
11
+ defaultPort?: string | number;
12
+ backend?: "local" | "cloud" | "auto";
13
+ authKeyExpirySeconds?: string | number;
14
+ cloudApiKey?: string;
15
+ cloudBaseUrl?: string;
16
+ label?: string;
17
+ }
18
+ declare function normalizeTailscaleAccountId(value: unknown): string;
19
+ declare function resolveTailscaleAccountId(runtime: IAgentRuntime, options?: Record<string, unknown>): string;
20
+ declare function readTailscaleAccounts(runtime: IAgentRuntime): TailscaleAccountConfig[];
21
+ declare function resolveTailscaleAccount(accounts: readonly TailscaleAccountConfig[], accountId: string): TailscaleAccountConfig | null;
22
+
23
+ /**
24
+ * Tailscale ConnectorAccountManager provider.
25
+ *
26
+ * Adapts the existing multi-account resolver in `accounts.ts` to the
27
+ * ConnectorAccountManager CRUD surface. Legacy single-account env/settings
28
+ * are surfaced as the default OWNER account; additional accounts can be
29
+ * declared through character.settings.tailscale.accounts or TAILSCALE_ACCOUNTS.
30
+ *
31
+ * Tailscale does not use an OAuth redirect flow here. Local CLI login and
32
+ * cloud auth-key/API-key provisioning remain owned by the backend services.
33
+ */
34
+
35
+ declare function createTailscaleConnectorAccountProvider(runtime: IAgentRuntime): ConnectorAccountProvider;
36
+
37
+ /**
38
+ * Tailscale plugin re-exports the canonical tunnel-service contract from
39
+ * `@elizaos/plugin-tunnel`. Both backends (local CLI, cloud auth-key minter)
40
+ * register under `serviceType="tunnel"`. Consumers should call
41
+ * `getTunnelService(runtime)` from `@elizaos/plugin-tunnel` to stay
42
+ * backend-agnostic.
43
+ */
44
+
45
+ type TailscaleBackendMode = "local" | "cloud" | "auto";
46
+
47
+ interface CloudFetchInit {
48
+ method: "POST";
49
+ headers: Record<string, string>;
50
+ body: string;
51
+ }
52
+ interface CloudFetchResponse {
53
+ ok: boolean;
54
+ status: number;
55
+ statusText: string;
56
+ json(): Promise<unknown>;
57
+ text(): Promise<string>;
58
+ }
59
+ type CloudFetch = (url: string, init: CloudFetchInit) => Promise<CloudFetchResponse>;
60
+ interface CloudTailscaleServiceOptions {
61
+ /** Override fetch impl for tests. */
62
+ fetch?: CloudFetch;
63
+ /** Override CLI runner for tests. */
64
+ cliRunner?: (cmd: string, args: string[]) => Promise<{
65
+ code: number | null;
66
+ stdout: string;
67
+ stderr: string;
68
+ }>;
69
+ }
70
+ declare class CloudTailscaleService extends Service implements ITunnelService {
71
+ static serviceType: string;
72
+ readonly capabilityDescription = "Provides Tailscale tunnel functionality via Eliza Cloud \u2014 auth keys are minted server-side and the local CLI joins the tailnet.";
73
+ private readonly fetchImpl;
74
+ private readonly cliRunner;
75
+ private tunnelUrl;
76
+ private tunnelPort;
77
+ private startedAt;
78
+ private isShuttingDown;
79
+ private joinedTailnet;
80
+ constructor(runtime?: IAgentRuntime, options?: CloudTailscaleServiceOptions);
81
+ static start(runtime: IAgentRuntime): Promise<Service>;
82
+ start(): Promise<void>;
83
+ stop(): Promise<void>;
84
+ startTunnel(port?: number, options?: {
85
+ accountId?: string;
86
+ }): Promise<string | undefined>;
87
+ stopTunnel(_options?: {
88
+ accountId?: string;
89
+ }): Promise<void>;
90
+ getUrl(): string | null;
91
+ isActive(): boolean;
92
+ getStatus(): TunnelStatus;
93
+ private joinTailnet;
94
+ private runServe;
95
+ private resolveCloudCredentials;
96
+ private cleanup;
97
+ }
98
+
99
+ declare class LocalTailscaleService extends Service implements ITunnelService {
100
+ static serviceType: string;
101
+ readonly capabilityDescription = "Provides secure tunnel functionality via the locally-installed `tailscale` CLI (serve / funnel).";
102
+ private tunnelUrl;
103
+ private tunnelPort;
104
+ private startedAt;
105
+ private isShuttingDown;
106
+ private useFunnel;
107
+ static start(runtime: IAgentRuntime): Promise<Service>;
108
+ start(): Promise<void>;
109
+ stop(): Promise<void>;
110
+ startTunnel(port?: number, options?: {
111
+ accountId?: string;
112
+ }): Promise<string | undefined>;
113
+ stopTunnel(_options?: {
114
+ accountId?: string;
115
+ }): Promise<void>;
116
+ getUrl(): string | null;
117
+ isActive(): boolean;
118
+ getStatus(): TunnelStatus;
119
+ private fetchSelfDnsName;
120
+ private cleanup;
121
+ }
122
+
123
+ type TunnelBackendCtor = typeof LocalTailscaleService | typeof CloudTailscaleService;
124
+ interface BackendDecision {
125
+ backend: TunnelBackendCtor;
126
+ mode: TailscaleBackendMode;
127
+ reason: string;
128
+ }
129
+ declare function readBackendMode(runtime: IAgentRuntime): TailscaleBackendMode;
130
+ declare function selectTunnelBackend(runtime: IAgentRuntime): BackendDecision;
131
+
132
+ /**
133
+ * Plugin doesn't list any services upfront. The selector runs in `init()` and
134
+ * registers exactly one Tailscale backend (local or cloud) under the canonical
135
+ * `serviceType="tunnel"` slot from `@elizaos/plugin-tunnel`. Coordination with
136
+ * other tunnel providers (ngrok, plugin-tunnel's local CLI, plugin-elizacloud's
137
+ * cloud tunnel) is first-active-wins via `tunnelSlotIsFree(runtime)`.
138
+ *
139
+ * Consumers should stay backend-agnostic via `getTunnelService(runtime)` from
140
+ * `@elizaos/plugin-tunnel`.
141
+ *
142
+ * The canonical TUNNEL action from `@elizaos/plugin-tunnel` handles start,
143
+ * stop, and status. This plugin only contributes a provider/backend.
144
+ */
145
+ declare const tailscalePlugin: Plugin;
146
+
147
+ export { type BackendDecision, CloudTailscaleService, DEFAULT_TAILSCALE_ACCOUNT_ID, LocalTailscaleService, type TailscaleAccountConfig, type TailscaleBackendMode, createTailscaleConnectorAccountProvider, tailscalePlugin as default, normalizeTailscaleAccountId, readBackendMode, readTailscaleAccounts, resolveTailscaleAccount, resolveTailscaleAccountId, selectTunnelBackend, tailscalePlugin };
package/dist/index.js ADDED
@@ -0,0 +1,902 @@
1
+ // src/index.ts
2
+ import {
3
+ elizaLogger as elizaLogger4,
4
+ getConnectorAccountManager
5
+ } from "@elizaos/core";
6
+ import { tunnelSlotIsFree } from "@elizaos/plugin-tunnel";
7
+
8
+ // src/services/CloudTailscaleService.ts
9
+ import { spawn } from "child_process";
10
+ import { elizaLogger, Service } from "@elizaos/core";
11
+ import { z as z2 } from "zod";
12
+
13
+ // src/accounts.ts
14
+ var DEFAULT_TAILSCALE_ACCOUNT_ID = "default";
15
+ function nonEmptyString(value) {
16
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
17
+ }
18
+ function readSetting(runtime, key) {
19
+ return nonEmptyString(runtime.getSetting(key));
20
+ }
21
+ function normalizeTailscaleAccountId(value) {
22
+ return nonEmptyString(value) ?? DEFAULT_TAILSCALE_ACCOUNT_ID;
23
+ }
24
+ function resolveTailscaleAccountId(runtime, options) {
25
+ return normalizeTailscaleAccountId(
26
+ options?.accountId ?? options?.tailscaleAccountId ?? readSetting(runtime, "TAILSCALE_DEFAULT_ACCOUNT_ID") ?? readSetting(runtime, "TAILSCALE_ACCOUNT_ID")
27
+ );
28
+ }
29
+ function parseAccountsJson(raw) {
30
+ if (!raw) return [];
31
+ try {
32
+ const parsed = JSON.parse(raw);
33
+ if (Array.isArray(parsed)) {
34
+ return parsed.filter(
35
+ (item) => Boolean(item) && typeof item === "object" && !Array.isArray(item)
36
+ );
37
+ }
38
+ if (parsed && typeof parsed === "object") {
39
+ return Object.entries(parsed).filter(([, value]) => value && typeof value === "object").map(([id, value]) => ({
40
+ ...value,
41
+ accountId: value.accountId ?? id
42
+ }));
43
+ }
44
+ } catch {
45
+ return [];
46
+ }
47
+ return [];
48
+ }
49
+ function readRawField(record, keys) {
50
+ const credentials = record.credentials && typeof record.credentials === "object" ? record.credentials : {};
51
+ const metadata = record.metadata && typeof record.metadata === "object" ? record.metadata : {};
52
+ const settings = record.settings && typeof record.settings === "object" ? record.settings : {};
53
+ for (const source of [record, credentials, metadata, settings]) {
54
+ for (const key of keys) {
55
+ const value = source[key];
56
+ if (value !== void 0 && value !== null && value !== "") return value;
57
+ }
58
+ }
59
+ return void 0;
60
+ }
61
+ function normalizeBackend(value) {
62
+ return value === "local" || value === "cloud" || value === "auto" ? value : void 0;
63
+ }
64
+ function accountFromRecord(record) {
65
+ const accountId = normalizeTailscaleAccountId(
66
+ record.accountId ?? record.id ?? record.name
67
+ );
68
+ const account = {
69
+ accountId,
70
+ authKey: nonEmptyString(
71
+ readRawField(record, [
72
+ "TAILSCALE_AUTH_KEY",
73
+ "authKey",
74
+ "accessToken",
75
+ "access"
76
+ ])
77
+ ),
78
+ tags: readRawField(record, ["TAILSCALE_TAGS", "tags"]),
79
+ funnel: readRawField(record, ["TAILSCALE_FUNNEL", "funnel"]),
80
+ defaultPort: readRawField(record, [
81
+ "TAILSCALE_DEFAULT_PORT",
82
+ "defaultPort"
83
+ ]),
84
+ backend: normalizeBackend(
85
+ readRawField(record, ["TAILSCALE_BACKEND", "backend"])
86
+ ),
87
+ authKeyExpirySeconds: readRawField(record, [
88
+ "TAILSCALE_AUTH_KEY_EXPIRY_SECONDS",
89
+ "authKeyExpirySeconds"
90
+ ]),
91
+ cloudApiKey: nonEmptyString(
92
+ readRawField(record, ["ELIZAOS_CLOUD_API_KEY", "cloudApiKey"])
93
+ ),
94
+ cloudBaseUrl: nonEmptyString(
95
+ readRawField(record, ["ELIZAOS_CLOUD_BASE_URL", "cloudBaseUrl"])
96
+ ),
97
+ label: nonEmptyString(record.label ?? record.displayName)
98
+ };
99
+ return account;
100
+ }
101
+ function addAccount(accounts, account) {
102
+ if (account) {
103
+ accounts.set(account.accountId, account);
104
+ }
105
+ }
106
+ function readTailscaleAccounts(runtime) {
107
+ const accounts = /* @__PURE__ */ new Map();
108
+ const characterConfig = runtime.character?.settings?.tailscale;
109
+ const characterAccounts = characterConfig?.accounts;
110
+ if (Array.isArray(characterAccounts)) {
111
+ for (const item of characterAccounts) {
112
+ if (item && typeof item === "object") {
113
+ addAccount(accounts, accountFromRecord(item));
114
+ }
115
+ }
116
+ } else if (characterAccounts && typeof characterAccounts === "object") {
117
+ for (const [id, value] of Object.entries(
118
+ characterAccounts
119
+ )) {
120
+ if (value && typeof value === "object") {
121
+ addAccount(
122
+ accounts,
123
+ accountFromRecord({
124
+ ...value,
125
+ accountId: value.accountId ?? id
126
+ })
127
+ );
128
+ }
129
+ }
130
+ }
131
+ for (const record of parseAccountsJson(
132
+ readSetting(runtime, "TAILSCALE_ACCOUNTS")
133
+ )) {
134
+ addAccount(accounts, accountFromRecord(record));
135
+ }
136
+ addAccount(accounts, {
137
+ accountId: normalizeTailscaleAccountId(
138
+ readSetting(runtime, "TAILSCALE_ACCOUNT_ID") ?? readSetting(runtime, "TAILSCALE_DEFAULT_ACCOUNT_ID")
139
+ ),
140
+ authKey: readSetting(runtime, "TAILSCALE_AUTH_KEY"),
141
+ tags: readSetting(runtime, "TAILSCALE_TAGS"),
142
+ funnel: readSetting(runtime, "TAILSCALE_FUNNEL"),
143
+ defaultPort: readSetting(runtime, "TAILSCALE_DEFAULT_PORT"),
144
+ backend: normalizeBackend(readSetting(runtime, "TAILSCALE_BACKEND")),
145
+ authKeyExpirySeconds: readSetting(
146
+ runtime,
147
+ "TAILSCALE_AUTH_KEY_EXPIRY_SECONDS"
148
+ ),
149
+ cloudApiKey: readSetting(runtime, "ELIZAOS_CLOUD_API_KEY"),
150
+ cloudBaseUrl: readSetting(runtime, "ELIZAOS_CLOUD_BASE_URL")
151
+ });
152
+ return Array.from(accounts.values());
153
+ }
154
+ function resolveTailscaleAccount(accounts, accountId) {
155
+ return accounts.find((account) => account.accountId === accountId) ?? accounts.find(
156
+ (account) => account.accountId === DEFAULT_TAILSCALE_ACCOUNT_ID
157
+ ) ?? accounts[0] ?? null;
158
+ }
159
+
160
+ // src/environment.ts
161
+ import { z } from "zod";
162
+ var tailscaleEnvSchema = z.object({
163
+ TAILSCALE_AUTH_KEY: z.string().optional(),
164
+ TAILSCALE_TAGS: z.union([z.string(), z.array(z.string())]).optional().transform((value) => {
165
+ if (Array.isArray(value)) return value.filter((tag) => tag.length > 0);
166
+ if (typeof value === "string" && value.length > 0)
167
+ return value.split(",").map((tag) => tag.trim()).filter((tag) => tag.length > 0);
168
+ return ["tag:eliza-tunnel"];
169
+ }).default(["tag:eliza-tunnel"]),
170
+ TAILSCALE_FUNNEL: z.union([z.string(), z.boolean()]).optional().transform((value) => value === true || value === "true" || value === "1").default(false),
171
+ TAILSCALE_DEFAULT_PORT: z.union([z.string(), z.number()]).optional().transform((value) => {
172
+ if (value === void 0 || value === "") return 3e3;
173
+ const num = typeof value === "string" ? Number.parseInt(value, 10) : value;
174
+ if (Number.isNaN(num) || num <= 0 || num > 65535) return 3e3;
175
+ return num;
176
+ }).default(3e3),
177
+ TAILSCALE_BACKEND: z.enum(["local", "cloud", "auto"]).optional().default("auto"),
178
+ TAILSCALE_AUTH_KEY_EXPIRY_SECONDS: z.union([z.string(), z.number()]).optional().transform((value) => {
179
+ if (value === void 0 || value === "") return 3600;
180
+ const num = typeof value === "string" ? Number.parseInt(value, 10) : value;
181
+ if (Number.isNaN(num) || num <= 0) return 3600;
182
+ return num;
183
+ }).default(3600)
184
+ });
185
+ function readSetting2(runtime, key) {
186
+ const value = runtime.getSetting(key);
187
+ if (value === null || value === void 0) return void 0;
188
+ return String(value);
189
+ }
190
+ async function validateTailscaleConfig(runtime, accountId) {
191
+ const resolvedAccountId = accountId ?? resolveTailscaleAccountId(runtime);
192
+ const account = resolveTailscaleAccount(
193
+ readTailscaleAccounts(runtime),
194
+ resolvedAccountId
195
+ );
196
+ const config = {
197
+ TAILSCALE_AUTH_KEY: account?.authKey ?? readSetting2(runtime, "TAILSCALE_AUTH_KEY") ?? process.env.TAILSCALE_AUTH_KEY,
198
+ TAILSCALE_TAGS: account?.tags ?? readSetting2(runtime, "TAILSCALE_TAGS") ?? process.env.TAILSCALE_TAGS,
199
+ TAILSCALE_FUNNEL: account?.funnel ?? readSetting2(runtime, "TAILSCALE_FUNNEL") ?? process.env.TAILSCALE_FUNNEL,
200
+ TAILSCALE_DEFAULT_PORT: account?.defaultPort ?? readSetting2(runtime, "TAILSCALE_DEFAULT_PORT") ?? process.env.TAILSCALE_DEFAULT_PORT,
201
+ TAILSCALE_BACKEND: account?.backend ?? readSetting2(runtime, "TAILSCALE_BACKEND") ?? process.env.TAILSCALE_BACKEND,
202
+ TAILSCALE_AUTH_KEY_EXPIRY_SECONDS: account?.authKeyExpirySeconds ?? readSetting2(runtime, "TAILSCALE_AUTH_KEY_EXPIRY_SECONDS") ?? process.env.TAILSCALE_AUTH_KEY_EXPIRY_SECONDS
203
+ };
204
+ return tailscaleEnvSchema.parse(config);
205
+ }
206
+
207
+ // src/services/CloudTailscaleService.ts
208
+ var CLOUD_BASE_FALLBACK = "https://api.elizacloud.ai/api/v1";
209
+ var authKeyResponseSchema = z2.object({
210
+ authKey: z2.string(),
211
+ tailnet: z2.string(),
212
+ loginServer: z2.string().optional(),
213
+ hostname: z2.string().optional(),
214
+ magicDnsName: z2.string()
215
+ });
216
+ function defaultCliRunner(cmd, args) {
217
+ return new Promise((resolve, reject) => {
218
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
219
+ const out = [];
220
+ const err = [];
221
+ child.stdout?.on("data", (chunk) => out.push(chunk));
222
+ child.stderr?.on("data", (chunk) => err.push(chunk));
223
+ child.on("error", reject);
224
+ child.on(
225
+ "exit",
226
+ (code) => resolve({
227
+ code,
228
+ stdout: Buffer.concat(out).toString("utf8"),
229
+ stderr: Buffer.concat(err).toString("utf8")
230
+ })
231
+ );
232
+ });
233
+ }
234
+ async function defaultFetch(url, init) {
235
+ return normalizeFetchResponse(await fetch(url, init));
236
+ }
237
+ var CloudTailscaleService = class _CloudTailscaleService extends Service {
238
+ static serviceType = "tunnel";
239
+ capabilityDescription = "Provides Tailscale tunnel functionality via Eliza Cloud \u2014 auth keys are minted server-side and the local CLI joins the tailnet.";
240
+ fetchImpl;
241
+ cliRunner;
242
+ tunnelUrl = null;
243
+ tunnelPort = null;
244
+ startedAt = null;
245
+ isShuttingDown = false;
246
+ joinedTailnet = false;
247
+ constructor(runtime, options = {}) {
248
+ super(runtime);
249
+ this.fetchImpl = options.fetch ?? defaultFetch;
250
+ this.cliRunner = options.cliRunner ?? defaultCliRunner;
251
+ }
252
+ static async start(runtime) {
253
+ const service = new _CloudTailscaleService(runtime);
254
+ await service.start();
255
+ return service;
256
+ }
257
+ async start() {
258
+ elizaLogger.info("[CloudTailscaleService] started");
259
+ }
260
+ async stop() {
261
+ await this.stopTunnel();
262
+ }
263
+ async startTunnel(port, options = {}) {
264
+ if (this.isActive()) {
265
+ elizaLogger.warn("[CloudTailscaleService] tunnel already running");
266
+ return this.tunnelUrl ?? void 0;
267
+ }
268
+ if (port === void 0 || port === null) {
269
+ elizaLogger.warn(
270
+ "[CloudTailscaleService] startTunnel called without a port \u2014 service active but no tunnel started"
271
+ );
272
+ return;
273
+ }
274
+ if (port < 1 || port > 65535) {
275
+ throw new Error("Invalid port number");
276
+ }
277
+ const config = await validateTailscaleConfig(
278
+ this.runtime,
279
+ options.accountId
280
+ );
281
+ const { baseUrl, apiKey } = this.resolveCloudCredentials(options.accountId);
282
+ const response = await this.fetchImpl(
283
+ `${baseUrl}/apis/tunnels/tailscale/auth-key`,
284
+ {
285
+ method: "POST",
286
+ headers: {
287
+ "Content-Type": "application/json",
288
+ Authorization: `Bearer ${apiKey}`
289
+ },
290
+ body: JSON.stringify({
291
+ tags: config.TAILSCALE_TAGS,
292
+ expirySeconds: config.TAILSCALE_AUTH_KEY_EXPIRY_SECONDS
293
+ })
294
+ }
295
+ );
296
+ if (!response.ok) {
297
+ const text = await safeReadText(response);
298
+ throw new Error(
299
+ `Cloud Tailscale auth-key mint failed (${response.status} ${response.statusText}): ${text}`
300
+ );
301
+ }
302
+ const rawJson = await response.json();
303
+ const parsed = authKeyResponseSchema.safeParse(rawJson);
304
+ if (!parsed.success) {
305
+ throw new Error(
306
+ `Cloud Tailscale response malformed: ${parsed.error.issues.map((i) => i.message).join("; ")}`
307
+ );
308
+ }
309
+ await this.joinTailnet(parsed.data);
310
+ await this.runServe(port, config.TAILSCALE_FUNNEL);
311
+ this.tunnelUrl = `https://${parsed.data.magicDnsName}`;
312
+ this.tunnelPort = port;
313
+ this.startedAt = /* @__PURE__ */ new Date();
314
+ this.joinedTailnet = true;
315
+ elizaLogger.info(
316
+ `[CloudTailscaleService] tunnel started: ${this.tunnelUrl}`
317
+ );
318
+ return this.tunnelUrl;
319
+ }
320
+ async stopTunnel(_options = {}) {
321
+ if (!this.isActive() && !this.joinedTailnet) {
322
+ elizaLogger.warn("[CloudTailscaleService] no active tunnel to stop");
323
+ return;
324
+ }
325
+ this.isShuttingDown = true;
326
+ elizaLogger.info("[CloudTailscaleService] stopping tunnel");
327
+ if (this.tunnelPort !== null) {
328
+ await this.cliRunner("tailscale", ["serve", "reset"]);
329
+ await this.cliRunner("tailscale", ["funnel", "reset"]);
330
+ }
331
+ if (this.joinedTailnet) {
332
+ await this.cliRunner("tailscale", ["logout"]);
333
+ }
334
+ this.cleanup();
335
+ this.isShuttingDown = false;
336
+ elizaLogger.info("[CloudTailscaleService] tunnel stopped");
337
+ }
338
+ getUrl() {
339
+ return this.tunnelUrl;
340
+ }
341
+ isActive() {
342
+ return this.tunnelUrl !== null && !this.isShuttingDown;
343
+ }
344
+ getStatus() {
345
+ return {
346
+ active: this.isActive(),
347
+ url: this.tunnelUrl,
348
+ port: this.tunnelPort,
349
+ startedAt: this.startedAt,
350
+ provider: "tailscale"
351
+ };
352
+ }
353
+ async joinTailnet(payload) {
354
+ const args = ["up", `--auth-key=${payload.authKey}`];
355
+ const loginServer = payload.loginServer ?? (payload.tailnet.startsWith("http") ? payload.tailnet : null);
356
+ if (loginServer) {
357
+ args.push(`--login-server=${loginServer}`);
358
+ }
359
+ if (payload.hostname) {
360
+ args.push(`--hostname=${payload.hostname}`);
361
+ }
362
+ const result = await this.cliRunner("tailscale", args);
363
+ if (result.code !== 0) {
364
+ throw new Error(
365
+ `tailscale up failed (code ${result.code}): ${result.stderr.trim()}`
366
+ );
367
+ }
368
+ }
369
+ async runServe(port, funnel) {
370
+ const args = funnel ? ["funnel", String(port)] : ["serve", "--bg", "--https=443", `localhost:${port}`];
371
+ const result = await this.cliRunner("tailscale", args);
372
+ if (result.code !== 0) {
373
+ throw new Error(
374
+ `tailscale ${args[0]} failed (code ${result.code}): ${result.stderr.trim()}`
375
+ );
376
+ }
377
+ }
378
+ resolveCloudCredentials(accountId) {
379
+ const account = accountId ? resolveTailscaleAccount(readTailscaleAccounts(this.runtime), accountId) : null;
380
+ const apiKey = readNonEmptyString(account?.cloudApiKey) ?? readNonEmptyString(this.runtime.getSetting("ELIZAOS_CLOUD_API_KEY"));
381
+ if (!apiKey) {
382
+ throw new Error(
383
+ "CloudTailscaleService requires ELIZAOS_CLOUD_API_KEY. Set it or use the local backend."
384
+ );
385
+ }
386
+ const baseRaw = readNonEmptyString(account?.cloudBaseUrl) ?? readNonEmptyString(this.runtime.getSetting("ELIZAOS_CLOUD_BASE_URL")) ?? CLOUD_BASE_FALLBACK;
387
+ return { baseUrl: stripTrailingSlash(baseRaw), apiKey };
388
+ }
389
+ cleanup() {
390
+ this.tunnelUrl = null;
391
+ this.tunnelPort = null;
392
+ this.startedAt = null;
393
+ this.joinedTailnet = false;
394
+ }
395
+ };
396
+ function readNonEmptyString(value) {
397
+ if (value === null || value === void 0) return null;
398
+ const trimmed = String(value).trim();
399
+ return trimmed.length > 0 ? trimmed : null;
400
+ }
401
+ function stripTrailingSlash(url) {
402
+ return url.replace(/\/+$/, "");
403
+ }
404
+ async function safeReadText(response) {
405
+ const text = await response.text().catch(() => "");
406
+ return text.slice(0, 500);
407
+ }
408
+ function normalizeFetchResponse(value) {
409
+ if (!isRecord(value)) {
410
+ throw new Error("Cloud Tailscale fetch returned a non-object response");
411
+ }
412
+ const { ok, status, statusText, json, text } = value;
413
+ if (typeof ok !== "boolean" || typeof status !== "number" || typeof statusText !== "string" || typeof json !== "function" || typeof text !== "function") {
414
+ throw new Error(
415
+ "Cloud Tailscale fetch response is missing required fields"
416
+ );
417
+ }
418
+ return {
419
+ ok,
420
+ status,
421
+ statusText,
422
+ json: async () => json.call(value),
423
+ text: async () => String(await text.call(value))
424
+ };
425
+ }
426
+ function isRecord(value) {
427
+ return typeof value === "object" && value !== null;
428
+ }
429
+
430
+ // src/services/LocalTailscaleService.ts
431
+ import { spawn as spawn2 } from "child_process";
432
+ import { elizaLogger as elizaLogger2, Service as Service2 } from "@elizaos/core";
433
+ import { z as z3 } from "zod";
434
+ var tailscaleStatusPeerSchema = z3.object({
435
+ DNSName: z3.string().optional(),
436
+ Online: z3.boolean().optional()
437
+ });
438
+ var tailscaleStatusSchema = z3.object({
439
+ Self: z3.object({
440
+ DNSName: z3.string().optional()
441
+ }).optional(),
442
+ MagicDNSSuffix: z3.string().optional(),
443
+ Peer: z3.record(z3.string(), tailscaleStatusPeerSchema).optional()
444
+ });
445
+ function runCommand(cmd, args) {
446
+ return new Promise((resolve, reject) => {
447
+ const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
448
+ const out = [];
449
+ const err = [];
450
+ child.stdout?.on("data", (chunk) => out.push(chunk));
451
+ child.stderr?.on("data", (chunk) => err.push(chunk));
452
+ child.on("error", reject);
453
+ child.on(
454
+ "exit",
455
+ (code) => resolve({
456
+ code,
457
+ stdout: Buffer.concat(out).toString("utf8"),
458
+ stderr: Buffer.concat(err).toString("utf8")
459
+ })
460
+ );
461
+ });
462
+ }
463
+ function checkTailscaleInstalled() {
464
+ return new Promise((resolve) => {
465
+ const proc = spawn2("which", ["tailscale"]);
466
+ proc.on("exit", (code) => resolve(code === 0));
467
+ proc.on("error", () => resolve(false));
468
+ });
469
+ }
470
+ function parseTailscaleStatus(stdout) {
471
+ let raw;
472
+ try {
473
+ raw = JSON.parse(stdout);
474
+ } catch {
475
+ return null;
476
+ }
477
+ const result = tailscaleStatusSchema.safeParse(raw);
478
+ return result.success ? result.data : null;
479
+ }
480
+ var LocalTailscaleService = class _LocalTailscaleService extends Service2 {
481
+ static serviceType = "tunnel";
482
+ capabilityDescription = "Provides secure tunnel functionality via the locally-installed `tailscale` CLI (serve / funnel).";
483
+ tunnelUrl = null;
484
+ tunnelPort = null;
485
+ startedAt = null;
486
+ isShuttingDown = false;
487
+ useFunnel = false;
488
+ static async start(runtime) {
489
+ const service = new _LocalTailscaleService(runtime);
490
+ await service.start();
491
+ return service;
492
+ }
493
+ async start() {
494
+ elizaLogger2.info("[LocalTailscaleService] starting");
495
+ const installed = await checkTailscaleInstalled();
496
+ if (!installed) {
497
+ throw new Error(
498
+ "tailscale is not installed. Install from https://tailscale.com/download or run: brew install tailscale"
499
+ );
500
+ }
501
+ }
502
+ async stop() {
503
+ await this.stopTunnel();
504
+ }
505
+ async startTunnel(port, options = {}) {
506
+ if (this.isActive()) {
507
+ elizaLogger2.warn("[LocalTailscaleService] tunnel already running");
508
+ return this.tunnelUrl ?? void 0;
509
+ }
510
+ if (port === void 0 || port === null) {
511
+ elizaLogger2.warn(
512
+ "[LocalTailscaleService] startTunnel called without a port \u2014 service active but no tunnel started"
513
+ );
514
+ return;
515
+ }
516
+ if (port < 1 || port > 65535) {
517
+ throw new Error("Invalid port number");
518
+ }
519
+ const config = await validateTailscaleConfig(
520
+ this.runtime,
521
+ options.accountId
522
+ );
523
+ this.useFunnel = config.TAILSCALE_FUNNEL;
524
+ elizaLogger2.info(
525
+ `[LocalTailscaleService] starting tunnel on port ${port} (funnel=${this.useFunnel})`
526
+ );
527
+ if (this.useFunnel) {
528
+ const result = await runCommand("tailscale", ["funnel", String(port)]);
529
+ if (result.code !== 0) {
530
+ throw new Error(
531
+ `tailscale funnel exited with code ${result.code}: ${result.stderr.trim()}`
532
+ );
533
+ }
534
+ } else {
535
+ const result = await runCommand("tailscale", [
536
+ "serve",
537
+ "--bg",
538
+ "--https=443",
539
+ `localhost:${port}`
540
+ ]);
541
+ if (result.code !== 0) {
542
+ throw new Error(
543
+ `tailscale serve exited with code ${result.code}: ${result.stderr.trim()}`
544
+ );
545
+ }
546
+ }
547
+ const dnsName = await this.fetchSelfDnsName();
548
+ if (!dnsName) {
549
+ throw new Error(
550
+ "tailscale serve started but no DNSName resolved from `tailscale status --json`"
551
+ );
552
+ }
553
+ this.tunnelUrl = this.useFunnel ? `https://${dnsName}` : `https://${dnsName}`;
554
+ this.tunnelPort = port;
555
+ this.startedAt = /* @__PURE__ */ new Date();
556
+ elizaLogger2.info(
557
+ `[LocalTailscaleService] tunnel started: ${this.tunnelUrl}`
558
+ );
559
+ return this.tunnelUrl;
560
+ }
561
+ async stopTunnel(_options = {}) {
562
+ if (!this.isActive()) {
563
+ elizaLogger2.warn("[LocalTailscaleService] no active tunnel to stop");
564
+ return;
565
+ }
566
+ this.isShuttingDown = true;
567
+ elizaLogger2.info("[LocalTailscaleService] stopping tunnel");
568
+ if (this.useFunnel) {
569
+ await runCommand("tailscale", ["funnel", "reset"]);
570
+ } else {
571
+ await runCommand("tailscale", ["serve", "reset"]);
572
+ }
573
+ this.cleanup();
574
+ this.isShuttingDown = false;
575
+ elizaLogger2.info("[LocalTailscaleService] tunnel stopped");
576
+ }
577
+ getUrl() {
578
+ return this.tunnelUrl;
579
+ }
580
+ isActive() {
581
+ return this.tunnelUrl !== null && !this.isShuttingDown;
582
+ }
583
+ getStatus() {
584
+ return {
585
+ active: this.isActive(),
586
+ url: this.tunnelUrl,
587
+ port: this.tunnelPort,
588
+ startedAt: this.startedAt,
589
+ provider: "tailscale"
590
+ };
591
+ }
592
+ async fetchSelfDnsName() {
593
+ const result = await runCommand("tailscale", ["status", "--json"]);
594
+ if (result.code !== 0) {
595
+ elizaLogger2.error(
596
+ `[LocalTailscaleService] tailscale status failed: ${result.stderr.trim()}`
597
+ );
598
+ return null;
599
+ }
600
+ const status = parseTailscaleStatus(result.stdout);
601
+ if (!status) {
602
+ elizaLogger2.error(
603
+ "[LocalTailscaleService] tailscale status returned malformed JSON"
604
+ );
605
+ return null;
606
+ }
607
+ const raw = status.Self?.DNSName;
608
+ if (!raw) return null;
609
+ return raw.replace(/\.$/, "");
610
+ }
611
+ cleanup() {
612
+ this.tunnelUrl = null;
613
+ this.tunnelPort = null;
614
+ this.startedAt = null;
615
+ this.useFunnel = false;
616
+ }
617
+ };
618
+
619
+ // src/__tests__/TailscaleTestSuite.ts
620
+ var CANONICAL_TUNNEL_SERVICE_TYPE = "tunnel";
621
+ var TailscaleTestSuite = class {
622
+ name = "tailscale";
623
+ tests = [
624
+ {
625
+ name: "LocalTailscaleService claims canonical tunnel service-type",
626
+ fn: (_runtime) => {
627
+ if (LocalTailscaleService.serviceType !== CANONICAL_TUNNEL_SERVICE_TYPE) {
628
+ throw new Error(
629
+ `LocalTailscaleService.serviceType must be "${CANONICAL_TUNNEL_SERVICE_TYPE}"`
630
+ );
631
+ }
632
+ }
633
+ },
634
+ {
635
+ name: "CloudTailscaleService claims canonical tunnel service-type",
636
+ fn: (_runtime) => {
637
+ if (CloudTailscaleService.serviceType !== CANONICAL_TUNNEL_SERVICE_TYPE) {
638
+ throw new Error(
639
+ `CloudTailscaleService.serviceType must be "${CANONICAL_TUNNEL_SERVICE_TYPE}"`
640
+ );
641
+ }
642
+ }
643
+ }
644
+ ];
645
+ };
646
+
647
+ // src/connector-account-provider.ts
648
+ var TAILSCALE_PROVIDER_ID = "tailscale";
649
+ var DEFAULT_PURPOSES = [
650
+ "admin",
651
+ "automation"
652
+ ];
653
+ function hasExplicitConfig(account) {
654
+ return Boolean(
655
+ account.authKey || account.tags !== void 0 || account.funnel !== void 0 || account.defaultPort !== void 0 || account.backend !== void 0 || account.authKeyExpirySeconds !== void 0 || account.cloudApiKey || account.cloudBaseUrl
656
+ );
657
+ }
658
+ function authMethodForAccount(account) {
659
+ if (account.cloudApiKey) return "cloud_api_key";
660
+ if (account.authKey) return "auth_key";
661
+ if (account.backend === "local") return "local_cli";
662
+ return "runtime";
663
+ }
664
+ function toConnectorAccount(account, defaultAccountId) {
665
+ const now = Date.now();
666
+ const accountId = normalizeTailscaleAccountId(account.accountId);
667
+ const configured = hasExplicitConfig(account);
668
+ return {
669
+ id: accountId,
670
+ provider: TAILSCALE_PROVIDER_ID,
671
+ label: account.label ?? `Tailscale (${accountId})`,
672
+ role: "OWNER",
673
+ purpose: DEFAULT_PURPOSES,
674
+ accessGate: "open",
675
+ status: configured ? "connected" : "disabled",
676
+ displayHandle: account.label ?? accountId,
677
+ createdAt: now,
678
+ updatedAt: now,
679
+ metadata: {
680
+ authMethod: authMethodForAccount(account),
681
+ source: "legacy",
682
+ isDefault: accountId === defaultAccountId,
683
+ backend: account.backend ?? "auto",
684
+ funnel: account.funnel ?? null,
685
+ defaultPort: account.defaultPort ?? null,
686
+ tags: account.tags ?? null,
687
+ authKeyExpirySeconds: account.authKeyExpirySeconds ?? null,
688
+ hasAuthKey: Boolean(account.authKey),
689
+ hasCloudApiKey: Boolean(account.cloudApiKey),
690
+ cloudBaseUrl: account.cloudBaseUrl ?? null
691
+ }
692
+ };
693
+ }
694
+ function normalizePurposes(purpose, fallback) {
695
+ if (Array.isArray(purpose)) return purpose;
696
+ if (typeof purpose === "string" && purpose.trim()) return [purpose];
697
+ return fallback;
698
+ }
699
+ function mergeStoredAccountPatch(account, patch) {
700
+ return {
701
+ ...account,
702
+ ...patch,
703
+ provider: TAILSCALE_PROVIDER_ID,
704
+ id: account.id,
705
+ purpose: normalizePurposes(patch.purpose, account.purpose),
706
+ externalId: patch.externalId === void 0 ? account.externalId : patch.externalId ?? void 0,
707
+ displayHandle: patch.displayHandle === void 0 ? account.displayHandle : patch.displayHandle ?? void 0,
708
+ ownerBindingId: patch.ownerBindingId === void 0 ? account.ownerBindingId : patch.ownerBindingId ?? void 0,
709
+ ownerIdentityId: patch.ownerIdentityId === void 0 ? account.ownerIdentityId : patch.ownerIdentityId ?? void 0,
710
+ metadata: patch.metadata ?? account.metadata,
711
+ createdAt: account.createdAt
712
+ };
713
+ }
714
+ function createTailscaleConnectorAccountProvider(runtime) {
715
+ return {
716
+ provider: TAILSCALE_PROVIDER_ID,
717
+ label: "Tailscale",
718
+ listAccounts: async (manager) => {
719
+ const stored = await manager.getStorage().listAccounts(TAILSCALE_PROVIDER_ID);
720
+ const storedById = new Set(stored.map((account) => account.id));
721
+ const defaultAccountId = resolveTailscaleAccountId(runtime);
722
+ const synthesized = readTailscaleAccounts(runtime).map((account) => toConnectorAccount(account, defaultAccountId)).filter((account) => !storedById.has(account.id));
723
+ return [...stored, ...synthesized];
724
+ },
725
+ createAccount: async (input, _manager) => {
726
+ return {
727
+ ...input,
728
+ provider: TAILSCALE_PROVIDER_ID,
729
+ role: input.role ?? "OWNER",
730
+ purpose: input.purpose ?? DEFAULT_PURPOSES,
731
+ accessGate: input.accessGate ?? "open",
732
+ status: input.status ?? "pending"
733
+ };
734
+ },
735
+ patchAccount: async (accountId, patch, manager) => {
736
+ const existing = await manager.getStorage().getAccount(TAILSCALE_PROVIDER_ID, accountId);
737
+ if (existing) {
738
+ return mergeStoredAccountPatch(existing, patch);
739
+ }
740
+ return { ...patch, provider: TAILSCALE_PROVIDER_ID };
741
+ },
742
+ deleteAccount: async (_accountId, _manager) => {
743
+ }
744
+ };
745
+ }
746
+
747
+ // src/types.ts
748
+ import {
749
+ getTunnelService
750
+ } from "@elizaos/plugin-tunnel";
751
+
752
+ // src/providers/tailscale-status.ts
753
+ function formatUptime(startedAt) {
754
+ const ms = Date.now() - startedAt.getTime();
755
+ const minutes = Math.floor(ms / 6e4);
756
+ const hours = Math.floor(minutes / 60);
757
+ if (hours > 0) {
758
+ return `${hours} hour${hours === 1 ? "" : "s"}, ${minutes % 60} minute${minutes % 60 === 1 ? "" : "s"}`;
759
+ }
760
+ return `${minutes} minute${minutes === 1 ? "" : "s"}`;
761
+ }
762
+ var tailscaleStatusProvider = {
763
+ name: "tailscaleStatus",
764
+ description: "Current Tailscale tunnel status: active flag, public URL, local port, uptime, backend provider.",
765
+ descriptionCompressed: "Tailscale tunnel status: active, url, port, uptime.",
766
+ dynamic: true,
767
+ contexts: ["settings", "connectors"],
768
+ contextGate: { anyOf: ["settings", "connectors"] },
769
+ cacheStable: false,
770
+ cacheScope: "turn",
771
+ get: async (runtime, _message, _state) => {
772
+ const tunnelService = getTunnelService(runtime);
773
+ if (!tunnelService) {
774
+ return { text: "" };
775
+ }
776
+ const status = tunnelService.getStatus();
777
+ const uptime = status.startedAt ? formatUptime(status.startedAt) : null;
778
+ const text = JSON.stringify({
779
+ tailscale: {
780
+ active: status.active,
781
+ url: status.url,
782
+ port: status.port,
783
+ uptime,
784
+ provider: status.provider
785
+ }
786
+ });
787
+ return {
788
+ text,
789
+ values: {
790
+ active: status.active,
791
+ url: status.url ?? "",
792
+ port: status.port ?? 0,
793
+ provider: status.provider
794
+ },
795
+ data: { status, uptime }
796
+ };
797
+ }
798
+ };
799
+
800
+ // src/services/TunnelBackendSelector.ts
801
+ import { isCloudConnected } from "@elizaos/cloud-routing";
802
+ import { elizaLogger as elizaLogger3 } from "@elizaos/core";
803
+ var ALLOWED_MODES = /* @__PURE__ */ new Set([
804
+ "local",
805
+ "cloud",
806
+ "auto"
807
+ ]);
808
+ function readBackendMode(runtime) {
809
+ const account = resolveTailscaleAccount(
810
+ readTailscaleAccounts(runtime),
811
+ resolveTailscaleAccountId(runtime)
812
+ );
813
+ const raw = account?.backend ?? runtime.getSetting("TAILSCALE_BACKEND");
814
+ if (raw === null || raw === void 0) return "auto";
815
+ const normalized = String(raw).trim().toLowerCase();
816
+ if (ALLOWED_MODES.has(normalized)) {
817
+ return normalized;
818
+ }
819
+ elizaLogger3.warn(
820
+ `[TunnelBackendSelector] invalid TAILSCALE_BACKEND="${raw}" \u2014 falling back to "auto"`
821
+ );
822
+ return "auto";
823
+ }
824
+ function selectTunnelBackend(runtime) {
825
+ const mode = readBackendMode(runtime);
826
+ switch (mode) {
827
+ case "local":
828
+ return {
829
+ backend: LocalTailscaleService,
830
+ mode,
831
+ reason: "TAILSCALE_BACKEND=local"
832
+ };
833
+ case "cloud":
834
+ return {
835
+ backend: CloudTailscaleService,
836
+ mode,
837
+ reason: "TAILSCALE_BACKEND=cloud"
838
+ };
839
+ case "auto": {
840
+ if (isCloudConnected(runtime)) {
841
+ return {
842
+ backend: CloudTailscaleService,
843
+ mode,
844
+ reason: "auto: cloud connected"
845
+ };
846
+ }
847
+ return {
848
+ backend: LocalTailscaleService,
849
+ mode,
850
+ reason: "auto: cloud not connected"
851
+ };
852
+ }
853
+ }
854
+ }
855
+
856
+ // src/index.ts
857
+ var tailscalePlugin = {
858
+ name: "tailscale",
859
+ description: "Tunnel plugin with local Tailscale serve/funnel and cloud-proxy backends.",
860
+ actions: [],
861
+ providers: [tailscaleStatusProvider],
862
+ tests: [new TailscaleTestSuite()],
863
+ init: async (_config, runtime) => {
864
+ try {
865
+ const manager = getConnectorAccountManager(runtime);
866
+ manager.registerProvider(
867
+ createTailscaleConnectorAccountProvider(runtime)
868
+ );
869
+ } catch (err) {
870
+ elizaLogger4.warn(
871
+ `[plugin-tailscale] failed to register ConnectorAccountManager provider: ${err instanceof Error ? err.message : String(err)}`
872
+ );
873
+ }
874
+ if (!tunnelSlotIsFree(runtime)) {
875
+ elizaLogger4.info(
876
+ "[plugin-tailscale] another tunnel service already registered \u2014 skipping Tailscale backend"
877
+ );
878
+ return;
879
+ }
880
+ const decision = selectTunnelBackend(runtime);
881
+ elizaLogger4.info(
882
+ `[plugin-tailscale] tunnel backend: ${decision.backend.name} (${decision.reason})`
883
+ );
884
+ await runtime.registerService(decision.backend);
885
+ }
886
+ };
887
+ var index_default = tailscalePlugin;
888
+ export {
889
+ CloudTailscaleService,
890
+ DEFAULT_TAILSCALE_ACCOUNT_ID,
891
+ LocalTailscaleService,
892
+ createTailscaleConnectorAccountProvider,
893
+ index_default as default,
894
+ normalizeTailscaleAccountId,
895
+ readBackendMode,
896
+ readTailscaleAccounts,
897
+ resolveTailscaleAccount,
898
+ resolveTailscaleAccountId,
899
+ selectTunnelBackend,
900
+ tailscalePlugin
901
+ };
902
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/services/CloudTailscaleService.ts","../src/accounts.ts","../src/environment.ts","../src/services/LocalTailscaleService.ts","../src/__tests__/TailscaleTestSuite.ts","../src/connector-account-provider.ts","../src/types.ts","../src/providers/tailscale-status.ts","../src/services/TunnelBackendSelector.ts"],"sourcesContent":["import {\n elizaLogger,\n getConnectorAccountManager,\n type Plugin,\n} from \"@elizaos/core\";\nimport { tunnelSlotIsFree } from \"@elizaos/plugin-tunnel\";\nimport { TailscaleTestSuite } from \"./__tests__/TailscaleTestSuite\";\nimport { createTailscaleConnectorAccountProvider } from \"./connector-account-provider\";\nimport { tailscaleStatusProvider } from \"./providers/tailscale-status\";\nimport { selectTunnelBackend } from \"./services/TunnelBackendSelector\";\n\n/**\n * Plugin doesn't list any services upfront. The selector runs in `init()` and\n * registers exactly one Tailscale backend (local or cloud) under the canonical\n * `serviceType=\"tunnel\"` slot from `@elizaos/plugin-tunnel`. Coordination with\n * other tunnel providers (ngrok, plugin-tunnel's local CLI, plugin-elizacloud's\n * cloud tunnel) is first-active-wins via `tunnelSlotIsFree(runtime)`.\n *\n * Consumers should stay backend-agnostic via `getTunnelService(runtime)` from\n * `@elizaos/plugin-tunnel`.\n *\n * The canonical TUNNEL action from `@elizaos/plugin-tunnel` handles start,\n * stop, and status. This plugin only contributes a provider/backend.\n */\nexport const tailscalePlugin: Plugin = {\n name: \"tailscale\",\n description:\n \"Tunnel plugin with local Tailscale serve/funnel and cloud-proxy backends.\",\n actions: [],\n providers: [tailscaleStatusProvider],\n tests: [new TailscaleTestSuite()],\n init: async (_config, runtime) => {\n try {\n const manager = getConnectorAccountManager(runtime);\n manager.registerProvider(\n createTailscaleConnectorAccountProvider(runtime),\n );\n } catch (err) {\n elizaLogger.warn(\n `[plugin-tailscale] failed to register ConnectorAccountManager provider: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n\n if (!tunnelSlotIsFree(runtime)) {\n elizaLogger.info(\n \"[plugin-tailscale] another tunnel service already registered — skipping Tailscale backend\",\n );\n return;\n }\n\n const decision = selectTunnelBackend(runtime);\n elizaLogger.info(\n `[plugin-tailscale] tunnel backend: ${decision.backend.name} (${decision.reason})`,\n );\n await runtime.registerService(decision.backend);\n },\n};\n\nexport default tailscalePlugin;\n\nexport * from \"./accounts\";\nexport { createTailscaleConnectorAccountProvider } from \"./connector-account-provider\";\nexport { CloudTailscaleService } from \"./services/CloudTailscaleService\";\nexport { LocalTailscaleService } from \"./services/LocalTailscaleService\";\nexport type { BackendDecision } from \"./services/TunnelBackendSelector\";\nexport {\n readBackendMode,\n selectTunnelBackend,\n} from \"./services/TunnelBackendSelector\";\nexport type {\n ITunnelService,\n TailscaleBackendMode,\n TunnelProvider,\n TunnelStatus,\n} from \"./types\";\n","import { spawn } from \"node:child_process\";\nimport { elizaLogger, type IAgentRuntime, Service } from \"@elizaos/core\";\nimport { z } from \"zod\";\nimport { readTailscaleAccounts, resolveTailscaleAccount } from \"../accounts\";\nimport { validateTailscaleConfig } from \"../environment\";\nimport type { ITunnelService, TunnelStatus } from \"../types\";\n\nconst CLOUD_BASE_FALLBACK = \"https://api.elizacloud.ai/api/v1\";\n\nconst authKeyResponseSchema = z.object({\n authKey: z.string(),\n tailnet: z.string(),\n loginServer: z.string().optional(),\n hostname: z.string().optional(),\n magicDnsName: z.string(),\n});\n\ntype AuthKeyResponse = z.infer<typeof authKeyResponseSchema>;\n\ninterface CloudFetchInit {\n method: \"POST\";\n headers: Record<string, string>;\n body: string;\n}\n\ninterface CloudFetchResponse {\n ok: boolean;\n status: number;\n statusText: string;\n json(): Promise<unknown>;\n text(): Promise<string>;\n}\n\ntype CloudFetch = (\n url: string,\n init: CloudFetchInit,\n) => Promise<CloudFetchResponse>;\n\ninterface CloudTailscaleServiceOptions {\n /** Override fetch impl for tests. */\n fetch?: CloudFetch;\n /** Override CLI runner for tests. */\n cliRunner?: (\n cmd: string,\n args: string[],\n ) => Promise<{ code: number | null; stdout: string; stderr: string }>;\n}\n\ninterface SpawnResult {\n code: number | null;\n stdout: string;\n stderr: string;\n}\n\nfunction defaultCliRunner(cmd: string, args: string[]): Promise<SpawnResult> {\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n const out: Buffer[] = [];\n const err: Buffer[] = [];\n child.stdout?.on(\"data\", (chunk: Buffer) => out.push(chunk));\n child.stderr?.on(\"data\", (chunk: Buffer) => err.push(chunk));\n child.on(\"error\", reject);\n child.on(\"exit\", (code) =>\n resolve({\n code,\n stdout: Buffer.concat(out).toString(\"utf8\"),\n stderr: Buffer.concat(err).toString(\"utf8\"),\n }),\n );\n });\n}\n\nasync function defaultFetch(\n url: string,\n init: CloudFetchInit,\n): Promise<CloudFetchResponse> {\n return normalizeFetchResponse(await fetch(url, init));\n}\n\nexport class CloudTailscaleService extends Service implements ITunnelService {\n static override serviceType = \"tunnel\";\n readonly capabilityDescription =\n \"Provides Tailscale tunnel functionality via Eliza Cloud — auth keys are minted server-side and the local CLI joins the tailnet.\";\n\n private readonly fetchImpl: CloudFetch;\n private readonly cliRunner: (\n cmd: string,\n args: string[],\n ) => Promise<SpawnResult>;\n\n private tunnelUrl: string | null = null;\n private tunnelPort: number | null = null;\n private startedAt: Date | null = null;\n private isShuttingDown = false;\n private joinedTailnet = false;\n\n constructor(\n runtime?: IAgentRuntime,\n options: CloudTailscaleServiceOptions = {},\n ) {\n super(runtime);\n this.fetchImpl = options.fetch ?? defaultFetch;\n this.cliRunner = options.cliRunner ?? defaultCliRunner;\n }\n\n static override async start(runtime: IAgentRuntime): Promise<Service> {\n const service = new CloudTailscaleService(runtime);\n await service.start();\n return service;\n }\n\n async start(): Promise<void> {\n elizaLogger.info(\"[CloudTailscaleService] started\");\n }\n\n async stop(): Promise<void> {\n await this.stopTunnel();\n }\n\n async startTunnel(\n port?: number,\n options: { accountId?: string } = {},\n ): Promise<string | undefined> {\n if (this.isActive()) {\n elizaLogger.warn(\"[CloudTailscaleService] tunnel already running\");\n return this.tunnelUrl ?? undefined;\n }\n\n if (port === undefined || port === null) {\n elizaLogger.warn(\n \"[CloudTailscaleService] startTunnel called without a port — service active but no tunnel started\",\n );\n return;\n }\n\n if (port < 1 || port > 65535) {\n throw new Error(\"Invalid port number\");\n }\n\n const config = await validateTailscaleConfig(\n this.runtime,\n options.accountId,\n );\n const { baseUrl, apiKey } = this.resolveCloudCredentials(options.accountId);\n\n const response = await this.fetchImpl(\n `${baseUrl}/apis/tunnels/tailscale/auth-key`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n tags: config.TAILSCALE_TAGS,\n expirySeconds: config.TAILSCALE_AUTH_KEY_EXPIRY_SECONDS,\n }),\n },\n );\n\n if (!response.ok) {\n const text = await safeReadText(response);\n throw new Error(\n `Cloud Tailscale auth-key mint failed (${response.status} ${response.statusText}): ${text}`,\n );\n }\n\n const rawJson: unknown = await response.json();\n const parsed = authKeyResponseSchema.safeParse(rawJson);\n if (!parsed.success) {\n throw new Error(\n `Cloud Tailscale response malformed: ${parsed.error.issues.map((i) => i.message).join(\"; \")}`,\n );\n }\n\n await this.joinTailnet(parsed.data);\n await this.runServe(port, config.TAILSCALE_FUNNEL);\n\n this.tunnelUrl = `https://${parsed.data.magicDnsName}`;\n this.tunnelPort = port;\n this.startedAt = new Date();\n this.joinedTailnet = true;\n elizaLogger.info(\n `[CloudTailscaleService] tunnel started: ${this.tunnelUrl}`,\n );\n return this.tunnelUrl;\n }\n\n async stopTunnel(_options: { accountId?: string } = {}): Promise<void> {\n if (!this.isActive() && !this.joinedTailnet) {\n elizaLogger.warn(\"[CloudTailscaleService] no active tunnel to stop\");\n return;\n }\n this.isShuttingDown = true;\n elizaLogger.info(\"[CloudTailscaleService] stopping tunnel\");\n\n if (this.tunnelPort !== null) {\n await this.cliRunner(\"tailscale\", [\"serve\", \"reset\"]);\n await this.cliRunner(\"tailscale\", [\"funnel\", \"reset\"]);\n }\n\n if (this.joinedTailnet) {\n await this.cliRunner(\"tailscale\", [\"logout\"]);\n }\n\n this.cleanup();\n this.isShuttingDown = false;\n elizaLogger.info(\"[CloudTailscaleService] tunnel stopped\");\n }\n\n getUrl(): string | null {\n return this.tunnelUrl;\n }\n\n isActive(): boolean {\n return this.tunnelUrl !== null && !this.isShuttingDown;\n }\n\n getStatus(): TunnelStatus {\n return {\n active: this.isActive(),\n url: this.tunnelUrl,\n port: this.tunnelPort,\n startedAt: this.startedAt,\n provider: \"tailscale\",\n };\n }\n\n private async joinTailnet(payload: AuthKeyResponse): Promise<void> {\n const args = [\"up\", `--auth-key=${payload.authKey}`];\n const loginServer =\n payload.loginServer ??\n (payload.tailnet.startsWith(\"http\") ? payload.tailnet : null);\n if (loginServer) {\n args.push(`--login-server=${loginServer}`);\n }\n if (payload.hostname) {\n args.push(`--hostname=${payload.hostname}`);\n }\n\n const result = await this.cliRunner(\"tailscale\", args);\n if (result.code !== 0) {\n throw new Error(\n `tailscale up failed (code ${result.code}): ${result.stderr.trim()}`,\n );\n }\n }\n\n private async runServe(port: number, funnel: boolean): Promise<void> {\n const args = funnel\n ? [\"funnel\", String(port)]\n : [\"serve\", \"--bg\", \"--https=443\", `localhost:${port}`];\n const result = await this.cliRunner(\"tailscale\", args);\n if (result.code !== 0) {\n throw new Error(\n `tailscale ${args[0]} failed (code ${result.code}): ${result.stderr.trim()}`,\n );\n }\n }\n\n private resolveCloudCredentials(accountId?: string): {\n baseUrl: string;\n apiKey: string;\n } {\n const account = accountId\n ? resolveTailscaleAccount(readTailscaleAccounts(this.runtime), accountId)\n : null;\n const apiKey =\n readNonEmptyString(account?.cloudApiKey) ??\n readNonEmptyString(this.runtime.getSetting(\"ELIZAOS_CLOUD_API_KEY\"));\n if (!apiKey) {\n throw new Error(\n \"CloudTailscaleService requires ELIZAOS_CLOUD_API_KEY. Set it or use the local backend.\",\n );\n }\n const baseRaw =\n readNonEmptyString(account?.cloudBaseUrl) ??\n readNonEmptyString(this.runtime.getSetting(\"ELIZAOS_CLOUD_BASE_URL\")) ??\n CLOUD_BASE_FALLBACK;\n return { baseUrl: stripTrailingSlash(baseRaw), apiKey };\n }\n\n private cleanup(): void {\n this.tunnelUrl = null;\n this.tunnelPort = null;\n this.startedAt = null;\n this.joinedTailnet = false;\n }\n}\n\nfunction readNonEmptyString(value: unknown): string | null {\n if (value === null || value === undefined) return null;\n const trimmed = String(value).trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\nfunction stripTrailingSlash(url: string): string {\n return url.replace(/\\/+$/, \"\");\n}\n\nasync function safeReadText(response: CloudFetchResponse): Promise<string> {\n const text = await response.text().catch(() => \"\");\n return text.slice(0, 500);\n}\n\nfunction normalizeFetchResponse(value: unknown): CloudFetchResponse {\n if (!isRecord(value)) {\n throw new Error(\"Cloud Tailscale fetch returned a non-object response\");\n }\n\n const { ok, status, statusText, json, text } = value;\n if (\n typeof ok !== \"boolean\" ||\n typeof status !== \"number\" ||\n typeof statusText !== \"string\" ||\n typeof json !== \"function\" ||\n typeof text !== \"function\"\n ) {\n throw new Error(\n \"Cloud Tailscale fetch response is missing required fields\",\n );\n }\n\n return {\n ok,\n status,\n statusText,\n json: async () => json.call(value),\n text: async () => String(await text.call(value)),\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n","import type { IAgentRuntime } from \"@elizaos/core\";\n\nexport const DEFAULT_TAILSCALE_ACCOUNT_ID = \"default\";\n\nexport interface TailscaleAccountConfig {\n accountId: string;\n authKey?: string;\n tags?: string | string[];\n funnel?: string | boolean;\n defaultPort?: string | number;\n backend?: \"local\" | \"cloud\" | \"auto\";\n authKeyExpirySeconds?: string | number;\n cloudApiKey?: string;\n cloudBaseUrl?: string;\n label?: string;\n}\n\ntype RawAccountRecord = Record<string, unknown>;\n\nfunction nonEmptyString(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim().length > 0\n ? value.trim()\n : undefined;\n}\n\nfunction readSetting(runtime: IAgentRuntime, key: string): string | undefined {\n return nonEmptyString(runtime.getSetting(key));\n}\n\nexport function normalizeTailscaleAccountId(value: unknown): string {\n return nonEmptyString(value) ?? DEFAULT_TAILSCALE_ACCOUNT_ID;\n}\n\nexport function resolveTailscaleAccountId(\n runtime: IAgentRuntime,\n options?: Record<string, unknown>,\n): string {\n return normalizeTailscaleAccountId(\n options?.accountId ??\n options?.tailscaleAccountId ??\n readSetting(runtime, \"TAILSCALE_DEFAULT_ACCOUNT_ID\") ??\n readSetting(runtime, \"TAILSCALE_ACCOUNT_ID\"),\n );\n}\n\nfunction parseAccountsJson(raw: string | undefined): RawAccountRecord[] {\n if (!raw) return [];\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (Array.isArray(parsed)) {\n return parsed.filter(\n (item): item is RawAccountRecord =>\n Boolean(item) && typeof item === \"object\" && !Array.isArray(item),\n );\n }\n if (parsed && typeof parsed === \"object\") {\n return Object.entries(parsed as Record<string, unknown>)\n .filter(([, value]) => value && typeof value === \"object\")\n .map(([id, value]) => ({\n ...(value as RawAccountRecord),\n accountId: (value as RawAccountRecord).accountId ?? id,\n }));\n }\n } catch {\n return [];\n }\n return [];\n}\n\nfunction readRawField(\n record: RawAccountRecord,\n keys: readonly string[],\n): unknown {\n const credentials =\n record.credentials && typeof record.credentials === \"object\"\n ? (record.credentials as RawAccountRecord)\n : {};\n const metadata =\n record.metadata && typeof record.metadata === \"object\"\n ? (record.metadata as RawAccountRecord)\n : {};\n const settings =\n record.settings && typeof record.settings === \"object\"\n ? (record.settings as RawAccountRecord)\n : {};\n\n for (const source of [record, credentials, metadata, settings]) {\n for (const key of keys) {\n const value = source[key];\n if (value !== undefined && value !== null && value !== \"\") return value;\n }\n }\n return undefined;\n}\n\nfunction normalizeBackend(\n value: unknown,\n): \"local\" | \"cloud\" | \"auto\" | undefined {\n return value === \"local\" || value === \"cloud\" || value === \"auto\"\n ? value\n : undefined;\n}\n\nfunction accountFromRecord(\n record: RawAccountRecord,\n): TailscaleAccountConfig | null {\n const accountId = normalizeTailscaleAccountId(\n record.accountId ?? record.id ?? record.name,\n );\n const account: TailscaleAccountConfig = {\n accountId,\n authKey: nonEmptyString(\n readRawField(record, [\n \"TAILSCALE_AUTH_KEY\",\n \"authKey\",\n \"accessToken\",\n \"access\",\n ]),\n ),\n tags: readRawField(record, [\"TAILSCALE_TAGS\", \"tags\"]) as\n | string\n | string[]\n | undefined,\n funnel: readRawField(record, [\"TAILSCALE_FUNNEL\", \"funnel\"]) as\n | string\n | boolean\n | undefined,\n defaultPort: readRawField(record, [\n \"TAILSCALE_DEFAULT_PORT\",\n \"defaultPort\",\n ]) as string | number | undefined,\n backend: normalizeBackend(\n readRawField(record, [\"TAILSCALE_BACKEND\", \"backend\"]),\n ),\n authKeyExpirySeconds: readRawField(record, [\n \"TAILSCALE_AUTH_KEY_EXPIRY_SECONDS\",\n \"authKeyExpirySeconds\",\n ]) as string | number | undefined,\n cloudApiKey: nonEmptyString(\n readRawField(record, [\"ELIZAOS_CLOUD_API_KEY\", \"cloudApiKey\"]),\n ),\n cloudBaseUrl: nonEmptyString(\n readRawField(record, [\"ELIZAOS_CLOUD_BASE_URL\", \"cloudBaseUrl\"]),\n ),\n label: nonEmptyString(record.label ?? record.displayName),\n };\n return account;\n}\n\nfunction addAccount(\n accounts: Map<string, TailscaleAccountConfig>,\n account: TailscaleAccountConfig | null,\n): void {\n if (account) {\n accounts.set(account.accountId, account);\n }\n}\n\nexport function readTailscaleAccounts(\n runtime: IAgentRuntime,\n): TailscaleAccountConfig[] {\n const accounts = new Map<string, TailscaleAccountConfig>();\n const characterConfig = runtime.character?.settings?.tailscale as\n | { accounts?: unknown }\n | undefined;\n const characterAccounts = characterConfig?.accounts;\n\n if (Array.isArray(characterAccounts)) {\n for (const item of characterAccounts) {\n if (item && typeof item === \"object\") {\n addAccount(accounts, accountFromRecord(item as RawAccountRecord));\n }\n }\n } else if (characterAccounts && typeof characterAccounts === \"object\") {\n for (const [id, value] of Object.entries(\n characterAccounts as Record<string, unknown>,\n )) {\n if (value && typeof value === \"object\") {\n addAccount(\n accounts,\n accountFromRecord({\n ...(value as RawAccountRecord),\n accountId: (value as RawAccountRecord).accountId ?? id,\n }),\n );\n }\n }\n }\n\n for (const record of parseAccountsJson(\n readSetting(runtime, \"TAILSCALE_ACCOUNTS\"),\n )) {\n addAccount(accounts, accountFromRecord(record));\n }\n\n addAccount(accounts, {\n accountId: normalizeTailscaleAccountId(\n readSetting(runtime, \"TAILSCALE_ACCOUNT_ID\") ??\n readSetting(runtime, \"TAILSCALE_DEFAULT_ACCOUNT_ID\"),\n ),\n authKey: readSetting(runtime, \"TAILSCALE_AUTH_KEY\"),\n tags: readSetting(runtime, \"TAILSCALE_TAGS\"),\n funnel: readSetting(runtime, \"TAILSCALE_FUNNEL\"),\n defaultPort: readSetting(runtime, \"TAILSCALE_DEFAULT_PORT\"),\n backend: normalizeBackend(readSetting(runtime, \"TAILSCALE_BACKEND\")),\n authKeyExpirySeconds: readSetting(\n runtime,\n \"TAILSCALE_AUTH_KEY_EXPIRY_SECONDS\",\n ),\n cloudApiKey: readSetting(runtime, \"ELIZAOS_CLOUD_API_KEY\"),\n cloudBaseUrl: readSetting(runtime, \"ELIZAOS_CLOUD_BASE_URL\"),\n });\n\n return Array.from(accounts.values());\n}\n\nexport function resolveTailscaleAccount(\n accounts: readonly TailscaleAccountConfig[],\n accountId: string,\n): TailscaleAccountConfig | null {\n return (\n accounts.find((account) => account.accountId === accountId) ??\n accounts.find(\n (account) => account.accountId === DEFAULT_TAILSCALE_ACCOUNT_ID,\n ) ??\n accounts[0] ??\n null\n );\n}\n","import type { IAgentRuntime } from \"@elizaos/core\";\nimport { z } from \"zod\";\nimport {\n readTailscaleAccounts,\n resolveTailscaleAccount,\n resolveTailscaleAccountId,\n} from \"./accounts\";\n\nconst tailscaleEnvSchema = z.object({\n TAILSCALE_AUTH_KEY: z.string().optional(),\n TAILSCALE_TAGS: z\n .union([z.string(), z.array(z.string())])\n .optional()\n .transform((value) => {\n if (Array.isArray(value)) return value.filter((tag) => tag.length > 0);\n if (typeof value === \"string\" && value.length > 0)\n return value\n .split(\",\")\n .map((tag) => tag.trim())\n .filter((tag) => tag.length > 0);\n return [\"tag:eliza-tunnel\"];\n })\n .default([\"tag:eliza-tunnel\"]),\n TAILSCALE_FUNNEL: z\n .union([z.string(), z.boolean()])\n .optional()\n .transform((value) => value === true || value === \"true\" || value === \"1\")\n .default(false),\n TAILSCALE_DEFAULT_PORT: z\n .union([z.string(), z.number()])\n .optional()\n .transform((value) => {\n if (value === undefined || value === \"\") return 3000;\n const num =\n typeof value === \"string\" ? Number.parseInt(value, 10) : value;\n if (Number.isNaN(num) || num <= 0 || num > 65535) return 3000;\n return num;\n })\n .default(3000),\n TAILSCALE_BACKEND: z\n .enum([\"local\", \"cloud\", \"auto\"])\n .optional()\n .default(\"auto\"),\n TAILSCALE_AUTH_KEY_EXPIRY_SECONDS: z\n .union([z.string(), z.number()])\n .optional()\n .transform((value) => {\n if (value === undefined || value === \"\") return 3600;\n const num =\n typeof value === \"string\" ? Number.parseInt(value, 10) : value;\n if (Number.isNaN(num) || num <= 0) return 3600;\n return num;\n })\n .default(3600),\n});\n\ntype TailscaleConfig = z.infer<typeof tailscaleEnvSchema>;\n\nfunction readSetting(runtime: IAgentRuntime, key: string): string | undefined {\n const value = runtime.getSetting(key);\n if (value === null || value === undefined) return undefined;\n return String(value);\n}\n\nexport async function validateTailscaleConfig(\n runtime: IAgentRuntime,\n accountId?: string,\n): Promise<TailscaleConfig> {\n const resolvedAccountId = accountId ?? resolveTailscaleAccountId(runtime);\n const account = resolveTailscaleAccount(\n readTailscaleAccounts(runtime),\n resolvedAccountId,\n );\n const config = {\n TAILSCALE_AUTH_KEY:\n account?.authKey ??\n readSetting(runtime, \"TAILSCALE_AUTH_KEY\") ??\n process.env.TAILSCALE_AUTH_KEY,\n TAILSCALE_TAGS:\n account?.tags ??\n readSetting(runtime, \"TAILSCALE_TAGS\") ??\n process.env.TAILSCALE_TAGS,\n TAILSCALE_FUNNEL:\n account?.funnel ??\n readSetting(runtime, \"TAILSCALE_FUNNEL\") ??\n process.env.TAILSCALE_FUNNEL,\n TAILSCALE_DEFAULT_PORT:\n account?.defaultPort ??\n readSetting(runtime, \"TAILSCALE_DEFAULT_PORT\") ??\n process.env.TAILSCALE_DEFAULT_PORT,\n TAILSCALE_BACKEND:\n account?.backend ??\n readSetting(runtime, \"TAILSCALE_BACKEND\") ??\n process.env.TAILSCALE_BACKEND,\n TAILSCALE_AUTH_KEY_EXPIRY_SECONDS:\n account?.authKeyExpirySeconds ??\n readSetting(runtime, \"TAILSCALE_AUTH_KEY_EXPIRY_SECONDS\") ??\n process.env.TAILSCALE_AUTH_KEY_EXPIRY_SECONDS,\n };\n return tailscaleEnvSchema.parse(config);\n}\n","import { spawn } from \"node:child_process\";\nimport { elizaLogger, type IAgentRuntime, Service } from \"@elizaos/core\";\nimport { z } from \"zod\";\nimport { validateTailscaleConfig } from \"../environment\";\nimport type { ITunnelService, TunnelStatus } from \"../types\";\n\nconst tailscaleStatusPeerSchema = z.object({\n DNSName: z.string().optional(),\n Online: z.boolean().optional(),\n});\n\nconst tailscaleStatusSchema = z.object({\n Self: z\n .object({\n DNSName: z.string().optional(),\n })\n .optional(),\n MagicDNSSuffix: z.string().optional(),\n Peer: z.record(z.string(), tailscaleStatusPeerSchema).optional(),\n});\n\ntype TailscaleStatus = z.infer<typeof tailscaleStatusSchema>;\n\ninterface SpawnResult {\n code: number | null;\n stdout: string;\n stderr: string;\n}\n\nfunction runCommand(cmd: string, args: string[]): Promise<SpawnResult> {\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n const out: Buffer[] = [];\n const err: Buffer[] = [];\n child.stdout?.on(\"data\", (chunk: Buffer) => out.push(chunk));\n child.stderr?.on(\"data\", (chunk: Buffer) => err.push(chunk));\n child.on(\"error\", reject);\n child.on(\"exit\", (code) =>\n resolve({\n code,\n stdout: Buffer.concat(out).toString(\"utf8\"),\n stderr: Buffer.concat(err).toString(\"utf8\"),\n }),\n );\n });\n}\n\nfunction checkTailscaleInstalled(): Promise<boolean> {\n return new Promise((resolve) => {\n const proc = spawn(\"which\", [\"tailscale\"]);\n proc.on(\"exit\", (code) => resolve(code === 0));\n proc.on(\"error\", () => resolve(false));\n });\n}\n\nfunction parseTailscaleStatus(stdout: string): TailscaleStatus | null {\n let raw: unknown;\n try {\n raw = JSON.parse(stdout);\n } catch {\n return null;\n }\n const result = tailscaleStatusSchema.safeParse(raw);\n return result.success ? result.data : null;\n}\n\nexport class LocalTailscaleService extends Service implements ITunnelService {\n static override serviceType = \"tunnel\";\n readonly capabilityDescription =\n \"Provides secure tunnel functionality via the locally-installed `tailscale` CLI (serve / funnel).\";\n\n private tunnelUrl: string | null = null;\n private tunnelPort: number | null = null;\n private startedAt: Date | null = null;\n private isShuttingDown = false;\n private useFunnel = false;\n\n static override async start(runtime: IAgentRuntime): Promise<Service> {\n const service = new LocalTailscaleService(runtime);\n await service.start();\n return service;\n }\n\n async start(): Promise<void> {\n elizaLogger.info(\"[LocalTailscaleService] starting\");\n const installed = await checkTailscaleInstalled();\n if (!installed) {\n throw new Error(\n \"tailscale is not installed. Install from https://tailscale.com/download or run: brew install tailscale\",\n );\n }\n }\n\n async stop(): Promise<void> {\n await this.stopTunnel();\n }\n\n async startTunnel(\n port?: number,\n options: { accountId?: string } = {},\n ): Promise<string | undefined> {\n if (this.isActive()) {\n elizaLogger.warn(\"[LocalTailscaleService] tunnel already running\");\n return this.tunnelUrl ?? undefined;\n }\n\n if (port === undefined || port === null) {\n elizaLogger.warn(\n \"[LocalTailscaleService] startTunnel called without a port — service active but no tunnel started\",\n );\n return;\n }\n\n if (port < 1 || port > 65535) {\n throw new Error(\"Invalid port number\");\n }\n\n const config = await validateTailscaleConfig(\n this.runtime,\n options.accountId,\n );\n this.useFunnel = config.TAILSCALE_FUNNEL;\n\n elizaLogger.info(\n `[LocalTailscaleService] starting tunnel on port ${port} (funnel=${this.useFunnel})`,\n );\n\n if (this.useFunnel) {\n const result = await runCommand(\"tailscale\", [\"funnel\", String(port)]);\n if (result.code !== 0) {\n throw new Error(\n `tailscale funnel exited with code ${result.code}: ${result.stderr.trim()}`,\n );\n }\n } else {\n const result = await runCommand(\"tailscale\", [\n \"serve\",\n \"--bg\",\n \"--https=443\",\n `localhost:${port}`,\n ]);\n if (result.code !== 0) {\n throw new Error(\n `tailscale serve exited with code ${result.code}: ${result.stderr.trim()}`,\n );\n }\n }\n\n const dnsName = await this.fetchSelfDnsName();\n if (!dnsName) {\n throw new Error(\n \"tailscale serve started but no DNSName resolved from `tailscale status --json`\",\n );\n }\n this.tunnelUrl = this.useFunnel\n ? `https://${dnsName}`\n : `https://${dnsName}`;\n this.tunnelPort = port;\n this.startedAt = new Date();\n elizaLogger.info(\n `[LocalTailscaleService] tunnel started: ${this.tunnelUrl}`,\n );\n return this.tunnelUrl;\n }\n\n async stopTunnel(_options: { accountId?: string } = {}): Promise<void> {\n if (!this.isActive()) {\n elizaLogger.warn(\"[LocalTailscaleService] no active tunnel to stop\");\n return;\n }\n this.isShuttingDown = true;\n elizaLogger.info(\"[LocalTailscaleService] stopping tunnel\");\n\n if (this.useFunnel) {\n await runCommand(\"tailscale\", [\"funnel\", \"reset\"]);\n } else {\n await runCommand(\"tailscale\", [\"serve\", \"reset\"]);\n }\n\n this.cleanup();\n this.isShuttingDown = false;\n elizaLogger.info(\"[LocalTailscaleService] tunnel stopped\");\n }\n\n getUrl(): string | null {\n return this.tunnelUrl;\n }\n\n isActive(): boolean {\n return this.tunnelUrl !== null && !this.isShuttingDown;\n }\n\n getStatus(): TunnelStatus {\n return {\n active: this.isActive(),\n url: this.tunnelUrl,\n port: this.tunnelPort,\n startedAt: this.startedAt,\n provider: \"tailscale\",\n };\n }\n\n private async fetchSelfDnsName(): Promise<string | null> {\n const result = await runCommand(\"tailscale\", [\"status\", \"--json\"]);\n if (result.code !== 0) {\n elizaLogger.error(\n `[LocalTailscaleService] tailscale status failed: ${result.stderr.trim()}`,\n );\n return null;\n }\n const status = parseTailscaleStatus(result.stdout);\n if (!status) {\n elizaLogger.error(\n \"[LocalTailscaleService] tailscale status returned malformed JSON\",\n );\n return null;\n }\n const raw = status.Self?.DNSName;\n if (!raw) return null;\n return raw.replace(/\\.$/, \"\");\n }\n\n private cleanup(): void {\n this.tunnelUrl = null;\n this.tunnelPort = null;\n this.startedAt = null;\n this.useFunnel = false;\n }\n}\n","import type { IAgentRuntime, TestCase, TestSuite } from \"@elizaos/core\";\nimport { CloudTailscaleService } from \"../services/CloudTailscaleService\";\nimport { LocalTailscaleService } from \"../services/LocalTailscaleService\";\n\nconst CANONICAL_TUNNEL_SERVICE_TYPE = \"tunnel\";\n\nexport class TailscaleTestSuite implements TestSuite {\n name = \"tailscale\";\n tests: TestCase[] = [\n {\n name: \"LocalTailscaleService claims canonical tunnel service-type\",\n fn: (_runtime: IAgentRuntime) => {\n if (\n LocalTailscaleService.serviceType !== CANONICAL_TUNNEL_SERVICE_TYPE\n ) {\n throw new Error(\n `LocalTailscaleService.serviceType must be \"${CANONICAL_TUNNEL_SERVICE_TYPE}\"`,\n );\n }\n },\n },\n {\n name: \"CloudTailscaleService claims canonical tunnel service-type\",\n fn: (_runtime: IAgentRuntime) => {\n if (\n CloudTailscaleService.serviceType !== CANONICAL_TUNNEL_SERVICE_TYPE\n ) {\n throw new Error(\n `CloudTailscaleService.serviceType must be \"${CANONICAL_TUNNEL_SERVICE_TYPE}\"`,\n );\n }\n },\n },\n ];\n}\n","/**\n * Tailscale ConnectorAccountManager provider.\n *\n * Adapts the existing multi-account resolver in `accounts.ts` to the\n * ConnectorAccountManager CRUD surface. Legacy single-account env/settings\n * are surfaced as the default OWNER account; additional accounts can be\n * declared through character.settings.tailscale.accounts or TAILSCALE_ACCOUNTS.\n *\n * Tailscale does not use an OAuth redirect flow here. Local CLI login and\n * cloud auth-key/API-key provisioning remain owned by the backend services.\n */\n\nimport type {\n ConnectorAccount,\n ConnectorAccountManager,\n ConnectorAccountPatch,\n ConnectorAccountProvider,\n ConnectorAccountPurpose,\n IAgentRuntime,\n} from \"@elizaos/core\";\nimport {\n normalizeTailscaleAccountId,\n readTailscaleAccounts,\n resolveTailscaleAccountId,\n type TailscaleAccountConfig,\n} from \"./accounts\";\n\nexport const TAILSCALE_PROVIDER_ID = \"tailscale\";\n\nconst DEFAULT_PURPOSES: ConnectorAccountPurpose[] = [\n \"admin\" as ConnectorAccountPurpose,\n \"automation\" as ConnectorAccountPurpose,\n];\n\nfunction hasExplicitConfig(account: TailscaleAccountConfig): boolean {\n return Boolean(\n account.authKey ||\n account.tags !== undefined ||\n account.funnel !== undefined ||\n account.defaultPort !== undefined ||\n account.backend !== undefined ||\n account.authKeyExpirySeconds !== undefined ||\n account.cloudApiKey ||\n account.cloudBaseUrl,\n );\n}\n\nfunction authMethodForAccount(account: TailscaleAccountConfig): string {\n if (account.cloudApiKey) return \"cloud_api_key\";\n if (account.authKey) return \"auth_key\";\n if (account.backend === \"local\") return \"local_cli\";\n return \"runtime\";\n}\n\nfunction toConnectorAccount(\n account: TailscaleAccountConfig,\n defaultAccountId: string,\n): ConnectorAccount {\n const now = Date.now();\n const accountId = normalizeTailscaleAccountId(account.accountId);\n const configured = hasExplicitConfig(account);\n return {\n id: accountId,\n provider: TAILSCALE_PROVIDER_ID,\n label: account.label ?? `Tailscale (${accountId})`,\n role: \"OWNER\",\n purpose: DEFAULT_PURPOSES,\n accessGate: \"open\",\n status: configured ? \"connected\" : \"disabled\",\n displayHandle: account.label ?? accountId,\n createdAt: now,\n updatedAt: now,\n metadata: {\n authMethod: authMethodForAccount(account),\n source: \"legacy\",\n isDefault: accountId === defaultAccountId,\n backend: account.backend ?? \"auto\",\n funnel: account.funnel ?? null,\n defaultPort: account.defaultPort ?? null,\n tags: account.tags ?? null,\n authKeyExpirySeconds: account.authKeyExpirySeconds ?? null,\n hasAuthKey: Boolean(account.authKey),\n hasCloudApiKey: Boolean(account.cloudApiKey),\n cloudBaseUrl: account.cloudBaseUrl ?? null,\n },\n };\n}\n\nfunction normalizePurposes(\n purpose: ConnectorAccountPatch[\"purpose\"] | undefined,\n fallback: ConnectorAccountPurpose[],\n): ConnectorAccountPurpose[] {\n if (Array.isArray(purpose)) return purpose;\n if (typeof purpose === \"string\" && purpose.trim()) return [purpose];\n return fallback;\n}\n\nfunction mergeStoredAccountPatch(\n account: ConnectorAccount,\n patch: ConnectorAccountPatch,\n): ConnectorAccount {\n return {\n ...account,\n ...patch,\n provider: TAILSCALE_PROVIDER_ID,\n id: account.id,\n purpose: normalizePurposes(patch.purpose, account.purpose),\n externalId:\n patch.externalId === undefined\n ? account.externalId\n : (patch.externalId ?? undefined),\n displayHandle:\n patch.displayHandle === undefined\n ? account.displayHandle\n : (patch.displayHandle ?? undefined),\n ownerBindingId:\n patch.ownerBindingId === undefined\n ? account.ownerBindingId\n : (patch.ownerBindingId ?? undefined),\n ownerIdentityId:\n patch.ownerIdentityId === undefined\n ? account.ownerIdentityId\n : (patch.ownerIdentityId ?? undefined),\n metadata: patch.metadata ?? account.metadata,\n createdAt: account.createdAt,\n };\n}\n\nexport function createTailscaleConnectorAccountProvider(\n runtime: IAgentRuntime,\n): ConnectorAccountProvider {\n return {\n provider: TAILSCALE_PROVIDER_ID,\n label: \"Tailscale\",\n\n listAccounts: async (\n manager: ConnectorAccountManager,\n ): Promise<ConnectorAccount[]> => {\n const stored = await manager\n .getStorage()\n .listAccounts(TAILSCALE_PROVIDER_ID);\n const storedById = new Set(stored.map((account) => account.id));\n const defaultAccountId = resolveTailscaleAccountId(runtime);\n const synthesized = readTailscaleAccounts(runtime)\n .map((account) => toConnectorAccount(account, defaultAccountId))\n .filter((account) => !storedById.has(account.id));\n return [...stored, ...synthesized];\n },\n\n createAccount: async (\n input: ConnectorAccountPatch,\n _manager: ConnectorAccountManager,\n ) => {\n return {\n ...input,\n provider: TAILSCALE_PROVIDER_ID,\n role: input.role ?? \"OWNER\",\n purpose: input.purpose ?? DEFAULT_PURPOSES,\n accessGate: input.accessGate ?? \"open\",\n status: input.status ?? \"pending\",\n };\n },\n\n patchAccount: async (\n accountId: string,\n patch: ConnectorAccountPatch,\n manager: ConnectorAccountManager,\n ) => {\n const existing = await manager\n .getStorage()\n .getAccount(TAILSCALE_PROVIDER_ID, accountId);\n if (existing) {\n return mergeStoredAccountPatch(existing, patch);\n }\n return { ...patch, provider: TAILSCALE_PROVIDER_ID };\n },\n\n deleteAccount: async (\n _accountId: string,\n _manager: ConnectorAccountManager,\n ): Promise<void> => {\n // Runtime credentials live in env/character settings or the selected\n // backend; the manager removes only its account row.\n },\n };\n}\n","/**\n * Tailscale plugin re-exports the canonical tunnel-service contract from\n * `@elizaos/plugin-tunnel`. Both backends (local CLI, cloud auth-key minter)\n * register under `serviceType=\"tunnel\"`. Consumers should call\n * `getTunnelService(runtime)` from `@elizaos/plugin-tunnel` to stay\n * backend-agnostic.\n */\n\nexport {\n getTunnelService,\n type ITunnelService,\n type TunnelProvider,\n type TunnelStatus,\n} from \"@elizaos/plugin-tunnel\";\n\nexport type TailscaleBackendMode = \"local\" | \"cloud\" | \"auto\";\n","/**\n * tailscaleStatus provider — injects the current tunnel status into the LLM\n * context as compact JSON.\n *\n * Replaces the previous GET_TAILSCALE_STATUS action: status is now passively\n * available every turn so the planner does not need to dispatch a dedicated\n * action just to read tunnel state.\n */\n\nimport type { IAgentRuntime, Memory, Provider, State } from \"@elizaos/core\";\nimport { getTunnelService } from \"../types\";\n\nfunction formatUptime(startedAt: Date): string {\n const ms = Date.now() - startedAt.getTime();\n const minutes = Math.floor(ms / 60_000);\n const hours = Math.floor(minutes / 60);\n if (hours > 0) {\n return `${hours} hour${hours === 1 ? \"\" : \"s\"}, ${minutes % 60} minute${minutes % 60 === 1 ? \"\" : \"s\"}`;\n }\n return `${minutes} minute${minutes === 1 ? \"\" : \"s\"}`;\n}\n\nexport const tailscaleStatusProvider: Provider = {\n name: \"tailscaleStatus\",\n description:\n \"Current Tailscale tunnel status: active flag, public URL, local port, uptime, backend provider.\",\n descriptionCompressed: \"Tailscale tunnel status: active, url, port, uptime.\",\n dynamic: true,\n contexts: [\"settings\", \"connectors\"],\n contextGate: { anyOf: [\"settings\", \"connectors\"] },\n cacheStable: false,\n cacheScope: \"turn\",\n get: async (runtime: IAgentRuntime, _message: Memory, _state: State) => {\n const tunnelService = getTunnelService(runtime);\n if (!tunnelService) {\n return { text: \"\" };\n }\n\n const status = tunnelService.getStatus();\n const uptime = status.startedAt ? formatUptime(status.startedAt) : null;\n\n const text = JSON.stringify({\n tailscale: {\n active: status.active,\n url: status.url,\n port: status.port,\n uptime,\n provider: status.provider,\n },\n });\n\n return {\n text,\n values: {\n active: status.active,\n url: status.url ?? \"\",\n port: status.port ?? 0,\n provider: status.provider,\n },\n data: { status, uptime },\n };\n },\n};\n","import { isCloudConnected } from \"@elizaos/cloud-routing\";\nimport { elizaLogger, type IAgentRuntime } from \"@elizaos/core\";\nimport {\n readTailscaleAccounts,\n resolveTailscaleAccount,\n resolveTailscaleAccountId,\n} from \"../accounts\";\nimport type { TailscaleBackendMode } from \"../types\";\nimport { CloudTailscaleService } from \"./CloudTailscaleService\";\nimport { LocalTailscaleService } from \"./LocalTailscaleService\";\n\ntype TunnelBackendCtor =\n | typeof LocalTailscaleService\n | typeof CloudTailscaleService;\n\nexport interface BackendDecision {\n backend: TunnelBackendCtor;\n mode: TailscaleBackendMode;\n reason: string;\n}\n\nconst ALLOWED_MODES: ReadonlySet<TailscaleBackendMode> = new Set([\n \"local\",\n \"cloud\",\n \"auto\",\n]);\n\nexport function readBackendMode(runtime: IAgentRuntime): TailscaleBackendMode {\n const account = resolveTailscaleAccount(\n readTailscaleAccounts(runtime),\n resolveTailscaleAccountId(runtime),\n );\n const raw = account?.backend ?? runtime.getSetting(\"TAILSCALE_BACKEND\");\n if (raw === null || raw === undefined) return \"auto\";\n const normalized = String(raw).trim().toLowerCase();\n if (ALLOWED_MODES.has(normalized as TailscaleBackendMode)) {\n return normalized as TailscaleBackendMode;\n }\n elizaLogger.warn(\n `[TunnelBackendSelector] invalid TAILSCALE_BACKEND=\"${raw}\" — falling back to \"auto\"`,\n );\n return \"auto\";\n}\n\nexport function selectTunnelBackend(runtime: IAgentRuntime): BackendDecision {\n const mode = readBackendMode(runtime);\n\n switch (mode) {\n case \"local\":\n return {\n backend: LocalTailscaleService,\n mode,\n reason: \"TAILSCALE_BACKEND=local\",\n };\n case \"cloud\":\n return {\n backend: CloudTailscaleService,\n mode,\n reason: \"TAILSCALE_BACKEND=cloud\",\n };\n case \"auto\": {\n if (isCloudConnected(runtime)) {\n return {\n backend: CloudTailscaleService,\n mode,\n reason: \"auto: cloud connected\",\n };\n }\n return {\n backend: LocalTailscaleService,\n mode,\n reason: \"auto: cloud not connected\",\n };\n }\n }\n}\n"],"mappings":"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","names":["elizaLogger","z","readSetting","z","spawn","elizaLogger","Service","z","z","spawn","Service","elizaLogger","elizaLogger","elizaLogger","elizaLogger"]}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@elizaos/plugin-tailscale",
3
+ "version": "2.0.0-beta.1",
4
+ "description": "Tunnel plugin for elizaOS — local Tailscale serve/funnel or Eliza Cloud-routed Tailscale auth-key minter",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format esm --dts",
22
+ "typecheck": "tsc --noEmit",
23
+ "test": "bun run --cwd ../../packages/cloud-routing build && vitest run",
24
+ "test:watch": "vitest",
25
+ "test:coverage": "vitest run --coverage",
26
+ "clean": "rm -rf dist coverage",
27
+ "lint": "bunx @biomejs/biome check --write --unsafe .",
28
+ "lint:check": "bunx @biomejs/biome check .",
29
+ "format": "bunx @biomejs/biome format --write .",
30
+ "format:check": "bunx @biomejs/biome format ."
31
+ },
32
+ "peerDependencies": {
33
+ "@elizaos/core": "2.0.0-beta.1",
34
+ "@elizaos/plugin-tunnel": "2.0.0-beta.1"
35
+ },
36
+ "dependencies": {
37
+ "@elizaos/cloud-routing": "2.0.0-beta.1",
38
+ "@elizaos/core": "2.0.0-beta.1",
39
+ "@elizaos/plugin-tunnel": "2.0.0-beta.1",
40
+ "zod": "^4.4.3"
41
+ },
42
+ "devDependencies": {
43
+ "@biomejs/biome": "^2.4.14",
44
+ "@types/node": "^25.6.0",
45
+ "tsup": "^8.5.1",
46
+ "typescript": "^6.0.3",
47
+ "vitest": "^4.0.17"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ }
52
+ }