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 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
- ? decryptSecret({ ciphertext: k.keySecretEncrypted, nonce: k.keySecretNonce, version: k.keySecretVersion ?? 1 })
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
- ? decryptSecret({ ciphertext: k.keySecretEncrypted, nonce: k.keySecretNonce, version: k.keySecretVersion ?? 1 })
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ldrouter",
3
- "version": "1.10.1",
3
+ "version": "1.10.2",
4
4
  "description": "LateDev Router — lightweight self-hosted LLM gateway with admin UI",
5
5
  "type": "module",
6
6
  "license": "MIT",