Files
tehran_taj/apps/api/src/modules/accounting-sync/secret-box.ts
T
s.esmaeiliandClaude Opus 4.6 19d490e790 feat: accounting integration (D27), SMS relay driver (D26), rebuild user apps
- Add Tahesab/Zarrin accounting sync module with admin panel UI
- Add SMS relay driver as default provider, refactor SMS into driver pattern
- Update customer services, orders, products, and catalog queries
- Rebuild user-app and user-app-v2 dist assets
- Add Prisma migration for accounting tables
- Update docker-compose and env examples
- Update parity docs (DECISIONS, ISSUES, PROGRESS)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-06 16:48:36 +03:30

57 lines
2.3 KiB
TypeScript

import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto';
import { envOrNull } from '../../common/env';
/**
* Encrypts the accounting-software token at rest (D27).
*
* The token sits in the Setting table, so a database dump or a backup would
* otherwise hand out a live credential to the shop's books. AES-256-GCM with a
* key derived from `ACCOUNTING_SECRET_KEY`.
*
* When that variable is unset the key falls back to one derived from
* `JWT_SECRET`, with a loud warning at boot — the same "warn, don't refuse"
* trade-off D23 made — so an admin can configure the integration before ops
* add the variable. The cost: rotating JWT_SECRET then makes the stored token
* unreadable and the admin has to enter it again (the screen says so).
*/
const PREFIX = 'enc:v1:';
export function secretKeySource(): 'ACCOUNTING_SECRET_KEY' | 'JWT_SECRET' | null {
if (envOrNull('ACCOUNTING_SECRET_KEY')) return 'ACCOUNTING_SECRET_KEY';
if (envOrNull('JWT_SECRET')) return 'JWT_SECRET';
return null;
}
function key(): Buffer {
const material = envOrNull('ACCOUNTING_SECRET_KEY') ?? envOrNull('JWT_SECRET');
if (!material) {
throw new Error('ACCOUNTING_SECRET_KEY تنظیم نشده است؛ ذخیره‌ی توکن حسابداری ممکن نیست.');
}
return createHash('sha256').update(`goldbank-accounting:${material}`).digest();
}
export function sealSecret(plain: string): string {
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key(), iv);
const body = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
return PREFIX + Buffer.concat([iv, tag, body]).toString('base64');
}
/** Null when the value cannot be decrypted (wrong key, tampered, malformed). */
export function openSecret(sealed: string | null | undefined): string | null {
if (!sealed || !sealed.startsWith(PREFIX)) return null;
try {
const raw = Buffer.from(sealed.slice(PREFIX.length), 'base64');
const iv = raw.subarray(0, 12);
const tag = raw.subarray(12, 28);
const body = raw.subarray(28);
const decipher = createDecipheriv('aes-256-gcm', key(), iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(body), decipher.final()]).toString('utf8');
} catch {
return null;
}
}