tapimo 0.0.2 → 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 (265) hide show
  1. package/README.md +11 -2
  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 +98 -209
  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 +1 -1
  46. package/dist/apps/balances.d.ts +2 -2
  47. package/dist/apps/receipts.d.ts +43 -43
  48. package/dist/apps/tokens.d.ts +454 -454
  49. package/dist/apps/transactions.d.ts +43 -43
  50. package/dist/apps/transfers.d.ts +29 -29
  51. package/dist/apps/webhooks.d.ts +5 -2
  52. package/dist/apps/webhooks.d.ts.map +1 -1
  53. package/dist/apps/webhooks.js +38 -34
  54. package/dist/apps/webhooks.js.map +1 -1
  55. package/dist/{Cli.d.ts → cli/Cli.d.ts} +13 -2
  56. package/dist/cli/Cli.d.ts.map +1 -0
  57. package/dist/cli/Cli.js +19 -0
  58. package/dist/cli/Cli.js.map +1 -0
  59. package/dist/cli/bin.d.ts +3 -0
  60. package/dist/cli/bin.d.ts.map +1 -0
  61. package/dist/cli/bin.js +4 -0
  62. package/dist/cli/bin.js.map +1 -0
  63. package/dist/cli/commands/admin.d.ts +15 -0
  64. package/dist/cli/commands/admin.d.ts.map +1 -0
  65. package/dist/cli/commands/admin.js +60 -0
  66. package/dist/cli/commands/admin.js.map +1 -0
  67. package/dist/cloudflare.d.ts +7 -0
  68. package/dist/cloudflare.d.ts.map +1 -1
  69. package/dist/cloudflare.js +9 -0
  70. package/dist/cloudflare.js.map +1 -1
  71. package/dist/db/Db.d.ts +103 -0
  72. package/dist/db/Db.d.ts.map +1 -0
  73. package/dist/db/Db.js +274 -0
  74. package/dist/db/Db.js.map +1 -0
  75. package/dist/db/Schema.d.ts +88 -0
  76. package/dist/db/Schema.d.ts.map +1 -0
  77. package/dist/db/Schema.js +130 -0
  78. package/dist/db/Schema.js.map +1 -0
  79. package/dist/db/tables/organizations.d.ts +79 -0
  80. package/dist/db/tables/organizations.d.ts.map +1 -0
  81. package/dist/db/tables/organizations.js +81 -0
  82. package/dist/db/tables/organizations.js.map +1 -0
  83. package/dist/db/tables/verifiedTokens.d.ts +53 -0
  84. package/dist/db/tables/verifiedTokens.d.ts.map +1 -0
  85. package/dist/db/tables/verifiedTokens.js +74 -0
  86. package/dist/db/tables/verifiedTokens.js.map +1 -0
  87. package/dist/db/tables/webhookDeliveries.d.ts +71 -0
  88. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -0
  89. package/dist/db/tables/webhookDeliveries.js +119 -0
  90. package/dist/db/tables/webhookDeliveries.js.map +1 -0
  91. package/dist/db/tables/webhookSubscriptions.d.ts +158 -0
  92. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -0
  93. package/dist/db/tables/webhookSubscriptions.js +272 -0
  94. package/dist/db/tables/webhookSubscriptions.js.map +1 -0
  95. package/dist/handlers/internal/executionError.d.ts +25 -0
  96. package/dist/handlers/internal/executionError.d.ts.map +1 -0
  97. package/dist/handlers/internal/executionError.js +184 -0
  98. package/dist/handlers/internal/executionError.js.map +1 -0
  99. package/dist/handlers/internal/multisig.d.ts +237 -0
  100. package/dist/handlers/internal/multisig.d.ts.map +1 -0
  101. package/dist/handlers/internal/multisig.js +567 -0
  102. package/dist/handlers/internal/multisig.js.map +1 -0
  103. package/dist/handlers/internal/requestListener.d.ts +124 -0
  104. package/dist/handlers/internal/requestListener.d.ts.map +1 -0
  105. package/dist/handlers/internal/requestListener.js +173 -0
  106. package/dist/handlers/internal/requestListener.js.map +1 -0
  107. package/dist/handlers/internal/sponsorship.d.ts +83 -0
  108. package/dist/handlers/internal/sponsorship.d.ts.map +1 -0
  109. package/dist/handlers/internal/sponsorship.js +103 -0
  110. package/dist/handlers/internal/sponsorship.js.map +1 -0
  111. package/dist/handlers/internal/types.d.ts +284 -0
  112. package/dist/handlers/internal/types.d.ts.map +1 -0
  113. package/dist/handlers/internal/types.js +2 -0
  114. package/dist/handlers/internal/types.js.map +1 -0
  115. package/dist/handlers/internal/utils.d.ts +34 -0
  116. package/dist/handlers/internal/utils.d.ts.map +1 -0
  117. package/dist/handlers/internal/utils.js +131 -0
  118. package/dist/handlers/internal/utils.js.map +1 -0
  119. package/dist/handlers/relay.d.ts +119 -0
  120. package/dist/handlers/relay.d.ts.map +1 -0
  121. package/dist/handlers/relay.js +1264 -0
  122. package/dist/handlers/relay.js.map +1 -0
  123. package/dist/index.d.ts +4 -2
  124. package/dist/index.d.ts.map +1 -1
  125. package/dist/index.js +4 -2
  126. package/dist/index.js.map +1 -1
  127. package/dist/internal/Auth.d.ts +16 -10
  128. package/dist/internal/Auth.d.ts.map +1 -1
  129. package/dist/internal/Auth.js +72 -12
  130. package/dist/internal/Auth.js.map +1 -1
  131. package/dist/internal/RpcSchema.d.ts +10 -0
  132. package/dist/internal/RpcSchema.d.ts.map +1 -0
  133. package/dist/internal/RpcSchema.js +2 -0
  134. package/dist/internal/RpcSchema.js.map +1 -0
  135. package/dist/internal/Store.d.ts +42 -3
  136. package/dist/internal/Store.d.ts.map +1 -1
  137. package/dist/internal/Store.js +65 -3
  138. package/dist/internal/Store.js.map +1 -1
  139. package/dist/internal/VerifiedTokens.d.ts +15 -34
  140. package/dist/internal/VerifiedTokens.d.ts.map +1 -1
  141. package/dist/internal/VerifiedTokens.js +115 -159
  142. package/dist/internal/VerifiedTokens.js.map +1 -1
  143. package/dist/internal/Webhooks.d.ts +49 -35
  144. package/dist/internal/Webhooks.d.ts.map +1 -1
  145. package/dist/internal/Webhooks.js +92 -205
  146. package/dist/internal/Webhooks.js.map +1 -1
  147. package/package.json +7 -2
  148. package/src/ApiKey.test.ts +3 -3
  149. package/src/ApiKey.ts +10 -12
  150. package/src/ApiKeys.test.ts +175 -0
  151. package/src/ApiKeys.ts +205 -0
  152. package/src/App.test-d.ts +1 -1
  153. package/src/App.test.ts +10768 -351
  154. package/src/App.ts +79 -36
  155. package/src/Client.test-d.ts +1 -1
  156. package/src/Handler.ts +154 -0
  157. package/src/Scope.ts +5 -1
  158. package/src/Webhooks.test.ts +8 -7
  159. package/src/Webhooks.ts +11 -5
  160. package/src/admin/Access.ts +1 -3
  161. package/src/admin/App.ts +19 -24
  162. package/src/admin/apps/api-keys.test.ts +19 -18
  163. package/src/admin/apps/api-keys.ts +8 -9
  164. package/src/admin/apps/scopes.test.ts +4 -0
  165. package/src/admin/apps/verified-tokens.test.ts +3 -30
  166. package/src/admin/apps/verified-tokens.ts +17 -36
  167. package/src/admin/ui.generated.ts +1 -1
  168. package/src/apps/balances.test.ts +4 -3
  169. package/src/apps/blocks.test.ts +1 -1
  170. package/src/apps/exchanges.test.ts +1 -1
  171. package/src/apps/indexer.test.ts +1 -1
  172. package/src/apps/receipts.test.ts +1 -1
  173. package/src/apps/rpc.test.ts +100 -10
  174. package/src/apps/tokenlist.test.ts +5 -7
  175. package/src/apps/tokens.test.ts +4 -3
  176. package/src/apps/transactions.test.ts +1 -1
  177. package/src/apps/transfers.test.ts +1 -1
  178. package/src/apps/verified-tokens.test.ts +24 -12
  179. package/src/apps/webhooks.test.ts +130 -143
  180. package/src/apps/webhooks.ts +45 -47
  181. package/src/{Cli.test.ts → cli/Cli.test.ts} +10 -9
  182. package/src/{Cli.ts → cli/Cli.ts} +16 -9
  183. package/src/cli/bin.ts +4 -0
  184. package/src/cli/commands/admin.test.ts +71 -0
  185. package/src/cli/commands/admin.ts +60 -0
  186. package/src/cloudflare.ts +10 -0
  187. package/src/db/Db.test.ts +84 -0
  188. package/src/db/Db.ts +383 -0
  189. package/src/db/Schema.ts +164 -0
  190. package/src/db/tables/organizations.test.ts +124 -0
  191. package/src/db/tables/organizations.ts +127 -0
  192. package/src/db/tables/verifiedTokens.ts +111 -0
  193. package/src/db/tables/webhookDeliveries.ts +159 -0
  194. package/src/db/tables/webhookSubscriptions.ts +358 -0
  195. package/src/handlers/internal/executionError.ts +205 -0
  196. package/src/handlers/internal/multisig.localnet.test.ts +146 -0
  197. package/src/handlers/internal/multisig.test.ts +744 -0
  198. package/src/handlers/internal/multisig.ts +858 -0
  199. package/src/handlers/internal/relay.localnet.test.ts +2035 -0
  200. package/src/handlers/internal/requestListener.ts +273 -0
  201. package/src/handlers/internal/sponsorship.ts +186 -0
  202. package/src/handlers/internal/types.ts +378 -0
  203. package/src/handlers/internal/utils.ts +153 -0
  204. package/src/handlers/relay.ts +1605 -0
  205. package/src/index.ts +4 -2
  206. package/src/internal/Auth.test.ts +21 -21
  207. package/src/internal/Auth.ts +92 -19
  208. package/src/internal/RateLimit.test.ts +1 -1
  209. package/src/internal/RpcSchema.ts +10 -0
  210. package/src/internal/Store.test.ts +98 -10
  211. package/src/internal/Store.ts +112 -6
  212. package/src/internal/VerifiedTokens.test.ts +99 -115
  213. package/src/internal/VerifiedTokens.ts +123 -175
  214. package/src/internal/Webhooks.test.ts +107 -107
  215. package/src/internal/Webhooks.ts +102 -220
  216. package/dist/ApiKeySource.d.ts +0 -166
  217. package/dist/ApiKeySource.d.ts.map +0 -1
  218. package/dist/ApiKeySource.js +0 -234
  219. package/dist/ApiKeySource.js.map +0 -1
  220. package/dist/Cli.d.ts.map +0 -1
  221. package/dist/Cli.js +0 -13
  222. package/dist/Cli.js.map +0 -1
  223. package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +0 -1
  224. package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
  225. package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
  226. package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
  227. package/seeds/31318/verified-tokens.json +0 -30
  228. package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
  229. package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +0 -1
  230. package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +0 -1
  231. package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +0 -4
  232. package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +0 -30
  233. package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +0 -1
  234. package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +0 -1
  235. package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +0 -1
  236. package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +0 -11
  237. package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +0 -1
  238. package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +0 -1
  239. package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
  240. package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +0 -1
  241. package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
  242. package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +0 -1
  243. package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +0 -5
  244. package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +0 -1
  245. package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +0 -1
  246. package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +0 -1
  247. package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +0 -1
  248. package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +0 -1
  249. package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +0 -1
  250. package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +0 -1
  251. package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +0 -1
  252. package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
  253. package/seeds/4217/verified-tokens.json +0 -177
  254. package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +0 -25
  255. package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +0 -1
  256. package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +0 -1
  257. package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +0 -1
  258. package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +0 -1
  259. package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +0 -1
  260. package/seeds/42431/verified-tokens.json +0 -44
  261. package/seeds/openrpc/README.md +0 -27
  262. package/seeds/openrpc/execution-apis.json +0 -8989
  263. package/src/ApiKeySource.test-d.ts +0 -11
  264. package/src/ApiKeySource.test.ts +0 -295
  265. package/src/ApiKeySource.ts +0 -303
@@ -6,6 +6,7 @@ import type * as App from '../App.js'
6
6
  import * as Auth from '../internal/Auth.js'
7
7
  import * as Cache from '../internal/Cache.js'
8
8
  import * as Cursor from '../internal/Cursor.js'
9
+ import * as Db from '../db/Db.js'
9
10
  import * as OpenApi from '../internal/OpenApi.js'
10
11
  import * as Response from '../internal/Response.js'
11
12
  import * as Schema from '../internal/Schema.js'
@@ -1306,7 +1307,7 @@ export function webhooks(options: webhooks.Options = {}) {
1306
1307
  })
1307
1308
 
1308
1309
  const webhook = c.get('webhook')!
1309
- const store = webhook.store
1310
+ const db = Db.get(c.get('db'))
1310
1311
  const owner = ownerFor(c)
1311
1312
  if (!owner) return unauthorized(c)
1312
1313
 
@@ -1315,7 +1316,7 @@ export function webhooks(options: webhooks.Options = {}) {
1315
1316
 
1316
1317
  try {
1317
1318
  const subscription = await Webhooks.createSubscription(
1318
- store,
1319
+ db,
1319
1320
  {
1320
1321
  chainId,
1321
1322
  ...(body.context === undefined ? {} : { context: body.context }),
@@ -1365,7 +1366,7 @@ export function webhooks(options: webhooks.Options = {}) {
1365
1366
  message: 'Check the query parameters and try again.',
1366
1367
  })
1367
1368
 
1368
- const store = c.get('webhook')!.store
1369
+ const db = Db.get(c.get('db'))
1369
1370
  const owner = ownerFor(c)
1370
1371
  if (!owner) return unauthorized(c)
1371
1372
 
@@ -1375,12 +1376,12 @@ export function webhooks(options: webhooks.Options = {}) {
1375
1376
  // run it concurrently with the page list. Best-effort: a failure
1376
1377
  // omits `meta` rather than failing the page.
1377
1378
  const countPromise = include.includes('totalCount')
1378
- ? Webhooks.countSubscriptions(store, owner).catch(() => undefined)
1379
+ ? Webhooks.countSubscriptions(db, owner).catch(() => undefined)
1379
1380
  : undefined
1380
1381
 
1381
1382
  // Fetch one extra to detect a further page without a second round-trip.
1382
1383
  // `page` translates to a positional slice of `(page - 1) * limit` rows.
1383
- const rows = await Webhooks.listSubscriptions(store, owner, {
1384
+ const rows = await Webhooks.listSubscriptions(db, owner, {
1384
1385
  cursor,
1385
1386
  limit: limit + 1,
1386
1387
  offset: page !== undefined && page > 1 ? (page - 1) * limit : undefined,
@@ -1445,13 +1446,13 @@ export function webhooks(options: webhooks.Options = {}) {
1445
1446
  message: 'Check the path parameters and try again.',
1446
1447
  })
1447
1448
 
1448
- const store = c.get('webhook')!.store
1449
+ const db = Db.get(c.get('db'))
1449
1450
  const owner = ownerFor(c)
1450
1451
  if (!owner) return unauthorized(c)
1451
1452
 
1452
1453
  const { id } = c.req.valid('param')
1453
1454
  try {
1454
- const subscription = await Webhooks.getSubscription(store, owner, id)
1455
+ const subscription = await Webhooks.getSubscription(db, owner, id)
1455
1456
  if (!subscription) return notFound(c)
1456
1457
  return c.json(
1457
1458
  Response.validated(schema.getWebhook.Response, toPublic(subscription)),
@@ -1506,7 +1507,7 @@ export function webhooks(options: webhooks.Options = {}) {
1506
1507
  message: 'Check the query parameters and try again.',
1507
1508
  })
1508
1509
 
1509
- const store = c.get('webhook')!.store
1510
+ const db = Db.get(c.get('db'))
1510
1511
  const owner = ownerFor(c)
1511
1512
  if (!owner) return unauthorized(c)
1512
1513
 
@@ -1515,19 +1516,19 @@ export function webhooks(options: webhooks.Options = {}) {
1515
1516
  try {
1516
1517
  // Owner-scope first: resolving the subscription under the caller's owner
1517
1518
  // key 404s ids they don't own (indistinguishable from "doesn't exist").
1518
- const subscription = await Webhooks.getSubscription(store, owner, id)
1519
+ const subscription = await Webhooks.getSubscription(db, owner, id)
1519
1520
  if (!subscription) return notFound(c)
1520
1521
 
1521
1522
  // `totalCount` is opt-in and exact (the delivery log is TTL-bounded);
1522
1523
  // run it concurrently with the page list. Best-effort: a failure
1523
1524
  // omits `meta` rather than failing the page.
1524
1525
  const countPromise = include.includes('totalCount')
1525
- ? Webhooks.countDeliveries(store, id).catch(() => undefined)
1526
+ ? Webhooks.countDeliveries(db, id).catch(() => undefined)
1526
1527
  : undefined
1527
1528
 
1528
1529
  // Fetch one extra to detect a further page without a second round-trip.
1529
1530
  // `page` translates to a positional slice of `(page - 1) * limit` rows.
1530
- const rows = await Webhooks.listDeliveries(store, id, {
1531
+ const rows = await Webhooks.listDeliveries(db, id, {
1531
1532
  cursor,
1532
1533
  limit: limit + 1,
1533
1534
  offset: page !== undefined && page > 1 ? (page - 1) * limit : undefined,
@@ -1589,20 +1590,20 @@ export function webhooks(options: webhooks.Options = {}) {
1589
1590
  message: 'Check the path parameters and try again.',
1590
1591
  })
1591
1592
 
1592
- const store = c.get('webhook')!.store
1593
+ const db = Db.get(c.get('db'))
1593
1594
  const owner = ownerFor(c)
1594
1595
  if (!owner) return unauthorized(c)
1595
1596
 
1596
1597
  const { id } = c.req.valid('param')
1597
1598
  try {
1598
1599
  // Owner-scope first so unowned/unknown ids are indistinguishable (404).
1599
- const subscription = await Webhooks.getSubscription(store, owner, id)
1600
+ const subscription = await Webhooks.getSubscription(db, owner, id)
1600
1601
  if (!subscription) return notFound(c)
1601
1602
 
1602
1603
  // Inline delivery (not the queue) so the caller gets an immediate
1603
1604
  // result. `ping` never mutates the subscription's failure/lifecycle
1604
1605
  // state — a test must not disable a healthy endpoint.
1605
- const { envelope, result } = await Webhooks.ping(store, subscription)
1606
+ const { envelope, result } = await Webhooks.ping(db, subscription)
1606
1607
  return c.json(
1607
1608
  Response.validated(schema.pingWebhook.Response, {
1608
1609
  delivered: result.ok,
@@ -1653,17 +1654,17 @@ export function webhooks(options: webhooks.Options = {}) {
1653
1654
  message: 'Check the path parameters and try again.',
1654
1655
  })
1655
1656
 
1656
- const store = c.get('webhook')!.store
1657
+ const db = Db.get(c.get('db'))
1657
1658
  const owner = ownerFor(c)
1658
1659
  if (!owner) return unauthorized(c)
1659
1660
 
1660
1661
  const { deliveryId, id } = c.req.valid('param')
1661
1662
  try {
1662
1663
  // Owner-scope the subscription first so unowned ids 404 like elsewhere.
1663
- const subscription = await Webhooks.getSubscription(store, owner, id)
1664
+ const subscription = await Webhooks.getSubscription(db, owner, id)
1664
1665
  if (!subscription) return notFound(c)
1665
1666
 
1666
- const delivery = await Webhooks.getDelivery(store, id, deliveryId)
1667
+ const delivery = await Webhooks.getDelivery(db, id, deliveryId)
1667
1668
  if (!delivery)
1668
1669
  return Response.error(c, {
1669
1670
  code: 'delivery_not_found',
@@ -1673,7 +1674,7 @@ export function webhooks(options: webhooks.Options = {}) {
1673
1674
 
1674
1675
  // Replay the exact persisted envelope inline. Unlike `ping`, this is a
1675
1676
  // real event, so the outcome updates the subscription's failure state.
1676
- const result = await Webhooks.deliverAndRecord(store, subscription, delivery.envelope)
1677
+ const result = await Webhooks.deliverAndRecord(db, subscription, delivery.envelope)
1677
1678
  return c.json(
1678
1679
  Response.validated(schema.retryWebhookDelivery.Response, {
1679
1680
  delivered: result.ok,
@@ -1733,7 +1734,7 @@ export function webhooks(options: webhooks.Options = {}) {
1733
1734
  message: 'Check the request body and try again.',
1734
1735
  })
1735
1736
 
1736
- const store = c.get('webhook')!.store
1737
+ const db = Db.get(c.get('db'))
1737
1738
  const owner = ownerFor(c)
1738
1739
  if (!owner) return unauthorized(c)
1739
1740
 
@@ -1746,11 +1747,11 @@ export function webhooks(options: webhooks.Options = {}) {
1746
1747
  // persisting — otherwise a bad filter would silently widen the match
1747
1748
  // set at scan time.
1748
1749
  if (patch.filters !== undefined) {
1749
- const existing = await Webhooks.getSubscription(store, owner, id)
1750
+ const existing = await Webhooks.getSubscription(db, owner, id)
1750
1751
  if (!existing) return notFound(c)
1751
1752
  parseFilters(existing.eventType, patch.filters)
1752
1753
  }
1753
- const subscription = await Webhooks.updateSubscription(store, owner, id, patch)
1754
+ const subscription = await Webhooks.updateSubscription(db, owner, id, patch)
1754
1755
  if (!subscription) return notFound(c)
1755
1756
  return c.json(
1756
1757
  Response.validated(schema.updateWebhook.Response, toPublic(subscription)),
@@ -1796,13 +1797,13 @@ export function webhooks(options: webhooks.Options = {}) {
1796
1797
  message: 'Check the path parameters and try again.',
1797
1798
  })
1798
1799
 
1799
- const store = c.get('webhook')!.store
1800
+ const db = Db.get(c.get('db'))
1800
1801
  const owner = ownerFor(c)
1801
1802
  if (!owner) return unauthorized(c)
1802
1803
 
1803
1804
  const { id } = c.req.valid('param')
1804
1805
  try {
1805
- const deleted = await Webhooks.deleteSubscription(store, owner, id)
1806
+ const deleted = await Webhooks.deleteSubscription(db, owner, id)
1806
1807
  if (!deleted) return notFound(c)
1807
1808
  return c.json(Response.validated(schema.deleteWebhook.Response, { id }), 200)
1808
1809
  } catch (cause) {
@@ -1874,12 +1875,13 @@ type ScanPage = {
1874
1875
  export async function poll(options: poll.Options): Promise<poll.Result> {
1875
1876
  const {
1876
1877
  batchSize = 100,
1878
+ cache: cache_metadata = Store.memory(),
1877
1879
  chainId,
1878
1880
  client,
1879
1881
  confirmations = 2,
1882
+ db,
1880
1883
  eventTypes = ['token:transfer', 'transaction:included', 'log:emitted', 'block:created'],
1881
1884
  maxBlocks = 1_000,
1882
- store,
1883
1885
  tidx,
1884
1886
  } = options
1885
1887
  const now = options.now ?? (() => new Date())
@@ -1888,7 +1890,7 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
1888
1890
  // Queue producer instead and the consumer runs `deliverAndRecord`.
1889
1891
  const dispatch =
1890
1892
  options.dispatch ??
1891
- (({ envelope, subscription }) => Webhooks.deliverAndRecord(store, subscription, envelope))
1893
+ (({ envelope, subscription }) => Webhooks.deliverAndRecord(db, subscription, envelope))
1892
1894
 
1893
1895
  // Head resolution is lazy and per-source. RPC types work back from the live
1894
1896
  // RPC head; the legacy TIDX types from the indexed head (never ahead of the
@@ -1901,6 +1903,9 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
1901
1903
  (tidxHead ??= tidx ? await getIndexedHead(tidx, chainId) : await resolveRpcHead())
1902
1904
 
1903
1905
  const rpcSafeBlock = Math.max(0, (await resolveRpcHead()) - confirmations)
1906
+ // Reclaim expired rows (MPP-owned subscriptions, aged delivery logs) once
1907
+ // per tick; reads filter expiry at query time, so this is hygiene only.
1908
+ await Webhooks.pruneExpired(db, { now }).catch(() => {})
1904
1909
  // Per-tick lazy cache so multiple subscriptions of the same/related types
1905
1910
  // share fetched blocks and receipts instead of refetching the same window.
1906
1911
  const cache = createChainCache(client)
@@ -1916,19 +1921,15 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
1916
1921
  for (const eventType of eventTypes) {
1917
1922
  const legacy = eventType === 'token:transfer'
1918
1923
  const safeBlock = legacy ? Math.max(0, (await resolveTidxHead()) - confirmations) : rpcSafeBlock
1919
- const subscriptions = await Webhooks.listActive(store, chainId, eventType)
1924
+ const subscriptions = await Webhooks.listActive(db, chainId, eventType)
1920
1925
  for (const subscription of subscriptions) {
1921
1926
  result.subscriptions++
1922
- const cursor = await Webhooks.getCursor(store, subscription.id)
1927
+ const cursor = await Webhooks.getCursor(db, subscription.id)
1923
1928
  if (cursor === null) {
1924
1929
  // First time we've seen this subscription: anchor it at the head of the
1925
1930
  // confirmed window so we never backfill from genesis. Events start
1926
1931
  // flowing from the next tick onward.
1927
- await Webhooks.setCursor(
1928
- store,
1929
- subscription.id,
1930
- Cursor.encode([safeBlock, endOfBlockIndex]),
1931
- )
1932
+ await Webhooks.setCursor(db, subscription.id, Cursor.encode([safeBlock, endOfBlockIndex]))
1932
1933
  continue
1933
1934
  }
1934
1935
  const page = await scanSubscription({
@@ -1951,7 +1952,7 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
1951
1952
  // payloads) can render `amount` in token units. Non-fatal on failure.
1952
1953
  const data =
1953
1954
  eventType === 'token:transfer'
1954
- ? await withTokenMetadata(item.data, { chainId, client, store })
1955
+ ? await withTokenMetadata(item.data, { cache: cache_metadata, chainId, client })
1955
1956
  : item.data
1956
1957
  const envelope = Webhooks.buildEnvelope({
1957
1958
  blockNumber: item.cursor[0],
@@ -1976,21 +1977,16 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
1976
1977
  // matches); keyset scanners advance past the last dispatched row, or — on
1977
1978
  // a complete empty page — to the end of the confirmed window.
1978
1979
  if (failed) {
1979
- if (accepted) await Webhooks.setCursor(store, subscription.id, Cursor.encode([...accepted]))
1980
+ if (accepted) await Webhooks.setCursor(db, subscription.id, Cursor.encode([...accepted]))
1980
1981
  } else if (page.scannedThrough !== undefined)
1981
1982
  await Webhooks.setCursor(
1982
- store,
1983
+ db,
1983
1984
  subscription.id,
1984
1985
  Cursor.encode([page.scannedThrough, endOfBlockIndex]),
1985
1986
  )
1986
- else if (accepted)
1987
- await Webhooks.setCursor(store, subscription.id, Cursor.encode([...accepted]))
1987
+ else if (accepted) await Webhooks.setCursor(db, subscription.id, Cursor.encode([...accepted]))
1988
1988
  else if (!page.hasMore)
1989
- await Webhooks.setCursor(
1990
- store,
1991
- subscription.id,
1992
- Cursor.encode([safeBlock, endOfBlockIndex]),
1993
- )
1989
+ await Webhooks.setCursor(db, subscription.id, Cursor.encode([safeBlock, endOfBlockIndex]))
1994
1990
  }
1995
1991
  }
1996
1992
 
@@ -2009,12 +2005,16 @@ export declare namespace poll {
2009
2005
  type Options = {
2010
2006
  /** Max matching rows scanned per subscription per tick (default 100). */
2011
2007
  batchSize?: number | undefined
2008
+ /** Cache store for the token-metadata memoize (defaults to a fresh in-memory store; pass a persistent one to share lookups across ticks). */
2009
+ cache?: Store.Store | undefined
2012
2010
  /** Chain to poll. Callers loop over chains. */
2013
2011
  chainId: number
2014
2012
  /** viem RPC client for the chain (used by the RPC monitoring scanners). */
2015
2013
  client: Viem.getClient.ReturnType
2016
2014
  /** Reorg-safety window: only emit blocks this far behind head (default 2). */
2017
2015
  confirmations?: number | undefined
2016
+ /** Database holding subscriptions + cursors. */
2017
+ db: Db.Db
2018
2018
  /**
2019
2019
  * Delivery transport. Defaults to inline {@link Webhooks.deliverAndRecord} (self-host);
2020
2020
  * on Cloudflare pass a Queue producer so retry/backoff/DLQ are native.
@@ -2028,8 +2028,6 @@ export declare namespace poll {
2028
2028
  maxBlocks?: number | undefined
2029
2029
  /** Clock for envelope timestamps; injectable for tests. */
2030
2030
  now?: (() => Date) | undefined
2031
- /** Webhook state store (subscriptions + cursors). */
2032
- store: Store.State
2033
2031
  /** TIDX query client for the legacy `token:transfer` type. */
2034
2032
  tidx?: Tidx.Client | undefined
2035
2033
  }
@@ -2059,9 +2057,9 @@ export declare namespace poll {
2059
2057
  async function withTokenMetadata(
2060
2058
  data: unknown,
2061
2059
  deps: {
2060
+ cache: Store.Store
2062
2061
  chainId: number
2063
2062
  client: Viem.getClient.ReturnType
2064
- store: Store.State
2065
2063
  },
2066
2064
  ): Promise<unknown> {
2067
2065
  if (typeof data !== 'object' || data === null) return data
@@ -2079,7 +2077,7 @@ async function withTokenMetadata(
2079
2077
  */
2080
2078
  async function resolveTokenMetadata(
2081
2079
  address: string,
2082
- deps: { chainId: number; client: Viem.getClient.ReturnType; store: Store.State },
2080
+ deps: { cache: Store.Store; chainId: number; client: Viem.getClient.ReturnType },
2083
2081
  ): Promise<{ decimals: number; symbol: string } | undefined> {
2084
2082
  try {
2085
2083
  return await Store.memoize(
@@ -2089,7 +2087,7 @@ async function resolveTokenMetadata(
2089
2087
  },
2090
2088
  {
2091
2089
  key: `webhook:token-meta:v1:${deps.chainId}:${address}`,
2092
- store: deps.store,
2090
+ store: deps.cache,
2093
2091
  ttl: Ttl.minutes(5),
2094
2092
  },
2095
2093
  )
@@ -1,6 +1,7 @@
1
- import { Cli } from 'cadent'
2
- import * as TestApp from '../test/App.js'
3
- import * as Runtime from '../test/runtime.js'
1
+ import { Cli } from 'tapimo'
2
+
3
+ import * as TestApp from '../../test/App.js'
4
+ import * as Runtime from '../../test/runtime.js'
4
5
 
5
6
  const apiUrl = 'https://api.tempo-api.test'
6
7
 
@@ -12,7 +13,7 @@ describe('from', () => {
12
13
  const cli = Cli.from(apiUrl, { version: '1.2.3' })
13
14
  await serve(cli, ['--help'])
14
15
 
15
- expect(cli.name).toMatchInlineSnapshot(`"tempo-api"`)
16
+ expect(cli.name).toMatchInlineSnapshot(`"tapimo"`)
16
17
  expect(cli.description).toMatchInlineSnapshot(`"Tempo API CLI"`)
17
18
  } finally {
18
19
  fetch.mockRestore()
@@ -20,11 +21,11 @@ describe('from', () => {
20
21
  })
21
22
 
22
23
  test('serves the hosted Tempo API command', async () => {
23
- // Verified tokens are KV/store-backed (no bundled fallback), so seed the
24
- // store for the default chain to exercise the `--verified` listing.
25
- const fetch = mockHostedApi(
26
- TestApp.create({ verifiedTokens: { store: TestApp.verifiedStore(Runtime.get().chainId) } }),
27
- )
24
+ // Verified tokens are database-backed (no bundled fallback), so seed the
25
+ // db for the default chain to exercise the `--verified` listing.
26
+ const db = TestApp.database()
27
+ await TestApp.verifiedSeed(db, Runtime.get().chainId)
28
+ const fetch = mockHostedApi(TestApp.create({ db }))
28
29
 
29
30
  try {
30
31
  const cli = Cli.from(apiUrl)
@@ -1,22 +1,29 @@
1
1
  import { Cli as incur_Cli, Fetch, type Openapi } from 'incur'
2
2
 
3
- /** Creates an incur CLI backed by a hosted Tempo API. */
3
+ import * as Admin from './commands/admin.js'
4
+
5
+ /**
6
+ * Creates an incur CLI backed by a hosted Tempo API, plus the operator
7
+ * commands (`<cli> admin …`).
8
+ */
4
9
  export function from(url: string | URL, options: from.Options = {}) {
5
10
  const {
6
11
  description = 'Tempo API CLI',
7
- name = 'tempo-api',
12
+ name = 'tapimo',
8
13
  openapi = 'openapi.json',
9
14
  request,
10
15
  version = '0.0.0',
11
16
  } = options
12
17
 
13
- return incur_Cli.create(name, {
14
- description,
15
- fetch: Fetch.fromRequest(url, request),
16
- openapi,
17
- openapiConfig: options.openapiConfig ?? { mode: 'namespace' },
18
- version,
19
- })
18
+ return incur_Cli
19
+ .create(name, {
20
+ description,
21
+ fetch: Fetch.fromRequest(url, request),
22
+ openapi,
23
+ openapiConfig: options.openapiConfig ?? { mode: 'namespace' },
24
+ version,
25
+ })
26
+ .command(Admin.admin)
20
27
  }
21
28
 
22
29
  export declare namespace from {
package/src/cli/bin.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import * as Cli from './Cli.js'
3
+
4
+ await Cli.from('https://api.tempo.xyz').serve()
@@ -0,0 +1,71 @@
1
+ import { nanoid } from 'nanoid'
2
+
3
+ import * as TestCli from '../../../test/cli.js'
4
+
5
+ describe('admin', () => {
6
+ describe('migrate', () => {
7
+ test('behavior: applies migrations', async () => {
8
+ const { code, envelope } = await TestCli.run([
9
+ 'admin',
10
+ 'migrate',
11
+ '--db-schema',
12
+ `t_${nanoid()}`,
13
+ ])
14
+ expect(code).toBe(0)
15
+ expect(envelope).toMatchInlineSnapshot(`
16
+ {
17
+ "data": {
18
+ "migrated": true,
19
+ },
20
+ "ok": true,
21
+ }
22
+ `)
23
+ })
24
+
25
+ test('behavior: applies migrations from DATABASE_URL without a schema option', async () => {
26
+ const { code, envelope } = await TestCli.run(['admin', 'migrate'])
27
+ expect(code).toBe(0)
28
+ expect(envelope).toEqual({ data: { migrated: true }, ok: true })
29
+ })
30
+
31
+ test('behavior: is idempotent across runs', async () => {
32
+ const schema = `t_${nanoid()}`
33
+ expect((await TestCli.run(['admin', 'migrate', '--db-schema', schema])).code).toBe(0)
34
+ const { code, envelope } = await TestCli.run(['admin', 'migrate', '--db-schema', schema])
35
+ expect(code).toBe(0)
36
+ expect(envelope).toEqual({ data: { migrated: true }, ok: true })
37
+ })
38
+
39
+ test('error: a missing connection string exits 1', async () => {
40
+ const { code, envelope } = await TestCli.run(['admin', 'migrate'], {
41
+ DATABASE_URL: undefined,
42
+ })
43
+ expect(code).toBe(1)
44
+ expect(envelope).toMatchInlineSnapshot(`
45
+ {
46
+ "error": {
47
+ "code": "missing_database_url",
48
+ "message": "Set --database-url or DATABASE_URL to the Postgres connection string.",
49
+ },
50
+ "ok": false,
51
+ }
52
+ `)
53
+ })
54
+
55
+ test('error: a migration failure exits 1', async () => {
56
+ const { code, envelope } = await TestCli.run(
57
+ [
58
+ 'admin',
59
+ 'migrate',
60
+ '--database-url',
61
+ 'postgres://postgres:postgres@127.0.0.1:1/tempo-api',
62
+ '--db-schema',
63
+ `t_${nanoid()}`,
64
+ ],
65
+ { DATABASE_URL: undefined },
66
+ )
67
+ expect(code).toBe(1)
68
+ expect(envelope.error.code).toBe('migrate_failed')
69
+ })
70
+ })
71
+ })
@@ -0,0 +1,60 @@
1
+ import { Cli as incur_Cli, z } from 'incur'
2
+
3
+ import * as Db from '../../db/Db.js'
4
+
5
+ /**
6
+ * Applies pending Postgres migrations, connecting directly via
7
+ * `--database-url` or `DATABASE_URL` (never through Hyperdrive — the migrate
8
+ * advisory lock needs session semantics); `--db-schema` scopes the session.
9
+ */
10
+ const migrate = incur_Cli.create('migrate', {
11
+ description: 'Apply pending database migrations.',
12
+ env: z.object({
13
+ DATABASE_URL: z
14
+ .string()
15
+ .optional()
16
+ .describe('Postgres connection string. Used when --database-url is omitted.'),
17
+ }),
18
+ options: z.object({
19
+ databaseUrl: z
20
+ .string()
21
+ .optional()
22
+ .describe('Postgres connection string. Overrides DATABASE_URL.'),
23
+ dbSchema: z.string().optional().describe('Postgres schema to create and scope the session to.'),
24
+ }),
25
+ async run(c) {
26
+ const connectionString = c.options.databaseUrl ?? c.env.DATABASE_URL
27
+ if (!connectionString)
28
+ return c.error({
29
+ code: 'missing_database_url',
30
+ message: 'Set --database-url or DATABASE_URL to the Postgres connection string.',
31
+ })
32
+ const db = Db.postgres({
33
+ connectionString,
34
+ ...(c.options.dbSchema ? { schema: c.options.dbSchema } : {}),
35
+ })
36
+ try {
37
+ await db.migrate()
38
+ return { migrated: true }
39
+ } catch (error) {
40
+ /* v8 ignore next */
41
+ const message = error instanceof Error ? error.message : 'Migration failed.'
42
+ return c.error({
43
+ code: 'migrate_failed',
44
+ message,
45
+ })
46
+ } finally {
47
+ await db.close()
48
+ }
49
+ },
50
+ })
51
+
52
+ /**
53
+ * Operator commands that run against a deployment's infrastructure rather
54
+ * than the hosted API (database migrations, …).
55
+ */
56
+ export const admin = incur_Cli
57
+ .create('admin', {
58
+ description: 'Operator commands for a Tempo API deployment.',
59
+ })
60
+ .command(migrate)
package/src/cloudflare.ts CHANGED
@@ -52,4 +52,14 @@ export class DurableObject extends CloudflareDurableObject {
52
52
  async put(key: string, value: string, options?: Store.Store.PutOptions) {
53
53
  await this.#store.put(key, value, options)
54
54
  }
55
+
56
+ /**
57
+ * Atomically compares-and-swaps a value.
58
+ *
59
+ * The compare and write run on the object's single thread behind its input
60
+ * gate, so concurrent callers cannot interleave between them.
61
+ */
62
+ async swap(key: string, expected: null | string, next: string, options?: Store.Store.PutOptions) {
63
+ return this.#store.swap!(key, expected, next, options)
64
+ }
55
65
  }
@@ -0,0 +1,84 @@
1
+ import { nanoid } from 'nanoid'
2
+
3
+ import { postgresUrl } from '../../test/containers.js'
4
+ import * as Db from './Db.js'
5
+ import * as Organizations from './tables/organizations.js'
6
+
7
+ const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
8
+
9
+ describe('postgres', () => {
10
+ test('behavior: migrate is idempotent', async () => {
11
+ const db = create()
12
+ await db.migrate()
13
+ await db.migrate()
14
+ expect(await Organizations.get(db, 'missing')).toBeUndefined()
15
+ await db.close()
16
+ })
17
+
18
+ test('behavior: creates a Postgres db without schema scoping', async () => {
19
+ const db = Db.postgres({ connectionString: postgresUrl })
20
+ await db.close()
21
+ })
22
+
23
+ test('behavior: concurrent migrate tolerates the ledger race', async () => {
24
+ const schema = `t_${nanoid()}`
25
+ // Pre-create the schema so concurrent migrate races only on the
26
+ // `migrations` ledger, not `CREATE SCHEMA`.
27
+ const seed = Db.postgres({ connectionString: postgresUrl, schema })
28
+ await Organizations.get(seed, 'x').catch(() => {})
29
+ await seed.close()
30
+
31
+ // Several runners migrate the same fresh schema at once (mirrors parallel
32
+ // deploys). The session advisory lock serializes them; none throw.
33
+ const dbs = Array.from({ length: 5 }, () =>
34
+ Db.postgres({ connectionString: postgresUrl, schema }),
35
+ )
36
+ await Promise.all(dbs.map((db) => db.migrate()))
37
+ const created = await Organizations.create(dbs[0]!, { id: 'org_race', name: 'Race' })
38
+ expect(created.id).toBe('org_race')
39
+ await Promise.all(dbs.map((db) => db.close()))
40
+ })
41
+
42
+ test('error: rejects a modified applied migration', async () => {
43
+ const db = create()
44
+ await db.migrate()
45
+ await db.kysely
46
+ .updateTable('migrations')
47
+ .set({ checksum: '00000000' })
48
+ .where('name', '=', Db.migrations[0]!.name)
49
+ .execute()
50
+ await expect(db.migrate()).rejects.toThrow(/checksum mismatch/)
51
+ await db.close()
52
+ })
53
+
54
+ test('behavior: transaction rolls back on throw', async () => {
55
+ const db = create()
56
+ await db.migrate()
57
+ await expect(
58
+ db.transaction(async (tx) => {
59
+ await Organizations.create(tx, { id: 'org_2', name: 'Roll' })
60
+ throw new Error('boom')
61
+ }),
62
+ ).rejects.toThrow('boom')
63
+ expect(await Organizations.get(db, 'org_2')).toBeUndefined()
64
+ await db.close()
65
+ })
66
+
67
+ test('error: transaction rejects nesting', async () => {
68
+ const db = create()
69
+ await db.migrate()
70
+ await expect(db.transaction(() => db.transaction(async () => 1))).rejects.toThrow(/nesting/)
71
+ await db.close()
72
+ })
73
+
74
+ test('behavior: transaction commits and returns the value', async () => {
75
+ const db = create()
76
+ await db.migrate()
77
+ const id = await db.transaction(
78
+ async (tx) => (await Organizations.create(tx, { id: 'org_3', name: 'Commit' })).id,
79
+ )
80
+ expect(id).toBe('org_3')
81
+ expect((await Organizations.get(db, 'org_3'))?.name).toBe('Commit')
82
+ await db.close()
83
+ })
84
+ })