tapimo 0.5.0 → 0.5.2

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 (197) hide show
  1. package/dist/App.d.ts +4854 -4829
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +20 -2
  4. package/dist/App.js.map +1 -1
  5. package/dist/Assets.js +3 -3
  6. package/dist/Assets.js.map +1 -1
  7. package/dist/Scope.d.ts.map +1 -1
  8. package/dist/Scope.js +1 -2
  9. package/dist/Scope.js.map +1 -1
  10. package/dist/Webhooks.d.ts +1 -1
  11. package/dist/Webhooks.d.ts.map +1 -1
  12. package/dist/Webhooks.js +5 -10
  13. package/dist/Webhooks.js.map +1 -1
  14. package/dist/admin/App.d.ts +6 -34
  15. package/dist/admin/App.d.ts.map +1 -1
  16. package/dist/admin/apps/api-keys.d.ts +3 -3
  17. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  18. package/dist/admin/apps/api-keys.js +4 -1
  19. package/dist/admin/apps/api-keys.js.map +1 -1
  20. package/dist/admin/apps/scopes.d.ts +6 -34
  21. package/dist/admin/apps/scopes.d.ts.map +1 -1
  22. package/dist/admin/apps/scopes.js +2 -2
  23. package/dist/admin/apps/scopes.js.map +1 -1
  24. package/dist/admin/apps/verified-tokens.d.ts +2 -2
  25. package/dist/admin/ui.generated.d.ts +1 -1
  26. package/dist/admin/ui.generated.js +33 -33
  27. package/dist/admin/ui.generated.js.map +1 -1
  28. package/dist/apps/data/App.d.ts +1398 -1405
  29. package/dist/apps/data/App.d.ts.map +1 -1
  30. package/dist/apps/data/App.js +25 -38
  31. package/dist/apps/data/App.js.map +1 -1
  32. package/dist/apps/data/FundingQuoteProviders.d.ts +108 -3
  33. package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
  34. package/dist/apps/data/FundingQuoteProviders.js +409 -2
  35. package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
  36. package/dist/apps/data/routes/activities.d.ts +245 -245
  37. package/dist/apps/data/routes/blocks.d.ts +123 -123
  38. package/dist/apps/data/routes/funding.d.ts +236 -146
  39. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  40. package/dist/apps/data/routes/funding.js +23 -12
  41. package/dist/apps/data/routes/funding.js.map +1 -1
  42. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  43. package/dist/apps/data/routes/indexer.js +9 -2
  44. package/dist/apps/data/routes/indexer.js.map +1 -1
  45. package/dist/apps/data/routes/rpc.d.ts +31 -31
  46. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  47. package/dist/apps/data/routes/transactions.d.ts +281 -281
  48. package/dist/apps/data/routes/transfers.d.ts +66 -73
  49. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  50. package/dist/apps/data/routes/transfers.js +21 -27
  51. package/dist/apps/data/routes/transfers.js.map +1 -1
  52. package/dist/apps/data/routes/verified-tokens.d.ts +86 -86
  53. package/dist/apps/data/routes/webhooks.d.ts +35 -35
  54. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  55. package/dist/apps/data/routes/webhooks.js +176 -42
  56. package/dist/apps/data/routes/webhooks.js.map +1 -1
  57. package/dist/apps/management/App.d.ts +520 -504
  58. package/dist/apps/management/App.d.ts.map +1 -1
  59. package/dist/apps/management/Billing.d.ts.map +1 -1
  60. package/dist/apps/management/Billing.js +5 -4
  61. package/dist/apps/management/Billing.js.map +1 -1
  62. package/dist/apps/management/routes/api-keys.d.ts +120 -120
  63. package/dist/apps/management/routes/billing.d.ts +211 -211
  64. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  65. package/dist/apps/management/routes/billing.js +1 -1
  66. package/dist/apps/management/routes/billing.js.map +1 -1
  67. package/dist/apps/management/routes/invitations.d.ts +72 -72
  68. package/dist/apps/management/routes/invite-links.d.ts +159 -159
  69. package/dist/apps/management/routes/me.d.ts +17 -17
  70. package/dist/apps/management/routes/members.d.ts +80 -80
  71. package/dist/apps/management/routes/orgs.d.ts +59 -43
  72. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  73. package/dist/apps/management/routes/orgs.js +18 -1
  74. package/dist/apps/management/routes/orgs.js.map +1 -1
  75. package/dist/apps/management/routes/projects.d.ts +109 -109
  76. package/dist/apps/management/routes/scopes.d.ts +11 -11
  77. package/dist/apps/management/routes/usage.d.ts +79 -79
  78. package/dist/apps/mcp.js +2 -2
  79. package/dist/apps/mcp.js.map +1 -1
  80. package/dist/apps/relay/App.d.ts +2 -2
  81. package/dist/apps/relay/App.js +2 -2
  82. package/dist/apps/relay/Sponsorships.d.ts +3 -0
  83. package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
  84. package/dist/apps/relay/Sponsorships.js +15 -1
  85. package/dist/apps/relay/Sponsorships.js.map +1 -1
  86. package/dist/db/Db.d.ts.map +1 -1
  87. package/dist/db/Db.js +4 -0
  88. package/dist/db/Db.js.map +1 -1
  89. package/dist/db/Schema.js +1 -1
  90. package/dist/db/Schema.js.map +1 -1
  91. package/dist/db/tables/organizations.d.ts +6 -0
  92. package/dist/db/tables/organizations.d.ts.map +1 -1
  93. package/dist/db/tables/organizations.js +26 -0
  94. package/dist/db/tables/organizations.js.map +1 -1
  95. package/dist/db/tables/requestUsage.d.ts +2 -11
  96. package/dist/db/tables/requestUsage.d.ts.map +1 -1
  97. package/dist/db/tables/requestUsage.js +1 -20
  98. package/dist/db/tables/requestUsage.js.map +1 -1
  99. package/dist/db/tables/sponsoredTransactions.d.ts +2 -1
  100. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  101. package/dist/db/tables/sponsoredTransactions.js +52 -10
  102. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  103. package/dist/internal/Auth.d.ts +4 -0
  104. package/dist/internal/Auth.d.ts.map +1 -1
  105. package/dist/internal/Auth.js +15 -8
  106. package/dist/internal/Auth.js.map +1 -1
  107. package/dist/internal/Cache.d.ts.map +1 -1
  108. package/dist/internal/Cache.js +9 -1
  109. package/dist/internal/Cache.js.map +1 -1
  110. package/dist/internal/EdgeCache.d.ts.map +1 -1
  111. package/dist/internal/EdgeCache.js +12 -3
  112. package/dist/internal/EdgeCache.js.map +1 -1
  113. package/dist/internal/FundingQuotes.d.ts +176 -56
  114. package/dist/internal/FundingQuotes.d.ts.map +1 -1
  115. package/dist/internal/FundingQuotes.js +100 -5
  116. package/dist/internal/FundingQuotes.js.map +1 -1
  117. package/dist/internal/Log.d.ts +18 -12
  118. package/dist/internal/Log.d.ts.map +1 -1
  119. package/dist/internal/Log.js +2 -0
  120. package/dist/internal/Log.js.map +1 -1
  121. package/dist/internal/RateLimit.d.ts +2 -0
  122. package/dist/internal/RateLimit.d.ts.map +1 -1
  123. package/dist/internal/RateLimit.js +4 -4
  124. package/dist/internal/RateLimit.js.map +1 -1
  125. package/dist/internal/Tidx.d.ts +9 -3
  126. package/dist/internal/Tidx.d.ts.map +1 -1
  127. package/dist/internal/Tidx.js +13 -7
  128. package/dist/internal/Tidx.js.map +1 -1
  129. package/dist/internal/Viem.d.ts +14 -1
  130. package/dist/internal/Viem.d.ts.map +1 -1
  131. package/dist/internal/Viem.js +19 -1
  132. package/dist/internal/Viem.js.map +1 -1
  133. package/package.json +7 -2
  134. package/src/ApiKeys.test.ts +2 -0
  135. package/src/App.test.ts +15 -4
  136. package/src/App.ts +31 -5
  137. package/src/Assets.ts +3 -3
  138. package/src/Client.test-d.ts +30 -3
  139. package/src/Scope.ts +4 -2
  140. package/src/Webhooks.ts +5 -10
  141. package/src/admin/apps/api-keys.test.ts +27 -0
  142. package/src/admin/apps/api-keys.ts +8 -1
  143. package/src/admin/apps/scopes.test.ts +7 -2
  144. package/src/admin/apps/scopes.ts +2 -2
  145. package/src/admin/ui/src/components/Dialog.tsx +8 -2
  146. package/src/admin/ui/src/lib/scopes.test.ts +19 -0
  147. package/src/admin/ui/src/lib/scopes.ts +12 -0
  148. package/src/admin/ui/src/routes/api-keys.tsx +128 -27
  149. package/src/admin/ui.generated.ts +33 -33
  150. package/src/apps/composition.test.ts +77 -0
  151. package/src/apps/data/App.ts +25 -45
  152. package/src/apps/data/FundingQuoteProviders.test.ts +615 -5
  153. package/src/apps/data/FundingQuoteProviders.ts +570 -4
  154. package/src/apps/data/routes/funding.test.ts +181 -7
  155. package/src/apps/data/routes/funding.ts +27 -12
  156. package/src/apps/data/routes/indexer.test.ts +82 -20
  157. package/src/apps/data/routes/indexer.ts +8 -2
  158. package/src/apps/data/routes/tokens.test.ts +54 -0
  159. package/src/apps/data/routes/transfers.test.ts +73 -1
  160. package/src/apps/data/routes/transfers.ts +24 -30
  161. package/src/apps/data/routes/webhooks.test.ts +203 -0
  162. package/src/apps/data/routes/webhooks.ts +191 -54
  163. package/src/apps/management/Billing.test.ts +52 -3
  164. package/src/apps/management/Billing.ts +5 -4
  165. package/src/apps/management/routes/billing.ts +6 -1
  166. package/src/apps/management/routes/orgs.ts +19 -1
  167. package/src/apps/mcp.test.ts +18 -0
  168. package/src/apps/mcp.ts +2 -2
  169. package/src/apps/relay/App.test.ts +6 -5
  170. package/src/apps/relay/App.ts +2 -2
  171. package/src/apps/relay/Sponsorships.test.ts +2 -2
  172. package/src/apps/relay/Sponsorships.ts +24 -1
  173. package/src/db/Db.test.ts +5 -5
  174. package/src/db/Db.ts +4 -0
  175. package/src/db/Schema.ts +1 -1
  176. package/src/db/tables/billingSettings.test.ts +2 -2
  177. package/src/db/tables/organizations.test.ts +32 -2
  178. package/src/db/tables/organizations.ts +28 -0
  179. package/src/db/tables/requestUsage.ts +3 -23
  180. package/src/db/tables/sponsoredTransactions.test.ts +60 -2
  181. package/src/db/tables/sponsoredTransactions.ts +67 -21
  182. package/src/db/tables/stripeCustomers.test.ts +2 -2
  183. package/src/internal/Auth.test.ts +63 -10
  184. package/src/internal/Auth.ts +21 -9
  185. package/src/internal/Cache.test.ts +19 -0
  186. package/src/internal/Cache.ts +13 -1
  187. package/src/internal/EdgeCache.test.ts +40 -0
  188. package/src/internal/EdgeCache.ts +11 -3
  189. package/src/internal/FundingQuotes.test.ts +130 -3
  190. package/src/internal/FundingQuotes.ts +113 -6
  191. package/src/internal/Log.ts +21 -14
  192. package/src/internal/RateLimit.test.ts +12 -0
  193. package/src/internal/RateLimit.ts +4 -4
  194. package/src/internal/Tidx.test.ts +48 -0
  195. package/src/internal/Tidx.ts +21 -7
  196. package/src/internal/Viem.test.ts +46 -0
  197. package/src/internal/Viem.ts +34 -1
@@ -1,5 +1,5 @@
1
1
  import { type Context, Hono } from 'hono'
2
- import { type AbiEvent, parseAbiItem } from 'viem'
2
+ import { type AbiEvent, decodeEventLog, encodeEventTopics, parseAbiItem, type RpcLog } from 'viem'
3
3
  import * as z from 'zod/mini'
4
4
 
5
5
  import type * as App from '../../../App.js'
@@ -15,6 +15,7 @@ import type * as Tidx from '../../../internal/Tidx.js'
15
15
  import * as Ttl from '../../../internal/Ttl.js'
16
16
  import * as Value from '../../../internal/Value.js'
17
17
  import type * as Viem from '../../../internal/Viem.js'
18
+ import type * as WebhookDestination from '../../../internal/WebhookDestination.js'
18
19
  import * as Webhooks from '../../../internal/Webhooks.js'
19
20
  import * as Receipts from './receipts.js'
20
21
  import * as Transactions from './transactions.js'
@@ -30,6 +31,9 @@ const defaultMaxPerOwner = 100
30
31
  */
31
32
  const mppTtlMs = 30 * 24 * 60 * 60 * 1_000
32
33
 
34
+ const redactedBetterstackToken = '[redacted]'
35
+ const redactedSlackUrl = 'https://hooks.slack.com/…'
36
+
33
37
  /**
34
38
  * The subscribable webhook event types, in alphabetical order. The source of
35
39
  * truth for the event-type *set*: the {@link EventType} union and the route zod
@@ -97,12 +101,11 @@ export namespace schema {
97
101
  z.meta({ examples: ['https://example.com/webhooks'] }),
98
102
  )
99
103
 
100
- const SlackUrl = z
101
- .url()
102
- .check(
103
- z.describe('Your Slack incoming-webhook URL (`https://hooks.slack.com/services/…`).'),
104
- z.meta({ examples: ['https://hooks.slack.com/services/T000/B000/XXXXXXXX'] }),
105
- )
104
+ const SlackUrl = z.url().check(
105
+ z.refine((url) => url !== redactedSlackUrl, { error: 'A Slack webhook URL is required.' }),
106
+ z.describe('Your Slack incoming-webhook URL (`https://hooks.slack.com/services/…`).'),
107
+ z.meta({ examples: ['https://hooks.slack.com/services/T000/B000/XXXXXXXX'] }),
108
+ )
106
109
 
107
110
  const BetterstackUrl = z
108
111
  .url()
@@ -113,13 +116,14 @@ export namespace schema {
113
116
  z.meta({ examples: ['https://s1234567.eu-nbg-2.betterstackdata.com'] }),
114
117
  )
115
118
 
116
- const BetterstackToken = z
117
- .string()
118
- .check(
119
- z.minLength(1),
120
- z.describe('Your Better Stack source token (sent as `Authorization: Bearer …`).'),
121
- z.meta({ examples: ['FczKcxEhjEDE58dBX7XaeX1q'] }),
122
- )
119
+ const BetterstackToken = z.string().check(
120
+ z.minLength(1),
121
+ z.refine((token) => token !== redactedBetterstackToken, {
122
+ error: 'A Better Stack source token is required.',
123
+ }),
124
+ z.describe('Your Better Stack source token (sent as `Authorization: Bearer …`).'),
125
+ z.meta({ examples: ['FczKcxEhjEDE58dBX7XaeX1q'] }),
126
+ )
123
127
 
124
128
  /**
125
129
  * Where matched events are delivered: a `url` destination (a signed event POST
@@ -149,6 +153,26 @@ export namespace schema {
149
153
  ),
150
154
  )
151
155
 
156
+ const PublicDestination = z
157
+ .discriminatedUnion('type', [
158
+ z.object({
159
+ type: z.literal('url'),
160
+ url: Url,
161
+ }),
162
+ z.object({
163
+ type: z.literal('slack'),
164
+ url: z.literal(redactedSlackUrl).check(z.describe('Redacted Slack webhook URL.')),
165
+ }),
166
+ z.object({
167
+ token: z
168
+ .literal(redactedBetterstackToken)
169
+ .check(z.describe('Redacted Better Stack source token.')),
170
+ type: z.literal('betterstack'),
171
+ url: BetterstackUrl,
172
+ }),
173
+ ])
174
+ .check(z.describe('Delivery destination with bearer credentials redacted.'))
175
+
152
176
  /**
153
177
  * Equality / membership operator for a filterable field. A consumer can pass a
154
178
  * bare value (shorthand for exact equality, the common case) or an operator
@@ -399,15 +423,10 @@ export namespace schema {
399
423
  z.refine((f) => !f.args || f.signature, {
400
424
  error: '`args` requires `signature` to decode the event.',
401
425
  }),
402
- z.refine(
403
- (f) =>
404
- Boolean(f.address) ||
405
- (Boolean(f.signature || f.topic0) && Boolean(f.topic1 || f.topic2 || f.topic3 || f.args)),
406
- {
407
- error:
408
- 'Addressless log filters must include an event signature or topic0 plus at least one indexed topic or argument.',
409
- },
410
- ),
426
+ z.refine((f) => hasLogPushdown(f), {
427
+ error:
428
+ 'Log filters must include a pushdown-capable address or an event/topic0 anchor plus an indexed topic or argument.',
429
+ }),
411
430
  )
412
431
 
413
432
  /**
@@ -507,7 +526,7 @@ export namespace schema {
507
526
  z.describe('When the subscription was created, as an ISO 8601 timestamp.'),
508
527
  z.meta({ examples: ['2024-01-01T00:00:00Z'] }),
509
528
  ),
510
- destination: Destination,
529
+ destination: PublicDestination,
511
530
  eventType: EventType.check(z.meta({ examples: ['token:transfer'] })),
512
531
  expiresAt: z
513
532
  .optional(z.iso.datetime())
@@ -1032,7 +1051,9 @@ export namespace schema {
1032
1051
  .check(z.describe('Details for creating a webhook subscription.'))
1033
1052
 
1034
1053
  export const Response = z
1035
- .extend(Subscription, {
1054
+ .object({
1055
+ ...Subscription.shape,
1056
+ destination: Destination,
1036
1057
  secret: z
1037
1058
  .string()
1038
1059
  .check(
@@ -2294,6 +2315,7 @@ export function matchesOperator(
2294
2315
  return value.toLowerCase().startsWith(String(o['startsWith']).toLowerCase())
2295
2316
  if ('in' in o) return (o['in'] as readonly unknown[]).some((v) => operandEquals(value, v, kind))
2296
2317
  if ('not' in o) return !operandEquals(value, o['not'], kind)
2318
+ if ('eq' in o && kind === 'string') return operandEquals(value, o['eq'], kind)
2297
2319
  // Comparison object: AND-combine whichever of eq/gt/gte/lt/lte are present.
2298
2320
  const n = toBig(value)
2299
2321
  let ok = true
@@ -2510,31 +2532,124 @@ function toLogEvent(
2510
2532
  }
2511
2533
  }
2512
2534
 
2535
+ /** Narrows an exact or membership operator into an RPC filter value. */
2536
+ function pushValue(op: unknown): unknown {
2537
+ const value = (() => {
2538
+ if (typeof op !== 'object' || op === null) return op
2539
+ const operator = op as Record<string, unknown>
2540
+ if ('eq' in operator) return operator['eq']
2541
+ if (Array.isArray(operator['in']) && operator['in'].length > 0) return operator['in']
2542
+ return undefined
2543
+ })()
2544
+ const scalar = (item: unknown) => ['bigint', 'boolean', 'number', 'string'].includes(typeof item)
2545
+ if (scalar(value)) return value
2546
+ if (Array.isArray(value) && value.length > 0 && value.every(scalar)) return value
2547
+ return undefined
2548
+ }
2549
+
2550
+ /** Narrows an `eq`/`in` operator into a hex RPC filter value. */
2551
+ function pushHex(op: unknown): `0x${string}` | `0x${string}`[] | undefined {
2552
+ const value = pushValue(op)
2553
+ if (typeof value === 'string') return value as `0x${string}`
2554
+ if (Array.isArray(value) && value.every((item) => typeof item === 'string'))
2555
+ return value as `0x${string}`[]
2556
+ return undefined
2557
+ }
2558
+
2513
2559
  /** Narrows an `eq`/`in` address operator into the `getLogs` address pushdown. */
2514
- function pushAddress(op: unknown): `0x${string}` | `0x${string}`[] | undefined {
2515
- if (op === undefined) return undefined
2516
- if (typeof op === 'string') return op as `0x${string}`
2517
- if (typeof op === 'object' && op !== null) {
2518
- const o = op as Record<string, unknown>
2519
- if (typeof o['eq'] === 'string') return o['eq'] as `0x${string}`
2520
- if (Array.isArray(o['in']))
2521
- return o['in'].filter((v): v is string => typeof v === 'string') as `0x${string}`[]
2560
+ function pushAddress(op: unknown) {
2561
+ return pushHex(op)
2562
+ }
2563
+
2564
+ /** Parses a human-readable event signature without letting refinements throw. */
2565
+ function parseEvent(signature: string | undefined): AbiEvent | undefined {
2566
+ if (!signature) return undefined
2567
+ try {
2568
+ const item = parseAbiItem(signature)
2569
+ return item.type === 'event' ? item : undefined
2570
+ } catch {
2571
+ return undefined
2572
+ }
2573
+ }
2574
+
2575
+ /** Returns exact indexed arguments that viem can push into `eth_getLogs`. */
2576
+ function pushEventArgs(args: Record<string, unknown> | undefined, event: AbiEvent) {
2577
+ const pushed: Record<string, unknown> = {}
2578
+ for (const [name, op] of Object.entries(args ?? {})) {
2579
+ const input = event.inputs.find((item) => item.indexed && item.name === name)
2580
+ const value = pushValue(op)
2581
+ if (input && value !== undefined) pushed[name] = value
2582
+ }
2583
+ return pushed
2584
+ }
2585
+
2586
+ /** Checks that the accepted filter has a selective `eth_getLogs` representation. */
2587
+ function hasLogPushdown(f: z.output<typeof schema.LogFilters>): boolean {
2588
+ if (pushAddress(f.address) !== undefined) return true
2589
+
2590
+ const secondaryTopic = [f.topic1, f.topic2, f.topic3].some(
2591
+ (topic) => pushHex(topic) !== undefined,
2592
+ )
2593
+ const event = parseEvent(f.signature)
2594
+ if (event && !event.anonymous)
2595
+ return secondaryTopic || Object.keys(pushEventArgs(f.args, event)).length > 0
2596
+ return pushHex(f.topic0) !== undefined && secondaryTopic
2597
+ }
2598
+
2599
+ /** Builds raw topic filters, preserving null wildcards between constrained positions. */
2600
+ function pushTopics(f: z.output<typeof schema.LogFilters>, event: AbiEvent | undefined) {
2601
+ const topics: (`0x${string}` | `0x${string}`[] | null | undefined)[] = [
2602
+ event ? encodeEventTopics({ abi: [event] })[0] : pushHex(f.topic0),
2603
+ pushHex(f.topic1),
2604
+ pushHex(f.topic2),
2605
+ pushHex(f.topic3),
2606
+ ]
2607
+ while (topics.at(-1) === undefined) topics.pop()
2608
+ return topics.map((topic) => topic ?? null)
2609
+ }
2610
+
2611
+ /** Converts one raw JSON-RPC log into the viem-shaped scanner input. */
2612
+ function fromRpcLog(log: RpcLog): ScanLog {
2613
+ return {
2614
+ address: log.address,
2615
+ blockNumber: log.blockNumber === null ? null : BigInt(log.blockNumber),
2616
+ data: log.data,
2617
+ logIndex: Value.hexToNumber(log.logIndex) ?? null,
2618
+ topics: log.topics,
2619
+ transactionHash: log.transactionHash,
2620
+ transactionIndex: Value.hexToNumber(log.transactionIndex) ?? null,
2621
+ }
2622
+ }
2623
+
2624
+ /** Best-effort decodes a raw log using the subscription event. */
2625
+ function decodeScanLog(log: ScanLog, event: AbiEvent): ScanLog {
2626
+ try {
2627
+ const decoded = decodeEventLog({
2628
+ abi: [event],
2629
+ data: log.data as `0x${string}`,
2630
+ strict: false,
2631
+ topics: log.topics as [`0x${string}`, ...`0x${string}`[]],
2632
+ })
2633
+ return {
2634
+ ...log,
2635
+ args: decoded.args,
2636
+ eventName: decoded.eventName,
2637
+ }
2638
+ } catch {
2639
+ return log
2522
2640
  }
2523
- // `not`/range forms can't be pushed; widen here, re-check in JS.
2524
- return undefined
2525
2641
  }
2526
2642
 
2527
2643
  /**
2528
- * Scans contract event logs over the confirmed window via viem's `getLogs`,
2529
- * then re-checks **every** filter in JS (so operators the RPC can't express
2530
- * `not`, ranges, decoded `args` still apply). Two modes:
2644
+ * Scans contract event logs over the confirmed window via `eth_getLogs`,
2645
+ * then re-checks **every** filter in JS. Operators the RPC cannot express
2646
+ * (`not`, ranges, non-indexed `args`) still apply. Two modes:
2531
2647
  * - **raw** (no `signature`): `topic0..3` are matched directly.
2532
- * - **decoded** (`signature` present): `getLogs` is given the `event`, so it
2533
- * pushes the event selector down, filters to matching logs, and best-effort
2534
- * decodes `args` — which the JS pass then matches by name.
2648
+ * - **decoded** (`signature` present): the selector, raw topics, and supported
2649
+ * indexed `args` are pushed down before the JS pass matches decoded values.
2535
2650
  *
2536
- * `address` (`eq`/`in`) is pushed down to the node; all other operators are
2537
- * re-checked in JS over the narrow window.
2651
+ * Exact and membership address/topic operators are pushed down to the node;
2652
+ * all operators are re-checked in JS over the narrow window.
2538
2653
  */
2539
2654
  async function scanLogs(options: {
2540
2655
  cache: ChainCache
@@ -2549,8 +2664,8 @@ async function scanLogs(options: {
2549
2664
  const window = scanWindow(cursor, safeBlock, maxBlocks)
2550
2665
  if (!window) return { items: [], scannedThrough: undefined }
2551
2666
 
2552
- const event = f.signature ? (parseAbiItem(f.signature) as AbiEvent) : undefined
2553
- if (event && !/^[A-Za-z_]\w*$/.test(event.name))
2667
+ const event = parseEvent(f.signature)
2668
+ if (f.signature && (!event || !/^[A-Za-z_]\w*$/.test(event.name)))
2554
2669
  throw new Webhooks.InvalidFilterError('log:emitted', [
2555
2670
  { message: 'Unsupported event signature.', path: ['signature'] },
2556
2671
  ])
@@ -2558,18 +2673,29 @@ async function scanLogs(options: {
2558
2673
  const address = pushAddress(f.address)
2559
2674
  const fromBlock = BigInt(window.from)
2560
2675
  const toBlock = BigInt(window.to)
2561
- const logs = (event
2562
- ? await client.getLogs({
2676
+ const topics = pushTopics(f, event)
2677
+ const hasRawTopics = topics.slice(1).some((topic) => topic !== null)
2678
+ const args = event ? pushEventArgs(f.args, event) : {}
2679
+ const logs = (!event || hasRawTopics
2680
+ ? (
2681
+ await rpc<RpcLog[]>(client, 'eth_getLogs', [
2682
+ {
2683
+ ...(address ? { address } : {}),
2684
+ fromBlock: blockTag(window.from),
2685
+ ...(topics.length > 0 ? { topics } : {}),
2686
+ toBlock: blockTag(window.to),
2687
+ },
2688
+ ])
2689
+ )
2690
+ .map(fromRpcLog)
2691
+ .map((log) => (event ? decodeScanLog(log, event) : log))
2692
+ : await client.getLogs({
2563
2693
  ...(address ? { address } : {}),
2694
+ ...(Object.keys(args).length > 0 ? { args: args as never } : {}),
2564
2695
  event,
2565
2696
  fromBlock,
2566
2697
  strict: false,
2567
2698
  toBlock,
2568
- })
2569
- : await client.getLogs({
2570
- ...(address ? { address } : {}),
2571
- fromBlock,
2572
- toBlock,
2573
2699
  })) as unknown as ScanLog[]
2574
2700
 
2575
2701
  logs.sort((a, b) => {
@@ -2884,7 +3010,7 @@ function toPublic(subscription: Webhooks.Subscription) {
2884
3010
  chainId: subscription.chainId,
2885
3011
  ...(subscription.context === undefined ? {} : { context: subscription.context }),
2886
3012
  createdAt: subscription.createdAt,
2887
- destination: subscription.destination,
3013
+ destination: redactDestination(subscription.destination),
2888
3014
  eventType: subscription.eventType,
2889
3015
  ...(subscription.expiresAt === undefined ? {} : { expiresAt: subscription.expiresAt }),
2890
3016
  failureCount: subscription.failureCount,
@@ -2900,7 +3026,18 @@ function toPublic(subscription: Webhooks.Subscription) {
2900
3026
 
2901
3027
  /** Public read shape plus the one-time signing secret (creation only). */
2902
3028
  function toCreated(subscription: Webhooks.Subscription) {
2903
- return { ...toPublic(subscription), secret: subscription.secret }
3029
+ return {
3030
+ ...toPublic(subscription),
3031
+ destination: subscription.destination,
3032
+ secret: subscription.secret,
3033
+ }
3034
+ }
3035
+
3036
+ /** Redacts bearer credentials from the destination returned by read endpoints. */
3037
+ function redactDestination(destination: WebhookDestination.Destination) {
3038
+ if (destination.type === 'slack') return { ...destination, url: redactedSlackUrl }
3039
+ if (destination.type === 'betterstack') return { ...destination, token: redactedBetterstackToken }
3040
+ return destination
2904
3041
  }
2905
3042
 
2906
3043
  function mutationError(c: Context<App.Environment>, cause: unknown) {
@@ -1,7 +1,7 @@
1
1
  import { nanoid } from 'nanoid'
2
2
  import type Stripe from 'stripe'
3
3
 
4
- import { postgresUrl } from '../../../test/containers.js'
4
+ import * as Runtime from '../../../test/runtime.js'
5
5
  import * as ApiKeys from '../../ApiKeys.js'
6
6
  import * as Db from '../../db/Db.js'
7
7
  import * as RequestUsage from '../../db/tables/requestUsage.js'
@@ -11,7 +11,7 @@ import * as Store from '../../internal/Store.js'
11
11
  import * as Viem from '../../internal/Viem.js'
12
12
  import * as Billing from './Billing.js'
13
13
 
14
- const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
14
+ const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
15
15
 
16
16
  /** Baseline billable mainnet sponsorship input; tests override per case. */
17
17
  const input = {
@@ -62,6 +62,7 @@ type StubSubscription = { items?: readonly Billing.MeterKind[]; status: string }
62
62
  function stubStripe(options: {
63
63
  meterEvent?: ((params: { identifier: string }) => void) | undefined
64
64
  subscriptions?: readonly StubSubscription[] | undefined
65
+ subscriptionsList?: (() => Promise<void>) | undefined
65
66
  }) {
66
67
  const meterEvents: {
67
68
  event_name: string
@@ -127,7 +128,10 @@ function stubStripe(options: {
127
128
  subscriptionsCreated.push(params)
128
129
  return {}
129
130
  },
130
- list: async () => ({ data: subscriptions }),
131
+ list: async () => {
132
+ await options.subscriptionsList?.()
133
+ return { data: subscriptions }
134
+ },
131
135
  },
132
136
  }
133
137
  return { itemsCreated, meterEvents, stripe: stripe as never as Stripe, subscriptionsCreated }
@@ -433,6 +437,51 @@ describe('createRequestUsageReporter', () => {
433
437
  await db.close()
434
438
  })
435
439
 
440
+ test('behavior: overlapping ticks cannot claim the same observed watermark twice', async () => {
441
+ const db = create()
442
+ await db.migrate()
443
+ await StripeCustomers.create(db, { orgId: 'org_1', stripeCustomerId: 'cus_1' })
444
+ await StripeCustomers.setStatus(db, 'org_1', 'active')
445
+
446
+ const paused = Promise.withResolvers<void>()
447
+ const resume = Promise.withResolvers<void>()
448
+ const delayed = stubStripe({
449
+ subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
450
+ subscriptionsList: async () => {
451
+ paused.resolve()
452
+ await resume.promise
453
+ },
454
+ })
455
+ const delayedTick = Billing.createRequestUsageReporter({
456
+ db,
457
+ now,
458
+ readCounts: async () => [{ bucketStart, count: 5, orgId: 'org_1' }],
459
+ stripe: delayed.stripe,
460
+ }).tick()
461
+
462
+ // Pause after this tick observes an empty watermark, then let an
463
+ // overlapping tick report and settle that same count first.
464
+ await paused.promise
465
+ const winner = stubStripe({
466
+ subscriptions: [{ items: ['feePayerSpend', 'apiRequests'], status: 'active' }],
467
+ })
468
+ expect(
469
+ await Billing.createRequestUsageReporter({
470
+ db,
471
+ now,
472
+ readCounts: async () => [{ bucketStart, count: 5, orgId: 'org_1' }],
473
+ stripe: winner.stripe,
474
+ }).tick(),
475
+ ).toStrictEqual({ failed: 0, reported: 1, skipped: 0 })
476
+
477
+ resume.resolve()
478
+ expect(await delayedTick).toStrictEqual({ failed: 0, reported: 0, skipped: 0 })
479
+ expect(winner.meterEvents).toHaveLength(1)
480
+ expect(delayed.meterEvents).toHaveLength(0)
481
+ expect((await RequestUsage.getBucket(db, { bucketStart, environment: 'production', orgId: 'org_1' }))?.reportedCount).toBe(5) // prettier-ignore
482
+ await db.close()
483
+ })
484
+
436
485
  test('behavior: seals only hours older than the settlement delay', async () => {
437
486
  const db = create()
438
487
  await db.migrate()
@@ -510,7 +510,8 @@ export function createRequestUsageReporter(
510
510
  subscriptions.set(orgId, subscription)
511
511
  }
512
512
 
513
- const sequence = await RequestUsage.countReported(database, key)
513
+ // The observed watermark is stable for every tick claiming this delta.
514
+ const sequence = watermark?.reportedCount ?? 0
514
515
  const identifier = usageIdentifier(environment, orgId, bucketStart, sequence)
515
516
  const inserted = await RequestUsage.insertPending(database, {
516
517
  ...key,
@@ -599,17 +600,17 @@ function startOfHour(ms: number) {
599
600
 
600
601
  /**
601
602
  * Deterministic meter-event identifier, bounded under Stripe's 100-char limit:
602
- * `rq_${env}_${sha(orgId)[:12]}_${bucketEpochHour}_${sequence}`.
603
+ * `rq_${env}_${sha(orgId)[:12]}_${bucketEpochHour}_${observedWatermark}`.
603
604
  */
604
605
  function usageIdentifier(
605
606
  environment: StripeCustomers.Record['environment'],
606
607
  orgId: string,
607
608
  bucketStart: string,
608
- sequence: number,
609
+ observedWatermark: number,
609
610
  ) {
610
611
  const hash = Hash.keccak256(Hex.fromString(orgId)).slice(2, 14)
611
612
  const epochHour = Math.floor(new Date(bucketStart).getTime() / hourMs)
612
- return `rq_${environment}_${hash}_${epochHour}_${sequence}`
613
+ return `rq_${environment}_${hash}_${epochHour}_${observedWatermark}`
613
614
  }
614
615
 
615
616
  /**
@@ -750,7 +750,12 @@ export function billing() {
750
750
  // the gate never opens (`Billing.status` reads the stored status)
751
751
  // without a subscription to invoice the spend against. A failure
752
752
  // here surfaces as 5xx and Stripe redelivers.
753
- if (status === 'active') await core_Billing.ensureSubscription(client, customer)
753
+ if (status === 'active')
754
+ await core_Billing.ensureSubscription(
755
+ client,
756
+ customer,
757
+ core_Billing.kindsFor(record.environment),
758
+ )
754
759
  await StripeCustomers.setStatus(db, record.orgId, status, record.environment)
755
760
  await syncKeyBilling(c, { environment: record.environment, orgId: record.orgId, status })
756
761
  return c.json({ received: true }, 200)
@@ -298,7 +298,13 @@ export function orgs() {
298
298
  description: 'Delete an organization and all of its projects.',
299
299
  operationId: 'deleteOrganization',
300
300
  responses: OpenApi.responses({
301
- errors: { 404: 'No accessible organization was found for this id.' },
301
+ errors: {
302
+ 404: 'No accessible organization was found for this id.',
303
+ 409: {
304
+ codes: ['sponsorships_unreported'],
305
+ description: 'Billable sponsorships are still awaiting settlement.',
306
+ },
307
+ },
302
308
  success: {
303
309
  description: 'Confirmation that the organization was deleted.',
304
310
  schema: schema.deleteOrganization.Response,
@@ -318,6 +324,8 @@ export function orgs() {
318
324
 
319
325
  const db = Db.get(c.get('db'))
320
326
  try {
327
+ if (await Organizations.hasUnreportedSponsorships(db, Auth.org(c).id))
328
+ return sponsorshipsUnreported(c)
321
329
  const kv = c.get('kv')
322
330
  if (kv)
323
331
  for (const record of await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
@@ -331,12 +339,22 @@ export function orgs() {
331
339
  200,
332
340
  )
333
341
  } catch (cause) {
342
+ if (cause instanceof Organizations.UnreportedSponsorshipsError)
343
+ return sponsorshipsUnreported(c)
334
344
  return Response.upstream(c, cause)
335
345
  }
336
346
  },
337
347
  )
338
348
  }
339
349
 
350
+ function sponsorshipsUnreported(c: Context<App.Environment>) {
351
+ return Response.error(c, {
352
+ code: 'sponsorships_unreported',
353
+ message: 'Billable sponsorships are still awaiting settlement',
354
+ status: 409,
355
+ })
356
+ }
357
+
340
358
  function serializeOrganization(
341
359
  record: Organizations.Record & { role?: Memberships.Role | undefined },
342
360
  ) {
@@ -269,3 +269,21 @@ test('documents POST /mcp and keeps it open under auth enforcement', async () =>
269
269
  )
270
270
  expect(rejected.status).toBe(401)
271
271
  })
272
+
273
+ test('applies the MCP anonymous rate limit', async () => {
274
+ const app = TestApp.create()
275
+ app.route('/', mcp({ docs: false, fetch: (request) => app.fetch(request) }))
276
+
277
+ const response = await app.fetch(
278
+ new Request(new URL('/mcp', origin), {
279
+ body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'tools/list', params: {} }),
280
+ headers: {
281
+ accept: 'application/json, text/event-stream',
282
+ 'content-type': 'application/json',
283
+ },
284
+ method: 'POST',
285
+ }),
286
+ )
287
+
288
+ expect(response.headers.get('RateLimit-Limit')).toMatchInlineSnapshot(`"100"`)
289
+ })
package/src/apps/mcp.ts CHANGED
@@ -125,10 +125,10 @@ export function mcp(options: mcp.Options) {
125
125
  const app = new Hono<App.Environment>()
126
126
  .post(
127
127
  '/mcp',
128
- Auth.policy({ apiKey: { scopes: [] }, public: true }),
128
+ Auth.policy({ apiKey: { scopes: [] }, public: { rateLimit: { perMinute: 100 } } }),
129
129
  OpenApi.describeRoute({
130
130
  description:
131
- "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.",
131
+ "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 allow 100 requests per minute; send `Authorization: Bearer <api key>` for protected tools.",
132
132
  operationId: 'mcpRequest',
133
133
  // The JSON-RPC request envelope is protocol-owned (like `/rpc`), so the
134
134
  // body documents an example rather than a schema.
@@ -450,7 +450,7 @@ describe.skipIf(runtime.mode !== 'localnet')('behavior: sponsorship', () => {
450
450
  `)
451
451
  })
452
452
 
453
- test('behavior: production keys record billable rows', async () => {
453
+ test('behavior: non-mainnet production keys record billable rows', async () => {
454
454
  const db = TestApp.database()
455
455
  const app = TestApp.create({
456
456
  auth: { keys },
@@ -1141,15 +1141,15 @@ describe.runIf(TestStripe.secretKey)(
1141
1141
  () => {
1142
1142
  // prettier-ignore
1143
1143
  const webhookSecret = `whsec_${'c'.repeat(32)}`
1144
- let node: Awaited<ReturnType<typeof Containers.startTempo>> | undefined
1144
+ let node: Awaited<ReturnType<typeof Containers.tempo>> | undefined
1145
1145
 
1146
1146
  beforeAll(async () => {
1147
- node = await Containers.startTempo({ chainId: Viem.chainId.mainnet })
1147
+ node = await Containers.tempo({ chainId: Viem.chainId.mainnet })
1148
1148
  }, 240_000)
1149
1149
 
1150
1150
  afterAll(async () => {
1151
1151
  await TestStripe.sweep()
1152
- await node?.container.stop()
1152
+ await node?.stop()
1153
1153
  })
1154
1154
 
1155
1155
  test(
@@ -1157,7 +1157,8 @@ describe.runIf(TestStripe.secretKey)(
1157
1157
  { retry: 0, timeout: 300_000 },
1158
1158
  async () => {
1159
1159
  // prettier-ignore
1160
- const { url } = node!
1160
+ const endpoint = node!.endpoints.default
1161
+ const url = `http://${endpoint.host}:${endpoint.port}`
1161
1162
  const stripe = TestStripe.client()
1162
1163
  const db = TestApp.database()
1163
1164
  // The key attributes to org_1; the row must exist for the usage read.
@@ -20,8 +20,8 @@ import * as Scope from '../../Scope.js'
20
20
 
21
21
  /**
22
22
  * The relay route group exposes wallet features at `/rpc/relay` and fee
23
- * sponsorship at `/rpc/sponsor`. Mount it after `data()`, which publishes
24
- * chain clients and supported chains.
23
+ * sponsorship at `/rpc/sponsor`. Mount it after `data()`, which publishes the
24
+ * verified-token and zone context used by relay handlers.
25
25
  *
26
26
  * ```ts
27
27
  * App.create(options).route('/', data()).route('/', relay())
@@ -2,12 +2,12 @@ import { nanoid } from 'nanoid'
2
2
  import { TxEnvelopeTempo } from 'ox/tempo'
3
3
  import type { Client } from 'viem'
4
4
 
5
- import { postgresUrl } from '../../../test/containers.js'
5
+ import * as Runtime from '../../../test/runtime.js'
6
6
  import * as Db from '../../db/Db.js'
7
7
  import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
8
8
  import * as Sponsorships from './Sponsorships.js'
9
9
 
10
- const create = () => Db.postgres({ connectionString: postgresUrl, schema: `t_${nanoid()}` })
10
+ const create = () => Db.postgres({ connectionString: Runtime.postgresUrl, schema: `t_${nanoid()}` })
11
11
 
12
12
  /** Baseline insert input; tests override per case. */
13
13
  const input = {