dsh-zen-remote 2.2.8 → 2.3.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/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/docs/interface.md +1 -1
- package/docs/remote-access.md +20 -0
- package/lan-gate.mjs +11 -1
- package/lib/adapters/dsh-better-sidebar/index.js +6 -2
- package/lib/adapters/dsh-better-sidebar/index.js.map +1 -1
- package/lib/adapters/dsh-better-sidebar/methods.js +3 -0
- package/lib/adapters/dsh-better-sidebar/methods.js.map +1 -1
- package/lib/admin-routes.js +33 -2
- package/lib/admin-routes.js.map +1 -1
- package/lib/client-routes.js +52 -0
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +3390 -10
- package/lib/config.js +70 -0
- package/lib/config.js.map +1 -1
- package/lib/index.js +232 -2
- package/lib/index.js.map +1 -1
- package/lib/intercept.js +43 -3
- package/lib/intercept.js.map +1 -1
- package/lib/lan-gate-server.cjs +29 -3
- package/lib/merge-streams.js +42 -12
- package/lib/merge-streams.js.map +1 -1
- package/lib/relay-client.js +71 -0
- package/lib/relay-client.js.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts +1 -1
- package/lib/types/adapters/dsh-better-sidebar/index.d.ts.map +1 -1
- package/lib/types/adapters/dsh-better-sidebar/methods.d.ts.map +1 -1
- package/lib/types/admin-routes.d.ts +13 -0
- package/lib/types/admin-routes.d.ts.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/locales.d.ts +204 -1
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/wizard/GatewayWizard.d.ts +29 -0
- package/lib/types/client/wizard/GatewayWizard.d.ts.map +1 -0
- package/lib/types/client/wizard/WizardCard.d.ts +30 -0
- package/lib/types/client/wizard/WizardCard.d.ts.map +1 -0
- package/lib/types/client/wizard/cards/MigrateCard.d.ts +15 -0
- package/lib/types/client/wizard/cards/MigrateCard.d.ts.map +1 -0
- package/lib/types/client/wizard/cards/PortForwardCard.d.ts +15 -0
- package/lib/types/client/wizard/cards/PortForwardCard.d.ts.map +1 -0
- package/lib/types/client/wizard/cards/ProxyCard.d.ts +19 -0
- package/lib/types/client/wizard/cards/ProxyCard.d.ts.map +1 -0
- package/lib/types/client/wizard/cards/TokenCard.d.ts +20 -0
- package/lib/types/client/wizard/cards/TokenCard.d.ts.map +1 -0
- package/lib/types/client/wizard/register-wizard.d.ts +51 -0
- package/lib/types/client/wizard/register-wizard.d.ts.map +1 -0
- package/lib/types/client/wizard/wizard-css.d.ts +10 -0
- package/lib/types/client/wizard/wizard-css.d.ts.map +1 -0
- package/lib/types/client-data/qr.d.ts +20 -0
- package/lib/types/client-data/qr.d.ts.map +1 -0
- package/lib/types/client-data/remote-status.d.ts +39 -2
- package/lib/types/client-data/remote-status.d.ts.map +1 -1
- package/lib/types/client-data/settings-form.d.ts +7 -2
- package/lib/types/client-data/settings-form.d.ts.map +1 -1
- package/lib/types/client-data/wizard.d.ts +247 -0
- package/lib/types/client-data/wizard.d.ts.map +1 -0
- package/lib/types/client-routes.d.ts +19 -0
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/config.d.ts +27 -0
- package/lib/types/config.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/intercept.d.ts.map +1 -1
- package/lib/types/merge-streams.d.ts.map +1 -1
- package/lib/types/relay-client.d.ts +10 -0
- package/lib/types/relay-client.d.ts.map +1 -1
- package/lib/types/wizard/binary-io.d.ts +111 -0
- package/lib/types/wizard/binary-io.d.ts.map +1 -0
- package/lib/types/wizard/binary.d.ts +118 -0
- package/lib/types/wizard/binary.d.ts.map +1 -0
- package/lib/types/wizard/caddy-process.d.ts +101 -0
- package/lib/types/wizard/caddy-process.d.ts.map +1 -0
- package/lib/types/wizard/caddy.d.ts +53 -0
- package/lib/types/wizard/caddy.d.ts.map +1 -0
- package/lib/types/wizard/cloudflare.d.ts +112 -0
- package/lib/types/wizard/cloudflare.d.ts.map +1 -0
- package/lib/types/wizard/cloudflared.d.ts +71 -0
- package/lib/types/wizard/cloudflared.d.ts.map +1 -0
- package/lib/types/wizard/probe.d.ts +66 -0
- package/lib/types/wizard/probe.d.ts.map +1 -0
- package/lib/types/wizard/proxy-snippets.d.ts +60 -0
- package/lib/types/wizard/proxy-snippets.d.ts.map +1 -0
- package/lib/types/wizard/routes.d.ts +152 -0
- package/lib/types/wizard/routes.d.ts.map +1 -0
- package/lib/types/wizard/source-detect.d.ts +53 -0
- package/lib/types/wizard/source-detect.d.ts.map +1 -0
- package/lib/types/wizard/status.d.ts +115 -0
- package/lib/types/wizard/status.d.ts.map +1 -0
- package/lib/types/wizard/token-store.d.ts +27 -0
- package/lib/types/wizard/token-store.d.ts.map +1 -0
- package/lib/types/wizard/tunnel-orchestrator.d.ts +90 -0
- package/lib/types/wizard/tunnel-orchestrator.d.ts.map +1 -0
- package/lib/types/wizard/verify.d.ts +97 -0
- package/lib/types/wizard/verify.d.ts.map +1 -0
- package/lib/wizard/binary-io.js +386 -0
- package/lib/wizard/binary-io.js.map +1 -0
- package/lib/wizard/binary.js +138 -0
- package/lib/wizard/binary.js.map +1 -0
- package/lib/wizard/caddy-process.js +162 -0
- package/lib/wizard/caddy-process.js.map +1 -0
- package/lib/wizard/caddy.js +80 -0
- package/lib/wizard/caddy.js.map +1 -0
- package/lib/wizard/cloudflare.js +334 -0
- package/lib/wizard/cloudflare.js.map +1 -0
- package/lib/wizard/cloudflared.js +158 -0
- package/lib/wizard/cloudflared.js.map +1 -0
- package/lib/wizard/probe.js +118 -0
- package/lib/wizard/probe.js.map +1 -0
- package/lib/wizard/proxy-snippets.js +112 -0
- package/lib/wizard/proxy-snippets.js.map +1 -0
- package/lib/wizard/routes.js +462 -0
- package/lib/wizard/routes.js.map +1 -0
- package/lib/wizard/source-detect.js +77 -0
- package/lib/wizard/source-detect.js.map +1 -0
- package/lib/wizard/status.js +94 -0
- package/lib/wizard/status.js.map +1 -0
- package/lib/wizard/token-store.js +71 -0
- package/lib/wizard/token-store.js.map +1 -0
- package/lib/wizard/tunnel-orchestrator.js +155 -0
- package/lib/wizard/tunnel-orchestrator.js.map +1 -0
- package/lib/wizard/verify.js +171 -0
- package/lib/wizard/verify.js.map +1 -0
- package/package.json +37 -37
- package/scripts/check-settings-form.mjs +5 -4
- package/scripts/check-wizard-ui.mjs +690 -0
- package/src/adapters/dsh-better-sidebar/index.ts +6 -2
- package/src/adapters/dsh-better-sidebar/methods.ts +3 -0
- package/src/admin-routes.ts +44 -2
- package/src/client/index.tsx +9 -0
- package/src/client/locales.ts +410 -2
- package/src/client/wizard/GatewayWizard.tsx +2220 -0
- package/src/client/wizard/WizardCard.tsx +81 -0
- package/src/client/wizard/cards/MigrateCard.tsx +42 -0
- package/src/client/wizard/cards/PortForwardCard.tsx +37 -0
- package/src/client/wizard/cards/ProxyCard.tsx +56 -0
- package/src/client/wizard/cards/TokenCard.tsx +81 -0
- package/src/client/wizard/register-wizard.ts +99 -0
- package/src/client/wizard/wizard-css.ts +948 -0
- package/src/client-data/qr.ts +284 -0
- package/src/client-data/remote-status.ts +128 -2
- package/src/client-data/settings-form.ts +12 -3
- package/src/client-data/wizard.ts +432 -0
- package/src/client-routes.ts +53 -0
- package/src/config.ts +87 -0
- package/src/index.ts +227 -2
- package/src/intercept.ts +43 -3
- package/src/merge-streams.ts +41 -12
- package/src/relay-client.ts +76 -0
- package/src/wizard/binary-io.ts +449 -0
- package/src/wizard/binary.ts +177 -0
- package/src/wizard/caddy-process.ts +228 -0
- package/src/wizard/caddy.ts +97 -0
- package/src/wizard/cloudflare.ts +413 -0
- package/src/wizard/cloudflared.ts +196 -0
- package/src/wizard/probe.ts +161 -0
- package/src/wizard/proxy-snippets.ts +128 -0
- package/src/wizard/routes.ts +537 -0
- package/src/wizard/source-detect.ts +87 -0
- package/src/wizard/status.ts +180 -0
- package/src/wizard/token-store.ts +73 -0
- package/src/wizard/tunnel-orchestrator.ts +197 -0
- package/src/wizard/verify.ts +236 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal QR encoder (GW-10): byte mode, versions 1–5, ECC level L, fixed
|
|
3
|
+
* mask 0 — enough for any sane origin URL (106 bytes at v5) while keeping
|
|
4
|
+
* the whole implementation single-block (no interleave) and reviewable.
|
|
5
|
+
* Returns the module matrix (true = dark) or null when the text does not
|
|
6
|
+
* fit. Pure: the check script verifies it at decoder level (RS syndromes
|
|
7
|
+
* all-zero + data recovery + format-info BCH), which is stronger than any
|
|
8
|
+
* memorized reference vector.
|
|
9
|
+
*
|
|
10
|
+
* Standard QR constants only: sizes 21/25/29/33/37, data codewords
|
|
11
|
+
* 19/34/55/80/108, ECC codewords 7/10/15/20/26, alignment centers
|
|
12
|
+
* [6,18]/[6,22]/[6,26]/[6,30], Galois field 0x11d, format-info generator
|
|
13
|
+
* 0x537 XOR-mask 0x5412.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface QrVersion {
|
|
17
|
+
readonly size: number
|
|
18
|
+
readonly dataCodewords: number
|
|
19
|
+
readonly eccCodewords: number
|
|
20
|
+
/** Alignment-pattern centers; v1 has none. */
|
|
21
|
+
readonly alignment: readonly number[] | null
|
|
22
|
+
/** Byte-mode payload capacity (data codewords minus mode+len header). */
|
|
23
|
+
readonly capacity: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const VERSIONS: readonly QrVersion[] = [
|
|
27
|
+
{ size: 21, dataCodewords: 19, eccCodewords: 7, alignment: null, capacity: 17 },
|
|
28
|
+
{ size: 25, dataCodewords: 34, eccCodewords: 10, alignment: [6, 18], capacity: 32 },
|
|
29
|
+
{ size: 29, dataCodewords: 55, eccCodewords: 15, alignment: [6, 22], capacity: 53 },
|
|
30
|
+
{ size: 33, dataCodewords: 80, eccCodewords: 20, alignment: [6, 26], capacity: 78 },
|
|
31
|
+
{ size: 37, dataCodewords: 108, eccCodewords: 26, alignment: [6, 30], capacity: 106 },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
// --- Galois field GF(256), polynomial 0x11d -----------------------------------
|
|
35
|
+
|
|
36
|
+
const EXP = new Uint8Array(512)
|
|
37
|
+
const LOG = new Uint8Array(256)
|
|
38
|
+
{
|
|
39
|
+
let value = 1
|
|
40
|
+
for (let exponent = 0; exponent < 255; exponent++) {
|
|
41
|
+
EXP[exponent] = value
|
|
42
|
+
LOG[value] = exponent
|
|
43
|
+
value <<= 1
|
|
44
|
+
if (value & 0x100) value ^= 0x11d
|
|
45
|
+
}
|
|
46
|
+
for (let exponent = 255; exponent < 512; exponent++) EXP[exponent] = EXP[exponent - 255] ?? 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function gfMul(a: number, b: number): number {
|
|
50
|
+
if (a === 0 || b === 0) return 0
|
|
51
|
+
const la = LOG[a]
|
|
52
|
+
const lb = LOG[b]
|
|
53
|
+
if (la === undefined || lb === undefined) return 0
|
|
54
|
+
return EXP[la + lb] ?? 0
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Reed–Solomon remainder of `data` under the generator for `eccLen` check
|
|
58
|
+
* codewords (LFSR division). The generator is ∏(x − α^i), i = 0…eccLen−1,
|
|
59
|
+
* coefficients high-order first; g[j]·x lands at index j, g[j]·α^i at
|
|
60
|
+
* index j+1. */
|
|
61
|
+
function rsRemainder(data: Uint8Array, eccLen: number): Uint8Array {
|
|
62
|
+
let generator: number[] = [1]
|
|
63
|
+
for (let i = 0; i < eccLen; i++) {
|
|
64
|
+
const next = new Array<number>(generator.length + 1).fill(0)
|
|
65
|
+
for (let j = 0; j < generator.length; j++) {
|
|
66
|
+
const coefficient = generator[j]
|
|
67
|
+
if (coefficient === undefined) continue
|
|
68
|
+
const low = next[j] ?? 0
|
|
69
|
+
const high = next[j + 1] ?? 0
|
|
70
|
+
next[j] = low ^ coefficient
|
|
71
|
+
next[j + 1] = high ^ gfMul(coefficient, EXP[i] ?? 0)
|
|
72
|
+
}
|
|
73
|
+
generator = next
|
|
74
|
+
}
|
|
75
|
+
const remainder = new Uint8Array(eccLen)
|
|
76
|
+
for (const byte of data) {
|
|
77
|
+
const factor = byte ^ (remainder[0] ?? 0)
|
|
78
|
+
remainder.copyWithin(0, 1)
|
|
79
|
+
remainder[eccLen - 1] = 0
|
|
80
|
+
if (factor !== 0) {
|
|
81
|
+
for (let i = 0; i < eccLen; i++) {
|
|
82
|
+
remainder[i] = (remainder[i] ?? 0) ^ gfMul(generator[i + 1] ?? 0, factor)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return remainder
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- bit assembly --------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
class BitBuffer {
|
|
92
|
+
readonly bits: number[] = []
|
|
93
|
+
|
|
94
|
+
push(value: number, length: number): void {
|
|
95
|
+
for (let shift = length - 1; shift >= 0; shift--) this.bits.push((value >>> shift) & 1)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildCodewords(text: Uint8Array, version: QrVersion): Uint8Array {
|
|
100
|
+
const buffer = new BitBuffer()
|
|
101
|
+
buffer.push(0b0100, 4) // byte mode
|
|
102
|
+
buffer.push(text.length, 8)
|
|
103
|
+
for (const byte of text) buffer.push(byte, 8)
|
|
104
|
+
// Terminator (up to 4 zero bits), then pad to a byte boundary.
|
|
105
|
+
buffer.push(0, Math.min(4, version.dataCodewords * 8 - buffer.bits.length))
|
|
106
|
+
while (buffer.bits.length % 8 !== 0) buffer.push(0, 1)
|
|
107
|
+
const bytes: number[] = []
|
|
108
|
+
for (let i = 0; i < buffer.bits.length; i += 8) {
|
|
109
|
+
let byte = 0
|
|
110
|
+
for (let j = 0; j < 8; j++) byte = (byte << 1) | (buffer.bits[i + j] ?? 0)
|
|
111
|
+
bytes.push(byte)
|
|
112
|
+
}
|
|
113
|
+
// Pad alternately 0xEC / 0x11 to the data capacity.
|
|
114
|
+
for (let pad = 0; bytes.length < version.dataCodewords; pad++) bytes.push(pad % 2 === 0 ? 0xec : 0x11)
|
|
115
|
+
const remainder = rsRemainder(Uint8Array.from(bytes), version.eccCodewords)
|
|
116
|
+
return Uint8Array.from([...bytes, ...remainder])
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- matrix ---------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
type Grid = (boolean | null)[][]
|
|
122
|
+
|
|
123
|
+
function blankGrid(size: number): Grid {
|
|
124
|
+
return Array.from({ length: size }, () => new Array<boolean | null>(size).fill(null))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function placeFinder(grid: Grid, row: number, col: number): void {
|
|
128
|
+
const n = grid.length
|
|
129
|
+
for (let dr = -1; dr <= 7; dr++) {
|
|
130
|
+
for (let dc = -1; dc <= 7; dc++) {
|
|
131
|
+
const r = row + dr
|
|
132
|
+
const c = col + dc
|
|
133
|
+
if (r < 0 || r >= n || c < 0 || c >= n) continue
|
|
134
|
+
const inRing = dr >= 0 && dr <= 6 && (dc === 0 || dc === 6)
|
|
135
|
+
const inRing2 = dc >= 0 && dc <= 6 && (dr === 0 || dr === 6)
|
|
136
|
+
const inCore = dr >= 2 && dr <= 4 && dc >= 2 && dc <= 4
|
|
137
|
+
const target = grid[r]
|
|
138
|
+
if (target !== undefined) target[c] = inRing || inRing2 || inCore
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function placeAlignment(grid: Grid, centers: readonly number[]): void {
|
|
144
|
+
for (const row of centers) {
|
|
145
|
+
for (const col of centers) {
|
|
146
|
+
// Skip the centers that sit inside a finder's 7×7 (only possible at
|
|
147
|
+
// the three corners; versions 2–5 have none).
|
|
148
|
+
let overlapsFinder = false
|
|
149
|
+
for (const [fr, fc] of [[0, 0], [0, grid.length - 7], [grid.length - 7, 0]] as const) {
|
|
150
|
+
if (row >= fr - 1 && row <= fr + 7 && col >= fc - 1 && col <= fc + 7) overlapsFinder = true
|
|
151
|
+
}
|
|
152
|
+
if (overlapsFinder) continue
|
|
153
|
+
for (let dr = -2; dr <= 2; dr++) {
|
|
154
|
+
for (let dc = -2; dc <= 2; dc++) {
|
|
155
|
+
const target = grid[row + dr]
|
|
156
|
+
if (target !== undefined) target[col + dc] = Math.max(Math.abs(dr), Math.abs(dc)) !== 1
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function reserveFormatAreas(grid: Grid): void {
|
|
164
|
+
const n = grid.length
|
|
165
|
+
// `false` marks RESERVED (kept from the data placer, overwritten by the
|
|
166
|
+
// real format bits later); null stays "free for data".
|
|
167
|
+
const put = (row: number, col: number, value: boolean): void => {
|
|
168
|
+
const target = grid[row]
|
|
169
|
+
if (target !== undefined) target[col] = value
|
|
170
|
+
}
|
|
171
|
+
for (let i = 0; i <= 8; i++) {
|
|
172
|
+
if (i !== 6) put(8, i, false)
|
|
173
|
+
if (i !== 6) put(i, 8, false)
|
|
174
|
+
}
|
|
175
|
+
for (let i = 0; i < 8; i++) {
|
|
176
|
+
put(n - 1 - i, 8, false)
|
|
177
|
+
// NOTE: unlike the first copy there is NO skip here — the second copy
|
|
178
|
+
// crosses no timing pattern, and leaving one cell unreserved (the
|
|
179
|
+
// first-copy rule pasted over) would let a data bit land inside it and
|
|
180
|
+
// desynchronize the whole stream (v2+).
|
|
181
|
+
put(8, n - 1 - i, false)
|
|
182
|
+
}
|
|
183
|
+
put(n - 8, 8, true) // the fixed dark module
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function formatBits(mask: number): number {
|
|
187
|
+
const data = (0b01 << 3) | mask // ECC level L
|
|
188
|
+
let rem = data << 10
|
|
189
|
+
for (let i = 4; i >= 0; i--) {
|
|
190
|
+
if ((rem >>> (i + 10)) & 1) rem ^= 0x537 << i
|
|
191
|
+
}
|
|
192
|
+
return ((data << 10) | rem) ^ 0x5412
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function placeFormat(grid: Grid, mask: number): void {
|
|
196
|
+
const n = grid.length
|
|
197
|
+
const bits = formatBits(mask)
|
|
198
|
+
// ISO/IEC 18004 Figure 25: bit 14 (MSB) sits at (8, 0) / (n - 1, 8) and
|
|
199
|
+
// bit 0 (LSB) sits at (0, 8) / (8, n - 1). Placing LSB-first reverses the
|
|
200
|
+
// 15-bit BCH codeword and makes every real scanner reject the symbol.
|
|
201
|
+
const bit = (index: number): boolean => ((bits >>> index) & 1) === 1
|
|
202
|
+
const put = (row: number, col: number, value: boolean): void => {
|
|
203
|
+
const target = grid[row]
|
|
204
|
+
if (target !== undefined) target[col] = value
|
|
205
|
+
}
|
|
206
|
+
for (let i = 0; i <= 5; i++) put(8, i, bit(14 - i))
|
|
207
|
+
put(8, 7, bit(8))
|
|
208
|
+
put(8, 8, bit(7))
|
|
209
|
+
put(7, 8, bit(6))
|
|
210
|
+
for (let i = 9; i <= 14; i++) put(14 - i, 8, bit(14 - i))
|
|
211
|
+
for (let i = 0; i <= 6; i++) put(n - 1 - i, 8, bit(14 - i))
|
|
212
|
+
for (let i = 7; i <= 14; i++) put(8, n - 15 + i, bit(14 - i))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function placeData(grid: Grid, codewords: Uint8Array): void {
|
|
216
|
+
const n = grid.length
|
|
217
|
+
// Fixed mask 0: (row + col) even flips. Any mask decodes; the penalty
|
|
218
|
+
// optimization is optional and this keeps the implementation small.
|
|
219
|
+
const maskFlips = (row: number, col: number): boolean => (row + col) % 2 === 0
|
|
220
|
+
let bitIndex = 7
|
|
221
|
+
let byteIndex = 0
|
|
222
|
+
let increment = -1
|
|
223
|
+
let row = n - 1
|
|
224
|
+
// The standard two-column zigzag: from the bottom-right corner upward,
|
|
225
|
+
// column PAIR by column PAIR, skipping the timing column pair (6).
|
|
226
|
+
for (let right = n - 1; right >= 0; right -= 2) {
|
|
227
|
+
if (right === 6) right = 5
|
|
228
|
+
// Fill the pair top-to-bottom or bottom-to-top until the edge.
|
|
229
|
+
while (true) {
|
|
230
|
+
const cells = grid[row]
|
|
231
|
+
// `row` is always in range here (the while loop flips before that can
|
|
232
|
+
// change); the guard only keeps a future invariant break non-fatal.
|
|
233
|
+
if (cells === undefined) break
|
|
234
|
+
for (let col = right; col >= right - 1; col--) {
|
|
235
|
+
const current = cells[col]
|
|
236
|
+
if (current !== true && current !== false) {
|
|
237
|
+
let dark = false
|
|
238
|
+
const byte = codewords[byteIndex]
|
|
239
|
+
if (byte !== undefined && byteIndex < codewords.length) dark = ((byte >>> bitIndex) & 1) === 1
|
|
240
|
+
if (maskFlips(row, col)) dark = !dark
|
|
241
|
+
cells[col] = dark
|
|
242
|
+
bitIndex -= 1
|
|
243
|
+
if (bitIndex === -1) {
|
|
244
|
+
byteIndex += 1
|
|
245
|
+
bitIndex = 7
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
row += increment
|
|
250
|
+
if (row < 0 || row >= n) {
|
|
251
|
+
row -= increment
|
|
252
|
+
increment = -increment
|
|
253
|
+
break
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The QR matrix for `text` (UTF-8, byte mode), or null when it does not fit
|
|
261
|
+
* into a version-5-L symbol (106 bytes).
|
|
262
|
+
*/
|
|
263
|
+
export function qrMatrix(text: string): boolean[][] | null {
|
|
264
|
+
const bytes = new TextEncoder().encode(text)
|
|
265
|
+
const version = VERSIONS.find((candidate) => candidate.capacity >= bytes.length)
|
|
266
|
+
if (version === undefined) return null
|
|
267
|
+
const grid = blankGrid(version.size)
|
|
268
|
+
placeFinder(grid, 0, 0)
|
|
269
|
+
placeFinder(grid, 0, version.size - 7)
|
|
270
|
+
placeFinder(grid, version.size - 7, 0)
|
|
271
|
+
if (version.alignment !== null) placeAlignment(grid, version.alignment)
|
|
272
|
+
// Timing patterns.
|
|
273
|
+
for (let i = 8; i < version.size - 8; i++) {
|
|
274
|
+
const timingRow = grid[6]
|
|
275
|
+
const column = grid[i]
|
|
276
|
+
if (timingRow !== undefined && timingRow[i] === null) timingRow[i] = i % 2 === 0
|
|
277
|
+
if (column !== undefined && column[6] === null) column[6] = i % 2 === 0
|
|
278
|
+
}
|
|
279
|
+
reserveFormatAreas(grid)
|
|
280
|
+
const codewords = buildCodewords(bytes, version)
|
|
281
|
+
placeData(grid, codewords)
|
|
282
|
+
placeFormat(grid, 0)
|
|
283
|
+
return grid.map((row) => row.map((cell) => cell === true))
|
|
284
|
+
}
|
|
@@ -35,6 +35,8 @@ import { zh } from '../client/locales.ts'
|
|
|
35
35
|
/** Same-origin client route feeding this store (host half: T34,
|
|
36
36
|
* src/client-routes.ts). */
|
|
37
37
|
export const REMOTE_STATUS_ROUTE = '/_dsh/zen-remote/client/remote-status'
|
|
38
|
+
/** the browser's network-restored hint (POST). */
|
|
39
|
+
export const NETWORK_HINT_ROUTE = '/_dsh/zen-remote/client/network-hint'
|
|
38
40
|
|
|
39
41
|
/** Same-origin client route closing one remote session from this machine. */
|
|
40
42
|
export const CLIENT_UNSHARE_ROUTE = '/_dsh/zen-remote/client/unshare'
|
|
@@ -224,6 +226,47 @@ export const documentVisibility: VisibilitySource = {
|
|
|
224
226
|
},
|
|
225
227
|
}
|
|
226
228
|
|
|
229
|
+
// --- the NETWORK-RESTORED hint source ---------------
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* the browser-side signal that a previously broken
|
|
233
|
+
* link MAY be back. Two triggers, both cheap and event-driven (never polled):
|
|
234
|
+
*
|
|
235
|
+
* - `window`'s `online` event — the browser's own reachability flag going
|
|
236
|
+
* true. Note this is a HINT, never proof: `navigator.onLine === true`
|
|
237
|
+
* means "an interface is up", not "the host is reachable". The host
|
|
238
|
+
* resolves the question with an authenticated `/ping` before anything
|
|
239
|
+
* reconnects.
|
|
240
|
+
* - `visibilitychange` → visible: a backgrounded tab (throttled timers, a
|
|
241
|
+
* possibly-changed network) coming back is the other moment a stale
|
|
242
|
+
* `offline` is most likely, and it costs one hint at most.
|
|
243
|
+
*
|
|
244
|
+
* Deliberately NOT wired: `window`'s `offline` event. Going offline needs no
|
|
245
|
+
* action (the poll and the streams already notice), and firing a hint there
|
|
246
|
+
* would only produce refused round-trips.
|
|
247
|
+
*/
|
|
248
|
+
export interface NetworkHintSource {
|
|
249
|
+
/** Attach; the returned function detaches. */
|
|
250
|
+
subscribe(listener: () => void): () => void
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The real source (Node / SSR: never notifying — a host has no navigator). */
|
|
254
|
+
export const windowNetworkHint: NetworkHintSource = {
|
|
255
|
+
subscribe: (listener) => {
|
|
256
|
+
if (typeof window === 'undefined' || typeof window.addEventListener !== 'function') return () => {}
|
|
257
|
+
const onOnline = (): void => { listener() }
|
|
258
|
+
const onVisibility = (): void => {
|
|
259
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'visible') listener()
|
|
260
|
+
}
|
|
261
|
+
window.addEventListener('online', onOnline)
|
|
262
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVisibility)
|
|
263
|
+
return () => {
|
|
264
|
+
window.removeEventListener('online', onOnline)
|
|
265
|
+
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVisibility)
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
}
|
|
269
|
+
|
|
227
270
|
/** How one `client/unshare` POST ended. A refused action carries the
|
|
228
271
|
* backend's error `code` when it sent one (`not-shared`, `remote-mismatch`,
|
|
229
272
|
* …). */
|
|
@@ -243,24 +286,42 @@ export interface RemoteStatusStore {
|
|
|
243
286
|
/** POST client/unshare for one VIRTUAL session id, then refresh.
|
|
244
287
|
* @returns the action's outcome. */
|
|
245
288
|
unshare(sessionId: string): Promise<RemoteUnshareOutcome>
|
|
289
|
+
|
|
290
|
+
/** fire one network-restored hint now (the same
|
|
291
|
+
* controlled round the window `online` event drives). Exposed for tests
|
|
292
|
+
* and for a host-side manual probe; never bypasses the host's probe. */
|
|
293
|
+
notifyNetworkRestored(): void
|
|
246
294
|
}
|
|
247
295
|
|
|
248
296
|
/** Poll cadence while the page is visible and at least one part is mounted
|
|
249
297
|
* (T34: 15 秒). */
|
|
250
298
|
export const REMOTE_STATUS_POLL_MS = 15_000
|
|
251
299
|
|
|
300
|
+
/** The minimum gap between two client-side network hints — the same 3s the
|
|
301
|
+
* host's `networkRestored()` enforces, so the two never fight. `hintGapMs`
|
|
302
|
+
* injects a smaller one (tests). */
|
|
303
|
+
const NETWORK_HINT_CLIENT_GAP_MS = 3_000
|
|
304
|
+
|
|
252
305
|
/**
|
|
253
306
|
* Build one store over an injectable fetch and visibility source — the
|
|
254
307
|
* shares-store discipline: latest-wins (an earlier request answering late
|
|
255
308
|
* never overwrites a newer table), every GET failure fails its own round,
|
|
256
309
|
* the timer is real while visible and gone while hidden, and becoming
|
|
257
310
|
* visible pulls once immediately. `pollMs` exists for tests: the shipped
|
|
258
|
-
* cadence is far too slow to observe.
|
|
311
|
+
* cadence is far too slow to observe. `hintGapMs` exists for the same
|
|
312
|
+
* reason: the shipped hint gap (default 3000) is far too slow to observe.
|
|
259
313
|
*/
|
|
260
314
|
export function createRemoteStatusStore(
|
|
261
315
|
fetchImpl: typeof fetch,
|
|
262
316
|
visibility: VisibilitySource = documentVisibility,
|
|
263
317
|
pollMs: number = REMOTE_STATUS_POLL_MS,
|
|
318
|
+
/** the network-restored hint source. Omitted → no
|
|
319
|
+
* hint wiring (a host mount, or a test). */
|
|
320
|
+
networkHint?: NetworkHintSource,
|
|
321
|
+
/** the minimum gap between two client-side hints, ms; default 3000 (the
|
|
322
|
+
* same anti-storm gap the host enforces). Exists for tests, like
|
|
323
|
+
* `pollMs`: the shipped gap is far too slow to observe. */
|
|
324
|
+
hintGapMs: number = NETWORK_HINT_CLIENT_GAP_MS,
|
|
264
325
|
): RemoteStatusStore {
|
|
265
326
|
const gate = createLatestGate()
|
|
266
327
|
const listeners = new Set<() => void>()
|
|
@@ -366,18 +427,71 @@ export function createRemoteStatusStore(
|
|
|
366
427
|
}
|
|
367
428
|
}
|
|
368
429
|
|
|
430
|
+
// ONE controlled round per hint.
|
|
431
|
+
//
|
|
432
|
+
// The chain is: hint → POST client/network-hint → host runs probeHealth()
|
|
433
|
+
// (authenticated GET /ping) → only a reachable host advances to
|
|
434
|
+
// networkRestored() (which is itself offline-only + 3s-gapped) → the next
|
|
435
|
+
// status poll reflects the new state, and the intercept layer's $zr/events
|
|
436
|
+
// leg reopens on its own once the relay reads online.
|
|
437
|
+
//
|
|
438
|
+
// THREE client-side guards keep a flapping machine from turning the hint
|
|
439
|
+
// into a poll: a healthy-link gate (the last status read said `online` —
|
|
440
|
+
// nothing to retire), an in-flight slot (one hint at a time), and a
|
|
441
|
+
// minimum gap (the same 3s the host enforces, so the two never fight).
|
|
442
|
+
let hintInFlight = false
|
|
443
|
+
let lastHintAt = 0
|
|
444
|
+
|
|
445
|
+
const sendNetworkHint = async (): Promise<void> => {
|
|
446
|
+
// The healthy-link gate: a hint exists to retire a stale `offline`. While
|
|
447
|
+
// the last status read says `online`, a tab switch or a browser `online`
|
|
448
|
+
// event must not spend the hint→ping→refresh triple. `view` undefined
|
|
449
|
+
// (no GET answered yet) passes — conservative before the first read.
|
|
450
|
+
if (snapshot.view?.state === 'online') return
|
|
451
|
+
if (hintInFlight) return
|
|
452
|
+
const now = Date.now()
|
|
453
|
+
if (now - lastHintAt < hintGapMs) return
|
|
454
|
+
lastHintAt = now
|
|
455
|
+
hintInFlight = true
|
|
456
|
+
try {
|
|
457
|
+
await fetchImpl(NETWORK_HINT_ROUTE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
|
|
458
|
+
} catch {
|
|
459
|
+
// A hint that cannot be delivered is simply dropped — the poll and the
|
|
460
|
+
// streams are still the truth, and the next event tries again.
|
|
461
|
+
} finally {
|
|
462
|
+
hintInFlight = false
|
|
463
|
+
// Whatever the hint achieved, re-read the status so the UI stops
|
|
464
|
+
// showing a stale `offline` the moment the host is back.
|
|
465
|
+
void refresh()
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
let offNetworkHint: (() => void) | undefined
|
|
470
|
+
const startHintWiring = (): void => {
|
|
471
|
+
if (networkHint === undefined || offNetworkHint !== undefined) return
|
|
472
|
+
offNetworkHint = networkHint.subscribe(() => { void sendNetworkHint() })
|
|
473
|
+
}
|
|
474
|
+
|
|
369
475
|
return {
|
|
370
476
|
getSnapshot: () => snapshot,
|
|
371
477
|
subscribe(listener: () => void): () => void {
|
|
372
478
|
listeners.add(listener)
|
|
373
479
|
syncPolling()
|
|
480
|
+
startHintWiring()
|
|
374
481
|
return () => {
|
|
375
482
|
listeners.delete(listener)
|
|
376
483
|
syncPolling()
|
|
484
|
+
// The hint listener follows the subscribers: no page, no hints.
|
|
485
|
+
if (listeners.size === 0 && offNetworkHint !== undefined) {
|
|
486
|
+
offNetworkHint()
|
|
487
|
+
offNetworkHint = undefined
|
|
488
|
+
}
|
|
377
489
|
}
|
|
378
490
|
},
|
|
379
491
|
refresh,
|
|
380
492
|
unshare,
|
|
493
|
+
/** explicit hint trigger (tests / manual probe). */
|
|
494
|
+
notifyNetworkRestored: () => { void sendNetworkHint() },
|
|
381
495
|
}
|
|
382
496
|
}
|
|
383
497
|
|
|
@@ -399,6 +513,18 @@ export function subscribeIfVirtual(store: RemoteStatusStore, sessionId: string,
|
|
|
399
513
|
* matter how many of the icon and banner parts are mounted at once. Created
|
|
400
514
|
* lazily over the real fetch, so check scripts never construct it. */
|
|
401
515
|
export function getRemoteStatusStore(): RemoteStatusStore {
|
|
402
|
-
|
|
516
|
+
// the REAL entry wiring. The network-hint source is
|
|
517
|
+
// passed here and nowhere else, so every mounted remote part (icon + banner)
|
|
518
|
+
// shares ONE store and therefore ONE hint subscription — no per-component
|
|
519
|
+
// listeners, no duplicate POSTs.
|
|
520
|
+
//
|
|
521
|
+
// Node / SSR: `windowNetworkHint.subscribe` returns a no-op detacher and
|
|
522
|
+
// never fires (there is no `window`), so a host mount is unaffected.
|
|
523
|
+
singleton ??= createRemoteStatusStore(
|
|
524
|
+
(input, init) => fetch(input, init),
|
|
525
|
+
documentVisibility,
|
|
526
|
+
REMOTE_STATUS_POLL_MS,
|
|
527
|
+
windowNetworkHint,
|
|
528
|
+
)
|
|
403
529
|
return singleton
|
|
404
530
|
}
|
|
@@ -403,10 +403,19 @@ export interface ClaimRouteBody {
|
|
|
403
403
|
retryAfterMs?: number
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
-
/**
|
|
407
|
-
*
|
|
406
|
+
/** The gateway's 32-character unambiguous pairing-code alphabet (no 0/O/1/I/L)
|
|
407
|
+
* and fixed 8-character length (`lib/lan-gate-server.cjs:330-335`). */
|
|
408
|
+
export const PAIRING_CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'
|
|
409
|
+
export const PAIRING_CODE_LENGTH = 8
|
|
410
|
+
|
|
411
|
+
/** What the pairing-code box keeps as its draft: uppercase, restricted to the
|
|
412
|
+
* 32-character pairing-code alphabet (excluding 0/O/1/I/L and symbols), and
|
|
413
|
+
* capped at 8 characters. */
|
|
408
414
|
export function normalizePairingCode(input: string): string {
|
|
409
|
-
return input
|
|
415
|
+
return input
|
|
416
|
+
.toUpperCase()
|
|
417
|
+
.replace(/[^ABCDEFGHJKMNPQRSTUVWXYZ23456789]/g, '')
|
|
418
|
+
.slice(0, PAIRING_CODE_LENGTH)
|
|
410
419
|
}
|
|
411
420
|
|
|
412
421
|
/**
|