@naculus/connect-appkit-core 0.6.0 → 0.8.0

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 ADDED
@@ -0,0 +1,13 @@
1
+ # `@naculus/connect-appkit-core`
2
+
3
+ ## Authorization and settlement views
4
+
5
+ `describeAuthorization` builds network-free consent text from a connect-core
6
+ `Authorization` or `ListedAuthorization`. Pass asset metadata explicitly to
7
+ show symbols and decimal amounts; without it, the view deliberately shows the
8
+ asset address and base units. `explainSpend` turns every evaluator refusal into
9
+ UI text without changing the authorization decision.
10
+
11
+ `describePayment` marks every server-reported settlement as `unverified`.
12
+ `applySettlementVerification` is the only helper that changes that state, and
13
+ the settlement label becomes verified only for a `verified` result.
package/dist/index.cjs CHANGED
@@ -21,10 +21,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
23
  DEFAULT_NOTIFICATION_SETTINGS: () => DEFAULT_NOTIFICATION_SETTINGS,
24
+ DEFAULT_VERIFY_RETRY: () => DEFAULT_VERIFY_RETRY,
24
25
  InvalidUserOperationReceiptError: () => InvalidUserOperationReceiptError,
25
26
  NOTIFICATION_SETTINGS_KEY: () => NOTIFICATION_SETTINGS_KEY,
26
27
  __resetResolverForTests: () => __resetResolverForTests,
27
28
  addInAppNotification: () => addInAppNotification,
29
+ applySettlementVerification: () => applySettlementVerification,
28
30
  assertHexSignature: () => assertHexSignature,
29
31
  atomicSupportFor: () => atomicSupportFor,
30
32
  bareEvmAddress: () => bareEvmAddress,
@@ -45,13 +47,16 @@ __export(src_exports, {
45
47
  decodeRevertReason: () => decodeRevertReason,
46
48
  defaultSiwxDomain: () => defaultSiwxDomain,
47
49
  defaultSiwxUri: () => defaultSiwxUri,
50
+ describeAuthorization: () => describeAuthorization,
48
51
  describeChain: () => describeChain,
49
52
  describePayment: () => describePayment,
50
53
  eip155ChainIdToNumber: () => eip155ChainIdToNumber,
51
54
  encodeErc20Approve: () => encodeErc20Approve,
52
55
  encodeErc20Transfer: () => encodeErc20Transfer,
53
56
  encodeErc20TransferFrom: () => encodeErc20TransferFrom,
57
+ explainSpend: () => explainSpend,
54
58
  fetchUserOperationReceipt: () => fetchUserOperationReceipt,
59
+ formatAuthorizationAmount: () => formatAuthorizationAmount,
55
60
  getResolver: () => getResolver,
56
61
  getSharedSessionKeyManager: () => getSharedSessionKeyManager,
57
62
  initialWeb3State: () => initialWeb3State,
@@ -67,8 +72,13 @@ __export(src_exports, {
67
72
  normalizeCapabilities: () => normalizeCapabilities,
68
73
  normalizeEip155ChainId: () => normalizeEip155ChainId,
69
74
  parseUserOperationReceipt: () => parseUserOperationReceipt,
75
+ readCurrentMppSessionChannel: () => readCurrentMppSessionChannel,
70
76
  readDecimals: () => readDecimals,
77
+ readMppRecoveryChannelId: () => readMppRecoveryChannelId,
78
+ readMppSessionChannel: () => readMppSessionChannel,
79
+ readMppSessionReceipt: () => readMppSessionReceipt,
71
80
  readSolanaRoles: () => readSolanaRoles,
81
+ replayNotificationSettings: () => replayNotificationSettings,
72
82
  resetSharedSessionKeyManager: () => resetSharedSessionKeyManager,
73
83
  resolveChain: () => resolveChain,
74
84
  resolveContextSimulationEndpoint: () => resolveContextSimulationEndpoint,
@@ -573,6 +583,13 @@ function saveNotificationSettings(storage, settings) {
573
583
  function updateNotificationSettings(current, update) {
574
584
  return { ...current, ...update };
575
585
  }
586
+ function replayNotificationSettings(persisted, updates) {
587
+ let settings = persisted ?? DEFAULT_NOTIFICATION_SETTINGS;
588
+ for (const update of updates) {
589
+ settings = updateNotificationSettings(settings, update(settings));
590
+ }
591
+ return settings;
592
+ }
576
593
  function channelsUpdate(channels) {
577
594
  const update = {};
578
595
  if (channels.includes("telegram")) update.telegram = true;
@@ -1362,7 +1379,125 @@ function createDelegationPolicyFlow(deps) {
1362
1379
  };
1363
1380
  }
1364
1381
 
1382
+ // src/authorization.ts
1383
+ var import_connect_core17 = require("@naculus/connect-core");
1384
+ function assetAddress(asset) {
1385
+ return asset.split(/[:/]/).at(-1);
1386
+ }
1387
+ function groupDigits(value) {
1388
+ return value.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1389
+ }
1390
+ function formatAuthorizationAmount(amount, metadata) {
1391
+ const baseUnits = amount.toString();
1392
+ if (!metadata || !metadata.symbol || !Number.isSafeInteger(metadata.decimals) || metadata.decimals < 0) {
1393
+ return {
1394
+ formatted: `${groupDigits(baseUnits)} base units`
1395
+ };
1396
+ }
1397
+ const decimals = metadata.decimals;
1398
+ const padded = baseUnits.padStart(decimals + 1, "0");
1399
+ const integer = decimals === 0 ? padded : padded.slice(0, -decimals);
1400
+ const fraction = decimals === 0 ? "" : padded.slice(-decimals).replace(/0+$/, "");
1401
+ const value = `${groupDigits(integer)}${fraction ? `.${fraction}` : ""}`;
1402
+ return { formatted: `${value} ${metadata.symbol}` };
1403
+ }
1404
+ function relativeExpiry(expiresAt, now, locale) {
1405
+ const seconds = expiresAt - now;
1406
+ const absolute = Math.abs(seconds);
1407
+ const divisor = absolute >= 31536e3 ? 31536e3 : absolute >= 2592e3 ? 2592e3 : absolute >= 86400 ? 86400 : absolute >= 3600 ? 3600 : absolute >= 60 ? 60 : 1;
1408
+ const unit = divisor === 31536e3 ? "year" : divisor === 2592e3 ? "month" : divisor === 86400 ? "day" : divisor === 3600 ? "hour" : divisor === 60 ? "minute" : "second";
1409
+ return new Intl.RelativeTimeFormat(locale).format(
1410
+ Math.round(seconds / divisor),
1411
+ unit
1412
+ );
1413
+ }
1414
+ function describeAuthorization(input, options = {}) {
1415
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
1416
+ const locale = options.locale ?? "en";
1417
+ const flags = "flags" in input ? input.flags : [];
1418
+ const label = "label" in input ? input.label ?? null : null;
1419
+ return {
1420
+ label,
1421
+ principal: input.principal ?? null,
1422
+ grants: input.grants.map((grant) => {
1423
+ const metadata = options.assets?.[grant.asset];
1424
+ return {
1425
+ asset: grant.asset,
1426
+ assetLabel: metadata?.symbol || assetAddress(grant.asset),
1427
+ recipients: grant.recipients.map((full) => ({
1428
+ shortened: full.length <= 14 ? full : `${full.slice(0, 6)}\u2026${full.slice(-6)}`,
1429
+ full
1430
+ })),
1431
+ perPayment: formatAuthorizationAmount(grant.maxPerPayment, metadata),
1432
+ total: formatAuthorizationAmount(grant.maxTotal, metadata),
1433
+ count: grant.maxCount ?? null,
1434
+ rails: grant.rails.map((r) => r.replace("-", " "))
1435
+ };
1436
+ }),
1437
+ expiry: {
1438
+ timestamp: input.expiresAt,
1439
+ absolute: new Intl.DateTimeFormat(locale, {
1440
+ year: "numeric",
1441
+ month: "short",
1442
+ day: "numeric",
1443
+ hour: "2-digit",
1444
+ minute: "2-digit",
1445
+ hourCycle: "h23",
1446
+ timeZone: "UTC",
1447
+ timeZoneName: "short"
1448
+ }).format(input.expiresAt * 1e3),
1449
+ relative: relativeExpiry(input.expiresAt, now, locale)
1450
+ },
1451
+ warnings: flags.map(
1452
+ (flag) => flag === "unrestricted-recipient-legacy" ? "This key can pay any recipient." : "This key can do more than shown here."
1453
+ )
1454
+ };
1455
+ }
1456
+ var REFUSAL_SENTENCES = {
1457
+ expired: "This authorization has expired.",
1458
+ "not-yet-valid": "This authorization is not valid yet.",
1459
+ "no-matching-grant": "No grant covers this asset.",
1460
+ "recipient-not-allowed": "This recipient is not allowed.",
1461
+ "over-per-payment": "This amount exceeds the per-payment limit.",
1462
+ "over-total": "This amount exceeds the remaining total limit.",
1463
+ "over-count": "This authorization has reached its payment count limit.",
1464
+ "rail-not-allowed": "This payment rail is not allowed.",
1465
+ "invalid-authorization": "The authorization or spend request is invalid."
1466
+ };
1467
+ function explainSpend(authorization, request, options = {}) {
1468
+ const verdict = (0, import_connect_core17.evaluateSpend)(authorization, request);
1469
+ if (!verdict.allow) {
1470
+ return {
1471
+ allowed: false,
1472
+ reason: verdict.reason,
1473
+ sentence: REFUSAL_SENTENCES[verdict.reason]
1474
+ };
1475
+ }
1476
+ const remainingTotal = authorization.grants[verdict.grant].maxTotal - request.spentSoFar - request.amount;
1477
+ const remainingTotalFormatted = formatAuthorizationAmount(
1478
+ remainingTotal,
1479
+ options.assets?.[authorization.grants[verdict.grant].asset]
1480
+ );
1481
+ return {
1482
+ allowed: true,
1483
+ grant: verdict.grant,
1484
+ remainingTotal,
1485
+ remainingTotalFormatted,
1486
+ sentence: `Allowed by grant ${verdict.grant + 1}; ${remainingTotalFormatted.formatted} remain.`
1487
+ };
1488
+ }
1489
+
1365
1490
  // src/payment-fetch.ts
1491
+ var DEFAULT_VERIFY_RETRY = { attempts: 5, delayMs: 4e3 };
1492
+ function applySettlementVerification(payment, result) {
1493
+ return {
1494
+ ...payment,
1495
+ verification: {
1496
+ status: result.status,
1497
+ ..."reason" in result && result.reason !== void 0 ? { reason: result.reason } : {}
1498
+ }
1499
+ };
1500
+ }
1366
1501
  function record(value) {
1367
1502
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1368
1503
  }
@@ -1392,7 +1527,8 @@ function describePayment(input, result) {
1392
1527
  payTo: text(paid.payTo),
1393
1528
  amount: text(paid.amount),
1394
1529
  // A hash reported for another chain is not this payment's.
1395
- reference: settlement?.success === true && settlement.network === paid.network ? text(settlement.transaction) : null
1530
+ reference: settlement?.success === true && settlement.network === paid.network ? text(settlement.transaction) : null,
1531
+ verification: { status: "unverified" }
1396
1532
  };
1397
1533
  }
1398
1534
  const challenge = record(record(paid.challenge)?.params);
@@ -1406,7 +1542,8 @@ function describePayment(input, result) {
1406
1542
  asset: text(request.currency),
1407
1543
  payTo: text(request.recipient),
1408
1544
  amount: text(request.amount),
1409
- reference: text(record(result.receipt)?.reference)
1545
+ reference: text(record(result.receipt)?.reference),
1546
+ verification: { status: "unverified" }
1410
1547
  };
1411
1548
  }
1412
1549
  return {
@@ -1416,16 +1553,47 @@ function describePayment(input, result) {
1416
1553
  asset: null,
1417
1554
  payTo: null,
1418
1555
  amount: null,
1419
- reference: null
1556
+ reference: null,
1557
+ verification: { status: "unverified" }
1420
1558
  };
1421
1559
  }
1422
1560
 
1561
+ // src/mpp-session.ts
1562
+ function record2(value) {
1563
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
1564
+ }
1565
+ function readMppSessionChannel(value) {
1566
+ const channel = record2(value);
1567
+ if (!channel || typeof channel.channelId !== "string" || typeof channel.payer !== "string" || typeof channel.payee !== "string" || typeof channel.mint !== "string" || typeof channel.channelProgram !== "string" || typeof channel.deposit !== "bigint" || typeof channel.gracePeriodSeconds !== "number" || typeof channel.openSlot !== "bigint" || typeof channel.salt !== "bigint") {
1568
+ return null;
1569
+ }
1570
+ return channel;
1571
+ }
1572
+ function readCurrentMppSessionChannel(session) {
1573
+ for (let index = session.channels.length - 1; index >= 0; index--) {
1574
+ const channel = readMppSessionChannel(session.channels[index]);
1575
+ if (channel) return channel;
1576
+ }
1577
+ return null;
1578
+ }
1579
+ function readMppSessionReceipt(value) {
1580
+ const receipt = record2(value);
1581
+ if (receipt?.status !== "success" || typeof receipt.method !== "string" || typeof receipt.timestamp !== "string" || typeof receipt.reference !== "string") {
1582
+ return null;
1583
+ }
1584
+ return receipt;
1585
+ }
1586
+ function readMppRecoveryChannelId(error) {
1587
+ const channelId = record2(error)?.channelId;
1588
+ return typeof channelId === "string" && channelId !== "" ? channelId : null;
1589
+ }
1590
+
1423
1591
  // src/solana-session-key.ts
1424
- var import_connect_core17 = require("@naculus/connect-core");
1592
+ var import_connect_core18 = require("@naculus/connect-core");
1425
1593
  function send(rpc) {
1426
1594
  const sendTransaction = rpc.sendTransaction;
1427
1595
  if (!sendTransaction) {
1428
- throw new import_connect_core17.WalletError(
1596
+ throw new import_connect_core18.WalletError(
1429
1597
  "invalid_input",
1430
1598
  "The Solana RPC must implement sendTransaction to approve or revoke."
1431
1599
  );
@@ -1463,7 +1631,7 @@ function createSolanaSessionKeyFlow(deps) {
1463
1631
  } catch (cause) {
1464
1632
  await manager.revoke(draft.id).catch(() => {
1465
1633
  });
1466
- throw new import_connect_core17.WalletError(
1634
+ throw new import_connect_core18.WalletError(
1467
1635
  "rpc_error",
1468
1636
  `The approval could not be confirmed as sent (${cause instanceof Error ? cause.message : String(cause)}); the key is revoked here. Call revoke("${draft.id}") to send the on-chain Revoke in case it landed.`
1469
1637
  );
@@ -1474,7 +1642,7 @@ function createSolanaSessionKeyFlow(deps) {
1474
1642
  async revoke(id) {
1475
1643
  const key = (await list()).find((k) => k.id === id);
1476
1644
  if (!key) {
1477
- throw new import_connect_core17.WalletError(
1645
+ throw new import_connect_core18.WalletError(
1478
1646
  "invalid_input",
1479
1647
  `No Solana session key ${id} for this owner.`
1480
1648
  );
@@ -1486,7 +1654,7 @@ function createSolanaSessionKeyFlow(deps) {
1486
1654
  rpc
1487
1655
  );
1488
1656
  const signed = await owner.signTransaction(transaction);
1489
- const revocation = (0, import_connect_core17.verifySignedOwnerTransaction)(signed, {
1657
+ const revocation = (0, import_connect_core18.verifySignedOwnerTransaction)(signed, {
1490
1658
  kind: "revoke",
1491
1659
  revocation: {
1492
1660
  owner: key.owner,
@@ -1501,7 +1669,7 @@ function createSolanaSessionKeyFlow(deps) {
1501
1669
  }
1502
1670
 
1503
1671
  // src/web3-store.ts
1504
- var import_connect_core18 = require("@naculus/connect-core");
1672
+ var import_connect_core19 = require("@naculus/connect-core");
1505
1673
  function withTimeout(promise, ms, label = "Operation") {
1506
1674
  let timeoutId;
1507
1675
  const timeout = new Promise((_, reject) => {
@@ -1523,7 +1691,7 @@ async function withRetry(fn, options) {
1523
1691
  lastError = error;
1524
1692
  if (attempt < options.maxRetries) {
1525
1693
  const delay = options.baseDelay * 2 ** attempt;
1526
- import_connect_core18.logger.warn(
1694
+ import_connect_core19.logger.warn(
1527
1695
  "appkit/core",
1528
1696
  `Retry attempt ${attempt + 1}/${options.maxRetries} after ${delay}ms`
1529
1697
  );
@@ -1638,10 +1806,12 @@ function siwxResultToSession(result, now = /* @__PURE__ */ new Date()) {
1638
1806
  // Annotate the CommonJS export names for ESM import in node:
1639
1807
  0 && (module.exports = {
1640
1808
  DEFAULT_NOTIFICATION_SETTINGS,
1809
+ DEFAULT_VERIFY_RETRY,
1641
1810
  InvalidUserOperationReceiptError,
1642
1811
  NOTIFICATION_SETTINGS_KEY,
1643
1812
  __resetResolverForTests,
1644
1813
  addInAppNotification,
1814
+ applySettlementVerification,
1645
1815
  assertHexSignature,
1646
1816
  atomicSupportFor,
1647
1817
  bareEvmAddress,
@@ -1662,13 +1832,16 @@ function siwxResultToSession(result, now = /* @__PURE__ */ new Date()) {
1662
1832
  decodeRevertReason,
1663
1833
  defaultSiwxDomain,
1664
1834
  defaultSiwxUri,
1835
+ describeAuthorization,
1665
1836
  describeChain,
1666
1837
  describePayment,
1667
1838
  eip155ChainIdToNumber,
1668
1839
  encodeErc20Approve,
1669
1840
  encodeErc20Transfer,
1670
1841
  encodeErc20TransferFrom,
1842
+ explainSpend,
1671
1843
  fetchUserOperationReceipt,
1844
+ formatAuthorizationAmount,
1672
1845
  getResolver,
1673
1846
  getSharedSessionKeyManager,
1674
1847
  initialWeb3State,
@@ -1684,8 +1857,13 @@ function siwxResultToSession(result, now = /* @__PURE__ */ new Date()) {
1684
1857
  normalizeCapabilities,
1685
1858
  normalizeEip155ChainId,
1686
1859
  parseUserOperationReceipt,
1860
+ readCurrentMppSessionChannel,
1687
1861
  readDecimals,
1862
+ readMppRecoveryChannelId,
1863
+ readMppSessionChannel,
1864
+ readMppSessionReceipt,
1688
1865
  readSolanaRoles,
1866
+ replayNotificationSettings,
1689
1867
  resetSharedSessionKeyManager,
1690
1868
  resolveChain,
1691
1869
  resolveContextSimulationEndpoint,