tslocal 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,695 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) {
14
+ __defProp(to, key, {
15
+ get: ((k) => from[k]).bind(null, key),
16
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
17
+ });
18
+ }
19
+ }
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
24
+ value: mod,
25
+ enumerable: true
26
+ }) : target, mod));
27
+
28
+ //#endregion
29
+ let node_http = require("node:http");
30
+ node_http = __toESM(node_http);
31
+ let node_child_process = require("node:child_process");
32
+ let node_fs_promises = require("node:fs/promises");
33
+ let node_os = require("node:os");
34
+ let node_path = require("node:path");
35
+ let node_util = require("node:util");
36
+ let zod = require("zod");
37
+
38
+ //#region ts/src/json.ts
39
+ /** JSON reviver that converts large integers to BigInt to avoid precision loss. */
40
+ const jsonReviver = (_key, value, context) => {
41
+ if (typeof value === "number" && context?.source && !Number.isSafeInteger(value) && /^-?\d+$/.test(context.source)) return BigInt(context.source);
42
+ return value;
43
+ };
44
+ /** Parse a JSON string using {@link jsonReviver} for BigInt-safe integer handling. */
45
+ const parseJSON = (str) => JSON.parse(str, jsonReviver);
46
+ /** JSON replacer that serializes BigInt values as raw JSON numbers. */
47
+ const jsonReplacer = (_key, value) => typeof value === "bigint" ? JSON.rawJSON(value.toString()) : value;
48
+
49
+ //#endregion
50
+ //#region ts/src/errors.ts
51
+ /** Base error for all Tailscale Local API errors. */
52
+ var TailscaleError = class extends Error {
53
+ constructor(message) {
54
+ super(message);
55
+ this.name = "TailscaleError";
56
+ }
57
+ };
58
+ /** Raised when the server returns HTTP 403. */
59
+ var AccessDeniedError = class extends TailscaleError {
60
+ constructor(message) {
61
+ super(`Access denied: ${message}`);
62
+ this.name = "AccessDeniedError";
63
+ }
64
+ };
65
+ /** Raised when the server returns HTTP 412. */
66
+ var PreconditionsFailedError = class extends TailscaleError {
67
+ constructor(message) {
68
+ super(`Preconditions failed: ${message}`);
69
+ this.name = "PreconditionsFailedError";
70
+ }
71
+ };
72
+ /** Raised when a WhoIs lookup returns HTTP 404. */
73
+ var PeerNotFoundError = class extends TailscaleError {
74
+ constructor(message) {
75
+ super(`Peer not found: ${message}`);
76
+ this.name = "PeerNotFoundError";
77
+ }
78
+ };
79
+ /** Raised when the connection to tailscaled fails. */
80
+ var ConnectionError = class extends TailscaleError {
81
+ constructor(message) {
82
+ super(message);
83
+ this.name = "ConnectionError";
84
+ }
85
+ };
86
+ /** Raised when tailscaled is not running. */
87
+ var DaemonNotRunningError = class extends ConnectionError {
88
+ constructor(message) {
89
+ super(message);
90
+ this.name = "DaemonNotRunningError";
91
+ }
92
+ };
93
+ /** Raised for unexpected HTTP status codes. */
94
+ var HttpError = class extends TailscaleError {
95
+ status;
96
+ constructor(status, message) {
97
+ super(`HTTP ${status}: ${message}`);
98
+ this.name = "HttpError";
99
+ this.status = status;
100
+ }
101
+ };
102
+ /** Extract error message from a JSON body like Go's errorMessageFromBody. */
103
+ function errorMessageFromBody(body) {
104
+ try {
105
+ return JSON.parse(body)?.error;
106
+ } catch {
107
+ return;
108
+ }
109
+ }
110
+
111
+ //#endregion
112
+ //#region ts/src/safesocket.ts
113
+ const LOCAL_API_HOST = "local-tailscaled.sock";
114
+ const CURRENT_CAP_VERSION = 131;
115
+ /** Return the default socket path for the current platform. */
116
+ function defaultSocketPath() {
117
+ if ((0, node_os.platform)() === "darwin") return "/var/run/tailscaled.socket";
118
+ return "/var/run/tailscale/tailscaled.sock";
119
+ }
120
+ /** Attempt to discover macOS TCP port and token for tailscaled. */
121
+ async function localTcpPortAndToken() {
122
+ if ((0, node_os.platform)() !== "darwin") return;
123
+ const result = await readMacosSameUserProof();
124
+ if (result) return result;
125
+ return readMacsysSameUserProof();
126
+ }
127
+ const execFileP = (0, node_util.promisify)(node_child_process.execFile);
128
+ async function readMacosSameUserProof() {
129
+ try {
130
+ const uid = process.getuid?.();
131
+ if (uid === void 0) return void 0;
132
+ const { stdout: output } = await execFileP("lsof", [
133
+ "-n",
134
+ "-a",
135
+ `-u${uid}`,
136
+ "-c",
137
+ "IPNExtension",
138
+ "-F"
139
+ ]);
140
+ return parseLsofOutput(output);
141
+ } catch {
142
+ return;
143
+ }
144
+ }
145
+ /** Parse lsof -F output looking for sameuserproof-PORT-TOKEN. */
146
+ function parseLsofOutput(output) {
147
+ const needle = ".tailscale.ipn.macos/sameuserproof-";
148
+ for (const line of output.split("\n")) {
149
+ const idx = line.indexOf(needle);
150
+ if (idx === -1) continue;
151
+ const rest = line.slice(idx + 35);
152
+ const dash = rest.indexOf("-");
153
+ if (dash === -1) continue;
154
+ const portStr = rest.slice(0, dash);
155
+ const token = rest.slice(dash + 1);
156
+ const port = parseInt(portStr, 10);
157
+ if (!isNaN(port)) return {
158
+ port,
159
+ token
160
+ };
161
+ }
162
+ }
163
+ async function readMacsysSameUserProof(sharedDir = "/Library/Tailscale") {
164
+ try {
165
+ const portStr = await (0, node_fs_promises.readlink)((0, node_path.join)(sharedDir, "ipnport"), "utf-8");
166
+ const port = parseInt(portStr, 10);
167
+ if (isNaN(port)) return void 0;
168
+ return {
169
+ port,
170
+ token: (await (0, node_fs_promises.readFile)((0, node_path.join)(sharedDir, `sameuserproof-${port}`), "utf-8")).trim()
171
+ };
172
+ } catch {
173
+ return;
174
+ }
175
+ }
176
+
177
+ //#endregion
178
+ //#region ts/src/transport.ts
179
+ /**
180
+ * Discover TCP port and token for this request.
181
+ */
182
+ async function resolvePortAndToken(useSocketOnly) {
183
+ if (useSocketOnly) return void 0;
184
+ return localTcpPortAndToken();
185
+ }
186
+ /**
187
+ * HTTP transport that connects to tailscaled.
188
+ * Reuses connections via Node.js http.Agent keep-alive.
189
+ * Port and token are discovered per-request (matching Go's behavior),
190
+ * so the client adapts to daemon restarts and late starts.
191
+ */
192
+ var Transport = class {
193
+ socketPath;
194
+ useSocketOnly;
195
+ agent;
196
+ constructor(opts = {}) {
197
+ this.socketPath = opts.socketPath ?? defaultSocketPath();
198
+ this.useSocketOnly = opts.useSocketOnly ?? false;
199
+ this.agent = new node_http.Agent({
200
+ keepAlive: true,
201
+ keepAliveMsecs: 6e4
202
+ });
203
+ }
204
+ async request(method, path, body, extraHeaders) {
205
+ const portAndToken = await resolvePortAndToken(this.useSocketOnly);
206
+ return new Promise((resolve, reject) => {
207
+ const headers = {
208
+ Host: LOCAL_API_HOST,
209
+ "Tailscale-Cap": String(CURRENT_CAP_VERSION),
210
+ ...extraHeaders
211
+ };
212
+ if (portAndToken) headers["Authorization"] = `Basic ${Buffer.from(`:${portAndToken.token}`).toString("base64")}`;
213
+ const options = {
214
+ method,
215
+ path,
216
+ headers,
217
+ agent: this.agent
218
+ };
219
+ if (portAndToken) {
220
+ options.host = "127.0.0.1";
221
+ options.port = portAndToken.port;
222
+ } else options.socketPath = this.socketPath;
223
+ const req = node_http.request(options, (res) => {
224
+ const chunks = [];
225
+ res.on("data", (chunk) => chunks.push(chunk));
226
+ res.on("end", () => {
227
+ resolve({
228
+ status: res.statusCode ?? 0,
229
+ body: Buffer.concat(chunks),
230
+ headers: res.headers
231
+ });
232
+ });
233
+ res.on("error", reject);
234
+ });
235
+ req.on("error", reject);
236
+ if (body !== void 0) req.write(body);
237
+ req.end();
238
+ });
239
+ }
240
+ destroy() {
241
+ this.agent.destroy();
242
+ }
243
+ };
244
+
245
+ //#endregion
246
+ //#region ts/src/types.ts
247
+ const goSlice = (item) => zod.z.array(item).nullish().transform((v) => v ?? []);
248
+ const goMap = (val) => zod.z.record(zod.z.string(), val).nullish().transform((v) => v ?? {});
249
+ const int64 = zod.z.union([zod.z.number().int(), zod.z.bigint()]).transform((v) => BigInt(v));
250
+ /**
251
+ * Location represents geographical location data about a
252
+ * Tailscale host. Location is optional and only set if
253
+ * explicitly declared by a node.
254
+ */
255
+ const LocationSchema = zod.z.object({
256
+ Country: zod.z.string().nullish(),
257
+ CountryCode: zod.z.string().nullish(),
258
+ City: zod.z.string().nullish(),
259
+ CityCode: zod.z.string().nullish(),
260
+ Latitude: zod.z.number().nullish(),
261
+ Longitude: zod.z.number().nullish(),
262
+ Priority: int64.nullish()
263
+ });
264
+ /**
265
+ * PeerStatus describes a peer node and its current state.
266
+ * WARNING: The fields in PeerStatus are merged by the AddPeer method in the StatusBuilder.
267
+ * When adding a new field to PeerStatus, you must update AddPeer to handle merging
268
+ * the new field. The AddPeer function is responsible for combining multiple updates
269
+ * to the same peer, and any new field that is not merged properly may lead to
270
+ * inconsistencies or lost data in the peer status.
271
+ */
272
+ const PeerStatusSchema = zod.z.object({
273
+ ID: zod.z.string().default(""),
274
+ PublicKey: zod.z.string().default(""),
275
+ HostName: zod.z.string().default(""),
276
+ DNSName: zod.z.string().default(""),
277
+ OS: zod.z.string().default(""),
278
+ UserID: int64.default(0n),
279
+ AltSharerUserID: int64.nullish(),
280
+ TailscaleIPs: goSlice(zod.z.string()),
281
+ AllowedIPs: goSlice(zod.z.string()),
282
+ Tags: goSlice(zod.z.string()),
283
+ PrimaryRoutes: goSlice(zod.z.string()),
284
+ Addrs: goSlice(zod.z.string()),
285
+ CurAddr: zod.z.string().default(""),
286
+ Relay: zod.z.string().default(""),
287
+ PeerRelay: zod.z.string().default(""),
288
+ RxBytes: int64.default(0n),
289
+ TxBytes: int64.default(0n),
290
+ Created: zod.z.string().default(""),
291
+ LastWrite: zod.z.string().default(""),
292
+ LastSeen: zod.z.string().default(""),
293
+ LastHandshake: zod.z.string().default(""),
294
+ Online: zod.z.boolean().default(false),
295
+ ExitNode: zod.z.boolean().default(false),
296
+ ExitNodeOption: zod.z.boolean().default(false),
297
+ Active: zod.z.boolean().default(false),
298
+ PeerAPIURL: goSlice(zod.z.string()),
299
+ TaildropTarget: int64.default(0n),
300
+ NoFileSharingReason: zod.z.string().default(""),
301
+ Capabilities: goSlice(zod.z.string()),
302
+ CapMap: goMap(goSlice(zod.z.unknown())),
303
+ sshHostKeys: goSlice(zod.z.string()),
304
+ ShareeNode: zod.z.boolean().nullish(),
305
+ InNetworkMap: zod.z.boolean().default(false),
306
+ InMagicSock: zod.z.boolean().default(false),
307
+ InEngine: zod.z.boolean().default(false),
308
+ Expired: zod.z.boolean().nullish(),
309
+ KeyExpiry: zod.z.string().nullish(),
310
+ Location: LocationSchema.nullish()
311
+ });
312
+ /** ExitNodeStatus describes the current exit node. */
313
+ const ExitNodeStatusSchema = zod.z.object({
314
+ ID: zod.z.string().default(""),
315
+ Online: zod.z.boolean().default(false),
316
+ TailscaleIPs: goSlice(zod.z.string())
317
+ });
318
+ /** TailnetStatus is information about a Tailscale network ("tailnet"). */
319
+ const TailnetStatusSchema = zod.z.object({
320
+ Name: zod.z.string().default(""),
321
+ MagicDNSSuffix: zod.z.string().default(""),
322
+ MagicDNSEnabled: zod.z.boolean().default(false)
323
+ });
324
+ /**
325
+ * A UserProfile is display-friendly data for a [User].
326
+ * It includes the LoginName for display purposes but *not* the Provider.
327
+ * It also includes derived data from one of the user's logins.
328
+ */
329
+ const UserProfileSchema = zod.z.object({
330
+ ID: int64.default(0n),
331
+ LoginName: zod.z.string().default(""),
332
+ DisplayName: zod.z.string().default(""),
333
+ ProfilePicURL: zod.z.string().nullish()
334
+ });
335
+ /**
336
+ * ClientVersion is information about the latest client version that's available
337
+ * for the client (and whether they're already running it).
338
+ *
339
+ * It does not include a URL to download the client, as that varies by platform.
340
+ */
341
+ const ClientVersionSchema = zod.z.object({
342
+ RunningLatest: zod.z.boolean().nullish(),
343
+ LatestVersion: zod.z.string().nullish(),
344
+ UrgentSecurityUpdate: zod.z.boolean().nullish(),
345
+ Notify: zod.z.boolean().nullish(),
346
+ NotifyURL: zod.z.string().nullish(),
347
+ NotifyText: zod.z.string().nullish()
348
+ });
349
+ /** Status represents the entire state of the IPN network. */
350
+ const StatusSchema = zod.z.object({
351
+ Version: zod.z.string().default(""),
352
+ TUN: zod.z.boolean().default(false),
353
+ BackendState: zod.z.string().default(""),
354
+ HaveNodeKey: zod.z.boolean().nullish(),
355
+ AuthURL: zod.z.string().default(""),
356
+ TailscaleIPs: goSlice(zod.z.string()),
357
+ Self: PeerStatusSchema.nullish(),
358
+ ExitNodeStatus: ExitNodeStatusSchema.nullish(),
359
+ Health: goSlice(zod.z.string()),
360
+ MagicDNSSuffix: zod.z.string().default(""),
361
+ CurrentTailnet: TailnetStatusSchema.nullish(),
362
+ CertDomains: goSlice(zod.z.string()),
363
+ Peer: goMap(PeerStatusSchema),
364
+ User: goMap(UserProfileSchema),
365
+ ClientVersion: ClientVersionSchema.nullish()
366
+ });
367
+ /** Service represents a service running on a node. */
368
+ const ServiceSchema = zod.z.object({
369
+ Proto: zod.z.string().default(""),
370
+ Port: zod.z.number().default(0),
371
+ Description: zod.z.string().nullish()
372
+ });
373
+ /** NetInfo contains information about the host's network state. */
374
+ const NetInfoSchema = zod.z.object({
375
+ MappingVariesByDestIP: zod.z.boolean().nullish(),
376
+ WorkingIPv6: zod.z.boolean().nullish(),
377
+ OSHasIPv6: zod.z.boolean().nullish(),
378
+ WorkingUDP: zod.z.boolean().nullish(),
379
+ WorkingICMPv4: zod.z.boolean().nullish(),
380
+ HavePortMap: zod.z.boolean().nullish(),
381
+ UPnP: zod.z.boolean().nullish(),
382
+ PMP: zod.z.boolean().nullish(),
383
+ PCP: zod.z.boolean().nullish(),
384
+ PreferredDERP: int64.nullish(),
385
+ LinkType: zod.z.string().nullish(),
386
+ DERPLatency: goMap(zod.z.number()),
387
+ FirewallMode: zod.z.string().nullish()
388
+ });
389
+ /**
390
+ * TPMInfo contains information about a TPM 2.0 device present on a node.
391
+ * All fields are read from TPM_CAP_TPM_PROPERTIES, see Part 2, section 6.13 of
392
+ * https://trustedcomputinggroup.org/resource/tpm-library-specification/.
393
+ */
394
+ const TPMInfoSchema = zod.z.object({
395
+ Manufacturer: zod.z.string().nullish(),
396
+ Vendor: zod.z.string().nullish(),
397
+ Model: int64.nullish(),
398
+ FirmwareVersion: int64.nullish(),
399
+ SpecRevision: int64.nullish(),
400
+ FamilyIndicator: zod.z.string().nullish()
401
+ });
402
+ /**
403
+ * Hostinfo contains a summary of a Tailscale host.
404
+ *
405
+ * Because it contains pointers (slices), this type should not be used
406
+ * as a value type.
407
+ */
408
+ const HostinfoSchema = zod.z.object({
409
+ IPNVersion: zod.z.string().nullish(),
410
+ FrontendLogID: zod.z.string().nullish(),
411
+ BackendLogID: zod.z.string().nullish(),
412
+ OS: zod.z.string().nullish(),
413
+ OSVersion: zod.z.string().nullish(),
414
+ Container: zod.z.boolean().nullish(),
415
+ Env: zod.z.string().nullish(),
416
+ Distro: zod.z.string().nullish(),
417
+ DistroVersion: zod.z.string().nullish(),
418
+ DistroCodeName: zod.z.string().nullish(),
419
+ App: zod.z.string().nullish(),
420
+ Desktop: zod.z.boolean().nullish(),
421
+ Package: zod.z.string().nullish(),
422
+ DeviceModel: zod.z.string().nullish(),
423
+ PushDeviceToken: zod.z.string().nullish(),
424
+ Hostname: zod.z.string().nullish(),
425
+ ShieldsUp: zod.z.boolean().nullish(),
426
+ ShareeNode: zod.z.boolean().nullish(),
427
+ NoLogsNoSupport: zod.z.boolean().nullish(),
428
+ WireIngress: zod.z.boolean().nullish(),
429
+ IngressEnabled: zod.z.boolean().nullish(),
430
+ AllowsUpdate: zod.z.boolean().nullish(),
431
+ Machine: zod.z.string().nullish(),
432
+ GoArch: zod.z.string().nullish(),
433
+ GoArchVar: zod.z.string().nullish(),
434
+ GoVersion: zod.z.string().nullish(),
435
+ RoutableIPs: goSlice(zod.z.string()),
436
+ RequestTags: goSlice(zod.z.string()),
437
+ WoLMACs: goSlice(zod.z.string()),
438
+ Services: goSlice(ServiceSchema),
439
+ NetInfo: NetInfoSchema.nullish(),
440
+ sshHostKeys: goSlice(zod.z.string()),
441
+ Cloud: zod.z.string().nullish(),
442
+ Userspace: zod.z.boolean().nullish(),
443
+ UserspaceRouter: zod.z.boolean().nullish(),
444
+ AppConnector: zod.z.boolean().nullish(),
445
+ ServicesHash: zod.z.string().nullish(),
446
+ ExitNodeID: zod.z.string().nullish(),
447
+ Location: LocationSchema.nullish(),
448
+ TPM: TPMInfoSchema.nullish(),
449
+ StateEncrypted: zod.z.boolean().nullish()
450
+ });
451
+ /** Resolver is the configuration for one DNS resolver. */
452
+ const ResolverSchema = zod.z.object({
453
+ Addr: zod.z.string().nullish(),
454
+ BootstrapResolution: goSlice(zod.z.string()),
455
+ UseWithExitNode: zod.z.boolean().nullish()
456
+ });
457
+ /** Node is a Tailscale device in a tailnet. */
458
+ const NodeSchema = zod.z.object({
459
+ ID: int64.default(0n),
460
+ StableID: zod.z.string().default(""),
461
+ Name: zod.z.string().default(""),
462
+ User: int64.default(0n),
463
+ Sharer: int64.nullish(),
464
+ Key: zod.z.string().default(""),
465
+ KeyExpiry: zod.z.string().nullish(),
466
+ KeySignature: zod.z.string().nullish(),
467
+ Machine: zod.z.string().nullish(),
468
+ DiscoKey: zod.z.string().nullish(),
469
+ Addresses: goSlice(zod.z.string()),
470
+ AllowedIPs: goSlice(zod.z.string()),
471
+ Endpoints: goSlice(zod.z.string()),
472
+ DERP: zod.z.string().nullish(),
473
+ HomeDERP: int64.nullish(),
474
+ Hostinfo: HostinfoSchema.nullish(),
475
+ Created: zod.z.string().nullish(),
476
+ Cap: int64.nullish(),
477
+ Tags: goSlice(zod.z.string()),
478
+ PrimaryRoutes: goSlice(zod.z.string()),
479
+ LastSeen: zod.z.string().nullish(),
480
+ Online: zod.z.boolean().nullish(),
481
+ MachineAuthorized: zod.z.boolean().nullish(),
482
+ Capabilities: goSlice(zod.z.string()),
483
+ CapMap: goMap(goSlice(zod.z.unknown())),
484
+ UnsignedPeerAPIOnly: zod.z.boolean().nullish(),
485
+ ComputedName: zod.z.string().nullish(),
486
+ ComputedNameWithHost: zod.z.string().nullish(),
487
+ DataPlaneAuditLogID: zod.z.string().nullish(),
488
+ Expired: zod.z.boolean().nullish(),
489
+ SelfNodeV4MasqAddrForThisPeer: zod.z.string().nullish(),
490
+ SelfNodeV6MasqAddrForThisPeer: zod.z.string().nullish(),
491
+ IsWireGuardOnly: zod.z.boolean().nullish(),
492
+ IsJailed: zod.z.boolean().nullish(),
493
+ ExitNodeDNSResolvers: goSlice(ResolverSchema)
494
+ });
495
+ /**
496
+ * TCPPortHandler describes what to do when handling a TCP
497
+ * connection.
498
+ */
499
+ const TCPPortHandlerSchema = zod.z.object({
500
+ HTTPS: zod.z.boolean().nullish(),
501
+ HTTP: zod.z.boolean().nullish(),
502
+ TCPForward: zod.z.string().nullish(),
503
+ TerminateTLS: zod.z.string().nullish(),
504
+ ProxyProtocol: int64.nullish()
505
+ });
506
+ /** HTTPHandler is either a path or a proxy to serve. */
507
+ const HTTPHandlerSchema = zod.z.object({
508
+ Path: zod.z.string().nullish(),
509
+ Proxy: zod.z.string().nullish(),
510
+ Text: zod.z.string().nullish(),
511
+ AcceptAppCaps: goSlice(zod.z.string()),
512
+ Redirect: zod.z.string().nullish()
513
+ });
514
+ /** WebServerConfig describes a web server's configuration. */
515
+ const WebServerConfigSchema = zod.z.object({ Handlers: goMap(HTTPHandlerSchema) });
516
+ /**
517
+ * ServiceConfig contains the config information for a single service.
518
+ * it contains a bool to indicate if the service is in Tun mode (L3 forwarding).
519
+ * If the service is not in Tun mode, the service is configured by the L4 forwarding
520
+ * (TCP ports) and/or the L7 forwarding (http handlers) information.
521
+ */
522
+ const ServiceConfigSchema = zod.z.object({
523
+ TCP: goMap(TCPPortHandlerSchema),
524
+ Web: goMap(WebServerConfigSchema),
525
+ Tun: zod.z.boolean().nullish()
526
+ });
527
+ const _ServeConfigRef = zod.z.lazy(() => ServeConfigSchema);
528
+ /**
529
+ * ServeConfig is the JSON type stored in the StateStore for
530
+ * StateKey "_serve/$PROFILE_ID" as returned by ServeConfigKey.
531
+ */
532
+ const ServeConfigSchema = zod.z.object({
533
+ TCP: goMap(TCPPortHandlerSchema),
534
+ Web: goMap(WebServerConfigSchema),
535
+ Services: goMap(ServiceConfigSchema),
536
+ AllowFunnel: goMap(zod.z.boolean()),
537
+ Foreground: goMap(_ServeConfigRef)
538
+ });
539
+ /**
540
+ * WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
541
+ * In successful whois responses, Node and UserProfile are never nil.
542
+ */
543
+ const WhoIsResponseSchema = zod.z.object({
544
+ Node: NodeSchema.nullish(),
545
+ UserProfile: UserProfileSchema.nullish(),
546
+ CapMap: goMap(goSlice(zod.z.unknown()))
547
+ });
548
+ /** Alias for TailnetStatus for backward compatibility. */
549
+ const CurrentTailnetSchema = zod.z.object({
550
+ Name: zod.z.string(),
551
+ MagicDNSSuffix: zod.z.string(),
552
+ MagicDNSEnabled: zod.z.boolean()
553
+ });
554
+
555
+ //#endregion
556
+ //#region ts/src/client.ts
557
+ /**
558
+ * Client for the Tailscale Local API.
559
+ *
560
+ * Connections are reused via HTTP keep-alive.
561
+ */
562
+ var LocalClient = class {
563
+ transport;
564
+ constructor(opts = {}) {
565
+ this.transport = new Transport(opts);
566
+ }
567
+ async doRequest(method, path, body, headers) {
568
+ try {
569
+ return await this.transport.request(method, path, body, headers);
570
+ } catch (err) {
571
+ const msg = err instanceof Error ? err.message : String(err);
572
+ if (msg.includes("ECONNREFUSED") || msg.includes("ENOENT")) throw new DaemonNotRunningError(msg);
573
+ throw new ConnectionError(msg);
574
+ }
575
+ }
576
+ async doRequestNice(method, path, body, headers) {
577
+ const resp = await this.doRequest(method, path, body, headers);
578
+ if (resp.status >= 200 && resp.status < 300) return resp.body;
579
+ const bodyStr = resp.body.toString("utf-8");
580
+ const msg = errorMessageFromBody(bodyStr) ?? bodyStr;
581
+ if (resp.status === 403) throw new AccessDeniedError(msg);
582
+ if (resp.status === 412) throw new PreconditionsFailedError(msg);
583
+ throw new HttpError(resp.status, msg);
584
+ }
585
+ async get200(path) {
586
+ return this.doRequestNice("GET", path);
587
+ }
588
+ async post200(path, body) {
589
+ return this.doRequestNice("POST", path, body);
590
+ }
591
+ /** Get the current tailscaled status. */
592
+ async status() {
593
+ const data = await this.get200("/localapi/v0/status");
594
+ return StatusSchema.parse(parseJSON(data.toString("utf-8")));
595
+ }
596
+ /** Get the current tailscaled status without peer information. */
597
+ async statusWithoutPeers() {
598
+ const data = await this.get200("/localapi/v0/status?peers=false");
599
+ return StatusSchema.parse(parseJSON(data.toString("utf-8")));
600
+ }
601
+ async doWhoIs(params) {
602
+ const resp = await this.doRequest("GET", `/localapi/v0/whois?${params}`);
603
+ if (resp.status === 404) throw new PeerNotFoundError(params);
604
+ if (resp.status !== 200) {
605
+ const bodyStr = resp.body.toString("utf-8");
606
+ const msg = errorMessageFromBody(bodyStr) ?? bodyStr;
607
+ if (resp.status === 403) throw new AccessDeniedError(msg);
608
+ throw new HttpError(resp.status, msg);
609
+ }
610
+ return WhoIsResponseSchema.parse(parseJSON(resp.body.toString("utf-8")));
611
+ }
612
+ /** Look up the owner of an IP address or IP:port. */
613
+ async whoIs(remoteAddr) {
614
+ return this.doWhoIs(`addr=${encodeURIComponent(remoteAddr)}`);
615
+ }
616
+ /** Look up a peer by node key. */
617
+ async whoIsNodeKey(nodeKey) {
618
+ return this.whoIs(nodeKey);
619
+ }
620
+ /** Look up the owner of an IP address with a specific protocol ("tcp" or "udp"). */
621
+ async whoIsProto(proto, remoteAddr) {
622
+ return this.doWhoIs(`proto=${encodeURIComponent(proto)}&addr=${encodeURIComponent(remoteAddr)}`);
623
+ }
624
+ /** Get a TLS certificate and private key for the given domain. */
625
+ async certPair(domain) {
626
+ return this.certPairWithValidity(domain, 0);
627
+ }
628
+ /** Get a TLS certificate with minimum validity duration (in seconds). */
629
+ async certPairWithValidity(domain, minValiditySecs) {
630
+ const body = await this.get200(`/localapi/v0/cert/${encodeURIComponent(domain)}?type=pair&min_validity=${minValiditySecs}s`);
631
+ const delimiter = Buffer.from("--\n--");
632
+ const pos = body.indexOf(delimiter);
633
+ if (pos === -1) throw new Error("unexpected cert response: no delimiter");
634
+ const split = pos + 3;
635
+ const key = body.subarray(0, split);
636
+ return {
637
+ cert: body.subarray(split),
638
+ key
639
+ };
640
+ }
641
+ /**
642
+ * Get the current serve configuration.
643
+ *
644
+ * The returned ServeConfig has its ETag field populated from the
645
+ * HTTP Etag response header.
646
+ */
647
+ async getServeConfig() {
648
+ const resp = await this.doRequest("GET", "/localapi/v0/serve-config");
649
+ if (resp.status !== 200) {
650
+ const bodyStr = resp.body.toString("utf-8");
651
+ const msg = errorMessageFromBody(bodyStr) ?? bodyStr;
652
+ if (resp.status === 403) throw new AccessDeniedError(msg);
653
+ if (resp.status === 412) throw new PreconditionsFailedError(msg);
654
+ throw new HttpError(resp.status, msg);
655
+ }
656
+ const config = ServeConfigSchema.parse(parseJSON(resp.body.toString("utf-8")));
657
+ config.ETag = resp.headers["etag"] ?? "";
658
+ return config;
659
+ }
660
+ /**
661
+ * Set the serve configuration.
662
+ *
663
+ * The ETag field on the config is sent as the If-Match header
664
+ * for conditional updates.
665
+ */
666
+ async setServeConfig(config) {
667
+ const headers = {};
668
+ if (config.ETag) headers["If-Match"] = config.ETag;
669
+ const body = JSON.stringify(config, jsonReplacer);
670
+ await this.doRequestNice("POST", "/localapi/v0/serve-config", body, headers);
671
+ }
672
+ /** Close the underlying transport and release resources. */
673
+ destroy() {
674
+ this.transport.destroy();
675
+ }
676
+ };
677
+
678
+ //#endregion
679
+ exports.AccessDeniedError = AccessDeniedError;
680
+ exports.ClientVersionSchema = ClientVersionSchema;
681
+ exports.ConnectionError = ConnectionError;
682
+ exports.CurrentTailnetSchema = CurrentTailnetSchema;
683
+ exports.DaemonNotRunningError = DaemonNotRunningError;
684
+ exports.HttpError = HttpError;
685
+ exports.LocalClient = LocalClient;
686
+ exports.PeerNotFoundError = PeerNotFoundError;
687
+ exports.PeerStatusSchema = PeerStatusSchema;
688
+ exports.PreconditionsFailedError = PreconditionsFailedError;
689
+ exports.ServeConfigSchema = ServeConfigSchema;
690
+ exports.StatusSchema = StatusSchema;
691
+ exports.TailnetStatusSchema = TailnetStatusSchema;
692
+ exports.TailscaleError = TailscaleError;
693
+ exports.UserProfileSchema = UserProfileSchema;
694
+ exports.WhoIsResponseSchema = WhoIsResponseSchema;
695
+ //# sourceMappingURL=index.cjs.map