tapimo 0.2.0 → 0.3.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 (313) hide show
  1. package/README.md +31 -3
  2. package/dist/ApiKey.d.ts +1 -0
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +5 -2
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +28 -1
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +34 -1
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +7167 -4507
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +8 -0
  13. package/dist/App.js.map +1 -1
  14. package/dist/Scope.d.ts +11 -2
  15. package/dist/Scope.d.ts.map +1 -1
  16. package/dist/Scope.js +14 -3
  17. package/dist/Scope.js.map +1 -1
  18. package/dist/admin/App.d.ts +128 -118
  19. package/dist/admin/App.d.ts.map +1 -1
  20. package/dist/admin/apps/api-keys.d.ts +7 -0
  21. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  22. package/dist/admin/apps/scopes.d.ts +8 -0
  23. package/dist/admin/apps/scopes.d.ts.map +1 -1
  24. package/dist/admin/apps/scopes.js +3 -0
  25. package/dist/admin/apps/scopes.js.map +1 -1
  26. package/dist/admin/apps/verified-tokens.d.ts +57 -57
  27. package/dist/admin/ui.generated.d.ts +1 -1
  28. package/dist/admin/ui.generated.js +13 -13
  29. package/dist/admin/ui.generated.js.map +1 -1
  30. package/dist/analytics/Analytics.d.ts +151 -0
  31. package/dist/analytics/Analytics.d.ts.map +1 -0
  32. package/dist/analytics/Analytics.js +212 -0
  33. package/dist/analytics/Analytics.js.map +1 -0
  34. package/dist/analytics/tables/requestEvents.d.ts +215 -0
  35. package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
  36. package/dist/analytics/tables/requestEvents.js +264 -0
  37. package/dist/analytics/tables/requestEvents.js.map +1 -0
  38. package/dist/apps/data/App.d.ts +1024 -624
  39. package/dist/apps/data/App.d.ts.map +1 -1
  40. package/dist/apps/data/App.js +36 -0
  41. package/dist/apps/data/App.js.map +1 -1
  42. package/dist/apps/data/routes/balances.d.ts +45 -45
  43. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  44. package/dist/apps/data/routes/exchanges.d.ts +455 -455
  45. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  46. package/dist/apps/data/routes/funding.d.ts +765 -0
  47. package/dist/apps/data/routes/funding.d.ts.map +1 -0
  48. package/dist/apps/data/routes/funding.js +383 -0
  49. package/dist/apps/data/routes/funding.js.map +1 -0
  50. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  51. package/dist/apps/data/routes/receipts.js +2 -8
  52. package/dist/apps/data/routes/receipts.js.map +1 -1
  53. package/dist/apps/data/routes/rpc.d.ts +29 -36
  54. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  55. package/dist/apps/data/routes/rpc.js +29 -1
  56. package/dist/apps/data/routes/rpc.js.map +1 -1
  57. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  58. package/dist/apps/data/routes/transfers.d.ts +56 -56
  59. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  60. package/dist/apps/data/routes/webhooks.d.ts +45 -45
  61. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  62. package/dist/apps/data/routes/webhooks.js +8 -8
  63. package/dist/apps/data/routes/webhooks.js.map +1 -1
  64. package/dist/apps/docs/App.d.ts +4 -2
  65. package/dist/apps/docs/App.d.ts.map +1 -1
  66. package/dist/apps/docs/App.js +27 -95
  67. package/dist/apps/docs/App.js.map +1 -1
  68. package/dist/apps/docs/reference.d.ts +21 -0
  69. package/dist/apps/docs/reference.d.ts.map +1 -0
  70. package/dist/apps/docs/reference.js +112 -0
  71. package/dist/apps/docs/reference.js.map +1 -0
  72. package/dist/apps/index.d.ts +1 -0
  73. package/dist/apps/index.d.ts.map +1 -1
  74. package/dist/apps/index.js +1 -0
  75. package/dist/apps/index.js.map +1 -1
  76. package/dist/apps/mcp.d.ts +76 -0
  77. package/dist/apps/mcp.d.ts.map +1 -0
  78. package/dist/apps/mcp.js +188 -0
  79. package/dist/apps/mcp.js.map +1 -0
  80. package/dist/apps/platform/App.d.ts +3005 -728
  81. package/dist/apps/platform/App.d.ts.map +1 -1
  82. package/dist/apps/platform/App.js +13 -3
  83. package/dist/apps/platform/App.js.map +1 -1
  84. package/dist/apps/platform/Billing.d.ts +279 -0
  85. package/dist/apps/platform/Billing.d.ts.map +1 -0
  86. package/dist/apps/platform/Billing.js +477 -0
  87. package/dist/apps/platform/Billing.js.map +1 -0
  88. package/dist/apps/platform/index.d.ts +1 -0
  89. package/dist/apps/platform/index.d.ts.map +1 -1
  90. package/dist/apps/platform/index.js +1 -0
  91. package/dist/apps/platform/index.js.map +1 -1
  92. package/dist/apps/platform/routes/api-keys.d.ts +341 -49
  93. package/dist/apps/platform/routes/api-keys.d.ts.map +1 -1
  94. package/dist/apps/platform/routes/api-keys.js +115 -13
  95. package/dist/apps/platform/routes/api-keys.js.map +1 -1
  96. package/dist/apps/platform/routes/billing.d.ts +1520 -0
  97. package/dist/apps/platform/routes/billing.d.ts.map +1 -0
  98. package/dist/apps/platform/routes/billing.js +795 -0
  99. package/dist/apps/platform/routes/billing.js.map +1 -0
  100. package/dist/apps/platform/routes/invitations.d.ts +201 -83
  101. package/dist/apps/platform/routes/invitations.d.ts.map +1 -1
  102. package/dist/apps/platform/routes/invitations.js +58 -0
  103. package/dist/apps/platform/routes/invitations.js.map +1 -1
  104. package/dist/apps/platform/routes/me.d.ts +17 -17
  105. package/dist/apps/platform/routes/orgs.d.ts +50 -36
  106. package/dist/apps/platform/routes/orgs.d.ts.map +1 -1
  107. package/dist/apps/platform/routes/orgs.js +14 -3
  108. package/dist/apps/platform/routes/orgs.js.map +1 -1
  109. package/dist/apps/platform/routes/projects.d.ts +97 -97
  110. package/dist/apps/platform/routes/scopes.d.ts +107 -0
  111. package/dist/apps/platform/routes/scopes.d.ts.map +1 -0
  112. package/dist/apps/platform/routes/scopes.js +66 -0
  113. package/dist/apps/platform/routes/scopes.js.map +1 -0
  114. package/dist/apps/platform/routes/usage.d.ts +588 -0
  115. package/dist/apps/platform/routes/usage.d.ts.map +1 -0
  116. package/dist/apps/platform/routes/usage.js +391 -0
  117. package/dist/apps/platform/routes/usage.js.map +1 -0
  118. package/dist/apps/relay/App.d.ts +4 -1
  119. package/dist/apps/relay/App.d.ts.map +1 -1
  120. package/dist/apps/relay/App.js +118 -10
  121. package/dist/apps/relay/App.js.map +1 -1
  122. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  123. package/dist/apps/relay/Sponsorships.js +2 -1
  124. package/dist/apps/relay/Sponsorships.js.map +1 -1
  125. package/dist/cli/Cli.d.ts +8 -1
  126. package/dist/cli/Cli.d.ts.map +1 -1
  127. package/dist/cli/Cli.js +11 -3
  128. package/dist/cli/Cli.js.map +1 -1
  129. package/dist/cli/commands/admin.d.ts +4 -0
  130. package/dist/cli/commands/admin.d.ts.map +1 -1
  131. package/dist/cli/commands/admin.js +80 -1
  132. package/dist/cli/commands/admin.js.map +1 -1
  133. package/dist/db/Db.d.ts +7 -0
  134. package/dist/db/Db.d.ts.map +1 -1
  135. package/dist/db/Db.js +126 -0
  136. package/dist/db/Db.js.map +1 -1
  137. package/dist/db/Schema.d.ts +79 -0
  138. package/dist/db/Schema.d.ts.map +1 -1
  139. package/dist/db/Schema.js +88 -2
  140. package/dist/db/Schema.js.map +1 -1
  141. package/dist/db/tables/billingSettings.d.ts +55 -0
  142. package/dist/db/tables/billingSettings.d.ts.map +1 -0
  143. package/dist/db/tables/billingSettings.js +70 -0
  144. package/dist/db/tables/billingSettings.js.map +1 -0
  145. package/dist/db/tables/invitations.js +2 -2
  146. package/dist/db/tables/invitations.js.map +1 -1
  147. package/dist/db/tables/organizations.d.ts +6 -2
  148. package/dist/db/tables/organizations.d.ts.map +1 -1
  149. package/dist/db/tables/organizations.js +10 -4
  150. package/dist/db/tables/organizations.js.map +1 -1
  151. package/dist/db/tables/projects.js +2 -2
  152. package/dist/db/tables/projects.js.map +1 -1
  153. package/dist/db/tables/requestUsage.d.ts +121 -0
  154. package/dist/db/tables/requestUsage.d.ts.map +1 -0
  155. package/dist/db/tables/requestUsage.js +150 -0
  156. package/dist/db/tables/requestUsage.js.map +1 -0
  157. package/dist/db/tables/sponsoredTransactions.d.ts +119 -0
  158. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  159. package/dist/db/tables/sponsoredTransactions.js +155 -2
  160. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  161. package/dist/db/tables/stripeCustomers.d.ts +81 -0
  162. package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
  163. package/dist/db/tables/stripeCustomers.js +120 -0
  164. package/dist/db/tables/stripeCustomers.js.map +1 -0
  165. package/dist/db/tables/users.js +2 -2
  166. package/dist/db/tables/users.js.map +1 -1
  167. package/dist/handlers/internal/sponsorship.d.ts +5 -2
  168. package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
  169. package/dist/handlers/internal/sponsorship.js +19 -5
  170. package/dist/handlers/internal/sponsorship.js.map +1 -1
  171. package/dist/handlers/internal/utils.d.ts +10 -3
  172. package/dist/handlers/internal/utils.d.ts.map +1 -1
  173. package/dist/handlers/internal/utils.js +14 -6
  174. package/dist/handlers/internal/utils.js.map +1 -1
  175. package/dist/index.d.ts +1 -0
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +1 -0
  178. package/dist/index.js.map +1 -1
  179. package/dist/internal/Auth.d.ts +18 -4
  180. package/dist/internal/Auth.d.ts.map +1 -1
  181. package/dist/internal/Auth.js +148 -76
  182. package/dist/internal/Auth.js.map +1 -1
  183. package/dist/internal/Cache.d.ts.map +1 -1
  184. package/dist/internal/Cache.js +4 -1
  185. package/dist/internal/Cache.js.map +1 -1
  186. package/dist/internal/EdgeCache.d.ts +10 -0
  187. package/dist/internal/EdgeCache.d.ts.map +1 -1
  188. package/dist/internal/EdgeCache.js +39 -8
  189. package/dist/internal/EdgeCache.js.map +1 -1
  190. package/dist/internal/Fees.d.ts +27 -0
  191. package/dist/internal/Fees.d.ts.map +1 -0
  192. package/dist/internal/Fees.js +48 -0
  193. package/dist/internal/Fees.js.map +1 -0
  194. package/dist/internal/FundingRoutes.d.ts +183 -0
  195. package/dist/internal/FundingRoutes.d.ts.map +1 -0
  196. package/dist/internal/FundingRoutes.js +490 -0
  197. package/dist/internal/FundingRoutes.js.map +1 -0
  198. package/dist/internal/Id.d.ts +15 -0
  199. package/dist/internal/Id.d.ts.map +1 -0
  200. package/dist/internal/Id.js +22 -0
  201. package/dist/internal/Id.js.map +1 -0
  202. package/dist/internal/Log.d.ts +2 -0
  203. package/dist/internal/Log.d.ts.map +1 -1
  204. package/dist/internal/Log.js +3 -0
  205. package/dist/internal/Log.js.map +1 -1
  206. package/dist/internal/OpenApi.d.ts +10 -1
  207. package/dist/internal/OpenApi.d.ts.map +1 -1
  208. package/dist/internal/OpenApi.js +8 -1
  209. package/dist/internal/OpenApi.js.map +1 -1
  210. package/dist/internal/RateLimit.d.ts +25 -0
  211. package/dist/internal/RateLimit.d.ts.map +1 -1
  212. package/dist/internal/RateLimit.js +51 -0
  213. package/dist/internal/RateLimit.js.map +1 -1
  214. package/dist/internal/Response.d.ts +1 -1
  215. package/dist/internal/Response.d.ts.map +1 -1
  216. package/dist/internal/Schema.d.ts +8 -0
  217. package/dist/internal/Schema.d.ts.map +1 -1
  218. package/dist/internal/Schema.js +7 -0
  219. package/dist/internal/Schema.js.map +1 -1
  220. package/dist/internal/Webhooks.d.ts.map +1 -1
  221. package/dist/internal/Webhooks.js +3 -3
  222. package/dist/internal/Webhooks.js.map +1 -1
  223. package/package.json +4 -3
  224. package/src/ApiKey.ts +9 -2
  225. package/src/ApiKeys.test.ts +51 -0
  226. package/src/ApiKeys.ts +50 -1
  227. package/src/App.test.ts +33 -86718
  228. package/src/App.ts +18 -0
  229. package/src/Client.test-d.ts +351 -0
  230. package/src/Scope.ts +16 -3
  231. package/src/admin/apps/scopes.test.ts +6 -0
  232. package/src/admin/apps/scopes.ts +3 -0
  233. package/src/admin/ui/src/components/Page.tsx +2 -3
  234. package/src/admin/ui/src/routes/__root.tsx +2 -2
  235. package/src/admin/ui/src/routes/api-keys.tsx +2 -2
  236. package/src/admin/ui.generated.ts +13 -13
  237. package/src/analytics/Analytics.test.ts +527 -0
  238. package/src/analytics/Analytics.ts +318 -0
  239. package/src/analytics/tables/requestEvents.ts +523 -0
  240. package/src/apps/data/App.test.ts +72 -0
  241. package/src/apps/data/App.ts +35 -0
  242. package/src/apps/data/routes/funding.test.ts +214 -0
  243. package/src/apps/data/routes/funding.ts +584 -0
  244. package/src/apps/data/routes/receipts.ts +2 -9
  245. package/src/apps/data/routes/rpc.test.ts +78 -2
  246. package/src/apps/data/routes/rpc.ts +31 -1
  247. package/src/apps/data/routes/webhooks.test.ts +1 -1
  248. package/src/apps/data/routes/webhooks.ts +11 -8
  249. package/src/apps/docs/App.ts +29 -100
  250. package/src/apps/docs/reference.ts +132 -0
  251. package/src/apps/index.ts +1 -0
  252. package/src/apps/mcp.test.ts +206 -0
  253. package/src/apps/mcp.ts +250 -0
  254. package/src/apps/platform/App.ts +43 -4
  255. package/src/apps/platform/Billing.test.ts +530 -0
  256. package/src/apps/platform/Billing.ts +682 -0
  257. package/src/apps/platform/index.ts +1 -0
  258. package/src/apps/platform/routes/api-keys.test.ts +172 -9
  259. package/src/apps/platform/routes/api-keys.ts +136 -20
  260. package/src/apps/platform/routes/billing.test.ts +1100 -0
  261. package/src/apps/platform/routes/billing.ts +966 -0
  262. package/src/apps/platform/routes/invitations.test.ts +55 -7
  263. package/src/apps/platform/routes/invitations.ts +68 -0
  264. package/src/apps/platform/routes/me.test.ts +230 -0
  265. package/src/apps/platform/routes/orgs.ts +26 -4
  266. package/src/apps/platform/routes/scopes.test.ts +29 -0
  267. package/src/apps/platform/routes/scopes.ts +85 -0
  268. package/src/apps/platform/routes/usage.test.ts +422 -0
  269. package/src/apps/platform/routes/usage.ts +527 -0
  270. package/src/apps/relay/App.test.ts +457 -8
  271. package/src/apps/relay/App.ts +120 -12
  272. package/src/apps/relay/Sponsorships.test.ts +1 -1
  273. package/src/apps/relay/Sponsorships.ts +2 -1
  274. package/src/cli/Cli.test.ts +25 -2
  275. package/src/cli/Cli.ts +17 -2
  276. package/src/cli/commands/admin.test.ts +83 -0
  277. package/src/cli/commands/admin.ts +89 -1
  278. package/src/db/Db.ts +137 -0
  279. package/src/db/Schema.ts +104 -2
  280. package/src/db/tables/billingSettings.test.ts +87 -0
  281. package/src/db/tables/billingSettings.ts +108 -0
  282. package/src/db/tables/invitations.ts +2 -2
  283. package/src/db/tables/organizations.test.ts +1 -1
  284. package/src/db/tables/organizations.ts +15 -5
  285. package/src/db/tables/projects.ts +2 -2
  286. package/src/db/tables/requestUsage.ts +231 -0
  287. package/src/db/tables/sponsoredTransactions.test.ts +108 -1
  288. package/src/db/tables/sponsoredTransactions.ts +230 -2
  289. package/src/db/tables/stripeCustomers.test.ts +109 -0
  290. package/src/db/tables/stripeCustomers.ts +162 -0
  291. package/src/db/tables/users.ts +2 -2
  292. package/src/handlers/internal/sponsorship.ts +34 -7
  293. package/src/handlers/internal/utils.ts +19 -7
  294. package/src/index.ts +1 -0
  295. package/src/internal/Auth.test.ts +175 -25
  296. package/src/internal/Auth.ts +175 -86
  297. package/src/internal/Cache.ts +5 -1
  298. package/src/internal/EdgeCache.test.ts +27 -4
  299. package/src/internal/EdgeCache.ts +47 -9
  300. package/src/internal/Fees.test.ts +32 -0
  301. package/src/internal/Fees.ts +51 -0
  302. package/src/internal/FundingRoutes.ts +583 -0
  303. package/src/internal/Id.test.ts +17 -0
  304. package/src/internal/Id.ts +24 -0
  305. package/src/internal/Log.test.ts +1 -0
  306. package/src/internal/Log.ts +5 -0
  307. package/src/internal/OpenApi.ts +12 -1
  308. package/src/internal/RateLimit.test.ts +84 -0
  309. package/src/internal/RateLimit.ts +70 -0
  310. package/src/internal/Response.ts +1 -1
  311. package/src/internal/Schema.ts +8 -0
  312. package/src/internal/Webhooks.test.ts +3 -3
  313. package/src/internal/Webhooks.ts +3 -3
@@ -0,0 +1,206 @@
1
+ import { Cli as incur_Cli, z } from 'incur'
2
+
3
+ import * as TestApp from '../../test/App.js'
4
+ import * as Runtime from '../../test/runtime.js'
5
+ import { mcp } from './mcp.js'
6
+
7
+ const origin = 'https://api.tempo-api.test'
8
+
9
+ /** API key with read-only webhook scope, used to prove header forwarding. */
10
+ const reader = {
11
+ id: 'key_reader',
12
+ orgId: 'org_test',
13
+ scopes: ['webhooks:read'],
14
+ token: 'secret_reader',
15
+ } satisfies TestApp.kvStore.Key
16
+
17
+ /** Composes a test app with the MCP group mounted, dispatching in-process. */
18
+ function createHost(
19
+ options: {
20
+ app?: ReturnType<typeof TestApp.create> | undefined
21
+ docs?: mcp.Options['docs'] | undefined
22
+ } = {},
23
+ ) {
24
+ const app = options.app ?? TestApp.create()
25
+ const host = mcp({ docs: options.docs ?? false, fetch: (request) => app.fetch(request) })
26
+ return { app, host }
27
+ }
28
+
29
+ /** Stub remote docs MCP server: an in-process incur CLI serving `/mcp`. */
30
+ function docsStub() {
31
+ const remote = incur_Cli.create('docs-remote', { version: '1.0.0' }).command('search', {
32
+ description: 'Search docs',
33
+ options: z.object({ query: z.string().describe('Search query') }),
34
+ run: (context) => ({ hits: [context.options.query] }),
35
+ })
36
+ return { fetch: (request: Request) => remote.fetch(request), url: 'http://docs.local/mcp' }
37
+ }
38
+
39
+ /** Anything serving `/mcp`: the group host, or a composed app it is mounted into. */
40
+ type Host = { fetch: (request: Request) => Response | Promise<Response> }
41
+
42
+ async function request(host: Host, body: unknown, headers: Record<string, string> = {}) {
43
+ const response = await host.fetch(
44
+ new Request(new URL('/mcp', origin), {
45
+ body: JSON.stringify(body),
46
+ headers: {
47
+ accept: 'application/json, text/event-stream',
48
+ 'content-type': 'application/json',
49
+ ...headers,
50
+ },
51
+ method: 'POST',
52
+ }),
53
+ )
54
+ expect(response.status).toBe(200)
55
+ return (await response.json()) as { result: any }
56
+ }
57
+
58
+ function initialize(host: Host) {
59
+ return request(host, {
60
+ id: 1,
61
+ jsonrpc: '2.0',
62
+ method: 'initialize',
63
+ params: {
64
+ capabilities: {},
65
+ clientInfo: { name: 'test-client', version: '1.0.0' },
66
+ protocolVersion: '2025-03-26',
67
+ },
68
+ })
69
+ }
70
+
71
+ async function toolsList(host: Host) {
72
+ const body = await request(host, { id: 2, jsonrpc: '2.0', method: 'tools/list', params: {} })
73
+ return (body.result.tools as { name: string }[]).map((tool) => tool.name)
74
+ }
75
+
76
+ async function toolsCall(
77
+ host: Host,
78
+ name: string,
79
+ args: Record<string, unknown>,
80
+ headers: Record<string, string> = {},
81
+ ) {
82
+ const body = await request(
83
+ host,
84
+ { id: 3, jsonrpc: '2.0', method: 'tools/call', params: { arguments: args, name } },
85
+ headers,
86
+ )
87
+ return body.result as {
88
+ content: { text: string; type: string }[]
89
+ isError?: boolean
90
+ structuredContent?: Record<string, unknown>
91
+ }
92
+ }
93
+
94
+ /** Parses a tool result: structured content when present, else the JSON text item. */
95
+ function resultJson(result: Awaited<ReturnType<typeof toolsCall>>) {
96
+ return result.structuredContent ?? JSON.parse(result.content[0]!.text)
97
+ }
98
+
99
+ test('scopes tools to the data domain', async () => {
100
+ const { app, host } = createHost({ docs: docsStub() })
101
+
102
+ // The domain filter must drop paths that actually exist in the spec.
103
+ const spec = (await (await app.fetch(new Request(new URL('/openapi.json', origin)))).json()) as {
104
+ paths: Record<string, unknown>
105
+ }
106
+ expect(Object.keys(spec.paths).some((path) => path.startsWith('/gecko/'))).toBe(true)
107
+ expect(Object.keys(spec.paths).some((path) => path.startsWith('/v1/auth/'))).toBe(true)
108
+ expect(Object.keys(spec.paths).some((path) => path.startsWith('/v1/orgs'))).toBe(true)
109
+
110
+ await initialize(host)
111
+ const tools = await toolsList(host)
112
+
113
+ expect(tools).toContain('v1_tokens_get')
114
+ expect(tools).toContain('docs_search')
115
+ // RPC is part of the data domain tag group.
116
+ expect(tools.some((name) => name.startsWith('rpc'))).toBe(true)
117
+ // Out of domain: CoinGecko mirrors, session auth, platform, relay.
118
+ expect(tools.some((name) => name.startsWith('gecko_'))).toBe(false)
119
+ expect(tools.some((name) => name.startsWith('v1_auth_'))).toBe(false)
120
+ expect(tools.some((name) => name.startsWith('v1_orgs'))).toBe(false)
121
+ expect(tools.some((name) => name.startsWith('v1_me'))).toBe(false)
122
+ })
123
+
124
+ test('executes generated tools against the composed app', async () => {
125
+ // Verified tokens are database-backed, so seed the default chain.
126
+ const db = TestApp.database()
127
+ await TestApp.verifiedSeed(db, Runtime.get().chainId)
128
+ const { host } = createHost({ app: TestApp.create({ db }) })
129
+
130
+ await initialize(host)
131
+ const result = await toolsCall(host, 'v1_tokens_get', { verified: true })
132
+
133
+ expect(result.isError).toBeFalsy()
134
+ const body = resultJson(result) as { data: { verified: boolean }[]; nextCursor: unknown }
135
+ expect(body.data.length).toBeGreaterThan(0)
136
+ expect(body.data.every((token) => token.verified === true)).toBe(true)
137
+ })
138
+
139
+ test('forwards the caller Authorization header to authenticated tools', async () => {
140
+ const { host } = createHost({ app: TestApp.create({ auth: { keys: [reader] } }) })
141
+ await initialize(host)
142
+
143
+ const denied = await toolsCall(host, 'v1_webhooks_get', {})
144
+ expect(denied.isError).toBe(true)
145
+
146
+ const allowed = await toolsCall(
147
+ host,
148
+ 'v1_webhooks_get',
149
+ {},
150
+ { authorization: `Bearer ${reader.token}` },
151
+ )
152
+ expect(allowed.isError).toBeFalsy()
153
+ expect(resultJson(allowed)).toMatchObject({ data: [] })
154
+ })
155
+
156
+ test('proxies docs tool calls to the remote MCP source', async () => {
157
+ const { host } = createHost({ docs: docsStub() })
158
+ await initialize(host)
159
+
160
+ const result = await toolsCall(host, 'docs_search', { query: 'fees' })
161
+
162
+ expect(result.isError).toBeFalsy()
163
+ expect(resultJson(result)).toMatchObject({ hits: ['fees'] })
164
+ })
165
+
166
+ test('documents POST /mcp and keeps it open under auth enforcement', async () => {
167
+ // Worker-style composition: the group mounted into the app it dispatches to.
168
+ const app = TestApp.create()
169
+ app.route('/', mcp({ docs: false, fetch: (request) => app.fetch(request) }))
170
+
171
+ const spec = (await (await app.fetch(new Request(new URL('/openapi.json', origin)))).json()) as {
172
+ paths: Record<string, { post?: { operationId?: string; security?: unknown; tags?: string[] } }>
173
+ 'x-tagGroups': { name: string; tags: string[] }[]
174
+ }
175
+ const operation = spec.paths['/mcp']?.post
176
+ expect(operation?.operationId).toBe('mcpRequest')
177
+ expect(operation?.tags).toEqual(['MCP'])
178
+ // Anonymous and any-valid-key access; no required scopes.
179
+ expect(operation?.security).toEqual([{ apiKey: [] }, { bearerAuth: [] }, {}])
180
+
181
+ // Surfaces under `Data API` in the reference, but the tool filter excludes the `MCP` tag.
182
+ const groups = Object.fromEntries(spec['x-tagGroups'].map((group) => [group.name, group.tags]))
183
+ expect(groups['MCP']).toBeUndefined()
184
+ expect(groups['Data API']).toContain('MCP')
185
+
186
+ // Documenting the route makes it auth-enforced; the public lane keeps it
187
+ // anonymously reachable, and the endpoint never surfaces itself as a tool.
188
+ await initialize(app)
189
+ const tools = await toolsList(app)
190
+ expect(tools.length).toBeGreaterThan(0)
191
+ expect(tools.some((name) => name.startsWith('mcp'))).toBe(false)
192
+
193
+ // A presented-but-invalid key is rejected at the transport.
194
+ const rejected = await app.fetch(
195
+ new Request(new URL('/mcp', origin), {
196
+ body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'tools/list', params: {} }),
197
+ headers: {
198
+ accept: 'application/json, text/event-stream',
199
+ authorization: 'Bearer not-a-key',
200
+ 'content-type': 'application/json',
201
+ },
202
+ method: 'POST',
203
+ }),
204
+ )
205
+ expect(rejected.status).toBe(401)
206
+ })
@@ -0,0 +1,250 @@
1
+ import { type Context, Hono } from 'hono'
2
+ import { Cli as incur_Cli, type Openapi } from 'incur'
3
+ import * as z from 'zod/mini'
4
+
5
+ import type * as App from '../App.js'
6
+ import * as Auth from '../internal/Auth.js'
7
+ import * as OpenApi from '../internal/OpenApi.js'
8
+ import * as Metadata from './metadata.js'
9
+
10
+ /** Document tag group whose operations surface as MCP tools (the data domain). */
11
+ const tagGroup = 'Data API'
12
+
13
+ /** Tags inside the group hidden from MCP: partner-shaped CoinGecko mirrors and the MCP endpoint itself. */
14
+ const excludedTags = ['CoinGecko', 'MCP']
15
+
16
+ /** Usage guidance surfaced to MCP clients on initialize. */
17
+ const instructions =
18
+ "Tools mirror the Tempo API's data domain. Authenticated tools accept a Tempo API key: send it as an `Authorization: Bearer` header on the HTTP request, or pass the `authorization` tool input per call. List endpoints paginate via `cursor`/`nextCursor`. `docs_*` tools search the Tempo documentation."
19
+
20
+ /** Zod schemas owned by the MCP endpoint's OpenAPI description. */
21
+ export namespace schema {
22
+ /** JSON-RPC request identifier. */
23
+ export const Id = z
24
+ .union([z.string(), z.number(), z.null()])
25
+ .check(z.describe('Client-supplied JSON-RPC request id used to match responses to requests.'))
26
+
27
+ /** JSON-RPC error payload. */
28
+ export const Error = z
29
+ .object({
30
+ code: z
31
+ .number()
32
+ .check(z.int(), z.describe('Numeric JSON-RPC error code returned by the MCP server.')),
33
+ data: z
34
+ .optional(z.unknown())
35
+ .check(z.describe('Optional extra error details returned by the MCP server.')),
36
+ message: z.string().check(z.describe('Human-readable JSON-RPC error message.')),
37
+ })
38
+ .check(z.describe('JSON-RPC error object returned when a message fails.'))
39
+
40
+ /** MCP JSON-RPC response payload. */
41
+ export const Response = z
42
+ .object({
43
+ error: z
44
+ .optional(Error)
45
+ .check(z.describe('JSON-RPC error object returned when a message fails.')),
46
+ id: z
47
+ .optional(Id)
48
+ .check(
49
+ z.describe('Client-supplied JSON-RPC request id used to match responses to requests.'),
50
+ ),
51
+ jsonrpc: z.literal('2.0').check(z.describe('JSON-RPC protocol version; MCP uses `2.0`.')),
52
+ result: z.optional(z.unknown()).check(z.describe('Result returned by the MCP method.')),
53
+ })
54
+ .check(z.describe('One MCP JSON-RPC response returned by the server.'))
55
+ }
56
+
57
+ /**
58
+ * The MCP route group: a stateless streamable-HTTP MCP server at `/mcp` whose
59
+ * tools mirror the API's data domain (scoped via the OpenAPI document's
60
+ * `Data API` tag group), plus the remote Tempo docs tools. Mount alongside
61
+ * the other route groups:
62
+ *
63
+ * ```ts
64
+ * App.create(options).route('/', mcp({ fetch: (request) => getApp().fetch(request) }))
65
+ * ```
66
+ */
67
+ export function mcp(options: mcp.Options) {
68
+ const { docs = 'https://mcp.tempo.xyz/mcp', fetch, name = 'tapimo', version = '0.0.0' } = options
69
+
70
+ // One incur instance per caller origin: the OpenAPI document is parsed once,
71
+ // and generated commands rebase onto the origin so tool results embed real
72
+ // absolute URLs (e.g. token asset URIs).
73
+ const clis = new Map<string, ReturnType<typeof build>>()
74
+
75
+ function resolve(origin: string) {
76
+ let cli = clis.get(origin)
77
+ if (!cli) {
78
+ cli = build(origin)
79
+ clis.set(origin, cli)
80
+ // A failed build (spec fetch or docs resolution) retries next request.
81
+ cli.catch(() => clis.delete(origin))
82
+ }
83
+ return cli
84
+ }
85
+
86
+ async function build(origin: string) {
87
+ const response = await fetch(new Request(new URL('/openapi.json', origin)))
88
+ if (!response.ok) throw new Error(`openapi.json request failed: ${response.status}`)
89
+ const document = (await response.json()) as Document
90
+ const cli = incur_Cli.create(name, {
91
+ description: 'Tempo API',
92
+ // Generated commands build requests against `http://localhost`; rebase
93
+ // them onto the caller origin and dispatch in-process.
94
+ fetch: (request) => {
95
+ const url = new URL(request.url)
96
+ return fetch(new Request(new URL(url.pathname + url.search, origin), request))
97
+ },
98
+ mcp: { instructions },
99
+ openapi: scope(document),
100
+ openapiConfig: { forwardHeaders: ['authorization'], mode: 'namespace' },
101
+ version,
102
+ })
103
+ if (docs !== false)
104
+ cli.command('docs', { description: 'Search the Tempo documentation.', mcp: docs })
105
+ return cli
106
+ }
107
+
108
+ async function handler(c: Context<App.Environment>) {
109
+ const cli = await resolve(new URL(c.req.url).origin)
110
+ // incur routes on the first path segment; pin the path to `/mcp` so a
111
+ // mount prefix never leaks into its router.
112
+ return cli.fetch(new Request(new URL('/mcp', c.req.url), c.req.raw))
113
+ }
114
+
115
+ // The documented POST carries the auth policy (described routes are
116
+ // default-closed); other methods fall through to the bare route and incur's 405.
117
+ const app = new Hono<App.Environment>()
118
+ .post(
119
+ '/mcp',
120
+ Auth.policy({ apiKey: { scopes: [] }, public: true }),
121
+ OpenApi.describeRoute({
122
+ description:
123
+ "A stateless streamable-HTTP MCP server: send JSON-RPC 2.0 messages as `POST` with `Accept: application/json, text/event-stream`. Tools mirror the API's data domain, plus `docs_*` documentation search. Anonymous requests use the public quota; send `Authorization: Bearer <api key>` for protected tools.",
124
+ operationId: 'mcpRequest',
125
+ // The JSON-RPC request envelope is protocol-owned (like `/rpc`), so the
126
+ // body documents an example rather than a schema.
127
+ requestBody: {
128
+ content: {
129
+ 'application/json': {
130
+ example: { jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} },
131
+ },
132
+ },
133
+ required: true,
134
+ },
135
+ responses: {
136
+ 200: {
137
+ content: {
138
+ 'application/json': {
139
+ example: {
140
+ jsonrpc: '2.0',
141
+ id: 1,
142
+ result: { tools: [{ name: 'v1_tokens_get', description: 'List tokens' }] },
143
+ },
144
+ schema: OpenApi.resolver(schema.Response),
145
+ },
146
+ },
147
+ description: 'JSON-RPC response returned by the MCP server.',
148
+ headers: { ...OpenApi.successHeaders },
149
+ },
150
+ 202: {
151
+ description:
152
+ 'The message was a notification or client response; there is nothing to return.',
153
+ },
154
+ 400: {
155
+ content: { 'application/json': { schema: OpenApi.resolver(schema.Response) } },
156
+ description:
157
+ 'Malformed or invalid MCP message, returned as a JSON-RPC error response rather than the standard error envelope. A malformed API key returns the standard `400` envelope instead.',
158
+ },
159
+ 401: OpenApi.standardError(
160
+ 401,
161
+ 'The presented API key is invalid. Anonymous requests are allowed under the public quota.',
162
+ ['api_key_invalid'],
163
+ ),
164
+ 406: {
165
+ content: { 'application/json': { schema: OpenApi.resolver(schema.Response) } },
166
+ description:
167
+ 'The `Accept` header must include both `application/json` and `text/event-stream`. Returned as a JSON-RPC error response.',
168
+ },
169
+ 429: OpenApi.standardError(429, 'Rate limit exceeded.'),
170
+ 500: OpenApi.standardError(500, 'Internal server error.'),
171
+ },
172
+ summary: 'Call MCP',
173
+ tags: ['MCP'],
174
+ }),
175
+ handler,
176
+ )
177
+ .all('/mcp', handler)
178
+ return Metadata.attach(app, openapi)
179
+ }
180
+
181
+ /** The OpenAPI metadata this group owns: the MCP endpoint's tag, surfaced under the `Data API` group. */
182
+ const openapi = {
183
+ tags: [
184
+ {
185
+ name: 'MCP',
186
+ description:
187
+ 'A hosted Model Context Protocol server: the data domain and Tempo docs exposed as tools.',
188
+ },
189
+ ],
190
+ 'x-tagGroups': [{ name: tagGroup, tags: ['MCP'] }],
191
+ } satisfies App.create.Metadata
192
+
193
+ /** The slice of the OpenAPI document the domain filter reads. */
194
+ type Document = Openapi.OpenAPISpec & {
195
+ paths: Record<string, Record<string, unknown>>
196
+ 'x-tagGroups'?: readonly { name: string; tags: readonly string[] }[] | undefined
197
+ }
198
+
199
+ /** HTTP methods that carry operations in an OpenAPI path item. */
200
+ const methods: readonly string[] = ['delete', 'get', 'head', 'options', 'patch', 'post', 'put', 'trace'] // prettier-ignore
201
+
202
+ /**
203
+ * Narrows the document to the data domain: operations tagged inside the
204
+ * `Data API` tag group, minus excluded tags. Platform, relay, and session
205
+ * operations never surface as tools.
206
+ */
207
+ function scope(document: Document): Openapi.OpenAPISpec {
208
+ const group = document['x-tagGroups']?.find((entry) => entry.name === tagGroup)
209
+ if (!group) throw new Error(`openapi.json is missing the "${tagGroup}" tag group`)
210
+ const tags = new Set(group.tags.filter((tag) => !excludedTags.includes(tag)))
211
+ const paths: Document['paths'] = {}
212
+ for (const [path, item] of Object.entries(document.paths)) {
213
+ // Keep in-domain operations; non-method keys (parameters, summary) ride along.
214
+ const entries = Object.entries(item).filter(([method, operation]) => {
215
+ if (!methods.includes(method)) return true
216
+ const tags_operation = (operation as { tags?: readonly string[] | undefined }).tags
217
+ return tags_operation?.some((tag) => tags.has(tag)) ?? false
218
+ })
219
+ if (entries.some(([method]) => methods.includes(method)))
220
+ paths[path] = Object.fromEntries(entries)
221
+ }
222
+ return { ...document, paths }
223
+ }
224
+
225
+ export declare namespace mcp {
226
+ /** Options for the MCP route group. */
227
+ type Options = {
228
+ /** Remote docs MCP source mounted as the `docs` tool group, or `false` to omit it. @default 'https://mcp.tempo.xyz/mcp' */
229
+ docs?: Source | false | undefined
230
+ /** In-process fetch handler for the composed app (e.g. `(request) => app.fetch(request)`). */
231
+ fetch: (request: Request) => Response | Promise<Response>
232
+ /** MCP server name. @default 'tapimo' */
233
+ name?: string | undefined
234
+ /** MCP server version. @default '0.0.0' */
235
+ version?: string | undefined
236
+ }
237
+
238
+ /** A remote streamable-HTTP MCP server endpoint. */
239
+ type Source =
240
+ | string
241
+ | URL
242
+ | {
243
+ /** Fetch handler used for MCP requests. Defaults to `globalThis.fetch`. */
244
+ fetch?: ((request: Request) => Response | Promise<Response>) | undefined
245
+ /** Headers merged into every MCP request. */
246
+ headers?: Record<string, string> | undefined
247
+ /** Streamable-HTTP MCP endpoint URL. */
248
+ url: string | URL
249
+ }
250
+ }
@@ -1,14 +1,18 @@
1
1
  import { Hono } from 'hono'
2
+ import type Stripe from 'stripe'
2
3
 
3
4
  import type * as App from '../../App.js'
4
5
  import * as Metadata from '../metadata.js'
5
6
 
6
7
  import { apiKeys } from './routes/api-keys.js'
8
+ import { billing } from './routes/billing.js'
7
9
  import { invitations } from './routes/invitations.js'
8
10
  import { me } from './routes/me.js'
9
11
  import { members } from './routes/members.js'
10
12
  import { orgs } from './routes/orgs.js'
11
13
  import { projects } from './routes/projects.js'
14
+ import { scopes } from './routes/scopes.js'
15
+ import { usage } from './routes/usage.js'
12
16
 
13
17
  /**
14
18
  * Minimal transactional email sender. Cloudflare deployments wrap the Email
@@ -40,13 +44,39 @@ export declare namespace Email {
40
44
  }
41
45
  }
42
46
 
47
+ /** Billing capability, one bag per billing source; Stripe is the sole source today. */
48
+ export type Billing = {
49
+ /**
50
+ * Stripe billing source.
51
+ */
52
+ stripe: {
53
+ /** Constructed Stripe client. */
54
+ client: Stripe
55
+ /** Console URL for Checkout/portal return links when the request carries no `Origin`. Defaults to the hosted console. */
56
+ consoleUrl?: string | undefined
57
+ /** Test-mode Stripe backing sandbox billing; omit to keep sandbox billing 501. */
58
+ sandbox?:
59
+ | {
60
+ /** Constructed test-mode Stripe client. */
61
+ client: Stripe
62
+ /** Test-mode webhook endpoint signing secret (`whsec_…`). */
63
+ webhookSecret: string
64
+ }
65
+ | undefined
66
+ /** Webhook endpoint signing secret (`whsec_…`). */
67
+ webhookSecret: string
68
+ }
69
+ }
70
+
43
71
  /**
44
72
  * Request environment for platform routes: the shared app environment plus the
45
- * `email` sender this group publishes on context. Owned here, not in core
46
- * `App.Environment`, since email is a platform concern.
73
+ * capabilities this group publishes on context (`billing`, `email`). Owned
74
+ * here, not in core `App.Environment`, since these are platform concerns.
47
75
  */
48
76
  export type Environment = {
49
77
  Variables: App.Environment['Variables'] & {
78
+ /** Stripe billing capability; undefined when billing is unconfigured. */
79
+ billing: Billing | undefined
50
80
  /** Transactional email sender for platform emails; undefined when dispatch is disabled. */
51
81
  email: Email | undefined
52
82
  }
@@ -66,18 +96,21 @@ export type Environment = {
66
96
  */
67
97
  export function platform(options: platform.Options = {}) {
68
98
  const app = new Hono<Environment>()
69
- // Publish the transactional email sender for this group's handlers
70
- // (invitations read it off context); undefined disables dispatch.
99
+ // Publish this group's capabilities for handlers; undefined disables each.
71
100
  .use('*', async (c, next) => {
101
+ c.set('billing', options.billing)
72
102
  c.set('email', options.email)
73
103
  await next()
74
104
  })
75
105
  .route('/', apiKeys({ enabled: options.apiKeys !== false }))
106
+ .route('/', billing())
76
107
  .route('/', invitations())
77
108
  .route('/', me())
78
109
  .route('/', members())
79
110
  .route('/', orgs())
80
111
  .route('/', projects())
112
+ .route('/', scopes())
113
+ .route('/', usage())
81
114
  return Metadata.attach(app, openapi)
82
115
  }
83
116
 
@@ -86,6 +119,8 @@ export declare namespace platform {
86
119
  type Options = {
87
120
  /** Expose the API-key management routes in the OpenAPI document. @default true */
88
121
  apiKeys?: boolean | undefined
122
+ /** Stripe billing capability; omit to disable billing routes (501) and keep production sponsorship closed. */
123
+ billing?: Billing | undefined
89
124
  /** Transactional email sender for platform emails (e.g. invitations); omit to disable dispatch — notified flows still work in-product. */
90
125
  email?: Email | undefined
91
126
  }
@@ -107,10 +142,12 @@ const openapi = {
107
142
  },
108
143
  tags: [
109
144
  { name: 'API Keys', description: 'Credentials for accessing the Tempo API.' },
145
+ { name: 'Billing', description: 'Organization billing backed by Stripe.' },
110
146
  { name: 'Invitations', description: 'Pending organization invitations.' },
111
147
  { name: 'Members', description: 'Organization team membership.' },
112
148
  { name: 'Organizations', description: 'Teams that own application workspaces and members.' },
113
149
  { name: 'Projects', description: 'Application workspaces within an organization.' },
150
+ { name: 'Usage', description: 'Request and sponsorship usage for organizations.' },
114
151
  { name: 'Users', description: 'Users on the Tempo Platform.' },
115
152
  ],
116
153
  'x-tagGroups': [
@@ -119,10 +156,12 @@ const openapi = {
119
156
  tags: [
120
157
  'API Keys',
121
158
  'Authentication',
159
+ 'Billing',
122
160
  'Invitations',
123
161
  'Members',
124
162
  'Organizations',
125
163
  'Projects',
164
+ 'Usage',
126
165
  'Users',
127
166
  ],
128
167
  },