tapimo 0.5.3 → 0.7.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 +15830 -7220
- 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 +1011 -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/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 +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/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/apps/verified-tokens.d.ts +526 -1
- 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 +95 -75
- 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 +6260 -4924
- 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 +194 -16
- 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 +3599 -1382
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +250 -88
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +872 -251
- 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 +77 -26
- 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/blocks.d.ts +39 -39
- package/dist/apps/data/routes/coingecko.d.ts +54 -54
- package/dist/apps/data/routes/exchanges.d.ts +856 -105
- 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 +81 -67
- 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 +80 -36
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +65 -31
- package/dist/apps/data/routes/funding.js.map +1 -1
- 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 +167 -174
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +15 -13
- 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 +157 -81
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +272 -130
- 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 +36 -36
- 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 +103 -123
- 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 +2 -1
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +2 -1
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/management/App.d.ts +8554 -1692
- 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 +7 -0
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +17 -3
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +157 -162
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +31 -23
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +232 -0
- package/dist/apps/management/routes/faucet.d.ts.map +1 -0
- package/dist/apps/management/routes/faucet.js +154 -0
- package/dist/apps/management/routes/faucet.js.map +1 -0
- package/dist/apps/management/routes/invitations.d.ts +60 -60
- package/dist/apps/management/routes/invite-links.d.ts +49 -20
- 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 +6 -6
- package/dist/apps/management/routes/members.d.ts +6 -6
- package/dist/apps/management/routes/orgs.d.ts +42 -42
- 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/projects.d.ts +44 -44
- package/dist/apps/management/routes/usage.d.ts +3 -0
- 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/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 +56 -9
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/db/Db.d.ts +6 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +110 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +53 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +56 -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/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.map +1 -1
- package/dist/db/tables/invitations.js +3 -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/organizations.d.ts +5 -2
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +13 -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 +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/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 +11 -2
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +14 -47
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/FundingQuotes.d.ts +147 -47
- package/dist/internal/FundingQuotes.d.ts.map +1 -1
- package/dist/internal/FundingQuotes.js +119 -33
- package/dist/internal/FundingQuotes.js.map +1 -1
- package/dist/internal/Log.d.ts +37 -5
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +52 -3
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +24 -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/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 +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/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 +7 -2
- package/src/App.test.ts +9 -4
- package/src/App.ts +23 -3
- package/src/Client.test-d.ts +733 -20
- package/src/Scope.ts +5 -0
- package/src/admin/App.test.ts +4 -0
- package/src/admin/App.ts +4 -0
- package/src/admin/apps/early-access.test.ts +125 -0
- package/src/admin/apps/early-access.ts +176 -0
- 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/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/organizations.test.ts +81 -1
- package/src/admin/ui/src/lib/organizations.ts +92 -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/api-keys.tsx +1 -1
- package/src/admin/ui/src/routes/early-access.tsx +162 -0
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +103 -0
- package/src/admin/ui/src/routes/verified-tokens.tsx +230 -19
- package/src/admin/ui.generated.ts +95 -94
- 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 +1 -1
- package/src/apps/data/App.ts +22 -14
- package/src/apps/data/FundingQuoteProviders.test.ts +297 -4
- package/src/apps/data/FundingQuoteProviders.ts +255 -29
- 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 +177 -3
- package/src/apps/data/routes/activities.ts +441 -132
- package/src/apps/data/routes/addresses.ts +19 -9
- package/src/apps/data/routes/balances.test.ts +148 -11
- package/src/apps/data/routes/balances.ts +140 -51
- package/src/apps/data/routes/exchanges.test.ts +544 -9
- package/src/apps/data/routes/exchanges.ts +690 -103
- 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 +113 -8
- package/src/apps/data/routes/funding.ts +77 -51
- package/src/apps/data/routes/receipts.test.ts +129 -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 +28 -13
- 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 +341 -31
- package/src/apps/data/routes/transfers.ts +358 -158
- package/src/apps/data/routes/valuation.test.ts +437 -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 +2 -1
- package/src/apps/management/App.ts +13 -0
- package/src/apps/management/routes/api-keys.test.ts +14 -0
- package/src/apps/management/routes/api-keys.ts +27 -3
- package/src/apps/management/routes/billing.test.ts +152 -20
- package/src/apps/management/routes/billing.ts +34 -18
- package/src/apps/management/routes/faucet.test.ts +87 -0
- package/src/apps/management/routes/faucet.ts +201 -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 +9 -99
- 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/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/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 +88 -6
- package/src/apps/relay/App.ts +61 -9
- package/src/db/Db.test.ts +19 -0
- package/src/db/Db.ts +125 -0
- package/src/db/Schema.ts +68 -0
- package/src/db/tables/earlyAccess.ts +86 -0
- package/src/db/tables/enabledBillingSources.test.ts +303 -0
- package/src/db/tables/enabledBillingSources.ts +118 -0
- package/src/db/tables/invitations.ts +3 -0
- package/src/db/tables/inviteLinks.ts +22 -3
- package/src/db/tables/organizations.test.ts +103 -0
- package/src/db/tables/organizations.ts +18 -2
- 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/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 +15 -47
- package/src/internal/FundingQuotes.test.ts +161 -6
- package/src/internal/FundingQuotes.ts +141 -39
- package/src/internal/Log.test-d.ts +21 -0
- package/src/internal/Log.test.ts +141 -0
- package/src/internal/Log.ts +93 -6
- package/src/internal/MetricSink.test.ts +215 -0
- package/src/internal/MetricSink.ts +24 -0
- package/src/internal/OpenApi.ts +3 -0
- package/src/internal/Response.test.ts +28 -0
- package/src/internal/Response.ts +12 -4
- 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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { Hono, type Context } from 'hono'
|
|
2
|
+
import * as z from 'zod/mini'
|
|
3
|
+
|
|
4
|
+
import * as Auth from '../../../internal/Auth.js'
|
|
5
|
+
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
6
|
+
import * as RateLimit from '../../../internal/RateLimit.js'
|
|
7
|
+
import * as Response from '../../../internal/Response.js'
|
|
8
|
+
import * as Schema from '../../../internal/Schema.js'
|
|
9
|
+
import * as Value from '../../../internal/Value.js'
|
|
10
|
+
import * as Viem from '../../../internal/Viem.js'
|
|
11
|
+
import type { Environment } from '../App.js'
|
|
12
|
+
|
|
13
|
+
const addressRateLimit = { perMinute: 1 } satisfies RateLimit.Limit
|
|
14
|
+
const userRateLimit = { perMinute: 3 } satisfies RateLimit.Limit
|
|
15
|
+
|
|
16
|
+
/** OpenAPI schemas owned by the sandbox faucet. */
|
|
17
|
+
export namespace schema {
|
|
18
|
+
const Token = Schema.describe(
|
|
19
|
+
z.object({
|
|
20
|
+
address: Schema.TokenAddress.check(z.describe('The test token contract that minted funds.')),
|
|
21
|
+
currency: z
|
|
22
|
+
.string()
|
|
23
|
+
.check(z.describe('The token’s currency label.'), z.meta({ examples: ['USD'] })),
|
|
24
|
+
decimals: z
|
|
25
|
+
.number()
|
|
26
|
+
.check(
|
|
27
|
+
z.int(),
|
|
28
|
+
z.nonnegative(),
|
|
29
|
+
z.describe('The decimal places used by the token.'),
|
|
30
|
+
z.meta({ examples: [6] }),
|
|
31
|
+
),
|
|
32
|
+
name: z
|
|
33
|
+
.string()
|
|
34
|
+
.check(z.describe('The token’s human-readable name.'), z.meta({ examples: ['Test USD'] })),
|
|
35
|
+
symbol: z.string().check(z.describe('The token’s symbol.'), z.meta({ examples: ['TUSD'] })),
|
|
36
|
+
}),
|
|
37
|
+
'A test token minted by the faucet.',
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const FundedToken = Schema.describe(
|
|
41
|
+
z.object({
|
|
42
|
+
amount: Schema.TokenAmount.check(z.describe('Amount minted by the faucet.')),
|
|
43
|
+
token: Token,
|
|
44
|
+
transactionHash: Schema.Hash.check(z.describe('The transaction that minted this token.')),
|
|
45
|
+
}),
|
|
46
|
+
'One token amount minted by the faucet.',
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
/** Faucet request body. */
|
|
50
|
+
export const FundBody = Schema.describe(
|
|
51
|
+
z.object({ address: Schema.Address }),
|
|
52
|
+
'A Tempo testnet account to fund.',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
/** Faucet response. */
|
|
56
|
+
export const FundResponse = Schema.describe(
|
|
57
|
+
z.object({
|
|
58
|
+
tokens: z
|
|
59
|
+
.array(FundedToken)
|
|
60
|
+
.check(z.describe('The configured test tokens minted to the account.')),
|
|
61
|
+
transactionHashes: z
|
|
62
|
+
.array(Schema.Hash)
|
|
63
|
+
.check(z.describe('Transactions that minted the configured test tokens.')),
|
|
64
|
+
}),
|
|
65
|
+
'Test tokens minted while funding the account.',
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
/** Faucet path parameters. */
|
|
69
|
+
export const Params = Schema.describe(
|
|
70
|
+
z.object({
|
|
71
|
+
orgId: z
|
|
72
|
+
.string()
|
|
73
|
+
.check(
|
|
74
|
+
z.describe('The organization id (`org_…`).'),
|
|
75
|
+
z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
|
|
76
|
+
),
|
|
77
|
+
}),
|
|
78
|
+
'Path parameters for the organization faucet.',
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const bodyValidation = {
|
|
83
|
+
code: 'body_invalid',
|
|
84
|
+
message: 'Enter a valid Tempo address.',
|
|
85
|
+
} as const
|
|
86
|
+
const paramValidation = {
|
|
87
|
+
code: 'param_invalid',
|
|
88
|
+
message: 'Check the path parameters and try again.',
|
|
89
|
+
} as const
|
|
90
|
+
|
|
91
|
+
/** Mounts the session-authenticated Tempo testnet faucet. */
|
|
92
|
+
export function faucet() {
|
|
93
|
+
return new Hono<Environment>().post(
|
|
94
|
+
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/faucet',
|
|
95
|
+
Auth.policy({ session: true }),
|
|
96
|
+
Auth.ensureOrg(),
|
|
97
|
+
OpenApi.validate('param', schema.Params, paramValidation),
|
|
98
|
+
OpenApi.validate('json', schema.FundBody, bodyValidation),
|
|
99
|
+
OpenApi.describeRoute({
|
|
100
|
+
description:
|
|
101
|
+
'Funds an account with the test tokens configured on Tempo testnet. This operation is available to signed-in organization members and is rate limited.',
|
|
102
|
+
operationId: 'fundTestnetAccount',
|
|
103
|
+
responses: OpenApi.responses({
|
|
104
|
+
errors: {
|
|
105
|
+
400: {
|
|
106
|
+
codes: ['api_key_malformed', 'body_invalid', 'param_invalid'],
|
|
107
|
+
description: 'The path or account address is invalid.',
|
|
108
|
+
},
|
|
109
|
+
404: 'No accessible organization was found.',
|
|
110
|
+
},
|
|
111
|
+
success: {
|
|
112
|
+
description: 'The account was funded.',
|
|
113
|
+
schema: schema.FundResponse,
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
summary: 'Fund testnet account',
|
|
117
|
+
tags: ['Faucet'],
|
|
118
|
+
}),
|
|
119
|
+
async (c) => {
|
|
120
|
+
if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
|
|
121
|
+
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
122
|
+
if (OpenApi.narrowValidation) return OpenApi.validationError(c, paramValidation)
|
|
123
|
+
if (OpenApi.narrowValidation) return OpenApi.validationError(c, bodyValidation)
|
|
124
|
+
|
|
125
|
+
const { address } = c.req.valid('json')
|
|
126
|
+
const limited = await consumeRateLimit(c, address)
|
|
127
|
+
if (limited) return rateLimitError(c, limited)
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const client = c.get('getClient')(Viem.chainId.testnet)
|
|
131
|
+
const receipts = await client.faucet.fundSync({ account: address, timeout: 30_000 })
|
|
132
|
+
const tokens = await Promise.all(
|
|
133
|
+
receipts.map(async (receipt) => {
|
|
134
|
+
const event = client.token.mint.extractEvent(receipt.logs)
|
|
135
|
+
const metadata = await client.token.getMetadata({ token: event.address })
|
|
136
|
+
return {
|
|
137
|
+
amount: Value.tokenAmount({
|
|
138
|
+
baseUnits: event.args.amount,
|
|
139
|
+
currency: metadata.currency,
|
|
140
|
+
decimals: metadata.decimals,
|
|
141
|
+
}),
|
|
142
|
+
token: {
|
|
143
|
+
address: event.address,
|
|
144
|
+
currency: metadata.currency,
|
|
145
|
+
decimals: metadata.decimals,
|
|
146
|
+
name: metadata.name,
|
|
147
|
+
symbol: metadata.symbol,
|
|
148
|
+
},
|
|
149
|
+
transactionHash: receipt.transactionHash,
|
|
150
|
+
}
|
|
151
|
+
}),
|
|
152
|
+
)
|
|
153
|
+
const transactionHashes = tokens.map((entry) => entry.transactionHash)
|
|
154
|
+
return c.json(Response.validated(schema.FundResponse, { tokens, transactionHashes }), 200)
|
|
155
|
+
} catch (cause) {
|
|
156
|
+
return Response.upstream(c, cause)
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function consumeRateLimit(c: Context<Environment>, address: string) {
|
|
163
|
+
const principal = Auth.getPrincipal(c)
|
|
164
|
+
const store = c.get('auth').rateLimit
|
|
165
|
+
if (!principal || !store) return undefined
|
|
166
|
+
|
|
167
|
+
const user = await store.consume({
|
|
168
|
+
key: `faucet:user:${principal.id}`,
|
|
169
|
+
limit: userRateLimit,
|
|
170
|
+
})
|
|
171
|
+
if (!user.allowed) return { result: user, scope: 'faucet-user' }
|
|
172
|
+
|
|
173
|
+
const account = await store.consume({
|
|
174
|
+
key: `faucet:address:${address}`,
|
|
175
|
+
limit: addressRateLimit,
|
|
176
|
+
})
|
|
177
|
+
if (!account.allowed) return { result: account, scope: 'faucet-address' }
|
|
178
|
+
return undefined
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function rateLimitError(
|
|
182
|
+
c: Context<Environment>,
|
|
183
|
+
limited: {
|
|
184
|
+
/** Denied quota result. */
|
|
185
|
+
result: RateLimit.Result
|
|
186
|
+
/** Quota dimension that denied the request. */
|
|
187
|
+
scope: string
|
|
188
|
+
},
|
|
189
|
+
) {
|
|
190
|
+
const { result, scope } = limited
|
|
191
|
+
c.header('RateLimit-Limit', String(result.limit))
|
|
192
|
+
c.header('RateLimit-Remaining', String(result.remaining))
|
|
193
|
+
c.header('RateLimit-Reset', String(result.reset))
|
|
194
|
+
c.header('RateLimit-Scope', scope)
|
|
195
|
+
c.header('Retry-After', String(Math.max(result.reset - Math.ceil(Date.now() / 1_000), 1)))
|
|
196
|
+
return Response.error(c, {
|
|
197
|
+
code: 'rate_limit_exceeded',
|
|
198
|
+
message: 'This account was funded recently. Try again after the cooldown.',
|
|
199
|
+
status: 429,
|
|
200
|
+
})
|
|
201
|
+
}
|
|
@@ -82,6 +82,7 @@ describe('inviteLinks', () => {
|
|
|
82
82
|
|
|
83
83
|
const link = await createLink(context, org.id, admin.cookie, { role: 'member' })
|
|
84
84
|
expect(link).toMatchObject({
|
|
85
|
+
allowedEmailDomains: null,
|
|
85
86
|
enabled: true,
|
|
86
87
|
lastUsedAt: null,
|
|
87
88
|
maxUses: null,
|
|
@@ -143,6 +144,122 @@ describe('inviteLinks', () => {
|
|
|
143
144
|
expect((await listed.json()).data).toEqual([])
|
|
144
145
|
})
|
|
145
146
|
|
|
147
|
+
test('normalizes and validates allowed email domains', async () => {
|
|
148
|
+
const context = createApp()
|
|
149
|
+
const { client } = context
|
|
150
|
+
const { org, owner } = await setup(context)
|
|
151
|
+
const orgLinks = client.v1.orgs[':orgId{org_[A-Za-z0-9_-]+}']['invite-links']
|
|
152
|
+
|
|
153
|
+
const link = await createLink(context, org.id, owner.cookie, {
|
|
154
|
+
allowedEmailDomains: [' Example.COM ', 'example.com', 'TEMPO.XYZ'],
|
|
155
|
+
name: 'Restricted',
|
|
156
|
+
})
|
|
157
|
+
expect(link.allowedEmailDomains).toEqual(['example.com', 'tempo.xyz'])
|
|
158
|
+
|
|
159
|
+
const listed = await orgLinks.$get({ param: { orgId: org.id } }, as(owner.cookie))
|
|
160
|
+
expect(listed.status).toBe(200)
|
|
161
|
+
if (listed.status !== 200) throw new Error(`Failed to list invite links: ${listed.status}`)
|
|
162
|
+
expect((await listed.json()).data[0]?.allowedEmailDomains).toEqual(['example.com', 'tempo.xyz'])
|
|
163
|
+
|
|
164
|
+
for (const allowedEmailDomains of [
|
|
165
|
+
[],
|
|
166
|
+
['user@example.com'],
|
|
167
|
+
['https://example.com'],
|
|
168
|
+
['*.example.com'],
|
|
169
|
+
]) {
|
|
170
|
+
const response = await orgLinks.$post(
|
|
171
|
+
{
|
|
172
|
+
json: { allowedEmailDomains },
|
|
173
|
+
param: { orgId: org.id },
|
|
174
|
+
},
|
|
175
|
+
as(owner.cookie),
|
|
176
|
+
)
|
|
177
|
+
expect(response.status).toBe(400)
|
|
178
|
+
if (response.status !== 400)
|
|
179
|
+
throw new Error(`Expected invalid invite link body, received ${response.status}`)
|
|
180
|
+
expect((await response.json()).error.code).toBe('body_invalid')
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('restricts new memberships without consuming rejected attempts', async () => {
|
|
185
|
+
const db = TestApp.database()
|
|
186
|
+
const context = createApp(db)
|
|
187
|
+
const { client } = context
|
|
188
|
+
const { org, owner } = await setup(context)
|
|
189
|
+
const orgLinks = client.v1.orgs[':orgId{org_[A-Za-z0-9_-]+}']['invite-links']
|
|
190
|
+
const publicLinks = client.v1['invite-links']
|
|
191
|
+
const link = await createLink(context, org.id, owner.cookie, {
|
|
192
|
+
allowedEmailDomains: [' Example.COM '],
|
|
193
|
+
expiresAt: null,
|
|
194
|
+
maxUses: 1,
|
|
195
|
+
name: 'Restricted',
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const member = await session(context)
|
|
199
|
+
await Users.setEmail(db, member.userId, 'member@sub.example.com')
|
|
200
|
+
await Memberships.create(db, { orgId: org.id, role: 'member', userId: member.userId })
|
|
201
|
+
const memberRetry = await publicLinks.accept.$post(
|
|
202
|
+
{ json: { token: link.token } },
|
|
203
|
+
as(member.cookie),
|
|
204
|
+
)
|
|
205
|
+
expect(memberRetry.status).toBe(200)
|
|
206
|
+
|
|
207
|
+
const subdomain = await session(context)
|
|
208
|
+
await Users.setEmail(db, subdomain.userId, 'dev@sub.example.com')
|
|
209
|
+
const rejected = await publicLinks.accept.$post(
|
|
210
|
+
{ json: { token: link.token } },
|
|
211
|
+
as(subdomain.cookie),
|
|
212
|
+
)
|
|
213
|
+
expect(rejected.status).toBe(403)
|
|
214
|
+
if (rejected.status !== 403)
|
|
215
|
+
throw new Error(`Expected forbidden invite link acceptance, received ${rejected.status}`)
|
|
216
|
+
expect((await rejected.json()).error.code).toBe('invite_link_email_forbidden')
|
|
217
|
+
expect(await Memberships.get(db, org.id, subdomain.userId)).toBeUndefined()
|
|
218
|
+
|
|
219
|
+
const audits = await orgLinks[':inviteLinkId{iln_[A-Za-z0-9_-]+}'].redemptions.$get(
|
|
220
|
+
{ param: { inviteLinkId: link.id, orgId: org.id } },
|
|
221
|
+
as(owner.cookie),
|
|
222
|
+
)
|
|
223
|
+
expect(audits.status).toBe(200)
|
|
224
|
+
if (audits.status !== 200) throw new Error(`Failed to list redemptions: ${audits.status}`)
|
|
225
|
+
expect((await audits.json()).data).toEqual([])
|
|
226
|
+
|
|
227
|
+
const beforeJoin = await orgLinks.$get({ param: { orgId: org.id } }, as(owner.cookie))
|
|
228
|
+
expect(beforeJoin.status).toBe(200)
|
|
229
|
+
if (beforeJoin.status !== 200)
|
|
230
|
+
throw new Error(`Failed to list invite links: ${beforeJoin.status}`)
|
|
231
|
+
expect((await beforeJoin.json()).data[0]).toMatchObject({ status: 'active', useCount: 0 })
|
|
232
|
+
|
|
233
|
+
const exact = await session(context)
|
|
234
|
+
await Users.setEmail(db, exact.userId, 'Dev@Example.com')
|
|
235
|
+
const accepted = await publicLinks.accept.$post(
|
|
236
|
+
{ json: { token: link.token } },
|
|
237
|
+
as(exact.cookie),
|
|
238
|
+
)
|
|
239
|
+
expect(accepted.status).toBe(200)
|
|
240
|
+
|
|
241
|
+
const exhaustedRetry = await publicLinks.accept.$post(
|
|
242
|
+
{ json: { token: link.token } },
|
|
243
|
+
as(member.cookie),
|
|
244
|
+
)
|
|
245
|
+
expect(exhaustedRetry.status).toBe(200)
|
|
246
|
+
|
|
247
|
+
const afterJoin = await orgLinks.$get({ param: { orgId: org.id } }, as(owner.cookie))
|
|
248
|
+
expect(afterJoin.status).toBe(200)
|
|
249
|
+
if (afterJoin.status !== 200)
|
|
250
|
+
throw new Error(`Failed to list invite links: ${afterJoin.status}`)
|
|
251
|
+
expect((await afterJoin.json()).data[0]).toMatchObject({ status: 'exhausted', useCount: 1 })
|
|
252
|
+
|
|
253
|
+
const auditsAfterJoin = await orgLinks[':inviteLinkId{iln_[A-Za-z0-9_-]+}'].redemptions.$get(
|
|
254
|
+
{ param: { inviteLinkId: link.id, orgId: org.id } },
|
|
255
|
+
as(owner.cookie),
|
|
256
|
+
)
|
|
257
|
+
expect(auditsAfterJoin.status).toBe(200)
|
|
258
|
+
if (auditsAfterJoin.status !== 200)
|
|
259
|
+
throw new Error(`Failed to list redemptions: ${auditsAfterJoin.status}`)
|
|
260
|
+
expect((await auditsAfterJoin.json()).data).toHaveLength(1)
|
|
261
|
+
})
|
|
262
|
+
|
|
146
263
|
test('resolve reveals only a summary and hides every unavailable reason', async () => {
|
|
147
264
|
const db = TestApp.database()
|
|
148
265
|
const context = createApp(db)
|
|
@@ -10,8 +10,34 @@ import * as Response from '../../../internal/Response.js'
|
|
|
10
10
|
import * as Schema from '../../../internal/Schema.js'
|
|
11
11
|
import type { Environment } from '../App.js'
|
|
12
12
|
|
|
13
|
+
// A bare domain (`example.org`), normalized before validation.
|
|
14
|
+
const domainPattern = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+$/
|
|
15
|
+
|
|
13
16
|
/** OpenAPI schemas owned by reusable invite links. */
|
|
14
17
|
export namespace schema {
|
|
18
|
+
const allowedEmailDomains = z
|
|
19
|
+
.nullable(
|
|
20
|
+
z.readonly(
|
|
21
|
+
z
|
|
22
|
+
.array(
|
|
23
|
+
z
|
|
24
|
+
.string()
|
|
25
|
+
.check(
|
|
26
|
+
z.trim(),
|
|
27
|
+
z.toLowerCase(),
|
|
28
|
+
z.regex(domainPattern, 'Must be a bare domain (example.org).'),
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
.check(
|
|
32
|
+
z.minLength(1),
|
|
33
|
+
z.overwrite((domains) => [...new Set(domains)]),
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
.check(
|
|
38
|
+
z.describe('Email domains allowed to redeem, or null for unrestricted.'),
|
|
39
|
+
z.meta({ examples: [['example.org'], null] }),
|
|
40
|
+
)
|
|
15
41
|
const nullableExpiry = z
|
|
16
42
|
.nullable(z.iso.datetime({ offset: true }))
|
|
17
43
|
.check(
|
|
@@ -31,6 +57,7 @@ export namespace schema {
|
|
|
31
57
|
/** Management representation, including the recopyable bearer id. */
|
|
32
58
|
export const InviteLink = Schema.describe(
|
|
33
59
|
z.object({
|
|
60
|
+
allowedEmailDomains,
|
|
34
61
|
createdAt: z
|
|
35
62
|
.string()
|
|
36
63
|
.check(
|
|
@@ -146,6 +173,7 @@ export namespace schema {
|
|
|
146
173
|
/** Create body. */
|
|
147
174
|
export const CreateBody = z
|
|
148
175
|
.object({
|
|
176
|
+
allowedEmailDomains: z.optional(allowedEmailDomains),
|
|
149
177
|
expiresAt: z.optional(nullableExpiry),
|
|
150
178
|
maxUses: z.optional(nullableUses),
|
|
151
179
|
name: z.optional(
|
|
@@ -318,6 +346,7 @@ export function inviteLinks() {
|
|
|
318
346
|
const body = c.req.valid('json')
|
|
319
347
|
try {
|
|
320
348
|
const record = await InviteLinks.create(Db.get(c.get('db')), {
|
|
349
|
+
allowedEmailDomains: body.allowedEmailDomains ?? null,
|
|
321
350
|
createdBy: Auth.membership(c)?.userId ?? 'super_admin',
|
|
322
351
|
expiresAt:
|
|
323
352
|
body.expiresAt === undefined
|
|
@@ -498,7 +527,13 @@ export function inviteLinks() {
|
|
|
498
527
|
OpenApi.describeRoute({
|
|
499
528
|
operationId: 'acceptInviteLink',
|
|
500
529
|
responses: OpenApi.responses({
|
|
501
|
-
errors: {
|
|
530
|
+
errors: {
|
|
531
|
+
403: {
|
|
532
|
+
codes: ['forbidden', 'invite_link_email_forbidden'],
|
|
533
|
+
description: 'A verified session email from an allowed domain is required.',
|
|
534
|
+
},
|
|
535
|
+
...errors,
|
|
536
|
+
},
|
|
502
537
|
success: { description: 'Membership.', schema: schema.AcceptResponse },
|
|
503
538
|
}),
|
|
504
539
|
summary: 'Accept invite link',
|
|
@@ -531,6 +566,12 @@ export function inviteLinks() {
|
|
|
531
566
|
200,
|
|
532
567
|
)
|
|
533
568
|
} catch (cause) {
|
|
569
|
+
if (cause instanceof InviteLinks.EmailDomainForbiddenError)
|
|
570
|
+
return Response.error(c, {
|
|
571
|
+
code: 'invite_link_email_forbidden',
|
|
572
|
+
message: 'Sign in with an email address on an allowed domain',
|
|
573
|
+
status: 403,
|
|
574
|
+
})
|
|
534
575
|
return Response.upstream(c, cause)
|
|
535
576
|
}
|
|
536
577
|
},
|
|
@@ -539,6 +580,7 @@ export function inviteLinks() {
|
|
|
539
580
|
|
|
540
581
|
function serialize(record: InviteLinks.Record) {
|
|
541
582
|
return {
|
|
583
|
+
allowedEmailDomains: record.allowedEmailDomains,
|
|
542
584
|
createdAt: record.createdAt,
|
|
543
585
|
createdBy: record.createdBy,
|
|
544
586
|
enabled: record.enabled,
|
|
@@ -182,13 +182,13 @@ describe('sign-in email requirement', () => {
|
|
|
182
182
|
})
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
-
describe('sign-up
|
|
186
|
-
test('allows new users on
|
|
187
|
-
const oidc = await createIssuer({ email: 'dev@
|
|
185
|
+
describe('open sign-up', () => {
|
|
186
|
+
test('allows new users on any domain', async () => {
|
|
187
|
+
const oidc = await createIssuer({ email: 'dev@example.com' })
|
|
188
188
|
try {
|
|
189
189
|
const app = TestApp.create({
|
|
190
190
|
auth: { keys: [TestApp.key] },
|
|
191
|
-
session: {
|
|
191
|
+
session: { wallet: { issuer: oidc.url, origin } },
|
|
192
192
|
})
|
|
193
193
|
const account = privateKeyToAccount(generatePrivateKey())
|
|
194
194
|
const { cookie, verifyResponse } = await TestApp.signIn(app, account, {
|
|
@@ -199,109 +199,19 @@ describe('sign-up email domains', () => {
|
|
|
199
199
|
const response = await app.request('/v1/me', { headers: { cookie: cookie! } })
|
|
200
200
|
expect(response.status).toBe(200)
|
|
201
201
|
const body = await TestApp.json(response, Me.schema.User)
|
|
202
|
-
expect(body.email).toBe('dev@
|
|
203
|
-
} finally {
|
|
204
|
-
await oidc.close()
|
|
205
|
-
}
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
test('rejects new users on other domains', async () => {
|
|
209
|
-
const oidc = await createIssuer({ email: 'dev@example.com' })
|
|
210
|
-
try {
|
|
211
|
-
const app = TestApp.create({
|
|
212
|
-
auth: { keys: [TestApp.key] },
|
|
213
|
-
session: { emailAllowlist: ['tempo.xyz'], wallet: { issuer: oidc.url, origin } },
|
|
214
|
-
})
|
|
215
|
-
const account = privateKeyToAccount(generatePrivateKey())
|
|
216
|
-
const { cookie, verifyResponse } = await TestApp.signIn(app, account, {
|
|
217
|
-
idToken: ({ nonce }) => oidc.mint({ address: account.address, nonce }),
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
expect(verifyResponse.status).toBe(403)
|
|
221
|
-
expect(cookie).toBeUndefined()
|
|
222
|
-
expect(await verifyResponse.json()).toMatchInlineSnapshot(`
|
|
223
|
-
{
|
|
224
|
-
"error": "sign-ups are restricted to approved email domains or invited emails",
|
|
225
|
-
}
|
|
226
|
-
`)
|
|
202
|
+
expect(body.email).toBe('dev@example.com')
|
|
227
203
|
} finally {
|
|
228
204
|
await oidc.close()
|
|
229
205
|
}
|
|
230
206
|
})
|
|
231
207
|
|
|
232
|
-
test('
|
|
233
|
-
const app =
|
|
234
|
-
auth: { keys: [TestApp.key] },
|
|
235
|
-
session: { emailAllowlist: ['tempo.xyz'], wallet: { origin } },
|
|
236
|
-
})
|
|
208
|
+
test('allows new users without an identity token', async () => {
|
|
209
|
+
const app = createApp()
|
|
237
210
|
const account = privateKeyToAccount(generatePrivateKey())
|
|
238
211
|
const { cookie, verifyResponse } = await TestApp.signIn(app, account)
|
|
239
212
|
|
|
240
|
-
expect(verifyResponse.status).toBe(
|
|
241
|
-
expect(cookie).
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
test('allows invited emails regardless of domain', async () => {
|
|
245
|
-
const oidc = await createIssuer({ email: 'Invited@Example.com' })
|
|
246
|
-
try {
|
|
247
|
-
const db = TestApp.database()
|
|
248
|
-
|
|
249
|
-
// Owner signs up while sign-up is open and issues the invitation.
|
|
250
|
-
const open = TestApp.create({
|
|
251
|
-
auth: { keys: [TestApp.key] },
|
|
252
|
-
db,
|
|
253
|
-
session: { wallet: { origin } },
|
|
254
|
-
})
|
|
255
|
-
const owner = privateKeyToAccount(generatePrivateKey())
|
|
256
|
-
const { cookie } = await TestApp.signIn(open, owner)
|
|
257
|
-
const created = await open.request('/v1/orgs', {
|
|
258
|
-
body: JSON.stringify({ name: 'Acme' }),
|
|
259
|
-
headers: { 'content-type': 'application/json', cookie: cookie! },
|
|
260
|
-
method: 'POST',
|
|
261
|
-
})
|
|
262
|
-
const org = (await created.json()) as { id: string }
|
|
263
|
-
const invited = await open.request(`/v1/orgs/${org.id}/invitations`, {
|
|
264
|
-
body: JSON.stringify({ email: 'invited@example.com', role: 'member' }),
|
|
265
|
-
headers: { 'content-type': 'application/json', cookie: cookie! },
|
|
266
|
-
method: 'POST',
|
|
267
|
-
})
|
|
268
|
-
expect(invited.status).toBe(200)
|
|
269
|
-
|
|
270
|
-
const restricted = TestApp.create({
|
|
271
|
-
auth: { keys: [TestApp.key] },
|
|
272
|
-
db,
|
|
273
|
-
session: { emailAllowlist: ['tempo.xyz'], wallet: { issuer: oidc.url, origin } },
|
|
274
|
-
})
|
|
275
|
-
const account = privateKeyToAccount(generatePrivateKey())
|
|
276
|
-
const { verifyResponse } = await TestApp.signIn(restricted, account, {
|
|
277
|
-
idToken: ({ nonce }) => oidc.mint({ address: account.address, nonce }),
|
|
278
|
-
})
|
|
279
|
-
expect(verifyResponse.status).toBe(200)
|
|
280
|
-
} finally {
|
|
281
|
-
await oidc.close()
|
|
282
|
-
}
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
test('allows existing users regardless of domain', async () => {
|
|
286
|
-
const db = TestApp.database()
|
|
287
|
-
const account = privateKeyToAccount(generatePrivateKey())
|
|
288
|
-
|
|
289
|
-
// Row created before the restriction (open sign-up deployment).
|
|
290
|
-
const open = TestApp.create({
|
|
291
|
-
auth: { keys: [TestApp.key] },
|
|
292
|
-
db,
|
|
293
|
-
session: { wallet: { origin } },
|
|
294
|
-
})
|
|
295
|
-
const first = await TestApp.signIn(open, account)
|
|
296
|
-
expect(first.verifyResponse.status).toBe(200)
|
|
297
|
-
|
|
298
|
-
const restricted = TestApp.create({
|
|
299
|
-
auth: { keys: [TestApp.key] },
|
|
300
|
-
db,
|
|
301
|
-
session: { emailAllowlist: ['tempo.xyz'], wallet: { origin } },
|
|
302
|
-
})
|
|
303
|
-
const second = await TestApp.signIn(restricted, account)
|
|
304
|
-
expect(second.verifyResponse.status).toBe(200)
|
|
213
|
+
expect(verifyResponse.status).toBe(200)
|
|
214
|
+
expect(cookie).toBeDefined()
|
|
305
215
|
})
|
|
306
216
|
})
|
|
307
217
|
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
|
|
2
2
|
|
|
3
3
|
import * as TestApp from '../../../../test/App.js'
|
|
4
|
+
import * as EarlyAccess from '../../../db/tables/earlyAccess.js'
|
|
5
|
+
import * as EnabledBillingSources from '../../../db/tables/enabledBillingSources.js'
|
|
4
6
|
import * as Organizations from '../../../db/tables/organizations.js'
|
|
5
7
|
import * as Projects from '../../../db/tables/projects.js'
|
|
8
|
+
import * as Users from '../../../db/tables/users.js'
|
|
6
9
|
import type * as Log from '../../../internal/Log.js'
|
|
7
10
|
import * as Orgs from './orgs.js'
|
|
8
11
|
|
|
@@ -196,7 +199,8 @@ describe('GET /orgs', () => {
|
|
|
196
199
|
|
|
197
200
|
describe('POST /orgs', () => {
|
|
198
201
|
test('creates a session-owned organization', async () => {
|
|
199
|
-
const
|
|
202
|
+
const db = TestApp.database()
|
|
203
|
+
const app = createApp(db)
|
|
200
204
|
const cookie = await session(app)
|
|
201
205
|
|
|
202
206
|
const response = await app.request('/v1/orgs', json('POST', cookie, { name: 'Acme' }))
|
|
@@ -204,10 +208,48 @@ describe('POST /orgs', () => {
|
|
|
204
208
|
const body = await TestApp.json(response, Orgs.schema.Organization)
|
|
205
209
|
expect(body.id.startsWith('org_')).toBe(true)
|
|
206
210
|
expect(body.name).toBe('Acme')
|
|
211
|
+
expect(await EnabledBillingSources.listByOrg(db, body.id)).toEqual([])
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test.each([
|
|
215
|
+
{ email: 'Dev@Example.NET', entry: 'dev@example.net', kind: 'email' },
|
|
216
|
+
{ email: 'Dev@Example.ORG', entry: 'example.org', kind: 'domain' },
|
|
217
|
+
])('enables Stripe for a session matching an early-access $kind', async ({ email, entry }) => {
|
|
218
|
+
const db = TestApp.database()
|
|
219
|
+
const app = createApp(db)
|
|
220
|
+
await EarlyAccess.add(db, { createdBy: 'test', entry })
|
|
221
|
+
const account = privateKeyToAccount(generatePrivateKey())
|
|
222
|
+
const { cookie } = await TestApp.signIn(app, account)
|
|
223
|
+
const user = await Users.getByAddress(db, account.address)
|
|
224
|
+
if (!user) throw new Error('sign-in did not create a user')
|
|
225
|
+
await Users.setEmail(db, user.id, email)
|
|
226
|
+
|
|
227
|
+
const response = await app.request('/v1/orgs', json('POST', cookie!, { name: 'Acme' }))
|
|
228
|
+
expect(response.status).toBe(200)
|
|
229
|
+
const org = await TestApp.json(response, Orgs.schema.Organization)
|
|
230
|
+
const sources = await EnabledBillingSources.listByOrg(db, org.id)
|
|
231
|
+
expect(sources.map(({ createdBy, source }) => ({ createdBy, source }))).toEqual([
|
|
232
|
+
{ createdBy: user.id, source: 'stripe' },
|
|
233
|
+
])
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('does not enable Stripe without an early-access grant', async () => {
|
|
237
|
+
const db = TestApp.database()
|
|
238
|
+
const app = createApp(db)
|
|
239
|
+
const account = privateKeyToAccount(generatePrivateKey())
|
|
240
|
+
const { cookie } = await TestApp.signIn(app, account)
|
|
241
|
+
const user = await Users.getByAddress(db, account.address)
|
|
242
|
+
if (!user) throw new Error('sign-in did not create a user')
|
|
243
|
+
await Users.setEmail(db, user.id, 'dev@example.net')
|
|
244
|
+
|
|
245
|
+
const response = await app.request('/v1/orgs', json('POST', cookie!, { name: 'Acme' }))
|
|
246
|
+
const org = await TestApp.json(response, Orgs.schema.Organization)
|
|
247
|
+
expect(await EnabledBillingSources.listByOrg(db, org.id)).toEqual([])
|
|
207
248
|
})
|
|
208
249
|
|
|
209
250
|
test('creates a memberless org as the super admin', async () => {
|
|
210
|
-
const
|
|
251
|
+
const db = TestApp.database()
|
|
252
|
+
const app = createApp(db)
|
|
211
253
|
const response = await app.request('/v1/orgs', {
|
|
212
254
|
body: JSON.stringify({ name: 'Ops' }),
|
|
213
255
|
headers: { 'content-type': 'application/json', 'tempo-api-key': secret },
|
|
@@ -216,6 +258,7 @@ describe('POST /orgs', () => {
|
|
|
216
258
|
expect(response.status).toBe(200)
|
|
217
259
|
const org = await TestApp.json(response, Orgs.schema.Organization)
|
|
218
260
|
expect(org.name).toBe('Ops')
|
|
261
|
+
expect(await EnabledBillingSources.listByOrg(db, org.id)).toEqual([])
|
|
219
262
|
|
|
220
263
|
// Memberless: invisible to sessions until an owner is seated.
|
|
221
264
|
const cookie = await session(app)
|
|
@@ -5,6 +5,7 @@ import * as ApiKeys from '../../../ApiKeys.js'
|
|
|
5
5
|
import type * as App from '../../../App.js'
|
|
6
6
|
import * as Auth from '../../../internal/Auth.js'
|
|
7
7
|
import * as Db from '../../../db/Db.js'
|
|
8
|
+
import * as EarlyAccess from '../../../db/tables/earlyAccess.js'
|
|
8
9
|
import * as Memberships from '../../../db/tables/memberships.js'
|
|
9
10
|
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
10
11
|
import * as Organizations from '../../../db/tables/organizations.js'
|
|
@@ -189,6 +190,11 @@ export function orgs() {
|
|
|
189
190
|
? await Organizations.create(db, { createdBy: 'super_admin', name: body.name })
|
|
190
191
|
: {
|
|
191
192
|
...(await Organizations.createOwned(db, {
|
|
193
|
+
enabledBillingSources:
|
|
194
|
+
principal.email &&
|
|
195
|
+
(await EarlyAccess.matches(db, principal.email.trim().toLowerCase()))
|
|
196
|
+
? ['stripe']
|
|
197
|
+
: [],
|
|
192
198
|
name: body.name,
|
|
193
199
|
userId: principal.id,
|
|
194
200
|
})),
|