conductor-remote 1.1.0 → 1.2.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/package.json +1 -1
- package/scripts/qr.ts +388 -0
- package/scripts/service.ts +18 -2
package/package.json
CHANGED
package/scripts/qr.ts
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free QR encoder — just enough to render the phone URL in the terminal.
|
|
3
|
+
*
|
|
4
|
+
* Scope is deliberately narrow: byte mode, error-correction level M, versions 1–10 (≈120 bytes max,
|
|
5
|
+
* and the phone URL is ~70). That keeps the version/EC tables small and hand-verifiable while covering
|
|
6
|
+
* every URL this relay can produce. Kept strip-clean like the rest of the repo (no enums/namespaces/
|
|
7
|
+
* param-property constructors) so it runs under plain `node` type-stripping with zero deps.
|
|
8
|
+
*
|
|
9
|
+
* References the ISO/IEC 18004 QR Code spec; placement/format math mirrors Nayuki's reference encoder.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ── Galois field GF(256), primitive polynomial 0x11d ────────────────────────────────────────────────
|
|
13
|
+
const EXP = new Uint8Array(512)
|
|
14
|
+
const LOG = new Uint8Array(256)
|
|
15
|
+
let gfx = 1
|
|
16
|
+
for (let i = 0; i < 255; i++) {
|
|
17
|
+
EXP[i] = gfx
|
|
18
|
+
LOG[gfx] = i
|
|
19
|
+
gfx <<= 1
|
|
20
|
+
if (gfx & 0x100) gfx ^= 0x11d
|
|
21
|
+
}
|
|
22
|
+
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255]
|
|
23
|
+
|
|
24
|
+
function gfMul(a: number, b: number): number {
|
|
25
|
+
return a === 0 || b === 0 ? 0 : EXP[LOG[a] + LOG[b]]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Reed–Solomon generator polynomial of the given degree; coeff[0] is the leading (highest) term. */
|
|
29
|
+
function rsGenerator(degree: number): number[] {
|
|
30
|
+
let poly = [1]
|
|
31
|
+
for (let i = 0; i < degree; i++) {
|
|
32
|
+
const next = new Array(poly.length + 1).fill(0)
|
|
33
|
+
for (let j = 0; j < poly.length; j++) {
|
|
34
|
+
next[j] ^= poly[j]
|
|
35
|
+
next[j + 1] ^= gfMul(poly[j], EXP[i])
|
|
36
|
+
}
|
|
37
|
+
poly = next
|
|
38
|
+
}
|
|
39
|
+
return poly
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The `ecLen` error-correction codewords for one data block (polynomial remainder). */
|
|
43
|
+
function rsRemainder(data: number[], ecLen: number): number[] {
|
|
44
|
+
const gen = rsGenerator(ecLen)
|
|
45
|
+
const res = new Array(ecLen).fill(0)
|
|
46
|
+
for (const d of data) {
|
|
47
|
+
const factor = d ^ res[0]
|
|
48
|
+
res.shift()
|
|
49
|
+
res.push(0)
|
|
50
|
+
if (factor !== 0) for (let i = 0; i < ecLen; i++) res[i] ^= gfMul(gen[i + 1], factor)
|
|
51
|
+
}
|
|
52
|
+
return res
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Version tables (level M only) ───────────────────────────────────────────────────────────────────
|
|
56
|
+
type Group = [number, number] // [block count, data codewords per block]
|
|
57
|
+
type VersionInfo = { align: number[]; ec: number; groups: Group[] }
|
|
58
|
+
|
|
59
|
+
const VERSIONS: Record<number, VersionInfo> = {
|
|
60
|
+
1: { align: [], ec: 10, groups: [[1, 16]] },
|
|
61
|
+
2: { align: [6, 18], ec: 16, groups: [[1, 28]] },
|
|
62
|
+
3: { align: [6, 22], ec: 26, groups: [[1, 44]] },
|
|
63
|
+
4: { align: [6, 26], ec: 18, groups: [[2, 32]] },
|
|
64
|
+
5: { align: [6, 30], ec: 24, groups: [[2, 43]] },
|
|
65
|
+
6: { align: [6, 34], ec: 16, groups: [[4, 27]] },
|
|
66
|
+
7: { align: [6, 22, 38], ec: 18, groups: [[4, 31]] },
|
|
67
|
+
8: {
|
|
68
|
+
align: [6, 24, 42],
|
|
69
|
+
ec: 22,
|
|
70
|
+
groups: [
|
|
71
|
+
[2, 38],
|
|
72
|
+
[2, 39]
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
9: {
|
|
76
|
+
align: [6, 26, 46],
|
|
77
|
+
ec: 22,
|
|
78
|
+
groups: [
|
|
79
|
+
[3, 36],
|
|
80
|
+
[2, 37]
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
10: {
|
|
84
|
+
align: [6, 28, 50],
|
|
85
|
+
ec: 26,
|
|
86
|
+
groups: [
|
|
87
|
+
[4, 43],
|
|
88
|
+
[1, 44]
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function dataCodewords(v: number): number {
|
|
94
|
+
return VERSIONS[v].groups.reduce((sum, [count, per]) => sum + count * per, 0)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function countBits(v: number): number {
|
|
98
|
+
return v <= 9 ? 8 : 16
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Bit helpers ─────────────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
function pushBits(bits: number[], value: number, len: number): void {
|
|
103
|
+
for (let i = len - 1; i >= 0; i--) bits.push((value >> i) & 1)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Data encoding: text → interleaved data+EC bitstream for the smallest fitting version ─────────────
|
|
107
|
+
function encodeText(text: string): { version: number; bits: number[] } {
|
|
108
|
+
const bytes = Array.from(new TextEncoder().encode(text))
|
|
109
|
+
let version = 0
|
|
110
|
+
for (let v = 1; v <= 10; v++) {
|
|
111
|
+
if (4 + countBits(v) + 8 * bytes.length <= dataCodewords(v) * 8) {
|
|
112
|
+
version = v
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (version === 0) throw new Error(`data too long for a v1–10 QR (${bytes.length} bytes)`)
|
|
117
|
+
|
|
118
|
+
const capacityBits = dataCodewords(version) * 8
|
|
119
|
+
const bits: number[] = []
|
|
120
|
+
pushBits(bits, 0b0100, 4) // byte mode
|
|
121
|
+
pushBits(bits, bytes.length, countBits(version))
|
|
122
|
+
for (const b of bytes) pushBits(bits, b, 8)
|
|
123
|
+
for (let i = 0, n = Math.min(4, capacityBits - bits.length); i < n; i++) bits.push(0) // terminator
|
|
124
|
+
while (bits.length % 8 !== 0) bits.push(0)
|
|
125
|
+
const pad = [0xec, 0x11]
|
|
126
|
+
for (let i = 0; bits.length < capacityBits; i++) pushBits(bits, pad[i % 2], 8)
|
|
127
|
+
|
|
128
|
+
const dataCw: number[] = []
|
|
129
|
+
for (let i = 0; i < bits.length; i += 8) {
|
|
130
|
+
let cw = 0
|
|
131
|
+
for (let b = 0; b < 8; b++) cw = (cw << 1) | bits[i + b]
|
|
132
|
+
dataCw.push(cw)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { ec, groups } = VERSIONS[version]
|
|
136
|
+
const blocks: { data: number[]; ec: number[] }[] = []
|
|
137
|
+
let idx = 0
|
|
138
|
+
for (const [count, per] of groups) {
|
|
139
|
+
for (let b = 0; b < count; b++) {
|
|
140
|
+
const data = dataCw.slice(idx, idx + per)
|
|
141
|
+
idx += per
|
|
142
|
+
blocks.push({ data, ec: rsRemainder(data, ec) })
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const out: number[] = []
|
|
147
|
+
const maxData = Math.max(...blocks.map(bl => bl.data.length))
|
|
148
|
+
for (let i = 0; i < maxData; i++) for (const bl of blocks) if (i < bl.data.length) out.push(bl.data[i])
|
|
149
|
+
for (let i = 0; i < ec; i++) for (const bl of blocks) out.push(bl.ec[i])
|
|
150
|
+
|
|
151
|
+
const dataBits: number[] = []
|
|
152
|
+
for (const cw of out) pushBits(dataBits, cw, 8)
|
|
153
|
+
return { version, bits: dataBits }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Matrix construction ─────────────────────────────────────────────────────────────────────────────
|
|
157
|
+
function buildMatrix(version: number, dataBits: number[]): boolean[][] {
|
|
158
|
+
const size = version * 4 + 17
|
|
159
|
+
const m: boolean[][] = Array.from({ length: size }, () => new Array(size).fill(false))
|
|
160
|
+
const reserved: boolean[][] = Array.from({ length: size }, () => new Array(size).fill(false))
|
|
161
|
+
|
|
162
|
+
const finder = (r0: number, c0: number) => {
|
|
163
|
+
for (let r = -1; r <= 7; r++)
|
|
164
|
+
for (let c = -1; c <= 7; c++) {
|
|
165
|
+
const rr = r0 + r
|
|
166
|
+
const cc = c0 + c
|
|
167
|
+
if (rr < 0 || rr >= size || cc < 0 || cc >= size) continue
|
|
168
|
+
const border = (r >= 0 && r <= 6 && (c === 0 || c === 6)) || (c >= 0 && c <= 6 && (r === 0 || r === 6))
|
|
169
|
+
const center = r >= 2 && r <= 4 && c >= 2 && c <= 4
|
|
170
|
+
m[rr][cc] = border || center
|
|
171
|
+
reserved[rr][cc] = true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
finder(0, 0)
|
|
175
|
+
finder(0, size - 7)
|
|
176
|
+
finder(size - 7, 0)
|
|
177
|
+
|
|
178
|
+
// Timing patterns
|
|
179
|
+
for (let i = 8; i < size - 8; i++) {
|
|
180
|
+
const v = i % 2 === 0
|
|
181
|
+
if (!reserved[6][i]) {
|
|
182
|
+
m[6][i] = v
|
|
183
|
+
reserved[6][i] = true
|
|
184
|
+
}
|
|
185
|
+
if (!reserved[i][6]) {
|
|
186
|
+
m[i][6] = v
|
|
187
|
+
reserved[i][6] = true
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Alignment patterns (skip the three that collide with finders)
|
|
192
|
+
const pos = VERSIONS[version].align
|
|
193
|
+
for (let i = 0; i < pos.length; i++)
|
|
194
|
+
for (let j = 0; j < pos.length; j++) {
|
|
195
|
+
const corner = (i === 0 && j === 0) || (i === 0 && j === pos.length - 1) || (i === pos.length - 1 && j === 0)
|
|
196
|
+
if (corner) continue
|
|
197
|
+
const cr = pos[i]
|
|
198
|
+
const cc = pos[j]
|
|
199
|
+
for (let r = -2; r <= 2; r++)
|
|
200
|
+
for (let c = -2; c <= 2; c++) {
|
|
201
|
+
m[cr + r][cc + c] = Math.max(Math.abs(r), Math.abs(c)) !== 1
|
|
202
|
+
reserved[cr + r][cc + c] = true
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Reserve format-info strips and (v≥7) version-info blocks — filled after masking.
|
|
207
|
+
for (let i = 0; i <= 8; i++) {
|
|
208
|
+
if (i !== 6) {
|
|
209
|
+
reserved[8][i] = true
|
|
210
|
+
reserved[i][8] = true
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (let i = 0; i < 8; i++) {
|
|
214
|
+
reserved[8][size - 1 - i] = true
|
|
215
|
+
reserved[size - 1 - i][8] = true
|
|
216
|
+
}
|
|
217
|
+
reserved[size - 8][8] = true // dark module
|
|
218
|
+
if (version >= 7)
|
|
219
|
+
for (let i = 0; i < 18; i++) {
|
|
220
|
+
const a = size - 11 + (i % 3)
|
|
221
|
+
const b = Math.floor(i / 3)
|
|
222
|
+
reserved[b][a] = true
|
|
223
|
+
reserved[a][b] = true
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Data placement: two-column zigzag from bottom-right, skipping the timing column.
|
|
227
|
+
let bit = 0
|
|
228
|
+
let upward = true
|
|
229
|
+
for (let col = size - 1; col > 0; col -= 2) {
|
|
230
|
+
if (col === 6) col--
|
|
231
|
+
for (let i = 0; i < size; i++) {
|
|
232
|
+
const row = upward ? size - 1 - i : i
|
|
233
|
+
for (let k = 0; k < 2; k++) {
|
|
234
|
+
const c = col - k
|
|
235
|
+
if (reserved[row][c]) continue
|
|
236
|
+
m[row][c] = bit < dataBits.length ? dataBits[bit] === 1 : false
|
|
237
|
+
bit++
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
upward = !upward
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (version >= 7) drawVersion(m, version, size)
|
|
244
|
+
|
|
245
|
+
// Try every mask, keep the lowest-penalty one.
|
|
246
|
+
let best: boolean[][] | null = null
|
|
247
|
+
let bestScore = Number.POSITIVE_INFINITY
|
|
248
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
249
|
+
const cand = m.map(row => row.slice())
|
|
250
|
+
for (let r = 0; r < size; r++)
|
|
251
|
+
for (let c = 0; c < size; c++) if (!reserved[r][c] && maskBit(mask, r, c)) cand[r][c] = !cand[r][c]
|
|
252
|
+
drawFormat(cand, mask, size)
|
|
253
|
+
const score = penalty(cand)
|
|
254
|
+
if (score < bestScore) {
|
|
255
|
+
bestScore = score
|
|
256
|
+
best = cand
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return best as boolean[][]
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function maskBit(mask: number, r: number, c: number): boolean {
|
|
263
|
+
switch (mask) {
|
|
264
|
+
case 0:
|
|
265
|
+
return (r + c) % 2 === 0
|
|
266
|
+
case 1:
|
|
267
|
+
return r % 2 === 0
|
|
268
|
+
case 2:
|
|
269
|
+
return c % 3 === 0
|
|
270
|
+
case 3:
|
|
271
|
+
return (r + c) % 3 === 0
|
|
272
|
+
case 4:
|
|
273
|
+
return (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0
|
|
274
|
+
case 5:
|
|
275
|
+
return ((r * c) % 2) + ((r * c) % 3) === 0
|
|
276
|
+
case 6:
|
|
277
|
+
return (((r * c) % 2) + ((r * c) % 3)) % 2 === 0
|
|
278
|
+
default:
|
|
279
|
+
return (((r + c) % 2) + ((r * c) % 3)) % 2 === 0
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** 15-bit format info (level M = 00) with BCH(15,5) and the 0x5412 mask. */
|
|
284
|
+
function drawFormat(m: boolean[][], mask: number, size: number): void {
|
|
285
|
+
const data = (0b00 << 3) | mask
|
|
286
|
+
let rem = data
|
|
287
|
+
for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >> 9) & 1 ? 0x537 : 0)
|
|
288
|
+
const bits = (((data << 10) | (rem & 0x3ff)) ^ 0x5412) & 0x7fff
|
|
289
|
+
const get = (i: number) => ((bits >> i) & 1) === 1
|
|
290
|
+
for (let i = 0; i <= 5; i++) m[i][8] = get(i)
|
|
291
|
+
m[7][8] = get(6)
|
|
292
|
+
m[8][8] = get(7)
|
|
293
|
+
m[8][7] = get(8)
|
|
294
|
+
for (let i = 9; i < 15; i++) m[8][14 - i] = get(i)
|
|
295
|
+
for (let i = 0; i < 8; i++) m[8][size - 1 - i] = get(i)
|
|
296
|
+
for (let i = 8; i < 15; i++) m[size - 15 + i][8] = get(i)
|
|
297
|
+
m[size - 8][8] = true // dark module
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** 18-bit version info (v≥7), BCH(18,6) with 0x1f25. */
|
|
301
|
+
function drawVersion(m: boolean[][], version: number, size: number): void {
|
|
302
|
+
let rem = version
|
|
303
|
+
for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >> 11) & 1 ? 0x1f25 : 0)
|
|
304
|
+
const bits = (version << 12) | (rem & 0xfff)
|
|
305
|
+
for (let i = 0; i < 18; i++) {
|
|
306
|
+
const b = ((bits >> i) & 1) === 1
|
|
307
|
+
const a = size - 11 + (i % 3)
|
|
308
|
+
const d = Math.floor(i / 3)
|
|
309
|
+
m[d][a] = b
|
|
310
|
+
m[a][d] = b
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ── Mask penalty (ISO/IEC 18004 §8.8.2) — only used to pick the mask, so exactness isn't critical ────
|
|
315
|
+
function penalty(m: boolean[][]): number {
|
|
316
|
+
const n = m.length
|
|
317
|
+
let score = 0
|
|
318
|
+
const runScore = (get: (i: number) => boolean) => {
|
|
319
|
+
let color = get(0)
|
|
320
|
+
let len = 1
|
|
321
|
+
for (let i = 1; i < n; i++) {
|
|
322
|
+
if (get(i) === color) len++
|
|
323
|
+
else {
|
|
324
|
+
if (len >= 5) score += len - 2
|
|
325
|
+
color = get(i)
|
|
326
|
+
len = 1
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (len >= 5) score += len - 2
|
|
330
|
+
}
|
|
331
|
+
for (let r = 0; r < n; r++) runScore(i => m[r][i])
|
|
332
|
+
for (let c = 0; c < n; c++) runScore(i => m[i][c])
|
|
333
|
+
|
|
334
|
+
for (let r = 0; r < n - 1; r++)
|
|
335
|
+
for (let c = 0; c < n - 1; c++) {
|
|
336
|
+
const v = m[r][c]
|
|
337
|
+
if (v === m[r][c + 1] && v === m[r + 1][c] && v === m[r + 1][c + 1]) score += 3
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const a = [true, false, true, true, true, false, true, false, false, false, false]
|
|
341
|
+
const b = [false, false, false, false, true, false, true, true, true, false, true]
|
|
342
|
+
const match = (get: (i: number) => boolean, start: number, pat: boolean[]) => {
|
|
343
|
+
for (let i = 0; i < 11; i++) if (get(start + i) !== pat[i]) return false
|
|
344
|
+
return true
|
|
345
|
+
}
|
|
346
|
+
for (let r = 0; r < n; r++)
|
|
347
|
+
for (let c = 0; c <= n - 11; c++) {
|
|
348
|
+
if (match(i => m[r][i], c, a) || match(i => m[r][i], c, b)) score += 40
|
|
349
|
+
}
|
|
350
|
+
for (let c = 0; c < n; c++)
|
|
351
|
+
for (let r = 0; r <= n - 11; r++) {
|
|
352
|
+
if (match(i => m[i][c], r, a) || match(i => m[i][c], r, b)) score += 40
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let dark = 0
|
|
356
|
+
for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) if (m[r][c]) dark++
|
|
357
|
+
score += Math.floor(Math.abs((dark / (n * n)) * 100 - 50) / 5) * 10
|
|
358
|
+
return score
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── Terminal rendering ──────────────────────────────────────────────────────────────────────────────
|
|
362
|
+
/**
|
|
363
|
+
* Render the matrix as half-block lines (two modules per character row, ~square modules) with a forced
|
|
364
|
+
* black-on-white style so it scans correctly regardless of the terminal theme, plus the 4-module quiet
|
|
365
|
+
* zone the spec requires. `indent` is prepended (outside the styled region) to align with other output.
|
|
366
|
+
*/
|
|
367
|
+
function render(m: boolean[][], indent: string): string[] {
|
|
368
|
+
const size = m.length
|
|
369
|
+
const quiet = 4
|
|
370
|
+
const dark = (r: number, c: number) => (r >= 0 && r < size && c >= 0 && c < size ? m[r][c] : false)
|
|
371
|
+
const lines: string[] = []
|
|
372
|
+
for (let r = -quiet; r < size + quiet; r += 2) {
|
|
373
|
+
let line = `${indent}\x1b[30;47m`
|
|
374
|
+
for (let c = -quiet; c < size + quiet; c++) {
|
|
375
|
+
const top = dark(r, c)
|
|
376
|
+
const bottom = dark(r + 1, c)
|
|
377
|
+
line += top && bottom ? '█' : top ? '▀' : bottom ? '▄' : ' '
|
|
378
|
+
}
|
|
379
|
+
lines.push(`${line}\x1b[0m`)
|
|
380
|
+
}
|
|
381
|
+
return lines
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Encode `text` and return the terminal lines that draw its QR code (each already indented). */
|
|
385
|
+
export function qrLines(text: string, indent = ' '): string[] {
|
|
386
|
+
const { version, bits } = encodeText(text)
|
|
387
|
+
return render(buildMatrix(version, bits), indent)
|
|
388
|
+
}
|
package/scripts/service.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { execFileSync } from 'node:child_process'
|
|
|
12
12
|
import fs from 'node:fs'
|
|
13
13
|
import os from 'node:os'
|
|
14
14
|
import path from 'node:path'
|
|
15
|
+
import { qrLines } from './qr.ts'
|
|
15
16
|
|
|
16
17
|
const LABEL = 'no.adluna.conductor-remote'
|
|
17
18
|
const projectDir = path.resolve(import.meta.dirname, '..')
|
|
@@ -294,14 +295,29 @@ function ensureTailscale(): void {
|
|
|
294
295
|
}
|
|
295
296
|
}
|
|
296
297
|
|
|
298
|
+
/** Print a scannable QR of `url` (theme-independent black-on-white). Never fatal — QR is a convenience. */
|
|
299
|
+
function printQr(url: string): void {
|
|
300
|
+
try {
|
|
301
|
+
console.info(`\n${qrLines(url).join('\n')}`)
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.info(` (QR skipped: ${err instanceof Error ? err.message : err})`)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
297
307
|
function printUrl(): void {
|
|
298
|
-
const
|
|
308
|
+
const token = currentToken()
|
|
309
|
+
const frag = `#token=${token ?? '<starts on first run>'}`
|
|
299
310
|
const bin = tailscaleBin()
|
|
300
311
|
const dns = bin ? magicDnsName(bin) : null
|
|
301
312
|
const state = bin ? tailscaleState(bin, dns) : { proxyOk: false, funnelOn: false }
|
|
302
313
|
if (dns && state.proxyOk) {
|
|
303
314
|
const scope = state.funnelOn ? 'public — any browser, token-gated' : 'same Tailnet only'
|
|
304
|
-
|
|
315
|
+
const url = `https://${dns}/${frag}`
|
|
316
|
+
console.info(`\n Phone URL (HTTPS, ${scope}):\n ${url}`)
|
|
317
|
+
if (token) {
|
|
318
|
+
console.info('\n Scan to open on your phone:')
|
|
319
|
+
printQr(url)
|
|
320
|
+
}
|
|
305
321
|
return
|
|
306
322
|
}
|
|
307
323
|
// Nothing fronting yet — the relay is only on loopback.
|