pinokiod 8.2.10 → 8.2.11

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.
@@ -1,147 +0,0 @@
1
- /*
2
- Minimal client for the tailscaled LocalAPI.
3
-
4
- tailscaled exposes an HTTP API over a unix socket (mac/linux) or a named
5
- pipe (Windows). We talk to it with plain node `http` by passing the socket
6
- path as `socketPath`; node's net layer handles both unix sockets and
7
- `\\.\pipe\...` names. The conda-forge `tailscaled` package ships only the
8
- daemon (no `tailscale` CLI), so every operation goes through this client.
9
-
10
- Verified against tailscaled 1.104.1 (see spec/requirements/pinokio-anywhere.md):
11
- GET /localapi/v0/status -> ipnstate.Status JSON
12
- GET /localapi/v0/prefs -> ipn.Prefs JSON
13
- PATCH /localapi/v0/prefs <- ipn.MaskedPrefs ({HostnameSet:true, Hostname:"pinokio", ...})
14
- POST /localapi/v0/login-interactive -> 204; AuthURL shows up in /status and on the IPN bus (BrowseToURL)
15
- POST /localapi/v0/logout -> 204
16
- GET /localapi/v0/serve-config -> ipn.ServeConfig JSON (or null) + Etag header
17
- POST /localapi/v0/serve-config <- ipn.ServeConfig JSON, optional If-Match: <etag>
18
- POST /localapi/v0/query-feature?feature=serve -> {Complete, Text, URL} (URL = one-click tailnet approval)
19
- */
20
- const http = require('http')
21
-
22
- class LocalAPIError extends Error {
23
- constructor(message, status, body) {
24
- super(message)
25
- this.name = 'LocalAPIError'
26
- this.status = status
27
- this.body = body
28
- }
29
- }
30
-
31
- class LocalAPI {
32
- constructor({ socketPath, timeout = 10000 } = {}) {
33
- if (!socketPath) {
34
- throw new Error('LocalAPI requires a socketPath')
35
- }
36
- this.socketPath = socketPath
37
- this.timeout = timeout
38
- this.base = '/localapi/v0'
39
- }
40
- request(method, route, { body, headers, timeout } = {}) {
41
- const payload = typeof body === 'undefined' || body === null
42
- ? null
43
- : (typeof body === 'string' || Buffer.isBuffer(body) ? body : JSON.stringify(body))
44
- const reqHeaders = Object.assign({
45
- // tailscaled rejects non-loopback origins without this on some
46
- // transports (the Windows GUI loopback port); harmless on sockets.
47
- 'Sec-Tailscale': 'localapi',
48
- 'Host': 'local-tailscaled.sock',
49
- }, headers || {})
50
- if (payload !== null) {
51
- reqHeaders['Content-Type'] = reqHeaders['Content-Type'] || 'application/json'
52
- reqHeaders['Content-Length'] = Buffer.byteLength(payload)
53
- }
54
- return new Promise((resolve, reject) => {
55
- const req = http.request({
56
- socketPath: this.socketPath,
57
- method,
58
- path: this.base + route,
59
- headers: reqHeaders,
60
- }, (res) => {
61
- const chunks = []
62
- res.on('data', (chunk) => chunks.push(chunk))
63
- res.on('end', () => {
64
- const text = Buffer.concat(chunks).toString('utf8')
65
- resolve({
66
- status: res.statusCode,
67
- headers: res.headers,
68
- text,
69
- json: () => {
70
- if (!text || !text.trim()) return null
71
- return JSON.parse(text)
72
- },
73
- })
74
- })
75
- res.on('error', reject)
76
- })
77
- req.setTimeout(timeout || this.timeout, () => {
78
- req.destroy(new Error(`LocalAPI ${method} ${route} timed out`))
79
- })
80
- req.on('error', reject)
81
- if (payload !== null) {
82
- req.write(payload)
83
- }
84
- req.end()
85
- })
86
- }
87
- async expect(method, route, options, okStatuses = [200, 204]) {
88
- const res = await this.request(method, route, options)
89
- if (!okStatuses.includes(res.status)) {
90
- throw new LocalAPIError(`LocalAPI ${method} ${route} failed: ${res.status} ${res.text.trim()}`, res.status, res.text)
91
- }
92
- return res
93
- }
94
- async status() {
95
- const res = await this.expect('GET', '/status')
96
- return res.json()
97
- }
98
- // masked: an ipn.MaskedPrefs object, e.g. { HostnameSet: true, Hostname: 'pinokio' }
99
- async patchPrefs(masked) {
100
- const res = await this.expect('PATCH', '/prefs', { body: masked })
101
- return res.json()
102
- }
103
- async loginInteractive() {
104
- await this.expect('POST', '/login-interactive', { body: '' })
105
- return true
106
- }
107
- async logout() {
108
- await this.expect('POST', '/logout', { body: '' })
109
- return true
110
- }
111
- // Restarts the backend's login state machine (what `tailscale up --force-reauth`
112
- // does): a stale or invalidated login URL is dropped and a fresh one is minted
113
- // on the next login-interactive.
114
- async start(options = {}) {
115
- await this.expect('POST', '/start', { body: options })
116
- return true
117
- }
118
- async serveConfig() {
119
- const res = await this.expect('GET', '/serve-config')
120
- return { config: res.json(), etag: res.headers.etag || res.headers.Etag || null }
121
- }
122
- async setServeConfig(config, etag) {
123
- const headers = {}
124
- if (etag) {
125
- headers['If-Match'] = etag
126
- }
127
- await this.expect('POST', '/serve-config', { body: config || {}, headers })
128
- return true
129
- }
130
- async queryFeature(feature) {
131
- const res = await this.request('POST', `/query-feature?feature=${encodeURIComponent(feature)}`, { body: '' })
132
- if (res.status !== 200) {
133
- throw new LocalAPIError(`query-feature ${feature} failed: ${res.status} ${res.text.trim()}`, res.status, res.text)
134
- }
135
- return res.json()
136
- }
137
- async ping() {
138
- try {
139
- const res = await this.request('GET', '/status', { timeout: 2000 })
140
- return res.status === 200
141
- } catch (e) {
142
- return false
143
- }
144
- }
145
- }
146
-
147
- module.exports = { LocalAPI, LocalAPIError }