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.
- package/README.md +31 -3
- package/dist/ApiKey.d.ts +1 -0
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +5 -2
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +28 -1
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +34 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +7167 -4507
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +8 -0
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +11 -2
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +14 -3
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +128 -118
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +7 -0
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/scopes.d.ts +8 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/scopes.js +3 -0
- package/dist/admin/apps/scopes.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +57 -57
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +13 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +151 -0
- package/dist/analytics/Analytics.d.ts.map +1 -0
- package/dist/analytics/Analytics.js +212 -0
- package/dist/analytics/Analytics.js.map +1 -0
- package/dist/analytics/tables/requestEvents.d.ts +215 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
- package/dist/analytics/tables/requestEvents.js +264 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -0
- package/dist/apps/data/App.d.ts +1024 -624
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +36 -0
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +45 -45
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +455 -455
- package/dist/apps/data/routes/fee-amm.d.ts +101 -101
- package/dist/apps/data/routes/funding.d.ts +765 -0
- package/dist/apps/data/routes/funding.d.ts.map +1 -0
- package/dist/apps/data/routes/funding.js +383 -0
- package/dist/apps/data/routes/funding.js.map +1 -0
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +2 -8
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts +29 -36
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +29 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/transfers.d.ts +56 -56
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +45 -45
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +8 -8
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/docs/App.d.ts +4 -2
- package/dist/apps/docs/App.d.ts.map +1 -1
- package/dist/apps/docs/App.js +27 -95
- package/dist/apps/docs/App.js.map +1 -1
- package/dist/apps/docs/reference.d.ts +21 -0
- package/dist/apps/docs/reference.d.ts.map +1 -0
- package/dist/apps/docs/reference.js +112 -0
- package/dist/apps/docs/reference.js.map +1 -0
- package/dist/apps/index.d.ts +1 -0
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +1 -0
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/mcp.d.ts +76 -0
- package/dist/apps/mcp.d.ts.map +1 -0
- package/dist/apps/mcp.js +188 -0
- package/dist/apps/mcp.js.map +1 -0
- package/dist/apps/platform/App.d.ts +3005 -728
- package/dist/apps/platform/App.d.ts.map +1 -1
- package/dist/apps/platform/App.js +13 -3
- package/dist/apps/platform/App.js.map +1 -1
- package/dist/apps/platform/Billing.d.ts +279 -0
- package/dist/apps/platform/Billing.d.ts.map +1 -0
- package/dist/apps/platform/Billing.js +477 -0
- package/dist/apps/platform/Billing.js.map +1 -0
- package/dist/apps/platform/index.d.ts +1 -0
- package/dist/apps/platform/index.d.ts.map +1 -1
- package/dist/apps/platform/index.js +1 -0
- package/dist/apps/platform/index.js.map +1 -1
- package/dist/apps/platform/routes/api-keys.d.ts +341 -49
- package/dist/apps/platform/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/platform/routes/api-keys.js +115 -13
- package/dist/apps/platform/routes/api-keys.js.map +1 -1
- package/dist/apps/platform/routes/billing.d.ts +1520 -0
- package/dist/apps/platform/routes/billing.d.ts.map +1 -0
- package/dist/apps/platform/routes/billing.js +795 -0
- package/dist/apps/platform/routes/billing.js.map +1 -0
- package/dist/apps/platform/routes/invitations.d.ts +201 -83
- package/dist/apps/platform/routes/invitations.d.ts.map +1 -1
- package/dist/apps/platform/routes/invitations.js +58 -0
- package/dist/apps/platform/routes/invitations.js.map +1 -1
- package/dist/apps/platform/routes/me.d.ts +17 -17
- package/dist/apps/platform/routes/orgs.d.ts +50 -36
- package/dist/apps/platform/routes/orgs.d.ts.map +1 -1
- package/dist/apps/platform/routes/orgs.js +14 -3
- package/dist/apps/platform/routes/orgs.js.map +1 -1
- package/dist/apps/platform/routes/projects.d.ts +97 -97
- package/dist/apps/platform/routes/scopes.d.ts +107 -0
- package/dist/apps/platform/routes/scopes.d.ts.map +1 -0
- package/dist/apps/platform/routes/scopes.js +66 -0
- package/dist/apps/platform/routes/scopes.js.map +1 -0
- package/dist/apps/platform/routes/usage.d.ts +588 -0
- package/dist/apps/platform/routes/usage.d.ts.map +1 -0
- package/dist/apps/platform/routes/usage.js +391 -0
- package/dist/apps/platform/routes/usage.js.map +1 -0
- package/dist/apps/relay/App.d.ts +4 -1
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +118 -10
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
- package/dist/apps/relay/Sponsorships.js +2 -1
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/cli/Cli.d.ts +8 -1
- package/dist/cli/Cli.d.ts.map +1 -1
- package/dist/cli/Cli.js +11 -3
- package/dist/cli/Cli.js.map +1 -1
- package/dist/cli/commands/admin.d.ts +4 -0
- package/dist/cli/commands/admin.d.ts.map +1 -1
- package/dist/cli/commands/admin.js +80 -1
- package/dist/cli/commands/admin.js.map +1 -1
- package/dist/db/Db.d.ts +7 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +126 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +79 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +88 -2
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/billingSettings.d.ts +55 -0
- package/dist/db/tables/billingSettings.d.ts.map +1 -0
- package/dist/db/tables/billingSettings.js +70 -0
- package/dist/db/tables/billingSettings.js.map +1 -0
- package/dist/db/tables/invitations.js +2 -2
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +6 -2
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +10 -4
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.js +2 -2
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/requestUsage.d.ts +121 -0
- package/dist/db/tables/requestUsage.d.ts.map +1 -0
- package/dist/db/tables/requestUsage.js +150 -0
- package/dist/db/tables/requestUsage.js.map +1 -0
- package/dist/db/tables/sponsoredTransactions.d.ts +119 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +155 -2
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/stripeCustomers.d.ts +81 -0
- package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
- package/dist/db/tables/stripeCustomers.js +120 -0
- package/dist/db/tables/stripeCustomers.js.map +1 -0
- package/dist/db/tables/users.js +2 -2
- package/dist/db/tables/users.js.map +1 -1
- package/dist/handlers/internal/sponsorship.d.ts +5 -2
- package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
- package/dist/handlers/internal/sponsorship.js +19 -5
- package/dist/handlers/internal/sponsorship.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts +10 -3
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +14 -6
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/Auth.d.ts +18 -4
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +148 -76
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.d.ts.map +1 -1
- package/dist/internal/Cache.js +4 -1
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +10 -0
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +39 -8
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/Fees.d.ts +27 -0
- package/dist/internal/Fees.d.ts.map +1 -0
- package/dist/internal/Fees.js +48 -0
- package/dist/internal/Fees.js.map +1 -0
- package/dist/internal/FundingRoutes.d.ts +183 -0
- package/dist/internal/FundingRoutes.d.ts.map +1 -0
- package/dist/internal/FundingRoutes.js +490 -0
- package/dist/internal/FundingRoutes.js.map +1 -0
- package/dist/internal/Id.d.ts +15 -0
- package/dist/internal/Id.d.ts.map +1 -0
- package/dist/internal/Id.js +22 -0
- package/dist/internal/Id.js.map +1 -0
- package/dist/internal/Log.d.ts +2 -0
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +3 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +10 -1
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +8 -1
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +25 -0
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +51 -0
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Response.d.ts +1 -1
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Schema.d.ts +8 -0
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +7 -0
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +3 -3
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +4 -3
- package/src/ApiKey.ts +9 -2
- package/src/ApiKeys.test.ts +51 -0
- package/src/ApiKeys.ts +50 -1
- package/src/App.test.ts +33 -86718
- package/src/App.ts +18 -0
- package/src/Client.test-d.ts +351 -0
- package/src/Scope.ts +16 -3
- package/src/admin/apps/scopes.test.ts +6 -0
- package/src/admin/apps/scopes.ts +3 -0
- package/src/admin/ui/src/components/Page.tsx +2 -3
- package/src/admin/ui/src/routes/__root.tsx +2 -2
- package/src/admin/ui/src/routes/api-keys.tsx +2 -2
- package/src/admin/ui.generated.ts +13 -13
- package/src/analytics/Analytics.test.ts +527 -0
- package/src/analytics/Analytics.ts +318 -0
- package/src/analytics/tables/requestEvents.ts +523 -0
- package/src/apps/data/App.test.ts +72 -0
- package/src/apps/data/App.ts +35 -0
- package/src/apps/data/routes/funding.test.ts +214 -0
- package/src/apps/data/routes/funding.ts +584 -0
- package/src/apps/data/routes/receipts.ts +2 -9
- package/src/apps/data/routes/rpc.test.ts +78 -2
- package/src/apps/data/routes/rpc.ts +31 -1
- package/src/apps/data/routes/webhooks.test.ts +1 -1
- package/src/apps/data/routes/webhooks.ts +11 -8
- package/src/apps/docs/App.ts +29 -100
- package/src/apps/docs/reference.ts +132 -0
- package/src/apps/index.ts +1 -0
- package/src/apps/mcp.test.ts +206 -0
- package/src/apps/mcp.ts +250 -0
- package/src/apps/platform/App.ts +43 -4
- package/src/apps/platform/Billing.test.ts +530 -0
- package/src/apps/platform/Billing.ts +682 -0
- package/src/apps/platform/index.ts +1 -0
- package/src/apps/platform/routes/api-keys.test.ts +172 -9
- package/src/apps/platform/routes/api-keys.ts +136 -20
- package/src/apps/platform/routes/billing.test.ts +1100 -0
- package/src/apps/platform/routes/billing.ts +966 -0
- package/src/apps/platform/routes/invitations.test.ts +55 -7
- package/src/apps/platform/routes/invitations.ts +68 -0
- package/src/apps/platform/routes/me.test.ts +230 -0
- package/src/apps/platform/routes/orgs.ts +26 -4
- package/src/apps/platform/routes/scopes.test.ts +29 -0
- package/src/apps/platform/routes/scopes.ts +85 -0
- package/src/apps/platform/routes/usage.test.ts +422 -0
- package/src/apps/platform/routes/usage.ts +527 -0
- package/src/apps/relay/App.test.ts +457 -8
- package/src/apps/relay/App.ts +120 -12
- package/src/apps/relay/Sponsorships.test.ts +1 -1
- package/src/apps/relay/Sponsorships.ts +2 -1
- package/src/cli/Cli.test.ts +25 -2
- package/src/cli/Cli.ts +17 -2
- package/src/cli/commands/admin.test.ts +83 -0
- package/src/cli/commands/admin.ts +89 -1
- package/src/db/Db.ts +137 -0
- package/src/db/Schema.ts +104 -2
- package/src/db/tables/billingSettings.test.ts +87 -0
- package/src/db/tables/billingSettings.ts +108 -0
- package/src/db/tables/invitations.ts +2 -2
- package/src/db/tables/organizations.test.ts +1 -1
- package/src/db/tables/organizations.ts +15 -5
- package/src/db/tables/projects.ts +2 -2
- package/src/db/tables/requestUsage.ts +231 -0
- package/src/db/tables/sponsoredTransactions.test.ts +108 -1
- package/src/db/tables/sponsoredTransactions.ts +230 -2
- package/src/db/tables/stripeCustomers.test.ts +109 -0
- package/src/db/tables/stripeCustomers.ts +162 -0
- package/src/db/tables/users.ts +2 -2
- package/src/handlers/internal/sponsorship.ts +34 -7
- package/src/handlers/internal/utils.ts +19 -7
- package/src/index.ts +1 -0
- package/src/internal/Auth.test.ts +175 -25
- package/src/internal/Auth.ts +175 -86
- package/src/internal/Cache.ts +5 -1
- package/src/internal/EdgeCache.test.ts +27 -4
- package/src/internal/EdgeCache.ts +47 -9
- package/src/internal/Fees.test.ts +32 -0
- package/src/internal/Fees.ts +51 -0
- package/src/internal/FundingRoutes.ts +583 -0
- package/src/internal/Id.test.ts +17 -0
- package/src/internal/Id.ts +24 -0
- package/src/internal/Log.test.ts +1 -0
- package/src/internal/Log.ts +5 -0
- package/src/internal/OpenApi.ts +12 -1
- package/src/internal/RateLimit.test.ts +84 -0
- package/src/internal/RateLimit.ts +70 -0
- package/src/internal/Response.ts +1 -1
- package/src/internal/Schema.ts +8 -0
- package/src/internal/Webhooks.test.ts +3 -3
- package/src/internal/Webhooks.ts +3 -3
package/src/internal/OpenApi.ts
CHANGED
|
@@ -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.
|
|
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 }
|
package/src/internal/Response.ts
CHANGED
|
@@ -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> = {
|
package/src/internal/Schema.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
409
|
-
expect(delivery.subscriptionId).toMatch(/^wh_\d{15}_[A-Za-z0-
|
|
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>' },
|
package/src/internal/Webhooks.ts
CHANGED
|
@@ -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}_${
|
|
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}_${
|
|
768
|
+
return `whd_${timestamp}_${Id.random()}`
|
|
769
769
|
}
|
|
770
770
|
|
|
771
771
|
function generateSecret(): string {
|