refactor(platform-service): use shared optional jwt context

This commit is contained in:
saravanakumardb1 2026-03-06 12:57:00 -08:00
parent cfca118c71
commit e4baa2fc16
3 changed files with 34 additions and 34 deletions

View File

@ -1012,8 +1012,12 @@ The ecosystem finishes with fewer duplicate patterns and fewer partial migration
- Scope: readiness support, requestId in error responses, configurable startup fatal-exit behavior, idempotent signal handling, optional plugin failure policy, wrapper tests
- Commit: `acfad8a`
- Status: Completed
- [ ] **Increment 2 — Phase 2 shared optional JWT hook and first consumer migration**
- [x] **Increment 2 — Phase 2 shared optional JWT hook and first consumer migration**
- Scope: shared optional JWT context helper in `@bytelyst/fastify-core`, direct helper tests, `mcp-server` migrated off local `parseJwt`, obsolete local parser removed
- Commit: `cfca118`
- Status: Completed
- [ ] **Increment 3 — Shared JWT helper migration in platform-service**
- Scope: `platform-service` migrated from inline optional JWT parsing to `registerOptionalJwtContext`, bootstrap tests updated, shared-package and service validations passed
- Commit: `PENDING_COMMIT_SHA`
- Status: In progress

View File

@ -2,13 +2,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
const resolveSecretsMock = vi.fn(async () => undefined);
const createServiceAppMock = vi.fn();
const registerOptionalJwtContextMock = vi.fn(async () => undefined);
const startServiceMock = vi.fn(async () => undefined);
const loadProductCacheMock = vi.fn(async () => undefined);
const initCosmosIfNeededMock = vi.fn(async () => undefined);
const verifyTokenMock = vi.fn(async () => ({ sub: 'user_1', productId: 'lysnrai' }));
const appMock = {
addHook: vi.fn(),
register: vi.fn(async () => undefined),
};
@ -32,6 +32,7 @@ vi.mock('@bytelyst/config', () => ({
vi.mock('@bytelyst/fastify-core', () => ({
createServiceApp: createServiceAppMock,
registerOptionalJwtContext: registerOptionalJwtContextMock,
startService: startServiceMock,
}));
@ -67,7 +68,6 @@ describe('server bootstrap', () => {
vi.resetModules();
vi.clearAllMocks();
createServiceAppMock.mockResolvedValue(appMock);
appMock.addHook.mockReset();
appMock.register.mockReset();
appMock.register.mockResolvedValue(undefined);
});
@ -79,38 +79,43 @@ describe('server bootstrap', () => {
expect(initCosmosIfNeededMock).toHaveBeenCalledOnce();
expect(loadProductCacheMock).toHaveBeenCalledOnce();
expect(createServiceAppMock).toHaveBeenCalledOnce();
expect(registerOptionalJwtContextMock).toHaveBeenCalledOnce();
expect(appMock.register).toHaveBeenCalled();
expect(appMock.register.mock.calls.length).toBeGreaterThan(15);
expect(startServiceMock).toHaveBeenCalledWith(appMock, { port: 4003, host: '0.0.0.0' });
});
it('onRequest hook sets jwtPayload when bearer token is valid', async () => {
it('registers optional jwt parsing with the shared fastify-core helper', async () => {
await import('./server.js');
const onRequestHook = appMock.addHook.mock.calls.find(([name]) => name === 'onRequest')?.[1];
expect(onRequestHook).toBeTypeOf('function');
const req = { headers: { authorization: 'Bearer token_abc' } } as {
headers: { authorization: string };
jwtPayload?: unknown;
};
await onRequestHook(req);
const helperCall = registerOptionalJwtContextMock.mock.calls[0] as unknown as [
unknown,
{
verifyToken: (token: string) => Promise<unknown>;
},
];
const options = helperCall[1];
expect(options).toBeDefined();
expect(options.verifyToken).toBeTypeOf('function');
await expect(options.verifyToken('token_abc')).resolves.toEqual({
sub: 'user_1',
productId: 'lysnrai',
});
expect(verifyTokenMock).toHaveBeenCalledWith('token_abc');
expect(req.jwtPayload).toEqual({ sub: 'user_1', productId: 'lysnrai' });
});
it('onRequest hook ignores invalid bearer token errors', async () => {
it('passes through verifyToken rejections to the shared helper callback', async () => {
verifyTokenMock.mockRejectedValueOnce(new Error('invalid token'));
await import('./server.js');
const onRequestHook = appMock.addHook.mock.calls.find(([name]) => name === 'onRequest')?.[1];
const req = { headers: { authorization: 'Bearer broken' } } as {
headers: { authorization: string };
jwtPayload?: unknown;
};
const helperCall = registerOptionalJwtContextMock.mock.calls[0] as unknown as [
unknown,
{
verifyToken: (token: string) => Promise<unknown>;
},
];
const options = helperCall[1];
await expect(onRequestHook(req)).resolves.toBeUndefined();
expect(req.jwtPayload).toBeUndefined();
await expect(options.verifyToken('broken')).rejects.toThrow('invalid token');
});
});

View File

@ -20,7 +20,7 @@ await resolveSecrets([
LYSNR_SECRETS.AZURE_BLOB_ACCOUNT_KEY,
]);
import { createServiceApp, startService } from '@bytelyst/fastify-core';
import { createServiceApp, registerOptionalJwtContext, startService } from '@bytelyst/fastify-core';
import { productRoutes } from './modules/products/routes.js';
import { loadProductCache } from './modules/products/cache.js';
import { authRoutes } from './modules/auth/routes.js';
@ -103,20 +103,11 @@ const app = await createServiceApp({
metrics: true,
});
// Parse JWT on every request (best-effort — doesn't block unauthenticated routes)
import { verifyToken } from './modules/auth/jwt.js';
import type { JwtPayload } from './lib/request-context.js';
app.addHook('onRequest', async req => {
const auth = req.headers.authorization;
if (!auth?.startsWith('Bearer ')) return;
try {
const payload = await verifyToken(auth.slice(7));
req.jwtPayload = payload as JwtPayload;
} catch {
// Token invalid/expired — leave jwtPayload undefined.
// Auth-required routes will handle this in their own validation.
}
await registerOptionalJwtContext(app, {
verifyToken: async token => (await verifyToken(token)) as JwtPayload,
});
// Register route modules