@h-ai/cli 1.0.0-beta.13 → 1.0.0-beta.15

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.
Files changed (40) hide show
  1. package/README.en.md +2 -0
  2. package/README.ja.md +2 -0
  3. package/README.md +2 -0
  4. package/dist/{chunk-OGUA3OIS.js → chunk-H4JCLCNZ.js} +80 -3
  5. package/dist/chunk-H4JCLCNZ.js.map +1 -0
  6. package/dist/cli-config-templates-Y5B3DCCP.js +5 -0
  7. package/dist/{cli-config-templates-TZSORUT6.js.map → cli-config-templates-Y5B3DCCP.js.map} +1 -1
  8. package/dist/index.js +16 -5
  9. package/dist/index.js.map +1 -1
  10. package/dist/metafile-esm.json +1 -1
  11. package/package.json +14 -14
  12. package/templates/apps/api/apps/{{projectName}}-serv/config/_serv.yml.hbs +3 -0
  13. package/templates/apps/api/apps/{{projectName}}-serv/src/app.ts.hbs +2 -0
  14. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +2 -0
  15. package/templates/apps/fullstack/apps/{{projectName}}-web/index.html.hbs +1 -0
  16. package/templates/apps/fullstack/docs/security-configuration.md.hbs +3 -0
  17. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +2 -0
  18. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +3 -0
  19. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +3 -0
  20. package/templates/base/e2e/app.spec.ts.hbs +2 -1
  21. package/templates/base/package.json.hbs +1 -1
  22. package/templates/base/src/app.html.hbs +1 -0
  23. package/templates/shared/browser-check/startup.js +89 -0
  24. package/templates/shared/browser-check/update.css +37 -0
  25. package/templates/shared/browser-check/update.html +19 -0
  26. package/templates/shared/browser-startup.spec.ts +21 -0
  27. package/templates/skills/hai-api-client/SKILL.md +1 -1
  28. package/templates/skills/hai-api-client/reference.md +1 -1
  29. package/templates/skills/hai-app-create/development.md +2 -0
  30. package/templates/skills/hai-crypto/SKILL.md +2 -2
  31. package/templates/skills/hai-crypto/reference.md +2 -2
  32. package/templates/skills/hai-kit/SKILL.md +1 -1
  33. package/templates/skills/hai-kit/reference.md +3 -1
  34. package/templates/skills/hai-reach/SKILL.md +1 -0
  35. package/templates/skills/hai-reach/reference.md +13 -1
  36. package/templates/skills/hai-serv/SKILL.md +1 -1
  37. package/templates/skills/hai-serv/reference.md +3 -1
  38. package/templates/skills/hai-ui/SKILL.md +1 -1
  39. package/dist/chunk-OGUA3OIS.js.map +0 -1
  40. package/dist/cli-config-templates-TZSORUT6.js +0 -5
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/cli",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.13",
4
+ "version": "1.0.0-beta.15",
5
5
  "description": "Hai Framework CLI for project scaffolding and code generation.",
6
6
  "license": "Apache-2.0",
7
7
  "keywords": [
@@ -30,7 +30,7 @@
30
30
  "node": "^22.23.4 || ^24.22.1 || ^26.12.1"
31
31
  },
32
32
  "dependencies": {
33
- "@h-ai/core": "1.0.0-beta.13",
33
+ "@h-ai/core": "1.0.0-beta.15",
34
34
  "cac": "^7.0.1",
35
35
  "chalk": "^6.0.1",
36
36
  "fast-wrap-ansi": "^0.2.2",
@@ -41,18 +41,18 @@
41
41
  "yaml": "^2.9.1"
42
42
  },
43
43
  "devDependencies": {
44
- "@h-ai/api-client": "1.0.0-beta.13",
45
- "@h-ai/cache": "1.0.0-beta.13",
46
- "@h-ai/crypto": "1.0.0-beta.13",
47
- "@h-ai/deploy": "1.0.0-beta.13",
48
- "@h-ai/feedback": "1.0.0-beta.13",
49
- "@h-ai/iam": "1.0.0-beta.13",
50
- "@h-ai/kit": "1.0.0-beta.13",
51
- "@h-ai/payment": "1.0.0-beta.13",
52
- "@h-ai/reach": "1.0.0-beta.13",
53
- "@h-ai/reldb": "1.0.0-beta.13",
54
- "@h-ai/storage": "1.0.0-beta.13",
55
- "@h-ai/vecdb": "1.0.0-beta.13",
44
+ "@h-ai/api-client": "1.0.0-beta.15",
45
+ "@h-ai/cache": "1.0.0-beta.15",
46
+ "@h-ai/crypto": "1.0.0-beta.15",
47
+ "@h-ai/deploy": "1.0.0-beta.15",
48
+ "@h-ai/feedback": "1.0.0-beta.15",
49
+ "@h-ai/iam": "1.0.0-beta.15",
50
+ "@h-ai/kit": "1.0.0-beta.15",
51
+ "@h-ai/payment": "1.0.0-beta.15",
52
+ "@h-ai/reach": "1.0.0-beta.15",
53
+ "@h-ai/reldb": "1.0.0-beta.15",
54
+ "@h-ai/storage": "1.0.0-beta.15",
55
+ "@h-ai/vecdb": "1.0.0-beta.15",
56
56
  "@types/fs-extra": "^11.0.4",
57
57
  "@types/node": "^26.6.5",
58
58
  "@types/prompts": "^2.4.9",
@@ -57,6 +57,9 @@ cors:
57
57
  {{else}}
58
58
  maxPlaintextBytes: ${HAI_SERV_TRANSPORT_MAXPLAINTEXTBYTES:1048576}
59
59
  {{/if}}
60
+ # 传输时钟窗口,单位秒;允许领先 0~300 秒,请求有效期 1~600 秒。
61
+ maxFutureSkewSeconds: ${HAI_SERV_TRANSPORT_MAXFUTURESKEWSECONDS:30}
62
+ maxRequestAgeSeconds: ${HAI_SERV_TRANSPORT_MAXREQUESTAGESECONDS:300}
60
63
  # HAI_SERV_TRANSPORT_ROUTEPLAINTEXTBYTES 可填 JSON 路径映射。
61
64
  routePlaintextBytes: {}
62
65
  # 单条路径需要更大请求体时填写字节数,* 只匹配一段路径。
@@ -48,6 +48,8 @@ export function {{api.serviceAppFactoryName}}(_options: CreateApiServiceAppOptio
48
48
  {{/if}}
49
49
  keyExchangePath: servConfig.transport.keyExchangePath,
50
50
  maxPlaintextBytes: servConfig.transport.maxPlaintextBytes,
51
+ maxFutureSkewSeconds: servConfig.transport.maxFutureSkewSeconds,
52
+ maxRequestAgeSeconds: servConfig.transport.maxRequestAgeSeconds,
51
53
  {{#if features.feedback}} routePlaintextBytes: {
52
54
  [`${servConfig.http.apiPrefix}/feedback/reports`]: feedbackBudget.maxTransportBytes,
53
55
  [`${servConfig.http.apiPrefix}/feedback/reports/*/evidence`]: feedbackBudget.maxTransportBytes,
@@ -24,6 +24,8 @@
24
24
 
25
25
  ### 编译、打包与部署
26
26
 
27
+ 入口 HTML 在主应用前执行 `public/_hai/browser-check/startup.js`。Chrome/Edge 低于 111、Firefox 低于 114、Safari 或 iOS/iPadOS 低于 16.4 时,独立升级页会显示当前版本、最低要求和厂商更新入口。发布时连同该目录的 HTML 和样式一起部署。
28
+
27
29
  ```bash
28
30
  # 开发预览,默认端口 {{fullstack.ports.web}}
29
31
  pnpm --filter {{projectName}}-web dev
@@ -5,6 +5,7 @@
5
5
  <head>
6
6
  <meta charset="UTF-8" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1" />
8
+ <script id="hai-browser-startup" src="./_hai/browser-check/startup.js"></script>
8
9
  <title>{{html fullstack.appName}}</title>
9
10
  <link
10
11
  rel="icon"
@@ -7,6 +7,7 @@
7
7
  | 文件 | 用途 |
8
8
  | ------------------------------------------------------------ | -------------------------------------------------- |
9
9
  | `packages/{{projectName}}-serv/config/_iam.yml` | 账号、密码、验证码、锁定、敏感操作、登录限制、会话 |
10
+ | `packages/{{projectName}}-serv/config/_serv.yml` | API 传输加密及请求时间窗口 |
10
11
  | `packages/{{projectName}}-serv/config/_audit.yml` | 审计采集、脱敏、保留、备份、告警 |
11
12
  | `packages/{{projectName}}-serv/config/_reach.yml` | 邮件、短信或通知 API 及通知模板 |
12
13
  | `packages/{{projectName}}-serv/config/_scheduler.yml` | 已有 Scheduler 的持久化及调度参数 |
@@ -30,6 +31,8 @@ HAI_AUDIT_BACKUP_ENABLED=true
30
31
 
31
32
  这里列的是 fullstack 模板默认值,不是所有项目或底层 Schema 的普适限制。
32
33
 
34
+ 加密请求默认有效 300 秒,允许客户端时间领先服务器最多 30 秒。可在 `_serv.yml` 的 `transport` 下设置 `maxRequestAgeSeconds`(1~600)和 `maxFutureSkewSeconds`(0~300),单位为整数秒。超限时,前端显示校验时的服务器 UTC 时间,提示用户开启设备自动校时或手工修改时间,然后刷新。升级旧版本后,从最后一个旧节点停止处理请求起等待 12 分钟,再把请求有效期调到 300 秒以上。
35
+
33
36
  | 条目 | fullstack 默认及启用入口 | 使用影响 |
34
37
  | ---------------------------- | --------------------------------------------------------------- | -------------------------------------------------------- |
35
38
  | B03 长期/临时账号 | 用户管理中的账号类型、到期时间 | 临时账号必须有未来到期时间;到期后旧会话也不能继续使用 |
@@ -78,6 +78,8 @@ for (const scenario of [{ offset: -600_000, width: 1440 }, { offset: 600_000, wi
78
78
  await page.goto('./')
79
79
  const advice = page.getByRole('alert').filter({ hasText: /同步|synchron|時刻/i })
80
80
  await expect(advice).toBeVisible()
81
+ await expect(advice).toContainText(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC/)
82
+ await expect(advice).toContainText(/请开启设备的自动校时或手工修改时间|set your device time manually|手動で時刻を設定/i)
81
83
  await expect(advice).toContainText(/刷新|refresh|再読み込み/i)
82
84
  await capture(page, `clock-skew-${scenario.width}.png`, testInfo.outputPath(`clock-skew-${scenario.width}.png`))
83
85
  // 校时后由用户明确刷新;不能自动重放之前失败的业务请求。
@@ -23,6 +23,9 @@ providers:
23
23
  # accessKeySecret: ${HAI_REACH_PROVIDERS_1_ACCESSKEYSECRET}
24
24
  # signName: MyApp
25
25
  # endpoint: dysmsapi.aliyuncs.com
26
+ # gateway: # 可选;配置后向完整 URL 发送 POST
27
+ # url: https://sms-gateway.example.com/dx
28
+ # body: {}
26
29
  # 自有发送服务:按需将 template.provider 改为 custom。
27
30
  # - name: custom
28
31
  # type: api
@@ -62,6 +62,9 @@ transport:
62
62
  {{else}}
63
63
  maxPlaintextBytes: ${HAI_SERV_TRANSPORT_MAXPLAINTEXTBYTES:1048576}
64
64
  {{/if}}
65
+ # 传输时钟窗口,单位秒;允许领先 0~300 秒,请求有效期 1~600 秒。
66
+ maxFutureSkewSeconds: ${HAI_SERV_TRANSPORT_MAXFUTURESKEWSECONDS:30}
67
+ maxRequestAgeSeconds: ${HAI_SERV_TRANSPORT_MAXREQUESTAGESECONDS:300}
65
68
  # HAI_SERV_TRANSPORT_ROUTEPLAINTEXTBYTES 可填 JSON,例如 {"/api/v1/uploads/*":8388608}。
66
69
  routePlaintextBytes: {}
67
70
  keyExchangePath: /_hai/key-exchange
@@ -17,7 +17,8 @@ async function whileClientBootPaused(page: Page, inspect: () => Promise<void>) {
17
17
  resumeClient = resolve
18
18
  })
19
19
  await page.route('**/*.js', async (route) => {
20
- await clientBoot
20
+ if (!new URL(route.request().url()).pathname.endsWith('/_hai/browser-check/startup.js'))
21
+ await clientBoot
21
22
  await route.continue()
22
23
  })
23
24
  try {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "engines": { "node": "^22.23.4 || ^24.22.1 || ^26.12.1" },
3
3
  "name": "{{projectName}}",
4
- "version": "1.0.0-beta.13",
4
+ "version": "1.0.0-beta.15",
5
5
  "private": true,
6
6
  "packageManager": "pnpm@12.11.2",
7
7
  "description": "{{#if_eq appType 'admin'}}Enterprise management system built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'website'}}Enterprise website built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'h5'}}Mobile H5 app built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'api'}}REST API service built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'mobile-app'}}Mobile app built with hai Agent Framework{{/if_eq}}",
@@ -13,6 +13,7 @@
13
13
  <head>
14
14
  <meta charset="utf-8" />
15
15
  <meta name="viewport" content="width=device-width, initial-scale=1" />
16
+ <script id="hai-browser-startup" src="%sveltekit.assets%/_hai/browser-check/startup.js"></script>
16
17
  <link rel="icon" href="%sveltekit.assets%/favicon.png" />
17
18
  <script>
18
19
  ;(function () {
@@ -0,0 +1,89 @@
1
+ /* eslint-disable no-var, object-shorthand, prefer-template, prefer-arrow-callback -- 此脚本须在低版本浏览器中直接执行,使用 ES5 语法。 */
2
+ ;(function () {
3
+ var agent = navigator.userAgent
4
+ var root = document.documentElement
5
+ var location = window.location
6
+ var edgeHelp = 'https://support.microsoft.com/edge/microsoft-edge-update-settings'
7
+ var chromeHelp = 'https://support.google.com/chrome/answer/95414'
8
+ var firefoxHelp = 'https://support.mozilla.org/kb/update-firefox-latest-release'
9
+ var safariHelp = 'https://support.apple.com/102665'
10
+ var iosHelp = 'https://support.apple.com/118575'
11
+ var browser = getOutdatedBrowser()
12
+
13
+ // 匹配 Vite 8 的默认构建基线。Edge 的 UA 同时包含 Chrome,iOS 先按系统版本判断。
14
+ function getOutdatedBrowser() {
15
+ var version = /\b(?:CPU (?:iPhone )?OS|iPhone OS) (\d+_\d+(?:_\d+)?)/.exec(agent)
16
+ if (version && /\b(?:iPhone|iPad|iPod)\b/.test(agent)) {
17
+ return outdated('iOS / iPadOS', version, 16, 4, iosHelp)
18
+ }
19
+ version = /\b(?:EdgA?|Edge)\/(\d+(?:\.\d+)*)/.exec(agent)
20
+ if (version) {
21
+ return outdated('Microsoft Edge', version, 111, 0, edgeHelp)
22
+ }
23
+ version = /\bFirefox\/(\d+(?:\.\d+)*)/.exec(agent)
24
+ if (version) {
25
+ return outdated('Firefox', version, 114, 0, firefoxHelp)
26
+ }
27
+ version = /\bChrome\/(\d+(?:\.\d+)*)/.exec(agent)
28
+ if (version) {
29
+ if (/\b(?:wv|OPR\/|SamsungBrowser\/|Vivaldi\/|YaBrowser\/|HeadlessChrome\/)/.test(agent))
30
+ return null
31
+ return outdated('Google Chrome', version, 111, 0, chromeHelp)
32
+ }
33
+ version = /\bVersion\/(\d+(?:\.\d+)*)/.exec(agent)
34
+ if (version && /\bSafari\//.test(agent)) {
35
+ return outdated('Safari', version, 16, 4, safariHelp)
36
+ }
37
+ return null
38
+ }
39
+
40
+ function outdated(name, match, major, minor, help) {
41
+ var version = match[1].replace(/_/g, '.')
42
+ var parts = version.split('.')
43
+ var currentMajor = Number(parts[0])
44
+ var currentMinor = Number(parts[1] || 0)
45
+ if (currentMajor > major || (currentMajor === major && currentMinor >= minor))
46
+ return null
47
+ return { name: name, version: version, minimum: minor ? major + '.' + minor : String(major), help: help }
48
+ }
49
+
50
+ function getLanguage(value) {
51
+ var language = (value || '').toLowerCase().split(/[-_]/)[0]
52
+ return language === 'zh' || language === 'ja' || language === 'en' ? language : ''
53
+ }
54
+
55
+ function openUpdatePage() {
56
+ // 以静态脚本地址为基准,保留发布目录。仅传语言,不把原页面地址写到升级页。
57
+ var script = document.getElementById('hai-browser-startup')
58
+ var language = getLanguage(root.lang) || getLanguage(navigator.language) || 'en'
59
+ location.replace(script.src.replace(/\/startup\.js$/, '/update.html') + '#' + language)
60
+ }
61
+
62
+ function showUpdateInstructions() {
63
+ var locale = getLanguage(location.hash.slice(1)) || getLanguage(navigator.language) || 'en'
64
+ var messages = {
65
+ zh: ['浏览器版本过低', '当前使用 {name} {version}。此站点至少需要 {name} {minimum}。', '请更新到最新版本,然后重新打开此站点。', '查看官方更新说明'],
66
+ en: ['Update your browser', 'You are using {name} {version}. This site requires {name} {minimum} or later.', 'Install the latest update, then reopen this site.', 'Official update instructions'],
67
+ ja: ['ブラウザーを更新してください', '現在のバージョンは {name} {version} です。このサイトには {name} {minimum} 以降が必要です。', '最新版に更新してから、このサイトを開き直してください。', '公式の更新手順を見る'],
68
+ }
69
+ var message = messages[locale]
70
+ var help = document.getElementById('browser-update-help')
71
+ root.lang = locale === 'zh' ? 'zh-CN' : locale
72
+ document.title = message[0]
73
+ document.getElementById('browser-update-title').textContent = message[0]
74
+ document.getElementById('browser-update-version').textContent = message[1].replace(/\{(name|version|minimum)\}/g, function (_match, key) {
75
+ return browser[key]
76
+ })
77
+ document.getElementById('browser-update-instruction').textContent = message[2]
78
+ help.textContent = message[3]
79
+ help.href = browser.help
80
+ help.hidden = false
81
+ }
82
+
83
+ if (!browser)
84
+ return
85
+ if (root.hasAttribute('data-hai-browser-update'))
86
+ showUpdateInstructions()
87
+ else
88
+ openUpdatePage()
89
+ })()
@@ -0,0 +1,37 @@
1
+ body {
2
+ margin: 0;
3
+ padding: 12vh 20px 32px;
4
+ background: #f4f6fa;
5
+ color: #1b263b;
6
+ font-family: system-ui, sans-serif;
7
+ font-size: 1rem;
8
+ line-height: 1.6;
9
+ }
10
+
11
+ main {
12
+ max-width: 33rem;
13
+ margin: 0 auto;
14
+ padding: 1.5rem;
15
+ border: 1px solid #dce2ec;
16
+ border-radius: 0.75rem;
17
+ background: #fff;
18
+ }
19
+
20
+ h1 {
21
+ margin: 0 0 1rem;
22
+ font-size: 1.5rem;
23
+ line-height: 1.25;
24
+ }
25
+
26
+ p {
27
+ margin: 0 0 0.75rem;
28
+ }
29
+
30
+ a {
31
+ color: #1642a3;
32
+ }
33
+
34
+ a:focus {
35
+ outline: 2px solid currentColor;
36
+ outline-offset: 4px;
37
+ }
@@ -0,0 +1,19 @@
1
+ <!doctype html>
2
+ <html lang="en" data-hai-browser-update>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="robots" content="noindex" />
7
+ <title>Browser requirements</title>
8
+ <link rel="stylesheet" href="./update.css" />
9
+ </head>
10
+ <body>
11
+ <main>
12
+ <h1 id="browser-update-title">Browser requirements</h1>
13
+ <p id="browser-update-version">This site requires a current browser.</p>
14
+ <p id="browser-update-instruction">Install the latest update, then reopen this site.</p>
15
+ <a id="browser-update-help" rel="noreferrer" hidden></a>
16
+ </main>
17
+ <script src="./startup.js"></script>
18
+ </body>
19
+ </html>
@@ -0,0 +1,21 @@
1
+ import { expect, test } from '@playwright/test'
2
+
3
+ test('old browser sees upgrade instructions without the application bundles', async ({ page }) => {
4
+ // 只改变浏览器脚本看到的版本,确保检测在客户端执行。
5
+ await page.addInitScript(() => {
6
+ Object.defineProperty(navigator, 'userAgent', { get: () => 'Mozilla/5.0 Gecko/20100101 Firefox/113.0' })
7
+ })
8
+ await page.route('**/*', async (route) => {
9
+ const { pathname } = new URL(route.request().url())
10
+ if (/\.(?:js|css)$/.test(pathname) && !pathname.includes('/_hai/browser-check/'))
11
+ await route.abort()
12
+ else
13
+ await route.continue()
14
+ })
15
+ await page.goto('./', { waitUntil: 'commit' })
16
+ await expect(page).toHaveURL(/\/_hai\/browser-check\/update\.html#(?:zh|en|ja)$/)
17
+ await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
18
+ await expect(page.getByRole('main')).toContainText('Firefox 113.0')
19
+ await expect(page.getByRole('main')).toContainText('Firefox 114')
20
+ await expect(page.getByRole('link')).toHaveAttribute('href', 'https://support.mozilla.org/kb/update-firefox-latest-release')
21
+ })
@@ -29,7 +29,7 @@ description: "接入或排查 @h-ai/api-client typed API、登录 Token、自动
29
29
 
30
30
  ## 按需参考
31
31
 
32
- serv/kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的报文返回加密的 HTTP 400 时间错误;客户端抛 `TransportTimeError`(`hai:crypto:071`),提示同步设备和服务器时间后刷新重试。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 保留该原因。不会自动重放请求或放宽防重放窗口;篡改或未认证报文仍返回通用认证失败。
32
+ Serv/Kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的请求返回加密的 HTTP 400 错误。客户端抛出 `TransportTimeError`(`hai:crypto:071`),显示校验时的服务器 UTC 时间,提示开启设备的自动校时或手工修改时间后刷新。`serverTime` 为 Unix 毫秒,旧服务端未提供该值时仍显示校时提示。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 和 `cause.serverTime` 保留诊断信息。
33
33
 
34
34
  按本次操作读取对应小节;以已安装版本的公开类型核验签名,示例中的业务变量由应用提供。
35
35
 
@@ -3,7 +3,7 @@
3
3
  > `@h-ai/api-client` 是跨端 typed API client。公共 HTTP API 由 `@h-ai/api-contract` 定义,服务端由 `@h-ai/serv` 挂载,客户端直接调用 `apiClient.<domain>.<group>.<operation>()`。
4
4
 
5
5
 
6
- 认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求有效期 5 分钟,未来时钟偏差最多 30 秒;共享 registry 原子消费 (clientId, requestId),nonce 至少保留到 issuedAt + 6 分钟以覆盖节点偏差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
6
+ 认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求默认有效期为 5 分钟,客户端时间最多可领先服务器 30 秒。服务端可用 `maxRequestAgeSeconds`(1~600)和 `maxFutureSkewSeconds`(0~300)调整,单位为整数秒。共享 registry 原子消费 (clientId, requestId),nonce 保留到 issuedAt + 11 分钟,覆盖最长可配窗口及节点时钟差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
7
7
 
8
8
  空 POST 也发送空明文的认证封装;GET/HEAD 将封装放在 X-Hai-Transport 头,204/304/HEAD 等无正文响应也使用同名头承载认证封装。其它请求/响应在 JSON 正文承载封装并带 X-Encrypted: true。客户端必须关联原请求上下文和 HTTP status,缺标记、旧格式或认证失败一律 reject;明文接口由 Serv/Kit 路径规则 `encryption: disabled` 或宿主 `excludePaths` 显式允许;调用方同步选择普通请求,API Client/Kit 包装器使用自己的 `excludePaths`。CORS 须允许并暴露 X-Hai-Transport。握手公钥的真实性依赖 TLS,不替代 IAM 业务授权。
9
9
 
@@ -26,6 +26,8 @@ README 编辑先读章节,把功能说明与示例合并到对应章节;新
26
26
 
27
27
  website默认SSR;静态发布须先移除服务端业务依赖、初始化及API/actions。静态构建与实际路径验证按[hai-build](../hai-build/SKILL.md)。
28
28
 
29
+ admin、website、h5 和 Fullstack Web 在入口 HTML 加载 `_hai/browser-check/startup.js` 后才进入应用。检测到 Chrome/Edge 低于 111、Firefox 低于 114、Safari 或 iOS/iPadOS 低于 16.4 时,浏览器跳转到同目录的静态升级页。保留经典脚本标签和静态资源,验证根目录和发布子目录都能在应用包加载失败时显示升级提示。
30
+
29
31
  网站挂载到`/yyy/`等目录时,按[网站挂载目录](../hai-build/paths.md)同步资源、链接、认证端点和服务端守卫。SvelteKit在编译时固定base,Vite默认相对资源加hash导航;API基址单独配置。SDK子目录制品使用格式5,健康与增量检查随制品处理,不手工修改旧格式清单来代替重新构建。
30
32
 
31
33
  页面共用根布局的 header/footer。独立移动应用的页面放在 `src/lib/pages/**/*.svelte`,Fullstack 放在 `packages/<name>-shared/src/lib/pages/**/*.svelte`,由 `HashRouter` 和 `import.meta.glob` 自动发现;新增页面无需修改路由表,菜单按业务配置入口。SvelteKit 保留 `src/routes/**/+page.svelte` 自动文件路由、服务端鉴权和刷新定位。
@@ -5,7 +5,7 @@ description: "实现 SM2/SM3/SM4 加解密、验签、密码哈希,或通过 s
5
5
 
6
6
  # hai-crypto
7
7
 
8
- transport 仅支持 v2:SM2 发送方签名、独立密钥的 Encrypt-then-HMAC、HTTP 上下文/响应状态绑定及共享原子防重放。GET/HEAD、空 POST 与空响应也认证;无降级开关。TLS 信任边界、5 分钟窗口、nonce 持久化与实现示例见 [参考](reference.md)。
8
+ transport 仅支持 v2:SM2 发送方签名、独立密钥的 Encrypt-then-HMAC、HTTP 上下文/响应状态绑定及共享原子防重放。GET/HEAD、空 POST 与空响应也认证;无降级开关。TLS 信任边界、可配时间窗口、nonce 持久化与实现示例见 [参考](reference.md)。
9
9
 
10
10
  ## 能力契约
11
11
 
@@ -32,7 +32,7 @@ transport 仅支持 v2:SM2 发送方签名、独立密钥的 Encrypt-then-HMAC
32
32
 
33
33
  业务容量统一配置 transport.maxPlaintextBytes:UTF-8 JSON 明文字节,默认 1 MiB,有效整数范围 1 KiB~64 MiB,请求和响应共用。客户端从握手获取服务端预算,可选本地上限取较小值;resolveTransportBodyLimits 自动推导密文预算为两倍明文加 32 KiB。握手 JSON 16 KiB、公钥 4 KiB、认证头 16 KiB、路径 8 KiB、认证及防重放保持独立。原流超限/取消立即停止、取消并释放 reader;请求 413、响应返回加密的 500 容量错误,客户端识别 TransportBodyTooLargeError / hai:crypto:070。 自定义适配器使用 manager.bodyLimits 和 readTransportBody 消费原流,不 clone/tee;握手返回 maxPlaintextBytes。
34
34
 
35
- serv/kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的报文返回加密的 HTTP 400 时间错误;客户端抛 `TransportTimeError`(`hai:crypto:071`),提示同步设备和服务器时间后刷新重试。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 保留该原因。不会自动重放请求或放宽防重放窗口;篡改或未认证报文仍返回通用认证失败。
35
+ Serv/Kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的请求返回加密的 HTTP 400 错误。客户端抛出 `TransportTimeError`(`hai:crypto:071`),显示校验时的服务器 UTC 时间,提示开启设备的自动校时或手工修改时间后刷新。`serverTime` 为 Unix 毫秒,旧服务端未提供该值时仍显示校时提示。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 和 `cause.serverTime` 保留诊断信息。
36
36
 
37
37
  ## 按需参考
38
38
 
@@ -3,7 +3,7 @@
3
3
  > `@h-ai/crypto` 提供非对称加密、哈希、对称加密与密码哈希能力,支持 Node.js 与浏览器双端。
4
4
 
5
5
 
6
- 认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求有效期 5 分钟,未来时钟偏差最多 30 秒;共享 registry 原子消费 (clientId, requestId),nonce 至少保留到 issuedAt + 6 分钟以覆盖节点偏差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
6
+ 认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求默认有效期为 5 分钟,客户端时间最多可领先服务器 30 秒。服务端可用 `maxRequestAgeSeconds`(1~600)和 `maxFutureSkewSeconds`(0~300)调整,单位为整数秒。共享 registry 原子消费 (clientId, requestId),nonce 保留到 issuedAt + 11 分钟,覆盖最长可配窗口及节点时钟差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
7
7
 
8
8
  空 POST 也发送空明文的认证封装;GET/HEAD 将封装放在 X-Hai-Transport 头,204/304/HEAD 等无正文响应也使用同名头承载认证封装。其它请求/响应在 JSON 正文承载封装并带 X-Encrypted: true。客户端必须关联原请求上下文和 HTTP status,缺标记、旧格式或认证失败一律 reject;明文接口由 Serv/Kit 路径规则 `encryption: disabled` 或宿主 `excludePaths` 显式允许;调用方同步选择普通请求,API Client/Kit 包装器使用自己的 `excludePaths`。CORS 须允许并暴露 X-Hai-Transport。握手公钥的真实性依赖 TLS,不替代 IAM 业务授权。
9
9
 
@@ -233,7 +233,7 @@ const manager = crypto.transport.createServer({ serverKeyRing: ring.data, keySto
233
233
 
234
234
  业务容量统一配置 transport.maxPlaintextBytes:UTF-8 JSON 明文字节,默认 1 MiB,有效整数范围 1 KiB~64 MiB,请求和响应共用。客户端从握手获取服务端预算,可选本地上限取较小值;resolveTransportBodyLimits(limit, { requiredPlaintextBytes }) 合并模块需求:保留显式上限,不足时报配置冲突;省略时按需要推导,无模块需要仍为 1 MiB。Serv/Kit 的 auto 规范化为 undefined。密文预算为两倍明文加 32 KiB。握手 JSON 16 KiB、公钥 4 KiB、认证头 16 KiB、路径 8 KiB、认证及防重放保持独立。原流超限/取消立即停止、取消并释放 reader;请求 413、响应返回加密的 500 容量错误,客户端识别 TransportBodyTooLargeError / hai:crypto:070。 自定义适配器使用 manager.bodyLimits 和 readTransportBody 消费原流,不 clone/tee;握手返回 maxPlaintextBytes。
235
235
 
236
- serv/kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的报文返回加密的 HTTP 400 时间错误;客户端抛 `TransportTimeError`(`hai:crypto:071`),提示同步设备和服务器时间后刷新重试。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 保留该原因。不会自动重放请求或放宽防重放窗口;篡改或未认证报文仍返回通用认证失败。
236
+ Serv/Kit 对通过请求绑定、签名和 MAC 校验但超出时间窗口的请求返回加密的 HTTP 400 错误。客户端抛出 `TransportTimeError`(`hai:crypto:071`),显示校验时的服务器 UTC 时间,提示开启设备的自动校时或手工修改时间后刷新。`serverTime` 为 Unix 毫秒,旧服务端未提供该值时仍显示校时提示。`api-client` 映射为 `VALIDATION_FAILED`,在 `cause.transportCode` 和 `cause.serverTime` 保留诊断信息。
237
237
 
238
238
  自定义 HTTP 适配器检查失败原因 `error instanceof TransportTimeError && error.context` 后,使用 `createTransportErrorResponse(error, { manager, context: error.context })` 加密拒绝响应;不能根据未认证报文的时间戳给出可信时间诊断。
239
239
 
@@ -7,7 +7,7 @@ description: "在 SvelteKit 中接入 hooks、认证守卫、输入校验、响
7
7
 
8
8
  限流采用首次成功提交为起点的固定窗口,后续请求不延长 TTL。必须显式传 store;应用先检查共享 Redis 初始化结果,所有节点使用相同 namespace/windowMs/maxRequests。缓存读取/条件提交失败或冲突耗尽返回503,超限429;不会回退 get+set 或本地计数。createMemoryRateLimitStore 仅开发或显式 HAI_E2E/HAI_E2E_MOCK 测试使用,无常驻定时器,丢弃引用即可回收;每次请求最多惰性扫描 64 项,最多保存 10000 个窗口,容量满时对新 key 返回503且不驱逐有效计数,外部 store 生命周期由创建者管理;生产不得设置测试开关。响应重置时间为节点时钟估算,实际窗口由 Redis TTL 决定,须同步时钟。默认键只读取可信 adapter 的 getClientAddress、规范化等价IPv6和IPv4映射地址,不采信原始转发头;代理信任通过部署adapter配置。
9
9
 
10
- transport 仅支持 v2:SM2 发送方签名、独立密钥的 Encrypt-then-HMAC、HTTP 上下文/响应状态绑定及共享原子防重放。GET/HEAD、空 POST 与空响应也认证;无降级开关。TLS 信任边界、5 分钟窗口、nonce 持久化与实现示例见 [参考](reference.md)。
10
+ transport 仅支持 v2:SM2 发送方签名、独立密钥的 Encrypt-then-HMAC、HTTP 上下文/响应状态绑定及共享原子防重放。GET/HEAD、空 POST 与空响应也认证;无降级开关。TLS 信任边界、可配时间窗口、nonce 持久化与实现示例见 [参考](reference.md)。
11
11
 
12
12
  ## 能力契约
13
13
 
@@ -2,7 +2,9 @@
2
2
 
3
3
  > `@h-ai/kit` 只负责 SvelteKit 集成:`handle` hook、guard、response、validate、client、auth、crud、A2A。公共跨端 HTTP API 契约统一使用 `@h-ai/api-contract` + `@h-ai/serv`,不要在 kit 中定义业务 API contract。
4
4
 
5
- 认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求有效期 5 分钟,未来时钟偏差最多 30 秒;共享 registry 原子消费 (clientId, requestId),nonce 至少保留到 issuedAt + 6 分钟以覆盖节点偏差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
5
+ 认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求默认有效期为 5 分钟,客户端时间最多可领先服务器 30 秒。服务端可用 `maxRequestAgeSeconds`(1~600)和 `maxFutureSkewSeconds`(0~300)调整,单位为整数秒。共享 registry 原子消费 (clientId, requestId),nonce 保留到 issuedAt + 11 分钟,覆盖最长可配窗口及节点时钟差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
6
+
7
+ 在 `_kit.yml` 的 `transport` 块配置时间窗口,将字段传给服务端 transport 装配。完成旧节点升级后等待 12 分钟,再把有效期调到 300 秒以上。校时提示与 `TransportTimeError.serverTime` 见 [Crypto 参考](../hai-crypto/reference.md)。`kit.client.createAuthActions()` 将已认证校时错误转为 `HaiResult`,供 AuthPanel 展示。
6
8
 
7
9
  空 POST 也发送空明文的认证封装;GET/HEAD 将封装放在 X-Hai-Transport 头,204/304/HEAD 等无正文响应也使用同名头承载认证封装。其它请求/响应在 JSON 正文承载封装并带 X-Encrypted: true。客户端必须关联原请求上下文和 HTTP status,缺标记、旧格式或认证失败一律 reject;明文接口由 Serv/Kit 路径规则 `encryption: disabled` 或宿主 `excludePaths` 显式允许;调用方同步选择普通请求,API Client/Kit 包装器使用自己的 `excludePaths`。CORS 须允许并暴露 X-Hai-Transport。握手公钥的真实性依赖 TLS,不替代 IAM 业务授权。
8
10
 
@@ -21,6 +21,7 @@ API 空 2xx 表示受理成功;非空 JSON 回执上限 64 KiB。redirect 默
21
21
 
22
22
  - 先配置并注册 Provider,再保存绑定 Provider 的模板;模板发送只传 template/to/vars 即可,显式 provider 可覆盖;直接发送须提供 provider。启用 DND delay 必须先初始化 reldb 并确保队列表可用;成功时 `deferred: true` 与 `queueId` 表示可靠入队,尚未投递;缺库、断连或插入失败须按 HaiResult 失败处理。即时投递与排队状态须区分。
23
23
  - 验证模板替换和错误分支时使用隔离测试收件人;真实通知须在用户授权范围内。
24
+ - 阿里云短信的 `gateway` 通过 POST 转发已签名查询,可配置请求头和正文;服务端 `reach.init` 可按 Provider 名注入 `gatewayHooks`。受信内网 HTTP 须显式启用,转发日志应脱敏查询串。
24
25
  - 缓存锁只减少扫描,按实际执行逐条领取并在途续租。API 只有接收端真正具备原子持久去重时才能声明 idempotent=true,全部请求固定用 queueId 作为 Idempotency-Key;旧节点迟到请求也必须去重。SMTP/短信/默认 API 的在途失败或过期转 unknown,禁止自动重发;可信服务端用 queue.get/resolveUnknown 在人工隔离旧 worker、核对外部记录后裁定 sent/discarded。
25
26
  - queue 重试策略随消息持久保存,默认最多领取 5 次,指数退避 60 秒至 1 小时;永久失败/超限进 dead,满批失败不会原地自旋。可信服务端用 queue.list 查询 dead/unknown;修复根因后以最新 updatedAt 调用 requeueDead,只能恢复 dead,保留同一 queueId 和接收端幂等要求。unknown 不能重排。
26
27
 
@@ -47,6 +47,10 @@ providers:
47
47
  accessKeyId: ${HAI_REACH_PROVIDERS_0_ACCESSKEYID:}
48
48
  accessKeySecret: ${HAI_REACH_PROVIDERS_0_ACCESSKEYSECRET:}
49
49
  signName: ${HAI_REACH_PROVIDERS_0_SIGNNAME:}
50
+ # 按需启用隔离网关
51
+ # gateway:
52
+ # url: https://sms-gateway.example.com/dx
53
+ # body: {}
50
54
  - name: webhook
51
55
  type: api
52
56
  url: ${HAI_REACH_WEBHOOK_URL:}
@@ -165,13 +169,21 @@ await reach.send({
165
169
  })
166
170
  ```
167
171
 
172
+ ### 隔离网关与动态字段
173
+
174
+ 在 `aliyun-sms` 下配置 `gateway.url`,使用包含路径的完整 HTTPS 地址。受信内网确需明文时设置 `gateway.allowInsecureHttp: true`。请求使用 POST,阿里云签名仍基于 RPC 路径 `/` 和查询参数,网关路径、头和正文仅供网关使用。`gateway.headers` 取字符串键值,`gateway.body` 可设 JSON 对象、数组或原始文本,`body: {}` 表示空 JSON 对象。
175
+
176
+ 从服务端 `reach.init(config, { gatewayHooks: { sms: hook } })` 注入同步或异步函数。`sms` 对应已配置网关的 Provider 名称,`hook` 接收 `{ method, url, body, signal }`,其中 `body` 已序列化,`url` 含签名和敏感查询。返回 `{ headers, body }` 时,头按名称忽略大小写覆盖,正文整段替换,省略字段时沿用静态配置。`Host` 和 `Content-Length` 等由 HTTP 客户端管理。函数和请求共用 10 秒期限,拒绝重定向。`reach.config` 仅展示网关的地址和 HTTP 开关。
177
+
178
+ 验证网关转发时确认上游路径、方法和查询参数,HTTP 2xx 且阿里云 `Code: OK` 表示已受理,送达状态通过 `BizId` 查询。明文连接的访问日志应脱敏查询串,配置及测试均使用占位数据或受控的私有注入。
179
+
168
180
  ## 核心 API
169
181
 
170
182
  ### reach 对象
171
183
 
172
184
  | 方法 / 属性 | 签名 | 说明 |
173
185
  | --------------------- | -------------------------------------------------------- | ---------------------------------- |
174
- | `reach.init` | `(config: ReachConfigInput) => Promise<HaiResult<void>>` | 初始化(注册多个 Provider) |
186
+ | `reach.init` | `(config: ReachConfigInput, options?: ReachInitOptions) => Promise<HaiResult<void>>` | 初始化 Provider,可注入网关函数 |
175
187
  | `reach.send` | `(message: ReachMessage) => Promise<HaiResult<SendResult>>` | 发送消息(通过 provider 字段路由) |
176
188
  | `reach.queue.get` | `(queueId: string) => Promise<HaiResult<ReachQueueEntry \| null>>` | 不含正文/收件人的队列状态 |
177
189
  | `reach.queue.list` | `(options: ReachQueueListOptions) => Promise<HaiResult<ReachQueueEntry[]>>` | 可信服务端分页查询 dead/unknown,每页至多 100 条 |
@@ -5,7 +5,7 @@ description: "为独立 API 服务装配 contract/procedures、认证守卫、No
5
5
 
6
6
  # hai-serv
7
7
 
8
- transport 仅支持 v2:SM2 发送方签名、独立密钥的 Encrypt-then-HMAC、HTTP 上下文/响应状态绑定及共享原子防重放。GET/HEAD、空 POST 与空响应也认证;无降级开关。TLS 信任边界、5 分钟窗口、nonce 持久化与实现示例见 [参考](reference.md)。
8
+ transport 仅支持 v2:SM2 发送方签名、独立密钥的 Encrypt-then-HMAC、HTTP 上下文/响应状态绑定及共享原子防重放。GET/HEAD、空 POST 与空响应也认证;无降级开关。TLS 信任边界、可配时间窗口、nonce 持久化与实现示例见 [参考](reference.md)。
9
9
 
10
10
  ## 能力契约
11
11
 
@@ -287,7 +287,9 @@ transport:
287
287
  - /_hai/scalar.js
288
288
  ```
289
289
 
290
- 认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求有效期 5 分钟,未来时钟偏差最多 30 秒;共享 registry 原子消费 (clientId, requestId),nonce 至少保留到 issuedAt + 6 分钟以覆盖节点偏差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
290
+ 认证协议仅支持 v2:独立随机 SM4-CBC 密钥和 HMAC-SM3 密钥由 SM2 封装,先验签、再验证 MAC,最后解密 CBC。请求由客户端私钥签名,响应由所选服务端版本签名。认证字段包括 version、direction、serverKeyId、clientId、128 位随机 requestId(兼 nonce)、issuedAt、HTTP method、pathname + 原始 search 的 SM3 pathHash,以及响应 status。请求默认有效期为 5 分钟,客户端时间最多可领先服务器 30 秒。服务端可用 `maxRequestAgeSeconds`(1~600)和 `maxFutureSkewSeconds`(0~300)调整,单位为整数秒。共享 registry 原子消费 (clientId, requestId),nonce 保留到 issuedAt + 11 分钟,覆盖最长可配窗口及节点时钟差,存储故障拒绝请求。Redis 必须保留有效 nonce,使用 noeviction/可靠持久化策略;各节点时钟须同步。此能力防止同一传输消息重放,业务重试仍需自己的幂等键。
291
+
292
+ 在 `_serv.yml` 的 `transport` 块配置时间窗口,将字段传给服务端 transport 装配。完成旧节点升级后等待 12 分钟,再把有效期调到 300 秒以上。校时提示与 `TransportTimeError.serverTime` 见 [Crypto 参考](../hai-crypto/reference.md)。
291
293
 
292
294
  空 POST 也发送空明文的认证封装;GET/HEAD 将封装放在 X-Hai-Transport 头,204/304/HEAD 等无正文响应也使用同名头承载认证封装。其它请求/响应在 JSON 正文承载封装并带 X-Encrypted: true。客户端必须关联原请求上下文和 HTTP status,缺标记、旧格式或认证失败一律 reject;明文接口由 Serv/Kit 路径规则 `encryption: disabled` 或宿主 `excludePaths` 显式允许;调用方同步选择普通请求,API Client/Kit 包装器使用自己的 `excludePaths`。CORS 须允许并暴露 X-Hai-Transport。握手公钥的真实性依赖 TLS,不替代 IAM 业务授权。
293
295
 
@@ -33,7 +33,7 @@ Toast 支持 F8 聚焦/返回、Escape 关闭可关闭提示并恢复焦点;Mo
33
33
 
34
34
  状态层可从轻量 `@h-ai/ui/i18n` 导入同一个 `authErrorMessage`,避免加载场景组件;管理台、移动/桌面示例与模板保持一致。
35
35
 
36
- 认证错误复用公开 `authErrorMessage(error)`(从 `@h-ai/ui/components/scenes/iam/index` 导入):凭据错误统一,不显示未知原文,保留有效 traceId 供排查图标展开;首次/到期改密、密码复用 034 和会话上限有明确指引。AuthPanel 与敏感操作确认已内置;自定义改密复用此映射。确认密码按原始字符串比较,不裁剪或 Unicode 归一化;字段错误、操作成功和认证错误均使用统一 Toast,字段保留定位与无障碍关联。
36
+ 认证错误复用公开 `authErrorMessage(error)`(从 `@h-ai/ui/components/scenes/iam/index` 导入):凭据错误统一,不显示未知原文,保留有效 traceId 供排查图标展开;首次/到期改密、密码复用 034 和会话上限有明确指引。AuthPanel 与敏感操作确认已内置;自定义改密复用此映射。确认密码按原始字符串比较,不裁剪或 Unicode 归一化;字段错误、操作成功和认证错误均使用统一 Toast,字段保留定位与无障碍关联。Kit 和 API Client 的已认证传输时间诊断会在认证提示中显示服务器 UTC 时间及校时建议。
37
37
 
38
38
  新页面或较大改版先使用 [hai-ui-design](../hai-ui-design/SKILL.md) 确定任务、布局、状态和组件映射;本 skill 负责正确实现。完成后按 [hai-ui-review](../hai-ui-review/SKILL.md) 查看真实页面,小范围修复沿用已有设计。
39
39