Migration 1788439142825_resellers.cjs: `resellers` table plus nullable
`users.reseller_id` (scope=reseller per ADR-0004), indexes on
resellers.active and users.reseller_id.
packages/database/src/repositories/resellers.ts: list/get/create/update,
COALESCE-based partial update, mirroring the companies repository
pattern. Also adds repositories/audit.ts (hash-chained appendAuditEvent
over the existing audit_log table) and repositories/users.ts
(assignUserToReseller) - foundational pieces this module depends on that
hadn't landed in this worktree yet; audit.ts mirrors the sibling
worktree's already-built implementation verbatim to minimize merge
friction, and users.ts intentionally contains only the one function the
task asked to append (the fuller users repository belongs to the
auth/identity module being built in parallel).
apps/api/src/resellers/*: controller/service/module/dto mirroring the
Companies module's conventions - read open to any authenticated user,
write gated by @RequirePermission('resellers', 'edit'). Deliberate
deviation from legacy noted in code comments: the legacy hardcoded this
CRUD to requireAdmin and exposed reseller<->user assignment as
`PATCH /users/:id`; EDEN generalizes to the permission model (ADR-0004)
and adds `PATCH /v1/resellers/:id/assign-user` instead. Audits
reseller.created / reseller.updated / reseller.user_assigned.
packages/domain-shared/src/validators/br-documents.ts: CPF/CNPJ
digit-check validators (mirrors the sibling worktree's implementation
verbatim), used for the reseller `document` field the same way Companies
validates cnpj.
Known integration gap (by design, not a bug in this module): this
worktree doesn't yet have apps/api/src/auth/* or the companies-phase
exports in packages/database/src/index.ts - both are being built in
sibling worktrees and are explicitly out of scope here (per the task's
boundaries, app.module.ts and packages/database/src/index.ts are left
untouched for the user to wire up at merge time). @eden/database
typecheck/build pass cleanly; @eden/api typecheck/build currently fail
with exactly ten "cannot find module ../auth/..." /
"no exported member resellersRepo/usersRepo/auditRepo/Reseller" errors,
all attributable to those two integration points and nothing else.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
67 lines
2.3 KiB
TypeScript
67 lines
2.3 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { query } from "../pool.js";
|
|
|
|
export interface AuditEventInput {
|
|
actorType: "user" | "system" | "service";
|
|
actorId?: string | null;
|
|
action: string;
|
|
resourceType: string;
|
|
resourceId?: string | null;
|
|
metadata?: Record<string, unknown>;
|
|
ipAddress?: string | null;
|
|
}
|
|
|
|
function canonicalJson(value: unknown): string {
|
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
const obj = value as Record<string, unknown>;
|
|
const keys = Object.keys(obj).sort();
|
|
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
|
|
}
|
|
|
|
/**
|
|
* Appends one event to the audit_log hash chain (see baseline-schema
|
|
* migration and .claude/skills/eden-security/references/audit.md). Never
|
|
* call this inside a larger transaction that might roll back for
|
|
* unrelated reasons without considering that the chain must stay
|
|
* contiguous — a rolled-back event never happened (fine, nothing was
|
|
* recorded), but a *committed* event must never be skipped when
|
|
* computing previous_event_hash for the next one.
|
|
*/
|
|
export async function appendAuditEvent(input: AuditEventInput): Promise<void> {
|
|
const { rows } = await query<{ event_hash: string }>(
|
|
"SELECT event_hash FROM audit_log ORDER BY seq DESC LIMIT 1",
|
|
);
|
|
const previousEventHash = rows[0]?.event_hash ?? "";
|
|
|
|
const fields = {
|
|
actorType: input.actorType,
|
|
actorId: input.actorId ?? null,
|
|
action: input.action,
|
|
resourceType: input.resourceType,
|
|
resourceId: input.resourceId ?? null,
|
|
metadata: input.metadata ?? {},
|
|
ipAddress: input.ipAddress ?? null,
|
|
};
|
|
const eventHash = createHash("sha256")
|
|
.update(previousEventHash + canonicalJson(fields))
|
|
.digest("hex");
|
|
|
|
await query(
|
|
`INSERT INTO audit_log
|
|
(actor_type, actor_id, action, resource_type, resource_id, metadata, ip_address, previous_event_hash, event_hash)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
|
[
|
|
input.actorType,
|
|
input.actorId ?? null,
|
|
input.action,
|
|
input.resourceType,
|
|
input.resourceId ?? null,
|
|
JSON.stringify(input.metadata ?? {}),
|
|
input.ipAddress ?? null,
|
|
previousEventHash || null,
|
|
eventHash,
|
|
],
|
|
);
|
|
}
|