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
|
@@ -51,7 +51,11 @@ const seed: readonly SeedToken[] = JSON.parse(
|
|
|
51
51
|
// Deterministic EUR-based rate set so conversion math is exact in assertions.
|
|
52
52
|
const fixed = FxOracle.from({
|
|
53
53
|
name: 'fixed',
|
|
54
|
-
rates: async () => ({
|
|
54
|
+
rates: async () => ({
|
|
55
|
+
asOf: '2026-01-01T00:00:00.000Z',
|
|
56
|
+
base: 'EUR',
|
|
57
|
+
rates: { AUD: '1.6', USD: '1.0' },
|
|
58
|
+
}),
|
|
55
59
|
})
|
|
56
60
|
|
|
57
61
|
async function holderWithBalance(client: ReturnType<typeof app>) {
|
|
@@ -93,6 +97,17 @@ async function chainSum(holder: `0x${string}`, currency: string) {
|
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
describe('GET /addresses/:address/valuation', () => {
|
|
100
|
+
test('requires a full pricing timestamp', () => {
|
|
101
|
+
const pricing = { asOf: '2026-01-01', basis: 'nominal', source: 'fixed' }
|
|
102
|
+
expect(Valuation.schema.Pricing.safeParse(pricing).success).toBe(false)
|
|
103
|
+
expect(
|
|
104
|
+
Valuation.schema.Pricing.safeParse({
|
|
105
|
+
...pricing,
|
|
106
|
+
asOf: '2026-01-01T00:00:00.000Z',
|
|
107
|
+
}).success,
|
|
108
|
+
).toBe(true)
|
|
109
|
+
})
|
|
110
|
+
|
|
96
111
|
test('normalizes the denomination', () => {
|
|
97
112
|
expect(
|
|
98
113
|
Valuation.schema.getAddressValuation.Query.parse({ denomination: 'aud' }).denomination,
|
|
@@ -176,7 +191,7 @@ describe('GET /addresses/:address/valuation', () => {
|
|
|
176
191
|
expect(aud.amount).toBe(core_Value.format((sum * 16n) / 10n, 6))
|
|
177
192
|
expect(aud.pricing).toMatchInlineSnapshot(`
|
|
178
193
|
{
|
|
179
|
-
"asOf": "2026-01-
|
|
194
|
+
"asOf": "2026-01-01T00:00:00.000Z",
|
|
180
195
|
"basis": "nominal",
|
|
181
196
|
"source": "fixed",
|
|
182
197
|
}
|
|
@@ -201,7 +216,7 @@ describe('GET /addresses/:address/valuation', () => {
|
|
|
201
216
|
expect(Number(body.amount)).toBeGreaterThan(0)
|
|
202
217
|
expect(body.pricing?.basis).toBe('nominal')
|
|
203
218
|
expect(body.pricing?.source).toBe('ecb')
|
|
204
|
-
expect(body.pricing?.asOf).
|
|
219
|
+
expect(body.pricing?.asOf).toBe('2026-01-01T00:00:00.000Z')
|
|
205
220
|
})
|
|
206
221
|
|
|
207
222
|
test.skipIf(runtime.mode !== 'localnet')(
|
|
@@ -44,11 +44,11 @@ export namespace schema {
|
|
|
44
44
|
/** Rate provenance for a converted valuation. */
|
|
45
45
|
export const Pricing = z
|
|
46
46
|
.object({
|
|
47
|
-
asOf: z
|
|
48
|
-
.
|
|
47
|
+
asOf: z.iso
|
|
48
|
+
.datetime()
|
|
49
49
|
.check(
|
|
50
|
-
z.describe('Publication date of the rate set
|
|
51
|
-
z.meta({ examples: ['2026-07-
|
|
50
|
+
z.describe('Publication date of the rate set as an ISO 8601 timestamp.'),
|
|
51
|
+
z.meta({ examples: ['2026-07-20T00:00:00.000Z'] }),
|
|
52
52
|
),
|
|
53
53
|
basis: z
|
|
54
54
|
.literal('nominal')
|
package/src/apps/index.ts
CHANGED
|
@@ -5,9 +5,7 @@ import * as Auth from '../../../internal/Auth.js'
|
|
|
5
5
|
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
6
6
|
import * as Scope from '../../../Scope.js'
|
|
7
7
|
import * as TestApp from '../../../../test/App.js'
|
|
8
|
-
import * as EarlyAccess from '../../../db/tables/earlyAccess.js'
|
|
9
8
|
import * as Memberships from '../../../db/tables/memberships.js'
|
|
10
|
-
import * as Users from '../../../db/tables/users.js'
|
|
11
9
|
import * as Keys from './api-keys.js'
|
|
12
10
|
import * as Me from './me.js'
|
|
13
11
|
import * as Orgs from './orgs.js'
|
|
@@ -45,7 +43,6 @@ function createApp(
|
|
|
45
43
|
db?: ReturnType<typeof TestApp.database>
|
|
46
44
|
kv?: { store: State }
|
|
47
45
|
scopes?: readonly Scope.Entry[]
|
|
48
|
-
tmp_earlyAccess?: boolean
|
|
49
46
|
} = {},
|
|
50
47
|
) {
|
|
51
48
|
return TestApp.create({
|
|
@@ -54,7 +51,6 @@ function createApp(
|
|
|
54
51
|
...(options.kv ? { kv: options.kv } : {}),
|
|
55
52
|
...(options.scopes ? { scopes: options.scopes } : {}),
|
|
56
53
|
session: { wallet: { origin } },
|
|
57
|
-
...(options.tmp_earlyAccess === undefined ? {} : { tmp_earlyAccess: options.tmp_earlyAccess }),
|
|
58
54
|
})
|
|
59
55
|
}
|
|
60
56
|
|
|
@@ -677,66 +673,9 @@ describe('keys', () => {
|
|
|
677
673
|
})
|
|
678
674
|
|
|
679
675
|
describe('early access', () => {
|
|
680
|
-
test('
|
|
681
|
-
const app = createApp(
|
|
682
|
-
const cookie = await session(app)
|
|
683
|
-
const { org, project } = await fixture(app, cookie)
|
|
684
|
-
|
|
685
|
-
const denied = await app.request(
|
|
686
|
-
`/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
|
|
687
|
-
json('POST', cookie, { scopes: ['data:read'] }),
|
|
688
|
-
)
|
|
689
|
-
expect(denied.status).toBe(403)
|
|
690
|
-
const { requestId, ...body } = (await denied.json()) as { requestId?: string }
|
|
691
|
-
expect(requestId).toBeDefined()
|
|
692
|
-
expect(body).toMatchInlineSnapshot(`
|
|
693
|
-
{
|
|
694
|
-
"error": {
|
|
695
|
-
"code": "early_access_required",
|
|
696
|
-
"message": "Requires early access. Ask a teammate with access to invite you by email, or contact Tempo.",
|
|
697
|
-
},
|
|
698
|
-
}
|
|
699
|
-
`)
|
|
700
|
-
})
|
|
701
|
-
|
|
702
|
-
test('the gate precedes membership resolution', async () => {
|
|
703
|
-
// The gate depends only on the caller, so it leaks nothing about the
|
|
704
|
-
// project; early-access strangers still get the membership 404.
|
|
705
|
-
const db = TestApp.database()
|
|
706
|
-
const app = createApp({ db, tmp_earlyAccess: true })
|
|
707
|
-
await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
|
|
708
|
-
const owner = await session(app)
|
|
709
|
-
const { org, project } = await fixture(app, owner)
|
|
710
|
-
|
|
711
|
-
const other = await session(app)
|
|
712
|
-
const gated = await app.request(
|
|
713
|
-
`/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
|
|
714
|
-
json('POST', other, { scopes: ['data:read'] }),
|
|
715
|
-
)
|
|
716
|
-
expect(gated.status).toBe(403)
|
|
717
|
-
expect(((await gated.json()) as { error: { code: string } }).error.code).toBe('early_access_required') // prettier-ignore
|
|
718
|
-
|
|
719
|
-
const granted = await session(app)
|
|
720
|
-
const me = await app.request('/v1/me', { headers: { cookie: granted } })
|
|
721
|
-
const { id: userId } = (await me.json()) as { id: string }
|
|
722
|
-
await Users.setEmail(db, userId, 'stranger@example.org')
|
|
723
|
-
const denied = await app.request(
|
|
724
|
-
`/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
|
|
725
|
-
json('POST', granted, { scopes: ['data:read'] }),
|
|
726
|
-
)
|
|
727
|
-
expect(denied.status).toBe(404)
|
|
728
|
-
})
|
|
729
|
-
|
|
730
|
-
test('lets early-access sessions mint', async () => {
|
|
731
|
-
const db = TestApp.database()
|
|
732
|
-
const app = createApp({ db, tmp_earlyAccess: true })
|
|
733
|
-
await EarlyAccess.add(db, { createdBy: 'test', entry: 'example.org' })
|
|
676
|
+
test('does not require an early-access grant', async () => {
|
|
677
|
+
const app = createApp()
|
|
734
678
|
const cookie = await session(app)
|
|
735
|
-
const user = await TestApp.json(
|
|
736
|
-
await app.request('/v1/me', { headers: { cookie } }),
|
|
737
|
-
Me.schema.User,
|
|
738
|
-
)
|
|
739
|
-
await Users.setEmail(db, user.id, 'dev@example.org')
|
|
740
679
|
const { org, project } = await fixture(app, cookie)
|
|
741
680
|
|
|
742
681
|
const minted = await app.request(
|
|
@@ -745,16 +684,4 @@ describe('early access', () => {
|
|
|
745
684
|
)
|
|
746
685
|
expect(minted.status).toBe(200)
|
|
747
686
|
})
|
|
748
|
-
|
|
749
|
-
test('lets the super admin mint', async () => {
|
|
750
|
-
const app = createApp({ tmp_earlyAccess: true })
|
|
751
|
-
const cookie = await session(app)
|
|
752
|
-
const { org, project } = await fixture(app, cookie)
|
|
753
|
-
|
|
754
|
-
const minted = await app.request(
|
|
755
|
-
`/v1/orgs/${org.id}/projects/${project.id}/api-keys`,
|
|
756
|
-
superAdminJson('POST', { scopes: ['data:read'] }),
|
|
757
|
-
)
|
|
758
|
-
expect(minted.status).toBe(200)
|
|
759
|
-
})
|
|
760
687
|
})
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type Context, Hono } from 'hono'
|
|
2
2
|
import * as z from 'zod/mini'
|
|
3
3
|
|
|
4
|
+
import * as Analytics from '../../../analytics/Analytics.js'
|
|
5
|
+
import * as RequestEvents from '../../../analytics/tables/requestEvents.js'
|
|
4
6
|
import * as ApiKeys from '../../../ApiKeys.js'
|
|
5
7
|
import type * as App from '../../../App.js'
|
|
6
8
|
import * as Db from '../../../db/Db.js'
|
|
@@ -43,6 +45,12 @@ export namespace schema {
|
|
|
43
45
|
z.describe('Opaque key id (`key_…`).'),
|
|
44
46
|
z.meta({ examples: ['key_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
|
|
45
47
|
),
|
|
48
|
+
lastUsedAt: z
|
|
49
|
+
.optional(z.string())
|
|
50
|
+
.check(
|
|
51
|
+
z.describe('When the key last authenticated a request (ISO 8601).'),
|
|
52
|
+
z.meta({ examples: ['2026-07-21T12:34:56.000Z'] }),
|
|
53
|
+
),
|
|
46
54
|
name: z
|
|
47
55
|
.optional(z.string())
|
|
48
56
|
.check(z.describe('Human-readable key name.'), z.meta({ examples: ['CI'] })),
|
|
@@ -225,7 +233,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
225
233
|
return new Hono<App.Environment>()
|
|
226
234
|
.post(
|
|
227
235
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys',
|
|
228
|
-
Auth.policy({ session: true
|
|
236
|
+
Auth.policy({ session: true }),
|
|
229
237
|
Auth.ensureOrg({ role: 'admin' }),
|
|
230
238
|
Auth.ensureProject(),
|
|
231
239
|
OpenApi.validate('param', schema.Params, {
|
|
@@ -243,8 +251,8 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
243
251
|
responses: OpenApi.responses({
|
|
244
252
|
errors: {
|
|
245
253
|
403: {
|
|
246
|
-
codes: ['scope_not_issuable'
|
|
247
|
-
description: 'The requested scopes are not issuable
|
|
254
|
+
codes: ['scope_not_issuable'],
|
|
255
|
+
description: 'The requested scopes are not issuable.',
|
|
248
256
|
},
|
|
249
257
|
404: 'No accessible project was found for this id.',
|
|
250
258
|
},
|
|
@@ -259,7 +267,6 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
259
267
|
async (c) => {
|
|
260
268
|
if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
|
|
261
269
|
if (Auth.narrowScope) return Auth.ensureProjectError(c)
|
|
262
|
-
if (Auth.tmp_narrowEarlyAccess) return Auth.tmp_ensureEarlyAccessError(c)
|
|
263
270
|
if (OpenApi.narrowValidation)
|
|
264
271
|
return OpenApi.validationError(c, {
|
|
265
272
|
code: 'param_invalid',
|
|
@@ -375,7 +382,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
375
382
|
})
|
|
376
383
|
return c.json(
|
|
377
384
|
Response.validated(schema.listApiKeys.Response, {
|
|
378
|
-
data: records
|
|
385
|
+
data: await serializeKeys(c, records),
|
|
379
386
|
}),
|
|
380
387
|
200,
|
|
381
388
|
)
|
|
@@ -436,7 +443,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
436
443
|
})
|
|
437
444
|
return c.json(
|
|
438
445
|
Response.validated(schema.listOrgApiKeys.Response, {
|
|
439
|
-
data: records
|
|
446
|
+
data: await serializeKeys(c, records),
|
|
440
447
|
}),
|
|
441
448
|
200,
|
|
442
449
|
)
|
|
@@ -506,14 +513,30 @@ export declare namespace apiKeys {
|
|
|
506
513
|
}
|
|
507
514
|
}
|
|
508
515
|
|
|
516
|
+
/** Enriches key records with their latest authenticated request timestamp. */
|
|
517
|
+
async function serializeKeys(c: Context<App.Environment>, records: readonly ApiKeys.Record[]) {
|
|
518
|
+
const source = c.get('analytics')
|
|
519
|
+
if (!source || records.length === 0) return records.map((record) => serializeKey(record))
|
|
520
|
+
const lastUsedAt = new Map(
|
|
521
|
+
(
|
|
522
|
+
await RequestEvents.readLastUsedAt(
|
|
523
|
+
Analytics.get(source),
|
|
524
|
+
records.map(({ id }) => id),
|
|
525
|
+
)
|
|
526
|
+
).map((usage) => [usage.apiKeyId, usage.lastUsedAt]),
|
|
527
|
+
)
|
|
528
|
+
return records.map((record) => serializeKey(record, lastUsedAt.get(record.id)))
|
|
529
|
+
}
|
|
530
|
+
|
|
509
531
|
/** Projects a persisted record onto the management key shape (no rate limits, no token). */
|
|
510
|
-
function serializeKey(record: ApiKeys.Record) {
|
|
532
|
+
function serializeKey(record: ApiKeys.Record, lastUsedAt?: string) {
|
|
511
533
|
return {
|
|
512
534
|
createdAt: record.createdAt,
|
|
513
535
|
...(record.createdBy === undefined ? {} : { createdBy: record.createdBy }),
|
|
514
536
|
environment: record.environment,
|
|
515
537
|
...(record.expiresAt === undefined ? {} : { expiresAt: record.expiresAt }),
|
|
516
538
|
id: record.id,
|
|
539
|
+
...(lastUsedAt === undefined ? {} : { lastUsedAt }),
|
|
517
540
|
...(record.name === undefined ? {} : { name: record.name }),
|
|
518
541
|
orgId: record.orgId,
|
|
519
542
|
...(record.projectId === undefined ? {} : { projectId: record.projectId }),
|
|
@@ -5,11 +5,10 @@ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
|
|
|
5
5
|
import * as ApiKeys from '../../../ApiKeys.js'
|
|
6
6
|
import * as TestApp from '../../../../test/App.js'
|
|
7
7
|
import * as TestStripe from '../../../../test/Stripe.js'
|
|
8
|
-
import * as
|
|
8
|
+
import * as EnabledBillingSources from '../../../db/tables/enabledBillingSources.js'
|
|
9
9
|
import * as Memberships from '../../../db/tables/memberships.js'
|
|
10
10
|
import * as SponsoredTransactions from '../../../db/tables/sponsoredTransactions.js'
|
|
11
11
|
import * as StripeCustomers from '../../../db/tables/stripeCustomers.js'
|
|
12
|
-
import * as Users from '../../../db/tables/users.js'
|
|
13
12
|
import * as Viem from '../../../internal/Viem.js'
|
|
14
13
|
import * as core_Billing from '../Billing.js'
|
|
15
14
|
import type * as App from '../../../App.js'
|
|
@@ -66,7 +65,6 @@ function createApp(
|
|
|
66
65
|
billing?: App.Billing | undefined
|
|
67
66
|
db?: TestApp.create.Options['db']
|
|
68
67
|
kv?: { store: ReturnType<typeof TestApp.kvStore> }
|
|
69
|
-
tmp_earlyAccess?: boolean
|
|
70
68
|
} = {},
|
|
71
69
|
) {
|
|
72
70
|
// prettier-ignore
|
|
@@ -76,7 +74,6 @@ function createApp(
|
|
|
76
74
|
db: options.db ?? TestApp.database(),
|
|
77
75
|
...(options.kv ? { kv: options.kv } : {}),
|
|
78
76
|
session: { wallet: { origin } },
|
|
79
|
-
...(options.tmp_earlyAccess === undefined ? {} : { tmp_earlyAccess: options.tmp_earlyAccess }),
|
|
80
77
|
})
|
|
81
78
|
}
|
|
82
79
|
|
|
@@ -108,6 +105,20 @@ async function seeded(app: ReturnType<typeof createApp>) {
|
|
|
108
105
|
return { cookie, orgId: id }
|
|
109
106
|
}
|
|
110
107
|
|
|
108
|
+
/** Seeds an owned organization with Stripe setup enabled. */
|
|
109
|
+
async function seededWithStripe(
|
|
110
|
+
app: ReturnType<typeof createApp>,
|
|
111
|
+
db: ReturnType<typeof TestApp.database>,
|
|
112
|
+
) {
|
|
113
|
+
const result = await seeded(app)
|
|
114
|
+
await EnabledBillingSources.add(db, {
|
|
115
|
+
createdBy: 'test',
|
|
116
|
+
orgId: result.orgId,
|
|
117
|
+
source: 'stripe',
|
|
118
|
+
})
|
|
119
|
+
return result
|
|
120
|
+
}
|
|
121
|
+
|
|
111
122
|
describe('GET /orgs/:orgId/billing', () => {
|
|
112
123
|
test('behavior: synthesizes none for orgs without a billing source', async () => {
|
|
113
124
|
const app = createApp()
|
|
@@ -116,6 +127,7 @@ describe('GET /orgs/:orgId/billing', () => {
|
|
|
116
127
|
expect(response.status).toBe(200)
|
|
117
128
|
expect(await response.json()).toMatchInlineSnapshot(`
|
|
118
129
|
{
|
|
130
|
+
"enabledSources": [],
|
|
119
131
|
"spend": {
|
|
120
132
|
"amount": "0",
|
|
121
133
|
"currency": "usd",
|
|
@@ -139,6 +151,28 @@ describe('GET /orgs/:orgId/billing', () => {
|
|
|
139
151
|
expect(body.updatedAt).toBeDefined()
|
|
140
152
|
})
|
|
141
153
|
|
|
154
|
+
test('behavior: lists organization-wide sources in canonical order', async () => {
|
|
155
|
+
const db = TestApp.database()
|
|
156
|
+
const app = createApp({ billing: billingWithSandbox(), db })
|
|
157
|
+
const { cookie, orgId } = await seeded(app)
|
|
158
|
+
await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'tempo' })
|
|
159
|
+
await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'stripe' })
|
|
160
|
+
|
|
161
|
+
const production = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } })
|
|
162
|
+
const sandbox = await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, {
|
|
163
|
+
headers: { cookie },
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
expect(await production.json()).toMatchObject({
|
|
167
|
+
enabledSources: ['stripe', 'tempo'],
|
|
168
|
+
status: 'none',
|
|
169
|
+
})
|
|
170
|
+
expect(await sandbox.json()).toMatchObject({
|
|
171
|
+
enabledSources: ['stripe', 'tempo'],
|
|
172
|
+
status: 'none',
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
142
176
|
test('behavior: the super admin reads any org', async () => {
|
|
143
177
|
const db = TestApp.database()
|
|
144
178
|
const app = createApp({ db })
|
|
@@ -165,7 +199,7 @@ describe('GET /orgs/:orgId/billing', () => {
|
|
|
165
199
|
expect(response.status).toBe(401)
|
|
166
200
|
})
|
|
167
201
|
|
|
168
|
-
test('behavior:
|
|
202
|
+
test('behavior: reads database-backed state when Stripe is unconfigured', async () => {
|
|
169
203
|
const app = TestApp.create({
|
|
170
204
|
auth: { superAdmin: { secret } },
|
|
171
205
|
db: TestApp.database(),
|
|
@@ -173,9 +207,18 @@ describe('GET /orgs/:orgId/billing', () => {
|
|
|
173
207
|
})
|
|
174
208
|
const { cookie, orgId } = await seeded(app)
|
|
175
209
|
const response = await app.request(`/v1/orgs/${orgId}/billing`, { headers: { cookie } })
|
|
176
|
-
expect(response.status).toBe(
|
|
177
|
-
|
|
178
|
-
|
|
210
|
+
expect(response.status).toBe(200)
|
|
211
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
212
|
+
{
|
|
213
|
+
"enabledSources": [],
|
|
214
|
+
"spend": {
|
|
215
|
+
"amount": "0",
|
|
216
|
+
"currency": "usd",
|
|
217
|
+
"period": "month",
|
|
218
|
+
},
|
|
219
|
+
"status": "none",
|
|
220
|
+
}
|
|
221
|
+
`)
|
|
179
222
|
})
|
|
180
223
|
})
|
|
181
224
|
|
|
@@ -202,6 +245,7 @@ describe('PATCH /orgs/:orgId/billing', () => {
|
|
|
202
245
|
expect(response.status).toBe(200)
|
|
203
246
|
expect(await response.json()).toMatchInlineSnapshot(`
|
|
204
247
|
{
|
|
248
|
+
"enabledSources": [],
|
|
205
249
|
"spend": {
|
|
206
250
|
"amount": "0",
|
|
207
251
|
"currency": "usd",
|
|
@@ -294,7 +338,7 @@ describe('PATCH /orgs/:orgId/billing', () => {
|
|
|
294
338
|
expect(response.status).toBe(404)
|
|
295
339
|
})
|
|
296
340
|
|
|
297
|
-
test('behavior:
|
|
341
|
+
test('behavior: updates database-backed state when Stripe is unconfigured', async () => {
|
|
298
342
|
const app = TestApp.create({
|
|
299
343
|
auth: { superAdmin: { secret } },
|
|
300
344
|
db: TestApp.database(),
|
|
@@ -303,9 +347,12 @@ describe('PATCH /orgs/:orgId/billing', () => {
|
|
|
303
347
|
const { cookie, orgId } = await seeded(app)
|
|
304
348
|
const response = await app.request(
|
|
305
349
|
`/v1/orgs/${orgId}/billing`,
|
|
306
|
-
patch(cookie, { spendLimit:
|
|
350
|
+
patch(cookie, { spendLimit: { amount: '250' } }),
|
|
307
351
|
)
|
|
308
|
-
expect(response.status).toBe(
|
|
352
|
+
expect(response.status).toBe(200)
|
|
353
|
+
expect(
|
|
354
|
+
((await response.json()) as { spendLimit?: { amount: string } }).spendLimit?.amount,
|
|
355
|
+
).toBe('250')
|
|
309
356
|
})
|
|
310
357
|
})
|
|
311
358
|
|
|
@@ -355,8 +402,9 @@ describe('POST /orgs/:orgId/billing/stripe/checkout', () => {
|
|
|
355
402
|
})
|
|
356
403
|
|
|
357
404
|
test('behavior: an unreachable Stripe maps to the upstream error envelope', async () => {
|
|
358
|
-
const
|
|
359
|
-
const {
|
|
405
|
+
const db = TestApp.database()
|
|
406
|
+
const app = createApp({ db })
|
|
407
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
360
408
|
// First Stripe call the route makes is the customer create; the connection
|
|
361
409
|
// is refused for real (no transport fake).
|
|
362
410
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
@@ -366,9 +414,13 @@ describe('POST /orgs/:orgId/billing/stripe/checkout', () => {
|
|
|
366
414
|
})
|
|
367
415
|
})
|
|
368
416
|
|
|
369
|
-
describe('
|
|
370
|
-
test('behavior: checkout requires
|
|
371
|
-
const app =
|
|
417
|
+
describe('billing source access', () => {
|
|
418
|
+
test('behavior: checkout requires Stripe to be enabled', async () => {
|
|
419
|
+
const app = TestApp.create({
|
|
420
|
+
auth: { superAdmin: { secret } },
|
|
421
|
+
db: TestApp.database(),
|
|
422
|
+
session: { wallet: { origin } },
|
|
423
|
+
})
|
|
372
424
|
const { cookie, orgId } = await seeded(app)
|
|
373
425
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
374
426
|
expect(response.status).toBe(403)
|
|
@@ -377,75 +429,29 @@ describe('early access', () => {
|
|
|
377
429
|
expect(body).toMatchInlineSnapshot(`
|
|
378
430
|
{
|
|
379
431
|
"error": {
|
|
380
|
-
"code": "
|
|
381
|
-
"message": "
|
|
432
|
+
"code": "billing_source_disabled",
|
|
433
|
+
"message": "Stripe billing is not enabled for this organization",
|
|
382
434
|
},
|
|
383
435
|
}
|
|
384
436
|
`)
|
|
385
437
|
})
|
|
386
438
|
|
|
387
|
-
test('behavior: the gate
|
|
388
|
-
// The gate depends only on the caller, so it leaks nothing about the org;
|
|
389
|
-
// early-access strangers still get the membership 404.
|
|
439
|
+
test('behavior: the source gate replaces temporary billing early access', async () => {
|
|
390
440
|
const db = TestApp.database()
|
|
391
|
-
const app = createApp({ db
|
|
392
|
-
|
|
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')
|
|
441
|
+
const app = createApp({ db })
|
|
442
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
438
443
|
|
|
439
|
-
// The unreachable Stripe upstream
|
|
444
|
+
// The unreachable Stripe upstream proves source access, not user early access, is the gate.
|
|
440
445
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
441
446
|
expect(response.status).toBe(502)
|
|
442
447
|
expect(((await response.json()) as { error: { code: string } }).error.code).toBe('upstream_error') // prettier-ignore
|
|
443
448
|
})
|
|
444
449
|
|
|
445
|
-
test('behavior: management
|
|
450
|
+
test('behavior: an enabled management key reaches Stripe', async () => {
|
|
451
|
+
const db = TestApp.database()
|
|
446
452
|
const store = TestApp.kvStore({ keys: [] })
|
|
447
|
-
const app = createApp({
|
|
448
|
-
const { orgId } = await
|
|
453
|
+
const app = createApp({ db, kv: { store } })
|
|
454
|
+
const { orgId } = await seededWithStripe(app, db)
|
|
449
455
|
const { token } = await ApiKeys.mint(store, { orgId, scopes: ['management:write'] })
|
|
450
456
|
|
|
451
457
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, {
|
|
@@ -455,9 +461,10 @@ describe('early access', () => {
|
|
|
455
461
|
expect(response.status).toBe(502)
|
|
456
462
|
})
|
|
457
463
|
|
|
458
|
-
test('behavior:
|
|
459
|
-
const
|
|
460
|
-
const {
|
|
464
|
+
test('behavior: an enabled super admin reaches Stripe', async () => {
|
|
465
|
+
const db = TestApp.database()
|
|
466
|
+
const app = createApp({ db })
|
|
467
|
+
const { orgId } = await seededWithStripe(app, db)
|
|
461
468
|
|
|
462
469
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, {
|
|
463
470
|
...as(secret),
|
|
@@ -551,6 +558,7 @@ describe('DELETE /orgs/:orgId', () => {
|
|
|
551
558
|
const db = TestApp.database()
|
|
552
559
|
const app = createApp({ db })
|
|
553
560
|
const { cookie, orgId } = await seeded(app)
|
|
561
|
+
await EnabledBillingSources.add(db, { createdBy: 'test', orgId, source: 'stripe' })
|
|
554
562
|
await StripeCustomers.create(db, { orgId, stripeCustomerId: 'cus_cascade' })
|
|
555
563
|
|
|
556
564
|
const response = await app.request(`/v1/orgs/${orgId}`, {
|
|
@@ -558,19 +566,30 @@ describe('DELETE /orgs/:orgId', () => {
|
|
|
558
566
|
method: 'DELETE',
|
|
559
567
|
})
|
|
560
568
|
expect(response.status).toBe(200)
|
|
569
|
+
expect(await EnabledBillingSources.listByOrg(db, orgId)).toEqual([])
|
|
561
570
|
expect(await StripeCustomers.get(db, orgId)).toBeUndefined()
|
|
562
571
|
})
|
|
563
572
|
})
|
|
564
573
|
|
|
565
574
|
describe('environment scoping', () => {
|
|
566
|
-
test('behavior: sandbox
|
|
575
|
+
test('behavior: sandbox billing state remains readable without a Stripe source', async () => {
|
|
567
576
|
const app = createApp()
|
|
568
577
|
const { cookie, orgId } = await seeded(app)
|
|
569
578
|
const response = await app.request(`/v1/orgs/${orgId}/billing?environment=sandbox`, {
|
|
570
579
|
headers: { cookie },
|
|
571
580
|
})
|
|
572
|
-
expect(response.status).toBe(
|
|
573
|
-
expect(
|
|
581
|
+
expect(response.status).toBe(200)
|
|
582
|
+
expect(await response.json()).toMatchInlineSnapshot(`
|
|
583
|
+
{
|
|
584
|
+
"enabledSources": [],
|
|
585
|
+
"spend": {
|
|
586
|
+
"amount": "0",
|
|
587
|
+
"currency": "usd",
|
|
588
|
+
"period": "month",
|
|
589
|
+
},
|
|
590
|
+
"status": "none",
|
|
591
|
+
}
|
|
592
|
+
`)
|
|
574
593
|
})
|
|
575
594
|
|
|
576
595
|
test('behavior: rejects an unknown environment', async () => {
|
|
@@ -756,7 +775,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
|
|
|
756
775
|
test('checkout provisions distinct customers per environment', { timeout: 30_000 }, async () => {
|
|
757
776
|
const db = TestApp.database()
|
|
758
777
|
const app = createApp({ billing: sandboxBillingBoth(), db })
|
|
759
|
-
const { cookie, orgId } = await
|
|
778
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
760
779
|
|
|
761
780
|
const prod = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
762
781
|
const sbx = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout?environment=sandbox`, post(cookie)) // prettier-ignore
|
|
@@ -783,7 +802,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
|
|
|
783
802
|
test('checkout provisions a real customer and setup session', { timeout: 30_000 }, async () => {
|
|
784
803
|
const db = TestApp.database()
|
|
785
804
|
const app = createApp({ billing: sandboxBilling(), db })
|
|
786
|
-
const { cookie, orgId } = await
|
|
805
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
787
806
|
|
|
788
807
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
789
808
|
// Track for teardown before asserting: a failed assertion must not leak
|
|
@@ -831,7 +850,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
|
|
|
831
850
|
// prettier-ignore
|
|
832
851
|
const db = TestApp.database()
|
|
833
852
|
const app = createApp({ billing: sandboxBilling(), db })
|
|
834
|
-
const { cookie, orgId } = await
|
|
853
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
835
854
|
const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
836
855
|
const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
|
|
837
856
|
if (customerId) TestStripe.track(customerId)
|
|
@@ -844,8 +863,9 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
|
|
|
844
863
|
invoice_settings: { default_payment_method: attached.paymentMethod },
|
|
845
864
|
})
|
|
846
865
|
await StripeCustomers.setStatus(db, orgId, 'active')
|
|
866
|
+
await EnabledBillingSources.remove(db, { orgId, source: 'stripe' })
|
|
847
867
|
|
|
848
|
-
//
|
|
868
|
+
// Disabling setup does not block management of the existing Stripe source.
|
|
849
869
|
const list = await app.request(`/v1/orgs/${orgId}/billing/payment-methods`, {
|
|
850
870
|
headers: { cookie },
|
|
851
871
|
})
|
|
@@ -900,7 +920,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
|
|
|
900
920
|
// prettier-ignore
|
|
901
921
|
const db = TestApp.database()
|
|
902
922
|
const app = createApp({ billing: sandboxBilling(), db })
|
|
903
|
-
const { cookie, orgId } = await
|
|
923
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
904
924
|
const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
905
925
|
const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
|
|
906
926
|
if (customerId) TestStripe.track(customerId)
|
|
@@ -930,11 +950,12 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (direct)', () => {
|
|
|
930
950
|
|
|
931
951
|
const db = TestApp.database()
|
|
932
952
|
const app = createApp({ billing: sandboxBilling(), db })
|
|
933
|
-
const { cookie, orgId } = await
|
|
953
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
934
954
|
const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
935
955
|
const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
|
|
936
956
|
if (customerId) TestStripe.track(customerId)
|
|
937
957
|
expect(checkout.status).toBe(200)
|
|
958
|
+
await EnabledBillingSources.remove(db, { orgId, source: 'stripe' })
|
|
938
959
|
|
|
939
960
|
const response = await app.request(`/v1/orgs/${orgId}/billing/stripe/manage`, post(cookie))
|
|
940
961
|
expect(response.status).toBe(200)
|
|
@@ -963,7 +984,7 @@ describe.runIf(TestStripe.secretKey)('stripe sandbox (live delivery)', () => {
|
|
|
963
984
|
})
|
|
964
985
|
|
|
965
986
|
test('attach → active, detach → none, delete → canceled', { timeout: 300_000 }, async () => {
|
|
966
|
-
const { cookie, orgId } = await
|
|
987
|
+
const { cookie, orgId } = await seededWithStripe(app, db)
|
|
967
988
|
const checkout = await app.request(`/v1/orgs/${orgId}/billing/stripe/checkout`, post(cookie))
|
|
968
989
|
const customerId = (await StripeCustomers.get(db, orgId))?.stripeCustomerId
|
|
969
990
|
if (customerId) TestStripe.track(customerId)
|