exflower-license-server 1.0.8 → 1.3.2
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/Dockerfile +13 -12
- package/README.md +113 -15
- package/activation.js +360 -0
- package/admin/index.html +1222 -430
- package/admin/vendor/pico.min.css +4 -0
- package/central-public.pem +3 -0
- package/db.js +138 -1
- package/index.js +24 -2
- package/package.json +8 -2
- package/routes.js +878 -36
- package/seed-templates.js +131 -0
- package/seeds/agent-templates/agent-1001-douyin.md +355 -0
- package/seeds/agent-templates/agent-1002-xiaoho.md +347 -0
- package/seeds/agent-templates/agent-1003-zhishi.md +515 -0
- package/seeds/agent-templates/agent-1004-weixin.md +362 -0
- package/seeds/agent-templates/agent-1005-qiyepe.md +323 -0
- package/seeds/agent-templates/agent-1006-fapiao.md +321 -0
- package/seeds/agent-templates/agent-1007-zhihu.md +325 -0
- package/seeds/agent-templates/agent-1008-chanpi.md +367 -0
- package/seeds/agent-templates/agent-1009-zhaopi.md +344 -0
- package/seeds/agent-templates/agent-1010-meiguo.md +355 -0
- package/seeds/agent-templates/agent-1011-falvwe.md +374 -0
- package/seeds/agent-templates/agent-1012-tishici.md +371 -0
- package/seeds/agent-templates/agent-1013-diansh.md +371 -0
- package/seeds/agent-templates/agent-1014-kuajing.md +347 -0
- package/seeds/agent-templates/agent-1015-zhibod.md +398 -0
- package/seeds/agent-templates/agent-1016-bizhan.md +347 -0
- package/seeds/agent-templates/agent-1017-shipin.md +359 -0
- package/seeds/agent-templates/agent-1018-tuxian.md +345 -0
- package/seeds/agent-templates/agent-1019-shipint.md +354 -0
- package/seeds/agent-templates/agent-1020-dingji.md +347 -0
- package/seeds/agent-templates/agent-1021-shangy.md +364 -0
- package/seeds/agent-templates/agent-1022-xiaosh.md +337 -0
- package/seeds/agent-templates/agent-1023-kehuch.md +356 -0
- package/seeds/agent-templates/agent-1024-bujiyu.md +323 -0
- package/seeds/agent-templates/agent-1025-shujuyi.md +326 -0
- package/seeds/agent-templates/agent-1026-yunyin.md +336 -0
- package/seeds/agent-templates/agent-1027-duansh.md +335 -0
- package/seeds/agent-templates/agent-1028-kefux.md +329 -0
- package/seeds/agent-templates/agent-1029-aisou.md +352 -0
- package/seeds/agent-templates/agent-1030-zhengq.md +400 -0
- package/seeds/agent-templates/agent-1031-aizhil.md +343 -0
- package/seeds/agent-templates/agent-1032-shouqi.md +345 -0
- package/seeds/agent-templates/agent-1033-jixiao.md +333 -0
- package/seeds/agent-templates/agent-1034-fengxi.md +323 -0
- package/seeds/agent-templates/agent-1035-biange.md +318 -0
- package/seeds/agent-templates/agent-1036-jijins.md +329 -0
- package/seeds/agent-templates/agent-1037-gonggu.md +343 -0
- package/seeds/agent-templates/agent-1038-weibo.md +330 -0
- package/seeds/agent-templates/agent-1039-kuaish.md +337 -0
- package/seeds/agent-templates/agent-1040-uishej.md +330 -0
- package/seeds/agent-templates/agent-1041-wendan.md +325 -0
- package/seeds/agent-templates/agent-1042-youjia.md +334 -0
- package/seeds/agent-templates/agent-1043-gerenc.md +313 -0
- package/seeds/agent-templates/agent-1044-baidus.md +337 -0
- package/seeds/agent-templates/agent-1045-sousuo.md +326 -0
- package/seeds/agent-templates/agent-1046-yingyo.md +334 -0
- package/seeds/agent-templates/agent-1047-yonghu.md +326 -0
- package/seeds/agent-templates/agent-1048-qiyezh.md +328 -0
- package/seeds/agent-templates/agent-1049-bokece.md +337 -0
- package/seeds/agent-templates/agent-1050-uxjia.md +334 -0
- package/seeds/agent-templates/agent-1051-pinpai.md +329 -0
- package/seeds/agent-templates/agent-1052-zhudon.md +330 -0
- package/seeds/agent-templates/agent-1053-meiri.md +336 -0
- package/seeds/agent-templates/agent-1054-jiudia.md +334 -0
- package/seeds/agent-templates/agent-1055-huokce.md +333 -0
- package/seeds/agent-templates/agent-1056-tuihuo.md +330 -0
- package/seeds/agent-templates/agent-1057-zouzha.md +320 -0
- package/seeds/agent-templates/agent-1058-xinlix.md +315 -0
- package/seeds/agent-templates/agent-1059-xushix.md +320 -0
- package/seeds/agent-templates/agent-1060-renlei.md +328 -0
- package/seeds/agent-templates/agent-1061-dilixu.md +329 -0
- package/seeds/agent-templates/agent-1062-lishix.md +322 -0
- package/seeds/agent-templates/agent-1063-ganggu.md +332 -0
- package/seeds/agent-templates/agent-1064-caiwuj.md +321 -0
- package/seeds/agent-templates/agent-1065-touziy.md +318 -0
- package/seeds/agent-templates/agent-1066-caiwuf.md +305 -0
- package/seeds/agent-templates/agent-1067-jinron.md +317 -0
- package/seeds/agent-templates/agent-1068-caiwuy.md +330 -0
- package/seeds/agent-templates/agent-1069-feishu.md +346 -0
- package/seeds/agent-templates/agent-1070-dingdin.md +330 -0
- package/seeds/agent-templates/agent-1071-guonei.md +336 -0
- package/seeds/agent-templates/agent-1072-gongyi.md +323 -0
- package/seeds/agent-templates/agent-1073-fuzhuan.md +329 -0
- package/seeds/agent-templates/agent-1074-kucun.md +323 -0
- package/seeds/agent-templates/agent-1075-wululu.md +325 -0
- package/seeds/agent-templates/agent-1076-gongys.md +338 -0
- package/seeds/agent-templates/agent-1077-fufeim.md +309 -0
- package/seeds/agent-templates/agent-1078-guangg.md +341 -0
- package/seeds/agent-templates/agent-1079-shejia.md +320 -0
- package/seeds/agent-templates/agent-1080-ppcjing.md +318 -0
- package/seeds/agent-templates/agent-1081-chengxu.md +313 -0
- package/seeds/agent-templates/agent-1082-sousc.md +318 -0
- package/seeds/agent-templates/agent-1083-zhuizon.md +316 -0
- package/seeds/agent-templates/agent-1084-xiangmu.md +345 -0
- package/seeds/agent-templates/agent-1085-shiyan.md +327 -0
- package/seeds/agent-templates/agent-1086-xingwei.md +310 -0
- package/seeds/agent-templates/agent-1087-fankui.md +294 -0
- package/seeds/agent-templates/agent-1088-sprint.md +316 -0
- package/seeds/agent-templates/agent-1089-qushiya.md +316 -0
- package/seeds/agent-templates/agent-1090-xuexig.md +322 -0
- package/seeds/agent-templates/agent-1091-zhipian.md +314 -0
- package/seeds/agent-templates/agent-1092-weixinx.md +327 -0
- package/seeds/agent-templates/agent-1093-duozhin.md +335 -0
- package/seeds/agent-templates/agent-1094-anquan.md +314 -0
- package/seeds/agent-templates/agent-1095-shijian.md +330 -0
- package/seeds/agent-templates/agent-1096-quanzhan.md +325 -0
- package/seeds/agent-templates/agent-1097-daimash.md +327 -0
- package/seeds/agent-templates/agent-1098-devops.md +318 -0
- package/seeds/agent-templates/agent-1099-shujug.md +328 -0
- package/seeds/agent-templates/agent-1100-jishuw.md +331 -0
- package/seeds/agent-templates/agent-1101-zhilia.md +321 -0
- package/seeds/agent-templates/agent-1102-jishux.md +331 -0
package/Dockerfile
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# ExFlower License Server Docker Image
|
|
2
2
|
#
|
|
3
|
-
#
|
|
3
|
+
# 直接从 npm registry 拉取已发布版本,构建机无需源码:
|
|
4
4
|
# docker build -t exflower/license-server:latest .
|
|
5
|
+
# docker build --build-arg VERSION=1.3.1 -t exflower/license-server:1.3.1 .
|
|
5
6
|
#
|
|
6
7
|
# Run:
|
|
7
8
|
# docker run -d -p 4001:4001 \
|
|
@@ -11,17 +12,16 @@
|
|
|
11
12
|
|
|
12
13
|
FROM node:18-alpine
|
|
13
14
|
|
|
14
|
-
#
|
|
15
|
-
|
|
15
|
+
# better-sqlite3 原生模块编译工具(alpine/musl 无预编译二进制)
|
|
16
|
+
# apk 源切腾讯云镜像(默认 dl-cdn 国内不可达);海外机构建可 --build-arg APK_MIRROR=dl-cdn.alpinelinux.org
|
|
17
|
+
ARG APK_MIRROR=mirrors.cloud.tencent.com
|
|
18
|
+
RUN sed -i "s|dl-cdn.alpinelinux.org|${APK_MIRROR}|g" /etc/apk/repositories \
|
|
19
|
+
&& apk add --no-cache python3 make g++
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
RUN npm ci --production
|
|
22
|
-
|
|
23
|
-
# Copy application files
|
|
24
|
-
COPY . .
|
|
21
|
+
# 从 npm registry 全局安装已发布版本(国内机构建机可换 npmmirror)
|
|
22
|
+
ARG VERSION=latest
|
|
23
|
+
ARG NPM_REGISTRY=https://registry.npmjs.org
|
|
24
|
+
RUN npm install -g --registry=${NPM_REGISTRY} exflower-license-server@${VERSION}
|
|
25
25
|
|
|
26
26
|
# Create data directory
|
|
27
27
|
RUN mkdir -p /app/data
|
|
@@ -39,5 +39,6 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
|
|
39
39
|
# Default environment
|
|
40
40
|
ENV PORT=4001
|
|
41
41
|
ENV NODE_ENV=production
|
|
42
|
+
ENV LICENSE_DATA_DIR=/app/data
|
|
42
43
|
|
|
43
|
-
CMD ["
|
|
44
|
+
CMD ["exflower-license-server"]
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ npm install -g exflower-license-server
|
|
|
12
12
|
|
|
13
13
|
# 查看版本
|
|
14
14
|
exflower-license-server --version
|
|
15
|
-
# 输出: 1.
|
|
15
|
+
# 输出: 1.3.1
|
|
16
16
|
|
|
17
17
|
# 运行
|
|
18
18
|
exflower-license-server
|
|
@@ -23,7 +23,19 @@ ADMIN_TOKEN=your-secret-token PORT=4001 exflower-license-server
|
|
|
23
23
|
|
|
24
24
|
### 方式二:Docker
|
|
25
25
|
|
|
26
|
+
镜像构建直接从 npm registry 拉取已发布版本(构建机无需源码):
|
|
27
|
+
|
|
26
28
|
```bash
|
|
29
|
+
# 构建(默认 latest;指定版本用 --build-arg VERSION=x.y.z)
|
|
30
|
+
docker build -t exflower/license-server:latest .
|
|
31
|
+
|
|
32
|
+
# 国内构建机走镜像源(npm registry 与 alpine apk 源都可换)
|
|
33
|
+
docker build \
|
|
34
|
+
--build-arg VERSION=1.3.1 \
|
|
35
|
+
--build-arg NPM_REGISTRY=https://registry.npmmirror.com \
|
|
36
|
+
--build-arg APK_MIRROR=mirrors.cloud.tencent.com \
|
|
37
|
+
-t exflower/license-server:1.3.1 .
|
|
38
|
+
|
|
27
39
|
# 使用 docker-compose(推荐)
|
|
28
40
|
docker-compose up -d
|
|
29
41
|
|
|
@@ -123,6 +135,7 @@ sudo systemctl enable --now exflower-license-server
|
|
|
123
135
|
|------|--------|------|
|
|
124
136
|
| `PORT` | `4001` | 服务端口 |
|
|
125
137
|
| `ADMIN_TOKEN` | `exflower-admin-2026` | 管理后台鉴权 Token |
|
|
138
|
+
| `ADMIN_TOKEN_HASH` | — | 管理 Token 的 sha256(hex)。设置后忽略 `ADMIN_TOKEN`,明文不落环境变量(`echo -n "你的token" \| shasum -a 256` 生成) |
|
|
126
139
|
| `LICENSE_DATA_DIR` | 见下方 | 数据库持久化目录 |
|
|
127
140
|
| `NODE_ENV` | `production` | 运行环境 |
|
|
128
141
|
|
|
@@ -140,14 +153,59 @@ sudo systemctl enable --now exflower-license-server
|
|
|
140
153
|
|
|
141
154
|
**⚠️ 重要**:npm 全局安装(`npm install -g`)或更新时,安装目录会被完全替换。默认不再将数据存放在包安装目录内,升级不会丢失已有数据。
|
|
142
155
|
|
|
156
|
+
### 升级与数据安全
|
|
157
|
+
|
|
158
|
+
升级(换新 tgz / 重建 Docker 镜像)只替换程序代码,以下内容全部保留:
|
|
159
|
+
|
|
160
|
+
- `license.db`:公司/License/实例记录、**客户上传的模板**、品牌设置、激活状态
|
|
161
|
+
- 上传的文件本体:`packages/`(轻应用)、`library-templates/`(资料图谱)、`updates/`(客户端更新包)
|
|
162
|
+
|
|
163
|
+
注意事项:
|
|
164
|
+
|
|
165
|
+
1. **自定义了 `LICENSE_DATA_DIR` 的部署**:升级后的启动脚本/systemd unit 必须继续带同一个值——换了路径等于换了一套空数据库(旧数据仍在原目录,切回即可恢复)。
|
|
166
|
+
2. **Docker 部署**:数据在命名卷 `license-data` 里,删容器/重建镜像不受影响;但不要随手 `docker compose down -v`(`-v` 会连卷一起删)。
|
|
167
|
+
3. **官方 Agent 模板播种**(v1.3.0 起随包自带 102 个全局模板):只在首次启动播种一次(settings 落标记),管理员删除的官方模板不会因升级/重启复活;播种用 `INSERT OR IGNORE`,绝不覆盖客户已有数据。
|
|
168
|
+
|
|
143
169
|
## 管理后台
|
|
144
170
|
|
|
145
|
-
启动后访问 `http://localhost:4001/admin`,使用 `ADMIN_TOKEN`
|
|
171
|
+
启动后访问 `http://localhost:4001/admin`,使用 `ADMIN_TOKEN` 登录(登录时会真实校验 Token,错误无法进入)。
|
|
172
|
+
|
|
173
|
+
### 中央激活(v1.3.0 起,必做)
|
|
174
|
+
|
|
175
|
+
**本服务部署后必须先向 ExFlower 中央授权服务激活,否则将拒绝全部客户端请求**(客户端 verify/心跳、模板、轻应用、更新下载一律 403;管理台不受影响)。
|
|
176
|
+
|
|
177
|
+
1. 向厂商获取本服务器专属的 `customer_key`(格式 `EXFC-XXXXXX-XXXXXX-XXXXXX-XXXXXX`)
|
|
178
|
+
2. 管理台首页 →「中央激活」→ 输入 key 激活
|
|
179
|
+
3. 激活后每日自动向中央复核(主端点 `https://license.exflower.pro`,可配置备端点;中央短暂不可达时靠本地缓存凭证照常服务)
|
|
180
|
+
|
|
181
|
+
有效期语义:合同到期前 30 天管理台开始提醒;过期后有 7 天宽限期(服务正常);宽限过后停止对客户端服务。同一 key 在新服务器激活会顶替旧服务器(旧服务器立即停服并显示大红横幅)。吊销/顶替/过期时管理台全站横幅提示。
|
|
182
|
+
|
|
183
|
+
**实例配额语义(v1.3.1 起)**:中央签发的 `max_instances` 是客户**创建激活码时可分配的总实例数**——所有激活码的"最大实例数"之和不得超过该额度(创建/编辑时校验,管理台会显示剩余可分配数)。客户端实际激活多少台由各激活码自己的 max_instances 管(7 天无心跳自动释放名额),中央不干预运行时激活。
|
|
184
|
+
|
|
185
|
+
管理后台功能(v1.2.0 起按私有化部署定位组织:一个后台 = 一个组织):
|
|
186
|
+
- **License 管理**:创建 License 时填写备注(如"市场部")并选择有效期(1/3/6 个月、1 年、2 年、永久);列表显示剩余天数(<30 天标黄、过期标红);支持编辑(改有效期/配额)、暂停/恢复、删除、**重置 Key**
|
|
187
|
+
- **品牌(白标)**:全局工作台名称设置(见下方"白标品牌名")
|
|
188
|
+
- **实例监控**:查看已激活的实例列表
|
|
189
|
+
- **模板管理**:按类型页签组织(agent/excard/doc/资料图谱),卡片式列表,支持分类管理与分类过滤、按 License 定向分发(多选),查看导入统计
|
|
190
|
+
- **轻应用分发**:上传 `.exfapp` 包并按 License 隔离分发;客户端「轻应用」页可浏览/下载/导入(上限 5GB,含图谱的包注意部署机磁盘)
|
|
191
|
+
- **客户端更新**:上传各平台更新包(mac 传 **zip**、win 传 NSIS **exe**),客户端启动自动检查 + 帮助菜单手动检查,electron-updater 静默下载安装(每个包 ~700MB+,注意部署机磁盘余量)
|
|
146
192
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
-
|
|
150
|
-
-
|
|
193
|
+
### License Key 显示策略(v1.2.0 起)
|
|
194
|
+
|
|
195
|
+
- 完整 Key **只在创建/重置成功时显示一次**(弹窗 + 复制按钮),关闭后无法再查看
|
|
196
|
+
- 列表中 Key 以掩码显示(`EXF-XXXXXX-······-······-XXXXXX` 首尾各一段),不可点击复制
|
|
197
|
+
- Key 丢失 → 使用「重置 Key」:旧 Key 立即失效(已激活客户端失去授权需重新激活),新 Key 同样只显示一次
|
|
198
|
+
|
|
199
|
+
### 安全特性(v1.2.0 起)
|
|
200
|
+
|
|
201
|
+
- 登录时真实校验 Token,错误 Token 无法进入后台
|
|
202
|
+
- 管理接口限速 60 次/分/IP,license 验证接口 120 次/分/IP(超限 429)
|
|
203
|
+
- Token 比较使用 timing-safe 方式;支持 `ADMIN_TOKEN_HASH` 哈希配置(明文不落环境变量)
|
|
204
|
+
- 使用默认 Token 启动时打印醒目警告;非 localhost 的 HTTP 访问显示明文传输警示 banner
|
|
205
|
+
|
|
206
|
+
## 白标品牌名
|
|
207
|
+
|
|
208
|
+
「品牌」频道设置**全局**工作台名称后,客户端每次 verify/心跳都会收到 `brand_name` 并写入本地 `~/.exflower/openexteam.json` 的 `title`,工作台标题随之热更新。留空则客户端保持默认 ExFlower。
|
|
151
209
|
|
|
152
210
|
## API 端点
|
|
153
211
|
|
|
@@ -156,37 +214,55 @@ sudo systemctl enable --now exflower-license-server
|
|
|
156
214
|
| 端点 | 方法 | 说明 |
|
|
157
215
|
|------|------|------|
|
|
158
216
|
| `/health` | GET | 健康检查 |
|
|
159
|
-
| `/api/license/verify` | POST | 验证 License Key |
|
|
160
|
-
| `/api/license/heartbeat` | POST |
|
|
217
|
+
| `/api/license/verify` | POST | 验证 License Key(响应含 `company.brand_name`,全局品牌设置) |
|
|
218
|
+
| `/api/license/heartbeat` | POST | 心跳上报(响应含 `brand_name`) |
|
|
161
219
|
| `/api/templates` | GET | 获取模板列表(需 `x-license-key`) |
|
|
162
220
|
| `/api/templates/:id` | GET | 获取模板详情(需 `x-license-key`) |
|
|
221
|
+
| `/api/templates/:id/download` | GET | 下载资料图谱模板 `.exflib`(需 `x-license-key`) |
|
|
222
|
+
| `/api/packages` | GET | 轻应用包列表,按公司隔离(需 `x-license-key`) |
|
|
223
|
+
| `/api/packages/:id/download` | GET | 下载轻应用包(需 `x-license-key`) |
|
|
224
|
+
| `/api/updates/latest.yml` | GET | Windows 更新清单(electron-updater,需 `x-license-key`) |
|
|
225
|
+
| `/api/updates/latest-mac.yml` | GET | macOS 更新清单(需 `x-license-key`) |
|
|
226
|
+
| `/api/updates/files/:platform/:filename` | GET | 更新包下载(需 `x-license-key`) |
|
|
163
227
|
|
|
164
228
|
### 管理后台接口
|
|
165
229
|
|
|
166
230
|
| 端点 | 方法 | 说明 |
|
|
167
231
|
|------|------|------|
|
|
168
|
-
| `/api/admin/companies` | GET/POST |
|
|
169
|
-
| `/api/admin/companies/:id` | PATCH/DELETE |
|
|
232
|
+
| `/api/admin/companies` | GET/POST | License 列表(**key 掩码返回**)/创建(**key 完整返回仅此一次**) |
|
|
233
|
+
| `/api/admin/companies/:id` | PATCH/DELETE | 更新/删除 License |
|
|
234
|
+
| `/api/admin/companies/:id/reset-key` | POST | 重置 License Key(旧 key 立即失效,新 key 完整返回仅此一次) |
|
|
235
|
+
| `/api/admin/brand` | GET/PUT | 全局品牌名(白标)读取/设置 |
|
|
170
236
|
| `/api/admin/instances` | GET | 实例列表 |
|
|
171
237
|
| `/api/admin/instances/:id` | DELETE | 删除实例 |
|
|
172
238
|
| `/api/admin/templates` | GET/POST | 模板列表/创建 |
|
|
173
239
|
| `/api/admin/templates/:id` | PATCH/DELETE | 更新/删除模板 |
|
|
240
|
+
| `/api/admin/templates/library` | POST | 资料图谱模板上传(multipart,字段 `file`,`.exflib`) |
|
|
241
|
+
| `/api/admin/template-categories` | GET/POST | 模板分类列表/创建(按类型分组) |
|
|
242
|
+
| `/api/admin/template-categories/:id` | PATCH/DELETE | 分类改名/删除 |
|
|
174
243
|
| `/api/admin/imports` | GET | 导入统计(支持 `?template_id=` / `?company_id=` 过滤) |
|
|
244
|
+
| `/api/admin/packages` | GET/POST | 轻应用包列表/上传(multipart,字段 `file`+`company_id`) |
|
|
245
|
+
| `/api/admin/packages/:id` | DELETE | 删除轻应用包 |
|
|
246
|
+
| `/api/admin/updates` | GET/POST | 更新包列表/上传(multipart,字段 `file`+`platform`+`version`) |
|
|
247
|
+
| `/api/admin/updates/:id` | DELETE | 删除更新包 |
|
|
175
248
|
|
|
176
249
|
所有管理接口需在 Header 中携带 `x-admin-token`。
|
|
177
250
|
|
|
178
251
|
## 模板系统
|
|
179
252
|
|
|
180
|
-
|
|
253
|
+
模板用于向客户端分发预设配置。管理后台支持 `agent`、`excard`、`doc`、`library`(资料图谱)四种类型的模板。
|
|
254
|
+
|
|
255
|
+
> **workflow 类型暂不开放**:上传/下载形态待重新设计,管理台与客户端均已隐藏入口;API 与存量数据保留,重新开放前不要清理 workflow 数据。
|
|
181
256
|
|
|
182
257
|
### 上传方式(v1.0.7 起)
|
|
183
258
|
|
|
184
259
|
管理后台根据类型提供不同的上传方式:
|
|
185
260
|
|
|
186
261
|
- **agent / excard / doc**:**Markdown 原生上传**。直接上传客户端导出的 `.md` 文件(或粘贴 Markdown 文本),后台自动解析 frontmatter 预填模板名称并生成 `content_json`,无需手写 JSON。
|
|
187
|
-
- **
|
|
262
|
+
- **library**:上传客户端导出的 `.exflib` 包(multipart `POST /api/admin/templates/library`,字段 `file`),服务端解包校验 manifest(`format: exflower-library`)并记录文件大小与 sha256;客户端经 `GET /api/templates/:id/download` 下载导入。
|
|
263
|
+
- **workflow**(暂不开放):以 JSON 描述步骤结构,上传框预填骨架示例。
|
|
188
264
|
|
|
189
|
-
通过 API 创建模板时,`content_json` 与 `content_md` 至少提供一个(`workflow` 类型必须提供 `content_json
|
|
265
|
+
通过 API 创建模板时,`content_json` 与 `content_md` 至少提供一个(`workflow` 类型必须提供 `content_json`;`library` 类型走专用上传端点)。
|
|
190
266
|
|
|
191
267
|
### 通用字段
|
|
192
268
|
|
|
@@ -340,9 +416,31 @@ curl -X POST http://localhost:4001/api/admin/templates \
|
|
|
340
416
|
}'
|
|
341
417
|
```
|
|
342
418
|
|
|
343
|
-
|
|
419
|
+
### 批量种子导入(部署自带模板)
|
|
420
|
+
|
|
421
|
+
`scripts/seed-templates.js` 用于部署时批量导入 Agent 模板(自动建分类、挂分类)与内置帮助文档,幂等可重复执行(分类按名复用,模板按 type+name 跳过):
|
|
422
|
+
|
|
423
|
+
```bash
|
|
424
|
+
node scripts/seed-templates.js \
|
|
425
|
+
--url http://127.0.0.1:4001 --token <ADMIN_TOKEN> \
|
|
426
|
+
--agents-dir /path/to/agent-mds \
|
|
427
|
+
[--guide /path/to/guide/README.md]
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
- Agent 目录中的 `agent-*.md` 按脚本内置的序号→分类映射归入 5 个分类(新媒体内容运营 / 增长与获客 / 销售与客户成功 / 企业职能与合规 / AI 与专业生产力),frontmatter 自动转为 `content_json`。
|
|
431
|
+
- 导入的模板均为**全局模板**(所有 License 可见)。
|
|
432
|
+
- 脚本遵守 admin 接口限速(60 次/分钟),遇到 429 自动退避重试。
|
|
433
|
+
|
|
434
|
+
## 发布新版本(厂商侧)
|
|
344
435
|
|
|
345
436
|
```bash
|
|
346
|
-
|
|
437
|
+
# 1. 提升 package.json 版本号
|
|
438
|
+
# 2. 确认 files 白名单覆盖全部运行时文件(activation.js / seed-templates.js /
|
|
439
|
+
# central-public.pem / seeds/ 缺一个都会发出启动即崩的包)
|
|
440
|
+
npm pack --dry-run # 检查 Tarball Contents
|
|
441
|
+
# 3. 发布(npm 已下线 Classic Token,用勾了 bypass 2FA 的 Granular Token)
|
|
442
|
+
npm publish
|
|
347
443
|
```
|
|
348
444
|
|
|
445
|
+
Docker 镜像构建参数见上方「方式二:Docker」。
|
|
446
|
+
|
package/activation.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中央授权激活模块(M5)
|
|
3
|
+
*
|
|
4
|
+
* license-server 作为「客户服务器」向中央授权服务(license.exflower.pro)激活并每日复核:
|
|
5
|
+
* - customer_key 永不网传:proof = HMAC_SHA256(sha256(key+'|hmac'), action|prefix|fingerprint|ts)
|
|
6
|
+
* - 中央返回 Ed25519 签名凭证(30 天滑动),本地缓存验签后生效
|
|
7
|
+
* - 主端点不通自动切备端点;双端点同时不通靠缓存凭证撑到 token_expires_at
|
|
8
|
+
* - 未激活 / 被顶替 / 合同过期 / 凭证超期 = 拒绝客户端服务(路由层门禁)
|
|
9
|
+
*
|
|
10
|
+
* 状态文件:DATA_DIR/activation.json
|
|
11
|
+
* 验签逻辑(canonical JSON + Ed25519)与 central-license/lib/credential.js 保持一致,
|
|
12
|
+
* 此处自包含拷贝——license-server 独立打包部署,不能跨目录引用。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const { DATA_DIR } = require('./db');
|
|
19
|
+
|
|
20
|
+
const STATE_FILE = path.join(DATA_DIR, 'activation.json');
|
|
21
|
+
const GRACE_MS = 7 * 86400000; // 合同过期宽限期(与中央一致)
|
|
22
|
+
const REMIND_MS = 30 * 86400000; // 到期前提醒起点(与中央一致)
|
|
23
|
+
const REVALIDATE_INTERVAL_MS = 24 * 3600000; // 每日复核
|
|
24
|
+
const RETRY_INTERVAL_MS = 3600000; // 失败后每小时重试
|
|
25
|
+
const HTTP_TIMEOUT_MS = 10000;
|
|
26
|
+
|
|
27
|
+
const DEFAULT_PRIMARY = 'https://license.exflower.pro';
|
|
28
|
+
|
|
29
|
+
// ── 状态读写 ──
|
|
30
|
+
|
|
31
|
+
let state = null;
|
|
32
|
+
|
|
33
|
+
function loadState() {
|
|
34
|
+
if (state) return state;
|
|
35
|
+
try {
|
|
36
|
+
state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
|
37
|
+
} catch {
|
|
38
|
+
state = {};
|
|
39
|
+
}
|
|
40
|
+
return state;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function saveState() {
|
|
44
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
45
|
+
const tmp = STATE_FILE + '.tmp';
|
|
46
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
47
|
+
fs.renameSync(tmp, STATE_FILE);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 服务器指纹:首次生成后持久不变(激活身份)
|
|
51
|
+
function getFingerprint() {
|
|
52
|
+
const s = loadState();
|
|
53
|
+
if (!s.fingerprint) {
|
|
54
|
+
s.fingerprint = crypto.randomUUID();
|
|
55
|
+
saveState();
|
|
56
|
+
}
|
|
57
|
+
return s.fingerprint;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── 验签(自包含,勿改逻辑,与 central-license/lib/credential.js 对齐) ──
|
|
61
|
+
|
|
62
|
+
const CRED_FORMAT = 'exflower-server-license';
|
|
63
|
+
|
|
64
|
+
function canonicalize(value) {
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
return '[' + value.map(canonicalize).join(',') + ']';
|
|
67
|
+
}
|
|
68
|
+
if (value && typeof value === 'object') {
|
|
69
|
+
const keys = Object.keys(value).sort();
|
|
70
|
+
return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalize(value[k])).join(',') + '}';
|
|
71
|
+
}
|
|
72
|
+
return JSON.stringify(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function loadCentralPublicKey() {
|
|
76
|
+
if (process.env.CENTRAL_PUBLIC_KEY) return process.env.CENTRAL_PUBLIC_KEY.replace(/\\n/g, '\n');
|
|
77
|
+
return fs.readFileSync(path.join(__dirname, 'central-public.pem'), 'utf8');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 验签凭证,返回明文主体;失败返回 null */
|
|
81
|
+
function verifyCredential(credential) {
|
|
82
|
+
try {
|
|
83
|
+
if (!credential || credential.format !== CRED_FORMAT || !credential.signature) return null;
|
|
84
|
+
const body = { ...credential };
|
|
85
|
+
delete body.signature;
|
|
86
|
+
const sig = Buffer.from(credential.signature, 'base64url');
|
|
87
|
+
const ok = crypto.verify(null, Buffer.from(canonicalize(body), 'utf8'), loadCentralPublicKey(), sig);
|
|
88
|
+
return ok ? body : null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── HMAC 证明(与 central-license/src/keys.js 协议一致) ──
|
|
95
|
+
|
|
96
|
+
function computeProof(key, action, fingerprint, ts) {
|
|
97
|
+
const prefix = key.split('-').slice(0, 2).join('-');
|
|
98
|
+
const secret = crypto.createHash('sha256').update(key + '|hmac', 'utf8').digest('hex');
|
|
99
|
+
const hmac = crypto.createHmac('sha256', secret)
|
|
100
|
+
.update(`${action}|${prefix}|${fingerprint}|${ts}`, 'utf8')
|
|
101
|
+
.digest('hex');
|
|
102
|
+
return { key_prefix: prefix, fingerprint, ts, hmac };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── 中央端点调用(主备 failover) ──
|
|
106
|
+
|
|
107
|
+
function getEndpoints() {
|
|
108
|
+
const s = loadState();
|
|
109
|
+
// CENTRAL_PRIMARY_URL:开发/测试覆盖主端点(生产默认 license.exflower.pro)
|
|
110
|
+
const list = [process.env.CENTRAL_PRIMARY_URL || (s.endpoints && s.endpoints.primary) || DEFAULT_PRIMARY];
|
|
111
|
+
if (s.endpoints && s.endpoints.backup) list.push(s.endpoints.backup);
|
|
112
|
+
return list.map(u => u.replace(/\/+$/, ''));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function postJson(url, body) {
|
|
116
|
+
const ctrl = new AbortController();
|
|
117
|
+
const timer = setTimeout(() => ctrl.abort(), HTTP_TIMEOUT_MS);
|
|
118
|
+
try {
|
|
119
|
+
const res = await fetch(url, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/json' },
|
|
122
|
+
body: JSON.stringify(body),
|
|
123
|
+
signal: ctrl.signal,
|
|
124
|
+
});
|
|
125
|
+
const data = await res.json().catch(() => ({}));
|
|
126
|
+
return { status: res.status, data };
|
|
127
|
+
} finally {
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 调中央 activate / revalidate,主备依次尝试。
|
|
134
|
+
* 返回 { ok, token?, contract_state?, error?, message?, superseded? }
|
|
135
|
+
*/
|
|
136
|
+
async function callCentral(action) {
|
|
137
|
+
const s = loadState();
|
|
138
|
+
const key = s.customer_key;
|
|
139
|
+
if (!key) return { ok: false, error: 'no_key' };
|
|
140
|
+
const ts = Date.now();
|
|
141
|
+
const proof = computeProof(key, action, getFingerprint(), ts);
|
|
142
|
+
let lastErr = 'all endpoints unreachable';
|
|
143
|
+
for (const base of getEndpoints()) {
|
|
144
|
+
try {
|
|
145
|
+
const { status, data } = await postJson(`${base}/api/v1/${action}`, proof);
|
|
146
|
+
if (data && data.success && data.token) {
|
|
147
|
+
return { ok: true, token: data.token, contract_state: data.contract_state, endpoint: base };
|
|
148
|
+
}
|
|
149
|
+
// 明确的业务拒绝(revoked/expired/superseded/bad_proof…)不再尝试备机——结果一样
|
|
150
|
+
const err = (data && data.error) || `http_${status}`;
|
|
151
|
+
return { ok: false, error: err, message: data && data.message };
|
|
152
|
+
} catch (e) {
|
|
153
|
+
lastErr = `${base}: ${e.name === 'AbortError' ? 'timeout' : e.message}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return { ok: false, error: 'unreachable', message: lastErr };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── 激活 / 复核 ──
|
|
160
|
+
|
|
161
|
+
/** 首次激活(或清除后重新激活)。返回 { ok, error?, message? } */
|
|
162
|
+
async function activateNow() {
|
|
163
|
+
const r = await callCentral('activate');
|
|
164
|
+
const s = loadState();
|
|
165
|
+
if (r.ok) {
|
|
166
|
+
const payload = verifyCredential(r.token);
|
|
167
|
+
if (!payload) {
|
|
168
|
+
s.last_error = '中央返回的凭证验签失败';
|
|
169
|
+
saveState();
|
|
170
|
+
return { ok: false, error: 'bad_signature' };
|
|
171
|
+
}
|
|
172
|
+
s.credential = r.token;
|
|
173
|
+
s.superseded = false;
|
|
174
|
+
s.terminal = null;
|
|
175
|
+
s.last_revalidate = new Date().toISOString();
|
|
176
|
+
s.last_error = null;
|
|
177
|
+
saveState();
|
|
178
|
+
return { ok: true, contract_state: r.contract_state };
|
|
179
|
+
}
|
|
180
|
+
s.last_error = r.message || r.error;
|
|
181
|
+
saveState();
|
|
182
|
+
return r;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 每日复核:凭证滑动顺延;被顶替/吊销/过期落状态。返回 { ok, error? } */
|
|
186
|
+
async function revalidateNow() {
|
|
187
|
+
const s = loadState();
|
|
188
|
+
if (!s.customer_key) return { ok: false, error: 'no_key' };
|
|
189
|
+
if (!s.credential) return activateNow(); // 本地无凭证 → 走激活
|
|
190
|
+
const r = await callCentral('revalidate');
|
|
191
|
+
if (r.ok) {
|
|
192
|
+
const payload = verifyCredential(r.token);
|
|
193
|
+
if (!payload) {
|
|
194
|
+
s.last_error = '中央返回的凭证验签失败';
|
|
195
|
+
saveState();
|
|
196
|
+
return { ok: false, error: 'bad_signature' };
|
|
197
|
+
}
|
|
198
|
+
s.credential = r.token;
|
|
199
|
+
s.superseded = false;
|
|
200
|
+
s.last_revalidate = new Date().toISOString();
|
|
201
|
+
s.last_error = null;
|
|
202
|
+
saveState();
|
|
203
|
+
return { ok: true, contract_state: r.contract_state };
|
|
204
|
+
}
|
|
205
|
+
if (r.error === 'superseded') {
|
|
206
|
+
s.superseded = true;
|
|
207
|
+
s.last_error = r.message || '本服务器的激活已被同 License 的新激活顶替';
|
|
208
|
+
saveState();
|
|
209
|
+
return r;
|
|
210
|
+
}
|
|
211
|
+
if (r.error === 'not_activated') {
|
|
212
|
+
// 中央侧无此激活记录(库被重置等)→ 重新激活
|
|
213
|
+
return activateNow();
|
|
214
|
+
}
|
|
215
|
+
if (r.error === 'revoked' || r.error === 'expired') {
|
|
216
|
+
// 合同终止/超宽限:清凭证,立即进入拒绝服务
|
|
217
|
+
s.credential = null;
|
|
218
|
+
s.terminal = r.error; // 终态:revoked / expired(区别于「从未激活」)
|
|
219
|
+
s.last_error = r.message || (r.error === 'revoked' ? 'License 已被吊销' : '合同已过期且超出宽限期');
|
|
220
|
+
saveState();
|
|
221
|
+
return r;
|
|
222
|
+
}
|
|
223
|
+
// 网络类失败:保留缓存凭证,下个周期再试
|
|
224
|
+
s.last_error = r.message || r.error;
|
|
225
|
+
saveState();
|
|
226
|
+
return r;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── 执行状态推导(路由门禁用) ──
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 当前执行状态:
|
|
233
|
+
* unactivated 未配置 customer_key,或从未激活成功
|
|
234
|
+
* superseded 被同 License 的新服务器顶替
|
|
235
|
+
* revoked 被中央吊销(复核时确认)
|
|
236
|
+
* expired 合同过期且超宽限期
|
|
237
|
+
* token_expired 缓存凭证超 30 天且复核未成功(双端点失联兜底边界)
|
|
238
|
+
* grace 合同过期宽限中(可服务,提醒)
|
|
239
|
+
* expiring_soon 合同 30 天内到期(可服务,提醒)
|
|
240
|
+
* active 正常
|
|
241
|
+
*/
|
|
242
|
+
function getStatus() {
|
|
243
|
+
const s = loadState();
|
|
244
|
+
const base = {
|
|
245
|
+
configured: !!s.customer_key,
|
|
246
|
+
key_prefix: s.customer_key ? s.customer_key.split('-').slice(0, 2).join('-') : null,
|
|
247
|
+
fingerprint: s.fingerprint || null,
|
|
248
|
+
endpoints: { primary: process.env.CENTRAL_PRIMARY_URL || (s.endpoints && s.endpoints.primary) || DEFAULT_PRIMARY, backup: (s.endpoints && s.endpoints.backup) || '' },
|
|
249
|
+
last_revalidate: s.last_revalidate || null,
|
|
250
|
+
last_error: s.last_error || null,
|
|
251
|
+
};
|
|
252
|
+
if (!s.customer_key) return { ...base, state: 'unactivated' };
|
|
253
|
+
if (s.superseded) return { ...base, state: 'superseded' };
|
|
254
|
+
if (!s.credential) return { ...base, state: s.terminal || 'unactivated', message: s.last_error };
|
|
255
|
+
|
|
256
|
+
const payload = verifyCredential(s.credential);
|
|
257
|
+
if (!payload) return { ...base, state: 'unactivated', message: '缓存凭证验签失败' };
|
|
258
|
+
|
|
259
|
+
const now = Date.now();
|
|
260
|
+
const expires = new Date(payload.expires_at).getTime();
|
|
261
|
+
const tokenExpires = new Date(payload.token_expires_at).getTime();
|
|
262
|
+
const info = {
|
|
263
|
+
customer: payload.customer,
|
|
264
|
+
license_id: payload.license_id,
|
|
265
|
+
expires_at: payload.expires_at,
|
|
266
|
+
token_expires_at: payload.token_expires_at,
|
|
267
|
+
days_remaining: Math.max(0, Math.ceil((expires - now) / 86400000)),
|
|
268
|
+
limits: payload.limits || {},
|
|
269
|
+
};
|
|
270
|
+
if (now > expires + GRACE_MS) return { ...base, ...info, state: 'expired' };
|
|
271
|
+
if (now > tokenExpires) return { ...base, ...info, state: 'token_expired' };
|
|
272
|
+
if (now > expires) return { ...base, ...info, state: 'grace' };
|
|
273
|
+
if (expires - now < REMIND_MS) return { ...base, ...info, state: 'expiring_soon' };
|
|
274
|
+
return { ...base, ...info, state: 'active' };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 是否放行客户端请求。放行时返回 null,否则返回 { status, message } */
|
|
278
|
+
function gateRejection() {
|
|
279
|
+
const st = getStatus();
|
|
280
|
+
switch (st.state) {
|
|
281
|
+
case 'active':
|
|
282
|
+
case 'expiring_soon':
|
|
283
|
+
case 'grace':
|
|
284
|
+
return null;
|
|
285
|
+
case 'superseded':
|
|
286
|
+
return { status: 403, message: '本服务器的激活已被同 License 的新激活顶替,已停止服务' };
|
|
287
|
+
case 'revoked':
|
|
288
|
+
return { status: 403, message: 'License 已被厂商吊销,已停止服务' };
|
|
289
|
+
case 'expired':
|
|
290
|
+
return { status: 403, message: '授权合同已过期且超出宽限期' };
|
|
291
|
+
case 'token_expired':
|
|
292
|
+
return { status: 403, message: '授权凭证已过期且无法联系中央授权服务复核' };
|
|
293
|
+
default:
|
|
294
|
+
return { status: 403, message: '授权服务器未激活:请联系厂商获取 customer_key 并在管理台完成激活' };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── 配置(admin) ──
|
|
299
|
+
|
|
300
|
+
/** 设置 customer_key / 备端点并立即激活 */
|
|
301
|
+
async function configure({ customer_key, backup_endpoint }) {
|
|
302
|
+
const s = loadState();
|
|
303
|
+
if (customer_key !== undefined) {
|
|
304
|
+
const key = String(customer_key || '').trim().toUpperCase();
|
|
305
|
+
if (!/^EXFC-[0-9A-F]{6}-[0-9A-F]{6}-[0-9A-F]{6}-[0-9A-F]{6}$/.test(key)) {
|
|
306
|
+
return { ok: false, error: 'bad_format', message: 'customer_key 格式应为 EXFC-XXXXXX-XXXXXX-XXXXXX-XXXXXX' };
|
|
307
|
+
}
|
|
308
|
+
if (key !== s.customer_key) {
|
|
309
|
+
// 换 key:旧凭证/顶替/终态作废
|
|
310
|
+
s.customer_key = key;
|
|
311
|
+
s.credential = null;
|
|
312
|
+
s.superseded = false;
|
|
313
|
+
s.terminal = null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (backup_endpoint !== undefined) {
|
|
317
|
+
s.endpoints = { primary: (s.endpoints && s.endpoints.primary) || DEFAULT_PRIMARY, backup: String(backup_endpoint || '').trim() };
|
|
318
|
+
}
|
|
319
|
+
s.last_error = null;
|
|
320
|
+
saveState();
|
|
321
|
+
return activateNow();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** 清除激活配置(回到未激活) */
|
|
325
|
+
function clear() {
|
|
326
|
+
const s = loadState();
|
|
327
|
+
const fingerprint = s.fingerprint; // 指纹保留,换 key 重激活仍同一身份
|
|
328
|
+
state = { fingerprint };
|
|
329
|
+
saveState();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── 调度器 ──
|
|
333
|
+
|
|
334
|
+
let timer = null;
|
|
335
|
+
|
|
336
|
+
async function tick() {
|
|
337
|
+
const s = loadState();
|
|
338
|
+
if (!s.customer_key) return;
|
|
339
|
+
const r = await revalidateNow();
|
|
340
|
+
const ok = r.ok;
|
|
341
|
+
schedule(ok ? REVALIDATE_INTERVAL_MS : RETRY_INTERVAL_MS);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function schedule(delay) {
|
|
345
|
+
if (timer) clearTimeout(timer);
|
|
346
|
+
timer = setTimeout(tick, delay);
|
|
347
|
+
timer.unref();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** 启动时调用:有 key 则立即复核一次,然后按日循环(失败 1 小时重试) */
|
|
351
|
+
function startScheduler() {
|
|
352
|
+
const s = loadState();
|
|
353
|
+
if (!s.customer_key) return;
|
|
354
|
+
tick().catch(e => {
|
|
355
|
+
console.error('[Activation] 复核异常:', e.message);
|
|
356
|
+
schedule(RETRY_INTERVAL_MS);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
module.exports = { getStatus, gateRejection, configure, clear, activateNow, revalidateNow, startScheduler };
|