@h-ai/cli 0.1.0-alpha.18 → 0.1.0-alpha.19
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/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/templates/apps/android-app/src/lib/api.ts.hbs +1 -1
- package/templates/base/package.json.hbs +25 -24
- package/templates/features/payment/routes/api/v1/payment/create/+server.ts +15 -7
- package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +1 -2
- package/templates/skills/hai-ai/SKILL.md +2 -0
- package/templates/skills/hai-api-client/SKILL.md +4 -0
- package/templates/skills/hai-audit/SKILL.md +7 -2
- package/templates/skills/hai-cache/SKILL.md +2 -0
- package/templates/skills/hai-capacitor/SKILL.md +11 -9
- package/templates/skills/hai-core/SKILL.md +4 -1
- package/templates/skills/hai-crypto/SKILL.md +42 -8
- package/templates/skills/hai-iam/SKILL.md +2 -0
- package/templates/skills/hai-kit/SKILL.md +9 -2
- package/templates/skills/hai-payment/SKILL.md +5 -0
- package/templates/skills/hai-reach/SKILL.md +3 -1
- package/templates/skills/hai-reldb/SKILL.md +2 -0
- package/templates/skills/hai-scheduler/SKILL.md +2 -0
- package/templates/skills/hai-serv/SKILL.md +3 -0
- package/templates/skills/hai-storage/SKILL.md +10 -1
- package/templates/skills/hai-vecdb/SKILL.md +8 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ai/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.19",
|
|
5
5
|
"description": "Hai Framework CLI for project scaffolding and code generation.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"keywords": [
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"ora": "^9.1.0",
|
|
35
35
|
"prompts": "^2.4.2",
|
|
36
36
|
"yaml": "^2.7.0",
|
|
37
|
-
"@h-ai/core": "0.1.0-alpha.
|
|
37
|
+
"@h-ai/core": "0.1.0-alpha.19"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/fs-extra": "^11.0.4",
|
|
@@ -15,7 +15,7 @@ const API_BASE = (import.meta.env.PUBLIC_API_BASE as string | undefined) ?? 'htt
|
|
|
15
15
|
* 初始化 API 客户端
|
|
16
16
|
*
|
|
17
17
|
* 应在应用启动时调用一次。
|
|
18
|
-
* - Token 存储使用 Capacitor
|
|
18
|
+
* - Token 存储使用 Capacitor 原生安全存储插件(仅原生环境启用)
|
|
19
19
|
* - 自动附加 Bearer Token、401 自动刷新重试
|
|
20
20
|
*/
|
|
21
21
|
export async function initApi() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "{{projectName}}",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.19",
|
|
4
4
|
"private": true,
|
|
5
5
|
"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 'android-app'}}Android app built with hai Agent Framework{{/if_eq}}",
|
|
6
6
|
"keywords": ["hai", "sveltekit", "svelte5", "{{appType}}"],
|
|
@@ -22,35 +22,36 @@
|
|
|
22
22
|
"clean": "rimraf .svelte-kit build .turbo"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@h-ai/core": "^0.1.0-alpha.
|
|
26
|
-
"@h-ai/kit": "^0.1.0-alpha.
|
|
27
|
-
"@h-ai/ui": "^0.1.0-alpha.
|
|
28
|
-
"@h-ai/reldb": "^0.1.0-alpha.
|
|
29
|
-
"@h-ai/cache": "^0.1.0-alpha.
|
|
30
|
-
"@h-ai/iam": "^0.1.0-alpha.
|
|
31
|
-
"@h-ai/crypto": "^0.1.0-alpha.
|
|
32
|
-
"@h-ai/storage": "^0.1.0-alpha.
|
|
33
|
-
"@h-ai/ai": "^0.1.0-alpha.
|
|
34
|
-
"@h-ai/audit": "^0.1.0-alpha.
|
|
35
|
-
"@h-ai/reach": "^0.1.0-alpha.
|
|
36
|
-
"@h-ai/payment": "^0.1.0-alpha.
|
|
37
|
-
"@h-ai/vecdb": "^0.1.0-alpha.
|
|
38
|
-
"@h-ai/datapipe": "^0.1.0-alpha.
|
|
39
|
-
"@h-ai/scheduler": "^0.1.0-alpha.
|
|
40
|
-
"@h-ai/deploy": "^0.1.0-alpha.
|
|
41
|
-
"@h-ai/api-client": "^0.1.0-alpha.
|
|
42
|
-
"@h-ai/capacitor": "^0.1.0-alpha.
|
|
43
|
-
"@h-ai/api-client": "^0.1.0-alpha.
|
|
44
|
-
"@h-ai/capacitor": "^0.1.0-alpha.
|
|
25
|
+
"@h-ai/core": "^0.1.0-alpha.19",
|
|
26
|
+
"@h-ai/kit": "^0.1.0-alpha.19"{{#if hasUi}},
|
|
27
|
+
"@h-ai/ui": "^0.1.0-alpha.19"{{/if}}{{#if features.db}},
|
|
28
|
+
"@h-ai/reldb": "^0.1.0-alpha.19"{{/if}}{{#if features.cache}},
|
|
29
|
+
"@h-ai/cache": "^0.1.0-alpha.19"{{/if}}{{#if features.iam}},
|
|
30
|
+
"@h-ai/iam": "^0.1.0-alpha.19"{{/if}}{{#if features.crypto}},
|
|
31
|
+
"@h-ai/crypto": "^0.1.0-alpha.19"{{/if}}{{#if features.storage}},
|
|
32
|
+
"@h-ai/storage": "^0.1.0-alpha.19"{{/if}}{{#if features.ai}},
|
|
33
|
+
"@h-ai/ai": "^0.1.0-alpha.19"{{/if}}{{#if features.audit}},
|
|
34
|
+
"@h-ai/audit": "^0.1.0-alpha.19"{{/if}}{{#if features.reach}},
|
|
35
|
+
"@h-ai/reach": "^0.1.0-alpha.19"{{/if}}{{#if features.payment}},
|
|
36
|
+
"@h-ai/payment": "^0.1.0-alpha.19"{{/if}}{{#if features.vecdb}},
|
|
37
|
+
"@h-ai/vecdb": "^0.1.0-alpha.19"{{/if}}{{#if features.datapipe}},
|
|
38
|
+
"@h-ai/datapipe": "^0.1.0-alpha.19"{{/if}}{{#if features.scheduler}},
|
|
39
|
+
"@h-ai/scheduler": "^0.1.0-alpha.19"{{/if}}{{#if features.deploy}},
|
|
40
|
+
"@h-ai/deploy": "^0.1.0-alpha.19"{{/if}}{{#if features.api-client}},
|
|
41
|
+
"@h-ai/api-client": "^0.1.0-alpha.19"{{/if}}{{#if features.capacitor}},
|
|
42
|
+
"@h-ai/capacitor": "^0.1.0-alpha.19"{{/if}}{{#if isCapacitorApp}}{{#unless features.api-client}},
|
|
43
|
+
"@h-ai/api-client": "^0.1.0-alpha.19"{{/unless}}{{#unless features.capacitor}},
|
|
44
|
+
"@h-ai/capacitor": "^0.1.0-alpha.19"{{/unless}}{{/if}},
|
|
45
45
|
"zod": "^4.4.3"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@antfu/eslint-config": "^9.0.0",
|
|
49
49
|
"@playwright/test": "^1.60.0",
|
|
50
50
|
{{#if isCapacitorApp}}
|
|
51
|
-
|
|
52
|
-
"@capacitor/
|
|
53
|
-
"@capacitor/
|
|
51
|
+
"@aparajita/capacitor-secure-storage": "^8.0.0",
|
|
52
|
+
"@capacitor/cli": "^8.3.4",
|
|
53
|
+
"@capacitor/core": "^8.3.4",
|
|
54
|
+
"@capacitor/android": "^8.3.4",
|
|
54
55
|
"@capacitor/preferences": "^8.0.1",
|
|
55
56
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
56
57
|
{{else}}
|
|
@@ -7,18 +7,26 @@ import { payment } from '@h-ai/payment'
|
|
|
7
7
|
|
|
8
8
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
|
9
9
|
const body = await request.json()
|
|
10
|
-
const { provider, tradeType, outTradeNo, totalAmount, subject, notifyUrl } = body
|
|
10
|
+
const { provider, tradeType, outTradeNo, totalAmount, subject, notifyUrl, userId, metadata } = body
|
|
11
11
|
|
|
12
12
|
if (!provider || !outTradeNo || !totalAmount || !subject) {
|
|
13
13
|
return kit.response.badRequest('Missing required fields: provider, outTradeNo, totalAmount, subject')
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
const normalizedTradeType = tradeType === 'h5'
|
|
17
|
+
|| tradeType === 'app'
|
|
18
|
+
|| tradeType === 'native'
|
|
19
|
+
|| tradeType === 'mini_program'
|
|
20
|
+
? tradeType
|
|
21
|
+
: 'jsapi'
|
|
22
|
+
|
|
23
|
+
const result = await payment.createOrder(provider, {
|
|
24
|
+
orderNo: outTradeNo,
|
|
25
|
+
amount: Number(totalAmount),
|
|
26
|
+
description: subject,
|
|
27
|
+
tradeType: normalizedTradeType,
|
|
28
|
+
userId,
|
|
29
|
+
metadata,
|
|
22
30
|
notifyUrl: notifyUrl ?? `${new URL(request.url).origin}/api/v1/payment/notify/${provider}`,
|
|
23
31
|
})
|
|
24
32
|
|
|
@@ -48,6 +48,8 @@ await apiClient.init({
|
|
|
48
48
|
})
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
> `createCapacitorTokenStorage()` 仅在 Android/iOS 原生环境 + `@aparajita/capacitor-secure-storage` 已安装时持久化 token;纯 Web 不会回退到 `localStorage`。
|
|
52
|
+
|
|
51
53
|
SSR / Node.js 测试场景请显式传入内存存储:
|
|
52
54
|
|
|
53
55
|
```ts
|
|
@@ -84,6 +86,8 @@ const client = apiClient.create(contract)
|
|
|
84
86
|
await client.init({ baseUrl: 'https://api.example.com/api/v1' })
|
|
85
87
|
```
|
|
86
88
|
|
|
89
|
+
`client.config` / `apiClient.config` 返回的是脱敏后的运行配置快照;`baseUrl` 的内嵌凭证与 `headers.authorization` 等敏感值会自动隐藏。
|
|
90
|
+
|
|
87
91
|
### 4. 关闭
|
|
88
92
|
|
|
89
93
|
```ts
|
|
@@ -52,7 +52,10 @@ await audit.init({
|
|
|
52
52
|
### 3. 关闭
|
|
53
53
|
|
|
54
54
|
```ts
|
|
55
|
-
await audit.close()
|
|
55
|
+
const closeResult = await audit.close()
|
|
56
|
+
if (!closeResult.success) {
|
|
57
|
+
throw new Error(closeResult.error.message)
|
|
58
|
+
}
|
|
56
59
|
```
|
|
57
60
|
|
|
58
61
|
## 核心 API
|
|
@@ -62,7 +65,7 @@ await audit.close()
|
|
|
62
65
|
| 方法 | 签名 | 说明 |
|
|
63
66
|
| --------------------- | ---------------------------------------------------------------- | ---------- |
|
|
64
67
|
| `audit.init` | `(config: AuditInitConfig) => Promise<HaiResult<void>>` | 初始化模块 |
|
|
65
|
-
| `audit.close` | `() => Promise<void
|
|
68
|
+
| `audit.close` | `() => Promise<HaiResult<void>>` | 关闭模块 |
|
|
66
69
|
| `audit.isInitialized` | `boolean` | 初始化状态 |
|
|
67
70
|
|
|
68
71
|
### 日志操作
|
|
@@ -86,6 +89,8 @@ await audit.close()
|
|
|
86
89
|
| `audit.helper.passwordResetComplete(userId, ip?, ua?)` | 密码重置完成 |
|
|
87
90
|
| `audit.helper.crud(input)` | CRUD 操作 |
|
|
88
91
|
|
|
92
|
+
> `audit.log()` / `audit.helper.*()` 中的 `details` 仅会自动脱敏凭证类字段(如 `password` / `token` / `secret`);邮箱、手机号默认保留原值。
|
|
93
|
+
|
|
89
94
|
## 错误码
|
|
90
95
|
|
|
91
96
|
| 错误码 | code | 说明 |
|
|
@@ -5,20 +5,20 @@ description: 使用 @h-ai/capacitor 桥接 Capacitor 原生能力(Token 安全
|
|
|
5
5
|
|
|
6
6
|
# hai-capacitor
|
|
7
7
|
|
|
8
|
-
> `@h-ai/capacitor` 是 hai-framework 的 Capacitor 原生桥接模块,封装常用原生能力为统一 API,返回 `HaiResult<T>`。与 `@h-ai/api-client`
|
|
8
|
+
> `@h-ai/capacitor` 是 hai-framework 的 Capacitor 原生桥接模块,封装常用原生能力为统一 API,返回 `HaiResult<T>`。与 `@h-ai/api-client` 配合时,Token 仅存于原生安全存储,不回退到 Web `localStorage`。
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
## 运行环境
|
|
13
13
|
|
|
14
|
-
> **浏览器端 / 原生 App 专用。** 在 Capacitor 原生环境(Android/iOS
|
|
14
|
+
> **浏览器端 / 原生 App 专用。** 在 Capacitor 原生环境(Android/iOS)中提供完整能力;`createCapacitorTokenStorage()` 仅在原生环境持久化 token,纯 Web 不做不安全回退。`capacitor.preferences` 仍可用于普通偏好数据。
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
## 适用场景
|
|
19
19
|
|
|
20
20
|
- Android/iOS 原生应用开发(SvelteKit + Capacitor)
|
|
21
|
-
- Token
|
|
21
|
+
- Token 安全存储(`@aparajita/capacitor-secure-storage`)
|
|
22
22
|
- 设备信息获取(平台、型号、版本)
|
|
23
23
|
- 推送通知注册与监听(FCM / APNs)
|
|
24
24
|
- 原生相机拍照 / 相册选取
|
|
@@ -64,13 +64,13 @@ await apiClient.init({
|
|
|
64
64
|
})
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
`createCapacitorTokenStorage()` 返回 `TokenStorage` 实例(兼容 `@h-ai/api-client`),底层使用 `@capacitor
|
|
67
|
+
`createCapacitorTokenStorage()` 返回 `TokenStorage` 实例(兼容 `@h-ai/api-client`),底层使用 `@aparajita/capacitor-secure-storage`:
|
|
68
68
|
|
|
69
|
-
- Android → SharedPreferences
|
|
70
|
-
- iOS →
|
|
71
|
-
- Web → localStorage
|
|
69
|
+
- Android → Android KeyStore + 加密 SharedPreferences
|
|
70
|
+
- iOS → Keychain
|
|
71
|
+
- Web → **不回退到 localStorage**;`get*()` 返回 `null`,`set*()` / `clear()` 为 no-op
|
|
72
72
|
|
|
73
|
-
所有方法内置 try-catch
|
|
73
|
+
所有方法内置 try-catch;原生安全存储异常时 get 返回 `null`、set/clear 静默失败并记录日志。
|
|
74
74
|
|
|
75
75
|
#### Preferences 子操作
|
|
76
76
|
|
|
@@ -208,6 +208,7 @@ await capacitor.statusBar.show()
|
|
|
208
208
|
| `HaiCapacitorError.PREFERENCES_SET_FAILED` | `hai:capacitor:012` | Preferences 写入失败 |
|
|
209
209
|
| `HaiCapacitorError.PREFERENCES_REMOVE_FAILED`| `hai:capacitor:013` | Preferences 删除失败 |
|
|
210
210
|
| `HaiCapacitorError.DEVICE_INFO_FAILED` | `hai:capacitor:020` | 获取设备信息失败 |
|
|
211
|
+
| `HaiCapacitorError.APP_VERSION_FAILED` | `hai:capacitor:021` | 获取应用版本失败 |
|
|
211
212
|
| `HaiCapacitorError.PUSH_REGISTER_FAILED` | `hai:capacitor:030` | 推送注册失败 |
|
|
212
213
|
| `HaiCapacitorError.PUSH_LISTEN_FAILED` | `hai:capacitor:031` | 推送监听失败 |
|
|
213
214
|
| `HaiCapacitorError.CAMERA_FAILED` | `hai:capacitor:040` | 拍照/相册失败 |
|
|
@@ -359,7 +360,8 @@ async function captureAndUpload() {
|
|
|
359
360
|
| 插件 | 类型 | 用于 |
|
|
360
361
|
| --- | --- | --- |
|
|
361
362
|
| `@capacitor/core` | peerDependency(必需) | 核心运行时 |
|
|
362
|
-
| `@capacitor
|
|
363
|
+
| `@aparajita/capacitor-secure-storage` | 可选 peerDependency(原生 token 存储必需) | `createCapacitorTokenStorage()` |
|
|
364
|
+
| `@capacitor/preferences` | peerDependency(必需) | `capacitor.preferences.*` 普通偏好数据 |
|
|
363
365
|
| `@capacitor/device` | 可选 | `getDeviceInfo()` |
|
|
364
366
|
| `@capacitor/app` | 可选 | `getAppVersion()` |
|
|
365
367
|
| `@capacitor/push-notifications` | 可选 | `registerPush()` / `listenPush()` |
|
|
@@ -16,7 +16,7 @@ description: 使用 @h-ai/core 进行配置加载、日志记录、i18n 国际
|
|
|
16
16
|
| 能力 | Node.js | 浏览器 |
|
|
17
17
|
|------|---------|--------|
|
|
18
18
|
| `core.logger` | ✅ pino(结构化 JSON / pretty) | ✅ loglevel(DevTools console) |
|
|
19
|
-
| `core.config` | ✅ 完整(YAML 加载、watch、validate) | ⚠️
|
|
19
|
+
| `core.config` | ✅ 完整(YAML 加载、watch、validate) | ⚠️ 占位 API;文件配置方法返回 `SERVICE_UNAVAILABLE` |
|
|
20
20
|
| `core.i18n` | ✅ | ✅ |
|
|
21
21
|
| `core.zodValidation` | ✅ | ✅ |
|
|
22
22
|
| `core.id` | ✅ | ✅ |
|
|
@@ -79,6 +79,8 @@ const formErrors = core.zodValidation.mapZodErrorToFormErrors(zodError, getMessa
|
|
|
79
79
|
|
|
80
80
|
### 配置管理 — `core.config`
|
|
81
81
|
|
|
82
|
+
> 浏览器端保留 `core.config` 对象用于类型/runtime 对齐,但没有文件系统:`get()` 返回 `undefined`,`has()` 返回 `false`,`keys()` 返回 `[]`;`load()` / `validate()` / `reload()` 返回 `HaiCommonError.SERVICE_UNAVAILABLE`;`watch()` 会立即回调该错误并返回 no-op 取消函数。
|
|
83
|
+
|
|
82
84
|
| 方法 | 签名 | 说明 |
|
|
83
85
|
| ------------ | -------------------------------------------------------------- | ------------------------------ |
|
|
84
86
|
| `get` | `<T>(name: string) => T \| undefined` | 获取模块配置段 |
|
|
@@ -541,6 +543,7 @@ logger.debug('Data fetched', { count: items.length })
|
|
|
541
543
|
|
|
542
544
|
| 功能 | 行为 |
|
|
543
545
|
|------|------|
|
|
546
|
+
| `core.config` 文件配置 | 不支持;文件相关方法返回 `SERVICE_UNAVAILABLE`,配置应由服务端读取后下发 |
|
|
544
547
|
| `format: 'json' \| 'pretty'` | 被忽略(始终输出到 DevTools console) |
|
|
545
548
|
| `redact` 字段脱敏 | 不生效(需手动脱敏) |
|
|
546
549
|
| `fatal` 级别 | 映射为 `console.error`(带 `[FATAL]` 前缀) |
|
|
@@ -106,8 +106,8 @@ const valid = crypto.hash.verify('hello', hash.data!)
|
|
|
106
106
|
| --------------- | ----------------------------------------------- | --------------------------- |
|
|
107
107
|
| `generateKey` | `() => string` | 生成 128-bit 密钥(32 hex) |
|
|
108
108
|
| `generateIV` | `() => string` | 生成随机 IV(32 hex) |
|
|
109
|
-
| `encrypt` | `(data, key, options?) => HaiResult<
|
|
110
|
-
| `decrypt` | `(
|
|
109
|
+
| `encrypt` | `(data, key, options?) => HaiResult<SymmetricEncryptedPayload>` | 加密(返回结构化密文) |
|
|
110
|
+
| `decrypt` | `(payload, key) => HaiResult<string>` | 解密结构化密文 |
|
|
111
111
|
| `encryptWithIV` | `(data, key) => HaiResult<EncryptWithIVResult>` | CBC 模式加密(自动生成 IV) |
|
|
112
112
|
| `decryptWithIV` | `(ciphertext, key, iv) => HaiResult<string>` | CBC 模式解密 |
|
|
113
113
|
| `deriveKey` | `(password, salt) => string` | 从密码和盐值派生密钥 |
|
|
@@ -116,13 +116,24 @@ const valid = crypto.hash.verify('hello', hash.data!)
|
|
|
116
116
|
|
|
117
117
|
```typescript
|
|
118
118
|
const key = crypto.symmetric.generateKey()
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
|
|
120
|
+
// 默认安全用法:CBC + 自动 IV,返回 { mode, ciphertext, iv, encoding }
|
|
121
|
+
const encrypted = crypto.symmetric.encrypt('明文数据', key)
|
|
122
|
+
if (encrypted.success) {
|
|
123
|
+
const decrypted = crypto.symmetric.decrypt(encrypted.data, key)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 需要单独存储 IV 时使用结构化结果
|
|
127
|
+
const withIV = crypto.symmetric.encryptWithIV('明文数据', key)
|
|
128
|
+
if (withIV.success) {
|
|
129
|
+
const decrypted = crypto.symmetric.decryptWithIV(withIV.data.ciphertext, key, withIV.data.iv)
|
|
122
130
|
}
|
|
123
131
|
```
|
|
124
132
|
|
|
125
|
-
**
|
|
133
|
+
**SymmetricEncryptOptions**:`{ mode?: 'ecb' | 'cbc', iv?: string, outputFormat?: 'hex' | 'base64' }`
|
|
134
|
+
**SymmetricEncryptedPayload**:`{ mode: 'ecb' | 'cbc', ciphertext: string, iv?: string, encoding: 'hex' | 'base64' }`
|
|
135
|
+
|
|
136
|
+
> `encrypt()` 默认使用 CBC 并自动生成 IV。ECB 会泄漏明文结构,只有底层协议明确要求时才显式 `{ mode: 'ecb' }`。
|
|
126
137
|
|
|
127
138
|
### 密码哈希 — `crypto.password`
|
|
128
139
|
|
|
@@ -143,7 +154,7 @@ if (hashed.success) {
|
|
|
143
154
|
|
|
144
155
|
### 传输加密 — `crypto.transport`
|
|
145
156
|
|
|
146
|
-
|
|
157
|
+
传输加密主工厂必须从 `crypto.transport` 访问;共享 `keyStore` provider 请从 `@h-ai/crypto` 根入口导入,不从子目录内部路径导入。
|
|
147
158
|
|
|
148
159
|
| 方法 | 签名 | 说明 |
|
|
149
160
|
| --- | --- | --- |
|
|
@@ -151,6 +162,14 @@ if (hashed.success) {
|
|
|
151
162
|
| `createClient` | `({ keyExchangeUrl, fetch? }) => TransportClient` | 创建客户端 encryptedFetch 会话 |
|
|
152
163
|
| `protocol` | `TRANSPORT_PROTOCOL` | 协议常量:`X-Client-Id`、`X-Encrypted`、默认协商路径 |
|
|
153
164
|
|
|
165
|
+
共享 `keyStore` provider(从 `@h-ai/crypto` 根入口导入):
|
|
166
|
+
|
|
167
|
+
| 工厂 | 签名 | 说明 |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| `createInMemoryKeyStore` | `(maxClients?) => TransportKeyStore` | 默认 FIFO 内存实现 |
|
|
170
|
+
| `createRedisTransportKeyStore` | `({ cache, ttlSeconds? }) => TransportKeyStore` | 基于 `@h-ai/cache` 的共享实现,通常配 Redis |
|
|
171
|
+
| `createReldbTransportKeyStore` | `({ reldb, ttlSeconds? }) => TransportKeyStore` | 基于 `@h-ai/reldb` 的共享实现,自动建表 |
|
|
172
|
+
|
|
154
173
|
```typescript
|
|
155
174
|
const server = crypto.transport.createServer({ maxClients: 10000 })
|
|
156
175
|
if (!server.success)
|
|
@@ -166,6 +185,17 @@ const resp = await client.encryptedFetch('https://api.example.com/api/v1/echo',
|
|
|
166
185
|
})
|
|
167
186
|
```
|
|
168
187
|
|
|
188
|
+
```typescript
|
|
189
|
+
import { createRedisTransportKeyStore, crypto } from '@h-ai/crypto'
|
|
190
|
+
import { cache } from '@h-ai/cache'
|
|
191
|
+
|
|
192
|
+
await cache.init({ type: 'redis', host: '127.0.0.1', port: 6379 })
|
|
193
|
+
|
|
194
|
+
const sharedServer = crypto.transport.createServer({
|
|
195
|
+
keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
|
|
196
|
+
})
|
|
197
|
+
```
|
|
198
|
+
|
|
169
199
|
使用流程:
|
|
170
200
|
1. 先 `await crypto.init()`。
|
|
171
201
|
2. 服务端 `createServer()` 后暴露一个 POST 密钥协商端点:接收 `clientPublicKey`,返回 `serverPublicKey + clientId`。
|
|
@@ -174,6 +204,8 @@ const resp = await client.encryptedFetch('https://api.example.com/api/v1/echo',
|
|
|
174
204
|
5. 服务端先 `decryptRequest()`,业务处理后再 `encryptResponse(clientId, data)`。
|
|
175
205
|
6. 客户端收到 `X-Encrypted: true` 的响应后自动解密;`destroy()` 可清空当前会话。
|
|
176
206
|
|
|
207
|
+
`encryptedFetch()` 与原生 fetch 一样会 reject:网络错误、密钥协商失败、请求加密失败或响应解密失败都应在调用方按 fetch 错误处理策略捕获。
|
|
208
|
+
|
|
177
209
|
常规应用优先使用上层封装:
|
|
178
210
|
- serv:`serv.createApp({ transport: { crypto } })`
|
|
179
211
|
- kit:`kit.createHandle({ crypto: { crypto, transport: true } })` + `kit.client.create({ transport: { crypto } })`
|
|
@@ -210,9 +242,11 @@ const encrypted = crypto.symmetric.encrypt(sensitiveData, key)
|
|
|
210
242
|
// 存入数据库
|
|
211
243
|
|
|
212
244
|
// 读取时解密
|
|
213
|
-
const decrypted = crypto.symmetric.decrypt(
|
|
245
|
+
const decrypted = crypto.symmetric.decrypt(storedPayload, key)
|
|
214
246
|
```
|
|
215
247
|
|
|
248
|
+
> 默认密文是结构化对象,必须完整存储 `mode` / `ciphertext` / `iv` / `encoding` 字段;如果数据库字段已拆分 IV 与密文,请改用 `encryptWithIV()` / `decryptWithIV()`。
|
|
249
|
+
|
|
216
250
|
### 密码存储与验证
|
|
217
251
|
|
|
218
252
|
```typescript
|
|
@@ -83,6 +83,8 @@ await iam.init({
|
|
|
83
83
|
})
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
`iam.config` 返回的是脱敏后的配置快照;LDAP `url` / `bindPassword` 等敏感值会自动隐藏。
|
|
87
|
+
|
|
86
88
|
初始化时自动创建数据库表(5 张):`hai_iam_users`、`hai_iam_roles`、`hai_iam_permissions`、`hai_iam_role_permissions`、`hai_iam_user_roles`。
|
|
87
89
|
|
|
88
90
|
Token 和 OTP 存储在 cache 中(不落库)。refreshToken 使用独立的 cache key 存储。
|
|
@@ -58,10 +58,12 @@ kit 不再维护本地传输加密实现;服务端和客户端都统一委托
|
|
|
58
58
|
|
|
59
59
|
```ts
|
|
60
60
|
// hooks.server.ts
|
|
61
|
-
import {
|
|
61
|
+
import { cache } from '@h-ai/cache'
|
|
62
|
+
import { createRedisTransportKeyStore, crypto } from '@h-ai/crypto'
|
|
62
63
|
import { kit } from '@h-ai/kit'
|
|
63
64
|
|
|
64
65
|
await crypto.init()
|
|
66
|
+
await cache.init({ type: 'redis', host: '127.0.0.1', port: 6379 })
|
|
65
67
|
|
|
66
68
|
export const handle = kit.createHandle({
|
|
67
69
|
auth: {
|
|
@@ -71,7 +73,10 @@ export const handle = kit.createHandle({
|
|
|
71
73
|
},
|
|
72
74
|
crypto: {
|
|
73
75
|
crypto,
|
|
74
|
-
transport: {
|
|
76
|
+
transport: {
|
|
77
|
+
requireEncryption: true,
|
|
78
|
+
keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
|
|
79
|
+
},
|
|
75
80
|
},
|
|
76
81
|
})
|
|
77
82
|
```
|
|
@@ -100,6 +105,8 @@ export const { apiFetch } = kit.client.create({ auth: true })
|
|
|
100
105
|
|
|
101
106
|
默认协商端点:`/api/_hai/key-exchange`。如 `keyExchangePath` 自定义,浏览器端同步设置 `keyExchangeUrl`。
|
|
102
107
|
|
|
108
|
+
多节点部署时,可在 `kit.createHandle({ crypto: { transport } })` 的运行时对象里直接传 `keyStore`;推荐使用 `@h-ai/crypto` 根入口导出的 `createRedisTransportKeyStore()` / `createReldbTransportKeyStore()`。这类对象依赖不要写进 `_kit.yml`。
|
|
109
|
+
|
|
103
110
|
transport 默认保护同源 `/api/*` endpoint 与 SvelteKit `__data.json` 页面数据请求;页面文档、静态资源与 `multipart/form-data` 上传请求保持明文透传。
|
|
104
111
|
|
|
105
112
|
安全策略默认 fail-closed:`requireEncryption: true` 时,受保护路径缺少 `X-Client-Id` 必须返回 400;服务端 transport 管理器不可用、响应体无法加密或超过单次加密上限时返回错误,禁止明文业务响应回退。`requireEncryption: false` 只适合迁移期灰度。
|
|
@@ -58,6 +58,7 @@ alipay:
|
|
|
58
58
|
stripe:
|
|
59
59
|
secretKey: ${HAI_PAYMENT_STRIPE_SECRET_KEY}
|
|
60
60
|
webhookSecret: ${HAI_PAYMENT_STRIPE_WEBHOOK_SECRET}
|
|
61
|
+
webhookToleranceSeconds: 300 # 可选,默认 300 秒
|
|
61
62
|
```
|
|
62
63
|
|
|
63
64
|
### 2. 初始化(服务端)
|
|
@@ -101,6 +102,8 @@ if (result.success) {
|
|
|
101
102
|
// result.data: PaymentOrder { provider, tradeType, clientParams, prepayId? }
|
|
102
103
|
// clientParams 包含调起支付所需参数
|
|
103
104
|
}
|
|
105
|
+
|
|
106
|
+
// amount / totalAmount 必须是正整数(单位:分)
|
|
104
107
|
```
|
|
105
108
|
|
|
106
109
|
### 4. 处理支付回调
|
|
@@ -262,6 +265,8 @@ export const POST = kit.handler(async ({ request, locals }) => {
|
|
|
262
265
|
|
|
263
266
|
关键支付操作(创建订单、回调、退款、关闭订单)成功后自动写入审计日志(依赖 `@h-ai/audit`),无需额外配置。`queryOrder` 为只读操作,不写审计日志。
|
|
264
267
|
|
|
268
|
+
内置 Provider 的 HTTP 请求默认带 15 秒超时;Stripe 可通过 `webhookToleranceSeconds` 调整 webhook 时间戳容忍窗口。
|
|
269
|
+
|
|
265
270
|
---
|
|
266
271
|
|
|
267
272
|
## 相关 Skills
|
|
@@ -96,6 +96,8 @@ await reach.init(core.config.get('reach'))
|
|
|
96
96
|
await reach.close()
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
`reach.config` 返回的是脱敏后的配置快照;`api.url` / `endpoint`、授权头、SMTP `pass`、短信密钥等敏感值会自动隐藏。
|
|
100
|
+
|
|
99
101
|
### 3. 保存模板(模板绑定 Provider)
|
|
100
102
|
|
|
101
103
|
```typescript
|
|
@@ -159,7 +161,7 @@ await reach.send({
|
|
|
159
161
|
| `reach.init` | `(config: ReachConfigInput) => Promise<HaiResult<void>>` | 初始化(注册多个 Provider) |
|
|
160
162
|
| `reach.send` | `(message: ReachMessage) => Promise<HaiResult<SendResult>>` | 发送消息(通过 provider 字段路由) |
|
|
161
163
|
| `reach.template` | `ReachTemplateRegistry` | 模板注册表 |
|
|
162
|
-
| `reach.config` | `ReachConfig \| null` |
|
|
164
|
+
| `reach.config` | `ReachConfig \| null` | 当前脱敏配置快照 |
|
|
163
165
|
| `reach.isInitialized` | `boolean` | 是否已初始化 |
|
|
164
166
|
| `reach.close` | `() => Promise<void>` | 关闭所有连接 |
|
|
165
167
|
|
|
@@ -80,6 +80,8 @@ await scheduler.register({
|
|
|
80
80
|
})
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
> ⚠️ **安全警示**:`kind: 'js'` 仅允许受信任的服务端代码。当前实现使用 Node.js `vm` 便捷执行,不是安全沙箱;禁止把用户、租户或运营后台自由输入的 JS 字符串直接注册为任务。需要可配置执行逻辑时,优先改用 `kind: 'api'` 或 `hooks.onTaskExecute`。
|
|
84
|
+
|
|
83
85
|
### 3. 使用全局 execute hook 处理无 handler 任务
|
|
84
86
|
|
|
85
87
|
```ts
|
|
@@ -271,6 +271,7 @@ const app = serv.createApp({
|
|
|
271
271
|
crypto,
|
|
272
272
|
// keyExchangePath 默认 '/_hai/key-exchange'
|
|
273
273
|
// maxClients: 10000,
|
|
274
|
+
// keyStore: createRedisTransportKeyStore({ cache, ttlSeconds: 3600 }),
|
|
274
275
|
},
|
|
275
276
|
})
|
|
276
277
|
```
|
|
@@ -298,6 +299,8 @@ await apiClient.init({
|
|
|
298
299
|
|
|
299
300
|
> 不要导入或暴露子目录内部 transport 工厂;公共装配点只有 `serv.createApp({ transport: { crypto } })`。
|
|
300
301
|
|
|
302
|
+
多节点部署时,直接在 `serv.createApp({ transport })` 的运行时对象里注入 `keyStore` 即可;推荐从 `@h-ai/crypto` 根入口导入 `createRedisTransportKeyStore()` / `createReldbTransportKeyStore()`。`keyStore` 不属于 `_serv.yml` 配置项,配置文件仍只保留静态路径和白名单。
|
|
303
|
+
|
|
301
304
|
**上下文工厂优先级**(`context.session` 填充来源):
|
|
302
305
|
|
|
303
306
|
1. 显式 `createContext`(高级场景:多租户、额外字段)
|
|
@@ -52,9 +52,14 @@ import { storage } from '@h-ai/storage'
|
|
|
52
52
|
|
|
53
53
|
await storage.init(core.config.get('storage'))
|
|
54
54
|
// 使用后关闭
|
|
55
|
-
await storage.close()
|
|
55
|
+
const closeResult = await storage.close()
|
|
56
|
+
if (!closeResult.success) {
|
|
57
|
+
throw new Error(closeResult.error.message)
|
|
58
|
+
}
|
|
56
59
|
```
|
|
57
60
|
|
|
61
|
+
> `storage.config` 返回的是**脱敏配置快照**;S3 的 `accessKeyId` / `secretAccessKey` 等敏感字段会被替换为 `[REDACTED]`。
|
|
62
|
+
|
|
58
63
|
---
|
|
59
64
|
|
|
60
65
|
## 核心 API
|
|
@@ -106,6 +111,8 @@ const downloadUrl = await storage.presign.getUrl('uploads/doc.pdf', {
|
|
|
106
111
|
})
|
|
107
112
|
```
|
|
108
113
|
|
|
114
|
+
> `storage.presign.putUrl()` 当前不支持 `maxSize` 约束参数;如需大小限制,请在应用层上传前校验,或在服务端落库/回调处二次校验。
|
|
115
|
+
|
|
109
116
|
---
|
|
110
117
|
|
|
111
118
|
## 错误码 — `HaiStorageError`
|
|
@@ -174,6 +181,8 @@ export const POST = kit.handler(async ({ request, locals }) => {
|
|
|
174
181
|
|
|
175
182
|
> 说明:`@h-ai/storage` 只提供存储能力,不强制要求 `/api/storage` 路由命名。应用可按业务语义自定义 API 路径。
|
|
176
183
|
|
|
184
|
+
> Local Provider 的 `storage.dir.list()` 会递归扫描匹配前缀下的文件,仅支持 `maxKeys` 截断,不支持 `continuationToken` 真分页;大目录建议缩小 `prefix` 或改用 S3 Provider。
|
|
185
|
+
|
|
177
186
|
### 头像上传
|
|
178
187
|
|
|
179
188
|
```typescript
|
|
@@ -55,9 +55,14 @@ import { vecdb } from '@h-ai/vecdb'
|
|
|
55
55
|
|
|
56
56
|
await vecdb.init(core.config.get('vecdb'))
|
|
57
57
|
// ... 使用向量数据库
|
|
58
|
-
await vecdb.close()
|
|
58
|
+
const closeResult = await vecdb.close()
|
|
59
|
+
if (!closeResult.success) {
|
|
60
|
+
throw new Error(closeResult.error.message)
|
|
61
|
+
}
|
|
59
62
|
```
|
|
60
63
|
|
|
64
|
+
> `vecdb.config` 返回的是**脱敏配置快照**;连接字符串中的用户名/密码、独立 `password` 字段和 `apiKey` 会被替换为 `[REDACTED]`。
|
|
65
|
+
|
|
61
66
|
### 3. 选择操作接口
|
|
62
67
|
|
|
63
68
|
| 接口 | 用途 | 入口 |
|
|
@@ -144,6 +149,8 @@ await vecdb.vector.upsert('docs', [
|
|
|
144
149
|
await vecdb.vector.delete('docs', ['doc-1', 'doc-2'])
|
|
145
150
|
```
|
|
146
151
|
|
|
152
|
+
> 空批量 `insert/upsert/delete` 会被视为 no-op;写入与搜索会校验向量维度,不匹配时返回 `HaiVecdbError.DIMENSION_MISMATCH`。Qdrant `collection.exists()` 仅在 404 时返回 `false`,网络异常会返回查询错误而不是误判不存在。
|
|
153
|
+
|
|
147
154
|
**VectorDocument**:
|
|
148
155
|
|
|
149
156
|
```typescript
|