UNPKG

@accounter/server

Version:
99 lines (98 loc) 4.24 kB
import { DBProvider } from '../../app-providers/db.provider.js'; import { type EmailListenerConfig } from '../../financial-entities/helpers/business-suggestion-data-schema.helper.js'; import { IngestReasonCode } from '../contracts.js'; export type AliasResolutionResult = { found: false; reason: typeof IngestReasonCode.UNKNOWN_ALIAS; } | { found: true; tenantId: string; }; export type IssuedGrant = { jti: string; tenantId: string; messageId: string; rawMessageHash: string; action: string; expiresAt: Date; decisionId: string; auditId: string; }; export type IssueGrantInput = { tenantId: string; messageId: string; rawMessageHash: string; expiresAt: Date; correlationId?: string; /** Recognized issuing business, bound into the grant for the ingest step. */ businessId?: string | null; }; export type BusinessRecognitionResult = { /** The recognized issuing business, or null when no business matched. */ businessId: string | null; /** The business's email-processing config (empty when unrecognized). */ config: EmailListenerConfig; }; export type ValidateGrantInput = { jti: string; tenantId: string; messageId: string; rawMessageHash: string; }; export type ValidatedGrant = { jti: string; tenantId: string; action: string; expiresAt: Date; /** Recognized issuing business bound at control time; null when unrecognized. */ businessId: string | null; }; export type GrantValidationResult = { valid: true; grant: ValidatedGrant; } | { valid: false; reason: typeof IngestReasonCode.GRANT_INVALID | typeof IngestReasonCode.TENANT_MISMATCH; }; export declare class EmailIngestionControlProvider { private dbProvider; constructor(dbProvider: DBProvider); /** * Resolve a recipient alias to the owning tenant. * Bypasses RLS via raw pool: alias lookup is a bootstrap step that runs * before any tenant context is known, so TenantAwareDBClient would throw * UNAUTHENTICATED. The alias_routing table has FOR SELECT USING (TRUE) to * explicitly allow cross-tenant reads at the DB policy level. */ resolveAlias(alias: string): Promise<AliasResolutionResult>; /** * Issue a short-lived, single-use ingest grant for the given tenant and message. * Returns the persisted grant together with decision/audit metadata. * The INSERT runs under the tenant's RLS context (see {@link withTenantContext}): * the grants table uses FORCE ROW LEVEL SECURITY with a tenant_isolation * WITH CHECK policy, so the raw pool cannot bypass it — the owner_id parameter * and the pinned business context must agree. */ issueGrant(input: IssueGrantInput): Promise<IssuedGrant>; /** * Recognize the issuing business behind an incoming email and load its * email-processing config. Runs on a client pinned to the resolved tenant so * the businesses RLS policy scopes the lookup to that tenant; returns a null * businessId (and empty config) when no email evidence is available or no * business matches, in which case the gateway applies default treatment. */ recognizeBusiness(tenantId: string, issuerEmail: string | null): Promise<BusinessRecognitionResult>; /** * Validate a presented grant against the stored record and atomically consume it. * Checks: existence, expiry, consumed state, action scope, tenant binding, and message binding. * The consume UPDATE (SET consumed_at = NOW() WHERE consumed_at IS NULL) is atomic — * if a concurrent request consumed the grant first the UPDATE returns 0 rows and * the method returns GRANT_INVALID, preventing double-use. * Runs under the claimed tenant's RLS context: the grants table uses FORCE ROW * LEVEL SECURITY, so the raw pool cannot read/update it without a pinned * business context. Pinning to input.tenantId means a grant owned by another * tenant is filtered out by the USING policy and surfaces as GRANT_INVALID; * the explicit owner_id check below remains as defense-in-depth. */ validateAndConsumeGrant(input: ValidateGrantInput): Promise<GrantValidationResult>; }