@yeying-community/web3-bs 1.0.3 → 1.0.4
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/README.md +64 -432
- package/dist/auth/provider.d.ts +3 -1
- package/dist/auth/types.d.ts +14 -0
- package/dist/web3-bs.esm.js +58 -3
- package/dist/web3-bs.esm.js.map +1 -1
- package/dist/web3-bs.umd.js +59 -2
- package/dist/web3-bs.umd.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,326 +3,19 @@
|
|
|
3
3
|
轻量级注入钱包 SDK,专注浏览器端 EIP-1193 Provider。默认优先选择 YeYing Wallet(支持 EIP-6963 多钱包发现)。
|
|
4
4
|
仅支持浏览器环境(依赖 `window` / `localStorage` / `fetch`)。
|
|
5
5
|
|
|
6
|
+
功能要点:
|
|
7
|
+
- 浏览器端 EIP-1193 Provider 辅助库
|
|
8
|
+
- 默认优先 YeYing Wallet
|
|
9
|
+
- 内置 `signMessage` / `loginWithChallenge` / `refresh` / `logout` 等方法
|
|
10
|
+
- 支持 UCAN Session + SIWE Bridge,用于多后端授权
|
|
11
|
+
|
|
6
12
|
## 安装
|
|
7
13
|
|
|
8
14
|
```bash
|
|
9
15
|
npm install @yeying-community/web3-bs
|
|
10
16
|
```
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
SDK 仅支持浏览器环境。推荐两种接入方式:
|
|
15
|
-
|
|
16
|
-
### 方式一:模块化(打包器 / Vite / Webpack)
|
|
17
|
-
|
|
18
|
-
```ts
|
|
19
|
-
import { getProvider, loginWithChallenge, authFetch } from '@yeying-community/web3-bs';
|
|
20
|
-
|
|
21
|
-
const provider = await getProvider({ timeoutMs: 3000 });
|
|
22
|
-
const login = await loginWithChallenge({
|
|
23
|
-
provider,
|
|
24
|
-
baseUrl: 'http://localhost:3203/api/v1/public/auth',
|
|
25
|
-
storeToken: false,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const res = await authFetch('http://localhost:3203/api/v1/public/profile', { method: 'GET' }, {
|
|
29
|
-
baseUrl: 'http://localhost:3203/api/v1/public/auth',
|
|
30
|
-
storeToken: false,
|
|
31
|
-
});
|
|
32
|
-
console.log(await res.json());
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### 方式二:UMD(纯 HTML)
|
|
36
|
-
|
|
37
|
-
```html
|
|
38
|
-
<script src="https://your-host/dist/web3-bs.umd.js"></script>
|
|
39
|
-
<script>
|
|
40
|
-
const { getProvider, loginWithChallenge } = window.YeYingWeb3;
|
|
41
|
-
async function login() {
|
|
42
|
-
const provider = await getProvider({ timeoutMs: 3000 });
|
|
43
|
-
const result = await loginWithChallenge({
|
|
44
|
-
provider,
|
|
45
|
-
baseUrl: 'http://localhost:3203/api/v1/public/auth',
|
|
46
|
-
storeToken: false,
|
|
47
|
-
});
|
|
48
|
-
console.log(result);
|
|
49
|
-
}
|
|
50
|
-
login();
|
|
51
|
-
</script>
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
提示:UMD 模式下全局对象为 `window.YeYingWeb3`,需要保证 `dist/web3-bs.umd.js` 可访问。
|
|
55
|
-
|
|
56
|
-
## SIWE 登录(不使用 UCAN)
|
|
57
|
-
|
|
58
|
-
如果只需要传统 SIWE + JWT 登录(单后端),可以不启用 UCAN:
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
import { getProvider, loginWithChallenge, authFetch, refreshAccessToken, logout } from '@yeying-community/web3-bs';
|
|
62
|
-
|
|
63
|
-
const provider = await getProvider();
|
|
64
|
-
const login = await loginWithChallenge({
|
|
65
|
-
provider,
|
|
66
|
-
baseUrl: 'http://localhost:3203/api/v1/public/auth',
|
|
67
|
-
storeToken: false,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const profile = await authFetch('http://localhost:3203/api/v1/public/profile', { method: 'GET' }, {
|
|
71
|
-
baseUrl: 'http://localhost:3203/api/v1/public/auth',
|
|
72
|
-
storeToken: false,
|
|
73
|
-
});
|
|
74
|
-
console.log(await profile.json());
|
|
75
|
-
|
|
76
|
-
await refreshAccessToken({ baseUrl: 'http://localhost:3203/api/v1/public/auth', storeToken: false });
|
|
77
|
-
await logout({ baseUrl: 'http://localhost:3203/api/v1/public/auth', storeToken: false });
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## SIWE 与 UCAN 完整登录流程
|
|
81
|
-
|
|
82
|
-
### SIWE vs UCAN 对比(token 来源 / 有效期 / 续期方式)
|
|
83
|
-
|
|
84
|
-
| 维度 | SIWE 登录(JWT) | UCAN 授权 |
|
|
85
|
-
| --- | --- | --- |
|
|
86
|
-
| Token 来源 | 后端签发 access token + refresh cookie | 前端生成 UCAN(Root/Invocation),后端只校验 |
|
|
87
|
-
| 有效期 | access/refresh 由后端配置 | Invocation 默认 5 分钟,Root 默认 24 小时(可配置) |
|
|
88
|
-
| 续期方式 | `POST /auth/refresh`(refresh cookie) | 重新生成 Invocation;Root 过期则重新生成 Root + Invocation |
|
|
89
|
-
|
|
90
|
-
### SIWE-only(单后端)
|
|
91
|
-
1. 前端检测钱包 Provider(`getProvider` / `requestAccounts`)。
|
|
92
|
-
2. 请求后端 challenge:`POST /api/v1/public/auth/challenge`。
|
|
93
|
-
3. 钱包签名 challenge(`personal_sign`)。
|
|
94
|
-
4. 提交签名到后端:`POST /api/v1/public/auth/verify`,获取 access token + refresh cookie。
|
|
95
|
-
5. 访问业务接口:`Authorization: Bearer <access-token>`(`authFetch` 会自动附带)。
|
|
96
|
-
6. access 过期时刷新:`POST /api/v1/public/auth/refresh`(依赖 httpOnly refresh cookie)。
|
|
97
|
-
7. 退出登录:`POST /api/v1/public/auth/logout`。
|
|
98
|
-
|
|
99
|
-
Mermaid:
|
|
100
|
-
|
|
101
|
-
```mermaid
|
|
102
|
-
sequenceDiagram
|
|
103
|
-
actor 用户
|
|
104
|
-
participant FE as Dapp 前端
|
|
105
|
-
participant WP as 钱包 Provider
|
|
106
|
-
participant BE as 认证后端
|
|
107
|
-
|
|
108
|
-
用户 ->> FE: 打开 Dapp / 点击登录
|
|
109
|
-
FE ->> WP: 获取 Provider / 请求账户
|
|
110
|
-
FE ->> BE: POST /auth/challenge { address }
|
|
111
|
-
BE -->> FE: 返回 challenge, nonce
|
|
112
|
-
FE ->> WP: 签名 challenge
|
|
113
|
-
WP -->> FE: 返回 signature
|
|
114
|
-
FE ->> BE: POST /auth/verify { address, signature }
|
|
115
|
-
BE -->> FE: 返回 access token + refresh cookie
|
|
116
|
-
FE ->> BE: GET /public/profile (Authorization: Bearer access)
|
|
117
|
-
BE -->> FE: 返回业务数据
|
|
118
|
-
FE ->> BE: POST /auth/refresh (refresh cookie)
|
|
119
|
-
BE -->> FE: 返回新 access token
|
|
120
|
-
FE ->> BE: POST /auth/logout
|
|
121
|
-
BE -->> FE: 登出成功
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### UCAN(多后端 / Delegation)
|
|
125
|
-
1. 前端检测钱包 Provider(`getProvider` / `requestAccounts`)。
|
|
126
|
-
2. 由钱包生成 UCAN Session Key:`createUcanSession`。
|
|
127
|
-
3. 用 SIWE 作为桥梁生成 Root UCAN(包含能力 `[{ resource, action }]`):`createRootUcan` 或 `getOrCreateUcanRoot`。
|
|
128
|
-
4. 针对每个后端生成 Invocation UCAN:`createInvocationUcan({ issuer: session, audience, capabilities, proofs: [root] })`。
|
|
129
|
-
5. 调用后端业务接口:`Authorization: Bearer <UCAN>`(可用 `authUcanFetch`)。
|
|
130
|
-
6. 如需可转授权,先生成 Delegation UCAN:`createDelegationUcan`,由被委任方再生成 Invocation UCAN。
|
|
131
|
-
7. Root/Invocation 过期时重新生成(或由 `initWebDavStorage` 自动处理)。
|
|
132
|
-
提示:Root 过期后前端应重新授权并重试(重新生成 Root + Invocation)。
|
|
133
|
-
|
|
134
|
-
推荐重试逻辑示例:
|
|
135
|
-
```ts
|
|
136
|
-
import { getOrCreateUcanRoot, createInvocationUcan } from '@yeying-community/web3-bs';
|
|
137
|
-
|
|
138
|
-
async function callWithUcanRetry(target, caps, provider, session) {
|
|
139
|
-
const root = await getOrCreateUcanRoot({ provider, session, capabilities: caps });
|
|
140
|
-
let token = await createInvocationUcan({
|
|
141
|
-
issuer: session,
|
|
142
|
-
audience: target.aud,
|
|
143
|
-
capabilities: caps,
|
|
144
|
-
proofs: [root],
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
let res = await fetch(target.url, {
|
|
148
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
149
|
-
});
|
|
150
|
-
if (res.status !== 401) return res;
|
|
151
|
-
|
|
152
|
-
// Root 过期:重新生成 Root 并重试一次
|
|
153
|
-
const newRoot = await getOrCreateUcanRoot({ provider, session, capabilities: caps, expiresInMs: 24 * 60 * 60 * 1000 });
|
|
154
|
-
token = await createInvocationUcan({
|
|
155
|
-
issuer: session,
|
|
156
|
-
audience: target.aud,
|
|
157
|
-
capabilities: caps,
|
|
158
|
-
proofs: [newRoot],
|
|
159
|
-
});
|
|
160
|
-
return await fetch(target.url, {
|
|
161
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
Mermaid:
|
|
167
|
-
|
|
168
|
-
```mermaid
|
|
169
|
-
sequenceDiagram
|
|
170
|
-
actor 用户
|
|
171
|
-
participant FE as Dapp 前端
|
|
172
|
-
participant WP as 钱包 Provider
|
|
173
|
-
participant BA as 后端 A
|
|
174
|
-
participant BB as 后端 B
|
|
175
|
-
|
|
176
|
-
用户 ->> FE: 打开 Dapp / 初始化 UCAN
|
|
177
|
-
FE ->> WP: createUcanSession
|
|
178
|
-
WP -->> FE: 返回 UCAN session key (did, signer)
|
|
179
|
-
FE ->> WP: createRootUcan (SIWE 桥接)
|
|
180
|
-
WP -->> FE: 返回 Root UCAN (proof)
|
|
181
|
-
|
|
182
|
-
FE ->> WP: createInvocationUcan(audience=后端 A)
|
|
183
|
-
WP -->> FE: 返回 Invocation UCAN (A)
|
|
184
|
-
FE ->> BA: GET /public/profile (Authorization: Bearer UCAN-A)
|
|
185
|
-
BA -->> FE: 返回响应
|
|
186
|
-
|
|
187
|
-
FE ->> WP: createInvocationUcan(audience=后端 B)
|
|
188
|
-
WP -->> FE: 返回 Invocation UCAN (B)
|
|
189
|
-
FE ->> BB: GET /public/profile (Authorization: Bearer UCAN-B)
|
|
190
|
-
BB -->> FE: 返回响应
|
|
191
|
-
|
|
192
|
-
Note over FE,WP: 可选委任链\ncreateDelegationUcan -> 转交 -> createInvocationUcan
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## 钱包交互 API
|
|
196
|
-
|
|
197
|
-
### Provider 发现
|
|
198
|
-
- `getProvider(options?)`
|
|
199
|
-
- 自动监听 `eip6963:announceProvider`
|
|
200
|
-
- 默认优先 YeYing(`isYeYing` 或 `rdns: io.github.yeying`)
|
|
201
|
-
|
|
202
|
-
### 核心方法
|
|
203
|
-
- `requestAccounts({ provider? })`
|
|
204
|
-
- `getAccounts(provider?)`
|
|
205
|
-
- `getChainId(provider?)`
|
|
206
|
-
- `getBalance(provider?, address?, blockTag?)`
|
|
207
|
-
- `signMessage({ provider?, message, address?, method? })`
|
|
208
|
-
- `method` 默认 `personal_sign`
|
|
209
|
-
|
|
210
|
-
### 事件
|
|
211
|
-
- `onAccountsChanged(provider, handler)`
|
|
212
|
-
- `onChainChanged(provider, handler)`
|
|
213
|
-
|
|
214
|
-
## 后端交互 API(推荐标准)
|
|
215
|
-
OpenAPI 规范:`docs/openapi.yaml`
|
|
216
|
-
|
|
217
|
-
### 响应封装(严格)
|
|
218
|
-
所有响应必须使用以下封装结构:
|
|
219
|
-
|
|
220
|
-
```json
|
|
221
|
-
{
|
|
222
|
-
"code": 0,
|
|
223
|
-
"message": "ok",
|
|
224
|
-
"data": { "...": "..." },
|
|
225
|
-
"timestamp": 1730000000000
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
- `code = 0` 表示成功
|
|
230
|
-
- `code != 0` 表示失败;`data` 应为 `null`
|
|
231
|
-
|
|
232
|
-
### 1) 获取 Challenge
|
|
233
|
-
|
|
234
|
-
`POST /api/v1/public/auth/challenge`
|
|
235
|
-
|
|
236
|
-
请求
|
|
237
|
-
```json
|
|
238
|
-
{ "address": "0xabc123..." }
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
响应
|
|
242
|
-
```json
|
|
243
|
-
{
|
|
244
|
-
"code": 0,
|
|
245
|
-
"message": "ok",
|
|
246
|
-
"data": {
|
|
247
|
-
"address": "0xabc123...",
|
|
248
|
-
"challenge": "Sign to login...",
|
|
249
|
-
"nonce": "random",
|
|
250
|
-
"issuedAt": 1730000000000,
|
|
251
|
-
"expiresAt": 1730000300000
|
|
252
|
-
},
|
|
253
|
-
"timestamp": 1730000000000
|
|
254
|
-
}
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
### 2) 验证签名
|
|
258
|
-
|
|
259
|
-
`POST /api/v1/public/auth/verify`
|
|
260
|
-
|
|
261
|
-
请求
|
|
262
|
-
```json
|
|
263
|
-
{ "address": "0xabc123...", "signature": "0x..." }
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
响应
|
|
267
|
-
```json
|
|
268
|
-
{
|
|
269
|
-
"code": 0,
|
|
270
|
-
"message": "ok",
|
|
271
|
-
"data": {
|
|
272
|
-
"address": "0xabc123...",
|
|
273
|
-
"token": "access-token",
|
|
274
|
-
"expiresAt": 1730086400000,
|
|
275
|
-
"refreshExpiresAt": 1730686400000
|
|
276
|
-
},
|
|
277
|
-
"timestamp": 1730000000000
|
|
278
|
-
}
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
说明
|
|
282
|
-
- `verify` 应设置 httpOnly 的 `refresh_token` Cookie(用于刷新 access token)。
|
|
283
|
-
- 访问受保护接口时,前端使用 `Authorization: Bearer <access-token>`。
|
|
284
|
-
|
|
285
|
-
### 3) 刷新 Access Token
|
|
286
|
-
|
|
287
|
-
`POST /api/v1/public/auth/refresh`
|
|
288
|
-
|
|
289
|
-
请求
|
|
290
|
-
- 依赖 httpOnly `refresh_token` Cookie
|
|
291
|
-
|
|
292
|
-
响应
|
|
293
|
-
```json
|
|
294
|
-
{
|
|
295
|
-
"code": 0,
|
|
296
|
-
"message": "ok",
|
|
297
|
-
"data": {
|
|
298
|
-
"address": "0xabc123...",
|
|
299
|
-
"token": "new-access-token",
|
|
300
|
-
"expiresAt": 1730086400000,
|
|
301
|
-
"refreshExpiresAt": 1730686400000
|
|
302
|
-
},
|
|
303
|
-
"timestamp": 1730000000000
|
|
304
|
-
}
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
### 4) 退出登录
|
|
308
|
-
|
|
309
|
-
`POST /api/v1/public/auth/logout`
|
|
310
|
-
|
|
311
|
-
响应
|
|
312
|
-
```json
|
|
313
|
-
{
|
|
314
|
-
"code": 0,
|
|
315
|
-
"message": "ok",
|
|
316
|
-
"data": { "logout": true },
|
|
317
|
-
"timestamp": 1730000000000
|
|
318
|
-
}
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### SDK 绑定
|
|
322
|
-
- `loginWithChallenge` 会从 `data.challenge` 读取 challenge,从 `data.token` 读取 token。
|
|
323
|
-
- `refreshAccessToken` 调用 `/refresh` 并更新 access token(默认 `credentials: 'include'`)。
|
|
324
|
-
- `authFetch` 会自动携带 access token,遇到 401 会尝试刷新再重试一次。
|
|
325
|
-
- `logout` 会清理刷新 Cookie 并清空本地 access token(若设置 `storeToken`)。
|
|
18
|
+
更多集成与流程说明见 `docs/sdk-design.md`。
|
|
326
19
|
|
|
327
20
|
## 示例
|
|
328
21
|
|
|
@@ -333,133 +26,49 @@ OpenAPI 规范:`docs/openapi.yaml`
|
|
|
333
26
|
- Backend server (Python): `examples/backend/python/app.py`
|
|
334
27
|
- Backend server (Java): `examples/backend/java/src/main/java/com/yeying/demo/AuthServer.java`
|
|
335
28
|
|
|
336
|
-
##
|
|
29
|
+
## 本地验证
|
|
337
30
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
31
|
+
1. 构建 SDK:`npm run build`
|
|
32
|
+
2. 启动后端:`scripts/backend.sh start node`(将 `node` 替换为 `go` / `python` / `java`)
|
|
33
|
+
3. 启动前端:`python3 -m http.server 8001 --bind 127.0.0.1`
|
|
34
|
+
4. 访问:`http://127.0.0.1:8001/examples/frontend/dapp.html`
|
|
35
|
+
5. 确保安装 YeYing 钱包扩展插件
|
|
36
|
+
6. 点击:`Detect Provider` → `Connect Wallet` → `Login`
|
|
341
37
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
- `createDelegationUcan(options)`
|
|
346
|
-
- `createInvocationUcan(options)`
|
|
347
|
-
- `authUcanFetch(url, init?, options?)`
|
|
38
|
+
提示:如果前端来自其他域名,请设置
|
|
39
|
+
`COOKIE_SAMESITE=none` 且 `COOKIE_SECURE=true` 并使用 HTTPS,
|
|
40
|
+
以便 `refresh_token` Cookie 能随 `credentials: 'include'` 发送。
|
|
348
41
|
|
|
349
|
-
|
|
350
|
-
```ts
|
|
351
|
-
const session = await createUcanSession();
|
|
352
|
-
const root = await createRootUcan({
|
|
353
|
-
provider,
|
|
354
|
-
session,
|
|
355
|
-
capabilities: [{ resource: 'profile', action: 'read' }],
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
const ucan = await createInvocationUcan({
|
|
359
|
-
issuer: session,
|
|
360
|
-
audience: 'did:web:localhost:3203',
|
|
361
|
-
capabilities: [{ resource: 'profile', action: 'read' }],
|
|
362
|
-
proofs: [root],
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
const res = await authUcanFetch('http://localhost:3203/api/v1/public/profile', { method: 'GET' }, { ucan });
|
|
366
|
-
console.log(await res.json());
|
|
367
|
-
```
|
|
42
|
+
## 后端脚本用法
|
|
368
43
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
- `UCAN_RESOURCE`:资源(默认 `profile`)
|
|
372
|
-
- `UCAN_ACTION`:动作(默认 `read`)
|
|
44
|
+
通用格式:
|
|
45
|
+
`./scripts/backend.sh <start|stop|restart|status> <node|go|python|java|all> [--setup] [--no-stop]`
|
|
373
46
|
|
|
374
|
-
|
|
47
|
+
示例:
|
|
48
|
+
```bash
|
|
49
|
+
# 启动 Node 后端
|
|
50
|
+
./scripts/backend.sh start node
|
|
375
51
|
|
|
376
|
-
|
|
52
|
+
# 启动 Go 后端并安装依赖/构建
|
|
53
|
+
./scripts/backend.sh start go --setup
|
|
377
54
|
|
|
378
|
-
|
|
55
|
+
# 查看全部后端状态
|
|
56
|
+
./scripts/backend.sh status all
|
|
379
57
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
- `ensureDirectory(path)`:递归创建目录(存在时自动跳过)
|
|
58
|
+
# 重启 Python 后端
|
|
59
|
+
./scripts/backend.sh restart python
|
|
383
60
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
import { createWebDavClient, initWebDavStorage, loginWithChallenge } from '@yeying-community/web3-bs';
|
|
387
|
-
|
|
388
|
-
const login = await loginWithChallenge({
|
|
389
|
-
provider,
|
|
390
|
-
baseUrl: 'http://localhost:6065/api/v1/public/auth',
|
|
391
|
-
storeToken: false,
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
const client = createWebDavClient({
|
|
395
|
-
baseUrl: 'http://localhost:6065',
|
|
396
|
-
token: login.token,
|
|
397
|
-
prefix: '/',
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
const listingXml = await client.listDirectory('/');
|
|
401
|
-
await client.upload('/docs/hello.txt', 'Hello WebDAV');
|
|
402
|
-
const content = await client.downloadText('/docs/hello.txt');
|
|
403
|
-
|
|
404
|
-
// UCAN 登录 + 自动创建应用目录(推荐)
|
|
405
|
-
const storage = await initWebDavStorage({
|
|
406
|
-
baseUrl: 'http://localhost:6065',
|
|
407
|
-
audience: 'did:web:localhost:6065',
|
|
408
|
-
appId: 'my-dapp',
|
|
409
|
-
capabilities: [{ resource: 'profile', action: 'read' }],
|
|
410
|
-
});
|
|
411
|
-
await storage.client.upload(`${storage.appDir}/hello.txt`, 'Hello WebDAV');
|
|
412
|
-
```
|
|
61
|
+
# 停止所有后端
|
|
62
|
+
./scripts/backend.sh stop all
|
|
413
63
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
- `upload(path, content, contentType?)`
|
|
417
|
-
- `download(path)` / `downloadText(path)` / `downloadArrayBuffer(path)`
|
|
418
|
-
- `createDirectory(path)`(MKCOL)
|
|
419
|
-
- `ensureDirectory(path)`(递归 MKCOL,已存在会跳过)
|
|
420
|
-
- `remove(path)`(DELETE)
|
|
421
|
-
- `move(path, destination, overwrite?)`
|
|
422
|
-
- `copy(path, destination, overwrite?)`
|
|
423
|
-
- `getQuota()` / `listRecycle()` / `recoverRecycle(hash)` / `deleteRecycle(hash)` / `clearRecycle()`
|
|
424
|
-
|
|
425
|
-
提示:`capabilities` 需与 WebDAV 后端 UCAN 策略一致;`appId` 默认映射为 `/apps/<appId>`。
|
|
426
|
-
|
|
427
|
-
## Dapp 快速接入(推荐)
|
|
428
|
-
|
|
429
|
-
一次完成 SIWE 登录 + UCAN WebDAV 初始化 + 创建应用目录:
|
|
430
|
-
|
|
431
|
-
```ts
|
|
432
|
-
import { initDappSession } from '@yeying-community/web3-bs';
|
|
433
|
-
|
|
434
|
-
const session = await initDappSession({
|
|
435
|
-
appAuth: {
|
|
436
|
-
baseUrl: 'http://localhost:3203/api/v1/public/auth',
|
|
437
|
-
storeToken: false,
|
|
438
|
-
},
|
|
439
|
-
webdav: {
|
|
440
|
-
baseUrl: 'http://localhost:6065',
|
|
441
|
-
audience: 'did:web:localhost:6065',
|
|
442
|
-
appId: 'my-dapp',
|
|
443
|
-
capabilities: [{ resource: 'profile', action: 'read' }],
|
|
444
|
-
},
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
const webdav = session.webdavClient;
|
|
448
|
-
await webdav?.upload(`${session.webdavAppDir}/hello.txt`, 'Hello WebDAV');
|
|
64
|
+
# 同时启动多个后端(不停止已运行的服务)
|
|
65
|
+
./scripts/backend.sh start all --no-stop
|
|
449
66
|
```
|
|
450
67
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
3. 启动前端:`python3 -m http.server 8001`
|
|
456
|
-
4. 访问:`http://localhost:8001/examples/frontend/dapp.html`
|
|
457
|
-
5. 确保安装 YeYing 钱包扩展插件
|
|
458
|
-
6. 点击:`Detect Provider` → `Connect Wallet` → `Login`
|
|
459
|
-
|
|
460
|
-
提示:如果前端来自其他域名,请设置
|
|
461
|
-
`COOKIE_SAMESITE=none` 且 `COOKIE_SECURE=true` 并使用 HTTPS,
|
|
462
|
-
以便 `refresh_token` Cookie 能随 `credentials: 'include'` 发送。
|
|
68
|
+
说明:
|
|
69
|
+
- 日志位于 `.tmp/backend-logs`
|
|
70
|
+
- 进程 PID 位于 `.tmp/backend-pids`
|
|
71
|
+
- 可通过环境变量传递端口/秘钥/TTL 等配置(如 `PORT`、`JWT_SECRET` 等)
|
|
463
72
|
|
|
464
73
|
## 多后端联调(不同端口)
|
|
465
74
|
|
|
@@ -477,8 +86,8 @@ await webdav?.upload(`${session.webdavAppDir}/hello.txt`, 'Hello WebDAV');
|
|
|
477
86
|
- Python `3204`
|
|
478
87
|
|
|
479
88
|
前端调用不同端口的后端时:
|
|
480
|
-
- 将前端 Origin 加入 `CORS_ORIGINS`(例如 `http://
|
|
481
|
-
- UCAN 调用的 `audience` 与后端 `UCAN_AUD` 一致(如 `did:web:
|
|
89
|
+
- 将前端 Origin 加入 `CORS_ORIGINS`(例如 `http://127.0.0.1:3203`)
|
|
90
|
+
- UCAN 调用的 `audience` 与后端 `UCAN_AUD` 一致(如 `did:web:127.0.0.1:3202`)
|
|
482
91
|
|
|
483
92
|
提示:`examples/frontend/dapp.html` 已内置多后端列表,可在一次 UCAN 授权后依次调用多个服务。
|
|
484
93
|
|
|
@@ -486,4 +95,27 @@ await webdav?.upload(`${session.webdavAppDir}/hello.txt`, 'Hello WebDAV');
|
|
|
486
95
|
|
|
487
96
|
### 刷新token失败
|
|
488
97
|
|
|
489
|
-
清理旧 Cookie 后重新登录:在浏览器 DevTools → Application → Cookies → http://
|
|
98
|
+
清理旧 Cookie 后重新登录:在浏览器 DevTools → Application → Cookies → http://127.0.0.1:8001 删除 refresh_token,再点 Login 后再点 Refresh Token。
|
|
99
|
+
|
|
100
|
+
### WebDAV 提示 invalid token
|
|
101
|
+
|
|
102
|
+
报错示例:`authentication failed` / `invalid token`(通常来自 WebDAV 服务端鉴权中间件)。
|
|
103
|
+
|
|
104
|
+
排查与解决:
|
|
105
|
+
- 确认 `baseUrl` 指向 WebDAV 服务(默认 `http://127.0.0.1:6065`),不要误用 320x 认证后端。
|
|
106
|
+
- `audience` 必须与 WebDAV 服务端 `UCAN_AUD` 一致(例如 `did:web:127.0.0.1:6065`)。
|
|
107
|
+
- `capabilities` 与服务端要求一致(`UCAN_RESOURCE` / `UCAN_ACTION`)。
|
|
108
|
+
- Token 过期或缓存异常时,清理 UCAN 会话并重新登录/生成 Root + Invocation。
|
|
109
|
+
|
|
110
|
+
### UCAN audience mismatch
|
|
111
|
+
|
|
112
|
+
日志示例:
|
|
113
|
+
```
|
|
114
|
+
[2026-01-31T05:19:44.478Z] UCAN profile failed { error: 'UCAN audience mismatch' }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
解决思路:
|
|
118
|
+
- 生成 UCAN 时的 `audience` 必须与目标后端的 `UCAN_AUD` 完全一致。
|
|
119
|
+
- 例如 Node 后端:`did:web:127.0.0.1:3203`
|
|
120
|
+
- 例如 WebDAV 服务:`did:web:127.0.0.1:6065`
|
|
121
|
+
- 多后端联调时,确保每个后端使用对应的 `audience` 生成 Invocation UCAN。
|
package/dist/auth/provider.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { Eip1193Provider, ProviderDiscoveryOptions, ProviderInfo, RequestAccountsOptions } from './types';
|
|
1
|
+
import { Eip1193Provider, ProviderDiscoveryOptions, ProviderInfo, RequestAccountsOptions, AccountSelection, PreferredAccountOptions, WatchAccountsOptions, AccountsChangedHandler } from './types';
|
|
2
2
|
export declare function isYeYingProvider(provider?: Eip1193Provider | null, info?: ProviderInfo): boolean;
|
|
3
3
|
export declare function getProvider(options?: ProviderDiscoveryOptions): Promise<Eip1193Provider | null>;
|
|
4
4
|
export declare function requireProvider(options?: ProviderDiscoveryOptions): Promise<Eip1193Provider>;
|
|
5
5
|
export declare function requestAccounts(options?: RequestAccountsOptions): Promise<string[]>;
|
|
6
6
|
export declare function getAccounts(provider?: Eip1193Provider): Promise<string[]>;
|
|
7
7
|
export declare function getChainId(provider?: Eip1193Provider): Promise<string | null>;
|
|
8
|
+
export declare function getPreferredAccount(options?: PreferredAccountOptions): Promise<AccountSelection>;
|
|
9
|
+
export declare function watchAccounts(provider: Eip1193Provider, handler: AccountsChangedHandler, options?: WatchAccountsOptions): () => void;
|
|
8
10
|
export declare function getBalance(provider?: Eip1193Provider, address?: string, blockTag?: string): Promise<string>;
|
|
9
11
|
export declare function onAccountsChanged(provider: Eip1193Provider, handler: (accounts: string[]) => void): () => void;
|
|
10
12
|
export declare function onChainChanged(provider: Eip1193Provider, handler: (chainId: string) => void): () => void;
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -34,6 +34,20 @@ export interface ProviderDiscoveryOptions {
|
|
|
34
34
|
export interface RequestAccountsOptions {
|
|
35
35
|
provider?: Eip1193Provider;
|
|
36
36
|
}
|
|
37
|
+
export type AccountSelection = {
|
|
38
|
+
account: string | null;
|
|
39
|
+
accounts: string[];
|
|
40
|
+
};
|
|
41
|
+
export interface PreferredAccountOptions extends RequestAccountsOptions {
|
|
42
|
+
storageKey?: string;
|
|
43
|
+
autoConnect?: boolean;
|
|
44
|
+
preferStored?: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface WatchAccountsOptions {
|
|
47
|
+
storageKey?: string;
|
|
48
|
+
preferStored?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export type AccountsChangedHandler = (payload: AccountSelection) => void;
|
|
37
51
|
export interface SignMessageOptions {
|
|
38
52
|
provider?: Eip1193Provider;
|
|
39
53
|
message: string;
|
package/dist/web3-bs.esm.js
CHANGED
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
const YEYING_RDNS = 'io.github.yeying';
|
|
2
2
|
const DEFAULT_TIMEOUT = 1000;
|
|
3
|
+
const DEFAULT_ACCOUNT_STORAGE_KEY = 'yeying:last_account';
|
|
3
4
|
function getWindowEthereum() {
|
|
4
5
|
if (typeof window === 'undefined')
|
|
5
6
|
return null;
|
|
6
7
|
return window.ethereum || null;
|
|
7
8
|
}
|
|
9
|
+
function readStoredAccount(storageKey) {
|
|
10
|
+
if (typeof localStorage === 'undefined')
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
return localStorage.getItem(storageKey);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function writeStoredAccount(storageKey, account) {
|
|
20
|
+
if (typeof localStorage === 'undefined')
|
|
21
|
+
return;
|
|
22
|
+
try {
|
|
23
|
+
if (account) {
|
|
24
|
+
localStorage.setItem(storageKey, account);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
localStorage.removeItem(storageKey);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// ignore storage errors
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function selectPreferredAccount(accounts, stored, preferStored) {
|
|
35
|
+
if (preferStored && stored && accounts.includes(stored)) {
|
|
36
|
+
return stored;
|
|
37
|
+
}
|
|
38
|
+
return accounts[0] || null;
|
|
39
|
+
}
|
|
8
40
|
function isYeYingProvider(provider, info) {
|
|
9
41
|
if (!provider)
|
|
10
42
|
return false;
|
|
@@ -110,6 +142,29 @@ async function getChainId(provider) {
|
|
|
110
142
|
const chainId = (await p.request({ method: 'eth_chainId' }));
|
|
111
143
|
return typeof chainId === 'string' ? chainId : null;
|
|
112
144
|
}
|
|
145
|
+
async function getPreferredAccount(options = {}) {
|
|
146
|
+
const provider = options.provider || (await requireProvider());
|
|
147
|
+
const storageKey = options.storageKey || DEFAULT_ACCOUNT_STORAGE_KEY;
|
|
148
|
+
const preferStored = options.preferStored !== false;
|
|
149
|
+
let accounts = await getAccounts(provider);
|
|
150
|
+
if (accounts.length === 0 && options.autoConnect) {
|
|
151
|
+
accounts = await requestAccounts({ provider });
|
|
152
|
+
}
|
|
153
|
+
const stored = readStoredAccount(storageKey);
|
|
154
|
+
const account = selectPreferredAccount(accounts, stored, preferStored);
|
|
155
|
+
writeStoredAccount(storageKey, account);
|
|
156
|
+
return { account, accounts };
|
|
157
|
+
}
|
|
158
|
+
function watchAccounts(provider, handler, options = {}) {
|
|
159
|
+
const storageKey = options.storageKey || DEFAULT_ACCOUNT_STORAGE_KEY;
|
|
160
|
+
const preferStored = options.preferStored !== false;
|
|
161
|
+
return onAccountsChanged(provider, (accounts) => {
|
|
162
|
+
const stored = readStoredAccount(storageKey);
|
|
163
|
+
const account = selectPreferredAccount(accounts, stored, preferStored);
|
|
164
|
+
writeStoredAccount(storageKey, account);
|
|
165
|
+
handler({ account, accounts });
|
|
166
|
+
});
|
|
167
|
+
}
|
|
113
168
|
async function getBalance(provider, address, blockTag = 'latest') {
|
|
114
169
|
const p = provider || (await requireProvider());
|
|
115
170
|
let target = address;
|
|
@@ -708,8 +763,8 @@ async function createRootUcan(options) {
|
|
|
708
763
|
const session = options.session || (await createUcanSession({ id: options.sessionId, provider }));
|
|
709
764
|
const address = await resolveAddress(provider, options.address);
|
|
710
765
|
const chainId = options.chainId || (await getChainId(provider)) || '1';
|
|
711
|
-
const domain = options.domain || (typeof window !== 'undefined' ? window.location.host : '
|
|
712
|
-
const uri = options.uri || (typeof window !== 'undefined' ? window.location.origin : 'http://
|
|
766
|
+
const domain = options.domain || (typeof window !== 'undefined' ? window.location.host : '127.0.0.1');
|
|
767
|
+
const uri = options.uri || (typeof window !== 'undefined' ? window.location.origin : 'http://127.0.0.1');
|
|
713
768
|
const nonce = options.nonce || randomNonce(8);
|
|
714
769
|
const exp = normalizeExpiry(undefined, options.expiresInMs ?? DEFAULT_SESSION_TTL);
|
|
715
770
|
const nbf = options.notBeforeMs;
|
|
@@ -1248,5 +1303,5 @@ async function initDappSession(options) {
|
|
|
1248
1303
|
return result;
|
|
1249
1304
|
}
|
|
1250
1305
|
|
|
1251
|
-
export { WebDavClient, authFetch, authUcanFetch, clearAccessToken, clearUcanSession, createDelegationUcan, createInvocationUcan, createRootUcan, createUcanSession, createWebDavClient, getAccessToken, getAccounts, getBalance, getChainId, getOrCreateUcanRoot, getProvider, getStoredUcanRoot, getUcanSession, initDappSession, initWebDavStorage, isYeYingProvider, loginWithChallenge, logout, onAccountsChanged, onChainChanged, refreshAccessToken, requestAccounts, requireProvider, setAccessToken, signMessage, storeUcanRoot };
|
|
1306
|
+
export { WebDavClient, authFetch, authUcanFetch, clearAccessToken, clearUcanSession, createDelegationUcan, createInvocationUcan, createRootUcan, createUcanSession, createWebDavClient, getAccessToken, getAccounts, getBalance, getChainId, getOrCreateUcanRoot, getPreferredAccount, getProvider, getStoredUcanRoot, getUcanSession, initDappSession, initWebDavStorage, isYeYingProvider, loginWithChallenge, logout, onAccountsChanged, onChainChanged, refreshAccessToken, requestAccounts, requireProvider, setAccessToken, signMessage, storeUcanRoot, watchAccounts };
|
|
1252
1307
|
//# sourceMappingURL=web3-bs.esm.js.map
|