tapimo 0.6.0 → 0.7.1
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/dist/App.d.ts +8641 -8263
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +9 -1
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +4 -0
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +5 -0
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +112 -11
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +2 -1
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/early-access.d.ts +2 -2
- package/dist/admin/apps/early-access.d.ts.map +1 -1
- package/dist/admin/apps/early-access.js +5 -5
- package/dist/admin/apps/early-access.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +124 -1
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/organizations.js +97 -6
- package/dist/admin/apps/organizations.js.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +47 -39
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +20 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +18 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +36 -7
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/data/App.d.ts +8050 -8019
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.js +2 -15
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
- package/dist/apps/data/FxOracle.d.ts +1 -1
- package/dist/apps/data/FxOracle.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.js +1 -1
- package/dist/apps/data/FxOracle.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +27 -27
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +21 -1
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +58 -58
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +669 -669
- package/dist/apps/data/routes/fee-amm.d.ts +157 -143
- package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
- package/dist/apps/data/routes/fee-amm.js +76 -91
- package/dist/apps/data/routes/fee-amm.js.map +1 -1
- package/dist/apps/data/routes/funding.d.ts +72 -34
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +34 -26
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +30 -30
- package/dist/apps/data/routes/receipts.d.ts +8 -8
- package/dist/apps/data/routes/rpc.d.ts +31 -31
- package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
- package/dist/apps/data/routes/tokenlist.js +12 -3
- package/dist/apps/data/routes/tokenlist.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +4 -4
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +5 -1
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +6 -6
- package/dist/apps/data/routes/transfers.d.ts +101 -90
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +226 -141
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +2 -2
- package/dist/apps/data/routes/valuation.js +3 -3
- package/dist/apps/data/routes/valuation.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/verified-tokens.js +34 -9
- package/dist/apps/data/routes/verified-tokens.js.map +1 -1
- package/dist/apps/data/routes/webhooks.d.ts +2 -2
- package/dist/apps/data/routes/webhooks.js +14 -6
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- 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 +460 -523
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +7 -23
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +20 -8
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +106 -151
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +28 -28
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +48 -34
- package/dist/apps/management/routes/faucet.d.ts.map +1 -1
- package/dist/apps/management/routes/faucet.js +33 -18
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +131 -131
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +0 -3
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/me.d.ts +6 -8
- package/dist/apps/management/routes/me.d.ts.map +1 -1
- package/dist/apps/management/routes/me.js +0 -4
- package/dist/apps/management/routes/me.js.map +1 -1
- package/dist/apps/management/routes/members.d.ts +73 -73
- package/dist/apps/management/routes/orgs.d.ts +94 -94
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +5 -0
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +72 -72
- package/dist/apps/management/routes/verified-token-requests.d.ts +94 -94
- package/dist/apps/management/routes/webhooks.d.ts +1 -1
- package/dist/apps/mpp/App.d.ts +363 -0
- package/dist/apps/mpp/App.d.ts.map +1 -0
- package/dist/apps/mpp/App.js +72 -0
- package/dist/apps/mpp/App.js.map +1 -0
- package/dist/apps/mpp/Mpp.d.ts +178 -0
- package/dist/apps/mpp/Mpp.d.ts.map +1 -0
- package/dist/apps/mpp/Mpp.js +183 -0
- package/dist/apps/mpp/Mpp.js.map +1 -0
- package/dist/apps/mpp/index.d.ts +3 -0
- package/dist/apps/mpp/index.d.ts.map +1 -0
- package/dist/apps/mpp/index.js +3 -0
- package/dist/apps/mpp/index.js.map +1 -0
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +57 -10
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/apps/relay/Sponsorships.js +1 -1
- package/dist/apps/relay/Sponsorships.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 +27 -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 +11 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/enabledBillingSources.d.ts +73 -0
- package/dist/db/tables/enabledBillingSources.d.ts.map +1 -0
- package/dist/db/tables/enabledBillingSources.js +69 -0
- package/dist/db/tables/enabledBillingSources.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +1 -12
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/invitations.js +3 -23
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +5 -2
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +32 -2
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts +2 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +13 -3
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts +1 -16
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +17 -6
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +37 -5
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +0 -32
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +3 -51
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +1 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +3 -2
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/FundingQuotes.d.ts +78 -32
- package/dist/internal/FundingQuotes.d.ts.map +1 -1
- package/dist/internal/FundingQuotes.js +45 -8
- package/dist/internal/FundingQuotes.js.map +1 -1
- package/dist/internal/Log.d.ts +18 -4
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +50 -3
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +15 -0
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/Response.d.ts +2 -2
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Response.js +12 -4
- package/dist/internal/Response.js.map +1 -1
- package/dist/internal/Schema.d.ts +2 -2
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +4 -2
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Viem.d.ts +1 -1
- package/package.json +7 -2
- package/src/App.test.ts +9 -4
- package/src/App.ts +14 -2
- package/src/Client.test-d.ts +109 -28
- package/src/Scope.ts +5 -0
- package/src/admin/App.test.ts +4 -0
- package/src/admin/apps/early-access.ts +6 -5
- package/src/admin/apps/organizations.test.ts +86 -2
- package/src/admin/apps/organizations.ts +155 -31
- package/src/admin/apps/scopes.test.ts +5 -0
- package/src/admin/ui/src/lib/organizations.test.ts +81 -1
- package/src/admin/ui/src/lib/organizations.ts +92 -0
- package/src/admin/ui/src/routes/api-keys.tsx +1 -1
- package/src/admin/ui/src/routes/early-access.tsx +8 -3
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +103 -0
- package/src/admin/ui.generated.ts +49 -39
- package/src/analytics/Analytics.test.ts +83 -0
- package/src/analytics/Analytics.ts +20 -0
- package/src/analytics/tables/requestEvents.ts +60 -7
- package/src/apps/composition.test.ts +11 -2
- package/src/apps/data/App.test.ts +14 -2
- package/src/apps/data/FundingQuoteProviders.test.ts +0 -5
- package/src/apps/data/FundingQuoteProviders.ts +2 -29
- package/src/apps/data/FxOracle.test.ts +2 -2
- package/src/apps/data/FxOracle.ts +2 -2
- package/src/apps/data/routes/activities.test.ts +6 -2
- package/src/apps/data/routes/activities.ts +39 -1
- package/src/apps/data/routes/balances.test.ts +6 -2
- package/src/apps/data/routes/exchanges.test.ts +7 -3
- package/src/apps/data/routes/fee-amm.test.ts +47 -23
- package/src/apps/data/routes/fee-amm.ts +98 -119
- package/src/apps/data/routes/funding.test.ts +29 -8
- package/src/apps/data/routes/funding.ts +42 -48
- package/src/apps/data/routes/receipts.test.ts +6 -2
- package/src/apps/data/routes/tokenlist.test.ts +15 -0
- package/src/apps/data/routes/tokenlist.ts +18 -5
- package/src/apps/data/routes/tokens.ts +5 -1
- package/src/apps/data/routes/transfers.test.ts +203 -35
- package/src/apps/data/routes/transfers.ts +294 -172
- package/src/apps/data/routes/valuation.test.ts +18 -3
- package/src/apps/data/routes/valuation.ts +4 -4
- package/src/apps/data/routes/verified-tokens.test.ts +51 -0
- package/src/apps/data/routes/verified-tokens.ts +52 -15
- package/src/apps/data/routes/webhooks.test.ts +56 -0
- package/src/apps/data/routes/webhooks.ts +17 -8
- package/src/apps/index.ts +1 -0
- package/src/apps/management/routes/api-keys.test.ts +2 -75
- package/src/apps/management/routes/api-keys.ts +30 -7
- package/src/apps/management/routes/billing.test.ts +106 -85
- package/src/apps/management/routes/billing.ts +32 -21
- package/src/apps/management/routes/faucet.test.ts +11 -3
- package/src/apps/management/routes/faucet.ts +46 -30
- package/src/apps/management/routes/invitations.test.ts +0 -172
- package/src/apps/management/routes/invitations.ts +0 -3
- package/src/apps/management/routes/me.test.ts +3 -145
- package/src/apps/management/routes/me.ts +0 -6
- package/src/apps/management/routes/orgs.test.ts +45 -2
- package/src/apps/management/routes/orgs.ts +6 -0
- package/src/apps/management/routes/scopes.test.ts +1 -0
- package/src/apps/mpp/App.ts +86 -0
- package/src/apps/mpp/Mpp.test.ts +193 -0
- package/src/apps/mpp/Mpp.ts +353 -0
- package/src/apps/mpp/index.ts +2 -0
- package/src/apps/mpp/internal/Adapter.ts +11 -0
- package/src/apps/mpp/internal/Error.ts +9 -0
- package/src/apps/mpp/internal/Idempotency.ts +7 -0
- package/src/apps/mpp/internal/Screening.ts +10 -0
- package/src/apps/relay/App.test.ts +90 -8
- package/src/apps/relay/App.ts +62 -10
- package/src/apps/relay/Sponsorships.test.ts +2 -0
- package/src/apps/relay/Sponsorships.ts +1 -1
- package/src/db/Db.test.ts +2 -2
- package/src/db/Db.ts +30 -0
- package/src/db/Schema.ts +15 -0
- package/src/db/tables/enabledBillingSources.test.ts +303 -0
- package/src/db/tables/enabledBillingSources.ts +118 -0
- package/src/db/tables/invitations.ts +4 -26
- package/src/db/tables/organizations.test.ts +191 -0
- package/src/db/tables/organizations.ts +44 -3
- package/src/db/tables/sponsoredTransactions.test.ts +41 -2
- package/src/db/tables/sponsoredTransactions.ts +15 -3
- package/src/handlers/internal/utils.ts +22 -6
- package/src/handlers/relay.errors.test.ts +153 -0
- package/src/handlers/relay.ts +42 -7
- package/src/internal/Auth.test.ts +3 -3
- package/src/internal/Auth.ts +3 -63
- package/src/internal/EdgeCache.test.ts +18 -5
- package/src/internal/EdgeCache.ts +4 -2
- package/src/internal/FundingQuotes.test.ts +18 -5
- package/src/internal/FundingQuotes.ts +46 -11
- package/src/internal/Log.test.ts +91 -0
- package/src/internal/Log.ts +69 -5
- package/src/internal/MetricSink.test.ts +55 -0
- package/src/internal/MetricSink.ts +14 -0
- package/src/internal/Response.test.ts +28 -0
- package/src/internal/Response.ts +12 -4
- package/src/internal/Schema.test.ts +13 -0
- package/src/internal/Schema.ts +14 -7
|
@@ -51,7 +51,11 @@ const seed: readonly SeedToken[] = JSON.parse(
|
|
|
51
51
|
// Deterministic EUR-based rate set so conversion math is exact in assertions.
|
|
52
52
|
const fixed = FxOracle.from({
|
|
53
53
|
name: 'fixed',
|
|
54
|
-
rates: async () => ({
|
|
54
|
+
rates: async () => ({
|
|
55
|
+
asOf: '2026-01-01T00:00:00.000Z',
|
|
56
|
+
base: 'EUR',
|
|
57
|
+
rates: { AUD: '1.6', USD: '1.0' },
|
|
58
|
+
}),
|
|
55
59
|
})
|
|
56
60
|
|
|
57
61
|
async function holderWithBalance(client: ReturnType<typeof app>) {
|
|
@@ -93,6 +97,17 @@ async function chainSum(holder: `0x${string}`, currency: string) {
|
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
describe('GET /addresses/:address/valuation', () => {
|
|
100
|
+
test('requires a full pricing timestamp', () => {
|
|
101
|
+
const pricing = { asOf: '2026-01-01', basis: 'nominal', source: 'fixed' }
|
|
102
|
+
expect(Valuation.schema.Pricing.safeParse(pricing).success).toBe(false)
|
|
103
|
+
expect(
|
|
104
|
+
Valuation.schema.Pricing.safeParse({
|
|
105
|
+
...pricing,
|
|
106
|
+
asOf: '2026-01-01T00:00:00.000Z',
|
|
107
|
+
}).success,
|
|
108
|
+
).toBe(true)
|
|
109
|
+
})
|
|
110
|
+
|
|
96
111
|
test('normalizes the denomination', () => {
|
|
97
112
|
expect(
|
|
98
113
|
Valuation.schema.getAddressValuation.Query.parse({ denomination: 'aud' }).denomination,
|
|
@@ -176,7 +191,7 @@ describe('GET /addresses/:address/valuation', () => {
|
|
|
176
191
|
expect(aud.amount).toBe(core_Value.format((sum * 16n) / 10n, 6))
|
|
177
192
|
expect(aud.pricing).toMatchInlineSnapshot(`
|
|
178
193
|
{
|
|
179
|
-
"asOf": "2026-01-
|
|
194
|
+
"asOf": "2026-01-01T00:00:00.000Z",
|
|
180
195
|
"basis": "nominal",
|
|
181
196
|
"source": "fixed",
|
|
182
197
|
}
|
|
@@ -201,7 +216,7 @@ describe('GET /addresses/:address/valuation', () => {
|
|
|
201
216
|
expect(Number(body.amount)).toBeGreaterThan(0)
|
|
202
217
|
expect(body.pricing?.basis).toBe('nominal')
|
|
203
218
|
expect(body.pricing?.source).toBe('ecb')
|
|
204
|
-
expect(body.pricing?.asOf).
|
|
219
|
+
expect(body.pricing?.asOf).toBe('2026-01-01T00:00:00.000Z')
|
|
205
220
|
})
|
|
206
221
|
|
|
207
222
|
test.skipIf(runtime.mode !== 'localnet')(
|
|
@@ -44,11 +44,11 @@ export namespace schema {
|
|
|
44
44
|
/** Rate provenance for a converted valuation. */
|
|
45
45
|
export const Pricing = z
|
|
46
46
|
.object({
|
|
47
|
-
asOf: z
|
|
48
|
-
.
|
|
47
|
+
asOf: z.iso
|
|
48
|
+
.datetime()
|
|
49
49
|
.check(
|
|
50
|
-
z.describe('Publication date of the rate set
|
|
51
|
-
z.meta({ examples: ['2026-07-
|
|
50
|
+
z.describe('Publication date of the rate set as an ISO 8601 timestamp.'),
|
|
51
|
+
z.meta({ examples: ['2026-07-20T00:00:00.000Z'] }),
|
|
52
52
|
),
|
|
53
53
|
basis: z
|
|
54
54
|
.literal('nominal')
|
|
@@ -87,6 +87,21 @@ describe('GET /verified-tokens', () => {
|
|
|
87
87
|
)
|
|
88
88
|
expect(second.status).toBe(304)
|
|
89
89
|
})
|
|
90
|
+
|
|
91
|
+
test('refreshes cached reads after a snapshot publish', async () => {
|
|
92
|
+
const db = TestApp.database()
|
|
93
|
+
await VerifiedTokens.replace(db, chainId, seedList)
|
|
94
|
+
const client = TestApp.client({ auth: { keys: [reader] }, db })
|
|
95
|
+
|
|
96
|
+
await client.v1['verified-tokens'].$get({ query: {} }, as(reader))
|
|
97
|
+
await VerifiedTokens.replace(db, chainId, seedList.slice(1))
|
|
98
|
+
|
|
99
|
+
const response = await client.v1['verified-tokens'].$get({ query: {} }, as(reader))
|
|
100
|
+
const body = await TestApp.json(response, Verified.schema.listVerifiedTokens.Response)
|
|
101
|
+
expect(body.data.map((token) => token.address)).toEqual(
|
|
102
|
+
seedList.slice(1).map((token) => token.address),
|
|
103
|
+
)
|
|
104
|
+
})
|
|
90
105
|
})
|
|
91
106
|
|
|
92
107
|
describe('GET /verified-tokens/currencies', () => {
|
|
@@ -97,6 +112,23 @@ describe('GET /verified-tokens/currencies', () => {
|
|
|
97
112
|
const body = await TestApp.json(response, Verified.schema.getVerifiedTokenCurrencies.Response)
|
|
98
113
|
expect([...body.data]).toEqual(['EUR', 'USD'])
|
|
99
114
|
})
|
|
115
|
+
|
|
116
|
+
test('refreshes cached reads after a snapshot publish', async () => {
|
|
117
|
+
const db = TestApp.database()
|
|
118
|
+
await VerifiedTokens.replace(db, chainId, seedList)
|
|
119
|
+
const client = TestApp.client({ auth: { keys: [reader] }, db })
|
|
120
|
+
|
|
121
|
+
await client.v1['verified-tokens'].currencies.$get({ query: {} }, as(reader))
|
|
122
|
+
await VerifiedTokens.replace(
|
|
123
|
+
db,
|
|
124
|
+
chainId,
|
|
125
|
+
seedList.filter((token) => token.currency === 'USD'),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
const response = await client.v1['verified-tokens'].currencies.$get({ query: {} }, as(reader))
|
|
129
|
+
const body = await TestApp.json(response, Verified.schema.getVerifiedTokenCurrencies.Response)
|
|
130
|
+
expect([...body.data]).toEqual(['USD'])
|
|
131
|
+
})
|
|
100
132
|
})
|
|
101
133
|
|
|
102
134
|
describe('GET /verified-tokens/:address', () => {
|
|
@@ -123,4 +155,23 @@ describe('GET /verified-tokens/:address', () => {
|
|
|
123
155
|
const body = (await response.json()) as { error?: { code?: string } }
|
|
124
156
|
expect(body.error?.code).toBe('verified_token_not_found')
|
|
125
157
|
})
|
|
158
|
+
|
|
159
|
+
test('refreshes cached reads after a snapshot publish', async () => {
|
|
160
|
+
const db = TestApp.database()
|
|
161
|
+
await VerifiedTokens.replace(db, chainId, seedList)
|
|
162
|
+
const client = TestApp.client({ auth: { keys: [reader] }, db })
|
|
163
|
+
const address = seedList[0].address
|
|
164
|
+
|
|
165
|
+
await client.v1['verified-tokens'][':address'].$get(
|
|
166
|
+
{ param: { address }, query: {} },
|
|
167
|
+
as(reader),
|
|
168
|
+
)
|
|
169
|
+
await VerifiedTokens.replace(db, chainId, seedList.slice(1))
|
|
170
|
+
|
|
171
|
+
const response = await client.v1['verified-tokens'][':address'].$get(
|
|
172
|
+
{ param: { address }, query: {} },
|
|
173
|
+
as(reader),
|
|
174
|
+
)
|
|
175
|
+
expect(response.status).toBe(404)
|
|
176
|
+
})
|
|
126
177
|
})
|
|
@@ -4,6 +4,7 @@ import * as z from 'zod/mini'
|
|
|
4
4
|
import type * as App from '../../../App.js'
|
|
5
5
|
import * as Auth from '../../../internal/Auth.js'
|
|
6
6
|
import * as Cache from '../../../internal/Cache.js'
|
|
7
|
+
import * as EdgeCache from '../../../internal/EdgeCache.js'
|
|
7
8
|
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
8
9
|
import * as Response from '../../../internal/Response.js'
|
|
9
10
|
import * as Schema from '../../../internal/Schema.js'
|
|
@@ -168,11 +169,23 @@ export function verifiedTokens() {
|
|
|
168
169
|
summary: 'List verified tokens',
|
|
169
170
|
tags: ['Verified Tokens'],
|
|
170
171
|
}),
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
// The shared edge cache is URL-keyed and cannot include the snapshot version.
|
|
173
|
+
EdgeCache.setEligibility(
|
|
174
|
+
Cache.response<App.Environment>({
|
|
175
|
+
cacheControl: Cache.policies.stable,
|
|
176
|
+
name: 'tempo-api:verified-tokens:v1',
|
|
177
|
+
key: async (c) => {
|
|
178
|
+
const base = Cache.urlKey(c, schema.listVerifiedTokens.Query)
|
|
179
|
+
const query = schema.listVerifiedTokens.Query.parse(
|
|
180
|
+
Object.fromEntries(new URL(c.req.url).searchParams),
|
|
181
|
+
)
|
|
182
|
+
const chainId = query.chainId ?? c.get('chainId')
|
|
183
|
+
const { version } = await VerifiedTokens.snapshot(c, chainId)
|
|
184
|
+
return `${base}:v:${version}`
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
() => false,
|
|
188
|
+
),
|
|
176
189
|
async (c) => {
|
|
177
190
|
if (Auth.narrowAccess) return Auth.paidAccessError(c)
|
|
178
191
|
if (OpenApi.narrowValidation)
|
|
@@ -213,11 +226,23 @@ export function verifiedTokens() {
|
|
|
213
226
|
summary: 'List verified currencies',
|
|
214
227
|
tags: ['Verified Tokens'],
|
|
215
228
|
}),
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
229
|
+
// The shared edge cache is URL-keyed and cannot include the snapshot version.
|
|
230
|
+
EdgeCache.setEligibility(
|
|
231
|
+
Cache.response<App.Environment>({
|
|
232
|
+
cacheControl: Cache.policies.stable,
|
|
233
|
+
name: 'tempo-api:verified-tokens:v1',
|
|
234
|
+
key: async (c) => {
|
|
235
|
+
const base = Cache.urlKey(c, schema.getVerifiedTokenCurrencies.Query)
|
|
236
|
+
const query = schema.getVerifiedTokenCurrencies.Query.parse(
|
|
237
|
+
Object.fromEntries(new URL(c.req.url).searchParams),
|
|
238
|
+
)
|
|
239
|
+
const chainId = query.chainId ?? c.get('chainId')
|
|
240
|
+
const { version } = await VerifiedTokens.snapshot(c, chainId)
|
|
241
|
+
return `${base}:v:${version}`
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
() => false,
|
|
245
|
+
),
|
|
221
246
|
async (c) => {
|
|
222
247
|
if (Auth.narrowAccess) return Auth.paidAccessError(c)
|
|
223
248
|
if (OpenApi.narrowValidation)
|
|
@@ -265,11 +290,23 @@ export function verifiedTokens() {
|
|
|
265
290
|
summary: 'Get verified token',
|
|
266
291
|
tags: ['Verified Tokens'],
|
|
267
292
|
}),
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
293
|
+
// The shared edge cache is URL-keyed and cannot include the snapshot version.
|
|
294
|
+
EdgeCache.setEligibility(
|
|
295
|
+
Cache.response<App.Environment>({
|
|
296
|
+
cacheControl: Cache.policies.stable,
|
|
297
|
+
name: 'tempo-api:verified-tokens:v1',
|
|
298
|
+
key: async (c) => {
|
|
299
|
+
const base = Cache.urlKey(c, schema.getVerifiedToken.Query)
|
|
300
|
+
const query = schema.getVerifiedToken.Query.parse(
|
|
301
|
+
Object.fromEntries(new URL(c.req.url).searchParams),
|
|
302
|
+
)
|
|
303
|
+
const chainId = query.chainId ?? c.get('chainId')
|
|
304
|
+
const { version } = await VerifiedTokens.snapshot(c, chainId)
|
|
305
|
+
return `${base}:v:${version}`
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
() => false,
|
|
309
|
+
),
|
|
273
310
|
async (c) => {
|
|
274
311
|
if (Auth.narrowAccess) return Auth.paidAccessError(c)
|
|
275
312
|
if (OpenApi.narrowValidation)
|
|
@@ -1736,6 +1736,33 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
|
|
|
1736
1736
|
`)
|
|
1737
1737
|
})
|
|
1738
1738
|
|
|
1739
|
+
test('block:created preserves the cursor when a block header cannot be read', async () => {
|
|
1740
|
+
const db = TestApp.database()
|
|
1741
|
+
const missingBlock = 4_000_000_000
|
|
1742
|
+
const cursor = encodeCursor([missingBlock - 1, endOfBlockIndex])
|
|
1743
|
+
const subscription = await Core.createSubscription(db, {
|
|
1744
|
+
chainId: 42431,
|
|
1745
|
+
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
1746
|
+
eventType: 'block:created',
|
|
1747
|
+
filters: { number: { eq: `0x${missingBlock.toString(16)}` } },
|
|
1748
|
+
owner: { orgId: 'org_test', type: 'api_key' },
|
|
1749
|
+
})
|
|
1750
|
+
await Core.setCursor(db, subscription.id, cursor)
|
|
1751
|
+
|
|
1752
|
+
await expect(
|
|
1753
|
+
Webhooks.poll({
|
|
1754
|
+
chainId: 42431,
|
|
1755
|
+
eventTypes: ['block:created'],
|
|
1756
|
+
headBlockNumber: missingBlock + 2,
|
|
1757
|
+
client,
|
|
1758
|
+
db,
|
|
1759
|
+
tidx: realTidx,
|
|
1760
|
+
}),
|
|
1761
|
+
).rejects.toThrow()
|
|
1762
|
+
|
|
1763
|
+
expect(await Core.getCursor(db, subscription.id)).toBe(cursor)
|
|
1764
|
+
})
|
|
1765
|
+
|
|
1739
1766
|
test('transaction:included filters on txs columns and inner calls', async () => {
|
|
1740
1767
|
const db = TestApp.database()
|
|
1741
1768
|
const subscription = await Core.createSubscription(db, {
|
|
@@ -1778,6 +1805,35 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
|
|
|
1778
1805
|
} else expect(tx?.feeToken).toBeUndefined()
|
|
1779
1806
|
})
|
|
1780
1807
|
|
|
1808
|
+
test('transaction:included preserves the cursor when a confirmed block is unavailable', async () => {
|
|
1809
|
+
const db = TestApp.database()
|
|
1810
|
+
const missingBlock = 4_000_000_000
|
|
1811
|
+
const cursor = encodeCursor([missingBlock - 1, endOfBlockIndex])
|
|
1812
|
+
const subscription = await Core.createSubscription(db, {
|
|
1813
|
+
chainId: 42431,
|
|
1814
|
+
destination: { type: 'url', url: 'https://hooks.example.com/x' },
|
|
1815
|
+
eventType: 'transaction:included',
|
|
1816
|
+
filters: { blockNumber: { eq: `0x${missingBlock.toString(16)}` } },
|
|
1817
|
+
owner: { orgId: 'org_test', type: 'api_key' },
|
|
1818
|
+
})
|
|
1819
|
+
await Core.setCursor(db, subscription.id, cursor)
|
|
1820
|
+
|
|
1821
|
+
await expect(
|
|
1822
|
+
Webhooks.poll({
|
|
1823
|
+
chainId: 42431,
|
|
1824
|
+
eventTypes: ['transaction:included'],
|
|
1825
|
+
headBlockNumber: missingBlock + 2,
|
|
1826
|
+
client,
|
|
1827
|
+
db,
|
|
1828
|
+
tidx: realTidx,
|
|
1829
|
+
}),
|
|
1830
|
+
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
|
1831
|
+
[Webhooks.BlockUnavailableError: Block 4000000000 is unavailable.]
|
|
1832
|
+
`)
|
|
1833
|
+
|
|
1834
|
+
expect(await Core.getCursor(db, subscription.id)).toBe(cursor)
|
|
1835
|
+
})
|
|
1836
|
+
|
|
1781
1837
|
test('transaction:included matches exact empty calldata through the poller', async () => {
|
|
1782
1838
|
const db = TestApp.database()
|
|
1783
1839
|
const subscription = await Core.createSubscription(db, {
|
|
@@ -2485,7 +2485,7 @@ type RpcBlock = Awaited<ReturnType<Viem.getClient.ReturnType['getBlock']>>
|
|
|
2485
2485
|
/** Per-tick lazy cache of fetched block headers, full blocks, and receipts. */
|
|
2486
2486
|
type ChainCache = {
|
|
2487
2487
|
/** Full raw block (with transactions) via `eth_getBlockByNumber(n, true)`. */
|
|
2488
|
-
block(n: number): Promise<Record<string, unknown
|
|
2488
|
+
block(n: number): Promise<Record<string, unknown>>
|
|
2489
2489
|
/** Viem-formatted block header (transaction hashes only) via `getBlock`. */
|
|
2490
2490
|
header(n: number): Promise<RpcBlock | undefined>
|
|
2491
2491
|
/** Receipts for a block keyed by lowercased transaction hash. */
|
|
@@ -2495,7 +2495,7 @@ type ChainCache = {
|
|
|
2495
2495
|
/** Builds a per-tick {@link ChainCache} bound to a viem client. */
|
|
2496
2496
|
function createChainCache(client: Viem.getClient.ReturnType): ChainCache {
|
|
2497
2497
|
const headers = new Map<number, Promise<RpcBlock | undefined>>()
|
|
2498
|
-
const blocks = new Map<number, Promise<Record<string, unknown
|
|
2498
|
+
const blocks = new Map<number, Promise<Record<string, unknown>>>()
|
|
2499
2499
|
const receipts = new Map<number, Promise<Map<string, Record<string, unknown>>>>()
|
|
2500
2500
|
return {
|
|
2501
2501
|
block(n) {
|
|
@@ -2506,7 +2506,10 @@ function createChainCache(client: Viem.getClient.ReturnType): ChainCache {
|
|
|
2506
2506
|
p = rpc<Record<string, unknown> | null>(client, 'eth_getBlockByNumber', [
|
|
2507
2507
|
blockTag(n),
|
|
2508
2508
|
true,
|
|
2509
|
-
]).then((
|
|
2509
|
+
]).then((block) => {
|
|
2510
|
+
if (!block) throw new BlockUnavailableError(n)
|
|
2511
|
+
return block
|
|
2512
|
+
})
|
|
2510
2513
|
blocks.set(n, p)
|
|
2511
2514
|
}
|
|
2512
2515
|
return p
|
|
@@ -2514,9 +2517,7 @@ function createChainCache(client: Viem.getClient.ReturnType): ChainCache {
|
|
|
2514
2517
|
header(n) {
|
|
2515
2518
|
let p = headers.get(n)
|
|
2516
2519
|
if (!p) {
|
|
2517
|
-
p = client
|
|
2518
|
-
.getBlock({ blockNumber: BigInt(n), includeTransactions: false })
|
|
2519
|
-
.catch(() => undefined)
|
|
2520
|
+
p = client.getBlock({ blockNumber: BigInt(n), includeTransactions: false })
|
|
2520
2521
|
headers.set(n, p)
|
|
2521
2522
|
}
|
|
2522
2523
|
return p
|
|
@@ -2999,10 +3000,10 @@ async function scanTxs(options: {
|
|
|
2999
3000
|
const items: ScanItem[] = []
|
|
3000
3001
|
for (let n = window.from; n <= window.to; n++) {
|
|
3001
3002
|
const block = await cache.block(n)
|
|
3002
|
-
const txs = Array.isArray(block
|
|
3003
|
+
const txs = Array.isArray(block['transactions']) ? (block['transactions'] as unknown[]) : []
|
|
3003
3004
|
if (txs.length === 0) continue
|
|
3004
3005
|
const receipts = needsReceipt || includeReceipt ? await cache.receipts(n) : undefined
|
|
3005
|
-
const timestamp = asString(block
|
|
3006
|
+
const timestamp = asString(block['timestamp'])
|
|
3006
3007
|
|
|
3007
3008
|
for (let i = 0; i < txs.length; i++) {
|
|
3008
3009
|
const tx = txs[i] as Record<string, unknown>
|
|
@@ -3195,3 +3196,11 @@ function unauthorized(c: Context<App.Environment>) {
|
|
|
3195
3196
|
status: 401,
|
|
3196
3197
|
})
|
|
3197
3198
|
}
|
|
3199
|
+
|
|
3200
|
+
class BlockUnavailableError extends Error {
|
|
3201
|
+
override name = 'Webhooks.BlockUnavailableError'
|
|
3202
|
+
|
|
3203
|
+
constructor(blockNumber: number) {
|
|
3204
|
+
super(`Block ${blockNumber} is unavailable.`)
|
|
3205
|
+
}
|
|
3206
|
+
}
|
package/src/apps/index.ts
CHANGED
|
@@ -5,9 +5,7 @@ import * as Auth from '../../../internal/Auth.js'
|
|
|
5
5
|
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
6
6
|
import * as Scope from '../../../Scope.js'
|
|
7
7
|
import * as TestApp from '../../../../test/App.js'
|
|
8
|
-
import * as EarlyAccess from '../../../db/tables/earlyAccess.js'
|
|
9
8
|
import * as Memberships from '../../../db/tables/memberships.js'
|
|
10
|
-
import * as Users from '../../../db/tables/users.js'
|
|
11
9
|
import * as Keys from './api-keys.js'
|
|
12
10
|
import * as Me from './me.js'
|
|
13
11
|
import * as Orgs from './orgs.js'
|
|
@@ -45,7 +43,6 @@ function createApp(
|
|
|
45
43
|
db?: ReturnType<typeof TestApp.database>
|
|
46
44
|
kv?: { store: State }
|
|
47
45
|
scopes?: readonly Scope.Entry[]
|
|
48
|
-
tmp_earlyAccess?: boolean
|
|
49
46
|
} = {},
|
|
50
47
|
) {
|
|
51
48
|
return TestApp.create({
|
|
@@ -54,7 +51,6 @@ function createApp(
|
|
|
54
51
|
...(options.kv ? { kv: options.kv } : {}),
|
|
55
52
|
...(options.scopes ? { scopes: options.scopes } : {}),
|
|
56
53
|
session: { wallet: { origin } },
|
|
57
|
-
...(options.tmp_earlyAccess === undefined ? {} : { tmp_earlyAccess: options.tmp_earlyAccess }),
|
|
58
54
|
})
|
|
59
55
|
}
|
|
60
56
|
|
|
@@ -677,66 +673,9 @@ describe('keys', () => {
|
|
|
677
673
|
})
|
|
678
674
|
|
|
679
675
|
describe('early access', () => {
|
|
680
|
-
test('
|
|
681
|
-
const app = createApp(
|
|
682
|
-
const cookie = await session(app)
|
|
683
|
-
const { org, project } = await fixture(app, cookie)
|
|
684
|
-
|
|
685
|
-
const denied = await app.request(
|
|
686
|
-
`/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
|
|
687
|
-
json('POST', cookie, { scopes: ['data:read'] }),
|
|
688
|
-
)
|
|
689
|
-
expect(denied.status).toBe(403)
|
|
690
|
-
const { requestId, ...body } = (await denied.json()) as { requestId?: string }
|
|
691
|
-
expect(requestId).toBeDefined()
|
|
692
|
-
expect(body).toMatchInlineSnapshot(`
|
|
693
|
-
{
|
|
694
|
-
"error": {
|
|
695
|
-
"code": "early_access_required",
|
|
696
|
-
"message": "Requires early access. Ask a teammate with access to invite you by email, or contact Tempo.",
|
|
697
|
-
},
|
|
698
|
-
}
|
|
699
|
-
`)
|
|
700
|
-
})
|
|
701
|
-
|
|
702
|
-
test('the gate precedes membership resolution', async () => {
|
|
703
|
-
// The gate depends only on the caller, so it leaks nothing about the
|
|
704
|
-
// project; early-access strangers still get the membership 404.
|
|
705
|
-
const db = TestApp.database()
|
|
706
|
-
const app = createApp({ db, tmp_earlyAccess: true })
|
|
707
|
-
await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
|
|
708
|
-
const owner = await session(app)
|
|
709
|
-
const { org, project } = await fixture(app, owner)
|
|
710
|
-
|
|
711
|
-
const other = await session(app)
|
|
712
|
-
const gated = await app.request(
|
|
713
|
-
`/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
|
|
714
|
-
json('POST', other, { scopes: ['data:read'] }),
|
|
715
|
-
)
|
|
716
|
-
expect(gated.status).toBe(403)
|
|
717
|
-
expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
|
|
718
|
-
|
|
719
|
-
const granted = await session(app)
|
|
720
|
-
const me = await app.request('/v1/me', { headers: { cookie: granted } })
|
|
721
|
-
const { id: userId } = (await me.json()) as { id: string }
|
|
722
|
-
await Users.setEmail(db, userId, 'stranger@example.org')
|
|
723
|
-
const denied = await app.request(
|
|
724
|
-
`/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
|
|
725
|
-
json('POST', granted, { scopes: ['data:read'] }),
|
|
726
|
-
)
|
|
727
|
-
expect(denied.status).toBe(404)
|
|
728
|
-
})
|
|
729
|
-
|
|
730
|
-
test('lets early-access sessions mint', async () => {
|
|
731
|
-
const db = TestApp.database()
|
|
732
|
-
const app = createApp({ db, tmp_earlyAccess: true })
|
|
733
|
-
await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
|
|
676
|
+
test('does not require an early-access grant', async () => {
|
|
677
|
+
const app = createApp()
|
|
734
678
|
const cookie = await session(app)
|
|
735
|
-
const user = await TestApp.json(
|
|
736
|
-
await app.request('/v1/me', { headers: { cookie } }),
|
|
737
|
-
Me.schema.User,
|
|
738
|
-
)
|
|
739
|
-
await Users.setEmail(db, user.id, 'dev@example.org')
|
|
740
679
|
const { org, project } = await fixture(app, cookie)
|
|
741
680
|
|
|
742
681
|
const minted = await app.request(
|
|
@@ -745,16 +684,4 @@ describe('early access', () => {
|
|
|
745
684
|
)
|
|
746
685
|
expect(minted.status).toBe(200)
|
|
747
686
|
})
|
|
748
|
-
|
|
749
|
-
test('lets the super admin mint', async () => {
|
|
750
|
-
const app = createApp({ tmp_earlyAccess: true })
|
|
751
|
-
const cookie = await session(app)
|
|
752
|
-
const { org, project } = await fixture(app, cookie)
|
|
753
|
-
|
|
754
|
-
const minted = await app.request(
|
|
755
|
-
`/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
|
|
756
|
-
superAdminJson('POST', { scopes: ['data:read'] }),
|
|
757
|
-
)
|
|
758
|
-
expect(minted.status).toBe(200)
|
|
759
|
-
})
|
|
760
687
|
})
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type Context, Hono } from 'hono'
|
|
2
2
|
import * as z from 'zod/mini'
|
|
3
3
|
|
|
4
|
+
import * as Analytics from '../../../analytics/Analytics.js'
|
|
5
|
+
import * as RequestEvents from '../../../analytics/tables/requestEvents.js'
|
|
4
6
|
import * as ApiKeys from '../../../ApiKeys.js'
|
|
5
7
|
import type * as App from '../../../App.js'
|
|
6
8
|
import * as Db from '../../../db/Db.js'
|
|
@@ -43,6 +45,12 @@ export namespace schema {
|
|
|
43
45
|
z.describe('Opaque key id (`key_…`).'),
|
|
44
46
|
z.meta({ examples: ['key_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
|
|
45
47
|
),
|
|
48
|
+
lastUsedAt: z
|
|
49
|
+
.optional(z.string())
|
|
50
|
+
.check(
|
|
51
|
+
z.describe('When the key last authenticated a request (ISO 8601).'),
|
|
52
|
+
z.meta({ examples: ['2026-07-21T12:34:56.000Z'] }),
|
|
53
|
+
),
|
|
46
54
|
name: z
|
|
47
55
|
.optional(z.string())
|
|
48
56
|
.check(z.describe('Human-readable key name.'), z.meta({ examples: ['CI'] })),
|
|
@@ -225,7 +233,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
225
233
|
return new Hono<App.Environment>()
|
|
226
234
|
.post(
|
|
227
235
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys',
|
|
228
|
-
Auth.policy({ session: true
|
|
236
|
+
Auth.policy({ session: true }),
|
|
229
237
|
Auth.ensureOrg({ role: 'admin' }),
|
|
230
238
|
Auth.ensureProject(),
|
|
231
239
|
OpenApi.validate('param', schema.Params, {
|
|
@@ -243,8 +251,8 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
243
251
|
responses: OpenApi.responses({
|
|
244
252
|
errors: {
|
|
245
253
|
403: {
|
|
246
|
-
codes: ['scope_not_issuable'
|
|
247
|
-
description: 'The requested scopes are not issuable
|
|
254
|
+
codes: ['scope_not_issuable'],
|
|
255
|
+
description: 'The requested scopes are not issuable.',
|
|
248
256
|
},
|
|
249
257
|
404: 'No accessible project was found for this id.',
|
|
250
258
|
},
|
|
@@ -259,7 +267,6 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
259
267
|
async (c) => {
|
|
260
268
|
if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
|
|
261
269
|
if (Auth.narrowScope) return Auth.ensureProjectError(c)
|
|
262
|
-
if (Auth.tmp_narrowEarlyAccess) return Auth.tmp_ensureEarlyAccessError(c)
|
|
263
270
|
if (OpenApi.narrowValidation)
|
|
264
271
|
return OpenApi.validationError(c, {
|
|
265
272
|
code: 'param_invalid',
|
|
@@ -375,7 +382,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
375
382
|
})
|
|
376
383
|
return c.json(
|
|
377
384
|
Response.validated(schema.listApiKeys.Response, {
|
|
378
|
-
data: records
|
|
385
|
+
data: await serializeKeys(c, records),
|
|
379
386
|
}),
|
|
380
387
|
200,
|
|
381
388
|
)
|
|
@@ -436,7 +443,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
436
443
|
})
|
|
437
444
|
return c.json(
|
|
438
445
|
Response.validated(schema.listOrgApiKeys.Response, {
|
|
439
|
-
data: records
|
|
446
|
+
data: await serializeKeys(c, records),
|
|
440
447
|
}),
|
|
441
448
|
200,
|
|
442
449
|
)
|
|
@@ -506,14 +513,30 @@ export declare namespace apiKeys {
|
|
|
506
513
|
}
|
|
507
514
|
}
|
|
508
515
|
|
|
516
|
+
/** Enriches key records with their latest authenticated request timestamp. */
|
|
517
|
+
async function serializeKeys(c: Context<App.Environment>, records: readonly ApiKeys.Record[]) {
|
|
518
|
+
const source = c.get('analytics')
|
|
519
|
+
if (!source || records.length === 0) return records.map((record) => serializeKey(record))
|
|
520
|
+
const lastUsedAt = new Map(
|
|
521
|
+
(
|
|
522
|
+
await RequestEvents.readLastUsedAt(
|
|
523
|
+
Analytics.get(source),
|
|
524
|
+
records.map(({ id }) => id),
|
|
525
|
+
)
|
|
526
|
+
).map((usage) => [usage.apiKeyId, usage.lastUsedAt]),
|
|
527
|
+
)
|
|
528
|
+
return records.map((record) => serializeKey(record, lastUsedAt.get(record.id)))
|
|
529
|
+
}
|
|
530
|
+
|
|
509
531
|
/** Projects a persisted record onto the management key shape (no rate limits, no token). */
|
|
510
|
-
function serializeKey(record: ApiKeys.Record) {
|
|
532
|
+
function serializeKey(record: ApiKeys.Record, lastUsedAt?: string) {
|
|
511
533
|
return {
|
|
512
534
|
createdAt: record.createdAt,
|
|
513
535
|
...(record.createdBy === undefined ? {} : { createdBy: record.createdBy }),
|
|
514
536
|
environment: record.environment,
|
|
515
537
|
...(record.expiresAt === undefined ? {} : { expiresAt: record.expiresAt }),
|
|
516
538
|
id: record.id,
|
|
539
|
+
...(lastUsedAt === undefined ? {} : { lastUsedAt }),
|
|
517
540
|
...(record.name === undefined ? {} : { name: record.name }),
|
|
518
541
|
orgId: record.orgId,
|
|
519
542
|
...(record.projectId === undefined ? {} : { projectId: record.projectId }),
|