tailscale-web 0.1.10

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) 2026 Adriano Sela Aviles
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 ADDED
@@ -0,0 +1,220 @@
1
+ # tailscale-web
2
+
3
+ [![npm version](https://img.shields.io/npm/v/tailscale-web.svg?style=flat-square)](https://www.npmjs.org/package/tailscale-web)
4
+ [![GitHub issues](https://img.shields.io/github/issues/adrianosela/tailscale-web.svg)](https://github.com/adrianosela/tailscale-web/issues)
5
+ [![license](https://img.shields.io/github/license/adrianosela/tailscale-web.svg)](https://github.com/adrianosela/tailscale-web/blob/master/LICENSE)
6
+ [![Go Report Card](https://goreportcard.com/badge/github.com/adrianosela/tailscale-web)](https://goreportcard.com/report/github.com/adrianosela/tailscale-web)
7
+
8
+ Run a [Tailscale](https://tailscale.com) device directly in the browser. Make HTTP requests, open TCP connections, ping hosts, and use exit nodes for networking beyond Tailscale devices — all from a web page, with no server-side proxy required.
9
+
10
+ <details>
11
+ <summary>**Click here for motivation**</summary>
12
+
13
+ ### Motivation
14
+
15
+ Tailscale software readily compiles for JavaScript runtimes (via WebAssembly). However, there isn't (as of February 2026) a JavaScript library that makes it easy to set-up Tailscale in the browser. You have to write the Go code yourself, compile it to WebAssembly bytecode (with GOOS=js GOARCH=wasm) yourself, and bundle it yourself alongside the JavaScript shim that sets up the Go runtime (a.k.a. `wasm_exec.js`).
16
+
17
+ Once you are done doing all that, then you have to implement any network clients you need. For example, if you want to perform HTTP requests over the Tailscale network, you have to implement a bridge (in Go) for your JavaScript to call. This is why this library includes common clients (TCP, ICMP, HTTP... with more to come).
18
+
19
+ </details>
20
+
21
+ ## Install
22
+
23
+ ### Web
24
+
25
+ ```html
26
+ <script type="module">
27
+ import { network } from "https://esm.sh/tailscale-web"
28
+ </script>
29
+ ```
30
+
31
+ ### Node / npm
32
+
33
+ ```bash
34
+ npm install tailscale-web
35
+ ```
36
+
37
+ ```ts
38
+ import { network } from "tailscale-web"
39
+ ```
40
+
41
+ ## Quick start
42
+
43
+ ### Web
44
+
45
+ ```html
46
+ <!DOCTYPE html>
47
+ <html>
48
+ <body>
49
+ <script type="module">
50
+ import { network } from "https://esm.sh/tailscale-web"
51
+
52
+ await network.init({
53
+ hostname: "my-app",
54
+ onAuthRequired(url) {
55
+ window.open(url, "_blank", "width=600,height=700")
56
+ },
57
+ onAuthComplete() {
58
+ console.log("connected!")
59
+ },
60
+ })
61
+
62
+ const resp = await network.fetch("http://my-server/api/data")
63
+ console.log(await resp.json())
64
+ </script>
65
+ </body>
66
+ </html>
67
+ ```
68
+
69
+ ### Node / npm
70
+
71
+ ```ts
72
+ import { network } from "tailscale-web"
73
+
74
+ await network.init({
75
+ hostname: "my-app",
76
+ onAuthRequired(url) {
77
+
78
+ // Open the URL however your environment allows
79
+ console.log("Authenticate at:", url)
80
+ },
81
+ onAuthComplete() {
82
+ console.log("connected!")
83
+ },
84
+ })
85
+
86
+ const resp = await network.fetch("http://my-server/api/data")
87
+ console.log(await resp.json())
88
+ ```
89
+
90
+ In a browser, state is persisted to `localStorage` automatically, so the device reconnects on the next page load without requiring login again. Pass a `storage` adapter in `init()` to use a custom backend.
91
+
92
+ ## API
93
+
94
+ ### `network.init(options?)`
95
+
96
+ Loads the WASM, starts the Tailscale node, and waits until it is authenticated and ready. Must be called before any other method.
97
+
98
+ If the node has persisted state it reconnects automatically; otherwise the OAuth flow is started and `onAuthRequired` is called with the login URL. Rejects if the auth URL does not arrive within 60 seconds, or if the user does not complete authentication within 5 minutes.
99
+
100
+ ```ts
101
+ await network.init({
102
+ // device name on the tailnet (default: "tailscale-web")
103
+ hostname?: string
104
+
105
+ // custom store; defaults to localStorage (browser) or in-memory (elsewhere)
106
+ storage?: StorageAdapter
107
+
108
+ // key prefix for the default store (default: "tailscale-web")
109
+ // keys are written as "{prefix}_{stateKey}"
110
+ storagePrefix?: string
111
+
112
+ // custom coordination server URL
113
+ controlUrl?: string
114
+
115
+ // called when login is needed
116
+ onAuthRequired?: (url: string) => void
117
+
118
+ // called once authenticated
119
+ onAuthComplete?: () => void
120
+ })
121
+ ```
122
+
123
+ ```ts
124
+ // Example: use sessionStorage as the backend
125
+ await network.init({
126
+ hostname: "my-app",
127
+ storage: {
128
+ get: key => sessionStorage.getItem(key),
129
+ set: (key, val) => sessionStorage.setItem(key, val),
130
+ },
131
+ onAuthRequired(url) { window.open(url, "_blank", "width=600,height=700") },
132
+ })
133
+ ```
134
+
135
+ ### `network.fetch(url, init?)`
136
+
137
+ Make an HTTP/HTTPS request through the tailnet. Supports `method`, `headers`, and `body`. Does not yet support `AbortSignal`, streaming bodies or responses, or other advanced Fetch API options (`mode`, `credentials`, `cache`, `redirect`).
138
+
139
+ ```ts
140
+ const resp = await network.fetch("https://internal-service/api", {
141
+ method: "POST",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: JSON.stringify({ key: "value" }),
144
+ })
145
+
146
+ console.log(resp.status) // 200
147
+ console.log(resp.ok) // true
148
+ const data = await resp.json()
149
+ ```
150
+
151
+ ### `network.ping(addr)`
152
+
153
+ ICMP ping a peer and measure round-trip time. `addr` may be a hostname or Tailscale IP.
154
+
155
+ ```ts
156
+ const result = await network.ping("my-server")
157
+ // { alive: true, rttMs: 3.2, nodeName: "my-server", nodeIP: "100.x.x.x", ... }
158
+ ```
159
+
160
+ ### `network.dialTCP(addr)`
161
+
162
+ Open a raw TCP connection through the tailnet. Returns a `Connection` object.
163
+
164
+ ```ts
165
+ const conn = await network.dialTCP("my-server:8080")
166
+
167
+ conn.onData(data => {
168
+ console.log(new TextDecoder().decode(data))
169
+ })
170
+
171
+ conn.write("hello\n")
172
+ conn.close()
173
+ ```
174
+
175
+ ### Exit nodes
176
+
177
+ ```ts
178
+ // list available exit nodes
179
+ const nodes = network.listExitNodes()
180
+ // [{ id, hostName, dnsName, tailscaleIP, active, online }, ...]
181
+
182
+ // activate an exit node
183
+ await network.setExitNode(nodes[0].id)
184
+
185
+ // clear the exit node (route traffic directly)
186
+ await network.setExitNode()
187
+ ```
188
+
189
+ ### Routes & preferences
190
+
191
+ ```ts
192
+ // accept subnet routes advertised by peers
193
+ await network.setAcceptRoutes(true)
194
+
195
+ // current preferences
196
+ const prefs = network.getPrefs()
197
+ // { acceptRoutes: true, exitNodeId: "..." }
198
+
199
+ // full routing table
200
+ const routes = network.getRoutes()
201
+ // [{ prefix, via, isPrimary, isExitRoute }, ...]
202
+ ```
203
+
204
+ ### DNS
205
+
206
+ ```ts
207
+ const dns = network.getDNS()
208
+ // {
209
+ // resolvers: string[]
210
+ // routes: Record<string, string[]> // split-DNS: suffix → resolvers
211
+ // domains: string[] // search domains
212
+ // extraRecords: { name, type, value }[]
213
+ // magicDNS: boolean
214
+ // }
215
+ ```
216
+
217
+ ## Notes
218
+
219
+ - **WASM size.** The Tailscale WASM binary is ~35 MB. It is loaded once and cached by the browser.
220
+ - **Auth persistence.** Tailscale auth state is stored in `localStorage` (or your custom adapter) under a key prefix. Call `localStorage.clear()` or remove the prefixed keys to log out.
@@ -0,0 +1,261 @@
1
+ export declare interface Connection {
2
+ /** Register a handler for incoming data. Must be called before write() — data can arrive as soon as the connection is established. */
3
+ onData(handler: (data: Uint8Array) => void): void;
4
+ /** Send data over the connection. Accepts a Uint8Array or a string. */
5
+ write(data: Uint8Array | string): void;
6
+ /** Close the connection and release all resources. */
7
+ close(): void;
8
+ }
9
+
10
+ export declare interface DNSInfo {
11
+ /** Global nameservers. */
12
+ resolvers: string[];
13
+ /** Split-DNS map: suffix → dedicated resolver addresses. */
14
+ routes: Record<string, string[]>;
15
+ /** Search/split-DNS domains. */
16
+ domains: string[];
17
+ /** Custom DNS records pushed by the control plane. */
18
+ extraRecords: {
19
+ name: string;
20
+ type: string;
21
+ value: string;
22
+ }[];
23
+ /** Whether MagicDNS proxied resolution is enabled. */
24
+ magicDNS: boolean;
25
+ }
26
+
27
+ export declare interface ExitNode {
28
+ /** Stable node ID — pass to setExitNode() to activate. */
29
+ id: string;
30
+ hostName: string;
31
+ /** MagicDNS FQDN (ends with a dot). */
32
+ dnsName: string;
33
+ /** Primary Tailscale IPv4 address. */
34
+ tailscaleIP: string;
35
+ /** Whether this node is the currently active exit node. */
36
+ active: boolean;
37
+ online: boolean;
38
+ }
39
+
40
+ export declare interface InitOptions {
41
+ /** Device name as it appears on the tailnet. Default: "tailscale-web" */
42
+ hostname?: string;
43
+ /**
44
+ * Custom storage backend for persisting Tailscale state.
45
+ * If omitted, defaults to localStorage in browser environments,
46
+ * or an in-memory store (no persistence) elsewhere.
47
+ */
48
+ storage?: StorageAdapter | null;
49
+ /** Key prefix used by the default localStorage store. Keys are written as "{prefix}_{stateKey}". Default: "tailscale-web" */
50
+ storagePrefix?: string;
51
+ /** Override the Tailscale coordination server URL. */
52
+ controlUrl?: string;
53
+ /** Called with the OAuth URL when interactive login is needed. */
54
+ onAuthRequired?: (url: string) => void;
55
+ /** Called once the device is authenticated and connected. */
56
+ onAuthComplete?: () => void;
57
+ }
58
+
59
+ export declare const network: {
60
+ /**
61
+ * Initialize and connect the Tailscale node. Must be called before any
62
+ * other method. Resolves once the node is authenticated and ready.
63
+ *
64
+ * If the node has persisted state from a previous session it reconnects
65
+ * automatically. Otherwise the OAuth flow is triggered via onAuthRequired.
66
+ * Rejects if the auth URL does not arrive within 60 seconds, or if the
67
+ * user does not complete authentication within 5 minutes.
68
+ *
69
+ * @example
70
+ * await network.init({
71
+ * hostname: "my-app",
72
+ * onAuthRequired(url) {
73
+ * window.open(url, "_blank", "width=600,height=700")
74
+ * },
75
+ * onAuthComplete() {
76
+ * console.log("connected!")
77
+ * },
78
+ * })
79
+ *
80
+ * @example
81
+ * // Custom storage backend (e.g. sessionStorage or any key/value store)
82
+ * await network.init({
83
+ * hostname: "my-app",
84
+ * storage: {
85
+ * get: key => sessionStorage.getItem(key),
86
+ * set: (key, val) => sessionStorage.setItem(key, val),
87
+ * },
88
+ * onAuthRequired(url) { console.log("Authenticate at:", url) },
89
+ * })
90
+ */
91
+ init(options?: InitOptions): Promise<void>;
92
+ /**
93
+ * Send an ICMP ping to addr and measure round-trip time.
94
+ * addr may be a hostname or Tailscale IP.
95
+ *
96
+ * @example
97
+ * const result = await network.ping("my-server")
98
+ * if (result.alive) {
99
+ * console.log(`rtt: ${result.rttMs.toFixed(3)} ms ip: ${result.nodeIP}`)
100
+ * } else {
101
+ * console.warn("unreachable:", result.err)
102
+ * }
103
+ */
104
+ ping(addr: string): Promise<PingResult>;
105
+ /**
106
+ * Open a raw TCP connection through the Tailscale network.
107
+ * Returns a Connection object for sending and receiving data.
108
+ *
109
+ * @example
110
+ * const conn = await network.dialTCP("my-server:8080")
111
+ *
112
+ * conn.onData(data => {
113
+ * console.log(new TextDecoder().decode(data))
114
+ * })
115
+ *
116
+ * conn.write("hello\n")
117
+ * conn.close()
118
+ */
119
+ dialTCP(addr: string): Promise<Connection>;
120
+ /**
121
+ * Make an HTTP request through the Tailscale network. Supports method,
122
+ * headers, and body. Does not yet support AbortSignal, streaming bodies
123
+ * or responses, or other advanced Fetch API options.
124
+ *
125
+ * @example
126
+ * const resp = await network.fetch("https://internal-service/api", {
127
+ * method: "POST",
128
+ * headers: { "Content-Type": "application/json" },
129
+ * body: JSON.stringify({ key: "value" }),
130
+ * })
131
+ * console.log(resp.status, resp.ok)
132
+ * const data = await resp.json()
133
+ */
134
+ fetch(url: string, init?: RequestInit_2): Promise<Response_2>;
135
+ /**
136
+ * Return the current preferences (acceptRoutes, exitNodeId).
137
+ * Synchronous — no await needed. Must be called after init() resolves.
138
+ *
139
+ * @example
140
+ * const { acceptRoutes, exitNodeId } = network.getPrefs()
141
+ * console.log("exit node:", exitNodeId || "(none)")
142
+ */
143
+ getPrefs(): Prefs;
144
+ /**
145
+ * Enable or disable acceptance of subnet routes advertised by peers.
146
+ * Equivalent to `tailscale set --accept-routes`.
147
+ *
148
+ * @example
149
+ * await network.setAcceptRoutes(true)
150
+ */
151
+ setAcceptRoutes(accept: boolean): Promise<void>;
152
+ /**
153
+ * Return all peers that advertise exit-node capability.
154
+ * Synchronous — no await needed. Returns an empty array if called before init() resolves.
155
+ *
156
+ * @example
157
+ * const nodes = network.listExitNodes()
158
+ * for (const n of nodes) {
159
+ * console.log(n.hostName, n.tailscaleIP, n.online ? "online" : "offline")
160
+ * }
161
+ */
162
+ listExitNodes(): ExitNode[];
163
+ /**
164
+ * Activate an exit node by its stable node ID.
165
+ * Pass an empty string (or omit) to clear the exit node.
166
+ *
167
+ * @example
168
+ * // Activate the first available online exit node
169
+ * const node = network.listExitNodes().find(n => n.online)
170
+ * if (node) await network.setExitNode(node.id)
171
+ *
172
+ * @example
173
+ * // Clear the active exit node
174
+ * await network.setExitNode()
175
+ */
176
+ setExitNode(id?: string): Promise<void>;
177
+ /**
178
+ * Return the full routing table (self + all peers).
179
+ * Synchronous — no await needed. Returns an empty array if called before init() resolves.
180
+ *
181
+ * @example
182
+ * const routes = network.getRoutes()
183
+ * for (const r of routes) {
184
+ * console.log(r.prefix, "via", r.via, r.isExitRoute ? "(exit)" : "")
185
+ * }
186
+ */
187
+ getRoutes(): Route[];
188
+ /**
189
+ * Return the current Tailscale-managed DNS configuration.
190
+ * Synchronous — no await needed. Returns an empty DNSInfo object if called before init() resolves.
191
+ *
192
+ * @example
193
+ * const dns = network.getDNS()
194
+ * console.log("resolvers:", dns.resolvers)
195
+ * console.log("MagicDNS:", dns.magicDNS)
196
+ * for (const [suffix, resolvers] of Object.entries(dns.routes)) {
197
+ * console.log(`split-DNS: ${suffix} → ${resolvers.join(", ")}`)
198
+ * }
199
+ */
200
+ getDNS(): DNSInfo;
201
+ };
202
+
203
+ export declare interface PingResult {
204
+ alive: boolean;
205
+ /** Round-trip time in milliseconds. Only meaningful when alive is true. */
206
+ rttMs: number;
207
+ /** MagicDNS name of the destination peer. */
208
+ nodeName: string;
209
+ /** Tailscale IP of the destination. */
210
+ nodeIP: string;
211
+ /** Direct UDP endpoint used if a direct path exists (e.g. an IP:port string). */
212
+ endpoint: string;
213
+ /** DERP relay region code (e.g. "nyc") if traffic was relayed; empty if direct. */
214
+ derpRegionCode: string;
215
+ /** Error reason when alive is false. */
216
+ err: string;
217
+ }
218
+
219
+ export declare interface Prefs {
220
+ /** Whether subnet routes advertised by peers are accepted. */
221
+ acceptRoutes: boolean;
222
+ /** Stable node ID of the active exit node, or empty string if none. */
223
+ exitNodeId: string;
224
+ }
225
+
226
+ declare interface RequestInit_2 {
227
+ method?: string;
228
+ headers?: Record<string, string>;
229
+ body?: string | Uint8Array;
230
+ }
231
+ export { RequestInit_2 as RequestInit }
232
+
233
+ declare interface Response_2 {
234
+ status: number;
235
+ statusText: string;
236
+ ok: boolean;
237
+ headers: Record<string, string>;
238
+ text(): Promise<string>;
239
+ json(): Promise<unknown>;
240
+ arrayBuffer(): Promise<ArrayBuffer>;
241
+ bytes(): Promise<Uint8Array>;
242
+ }
243
+ export { Response_2 as Response }
244
+
245
+ export declare interface Route {
246
+ /** CIDR prefix (e.g. a subnet or default route). */
247
+ prefix: string;
248
+ /** Display name of the advertising node, or "self". */
249
+ via: string;
250
+ /** Whether this node is the primary (active) router for the prefix. */
251
+ isPrimary: boolean;
252
+ /** Whether this is a default/exit route (0.0.0.0/0 or ::/0). */
253
+ isExitRoute: boolean;
254
+ }
255
+
256
+ export declare interface StorageAdapter {
257
+ get(key: string): string | null;
258
+ set(key: string, value: string): void;
259
+ }
260
+
261
+ export { }
package/dist/main.wasm ADDED
Binary file
@@ -0,0 +1,629 @@
1
+ (() => {
2
+ const n = () => {
3
+ const c = new Error("not implemented");
4
+ return c.code = "ENOSYS", c;
5
+ };
6
+ if (!globalThis.fs) {
7
+ let c = "";
8
+ globalThis.fs = {
9
+ constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 },
10
+ // unused
11
+ writeSync(i, s) {
12
+ c += g.decode(s);
13
+ const r = c.lastIndexOf(`
14
+ `);
15
+ return r != -1 && (console.log(c.substring(0, r)), c = c.substring(r + 1)), s.length;
16
+ },
17
+ write(i, s, r, a, y, u) {
18
+ if (r !== 0 || a !== s.length || y !== null) {
19
+ u(n());
20
+ return;
21
+ }
22
+ const w = this.writeSync(i, s);
23
+ u(null, w);
24
+ },
25
+ chmod(i, s, r) {
26
+ r(n());
27
+ },
28
+ chown(i, s, r, a) {
29
+ a(n());
30
+ },
31
+ close(i, s) {
32
+ s(n());
33
+ },
34
+ fchmod(i, s, r) {
35
+ r(n());
36
+ },
37
+ fchown(i, s, r, a) {
38
+ a(n());
39
+ },
40
+ fstat(i, s) {
41
+ s(n());
42
+ },
43
+ fsync(i, s) {
44
+ s(null);
45
+ },
46
+ ftruncate(i, s, r) {
47
+ r(n());
48
+ },
49
+ lchown(i, s, r, a) {
50
+ a(n());
51
+ },
52
+ link(i, s, r) {
53
+ r(n());
54
+ },
55
+ lstat(i, s) {
56
+ s(n());
57
+ },
58
+ mkdir(i, s, r) {
59
+ r(n());
60
+ },
61
+ open(i, s, r, a) {
62
+ a(n());
63
+ },
64
+ read(i, s, r, a, y, u) {
65
+ u(n());
66
+ },
67
+ readdir(i, s) {
68
+ s(n());
69
+ },
70
+ readlink(i, s) {
71
+ s(n());
72
+ },
73
+ rename(i, s, r) {
74
+ r(n());
75
+ },
76
+ rmdir(i, s) {
77
+ s(n());
78
+ },
79
+ stat(i, s) {
80
+ s(n());
81
+ },
82
+ symlink(i, s, r) {
83
+ r(n());
84
+ },
85
+ truncate(i, s, r) {
86
+ r(n());
87
+ },
88
+ unlink(i, s) {
89
+ s(n());
90
+ },
91
+ utimes(i, s, r, a) {
92
+ a(n());
93
+ }
94
+ };
95
+ }
96
+ if (globalThis.process || (globalThis.process = {
97
+ getuid() {
98
+ return -1;
99
+ },
100
+ getgid() {
101
+ return -1;
102
+ },
103
+ geteuid() {
104
+ return -1;
105
+ },
106
+ getegid() {
107
+ return -1;
108
+ },
109
+ getgroups() {
110
+ throw n();
111
+ },
112
+ pid: -1,
113
+ ppid: -1,
114
+ umask() {
115
+ throw n();
116
+ },
117
+ cwd() {
118
+ throw n();
119
+ },
120
+ chdir() {
121
+ throw n();
122
+ }
123
+ }), globalThis.path || (globalThis.path = {
124
+ resolve(...c) {
125
+ return c.join("/");
126
+ }
127
+ }), !globalThis.crypto)
128
+ throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
129
+ if (!globalThis.performance)
130
+ throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
131
+ if (!globalThis.TextEncoder)
132
+ throw new Error("globalThis.TextEncoder is not available, polyfill required");
133
+ if (!globalThis.TextDecoder)
134
+ throw new Error("globalThis.TextDecoder is not available, polyfill required");
135
+ const h = new TextEncoder("utf-8"), g = new TextDecoder("utf-8");
136
+ globalThis.Go = class {
137
+ constructor() {
138
+ this.argv = ["js"], this.env = {}, this.exit = (t) => {
139
+ t !== 0 && console.warn("exit code:", t);
140
+ }, this._exitPromise = new Promise((t) => {
141
+ this._resolveExitPromise = t;
142
+ }), this._pendingEvent = null, this._scheduledTimeouts = /* @__PURE__ */ new Map(), this._nextCallbackTimeoutID = 1;
143
+ const c = (t, e) => {
144
+ this.mem.setUint32(t + 0, e, !0), this.mem.setUint32(t + 4, Math.floor(e / 4294967296), !0);
145
+ }, i = (t) => {
146
+ const e = this.mem.getUint32(t + 0, !0), o = this.mem.getInt32(t + 4, !0);
147
+ return e + o * 4294967296;
148
+ }, s = (t) => {
149
+ const e = this.mem.getFloat64(t, !0);
150
+ if (e === 0)
151
+ return;
152
+ if (!isNaN(e))
153
+ return e;
154
+ const o = this.mem.getUint32(t, !0);
155
+ return this._values[o];
156
+ }, r = (t, e) => {
157
+ if (typeof e == "number" && e !== 0) {
158
+ if (isNaN(e)) {
159
+ this.mem.setUint32(t + 4, 2146959360, !0), this.mem.setUint32(t, 0, !0);
160
+ return;
161
+ }
162
+ this.mem.setFloat64(t, e, !0);
163
+ return;
164
+ }
165
+ if (e === void 0) {
166
+ this.mem.setFloat64(t, 0, !0);
167
+ return;
168
+ }
169
+ let l = this._ids.get(e);
170
+ l === void 0 && (l = this._idPool.pop(), l === void 0 && (l = this._values.length), this._values[l] = e, this._goRefCounts[l] = 0, this._ids.set(e, l)), this._goRefCounts[l]++;
171
+ let m = 0;
172
+ switch (typeof e) {
173
+ case "object":
174
+ e !== null && (m = 1);
175
+ break;
176
+ case "string":
177
+ m = 2;
178
+ break;
179
+ case "symbol":
180
+ m = 3;
181
+ break;
182
+ case "function":
183
+ m = 4;
184
+ break;
185
+ }
186
+ this.mem.setUint32(t + 4, 2146959360 | m, !0), this.mem.setUint32(t, l, !0);
187
+ }, a = (t) => {
188
+ const e = i(t + 0), o = i(t + 8);
189
+ return new Uint8Array(this._inst.exports.mem.buffer, e, o);
190
+ }, y = (t) => {
191
+ const e = i(t + 0), o = i(t + 8), l = new Array(o);
192
+ for (let m = 0; m < o; m++)
193
+ l[m] = s(e + m * 8);
194
+ return l;
195
+ }, u = (t) => {
196
+ const e = i(t + 0), o = i(t + 8);
197
+ return g.decode(new DataView(this._inst.exports.mem.buffer, e, o));
198
+ }, w = (t, e) => (this._inst.exports.testExport0(), this._inst.exports.testExport(t, e)), d = Date.now() - performance.now();
199
+ this.importObject = {
200
+ _gotest: {
201
+ add: (t, e) => t + e,
202
+ callExport: w
203
+ },
204
+ gojs: {
205
+ // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
206
+ // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
207
+ // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
208
+ // This changes the SP, thus we have to update the SP used by the imported function.
209
+ // func wasmExit(code int32)
210
+ "runtime.wasmExit": (t) => {
211
+ t >>>= 0;
212
+ const e = this.mem.getInt32(t + 8, !0);
213
+ this.exited = !0, delete this._inst, delete this._values, delete this._goRefCounts, delete this._ids, delete this._idPool, this.exit(e);
214
+ },
215
+ // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
216
+ "runtime.wasmWrite": (t) => {
217
+ t >>>= 0;
218
+ const e = i(t + 8), o = i(t + 16), l = this.mem.getInt32(t + 24, !0);
219
+ fs.writeSync(e, new Uint8Array(this._inst.exports.mem.buffer, o, l));
220
+ },
221
+ // func resetMemoryDataView()
222
+ "runtime.resetMemoryDataView": (t) => {
223
+ this.mem = new DataView(this._inst.exports.mem.buffer);
224
+ },
225
+ // func nanotime1() int64
226
+ "runtime.nanotime1": (t) => {
227
+ t >>>= 0, c(t + 8, (d + performance.now()) * 1e6);
228
+ },
229
+ // func walltime() (sec int64, nsec int32)
230
+ "runtime.walltime": (t) => {
231
+ t >>>= 0;
232
+ const e = (/* @__PURE__ */ new Date()).getTime();
233
+ c(t + 8, e / 1e3), this.mem.setInt32(t + 16, e % 1e3 * 1e6, !0);
234
+ },
235
+ // func scheduleTimeoutEvent(delay int64) int32
236
+ "runtime.scheduleTimeoutEvent": (t) => {
237
+ t >>>= 0;
238
+ const e = this._nextCallbackTimeoutID;
239
+ this._nextCallbackTimeoutID++, this._scheduledTimeouts.set(e, setTimeout(
240
+ () => {
241
+ for (this._resume(); this._scheduledTimeouts.has(e); )
242
+ console.warn("scheduleTimeoutEvent: missed timeout event"), this._resume();
243
+ },
244
+ i(t + 8)
245
+ )), this.mem.setInt32(t + 16, e, !0);
246
+ },
247
+ // func clearTimeoutEvent(id int32)
248
+ "runtime.clearTimeoutEvent": (t) => {
249
+ t >>>= 0;
250
+ const e = this.mem.getInt32(t + 8, !0);
251
+ clearTimeout(this._scheduledTimeouts.get(e)), this._scheduledTimeouts.delete(e);
252
+ },
253
+ // func getRandomData(r []byte)
254
+ "runtime.getRandomData": (t) => {
255
+ t >>>= 0, crypto.getRandomValues(a(t + 8));
256
+ },
257
+ // func finalizeRef(v ref)
258
+ "syscall/js.finalizeRef": (t) => {
259
+ t >>>= 0;
260
+ const e = this.mem.getUint32(t + 8, !0);
261
+ if (this._goRefCounts[e]--, this._goRefCounts[e] === 0) {
262
+ const o = this._values[e];
263
+ this._values[e] = null, this._ids.delete(o), this._idPool.push(e);
264
+ }
265
+ },
266
+ // func stringVal(value string) ref
267
+ "syscall/js.stringVal": (t) => {
268
+ t >>>= 0, r(t + 24, u(t + 8));
269
+ },
270
+ // func valueGet(v ref, p string) ref
271
+ "syscall/js.valueGet": (t) => {
272
+ t >>>= 0;
273
+ const e = Reflect.get(s(t + 8), u(t + 16));
274
+ t = this._inst.exports.getsp() >>> 0, r(t + 32, e);
275
+ },
276
+ // func valueSet(v ref, p string, x ref)
277
+ "syscall/js.valueSet": (t) => {
278
+ t >>>= 0, Reflect.set(s(t + 8), u(t + 16), s(t + 32));
279
+ },
280
+ // func valueDelete(v ref, p string)
281
+ "syscall/js.valueDelete": (t) => {
282
+ t >>>= 0, Reflect.deleteProperty(s(t + 8), u(t + 16));
283
+ },
284
+ // func valueIndex(v ref, i int) ref
285
+ "syscall/js.valueIndex": (t) => {
286
+ t >>>= 0, r(t + 24, Reflect.get(s(t + 8), i(t + 16)));
287
+ },
288
+ // valueSetIndex(v ref, i int, x ref)
289
+ "syscall/js.valueSetIndex": (t) => {
290
+ t >>>= 0, Reflect.set(s(t + 8), i(t + 16), s(t + 24));
291
+ },
292
+ // func valueCall(v ref, m string, args []ref) (ref, bool)
293
+ "syscall/js.valueCall": (t) => {
294
+ t >>>= 0;
295
+ try {
296
+ const e = s(t + 8), o = Reflect.get(e, u(t + 16)), l = y(t + 32), m = Reflect.apply(o, e, l);
297
+ t = this._inst.exports.getsp() >>> 0, r(t + 56, m), this.mem.setUint8(t + 64, 1);
298
+ } catch (e) {
299
+ t = this._inst.exports.getsp() >>> 0, r(t + 56, e), this.mem.setUint8(t + 64, 0);
300
+ }
301
+ },
302
+ // func valueInvoke(v ref, args []ref) (ref, bool)
303
+ "syscall/js.valueInvoke": (t) => {
304
+ t >>>= 0;
305
+ try {
306
+ const e = s(t + 8), o = y(t + 16), l = Reflect.apply(e, void 0, o);
307
+ t = this._inst.exports.getsp() >>> 0, r(t + 40, l), this.mem.setUint8(t + 48, 1);
308
+ } catch (e) {
309
+ t = this._inst.exports.getsp() >>> 0, r(t + 40, e), this.mem.setUint8(t + 48, 0);
310
+ }
311
+ },
312
+ // func valueNew(v ref, args []ref) (ref, bool)
313
+ "syscall/js.valueNew": (t) => {
314
+ t >>>= 0;
315
+ try {
316
+ const e = s(t + 8), o = y(t + 16), l = Reflect.construct(e, o);
317
+ t = this._inst.exports.getsp() >>> 0, r(t + 40, l), this.mem.setUint8(t + 48, 1);
318
+ } catch (e) {
319
+ t = this._inst.exports.getsp() >>> 0, r(t + 40, e), this.mem.setUint8(t + 48, 0);
320
+ }
321
+ },
322
+ // func valueLength(v ref) int
323
+ "syscall/js.valueLength": (t) => {
324
+ t >>>= 0, c(t + 16, parseInt(s(t + 8).length));
325
+ },
326
+ // valuePrepareString(v ref) (ref, int)
327
+ "syscall/js.valuePrepareString": (t) => {
328
+ t >>>= 0;
329
+ const e = h.encode(String(s(t + 8)));
330
+ r(t + 16, e), c(t + 24, e.length);
331
+ },
332
+ // valueLoadString(v ref, b []byte)
333
+ "syscall/js.valueLoadString": (t) => {
334
+ t >>>= 0;
335
+ const e = s(t + 8);
336
+ a(t + 16).set(e);
337
+ },
338
+ // func valueInstanceOf(v ref, t ref) bool
339
+ "syscall/js.valueInstanceOf": (t) => {
340
+ t >>>= 0, this.mem.setUint8(t + 24, s(t + 8) instanceof s(t + 16) ? 1 : 0);
341
+ },
342
+ // func copyBytesToGo(dst []byte, src ref) (int, bool)
343
+ "syscall/js.copyBytesToGo": (t) => {
344
+ t >>>= 0;
345
+ const e = a(t + 8), o = s(t + 32);
346
+ if (!(o instanceof Uint8Array || o instanceof Uint8ClampedArray)) {
347
+ this.mem.setUint8(t + 48, 0);
348
+ return;
349
+ }
350
+ const l = o.subarray(0, e.length);
351
+ e.set(l), c(t + 40, l.length), this.mem.setUint8(t + 48, 1);
352
+ },
353
+ // func copyBytesToJS(dst ref, src []byte) (int, bool)
354
+ "syscall/js.copyBytesToJS": (t) => {
355
+ t >>>= 0;
356
+ const e = s(t + 8), o = a(t + 16);
357
+ if (!(e instanceof Uint8Array || e instanceof Uint8ClampedArray)) {
358
+ this.mem.setUint8(t + 48, 0);
359
+ return;
360
+ }
361
+ const l = o.subarray(0, e.length);
362
+ e.set(l), c(t + 40, l.length), this.mem.setUint8(t + 48, 1);
363
+ },
364
+ debug: (t) => {
365
+ console.log(t);
366
+ }
367
+ }
368
+ };
369
+ }
370
+ async run(c) {
371
+ if (!(c instanceof WebAssembly.Instance))
372
+ throw new Error("Go.run: WebAssembly.Instance expected");
373
+ this._inst = c, this.mem = new DataView(this._inst.exports.mem.buffer), this._values = [
374
+ // JS values that Go currently has references to, indexed by reference id
375
+ NaN,
376
+ 0,
377
+ null,
378
+ !0,
379
+ !1,
380
+ globalThis,
381
+ this
382
+ ], this._goRefCounts = new Array(this._values.length).fill(1 / 0), this._ids = /* @__PURE__ */ new Map([
383
+ // mapping from JS values to reference ids
384
+ [0, 1],
385
+ [null, 2],
386
+ [!0, 3],
387
+ [!1, 4],
388
+ [globalThis, 5],
389
+ [this, 6]
390
+ ]), this._idPool = [], this.exited = !1;
391
+ let i = 4096;
392
+ const s = (d) => {
393
+ const t = i, e = h.encode(d + "\0");
394
+ return new Uint8Array(this.mem.buffer, i, e.length).set(e), i += e.length, i % 8 !== 0 && (i += 8 - i % 8), t;
395
+ }, r = this.argv.length, a = [];
396
+ this.argv.forEach((d) => {
397
+ a.push(s(d));
398
+ }), a.push(0), Object.keys(this.env).sort().forEach((d) => {
399
+ a.push(s(`${d}=${this.env[d]}`));
400
+ }), a.push(0);
401
+ const u = i;
402
+ if (a.forEach((d) => {
403
+ this.mem.setUint32(i, d, !0), this.mem.setUint32(i + 4, 0, !0), i += 8;
404
+ }), i >= 12288)
405
+ throw new Error("total length of command line and environment variables exceeds limit");
406
+ this._inst.exports.run(r, u), this.exited && this._resolveExitPromise(), await this._exitPromise;
407
+ }
408
+ _resume() {
409
+ if (this.exited)
410
+ throw new Error("Go program has already exited");
411
+ this._inst.exports.resume(), this.exited && this._resolveExitPromise();
412
+ }
413
+ _makeFuncWrapper(c) {
414
+ const i = this;
415
+ return function() {
416
+ const s = { id: c, this: this, args: arguments };
417
+ return i._pendingEvent = s, i._resume(), s.result;
418
+ };
419
+ }
420
+ };
421
+ })();
422
+ const b = new URL("main.wasm", import.meta.url).href;
423
+ (() => {
424
+ const n = globalThis, h = "process";
425
+ n[h] ? n[h].pid == null && (n[h].pid = 1) : n[h] = { pid: 1 };
426
+ })();
427
+ let _ = !1;
428
+ async function x() {
429
+ if (_) return;
430
+ const n = new globalThis.Go(), h = await WebAssembly.instantiateStreaming(
431
+ fetch(b),
432
+ n.importObject
433
+ );
434
+ n.run(h.instance), _ = !0;
435
+ }
436
+ function f() {
437
+ return globalThis.__tailscaleWeb;
438
+ }
439
+ function T(n) {
440
+ return {
441
+ status: n.status,
442
+ statusText: n.statusText,
443
+ ok: n.ok,
444
+ headers: n.headers,
445
+ text: async () => new TextDecoder().decode(n.body),
446
+ json: async () => JSON.parse(new TextDecoder().decode(n.body)),
447
+ arrayBuffer: async () => n.body.buffer,
448
+ bytes: async () => n.body
449
+ };
450
+ }
451
+ const p = {
452
+ /**
453
+ * Initialize and connect the Tailscale node. Must be called before any
454
+ * other method. Resolves once the node is authenticated and ready.
455
+ *
456
+ * If the node has persisted state from a previous session it reconnects
457
+ * automatically. Otherwise the OAuth flow is triggered via onAuthRequired.
458
+ * Rejects if the auth URL does not arrive within 60 seconds, or if the
459
+ * user does not complete authentication within 5 minutes.
460
+ *
461
+ * @example
462
+ * await network.init({
463
+ * hostname: "my-app",
464
+ * onAuthRequired(url) {
465
+ * window.open(url, "_blank", "width=600,height=700")
466
+ * },
467
+ * onAuthComplete() {
468
+ * console.log("connected!")
469
+ * },
470
+ * })
471
+ *
472
+ * @example
473
+ * // Custom storage backend (e.g. sessionStorage or any key/value store)
474
+ * await network.init({
475
+ * hostname: "my-app",
476
+ * storage: {
477
+ * get: key => sessionStorage.getItem(key),
478
+ * set: (key, val) => sessionStorage.setItem(key, val),
479
+ * },
480
+ * onAuthRequired(url) { console.log("Authenticate at:", url) },
481
+ * })
482
+ */
483
+ async init(n = {}) {
484
+ return await x(), f().init(n);
485
+ },
486
+ /**
487
+ * Send an ICMP ping to addr and measure round-trip time.
488
+ * addr may be a hostname or Tailscale IP.
489
+ *
490
+ * @example
491
+ * const result = await network.ping("my-server")
492
+ * if (result.alive) {
493
+ * console.log(`rtt: ${result.rttMs.toFixed(3)} ms ip: ${result.nodeIP}`)
494
+ * } else {
495
+ * console.warn("unreachable:", result.err)
496
+ * }
497
+ */
498
+ async ping(n) {
499
+ return f().ping(n);
500
+ },
501
+ /**
502
+ * Open a raw TCP connection through the Tailscale network.
503
+ * Returns a Connection object for sending and receiving data.
504
+ *
505
+ * @example
506
+ * const conn = await network.dialTCP("my-server:8080")
507
+ *
508
+ * conn.onData(data => {
509
+ * console.log(new TextDecoder().decode(data))
510
+ * })
511
+ *
512
+ * conn.write("hello\n")
513
+ * conn.close()
514
+ */
515
+ async dialTCP(n) {
516
+ const h = await f().dialTCP(n);
517
+ return {
518
+ onData(g) {
519
+ h.onData(g);
520
+ },
521
+ write(g) {
522
+ h.write(
523
+ typeof g == "string" ? new TextEncoder().encode(g) : g
524
+ );
525
+ },
526
+ close() {
527
+ h.close();
528
+ }
529
+ };
530
+ },
531
+ /**
532
+ * Make an HTTP request through the Tailscale network. Supports method,
533
+ * headers, and body. Does not yet support AbortSignal, streaming bodies
534
+ * or responses, or other advanced Fetch API options.
535
+ *
536
+ * @example
537
+ * const resp = await network.fetch("https://internal-service/api", {
538
+ * method: "POST",
539
+ * headers: { "Content-Type": "application/json" },
540
+ * body: JSON.stringify({ key: "value" }),
541
+ * })
542
+ * console.log(resp.status, resp.ok)
543
+ * const data = await resp.json()
544
+ */
545
+ async fetch(n, h = {}) {
546
+ return T(await f().fetch(n, h));
547
+ },
548
+ /**
549
+ * Return the current preferences (acceptRoutes, exitNodeId).
550
+ * Synchronous — no await needed. Must be called after init() resolves.
551
+ *
552
+ * @example
553
+ * const { acceptRoutes, exitNodeId } = network.getPrefs()
554
+ * console.log("exit node:", exitNodeId || "(none)")
555
+ */
556
+ getPrefs() {
557
+ return f().getPrefs();
558
+ },
559
+ /**
560
+ * Enable or disable acceptance of subnet routes advertised by peers.
561
+ * Equivalent to `tailscale set --accept-routes`.
562
+ *
563
+ * @example
564
+ * await network.setAcceptRoutes(true)
565
+ */
566
+ async setAcceptRoutes(n) {
567
+ return f().setAcceptRoutes(n);
568
+ },
569
+ /**
570
+ * Return all peers that advertise exit-node capability.
571
+ * Synchronous — no await needed. Returns an empty array if called before init() resolves.
572
+ *
573
+ * @example
574
+ * const nodes = network.listExitNodes()
575
+ * for (const n of nodes) {
576
+ * console.log(n.hostName, n.tailscaleIP, n.online ? "online" : "offline")
577
+ * }
578
+ */
579
+ listExitNodes() {
580
+ return Array.from(f().listExitNodes());
581
+ },
582
+ /**
583
+ * Activate an exit node by its stable node ID.
584
+ * Pass an empty string (or omit) to clear the exit node.
585
+ *
586
+ * @example
587
+ * // Activate the first available online exit node
588
+ * const node = network.listExitNodes().find(n => n.online)
589
+ * if (node) await network.setExitNode(node.id)
590
+ *
591
+ * @example
592
+ * // Clear the active exit node
593
+ * await network.setExitNode()
594
+ */
595
+ async setExitNode(n = "") {
596
+ return f().setExitNode(n);
597
+ },
598
+ /**
599
+ * Return the full routing table (self + all peers).
600
+ * Synchronous — no await needed. Returns an empty array if called before init() resolves.
601
+ *
602
+ * @example
603
+ * const routes = network.getRoutes()
604
+ * for (const r of routes) {
605
+ * console.log(r.prefix, "via", r.via, r.isExitRoute ? "(exit)" : "")
606
+ * }
607
+ */
608
+ getRoutes() {
609
+ return Array.from(f().getRoutes());
610
+ },
611
+ /**
612
+ * Return the current Tailscale-managed DNS configuration.
613
+ * Synchronous — no await needed. Returns an empty DNSInfo object if called before init() resolves.
614
+ *
615
+ * @example
616
+ * const dns = network.getDNS()
617
+ * console.log("resolvers:", dns.resolvers)
618
+ * console.log("MagicDNS:", dns.magicDNS)
619
+ * for (const [suffix, resolvers] of Object.entries(dns.routes)) {
620
+ * console.log(`split-DNS: ${suffix} → ${resolvers.join(", ")}`)
621
+ * }
622
+ */
623
+ getDNS() {
624
+ return f().getDNS();
625
+ }
626
+ };
627
+ export {
628
+ p as network
629
+ };
@@ -0,0 +1,2 @@
1
+ (function(w,_){typeof exports=="object"&&typeof module<"u"?_(exports):typeof define=="function"&&define.amd?define(["exports"],_):(w=typeof globalThis<"u"?globalThis:w||self,_(w["tailscale-web"]={}))})(this,(function(w){"use strict";(()=>{const n=()=>{const c=new Error("not implemented");return c.code="ENOSYS",c};if(!globalThis.fs){let c="";globalThis.fs={constants:{O_WRONLY:-1,O_RDWR:-1,O_CREAT:-1,O_TRUNC:-1,O_APPEND:-1,O_EXCL:-1,O_DIRECTORY:-1},writeSync(i,s){c+=g.decode(s);const r=c.lastIndexOf(`
2
+ `);return r!=-1&&(console.log(c.substring(0,r)),c=c.substring(r+1)),s.length},write(i,s,r,a,y,h){if(r!==0||a!==s.length||y!==null){h(n());return}const b=this.writeSync(i,s);h(null,b)},chmod(i,s,r){r(n())},chown(i,s,r,a){a(n())},close(i,s){s(n())},fchmod(i,s,r){r(n())},fchown(i,s,r,a){a(n())},fstat(i,s){s(n())},fsync(i,s){s(null)},ftruncate(i,s,r){r(n())},lchown(i,s,r,a){a(n())},link(i,s,r){r(n())},lstat(i,s){s(n())},mkdir(i,s,r){r(n())},open(i,s,r,a){a(n())},read(i,s,r,a,y,h){h(n())},readdir(i,s){s(n())},readlink(i,s){s(n())},rename(i,s,r){r(n())},rmdir(i,s){s(n())},stat(i,s){s(n())},symlink(i,s,r){r(n())},truncate(i,s,r){r(n())},unlink(i,s){s(n())},utimes(i,s,r,a){a(n())}}}if(globalThis.process||(globalThis.process={getuid(){return-1},getgid(){return-1},geteuid(){return-1},getegid(){return-1},getgroups(){throw n()},pid:-1,ppid:-1,umask(){throw n()},cwd(){throw n()},chdir(){throw n()}}),globalThis.path||(globalThis.path={resolve(...c){return c.join("/")}}),!globalThis.crypto)throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");if(!globalThis.performance)throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");if(!globalThis.TextEncoder)throw new Error("globalThis.TextEncoder is not available, polyfill required");if(!globalThis.TextDecoder)throw new Error("globalThis.TextDecoder is not available, polyfill required");const u=new TextEncoder("utf-8"),g=new TextDecoder("utf-8");globalThis.Go=class{constructor(){this.argv=["js"],this.env={},this.exit=t=>{t!==0&&console.warn("exit code:",t)},this._exitPromise=new Promise(t=>{this._resolveExitPromise=t}),this._pendingEvent=null,this._scheduledTimeouts=new Map,this._nextCallbackTimeoutID=1;const c=(t,e)=>{this.mem.setUint32(t+0,e,!0),this.mem.setUint32(t+4,Math.floor(e/4294967296),!0)},i=t=>{const e=this.mem.getUint32(t+0,!0),o=this.mem.getInt32(t+4,!0);return e+o*4294967296},s=t=>{const e=this.mem.getFloat64(t,!0);if(e===0)return;if(!isNaN(e))return e;const o=this.mem.getUint32(t,!0);return this._values[o]},r=(t,e)=>{if(typeof e=="number"&&e!==0){if(isNaN(e)){this.mem.setUint32(t+4,2146959360,!0),this.mem.setUint32(t,0,!0);return}this.mem.setFloat64(t,e,!0);return}if(e===void 0){this.mem.setFloat64(t,0,!0);return}let l=this._ids.get(e);l===void 0&&(l=this._idPool.pop(),l===void 0&&(l=this._values.length),this._values[l]=e,this._goRefCounts[l]=0,this._ids.set(e,l)),this._goRefCounts[l]++;let m=0;switch(typeof e){case"object":e!==null&&(m=1);break;case"string":m=2;break;case"symbol":m=3;break;case"function":m=4;break}this.mem.setUint32(t+4,2146959360|m,!0),this.mem.setUint32(t,l,!0)},a=t=>{const e=i(t+0),o=i(t+8);return new Uint8Array(this._inst.exports.mem.buffer,e,o)},y=t=>{const e=i(t+0),o=i(t+8),l=new Array(o);for(let m=0;m<o;m++)l[m]=s(e+m*8);return l},h=t=>{const e=i(t+0),o=i(t+8);return g.decode(new DataView(this._inst.exports.mem.buffer,e,o))},b=(t,e)=>(this._inst.exports.testExport0(),this._inst.exports.testExport(t,e)),f=Date.now()-performance.now();this.importObject={_gotest:{add:(t,e)=>t+e,callExport:b},gojs:{"runtime.wasmExit":t=>{t>>>=0;const e=this.mem.getInt32(t+8,!0);this.exited=!0,delete this._inst,delete this._values,delete this._goRefCounts,delete this._ids,delete this._idPool,this.exit(e)},"runtime.wasmWrite":t=>{t>>>=0;const e=i(t+8),o=i(t+16),l=this.mem.getInt32(t+24,!0);fs.writeSync(e,new Uint8Array(this._inst.exports.mem.buffer,o,l))},"runtime.resetMemoryDataView":t=>{this.mem=new DataView(this._inst.exports.mem.buffer)},"runtime.nanotime1":t=>{t>>>=0,c(t+8,(f+performance.now())*1e6)},"runtime.walltime":t=>{t>>>=0;const e=new Date().getTime();c(t+8,e/1e3),this.mem.setInt32(t+16,e%1e3*1e6,!0)},"runtime.scheduleTimeoutEvent":t=>{t>>>=0;const e=this._nextCallbackTimeoutID;this._nextCallbackTimeoutID++,this._scheduledTimeouts.set(e,setTimeout(()=>{for(this._resume();this._scheduledTimeouts.has(e);)console.warn("scheduleTimeoutEvent: missed timeout event"),this._resume()},i(t+8))),this.mem.setInt32(t+16,e,!0)},"runtime.clearTimeoutEvent":t=>{t>>>=0;const e=this.mem.getInt32(t+8,!0);clearTimeout(this._scheduledTimeouts.get(e)),this._scheduledTimeouts.delete(e)},"runtime.getRandomData":t=>{t>>>=0,crypto.getRandomValues(a(t+8))},"syscall/js.finalizeRef":t=>{t>>>=0;const e=this.mem.getUint32(t+8,!0);if(this._goRefCounts[e]--,this._goRefCounts[e]===0){const o=this._values[e];this._values[e]=null,this._ids.delete(o),this._idPool.push(e)}},"syscall/js.stringVal":t=>{t>>>=0,r(t+24,h(t+8))},"syscall/js.valueGet":t=>{t>>>=0;const e=Reflect.get(s(t+8),h(t+16));t=this._inst.exports.getsp()>>>0,r(t+32,e)},"syscall/js.valueSet":t=>{t>>>=0,Reflect.set(s(t+8),h(t+16),s(t+32))},"syscall/js.valueDelete":t=>{t>>>=0,Reflect.deleteProperty(s(t+8),h(t+16))},"syscall/js.valueIndex":t=>{t>>>=0,r(t+24,Reflect.get(s(t+8),i(t+16)))},"syscall/js.valueSetIndex":t=>{t>>>=0,Reflect.set(s(t+8),i(t+16),s(t+24))},"syscall/js.valueCall":t=>{t>>>=0;try{const e=s(t+8),o=Reflect.get(e,h(t+16)),l=y(t+32),m=Reflect.apply(o,e,l);t=this._inst.exports.getsp()>>>0,r(t+56,m),this.mem.setUint8(t+64,1)}catch(e){t=this._inst.exports.getsp()>>>0,r(t+56,e),this.mem.setUint8(t+64,0)}},"syscall/js.valueInvoke":t=>{t>>>=0;try{const e=s(t+8),o=y(t+16),l=Reflect.apply(e,void 0,o);t=this._inst.exports.getsp()>>>0,r(t+40,l),this.mem.setUint8(t+48,1)}catch(e){t=this._inst.exports.getsp()>>>0,r(t+40,e),this.mem.setUint8(t+48,0)}},"syscall/js.valueNew":t=>{t>>>=0;try{const e=s(t+8),o=y(t+16),l=Reflect.construct(e,o);t=this._inst.exports.getsp()>>>0,r(t+40,l),this.mem.setUint8(t+48,1)}catch(e){t=this._inst.exports.getsp()>>>0,r(t+40,e),this.mem.setUint8(t+48,0)}},"syscall/js.valueLength":t=>{t>>>=0,c(t+16,parseInt(s(t+8).length))},"syscall/js.valuePrepareString":t=>{t>>>=0;const e=u.encode(String(s(t+8)));r(t+16,e),c(t+24,e.length)},"syscall/js.valueLoadString":t=>{t>>>=0;const e=s(t+8);a(t+16).set(e)},"syscall/js.valueInstanceOf":t=>{t>>>=0,this.mem.setUint8(t+24,s(t+8)instanceof s(t+16)?1:0)},"syscall/js.copyBytesToGo":t=>{t>>>=0;const e=a(t+8),o=s(t+32);if(!(o instanceof Uint8Array||o instanceof Uint8ClampedArray)){this.mem.setUint8(t+48,0);return}const l=o.subarray(0,e.length);e.set(l),c(t+40,l.length),this.mem.setUint8(t+48,1)},"syscall/js.copyBytesToJS":t=>{t>>>=0;const e=s(t+8),o=a(t+16);if(!(e instanceof Uint8Array||e instanceof Uint8ClampedArray)){this.mem.setUint8(t+48,0);return}const l=o.subarray(0,e.length);e.set(l),c(t+40,l.length),this.mem.setUint8(t+48,1)},debug:t=>{console.log(t)}}}}async run(c){if(!(c instanceof WebAssembly.Instance))throw new Error("Go.run: WebAssembly.Instance expected");this._inst=c,this.mem=new DataView(this._inst.exports.mem.buffer),this._values=[NaN,0,null,!0,!1,globalThis,this],this._goRefCounts=new Array(this._values.length).fill(1/0),this._ids=new Map([[0,1],[null,2],[!0,3],[!1,4],[globalThis,5],[this,6]]),this._idPool=[],this.exited=!1;let i=4096;const s=f=>{const t=i,e=u.encode(f+"\0");return new Uint8Array(this.mem.buffer,i,e.length).set(e),i+=e.length,i%8!==0&&(i+=8-i%8),t},r=this.argv.length,a=[];this.argv.forEach(f=>{a.push(s(f))}),a.push(0),Object.keys(this.env).sort().forEach(f=>{a.push(s(`${f}=${this.env[f]}`))}),a.push(0);const h=i;if(a.forEach(f=>{this.mem.setUint32(i,f,!0),this.mem.setUint32(i+4,0,!0),i+=8}),i>=12288)throw new Error("total length of command line and environment variables exceeds limit");this._inst.exports.run(r,h),this.exited&&this._resolveExitPromise(),await this._exitPromise}_resume(){if(this.exited)throw new Error("Go program has already exited");this._inst.exports.resume(),this.exited&&this._resolveExitPromise()}_makeFuncWrapper(c){const i=this;return function(){const s={id:c,this:this,args:arguments};return i._pendingEvent=s,i._resume(),s.result}}}})();const _=typeof document>"u"&&typeof location>"u"?require("url").pathToFileURL(__dirname+"/main.wasm").href:new URL("main.wasm",typeof document>"u"?location.href:document.currentScript&&document.currentScript.tagName.toUpperCase()==="SCRIPT"&&document.currentScript.src||document.baseURI).href;(()=>{const n=globalThis,u="process";n[u]?n[u].pid==null&&(n[u].pid=1):n[u]={pid:1}})();let x=!1;async function p(){if(x)return;const n=new globalThis.Go,u=await WebAssembly.instantiateStreaming(fetch(_),n.importObject);n.run(u.instance),x=!0}function d(){return globalThis.__tailscaleWeb}function T(n){return{status:n.status,statusText:n.statusText,ok:n.ok,headers:n.headers,text:async()=>new TextDecoder().decode(n.body),json:async()=>JSON.parse(new TextDecoder().decode(n.body)),arrayBuffer:async()=>n.body.buffer,bytes:async()=>n.body}}const v={async init(n={}){return await p(),d().init(n)},async ping(n){return d().ping(n)},async dialTCP(n){const u=await d().dialTCP(n);return{onData(g){u.onData(g)},write(g){u.write(typeof g=="string"?new TextEncoder().encode(g):g)},close(){u.close()}}},async fetch(n,u={}){return T(await d().fetch(n,u))},getPrefs(){return d().getPrefs()},async setAcceptRoutes(n){return d().setAcceptRoutes(n)},listExitNodes(){return Array.from(d().listExitNodes())},async setExitNode(n=""){return d().setExitNode(n)},getRoutes(){return Array.from(d().getRoutes())},getDNS(){return d().getDNS()}};w.network=v,Object.defineProperty(w,Symbol.toStringTag,{value:"Module"})}));
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "tailscale-web",
3
+ "version": "0.1.10",
4
+ "description": "Run a Tailscale node in JS environments via WebAssembly",
5
+ "type": "module",
6
+ "main": "./dist/tailscale-web.umd.js",
7
+ "module": "./dist/tailscale-web.es.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/tailscale-web.es.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/tailscale-web.umd.js"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build:wasm": "GOOS=js GOARCH=wasm go build -ldflags='-s -w' -o src/wasm/main.wasm",
26
+ "build": "npm run build:wasm && vite build",
27
+ "docs": "typedoc"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/adrianosela/tailscale-web.git"
32
+ },
33
+ "keywords": [
34
+ "tailscale",
35
+ "vpn",
36
+ "wasm",
37
+ "webassembly",
38
+ "networking"
39
+ ],
40
+ "author": "",
41
+ "license": "MIT",
42
+ "bugs": {
43
+ "url": "https://github.com/adrianosela/tailscale-web/issues"
44
+ },
45
+ "homepage": "https://github.com/adrianosela/tailscale-web#readme",
46
+ "devDependencies": {
47
+ "typedoc": "^0.28.17",
48
+ "typescript": "^5.9.3",
49
+ "vite": "^7.3.1",
50
+ "vite-plugin-dts": "^4.5.4"
51
+ }
52
+ }