From 507f0fdd1fe6618d559a5637c221038d7be504c3 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 15 Mar 2026 06:09:42 +0000 Subject: [PATCH] feat(tokens): classify machine credentials --- .../src/modules/tokens/repository.test.ts | 2 ++ .../src/modules/tokens/routes.test.ts | 8 ++++++++ .../src/modules/tokens/routes.ts | 4 +++- .../src/modules/tokens/tokens.test.ts | 19 ++++++++++++++++++- .../src/modules/tokens/types.ts | 4 ++++ services/platform-service/src/server.test.ts | 2 +- 6 files changed, 36 insertions(+), 3 deletions(-) diff --git a/services/platform-service/src/modules/tokens/repository.test.ts b/services/platform-service/src/modules/tokens/repository.test.ts index 9b5c6b6e..fa3366a1 100644 --- a/services/platform-service/src/modules/tokens/repository.test.ts +++ b/services/platform-service/src/modules/tokens/repository.test.ts @@ -14,6 +14,8 @@ const baseToken: ApiTokenDoc = { userId: 'user_1', userName: 'Test User', name: 'Test Token', + tokenType: 'user_api', + environment: 'prod', tokenHash: 'hash_abc', prefix: 'lysnr_', status: 'active', diff --git a/services/platform-service/src/modules/tokens/routes.test.ts b/services/platform-service/src/modules/tokens/routes.test.ts index 665c62a7..cb171ad9 100644 --- a/services/platform-service/src/modules/tokens/routes.test.ts +++ b/services/platform-service/src/modules/tokens/routes.test.ts @@ -23,6 +23,8 @@ const baseToken = { userId: 'admin_1', userName: 'admin@example.com', name: 'ci-token', + tokenType: 'service_api', + environment: 'prod', prefix: 'wai_12345678', status: 'active', scopes: ['read'], @@ -117,6 +119,12 @@ describe('tokenRoutes', () => { const data = JSON.parse(res.body); expect(data).toHaveProperty('rawToken'); expect(data.rawToken.startsWith('wai_')).toBe(true); + expect(repoMock.create).toHaveBeenCalledWith( + expect.objectContaining({ + tokenType: 'user_api', + environment: 'prod', + }) + ); expect(repoMock.hashToken).toHaveBeenCalled(); }); diff --git a/services/platform-service/src/modules/tokens/routes.ts b/services/platform-service/src/modules/tokens/routes.ts index 187a5b29..d3894452 100644 --- a/services/platform-service/src/modules/tokens/routes.ts +++ b/services/platform-service/src/modules/tokens/routes.ts @@ -50,7 +50,7 @@ export async function tokenRoutes(app: FastifyInstance) { if (!parsed.success) { throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; ')); } - const { name, scopes, expiresInDays } = parsed.data; + const { name, tokenType, environment, scopes, expiresInDays } = parsed.data; const rawToken = `wai_${crypto.randomBytes(32).toString('hex')}`; const prefix = rawToken.slice(0, 12); @@ -64,6 +64,8 @@ export async function tokenRoutes(app: FastifyInstance) { userId: payload.sub, userName: payload.email ?? '', name, + tokenType, + environment, prefix, tokenHash, status: 'active', diff --git a/services/platform-service/src/modules/tokens/tokens.test.ts b/services/platform-service/src/modules/tokens/tokens.test.ts index 55192606..c74a8b66 100644 --- a/services/platform-service/src/modules/tokens/tokens.test.ts +++ b/services/platform-service/src/modules/tokens/tokens.test.ts @@ -12,6 +12,8 @@ describe('CreateTokenSchema', () => { expect(result.success).toBe(true); if (result.success) { expect(result.data.name).toBe('CI Pipeline'); + expect(result.data.tokenType).toBe('user_api'); + expect(result.data.environment).toBe('prod'); expect(result.data.scopes).toEqual(['read']); expect(result.data.expiresInDays).toBe(90); } @@ -20,11 +22,15 @@ describe('CreateTokenSchema', () => { it('accepts full input', () => { const result = CreateTokenSchema.safeParse({ name: 'Deploy Key', + tokenType: 'service_api', + environment: 'staging', scopes: ['read', 'write', 'admin'], expiresInDays: 30, }); expect(result.success).toBe(true); if (result.success) { + expect(result.data.tokenType).toBe('service_api'); + expect(result.data.environment).toBe('staging'); expect(result.data.scopes).toEqual(['read', 'write', 'admin']); expect(result.data.expiresInDays).toBe(30); } @@ -102,6 +108,8 @@ describe('ApiTokenDoc type shape', () => { userId: 'user_1', userName: 'admin@example.com', name: 'CI Token', + tokenType: 'service_api', + environment: 'prod', prefix: 'wai_abc12345', tokenHash: '$2a$10$hashedvalue', status: 'active', @@ -113,6 +121,8 @@ describe('ApiTokenDoc type shape', () => { }; expect(doc.status).toBe('active'); + expect(doc.tokenType).toBe('service_api'); + expect(doc.environment).toBe('prod'); expect(doc.scopes).toContain('read'); expect(doc.lastUsed).toBeNull(); }); @@ -126,6 +136,8 @@ describe('ApiTokenDoc type shape', () => { userId: 'u1', userName: 'test', name: 'test', + tokenType: 'user_api', + environment: 'prod', prefix: 'wai_', tokenHash: 'hash', status, @@ -147,6 +159,8 @@ describe('ApiTokenResponse type shape', () => { userId: 'u1', userName: 'test@test.com', name: 'My Token', + tokenType: 'product_api', + environment: 'staging', prefix: 'wai_abc', tokenHash: 'secret_hash', status: 'active', @@ -157,11 +171,14 @@ describe('ApiTokenResponse type shape', () => { }; // Simulate what stripHash does - const { tokenHash: _hash, ...response } = doc; + const { tokenHash: _tokenHash, ...response } = doc; + void _tokenHash; const apiResponse: ApiTokenResponse = response; expect(apiResponse.id).toBe('tok_1'); expect(apiResponse.name).toBe('My Token'); + expect(apiResponse.tokenType).toBe('product_api'); + expect(apiResponse.environment).toBe('staging'); expect('tokenHash' in apiResponse).toBe(false); }); }); diff --git a/services/platform-service/src/modules/tokens/types.ts b/services/platform-service/src/modules/tokens/types.ts index bd4c5a93..3d35064d 100644 --- a/services/platform-service/src/modules/tokens/types.ts +++ b/services/platform-service/src/modules/tokens/types.ts @@ -10,6 +10,8 @@ export interface ApiTokenDoc { userId: string; userName: string; name: string; + tokenType: 'user_api' | 'product_api' | 'service_api'; + environment: 'dev' | 'staging' | 'prod'; prefix: string; tokenHash: string; status: 'active' | 'revoked' | 'expired'; @@ -24,6 +26,8 @@ export type ApiTokenResponse = Omit; export const CreateTokenSchema = z.object({ name: z.string().min(1), + tokenType: z.enum(['user_api', 'product_api', 'service_api']).default('user_api'), + environment: z.enum(['dev', 'staging', 'prod']).default('prod'), scopes: z.array(z.string()).default(['read']), expiresInDays: z.number().int().min(1).max(365).default(90), }); diff --git a/services/platform-service/src/server.test.ts b/services/platform-service/src/server.test.ts index c3c04eed..e5647465 100644 --- a/services/platform-service/src/server.test.ts +++ b/services/platform-service/src/server.test.ts @@ -101,7 +101,7 @@ describe('server bootstrap', () => { expect(appMock.register).toHaveBeenCalled(); expect(appMock.register.mock.calls.length).toBeGreaterThan(15); expect(startServiceMock).toHaveBeenCalledWith(appMock, { port: 4003, host: '0.0.0.0' }); - }); + }, 15000); it('registers optional jwt parsing with the shared fastify-core helper', async () => { await import('./server.js');