@yeying-community/web3-bs 1.0.11 → 1.0.12

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.
@@ -694,8 +694,9 @@
694
694
  }
695
695
 
696
696
  const DEFAULT_SESSION_ID = 'default';
697
- const DEFAULT_SESSION_TTL = 24 * 60 * 60 * 1000;
698
- const DEFAULT_UCAN_TTL = 5 * 60 * 1000;
697
+ const DEFAULT_UCAN_SESSION_TTL_MS = 24 * 60 * 60 * 1000;
698
+ const DEFAULT_UCAN_TOKEN_TTL_MS = 40 * 60 * 1000;
699
+ const DEFAULT_UCAN_TOKEN_SKEW_MS = 60 * 1000;
699
700
  const DB_NAME = 'yeying-web3';
700
701
  const DB_STORE = 'ucan-sessions';
701
702
  const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
@@ -776,6 +777,30 @@
776
777
  function encodeJson(value) {
777
778
  return toBase64Url(textEncoder.encode(JSON.stringify(value)));
778
779
  }
780
+ function decodeBase64Url(input) {
781
+ if (!input)
782
+ return null;
783
+ const base64 = input.replace(/-/g, '+').replace(/_/g, '/');
784
+ const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
785
+ try {
786
+ if (typeof atob === 'function') {
787
+ return atob(padded);
788
+ }
789
+ }
790
+ catch {
791
+ // Try Node-compatible fallback below.
792
+ }
793
+ try {
794
+ const nodeBuffer = globalThis.Buffer;
795
+ if (nodeBuffer) {
796
+ return nodeBuffer.from(padded, 'base64').toString('utf8');
797
+ }
798
+ }
799
+ catch {
800
+ return null;
801
+ }
802
+ return null;
803
+ }
779
804
  function randomNonce(bytes = 16) {
780
805
  const buffer = new Uint8Array(bytes);
781
806
  crypto.getRandomValues(buffer);
@@ -784,8 +809,144 @@
784
809
  .join('');
785
810
  }
786
811
  function normalizeExpiry(exp, fallbackMs) {
812
+ if (typeof exp === 'number' && !Number.isNaN(exp))
813
+ return exp;
787
814
  return Date.now() + fallbackMs;
788
815
  }
816
+ function normalizeUcanExpiry(exp, fallbackMs) {
817
+ return normalizeExpiry(exp, fallbackMs);
818
+ }
819
+ function decodeUcanPayload(token) {
820
+ const parts = String(token || '').split('.');
821
+ if (parts.length < 2)
822
+ return null;
823
+ const decoded = decodeBase64Url(parts[1]);
824
+ if (!decoded)
825
+ return null;
826
+ try {
827
+ const payload = JSON.parse(decoded);
828
+ if (!payload || typeof payload !== 'object')
829
+ return null;
830
+ return payload;
831
+ }
832
+ catch {
833
+ return null;
834
+ }
835
+ }
836
+ function getUcanTokenTiming(token, options = {}) {
837
+ const nowMs = options.nowMs ?? Date.now();
838
+ const payload = decodeUcanPayload(token);
839
+ const exp = typeof payload?.exp === 'number' ? payload.exp : null;
840
+ const nbf = typeof payload?.nbf === 'number' ? payload.nbf : null;
841
+ const payloadWithIat = payload;
842
+ const issuedAt = typeof payloadWithIat?.iat === 'number'
843
+ ? payloadWithIat.iat
844
+ : null;
845
+ const remainingMs = exp === null ? null : exp - nowMs;
846
+ const activeInMs = nbf === null ? 0 : Math.max(0, nbf - nowMs);
847
+ const expired = exp === null || (remainingMs !== null && remainingMs <= 0);
848
+ const notBefore = activeInMs > 0;
849
+ return {
850
+ valid: Boolean(payload && !expired && !notBefore),
851
+ payload,
852
+ exp,
853
+ nbf,
854
+ issuedAt,
855
+ nowMs,
856
+ remainingMs,
857
+ activeInMs,
858
+ expired,
859
+ notBefore,
860
+ };
861
+ }
862
+ function isUcanTokenFresh(tokenOrTiming, options = {}) {
863
+ const timing = typeof tokenOrTiming === 'string'
864
+ ? getUcanTokenTiming(tokenOrTiming, { nowMs: options.nowMs })
865
+ : tokenOrTiming;
866
+ if (!timing.valid)
867
+ return false;
868
+ const skewMs = Math.max(0, options.skewMs ?? DEFAULT_UCAN_TOKEN_SKEW_MS);
869
+ return typeof timing.remainingMs === 'number' && timing.remainingMs > skewMs;
870
+ }
871
+ function readErrorField(error, field) {
872
+ if (!error || typeof error !== 'object')
873
+ return undefined;
874
+ const value = error[field];
875
+ if (value !== undefined)
876
+ return value;
877
+ const nestedError = error.error;
878
+ if (nestedError && typeof nestedError === 'object') {
879
+ return nestedError[field];
880
+ }
881
+ return undefined;
882
+ }
883
+ function classifyUcanAuthError(error) {
884
+ const messageValue = readErrorField(error, 'message');
885
+ const codeValue = readErrorField(error, 'code');
886
+ const statusValue = readErrorField(error, 'status') ?? readErrorField(error, 'statusCode');
887
+ const message = typeof messageValue === 'string'
888
+ ? messageValue
889
+ : error instanceof Error
890
+ ? error.message
891
+ : String(messageValue || error || '');
892
+ const status = typeof statusValue === 'number' ? statusValue : undefined;
893
+ const code = typeof codeValue === 'string' || typeof codeValue === 'number' ? codeValue : undefined;
894
+ const normalized = `${message} ${String(code || '')}`.toLowerCase();
895
+ if (/ucan.*expired|expired.*ucan|token.*expired|jwt.*expired|\bexp\b/.test(normalized)) {
896
+ return { type: 'expired', message, retryable: true, shouldRefresh: true, status, code };
897
+ }
898
+ if (/not.?before|\bnbf\b|not yet valid/.test(normalized)) {
899
+ return { type: 'not-before', message, retryable: true, shouldRefresh: false, status, code };
900
+ }
901
+ if (/invalid.*token|malformed.*token|bad.*ucan|invalid.*ucan/.test(normalized)) {
902
+ return { type: 'invalid-token', message, retryable: true, shouldRefresh: true, status, code };
903
+ }
904
+ if (status === 401 || /unauthori[sz]ed|unauthenticated/.test(normalized)) {
905
+ return { type: 'unauthorized', message, retryable: true, shouldRefresh: true, status, code };
906
+ }
907
+ if (status === 403 || /forbidden|permission denied|capability/.test(normalized)) {
908
+ return { type: 'forbidden', message, retryable: false, shouldRefresh: false, status, code };
909
+ }
910
+ return { type: 'unknown', message, retryable: false, shouldRefresh: false, status, code };
911
+ }
912
+ function isReplayableRequestBody(body) {
913
+ if (body == null)
914
+ return true;
915
+ if (typeof body === 'string')
916
+ return true;
917
+ if (typeof URLSearchParams !== 'undefined' && body instanceof URLSearchParams)
918
+ return true;
919
+ if (typeof FormData !== 'undefined' && body instanceof FormData)
920
+ return true;
921
+ if (typeof Blob !== 'undefined' && body instanceof Blob)
922
+ return true;
923
+ if (body instanceof ArrayBuffer)
924
+ return true;
925
+ if (ArrayBuffer.isView(body))
926
+ return true;
927
+ return false;
928
+ }
929
+ async function parseResponseJsonBody(response) {
930
+ const text = await response.text();
931
+ if (!text)
932
+ return null;
933
+ try {
934
+ return JSON.parse(text);
935
+ }
936
+ catch {
937
+ return { raw: text };
938
+ }
939
+ }
940
+ function shouldRetryUcanFetch(response, errorInfo) {
941
+ if (errorInfo.type === 'expired' || errorInfo.shouldRefresh) {
942
+ return true;
943
+ }
944
+ if (response.status === 401)
945
+ return true;
946
+ if (response.status === 403 && errorInfo.type === 'forbidden')
947
+ return false;
948
+ return false;
949
+ }
789
950
  function isSessionExpired(expiresAt, nowMs = Date.now()) {
790
951
  return typeof expiresAt === 'number' && nowMs >= expiresAt;
791
952
  }
@@ -882,7 +1043,7 @@
882
1043
  buildDidKey(pair.publicKey),
883
1044
  ]);
884
1045
  const createdAt = Date.now();
885
- const expiresAt = normalizeExpiry(undefined, options.expiresInMs ?? DEFAULT_SESSION_TTL);
1046
+ const expiresAt = normalizeExpiry(undefined, options.expiresInMs ?? DEFAULT_UCAN_SESSION_TTL_MS);
886
1047
  const root = shouldKeepRootForSession(record?.root, did, createdAt) ? record?.root : undefined;
887
1048
  await writeSessionRecord({
888
1049
  id: sessionId,
@@ -1150,7 +1311,7 @@
1150
1311
  const domain = options.domain || (typeof window !== 'undefined' ? window.location.host : '127.0.0.1');
1151
1312
  const uri = options.uri || (typeof window !== 'undefined' ? window.location.origin : 'http://127.0.0.1');
1152
1313
  const nonce = options.nonce || randomNonce(8);
1153
- const exp = normalizeExpiry(undefined, options.expiresInMs ?? DEFAULT_SESSION_TTL);
1314
+ const exp = normalizeExpiry(undefined, options.expiresInMs ?? DEFAULT_UCAN_SESSION_TTL_MS);
1154
1315
  const nbf = options.notBeforeMs;
1155
1316
  const normalizedCapabilities = normalizeUcanCapabilities(options.capabilities);
1156
1317
  if (!normalizedCapabilities.length) {
@@ -1234,7 +1395,7 @@
1234
1395
  if (!normalizedCapabilities.length) {
1235
1396
  throw new Error('Missing UCAN capabilities');
1236
1397
  }
1237
- const exp = normalizeExpiry(undefined, options.expiresInMs ?? DEFAULT_UCAN_TTL);
1398
+ const exp = normalizeExpiry(undefined, options.expiresInMs ?? DEFAULT_UCAN_TOKEN_TTL_MS);
1238
1399
  const payload = {
1239
1400
  iss: issuer.did,
1240
1401
  aud: options.audience,
@@ -1256,7 +1417,7 @@
1256
1417
  if (!normalizedCapabilities.length) {
1257
1418
  throw new Error('Missing UCAN capabilities');
1258
1419
  }
1259
- const exp = normalizeExpiry(undefined, options.expiresInMs ?? DEFAULT_UCAN_TTL);
1420
+ const exp = normalizeExpiry(undefined, options.expiresInMs ?? DEFAULT_UCAN_TOKEN_TTL_MS);
1260
1421
  const payload = {
1261
1422
  iss: issuer.did,
1262
1423
  aud: options.audience,
@@ -1267,30 +1428,84 @@
1267
1428
  };
1268
1429
  return await signUcanPayload(payload, issuer);
1269
1430
  }
1431
+ async function getOrCreateInvocationUcan(options) {
1432
+ if (options.ucan &&
1433
+ isUcanTokenFresh(options.ucan, {
1434
+ nowMs: options.nowMs,
1435
+ skewMs: options.skewMs,
1436
+ })) {
1437
+ return options.ucan;
1438
+ }
1439
+ return await createInvocationUcan({
1440
+ issuer: options.issuer,
1441
+ sessionId: options.sessionId,
1442
+ provider: options.provider,
1443
+ audience: options.audience,
1444
+ capabilities: options.capabilities,
1445
+ expiresInMs: options.expiresInMs,
1446
+ notBeforeMs: options.notBeforeMs,
1447
+ proofs: options.proofs,
1448
+ });
1449
+ }
1270
1450
  async function authUcanFetch(input, init = {}, options = {}) {
1271
1451
  const fetcher = options.fetcher || fetch;
1272
- let token = options.ucan;
1273
- if (!token) {
1274
- if (!options.audience || !options.capabilities) {
1452
+ const audience = options.audience;
1453
+ const capabilities = options.capabilities;
1454
+ const canRefresh = Boolean(audience && capabilities);
1455
+ const canRetry = canRefresh && isReplayableRequestBody(init.body);
1456
+ let token = options.ucan || '';
1457
+ if (!token || (canRefresh && !isUcanTokenFresh(token, { skewMs: options.skewMs }))) {
1458
+ if (!audience || !capabilities) {
1275
1459
  throw new Error('Missing UCAN audience or capabilities');
1276
1460
  }
1277
- token = await createInvocationUcan({
1461
+ token = await getOrCreateInvocationUcan({
1462
+ ucan: options.ucan,
1278
1463
  issuer: options.issuer,
1279
1464
  sessionId: options.sessionId,
1280
1465
  provider: options.provider,
1281
- audience: options.audience,
1282
- capabilities: options.capabilities,
1466
+ audience,
1467
+ capabilities,
1283
1468
  expiresInMs: options.expiresInMs,
1284
1469
  notBeforeMs: options.notBeforeMs,
1285
1470
  proofs: options.proofs,
1471
+ skewMs: options.skewMs,
1286
1472
  });
1287
1473
  }
1288
- const headers = new Headers(init.headers || {});
1289
- headers.set('Authorization', `Bearer ${token}`);
1290
- return fetcher(input, {
1474
+ const makeHeaders = (bearer) => {
1475
+ const headers = new Headers(init.headers || {});
1476
+ headers.set('Authorization', `Bearer ${bearer}`);
1477
+ return headers;
1478
+ };
1479
+ let response = await fetcher(input, {
1291
1480
  ...init,
1292
- headers,
1481
+ headers: makeHeaders(token),
1482
+ });
1483
+ if (!canRetry || response.ok) {
1484
+ return response;
1485
+ }
1486
+ const payload = await parseResponseJsonBody(response.clone()).catch(() => null);
1487
+ const errorInfo = classifyUcanAuthError(payload || response.statusText || response);
1488
+ if (!shouldRetryUcanFetch(response, errorInfo)) {
1489
+ return response;
1490
+ }
1491
+ if (!audience || !capabilities) {
1492
+ return response;
1493
+ }
1494
+ const refreshedToken = await createInvocationUcan({
1495
+ issuer: options.issuer,
1496
+ sessionId: options.sessionId,
1497
+ provider: options.provider,
1498
+ audience,
1499
+ capabilities,
1500
+ expiresInMs: options.expiresInMs,
1501
+ notBeforeMs: options.notBeforeMs,
1502
+ proofs: options.proofs,
1293
1503
  });
1504
+ response = await fetcher(input, {
1505
+ ...init,
1506
+ headers: makeHeaders(refreshedToken),
1507
+ });
1508
+ return response;
1294
1509
  }
1295
1510
 
1296
1511
  const DEFAULT_BASE_URL = '/api/v1/public/auth/central';
@@ -1885,7 +2100,6 @@
1885
2100
  }
1886
2101
 
1887
2102
  const tokenCache = new Map();
1888
- const TOKEN_SKEW_MS = 5000;
1889
2103
  const DEFAULT_APP_ACTION = 'write';
1890
2104
  const LOOPBACK_HOST_ALIASES = new Set([
1891
2105
  'localhost',
@@ -2027,64 +2241,19 @@
2027
2241
  const caps = options.invocationCapabilities || fallbackCaps;
2028
2242
  return ensureAppCapability(caps, options);
2029
2243
  }
2030
- function isTokenValid(entry, nowMs) {
2031
- if (!entry.exp)
2032
- return false;
2033
- if (entry.nbf && nowMs < entry.nbf)
2034
- return false;
2035
- return entry.exp - TOKEN_SKEW_MS > nowMs;
2036
- }
2037
- function decodeBase64Url(input) {
2038
- if (!input)
2039
- return null;
2040
- const base64 = input.replace(/-/g, '+').replace(/_/g, '/');
2041
- const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, '=');
2042
- try {
2043
- if (typeof atob === 'function') {
2044
- return atob(padded);
2045
- }
2046
- }
2047
- catch {
2048
- // ignore
2049
- }
2050
- try {
2051
- const nodeBuffer = globalThis.Buffer;
2052
- if (nodeBuffer) {
2053
- return nodeBuffer.from(padded, 'base64').toString('utf8');
2054
- }
2055
- }
2056
- catch {
2057
- return null;
2058
- }
2059
- return null;
2060
- }
2061
- function decodeUcanPayload(token) {
2062
- const parts = token.split('.');
2063
- if (parts.length < 2)
2064
- return null;
2065
- const decoded = decodeBase64Url(parts[1]);
2066
- if (!decoded)
2067
- return null;
2068
- try {
2069
- return JSON.parse(decoded);
2070
- }
2071
- catch {
2072
- return null;
2073
- }
2074
- }
2075
2244
  async function getCachedInvocationToken(options) {
2076
2245
  const cacheKey = buildTokenCacheKey(options.issuer, options.audience, options.capabilities);
2077
2246
  const cached = tokenCache.get(cacheKey);
2078
2247
  const nowMs = Date.now();
2079
- if (cached && isTokenValid(cached, nowMs)) {
2080
- return cached.token;
2081
- }
2082
- const token = await createInvocationUcan({
2248
+ const token = await getOrCreateInvocationUcan({
2249
+ ucan: cached?.token,
2083
2250
  issuer: options.issuer,
2084
2251
  audience: options.audience,
2085
2252
  capabilities: options.capabilities,
2086
2253
  proofs: options.proofs,
2087
2254
  expiresInMs: options.expiresInMs,
2255
+ skewMs: options.skewMs,
2256
+ nowMs,
2088
2257
  notBeforeMs: options.notBeforeMs,
2089
2258
  });
2090
2259
  const payload = decodeUcanPayload(token);
@@ -2135,6 +2304,7 @@
2135
2304
  capabilities: invocationCaps,
2136
2305
  proofs: [root],
2137
2306
  expiresInMs: options.invocationExpiresInMs,
2307
+ skewMs: options.invocationSkewMs,
2138
2308
  notBeforeMs: options.notBeforeMs,
2139
2309
  });
2140
2310
  const client = createWebDavClient({
@@ -2191,10 +2361,14 @@
2191
2361
  return result;
2192
2362
  }
2193
2363
 
2364
+ exports.DEFAULT_UCAN_SESSION_TTL_MS = DEFAULT_UCAN_SESSION_TTL_MS;
2365
+ exports.DEFAULT_UCAN_TOKEN_SKEW_MS = DEFAULT_UCAN_TOKEN_SKEW_MS;
2366
+ exports.DEFAULT_UCAN_TOKEN_TTL_MS = DEFAULT_UCAN_TOKEN_TTL_MS;
2194
2367
  exports.WebDavClient = WebDavClient;
2195
2368
  exports.authCentralUcanFetch = authCentralUcanFetch;
2196
2369
  exports.authFetch = authFetch;
2197
2370
  exports.authUcanFetch = authUcanFetch;
2371
+ exports.classifyUcanAuthError = classifyUcanAuthError;
2198
2372
  exports.classifyWalletError = classifyWalletError;
2199
2373
  exports.clearAccessToken = clearAccessToken;
2200
2374
  exports.clearCentralSessionToken = clearCentralSessionToken;
@@ -2206,6 +2380,7 @@
2206
2380
  exports.createRootUcan = createRootUcan;
2207
2381
  exports.createUcanSession = createUcanSession;
2208
2382
  exports.createWebDavClient = createWebDavClient;
2383
+ exports.decodeUcanPayload = decodeUcanPayload;
2209
2384
  exports.deriveAppIdFromHost = deriveAppIdFromHost;
2210
2385
  exports.deriveAppIdFromLocation = deriveAppIdFromLocation;
2211
2386
  exports.focusPendingApproval = focusPendingApproval;
@@ -2217,15 +2392,18 @@
2217
2392
  exports.getCentralIssuerInfo = getCentralIssuerInfo;
2218
2393
  exports.getCentralSessionToken = getCentralSessionToken;
2219
2394
  exports.getChainId = getChainId;
2395
+ exports.getOrCreateInvocationUcan = getOrCreateInvocationUcan;
2220
2396
  exports.getOrCreateUcanRoot = getOrCreateUcanRoot;
2221
2397
  exports.getPreferredAccount = getPreferredAccount;
2222
2398
  exports.getProvider = getProvider;
2223
2399
  exports.getStoredUcanRoot = getStoredUcanRoot;
2224
2400
  exports.getUcanSession = getUcanSession;
2401
+ exports.getUcanTokenTiming = getUcanTokenTiming;
2225
2402
  exports.getWalletErrorCode = getWalletErrorCode;
2226
2403
  exports.getWalletErrorMessage = getWalletErrorMessage;
2227
2404
  exports.initDappSession = initDappSession;
2228
2405
  exports.initWebDavStorage = initWebDavStorage;
2406
+ exports.isUcanTokenFresh = isUcanTokenFresh;
2229
2407
  exports.isUserRejectedWalletAction = isUserRejectedWalletAction;
2230
2408
  exports.isWalletReconnectError = isWalletReconnectError;
2231
2409
  exports.isYeYingProvider = isYeYingProvider;
@@ -2235,6 +2413,7 @@
2235
2413
  exports.normalizeAppHostnameForAppId = normalizeAppHostnameForAppId;
2236
2414
  exports.normalizeUcanCapabilities = normalizeUcanCapabilities;
2237
2415
  exports.normalizeUcanCapability = normalizeUcanCapability;
2416
+ exports.normalizeUcanExpiry = normalizeUcanExpiry;
2238
2417
  exports.onAccountsChanged = onAccountsChanged;
2239
2418
  exports.onChainChanged = onChainChanged;
2240
2419
  exports.refreshAccessToken = refreshAccessToken;