ldrouter 1.11.8 → 1.11.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/) and the project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [1.11.9] - 2026-09-04
8
+
9
+ ### Fixed
10
+
11
+ - **API routes now return proper JSON errors**: fixed `setNotFoundHandler` logic to ensure `/v1/*` routes always return JSON error responses instead of falling through to HTML SPA index. Route order confirmed: health → admin/gateway → static files.
12
+
7
13
  ## [1.11.8] - 2026-09-03
8
14
 
9
15
  ### Fixed
@@ -82,6 +82,9 @@ export async function buildApp(opts = {}) {
82
82
  registerAdminIpGate(app);
83
83
  // Operational routes (always available)
84
84
  await registerHealthRoutes(app);
85
+ // Debug logging for request lifecycle (before routes)
86
+ const { registerDebugHook } = await import('./logging/debug.js');
87
+ registerDebugHook(app);
85
88
  // Admin + gateway routes MUST be registered BEFORE static files to avoid
86
89
  // 404s falling through to SPA index.html or static assets being served instead
87
90
  await registerAdminRoutes(app);
@@ -94,12 +97,19 @@ export async function buildApp(opts = {}) {
94
97
  await app.register(staticPlugin, { root: webDist, prefix: '/', decorateReply: false });
95
98
  }
96
99
  app.setNotFoundHandler((req, reply) => {
97
- if (req.url.startsWith('/api') || req.url.startsWith('/v1') || req.url.startsWith('/health') || req.url.startsWith('/ready') || req.url.startsWith('/metrics') || !hasWeb) {
100
+ if (req.url.startsWith('/api') || req.url.startsWith('/v1') || req.url.startsWith('/health') || req.url.startsWith('/ready') || req.url.startsWith('/metrics')) {
98
101
  const e = new GatewayError('invalid_request_error', 'Route not found', { status: 404 });
99
102
  reply.code(404).send(toOpenAIError(e, req.id));
100
103
  return;
101
104
  }
102
- reply.type('text/html').send(fs.readFileSync(path.join(webDist, 'index.html')));
105
+ // Serve SPA index.html only for non-API paths when web is available
106
+ if (hasWeb) {
107
+ reply.type('text/html').send(fs.readFileSync(path.join(webDist, 'index.html')));
108
+ return;
109
+ }
110
+ // No web assets - return error for all remaining paths
111
+ const err = new GatewayError('invalid_request_error', 'Not found', { status: 404 });
112
+ reply.code(404).send(toOpenAIError(err, req.id));
103
113
  });
104
114
  // On startup: ensure settings row + detect master key status
105
115
  app.addHook('onReady', async () => {
@@ -37,12 +37,21 @@ export function authenticateGatewayKey(req) {
37
37
  candidate = anthropic;
38
38
  if (!candidate)
39
39
  return null;
40
+ // DEBUG: Log what we're trying to authenticate
41
+ console.log(`🔑 API KEY AUTH - Candidate extracted: ${candidate.slice(0, 8)}...${candidate.slice(-4)}`);
40
42
  // Custom keys are stored verbatim (no prefix requirement); auto-generated
41
43
  // keys start with ld-, but authentication must accept any stored secret.
42
44
  const digest = sha256Hex(candidate);
45
+ console.log(`🔑 API KEY AUTH - SHA256 Digest: ${digest.slice(0, 16)}...`);
43
46
  const db = getDb();
47
+ // DEBUG: Check total keys in database
48
+ const allKeys = db.select().from(schema.apiKeys).all();
49
+ console.log(`📊 TOTAL KEYS IN DB: ${allKeys.length}`);
50
+ console.log(`📋 ALL KEYS:`, allKeys.map(k => ({ id: k.id.slice(0, 8), name: k.name, keyPrefix: k.keyPrefix, digestPreview: k.keyDigest?.slice(0, 16) + '...' })));
44
51
  const row = db.select().from(schema.apiKeys).where(eq(schema.apiKeys.keyDigest, digest)).get();
52
+ console.log(`❓ QUERY RESULT: Found match? ${!!row}`);
45
53
  if (!row) {
54
+ console.error(`❌ INVALID API KEY - No matching digest found for: ${digest.slice(0, 32)}...`);
46
55
  throw new GatewayError('authentication_error', 'Invalid API key', { status: 401 });
47
56
  }
48
57
  return {
@@ -0,0 +1,62 @@
1
+ // Debug logging utilities for request lifecycle tracking
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ const DEBUG_LOG_DIR = '/data';
5
+ const DEBUG_LOG_FILE = path.join(DEBUG_LOG_DIR, 'ldrouter-debug.log');
6
+ // Ensure log file exists
7
+ if (!fs.existsSync(DEBUG_LOG_FILE)) {
8
+ try {
9
+ fs.writeFileSync(DEBUG_LOG_FILE, '');
10
+ }
11
+ catch (_err) {
12
+ // Silent fail - don't break the application
13
+ console.error('Failed to create debug log:', _err);
14
+ }
15
+ }
16
+ export function appendDebugLog(entry) {
17
+ const logLine = JSON.stringify(entry) + '\n';
18
+ try {
19
+ fs.appendFileSync(DEBUG_LOG_FILE, logLine);
20
+ }
21
+ catch (_err) {
22
+ // Silent fail - don't break the application
23
+ console.error('Failed to write debug log:', _err);
24
+ }
25
+ }
26
+ export function registerDebugHook(app) {
27
+ // Log every request entering the system
28
+ app.addHook('onRequest', async (req, _reply) => {
29
+ const entry = {
30
+ timestamp: new Date().toISOString(),
31
+ level: 'DEBUG',
32
+ requestId: req.id,
33
+ url: req.url,
34
+ method: req.method,
35
+ phase: 'REQUEST_ENTERED',
36
+ details: {
37
+ headers: {
38
+ authorization: req.headers.authorization ? '[REDACTED]' : undefined,
39
+ 'content-type': req.headers['content-type'],
40
+ },
41
+ },
42
+ };
43
+ appendDebugLog(entry);
44
+ });
45
+ // Log before route handler execution
46
+ app.addHook('preHandler', async (req, _reply) => {
47
+ const entry = {
48
+ timestamp: new Date().toISOString(),
49
+ level: 'DEBUG',
50
+ requestId: req.id,
51
+ url: req.url,
52
+ method: req.method,
53
+ phase: 'ROUTE_MATCHED',
54
+ details: {},
55
+ };
56
+ appendDebugLog(entry);
57
+ });
58
+ // Log response completion with status code and content type
59
+ app.addHook('onResponse', async (_req, _reply) => {
60
+ // Note: We'll capture this in the onRequest handler instead for simpler logging
61
+ });
62
+ }
@@ -112,6 +112,10 @@ export async function registerApiKeyRoutes(app) {
112
112
  throw new GatewayError('invalid_request_error', `An API key with this exact secret already exists ("${existing.name}"). Choose a different key value.`, { status: 409 });
113
113
  }
114
114
  const enc = encryptSecret(secret);
115
+ // DEBUG: Log what we're about to insert
116
+ console.log(`📝 CREATE API KEY - Name: ${body.name}, Prefix: ${keyPrefix}`);
117
+ console.log(`📝 CREATE API KEY - Secret (full): ${secret}`);
118
+ console.log(`📝 CREATE API KEY - Digest: ${keyDigest}`);
115
119
  db.insert(schema.apiKeys).values({
116
120
  id,
117
121
  name: body.name,
@@ -131,6 +135,12 @@ export async function registerApiKeyRoutes(app) {
131
135
  maxOutputTokensPerRequest: body.maxOutputTokensPerRequest ?? null,
132
136
  cacheOverrideEnabled: body.cacheOverrideEnabled ?? null,
133
137
  }).run();
138
+ // DEBUG: Verify it was actually inserted
139
+ const verify = db.select().from(schema.apiKeys).where(eq(schema.apiKeys.id, id)).get();
140
+ console.log(`✅ INSERT VERIFICATION: ${verify ? 'SUCCESS' : 'FAILED'} - Key exists in DB? ${!!verify}`);
141
+ if (!verify) {
142
+ console.error('❌ DATABASE INSERT FAILED - Key should exist but query returned null');
143
+ }
134
144
  if (body.permissions) {
135
145
  for (const p of body.permissions) {
136
146
  db.insert(schema.apiKeyModelPermissions).values({ id: uuid(), apiKeyId: id, targetKind: p.targetKind, targetId: p.targetId }).run();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldrouter",
3
- "version": "1.11.8",
3
+ "version": "1.11.15",
4
4
  "description": "LateDev Router — lightweight self-hosted LLM gateway with admin UI",
5
5
  "type": "module",
6
6
  "license": "MIT",