auth-core-m2m.ts233 lines · main
1/**
2 * briven-engine M2M client credentials.
3 *
4 * Dashboard (session + project admin):
5 * GET/POST /v1/auth-core/projects/:projectId/m2m/clients
6 * DELETE /v1/auth-core/projects/:projectId/m2m/clients/:clientId
7 *
8 * Public token endpoint (OAuth2 client_credentials):
9 * POST /v1/auth-core/oauth/token
10 */
11
12import { Hono } from 'hono';
13
14import { requireAuthCoreProject } from '../middleware/auth-core-guard.js';
15import { BRIVEN_ENGINE_ID } from '../services/auth-core/engine.js';
16import {
17 createM2mClient,
18 isM2mRole,
19 issueM2mToken,
20 listM2mClients,
21 revokeM2mClient,
22 type M2mRole,
23} from '../services/auth-core/m2m.js';
24import type { AppEnv } from '../types/app-env.js';
25import type { User } from '../middleware/session.js';
26
27export const authCoreM2mRouter = new Hono<AppEnv>();
28
29// ─── Dashboard: manage clients ───────────────────────────────────────
30
31authCoreM2mRouter.use(
32 '/v1/auth-core/projects/:projectId/m2m/clients',
33 ...requireAuthCoreProject('admin'),
34);
35authCoreM2mRouter.use(
36 '/v1/auth-core/projects/:projectId/m2m/clients/*',
37 ...requireAuthCoreProject('admin'),
38);
39
40authCoreM2mRouter.get(
41 '/v1/auth-core/projects/:projectId/m2m/clients',
42 async (c) => {
43 const projectId = c.req.param('projectId');
44 try {
45 const clients = await listM2mClients(projectId);
46 return c.json({
47 engine: BRIVEN_ENGINE_ID,
48 projectId,
49 clients: clients.map((cl) => ({
50 id: cl.id,
51 clientId: cl.clientId,
52 name: cl.name,
53 role: cl.role,
54 hint: `…${cl.secretSuffix}`,
55 revokedAt: cl.revokedAt,
56 lastUsedAt: cl.lastUsedAt,
57 createdAt: cl.createdAt,
58 })),
59 });
60 } catch (err) {
61 return c.json(
62 {
63 engine: BRIVEN_ENGINE_ID,
64 code: 'list_failed',
65 message: err instanceof Error ? err.message : String(err),
66 },
67 500,
68 );
69 }
70 },
71);
72
73authCoreM2mRouter.post(
74 '/v1/auth-core/projects/:projectId/m2m/clients',
75 async (c) => {
76 const projectId = c.req.param('projectId');
77 let body: { name?: string; role?: string } = {};
78 try {
79 body = await c.req.json();
80 } catch {
81 body = {};
82 }
83 if (!body.name?.trim()) {
84 return c.json(
85 { engine: BRIVEN_ENGINE_ID, code: 'name_required', message: 'name required' },
86 400,
87 );
88 }
89 const role: M2mRole =
90 body.role && isM2mRole(body.role) ? body.role : 'developer';
91 const user = c.get('user') as User | null;
92 try {
93 const created = await createM2mClient({
94 projectId,
95 name: body.name,
96 role,
97 createdBy: user?.id ?? null,
98 });
99 return c.json({
100 engine: BRIVEN_ENGINE_ID,
101 projectId,
102 client: {
103 id: created.client.id,
104 clientId: created.client.clientId,
105 name: created.client.name,
106 role: created.client.role,
107 hint: `…${created.client.secretSuffix}`,
108 /** Only once */
109 clientSecret: created.clientSecret,
110 },
111 note: 'Copy client_id and client_secret now — secret is not shown again.',
112 tokenUrl: `${(process.env.BRIVEN_API_ORIGIN ?? 'https://api.briven.tech').replace(/\/$/, '')}/v1/auth-core/oauth/token`,
113 });
114 } catch (err) {
115 return c.json(
116 {
117 engine: BRIVEN_ENGINE_ID,
118 code: 'create_failed',
119 message: err instanceof Error ? err.message : String(err),
120 },
121 400,
122 );
123 }
124 },
125);
126
127authCoreM2mRouter.delete(
128 '/v1/auth-core/projects/:projectId/m2m/clients/:clientId',
129 async (c) => {
130 const projectId = c.req.param('projectId');
131 const clientId = c.req.param('clientId');
132 try {
133 await revokeM2mClient(projectId, clientId);
134 return c.json({
135 engine: BRIVEN_ENGINE_ID,
136 ok: true,
137 projectId,
138 clientId,
139 });
140 } catch (err) {
141 return c.json(
142 {
143 engine: BRIVEN_ENGINE_ID,
144 code: 'revoke_failed',
145 message: err instanceof Error ? err.message : String(err),
146 },
147 404,
148 );
149 }
150 },
151);
152
153// ─── Public: OAuth2 token endpoint ───────────────────────────────────
154
155/**
156 * POST /v1/auth-core/oauth/token
157 * grant_type=client_credentials
158 * Accepts JSON or form body; optional HTTP Basic client_id:client_secret.
159 */
160authCoreM2mRouter.post('/v1/auth-core/oauth/token', async (c) => {
161 let clientId = '';
162 let clientSecret = '';
163 let grantType = '';
164
165 const auth = c.req.header('authorization');
166 if (auth?.startsWith('Basic ')) {
167 try {
168 const decoded = Buffer.from(auth.slice(6), 'base64').toString('utf8');
169 const colon = decoded.indexOf(':');
170 if (colon > 0) {
171 clientId = decoded.slice(0, colon);
172 clientSecret = decoded.slice(colon + 1);
173 }
174 } catch {
175 // ignore — body may still provide credentials
176 }
177 }
178
179 const ct = c.req.header('content-type') ?? '';
180 if (ct.includes('application/x-www-form-urlencoded')) {
181 const form = await c.req.parseBody();
182 grantType = String(form.grant_type ?? '');
183 if (!clientId) clientId = String(form.client_id ?? '');
184 if (!clientSecret) clientSecret = String(form.client_secret ?? '');
185 } else {
186 let body: {
187 grant_type?: string;
188 client_id?: string;
189 client_secret?: string;
190 } = {};
191 try {
192 body = await c.req.json();
193 } catch {
194 body = {};
195 }
196 grantType = body.grant_type ?? '';
197 if (!clientId) clientId = body.client_id ?? '';
198 if (!clientSecret) clientSecret = body.client_secret ?? '';
199 }
200
201 if (grantType !== 'client_credentials') {
202 return c.json(
203 {
204 error: 'unsupported_grant_type',
205 error_description: 'only client_credentials is supported',
206 engine: BRIVEN_ENGINE_ID,
207 },
208 400,
209 );
210 }
211
212 const result = await issueM2mToken({ clientId, clientSecret });
213 if (!result.ok) {
214 return c.json(
215 {
216 error: result.code,
217 error_description: result.message,
218 engine: BRIVEN_ENGINE_ID,
219 },
220 401,
221 );
222 }
223
224 return c.json({
225 access_token: result.accessToken,
226 token_type: result.tokenType,
227 expires_in: result.expiresIn,
228 engine: BRIVEN_ENGINE_ID,
229 project_id: result.projectId,
230 client_id: result.clientId,
231 role: result.role,
232 });
233});