tapimo 0.7.2 → 0.8.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 +20434 -19484
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +62 -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 +168 -168
- 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 +10305 -12785
- 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 +24 -24
- package/dist/apps/data/routes/addresses.d.ts +288 -288
- package/dist/apps/data/routes/balances.d.ts +6 -6
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/coingecko.d.ts +170 -170
- package/dist/apps/data/routes/earn.d.ts +436 -6
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +631 -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 +1 -1
- 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 +6 -6
- package/dist/apps/data/routes/transactions.d.ts +2 -2
- package/dist/apps/data/routes/transfers.d.ts +1 -1
- 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} +1717 -3035
- package/dist/apps/funding/App.d.ts.map +1 -0
- package/dist/apps/funding/App.js +46 -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 +2388 -0
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
- package/dist/apps/funding/routes/deposit-addresses.js +860 -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 +48 -48
- 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 +37 -37
- 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 +18 -18
- 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 +2 -2
- 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 +37 -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 +57 -1
- package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
- package/dist/db/tables/fundingDeposits.js +82 -0
- package/dist/db/tables/fundingDeposits.js.map +1 -1
- package/dist/internal/Auth.d.ts +11 -11
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +41 -15
- 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 +257 -48
- 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 +434 -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 +23 -5
- package/src/App.ts +76 -5
- package/src/Client.test-d.ts +171 -19
- 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 +100 -2
- package/src/apps/data/App.test.ts +0 -12
- package/src/apps/data/App.ts +4 -8
- package/src/apps/data/routes/earn.test.ts +283 -0
- package/src/apps/data/routes/earn.ts +800 -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 +63 -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 +1628 -0
- package/src/apps/funding/routes/deposit-addresses.ts +1015 -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 +1255 -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 +62 -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 +138 -0
- package/src/db/tables/fundingDeposits.ts +145 -1
- package/src/internal/Auth.test.ts +177 -5
- package/src/internal/Auth.ts +48 -18
- 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 +1052 -35
- package/src/internal/funding/Reconciliation.ts +328 -56
- 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 +528 -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
|
@@ -77,6 +77,79 @@ describe('listByAddress', () => {
|
|
|
77
77
|
})
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
+
describe('listByRecipient', () => {
|
|
81
|
+
test('pages recipient deposits across addresses and hides foreign ownership', async () => {
|
|
82
|
+
const recipient = '0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD'
|
|
83
|
+
const firstAddress = await seedAddress({
|
|
84
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
85
|
+
provider: { id: 'relay_recipient_first', name: 'Relay' },
|
|
86
|
+
recipient,
|
|
87
|
+
}),
|
|
88
|
+
})
|
|
89
|
+
const secondAddress = await seedAddress({
|
|
90
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
91
|
+
provider: { id: 'relay_recipient_second', name: 'Relay' },
|
|
92
|
+
recipient,
|
|
93
|
+
subsidize: true,
|
|
94
|
+
}),
|
|
95
|
+
})
|
|
96
|
+
const first = await seedDeposit(firstAddress)
|
|
97
|
+
const second = await seedDeposit(secondAddress)
|
|
98
|
+
const third = await seedDeposit(firstAddress)
|
|
99
|
+
const unrelatedAddress = await seedAddress()
|
|
100
|
+
await seedDeposit(unrelatedAddress)
|
|
101
|
+
const foreignAddress = await seedAddress({
|
|
102
|
+
orgId: 'org_other',
|
|
103
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
104
|
+
provider: { id: 'relay_recipient_foreign', name: 'Relay' },
|
|
105
|
+
recipient,
|
|
106
|
+
}),
|
|
107
|
+
})
|
|
108
|
+
await seedDeposit(foreignAddress)
|
|
109
|
+
|
|
110
|
+
const page = await FundingDeposits.listByRecipient(db, {
|
|
111
|
+
limit: 2,
|
|
112
|
+
owner,
|
|
113
|
+
recipient: recipient.toLowerCase(),
|
|
114
|
+
})
|
|
115
|
+
expect(page.map((row) => row.id)).toEqual([third.id, second.id])
|
|
116
|
+
const rest = await FundingDeposits.listByRecipient(db, {
|
|
117
|
+
cursor: { createdAt: second.createdAt, id: second.id },
|
|
118
|
+
limit: 2,
|
|
119
|
+
owner,
|
|
120
|
+
recipient: recipient.toLowerCase(),
|
|
121
|
+
})
|
|
122
|
+
expect(rest.map((row) => row.id)).toEqual([first.id])
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('narrows project-attributed owners to their project', async () => {
|
|
126
|
+
const recipient = `0x${'ab'.repeat(20)}`
|
|
127
|
+
const firstAddress = await seedAddress({
|
|
128
|
+
projectId: 'prj_deposit',
|
|
129
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
130
|
+
provider: { id: 'relay_recipient_project', name: 'Relay' },
|
|
131
|
+
recipient,
|
|
132
|
+
}),
|
|
133
|
+
})
|
|
134
|
+
const secondAddress = await seedAddress({
|
|
135
|
+
projectId: 'prj_other',
|
|
136
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
137
|
+
provider: { id: 'relay_recipient_other_project', name: 'Relay' },
|
|
138
|
+
recipient,
|
|
139
|
+
}),
|
|
140
|
+
})
|
|
141
|
+
const first = await seedDeposit(firstAddress)
|
|
142
|
+
await seedDeposit(secondAddress)
|
|
143
|
+
|
|
144
|
+
const rows = await FundingDeposits.listByRecipient(db, {
|
|
145
|
+
limit: 10,
|
|
146
|
+
owner: { ...owner, projectId: 'prj_deposit' },
|
|
147
|
+
recipient,
|
|
148
|
+
})
|
|
149
|
+
expect(rows.map((row) => row.id)).toEqual([first.id])
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
80
153
|
describe('getOwned', () => {
|
|
81
154
|
test('returns owned deposits and hides foreign ownership', async () => {
|
|
82
155
|
const address = await seedAddress()
|
|
@@ -227,6 +300,71 @@ describe('update', () => {
|
|
|
227
300
|
})
|
|
228
301
|
})
|
|
229
302
|
|
|
303
|
+
describe('recordRequestObservation', () => {
|
|
304
|
+
test('attaches the first poll and webhook timestamps without changing material state', async () => {
|
|
305
|
+
const address = await seedAddress()
|
|
306
|
+
const record = await seedDeposit(address, { providerRequestId: 'relay_observed' })
|
|
307
|
+
await FundingDeposits.recordRequestObservation(db, {
|
|
308
|
+
depositAddressId: address.id,
|
|
309
|
+
pollObservedAt: '2026-01-02T00:00:02.000Z',
|
|
310
|
+
providerRequestId: 'relay_observed',
|
|
311
|
+
webhookReceivedAt: '2026-01-02T00:00:03.000Z',
|
|
312
|
+
})
|
|
313
|
+
await FundingDeposits.recordRequestObservation(db, {
|
|
314
|
+
depositAddressId: address.id,
|
|
315
|
+
pollObservedAt: '2026-01-02T00:00:01.000Z',
|
|
316
|
+
providerRequestId: 'relay_observed',
|
|
317
|
+
webhookReceivedAt: '2026-01-02T00:00:04.000Z',
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
expect(await FundingDeposits.get(db, record.id)).toMatchInlineSnapshot(
|
|
321
|
+
{
|
|
322
|
+
createdAt: expect.any(String),
|
|
323
|
+
depositAddressId: expect.any(String),
|
|
324
|
+
id: expect.any(String),
|
|
325
|
+
providerOutputToken: expect.any(Object),
|
|
326
|
+
snapshot: expect.any(Object),
|
|
327
|
+
updatedAt: expect.any(String),
|
|
328
|
+
},
|
|
329
|
+
`
|
|
330
|
+
{
|
|
331
|
+
"createdAt": Any<String>,
|
|
332
|
+
"depositAddressId": Any<String>,
|
|
333
|
+
"detectionTrigger": null,
|
|
334
|
+
"environment": "production",
|
|
335
|
+
"id": Any<String>,
|
|
336
|
+
"orgId": "org_deposits",
|
|
337
|
+
"pollObservedAt": "2026-01-02T00:00:01.000Z",
|
|
338
|
+
"projectId": null,
|
|
339
|
+
"providerOutputAmount": null,
|
|
340
|
+
"providerOutputToken": Any<Object>,
|
|
341
|
+
"providerRequestId": "relay_observed",
|
|
342
|
+
"providerRequestIds": [
|
|
343
|
+
"relay_observed",
|
|
344
|
+
],
|
|
345
|
+
"providerState": null,
|
|
346
|
+
"providerTransactionHashes": [],
|
|
347
|
+
"providerTransferIndex": 0,
|
|
348
|
+
"retryState": null,
|
|
349
|
+
"settlementTransaction": null,
|
|
350
|
+
"settlementTransactionHash": null,
|
|
351
|
+
"snapshot": Any<Object>,
|
|
352
|
+
"sourceChainId": "tron:0x2b6653dc",
|
|
353
|
+
"sourceTransactionHash": "0000000000000000000000000000000000000000000000000000000000000011",
|
|
354
|
+
"sourceTransferIndex": 0,
|
|
355
|
+
"status": "detected",
|
|
356
|
+
"statusReason": null,
|
|
357
|
+
"subsidyAmount": null,
|
|
358
|
+
"tempoGasPaid": null,
|
|
359
|
+
"updatedAt": Any<String>,
|
|
360
|
+
"version": 1,
|
|
361
|
+
"webhookReceivedAt": "2026-01-02T00:00:03.000Z",
|
|
362
|
+
}
|
|
363
|
+
`,
|
|
364
|
+
)
|
|
365
|
+
})
|
|
366
|
+
})
|
|
367
|
+
|
|
230
368
|
describe('toPublic', () => {
|
|
231
369
|
test('omits private reconciliation and settlement state', async () => {
|
|
232
370
|
const address = await seedAddress()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { sql, type JSONColumnType, type Selectable } from 'kysely'
|
|
2
2
|
|
|
3
3
|
import type * as Db from '../Db.js'
|
|
4
4
|
import type * as db_Schema from '../Schema.js'
|
|
@@ -40,6 +40,63 @@ export function insert(db: Db.Db, record: Record): Promise<Record> {
|
|
|
40
40
|
.executeTakeFirstOrThrow()
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** Inserts a chain-observed source transfer or returns its existing deposit. */
|
|
44
|
+
export async function insertOrGetSource(
|
|
45
|
+
db: Db.Db,
|
|
46
|
+
record: Record & { sourceTransferIndex: number },
|
|
47
|
+
): Promise<insertOrGetSource.Result> {
|
|
48
|
+
const inserted = await db.kysely
|
|
49
|
+
.insertInto('funding_deposits')
|
|
50
|
+
.values({
|
|
51
|
+
...record,
|
|
52
|
+
providerRequestIds: JSON.stringify(record.providerRequestIds),
|
|
53
|
+
providerTransactionHashes: JSON.stringify(record.providerTransactionHashes),
|
|
54
|
+
})
|
|
55
|
+
.onConflict((oc) =>
|
|
56
|
+
oc.columns(['depositAddressId', 'sourceTransactionHash', 'sourceTransferIndex']).doNothing(),
|
|
57
|
+
)
|
|
58
|
+
.returningAll()
|
|
59
|
+
.executeTakeFirst()
|
|
60
|
+
if (inserted) return { record: inserted, type: 'created' }
|
|
61
|
+
const existing = await db.kysely
|
|
62
|
+
.selectFrom('funding_deposits')
|
|
63
|
+
.selectAll()
|
|
64
|
+
.where('depositAddressId', '=', record.depositAddressId)
|
|
65
|
+
.where('sourceTransactionHash', '=', record.sourceTransactionHash)
|
|
66
|
+
.where('sourceTransferIndex', '=', record.sourceTransferIndex)
|
|
67
|
+
.executeTakeFirstOrThrow()
|
|
68
|
+
return { record: existing, type: 'existing' }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export declare namespace insertOrGetSource {
|
|
72
|
+
/** Result of idempotently persisting one verified source transfer. */
|
|
73
|
+
type Result = { record: Record; type: 'created' | 'existing' }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Serializes source attribution for one address and transaction. */
|
|
77
|
+
export function withSourceTransaction<const result>(
|
|
78
|
+
db: Db.Db,
|
|
79
|
+
options: withSourceTransaction.Options<result>,
|
|
80
|
+
): Promise<result> {
|
|
81
|
+
return db.transaction(async (tx) => {
|
|
82
|
+
const key = `${options.depositAddressId}:${options.sourceTransactionHash.toLowerCase()}`
|
|
83
|
+
await sql`SELECT pg_advisory_xact_lock(hashtextextended(${key}, 0))`.execute(tx.kysely)
|
|
84
|
+
return options.fn(tx)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export declare namespace withSourceTransaction {
|
|
89
|
+
/** Source identity and transaction-scoped work protected by its advisory lock. */
|
|
90
|
+
type Options<result> = {
|
|
91
|
+
/** Funding deposit address that received the transfer. */
|
|
92
|
+
depositAddressId: string
|
|
93
|
+
/** Database work serialized for the source transaction. */
|
|
94
|
+
fn: (db: Db.Db) => Promise<result>
|
|
95
|
+
/** Source transaction whose deposit attribution must not race. */
|
|
96
|
+
sourceTransactionHash: string
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
43
100
|
/** Gets deposits created from one provider-observed source transaction. */
|
|
44
101
|
export function listByProviderObservation(
|
|
45
102
|
db: Db.Db,
|
|
@@ -67,6 +124,45 @@ export declare namespace listByProviderObservation {
|
|
|
67
124
|
}
|
|
68
125
|
}
|
|
69
126
|
|
|
127
|
+
/** Attaches first-observation timestamps to every deposit from one provider request. */
|
|
128
|
+
export function recordRequestObservation(
|
|
129
|
+
db: Db.Db,
|
|
130
|
+
options: recordRequestObservation.Options,
|
|
131
|
+
): Promise<Record[]> {
|
|
132
|
+
return db.kysely
|
|
133
|
+
.updateTable('funding_deposits')
|
|
134
|
+
.set({
|
|
135
|
+
...(options.pollObservedAt
|
|
136
|
+
? {
|
|
137
|
+
pollObservedAt: sql<string>`LEAST(COALESCE(poll_observed_at, ${options.pollObservedAt}), ${options.pollObservedAt})`,
|
|
138
|
+
}
|
|
139
|
+
: {}),
|
|
140
|
+
...(options.webhookReceivedAt
|
|
141
|
+
? {
|
|
142
|
+
webhookReceivedAt: sql<string>`LEAST(COALESCE(webhook_received_at, ${options.webhookReceivedAt}), ${options.webhookReceivedAt})`,
|
|
143
|
+
}
|
|
144
|
+
: {}),
|
|
145
|
+
})
|
|
146
|
+
.where('depositAddressId', '=', options.depositAddressId)
|
|
147
|
+
.where('providerRequestId', '=', options.providerRequestId)
|
|
148
|
+
.returningAll()
|
|
149
|
+
.execute()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export declare namespace recordRequestObservation {
|
|
153
|
+
/** First observations associated with one provider request. */
|
|
154
|
+
type Options = {
|
|
155
|
+
/** Funding deposit address id (`fda_…`). */
|
|
156
|
+
depositAddressId: string
|
|
157
|
+
/** When polling first observed the provider request. */
|
|
158
|
+
pollObservedAt?: string | undefined
|
|
159
|
+
/** Provider request identifier. */
|
|
160
|
+
providerRequestId: string
|
|
161
|
+
/** When Tempo first received an authenticated provider webhook. */
|
|
162
|
+
webhookReceivedAt?: string | undefined
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
70
166
|
/** Gets deposits associated with one address and source transaction. */
|
|
71
167
|
export function listBySourceTransaction(
|
|
72
168
|
db: Db.Db,
|
|
@@ -91,6 +187,20 @@ export declare namespace listBySourceTransaction {
|
|
|
91
187
|
}
|
|
92
188
|
}
|
|
93
189
|
|
|
190
|
+
/** Returns whether an address has a nonterminal source transfer awaiting provider attribution. */
|
|
191
|
+
export async function hasUnattributed(db: Db.Db, depositAddressId: string): Promise<boolean> {
|
|
192
|
+
return Boolean(
|
|
193
|
+
await db.kysely
|
|
194
|
+
.selectFrom('funding_deposits')
|
|
195
|
+
.select('id')
|
|
196
|
+
.where('depositAddressId', '=', depositAddressId)
|
|
197
|
+
.where('providerRequestId', 'is', null)
|
|
198
|
+
.where('status', 'not in', ['completed', 'refunded'])
|
|
199
|
+
.limit(1)
|
|
200
|
+
.executeTakeFirst(),
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
94
204
|
/** Gets a funding deposit without ownership scoping. */
|
|
95
205
|
export function get(db: Db.Db, id: string): Promise<Record | undefined> {
|
|
96
206
|
return db.kysely
|
|
@@ -155,6 +265,40 @@ export declare namespace listByAddress {
|
|
|
155
265
|
}
|
|
156
266
|
}
|
|
157
267
|
|
|
268
|
+
/** Lists one recipient's owner-visible deposits newest-first. */
|
|
269
|
+
export function listByRecipient(db: Db.Db, options: listByRecipient.Options): Promise<Record[]> {
|
|
270
|
+
const { cursor, limit, owner, recipient } = options
|
|
271
|
+
let query = db.kysely
|
|
272
|
+
.selectFrom('funding_deposits')
|
|
273
|
+
.selectAll()
|
|
274
|
+
.where('environment', '=', owner.environment)
|
|
275
|
+
.where('orgId', '=', owner.orgId)
|
|
276
|
+
.where(sql<boolean>`lower(snapshot->>'recipient') = ${recipient.toLowerCase()}`)
|
|
277
|
+
if (owner.projectId !== undefined) query = query.where('projectId', '=', owner.projectId)
|
|
278
|
+
if (cursor)
|
|
279
|
+
query = query.where((eb) =>
|
|
280
|
+
eb.or([
|
|
281
|
+
eb('createdAt', '<', cursor.createdAt),
|
|
282
|
+
eb.and([eb('createdAt', '=', cursor.createdAt), eb('id', '<', cursor.id)]),
|
|
283
|
+
]),
|
|
284
|
+
)
|
|
285
|
+
return query.orderBy('createdAt', 'desc').orderBy('id', 'desc').limit(limit).execute()
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export declare namespace listByRecipient {
|
|
289
|
+
/** Recipient filter, ownership, and paging options. */
|
|
290
|
+
type Options = {
|
|
291
|
+
/** Exclusive lower bound from the previous page. */
|
|
292
|
+
cursor?: listByAddress.Cursor | undefined
|
|
293
|
+
/** Maximum rows to return. */
|
|
294
|
+
limit: number
|
|
295
|
+
/** Ownership filter derived from the authenticated API key. */
|
|
296
|
+
owner: Owner
|
|
297
|
+
/** Tempo account that receives completed deposits. */
|
|
298
|
+
recipient: string
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
158
302
|
/** Applies a version-guarded deposit update. */
|
|
159
303
|
export function update(db: Db.Db, options: update.Options): Promise<Record | undefined> {
|
|
160
304
|
return db.kysely
|
|
@@ -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: {},
|
|
@@ -1228,7 +1389,7 @@ describe('require', () => {
|
|
|
1228
1389
|
})
|
|
1229
1390
|
})
|
|
1230
1391
|
|
|
1231
|
-
test('
|
|
1392
|
+
test('tops up the channel before signing a voucher that exceeds its deposit', async () => {
|
|
1232
1393
|
const mpp = createMppServer()
|
|
1233
1394
|
const app = createApp({
|
|
1234
1395
|
auth: {
|
|
@@ -1243,14 +1404,25 @@ describe('require', () => {
|
|
|
1243
1404
|
})
|
|
1244
1405
|
const client = createMppClient(app)
|
|
1245
1406
|
|
|
1246
|
-
// The opening deposit covers one request
|
|
1247
|
-
//
|
|
1407
|
+
// The opening deposit covers one request. The client tops up the channel
|
|
1408
|
+
// before signing the second voucher.
|
|
1248
1409
|
const first = await client.fetch('http://tempo-api.test/protected')
|
|
1249
1410
|
const second = await client.fetch('http://tempo-api.test/protected')
|
|
1250
1411
|
|
|
1251
1412
|
expect(first.status).toBe(200)
|
|
1252
|
-
expect(second.status).toBe(
|
|
1253
|
-
|
|
1413
|
+
expect(second.status).toBe(200)
|
|
1414
|
+
|
|
1415
|
+
const receipt1 = sessionReceipt(first)
|
|
1416
|
+
const receipt2 = sessionReceipt(second)
|
|
1417
|
+
expect(receipt2).toMatchObject({
|
|
1418
|
+
channelId: receipt1.channelId,
|
|
1419
|
+
spent: '2000000',
|
|
1420
|
+
})
|
|
1421
|
+
|
|
1422
|
+
const state = await Actions.channel.getStates(Tempo.client, {
|
|
1423
|
+
channel: receipt1.channelId,
|
|
1424
|
+
})
|
|
1425
|
+
expect(state.deposit).toBe(2_000_000n)
|
|
1254
1426
|
})
|
|
1255
1427
|
|
|
1256
1428
|
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'
|
|
@@ -438,12 +439,16 @@ export function accessError(c: hono_Context) {
|
|
|
438
439
|
status: 401,
|
|
439
440
|
})
|
|
440
441
|
}
|
|
441
|
-
if (status === 403)
|
|
442
|
+
if (status === 403) {
|
|
443
|
+
const ipForbidden = c.req.header('tempo-narrow-access-code') === 'api_key_ip_forbidden'
|
|
442
444
|
return Response.error(c, {
|
|
443
|
-
code: 'api_key_forbidden',
|
|
444
|
-
message:
|
|
445
|
+
code: ipForbidden ? 'api_key_ip_forbidden' : 'api_key_forbidden',
|
|
446
|
+
message: ipForbidden
|
|
447
|
+
? 'API key not permitted from this IP address'
|
|
448
|
+
: 'API key missing required scope',
|
|
445
449
|
status: 403,
|
|
446
450
|
})
|
|
451
|
+
}
|
|
447
452
|
const code =
|
|
448
453
|
c.req.header('tempo-narrow-access-code') === 'rate_limit_exceeded'
|
|
449
454
|
? 'rate_limit_exceeded'
|
|
@@ -511,8 +516,8 @@ export function middleware<
|
|
|
511
516
|
// store is configured, every token resolves to `null` (closed).
|
|
512
517
|
// Resolution is cached per isolate by default (positive 60s / negative 10s,
|
|
513
518
|
// 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
|
-
//
|
|
519
|
+
// of every request. Pass `apiKey.cache: false` when immediate key updates
|
|
520
|
+
// matter more than the per-request read.
|
|
516
521
|
const resolve = (() => {
|
|
517
522
|
const uncached = (c: hono_Context, token: string) => {
|
|
518
523
|
const kv = contextKv(c)
|
|
@@ -606,8 +611,8 @@ export declare namespace middleware {
|
|
|
606
611
|
/**
|
|
607
612
|
* Resolve-cache tuning (per-isolate, positive 60s / negative 10s by
|
|
608
613
|
* default), or `false` to resolve uncached on every request — e.g.
|
|
609
|
-
* when
|
|
610
|
-
* read.
|
|
614
|
+
* when immediate key updates matter more than the per-request
|
|
615
|
+
* backend read.
|
|
611
616
|
*/
|
|
612
617
|
cache?: Cache | false | undefined
|
|
613
618
|
/** API-key quotas keyed by quota scope (reserved `'*'` is the config default). */
|
|
@@ -624,7 +629,7 @@ export declare namespace middleware {
|
|
|
624
629
|
/** Default public access policy. Pass false to disable anonymous access. */
|
|
625
630
|
public?:
|
|
626
631
|
| {
|
|
627
|
-
/** Resolves the client IP from trusted runtime or proxy metadata.
|
|
632
|
+
/** Resolves the client IP from trusted runtime or proxy metadata. Required outside Cloudflare for API-key allowlists. */
|
|
628
633
|
clientIp?: ((request: Request) => string | undefined) | undefined
|
|
629
634
|
/** Anonymous public quota for protected routes. */
|
|
630
635
|
rateLimit?: RateLimit.Limit | undefined
|
|
@@ -1448,6 +1453,28 @@ export function require<environment extends Environment = Environment>(
|
|
|
1448
1453
|
const principal = principalFromApiKey(apiKey)
|
|
1449
1454
|
c.set('principal', principal)
|
|
1450
1455
|
|
|
1456
|
+
// Default-deny: routes with no API-key lane allow only the super admin.
|
|
1457
|
+
// A valid key is authenticated but forbidden.
|
|
1458
|
+
if (!policy.apiKey)
|
|
1459
|
+
return jsonError(c, {
|
|
1460
|
+
code: 'forbidden',
|
|
1461
|
+
message: 'API key not permitted for this route',
|
|
1462
|
+
status: 403,
|
|
1463
|
+
})
|
|
1464
|
+
|
|
1465
|
+
if (
|
|
1466
|
+
apiKey.allowedIps.length &&
|
|
1467
|
+
!(await IpAllowlist.allows(c, {
|
|
1468
|
+
address: trustedClientIp(c, auth),
|
|
1469
|
+
rules: apiKey.allowedIps,
|
|
1470
|
+
}))
|
|
1471
|
+
)
|
|
1472
|
+
return jsonError(c, {
|
|
1473
|
+
code: 'api_key_ip_forbidden',
|
|
1474
|
+
message: 'API key not permitted from this IP address',
|
|
1475
|
+
status: 403,
|
|
1476
|
+
})
|
|
1477
|
+
|
|
1451
1478
|
if (
|
|
1452
1479
|
apiKey.environment === 'sandbox' &&
|
|
1453
1480
|
explicitChain !== undefined &&
|
|
@@ -1459,14 +1486,6 @@ export function require<environment extends Environment = Environment>(
|
|
|
1459
1486
|
status: 403,
|
|
1460
1487
|
})
|
|
1461
1488
|
|
|
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
1489
|
const missing = apiKey.scopes.includes(Scope.wildcard)
|
|
1471
1490
|
? []
|
|
1472
1491
|
: (policy.apiKey?.scopes.filter((scope) => !apiKey.scopes.includes(scope)) ?? [])
|
|
@@ -1821,6 +1840,17 @@ function getApiKeyCredential(c: hono_Context): ApiKeyCredential | false | null {
|
|
|
1821
1840
|
return null
|
|
1822
1841
|
}
|
|
1823
1842
|
|
|
1843
|
+
function clientIp(c: hono_Context, auth: Context): string | undefined {
|
|
1844
|
+
return auth.publicClientIp ? auth.publicClientIp(c.req.raw) : c.req.header('cf-connecting-ip')
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function trustedClientIp(c: hono_Context, auth: Context): string | undefined {
|
|
1848
|
+
if (auth.publicClientIp) return auth.publicClientIp(c.req.raw)
|
|
1849
|
+
// Cloudflare supplies both the request metadata and this header.
|
|
1850
|
+
if (!('cf' in c.req.raw)) return undefined
|
|
1851
|
+
return c.req.header('cf-connecting-ip')
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1824
1854
|
/** Removes the reserved auth query parameter before route validation. */
|
|
1825
1855
|
function stripApiKeyQuery(c: hono_Context) {
|
|
1826
1856
|
const url = new URL(c.req.url)
|
|
@@ -1936,7 +1966,7 @@ function principalFromApiKey(apiKey: ApiKey.ApiKey): Principal {
|
|
|
1936
1966
|
}
|
|
1937
1967
|
|
|
1938
1968
|
function principalFromPublic(c: hono_Context, auth: Context): Principal {
|
|
1939
|
-
const ip =
|
|
1969
|
+
const ip = clientIp(c, auth)
|
|
1940
1970
|
return {
|
|
1941
1971
|
id: ip ? RateLimit.normalizeIp(ip) : 'anonymous',
|
|
1942
1972
|
type: 'public',
|
|
@@ -2056,7 +2086,7 @@ const cachedMaxEntries = 10_000
|
|
|
2056
2086
|
* (default 60s), negative for `negativeTtl` (default 10s) so a freshly minted
|
|
2057
2087
|
* key is usable quickly; concurrent misses for one token are single-flighted.
|
|
2058
2088
|
* Entries are keyed by the token's sha256 (`ApiKey.hash`) — never the
|
|
2059
|
-
* plaintext.
|
|
2089
|
+
* plaintext. Key updates may remain stale for up to `ttl`, the accepted
|
|
2060
2090
|
* trade-off for removing a backend read from the auth path.
|
|
2061
2091
|
*/
|
|
2062
2092
|
function cachedResolve(
|