tapimo 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -3
- package/dist/ApiKey.d.ts +1 -0
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +5 -2
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +28 -1
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +34 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +7167 -4507
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +8 -0
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +11 -2
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +14 -3
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +128 -118
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +7 -0
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/scopes.d.ts +8 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/apps/scopes.js +3 -0
- package/dist/admin/apps/scopes.js.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.js +13 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +151 -0
- package/dist/analytics/Analytics.d.ts.map +1 -0
- package/dist/analytics/Analytics.js +212 -0
- package/dist/analytics/Analytics.js.map +1 -0
- package/dist/analytics/tables/requestEvents.d.ts +215 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -0
- package/dist/analytics/tables/requestEvents.js +264 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -0
- package/dist/apps/data/App.d.ts +1024 -624
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +36 -0
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +45 -45
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +455 -455
- package/dist/apps/data/routes/fee-amm.d.ts +101 -101
- package/dist/apps/data/routes/funding.d.ts +765 -0
- package/dist/apps/data/routes/funding.d.ts.map +1 -0
- package/dist/apps/data/routes/funding.js +383 -0
- package/dist/apps/data/routes/funding.js.map +1 -0
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +2 -8
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/rpc.d.ts +29 -36
- package/dist/apps/data/routes/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +29 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/transfers.d.ts +56 -56
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +45 -45
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +8 -8
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/docs/App.d.ts +4 -2
- package/dist/apps/docs/App.d.ts.map +1 -1
- package/dist/apps/docs/App.js +27 -95
- package/dist/apps/docs/App.js.map +1 -1
- package/dist/apps/docs/reference.d.ts +21 -0
- package/dist/apps/docs/reference.d.ts.map +1 -0
- package/dist/apps/docs/reference.js +112 -0
- package/dist/apps/docs/reference.js.map +1 -0
- package/dist/apps/index.d.ts +1 -0
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +1 -0
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/mcp.d.ts +76 -0
- package/dist/apps/mcp.d.ts.map +1 -0
- package/dist/apps/mcp.js +188 -0
- package/dist/apps/mcp.js.map +1 -0
- package/dist/apps/platform/App.d.ts +3005 -728
- package/dist/apps/platform/App.d.ts.map +1 -1
- package/dist/apps/platform/App.js +13 -3
- package/dist/apps/platform/App.js.map +1 -1
- package/dist/apps/platform/Billing.d.ts +279 -0
- package/dist/apps/platform/Billing.d.ts.map +1 -0
- package/dist/apps/platform/Billing.js +477 -0
- package/dist/apps/platform/Billing.js.map +1 -0
- package/dist/apps/platform/index.d.ts +1 -0
- package/dist/apps/platform/index.d.ts.map +1 -1
- package/dist/apps/platform/index.js +1 -0
- package/dist/apps/platform/index.js.map +1 -1
- package/dist/apps/platform/routes/api-keys.d.ts +341 -49
- package/dist/apps/platform/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/platform/routes/api-keys.js +115 -13
- package/dist/apps/platform/routes/api-keys.js.map +1 -1
- package/dist/apps/platform/routes/billing.d.ts +1520 -0
- package/dist/apps/platform/routes/billing.d.ts.map +1 -0
- package/dist/apps/platform/routes/billing.js +795 -0
- package/dist/apps/platform/routes/billing.js.map +1 -0
- package/dist/apps/platform/routes/invitations.d.ts +201 -83
- package/dist/apps/platform/routes/invitations.d.ts.map +1 -1
- package/dist/apps/platform/routes/invitations.js +58 -0
- package/dist/apps/platform/routes/invitations.js.map +1 -1
- package/dist/apps/platform/routes/me.d.ts +17 -17
- package/dist/apps/platform/routes/orgs.d.ts +50 -36
- package/dist/apps/platform/routes/orgs.d.ts.map +1 -1
- package/dist/apps/platform/routes/orgs.js +14 -3
- package/dist/apps/platform/routes/orgs.js.map +1 -1
- package/dist/apps/platform/routes/projects.d.ts +97 -97
- package/dist/apps/platform/routes/scopes.d.ts +107 -0
- package/dist/apps/platform/routes/scopes.d.ts.map +1 -0
- package/dist/apps/platform/routes/scopes.js +66 -0
- package/dist/apps/platform/routes/scopes.js.map +1 -0
- package/dist/apps/platform/routes/usage.d.ts +588 -0
- package/dist/apps/platform/routes/usage.d.ts.map +1 -0
- package/dist/apps/platform/routes/usage.js +391 -0
- package/dist/apps/platform/routes/usage.js.map +1 -0
- package/dist/apps/relay/App.d.ts +4 -1
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +118 -10
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/apps/relay/Sponsorships.d.ts.map +1 -1
- package/dist/apps/relay/Sponsorships.js +2 -1
- package/dist/apps/relay/Sponsorships.js.map +1 -1
- package/dist/cli/Cli.d.ts +8 -1
- package/dist/cli/Cli.d.ts.map +1 -1
- package/dist/cli/Cli.js +11 -3
- package/dist/cli/Cli.js.map +1 -1
- package/dist/cli/commands/admin.d.ts +4 -0
- package/dist/cli/commands/admin.d.ts.map +1 -1
- package/dist/cli/commands/admin.js +80 -1
- package/dist/cli/commands/admin.js.map +1 -1
- package/dist/db/Db.d.ts +7 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +126 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +79 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +88 -2
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/billingSettings.d.ts +55 -0
- package/dist/db/tables/billingSettings.d.ts.map +1 -0
- package/dist/db/tables/billingSettings.js +70 -0
- package/dist/db/tables/billingSettings.js.map +1 -0
- package/dist/db/tables/invitations.js +2 -2
- package/dist/db/tables/invitations.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +6 -2
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +10 -4
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.js +2 -2
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/requestUsage.d.ts +121 -0
- package/dist/db/tables/requestUsage.d.ts.map +1 -0
- package/dist/db/tables/requestUsage.js +150 -0
- package/dist/db/tables/requestUsage.js.map +1 -0
- package/dist/db/tables/sponsoredTransactions.d.ts +119 -0
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +155 -2
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/stripeCustomers.d.ts +81 -0
- package/dist/db/tables/stripeCustomers.d.ts.map +1 -0
- package/dist/db/tables/stripeCustomers.js +120 -0
- package/dist/db/tables/stripeCustomers.js.map +1 -0
- package/dist/db/tables/users.js +2 -2
- package/dist/db/tables/users.js.map +1 -1
- package/dist/handlers/internal/sponsorship.d.ts +5 -2
- package/dist/handlers/internal/sponsorship.d.ts.map +1 -1
- package/dist/handlers/internal/sponsorship.js +19 -5
- package/dist/handlers/internal/sponsorship.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts +10 -3
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +14 -6
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/Auth.d.ts +18 -4
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +148 -76
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.d.ts.map +1 -1
- package/dist/internal/Cache.js +4 -1
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +10 -0
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +39 -8
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/Fees.d.ts +27 -0
- package/dist/internal/Fees.d.ts.map +1 -0
- package/dist/internal/Fees.js +48 -0
- package/dist/internal/Fees.js.map +1 -0
- package/dist/internal/FundingRoutes.d.ts +183 -0
- package/dist/internal/FundingRoutes.d.ts.map +1 -0
- package/dist/internal/FundingRoutes.js +490 -0
- package/dist/internal/FundingRoutes.js.map +1 -0
- package/dist/internal/Id.d.ts +15 -0
- package/dist/internal/Id.d.ts.map +1 -0
- package/dist/internal/Id.js +22 -0
- package/dist/internal/Id.js.map +1 -0
- package/dist/internal/Log.d.ts +2 -0
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +3 -0
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +10 -1
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +8 -1
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +25 -0
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +51 -0
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Response.d.ts +1 -1
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Schema.d.ts +8 -0
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +7 -0
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +3 -3
- package/dist/internal/Webhooks.js.map +1 -1
- package/package.json +4 -3
- package/src/ApiKey.ts +9 -2
- package/src/ApiKeys.test.ts +51 -0
- package/src/ApiKeys.ts +50 -1
- package/src/App.test.ts +33 -86718
- package/src/App.ts +18 -0
- package/src/Client.test-d.ts +351 -0
- package/src/Scope.ts +16 -3
- package/src/admin/apps/scopes.test.ts +6 -0
- package/src/admin/apps/scopes.ts +3 -0
- package/src/admin/ui/src/components/Page.tsx +2 -3
- package/src/admin/ui/src/routes/__root.tsx +2 -2
- package/src/admin/ui/src/routes/api-keys.tsx +2 -2
- package/src/admin/ui.generated.ts +13 -13
- package/src/analytics/Analytics.test.ts +527 -0
- package/src/analytics/Analytics.ts +318 -0
- package/src/analytics/tables/requestEvents.ts +523 -0
- package/src/apps/data/App.test.ts +72 -0
- package/src/apps/data/App.ts +35 -0
- package/src/apps/data/routes/funding.test.ts +214 -0
- package/src/apps/data/routes/funding.ts +584 -0
- package/src/apps/data/routes/receipts.ts +2 -9
- package/src/apps/data/routes/rpc.test.ts +78 -2
- package/src/apps/data/routes/rpc.ts +31 -1
- package/src/apps/data/routes/webhooks.test.ts +1 -1
- package/src/apps/data/routes/webhooks.ts +11 -8
- package/src/apps/docs/App.ts +29 -100
- package/src/apps/docs/reference.ts +132 -0
- package/src/apps/index.ts +1 -0
- package/src/apps/mcp.test.ts +206 -0
- package/src/apps/mcp.ts +250 -0
- package/src/apps/platform/App.ts +43 -4
- package/src/apps/platform/Billing.test.ts +530 -0
- package/src/apps/platform/Billing.ts +682 -0
- package/src/apps/platform/index.ts +1 -0
- package/src/apps/platform/routes/api-keys.test.ts +172 -9
- package/src/apps/platform/routes/api-keys.ts +136 -20
- package/src/apps/platform/routes/billing.test.ts +1100 -0
- package/src/apps/platform/routes/billing.ts +966 -0
- package/src/apps/platform/routes/invitations.test.ts +55 -7
- package/src/apps/platform/routes/invitations.ts +68 -0
- package/src/apps/platform/routes/me.test.ts +230 -0
- package/src/apps/platform/routes/orgs.ts +26 -4
- package/src/apps/platform/routes/scopes.test.ts +29 -0
- package/src/apps/platform/routes/scopes.ts +85 -0
- package/src/apps/platform/routes/usage.test.ts +422 -0
- package/src/apps/platform/routes/usage.ts +527 -0
- package/src/apps/relay/App.test.ts +457 -8
- package/src/apps/relay/App.ts +120 -12
- package/src/apps/relay/Sponsorships.test.ts +1 -1
- package/src/apps/relay/Sponsorships.ts +2 -1
- package/src/cli/Cli.test.ts +25 -2
- package/src/cli/Cli.ts +17 -2
- package/src/cli/commands/admin.test.ts +83 -0
- package/src/cli/commands/admin.ts +89 -1
- package/src/db/Db.ts +137 -0
- package/src/db/Schema.ts +104 -2
- package/src/db/tables/billingSettings.test.ts +87 -0
- package/src/db/tables/billingSettings.ts +108 -0
- package/src/db/tables/invitations.ts +2 -2
- package/src/db/tables/organizations.test.ts +1 -1
- package/src/db/tables/organizations.ts +15 -5
- package/src/db/tables/projects.ts +2 -2
- package/src/db/tables/requestUsage.ts +231 -0
- package/src/db/tables/sponsoredTransactions.test.ts +108 -1
- package/src/db/tables/sponsoredTransactions.ts +230 -2
- package/src/db/tables/stripeCustomers.test.ts +109 -0
- package/src/db/tables/stripeCustomers.ts +162 -0
- package/src/db/tables/users.ts +2 -2
- package/src/handlers/internal/sponsorship.ts +34 -7
- package/src/handlers/internal/utils.ts +19 -7
- package/src/index.ts +1 -0
- package/src/internal/Auth.test.ts +175 -25
- package/src/internal/Auth.ts +175 -86
- package/src/internal/Cache.ts +5 -1
- package/src/internal/EdgeCache.test.ts +27 -4
- package/src/internal/EdgeCache.ts +47 -9
- package/src/internal/Fees.test.ts +32 -0
- package/src/internal/Fees.ts +51 -0
- package/src/internal/FundingRoutes.ts +583 -0
- package/src/internal/Id.test.ts +17 -0
- package/src/internal/Id.ts +24 -0
- package/src/internal/Log.test.ts +1 -0
- package/src/internal/Log.ts +5 -0
- package/src/internal/OpenApi.ts +12 -1
- package/src/internal/RateLimit.test.ts +84 -0
- package/src/internal/RateLimit.ts +70 -0
- package/src/internal/Response.ts +1 -1
- package/src/internal/Schema.ts +8 -0
- package/src/internal/Webhooks.test.ts +3 -3
- package/src/internal/Webhooks.ts +3 -3
|
@@ -76,13 +76,13 @@ describe('invitations', () => {
|
|
|
76
76
|
expect(sent).toHaveLength(1)
|
|
77
77
|
expect(sent[0]!.to).toBe('dev@example.com')
|
|
78
78
|
expect(sent[0]!.subject).toContain('Acme')
|
|
79
|
-
expect(sent[0]!.text).
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Sign in at https://console.tempo.xyz/?email=dev%40example.com to accept. This invitation expires in 7 days."
|
|
83
|
-
`)
|
|
79
|
+
expect(sent[0]!.text).toBe(
|
|
80
|
+
`You've been invited to join Acme on the Tempo Developer Platform\n\nSign in at https://console.tempo.xyz/?email=dev%40example.com&invitation=${invitation.id} to accept. This invitation expires in 7 days.`,
|
|
81
|
+
)
|
|
84
82
|
expect(sent[0]!.html).toContain('<strong>Acme</strong>')
|
|
85
|
-
expect(sent[0]!.html).toContain(
|
|
83
|
+
expect(sent[0]!.html).toContain(
|
|
84
|
+
`https://console.tempo.xyz/?email=dev%40example.com&invitation=${invitation.id}`,
|
|
85
|
+
)
|
|
86
86
|
|
|
87
87
|
// Invisible without a verified email; visible once it matches.
|
|
88
88
|
const invitee = await session(app)
|
|
@@ -260,6 +260,46 @@ describe('invitations', () => {
|
|
|
260
260
|
expect(acceptExpired.status).toBe(404)
|
|
261
261
|
})
|
|
262
262
|
|
|
263
|
+
test('declining settles the invitation', async () => {
|
|
264
|
+
const db = TestApp.database()
|
|
265
|
+
const app = createApp({ db })
|
|
266
|
+
const owner = await session(app)
|
|
267
|
+
const created = await app.request('/v1/orgs', json('POST', owner.cookie, { name: 'Acme' }))
|
|
268
|
+
const org = await TestApp.json(created, Orgs.schema.Organization)
|
|
269
|
+
const invited = await app.request(
|
|
270
|
+
`/v1/orgs/${org.id}/invitations`,
|
|
271
|
+
json('POST', owner.cookie, { email: 'dev@example.com', role: 'member' }),
|
|
272
|
+
)
|
|
273
|
+
const invitation = await TestApp.json(invited, InvitationsApp.schema.Invitation)
|
|
274
|
+
|
|
275
|
+
const invitee = await session(app)
|
|
276
|
+
await Users.setEmail(db, invitee.userId, 'dev@example.com')
|
|
277
|
+
const declined = await app.request(`/v1/invitations/${invitation.id}/decline`, {
|
|
278
|
+
headers: { cookie: invitee.cookie },
|
|
279
|
+
method: 'POST',
|
|
280
|
+
})
|
|
281
|
+
expect(declined.status).toBe(200)
|
|
282
|
+
expect(await TestApp.json(declined, InvitationsApp.schema.declineInvitation.Response)).toEqual({
|
|
283
|
+
id: invitation.id,
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// The pending list drains and the invitation can no longer be accepted or re-declined.
|
|
287
|
+
const mine = await app.request('/v1/invitations', { headers: { cookie: invitee.cookie } })
|
|
288
|
+
expect(
|
|
289
|
+
(await TestApp.json(mine, InvitationsApp.schema.listMyInvitations.Response)).data,
|
|
290
|
+
).toEqual([])
|
|
291
|
+
const accepted = await app.request(`/v1/invitations/${invitation.id}/accept`, {
|
|
292
|
+
headers: { cookie: invitee.cookie },
|
|
293
|
+
method: 'POST',
|
|
294
|
+
})
|
|
295
|
+
expect(accepted.status).toBe(404)
|
|
296
|
+
const again = await app.request(`/v1/invitations/${invitation.id}/decline`, {
|
|
297
|
+
headers: { cookie: invitee.cookie },
|
|
298
|
+
method: 'POST',
|
|
299
|
+
})
|
|
300
|
+
expect(again.status).toBe(404)
|
|
301
|
+
})
|
|
302
|
+
|
|
263
303
|
test("hides invitations addressed to someone else's email", async () => {
|
|
264
304
|
const db = TestApp.database()
|
|
265
305
|
const app = createApp({ db })
|
|
@@ -279,6 +319,11 @@ describe('invitations', () => {
|
|
|
279
319
|
method: 'POST',
|
|
280
320
|
})
|
|
281
321
|
expect(accepted.status).toBe(404)
|
|
322
|
+
const declined = await app.request(`/v1/invitations/${invitation.id}/decline`, {
|
|
323
|
+
headers: { cookie: other.cookie },
|
|
324
|
+
method: 'POST',
|
|
325
|
+
})
|
|
326
|
+
expect(declined.status).toBe(404)
|
|
282
327
|
})
|
|
283
328
|
|
|
284
329
|
test('a failing email sender never fails the invite', async () => {
|
|
@@ -356,6 +401,9 @@ describe('invitations', () => {
|
|
|
356
401
|
json('POST', owner.cookie, { email: 'dev@example.com', role: 'member' }),
|
|
357
402
|
)
|
|
358
403
|
expect(invited.status).toBe(200)
|
|
359
|
-
|
|
404
|
+
const invitation = await TestApp.json(invited, InvitationsApp.schema.Invitation)
|
|
405
|
+
expect(sent[0]!.text).toContain(
|
|
406
|
+
`https://portal.selfhost.example/?email=dev%40example.com&invitation=${invitation.id}`,
|
|
407
|
+
)
|
|
360
408
|
})
|
|
361
409
|
})
|
|
@@ -171,6 +171,21 @@ export namespace schema {
|
|
|
171
171
|
)
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
/** Schemas for the declineInvitation operation. */
|
|
175
|
+
export namespace declineInvitation {
|
|
176
|
+
/** Confirmation that the invitation was declined. */
|
|
177
|
+
export const Response = z
|
|
178
|
+
.object({
|
|
179
|
+
id: z
|
|
180
|
+
.string()
|
|
181
|
+
.check(
|
|
182
|
+
z.describe('ID of the invitation that was declined.'),
|
|
183
|
+
z.meta({ examples: ['inv_1a2b3c4d5e6f7g8h9j0k1m2n'] }),
|
|
184
|
+
),
|
|
185
|
+
})
|
|
186
|
+
.check(z.describe('Confirmation that the invitation was declined.'))
|
|
187
|
+
}
|
|
188
|
+
|
|
174
189
|
/** Schemas for the revokeInvitation operation. */
|
|
175
190
|
export namespace revokeInvitation {
|
|
176
191
|
/** Confirmation that the invitation was revoked. */
|
|
@@ -450,6 +465,57 @@ export function invitations() {
|
|
|
450
465
|
}
|
|
451
466
|
},
|
|
452
467
|
)
|
|
468
|
+
.post(
|
|
469
|
+
'/v1/invitations/:invitationId{inv_[A-Za-z0-9_-]+}/decline',
|
|
470
|
+
Auth.policy({ session: true }),
|
|
471
|
+
OpenApi.validate('param', schema.AcceptParams, {
|
|
472
|
+
code: 'param_invalid',
|
|
473
|
+
message: 'Check the path parameters and try again.',
|
|
474
|
+
}),
|
|
475
|
+
OpenApi.describeRoute({
|
|
476
|
+
description: "Decline an invitation addressed to the session's verified email. Declining settles the invitation; it can no longer be accepted.", // prettier-ignore
|
|
477
|
+
operationId: 'declineInvitation',
|
|
478
|
+
responses: OpenApi.responses({
|
|
479
|
+
errors: { 404: 'No accessible pending invitation was found for this id.' },
|
|
480
|
+
success: {
|
|
481
|
+
description: 'Confirmation that the invitation was declined.',
|
|
482
|
+
schema: schema.declineInvitation.Response,
|
|
483
|
+
},
|
|
484
|
+
}),
|
|
485
|
+
summary: 'Decline invitation',
|
|
486
|
+
tags: ['Invitations'],
|
|
487
|
+
}),
|
|
488
|
+
async (c) => {
|
|
489
|
+
if (Auth.narrowAccess) return Auth.superAdminAccessError(c)
|
|
490
|
+
if (OpenApi.narrowValidation)
|
|
491
|
+
return OpenApi.validationError(c, {
|
|
492
|
+
code: 'param_invalid',
|
|
493
|
+
message: 'Check the path parameters and try again.',
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
const principal = Auth.getPrincipal(c)
|
|
497
|
+
if (principal?.type !== 'session') return sessionRequired(c)
|
|
498
|
+
|
|
499
|
+
const { invitationId } = c.req.valid('param')
|
|
500
|
+
const db = Db.get(c.get('db'))
|
|
501
|
+
try {
|
|
502
|
+
// An invitation is visible only to the verified holder of its email;
|
|
503
|
+
// everyone else sees 404.
|
|
504
|
+
const record = await Invitations.get(db, invitationId)
|
|
505
|
+
if (!record || record.email !== principal.email?.toLowerCase())
|
|
506
|
+
return invitationNotFound(c)
|
|
507
|
+
// Declining reuses the revoke settle; no surface distinguishes the two.
|
|
508
|
+
const declined = await Invitations.revoke(db, invitationId)
|
|
509
|
+
if (!declined) return invitationNotFound(c)
|
|
510
|
+
return c.json(
|
|
511
|
+
Response.validated(schema.declineInvitation.Response, { id: invitationId }),
|
|
512
|
+
200,
|
|
513
|
+
)
|
|
514
|
+
} catch (cause) {
|
|
515
|
+
return Response.upstream(c, cause)
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
)
|
|
453
519
|
}
|
|
454
520
|
|
|
455
521
|
/** Dispatches the invitation email through the app's sender, best-effort. */
|
|
@@ -463,6 +529,8 @@ function dispatchEmail(
|
|
|
463
529
|
const expiry = days === 1 ? '1 day' : `${days} days`
|
|
464
530
|
const link = new URL(sender.consoleUrl ?? 'https://console.tempo.xyz')
|
|
465
531
|
link.searchParams.set('email', record.email)
|
|
532
|
+
// The console's index route steers `?invitation=` to the accept screen.
|
|
533
|
+
link.searchParams.set('invitation', record.id)
|
|
466
534
|
const url = link.href
|
|
467
535
|
const dispatch = Promise.resolve(
|
|
468
536
|
sender.send({
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { serve } from '@hono/node-server'
|
|
2
|
+
import * as Accounts from 'accounts/server'
|
|
3
|
+
import { exportJWK, generateKeyPair } from 'jose'
|
|
1
4
|
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
|
|
2
5
|
|
|
3
6
|
import * as TestApp from '../../../../test/App.js'
|
|
@@ -77,6 +80,24 @@ describe('GET /me', () => {
|
|
|
77
80
|
expect(response.status).toBe(401)
|
|
78
81
|
})
|
|
79
82
|
|
|
83
|
+
test('logout revokes bearer-token sessions', async () => {
|
|
84
|
+
const app = createApp()
|
|
85
|
+
const account = privateKeyToAccount(generatePrivateKey())
|
|
86
|
+
const { verifyResponse } = await TestApp.signIn(app, account, { returnToken: true })
|
|
87
|
+
const { token } = (await verifyResponse.json()) as { token: string }
|
|
88
|
+
|
|
89
|
+
const logout = await app.request('/v1/auth/siwe/logout', {
|
|
90
|
+
headers: { authorization: `Bearer ${token}` },
|
|
91
|
+
method: 'POST',
|
|
92
|
+
})
|
|
93
|
+
expect(logout.status).toBe(204)
|
|
94
|
+
|
|
95
|
+
const response = await app.request('/v1/me', {
|
|
96
|
+
headers: { authorization: `Bearer ${token}` },
|
|
97
|
+
})
|
|
98
|
+
expect(response.status).toBe(401)
|
|
99
|
+
})
|
|
100
|
+
|
|
80
101
|
test('rejects anonymous requests', async () => {
|
|
81
102
|
const app = createApp()
|
|
82
103
|
const response = await app.request('/v1/me')
|
|
@@ -119,3 +140,212 @@ describe('GET /me', () => {
|
|
|
119
140
|
`)
|
|
120
141
|
})
|
|
121
142
|
})
|
|
143
|
+
|
|
144
|
+
describe('sign-in email requirement', () => {
|
|
145
|
+
test('rejects sign-ins without an identity token', async () => {
|
|
146
|
+
const app = TestApp.create({
|
|
147
|
+
auth: { keys: [TestApp.key] },
|
|
148
|
+
session: { wallet: { origin, requireEmail: true } },
|
|
149
|
+
})
|
|
150
|
+
const account = privateKeyToAccount(generatePrivateKey())
|
|
151
|
+
const { cookie, verifyResponse } = await TestApp.signIn(app, account)
|
|
152
|
+
|
|
153
|
+
expect(verifyResponse.status).toBe(400)
|
|
154
|
+
expect(cookie).toBeUndefined()
|
|
155
|
+
expect(await verifyResponse.json()).toMatchInlineSnapshot(`
|
|
156
|
+
{
|
|
157
|
+
"error": "identity token required",
|
|
158
|
+
}
|
|
159
|
+
`)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('folds the token-verified email onto the session', async () => {
|
|
163
|
+
const oidc = await createIssuer({ email: 'dev@example.com' })
|
|
164
|
+
try {
|
|
165
|
+
const app = TestApp.create({
|
|
166
|
+
auth: { keys: [TestApp.key] },
|
|
167
|
+
session: { wallet: { issuer: oidc.url, origin, requireEmail: true } },
|
|
168
|
+
})
|
|
169
|
+
const account = privateKeyToAccount(generatePrivateKey())
|
|
170
|
+
const { cookie, verifyResponse } = await TestApp.signIn(app, account, {
|
|
171
|
+
idToken: ({ nonce }) => oidc.mint({ address: account.address, nonce }),
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
expect(verifyResponse.status).toBe(200)
|
|
175
|
+
const response = await app.request('/v1/me', { headers: { cookie: cookie! } })
|
|
176
|
+
expect(response.status).toBe(200)
|
|
177
|
+
const body = await TestApp.json(response, Me.schema.User)
|
|
178
|
+
expect(body.email).toBe('dev@example.com')
|
|
179
|
+
} finally {
|
|
180
|
+
await oidc.close()
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('sign-up email domains', () => {
|
|
186
|
+
test('allows new users on an allowlisted domain', async () => {
|
|
187
|
+
const oidc = await createIssuer({ email: 'dev@TEMPO.xyz' })
|
|
188
|
+
try {
|
|
189
|
+
const app = TestApp.create({
|
|
190
|
+
auth: { keys: [TestApp.key] },
|
|
191
|
+
session: { emailAllowlist: ['Tempo.XYZ'], wallet: { issuer: oidc.url, origin } },
|
|
192
|
+
})
|
|
193
|
+
const account = privateKeyToAccount(generatePrivateKey())
|
|
194
|
+
const { cookie, verifyResponse } = await TestApp.signIn(app, account, {
|
|
195
|
+
idToken: ({ nonce }) => oidc.mint({ address: account.address, nonce }),
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
expect(verifyResponse.status).toBe(200)
|
|
199
|
+
const response = await app.request('/v1/me', { headers: { cookie: cookie! } })
|
|
200
|
+
expect(response.status).toBe(200)
|
|
201
|
+
const body = await TestApp.json(response, Me.schema.User)
|
|
202
|
+
expect(body.email).toBe('dev@TEMPO.xyz')
|
|
203
|
+
} finally {
|
|
204
|
+
await oidc.close()
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('rejects new users on other domains', async () => {
|
|
209
|
+
const oidc = await createIssuer({ email: 'dev@example.com' })
|
|
210
|
+
try {
|
|
211
|
+
const app = TestApp.create({
|
|
212
|
+
auth: { keys: [TestApp.key] },
|
|
213
|
+
session: { emailAllowlist: ['tempo.xyz'], wallet: { issuer: oidc.url, origin } },
|
|
214
|
+
})
|
|
215
|
+
const account = privateKeyToAccount(generatePrivateKey())
|
|
216
|
+
const { cookie, verifyResponse } = await TestApp.signIn(app, account, {
|
|
217
|
+
idToken: ({ nonce }) => oidc.mint({ address: account.address, nonce }),
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
expect(verifyResponse.status).toBe(403)
|
|
221
|
+
expect(cookie).toBeUndefined()
|
|
222
|
+
expect(await verifyResponse.json()).toMatchInlineSnapshot(`
|
|
223
|
+
{
|
|
224
|
+
"error": "sign-ups are restricted to approved email domains or invited emails",
|
|
225
|
+
}
|
|
226
|
+
`)
|
|
227
|
+
} finally {
|
|
228
|
+
await oidc.close()
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('rejects new users without an identity token', async () => {
|
|
233
|
+
const app = TestApp.create({
|
|
234
|
+
auth: { keys: [TestApp.key] },
|
|
235
|
+
session: { emailAllowlist: ['tempo.xyz'], wallet: { origin } },
|
|
236
|
+
})
|
|
237
|
+
const account = privateKeyToAccount(generatePrivateKey())
|
|
238
|
+
const { cookie, verifyResponse } = await TestApp.signIn(app, account)
|
|
239
|
+
|
|
240
|
+
expect(verifyResponse.status).toBe(403)
|
|
241
|
+
expect(cookie).toBeUndefined()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test('allows invited emails regardless of domain', async () => {
|
|
245
|
+
const oidc = await createIssuer({ email: 'Invited@Example.com' })
|
|
246
|
+
try {
|
|
247
|
+
const db = TestApp.database()
|
|
248
|
+
|
|
249
|
+
// Owner signs up while sign-up is open and issues the invitation.
|
|
250
|
+
const open = TestApp.create({
|
|
251
|
+
auth: { keys: [TestApp.key] },
|
|
252
|
+
db,
|
|
253
|
+
session: { wallet: { origin } },
|
|
254
|
+
})
|
|
255
|
+
const owner = privateKeyToAccount(generatePrivateKey())
|
|
256
|
+
const { cookie } = await TestApp.signIn(open, owner)
|
|
257
|
+
const created = await open.request('/v1/orgs', {
|
|
258
|
+
body: JSON.stringify({ name: 'Acme' }),
|
|
259
|
+
headers: { 'content-type': 'application/json', cookie: cookie! },
|
|
260
|
+
method: 'POST',
|
|
261
|
+
})
|
|
262
|
+
const org = (await created.json()) as { id: string }
|
|
263
|
+
const invited = await open.request(`/v1/orgs/${org.id}/invitations`, {
|
|
264
|
+
body: JSON.stringify({ email: 'invited@example.com', role: 'member' }),
|
|
265
|
+
headers: { 'content-type': 'application/json', cookie: cookie! },
|
|
266
|
+
method: 'POST',
|
|
267
|
+
})
|
|
268
|
+
expect(invited.status).toBe(200)
|
|
269
|
+
|
|
270
|
+
const restricted = TestApp.create({
|
|
271
|
+
auth: { keys: [TestApp.key] },
|
|
272
|
+
db,
|
|
273
|
+
session: { emailAllowlist: ['tempo.xyz'], wallet: { issuer: oidc.url, origin } },
|
|
274
|
+
})
|
|
275
|
+
const account = privateKeyToAccount(generatePrivateKey())
|
|
276
|
+
const { verifyResponse } = await TestApp.signIn(restricted, account, {
|
|
277
|
+
idToken: ({ nonce }) => oidc.mint({ address: account.address, nonce }),
|
|
278
|
+
})
|
|
279
|
+
expect(verifyResponse.status).toBe(200)
|
|
280
|
+
} finally {
|
|
281
|
+
await oidc.close()
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('allows existing users regardless of domain', async () => {
|
|
286
|
+
const db = TestApp.database()
|
|
287
|
+
const account = privateKeyToAccount(generatePrivateKey())
|
|
288
|
+
|
|
289
|
+
// Row created before the restriction (open sign-up deployment).
|
|
290
|
+
const open = TestApp.create({
|
|
291
|
+
auth: { keys: [TestApp.key] },
|
|
292
|
+
db,
|
|
293
|
+
session: { wallet: { origin } },
|
|
294
|
+
})
|
|
295
|
+
const first = await TestApp.signIn(open, account)
|
|
296
|
+
expect(first.verifyResponse.status).toBe(200)
|
|
297
|
+
|
|
298
|
+
const restricted = TestApp.create({
|
|
299
|
+
auth: { keys: [TestApp.key] },
|
|
300
|
+
db,
|
|
301
|
+
session: { emailAllowlist: ['tempo.xyz'], wallet: { origin } },
|
|
302
|
+
})
|
|
303
|
+
const second = await TestApp.signIn(restricted, account)
|
|
304
|
+
expect(second.verifyResponse.status).toBe(200)
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
/** Local OIDC issuer standing in for the wallet: the SDK's own provider (discovery, JWKS, token mint). */
|
|
309
|
+
async function createIssuer(options: { email: string }) {
|
|
310
|
+
// Extractable: the provider takes key material as JWK strings.
|
|
311
|
+
const { privateKey, publicKey } = await generateKeyPair('EdDSA', { extractable: true })
|
|
312
|
+
// The provider pins its `issuer` URL at construction, but the ephemeral
|
|
313
|
+
// port is only known after bind — listen through a closure, assign after.
|
|
314
|
+
let provider!: ReturnType<typeof Accounts.Handler.oidcProvider>
|
|
315
|
+
type Listener = { close: () => Promise<void>; url: string }
|
|
316
|
+
const { close, url } = await new Promise<Listener>((resolve) => {
|
|
317
|
+
const server = serve({ fetch: (request) => provider.fetch(request), port: 0 }, (info) =>
|
|
318
|
+
resolve({
|
|
319
|
+
close: () => new Promise((done) => server.close(() => done())),
|
|
320
|
+
url: `http://127.0.0.1:${info.port}`,
|
|
321
|
+
}),
|
|
322
|
+
)
|
|
323
|
+
})
|
|
324
|
+
provider = Accounts.Handler.oidcProvider({
|
|
325
|
+
// Test-only: no `authenticate`, so the request body's `subject` is trusted.
|
|
326
|
+
getClaims: () => ({ email: options.email, email_verified: true }),
|
|
327
|
+
issuer: url,
|
|
328
|
+
publicKey: JSON.stringify(await exportJWK(publicKey)),
|
|
329
|
+
signingKey: JSON.stringify(await exportJWK(privateKey)),
|
|
330
|
+
})
|
|
331
|
+
return {
|
|
332
|
+
close,
|
|
333
|
+
/** Mints a wallet-style identity token bound to the SIWE nonce and signer. */
|
|
334
|
+
async mint(mintOptions: { address: string; nonce: string }) {
|
|
335
|
+
const response = await provider.fetch(
|
|
336
|
+
new Request(`${url}/token`, {
|
|
337
|
+
body: JSON.stringify({
|
|
338
|
+
audience: origin,
|
|
339
|
+
nonce: mintOptions.nonce,
|
|
340
|
+
subject: mintOptions.address,
|
|
341
|
+
}),
|
|
342
|
+
headers: { 'content-type': 'application/json' },
|
|
343
|
+
method: 'POST',
|
|
344
|
+
}),
|
|
345
|
+
)
|
|
346
|
+
const { idToken } = (await response.json()) as { idToken: string }
|
|
347
|
+
return idToken
|
|
348
|
+
},
|
|
349
|
+
url,
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -5,6 +5,7 @@ import * as ApiKeys from '../../../ApiKeys.js'
|
|
|
5
5
|
import type * as App from '../../../App.js'
|
|
6
6
|
import * as Auth from '../../../internal/Auth.js'
|
|
7
7
|
import * as Db from '../../../db/Db.js'
|
|
8
|
+
import * as Memberships from '../../../db/tables/memberships.js'
|
|
8
9
|
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
9
10
|
import * as Organizations from '../../../db/tables/organizations.js'
|
|
10
11
|
import * as Response from '../../../internal/Response.js'
|
|
@@ -42,6 +43,12 @@ export namespace schema {
|
|
|
42
43
|
z.describe('Human-readable organization name.'),
|
|
43
44
|
z.meta({ examples: ['Acme, Inc.'] }),
|
|
44
45
|
),
|
|
46
|
+
role: z
|
|
47
|
+
.optional(z.enum(Memberships.roles))
|
|
48
|
+
.check(
|
|
49
|
+
z.describe("The caller's role in this organization; absent for the super admin."),
|
|
50
|
+
z.meta({ examples: ['owner'] }),
|
|
51
|
+
),
|
|
45
52
|
updatedAt: z
|
|
46
53
|
.string()
|
|
47
54
|
.check(
|
|
@@ -181,7 +188,13 @@ export function orgs() {
|
|
|
181
188
|
const record =
|
|
182
189
|
principal.type === 'super_admin'
|
|
183
190
|
? await Organizations.create(db, { createdBy: 'super_admin', name: body.name })
|
|
184
|
-
:
|
|
191
|
+
: {
|
|
192
|
+
...(await Organizations.createOwned(db, {
|
|
193
|
+
name: body.name,
|
|
194
|
+
userId: principal.id,
|
|
195
|
+
})),
|
|
196
|
+
role: 'owner' as const,
|
|
197
|
+
}
|
|
185
198
|
return c.json(Response.validated(schema.Organization, serializeOrganization(record)), 200)
|
|
186
199
|
} catch (cause) {
|
|
187
200
|
return Response.upstream(c, cause)
|
|
@@ -215,7 +228,10 @@ export function orgs() {
|
|
|
215
228
|
})
|
|
216
229
|
|
|
217
230
|
return c.json(
|
|
218
|
-
Response.validated(
|
|
231
|
+
Response.validated(
|
|
232
|
+
schema.Organization,
|
|
233
|
+
serializeOrganization({ ...Auth.org(c), role: Auth.membership(c)?.role }),
|
|
234
|
+
),
|
|
219
235
|
200,
|
|
220
236
|
)
|
|
221
237
|
},
|
|
@@ -260,7 +276,10 @@ export function orgs() {
|
|
|
260
276
|
const updated = await Organizations.update(db, Auth.org(c).id, { name: body.name })
|
|
261
277
|
if (!updated) return organizationNotFound(c)
|
|
262
278
|
return c.json(
|
|
263
|
-
Response.validated(
|
|
279
|
+
Response.validated(
|
|
280
|
+
schema.Organization,
|
|
281
|
+
serializeOrganization({ ...updated, role: Auth.membership(c)?.role }),
|
|
282
|
+
),
|
|
264
283
|
200,
|
|
265
284
|
)
|
|
266
285
|
} catch (cause) {
|
|
@@ -317,11 +336,14 @@ export function orgs() {
|
|
|
317
336
|
)
|
|
318
337
|
}
|
|
319
338
|
|
|
320
|
-
function serializeOrganization(
|
|
339
|
+
function serializeOrganization(
|
|
340
|
+
record: Organizations.Record & { role?: Memberships.Role | undefined },
|
|
341
|
+
) {
|
|
321
342
|
return {
|
|
322
343
|
createdAt: record.createdAt,
|
|
323
344
|
id: record.id,
|
|
324
345
|
name: record.name,
|
|
346
|
+
...(record.role ? { role: record.role } : {}),
|
|
325
347
|
updatedAt: record.updatedAt,
|
|
326
348
|
}
|
|
327
349
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as Scope from '../../../Scope.js'
|
|
2
|
+
import * as TestApp from '../../../../test/App.js'
|
|
3
|
+
import * as Scopes from './scopes.js'
|
|
4
|
+
|
|
5
|
+
describe('scopes', () => {
|
|
6
|
+
test('lists the scope catalog without authentication (public catalog)', async () => {
|
|
7
|
+
const app = TestApp.create()
|
|
8
|
+
const response = await app.request('/v1/scopes')
|
|
9
|
+
const body = await TestApp.json(response, Scopes.schema.getScopes.Response)
|
|
10
|
+
|
|
11
|
+
expect(response.status).toMatchInlineSnapshot(`200`)
|
|
12
|
+
// Static public data must be cacheable (`stable`); without it every
|
|
13
|
+
// request pays the full auth/rate-limit floor.
|
|
14
|
+
expect(response.headers.get('cache-control')).toMatchInlineSnapshot(
|
|
15
|
+
`"private, max-age=300, stale-while-revalidate=3600"`,
|
|
16
|
+
)
|
|
17
|
+
expect(body.data).toEqual(Scope.catalog.map((entry) => ({ ...entry })))
|
|
18
|
+
expect(body.data.map((entry) => entry.scope)).toMatchInlineSnapshot(`
|
|
19
|
+
[
|
|
20
|
+
"data:read",
|
|
21
|
+
"indexer:query",
|
|
22
|
+
"rpc-relay:read",
|
|
23
|
+
"rpc-relay:sponsor",
|
|
24
|
+
"webhooks:read",
|
|
25
|
+
"webhooks:write",
|
|
26
|
+
]
|
|
27
|
+
`)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
|
+
import * as z from 'zod/mini'
|
|
3
|
+
|
|
4
|
+
import type * as App from '../../../App.js'
|
|
5
|
+
import * as Auth from '../../../internal/Auth.js'
|
|
6
|
+
import * as Cache from '../../../internal/Cache.js'
|
|
7
|
+
import * as OpenApi from '../../../internal/OpenApi.js'
|
|
8
|
+
import * as Response from '../../../internal/Response.js'
|
|
9
|
+
import * as Scope from '../../../Scope.js'
|
|
10
|
+
|
|
11
|
+
/** Zod schemas owned by the scope-catalog resource. */
|
|
12
|
+
export namespace schema {
|
|
13
|
+
/** Schemas for the getScopes operation. */
|
|
14
|
+
export namespace getScopes {
|
|
15
|
+
/** A single issuable API-key scope. */
|
|
16
|
+
export const Entry = z
|
|
17
|
+
.object({
|
|
18
|
+
description: z
|
|
19
|
+
.string()
|
|
20
|
+
.check(
|
|
21
|
+
z.describe('Human-readable explanation of what the scope grants.'),
|
|
22
|
+
z.meta({ examples: ['Read webhook subscriptions.'] }),
|
|
23
|
+
),
|
|
24
|
+
scope: z
|
|
25
|
+
.enum(Scope.catalog.map((entry) => entry.scope))
|
|
26
|
+
.check(
|
|
27
|
+
z.describe('Scope identifier used in API-key grants and route policies.'),
|
|
28
|
+
z.meta({ examples: ['data:read'] }),
|
|
29
|
+
),
|
|
30
|
+
selfServe: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.check(
|
|
33
|
+
z.describe('Whether a session may self-mint this scope (else super-admin-only).'),
|
|
34
|
+
z.meta({ examples: [true] }),
|
|
35
|
+
),
|
|
36
|
+
})
|
|
37
|
+
.check(z.describe('A single issuable API-key scope.'))
|
|
38
|
+
|
|
39
|
+
/** Response body for the scope catalog. */
|
|
40
|
+
export const Response = z
|
|
41
|
+
.object({
|
|
42
|
+
data: z.array(Entry).check(z.describe('Issuable API-key scopes.')),
|
|
43
|
+
})
|
|
44
|
+
.check(z.describe('Response body for the scope catalog.'))
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Mounts the `/scopes` resource: the issuable API-key scope vocabulary, served
|
|
50
|
+
* from {@link Scope.catalog}. Public catalog data: anonymous callers are served
|
|
51
|
+
* within the public quota; the session lane lets signed-in consoles skip it.
|
|
52
|
+
*/
|
|
53
|
+
export function scopes() {
|
|
54
|
+
return new Hono<App.Environment>().get(
|
|
55
|
+
'/v1/scopes',
|
|
56
|
+
Auth.policy({ public: true, session: true }),
|
|
57
|
+
OpenApi.describeRoute({
|
|
58
|
+
description: 'See which scopes can be granted to API keys.',
|
|
59
|
+
operationId: 'getScopes',
|
|
60
|
+
responses: OpenApi.responses({
|
|
61
|
+
success: { description: 'Issuable scope catalog.', schema: schema.getScopes.Response },
|
|
62
|
+
}),
|
|
63
|
+
summary: 'List scopes',
|
|
64
|
+
tags: ['API Keys'],
|
|
65
|
+
}),
|
|
66
|
+
Cache.response<App.Environment>({
|
|
67
|
+
cacheControl: Cache.policies.stable,
|
|
68
|
+
name: 'tempo-api:scopes:v1',
|
|
69
|
+
key: (c) => {
|
|
70
|
+
const url = new URL(c.req.url)
|
|
71
|
+
url.search = ''
|
|
72
|
+
return url.toString()
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
(c) => {
|
|
76
|
+
if (Auth.narrowAccess) return Auth.accessError(c)
|
|
77
|
+
return c.json(
|
|
78
|
+
Response.validated(schema.getScopes.Response, {
|
|
79
|
+
data: Scope.catalog.map((entry) => ({ ...entry })),
|
|
80
|
+
}),
|
|
81
|
+
200,
|
|
82
|
+
)
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
}
|