tapimo 0.7.2 → 0.9.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 (300) 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 +20117 -19183
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +61 -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 +11 -11
  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 +11696 -13937
  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 +271 -271
  35. package/dist/apps/data/routes/addresses.d.ts +398 -167
  36. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  37. package/dist/apps/data/routes/balances.d.ts +278 -8
  38. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  39. package/dist/apps/data/routes/balances.js +114 -2
  40. package/dist/apps/data/routes/balances.js.map +1 -1
  41. package/dist/apps/data/routes/blocks.d.ts +123 -123
  42. package/dist/apps/data/routes/coingecko.d.ts +170 -170
  43. package/dist/apps/data/routes/earn.d.ts +498 -6
  44. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  45. package/dist/apps/data/routes/earn.js +876 -4
  46. package/dist/apps/data/routes/earn.js.map +1 -1
  47. package/dist/apps/data/routes/exchanges.d.ts +10 -10
  48. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  49. package/dist/apps/data/routes/indexer.d.ts +6 -6
  50. package/dist/apps/data/routes/indexer.js +4 -1
  51. package/dist/apps/data/routes/indexer.js.map +1 -1
  52. package/dist/apps/data/routes/receipts.d.ts +235 -235
  53. package/dist/apps/data/routes/rpc.d.ts +31 -31
  54. package/dist/apps/data/routes/rpc.js +4 -1
  55. package/dist/apps/data/routes/rpc.js.map +1 -1
  56. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  57. package/dist/apps/data/routes/tokens.d.ts +449 -449
  58. package/dist/apps/data/routes/transactions.d.ts +2 -2
  59. package/dist/apps/data/routes/transfers.d.ts +81 -81
  60. package/dist/apps/data/routes/valuation.d.ts +1 -1
  61. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  62. package/dist/apps/data/routes/webhooks.d.ts +9 -9
  63. package/dist/apps/data/routes/zones.d.ts +14 -14
  64. package/dist/apps/funding/Access.d.ts +16 -0
  65. package/dist/apps/funding/Access.d.ts.map +1 -0
  66. package/dist/apps/funding/Access.js +22 -0
  67. package/dist/apps/funding/Access.js.map +1 -0
  68. package/dist/apps/{data/routes/funding.d.ts → funding/App.d.ts} +1640 -3217
  69. package/dist/apps/funding/App.d.ts.map +1 -0
  70. package/dist/apps/funding/App.js +61 -0
  71. package/dist/apps/funding/App.js.map +1 -0
  72. package/dist/apps/funding/Inventory.d.ts +5 -0
  73. package/dist/apps/funding/Inventory.d.ts.map +1 -0
  74. package/dist/apps/funding/Inventory.js +25 -0
  75. package/dist/apps/funding/Inventory.js.map +1 -0
  76. package/dist/apps/funding/ProviderAttempts.d.ts +5 -0
  77. package/dist/apps/funding/ProviderAttempts.d.ts.map +1 -0
  78. package/dist/apps/funding/ProviderAttempts.js +24 -0
  79. package/dist/apps/funding/ProviderAttempts.js.map +1 -0
  80. package/dist/apps/funding/index.d.ts +2 -0
  81. package/dist/apps/funding/index.d.ts.map +1 -0
  82. package/dist/apps/funding/index.js +2 -0
  83. package/dist/apps/funding/index.js.map +1 -0
  84. package/dist/apps/funding/routes/chains.d.ts +169 -0
  85. package/dist/apps/funding/routes/chains.d.ts.map +1 -0
  86. package/dist/apps/funding/routes/chains.js +80 -0
  87. package/dist/apps/funding/routes/chains.js.map +1 -0
  88. package/dist/apps/funding/routes/deposit-addresses.d.ts +1997 -0
  89. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
  90. package/dist/apps/funding/routes/deposit-addresses.js +778 -0
  91. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -0
  92. package/dist/apps/funding/routes/providers.d.ts +91 -0
  93. package/dist/apps/funding/routes/providers.d.ts.map +1 -0
  94. package/dist/apps/funding/routes/providers.js +56 -0
  95. package/dist/apps/funding/routes/providers.js.map +1 -0
  96. package/dist/apps/funding/routes/quotes.d.ts +426 -0
  97. package/dist/apps/funding/routes/quotes.d.ts.map +1 -0
  98. package/dist/apps/funding/routes/quotes.js +188 -0
  99. package/dist/apps/funding/routes/quotes.js.map +1 -0
  100. package/dist/apps/funding/routes/transfers.d.ts +1999 -0
  101. package/dist/apps/funding/routes/transfers.d.ts.map +1 -0
  102. package/dist/apps/funding/routes/transfers.js +614 -0
  103. package/dist/apps/funding/routes/transfers.js.map +1 -0
  104. package/dist/apps/index.d.ts +1 -0
  105. package/dist/apps/index.d.ts.map +1 -1
  106. package/dist/apps/index.js +1 -0
  107. package/dist/apps/index.js.map +1 -1
  108. package/dist/apps/management/App.d.ts +430 -165
  109. package/dist/apps/management/App.d.ts.map +1 -1
  110. package/dist/apps/management/routes/api-keys.d.ts +298 -21
  111. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  112. package/dist/apps/management/routes/api-keys.js +75 -1
  113. package/dist/apps/management/routes/api-keys.js.map +1 -1
  114. package/dist/apps/management/routes/billing.d.ts +6 -6
  115. package/dist/apps/management/routes/faucet.d.ts +35 -35
  116. package/dist/apps/management/routes/invitations.d.ts +122 -122
  117. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  118. package/dist/apps/management/routes/members.d.ts +69 -69
  119. package/dist/apps/management/routes/orgs.d.ts +99 -99
  120. package/dist/apps/management/routes/projects.d.ts +25 -25
  121. package/dist/apps/management/routes/scopes.d.ts +1 -1
  122. package/dist/apps/management/routes/usage.d.ts +72 -72
  123. package/dist/apps/management/routes/verified-token-requests.d.ts +77 -77
  124. package/dist/apps/management/routes/webhooks.d.ts +128 -128
  125. package/dist/apps/mcp.d.ts +2 -3
  126. package/dist/apps/mcp.d.ts.map +1 -1
  127. package/dist/apps/mcp.js +16 -14
  128. package/dist/apps/mcp.js.map +1 -1
  129. package/dist/apps/metadata.d.ts +4 -0
  130. package/dist/apps/metadata.d.ts.map +1 -1
  131. package/dist/apps/metadata.js +10 -0
  132. package/dist/apps/metadata.js.map +1 -1
  133. package/dist/apps/mpp/App.d.ts +44 -44
  134. package/dist/cloudflare.d.ts +50 -0
  135. package/dist/cloudflare.d.ts.map +1 -1
  136. package/dist/cloudflare.js +178 -0
  137. package/dist/cloudflare.js.map +1 -1
  138. package/dist/db/Db.d.ts +2 -0
  139. package/dist/db/Db.d.ts.map +1 -1
  140. package/dist/db/Db.js +45 -0
  141. package/dist/db/Db.js.map +1 -1
  142. package/dist/db/Schema.d.ts +12 -0
  143. package/dist/db/Schema.d.ts.map +1 -1
  144. package/dist/db/Schema.js +20 -0
  145. package/dist/db/Schema.js.map +1 -1
  146. package/dist/db/tables/fundingDepositAddresses.d.ts +13 -0
  147. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  148. package/dist/db/tables/fundingDepositAddresses.js +12 -0
  149. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  150. package/dist/db/tables/fundingDepositRequestObservations.d.ts +45 -0
  151. package/dist/db/tables/fundingDepositRequestObservations.d.ts.map +1 -0
  152. package/dist/db/tables/fundingDepositRequestObservations.js +46 -0
  153. package/dist/db/tables/fundingDepositRequestObservations.js.map +1 -0
  154. package/dist/db/tables/fundingDeposits.d.ts +74 -1
  155. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  156. package/dist/db/tables/fundingDeposits.js +126 -0
  157. package/dist/db/tables/fundingDeposits.js.map +1 -1
  158. package/dist/internal/Auth.d.ts +15 -11
  159. package/dist/internal/Auth.d.ts.map +1 -1
  160. package/dist/internal/Auth.js +52 -17
  161. package/dist/internal/Auth.js.map +1 -1
  162. package/dist/internal/Cache.js +1 -1
  163. package/dist/internal/Cache.js.map +1 -1
  164. package/dist/internal/EdgeCache.d.ts +2 -2
  165. package/dist/internal/EdgeCache.d.ts.map +1 -1
  166. package/dist/internal/EdgeCache.js +10 -4
  167. package/dist/internal/EdgeCache.js.map +1 -1
  168. package/dist/internal/IpAllowlist.d.ts +15 -0
  169. package/dist/internal/IpAllowlist.d.ts.map +1 -0
  170. package/dist/internal/IpAllowlist.js +27 -0
  171. package/dist/internal/IpAllowlist.js.map +1 -0
  172. package/dist/internal/OpenApi.d.ts +1 -1
  173. package/dist/internal/OpenApi.js +1 -1
  174. package/dist/internal/OpenApi.js.map +1 -1
  175. package/dist/internal/Tidx.d.ts +6 -0
  176. package/dist/internal/Tidx.d.ts.map +1 -1
  177. package/dist/internal/Tidx.js +13 -3
  178. package/dist/internal/Tidx.js.map +1 -1
  179. package/dist/internal/Viem.d.ts +8 -0
  180. package/dist/internal/Viem.d.ts.map +1 -1
  181. package/dist/internal/Viem.js +12 -5
  182. package/dist/internal/Viem.js.map +1 -1
  183. package/dist/internal/funding/Deposit.d.ts +42 -0
  184. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  185. package/dist/internal/funding/Deposit.js +37 -5
  186. package/dist/internal/funding/Deposit.js.map +1 -1
  187. package/dist/internal/funding/Evidence.d.ts +1 -5
  188. package/dist/internal/funding/Evidence.d.ts.map +1 -1
  189. package/dist/internal/funding/Evidence.js +4 -3
  190. package/dist/internal/funding/Evidence.js.map +1 -1
  191. package/dist/internal/funding/Provider.d.ts +102 -0
  192. package/dist/internal/funding/Provider.d.ts.map +1 -1
  193. package/dist/internal/funding/Provider.js +5 -0
  194. package/dist/internal/funding/Provider.js.map +1 -1
  195. package/dist/internal/funding/Reconciliation.d.ts +16 -2
  196. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  197. package/dist/internal/funding/Reconciliation.js +348 -54
  198. package/dist/internal/funding/Reconciliation.js.map +1 -1
  199. package/dist/internal/funding/SourceObservation.d.ts +108 -0
  200. package/dist/internal/funding/SourceObservation.d.ts.map +1 -0
  201. package/dist/internal/funding/SourceObservation.js +143 -0
  202. package/dist/internal/funding/SourceObservation.js.map +1 -0
  203. package/dist/internal/funding/Subsidy.d.ts +2 -2
  204. package/dist/internal/funding/Subsidy.d.ts.map +1 -1
  205. package/dist/internal/funding/Subsidy.js +1 -1
  206. package/dist/internal/funding/Subsidy.js.map +1 -1
  207. package/dist/internal/funding/index.d.ts +1 -0
  208. package/dist/internal/funding/index.d.ts.map +1 -1
  209. package/dist/internal/funding/index.js +1 -0
  210. package/dist/internal/funding/index.js.map +1 -1
  211. package/dist/internal/funding/providers/relay.d.ts +43 -1
  212. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  213. package/dist/internal/funding/providers/relay.js +438 -27
  214. package/dist/internal/funding/providers/relay.js.map +1 -1
  215. package/package.json +3 -3
  216. package/src/ApiKey.test.ts +35 -0
  217. package/src/ApiKey.ts +27 -0
  218. package/src/ApiKeys.test.ts +29 -0
  219. package/src/ApiKeys.ts +11 -3
  220. package/src/App.test.ts +55 -5
  221. package/src/App.ts +79 -5
  222. package/src/Client.test-d.ts +203 -28
  223. package/src/admin/apps/api-keys.test.ts +25 -1
  224. package/src/admin/apps/api-keys.ts +6 -2
  225. package/src/admin/ui.generated.ts +67 -67
  226. package/src/analytics/Analytics.ts +4 -0
  227. package/src/apps/composition.test.ts +130 -3
  228. package/src/apps/data/App.test.ts +0 -12
  229. package/src/apps/data/App.ts +4 -8
  230. package/src/apps/data/routes/balances.test.ts +42 -0
  231. package/src/apps/data/routes/balances.ts +218 -85
  232. package/src/apps/data/routes/earn.test.ts +363 -0
  233. package/src/apps/data/routes/earn.ts +1141 -3
  234. package/src/apps/data/routes/indexer.ts +2 -1
  235. package/src/apps/data/routes/rpc.ts +2 -0
  236. package/src/apps/data/routes/tokens.test.ts +6 -8
  237. package/src/apps/funding/Access.ts +26 -0
  238. package/src/apps/funding/App.test.ts +78 -0
  239. package/src/apps/funding/App.ts +78 -0
  240. package/src/apps/funding/Inventory.ts +30 -0
  241. package/src/apps/funding/ProviderAttempts.ts +26 -0
  242. package/src/apps/funding/index.ts +1 -0
  243. package/src/apps/funding/routes/chains.test.ts +78 -0
  244. package/src/apps/funding/routes/chains.ts +101 -0
  245. package/src/apps/funding/routes/deposit-addresses.test.ts +1649 -0
  246. package/src/apps/funding/routes/deposit-addresses.ts +932 -0
  247. package/src/apps/funding/routes/providers.test.ts +165 -0
  248. package/src/apps/funding/routes/providers.ts +73 -0
  249. package/src/apps/funding/routes/quotes.test.ts +511 -0
  250. package/src/apps/funding/routes/quotes.ts +205 -0
  251. package/src/apps/funding/routes/transfers.test.ts +1236 -0
  252. package/src/apps/funding/routes/transfers.ts +704 -0
  253. package/src/apps/index.ts +1 -0
  254. package/src/apps/management/routes/api-keys.test.ts +98 -0
  255. package/src/apps/management/routes/api-keys.ts +96 -1
  256. package/src/apps/mcp.test.ts +17 -2
  257. package/src/apps/mcp.ts +18 -13
  258. package/src/apps/metadata.ts +12 -0
  259. package/src/apps/mpp/internal/Policy.test.ts +7 -1
  260. package/src/cloudflare.ts +279 -0
  261. package/src/db/Db.ts +71 -0
  262. package/src/db/Schema.ts +24 -0
  263. package/src/db/tables/fundingDepositAddresses.ts +28 -0
  264. package/src/db/tables/fundingDepositRequestObservations.test.ts +106 -0
  265. package/src/db/tables/fundingDepositRequestObservations.ts +99 -0
  266. package/src/db/tables/fundingDeposits.test.ts +239 -0
  267. package/src/db/tables/fundingDeposits.ts +214 -1
  268. package/src/internal/Auth.test.ts +205 -5
  269. package/src/internal/Auth.ts +63 -19
  270. package/src/internal/Cache.test.ts +37 -3
  271. package/src/internal/Cache.ts +1 -1
  272. package/src/internal/EdgeCache.test.ts +7 -7
  273. package/src/internal/EdgeCache.ts +13 -4
  274. package/src/internal/IpAllowlist.test.ts +52 -0
  275. package/src/internal/IpAllowlist.ts +39 -0
  276. package/src/internal/Log.test.ts +7 -1
  277. package/src/internal/OpenApi.ts +1 -1
  278. package/src/internal/Tidx.test.ts +28 -0
  279. package/src/internal/Tidx.ts +19 -3
  280. package/src/internal/Viem.test.ts +28 -0
  281. package/src/internal/Viem.ts +21 -5
  282. package/src/internal/funding/Deposit.ts +74 -5
  283. package/src/internal/funding/Evidence.test.ts +2 -2
  284. package/src/internal/funding/Evidence.ts +4 -9
  285. package/src/internal/funding/Provider.ts +115 -0
  286. package/src/internal/funding/Reconciliation.local.test.ts +7 -0
  287. package/src/internal/funding/Reconciliation.test.ts +1364 -109
  288. package/src/internal/funding/Reconciliation.ts +467 -88
  289. package/src/internal/funding/SourceObservation.test.ts +301 -0
  290. package/src/internal/funding/SourceObservation.ts +267 -0
  291. package/src/internal/funding/Subsidy.ts +2 -2
  292. package/src/internal/funding/index.test.ts +1 -0
  293. package/src/internal/funding/index.ts +1 -0
  294. package/src/internal/funding/providers/relay.test.ts +94 -4
  295. package/src/internal/funding/providers/relay.ts +533 -27
  296. package/dist/apps/data/routes/funding.d.ts.map +0 -1
  297. package/dist/apps/data/routes/funding.js +0 -1557
  298. package/dist/apps/data/routes/funding.js.map +0 -1
  299. package/src/apps/data/routes/funding.test.ts +0 -2994
  300. package/src/apps/data/routes/funding.ts +0 -1821
@@ -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: {},
@@ -1095,6 +1256,34 @@ describe('require', () => {
1095
1256
  })
1096
1257
  })
1097
1258
 
1259
+ test('session challenge rejects a chain outside the configured allowlist', async () => {
1260
+ const mpp = Object.assign(
1261
+ createMppServer({
1262
+ chainId: Viem.chainId.mainnet,
1263
+ getClient: ({ chainId = Viem.chainId.mainnet }) => Viem.getClient({ chainId }),
1264
+ }),
1265
+ { sessionChainIds: new Set([Viem.chainId.mainnet, Viem.chainId.testnet]) },
1266
+ )
1267
+ const app = createApp({
1268
+ auth: {
1269
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
1270
+ mpp,
1271
+ rateLimit: overQuota(),
1272
+ },
1273
+ policy: {
1274
+ mpp: { session: { amount: '1' } },
1275
+ public: { rateLimit: { perMinute: 1 } },
1276
+ },
1277
+ })
1278
+
1279
+ const response = await app.request('/protected?chainId=31318')
1280
+ const body = await response.json()
1281
+
1282
+ expect(response.status).toBe(429)
1283
+ expect(response.headers.has('www-authenticate')).toBe(false)
1284
+ expect(body).toMatchObject({ error: { code: 'payment_required' } })
1285
+ })
1286
+
1098
1287
  test('route session overrides propagate to the challenge', async () => {
1099
1288
  const mpp = createMppServer()
1100
1289
  const app = createApp({
@@ -1228,7 +1417,7 @@ describe('require', () => {
1228
1417
  })
1229
1418
  })
1230
1419
 
1231
- test('rejects a voucher that exceeds the channel deposit', async () => {
1420
+ test('tops up the channel before signing a voucher that exceeds its deposit', async () => {
1232
1421
  const mpp = createMppServer()
1233
1422
  const app = createApp({
1234
1423
  auth: {
@@ -1243,14 +1432,25 @@ describe('require', () => {
1243
1432
  })
1244
1433
  const client = createMppClient(app)
1245
1434
 
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.
1435
+ // The opening deposit covers one request. The client tops up the channel
1436
+ // before signing the second voucher.
1248
1437
  const first = await client.fetch('http://tempo-api.test/protected')
1249
1438
  const second = await client.fetch('http://tempo-api.test/protected')
1250
1439
 
1251
1440
  expect(first.status).toBe(200)
1252
- expect(second.status).toBe(402)
1253
- expect(second.headers.get('www-authenticate')?.startsWith('Payment ')).toBe(true)
1441
+ expect(second.status).toBe(200)
1442
+
1443
+ const receipt1 = sessionReceipt(first)
1444
+ const receipt2 = sessionReceipt(second)
1445
+ expect(receipt2).toMatchObject({
1446
+ channelId: receipt1.channelId,
1447
+ spent: '2000000',
1448
+ })
1449
+
1450
+ const state = await Actions.channel.getStates(Tempo.client, {
1451
+ channel: receipt1.channelId,
1452
+ })
1453
+ expect(state.deposit).toBe(2_000_000n)
1254
1454
  })
1255
1455
 
1256
1456
  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'
@@ -116,6 +117,8 @@ type MppHandler = {
116
117
  session(options: Mpp.Session): MiddlewareHandler
117
118
  /** Resolves the chain from a challenge issued by this server. */
118
119
  sessionChainId?: ((c: hono_Context) => number | undefined) | undefined
120
+ /** Chains accepted from request-driven Tempo Session payment overrides. */
121
+ sessionChainIds?: ReadonlySet<number> | undefined
119
122
  }
120
123
 
121
124
  /**
@@ -438,12 +441,16 @@ export function accessError(c: hono_Context) {
438
441
  status: 401,
439
442
  })
440
443
  }
441
- if (status === 403)
444
+ if (status === 403) {
445
+ const ipForbidden = c.req.header('tempo-narrow-access-code') === 'api_key_ip_forbidden'
442
446
  return Response.error(c, {
443
- code: 'api_key_forbidden',
444
- message: 'API key missing required scope',
447
+ code: ipForbidden ? 'api_key_ip_forbidden' : 'api_key_forbidden',
448
+ message: ipForbidden
449
+ ? 'API key not permitted from this IP address'
450
+ : 'API key missing required scope',
445
451
  status: 403,
446
452
  })
453
+ }
447
454
  const code =
448
455
  c.req.header('tempo-narrow-access-code') === 'rate_limit_exceeded'
449
456
  ? 'rate_limit_exceeded'
@@ -499,6 +506,7 @@ export function middleware<
499
506
  const {
500
507
  rateLimit: mppRateLimit = defaultMppRateLimit,
501
508
  session: session_ = {},
509
+ sessionChainIds,
502
510
  ...mpp
503
511
  } = options.mpp === false ? {} : (options.mpp ?? {})
504
512
  const secretKey = resolveMppSecretKey(mpp.secretKey)
@@ -511,8 +519,8 @@ export function middleware<
511
519
  // store is configured, every token resolves to `null` (closed).
512
520
  // Resolution is cached per isolate by default (positive 60s / negative 10s,
513
521
  // 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.
522
+ // of every request. Pass `apiKey.cache: false` when immediate key updates
523
+ // matter more than the per-request read.
516
524
  const resolve = (() => {
517
525
  const uncached = (c: hono_Context, token: string) => {
518
526
  const kv = contextKv(c)
@@ -560,6 +568,7 @@ export function middleware<
560
568
  ],
561
569
  }).session,
562
570
  sessionChainId: (c) => paymentSessionChainId(c, secretKey),
571
+ ...(sessionChainIds ? { sessionChainIds: new Set(sessionChainIds) } : {}),
563
572
  },
564
573
  }),
565
574
  rateLimit: RateLimit.memory(options.rateLimit),
@@ -606,8 +615,8 @@ export declare namespace middleware {
606
615
  /**
607
616
  * Resolve-cache tuning (per-isolate, positive 60s / negative 10s by
608
617
  * default), or `false` to resolve uncached on every request — e.g.
609
- * when instant revocation matters more than the per-request backend
610
- * read.
618
+ * when immediate key updates matter more than the per-request
619
+ * backend read.
611
620
  */
612
621
  cache?: Cache | false | undefined
613
622
  /** API-key quotas keyed by quota scope (reserved `'*'` is the config default). */
@@ -624,7 +633,7 @@ export declare namespace middleware {
624
633
  /** Default public access policy. Pass false to disable anonymous access. */
625
634
  public?:
626
635
  | {
627
- /** Resolves the client IP from trusted runtime or proxy metadata. Defaults to `cf-connecting-ip`. */
636
+ /** Resolves the client IP from trusted runtime or proxy metadata. Required outside Cloudflare for API-key allowlists. */
628
637
  clientIp?: ((request: Request) => string | undefined) | undefined
629
638
  /** Anonymous public quota for protected routes. */
630
639
  rateLimit?: RateLimit.Limit | undefined
@@ -661,6 +670,8 @@ export declare namespace middleware {
661
670
  rateLimit?: RateLimit.Limit | undefined
662
671
  /** Global Tempo Session options passed to the MPP method instantiator. */
663
672
  session?: NonNullable<Parameters<typeof tempo.session>[0]> | undefined
673
+ /** Chains accepted from request-driven Tempo Session payment overrides. Omit to preserve the MPP client's chain support. */
674
+ sessionChainIds?: readonly number[] | undefined
664
675
  }
665
676
  }
666
677
 
@@ -1448,6 +1459,28 @@ export function require<environment extends Environment = Environment>(
1448
1459
  const principal = principalFromApiKey(apiKey)
1449
1460
  c.set('principal', principal)
1450
1461
 
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
+
1471
+ if (
1472
+ apiKey.allowedIps.length &&
1473
+ !(await IpAllowlist.allows(c, {
1474
+ address: trustedClientIp(c, auth),
1475
+ rules: apiKey.allowedIps,
1476
+ }))
1477
+ )
1478
+ return jsonError(c, {
1479
+ code: 'api_key_ip_forbidden',
1480
+ message: 'API key not permitted from this IP address',
1481
+ status: 403,
1482
+ })
1483
+
1451
1484
  if (
1452
1485
  apiKey.environment === 'sandbox' &&
1453
1486
  explicitChain !== undefined &&
@@ -1459,14 +1492,6 @@ export function require<environment extends Environment = Environment>(
1459
1492
  status: 403,
1460
1493
  })
1461
1494
 
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
1495
  const missing = apiKey.scopes.includes(Scope.wildcard)
1471
1496
  ? []
1472
1497
  : (policy.apiKey?.scopes.filter((scope) => !apiKey.scopes.includes(scope)) ?? [])
@@ -1821,6 +1846,17 @@ function getApiKeyCredential(c: hono_Context): ApiKeyCredential | false | null {
1821
1846
  return null
1822
1847
  }
1823
1848
 
1849
+ function clientIp(c: hono_Context, auth: Context): string | undefined {
1850
+ return auth.publicClientIp ? auth.publicClientIp(c.req.raw) : c.req.header('cf-connecting-ip')
1851
+ }
1852
+
1853
+ function trustedClientIp(c: hono_Context, auth: Context): string | undefined {
1854
+ if (auth.publicClientIp) return auth.publicClientIp(c.req.raw)
1855
+ // Cloudflare supplies both the request metadata and this header.
1856
+ if (!('cf' in c.req.raw)) return undefined
1857
+ return c.req.header('cf-connecting-ip')
1858
+ }
1859
+
1824
1860
  /** Removes the reserved auth query parameter before route validation. */
1825
1861
  function stripApiKeyQuery(c: hono_Context) {
1826
1862
  const url = new URL(c.req.url)
@@ -1936,7 +1972,7 @@ function principalFromApiKey(apiKey: ApiKey.ApiKey): Principal {
1936
1972
  }
1937
1973
 
1938
1974
  function principalFromPublic(c: hono_Context, auth: Context): Principal {
1939
- const ip = auth.publicClientIp ? auth.publicClientIp(c.req.raw) : c.req.header('cf-connecting-ip')
1975
+ const ip = clientIp(c, auth)
1940
1976
  return {
1941
1977
  id: ip ? RateLimit.normalizeIp(ip) : 'anonymous',
1942
1978
  type: 'public',
@@ -1961,8 +1997,16 @@ async function requirePayment<environment extends Environment>(
1961
1997
  }
1962
1998
 
1963
1999
  const session = options.policy.mpp?.session ?? {}
2000
+ const explicit = explicitChainId(c)
2001
+ const sessionChainIds = options.auth.mpp?.sessionChainIds
2002
+ if (explicit !== undefined && sessionChainIds && !sessionChainIds.has(explicit))
2003
+ return jsonError(c, {
2004
+ code: 'payment_required',
2005
+ message: 'Payment is not available for the requested chain',
2006
+ status: 429,
2007
+ })
1964
2008
  const chainId =
1965
- explicitChainId(c) ??
2009
+ explicit ??
1966
2010
  (c.req.method === 'POST' && session.scope?.startsWith('GET ')
1967
2011
  ? options.auth.mpp?.sessionChainId?.(c)
1968
2012
  : undefined)
@@ -2056,7 +2100,7 @@ const cachedMaxEntries = 10_000
2056
2100
  * (default 60s), negative for `negativeTtl` (default 10s) so a freshly minted
2057
2101
  * key is usable quickly; concurrent misses for one token are single-flighted.
2058
2102
  * 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
2103
+ * plaintext. Key updates may remain stale for up to `ttl`, the accepted
2060
2104
  * trade-off for removing a backend read from the auth path.
2061
2105
  */
2062
2106
  function cachedResolve(
@@ -52,7 +52,7 @@ describe('response', () => {
52
52
  expect(etag(first)).toMatchInlineSnapshot(`"<etag>"`)
53
53
  expect(first.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"9"`)
54
54
  expect(first.headers.get('vary')).toMatchInlineSnapshot(
55
- `"Accept-Encoding, Authorization, Tempo-API-Key"`,
55
+ `"Accept-Encoding, Authorization, Tempo-API-Key, X-API-Key"`,
56
56
  )
57
57
 
58
58
  expect(await second.json()).toMatchInlineSnapshot(`
@@ -66,7 +66,7 @@ describe('response', () => {
66
66
  expect(second.headers.get('etag') === first.headers.get('etag')).toMatchInlineSnapshot(`true`)
67
67
  expect(second.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"8"`)
68
68
  expect(second.headers.get('vary')).toMatchInlineSnapshot(
69
- `"Accept-Encoding, Authorization, Tempo-API-Key"`,
69
+ `"Accept-Encoding, Authorization, Tempo-API-Key, X-API-Key"`,
70
70
  )
71
71
 
72
72
  expect(third.status).toMatchInlineSnapshot(`304`)
@@ -77,11 +77,45 @@ describe('response', () => {
77
77
  expect(third.headers.get('etag') === first.headers.get('etag')).toMatchInlineSnapshot(`true`)
78
78
  expect(third.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"8"`)
79
79
  expect(third.headers.get('vary')).toMatchInlineSnapshot(
80
- `"Accept-Encoding, Authorization, Tempo-API-Key"`,
80
+ `"Accept-Encoding, Authorization, Tempo-API-Key, X-API-Key"`,
81
81
  )
82
82
  })
83
83
  })
84
84
 
85
+ test('varies cached responses by the legacy API-key header', async () => {
86
+ await withCaches(async () => {
87
+ let calls = 0
88
+ const app = new Hono()
89
+ app.get('/', Cache.response({ key: (c) => c.req.url, name: 'test' }), (c) =>
90
+ c.json({ calls: ++calls, token: c.req.header('x-api-key') }),
91
+ )
92
+
93
+ const first = await app.request('/', { headers: { 'x-api-key': 'one' } })
94
+ const second = await app.request('/', { headers: { 'x-api-key': 'two' } })
95
+ const hit = await app.request('/', { headers: { 'x-api-key': 'one' } })
96
+
97
+ expect(calls).toMatchInlineSnapshot(`2`)
98
+ expect(await first.json()).toMatchInlineSnapshot(`
99
+ {
100
+ "calls": 1,
101
+ "token": "one",
102
+ }
103
+ `)
104
+ expect(await second.json()).toMatchInlineSnapshot(`
105
+ {
106
+ "calls": 2,
107
+ "token": "two",
108
+ }
109
+ `)
110
+ expect(await hit.json()).toMatchInlineSnapshot(`
111
+ {
112
+ "calls": 1,
113
+ "token": "one",
114
+ }
115
+ `)
116
+ })
117
+ })
118
+
85
119
  test('stores entries with the shared policy form, not the client form', async () => {
86
120
  await withCaches(async (stores) => {
87
121
  const app = new Hono()
@@ -57,7 +57,7 @@ export function response<environment extends Env>(
57
57
  options: response.Options<environment>,
58
58
  ): MiddlewareHandler<environment> {
59
59
  const defaultCacheControl = options.cacheControl ?? policies.default
60
- const vary = options.vary ?? ['Accept-Encoding', 'Authorization', 'Tempo-API-Key']
60
+ const vary = options.vary ?? ['Accept-Encoding', 'Authorization', 'Tempo-API-Key', 'X-API-Key']
61
61
  // hono/cache suffixes its key with `::<directive>=<value>` for every
62
62
  // configured vary directive (trimmed, lowercased, deduped). Mirror that
63
63
  // normalization so the `no-cache` purge below deletes exactly the entry the
@@ -37,18 +37,18 @@ describe('middleware', () => {
37
37
  expect(second.headers.get('vary')).toMatchInlineSnapshot(`"Accept-Encoding"`)
38
38
  })
39
39
 
40
- test('serves authenticated requests from the same shared cache entry', async () => {
40
+ test('bypasses the cache for credential headers', async () => {
41
41
  const { app, store, downstreamCalls } = createApp()
42
42
 
43
- // An authenticated request warms the cache; subsequent requests — whether
44
- // authenticated (with any key) or anonymous — are all served from the one
45
- // shared entry without re-running downstream auth/rate-limiting.
46
43
  await app.request('/cached', { headers: { authorization: 'Bearer one' } })
44
+ await app.request('/cached', { headers: { authorization: 'Basic malformed' } })
45
+ await app.request('/cached', { headers: { authorization: 'Payment paid' } })
47
46
  await app.request('/cached', { headers: { 'tempo-api-key': 'two' } })
48
- await app.request('/cached')
47
+ await app.request('/cached', { headers: { 'x-api-key': 'three' } })
49
48
 
50
- expect(downstreamCalls()).toMatchInlineSnapshot(`1`)
51
- expect(store.size).toMatchInlineSnapshot(`1`)
49
+ expect(downstreamCalls()).toMatchInlineSnapshot(`5`)
50
+ expect(store.gets).toMatchInlineSnapshot(`0`)
51
+ expect(store.size).toMatchInlineSnapshot(`0`)
52
52
  })
53
53
 
54
54
  test('bypasses the cache for query API key credentials', async () => {
@@ -48,8 +48,8 @@ const volatileHeaders = [
48
48
  /**
49
49
  * Serves GET responses from the edge cache store BEFORE auth and rate-limiting
50
50
  * run, so cache hits skip the per-request store round-trips that dominate
51
- * latency for every caller, authenticated or not. Place this ahead of the
52
- * auth middleware.
51
+ * latency. Credential-bearing requests bypass this layer so per-key policy
52
+ * enforcement always runs. Place this ahead of the auth middleware.
53
53
  *
54
54
  * Only 2xx responses from routes that opted into caching (via `Cache.response`,
55
55
  * which publishes the `edgeCache` policy) are stored, and the policy is only
@@ -84,8 +84,8 @@ export function middleware<environment extends Env>(
84
84
  store !== undefined &&
85
85
  c.req.method === 'GET' &&
86
86
  !c.req.raw.headers.has('range') &&
87
- // Query credentials must reach auth and never enter cache keys.
88
- !new URL(c.req.url).searchParams.has('key') &&
87
+ // Credential-bearing requests must reach auth for per-key policy enforcement.
88
+ !hasCredential(c.req.raw) &&
89
89
  isCacheableRoute(c)
90
90
  const key = eligible
91
91
  ? await routeCacheKey(c).catch(() => {
@@ -144,6 +144,15 @@ export function middleware<environment extends Env>(
144
144
  }
145
145
  }
146
146
 
147
+ function hasCredential(request: Request): boolean {
148
+ return (
149
+ request.headers.has('tempo-api-key') ||
150
+ request.headers.has('x-api-key') ||
151
+ request.headers.has('authorization') ||
152
+ new URL(request.url).searchParams.has('key')
153
+ )
154
+ }
155
+
147
156
  // Marks handlers whose route publishes an edge-cache policy; the middleware
148
157
  // pays the store lookup only when a matched handler carries it.
149
158
  const cacheableSymbol = Symbol('tempoApi.edgeCacheable')
@@ -0,0 +1,52 @@
1
+ import { Hono } from 'hono'
2
+
3
+ import * as IpAllowlist from './IpAllowlist.js'
4
+
5
+ describe('isRule', () => {
6
+ test.each([
7
+ ['203.0.113.7', true],
8
+ ['0.0.0.0/0', true],
9
+ ['203.0.113.0/24', true],
10
+ ['203.0.113.7/32', true],
11
+ ['2001:db8::7', true],
12
+ ['::/0', true],
13
+ ['2001:db8::/32', true],
14
+ ['2001:db8::7/128', true],
15
+ ['::ffff:192.0.2.1', true],
16
+ ['203.0.113.0/33', false],
17
+ ['2001:db8::/129', false],
18
+ ['203.0.113.0/-1', false],
19
+ ['203.0.113.999', false],
20
+ ['2001:db8::1%eth0', false],
21
+ ['203.0.113.0/24/1', false],
22
+ [' 203.0.113.7', false],
23
+ ['203.0.113.7 ', false],
24
+ ['*', false],
25
+ ['', false],
26
+ ])('%s -> %s', (rule, expected) => {
27
+ expect(IpAllowlist.isRule(rule)).toBe(expected)
28
+ })
29
+ })
30
+
31
+ describe('allows', () => {
32
+ test.each([
33
+ ['203.0.113.7', ['203.0.113.7'], true],
34
+ ['203.0.113.7', ['0.0.0.0/0'], true],
35
+ ['203.0.113.7', ['203.0.113.0/24'], true],
36
+ ['203.0.113.0', ['203.0.113.0/24'], true],
37
+ ['203.0.113.255', ['203.0.113.0/24'], true],
38
+ ['203.0.114.7', ['203.0.113.0/24'], false],
39
+ ['2001:db8:1::7', ['2001:db8::/32'], true],
40
+ ['2001:db8::7', ['2001:db8::7/128'], true],
41
+ ['2001:db8::8', ['2001:db8::7/128'], false],
42
+ ['2001:db9::7', ['2001:db8::/32'], false],
43
+ ['203.0.113.7', ['2001:db8::/32'], false],
44
+ ['::ffff:192.0.2.1', ['::ffff:192.0.2.1'], true],
45
+ ['not-an-ip', ['203.0.113.0/24'], false],
46
+ ])('%s in %j -> %s', async (address, rules, expected) => {
47
+ const app = new Hono().get('/', async (c) =>
48
+ c.json({ allowed: await IpAllowlist.allows(c, { address, rules }) }),
49
+ )
50
+ expect(await (await app.request('/')).json()).toEqual({ allowed: expected })
51
+ })
52
+ })
@@ -0,0 +1,39 @@
1
+ import type { Context } from 'hono'
2
+ import { ipRestriction } from 'hono/ip-restriction'
3
+
4
+ /** Returns whether a value is an IPv4/IPv6 address or CIDR range. */
5
+ export function isRule(value: string): boolean {
6
+ if (value === '*' || value.trim() !== value || value.split('/').length > 2) return false
7
+ try {
8
+ ipRestriction(() => '', { allowList: [value] })
9
+ return true
10
+ } catch {
11
+ return false
12
+ }
13
+ }
14
+
15
+ /** Returns whether the client address matches at least one allowlist rule. */
16
+ export async function allows(c: Context, options: allows.Options): Promise<boolean> {
17
+ const { address, rules } = options
18
+ if (!address) return false
19
+ try {
20
+ const denied = await ipRestriction(
21
+ () => address,
22
+ { allowList: [...rules] },
23
+ () => new Response(null, { status: 403 }),
24
+ )(c, async () => {})
25
+ return denied === undefined
26
+ } catch {
27
+ return false
28
+ }
29
+ }
30
+
31
+ export declare namespace allows {
32
+ /** Inputs used to check a client address. */
33
+ type Options = {
34
+ /** Trusted client IP address. */
35
+ address?: string | undefined
36
+ /** Exact IP and CIDR rules to match. */
37
+ rules: readonly string[]
38
+ }
39
+ }