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
package/src/db/Db.ts ADDED
@@ -0,0 +1,383 @@
1
+ import {
2
+ CamelCasePlugin,
3
+ type Compilable,
4
+ CompiledQuery,
5
+ type Dialect,
6
+ Kysely,
7
+ PostgresDialect,
8
+ sql,
9
+ } from 'kysely'
10
+ import * as pg from 'pg'
11
+
12
+ import type * as Organizations from './tables/organizations.js'
13
+ import type * as VerifiedTokens from './tables/verifiedTokens.js'
14
+ import type * as WebhookDeliveries from './tables/webhookDeliveries.js'
15
+ import type * as WebhookSubscriptions from './tables/webhookSubscriptions.js'
16
+
17
+ /**
18
+ * Kysely schema with camelCase columns. {@link CamelCasePlugin} maps table
19
+ * records, queries, and {@link migrations} DDL to snake_case storage columns.
20
+ */
21
+ export type Database = {
22
+ migrations: {
23
+ appliedAt: string
24
+ checksum: string
25
+ name: string
26
+ /** SQL as applied; null on rows written before the column existed. */
27
+ sql: string | null
28
+ }
29
+ organizations: Organizations.Table
30
+ verified_token_lists: VerifiedTokens.ListTable
31
+ verified_tokens: VerifiedTokens.Table
32
+ webhook_deliveries: WebhookDeliveries.Table
33
+ webhook_subscriptions: WebhookSubscriptions.Table
34
+ }
35
+
36
+ /**
37
+ * A SQL-backed authoritative store: the underlying Kysely handle (table
38
+ * repository modules query through it), schema migrations, and transactions.
39
+ */
40
+ export type Db = {
41
+ /** Closes the underlying connection/pool. */
42
+ close(): Promise<void>
43
+ /**
44
+ * Underlying Kysely handle. Transaction-scoped {@link Db} values carry the
45
+ * transaction handle, so repositories called with `tx` run inside it. */
46
+ kysely: Kysely<Database>
47
+ /** Applies any pending schema migrations; safe to call repeatedly and concurrently. */
48
+ migrate(): Promise<void>
49
+ /**
50
+ * Runs `fn` in a transaction — commits on return, rolls back on throw. `fn`
51
+ * receives a transaction-scoped {@link Db}; pass it (not the outer `Db`) to
52
+ * repositories so every query inside runs in the transaction.
53
+ */
54
+ transaction<value>(fn: (tx: Db) => Promise<value>): Promise<value>
55
+ }
56
+
57
+ /**
58
+ * A {@link Db}, or a factory for per-request Workers/Hyperdrive connections.
59
+ * Node can pass a long-lived singleton. Resolve at the leaf with {@link get}.
60
+ */
61
+ export type Source = Db | (() => Db)
62
+
63
+ /**
64
+ * Resolves a {@link Source} to a {@link Db}. Call at the request leaf so
65
+ * factories create per-request instances; singletons resolve to themselves.
66
+ *
67
+ * @param source - The database or factory.
68
+ * @returns The resolved {@link Db}.
69
+ */
70
+ export function get(source: Source): Db {
71
+ return typeof source === 'function' ? source() : source
72
+ }
73
+
74
+ /**
75
+ * One schema migration. `up` builds a single idempotent Kysely DDL statement;
76
+ * {@link Db.migrate} compiles it for the checksum, then applies it.
77
+ */
78
+ type Migration = {
79
+ /** Unique, ordered migration name (e.g. `0001_organizations`). */
80
+ name: string
81
+ /**
82
+ * Builds the migration's DDL against `db`. Use `.ifNotExists()` so the
83
+ * statement is safe to re-run without a wrapping transaction. */
84
+ up: (db: Kysely<Database>) => Compilable & { execute(): Promise<unknown> }
85
+ }
86
+
87
+ /**
88
+ * Ordered schema migrations for {@link Db.migrate}. Each `up` is one
89
+ * idempotent statement; compiled-SQL FNV-1a checksums reject edited applied
90
+ * migrations.
91
+ */
92
+ export const migrations: readonly Migration[] = [
93
+ {
94
+ name: '0001_organizations',
95
+ up: (db) =>
96
+ db.schema
97
+ .createTable('organizations')
98
+ .ifNotExists()
99
+ .addColumn('id', 'text', (c) => c.primaryKey())
100
+ .addColumn('name', 'text', (c) => c.notNull())
101
+ .addColumn('createdBy', 'text')
102
+ .addColumn('createdAt', 'text', (c) => c.notNull())
103
+ .addColumn('updatedAt', 'text', (c) => c.notNull()),
104
+ },
105
+ {
106
+ name: '0002_webhook_subscriptions',
107
+ up: (db) =>
108
+ db.schema
109
+ .createTable('webhookSubscriptions')
110
+ .ifNotExists()
111
+ .addColumn('id', 'text', (c) => c.primaryKey())
112
+ .addColumn('ownerType', 'text', (c) => c.notNull())
113
+ .addColumn('ownerId', 'text', (c) => c.notNull())
114
+ .addColumn('chainId', 'integer', (c) => c.notNull())
115
+ .addColumn('eventType', 'text', (c) => c.notNull())
116
+ .addColumn('status', 'text', (c) => c.notNull().defaultTo('active'))
117
+ .addColumn('destination', 'jsonb', (c) => c.notNull())
118
+ .addColumn('filters', 'jsonb', (c) => c.notNull().defaultTo(sql`'{}'::jsonb`))
119
+ .addColumn('context', 'jsonb')
120
+ .addColumn('secret', 'text', (c) => c.notNull())
121
+ .addColumn('failureCount', 'integer', (c) => c.notNull().defaultTo(0))
122
+ .addColumn('lastDeliveryAt', 'text')
123
+ .addColumn('pollerCursor', 'text')
124
+ .addColumn('expiresAt', 'text')
125
+ .addColumn('createdAt', 'text', (c) => c.notNull())
126
+ .addColumn('updatedAt', 'text', (c) => c.notNull()),
127
+ },
128
+ {
129
+ name: '0003_webhook_deliveries',
130
+ up: (db) =>
131
+ db.schema
132
+ .createTable('webhookDeliveries')
133
+ .ifNotExists()
134
+ .addColumn('id', 'text', (c) => c.primaryKey())
135
+ .addColumn('subscriptionId', 'text', (c) =>
136
+ c.notNull().references('webhookSubscriptions.id').onDelete('cascade'),
137
+ )
138
+ .addColumn('eventId', 'text', (c) => c.notNull())
139
+ .addColumn('attempt', 'integer', (c) => c.notNull())
140
+ .addColumn('status', 'text', (c) => c.notNull())
141
+ .addColumn('requestUrl', 'text', (c) => c.notNull())
142
+ .addColumn('responseStatus', 'integer')
143
+ .addColumn('responseMs', 'integer')
144
+ .addColumn('error', 'text')
145
+ .addColumn('envelope', 'jsonb', (c) => c.notNull())
146
+ .addColumn('createdAt', 'text', (c) => c.notNull())
147
+ .addColumn('expiresAt', 'text', (c) => c.notNull()),
148
+ },
149
+ {
150
+ name: '0004_verified_token_lists',
151
+ up: (db) =>
152
+ db.schema
153
+ .createTable('verifiedTokenLists')
154
+ .ifNotExists()
155
+ .addColumn('chainId', 'integer', (c) => c.primaryKey())
156
+ .addColumn('version', 'text', (c) => c.notNull())
157
+ .addColumn('updatedAt', 'text', (c) => c.notNull()),
158
+ },
159
+ {
160
+ name: '0005_verified_tokens',
161
+ up: (db) =>
162
+ db.schema
163
+ .createTable('verifiedTokens')
164
+ .ifNotExists()
165
+ .addColumn('chainId', 'integer', (c) => c.notNull())
166
+ .addColumn('address', 'text', (c) => c.notNull())
167
+ .addColumn('symbol', 'text', (c) => c.notNull())
168
+ .addColumn('name', 'text', (c) => c.notNull())
169
+ .addColumn('currency', 'text', (c) => c.notNull())
170
+ .addColumn('decimals', 'integer', (c) => c.notNull())
171
+ .addColumn('logoUri', 'text')
172
+ .addColumn('position', 'integer', (c) => c.notNull())
173
+ .addPrimaryKeyConstraint('verified_tokens_pkey', ['chainId', 'address']),
174
+ },
175
+ {
176
+ // Case-insensitive symbol uniqueness per chain (`GET /tokens/:symbol`
177
+ // would otherwise be ambiguous).
178
+ name: '0006_verified_tokens_symbol_index',
179
+ up: (db) =>
180
+ db.schema
181
+ .createIndex('verified_tokens_symbol_idx')
182
+ .ifNotExists()
183
+ .unique()
184
+ .on('verifiedTokens')
185
+ .expression(sql`chain_id, lower(symbol)`),
186
+ },
187
+ {
188
+ // Owner list/count/cap scans and `id <` keyset paging.
189
+ name: '0007_webhook_subscriptions_owner_index',
190
+ up: (db) =>
191
+ db.schema
192
+ .createIndex('webhook_subscriptions_owner_idx')
193
+ .ifNotExists()
194
+ .on('webhookSubscriptions')
195
+ .columns(['ownerType', 'ownerId', 'id']),
196
+ },
197
+ {
198
+ // Partial index over active subscriptions: the poller's per-stream lookup.
199
+ // `sql.lit` keeps the predicate literal — DDL cannot carry parameters.
200
+ name: '0008_webhook_subscriptions_active_index',
201
+ up: (db) =>
202
+ db.schema
203
+ .createIndex('webhook_subscriptions_active_idx')
204
+ .ifNotExists()
205
+ .on('webhookSubscriptions')
206
+ .columns(['chainId', 'eventType'])
207
+ .where(sql.ref('status'), '=', sql.lit('active')),
208
+ },
209
+ {
210
+ name: '0009_webhook_subscriptions_expires_index',
211
+ up: (db) =>
212
+ db.schema
213
+ .createIndex('webhook_subscriptions_expires_idx')
214
+ .ifNotExists()
215
+ .on('webhookSubscriptions')
216
+ .column('expiresAt')
217
+ .where('expiresAt', 'is not', null),
218
+ },
219
+ {
220
+ // Per-subscription delivery-log paging.
221
+ name: '0010_webhook_deliveries_subscription_index',
222
+ up: (db) =>
223
+ db.schema
224
+ .createIndex('webhook_deliveries_subscription_idx')
225
+ .ifNotExists()
226
+ .on('webhookDeliveries')
227
+ .columns(['subscriptionId', 'id']),
228
+ },
229
+ {
230
+ name: '0011_webhook_deliveries_expires_index',
231
+ up: (db) =>
232
+ db.schema
233
+ .createIndex('webhook_deliveries_expires_idx')
234
+ .ifNotExists()
235
+ .on('webhookDeliveries')
236
+ .column('expiresAt'),
237
+ },
238
+ ]
239
+
240
+ /**
241
+ * Builds a {@link Db} for any Kysely {@link Dialect}. All backends share
242
+ * migrations, repositories, and the required {@link CamelCasePlugin} mapping.
243
+ *
244
+ * @param dialect - The Kysely dialect for the target backend.
245
+ * @returns The database.
246
+ */
247
+ export function from(dialect: Dialect): Db {
248
+ // Builds Db over a top-level Kysely handle or transaction-scoped Kysely handle.
249
+ function build(kysely: Kysely<Database>): Db {
250
+ // Prevents same-handle nested transactions from deadlocking capped pools.
251
+ // Nested work must use the callback's scoped `tx`.
252
+ let active = false
253
+ return {
254
+ async close() {
255
+ await kysely.destroy()
256
+ },
257
+ kysely,
258
+ async migrate() {
259
+ // A session advisory lock serializes the full run, including bootstrap
260
+ // DDL and ledger inserts. Pin one connection and unlock in `finally`.
261
+ await kysely.connection().execute(async (db) => {
262
+ await sql`SELECT pg_advisory_lock(${migrateLockKey})`.execute(db)
263
+ try {
264
+ await sql`CREATE TABLE IF NOT EXISTS migrations (name TEXT PRIMARY KEY, checksum TEXT NOT NULL, applied_at TEXT NOT NULL)`.execute(
265
+ db,
266
+ )
267
+ await sql`ALTER TABLE migrations ADD COLUMN IF NOT EXISTS sql TEXT`.execute(db)
268
+ const applied = new Map(
269
+ (await db.selectFrom('migrations').select(['name', 'checksum', 'sql']).execute()).map(
270
+ (row) => [row.name, row] as const,
271
+ ),
272
+ )
273
+ for (const migration of migrations) {
274
+ // Build once: compile (no I/O) for the checksum, execute to apply.
275
+ const statement = migration.up(db)
276
+ const compiled = statement.compile().sql
277
+ const digest = checksum(compiled)
278
+ const existing = applied.get(migration.name)
279
+ if (existing !== undefined) {
280
+ // The digest hashes kysely's rendered SQL, so a kysely upgrade
281
+ // can flip it with unchanged sources. The stored `sql` is what
282
+ // actually ran — diff it against the new render to tell render
283
+ // drift from a real edit (re-baseline procedure in AGENTS.md).
284
+ if (existing.checksum !== digest)
285
+ throw new Error(
286
+ `Migration "${migration.name}" was modified after being applied (checksum mismatch). Diff the migrations table's stored sql against the current render before re-baselining.`,
287
+ )
288
+ // Backfill rows from before the column existed; the matching
289
+ // checksum proves the current render is what was applied.
290
+ if (existing.sql === null)
291
+ await db
292
+ .updateTable('migrations')
293
+ .set({ sql: compiled })
294
+ .where('name', '=', migration.name)
295
+ .execute()
296
+ continue
297
+ }
298
+ await statement.execute()
299
+ await db
300
+ .insertInto('migrations')
301
+ .values({
302
+ appliedAt: new Date().toISOString(),
303
+ checksum: digest,
304
+ name: migration.name,
305
+ sql: compiled,
306
+ })
307
+ .execute()
308
+ }
309
+ } finally {
310
+ await sql`SELECT pg_advisory_unlock(${migrateLockKey})`.execute(db)
311
+ }
312
+ })
313
+ },
314
+ async transaction(fn) {
315
+ if (active) throw new Error('Db.transaction does not support nesting.')
316
+ active = true
317
+ try {
318
+ return await kysely.transaction().execute((trx) => fn(build(trx)))
319
+ } finally {
320
+ active = false
321
+ }
322
+ },
323
+ }
324
+ }
325
+ return build(new Kysely<Database>({ dialect, plugins: [new CamelCasePlugin()] }))
326
+ }
327
+
328
+ /**
329
+ * Creates a Postgres-backed {@link Db} via Kysely's `PostgresDialect`. Optional
330
+ * `schema` scopes every pooled connection and caps the pool for test isolation.
331
+ *
332
+ * @param options - Postgres options.
333
+ * @returns The database.
334
+ */
335
+ export function postgres(options: postgres.Options): Db {
336
+ const { connectionString, schema } = options
337
+ const pool = new pg.Pool(schema ? { connectionString, max: 1 } : { connectionString })
338
+ return from(
339
+ new PostgresDialect({
340
+ pool,
341
+ ...(schema
342
+ ? {
343
+ onCreateConnection: async (connection) => {
344
+ await connection.executeQuery(
345
+ CompiledQuery.raw(`CREATE SCHEMA IF NOT EXISTS "${schema}"`),
346
+ )
347
+ await connection.executeQuery(CompiledQuery.raw(`SET search_path TO "${schema}"`))
348
+ },
349
+ }
350
+ : {}),
351
+ }),
352
+ )
353
+ }
354
+
355
+ export declare namespace postgres {
356
+ /** Options for {@link postgres}. */
357
+ type Options = {
358
+ /** Postgres connection string (`postgresql://…`). */
359
+ connectionString: string
360
+ /** Schema to create and scope the session to (isolates concurrent tests). */
361
+ schema?: string | undefined
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Fixed key for the {@link Db.migrate} Postgres session advisory lock: the
367
+ * ASCII bytes of `"tapi"`, making all migrators serialize together.
368
+ */
369
+ const migrateLockKey = 0x74617069
370
+
371
+ /**
372
+ * Deterministic FNV-1a hash of migration SQL, so an applied migration whose
373
+ * source later changes is rejected (instead of silently diverging). Not
374
+ * cryptographic — only needs collision resistance for typos.
375
+ */
376
+ function checksum(sql: string): string {
377
+ let hash = 0x811c9dc5
378
+ for (let i = 0; i < sql.length; i++) {
379
+ hash ^= sql.charCodeAt(i)
380
+ hash = Math.imul(hash, 0x01000193)
381
+ }
382
+ return (hash >>> 0).toString(16).padStart(8, '0')
383
+ }
@@ -0,0 +1,164 @@
1
+ import * as z from 'zod/mini'
2
+
3
+ import type * as Webhooks from '../internal/Webhooks.js'
4
+
5
+ // Row schemas for database tables (`zod/mini`), the source of truth each
6
+ // `tables/` module derives its `Table` type from. Nullable fields map to
7
+ // nullable columns; the domain layer converts `null` back to absent fields.
8
+ // Domain-owned unions and jsonb values stay typed by their owning module via
9
+ // `z.custom` (type-only imports) so the two cannot drift.
10
+
11
+ /** An organization, the top-level tenant that owns API keys. */
12
+ export const Organization = z.object({
13
+ createdAt: z.string().check(z.describe('When the organization was created (ISO 8601).')),
14
+ createdBy: z
15
+ .nullable(z.string())
16
+ .check(z.describe('Identity that created the organization (e.g. admin email), or null.')),
17
+ id: z
18
+ .string()
19
+ .check(
20
+ z.describe('Opaque organization id (`org_…`).'),
21
+ z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
22
+ ),
23
+ name: z
24
+ .string()
25
+ .check(z.describe('Human-readable organization name.'), z.meta({ examples: ['Acme, Inc.'] })),
26
+ updatedAt: z.string().check(z.describe('When the organization was last updated (ISO 8601).')),
27
+ })
28
+
29
+ /** Columns of the `organizations` table. */
30
+ export type Organization = z.output<typeof Organization>
31
+
32
+ /**
33
+ * A verified token: one row per curated token, ordered by `position` (list
34
+ * order is canonical and surfaced to clients). Addresses are stored lowercase;
35
+ * case-insensitive symbol uniqueness is enforced by a functional index on
36
+ * `lower(symbol)`.
37
+ */
38
+ export const VerifiedToken = z.object({
39
+ address: z.string().check(z.describe('Verified TIP-20 token contract address (lowercase).')),
40
+ chainId: z.number().check(z.int(), z.describe('Chain the token belongs to.')),
41
+ currency: z.string().check(z.describe('Display currency, e.g. `USD`.')),
42
+ decimals: z.number().check(z.int(), z.describe('Decimal precision.')),
43
+ logoUri: z.nullable(z.string()).check(z.describe('Curated HTTPS logo URL, or null.')),
44
+ name: z.string().check(z.describe('Display name.')),
45
+ position: z
46
+ .number()
47
+ .check(z.int(), z.describe("Zero-based position in the chain's canonical list order.")),
48
+ symbol: z.string().check(z.describe('Ticker symbol.'), z.meta({ examples: ['USDC'] })),
49
+ })
50
+
51
+ /** Columns of the `verified_tokens` table. */
52
+ export type VerifiedToken = z.output<typeof VerifiedToken>
53
+
54
+ /**
55
+ * A chain's verified-token list metadata — the opaque `version` that drives
56
+ * the soft-refresh cache and `If-Match` preconditions, bumped on every publish.
57
+ */
58
+ export const VerifiedTokenList = z.object({
59
+ chainId: z.number().check(z.int(), z.describe('Chain the list belongs to.')),
60
+ updatedAt: z.string().check(z.describe('ISO timestamp of the last publish.')),
61
+ version: z
62
+ .string()
63
+ .check(z.describe('Opaque list version (`<epoch-ms>_<hex>`); changes on every publish.')),
64
+ })
65
+
66
+ /** Columns of the `verified_token_lists` table. */
67
+ export type VerifiedTokenList = z.output<typeof VerifiedTokenList>
68
+
69
+ /**
70
+ * A webhook subscription. The `Webhooks.Owner` union maps to (`ownerType`,
71
+ * `ownerId`); jsonb object columns round-trip as parsed values. `pollerCursor`
72
+ * replaces the old per-subscription cursor key.
73
+ */
74
+ export const WebhookSubscription = z.object({
75
+ chainId: z.number().check(z.int(), z.describe('Chain the subscription listens on.')),
76
+ context: z
77
+ .nullable(z.custom<Webhooks.Context>())
78
+ .check(z.describe('Human context (`jsonb`), or null.')),
79
+ createdAt: z.string().check(z.describe('ISO timestamp of creation.')),
80
+ destination: z
81
+ .custom<Webhooks.Destination>()
82
+ .check(z.describe('Delivery destination (`jsonb`).')),
83
+ eventType: z
84
+ .custom<Webhooks.EventType>()
85
+ .check(z.describe('Event type the subscription listens to.')),
86
+ expiresAt: z
87
+ .nullable(z.string())
88
+ .check(z.describe('ISO expiry timestamp (MPP-owned subscriptions), or null.')),
89
+ failureCount: z
90
+ .number()
91
+ .check(z.int(), z.describe('Consecutive delivery failures; drives auto-disable.')),
92
+ filters: z
93
+ .custom<Webhooks.Subscription['filters']>()
94
+ .check(z.describe('Event-type-specific filter predicates (`jsonb`).')),
95
+ id: z
96
+ .string()
97
+ .check(
98
+ z.describe('Subscription id (`wh_…`, lexically time-ordered).'),
99
+ z.meta({ examples: ['wh_0000019c1a2b3c4d5e6f7g8h'] }),
100
+ ),
101
+ lastDeliveryAt: z
102
+ .nullable(z.string())
103
+ .check(z.describe('ISO timestamp of the last successful delivery, or null.')),
104
+ ownerId: z
105
+ .string()
106
+ .check(z.describe('Owner identifier: `orgId` for `api_key` owners, `payer` for `mpp`.')),
107
+ ownerType: z.custom<Webhooks.Owner['type']>().check(z.describe('Owner kind.')),
108
+ pollerCursor: z
109
+ .nullable(z.string())
110
+ .check(z.describe('Poller keyset cursor, or null before the first tick.')),
111
+ secret: z.string().check(z.describe('HMAC signing secret.')),
112
+ status: z.custom<Webhooks.Status>().check(z.describe('Lifecycle status.')),
113
+ updatedAt: z
114
+ .string()
115
+ .check(z.describe('ISO timestamp of the last mutation (`pollerCursor` writes excluded).')),
116
+ })
117
+
118
+ /** Columns of the `webhook_subscriptions` table. */
119
+ export type WebhookSubscription = z.output<typeof WebhookSubscription>
120
+
121
+ /**
122
+ * A webhook delivery attempt. Rows are invisible past `expiresAt` (the
123
+ * retention deadline that replaces the old store TTL) and pruned by the
124
+ * poller; deleting a subscription cascades to its rows.
125
+ */
126
+ export const WebhookDelivery = z.object({
127
+ attempt: z
128
+ .number()
129
+ .check(
130
+ z.int(),
131
+ z.describe('Consecutive attempt number (1-based) at the time of the delivery.'),
132
+ ),
133
+ createdAt: z.string().check(z.describe('ISO timestamp the attempt was recorded.')),
134
+ envelope: z
135
+ .custom<Webhooks.Envelope>()
136
+ .check(z.describe('The exact envelope that was sent (`jsonb`), for verbatim replay.')),
137
+ error: z
138
+ .nullable(z.string())
139
+ .check(z.describe('Failure reason when `status` is `failed`, or null.')),
140
+ eventId: z.string().check(z.describe('Idempotent event id the delivery carried (`evt_…`).')),
141
+ expiresAt: z
142
+ .string()
143
+ .check(z.describe('ISO retention deadline; rows past it are invisible and pruned.')),
144
+ id: z
145
+ .string()
146
+ .check(
147
+ z.describe('Delivery id (`whd_…`, lexically time-ordered).'),
148
+ z.meta({ examples: ['whd_0000019c1a2b3c4d5e6f7g8h'] }),
149
+ ),
150
+ requestUrl: z.string().check(z.describe('Subscriber callback URL the attempt targeted.')),
151
+ responseMs: z
152
+ .nullable(z.number())
153
+ .check(z.describe('Wall-clock duration of the attempt in ms, or null.')),
154
+ responseStatus: z
155
+ .nullable(z.number())
156
+ .check(z.describe('HTTP response status, or null when no response was received.')),
157
+ status: z.custom<Webhooks.DeliveryStatus>().check(z.describe('Outcome status.')),
158
+ subscriptionId: z
159
+ .string()
160
+ .check(z.describe('Owning subscription id (`wh_…`); FK with `ON DELETE CASCADE`.')),
161
+ })
162
+
163
+ /** Columns of the `webhook_deliveries` table. */
164
+ export type WebhookDelivery = z.output<typeof WebhookDelivery>
@@ -0,0 +1,124 @@
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 './organizations.js'
6
+
7
+ const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
8
+
9
+ /** Redacts nondeterministic row fields (generated nanoid ids, timestamps). */
10
+ function redact(record: Organizations.Record) {
11
+ return {
12
+ ...record,
13
+ createdAt: '<timestamp>',
14
+ id: /^org_[\w-]{21}$/.test(record.id) ? 'org_<id>' : record.id,
15
+ updatedAt: '<timestamp>',
16
+ }
17
+ }
18
+
19
+ describe('create', () => {
20
+ test('behavior: inserts with a generated id and defaults', async () => {
21
+ const db = create()
22
+ await db.migrate()
23
+ const record = await Organizations.create(db, { name: 'Acme, Inc.' })
24
+ expect(record.id).toMatch(/^org_/)
25
+ expect(redact(record)).toMatchInlineSnapshot(`
26
+ {
27
+ "createdAt": "<timestamp>",
28
+ "createdBy": null,
29
+ "id": "org_<id>",
30
+ "name": "Acme, Inc.",
31
+ "updatedAt": "<timestamp>",
32
+ }
33
+ `)
34
+ await db.close()
35
+ })
36
+
37
+ test('behavior: preserves an explicit id and createdBy', async () => {
38
+ const db = create()
39
+ await db.migrate()
40
+ const record = await Organizations.create(db, {
41
+ createdBy: 'admin@tempo.xyz',
42
+ id: 'org_explicit',
43
+ name: 'Explicit',
44
+ })
45
+ expect(redact(record)).toMatchInlineSnapshot(`
46
+ {
47
+ "createdAt": "<timestamp>",
48
+ "createdBy": "admin@tempo.xyz",
49
+ "id": "org_explicit",
50
+ "name": "Explicit",
51
+ "updatedAt": "<timestamp>",
52
+ }
53
+ `)
54
+ await db.close()
55
+ })
56
+ })
57
+
58
+ describe('get', () => {
59
+ test('behavior: returns undefined when absent', async () => {
60
+ const db = create()
61
+ await db.migrate()
62
+ expect(await Organizations.get(db, 'org_missing')).toBeUndefined()
63
+ await db.close()
64
+ })
65
+ })
66
+
67
+ describe('list', () => {
68
+ test('behavior: lists newest first', async () => {
69
+ const db = create()
70
+ await db.migrate()
71
+ await Organizations.create(db, { id: 'org_a', name: 'A' })
72
+ // Distinct createdAt for a deterministic order.
73
+ await new Promise((resolve) => setTimeout(resolve, 5))
74
+ await Organizations.create(db, { id: 'org_b', name: 'B' })
75
+ expect((await Organizations.list(db)).map((record) => record.id)).toMatchInlineSnapshot(`
76
+ [
77
+ "org_b",
78
+ "org_a",
79
+ ]
80
+ `)
81
+ await db.close()
82
+ })
83
+ })
84
+
85
+ describe('update', () => {
86
+ test('behavior: renames and bumps updatedAt', async () => {
87
+ const db = create()
88
+ await db.migrate()
89
+ const created = await Organizations.create(db, { id: 'org_1', name: 'Before' })
90
+ await new Promise((resolve) => setTimeout(resolve, 5))
91
+ const updated = await Organizations.update(db, 'org_1', { name: 'After' })
92
+ expect(updated?.name).toMatchInlineSnapshot(`"After"`)
93
+ expect(updated && updated.updatedAt > created.updatedAt).toBe(true)
94
+ await db.close()
95
+ })
96
+
97
+ test('behavior: returns undefined when absent', async () => {
98
+ const db = create()
99
+ await db.migrate()
100
+ expect(await Organizations.update(db, 'org_missing', { name: 'X' })).toBeUndefined()
101
+ await db.close()
102
+ })
103
+ })
104
+
105
+ describe('upsert', () => {
106
+ test('behavior: inserts when absent, defaulting name to the id', async () => {
107
+ const db = create()
108
+ await db.migrate()
109
+ await Organizations.upsert(db, { id: 'key_fallback' })
110
+ expect((await Organizations.get(db, 'key_fallback'))?.name).toMatchInlineSnapshot(
111
+ `"key_fallback"`,
112
+ )
113
+ await db.close()
114
+ })
115
+
116
+ test('behavior: leaves an existing row untouched', async () => {
117
+ const db = create()
118
+ await db.migrate()
119
+ await Organizations.create(db, { id: 'org_1', name: 'Original' })
120
+ await Organizations.upsert(db, { id: 'org_1', name: 'Clobbered' })
121
+ expect((await Organizations.get(db, 'org_1'))?.name).toMatchInlineSnapshot(`"Original"`)
122
+ await db.close()
123
+ })
124
+ })