tapimo 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +10 -7
  2. package/dist/ApiKey.d.ts +3 -0
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +10 -0
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +7 -3
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +9 -2
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +20434 -19484
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +62 -4
  13. package/dist/App.js.map +1 -1
  14. package/dist/admin/App.d.ts +84 -74
  15. package/dist/admin/App.d.ts.map +1 -1
  16. package/dist/admin/apps/api-keys.d.ts +14 -1
  17. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  18. package/dist/admin/apps/api-keys.js +6 -2
  19. package/dist/admin/apps/api-keys.js.map +1 -1
  20. package/dist/admin/apps/organizations.d.ts +2 -0
  21. package/dist/admin/apps/organizations.d.ts.map +1 -1
  22. package/dist/admin/apps/verified-tokens.d.ts +168 -168
  23. package/dist/admin/ui.generated.d.ts +1 -1
  24. package/dist/admin/ui.generated.js +67 -67
  25. package/dist/admin/ui.generated.js.map +1 -1
  26. package/dist/analytics/Analytics.d.ts +2 -0
  27. package/dist/analytics/Analytics.d.ts.map +1 -1
  28. package/dist/analytics/Analytics.js +3 -0
  29. package/dist/analytics/Analytics.js.map +1 -1
  30. package/dist/apps/data/App.d.ts +10305 -12785
  31. package/dist/apps/data/App.d.ts.map +1 -1
  32. package/dist/apps/data/App.js +4 -8
  33. package/dist/apps/data/App.js.map +1 -1
  34. package/dist/apps/data/routes/activities.d.ts +24 -24
  35. package/dist/apps/data/routes/addresses.d.ts +288 -288
  36. package/dist/apps/data/routes/balances.d.ts +6 -6
  37. package/dist/apps/data/routes/blocks.d.ts +39 -39
  38. package/dist/apps/data/routes/coingecko.d.ts +170 -170
  39. package/dist/apps/data/routes/earn.d.ts +436 -6
  40. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  41. package/dist/apps/data/routes/earn.js +631 -4
  42. package/dist/apps/data/routes/earn.js.map +1 -1
  43. package/dist/apps/data/routes/exchanges.d.ts +10 -10
  44. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  45. package/dist/apps/data/routes/indexer.d.ts +6 -6
  46. package/dist/apps/data/routes/indexer.js +4 -1
  47. package/dist/apps/data/routes/indexer.js.map +1 -1
  48. package/dist/apps/data/routes/receipts.d.ts +235 -235
  49. package/dist/apps/data/routes/rpc.d.ts +1 -1
  50. package/dist/apps/data/routes/rpc.js +4 -1
  51. package/dist/apps/data/routes/rpc.js.map +1 -1
  52. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  53. package/dist/apps/data/routes/tokens.d.ts +6 -6
  54. package/dist/apps/data/routes/transactions.d.ts +2 -2
  55. package/dist/apps/data/routes/transfers.d.ts +1 -1
  56. package/dist/apps/data/routes/valuation.d.ts +1 -1
  57. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  58. package/dist/apps/data/routes/webhooks.d.ts +9 -9
  59. package/dist/apps/data/routes/zones.d.ts +14 -14
  60. package/dist/apps/funding/Access.d.ts +16 -0
  61. package/dist/apps/funding/Access.d.ts.map +1 -0
  62. package/dist/apps/funding/Access.js +22 -0
  63. package/dist/apps/funding/Access.js.map +1 -0
  64. package/dist/apps/{data/routes/funding.d.ts → funding/App.d.ts} +1717 -3035
  65. package/dist/apps/funding/App.d.ts.map +1 -0
  66. package/dist/apps/funding/App.js +46 -0
  67. package/dist/apps/funding/App.js.map +1 -0
  68. package/dist/apps/funding/Inventory.d.ts +5 -0
  69. package/dist/apps/funding/Inventory.d.ts.map +1 -0
  70. package/dist/apps/funding/Inventory.js +25 -0
  71. package/dist/apps/funding/Inventory.js.map +1 -0
  72. package/dist/apps/funding/ProviderAttempts.d.ts +5 -0
  73. package/dist/apps/funding/ProviderAttempts.d.ts.map +1 -0
  74. package/dist/apps/funding/ProviderAttempts.js +24 -0
  75. package/dist/apps/funding/ProviderAttempts.js.map +1 -0
  76. package/dist/apps/funding/index.d.ts +2 -0
  77. package/dist/apps/funding/index.d.ts.map +1 -0
  78. package/dist/apps/funding/index.js +2 -0
  79. package/dist/apps/funding/index.js.map +1 -0
  80. package/dist/apps/funding/routes/chains.d.ts +169 -0
  81. package/dist/apps/funding/routes/chains.d.ts.map +1 -0
  82. package/dist/apps/funding/routes/chains.js +80 -0
  83. package/dist/apps/funding/routes/chains.js.map +1 -0
  84. package/dist/apps/funding/routes/deposit-addresses.d.ts +2388 -0
  85. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
  86. package/dist/apps/funding/routes/deposit-addresses.js +860 -0
  87. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -0
  88. package/dist/apps/funding/routes/providers.d.ts +91 -0
  89. package/dist/apps/funding/routes/providers.d.ts.map +1 -0
  90. package/dist/apps/funding/routes/providers.js +56 -0
  91. package/dist/apps/funding/routes/providers.js.map +1 -0
  92. package/dist/apps/funding/routes/quotes.d.ts +426 -0
  93. package/dist/apps/funding/routes/quotes.d.ts.map +1 -0
  94. package/dist/apps/funding/routes/quotes.js +188 -0
  95. package/dist/apps/funding/routes/quotes.js.map +1 -0
  96. package/dist/apps/funding/routes/transfers.d.ts +1999 -0
  97. package/dist/apps/funding/routes/transfers.d.ts.map +1 -0
  98. package/dist/apps/funding/routes/transfers.js +614 -0
  99. package/dist/apps/funding/routes/transfers.js.map +1 -0
  100. package/dist/apps/index.d.ts +1 -0
  101. package/dist/apps/index.d.ts.map +1 -1
  102. package/dist/apps/index.js +1 -0
  103. package/dist/apps/index.js.map +1 -1
  104. package/dist/apps/management/App.d.ts +430 -165
  105. package/dist/apps/management/App.d.ts.map +1 -1
  106. package/dist/apps/management/routes/api-keys.d.ts +298 -21
  107. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  108. package/dist/apps/management/routes/api-keys.js +75 -1
  109. package/dist/apps/management/routes/api-keys.js.map +1 -1
  110. package/dist/apps/management/routes/billing.d.ts +6 -6
  111. package/dist/apps/management/routes/faucet.d.ts +35 -35
  112. package/dist/apps/management/routes/invitations.d.ts +48 -48
  113. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  114. package/dist/apps/management/routes/members.d.ts +69 -69
  115. package/dist/apps/management/routes/orgs.d.ts +37 -37
  116. package/dist/apps/management/routes/projects.d.ts +25 -25
  117. package/dist/apps/management/routes/scopes.d.ts +1 -1
  118. package/dist/apps/management/routes/usage.d.ts +72 -72
  119. package/dist/apps/management/routes/verified-token-requests.d.ts +18 -18
  120. package/dist/apps/management/routes/webhooks.d.ts +128 -128
  121. package/dist/apps/mcp.d.ts +2 -3
  122. package/dist/apps/mcp.d.ts.map +1 -1
  123. package/dist/apps/mcp.js +16 -14
  124. package/dist/apps/mcp.js.map +1 -1
  125. package/dist/apps/metadata.d.ts +4 -0
  126. package/dist/apps/metadata.d.ts.map +1 -1
  127. package/dist/apps/metadata.js +10 -0
  128. package/dist/apps/metadata.js.map +1 -1
  129. package/dist/apps/mpp/App.d.ts +2 -2
  130. package/dist/cloudflare.d.ts +50 -0
  131. package/dist/cloudflare.d.ts.map +1 -1
  132. package/dist/cloudflare.js +178 -0
  133. package/dist/cloudflare.js.map +1 -1
  134. package/dist/db/Db.d.ts +2 -0
  135. package/dist/db/Db.d.ts.map +1 -1
  136. package/dist/db/Db.js +37 -0
  137. package/dist/db/Db.js.map +1 -1
  138. package/dist/db/Schema.d.ts +12 -0
  139. package/dist/db/Schema.d.ts.map +1 -1
  140. package/dist/db/Schema.js +20 -0
  141. package/dist/db/Schema.js.map +1 -1
  142. package/dist/db/tables/fundingDepositAddresses.d.ts +13 -0
  143. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  144. package/dist/db/tables/fundingDepositAddresses.js +12 -0
  145. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  146. package/dist/db/tables/fundingDepositRequestObservations.d.ts +45 -0
  147. package/dist/db/tables/fundingDepositRequestObservations.d.ts.map +1 -0
  148. package/dist/db/tables/fundingDepositRequestObservations.js +46 -0
  149. package/dist/db/tables/fundingDepositRequestObservations.js.map +1 -0
  150. package/dist/db/tables/fundingDeposits.d.ts +57 -1
  151. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  152. package/dist/db/tables/fundingDeposits.js +82 -0
  153. package/dist/db/tables/fundingDeposits.js.map +1 -1
  154. package/dist/internal/Auth.d.ts +11 -11
  155. package/dist/internal/Auth.d.ts.map +1 -1
  156. package/dist/internal/Auth.js +41 -15
  157. package/dist/internal/Auth.js.map +1 -1
  158. package/dist/internal/Cache.js +1 -1
  159. package/dist/internal/Cache.js.map +1 -1
  160. package/dist/internal/EdgeCache.d.ts +2 -2
  161. package/dist/internal/EdgeCache.d.ts.map +1 -1
  162. package/dist/internal/EdgeCache.js +10 -4
  163. package/dist/internal/EdgeCache.js.map +1 -1
  164. package/dist/internal/IpAllowlist.d.ts +15 -0
  165. package/dist/internal/IpAllowlist.d.ts.map +1 -0
  166. package/dist/internal/IpAllowlist.js +27 -0
  167. package/dist/internal/IpAllowlist.js.map +1 -0
  168. package/dist/internal/OpenApi.d.ts +1 -1
  169. package/dist/internal/OpenApi.js +1 -1
  170. package/dist/internal/OpenApi.js.map +1 -1
  171. package/dist/internal/Tidx.d.ts +6 -0
  172. package/dist/internal/Tidx.d.ts.map +1 -1
  173. package/dist/internal/Tidx.js +13 -3
  174. package/dist/internal/Tidx.js.map +1 -1
  175. package/dist/internal/Viem.d.ts +8 -0
  176. package/dist/internal/Viem.d.ts.map +1 -1
  177. package/dist/internal/Viem.js +12 -5
  178. package/dist/internal/Viem.js.map +1 -1
  179. package/dist/internal/funding/Deposit.d.ts +42 -0
  180. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  181. package/dist/internal/funding/Deposit.js +37 -5
  182. package/dist/internal/funding/Deposit.js.map +1 -1
  183. package/dist/internal/funding/Evidence.d.ts +1 -5
  184. package/dist/internal/funding/Evidence.d.ts.map +1 -1
  185. package/dist/internal/funding/Evidence.js +4 -3
  186. package/dist/internal/funding/Evidence.js.map +1 -1
  187. package/dist/internal/funding/Provider.d.ts +102 -0
  188. package/dist/internal/funding/Provider.d.ts.map +1 -1
  189. package/dist/internal/funding/Provider.js +5 -0
  190. package/dist/internal/funding/Provider.js.map +1 -1
  191. package/dist/internal/funding/Reconciliation.d.ts +16 -2
  192. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  193. package/dist/internal/funding/Reconciliation.js +257 -48
  194. package/dist/internal/funding/Reconciliation.js.map +1 -1
  195. package/dist/internal/funding/SourceObservation.d.ts +108 -0
  196. package/dist/internal/funding/SourceObservation.d.ts.map +1 -0
  197. package/dist/internal/funding/SourceObservation.js +143 -0
  198. package/dist/internal/funding/SourceObservation.js.map +1 -0
  199. package/dist/internal/funding/Subsidy.d.ts +2 -2
  200. package/dist/internal/funding/Subsidy.d.ts.map +1 -1
  201. package/dist/internal/funding/Subsidy.js +1 -1
  202. package/dist/internal/funding/Subsidy.js.map +1 -1
  203. package/dist/internal/funding/index.d.ts +1 -0
  204. package/dist/internal/funding/index.d.ts.map +1 -1
  205. package/dist/internal/funding/index.js +1 -0
  206. package/dist/internal/funding/index.js.map +1 -1
  207. package/dist/internal/funding/providers/relay.d.ts +43 -1
  208. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  209. package/dist/internal/funding/providers/relay.js +434 -27
  210. package/dist/internal/funding/providers/relay.js.map +1 -1
  211. package/package.json +3 -3
  212. package/src/ApiKey.test.ts +35 -0
  213. package/src/ApiKey.ts +27 -0
  214. package/src/ApiKeys.test.ts +29 -0
  215. package/src/ApiKeys.ts +11 -3
  216. package/src/App.test.ts +23 -5
  217. package/src/App.ts +76 -5
  218. package/src/Client.test-d.ts +171 -19
  219. package/src/admin/apps/api-keys.test.ts +25 -1
  220. package/src/admin/apps/api-keys.ts +6 -2
  221. package/src/admin/ui.generated.ts +67 -67
  222. package/src/analytics/Analytics.ts +4 -0
  223. package/src/apps/composition.test.ts +100 -2
  224. package/src/apps/data/App.test.ts +0 -12
  225. package/src/apps/data/App.ts +4 -8
  226. package/src/apps/data/routes/earn.test.ts +283 -0
  227. package/src/apps/data/routes/earn.ts +800 -3
  228. package/src/apps/data/routes/indexer.ts +2 -1
  229. package/src/apps/data/routes/rpc.ts +2 -0
  230. package/src/apps/data/routes/tokens.test.ts +6 -8
  231. package/src/apps/funding/Access.ts +26 -0
  232. package/src/apps/funding/App.test.ts +78 -0
  233. package/src/apps/funding/App.ts +63 -0
  234. package/src/apps/funding/Inventory.ts +30 -0
  235. package/src/apps/funding/ProviderAttempts.ts +26 -0
  236. package/src/apps/funding/index.ts +1 -0
  237. package/src/apps/funding/routes/chains.test.ts +78 -0
  238. package/src/apps/funding/routes/chains.ts +101 -0
  239. package/src/apps/funding/routes/deposit-addresses.test.ts +1628 -0
  240. package/src/apps/funding/routes/deposit-addresses.ts +1015 -0
  241. package/src/apps/funding/routes/providers.test.ts +165 -0
  242. package/src/apps/funding/routes/providers.ts +73 -0
  243. package/src/apps/funding/routes/quotes.test.ts +511 -0
  244. package/src/apps/funding/routes/quotes.ts +205 -0
  245. package/src/apps/funding/routes/transfers.test.ts +1255 -0
  246. package/src/apps/funding/routes/transfers.ts +704 -0
  247. package/src/apps/index.ts +1 -0
  248. package/src/apps/management/routes/api-keys.test.ts +98 -0
  249. package/src/apps/management/routes/api-keys.ts +96 -1
  250. package/src/apps/mcp.test.ts +17 -2
  251. package/src/apps/mcp.ts +18 -13
  252. package/src/apps/metadata.ts +12 -0
  253. package/src/apps/mpp/internal/Policy.test.ts +7 -1
  254. package/src/cloudflare.ts +279 -0
  255. package/src/db/Db.ts +62 -0
  256. package/src/db/Schema.ts +24 -0
  257. package/src/db/tables/fundingDepositAddresses.ts +28 -0
  258. package/src/db/tables/fundingDepositRequestObservations.test.ts +106 -0
  259. package/src/db/tables/fundingDepositRequestObservations.ts +99 -0
  260. package/src/db/tables/fundingDeposits.test.ts +138 -0
  261. package/src/db/tables/fundingDeposits.ts +145 -1
  262. package/src/internal/Auth.test.ts +177 -5
  263. package/src/internal/Auth.ts +48 -18
  264. package/src/internal/Cache.test.ts +37 -3
  265. package/src/internal/Cache.ts +1 -1
  266. package/src/internal/EdgeCache.test.ts +7 -7
  267. package/src/internal/EdgeCache.ts +13 -4
  268. package/src/internal/IpAllowlist.test.ts +52 -0
  269. package/src/internal/IpAllowlist.ts +39 -0
  270. package/src/internal/Log.test.ts +7 -1
  271. package/src/internal/OpenApi.ts +1 -1
  272. package/src/internal/Tidx.test.ts +28 -0
  273. package/src/internal/Tidx.ts +19 -3
  274. package/src/internal/Viem.test.ts +28 -0
  275. package/src/internal/Viem.ts +21 -5
  276. package/src/internal/funding/Deposit.ts +74 -5
  277. package/src/internal/funding/Evidence.test.ts +2 -2
  278. package/src/internal/funding/Evidence.ts +4 -9
  279. package/src/internal/funding/Provider.ts +115 -0
  280. package/src/internal/funding/Reconciliation.local.test.ts +7 -0
  281. package/src/internal/funding/Reconciliation.test.ts +1052 -35
  282. package/src/internal/funding/Reconciliation.ts +328 -56
  283. package/src/internal/funding/SourceObservation.test.ts +301 -0
  284. package/src/internal/funding/SourceObservation.ts +267 -0
  285. package/src/internal/funding/Subsidy.ts +2 -2
  286. package/src/internal/funding/index.test.ts +1 -0
  287. package/src/internal/funding/index.ts +1 -0
  288. package/src/internal/funding/providers/relay.test.ts +94 -4
  289. package/src/internal/funding/providers/relay.ts +528 -27
  290. package/dist/apps/data/routes/funding.d.ts.map +0 -1
  291. package/dist/apps/data/routes/funding.js +0 -1557
  292. package/dist/apps/data/routes/funding.js.map +0 -1
  293. package/src/apps/data/routes/funding.test.ts +0 -2994
  294. package/src/apps/data/routes/funding.ts +0 -1821
@@ -77,6 +77,79 @@ describe('listByAddress', () => {
77
77
  })
78
78
  })
79
79
 
80
+ describe('listByRecipient', () => {
81
+ test('pages recipient deposits across addresses and hides foreign ownership', async () => {
82
+ const recipient = '0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD'
83
+ const firstAddress = await seedAddress({
84
+ snapshot: TestFunding.depositAddressSnapshot({
85
+ provider: { id: 'relay_recipient_first', name: 'Relay' },
86
+ recipient,
87
+ }),
88
+ })
89
+ const secondAddress = await seedAddress({
90
+ snapshot: TestFunding.depositAddressSnapshot({
91
+ provider: { id: 'relay_recipient_second', name: 'Relay' },
92
+ recipient,
93
+ subsidize: true,
94
+ }),
95
+ })
96
+ const first = await seedDeposit(firstAddress)
97
+ const second = await seedDeposit(secondAddress)
98
+ const third = await seedDeposit(firstAddress)
99
+ const unrelatedAddress = await seedAddress()
100
+ await seedDeposit(unrelatedAddress)
101
+ const foreignAddress = await seedAddress({
102
+ orgId: 'org_other',
103
+ snapshot: TestFunding.depositAddressSnapshot({
104
+ provider: { id: 'relay_recipient_foreign', name: 'Relay' },
105
+ recipient,
106
+ }),
107
+ })
108
+ await seedDeposit(foreignAddress)
109
+
110
+ const page = await FundingDeposits.listByRecipient(db, {
111
+ limit: 2,
112
+ owner,
113
+ recipient: recipient.toLowerCase(),
114
+ })
115
+ expect(page.map((row) => row.id)).toEqual([third.id, second.id])
116
+ const rest = await FundingDeposits.listByRecipient(db, {
117
+ cursor: { createdAt: second.createdAt, id: second.id },
118
+ limit: 2,
119
+ owner,
120
+ recipient: recipient.toLowerCase(),
121
+ })
122
+ expect(rest.map((row) => row.id)).toEqual([first.id])
123
+ })
124
+
125
+ test('narrows project-attributed owners to their project', async () => {
126
+ const recipient = `0x${'ab'.repeat(20)}`
127
+ const firstAddress = await seedAddress({
128
+ projectId: 'prj_deposit',
129
+ snapshot: TestFunding.depositAddressSnapshot({
130
+ provider: { id: 'relay_recipient_project', name: 'Relay' },
131
+ recipient,
132
+ }),
133
+ })
134
+ const secondAddress = await seedAddress({
135
+ projectId: 'prj_other',
136
+ snapshot: TestFunding.depositAddressSnapshot({
137
+ provider: { id: 'relay_recipient_other_project', name: 'Relay' },
138
+ recipient,
139
+ }),
140
+ })
141
+ const first = await seedDeposit(firstAddress)
142
+ await seedDeposit(secondAddress)
143
+
144
+ const rows = await FundingDeposits.listByRecipient(db, {
145
+ limit: 10,
146
+ owner: { ...owner, projectId: 'prj_deposit' },
147
+ recipient,
148
+ })
149
+ expect(rows.map((row) => row.id)).toEqual([first.id])
150
+ })
151
+ })
152
+
80
153
  describe('getOwned', () => {
81
154
  test('returns owned deposits and hides foreign ownership', async () => {
82
155
  const address = await seedAddress()
@@ -227,6 +300,71 @@ describe('update', () => {
227
300
  })
228
301
  })
229
302
 
303
+ describe('recordRequestObservation', () => {
304
+ test('attaches the first poll and webhook timestamps without changing material state', async () => {
305
+ const address = await seedAddress()
306
+ const record = await seedDeposit(address, { providerRequestId: 'relay_observed' })
307
+ await FundingDeposits.recordRequestObservation(db, {
308
+ depositAddressId: address.id,
309
+ pollObservedAt: '2026-01-02T00:00:02.000Z',
310
+ providerRequestId: 'relay_observed',
311
+ webhookReceivedAt: '2026-01-02T00:00:03.000Z',
312
+ })
313
+ await FundingDeposits.recordRequestObservation(db, {
314
+ depositAddressId: address.id,
315
+ pollObservedAt: '2026-01-02T00:00:01.000Z',
316
+ providerRequestId: 'relay_observed',
317
+ webhookReceivedAt: '2026-01-02T00:00:04.000Z',
318
+ })
319
+
320
+ expect(await FundingDeposits.get(db, record.id)).toMatchInlineSnapshot(
321
+ {
322
+ createdAt: expect.any(String),
323
+ depositAddressId: expect.any(String),
324
+ id: expect.any(String),
325
+ providerOutputToken: expect.any(Object),
326
+ snapshot: expect.any(Object),
327
+ updatedAt: expect.any(String),
328
+ },
329
+ `
330
+ {
331
+ "createdAt": Any<String>,
332
+ "depositAddressId": Any<String>,
333
+ "detectionTrigger": null,
334
+ "environment": "production",
335
+ "id": Any<String>,
336
+ "orgId": "org_deposits",
337
+ "pollObservedAt": "2026-01-02T00:00:01.000Z",
338
+ "projectId": null,
339
+ "providerOutputAmount": null,
340
+ "providerOutputToken": Any<Object>,
341
+ "providerRequestId": "relay_observed",
342
+ "providerRequestIds": [
343
+ "relay_observed",
344
+ ],
345
+ "providerState": null,
346
+ "providerTransactionHashes": [],
347
+ "providerTransferIndex": 0,
348
+ "retryState": null,
349
+ "settlementTransaction": null,
350
+ "settlementTransactionHash": null,
351
+ "snapshot": Any<Object>,
352
+ "sourceChainId": "tron:0x2b6653dc",
353
+ "sourceTransactionHash": "0000000000000000000000000000000000000000000000000000000000000011",
354
+ "sourceTransferIndex": 0,
355
+ "status": "detected",
356
+ "statusReason": null,
357
+ "subsidyAmount": null,
358
+ "tempoGasPaid": null,
359
+ "updatedAt": Any<String>,
360
+ "version": 1,
361
+ "webhookReceivedAt": "2026-01-02T00:00:03.000Z",
362
+ }
363
+ `,
364
+ )
365
+ })
366
+ })
367
+
230
368
  describe('toPublic', () => {
231
369
  test('omits private reconciliation and settlement state', async () => {
232
370
  const address = await seedAddress()
@@ -1,4 +1,4 @@
1
- import type { JSONColumnType, Selectable } from 'kysely'
1
+ import { sql, type JSONColumnType, type Selectable } from 'kysely'
2
2
 
3
3
  import type * as Db from '../Db.js'
4
4
  import type * as db_Schema from '../Schema.js'
@@ -40,6 +40,63 @@ export function insert(db: Db.Db, record: Record): Promise<Record> {
40
40
  .executeTakeFirstOrThrow()
41
41
  }
42
42
 
43
+ /** Inserts a chain-observed source transfer or returns its existing deposit. */
44
+ export async function insertOrGetSource(
45
+ db: Db.Db,
46
+ record: Record & { sourceTransferIndex: number },
47
+ ): Promise<insertOrGetSource.Result> {
48
+ const inserted = await db.kysely
49
+ .insertInto('funding_deposits')
50
+ .values({
51
+ ...record,
52
+ providerRequestIds: JSON.stringify(record.providerRequestIds),
53
+ providerTransactionHashes: JSON.stringify(record.providerTransactionHashes),
54
+ })
55
+ .onConflict((oc) =>
56
+ oc.columns(['depositAddressId', 'sourceTransactionHash', 'sourceTransferIndex']).doNothing(),
57
+ )
58
+ .returningAll()
59
+ .executeTakeFirst()
60
+ if (inserted) return { record: inserted, type: 'created' }
61
+ const existing = await db.kysely
62
+ .selectFrom('funding_deposits')
63
+ .selectAll()
64
+ .where('depositAddressId', '=', record.depositAddressId)
65
+ .where('sourceTransactionHash', '=', record.sourceTransactionHash)
66
+ .where('sourceTransferIndex', '=', record.sourceTransferIndex)
67
+ .executeTakeFirstOrThrow()
68
+ return { record: existing, type: 'existing' }
69
+ }
70
+
71
+ export declare namespace insertOrGetSource {
72
+ /** Result of idempotently persisting one verified source transfer. */
73
+ type Result = { record: Record; type: 'created' | 'existing' }
74
+ }
75
+
76
+ /** Serializes source attribution for one address and transaction. */
77
+ export function withSourceTransaction<const result>(
78
+ db: Db.Db,
79
+ options: withSourceTransaction.Options<result>,
80
+ ): Promise<result> {
81
+ return db.transaction(async (tx) => {
82
+ const key = `${options.depositAddressId}:${options.sourceTransactionHash.toLowerCase()}`
83
+ await sql`SELECT pg_advisory_xact_lock(hashtextextended(${key}, 0))`.execute(tx.kysely)
84
+ return options.fn(tx)
85
+ })
86
+ }
87
+
88
+ export declare namespace withSourceTransaction {
89
+ /** Source identity and transaction-scoped work protected by its advisory lock. */
90
+ type Options<result> = {
91
+ /** Funding deposit address that received the transfer. */
92
+ depositAddressId: string
93
+ /** Database work serialized for the source transaction. */
94
+ fn: (db: Db.Db) => Promise<result>
95
+ /** Source transaction whose deposit attribution must not race. */
96
+ sourceTransactionHash: string
97
+ }
98
+ }
99
+
43
100
  /** Gets deposits created from one provider-observed source transaction. */
44
101
  export function listByProviderObservation(
45
102
  db: Db.Db,
@@ -67,6 +124,45 @@ export declare namespace listByProviderObservation {
67
124
  }
68
125
  }
69
126
 
127
+ /** Attaches first-observation timestamps to every deposit from one provider request. */
128
+ export function recordRequestObservation(
129
+ db: Db.Db,
130
+ options: recordRequestObservation.Options,
131
+ ): Promise<Record[]> {
132
+ return db.kysely
133
+ .updateTable('funding_deposits')
134
+ .set({
135
+ ...(options.pollObservedAt
136
+ ? {
137
+ pollObservedAt: sql<string>`LEAST(COALESCE(poll_observed_at, ${options.pollObservedAt}), ${options.pollObservedAt})`,
138
+ }
139
+ : {}),
140
+ ...(options.webhookReceivedAt
141
+ ? {
142
+ webhookReceivedAt: sql<string>`LEAST(COALESCE(webhook_received_at, ${options.webhookReceivedAt}), ${options.webhookReceivedAt})`,
143
+ }
144
+ : {}),
145
+ })
146
+ .where('depositAddressId', '=', options.depositAddressId)
147
+ .where('providerRequestId', '=', options.providerRequestId)
148
+ .returningAll()
149
+ .execute()
150
+ }
151
+
152
+ export declare namespace recordRequestObservation {
153
+ /** First observations associated with one provider request. */
154
+ type Options = {
155
+ /** Funding deposit address id (`fda_…`). */
156
+ depositAddressId: string
157
+ /** When polling first observed the provider request. */
158
+ pollObservedAt?: string | undefined
159
+ /** Provider request identifier. */
160
+ providerRequestId: string
161
+ /** When Tempo first received an authenticated provider webhook. */
162
+ webhookReceivedAt?: string | undefined
163
+ }
164
+ }
165
+
70
166
  /** Gets deposits associated with one address and source transaction. */
71
167
  export function listBySourceTransaction(
72
168
  db: Db.Db,
@@ -91,6 +187,20 @@ export declare namespace listBySourceTransaction {
91
187
  }
92
188
  }
93
189
 
190
+ /** Returns whether an address has a nonterminal source transfer awaiting provider attribution. */
191
+ export async function hasUnattributed(db: Db.Db, depositAddressId: string): Promise<boolean> {
192
+ return Boolean(
193
+ await db.kysely
194
+ .selectFrom('funding_deposits')
195
+ .select('id')
196
+ .where('depositAddressId', '=', depositAddressId)
197
+ .where('providerRequestId', 'is', null)
198
+ .where('status', 'not in', ['completed', 'refunded'])
199
+ .limit(1)
200
+ .executeTakeFirst(),
201
+ )
202
+ }
203
+
94
204
  /** Gets a funding deposit without ownership scoping. */
95
205
  export function get(db: Db.Db, id: string): Promise<Record | undefined> {
96
206
  return db.kysely
@@ -155,6 +265,40 @@ export declare namespace listByAddress {
155
265
  }
156
266
  }
157
267
 
268
+ /** Lists one recipient's owner-visible deposits newest-first. */
269
+ export function listByRecipient(db: Db.Db, options: listByRecipient.Options): Promise<Record[]> {
270
+ const { cursor, limit, owner, recipient } = options
271
+ let query = db.kysely
272
+ .selectFrom('funding_deposits')
273
+ .selectAll()
274
+ .where('environment', '=', owner.environment)
275
+ .where('orgId', '=', owner.orgId)
276
+ .where(sql<boolean>`lower(snapshot->>'recipient') = ${recipient.toLowerCase()}`)
277
+ if (owner.projectId !== undefined) query = query.where('projectId', '=', owner.projectId)
278
+ if (cursor)
279
+ query = query.where((eb) =>
280
+ eb.or([
281
+ eb('createdAt', '<', cursor.createdAt),
282
+ eb.and([eb('createdAt', '=', cursor.createdAt), eb('id', '<', cursor.id)]),
283
+ ]),
284
+ )
285
+ return query.orderBy('createdAt', 'desc').orderBy('id', 'desc').limit(limit).execute()
286
+ }
287
+
288
+ export declare namespace listByRecipient {
289
+ /** Recipient filter, ownership, and paging options. */
290
+ type Options = {
291
+ /** Exclusive lower bound from the previous page. */
292
+ cursor?: listByAddress.Cursor | undefined
293
+ /** Maximum rows to return. */
294
+ limit: number
295
+ /** Ownership filter derived from the authenticated API key. */
296
+ owner: Owner
297
+ /** Tempo account that receives completed deposits. */
298
+ recipient: string
299
+ }
300
+ }
301
+
158
302
  /** Applies a version-guarded deposit update. */
159
303
  export function update(db: Db.Db, options: update.Options): Promise<Record | undefined> {
160
304
  return db.kysely
@@ -21,6 +21,7 @@ import * as TestStore from '../../test/Store.js'
21
21
  import * as Tempo from '../../test/Tempo.js'
22
22
 
23
23
  const key = {
24
+ allowedIps: [],
24
25
  environment: 'production',
25
26
  id: 'key_test',
26
27
  name: 'Test key',
@@ -295,6 +296,135 @@ describe('require', () => {
295
296
  }
296
297
  `)
297
298
  })
299
+
300
+ test.each([
301
+ ['tempo-api-key', '/protected', { 'tempo-api-key': token }],
302
+ ['Bearer', '/protected', { authorization: `Bearer ${token}` }],
303
+ ['x-api-key', '/protected', { 'x-api-key': token }],
304
+ ['query key', `/protected?key=${token}`, {}],
305
+ ] as const)('enforces an allowlist for the %s credential', async (_name, path, credential) => {
306
+ const app = createApp({
307
+ auth: {
308
+ apiKey: {
309
+ resolve: TestApp.resolver({
310
+ keys: [{ ...key, allowedIps: ['203.0.113.0/24'], token }],
311
+ }),
312
+ },
313
+ rateLimit: RateLimit.memory(),
314
+ },
315
+ })
316
+ const response = await app.request(path, {
317
+ headers: { ...credential, 'cf-connecting-ip': '198.51.100.7' },
318
+ })
319
+
320
+ expect(response.status).toBe(403)
321
+ expect(await response.json()).toMatchObject({ error: { code: 'api_key_ip_forbidden' } })
322
+ })
323
+
324
+ test('rejects a missing client IP', async () => {
325
+ const app = createApp({
326
+ auth: {
327
+ apiKey: {
328
+ resolve: TestApp.resolver({
329
+ keys: [{ ...key, allowedIps: ['203.0.113.0/24'], token }],
330
+ }),
331
+ },
332
+ rateLimit: RateLimit.memory(),
333
+ },
334
+ })
335
+ const response = await app.request('/protected', {
336
+ headers: { 'tempo-api-key': token },
337
+ })
338
+
339
+ expect(response.status).toBe(403)
340
+ expect(await response.json()).toMatchObject({ error: { code: 'api_key_ip_forbidden' } })
341
+ })
342
+
343
+ test('does not trust a Cloudflare IP header outside Cloudflare', async () => {
344
+ const app = createApp({
345
+ auth: {
346
+ apiKey: {
347
+ resolve: TestApp.resolver({
348
+ keys: [{ ...key, allowedIps: ['203.0.113.0/24'], token }],
349
+ }),
350
+ },
351
+ rateLimit: RateLimit.memory(),
352
+ },
353
+ })
354
+ const response = await app.request('/protected', {
355
+ headers: {
356
+ 'cf-connecting-ip': '203.0.113.7',
357
+ 'tempo-api-key': token,
358
+ },
359
+ })
360
+
361
+ expect(response.status).toBe(403)
362
+ expect(await response.json()).toMatchObject({ error: { code: 'api_key_ip_forbidden' } })
363
+ })
364
+
365
+ test('uses the Cloudflare IP header with Cloudflare request metadata', async () => {
366
+ const app = createApp({
367
+ auth: {
368
+ apiKey: {
369
+ resolve: TestApp.resolver({
370
+ keys: [{ ...key, allowedIps: ['203.0.113.0/24'], token }],
371
+ }),
372
+ },
373
+ rateLimit: RateLimit.memory(),
374
+ },
375
+ })
376
+ const request = Object.assign(
377
+ new Request('http://localhost/protected', {
378
+ headers: {
379
+ 'cf-connecting-ip': '203.0.113.7',
380
+ 'tempo-api-key': token,
381
+ },
382
+ }),
383
+ { cf: {} },
384
+ )
385
+ const response = await app.fetch(request)
386
+
387
+ expect(response.status).toBe(200)
388
+ })
389
+
390
+ test('uses the configured trusted client-IP resolver', async () => {
391
+ const app = createApp({
392
+ auth: {
393
+ apiKey: {
394
+ resolve: TestApp.resolver({
395
+ keys: [{ ...key, allowedIps: ['203.0.113.0/24'], token }],
396
+ }),
397
+ },
398
+ publicClientIp: (request) => request.headers.get('x-real-ip') ?? undefined,
399
+ rateLimit: RateLimit.memory(),
400
+ },
401
+ })
402
+ const response = await app.request('/protected', {
403
+ headers: {
404
+ 'cf-connecting-ip': '198.51.100.7',
405
+ 'tempo-api-key': token,
406
+ 'x-real-ip': '203.0.113.7',
407
+ },
408
+ })
409
+
410
+ expect(response.status).toBe(200)
411
+ })
412
+
413
+ test('treats an empty allowlist as unrestricted', async () => {
414
+ const app = createApp({
415
+ auth: {
416
+ apiKey: {
417
+ resolve: TestApp.resolver({ keys: [{ ...key, allowedIps: [], token }] }),
418
+ },
419
+ rateLimit: RateLimit.memory(),
420
+ },
421
+ })
422
+ const response = await app.request('/protected', {
423
+ headers: { 'tempo-api-key': token },
424
+ })
425
+
426
+ expect(response.status).toBe(200)
427
+ })
298
428
  })
299
429
 
300
430
  describe('session', () => {
@@ -383,6 +513,37 @@ describe('require', () => {
383
513
  `)
384
514
  })
385
515
 
516
+ test('applies the session-only route error before key restrictions', async () => {
517
+ const app = createApp({
518
+ auth: {
519
+ apiKey: {
520
+ resolve: TestApp.resolver({
521
+ keys: [{ ...key, allowedIps: ['203.0.113.0/24'], token }],
522
+ }),
523
+ },
524
+ publicClientIp: (request) => request.headers.get('x-real-ip') ?? undefined,
525
+ rateLimit: RateLimit.memory(),
526
+ },
527
+ policy: { session: true },
528
+ })
529
+ const response = await app.request('/protected', {
530
+ headers: {
531
+ 'tempo-api-key': token,
532
+ 'x-real-ip': '198.51.100.7',
533
+ },
534
+ })
535
+
536
+ expect(response.status).toBe(403)
537
+ expect(await response.json()).toMatchInlineSnapshot(`
538
+ {
539
+ "error": {
540
+ "code": "forbidden",
541
+ "message": "API key not permitted for this route",
542
+ },
543
+ }
544
+ `)
545
+ })
546
+
386
547
  test('is not consulted on lane-less routes', async () => {
387
548
  const app = createSessionApp({
388
549
  policy: {},
@@ -1228,7 +1389,7 @@ describe('require', () => {
1228
1389
  })
1229
1390
  })
1230
1391
 
1231
- test('rejects a voucher that exceeds the channel deposit', async () => {
1392
+ test('tops up the channel before signing a voucher that exceeds its deposit', async () => {
1232
1393
  const mpp = createMppServer()
1233
1394
  const app = createApp({
1234
1395
  auth: {
@@ -1243,14 +1404,25 @@ describe('require', () => {
1243
1404
  })
1244
1405
  const client = createMppClient(app)
1245
1406
 
1246
- // The opening deposit covers one request; the second voucher overruns
1247
- // it and the low-level client cannot top up, so the server re-challenges.
1407
+ // The opening deposit covers one request. The client tops up the channel
1408
+ // before signing the second voucher.
1248
1409
  const first = await client.fetch('http://tempo-api.test/protected')
1249
1410
  const second = await client.fetch('http://tempo-api.test/protected')
1250
1411
 
1251
1412
  expect(first.status).toBe(200)
1252
- expect(second.status).toBe(402)
1253
- expect(second.headers.get('www-authenticate')?.startsWith('Payment ')).toBe(true)
1413
+ expect(second.status).toBe(200)
1414
+
1415
+ const receipt1 = sessionReceipt(first)
1416
+ const receipt2 = sessionReceipt(second)
1417
+ expect(receipt2).toMatchObject({
1418
+ channelId: receipt1.channelId,
1419
+ spent: '2000000',
1420
+ })
1421
+
1422
+ const state = await Actions.channel.getStates(Tempo.client, {
1423
+ channel: receipt1.channelId,
1424
+ })
1425
+ expect(state.deposit).toBe(2_000_000n)
1254
1426
  })
1255
1427
 
1256
1428
  test('rejects a malformed payment credential with a fresh challenge', async () => {
@@ -18,6 +18,7 @@ import * as ApiKeys from '../ApiKeys.js'
18
18
  import * as Db from '../db/Db.js'
19
19
  import type * as Store from './Store.js'
20
20
  import * as Memberships from '../db/tables/memberships.js'
21
+ import * as IpAllowlist from './IpAllowlist.js'
21
22
  import * as Metadata from '../apps/metadata.js'
22
23
  import * as OpenApi from './OpenApi.js'
23
24
  import * as Organizations from '../db/tables/organizations.js'
@@ -438,12 +439,16 @@ export function accessError(c: hono_Context) {
438
439
  status: 401,
439
440
  })
440
441
  }
441
- if (status === 403)
442
+ if (status === 403) {
443
+ const ipForbidden = c.req.header('tempo-narrow-access-code') === 'api_key_ip_forbidden'
442
444
  return Response.error(c, {
443
- code: 'api_key_forbidden',
444
- message: 'API key missing required scope',
445
+ code: ipForbidden ? 'api_key_ip_forbidden' : 'api_key_forbidden',
446
+ message: ipForbidden
447
+ ? 'API key not permitted from this IP address'
448
+ : 'API key missing required scope',
445
449
  status: 403,
446
450
  })
451
+ }
447
452
  const code =
448
453
  c.req.header('tempo-narrow-access-code') === 'rate_limit_exceeded'
449
454
  ? 'rate_limit_exceeded'
@@ -511,8 +516,8 @@ export function middleware<
511
516
  // store is configured, every token resolves to `null` (closed).
512
517
  // Resolution is cached per isolate by default (positive 60s / negative 10s,
513
518
  // keyed by token sha256) so hot keys skip the backend read on the auth path
514
- // of every request. Pass `apiKey.cache: false` when instant revocation
515
- // matters more than the per-request read.
519
+ // of every request. Pass `apiKey.cache: false` when immediate key updates
520
+ // matter more than the per-request read.
516
521
  const resolve = (() => {
517
522
  const uncached = (c: hono_Context, token: string) => {
518
523
  const kv = contextKv(c)
@@ -606,8 +611,8 @@ export declare namespace middleware {
606
611
  /**
607
612
  * Resolve-cache tuning (per-isolate, positive 60s / negative 10s by
608
613
  * default), or `false` to resolve uncached on every request — e.g.
609
- * when instant revocation matters more than the per-request backend
610
- * read.
614
+ * when immediate key updates matter more than the per-request
615
+ * backend read.
611
616
  */
612
617
  cache?: Cache | false | undefined
613
618
  /** API-key quotas keyed by quota scope (reserved `'*'` is the config default). */
@@ -624,7 +629,7 @@ export declare namespace middleware {
624
629
  /** Default public access policy. Pass false to disable anonymous access. */
625
630
  public?:
626
631
  | {
627
- /** Resolves the client IP from trusted runtime or proxy metadata. Defaults to `cf-connecting-ip`. */
632
+ /** Resolves the client IP from trusted runtime or proxy metadata. Required outside Cloudflare for API-key allowlists. */
628
633
  clientIp?: ((request: Request) => string | undefined) | undefined
629
634
  /** Anonymous public quota for protected routes. */
630
635
  rateLimit?: RateLimit.Limit | undefined
@@ -1448,6 +1453,28 @@ export function require<environment extends Environment = Environment>(
1448
1453
  const principal = principalFromApiKey(apiKey)
1449
1454
  c.set('principal', principal)
1450
1455
 
1456
+ // Default-deny: routes with no API-key lane allow only the super admin.
1457
+ // A valid key is authenticated but forbidden.
1458
+ if (!policy.apiKey)
1459
+ return jsonError(c, {
1460
+ code: 'forbidden',
1461
+ message: 'API key not permitted for this route',
1462
+ status: 403,
1463
+ })
1464
+
1465
+ if (
1466
+ apiKey.allowedIps.length &&
1467
+ !(await IpAllowlist.allows(c, {
1468
+ address: trustedClientIp(c, auth),
1469
+ rules: apiKey.allowedIps,
1470
+ }))
1471
+ )
1472
+ return jsonError(c, {
1473
+ code: 'api_key_ip_forbidden',
1474
+ message: 'API key not permitted from this IP address',
1475
+ status: 403,
1476
+ })
1477
+
1451
1478
  if (
1452
1479
  apiKey.environment === 'sandbox' &&
1453
1480
  explicitChain !== undefined &&
@@ -1459,14 +1486,6 @@ export function require<environment extends Environment = Environment>(
1459
1486
  status: 403,
1460
1487
  })
1461
1488
 
1462
- // Default-deny: routes with no API-key lane allow only the super admin.
1463
- // A valid key is authenticated but forbidden.
1464
- if (!policy.apiKey)
1465
- return jsonError(c, {
1466
- code: 'forbidden',
1467
- message: 'API key not permitted for this route',
1468
- status: 403,
1469
- })
1470
1489
  const missing = apiKey.scopes.includes(Scope.wildcard)
1471
1490
  ? []
1472
1491
  : (policy.apiKey?.scopes.filter((scope) => !apiKey.scopes.includes(scope)) ?? [])
@@ -1821,6 +1840,17 @@ function getApiKeyCredential(c: hono_Context): ApiKeyCredential | false | null {
1821
1840
  return null
1822
1841
  }
1823
1842
 
1843
+ function clientIp(c: hono_Context, auth: Context): string | undefined {
1844
+ return auth.publicClientIp ? auth.publicClientIp(c.req.raw) : c.req.header('cf-connecting-ip')
1845
+ }
1846
+
1847
+ function trustedClientIp(c: hono_Context, auth: Context): string | undefined {
1848
+ if (auth.publicClientIp) return auth.publicClientIp(c.req.raw)
1849
+ // Cloudflare supplies both the request metadata and this header.
1850
+ if (!('cf' in c.req.raw)) return undefined
1851
+ return c.req.header('cf-connecting-ip')
1852
+ }
1853
+
1824
1854
  /** Removes the reserved auth query parameter before route validation. */
1825
1855
  function stripApiKeyQuery(c: hono_Context) {
1826
1856
  const url = new URL(c.req.url)
@@ -1936,7 +1966,7 @@ function principalFromApiKey(apiKey: ApiKey.ApiKey): Principal {
1936
1966
  }
1937
1967
 
1938
1968
  function principalFromPublic(c: hono_Context, auth: Context): Principal {
1939
- const ip = auth.publicClientIp ? auth.publicClientIp(c.req.raw) : c.req.header('cf-connecting-ip')
1969
+ const ip = clientIp(c, auth)
1940
1970
  return {
1941
1971
  id: ip ? RateLimit.normalizeIp(ip) : 'anonymous',
1942
1972
  type: 'public',
@@ -2056,7 +2086,7 @@ const cachedMaxEntries = 10_000
2056
2086
  * (default 60s), negative for `negativeTtl` (default 10s) so a freshly minted
2057
2087
  * key is usable quickly; concurrent misses for one token are single-flighted.
2058
2088
  * Entries are keyed by the token's sha256 (`ApiKey.hash`) — never the
2059
- * plaintext. A revoked key may stay resolvable for up to `ttl` the accepted
2089
+ * plaintext. Key updates may remain stale for up to `ttl`, the accepted
2060
2090
  * trade-off for removing a backend read from the auth path.
2061
2091
  */
2062
2092
  function cachedResolve(