tapimo 0.6.0 → 0.7.1

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 (288) hide show
  1. package/dist/App.d.ts +8641 -8263
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +9 -1
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +4 -0
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +5 -0
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/admin/App.d.ts +112 -11
  10. package/dist/admin/App.d.ts.map +1 -1
  11. package/dist/admin/apps/api-keys.d.ts +2 -1
  12. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  13. package/dist/admin/apps/early-access.d.ts +2 -2
  14. package/dist/admin/apps/early-access.d.ts.map +1 -1
  15. package/dist/admin/apps/early-access.js +5 -5
  16. package/dist/admin/apps/early-access.js.map +1 -1
  17. package/dist/admin/apps/organizations.d.ts +124 -1
  18. package/dist/admin/apps/organizations.d.ts.map +1 -1
  19. package/dist/admin/apps/organizations.js +97 -6
  20. package/dist/admin/apps/organizations.js.map +1 -1
  21. package/dist/admin/ui.generated.d.ts +1 -1
  22. package/dist/admin/ui.generated.d.ts.map +1 -1
  23. package/dist/admin/ui.generated.js +47 -39
  24. package/dist/admin/ui.generated.js.map +1 -1
  25. package/dist/analytics/Analytics.d.ts.map +1 -1
  26. package/dist/analytics/Analytics.js +20 -0
  27. package/dist/analytics/Analytics.js.map +1 -1
  28. package/dist/analytics/tables/requestEvents.d.ts +18 -0
  29. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  30. package/dist/analytics/tables/requestEvents.js +36 -7
  31. package/dist/analytics/tables/requestEvents.js.map +1 -1
  32. package/dist/apps/data/App.d.ts +8050 -8019
  33. package/dist/apps/data/App.d.ts.map +1 -1
  34. package/dist/apps/data/FundingQuoteProviders.js +2 -15
  35. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  36. package/dist/apps/data/FxOracle.d.ts +1 -1
  37. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  38. package/dist/apps/data/FxOracle.js +1 -1
  39. package/dist/apps/data/FxOracle.js.map +1 -1
  40. package/dist/apps/data/routes/activities.d.ts +27 -27
  41. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  42. package/dist/apps/data/routes/activities.js +21 -1
  43. package/dist/apps/data/routes/activities.js.map +1 -1
  44. package/dist/apps/data/routes/balances.d.ts +58 -58
  45. package/dist/apps/data/routes/blocks.d.ts +39 -39
  46. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  47. package/dist/apps/data/routes/exchanges.d.ts +669 -669
  48. package/dist/apps/data/routes/fee-amm.d.ts +157 -143
  49. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  50. package/dist/apps/data/routes/fee-amm.js +76 -91
  51. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  52. package/dist/apps/data/routes/funding.d.ts +72 -34
  53. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  54. package/dist/apps/data/routes/funding.js +34 -26
  55. package/dist/apps/data/routes/funding.js.map +1 -1
  56. package/dist/apps/data/routes/indexer.d.ts +30 -30
  57. package/dist/apps/data/routes/receipts.d.ts +8 -8
  58. package/dist/apps/data/routes/rpc.d.ts +31 -31
  59. package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
  60. package/dist/apps/data/routes/tokenlist.js +12 -3
  61. package/dist/apps/data/routes/tokenlist.js.map +1 -1
  62. package/dist/apps/data/routes/tokens.d.ts +4 -4
  63. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  64. package/dist/apps/data/routes/tokens.js +5 -1
  65. package/dist/apps/data/routes/tokens.js.map +1 -1
  66. package/dist/apps/data/routes/transactions.d.ts +6 -6
  67. package/dist/apps/data/routes/transfers.d.ts +101 -90
  68. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  69. package/dist/apps/data/routes/transfers.js +226 -141
  70. package/dist/apps/data/routes/transfers.js.map +1 -1
  71. package/dist/apps/data/routes/valuation.d.ts +2 -2
  72. package/dist/apps/data/routes/valuation.js +3 -3
  73. package/dist/apps/data/routes/valuation.js.map +1 -1
  74. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  75. package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
  76. package/dist/apps/data/routes/verified-tokens.js +34 -9
  77. package/dist/apps/data/routes/verified-tokens.js.map +1 -1
  78. package/dist/apps/data/routes/webhooks.d.ts +2 -2
  79. package/dist/apps/data/routes/webhooks.js +14 -6
  80. package/dist/apps/data/routes/webhooks.js.map +1 -1
  81. package/dist/apps/index.d.ts +1 -0
  82. package/dist/apps/index.d.ts.map +1 -1
  83. package/dist/apps/index.js +1 -0
  84. package/dist/apps/index.js.map +1 -1
  85. package/dist/apps/management/App.d.ts +460 -523
  86. package/dist/apps/management/App.d.ts.map +1 -1
  87. package/dist/apps/management/routes/api-keys.d.ts +7 -23
  88. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  89. package/dist/apps/management/routes/api-keys.js +20 -8
  90. package/dist/apps/management/routes/api-keys.js.map +1 -1
  91. package/dist/apps/management/routes/billing.d.ts +106 -151
  92. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  93. package/dist/apps/management/routes/billing.js +28 -28
  94. package/dist/apps/management/routes/billing.js.map +1 -1
  95. package/dist/apps/management/routes/faucet.d.ts +48 -34
  96. package/dist/apps/management/routes/faucet.d.ts.map +1 -1
  97. package/dist/apps/management/routes/faucet.js +33 -18
  98. package/dist/apps/management/routes/faucet.js.map +1 -1
  99. package/dist/apps/management/routes/invitations.d.ts +131 -131
  100. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  101. package/dist/apps/management/routes/invitations.js +0 -3
  102. package/dist/apps/management/routes/invitations.js.map +1 -1
  103. package/dist/apps/management/routes/me.d.ts +6 -8
  104. package/dist/apps/management/routes/me.d.ts.map +1 -1
  105. package/dist/apps/management/routes/me.js +0 -4
  106. package/dist/apps/management/routes/me.js.map +1 -1
  107. package/dist/apps/management/routes/members.d.ts +73 -73
  108. package/dist/apps/management/routes/orgs.d.ts +94 -94
  109. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  110. package/dist/apps/management/routes/orgs.js +5 -0
  111. package/dist/apps/management/routes/orgs.js.map +1 -1
  112. package/dist/apps/management/routes/usage.d.ts +72 -72
  113. package/dist/apps/management/routes/verified-token-requests.d.ts +94 -94
  114. package/dist/apps/management/routes/webhooks.d.ts +1 -1
  115. package/dist/apps/mpp/App.d.ts +363 -0
  116. package/dist/apps/mpp/App.d.ts.map +1 -0
  117. package/dist/apps/mpp/App.js +72 -0
  118. package/dist/apps/mpp/App.js.map +1 -0
  119. package/dist/apps/mpp/Mpp.d.ts +178 -0
  120. package/dist/apps/mpp/Mpp.d.ts.map +1 -0
  121. package/dist/apps/mpp/Mpp.js +183 -0
  122. package/dist/apps/mpp/Mpp.js.map +1 -0
  123. package/dist/apps/mpp/index.d.ts +3 -0
  124. package/dist/apps/mpp/index.d.ts.map +1 -0
  125. package/dist/apps/mpp/index.js +3 -0
  126. package/dist/apps/mpp/index.js.map +1 -0
  127. package/dist/apps/relay/App.d.ts.map +1 -1
  128. package/dist/apps/relay/App.js +57 -10
  129. package/dist/apps/relay/App.js.map +1 -1
  130. package/dist/apps/relay/Sponsorships.js +1 -1
  131. package/dist/apps/relay/Sponsorships.js.map +1 -1
  132. package/dist/db/Db.d.ts +2 -0
  133. package/dist/db/Db.d.ts.map +1 -1
  134. package/dist/db/Db.js +27 -0
  135. package/dist/db/Db.js.map +1 -1
  136. package/dist/db/Schema.d.ts +12 -0
  137. package/dist/db/Schema.d.ts.map +1 -1
  138. package/dist/db/Schema.js +11 -0
  139. package/dist/db/Schema.js.map +1 -1
  140. package/dist/db/tables/enabledBillingSources.d.ts +73 -0
  141. package/dist/db/tables/enabledBillingSources.d.ts.map +1 -0
  142. package/dist/db/tables/enabledBillingSources.js +69 -0
  143. package/dist/db/tables/enabledBillingSources.js.map +1 -0
  144. package/dist/db/tables/invitations.d.ts +1 -12
  145. package/dist/db/tables/invitations.d.ts.map +1 -1
  146. package/dist/db/tables/invitations.js +3 -23
  147. package/dist/db/tables/invitations.js.map +1 -1
  148. package/dist/db/tables/organizations.d.ts +5 -2
  149. package/dist/db/tables/organizations.d.ts.map +1 -1
  150. package/dist/db/tables/organizations.js +32 -2
  151. package/dist/db/tables/organizations.js.map +1 -1
  152. package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
  153. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  154. package/dist/db/tables/sponsoredTransactions.js +13 -3
  155. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  156. package/dist/handlers/internal/utils.d.ts +1 -16
  157. package/dist/handlers/internal/utils.d.ts.map +1 -1
  158. package/dist/handlers/internal/utils.js +17 -6
  159. package/dist/handlers/internal/utils.js.map +1 -1
  160. package/dist/handlers/relay.d.ts.map +1 -1
  161. package/dist/handlers/relay.js +37 -5
  162. package/dist/handlers/relay.js.map +1 -1
  163. package/dist/internal/Auth.d.ts +0 -32
  164. package/dist/internal/Auth.d.ts.map +1 -1
  165. package/dist/internal/Auth.js +3 -51
  166. package/dist/internal/Auth.js.map +1 -1
  167. package/dist/internal/EdgeCache.d.ts +1 -1
  168. package/dist/internal/EdgeCache.d.ts.map +1 -1
  169. package/dist/internal/EdgeCache.js +3 -2
  170. package/dist/internal/EdgeCache.js.map +1 -1
  171. package/dist/internal/FundingQuotes.d.ts +78 -32
  172. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  173. package/dist/internal/FundingQuotes.js +45 -8
  174. package/dist/internal/FundingQuotes.js.map +1 -1
  175. package/dist/internal/Log.d.ts +18 -4
  176. package/dist/internal/Log.d.ts.map +1 -1
  177. package/dist/internal/Log.js +50 -3
  178. package/dist/internal/Log.js.map +1 -1
  179. package/dist/internal/MetricSink.d.ts.map +1 -1
  180. package/dist/internal/MetricSink.js +15 -0
  181. package/dist/internal/MetricSink.js.map +1 -1
  182. package/dist/internal/Response.d.ts +2 -2
  183. package/dist/internal/Response.d.ts.map +1 -1
  184. package/dist/internal/Response.js +12 -4
  185. package/dist/internal/Response.js.map +1 -1
  186. package/dist/internal/Schema.d.ts +2 -2
  187. package/dist/internal/Schema.d.ts.map +1 -1
  188. package/dist/internal/Schema.js +4 -2
  189. package/dist/internal/Schema.js.map +1 -1
  190. package/dist/internal/Viem.d.ts +1 -1
  191. package/package.json +7 -2
  192. package/src/App.test.ts +9 -4
  193. package/src/App.ts +14 -2
  194. package/src/Client.test-d.ts +109 -28
  195. package/src/Scope.ts +5 -0
  196. package/src/admin/App.test.ts +4 -0
  197. package/src/admin/apps/early-access.ts +6 -5
  198. package/src/admin/apps/organizations.test.ts +86 -2
  199. package/src/admin/apps/organizations.ts +155 -31
  200. package/src/admin/apps/scopes.test.ts +5 -0
  201. package/src/admin/ui/src/lib/organizations.test.ts +81 -1
  202. package/src/admin/ui/src/lib/organizations.ts +92 -0
  203. package/src/admin/ui/src/routes/api-keys.tsx +1 -1
  204. package/src/admin/ui/src/routes/early-access.tsx +8 -3
  205. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +103 -0
  206. package/src/admin/ui.generated.ts +49 -39
  207. package/src/analytics/Analytics.test.ts +83 -0
  208. package/src/analytics/Analytics.ts +20 -0
  209. package/src/analytics/tables/requestEvents.ts +60 -7
  210. package/src/apps/composition.test.ts +11 -2
  211. package/src/apps/data/App.test.ts +14 -2
  212. package/src/apps/data/FundingQuoteProviders.test.ts +0 -5
  213. package/src/apps/data/FundingQuoteProviders.ts +2 -29
  214. package/src/apps/data/FxOracle.test.ts +2 -2
  215. package/src/apps/data/FxOracle.ts +2 -2
  216. package/src/apps/data/routes/activities.test.ts +6 -2
  217. package/src/apps/data/routes/activities.ts +39 -1
  218. package/src/apps/data/routes/balances.test.ts +6 -2
  219. package/src/apps/data/routes/exchanges.test.ts +7 -3
  220. package/src/apps/data/routes/fee-amm.test.ts +47 -23
  221. package/src/apps/data/routes/fee-amm.ts +98 -119
  222. package/src/apps/data/routes/funding.test.ts +29 -8
  223. package/src/apps/data/routes/funding.ts +42 -48
  224. package/src/apps/data/routes/receipts.test.ts +6 -2
  225. package/src/apps/data/routes/tokenlist.test.ts +15 -0
  226. package/src/apps/data/routes/tokenlist.ts +18 -5
  227. package/src/apps/data/routes/tokens.ts +5 -1
  228. package/src/apps/data/routes/transfers.test.ts +203 -35
  229. package/src/apps/data/routes/transfers.ts +294 -172
  230. package/src/apps/data/routes/valuation.test.ts +18 -3
  231. package/src/apps/data/routes/valuation.ts +4 -4
  232. package/src/apps/data/routes/verified-tokens.test.ts +51 -0
  233. package/src/apps/data/routes/verified-tokens.ts +52 -15
  234. package/src/apps/data/routes/webhooks.test.ts +56 -0
  235. package/src/apps/data/routes/webhooks.ts +17 -8
  236. package/src/apps/index.ts +1 -0
  237. package/src/apps/management/routes/api-keys.test.ts +2 -75
  238. package/src/apps/management/routes/api-keys.ts +30 -7
  239. package/src/apps/management/routes/billing.test.ts +106 -85
  240. package/src/apps/management/routes/billing.ts +32 -21
  241. package/src/apps/management/routes/faucet.test.ts +11 -3
  242. package/src/apps/management/routes/faucet.ts +46 -30
  243. package/src/apps/management/routes/invitations.test.ts +0 -172
  244. package/src/apps/management/routes/invitations.ts +0 -3
  245. package/src/apps/management/routes/me.test.ts +3 -145
  246. package/src/apps/management/routes/me.ts +0 -6
  247. package/src/apps/management/routes/orgs.test.ts +45 -2
  248. package/src/apps/management/routes/orgs.ts +6 -0
  249. package/src/apps/management/routes/scopes.test.ts +1 -0
  250. package/src/apps/mpp/App.ts +86 -0
  251. package/src/apps/mpp/Mpp.test.ts +193 -0
  252. package/src/apps/mpp/Mpp.ts +353 -0
  253. package/src/apps/mpp/index.ts +2 -0
  254. package/src/apps/mpp/internal/Adapter.ts +11 -0
  255. package/src/apps/mpp/internal/Error.ts +9 -0
  256. package/src/apps/mpp/internal/Idempotency.ts +7 -0
  257. package/src/apps/mpp/internal/Screening.ts +10 -0
  258. package/src/apps/relay/App.test.ts +90 -8
  259. package/src/apps/relay/App.ts +62 -10
  260. package/src/apps/relay/Sponsorships.test.ts +2 -0
  261. package/src/apps/relay/Sponsorships.ts +1 -1
  262. package/src/db/Db.test.ts +2 -2
  263. package/src/db/Db.ts +30 -0
  264. package/src/db/Schema.ts +15 -0
  265. package/src/db/tables/enabledBillingSources.test.ts +303 -0
  266. package/src/db/tables/enabledBillingSources.ts +118 -0
  267. package/src/db/tables/invitations.ts +4 -26
  268. package/src/db/tables/organizations.test.ts +191 -0
  269. package/src/db/tables/organizations.ts +44 -3
  270. package/src/db/tables/sponsoredTransactions.test.ts +41 -2
  271. package/src/db/tables/sponsoredTransactions.ts +15 -3
  272. package/src/handlers/internal/utils.ts +22 -6
  273. package/src/handlers/relay.errors.test.ts +153 -0
  274. package/src/handlers/relay.ts +42 -7
  275. package/src/internal/Auth.test.ts +3 -3
  276. package/src/internal/Auth.ts +3 -63
  277. package/src/internal/EdgeCache.test.ts +18 -5
  278. package/src/internal/EdgeCache.ts +4 -2
  279. package/src/internal/FundingQuotes.test.ts +18 -5
  280. package/src/internal/FundingQuotes.ts +46 -11
  281. package/src/internal/Log.test.ts +91 -0
  282. package/src/internal/Log.ts +69 -5
  283. package/src/internal/MetricSink.test.ts +55 -0
  284. package/src/internal/MetricSink.ts +14 -0
  285. package/src/internal/Response.test.ts +28 -0
  286. package/src/internal/Response.ts +12 -4
  287. package/src/internal/Schema.test.ts +13 -0
  288. package/src/internal/Schema.ts +14 -7
@@ -51,7 +51,11 @@ const seed: readonly SeedToken[] = JSON.parse(
51
51
  // Deterministic EUR-based rate set so conversion math is exact in assertions.
52
52
  const fixed = FxOracle.from({
53
53
  name: 'fixed',
54
- rates: async () => ({ asOf: '2026-01-01', base: 'EUR', rates: { AUD: '1.6', USD: '1.0' } }),
54
+ rates: async () => ({
55
+ asOf: '2026-01-01T00:00:00.000Z',
56
+ base: 'EUR',
57
+ rates: { AUD: '1.6', USD: '1.0' },
58
+ }),
55
59
  })
56
60
 
57
61
  async function holderWithBalance(client: ReturnType<typeof app>) {
@@ -93,6 +97,17 @@ async function chainSum(holder: `0x${string}`, currency: string) {
93
97
  }
94
98
 
95
99
  describe('GET /addresses/:address/valuation', () => {
100
+ test('requires a full pricing timestamp', () => {
101
+ const pricing = { asOf: '2026-01-01', basis: 'nominal', source: 'fixed' }
102
+ expect(Valuation.schema.Pricing.safeParse(pricing).success).toBe(false)
103
+ expect(
104
+ Valuation.schema.Pricing.safeParse({
105
+ ...pricing,
106
+ asOf: '2026-01-01T00:00:00.000Z',
107
+ }).success,
108
+ ).toBe(true)
109
+ })
110
+
96
111
  test('normalizes the denomination', () => {
97
112
  expect(
98
113
  Valuation.schema.getAddressValuation.Query.parse({ denomination: 'aud' }).denomination,
@@ -176,7 +191,7 @@ describe('GET /addresses/:address/valuation', () => {
176
191
  expect(aud.amount).toBe(core_Value.format((sum * 16n) / 10n, 6))
177
192
  expect(aud.pricing).toMatchInlineSnapshot(`
178
193
  {
179
- "asOf": "2026-01-01",
194
+ "asOf": "2026-01-01T00:00:00.000Z",
180
195
  "basis": "nominal",
181
196
  "source": "fixed",
182
197
  }
@@ -201,7 +216,7 @@ describe('GET /addresses/:address/valuation', () => {
201
216
  expect(Number(body.amount)).toBeGreaterThan(0)
202
217
  expect(body.pricing?.basis).toBe('nominal')
203
218
  expect(body.pricing?.source).toBe('ecb')
204
- expect(body.pricing?.asOf).toMatch(/^\d{4}-\d{2}-\d{2}$/)
219
+ expect(body.pricing?.asOf).toBe('2026-01-01T00:00:00.000Z')
205
220
  })
206
221
 
207
222
  test.skipIf(runtime.mode !== 'localnet')(
@@ -44,11 +44,11 @@ export namespace schema {
44
44
  /** Rate provenance for a converted valuation. */
45
45
  export const Pricing = z
46
46
  .object({
47
- asOf: z
48
- .string()
47
+ asOf: z.iso
48
+ .datetime()
49
49
  .check(
50
- z.describe('Publication date of the rate set used for conversion.'),
51
- z.meta({ examples: ['2026-07-20'] }),
50
+ z.describe('Publication date of the rate set as an ISO 8601 timestamp.'),
51
+ z.meta({ examples: ['2026-07-20T00:00:00.000Z'] }),
52
52
  ),
53
53
  basis: z
54
54
  .literal('nominal')
@@ -87,6 +87,21 @@ describe('GET /verified-tokens', () => {
87
87
  )
88
88
  expect(second.status).toBe(304)
89
89
  })
90
+
91
+ test('refreshes cached reads after a snapshot publish', async () => {
92
+ const db = TestApp.database()
93
+ await VerifiedTokens.replace(db, chainId, seedList)
94
+ const client = TestApp.client({ auth: { keys: [reader] }, db })
95
+
96
+ await client.v1['verified-tokens'].$get({ query: {} }, as(reader))
97
+ await VerifiedTokens.replace(db, chainId, seedList.slice(1))
98
+
99
+ const response = await client.v1['verified-tokens'].$get({ query: {} }, as(reader))
100
+ const body = await TestApp.json(response, Verified.schema.listVerifiedTokens.Response)
101
+ expect(body.data.map((token) => token.address)).toEqual(
102
+ seedList.slice(1).map((token) => token.address),
103
+ )
104
+ })
90
105
  })
91
106
 
92
107
  describe('GET /verified-tokens/currencies', () => {
@@ -97,6 +112,23 @@ describe('GET /verified-tokens/currencies', () => {
97
112
  const body = await TestApp.json(response, Verified.schema.getVerifiedTokenCurrencies.Response)
98
113
  expect([...body.data]).toEqual(['EUR', 'USD'])
99
114
  })
115
+
116
+ test('refreshes cached reads after a snapshot publish', async () => {
117
+ const db = TestApp.database()
118
+ await VerifiedTokens.replace(db, chainId, seedList)
119
+ const client = TestApp.client({ auth: { keys: [reader] }, db })
120
+
121
+ await client.v1['verified-tokens'].currencies.$get({ query: {} }, as(reader))
122
+ await VerifiedTokens.replace(
123
+ db,
124
+ chainId,
125
+ seedList.filter((token) => token.currency === 'USD'),
126
+ )
127
+
128
+ const response = await client.v1['verified-tokens'].currencies.$get({ query: {} }, as(reader))
129
+ const body = await TestApp.json(response, Verified.schema.getVerifiedTokenCurrencies.Response)
130
+ expect([...body.data]).toEqual(['USD'])
131
+ })
100
132
  })
101
133
 
102
134
  describe('GET /verified-tokens/:address', () => {
@@ -123,4 +155,23 @@ describe('GET /verified-tokens/:address', () => {
123
155
  const body = (await response.json()) as { error?: { code?: string } }
124
156
  expect(body.error?.code).toBe('verified_token_not_found')
125
157
  })
158
+
159
+ test('refreshes cached reads after a snapshot publish', async () => {
160
+ const db = TestApp.database()
161
+ await VerifiedTokens.replace(db, chainId, seedList)
162
+ const client = TestApp.client({ auth: { keys: [reader] }, db })
163
+ const address = seedList[0].address
164
+
165
+ await client.v1['verified-tokens'][':address'].$get(
166
+ { param: { address }, query: {} },
167
+ as(reader),
168
+ )
169
+ await VerifiedTokens.replace(db, chainId, seedList.slice(1))
170
+
171
+ const response = await client.v1['verified-tokens'][':address'].$get(
172
+ { param: { address }, query: {} },
173
+ as(reader),
174
+ )
175
+ expect(response.status).toBe(404)
176
+ })
126
177
  })
@@ -4,6 +4,7 @@ import * as z from 'zod/mini'
4
4
  import type * as App from '../../../App.js'
5
5
  import * as Auth from '../../../internal/Auth.js'
6
6
  import * as Cache from '../../../internal/Cache.js'
7
+ import * as EdgeCache from '../../../internal/EdgeCache.js'
7
8
  import * as OpenApi from '../../../internal/OpenApi.js'
8
9
  import * as Response from '../../../internal/Response.js'
9
10
  import * as Schema from '../../../internal/Schema.js'
@@ -168,11 +169,23 @@ export function verifiedTokens() {
168
169
  summary: 'List verified tokens',
169
170
  tags: ['Verified Tokens'],
170
171
  }),
171
- Cache.response<App.Environment>({
172
- cacheControl: Cache.policies.stable,
173
- name: 'tempo-api:verified-tokens:v1',
174
- key: (c) => Cache.urlKey(c, schema.listVerifiedTokens.Query),
175
- }),
172
+ // The shared edge cache is URL-keyed and cannot include the snapshot version.
173
+ EdgeCache.setEligibility(
174
+ Cache.response<App.Environment>({
175
+ cacheControl: Cache.policies.stable,
176
+ name: 'tempo-api:verified-tokens:v1',
177
+ key: async (c) => {
178
+ const base = Cache.urlKey(c, schema.listVerifiedTokens.Query)
179
+ const query = schema.listVerifiedTokens.Query.parse(
180
+ Object.fromEntries(new URL(c.req.url).searchParams),
181
+ )
182
+ const chainId = query.chainId ?? c.get('chainId')
183
+ const { version } = await VerifiedTokens.snapshot(c, chainId)
184
+ return `${base}:v:${version}`
185
+ },
186
+ }),
187
+ () => false,
188
+ ),
176
189
  async (c) => {
177
190
  if (Auth.narrowAccess) return Auth.paidAccessError(c)
178
191
  if (OpenApi.narrowValidation)
@@ -213,11 +226,23 @@ export function verifiedTokens() {
213
226
  summary: 'List verified currencies',
214
227
  tags: ['Verified Tokens'],
215
228
  }),
216
- Cache.response<App.Environment>({
217
- cacheControl: Cache.policies.stable,
218
- name: 'tempo-api:verified-tokens:v1',
219
- key: (c) => Cache.urlKey(c, schema.getVerifiedTokenCurrencies.Query),
220
- }),
229
+ // The shared edge cache is URL-keyed and cannot include the snapshot version.
230
+ EdgeCache.setEligibility(
231
+ Cache.response<App.Environment>({
232
+ cacheControl: Cache.policies.stable,
233
+ name: 'tempo-api:verified-tokens:v1',
234
+ key: async (c) => {
235
+ const base = Cache.urlKey(c, schema.getVerifiedTokenCurrencies.Query)
236
+ const query = schema.getVerifiedTokenCurrencies.Query.parse(
237
+ Object.fromEntries(new URL(c.req.url).searchParams),
238
+ )
239
+ const chainId = query.chainId ?? c.get('chainId')
240
+ const { version } = await VerifiedTokens.snapshot(c, chainId)
241
+ return `${base}:v:${version}`
242
+ },
243
+ }),
244
+ () => false,
245
+ ),
221
246
  async (c) => {
222
247
  if (Auth.narrowAccess) return Auth.paidAccessError(c)
223
248
  if (OpenApi.narrowValidation)
@@ -265,11 +290,23 @@ export function verifiedTokens() {
265
290
  summary: 'Get verified token',
266
291
  tags: ['Verified Tokens'],
267
292
  }),
268
- Cache.response<App.Environment>({
269
- cacheControl: Cache.policies.stable,
270
- name: 'tempo-api:verified-tokens:v1',
271
- key: (c) => Cache.urlKey(c, schema.getVerifiedToken.Query),
272
- }),
293
+ // The shared edge cache is URL-keyed and cannot include the snapshot version.
294
+ EdgeCache.setEligibility(
295
+ Cache.response<App.Environment>({
296
+ cacheControl: Cache.policies.stable,
297
+ name: 'tempo-api:verified-tokens:v1',
298
+ key: async (c) => {
299
+ const base = Cache.urlKey(c, schema.getVerifiedToken.Query)
300
+ const query = schema.getVerifiedToken.Query.parse(
301
+ Object.fromEntries(new URL(c.req.url).searchParams),
302
+ )
303
+ const chainId = query.chainId ?? c.get('chainId')
304
+ const { version } = await VerifiedTokens.snapshot(c, chainId)
305
+ return `${base}:v:${version}`
306
+ },
307
+ }),
308
+ () => false,
309
+ ),
273
310
  async (c) => {
274
311
  if (Auth.narrowAccess) return Auth.paidAccessError(c)
275
312
  if (OpenApi.narrowValidation)
@@ -1736,6 +1736,33 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1736
1736
  `)
1737
1737
  })
1738
1738
 
1739
+ test('block:created preserves the cursor when a block header cannot be read', async () => {
1740
+ const db = TestApp.database()
1741
+ const missingBlock = 4_000_000_000
1742
+ const cursor = encodeCursor([missingBlock - 1, endOfBlockIndex])
1743
+ const subscription = await Core.createSubscription(db, {
1744
+ chainId: 42431,
1745
+ destination: { type: 'url', url: 'https://hooks.example.com/x' },
1746
+ eventType: 'block:created',
1747
+ filters: { number: { eq: `0x${missingBlock.toString(16)}` } },
1748
+ owner: { orgId: 'org_test', type: 'api_key' },
1749
+ })
1750
+ await Core.setCursor(db, subscription.id, cursor)
1751
+
1752
+ await expect(
1753
+ Webhooks.poll({
1754
+ chainId: 42431,
1755
+ eventTypes: ['block:created'],
1756
+ headBlockNumber: missingBlock + 2,
1757
+ client,
1758
+ db,
1759
+ tidx: realTidx,
1760
+ }),
1761
+ ).rejects.toThrow()
1762
+
1763
+ expect(await Core.getCursor(db, subscription.id)).toBe(cursor)
1764
+ })
1765
+
1739
1766
  test('transaction:included filters on txs columns and inner calls', async () => {
1740
1767
  const db = TestApp.database()
1741
1768
  const subscription = await Core.createSubscription(db, {
@@ -1778,6 +1805,35 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1778
1805
  } else expect(tx?.feeToken).toBeUndefined()
1779
1806
  })
1780
1807
 
1808
+ test('transaction:included preserves the cursor when a confirmed block is unavailable', async () => {
1809
+ const db = TestApp.database()
1810
+ const missingBlock = 4_000_000_000
1811
+ const cursor = encodeCursor([missingBlock - 1, endOfBlockIndex])
1812
+ const subscription = await Core.createSubscription(db, {
1813
+ chainId: 42431,
1814
+ destination: { type: 'url', url: 'https://hooks.example.com/x' },
1815
+ eventType: 'transaction:included',
1816
+ filters: { blockNumber: { eq: `0x${missingBlock.toString(16)}` } },
1817
+ owner: { orgId: 'org_test', type: 'api_key' },
1818
+ })
1819
+ await Core.setCursor(db, subscription.id, cursor)
1820
+
1821
+ await expect(
1822
+ Webhooks.poll({
1823
+ chainId: 42431,
1824
+ eventTypes: ['transaction:included'],
1825
+ headBlockNumber: missingBlock + 2,
1826
+ client,
1827
+ db,
1828
+ tidx: realTidx,
1829
+ }),
1830
+ ).rejects.toThrowErrorMatchingInlineSnapshot(`
1831
+ [Webhooks.BlockUnavailableError: Block 4000000000 is unavailable.]
1832
+ `)
1833
+
1834
+ expect(await Core.getCursor(db, subscription.id)).toBe(cursor)
1835
+ })
1836
+
1781
1837
  test('transaction:included matches exact empty calldata through the poller', async () => {
1782
1838
  const db = TestApp.database()
1783
1839
  const subscription = await Core.createSubscription(db, {
@@ -2485,7 +2485,7 @@ type RpcBlock = Awaited<ReturnType<Viem.getClient.ReturnType['getBlock']>>
2485
2485
  /** Per-tick lazy cache of fetched block headers, full blocks, and receipts. */
2486
2486
  type ChainCache = {
2487
2487
  /** Full raw block (with transactions) via `eth_getBlockByNumber(n, true)`. */
2488
- block(n: number): Promise<Record<string, unknown> | undefined>
2488
+ block(n: number): Promise<Record<string, unknown>>
2489
2489
  /** Viem-formatted block header (transaction hashes only) via `getBlock`. */
2490
2490
  header(n: number): Promise<RpcBlock | undefined>
2491
2491
  /** Receipts for a block keyed by lowercased transaction hash. */
@@ -2495,7 +2495,7 @@ type ChainCache = {
2495
2495
  /** Builds a per-tick {@link ChainCache} bound to a viem client. */
2496
2496
  function createChainCache(client: Viem.getClient.ReturnType): ChainCache {
2497
2497
  const headers = new Map<number, Promise<RpcBlock | undefined>>()
2498
- const blocks = new Map<number, Promise<Record<string, unknown> | undefined>>()
2498
+ const blocks = new Map<number, Promise<Record<string, unknown>>>()
2499
2499
  const receipts = new Map<number, Promise<Map<string, Record<string, unknown>>>>()
2500
2500
  return {
2501
2501
  block(n) {
@@ -2506,7 +2506,10 @@ function createChainCache(client: Viem.getClient.ReturnType): ChainCache {
2506
2506
  p = rpc<Record<string, unknown> | null>(client, 'eth_getBlockByNumber', [
2507
2507
  blockTag(n),
2508
2508
  true,
2509
- ]).then((b) => b ?? undefined)
2509
+ ]).then((block) => {
2510
+ if (!block) throw new BlockUnavailableError(n)
2511
+ return block
2512
+ })
2510
2513
  blocks.set(n, p)
2511
2514
  }
2512
2515
  return p
@@ -2514,9 +2517,7 @@ function createChainCache(client: Viem.getClient.ReturnType): ChainCache {
2514
2517
  header(n) {
2515
2518
  let p = headers.get(n)
2516
2519
  if (!p) {
2517
- p = client
2518
- .getBlock({ blockNumber: BigInt(n), includeTransactions: false })
2519
- .catch(() => undefined)
2520
+ p = client.getBlock({ blockNumber: BigInt(n), includeTransactions: false })
2520
2521
  headers.set(n, p)
2521
2522
  }
2522
2523
  return p
@@ -2999,10 +3000,10 @@ async function scanTxs(options: {
2999
3000
  const items: ScanItem[] = []
3000
3001
  for (let n = window.from; n <= window.to; n++) {
3001
3002
  const block = await cache.block(n)
3002
- const txs = Array.isArray(block?.['transactions']) ? (block!['transactions'] as unknown[]) : []
3003
+ const txs = Array.isArray(block['transactions']) ? (block['transactions'] as unknown[]) : []
3003
3004
  if (txs.length === 0) continue
3004
3005
  const receipts = needsReceipt || includeReceipt ? await cache.receipts(n) : undefined
3005
- const timestamp = asString(block?.['timestamp'])
3006
+ const timestamp = asString(block['timestamp'])
3006
3007
 
3007
3008
  for (let i = 0; i < txs.length; i++) {
3008
3009
  const tx = txs[i] as Record<string, unknown>
@@ -3195,3 +3196,11 @@ function unauthorized(c: Context<App.Environment>) {
3195
3196
  status: 401,
3196
3197
  })
3197
3198
  }
3199
+
3200
+ class BlockUnavailableError extends Error {
3201
+ override name = 'Webhooks.BlockUnavailableError'
3202
+
3203
+ constructor(blockNumber: number) {
3204
+ super(`Block ${blockNumber} is unavailable.`)
3205
+ }
3206
+ }
package/src/apps/index.ts CHANGED
@@ -1,4 +1,5 @@
1
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
+ export { mpp } from './mpp/index.js'
4
5
  export { relay } from './relay/index.js'
@@ -5,9 +5,7 @@ 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'
9
8
  import * as Memberships from '../../../db/tables/memberships.js'
10
- import * as Users from '../../../db/tables/users.js'
11
9
  import * as Keys from './api-keys.js'
12
10
  import * as Me from './me.js'
13
11
  import * as Orgs from './orgs.js'
@@ -45,7 +43,6 @@ function createApp(
45
43
  db?: ReturnType<typeof TestApp.database>
46
44
  kv?: { store: State }
47
45
  scopes?: readonly Scope.Entry[]
48
- tmp_earlyAccess?: boolean
49
46
  } = {},
50
47
  ) {
51
48
  return TestApp.create({
@@ -54,7 +51,6 @@ function createApp(
54
51
  ...(options.kv ? { kv: options.kv } : {}),
55
52
  ...(options.scopes ? { scopes: options.scopes } : {}),
56
53
  session: { wallet: { origin } },
57
- ...(options.tmp_earlyAccess === undefined ? {} : { tmp_earlyAccess: options.tmp_earlyAccess }),
58
54
  })
59
55
  }
60
56
 
@@ -677,66 +673,9 @@ describe('keys', () => {
677
673
  })
678
674
 
679
675
  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' })
676
+ test('does not require an early-access grant', async () => {
677
+ const app = createApp()
734
678
  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
679
  const { org, project } = await fixture(app, cookie)
741
680
 
742
681
  const minted = await app.request(
@@ -745,16 +684,4 @@ describe('early access', () => {
745
684
  )
746
685
  expect(minted.status).toBe(200)
747
686
  })
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
687
  })
@@ -1,6 +1,8 @@
1
1
  import { type Context, Hono } from 'hono'
2
2
  import * as z from 'zod/mini'
3
3
 
4
+ import * as Analytics from '../../../analytics/Analytics.js'
5
+ import * as RequestEvents from '../../../analytics/tables/requestEvents.js'
4
6
  import * as ApiKeys from '../../../ApiKeys.js'
5
7
  import type * as App from '../../../App.js'
6
8
  import * as Db from '../../../db/Db.js'
@@ -43,6 +45,12 @@ export namespace schema {
43
45
  z.describe('Opaque key id (`key_…`).'),
44
46
  z.meta({ examples: ['key_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
45
47
  ),
48
+ lastUsedAt: z
49
+ .optional(z.string())
50
+ .check(
51
+ z.describe('When the key last authenticated a request (ISO 8601).'),
52
+ z.meta({ examples: ['2026-07-21T12:34:56.000Z'] }),
53
+ ),
46
54
  name: z
47
55
  .optional(z.string())
48
56
  .check(z.describe('Human-readable key name.'), z.meta({ examples: ['CI'] })),
@@ -225,7 +233,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
225
233
  return new Hono<App.Environment>()
226
234
  .post(
227
235
  '/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys',
228
- Auth.policy({ session: true, tmp_earlyAccess: true }),
236
+ Auth.policy({ session: true }),
229
237
  Auth.ensureOrg({ role: 'admin' }),
230
238
  Auth.ensureProject(),
231
239
  OpenApi.validate('param', schema.Params, {
@@ -243,8 +251,8 @@ export function apiKeys(options: apiKeys.Options = {}) {
243
251
  responses: OpenApi.responses({
244
252
  errors: {
245
253
  403: {
246
- codes: ['scope_not_issuable', 'early_access_required'],
247
- description: 'The requested scopes are not issuable, or the caller lacks early access.', // prettier-ignore
254
+ codes: ['scope_not_issuable'],
255
+ description: 'The requested scopes are not issuable.',
248
256
  },
249
257
  404: 'No accessible project was found for this id.',
250
258
  },
@@ -259,7 +267,6 @@ export function apiKeys(options: apiKeys.Options = {}) {
259
267
  async (c) => {
260
268
  if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
261
269
  if (Auth.narrowScope) return Auth.ensureProjectError(c)
262
- if (Auth.tmp_narrowEarlyAccess) return Auth.tmp_ensureEarlyAccessError(c)
263
270
  if (OpenApi.narrowValidation)
264
271
  return OpenApi.validationError(c, {
265
272
  code: 'param_invalid',
@@ -375,7 +382,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
375
382
  })
376
383
  return c.json(
377
384
  Response.validated(schema.listApiKeys.Response, {
378
- data: records.map(serializeKey),
385
+ data: await serializeKeys(c, records),
379
386
  }),
380
387
  200,
381
388
  )
@@ -436,7 +443,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
436
443
  })
437
444
  return c.json(
438
445
  Response.validated(schema.listOrgApiKeys.Response, {
439
- data: records.map(serializeKey),
446
+ data: await serializeKeys(c, records),
440
447
  }),
441
448
  200,
442
449
  )
@@ -506,14 +513,30 @@ export declare namespace apiKeys {
506
513
  }
507
514
  }
508
515
 
516
+ /** Enriches key records with their latest authenticated request timestamp. */
517
+ async function serializeKeys(c: Context<App.Environment>, records: readonly ApiKeys.Record[]) {
518
+ const source = c.get('analytics')
519
+ if (!source || records.length === 0) return records.map((record) => serializeKey(record))
520
+ const lastUsedAt = new Map(
521
+ (
522
+ await RequestEvents.readLastUsedAt(
523
+ Analytics.get(source),
524
+ records.map(({ id }) => id),
525
+ )
526
+ ).map((usage) => [usage.apiKeyId, usage.lastUsedAt]),
527
+ )
528
+ return records.map((record) => serializeKey(record, lastUsedAt.get(record.id)))
529
+ }
530
+
509
531
  /** Projects a persisted record onto the management key shape (no rate limits, no token). */
510
- function serializeKey(record: ApiKeys.Record) {
532
+ function serializeKey(record: ApiKeys.Record, lastUsedAt?: string) {
511
533
  return {
512
534
  createdAt: record.createdAt,
513
535
  ...(record.createdBy === undefined ? {} : { createdBy: record.createdBy }),
514
536
  environment: record.environment,
515
537
  ...(record.expiresAt === undefined ? {} : { expiresAt: record.expiresAt }),
516
538
  id: record.id,
539
+ ...(lastUsedAt === undefined ? {} : { lastUsedAt }),
517
540
  ...(record.name === undefined ? {} : { name: record.name }),
518
541
  orgId: record.orgId,
519
542
  ...(record.projectId === undefined ? {} : { projectId: record.projectId }),