tapimo 0.7.2 → 0.9.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 +10 -7
- package/dist/ApiKey.d.ts +3 -0
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +10 -0
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +7 -3
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +9 -2
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +20117 -19183
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +61 -4
- package/dist/App.js.map +1 -1
- package/dist/admin/App.d.ts +84 -74
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +14 -1
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +6 -2
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +2 -0
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +11 -11
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +67 -67
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +2 -0
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +3 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/apps/data/App.d.ts +11696 -13937
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +4 -8
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +271 -271
- package/dist/apps/data/routes/addresses.d.ts +398 -167
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +278 -8
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +114 -2
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +123 -123
- package/dist/apps/data/routes/coingecko.d.ts +170 -170
- package/dist/apps/data/routes/earn.d.ts +498 -6
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +876 -4
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +10 -10
- package/dist/apps/data/routes/fee-amm.d.ts +2 -2
- package/dist/apps/data/routes/indexer.d.ts +6 -6
- package/dist/apps/data/routes/indexer.js +4 -1
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts +235 -235
- package/dist/apps/data/routes/rpc.d.ts +31 -31
- package/dist/apps/data/routes/rpc.js +4 -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/tokens.d.ts +449 -449
- package/dist/apps/data/routes/transactions.d.ts +2 -2
- package/dist/apps/data/routes/transfers.d.ts +81 -81
- package/dist/apps/data/routes/valuation.d.ts +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +9 -9
- package/dist/apps/data/routes/zones.d.ts +14 -14
- package/dist/apps/funding/Access.d.ts +16 -0
- package/dist/apps/funding/Access.d.ts.map +1 -0
- package/dist/apps/funding/Access.js +22 -0
- package/dist/apps/funding/Access.js.map +1 -0
- package/dist/apps/{data/routes/funding.d.ts → funding/App.d.ts} +1640 -3217
- package/dist/apps/funding/App.d.ts.map +1 -0
- package/dist/apps/funding/App.js +61 -0
- package/dist/apps/funding/App.js.map +1 -0
- package/dist/apps/funding/Inventory.d.ts +5 -0
- package/dist/apps/funding/Inventory.d.ts.map +1 -0
- package/dist/apps/funding/Inventory.js +25 -0
- package/dist/apps/funding/Inventory.js.map +1 -0
- package/dist/apps/funding/ProviderAttempts.d.ts +5 -0
- package/dist/apps/funding/ProviderAttempts.d.ts.map +1 -0
- package/dist/apps/funding/ProviderAttempts.js +24 -0
- package/dist/apps/funding/ProviderAttempts.js.map +1 -0
- package/dist/apps/funding/index.d.ts +2 -0
- package/dist/apps/funding/index.d.ts.map +1 -0
- package/dist/apps/funding/index.js +2 -0
- package/dist/apps/funding/index.js.map +1 -0
- package/dist/apps/funding/routes/chains.d.ts +169 -0
- package/dist/apps/funding/routes/chains.d.ts.map +1 -0
- package/dist/apps/funding/routes/chains.js +80 -0
- package/dist/apps/funding/routes/chains.js.map +1 -0
- package/dist/apps/funding/routes/deposit-addresses.d.ts +1997 -0
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
- package/dist/apps/funding/routes/deposit-addresses.js +778 -0
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -0
- package/dist/apps/funding/routes/providers.d.ts +91 -0
- package/dist/apps/funding/routes/providers.d.ts.map +1 -0
- package/dist/apps/funding/routes/providers.js +56 -0
- package/dist/apps/funding/routes/providers.js.map +1 -0
- package/dist/apps/funding/routes/quotes.d.ts +426 -0
- package/dist/apps/funding/routes/quotes.d.ts.map +1 -0
- package/dist/apps/funding/routes/quotes.js +188 -0
- package/dist/apps/funding/routes/quotes.js.map +1 -0
- package/dist/apps/funding/routes/transfers.d.ts +1999 -0
- package/dist/apps/funding/routes/transfers.d.ts.map +1 -0
- package/dist/apps/funding/routes/transfers.js +614 -0
- package/dist/apps/funding/routes/transfers.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/management/App.d.ts +430 -165
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +298 -21
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +75 -1
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +6 -6
- package/dist/apps/management/routes/faucet.d.ts +35 -35
- package/dist/apps/management/routes/invitations.d.ts +122 -122
- package/dist/apps/management/routes/invite-links.d.ts +38 -38
- package/dist/apps/management/routes/members.d.ts +69 -69
- package/dist/apps/management/routes/orgs.d.ts +99 -99
- package/dist/apps/management/routes/projects.d.ts +25 -25
- package/dist/apps/management/routes/scopes.d.ts +1 -1
- package/dist/apps/management/routes/usage.d.ts +72 -72
- package/dist/apps/management/routes/verified-token-requests.d.ts +77 -77
- package/dist/apps/management/routes/webhooks.d.ts +128 -128
- package/dist/apps/mcp.d.ts +2 -3
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +16 -14
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/metadata.d.ts +4 -0
- package/dist/apps/metadata.d.ts.map +1 -1
- package/dist/apps/metadata.js +10 -0
- package/dist/apps/metadata.js.map +1 -1
- package/dist/apps/mpp/App.d.ts +44 -44
- package/dist/cloudflare.d.ts +50 -0
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +178 -0
- package/dist/cloudflare.js.map +1 -1
- package/dist/db/Db.d.ts +2 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +45 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +12 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +20 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.d.ts +13 -0
- package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.js +12 -0
- package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
- package/dist/db/tables/fundingDepositRequestObservations.d.ts +45 -0
- package/dist/db/tables/fundingDepositRequestObservations.d.ts.map +1 -0
- package/dist/db/tables/fundingDepositRequestObservations.js +46 -0
- package/dist/db/tables/fundingDepositRequestObservations.js.map +1 -0
- package/dist/db/tables/fundingDeposits.d.ts +74 -1
- package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
- package/dist/db/tables/fundingDeposits.js +126 -0
- package/dist/db/tables/fundingDeposits.js.map +1 -1
- package/dist/internal/Auth.d.ts +15 -11
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +52 -17
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.js +1 -1
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +2 -2
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +10 -4
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/IpAllowlist.d.ts +15 -0
- package/dist/internal/IpAllowlist.d.ts.map +1 -0
- package/dist/internal/IpAllowlist.js +27 -0
- package/dist/internal/IpAllowlist.js.map +1 -0
- package/dist/internal/OpenApi.d.ts +1 -1
- package/dist/internal/OpenApi.js +1 -1
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/Tidx.d.ts +6 -0
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +13 -3
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Viem.d.ts +8 -0
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +12 -5
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/funding/Deposit.d.ts +42 -0
- package/dist/internal/funding/Deposit.d.ts.map +1 -1
- package/dist/internal/funding/Deposit.js +37 -5
- package/dist/internal/funding/Deposit.js.map +1 -1
- package/dist/internal/funding/Evidence.d.ts +1 -5
- package/dist/internal/funding/Evidence.d.ts.map +1 -1
- package/dist/internal/funding/Evidence.js +4 -3
- package/dist/internal/funding/Evidence.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +102 -0
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +5 -0
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/Reconciliation.d.ts +16 -2
- package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
- package/dist/internal/funding/Reconciliation.js +348 -54
- package/dist/internal/funding/Reconciliation.js.map +1 -1
- package/dist/internal/funding/SourceObservation.d.ts +108 -0
- package/dist/internal/funding/SourceObservation.d.ts.map +1 -0
- package/dist/internal/funding/SourceObservation.js +143 -0
- package/dist/internal/funding/SourceObservation.js.map +1 -0
- package/dist/internal/funding/Subsidy.d.ts +2 -2
- package/dist/internal/funding/Subsidy.d.ts.map +1 -1
- package/dist/internal/funding/Subsidy.js +1 -1
- package/dist/internal/funding/Subsidy.js.map +1 -1
- package/dist/internal/funding/index.d.ts +1 -0
- package/dist/internal/funding/index.d.ts.map +1 -1
- package/dist/internal/funding/index.js +1 -0
- package/dist/internal/funding/index.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts +43 -1
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +438 -27
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +3 -3
- package/src/ApiKey.test.ts +35 -0
- package/src/ApiKey.ts +27 -0
- package/src/ApiKeys.test.ts +29 -0
- package/src/ApiKeys.ts +11 -3
- package/src/App.test.ts +55 -5
- package/src/App.ts +79 -5
- package/src/Client.test-d.ts +203 -28
- package/src/admin/apps/api-keys.test.ts +25 -1
- package/src/admin/apps/api-keys.ts +6 -2
- package/src/admin/ui.generated.ts +67 -67
- package/src/analytics/Analytics.ts +4 -0
- package/src/apps/composition.test.ts +130 -3
- package/src/apps/data/App.test.ts +0 -12
- package/src/apps/data/App.ts +4 -8
- package/src/apps/data/routes/balances.test.ts +42 -0
- package/src/apps/data/routes/balances.ts +218 -85
- package/src/apps/data/routes/earn.test.ts +363 -0
- package/src/apps/data/routes/earn.ts +1141 -3
- package/src/apps/data/routes/indexer.ts +2 -1
- package/src/apps/data/routes/rpc.ts +2 -0
- package/src/apps/data/routes/tokens.test.ts +6 -8
- package/src/apps/funding/Access.ts +26 -0
- package/src/apps/funding/App.test.ts +78 -0
- package/src/apps/funding/App.ts +78 -0
- package/src/apps/funding/Inventory.ts +30 -0
- package/src/apps/funding/ProviderAttempts.ts +26 -0
- package/src/apps/funding/index.ts +1 -0
- package/src/apps/funding/routes/chains.test.ts +78 -0
- package/src/apps/funding/routes/chains.ts +101 -0
- package/src/apps/funding/routes/deposit-addresses.test.ts +1649 -0
- package/src/apps/funding/routes/deposit-addresses.ts +932 -0
- package/src/apps/funding/routes/providers.test.ts +165 -0
- package/src/apps/funding/routes/providers.ts +73 -0
- package/src/apps/funding/routes/quotes.test.ts +511 -0
- package/src/apps/funding/routes/quotes.ts +205 -0
- package/src/apps/funding/routes/transfers.test.ts +1236 -0
- package/src/apps/funding/routes/transfers.ts +704 -0
- package/src/apps/index.ts +1 -0
- package/src/apps/management/routes/api-keys.test.ts +98 -0
- package/src/apps/management/routes/api-keys.ts +96 -1
- package/src/apps/mcp.test.ts +17 -2
- package/src/apps/mcp.ts +18 -13
- package/src/apps/metadata.ts +12 -0
- package/src/apps/mpp/internal/Policy.test.ts +7 -1
- package/src/cloudflare.ts +279 -0
- package/src/db/Db.ts +71 -0
- package/src/db/Schema.ts +24 -0
- package/src/db/tables/fundingDepositAddresses.ts +28 -0
- package/src/db/tables/fundingDepositRequestObservations.test.ts +106 -0
- package/src/db/tables/fundingDepositRequestObservations.ts +99 -0
- package/src/db/tables/fundingDeposits.test.ts +239 -0
- package/src/db/tables/fundingDeposits.ts +214 -1
- package/src/internal/Auth.test.ts +205 -5
- package/src/internal/Auth.ts +63 -19
- package/src/internal/Cache.test.ts +37 -3
- package/src/internal/Cache.ts +1 -1
- package/src/internal/EdgeCache.test.ts +7 -7
- package/src/internal/EdgeCache.ts +13 -4
- package/src/internal/IpAllowlist.test.ts +52 -0
- package/src/internal/IpAllowlist.ts +39 -0
- package/src/internal/Log.test.ts +7 -1
- package/src/internal/OpenApi.ts +1 -1
- package/src/internal/Tidx.test.ts +28 -0
- package/src/internal/Tidx.ts +19 -3
- package/src/internal/Viem.test.ts +28 -0
- package/src/internal/Viem.ts +21 -5
- package/src/internal/funding/Deposit.ts +74 -5
- package/src/internal/funding/Evidence.test.ts +2 -2
- package/src/internal/funding/Evidence.ts +4 -9
- package/src/internal/funding/Provider.ts +115 -0
- package/src/internal/funding/Reconciliation.local.test.ts +7 -0
- package/src/internal/funding/Reconciliation.test.ts +1364 -109
- package/src/internal/funding/Reconciliation.ts +467 -88
- package/src/internal/funding/SourceObservation.test.ts +301 -0
- package/src/internal/funding/SourceObservation.ts +267 -0
- package/src/internal/funding/Subsidy.ts +2 -2
- package/src/internal/funding/index.test.ts +1 -0
- package/src/internal/funding/index.ts +1 -0
- package/src/internal/funding/providers/relay.test.ts +94 -4
- package/src/internal/funding/providers/relay.ts +533 -27
- package/dist/apps/data/routes/funding.d.ts.map +0 -1
- package/dist/apps/data/routes/funding.js +0 -1557
- package/dist/apps/data/routes/funding.js.map +0 -1
- package/src/apps/data/routes/funding.test.ts +0 -2994
- package/src/apps/data/routes/funding.ts +0 -1821
|
@@ -21,6 +21,7 @@ import * as TestStore from '../../test/Store.js'
|
|
|
21
21
|
import * as Tempo from '../../test/Tempo.js'
|
|
22
22
|
|
|
23
23
|
const key = {
|
|
24
|
+
allowedIps: [],
|
|
24
25
|
environment: 'production',
|
|
25
26
|
id: 'key_test',
|
|
26
27
|
name: 'Test key',
|
|
@@ -295,6 +296,135 @@ describe('require', () => {
|
|
|
295
296
|
}
|
|
296
297
|
`)
|
|
297
298
|
})
|
|
299
|
+
|
|
300
|
+
test.each([
|
|
301
|
+
['tempo-api-key', '/protected', { 'tempo-api-key': token }],
|
|
302
|
+
['Bearer', '/protected', { authorization: `Bearer ${token}` }],
|
|
303
|
+
['x-api-key', '/protected', { 'x-api-key': token }],
|
|
304
|
+
['query key', `/protected?key=${token}`, {}],
|
|
305
|
+
] as const)('enforces an allowlist for the %s credential', async (_name, path, credential) => {
|
|
306
|
+
const app = createApp({
|
|
307
|
+
auth: {
|
|
308
|
+
apiKey: {
|
|
309
|
+
resolve: TestApp.resolver({
|
|
310
|
+
keys: [{ ...key, allowedIps: ['203.0.113.0/24'], token }],
|
|
311
|
+
}),
|
|
312
|
+
},
|
|
313
|
+
rateLimit: RateLimit.memory(),
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
const response = await app.request(path, {
|
|
317
|
+
headers: { ...credential, 'cf-connecting-ip': '198.51.100.7' },
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
expect(response.status).toBe(403)
|
|
321
|
+
expect(await response.json()).toMatchObject({ error: { code: 'api_key_ip_forbidden' } })
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('rejects a missing client IP', async () => {
|
|
325
|
+
const app = createApp({
|
|
326
|
+
auth: {
|
|
327
|
+
apiKey: {
|
|
328
|
+
resolve: TestApp.resolver({
|
|
329
|
+
keys: [{ ...key, allowedIps: ['203.0.113.0/24'], token }],
|
|
330
|
+
}),
|
|
331
|
+
},
|
|
332
|
+
rateLimit: RateLimit.memory(),
|
|
333
|
+
},
|
|
334
|
+
})
|
|
335
|
+
const response = await app.request('/protected', {
|
|
336
|
+
headers: { 'tempo-api-key': token },
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
expect(response.status).toBe(403)
|
|
340
|
+
expect(await response.json()).toMatchObject({ error: { code: 'api_key_ip_forbidden' } })
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
test('does not trust a Cloudflare IP header outside Cloudflare', async () => {
|
|
344
|
+
const app = createApp({
|
|
345
|
+
auth: {
|
|
346
|
+
apiKey: {
|
|
347
|
+
resolve: TestApp.resolver({
|
|
348
|
+
keys: [{ ...key, allowedIps: ['203.0.113.0/24'], token }],
|
|
349
|
+
}),
|
|
350
|
+
},
|
|
351
|
+
rateLimit: RateLimit.memory(),
|
|
352
|
+
},
|
|
353
|
+
})
|
|
354
|
+
const response = await app.request('/protected', {
|
|
355
|
+
headers: {
|
|
356
|
+
'cf-connecting-ip': '203.0.113.7',
|
|
357
|
+
'tempo-api-key': token,
|
|
358
|
+
},
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
expect(response.status).toBe(403)
|
|
362
|
+
expect(await response.json()).toMatchObject({ error: { code: 'api_key_ip_forbidden' } })
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('uses the Cloudflare IP header with Cloudflare request metadata', async () => {
|
|
366
|
+
const app = createApp({
|
|
367
|
+
auth: {
|
|
368
|
+
apiKey: {
|
|
369
|
+
resolve: TestApp.resolver({
|
|
370
|
+
keys: [{ ...key, allowedIps: ['203.0.113.0/24'], token }],
|
|
371
|
+
}),
|
|
372
|
+
},
|
|
373
|
+
rateLimit: RateLimit.memory(),
|
|
374
|
+
},
|
|
375
|
+
})
|
|
376
|
+
const request = Object.assign(
|
|
377
|
+
new Request('http://localhost/protected', {
|
|
378
|
+
headers: {
|
|
379
|
+
'cf-connecting-ip': '203.0.113.7',
|
|
380
|
+
'tempo-api-key': token,
|
|
381
|
+
},
|
|
382
|
+
}),
|
|
383
|
+
{ cf: {} },
|
|
384
|
+
)
|
|
385
|
+
const response = await app.fetch(request)
|
|
386
|
+
|
|
387
|
+
expect(response.status).toBe(200)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
test('uses the configured trusted client-IP resolver', async () => {
|
|
391
|
+
const app = createApp({
|
|
392
|
+
auth: {
|
|
393
|
+
apiKey: {
|
|
394
|
+
resolve: TestApp.resolver({
|
|
395
|
+
keys: [{ ...key, allowedIps: ['203.0.113.0/24'], token }],
|
|
396
|
+
}),
|
|
397
|
+
},
|
|
398
|
+
publicClientIp: (request) => request.headers.get('x-real-ip') ?? undefined,
|
|
399
|
+
rateLimit: RateLimit.memory(),
|
|
400
|
+
},
|
|
401
|
+
})
|
|
402
|
+
const response = await app.request('/protected', {
|
|
403
|
+
headers: {
|
|
404
|
+
'cf-connecting-ip': '198.51.100.7',
|
|
405
|
+
'tempo-api-key': token,
|
|
406
|
+
'x-real-ip': '203.0.113.7',
|
|
407
|
+
},
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
expect(response.status).toBe(200)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test('treats an empty allowlist as unrestricted', async () => {
|
|
414
|
+
const app = createApp({
|
|
415
|
+
auth: {
|
|
416
|
+
apiKey: {
|
|
417
|
+
resolve: TestApp.resolver({ keys: [{ ...key, allowedIps: [], token }] }),
|
|
418
|
+
},
|
|
419
|
+
rateLimit: RateLimit.memory(),
|
|
420
|
+
},
|
|
421
|
+
})
|
|
422
|
+
const response = await app.request('/protected', {
|
|
423
|
+
headers: { 'tempo-api-key': token },
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
expect(response.status).toBe(200)
|
|
427
|
+
})
|
|
298
428
|
})
|
|
299
429
|
|
|
300
430
|
describe('session', () => {
|
|
@@ -383,6 +513,37 @@ describe('require', () => {
|
|
|
383
513
|
`)
|
|
384
514
|
})
|
|
385
515
|
|
|
516
|
+
test('applies the session-only route error before key restrictions', async () => {
|
|
517
|
+
const app = createApp({
|
|
518
|
+
auth: {
|
|
519
|
+
apiKey: {
|
|
520
|
+
resolve: TestApp.resolver({
|
|
521
|
+
keys: [{ ...key, allowedIps: ['203.0.113.0/24'], token }],
|
|
522
|
+
}),
|
|
523
|
+
},
|
|
524
|
+
publicClientIp: (request) => request.headers.get('x-real-ip') ?? undefined,
|
|
525
|
+
rateLimit: RateLimit.memory(),
|
|
526
|
+
},
|
|
527
|
+
policy: { session: true },
|
|
528
|
+
})
|
|
529
|
+
const response = await app.request('/protected', {
|
|
530
|
+
headers: {
|
|
531
|
+
'tempo-api-key': token,
|
|
532
|
+
'x-real-ip': '198.51.100.7',
|
|
533
|
+
},
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
expect(response.status).toBe(403)
|
|
537
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
538
|
+
{
|
|
539
|
+
"error": {
|
|
540
|
+
"code": "forbidden",
|
|
541
|
+
"message": "API key not permitted for this route",
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
`)
|
|
545
|
+
})
|
|
546
|
+
|
|
386
547
|
test('is not consulted on lane-less routes', async () => {
|
|
387
548
|
const app = createSessionApp({
|
|
388
549
|
policy: {},
|
|
@@ -1095,6 +1256,34 @@ describe('require', () => {
|
|
|
1095
1256
|
})
|
|
1096
1257
|
})
|
|
1097
1258
|
|
|
1259
|
+
test('session challenge rejects a chain outside the configured allowlist', async () => {
|
|
1260
|
+
const mpp = Object.assign(
|
|
1261
|
+
createMppServer({
|
|
1262
|
+
chainId: Viem.chainId.mainnet,
|
|
1263
|
+
getClient: ({ chainId = Viem.chainId.mainnet }) => Viem.getClient({ chainId }),
|
|
1264
|
+
}),
|
|
1265
|
+
{ sessionChainIds: new Set([Viem.chainId.mainnet, Viem.chainId.testnet]) },
|
|
1266
|
+
)
|
|
1267
|
+
const app = createApp({
|
|
1268
|
+
auth: {
|
|
1269
|
+
apiKey: { resolve: TestApp.resolver({ keys: [{ ...key, token }] }) },
|
|
1270
|
+
mpp,
|
|
1271
|
+
rateLimit: overQuota(),
|
|
1272
|
+
},
|
|
1273
|
+
policy: {
|
|
1274
|
+
mpp: { session: { amount: '1' } },
|
|
1275
|
+
public: { rateLimit: { perMinute: 1 } },
|
|
1276
|
+
},
|
|
1277
|
+
})
|
|
1278
|
+
|
|
1279
|
+
const response = await app.request('/protected?chainId=31318')
|
|
1280
|
+
const body = await response.json()
|
|
1281
|
+
|
|
1282
|
+
expect(response.status).toBe(429)
|
|
1283
|
+
expect(response.headers.has('www-authenticate')).toBe(false)
|
|
1284
|
+
expect(body).toMatchObject({ error: { code: 'payment_required' } })
|
|
1285
|
+
})
|
|
1286
|
+
|
|
1098
1287
|
test('route session overrides propagate to the challenge', async () => {
|
|
1099
1288
|
const mpp = createMppServer()
|
|
1100
1289
|
const app = createApp({
|
|
@@ -1228,7 +1417,7 @@ describe('require', () => {
|
|
|
1228
1417
|
})
|
|
1229
1418
|
})
|
|
1230
1419
|
|
|
1231
|
-
test('
|
|
1420
|
+
test('tops up the channel before signing a voucher that exceeds its deposit', async () => {
|
|
1232
1421
|
const mpp = createMppServer()
|
|
1233
1422
|
const app = createApp({
|
|
1234
1423
|
auth: {
|
|
@@ -1243,14 +1432,25 @@ describe('require', () => {
|
|
|
1243
1432
|
})
|
|
1244
1433
|
const client = createMppClient(app)
|
|
1245
1434
|
|
|
1246
|
-
// The opening deposit covers one request
|
|
1247
|
-
//
|
|
1435
|
+
// The opening deposit covers one request. The client tops up the channel
|
|
1436
|
+
// before signing the second voucher.
|
|
1248
1437
|
const first = await client.fetch('http://tempo-api.test/protected')
|
|
1249
1438
|
const second = await client.fetch('http://tempo-api.test/protected')
|
|
1250
1439
|
|
|
1251
1440
|
expect(first.status).toBe(200)
|
|
1252
|
-
expect(second.status).toBe(
|
|
1253
|
-
|
|
1441
|
+
expect(second.status).toBe(200)
|
|
1442
|
+
|
|
1443
|
+
const receipt1 = sessionReceipt(first)
|
|
1444
|
+
const receipt2 = sessionReceipt(second)
|
|
1445
|
+
expect(receipt2).toMatchObject({
|
|
1446
|
+
channelId: receipt1.channelId,
|
|
1447
|
+
spent: '2000000',
|
|
1448
|
+
})
|
|
1449
|
+
|
|
1450
|
+
const state = await Actions.channel.getStates(Tempo.client, {
|
|
1451
|
+
channel: receipt1.channelId,
|
|
1452
|
+
})
|
|
1453
|
+
expect(state.deposit).toBe(2_000_000n)
|
|
1254
1454
|
})
|
|
1255
1455
|
|
|
1256
1456
|
test('rejects a malformed payment credential with a fresh challenge', async () => {
|
package/src/internal/Auth.ts
CHANGED
|
@@ -18,6 +18,7 @@ import * as ApiKeys from '../ApiKeys.js'
|
|
|
18
18
|
import * as Db from '../db/Db.js'
|
|
19
19
|
import type * as Store from './Store.js'
|
|
20
20
|
import * as Memberships from '../db/tables/memberships.js'
|
|
21
|
+
import * as IpAllowlist from './IpAllowlist.js'
|
|
21
22
|
import * as Metadata from '../apps/metadata.js'
|
|
22
23
|
import * as OpenApi from './OpenApi.js'
|
|
23
24
|
import * as Organizations from '../db/tables/organizations.js'
|
|
@@ -116,6 +117,8 @@ type MppHandler = {
|
|
|
116
117
|
session(options: Mpp.Session): MiddlewareHandler
|
|
117
118
|
/** Resolves the chain from a challenge issued by this server. */
|
|
118
119
|
sessionChainId?: ((c: hono_Context) => number | undefined) | undefined
|
|
120
|
+
/** Chains accepted from request-driven Tempo Session payment overrides. */
|
|
121
|
+
sessionChainIds?: ReadonlySet<number> | undefined
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
/**
|
|
@@ -438,12 +441,16 @@ export function accessError(c: hono_Context) {
|
|
|
438
441
|
status: 401,
|
|
439
442
|
})
|
|
440
443
|
}
|
|
441
|
-
if (status === 403)
|
|
444
|
+
if (status === 403) {
|
|
445
|
+
const ipForbidden = c.req.header('tempo-narrow-access-code') === 'api_key_ip_forbidden'
|
|
442
446
|
return Response.error(c, {
|
|
443
|
-
code: 'api_key_forbidden',
|
|
444
|
-
message:
|
|
447
|
+
code: ipForbidden ? 'api_key_ip_forbidden' : 'api_key_forbidden',
|
|
448
|
+
message: ipForbidden
|
|
449
|
+
? 'API key not permitted from this IP address'
|
|
450
|
+
: 'API key missing required scope',
|
|
445
451
|
status: 403,
|
|
446
452
|
})
|
|
453
|
+
}
|
|
447
454
|
const code =
|
|
448
455
|
c.req.header('tempo-narrow-access-code') === 'rate_limit_exceeded'
|
|
449
456
|
? 'rate_limit_exceeded'
|
|
@@ -499,6 +506,7 @@ export function middleware<
|
|
|
499
506
|
const {
|
|
500
507
|
rateLimit: mppRateLimit = defaultMppRateLimit,
|
|
501
508
|
session: session_ = {},
|
|
509
|
+
sessionChainIds,
|
|
502
510
|
...mpp
|
|
503
511
|
} = options.mpp === false ? {} : (options.mpp ?? {})
|
|
504
512
|
const secretKey = resolveMppSecretKey(mpp.secretKey)
|
|
@@ -511,8 +519,8 @@ export function middleware<
|
|
|
511
519
|
// store is configured, every token resolves to `null` (closed).
|
|
512
520
|
// Resolution is cached per isolate by default (positive 60s / negative 10s,
|
|
513
521
|
// keyed by token sha256) so hot keys skip the backend read on the auth path
|
|
514
|
-
// of every request. Pass `apiKey.cache: false` when
|
|
515
|
-
//
|
|
522
|
+
// of every request. Pass `apiKey.cache: false` when immediate key updates
|
|
523
|
+
// matter more than the per-request read.
|
|
516
524
|
const resolve = (() => {
|
|
517
525
|
const uncached = (c: hono_Context, token: string) => {
|
|
518
526
|
const kv = contextKv(c)
|
|
@@ -560,6 +568,7 @@ export function middleware<
|
|
|
560
568
|
],
|
|
561
569
|
}).session,
|
|
562
570
|
sessionChainId: (c) => paymentSessionChainId(c, secretKey),
|
|
571
|
+
...(sessionChainIds ? { sessionChainIds: new Set(sessionChainIds) } : {}),
|
|
563
572
|
},
|
|
564
573
|
}),
|
|
565
574
|
rateLimit: RateLimit.memory(options.rateLimit),
|
|
@@ -606,8 +615,8 @@ export declare namespace middleware {
|
|
|
606
615
|
/**
|
|
607
616
|
* Resolve-cache tuning (per-isolate, positive 60s / negative 10s by
|
|
608
617
|
* default), or `false` to resolve uncached on every request — e.g.
|
|
609
|
-
* when
|
|
610
|
-
* read.
|
|
618
|
+
* when immediate key updates matter more than the per-request
|
|
619
|
+
* backend read.
|
|
611
620
|
*/
|
|
612
621
|
cache?: Cache | false | undefined
|
|
613
622
|
/** API-key quotas keyed by quota scope (reserved `'*'` is the config default). */
|
|
@@ -624,7 +633,7 @@ export declare namespace middleware {
|
|
|
624
633
|
/** Default public access policy. Pass false to disable anonymous access. */
|
|
625
634
|
public?:
|
|
626
635
|
| {
|
|
627
|
-
/** Resolves the client IP from trusted runtime or proxy metadata.
|
|
636
|
+
/** Resolves the client IP from trusted runtime or proxy metadata. Required outside Cloudflare for API-key allowlists. */
|
|
628
637
|
clientIp?: ((request: Request) => string | undefined) | undefined
|
|
629
638
|
/** Anonymous public quota for protected routes. */
|
|
630
639
|
rateLimit?: RateLimit.Limit | undefined
|
|
@@ -661,6 +670,8 @@ export declare namespace middleware {
|
|
|
661
670
|
rateLimit?: RateLimit.Limit | undefined
|
|
662
671
|
/** Global Tempo Session options passed to the MPP method instantiator. */
|
|
663
672
|
session?: NonNullable<Parameters<typeof tempo.session>[0]> | undefined
|
|
673
|
+
/** Chains accepted from request-driven Tempo Session payment overrides. Omit to preserve the MPP client's chain support. */
|
|
674
|
+
sessionChainIds?: readonly number[] | undefined
|
|
664
675
|
}
|
|
665
676
|
}
|
|
666
677
|
|
|
@@ -1448,6 +1459,28 @@ export function require<environment extends Environment = Environment>(
|
|
|
1448
1459
|
const principal = principalFromApiKey(apiKey)
|
|
1449
1460
|
c.set('principal', principal)
|
|
1450
1461
|
|
|
1462
|
+
// Default-deny: routes with no API-key lane allow only the super admin.
|
|
1463
|
+
// A valid key is authenticated but forbidden.
|
|
1464
|
+
if (!policy.apiKey)
|
|
1465
|
+
return jsonError(c, {
|
|
1466
|
+
code: 'forbidden',
|
|
1467
|
+
message: 'API key not permitted for this route',
|
|
1468
|
+
status: 403,
|
|
1469
|
+
})
|
|
1470
|
+
|
|
1471
|
+
if (
|
|
1472
|
+
apiKey.allowedIps.length &&
|
|
1473
|
+
!(await IpAllowlist.allows(c, {
|
|
1474
|
+
address: trustedClientIp(c, auth),
|
|
1475
|
+
rules: apiKey.allowedIps,
|
|
1476
|
+
}))
|
|
1477
|
+
)
|
|
1478
|
+
return jsonError(c, {
|
|
1479
|
+
code: 'api_key_ip_forbidden',
|
|
1480
|
+
message: 'API key not permitted from this IP address',
|
|
1481
|
+
status: 403,
|
|
1482
|
+
})
|
|
1483
|
+
|
|
1451
1484
|
if (
|
|
1452
1485
|
apiKey.environment === 'sandbox' &&
|
|
1453
1486
|
explicitChain !== undefined &&
|
|
@@ -1459,14 +1492,6 @@ export function require<environment extends Environment = Environment>(
|
|
|
1459
1492
|
status: 403,
|
|
1460
1493
|
})
|
|
1461
1494
|
|
|
1462
|
-
// Default-deny: routes with no API-key lane allow only the super admin.
|
|
1463
|
-
// A valid key is authenticated but forbidden.
|
|
1464
|
-
if (!policy.apiKey)
|
|
1465
|
-
return jsonError(c, {
|
|
1466
|
-
code: 'forbidden',
|
|
1467
|
-
message: 'API key not permitted for this route',
|
|
1468
|
-
status: 403,
|
|
1469
|
-
})
|
|
1470
1495
|
const missing = apiKey.scopes.includes(Scope.wildcard)
|
|
1471
1496
|
? []
|
|
1472
1497
|
: (policy.apiKey?.scopes.filter((scope) => !apiKey.scopes.includes(scope)) ?? [])
|
|
@@ -1821,6 +1846,17 @@ function getApiKeyCredential(c: hono_Context): ApiKeyCredential | false | null {
|
|
|
1821
1846
|
return null
|
|
1822
1847
|
}
|
|
1823
1848
|
|
|
1849
|
+
function clientIp(c: hono_Context, auth: Context): string | undefined {
|
|
1850
|
+
return auth.publicClientIp ? auth.publicClientIp(c.req.raw) : c.req.header('cf-connecting-ip')
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function trustedClientIp(c: hono_Context, auth: Context): string | undefined {
|
|
1854
|
+
if (auth.publicClientIp) return auth.publicClientIp(c.req.raw)
|
|
1855
|
+
// Cloudflare supplies both the request metadata and this header.
|
|
1856
|
+
if (!('cf' in c.req.raw)) return undefined
|
|
1857
|
+
return c.req.header('cf-connecting-ip')
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1824
1860
|
/** Removes the reserved auth query parameter before route validation. */
|
|
1825
1861
|
function stripApiKeyQuery(c: hono_Context) {
|
|
1826
1862
|
const url = new URL(c.req.url)
|
|
@@ -1936,7 +1972,7 @@ function principalFromApiKey(apiKey: ApiKey.ApiKey): Principal {
|
|
|
1936
1972
|
}
|
|
1937
1973
|
|
|
1938
1974
|
function principalFromPublic(c: hono_Context, auth: Context): Principal {
|
|
1939
|
-
const ip =
|
|
1975
|
+
const ip = clientIp(c, auth)
|
|
1940
1976
|
return {
|
|
1941
1977
|
id: ip ? RateLimit.normalizeIp(ip) : 'anonymous',
|
|
1942
1978
|
type: 'public',
|
|
@@ -1961,8 +1997,16 @@ async function requirePayment<environment extends Environment>(
|
|
|
1961
1997
|
}
|
|
1962
1998
|
|
|
1963
1999
|
const session = options.policy.mpp?.session ?? {}
|
|
2000
|
+
const explicit = explicitChainId(c)
|
|
2001
|
+
const sessionChainIds = options.auth.mpp?.sessionChainIds
|
|
2002
|
+
if (explicit !== undefined && sessionChainIds && !sessionChainIds.has(explicit))
|
|
2003
|
+
return jsonError(c, {
|
|
2004
|
+
code: 'payment_required',
|
|
2005
|
+
message: 'Payment is not available for the requested chain',
|
|
2006
|
+
status: 429,
|
|
2007
|
+
})
|
|
1964
2008
|
const chainId =
|
|
1965
|
-
|
|
2009
|
+
explicit ??
|
|
1966
2010
|
(c.req.method === 'POST' && session.scope?.startsWith('GET ')
|
|
1967
2011
|
? options.auth.mpp?.sessionChainId?.(c)
|
|
1968
2012
|
: undefined)
|
|
@@ -2056,7 +2100,7 @@ const cachedMaxEntries = 10_000
|
|
|
2056
2100
|
* (default 60s), negative for `negativeTtl` (default 10s) so a freshly minted
|
|
2057
2101
|
* key is usable quickly; concurrent misses for one token are single-flighted.
|
|
2058
2102
|
* Entries are keyed by the token's sha256 (`ApiKey.hash`) — never the
|
|
2059
|
-
* plaintext.
|
|
2103
|
+
* plaintext. Key updates may remain stale for up to `ttl`, the accepted
|
|
2060
2104
|
* trade-off for removing a backend read from the auth path.
|
|
2061
2105
|
*/
|
|
2062
2106
|
function cachedResolve(
|
|
@@ -52,7 +52,7 @@ describe('response', () => {
|
|
|
52
52
|
expect(etag(first)).toMatchInlineSnapshot(`"<etag>"`)
|
|
53
53
|
expect(first.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"9"`)
|
|
54
54
|
expect(first.headers.get('vary')).toMatchInlineSnapshot(
|
|
55
|
-
`"Accept-Encoding, Authorization, Tempo-API-Key"`,
|
|
55
|
+
`"Accept-Encoding, Authorization, Tempo-API-Key, X-API-Key"`,
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
expect(await second.json()).toMatchInlineSnapshot(`
|
|
@@ -66,7 +66,7 @@ describe('response', () => {
|
|
|
66
66
|
expect(second.headers.get('etag') === first.headers.get('etag')).toMatchInlineSnapshot(`true`)
|
|
67
67
|
expect(second.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"8"`)
|
|
68
68
|
expect(second.headers.get('vary')).toMatchInlineSnapshot(
|
|
69
|
-
`"Accept-Encoding, Authorization, Tempo-API-Key"`,
|
|
69
|
+
`"Accept-Encoding, Authorization, Tempo-API-Key, X-API-Key"`,
|
|
70
70
|
)
|
|
71
71
|
|
|
72
72
|
expect(third.status).toMatchInlineSnapshot(`304`)
|
|
@@ -77,11 +77,45 @@ describe('response', () => {
|
|
|
77
77
|
expect(third.headers.get('etag') === first.headers.get('etag')).toMatchInlineSnapshot(`true`)
|
|
78
78
|
expect(third.headers.get('ratelimit-remaining')).toMatchInlineSnapshot(`"8"`)
|
|
79
79
|
expect(third.headers.get('vary')).toMatchInlineSnapshot(
|
|
80
|
-
`"Accept-Encoding, Authorization, Tempo-API-Key"`,
|
|
80
|
+
`"Accept-Encoding, Authorization, Tempo-API-Key, X-API-Key"`,
|
|
81
81
|
)
|
|
82
82
|
})
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
+
test('varies cached responses by the legacy API-key header', async () => {
|
|
86
|
+
await withCaches(async () => {
|
|
87
|
+
let calls = 0
|
|
88
|
+
const app = new Hono()
|
|
89
|
+
app.get('/', Cache.response({ key: (c) => c.req.url, name: 'test' }), (c) =>
|
|
90
|
+
c.json({ calls: ++calls, token: c.req.header('x-api-key') }),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const first = await app.request('/', { headers: { 'x-api-key': 'one' } })
|
|
94
|
+
const second = await app.request('/', { headers: { 'x-api-key': 'two' } })
|
|
95
|
+
const hit = await app.request('/', { headers: { 'x-api-key': 'one' } })
|
|
96
|
+
|
|
97
|
+
expect(calls).toMatchInlineSnapshot(`2`)
|
|
98
|
+
expect(await first.json()).toMatchInlineSnapshot(`
|
|
99
|
+
{
|
|
100
|
+
"calls": 1,
|
|
101
|
+
"token": "one",
|
|
102
|
+
}
|
|
103
|
+
`)
|
|
104
|
+
expect(await second.json()).toMatchInlineSnapshot(`
|
|
105
|
+
{
|
|
106
|
+
"calls": 2,
|
|
107
|
+
"token": "two",
|
|
108
|
+
}
|
|
109
|
+
`)
|
|
110
|
+
expect(await hit.json()).toMatchInlineSnapshot(`
|
|
111
|
+
{
|
|
112
|
+
"calls": 1,
|
|
113
|
+
"token": "one",
|
|
114
|
+
}
|
|
115
|
+
`)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
85
119
|
test('stores entries with the shared policy form, not the client form', async () => {
|
|
86
120
|
await withCaches(async (stores) => {
|
|
87
121
|
const app = new Hono()
|
package/src/internal/Cache.ts
CHANGED
|
@@ -57,7 +57,7 @@ export function response<environment extends Env>(
|
|
|
57
57
|
options: response.Options<environment>,
|
|
58
58
|
): MiddlewareHandler<environment> {
|
|
59
59
|
const defaultCacheControl = options.cacheControl ?? policies.default
|
|
60
|
-
const vary = options.vary ?? ['Accept-Encoding', 'Authorization', 'Tempo-API-Key']
|
|
60
|
+
const vary = options.vary ?? ['Accept-Encoding', 'Authorization', 'Tempo-API-Key', 'X-API-Key']
|
|
61
61
|
// hono/cache suffixes its key with `::<directive>=<value>` for every
|
|
62
62
|
// configured vary directive (trimmed, lowercased, deduped). Mirror that
|
|
63
63
|
// normalization so the `no-cache` purge below deletes exactly the entry the
|
|
@@ -37,18 +37,18 @@ describe('middleware', () => {
|
|
|
37
37
|
expect(second.headers.get('vary')).toMatchInlineSnapshot(`"Accept-Encoding"`)
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
test('
|
|
40
|
+
test('bypasses the cache for credential headers', async () => {
|
|
41
41
|
const { app, store, downstreamCalls } = createApp()
|
|
42
42
|
|
|
43
|
-
// An authenticated request warms the cache; subsequent requests — whether
|
|
44
|
-
// authenticated (with any key) or anonymous — are all served from the one
|
|
45
|
-
// shared entry without re-running downstream auth/rate-limiting.
|
|
46
43
|
await app.request('/cached', { headers: { authorization: 'Bearer one' } })
|
|
44
|
+
await app.request('/cached', { headers: { authorization: 'Basic malformed' } })
|
|
45
|
+
await app.request('/cached', { headers: { authorization: 'Payment paid' } })
|
|
47
46
|
await app.request('/cached', { headers: { 'tempo-api-key': 'two' } })
|
|
48
|
-
await app.request('/cached')
|
|
47
|
+
await app.request('/cached', { headers: { 'x-api-key': 'three' } })
|
|
49
48
|
|
|
50
|
-
expect(downstreamCalls()).toMatchInlineSnapshot(`
|
|
51
|
-
expect(store.
|
|
49
|
+
expect(downstreamCalls()).toMatchInlineSnapshot(`5`)
|
|
50
|
+
expect(store.gets).toMatchInlineSnapshot(`0`)
|
|
51
|
+
expect(store.size).toMatchInlineSnapshot(`0`)
|
|
52
52
|
})
|
|
53
53
|
|
|
54
54
|
test('bypasses the cache for query API key credentials', async () => {
|
|
@@ -48,8 +48,8 @@ const volatileHeaders = [
|
|
|
48
48
|
/**
|
|
49
49
|
* Serves GET responses from the edge cache store BEFORE auth and rate-limiting
|
|
50
50
|
* run, so cache hits skip the per-request store round-trips that dominate
|
|
51
|
-
* latency
|
|
52
|
-
* auth middleware.
|
|
51
|
+
* latency. Credential-bearing requests bypass this layer so per-key policy
|
|
52
|
+
* enforcement always runs. Place this ahead of the auth middleware.
|
|
53
53
|
*
|
|
54
54
|
* Only 2xx responses from routes that opted into caching (via `Cache.response`,
|
|
55
55
|
* which publishes the `edgeCache` policy) are stored, and the policy is only
|
|
@@ -84,8 +84,8 @@ export function middleware<environment extends Env>(
|
|
|
84
84
|
store !== undefined &&
|
|
85
85
|
c.req.method === 'GET' &&
|
|
86
86
|
!c.req.raw.headers.has('range') &&
|
|
87
|
-
//
|
|
88
|
-
!
|
|
87
|
+
// Credential-bearing requests must reach auth for per-key policy enforcement.
|
|
88
|
+
!hasCredential(c.req.raw) &&
|
|
89
89
|
isCacheableRoute(c)
|
|
90
90
|
const key = eligible
|
|
91
91
|
? await routeCacheKey(c).catch(() => {
|
|
@@ -144,6 +144,15 @@ export function middleware<environment extends Env>(
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
function hasCredential(request: Request): boolean {
|
|
148
|
+
return (
|
|
149
|
+
request.headers.has('tempo-api-key') ||
|
|
150
|
+
request.headers.has('x-api-key') ||
|
|
151
|
+
request.headers.has('authorization') ||
|
|
152
|
+
new URL(request.url).searchParams.has('key')
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
147
156
|
// Marks handlers whose route publishes an edge-cache policy; the middleware
|
|
148
157
|
// pays the store lookup only when a matched handler carries it.
|
|
149
158
|
const cacheableSymbol = Symbol('tempoApi.edgeCacheable')
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
|
|
3
|
+
import * as IpAllowlist from './IpAllowlist.js'
|
|
4
|
+
|
|
5
|
+
describe('isRule', () => {
|
|
6
|
+
test.each([
|
|
7
|
+
['203.0.113.7', true],
|
|
8
|
+
['0.0.0.0/0', true],
|
|
9
|
+
['203.0.113.0/24', true],
|
|
10
|
+
['203.0.113.7/32', true],
|
|
11
|
+
['2001:db8::7', true],
|
|
12
|
+
['::/0', true],
|
|
13
|
+
['2001:db8::/32', true],
|
|
14
|
+
['2001:db8::7/128', true],
|
|
15
|
+
['::ffff:192.0.2.1', true],
|
|
16
|
+
['203.0.113.0/33', false],
|
|
17
|
+
['2001:db8::/129', false],
|
|
18
|
+
['203.0.113.0/-1', false],
|
|
19
|
+
['203.0.113.999', false],
|
|
20
|
+
['2001:db8::1%eth0', false],
|
|
21
|
+
['203.0.113.0/24/1', false],
|
|
22
|
+
[' 203.0.113.7', false],
|
|
23
|
+
['203.0.113.7 ', false],
|
|
24
|
+
['*', false],
|
|
25
|
+
['', false],
|
|
26
|
+
])('%s -> %s', (rule, expected) => {
|
|
27
|
+
expect(IpAllowlist.isRule(rule)).toBe(expected)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('allows', () => {
|
|
32
|
+
test.each([
|
|
33
|
+
['203.0.113.7', ['203.0.113.7'], true],
|
|
34
|
+
['203.0.113.7', ['0.0.0.0/0'], true],
|
|
35
|
+
['203.0.113.7', ['203.0.113.0/24'], true],
|
|
36
|
+
['203.0.113.0', ['203.0.113.0/24'], true],
|
|
37
|
+
['203.0.113.255', ['203.0.113.0/24'], true],
|
|
38
|
+
['203.0.114.7', ['203.0.113.0/24'], false],
|
|
39
|
+
['2001:db8:1::7', ['2001:db8::/32'], true],
|
|
40
|
+
['2001:db8::7', ['2001:db8::7/128'], true],
|
|
41
|
+
['2001:db8::8', ['2001:db8::7/128'], false],
|
|
42
|
+
['2001:db9::7', ['2001:db8::/32'], false],
|
|
43
|
+
['203.0.113.7', ['2001:db8::/32'], false],
|
|
44
|
+
['::ffff:192.0.2.1', ['::ffff:192.0.2.1'], true],
|
|
45
|
+
['not-an-ip', ['203.0.113.0/24'], false],
|
|
46
|
+
])('%s in %j -> %s', async (address, rules, expected) => {
|
|
47
|
+
const app = new Hono().get('/', async (c) =>
|
|
48
|
+
c.json({ allowed: await IpAllowlist.allows(c, { address, rules }) }),
|
|
49
|
+
)
|
|
50
|
+
expect(await (await app.request('/')).json()).toEqual({ allowed: expected })
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Context } from 'hono'
|
|
2
|
+
import { ipRestriction } from 'hono/ip-restriction'
|
|
3
|
+
|
|
4
|
+
/** Returns whether a value is an IPv4/IPv6 address or CIDR range. */
|
|
5
|
+
export function isRule(value: string): boolean {
|
|
6
|
+
if (value === '*' || value.trim() !== value || value.split('/').length > 2) return false
|
|
7
|
+
try {
|
|
8
|
+
ipRestriction(() => '', { allowList: [value] })
|
|
9
|
+
return true
|
|
10
|
+
} catch {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Returns whether the client address matches at least one allowlist rule. */
|
|
16
|
+
export async function allows(c: Context, options: allows.Options): Promise<boolean> {
|
|
17
|
+
const { address, rules } = options
|
|
18
|
+
if (!address) return false
|
|
19
|
+
try {
|
|
20
|
+
const denied = await ipRestriction(
|
|
21
|
+
() => address,
|
|
22
|
+
{ allowList: [...rules] },
|
|
23
|
+
() => new Response(null, { status: 403 }),
|
|
24
|
+
)(c, async () => {})
|
|
25
|
+
return denied === undefined
|
|
26
|
+
} catch {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export declare namespace allows {
|
|
32
|
+
/** Inputs used to check a client address. */
|
|
33
|
+
type Options = {
|
|
34
|
+
/** Trusted client IP address. */
|
|
35
|
+
address?: string | undefined
|
|
36
|
+
/** Exact IP and CIDR rules to match. */
|
|
37
|
+
rules: readonly string[]
|
|
38
|
+
}
|
|
39
|
+
}
|