tapimo 0.19.0 → 0.20.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.
Files changed (494) hide show
  1. package/README.md +13 -5
  2. package/dist/ApiKey.d.ts +2 -2
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +2 -2
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +85 -11
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +218 -56
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +12543 -13912
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +4 -5
  13. package/dist/App.js.map +1 -1
  14. package/dist/Scope.d.ts +12 -20
  15. package/dist/Scope.d.ts.map +1 -1
  16. package/dist/Scope.js +20 -36
  17. package/dist/Scope.js.map +1 -1
  18. package/dist/admin/App.d.ts +871 -828
  19. package/dist/admin/App.d.ts.map +1 -1
  20. package/dist/admin/App.js +14 -26
  21. package/dist/admin/App.js.map +1 -1
  22. package/dist/admin/apps/api-keys.d.ts +93 -186
  23. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  24. package/dist/admin/apps/api-keys.js +3 -5
  25. package/dist/admin/apps/api-keys.js.map +1 -1
  26. package/dist/admin/apps/audit-logs.d.ts +18 -18
  27. package/dist/admin/apps/audit-logs.js +1 -1
  28. package/dist/admin/apps/audit-logs.js.map +1 -1
  29. package/dist/admin/apps/earn-vaults.d.ts +27 -27
  30. package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
  31. package/dist/admin/apps/earn-vaults.js +11 -5
  32. package/dist/admin/apps/earn-vaults.js.map +1 -1
  33. package/dist/admin/apps/organizations.d.ts +17 -17
  34. package/dist/admin/apps/prop-amms.d.ts +4 -4
  35. package/dist/admin/apps/routes-support.js +1 -38
  36. package/dist/admin/apps/routes-support.js.map +1 -1
  37. package/dist/admin/apps/routes.d.ts +153 -153
  38. package/dist/admin/apps/sentinel.d.ts +4 -4
  39. package/dist/admin/apps/verified-tokens.d.ts +24 -24
  40. package/dist/admin/apps/zone-delegation-requests.d.ts +332 -0
  41. package/dist/admin/apps/zone-delegation-requests.d.ts.map +1 -0
  42. package/dist/admin/apps/zone-delegation-requests.js +200 -0
  43. package/dist/admin/apps/zone-delegation-requests.js.map +1 -0
  44. package/dist/admin/apps/zone-delegations.d.ts +512 -52
  45. package/dist/admin/apps/zone-delegations.d.ts.map +1 -1
  46. package/dist/admin/apps/zone-delegations.js +134 -44
  47. package/dist/admin/apps/zone-delegations.js.map +1 -1
  48. package/dist/admin/index.d.ts +1 -1
  49. package/dist/admin/index.d.ts.map +1 -1
  50. package/dist/admin/index.js +1 -1
  51. package/dist/admin/index.js.map +1 -1
  52. package/dist/admin/ui.generated.d.ts +1 -1
  53. package/dist/admin/ui.generated.d.ts.map +1 -1
  54. package/dist/admin/ui.generated.js +341 -397
  55. package/dist/admin/ui.generated.js.map +1 -1
  56. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  57. package/dist/analytics/tables/requestEvents.js +4 -2
  58. package/dist/analytics/tables/requestEvents.js.map +1 -1
  59. package/dist/apps/data/App.d.ts +11932 -13214
  60. package/dist/apps/data/App.d.ts.map +1 -1
  61. package/dist/apps/data/App.js +32 -13
  62. package/dist/apps/data/App.js.map +1 -1
  63. package/dist/apps/data/ZoneSelection.d.ts +1 -1
  64. package/dist/apps/data/ZoneSelection.d.ts.map +1 -1
  65. package/dist/apps/data/ZoneSelection.js +2 -2
  66. package/dist/apps/data/ZoneSelection.js.map +1 -1
  67. package/dist/apps/data/routes/activities.d.ts +60 -27
  68. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  69. package/dist/apps/data/routes/activities.js +91 -16
  70. package/dist/apps/data/routes/activities.js.map +1 -1
  71. package/dist/apps/data/routes/addresses.d.ts +893 -875
  72. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  73. package/dist/apps/data/routes/balances.d.ts +36 -36
  74. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  75. package/dist/apps/data/routes/balances.js +3 -1
  76. package/dist/apps/data/routes/balances.js.map +1 -1
  77. package/dist/apps/data/routes/blocks.d.ts +40 -40
  78. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  79. package/dist/apps/data/routes/blocks.js +3 -7
  80. package/dist/apps/data/routes/blocks.js.map +1 -1
  81. package/dist/apps/data/routes/coingecko.d.ts +236 -236
  82. package/dist/apps/data/routes/earn-transactions.d.ts +14 -14
  83. package/dist/apps/data/routes/earn.d.ts +261 -1445
  84. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  85. package/dist/apps/data/routes/earn.js +64 -490
  86. package/dist/apps/data/routes/earn.js.map +1 -1
  87. package/dist/apps/data/routes/exchanges.d.ts +144 -144
  88. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  89. package/dist/apps/data/routes/exchanges.js +5 -15
  90. package/dist/apps/data/routes/exchanges.js.map +1 -1
  91. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  92. package/dist/apps/data/routes/fee-amm.js +3 -9
  93. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  94. package/dist/apps/data/routes/indexer.d.ts +13 -13
  95. package/dist/apps/data/routes/receipts.d.ts +53 -53
  96. package/dist/apps/data/routes/receipts.js +1 -1
  97. package/dist/apps/data/routes/receipts.js.map +1 -1
  98. package/dist/apps/data/routes/rpc.d.ts +52 -0
  99. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  100. package/dist/apps/data/routes/rpc.js +19 -18
  101. package/dist/apps/data/routes/rpc.js.map +1 -1
  102. package/dist/apps/data/routes/tokenlist.d.ts +14 -14
  103. package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
  104. package/dist/apps/data/routes/tokenlist.js +1 -3
  105. package/dist/apps/data/routes/tokenlist.js.map +1 -1
  106. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  107. package/dist/apps/data/routes/tokens.js +7 -5
  108. package/dist/apps/data/routes/tokens.js.map +1 -1
  109. package/dist/apps/data/routes/transactions.d.ts +281 -281
  110. package/dist/apps/data/routes/transactions.js +3 -3
  111. package/dist/apps/data/routes/transactions.js.map +1 -1
  112. package/dist/apps/data/routes/transfers.d.ts +12 -12
  113. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  114. package/dist/apps/data/routes/transfers.js +1 -3
  115. package/dist/apps/data/routes/transfers.js.map +1 -1
  116. package/dist/apps/data/routes/valuation.d.ts +30 -30
  117. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  118. package/dist/apps/data/routes/valuation.js +1 -3
  119. package/dist/apps/data/routes/valuation.js.map +1 -1
  120. package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
  121. package/dist/apps/data/routes/webhooks.d.ts +102 -102
  122. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  123. package/dist/apps/data/routes/webhooks.js +9 -23
  124. package/dist/apps/data/routes/webhooks.js.map +1 -1
  125. package/dist/apps/data/routes/zone-deposits.d.ts +16 -16
  126. package/dist/apps/data/routes/zone-withdrawals.d.ts +31 -31
  127. package/dist/apps/data/routes/zones.d.ts +28 -28
  128. package/dist/apps/management/App.d.ts +18 -102
  129. package/dist/apps/management/App.d.ts.map +1 -1
  130. package/dist/apps/management/App.js +1 -1
  131. package/dist/apps/management/App.js.map +1 -1
  132. package/dist/apps/management/routes/api-keys.d.ts +253 -339
  133. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  134. package/dist/apps/management/routes/api-keys.js +25 -11
  135. package/dist/apps/management/routes/api-keys.js.map +1 -1
  136. package/dist/apps/management/routes/billing.d.ts +92 -92
  137. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  138. package/dist/apps/management/routes/billing.js +5 -13
  139. package/dist/apps/management/routes/billing.js.map +1 -1
  140. package/dist/apps/management/routes/cli.d.ts.map +1 -1
  141. package/dist/apps/management/routes/cli.js +3 -9
  142. package/dist/apps/management/routes/cli.js.map +1 -1
  143. package/dist/apps/management/routes/faucet.d.ts +16 -16
  144. package/dist/apps/management/routes/fee-payer-usage.d.ts +38 -38
  145. package/dist/apps/management/routes/fee-payer-usage.d.ts.map +1 -1
  146. package/dist/apps/management/routes/fee-payer-usage.js +2 -4
  147. package/dist/apps/management/routes/fee-payer-usage.js.map +1 -1
  148. package/dist/apps/management/routes/invitations.d.ts +52 -52
  149. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  150. package/dist/apps/management/routes/invitations.js +4 -12
  151. package/dist/apps/management/routes/invitations.js.map +1 -1
  152. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  153. package/dist/apps/management/routes/invite-links.js +6 -14
  154. package/dist/apps/management/routes/invite-links.js.map +1 -1
  155. package/dist/apps/management/routes/me.d.ts +6 -6
  156. package/dist/apps/management/routes/me.d.ts.map +1 -1
  157. package/dist/apps/management/routes/me.js +3 -7
  158. package/dist/apps/management/routes/me.js.map +1 -1
  159. package/dist/apps/management/routes/members.d.ts +84 -84
  160. package/dist/apps/management/routes/members.d.ts.map +1 -1
  161. package/dist/apps/management/routes/members.js +2 -4
  162. package/dist/apps/management/routes/members.js.map +1 -1
  163. package/dist/apps/management/routes/orgs.d.ts +44 -44
  164. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  165. package/dist/apps/management/routes/orgs.js +2 -6
  166. package/dist/apps/management/routes/orgs.js.map +1 -1
  167. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  168. package/dist/apps/management/routes/projects.js +2 -6
  169. package/dist/apps/management/routes/projects.js.map +1 -1
  170. package/dist/apps/management/routes/route-usage.d.ts +34 -34
  171. package/dist/apps/management/routes/route-usage.d.ts.map +1 -1
  172. package/dist/apps/management/routes/route-usage.js +2 -4
  173. package/dist/apps/management/routes/route-usage.js.map +1 -1
  174. package/dist/apps/management/routes/routes-access-requests.d.ts +120 -91
  175. package/dist/apps/management/routes/routes-access-requests.d.ts.map +1 -1
  176. package/dist/apps/management/routes/routes-access-requests.js +49 -9
  177. package/dist/apps/management/routes/routes-access-requests.js.map +1 -1
  178. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  179. package/dist/apps/management/routes/usage.js +8 -16
  180. package/dist/apps/management/routes/usage.js.map +1 -1
  181. package/dist/apps/management/routes/verified-token-requests.d.ts +39 -39
  182. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -1
  183. package/dist/apps/management/routes/verified-token-requests.js +3 -7
  184. package/dist/apps/management/routes/verified-token-requests.js.map +1 -1
  185. package/dist/apps/management/routes/webhooks.d.ts +2089 -2089
  186. package/dist/apps/management/routes/zone-delegations.d.ts +14 -17
  187. package/dist/apps/management/routes/zone-delegations.d.ts.map +1 -1
  188. package/dist/apps/management/routes/zone-delegations.js +1 -6
  189. package/dist/apps/management/routes/zone-delegations.js.map +1 -1
  190. package/dist/apps/mpp/App.d.ts +44 -44
  191. package/dist/apps/mpp/App.js +3 -3
  192. package/dist/apps/mpp/App.js.map +1 -1
  193. package/dist/apps/mpp/internal/Adapter.js +1 -1
  194. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  195. package/dist/apps/mpp/internal/Error.d.ts.map +1 -1
  196. package/dist/apps/mpp/internal/Error.js +1 -0
  197. package/dist/apps/mpp/internal/Error.js.map +1 -1
  198. package/dist/apps/mpp/internal/Policy.d.ts +5 -1
  199. package/dist/apps/mpp/internal/Policy.d.ts.map +1 -1
  200. package/dist/apps/mpp/internal/Policy.js +35 -13
  201. package/dist/apps/mpp/internal/Policy.js.map +1 -1
  202. package/dist/apps/relay/App.d.ts +2 -2
  203. package/dist/apps/relay/App.d.ts.map +1 -1
  204. package/dist/apps/relay/App.js +25 -16
  205. package/dist/apps/relay/App.js.map +1 -1
  206. package/dist/apps/routes/App.d.ts +311 -311
  207. package/dist/apps/routes/routes/deposit-addresses.d.ts +1099 -1099
  208. package/dist/apps/routes/routes/list.d.ts +74 -74
  209. package/dist/apps/routes/routes/transfers.d.ts +94 -94
  210. package/dist/cli/ApiKeys.d.ts +19 -26
  211. package/dist/cli/ApiKeys.d.ts.map +1 -1
  212. package/dist/db/Db.d.ts +3 -3
  213. package/dist/db/Db.d.ts.map +1 -1
  214. package/dist/db/Db.js +27 -0
  215. package/dist/db/Db.js.map +1 -1
  216. package/dist/db/Schema.d.ts +28 -62
  217. package/dist/db/Schema.d.ts.map +1 -1
  218. package/dist/db/Schema.js +18 -18
  219. package/dist/db/Schema.js.map +1 -1
  220. package/dist/db/tables/rewardRuns.d.ts +21 -0
  221. package/dist/db/tables/rewardRuns.d.ts.map +1 -1
  222. package/dist/db/tables/rewardRuns.js +19 -0
  223. package/dist/db/tables/rewardRuns.js.map +1 -1
  224. package/dist/db/tables/routesAccessRequests.d.ts +13 -0
  225. package/dist/db/tables/routesAccessRequests.d.ts.map +1 -1
  226. package/dist/db/tables/routesAccessRequests.js +12 -0
  227. package/dist/db/tables/routesAccessRequests.js.map +1 -1
  228. package/dist/db/tables/zoneDelegationRequests.d.ts +165 -0
  229. package/dist/db/tables/zoneDelegationRequests.d.ts.map +1 -0
  230. package/dist/db/tables/{zoneKeyRequests.js → zoneDelegationRequests.js} +43 -42
  231. package/dist/db/tables/zoneDelegationRequests.js.map +1 -0
  232. package/dist/db/tables/zoneDelegations.d.ts +11 -5
  233. package/dist/db/tables/zoneDelegations.d.ts.map +1 -1
  234. package/dist/db/tables/zoneDelegations.js +21 -3
  235. package/dist/db/tables/zoneDelegations.js.map +1 -1
  236. package/dist/handlers/internal/accounts.d.ts +7 -0
  237. package/dist/handlers/internal/accounts.d.ts.map +1 -0
  238. package/dist/handlers/internal/{multisig.js → accounts.js} +10 -10
  239. package/dist/handlers/internal/{multisig.js.map → accounts.js.map} +1 -1
  240. package/dist/handlers/internal/sponsorship.js +17 -17
  241. package/dist/handlers/internal/sponsorship.js.map +1 -1
  242. package/dist/handlers/relay.d.ts +6 -6
  243. package/dist/handlers/relay.d.ts.map +1 -1
  244. package/dist/handlers/relay.js +15 -15
  245. package/dist/handlers/relay.js.map +1 -1
  246. package/dist/internal/Auth.d.ts.map +1 -1
  247. package/dist/internal/Auth.js +4 -8
  248. package/dist/internal/Auth.js.map +1 -1
  249. package/dist/internal/EarnRates.d.ts.map +1 -1
  250. package/dist/internal/EarnRates.js +1 -3
  251. package/dist/internal/EarnRates.js.map +1 -1
  252. package/dist/internal/EarnVaults.js +2 -2
  253. package/dist/internal/Log.d.ts +2 -4
  254. package/dist/internal/Log.d.ts.map +1 -1
  255. package/dist/internal/Log.js +0 -3
  256. package/dist/internal/Log.js.map +1 -1
  257. package/dist/internal/Response.d.ts +1 -1
  258. package/dist/internal/Response.d.ts.map +1 -1
  259. package/dist/internal/Response.js +0 -3
  260. package/dist/internal/Response.js.map +1 -1
  261. package/dist/internal/Schema.d.ts +11 -0
  262. package/dist/internal/Schema.d.ts.map +1 -1
  263. package/dist/internal/Schema.js +24 -1
  264. package/dist/internal/Schema.js.map +1 -1
  265. package/dist/internal/ZoneAccess.d.ts +70 -35
  266. package/dist/internal/ZoneAccess.d.ts.map +1 -1
  267. package/dist/internal/ZoneAccess.js +312 -131
  268. package/dist/internal/ZoneAccess.js.map +1 -1
  269. package/dist/internal/ZoneDelegation.d.ts +7 -19
  270. package/dist/internal/ZoneDelegation.d.ts.map +1 -1
  271. package/dist/internal/ZoneDelegation.js +8 -29
  272. package/dist/internal/ZoneDelegation.js.map +1 -1
  273. package/dist/internal/{ZoneKeyApproval.d.ts → ZoneDelegationApproval.d.ts} +3 -5
  274. package/dist/internal/ZoneDelegationApproval.d.ts.map +1 -0
  275. package/dist/internal/ZoneDelegationApproval.js +16 -0
  276. package/dist/internal/ZoneDelegationApproval.js.map +1 -0
  277. package/dist/internal/ZoneKeyRewrite.d.ts +98 -0
  278. package/dist/internal/ZoneKeyRewrite.d.ts.map +1 -0
  279. package/dist/internal/ZoneKeyRewrite.js +151 -0
  280. package/dist/internal/ZoneKeyRewrite.js.map +1 -0
  281. package/dist/internal/rewards/Campaigns.d.ts +2 -0
  282. package/dist/internal/rewards/Campaigns.d.ts.map +1 -1
  283. package/dist/internal/rewards/Campaigns.js.map +1 -1
  284. package/dist/internal/rewards/Distributions.d.ts.map +1 -1
  285. package/dist/internal/rewards/Distributions.js +2 -6
  286. package/dist/internal/rewards/Distributions.js.map +1 -1
  287. package/dist/internal/rewards/Monitor.d.ts +4 -1
  288. package/dist/internal/rewards/Monitor.d.ts.map +1 -1
  289. package/dist/internal/rewards/Monitor.js +19 -2
  290. package/dist/internal/rewards/Monitor.js.map +1 -1
  291. package/dist/internal/rewards/Runner.d.ts.map +1 -1
  292. package/dist/internal/rewards/Runner.js +9 -1
  293. package/dist/internal/rewards/Runner.js.map +1 -1
  294. package/dist/internal/routes/Action.d.ts.map +1 -1
  295. package/dist/internal/routes/Action.js +2 -3
  296. package/dist/internal/routes/Action.js.map +1 -1
  297. package/dist/internal/routes/Deposit.d.ts.map +1 -1
  298. package/dist/internal/routes/Deposit.js +2 -6
  299. package/dist/internal/routes/Deposit.js.map +1 -1
  300. package/dist/internal/routes/DepositAddress.d.ts.map +1 -1
  301. package/dist/internal/routes/DepositAddress.js +4 -10
  302. package/dist/internal/routes/DepositAddress.js.map +1 -1
  303. package/dist/internal/routes/Policy.d.ts.map +1 -1
  304. package/dist/internal/routes/Policy.js +2 -1
  305. package/dist/internal/routes/Policy.js.map +1 -1
  306. package/dist/internal/routes/Provider.js +8 -8
  307. package/dist/internal/routes/Provider.js.map +1 -1
  308. package/dist/internal/routes/Reconciliation.d.ts.map +1 -1
  309. package/dist/internal/routes/Reconciliation.js +3 -2
  310. package/dist/internal/routes/Reconciliation.js.map +1 -1
  311. package/dist/internal/routes/Transfer.d.ts.map +1 -1
  312. package/dist/internal/routes/Transfer.js +4 -12
  313. package/dist/internal/routes/Transfer.js.map +1 -1
  314. package/dist/internal/routes/UniversalDepositAddress.d.ts.map +1 -1
  315. package/dist/internal/routes/UniversalDepositAddress.js +3 -6
  316. package/dist/internal/routes/UniversalDepositAddress.js.map +1 -1
  317. package/dist/internal/routes/providers/SquidOrder.d.ts.map +1 -1
  318. package/dist/internal/routes/providers/SquidOrder.js +3 -2
  319. package/dist/internal/routes/providers/SquidOrder.js.map +1 -1
  320. package/dist/internal/routes/providers/relay.d.ts.map +1 -1
  321. package/dist/internal/routes/providers/relay.js +4 -3
  322. package/dist/internal/routes/providers/relay.js.map +1 -1
  323. package/package.json +20 -5
  324. package/src/ApiKey.ts +2 -2
  325. package/src/ApiKeys.test.ts +284 -51
  326. package/src/ApiKeys.ts +297 -64
  327. package/src/App.ts +4 -5
  328. package/src/Client.test-d.ts +33 -126
  329. package/src/Scope.test.ts +26 -45
  330. package/src/Scope.ts +24 -37
  331. package/src/admin/App.test.ts +51 -4
  332. package/src/admin/App.ts +25 -33
  333. package/src/admin/Mcp.test.ts +2 -1
  334. package/src/admin/apps/api-keys.test.ts +90 -20
  335. package/src/admin/apps/api-keys.ts +5 -8
  336. package/src/admin/apps/audit-logs.ts +1 -1
  337. package/src/admin/apps/earn-vaults.test.ts +19 -19
  338. package/src/admin/apps/earn-vaults.ts +11 -5
  339. package/src/admin/apps/routes-support.test.ts +254 -3
  340. package/src/admin/apps/routes-support.ts +1 -44
  341. package/src/admin/apps/scopes.test.ts +3 -3
  342. package/src/admin/apps/zone-delegation-requests.test.ts +246 -0
  343. package/src/admin/apps/zone-delegation-requests.ts +240 -0
  344. package/src/admin/apps/zone-delegations.test.ts +218 -58
  345. package/src/admin/apps/zone-delegations.ts +146 -46
  346. package/src/admin/index.ts +1 -1
  347. package/src/admin/ui/src/components/ApiKeyForm.tsx +4 -3
  348. package/src/admin/ui/src/lib/config.ts +3 -3
  349. package/src/admin/ui/src/lib/query-client.ts +1 -1
  350. package/src/admin/ui/src/lib/review-queues.ts +10 -10
  351. package/src/admin/ui/src/lib/routes-setup.test.ts +1 -2
  352. package/src/admin/ui/src/lib/routes-setup.ts +3 -5
  353. package/src/admin/ui/src/lib/scopes.test.ts +3 -6
  354. package/src/admin/ui/src/lib/scopes.ts +2 -9
  355. package/src/admin/ui/src/lib/{zone-key-requests.ts → zone-delegation-requests.ts} +27 -31
  356. package/src/admin/ui/src/routeTree.gen.ts +17 -17
  357. package/src/admin/ui/src/routes/__root.tsx +1 -1
  358. package/src/admin/ui/src/routes/api-keys.tsx +2 -15
  359. package/src/admin/ui/src/routes/audit-logs.tsx +2 -2
  360. package/src/admin/ui/src/routes/index.tsx +11 -11
  361. package/src/admin/ui/src/routes/organizations.tsx +3 -1
  362. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +3 -1
  363. package/src/admin/ui/src/routes/organizations_.$orgId_.setup.tsx +1 -3
  364. package/src/admin/ui/src/routes/{zone-key-requests.tsx → zone-delegation-requests.tsx} +228 -172
  365. package/src/admin/ui/vite.config.ts +14 -1
  366. package/src/admin/ui.generated.ts +412 -583
  367. package/src/analytics/tables/requestEvents.test.ts +13 -3
  368. package/src/analytics/tables/requestEvents.ts +4 -3
  369. package/src/apps/data/App.test.ts +72 -35
  370. package/src/apps/data/App.ts +29 -13
  371. package/src/apps/data/ZoneSelection.test.ts +53 -70
  372. package/src/apps/data/ZoneSelection.ts +2 -2
  373. package/src/apps/data/routes/activities.test.ts +140 -6
  374. package/src/apps/data/routes/activities.ts +124 -18
  375. package/src/apps/data/routes/balances.ts +5 -1
  376. package/src/apps/data/routes/blocks.ts +9 -13
  377. package/src/apps/data/routes/earn-transactions.local.test.ts +6 -1
  378. package/src/apps/data/routes/earn-transactions.test.ts +7 -3
  379. package/src/apps/data/routes/earn.bench.ts +0 -14
  380. package/src/apps/data/routes/earn.discovery.test.ts +4 -39
  381. package/src/apps/data/routes/earn.local.test.ts +27 -34
  382. package/src/apps/data/routes/earn.test.ts +172 -490
  383. package/src/apps/data/routes/earn.ts +103 -652
  384. package/src/apps/data/routes/exchanges.ts +20 -30
  385. package/src/apps/data/routes/fee-amm.ts +12 -18
  386. package/src/apps/data/routes/indexer.test.ts +14 -12
  387. package/src/apps/data/routes/receipts.ts +1 -1
  388. package/src/apps/data/routes/rpc.test.ts +23 -18
  389. package/src/apps/data/routes/rpc.ts +21 -21
  390. package/src/apps/data/routes/rpc.zone.test.ts +60 -6
  391. package/src/apps/data/routes/tokenlist.ts +4 -6
  392. package/src/apps/data/routes/tokens.test.ts +20 -0
  393. package/src/apps/data/routes/tokens.ts +10 -5
  394. package/src/apps/data/routes/transactions.ts +3 -3
  395. package/src/apps/data/routes/transfers.ts +4 -6
  396. package/src/apps/data/routes/valuation.test.ts +2 -4
  397. package/src/apps/data/routes/valuation.ts +4 -6
  398. package/src/apps/data/routes/webhooks.test.ts +1 -5
  399. package/src/apps/data/routes/webhooks.ts +30 -44
  400. package/src/apps/data/routes/zones.test.ts +35 -19
  401. package/src/apps/management/App.ts +3 -1
  402. package/src/apps/management/routes/api-keys.test.ts +112 -22
  403. package/src/apps/management/routes/api-keys.ts +30 -15
  404. package/src/apps/management/routes/billing.ts +17 -25
  405. package/src/apps/management/routes/cli.ts +12 -18
  406. package/src/apps/management/routes/fee-payer-usage.ts +5 -4
  407. package/src/apps/management/routes/invitations.ts +16 -21
  408. package/src/apps/management/routes/invite-links.ts +18 -26
  409. package/src/apps/management/routes/me.ts +9 -13
  410. package/src/apps/management/routes/members.ts +5 -7
  411. package/src/apps/management/routes/orgs.ts +8 -12
  412. package/src/apps/management/routes/projects.ts +8 -12
  413. package/src/apps/management/routes/route-usage.ts +5 -4
  414. package/src/apps/management/routes/routes-access-requests.test.ts +64 -2
  415. package/src/apps/management/routes/routes-access-requests.ts +99 -15
  416. package/src/apps/management/routes/scopes.test.ts +3 -3
  417. package/src/apps/management/routes/usage.ts +20 -28
  418. package/src/apps/management/routes/verified-token-requests.ts +9 -13
  419. package/src/apps/management/routes/zone-delegations.test.ts +3 -5
  420. package/src/apps/management/routes/zone-delegations.ts +4 -9
  421. package/src/apps/mpp/App.ts +3 -3
  422. package/src/apps/mpp/Mpp.zone.test.ts +49 -18
  423. package/src/apps/mpp/internal/Adapter.ts +1 -1
  424. package/src/apps/mpp/internal/Error.test.ts +3 -0
  425. package/src/apps/mpp/internal/Error.ts +1 -0
  426. package/src/apps/mpp/internal/Policy.test.ts +9 -8
  427. package/src/apps/mpp/internal/Policy.ts +37 -14
  428. package/src/apps/relay/App.sandbox.test.ts +15 -14
  429. package/src/apps/relay/App.test.ts +120 -61
  430. package/src/apps/relay/App.ts +28 -18
  431. package/src/apps/relay/App.zone-billing.test.ts +9 -4
  432. package/src/apps/routes/App.test.ts +21 -22
  433. package/src/db/Db.ts +34 -3
  434. package/src/db/Schema.ts +26 -26
  435. package/src/db/tables/rewardRuns.ts +39 -0
  436. package/src/db/tables/routesAccessRequests.test.ts +65 -0
  437. package/src/db/tables/routesAccessRequests.ts +25 -0
  438. package/src/db/tables/{zoneKeyRequests.test.ts → zoneDelegationRequests.test.ts} +36 -77
  439. package/src/db/tables/{zoneKeyRequests.ts → zoneDelegationRequests.ts} +69 -52
  440. package/src/db/tables/zoneDelegations.test.ts +5 -6
  441. package/src/db/tables/zoneDelegations.ts +25 -8
  442. package/src/handlers/internal/{multisig.store.test.ts → accounts.store.test.ts} +16 -16
  443. package/src/handlers/internal/{multisig.test.ts → accounts.test.ts} +85 -82
  444. package/src/handlers/internal/{multisig.ts → accounts.ts} +9 -9
  445. package/src/handlers/internal/sponsorship.ts +20 -20
  446. package/src/handlers/relay.test.ts +48 -46
  447. package/src/handlers/relay.ts +26 -26
  448. package/src/internal/Auth.test.ts +6 -2
  449. package/src/internal/Auth.ts +4 -10
  450. package/src/internal/EarnRates.ts +4 -6
  451. package/src/internal/EarnVaults.ts +2 -2
  452. package/src/internal/Log.test-d.ts +1 -0
  453. package/src/internal/Log.test.ts +4 -6
  454. package/src/internal/Log.ts +2 -6
  455. package/src/internal/Response.ts +0 -3
  456. package/src/internal/Schema.test.ts +42 -0
  457. package/src/internal/Schema.ts +36 -1
  458. package/src/internal/ZoneAccess.test.ts +515 -312
  459. package/src/internal/ZoneAccess.ts +410 -158
  460. package/src/internal/ZoneDelegation.test.ts +1 -41
  461. package/src/internal/ZoneDelegation.ts +10 -48
  462. package/src/internal/ZoneDelegationApproval.test.ts +22 -0
  463. package/src/internal/{ZoneKeyApproval.ts → ZoneDelegationApproval.ts} +4 -11
  464. package/src/internal/ZoneKeyRewrite.ts +231 -0
  465. package/src/internal/rewards/Campaigns.ts +2 -0
  466. package/src/internal/rewards/Distributions.ts +8 -12
  467. package/src/internal/rewards/Monitor.test.ts +64 -0
  468. package/src/internal/rewards/Monitor.ts +27 -4
  469. package/src/internal/rewards/Runner.ts +9 -1
  470. package/src/internal/routes/Action.ts +5 -6
  471. package/src/internal/routes/Deposit.ts +8 -12
  472. package/src/internal/routes/DepositAddress.ts +13 -19
  473. package/src/internal/routes/Policy.ts +2 -1
  474. package/src/internal/routes/Provider.ts +8 -8
  475. package/src/internal/routes/Reconciliation.ts +3 -2
  476. package/src/internal/routes/Transfer.ts +16 -24
  477. package/src/internal/routes/UniversalDepositAddress.ts +9 -12
  478. package/src/internal/routes/providers/SquidOrder.ts +3 -2
  479. package/src/internal/routes/providers/relay.ts +4 -3
  480. package/dist/admin/apps/zone-key-requests.d.ts +0 -406
  481. package/dist/admin/apps/zone-key-requests.d.ts.map +0 -1
  482. package/dist/admin/apps/zone-key-requests.js +0 -183
  483. package/dist/admin/apps/zone-key-requests.js.map +0 -1
  484. package/dist/db/tables/zoneKeyRequests.d.ts +0 -289
  485. package/dist/db/tables/zoneKeyRequests.d.ts.map +0 -1
  486. package/dist/db/tables/zoneKeyRequests.js.map +0 -1
  487. package/dist/handlers/internal/multisig.d.ts +0 -7
  488. package/dist/handlers/internal/multisig.d.ts.map +0 -1
  489. package/dist/internal/ZoneKeyApproval.d.ts.map +0 -1
  490. package/dist/internal/ZoneKeyApproval.js +0 -21
  491. package/dist/internal/ZoneKeyApproval.js.map +0 -1
  492. package/src/admin/apps/zone-key-requests.test.ts +0 -272
  493. package/src/admin/apps/zone-key-requests.ts +0 -231
  494. package/src/internal/ZoneKeyApproval.test.ts +0 -46
@@ -1,26 +1,33 @@
1
1
  import type { Context } from 'hono'
2
+ import { HTTPException } from 'hono/http-exception'
2
3
 
3
4
  import * as Db from '../db/Db.js'
4
5
  import * as ZoneDelegations from '../db/tables/zoneDelegations.js'
5
6
  import type * as Metrics from '../Metrics.js'
6
- import * as Scope from '../Scope.js'
7
7
  import * as Auth from './Auth.js'
8
8
  import * as ExecutionContext from './ExecutionContext.js'
9
+ import * as Response from './Response.js'
9
10
  import * as Store from './Store.js'
10
11
  import * as Timeout from './Timeout.js'
12
+ import * as Timing from './Timing.js'
11
13
  import * as ZoneDelegation from './ZoneDelegation.js'
12
14
 
13
- /** Hono variables written by {@link authorize}, {@link deny}, and {@link settle}, read by the request logger. */
15
+ /** Hono variables written by {@link ensure}, {@link authorize}, and {@link deny}, read by the request logger. */
14
16
  export type Variables = {
15
17
  /** Zone access decision for the Zone the request is about, recorded even when denied. */
16
18
  zoneAccess?: Access | undefined
17
- /** Delegation evaluation started by {@link authorize} for the recorded access, awaited by {@link settle}. */
18
- zoneDelegationDecision?: Promise<ZoneDelegation.Result> | undefined
19
+ /** Every decision {@link authorize} recorded for this request, metered once by {@link settle}. */
20
+ zoneAccessDecisions?: Access[] | undefined
21
+ /** The caller organization's active delegations once {@link ensure} settled; absent or failed denies every Zone read. */
22
+ zoneDelegations?: Delegations | undefined
23
+ /** The delegation read {@link ensure} started for this request, awaited by every later caller. */
24
+ zoneDelegationsPending?: Promise<Delegations> | undefined
19
25
  }
20
26
 
21
- /** Hono environment required by {@link check}, {@link authorize}, and {@link settle}. */
27
+ /** Hono environment required by {@link ensure}, {@link check}, {@link authorize}, and {@link settle}. */
22
28
  export type Environment = {
23
29
  Variables: Auth.Variables &
30
+ Timing.Variables &
24
31
  Variables & {
25
32
  /** Authoritative store holding Zone delegations. */
26
33
  db: Db.Source
@@ -28,74 +35,136 @@ export type Environment = {
28
35
  metrics: Metrics.Metrics | undefined
29
36
  /** Cache store for memoized delegation reads. */
30
37
  store: Store.Store
38
+ /** Hosted Zones keyed by chain id; no Zones means no delegation reads. */
39
+ zones: ReadonlyMap<number, unknown>
31
40
  }
32
41
  }
33
42
 
34
- /** Bounded counter of Zone decisions by Zone, scope outcome, grant, and delegation agreement. */
43
+ /** Bounded counter of Zone decisions by Zone, outcome, and grant. */
35
44
  export const decisionMetric = 'zone_access_decision_count'
36
45
 
37
- /** Counter of delegation reads that failed, leaving a decision without a shadow comparison. */
46
+ /** Counter of failed delegation reads: `phase="request"` refused a request with a `503`, `phase="refresh"` was a background refresh that left the cached copy serving. */
38
47
  export const lookupFailureMetric = 'zone_delegation_lookup_failure_count'
39
48
 
40
- /** How long one organization's delegations are cached per colo; Postgres stays authoritative. */
49
+ /** Age after which a cached delegation read is refreshed in the background while still answering. */
41
50
  export const delegationTtl = 30_000
42
51
 
43
- /** Longest a delegation read may run; the comparison is advisory, so it gives up early. */
44
- export const delegationTimeout = 2_000
52
+ /** Oldest cached delegation read that may answer; beyond it the request reads Postgres. Bounds revocation latency per colo. */
53
+ export const delegationMaxAge = 60_000
45
54
 
46
- /** Longest {@link settle} waits for a read still in flight once the handler returned. */
47
- export const settleGrace = 100
55
+ /** Longest a delegation read (cache and Postgres together) may run before it fails. */
56
+ export const delegationTimeout = 5_000
48
57
 
49
- /** Operation a Zone request performs; a write scope never reads and a read scope never broadcasts. */
58
+ /** Operation a Zone request performs; a write scope never reads and a delegation never broadcasts. */
50
59
  export type Verb = 'read' | 'write'
51
60
 
52
61
  /** How a Zone request was admitted. */
53
- export type Grant = 'scope' | 'wildcard' | 'zone_token'
62
+ export type Grant = 'delegation' | 'scope' | 'zone_token'
63
+
64
+ /** The caller organization's active delegations as {@link ensure} left them. */
65
+ export type Delegations =
66
+ | {
67
+ /** Active delegations across the organization's Zones in the key's environment. */
68
+ records: readonly ZoneDelegations.Record[]
69
+ }
70
+ | {
71
+ /** The read failed or timed out with no usable cached copy, so every delegated Zone read is unavailable. */
72
+ failed: true
73
+ }
54
74
 
55
75
  /** Recorded Zone access decision. */
56
76
  export type Access = {
57
77
  /** Lowercase account addresses the request is about; empty when not account-scoped. */
58
78
  accounts: readonly string[]
59
- /** Whether the delegation decision agrees with the scope decision; absent until {@link settle} compares them. */
60
- agreement?: boolean | undefined
61
79
  /** Zone chain id. */
62
80
  chainId: number
63
- /** Delegation decision computed beside the scope decision; absent until {@link settle} runs or when no organization applies. */
81
+ /** Delegation decision for a read the key was otherwise eligible for; absent for writes, tokens, and ineligible keys. */
64
82
  delegation?: ZoneDelegation.Result | undefined
65
83
  /** Grant that admitted the request; absent when denied, including by a later check such as quota or sandbox. */
66
84
  grant?: Grant | undefined
67
- /** Grant the scope decision produced, kept through {@link deny} because delegations replace scopes only. */
68
- scopeGrant?: Grant | undefined
69
85
  /** Lowercase token addresses the request is about; empty when not token-scoped. */
70
86
  tokens: readonly string[]
71
87
  /** Requested operation. */
72
88
  verb: Verb
73
89
  }
74
90
 
91
+ /**
92
+ * Loads the caller organization's active delegations on first demand and
93
+ * shares the read across the request, for API keys that can read or broadcast
94
+ * to a Zone at all. Call it before {@link check} or {@link eligible} decide;
95
+ * requests never touching a Zone pay nothing. Client cache directives are
96
+ * ignored. A read Postgres cannot answer refuses delegated Zone access with a
97
+ * retriable `503`.
98
+ */
99
+ export function ensure<environment extends Environment>(
100
+ c: Context<environment>,
101
+ ): Promise<Delegations | undefined> {
102
+ const pending = c.get('zoneDelegationsPending' as never) as Promise<Delegations> | undefined
103
+ if (pending) return pending
104
+ const principal = Auth.getPrincipal(c)
105
+ if (principal?.type !== 'api_key' || !zoneCapable(principal)) return Promise.resolve(undefined)
106
+ const zones = c.get('zones' as never) as ReadonlyMap<number, unknown> | undefined
107
+ if (!zones?.size) return Promise.resolve(undefined)
108
+ const owner = { environment: principal.environment, orgId: principal.orgId }
109
+ const started = Timing.time(c, 'zone_delegations', () => Cached.read(c, owner)).then(
110
+ (records): Delegations => ({ records }),
111
+ (): Delegations => {
112
+ metrics(c)?.count(lookupFailureMetric, 1, { phase: 'request' })
113
+ return { failed: true }
114
+ },
115
+ )
116
+ void started.then((delegations) => c.set('zoneDelegations' as never, delegations as never))
117
+ c.set('zoneDelegationsPending' as never, started as never)
118
+ return started
119
+ }
120
+
75
121
  /**
76
122
  * Decides whether the caller may access one Zone without recording anything.
77
- * Filters narrowing a Zone list use this; the gate for the Zone a request is
78
- * about uses {@link authorize}. Scopes are inspected only here.
123
+ * Filters narrowing a Zone list use this after awaiting {@link ensure}; the gate
124
+ * for the Zone a request is about uses {@link authorize}. Without the request's
125
+ * addresses, only a delegation covering every account and token admits a read.
79
126
  */
80
127
  export function check<environment extends Environment>(
81
128
  c: Context<environment>,
82
129
  options: check.Options,
83
130
  ): check.Result {
84
- const { chainId, verb, zoneToken = false } = options
85
- const principal = Auth.getPrincipal(c)
86
- const scopes = principal?.type === 'api_key' ? principal.apiKey.scopes : undefined
87
- // A forwarded Zone token is the operative credential, so it outranks scopes.
88
- const grant = ((): Grant | undefined => {
89
- if (zoneToken) return 'zone_token'
90
- if (scopes?.includes(Scope.wildcard)) return 'wildcard'
91
- if (scopes?.includes(`zone:${chainId}:${verb}`)) return 'scope'
92
- return undefined
93
- })()
94
- if (grant) return { allowed: true, grant }
95
- return {
96
- allowed: false,
97
- error: { code: 'api_key_forbidden', message: denialMessage(options), status: 403 },
98
- }
131
+ const gated = gate(c, options)
132
+ if (!('eligible' in gated)) return gated
133
+ const delegation = ZoneDelegation.decide({
134
+ accounts: [],
135
+ delegation: coverage(gated.record, { accounts: [], tokens: [] }),
136
+ tokens: [],
137
+ })
138
+ return conclude(options, delegation)
139
+ }
140
+
141
+ /**
142
+ * Whether the caller could read one Zone given a covering request: the key is
143
+ * tagged and scoped for it and its organization holds an active delegation.
144
+ * Selection filters use this before {@link authorize} decides each Zone with
145
+ * the request's addresses; an unreadable delegation counts, so `authorize` answers `503`.
146
+ */
147
+ export function eligible<environment extends Environment>(
148
+ c: Context<environment>,
149
+ options: Pick<check.Options, 'chainId'>,
150
+ ): boolean {
151
+ const gated = gate(c, { chainId: options.chainId, verb: 'read' })
152
+ return 'eligible' in gated || gated.allowed || gated.error.status === 503
153
+ }
154
+
155
+ /**
156
+ * Whether a listing may include one Zone the caller reads, after awaiting
157
+ * {@link ensure}. Throws a `503` when the delegations could not be read, so a
158
+ * listing never silently omits Zones it could not verify.
159
+ */
160
+ export function visible<environment extends Environment>(
161
+ c: Context<environment>,
162
+ options: Pick<check.Options, 'chainId'>,
163
+ ): boolean {
164
+ const result = check(c, { chainId: options.chainId, verb: 'read' })
165
+ if (!result.allowed && result.error.status === 503)
166
+ throw new HTTPException(503, { res: Response.error(c, result.error) })
167
+ return result.allowed
99
168
  }
100
169
 
101
170
  export declare namespace check {
@@ -126,42 +195,65 @@ export declare namespace check {
126
195
  error: Denial
127
196
  }
128
197
 
129
- /** Denial error envelope fields. */
130
- type Denial = {
131
- /** Stable error code. */
132
- code: 'api_key_forbidden'
133
- /** Corrective guidance naming the missing grant. */
134
- message: string
135
- /** HTTP status. */
136
- status: 403
137
- }
198
+ /** Denial error envelope fields: a missing grant, or delegations that could not be read. */
199
+ type Denial =
200
+ | {
201
+ /** Stable error code. */
202
+ code: 'api_key_forbidden'
203
+ /** Corrective guidance naming the missing grant. */
204
+ message: string
205
+ /** HTTP status. */
206
+ status: 403
207
+ }
208
+ | {
209
+ /** Stable error code. */
210
+ code: 'zone_access_unavailable'
211
+ /** Guidance to retry. */
212
+ message: string
213
+ /** HTTP status. */
214
+ status: 503
215
+ }
138
216
  }
139
217
 
140
218
  /**
141
219
  * Decides whether the caller may access the Zone the request is about and
142
- * records the decision on the context for the request log. The caller
143
- * organization's delegations start loading here so {@link settle} can compare
144
- * them against the final scope decision without delaying the response.
220
+ * records the decision on the context for the request log. A read loads the
221
+ * delegations, then checks the requested accounts and tokens against the
222
+ * delegation's members; a failed read refuses the request with a retriable `503`.
145
223
  */
146
- export function authorize<environment extends Environment>(
224
+ export async function authorize<environment extends Environment>(
147
225
  c: Context<environment>,
148
226
  options: authorize.Options,
149
- ): authorize.Result {
150
- const result = check(c, options)
227
+ ): Promise<authorize.Result> {
228
+ const addresses = { accounts: normalize(options.accounts), tokens: normalize(options.tokens) }
229
+ // A disconnect while the delegation or member reads are pending logs after the decision, so denials stay unbilled.
230
+ const { delegation, result } = await Auth.check(c, {
231
+ allows: (decided) => decided.result.allowed,
232
+ fn: async () => {
233
+ // Generic writes need the delegation too; a forwarded Zone token never does.
234
+ if (!options.zoneToken) await ensure(c)
235
+ const gated = gate(c, options)
236
+ if (!('eligible' in gated)) return { delegation: undefined, result: gated }
237
+ const delegation = await evaluate(c, gated, addresses)
238
+ if (delegation) return { delegation, result: conclude(options, delegation) }
239
+ const denied: ZoneDelegation.Result = { decision: 'deny', id: gated.record.id }
240
+ return { delegation: denied, result: unavailable(options) }
241
+ },
242
+ })
151
243
  const access: Access = {
152
- accounts: normalize(options.accounts),
244
+ ...addresses,
153
245
  chainId: options.chainId,
154
- ...(result.allowed ? { grant: result.grant, scopeGrant: result.grant } : {}),
155
- tokens: normalize(options.tokens),
246
+ ...(delegation === undefined ? {} : { delegation }),
247
+ ...(result.allowed ? { grant: result.grant } : {}),
156
248
  verb: options.verb,
157
249
  }
158
250
  c.set('zoneAccess' as never, access as never)
159
- const scope = delegationScope(c, access)
160
- // Starts the evaluation behind the handler; `settle` awaits this same promise.
161
- // A rejection is reported there, so an evaluation nobody settles stays quiet.
162
- const decision = scope ? evaluate(c, access, scope) : undefined
163
- decision?.catch(() => undefined)
164
- c.set('zoneDelegationDecision' as never, decision as never)
251
+ // A later decision for the same Zone (raw RPC per method, relay per item) supersedes the earlier one.
252
+ const decisions = (c.get('zoneAccessDecisions' as never) as Access[] | undefined) ?? []
253
+ const index = decisions.findIndex((decision) => decision.chainId === access.chainId)
254
+ if (index === -1) decisions.push(access)
255
+ else decisions[index] = access
256
+ c.set('zoneAccessDecisions' as never, decisions as never)
165
257
  return result
166
258
  }
167
259
 
@@ -184,117 +276,174 @@ export function deny<environment extends Environment>(c: Context<environment>) {
184
276
  if (access?.grant === undefined) return
185
277
  const { grant: _grant, ...denied } = access
186
278
  c.set('zoneAccess' as never, denied as never)
279
+ const decisions = c.get('zoneAccessDecisions' as never) as Access[] | undefined
280
+ if (decisions) {
281
+ const index = decisions.indexOf(access)
282
+ if (index !== -1) decisions[index] = denied
283
+ }
284
+ }
285
+
286
+ /** Meters every Zone decision recorded for the request once the handler finished, after later refusals applied. */
287
+ export function settle<environment extends Environment>(c: Context<environment>): void {
288
+ const decisions = c.get('zoneAccessDecisions' as never) as Access[] | undefined
289
+ // A disconnected client already emitted its log entry; its decisions were never final.
290
+ if (!decisions?.length || c.req.raw.signal.aborted) return
291
+ for (const access of decisions)
292
+ metrics(c)?.count(decisionMetric, 1, {
293
+ chain_id: access.chainId,
294
+ grant: access.grant ?? 'none',
295
+ outcome: access.grant === undefined ? 'deny' : 'allow',
296
+ })
297
+ }
298
+
299
+ /** A read the key may perform if its organization's delegation covers the request. */
300
+ type Eligible = {
301
+ /** Marks the gate outcome as awaiting the delegation decision. */
302
+ eligible: true
303
+ /** The organization's active delegation for the Zone. */
304
+ record: ZoneDelegations.Record
187
305
  }
188
306
 
189
307
  /**
190
- * Computes the delegation decision for the recorded Zone access once the
191
- * handler finished and records it with the agreement flag and one bounded
192
- * metric. Agreement compares against the scope decision alone: later refusals
193
- * (quota, method allowlist, sandbox, workload identity) stay in place when
194
- * delegations replace scopes, so they are not disagreements. Admission is
195
- * already decided; a failed delegation read only leaves the comparison out.
308
+ * Applies the credential checks that need no addresses: a forwarded Zone token
309
+ * outranks everything, writes stay on `zone:<chainId>:write`, and a read needs
310
+ * `zone:read`, a tag naming the Zone, and an active delegation. `*` grants no Zone.
196
311
  */
197
- export async function settle<environment extends Environment>(
312
+ function gate<environment extends Environment>(
198
313
  c: Context<environment>,
199
- ): Promise<void> {
200
- const access = c.get('zoneAccess' as never) as Access | undefined
201
- if (!access) return
202
- // A disconnected client already emitted its log entry without these fields.
203
- if (c.req.raw.signal.aborted) return
204
- const metrics = c.get('metrics' as never) as Metrics.Metrics | undefined
205
- const tags = { chain_id: access.chainId, grant: access.grant ?? 'none', outcome: access.grant === undefined ? 'deny' : 'allow' } // prettier-ignore
206
- const decision = c.get('zoneDelegationDecision' as never) as
207
- | Promise<ZoneDelegation.Result>
208
- | undefined
209
- if (!decision) {
210
- metrics?.count(decisionMetric, 1, { ...tags, agreement: 'skipped' })
211
- return
212
- }
213
- // The evaluation usually finished behind the handler; a cold miss must not extend the response.
214
- const pending = decision.then(
215
- (value) => ({ value }),
216
- () => ({ value: undefined }),
217
- )
218
- const outcome = await Timeout.run(() => pending, { timeout: settleGrace }).catch(() => undefined)
219
- if (outcome === undefined) {
220
- // The read finishes in the background to warm the cache; a late failure still counts.
221
- ExecutionContext.waitUntil(c)?.(
222
- pending.then(({ value }) => {
223
- if (value === undefined) metrics?.count(lookupFailureMetric, 1)
224
- }),
225
- )
226
- metrics?.count(decisionMetric, 1, { ...tags, agreement: 'pending' })
227
- return
228
- }
229
- const delegation = outcome.value
230
- if (!delegation) {
231
- metrics?.count(lookupFailureMetric, 1)
232
- metrics?.count(decisionMetric, 1, { ...tags, agreement: 'unavailable' })
233
- return
314
+ options: check.Options,
315
+ ): check.Result | Eligible {
316
+ const { chainId, verb, zoneToken = false } = options
317
+ const principal = Auth.getPrincipal(c)
318
+ if (zoneToken) return { allowed: true, grant: 'zone_token' }
319
+ const refuse = (): check.Result => ({
320
+ allowed: false,
321
+ error: { code: 'api_key_forbidden', message: denialMessage(options), status: 403 },
322
+ })
323
+ if (principal?.type !== 'api_key') return refuse()
324
+ const { scopes } = principal.apiKey
325
+ // Absent tags mean no Zone, so a later delegation never widens an existing key.
326
+ const tagged = principal.zoneTags?.includes(chainId) ?? false
327
+ if (verb === 'write') {
328
+ // A stored per-Zone write scope keeps admitting its own Zone this release, exactly as before,
329
+ // until the write rewrite has run and the cleanup release strips it.
330
+ if (scopes.includes(`zone:${chainId}:write`)) return { allowed: true, grant: 'scope' }
331
+ // Broadcasts are account-signed, so the delegation's existence is the whole bound: revoking it
332
+ // stops the key, accounts and tokens are never checked, and the wildcard grants no Zone.
333
+ if (!scopes.includes('zone:write') || !tagged) return refuse()
334
+ if (lookupFailed(c)) return unavailable(options)
335
+ if (!delegationFor(c, chainId)) return refuse()
336
+ return { allowed: true, grant: 'scope' }
234
337
  }
235
- const agreement = (delegation.decision === 'allow') === (access.scopeGrant !== undefined)
236
- c.set('zoneAccess' as never, { ...access, agreement, delegation } as never)
237
- metrics?.count(decisionMetric, 1, { ...tags, agreement: agreement ? 'agree' : 'disagree' })
338
+ if (!readable(principal) || !tagged) return refuse()
339
+ // Only keys a delegation could admit learn the read failed; others keep their 403.
340
+ if (lookupFailed(c)) return unavailable(options)
341
+ const record = delegationFor(c, chainId)
342
+ if (!record) return refuse()
343
+ return { eligible: true, record }
238
344
  }
239
345
 
240
- /**
241
- * Organization and environment whose delegations decide the request, or
242
- * undefined when no delegation applies: writes, anonymous and session callers
243
- * (no organization), and forwarded Zone tokens (the operative credential).
244
- */
245
- function delegationScope<environment extends Environment>(
346
+ /** The organization's active delegation for one Zone; undefined when none, or when `ensure` never ran or failed, so callers fail closed. */
347
+ function delegationFor<environment extends Environment>(
246
348
  c: Context<environment>,
247
- access: Access,
248
- ): ZoneDelegations.Owner | undefined {
249
- const principal = Auth.getPrincipal(c)
250
- // Broadcasts are user-signed, so they stay on key scopes and are never compared.
251
- if (access.verb === 'write') return undefined
252
- if (principal?.type !== 'api_key' || access.scopeGrant === 'zone_token') return undefined
253
- return { environment: principal.environment, orgId: principal.orgId }
349
+ chainId: number,
350
+ ): ZoneDelegations.Record | undefined {
351
+ const delegations = c.get('zoneDelegations' as never) as Delegations | undefined
352
+ if (!delegations || 'failed' in delegations) return undefined
353
+ return delegations.records.find((candidate) => candidate.chainId === chainId)
354
+ }
355
+
356
+ /** Whether {@link ensure} ran and its delegation read failed with no recent copy to answer from. */
357
+ function lookupFailed<environment extends Environment>(c: Context<environment>): boolean {
358
+ const delegations = c.get('zoneDelegations' as never) as Delegations | undefined
359
+ return delegations !== undefined && 'failed' in delegations
360
+ }
361
+
362
+ /** Refusal for a key a delegation might admit when the delegations could not be read. */
363
+ function unavailable(options: check.Options): check.Result {
364
+ return {
365
+ allowed: false,
366
+ error: {
367
+ code: 'zone_access_unavailable',
368
+ message: `Zone ${options.chainId} access could not be verified. Retry the request.`,
369
+ status: 503,
370
+ },
371
+ }
372
+ }
373
+
374
+ /** Whether the key's scopes and tags let it read any Zone at all: `zone:read` plus at least one tag. */
375
+ function readable(principal: Extract<Auth.Principal, { type: 'api_key' }>): boolean {
376
+ return principal.apiKey.scopes.includes('zone:read') && (principal.zoneTags?.length ?? 0) > 0
377
+ }
378
+
379
+ /** Whether the key's scopes and tags let it use any Zone at all, reading or broadcasting. */
380
+ function zoneCapable(principal: Extract<Auth.Principal, { type: 'api_key' }>): boolean {
381
+ const { scopes } = principal.apiKey
382
+ return (
383
+ (scopes.includes('zone:read') || scopes.includes('zone:write')) &&
384
+ (principal.zoneTags?.length ?? 0) > 0
385
+ )
386
+ }
387
+
388
+ /** Maps a delegation decision onto the gate result for the requested Zone. */
389
+ function conclude(options: check.Options, delegation: ZoneDelegation.Result): check.Result {
390
+ if (delegation.decision === 'allow') return { allowed: true, grant: 'delegation' }
391
+ return {
392
+ allowed: false,
393
+ error: { code: 'api_key_forbidden', message: denialMessage(options), status: 403 },
394
+ }
254
395
  }
255
396
 
256
397
  /**
257
- * Evaluates the caller organization's delegation for one recorded access: the
258
- * delegation rows are cached per organization and environment, and the
259
- * requested accounts and tokens are checked for membership in one statement
260
- * each, cached per address, so a request never loads a whole member set.
261
- * Client cache directives are ignored throughout. Rejections surface in {@link settle}.
398
+ * Decides an eligible read against the delegation's members: the requested
399
+ * accounts and tokens are checked for membership in one statement each, cached
400
+ * per address, so a request never loads a whole member set. Returns undefined
401
+ * when the member read failed, which the caller refuses as unavailable.
262
402
  */
263
403
  async function evaluate<environment extends Environment>(
264
404
  c: Context<environment>,
265
- access: Access,
266
- scope: ZoneDelegations.Owner,
267
- ): Promise<ZoneDelegation.Result> {
268
- const principal = Auth.getPrincipal(c)
405
+ eligible: Eligible,
406
+ addresses: Addresses,
407
+ ): Promise<ZoneDelegation.Result | undefined> {
408
+ const { record } = eligible
409
+ const decide = (delegation: ZoneDelegation.Delegation) =>
410
+ ZoneDelegation.decide({ ...addresses, delegation })
411
+ const listed = { accounts: !record.accountsAll, tokens: !record.tokensAll }
412
+ if (
413
+ (!listed.accounts || !addresses.accounts.length) &&
414
+ (!listed.tokens || !addresses.tokens.length)
415
+ )
416
+ return decide(coverage(record, { accounts: [], tokens: [] }))
269
417
  const db = Db.get(c.get('db' as never) as Db.Source)
270
418
  const store = Store.authoritative(c.get('store' as never) as Store.Store)
271
- const records = await Store.memoize(() => ZoneDelegations.listActive(db, scope), {
272
- flightTimeout: delegationTimeout,
273
- key: `zone_delegations:v1:${scope.orgId}:${scope.environment}`,
274
- store,
275
- ttl: delegationTtl,
276
- })
277
- const record = records.find((candidate) => candidate.chainId === access.chainId)
278
- const grant = access.scopeGrant
279
- const decide = (delegation?: ZoneDelegation.Delegation) =>
280
- ZoneDelegation.decide({
281
- accounts: access.accounts,
282
- delegation,
283
- grant,
284
- projectId: principal?.type === 'api_key' ? principal.projectId : undefined,
285
- tokens: access.tokens,
286
- })
287
- // A key without its own grant is denied before coverage matters, so its members are never read.
288
- if (!record || grant === undefined)
289
- return decide(record && { accounts: [], id: record.id, projects: record.projects, tokens: [] })
290
419
  const wiring = { c, db, id: record.id, store }
291
- const [accounts, tokens] = await Promise.all([
292
- record.accountsAll
293
- ? ('all' as const)
294
- : covered({ ...wiring, addresses: access.accounts, kind: 'account' }),
295
- record.tokensAll ? ('all' as const) : covered({ ...wiring, addresses: access.tokens, kind: 'token' }), // prettier-ignore
296
- ])
297
- return decide({ accounts, id: record.id, projects: record.projects, tokens })
420
+ try {
421
+ const [accounts, tokens] = await Promise.all([
422
+ listed.accounts ? covered({ ...wiring, addresses: addresses.accounts, kind: 'account' }) : [],
423
+ listed.tokens ? covered({ ...wiring, addresses: addresses.tokens, kind: 'token' }) : [],
424
+ ])
425
+ return decide(coverage(record, { accounts, tokens }))
426
+ } catch {
427
+ metrics(c)?.count(lookupFailureMetric, 1, { phase: 'request' })
428
+ return undefined
429
+ }
430
+ }
431
+
432
+ /** Builds the gate's view of a delegation from its row and the members already resolved. */
433
+ function coverage(record: ZoneDelegations.Record, members: Addresses): ZoneDelegation.Delegation {
434
+ return {
435
+ accounts: record.accountsAll ? 'all' : members.accounts,
436
+ id: record.id,
437
+ tokens: record.tokensAll ? 'all' : members.tokens,
438
+ }
439
+ }
440
+
441
+ /** Lowercase accounts and tokens a request names. */
442
+ type Addresses = {
443
+ /** Account addresses. */
444
+ accounts: readonly string[]
445
+ /** Token contract addresses. */
446
+ tokens: readonly string[]
298
447
  }
299
448
 
300
449
  /**
@@ -331,6 +480,106 @@ async function covered<environment extends Environment>(
331
480
  return addresses.filter((address) => hits.get(address) === '1' || found.has(address))
332
481
  }
333
482
 
483
+ /**
484
+ * Per-colo delegation cache with stale-while-revalidate: entries younger than
485
+ * {@link delegationTtl} answer directly, older ones up to {@link delegationMaxAge}
486
+ * answer while a background read refreshes them, and anything older reads Postgres.
487
+ */
488
+ namespace Cached {
489
+ type Entry = {
490
+ /** When Postgres returned the records, in epoch milliseconds. */
491
+ at: number
492
+ /** Active delegations at that time. */
493
+ records: readonly ZoneDelegations.Record[]
494
+ }
495
+
496
+ /** Keys whose background refresh this isolate started, by start time, so concurrent requests start one. */
497
+ const refreshing = new Map<string, number>()
498
+
499
+ export async function read<environment extends Environment>(
500
+ c: Context<environment>,
501
+ owner: ZoneDelegations.Owner,
502
+ ): Promise<readonly ZoneDelegations.Record[]> {
503
+ const deadline = performance.now() + delegationTimeout
504
+ const db = Db.get(c.get('db' as never) as Db.Source)
505
+ // Client cache directives never bypass or refresh authorization data.
506
+ const store = Store.authoritative(c.get('store' as never) as Store.Store)
507
+ // v2 entries carry their read time; v1 entries were bare records.
508
+ const key = `zone_delegations:v2:${owner.orgId}:${owner.environment}`
509
+ const entry = await Timing.time(c, 'zone_delegations_cache', () =>
510
+ Timeout.run(() => store.get(key), { timeout: delegationTimeout }).then(
511
+ decode,
512
+ () => undefined,
513
+ ),
514
+ )
515
+ const age = entry ? Date.now() - entry.at : Infinity
516
+ const wiring = { db, key, owner, store, waitUntil: ExecutionContext.waitUntil(c) }
517
+ if (entry && age <= delegationMaxAge) {
518
+ if (age > delegationTtl) refresh(c, wiring)
519
+ return entry.records
520
+ }
521
+ // A cache read that spent the budget never starts a query the caller no longer awaits.
522
+ const remaining = deadline - performance.now()
523
+ if (remaining <= 0) throw new Error('Zone delegation read exceeded its budget.')
524
+ // Present on a failed request, this stage shows how long Postgres (through Hyperdrive) took.
525
+ return Timing.time(c, 'zone_delegations_query', () => load({ ...wiring, timeout: remaining }))
526
+ }
527
+
528
+ type Options = {
529
+ /** Database to read. */
530
+ db: Db.Db
531
+ /** Cache key. */
532
+ key: string
533
+ /** Organization and environment whose delegations are read. */
534
+ owner: ZoneDelegations.Owner
535
+ /** Colo cache store. */
536
+ store: Store.Store
537
+ /** Keeps background work alive after the response; absent outside Workers. */
538
+ waitUntil: ExecutionContext.waitUntil.ReturnType
539
+ }
540
+
541
+ /** Reads Postgres within the timeout and caches the result; cache writes are best-effort and never delay it. */
542
+ async function load(options: Options & { timeout: number }) {
543
+ const { db, key, owner, store, timeout, waitUntil } = options
544
+ const records = await Timeout.run(() => ZoneDelegations.listActive(db, owner), { timeout })
545
+ const entry: Entry = { at: Date.now(), records }
546
+ const persist = store
547
+ .put(key, JSON.stringify(entry), { ttl: delegationMaxAge })
548
+ .catch(() => undefined)
549
+ waitUntil?.(persist)
550
+ return records
551
+ }
552
+
553
+ /** Refreshes an aging entry after the response; a failure leaves the entry answering until it is too old. */
554
+ function refresh<environment extends Environment>(c: Context<environment>, options: Options) {
555
+ const started = refreshing.get(options.key)
556
+ if (started !== undefined && Date.now() - started < delegationTimeout) return
557
+ refreshing.set(options.key, Date.now())
558
+ const work = load({ ...options, timeout: delegationTimeout })
559
+ .catch(() => {
560
+ // The request's metrics already flushed, so a late count flushes itself.
561
+ metrics(c)?.count(lookupFailureMetric, 1, { phase: 'refresh' })
562
+ metrics(c)?.flush()
563
+ })
564
+ .finally(() => refreshing.delete(options.key))
565
+ options.waitUntil?.(work)
566
+ }
567
+
568
+ function decode(raw: string | null): Entry | undefined {
569
+ if (raw === null) return undefined
570
+ try {
571
+ const entry = JSON.parse(raw) as Entry
572
+ return typeof entry.at === 'number' && Array.isArray(entry.records) ? entry : undefined
573
+ } catch {
574
+ return undefined
575
+ }
576
+ }
577
+ }
578
+
579
+ function metrics<environment extends Environment>(c: Context<environment>) {
580
+ return c.get('metrics' as never) as Metrics.Metrics | undefined
581
+ }
582
+
334
583
  /** Lowercases addresses so logged accounts and tokens compare exactly. */
335
584
  function normalize(addresses: readonly string[] | undefined): readonly string[] {
336
585
  return addresses?.map((address) => address.toLowerCase()) ?? []
@@ -339,11 +588,14 @@ function normalize(addresses: readonly string[] | undefined): readonly string[]
339
588
  /** Denial guidance per verb, keeping each route's published wording. */
340
589
  function denialMessage(options: check.Options): string {
341
590
  const { chainId, suggestZoneToken = false, verb } = options
342
- if (verb === 'write')
591
+ if (verb === 'write') {
592
+ const grant = `Broadcasting to zone ${chainId} requires \`zone:write\` with a tag for Zone ${chainId}, issued to an organization delegated that Zone`
343
593
  return suggestZoneToken
344
- ? `Broadcasting to zone ${chainId} requires \`zone:${chainId}:write\`, or a Zone token via \`X-Authorization-Token\`.`
345
- : `Broadcasting to zone ${chainId} requires \`zone:${chainId}:write\`.`
594
+ ? `${grant}, or a Zone token via \`X-Authorization-Token\`.`
595
+ : `${grant}.`
596
+ }
597
+ const grant = `Use an API key granting \`zone:read\` and tagged for Zone ${chainId}, issued to an organization delegated that Zone`
346
598
  return suggestZoneToken
347
- ? `Chain id ${chainId} is a zone. Use an API key granting \`zone:${chainId}:read\`, or pass a Zone token via \`X-Authorization-Token\`.`
348
- : `Chain ID ${chainId} is a Zone. Use an API key granting \`zone:${chainId}:read\`.`
599
+ ? `Chain ID ${chainId} is a Zone. ${grant}, or pass a Zone token via \`X-Authorization-Token\`.`
600
+ : `Chain ID ${chainId} is a Zone. ${grant}.`
349
601
  }