UNPKG

@accounter/server

Version:
83 lines (82 loc) 3.65 kB
import { type Injector } from 'graphql-modules'; import { CloudinaryProvider } from '../../app-providers/cloudinary.js'; import { DBProvider } from '../../app-providers/db.provider.js'; import { IngestOutcome } from '../contracts.js'; import { EmailIngestionControlProvider } from './email-ingestion-control.provider.js'; export type IngestInput = { grantJti: string; idempotencyKey: string; tenantId: string; messageId: string; rawMessageHash: string; correlationId?: string; extractedDocuments: Array<{ hash: string; sizeBytes: number; mimeType: string; filename?: string | null; /** Base64-encoded document bytes (inline transport); omitted = metadata only. */ content?: string | null; }>; }; export type IngestResult = { outcome: typeof IngestOutcome.INSERTED; ingestId: string; auditId: string; } | { outcome: typeof IngestOutcome.DUPLICATE; existingIngestId: string | null; auditId: string; } | { outcome: typeof IngestOutcome.QUARANTINED; auditId: string; reasonCode: string; } | { outcome: typeof IngestOutcome.REJECTED; reasonCode: string; }; /** * Orchestrates the v2 ingest flow for gateway-initiated ingestion. * * The caller presents a gateway_control_plane auth token with an empty * businessId, so TenantAwareDBClient (which derives the tenant from the auth * session) cannot be used here — the authoritative tenant comes from the * single-use, cryptographically-validated grant instead. To still get RLS as a * second defense layer, all tenant-bound reads/writes run inside one * transaction whose `app.current_business_id` is pinned to the grant tenant * (see {@link withTenantContext}); the `tenant_isolation` WITH CHECK policies * on the idempotency/dedup/quarantine tables then enforce * `owner_id = tenantId` on top of the explicit owner_id filters in each query. * * Flow: grant validation → idempotency check → dedup check → quarantine or insert. */ export declare class EmailIngestionIngestProvider { private dbProvider; private controlProvider; private cloudinaryProvider; constructor(dbProvider: DBProvider, controlProvider: EmailIngestionControlProvider, cloudinaryProvider: CloudinaryProvider); performIngest(input: IngestInput, injector: Injector): Promise<IngestResult>; /** * Prepare documents for persistence WITHOUT holding the write transaction open: * dedup new documents by hash (a short read), then upload to Cloudinary and OCR * (Anthropic) in parallel, outside any transaction. This mirrors the legacy * `getDocumentFromFile` path (Cloudinary upload + `getOcrData` + `figureOutSides`) * so v2 produces the same classified documents as `insertEmailDocuments`, but * owned by the grant tenant (the auth-coupled providers cannot run in the gateway * control-plane context). * * The hash matches the legacy `hashStringToInt(file.text())` scheme so the dedup * is consistent across both paths; re-deliveries short-circuit here and never * re-upload or re-OCR. Metadata-only entries (no inline bytes) yield an empty * result. OCR failure is non-fatal — the document falls back to UNPROCESSED * rather than failing the whole ingest. */ private prepareDocuments; /** * Insert already-prepared (uploaded + OCR'd) documents under one charge, owned * by the grant tenant. Runs entirely inside the caller's transaction with no * network I/O. Returns the created charge id. */ private insertPreparedDocuments; private persistIdempotencyAndDedup; }