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
|
package/dist/server/app.js
CHANGED
|
@@ -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')
|
|
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
|
-
|
|
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();
|