betaquant 0.6.0__tar.gz → 0.6.2__tar.gz
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.
- {betaquant-0.6.0 → betaquant-0.6.2}/Cargo.lock +1 -1
- {betaquant-0.6.0 → betaquant-0.6.2}/Cargo.toml +1 -1
- {betaquant-0.6.0 → betaquant-0.6.2}/PKG-INFO +59 -1
- {betaquant-0.6.0 → betaquant-0.6.2}/README.md +58 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/build.rs +7 -0
- betaquant-0.6.2/python/betaquant/algo/__init__.py +14 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/lib.rs +27 -6
- {betaquant-0.6.0 → betaquant-0.6.2}/src/license.rs +54 -8
- betaquant-0.6.0/python/betaquant/algo/__init__.py +0 -6
- {betaquant-0.6.0 → betaquant-0.6.2}/.gitea/builder/Dockerfile +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/.gitea/builder/README.md +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/.gitea/workflows/publish.yml +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/.gitignore +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/CHANGELOG.md +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/LICENSE +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/gtja191/al/__init__.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/gtja191/al/alpha191.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/gtja191/al/alpha191_context.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/gtja191/alpha191.txt +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/gtja191/main.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/quickstart/full_demo.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/quickstart/rank.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/quickstart/usage.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/quickstart/verify_sumif.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/wq101/al/__init__.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/wq101/al/alpha101.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/wq101/al/alpha101_context.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/wq101/alpha101.txt +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/examples/wq101/main.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/pyproject.toml +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/__init__.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/algo/algo_gen.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/algo/manual.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/algo.md +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/context.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/lang/__init__.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/lang/__main__.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/lang/alpha.lark +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/lang/parser.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/lang/to_python.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/perf.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/betaquant/transforms.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/tests/test_grammar.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/tests/test_rank.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/python/tests/test_to_python.py +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/rustfmt.toml +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/alpha.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/backfill.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/context.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/cross.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/cut.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/drawdown.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/ema.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/entropy.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/error.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/extremum.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/group.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/internal.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/ma.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/misc.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/mod.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/moments.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/neutralize.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/quantile.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/rank.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/returns.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/rolling.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/series.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/sharpe.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/slope.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/spec.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/split.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/stats.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/stddev.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/sum.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/topk.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/wsum.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/algo/zscore.rs +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src/license/public_key.bin +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/src//347/256/227/345/255/220/350/247/204/345/210/231/345/277/205/350/257/273.md" +0 -0
- {betaquant-0.6.0 → betaquant-0.6.2}/tag_release.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: betaquant
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
@@ -28,6 +28,64 @@ Project-URL: repository, https://home.zhaojun.com
|
|
|
28
28
|
pip install betaquant
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## 授权(License Token)
|
|
32
|
+
|
|
33
|
+
betaquant 的算子受许可证(license)保护,**使用前需持有有效的 license token**。
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
### 取得机器指纹(机器绑定用)
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import betaquant
|
|
40
|
+
|
|
41
|
+
machine = betaquant.machine_fingerprint() # 跨 Linux/Windows,机器绑定以它为准
|
|
42
|
+
print(machine)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
把该指纹提交给签发方,换取绑定到本机的 token。
|
|
46
|
+
|
|
47
|
+
### 启用 token:算子入口自动鉴权(推荐)
|
|
48
|
+
|
|
49
|
+
调用 `set_license_token` 把 token 设入进程级状态后,**每个算子入口都会自动鉴权**:
|
|
50
|
+
未授权 / 过期 / 未设 token 时算子直接抛 `ValueError`,无需在每次调用前手写判断。
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import betaquant
|
|
55
|
+
|
|
56
|
+
token = "<从签发方取得的 license key>"
|
|
57
|
+
betaquant.set_license_token(token) # 启动时一次
|
|
58
|
+
|
|
59
|
+
result = betaquant.ts_ma(data, 3) # 已授权 → 正常计算
|
|
60
|
+
# 若 token 未设置 / 过期 / 该算子不在 allow,betaquant.ts_ma(...) 直接抛 ValueError
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
> 不调用 `set_license_token` 而直接调算子会抛 `ValueError`。这是与早期版本的关键区别:
|
|
64
|
+
> 鉴权不再依赖调用方自觉判断,而是内嵌在每个算子入口。
|
|
65
|
+
|
|
66
|
+
### 手动校验(可选)
|
|
67
|
+
|
|
68
|
+
也可显式验签或单独判断某算子是否授权(不改变进程级 token 状态):
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# 验签 + 校验(失败 / 过期 / 机器不符抛 ValueError),返回声明 dict
|
|
72
|
+
claims = betaquant.verify_license(token, machine=machine)
|
|
73
|
+
|
|
74
|
+
# 判断某算子是否被授权(token 无效 / 过期直接返回 False)
|
|
75
|
+
if betaquant.license_allows(token, "ts_ma", machine=machine):
|
|
76
|
+
...
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
授权语义(**默认拒绝**):算子必须显式出现在 `allow`(或 `allow` 含 `"*"`)才放行;
|
|
80
|
+
`deny` 优先级高于 `allow`。`allow=[]` 表示全部拒绝,并非不限制。
|
|
81
|
+
|
|
82
|
+
> 过期判定使用本机系统时间,离线方案对“改系统时间”无法根治,属已知限制:用短有效期
|
|
83
|
+
> + 定期换发缓解。
|
|
84
|
+
|
|
85
|
+
更完整的端到端示例(签发 → 启用 token → 算子硬门禁 → 过期续期 → 机器绑定)见
|
|
86
|
+
`encryptor/license/examples/demo.py`。签发方封装见 `encryptor/license`
|
|
87
|
+
(`LicenseService`)。
|
|
88
|
+
|
|
31
89
|
## 使用
|
|
32
90
|
|
|
33
91
|
### 上下文设置
|
|
@@ -12,6 +12,64 @@
|
|
|
12
12
|
pip install betaquant
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
## 授权(License Token)
|
|
16
|
+
|
|
17
|
+
betaquant 的算子受许可证(license)保护,**使用前需持有有效的 license token**。
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### 取得机器指纹(机器绑定用)
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import betaquant
|
|
24
|
+
|
|
25
|
+
machine = betaquant.machine_fingerprint() # 跨 Linux/Windows,机器绑定以它为准
|
|
26
|
+
print(machine)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
把该指纹提交给签发方,换取绑定到本机的 token。
|
|
30
|
+
|
|
31
|
+
### 启用 token:算子入口自动鉴权(推荐)
|
|
32
|
+
|
|
33
|
+
调用 `set_license_token` 把 token 设入进程级状态后,**每个算子入口都会自动鉴权**:
|
|
34
|
+
未授权 / 过期 / 未设 token 时算子直接抛 `ValueError`,无需在每次调用前手写判断。
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import betaquant
|
|
39
|
+
|
|
40
|
+
token = "<从签发方取得的 license key>"
|
|
41
|
+
betaquant.set_license_token(token) # 启动时一次
|
|
42
|
+
|
|
43
|
+
result = betaquant.ts_ma(data, 3) # 已授权 → 正常计算
|
|
44
|
+
# 若 token 未设置 / 过期 / 该算子不在 allow,betaquant.ts_ma(...) 直接抛 ValueError
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> 不调用 `set_license_token` 而直接调算子会抛 `ValueError`。这是与早期版本的关键区别:
|
|
48
|
+
> 鉴权不再依赖调用方自觉判断,而是内嵌在每个算子入口。
|
|
49
|
+
|
|
50
|
+
### 手动校验(可选)
|
|
51
|
+
|
|
52
|
+
也可显式验签或单独判断某算子是否授权(不改变进程级 token 状态):
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
# 验签 + 校验(失败 / 过期 / 机器不符抛 ValueError),返回声明 dict
|
|
56
|
+
claims = betaquant.verify_license(token, machine=machine)
|
|
57
|
+
|
|
58
|
+
# 判断某算子是否被授权(token 无效 / 过期直接返回 False)
|
|
59
|
+
if betaquant.license_allows(token, "ts_ma", machine=machine):
|
|
60
|
+
...
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
授权语义(**默认拒绝**):算子必须显式出现在 `allow`(或 `allow` 含 `"*"`)才放行;
|
|
64
|
+
`deny` 优先级高于 `allow`。`allow=[]` 表示全部拒绝,并非不限制。
|
|
65
|
+
|
|
66
|
+
> 过期判定使用本机系统时间,离线方案对“改系统时间”无法根治,属已知限制:用短有效期
|
|
67
|
+
> + 定期换发缓解。
|
|
68
|
+
|
|
69
|
+
更完整的端到端示例(签发 → 启用 token → 算子硬门禁 → 过期续期 → 机器绑定)见
|
|
70
|
+
`encryptor/license/examples/demo.py`。签发方封装见 `encryptor/license`
|
|
71
|
+
(`LicenseService`)。
|
|
72
|
+
|
|
15
73
|
## 使用
|
|
16
74
|
|
|
17
75
|
### 上下文设置
|
|
@@ -218,6 +218,13 @@ fn build_py_bindings(functions: &[TaFunc]) -> Result<()> {
|
|
|
218
218
|
write!(code, "{}", py_args)?;
|
|
219
219
|
writeln!(code, " ) -> PyResult<()> {{")?;
|
|
220
220
|
|
|
221
|
+
// license 硬门禁:算子入口先做鉴权,未授权 / 过期 / 未设置 token 直接抛 ValueError。
|
|
222
|
+
writeln!(
|
|
223
|
+
code,
|
|
224
|
+
" crate::license::require_for(\"{}\").map_err(|e| PyValueError::new_err(e.to_string()))?;",
|
|
225
|
+
py_func_name
|
|
226
|
+
)?;
|
|
227
|
+
|
|
221
228
|
writeln!(code, " let ctx = ctx(py);")?;
|
|
222
229
|
|
|
223
230
|
let arrays: Vec<(&String, &TaType)> = func
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright 2026 MSD-RS Project LiJia
|
|
2
|
+
# SPDX-License-Identifier: BSD-2-Clause
|
|
3
|
+
|
|
4
|
+
from .manual import ts_ema
|
|
5
|
+
from .algo_gen import *
|
|
6
|
+
from ._algo import (
|
|
7
|
+
set_ctx,
|
|
8
|
+
reset_ctx,
|
|
9
|
+
verify_license,
|
|
10
|
+
license_allows,
|
|
11
|
+
machine_fingerprint,
|
|
12
|
+
set_license_token,
|
|
13
|
+
clear_license_token,
|
|
14
|
+
)
|
|
@@ -78,6 +78,8 @@ mod algo_impl {
|
|
|
78
78
|
input: &'py Bound<'_, PyAny>,
|
|
79
79
|
periods: usize,
|
|
80
80
|
) -> PyResult<()> {
|
|
81
|
+
crate::license::require_for("ts_ema")
|
|
82
|
+
.map_err(|e| PyValueError::new_err(e.to_string()))?;
|
|
81
83
|
let ctx = ctx(py);
|
|
82
84
|
|
|
83
85
|
if let Some((mut r, input)) = r
|
|
@@ -131,17 +133,17 @@ mod license_impl {
|
|
|
131
133
|
|
|
132
134
|
/// 验签 + 校验 license key,成功返回声明 dict,失败抛 ValueError。
|
|
133
135
|
///
|
|
136
|
+
/// 机器绑定自动用本机指纹校验(betaquant 是客户端,永远校验本机)。
|
|
134
137
|
/// 返回的 dict 含:sub / allow / deny / iat / exp / machine / kid。
|
|
135
138
|
#[pyfunction]
|
|
136
|
-
#[pyo3(signature = (key, now=None,
|
|
139
|
+
#[pyo3(signature = (key, now=None, check_expiry=true))]
|
|
137
140
|
pub fn verify_license<'py>(
|
|
138
141
|
py: Python<'py>,
|
|
139
142
|
key: &str,
|
|
140
143
|
now: Option<i64>,
|
|
141
|
-
machine: Option<&str>,
|
|
142
144
|
check_expiry: bool,
|
|
143
145
|
) -> PyResult<Bound<'py, PyDict>> {
|
|
144
|
-
let claims = license::verify(key, now,
|
|
146
|
+
let claims = license::verify(key, now, check_expiry).map_err(to_pyerr)?;
|
|
145
147
|
let d = PyDict::new(py);
|
|
146
148
|
d.set_item("sub", claims.sub)?;
|
|
147
149
|
d.set_item("allow", PyList::new(py, &claims.allow)?)?;
|
|
@@ -154,17 +156,18 @@ mod license_impl {
|
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
/// 校验 key 后判断某算子是否可用;key 无效 / 过期直接视为不可用(返回 False)。
|
|
159
|
+
///
|
|
160
|
+
/// 机器绑定自动用本机指纹校验。
|
|
157
161
|
#[pyfunction]
|
|
158
|
-
#[pyo3(signature = (key, operator, now=None,
|
|
162
|
+
#[pyo3(signature = (key, operator, now=None, check_expiry=true))]
|
|
159
163
|
pub fn license_allows<'py>(
|
|
160
164
|
_py: Python<'py>,
|
|
161
165
|
key: &str,
|
|
162
166
|
operator: &str,
|
|
163
167
|
now: Option<i64>,
|
|
164
|
-
machine: Option<&str>,
|
|
165
168
|
check_expiry: bool,
|
|
166
169
|
) -> PyResult<bool> {
|
|
167
|
-
match license::verify(key, now,
|
|
170
|
+
match license::verify(key, now, check_expiry) {
|
|
168
171
|
Ok(claims) => Ok(claims.allows(operator)),
|
|
169
172
|
Err(_) => Ok(false),
|
|
170
173
|
}
|
|
@@ -175,6 +178,22 @@ mod license_impl {
|
|
|
175
178
|
pub fn machine_fingerprint<'py>(_py: Python<'py>) -> PyResult<String> {
|
|
176
179
|
Ok(license::machine_fingerprint())
|
|
177
180
|
}
|
|
181
|
+
|
|
182
|
+
/// 设置进程内 license token:验签通过后所有算子调用自动鉴权。
|
|
183
|
+
///
|
|
184
|
+
/// 失败抛 ``ValueError``。机器绑定自动用本机指纹校验。
|
|
185
|
+
#[pyfunction]
|
|
186
|
+
pub fn set_license_token<'py>(_py: Python<'py>, key: &str) -> PyResult<()> {
|
|
187
|
+
license::set_token(key).map_err(to_pyerr)?;
|
|
188
|
+
Ok(())
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// 清空进程内 license token(主要用于测试 / 释放)。
|
|
192
|
+
#[pyfunction]
|
|
193
|
+
pub fn clear_license_token<'py>(_py: Python<'py>) -> PyResult<()> {
|
|
194
|
+
license::clear_token();
|
|
195
|
+
Ok(())
|
|
196
|
+
}
|
|
178
197
|
}
|
|
179
198
|
|
|
180
199
|
#[pymodule]
|
|
@@ -188,6 +207,8 @@ fn _algo(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|
|
188
207
|
m.add_function(wrap_pyfunction!(license_impl::verify_license, m)?)?;
|
|
189
208
|
m.add_function(wrap_pyfunction!(license_impl::license_allows, m)?)?;
|
|
190
209
|
m.add_function(wrap_pyfunction!(license_impl::machine_fingerprint, m)?)?;
|
|
210
|
+
m.add_function(wrap_pyfunction!(license_impl::set_license_token, m)?)?;
|
|
211
|
+
m.add_function(wrap_pyfunction!(license_impl::clear_license_token, m)?)?;
|
|
191
212
|
algo_impl::register_functions(m)?;
|
|
192
213
|
Ok(())
|
|
193
214
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
//!
|
|
10
10
|
//! 验签逻辑放在编译产物里(而非明文 Python),显著抬高 patch / 绕过门槛。
|
|
11
11
|
|
|
12
|
+
use std::sync::{LazyLock, RwLock};
|
|
12
13
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
13
14
|
|
|
14
15
|
use base64::Engine;
|
|
@@ -20,6 +21,11 @@ use sha2::{Digest, Sha256};
|
|
|
20
21
|
/// 编译期内嵌的 32 字节原始 Ed25519 公钥。换钥 = 替换此文件 + 重新编译。
|
|
21
22
|
static PUBLIC_KEY_BYTES: &[u8] = include_bytes!("license/public_key.bin");
|
|
22
23
|
|
|
24
|
+
/// 进程内当前生效的 license claims。`set_token` 写入,`require_for` 读取。
|
|
25
|
+
/// 只在二进制内可见 / 可改,Python 侧无法 patch。
|
|
26
|
+
static CURRENT_CLAIMS: LazyLock<RwLock<Option<Claims>>> =
|
|
27
|
+
LazyLock::new(|| RwLock::new(None));
|
|
28
|
+
|
|
23
29
|
#[derive(Debug, thiserror::Error)]
|
|
24
30
|
pub enum LicenseError {
|
|
25
31
|
#[error("license key 格式错误(应为 payload.signature)")]
|
|
@@ -38,6 +44,10 @@ pub enum LicenseError {
|
|
|
38
44
|
Expired,
|
|
39
45
|
#[error("license 与当前机器不匹配")]
|
|
40
46
|
MachineMismatch,
|
|
47
|
+
#[error("license token 未设置(请先调用 set_license_token)")]
|
|
48
|
+
NotSet,
|
|
49
|
+
#[error("算子 {0} 未授权")]
|
|
50
|
+
NotAllowed(String),
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
/// 反序列化的声明,字段与 Python 端 `License.to_dict()` 对应。
|
|
@@ -283,11 +293,11 @@ fn verifying_key() -> Result<VerifyingKey, LicenseError> {
|
|
|
283
293
|
/// 验签 + 校验,返回可信 `Claims`。
|
|
284
294
|
///
|
|
285
295
|
/// - `now`:当前 Unix 秒;`None` 取系统时间。便于测试。
|
|
286
|
-
/// -
|
|
296
|
+
/// - 机器绑定:claims 未绑机器(`machine=None`)则不校验;绑了机器则必须与
|
|
297
|
+
/// **本机指纹**一致(betaquant 是客户端,永远校验本机,无需外部传入)。
|
|
287
298
|
pub fn verify(
|
|
288
299
|
key: &str,
|
|
289
300
|
now: Option<i64>,
|
|
290
|
-
machine: Option<&str>,
|
|
291
301
|
check_expiry: bool,
|
|
292
302
|
) -> Result<Claims, LicenseError> {
|
|
293
303
|
let key = key.trim();
|
|
@@ -317,9 +327,9 @@ pub fn verify(
|
|
|
317
327
|
return Err(LicenseError::Expired);
|
|
318
328
|
}
|
|
319
329
|
|
|
320
|
-
// 4)
|
|
321
|
-
if let
|
|
322
|
-
if bound !=
|
|
330
|
+
// 4) 机器绑定校验:未绑机器不校验;绑了则必须等于本机指纹
|
|
331
|
+
if let Some(bound) = claims.machine.as_deref() {
|
|
332
|
+
if bound != machine_fingerprint() {
|
|
323
333
|
return Err(LicenseError::MachineMismatch);
|
|
324
334
|
}
|
|
325
335
|
}
|
|
@@ -327,6 +337,42 @@ pub fn verify(
|
|
|
327
337
|
Ok(claims)
|
|
328
338
|
}
|
|
329
339
|
|
|
340
|
+
/// 设置进程内 license token:验签通过则缓存其 claims,供后续算子鉴权使用。
|
|
341
|
+
///
|
|
342
|
+
/// 机器绑定由 `verify` 自动用本机指纹校验(betaquant 是客户端,永远校验本机)。
|
|
343
|
+
/// 失败会返回错误;调用方应把错误传给 Python 抛 ``ValueError``。
|
|
344
|
+
pub fn set_token(key: &str) -> Result<Claims, LicenseError> {
|
|
345
|
+
let claims = verify(key, None, true)?;
|
|
346
|
+
let mut slot = CURRENT_CLAIMS
|
|
347
|
+
.write()
|
|
348
|
+
.map_err(|_| LicenseError::BadClaims)?;
|
|
349
|
+
*slot = Some(claims.clone());
|
|
350
|
+
Ok(claims)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// 清空进程内 license token(主要用于测试)。
|
|
354
|
+
pub fn clear_token() {
|
|
355
|
+
if let Ok(mut slot) = CURRENT_CLAIMS.write() {
|
|
356
|
+
*slot = None;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/// 算子鉴权钩子:每个算子入口调用。无 token / 过期 / 算子未授权都返 Err。
|
|
361
|
+
///
|
|
362
|
+
/// 这是 Rust 一侧"硬门禁"——校验在编译产物里,Python 改不了。
|
|
363
|
+
pub fn require_for(operator: &str) -> Result<(), LicenseError> {
|
|
364
|
+
let slot = CURRENT_CLAIMS.read().map_err(|_| LicenseError::BadClaims)?;
|
|
365
|
+
let claims = slot.as_ref().ok_or(LicenseError::NotSet)?;
|
|
366
|
+
// 过期保护(带容差):缓存的 claims 也可能跨过 exp。
|
|
367
|
+
if claims.is_expired(now_unix()) {
|
|
368
|
+
return Err(LicenseError::Expired);
|
|
369
|
+
}
|
|
370
|
+
if !claims.allows(operator) {
|
|
371
|
+
return Err(LicenseError::NotAllowed(operator.to_string()));
|
|
372
|
+
}
|
|
373
|
+
Ok(())
|
|
374
|
+
}
|
|
375
|
+
|
|
330
376
|
#[cfg(test)]
|
|
331
377
|
mod tests {
|
|
332
378
|
use super::*;
|
|
@@ -334,8 +380,8 @@ mod tests {
|
|
|
334
380
|
// 注意:这些测试依赖内嵌公钥与对应私钥签发的 key,集成测试在 Python 端覆盖。
|
|
335
381
|
#[test]
|
|
336
382
|
fn rejects_garbage() {
|
|
337
|
-
assert!(verify("not-a-key", None,
|
|
338
|
-
assert!(verify("a.b.c", None,
|
|
339
|
-
assert!(verify("", None,
|
|
383
|
+
assert!(verify("not-a-key", None, true).is_err());
|
|
384
|
+
assert!(verify("a.b.c", None, true).is_err());
|
|
385
|
+
assert!(verify("", None, true).is_err());
|
|
340
386
|
}
|
|
341
387
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|