* Mask a batch of collected strings via Presidio. On a hard failure, either scrub * to REDACTION_FAILED_MARKER or throw PiiRedactionError, per * `options.onFailure`. Returns masked values aligned 1:1 with `collected`. * * There is no total-size ceiling — the batching layer chunks
( collected: string[], options: PiiRedactionOptions )
| 145 | * rather than scrubbed. The per-chunk request timeout is the real backstop. |
| 146 | */ |
| 147 | async function maskCollected( |
| 148 | collected: string[], |
| 149 | options: PiiRedactionOptions |
| 150 | ): Promise<{ masked: string[]; scrubbed: boolean }> { |
| 151 | const onFailure = options.onFailure ?? 'scrub' |
| 152 | const language = options.language ?? 'en' |
| 153 | |
| 154 | try { |
| 155 | // Presidio runs only in the app container; the persist + execution paths also |
| 156 | // run in the trigger.dev runtime, so masking always goes over HTTP to the app. |
| 157 | const masked = await maskPIIBatchViaHttp(collected, options.entityTypes, language) |
| 158 | return { masked, scrubbed: false } |
| 159 | } catch (error) { |
| 160 | logger.error('PII masking failed', { |
| 161 | error: getErrorMessage(error), |
| 162 | stringCount: collected.length, |
| 163 | onFailure, |
| 164 | }) |
| 165 | if (onFailure === 'throw') { |
| 166 | throw new PiiRedactionError(`PII redaction failed: ${getErrorMessage(error)}`) |
| 167 | } |
| 168 | return { masked: collected.map(() => REDACTION_FAILED_MARKER), scrubbed: true } |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * Mask every eligible string leaf of an arbitrary object in place-preserving |
no test coverage detected