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
|
@@ -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, {
|
|
@@ -12,14 +12,30 @@ const origin = 'http://localhost'
|
|
|
12
12
|
/** Super admin secret configured on the test app. */
|
|
13
13
|
const secret = 'tempo:sk:b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1'
|
|
14
14
|
|
|
15
|
-
/** Wildcard-scoped key: full
|
|
15
|
+
/** Wildcard-scoped key: full API access, still not a super admin. */
|
|
16
16
|
const wildcard = {
|
|
17
17
|
id: 'key_wildcard',
|
|
18
|
-
orgId: '
|
|
18
|
+
orgId: 'org_a',
|
|
19
19
|
scopes: ['*'],
|
|
20
20
|
token: 'secret_wildcard_key',
|
|
21
21
|
} satisfies TestApp.kvStore.Key
|
|
22
22
|
|
|
23
|
+
/** Key limited to management reads for Org A. */
|
|
24
|
+
const managementRead = {
|
|
25
|
+
id: 'key_management_read',
|
|
26
|
+
orgId: 'org_a',
|
|
27
|
+
scopes: ['management:read'],
|
|
28
|
+
token: 'secret_management_read_key',
|
|
29
|
+
} satisfies TestApp.kvStore.Key
|
|
30
|
+
|
|
31
|
+
/** Key limited to management writes for Org A. */
|
|
32
|
+
const managementWrite = {
|
|
33
|
+
id: 'key_management_write',
|
|
34
|
+
orgId: 'org_a',
|
|
35
|
+
scopes: ['management:write'],
|
|
36
|
+
token: 'secret_management_write_key',
|
|
37
|
+
} satisfies TestApp.kvStore.Key
|
|
38
|
+
|
|
23
39
|
/** RequestInit presenting the given token. */
|
|
24
40
|
function as(token: string) {
|
|
25
41
|
return { headers: { 'tempo-api-key': token } } as const
|
|
@@ -36,7 +52,10 @@ function json(method: 'PATCH' | 'POST', cookie: string, body: unknown) {
|
|
|
36
52
|
|
|
37
53
|
function createApp(db = TestApp.database()) {
|
|
38
54
|
return TestApp.create({
|
|
39
|
-
auth: {
|
|
55
|
+
auth: {
|
|
56
|
+
keys: [TestApp.key, managementRead, managementWrite, wildcard],
|
|
57
|
+
superAdmin: { secret },
|
|
58
|
+
},
|
|
40
59
|
db,
|
|
41
60
|
session: { wallet: { origin } },
|
|
42
61
|
})
|
|
@@ -81,6 +100,19 @@ describe('GET /orgs', () => {
|
|
|
81
100
|
`)
|
|
82
101
|
})
|
|
83
102
|
|
|
103
|
+
test('accepts a management API key in the query parameter', async () => {
|
|
104
|
+
const app = await seeded()
|
|
105
|
+
const response = await app.request(`/v1/orgs?key=${encodeURIComponent(managementRead.token)}`)
|
|
106
|
+
const body = await TestApp.json(response, Orgs.schema.listOrganizations.Response)
|
|
107
|
+
|
|
108
|
+
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
109
|
+
expect(body.data.map(({ id }) => id)).toMatchInlineSnapshot(`
|
|
110
|
+
[
|
|
111
|
+
"org_a",
|
|
112
|
+
]
|
|
113
|
+
`)
|
|
114
|
+
})
|
|
115
|
+
|
|
84
116
|
test('lists only owned organizations for a session', async () => {
|
|
85
117
|
const app = await seeded()
|
|
86
118
|
const cookie = await session(app)
|
|
@@ -112,24 +144,26 @@ describe('GET /orgs', () => {
|
|
|
112
144
|
`)
|
|
113
145
|
})
|
|
114
146
|
|
|
115
|
-
test('rejects API keys without the
|
|
147
|
+
test('rejects API keys without the management read scope', async () => {
|
|
116
148
|
const response = await (await seeded()).request('/v1/orgs', as(TestApp.key.token))
|
|
117
149
|
expect(response.status).toBe(403)
|
|
118
150
|
})
|
|
119
151
|
|
|
120
|
-
test('
|
|
152
|
+
test('lists only the owning organization for scoped and wildcard keys', async () => {
|
|
153
|
+
const app = await seeded()
|
|
154
|
+
const scoped = await app.request('/v1/orgs', as(managementRead.token))
|
|
155
|
+
expect(scoped.status).toBe(200)
|
|
156
|
+
expect(
|
|
157
|
+
(await TestApp.json(scoped, Orgs.schema.listOrganizations.Response)).data.map(({ id }) => id),
|
|
158
|
+
).toEqual(['org_a'])
|
|
159
|
+
|
|
121
160
|
const response = await (await seeded()).request('/v1/orgs', as(wildcard.token))
|
|
122
|
-
expect(response.status).toBe(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
"code": "forbidden",
|
|
129
|
-
"message": "API key not permitted for this route",
|
|
130
|
-
},
|
|
131
|
-
}
|
|
132
|
-
`)
|
|
161
|
+
expect(response.status).toBe(200)
|
|
162
|
+
expect(
|
|
163
|
+
(await TestApp.json(response, Orgs.schema.listOrganizations.Response)).data.map(
|
|
164
|
+
({ id }) => id,
|
|
165
|
+
),
|
|
166
|
+
).toEqual(['org_a'])
|
|
133
167
|
})
|
|
134
168
|
|
|
135
169
|
test('rejects the secret when super admin is unconfigured', async () => {
|
|
@@ -200,6 +234,13 @@ describe('POST /orgs', () => {
|
|
|
200
234
|
})
|
|
201
235
|
|
|
202
236
|
describe('GET /orgs/:orgId', () => {
|
|
237
|
+
test('binds API keys to their owning organization', async () => {
|
|
238
|
+
const app = await seeded()
|
|
239
|
+
expect((await app.request('/v1/orgs/org_a', as(managementRead.token))).status).toBe(200)
|
|
240
|
+
expect((await app.request('/v1/orgs/org_b', as(managementRead.token))).status).toBe(404)
|
|
241
|
+
expect((await app.request('/v1/orgs/org_a', as(managementWrite.token))).status).toBe(403)
|
|
242
|
+
})
|
|
243
|
+
|
|
203
244
|
test('hides foreign organizations behind 404', async () => {
|
|
204
245
|
const app = createApp()
|
|
205
246
|
const owner = await session(app)
|
|
@@ -219,6 +260,27 @@ describe('GET /orgs/:orgId', () => {
|
|
|
219
260
|
})
|
|
220
261
|
|
|
221
262
|
describe('PATCH /orgs/:orgId', () => {
|
|
263
|
+
test('requires the management write scope', async () => {
|
|
264
|
+
const app = await seeded()
|
|
265
|
+
const write = await app.request(
|
|
266
|
+
`/v1/orgs/org_a?key=${encodeURIComponent(managementWrite.token)}`,
|
|
267
|
+
{
|
|
268
|
+
body: JSON.stringify({ name: 'Managed' }),
|
|
269
|
+
headers: { 'content-type': 'application/json' },
|
|
270
|
+
method: 'PATCH',
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
expect(write.status).toBe(200)
|
|
274
|
+
expect((await TestApp.json(write, Orgs.schema.Organization)).name).toBe('Managed')
|
|
275
|
+
|
|
276
|
+
const readOnly = await app.request('/v1/orgs/org_a', {
|
|
277
|
+
body: JSON.stringify({ name: 'Denied' }),
|
|
278
|
+
headers: { 'content-type': 'application/json', 'tempo-api-key': managementRead.token },
|
|
279
|
+
method: 'PATCH',
|
|
280
|
+
})
|
|
281
|
+
expect(readOnly.status).toBe(403)
|
|
282
|
+
})
|
|
283
|
+
|
|
222
284
|
test('renames an owned organization', async () => {
|
|
223
285
|
const app = createApp()
|
|
224
286
|
const cookie = await session(app)
|
|
@@ -46,7 +46,7 @@ export namespace schema {
|
|
|
46
46
|
role: z
|
|
47
47
|
.optional(z.enum(Memberships.roles))
|
|
48
48
|
.check(
|
|
49
|
-
z.describe("The caller's role
|
|
49
|
+
z.describe("The caller's role; absent for API keys and the super admin."),
|
|
50
50
|
z.meta({ examples: ['owner'] }),
|
|
51
51
|
),
|
|
52
52
|
updatedAt: z
|
|
@@ -104,17 +104,13 @@ export namespace schema {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
* Mounts
|
|
108
|
-
* owned by the user that created it). The super admin bypasses ownership;
|
|
109
|
-
* inaccessible or absent ids render `404` so foreign org ids are
|
|
110
|
-
* indistinguishable from missing ones. Projects nest under `/orgs/:orgId` in
|
|
111
|
-
* the projects app.
|
|
107
|
+
* Mounts organizations. Sessions access memberships, API keys access their owning organization, and the super admin bypasses ownership.
|
|
112
108
|
*/
|
|
113
109
|
export function orgs() {
|
|
114
110
|
return new Hono<App.Environment>()
|
|
115
111
|
.get(
|
|
116
112
|
'/v1/orgs',
|
|
117
|
-
Auth.policy({ session: true }),
|
|
113
|
+
Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
|
|
118
114
|
OpenApi.describeRoute({
|
|
119
115
|
operationId: 'listOrganizations',
|
|
120
116
|
responses: OpenApi.responses({
|
|
@@ -127,18 +123,21 @@ export function orgs() {
|
|
|
127
123
|
tags: ['Organizations'],
|
|
128
124
|
}),
|
|
129
125
|
async (c) => {
|
|
130
|
-
if (Auth.narrowAccess) return Auth.
|
|
131
|
-
|
|
132
|
-
const principal = Auth.getPrincipal(c)
|
|
133
|
-
if (principal?.type !== 'session' && principal?.type !== 'super_admin')
|
|
134
|
-
return sessionRequired(c)
|
|
126
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
135
127
|
|
|
136
128
|
const db = Db.get(c.get('db'))
|
|
137
129
|
try {
|
|
130
|
+
const principal = Auth.getPrincipal(c)
|
|
131
|
+
if (!principal || principal.type === 'public')
|
|
132
|
+
throw new Error('organization auth policy admitted an unsupported principal')
|
|
138
133
|
const records =
|
|
139
134
|
principal.type === 'super_admin'
|
|
140
135
|
? await Organizations.list(db)
|
|
141
|
-
:
|
|
136
|
+
: principal.type === 'api_key'
|
|
137
|
+
? [await Organizations.get(db, principal.orgId)].filter(
|
|
138
|
+
(record): record is Organizations.Record => record !== undefined,
|
|
139
|
+
)
|
|
140
|
+
: await Organizations.listByMember(db, principal.id)
|
|
142
141
|
return c.json(
|
|
143
142
|
Response.validated(schema.listOrganizations.Response, {
|
|
144
143
|
data: records.map(serializeOrganization),
|
|
@@ -203,7 +202,7 @@ export function orgs() {
|
|
|
203
202
|
)
|
|
204
203
|
.get(
|
|
205
204
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}',
|
|
206
|
-
Auth.policy({ session: true }),
|
|
205
|
+
Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
|
|
207
206
|
Auth.ensureOrg(),
|
|
208
207
|
OpenApi.validate('param', schema.Params, {
|
|
209
208
|
code: 'param_invalid',
|
|
@@ -219,7 +218,7 @@ export function orgs() {
|
|
|
219
218
|
tags: ['Organizations'],
|
|
220
219
|
}),
|
|
221
220
|
async (c) => {
|
|
222
|
-
if (Auth.narrowAccess) return Auth.
|
|
221
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
223
222
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
224
223
|
if (OpenApi.narrowValidation)
|
|
225
224
|
return OpenApi.validationError(c, {
|
|
@@ -238,7 +237,7 @@ export function orgs() {
|
|
|
238
237
|
)
|
|
239
238
|
.patch(
|
|
240
239
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}',
|
|
241
|
-
Auth.policy({ session: true }),
|
|
240
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
242
241
|
Auth.ensureOrg({ role: 'owner' }),
|
|
243
242
|
OpenApi.validate('param', schema.Params, {
|
|
244
243
|
code: 'param_invalid',
|
|
@@ -258,7 +257,7 @@ export function orgs() {
|
|
|
258
257
|
tags: ['Organizations'],
|
|
259
258
|
}),
|
|
260
259
|
async (c) => {
|
|
261
|
-
if (Auth.narrowAccess) return Auth.
|
|
260
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
262
261
|
if (OpenApi.narrowValidation)
|
|
263
262
|
return OpenApi.validationError(c, {
|
|
264
263
|
code: 'param_invalid',
|
|
@@ -289,7 +288,7 @@ export function orgs() {
|
|
|
289
288
|
)
|
|
290
289
|
.delete(
|
|
291
290
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}',
|
|
292
|
-
Auth.policy({ session: true }),
|
|
291
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
293
292
|
Auth.ensureOrg({ role: 'owner' }),
|
|
294
293
|
OpenApi.validate('param', schema.Params, {
|
|
295
294
|
code: 'param_invalid',
|
|
@@ -309,7 +308,7 @@ export function orgs() {
|
|
|
309
308
|
tags: ['Organizations'],
|
|
310
309
|
}),
|
|
311
310
|
async (c) => {
|
|
312
|
-
if (Auth.narrowAccess) return Auth.
|
|
311
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
313
312
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
314
313
|
if (OpenApi.narrowValidation)
|
|
315
314
|
return OpenApi.validationError(c, {
|
|
@@ -321,7 +320,9 @@ export function orgs() {
|
|
|
321
320
|
try {
|
|
322
321
|
const kv = c.get('kv')
|
|
323
322
|
if (kv)
|
|
324
|
-
for (const record of await ApiKeys.listByOrg(kv.store, Auth.org(c).id
|
|
323
|
+
for (const record of await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
|
|
324
|
+
scopeCatalog: c.get('scopeCatalog'),
|
|
325
|
+
}))
|
|
325
326
|
await ApiKeys.revoke(kv.store, record.id)
|
|
326
327
|
const deleted = await Organizations.deleteOrganization(db, Auth.org(c).id)
|
|
327
328
|
if (!deleted) return organizationNotFound(c)
|
|
@@ -125,15 +125,13 @@ export namespace schema {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
|
-
* Mounts the
|
|
129
|
-
* owning organization. Access follows the org scope (owning session or super
|
|
130
|
-
* admin); inaccessible or absent ids render `404`.
|
|
128
|
+
* Mounts projects under their owning organization. Access follows the scoped session, API key, or super admin; inaccessible projects return `404`.
|
|
131
129
|
*/
|
|
132
130
|
export function projects() {
|
|
133
131
|
return new Hono<App.Environment>()
|
|
134
132
|
.get(
|
|
135
133
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects',
|
|
136
|
-
Auth.policy({ session: true }),
|
|
134
|
+
Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
|
|
137
135
|
Auth.ensureOrg(),
|
|
138
136
|
OpenApi.validate('param', schema.Params, {
|
|
139
137
|
code: 'param_invalid',
|
|
@@ -152,7 +150,7 @@ export function projects() {
|
|
|
152
150
|
tags: ['Projects'],
|
|
153
151
|
}),
|
|
154
152
|
async (c) => {
|
|
155
|
-
if (Auth.narrowAccess) return Auth.
|
|
153
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
156
154
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
157
155
|
if (OpenApi.narrowValidation)
|
|
158
156
|
return OpenApi.validationError(c, {
|
|
@@ -176,7 +174,7 @@ export function projects() {
|
|
|
176
174
|
)
|
|
177
175
|
.post(
|
|
178
176
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects',
|
|
179
|
-
Auth.policy({ session: true }),
|
|
177
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
180
178
|
Auth.ensureOrg({ role: 'admin' }),
|
|
181
179
|
OpenApi.validate('param', schema.Params, {
|
|
182
180
|
code: 'param_invalid',
|
|
@@ -196,7 +194,7 @@ export function projects() {
|
|
|
196
194
|
tags: ['Projects'],
|
|
197
195
|
}),
|
|
198
196
|
async (c) => {
|
|
199
|
-
if (Auth.narrowAccess) return Auth.
|
|
197
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
200
198
|
if (Auth.narrowScope) return Auth.ensureOrgError(c)
|
|
201
199
|
if (OpenApi.narrowValidation)
|
|
202
200
|
return OpenApi.validationError(c, {
|
|
@@ -221,7 +219,7 @@ export function projects() {
|
|
|
221
219
|
)
|
|
222
220
|
.get(
|
|
223
221
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}',
|
|
224
|
-
Auth.policy({ session: true }),
|
|
222
|
+
Auth.policy({ apiKey: { scopes: ['management:read'] }, session: true }),
|
|
225
223
|
Auth.ensureOrg(),
|
|
226
224
|
Auth.ensureProject(),
|
|
227
225
|
OpenApi.validate('param', schema.ProjectParams, {
|
|
@@ -238,7 +236,7 @@ export function projects() {
|
|
|
238
236
|
tags: ['Projects'],
|
|
239
237
|
}),
|
|
240
238
|
async (c) => {
|
|
241
|
-
if (Auth.narrowAccess) return Auth.
|
|
239
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
242
240
|
if (Auth.narrowScope) return Auth.ensureProjectError(c)
|
|
243
241
|
if (OpenApi.narrowValidation)
|
|
244
242
|
return OpenApi.validationError(c, {
|
|
@@ -251,7 +249,7 @@ export function projects() {
|
|
|
251
249
|
)
|
|
252
250
|
.patch(
|
|
253
251
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}',
|
|
254
|
-
Auth.policy({ session: true }),
|
|
252
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
255
253
|
Auth.ensureOrg({ role: 'admin' }),
|
|
256
254
|
Auth.ensureProject(),
|
|
257
255
|
OpenApi.validate('param', schema.ProjectParams, {
|
|
@@ -272,7 +270,7 @@ export function projects() {
|
|
|
272
270
|
tags: ['Projects'],
|
|
273
271
|
}),
|
|
274
272
|
async (c) => {
|
|
275
|
-
if (Auth.narrowAccess) return Auth.
|
|
273
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
276
274
|
if (OpenApi.narrowValidation)
|
|
277
275
|
return OpenApi.validationError(c, {
|
|
278
276
|
code: 'param_invalid',
|
|
@@ -298,7 +296,7 @@ export function projects() {
|
|
|
298
296
|
)
|
|
299
297
|
.delete(
|
|
300
298
|
'/v1/orgs/:orgId{org_[A-Za-z0-9_-]+}/projects/:projectId{prj_[A-Za-z0-9_-]+}',
|
|
301
|
-
Auth.policy({ session: true }),
|
|
299
|
+
Auth.policy({ apiKey: { scopes: ['management:write'] }, session: true }),
|
|
302
300
|
Auth.ensureOrg({ role: 'admin' }),
|
|
303
301
|
Auth.ensureProject(),
|
|
304
302
|
OpenApi.validate('param', schema.ProjectParams, {
|
|
@@ -318,7 +316,7 @@ export function projects() {
|
|
|
318
316
|
tags: ['Projects'],
|
|
319
317
|
}),
|
|
320
318
|
async (c) => {
|
|
321
|
-
if (Auth.narrowAccess) return Auth.
|
|
319
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
322
320
|
if (Auth.narrowScope) return Auth.ensureProjectError(c)
|
|
323
321
|
if (OpenApi.narrowValidation)
|
|
324
322
|
return OpenApi.validationError(c, {
|
|
@@ -332,6 +330,7 @@ export function projects() {
|
|
|
332
330
|
if (kv)
|
|
333
331
|
for (const record of await ApiKeys.listByOrg(kv.store, Auth.org(c).id, {
|
|
334
332
|
projectId: Auth.project(c).id,
|
|
333
|
+
scopeCatalog: c.get('scopeCatalog'),
|
|
335
334
|
}))
|
|
336
335
|
await ApiKeys.revoke(kv.store, record.id)
|
|
337
336
|
const deleted = await Projects.deleteProject(db, Auth.project(c).id)
|