* Mask PII from log content before persistence when the execution's workspace * (via workspace override or org default) has enterprise PII redaction enabled. * Resolved at persist time so both the inline and externalized write paths are * covered. Returns the payload unchanged when disabled
(
workspaceId: string | null,
payload: RedactablePayload,
storeContext: { workflowId?: string | null; executionId: string; userId?: string | null }
)
| 617 | * covered. Returns the payload unchanged when disabled or non-enterprise. |
| 618 | */ |
| 619 | private async applyPiiRedaction( |
| 620 | workspaceId: string | null, |
| 621 | payload: RedactablePayload, |
| 622 | storeContext: { workflowId?: string | null; executionId: string; userId?: string | null } |
| 623 | ): Promise<RedactablePayload> { |
| 624 | if (!workspaceId) return payload |
| 625 | |
| 626 | const [row] = await db |
| 627 | .select({ orgSettings: organization.dataRetentionSettings }) |
| 628 | .from(workspace) |
| 629 | .leftJoin(organization, eq(organization.id, workspace.organizationId)) |
| 630 | .where(eq(workspace.id, workspaceId)) |
| 631 | .limit(1) |
| 632 | if (!row) return payload |
| 633 | |
| 634 | // Resolve from stored rules UNCONDITIONALLY — deliberately NOT gated on the |
| 635 | // `pii-redaction` feature flag or the enterprise-plan check. Rules are only |
| 636 | // writable by entitled orgs (route-gated), so their presence is the source of |
| 637 | // truth; re-checking the flag/plan here returns false on a transient read and |
| 638 | // would silently skip masking, leaking PII (fail-open). Absence of rules |
| 639 | // yields the disabled default, so non-PII orgs incur only the lookup. |
| 640 | const config = resolveEffectivePiiRedaction({ orgSettings: row.orgSettings, workspaceId }).logs |
| 641 | if (!config.enabled) return payload |
| 642 | |
| 643 | // The string redactor can't reach values already offloaded to large-value |
| 644 | // storage (>8MB refs). Always hydrate → mask → re-store them under the LOGS |
| 645 | // policy, even if the block-output stage already masked before offload: that |
| 646 | // used the block-output entity set, which can differ from the logs set, so |
| 647 | // the log's large values must get the logs policy applied like inline content |
| 648 | // does. Masking is idempotent, so already-masked spans are unaffected; a ref |
| 649 | // that can't be materialized/re-stored falls back to a marker. |
| 650 | const working = await redactLargeValueRefs(payload, { |
| 651 | entityTypes: config.entityTypes, |
| 652 | language: config.language, |
| 653 | store: { |
| 654 | workspaceId, |
| 655 | workflowId: storeContext.workflowId ?? undefined, |
| 656 | executionId: storeContext.executionId, |
| 657 | userId: storeContext.userId ?? undefined, |
| 658 | }, |
| 659 | }) |
| 660 | |
| 661 | return redactPIIFromExecution(working, { |
| 662 | entityTypes: config.entityTypes, |
| 663 | language: config.language, |
| 664 | }) |
| 665 | } |
| 666 | |
| 667 | async completeWorkflowExecution(params: { |
| 668 | executionId: string |
no test coverage detected