@yeying-community/web3-bs 1.0.2 → 1.0.3
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 +229 -5
- package/dist/auth/ucan.d.ts +1 -0
- package/dist/dapp.d.ts +48 -0
- package/dist/index.d.ts +1 -0
- package/dist/storage/webdav.d.ts +1 -0
- package/dist/web3-bs.esm.js +242 -2
- package/dist/web3-bs.esm.js.map +1 -1
- package/dist/web3-bs.umd.js +244 -1
- package/dist/web3-bs.umd.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,189 @@
|
|
|
9
9
|
npm install @yeying-community/web3-bs
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
## Dapp 集成(浏览器)
|
|
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
|
+
|
|
12
195
|
## 钱包交互 API
|
|
13
196
|
|
|
14
197
|
### Provider 发现
|
|
@@ -179,7 +362,7 @@ const ucan = await createInvocationUcan({
|
|
|
179
362
|
proofs: [root],
|
|
180
363
|
});
|
|
181
364
|
|
|
182
|
-
const res = await authUcanFetch('http://localhost:3203/api/v1/
|
|
365
|
+
const res = await authUcanFetch('http://localhost:3203/api/v1/public/profile', { method: 'GET' }, { ucan });
|
|
183
366
|
console.log(await res.json());
|
|
184
367
|
```
|
|
185
368
|
|
|
@@ -194,9 +377,13 @@ console.log(await res.json());
|
|
|
194
377
|
|
|
195
378
|
提供基于 WebDAV 服务的文件操作封装(上传/下载/删除/目录等),适配 `webdav` 项目 API。
|
|
196
379
|
|
|
380
|
+
新增便捷能力:
|
|
381
|
+
- `initWebDavStorage`:自动生成/复用 UCAN Invocation Token,并可在首次使用时创建应用目录
|
|
382
|
+
- `ensureDirectory(path)`:递归创建目录(存在时自动跳过)
|
|
383
|
+
|
|
197
384
|
示例:
|
|
198
385
|
```ts
|
|
199
|
-
import { createWebDavClient, loginWithChallenge } from '@yeying-community/web3-bs';
|
|
386
|
+
import { createWebDavClient, initWebDavStorage, loginWithChallenge } from '@yeying-community/web3-bs';
|
|
200
387
|
|
|
201
388
|
const login = await loginWithChallenge({
|
|
202
389
|
provider,
|
|
@@ -213,6 +400,15 @@ const client = createWebDavClient({
|
|
|
213
400
|
const listingXml = await client.listDirectory('/');
|
|
214
401
|
await client.upload('/docs/hello.txt', 'Hello WebDAV');
|
|
215
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');
|
|
216
412
|
```
|
|
217
413
|
|
|
218
414
|
常用方法:
|
|
@@ -220,18 +416,46 @@ const content = await client.downloadText('/docs/hello.txt');
|
|
|
220
416
|
- `upload(path, content, contentType?)`
|
|
221
417
|
- `download(path)` / `downloadText(path)` / `downloadArrayBuffer(path)`
|
|
222
418
|
- `createDirectory(path)`(MKCOL)
|
|
419
|
+
- `ensureDirectory(path)`(递归 MKCOL,已存在会跳过)
|
|
223
420
|
- `remove(path)`(DELETE)
|
|
224
421
|
- `move(path, destination, overwrite?)`
|
|
225
422
|
- `copy(path, destination, overwrite?)`
|
|
226
423
|
- `getQuota()` / `listRecycle()` / `recoverRecycle(hash)` / `deleteRecycle(hash)` / `clearRecycle()`
|
|
227
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');
|
|
449
|
+
```
|
|
450
|
+
|
|
228
451
|
## 本地验证
|
|
229
452
|
|
|
230
453
|
1. 构建 SDK:`npm run build`
|
|
231
454
|
2. 启动后端:`node examples/backend/node/server.js`
|
|
232
|
-
3. 启动前端:`python3 -m http.server 8001
|
|
233
|
-
4.
|
|
234
|
-
5.
|
|
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`
|
|
235
459
|
|
|
236
460
|
提示:如果前端来自其他域名,请设置
|
|
237
461
|
`COOKIE_SAMESITE=none` 且 `COOKIE_SECURE=true` 并使用 HTTPS,
|
package/dist/auth/ucan.d.ts
CHANGED
|
@@ -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
package/dist/storage/webdav.d.ts
CHANGED
|
@@ -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>;
|
package/dist/web3-bs.esm.js
CHANGED
|
@@ -637,6 +637,25 @@ async function getStoredUcanRoot(id = DEFAULT_SESSION_ID) {
|
|
|
637
637
|
const record = await readSessionRecord(id);
|
|
638
638
|
return record?.root || null;
|
|
639
639
|
}
|
|
640
|
+
function capsEqual(a, b) {
|
|
641
|
+
return JSON.stringify(a || []) === JSON.stringify(b || []);
|
|
642
|
+
}
|
|
643
|
+
function isRootExpired(root, nowMs) {
|
|
644
|
+
return Boolean(root.exp && nowMs > root.exp);
|
|
645
|
+
}
|
|
646
|
+
async function getOrCreateUcanRoot(options) {
|
|
647
|
+
const provider = options.provider || (await requireProvider());
|
|
648
|
+
const session = options.session || (await createUcanSession({ id: options.sessionId, provider }));
|
|
649
|
+
const nowMs = Date.now();
|
|
650
|
+
const stored = await getStoredUcanRoot(session.id);
|
|
651
|
+
if (stored &&
|
|
652
|
+
(!stored.aud || stored.aud === session.did) &&
|
|
653
|
+
capsEqual(stored.cap, options.capabilities) &&
|
|
654
|
+
!isRootExpired(stored, nowMs)) {
|
|
655
|
+
return stored;
|
|
656
|
+
}
|
|
657
|
+
return await createRootUcan({ ...options, provider, session });
|
|
658
|
+
}
|
|
640
659
|
function buildUcanStatement(payload) {
|
|
641
660
|
return `UCAN-AUTH ${JSON.stringify(payload)}`;
|
|
642
661
|
}
|
|
@@ -661,7 +680,15 @@ function buildSiweMessage(params) {
|
|
|
661
680
|
async function resolveAddress(provider, address) {
|
|
662
681
|
if (address)
|
|
663
682
|
return address;
|
|
664
|
-
|
|
683
|
+
let accounts = await getAccounts(provider);
|
|
684
|
+
if (!accounts[0]) {
|
|
685
|
+
const requested = (await provider.request({
|
|
686
|
+
method: 'eth_requestAccounts',
|
|
687
|
+
}));
|
|
688
|
+
if (Array.isArray(requested)) {
|
|
689
|
+
accounts = requested;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
665
692
|
if (!accounts[0])
|
|
666
693
|
throw new Error('No account available');
|
|
667
694
|
return accounts[0];
|
|
@@ -925,6 +952,27 @@ class WebDavClient {
|
|
|
925
952
|
async createDirectory(path) {
|
|
926
953
|
return await this.request('MKCOL', path);
|
|
927
954
|
}
|
|
955
|
+
async ensureDirectory(path) {
|
|
956
|
+
if (!path || path === '/')
|
|
957
|
+
return;
|
|
958
|
+
const segments = path.split('/').filter(Boolean);
|
|
959
|
+
if (segments.length === 0)
|
|
960
|
+
return;
|
|
961
|
+
let current = '';
|
|
962
|
+
for (const segment of segments) {
|
|
963
|
+
current = `${current}/${segment}`;
|
|
964
|
+
const res = await this.fetcher(this.buildUrl(current), {
|
|
965
|
+
method: 'MKCOL',
|
|
966
|
+
headers: this.buildHeaders(),
|
|
967
|
+
credentials: this.credentials,
|
|
968
|
+
});
|
|
969
|
+
if (res.ok)
|
|
970
|
+
continue;
|
|
971
|
+
if (res.status === 405)
|
|
972
|
+
continue;
|
|
973
|
+
throw new Error(`WebDAV MKCOL ${current} failed: ${res.status} ${res.statusText}`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
928
976
|
async remove(path) {
|
|
929
977
|
return await this.request('DELETE', path);
|
|
930
978
|
}
|
|
@@ -1008,5 +1056,197 @@ function createWebDavClient(options) {
|
|
|
1008
1056
|
return new WebDavClient(options);
|
|
1009
1057
|
}
|
|
1010
1058
|
|
|
1011
|
-
|
|
1059
|
+
const tokenCache = new Map();
|
|
1060
|
+
const TOKEN_SKEW_MS = 5000;
|
|
1061
|
+
function normalizeAppDir(path) {
|
|
1062
|
+
const trimmed = path.trim();
|
|
1063
|
+
if (!trimmed)
|
|
1064
|
+
return '/';
|
|
1065
|
+
let next = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
1066
|
+
next = next.replace(/\/+$/, '');
|
|
1067
|
+
return next || '/';
|
|
1068
|
+
}
|
|
1069
|
+
function sanitizeAppId(appId) {
|
|
1070
|
+
return appId.trim().replace(/[^a-zA-Z0-9._-]/g, '-');
|
|
1071
|
+
}
|
|
1072
|
+
function resolveAppDir(options) {
|
|
1073
|
+
if (options.appDir) {
|
|
1074
|
+
return normalizeAppDir(options.appDir);
|
|
1075
|
+
}
|
|
1076
|
+
if (options.appId) {
|
|
1077
|
+
return normalizeAppDir(`/apps/${sanitizeAppId(options.appId)}`);
|
|
1078
|
+
}
|
|
1079
|
+
return undefined;
|
|
1080
|
+
}
|
|
1081
|
+
function buildCapsKey(caps) {
|
|
1082
|
+
return JSON.stringify(caps || []);
|
|
1083
|
+
}
|
|
1084
|
+
function buildTokenCacheKey(issuer, audience, caps) {
|
|
1085
|
+
return `${issuer.did}|${audience}|${buildCapsKey(caps)}`;
|
|
1086
|
+
}
|
|
1087
|
+
function isTokenValid(entry, nowMs) {
|
|
1088
|
+
if (!entry.exp)
|
|
1089
|
+
return false;
|
|
1090
|
+
if (entry.nbf && nowMs < entry.nbf)
|
|
1091
|
+
return false;
|
|
1092
|
+
return entry.exp - TOKEN_SKEW_MS > nowMs;
|
|
1093
|
+
}
|
|
1094
|
+
function decodeBase64Url(input) {
|
|
1095
|
+
if (!input)
|
|
1096
|
+
return null;
|
|
1097
|
+
const base64 = input.replace(/-/g, '+').replace(/_/g, '/');
|
|
1098
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
|
|
1099
|
+
try {
|
|
1100
|
+
if (typeof atob === 'function') {
|
|
1101
|
+
return atob(padded);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
// ignore
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
const nodeBuffer = globalThis.Buffer;
|
|
1109
|
+
if (nodeBuffer) {
|
|
1110
|
+
return nodeBuffer.from(padded, 'base64').toString('utf8');
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
catch {
|
|
1114
|
+
return null;
|
|
1115
|
+
}
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
function decodeUcanPayload(token) {
|
|
1119
|
+
const parts = token.split('.');
|
|
1120
|
+
if (parts.length < 2)
|
|
1121
|
+
return null;
|
|
1122
|
+
const decoded = decodeBase64Url(parts[1]);
|
|
1123
|
+
if (!decoded)
|
|
1124
|
+
return null;
|
|
1125
|
+
try {
|
|
1126
|
+
return JSON.parse(decoded);
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
async function getCachedInvocationToken(options) {
|
|
1133
|
+
const cacheKey = buildTokenCacheKey(options.issuer, options.audience, options.capabilities);
|
|
1134
|
+
const cached = tokenCache.get(cacheKey);
|
|
1135
|
+
const nowMs = Date.now();
|
|
1136
|
+
if (cached && isTokenValid(cached, nowMs)) {
|
|
1137
|
+
return cached.token;
|
|
1138
|
+
}
|
|
1139
|
+
const token = await createInvocationUcan({
|
|
1140
|
+
issuer: options.issuer,
|
|
1141
|
+
audience: options.audience,
|
|
1142
|
+
capabilities: options.capabilities,
|
|
1143
|
+
proofs: options.proofs,
|
|
1144
|
+
expiresInMs: options.expiresInMs,
|
|
1145
|
+
notBeforeMs: options.notBeforeMs,
|
|
1146
|
+
});
|
|
1147
|
+
const payload = decodeUcanPayload(token);
|
|
1148
|
+
if (payload && typeof payload.exp === 'number') {
|
|
1149
|
+
tokenCache.set(cacheKey, {
|
|
1150
|
+
token,
|
|
1151
|
+
exp: payload.exp,
|
|
1152
|
+
nbf: payload.nbf,
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
return token;
|
|
1156
|
+
}
|
|
1157
|
+
async function initWebDavStorage(options) {
|
|
1158
|
+
const caps = options.capabilities || options.root?.cap;
|
|
1159
|
+
if (!caps || caps.length === 0) {
|
|
1160
|
+
throw new Error('Missing UCAN capabilities for WebDAV');
|
|
1161
|
+
}
|
|
1162
|
+
const needsProvider = !options.session || !options.root;
|
|
1163
|
+
const provider = options.provider || (needsProvider ? await requireProvider() : undefined);
|
|
1164
|
+
const session = options.session ||
|
|
1165
|
+
(await createUcanSession({
|
|
1166
|
+
id: options.sessionId,
|
|
1167
|
+
provider,
|
|
1168
|
+
}));
|
|
1169
|
+
const nowMs = Date.now();
|
|
1170
|
+
let root = options.root;
|
|
1171
|
+
if (root && root.aud && root.aud !== session.did) {
|
|
1172
|
+
root = undefined;
|
|
1173
|
+
}
|
|
1174
|
+
if (root && buildCapsKey(root.cap) !== buildCapsKey(caps)) {
|
|
1175
|
+
root = undefined;
|
|
1176
|
+
}
|
|
1177
|
+
if (root && root.exp && nowMs > root.exp) {
|
|
1178
|
+
root = undefined;
|
|
1179
|
+
}
|
|
1180
|
+
if (!root) {
|
|
1181
|
+
root = await getOrCreateUcanRoot({
|
|
1182
|
+
provider: provider || (await requireProvider()),
|
|
1183
|
+
session,
|
|
1184
|
+
capabilities: caps,
|
|
1185
|
+
expiresInMs: options.rootExpiresInMs,
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
const invocationCaps = options.invocationCapabilities || caps;
|
|
1189
|
+
const token = await getCachedInvocationToken({
|
|
1190
|
+
issuer: session,
|
|
1191
|
+
audience: options.audience,
|
|
1192
|
+
capabilities: invocationCaps,
|
|
1193
|
+
proofs: [root],
|
|
1194
|
+
expiresInMs: options.invocationExpiresInMs,
|
|
1195
|
+
notBeforeMs: options.notBeforeMs,
|
|
1196
|
+
});
|
|
1197
|
+
const client = createWebDavClient({
|
|
1198
|
+
baseUrl: options.baseUrl,
|
|
1199
|
+
prefix: options.prefix,
|
|
1200
|
+
token,
|
|
1201
|
+
fetcher: options.fetcher,
|
|
1202
|
+
credentials: options.credentials,
|
|
1203
|
+
});
|
|
1204
|
+
const appDir = resolveAppDir(options);
|
|
1205
|
+
if (appDir && options.ensureAppDir !== false) {
|
|
1206
|
+
await client.ensureDirectory(appDir);
|
|
1207
|
+
}
|
|
1208
|
+
return {
|
|
1209
|
+
client,
|
|
1210
|
+
token,
|
|
1211
|
+
appDir,
|
|
1212
|
+
session,
|
|
1213
|
+
root,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
async function initDappSession(options) {
|
|
1217
|
+
if (!options.appAuth && !options.webdav) {
|
|
1218
|
+
throw new Error('No init options provided');
|
|
1219
|
+
}
|
|
1220
|
+
const provider = options.provider ||
|
|
1221
|
+
options.appAuth?.provider ||
|
|
1222
|
+
options.webdav?.provider ||
|
|
1223
|
+
(await requireProvider());
|
|
1224
|
+
const result = {
|
|
1225
|
+
provider,
|
|
1226
|
+
address: options.address,
|
|
1227
|
+
};
|
|
1228
|
+
if (options.appAuth) {
|
|
1229
|
+
const appLogin = await loginWithChallenge({
|
|
1230
|
+
...options.appAuth,
|
|
1231
|
+
provider: options.appAuth.provider || provider,
|
|
1232
|
+
address: options.appAuth.address || options.address,
|
|
1233
|
+
});
|
|
1234
|
+
result.appLogin = appLogin;
|
|
1235
|
+
result.address = appLogin.address;
|
|
1236
|
+
}
|
|
1237
|
+
if (options.webdav) {
|
|
1238
|
+
const webdav = await initWebDavStorage({
|
|
1239
|
+
...options.webdav,
|
|
1240
|
+
provider: options.webdav.provider || provider,
|
|
1241
|
+
});
|
|
1242
|
+
result.ucanSession = webdav.session;
|
|
1243
|
+
result.ucanRoot = webdav.root;
|
|
1244
|
+
result.webdavClient = webdav.client;
|
|
1245
|
+
result.webdavToken = webdav.token;
|
|
1246
|
+
result.webdavAppDir = webdav.appDir;
|
|
1247
|
+
}
|
|
1248
|
+
return result;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
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 };
|
|
1012
1252
|
//# sourceMappingURL=web3-bs.esm.js.map
|