ldrouter 1.10.1 → 1.10.2
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 +7 -0
- package/dist/server/routes/admin/api-keys.js +21 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ 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.10.2] - 2026-09-01
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **API keys page no longer crashes after restoring a backup from another instance**: the key list/detail endpoints decrypt every stored key secret; a key encrypted with a different master key (restore from a different machine) threw an AES-GCM auth error that took down the whole request as a generic "Gateway error". Undecryptable keys now yield `secret: null` and the rest of the list still loads.
|
|
12
|
+
- **Creating an API key with an already-used custom secret** now returns a clear `409` ("An API key with this exact secret already exists…") instead of a raw SQLite `UNIQUE constraint failed` surfacing as a generic "Gateway error".
|
|
13
|
+
|
|
7
14
|
## [1.10.1] - 2026-09-01
|
|
8
15
|
|
|
9
16
|
### Fixed
|
|
@@ -7,6 +7,18 @@ import { recordAudit } from '../../db/repositories/audit.js';
|
|
|
7
7
|
import { generateApiKeySecret, sha256Hex, uuid } from '../../auth/ids.js';
|
|
8
8
|
import { encryptSecret, decryptSecret } from '../../auth/crypto.js';
|
|
9
9
|
import { GatewayError } from '../../errors.js';
|
|
10
|
+
/** Decrypt an API key secret, tolerating keys that were encrypted with a
|
|
11
|
+
* different master key (e.g. after restoring a backup from another
|
|
12
|
+
* instance). A failed decrypt yields `null` instead of crashing the whole
|
|
13
|
+
* key list/detail request. */
|
|
14
|
+
function safeDecrypt(payload) {
|
|
15
|
+
try {
|
|
16
|
+
return decryptSecret(payload);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
10
22
|
const PermEntry = z.object({ targetKind: z.enum(['model', 'combo', 'alias']), targetId: z.string() });
|
|
11
23
|
const IPRule = z.object({ mode: z.enum(['allow', 'deny']), cidr: z.string().min(1).max(64) });
|
|
12
24
|
const ApiKeyCreate = z.object({
|
|
@@ -46,7 +58,7 @@ export async function registerApiKeyRoutes(app) {
|
|
|
46
58
|
tpmLimit: k.tpmLimit,
|
|
47
59
|
concurrencyLimit: k.maxConcurrent,
|
|
48
60
|
secret: k.keySecretEncrypted && k.keySecretNonce
|
|
49
|
-
?
|
|
61
|
+
? safeDecrypt({ ciphertext: k.keySecretEncrypted, nonce: k.keySecretNonce, version: k.keySecretVersion ?? 1 })
|
|
50
62
|
: null,
|
|
51
63
|
})),
|
|
52
64
|
};
|
|
@@ -77,7 +89,7 @@ export async function registerApiKeyRoutes(app) {
|
|
|
77
89
|
maxOutputTokensPerRequest: k.maxOutputTokensPerRequest,
|
|
78
90
|
cacheOverrideEnabled: k.cacheOverrideEnabled,
|
|
79
91
|
secret: k.keySecretEncrypted && k.keySecretNonce
|
|
80
|
-
?
|
|
92
|
+
? safeDecrypt({ ciphertext: k.keySecretEncrypted, nonce: k.keySecretNonce, version: k.keySecretVersion ?? 1 })
|
|
81
93
|
: null,
|
|
82
94
|
},
|
|
83
95
|
};
|
|
@@ -92,6 +104,13 @@ export async function registerApiKeyRoutes(app) {
|
|
|
92
104
|
const id = uuid();
|
|
93
105
|
const keyPrefix = secret.slice(0, 11);
|
|
94
106
|
const keyDigest = sha256Hex(secret);
|
|
107
|
+
// Reject a duplicate key before inserting: the digest is UNIQUE, and a
|
|
108
|
+
// raw SqliteError (not a GatewayError) would otherwise surface as a
|
|
109
|
+
// generic "Gateway error" 500 to the admin.
|
|
110
|
+
const existing = db.select().from(schema.apiKeys).where(eq(schema.apiKeys.keyDigest, keyDigest)).get();
|
|
111
|
+
if (existing) {
|
|
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
|
+
}
|
|
95
114
|
const enc = encryptSecret(secret);
|
|
96
115
|
db.insert(schema.apiKeys).values({
|
|
97
116
|
id,
|