@yeying-community/web3-bs 1.0.2 → 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 CHANGED
@@ -3,143 +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
- ## 钱包交互 API
13
-
14
- ### Provider 发现
15
- - `getProvider(options?)`
16
- - 自动监听 `eip6963:announceProvider`
17
- - 默认优先 YeYing(`isYeYing` 或 `rdns: io.github.yeying`)
18
-
19
- ### 核心方法
20
- - `requestAccounts({ provider? })`
21
- - `getAccounts(provider?)`
22
- - `getChainId(provider?)`
23
- - `getBalance(provider?, address?, blockTag?)`
24
- - `signMessage({ provider?, message, address?, method? })`
25
- - `method` 默认 `personal_sign`
26
-
27
- ### 事件
28
- - `onAccountsChanged(provider, handler)`
29
- - `onChainChanged(provider, handler)`
30
-
31
- ## 后端交互 API(推荐标准)
32
- OpenAPI 规范:`docs/openapi.yaml`
33
-
34
- ### 响应封装(严格)
35
- 所有响应必须使用以下封装结构:
36
-
37
- ```json
38
- {
39
- "code": 0,
40
- "message": "ok",
41
- "data": { "...": "..." },
42
- "timestamp": 1730000000000
43
- }
44
- ```
45
-
46
- - `code = 0` 表示成功
47
- - `code != 0` 表示失败;`data` 应为 `null`
48
-
49
- ### 1) 获取 Challenge
50
-
51
- `POST /api/v1/public/auth/challenge`
52
-
53
- 请求
54
- ```json
55
- { "address": "0xabc123..." }
56
- ```
57
-
58
- 响应
59
- ```json
60
- {
61
- "code": 0,
62
- "message": "ok",
63
- "data": {
64
- "address": "0xabc123...",
65
- "challenge": "Sign to login...",
66
- "nonce": "random",
67
- "issuedAt": 1730000000000,
68
- "expiresAt": 1730000300000
69
- },
70
- "timestamp": 1730000000000
71
- }
72
- ```
73
-
74
- ### 2) 验证签名
75
-
76
- `POST /api/v1/public/auth/verify`
77
-
78
- 请求
79
- ```json
80
- { "address": "0xabc123...", "signature": "0x..." }
81
- ```
82
-
83
- 响应
84
- ```json
85
- {
86
- "code": 0,
87
- "message": "ok",
88
- "data": {
89
- "address": "0xabc123...",
90
- "token": "access-token",
91
- "expiresAt": 1730086400000,
92
- "refreshExpiresAt": 1730686400000
93
- },
94
- "timestamp": 1730000000000
95
- }
96
- ```
97
-
98
- 说明
99
- - `verify` 应设置 httpOnly 的 `refresh_token` Cookie(用于刷新 access token)。
100
- - 访问受保护接口时,前端使用 `Authorization: Bearer <access-token>`。
101
-
102
- ### 3) 刷新 Access Token
103
-
104
- `POST /api/v1/public/auth/refresh`
105
-
106
- 请求
107
- - 依赖 httpOnly `refresh_token` Cookie
108
-
109
- 响应
110
- ```json
111
- {
112
- "code": 0,
113
- "message": "ok",
114
- "data": {
115
- "address": "0xabc123...",
116
- "token": "new-access-token",
117
- "expiresAt": 1730086400000,
118
- "refreshExpiresAt": 1730686400000
119
- },
120
- "timestamp": 1730000000000
121
- }
122
- ```
123
-
124
- ### 4) 退出登录
125
-
126
- `POST /api/v1/public/auth/logout`
127
-
128
- 响应
129
- ```json
130
- {
131
- "code": 0,
132
- "message": "ok",
133
- "data": { "logout": true },
134
- "timestamp": 1730000000000
135
- }
136
- ```
137
-
138
- ### SDK 绑定
139
- - `loginWithChallenge` 会从 `data.challenge` 读取 challenge,从 `data.token` 读取 token。
140
- - `refreshAccessToken` 调用 `/refresh` 并更新 access token(默认 `credentials: 'include'`)。
141
- - `authFetch` 会自动携带 access token,遇到 401 会尝试刷新再重试一次。
142
- - `logout` 会清理刷新 Cookie 并清空本地 access token(若设置 `storeToken`)。
18
+ 更多集成与流程说明见 `docs/sdk-design.md`。
143
19
 
144
20
  ## 示例
145
21
 
@@ -150,92 +26,49 @@ OpenAPI 规范:`docs/openapi.yaml`
150
26
  - Backend server (Python): `examples/backend/python/app.py`
151
27
  - Backend server (Java): `examples/backend/java/src/main/java/com/yeying/demo/AuthServer.java`
152
28
 
153
- ## UCAN 授权(SIWE Bridge)
154
-
155
- SDK 通过 YeYing 钱包生成 UCAN Session Key 并完成签名(由钱包后台隔离私钥)。
156
- Root UCAN 基于 SIWE 签名,
157
- 用于多后端统一鉴权。UCAN 以 `Authorization: Bearer <UCAN>` 发送,后端会验证 UCAN 证明链与能力。
29
+ ## 本地验证
158
30
 
159
- 新增 API:
160
- - `createUcanSession(options?)`
161
- - `createRootUcan(options)`
162
- - `createDelegationUcan(options)`
163
- - `createInvocationUcan(options)`
164
- - `authUcanFetch(url, init?, options?)`
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`
165
37
 
166
- 示例:
167
- ```ts
168
- const session = await createUcanSession();
169
- const root = await createRootUcan({
170
- provider,
171
- session,
172
- capabilities: [{ resource: 'profile', action: 'read' }],
173
- });
174
-
175
- const ucan = await createInvocationUcan({
176
- issuer: session,
177
- audience: 'did:web:localhost:3203',
178
- capabilities: [{ resource: 'profile', action: 'read' }],
179
- proofs: [root],
180
- });
181
-
182
- const res = await authUcanFetch('http://localhost:3203/api/v1/private/profile', { method: 'GET' }, { ucan });
183
- console.log(await res.json());
184
- ```
38
+ 提示:如果前端来自其他域名,请设置
39
+ `COOKIE_SAMESITE=none` 且 `COOKIE_SECURE=true` 并使用 HTTPS,
40
+ 以便 `refresh_token` Cookie 能随 `credentials: 'include'` 发送。
185
41
 
186
- 后端默认要求的能力为 `resource=profile`、`action=read`,可通过环境变量覆盖:
187
- - `UCAN_AUD`:服务 DID(默认 `did:web:localhost:3203`)
188
- - `UCAN_RESOURCE`:资源(默认 `profile`)
189
- - `UCAN_ACTION`:动作(默认 `read`)
42
+ ## 后端脚本用法
190
43
 
191
- 提示:如需可转授权,使用 `createDelegationUcan` 创建委任链,再用被委任的 Key 生成 Invocation UCAN。
44
+ 通用格式:
45
+ `./scripts/backend.sh <start|stop|restart|status> <node|go|python|java|all> [--setup] [--no-stop]`
192
46
 
193
- ## WebDAV 存储(Storage)
47
+ 示例:
48
+ ```bash
49
+ # 启动 Node 后端
50
+ ./scripts/backend.sh start node
194
51
 
195
- 提供基于 WebDAV 服务的文件操作封装(上传/下载/删除/目录等),适配 `webdav` 项目 API。
52
+ # 启动 Go 后端并安装依赖/构建
53
+ ./scripts/backend.sh start go --setup
196
54
 
197
- 示例:
198
- ```ts
199
- import { createWebDavClient, loginWithChallenge } from '@yeying-community/web3-bs';
200
-
201
- const login = await loginWithChallenge({
202
- provider,
203
- baseUrl: 'http://localhost:6065/api/v1/public/auth',
204
- storeToken: false,
205
- });
206
-
207
- const client = createWebDavClient({
208
- baseUrl: 'http://localhost:6065',
209
- token: login.token,
210
- prefix: '/',
211
- });
212
-
213
- const listingXml = await client.listDirectory('/');
214
- await client.upload('/docs/hello.txt', 'Hello WebDAV');
215
- const content = await client.downloadText('/docs/hello.txt');
216
- ```
55
+ # 查看全部后端状态
56
+ ./scripts/backend.sh status all
217
57
 
218
- 常用方法:
219
- - `listDirectory(path?, depth?)`(PROPFIND,返回 XML 文本)
220
- - `upload(path, content, contentType?)`
221
- - `download(path)` / `downloadText(path)` / `downloadArrayBuffer(path)`
222
- - `createDirectory(path)`(MKCOL)
223
- - `remove(path)`(DELETE)
224
- - `move(path, destination, overwrite?)`
225
- - `copy(path, destination, overwrite?)`
226
- - `getQuota()` / `listRecycle()` / `recoverRecycle(hash)` / `deleteRecycle(hash)` / `clearRecycle()`
58
+ # 重启 Python 后端
59
+ ./scripts/backend.sh restart python
227
60
 
228
- ## 本地验证
61
+ # 停止所有后端
62
+ ./scripts/backend.sh stop all
229
63
 
230
- 1. 构建 SDK:`npm run build`
231
- 2. 启动后端:`node examples/backend/node/server.js`
232
- 3. 启动前端:`python3 -m http.server 8001`,然后在浏览器输入访问地址:`http://localhost:8001/examples/frontend/dapp.html`
233
- 4. 确保安装 YeYing 钱包扩展插件
234
- 5. 点击:`Detect Provider` → `Connect Wallet` → `Login`
64
+ # 同时启动多个后端(不停止已运行的服务)
65
+ ./scripts/backend.sh start all --no-stop
66
+ ```
235
67
 
236
- 提示:如果前端来自其他域名,请设置
237
- `COOKIE_SAMESITE=none` `COOKIE_SECURE=true` 并使用 HTTPS,
238
- 以便 `refresh_token` Cookie 能随 `credentials: 'include'` 发送。
68
+ 说明:
69
+ - 日志位于 `.tmp/backend-logs`
70
+ - 进程 PID 位于 `.tmp/backend-pids`
71
+ - 可通过环境变量传递端口/秘钥/TTL 等配置(如 `PORT`、`JWT_SECRET` 等)
239
72
 
240
73
  ## 多后端联调(不同端口)
241
74
 
@@ -253,8 +86,8 @@ const content = await client.downloadText('/docs/hello.txt');
253
86
  - Python `3204`
254
87
 
255
88
  前端调用不同端口的后端时:
256
- - 将前端 Origin 加入 `CORS_ORIGINS`(例如 `http://localhost:3203`)
257
- - UCAN 调用的 `audience` 与后端 `UCAN_AUD` 一致(如 `did:web:localhost:3202`)
89
+ - 将前端 Origin 加入 `CORS_ORIGINS`(例如 `http://127.0.0.1:3203`)
90
+ - UCAN 调用的 `audience` 与后端 `UCAN_AUD` 一致(如 `did:web:127.0.0.1:3202`)
258
91
 
259
92
  提示:`examples/frontend/dapp.html` 已内置多后端列表,可在一次 UCAN 授权后依次调用多个服务。
260
93
 
@@ -262,4 +95,27 @@ const content = await client.downloadText('/docs/hello.txt');
262
95
 
263
96
  ### 刷新token失败
264
97
 
265
- 清理旧 Cookie 后重新登录:在浏览器 DevTools → Application → Cookies → http://localhost:8001 删除 refresh_token,再点 Login 后再点 Refresh Token。
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。
@@ -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;
@@ -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;
@@ -86,6 +86,7 @@ export declare function createUcanSession(options?: CreateUcanSessionOptions): P
86
86
  export declare function clearUcanSession(id?: string): Promise<void>;
87
87
  export declare function storeUcanRoot(root: UcanRootProof, id?: string): Promise<void>;
88
88
  export declare function getStoredUcanRoot(id?: string): Promise<UcanRootProof | null>;
89
+ export declare function getOrCreateUcanRoot(options: CreateRootUcanOptions): Promise<UcanRootProof>;
89
90
  export declare function createRootUcan(options: CreateRootUcanOptions): Promise<UcanRootProof>;
90
91
  export declare function createDelegationUcan(options: CreateUcanTokenOptions): Promise<string>;
91
92
  export declare function createInvocationUcan(options: CreateUcanTokenOptions): Promise<string>;
package/dist/dapp.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { loginWithChallenge } from './auth/siwe';
2
+ import { UcanCapability, UcanRootProof, UcanSessionKey } from './auth/ucan';
3
+ import { Eip1193Provider, LoginWithChallengeOptions } from './auth/types';
4
+ import { WebDavClient } from './storage/webdav';
5
+ export type InitWebDavStorageOptions = {
6
+ baseUrl: string;
7
+ audience: string;
8
+ prefix?: string;
9
+ appDir?: string;
10
+ appId?: string;
11
+ ensureAppDir?: boolean;
12
+ capabilities?: UcanCapability[];
13
+ invocationCapabilities?: UcanCapability[];
14
+ provider?: Eip1193Provider;
15
+ sessionId?: string;
16
+ session?: UcanSessionKey;
17
+ root?: UcanRootProof;
18
+ rootExpiresInMs?: number;
19
+ invocationExpiresInMs?: number;
20
+ notBeforeMs?: number;
21
+ fetcher?: typeof fetch;
22
+ credentials?: RequestCredentials;
23
+ };
24
+ export type InitWebDavStorageResult = {
25
+ client: WebDavClient;
26
+ token: string;
27
+ appDir?: string;
28
+ session: UcanSessionKey;
29
+ root: UcanRootProof;
30
+ };
31
+ export type InitDappSessionOptions = {
32
+ provider?: Eip1193Provider;
33
+ address?: string;
34
+ appAuth?: LoginWithChallengeOptions;
35
+ webdav?: InitWebDavStorageOptions;
36
+ };
37
+ export type InitDappSessionResult = {
38
+ provider: Eip1193Provider;
39
+ address?: string;
40
+ appLogin?: Awaited<ReturnType<typeof loginWithChallenge>>;
41
+ ucanSession?: UcanSessionKey;
42
+ ucanRoot?: UcanRootProof;
43
+ webdavClient?: WebDavClient;
44
+ webdavToken?: string;
45
+ webdavAppDir?: string;
46
+ };
47
+ export declare function initWebDavStorage(options: InitWebDavStorageOptions): Promise<InitWebDavStorageResult>;
48
+ export declare function initDappSession(options: InitDappSessionOptions): Promise<InitDappSessionResult>;
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './auth';
2
2
  export * from './storage';
3
+ export * from './dapp';
@@ -42,6 +42,7 @@ export declare class WebDavClient {
42
42
  downloadArrayBuffer(path: string): Promise<ArrayBuffer>;
43
43
  upload(path: string, content: BodyInit, contentType?: string): Promise<Response>;
44
44
  createDirectory(path: string): Promise<Response>;
45
+ ensureDirectory(path: string): Promise<void>;
45
46
  remove(path: string): Promise<Response>;
46
47
  move(path: string, destination: string, overwrite?: boolean): Promise<Response>;
47
48
  copy(path: string, destination: string, overwrite?: boolean): Promise<Response>;