tapimo 0.0.1 → 0.1.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 (274) hide show
  1. package/README.md +13 -3
  2. package/dist/ApiKey.d.ts +12 -11
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +10 -12
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +139 -0
  7. package/dist/ApiKeys.d.ts.map +1 -0
  8. package/dist/ApiKeys.js +166 -0
  9. package/dist/ApiKeys.js.map +1 -0
  10. package/dist/App.d.ts +920 -910
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +36 -12
  13. package/dist/App.js.map +1 -1
  14. package/dist/Handler.d.ts +86 -0
  15. package/dist/Handler.d.ts.map +1 -0
  16. package/dist/Handler.js +83 -0
  17. package/dist/Handler.js.map +1 -0
  18. package/dist/Scope.d.ts +4 -1
  19. package/dist/Scope.d.ts.map +1 -1
  20. package/dist/Scope.js +5 -1
  21. package/dist/Scope.js.map +1 -1
  22. package/dist/Webhooks.d.ts +3 -5
  23. package/dist/Webhooks.d.ts.map +1 -1
  24. package/dist/Webhooks.js +9 -2
  25. package/dist/Webhooks.js.map +1 -1
  26. package/dist/admin/Access.d.ts +1 -3
  27. package/dist/admin/Access.d.ts.map +1 -1
  28. package/dist/admin/Access.js +1 -3
  29. package/dist/admin/Access.js.map +1 -1
  30. package/dist/admin/App.d.ts +49 -160
  31. package/dist/admin/App.d.ts.map +1 -1
  32. package/dist/admin/App.js +2 -4
  33. package/dist/admin/App.js.map +1 -1
  34. package/dist/admin/apps/api-keys.d.ts +7 -5
  35. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  36. package/dist/admin/apps/api-keys.js +8 -9
  37. package/dist/admin/apps/api-keys.js.map +1 -1
  38. package/dist/admin/apps/scopes.d.ts +3 -0
  39. package/dist/admin/apps/scopes.d.ts.map +1 -1
  40. package/dist/admin/apps/verified-tokens.d.ts +101 -215
  41. package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
  42. package/dist/admin/apps/verified-tokens.js +17 -38
  43. package/dist/admin/apps/verified-tokens.js.map +1 -1
  44. package/dist/admin/ui.generated.d.ts +1 -1
  45. package/dist/admin/ui.generated.js +13 -13
  46. package/dist/admin/ui.generated.js.map +1 -1
  47. package/dist/apps/balances.d.ts +43 -43
  48. package/dist/apps/coingecko.d.ts +176 -176
  49. package/dist/apps/coingecko.js +1 -1
  50. package/dist/apps/coingecko.js.map +1 -1
  51. package/dist/apps/receipts.d.ts +43 -43
  52. package/dist/apps/tokenlist.d.ts +36 -36
  53. package/dist/apps/tokens.d.ts +454 -454
  54. package/dist/apps/transactions.d.ts +43 -43
  55. package/dist/apps/transfers.d.ts +29 -29
  56. package/dist/apps/webhooks.d.ts +5 -2
  57. package/dist/apps/webhooks.d.ts.map +1 -1
  58. package/dist/apps/webhooks.js +38 -34
  59. package/dist/apps/webhooks.js.map +1 -1
  60. package/dist/{Cli.d.ts → cli/Cli.d.ts} +13 -2
  61. package/dist/cli/Cli.d.ts.map +1 -0
  62. package/dist/cli/Cli.js +19 -0
  63. package/dist/cli/Cli.js.map +1 -0
  64. package/dist/cli/bin.d.ts +3 -0
  65. package/dist/cli/bin.d.ts.map +1 -0
  66. package/dist/cli/bin.js +4 -0
  67. package/dist/cli/bin.js.map +1 -0
  68. package/dist/cli/commands/admin.d.ts +15 -0
  69. package/dist/cli/commands/admin.d.ts.map +1 -0
  70. package/dist/cli/commands/admin.js +60 -0
  71. package/dist/cli/commands/admin.js.map +1 -0
  72. package/dist/cloudflare.d.ts +7 -0
  73. package/dist/cloudflare.d.ts.map +1 -1
  74. package/dist/cloudflare.js +9 -0
  75. package/dist/cloudflare.js.map +1 -1
  76. package/dist/db/Db.d.ts +103 -0
  77. package/dist/db/Db.d.ts.map +1 -0
  78. package/dist/db/Db.js +274 -0
  79. package/dist/db/Db.js.map +1 -0
  80. package/dist/db/Schema.d.ts +88 -0
  81. package/dist/db/Schema.d.ts.map +1 -0
  82. package/dist/db/Schema.js +130 -0
  83. package/dist/db/Schema.js.map +1 -0
  84. package/dist/db/tables/organizations.d.ts +79 -0
  85. package/dist/db/tables/organizations.d.ts.map +1 -0
  86. package/dist/db/tables/organizations.js +81 -0
  87. package/dist/db/tables/organizations.js.map +1 -0
  88. package/dist/db/tables/verifiedTokens.d.ts +53 -0
  89. package/dist/db/tables/verifiedTokens.d.ts.map +1 -0
  90. package/dist/db/tables/verifiedTokens.js +74 -0
  91. package/dist/db/tables/verifiedTokens.js.map +1 -0
  92. package/dist/db/tables/webhookDeliveries.d.ts +71 -0
  93. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -0
  94. package/dist/db/tables/webhookDeliveries.js +119 -0
  95. package/dist/db/tables/webhookDeliveries.js.map +1 -0
  96. package/dist/db/tables/webhookSubscriptions.d.ts +158 -0
  97. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -0
  98. package/dist/db/tables/webhookSubscriptions.js +272 -0
  99. package/dist/db/tables/webhookSubscriptions.js.map +1 -0
  100. package/dist/handlers/internal/executionError.d.ts +25 -0
  101. package/dist/handlers/internal/executionError.d.ts.map +1 -0
  102. package/dist/handlers/internal/executionError.js +184 -0
  103. package/dist/handlers/internal/executionError.js.map +1 -0
  104. package/dist/handlers/internal/multisig.d.ts +237 -0
  105. package/dist/handlers/internal/multisig.d.ts.map +1 -0
  106. package/dist/handlers/internal/multisig.js +567 -0
  107. package/dist/handlers/internal/multisig.js.map +1 -0
  108. package/dist/handlers/internal/requestListener.d.ts +124 -0
  109. package/dist/handlers/internal/requestListener.d.ts.map +1 -0
  110. package/dist/handlers/internal/requestListener.js +173 -0
  111. package/dist/handlers/internal/requestListener.js.map +1 -0
  112. package/dist/handlers/internal/sponsorship.d.ts +83 -0
  113. package/dist/handlers/internal/sponsorship.d.ts.map +1 -0
  114. package/dist/handlers/internal/sponsorship.js +103 -0
  115. package/dist/handlers/internal/sponsorship.js.map +1 -0
  116. package/dist/handlers/internal/types.d.ts +284 -0
  117. package/dist/handlers/internal/types.d.ts.map +1 -0
  118. package/dist/handlers/internal/types.js +2 -0
  119. package/dist/handlers/internal/types.js.map +1 -0
  120. package/dist/handlers/internal/utils.d.ts +34 -0
  121. package/dist/handlers/internal/utils.d.ts.map +1 -0
  122. package/dist/handlers/internal/utils.js +131 -0
  123. package/dist/handlers/internal/utils.js.map +1 -0
  124. package/dist/handlers/relay.d.ts +119 -0
  125. package/dist/handlers/relay.d.ts.map +1 -0
  126. package/dist/handlers/relay.js +1264 -0
  127. package/dist/handlers/relay.js.map +1 -0
  128. package/dist/index.d.ts +4 -2
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +4 -2
  131. package/dist/index.js.map +1 -1
  132. package/dist/internal/Auth.d.ts +16 -10
  133. package/dist/internal/Auth.d.ts.map +1 -1
  134. package/dist/internal/Auth.js +78 -13
  135. package/dist/internal/Auth.js.map +1 -1
  136. package/dist/internal/RpcSchema.d.ts +10 -0
  137. package/dist/internal/RpcSchema.d.ts.map +1 -0
  138. package/dist/internal/RpcSchema.js +2 -0
  139. package/dist/internal/RpcSchema.js.map +1 -0
  140. package/dist/internal/Store.d.ts +42 -3
  141. package/dist/internal/Store.d.ts.map +1 -1
  142. package/dist/internal/Store.js +87 -8
  143. package/dist/internal/Store.js.map +1 -1
  144. package/dist/internal/VerifiedTokens.d.ts +15 -34
  145. package/dist/internal/VerifiedTokens.d.ts.map +1 -1
  146. package/dist/internal/VerifiedTokens.js +115 -159
  147. package/dist/internal/VerifiedTokens.js.map +1 -1
  148. package/dist/internal/Viem.d.ts +81 -7
  149. package/dist/internal/Viem.d.ts.map +1 -1
  150. package/dist/internal/Webhooks.d.ts +49 -35
  151. package/dist/internal/Webhooks.d.ts.map +1 -1
  152. package/dist/internal/Webhooks.js +92 -205
  153. package/dist/internal/Webhooks.js.map +1 -1
  154. package/package.json +10 -5
  155. package/src/ApiKey.test.ts +3 -3
  156. package/src/ApiKey.ts +10 -12
  157. package/src/ApiKeys.test.ts +175 -0
  158. package/src/ApiKeys.ts +205 -0
  159. package/src/App.test-d.ts +1 -1
  160. package/src/App.test.ts +10768 -351
  161. package/src/App.ts +79 -36
  162. package/src/Client.test-d.ts +1 -1
  163. package/src/Handler.ts +154 -0
  164. package/src/Scope.ts +5 -1
  165. package/src/Webhooks.test.ts +8 -7
  166. package/src/Webhooks.ts +11 -5
  167. package/src/admin/Access.ts +1 -3
  168. package/src/admin/App.ts +19 -24
  169. package/src/admin/apps/api-keys.test.ts +19 -18
  170. package/src/admin/apps/api-keys.ts +8 -9
  171. package/src/admin/apps/scopes.test.ts +4 -0
  172. package/src/admin/apps/verified-tokens.test.ts +3 -30
  173. package/src/admin/apps/verified-tokens.ts +17 -36
  174. package/src/admin/ui.generated.ts +13 -13
  175. package/src/apps/balances.test.ts +4 -3
  176. package/src/apps/blocks.test.ts +1 -1
  177. package/src/apps/coingecko.ts +1 -1
  178. package/src/apps/exchanges.test.ts +1 -1
  179. package/src/apps/indexer.test.ts +2 -2
  180. package/src/apps/receipts.test.ts +1 -1
  181. package/src/apps/rpc.test.ts +100 -10
  182. package/src/apps/tokenlist.test.ts +5 -7
  183. package/src/apps/tokens.test.ts +4 -3
  184. package/src/apps/transactions.test.ts +1 -1
  185. package/src/apps/transfers.test.ts +1 -1
  186. package/src/apps/verified-tokens.test.ts +24 -12
  187. package/src/apps/webhooks.test.ts +131 -144
  188. package/src/apps/webhooks.ts +45 -47
  189. package/src/{Cli.test.ts → cli/Cli.test.ts} +10 -9
  190. package/src/{Cli.ts → cli/Cli.ts} +16 -9
  191. package/src/cli/bin.ts +4 -0
  192. package/src/cli/commands/admin.test.ts +71 -0
  193. package/src/cli/commands/admin.ts +60 -0
  194. package/src/cloudflare.ts +10 -0
  195. package/src/db/Db.test.ts +84 -0
  196. package/src/db/Db.ts +383 -0
  197. package/src/db/Schema.ts +164 -0
  198. package/src/db/tables/organizations.test.ts +124 -0
  199. package/src/db/tables/organizations.ts +127 -0
  200. package/src/db/tables/verifiedTokens.ts +111 -0
  201. package/src/db/tables/webhookDeliveries.ts +159 -0
  202. package/src/db/tables/webhookSubscriptions.ts +358 -0
  203. package/src/handlers/internal/executionError.ts +205 -0
  204. package/src/handlers/internal/multisig.localnet.test.ts +146 -0
  205. package/src/handlers/internal/multisig.test.ts +744 -0
  206. package/src/handlers/internal/multisig.ts +858 -0
  207. package/src/handlers/internal/relay.localnet.test.ts +2035 -0
  208. package/src/handlers/internal/requestListener.ts +273 -0
  209. package/src/handlers/internal/sponsorship.ts +186 -0
  210. package/src/handlers/internal/types.ts +378 -0
  211. package/src/handlers/internal/utils.ts +153 -0
  212. package/src/handlers/relay.ts +1605 -0
  213. package/src/index.ts +4 -2
  214. package/src/internal/Auth.test.ts +78 -20
  215. package/src/internal/Auth.ts +98 -20
  216. package/src/internal/RateLimit.test.ts +1 -1
  217. package/src/internal/RpcSchema.ts +10 -0
  218. package/src/internal/Store.test.ts +179 -7
  219. package/src/internal/Store.ts +136 -11
  220. package/src/internal/VerifiedTokens.test.ts +99 -115
  221. package/src/internal/VerifiedTokens.ts +123 -175
  222. package/src/internal/Viem.ts +2 -1
  223. package/src/internal/Webhooks.test.ts +107 -107
  224. package/src/internal/Webhooks.ts +102 -220
  225. package/dist/ApiKeySource.d.ts +0 -166
  226. package/dist/ApiKeySource.d.ts.map +0 -1
  227. package/dist/ApiKeySource.js +0 -234
  228. package/dist/ApiKeySource.js.map +0 -1
  229. package/dist/Cli.d.ts.map +0 -1
  230. package/dist/Cli.js +0 -13
  231. package/dist/Cli.js.map +0 -1
  232. package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +0 -1
  233. package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
  234. package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
  235. package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
  236. package/seeds/31318/verified-tokens.json +0 -30
  237. package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
  238. package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +0 -1
  239. package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +0 -1
  240. package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +0 -4
  241. package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +0 -30
  242. package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +0 -1
  243. package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +0 -1
  244. package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +0 -1
  245. package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +0 -11
  246. package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +0 -1
  247. package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +0 -1
  248. package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
  249. package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +0 -1
  250. package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
  251. package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +0 -1
  252. package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +0 -5
  253. package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +0 -1
  254. package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +0 -1
  255. package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +0 -1
  256. package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +0 -1
  257. package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +0 -1
  258. package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +0 -1
  259. package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +0 -1
  260. package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +0 -1
  261. package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
  262. package/seeds/4217/verified-tokens.json +0 -177
  263. package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
  264. package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
  265. package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
  266. package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
  267. package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +0 -1
  268. package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +0 -1
  269. package/seeds/42431/verified-tokens.json +0 -44
  270. package/seeds/openrpc/README.md +0 -27
  271. package/seeds/openrpc/execution-apis.json +0 -8989
  272. package/src/ApiKeySource.test-d.ts +0 -11
  273. package/src/ApiKeySource.test.ts +0 -295
  274. package/src/ApiKeySource.ts +0 -303
package/src/index.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  export * as ApiKey from './ApiKey.js'
2
- export * as ApiKeySource from './ApiKeySource.js'
2
+ export * as ApiKeys from './ApiKeys.js'
3
3
  export * as App from './App.js'
4
4
  export * as Assets from './Assets.js'
5
5
  export * as Auth from './Auth.js'
6
- export * as Cli from './Cli.js'
6
+ export * as Cli from './cli/Cli.js'
7
7
  export * as Client from './Client.js'
8
+ export * as Db from './db/Db.js'
9
+ export * as Handler from './Handler.js'
8
10
  export * as Log from './internal/Log.js'
9
11
  export * as Metrics from './Metrics.js'
10
12
  export * as RateLimit from './internal/RateLimit.js'
@@ -1,7 +1,7 @@
1
1
  import { Hono } from 'hono'
2
2
  import { requestId } from 'hono/request-id'
3
3
  import { Mppx, tempo } from 'mppx/hono'
4
- import { RateLimit } from 'cadent'
4
+ import { RateLimit } from 'tapimo'
5
5
  import * as ApiKey from '../ApiKey.js'
6
6
  import * as Auth from './Auth.js'
7
7
  import * as Log from './Log.js'
@@ -96,7 +96,7 @@ describe('require', () => {
96
96
  // the downstream validator rejects it as `query_invalid` and the legacy
97
97
  // `?key=...` lane breaks on every strict-query endpoint.
98
98
  const auth = {
99
- apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
99
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
100
100
  rateLimit: RateLimit.memory(),
101
101
  }
102
102
  const app = new Hono<Auth.Environment>()
@@ -241,7 +241,7 @@ describe('require', () => {
241
241
  const app = createApp({
242
242
  auth: {
243
243
  apiKey: {
244
- source: TestApp.source({
244
+ resolve: TestApp.resolver({
245
245
  keys: [{ ...key, scopes: [Scope.wildcard], token }],
246
246
  }),
247
247
  },
@@ -273,7 +273,7 @@ describe('require', () => {
273
273
  function createChainApp(options: { public?: boolean } = {}) {
274
274
  const auth: Auth.Context = {
275
275
  apiKey: {
276
- source: TestApp.source({
276
+ resolve: TestApp.resolver({
277
277
  keys: [
278
278
  { ...key, scopes: [Scope.wildcard], token },
279
279
  {
@@ -399,7 +399,7 @@ describe('require', () => {
399
399
  const mpp = createMppServer()
400
400
  const app = createApp({
401
401
  auth: {
402
- apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
402
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
403
403
  mpp,
404
404
  rateLimit: overQuota(),
405
405
  },
@@ -420,7 +420,7 @@ describe('require', () => {
420
420
  const mpp = createMppServer()
421
421
  const app = createApp({
422
422
  auth: {
423
- apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
423
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
424
424
  mpp,
425
425
  rateLimit: overQuotaOnce(),
426
426
  },
@@ -451,7 +451,7 @@ describe('require', () => {
451
451
  const mpp = createMppServer()
452
452
  const app = createApp({
453
453
  auth: {
454
- apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
454
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
455
455
  mpp,
456
456
  rateLimit: overQuota(),
457
457
  },
@@ -477,7 +477,7 @@ describe('require', () => {
477
477
  const mpp = createMppServer()
478
478
  const app = createApp({
479
479
  auth: {
480
- apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
480
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
481
481
  mpp,
482
482
  rateLimit: overQuotaOnce(),
483
483
  },
@@ -537,7 +537,7 @@ describe('require', () => {
537
537
  test('returns rate-limit error without payment context', async () => {
538
538
  const app = createApp({
539
539
  auth: {
540
- apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
540
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
541
541
  rateLimit: overQuota(),
542
542
  },
543
543
  policy: { public: { rateLimit: { perMinute: 1 } } },
@@ -555,6 +555,64 @@ describe('require', () => {
555
555
  }
556
556
  `)
557
557
  })
558
+
559
+ test('fails open when the public rate-limit store errors', async () => {
560
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {})
561
+ try {
562
+ const app = createApp({
563
+ auth: {
564
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
565
+ rateLimit: {
566
+ async consume() {
567
+ throw new Error('Network connection lost.')
568
+ },
569
+ },
570
+ },
571
+ policy: { public: { rateLimit: { perMinute: 1 } } },
572
+ })
573
+ const response = await app.request('/protected', {
574
+ headers: { 'x-forwarded-for': '203.0.113.10' },
575
+ })
576
+ const body = await response.json()
577
+
578
+ expect(response.status).toMatchInlineSnapshot(`200`)
579
+ expect(response.headers.has('ratelimit-limit')).toMatchInlineSnapshot(`false`)
580
+ expect(body).toMatchInlineSnapshot(`
581
+ {
582
+ "id": "203.0.113.10",
583
+ "type": "public",
584
+ }
585
+ `)
586
+ expect(error.mock.calls.length).toMatchInlineSnapshot(`1`)
587
+ } finally {
588
+ error.mockRestore()
589
+ }
590
+ })
591
+
592
+ test('fails open when the API-key rate-limit store errors', async () => {
593
+ const error = vi.spyOn(console, 'error').mockImplementation(() => {})
594
+ try {
595
+ const app = createApp({
596
+ auth: {
597
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
598
+ rateLimit: {
599
+ async consume() {
600
+ throw new Error('Network connection lost.')
601
+ },
602
+ },
603
+ },
604
+ })
605
+ const response = await app.request('/protected', {
606
+ headers: { 'tempo-api-key': token },
607
+ })
608
+
609
+ expect(response.status).toMatchInlineSnapshot(`200`)
610
+ expect(response.headers.has('ratelimit-limit')).toMatchInlineSnapshot(`false`)
611
+ expect(error.mock.calls.length).toMatchInlineSnapshot(`1`)
612
+ } finally {
613
+ error.mockRestore()
614
+ }
615
+ })
558
616
  })
559
617
 
560
618
  describe('rateLimit per-scope', () => {
@@ -574,7 +632,7 @@ describe('require', () => {
574
632
  const auth = {
575
633
  apiKey: {
576
634
  rateLimits: { 'data:read': { perMinute: 1 }, 'webhooks:write': { perMinute: 1 } },
577
- source: TestApp.source({
635
+ resolve: TestApp.resolver({
578
636
  keys: [{ ...key, scopes: [Scope.wildcard], token }],
579
637
  }),
580
638
  },
@@ -628,7 +686,7 @@ describe('require', () => {
628
686
  const auth = {
629
687
  apiKey: {
630
688
  rateLimits: { 'data:read': { perMinute: 1_000 } },
631
- source: TestApp.source({
689
+ resolve: TestApp.resolver({
632
690
  keys: [{ ...key, scopes: [Scope.wildcard], token }],
633
691
  }),
634
692
  },
@@ -652,7 +710,7 @@ describe('require', () => {
652
710
  })
653
711
 
654
712
  test('per-key default overrides config default but not a route cap', async () => {
655
- const source = TestApp.source({
713
+ const resolve = TestApp.resolver({
656
714
  keys: [
657
715
  {
658
716
  ...key,
@@ -665,7 +723,7 @@ describe('require', () => {
665
723
 
666
724
  const open = createScopedApp({
667
725
  auth: {
668
- apiKey: { rateLimits: { '*': { perMinute: 1_000 } }, source },
726
+ apiKey: { rateLimits: { '*': { perMinute: 1_000 } }, resolve },
669
727
  rateLimit: RateLimit.memory(),
670
728
  },
671
729
  routes: [{ path: '/protected', policy: { apiKey: { scopes: ['data:read'] } } }],
@@ -676,7 +734,7 @@ describe('require', () => {
676
734
 
677
735
  const capped = createScopedApp({
678
736
  auth: {
679
- apiKey: { rateLimits: { '*': { perMinute: 1_000 } }, source },
737
+ apiKey: { rateLimits: { '*': { perMinute: 1_000 } }, resolve },
680
738
  rateLimit: RateLimit.memory(),
681
739
  },
682
740
  routes: [
@@ -699,7 +757,7 @@ describe('require', () => {
699
757
  test('per-key per-scope overrides everything including a route cap', async () => {
700
758
  const auth = {
701
759
  apiKey: {
702
- source: TestApp.source({
760
+ resolve: TestApp.resolver({
703
761
  keys: [
704
762
  {
705
763
  ...key,
@@ -732,7 +790,7 @@ describe('require', () => {
732
790
  const entries: Log.Entry[] = []
733
791
  const app = createApp({
734
792
  auth: {
735
- apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
793
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
736
794
  rateLimit: overQuota(),
737
795
  },
738
796
  logger: (entry) => void entries.push(entry),
@@ -759,7 +817,7 @@ describe('require', () => {
759
817
  const mpp = createMppServer()
760
818
  const app = createApp({
761
819
  auth: {
762
- apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
820
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
763
821
  mpp,
764
822
  rateLimit: overQuota(),
765
823
  },
@@ -788,7 +846,7 @@ function createScopedApp(options: {
788
846
  }) {
789
847
  const auth = options.auth ?? {
790
848
  apiKey: {
791
- source: TestApp.source({ keys: [{ ...key, scopes: [Scope.wildcard], token }] }),
849
+ resolve: TestApp.resolver({ keys: [{ ...key, scopes: [Scope.wildcard], token }] }),
792
850
  },
793
851
  rateLimit: RateLimit.memory(),
794
852
  }
@@ -814,7 +872,7 @@ type Options = {
814
872
 
815
873
  function createApp(options: Options = {}) {
816
874
  const auth = options.auth ?? {
817
- apiKey: { source: TestApp.source({ keys: [{ ...key, token }] }) },
875
+ apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
818
876
  rateLimit: RateLimit.memory(),
819
877
  }
820
878
  const app = new Hono<Auth.Environment>()
@@ -845,7 +903,7 @@ function createMppServer() {
845
903
  }),
846
904
  ],
847
905
  realm: 'tempo-api-test',
848
- secretKey: 'secret_test_key',
906
+ secretKey: 'secret_test_key_0123456789abcdef',
849
907
  })
850
908
  }
851
909
 
@@ -7,8 +7,9 @@ import { findTargetHandler } from 'hono/utils/handler'
7
7
  import { Credential, type Method } from 'mppx'
8
8
  import { Mppx, tempo } from 'mppx/hono'
9
9
 
10
- import * as ApiKeySource from '../ApiKeySource.js'
11
10
  import * as ApiKey from '../ApiKey.js'
11
+ import * as ApiKeys from '../ApiKeys.js'
12
+ import type * as Store from './Store.js'
12
13
  import * as OpenApi from './OpenApi.js'
13
14
  import * as RateLimit from './RateLimit.js'
14
15
  import * as Response from './Response.js'
@@ -29,8 +30,8 @@ export type Context = {
29
30
  * per-key {@link ApiKey.ApiKey.rateLimits} shape.
30
31
  */
31
32
  rateLimits?: Record<string, RateLimit.Limit> | undefined
32
- /** Backing API-key source. */
33
- source: ApiKeySource.Source
33
+ /** Resolves a presented token against the app's KV store on `c`. */
34
+ resolve: (c: hono_Context, token: string) => Promise<ApiKey.ApiKey | null>
34
35
  }
35
36
  | undefined
36
37
  /** MPP payment configuration. */
@@ -217,14 +218,20 @@ export function middleware<
217
218
  const { charge = {}, ...mpp } = options.mpp === false ? {} : (options.mpp ?? {})
218
219
  const defaults = laneDefaults(options)
219
220
  const base = basePolicy(options, defaults)
221
+ // Keys resolve against the app's KV state store on request context
222
+ // (`App.create({ kv })`), so the middleware needs no storage wiring; when no
223
+ // store is configured, every token resolves to `null` (closed).
220
224
  // Resolution is cached per isolate by default (positive 60s / negative 10s,
221
- // keyed by token sha256 — see `ApiKeySource.cached`) so hot keys skip the
222
- // backend read on the auth path of every request. Pass `apiKey.cache: false`
223
- // when instant revocation matters more than the per-request read.
224
- const source = (() => {
225
- if (options.apiKey === false || !options.apiKey?.source) return ApiKeySource.noop
226
- if (options.apiKey.cache === false) return options.apiKey.source
227
- return ApiKeySource.cached(options.apiKey.source, options.apiKey.cache)
225
+ // keyed by token sha256) so hot keys skip the backend read on the auth path
226
+ // of every request. Pass `apiKey.cache: false` when instant revocation
227
+ // matters more than the per-request read.
228
+ const resolve = (() => {
229
+ const uncached = (c: hono_Context, token: string) => {
230
+ const kv = contextKv(c)
231
+ return kv ? ApiKeys.resolve(kv.store, token) : Promise.resolve(null)
232
+ }
233
+ if (options.apiKey === false || options.apiKey?.cache === false) return uncached
234
+ return cachedResolve(uncached, options.apiKey?.cache ?? {})
228
235
  })()
229
236
  const auth: Context = {
230
237
  ...(options.apiKey === false
@@ -232,7 +239,7 @@ export function middleware<
232
239
  : {
233
240
  apiKey: {
234
241
  ...(options.apiKey?.rateLimits ? { rateLimits: options.apiKey.rateLimits } : {}),
235
- source,
242
+ resolve,
236
243
  },
237
244
  }),
238
245
  ...(options.mpp === false
@@ -280,16 +287,14 @@ export declare namespace middleware {
280
287
  apiKey?:
281
288
  | {
282
289
  /**
283
- * Resolve-cache tuning for the source (per-isolate, positive 60s /
284
- * negative 10s by default), or `false` to resolve uncached on every
285
- * request — e.g. when instant revocation matters more than the
286
- * per-request backend read.
290
+ * Resolve-cache tuning (per-isolate, positive 60s / negative 10s by
291
+ * default), or `false` to resolve uncached on every request — e.g.
292
+ * when instant revocation matters more than the per-request backend
293
+ * read.
287
294
  */
288
- cache?: ApiKeySource.cached.Options | false | undefined
295
+ cache?: Cache | false | undefined
289
296
  /** API-key quotas keyed by quota scope (reserved `'*'` is the config default). */
290
297
  rateLimits?: Record<string, RateLimit.Limit> | undefined
291
- /** Backing API-key source. Defaults to an empty source (rejects all). */
292
- source?: ApiKeySource.Source | undefined
293
298
  /** Required API-key scopes. */
294
299
  scopes?: readonly Scope.Id[] | undefined
295
300
  }
@@ -311,6 +316,16 @@ export declare namespace middleware {
311
316
  rateLimit?: RateLimit.memory.Options | undefined
312
317
  }
313
318
 
319
+ /** Resolve-cache tuning for API-key resolution. */
320
+ type Cache = {
321
+ /** Time-to-live for negative (`null`) results in milliseconds. */
322
+ negativeTtl?: number | undefined
323
+ /** Clock used for expiry. */
324
+ now?: (() => number) | undefined
325
+ /** Time-to-live for positive results in milliseconds. */
326
+ ttl?: number | undefined
327
+ }
328
+
314
329
  /** MPP payment options used by auth middleware. */
315
330
  type MppOptions = Omit<NonNullable<Parameters<typeof Mppx.create>[0]>, 'methods'> & {
316
331
  /** Global Tempo charge options passed to the MPP method instantiator. */
@@ -612,7 +627,7 @@ export function require<environment extends Environment = Environment>(
612
627
 
613
628
  const hasPayment = hasPaymentCredential(c) && isPaymentEnabled(auth, policy)
614
629
  if (credential) {
615
- const apiKey = await auth.apiKey?.source.resolve(credential.token)
630
+ const apiKey = await auth.apiKey?.resolve(c, credential.token)
616
631
  if (!apiKey)
617
632
  return jsonError(c, {
618
633
  code: 'api_key_invalid',
@@ -796,7 +811,12 @@ function consumeRateLimit(
796
811
  options: RateLimit.Store.ConsumeOptions,
797
812
  ): Promise<RateLimit.Result | null> {
798
813
  if (!auth.rateLimit) return Promise.resolve(null)
799
- return auth.rateLimit.consume(options)
814
+ // Fail open: a rate-limit store error (e.g. a lost Durable Object
815
+ // connection) degrades to unenforced quota instead of failing the request.
816
+ return auth.rateLimit.consume(options).catch((error) => {
817
+ console.error('[auth] rate-limit consume failed; failing open', error)
818
+ return null
819
+ })
800
820
  }
801
821
 
802
822
  /**
@@ -1005,3 +1025,61 @@ function setRateLimitHeaders(c: hono_Context, result: RateLimit.Result) {
1005
1025
  c.header('RateLimit-Remaining', String(result.remaining))
1006
1026
  c.header('RateLimit-Reset', String(result.reset))
1007
1027
  }
1028
+
1029
+ // Reads the app's KV state store from request context (set by `App.create`
1030
+ // from its `kv` option); `undefined` leaves API-key auth closed.
1031
+ function contextKv(c: hono_Context): { store: Store.State } | undefined {
1032
+ return (c.get as (key: string) => unknown)('kv') as { store: Store.State } | undefined
1033
+ }
1034
+
1035
+ /** Maximum resolve-cache entries held per isolate before eviction. */
1036
+ const cachedMaxEntries = 10_000
1037
+
1038
+ /**
1039
+ * Wraps token resolution with a short-TTL per-isolate cache so hot keys skip
1040
+ * the backend read on every request. Positive results cache for `ttl`
1041
+ * (default 60s), negative for `negativeTtl` (default 10s) so a freshly minted
1042
+ * key is usable quickly; concurrent misses for one token are single-flighted.
1043
+ * Entries are keyed by the token's sha256 (`ApiKey.hash`) — never the
1044
+ * plaintext. A revoked key may stay resolvable for up to `ttl` — the accepted
1045
+ * trade-off for removing a backend read from the auth path.
1046
+ */
1047
+ function cachedResolve(
1048
+ resolve: (c: hono_Context, token: string) => Promise<ApiKey.ApiKey | null>,
1049
+ options: middleware.Cache,
1050
+ ): (c: hono_Context, token: string) => Promise<ApiKey.ApiKey | null> {
1051
+ const { negativeTtl = 10_000, now = Date.now, ttl = 60_000 } = options
1052
+ type Cell = { expiresAt: number; value: ApiKey.ApiKey | null }
1053
+ const cells = new Map<string, Cell>()
1054
+ const flights = new Map<string, Promise<ApiKey.ApiKey | null>>()
1055
+
1056
+ return async (c, token) => {
1057
+ // Versioned so a future record-shape change can't serve stale shapes.
1058
+ const key = `v1:${ApiKey.hash(token)}`
1059
+ const cell = cells.get(key)
1060
+ if (cell && cell.expiresAt > now()) return cell.value
1061
+ if (cell) cells.delete(key)
1062
+
1063
+ const existing = flights.get(key)
1064
+ if (existing) return existing
1065
+
1066
+ const flight = (async () => {
1067
+ const value = await resolve(c, token)
1068
+ // Bound the cache: random invalid tokens each add a (negative) entry,
1069
+ // so sweep expired cells at the cap and then drop the oldest.
1070
+ if (cells.size >= cachedMaxEntries) {
1071
+ const time = now()
1072
+ for (const [name, entry] of cells) if (entry.expiresAt <= time) cells.delete(name)
1073
+ while (cells.size >= cachedMaxEntries) cells.delete(cells.keys().next().value as string)
1074
+ }
1075
+ cells.set(key, { expiresAt: now() + (value ? ttl : negativeTtl), value })
1076
+ return value
1077
+ })()
1078
+ flights.set(key, flight)
1079
+ try {
1080
+ return await flight
1081
+ } finally {
1082
+ flights.delete(key)
1083
+ }
1084
+ }
1085
+ }
@@ -1,4 +1,4 @@
1
- import { RateLimit, Store } from 'cadent'
1
+ import { RateLimit, Store } from 'tapimo'
2
2
 
3
3
  describe('memory', () => {
4
4
  test('tracks fixed-window quota in a store', async () => {
@@ -0,0 +1,10 @@
1
+ import type { RpcSchema } from 'ox'
2
+
3
+ /**
4
+ * Ox-compatible JSON-RPC schema union used to type the relay handler's incoming
5
+ * requests (`RpcRequest.RpcRequest<Ox>`). The relay switches on `request.method`
6
+ * string literals and casts params per method, so a permissive schema whose
7
+ * `params` is an array is sufficient — the full method-by-method schema union
8
+ * from the accounts SDK is not needed here.
9
+ */
10
+ export type Ox = RpcSchema.Generic<string, readonly unknown[]>
@@ -1,4 +1,4 @@
1
- import { Store } from 'cadent'
1
+ import { Store } from 'tapimo'
2
2
 
3
3
  describe('from', () => {
4
4
  const ops = {
@@ -68,7 +68,89 @@ describe('increment', () => {
68
68
  })
69
69
  })
70
70
 
71
+ describe('update', () => {
72
+ test('falls back to a get + put read-modify-write without a native swap', async () => {
73
+ const cells = new Map<string, string>()
74
+ const store = Store.from({
75
+ async delete(key) {
76
+ cells.delete(key)
77
+ },
78
+ async get(key) {
79
+ return cells.get(key) ?? null
80
+ },
81
+ async list() {
82
+ return { keys: [] }
83
+ },
84
+ async put(key, value) {
85
+ cells.set(key, value)
86
+ },
87
+ })
88
+
89
+ expect(await Store.update(store, 'value', (current) => `${current ?? 'start'}+`)).toMatchInlineSnapshot(`"start+"`) // prettier-ignore
90
+ expect(await Store.update(store, 'value', (current) => `${current}+`)).toMatchInlineSnapshot(`"start++"`) // prettier-ignore
91
+ })
92
+
93
+ test('retries the transform when the compare-and-swap loses', async () => {
94
+ const store = Store.memory()
95
+ await store.put('value', 'a')
96
+
97
+ // Interleave a competing write between the read and the swap once.
98
+ let raced = false
99
+ const get = store.get.bind(store)
100
+ store.get = async (key) => {
101
+ const current = await get(key)
102
+ if (!raced) {
103
+ raced = true
104
+ await store.put('value', 'b')
105
+ }
106
+ return current
107
+ }
108
+
109
+ const transforms: (null | string)[] = []
110
+ const written = await Store.update(store, 'value', (current) => {
111
+ transforms.push(current)
112
+ return `${current}c`
113
+ })
114
+
115
+ expect(written).toMatchInlineSnapshot(`"bc"`)
116
+ expect(transforms).toMatchInlineSnapshot(`
117
+ [
118
+ "a",
119
+ "b",
120
+ ]
121
+ `)
122
+ })
123
+
124
+ test('fails after exhausting compare-and-swap attempts', async () => {
125
+ const store = Store.memory()
126
+ await store.put('value', 'a')
127
+
128
+ // Every read is immediately invalidated by a competing write.
129
+ const get = store.get.bind(store)
130
+ store.get = async (key) => {
131
+ const current = await get(key)
132
+ await store.put('value', `${current}!`)
133
+ return current
134
+ }
135
+
136
+ await expect(
137
+ Store.update(store, 'value', (current) => `${current}c`, { attempts: 2 }),
138
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
139
+ `[Store.UpdateContentionError: Store update on "value" lost 2 compare-and-swap attempts.]`,
140
+ )
141
+ })
142
+ })
143
+
71
144
  describe('memory', () => {
145
+ test('swaps atomically against the expected value', async () => {
146
+ const store = Store.memory()
147
+
148
+ expect(await store.swap!('value', null, 'first')).toMatchInlineSnapshot(`true`)
149
+ expect(await store.swap!('value', null, 'clobber')).toMatchInlineSnapshot(`false`)
150
+ expect(await store.swap!('value', 'first', 'second')).toMatchInlineSnapshot(`true`)
151
+ expect(await store.get('value')).toMatchInlineSnapshot(`"second"`)
152
+ })
153
+
72
154
  test('treats expired entries as absent', async () => {
73
155
  const store = Store.memory()
74
156
 
@@ -193,6 +275,16 @@ describe('cloudflareKv', () => {
193
275
  })
194
276
 
195
277
  describe('durableObject', () => {
278
+ // The stub contract requires native `increment` and `swap` (our DO class
279
+ // implements them); derive them for the in-memory fakes.
280
+ function stubFor(backing: Store.State): Store.durableObject.Stub {
281
+ return {
282
+ ...backing,
283
+ increment: (key, options) => Store.increment(backing, key, options),
284
+ swap: (key, expected, next, options) => backing.swap!(key, expected, next, options),
285
+ }
286
+ }
287
+
196
288
  test('adapts a Durable Object namespace without caching stubs', async () => {
197
289
  const calls: string[] = []
198
290
  const backing = Store.memory({
@@ -201,12 +293,7 @@ describe('durableObject', () => {
201
293
  const namespace = {
202
294
  getByName(name) {
203
295
  calls.push(name)
204
- // The stub contract requires a native `increment` (our DO class
205
- // implements it); derive one for the in-memory fake.
206
- return {
207
- ...backing,
208
- increment: (key, options) => Store.increment(backing, key, options),
209
- }
296
+ return stubFor(backing)
210
297
  },
211
298
  } satisfies Store.durableObject.Namespace
212
299
  const store = Store.durableObject(namespace, { name: 'tempo-api' })
@@ -250,6 +337,7 @@ describe('durableObject', () => {
250
337
  increment: (key, options) => Store.increment(backing, key, options),
251
338
  list: (options) => backing.list(options),
252
339
  put: (key, value, options) => backing.put(key, value, options),
340
+ swap: (key, expected, next, options) => backing.swap!(key, expected, next, options),
253
341
  } satisfies Store.durableObject.Stub
254
342
  return new Proxy(stub, {
255
343
  get(target, method: string) {
@@ -286,6 +374,90 @@ describe('durableObject', () => {
286
374
  )
287
375
  })
288
376
 
377
+ test('retries a retryable stub error once on a fresh stub', async () => {
378
+ let stubs = 0
379
+ let failures = 1
380
+ const backing = Store.memory()
381
+ const namespace = {
382
+ getByName() {
383
+ stubs += 1
384
+ return {
385
+ ...stubFor(backing),
386
+ increment: (key, options) => {
387
+ if (failures > 0) {
388
+ failures -= 1
389
+ throw Object.assign(new Error('Network connection lost.'), { retryable: true })
390
+ }
391
+ return Store.increment(backing, key, options)
392
+ },
393
+ }
394
+ },
395
+ } satisfies Store.durableObject.Namespace
396
+ const store = Store.durableObject(namespace, { name: 'tempo-api' })
397
+
398
+ expect(await Store.increment(store, 'counter')).toMatchInlineSnapshot(`1`)
399
+ // One stub per attempt: a stub that threw is disconnected.
400
+ expect(stubs).toMatchInlineSnapshot(`2`)
401
+ })
402
+
403
+ test('propagates a retryable stub error when the retry also fails', async () => {
404
+ let attempts = 0
405
+ const namespace = {
406
+ getByName() {
407
+ return {
408
+ ...stubFor(Store.memory()),
409
+ increment: () => {
410
+ attempts += 1
411
+ throw Object.assign(new Error('Network connection lost.'), { retryable: true })
412
+ },
413
+ }
414
+ },
415
+ } satisfies Store.durableObject.Namespace
416
+ const store = Store.durableObject(namespace, { name: 'tempo-api' })
417
+
418
+ await expect(Store.increment(store, 'counter')).rejects.toThrowErrorMatchingInlineSnapshot(
419
+ `[Error: Network connection lost.]`,
420
+ )
421
+ expect(attempts).toMatchInlineSnapshot(`2`)
422
+ })
423
+
424
+ test('does not retry non-retryable or overloaded stub errors', async () => {
425
+ let attempts = 0
426
+ const namespaceFor = (error: Error) =>
427
+ ({
428
+ getByName() {
429
+ return {
430
+ ...stubFor(Store.memory()),
431
+ increment: () => {
432
+ attempts += 1
433
+ throw error
434
+ },
435
+ }
436
+ },
437
+ }) satisfies Store.durableObject.Namespace
438
+
439
+ const plain = Store.durableObject(namespaceFor(new Error('boom')), { name: 'tempo-api' })
440
+ await expect(Store.increment(plain, 'counter')).rejects.toThrowErrorMatchingInlineSnapshot(
441
+ `[Error: boom]`,
442
+ )
443
+ expect(attempts).toMatchInlineSnapshot(`1`)
444
+
445
+ attempts = 0
446
+ const overloaded = Store.durableObject(
447
+ namespaceFor(
448
+ Object.assign(new Error('Durable Object is overloaded.'), {
449
+ overloaded: true,
450
+ retryable: true,
451
+ }),
452
+ ),
453
+ { name: 'tempo-api' },
454
+ )
455
+ await expect(Store.increment(overloaded, 'counter')).rejects.toThrowErrorMatchingInlineSnapshot(
456
+ `[Error: Durable Object is overloaded.]`,
457
+ )
458
+ expect(attempts).toMatchInlineSnapshot(`1`)
459
+ })
460
+
289
461
  test('defers missing namespace failures until an operation is attempted', async () => {
290
462
  const store = Store.durableObject(undefined as unknown as Store.durableObject.Namespace, {
291
463
  name: 'tempo-api',