File size: 1,538 Bytes
6111b2b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { randomBytes } from "node:crypto";
import { encrypt, decrypt } from "@/lib/db/encryption";
import { getServiceRow, updateServiceField } from "@/lib/db/versionManager";

export function generateServiceApiKey(prefix = "nr"): string {
  return `${prefix}_${randomBytes(32).toString("base64url")}`;
}

export class ServiceApiKeyDecryptError extends Error {
  constructor(tool: string) {
    super(
      `Stored API key for service '${tool}' could not be decrypted. ` +
        `STORAGE_ENCRYPTION_KEY may have changed, the row may be corrupted, ` +
        `or it was written on a different machine. Reinstall the service ` +
        `or rotate the key via POST /api/services/${tool}/rotate-key.`
    );
    this.name = "ServiceApiKeyDecryptError";
  }
}

export async function getOrCreateApiKey(tool: string): Promise<string> {
  const row = await getServiceRow(tool);
  if (row?.apiKey) {
    const decrypted = decrypt(row.apiKey);
    if (decrypted) return decrypted;
    // Fail loud: silently regenerating would mint a new key the embedded
    // service has never been told, causing every request to 401 with no
    // operator-facing signal.
    throw new ServiceApiKeyDecryptError(tool);
  }
  const key = generateServiceApiKey(tool === "9router" ? "nr" : "cp");
  await updateServiceField(tool, "apiKey", encrypt(key) ?? key);
  return key;
}

export function maskApiKey(plainKey: string): string {
  const last4 = plainKey.slice(-4);
  return `nr_••••••••${last4}`;
}