dsh-voice 0.4.3 → 0.5.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/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@
4
4
 
5
5
  [历史英文记录](docs/CHANGELOG.en.md)
6
6
 
7
+ ## 0.5.0 (2026-10-28)
8
+
9
+ - 跟进 edge-tts 7.3.1 的 v2 令牌与 Cookie 协议,恢复真实 Edge 在线语音;独立模块保留上游 LGPLv3 许可与源码,主体仍为 MIT。
10
+
11
+ - ASR 不再跟随重定向上传音频;请将 asrBaseUrl 直接设为可信的最终接口。直连和插件代理均适用。
12
+ - 修复同名音频并发生成时的覆盖问题,转写和试听也返回实际保存的独立文件名。
13
+ - 显式覆盖只允许没有硬链接的普通文件,保留工作区和链接边界检查。
14
+
7
15
  ## 0.4.3 (2026-10-27)
8
16
 
9
17
  - 修复分批更新时的新旧更新控件混用:保留原页面、草稿与安装保护,空闲后接管旧控制器,兼容旧入口。
@@ -0,0 +1,195 @@
1
+ The MIT license is used for 'src/edge_tts/srt_composer.py' only. All
2
+ remaining files are licensed under the LGPLv3.
3
+
4
+ -----------------------------------------------------------------------
5
+
6
+ The MIT License
7
+
8
+ Copyright (c) 2014-2023 Christopher Down
9
+ Copyright (c) 2025- rany <rany@riseup.net>
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in
19
+ all copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27
+ THE SOFTWARE.
28
+
29
+ -----------------------------------------------------------------------
30
+
31
+ GNU LESSER GENERAL PUBLIC LICENSE
32
+ Version 3, 29 June 2007
33
+
34
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
35
+ Everyone is permitted to copy and distribute verbatim copies
36
+ of this license document, but changing it is not allowed.
37
+
38
+
39
+ This version of the GNU Lesser General Public License incorporates
40
+ the terms and conditions of version 3 of the GNU General Public
41
+ License, supplemented by the additional permissions listed below.
42
+
43
+ 0. Additional Definitions.
44
+
45
+ As used herein, "this License" refers to version 3 of the GNU Lesser
46
+ General Public License, and the "GNU GPL" refers to version 3 of the GNU
47
+ General Public License.
48
+
49
+ "The Library" refers to a covered work governed by this License,
50
+ other than an Application or a Combined Work as defined below.
51
+
52
+ An "Application" is any work that makes use of an interface provided
53
+ by the Library, but which is not otherwise based on the Library.
54
+ Defining a subclass of a class defined by the Library is deemed a mode
55
+ of using an interface provided by the Library.
56
+
57
+ A "Combined Work" is a work produced by combining or linking an
58
+ Application with the Library. The particular version of the Library
59
+ with which the Combined Work was made is also called the "Linked
60
+ Version".
61
+
62
+ The "Minimal Corresponding Source" for a Combined Work means the
63
+ Corresponding Source for the Combined Work, excluding any source code
64
+ for portions of the Combined Work that, considered in isolation, are
65
+ based on the Application, and not on the Linked Version.
66
+
67
+ The "Corresponding Application Code" for a Combined Work means the
68
+ object code and/or source code for the Application, including any data
69
+ and utility programs needed for reproducing the Combined Work from the
70
+ Application, but excluding the System Libraries of the Combined Work.
71
+
72
+ 1. Exception to Section 3 of the GNU GPL.
73
+
74
+ You may convey a covered work under sections 3 and 4 of this License
75
+ without being bound by section 3 of the GNU GPL.
76
+
77
+ 2. Conveying Modified Versions.
78
+
79
+ If you modify a copy of the Library, and, in your modifications, a
80
+ facility refers to a function or data to be supplied by an Application
81
+ that uses the facility (other than as an argument passed when the
82
+ facility is invoked), then you may convey a copy of the modified
83
+ version:
84
+
85
+ a) under this License, provided that you make a good faith effort to
86
+ ensure that, in the event an Application does not supply the
87
+ function or data, the facility still operates, and performs
88
+ whatever part of its purpose remains meaningful, or
89
+
90
+ b) under the GNU GPL, with none of the additional permissions of
91
+ this License applicable to that copy.
92
+
93
+ 3. Object Code Incorporating Material from Library Header Files.
94
+
95
+ The object code form of an Application may incorporate material from
96
+ a header file that is part of the Library. You may convey such object
97
+ code under terms of your choice, provided that, if the incorporated
98
+ material is not limited to numerical parameters, data structure
99
+ layouts and accessors, or small macros, inline functions and templates
100
+ (ten or fewer lines in length), you do both of the following:
101
+
102
+ a) Give prominent notice with each copy of the object code that the
103
+ Library is used in it and that the Library and its use are
104
+ covered by this License.
105
+
106
+ b) Accompany the object code with a copy of the GNU GPL and this license
107
+ document.
108
+
109
+ 4. Combined Works.
110
+
111
+ You may convey a Combined Work under terms of your choice that,
112
+ taken together, effectively do not restrict modification of the
113
+ portions of the Library contained in the Combined Work and reverse
114
+ engineering for debugging such modifications, if you also do each of
115
+ the following:
116
+
117
+ a) Give prominent notice with each copy of the Combined Work that
118
+ the Library is used in it and that the Library and its use are
119
+ covered by this License.
120
+
121
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
122
+ document.
123
+
124
+ c) For a Combined Work that displays copyright notices during
125
+ execution, include the copyright notice for the Library among
126
+ these notices, as well as a reference directing the user to the
127
+ copies of the GNU GPL and this license document.
128
+
129
+ d) Do one of the following:
130
+
131
+ 0) Convey the Minimal Corresponding Source under the terms of this
132
+ License, and the Corresponding Application Code in a form
133
+ suitable for, and under terms that permit, the user to
134
+ recombine or relink the Application with a modified version of
135
+ the Linked Version to produce a modified Combined Work, in the
136
+ manner specified by section 6 of the GNU GPL for conveying
137
+ Corresponding Source.
138
+
139
+ 1) Use a suitable shared library mechanism for linking with the
140
+ Library. A suitable mechanism is one that (a) uses at run time
141
+ a copy of the Library already present on the user's computer
142
+ system, and (b) will operate properly with a modified version
143
+ of the Library that is interface-compatible with the Linked
144
+ Version.
145
+
146
+ e) Provide Installation Information, but only if you would otherwise
147
+ be required to provide such information under section 6 of the
148
+ GNU GPL, and only to the extent that such information is
149
+ necessary to install and execute a modified version of the
150
+ Combined Work produced by recombining or relinking the
151
+ Application with a modified version of the Linked Version. (If
152
+ you use option 4d0, the Installation Information must accompany
153
+ the Minimal Corresponding Source and Corresponding Application
154
+ Code. If you use option 4d1, you must provide the Installation
155
+ Information in the manner specified by section 6 of the GNU GPL
156
+ for conveying Corresponding Source.)
157
+
158
+ 5. Combined Libraries.
159
+
160
+ You may place library facilities that are a work based on the
161
+ Library side by side in a single library together with other library
162
+ facilities that are not Applications and are not covered by this
163
+ License, and convey such a combined library under terms of your
164
+ choice, if you do both of the following:
165
+
166
+ a) Accompany the combined library with a copy of the same work based
167
+ on the Library, uncombined with any other library facilities,
168
+ conveyed under the terms of this License.
169
+
170
+ b) Give prominent notice with the combined library that part of it
171
+ is a work based on the Library, and explaining where to find the
172
+ accompanying uncombined form of the same work.
173
+
174
+ 6. Revised Versions of the GNU Lesser General Public License.
175
+
176
+ The Free Software Foundation may publish revised and/or new versions
177
+ of the GNU Lesser General Public License from time to time. Such new
178
+ versions will be similar in spirit to the present version, but may
179
+ differ in detail to address new problems or concerns.
180
+
181
+ Each version is given a distinguishing version number. If the
182
+ Library as you received it specifies that a certain numbered version
183
+ of the GNU Lesser General Public License "or any later version"
184
+ applies to it, you have the option of following the terms and
185
+ conditions either of that published version or of any later version
186
+ published by the Free Software Foundation. If the Library as you
187
+ received it does not specify a version number of the GNU Lesser
188
+ General Public License, you may choose any version of the GNU Lesser
189
+ General Public License ever published by the Free Software Foundation.
190
+
191
+ If the Library as you received it specifies that a proxy can decide
192
+ whether future versions of the GNU Lesser General Public License shall
193
+ apply, that proxy's public statement of acceptance of any version is
194
+ permanent authorization for you to choose that version for the
195
+ Library.
package/README.en.md CHANGED
@@ -51,4 +51,4 @@ Older releases need one manual update in native plugin management to acquire the
51
51
 
52
52
  ## License
53
53
 
54
- [MIT](LICENSE)
54
+ Original code: [MIT](LICENSE). The separate Edge v2 token module is LGPLv3; its source and license are included. See [third-party notices](THIRD_PARTY_NOTICES.md).
package/README.md CHANGED
@@ -51,4 +51,4 @@ dsh plugin --profile desktop add dsh-voice
51
51
 
52
52
  ## License
53
53
 
54
- [MIT](LICENSE)
54
+ 主体采用 [MIT](LICENSE);独立的 Edge v2 令牌模块采用上游 LGPLv3,源码与许可随包提供,见[第三方声明](THIRD_PARTY_NOTICES.md)。
@@ -0,0 +1,12 @@
1
+ # Third-party notices
2
+
3
+ `src/edge-drm-v2.ts` and its compiled module are adapted from
4
+ [rany2/edge-tts 7.3.1](https://github.com/rany2/edge-tts/tree/7.3.1),
5
+ `src/edge_tts/drm.py` (blob `707dc7724b63242624b9b62175556fa1fdcb7fad`),
6
+ copyright rany and contributors, licensed under LGPLv3.
7
+ The adaptation replaces the Python digest implementation with a separate
8
+ TypeScript module, retaining the protocol constants and calculations.
9
+ Its source and the upstream license are included in the npm package, under
10
+ `src/edge-drm-v2.ts` and `LICENSES/edge-tts-LGPLv3.txt`.
11
+ You may modify and replace this module under LGPLv3; no replacement lock is used.
12
+ The remaining original dsh-voice code remains under the repository's MIT license.
package/docs/USAGE.en.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  ## Current improvements
6
6
 
7
+ ASR stops on redirects instead of forwarding audio to another endpoint, including through the plugin proxy. Set `asrBaseUrl` to the trusted final API address. With `overwrite=false`, concurrent outputs are saved separately and tools return the actual saved path. Explicit overwrite requires a regular file with no hard links.
8
+
7
9
  Check the ASR endpoint, key and response format after a failure. Blank results are not saved as successful transcripts. Verify TTS and ASR service availability separately.
8
10
 
9
11
  ## Installation
@@ -58,7 +60,7 @@ voice_health {} # self-check TTS /
58
60
 
59
61
  ## Implementation and file limits
60
62
 
61
- - **Direct edge-tts protocol**: the Sec-MS-GEC token is **generated locally** with the official DRM algorithm (SHA256 of Windows file time + trusted client token, 5-minute windows); transport uses the `ws` library with permessage-deflate and an optional HTTP CONNECT proxy tunnel
63
+ - **Direct edge-tts protocol**: follows open-source edge-tts 7.3.1 Sec-MS-GEC v2, generating tokens locally in five-minute windows. Uses `ws` and optional HTTP CONNECT. This is not a Microsoft SDK, and the service protocol may change.
62
64
  - **API configuration**: TTS needs no separate API key. STT uses your configured ASR service, whose pricing and limits apply.
63
65
  - Up-front validation: text ≤ 5000 chars, audio ≤ 25MB (re-checked against the bytes actually read); same-name outputs auto-suffixed
64
66
  - **Reads and writes stay inside the session workspace**: `audio`, `output` and `outputDir` must resolve (symlinks included) under `session.header.cwd`; anything outside is rejected. Copy the file in first.
@@ -77,4 +79,4 @@ pnpm test:integration # opt-in real edge-tts request; network and assertion err
77
79
 
78
80
  ## License
79
81
 
80
- MIT
82
+ Original code MIT; separate Edge v2 token module LGPLv3. See [third-party notices](../THIRD_PARTY_NOTICES.md).
package/docs/USAGE.md CHANGED
@@ -58,13 +58,15 @@ voice_health {} # 自检 TTS / ASR /
58
58
 
59
59
  ## 实现与文件限制
60
60
 
61
- - **edge-tts 协议直连**:Sec-MS-GEC 令牌按官方 DRM 算法**本地生成**(SHA256(Windows 文件时间 + TrustedClientToken),5 分钟窗口),WS 传输用 `ws` 库 + permessage-deflate 压缩 + 可选 HTTP CONNECT 代理隧道
61
+ - **edge-tts 协议直连**:对齐开源 edge-tts 7.3.1 的 Sec-MS-GEC v2 协议,令牌在本地按 5 分钟窗口生成;WS 使用 `ws` 库与可选 HTTP CONNECT 代理。不是微软官方 SDK,服务协议仍可能变更。
62
62
  - **接口配置**:TTS 无需另填 API Key;STT 使用你配置的 ASR 服务,其计费与额度由该服务决定。
63
63
  - 文本 ≤5000 字符、音频 ≤25MB 前置校验(读入后再按实际字节数复查一次);输出同名自动加序号
64
64
  - **读写都在会话工作区内**:`audio` 与 `output` / `outputDir` 解析后(含符号链接)必须落在 `session.header.cwd` 之下,越界直接报错。想转写工作区外的录音,先复制进来;输出同理
65
65
  - **不跟随符号链接写文件**:目标是符号链接(含悬空链接)时拒绝写入,新建文件用 `O_EXCL`,避免检查完再被换成链接
66
66
  - **转写文本 0600**:`voice_stt` 写出的 `.txt` 只给当前用户读写(Windows 不检查权限位)
67
67
  - **密钥只发给配置的地址**:ASR 请求地址一律取配置 `asrBaseUrl`(groq/openai 的预设只是缺省值),模型传的 `engine` 与配置不一致会直接报错,避免把密钥发到另一家的主机
68
+ - **重定向需要用户确认地址**:接口返回 3xx 时停止,不向跳转地址发送音频。请检查服务文档,把 `asrBaseUrl` 设为可信的最终接口;这也适用于配置了插件代理的请求。
69
+ - **并发生成不覆盖**:`overwrite=false` 时即使两个任务同时写同名文件,或合成期间出现用户同名文件,也会另存并返回实际文件名。显式覆盖只接受没有硬链接的普通文件。
68
70
  - Edge TTS 协议适配保留在插件实现中,不使用旧令牌端点。
69
71
 
70
72
  ## 开发
@@ -77,4 +79,4 @@ pnpm test:integration # 显式联网,调用真实 edge-tts;网络或断言
77
79
 
78
80
  ## License
79
81
 
80
- MIT
82
+ 主体 MIT;独立的 Edge v2 令牌模块 LGPLv3,见[第三方声明](../THIRD_PARTY_NOTICES.md)。
@@ -1,5 +1,9 @@
1
1
  # dsh-voice 验证记录
2
2
 
3
+ ## 2026-10-28:0.5.0
4
+
5
+ [本次修复与验收边界](validation-0.5.0.md)。
6
+
3
7
  本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
4
8
 
5
9
  ## 原中文记录
@@ -0,0 +1,11 @@
1
+ # 0.5.0 验证记录(2026-10-28)
2
+
3
+ Windows 回归 74 项:72 通过、2 跳过(文件符号链接权限、POSIX 0600 位)。硬链接、目录 junction、并发合成/转写/试听均另有真实 Windows 文件系统用例。
4
+
5
+ 官方 DSH 0.2.1-rc.4 隔离 profile 实际注册工具并生成两段在线 Edge MP3,同名输出各自保留;旧 v1 协议连接失败,更新后真实合成通过。v2 计算同时匹配固定的 upstream Python 7.3.1 参考向量 44 组。不是仅用模拟音频验收。
6
+
7
+ ASR 使用本机 HTTP 服务与真实 multipart 请求验证:引擎错配在联网前拒绝、工作区外文件拒绝、307 不转发音频;301/302/303/307/308 同时覆盖直连与真实 HTTP 代理。模拟 ASR 返回不代表真实商业转写服务通过。
8
+
9
+ 官方 Web 浏览器实际选择工作区、打开会话轨迹并查看转写结果与插件列表;窄屏无水平溢出。未操作用户桌面窗口,未验证 Electron 原生窗口或真实 ASR 账号。
10
+
11
+ Edge v2 模块单独保留上游 LGPLv3 许可与可替换源码,其他原代码仍为 MIT,见 THIRD_PARTY_NOTICES.md。在线服务后续仍可能变更。
@@ -0,0 +1,8 @@
1
+ /**
2
+ * SPDX-License-Identifier: LGPL-3.0-only
3
+ * Adapted from rany2/edge-tts 7.3.1 src/edge_tts/drm.py (rany and contributors).
4
+ * https://github.com/rany2/edge-tts/blob/7.3.1/src/edge_tts/drm.py
5
+ * TypeScript adaptation, 2026 STARDUSTLC666. See LICENSES/edge-tts-LGPLv3.txt.
6
+ * Kept as a separate, replaceable module; the rest of dsh-voice remains MIT.
7
+ */
8
+ export declare function edgeTokenV2(data: string): string;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * SPDX-License-Identifier: LGPL-3.0-only
3
+ * Adapted from rany2/edge-tts 7.3.1 src/edge_tts/drm.py (rany and contributors).
4
+ * https://github.com/rany2/edge-tts/blob/7.3.1/src/edge_tts/drm.py
5
+ * TypeScript adaptation, 2026 STARDUSTLC666. See LICENSES/edge-tts-LGPLv3.txt.
6
+ * Kept as a separate, replaceable module; the rest of dsh-voice remains MIT.
7
+ */
8
+ const INITIAL = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19];
9
+ const STATE = [0xFD9FBEA2, 0x91243CE7, 0xBE499094, 0xCD2E1E0A, 0x6C006F49, 0x04BD35AB, 0xFEB0A7E4, 0x03EFFBD2];
10
+ const SCHEDULE = [0xEB24BF9E, 0x1558691F, 0xA0C09EE2, 0x0CFA32BA, 0xA622EE8C, 0xD1DD9491, 0xC0DD25A0, 0xF9E70AB6, 0xF0C31573, 0x7058CE4E, 0x7188E606, 0x00E5AB33, 0xD258B060, 0x72766564];
11
+ const K = [
12
+ 0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
13
+ 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
14
+ 0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
15
+ 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
16
+ 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
17
+ 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
18
+ 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
19
+ 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2,
20
+ ];
21
+ const COEFFICIENTS = [
22
+ [0xE82E91B1, 0xB32700D0], [0x44DE24C6, 0x88DFF51D], [0x3BEB65BC, 0x29514038], [0xC89C5933, 0xB328CBDF],
23
+ [0x8D1EC211, 0xD405B4D5], [0x314EF5D5, 0x26E55F3C], [0xE92491B1, 0x607801C6], [0xF0A5B9D5, 0x82B2C1FB],
24
+ [0x06064CF3, 0xFA51A3DA], [0xDA79FA60, 0x207CC464], [0xBED20560, 0x3C3C74F3], [0x89F9C9F8, 0xD2D14B61],
25
+ [0x35FA38C6, 0xBB04A2C1], [0xCB49E3C6, 0xC5626167],
26
+ ];
27
+ const rol16 = (x) => ((x << 16) | (x >>> 16)) >>> 0;
28
+ const rotr = (x, n) => (x >>> n) | (x << (32 - n));
29
+ const sigma0 = (x) => (rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3)) >>> 0;
30
+ const sigma1 = (x) => (rotr(x, 17) ^ rotr(x, 19) ^ (x >>> 10)) >>> 0;
31
+ const mul = (x, y) => Math.imul(x, y) >>> 0;
32
+ function expand(words, first = 16) {
33
+ for (let i = first; i < 64; i++)
34
+ words.push((sigma1(words[i - 2]) + words[i - 7] + sigma0(words[i - 15]) + words[i - 16]) >>> 0);
35
+ }
36
+ function rounds(state, words, first = 0) {
37
+ let [a, b, c, d, e, f, g, h] = state;
38
+ for (let i = first; i < 64; i++) {
39
+ const constant = first === 14 && i === 14 ? 0xC9DC06A7 : K[i];
40
+ const t1 = (h + ((rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) >>> 0) + ((g ^ (e & (f ^ g))) >>> 0) + constant + words[i]) >>> 0;
41
+ const t2 = (((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) >>> 0) + (((a & b) | (c & (a | b))) >>> 0)) >>> 0;
42
+ [a, b, c, d, e, f, g, h] = [(t1 + t2) >>> 0, a, b, c, (d + t1) >>> 0, e, f, g];
43
+ }
44
+ return [a, b, c, d, e, f, g, h];
45
+ }
46
+ function tokenDigest(data) {
47
+ const words = new Array(14).fill(0);
48
+ words.push(data.readUIntBE(0, 3), data.readUInt32BE(3));
49
+ for (let i = 16; i < 30; i++) {
50
+ let value = SCHEDULE[i - 16];
51
+ if (i - 2 >= 14)
52
+ value += (sigma1(words[i - 2]) ^ (i === 16 ? 0x000B92C0 : 0)) >>> 0;
53
+ if (i - 7 >= 14)
54
+ value += words[i - 7];
55
+ if (i - 15 >= 14)
56
+ value += (sigma0(words[i - 15]) ^ (i === 29 ? 0x059C0B80 : 0)) >>> 0;
57
+ words.push(value >>> 0);
58
+ }
59
+ words.push((sigma1(words[28]) + words[23] + sigma0(words[15]) + words[14] + 0xC9DC06A7 - K[14]) >>> 0);
60
+ expand(words, 31);
61
+ let state = rounds(STATE, words, 14).map((x, i) => (x + INITIAL[i]) >>> 0);
62
+ const length = 57 + data.length, padding = ((55 - length) % 64 + 64) % 64;
63
+ const tail = Buffer.concat([data.subarray(7), Buffer.from([0x80]), Buffer.alloc(padding + 8)]);
64
+ tail.writeBigUInt64BE(BigInt(length) * 8n, tail.length - 8);
65
+ for (let offset = 0; offset < tail.length; offset += 64) {
66
+ const block = Array.from({ length: 16 }, (_, i) => tail.readUInt32BE(offset + 4 * i));
67
+ expand(block);
68
+ state = rounds(state, block).map((x, i) => (x + state[i]) >>> 0);
69
+ }
70
+ const out = Buffer.alloc(32);
71
+ state.forEach((x, i) => out.writeUInt32BE(x, 4 * i));
72
+ return out;
73
+ }
74
+ function cs64(data, digest) {
75
+ const keyA = (digest.readUInt32LE(0) | 1) >>> 0, keyB = (digest.readUInt32LE(4) | 1) >>> 0;
76
+ const factors = [mul(keyA, 0x82893933), mul(keyB, 0x82893933)];
77
+ const terms = COEFFICIENTS.map(([coefficient], i) => mul(coefficient, factors[i % 2])), offsets = COEFFICIENTS.map(([, offset]) => offset);
78
+ const padded = Buffer.alloc(Math.ceil((data.length + 1) / 8) * 8);
79
+ data.copy(padded, 1);
80
+ for (let i = 0; i < padded.length / 4; i++) {
81
+ const word = padded.readUInt32LE(4 * i);
82
+ let term = mul(word, i % 2 === 0 ? keyA : keyB), offset = mul(rol16(word), i % 2 === 0 ? 0xEF0569FB : 0xC31713DB);
83
+ if (i === 0) {
84
+ term = (term + mul(0xF2F90B1A, factors[0])) >>> 0;
85
+ offset = (offset + 0x0B1A0000) >>> 0;
86
+ }
87
+ terms.push(term);
88
+ offsets.push(offset);
89
+ }
90
+ let swapAcc = 0, swapLast = 0, state = 0, acc = 0;
91
+ for (let i = 0; i < terms.length; i += 2) {
92
+ const x = terms[i], y = terms[i + 1];
93
+ let t = rol16((offsets[i] + x) >>> 0), q = mul(t, 0x76F50F34);
94
+ swapAcc += (mul(rol16(q), 0xC3EFEA97) + q) >>> 0;
95
+ t = rol16((offsets[i + 1] + y) >>> 0);
96
+ q = mul(t, 0x1881CE3C);
97
+ swapLast = (mul(rol16(q), 0x35BD1EC9) + q) >>> 0;
98
+ swapAcc = (swapAcc + swapLast) >>> 0;
99
+ state = (mul(state, keyA) + x) >>> 0;
100
+ for (const factor of [0xCF98B111, 0x87085B9F, 0x12CEB96D, 0x257E1D83])
101
+ state = mul(rol16(state), factor);
102
+ acc += state;
103
+ t = (mul(state, keyB) + y) >>> 0;
104
+ for (const factor of [0xA27416F5, 0xD38396FF])
105
+ t = mul(rol16(t), factor);
106
+ state = mul(rol16(mul(rol16(t), 0x7C932B89)), 0xBFA49F69);
107
+ acc = (acc + state) >>> 0;
108
+ }
109
+ const out = Buffer.alloc(8);
110
+ out.writeUInt32LE((swapLast ^ state) >>> 0);
111
+ out.writeUInt32LE((swapAcc ^ acc) >>> 0, 4);
112
+ return out;
113
+ }
114
+ let lastData = '', lastToken = '';
115
+ export function edgeTokenV2(data) {
116
+ if (data === lastData)
117
+ return lastToken;
118
+ const bytes = Buffer.from(data, 'ascii'), digest = tokenDigest(bytes);
119
+ lastToken = cs64(bytes, digest).toString('hex').toUpperCase() + '-' + digest.toString('hex').toUpperCase();
120
+ lastData = data;
121
+ return lastToken;
122
+ }
package/lib/edge-tts.d.ts CHANGED
@@ -26,7 +26,7 @@ export type WebSocketFactory = (url: string, options: {
26
26
  headers: Record<string, string>;
27
27
  }) => WebSocketLike;
28
28
  /**
29
- * 本地生成 Sec-MS-GEC 令牌(对齐 edge-tts DRM 算法)。
29
+ * 本地生成 Sec-MS-GEC v2 令牌(对齐 edge-tts 7.3.1)。
30
30
  */
31
31
  export declare function generateSecMsGec(nowSeconds?: number): string;
32
32
  /** 生成 SSML。 */
package/lib/edge-tts.js CHANGED
@@ -5,13 +5,14 @@
5
5
  *
6
6
  * @module dsh-voice/edge-tts
7
7
  */
8
- import { createHash, randomBytes, randomUUID } from 'node:crypto';
8
+ import { randomBytes, randomUUID } from 'node:crypto';
9
+ import { edgeTokenV2 } from './edge-drm-v2.js';
9
10
  import { HttpsProxyAgent } from 'https-proxy-agent';
10
11
  import WebSocket from 'ws';
11
12
  const TRUSTED_CLIENT_TOKEN = '6A5AA1D4EAFF4E9FB37E23D68491D6F4';
12
13
  const WS_BASE = 'wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1';
13
14
  const OUTPUT_FORMAT = 'audio-24khz-48kbitrate-mono-mp3';
14
- const SEC_MS_GEC_VERSION = '1-143.0.3650.75';
15
+ const SEC_MS_GEC_VERSION = '2-143.0.3650.75';
15
16
  const WIN_EPOCH_SECONDS = 11644473600;
16
17
  const WSS_HEADERS = {
17
18
  Pragma: 'no-cache',
@@ -32,14 +33,14 @@ function proxyWebSocket(proxyUrl) {
32
33
  return (url, options) => new WebSocket(url, { headers: options.headers, agent });
33
34
  }
34
35
  /**
35
- * 本地生成 Sec-MS-GEC 令牌(对齐 edge-tts DRM 算法)。
36
+ * 本地生成 Sec-MS-GEC v2 令牌(对齐 edge-tts 7.3.1)。
36
37
  */
37
38
  export function generateSecMsGec(nowSeconds = Date.now() / 1000) {
38
39
  let ticks = Math.floor(nowSeconds) + WIN_EPOCH_SECONDS;
39
40
  ticks -= ticks % 300;
40
41
  const windowsTicks = ticks * 10000000;
41
42
  const raw = String(windowsTicks) + TRUSTED_CLIENT_TOKEN;
42
- return createHash('sha256').update(raw, 'ascii').digest('hex').toUpperCase();
43
+ return edgeTokenV2(raw);
43
44
  }
44
45
  /** XML 转义。 */
45
46
  function escapeXml(text) {
@@ -75,7 +76,7 @@ export async function synthesizeSpeech(options, deps = {}, timeoutMs = 30000, si
75
76
  // 对齐 edge-tts 当前协议:Sec-MS-GEC 走 URL 查询参数,MUID 走 header
76
77
  const url = WS_BASE + '?TrustedClientToken=' + TRUSTED_CLIENT_TOKEN + '&ConnectionId=' + randomUUID().replace(/-/g, '') + '&Sec-MS-GEC=' + token + '&Sec-MS-GEC-Version=' + SEC_MS_GEC_VERSION;
77
78
  const socket = factory(url, {
78
- headers: { ...WSS_HEADERS, MUID: randomBytes(16).toString('hex').toUpperCase() },
79
+ headers: { ...WSS_HEADERS, Cookie: 'muid=' + randomBytes(16).toString('hex').toUpperCase() + ';' },
79
80
  });
80
81
  const chunks = [];
81
82
  let done = false;
package/lib/paths.d.ts CHANGED
@@ -16,8 +16,8 @@ export declare function resolveOutputPath(explicit: string | undefined, defaultN
16
16
  /** 确保输出目录存在且落在工作区内:不存在则创建,越界或符号链接报错。 */
17
17
  export declare function ensureWorkspaceDirectory(input: string, cwd?: string): string;
18
18
  /**
19
- * 写工作区内的文件:先建父目录,再按「新建用 wx / 覆盖用 w」落盘。
19
+ * 写工作区内的文件,返回实际写入的路径。同名竞争通过 wx 排他创建解决。
20
20
  *
21
21
  * 新建时用 O_EXCL,避免检查完成到写入之间被人塞进一个符号链接。
22
22
  */
23
- export declare function writeWorkspaceFile(target: string, data: string | Buffer, cwd?: string, mode?: number): void;
23
+ export declare function writeWorkspaceFile(target: string, data: string | Buffer, cwd?: string, mode?: number, overwrite?: boolean): string;
package/lib/paths.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @module dsh-voice/paths
5
5
  */
6
- import { lstatSync, mkdirSync, openSync, closeSync, realpathSync, statSync, writeFileSync } from 'node:fs';
6
+ import { lstatSync, mkdirSync, openSync, closeSync, realpathSync, statSync, writeFileSync, fstatSync, ftruncateSync, fchmodSync, constants } from 'node:fs';
7
7
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
8
8
  /** 会话工作区的真实路径(解析符号链接);取不到就退回解析后的绝对路径。 */
9
9
  function workspaceRoot(cwd) {
@@ -24,10 +24,17 @@ function pathState(target) {
24
24
  try {
25
25
  return lstatSync(target).isSymbolicLink() ? 'symlink' : 'present';
26
26
  }
27
- catch {
27
+ catch (error) {
28
+ if (error.code !== 'ENOENT')
29
+ throw error;
28
30
  return 'missing';
29
31
  }
30
32
  }
33
+ function assertOverwriteFile(target, stat = lstatSync(target)) {
34
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
35
+ throw new Error('覆盖目标必须是没有硬链接的普通文件:' + target + '。请改用新的输出文件名。');
36
+ }
37
+ }
31
38
  /**
32
39
  * 沿着最近的已存在祖先解析真实路径,再接回尚不存在的后缀。
33
40
  *
@@ -96,6 +103,8 @@ export function resolveOutputPath(explicit, defaultName, overwrite, cwd = proces
96
103
  const requested = explicit !== undefined && explicit.trim() !== '' ? explicit.trim() : defaultName;
97
104
  const target = resolve(root, requested);
98
105
  assertWritableTarget(target, root, '输出路径');
106
+ if (overwrite && pathState(target) === 'present')
107
+ assertOverwriteFile(target);
99
108
  if (overwrite || pathState(target) === 'missing')
100
109
  return target;
101
110
  const directory = dirname(target);
@@ -129,24 +138,58 @@ export function ensureWorkspaceDirectory(input, cwd = process.cwd()) {
129
138
  return target;
130
139
  }
131
140
  /**
132
- * 写工作区内的文件:先建父目录,再按「新建用 wx / 覆盖用 w」落盘。
141
+ * 写工作区内的文件,返回实际写入的路径。同名竞争通过 wx 排他创建解决。
133
142
  *
134
143
  * 新建时用 O_EXCL,避免检查完成到写入之间被人塞进一个符号链接。
135
144
  */
136
- export function writeWorkspaceFile(target, data, cwd = process.cwd(), mode) {
145
+ export function writeWorkspaceFile(target, data, cwd = process.cwd(), mode, overwrite = false) {
137
146
  const root = workspaceRoot(cwd);
138
147
  assertWritableTarget(target, root, '输出路径');
139
148
  mkdirSync(dirname(target), { recursive: true });
140
- assertWritableTarget(target, root, '输出路径');
141
- if (pathState(target) === 'missing') {
142
- const fd = openSync(target, 'wx', mode);
149
+ const extension = extname(target), base = basename(target, extension);
150
+ for (let index = 0; index < 1000; index++) {
151
+ const candidate = index === 0 ? target : join(dirname(target), base + '_' + index + extension);
152
+ if (index > 0 && pathState(candidate) !== 'missing')
153
+ continue;
154
+ assertWritableTarget(candidate, root, '输出路径');
155
+ let fd;
156
+ try {
157
+ fd = openSync(candidate, 'wx', mode);
158
+ }
159
+ catch (error) {
160
+ if (error.code !== 'EEXIST')
161
+ throw error;
162
+ if (!overwrite)
163
+ continue;
164
+ // 不在校验前截断文件。打开后还需检查真实句柄,防止静态硬链接和替换。
165
+ const before = lstatSync(candidate);
166
+ assertOverwriteFile(candidate, before);
167
+ fd = openSync(candidate, constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0));
168
+ try {
169
+ const opened = fstatSync(fd), current = lstatSync(candidate);
170
+ assertOverwriteFile(candidate, opened);
171
+ assertOverwriteFile(candidate, current);
172
+ assertWritableTarget(candidate, root, '输出路径');
173
+ if (opened.dev !== before.dev || opened.ino !== before.ino || opened.dev !== current.dev || opened.ino !== current.ino) {
174
+ throw new Error('输出文件在写入前发生变化,请重试:' + candidate);
175
+ }
176
+ if (mode !== undefined && process.platform !== 'win32')
177
+ fchmodSync(fd, mode);
178
+ ftruncateSync(fd, 0);
179
+ writeFileSync(fd, data);
180
+ }
181
+ finally {
182
+ closeSync(fd);
183
+ }
184
+ return candidate;
185
+ }
143
186
  try {
144
187
  writeFileSync(fd, data);
145
188
  }
146
189
  finally {
147
190
  closeSync(fd);
148
191
  }
149
- return;
192
+ return candidate;
150
193
  }
151
- writeFileSync(target, data, mode === undefined ? undefined : { mode });
194
+ throw new Error('找不到可用的输出文件名(同名文件超过 999 个),请显式指定 output。');
152
195
  }
package/lib/stt.js CHANGED
@@ -56,6 +56,8 @@ export async function transcribe(baseUrl, apiKey, options, fetchImpl = globalThi
56
56
  try {
57
57
  response = await raceAbort(fetchImpl(baseUrl + '/audio/transcriptions', {
58
58
  method: 'POST',
59
+ // 音频只交给配置的端点;307/308 也不能把 multipart 转发到另一站。
60
+ redirect: 'manual',
59
61
  headers: { authorization: 'Bearer ' + apiKey },
60
62
  body: form,
61
63
  signal: requestSignal,
@@ -67,6 +69,14 @@ export async function transcribe(baseUrl, apiKey, options, fetchImpl = globalThi
67
69
  throw new Error('ASR 请求超时,请重试或缩短音频。');
68
70
  throw new Error('ASR 请求失败:' + (error instanceof Error ? error.message : String(error)) + '。若接口需要特殊代理(梯子),请在 cordis.patch.yml 配置 proxyUrl 后重启。');
69
71
  }
72
+ if (response.status >= 300 && response.status < 400) {
73
+ try {
74
+ await raceAbort(response.body?.cancel(), requestSignal);
75
+ }
76
+ catch { /* 尽力释放响应,仍受请求超时限制。 */ }
77
+ throwIfAborted(abortSignal);
78
+ throw new Error('ASR 接口返回重定向(HTTP ' + response.status + '),未向跳转地址发送音频。请确认服务地址,并在 asrBaseUrl 中直接填写可信的最终接口地址。');
79
+ }
70
80
  if (!response.ok) {
71
81
  let body;
72
82
  try {
package/lib/tools.js CHANGED
@@ -197,10 +197,11 @@ export function buildVoiceTools(config, deps = {}) {
197
197
  if (!/^[+-]?\d+(\.\d+)?(%|Hz|st)$/.test(value))
198
198
  throw new Error(label + ' 不合法:' + value + '。合法格式如 +10%、-2Hz、+1st。');
199
199
  }
200
- const output = resolveOutputPath(optionalString(args, 'output'), 'voice_output.mp3', cfg.overwrite, sessionCwd(exec));
200
+ const requested = optionalString(args, 'output'), cwd = sessionCwd(exec);
201
+ resolveOutputPath(requested, 'voice_output.mp3', cfg.overwrite, cwd);
201
202
  const audio = await raceAbort((deps.tts ?? synthesizeSpeech)({ text, voice, rate, pitch }, { proxyUrl: cfg.proxyUrl }, timeout, signal), signal);
202
203
  throwIfAborted(signal);
203
- writeWorkspaceFile(output, audio, sessionCwd(exec));
204
+ const output = writeWorkspaceFile(resolveOutputPath(requested, 'voice_output.mp3', cfg.overwrite, cwd), audio, cwd, undefined, cfg.overwrite);
204
205
  return { output, bytes: audio.length, voice, rate, pitch, textLength: text.length };
205
206
  },
206
207
  timeoutMs: timeout + 10000,
@@ -258,8 +259,7 @@ export function buildVoiceTools(config, deps = {}) {
258
259
  if (output !== undefined) {
259
260
  const target = resolveOutputPath(output, '', cfg.overwrite, cwd);
260
261
  // 转写文本可能含隐私内容,只用 0600 写入。
261
- writeWorkspaceFile(target, text, cwd, 0o600);
262
- transcriptFile = target;
262
+ transcriptFile = writeWorkspaceFile(target, text, cwd, 0o600, cfg.overwrite);
263
263
  }
264
264
  // The output schema declares these as strings — never return null, or
265
265
  // dsh rejects the whole tool result (issue #1).
@@ -319,8 +319,8 @@ export function buildVoiceTools(config, deps = {}) {
319
319
  const name = 'voice-preview-' + voice.replace(/[^a-zA-Z0-9-]/g, '_') + '.mp3';
320
320
  // 复用 resolveOutputPath:overwrite=false 时改名加序号,不再直接覆盖同名样例。
321
321
  const file = resolveOutputPath(join(outDir, name), '', cfg.overwrite, cwd);
322
- writeWorkspaceFile(file, audio, cwd);
323
- return { voice, output: file, bytes: audio.length };
322
+ const output = writeWorkspaceFile(file, audio, cwd, undefined, cfg.overwrite);
323
+ return { voice, output, bytes: audio.length };
324
324
  }
325
325
  catch (error) {
326
326
  throwIfAborted(signal);
@@ -437,7 +437,7 @@ function __stardustUpdatesClient(ctx, React, require, shared) {
437
437
  }
438
438
 
439
439
  /* STARDUSTLC_UPDATE_CLIENT_BODY_START */
440
- window.__ModuleLoader__.load(__stardustUpdatesWrap({id:"dsh-voice",factory:require=>({apply(){}})},{"name":"dsh-voice","version":"0.1.0","hasWorkbench":false,"packageVersion":"0.4.3","label":"语音","labelEn":"Voice","suiteComponent":true}));
440
+ window.__ModuleLoader__.load(__stardustUpdatesWrap({id:"dsh-voice",factory:require=>({apply(){}})},{"name":"dsh-voice","version":"0.1.0","hasWorkbench":false,"packageVersion":"0.5.0","label":"语音","labelEn":"Voice","suiteComponent":true}));
441
441
 
442
442
  /* STARDUSTLC_UPDATE_CLIENT_BODY_END */
443
443
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-voice",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "把文字生成语音,或通过兼容接口把音频转为文字。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -20,7 +20,10 @@
20
20
  "README.md",
21
21
  "README.en.md",
22
22
  "docs",
23
- "CHANGELOG.md"
23
+ "CHANGELOG.md",
24
+ "src/edge-drm-v2.ts",
25
+ "LICENSES",
26
+ "THIRD_PARTY_NOTICES.md"
24
27
  ],
25
28
  "scripts": {
26
29
  "build": "tsc -p tsconfig.json && node scripts/build-updates.mjs",
@@ -52,7 +55,7 @@
52
55
  "voice",
53
56
  "dsh-plugin"
54
57
  ],
55
- "license": "MIT",
58
+ "license": "MIT AND LGPL-3.0-only",
56
59
  "engines": {
57
60
  "node": "^22.19.0 || >=24.0.0",
58
61
  "dsh": ">=0.2.1-rc.3"
@@ -0,0 +1,105 @@
1
+ /**
2
+ * SPDX-License-Identifier: LGPL-3.0-only
3
+ * Adapted from rany2/edge-tts 7.3.1 src/edge_tts/drm.py (rany and contributors).
4
+ * https://github.com/rany2/edge-tts/blob/7.3.1/src/edge_tts/drm.py
5
+ * TypeScript adaptation, 2026 STARDUSTLC666. See LICENSES/edge-tts-LGPLv3.txt.
6
+ * Kept as a separate, replaceable module; the rest of dsh-voice remains MIT.
7
+ */
8
+
9
+ const INITIAL = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]
10
+ const STATE = [0xFD9FBEA2, 0x91243CE7, 0xBE499094, 0xCD2E1E0A, 0x6C006F49, 0x04BD35AB, 0xFEB0A7E4, 0x03EFFBD2]
11
+ const SCHEDULE = [0xEB24BF9E, 0x1558691F, 0xA0C09EE2, 0x0CFA32BA, 0xA622EE8C, 0xD1DD9491, 0xC0DD25A0, 0xF9E70AB6, 0xF0C31573, 0x7058CE4E, 0x7188E606, 0x00E5AB33, 0xD258B060, 0x72766564]
12
+ const K = [
13
+ 0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
14
+ 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
15
+ 0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
16
+ 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
17
+ 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
18
+ 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
19
+ 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
20
+ 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2,
21
+ ]
22
+ const COEFFICIENTS = [
23
+ [0xE82E91B1, 0xB32700D0], [0x44DE24C6, 0x88DFF51D], [0x3BEB65BC, 0x29514038], [0xC89C5933, 0xB328CBDF],
24
+ [0x8D1EC211, 0xD405B4D5], [0x314EF5D5, 0x26E55F3C], [0xE92491B1, 0x607801C6], [0xF0A5B9D5, 0x82B2C1FB],
25
+ [0x06064CF3, 0xFA51A3DA], [0xDA79FA60, 0x207CC464], [0xBED20560, 0x3C3C74F3], [0x89F9C9F8, 0xD2D14B61],
26
+ [0x35FA38C6, 0xBB04A2C1], [0xCB49E3C6, 0xC5626167],
27
+ ]
28
+ const rol16 = (x: number) => ((x << 16) | (x >>> 16)) >>> 0
29
+ const rotr = (x: number, n: number) => (x >>> n) | (x << (32 - n))
30
+ const sigma0 = (x: number) => (rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3)) >>> 0
31
+ const sigma1 = (x: number) => (rotr(x, 17) ^ rotr(x, 19) ^ (x >>> 10)) >>> 0
32
+ const mul = (x: number, y: number) => Math.imul(x, y) >>> 0
33
+
34
+ function expand(words: number[], first = 16): void {
35
+ for (let i = first; i < 64; i++) words.push((sigma1(words[i - 2]) + words[i - 7] + sigma0(words[i - 15]) + words[i - 16]) >>> 0)
36
+ }
37
+ function rounds(state: number[], words: number[], first = 0): number[] {
38
+ let [a, b, c, d, e, f, g, h] = state
39
+ for (let i = first; i < 64; i++) {
40
+ const constant = first === 14 && i === 14 ? 0xC9DC06A7 : K[i]
41
+ const t1 = (h + ((rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) >>> 0) + ((g ^ (e & (f ^ g))) >>> 0) + constant + words[i]) >>> 0
42
+ const t2 = (((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) >>> 0) + (((a & b) | (c & (a | b))) >>> 0)) >>> 0
43
+ ;[a, b, c, d, e, f, g, h] = [(t1 + t2) >>> 0, a, b, c, (d + t1) >>> 0, e, f, g]
44
+ }
45
+ return [a, b, c, d, e, f, g, h]
46
+ }
47
+ function tokenDigest(data: Buffer): Buffer {
48
+ const words = new Array<number>(14).fill(0)
49
+ words.push(data.readUIntBE(0, 3), data.readUInt32BE(3))
50
+ for (let i = 16; i < 30; i++) {
51
+ let value = SCHEDULE[i - 16]
52
+ if (i - 2 >= 14) value += (sigma1(words[i - 2]) ^ (i === 16 ? 0x000B92C0 : 0)) >>> 0
53
+ if (i - 7 >= 14) value += words[i - 7]
54
+ if (i - 15 >= 14) value += (sigma0(words[i - 15]) ^ (i === 29 ? 0x059C0B80 : 0)) >>> 0
55
+ words.push(value >>> 0)
56
+ }
57
+ words.push((sigma1(words[28]) + words[23] + sigma0(words[15]) + words[14] + 0xC9DC06A7 - K[14]) >>> 0)
58
+ expand(words, 31)
59
+ let state = rounds(STATE, words, 14).map((x, i) => (x + INITIAL[i]) >>> 0)
60
+ const length = 57 + data.length, padding = ((55 - length) % 64 + 64) % 64
61
+ const tail = Buffer.concat([data.subarray(7), Buffer.from([0x80]), Buffer.alloc(padding + 8)])
62
+ tail.writeBigUInt64BE(BigInt(length) * 8n, tail.length - 8)
63
+ for (let offset = 0; offset < tail.length; offset += 64) {
64
+ const block = Array.from({ length: 16 }, (_, i) => tail.readUInt32BE(offset + 4 * i))
65
+ expand(block)
66
+ state = rounds(state, block).map((x, i) => (x + state[i]) >>> 0)
67
+ }
68
+ const out = Buffer.alloc(32); state.forEach((x, i) => out.writeUInt32BE(x, 4 * i)); return out
69
+ }
70
+ function cs64(data: Buffer, digest: Buffer): Buffer {
71
+ const keyA = (digest.readUInt32LE(0) | 1) >>> 0, keyB = (digest.readUInt32LE(4) | 1) >>> 0
72
+ const factors = [mul(keyA, 0x82893933), mul(keyB, 0x82893933)]
73
+ const terms = COEFFICIENTS.map(([coefficient], i) => mul(coefficient, factors[i % 2])), offsets = COEFFICIENTS.map(([, offset]) => offset)
74
+ const padded = Buffer.alloc(Math.ceil((data.length + 1) / 8) * 8); data.copy(padded, 1)
75
+ for (let i = 0; i < padded.length / 4; i++) {
76
+ const word = padded.readUInt32LE(4 * i)
77
+ let term = mul(word, i % 2 === 0 ? keyA : keyB), offset = mul(rol16(word), i % 2 === 0 ? 0xEF0569FB : 0xC31713DB)
78
+ if (i === 0) { term = (term + mul(0xF2F90B1A, factors[0])) >>> 0; offset = (offset + 0x0B1A0000) >>> 0 }
79
+ terms.push(term); offsets.push(offset)
80
+ }
81
+ let swapAcc = 0, swapLast = 0, state = 0, acc = 0
82
+ for (let i = 0; i < terms.length; i += 2) {
83
+ const x = terms[i], y = terms[i + 1]
84
+ let t = rol16((offsets[i] + x) >>> 0), q = mul(t, 0x76F50F34)
85
+ swapAcc += (mul(rol16(q), 0xC3EFEA97) + q) >>> 0
86
+ t = rol16((offsets[i + 1] + y) >>> 0); q = mul(t, 0x1881CE3C)
87
+ swapLast = (mul(rol16(q), 0x35BD1EC9) + q) >>> 0; swapAcc = (swapAcc + swapLast) >>> 0
88
+ state = (mul(state, keyA) + x) >>> 0
89
+ for (const factor of [0xCF98B111, 0x87085B9F, 0x12CEB96D, 0x257E1D83]) state = mul(rol16(state), factor)
90
+ acc += state
91
+ t = (mul(state, keyB) + y) >>> 0
92
+ for (const factor of [0xA27416F5, 0xD38396FF]) t = mul(rol16(t), factor)
93
+ state = mul(rol16(mul(rol16(t), 0x7C932B89)), 0xBFA49F69); acc = (acc + state) >>> 0
94
+ }
95
+ const out = Buffer.alloc(8); out.writeUInt32LE((swapLast ^ state) >>> 0); out.writeUInt32LE((swapAcc ^ acc) >>> 0, 4); return out
96
+ }
97
+
98
+ let lastData = '', lastToken = ''
99
+ export function edgeTokenV2(data: string): string {
100
+ if (data === lastData) return lastToken
101
+ const bytes = Buffer.from(data, 'ascii'), digest = tokenDigest(bytes)
102
+ lastToken = cs64(bytes, digest).toString('hex').toUpperCase() + '-' + digest.toString('hex').toUpperCase()
103
+ lastData = data
104
+ return lastToken
105
+ }