tapimo 0.4.1 → 0.4.4
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/README.md +1 -1
- package/dist/ApiKey.d.ts +1 -9
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +2 -3
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +53 -27
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +32 -17
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +1990 -1442
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +23 -9
- package/dist/App.js.map +1 -1
- package/dist/Assets.d.ts +10 -0
- package/dist/Assets.d.ts.map +1 -1
- package/dist/Assets.js +4 -0
- package/dist/Assets.js.map +1 -1
- package/dist/Auth.d.ts +2 -1
- package/dist/Auth.d.ts.map +1 -1
- package/dist/Auth.js +2 -1
- package/dist/Auth.js.map +1 -1
- package/dist/Scope.d.ts +40 -15
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +44 -13
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +560 -9
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +38 -6
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +10 -24
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +10 -3
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +833 -0
- package/dist/admin/apps/organizations.d.ts.map +1 -0
- package/dist/admin/apps/organizations.js +733 -0
- package/dist/admin/apps/organizations.js.map +1 -0
- package/dist/admin/apps/scopes.d.ts +8 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +57 -57
- 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 +53 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +10 -0
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +8 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +9 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +14 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/data/App.d.ts +1304 -1304
- package/dist/apps/data/FundingRouteProviders.d.ts +4 -22
- package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingRouteProviders.js +10 -120
- package/dist/apps/data/FundingRouteProviders.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +38 -38
- package/dist/apps/data/routes/addresses.d.ts +24 -24
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/fee-amm.d.ts +20 -20
- package/dist/apps/data/routes/funding.js +7 -7
- 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/tokenlist.d.ts +12 -12
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/management/App.d.ts +808 -267
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +2 -2
- package/dist/apps/management/Billing.d.ts +3 -0
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +7 -2
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +116 -100
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +41 -30
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +182 -50
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +14 -14
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +93 -20
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +12 -15
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/members.d.ts +89 -18
- package/dist/apps/management/routes/members.d.ts.map +1 -1
- package/dist/apps/management/routes/members.js +10 -10
- package/dist/apps/management/routes/members.js.map +1 -1
- package/dist/apps/management/routes/orgs.d.ts +100 -40
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +19 -19
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/projects.d.ts +122 -33
- package/dist/apps/management/routes/projects.d.ts.map +1 -1
- package/dist/apps/management/routes/projects.js +12 -13
- package/dist/apps/management/routes/projects.js.map +1 -1
- package/dist/apps/management/routes/scopes.d.ts +4 -18
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +6 -7
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +63 -19
- package/dist/apps/management/routes/usage.d.ts.map +1 -1
- package/dist/apps/management/routes/usage.js +9 -10
- package/dist/apps/management/routes/usage.js.map +1 -1
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +38 -4
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/relay/App.d.ts +26 -4
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +17 -14
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/cloudflare.d.ts +2 -0
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +4 -0
- package/dist/cloudflare.js.map +1 -1
- package/dist/db/Db.d.ts +5 -3
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +52 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +13 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +16 -1
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/adminAuditLogs.d.ts +27 -0
- package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
- package/dist/db/tables/adminAuditLogs.js +19 -0
- package/dist/db/tables/adminAuditLogs.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +1 -1
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/memberships.d.ts +32 -4
- package/dist/db/tables/memberships.d.ts.map +1 -1
- package/dist/db/tables/memberships.js +31 -0
- package/dist/db/tables/memberships.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +43 -1
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +103 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.d.ts +25 -0
- package/dist/db/tables/projects.d.ts.map +1 -1
- package/dist/db/tables/projects.js +22 -0
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/webhookDeliveries.d.ts +26 -0
- package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
- package/dist/db/tables/webhookDeliveries.js +37 -0
- package/dist/db/tables/webhookDeliveries.js.map +1 -1
- package/dist/internal/Auth.d.ts +46 -40
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +118 -69
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cursor.d.ts +1 -1
- package/dist/internal/Cursor.d.ts.map +1 -1
- package/dist/internal/Cursor.js +4 -0
- package/dist/internal/Cursor.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +2 -0
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/Errors.d.ts +6 -0
- package/dist/internal/Errors.d.ts.map +1 -0
- package/dist/internal/Errors.js +16 -0
- package/dist/internal/Errors.js.map +1 -0
- package/dist/internal/FundingRoutes.d.ts +15 -43
- package/dist/internal/FundingRoutes.d.ts.map +1 -1
- package/dist/internal/FundingRoutes.js +8 -17
- package/dist/internal/FundingRoutes.js.map +1 -1
- package/dist/internal/Log.d.ts +16 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +5 -3
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/Store.d.ts +61 -16
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +72 -17
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Viem.d.ts +1 -1
- package/dist/internal/index.d.ts +27 -0
- package/dist/internal/index.d.ts.map +1 -0
- package/dist/internal/index.js +27 -0
- package/dist/internal/index.js.map +1 -0
- package/package.json +8 -3
- package/src/ApiKey.ts +2 -3
- package/src/ApiKeys.ts +95 -17
- package/src/App.test.ts +104 -6
- package/src/App.ts +29 -9
- package/src/Assets.ts +8 -0
- package/src/Auth.test-d.ts +36 -0
- package/src/Auth.ts +20 -1
- package/src/Client.test-d.ts +17 -17
- package/src/Scope.test.ts +34 -0
- package/src/Scope.ts +53 -17
- package/src/admin/App.test.ts +33 -3
- package/src/admin/App.ts +49 -14
- package/src/admin/apps/api-keys.ts +10 -3
- package/src/admin/apps/organizations.test.ts +455 -0
- package/src/admin/apps/organizations.ts +953 -0
- package/src/admin/apps/scopes.test.ts +10 -0
- package/src/admin/ui/plugin.ts +1 -1
- package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
- package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
- package/src/admin/ui/src/lib/api-keys.ts +36 -19
- package/src/admin/ui/src/lib/config.ts +2 -3
- package/src/admin/ui/src/lib/organizations.test.ts +89 -0
- package/src/admin/ui/src/lib/organizations.ts +262 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -1
- package/src/admin/ui/src/lib/theme.ts +57 -0
- package/src/admin/ui/src/routeTree.gen.ts +47 -4
- package/src/admin/ui/src/routes/__root.tsx +18 -3
- package/src/admin/ui/src/routes/api-keys.tsx +52 -7
- package/src/admin/ui/src/routes/index.tsx +1 -1
- package/src/admin/ui/src/routes/organizations.tsx +158 -0
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
- package/src/admin/ui/src/start.ts +6 -0
- package/src/admin/ui.generated.ts +63 -13
- package/src/analytics/Analytics.ts +25 -0
- package/src/analytics/tables/requestEvents.ts +33 -0
- package/src/apps/composition.test.ts +82 -2
- package/src/apps/data/FundingRouteProviders.test.ts +37 -93
- package/src/apps/data/FundingRouteProviders.ts +14 -161
- package/src/apps/data/routes/funding.test.ts +7 -42
- package/src/apps/data/routes/funding.ts +8 -8
- package/src/apps/data/routes/indexer.test.ts +2 -2
- package/src/apps/data/routes/webhooks.test.ts +2 -2
- package/src/apps/management/App.ts +2 -2
- package/src/apps/management/Billing.ts +10 -2
- package/src/apps/management/routes/api-keys.test.ts +137 -1
- package/src/apps/management/routes/api-keys.ts +63 -38
- package/src/apps/management/routes/billing.ts +14 -14
- package/src/apps/management/routes/invitations.test.ts +102 -1
- package/src/apps/management/routes/invitations.ts +13 -15
- package/src/apps/management/routes/members.ts +10 -10
- package/src/apps/management/routes/orgs.test.ts +78 -16
- package/src/apps/management/routes/orgs.ts +21 -20
- package/src/apps/management/routes/projects.ts +12 -13
- package/src/apps/management/routes/scopes.test.ts +16 -0
- package/src/apps/management/routes/scopes.ts +6 -7
- package/src/apps/management/routes/usage.ts +9 -14
- package/src/apps/mcp.test.ts +72 -7
- package/src/apps/mcp.ts +46 -4
- package/src/apps/relay/App.test.ts +23 -18
- package/src/apps/relay/App.ts +35 -14
- package/src/cloudflare.ts +5 -0
- package/src/db/Db.ts +59 -3
- package/src/db/Schema.ts +20 -1
- package/src/db/tables/adminAuditLogs.ts +47 -0
- package/src/db/tables/invitations.ts +1 -1
- package/src/db/tables/memberships.ts +59 -3
- package/src/db/tables/organizations.ts +148 -1
- package/src/db/tables/projects.ts +47 -0
- package/src/db/tables/webhookDeliveries.ts +83 -0
- package/src/internal/Auth.test.ts +571 -23
- package/src/internal/Auth.ts +166 -94
- package/src/internal/Cursor.test.ts +10 -0
- package/src/internal/Cursor.ts +5 -1
- package/src/internal/EdgeCache.test.ts +11 -0
- package/src/internal/EdgeCache.ts +2 -0
- package/src/internal/FundingRoutes.test.ts +8 -8
- package/src/internal/FundingRoutes.ts +25 -29
- package/src/internal/Log.test.ts +41 -3
- package/src/internal/Log.ts +23 -4
- package/src/internal/Store.test-d.ts +15 -0
- package/src/internal/Store.test.ts +153 -2
- package/src/internal/Store.ts +157 -41
- package/src/internal/index.test.ts +34 -0
- package/src/internal/index.ts +26 -0
package/src/admin/App.ts
CHANGED
|
@@ -4,11 +4,14 @@ import { requestId, type RequestIdVariables } from 'hono/request-id'
|
|
|
4
4
|
|
|
5
5
|
import * as ApiKey from '../ApiKey.js'
|
|
6
6
|
import type * as Assets from '../Assets.js'
|
|
7
|
-
import type * as
|
|
7
|
+
import type * as Analytics from '../analytics/Analytics.js'
|
|
8
|
+
import * as Db from '../db/Db.js'
|
|
9
|
+
import * as AdminAuditLogs from '../db/tables/adminAuditLogs.js'
|
|
8
10
|
import type * as Store from '../internal/Store.js'
|
|
9
11
|
import * as Response from '../internal/Response.js'
|
|
10
12
|
|
|
11
13
|
import { apiKeys } from './apps/api-keys.js'
|
|
14
|
+
import { organizations } from './apps/organizations.js'
|
|
12
15
|
import { scopes } from './apps/scopes.js'
|
|
13
16
|
import { verifiedTokens } from './apps/verified-tokens.js'
|
|
14
17
|
import * as Ui from './ui.generated.js'
|
|
@@ -22,6 +25,8 @@ export type Identity = {
|
|
|
22
25
|
/** Hono environment for the admin app. */
|
|
23
26
|
export type Environment = {
|
|
24
27
|
Variables: RequestIdVariables & {
|
|
28
|
+
/** ClickHouse analytics source used by operational usage reads. */
|
|
29
|
+
analytics: Analytics.Source
|
|
25
30
|
/** Static-asset writer (e.g. the R2 icon bucket), when configured. */
|
|
26
31
|
assets: create.Assets | undefined
|
|
27
32
|
/** Authoritative Postgres store, or a per-request factory. Resolve at the leaf with `Db.get`. */
|
|
@@ -54,6 +59,7 @@ export function create(options: create.Options) {
|
|
|
54
59
|
app.use('*', async (c, next) => {
|
|
55
60
|
// Admin data is per-principal and mutating: never cache.
|
|
56
61
|
c.header('Cache-Control', 'no-store')
|
|
62
|
+
c.set('analytics', options.analytics)
|
|
57
63
|
c.set('assets', options.assets)
|
|
58
64
|
c.set('db', options.db)
|
|
59
65
|
c.set('kv', options.kv)
|
|
@@ -73,17 +79,18 @@ export function create(options: create.Options) {
|
|
|
73
79
|
}
|
|
74
80
|
}
|
|
75
81
|
|
|
76
|
-
// Renders the public SPA shell:
|
|
77
|
-
//
|
|
78
|
-
// `identity` is `null` when logged out). `<` is escaped to keep `<script>`.
|
|
82
|
+
// Renders the public SPA shell: stamps its hydration-safe base and auth
|
|
83
|
+
// placeholders with request values. `<` is escaped to keep the config script.
|
|
79
84
|
const serveShell = (c: Context<Environment>) => {
|
|
80
85
|
const mount = c.req.routePath.replace(/\*$/, '').replace(/\/$/, '')
|
|
81
86
|
const config = JSON.stringify(authConfig(c.get('identity'))).replaceAll('<', '\\u003c')
|
|
82
87
|
return c.html(
|
|
83
|
-
Ui.shell
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
Ui.shell
|
|
89
|
+
.replace(
|
|
90
|
+
'<base data-tempo-base="true" href="/"/>',
|
|
91
|
+
`<base data-tempo-base="true" href="${mount}/"/>`,
|
|
92
|
+
)
|
|
93
|
+
.replace('window.__TEMPO_API_CONFIG__=undefined', `window.__TEMPO_API_CONFIG__=${config}`),
|
|
87
94
|
)
|
|
88
95
|
}
|
|
89
96
|
|
|
@@ -106,20 +113,46 @@ export function create(options: create.Options) {
|
|
|
106
113
|
})
|
|
107
114
|
return next()
|
|
108
115
|
}
|
|
109
|
-
|
|
116
|
+
const protectedPaths = [
|
|
110
117
|
'/config.json',
|
|
111
118
|
'/api-keys',
|
|
112
119
|
'/api-keys/*',
|
|
120
|
+
'/organizations',
|
|
121
|
+
'/organizations/*',
|
|
113
122
|
'/scopes',
|
|
114
123
|
'/scopes/*',
|
|
115
124
|
'/verified-tokens',
|
|
116
125
|
'/verified-tokens/*',
|
|
117
|
-
]
|
|
118
|
-
|
|
126
|
+
] as const
|
|
127
|
+
for (const path of protectedPaths) app.use(path, requireAuth)
|
|
128
|
+
|
|
129
|
+
for (const path of protectedPaths)
|
|
130
|
+
app.use(path, async (c, next) => {
|
|
131
|
+
const identity = c.get('identity')
|
|
132
|
+
if (!identity) return
|
|
133
|
+
const search = new URL(c.req.url).searchParams
|
|
134
|
+
const record = (status: number) =>
|
|
135
|
+
AdminAuditLogs.insert(Db.get(c.get('db')), {
|
|
136
|
+
actor: identity.email,
|
|
137
|
+
method: c.req.method,
|
|
138
|
+
path: c.req.path,
|
|
139
|
+
query: search.size ? search.toString() : undefined,
|
|
140
|
+
requestId: c.get('requestId'),
|
|
141
|
+
status,
|
|
142
|
+
})
|
|
143
|
+
try {
|
|
144
|
+
await next()
|
|
145
|
+
} catch (cause) {
|
|
146
|
+
await record(500)
|
|
147
|
+
throw cause
|
|
148
|
+
}
|
|
149
|
+
await record(c.res.status)
|
|
150
|
+
})
|
|
119
151
|
|
|
120
152
|
const routed = app
|
|
121
153
|
.get('/config.json', (c) => c.json(authConfig(c.get('identity')), 200))
|
|
122
154
|
.route('/api-keys', apiKeys())
|
|
155
|
+
.route('/organizations', organizations())
|
|
123
156
|
.route('/scopes', scopes())
|
|
124
157
|
.route('/verified-tokens', verifiedTokens())
|
|
125
158
|
|
|
@@ -172,15 +205,17 @@ export declare namespace create {
|
|
|
172
205
|
|
|
173
206
|
/** Options for {@link create}. */
|
|
174
207
|
type Options = {
|
|
208
|
+
/** ClickHouse analytics source used by operational usage reads. */
|
|
209
|
+
analytics: Analytics.Source
|
|
175
210
|
/**
|
|
176
211
|
* Static-asset writer for logo uploads (shared with the main API's `assets`
|
|
177
212
|
* loader) to enable `PUT /verified-tokens/:address/logo`; omit for `404`.
|
|
178
213
|
*/
|
|
179
214
|
assets?: Assets | undefined
|
|
180
215
|
/**
|
|
181
|
-
* Authoritative Postgres store shared with the main API
|
|
182
|
-
*
|
|
183
|
-
*
|
|
216
|
+
* Authoritative Postgres store shared with the main API. Pass a long-lived
|
|
217
|
+
* `Db.postgres(...)` singleton on Node, or a factory
|
|
218
|
+
* (`() => Db.postgres(...)`) on Workers.
|
|
184
219
|
*/
|
|
185
220
|
db: Db.Source
|
|
186
221
|
/**
|
|
@@ -5,6 +5,7 @@ import * as ApiKeys from '../../ApiKeys.js'
|
|
|
5
5
|
import * as OpenApi from '../../internal/OpenApi.js'
|
|
6
6
|
import * as Response from '../../internal/Response.js'
|
|
7
7
|
import * as Schema from '../../internal/Schema.js'
|
|
8
|
+
import * as Scope from '../../Scope.js'
|
|
8
9
|
import type * as App from '../App.js'
|
|
9
10
|
|
|
10
11
|
/** Zod schemas owned by the admin API-keys handler. */
|
|
@@ -16,7 +17,11 @@ export namespace schema {
|
|
|
16
17
|
* server sets from the verified identity so it can't be spoofed).
|
|
17
18
|
*/
|
|
18
19
|
export const Body = Schema.describe(
|
|
19
|
-
z.omit(ApiKeys.schema.MintInput, { createdBy: true }),
|
|
20
|
+
z.extend(z.omit(ApiKeys.schema.MintInput, { createdBy: true }), {
|
|
21
|
+
scopes: z
|
|
22
|
+
.readonly(z.array(z.enum([...Scope.catalog.map((entry) => entry.scope), Scope.wildcard])))
|
|
23
|
+
.check(z.describe("Granted scopes. '*' grants all scopes.")),
|
|
24
|
+
}),
|
|
20
25
|
'Fields accepted when minting an API key.',
|
|
21
26
|
)
|
|
22
27
|
|
|
@@ -59,7 +64,7 @@ export namespace schema {
|
|
|
59
64
|
export const Body = Schema.describe(
|
|
60
65
|
z.object({
|
|
61
66
|
orgId: z.optional(z.string()).check(z.describe('New owning organization id (`org_…`).')),
|
|
62
|
-
projectId: z.optional(z.string()).check(z.describe('New
|
|
67
|
+
projectId: z.optional(z.string()).check(z.describe('New attributed project id (`prj_…`).')),
|
|
63
68
|
}),
|
|
64
69
|
'Attribution to apply to an API key.',
|
|
65
70
|
)
|
|
@@ -174,7 +179,9 @@ export function apiKeys() {
|
|
|
174
179
|
}),
|
|
175
180
|
async (c) => {
|
|
176
181
|
const { orgId } = c.req.valid('query')
|
|
177
|
-
const data =
|
|
182
|
+
const data = orgId
|
|
183
|
+
? await ApiKeys.listByOrg(c.get('kv').store, orgId)
|
|
184
|
+
: await ApiKeys.list(c.get('kv').store)
|
|
178
185
|
return c.json(Response.validated(schema.listApiKeys.Response, { data }))
|
|
179
186
|
},
|
|
180
187
|
)
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import type * as Analytics from '../../analytics/Analytics.js'
|
|
2
|
+
import * as ApiKeys from '../../ApiKeys.js'
|
|
3
|
+
import * as BillingSettings from '../../db/tables/billingSettings.js'
|
|
4
|
+
import * as Memberships from '../../db/tables/memberships.js'
|
|
5
|
+
import * as Organizations from '../../db/tables/organizations.js'
|
|
6
|
+
import * as Projects from '../../db/tables/projects.js'
|
|
7
|
+
import * as SponsoredTransactions from '../../db/tables/sponsoredTransactions.js'
|
|
8
|
+
import * as StripeCustomers from '../../db/tables/stripeCustomers.js'
|
|
9
|
+
import * as Users from '../../db/tables/users.js'
|
|
10
|
+
import * as WebhookDeliveries from '../../db/tables/webhookDeliveries.js'
|
|
11
|
+
import * as WebhookSubscriptions from '../../db/tables/webhookSubscriptions.js'
|
|
12
|
+
import * as Viem from '../../internal/Viem.js'
|
|
13
|
+
|
|
14
|
+
import * as TestAdmin from '../../../test/Admin.js'
|
|
15
|
+
import * as TestApp from '../../../test/App.js'
|
|
16
|
+
import * as AdminOrganizations from './organizations.js'
|
|
17
|
+
|
|
18
|
+
describe('GET /organizations', () => {
|
|
19
|
+
test('lists summaries newest first with member and project counts', async () => {
|
|
20
|
+
const { client, db } = TestAdmin.setup()
|
|
21
|
+
const user = await Users.upsertByAddress(db, {
|
|
22
|
+
address: `0x${'11'.repeat(20)}`,
|
|
23
|
+
})
|
|
24
|
+
await Users.setEmail(db, user.id, 'owner@acme.example')
|
|
25
|
+
const organization = await Organizations.createOwned(db, { name: 'Acme', userId: user.id })
|
|
26
|
+
await Projects.create(db, { name: 'Checkout', orgId: organization.id })
|
|
27
|
+
await Projects.create(db, { name: 'Treasury', orgId: organization.id })
|
|
28
|
+
|
|
29
|
+
const response = await client.organizations.$get({ query: {} })
|
|
30
|
+
const body = await TestApp.json(response, AdminOrganizations.schema.listOrganizations.Response)
|
|
31
|
+
|
|
32
|
+
expect(response.status).toBe(200)
|
|
33
|
+
expect(body.nextCursor).toBeNull()
|
|
34
|
+
expect(body.data).toHaveLength(1)
|
|
35
|
+
expect(body.data[0]).toMatchObject({
|
|
36
|
+
id: organization.id,
|
|
37
|
+
memberCount: 1,
|
|
38
|
+
name: 'Acme',
|
|
39
|
+
projectCount: 2,
|
|
40
|
+
userId: user.id,
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('searches organization and member identity fields case-insensitively', async () => {
|
|
45
|
+
const { client, db } = TestAdmin.setup()
|
|
46
|
+
const user = await Users.upsertByAddress(db, {
|
|
47
|
+
address: `0x${'22'.repeat(20)}`,
|
|
48
|
+
})
|
|
49
|
+
await Users.setEmail(db, user.id, 'Owner@Example.com')
|
|
50
|
+
const organization = await Organizations.createOwned(db, {
|
|
51
|
+
name: 'Merchant Operations',
|
|
52
|
+
userId: user.id,
|
|
53
|
+
})
|
|
54
|
+
await Organizations.create(db, { id: 'org_unrelated', name: 'Unrelated' })
|
|
55
|
+
|
|
56
|
+
for (const query of ['merchant', organization.id, user.id, 'owner@']) {
|
|
57
|
+
const response = await client.organizations.$get({ query: { query } })
|
|
58
|
+
const body = await TestApp.json(
|
|
59
|
+
response,
|
|
60
|
+
AdminOrganizations.schema.listOrganizations.Response,
|
|
61
|
+
)
|
|
62
|
+
expect(body.data.map(({ id }) => id)).toEqual([organization.id])
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('treats SQL wildcard characters as literal search text', async () => {
|
|
67
|
+
const { client, db } = TestAdmin.setup()
|
|
68
|
+
const organization = await Organizations.create(db, { name: 'A_B Merchant' })
|
|
69
|
+
await Organizations.create(db, { name: 'AcB Merchant' })
|
|
70
|
+
|
|
71
|
+
const response = await client.organizations.$get({ query: { query: 'a_b' } })
|
|
72
|
+
const body = await TestApp.json(response, AdminOrganizations.schema.listOrganizations.Response)
|
|
73
|
+
|
|
74
|
+
expect(body.data.map(({ id }) => id)).toEqual([organization.id])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('orders distinct creation times newest first', async () => {
|
|
78
|
+
const { client, db } = TestAdmin.setup()
|
|
79
|
+
vi.useFakeTimers()
|
|
80
|
+
try {
|
|
81
|
+
vi.setSystemTime(new Date('2026-01-01T00:00:00.000Z'))
|
|
82
|
+
const older = await Organizations.create(db, { name: 'Older' })
|
|
83
|
+
vi.setSystemTime(new Date('2026-01-02T00:00:00.000Z'))
|
|
84
|
+
const newer = await Organizations.create(db, { name: 'Newer' })
|
|
85
|
+
|
|
86
|
+
const response = await client.organizations.$get({ query: {} })
|
|
87
|
+
const body = await TestApp.json(
|
|
88
|
+
response,
|
|
89
|
+
AdminOrganizations.schema.listOrganizations.Response,
|
|
90
|
+
)
|
|
91
|
+
expect(body.data.map(({ id }) => id)).toEqual([newer.id, older.id])
|
|
92
|
+
} finally {
|
|
93
|
+
vi.useRealTimers()
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('rejects broad queries shorter than three characters', async () => {
|
|
98
|
+
const { client } = TestAdmin.setup()
|
|
99
|
+
const response = await client.organizations.$get({ query: { query: 'a' } })
|
|
100
|
+
expect(response.status).toBe(400)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('walks a stable cursor without duplicate organizations', async () => {
|
|
104
|
+
const { client, db } = TestAdmin.setup()
|
|
105
|
+
for (const id of ['org_f', 'org_e', 'org_d', 'org_c', 'org_b', 'org_a'])
|
|
106
|
+
await Organizations.create(db, { id, name: id })
|
|
107
|
+
|
|
108
|
+
const firstResponse = await client.organizations.$get({ query: { limit: '5' } })
|
|
109
|
+
const first = await TestApp.json(
|
|
110
|
+
firstResponse,
|
|
111
|
+
AdminOrganizations.schema.listOrganizations.Response,
|
|
112
|
+
)
|
|
113
|
+
expect(first.data).toHaveLength(5)
|
|
114
|
+
expect(first.nextCursor).not.toBeNull()
|
|
115
|
+
|
|
116
|
+
const secondResponse = await client.organizations.$get({
|
|
117
|
+
query: { cursor: first.nextCursor!, limit: '5' },
|
|
118
|
+
})
|
|
119
|
+
const second = await TestApp.json(
|
|
120
|
+
secondResponse,
|
|
121
|
+
AdminOrganizations.schema.listOrganizations.Response,
|
|
122
|
+
)
|
|
123
|
+
expect(second.data).toHaveLength(1)
|
|
124
|
+
expect(second.nextCursor).toBeNull()
|
|
125
|
+
expect(new Set([...first.data, ...second.data].map(({ id }) => id)).size).toBe(6)
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
describe('GET /organizations/:orgId', () => {
|
|
130
|
+
test('returns counts and billing state split by environment', async () => {
|
|
131
|
+
const { client, db } = TestAdmin.setup()
|
|
132
|
+
const user = await Users.upsertByAddress(db, {
|
|
133
|
+
address: `0x${'33'.repeat(20)}`,
|
|
134
|
+
})
|
|
135
|
+
await Users.setEmail(db, user.id, 'owner@tempo.xyz')
|
|
136
|
+
const organization = await Organizations.createOwned(db, { name: 'Tempo', userId: user.id })
|
|
137
|
+
await Projects.create(db, { name: 'API', orgId: organization.id })
|
|
138
|
+
await BillingSettings.upsert(db, {
|
|
139
|
+
environment: 'production',
|
|
140
|
+
orgId: organization.id,
|
|
141
|
+
spendLimit: '250',
|
|
142
|
+
txFeeLimit: '0.50',
|
|
143
|
+
})
|
|
144
|
+
await BillingSettings.upsert(db, {
|
|
145
|
+
environment: 'sandbox',
|
|
146
|
+
orgId: organization.id,
|
|
147
|
+
spendLimit: '1000',
|
|
148
|
+
})
|
|
149
|
+
await StripeCustomers.create(db, {
|
|
150
|
+
environment: 'production',
|
|
151
|
+
orgId: organization.id,
|
|
152
|
+
stripeCustomerId: 'cus_admin_lookup',
|
|
153
|
+
})
|
|
154
|
+
await StripeCustomers.setStatus(db, organization.id, 'active', 'production')
|
|
155
|
+
|
|
156
|
+
const response = await client.organizations[':orgId'].$get({
|
|
157
|
+
param: { orgId: organization.id },
|
|
158
|
+
})
|
|
159
|
+
const body = await TestApp.json(response, AdminOrganizations.schema.getOrganization.Response)
|
|
160
|
+
|
|
161
|
+
expect(response.status).toBe(200)
|
|
162
|
+
expect(body.memberCount).toBe(1)
|
|
163
|
+
expect(body.projectCount).toBe(1)
|
|
164
|
+
expect(body.billing.production).toMatchObject({
|
|
165
|
+
settings: { spendLimit: '250', txFeeLimit: '0.50' },
|
|
166
|
+
source: { status: 'active', stripeCustomerId: 'cus_admin_lookup' },
|
|
167
|
+
})
|
|
168
|
+
expect(body.billing.sandbox).toMatchObject({
|
|
169
|
+
settings: { spendLimit: '1000', txFeeLimit: null },
|
|
170
|
+
source: null,
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
test('returns empty counts and billing for a memberless organization', async () => {
|
|
175
|
+
const { client, db } = TestAdmin.setup()
|
|
176
|
+
const organization = await Organizations.create(db, { name: 'Support-created' })
|
|
177
|
+
|
|
178
|
+
const response = await client.organizations[':orgId'].$get({
|
|
179
|
+
param: { orgId: organization.id },
|
|
180
|
+
})
|
|
181
|
+
const body = await TestApp.json(response, AdminOrganizations.schema.getOrganization.Response)
|
|
182
|
+
|
|
183
|
+
expect(body).toMatchObject({
|
|
184
|
+
billing: {
|
|
185
|
+
production: { settings: null, source: null },
|
|
186
|
+
sandbox: { settings: null, source: null },
|
|
187
|
+
},
|
|
188
|
+
memberCount: 0,
|
|
189
|
+
projectCount: 0,
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('returns 404 for an unknown organization', async () => {
|
|
194
|
+
const { client } = TestAdmin.setup()
|
|
195
|
+
const response = await client.organizations[':orgId'].$get({
|
|
196
|
+
param: { orgId: 'org_missing' },
|
|
197
|
+
})
|
|
198
|
+
const body = (await response.json()) as { error: { code: string } }
|
|
199
|
+
|
|
200
|
+
expect(response.status).toBe(404)
|
|
201
|
+
expect(body.error.code).toBe('not_found')
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe('GET /organizations/:orgId/members', () => {
|
|
206
|
+
test('returns a bounded cursor page without duplicates', async () => {
|
|
207
|
+
const { client, db } = TestAdmin.setup()
|
|
208
|
+
const organization = await Organizations.create(db, { name: 'Members' })
|
|
209
|
+
for (let index = 0; index < 6; index++) {
|
|
210
|
+
const user = await Users.upsertByAddress(db, {
|
|
211
|
+
address: `0x${String(index + 1).padStart(40, '0')}`,
|
|
212
|
+
})
|
|
213
|
+
await Users.setEmail(db, user.id, `member-${index}@example.com`)
|
|
214
|
+
await Memberships.create(db, { orgId: organization.id, role: 'member', userId: user.id })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const firstResponse = await client.organizations[':orgId'].members.$get({
|
|
218
|
+
param: { orgId: organization.id },
|
|
219
|
+
query: { limit: '5' },
|
|
220
|
+
})
|
|
221
|
+
const first = await TestApp.json(
|
|
222
|
+
firstResponse,
|
|
223
|
+
AdminOrganizations.schema.listOrganizationMembers.Response,
|
|
224
|
+
)
|
|
225
|
+
const secondResponse = await client.organizations[':orgId'].members.$get({
|
|
226
|
+
param: { orgId: organization.id },
|
|
227
|
+
query: { cursor: first.nextCursor!, limit: '5' },
|
|
228
|
+
})
|
|
229
|
+
const second = await TestApp.json(
|
|
230
|
+
secondResponse,
|
|
231
|
+
AdminOrganizations.schema.listOrganizationMembers.Response,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
expect(first.data).toHaveLength(5)
|
|
235
|
+
expect(second.data).toHaveLength(1)
|
|
236
|
+
expect(new Set([...first.data, ...second.data].map(({ userId }) => userId)).size).toBe(6)
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe('GET /organizations/:orgId/projects', () => {
|
|
241
|
+
test('returns a bounded cursor page without duplicates', async () => {
|
|
242
|
+
const { client, db } = TestAdmin.setup()
|
|
243
|
+
const organization = await Organizations.create(db, { name: 'Projects' })
|
|
244
|
+
for (let index = 0; index < 6; index++)
|
|
245
|
+
await Projects.create(db, { name: `Project ${index}`, orgId: organization.id })
|
|
246
|
+
|
|
247
|
+
const firstResponse = await client.organizations[':orgId'].projects.$get({
|
|
248
|
+
param: { orgId: organization.id },
|
|
249
|
+
query: { limit: '5' },
|
|
250
|
+
})
|
|
251
|
+
const first = await TestApp.json(
|
|
252
|
+
firstResponse,
|
|
253
|
+
AdminOrganizations.schema.listOrganizationProjects.Response,
|
|
254
|
+
)
|
|
255
|
+
const secondResponse = await client.organizations[':orgId'].projects.$get({
|
|
256
|
+
param: { orgId: organization.id },
|
|
257
|
+
query: { cursor: first.nextCursor!, limit: '5' },
|
|
258
|
+
})
|
|
259
|
+
const second = await TestApp.json(
|
|
260
|
+
secondResponse,
|
|
261
|
+
AdminOrganizations.schema.listOrganizationProjects.Response,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
expect(first.data).toHaveLength(5)
|
|
265
|
+
expect(second.data).toHaveLength(1)
|
|
266
|
+
expect(new Set([...first.data, ...second.data].map(({ id }) => id)).size).toBe(6)
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
describe('GET /organizations/:orgId/api-keys', () => {
|
|
271
|
+
test('returns active key environment, scopes, and creator without tokens', async () => {
|
|
272
|
+
const { client, db, store } = TestAdmin.setup()
|
|
273
|
+
const organization = await Organizations.create(db, { name: 'Keys' })
|
|
274
|
+
await ApiKeys.mint(store, {
|
|
275
|
+
createdBy: 'ops@tempo.xyz',
|
|
276
|
+
environment: 'sandbox',
|
|
277
|
+
orgId: organization.id,
|
|
278
|
+
scopes: ['data:read'],
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const response = await client.organizations[':orgId']['api-keys'].$get({
|
|
282
|
+
param: { orgId: organization.id },
|
|
283
|
+
})
|
|
284
|
+
const body = await TestApp.json(
|
|
285
|
+
response,
|
|
286
|
+
AdminOrganizations.schema.listOrganizationApiKeys.Response,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
expect(body.data[0]).toMatchObject({
|
|
290
|
+
createdBy: 'ops@tempo.xyz',
|
|
291
|
+
environment: 'sandbox',
|
|
292
|
+
scopes: ['data:read'],
|
|
293
|
+
})
|
|
294
|
+
expect(body.data[0]).not.toHaveProperty('status')
|
|
295
|
+
expect(body.data[0]).not.toHaveProperty('token')
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
describe('GET /organizations/:orgId/usage', () => {
|
|
300
|
+
test('returns recent totals and error breakdowns from analytics', async () => {
|
|
301
|
+
const { client, db } = TestAdmin.setup({ analytics: usageAnalytics })
|
|
302
|
+
const organization = await Organizations.create(db, { name: 'Usage' })
|
|
303
|
+
const response = await client.organizations[':orgId'].usage.$get({
|
|
304
|
+
param: { orgId: organization.id },
|
|
305
|
+
query: { period: '7d' },
|
|
306
|
+
})
|
|
307
|
+
const body = await TestApp.json(
|
|
308
|
+
response,
|
|
309
|
+
AdminOrganizations.schema.getOrganizationUsage.Response,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
expect(body).toMatchObject({
|
|
313
|
+
byError: [{ code: 'upstream_error', requests: 2 }],
|
|
314
|
+
byRoute: [{ averageDurationMs: 12.5, errors: 2, requests: 20, route: '/v1/tokens' }],
|
|
315
|
+
byStatus: [{ requests: 2, status: 500 }],
|
|
316
|
+
period: '7d',
|
|
317
|
+
totals: { averageDurationMs: 10.5, errors: 2, requests: 25 },
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
describe('GET /organizations/:orgId/sponsorship', () => {
|
|
323
|
+
test('returns current-period committed spend split by environment', async () => {
|
|
324
|
+
const { client, db } = TestAdmin.setup()
|
|
325
|
+
const organization = await Organizations.create(db, { name: 'Sponsorship' })
|
|
326
|
+
await SponsoredTransactions.upsert(db, {
|
|
327
|
+
apiKeyId: 'key_1',
|
|
328
|
+
billable: true,
|
|
329
|
+
chainId: Viem.chainId.mainnet,
|
|
330
|
+
environment: 'production',
|
|
331
|
+
feeMax: '1250000',
|
|
332
|
+
orgId: organization.id,
|
|
333
|
+
projectId: 'prj_1',
|
|
334
|
+
signPayload: `0x${'11'.repeat(32)}`,
|
|
335
|
+
transaction: `0x${'22'.repeat(32)}`,
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
const response = await client.organizations[':orgId'].sponsorship.$get({
|
|
339
|
+
param: { orgId: organization.id },
|
|
340
|
+
})
|
|
341
|
+
const body = await TestApp.json(
|
|
342
|
+
response,
|
|
343
|
+
AdminOrganizations.schema.getOrganizationSponsorship.Response,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
expect(body.production).toMatchObject({
|
|
347
|
+
committedSpend: '1.25',
|
|
348
|
+
currency: 'usd',
|
|
349
|
+
period: 'month',
|
|
350
|
+
})
|
|
351
|
+
expect(body.sandbox).toMatchObject({
|
|
352
|
+
committedSpend: '0',
|
|
353
|
+
currency: 'usd',
|
|
354
|
+
period: 'month',
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
describe('GET /organizations/:orgId/webhooks', () => {
|
|
360
|
+
test('redacts destinations and returns recent delivery failures', async () => {
|
|
361
|
+
const { client, db } = TestAdmin.setup()
|
|
362
|
+
const organization = await Organizations.create(db, { name: 'Webhooks' })
|
|
363
|
+
const createdAt = new Date().toISOString()
|
|
364
|
+
await WebhookSubscriptions.insert(db, {
|
|
365
|
+
chainId: 4217,
|
|
366
|
+
createdAt,
|
|
367
|
+
destination: { type: 'url', url: 'https://hooks.example.com/private/path' },
|
|
368
|
+
eventType: 'token:transfer',
|
|
369
|
+
failureCount: 1,
|
|
370
|
+
filters: {},
|
|
371
|
+
id: 'wh_admin_visibility',
|
|
372
|
+
owner: { orgId: organization.id, type: 'api_key' },
|
|
373
|
+
secret: 'secret',
|
|
374
|
+
status: 'active',
|
|
375
|
+
updatedAt: createdAt,
|
|
376
|
+
})
|
|
377
|
+
for (let index = 0; index < 26; index++)
|
|
378
|
+
await WebhookDeliveries.insert(
|
|
379
|
+
db,
|
|
380
|
+
{
|
|
381
|
+
attempt: 1,
|
|
382
|
+
createdAt,
|
|
383
|
+
envelope: {
|
|
384
|
+
chainId: 4217,
|
|
385
|
+
createdAt,
|
|
386
|
+
data: {},
|
|
387
|
+
id: `evt_admin_visibility_${index}`,
|
|
388
|
+
subscriptionId: 'wh_admin_visibility',
|
|
389
|
+
type: 'token:transfer',
|
|
390
|
+
},
|
|
391
|
+
error: 'HTTP 500',
|
|
392
|
+
eventId: `evt_admin_visibility_${index}`,
|
|
393
|
+
id: `whd_admin_visibility_${index}`,
|
|
394
|
+
requestUrl: 'https://hooks.example.com/private/path',
|
|
395
|
+
responseStatus: 500,
|
|
396
|
+
status: 'failed',
|
|
397
|
+
subscriptionId: 'wh_admin_visibility',
|
|
398
|
+
},
|
|
399
|
+
new Date(Date.now() + 60_000).toISOString(),
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
const oldCreatedAt = new Date(Date.now() - 8 * 24 * 60 * 60 * 1_000).toISOString()
|
|
403
|
+
await WebhookDeliveries.insert(
|
|
404
|
+
db,
|
|
405
|
+
{
|
|
406
|
+
attempt: 1,
|
|
407
|
+
createdAt: oldCreatedAt,
|
|
408
|
+
envelope: {
|
|
409
|
+
chainId: 4217,
|
|
410
|
+
createdAt: oldCreatedAt,
|
|
411
|
+
data: {},
|
|
412
|
+
id: 'evt_admin_visibility_old',
|
|
413
|
+
subscriptionId: 'wh_admin_visibility',
|
|
414
|
+
type: 'token:transfer',
|
|
415
|
+
},
|
|
416
|
+
error: 'HTTP 500',
|
|
417
|
+
eventId: 'evt_admin_visibility_old',
|
|
418
|
+
id: 'whd_admin_visibility_old',
|
|
419
|
+
requestUrl: 'https://hooks.example.com/private/path',
|
|
420
|
+
responseStatus: 500,
|
|
421
|
+
status: 'failed',
|
|
422
|
+
subscriptionId: 'wh_admin_visibility',
|
|
423
|
+
},
|
|
424
|
+
new Date(Date.now() + 60_000).toISOString(),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
const response = await client.organizations[':orgId'].webhooks.$get({
|
|
428
|
+
param: { orgId: organization.id },
|
|
429
|
+
})
|
|
430
|
+
const body = await TestApp.json(
|
|
431
|
+
response,
|
|
432
|
+
AdminOrganizations.schema.getOrganizationWebhooks.Response,
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
expect(body.subscriptions[0]?.destination).toBe('https://hooks.example.com/…')
|
|
436
|
+
expect(body.failureCount).toBe(26)
|
|
437
|
+
expect(body.failures).toHaveLength(25)
|
|
438
|
+
expect(body.failures[0]).toMatchObject({ error: 'HTTP 500', responseStatus: 500 })
|
|
439
|
+
expect(body.failuresTruncated).toBe(true)
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
const usageAnalytics: Analytics.Analytics = {
|
|
444
|
+
async insert() {},
|
|
445
|
+
async migrate() {},
|
|
446
|
+
async query(sql) {
|
|
447
|
+
if (sql.includes('GROUP BY error_code'))
|
|
448
|
+
return [{ code: 'upstream_error', requests: 2 }] as never[]
|
|
449
|
+
if (sql.includes('GROUP BY route'))
|
|
450
|
+
return [{ averageDurationMs: 12.5, errors: 2, requests: 20, route: '/v1/tokens' }] as never[]
|
|
451
|
+
if (sql.includes('GROUP BY status')) return [{ requests: 2, status: 500 }] as never[]
|
|
452
|
+
if (sql.includes('GROUP BY key_id') || sql.includes('GROUP BY toStartOf')) return []
|
|
453
|
+
return [{ averageDurationMs: 10.5, errors: 2, requests: 25 }] as never[]
|
|
454
|
+
},
|
|
455
|
+
}
|