tapimo 0.6.0 → 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 +8641 -8263
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +9 -1
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +4 -0
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +5 -0
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +112 -11
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +2 -1
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/early-access.d.ts +2 -2
- package/dist/admin/apps/early-access.d.ts.map +1 -1
- package/dist/admin/apps/early-access.js +5 -5
- package/dist/admin/apps/early-access.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +124 -1
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/organizations.js +97 -6
- package/dist/admin/apps/organizations.js.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +95 -87
- 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 +5016 -4985
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/FundingQuoteProviders.js +2 -15
- package/dist/apps/data/FundingQuoteProviders.js.map +1 -1
- package/dist/apps/data/FxOracle.d.ts +1 -1
- package/dist/apps/data/FxOracle.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.js +1 -1
- package/dist/apps/data/FxOracle.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +27 -27
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +21 -1
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +58 -58
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +113 -113
- package/dist/apps/data/routes/fee-amm.d.ts +105 -91
- package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
- package/dist/apps/data/routes/fee-amm.js +76 -91
- package/dist/apps/data/routes/fee-amm.js.map +1 -1
- package/dist/apps/data/routes/funding.d.ts +72 -34
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +34 -26
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/receipts.d.ts +6 -6
- package/dist/apps/data/routes/rpc.d.ts +31 -31
- package/dist/apps/data/routes/tokens.d.ts +2 -2
- package/dist/apps/data/routes/tokens.js +1 -1
- package/dist/apps/data/routes/transactions.d.ts +4 -4
- package/dist/apps/data/routes/transfers.d.ts +99 -88
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +226 -141
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +34 -34
- package/dist/apps/data/routes/valuation.js +3 -3
- package/dist/apps/data/routes/valuation.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +366 -366
- package/dist/apps/index.d.ts +1 -0
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +1 -0
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/management/App.d.ts +460 -523
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +7 -23
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +20 -8
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +106 -151
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +28 -28
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +40 -26
- package/dist/apps/management/routes/faucet.d.ts.map +1 -1
- package/dist/apps/management/routes/faucet.js +33 -18
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +131 -131
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +0 -3
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/me.d.ts +6 -8
- package/dist/apps/management/routes/me.d.ts.map +1 -1
- package/dist/apps/management/routes/me.js +0 -4
- package/dist/apps/management/routes/me.js.map +1 -1
- package/dist/apps/management/routes/members.d.ts +45 -45
- package/dist/apps/management/routes/orgs.d.ts +94 -94
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +5 -0
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +32 -32
- package/dist/apps/management/routes/verified-token-requests.d.ts +94 -94
- package/dist/apps/management/routes/webhooks.d.ts +1 -1
- package/dist/apps/mpp/App.d.ts +363 -0
- package/dist/apps/mpp/App.d.ts.map +1 -0
- package/dist/apps/mpp/App.js +72 -0
- package/dist/apps/mpp/App.js.map +1 -0
- package/dist/apps/mpp/Mpp.d.ts +178 -0
- package/dist/apps/mpp/Mpp.d.ts.map +1 -0
- package/dist/apps/mpp/Mpp.js +183 -0
- package/dist/apps/mpp/Mpp.js.map +1 -0
- package/dist/apps/mpp/index.d.ts +3 -0
- package/dist/apps/mpp/index.d.ts.map +1 -0
- package/dist/apps/mpp/index.js +3 -0
- package/dist/apps/mpp/index.js.map +1 -0
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +56 -9
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/db/Db.d.ts +2 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +27 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +12 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +11 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/enabledBillingSources.d.ts +73 -0
- package/dist/db/tables/enabledBillingSources.d.ts.map +1 -0
- package/dist/db/tables/enabledBillingSources.js +69 -0
- package/dist/db/tables/enabledBillingSources.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +1 -12
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/invitations.js +3 -23
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +5 -2
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +13 -2
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts +1 -16
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +17 -6
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +37 -5
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +0 -32
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +3 -51
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/FundingQuotes.d.ts +78 -32
- package/dist/internal/FundingQuotes.d.ts.map +1 -1
- package/dist/internal/FundingQuotes.js +45 -8
- package/dist/internal/FundingQuotes.js.map +1 -1
- package/dist/internal/Log.d.ts +18 -4
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +50 -3
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +15 -0
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/Response.d.ts +2 -2
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Response.js +12 -4
- package/dist/internal/Response.js.map +1 -1
- package/dist/internal/Viem.d.ts +1 -1
- package/package.json +7 -2
- package/src/App.test.ts +9 -4
- package/src/App.ts +14 -2
- package/src/Client.test-d.ts +109 -28
- package/src/Scope.ts +5 -0
- package/src/admin/App.test.ts +4 -0
- package/src/admin/apps/early-access.ts +6 -5
- package/src/admin/apps/organizations.test.ts +86 -2
- package/src/admin/apps/organizations.ts +155 -31
- package/src/admin/apps/scopes.test.ts +5 -0
- package/src/admin/ui/src/lib/organizations.test.ts +81 -1
- package/src/admin/ui/src/lib/organizations.ts +92 -0
- package/src/admin/ui/src/routes/api-keys.tsx +1 -1
- package/src/admin/ui/src/routes/early-access.tsx +8 -3
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +103 -0
- package/src/admin/ui.generated.ts +95 -109
- 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/FundingQuoteProviders.test.ts +0 -5
- package/src/apps/data/FundingQuoteProviders.ts +2 -29
- package/src/apps/data/FxOracle.test.ts +2 -2
- package/src/apps/data/FxOracle.ts +2 -2
- package/src/apps/data/routes/activities.test.ts +6 -2
- package/src/apps/data/routes/activities.ts +39 -1
- package/src/apps/data/routes/balances.test.ts +6 -2
- package/src/apps/data/routes/exchanges.test.ts +7 -3
- package/src/apps/data/routes/fee-amm.test.ts +47 -23
- package/src/apps/data/routes/fee-amm.ts +98 -119
- package/src/apps/data/routes/funding.test.ts +29 -8
- package/src/apps/data/routes/funding.ts +42 -48
- package/src/apps/data/routes/receipts.test.ts +6 -2
- package/src/apps/data/routes/tokens.ts +1 -1
- package/src/apps/data/routes/transfers.test.ts +203 -35
- package/src/apps/data/routes/transfers.ts +294 -172
- package/src/apps/data/routes/valuation.test.ts +18 -3
- package/src/apps/data/routes/valuation.ts +4 -4
- package/src/apps/index.ts +1 -0
- package/src/apps/management/routes/api-keys.test.ts +2 -75
- package/src/apps/management/routes/api-keys.ts +30 -7
- package/src/apps/management/routes/billing.test.ts +106 -85
- package/src/apps/management/routes/billing.ts +32 -21
- package/src/apps/management/routes/faucet.test.ts +11 -3
- package/src/apps/management/routes/faucet.ts +46 -30
- package/src/apps/management/routes/invitations.test.ts +0 -172
- package/src/apps/management/routes/invitations.ts +0 -3
- package/src/apps/management/routes/me.test.ts +3 -145
- package/src/apps/management/routes/me.ts +0 -6
- package/src/apps/management/routes/orgs.test.ts +45 -2
- package/src/apps/management/routes/orgs.ts +6 -0
- package/src/apps/management/routes/scopes.test.ts +1 -0
- package/src/apps/mpp/App.ts +86 -0
- package/src/apps/mpp/Mpp.test.ts +193 -0
- package/src/apps/mpp/Mpp.ts +353 -0
- package/src/apps/mpp/index.ts +2 -0
- package/src/apps/mpp/internal/Adapter.ts +11 -0
- package/src/apps/mpp/internal/Error.ts +9 -0
- package/src/apps/mpp/internal/Idempotency.ts +7 -0
- package/src/apps/mpp/internal/Screening.ts +10 -0
- package/src/apps/relay/App.test.ts +88 -6
- package/src/apps/relay/App.ts +61 -9
- package/src/db/Db.test.ts +2 -2
- package/src/db/Db.ts +30 -0
- package/src/db/Schema.ts +15 -0
- package/src/db/tables/enabledBillingSources.test.ts +303 -0
- package/src/db/tables/enabledBillingSources.ts +118 -0
- package/src/db/tables/invitations.ts +4 -26
- package/src/db/tables/organizations.test.ts +103 -0
- package/src/db/tables/organizations.ts +18 -2
- package/src/handlers/internal/utils.ts +22 -6
- package/src/handlers/relay.errors.test.ts +153 -0
- package/src/handlers/relay.ts +42 -7
- package/src/internal/Auth.test.ts +3 -3
- package/src/internal/Auth.ts +3 -63
- package/src/internal/FundingQuotes.test.ts +18 -5
- package/src/internal/FundingQuotes.ts +46 -11
- package/src/internal/Log.test.ts +91 -0
- package/src/internal/Log.ts +69 -5
- package/src/internal/MetricSink.test.ts +55 -0
- package/src/internal/MetricSink.ts +14 -0
- package/src/internal/Response.test.ts +28 -0
- package/src/internal/Response.ts +12 -4
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tapimo",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"src"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@hono/node-server": "^2.0.
|
|
16
|
+
"@hono/node-server": "^2.0.5",
|
|
17
17
|
"@hono/standard-validator": "^0.2.2",
|
|
18
18
|
"accounts": "^0.15.1",
|
|
19
19
|
"hono": "^4.12.26",
|
|
@@ -80,6 +80,11 @@
|
|
|
80
80
|
"types": "./dist/apps/management/index.d.ts",
|
|
81
81
|
"default": "./dist/apps/management/index.js"
|
|
82
82
|
},
|
|
83
|
+
"./mpp": {
|
|
84
|
+
"src": "./src/apps/mpp/index.ts",
|
|
85
|
+
"types": "./dist/apps/mpp/index.d.ts",
|
|
86
|
+
"default": "./dist/apps/mpp/index.js"
|
|
87
|
+
},
|
|
83
88
|
"./relay": {
|
|
84
89
|
"src": "./src/apps/relay/index.ts",
|
|
85
90
|
"types": "./dist/apps/relay/index.d.ts",
|
package/src/App.test.ts
CHANGED
|
@@ -578,16 +578,21 @@ describe('create', () => {
|
|
|
578
578
|
})
|
|
579
579
|
|
|
580
580
|
test('returns payment challenge when API-key quota is exhausted', async () => {
|
|
581
|
+
const limit = { perMinute: 1 }
|
|
582
|
+
const key = { ...TestApp.key, rateLimits: { 'data:read': limit } }
|
|
581
583
|
const rateLimitStore = Store.memory()
|
|
582
|
-
const app = TestApp.create({
|
|
584
|
+
const app = TestApp.create({
|
|
585
|
+
auth: { keys: [key] },
|
|
586
|
+
rateLimit: { store: rateLimitStore },
|
|
587
|
+
})
|
|
583
588
|
await exhaustQuota(
|
|
584
589
|
RateLimit.memory({ store: rateLimitStore }),
|
|
585
|
-
|
|
586
|
-
|
|
590
|
+
`api_key:${key.id}:scope:data:read`,
|
|
591
|
+
limit,
|
|
587
592
|
)
|
|
588
593
|
|
|
589
594
|
const response = await app.request(`/v1/tokens/${TestApp.token}`, {
|
|
590
|
-
headers: { 'tempo-api-key':
|
|
595
|
+
headers: { 'tempo-api-key': key.token },
|
|
591
596
|
})
|
|
592
597
|
|
|
593
598
|
expect(response.status).toMatchInlineSnapshot(`402`)
|
package/src/App.ts
CHANGED
|
@@ -29,14 +29,22 @@ import openrpcSpec from './openrpc.json' with { type: 'json' }
|
|
|
29
29
|
import * as Metadata from './apps/metadata.js'
|
|
30
30
|
import * as Data from './apps/data/App.js'
|
|
31
31
|
import * as Management from './apps/management/App.js'
|
|
32
|
+
import * as Mpp from './apps/mpp/App.js'
|
|
33
|
+
import * as Relay from './apps/relay/App.js'
|
|
32
34
|
|
|
33
35
|
export import data = Data.data
|
|
34
36
|
export import management = Management.management
|
|
37
|
+
export import mpp = Mpp.mpp
|
|
38
|
+
export import relay = Relay.relay
|
|
35
39
|
|
|
36
40
|
/** The chain-data route group; see {@link data}. */
|
|
37
41
|
export type DataApp = ReturnType<typeof data>
|
|
38
42
|
/** The management route group; see {@link management}. */
|
|
39
43
|
export type ManagementApp = ReturnType<typeof management>
|
|
44
|
+
/** The MPP credential relay route group; see {@link mpp}. */
|
|
45
|
+
export type MppApp = ReturnType<typeof mpp>
|
|
46
|
+
/** The wallet relay route group; see {@link relay}. */
|
|
47
|
+
export type RelayApp = ReturnType<typeof relay>
|
|
40
48
|
|
|
41
49
|
/** Hono environment variables set by the Tempo API app. */
|
|
42
50
|
export type Environment = {
|
|
@@ -82,7 +90,7 @@ export type Environment = {
|
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
/**
|
|
85
|
-
* The canonical fully-composed Tempo API app ({@link create} with
|
|
93
|
+
* The canonical fully-composed Tempo API app ({@link create} with all route
|
|
86
94
|
* groups). {@link Client} and the reference worker target this type; a subset
|
|
87
95
|
* consumer infers its own narrower type from its own composition chain.
|
|
88
96
|
*/
|
|
@@ -97,7 +105,11 @@ export function from<const app extends Hono<any, any>>(app: app, metadata: creat
|
|
|
97
105
|
// `App` (and thus `Client`) reflect the full route surface; real consumers
|
|
98
106
|
// compose explicitly.
|
|
99
107
|
function compose(options: create.Options) {
|
|
100
|
-
return create(options)
|
|
108
|
+
return create(options)
|
|
109
|
+
.route('/', data())
|
|
110
|
+
.route('/', relay())
|
|
111
|
+
.route('/', mpp())
|
|
112
|
+
.route('/', management())
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
/** Node.js request listener for `node:http` and compatible interfaces. */
|
package/src/Client.test-d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Client } from 'tapimo'
|
|
2
|
+
import { Mpp as PublicMpp } from 'tapimo/mpp'
|
|
2
3
|
import type * as z from 'zod/mini'
|
|
3
4
|
|
|
4
5
|
import * as Activities from './apps/data/routes/activities.js'
|
|
@@ -12,6 +13,7 @@ import * as Funding from './apps/data/routes/funding.js'
|
|
|
12
13
|
import * as Indexer from './apps/data/routes/indexer.js'
|
|
13
14
|
import * as InviteLinks from './apps/management/routes/invite-links.js'
|
|
14
15
|
import * as Me from './apps/management/routes/me.js'
|
|
16
|
+
import * as Mpp from './apps/mpp/Mpp.js'
|
|
15
17
|
import * as Orgs from './apps/management/routes/orgs.js'
|
|
16
18
|
import * as OrgWebhooks from './apps/management/routes/webhooks.js'
|
|
17
19
|
import * as Receipts from './apps/data/routes/receipts.js'
|
|
@@ -28,6 +30,18 @@ import * as Webhooks from './apps/data/routes/webhooks.js'
|
|
|
28
30
|
type Access400<code extends string> = code | 'api_key_malformed'
|
|
29
31
|
type PaidAccessStatus = PublicAccessStatus | 402
|
|
30
32
|
type PublicAccessStatus = 400 | 401 | 403 | 429
|
|
33
|
+
const mppInput = {
|
|
34
|
+
challenge: {
|
|
35
|
+
description: 'Tempo API payment',
|
|
36
|
+
id: 'ch_01j3j1k2l3m4n5p6q7r8s9t0u',
|
|
37
|
+
intent: 'charge',
|
|
38
|
+
method: 'tempo',
|
|
39
|
+
opaque: 'eyJhbGciOiJIUzI1NiJ9',
|
|
40
|
+
realm: 'merchant.example',
|
|
41
|
+
request: { amount: '1', currency: 'USD' },
|
|
42
|
+
},
|
|
43
|
+
payload: { signature: `0x${'aa'.repeat(65)}`, type: 'transaction' },
|
|
44
|
+
} satisfies Mpp.RelayInput
|
|
31
45
|
/** A `Schema.TokenAmount` carrying its optional nested valuation. */
|
|
32
46
|
type ValuedTokenAmount = {
|
|
33
47
|
baseUnits: string
|
|
@@ -54,9 +68,49 @@ test('returns typed client', async () => {
|
|
|
54
68
|
|
|
55
69
|
const typed: Client.Client = client
|
|
56
70
|
void typed
|
|
71
|
+
|
|
72
|
+
expectTypeOf<PublicMpp.RelayInput>().toEqualTypeOf<Mpp.RelayInput>()
|
|
57
73
|
})
|
|
58
74
|
|
|
59
75
|
describe('narrows responses by status', () => {
|
|
76
|
+
test('POST /v1/mpp/verify', async () => {
|
|
77
|
+
const client = Client.create()
|
|
78
|
+
const res = await client.v1.mpp.verify.$post({ json: mppInput })
|
|
79
|
+
|
|
80
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 400 | 401 | 403 | 429 | 503>()
|
|
81
|
+
|
|
82
|
+
if (res.status === 200) {
|
|
83
|
+
const json = await res.json()
|
|
84
|
+
expectTypeOf(json).toEqualTypeOf<Mpp.VerifyResponse>()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (res.status === 503) {
|
|
89
|
+
const json = await res.json()
|
|
90
|
+
expectTypeOf(json).toEqualTypeOf<Mpp.Unavailable>()
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('POST /v1/mpp/broadcast', async () => {
|
|
96
|
+
const client = Client.create()
|
|
97
|
+
const res = await client.v1.mpp.broadcast.$post({ json: mppInput })
|
|
98
|
+
|
|
99
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 400 | 401 | 403 | 429 | 503>()
|
|
100
|
+
|
|
101
|
+
if (res.status === 200) {
|
|
102
|
+
const json = await res.json()
|
|
103
|
+
expectTypeOf(json).toEqualTypeOf<Mpp.BroadcastResponse>()
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (res.status === 503) {
|
|
108
|
+
const json = await res.json()
|
|
109
|
+
expectTypeOf(json).toEqualTypeOf<Mpp.Unavailable>()
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
60
114
|
test('GET /v1/blocks', async () => {
|
|
61
115
|
const client = Client.create()
|
|
62
116
|
const res = await client.v1.blocks.$get()
|
|
@@ -125,7 +179,6 @@ describe('narrows responses by status', () => {
|
|
|
125
179
|
case 200: {
|
|
126
180
|
const json = await res.json()
|
|
127
181
|
expectTypeOf(json.id).toEqualTypeOf<z.output<typeof Me.schema.User>['id']>()
|
|
128
|
-
expectTypeOf(json.tmp_earlyAccess).toEqualTypeOf<boolean>()
|
|
129
182
|
void json
|
|
130
183
|
return
|
|
131
184
|
}
|
|
@@ -229,6 +282,14 @@ describe('narrows responses by status', () => {
|
|
|
229
282
|
expectTypeOf(json.tokens).toEqualTypeOf<
|
|
230
283
|
z.output<typeof Faucet.schema.FundResponse>['tokens']
|
|
231
284
|
>()
|
|
285
|
+
expectTypeOf(json.tokens[0]!.amount).toEqualTypeOf<
|
|
286
|
+
z.output<typeof Faucet.schema.FundResponse>['tokens'][number]['amount']
|
|
287
|
+
>()
|
|
288
|
+
expectTypeOf(json.tokens[0]!.token.symbol).toEqualTypeOf<string>()
|
|
289
|
+
// @ts-expect-error token metadata is nested under `token`
|
|
290
|
+
void json.tokens[0]!.symbol
|
|
291
|
+
// @ts-expect-error quantities are nested under `amount`
|
|
292
|
+
void json.tokens[0]!.token.amount
|
|
232
293
|
expectTypeOf(json.transactionHashes).toEqualTypeOf<
|
|
233
294
|
z.output<typeof Faucet.schema.FundResponse>['transactionHashes']
|
|
234
295
|
>()
|
|
@@ -250,21 +311,16 @@ describe('narrows responses by status', () => {
|
|
|
250
311
|
param: { orgId: 'org_test' },
|
|
251
312
|
})
|
|
252
313
|
|
|
253
|
-
expectTypeOf(res.status).toEqualTypeOf<200 | 404 |
|
|
314
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 502 | PublicAccessStatus>()
|
|
254
315
|
|
|
255
316
|
switch (res.status) {
|
|
256
317
|
case 200: {
|
|
257
318
|
const json = await res.json()
|
|
319
|
+
expectTypeOf(json.enabledSources).toEqualTypeOf<('stripe' | 'tempo')[]>()
|
|
258
320
|
expectTypeOf(json.status).toEqualTypeOf<z.output<typeof Billing.schema.Billing>['status']>()
|
|
259
321
|
void json
|
|
260
322
|
return
|
|
261
323
|
}
|
|
262
|
-
case 501: {
|
|
263
|
-
const json = await res.json()
|
|
264
|
-
expectTypeOf(json.error.code).toEqualTypeOf<'billing_unconfigured'>()
|
|
265
|
-
void json
|
|
266
|
-
return
|
|
267
|
-
}
|
|
268
324
|
}
|
|
269
325
|
})
|
|
270
326
|
|
|
@@ -302,11 +358,12 @@ describe('narrows responses by status', () => {
|
|
|
302
358
|
param: { orgId: 'org_test' },
|
|
303
359
|
})
|
|
304
360
|
|
|
305
|
-
expectTypeOf(res.status).toEqualTypeOf<200 | 404 |
|
|
361
|
+
expectTypeOf(res.status).toEqualTypeOf<200 | 404 | 502 | PublicAccessStatus>()
|
|
306
362
|
|
|
307
363
|
switch (res.status) {
|
|
308
364
|
case 200: {
|
|
309
365
|
const json = await res.json()
|
|
366
|
+
expectTypeOf(json.enabledSources).toEqualTypeOf<('stripe' | 'tempo')[]>()
|
|
310
367
|
expectTypeOf(json.spendLimit).toEqualTypeOf<
|
|
311
368
|
z.output<typeof Billing.schema.SpendLimit> | undefined
|
|
312
369
|
>()
|
|
@@ -314,12 +371,6 @@ describe('narrows responses by status', () => {
|
|
|
314
371
|
void json
|
|
315
372
|
return
|
|
316
373
|
}
|
|
317
|
-
case 501: {
|
|
318
|
-
const json = await res.json()
|
|
319
|
-
expectTypeOf(json.error.code).toEqualTypeOf<'billing_unconfigured'>()
|
|
320
|
-
void json
|
|
321
|
-
return
|
|
322
|
-
}
|
|
323
374
|
}
|
|
324
375
|
})
|
|
325
376
|
|
|
@@ -341,7 +392,7 @@ describe('narrows responses by status', () => {
|
|
|
341
392
|
case 403: {
|
|
342
393
|
const json = await res.json()
|
|
343
394
|
expectTypeOf(json.error.code).toEqualTypeOf<
|
|
344
|
-
'api_key_forbidden' | '
|
|
395
|
+
'api_key_forbidden' | 'billing_source_disabled' | 'forbidden'
|
|
345
396
|
>()
|
|
346
397
|
void json
|
|
347
398
|
return
|
|
@@ -644,9 +695,7 @@ describe('narrows responses by status', () => {
|
|
|
644
695
|
}
|
|
645
696
|
case 403: {
|
|
646
697
|
const json = await res.json()
|
|
647
|
-
expectTypeOf(json.error.code).toEqualTypeOf<
|
|
648
|
-
'early_access_required' | 'forbidden' | 'scope_not_issuable'
|
|
649
|
-
>()
|
|
698
|
+
expectTypeOf(json.error.code).toEqualTypeOf<'forbidden' | 'scope_not_issuable'>()
|
|
650
699
|
void json
|
|
651
700
|
return
|
|
652
701
|
}
|
|
@@ -1503,10 +1552,20 @@ describe('narrows responses by status', () => {
|
|
|
1503
1552
|
expectTypeOf(json.data[0]!.attribution).toEqualTypeOf<
|
|
1504
1553
|
z.output<typeof Transfers.schema.getTransfers.Transfer>['attribution']
|
|
1505
1554
|
>()
|
|
1506
|
-
|
|
1507
|
-
expectTypeOf(json.data[0]!.
|
|
1508
|
-
|
|
1509
|
-
>()
|
|
1555
|
+
expectTypeOf(json.data[0]!.destinationAmount).toEqualTypeOf<ValuedTokenAmount | undefined>()
|
|
1556
|
+
expectTypeOf(json.data[0]!.sourceAmount).toEqualTypeOf<ValuedTokenAmount>()
|
|
1557
|
+
expectTypeOf(json.data[0]!.sourceToken.currency).toEqualTypeOf<string>()
|
|
1558
|
+
expectTypeOf(json.data[0]!.sourceToken.decimals).toEqualTypeOf<number>()
|
|
1559
|
+
expectTypeOf(json.data[0]!.sourceToken.name).toEqualTypeOf<string>()
|
|
1560
|
+
expectTypeOf(json.data[0]!.sourceToken.symbol).toEqualTypeOf<string>()
|
|
1561
|
+
// @ts-expect-error token references do not contain quantities
|
|
1562
|
+
void json.data[0]!.sourceToken.amount
|
|
1563
|
+
// @ts-expect-error valuations belong to amount fields
|
|
1564
|
+
void json.data[0]!.sourceToken.valuation
|
|
1565
|
+
// @ts-expect-error destination token references do not contain quantities
|
|
1566
|
+
void json.data[0]!.destinationToken?.amount
|
|
1567
|
+
// @ts-expect-error destination valuations belong to amount fields
|
|
1568
|
+
void json.data[0]!.destinationToken?.valuation
|
|
1510
1569
|
// `meta` (the opt-in `include=totalCount` capped count) is optional.
|
|
1511
1570
|
expectTypeOf(json.meta).toEqualTypeOf<
|
|
1512
1571
|
z.output<typeof Transfers.schema.getTransfers.Response>['meta']
|
|
@@ -1547,14 +1606,32 @@ describe('narrows responses by status', () => {
|
|
|
1547
1606
|
switch (res.status) {
|
|
1548
1607
|
case 200: {
|
|
1549
1608
|
const json = await res.json()
|
|
1609
|
+
expectTypeOf(json.data[0]!.destinationAmount).toEqualTypeOf<{
|
|
1610
|
+
baseUnits: string
|
|
1611
|
+
currency: string
|
|
1612
|
+
decimals: number
|
|
1613
|
+
formatted: string
|
|
1614
|
+
}>()
|
|
1615
|
+
expectTypeOf(json.data[0]!.destinationAmountMin).toEqualTypeOf<
|
|
1616
|
+
| {
|
|
1617
|
+
baseUnits: string
|
|
1618
|
+
currency: string
|
|
1619
|
+
decimals: number
|
|
1620
|
+
formatted: string
|
|
1621
|
+
}
|
|
1622
|
+
| undefined
|
|
1623
|
+
>()
|
|
1550
1624
|
expectTypeOf(json.data[0]!.sourceAmount).toEqualTypeOf<
|
|
1551
1625
|
z.output<typeof Funding.schema.quoteFunding.Response>['data'][number]['sourceAmount']
|
|
1552
1626
|
>()
|
|
1627
|
+
// @ts-expect-error token references do not contain quantities
|
|
1628
|
+
void json.data[0]!.sourceToken.amount
|
|
1629
|
+
// @ts-expect-error provider details no longer duplicate destination amounts
|
|
1630
|
+
void json.data[0]!.quote.outputAmount
|
|
1553
1631
|
expectTypeOf(json.data[0]!.provider.id).toEqualTypeOf<
|
|
1554
1632
|
'across' | 'relay' | 'rhino' | 'squid' | 'symbiosis'
|
|
1555
1633
|
>()
|
|
1556
1634
|
expectTypeOf(json.data[0]!.quote.expiresAt).toEqualTypeOf<string | undefined>()
|
|
1557
|
-
expectTypeOf(json.data[0]!.quote.outputAmount).toEqualTypeOf<string>()
|
|
1558
1635
|
void json
|
|
1559
1636
|
return
|
|
1560
1637
|
}
|
|
@@ -2053,12 +2130,16 @@ describe('narrows responses by status', () => {
|
|
|
2053
2130
|
expectTypeOf(json.data[0]!.userToken.address).toEqualTypeOf<
|
|
2054
2131
|
z.output<typeof FeeAmm.schema.getFeeAmmPools.Token>['address']
|
|
2055
2132
|
>()
|
|
2056
|
-
expectTypeOf(json.data[0]!.
|
|
2057
|
-
z.output<typeof FeeAmm.schema.getFeeAmmPools.
|
|
2133
|
+
expectTypeOf(json.data[0]!.userAmount).toEqualTypeOf<
|
|
2134
|
+
z.output<typeof FeeAmm.schema.getFeeAmmPools.Pool>['userAmount']
|
|
2058
2135
|
>()
|
|
2059
|
-
expectTypeOf(json.data[0]!.
|
|
2060
|
-
z.output<typeof FeeAmm.schema.getFeeAmmPools.
|
|
2136
|
+
expectTypeOf(json.data[0]!.validatorAmount).toEqualTypeOf<
|
|
2137
|
+
z.output<typeof FeeAmm.schema.getFeeAmmPools.Pool>['validatorAmount']
|
|
2061
2138
|
>()
|
|
2139
|
+
// @ts-expect-error token references do not contain quantities
|
|
2140
|
+
void json.data[0]!.userToken.amount
|
|
2141
|
+
// @ts-expect-error token references do not contain formatted quantities
|
|
2142
|
+
void json.data[0]!.validatorToken.formatted
|
|
2062
2143
|
expectTypeOf(json.nextCursor).toEqualTypeOf<string | null>()
|
|
2063
2144
|
void json
|
|
2064
2145
|
return
|
package/src/Scope.ts
CHANGED
|
@@ -49,6 +49,11 @@ export const catalog = [
|
|
|
49
49
|
scope: 'management:write',
|
|
50
50
|
selfServe: true,
|
|
51
51
|
},
|
|
52
|
+
{
|
|
53
|
+
description: 'Validate and broadcast MPP payment credentials through the Tempo relay.',
|
|
54
|
+
scope: 'mpp:write',
|
|
55
|
+
selfServe: true,
|
|
56
|
+
},
|
|
52
57
|
{
|
|
53
58
|
description: 'Fill and submit transactions through the relay.',
|
|
54
59
|
scope: 'rpc-relay:read',
|
package/src/admin/App.test.ts
CHANGED
|
@@ -20,6 +20,10 @@ describe('create', () => {
|
|
|
20
20
|
new Request('http://admin/api-keys/key_x', { method: 'DELETE' }),
|
|
21
21
|
new Request('http://admin/organizations'),
|
|
22
22
|
new Request('http://admin/organizations/org_x'),
|
|
23
|
+
new Request('http://admin/organizations/org_x/billing-sources/stripe', { method: 'PUT' }),
|
|
24
|
+
new Request('http://admin/organizations/org_x/billing-sources/stripe', {
|
|
25
|
+
method: 'DELETE',
|
|
26
|
+
}),
|
|
23
27
|
new Request('http://admin/organizations/org_x/members'),
|
|
24
28
|
new Request('http://admin/organizations/org_x/projects'),
|
|
25
29
|
new Request('http://admin/verified-tokens?chainId=42431', {
|
|
@@ -29,7 +29,7 @@ export namespace schema {
|
|
|
29
29
|
z.object({
|
|
30
30
|
data: z.array(Entry).check(z.describe('Allowlist entries, ordered by entry.')),
|
|
31
31
|
}),
|
|
32
|
-
'
|
|
32
|
+
'Accounts whose new organizations receive early access features by default.',
|
|
33
33
|
)
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -42,7 +42,7 @@ export namespace schema {
|
|
|
42
42
|
z.trim(),
|
|
43
43
|
z.toLowerCase(),
|
|
44
44
|
z.regex(entryPattern, 'Must be a bare domain (example.org) or a full email (user@example.org).'), // prettier-ignore
|
|
45
|
-
z.describe('Domain or exact email
|
|
45
|
+
z.describe('Domain or exact email whose new organizations receive early access features; normalized lowercase.'), // prettier-ignore
|
|
46
46
|
),
|
|
47
47
|
})
|
|
48
48
|
.check(z.describe('Request body for adding an allowlist entry.'))
|
|
@@ -77,8 +77,8 @@ export namespace schema {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* Management routes for the
|
|
81
|
-
*
|
|
80
|
+
* Management routes for the early-access allowlist (`early_access` table).
|
|
81
|
+
* Matching users receive Stripe by default when they create an organization.
|
|
82
82
|
*
|
|
83
83
|
* - `GET /` lists entries.
|
|
84
84
|
* - `POST /` adds an entry (`409` on a duplicate).
|
|
@@ -92,7 +92,8 @@ export function earlyAccess() {
|
|
|
92
92
|
operationId: 'listEarlyAccessEntries',
|
|
93
93
|
responses: OpenApi.responses({
|
|
94
94
|
success: {
|
|
95
|
-
description:
|
|
95
|
+
description:
|
|
96
|
+
'Accounts whose new organizations receive early access features by default.',
|
|
96
97
|
schema: schema.listEarlyAccessEntries.Response,
|
|
97
98
|
},
|
|
98
99
|
}),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type * as Analytics from '../../analytics/Analytics.js'
|
|
2
2
|
import * as ApiKeys from '../../ApiKeys.js'
|
|
3
3
|
import * as BillingSettings from '../../db/tables/billingSettings.js'
|
|
4
|
+
import * as EnabledBillingSources from '../../db/tables/enabledBillingSources.js'
|
|
4
5
|
import * as Memberships from '../../db/tables/memberships.js'
|
|
5
6
|
import * as Organizations from '../../db/tables/organizations.js'
|
|
6
7
|
import * as Projects from '../../db/tables/projects.js'
|
|
@@ -134,6 +135,16 @@ describe('GET /organizations/:orgId', () => {
|
|
|
134
135
|
})
|
|
135
136
|
await Users.setEmail(db, user.id, 'owner@tempo.xyz')
|
|
136
137
|
const organization = await Organizations.createOwned(db, { name: 'Tempo', userId: user.id })
|
|
138
|
+
await EnabledBillingSources.add(db, {
|
|
139
|
+
createdBy: TestAdmin.identity.email,
|
|
140
|
+
orgId: organization.id,
|
|
141
|
+
source: 'stripe',
|
|
142
|
+
})
|
|
143
|
+
await EnabledBillingSources.add(db, {
|
|
144
|
+
createdBy: TestAdmin.identity.email,
|
|
145
|
+
orgId: organization.id,
|
|
146
|
+
source: 'tempo',
|
|
147
|
+
})
|
|
137
148
|
await Projects.create(db, { name: 'API', orgId: organization.id })
|
|
138
149
|
await BillingSettings.upsert(db, {
|
|
139
150
|
environment: 'production',
|
|
@@ -161,6 +172,7 @@ describe('GET /organizations/:orgId', () => {
|
|
|
161
172
|
expect(response.status).toBe(200)
|
|
162
173
|
expect(body.memberCount).toBe(1)
|
|
163
174
|
expect(body.projectCount).toBe(1)
|
|
175
|
+
expect(body.billing.enabledSources).toEqual(['stripe', 'tempo'])
|
|
164
176
|
expect(body.billing.production).toMatchObject({
|
|
165
177
|
settings: { spendLimit: '250', txFeeLimit: '0.50' },
|
|
166
178
|
source: { status: 'active', stripeCustomerId: 'cus_admin_lookup' },
|
|
@@ -182,6 +194,7 @@ describe('GET /organizations/:orgId', () => {
|
|
|
182
194
|
|
|
183
195
|
expect(body).toMatchObject({
|
|
184
196
|
billing: {
|
|
197
|
+
enabledSources: [],
|
|
185
198
|
production: { settings: null, source: null },
|
|
186
199
|
sandbox: { settings: null, source: null },
|
|
187
200
|
},
|
|
@@ -202,6 +215,78 @@ describe('GET /organizations/:orgId', () => {
|
|
|
202
215
|
})
|
|
203
216
|
})
|
|
204
217
|
|
|
218
|
+
describe('PUT/DELETE /organizations/:orgId/billing-sources/:source', () => {
|
|
219
|
+
test('enables and disables sources idempotently', async () => {
|
|
220
|
+
const { client, db } = TestAdmin.setup()
|
|
221
|
+
const organization = await Organizations.create(db, { name: 'Billing sources' })
|
|
222
|
+
|
|
223
|
+
for (const source of ['tempo', 'stripe', 'stripe'] as const) {
|
|
224
|
+
const response = await client.organizations[':orgId']['billing-sources'][':source'].$put({
|
|
225
|
+
param: { orgId: organization.id, source },
|
|
226
|
+
})
|
|
227
|
+
const body = await TestApp.json(response, AdminOrganizations.schema.setBillingSource.Response)
|
|
228
|
+
expect(response.status).toBe(200)
|
|
229
|
+
expect(body.data).toEqual(source === 'tempo' ? ['tempo'] : ['stripe', 'tempo'])
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
expect(await EnabledBillingSources.listByOrg(db, organization.id)).toMatchObject([
|
|
233
|
+
{ createdBy: TestAdmin.identity.email, source: 'stripe' },
|
|
234
|
+
{ createdBy: TestAdmin.identity.email, source: 'tempo' },
|
|
235
|
+
])
|
|
236
|
+
|
|
237
|
+
for (const source of ['stripe', 'stripe'] as const) {
|
|
238
|
+
const response = await client.organizations[':orgId']['billing-sources'][':source'].$delete({
|
|
239
|
+
param: { orgId: organization.id, source },
|
|
240
|
+
})
|
|
241
|
+
const body = await TestApp.json(response, AdminOrganizations.schema.setBillingSource.Response)
|
|
242
|
+
expect(response.status).toBe(200)
|
|
243
|
+
expect(body.data).toEqual(['tempo'])
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const detailResponse = await client.organizations[':orgId'].$get({
|
|
247
|
+
param: { orgId: organization.id },
|
|
248
|
+
})
|
|
249
|
+
const detail = await TestApp.json(
|
|
250
|
+
detailResponse,
|
|
251
|
+
AdminOrganizations.schema.getOrganization.Response,
|
|
252
|
+
)
|
|
253
|
+
expect(detail.billing.enabledSources).toEqual(['tempo'])
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('rejects unsupported sources and unknown organizations', async () => {
|
|
257
|
+
const { app, client } = TestAdmin.setup()
|
|
258
|
+
const invalid = await app.request('/organizations/org_example/billing-sources/card', {
|
|
259
|
+
method: 'PUT',
|
|
260
|
+
})
|
|
261
|
+
expect(invalid.status).toBe(400)
|
|
262
|
+
|
|
263
|
+
const missing = await client.organizations[':orgId']['billing-sources'][':source'].$put({
|
|
264
|
+
param: { orgId: 'org_missing', source: 'stripe' },
|
|
265
|
+
})
|
|
266
|
+
expect(missing.status).toBe(404)
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
test('records billing-source mutations in the admin audit log', async () => {
|
|
270
|
+
const { client, db } = TestAdmin.setup()
|
|
271
|
+
const organization = await Organizations.create(db, { name: 'Audited billing' })
|
|
272
|
+
await client.organizations[':orgId']['billing-sources'][':source'].$put({
|
|
273
|
+
param: { orgId: organization.id, source: 'tempo' },
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const records = await db.kysely
|
|
277
|
+
.selectFrom('admin_audit_logs')
|
|
278
|
+
.selectAll()
|
|
279
|
+
.orderBy('createdAt', 'desc')
|
|
280
|
+
.execute()
|
|
281
|
+
expect(records[0]).toMatchObject({
|
|
282
|
+
actor: TestAdmin.identity.email,
|
|
283
|
+
method: 'PUT',
|
|
284
|
+
path: `/organizations/${organization.id}/billing-sources/tempo`,
|
|
285
|
+
status: 200,
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
205
290
|
describe('GET /organizations/:orgId/members', () => {
|
|
206
291
|
test('returns a bounded cursor page without duplicates', async () => {
|
|
207
292
|
const { client, db } = TestAdmin.setup()
|
|
@@ -444,8 +529,7 @@ const usageAnalytics: Analytics.Analytics = {
|
|
|
444
529
|
async insert() {},
|
|
445
530
|
async migrate() {},
|
|
446
531
|
async query(sql) {
|
|
447
|
-
if (sql.includes('GROUP BY
|
|
448
|
-
return [{ code: 'upstream_error', requests: 2 }] as never[]
|
|
532
|
+
if (sql.includes('GROUP BY code')) return [{ code: 'upstream_error', requests: 2 }] as never[]
|
|
449
533
|
if (sql.includes('GROUP BY route'))
|
|
450
534
|
return [{ averageDurationMs: 12.5, errors: 2, requests: 20, route: '/v1/tokens' }] as never[]
|
|
451
535
|
if (sql.includes('GROUP BY status')) return [{ requests: 2, status: 500 }] as never[]
|