tapimo 0.5.2 → 0.6.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 (299) hide show
  1. package/dist/App.d.ts +13987 -5755
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js.map +1 -1
  4. package/dist/admin/App.d.ts +910 -412
  5. package/dist/admin/App.d.ts.map +1 -1
  6. package/dist/admin/App.js +4 -0
  7. package/dist/admin/App.js.map +1 -1
  8. package/dist/admin/apps/early-access.d.ts +148 -0
  9. package/dist/admin/apps/early-access.d.ts.map +1 -0
  10. package/dist/admin/apps/early-access.js +133 -0
  11. package/dist/admin/apps/early-access.js.map +1 -0
  12. package/dist/admin/apps/verified-tokens.d.ts +643 -118
  13. package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
  14. package/dist/admin/apps/verified-tokens.js +214 -1
  15. package/dist/admin/apps/verified-tokens.js.map +1 -1
  16. package/dist/admin/ui.generated.d.ts +1 -1
  17. package/dist/admin/ui.generated.d.ts.map +1 -1
  18. package/dist/admin/ui.generated.js +45 -33
  19. package/dist/admin/ui.generated.js.map +1 -1
  20. package/dist/apps/data/App.d.ts +3733 -2428
  21. package/dist/apps/data/App.d.ts.map +1 -1
  22. package/dist/apps/data/App.js +11 -6
  23. package/dist/apps/data/App.js.map +1 -1
  24. package/dist/apps/data/FundingQuoteProviders.d.ts +34 -1
  25. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
  26. package/dist/apps/data/FundingQuoteProviders.js +193 -2
  27. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  28. package/dist/apps/data/FxOracle.d.ts +71 -0
  29. package/dist/apps/data/FxOracle.d.ts.map +1 -0
  30. package/dist/apps/data/FxOracle.js +97 -0
  31. package/dist/apps/data/FxOracle.js.map +1 -0
  32. package/dist/apps/data/index.d.ts +1 -0
  33. package/dist/apps/data/index.d.ts.map +1 -1
  34. package/dist/apps/data/index.js +1 -0
  35. package/dist/apps/data/index.js.map +1 -1
  36. package/dist/apps/data/routes/activities.d.ts +3575 -1358
  37. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  38. package/dist/apps/data/routes/activities.js +230 -88
  39. package/dist/apps/data/routes/activities.js.map +1 -1
  40. package/dist/apps/data/routes/addresses.d.ts +963 -342
  41. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  42. package/dist/apps/data/routes/addresses.js +11 -9
  43. package/dist/apps/data/routes/addresses.js.map +1 -1
  44. package/dist/apps/data/routes/balances.d.ts +108 -57
  45. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  46. package/dist/apps/data/routes/balances.js +88 -45
  47. package/dist/apps/data/routes/balances.js.map +1 -1
  48. package/dist/apps/data/routes/coingecko.d.ts +169 -169
  49. package/dist/apps/data/routes/exchanges.d.ts +952 -201
  50. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  51. package/dist/apps/data/routes/exchanges.js +518 -76
  52. package/dist/apps/data/routes/exchanges.js.map +1 -1
  53. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  54. package/dist/apps/data/routes/funding.d.ts +8 -2
  55. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  56. package/dist/apps/data/routes/funding.js +32 -6
  57. package/dist/apps/data/routes/funding.js.map +1 -1
  58. package/dist/apps/data/routes/indexer.d.ts +5 -5
  59. package/dist/apps/data/routes/receipts.d.ts +392 -315
  60. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  61. package/dist/apps/data/routes/receipts.js +152 -77
  62. package/dist/apps/data/routes/receipts.js.map +1 -1
  63. package/dist/apps/data/routes/tokens.d.ts +166 -173
  64. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  65. package/dist/apps/data/routes/tokens.js +14 -12
  66. package/dist/apps/data/routes/tokens.js.map +1 -1
  67. package/dist/apps/data/routes/transactions.d.ts +277 -536
  68. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  69. package/dist/apps/data/routes/transactions.js +96 -77
  70. package/dist/apps/data/routes/transactions.js.map +1 -1
  71. package/dist/apps/data/routes/transfers.d.ts +82 -17
  72. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  73. package/dist/apps/data/routes/transfers.js +70 -13
  74. package/dist/apps/data/routes/transfers.js.map +1 -1
  75. package/dist/apps/data/routes/valuation.d.ts +307 -0
  76. package/dist/apps/data/routes/valuation.d.ts.map +1 -0
  77. package/dist/apps/data/routes/valuation.js +292 -0
  78. package/dist/apps/data/routes/valuation.js.map +1 -0
  79. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
  80. package/dist/apps/data/routes/verified-tokens.js +7 -5
  81. package/dist/apps/data/routes/verified-tokens.js.map +1 -1
  82. package/dist/apps/data/routes/webhooks.d.ts +459 -479
  83. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  84. package/dist/apps/data/routes/webhooks.js +124 -25
  85. package/dist/apps/data/routes/webhooks.js.map +1 -1
  86. package/dist/apps/index.d.ts +1 -1
  87. package/dist/apps/index.d.ts.map +1 -1
  88. package/dist/apps/index.js +1 -1
  89. package/dist/apps/index.js.map +1 -1
  90. package/dist/apps/management/App.d.ts +8606 -1681
  91. package/dist/apps/management/App.d.ts.map +1 -1
  92. package/dist/apps/management/App.js +12 -1
  93. package/dist/apps/management/App.js.map +1 -1
  94. package/dist/apps/management/routes/api-keys.d.ts +23 -0
  95. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  96. package/dist/apps/management/routes/api-keys.js +5 -3
  97. package/dist/apps/management/routes/api-keys.js.map +1 -1
  98. package/dist/apps/management/routes/billing.d.ts +88 -48
  99. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  100. package/dist/apps/management/routes/billing.js +10 -2
  101. package/dist/apps/management/routes/billing.js.map +1 -1
  102. package/dist/apps/management/routes/faucet.d.ts +218 -0
  103. package/dist/apps/management/routes/faucet.d.ts.map +1 -0
  104. package/dist/apps/management/routes/faucet.js +139 -0
  105. package/dist/apps/management/routes/faucet.js.map +1 -0
  106. package/dist/apps/management/routes/invitations.d.ts +121 -121
  107. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  108. package/dist/apps/management/routes/invitations.js +3 -0
  109. package/dist/apps/management/routes/invitations.js.map +1 -1
  110. package/dist/apps/management/routes/invite-links.d.ts +197 -168
  111. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  112. package/dist/apps/management/routes/invite-links.js +26 -1
  113. package/dist/apps/management/routes/invite-links.js.map +1 -1
  114. package/dist/apps/management/routes/me.d.ts +2 -0
  115. package/dist/apps/management/routes/me.d.ts.map +1 -1
  116. package/dist/apps/management/routes/me.js +4 -0
  117. package/dist/apps/management/routes/me.js.map +1 -1
  118. package/dist/apps/management/routes/members.d.ts +49 -49
  119. package/dist/apps/management/routes/orgs.d.ts +91 -91
  120. package/dist/apps/management/routes/projects.d.ts +115 -115
  121. package/dist/apps/management/routes/usage.d.ts +35 -32
  122. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  123. package/dist/apps/management/routes/usage.js +4 -0
  124. package/dist/apps/management/routes/usage.js.map +1 -1
  125. package/dist/apps/management/routes/verified-token-requests.d.ts +697 -0
  126. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -0
  127. package/dist/apps/management/routes/verified-token-requests.js +271 -0
  128. package/dist/apps/management/routes/verified-token-requests.js.map +1 -0
  129. package/dist/apps/management/routes/webhooks.d.ts +6977 -0
  130. package/dist/apps/management/routes/webhooks.d.ts.map +1 -0
  131. package/dist/apps/management/routes/webhooks.js +573 -0
  132. package/dist/apps/management/routes/webhooks.js.map +1 -0
  133. package/dist/apps/mcp.d.ts +9 -2
  134. package/dist/apps/mcp.d.ts.map +1 -1
  135. package/dist/apps/mcp.js +9 -4
  136. package/dist/apps/mcp.js.map +1 -1
  137. package/dist/cli/Cli.d.ts.map +1 -1
  138. package/dist/cli/Cli.js +7 -2
  139. package/dist/cli/Cli.js.map +1 -1
  140. package/dist/db/Db.d.ts +4 -0
  141. package/dist/db/Db.d.ts.map +1 -1
  142. package/dist/db/Db.js +83 -0
  143. package/dist/db/Db.js.map +1 -1
  144. package/dist/db/Schema.d.ts +41 -0
  145. package/dist/db/Schema.d.ts.map +1 -1
  146. package/dist/db/Schema.js +45 -0
  147. package/dist/db/Schema.js.map +1 -1
  148. package/dist/db/tables/earlyAccess.d.ts +50 -0
  149. package/dist/db/tables/earlyAccess.d.ts.map +1 -0
  150. package/dist/db/tables/earlyAccess.js +63 -0
  151. package/dist/db/tables/earlyAccess.js.map +1 -0
  152. package/dist/db/tables/invitations.d.ts +12 -1
  153. package/dist/db/tables/invitations.d.ts.map +1 -1
  154. package/dist/db/tables/invitations.js +23 -0
  155. package/dist/db/tables/invitations.js.map +1 -1
  156. package/dist/db/tables/inviteLinks.d.ts +10 -2
  157. package/dist/db/tables/inviteLinks.d.ts.map +1 -1
  158. package/dist/db/tables/inviteLinks.js +10 -1
  159. package/dist/db/tables/inviteLinks.js.map +1 -1
  160. package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
  161. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  162. package/dist/db/tables/sponsoredTransactions.js +2 -0
  163. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  164. package/dist/db/tables/verifiedTokenRequests.d.ts +94 -0
  165. package/dist/db/tables/verifiedTokenRequests.d.ts.map +1 -0
  166. package/dist/db/tables/verifiedTokenRequests.js +120 -0
  167. package/dist/db/tables/verifiedTokenRequests.js.map +1 -0
  168. package/dist/db/tables/webhookSubscriptions.d.ts +14 -0
  169. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  170. package/dist/db/tables/webhookSubscriptions.js +16 -0
  171. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  172. package/dist/internal/Auth.d.ts +43 -2
  173. package/dist/internal/Auth.d.ts.map +1 -1
  174. package/dist/internal/Auth.js +60 -45
  175. package/dist/internal/Auth.js.map +1 -1
  176. package/dist/internal/FundingQuotes.d.ts +69 -15
  177. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  178. package/dist/internal/FundingQuotes.js +74 -25
  179. package/dist/internal/FundingQuotes.js.map +1 -1
  180. package/dist/internal/Log.d.ts +19 -1
  181. package/dist/internal/Log.d.ts.map +1 -1
  182. package/dist/internal/Log.js +2 -0
  183. package/dist/internal/Log.js.map +1 -1
  184. package/dist/internal/MetricSink.d.ts.map +1 -1
  185. package/dist/internal/MetricSink.js +9 -0
  186. package/dist/internal/MetricSink.js.map +1 -1
  187. package/dist/internal/OpenApi.d.ts +2 -2
  188. package/dist/internal/OpenApi.d.ts.map +1 -1
  189. package/dist/internal/OpenApi.js +3 -0
  190. package/dist/internal/OpenApi.js.map +1 -1
  191. package/dist/internal/Schema.d.ts +14 -5
  192. package/dist/internal/Schema.d.ts.map +1 -1
  193. package/dist/internal/Schema.js +23 -5
  194. package/dist/internal/Schema.js.map +1 -1
  195. package/dist/internal/Value.d.ts +18 -0
  196. package/dist/internal/Value.d.ts.map +1 -1
  197. package/dist/internal/Value.js +11 -1
  198. package/dist/internal/Value.js.map +1 -1
  199. package/dist/internal/VerifiedTokens.d.ts.map +1 -1
  200. package/dist/internal/VerifiedTokens.js +10 -1
  201. package/dist/internal/VerifiedTokens.js.map +1 -1
  202. package/dist/internal/Viem.d.ts +1 -1
  203. package/dist/internal/Webhooks.d.ts +24 -3
  204. package/dist/internal/Webhooks.d.ts.map +1 -1
  205. package/dist/internal/Webhooks.js +34 -8
  206. package/dist/internal/Webhooks.js.map +1 -1
  207. package/package.json +2 -2
  208. package/src/App.ts +9 -1
  209. package/src/Client.test-d.ts +638 -6
  210. package/src/admin/App.ts +4 -0
  211. package/src/admin/apps/early-access.test.ts +125 -0
  212. package/src/admin/apps/early-access.ts +175 -0
  213. package/src/admin/apps/verified-tokens.test.ts +201 -0
  214. package/src/admin/apps/verified-tokens.ts +532 -286
  215. package/src/admin/ui/plugin.ts +2 -1
  216. package/src/admin/ui/src/lib/early-access.ts +63 -0
  217. package/src/admin/ui/src/lib/verified-tokens.ts +79 -0
  218. package/src/admin/ui/src/routeTree.gen.ts +33 -2
  219. package/src/admin/ui/src/routes/__root.tsx +2 -1
  220. package/src/admin/ui/src/routes/early-access.tsx +157 -0
  221. package/src/admin/ui/src/routes/verified-tokens.tsx +230 -19
  222. package/src/admin/ui.generated.ts +48 -33
  223. package/src/apps/data/App.ts +22 -14
  224. package/src/apps/data/FundingQuoteProviders.test.ts +298 -0
  225. package/src/apps/data/FundingQuoteProviders.ts +257 -4
  226. package/src/apps/data/FxOracle.test.ts +118 -0
  227. package/src/apps/data/FxOracle.ts +153 -0
  228. package/src/apps/data/index.ts +1 -0
  229. package/src/apps/data/routes/activities.test.ts +173 -3
  230. package/src/apps/data/routes/activities.ts +403 -132
  231. package/src/apps/data/routes/addresses.ts +19 -9
  232. package/src/apps/data/routes/balances.test.ts +144 -11
  233. package/src/apps/data/routes/balances.ts +140 -51
  234. package/src/apps/data/routes/exchanges.test.ts +540 -9
  235. package/src/apps/data/routes/exchanges.ts +690 -103
  236. package/src/apps/data/routes/funding.test.ts +84 -0
  237. package/src/apps/data/routes/funding.ts +38 -6
  238. package/src/apps/data/routes/receipts.test.ts +125 -23
  239. package/src/apps/data/routes/receipts.ts +205 -100
  240. package/src/apps/data/routes/tokens.test.ts +43 -0
  241. package/src/apps/data/routes/tokens.ts +27 -12
  242. package/src/apps/data/routes/transactions.test.ts +72 -19
  243. package/src/apps/data/routes/transactions.ts +162 -92
  244. package/src/apps/data/routes/transfers.test.ts +146 -4
  245. package/src/apps/data/routes/transfers.ts +91 -13
  246. package/src/apps/data/routes/valuation.test.ts +422 -0
  247. package/src/apps/data/routes/valuation.ts +423 -0
  248. package/src/apps/data/routes/verified-tokens.ts +14 -2
  249. package/src/apps/data/routes/webhooks.test.ts +56 -8
  250. package/src/apps/data/routes/webhooks.ts +144 -27
  251. package/src/apps/index.ts +1 -1
  252. package/src/apps/management/App.ts +13 -0
  253. package/src/apps/management/routes/api-keys.test.ts +87 -0
  254. package/src/apps/management/routes/api-keys.ts +4 -3
  255. package/src/apps/management/routes/billing.test.ts +112 -1
  256. package/src/apps/management/routes/billing.ts +7 -2
  257. package/src/apps/management/routes/faucet.test.ts +79 -0
  258. package/src/apps/management/routes/faucet.ts +185 -0
  259. package/src/apps/management/routes/invitations.test.ts +172 -0
  260. package/src/apps/management/routes/invitations.ts +3 -0
  261. package/src/apps/management/routes/invite-links.test.ts +117 -0
  262. package/src/apps/management/routes/invite-links.ts +43 -1
  263. package/src/apps/management/routes/me.test.ts +140 -88
  264. package/src/apps/management/routes/me.ts +6 -0
  265. package/src/apps/management/routes/usage.test.ts +6 -0
  266. package/src/apps/management/routes/usage.ts +7 -0
  267. package/src/apps/management/routes/verified-token-requests.test.ts +377 -0
  268. package/src/apps/management/routes/verified-token-requests.ts +397 -0
  269. package/src/apps/management/routes/webhooks.test.ts +426 -0
  270. package/src/apps/management/routes/webhooks.ts +672 -0
  271. package/src/apps/mcp.test.ts +13 -0
  272. package/src/apps/mcp.ts +18 -5
  273. package/src/cli/Cli.test.ts +36 -0
  274. package/src/cli/Cli.ts +7 -2
  275. package/src/db/Db.test.ts +19 -0
  276. package/src/db/Db.ts +95 -0
  277. package/src/db/Schema.ts +53 -0
  278. package/src/db/tables/earlyAccess.ts +86 -0
  279. package/src/db/tables/invitations.ts +26 -1
  280. package/src/db/tables/inviteLinks.ts +22 -3
  281. package/src/db/tables/sponsoredTransactions.ts +4 -0
  282. package/src/db/tables/verifiedTokenRequests.test.ts +200 -0
  283. package/src/db/tables/verifiedTokenRequests.ts +182 -0
  284. package/src/db/tables/webhookSubscriptions.ts +31 -0
  285. package/src/internal/Auth.ts +73 -45
  286. package/src/internal/FundingQuotes.test.ts +143 -1
  287. package/src/internal/FundingQuotes.ts +95 -28
  288. package/src/internal/Log.test-d.ts +21 -0
  289. package/src/internal/Log.test.ts +50 -0
  290. package/src/internal/Log.ts +24 -1
  291. package/src/internal/MetricSink.test.ts +160 -0
  292. package/src/internal/MetricSink.ts +10 -0
  293. package/src/internal/OpenApi.ts +3 -0
  294. package/src/internal/Schema.ts +50 -5
  295. package/src/internal/Value.test.ts +20 -0
  296. package/src/internal/Value.ts +24 -1
  297. package/src/internal/VerifiedTokens.ts +9 -1
  298. package/src/internal/Webhooks.test.ts +140 -0
  299. package/src/internal/Webhooks.ts +53 -9
@@ -21,7 +21,7 @@ import * as Receipts from './receipts.js'
21
21
  import * as Transactions from './transactions.js'
22
22
  import * as Transfers from './transfers.js'
23
23
 
24
- /** Default cap on active subscriptions per owner, overridable via `webhook.maxPerOwner`. */
24
+ /** Default cap on live subscriptions per owner, overridable via `webhook.maxPerOwner`. */
25
25
  const defaultMaxPerOwner = 100
26
26
 
27
27
  /**
@@ -1317,10 +1317,13 @@ export function webhooks(options: webhooks.Options = {}) {
1317
1317
  responses: OpenApi.responses({
1318
1318
  errors: {
1319
1319
  400: {
1320
- codes: ['chain_id_unsupported'],
1321
- description: 'Webhook subscriptions do not support Zone chains.',
1320
+ codes: ['body_invalid', 'chain_id_unsupported', 'filters_invalid', 'url_invalid'],
1321
+ description: 'The request, destination, filters, or chain are invalid.',
1322
+ },
1323
+ 403: {
1324
+ codes: ['limit_exceeded'],
1325
+ description: 'You have reached the webhook subscription limit.',
1322
1326
  },
1323
- 403: 'You have reached the webhook subscription limit.',
1324
1327
  },
1325
1328
  success: {
1326
1329
  description:
@@ -1347,12 +1350,9 @@ export function webhooks(options: webhooks.Options = {}) {
1347
1350
 
1348
1351
  const body = c.req.valid('json')
1349
1352
  const chainId = body.chainId ?? c.get('chainId')
1350
- if (c.get('zones').has(chainId))
1351
- return Response.error(c, {
1352
- code: 'chain_id_unsupported',
1353
- message: 'Webhook subscriptions do not support Zone chains.',
1354
- status: 400,
1355
- })
1353
+ const supportedChainIds = new Set(webhook.supportedChainIds)
1354
+ if (!supportedChainIds.has(chainId))
1355
+ return Response.unsupportedChainId(c, chainId, supportedChainIds)
1356
1356
 
1357
1357
  try {
1358
1358
  const subscription = await Webhooks.createSubscription(
@@ -1373,7 +1373,7 @@ export function webhooks(options: webhooks.Options = {}) {
1373
1373
  200,
1374
1374
  )
1375
1375
  } catch (cause) {
1376
- return mutationError(c, cause)
1376
+ return createMutationError(c, cause)
1377
1377
  }
1378
1378
  },
1379
1379
  )
@@ -1747,6 +1747,16 @@ export function webhooks(options: webhooks.Options = {}) {
1747
1747
  operationId: 'updateWebhook',
1748
1748
  responses: OpenApi.responses({
1749
1749
  errors: {
1750
+ 400: {
1751
+ codes: [
1752
+ 'body_invalid',
1753
+ 'destination_transition_invalid',
1754
+ 'filters_invalid',
1755
+ 'param_invalid',
1756
+ 'url_invalid',
1757
+ ],
1758
+ description: 'The update, destination, or filters are invalid.',
1759
+ },
1750
1760
  404: {
1751
1761
  description: 'No webhook subscription was found for that ID.',
1752
1762
  codes: ['webhook_not_found'],
@@ -1798,7 +1808,7 @@ export function webhooks(options: webhooks.Options = {}) {
1798
1808
  200,
1799
1809
  )
1800
1810
  } catch (cause) {
1801
- return mutationError(c, cause)
1811
+ return updateMutationError(c, cause)
1802
1812
  }
1803
1813
  },
1804
1814
  )
@@ -1987,13 +1997,14 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
1987
1997
  let accepted: readonly [number, number] | undefined
1988
1998
  let failed = false
1989
1999
  for (const item of page.items) {
1990
- // Enrich `token:transfer` rows with best-effort token metadata so
1991
- // downstream destinations (Slack human amounts, richer `url`/`betterstack`
1992
- // payloads) can render `amount` in token units. Non-fatal on failure.
2000
+ // Token metadata is best-effort for webhooks: transfers use it for display,
2001
+ // while transactions expose fee-token references without blocking delivery.
1993
2002
  const data =
1994
2003
  eventType === 'token:transfer'
1995
2004
  ? await withTokenMetadata(item.data, { cache: cache_metadata, chainId, client })
1996
- : item.data
2005
+ : eventType === 'transaction:included'
2006
+ ? await withFeeTokenMetadata(item.data, { cache: cache_metadata, chainId, client })
2007
+ : item.data
1997
2008
  const envelope = Webhooks.buildEnvelope({
1998
2009
  blockNumber: item.cursor[0],
1999
2010
  createdAt: now(),
@@ -2106,27 +2117,45 @@ async function withTokenMetadata(
2106
2117
  const address = (data as Record<string, unknown>)['address']
2107
2118
  if (typeof address !== 'string') return data
2108
2119
  const token = await resolveTokenMetadata(address, deps)
2109
- return token ? { ...(data as Record<string, unknown>), token } : data
2120
+ return token
2121
+ ? {
2122
+ ...(data as Record<string, unknown>),
2123
+ token: { decimals: token.decimals, symbol: token.symbol },
2124
+ }
2125
+ : data
2126
+ }
2127
+
2128
+ /** Core RPC metadata needed by a humanized token reference. */
2129
+ type TokenMetadata = {
2130
+ currency: string
2131
+ decimals: number
2132
+ name: string
2133
+ symbol: string
2110
2134
  }
2111
2135
 
2112
2136
  /**
2113
- * Resolves a token's `{ symbol, decimals }` via `token.getMetadata`, memoized per
2137
+ * Resolves core token metadata via `token.getMetadata`, memoized per
2114
2138
  * `(chainId, address)` with a short TTL so a busy transfer stream shares one RPC
2115
- * lookup per token. Returns `undefined` on any failure metadata is a rendering
2139
+ * lookup per token. Returns `undefined` on any failure; metadata is a rendering
2116
2140
  * nicety, never a delivery blocker.
2117
2141
  */
2118
2142
  async function resolveTokenMetadata(
2119
2143
  address: string,
2120
2144
  deps: { cache: Store.Store; chainId: number; client: Viem.getClient.ReturnType },
2121
- ): Promise<{ decimals: number; symbol: string } | undefined> {
2145
+ ): Promise<TokenMetadata | undefined> {
2122
2146
  try {
2123
2147
  return await Store.memoize(
2124
2148
  async () => {
2125
2149
  const meta = await deps.client.token.getMetadata({ token: address as `0x${string}` })
2126
- return { decimals: meta.decimals, symbol: meta.symbol }
2150
+ return {
2151
+ currency: meta.currency,
2152
+ decimals: meta.decimals,
2153
+ name: meta.name,
2154
+ symbol: meta.symbol,
2155
+ }
2127
2156
  },
2128
2157
  {
2129
- key: `webhook:token-meta:v1:${deps.chainId}:${address}`,
2158
+ key: `webhook:token-meta:v2:${deps.chainId}:${address}`,
2130
2159
  store: deps.cache,
2131
2160
  ttl: Ttl.minutes(5),
2132
2161
  },
@@ -2137,6 +2166,69 @@ async function resolveTokenMetadata(
2137
2166
  }
2138
2167
  }
2139
2168
 
2169
+ /** Returns the address from an unenriched or enriched fee-token value. */
2170
+ function feeTokenAddress(value: unknown) {
2171
+ if (typeof value === 'string') return value
2172
+ if (typeof value !== 'object' || value === null) return undefined
2173
+ const address = (value as Record<string, unknown>)['address']
2174
+ return typeof address === 'string' ? address : undefined
2175
+ }
2176
+
2177
+ /** Replaces an address-valued fee token with its resolved reference, or omits it on failure. */
2178
+ function withResolvedFeeToken(data: Record<string, unknown>, metadata: Map<string, TokenMetadata>) {
2179
+ const address = feeTokenAddress(data['feeToken'])
2180
+ if (!address) return data
2181
+ const result = { ...data }
2182
+ delete result['feeToken']
2183
+ const token = metadata.get(address.toLowerCase())
2184
+ if (token) result['feeToken'] = { address, ...token }
2185
+ return result
2186
+ }
2187
+
2188
+ /** Adds best-effort fee-token references to a transaction and its embedded receipt. */
2189
+ async function withFeeTokenMetadata(
2190
+ data: unknown,
2191
+ deps: {
2192
+ cache: Store.Store
2193
+ chainId: number
2194
+ client: Viem.getClient.ReturnType
2195
+ },
2196
+ ): Promise<unknown> {
2197
+ if (typeof data !== 'object' || data === null) return data
2198
+ const transaction = data as Record<string, unknown>
2199
+ const meta = transaction['meta']
2200
+ const receipt =
2201
+ typeof meta === 'object' && meta !== null
2202
+ ? (meta as Record<string, unknown>)['receipt']
2203
+ : undefined
2204
+ const addresses = new Set<string>()
2205
+ const transactionAddress = feeTokenAddress(transaction['feeToken'])
2206
+ if (transactionAddress) addresses.add(transactionAddress.toLowerCase())
2207
+ if (typeof receipt === 'object' && receipt !== null) {
2208
+ const receiptAddress = feeTokenAddress((receipt as Record<string, unknown>)['feeToken'])
2209
+ if (receiptAddress) addresses.add(receiptAddress.toLowerCase())
2210
+ }
2211
+ if (addresses.size === 0) return data
2212
+
2213
+ const resolved = await Promise.all(
2214
+ [...addresses].map(
2215
+ async (address) => [address, await resolveTokenMetadata(address, deps)] as const,
2216
+ ),
2217
+ )
2218
+ const metadata = new Map(
2219
+ resolved.filter((entry): entry is readonly [string, TokenMetadata] => entry[1] !== undefined),
2220
+ )
2221
+ const result = withResolvedFeeToken(transaction, metadata)
2222
+ if (typeof receipt !== 'object' || receipt === null) return result
2223
+ return {
2224
+ ...result,
2225
+ meta: {
2226
+ ...(meta as Record<string, unknown>),
2227
+ receipt: withResolvedFeeToken(receipt as Record<string, unknown>, metadata),
2228
+ },
2229
+ }
2230
+ }
2231
+
2140
2232
  /** Returns the live RPC head block number for the chain. */
2141
2233
  export async function getRpcHead(client: Viem.getClient.ReturnType): Promise<number> {
2142
2234
  return Number(await client.getBlockNumber())
@@ -2883,8 +2975,8 @@ const txFields = {
2883
2975
  * Scans included transactions over the confirmed window. Each block is fetched
2884
2976
  * with full transactions via `eth_getBlockByNumber(n, true)`; receipts are
2885
2977
  * fetched lazily (only when a `status`/`gasUsed`/`feePayer` filter needs them).
2886
- * The delivered payload reuses `Transactions.humanizeTransaction`, so it is
2887
- * byte-identical to the read endpoint.
2978
+ * The delivered payload starts from `Transactions.humanizeTransaction`; the
2979
+ * poller then resolves its fee-token references.
2888
2980
  */
2889
2981
  async function scanTxs(options: {
2890
2982
  cache: ChainCache
@@ -2921,13 +3013,19 @@ async function scanTxs(options: {
2921
3013
  if (!matchesTx(tx, f, includeCalls, receipt, timestamp, n)) continue
2922
3014
  const parsed = Transactions.schema.Rpc.Transaction.safeParse(tx)
2923
3015
  if (!parsed.success) continue
2924
- let data = Transactions.humanizeTransaction(parsed.data)
3016
+ let data: Record<string, unknown> = Transactions.humanizeTransaction(parsed.data)
2925
3017
  // Embed the humanized receipt under `meta.receipt`, matching the read
2926
3018
  // endpoint's `include=receipt` shape so `data` stays a valid envelope.
2927
3019
  if (includeReceipt && receipt) {
2928
3020
  const r = Receipts.schema.Rpc.Receipt.safeParse(receipt)
2929
3021
  if (r.success)
2930
- data = { ...data, meta: { ...data.meta, receipt: Receipts.humanizeReceipt(r.data) } }
3022
+ data = {
3023
+ ...data,
3024
+ meta: {
3025
+ ...(data['meta'] as Record<string, unknown>),
3026
+ receipt: Receipts.humanizeReceipt(r.data),
3027
+ },
3028
+ }
2931
3029
  }
2932
3030
  items.push({ cursor: [n, index], data })
2933
3031
  }
@@ -3040,7 +3138,7 @@ function redactDestination(destination: WebhookDestination.Destination) {
3040
3138
  return destination
3041
3139
  }
3042
3140
 
3043
- function mutationError(c: Context<App.Environment>, cause: unknown) {
3141
+ function createMutationError(c: Context<App.Environment>, cause: unknown) {
3044
3142
  if (cause instanceof Webhooks.InvalidUrlError)
3045
3143
  return Response.error(c, { code: 'url_invalid', message: cause.message, status: 400 })
3046
3144
  if (cause instanceof Webhooks.InvalidFilterError)
@@ -3055,6 +3153,25 @@ function mutationError(c: Context<App.Environment>, cause: unknown) {
3055
3153
  return Response.upstream(c, cause)
3056
3154
  }
3057
3155
 
3156
+ function updateMutationError(c: Context<App.Environment>, cause: unknown) {
3157
+ if (cause instanceof Webhooks.InvalidDestinationTransitionError)
3158
+ return Response.error(c, {
3159
+ code: 'destination_transition_invalid',
3160
+ message: cause.message,
3161
+ status: 400,
3162
+ })
3163
+ if (cause instanceof Webhooks.InvalidUrlError)
3164
+ return Response.error(c, { code: 'url_invalid', message: cause.message, status: 400 })
3165
+ if (cause instanceof Webhooks.InvalidFilterError)
3166
+ return Response.error(c, {
3167
+ code: 'filters_invalid',
3168
+ details: cause.details,
3169
+ message: cause.message,
3170
+ status: 400,
3171
+ })
3172
+ return Response.upstream(c, cause)
3173
+ }
3174
+
3058
3175
  function notFound(c: Context<App.Environment>) {
3059
3176
  return Response.error(c, {
3060
3177
  code: 'webhook_not_found',
package/src/apps/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { data, FundingQuoteProviders } from './data/index.js'
1
+ export { data, FundingQuoteProviders, FxOracle } from './data/index.js'
2
2
  export { management } from './management/index.js'
3
3
  export { mcp } from './mcp.js'
4
4
  export { relay } from './relay/index.js'
@@ -6,6 +6,7 @@ import * as Metadata from '../metadata.js'
6
6
 
7
7
  import { apiKeys } from './routes/api-keys.js'
8
8
  import { billing } from './routes/billing.js'
9
+ import { faucet } from './routes/faucet.js'
9
10
  import { invitations } from './routes/invitations.js'
10
11
  import { inviteLinks } from './routes/invite-links.js'
11
12
  import { me } from './routes/me.js'
@@ -14,6 +15,8 @@ import { orgs } from './routes/orgs.js'
14
15
  import { projects } from './routes/projects.js'
15
16
  import { scopes } from './routes/scopes.js'
16
17
  import { usage } from './routes/usage.js'
18
+ import { verifiedTokenRequests } from './routes/verified-token-requests.js'
19
+ import { webhooks } from './routes/webhooks.js'
17
20
 
18
21
  /**
19
22
  * Minimal transactional email sender. Cloudflare deployments wrap the Email
@@ -105,6 +108,7 @@ export function management(options: management.Options = {}) {
105
108
  })
106
109
  .route('/', apiKeys({ enabled: options.apiKeys !== false }))
107
110
  .route('/', billing())
111
+ .route('/', faucet())
108
112
  .route('/', invitations())
109
113
  .route('/', inviteLinks())
110
114
  .route('/', me())
@@ -113,6 +117,8 @@ export function management(options: management.Options = {}) {
113
117
  .route('/', projects())
114
118
  .route('/', scopes())
115
119
  .route('/', usage())
120
+ .route('/', verifiedTokenRequests())
121
+ .route('/', webhooks({ webhook: options.webhook === false ? undefined : options.webhook }))
116
122
  return Metadata.attach(app, openapi)
117
123
  }
118
124
 
@@ -125,6 +131,8 @@ export declare namespace management {
125
131
  billing?: Billing | undefined
126
132
  /** Transactional email sender for management emails (e.g. invitations); omit to disable dispatch — notified flows still work in-product. */
127
133
  email?: Email | undefined
134
+ /** Webhook capability shared with the data group and host poller; omit or pass false to disable management. */
135
+ webhook?: App.Webhook | false | undefined
128
136
  }
129
137
  }
130
138
 
@@ -145,6 +153,7 @@ const openapi = {
145
153
  tags: [
146
154
  { name: 'API Keys', description: 'Credentials for accessing the Tempo API.' },
147
155
  { name: 'Billing', description: 'Organization billing backed by Stripe.' },
156
+ { name: 'Faucet', description: 'Test token funding for Tempo testnet accounts.' },
148
157
  { name: 'Invitations', description: 'Pending organization invitations.' },
149
158
  { name: 'Invite Links', description: 'Reusable organization invite links.' },
150
159
  { name: 'Members', description: 'Organization team membership.' },
@@ -152,6 +161,8 @@ const openapi = {
152
161
  { name: 'Projects', description: 'Application workspaces within an organization.' },
153
162
  { name: 'Usage', description: 'Request and sponsorship usage for organizations.' },
154
163
  { name: 'Users', description: 'Users on the Tempo Platform.' },
164
+ { name: 'Verified Token Requests', description: 'Organization requests to add tokens to the curated verified list.' }, // prettier-ignore
165
+ { name: 'Webhooks', description: 'Organization webhook subscriptions.' },
155
166
  ],
156
167
  'x-tagGroups': [
157
168
  {
@@ -167,6 +178,8 @@ const openapi = {
167
178
  'Projects',
168
179
  'Usage',
169
180
  'Users',
181
+ 'Verified Token Requests',
182
+ 'Webhooks',
170
183
  ],
171
184
  },
172
185
  ],
@@ -5,7 +5,9 @@ import * as Auth from '../../../internal/Auth.js'
5
5
  import * as OpenApi from '../../../internal/OpenApi.js'
6
6
  import * as Scope from '../../../Scope.js'
7
7
  import * as TestApp from '../../../../test/App.js'
8
+ import * as EarlyAccess from '../../../db/tables/earlyAccess.js'
8
9
  import * as Memberships from '../../../db/tables/memberships.js'
10
+ import * as Users from '../../../db/tables/users.js'
9
11
  import * as Keys from './api-keys.js'
10
12
  import * as Me from './me.js'
11
13
  import * as Orgs from './orgs.js'
@@ -43,6 +45,7 @@ function createApp(
43
45
  db?: ReturnType<typeof TestApp.database>
44
46
  kv?: { store: State }
45
47
  scopes?: readonly Scope.Entry[]
48
+ tmp_earlyAccess?: boolean
46
49
  } = {},
47
50
  ) {
48
51
  return TestApp.create({
@@ -51,6 +54,7 @@ function createApp(
51
54
  ...(options.kv ? { kv: options.kv } : {}),
52
55
  ...(options.scopes ? { scopes: options.scopes } : {}),
53
56
  session: { wallet: { origin } },
57
+ ...(options.tmp_earlyAccess === undefined ? {} : { tmp_earlyAccess: options.tmp_earlyAccess }),
54
58
  })
55
59
  }
56
60
 
@@ -671,3 +675,86 @@ describe('keys', () => {
671
675
  expect(response.status).toBe(403)
672
676
  })
673
677
  })
678
+
679
+ describe('early access', () => {
680
+ test('rejects mints from non-early-access sessions', async () => {
681
+ const app = createApp({ tmp_earlyAccess: true })
682
+ const cookie = await session(app)
683
+ const { org, project } = await fixture(app, cookie)
684
+
685
+ const denied = await app.request(
686
+ `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
687
+ json('POST', cookie, { scopes: ['data:read'] }),
688
+ )
689
+ expect(denied.status).toBe(403)
690
+ const { requestId, ...body } = (await denied.json()) as { requestId?: string }
691
+ expect(requestId).toBeDefined()
692
+ expect(body).toMatchInlineSnapshot(`
693
+ {
694
+ "error": {
695
+ "code": "early_access_required",
696
+ "message": "Requires early access. Ask a teammate with access to invite you by email, or contact Tempo.",
697
+ },
698
+ }
699
+ `)
700
+ })
701
+
702
+ test('the gate precedes membership resolution', async () => {
703
+ // The gate depends only on the caller, so it leaks nothing about the
704
+ // project; early-access strangers still get the membership 404.
705
+ const db = TestApp.database()
706
+ const app = createApp({ db, tmp_earlyAccess: true })
707
+ await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
708
+ const owner = await session(app)
709
+ const { org, project } = await fixture(app, owner)
710
+
711
+ const other = await session(app)
712
+ const gated = await app.request(
713
+ `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
714
+ json('POST', other, { scopes: ['data:read'] }),
715
+ )
716
+ expect(gated.status).toBe(403)
717
+ expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
718
+
719
+ const granted = await session(app)
720
+ const me = await app.request('/v1/me', { headers: { cookie: granted } })
721
+ const { id: userId } = (await me.json()) as { id: string }
722
+ await Users.setEmail(db, userId, 'stranger@example.org')
723
+ const denied = await app.request(
724
+ `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
725
+ json('POST', granted, { scopes: ['data:read'] }),
726
+ )
727
+ expect(denied.status).toBe(404)
728
+ })
729
+
730
+ test('lets early-access sessions mint', async () => {
731
+ const db = TestApp.database()
732
+ const app = createApp({ db, tmp_earlyAccess: true })
733
+ await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
734
+ const cookie = await session(app)
735
+ const user = await TestApp.json(
736
+ await app.request('/v1/me', { headers: { cookie } }),
737
+ Me.schema.User,
738
+ )
739
+ await Users.setEmail(db, user.id, 'dev@example.org')
740
+ const { org, project } = await fixture(app, cookie)
741
+
742
+ const minted = await app.request(
743
+ `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
744
+ json('POST', cookie, { scopes: ['data:read'] }),
745
+ )
746
+ expect(minted.status).toBe(200)
747
+ })
748
+
749
+ test('lets the super admin mint', async () => {
750
+ const app = createApp({ tmp_earlyAccess: true })
751
+ const cookie = await session(app)
752
+ const { org, project } = await fixture(app, cookie)
753
+
754
+ const minted = await app.request(
755
+ `/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
756
+ superAdminJson('POST', { scopes: ['data:read'] }),
757
+ )
758
+ expect(minted.status).toBe(200)
759
+ })
760
+ })
@@ -225,7 +225,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
225
225
  return new Hono<App.Environment>()
226
226
  .post(
227
227
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys',
228
- Auth.policy({ session: true }),
228
+ Auth.policy({ session: true, tmp_earlyAccess: true }),
229
229
  Auth.ensureOrg({ role: 'admin' }),
230
230
  Auth.ensureProject(),
231
231
  OpenApi.validate('param', schema.Params, {
@@ -243,8 +243,8 @@ export function apiKeys(options: apiKeys.Options = {}) {
243
243
  responses: OpenApi.responses({
244
244
  errors: {
245
245
  403: {
246
- codes: ['scope_not_issuable'],
247
- description: 'The requested scopes are not issuable.',
246
+ codes: ['scope_not_issuable', 'early_access_required'],
247
+ description: 'The requested scopes are not issuable, or the caller lacks early access.', // prettier-ignore
248
248
  },
249
249
  404: 'No accessible project was found for this id.',
250
250
  },
@@ -259,6 +259,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
259
259
  async (c) => {
260
260
  if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
261
261
  if (Auth.narrowScope) return Auth.ensureProjectError(c)
262
+ if (Auth.tmp_narrowEarlyAccess) return Auth.tmp_ensureEarlyAccessError(c)
262
263
  if (OpenApi.narrowValidation)
263
264
  return OpenApi.validationError(c, {
264
265
  code: 'param_invalid',
@@ -2,11 +2,14 @@ import { nanoid } from 'nanoid'
2
2
  import Stripe from 'stripe'
3
3
  import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
4
4
 
5
+ import * as ApiKeys from '../../../ApiKeys.js'
5
6
  import * as TestApp from '../../../../test/App.js'
6
7
  import * as TestStripe from '../../../../test/Stripe.js'
8
+ import * as EarlyAccess from '../../../db/tables/earlyAccess.js'
7
9
  import * as Memberships from '../../../db/tables/memberships.js'
8
10
  import * as SponsoredTransactions from '../../../db/tables/sponsoredTransactions.js'
9
11
  import * as StripeCustomers from '../../../db/tables/stripeCustomers.js'
12
+ import * as Users from '../../../db/tables/users.js'
10
13
  import * as Viem from '../../../internal/Viem.js'
11
14
  import * as core_Billing from '../Billing.js'
12
15
  import type * as App from '../../../App.js'
@@ -59,14 +62,21 @@ function billingWithSandbox(): App.Billing {
59
62
  }
60
63
 
61
64
  function createApp(
62
- options: { billing?: App.Billing | undefined; db?: TestApp.create.Options['db'] } = {},
65
+ options: {
66
+ billing?: App.Billing | undefined
67
+ db?: TestApp.create.Options['db']
68
+ kv?: { store: ReturnType<typeof TestApp.kvStore> }
69
+ tmp_earlyAccess?: boolean
70
+ } = {},
63
71
  ) {
64
72
  // prettier-ignore
65
73
  return TestApp.create({
66
74
  auth: { superAdmin: { secret } },
67
75
  billing: options.billing ?? billing(),
68
76
  db: options.db ?? TestApp.database(),
77
+ ...(options.kv ? { kv: options.kv } : {}),
69
78
  session: { wallet: { origin } },
79
+ ...(options.tmp_earlyAccess === undefined ? {} : { tmp_earlyAccess: options.tmp_earlyAccess }),
70
80
  })
71
81
  }
72
82
 
@@ -356,6 +366,107 @@ describe('POST /orgs/:orgId/billing/stripe/checkout', () => {
356
366
  })
357
367
  })
358
368
 
369
+ describe('early access', () => {
370
+ test('behavior: checkout requires early access', async () => {
371
+ const app = createApp({ tmp_earlyAccess: true })
372
+ const { cookie, orgId } = await seeded(app)
373
+ const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
374
+ expect(response.status).toBe(403)
375
+ const { requestId, ...body } = (await response.json()) as { requestId?: string }
376
+ expect(requestId).toBeDefined()
377
+ expect(body).toMatchInlineSnapshot(`
378
+ {
379
+ "error": {
380
+ "code": "early_access_required",
381
+ "message": "Requires early access. Ask a teammate with access to invite you by email, or contact Tempo.",
382
+ },
383
+ }
384
+ `)
385
+ })
386
+
387
+ test('behavior: the gate precedes membership resolution', async () => {
388
+ // The gate depends only on the caller, so it leaks nothing about the org;
389
+ // early-access strangers still get the membership 404.
390
+ const db = TestApp.database()
391
+ const app = createApp({ db, tmp_earlyAccess: true })
392
+ await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
393
+ const { orgId } = await seeded(app)
394
+
395
+ const other = await session(app)
396
+ const gated = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(other))
397
+ expect(gated.status).toBe(403)
398
+ expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
399
+
400
+ const granted = await session(app)
401
+ const me = await app.request('/v1/me', { headers: { cookie: granted } })
402
+ const { id: userId } = (await me.json()) as { id: string }
403
+ await Users.setEmail(db, userId, 'stranger@example.org')
404
+ expect(
405
+ (await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(granted))).status,
406
+ ).toBe(404)
407
+ })
408
+
409
+ test('behavior: the gate precedes the owner-role check', async () => {
410
+ const db = TestApp.database()
411
+ const app = createApp({ db, tmp_earlyAccess: true })
412
+ await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
413
+ const { orgId } = await seeded(app)
414
+ const member = await session(app)
415
+ const me = await app.request('/v1/me', { headers: { cookie: member } })
416
+ const { id: userId } = (await me.json()) as { id: string }
417
+ await Memberships.create(db, { orgId, role: 'member', userId })
418
+
419
+ const gated = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(member))
420
+ expect(gated.status).toBe(403)
421
+ expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
422
+
423
+ // Early-access members still fail the owner-role check.
424
+ await Users.setEmail(db, userId, 'member@example.org')
425
+ const write = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(member))
426
+ expect(write.status).toBe(403)
427
+ expect(((await write.json()) as { error: { code: string } }).error.code).toBe('forbidden')
428
+ })
429
+
430
+ test('behavior: early-access owners pass the gate', async () => {
431
+ const db = TestApp.database()
432
+ const app = createApp({ db, tmp_earlyAccess: true })
433
+ await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
434
+ const { cookie, orgId } = await seeded(app)
435
+ const me = await app.request('/v1/me', { headers: { cookie } })
436
+ const { id: userId } = (await me.json()) as { id: string }
437
+ await Users.setEmail(db, userId, 'dev@example.org')
438
+
439
+ // The unreachable Stripe upstream (502) proves the request passed the gate.
440
+ const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
441
+ expect(response.status).toBe(502)
442
+ expect(((await response.json()) as { error: { code: string } }).error.code).toBe('upstream_error') // prettier-ignore
443
+ })
444
+
445
+ test('behavior: management keys pass the gate', async () => {
446
+ const store = TestApp.kvStore({ keys: [] })
447
+ const app = createApp({ tmp_earlyAccess: true, kv: { store } })
448
+ const { orgId } = await seeded(app)
449
+ const { token } = await ApiKeys.mint(store, { orgId, scopes: ['management:write'] })
450
+
451
+ const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, {
452
+ headers: { 'tempo-api-key': token },
453
+ method: 'POST',
454
+ })
455
+ expect(response.status).toBe(502)
456
+ })
457
+
458
+ test('behavior: the super admin passes the gate', async () => {
459
+ const app = createApp({ tmp_earlyAccess: true })
460
+ const { orgId } = await seeded(app)
461
+
462
+ const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, {
463
+ ...as(secret),
464
+ method: 'POST',
465
+ })
466
+ expect(response.status).toBe(502)
467
+ })
468
+ })
469
+
359
470
  describe('POST /orgs/:orgId/billing/stripe/manage', () => {
360
471
  test('behavior: 404 before any billing exists', async () => {
361
472
  const app = createApp()
@@ -380,7 +380,7 @@ export function billing() {
380
380
  )
381
381
  .post(
382
382
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/stripe/checkout',
383
- Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
383
+ Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true, tmp_earlyAccess: true }), // prettier-ignore
384
384
  Auth.ensureOrg({ role: 'owner' }),
385
385
  OpenApi.validate('param', schema.Params, {
386
386
  code: 'param_invalid',
@@ -394,7 +394,10 @@ export function billing() {
394
394
  operationId: 'createStripeCheckout',
395
395
  responses: OpenApi.responses({
396
396
  errors: {
397
- 403: 'Requires the owner role.',
397
+ 403: {
398
+ codes: ['api_key_forbidden', 'early_access_required', 'forbidden'],
399
+ description: 'Requires the owner role and early access.',
400
+ },
398
401
  404: 'No accessible organization was found for this id.',
399
402
  501: 'Billing is not configured on this deployment.',
400
403
  },
@@ -409,6 +412,8 @@ export function billing() {
409
412
  async (c) => {
410
413
  if (Auth.narrowAccess) return Auth.accessError(c)
411
414
  if (Auth.narrowScope) return Auth.ensureOrgError(c)
415
+ if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c)
416
+ if (Auth.tmp_narrowEarlyAccess) return Auth.tmp_ensureEarlyAccessError(c)
412
417
  if (OpenApi.narrowValidation)
413
418
  return OpenApi.validationError(c, {
414
419
  code: 'param_invalid',