import { isRecord, parseDotPath } from "./shared.js"; import type { SecretTargetRegistryEntry } from "./target-registry-types.js"; export type PathPatternToken = | { kind: "literal"; value: string } | { kind: "wildcard" } | { kind: "array"; field: string }; export type CompiledTargetRegistryEntry = SecretTargetRegistryEntry & { pathTokens: PathPatternToken[]; pathDynamicTokenCount: number; refPathTokens?: PathPatternToken[]; refPathDynamicTokenCount: number; }; export type ExpandedPathMatch = { segments: string[]; captures: string[]; value: unknown; }; function countDynamicPatternTokens(tokens: PathPatternToken[]): number { return tokens.filter((token) => token.kind === "wildcard" || token.kind === "array").length; } export function parsePathPattern(pathPattern: string): PathPatternToken[] { const segments = parseDotPath(pathPattern); return segments.map((segment) => { if (segment === "*") { return { kind: "wildcard" } as const; } if (segment.endsWith("[]")) { const field = segment.slice(0, -2).trim(); if (!field) { throw new Error(`Invalid target path pattern: ${pathPattern}`); } return { kind: "array", field } as const; } return { kind: "literal", value: segment } as const; }); } export function compileTargetRegistryEntry( entry: SecretTargetRegistryEntry, ): CompiledTargetRegistryEntry { const pathTokens = parsePathPattern(entry.pathPattern); const pathDynamicTokenCount = countDynamicPatternTokens(pathTokens); const refPathTokens = entry.refPathPattern ? parsePathPattern(entry.refPathPattern) : undefined; const refPathDynamicTokenCount = refPathTokens ? countDynamicPatternTokens(refPathTokens) : 0; const requiresSiblingRefPath = entry.secretShape === "sibling_ref"; // pragma: allowlist secret if (requiresSiblingRefPath && !refPathTokens) { throw new Error(`Missing refPathPattern for sibling_ref target: ${entry.id}`); } if (refPathTokens && refPathDynamicTokenCount !== pathDynamicTokenCount) { throw new Error(`Mismatched wildcard shape for target ref path: ${entry.id}`); } return { ...entry, pathTokens, pathDynamicTokenCount, refPathTokens, refPathDynamicTokenCount, }; } export function matchPathTokens( segments: string[], tokens: PathPatternToken[], ): { captures: string[]; } | null { const captures: string[] = []; let index = 0; for (const token of tokens) { if (token.kind === "literal") { if (segments[index] !== token.value) { return null; } index += 1; continue; } if (token.kind === "wildcard") { const value = segments[index]; if (!value) { return null; } captures.push(value); index += 1; continue; } if (segments[index] !== token.field) { return null; } const next = segments[index + 1]; if (!next || !/^\d+$/.test(next)) { return null; } captures.push(next); index += 2; } return index === segments.length ? { captures } : null; } export function materializePathTokens( tokens: PathPatternToken[], captures: string[], ): string[] | null { const out: string[] = []; let captureIndex = 0; for (const token of tokens) { if (token.kind === "literal") { out.push(token.value); continue; } if (token.kind === "wildcard") { const value = captures[captureIndex]; if (!value) { return null; } out.push(value); captureIndex += 1; continue; } const arrayIndex = captures[captureIndex]; if (!arrayIndex || !/^\d+$/.test(arrayIndex)) { return null; } out.push(token.field, arrayIndex); captureIndex += 1; } return captureIndex === captures.length ? out : null; } export function expandPathTokens(root: unknown, tokens: PathPatternToken[]): ExpandedPathMatch[] { const out: ExpandedPathMatch[] = []; const walk = ( node: unknown, tokenIndex: number, segments: string[], captures: string[], ): void => { const token = tokens[tokenIndex]; if (!token) { out.push({ segments, captures, value: node }); return; } const isLeaf = tokenIndex === tokens.length - 1; if (token.kind === "literal") { if (!isRecord(node)) { return; } if (isLeaf) { out.push({ segments: [...segments, token.value], captures, value: node[token.value], }); return; } if (!Object.prototype.hasOwnProperty.call(node, token.value)) { return; } walk(node[token.value], tokenIndex + 1, [...segments, token.value], captures); return; } if (token.kind === "wildcard") { if (!isRecord(node)) { return; } for (const [key, value] of Object.entries(node)) { if (isLeaf) { out.push({ segments: [...segments, key], captures: [...captures, key], value, }); continue; } walk(value, tokenIndex + 1, [...segments, key], [...captures, key]); } return; } if (!isRecord(node)) { return; } const items = node[token.field]; if (!Array.isArray(items)) { return; } for (let index = 0; index < items.length; index += 1) { const item = items[index]; const indexString = String(index); if (isLeaf) { out.push({ segments: [...segments, token.field, indexString], captures: [...captures, indexString], value: item, }); continue; } walk( item, tokenIndex + 1, [...segments, token.field, indexString], [...captures, indexString], ); } }; walk(root, 0, [], []); return out; }