storage-keys.ts126 lines · main
1import { Hono } from 'hono';
2import { z } from 'zod';
3
4import { env } from '../env.js';
5import { log } from '../lib/logger.js';
6import {
7 requireProjectAuth,
8 requireProjectRole,
9} from '../middleware/project-auth.js';
10import { audit, hashIp } from '../services/audit.js';
11import { isMinioAdminConfigured } from '../services/minio-admin.js';
12import {
13 createStorageKey,
14 listStorageKeys,
15 revokeStorageKey,
16} from '../services/storage-keys.js';
17import type { AppEnv } from '../types/app-env.js';
18import type { User } from '../middleware/session.js';
19
20/**
21 * Customer-facing storage keys — mint / list / revoke bucket-scoped S3 keys for
22 * a project. Accepts dashboard session OR project CLI key (brk_…), same as
23 * env/db routes — so `briven storage setup` works after Convex-style setup.
24 * Secret returned ONCE on create.
25 */
26export const storageKeysRouter = new Hono<AppEnv>();
27
28storageKeysRouter.use('/v1/projects/:id/storage-keys', requireProjectAuth());
29storageKeysRouter.use('/v1/projects/:id/storage-keys/*', requireProjectAuth());
30
31const createSchema = z.object({ name: z.string().min(1).max(80) });
32
33function ipHashFrom(c: {
34 req: { header: (n: string) => string | undefined };
35}): string | null {
36 return hashIp(c.req.header('cf-connecting-ip') ?? c.req.header('x-forwarded-for') ?? null);
37}
38
39function notConfigured(c: { json: (b: unknown, s: number) => Response }) {
40 return c.json(
41 { code: 'storage_not_configured', message: 'object storage is not configured on this api' },
42 503,
43 );
44}
45
46storageKeysRouter.get(
47 '/v1/projects/:id/storage-keys',
48 requireProjectRole('viewer'),
49 async (c) => {
50 const projectId = c.req.param('id');
51 const keys = await listStorageKeys(projectId);
52 return c.json({
53 keys,
54 endpoint: env.BRIVEN_MINIO_PUBLIC_ENDPOINT ?? env.BRIVEN_MINIO_ENDPOINT ?? '',
55 });
56 },
57);
58
59storageKeysRouter.post(
60 '/v1/projects/:id/storage-keys',
61 requireProjectRole('admin'),
62 async (c) => {
63 if (!isMinioAdminConfigured()) return notConfigured(c);
64 const projectId = c.req.param('id');
65 const user = c.get('user') as User | null;
66
67 const body = await c.req.json().catch(() => null);
68 const parsed = createSchema.safeParse(body);
69 if (!parsed.success) {
70 return c.json(
71 {
72 code: 'validation_failed',
73 message: 'invalid request body',
74 issues: parsed.error.issues,
75 },
76 400,
77 );
78 }
79
80 let created;
81 try {
82 created = await createStorageKey({
83 projectId,
84 name: parsed.data.name,
85 createdBy: user?.id ?? null,
86 publicEndpoint: env.BRIVEN_MINIO_PUBLIC_ENDPOINT ?? env.BRIVEN_MINIO_ENDPOINT ?? '',
87 });
88 } catch (err) {
89 const message = err instanceof Error ? err.message : String(err);
90 log.error('storage_key_mint_failed', { projectId, error: message });
91 return c.json(
92 { code: 'mint_failed', message: `could not mint key — ${message}`.slice(0, 600) },
93 502,
94 );
95 }
96 await audit({
97 actorId: user?.id ?? c.get('apiKeyId') ?? 'api_key',
98 projectId,
99 action: 'storage_key.create',
100 ipHash: ipHashFrom(c),
101 userAgent: c.req.header('user-agent') ?? null,
102 metadata: { keyId: created.record.id, name: created.record.name, bucket: created.bucket },
103 });
104 return c.json(created, 201);
105 },
106);
107
108storageKeysRouter.delete(
109 '/v1/projects/:id/storage-keys/:keyId',
110 requireProjectRole('admin'),
111 async (c) => {
112 const projectId = c.req.param('id');
113 const user = c.get('user') as User | null;
114 const keyId = c.req.param('keyId');
115 await revokeStorageKey(projectId, keyId);
116 await audit({
117 actorId: user?.id ?? c.get('apiKeyId') ?? 'api_key',
118 projectId,
119 action: 'storage_key.revoke',
120 ipHash: ipHashFrom(c),
121 userAgent: c.req.header('user-agent') ?? null,
122 metadata: { keyId },
123 });
124 return c.json({ ok: true });
125 },
126);