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
@@ -89,9 +89,11 @@ export const errorCodes = {
89
89
  'webhook_not_found',
90
90
  'delivery_not_found',
91
91
  'webhooks_not_enabled',
92
+ 'billing_not_found',
92
93
  ],
93
94
  429: ['rate_limit_exceeded', 'payment_required'],
94
95
  500: ['internal_error'],
96
+ 501: ['billing_unconfigured'],
95
97
  502: ['upstream_error'],
96
98
  } as const satisfies Record<number, readonly [string, ...string[]]>
97
99
 
@@ -259,7 +261,11 @@ export function responses<schema extends z.ZodMiniType>(
259
261
  content: {
260
262
  'application/json': {
261
263
  schema: resolver(success.schema, schemaOptions),
262
- ...(success.example === undefined ? {} : { example: success.example }),
264
+ ...(success.examples === undefined
265
+ ? success.example === undefined
266
+ ? {}
267
+ : { example: success.example }
268
+ : { examples: success.examples }),
263
269
  },
264
270
  },
265
271
  description: success.description,
@@ -275,6 +281,7 @@ export function responses<schema extends z.ZodMiniType>(
275
281
  ...(errors?.[415] && { 415: error(415, 'Unsupported media type.') }),
276
282
  429: responseRef('RateLimited'),
277
283
  500: responseRef('InternalError'),
284
+ ...(errors?.[501] && { 501: error(501, 'Not implemented.') }),
278
285
  502: error(502, 'Upstream data failure.'),
279
286
  }
280
287
  }
@@ -318,6 +325,8 @@ export declare namespace responses {
318
325
  413?: ErrorOverride | undefined
319
326
  /** Override for the 415 response. Setting this includes 415 in the map. */
320
327
  415?: ErrorOverride | undefined
328
+ /** Override for the 501 response. Setting this includes 501 in the map. */
329
+ 501?: ErrorOverride | undefined
321
330
  /** Override for the 502 response. */
322
331
  502?: ErrorOverride | undefined
323
332
  }
@@ -328,6 +337,8 @@ export declare namespace responses {
328
337
  description: string
329
338
  /** Optional full example body rendered for the 200 response. */
330
339
  example?: unknown
340
+ /** Optional named example bodies rendered for the 200 response. */
341
+ examples?: Record<string, { description?: string; summary?: string; value: unknown }>
331
342
  /** Zod schema describing the 200 response body. */
332
343
  schema: schema
333
344
  }
@@ -91,3 +91,87 @@ describe('memory', () => {
91
91
  `)
92
92
  })
93
93
  })
94
+
95
+ describe('edge', () => {
96
+ function binding(success = true) {
97
+ return { limit: vi.fn(async (_: { key: string }) => ({ success })) }
98
+ }
99
+
100
+ function request(options: { asn?: number | undefined; ip?: string | undefined } = {}) {
101
+ const value = new Request('https://api.tempo.xyz/v1/health', {
102
+ headers: options.ip ? { 'cf-connecting-ip': options.ip } : {},
103
+ })
104
+ if (options.asn) Object.assign(value, { cf: { asn: options.asn } })
105
+ return value
106
+ }
107
+
108
+ test('allows when every layer is under quota', async () => {
109
+ const asn = binding()
110
+ const global = binding()
111
+ const ip = binding()
112
+
113
+ const response = await RateLimit.edge(request({ asn: 64512, ip: '203.0.113.7' }), {
114
+ asn,
115
+ global,
116
+ ip,
117
+ })
118
+
119
+ expect(response).toBeUndefined()
120
+ expect(ip.limit).toHaveBeenCalledWith({ key: '203.0.113.7' })
121
+ expect(asn.limit).toHaveBeenCalledWith({ key: 'asn:64512' })
122
+ expect(global.limit).toHaveBeenCalledWith({ key: 'global' })
123
+ })
124
+
125
+ test('buckets IPv6 clients by /64 prefix', async () => {
126
+ const ip = binding()
127
+
128
+ await RateLimit.edge(request({ ip: '2001:db8:abcd:12::1' }), { ip })
129
+
130
+ expect(ip.limit).toHaveBeenCalledWith({ key: '2001:db8:abcd:12' })
131
+ })
132
+
133
+ test('returns 429 when any layer denies', async () => {
134
+ const response = await RateLimit.edge(request({ asn: 64512, ip: '203.0.113.7' }), {
135
+ asn: binding(false),
136
+ global: binding(),
137
+ ip: binding(),
138
+ })
139
+
140
+ expect(response?.status).toBe(429)
141
+ expect(response?.headers.get('retry-after')).toBe('10')
142
+ })
143
+
144
+ test('skips the asn layer when the request has no cf metadata', async () => {
145
+ const asn = binding(false)
146
+
147
+ const response = await RateLimit.edge(request({ ip: '203.0.113.7' }), { asn })
148
+
149
+ expect(response).toBeUndefined()
150
+ expect(asn.limit).not.toHaveBeenCalled()
151
+ })
152
+
153
+ test('keys unknown clients together when the ip header is missing', async () => {
154
+ const ip = binding()
155
+
156
+ await RateLimit.edge(request(), { ip })
157
+
158
+ expect(ip.limit).toHaveBeenCalledWith({ key: 'unknown' })
159
+ })
160
+
161
+ test('fails open when a binding errors', async () => {
162
+ const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
163
+ try {
164
+ const response = await RateLimit.edge(request({ ip: '203.0.113.7' }), {
165
+ ip: {
166
+ limit() {
167
+ return Promise.reject(new Error('binding unavailable'))
168
+ },
169
+ },
170
+ })
171
+
172
+ expect(response).toBeUndefined()
173
+ } finally {
174
+ consoleError.mockRestore()
175
+ }
176
+ })
177
+ })
@@ -90,4 +90,74 @@ export declare namespace memory {
90
90
  }
91
91
  }
92
92
 
93
+ /** Cloudflare rate-limit binding (per-colo counters). */
94
+ export type Binding = {
95
+ /** Consumes one request; `success: false` when the key is over quota. */
96
+ limit(options: { key: string }): Promise<{ success: boolean }>
97
+ }
98
+
99
+ /**
100
+ * Layered edge rate limit over Cloudflare rate-limit bindings. Returns a 429
101
+ * when any configured layer denies; fails open when a binding errors.
102
+ */
103
+ export async function edge(request: Request, options: edge.Options): Promise<Response | undefined> {
104
+ try {
105
+ const checks: Promise<{ success: boolean }>[] = []
106
+ if (options.ip) checks.push(options.ip.limit({ key: clientKey(request) }))
107
+ const asn = (request as Request & { cf?: CfProperties }).cf?.asn
108
+ if (options.asn && asn !== undefined) checks.push(options.asn.limit({ key: `asn:${asn}` }))
109
+ if (options.global) checks.push(options.global.limit({ key: 'global' }))
110
+
111
+ const results = await Promise.all(checks)
112
+ if (results.some((result) => !result.success))
113
+ return new Response('Rate limit exceeded', {
114
+ headers: { 'retry-after': '10' },
115
+ status: 429,
116
+ })
117
+ } catch (error) {
118
+ console.error('Edge rate limit check failed:', error)
119
+ }
120
+ return undefined
121
+ }
122
+
123
+ export declare namespace edge {
124
+ /** Layers applied by {@link edge}; omitted layers are skipped. */
125
+ type Options = {
126
+ /** Per-network backstop; catches clients rotating addresses within one ASN. */
127
+ asn?: Binding | undefined
128
+ /** Fixed-key circuit breaker capping total throughput per colo. */
129
+ global?: Binding | undefined
130
+ /** Per-client limit, keyed by IPv4 address or IPv6 /64 prefix. */
131
+ ip?: Binding | undefined
132
+ }
133
+ }
134
+
135
+ /** Cloudflare-enriched request properties; absent outside Workers (tests, service bindings). */
136
+ type CfProperties = { asn?: number | undefined }
137
+
138
+ /** IPv4 clients key by address; IPv6 clients by /64 prefix so they cannot rotate within their allocation. */
139
+ function clientKey(request: Request): string {
140
+ const ip = request.headers.get('cf-connecting-ip')
141
+ if (!ip) return 'unknown'
142
+ if (ip.includes('.')) return ip
143
+ return ipv6Prefix(ip)
144
+ }
145
+
146
+ /** First four hextets of an IPv6 address, with `::` expanded. */
147
+ function ipv6Prefix(ip: string): string {
148
+ const [address = ''] = ip.split('%')
149
+ const [head = '', tail = ''] = address.split('::')
150
+ const left = head ? head.split(':') : []
151
+ const right = tail ? tail.split(':') : []
152
+ const groups = [
153
+ ...left,
154
+ ...Array.from({ length: Math.max(8 - left.length - right.length, 0) }, () => '0'),
155
+ ...right,
156
+ ]
157
+ return groups
158
+ .slice(0, 4)
159
+ .map((group) => Number.parseInt(group || '0', 16).toString(16))
160
+ .join(':')
161
+ }
162
+
93
163
  export { type RateLimitStore as Store }
@@ -96,7 +96,7 @@ export declare namespace error {
96
96
  }
97
97
 
98
98
  /** HTTP status code accepted by the public error helper. */
99
- type Status = 400 | 401 | 403 | 404 | 409 | 412 | 413 | 415 | 429 | 500 | 502 | 503
99
+ type Status = 400 | 401 | 403 | 404 | 409 | 412 | 413 | 415 | 429 | 500 | 501 | 502 | 503
100
100
 
101
101
  /** Options for creating a JSON error response. */
102
102
  type Options<status extends Status = Status, code extends string = string> = {
@@ -417,6 +417,14 @@ export const TotalCountCapped = z
417
417
  z.meta({ examples: [false] }),
418
418
  )
419
419
 
420
+ /**
421
+ * Resource environment (`production`/`sandbox`) query parameter shared across
422
+ * environment-scoped endpoints. Defaults to `production`.
423
+ */
424
+ export const Environment = z
425
+ ._default(z.enum(['production', 'sandbox']), 'production')
426
+ .check(z.describe('Resource environment.'), z.meta({ examples: ['sandbox'] }))
427
+
420
428
  /** Sort-direction (`asc`/`desc`) query parameter shared across list endpoints. */
421
429
  export const Order = z
422
430
  ._default(z.enum(['asc', 'desc']), 'desc')
@@ -10,7 +10,7 @@ const fixedNow = () => new Date('2026-01-01T00:00:00.000Z')
10
10
  /** Asserts the random id/secret shape, then redacts them for a stable snapshot. */
11
11
  function redact(subscription: Webhooks.Subscription | null) {
12
12
  expect(subscription).not.toBeNull()
13
- expect(subscription?.id).toMatch(/^wh_\d{15}_[A-Za-z0-9_-]{21}$/)
13
+ expect(subscription?.id).toMatch(/^wh_\d{15}_[A-Za-z0-9]{24}$/)
14
14
  expect(subscription?.secret).toMatch(/^whsec_[0-9a-f]{64}$/)
15
15
  return { ...subscription, id: '<id>', secret: '<secret>' }
16
16
  }
@@ -405,8 +405,8 @@ describe('eventId / buildEnvelope', () => {
405
405
  describe('delivery log', () => {
406
406
  /** Asserts the time-sortable delivery id shape and redacts non-deterministic fields. */
407
407
  function redactDelivery(delivery: Webhooks.Delivery) {
408
- expect(delivery.id).toMatch(/^whd_\d{15}_[A-Za-z0-9_-]{21}$/)
409
- expect(delivery.subscriptionId).toMatch(/^wh_\d{15}_[A-Za-z0-9_-]{21}$/)
408
+ expect(delivery.id).toMatch(/^whd_\d{15}_[A-Za-z0-9]{24}$/)
409
+ expect(delivery.subscriptionId).toMatch(/^wh_\d{15}_[A-Za-z0-9]{24}$/)
410
410
  return {
411
411
  ...delivery,
412
412
  envelope: { ...delivery.envelope, subscriptionId: '<subscriptionId>' },
@@ -1,4 +1,3 @@
1
- import { nanoid } from 'nanoid'
2
1
  import { Hash, Hex } from 'ox'
3
2
 
4
3
  import type * as app_Webhooks from '../apps/data/routes/webhooks.js'
@@ -6,6 +5,7 @@ import type * as Db from '../db/Db.js'
6
5
  import * as WebhookDeliveries from '../db/tables/webhookDeliveries.js'
7
6
  import * as WebhookSubscriptions from '../db/tables/webhookSubscriptions.js'
8
7
  import type * as Metrics from '../Metrics.js'
8
+ import * as Id from './Id.js'
9
9
  import * as MetricSink from './MetricSink.js'
10
10
  import * as WebhookDestination from './WebhookDestination.js'
11
11
 
@@ -758,14 +758,14 @@ function generateId(now: Date): string {
758
758
  // (mirroring delivery ids), letting listSubscriptions page newest-first; 15
759
759
  // digits leaves headroom well past the year 5138.
760
760
  const timestamp = now.getTime().toString().padStart(15, '0')
761
- return `wh_${timestamp}_${nanoid()}`
761
+ return `wh_${timestamp}_${Id.random()}`
762
762
  }
763
763
 
764
764
  function generateDeliveryId(now: Date): string {
765
765
  // Zero-pad the epoch-ms so ids sort chronologically by string comparison; 15
766
766
  // digits leaves headroom well past the year 5138.
767
767
  const timestamp = now.getTime().toString().padStart(15, '0')
768
- return `whd_${timestamp}_${nanoid()}`
768
+ return `whd_${timestamp}_${Id.random()}`
769
769
  }
770
770
 
771
771
  function generateSecret(): string {