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