@yeying-community/web3-bs 1.0.7 → 1.0.10

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
@@ -21,9 +21,13 @@ npm install @yeying-community/web3-bs
21
21
  ### 1) Provider 与账户
22
22
 
23
23
  - `getProvider` / `requireProvider`
24
+ - `watchProvider`
24
25
  - `requestAccounts` / `getAccounts` / `getPreferredAccount` / `watchAccounts`
25
26
  - `getChainId` / `getBalance`
26
27
  - `onAccountsChanged` / `onChainChanged`
28
+ - `classifyWalletError` / `isUserRejectedWalletAction` / `isWalletReconnectError`
29
+
30
+ `requestAccounts` 默认会复用同一 provider 上尚未完成的连接请求,避免用户重复点击时触发多个钱包授权弹窗。
27
31
 
28
32
  ### 2) SIWE + JWT
29
33
 
@@ -59,6 +63,7 @@ npm install @yeying-community/web3-bs
59
63
 
60
64
  - `createWebDavClient`
61
65
  - `initWebDavStorage`(自动生成/复用 WebDAV Invocation UCAN,可自动创建应用目录)
66
+ - `createShareLink` / `listShareLinks` / `revokeShareLink`(公开分享链接管理)
62
67
  - `initDappSession`(SIWE 登录 + WebDAV UCAN 初始化)
63
68
  - `deriveAppIdFromLocation` / `deriveAppIdFromHost`
64
69
 
@@ -97,6 +102,30 @@ const webdav = await initWebDavStorage({
97
102
  await webdav.client.upload(`${webdav.appDir}/hello.txt`, 'Hello WebDAV');
98
103
  ```
99
104
 
105
+ ### WebDAV 分享链接(warehouse)
106
+
107
+ ```ts
108
+ import { createWebDavClient } from '@yeying-community/web3-bs';
109
+
110
+ const client = createWebDavClient({
111
+ baseUrl: 'http://localhost:6065',
112
+ token: '<JWT_OR_UCAN>',
113
+ });
114
+
115
+ // expiresValue = 0 表示长期(不过期)分享链接
116
+ const share = await client.createShareLink({
117
+ path: '/apps/demo/hello.txt',
118
+ expiresValue: 0,
119
+ expiresUnit: 'day',
120
+ });
121
+
122
+ console.log(share.url);
123
+ ```
124
+
125
+ 分享链接权限边界(warehouse 当前实现):
126
+ - 使用 UCAN 且 capability 带 `app` scope(如 `app:all:<appId>`)时,`createShareLink` / `listShareLinks` 仅允许授权目录内的文件路径。
127
+ - 使用 JWT(或未携带 UCAN `app` scope)时,不会自动套用上述目录过滤,最终范围由后端鉴权策略决定。
128
+
100
129
  ## Demo
101
130
 
102
131
  - 前端 Demo(双 Tab:单后端 SIWE / 多后端 UCAN+WebDAV):
@@ -1,6 +1,12 @@
1
- import { Eip1193Provider, ProviderDiscoveryOptions, ProviderInfo, RequestAccountsOptions, AccountSelection, PreferredAccountOptions, WatchAccountsOptions, AccountsChangedHandler } from './types';
1
+ import { Eip1193Provider, ProviderDiscoveryOptions, ProviderInfo, RequestAccountsOptions, AccountSelection, PreferredAccountOptions, WatchAccountsOptions, AccountsChangedHandler, WatchProviderOptions, ProviderChangedHandler, WalletErrorInfo } 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
+ export declare function watchProvider(handler: ProviderChangedHandler, options?: WatchProviderOptions): () => void;
5
+ export declare function getWalletErrorMessage(error: unknown): string;
6
+ export declare function getWalletErrorCode(error: unknown): number | null;
7
+ export declare function classifyWalletError(error: unknown): WalletErrorInfo;
8
+ export declare function isUserRejectedWalletAction(error: unknown): boolean;
9
+ export declare function isWalletReconnectError(error: unknown): boolean;
4
10
  export declare function requireProvider(options?: ProviderDiscoveryOptions): Promise<Eip1193Provider>;
5
11
  export declare function requestAccounts(options?: RequestAccountsOptions): Promise<string[]>;
6
12
  export declare function getAccounts(provider?: Eip1193Provider): Promise<string[]>;
@@ -16,6 +16,8 @@ export interface Eip1193Provider {
16
16
  };
17
17
  isMetaMask?: boolean;
18
18
  isYeYing?: boolean;
19
+ providers?: Eip1193Provider[];
20
+ [key: string]: unknown;
19
21
  }
20
22
  export interface ProviderInfo {
21
23
  uuid?: string;
@@ -31,8 +33,23 @@ export interface ProviderDiscoveryOptions {
31
33
  timeoutMs?: number;
32
34
  preferYeYing?: boolean;
33
35
  }
36
+ export interface WatchProviderOptions extends ProviderDiscoveryOptions {
37
+ pollIntervalMs?: number;
38
+ maxPolls?: number;
39
+ }
40
+ export type ProviderChangedHandler = (payload: {
41
+ provider: Eip1193Provider | null;
42
+ present: boolean;
43
+ }) => void;
44
+ export type WalletErrorType = 'userRejected' | 'disconnected' | 'timeout' | 'notFound' | 'unknown';
45
+ export type WalletErrorInfo = {
46
+ type: WalletErrorType;
47
+ code: number | null;
48
+ message: string;
49
+ };
34
50
  export interface RequestAccountsOptions {
35
51
  provider?: Eip1193Provider;
52
+ dedupe?: boolean;
36
53
  }
37
54
  export type AccountSelection = {
38
55
  account: string | null;
@@ -23,6 +23,40 @@ export type WebDavRequestOptions = {
23
23
  contentType?: string;
24
24
  signal?: AbortSignal;
25
25
  };
26
+ export type WebDavShareExpiresUnit = 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';
27
+ export type WebDavShareItem = {
28
+ token: string;
29
+ name: string;
30
+ path: string;
31
+ url: string;
32
+ viewCount: number;
33
+ downloadCount: number;
34
+ expiresAt?: string;
35
+ createdAt?: string;
36
+ };
37
+ export type CreateWebDavShareLinkOptions = {
38
+ path: string;
39
+ expiresIn?: number;
40
+ expiresValue?: number;
41
+ expiresUnit?: WebDavShareExpiresUnit;
42
+ auth?: WebDavAuth;
43
+ token?: string;
44
+ signal?: AbortSignal;
45
+ };
46
+ export type WebDavShareListOptions = {
47
+ auth?: WebDavAuth;
48
+ token?: string;
49
+ signal?: AbortSignal;
50
+ };
51
+ export type WebDavShareRevokeOptions = {
52
+ auth?: WebDavAuth;
53
+ token?: string;
54
+ signal?: AbortSignal;
55
+ };
56
+ export type WebDavShareRevokeResult = {
57
+ message?: string;
58
+ revoked?: boolean;
59
+ };
26
60
  export declare class WebDavClient {
27
61
  private baseUrl;
28
62
  private prefix;
@@ -51,5 +85,10 @@ export declare class WebDavClient {
51
85
  recoverRecycle(hash: string): Promise<unknown>;
52
86
  deleteRecycle(hash: string): Promise<unknown>;
53
87
  clearRecycle(): Promise<unknown>;
88
+ private requestApiJson;
89
+ getShareAccessUrl(token: string, fileName?: string): string;
90
+ createShareLink(options: CreateWebDavShareLinkOptions): Promise<WebDavShareItem>;
91
+ listShareLinks(options?: WebDavShareListOptions): Promise<WebDavShareItem[]>;
92
+ revokeShareLink(token: string, options?: WebDavShareRevokeOptions): Promise<WebDavShareRevokeResult>;
54
93
  }
55
94
  export declare function createWebDavClient(options: WebDavClientOptions): WebDavClient;
@@ -1,10 +1,46 @@
1
1
  const YEYING_RDNS = 'io.github.yeying';
2
2
  const DEFAULT_TIMEOUT = 1000;
3
3
  const DEFAULT_ACCOUNT_STORAGE_KEY = 'yeying:last_account';
4
+ const DEFAULT_PROVIDER_POLL_INTERVAL = 100;
5
+ const DEFAULT_PROVIDER_MAX_POLLS = 20;
6
+ const requestAccountsInFlight = new WeakMap();
7
+ function isProvider(value) {
8
+ return !!value && typeof value.request === 'function';
9
+ }
4
10
  function getWindowEthereum() {
5
11
  if (typeof window === 'undefined')
6
12
  return null;
7
- return window.ethereum || null;
13
+ const ethereum = window.ethereum;
14
+ return isProvider(ethereum) ? ethereum : null;
15
+ }
16
+ function getWindowProviderCandidates() {
17
+ if (typeof window === 'undefined')
18
+ return [];
19
+ const source = window;
20
+ const candidates = [];
21
+ const addProvider = (provider) => {
22
+ if (isProvider(provider) && !candidates.includes(provider)) {
23
+ candidates.push(provider);
24
+ }
25
+ };
26
+ for (const name of [
27
+ 'ethereum',
28
+ 'yeeying',
29
+ 'yeying',
30
+ 'coinbaseWallet',
31
+ 'bitkeep',
32
+ 'tokenpocket',
33
+ '__YEYING_PROVIDER__',
34
+ ]) {
35
+ addProvider(source[name]);
36
+ }
37
+ const ethereum = getWindowEthereum();
38
+ if (Array.isArray(ethereum?.providers)) {
39
+ for (const provider of ethereum.providers) {
40
+ addProvider(provider);
41
+ }
42
+ }
43
+ return candidates;
8
44
  }
9
45
  function readStoredAccount(storageKey) {
10
46
  if (typeof localStorage === 'undefined')
@@ -48,7 +84,7 @@ function isYeYingProvider(provider, info) {
48
84
  }
49
85
  function selectBestProvider(candidates, preferYeYing) {
50
86
  if (candidates.length === 0)
51
- return null;
87
+ return selectBestWindowProvider(preferYeYing);
52
88
  if (preferYeYing) {
53
89
  const yeying = candidates.find(c => isYeYingProvider(c.provider, c.info));
54
90
  if (yeying)
@@ -56,10 +92,21 @@ function selectBestProvider(candidates, preferYeYing) {
56
92
  }
57
93
  return candidates[0].provider;
58
94
  }
95
+ function selectBestWindowProvider(preferYeYing) {
96
+ const candidates = getWindowProviderCandidates();
97
+ if (candidates.length === 0)
98
+ return null;
99
+ if (preferYeYing) {
100
+ const yeying = candidates.find(provider => isYeYingProvider(provider));
101
+ if (yeying)
102
+ return yeying;
103
+ }
104
+ return candidates[0];
105
+ }
59
106
  async function getProvider(options = {}) {
60
107
  const preferYeYing = options.preferYeYing !== false;
61
108
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT;
62
- const windowProvider = getWindowEthereum();
109
+ const windowProvider = selectBestWindowProvider(preferYeYing);
63
110
  if (preferYeYing && isYeYingProvider(windowProvider)) {
64
111
  return windowProvider;
65
112
  }
@@ -92,7 +139,7 @@ async function getProvider(options = {}) {
92
139
  }
93
140
  };
94
141
  const onEthereumInitialized = () => {
95
- const injected = getWindowEthereum();
142
+ const injected = selectBestWindowProvider(preferYeYing);
96
143
  if (preferYeYing && isYeYingProvider(injected)) {
97
144
  safeResolve(injected);
98
145
  }
@@ -104,7 +151,7 @@ async function getProvider(options = {}) {
104
151
  return;
105
152
  const best = selectBestProvider(discovered, preferYeYing) ||
106
153
  windowProvider ||
107
- getWindowEthereum();
154
+ selectBestWindowProvider(preferYeYing);
108
155
  safeResolve(best || null);
109
156
  }, timeoutMs);
110
157
  try {
@@ -118,6 +165,107 @@ async function getProvider(options = {}) {
118
165
  }
119
166
  });
120
167
  }
168
+ function watchProvider(handler, options = {}) {
169
+ if (typeof window === 'undefined') {
170
+ handler({ provider: null, present: false });
171
+ return () => { };
172
+ }
173
+ const preferYeYing = options.preferYeYing !== false;
174
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_PROVIDER_POLL_INTERVAL;
175
+ const maxPolls = options.maxPolls ?? DEFAULT_PROVIDER_MAX_POLLS;
176
+ let stopped = false;
177
+ let lastProvider;
178
+ let pollCount = 0;
179
+ let pollTimer = null;
180
+ const emit = () => {
181
+ if (stopped)
182
+ return;
183
+ const provider = selectBestWindowProvider(preferYeYing);
184
+ if (provider === lastProvider)
185
+ return;
186
+ lastProvider = provider;
187
+ handler({ provider, present: !!provider });
188
+ };
189
+ const poll = () => {
190
+ if (stopped)
191
+ return;
192
+ emit();
193
+ pollCount += 1;
194
+ if (lastProvider || pollCount >= maxPolls)
195
+ return;
196
+ pollTimer = setTimeout(poll, pollIntervalMs);
197
+ };
198
+ const handleProviderReady = () => {
199
+ emit();
200
+ };
201
+ window.addEventListener('ethereum#initialized', handleProviderReady);
202
+ window.addEventListener('eip6963:announceProvider', handleProviderReady);
203
+ try {
204
+ window.dispatchEvent(new Event('eip6963:requestProvider'));
205
+ }
206
+ catch {
207
+ // Ignore unsupported event dispatch environments.
208
+ }
209
+ poll();
210
+ return () => {
211
+ stopped = true;
212
+ if (pollTimer) {
213
+ clearTimeout(pollTimer);
214
+ }
215
+ window.removeEventListener('ethereum#initialized', handleProviderReady);
216
+ window.removeEventListener('eip6963:announceProvider', handleProviderReady);
217
+ };
218
+ }
219
+ function getWalletErrorMessage(error) {
220
+ if (!error)
221
+ return '';
222
+ if (typeof error === 'string')
223
+ return error;
224
+ if (error instanceof Error)
225
+ return error.message || String(error);
226
+ const message = error.message;
227
+ if (typeof message === 'string')
228
+ return message;
229
+ return String(error);
230
+ }
231
+ function getWalletErrorCode(error) {
232
+ const code = Number(error?.code);
233
+ if (!Number.isNaN(code))
234
+ return code;
235
+ const causeCode = Number(error?.cause?.code);
236
+ if (!Number.isNaN(causeCode))
237
+ return causeCode;
238
+ return null;
239
+ }
240
+ function classifyWalletError(error) {
241
+ const code = getWalletErrorCode(error);
242
+ const message = getWalletErrorMessage(error);
243
+ const lowerMessage = message.toLowerCase();
244
+ if (code === 4001 || lowerMessage.includes('user rejected')) {
245
+ return { type: 'userRejected', code, message };
246
+ }
247
+ if (code === 4900 ||
248
+ lowerMessage.includes('disconnected') ||
249
+ lowerMessage.includes('reconnect') ||
250
+ lowerMessage.includes('not connected')) {
251
+ return { type: 'disconnected', code, message };
252
+ }
253
+ if (lowerMessage.includes('timeout')) {
254
+ return { type: 'timeout', code, message };
255
+ }
256
+ if (lowerMessage.includes('no injected wallet provider') ||
257
+ lowerMessage.includes('未检测到钱包')) {
258
+ return { type: 'notFound', code, message };
259
+ }
260
+ return { type: 'unknown', code, message };
261
+ }
262
+ function isUserRejectedWalletAction(error) {
263
+ return classifyWalletError(error).type === 'userRejected';
264
+ }
265
+ function isWalletReconnectError(error) {
266
+ const type = classifyWalletError(error).type;
267
+ return type === 'disconnected' || type === 'timeout';
268
+ }
121
269
  async function requireProvider(options = {}) {
122
270
  const provider = await getProvider(options);
123
271
  if (!provider) {
@@ -127,10 +275,24 @@ async function requireProvider(options = {}) {
127
275
  }
128
276
  async function requestAccounts(options = {}) {
129
277
  const provider = options.provider || (await requireProvider());
130
- const accounts = (await provider.request({
278
+ const dedupe = options.dedupe !== false;
279
+ if (dedupe) {
280
+ const pending = requestAccountsInFlight.get(provider);
281
+ if (pending)
282
+ return pending;
283
+ }
284
+ const request = provider.request({
131
285
  method: 'eth_requestAccounts',
132
- }));
133
- return Array.isArray(accounts) ? accounts : [];
286
+ }).then(accounts => (Array.isArray(accounts) ? accounts : []));
287
+ if (!dedupe)
288
+ return request;
289
+ requestAccountsInFlight.set(provider, request);
290
+ try {
291
+ return await request;
292
+ }
293
+ finally {
294
+ requestAccountsInFlight.delete(provider);
295
+ }
134
296
  }
135
297
  async function getAccounts(provider) {
136
298
  const p = provider || (await requireProvider());
@@ -1435,6 +1597,9 @@ function joinUrl(baseUrl, path) {
1435
1597
  const suffix = path.startsWith('/') ? path : `/${path}`;
1436
1598
  return `${base}${suffix}`;
1437
1599
  }
1600
+ function isRecord(value) {
1601
+ return typeof value === 'object' && value !== null;
1602
+ }
1438
1603
  function resolveAuthHeader(auth, token) {
1439
1604
  if (auth?.type === 'bearer') {
1440
1605
  return `Bearer ${auth.token}`;
@@ -1623,6 +1788,72 @@ class WebDavClient {
1623
1788
  }
1624
1789
  return await res.json();
1625
1790
  }
1791
+ async requestApiJson(method, apiPath, body, options) {
1792
+ const headers = this.buildHeaders({
1793
+ auth: options?.auth,
1794
+ token: options?.token,
1795
+ contentType: body === undefined ? undefined : 'application/json',
1796
+ });
1797
+ const response = await this.fetcher(joinUrl(this.baseUrl, apiPath), {
1798
+ method,
1799
+ headers,
1800
+ body: body === undefined ? undefined : JSON.stringify(body),
1801
+ credentials: this.credentials,
1802
+ signal: options?.signal,
1803
+ });
1804
+ if (!response.ok) {
1805
+ throw new Error(`WebDAV ${method} ${apiPath} failed: ${response.status} ${response.statusText}`);
1806
+ }
1807
+ return await response.json();
1808
+ }
1809
+ getShareAccessUrl(token, fileName) {
1810
+ const normalizedToken = encodeURIComponent(String(token || '').trim());
1811
+ if (!normalizedToken) {
1812
+ throw new Error('Share token is required');
1813
+ }
1814
+ const encodedFileName = String(fileName || '').trim()
1815
+ ? `/${encodeURIComponent(String(fileName || '').trim())}`
1816
+ : '';
1817
+ return joinUrl(this.baseUrl, `/api/v1/public/share/${normalizedToken}${encodedFileName}`);
1818
+ }
1819
+ async createShareLink(options) {
1820
+ const normalizedPath = String(options.path || '').trim();
1821
+ if (!normalizedPath) {
1822
+ throw new Error('Share path is required');
1823
+ }
1824
+ const payload = await this.requestApiJson('POST', '/api/v1/public/share/create', {
1825
+ path: normalizedPath,
1826
+ expiresIn: options.expiresIn,
1827
+ expiresValue: options.expiresValue,
1828
+ expiresUnit: options.expiresUnit,
1829
+ }, options);
1830
+ if (!isRecord(payload)) {
1831
+ throw new Error('WebDAV share create response is invalid');
1832
+ }
1833
+ return payload;
1834
+ }
1835
+ async listShareLinks(options = {}) {
1836
+ const payload = await this.requestApiJson('GET', '/api/v1/public/share/list', undefined, options);
1837
+ if (!isRecord(payload)) {
1838
+ return [];
1839
+ }
1840
+ const items = payload.items;
1841
+ if (!Array.isArray(items)) {
1842
+ return [];
1843
+ }
1844
+ return items.filter(isRecord);
1845
+ }
1846
+ async revokeShareLink(token, options = {}) {
1847
+ const normalizedToken = String(token || '').trim();
1848
+ if (!normalizedToken) {
1849
+ throw new Error('Share token is required');
1850
+ }
1851
+ const payload = await this.requestApiJson('POST', '/api/v1/public/share/revoke', { token: normalizedToken }, options);
1852
+ if (!isRecord(payload)) {
1853
+ return {};
1854
+ }
1855
+ return payload;
1856
+ }
1626
1857
  }
1627
1858
  function createWebDavClient(options) {
1628
1859
  return new WebDavClient(options);
@@ -1935,5 +2166,5 @@ async function initDappSession(options) {
1935
2166
  return result;
1936
2167
  }
1937
2168
 
1938
- export { WebDavClient, authCentralUcanFetch, authFetch, authUcanFetch, clearAccessToken, clearCentralSessionToken, clearUcanSession, createAndIssueCentralUcan, createCentralSession, createDelegationUcan, createInvocationUcan, createRootUcan, createUcanSession, createWebDavClient, deriveAppIdFromHost, deriveAppIdFromLocation, getAccessToken, getAccounts, getBalance, getCapabilityAction, getCapabilityResource, getCentralIssuerInfo, getCentralSessionToken, getChainId, getOrCreateUcanRoot, getPreferredAccount, getProvider, getStoredUcanRoot, getUcanSession, initDappSession, initWebDavStorage, isYeYingProvider, issueCentralUcan, loginWithChallenge, logout, normalizeAppHostnameForAppId, normalizeUcanCapabilities, normalizeUcanCapability, onAccountsChanged, onChainChanged, refreshAccessToken, requestAccounts, requireProvider, setAccessToken, setCentralSessionToken, signMessage, storeUcanRoot, watchAccounts };
2169
+ export { WebDavClient, authCentralUcanFetch, authFetch, authUcanFetch, classifyWalletError, clearAccessToken, clearCentralSessionToken, clearUcanSession, createAndIssueCentralUcan, createCentralSession, createDelegationUcan, createInvocationUcan, createRootUcan, createUcanSession, createWebDavClient, deriveAppIdFromHost, deriveAppIdFromLocation, getAccessToken, getAccounts, getBalance, getCapabilityAction, getCapabilityResource, getCentralIssuerInfo, getCentralSessionToken, getChainId, getOrCreateUcanRoot, getPreferredAccount, getProvider, getStoredUcanRoot, getUcanSession, getWalletErrorCode, getWalletErrorMessage, initDappSession, initWebDavStorage, isUserRejectedWalletAction, isWalletReconnectError, isYeYingProvider, issueCentralUcan, loginWithChallenge, logout, normalizeAppHostnameForAppId, normalizeUcanCapabilities, normalizeUcanCapability, onAccountsChanged, onChainChanged, refreshAccessToken, requestAccounts, requireProvider, setAccessToken, setCentralSessionToken, signMessage, storeUcanRoot, watchAccounts, watchProvider };
1939
2170
  //# sourceMappingURL=web3-bs.esm.js.map