conductor-remote 1.8.0 → 1.9.1

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/bin/cli.js CHANGED
@@ -1,13 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  // conductor-remote CLI entrypoint.
3
3
  //
4
- // Runs on plain Node ≥24 with zero flags: the relay is stdlib-only and the two
5
- // param-property constructors that once needed --experimental-transform-types
6
- // are gone, so default type-stripping handles the .ts sources on import.
4
+ // Runs on plain Node ≥24 with zero flags. The published tarball ships compiled JS
5
+ // under dist-node/ (Node REFUSES to type-strip .ts under node_modules, so an
6
+ // installed package can't run raw sources); a dev checkout has no dist-node/ and
7
+ // runs the .ts sources directly, since type-stripping works outside node_modules.
8
+ // resolveEntry() prefers compiled, falls back to source — one entrypoint, both worlds.
7
9
  //
8
10
  // The one remaining wrinkle is node:sqlite, still flagged experimental in 24.
9
11
  // We silence *only* that warning here (instead of re-execing node with
10
12
  // --disable-warning) so the entrypoint stays a shebang + import.
13
+ import { existsSync, readFileSync } from 'node:fs'
14
+ import { fileURLToPath } from 'node:url'
15
+
11
16
  const emitWarning = process.emitWarning.bind(process)
12
17
  process.emitWarning = (warning, ...rest) => {
13
18
  const type = typeof rest[0] === 'string' ? rest[0] : rest[0]?.type
@@ -25,8 +30,27 @@ if (major < REQUIRED_MAJOR) {
25
30
  process.exit(1)
26
31
  }
27
32
 
33
+ // Under node_modules (installed package) Node can't type-strip, so run compiled dist-node/; outside it
34
+ // (dev checkout) run the live .ts source so edits take effect without a build. Each falls back to the
35
+ // other if its preferred artifact is missing.
36
+ function resolveEntry(compiledRel, sourceRel) {
37
+ const order = import.meta.url.includes('/node_modules/') ? [compiledRel, sourceRel] : [sourceRel, compiledRel]
38
+ for (const rel of order) {
39
+ const url = new URL(rel, import.meta.url)
40
+ if (existsSync(fileURLToPath(url))) return url.href
41
+ }
42
+ return new URL(order[0], import.meta.url).href
43
+ }
44
+
28
45
  const [cmd, ...rest] = process.argv.slice(2)
29
46
 
47
+ function version() {
48
+ // Read the installed manifest lazily; semantic-release stamps the real version
49
+ // into it at publish time (the checked-in value is a placeholder).
50
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
51
+ console.info(version)
52
+ }
53
+
30
54
  function usage() {
31
55
  console.info(
32
56
  [
@@ -36,6 +60,7 @@ function usage() {
36
60
  ' conductor-remote [start] run the relay (default)',
37
61
  ' conductor-remote service <subcommand> manage the login LaunchAgent',
38
62
  ' install | uninstall | restart | status',
63
+ ' conductor-remote --version print the installed version',
39
64
  '',
40
65
  'Install flags (each also settable via the env var in [brackets]):',
41
66
  ' --expose public|tailnet reachability: public Funnel (default) or tailnet-only [EXPOSE]',
@@ -55,12 +80,17 @@ function usage() {
55
80
  switch (cmd) {
56
81
  case undefined:
57
82
  case 'start':
58
- await import('../src/server.ts')
83
+ await import(resolveEntry('../dist-node/src/server.js', '../src/server.ts'))
59
84
  break
60
85
  case 'service':
61
86
  // service.ts reads its subcommand from argv[2]; re-shape argv so `service install` → `install`.
62
87
  process.argv = [process.argv[0], process.argv[1], ...rest]
63
- await import('../scripts/service.ts')
88
+ await import(resolveEntry('../dist-node/scripts/service.js', '../scripts/service.ts'))
89
+ break
90
+ case '-v':
91
+ case '--version':
92
+ case 'version':
93
+ version()
64
94
  break
65
95
  case '-h':
66
96
  case '--help':
@@ -0,0 +1,400 @@
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
+ // ── Galois field GF(256), primitive polynomial 0x11d ────────────────────────────────────────────────
12
+ const EXP = new Uint8Array(512);
13
+ const LOG = new Uint8Array(256);
14
+ let gfx = 1;
15
+ for (let i = 0; i < 255; i++) {
16
+ EXP[i] = gfx;
17
+ LOG[gfx] = i;
18
+ gfx <<= 1;
19
+ if (gfx & 0x100)
20
+ gfx ^= 0x11d;
21
+ }
22
+ for (let i = 255; i < 512; i++)
23
+ EXP[i] = EXP[i - 255];
24
+ function gfMul(a, b) {
25
+ return a === 0 || b === 0 ? 0 : EXP[LOG[a] + LOG[b]];
26
+ }
27
+ /** Reed–Solomon generator polynomial of the given degree; coeff[0] is the leading (highest) term. */
28
+ function rsGenerator(degree) {
29
+ let poly = [1];
30
+ for (let i = 0; i < degree; i++) {
31
+ const next = new Array(poly.length + 1).fill(0);
32
+ for (let j = 0; j < poly.length; j++) {
33
+ next[j] ^= poly[j];
34
+ next[j + 1] ^= gfMul(poly[j], EXP[i]);
35
+ }
36
+ poly = next;
37
+ }
38
+ return poly;
39
+ }
40
+ /** The `ecLen` error-correction codewords for one data block (polynomial remainder). */
41
+ function rsRemainder(data, ecLen) {
42
+ const gen = rsGenerator(ecLen);
43
+ const res = new Array(ecLen).fill(0);
44
+ for (const d of data) {
45
+ const factor = d ^ res[0];
46
+ res.shift();
47
+ res.push(0);
48
+ if (factor !== 0)
49
+ for (let i = 0; i < ecLen; i++)
50
+ res[i] ^= gfMul(gen[i + 1], factor);
51
+ }
52
+ return res;
53
+ }
54
+ const VERSIONS = {
55
+ 1: { align: [], ec: 10, groups: [[1, 16]] },
56
+ 2: { align: [6, 18], ec: 16, groups: [[1, 28]] },
57
+ 3: { align: [6, 22], ec: 26, groups: [[1, 44]] },
58
+ 4: { align: [6, 26], ec: 18, groups: [[2, 32]] },
59
+ 5: { align: [6, 30], ec: 24, groups: [[2, 43]] },
60
+ 6: { align: [6, 34], ec: 16, groups: [[4, 27]] },
61
+ 7: { align: [6, 22, 38], ec: 18, groups: [[4, 31]] },
62
+ 8: {
63
+ align: [6, 24, 42],
64
+ ec: 22,
65
+ groups: [
66
+ [2, 38],
67
+ [2, 39]
68
+ ]
69
+ },
70
+ 9: {
71
+ align: [6, 26, 46],
72
+ ec: 22,
73
+ groups: [
74
+ [3, 36],
75
+ [2, 37]
76
+ ]
77
+ },
78
+ 10: {
79
+ align: [6, 28, 50],
80
+ ec: 26,
81
+ groups: [
82
+ [4, 43],
83
+ [1, 44]
84
+ ]
85
+ }
86
+ };
87
+ function dataCodewords(v) {
88
+ return VERSIONS[v].groups.reduce((sum, [count, per]) => sum + count * per, 0);
89
+ }
90
+ function countBits(v) {
91
+ return v <= 9 ? 8 : 16;
92
+ }
93
+ // ── Bit helpers ─────────────────────────────────────────────────────────────────────────────────────
94
+ function pushBits(bits, value, len) {
95
+ for (let i = len - 1; i >= 0; i--)
96
+ bits.push((value >> i) & 1);
97
+ }
98
+ // ── Data encoding: text → interleaved data+EC bitstream for the smallest fitting version ─────────────
99
+ function encodeText(text) {
100
+ const bytes = Array.from(new TextEncoder().encode(text));
101
+ let version = 0;
102
+ for (let v = 1; v <= 10; v++) {
103
+ if (4 + countBits(v) + 8 * bytes.length <= dataCodewords(v) * 8) {
104
+ version = v;
105
+ break;
106
+ }
107
+ }
108
+ if (version === 0)
109
+ throw new Error(`data too long for a v1–10 QR (${bytes.length} bytes)`);
110
+ const capacityBits = dataCodewords(version) * 8;
111
+ const bits = [];
112
+ pushBits(bits, 0b0100, 4); // byte mode
113
+ pushBits(bits, bytes.length, countBits(version));
114
+ for (const b of bytes)
115
+ pushBits(bits, b, 8);
116
+ for (let i = 0, n = Math.min(4, capacityBits - bits.length); i < n; i++)
117
+ bits.push(0); // terminator
118
+ while (bits.length % 8 !== 0)
119
+ bits.push(0);
120
+ const pad = [0xec, 0x11];
121
+ for (let i = 0; bits.length < capacityBits; i++)
122
+ pushBits(bits, pad[i % 2], 8);
123
+ const dataCw = [];
124
+ for (let i = 0; i < bits.length; i += 8) {
125
+ let cw = 0;
126
+ for (let b = 0; b < 8; b++)
127
+ cw = (cw << 1) | bits[i + b];
128
+ dataCw.push(cw);
129
+ }
130
+ const { ec, groups } = VERSIONS[version];
131
+ const blocks = [];
132
+ let idx = 0;
133
+ for (const [count, per] of groups) {
134
+ for (let b = 0; b < count; b++) {
135
+ const data = dataCw.slice(idx, idx + per);
136
+ idx += per;
137
+ blocks.push({ data, ec: rsRemainder(data, ec) });
138
+ }
139
+ }
140
+ const out = [];
141
+ const maxData = Math.max(...blocks.map(bl => bl.data.length));
142
+ for (let i = 0; i < maxData; i++)
143
+ for (const bl of blocks)
144
+ if (i < bl.data.length)
145
+ out.push(bl.data[i]);
146
+ for (let i = 0; i < ec; i++)
147
+ for (const bl of blocks)
148
+ out.push(bl.ec[i]);
149
+ const dataBits = [];
150
+ for (const cw of out)
151
+ pushBits(dataBits, cw, 8);
152
+ return { version, bits: dataBits };
153
+ }
154
+ // ── Matrix construction ─────────────────────────────────────────────────────────────────────────────
155
+ function buildMatrix(version, dataBits) {
156
+ const size = version * 4 + 17;
157
+ const m = Array.from({ length: size }, () => new Array(size).fill(false));
158
+ const reserved = Array.from({ length: size }, () => new Array(size).fill(false));
159
+ const finder = (r0, c0) => {
160
+ for (let r = -1; r <= 7; r++)
161
+ for (let c = -1; c <= 7; c++) {
162
+ const rr = r0 + r;
163
+ const cc = c0 + c;
164
+ if (rr < 0 || rr >= size || cc < 0 || cc >= size)
165
+ continue;
166
+ const border = (r >= 0 && r <= 6 && (c === 0 || c === 6)) || (c >= 0 && c <= 6 && (r === 0 || r === 6));
167
+ const center = r >= 2 && r <= 4 && c >= 2 && c <= 4;
168
+ m[rr][cc] = border || center;
169
+ reserved[rr][cc] = true;
170
+ }
171
+ };
172
+ finder(0, 0);
173
+ finder(0, size - 7);
174
+ finder(size - 7, 0);
175
+ // Timing patterns
176
+ for (let i = 8; i < size - 8; i++) {
177
+ const v = i % 2 === 0;
178
+ if (!reserved[6][i]) {
179
+ m[6][i] = v;
180
+ reserved[6][i] = true;
181
+ }
182
+ if (!reserved[i][6]) {
183
+ m[i][6] = v;
184
+ reserved[i][6] = true;
185
+ }
186
+ }
187
+ // Alignment patterns (skip the three that collide with finders)
188
+ const pos = VERSIONS[version].align;
189
+ for (let i = 0; i < pos.length; i++)
190
+ for (let j = 0; j < pos.length; j++) {
191
+ const corner = (i === 0 && j === 0) || (i === 0 && j === pos.length - 1) || (i === pos.length - 1 && j === 0);
192
+ if (corner)
193
+ continue;
194
+ const cr = pos[i];
195
+ const cc = pos[j];
196
+ for (let r = -2; r <= 2; r++)
197
+ for (let c = -2; c <= 2; c++) {
198
+ m[cr + r][cc + c] = Math.max(Math.abs(r), Math.abs(c)) !== 1;
199
+ reserved[cr + r][cc + c] = true;
200
+ }
201
+ }
202
+ // Reserve format-info strips and (v≥7) version-info blocks — filled after masking.
203
+ for (let i = 0; i <= 8; i++) {
204
+ if (i !== 6) {
205
+ reserved[8][i] = true;
206
+ reserved[i][8] = true;
207
+ }
208
+ }
209
+ for (let i = 0; i < 8; i++) {
210
+ reserved[8][size - 1 - i] = true;
211
+ reserved[size - 1 - i][8] = true;
212
+ }
213
+ reserved[size - 8][8] = true; // dark module
214
+ if (version >= 7)
215
+ for (let i = 0; i < 18; i++) {
216
+ const a = size - 11 + (i % 3);
217
+ const b = Math.floor(i / 3);
218
+ reserved[b][a] = true;
219
+ reserved[a][b] = true;
220
+ }
221
+ // Data placement: two-column zigzag from bottom-right, skipping the timing column.
222
+ let bit = 0;
223
+ let upward = true;
224
+ for (let col = size - 1; col > 0; col -= 2) {
225
+ if (col === 6)
226
+ col--;
227
+ for (let i = 0; i < size; i++) {
228
+ const row = upward ? size - 1 - i : i;
229
+ for (let k = 0; k < 2; k++) {
230
+ const c = col - k;
231
+ if (reserved[row][c])
232
+ continue;
233
+ m[row][c] = bit < dataBits.length ? dataBits[bit] === 1 : false;
234
+ bit++;
235
+ }
236
+ }
237
+ upward = !upward;
238
+ }
239
+ if (version >= 7)
240
+ drawVersion(m, version, size);
241
+ // Try every mask, keep the lowest-penalty one.
242
+ let best = null;
243
+ let bestScore = Number.POSITIVE_INFINITY;
244
+ for (let mask = 0; mask < 8; mask++) {
245
+ const cand = m.map(row => row.slice());
246
+ for (let r = 0; r < size; r++)
247
+ for (let c = 0; c < size; c++)
248
+ if (!reserved[r][c] && maskBit(mask, r, c))
249
+ cand[r][c] = !cand[r][c];
250
+ drawFormat(cand, mask, size);
251
+ const score = penalty(cand);
252
+ if (score < bestScore) {
253
+ bestScore = score;
254
+ best = cand;
255
+ }
256
+ }
257
+ return best;
258
+ }
259
+ function maskBit(mask, r, c) {
260
+ switch (mask) {
261
+ case 0:
262
+ return (r + c) % 2 === 0;
263
+ case 1:
264
+ return r % 2 === 0;
265
+ case 2:
266
+ return c % 3 === 0;
267
+ case 3:
268
+ return (r + c) % 3 === 0;
269
+ case 4:
270
+ return (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0;
271
+ case 5:
272
+ return ((r * c) % 2) + ((r * c) % 3) === 0;
273
+ case 6:
274
+ return (((r * c) % 2) + ((r * c) % 3)) % 2 === 0;
275
+ default:
276
+ return (((r + c) % 2) + ((r * c) % 3)) % 2 === 0;
277
+ }
278
+ }
279
+ /** 15-bit format info (level M = 00) with BCH(15,5) and the 0x5412 mask. */
280
+ function drawFormat(m, mask, size) {
281
+ const data = (0b00 << 3) | mask;
282
+ let rem = data;
283
+ for (let i = 0; i < 10; i++)
284
+ rem = (rem << 1) ^ ((rem >> 9) & 1 ? 0x537 : 0);
285
+ const bits = (((data << 10) | (rem & 0x3ff)) ^ 0x5412) & 0x7fff;
286
+ const get = (i) => ((bits >> i) & 1) === 1;
287
+ for (let i = 0; i <= 5; i++)
288
+ m[i][8] = get(i);
289
+ m[7][8] = get(6);
290
+ m[8][8] = get(7);
291
+ m[8][7] = get(8);
292
+ for (let i = 9; i < 15; i++)
293
+ m[8][14 - i] = get(i);
294
+ for (let i = 0; i < 8; i++)
295
+ m[8][size - 1 - i] = get(i);
296
+ for (let i = 8; i < 15; i++)
297
+ m[size - 15 + i][8] = get(i);
298
+ m[size - 8][8] = true; // dark module
299
+ }
300
+ /** 18-bit version info (v≥7), BCH(18,6) with 0x1f25. */
301
+ function drawVersion(m, version, size) {
302
+ let rem = version;
303
+ for (let i = 0; i < 12; i++)
304
+ rem = (rem << 1) ^ ((rem >> 11) & 1 ? 0x1f25 : 0);
305
+ const bits = (version << 12) | (rem & 0xfff);
306
+ for (let i = 0; i < 18; i++) {
307
+ const b = ((bits >> i) & 1) === 1;
308
+ const a = size - 11 + (i % 3);
309
+ const d = Math.floor(i / 3);
310
+ m[d][a] = b;
311
+ m[a][d] = b;
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) {
316
+ const n = m.length;
317
+ let score = 0;
318
+ const runScore = (get) => {
319
+ let color = get(0);
320
+ let len = 1;
321
+ for (let i = 1; i < n; i++) {
322
+ if (get(i) === color)
323
+ len++;
324
+ else {
325
+ if (len >= 5)
326
+ score += len - 2;
327
+ color = get(i);
328
+ len = 1;
329
+ }
330
+ }
331
+ if (len >= 5)
332
+ score += len - 2;
333
+ };
334
+ for (let r = 0; r < n; r++)
335
+ runScore(i => m[r][i]);
336
+ for (let c = 0; c < n; c++)
337
+ runScore(i => m[i][c]);
338
+ for (let r = 0; r < n - 1; r++)
339
+ for (let c = 0; c < n - 1; c++) {
340
+ const v = m[r][c];
341
+ if (v === m[r][c + 1] && v === m[r + 1][c] && v === m[r + 1][c + 1])
342
+ score += 3;
343
+ }
344
+ const a = [true, false, true, true, true, false, true, false, false, false, false];
345
+ const b = [false, false, false, false, true, false, true, true, true, false, true];
346
+ const match = (get, start, pat) => {
347
+ for (let i = 0; i < 11; i++)
348
+ if (get(start + i) !== pat[i])
349
+ return false;
350
+ return true;
351
+ };
352
+ for (let r = 0; r < n; r++)
353
+ for (let c = 0; c <= n - 11; c++) {
354
+ if (match(i => m[r][i], c, a) || match(i => m[r][i], c, b))
355
+ score += 40;
356
+ }
357
+ for (let c = 0; c < n; c++)
358
+ for (let r = 0; r <= n - 11; r++) {
359
+ if (match(i => m[i][c], r, a) || match(i => m[i][c], r, b))
360
+ score += 40;
361
+ }
362
+ let dark = 0;
363
+ for (let r = 0; r < n; r++)
364
+ for (let c = 0; c < n; c++)
365
+ if (m[r][c])
366
+ dark++;
367
+ score += Math.floor(Math.abs((dark / (n * n)) * 100 - 50) / 5) * 10;
368
+ return score;
369
+ }
370
+ // ── Terminal rendering ──────────────────────────────────────────────────────────────────────────────
371
+ /**
372
+ * Render the matrix as half-block lines (two modules per character row, ~square modules) with a forced
373
+ * black-on-white style so it scans correctly regardless of the terminal theme, plus the 4-module quiet
374
+ * zone the spec requires. `indent` is prepended (outside the styled region) to align with other output.
375
+ */
376
+ function render(m, indent) {
377
+ const size = m.length;
378
+ const quiet = 4;
379
+ const dark = (r, c) => (r >= 0 && r < size && c >= 0 && c < size ? m[r][c] : false);
380
+ const lines = [];
381
+ for (let r = -quiet; r < size + quiet; r += 2) {
382
+ let line = `${indent}\x1b[30;47m`;
383
+ for (let c = -quiet; c < size + quiet; c++) {
384
+ const top = dark(r, c);
385
+ const bottom = dark(r + 1, c);
386
+ line += top && bottom ? '█' : top ? '▀' : bottom ? '▄' : ' ';
387
+ }
388
+ lines.push(`${line}\x1b[0m`);
389
+ }
390
+ return lines;
391
+ }
392
+ /** Encode `text` into its QR module matrix (`true` = dark). Shared by the terminal and web SVG renderers. */
393
+ export function qrMatrix(text) {
394
+ const { version, bits } = encodeText(text);
395
+ return buildMatrix(version, bits);
396
+ }
397
+ /** Encode `text` and return the terminal lines that draw its QR code (each already indented). */
398
+ export function qrLines(text, indent = ' ') {
399
+ return render(qrMatrix(text), indent);
400
+ }