feat(eims): allow private key/cert as inline base64 env vars

EIMS_PRIVATE_KEY_BASE64 / EIMS_CERTIFICATE_BASE64, alternative to the
existing _PATH vars. Wins over the path when set; falls back to the
file otherwise. Neither var required at boot on its own — the
either/or check moved out of the flat REQUIRED_VARS list.

Lets a dockerized deployment receive the key/cert the same way it
already receives every other EIMS_* secret (plain env var into the
container) instead of needing a host bind mount into the container
filesystem.
This commit is contained in:
Hagernesh
2026-08-17 09:14:38 +00:00
parent 28c9dd93e0
commit 4a4b1981cb
4 changed files with 72 additions and 20 deletions

View File

@@ -30,6 +30,14 @@ export interface EimsConfig {
privateKeyPath: string;
/** Filesystem path to the INSA-issued certificate bundle; sent as base64 of its exact bytes. */
certificatePath: string;
/**
* Inline alternative to `privateKeyPath` — the key file's own bytes, base64-encoded, so a
* container that can't be given a host bind mount can still receive it as a plain env var.
* Takes precedence over the path when set. Either one must be present when EIMS is enabled.
*/
privateKeyBase64: string;
/** Inline alternative to `certificatePath`, same precedence rule. */
certificateBase64: string;
httpTimeoutMs: number;
/** Re-authenticate this many ms before the access token actually expires. */
tokenSkewMs: number;
@@ -135,14 +143,14 @@ export interface EimsInvoiceConfig {
buyerIdNumber: string | null;
}
const REQUIRED_VARS = [
"EIMS_CLIENT_ID",
"EIMS_CLIENT_SECRET",
"EIMS_API_KEY",
"EIMS_TIN",
"EIMS_PRIVATE_KEY_PATH",
"EIMS_CERTIFICATE_PATH",
] as const;
const REQUIRED_VARS = ["EIMS_CLIENT_ID", "EIMS_CLIENT_SECRET", "EIMS_API_KEY", "EIMS_TIN"] as const;
// Key/cert each have two ways in (file path or inline base64) — checked separately from
// REQUIRED_VARS since it's "at least one of", not "this exact var".
const REQUIRED_EITHER_OR: Array<[string, string]> = [
["EIMS_PRIVATE_KEY_PATH", "EIMS_PRIVATE_KEY_BASE64"],
["EIMS_CERTIFICATE_PATH", "EIMS_CERTIFICATE_BASE64"],
];
const positiveInt = (raw: string | undefined, fallback: number, name: string): number => {
if (raw === undefined || raw === "") return fallback;
@@ -189,6 +197,8 @@ export default registerAs("eims", (): EimsConfig => {
systemType: process.env.EIMS_SYSTEM_TYPE ?? "",
privateKeyPath: process.env.EIMS_PRIVATE_KEY_PATH ?? "",
certificatePath: process.env.EIMS_CERTIFICATE_PATH ?? "",
privateKeyBase64: process.env.EIMS_PRIVATE_KEY_BASE64 ?? "",
certificateBase64: process.env.EIMS_CERTIFICATE_BASE64 ?? "",
httpTimeoutMs,
tokenSkewMs,
autoSubmit: (process.env.EIMS_AUTO_SUBMIT ?? "false").toLowerCase() === "true",
@@ -245,7 +255,10 @@ export default registerAs("eims", (): EimsConfig => {
if (!enabled) return base;
const missing = REQUIRED_VARS.filter((name) => !process.env[name]);
const missing: string[] = REQUIRED_VARS.filter((name) => !process.env[name]);
for (const [pathVar, base64Var] of REQUIRED_EITHER_OR) {
if (!process.env[pathVar] && !process.env[base64Var]) missing.push(`${pathVar} or ${base64Var}`);
}
if (missing.length > 0) {
throw new Error(
`EIMS integration is enabled (EIMS_ENABLED=true) but the following env vars are missing: ${missing.join(", ")}`,

View File

@@ -24,25 +24,31 @@ export class EimsCredentialsProvider {
return this.config.get<EimsConfig>("eims")!;
}
/** RSA private key, parsed once. Throws a config error if the path is missing or unusable. */
/**
* RSA private key, parsed once. `privateKeyBase64` wins when set (no file I/O at all — for a
* container that can't be given a host bind mount); otherwise falls back to `privateKeyPath`.
* Throws a config error if neither is usable.
*/
getPrivateKey(): KeyObject {
if (this.privateKey) return this.privateKey;
const path = this.cfg.privateKeyPath;
if (!path) throw new EimsConfigException("EIMS_PRIVATE_KEY_PATH is not set");
const { privateKeyBase64, privateKeyPath: path } = this.cfg;
const source = privateKeyBase64 ? "EIMS_PRIVATE_KEY_BASE64" : `EIMS_PRIVATE_KEY_PATH (${path})`;
if (!privateKeyBase64 && !path) throw new EimsConfigException("EIMS_PRIVATE_KEY_PATH is not set");
let key: KeyObject;
try {
key = createPrivateKey(readFileSync(path));
const bytes = privateKeyBase64 ? Buffer.from(privateKeyBase64, "base64") : readFileSync(path);
key = createPrivateKey(bytes);
} catch (err) {
// The path is operational information, not a secret; the key material never appears.
// The source is operational information, not a secret; the key material never appears.
throw new EimsConfigException(
`EIMS private key at ${path} could not be read or parsed: ${(err as Error).message}`,
`EIMS private key from ${source} could not be read or parsed: ${(err as Error).message}`,
);
}
if (key.asymmetricKeyType !== "rsa") {
throw new EimsConfigException(
`EIMS private key at ${path} is ${key.asymmetricKeyType ?? "of unknown type"}; EIMS requires RSA`,
`EIMS private key from ${source} is ${key.asymmetricKeyType ?? "of unknown type"}; EIMS requires RSA`,
);
}
@@ -51,11 +57,20 @@ export class EimsCredentialsProvider {
return key;
}
/** Base64 of the certificate file's exact bytes. No parsing, no re-encoding. */
/**
* Base64 of the certificate file's exact bytes. No parsing, no re-encoding. `certificateBase64`
* config wins when set (already base64, used as-is); otherwise read from `certificatePath`.
*/
getCertificateBase64(): string {
if (this.certificateBase64) return this.certificateBase64;
const path = this.cfg.certificatePath;
const { certificateBase64: inline, certificatePath: path } = this.cfg;
if (inline) {
this.certificateBase64 = inline;
this.logger.log(`EIMS certificate bundle loaded from EIMS_CERTIFICATE_BASE64`);
return this.certificateBase64;
}
if (!path) throw new EimsConfigException("EIMS_CERTIFICATE_PATH is not set");
let bytes: Buffer;

View File

@@ -97,8 +97,12 @@ describe("EimsSignerService", () => {
});
describe("EimsCredentialsProvider", () => {
const providerFor = (paths: { privateKeyPath?: string; certificatePath?: string }) =>
new EimsCredentialsProvider({ get: () => paths } as unknown as ConfigService);
const providerFor = (cfg: {
privateKeyPath?: string;
certificatePath?: string;
privateKeyBase64?: string;
certificateBase64?: string;
}) => new EimsCredentialsProvider({ get: () => cfg } as unknown as ConfigService);
it("fails clearly when the key path is unset", () => {
expect(() => providerFor({}).getPrivateKey()).toThrow(/EIMS_PRIVATE_KEY_PATH is not set/);
@@ -115,4 +119,22 @@ describe("EimsCredentialsProvider", () => {
writeFileSync(emptyPath, "");
expect(() => providerFor({ certificatePath: emptyPath }).getCertificateBase64()).toThrow(/is empty/);
});
it("loads the key from inline base64, no file involved", () => {
const keyBase64 = readFileSync(keyPath).toString("base64");
const key = providerFor({ privateKeyBase64: keyBase64 }).getPrivateKey();
expect(key.asymmetricKeyType).toBe("rsa");
});
it("prefers inline base64 over the path when both are set", () => {
const keyBase64 = readFileSync(keyPath).toString("base64");
// A path that would fail if it were ever actually read.
const key = providerFor({ privateKeyBase64: keyBase64, privateKeyPath: join(dir, "nope.key") }).getPrivateKey();
expect(key.asymmetricKeyType).toBe("rsa");
});
it("loads the certificate from inline base64 as-is, no re-encoding", () => {
const certBase64 = Buffer.from(CERTIFICATE_FIXTURE, "utf8").toString("base64");
expect(providerFor({ certificateBase64: certBase64 }).getCertificateBase64()).toBe(certBase64);
});
});

View File

@@ -59,6 +59,8 @@ export const eimsConfig = (over: Partial<EimsConfig> = {}): EimsConfig => ({
systemType: EIMS_SYSTEM_TYPE,
privateKeyPath: "/dev/null",
certificatePath: "/dev/null",
privateKeyBase64: "",
certificateBase64: "",
httpTimeoutMs: 30_000,
tokenSkewMs: 45_000,
autoSubmit: false,