import { MAX_ADDED_MASKS, maskValues, withAddedMask } from "./markers";
const MASKS_KEY = "masks";
const MASKING_UNAVAILABLE = "[masking unavailable; line hidden]";
const MAX_CARRY_CHARS = 16 * 1024;
const KEPT_CARRY_CHARS = 4 * 1024;
/**
* Splits a self-hosted log chunk into whole lines, carrying the unterminated tail into the next chunk so a
* secret split across two POSTs is still masked as one line (CI-P8). An overlong tail is flushed except for
* its end, which stays carried so a secret near the cut can still match.
*/
export function splitLogChunk(carry: string, text: string): { lines: string[]; carry: string } {
const lines = (carry + text).split("\n");
const tail = lines.pop() ?? "";
if (tail.length > MAX_CARRY_CHARS) {
return { lines: [...lines, tail.slice(0, -KEPT_CARRY_CHARS)], carry: tail.slice(-KEPT_CARRY_CHARS) };
}
return { lines, carry: tail };
}
/** Mask bookkeeping a Runner Durable Object keeps in its job meta. */
export type MaskState = { addedMasks?: number; pendingMasks?: string[] };
/**
* A job's log masks, persisted in Durable Object storage. Fails closed: while the list can't be loaded,
* every line is hidden, and `::add-mask::` values seen meanwhile wait in the meta until it loads.
*/
export class JobMasks {
private masks: string[] | null = null;
constructor(private readonly storage: DurableObjectStorage) {}
async store(values: string[]): Promise<void> {
this.masks = maskValues(values);
await this.storage.put(MASKS_KEY, this.masks);
}
/** Loads the stored list, or rebuilds it with `rebuild` (which may return null when it can't). */
async ensure(rebuild: () => Promise<string[] | null>, state: MaskState): Promise<void> {