@yeying-community/web3-bs 1.0.4 → 1.0.6

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.
@@ -199,12 +199,12 @@
199
199
  return () => provider.removeListener?.('chainChanged', handler);
200
200
  }
201
201
 
202
- function normalizeBaseUrl$1(baseUrl) {
202
+ function normalizeBaseUrl$2(baseUrl) {
203
203
  return baseUrl.replace(/\/+$/, '');
204
204
  }
205
- function joinUrl$1(baseUrl, path) {
205
+ function joinUrl$2(baseUrl, path) {
206
206
  const trimmed = path.replace(/^\/+/, '');
207
- return `${normalizeBaseUrl$1(baseUrl)}/${trimmed}`;
207
+ return `${normalizeBaseUrl$2(baseUrl)}/${trimmed}`;
208
208
  }
209
209
  const DEFAULT_TOKEN_KEY = 'authToken';
210
210
  let cachedAccessToken = null;
@@ -215,10 +215,10 @@
215
215
  function shouldStoreToken(options) {
216
216
  return options?.storeToken !== false;
217
217
  }
218
- function resolveFetcher(options) {
218
+ function resolveFetcher$1(options) {
219
219
  return options?.fetcher || fetch;
220
220
  }
221
- function resolveCredentials(options) {
221
+ function resolveCredentials$1(options) {
222
222
  return options?.credentials ?? 'include';
223
223
  }
224
224
  function readStoredToken(options) {
@@ -358,11 +358,11 @@
358
358
  async function loginWithChallenge(options = {}) {
359
359
  const provider = options.provider || (await requireProvider());
360
360
  const address = await resolveAddress$1(provider, options.address);
361
- const fetcher = resolveFetcher(options);
362
- const credentials = resolveCredentials(options);
361
+ const fetcher = resolveFetcher$1(options);
362
+ const credentials = resolveCredentials$1(options);
363
363
  const baseUrl = options.baseUrl || '/api/v1/public/auth';
364
- const challengeUrl = joinUrl$1(baseUrl, options.challengePath || 'challenge');
365
- const verifyUrl = joinUrl$1(baseUrl, options.verifyPath || 'verify');
364
+ const challengeUrl = joinUrl$2(baseUrl, options.challengePath || 'challenge');
365
+ const verifyUrl = joinUrl$2(baseUrl, options.verifyPath || 'verify');
366
366
  const challengeBody = {
367
367
  address,
368
368
  };
@@ -426,10 +426,10 @@
426
426
  return refreshInFlight;
427
427
  }
428
428
  const task = (async () => {
429
- const fetcher = resolveFetcher(options);
430
- const credentials = resolveCredentials(options);
429
+ const fetcher = resolveFetcher$1(options);
430
+ const credentials = resolveCredentials$1(options);
431
431
  const baseUrl = options.baseUrl || '/api/v1/public/auth';
432
- const refreshUrl = joinUrl$1(baseUrl, options.refreshPath || 'refresh');
432
+ const refreshUrl = joinUrl$2(baseUrl, options.refreshPath || 'refresh');
433
433
  const refreshRes = await fetcher(refreshUrl, {
434
434
  method: 'POST',
435
435
  headers: {
@@ -458,10 +458,10 @@
458
458
  }
459
459
  }
460
460
  async function logout(options = {}) {
461
- const fetcher = resolveFetcher(options);
462
- const credentials = resolveCredentials(options);
461
+ const fetcher = resolveFetcher$1(options);
462
+ const credentials = resolveCredentials$1(options);
463
463
  const baseUrl = options.baseUrl || '/api/v1/public/auth';
464
- const logoutUrl = joinUrl$1(baseUrl, options.logoutPath || 'logout');
464
+ const logoutUrl = joinUrl$2(baseUrl, options.logoutPath || 'logout');
465
465
  const logoutRes = await fetcher(logoutUrl, {
466
466
  method: 'POST',
467
467
  headers: {
@@ -484,8 +484,8 @@
484
484
  return { response: payload };
485
485
  }
486
486
  async function authFetch(input, init = {}, options = {}) {
487
- const fetcher = resolveFetcher(options);
488
- const credentials = resolveCredentials(options);
487
+ const fetcher = resolveFetcher$1(options);
488
+ const credentials = resolveCredentials$1(options);
489
489
  const retryOnUnauthorized = options.retryOnUnauthorized !== false;
490
490
  const performRequest = async (tokenOverride) => {
491
491
  const headers = new Headers(init.headers || {});
@@ -517,6 +517,8 @@
517
517
  const DEFAULT_UCAN_TTL = 5 * 60 * 1000;
518
518
  const DB_NAME = 'yeying-web3';
519
519
  const DB_STORE = 'ucan-sessions';
520
+ const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
521
+ const DID_KEY_ED25519_MULTICODEC = new Uint8Array([0xed, 0x01]);
520
522
  const textEncoder = new TextEncoder();
521
523
  function toBase64Url(data) {
522
524
  const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
@@ -526,6 +528,70 @@
526
528
  }
527
529
  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
528
530
  }
531
+ function normalizeActionExpression(raw) {
532
+ const normalized = String(raw || '').trim().toLowerCase().replace(/\|/g, ',');
533
+ if (!normalized)
534
+ return '';
535
+ const parts = normalized
536
+ .split(',')
537
+ .map(part => part.trim())
538
+ .filter(Boolean);
539
+ if (!parts.length)
540
+ return '';
541
+ return Array.from(new Set(parts)).join(',');
542
+ }
543
+ function getCapabilityResource(cap) {
544
+ if (!cap || typeof cap !== 'object')
545
+ return '';
546
+ const withValue = typeof cap.with === 'string' ? cap.with.trim() : '';
547
+ if (withValue)
548
+ return withValue;
549
+ return typeof cap.resource === 'string' ? cap.resource.trim() : '';
550
+ }
551
+ function getCapabilityAction(cap) {
552
+ if (!cap || typeof cap !== 'object')
553
+ return '';
554
+ const canValue = typeof cap.can === 'string' ? cap.can.trim() : '';
555
+ if (canValue)
556
+ return normalizeActionExpression(canValue);
557
+ const actionValue = typeof cap.action === 'string' ? cap.action.trim() : '';
558
+ return normalizeActionExpression(actionValue);
559
+ }
560
+ function normalizeUcanCapability(cap, options = {}) {
561
+ const includeLegacyAliases = options.includeLegacyAliases !== false;
562
+ const resource = getCapabilityResource(cap);
563
+ const action = getCapabilityAction(cap);
564
+ if (!resource || !action)
565
+ return null;
566
+ const normalized = {
567
+ with: resource,
568
+ can: action,
569
+ };
570
+ if (includeLegacyAliases) {
571
+ normalized.resource = resource;
572
+ normalized.action = action;
573
+ }
574
+ if (cap && Object.prototype.hasOwnProperty.call(cap, 'nb')) {
575
+ normalized.nb = cap.nb;
576
+ }
577
+ return normalized;
578
+ }
579
+ function normalizeUcanCapabilities(caps, options = {}) {
580
+ const includeLegacyAliases = options.includeLegacyAliases !== false;
581
+ const seen = new Set();
582
+ const result = [];
583
+ for (const cap of caps || []) {
584
+ const normalized = normalizeUcanCapability(cap, { includeLegacyAliases });
585
+ if (!normalized)
586
+ continue;
587
+ const key = `${normalized.with}|${normalized.can}`;
588
+ if (seen.has(key))
589
+ continue;
590
+ seen.add(key);
591
+ result.push(normalized);
592
+ }
593
+ return result;
594
+ }
529
595
  function encodeJson(value) {
530
596
  return toBase64Url(textEncoder.encode(JSON.stringify(value)));
531
597
  }
@@ -539,6 +605,123 @@
539
605
  function normalizeExpiry(exp, fallbackMs) {
540
606
  return Date.now() + fallbackMs;
541
607
  }
608
+ function isSessionExpired(expiresAt, nowMs = Date.now()) {
609
+ return typeof expiresAt === 'number' && nowMs >= expiresAt;
610
+ }
611
+ function encodeBase58(bytes) {
612
+ if (bytes.length === 0)
613
+ return '';
614
+ let value = 0n;
615
+ for (const byte of bytes) {
616
+ value = (value << 8n) + BigInt(byte);
617
+ }
618
+ let encoded = '';
619
+ while (value > 0n) {
620
+ const mod = Number(value % 58n);
621
+ encoded = `${BASE58_ALPHABET[mod]}${encoded}`;
622
+ value /= 58n;
623
+ }
624
+ let leadingZeroCount = 0;
625
+ while (leadingZeroCount < bytes.length && bytes[leadingZeroCount] === 0) {
626
+ leadingZeroCount += 1;
627
+ }
628
+ if (leadingZeroCount > 0) {
629
+ encoded = `${'1'.repeat(leadingZeroCount)}${encoded}`;
630
+ }
631
+ return encoded || '1';
632
+ }
633
+ function ensureWebCrypto() {
634
+ if (typeof crypto === 'undefined' || !crypto.subtle) {
635
+ throw new Error('WebCrypto not available for UCAN session');
636
+ }
637
+ return crypto;
638
+ }
639
+ function parseSessionId(options) {
640
+ return options.id || DEFAULT_SESSION_ID;
641
+ }
642
+ function isLocalSessionRecord(record) {
643
+ return Boolean(record?.source === 'local' || record?.privateKeyJwk);
644
+ }
645
+ async function buildDidKey(publicKey) {
646
+ const webCrypto = ensureWebCrypto();
647
+ const raw = new Uint8Array(await webCrypto.subtle.exportKey('raw', publicKey));
648
+ const prefixed = new Uint8Array(DID_KEY_ED25519_MULTICODEC.length + raw.length);
649
+ prefixed.set(DID_KEY_ED25519_MULTICODEC, 0);
650
+ prefixed.set(raw, DID_KEY_ED25519_MULTICODEC.length);
651
+ return `did:key:z${encodeBase58(prefixed)}`;
652
+ }
653
+ async function importLocalPrivateKey(privateKeyJwk) {
654
+ const webCrypto = ensureWebCrypto();
655
+ return await webCrypto.subtle.importKey('jwk', privateKeyJwk, 'Ed25519', true, ['sign']);
656
+ }
657
+ async function loadLocalSessionFromRecord(id, record) {
658
+ if (!record || !isLocalSessionRecord(record) || !record.privateKeyJwk) {
659
+ return null;
660
+ }
661
+ if (isSessionExpired(record.expiresAt)) {
662
+ await deleteSessionRecord(id);
663
+ return null;
664
+ }
665
+ try {
666
+ const privateKey = await importLocalPrivateKey(record.privateKeyJwk);
667
+ return {
668
+ id: record.id || id,
669
+ did: record.did,
670
+ createdAt: record.createdAt,
671
+ expiresAt: record.expiresAt,
672
+ source: 'local',
673
+ privateKey,
674
+ };
675
+ }
676
+ catch {
677
+ return null;
678
+ }
679
+ }
680
+ function shouldKeepRootForSession(root, did, nowMs) {
681
+ if (!root)
682
+ return false;
683
+ if (root.aud && root.aud !== did)
684
+ return false;
685
+ if (isRootExpired(root, nowMs))
686
+ return false;
687
+ return true;
688
+ }
689
+ async function createLocalSession(options, record) {
690
+ const webCrypto = ensureWebCrypto();
691
+ const sessionId = parseSessionId(options);
692
+ if (!options.forceNew) {
693
+ const existing = await loadLocalSessionFromRecord(sessionId, record);
694
+ if (existing)
695
+ return existing;
696
+ }
697
+ const pair = (await webCrypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']));
698
+ const [privateKeyJwk, publicKeyJwk, did] = await Promise.all([
699
+ webCrypto.subtle.exportKey('jwk', pair.privateKey),
700
+ webCrypto.subtle.exportKey('jwk', pair.publicKey),
701
+ buildDidKey(pair.publicKey),
702
+ ]);
703
+ const createdAt = Date.now();
704
+ const expiresAt = normalizeExpiry(undefined, options.expiresInMs ?? DEFAULT_SESSION_TTL);
705
+ const root = shouldKeepRootForSession(record?.root, did, createdAt) ? record?.root : undefined;
706
+ await writeSessionRecord({
707
+ id: sessionId,
708
+ did,
709
+ createdAt,
710
+ expiresAt,
711
+ source: 'local',
712
+ privateKeyJwk,
713
+ publicKeyJwk,
714
+ root,
715
+ });
716
+ return {
717
+ id: sessionId,
718
+ did,
719
+ createdAt,
720
+ expiresAt,
721
+ source: 'local',
722
+ privateKey: pair.privateKey,
723
+ };
724
+ }
542
725
  function openDb() {
543
726
  if (typeof indexedDB === 'undefined') {
544
727
  return Promise.reject(new Error('IndexedDB not available'));
@@ -601,17 +784,19 @@
601
784
  }
602
785
  }
603
786
  async function getUcanSession(id = DEFAULT_SESSION_ID, provider) {
787
+ const record = await readSessionRecord(id);
604
788
  const walletProvider = provider || (typeof window !== 'undefined'
605
789
  ? await getProvider({ preferYeYing: true })
606
790
  : null);
607
- if (!walletProvider)
608
- return null;
609
- try {
610
- return await requestWalletUcanSession(walletProvider, { id });
611
- }
612
- catch {
613
- return null;
791
+ if (walletProvider) {
792
+ try {
793
+ return await requestWalletUcanSession(walletProvider, { id });
794
+ }
795
+ catch {
796
+ return await loadLocalSessionFromRecord(id, record);
797
+ }
614
798
  }
799
+ return await loadLocalSessionFromRecord(id, record);
615
800
  }
616
801
  async function requestWalletUcanSession(provider, options) {
617
802
  const sessionId = options.id || DEFAULT_SESSION_ID;
@@ -636,6 +821,7 @@
636
821
  did: result.did,
637
822
  createdAt,
638
823
  expiresAt,
824
+ source: 'wallet',
639
825
  root: existing?.root,
640
826
  };
641
827
  if (nextRecord.root && nextRecord.root.aud && nextRecord.root.aud !== nextRecord.did) {
@@ -647,6 +833,7 @@
647
833
  did: result.did,
648
834
  createdAt,
649
835
  expiresAt,
836
+ source: 'wallet',
650
837
  signer: async (signingInput, payload) => {
651
838
  const signatureResult = (await provider.request({
652
839
  method: 'yeying_ucan_sign',
@@ -669,13 +856,20 @@
669
856
  };
670
857
  }
671
858
  async function createUcanSession(options = {}) {
859
+ const sessionId = parseSessionId(options);
860
+ const record = await readSessionRecord(sessionId);
672
861
  const provider = options.provider || (typeof window !== 'undefined'
673
862
  ? await getProvider({ preferYeYing: true })
674
863
  : null);
675
- if (!provider) {
676
- throw new Error('No wallet provider for UCAN session');
864
+ if (provider) {
865
+ try {
866
+ return await requestWalletUcanSession(provider, { ...options, id: sessionId });
867
+ }
868
+ catch {
869
+ // fallback to local ed25519 session
870
+ }
677
871
  }
678
- return await requestWalletUcanSession(provider, options);
872
+ return await createLocalSession({ ...options, id: sessionId }, record);
679
873
  }
680
874
  async function clearUcanSession(id = DEFAULT_SESSION_ID) {
681
875
  await deleteSessionRecord(id);
@@ -686,6 +880,7 @@
686
880
  const expiresAt = record?.expiresAt ?? null;
687
881
  const did = record?.did || root.aud;
688
882
  const nextRecord = {
883
+ ...(record || {}),
689
884
  id,
690
885
  did,
691
886
  createdAt,
@@ -699,7 +894,9 @@
699
894
  return record?.root || null;
700
895
  }
701
896
  function capsEqual(a, b) {
702
- return JSON.stringify(a || []) === JSON.stringify(b || []);
897
+ const left = normalizeUcanCapabilities(a, { includeLegacyAliases: false });
898
+ const right = normalizeUcanCapabilities(b, { includeLegacyAliases: false });
899
+ return JSON.stringify(left) === JSON.stringify(right);
703
900
  }
704
901
  function isRootExpired(root, nowMs) {
705
902
  return Boolean(root.exp && nowMs > root.exp);
@@ -774,9 +971,13 @@
774
971
  const nonce = options.nonce || randomNonce(8);
775
972
  const exp = normalizeExpiry(undefined, options.expiresInMs ?? DEFAULT_SESSION_TTL);
776
973
  const nbf = options.notBeforeMs;
974
+ const normalizedCapabilities = normalizeUcanCapabilities(options.capabilities);
975
+ if (!normalizedCapabilities.length) {
976
+ throw new Error('Missing UCAN capabilities');
977
+ }
777
978
  const statementPayload = {
778
979
  aud: session.did,
779
- cap: options.capabilities,
980
+ cap: normalizedCapabilities,
780
981
  exp,
781
982
  };
782
983
  if (nbf)
@@ -799,7 +1000,7 @@
799
1000
  type: 'siwe',
800
1001
  iss: `did:pkh:eth:${address.toLowerCase()}`,
801
1002
  aud: session.did,
802
- cap: options.capabilities,
1003
+ cap: normalizedCapabilities,
803
1004
  exp,
804
1005
  nbf,
805
1006
  siwe: {
@@ -848,11 +1049,15 @@
848
1049
  }));
849
1050
  if (!issuer)
850
1051
  throw new Error('Missing UCAN session key');
1052
+ const normalizedCapabilities = normalizeUcanCapabilities(options.capabilities);
1053
+ if (!normalizedCapabilities.length) {
1054
+ throw new Error('Missing UCAN capabilities');
1055
+ }
851
1056
  const exp = normalizeExpiry(undefined, options.expiresInMs ?? DEFAULT_UCAN_TTL);
852
1057
  const payload = {
853
1058
  iss: issuer.did,
854
1059
  aud: options.audience,
855
- cap: options.capabilities,
1060
+ cap: normalizedCapabilities,
856
1061
  exp,
857
1062
  nbf: options.notBeforeMs,
858
1063
  prf: await resolveProofs(options, issuer),
@@ -866,11 +1071,15 @@
866
1071
  }));
867
1072
  if (!issuer)
868
1073
  throw new Error('Missing UCAN session key');
1074
+ const normalizedCapabilities = normalizeUcanCapabilities(options.capabilities);
1075
+ if (!normalizedCapabilities.length) {
1076
+ throw new Error('Missing UCAN capabilities');
1077
+ }
869
1078
  const exp = normalizeExpiry(undefined, options.expiresInMs ?? DEFAULT_UCAN_TTL);
870
1079
  const payload = {
871
1080
  iss: issuer.did,
872
1081
  aud: options.audience,
873
- cap: options.capabilities,
1082
+ cap: normalizedCapabilities,
874
1083
  exp,
875
1084
  nbf: options.notBeforeMs,
876
1085
  prf: await resolveProofs(options, issuer),
@@ -903,6 +1112,291 @@
903
1112
  });
904
1113
  }
905
1114
 
1115
+ const DEFAULT_BASE_URL = '/api/v1/public/auth/central';
1116
+ const DEFAULT_ISSUER_PATH = 'issuer';
1117
+ const DEFAULT_SESSION_PATH = 'session';
1118
+ const DEFAULT_ISSUE_PATH = 'issue';
1119
+ const DEFAULT_SESSION_TOKEN_KEY = 'centralUcanSessionToken';
1120
+ let cachedCentralSessionToken = null;
1121
+ function normalizeBaseUrl$1(baseUrl) {
1122
+ return baseUrl.replace(/\/+$/, '');
1123
+ }
1124
+ function joinUrl$1(baseUrl, path) {
1125
+ const trimmed = path.replace(/^\/+/, '');
1126
+ return `${normalizeBaseUrl$1(baseUrl)}/${trimmed}`;
1127
+ }
1128
+ function resolveBaseUrl(options) {
1129
+ return options?.baseUrl || DEFAULT_BASE_URL;
1130
+ }
1131
+ function resolveFetcher(options) {
1132
+ return options?.fetcher || fetch;
1133
+ }
1134
+ function resolveCredentials(options) {
1135
+ return options?.credentials ?? 'include';
1136
+ }
1137
+ function resolveSessionTokenKey(options) {
1138
+ return options?.sessionTokenStorageKey || DEFAULT_SESSION_TOKEN_KEY;
1139
+ }
1140
+ function shouldStoreSessionToken(options) {
1141
+ return options?.storeSessionToken !== false;
1142
+ }
1143
+ function parseObject(value) {
1144
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
1145
+ return {};
1146
+ }
1147
+ return value;
1148
+ }
1149
+ function parseEnvelopeData(payload) {
1150
+ const root = parseObject(payload);
1151
+ if (Object.prototype.hasOwnProperty.call(root, 'data')) {
1152
+ return parseObject(root.data);
1153
+ }
1154
+ return root;
1155
+ }
1156
+ function parseStringField(obj, keys) {
1157
+ for (const key of keys) {
1158
+ const value = obj[key];
1159
+ if (typeof value === 'string') {
1160
+ return value;
1161
+ }
1162
+ }
1163
+ return undefined;
1164
+ }
1165
+ function parseNumberField(obj, keys) {
1166
+ for (const key of keys) {
1167
+ const value = obj[key];
1168
+ if (typeof value === 'number' && Number.isFinite(value)) {
1169
+ return value;
1170
+ }
1171
+ }
1172
+ return undefined;
1173
+ }
1174
+ function parseCapabilitiesField(obj, keys) {
1175
+ for (const key of keys) {
1176
+ const value = obj[key];
1177
+ if (!Array.isArray(value))
1178
+ continue;
1179
+ const caps = value
1180
+ .filter(item => item && typeof item === 'object')
1181
+ .map(item => normalizeUcanCapability(item))
1182
+ .filter((cap) => Boolean(cap));
1183
+ return caps;
1184
+ }
1185
+ return undefined;
1186
+ }
1187
+ function readStoredSessionToken(options) {
1188
+ if (!shouldStoreSessionToken(options))
1189
+ return null;
1190
+ if (typeof localStorage === 'undefined')
1191
+ return null;
1192
+ const key = resolveSessionTokenKey(options);
1193
+ return localStorage.getItem(key);
1194
+ }
1195
+ function persistSessionToken(token, options) {
1196
+ cachedCentralSessionToken = token;
1197
+ if (!shouldStoreSessionToken(options))
1198
+ return;
1199
+ if (typeof localStorage === 'undefined')
1200
+ return;
1201
+ const key = resolveSessionTokenKey(options);
1202
+ if (!token) {
1203
+ localStorage.removeItem(key);
1204
+ }
1205
+ else {
1206
+ localStorage.setItem(key, token);
1207
+ }
1208
+ }
1209
+ async function parseJsonBody(response) {
1210
+ const text = await response.text();
1211
+ if (!text)
1212
+ return null;
1213
+ try {
1214
+ return JSON.parse(text);
1215
+ }
1216
+ catch {
1217
+ return { raw: text };
1218
+ }
1219
+ }
1220
+ function getCentralSessionToken(options) {
1221
+ if (cachedCentralSessionToken)
1222
+ return cachedCentralSessionToken;
1223
+ const stored = readStoredSessionToken(options);
1224
+ if (stored) {
1225
+ cachedCentralSessionToken = stored;
1226
+ }
1227
+ return stored;
1228
+ }
1229
+ function setCentralSessionToken(token, options) {
1230
+ persistSessionToken(token, options);
1231
+ }
1232
+ function clearCentralSessionToken(options) {
1233
+ cachedCentralSessionToken = null;
1234
+ if (typeof localStorage === 'undefined')
1235
+ return;
1236
+ const key = resolveSessionTokenKey(options);
1237
+ localStorage.removeItem(key);
1238
+ }
1239
+ async function getCentralIssuerInfo(options = {}) {
1240
+ const fetcher = resolveFetcher(options);
1241
+ const credentials = resolveCredentials(options);
1242
+ const url = joinUrl$1(resolveBaseUrl(options), options.issuerPath || DEFAULT_ISSUER_PATH);
1243
+ const response = await fetcher(url, {
1244
+ method: 'GET',
1245
+ headers: {
1246
+ accept: 'application/json',
1247
+ },
1248
+ credentials,
1249
+ });
1250
+ const payload = await parseJsonBody(response);
1251
+ if (!response.ok) {
1252
+ throw new Error(`Central issuer request failed: ${response.status} ${JSON.stringify(payload)}`);
1253
+ }
1254
+ const data = parseEnvelopeData(payload);
1255
+ return {
1256
+ enabled: typeof data.enabled === 'boolean' ? data.enabled : undefined,
1257
+ issuerDid: parseStringField(data, ['issuerDid']),
1258
+ defaultAudience: parseStringField(data, ['defaultAudience']),
1259
+ defaultCapabilities: parseCapabilitiesField(data, ['defaultCapabilities']),
1260
+ response: payload,
1261
+ };
1262
+ }
1263
+ async function createCentralSession(options) {
1264
+ const subject = String(options?.subject || '').trim();
1265
+ if (!subject) {
1266
+ throw new Error('Missing subject');
1267
+ }
1268
+ const fetcher = resolveFetcher(options);
1269
+ const credentials = resolveCredentials(options);
1270
+ const url = joinUrl$1(resolveBaseUrl(options), options.sessionPath || DEFAULT_SESSION_PATH);
1271
+ const response = await fetcher(url, {
1272
+ method: 'POST',
1273
+ headers: {
1274
+ 'Content-Type': 'application/json',
1275
+ accept: 'application/json',
1276
+ },
1277
+ credentials,
1278
+ body: JSON.stringify({
1279
+ subject,
1280
+ sessionTtlMs: options.sessionTtlMs,
1281
+ }),
1282
+ });
1283
+ const payload = await parseJsonBody(response);
1284
+ if (!response.ok) {
1285
+ throw new Error(`Central session request failed: ${response.status} ${JSON.stringify(payload)}`);
1286
+ }
1287
+ const data = parseEnvelopeData(payload);
1288
+ const sessionToken = parseStringField(data, ['sessionToken']);
1289
+ if (!sessionToken) {
1290
+ throw new Error('Central session response missing sessionToken');
1291
+ }
1292
+ persistSessionToken(sessionToken, options);
1293
+ return {
1294
+ subject: parseStringField(data, ['subject']) || subject,
1295
+ sessionToken,
1296
+ expiresAt: parseNumberField(data, ['expiresAt']),
1297
+ issuerDid: parseStringField(data, ['issuerDid']),
1298
+ response: payload,
1299
+ };
1300
+ }
1301
+ async function issueCentralUcan(options = {}) {
1302
+ const sessionToken = options.sessionToken || getCentralSessionToken(options);
1303
+ if (!sessionToken) {
1304
+ throw new Error('Missing central session token');
1305
+ }
1306
+ const normalizedCapabilities = options.capabilities
1307
+ ? normalizeUcanCapabilities(options.capabilities)
1308
+ : undefined;
1309
+ const fetcher = resolveFetcher(options);
1310
+ const credentials = resolveCredentials(options);
1311
+ const url = joinUrl$1(resolveBaseUrl(options), options.issuePath || DEFAULT_ISSUE_PATH);
1312
+ const response = await fetcher(url, {
1313
+ method: 'POST',
1314
+ headers: {
1315
+ 'Content-Type': 'application/json',
1316
+ accept: 'application/json',
1317
+ Authorization: `Bearer ${sessionToken}`,
1318
+ },
1319
+ credentials,
1320
+ body: JSON.stringify({
1321
+ audience: options.audience,
1322
+ capabilities: normalizedCapabilities,
1323
+ expiresInMs: options.expiresInMs,
1324
+ ttlMs: options.ttlMs,
1325
+ }),
1326
+ });
1327
+ const payload = await parseJsonBody(response);
1328
+ if (!response.ok) {
1329
+ throw new Error(`Central UCAN issue failed: ${response.status} ${JSON.stringify(payload)}`);
1330
+ }
1331
+ const data = parseEnvelopeData(payload);
1332
+ const ucan = parseStringField(data, ['ucan']);
1333
+ if (!ucan) {
1334
+ throw new Error('Central UCAN response missing ucan');
1335
+ }
1336
+ return {
1337
+ ucan,
1338
+ issuerDid: parseStringField(data, ['issuerDid']),
1339
+ subject: parseStringField(data, ['subject']),
1340
+ audience: parseStringField(data, ['audience']),
1341
+ capabilities: parseCapabilitiesField(data, ['capabilities']),
1342
+ exp: parseNumberField(data, ['exp']),
1343
+ nbf: parseNumberField(data, ['nbf']),
1344
+ iat: parseNumberField(data, ['iat']),
1345
+ response: payload,
1346
+ };
1347
+ }
1348
+ async function createAndIssueCentralUcan(options) {
1349
+ const session = await createCentralSession({
1350
+ ...options,
1351
+ subject: options.subject,
1352
+ sessionTtlMs: options.sessionTtlMs,
1353
+ });
1354
+ const issue = await issueCentralUcan({
1355
+ ...options,
1356
+ sessionToken: session.sessionToken,
1357
+ audience: options.audience,
1358
+ capabilities: options.capabilities,
1359
+ expiresInMs: options.expiresInMs,
1360
+ ttlMs: options.ttlMs,
1361
+ });
1362
+ return { session, issue };
1363
+ }
1364
+ async function authCentralUcanFetch(input, init = {}, options = {}) {
1365
+ const fetcher = resolveFetcher(options);
1366
+ const credentials = resolveCredentials(options);
1367
+ let token = options.ucan || null;
1368
+ if (!token) {
1369
+ let sessionToken = options.sessionToken || getCentralSessionToken(options);
1370
+ if (!sessionToken) {
1371
+ if (!options.subject) {
1372
+ throw new Error('Missing central session token or subject');
1373
+ }
1374
+ const session = await createCentralSession({
1375
+ ...options,
1376
+ subject: options.subject,
1377
+ sessionTtlMs: options.sessionTtlMs,
1378
+ });
1379
+ sessionToken = session.sessionToken;
1380
+ }
1381
+ const issued = await issueCentralUcan({
1382
+ ...options,
1383
+ sessionToken,
1384
+ audience: options.audience,
1385
+ capabilities: options.capabilities,
1386
+ expiresInMs: options.expiresInMs,
1387
+ ttlMs: options.ttlMs,
1388
+ });
1389
+ token = issued.ucan;
1390
+ }
1391
+ const headers = new Headers(init.headers || {});
1392
+ headers.set('Authorization', `Bearer ${token}`);
1393
+ return fetcher(input, {
1394
+ ...init,
1395
+ headers,
1396
+ credentials,
1397
+ });
1398
+ }
1399
+
906
1400
  function normalizeBaseUrl(baseUrl) {
907
1401
  return baseUrl.replace(/\/+$/, '');
908
1402
  }
@@ -1119,6 +1613,7 @@
1119
1613
 
1120
1614
  const tokenCache = new Map();
1121
1615
  const TOKEN_SKEW_MS = 5000;
1616
+ const DEFAULT_APP_ACTION = 'write';
1122
1617
  function normalizeAppDir(path) {
1123
1618
  const trimmed = path.trim();
1124
1619
  if (!trimmed)
@@ -1130,6 +1625,36 @@
1130
1625
  function sanitizeAppId(appId) {
1131
1626
  return appId.trim().replace(/[^a-zA-Z0-9._-]/g, '-');
1132
1627
  }
1628
+ function normalizeAction(action) {
1629
+ const trimmed = (action || '').trim();
1630
+ return trimmed ? trimmed : null;
1631
+ }
1632
+ function buildAppCapability(options) {
1633
+ if (!options.appId)
1634
+ return null;
1635
+ const action = normalizeAction(options.appAction) || DEFAULT_APP_ACTION;
1636
+ const resource = `app:all:${sanitizeAppId(options.appId)}`;
1637
+ return {
1638
+ with: resource,
1639
+ can: action,
1640
+ resource,
1641
+ action,
1642
+ };
1643
+ }
1644
+ function hasAppCapability(caps) {
1645
+ return (caps || []).some(cap => getCapabilityResource(cap).startsWith('app:'));
1646
+ }
1647
+ function dedupeCapabilities(caps) {
1648
+ return normalizeUcanCapabilities(caps);
1649
+ }
1650
+ function ensureAppCapability(caps, options) {
1651
+ const appCap = buildAppCapability(options);
1652
+ if (!appCap)
1653
+ return caps || [];
1654
+ if (hasAppCapability(caps || []))
1655
+ return caps || [];
1656
+ return dedupeCapabilities([...(caps || []), appCap]);
1657
+ }
1133
1658
  function resolveAppDir(options) {
1134
1659
  if (options.appDir) {
1135
1660
  return normalizeAppDir(options.appDir);
@@ -1140,11 +1665,25 @@
1140
1665
  return undefined;
1141
1666
  }
1142
1667
  function buildCapsKey(caps) {
1143
- return JSON.stringify(caps || []);
1668
+ const canonical = (caps || [])
1669
+ .map(cap => ({
1670
+ with: getCapabilityResource(cap),
1671
+ can: getCapabilityAction(cap),
1672
+ }))
1673
+ .filter(cap => Boolean(cap.with && cap.can));
1674
+ return JSON.stringify(canonical);
1144
1675
  }
1145
1676
  function buildTokenCacheKey(issuer, audience, caps) {
1146
1677
  return `${issuer.did}|${audience}|${buildCapsKey(caps)}`;
1147
1678
  }
1679
+ function resolveWebdavCaps(options) {
1680
+ const baseCaps = options.capabilities || options.root?.cap || [];
1681
+ return ensureAppCapability(baseCaps, options);
1682
+ }
1683
+ function resolveInvocationCaps(options, fallbackCaps) {
1684
+ const caps = options.invocationCapabilities || fallbackCaps;
1685
+ return ensureAppCapability(caps, options);
1686
+ }
1148
1687
  function isTokenValid(entry, nowMs) {
1149
1688
  if (!entry.exp)
1150
1689
  return false;
@@ -1216,7 +1755,7 @@
1216
1755
  return token;
1217
1756
  }
1218
1757
  async function initWebDavStorage(options) {
1219
- const caps = options.capabilities || options.root?.cap;
1758
+ const caps = resolveWebdavCaps(options);
1220
1759
  if (!caps || caps.length === 0) {
1221
1760
  throw new Error('Missing UCAN capabilities for WebDAV');
1222
1761
  }
@@ -1246,7 +1785,7 @@
1246
1785
  expiresInMs: options.rootExpiresInMs,
1247
1786
  });
1248
1787
  }
1249
- const invocationCaps = options.invocationCapabilities || caps;
1788
+ const invocationCaps = resolveInvocationCaps(options, caps);
1250
1789
  const token = await getCachedInvocationToken({
1251
1790
  issuer: session,
1252
1791
  audience: options.audience,
@@ -1310,10 +1849,14 @@
1310
1849
  }
1311
1850
 
1312
1851
  exports.WebDavClient = WebDavClient;
1852
+ exports.authCentralUcanFetch = authCentralUcanFetch;
1313
1853
  exports.authFetch = authFetch;
1314
1854
  exports.authUcanFetch = authUcanFetch;
1315
1855
  exports.clearAccessToken = clearAccessToken;
1856
+ exports.clearCentralSessionToken = clearCentralSessionToken;
1316
1857
  exports.clearUcanSession = clearUcanSession;
1858
+ exports.createAndIssueCentralUcan = createAndIssueCentralUcan;
1859
+ exports.createCentralSession = createCentralSession;
1317
1860
  exports.createDelegationUcan = createDelegationUcan;
1318
1861
  exports.createInvocationUcan = createInvocationUcan;
1319
1862
  exports.createRootUcan = createRootUcan;
@@ -1322,6 +1865,10 @@
1322
1865
  exports.getAccessToken = getAccessToken;
1323
1866
  exports.getAccounts = getAccounts;
1324
1867
  exports.getBalance = getBalance;
1868
+ exports.getCapabilityAction = getCapabilityAction;
1869
+ exports.getCapabilityResource = getCapabilityResource;
1870
+ exports.getCentralIssuerInfo = getCentralIssuerInfo;
1871
+ exports.getCentralSessionToken = getCentralSessionToken;
1325
1872
  exports.getChainId = getChainId;
1326
1873
  exports.getOrCreateUcanRoot = getOrCreateUcanRoot;
1327
1874
  exports.getPreferredAccount = getPreferredAccount;
@@ -1331,14 +1878,18 @@
1331
1878
  exports.initDappSession = initDappSession;
1332
1879
  exports.initWebDavStorage = initWebDavStorage;
1333
1880
  exports.isYeYingProvider = isYeYingProvider;
1881
+ exports.issueCentralUcan = issueCentralUcan;
1334
1882
  exports.loginWithChallenge = loginWithChallenge;
1335
1883
  exports.logout = logout;
1884
+ exports.normalizeUcanCapabilities = normalizeUcanCapabilities;
1885
+ exports.normalizeUcanCapability = normalizeUcanCapability;
1336
1886
  exports.onAccountsChanged = onAccountsChanged;
1337
1887
  exports.onChainChanged = onChainChanged;
1338
1888
  exports.refreshAccessToken = refreshAccessToken;
1339
1889
  exports.requestAccounts = requestAccounts;
1340
1890
  exports.requireProvider = requireProvider;
1341
1891
  exports.setAccessToken = setAccessToken;
1892
+ exports.setCentralSessionToken = setCentralSessionToken;
1342
1893
  exports.signMessage = signMessage;
1343
1894
  exports.storeUcanRoot = storeUcanRoot;
1344
1895
  exports.watchAccounts = watchAccounts;