dsh-mobile-plugin 0.5.22 → 0.5.24

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.
@@ -0,0 +1,107 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * 一次性入场券(transfer ticket)的进程内 TTL 表。
5
+ *
6
+ * 设备批量导入/导出时,手机 A 向某台电脑申领一张入场券,塞进二维码交给手机 B;B 拿这张券
7
+ * 走一次正常配对(`/pair` 带 `ticketId`),配对成功券即作废。券的内容与配对票据同一种原语
8
+ * (32 字节 base64url,用来派生 pairKey),但**不进 state、不落盘**——它只活在这台电脑的内存
9
+ * 里、只活很短一段时间(默认 10 分钟)。
10
+ *
11
+ * 为什么一次性 + 短 TTL:券等价于「可以跟这台电脑配一次对」的资格。二维码要在屏幕上长显,
12
+ * 截图 / 偷看都可能发生;配对成功立刻 `consume`,同一张券就配不了第二台手机;短 TTL 让一张
13
+ * 没被用掉的券自己过期。这是拿「可用窗口」换「被截屏后的暴露面」的取舍:窗口之外,即使券
14
+ * 泄露也换不到任何东西。
15
+ *
16
+ * 进程内表:插件重载即清空——券本来就是为了下一次配对临时生成的,没配上的重扫一张新的即可。
17
+ */
18
+
19
+ const crypto = require('node:crypto');
20
+
21
+ /** 默认有效期:10 分钟。够用户扫完码、走完配对,又不至于让一张漏出去的券长期可用。 */
22
+ const DEFAULT_TTL_MS = 10 * 60 * 1000;
23
+
24
+ /** 单次最多发几张:一张码换一台新手机,一次导出最多扩 8 台,再多该分批或先看是不是刷券。 */
25
+ const MAX_COUNT = 8;
26
+
27
+ /** 单张券有效期上限(24 小时):自动同步收件箱里的券要活到后开 App 的手机来取,封顶一天。 */
28
+ const MAX_TTL_MS = 24 * 60 * 60 * 1000;
29
+
30
+ /** 键的分隔符:instanceId 与 ticketId 都来自内部生成,用不可见字符只为拼出唯一键。 */
31
+ const SEP = '\u0000';
32
+
33
+ /**
34
+ * 建一张券表。默认用真实时钟与 10 分钟 TTL;测试可注入 `now` / `ttlMs` 观察过期。
35
+ *
36
+ * @param {object} [options]
37
+ * @param {number} [options.ttlMs] 券的有效期(毫秒)
38
+ * @param {() => number} [options.now] 当前时刻的取值器(测试用)
39
+ */
40
+ function createTransferTickets(options = {}) {
41
+ const now = typeof options.now === 'function' ? options.now : Date.now;
42
+ const ttlMs = Number.isFinite(options.ttlMs) && options.ttlMs > 0 ? options.ttlMs : DEFAULT_TTL_MS;
43
+
44
+ /** 键 → { ticket, expiresAt }。键把实例号也编进去:两张不同宿主的券不会互相顶掉。 */
45
+ const table = new Map();
46
+ const keyOf = (instanceId, ticketId) => `${String(instanceId || '')}${SEP}${ticketId}`;
47
+
48
+ /**
49
+ * 发券:返回 [{ ticket, ticketId }],`count` 默认 1、封顶 `MAX_COUNT`。
50
+ *
51
+ * `opts.ttlMs` 覆盖单张券的有效期(封顶 `MAX_TTL_MS`);`opts.reusable` 为真时这张券
52
+ * **可被多台手机各用一次、配对成功后不作废**(到期才失效)。两种券用途不同:
53
+ * · 导出二维码里的是**一次性短券**(默认),防截图;
54
+ * · 自动同步收件箱里的是**可复用长券**(24h 内多台组内手机各领一次),因为它只经 SGK
55
+ * 密文投递、不暴露给外人,短 TTL 反而会让后开 App 的手机永远配不上、多台手机只有
56
+ * 第一台抢得到。
57
+ */
58
+ function mint(instanceId, count, opts = {}) {
59
+ prune();
60
+ const n = Number.isInteger(count) && count > 0 ? Math.min(count, MAX_COUNT) : 1;
61
+ const wantTtl = Number.isFinite(opts.ttlMs) && opts.ttlMs > 0 ? opts.ttlMs : ttlMs;
62
+ const life = Math.min(wantTtl, MAX_TTL_MS);
63
+ const reusable = opts.reusable === true;
64
+ const issued = [];
65
+ const expiresAt = now() + life;
66
+ for (let i = 0; i < n; i += 1) {
67
+ const ticket = crypto.randomBytes(32).toString('base64url');
68
+ const ticketId = crypto.randomBytes(16).toString('base64url');
69
+ table.set(keyOf(instanceId, ticketId), { ticket, expiresAt, reusable });
70
+ issued.push({ ticket, ticketId });
71
+ }
72
+ return issued;
73
+ }
74
+
75
+ /** 取券:不存在或已过期都返回 null(过期的顺手清掉)。 */
76
+ function resolve(instanceId, ticketId) {
77
+ if (typeof ticketId !== 'string' || !ticketId) return null;
78
+ const key = keyOf(instanceId, ticketId);
79
+ const entry = table.get(key);
80
+ if (!entry) return null;
81
+ if (entry.expiresAt <= now()) {
82
+ table.delete(key);
83
+ return null;
84
+ }
85
+ return entry.ticket;
86
+ }
87
+
88
+ /** 用掉一张券:一次性券直接删除;可复用券不动它(到期由 prune 清)。 */
89
+ function consume(instanceId, ticketId) {
90
+ if (typeof ticketId !== 'string' || !ticketId) return;
91
+ const key = keyOf(instanceId, ticketId);
92
+ const entry = table.get(key);
93
+ if (entry && entry.reusable) return;
94
+ table.delete(key);
95
+ }
96
+
97
+ /** 清掉所有过期券;每次 mint / 可在别处定期调用。 */
98
+ function prune(at = now()) {
99
+ for (const [key, entry] of table) {
100
+ if (entry.expiresAt <= at) table.delete(key);
101
+ }
102
+ }
103
+
104
+ return { mint, resolve, consume, prune, size: () => table.size };
105
+ }
106
+
107
+ module.exports = { createTransferTickets, DEFAULT_TTL_MS, MAX_COUNT };