@soyeht/soyeht 0.1.2 → 0.2.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/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/channel.ts +121 -19
- package/src/config.ts +3 -0
- package/src/http.ts +223 -41
- package/src/index.ts +16 -1
- package/src/outbound-queue.ts +230 -0
- package/src/pairing.ts +86 -10
- package/src/qr.ts +448 -0
- package/src/rpc.ts +54 -44
- package/src/service.ts +107 -1
- package/src/types.ts +52 -0
- package/src/version.ts +1 -1
package/src/qr.ts
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Minimal QR Code encoder — byte mode, EC level M, versions 1-20
|
|
3
|
+
// Zero external dependencies. Used for terminal rendering on service start.
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
// Galois Field GF(2^8) with primitive polynomial 0x11d
|
|
7
|
+
const EXP = new Uint8Array(256);
|
|
8
|
+
const LOG = new Uint8Array(256);
|
|
9
|
+
(function initGF() {
|
|
10
|
+
let v = 1;
|
|
11
|
+
for (let i = 0; i < 255; i++) {
|
|
12
|
+
EXP[i] = v;
|
|
13
|
+
LOG[v] = i;
|
|
14
|
+
v <<= 1;
|
|
15
|
+
if (v >= 256) v ^= 0x11d;
|
|
16
|
+
}
|
|
17
|
+
EXP[255] = EXP[0];
|
|
18
|
+
})();
|
|
19
|
+
|
|
20
|
+
function gfMul(a: number, b: number): number {
|
|
21
|
+
if (a === 0 || b === 0) return 0;
|
|
22
|
+
return EXP[(LOG[a] + LOG[b]) % 255];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function rsGenPoly(n: number): number[] {
|
|
26
|
+
let g = [1];
|
|
27
|
+
for (let i = 0; i < n; i++) {
|
|
28
|
+
const ng = new Array(g.length + 1).fill(0);
|
|
29
|
+
for (let j = 0; j < g.length; j++) {
|
|
30
|
+
ng[j] ^= g[j];
|
|
31
|
+
ng[j + 1] ^= gfMul(g[j], EXP[i]);
|
|
32
|
+
}
|
|
33
|
+
g = ng;
|
|
34
|
+
}
|
|
35
|
+
return g;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function rsEncode(data: number[], ecLen: number): number[] {
|
|
39
|
+
const gen = rsGenPoly(ecLen);
|
|
40
|
+
const msg = new Array(data.length + ecLen).fill(0);
|
|
41
|
+
for (let i = 0; i < data.length; i++) msg[i] = data[i];
|
|
42
|
+
for (let i = 0; i < data.length; i++) {
|
|
43
|
+
const coef = msg[i];
|
|
44
|
+
if (coef !== 0) {
|
|
45
|
+
for (let j = 0; j < gen.length; j++) {
|
|
46
|
+
msg[i + j] ^= gfMul(gen[j], coef);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return msg.slice(data.length);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Version table: [totalCodewords, ecPerBlock, blocks1, dataPerBlock1, blocks2, dataPerBlock2]
|
|
54
|
+
const VERSIONS: (number[] | null)[] = [
|
|
55
|
+
null,
|
|
56
|
+
[26, 10, 1, 16, 0, 0],
|
|
57
|
+
[44, 16, 1, 28, 0, 0],
|
|
58
|
+
[70, 26, 1, 44, 0, 0],
|
|
59
|
+
[100, 18, 2, 32, 0, 0],
|
|
60
|
+
[134, 24, 2, 43, 0, 0],
|
|
61
|
+
[172, 16, 4, 27, 0, 0],
|
|
62
|
+
[196, 18, 4, 31, 0, 0],
|
|
63
|
+
[242, 22, 4, 38, 0, 0],
|
|
64
|
+
[292, 22, 2, 36, 2, 37],
|
|
65
|
+
[346, 26, 4, 43, 1, 44],
|
|
66
|
+
[404, 30, 1, 50, 4, 51],
|
|
67
|
+
[466, 22, 6, 36, 2, 37],
|
|
68
|
+
[532, 22, 8, 37, 1, 38],
|
|
69
|
+
[581, 24, 4, 40, 5, 41],
|
|
70
|
+
[655, 24, 5, 41, 5, 42],
|
|
71
|
+
[733, 28, 7, 45, 3, 46],
|
|
72
|
+
[815, 28, 10, 46, 1, 47],
|
|
73
|
+
[901, 26, 9, 43, 4, 44],
|
|
74
|
+
[991, 26, 3, 44, 11, 45],
|
|
75
|
+
[1085, 28, 3, 41, 13, 42],
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const DATA_CAP = VERSIONS.map((v) => {
|
|
79
|
+
if (!v) return 0;
|
|
80
|
+
return v[2] * v[3] + (v[4] ? v[4] * v[5] : 0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
function chooseVersion(dataLen: number): number {
|
|
84
|
+
for (let v = 1; v <= 20; v++) {
|
|
85
|
+
if (DATA_CAP[v] >= dataLen) return v;
|
|
86
|
+
}
|
|
87
|
+
return -1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function encodeToBytes(str: string): number[] {
|
|
91
|
+
return Array.from(Buffer.from(str, "utf8"));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildBitstream(dataBytes: number[], version: number): number[] {
|
|
95
|
+
const bits: number[] = [];
|
|
96
|
+
function push(val: number, len: number) {
|
|
97
|
+
for (let i = len - 1; i >= 0; i--) bits.push((val >> i) & 1);
|
|
98
|
+
}
|
|
99
|
+
push(4, 4); // byte mode
|
|
100
|
+
const ccBits = version <= 9 ? 8 : 16;
|
|
101
|
+
push(dataBytes.length, ccBits);
|
|
102
|
+
for (const b of dataBytes) push(b, 8);
|
|
103
|
+
const totalBits = DATA_CAP[version] * 8;
|
|
104
|
+
push(0, Math.min(4, totalBits - bits.length));
|
|
105
|
+
while (bits.length % 8 !== 0) bits.push(0);
|
|
106
|
+
const padBytes = [0xec, 0x11];
|
|
107
|
+
let pi = 0;
|
|
108
|
+
while (bits.length < totalBits) {
|
|
109
|
+
push(padBytes[pi], 8);
|
|
110
|
+
pi = (pi + 1) % 2;
|
|
111
|
+
}
|
|
112
|
+
const out: number[] = [];
|
|
113
|
+
for (let i = 0; i < bits.length; i += 8) {
|
|
114
|
+
let b = 0;
|
|
115
|
+
for (let j = 0; j < 8; j++) b = (b << 1) | bits[i + j];
|
|
116
|
+
out.push(b);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function interleaveBlocks(data: number[], version: number): number[] {
|
|
122
|
+
const vInfo = VERSIONS[version]!;
|
|
123
|
+
const ecPerBlock = vInfo[1];
|
|
124
|
+
const blocks1 = vInfo[2], dPerBlock1 = vInfo[3];
|
|
125
|
+
const blocks2 = vInfo[4] || 0, dPerBlock2 = vInfo[5] || 0;
|
|
126
|
+
const dataBlocks: number[][] = [];
|
|
127
|
+
const ecBlocks: number[][] = [];
|
|
128
|
+
let offset = 0;
|
|
129
|
+
for (let i = 0; i < blocks1; i++) {
|
|
130
|
+
const block = data.slice(offset, offset + dPerBlock1);
|
|
131
|
+
dataBlocks.push(block);
|
|
132
|
+
ecBlocks.push(rsEncode(block, ecPerBlock));
|
|
133
|
+
offset += dPerBlock1;
|
|
134
|
+
}
|
|
135
|
+
for (let i = 0; i < blocks2; i++) {
|
|
136
|
+
const block = data.slice(offset, offset + dPerBlock2);
|
|
137
|
+
dataBlocks.push(block);
|
|
138
|
+
ecBlocks.push(rsEncode(block, ecPerBlock));
|
|
139
|
+
offset += dPerBlock2;
|
|
140
|
+
}
|
|
141
|
+
const result: number[] = [];
|
|
142
|
+
const maxDataLen = Math.max(dPerBlock1, dPerBlock2 || 0);
|
|
143
|
+
for (let i = 0; i < maxDataLen; i++) {
|
|
144
|
+
for (const block of dataBlocks) {
|
|
145
|
+
if (i < block.length) result.push(block[i]);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (let i = 0; i < ecPerBlock; i++) {
|
|
149
|
+
for (const block of ecBlocks) {
|
|
150
|
+
if (i < block.length) result.push(block[i]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const SIZE_FOR_VERSION = (v: number) => 17 + v * 4;
|
|
157
|
+
|
|
158
|
+
function createMatrix(version: number): number[][] {
|
|
159
|
+
const size = SIZE_FOR_VERSION(version);
|
|
160
|
+
return Array.from({ length: size }, () => new Array(size).fill(-1));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function setMod(m: number[][], r: number, c: number, val: number) {
|
|
164
|
+
if (r >= 0 && r < m.length && c >= 0 && c < m.length) m[r][c] = val;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function placeFinderPattern(m: number[][], row: number, col: number) {
|
|
168
|
+
for (let r = -1; r <= 7; r++) {
|
|
169
|
+
for (let c = -1; c <= 7; c++) {
|
|
170
|
+
let val: number;
|
|
171
|
+
if (r === -1 || r === 7 || c === -1 || c === 7) val = 0;
|
|
172
|
+
else if (r === 0 || r === 6 || c === 0 || c === 6) val = 1;
|
|
173
|
+
else if (r >= 2 && r <= 4 && c >= 2 && c <= 4) val = 1;
|
|
174
|
+
else val = 0;
|
|
175
|
+
setMod(m, row + r, col + c, val);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const ALIGN_POS: (number[] | null)[] = [
|
|
181
|
+
null, [],
|
|
182
|
+
[6, 18], [6, 22], [6, 26], [6, 30], [6, 34],
|
|
183
|
+
[6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50],
|
|
184
|
+
[6, 30, 54], [6, 32, 58], [6, 34, 62], [6, 26, 46, 66],
|
|
185
|
+
[6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78],
|
|
186
|
+
[6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90],
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
function placeAlignmentPatterns(m: number[][], version: number) {
|
|
190
|
+
if (version < 2) return;
|
|
191
|
+
const pos = ALIGN_POS[version]!;
|
|
192
|
+
for (const r of pos) {
|
|
193
|
+
for (const c of pos) {
|
|
194
|
+
if (m[r][c] !== -1) continue;
|
|
195
|
+
for (let dr = -2; dr <= 2; dr++) {
|
|
196
|
+
for (let dc = -2; dc <= 2; dc++) {
|
|
197
|
+
const val = (Math.abs(dr) === 2 || Math.abs(dc) === 2 || (dr === 0 && dc === 0)) ? 1 : 0;
|
|
198
|
+
setMod(m, r + dr, c + dc, val);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function placeTimingPatterns(m: number[][]) {
|
|
206
|
+
const size = m.length;
|
|
207
|
+
for (let i = 8; i < size - 8; i++) {
|
|
208
|
+
if (m[6][i] === -1) m[6][i] = (i % 2 === 0) ? 1 : 0;
|
|
209
|
+
if (m[i][6] === -1) m[i][6] = (i % 2 === 0) ? 1 : 0;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function reserveFormatBits(m: number[][]) {
|
|
214
|
+
const size = m.length;
|
|
215
|
+
for (let i = 0; i <= 8; i++) {
|
|
216
|
+
if (m[8][i] === -1) m[8][i] = 0;
|
|
217
|
+
if (m[i][8] === -1) m[i][8] = 0;
|
|
218
|
+
}
|
|
219
|
+
for (let i = 0; i <= 7; i++) {
|
|
220
|
+
if (m[8][size - 1 - i] === -1) m[8][size - 1 - i] = 0;
|
|
221
|
+
}
|
|
222
|
+
for (let i = 0; i <= 7; i++) {
|
|
223
|
+
if (m[size - 1 - i][8] === -1) m[size - 1 - i][8] = 0;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function reserveVersionBits(m: number[][], version: number) {
|
|
228
|
+
if (version < 7) return;
|
|
229
|
+
const size = m.length;
|
|
230
|
+
for (let i = 0; i < 6; i++) {
|
|
231
|
+
for (let j = 0; j < 3; j++) {
|
|
232
|
+
if (m[i][size - 11 + j] === -1) m[i][size - 11 + j] = 0;
|
|
233
|
+
if (m[size - 11 + j][i] === -1) m[size - 11 + j][i] = 0;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function placeDataBits(m: number[][], dataBits: number[]) {
|
|
239
|
+
const size = m.length;
|
|
240
|
+
let bitIdx = 0;
|
|
241
|
+
let upward = true;
|
|
242
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
243
|
+
if (right === 6) right = 5;
|
|
244
|
+
for (let cnt = 0; cnt < size; cnt++) {
|
|
245
|
+
const row = upward ? (size - 1 - cnt) : cnt;
|
|
246
|
+
for (let dx = 0; dx <= 1; dx++) {
|
|
247
|
+
const col = right - dx;
|
|
248
|
+
if (col < 0 || col >= size) continue;
|
|
249
|
+
if (m[row][col] !== -1) continue;
|
|
250
|
+
m[row][col] = (bitIdx < dataBits.length) ? dataBits[bitIdx] : 0;
|
|
251
|
+
bitIdx++;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
upward = !upward;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const MASK_FNS: ((r: number, c: number) => boolean)[] = [
|
|
259
|
+
(r, c) => (r + c) % 2 === 0,
|
|
260
|
+
(r) => r % 2 === 0,
|
|
261
|
+
(_, c) => c % 3 === 0,
|
|
262
|
+
(r, c) => (r + c) % 3 === 0,
|
|
263
|
+
(r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0,
|
|
264
|
+
(r, c) => (r * c) % 2 + (r * c) % 3 === 0,
|
|
265
|
+
(r, c) => ((r * c) % 2 + (r * c) % 3) % 2 === 0,
|
|
266
|
+
(r, c) => ((r + c) % 2 + (r * c) % 3) % 2 === 0,
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
function applyMask(m: number[][], reserved: number[][], maskIdx: number) {
|
|
270
|
+
const fn = MASK_FNS[maskIdx];
|
|
271
|
+
const size = m.length;
|
|
272
|
+
for (let r = 0; r < size; r++) {
|
|
273
|
+
for (let c = 0; c < size; c++) {
|
|
274
|
+
if (reserved[r][c] === -1 && fn(r, c)) m[r][c] ^= 1;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function scorePenalty(m: number[][]): number {
|
|
280
|
+
const size = m.length;
|
|
281
|
+
let score = 0;
|
|
282
|
+
for (let r = 0; r < size; r++) {
|
|
283
|
+
let run = 1;
|
|
284
|
+
for (let c = 1; c < size; c++) {
|
|
285
|
+
if (m[r][c] === m[r][c - 1]) run++;
|
|
286
|
+
else { if (run >= 5) score += run - 2; run = 1; }
|
|
287
|
+
}
|
|
288
|
+
if (run >= 5) score += run - 2;
|
|
289
|
+
}
|
|
290
|
+
for (let c = 0; c < size; c++) {
|
|
291
|
+
let run = 1;
|
|
292
|
+
for (let r = 1; r < size; r++) {
|
|
293
|
+
if (m[r][c] === m[r - 1][c]) run++;
|
|
294
|
+
else { if (run >= 5) score += run - 2; run = 1; }
|
|
295
|
+
}
|
|
296
|
+
if (run >= 5) score += run - 2;
|
|
297
|
+
}
|
|
298
|
+
for (let r = 0; r < size - 1; r++) {
|
|
299
|
+
for (let c = 0; c < size - 1; c++) {
|
|
300
|
+
const v = m[r][c];
|
|
301
|
+
if (v === m[r][c + 1] && v === m[r + 1][c] && v === m[r + 1][c + 1]) score += 3;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return score;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// EC level M format info table (pre-computed with BCH)
|
|
308
|
+
const FORMAT_BITS: number[] = [
|
|
309
|
+
0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0,
|
|
310
|
+
0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976,
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
function placeFormatBits(m: number[][], maskIdx: number) {
|
|
314
|
+
const formatInfo = FORMAT_BITS[maskIdx]; // EC level M = index 0..7
|
|
315
|
+
const size = m.length;
|
|
316
|
+
const bits: number[] = [];
|
|
317
|
+
for (let i = 14; i >= 0; i--) bits.push((formatInfo >> i) & 1);
|
|
318
|
+
const pos1: [number, number][] = [
|
|
319
|
+
[0, 8], [1, 8], [2, 8], [3, 8], [4, 8], [5, 8], [7, 8], [8, 8],
|
|
320
|
+
[8, 7], [8, 5], [8, 4], [8, 3], [8, 2], [8, 1], [8, 0],
|
|
321
|
+
];
|
|
322
|
+
for (let i = 0; i < 15; i++) m[pos1[i][0]][pos1[i][1]] = bits[i];
|
|
323
|
+
for (let i = 0; i < 7; i++) m[size - 1 - i][8] = bits[i];
|
|
324
|
+
for (let i = 7; i < 15; i++) m[8][size - 15 + i] = bits[i];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const VERSION_INFO: number[] = [
|
|
328
|
+
0, 0, 0, 0, 0, 0, 0,
|
|
329
|
+
0x07c94, 0x085bc, 0x09a99, 0x0a4d3, 0x0bbf6, 0x0c762, 0x0d847,
|
|
330
|
+
0x0e60d, 0x0f928, 0x10b78, 0x1145d, 0x12a17, 0x13532, 0x149a6,
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
function placeVersionBits(m: number[][], version: number) {
|
|
334
|
+
if (version < 7) return;
|
|
335
|
+
const info = VERSION_INFO[version];
|
|
336
|
+
const size = m.length;
|
|
337
|
+
for (let i = 0; i < 18; i++) {
|
|
338
|
+
const bit = (info >> i) & 1;
|
|
339
|
+
const r = Math.floor(i / 3);
|
|
340
|
+
const c = size - 11 + (i % 3);
|
|
341
|
+
m[r][c] = bit;
|
|
342
|
+
m[c][r] = bit;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
// Public API
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
export type QRResult = {
|
|
351
|
+
matrix: number[][];
|
|
352
|
+
size: number;
|
|
353
|
+
version: number;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
export function generateQR(text: string): QRResult | null {
|
|
357
|
+
const dataBytes = encodeToBytes(text);
|
|
358
|
+
let overhead = Math.ceil((4 + 8) / 8);
|
|
359
|
+
let version = chooseVersion(dataBytes.length + overhead);
|
|
360
|
+
if (version === -1) {
|
|
361
|
+
overhead = Math.ceil((4 + 16) / 8);
|
|
362
|
+
version = chooseVersion(dataBytes.length + overhead);
|
|
363
|
+
}
|
|
364
|
+
if (version === -1) return null;
|
|
365
|
+
|
|
366
|
+
const ccBits = version <= 9 ? 8 : 16;
|
|
367
|
+
const needed = Math.ceil((4 + ccBits + dataBytes.length * 8) / 8);
|
|
368
|
+
if (needed > DATA_CAP[version]) {
|
|
369
|
+
version++;
|
|
370
|
+
if (version > 20) return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const bitstream = buildBitstream(dataBytes, version);
|
|
374
|
+
const codewords = interleaveBlocks(bitstream, version);
|
|
375
|
+
const dataBits: number[] = [];
|
|
376
|
+
for (const cw of codewords) {
|
|
377
|
+
for (let j = 7; j >= 0; j--) dataBits.push((cw >> j) & 1);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const m = createMatrix(version);
|
|
381
|
+
placeFinderPattern(m, 0, 0);
|
|
382
|
+
placeFinderPattern(m, 0, m.length - 7);
|
|
383
|
+
placeFinderPattern(m, m.length - 7, 0);
|
|
384
|
+
placeAlignmentPatterns(m, version);
|
|
385
|
+
placeTimingPatterns(m);
|
|
386
|
+
m[(4 * version) + 9][8] = 1; // dark module
|
|
387
|
+
reserveFormatBits(m);
|
|
388
|
+
reserveVersionBits(m, version);
|
|
389
|
+
|
|
390
|
+
const reserved = m.map((row) => row.slice());
|
|
391
|
+
placeDataBits(m, dataBits);
|
|
392
|
+
|
|
393
|
+
let bestMask = 0;
|
|
394
|
+
let bestScore = Infinity;
|
|
395
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
396
|
+
const trial = m.map((row) => row.slice());
|
|
397
|
+
applyMask(trial, reserved, mask);
|
|
398
|
+
placeFormatBits(trial, mask);
|
|
399
|
+
placeVersionBits(trial, version);
|
|
400
|
+
const s = scorePenalty(trial);
|
|
401
|
+
if (s < bestScore) { bestScore = s; bestMask = mask; }
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
applyMask(m, reserved, bestMask);
|
|
405
|
+
placeFormatBits(m, bestMask);
|
|
406
|
+
placeVersionBits(m, version);
|
|
407
|
+
|
|
408
|
+
return { matrix: m, size: m.length, version };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
// Terminal renderer — ANSI colors, half-block characters
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
|
|
415
|
+
export function renderQrTerminal(text: string): string | null {
|
|
416
|
+
const qr = generateQR(text);
|
|
417
|
+
if (!qr) return null;
|
|
418
|
+
|
|
419
|
+
const { matrix, size } = qr;
|
|
420
|
+
const quiet = 2;
|
|
421
|
+
const total = size + quiet * 2;
|
|
422
|
+
|
|
423
|
+
function mod(r: number, c: number): number {
|
|
424
|
+
if (r < quiet || r >= size + quiet || c < quiet || c >= size + quiet) return 0;
|
|
425
|
+
return matrix[r - quiet][c - quiet];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ▄ (lower half block): foreground = lower half, background = upper half
|
|
429
|
+
function cell(top: number, bottom: number): string {
|
|
430
|
+
if (top === 1 && bottom === 1) return "\x1b[40m \x1b[0m"; // both black
|
|
431
|
+
if (top === 0 && bottom === 0) return "\x1b[47m \x1b[0m"; // both white
|
|
432
|
+
if (top === 1 && bottom === 0) return "\x1b[37;40m\u2584\x1b[0m"; // top black, bottom white
|
|
433
|
+
return "\x1b[30;47m\u2584\x1b[0m"; // top white, bottom black
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const lines: string[] = [];
|
|
437
|
+
for (let r = 0; r < total; r += 2) {
|
|
438
|
+
let line = "";
|
|
439
|
+
for (let c = 0; c < total; c++) {
|
|
440
|
+
const top = mod(r, c);
|
|
441
|
+
const bottom = (r + 1 < total) ? mod(r + 1, c) : 0;
|
|
442
|
+
line += cell(top, bottom);
|
|
443
|
+
}
|
|
444
|
+
lines.push(line);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return lines.join("\n");
|
|
448
|
+
}
|
package/src/rpc.ts
CHANGED
|
@@ -9,11 +9,11 @@ import {
|
|
|
9
9
|
normalizeAccountId,
|
|
10
10
|
} from "./config.js";
|
|
11
11
|
import {
|
|
12
|
-
deliverTextMessage,
|
|
13
12
|
postToBackend,
|
|
14
13
|
buildOutboundEnvelope,
|
|
15
14
|
type PostToBackendOptions,
|
|
16
15
|
} from "./outbound.js";
|
|
16
|
+
import { encryptEnvelopeV2 } from "./envelope-v2.js";
|
|
17
17
|
import type { TextMessagePayload } from "./types.js";
|
|
18
18
|
import {
|
|
19
19
|
base64UrlDecode,
|
|
@@ -115,14 +115,6 @@ export function handleNotify(
|
|
|
115
115
|
return;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
if (!account.backendBaseUrl || !account.pluginAuthToken) {
|
|
119
|
-
respond(false, undefined, {
|
|
120
|
-
code: "NOT_CONFIGURED",
|
|
121
|
-
message: "Account is not fully configured",
|
|
122
|
-
});
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
118
|
const to = params["to"] as string;
|
|
127
119
|
const text = params["text"] as string;
|
|
128
120
|
if (!to || !text) {
|
|
@@ -133,54 +125,67 @@ export function handleNotify(
|
|
|
133
125
|
return;
|
|
134
126
|
}
|
|
135
127
|
|
|
136
|
-
if
|
|
128
|
+
// Direct mode: encrypt and enqueue if V2 session exists
|
|
129
|
+
if (v2deps) {
|
|
137
130
|
const ratchetSession = v2deps.sessions.get(account.accountId);
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
131
|
+
if (ratchetSession) {
|
|
132
|
+
if (ratchetSession.expiresAt < Date.now()) {
|
|
133
|
+
respond(false, undefined, {
|
|
134
|
+
code: "SESSION_EXPIRED",
|
|
135
|
+
message: "V2 session has expired, re-handshake required",
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const message: TextMessagePayload = { contentType: "text", text };
|
|
141
|
+
const envelope = buildOutboundEnvelope(account.accountId, to, message);
|
|
142
|
+
const { envelope: v2env, updatedSession } = encryptEnvelopeV2({
|
|
143
|
+
session: ratchetSession,
|
|
144
|
+
accountId: account.accountId,
|
|
145
|
+
plaintext: JSON.stringify(envelope),
|
|
146
|
+
dhRatchetCfg: {
|
|
147
|
+
intervalMessages: account.security.dhRatchetIntervalMessages,
|
|
148
|
+
intervalMs: account.security.dhRatchetIntervalMs,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
v2deps.sessions.set(account.accountId, updatedSession);
|
|
152
|
+
const entry = v2deps.outboundQueue.enqueue(account.accountId, v2env);
|
|
153
|
+
respond(true, {
|
|
154
|
+
deliveryId: envelope.deliveryId,
|
|
155
|
+
meta: { transportMode: "direct", queueEntryId: entry.id },
|
|
142
156
|
});
|
|
143
157
|
return;
|
|
144
158
|
}
|
|
145
159
|
|
|
146
|
-
if (
|
|
160
|
+
if (account.security.enabled) {
|
|
147
161
|
respond(false, undefined, {
|
|
148
|
-
code: "
|
|
149
|
-
message: "V2 session
|
|
162
|
+
code: "SESSION_REQUIRED",
|
|
163
|
+
message: "V2 session required for secure delivery",
|
|
150
164
|
});
|
|
151
165
|
return;
|
|
152
166
|
}
|
|
167
|
+
}
|
|
153
168
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
intervalMs: account.security.dhRatchetIntervalMs,
|
|
161
|
-
},
|
|
162
|
-
onSessionUpdated: (updated) => v2deps.sessions.set(account.accountId, updated),
|
|
163
|
-
securityEnabled: true,
|
|
164
|
-
};
|
|
165
|
-
const result = await postToBackend(
|
|
166
|
-
account.backendBaseUrl,
|
|
167
|
-
account.pluginAuthToken,
|
|
168
|
-
envelope,
|
|
169
|
-
opts,
|
|
170
|
-
);
|
|
171
|
-
respond(true, { deliveryId: result.messageId, meta: result.meta });
|
|
169
|
+
// Backend mode fallback
|
|
170
|
+
if (!account.backendBaseUrl || !account.pluginAuthToken) {
|
|
171
|
+
respond(false, undefined, {
|
|
172
|
+
code: "NOT_CONFIGURED",
|
|
173
|
+
message: "Account is not fully configured (no session and no backend)",
|
|
174
|
+
});
|
|
172
175
|
return;
|
|
173
176
|
}
|
|
174
177
|
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
178
|
+
const message: TextMessagePayload = { contentType: "text", text };
|
|
179
|
+
const envelope = buildOutboundEnvelope(account.accountId, to, message);
|
|
180
|
+
const opts: PostToBackendOptions = {
|
|
181
|
+
securityEnabled: false,
|
|
182
|
+
};
|
|
183
|
+
const result = await postToBackend(
|
|
184
|
+
account.backendBaseUrl,
|
|
185
|
+
account.pluginAuthToken,
|
|
186
|
+
envelope,
|
|
187
|
+
opts,
|
|
188
|
+
);
|
|
184
189
|
respond(true, { deliveryId: result.messageId, meta: result.meta });
|
|
185
190
|
};
|
|
186
191
|
}
|
|
@@ -456,6 +461,10 @@ export function handleSecurityHandshakeFinish(
|
|
|
456
461
|
});
|
|
457
462
|
}
|
|
458
463
|
|
|
464
|
+
// Revoke old stream tokens and issue a fresh one for SSE auth
|
|
465
|
+
v2deps.outboundQueue.revokeStreamTokensForAccount(accountId);
|
|
466
|
+
const streamToken = v2deps.outboundQueue.createStreamToken(accountId, pending.sessionExpiresAt);
|
|
467
|
+
|
|
459
468
|
api.logger.info("[soyeht] V2 handshake completed", { accountId });
|
|
460
469
|
|
|
461
470
|
respond(true, {
|
|
@@ -463,6 +472,7 @@ export function handleSecurityHandshakeFinish(
|
|
|
463
472
|
phase: "finish",
|
|
464
473
|
complete: true,
|
|
465
474
|
expiresAt: pending.sessionExpiresAt,
|
|
475
|
+
streamToken,
|
|
466
476
|
});
|
|
467
477
|
};
|
|
468
478
|
}
|