tapimo 0.4.0 → 0.4.3
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 +2451 -1533
- 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/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 +1354 -984
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +20 -3
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingRouteProviders.d.ts +94 -5
- package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingRouteProviders.js +196 -5
- 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/balances.d.ts +45 -45
- 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 +453 -453
- package/dist/apps/data/routes/fee-amm.d.ts +101 -101
- package/dist/apps/data/routes/funding.d.ts +730 -138
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +234 -0
- 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/rpc.d.ts +31 -31
- package/dist/apps/data/routes/verified-tokens.d.ts +81 -81
- package/dist/apps/management/App.d.ts +1156 -615
- 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 +106 -90
- 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 +278 -146
- 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/me.d.ts +13 -13
- 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 +130 -70
- 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 +137 -48
- 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 +14 -28
- 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 +35 -3
- 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/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/handlers/relay.d.ts +13 -10
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +46 -16
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +13 -18
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +78 -58
- 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/FundingRoutes.d.ts +170 -0
- package/dist/internal/FundingRoutes.d.ts.map +1 -1
- package/dist/internal/FundingRoutes.js +68 -6
- 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/package.json +2 -2
- package/src/ApiKey.ts +2 -3
- package/src/ApiKeys.ts +95 -17
- package/src/App.test.ts +44 -2
- 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 +64 -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/App.ts +24 -3
- package/src/apps/data/FundingRouteProviders.test.ts +204 -0
- package/src/apps/data/FundingRouteProviders.ts +294 -9
- package/src/apps/data/routes/funding.test.ts +90 -23
- package/src/apps/data/routes/funding.ts +315 -0
- package/src/apps/data/routes/rpc.test.ts +33 -0
- 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 +25 -1
- package/src/apps/mcp.ts +43 -3
- package/src/apps/relay/App.test.ts +23 -18
- package/src/apps/relay/App.ts +35 -14
- 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/handlers/relay.test-d.ts +8 -0
- package/src/handlers/relay.test.ts +2 -2
- package/src/handlers/relay.ts +62 -22
- package/src/internal/Auth.test.ts +174 -7
- package/src/internal/Auth.ts +90 -62
- 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 +26 -0
- package/src/internal/FundingRoutes.ts +187 -18
- package/src/internal/Log.test.ts +41 -3
- package/src/internal/Log.ts +23 -4
|
@@ -53,11 +53,11 @@ export namespace schema {
|
|
|
53
53
|
z.meta({ examples: ['org_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
|
|
54
54
|
),
|
|
55
55
|
projectId: z.optional(z.string()).check(
|
|
56
|
-
z.describe('
|
|
56
|
+
z.describe('Attributed project id (`prj_…`).'), // prettier-ignore
|
|
57
57
|
z.meta({ examples: ['prj_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
|
|
58
58
|
),
|
|
59
59
|
scopes: z
|
|
60
|
-
.array(z.
|
|
60
|
+
.array(z.string().check(z.minLength(1)))
|
|
61
61
|
.check(
|
|
62
62
|
z.describe(
|
|
63
63
|
"Granted scopes. '*' (grant-all) appears only on keys minted by the super admin.",
|
|
@@ -138,7 +138,15 @@ export namespace schema {
|
|
|
138
138
|
.optional(z.string().check(z.minLength(1), z.maxLength(100)))
|
|
139
139
|
.check(z.describe('Human-readable key name.'), z.meta({ examples: ['CI'] })),
|
|
140
140
|
scopes: z
|
|
141
|
-
._default(
|
|
141
|
+
._default(
|
|
142
|
+
z.array(
|
|
143
|
+
z.string().check(
|
|
144
|
+
z.minLength(1),
|
|
145
|
+
z.refine((scope) => scope !== Scope.wildcard),
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
[],
|
|
149
|
+
)
|
|
142
150
|
.check(z.describe('Scopes to grant.'), z.meta({ examples: [['data:read']] })),
|
|
143
151
|
})
|
|
144
152
|
.check(z.describe('Fields for minting an API key.'))
|
|
@@ -210,11 +218,7 @@ export namespace schema {
|
|
|
210
218
|
}
|
|
211
219
|
|
|
212
220
|
/**
|
|
213
|
-
* Mounts the
|
|
214
|
-
* (project-scoped mint/list/revoke) plus the org-wide read
|
|
215
|
-
* `/orgs/:orgId/api-keys`, over the KV state store. Access follows the
|
|
216
|
-
* org/project scope (owning session or super admin); the token appears once,
|
|
217
|
-
* at mint.
|
|
221
|
+
* Mounts project-attributed API key mint, list, and revoke routes plus the org-wide list. Access follows the caller's organization ownership.
|
|
218
222
|
*/
|
|
219
223
|
export function apiKeys(options: apiKeys.Options = {}) {
|
|
220
224
|
const hidden = !options.enabled
|
|
@@ -233,7 +237,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
233
237
|
message: 'Check the request body and try again.',
|
|
234
238
|
}),
|
|
235
239
|
OpenApi.describeRoute({
|
|
236
|
-
description: 'Mint a project-
|
|
240
|
+
description: 'Mint a project-attributed API key. The token appears once and is never shown again.', // prettier-ignore
|
|
237
241
|
hide: hidden,
|
|
238
242
|
operationId: 'createApiKey',
|
|
239
243
|
responses: OpenApi.responses({
|
|
@@ -273,20 +277,19 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
273
277
|
|
|
274
278
|
const body = c.req.valid('json')
|
|
275
279
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (denied.length > 0)
|
|
283
|
-
return Response.error(c, {
|
|
284
|
-
code: 'scope_not_issuable',
|
|
285
|
-
message: 'The requested scopes are not issuable.',
|
|
286
|
-
status: 403,
|
|
287
|
-
})
|
|
288
|
-
}
|
|
280
|
+
if (!body.scopes.every((scope) => Scope.includes(c.get('scopeCatalog'), scope)))
|
|
281
|
+
return Response.error(c, {
|
|
282
|
+
code: 'body_invalid',
|
|
283
|
+
message: 'Check the request body and try again.',
|
|
284
|
+
status: 400,
|
|
285
|
+
})
|
|
289
286
|
|
|
287
|
+
if (!canIssue(c, body.scopes))
|
|
288
|
+
return Response.error(c, {
|
|
289
|
+
code: 'scope_not_issuable',
|
|
290
|
+
message: 'The requested scopes are not issuable.',
|
|
291
|
+
status: 403,
|
|
292
|
+
})
|
|
290
293
|
try {
|
|
291
294
|
const orgId = Auth.org(c).id
|
|
292
295
|
// Snapshot billing onto the record so the auth path throttles a
|
|
@@ -296,15 +299,19 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
296
299
|
body.environment === 'sandbox'
|
|
297
300
|
? await Billing.active(Db.get(c.get('db')), orgId, 'sandbox')
|
|
298
301
|
: undefined
|
|
299
|
-
const { record, token } = await ApiKeys.mint(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
302
|
+
const { record, token } = await ApiKeys.mint(
|
|
303
|
+
kv.store,
|
|
304
|
+
{
|
|
305
|
+
createdBy,
|
|
306
|
+
environment: body.environment,
|
|
307
|
+
...(billingActive === undefined ? {} : { billingActive }),
|
|
308
|
+
...(body.name === undefined ? {} : { name: body.name }),
|
|
309
|
+
orgId,
|
|
310
|
+
projectId: Auth.project(c).id,
|
|
311
|
+
scopes: body.scopes,
|
|
312
|
+
},
|
|
313
|
+
{ scopeCatalog: c.get('scopeCatalog') },
|
|
314
|
+
)
|
|
308
315
|
return c.json(
|
|
309
316
|
Response.validated(schema.createApiKey.Response, { ...serializeKey(record), token }),
|
|
310
317
|
200,
|
|
@@ -316,7 +323,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
316
323
|
)
|
|
317
324
|
.get(
|
|
318
325
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys',
|
|
319
|
-
Auth.policy({ session: true }),
|
|
326
|
+
Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
|
|
320
327
|
Auth.ensureOrg(),
|
|
321
328
|
Auth.ensureProject(),
|
|
322
329
|
OpenApi.validate('param', schema.Params, {
|
|
@@ -344,7 +351,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
344
351
|
tags: ['API Keys'],
|
|
345
352
|
}),
|
|
346
353
|
async (c) => {
|
|
347
|
-
if (Auth.narrowAccess) return Auth.
|
|
354
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
348
355
|
if (Auth.narrowScope) return Auth.ensureProjectError(c)
|
|
349
356
|
if (OpenApi.narrowValidation)
|
|
350
357
|
return OpenApi.validationError(c, {
|
|
@@ -363,6 +370,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
363
370
|
const records = await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
|
|
364
371
|
environment: c.req.valid('query').environment,
|
|
365
372
|
projectId: Auth.project(c).id,
|
|
373
|
+
scopeCatalog: c.get('scopeCatalog'),
|
|
366
374
|
})
|
|
367
375
|
return c.json(
|
|
368
376
|
Response.validated(schema.listApiKeys.Response, {
|
|
@@ -377,7 +385,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
377
385
|
)
|
|
378
386
|
.get(
|
|
379
387
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/api-keys',
|
|
380
|
-
Auth.policy({ session: true }),
|
|
388
|
+
Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
|
|
381
389
|
Auth.ensureOrg(),
|
|
382
390
|
OpenApi.validate('param', schema.OrgParams, {
|
|
383
391
|
code: 'param_invalid',
|
|
@@ -405,7 +413,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
405
413
|
tags: ['API Keys'],
|
|
406
414
|
}),
|
|
407
415
|
async (c) => {
|
|
408
|
-
if (Auth.narrowAccess) return Auth.
|
|
416
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
409
417
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
410
418
|
if (OpenApi.narrowValidation)
|
|
411
419
|
return OpenApi.validationError(c, {
|
|
@@ -423,6 +431,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
423
431
|
try {
|
|
424
432
|
const records = await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
|
|
425
433
|
environment: c.req.valid('query').environment,
|
|
434
|
+
scopeCatalog: c.get('scopeCatalog'),
|
|
426
435
|
})
|
|
427
436
|
return c.json(
|
|
428
437
|
Response.validated(schema.listOrgApiKeys.Response, {
|
|
@@ -437,7 +446,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
437
446
|
)
|
|
438
447
|
.delete(
|
|
439
448
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}/api-keys/:keyId{key_[A-Za-z0-9_-]+}',
|
|
440
|
-
Auth.policy({ session: true }),
|
|
449
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
441
450
|
Auth.ensureOrg({ role: 'admin' }),
|
|
442
451
|
Auth.ensureProject(),
|
|
443
452
|
OpenApi.validate('param', schema.KeyParams, {
|
|
@@ -459,7 +468,7 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
459
468
|
tags: ['API Keys'],
|
|
460
469
|
}),
|
|
461
470
|
async (c) => {
|
|
462
|
-
if (Auth.narrowAccess) return Auth.
|
|
471
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
463
472
|
if (Auth.narrowScope) return Auth.ensureProjectError(c)
|
|
464
473
|
if (OpenApi.narrowValidation)
|
|
465
474
|
return OpenApi.validationError(c, {
|
|
@@ -473,7 +482,9 @@ export function apiKeys(options: apiKeys.Options = {}) {
|
|
|
473
482
|
try {
|
|
474
483
|
// Bind the key to the scoped project before revoking: a key id from
|
|
475
484
|
// another org or project must be indistinguishable from a missing one.
|
|
476
|
-
const record = await ApiKeys.get(kv.store, keyId
|
|
485
|
+
const record = await ApiKeys.get(kv.store, keyId, {
|
|
486
|
+
scopeCatalog: c.get('scopeCatalog'),
|
|
487
|
+
})
|
|
477
488
|
if (!record || record.orgId !== Auth.org(c).id || record.projectId !== Auth.project(c).id)
|
|
478
489
|
return keyNotFound(c)
|
|
479
490
|
const revoked = await ApiKeys.revoke(kv.store, keyId)
|
|
@@ -518,6 +529,20 @@ function mintedBy(c: Context<App.Environment>) {
|
|
|
518
529
|
return undefined
|
|
519
530
|
}
|
|
520
531
|
|
|
532
|
+
/** Checks whether the caller may grant every requested scope. */
|
|
533
|
+
function canIssue(c: Context<App.Environment>, scopes: readonly Scope.Id[]) {
|
|
534
|
+
const principal = Auth.getPrincipal(c)
|
|
535
|
+
if (principal?.type === 'super_admin') return true
|
|
536
|
+
if (principal?.type === 'session') {
|
|
537
|
+
const selfServe = Scope.selfServeFrom(c.get('scopeCatalog'))
|
|
538
|
+
return (
|
|
539
|
+
scopes.every((scope) => selfServe.includes(scope)) &&
|
|
540
|
+
(!scopes.includes('management:write') || Auth.membership(c)?.role === 'owner')
|
|
541
|
+
)
|
|
542
|
+
}
|
|
543
|
+
return false
|
|
544
|
+
}
|
|
545
|
+
|
|
521
546
|
function keyNotFound(c: Context<App.Environment>) {
|
|
522
547
|
return Response.error(c, {
|
|
523
548
|
code: 'api_key_not_found',
|
|
@@ -234,14 +234,13 @@ export namespace schema {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
/**
|
|
237
|
-
* Mounts
|
|
238
|
-
* a Stripe billing source read as `none`. Session lane; reads for members.
|
|
237
|
+
* Mounts billing routes. Reads require membership or scoped API key access; organizations without Stripe state return `none`.
|
|
239
238
|
*/
|
|
240
239
|
export function billing() {
|
|
241
240
|
return new Hono<Environment>()
|
|
242
241
|
.get(
|
|
243
242
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing',
|
|
244
|
-
Auth.policy({ session: true }),
|
|
243
|
+
Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
|
|
245
244
|
Auth.ensureOrg(),
|
|
246
245
|
OpenApi.validate('param', schema.Params, {
|
|
247
246
|
code: 'param_invalid',
|
|
@@ -264,7 +263,7 @@ export function billing() {
|
|
|
264
263
|
tags: ['Billing'],
|
|
265
264
|
}),
|
|
266
265
|
async (c) => {
|
|
267
|
-
if (Auth.narrowAccess) return Auth.
|
|
266
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
268
267
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
269
268
|
if (OpenApi.narrowValidation)
|
|
270
269
|
return OpenApi.validationError(c, {
|
|
@@ -294,7 +293,7 @@ export function billing() {
|
|
|
294
293
|
)
|
|
295
294
|
.patch(
|
|
296
295
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing',
|
|
297
|
-
Auth.policy({ session: true }),
|
|
296
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
298
297
|
Auth.ensureOrg({ role: 'owner' }),
|
|
299
298
|
OpenApi.validate('param', schema.Params, {
|
|
300
299
|
code: 'param_invalid',
|
|
@@ -325,7 +324,7 @@ export function billing() {
|
|
|
325
324
|
tags: ['Billing'],
|
|
326
325
|
}),
|
|
327
326
|
async (c) => {
|
|
328
|
-
if (Auth.narrowAccess) return Auth.
|
|
327
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
329
328
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
330
329
|
if (OpenApi.narrowValidation)
|
|
331
330
|
return OpenApi.validationError(c, {
|
|
@@ -381,7 +380,7 @@ export function billing() {
|
|
|
381
380
|
)
|
|
382
381
|
.post(
|
|
383
382
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/stripe/checkout',
|
|
384
|
-
Auth.policy({ session: true }),
|
|
383
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
385
384
|
Auth.ensureOrg({ role: 'owner' }),
|
|
386
385
|
OpenApi.validate('param', schema.Params, {
|
|
387
386
|
code: 'param_invalid',
|
|
@@ -408,7 +407,7 @@ export function billing() {
|
|
|
408
407
|
tags: ['Billing'],
|
|
409
408
|
}),
|
|
410
409
|
async (c) => {
|
|
411
|
-
if (Auth.narrowAccess) return Auth.
|
|
410
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
412
411
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
413
412
|
if (OpenApi.narrowValidation)
|
|
414
413
|
return OpenApi.validationError(c, {
|
|
@@ -466,7 +465,7 @@ export function billing() {
|
|
|
466
465
|
)
|
|
467
466
|
.post(
|
|
468
467
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/stripe/manage',
|
|
469
|
-
Auth.policy({ session: true }),
|
|
468
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
470
469
|
Auth.ensureOrg({ role: 'owner' }),
|
|
471
470
|
OpenApi.validate('param', schema.Params, {
|
|
472
471
|
code: 'param_invalid',
|
|
@@ -493,7 +492,7 @@ export function billing() {
|
|
|
493
492
|
tags: ['Billing'],
|
|
494
493
|
}),
|
|
495
494
|
async (c) => {
|
|
496
|
-
if (Auth.narrowAccess) return Auth.
|
|
495
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
497
496
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
498
497
|
if (OpenApi.narrowValidation)
|
|
499
498
|
return OpenApi.validationError(c, {
|
|
@@ -529,7 +528,7 @@ export function billing() {
|
|
|
529
528
|
)
|
|
530
529
|
.get(
|
|
531
530
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/payment-methods',
|
|
532
|
-
Auth.policy({ session: true }),
|
|
531
|
+
Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
|
|
533
532
|
Auth.ensureOrg(),
|
|
534
533
|
OpenApi.validate('param', schema.Params, {
|
|
535
534
|
code: 'param_invalid',
|
|
@@ -555,7 +554,7 @@ export function billing() {
|
|
|
555
554
|
tags: ['Billing'],
|
|
556
555
|
}),
|
|
557
556
|
async (c) => {
|
|
558
|
-
if (Auth.narrowAccess) return Auth.
|
|
557
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
559
558
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
560
559
|
if (OpenApi.narrowValidation)
|
|
561
560
|
return OpenApi.validationError(c, {
|
|
@@ -615,7 +614,7 @@ export function billing() {
|
|
|
615
614
|
)
|
|
616
615
|
.delete(
|
|
617
616
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/billing/payment-methods/:methodId{pm_[A-Za-z0-9_-]+}',
|
|
618
|
-
Auth.policy({ session: true }),
|
|
617
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
619
618
|
Auth.ensureOrg({ role: 'owner' }),
|
|
620
619
|
OpenApi.validate('param', schema.MethodParams, {
|
|
621
620
|
code: 'param_invalid',
|
|
@@ -642,7 +641,7 @@ export function billing() {
|
|
|
642
641
|
tags: ['Billing'],
|
|
643
642
|
}),
|
|
644
643
|
async (c) => {
|
|
645
|
-
if (Auth.narrowAccess) return Auth.
|
|
644
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
646
645
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
647
646
|
if (OpenApi.narrowValidation)
|
|
648
647
|
return OpenApi.validationError(c, {
|
|
@@ -797,6 +796,7 @@ async function syncKeyBilling(
|
|
|
797
796
|
active: options.status === 'active',
|
|
798
797
|
environment: 'sandbox',
|
|
799
798
|
orgId: options.orgId,
|
|
799
|
+
scopeCatalog: c.get('scopeCatalog'),
|
|
800
800
|
})
|
|
801
801
|
}
|
|
802
802
|
|
|
@@ -6,6 +6,7 @@ import * as Invitations from '../../../db/tables/invitations.js'
|
|
|
6
6
|
import * as InvitationsApp from './invitations.js'
|
|
7
7
|
import * as Memberships from '../../../db/tables/memberships.js'
|
|
8
8
|
import * as Orgs from './orgs.js'
|
|
9
|
+
import * as Organizations from '../../../db/tables/organizations.js'
|
|
9
10
|
import * as Users from '../../../db/tables/users.js'
|
|
10
11
|
|
|
11
12
|
/** Origin pinned for SIWE domain binding; Hono test requests use this host. */
|
|
@@ -36,11 +37,26 @@ function sender() {
|
|
|
36
37
|
/** Super admin secret configured on the test app. */
|
|
37
38
|
const secret = 'tempo:sk:d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3'
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
/** Organization management credential used by API-key route tests. */
|
|
41
|
+
const management = {
|
|
42
|
+
id: 'key_management',
|
|
43
|
+
orgId: 'org_management',
|
|
44
|
+
scopes: ['management:read', 'management:write'],
|
|
45
|
+
token: 'secret_management_key',
|
|
46
|
+
} satisfies TestApp.kvStore.Key
|
|
47
|
+
|
|
48
|
+
function createApp(
|
|
49
|
+
options: {
|
|
50
|
+
db?: ReturnType<typeof TestApp.database>
|
|
51
|
+
email?: App.Email
|
|
52
|
+
kv?: { store: ReturnType<typeof TestApp.kvStore> }
|
|
53
|
+
} = {},
|
|
54
|
+
) {
|
|
40
55
|
return TestApp.create({
|
|
41
56
|
auth: { superAdmin: { secret } },
|
|
42
57
|
db: options.db ?? TestApp.database(),
|
|
43
58
|
...(options.email ? { email: options.email } : {}),
|
|
59
|
+
...(options.kv ? { kv: options.kv } : {}),
|
|
44
60
|
session: { wallet: { origin } },
|
|
45
61
|
})
|
|
46
62
|
}
|
|
@@ -55,6 +71,47 @@ async function session(app: ReturnType<typeof createApp>) {
|
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
describe('invitations', () => {
|
|
74
|
+
test('lets management keys manage organization invitations', async () => {
|
|
75
|
+
const db = TestApp.database()
|
|
76
|
+
await Organizations.create(db, { id: management.orgId, name: 'Acme' })
|
|
77
|
+
const app = createApp({
|
|
78
|
+
db,
|
|
79
|
+
kv: { store: TestApp.kvStore({ keys: [management] }) },
|
|
80
|
+
})
|
|
81
|
+
const path = `/v1/orgs/${management.orgId}/invitations`
|
|
82
|
+
const headers = {
|
|
83
|
+
'content-type': 'application/json',
|
|
84
|
+
'tempo-api-key': management.token,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const created = await app.request(path, {
|
|
88
|
+
body: JSON.stringify({ email: 'dev@example.com', role: 'owner' }),
|
|
89
|
+
headers,
|
|
90
|
+
method: 'POST',
|
|
91
|
+
})
|
|
92
|
+
expect(created.status).toBe(200)
|
|
93
|
+
const invitation = await TestApp.json(created, InvitationsApp.schema.Invitation)
|
|
94
|
+
expect(invitation.invitedBy).toBe(management.id)
|
|
95
|
+
|
|
96
|
+
const listed = await app.request(path, { headers })
|
|
97
|
+
expect(listed.status).toBe(200)
|
|
98
|
+
expect(
|
|
99
|
+
(await TestApp.json(listed, InvitationsApp.schema.listInvitations.Response)).data.map(
|
|
100
|
+
({ id }) => id,
|
|
101
|
+
),
|
|
102
|
+
).toEqual([invitation.id])
|
|
103
|
+
|
|
104
|
+
expect((await app.request('/v1/invitations', { headers })).status).toBe(403)
|
|
105
|
+
expect(
|
|
106
|
+
(
|
|
107
|
+
await app.request(`${path}/${invitation.id}`, {
|
|
108
|
+
headers,
|
|
109
|
+
method: 'DELETE',
|
|
110
|
+
})
|
|
111
|
+
).status,
|
|
112
|
+
).toBe(200)
|
|
113
|
+
})
|
|
114
|
+
|
|
58
115
|
test('invites, surfaces, and accepts into membership', async () => {
|
|
59
116
|
const db = TestApp.database()
|
|
60
117
|
const { email, sent } = sender()
|
|
@@ -123,6 +180,50 @@ describe('invitations', () => {
|
|
|
123
180
|
expect(again.status).toBe(404)
|
|
124
181
|
})
|
|
125
182
|
|
|
183
|
+
test('accepting an invitation preserves membership in another organization', async () => {
|
|
184
|
+
const db = TestApp.database()
|
|
185
|
+
const app = createApp({ db })
|
|
186
|
+
const invitee = await session(app)
|
|
187
|
+
const createdA = await app.request('/v1/orgs', json('POST', invitee.cookie, { name: 'Org A' }))
|
|
188
|
+
const orgA = await TestApp.json(createdA, Orgs.schema.Organization)
|
|
189
|
+
await Users.setEmail(db, invitee.userId, 'dev@example.com')
|
|
190
|
+
|
|
191
|
+
const owner = await session(app)
|
|
192
|
+
const createdB = await app.request('/v1/orgs', json('POST', owner.cookie, { name: 'Org B' }))
|
|
193
|
+
const orgB = await TestApp.json(createdB, Orgs.schema.Organization)
|
|
194
|
+
const invited = await app.request(
|
|
195
|
+
`/v1/orgs/${orgB.id}/invitations`,
|
|
196
|
+
json('POST', owner.cookie, { email: 'dev@example.com', role: 'member' }),
|
|
197
|
+
)
|
|
198
|
+
const invitation = await TestApp.json(invited, InvitationsApp.schema.Invitation)
|
|
199
|
+
|
|
200
|
+
const accepted = await app.request(`/v1/invitations/${invitation.id}/accept`, {
|
|
201
|
+
headers: { cookie: invitee.cookie },
|
|
202
|
+
method: 'POST',
|
|
203
|
+
})
|
|
204
|
+
expect(accepted.status).toBe(200)
|
|
205
|
+
|
|
206
|
+
const listed = await app.request('/v1/orgs', { headers: { cookie: invitee.cookie } })
|
|
207
|
+
const organizations = await TestApp.json(listed, Orgs.schema.listOrganizations.Response)
|
|
208
|
+
expect(
|
|
209
|
+
organizations.data
|
|
210
|
+
.map(({ id, role }) => ({ id, role }))
|
|
211
|
+
.sort((a, b) => a.id.localeCompare(b.id)),
|
|
212
|
+
).toEqual(
|
|
213
|
+
[
|
|
214
|
+
{ id: orgA.id, role: 'owner' },
|
|
215
|
+
{ id: orgB.id, role: 'member' },
|
|
216
|
+
].sort((a, b) => a.id.localeCompare(b.id)),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const [readA, readB] = await Promise.all([
|
|
220
|
+
app.request(`/v1/orgs/${orgA.id}`, { headers: { cookie: invitee.cookie } }),
|
|
221
|
+
app.request(`/v1/orgs/${orgB.id}`, { headers: { cookie: invitee.cookie } }),
|
|
222
|
+
])
|
|
223
|
+
expect(readA.status).toBe(200)
|
|
224
|
+
expect(readB.status).toBe(200)
|
|
225
|
+
})
|
|
226
|
+
|
|
126
227
|
test('accepting elevates an existing member but never demotes', async () => {
|
|
127
228
|
const db = TestApp.database()
|
|
128
229
|
const app = createApp({ db })
|
|
@@ -48,7 +48,7 @@ export namespace schema {
|
|
|
48
48
|
invitedBy: z
|
|
49
49
|
.string()
|
|
50
50
|
.check(
|
|
51
|
-
z.describe('User id
|
|
51
|
+
z.describe('User or API key id that created the invitation.'),
|
|
52
52
|
z.meta({ examples: ['usr_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
|
|
53
53
|
),
|
|
54
54
|
orgId: z
|
|
@@ -204,16 +204,14 @@ export namespace schema {
|
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
206
|
* Mounts the invitations surface: org-scoped management
|
|
207
|
-
* (`/orgs/:orgId/invitations
|
|
208
|
-
*
|
|
209
|
-
* emails dispatch through the sender the management group publishes on context
|
|
210
|
-
* (`c.get('email')`), best-effort.
|
|
207
|
+
* (`/orgs/:orgId/invitations`) plus session-only invitee actions. Invitation
|
|
208
|
+
* emails dispatch through the management group's sender, best-effort.
|
|
211
209
|
*/
|
|
212
210
|
export function invitations() {
|
|
213
211
|
return new Hono<Environment>()
|
|
214
212
|
.post(
|
|
215
213
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/invitations',
|
|
216
|
-
Auth.policy({ session: true }),
|
|
214
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
217
215
|
Auth.ensureOrg({ role: 'admin' }),
|
|
218
216
|
OpenApi.validate('param', schema.Params, {
|
|
219
217
|
code: 'param_invalid',
|
|
@@ -234,7 +232,7 @@ export function invitations() {
|
|
|
234
232
|
tags: ['Invitations'],
|
|
235
233
|
}),
|
|
236
234
|
async (c) => {
|
|
237
|
-
if (Auth.narrowAccess) return Auth.
|
|
235
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
238
236
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
239
237
|
if (OpenApi.narrowValidation)
|
|
240
238
|
return OpenApi.validationError(c, {
|
|
@@ -248,9 +246,7 @@ export function invitations() {
|
|
|
248
246
|
})
|
|
249
247
|
|
|
250
248
|
const body = c.req.valid('json')
|
|
251
|
-
// Owners
|
|
252
|
-
// their own (an admin invites members, never peers). The super admin
|
|
253
|
-
// has no membership and may grant any role.
|
|
249
|
+
// Owners, write-scoped keys, and the super admin grant any role; admins grant only members.
|
|
254
250
|
const caller = Auth.membership(c)
|
|
255
251
|
if (
|
|
256
252
|
caller &&
|
|
@@ -262,7 +258,9 @@ export function invitations() {
|
|
|
262
258
|
message: 'Cannot invite at or above your own role',
|
|
263
259
|
status: 403,
|
|
264
260
|
})
|
|
265
|
-
const
|
|
261
|
+
const principal = Auth.getPrincipal(c)
|
|
262
|
+
const invitedBy =
|
|
263
|
+
principal?.type === 'api_key' ? principal.id : (caller?.userId ?? 'super_admin')
|
|
266
264
|
|
|
267
265
|
const db = Db.get(c.get('db'))
|
|
268
266
|
try {
|
|
@@ -282,7 +280,7 @@ export function invitations() {
|
|
|
282
280
|
)
|
|
283
281
|
.get(
|
|
284
282
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/invitations',
|
|
285
|
-
Auth.policy({ session: true }),
|
|
283
|
+
Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
|
|
286
284
|
Auth.ensureOrg({ role: 'admin' }),
|
|
287
285
|
OpenApi.validate('param', schema.Params, {
|
|
288
286
|
code: 'param_invalid',
|
|
@@ -301,7 +299,7 @@ export function invitations() {
|
|
|
301
299
|
tags: ['Invitations'],
|
|
302
300
|
}),
|
|
303
301
|
async (c) => {
|
|
304
|
-
if (Auth.narrowAccess) return Auth.
|
|
302
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
305
303
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
306
304
|
if (OpenApi.narrowValidation)
|
|
307
305
|
return OpenApi.validationError(c, {
|
|
@@ -325,7 +323,7 @@ export function invitations() {
|
|
|
325
323
|
)
|
|
326
324
|
.delete(
|
|
327
325
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/invitations/:invitationId{inv_[A-Za-z0-9_-]+}',
|
|
328
|
-
Auth.policy({ session: true }),
|
|
326
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
329
327
|
Auth.ensureOrg({ role: 'admin' }),
|
|
330
328
|
OpenApi.validate('param', schema.InvitationParams, {
|
|
331
329
|
code: 'param_invalid',
|
|
@@ -344,7 +342,7 @@ export function invitations() {
|
|
|
344
342
|
tags: ['Invitations'],
|
|
345
343
|
}),
|
|
346
344
|
async (c) => {
|
|
347
|
-
if (Auth.narrowAccess) return Auth.
|
|
345
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
348
346
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
349
347
|
if (OpenApi.narrowValidation)
|
|
350
348
|
return OpenApi.validationError(c, {
|
|
@@ -113,14 +113,14 @@ export namespace schema {
|
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
115
|
* Mounts the `/orgs/:orgId/members` resource: the organization's team.
|
|
116
|
-
* Members read
|
|
117
|
-
*
|
|
116
|
+
* Members and read-scoped keys list. Owners and write-scoped keys manage;
|
|
117
|
+
* members may remove themselves. The last owner remains protected.
|
|
118
118
|
*/
|
|
119
119
|
export function members() {
|
|
120
120
|
return new Hono<App.Environment>()
|
|
121
121
|
.get(
|
|
122
122
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/members',
|
|
123
|
-
Auth.policy({ session: true }),
|
|
123
|
+
Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
|
|
124
124
|
Auth.ensureOrg(),
|
|
125
125
|
OpenApi.validate('param', schema.Params, {
|
|
126
126
|
code: 'param_invalid',
|
|
@@ -139,7 +139,7 @@ export function members() {
|
|
|
139
139
|
tags: ['Members'],
|
|
140
140
|
}),
|
|
141
141
|
async (c) => {
|
|
142
|
-
if (Auth.narrowAccess) return Auth.
|
|
142
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
143
143
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
144
144
|
if (OpenApi.narrowValidation)
|
|
145
145
|
return OpenApi.validationError(c, {
|
|
@@ -163,7 +163,7 @@ export function members() {
|
|
|
163
163
|
)
|
|
164
164
|
.patch(
|
|
165
165
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/members/:userId{usr_[A-Za-z0-9_-]+}',
|
|
166
|
-
Auth.policy({ session: true }),
|
|
166
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
167
167
|
Auth.ensureOrg({ role: 'owner' }),
|
|
168
168
|
OpenApi.validate('param', schema.MemberParams, {
|
|
169
169
|
code: 'param_invalid',
|
|
@@ -190,7 +190,7 @@ export function members() {
|
|
|
190
190
|
tags: ['Members'],
|
|
191
191
|
}),
|
|
192
192
|
async (c) => {
|
|
193
|
-
if (Auth.narrowAccess) return Auth.
|
|
193
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
194
194
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
195
195
|
if (OpenApi.narrowValidation)
|
|
196
196
|
return OpenApi.validationError(c, {
|
|
@@ -226,14 +226,14 @@ export function members() {
|
|
|
226
226
|
)
|
|
227
227
|
.delete(
|
|
228
228
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/members/:userId{usr_[A-Za-z0-9_-]+}',
|
|
229
|
-
Auth.policy({ session: true }),
|
|
229
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
230
230
|
Auth.ensureOrg(),
|
|
231
231
|
OpenApi.validate('param', schema.MemberParams, {
|
|
232
232
|
code: 'param_invalid',
|
|
233
233
|
message: 'Check the path parameters and try again.',
|
|
234
234
|
}),
|
|
235
235
|
OpenApi.describeRoute({
|
|
236
|
-
description: 'Remove a member. Owners remove anyone;
|
|
236
|
+
description: 'Remove a member. Owners and management-write keys remove anyone; members remove themselves. The last owner stays.', // prettier-ignore
|
|
237
237
|
operationId: 'removeMember',
|
|
238
238
|
responses: OpenApi.responses({
|
|
239
239
|
errors: {
|
|
@@ -252,7 +252,7 @@ export function members() {
|
|
|
252
252
|
tags: ['Members'],
|
|
253
253
|
}),
|
|
254
254
|
async (c) => {
|
|
255
|
-
if (Auth.narrowAccess) return Auth.
|
|
255
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
256
256
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
257
257
|
if (OpenApi.narrowValidation)
|
|
258
258
|
return OpenApi.validationError(c, {
|
|
@@ -261,7 +261,7 @@ export function members() {
|
|
|
261
261
|
})
|
|
262
262
|
|
|
263
263
|
const { userId } = c.req.valid('param')
|
|
264
|
-
// Owners remove anyone;
|
|
264
|
+
// Owners and write-scoped keys remove anyone; other members only themselves.
|
|
265
265
|
const caller = Auth.membership(c)
|
|
266
266
|
if (caller && caller.role !== 'owner' && caller.userId !== userId)
|
|
267
267
|
return Response.error(c, {
|