fix: live-run wiring (SDK 1.29, zod 3.25, OAUTH_ISSUER split, alt host ports, web on 3001, log level cast, pino transport)

- Bump @modelcontextprotocol/sdk from 1.0.4 to 1.29.0 in runner-template
  (1.0.4 has no McpServer or StreamableHTTPServerTransport — file not found at runtime).
- Bump zod to 3.25.76 across workspace to satisfy modern SDK peer dep.
- Split OAUTH_ISSUER (canonical, host-reachable) from CONTROL_PLANE_URL (container-reachable for JWKS).
  Runner verifies iss against OAUTH_ISSUER; fetches JWKS from CONTROL_PLANE_URL.
  Both API and runner now agree on http://localhost:4000/oauth as the issuer in dev.
- Move postgres host port 5432 to 5440, redis 6379 to 6390 to avoid collisions with
  native installs on the dev machine.
- Move web from 3000 to 3001 (3000 occupied by Gitea on dev machine).
- Drop pino-pretty transport from API to avoid runtime require of an unbundled dep.
- Cast build_logs.level (varchar) to BuildEvent's literal union in WS replay path.
- Remove unused reqBase helper in oauth.ts.
This commit is contained in:
Marco Sadjadi
2026-05-19 00:57:23 +02:00
parent ea1ec1e801
commit ab67203921
18 changed files with 3747 additions and 41 deletions

View File

@@ -21,7 +21,7 @@
"fastify": "5.2.0",
"ioredis": "5.4.1",
"jose": "5.9.6",
"zod": "3.23.8"
"zod": "3.25.76"
},
"devDependencies": {
"@types/node": "22.10.2",

View File

@@ -5,13 +5,14 @@ const Env = z.object({
DATABASE_URL: z.string(),
REDIS_URL: z.string().default('redis://localhost:6379'),
PORT: z.coerce.number().default(4000),
NEXT_PUBLIC_APP_URL: z.string().default('http://localhost:3000'),
NEXT_PUBLIC_APP_URL: z.string().default('http://localhost:3001'),
OAUTH_KEY_DIR: z.string().default('./keys'),
ANTHROPIC_API_KEY: z.string().optional(),
SECRETS_ENCRYPTION_KEY: z
.string()
.min(64, '32 bytes hex required')
.default('0000000000000000000000000000000000000000000000000000000000000000'),
CONTROL_PLANE_PUBLIC_URL: z.string().default('http://localhost:4000'),
});
export const config = Env.parse({

View File

@@ -10,10 +10,6 @@ import { oauthRoutes } from './routes/oauth.js';
const app = Fastify({
logger: {
level: config.NODE_ENV === 'production' ? 'info' : 'debug',
transport:
config.NODE_ENV === 'development'
? { target: 'pino-pretty', options: { colorize: true, singleLine: true } }
: undefined,
},
});

View File

@@ -46,7 +46,7 @@ async function resolveServerByResource(resource: string) {
export async function oauthRoutes(app: FastifyInstance): Promise<void> {
// Authorization Server Metadata (RFC 8414) — control-plane wide
app.get('/oauth/.well-known/oauth-authorization-server', async (_req, reply) => {
const base = `${reqBase(_req)}`;
const base = `${config.CONTROL_PLANE_PUBLIC_URL}`;
return reply.send({
issuer: `${base}/oauth`,
authorization_endpoint: `${base}/oauth/authorize`,
@@ -215,7 +215,7 @@ export async function oauthRoutes(app: FastifyInstance): Promise<void> {
const accessToken = await signAccessToken({
subject: row.code.userId ?? row.client.clientId,
audience: resource,
issuer: `${reqBase(req)}/oauth`,
issuer: `${config.CONTROL_PLANE_PUBLIC_URL}/oauth`,
scope: row.code.scope ?? '',
ttlSeconds: 3600,
});
@@ -249,7 +249,7 @@ export async function oauthRoutes(app: FastifyInstance): Promise<void> {
if (!parsed.success) return reply.code(400).send({ error: 'invalid_request' });
const server = await resolveServerByResource(parsed.data.resource);
if (!server) return reply.code(404).send({ error: 'not_found' });
const base = reqBase(req);
const base = config.CONTROL_PLANE_PUBLIC_URL;
return reply.send({
resource: parsed.data.resource,
authorization_servers: [`${base}/oauth`],
@@ -259,12 +259,3 @@ export async function oauthRoutes(app: FastifyInstance): Promise<void> {
});
}
function reqBase(req: { protocol?: string; headers: Record<string, string | string[] | undefined> }): string {
const host =
(req.headers['x-forwarded-host'] as string | undefined) ??
(req.headers.host as string | undefined) ??
`localhost:${config.PORT}`;
const proto =
(req.headers['x-forwarded-proto'] as string | undefined) ?? req.protocol ?? 'http';
return `${proto}://${host}`;
}

View File

@@ -183,10 +183,12 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
.where(eq(buildLogs.buildId, buildId))
.orderBy(buildLogs.timestamp);
for (const log of logs) {
const level: 'info' | 'warn' | 'error' =
log.level === 'warn' || log.level === 'error' ? log.level : 'info';
socket.send(
JSON.stringify({
type: 'log',
level: log.level,
level,
message: log.message,
at: log.timestamp.toISOString(),
} satisfies BuildEvent),