199 lines
5.4 KiB
TypeScript
199 lines
5.4 KiB
TypeScript
import type { ExecAsk, ExecHost, ExecSecurity } from "../../../infra/exec-approvals.js";
|
|
|
|
type ExecDirectiveParse = {
|
|
cleaned: string;
|
|
hasDirective: boolean;
|
|
execHost?: ExecHost;
|
|
execSecurity?: ExecSecurity;
|
|
execAsk?: ExecAsk;
|
|
execNode?: string;
|
|
rawExecHost?: string;
|
|
rawExecSecurity?: string;
|
|
rawExecAsk?: string;
|
|
rawExecNode?: string;
|
|
hasExecOptions: boolean;
|
|
invalidHost: boolean;
|
|
invalidSecurity: boolean;
|
|
invalidAsk: boolean;
|
|
invalidNode: boolean;
|
|
};
|
|
|
|
function normalizeExecHost(value?: string): ExecHost | undefined {
|
|
const normalized = value?.trim().toLowerCase();
|
|
if (normalized === "sandbox" || normalized === "gateway" || normalized === "node") return normalized;
|
|
return undefined;
|
|
}
|
|
|
|
function normalizeExecSecurity(value?: string): ExecSecurity | undefined {
|
|
const normalized = value?.trim().toLowerCase();
|
|
if (normalized === "deny" || normalized === "allowlist" || normalized === "full") return normalized;
|
|
return undefined;
|
|
}
|
|
|
|
function normalizeExecAsk(value?: string): ExecAsk | undefined {
|
|
const normalized = value?.trim().toLowerCase();
|
|
if (normalized === "off" || normalized === "on-miss" || normalized === "always") {
|
|
return normalized as ExecAsk;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function parseExecDirectiveArgs(raw: string): Omit<ExecDirectiveParse, "cleaned" | "hasDirective"> & {
|
|
consumed: number;
|
|
} {
|
|
let i = 0;
|
|
const len = raw.length;
|
|
while (i < len && /\s/.test(raw[i])) i += 1;
|
|
if (raw[i] === ":") {
|
|
i += 1;
|
|
while (i < len && /\s/.test(raw[i])) i += 1;
|
|
}
|
|
let consumed = i;
|
|
let execHost: ExecHost | undefined;
|
|
let execSecurity: ExecSecurity | undefined;
|
|
let execAsk: ExecAsk | undefined;
|
|
let execNode: string | undefined;
|
|
let rawExecHost: string | undefined;
|
|
let rawExecSecurity: string | undefined;
|
|
let rawExecAsk: string | undefined;
|
|
let rawExecNode: string | undefined;
|
|
let hasExecOptions = false;
|
|
let invalidHost = false;
|
|
let invalidSecurity = false;
|
|
let invalidAsk = false;
|
|
let invalidNode = false;
|
|
|
|
const takeToken = (): string | null => {
|
|
if (i >= len) return null;
|
|
const start = i;
|
|
while (i < len && !/\s/.test(raw[i])) i += 1;
|
|
if (start === i) return null;
|
|
const token = raw.slice(start, i);
|
|
while (i < len && /\s/.test(raw[i])) i += 1;
|
|
return token;
|
|
};
|
|
|
|
const splitToken = (token: string): { key: string; value: string } | null => {
|
|
const eq = token.indexOf("=");
|
|
const colon = token.indexOf(":");
|
|
const idx =
|
|
eq === -1 ? colon : colon === -1 ? eq : Math.min(eq, colon);
|
|
if (idx === -1) return null;
|
|
const key = token.slice(0, idx).trim().toLowerCase();
|
|
const value = token.slice(idx + 1).trim();
|
|
if (!key) return null;
|
|
return { key, value };
|
|
};
|
|
|
|
while (i < len) {
|
|
const token = takeToken();
|
|
if (!token) break;
|
|
const parsed = splitToken(token);
|
|
if (!parsed) break;
|
|
const { key, value } = parsed;
|
|
if (key === "host") {
|
|
rawExecHost = value;
|
|
execHost = normalizeExecHost(value);
|
|
if (!execHost) invalidHost = true;
|
|
hasExecOptions = true;
|
|
consumed = i;
|
|
continue;
|
|
}
|
|
if (key === "security") {
|
|
rawExecSecurity = value;
|
|
execSecurity = normalizeExecSecurity(value);
|
|
if (!execSecurity) invalidSecurity = true;
|
|
hasExecOptions = true;
|
|
consumed = i;
|
|
continue;
|
|
}
|
|
if (key === "ask") {
|
|
rawExecAsk = value;
|
|
execAsk = normalizeExecAsk(value);
|
|
if (!execAsk) invalidAsk = true;
|
|
hasExecOptions = true;
|
|
consumed = i;
|
|
continue;
|
|
}
|
|
if (key === "node") {
|
|
rawExecNode = value;
|
|
const trimmed = value.trim();
|
|
if (!trimmed) {
|
|
invalidNode = true;
|
|
} else {
|
|
execNode = trimmed;
|
|
}
|
|
hasExecOptions = true;
|
|
consumed = i;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
return {
|
|
consumed,
|
|
execHost,
|
|
execSecurity,
|
|
execAsk,
|
|
execNode,
|
|
rawExecHost,
|
|
rawExecSecurity,
|
|
rawExecAsk,
|
|
rawExecNode,
|
|
hasExecOptions,
|
|
invalidHost,
|
|
invalidSecurity,
|
|
invalidAsk,
|
|
invalidNode,
|
|
};
|
|
}
|
|
|
|
export function extractExecDirective(body?: string): ExecDirectiveParse {
|
|
if (!body) {
|
|
return {
|
|
cleaned: "",
|
|
hasDirective: false,
|
|
hasExecOptions: false,
|
|
invalidHost: false,
|
|
invalidSecurity: false,
|
|
invalidAsk: false,
|
|
invalidNode: false,
|
|
};
|
|
}
|
|
const re = /(?:^|\s)\/exec(?=$|\s|:)/i;
|
|
const match = re.exec(body);
|
|
if (!match) {
|
|
return {
|
|
cleaned: body.trim(),
|
|
hasDirective: false,
|
|
hasExecOptions: false,
|
|
invalidHost: false,
|
|
invalidSecurity: false,
|
|
invalidAsk: false,
|
|
invalidNode: false,
|
|
};
|
|
}
|
|
const start = match.index + match[0].indexOf("/exec");
|
|
const argsStart = start + "/exec".length;
|
|
const parsed = parseExecDirectiveArgs(body.slice(argsStart));
|
|
const cleanedRaw = `${body.slice(0, start)} ${body.slice(argsStart + parsed.consumed)}`;
|
|
const cleaned = cleanedRaw.replace(/\s+/g, " ").trim();
|
|
return {
|
|
cleaned,
|
|
hasDirective: true,
|
|
execHost: parsed.execHost,
|
|
execSecurity: parsed.execSecurity,
|
|
execAsk: parsed.execAsk,
|
|
execNode: parsed.execNode,
|
|
rawExecHost: parsed.rawExecHost,
|
|
rawExecSecurity: parsed.rawExecSecurity,
|
|
rawExecAsk: parsed.rawExecAsk,
|
|
rawExecNode: parsed.rawExecNode,
|
|
hasExecOptions: parsed.hasExecOptions,
|
|
invalidHost: parsed.invalidHost,
|
|
invalidSecurity: parsed.invalidSecurity,
|
|
invalidAsk: parsed.invalidAsk,
|
|
invalidNode: parsed.invalidNode,
|
|
};
|
|
}
|