@whyshu/argent-engine 0.0.0-stage → 0.6.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/MANIFEST.json +307 -0
- package/engine/VERSION +1 -0
- package/engine/agents/catalog.json +454 -0
- package/engine/configs/_aliases.proposed.json +651 -0
- package/engine/configs/_migration-plan.proposed.json +899 -0
- package/engine/configs/account.json +272 -0
- package/engine/configs/advanced.json +205 -0
- package/engine/configs/host.json +149 -0
- package/engine/configs/hr.json +27 -0
- package/engine/configs/notify.json +264 -0
- package/engine/core/cli/argent.mjs +277 -0
- package/engine/core/contracts/argent.contracts.json +1065 -0
- package/engine/core/contracts/verbs.schema.json +2539 -0
- package/engine/core/lib/argent-datadir.mjs +223 -0
- package/engine/core/lib/audit.mjs +187 -0
- package/engine/core/lib/claim.mjs +258 -0
- package/engine/core/lib/config-catalog.mjs +1172 -0
- package/engine/core/lib/doctor-customer.mjs +577 -0
- package/engine/core/lib/flow-list.mjs +138 -0
- package/engine/core/lib/flow-registry.mjs +48 -0
- package/engine/core/lib/init.mjs +247 -0
- package/engine/core/lib/ledger.mjs +124 -0
- package/engine/core/lib/license-activate.mjs +813 -0
- package/engine/core/lib/license-device.mjs +314 -0
- package/engine/core/lib/license-file.mjs +349 -0
- package/engine/core/lib/license-guard.mjs +123 -0
- package/engine/core/lib/license-ops.mjs +1645 -0
- package/engine/core/lib/license-verdict.mjs +133 -0
- package/engine/core/lib/license.mjs +577 -0
- package/engine/core/lib/notify/dedup.mjs +101 -0
- package/engine/core/lib/notify/feishu.mjs +38 -0
- package/engine/core/lib/notify/index.mjs +95 -0
- package/engine/core/lib/notify/verb.mjs +327 -0
- package/engine/core/lib/notify/webhook.mjs +135 -0
- package/engine/core/lib/notify/wecom.mjs +36 -0
- package/engine/core/lib/python.mjs +145 -0
- package/engine/core/lib/sop.mjs +116 -0
- package/engine/core/lib/versions.mjs +205 -0
- package/engine/core/mcp/client.mjs +67 -0
- package/engine/core/mcp/http.mjs +138 -0
- package/engine/core/mcp/server.mjs +702 -0
- package/engine/scripts/_engine_rules_probe.py +15 -0
- package/engine/scripts/analyze-full-day.py +568 -0
- package/engine/scripts/archive/feishu-bridge.js +246 -0
- package/engine/scripts/argent-agents.mjs +527 -0
- package/engine/scripts/argent-capabilities.mjs +776 -0
- package/engine/scripts/argent-catalog.mjs +186 -0
- package/engine/scripts/argent-datadir.py +62 -0
- package/engine/scripts/argent-doctor.mjs +624 -0
- package/engine/scripts/argent-env-write.mjs +302 -0
- package/engine/scripts/argent-schedule.mjs +620 -0
- package/engine/scripts/argent-setup-check.mjs +369 -0
- package/engine/scripts/argent-upgrade.mjs +1046 -0
- package/engine/scripts/base-bootstrap.sh +575 -0
- package/engine/scripts/billing-mode-daily.sh +12 -0
- package/engine/scripts/build-engine-package.mjs +123 -0
- package/engine/scripts/capability-packs.lock.json +484 -0
- package/engine/scripts/capability_pack.py +1242 -0
- package/engine/scripts/check-1.0-criteria.mjs +355 -0
- package/engine/scripts/check-agent-catalog.mjs +141 -0
- package/engine/scripts/check-billing-mode.sh +377 -0
- package/engine/scripts/check-child-sessions.mjs +83 -0
- package/engine/scripts/check-config-catalog.mjs +677 -0
- package/engine/scripts/check-docs-drift.mjs +522 -0
- package/engine/scripts/check-dsh-web-expose.sh +98 -0
- package/engine/scripts/check-judgment-drift.mjs +403 -0
- package/engine/scripts/check-license.mjs +199 -0
- package/engine/scripts/check-model-auth.py +443 -0
- package/engine/scripts/check-pack-closure.mjs +661 -0
- package/engine/scripts/check-portable-symbols.mjs +116 -0
- package/engine/scripts/check-preset-landing.mjs +124 -0
- package/engine/scripts/check-production-home.mjs +602 -0
- package/engine/scripts/check-role-mapping.mjs +461 -0
- package/engine/scripts/check-sample-redaction.mjs +320 -0
- package/engine/scripts/check-skill-classification.mjs +188 -0
- package/engine/scripts/check-sop-consistency.mjs +431 -0
- package/engine/scripts/check-test-env-safety.mjs +627 -0
- package/engine/scripts/check-version-consistency.mjs +452 -0
- package/engine/scripts/ci-run.sh +558 -0
- package/engine/scripts/config-explain.mjs +278 -0
- package/engine/scripts/configure-provider.rb +29 -0
- package/engine/scripts/customer-config-migrate.mjs +179 -0
- package/engine/scripts/customer-preflight.py +892 -0
- package/engine/scripts/customer-readiness.sh +134 -0
- package/engine/scripts/daily-report-inbox.py +242 -0
- package/engine/scripts/daily-report-reconcile.mjs +312 -0
- package/engine/scripts/daily-report.mjs +1310 -0
- package/engine/scripts/daily-report.sh +46 -0
- package/engine/scripts/data/lip-product-dict.json +306 -0
- package/engine/scripts/deliver-customer.mjs +932 -0
- package/engine/scripts/delivery-faces.txt +31 -0
- package/engine/scripts/desktop-trial.sh +146 -0
- package/engine/scripts/dev-window.sh +260 -0
- package/engine/scripts/drill-clean-machine.sh +315 -0
- package/engine/scripts/dsh-api-client.mjs +215 -0
- package/engine/scripts/dsh-backup-scope.sh +83 -0
- package/engine/scripts/dsh-offline-pack.mjs +77 -0
- package/engine/scripts/dsh-overlay-pin.sh +87 -0
- package/engine/scripts/dsh-plane.mjs +153 -0
- package/engine/scripts/dsh-portforward.py +223 -0
- package/engine/scripts/dsh-preflight.mjs +179 -0
- package/engine/scripts/dsh-release-watch-daily.sh +19 -0
- package/engine/scripts/dsh-release-watch.mjs +458 -0
- package/engine/scripts/dsh-remote-check.sh +99 -0
- package/engine/scripts/dsh-restart.sh +179 -0
- package/engine/scripts/dsh-restore.sh +111 -0
- package/engine/scripts/dsh-scope-rewire.mjs +803 -0
- package/engine/scripts/dsh-snapshot.mjs +116 -0
- package/engine/scripts/dsh-upgrade-readiness.mjs +165 -0
- package/engine/scripts/dsh-upgrade.mjs +612 -0
- package/engine/scripts/dsh-url.sh +86 -0
- package/engine/scripts/dsh-web-launch.mjs +155 -0
- package/engine/scripts/ensure-store-windows.py +509 -0
- package/engine/scripts/entitlements.py +458 -0
- package/engine/scripts/env_probe.py +411 -0
- package/engine/scripts/feishu-card.mjs +252 -0
- package/engine/scripts/fix-skill-frontmatter.py +209 -0
- package/engine/scripts/flow-auto-retry.py +454 -0
- package/engine/scripts/flow-requirements.json +720 -0
- package/engine/scripts/flow-scaffold.py +275 -0
- package/engine/scripts/flow-timers-plist.sh +86 -0
- package/engine/scripts/flow-timers.sh +182 -0
- package/engine/scripts/flow-trigger-wake.mjs +284 -0
- package/engine/scripts/flow-trigger.sh +571 -0
- package/engine/scripts/flow_billing.py +354 -0
- package/engine/scripts/flow_capability_stats.py +314 -0
- package/engine/scripts/flow_case.py +194 -0
- package/engine/scripts/flow_claim.py +497 -0
- package/engine/scripts/flow_config.py +1024 -0
- package/engine/scripts/flow_failure.py +588 -0
- package/engine/scripts/flow_learn.py +105 -0
- package/engine/scripts/flow_ledger.py +235 -0
- package/engine/scripts/flow_policy.py +238 -0
- package/engine/scripts/flow_preflight.py +339 -0
- package/engine/scripts/flow_registry.py +341 -0
- package/engine/scripts/flow_retry.py +304 -0
- package/engine/scripts/flow_return_to_ops.py +191 -0
- package/engine/scripts/flow_source.py +315 -0
- package/engine/scripts/flow_sources/__init__.py +53 -0
- package/engine/scripts/flow_sources/cases_json.py +106 -0
- package/engine/scripts/flow_sources/common.py +428 -0
- package/engine/scripts/flow_sources/feishu_base.py +273 -0
- package/engine/scripts/flow_stale.py +185 -0
- package/engine/scripts/flow_status_reconcile.py +344 -0
- package/engine/scripts/flow_triage.py +849 -0
- package/engine/scripts/flow_write_verify.py +496 -0
- package/engine/scripts/fulfillment.js +586 -0
- package/engine/scripts/gen-agent-bundles.mjs +276 -0
- package/engine/scripts/gen-preset-rows.mjs +259 -0
- package/engine/scripts/init-ml.sh +2275 -0
- package/engine/scripts/install-internal-role.sh +190 -0
- package/engine/scripts/install-license.mjs +641 -0
- package/engine/scripts/install-one-shot.mjs +665 -0
- package/engine/scripts/integration_contract.py +220 -0
- package/engine/scripts/integrations/wenyao/adapter.py +98 -0
- package/engine/scripts/integrations/xzh/adapter.py +75 -0
- package/engine/scripts/invoice-mapping.example.json +40 -0
- package/engine/scripts/invoice-table.sh +194 -0
- package/engine/scripts/invoice_carrier_notice.py +279 -0
- package/engine/scripts/invoice_fail_closeout.py +297 -0
- package/engine/scripts/invoice_lip_runner.py +1508 -0
- package/engine/scripts/invoice_lip_runner.py.bak-20260922-1710-freready +1090 -0
- package/engine/scripts/invoice_ready_audit.py +191 -0
- package/engine/scripts/invoice_runner.py +675 -0
- package/engine/scripts/invoice_stageb.py +516 -0
- package/engine/scripts/launchd/com.whyshu.argent-bridge.plist.example +42 -0
- package/engine/scripts/launchd/com.whyshu.fulfillment-poll.plist.example +29 -0
- package/engine/scripts/lease.py +377 -0
- package/engine/scripts/lib/agent-install.mjs +649 -0
- package/engine/scripts/lib/argent-datadir.mjs +16 -0
- package/engine/scripts/lib/argent_datadir.py +262 -0
- package/engine/scripts/lib/argent_filelock.py +507 -0
- package/engine/scripts/lib/bootcheck-wiring.mjs +510 -0
- package/engine/scripts/lib/cdp_ports.py +197 -0
- package/engine/scripts/lib/customer-env.mjs +102 -0
- package/engine/scripts/lib/daily-report-core.mjs +517 -0
- package/engine/scripts/lib/dep-declaration.mjs +81 -0
- package/engine/scripts/lib/deps-migrate.mjs +310 -0
- package/engine/scripts/lib/dsh-host-resolve.mjs +109 -0
- package/engine/scripts/lib/dsh-scope.mjs +500 -0
- package/engine/scripts/lib/dsh-snapshot-path.mjs +35 -0
- package/engine/scripts/lib/dsh-versions.mjs +96 -0
- package/engine/scripts/lib/dsh-web-probe.mjs +214 -0
- package/engine/scripts/lib/is-direct-run.mjs +61 -0
- package/engine/scripts/lib/patch-compose.mjs +241 -0
- package/engine/scripts/lib/patch-target.mjs +35 -0
- package/engine/scripts/lib/plugin-lifecycle.mjs +128 -0
- package/engine/scripts/lib/plugin-packages.mjs +332 -0
- package/engine/scripts/lib/preset-compat.mjs +127 -0
- package/engine/scripts/lib/preset-install.mjs +575 -0
- package/engine/scripts/lib/preset-mechanism.mjs +56 -0
- package/engine/scripts/lib/preset-sync.mjs +59 -0
- package/engine/scripts/lib/profile-retire.mjs +97 -0
- package/engine/scripts/lib/python-runtime.mjs +190 -0
- package/engine/scripts/lib/registry.mjs +72 -0
- package/engine/scripts/lib/repo-root.sh +56 -0
- package/engine/scripts/lib/schedule-mechanism.mjs +659 -0
- package/engine/scripts/lib/schedule-probe.mjs +835 -0
- package/engine/scripts/lib/scope-pointer.mjs +164 -0
- package/engine/scripts/lib/settings-panels.mjs +816 -0
- package/engine/scripts/lib/stdio_utf8.py +86 -0
- package/engine/scripts/lib/wake-auth.mjs +392 -0
- package/engine/scripts/lib/webdriver_ipc.py +264 -0
- package/engine/scripts/lib/zstd-frames.mjs +104 -0
- package/engine/scripts/license-e2e-drill.mjs +426 -0
- package/engine/scripts/license-gate.mjs +245 -0
- package/engine/scripts/lip_backlog.py +171 -0
- package/engine/scripts/lip_table.py +185 -0
- package/engine/scripts/local-usage.mjs +214 -0
- package/engine/scripts/migrate-providers.mjs +127 -0
- package/engine/scripts/migrate-skills.sh +65 -0
- package/engine/scripts/ml-oauth-setup.mjs +550 -0
- package/engine/scripts/ml-store-ids.json +26 -0
- package/engine/scripts/ml_artifact_attribution.py +333 -0
- package/engine/scripts/ml_capacity_probe.py +1158 -0
- package/engine/scripts/ml_hub_status.py +1556 -0
- package/engine/scripts/ml_schema_check.py +115 -0
- package/engine/scripts/ml_store_ids.py +139 -0
- package/engine/scripts/npm-publish-argent.mjs +499 -0
- package/engine/scripts/npm-publish-argent.sh +20 -0
- package/engine/scripts/orchestrator.py +13811 -0
- package/engine/scripts/pack-plugins.mjs +140 -0
- package/engine/scripts/patches/dsh-llm-deepseek--transport-diagnostics.patch +297 -0
- package/engine/scripts/patches/dsh-session--append-message-shape.patch +15 -0
- package/engine/scripts/patches/dsh-session--tool-result-block-017.patch +28 -0
- package/engine/scripts/patches/dsh-session-format-v0-to-v1--permission-preset-origin.patch +11 -0
- package/engine/scripts/patches/dsh-session-format-v3-to-v4--turn-outside-replace.patch +20 -0
- package/engine/scripts/patches/dsh-session-persistence-jsonl--worker-bundled-migrators.patch +39 -0
- package/engine/scripts/patches/dsh-tools--render-return-shape.patch +50 -0
- package/engine/scripts/patches/verify-bundled-migrators.mjs +79 -0
- package/engine/scripts/patches/verify-hardening.mjs +167 -0
- package/engine/scripts/patches/verify-session-migration.mjs +149 -0
- package/engine/scripts/patches/verify-transport-diagnostics.mjs +227 -0
- package/engine/scripts/plan-role-rename.mjs +125 -0
- package/engine/scripts/plugin-bootcheck.mjs +451 -0
- package/engine/scripts/plugin-composition.json +15 -0
- package/engine/scripts/poc-mcp-host.mjs +455 -0
- package/engine/scripts/poll-fulfillment.sh +293 -0
- package/engine/scripts/poll-invoice.sh +74 -0
- package/engine/scripts/profile-doctor.mjs +686 -0
- package/engine/scripts/provider-select.sh +70 -0
- package/engine/scripts/provider-switch.sh +146 -0
- package/engine/scripts/provider-sync-target.check.mjs +64 -0
- package/engine/scripts/python-runtime.sh +78 -0
- package/engine/scripts/query_runner.py +310 -0
- package/engine/scripts/release-argent.sh +574 -0
- package/engine/scripts/repair-unresolved-tools.mjs +178 -0
- package/engine/scripts/run-tests.sh +256 -0
- package/engine/scripts/self-sign-license.mjs +179 -0
- package/engine/scripts/session-health-scan.mjs +63 -0
- package/engine/scripts/session-repair-tool-result-content.mjs +181 -0
- package/engine/scripts/setup.sh +22 -0
- package/engine/scripts/skill-install.mjs +629 -0
- package/engine/scripts/sku_translate.py +148 -0
- package/engine/scripts/sync-plugins.sh +90 -0
- package/engine/scripts/sync-presets.sh +27 -0
- package/engine/scripts/sync-scripts.sh +445 -0
- package/engine/scripts/telemetry.py +873 -0
- package/engine/scripts/ui-copy-count.mjs +220 -0
- package/engine/scripts/usage-anchor.sh +136 -0
- package/engine/scripts/usage_report.py +531 -0
- package/engine/scripts/validate-preset-schema.mjs +208 -0
- package/engine/scripts/vertical-init.mjs +323 -0
- package/engine/scripts/vertical-lint.mjs +482 -0
- package/engine/scripts/wenyao-browser.sh +73 -0
- package/engine/scripts/wenyao-fixture-export.py +116 -0
- package/engine/scripts/wenyao-invoice-writeback.mjs +540 -0
- package/engine/scripts/wenyao-login.sh +34 -0
- package/engine/scripts/wenyao-template-map.json +292 -0
- package/engine/scripts/wenyao_attach.py +131 -0
- package/engine/scripts/wenyao_download_template.py +144 -0
- package/engine/scripts/wenyao_fill_template.py +568 -0
- package/engine/scripts/wenyao_login.py +145 -0
- package/engine/scripts/wenyao_portal.py +633 -0
- package/engine/scripts/wenyao_recon.py +230 -0
- package/engine/scripts/wenyao_run.py +503 -0
- package/engine/scripts/wenyao_shadow_diff.py +160 -0
- package/engine/scripts/wenyao_skill_contract.py +156 -0
- package/engine/scripts/wenyao_template_fetch.py +175 -0
- package/engine/scripts/win/dsh-web-autostart.ps1 +372 -0
- package/engine/scripts/ziniao-auth-probe.sh +268 -0
- package/engine/scripts/ziniao-keepalive.sh +216 -0
- package/engine/scripts/ziniao-post-upgrade-check.sh +120 -0
- package/engine/scripts/ziniao-windows.sh +153 -0
- package/engine/version.mjs +1 -0
- package/engine/verticals/README.md +103 -0
- package/engine/verticals/_engine-words.baseline.txt +237 -0
- package/engine/verticals/_engine-words.json +19 -0
- package/engine/verticals/mercadolibre/README.md +43 -0
- package/engine/verticals/mercadolibre/config.json +709 -0
- package/engine/verticals/mercadolibre/flows.json +706 -0
- package/engine/verticals/mercadolibre/manifest.json +538 -0
- package/engine/verticals/mercadolibre/migrate-machine-state.mjs +138 -0
- package/engine/verticals/mercadolibre/preflight.py +143 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.failure.md +69 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.md +47 -0
- package/engine/verticals/mercadolibre/prompts/daily-report.scheduled.md +79 -0
- package/engine/verticals/mercadolibre/prompts/full.failure.md +125 -0
- package/engine/verticals/mercadolibre/prompts/full.md +91 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.failure.md +88 -0
- package/engine/verticals/mercadolibre/prompts/wenyao-invoice.md +64 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.failure.md +91 -0
- package/engine/verticals/mercadolibre/prompts/xzh-invoice.md +46 -0
- package/package.json +21 -4
- package/README.md +0 -3
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 离线签名许可(客户端验签,2026-09-30)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要它
|
|
5
|
+
* 客户机是「BYO(自带 DeepSeek Key)+ **不登录问述账号**」⇒ 机器上没有账号 JWT,
|
|
6
|
+
* `/api/argent/entitlements`、`/api/argent/lease` 这类要鉴权的接口一个都调不了。
|
|
7
|
+
* 授权改为一份由服务端**离线签名**的许可文件(`~/.dsh/argent/license.json`):
|
|
8
|
+
* 装机时由 `scripts/install-license.mjs` 验签后落盘,之后**断网也能自证授权**。
|
|
9
|
+
*
|
|
10
|
+
* ## 签名契约(与服务端 `whyshu-svr/src/services/licenseFile.js` **逐字节一致**,改一侧必须同步另一侧)
|
|
11
|
+
* payload = canonicalJson({ v, userId, capabilities, verticals, seats, issuedAt, expiresAt, graceDays, instanceHint })
|
|
12
|
+
* sig = HMAC-SHA256(LICENSE_SIGNING_SECRET, payload) 的 hex(小写)
|
|
13
|
+
* canonicalJson = 键按字典序、**无空白**、数组保序(嵌套同样规则)
|
|
14
|
+
* v1 的签名字段集**固定为上面 9 项**(多余字段不参与签名)。
|
|
15
|
+
*
|
|
16
|
+
* ## v2(2026-10-03):把 `device` 纳入签名 —— 精确契约(跨仓库唯一口径)
|
|
17
|
+
* ```text
|
|
18
|
+
* payloadV2 = canonicalJson({
|
|
19
|
+
* v: 2, userId, capabilities, verticals, seats, issuedAt, expiresAt, graceDays, instanceHint, // ← 与 v1 同 9 项
|
|
20
|
+
* device: { code, algo, issuedAt } // ← 仅当**声明**了设备
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
* 规则(三处必须逐字节一致:本模块 / 服务端 `licenseFile.js` / `plugins/argent-core` 的内联退路):
|
|
24
|
+
* ① **字段集随 `v` 定**:`v:1` ⇒ 恰好 9 项(`device` **被忽略**,哪怕写了也不进签名 —— v1 判据一字不改);
|
|
25
|
+
* `v:2` ⇒ 9 项 + **device 键**。`v` 不在 {1,2} ⇒ `bad_version`。
|
|
26
|
+
* ② **"缺位"也是被签名的状态**:`v:2` 未声明设备(`device` 键不存在 **或** 值为 `null`)⇒ payloadV2 **不含**
|
|
27
|
+
* `device` 键。⇒ 事后往一份"未声明"的 v2 许可里塞 `device` 会改变 payload ⇒ **bad_signature**。
|
|
28
|
+
* ③ 从一份"已声明"的 v2 许可里**摘掉** `device`(保留 `v:2`,或把 `v` 降成 1 重拼载荷)⇒ payload 变 ⇒
|
|
29
|
+
* **bad_signature**(这就是 v1 做不到的那件事:v1 里改 `device` 不影响验签)。
|
|
30
|
+
* ④ `device` 声明的三键**必须写全**(`code`/`algo`/`issuedAt`);缺任一或形状非法 ⇒ **malformed**
|
|
31
|
+
* (**不得**降级成 unbound:那会把"绑定写坏了"静默变成"没绑定"= 一次静默的授权放宽)。
|
|
32
|
+
* `device` 内的**额外键不参与签名**,客户端也**不得**读(判据只有这三键)。
|
|
33
|
+
* ⑤ `device.issuedAt` 与 `issuedAt`/`expiresAt` 同规:ISO(UTC, 毫秒) —— 服务端必须写 `toISOString()` 形态。
|
|
34
|
+
* ⑥ `device` 必须**经 v2 验签通过**才可采信:`loadLicenseFile().deviceSigned`(= `签名版本 2 ∧ 验签通过`)
|
|
35
|
+
* 是唯一的机器可判形式;v1 的 `device` 仍只是"**服务端声明 + 客户端比对**"(`deviceSigned:false`)。
|
|
36
|
+
* ⚠️ **v2 也不是"防伪"的全部**(如实标注):签名是 **HMAC-SHA256(对称密钥)**,而验签方**必须持有**
|
|
37
|
+
* 同一把密钥 ⇒ 能读 `license.secret` 的人**仍然可以自签一份**。v2 堵掉的是**最低成本的那条绕过**
|
|
38
|
+
* (改字段/摘字段不改签名)与"混用/降级重拼";要真正防伪造得换成**非对称签名**(Ed25519,客户端只拿公钥,
|
|
39
|
+
* 见 §3.6③ 与 `docs/Argent-陪跑式交付-实施方案.md` 服务端方案的 v3 候选)。
|
|
40
|
+
*
|
|
41
|
+
* ## 密钥从哪来(**绝不硬编码**)
|
|
42
|
+
* ① 环境变量 `ARGENT_LICENSE_SECRET`;② `~/.dsh/argent/license.secret`(600)。
|
|
43
|
+
* 两处都没有 ⇒ `reason: 'no_secret'` —— **拒绝**,而不是"没密钥就当通过"。
|
|
44
|
+
*
|
|
45
|
+
* ## 口径(与服务端 entitlement.checkAccess 对齐)
|
|
46
|
+
* capabilities:**白名单**,空数组 = 什么能力都没有(不是"全部");
|
|
47
|
+
* verticals :空数组 = 不限业务线;
|
|
48
|
+
* 到期 :`expiresAt` 之后仍在 `graceDays` 内 ⇒ `ok:true` + `inGrace:true`(放行但必须告警);
|
|
49
|
+
* 超出宽限 ⇒ `ok:false` + `reason:'expired'`(fail-closed)。
|
|
50
|
+
*/
|
|
51
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
52
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
53
|
+
import { resolveDataDir } from './argent-datadir.mjs';
|
|
54
|
+
import { deviceClaimForSignature, deviceClaimOf } from './license-device.mjs';
|
|
55
|
+
import { join } from 'node:path';
|
|
56
|
+
|
|
57
|
+
/** v1 = 默认版本(判据一字不改);v2 = 把 `device` 纳入签名(2026-10-03)。 */
|
|
58
|
+
export const LICENSE_FILE_VERSION = 1;
|
|
59
|
+
export const LICENSE_FILE_VERSION_MAX = 2;
|
|
60
|
+
/** 本客户端认识的签名版本(`v` 不在此列 ⇒ `bad_version`)。 */
|
|
61
|
+
export const LICENSE_SIGNATURE_VERSIONS = [1, 2];
|
|
62
|
+
export const DEFAULT_GRACE_DAYS = 14;
|
|
63
|
+
/** v1 参与签名的字段集(顺序无关,规范化时按键排序)。 */
|
|
64
|
+
export const LICENSE_FILE_FIELDS = [
|
|
65
|
+
'v', 'userId', 'capabilities', 'verticals', 'seats', 'issuedAt', 'expiresAt', 'graceDays', 'instanceHint',
|
|
66
|
+
];
|
|
67
|
+
/** v2 参与签名的字段集 = v1 的 9 项 + `device`(**仅当声明了设备**;缺位时 payload 里不含该键)。 */
|
|
68
|
+
export const LICENSE_FILE_V2_FIELDS = [...LICENSE_FILE_FIELDS, 'device'];
|
|
69
|
+
/** `device` 声明进签名的**恰好**三键(额外键不参与签名、客户端不读)。 */
|
|
70
|
+
export const DEVICE_CLAIM_FIELDS = ['code', 'algo', 'issuedAt'];
|
|
71
|
+
const DAY_MS = 86400000;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 数据目录:**唯一判据在内核** `core/lib/argent-datadir.mjs`
|
|
75
|
+
* (`ARGENT_DATA_DIR → $DSH_HOME/argent → ~/.dsh/argent`,2026-10-01 统一)。
|
|
76
|
+
* 此前这里只看 `ARGENT_DATA_DIR`、**不看 `DSH_HOME`** ⇒ 桌面端隔离实例会读生产许可文件。
|
|
77
|
+
* ⚠️ 与 `plugins/argent-core` 的 `licenseDataDir()` 必须同判据(那边同样取内核 `DATA_DIR`)。
|
|
78
|
+
*/
|
|
79
|
+
export function dataDir() {
|
|
80
|
+
return resolveDataDir({ env: process.env }).dir;
|
|
81
|
+
}
|
|
82
|
+
export function defaultLicensePath() {
|
|
83
|
+
return join(dataDir(), 'license.json');
|
|
84
|
+
}
|
|
85
|
+
export function defaultSecretPath() {
|
|
86
|
+
return join(dataDir(), 'license.secret');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 规范化 JSON:键按字典序、无空格、数组保序。
|
|
91
|
+
* 必须与服务端逐字节一致 —— 任何"更漂亮"的写法都会让两侧哈希漂移(历史教训:租约签名串漂移过一次)。
|
|
92
|
+
*/
|
|
93
|
+
export function canonicalJson(value) {
|
|
94
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value ?? null);
|
|
95
|
+
if (Array.isArray(value)) return `[${value.map((v) => canonicalJson(v)).join(',')}]`;
|
|
96
|
+
const keys = Object.keys(value).sort();
|
|
97
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(',')}}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 时间一律归一成 ISO(UTC,毫秒精度),容忍 `+08:00` / 无毫秒的写法。 */
|
|
101
|
+
function toIso(v) {
|
|
102
|
+
if (v === null || v === undefined || v === '') return '';
|
|
103
|
+
const d = new Date(v);
|
|
104
|
+
return Number.isNaN(d.getTime()) ? '' : d.toISOString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const uniqSorted = (arr) => [...new Set((Array.isArray(arr) ? arr : []).map((x) => String(x)).filter(Boolean))].sort();
|
|
108
|
+
|
|
109
|
+
/** 归一化到 v1 字段集;时间不可解析 ⇒ null(判 malformed,绝不"宽容"成永不过期)。 */
|
|
110
|
+
export function normalizeLicenseFile(file) {
|
|
111
|
+
const f = file && typeof file === 'object' ? file : {};
|
|
112
|
+
const issuedAt = toIso(f.issuedAt);
|
|
113
|
+
const expiresAt = toIso(f.expiresAt);
|
|
114
|
+
if (!issuedAt || !expiresAt) return null;
|
|
115
|
+
const graceDays = Number(f.graceDays ?? DEFAULT_GRACE_DAYS);
|
|
116
|
+
return {
|
|
117
|
+
v: Number(f.v ?? LICENSE_FILE_VERSION),
|
|
118
|
+
userId: String(f.userId ?? ''),
|
|
119
|
+
capabilities: uniqSorted(f.capabilities),
|
|
120
|
+
verticals: uniqSorted(f.verticals),
|
|
121
|
+
seats: Number(f.seats ?? 1),
|
|
122
|
+
issuedAt,
|
|
123
|
+
expiresAt,
|
|
124
|
+
graceDays: Number.isFinite(graceDays) && graceDays >= 0 ? graceDays : DEFAULT_GRACE_DAYS,
|
|
125
|
+
instanceHint: String(f.instanceHint ?? ''),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* v2 归一(**只增不改** v1):v1 的 9 项(`v` 替换为 2)+ 可选的 `device`。
|
|
131
|
+
* · `device` 缺位(键不存在或 null)⇒ 结果里**没有** `device` 键(缺位本身进签名)
|
|
132
|
+
* · `device` 存在 ⇒ 必须三键合法(形状判据在 `license-device.mjs#deviceClaimForSignature`)
|
|
133
|
+
* @returns {object|null} 时间不可解析/`device` 形状非法 ⇒ null(调用方按 `malformed` 拒绝)
|
|
134
|
+
*/
|
|
135
|
+
export function normalizeLicenseFileV2(file) {
|
|
136
|
+
const base = normalizeLicenseFile(file);
|
|
137
|
+
if (!base) return null;
|
|
138
|
+
const claim = deviceClaimForSignature(file);
|
|
139
|
+
if (!claim.ok) return null;
|
|
140
|
+
const out = { ...base, v: 2 };
|
|
141
|
+
if (claim.device) out.device = claim.device; // 只放签名的三键;额外键不入载荷
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 签名版本(1 / 2);不认识(缺失、非数字、v3…)⇒ null。 */
|
|
146
|
+
export function signatureVersionOf(file) {
|
|
147
|
+
const v = Number(file && typeof file === 'object' ? file.v : NaN);
|
|
148
|
+
return LICENSE_SIGNATURE_VERSIONS.includes(v) ? v : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 按 `v` 取签名载荷(v1 9 项 / v2 9 项 + 可选 device);形状非法 ⇒ null。 */
|
|
152
|
+
export function signaturePayloadOf(file) {
|
|
153
|
+
const version = signatureVersionOf(file);
|
|
154
|
+
if (version === null) return null;
|
|
155
|
+
const normalized = version === 2 ? normalizeLicenseFileV2(file) : normalizeLicenseFile(file);
|
|
156
|
+
return normalized ? { version, normalized, payload: canonicalJson(normalized) } : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* **本地签名**(自签 / 离线演练 / 与服务端同口径的签发实现)。
|
|
161
|
+
*
|
|
162
|
+
* ⛔ 它**不是**签发服务:真正的签发在服务端(`whyshu-svr`)。本函数存在的理由有两个 ——
|
|
163
|
+
* ① 让"离线激活"闭环可在**没有服务端**的场合演练(`license request` → 自签 → `install --file`);
|
|
164
|
+
* ② 把 v2 载荷的构造**只写一份**(`scripts/install-license.mjs --sign-v2` 与自签工具都用它),
|
|
165
|
+
* 否则第二个实现早晚与契约漂移。
|
|
166
|
+
* ⚠️ 它**不扩权**:`capabilities/verticals/seats/issuedAt/expiresAt/graceDays` 逐字取自入参文件,
|
|
167
|
+
* 调用方必须先 `verifyLicenseFile()` 验过原件再重签(`install-license.mjs` 就是这么做的)。
|
|
168
|
+
*
|
|
169
|
+
* @param {object} file 许可(v1 或 v2 形态;`v` 由 `version` 决定)
|
|
170
|
+
* @param {string} secret 签名密钥
|
|
171
|
+
* @param {object} [opts] { version=1|2, now=Date }
|
|
172
|
+
* @returns {{ok:true, file:object, payload:string, signatureVersion:number}|{ok:false, reason:string, warning?:string}}
|
|
173
|
+
*/
|
|
174
|
+
export function signLicenseFile(file, secret, { version = LICENSE_FILE_VERSION, now = new Date() } = {}) {
|
|
175
|
+
const v = Number(version);
|
|
176
|
+
if (!LICENSE_SIGNATURE_VERSIONS.includes(v)) return { ok: false, reason: 'bad_version' };
|
|
177
|
+
const key = String(secret || '');
|
|
178
|
+
if (!key) return { ok: false, reason: 'no_secret' };
|
|
179
|
+
const src = { ...(file && typeof file === 'object' && !Array.isArray(file) ? file : {}), v };
|
|
180
|
+
if (v === 2) {
|
|
181
|
+
const claim = deviceClaimForSignature(src);
|
|
182
|
+
if (!claim.ok) return { ok: false, reason: 'malformed', warning: claim.warning };
|
|
183
|
+
}
|
|
184
|
+
const normalized = v === 2 ? normalizeLicenseFileV2(src) : normalizeLicenseFile(src);
|
|
185
|
+
if (!normalized || !normalized.userId) return { ok: false, reason: 'malformed' };
|
|
186
|
+
// **只签不改**:本函数不动任何字段值(`issuedAt` 由调用方决定 —— 服务端 `buildLicenseFile`
|
|
187
|
+
// 自己把 issuedAt 设成"这份文件的签发时刻";本地重签是否刷新它由 `install-license --sign-v2` 定)。
|
|
188
|
+
const payload = canonicalJson(normalized);
|
|
189
|
+
const sig = createHmac('sha256', key).update(payload).digest('hex');
|
|
190
|
+
return { ok: true, file: { ...normalized, sig }, payload, signatureVersion: v };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** 宽限截止时刻(ISO);到期时间不可解析 ⇒ null。 */
|
|
194
|
+
export function graceUntilOf(file) { const exp = new Date(file?.expiresAt).getTime();
|
|
195
|
+
if (!Number.isFinite(exp)) return null;
|
|
196
|
+
const g = Number(file?.graceDays ?? DEFAULT_GRACE_DAYS);
|
|
197
|
+
return new Date(exp + (Number.isFinite(g) && g >= 0 ? g : DEFAULT_GRACE_DAYS) * DAY_MS).toISOString();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 取签名密钥:`ARGENT_LICENSE_SECRET` → `~/.dsh/argent/license.secret`(600)。
|
|
202
|
+
* @returns {{secret:string, source:string, warning:string|null}}
|
|
203
|
+
*/
|
|
204
|
+
export function resolveSecret({ secret, secretPath } = {}) {
|
|
205
|
+
if (secret) return { secret: String(secret), source: 'explicit', warning: null };
|
|
206
|
+
const fromEnv = String(process.env.ARGENT_LICENSE_SECRET || '').trim();
|
|
207
|
+
if (fromEnv) return { secret: fromEnv, source: 'env', warning: null };
|
|
208
|
+
const p = secretPath || defaultSecretPath();
|
|
209
|
+
if (!existsSync(p)) return { secret: '', source: 'none', warning: null };
|
|
210
|
+
let warning = null;
|
|
211
|
+
try {
|
|
212
|
+
const mode = statSync(p).mode & 0o777;
|
|
213
|
+
// 权限过松只告警不拒绝:许可本身是签名的(泄漏的是"能不能验签"而不是"能不能伪造"),
|
|
214
|
+
// 但密钥文件可读 = 同机其他用户可以伪造许可 ⇒ 必须响亮提示(install-license.mjs 落盘时强制 600)。
|
|
215
|
+
if (mode & 0o077) warning = `密钥文件权限过松(0${mode.toString(8)},应为 0600):${p}`;
|
|
216
|
+
} catch { /* stat 失败不影响读取 */ }
|
|
217
|
+
let text = '';
|
|
218
|
+
try { text = readFileSync(p, 'utf-8').trim(); } catch { /* 读不到按缺密钥处理 */ }
|
|
219
|
+
return { secret: text, source: text ? 'file' : 'none', warning };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** 空结果(缺文件/缺密钥/坏文件统一形状,调用方不用判字段存在性)。 */
|
|
223
|
+
function emptyResult(reason, extra = {}) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false, reason, capabilities: [], verticals: [], userId: '', seats: 0,
|
|
226
|
+
instanceHint: '', issuedAt: '', expiresAt: '', graceDays: DEFAULT_GRACE_DAYS,
|
|
227
|
+
graceUntil: null, inGrace: false, warnings: [],
|
|
228
|
+
// 签名版本 / 设备是否进签名:**判不出来就 null / false**(绝不乐观默认)
|
|
229
|
+
signatureVersion: null, deviceSigned: false, ...extra,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 验签 + 判期(**唯一判定入口**)。
|
|
235
|
+
*
|
|
236
|
+
* v1 / v2 的分派在**这里**(唯一一处):`v:1` 走 9 项载荷(判据与 2026-09-30 版**逐字一致**);
|
|
237
|
+
* `v:2` 走 9 项 + 可选 `device` 的载荷。载荷定义见文件头 "v2 精确契约"。
|
|
238
|
+
*
|
|
239
|
+
* @param {object} obj 许可文件内容(已 JSON.parse)
|
|
240
|
+
* @param {object} opts { secret, now }(secret 缺省按 resolveSecret 取;now 供测试注入)
|
|
241
|
+
* @returns {{ok:boolean, reason:string, capabilities:string[], verticals:string[], userId:string,
|
|
242
|
+
* seats:number, issuedAt:string, expiresAt:string, graceDays:number,
|
|
243
|
+
* graceUntil:string|null, inGrace:boolean, warnings:string[],
|
|
244
|
+
* signatureVersion:1|2|null, deviceSigned:boolean}}
|
|
245
|
+
* reason:ok | in_grace | bad_signature | expired | malformed | bad_version | no_secret
|
|
246
|
+
* `deviceSigned` = **签名版本为 2 且验签通过**(此刻 `device` 声明的内容/缺位才真的被覆盖);
|
|
247
|
+
* 验签失败/版本未知一律 `false` —— 免得"没验过的字段"被读成已验签(§3.6③ 的机器可判形式)。
|
|
248
|
+
*/
|
|
249
|
+
export function verifyLicenseFile(obj, { secret, now = Date.now(), secretPath } = {}) {
|
|
250
|
+
const key = resolveSecret({ secret, secretPath });
|
|
251
|
+
if (!key.secret) return emptyResult('no_secret', { warnings: key.warning ? [key.warning] : [] });
|
|
252
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return emptyResult('malformed');
|
|
253
|
+
const version = signatureVersionOf(obj);
|
|
254
|
+
if (version === null) return emptyResult('bad_version');
|
|
255
|
+
const warnings = key.warning ? [key.warning] : [];
|
|
256
|
+
// ── 形状归一(**先形状、后验签**:形状坏 ⇒ malformed,不做"猜一半"的验签)──
|
|
257
|
+
let normalized = null;
|
|
258
|
+
if (version === 2) {
|
|
259
|
+
const claim = deviceClaimForSignature(obj);
|
|
260
|
+
// device 形状坏 ⇒ **拒绝**(不降级成 unbound):否则"绑定写坏了"会静默变成"没绑定"
|
|
261
|
+
if (!claim.ok) return emptyResult('malformed', { warnings: [...warnings, claim.warning], signatureVersion: 2 });
|
|
262
|
+
normalized = normalizeLicenseFileV2(obj);
|
|
263
|
+
} else {
|
|
264
|
+
normalized = normalizeLicenseFile(obj);
|
|
265
|
+
}
|
|
266
|
+
if (!normalized || !normalized.userId) return emptyResult('malformed', { warnings, signatureVersion: version });
|
|
267
|
+
|
|
268
|
+
const base = {
|
|
269
|
+
capabilities: normalized.capabilities, verticals: normalized.verticals, userId: normalized.userId,
|
|
270
|
+
seats: normalized.seats, instanceHint: normalized.instanceHint, issuedAt: normalized.issuedAt,
|
|
271
|
+
expiresAt: normalized.expiresAt, graceDays: normalized.graceDays, graceUntil: graceUntilOf(normalized),
|
|
272
|
+
signatureVersion: version,
|
|
273
|
+
deviceSigned: false, // 只有**验签通过**才置 true(下面两处出口)
|
|
274
|
+
};
|
|
275
|
+
// 常量时间比较(长度不等直接判否;hex 解不出内容也算否,绝不放行)
|
|
276
|
+
const actual = Buffer.from(String(obj.sig || '').toLowerCase(), 'hex');
|
|
277
|
+
const expected = Buffer.from(createHmac('sha256', key.secret).update(canonicalJson(normalized)).digest('hex'), 'hex');
|
|
278
|
+
if (actual.length !== expected.length || actual.length === 0 || !timingSafeEqual(actual, expected)) {
|
|
279
|
+
return emptyResult('bad_signature', { ...base, warnings });
|
|
280
|
+
}
|
|
281
|
+
const signed = { ...base, deviceSigned: version === 2 };
|
|
282
|
+
const exp = new Date(normalized.expiresAt).getTime();
|
|
283
|
+
const graceUntil = signed.graceUntil;
|
|
284
|
+
if (now > exp) {
|
|
285
|
+
if (now > new Date(graceUntil).getTime()) {
|
|
286
|
+
return emptyResult('expired', { ...signed, warnings: [...warnings, `许可已于 ${normalized.expiresAt} 到期(宽限 ${normalized.graceDays} 天已用完)`] });
|
|
287
|
+
}
|
|
288
|
+
const leftDays = Math.max(0, (new Date(graceUntil).getTime() - now) / DAY_MS);
|
|
289
|
+
return {
|
|
290
|
+
ok: true, reason: 'in_grace', ...signed, inGrace: true,
|
|
291
|
+
warnings: [...warnings, `许可已过期,处于宽限期(剩 ${leftDays.toFixed(1)} 天,至 ${graceUntil})—— 请尽快续期`],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return { ok: true, reason: 'ok', ...signed, inGrace: false, warnings };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 读取并验签本地许可文件。
|
|
299
|
+
*
|
|
300
|
+
* ⚠️ **`deviceClaimed` 是不是"已验签的字段"要看 `deviceSigned`**:
|
|
301
|
+
* · `v:1` ⇒ 签名只覆盖 `LICENSE_FILE_FIELDS` 那 9 项,载荷里多出来的 `device` **不参与签名**
|
|
302
|
+
* ⇒ `deviceSigned:false`:它是"**服务端声明 + 客户端比对**",**不得**被当成已验签字段引用;
|
|
303
|
+
* · `v:2` **验签通过** ⇒ `deviceSigned:true`:`device` 的内容(或它的**缺位**)确实被签名覆盖。
|
|
304
|
+
* 判据只有这一处(`signatureVersion` + `deviceSigned`),调用方不要自己再判一遍版本。
|
|
305
|
+
*
|
|
306
|
+
* @param {string} [path] 缺省 `~/.dsh/argent/license.json`(`ARGENT_DATA_DIR` 可隔离)
|
|
307
|
+
* @returns 同 verifyLicenseFile,外加 `path` / `source` / `mode` / `deviceClaimed`;文件不存在 ⇒ `{ok:false, reason:'missing'}`
|
|
308
|
+
*/
|
|
309
|
+
export function loadLicenseFile(path, { secret, now = Date.now(), secretPath } = {}) {
|
|
310
|
+
const p = path || defaultLicensePath();
|
|
311
|
+
if (!existsSync(p)) return { ...emptyResult('missing'), path: p, deviceClaimed: null };
|
|
312
|
+
let mode = null;
|
|
313
|
+
try { mode = statSync(p).mode & 0o777; } catch { /* 拿不到权限不致命 */ }
|
|
314
|
+
let raw = '';
|
|
315
|
+
try { raw = readFileSync(p, 'utf-8'); } catch (e) {
|
|
316
|
+
return { ...emptyResult('unreadable', { warnings: [String(e.message)] }), path: p, mode, deviceClaimed: null };
|
|
317
|
+
}
|
|
318
|
+
let obj = null;
|
|
319
|
+
try { obj = JSON.parse(raw); } catch {
|
|
320
|
+
return { ...emptyResult('malformed', { warnings: ['许可文件不是合法 JSON'] }), path: p, mode, deviceClaimed: null };
|
|
321
|
+
}
|
|
322
|
+
const r = verifyLicenseFile(obj, { secret, now, secretPath });
|
|
323
|
+
const warnings = [...r.warnings];
|
|
324
|
+
// 0600 检查:只是卫生要求(许可内容是公开可读也不影响安全性),过松就告警,不拦人
|
|
325
|
+
if (mode !== null && (mode & 0o077)) warnings.push(`许可文件权限过松(0${mode.toString(8)},应为 0600):${p}`);
|
|
326
|
+
return { ...r, warnings, path: p, mode, deviceClaimed: deviceClaimOf(obj) };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 能力判定(与服务端 `checkAccess` 同口径):
|
|
331
|
+
* ① 许可未通过验签/已超宽限 ⇒ 拒;② capabilityId 为空 ⇒ 只看许可有效性;
|
|
332
|
+
* ③ 否则 `capabilities` 必须**显式包含**该 id(空数组 = 什么都不许,不是"全部")。
|
|
333
|
+
* @param {object} license **验签过**的许可(verifyLicenseFile/loadLicenseFile 的结果或文件对象)
|
|
334
|
+
* @param {string} capabilityId
|
|
335
|
+
* @param {object} [opts] { now }
|
|
336
|
+
*/
|
|
337
|
+
export function capabilityAllowed(license, capabilityId, { now = Date.now() } = {}) {
|
|
338
|
+
if (!license || typeof license !== 'object') return false;
|
|
339
|
+
if (license.ok === false) return false; // 传进来的是失败结果 ⇒ 直接拒
|
|
340
|
+
const exp = new Date(license.expiresAt).getTime();
|
|
341
|
+
if (!Number.isFinite(exp)) return false; // 判不了到期 ⇒ 拒(fail-closed)
|
|
342
|
+
const g = Number(license.graceDays ?? DEFAULT_GRACE_DAYS);
|
|
343
|
+
const graceUntil = exp + (Number.isFinite(g) && g >= 0 ? g : DEFAULT_GRACE_DAYS) * DAY_MS;
|
|
344
|
+
if (now > graceUntil) return false; // 超出宽限 ⇒ 全部拒
|
|
345
|
+
const id = String(capabilityId || '');
|
|
346
|
+
if (!id) return true;
|
|
347
|
+
const caps = Array.isArray(license.capabilities) ? license.capabilities.map(String) : [];
|
|
348
|
+
return caps.includes(id);
|
|
349
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 写类 verb 的**许可闸门**(§3.6① 第 4 步 + §3.5① 纪律 1)—— 薄封装,**不复制任何判据**。
|
|
3
|
+
*
|
|
4
|
+
* ## 它做什么
|
|
5
|
+
* 「任何写操作前先判 `valid|grace|denied`」:本模块调**既有** `scripts/license-gate.mjs`
|
|
6
|
+
* (flow-trigger.sh:238 用的同一个模块、同一套 `off/observe/enforce` 语义、同一个证据文件),
|
|
7
|
+
* 把它的 `--json` 结论翻译成写类 verb 要的形状。判定与取证**一份都不重写**:
|
|
8
|
+
* · 判定 = `license-gate.mjs` → `evaluate()` → `core/lib/license-verdict.mjs`(三态 + 设备)
|
|
9
|
+
* · 证据 = `<数据目录>/license-gate.jsonl`(每次判定一行;observe 也留,`decision:would_block`)
|
|
10
|
+
*
|
|
11
|
+
* ## 模式(`ARGENT_LICENSE_ENFORCE`,与 flow-trigger.sh 同一套取值)
|
|
12
|
+
* `0`/`off` **不查**(连判定都不做;也不落证据)—— 逃生阀
|
|
13
|
+
* `observe` **默认**:照常判定 + 留证据,但**绝不拦**(`would_block` 只写进证据)
|
|
14
|
+
* `enforce` 判定不可用 ⇒ 写操作被拒:非零 + `reason:"license_denied"`(**零副作用**:不写台账/不落盘)
|
|
15
|
+
*
|
|
16
|
+
* ## 失败形态(与 flow-trigger.sh 同口径:**宁可不拦,不可误停**)
|
|
17
|
+
* 闸门自己跑不起来(脚本缺失/输出不可解析/超时)⇒ **放行 + 响亮告警**,证据里记 `gate_error`。
|
|
18
|
+
* 理由:闸门是我们的零件,坏了不该把客户的业务停掉 —— 与 flow-trigger.sh 对缺 `license-gate.mjs`
|
|
19
|
+
* 的既有处置逐字一致("旧机器:响亮告警但不停派")。
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from 'node:child_process'
|
|
22
|
+
import { existsSync } from 'node:fs'
|
|
23
|
+
import { join } from 'node:path'
|
|
24
|
+
|
|
25
|
+
/** 与 `scripts/license-gate.mjs#normalizeMode` 同取值(这里只做"要不要查"的分支,不重写判定)。 */
|
|
26
|
+
export function gateModeOf(env = process.env) {
|
|
27
|
+
const v = String(env.ARGENT_LICENSE_ENFORCE ?? '').trim().toLowerCase()
|
|
28
|
+
if (['0', 'off', 'false', 'no', 'skip'].includes(v)) return 'off'
|
|
29
|
+
if (['enforce', '1', 'true', 'yes', 'on'].includes(v)) return 'enforce'
|
|
30
|
+
return 'observe'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 跑一次闸门判定。
|
|
35
|
+
*
|
|
36
|
+
* @param {object} o
|
|
37
|
+
* @param {string} o.repo 仓库根(找 `scripts/license-gate.mjs`)
|
|
38
|
+
* @param {string} o.flow 流程名(仅证据/文案)
|
|
39
|
+
* @param {string} [o.cap] 能力 id(缺省用 flow)—— 能力门判据在 check-license 里,不在这里
|
|
40
|
+
* @param {string} [o.vertical] 业务线(缺省不传 ⇒ 只看能力)
|
|
41
|
+
* @param {object} [o.env] 环境(`ARGENT_LICENSE_ENFORCE` / 数据目录)
|
|
42
|
+
* @param {string} [o.mode] 显式指定模式(缺省按 env)
|
|
43
|
+
* @param {Function} [o.spawn] 注入 spawn(夹具)
|
|
44
|
+
* @returns {{checked:boolean, mode:string, allowed:boolean, blocked:boolean, code:number,
|
|
45
|
+
* reason:string, licenseState:string, decision:string, human:string, error:string,
|
|
46
|
+
* evidence:string, verdict:object|null}}
|
|
47
|
+
*/
|
|
48
|
+
export function gateWrite({ repo, flow, cap = '', vertical = null, env = process.env, mode = null, spawn = spawnSync, timeout = 30000 } = {}) {
|
|
49
|
+
const m = mode || gateModeOf(env)
|
|
50
|
+
const base = { checked: false, mode: m, allowed: true, blocked: false, code: 0, reason: 'enforce_off',
|
|
51
|
+
licenseState: 'skipped', decision: 'bypassed', human: '', error: '', verdict: null }
|
|
52
|
+
// `off`:**不查**(不 spawn、不读许可、不落证据)—— 逃生阀的字面含义
|
|
53
|
+
if (m === 'off') return { ...base, human: '许可闸门[off]:逃生阀已开(ARGENT_LICENSE_ENFORCE=off)⇒ 跳过许可判定,照常写' }
|
|
54
|
+
|
|
55
|
+
const script = join(repo, 'scripts', 'license-gate.mjs')
|
|
56
|
+
if (!existsSync(script)) {
|
|
57
|
+
// 旧克隆/半坏机器:**响亮告警但不停**(与 flow-trigger.sh 对缺文件的处置逐字一致)
|
|
58
|
+
return { ...base, checked: false, reason: 'gate_error', error: `缺少 ${script}(旧版本?)⇒ 闸门不生效,本次照常写(enforce 下也一样:不拦)` }
|
|
59
|
+
}
|
|
60
|
+
const args = [script, '--flow', String(flow || '?'), '--cap', String(cap || flow || '?'),
|
|
61
|
+
'--mode', m, '--json', '--quiet']
|
|
62
|
+
if (vertical) args.push('--vertical', String(vertical))
|
|
63
|
+
let r = null
|
|
64
|
+
try { r = spawn(process.execPath, args, { encoding: 'utf8', timeout, env }) } catch (e) {
|
|
65
|
+
return { ...base, reason: 'gate_error', error: `闸门执行异常:${String(e.message || e)}` }
|
|
66
|
+
}
|
|
67
|
+
const out = String((r && r.stdout) || '')
|
|
68
|
+
let j = null
|
|
69
|
+
try { j = JSON.parse(out.slice(out.indexOf('{'))) } catch { j = null }
|
|
70
|
+
if (!j || typeof j.allowed !== 'boolean') {
|
|
71
|
+
const err = String((r && r.stderr) || (r && r.error && r.error.message) || '').trim().slice(0, 200)
|
|
72
|
+
return { ...base, reason: 'gate_error', error: `闸门输出不可解析(exit=${r && r.status}${err ? `: ${err}` : ''})⇒ 不拦` }
|
|
73
|
+
}
|
|
74
|
+
// 证据落在哪:由 license-gate.mjs 自己回报(`--json` 里的 log 字段)——本层不猜路径
|
|
75
|
+
const evidence = j.log || null
|
|
76
|
+
const allowed = j.allowed === true
|
|
77
|
+
const blocked = m === 'enforce' && !allowed
|
|
78
|
+
return {
|
|
79
|
+
checked: true,
|
|
80
|
+
mode: m,
|
|
81
|
+
allowed,
|
|
82
|
+
blocked,
|
|
83
|
+
code: blocked ? 1 : (allowed && j.licenseState === 'grace' ? 3 : 0),
|
|
84
|
+
reason: blocked ? 'license_denied' : (allowed && j.licenseState === 'grace' ? 'license_grace' : j.reason || 'ok'),
|
|
85
|
+
licenseState: j.licenseState || 'unknown',
|
|
86
|
+
decision: j.decision || 'unknown',
|
|
87
|
+
human: j.human || '',
|
|
88
|
+
error: '',
|
|
89
|
+
evidence,
|
|
90
|
+
verdict: j,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 把闸门结论并进写类 verb 的 JSON(A2)。
|
|
96
|
+
* · `observe` 下 `blocked:false` —— **只留证据、不拦**(这是本批的真实姿态,别对外说成 enforce)
|
|
97
|
+
* · grace ⇒ `code:3`(A1:3 只表示许可宽限放行)+ `reason:"license_grace"`(必须上报)
|
|
98
|
+
* · enforce 且不可用 ⇒ 调用方据此**整体拒绝**(不执行写)
|
|
99
|
+
*/
|
|
100
|
+
export function applyGate(json, gate) {
|
|
101
|
+
const out = { ...json }
|
|
102
|
+
out.license = {
|
|
103
|
+
checked: gate.checked, mode: gate.mode, allowed: gate.allowed, blocked: gate.blocked,
|
|
104
|
+
state: gate.licenseState, decision: gate.decision, reason: gate.reason,
|
|
105
|
+
evidence: gate.evidence || null, human: gate.human || null,
|
|
106
|
+
note: gate.mode === 'observe'
|
|
107
|
+
? '当前是 observe(默认):**只留证据、不拦**;enforce 下同样的判定会拒绝本次写'
|
|
108
|
+
: (gate.mode === 'off' ? '逃生阀 off:没有做许可判定' : 'enforce:不可用即拒绝'),
|
|
109
|
+
}
|
|
110
|
+
if (gate.error) out.license.error = gate.error
|
|
111
|
+
if (gate.blocked) {
|
|
112
|
+
out.ok = false
|
|
113
|
+
out.empty = false
|
|
114
|
+
out.code = 1
|
|
115
|
+
out.reason = 'license_denied'
|
|
116
|
+
out.error = gate.human || '离线许可不可用(enforce)⇒ 拒绝写操作(零副作用)'
|
|
117
|
+
} else if (gate.checked && gate.licenseState === 'grace') {
|
|
118
|
+
out.code = 3
|
|
119
|
+
out.reason = 'license_grace'
|
|
120
|
+
out.licenseReport = '许可在宽限期内:业务放行,但**必须上报**(A1:code 3 只表示许可宽限放行)'
|
|
121
|
+
}
|
|
122
|
+
return out
|
|
123
|
+
}
|