| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- /**
- * briven-engine M2M client credentials.
- *
- * Dashboard (session + project admin):
- * GET/POST /v1/auth-core/projects/:projectId/m2m/clients
- * DELETE /v1/auth-core/projects/:projectId/m2m/clients/:clientId
- *
- * Public token endpoint (OAuth2 client_credentials):
- * POST /v1/auth-core/oauth/token
- */
- import { Hono } from 'hono';
- import { requireAuthCoreProject } from '../middleware/auth-core-guard.js';
- import { BRIVEN_ENGINE_ID } from '../services/auth-core/engine.js';
- import {
- createM2mClient,
- isM2mRole,
- issueM2mToken,
- listM2mClients,
- revokeM2mClient,
- type M2mRole,
- } from '../services/auth-core/m2m.js';
- import type { AppEnv } from '../types/app-env.js';
- import type { User } from '../middleware/session.js';
- export const authCoreM2mRouter = new Hono<AppEnv>();
- // ─── Dashboard: manage clients ───────────────────────────────────────
- authCoreM2mRouter.use(
- '/v1/auth-core/projects/:projectId/m2m/clients',
- ...requireAuthCoreProject('admin'),
- );
- authCoreM2mRouter.use(
- '/v1/auth-core/projects/:projectId/m2m/clients/*',
- ...requireAuthCoreProject('admin'),
- );
- authCoreM2mRouter.get(
- '/v1/auth-core/projects/:projectId/m2m/clients',
- async (c) => {
- const projectId = c.req.param('projectId');
- try {
- const clients = await listM2mClients(projectId);
- return c.json({
- engine: BRIVEN_ENGINE_ID,
- projectId,
- clients: clients.map((cl) => ({
- id: cl.id,
- clientId: cl.clientId,
- name: cl.name,
- role: cl.role,
- hint: `…${cl.secretSuffix}`,
- revokedAt: cl.revokedAt,
- lastUsedAt: cl.lastUsedAt,
- createdAt: cl.createdAt,
- })),
- });
- } catch (err) {
- return c.json(
- {
- engine: BRIVEN_ENGINE_ID,
- code: 'list_failed',
- message: err instanceof Error ? err.message : String(err),
- },
- 500,
- );
- }
- },
- );
- authCoreM2mRouter.post(
- '/v1/auth-core/projects/:projectId/m2m/clients',
- async (c) => {
- const projectId = c.req.param('projectId');
- let body: { name?: string; role?: string } = {};
- try {
- body = await c.req.json();
- } catch {
- body = {};
- }
- if (!body.name?.trim()) {
- return c.json(
- { engine: BRIVEN_ENGINE_ID, code: 'name_required', message: 'name required' },
- 400,
- );
- }
- const role: M2mRole =
- body.role && isM2mRole(body.role) ? body.role : 'developer';
- const user = c.get('user') as User | null;
- try {
- const created = await createM2mClient({
- projectId,
- name: body.name,
- role,
- createdBy: user?.id ?? null,
- });
- return c.json({
- engine: BRIVEN_ENGINE_ID,
- projectId,
- client: {
- id: created.client.id,
- clientId: created.client.clientId,
- name: created.client.name,
- role: created.client.role,
- hint: `…${created.client.secretSuffix}`,
- /** Only once */
- clientSecret: created.clientSecret,
- },
- note: 'Copy client_id and client_secret now — secret is not shown again.',
- tokenUrl: `${(process.env.BRIVEN_API_ORIGIN ?? 'https://api.briven.tech').replace(/\/$/, '')}/v1/auth-core/oauth/token`,
- });
- } catch (err) {
- return c.json(
- {
- engine: BRIVEN_ENGINE_ID,
- code: 'create_failed',
- message: err instanceof Error ? err.message : String(err),
- },
- 400,
- );
- }
- },
- );
- authCoreM2mRouter.delete(
- '/v1/auth-core/projects/:projectId/m2m/clients/:clientId',
- async (c) => {
- const projectId = c.req.param('projectId');
- const clientId = c.req.param('clientId');
- try {
- await revokeM2mClient(projectId, clientId);
- return c.json({
- engine: BRIVEN_ENGINE_ID,
- ok: true,
- projectId,
- clientId,
- });
- } catch (err) {
- return c.json(
- {
- engine: BRIVEN_ENGINE_ID,
- code: 'revoke_failed',
- message: err instanceof Error ? err.message : String(err),
- },
- 404,
- );
- }
- },
- );
- // ─── Public: OAuth2 token endpoint ───────────────────────────────────
- /**
- * POST /v1/auth-core/oauth/token
- * grant_type=client_credentials
- * Accepts JSON or form body; optional HTTP Basic client_id:client_secret.
- */
- authCoreM2mRouter.post('/v1/auth-core/oauth/token', async (c) => {
- // Abuse throttle (credential stuffing) — SuperTokens-style protect token endpoint.
- try {
- const { getRedis } = await import('../lib/redis.js');
- const redis = getRedis();
- const ip =
- c.req.header('cf-connecting-ip')?.trim() ||
- c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ||
- c.req.header('x-real-ip')?.trim() ||
- 'unknown';
- if (redis) {
- const windowSec = 60;
- const max = 30;
- const key = `rl:m2m:token:${ip}`;
- const n = await redis.incr(key);
- if (n === 1) await redis.expire(key, windowSec);
- if (n > max) {
- return c.json(
- {
- error: 'rate_limited',
- error_description: 'too many token requests — try again shortly',
- engine: BRIVEN_ENGINE_ID,
- },
- 429,
- );
- }
- }
- } catch {
- /* fail open if redis unavailable */
- }
- let clientId = '';
- let clientSecret = '';
- let grantType = '';
- const auth = c.req.header('authorization');
- if (auth?.startsWith('Basic ')) {
- try {
- const decoded = Buffer.from(auth.slice(6), 'base64').toString('utf8');
- const colon = decoded.indexOf(':');
- if (colon > 0) {
- clientId = decoded.slice(0, colon);
- clientSecret = decoded.slice(colon + 1);
- }
- } catch {
- // ignore — body may still provide credentials
- }
- }
- const ct = c.req.header('content-type') ?? '';
- if (ct.includes('application/x-www-form-urlencoded')) {
- const form = await c.req.parseBody();
- grantType = String(form.grant_type ?? '');
- if (!clientId) clientId = String(form.client_id ?? '');
- if (!clientSecret) clientSecret = String(form.client_secret ?? '');
- } else {
- let body: {
- grant_type?: string;
- client_id?: string;
- client_secret?: string;
- } = {};
- try {
- body = await c.req.json();
- } catch {
- body = {};
- }
- grantType = body.grant_type ?? '';
- if (!clientId) clientId = body.client_id ?? '';
- if (!clientSecret) clientSecret = body.client_secret ?? '';
- }
- if (grantType !== 'client_credentials') {
- return c.json(
- {
- error: 'unsupported_grant_type',
- error_description: 'only client_credentials is supported',
- engine: BRIVEN_ENGINE_ID,
- },
- 400,
- );
- }
- const result = await issueM2mToken({ clientId, clientSecret });
- if (!result.ok) {
- return c.json(
- {
- error: result.code,
- error_description: result.message,
- engine: BRIVEN_ENGINE_ID,
- },
- 401,
- );
- }
- return c.json({
- access_token: result.accessToken,
- token_type: result.tokenType,
- expires_in: result.expiresIn,
- engine: BRIVEN_ENGINE_ID,
- project_id: result.projectId,
- client_id: result.clientId,
- role: result.role,
- });
- });
|