tapimo 0.5.2 → 0.6.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/dist/App.d.ts +13987 -5755
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js.map +1 -1
- package/dist/admin/App.d.ts +910 -412
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +4 -0
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/early-access.d.ts +148 -0
- package/dist/admin/apps/early-access.d.ts.map +1 -0
- package/dist/admin/apps/early-access.js +133 -0
- package/dist/admin/apps/early-access.js.map +1 -0
- package/dist/admin/apps/verified-tokens.d.ts +643 -118
- package/dist/admin/apps/verified-tokens.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.js +214 -1
- package/dist/admin/apps/verified-tokens.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 +45 -33
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/data/App.d.ts +3733 -2428
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +11 -6
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts +34 -1
- package/dist/apps/data/FundingQuoteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.js +193 -2
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
- package/dist/apps/data/FxOracle.d.ts +71 -0
- package/dist/apps/data/FxOracle.d.ts.map +1 -0
- package/dist/apps/data/FxOracle.js +97 -0
- package/dist/apps/data/FxOracle.js.map +1 -0
- package/dist/apps/data/index.d.ts +1 -0
- package/dist/apps/data/index.d.ts.map +1 -1
- package/dist/apps/data/index.js +1 -0
- package/dist/apps/data/index.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +3575 -1358
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +230 -88
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +963 -342
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/addresses.js +11 -9
- package/dist/apps/data/routes/addresses.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +108 -57
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +88 -45
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +169 -169
- package/dist/apps/data/routes/exchanges.d.ts +952 -201
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +518 -76
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/fee-amm.d.ts +24 -24
- package/dist/apps/data/routes/funding.d.ts +8 -2
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +32 -6
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/receipts.d.ts +392 -315
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +152 -77
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +166 -173
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +14 -12
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +277 -536
- package/dist/apps/data/routes/transactions.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.js +96 -77
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts +82 -17
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +70 -13
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +307 -0
- package/dist/apps/data/routes/valuation.d.ts.map +1 -0
- package/dist/apps/data/routes/valuation.js +292 -0
- package/dist/apps/data/routes/valuation.js.map +1 -0
- package/dist/apps/data/routes/verified-tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/verified-tokens.js +7 -5
- package/dist/apps/data/routes/verified-tokens.js.map +1 -1
- package/dist/apps/data/routes/webhooks.d.ts +459 -479
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +124 -25
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +1 -1
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/management/App.d.ts +8606 -1681
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +12 -1
- package/dist/apps/management/App.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +23 -0
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +5 -3
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +88 -48
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +10 -2
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +218 -0
- package/dist/apps/management/routes/faucet.d.ts.map +1 -0
- package/dist/apps/management/routes/faucet.js +139 -0
- package/dist/apps/management/routes/faucet.js.map +1 -0
- package/dist/apps/management/routes/invitations.d.ts +121 -121
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +3 -0
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/invite-links.d.ts +197 -168
- package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
- package/dist/apps/management/routes/invite-links.js +26 -1
- package/dist/apps/management/routes/invite-links.js.map +1 -1
- package/dist/apps/management/routes/me.d.ts +2 -0
- package/dist/apps/management/routes/me.d.ts.map +1 -1
- package/dist/apps/management/routes/me.js +4 -0
- package/dist/apps/management/routes/me.js.map +1 -1
- package/dist/apps/management/routes/members.d.ts +49 -49
- package/dist/apps/management/routes/orgs.d.ts +91 -91
- package/dist/apps/management/routes/projects.d.ts +115 -115
- package/dist/apps/management/routes/usage.d.ts +35 -32
- package/dist/apps/management/routes/usage.d.ts.map +1 -1
- package/dist/apps/management/routes/usage.js +4 -0
- package/dist/apps/management/routes/usage.js.map +1 -1
- package/dist/apps/management/routes/verified-token-requests.d.ts +697 -0
- package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -0
- package/dist/apps/management/routes/verified-token-requests.js +271 -0
- package/dist/apps/management/routes/verified-token-requests.js.map +1 -0
- package/dist/apps/management/routes/webhooks.d.ts +6977 -0
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -0
- package/dist/apps/management/routes/webhooks.js +573 -0
- package/dist/apps/management/routes/webhooks.js.map +1 -0
- package/dist/apps/mcp.d.ts +9 -2
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +9 -4
- package/dist/apps/mcp.js.map +1 -1
- package/dist/cli/Cli.d.ts.map +1 -1
- package/dist/cli/Cli.js +7 -2
- package/dist/cli/Cli.js.map +1 -1
- package/dist/db/Db.d.ts +4 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +83 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +41 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +45 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/earlyAccess.d.ts +50 -0
- package/dist/db/tables/earlyAccess.d.ts.map +1 -0
- package/dist/db/tables/earlyAccess.js +63 -0
- package/dist/db/tables/earlyAccess.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +12 -1
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/invitations.js +23 -0
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/inviteLinks.d.ts +10 -2
- package/dist/db/tables/inviteLinks.d.ts.map +1 -1
- package/dist/db/tables/inviteLinks.js +10 -1
- package/dist/db/tables/inviteLinks.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 +2 -0
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/verifiedTokenRequests.d.ts +94 -0
- package/dist/db/tables/verifiedTokenRequests.d.ts.map +1 -0
- package/dist/db/tables/verifiedTokenRequests.js +120 -0
- package/dist/db/tables/verifiedTokenRequests.js.map +1 -0
- package/dist/db/tables/webhookSubscriptions.d.ts +14 -0
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +16 -0
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +43 -2
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +60 -45
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/FundingQuotes.d.ts +69 -15
- package/dist/internal/FundingQuotes.d.ts.map +1 -1
- package/dist/internal/FundingQuotes.js +74 -25
- package/dist/internal/FundingQuotes.js.map +1 -1
- package/dist/internal/Log.d.ts +19 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +2 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +9 -0
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +2 -2
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +3 -0
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/Schema.d.ts +14 -5
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +23 -5
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Value.d.ts +18 -0
- package/dist/internal/Value.d.ts.map +1 -1
- package/dist/internal/Value.js +11 -1
- package/dist/internal/Value.js.map +1 -1
- package/dist/internal/VerifiedTokens.d.ts.map +1 -1
- package/dist/internal/VerifiedTokens.js +10 -1
- package/dist/internal/VerifiedTokens.js.map +1 -1
- package/dist/internal/Viem.d.ts +1 -1
- package/dist/internal/Webhooks.d.ts +24 -3
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +34 -8
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +2 -2
- package/src/App.ts +9 -1
- package/src/Client.test-d.ts +638 -6
- package/src/admin/App.ts +4 -0
- package/src/admin/apps/early-access.test.ts +125 -0
- package/src/admin/apps/early-access.ts +175 -0
- package/src/admin/apps/verified-tokens.test.ts +201 -0
- package/src/admin/apps/verified-tokens.ts +532 -286
- package/src/admin/ui/plugin.ts +2 -1
- package/src/admin/ui/src/lib/early-access.ts +63 -0
- package/src/admin/ui/src/lib/verified-tokens.ts +79 -0
- package/src/admin/ui/src/routeTree.gen.ts +33 -2
- package/src/admin/ui/src/routes/__root.tsx +2 -1
- package/src/admin/ui/src/routes/early-access.tsx +157 -0
- package/src/admin/ui/src/routes/verified-tokens.tsx +230 -19
- package/src/admin/ui.generated.ts +48 -33
- package/src/apps/data/App.ts +22 -14
- package/src/apps/data/FundingQuoteProviders.test.ts +298 -0
- package/src/apps/data/FundingQuoteProviders.ts +257 -4
- package/src/apps/data/FxOracle.test.ts +118 -0
- package/src/apps/data/FxOracle.ts +153 -0
- package/src/apps/data/index.ts +1 -0
- package/src/apps/data/routes/activities.test.ts +173 -3
- package/src/apps/data/routes/activities.ts +403 -132
- package/src/apps/data/routes/addresses.ts +19 -9
- package/src/apps/data/routes/balances.test.ts +144 -11
- package/src/apps/data/routes/balances.ts +140 -51
- package/src/apps/data/routes/exchanges.test.ts +540 -9
- package/src/apps/data/routes/exchanges.ts +690 -103
- package/src/apps/data/routes/funding.test.ts +84 -0
- package/src/apps/data/routes/funding.ts +38 -6
- package/src/apps/data/routes/receipts.test.ts +125 -23
- package/src/apps/data/routes/receipts.ts +205 -100
- package/src/apps/data/routes/tokens.test.ts +43 -0
- package/src/apps/data/routes/tokens.ts +27 -12
- package/src/apps/data/routes/transactions.test.ts +72 -19
- package/src/apps/data/routes/transactions.ts +162 -92
- package/src/apps/data/routes/transfers.test.ts +146 -4
- package/src/apps/data/routes/transfers.ts +91 -13
- package/src/apps/data/routes/valuation.test.ts +422 -0
- package/src/apps/data/routes/valuation.ts +423 -0
- package/src/apps/data/routes/verified-tokens.ts +14 -2
- package/src/apps/data/routes/webhooks.test.ts +56 -8
- package/src/apps/data/routes/webhooks.ts +144 -27
- package/src/apps/index.ts +1 -1
- package/src/apps/management/App.ts +13 -0
- package/src/apps/management/routes/api-keys.test.ts +87 -0
- package/src/apps/management/routes/api-keys.ts +4 -3
- package/src/apps/management/routes/billing.test.ts +112 -1
- package/src/apps/management/routes/billing.ts +7 -2
- package/src/apps/management/routes/faucet.test.ts +79 -0
- package/src/apps/management/routes/faucet.ts +185 -0
- package/src/apps/management/routes/invitations.test.ts +172 -0
- package/src/apps/management/routes/invitations.ts +3 -0
- package/src/apps/management/routes/invite-links.test.ts +117 -0
- package/src/apps/management/routes/invite-links.ts +43 -1
- package/src/apps/management/routes/me.test.ts +140 -88
- package/src/apps/management/routes/me.ts +6 -0
- package/src/apps/management/routes/usage.test.ts +6 -0
- package/src/apps/management/routes/usage.ts +7 -0
- package/src/apps/management/routes/verified-token-requests.test.ts +377 -0
- package/src/apps/management/routes/verified-token-requests.ts +397 -0
- package/src/apps/management/routes/webhooks.test.ts +426 -0
- package/src/apps/management/routes/webhooks.ts +672 -0
- package/src/apps/mcp.test.ts +13 -0
- package/src/apps/mcp.ts +18 -5
- package/src/cli/Cli.test.ts +36 -0
- package/src/cli/Cli.ts +7 -2
- package/src/db/Db.test.ts +19 -0
- package/src/db/Db.ts +95 -0
- package/src/db/Schema.ts +53 -0
- package/src/db/tables/earlyAccess.ts +86 -0
- package/src/db/tables/invitations.ts +26 -1
- package/src/db/tables/inviteLinks.ts +22 -3
- package/src/db/tables/sponsoredTransactions.ts +4 -0
- package/src/db/tables/verifiedTokenRequests.test.ts +200 -0
- package/src/db/tables/verifiedTokenRequests.ts +182 -0
- package/src/db/tables/webhookSubscriptions.ts +31 -0
- package/src/internal/Auth.ts +73 -45
- package/src/internal/FundingQuotes.test.ts +143 -1
- package/src/internal/FundingQuotes.ts +95 -28
- package/src/internal/Log.test-d.ts +21 -0
- package/src/internal/Log.test.ts +50 -0
- package/src/internal/Log.ts +24 -1
- package/src/internal/MetricSink.test.ts +160 -0
- package/src/internal/MetricSink.ts +10 -0
- package/src/internal/OpenApi.ts +3 -0
- package/src/internal/Schema.ts +50 -5
- package/src/internal/Value.test.ts +20 -0
- package/src/internal/Value.ts +24 -1
- package/src/internal/VerifiedTokens.ts +9 -1
- package/src/internal/Webhooks.test.ts +140 -0
- package/src/internal/Webhooks.ts +53 -9
|
@@ -21,7 +21,7 @@ import * as Receipts from './receipts.js'
|
|
|
21
21
|
import * as Transactions from './transactions.js'
|
|
22
22
|
import * as Transfers from './transfers.js'
|
|
23
23
|
|
|
24
|
-
/** Default cap on
|
|
24
|
+
/** Default cap on live subscriptions per owner, overridable via `webhook.maxPerOwner`. */
|
|
25
25
|
const defaultMaxPerOwner = 100
|
|
26
26
|
|
|
27
27
|
/**
|
|
@@ -1317,10 +1317,13 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1317
1317
|
responses: OpenApi.responses({
|
|
1318
1318
|
errors: {
|
|
1319
1319
|
400: {
|
|
1320
|
-
codes: ['chain_id_unsupported'],
|
|
1321
|
-
description: '
|
|
1320
|
+
codes: ['body_invalid', 'chain_id_unsupported', 'filters_invalid', 'url_invalid'],
|
|
1321
|
+
description: 'The request, destination, filters, or chain are invalid.',
|
|
1322
|
+
},
|
|
1323
|
+
403: {
|
|
1324
|
+
codes: ['limit_exceeded'],
|
|
1325
|
+
description: 'You have reached the webhook subscription limit.',
|
|
1322
1326
|
},
|
|
1323
|
-
403: 'You have reached the webhook subscription limit.',
|
|
1324
1327
|
},
|
|
1325
1328
|
success: {
|
|
1326
1329
|
description:
|
|
@@ -1347,12 +1350,9 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1347
1350
|
|
|
1348
1351
|
const body = c.req.valid('json')
|
|
1349
1352
|
const chainId = body.chainId ?? c.get('chainId')
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
message: 'Webhook subscriptions do not support Zone chains.',
|
|
1354
|
-
status: 400,
|
|
1355
|
-
})
|
|
1353
|
+
const supportedChainIds = new Set(webhook.supportedChainIds)
|
|
1354
|
+
if (!supportedChainIds.has(chainId))
|
|
1355
|
+
return Response.unsupportedChainId(c, chainId, supportedChainIds)
|
|
1356
1356
|
|
|
1357
1357
|
try {
|
|
1358
1358
|
const subscription = await Webhooks.createSubscription(
|
|
@@ -1373,7 +1373,7 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1373
1373
|
200,
|
|
1374
1374
|
)
|
|
1375
1375
|
} catch (cause) {
|
|
1376
|
-
return
|
|
1376
|
+
return createMutationError(c, cause)
|
|
1377
1377
|
}
|
|
1378
1378
|
},
|
|
1379
1379
|
)
|
|
@@ -1747,6 +1747,16 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1747
1747
|
operationId: 'updateWebhook',
|
|
1748
1748
|
responses: OpenApi.responses({
|
|
1749
1749
|
errors: {
|
|
1750
|
+
400: {
|
|
1751
|
+
codes: [
|
|
1752
|
+
'body_invalid',
|
|
1753
|
+
'destination_transition_invalid',
|
|
1754
|
+
'filters_invalid',
|
|
1755
|
+
'param_invalid',
|
|
1756
|
+
'url_invalid',
|
|
1757
|
+
],
|
|
1758
|
+
description: 'The update, destination, or filters are invalid.',
|
|
1759
|
+
},
|
|
1750
1760
|
404: {
|
|
1751
1761
|
description: 'No webhook subscription was found for that ID.',
|
|
1752
1762
|
codes: ['webhook_not_found'],
|
|
@@ -1798,7 +1808,7 @@ export function webhooks(options: webhooks.Options = {}) {
|
|
|
1798
1808
|
200,
|
|
1799
1809
|
)
|
|
1800
1810
|
} catch (cause) {
|
|
1801
|
-
return
|
|
1811
|
+
return updateMutationError(c, cause)
|
|
1802
1812
|
}
|
|
1803
1813
|
},
|
|
1804
1814
|
)
|
|
@@ -1987,13 +1997,14 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
|
|
|
1987
1997
|
let accepted: readonly [number, number] | undefined
|
|
1988
1998
|
let failed = false
|
|
1989
1999
|
for (const item of page.items) {
|
|
1990
|
-
//
|
|
1991
|
-
//
|
|
1992
|
-
// payloads) can render `amount` in token units. Non-fatal on failure.
|
|
2000
|
+
// Token metadata is best-effort for webhooks: transfers use it for display,
|
|
2001
|
+
// while transactions expose fee-token references without blocking delivery.
|
|
1993
2002
|
const data =
|
|
1994
2003
|
eventType === 'token:transfer'
|
|
1995
2004
|
? await withTokenMetadata(item.data, { cache: cache_metadata, chainId, client })
|
|
1996
|
-
:
|
|
2005
|
+
: eventType === 'transaction:included'
|
|
2006
|
+
? await withFeeTokenMetadata(item.data, { cache: cache_metadata, chainId, client })
|
|
2007
|
+
: item.data
|
|
1997
2008
|
const envelope = Webhooks.buildEnvelope({
|
|
1998
2009
|
blockNumber: item.cursor[0],
|
|
1999
2010
|
createdAt: now(),
|
|
@@ -2106,27 +2117,45 @@ async function withTokenMetadata(
|
|
|
2106
2117
|
const address = (data as Record<string, unknown>)['address']
|
|
2107
2118
|
if (typeof address !== 'string') return data
|
|
2108
2119
|
const token = await resolveTokenMetadata(address, deps)
|
|
2109
|
-
return token
|
|
2120
|
+
return token
|
|
2121
|
+
? {
|
|
2122
|
+
...(data as Record<string, unknown>),
|
|
2123
|
+
token: { decimals: token.decimals, symbol: token.symbol },
|
|
2124
|
+
}
|
|
2125
|
+
: data
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
/** Core RPC metadata needed by a humanized token reference. */
|
|
2129
|
+
type TokenMetadata = {
|
|
2130
|
+
currency: string
|
|
2131
|
+
decimals: number
|
|
2132
|
+
name: string
|
|
2133
|
+
symbol: string
|
|
2110
2134
|
}
|
|
2111
2135
|
|
|
2112
2136
|
/**
|
|
2113
|
-
* Resolves
|
|
2137
|
+
* Resolves core token metadata via `token.getMetadata`, memoized per
|
|
2114
2138
|
* `(chainId, address)` with a short TTL so a busy transfer stream shares one RPC
|
|
2115
|
-
* lookup per token. Returns `undefined` on any failure
|
|
2139
|
+
* lookup per token. Returns `undefined` on any failure; metadata is a rendering
|
|
2116
2140
|
* nicety, never a delivery blocker.
|
|
2117
2141
|
*/
|
|
2118
2142
|
async function resolveTokenMetadata(
|
|
2119
2143
|
address: string,
|
|
2120
2144
|
deps: { cache: Store.Store; chainId: number; client: Viem.getClient.ReturnType },
|
|
2121
|
-
): Promise<
|
|
2145
|
+
): Promise<TokenMetadata | undefined> {
|
|
2122
2146
|
try {
|
|
2123
2147
|
return await Store.memoize(
|
|
2124
2148
|
async () => {
|
|
2125
2149
|
const meta = await deps.client.token.getMetadata({ token: address as `0x${string}` })
|
|
2126
|
-
return {
|
|
2150
|
+
return {
|
|
2151
|
+
currency: meta.currency,
|
|
2152
|
+
decimals: meta.decimals,
|
|
2153
|
+
name: meta.name,
|
|
2154
|
+
symbol: meta.symbol,
|
|
2155
|
+
}
|
|
2127
2156
|
},
|
|
2128
2157
|
{
|
|
2129
|
-
key: `webhook:token-meta:
|
|
2158
|
+
key: `webhook:token-meta:v2:${deps.chainId}:${address}`,
|
|
2130
2159
|
store: deps.cache,
|
|
2131
2160
|
ttl: Ttl.minutes(5),
|
|
2132
2161
|
},
|
|
@@ -2137,6 +2166,69 @@ async function resolveTokenMetadata(
|
|
|
2137
2166
|
}
|
|
2138
2167
|
}
|
|
2139
2168
|
|
|
2169
|
+
/** Returns the address from an unenriched or enriched fee-token value. */
|
|
2170
|
+
function feeTokenAddress(value: unknown) {
|
|
2171
|
+
if (typeof value === 'string') return value
|
|
2172
|
+
if (typeof value !== 'object' || value === null) return undefined
|
|
2173
|
+
const address = (value as Record<string, unknown>)['address']
|
|
2174
|
+
return typeof address === 'string' ? address : undefined
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
/** Replaces an address-valued fee token with its resolved reference, or omits it on failure. */
|
|
2178
|
+
function withResolvedFeeToken(data: Record<string, unknown>, metadata: Map<string, TokenMetadata>) {
|
|
2179
|
+
const address = feeTokenAddress(data['feeToken'])
|
|
2180
|
+
if (!address) return data
|
|
2181
|
+
const result = { ...data }
|
|
2182
|
+
delete result['feeToken']
|
|
2183
|
+
const token = metadata.get(address.toLowerCase())
|
|
2184
|
+
if (token) result['feeToken'] = { address, ...token }
|
|
2185
|
+
return result
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
/** Adds best-effort fee-token references to a transaction and its embedded receipt. */
|
|
2189
|
+
async function withFeeTokenMetadata(
|
|
2190
|
+
data: unknown,
|
|
2191
|
+
deps: {
|
|
2192
|
+
cache: Store.Store
|
|
2193
|
+
chainId: number
|
|
2194
|
+
client: Viem.getClient.ReturnType
|
|
2195
|
+
},
|
|
2196
|
+
): Promise<unknown> {
|
|
2197
|
+
if (typeof data !== 'object' || data === null) return data
|
|
2198
|
+
const transaction = data as Record<string, unknown>
|
|
2199
|
+
const meta = transaction['meta']
|
|
2200
|
+
const receipt =
|
|
2201
|
+
typeof meta === 'object' && meta !== null
|
|
2202
|
+
? (meta as Record<string, unknown>)['receipt']
|
|
2203
|
+
: undefined
|
|
2204
|
+
const addresses = new Set<string>()
|
|
2205
|
+
const transactionAddress = feeTokenAddress(transaction['feeToken'])
|
|
2206
|
+
if (transactionAddress) addresses.add(transactionAddress.toLowerCase())
|
|
2207
|
+
if (typeof receipt === 'object' && receipt !== null) {
|
|
2208
|
+
const receiptAddress = feeTokenAddress((receipt as Record<string, unknown>)['feeToken'])
|
|
2209
|
+
if (receiptAddress) addresses.add(receiptAddress.toLowerCase())
|
|
2210
|
+
}
|
|
2211
|
+
if (addresses.size === 0) return data
|
|
2212
|
+
|
|
2213
|
+
const resolved = await Promise.all(
|
|
2214
|
+
[...addresses].map(
|
|
2215
|
+
async (address) => [address, await resolveTokenMetadata(address, deps)] as const,
|
|
2216
|
+
),
|
|
2217
|
+
)
|
|
2218
|
+
const metadata = new Map(
|
|
2219
|
+
resolved.filter((entry): entry is readonly [string, TokenMetadata] => entry[1] !== undefined),
|
|
2220
|
+
)
|
|
2221
|
+
const result = withResolvedFeeToken(transaction, metadata)
|
|
2222
|
+
if (typeof receipt !== 'object' || receipt === null) return result
|
|
2223
|
+
return {
|
|
2224
|
+
...result,
|
|
2225
|
+
meta: {
|
|
2226
|
+
...(meta as Record<string, unknown>),
|
|
2227
|
+
receipt: withResolvedFeeToken(receipt as Record<string, unknown>, metadata),
|
|
2228
|
+
},
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2140
2232
|
/** Returns the live RPC head block number for the chain. */
|
|
2141
2233
|
export async function getRpcHead(client: Viem.getClient.ReturnType): Promise<number> {
|
|
2142
2234
|
return Number(await client.getBlockNumber())
|
|
@@ -2883,8 +2975,8 @@ const txFields = {
|
|
|
2883
2975
|
* Scans included transactions over the confirmed window. Each block is fetched
|
|
2884
2976
|
* with full transactions via `eth_getBlockByNumber(n, true)`; receipts are
|
|
2885
2977
|
* fetched lazily (only when a `status`/`gasUsed`/`feePayer` filter needs them).
|
|
2886
|
-
* The delivered payload
|
|
2887
|
-
*
|
|
2978
|
+
* The delivered payload starts from `Transactions.humanizeTransaction`; the
|
|
2979
|
+
* poller then resolves its fee-token references.
|
|
2888
2980
|
*/
|
|
2889
2981
|
async function scanTxs(options: {
|
|
2890
2982
|
cache: ChainCache
|
|
@@ -2921,13 +3013,19 @@ async function scanTxs(options: {
|
|
|
2921
3013
|
if (!matchesTx(tx, f, includeCalls, receipt, timestamp, n)) continue
|
|
2922
3014
|
const parsed = Transactions.schema.Rpc.Transaction.safeParse(tx)
|
|
2923
3015
|
if (!parsed.success) continue
|
|
2924
|
-
let data = Transactions.humanizeTransaction(parsed.data)
|
|
3016
|
+
let data: Record<string, unknown> = Transactions.humanizeTransaction(parsed.data)
|
|
2925
3017
|
// Embed the humanized receipt under `meta.receipt`, matching the read
|
|
2926
3018
|
// endpoint's `include=receipt` shape so `data` stays a valid envelope.
|
|
2927
3019
|
if (includeReceipt && receipt) {
|
|
2928
3020
|
const r = Receipts.schema.Rpc.Receipt.safeParse(receipt)
|
|
2929
3021
|
if (r.success)
|
|
2930
|
-
data = {
|
|
3022
|
+
data = {
|
|
3023
|
+
...data,
|
|
3024
|
+
meta: {
|
|
3025
|
+
...(data['meta'] as Record<string, unknown>),
|
|
3026
|
+
receipt: Receipts.humanizeReceipt(r.data),
|
|
3027
|
+
},
|
|
3028
|
+
}
|
|
2931
3029
|
}
|
|
2932
3030
|
items.push({ cursor: [n, index], data })
|
|
2933
3031
|
}
|
|
@@ -3040,7 +3138,7 @@ function redactDestination(destination: WebhookDestination.Destination) {
|
|
|
3040
3138
|
return destination
|
|
3041
3139
|
}
|
|
3042
3140
|
|
|
3043
|
-
function
|
|
3141
|
+
function createMutationError(c: Context<App.Environment>, cause: unknown) {
|
|
3044
3142
|
if (cause instanceof Webhooks.InvalidUrlError)
|
|
3045
3143
|
return Response.error(c, { code: 'url_invalid', message: cause.message, status: 400 })
|
|
3046
3144
|
if (cause instanceof Webhooks.InvalidFilterError)
|
|
@@ -3055,6 +3153,25 @@ function mutationError(c: Context<App.Environment>, cause: unknown) {
|
|
|
3055
3153
|
return Response.upstream(c, cause)
|
|
3056
3154
|
}
|
|
3057
3155
|
|
|
3156
|
+
function updateMutationError(c: Context<App.Environment>, cause: unknown) {
|
|
3157
|
+
if (cause instanceof Webhooks.InvalidDestinationTransitionError)
|
|
3158
|
+
return Response.error(c, {
|
|
3159
|
+
code: 'destination_transition_invalid',
|
|
3160
|
+
message: cause.message,
|
|
3161
|
+
status: 400,
|
|
3162
|
+
})
|
|
3163
|
+
if (cause instanceof Webhooks.InvalidUrlError)
|
|
3164
|
+
return Response.error(c, { code: 'url_invalid', message: cause.message, status: 400 })
|
|
3165
|
+
if (cause instanceof Webhooks.InvalidFilterError)
|
|
3166
|
+
return Response.error(c, {
|
|
3167
|
+
code: 'filters_invalid',
|
|
3168
|
+
details: cause.details,
|
|
3169
|
+
message: cause.message,
|
|
3170
|
+
status: 400,
|
|
3171
|
+
})
|
|
3172
|
+
return Response.upstream(c, cause)
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3058
3175
|
function notFound(c: Context<App.Environment>) {
|
|
3059
3176
|
return Response.error(c, {
|
|
3060
3177
|
code: 'webhook_not_found',
|
package/src/apps/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import * as Metadata from '../metadata.js'
|
|
|
6
6
|
|
|
7
7
|
import { apiKeys } from './routes/api-keys.js'
|
|
8
8
|
import { billing } from './routes/billing.js'
|
|
9
|
+
import { faucet } from './routes/faucet.js'
|
|
9
10
|
import { invitations } from './routes/invitations.js'
|
|
10
11
|
import { inviteLinks } from './routes/invite-links.js'
|
|
11
12
|
import { me } from './routes/me.js'
|
|
@@ -14,6 +15,8 @@ import { orgs } from './routes/orgs.js'
|
|
|
14
15
|
import { projects } from './routes/projects.js'
|
|
15
16
|
import { scopes } from './routes/scopes.js'
|
|
16
17
|
import { usage } from './routes/usage.js'
|
|
18
|
+
import { verifiedTokenRequests } from './routes/verified-token-requests.js'
|
|
19
|
+
import { webhooks } from './routes/webhooks.js'
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* Minimal transactional email sender. Cloudflare deployments wrap the Email
|
|
@@ -105,6 +108,7 @@ export function management(options: management.Options = {}) {
|
|
|
105
108
|
})
|
|
106
109
|
.route('/', apiKeys({ enabled: options.apiKeys !== false }))
|
|
107
110
|
.route('/', billing())
|
|
111
|
+
.route('/', faucet())
|
|
108
112
|
.route('/', invitations())
|
|
109
113
|
.route('/', inviteLinks())
|
|
110
114
|
.route('/', me())
|
|
@@ -113,6 +117,8 @@ export function management(options: management.Options = {}) {
|
|
|
113
117
|
.route('/', projects())
|
|
114
118
|
.route('/', scopes())
|
|
115
119
|
.route('/', usage())
|
|
120
|
+
.route('/', verifiedTokenRequests())
|
|
121
|
+
.route('/', webhooks({ webhook: options.webhook === false ? undefined : options.webhook }))
|
|
116
122
|
return Metadata.attach(app, openapi)
|
|
117
123
|
}
|
|
118
124
|
|
|
@@ -125,6 +131,8 @@ export declare namespace management {
|
|
|
125
131
|
billing?: Billing | undefined
|
|
126
132
|
/** Transactional email sender for management emails (e.g. invitations); omit to disable dispatch — notified flows still work in-product. */
|
|
127
133
|
email?: Email | undefined
|
|
134
|
+
/** Webhook capability shared with the data group and host poller; omit or pass false to disable management. */
|
|
135
|
+
webhook?: App.Webhook | false | undefined
|
|
128
136
|
}
|
|
129
137
|
}
|
|
130
138
|
|
|
@@ -145,6 +153,7 @@ const openapi = {
|
|
|
145
153
|
tags: [
|
|
146
154
|
{ name: 'API Keys', description: 'Credentials for accessing the Tempo API.' },
|
|
147
155
|
{ name: 'Billing', description: 'Organization billing backed by Stripe.' },
|
|
156
|
+
{ name: 'Faucet', description: 'Test token funding for Tempo testnet accounts.' },
|
|
148
157
|
{ name: 'Invitations', description: 'Pending organization invitations.' },
|
|
149
158
|
{ name: 'Invite Links', description: 'Reusable organization invite links.' },
|
|
150
159
|
{ name: 'Members', description: 'Organization team membership.' },
|
|
@@ -152,6 +161,8 @@ const openapi = {
|
|
|
152
161
|
{ name: 'Projects', description: 'Application workspaces within an organization.' },
|
|
153
162
|
{ name: 'Usage', description: 'Request and sponsorship usage for organizations.' },
|
|
154
163
|
{ name: 'Users', description: 'Users on the Tempo Platform.' },
|
|
164
|
+
{ name: 'Verified Token Requests', description: 'Organization requests to add tokens to the curated verified list.' }, // prettier-ignore
|
|
165
|
+
{ name: 'Webhooks', description: 'Organization webhook subscriptions.' },
|
|
155
166
|
],
|
|
156
167
|
'x-tagGroups': [
|
|
157
168
|
{
|
|
@@ -167,6 +178,8 @@ const openapi = {
|
|
|
167
178
|
'Projects',
|
|
168
179
|
'Usage',
|
|
169
180
|
'Users',
|
|
181
|
+
'Verified Token Requests',
|
|
182
|
+
'Webhooks',
|
|
170
183
|
],
|
|
171
184
|
},
|
|
172
185
|
],
|
|
@@ -5,7 +5,9 @@ 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'
|
|
8
9
|
import * as Memberships from '../../../db/tables/memberships.js'
|
|
10
|
+
import * as Users from '../../../db/tables/users.js'
|
|
9
11
|
import * as Keys from './api-keys.js'
|
|
10
12
|
import * as Me from './me.js'
|
|
11
13
|
import * as Orgs from './orgs.js'
|
|
@@ -43,6 +45,7 @@ function createApp(
|
|
|
43
45
|
db?: ReturnType<typeof TestApp.database>
|
|
44
46
|
kv?: { store: State }
|
|
45
47
|
scopes?: readonly Scope.Entry[]
|
|
48
|
+
tmp_earlyAccess?: boolean
|
|
46
49
|
} = {},
|
|
47
50
|
) {
|
|
48
51
|
return TestApp.create({
|
|
@@ -51,6 +54,7 @@ function createApp(
|
|
|
51
54
|
...(options.kv ? { kv: options.kv } : {}),
|
|
52
55
|
...(options.scopes ? { scopes: options.scopes } : {}),
|
|
53
56
|
session: { wallet: { origin } },
|
|
57
|
+
...(options.tmp_earlyAccess === undefined ? {} : { tmp_earlyAccess: options.tmp_earlyAccess }),
|
|
54
58
|
})
|
|
55
59
|
}
|
|
56
60
|
|
|
@@ -671,3 +675,86 @@ describe('keys', () => {
|
|
|
671
675
|
expect(response.status).toBe(403)
|
|
672
676
|
})
|
|
673
677
|
})
|
|
678
|
+
|
|
679
|
+
describe('early access', () => {
|
|
680
|
+
test('rejects mints from non-early-access sessions', async () => {
|
|
681
|
+
const app = createApp({ tmp_earlyAccess: true })
|
|
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' })
|
|
734
|
+
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
|
+
const { org, project } = await fixture(app, cookie)
|
|
741
|
+
|
|
742
|
+
const minted = await app.request(
|
|
743
|
+
`/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
|
|
744
|
+
json('POST', cookie, { scopes: ['data:read'] }),
|
|
745
|
+
)
|
|
746
|
+
expect(minted.status).toBe(200)
|
|
747
|
+
})
|
|
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
|
+
})
|
|
@@ -225,7 +225,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
225
225
|
return new Hono<App.Environment>()
|
|
226
226
|
.post(
|
|
227
227
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys',
|
|
228
|
-
Auth.policy({ session: true }),
|
|
228
|
+
Auth.policy({ session: true, tmp_earlyAccess: true }),
|
|
229
229
|
Auth.ensureOrg({ role: 'admin' }),
|
|
230
230
|
Auth.ensureProject(),
|
|
231
231
|
OpenApi.validate('param', schema.Params, {
|
|
@@ -243,8 +243,8 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
243
243
|
responses: OpenApi.responses({
|
|
244
244
|
errors: {
|
|
245
245
|
403: {
|
|
246
|
-
codes: ['scope_not_issuable'],
|
|
247
|
-
description: 'The requested scopes are not issuable.',
|
|
246
|
+
codes: ['scope_not_issuable', 'early_access_required'],
|
|
247
|
+
description: 'The requested scopes are not issuable, or the caller lacks early access.', // prettier-ignore
|
|
248
248
|
},
|
|
249
249
|
404: 'No accessible project was found for this id.',
|
|
250
250
|
},
|
|
@@ -259,6 +259,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
259
259
|
async (c) => {
|
|
260
260
|
if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
|
|
261
261
|
if (Auth.narrowScope) return Auth.ensureProjectError(c)
|
|
262
|
+
if (Auth.tmp_narrowEarlyAccess) return Auth.tmp_ensureEarlyAccessError(c)
|
|
262
263
|
if (OpenApi.narrowValidation)
|
|
263
264
|
return OpenApi.validationError(c, {
|
|
264
265
|
code: 'param_invalid',
|
|
@@ -2,11 +2,14 @@ import { nanoid } from 'nanoid'
|
|
|
2
2
|
import Stripe from 'stripe'
|
|
3
3
|
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
|
|
4
4
|
|
|
5
|
+
import * as ApiKeys from '../../../ApiKeys.js'
|
|
5
6
|
import * as TestApp from '../../../../test/App.js'
|
|
6
7
|
import * as TestStripe from '../../../../test/Stripe.js'
|
|
8
|
+
import * as EarlyAccess from '../../../db/tables/earlyAccess.js'
|
|
7
9
|
import * as Memberships from '../../../db/tables/memberships.js'
|
|
8
10
|
import * as SponsoredTransactions from '../../../db/tables/sponsoredTransactions.js'
|
|
9
11
|
import * as StripeCustomers from '../../../db/tables/stripeCustomers.js'
|
|
12
|
+
import * as Users from '../../../db/tables/users.js'
|
|
10
13
|
import * as Viem from '../../../internal/Viem.js'
|
|
11
14
|
import * as core_Billing from '../Billing.js'
|
|
12
15
|
import type * as App from '../../../App.js'
|
|
@@ -59,14 +62,21 @@ function billingWithSandbox(): App.Billing {
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
function createApp(
|
|
62
|
-
options: {
|
|
65
|
+
options: {
|
|
66
|
+
billing?: App.Billing | undefined
|
|
67
|
+
db?: TestApp.create.Options['db']
|
|
68
|
+
kv?: { store: ReturnType<typeof TestApp.kvStore> }
|
|
69
|
+
tmp_earlyAccess?: boolean
|
|
70
|
+
} = {},
|
|
63
71
|
) {
|
|
64
72
|
// prettier-ignore
|
|
65
73
|
return TestApp.create({
|
|
66
74
|
auth: { superAdmin: { secret } },
|
|
67
75
|
billing: options.billing ?? billing(),
|
|
68
76
|
db: options.db ?? TestApp.database(),
|
|
77
|
+
...(options.kv ? { kv: options.kv } : {}),
|
|
69
78
|
session: { wallet: { origin } },
|
|
79
|
+
...(options.tmp_earlyAccess === undefined ? {} : { tmp_earlyAccess: options.tmp_earlyAccess }),
|
|
70
80
|
})
|
|
71
81
|
}
|
|
72
82
|
|
|
@@ -356,6 +366,107 @@ describe('POST /orgs/:orgId/billing/stripe/checkout', () => {
|
|
|
356
366
|
})
|
|
357
367
|
})
|
|
358
368
|
|
|
369
|
+
describe('early access', () => {
|
|
370
|
+
test('behavior: checkout requires early access', async () => {
|
|
371
|
+
const app = createApp({ tmp_earlyAccess: true })
|
|
372
|
+
const { cookie, orgId } = await seeded(app)
|
|
373
|
+
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
374
|
+
expect(response.status).toBe(403)
|
|
375
|
+
const { requestId, ...body } = (await response.json()) as { requestId?: string }
|
|
376
|
+
expect(requestId).toBeDefined()
|
|
377
|
+
expect(body).toMatchInlineSnapshot(`
|
|
378
|
+
{
|
|
379
|
+
"error": {
|
|
380
|
+
"code": "early_access_required",
|
|
381
|
+
"message": "Requires early access. Ask a teammate with access to invite you by email, or contact Tempo.",
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
`)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
test('behavior: the gate precedes membership resolution', async () => {
|
|
388
|
+
// The gate depends only on the caller, so it leaks nothing about the org;
|
|
389
|
+
// early-access strangers still get the membership 404.
|
|
390
|
+
const db = TestApp.database()
|
|
391
|
+
const app = createApp({ db, tmp_earlyAccess: true })
|
|
392
|
+
await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
|
|
393
|
+
const { orgId } = await seeded(app)
|
|
394
|
+
|
|
395
|
+
const other = await session(app)
|
|
396
|
+
const gated = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(other))
|
|
397
|
+
expect(gated.status).toBe(403)
|
|
398
|
+
expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
|
|
399
|
+
|
|
400
|
+
const granted = await session(app)
|
|
401
|
+
const me = await app.request('/v1/me', { headers: { cookie: granted } })
|
|
402
|
+
const { id: userId } = (await me.json()) as { id: string }
|
|
403
|
+
await Users.setEmail(db, userId, 'stranger@example.org')
|
|
404
|
+
expect(
|
|
405
|
+
(await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(granted))).status,
|
|
406
|
+
).toBe(404)
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test('behavior: the gate precedes the owner-role check', async () => {
|
|
410
|
+
const db = TestApp.database()
|
|
411
|
+
const app = createApp({ db, tmp_earlyAccess: true })
|
|
412
|
+
await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
|
|
413
|
+
const { orgId } = await seeded(app)
|
|
414
|
+
const member = await session(app)
|
|
415
|
+
const me = await app.request('/v1/me', { headers: { cookie: member } })
|
|
416
|
+
const { id: userId } = (await me.json()) as { id: string }
|
|
417
|
+
await Memberships.create(db, { orgId, role: 'member', userId })
|
|
418
|
+
|
|
419
|
+
const gated = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(member))
|
|
420
|
+
expect(gated.status).toBe(403)
|
|
421
|
+
expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
|
|
422
|
+
|
|
423
|
+
// Early-access members still fail the owner-role check.
|
|
424
|
+
await Users.setEmail(db, userId, 'member@example.org')
|
|
425
|
+
const write = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(member))
|
|
426
|
+
expect(write.status).toBe(403)
|
|
427
|
+
expect(((await write.json()) as { error: { code: string } }).error.code).toBe('forbidden')
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('behavior: early-access owners pass the gate', async () => {
|
|
431
|
+
const db = TestApp.database()
|
|
432
|
+
const app = createApp({ db, tmp_earlyAccess: true })
|
|
433
|
+
await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
|
|
434
|
+
const { cookie, orgId } = await seeded(app)
|
|
435
|
+
const me = await app.request('/v1/me', { headers: { cookie } })
|
|
436
|
+
const { id: userId } = (await me.json()) as { id: string }
|
|
437
|
+
await Users.setEmail(db, userId, 'dev@example.org')
|
|
438
|
+
|
|
439
|
+
// The unreachable Stripe upstream (502) proves the request passed the gate.
|
|
440
|
+
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
441
|
+
expect(response.status).toBe(502)
|
|
442
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe('upstream_error') // prettier-ignore
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
test('behavior: management keys pass the gate', async () => {
|
|
446
|
+
const store = TestApp.kvStore({ keys: [] })
|
|
447
|
+
const app = createApp({ tmp_earlyAccess: true, kv: { store } })
|
|
448
|
+
const { orgId } = await seeded(app)
|
|
449
|
+
const { token } = await ApiKeys.mint(store, { orgId, scopes: ['management:write'] })
|
|
450
|
+
|
|
451
|
+
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, {
|
|
452
|
+
headers: { 'tempo-api-key': token },
|
|
453
|
+
method: 'POST',
|
|
454
|
+
})
|
|
455
|
+
expect(response.status).toBe(502)
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
test('behavior: the super admin passes the gate', async () => {
|
|
459
|
+
const app = createApp({ tmp_earlyAccess: true })
|
|
460
|
+
const { orgId } = await seeded(app)
|
|
461
|
+
|
|
462
|
+
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, {
|
|
463
|
+
...as(secret),
|
|
464
|
+
method: 'POST',
|
|
465
|
+
})
|
|
466
|
+
expect(response.status).toBe(502)
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
|
|
359
470
|
describe('POST /orgs/:orgId/billing/stripe/manage', () => {
|
|
360
471
|
test('behavior: 404 before any billing exists', async () => {
|
|
361
472
|
const app = createApp()
|
|
@@ -380,7 +380,7 @@ export function billing() {
|
|
|
380
380
|
)
|
|
381
381
|
.post(
|
|
382
382
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/stripe/checkout',
|
|
383
|
-
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
383
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true, tmp_earlyAccess: true }), // prettier-ignore
|
|
384
384
|
Auth.ensureOrg({ role: 'owner' }),
|
|
385
385
|
OpenApi.validate('param', schema.Params, {
|
|
386
386
|
code: 'param_invalid',
|
|
@@ -394,7 +394,10 @@ export function billing() {
|
|
|
394
394
|
operationId: 'createStripeCheckout',
|
|
395
395
|
responses: OpenApi.responses({
|
|
396
396
|
errors: {
|
|
397
|
-
403:
|
|
397
|
+
403: {
|
|
398
|
+
codes: ['api_key_forbidden', 'early_access_required', 'forbidden'],
|
|
399
|
+
description: 'Requires the owner role and early access.',
|
|
400
|
+
},
|
|
398
401
|
404: 'No accessible organization was found for this id.',
|
|
399
402
|
501: 'Billing is not configured on this deployment.',
|
|
400
403
|
},
|
|
@@ -409,6 +412,8 @@ export function billing() {
|
|
|
409
412
|
async (c) => {
|
|
410
413
|
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
411
414
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
415
|
+
if (Auth.narrowOrgRole) return Auth.ensureOrgRoleError(c)
|
|
416
|
+
if (Auth.tmp_narrowEarlyAccess) return Auth.tmp_ensureEarlyAccessError(c)
|
|
412
417
|
if (OpenApi.narrowValidation)
|
|
413
418
|
return OpenApi.validationError(c, {
|
|
414
419
|
code: 'param_invalid',
|