mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 01:48:12 +00:00
Map EDR invoices onto the MoR EIMS /v1/register document and add the cryptographic transport needed to talk to core.mor.gov.et. Mapper: DTOs mirror the supplied Postman collection section by section. Tax is resolved per line via a caller-supplied resolver and throws when unresolved -- the app models no tax at all (invoice.taxAmount is always 0, invoice_lines and the rate catalogue carry no fiscal columns), so a zero-rated default would assert a tax position the codebase cannot support. Seller identity, document number, counters and previous IRN are passed in explicitly; the mapper stays pure. Transport: config, credential loading, RSA-SHA512 signing and /auth/login with an in-memory token cache. Signing reproduces the process that produced a working live token -- compact JSON of the inner request only, exact UTF-8 bytes, base64 signature, and base64 of the certificate file's exact bytes with no parsing or re-encoding. Concurrent callers share one login via an in-flight promise. Refresh is deliberately unimplemented: the collection shows an unsigned refresh body but also ships unsigned examples of calls that do require signing, so an expired token re-logs in instead. Errors normalise to EimsApiException carrying only the gateway's own error fields; secrets, signature, certificate and tokens never reach logs. Key and certificate file patterns are gitignored. Nothing calls EIMS automatically and no invoice entity, migration or UI is touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
38 lines
1.5 KiB
TypeScript
38 lines
1.5 KiB
TypeScript
import { createSign } from "node:crypto";
|
|
import { Injectable } from "@nestjs/common";
|
|
import { EimsCredentialsProvider } from "./eims-credentials.provider";
|
|
import { EimsSignedRequest } from "./eims.types";
|
|
|
|
/**
|
|
* Signs EIMS request objects, reproducing the process that produced a working live access token:
|
|
*
|
|
* 1. compact `JSON.stringify` of the **inner** request object only,
|
|
* 2. those exact UTF-8 bytes,
|
|
* 3. RSA + SHA-512 (`SHA512withRSA`, PKCS#1 v1.5 — Node's default RSA padding),
|
|
* 4. base64 of the raw signature bytes (256 bytes for an RSA-2048 key),
|
|
* 5. base64 of the certificate file's exact bytes.
|
|
*
|
|
* The outer `{request, signature, certificate}` envelope is never itself signed, and the request
|
|
* object is never mutated after serialization.
|
|
*/
|
|
@Injectable()
|
|
export class EimsSignerService {
|
|
constructor(private readonly credentials: EimsCredentialsProvider) {}
|
|
|
|
signRequest<T>(request: T): EimsSignedRequest<T> {
|
|
const payload = JSON.stringify(request);
|
|
const signature = createSign("RSA-SHA512")
|
|
.update(payload, "utf8")
|
|
.sign(this.credentials.getPrivateKey(), "base64");
|
|
|
|
return { request, signature, certificate: this.credentials.getCertificateBase64() };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exact wire body for a signed envelope. Serializing here (rather than handing axios an object)
|
|
* keeps one serializer in play: the `request` segment of this string is byte-identical to the
|
|
* string that was signed.
|
|
*/
|
|
export const toSignedBody = <T>(signed: EimsSignedRequest<T>): string => JSON.stringify(signed);
|