From 6eeedc3ee45439cd13dca66fb841ca31aec544dd Mon Sep 17 00:00:00 2001 From: sabahattink Date: Fri, 30 Oct 2026 16:59:12 +0400 Subject: [PATCH 1/4] feat(trpc): recognize auth in front of every procedure Mutations are not reported as missing auth middleware when the whole router is authenticated before procedures run: - a createContext (or a project function it calls) that throws TRPCError UNAUTHORIZED/FORBIDDEN or throws when the session is missing; T3's createTRPCContext, which only reads the session, does not count; - auth middleware ahead of the Express mount that serves the adapter, in the same use() call or an earlier global or enclosing-path use(); - a Next.js middleware.ts (proxy.ts in Next.js 16) that rejects anonymous requests (next-auth/middleware, Clerk auth.protect() under a route matcher, or a redirect or 401/403 when the session is missing) and whose matcher covers the tRPC route. Matchers are tested against the route path only, so the common /((?!api|...).*) matcher that skips the API does not count. The API Health summary names where the perimeter was found. --- dist/action.cjs | 874 ++++++++++++++++++++------------ src/analyzers/trpc-api.ts | 7 +- src/analyzers/trpc-perimeter.ts | 411 +++++++++++++++ src/analyzers/trpc-syntax.ts | 3 +- tests/trpc-api.test.ts | 236 +++++++++ 5 files changed, 1208 insertions(+), 323 deletions(-) create mode 100644 src/analyzers/trpc-perimeter.ts diff --git a/dist/action.cjs b/dist/action.cjs index 5d96339..621c642 100644 --- a/dist/action.cjs +++ b/dist/action.cjs @@ -1037,7 +1037,7 @@ var require_typescript = __commonJS({ SymbolDisplayPartKind: () => SymbolDisplayPartKind, SymbolFlags: () => SymbolFlags, SymbolFormatFlags: () => SymbolFormatFlags, - SyntaxKind: () => SyntaxKind17, + SyntaxKind: () => SyntaxKind18, Ternary: () => Ternary, ThrottledCancellationToken: () => ThrottledCancellationToken, TokenClass: () => TokenClass, @@ -4928,7 +4928,7 @@ Node ${formatSyntaxKind(node.kind)} was unexpected.`, function formatSyntaxKind(kind) { return formatEnum( kind, - SyntaxKind17, + SyntaxKind18, /*isFlags*/ false ); @@ -6376,7 +6376,7 @@ ${lanes.join("\n")} })(tracingEnabled || (tracingEnabled = {})); var startTracing = tracingEnabled.startTracing; var dumpTracingLegend = tracingEnabled.dumpLegend; - var SyntaxKind17 = /* @__PURE__ */ ((SyntaxKind52) => { + var SyntaxKind18 = /* @__PURE__ */ ((SyntaxKind52) => { SyntaxKind52[SyntaxKind52["Unknown"] = 0] = "Unknown"; SyntaxKind52[SyntaxKind52["EndOfFileToken"] = 1] = "EndOfFileToken"; SyntaxKind52[SyntaxKind52["SingleLineCommentTrivia"] = 2] = "SingleLineCommentTrivia"; @@ -6882,7 +6882,7 @@ ${lanes.join("\n")} /* LastKeyword */ ] = "LastContextualKeyword"; return SyntaxKind52; - })(SyntaxKind17 || {}); + })(SyntaxKind18 || {}); var NodeFlags = /* @__PURE__ */ ((NodeFlags3) => { NodeFlags3[NodeFlags3["None"] = 0] = "None"; NodeFlags3[NodeFlags3["Let"] = 1] = "Let"; @@ -79200,7 +79200,7 @@ ${lanes.join("\n")} const targetEndText = targetTexts[lastTargetIndex]; if (lastSourceIndex === 0 && sourceStartText.length < targetStartText.length + targetEndText.length || !sourceStartText.startsWith(targetStartText) || !sourceEndText.endsWith(targetEndText)) return void 0; const remainingEndText = sourceEndText.slice(0, sourceEndText.length - targetEndText.length); - const matches2 = []; + const matches3 = []; let seg = 0; let pos = targetStartText.length; for (let i = 1; i < lastTargetIndex; i++) { @@ -79226,7 +79226,7 @@ ${lanes.join("\n")} } } addMatch(lastSourceIndex, getSourceText(lastSourceIndex).length); - return matches2; + return matches3; function getSourceText(index) { return index < lastSourceIndex ? sourceTexts[index] : remainingEndText; } @@ -79235,7 +79235,7 @@ ${lanes.join("\n")} [sourceTexts[seg].slice(pos), ...sourceTexts.slice(seg + 1, s), getSourceText(s).slice(0, p)], sourceTypes.slice(seg, s) ); - matches2.push(matchType); + matches3.push(matchType); seg = s; pos = p; } @@ -79481,12 +79481,12 @@ ${lanes.join("\n")} visited.set(key, inferencePriority); inferencePriority = Math.min(inferencePriority, saveInferencePriority); } - function inferFromMatchingTypes(sources, targets, matches2) { + function inferFromMatchingTypes(sources, targets, matches3) { let matchedSources; let matchedTargets; for (const t of targets) { for (const s of sources) { - if (matches2(s, t)) { + if (matches3(s, t)) { inferFromTypes(s, t); matchedSources = appendIfUnique(matchedSources, s); matchedTargets = appendIfUnique(matchedTargets, t); @@ -79662,11 +79662,11 @@ ${lanes.join("\n")} } } function inferToTemplateLiteralType(source, target) { - const matches2 = inferTypesFromTemplateLiteralType(source, target); + const matches3 = inferTypesFromTemplateLiteralType(source, target); const types2 = target.types; - if (matches2 || every(target.texts, (s) => s.length === 0)) { + if (matches3 || every(target.texts, (s) => s.length === 0)) { for (let i = 0; i < types2.length; i++) { - const source2 = matches2 ? matches2[i] : neverType; + const source2 = matches3 ? matches3[i] : neverType; const target2 = types2[i]; if (source2.flags & 1024 && target2.flags & 34078720) { const inferenceContext = getInferenceInfoForType(target2); @@ -150015,23 +150015,23 @@ ${lanes.join("\n")} return 4; } if (supportScopedPackage) { - const matches2 = /^@([^/]+)\/([^/]+)$/.exec(packageName2); - if (matches2) { + const matches3 = /^@([^/]+)\/([^/]+)$/.exec(packageName2); + if (matches3) { const scopeResult = validatePackageNameWorker( - matches2[1], + matches3[1], /*supportScopedPackage*/ false ); if (scopeResult !== 0) { - return { name: matches2[1], isScopeName: true, result: scopeResult }; + return { name: matches3[1], isScopeName: true, result: scopeResult }; } const packageResult = validatePackageNameWorker( - matches2[2], + matches3[2], /*supportScopedPackage*/ false ); if (packageResult !== 0) { - return { name: matches2[2], isScopeName: false, result: packageResult }; + return { name: matches3[2], isScopeName: false, result: packageResult }; } return 0; } @@ -153343,12 +153343,12 @@ ${lanes.join("\n")} const result = exportInfo.get(key2); return result == null ? void 0 : result.map(rehydrateCachedInfo); }, - search: (importingFile, preferCapitalized, matches2, action) => { + search: (importingFile, preferCapitalized, matches3, action) => { if (importingFile !== usableByFileName) return; return forEachEntry(exportInfo, (info, key2) => { const { symbolName: symbolName2, ambientModuleName } = parseKey(key2); const name = preferCapitalized && info[0].capitalizedSymbolName || symbolName2; - if (matches2(name, info[0].targetFlags)) { + if (matches3(name, info[0].targetFlags)) { const rehydrated = info.map(rehydrateCachedInfo); const filtered = rehydrated.filter((r, i) => isNotShadowedByDeeperNodeModulesPackage(r, info[i].packageName)); if (filtered.length) { @@ -183904,23 +183904,23 @@ ${newComment.split("\n").map((c) => ` * ${c}`).join("\n")} ].filter(isString); const includeGlobs = normalizedSuffix ? matchingSuffixes.map((suffix) => "**/*" + suffix) : ["./*"]; const isExportsOrImportsWildcard = (isExports || isImports) && endsWith(pattern, "/*"); - let matches2 = getMatchesWithPrefix(baseDirectory2); + let matches3 = getMatchesWithPrefix(baseDirectory2); if (possibleInputBaseDirectoryForOutDir) { - matches2 = concatenate(matches2, getMatchesWithPrefix(possibleInputBaseDirectoryForOutDir)); + matches3 = concatenate(matches3, getMatchesWithPrefix(possibleInputBaseDirectoryForOutDir)); } if (possibleInputBaseDirectoryForDeclarationDir) { - matches2 = concatenate(matches2, getMatchesWithPrefix(possibleInputBaseDirectoryForDeclarationDir)); + matches3 = concatenate(matches3, getMatchesWithPrefix(possibleInputBaseDirectoryForDeclarationDir)); } if (!normalizedSuffix) { - matches2 = concatenate(matches2, getDirectoryMatches(baseDirectory2)); + matches3 = concatenate(matches3, getDirectoryMatches(baseDirectory2)); if (possibleInputBaseDirectoryForOutDir) { - matches2 = concatenate(matches2, getDirectoryMatches(possibleInputBaseDirectoryForOutDir)); + matches3 = concatenate(matches3, getDirectoryMatches(possibleInputBaseDirectoryForOutDir)); } if (possibleInputBaseDirectoryForDeclarationDir) { - matches2 = concatenate(matches2, getDirectoryMatches(possibleInputBaseDirectoryForDeclarationDir)); + matches3 = concatenate(matches3, getDirectoryMatches(possibleInputBaseDirectoryForDeclarationDir)); } } - return matches2; + return matches3; function getMatchesWithPrefix(directory) { const completePrefix = fragmentHasPath ? directory : ensureTrailingDirectorySeparator(directory) + normalizedPrefixBase; return mapDefined(tryReadDirectory( @@ -197288,7 +197288,7 @@ ${options.prefix}` : "\n" : options.prefix SymbolDisplayPartKind: () => SymbolDisplayPartKind, SymbolFlags: () => SymbolFlags, SymbolFormatFlags: () => SymbolFormatFlags, - SyntaxKind: () => SyntaxKind17, + SyntaxKind: () => SyntaxKind18, Ternary: () => Ternary, ThrottledCancellationToken: () => ThrottledCancellationToken, TokenClass: () => TokenClass, @@ -209692,8 +209692,8 @@ Project '${project.projectName}' (${ProjectKind[project.projectKind]}) ${counter return searchForDeclaration(nameToSearch, fileToSearch, noDtsProgram); } function searchForDeclaration(declarationName, fileToSearch, noDtsProgram) { - const matches2 = ts_FindAllReferences_exports.Core.getTopMostDeclarationNamesInFile(declarationName, fileToSearch); - return mapDefined(matches2, (match2) => { + const matches3 = ts_FindAllReferences_exports.Core.getTopMostDeclarationNamesInFile(declarationName, fileToSearch); + return mapDefined(matches3, (match2) => { const symbol = noDtsProgram.getTypeChecker().getSymbolAtLocation(match2); const decl = getDeclarationFromName(match2); if (symbol && decl) { @@ -217262,9 +217262,9 @@ var require_dist2 = __commonJS({ const crawler = new fdir.fdir({ filters: [debug ? (p, isDirectory) => { const path3 = format(p, isDirectory); - const matches2 = matcher(path3) && !ignore2(path3); - if (matches2) log(`matched ${path3}`); - return matches2; + const matches3 = matcher(path3) && !ignore2(path3); + if (matches3) log(`matched ${path3}`); + return matches3; } : (p, isDirectory) => { const path3 = format(p, isDirectory); return matcher(path3) && !ignore2(path3); @@ -221561,7 +221561,7 @@ var require_ts_morph = __commonJS({ const nameNode = node.getNameNode(); if (nameNode == null) return false; - if (Node20.isArrayBindingPattern(nameNode) || Node20.isObjectBindingPattern(nameNode)) + if (Node21.isArrayBindingPattern(nameNode) || Node21.isObjectBindingPattern(nameNode)) return nameNode.getElements().some((element) => nodeHasName(element, name)); const nodeName = node.getName != null ? node.getName() : nameNode.getText(); return nodeName === name; @@ -221991,7 +221991,7 @@ var require_ts_morph = __commonJS({ } function checkNodeIsAmbient(node) { const isThisAmbient = (node.getCombinedModifierFlags() & common.ts.ModifierFlags.Ambient) === common.ts.ModifierFlags.Ambient; - return isThisAmbient || Node20.isInterfaceDeclaration(node) || Node20.isTypeAliasDeclaration(node); + return isThisAmbient || Node21.isInterfaceDeclaration(node) || Node21.isTypeAliasDeclaration(node); } function isStringKind(kind) { switch (kind) { @@ -222392,15 +222392,15 @@ var require_ts_morph = __commonJS({ #getSourceFileForLiteral(literal) { const parent = literal.getParentOrThrow(); const grandParent = parent.getParent(); - if (Node20.isImportDeclaration(parent) || Node20.isExportDeclaration(parent)) + if (Node21.isImportDeclaration(parent) || Node21.isExportDeclaration(parent)) return parent.getModuleSpecifierSourceFile(); - else if (grandParent != null && Node20.isImportEqualsDeclaration(grandParent)) + else if (grandParent != null && Node21.isImportEqualsDeclaration(grandParent)) return grandParent.getExternalModuleReferenceSourceFile(); - else if (grandParent != null && Node20.isImportTypeNode(grandParent)) { + else if (grandParent != null && Node21.isImportTypeNode(grandParent)) { const importTypeSymbol = grandParent.getSymbol(); if (importTypeSymbol != null) return ModuleUtils.getReferencedSourceFileFromSymbol(importTypeSymbol); - } else if (Node20.isCallExpression(parent)) { + } else if (Node21.isCallExpression(parent)) { const literalSymbol = literal.getSymbol(); if (literalSymbol != null) return ModuleUtils.getReferencedSourceFileFromSymbol(literalSymbol); @@ -222492,16 +222492,16 @@ var require_ts_morph = __commonJS({ FormattingKind2[FormattingKind2["None"] = 3] = "None"; })(FormattingKind || (FormattingKind = {})); function getClassMemberFormatting(parent, member) { - if (Node20.isAmbientable(parent) && parent.isAmbient()) + if (Node21.isAmbientable(parent) && parent.isAmbient()) return FormattingKind.Newline; if (hasBody$1(member)) return FormattingKind.Blankline; return FormattingKind.Newline; } function hasBody$1(node) { - if (Node20.isBodyable(node) && node.getBody() != null) + if (Node21.isBodyable(node) && node.getBody() != null) return true; - if (Node20.isBodied(node)) + if (Node21.isBodied(node)) return true; return false; } @@ -222523,11 +222523,11 @@ var require_ts_morph = __commonJS({ return FormattingKind.Newline; } function hasBody(node) { - if (Node20.isBodyable(node) && node.hasBody()) + if (Node21.isBodyable(node) && node.hasBody()) return true; - if (Node20.isBodied(node)) + if (Node21.isBodied(node)) return true; - return Node20.isInterfaceDeclaration(node) || Node20.isClassDeclaration(node) || Node20.isEnumDeclaration(node); + return Node21.isInterfaceDeclaration(node) || Node21.isClassDeclaration(node) || Node21.isEnumDeclaration(node); } function getStatementedNodeChildFormatting(parent, member) { if (hasBody(member)) @@ -222538,9 +222538,9 @@ var require_ts_morph = __commonJS({ return FormattingKind.Newline; } function getGeneralFormatting(parent, child) { - if (Node20.isClassDeclaration(parent)) + if (Node21.isClassDeclaration(parent)) return getClassMemberFormatting(parent, child); - if (Node20.isInterfaceDeclaration(parent)) + if (Node21.isInterfaceDeclaration(parent)) return getInterfaceMemberFormatting(); return getStatementedNodeChildFormatting(parent, child); } @@ -222714,9 +222714,9 @@ var require_ts_morph = __commonJS({ function getInsertPosFromIndex(index, syntaxList, children) { if (index === 0) { const parent = syntaxList.getParentOrThrow(); - if (Node20.isSourceFile(parent)) + if (Node21.isSourceFile(parent)) return 0; - else if (Node20.isCaseClause(parent) || Node20.isDefaultClause(parent)) { + else if (Node21.isCaseClause(parent) || Node21.isDefaultClause(parent)) { const colonToken = parent.getFirstChildByKindOrThrow(common.SyntaxKind.ColonToken); return colonToken.getEnd(); } @@ -222733,9 +222733,9 @@ var require_ts_morph = __commonJS({ function getEndPosFromIndex(index, parent, children, fullText) { let endPos; if (index === children.length) { - if (Node20.isSourceFile(parent)) + if (Node21.isSourceFile(parent)) endPos = parent.getEnd(); - else if (Node20.isCaseClause(parent) || Node20.isDefaultClause(parent)) + else if (Node21.isCaseClause(parent) || Node21.isDefaultClause(parent)) endPos = parent.getEnd(); else { const parentContainer = getParentContainerOrThrow(parent); @@ -222751,14 +222751,14 @@ var require_ts_morph = __commonJS({ return getPosAtStartOfLineOrNonWhitespace(fullText, endPos); } function getParentContainerOrThrow(parent) { - if (Node20.isModuleDeclaration(parent)) { + if (Node21.isModuleDeclaration(parent)) { const innerBody = parent._getInnerBody(); if (innerBody == null) throw new common.errors.InvalidOperationError("This operation requires the module to have a body."); return innerBody; - } else if (Node20.isBodied(parent)) + } else if (Node21.isBodied(parent)) return parent.getBody(); - else if (Node20.isBodyable(parent)) + else if (Node21.isBodyable(parent)) return parent.getBodyOrThrow(); else return parent; @@ -222805,7 +222805,7 @@ var require_ts_morph = __commonJS({ const result = []; for (let i = 0; i < newLength; i++) { const currentChild = newChildren[index + i]; - if (allowCommentNodes || !Node20.isCommentNode(currentChild)) + if (allowCommentNodes || !Node21.isCommentNode(currentChild)) result.push(currentChild); } return result; @@ -223258,7 +223258,7 @@ var require_ts_morph = __commonJS({ this.#handler = handler; } handleNode(currentNode, newNode, newSourceFile) { - if (!Node20.isSourceFile(currentNode)) + if (!Node21.isSourceFile(currentNode)) throw new common.errors.InvalidOperationError(`Can only use a TryOrForgetNodeHandler with a source file.`); try { this.#handler.handleNode(currentNode, newNode, newSourceFile); @@ -223414,7 +223414,7 @@ var require_ts_morph = __commonJS({ const lowerIndex = Math.min(newIndex, oldIndex); const upperIndex = Math.max(newIndex, oldIndex); const childrenInNewOrder = getChildrenInNewOrder(); - const isParentSourceFile = Node20.isSourceFile(parent.getParentOrThrow()); + const isParentSourceFile = Node21.isSourceFile(parent.getParentOrThrow()); let finalText = ""; fillPrefixText(); fillTextForIndex(lowerIndex); @@ -223741,14 +223741,14 @@ var require_ts_morph = __commonJS({ const bodyNode = getBodyNodeOrThrow(); return [bodyNode.getStart() + 1, bodyNode.getEnd() - 1]; function getBodyNodeOrThrow() { - if (Node20.isModuleDeclaration(node)) { + if (Node21.isModuleDeclaration(node)) { const bodyNode2 = node._getInnerBody(); if (bodyNode2 == null) throw new common.errors.InvalidOperationError("This operation requires the module to have a body."); return bodyNode2; - } else if (Node20.isBodied(node)) + } else if (Node21.isBodied(node)) return node.getBody(); - else if (Node20.isBodyable(node)) + else if (Node21.isBodyable(node)) return node.getBodyOrThrow(); else common.errors.throwNotImplementedForSyntaxKindError(node.getKind(), node); @@ -223923,14 +223923,14 @@ Path: ` + sourceFile.getFilePath() + "\nText: " + JSON.stringify(textManipulator } function getPreviousNonCommentNode() { for (let i = insertIndex - 1; i >= 0; i--) { - if (!Node20.isCommentNode(currentNodes[i])) + if (!Node21.isCommentNode(currentNodes[i])) return currentNodes[i]; } return void 0; } function getNextNonCommentNode() { for (let i = insertIndex; i < currentNodes.length; i++) { - if (!Node20.isCommentNode(currentNodes[i])) + if (!Node21.isCommentNode(currentNodes[i])) return currentNodes[i]; } return void 0; @@ -223982,7 +223982,7 @@ Path: ` + sourceFile.getFilePath() + "\nText: " + JSON.stringify(textManipulator function getChild(child) { if (child == null) return child; - else if (Node20.isOverloadable(child)) + else if (Node21.isOverloadable(child)) return child.getImplementation() || child; else return child; @@ -224013,7 +224013,7 @@ Path: ` + sourceFile.getFilePath() + "\nText: " + JSON.stringify(textManipulator let count = 0; for (let i = index - 1; i >= 0; i--) { const node = startChildren[i]; - if (Node20.isCommentNode(node)) { + if (Node21.isCommentNode(node)) { commentCount++; if (node.getText().startsWith("/**")) count = commentCount; @@ -224083,12 +224083,12 @@ Path: ` + sourceFile.getFilePath() + "\nText: " + JSON.stringify(textManipulator })); } function removeClassMember(classMember) { - if (Node20.isOverloadable(classMember)) { + if (Node21.isOverloadable(classMember)) { if (classMember.isImplementation()) removeClassMembers([...classMember.getOverloads(), classMember]); else { const parent = classMember.getParentOrThrow(); - if (Node20.isAmbientable(parent) && parent.isAmbient()) + if (Node21.isAmbientable(parent) && parent.isAmbient()) removeClassMembers([classMember]); else removeChildren({ children: [classMember], removeFollowingSpaces: true, removeFollowingNewLines: true }); @@ -224409,7 +224409,7 @@ Path: ` + sourceFile.getFilePath() + "\nText: " + JSON.stringify(textManipulator return this.compilerObject.kind; } }; - var Node20 = class _Node { + var Node21 = class _Node { #compilerNode; #forgottenText; #childStringRanges; @@ -227108,7 +227108,7 @@ Node text: ${this.#forgottenText}`; return condition == null ? void 0 : ((c) => condition(thisNode._getNodeFromCompilerNode(c))); } function insertWhiteSpaceTextAtPos(node, insertPos, textOrWriterFunction, methodName2) { - const parent = Node20.isSourceFile(node) ? node.getChildSyntaxListOrThrow() : node.getParentSyntaxList() || node.getParentOrThrow(); + const parent = Node21.isSourceFile(node) ? node.getChildSyntaxListOrThrow() : node.getParentSyntaxList() || node.getParentOrThrow(); const newText = getTextFromStringOrWriter(node._getWriterWithQueuedIndentation(), textOrWriterFunction); if (!/^[\s\r\n]*$/.test(newText)) throw new common.errors.InvalidOperationError(`Cannot insert non-whitespace into ${methodName2}.`); @@ -227151,7 +227151,7 @@ Node text: ${this.#forgottenText}`; Scope["Protected"] = "protected"; Scope["Private"] = "private"; })(exports2.Scope || (exports2.Scope = {})); - var SyntaxList = class extends Node20 { + var SyntaxList = class extends Node21 { addChildText(textOrWriterFunction) { return this.insertChildText(this.getChildCount(), textOrWriterFunction); } @@ -227170,12 +227170,12 @@ Node text: ${this.#forgottenText}`; else insertText = " " + insertText; } else { - if (index === 0 && Node20.isSourceFile(parent)) { + if (index === 0 && Node21.isSourceFile(parent)) { if (!insertText.endsWith("\n")) insertText += newLineKind; } else { insertText = newLineKind + insertText; - if (!Node20.isSourceFile(parent) && index === initialChildCount && insertText.endsWith("\n")) + if (!Node21.isSourceFile(parent) && index === initialChildCount && insertText.endsWith("\n")) insertText = insertText.replace(/\r?\n$/, ""); } } @@ -227424,7 +227424,7 @@ Node text: ${this.#forgottenText}`; if (value === hasExclamationToken) return this; if (value) { - if (Node20.isQuestionTokenable(this)) + if (Node21.isQuestionTokenable(this)) this.setHasQuestionToken(false); const colonNode = this.getFirstChildByKind(common.SyntaxKind.ColonToken); if (colonNode == null) @@ -227458,11 +227458,11 @@ Node text: ${this.#forgottenText}`; return this.getExportKeyword() != null; } getExportKeyword() { - if (Node20.isVariableDeclaration(this)) { + if (Node21.isVariableDeclaration(this)) { const variableStatement = this.getVariableStatement(); return variableStatement?.getExportKeyword(); } - if (!Node20.isModifierable(this)) + if (!Node21.isModifierable(this)) return throwForNotModifierableNode(); return this.getFirstModifierByKind(common.SyntaxKind.ExportKeyword); } @@ -227473,11 +227473,11 @@ Node text: ${this.#forgottenText}`; return this.getDefaultKeyword() != null; } getDefaultKeyword() { - if (Node20.isVariableDeclaration(this)) { + if (Node21.isVariableDeclaration(this)) { const variableStatement = this.getVariableStatement(); return variableStatement?.getDefaultKeyword(); } - if (!Node20.isModifierable(this)) + if (!Node21.isModifierable(this)) return throwForNotModifierableNode(); return this.getFirstModifierByKind(common.SyntaxKind.DefaultKeyword); } @@ -227533,7 +227533,7 @@ Node text: ${this.#forgottenText}`; setIsDefaultExport(value) { if (value === this.isDefaultExport()) return this; - if (value && !Node20.isSourceFile(this.getParentOrThrow())) + if (value && !Node21.isSourceFile(this.getParentOrThrow())) throw new common.errors.InvalidOperationError("The parent must be a source file in order to set this node as a default export."); const sourceFile = this.getSourceFile(); const fileDefaultExportSymbol = sourceFile.getDefaultExportSymbol(); @@ -227541,7 +227541,7 @@ Node text: ${this.#forgottenText}`; sourceFile.removeDefaultExport(fileDefaultExportSymbol); if (!value) return this; - if (Node20.hasName(this) && shouldWriteAsSeparateStatement.call(this)) { + if (Node21.hasName(this) && shouldWriteAsSeparateStatement.call(this)) { const parentSyntaxList = this.getFirstAncestorByKindOrThrow(common.SyntaxKind.SyntaxList); const name = this.getName(); parentSyntaxList.insertChildText(this.getChildIndex() + 1, (writer) => { @@ -227553,15 +227553,15 @@ Node text: ${this.#forgottenText}`; } return this; function shouldWriteAsSeparateStatement() { - if (Node20.isEnumDeclaration(this) || Node20.isModuleDeclaration(this) || Node20.isTypeAliasDeclaration(this)) + if (Node21.isEnumDeclaration(this) || Node21.isModuleDeclaration(this) || Node21.isTypeAliasDeclaration(this)) return true; - if (Node20.isAmbientable(this) && this.isAmbient()) + if (Node21.isAmbientable(this) && this.isAmbient()) return true; return false; } } setIsExported(value) { - if (Node20.isSourceFile(this.getParentOrThrow())) + if (Node21.isSourceFile(this.getParentOrThrow())) this.toggleModifier("default", false); this.toggleModifier("export", value); return this; @@ -231017,8 +231017,8 @@ Node text: ${this.#forgottenText}`; this._standardWrite(writer, info, () => { this._context.structurePrinterFactory.forImportDeclaration().printTexts(writer, structures); }, { - previousNewLine: (previousMember) => Node20.isImportDeclaration(previousMember) || isComment(previousMember.compilerNode), - nextNewLine: (nextMember) => Node20.isImportDeclaration(nextMember) + previousNewLine: (previousMember) => Node21.isImportDeclaration(previousMember) || isComment(previousMember.compilerNode), + nextNewLine: (nextMember) => Node21.isImportDeclaration(nextMember) }); } }); @@ -231036,7 +231036,7 @@ Node text: ${this.#forgottenText}`; return common.errors.throwIfNullOrUndefined(this.getImportDeclaration(conditionOrModuleSpecifier), message ?? "Expected to find an import with the provided condition.", this); } getImportDeclarations() { - return this.getStatements().filter(Node20.isImportDeclaration); + return this.getStatements().filter(Node21.isImportDeclaration); } addExportDeclaration(structure) { return this.addExportDeclarations([structure])[0]; @@ -231056,8 +231056,8 @@ Node text: ${this.#forgottenText}`; this._standardWrite(writer, info, () => { this._context.structurePrinterFactory.forExportDeclaration().printTexts(writer, structures); }, { - previousNewLine: (previousMember) => Node20.isExportDeclaration(previousMember) || isComment(previousMember.compilerNode), - nextNewLine: (nextMember) => Node20.isExportDeclaration(nextMember) + previousNewLine: (previousMember) => Node21.isExportDeclaration(previousMember) || isComment(previousMember.compilerNode), + nextNewLine: (nextMember) => Node21.isExportDeclaration(nextMember) }); } }); @@ -231075,7 +231075,7 @@ Node text: ${this.#forgottenText}`; return common.errors.throwIfNullOrUndefined(this.getExportDeclaration(conditionOrModuleSpecifier), message ?? "Expected to find an export declaration with the provided condition.", this); } getExportDeclarations() { - return this.getStatements().filter(Node20.isExportDeclaration); + return this.getStatements().filter(Node21.isExportDeclaration); } addExportAssignment(structure) { return this.addExportAssignments([structure])[0]; @@ -231095,8 +231095,8 @@ Node text: ${this.#forgottenText}`; this._standardWrite(writer, info, () => { this._context.structurePrinterFactory.forExportAssignment().printTexts(writer, structures); }, { - previousNewLine: (previousMember) => Node20.isExportAssignment(previousMember) || isComment(previousMember.compilerNode), - nextNewLine: (nextMember) => Node20.isExportAssignment(nextMember) + previousNewLine: (previousMember) => Node21.isExportAssignment(previousMember) || isComment(previousMember.compilerNode), + nextNewLine: (nextMember) => Node21.isExportAssignment(nextMember) }); } }); @@ -231108,7 +231108,7 @@ Node text: ${this.#forgottenText}`; return common.errors.throwIfNullOrUndefined(this.getExportAssignment(condition), message ?? "Expected to find an export assignment with the provided condition.", this); } getExportAssignments() { - return this.getStatements().filter(Node20.isExportAssignment); + return this.getStatements().filter(Node21.isExportAssignment); } getDefaultExportSymbol() { const sourceFileSymbol = this.getSymbol(); @@ -231139,23 +231139,23 @@ Node text: ${this.#forgottenText}`; } return result; function* getDeclarationHandlingImportsAndExports(declaration) { - if (Node20.isExportSpecifier(declaration)) { + if (Node21.isExportSpecifier(declaration)) { for (const d of declaration.getLocalTargetDeclarations()) yield* getDeclarationHandlingImportsAndExports(d); - } else if (Node20.isExportAssignment(declaration)) { + } else if (Node21.isExportAssignment(declaration)) { const expression = declaration.getExpression(); if (expression == null || expression.getKind() !== common.SyntaxKind.Identifier) { yield expression; return; } yield* getDeclarationsForSymbol(expression.getSymbol()); - } else if (Node20.isImportSpecifier(declaration)) { + } else if (Node21.isImportSpecifier(declaration)) { const identifier = declaration.getNameNode(); const symbol = identifier.getSymbol(); if (symbol == null) return; yield* getDeclarationsForSymbol(symbol.getAliasedSymbol() || symbol); - } else if (Node20.isImportClause(declaration)) { + } else if (Node21.isImportClause(declaration)) { const identifier = declaration.getDefaultImport(); if (identifier == null) return; @@ -231163,7 +231163,7 @@ Node text: ${this.#forgottenText}`; if (symbol == null) return; yield* getDeclarationsForSymbol(symbol.getAliasedSymbol() || symbol); - } else if (Node20.isNamespaceImport(declaration) || Node20.isNamespaceExport(declaration)) { + } else if (Node21.isNamespaceImport(declaration) || Node21.isNamespaceExport(declaration)) { const symbol = declaration.getNameNode().getSymbol(); if (symbol == null) return; @@ -231186,7 +231186,7 @@ Node text: ${this.#forgottenText}`; const declaration = defaultExportSymbol.getDeclarations()[0]; if (declaration.compilerNode.kind === common.SyntaxKind.ExportAssignment) removeChildrenWithFormatting({ children: [declaration], getSiblingFormatting: () => FormattingKind.Newline }); - else if (Node20.isModifierable(declaration)) { + else if (Node21.isModifierable(declaration)) { declaration.toggleModifier("default", false); declaration.toggleModifier("export", false); } @@ -231226,12 +231226,12 @@ Node text: ${this.#forgottenText}`; }; } function getNodeForReferences(node) { - if (Node20.isIdentifier(node) || Node20.isStringLiteral(node)) + if (Node21.isIdentifier(node) || Node21.isStringLiteral(node)) return node; const nameNode = node.getNodeProperty("name"); if (nameNode != null) return nameNode; - if (Node20.isExportable(node)) + if (Node21.isExportable(node)) return node.getDefaultKeyword() || node; return node; } @@ -231241,12 +231241,12 @@ Node text: ${this.#forgottenText}`; renameNode(getNodeToRename(this), newName, options); return this; function getNodeToRename(thisNode) { - if (Node20.isIdentifier(thisNode) || Node20.isPrivateIdentifier(thisNode) || Node20.isStringLiteral(thisNode)) + if (Node21.isIdentifier(thisNode) || Node21.isPrivateIdentifier(thisNode) || Node21.isStringLiteral(thisNode)) return thisNode; else if (thisNode.getNameNode != null) { const node = thisNode.getNameNode(); common.errors.throwIfNullOrUndefined(node, "Expected to find a name node when renaming."); - if (Node20.isArrayBindingPattern(node) || Node20.isObjectBindingPattern(node)) + if (Node21.isArrayBindingPattern(node) || Node21.isObjectBindingPattern(node)) throw new common.errors.NotImplementedError(`Not implemented renameable scenario for ${node.getKindName()}.`); return node; } else { @@ -231328,7 +231328,7 @@ Node text: ${this.#forgottenText}`; }; } function addNameNode(node, newName) { - if (Node20.isClassDeclaration(node) || Node20.isClassExpression(node)) { + if (Node21.isClassDeclaration(node) || Node21.isClassExpression(node)) { const classKeyword = node.getFirstChildByKindOrThrow(common.SyntaxKind.ClassKeyword); insertIntoParentTextRange({ insertPos: classKeyword.getEnd(), @@ -231450,7 +231450,7 @@ Node text: ${this.#forgottenText}`; if (value === hasQuestionDotToken) return this; if (value) { - if (Node20.isPropertyAccessExpression(this)) + if (Node21.isPropertyAccessExpression(this)) this.getFirstChildByKindOrThrow(common.SyntaxKind.DotToken).replaceWithText("?."); else { insertIntoParentTextRange({ @@ -231460,16 +231460,16 @@ Node text: ${this.#forgottenText}`; }); } } else { - if (Node20.isPropertyAccessExpression(this)) + if (Node21.isPropertyAccessExpression(this)) questionDotTokenNode.replaceWithText("."); else removeChildren({ children: [questionDotTokenNode] }); } return this; function getInsertPos2() { - if (Node20.isCallExpression(this)) + if (Node21.isCallExpression(this)) return this.getFirstChildByKindOrThrow(common.SyntaxKind.OpenParenToken).getStart(); - if (Node20.isElementAccessExpression(this)) + if (Node21.isElementAccessExpression(this)) return this.getFirstChildByKindOrThrow(common.SyntaxKind.OpenBracketToken).getStart(); common.errors.throwNotImplementedForSyntaxKindError(this.compilerNode.kind); } @@ -231504,7 +231504,7 @@ Node text: ${this.#forgottenText}`; if (value === hasQuestionToken) return this; if (value) { - if (Node20.isExclamationTokenable(this)) + if (Node21.isExclamationTokenable(this)) this.setHasExclamationToken(false); insertIntoParentTextRange({ insertPos: getInsertPos2.call(this), @@ -231516,7 +231516,7 @@ Node text: ${this.#forgottenText}`; } return this; function getInsertPos2() { - if (Node20.hasName(this)) + if (Node21.hasName(this)) return this.getNameNode().getEnd(); const colonNode = this.getFirstChildByKind(common.SyntaxKind.ColonToken); if (colonNode != null) @@ -231637,7 +231637,7 @@ Node text: ${this.#forgottenText}`; const scope = getScopeForNode(this); if (scope != null) return scope; - if (Node20.isParameterDeclaration(this) && this.isReadonly()) + if (Node21.isParameterDeclaration(this) && this.isReadonly()) return exports2.Scope.Public; return void 0; } @@ -231784,14 +231784,14 @@ Node text: ${this.#forgottenText}`; } function getValidRange(thisNode) { const rangeNode = getRangeNode(); - const openBrace = Node20.isSourceFile(rangeNode) ? void 0 : rangeNode.getPreviousSiblingIfKind(common.SyntaxKind.OpenBraceToken); + const openBrace = Node21.isSourceFile(rangeNode) ? void 0 : rangeNode.getPreviousSiblingIfKind(common.SyntaxKind.OpenBraceToken); const closeBrace = openBrace == null ? void 0 : rangeNode.getNextSiblingIfKind(common.SyntaxKind.CloseBraceToken); if (openBrace != null && closeBrace != null) return [openBrace.getEnd(), closeBrace.getStart()]; else return [rangeNode.getPos(), rangeNode.getEnd()]; function getRangeNode() { - if (Node20.isSourceFile(thisNode)) + if (Node21.isSourceFile(thisNode)) return thisNode; return thisNode.getChildSyntaxListOrThrow(); } @@ -231818,8 +231818,8 @@ Node text: ${this.#forgottenText}`; const typeArguments = this.getTypeArguments(); index = verifyAndGetIndex(index, typeArguments.length); if (typeArguments.length === 0) { - const expression = Node20.hasExpression(this) ? this.getExpression() : void 0; - const identifier = Node20.isPropertyAccessExpression(expression) ? expression.getLastChildByKindOrThrow(common.SyntaxKind.Identifier) : this.getFirstChildByKindOrThrow(common.SyntaxKind.Identifier); + const expression = Node21.hasExpression(this) ? this.getExpression() : void 0; + const identifier = Node21.isPropertyAccessExpression(expression) ? expression.getLastChildByKindOrThrow(common.SyntaxKind.Identifier) : this.getFirstChildByKindOrThrow(common.SyntaxKind.Identifier); insertIntoParentTextRange({ insertPos: identifier.getEnd(), parent: this, @@ -232306,7 +232306,7 @@ Node text: ${this.#forgottenText}`; const namedNode = node; if (namedNode.getNameNode != null) return namedNode.getNameNode().getEnd(); - else if (Node20.isCallSignatureDeclaration(node) || Node20.isFunctionTypeNode(node)) + else if (Node21.isCallSignatureDeclaration(node) || Node21.isFunctionTypeNode(node)) return node.getFirstChildByKindOrThrow(common.SyntaxKind.OpenParenToken).getStart(); else throw new common.errors.NotImplementedError(`Not implemented scenario inserting type parameters for node with kind ${node.getKindName()}.`); @@ -232318,13 +232318,13 @@ Node text: ${this.#forgottenText}`; } }; } - var ArrayBindingPattern = class extends Node20 { + var ArrayBindingPattern = class extends Node21 { getElements() { return this.compilerNode.elements.map((e) => this._getNodeFromCompilerNode(e)); } }; var createBase$F = (ctor) => DotDotDotTokenableNode(InitializerExpressionableNode(BindingNamedNode(ctor))); - var BindingElementBase = createBase$F(Node20); + var BindingElementBase = createBase$F(Node21); var BindingElement = class extends BindingElementBase { getPropertyNameNodeOrThrow(message) { return common.errors.throwIfNullOrUndefined(this.getPropertyNameNode(), message ?? "Expected to find a property name node.", this); @@ -232333,7 +232333,7 @@ Node text: ${this.#forgottenText}`; return this._getNodeFromCompilerNodeIfExists(this.compilerNode.propertyName); } }; - var ObjectBindingPattern = class extends Node20 { + var ObjectBindingPattern = class extends Node21 { getElements() { return this.compilerNode.elements.map((e) => this._getNodeFromCompilerNode(e)); } @@ -232366,7 +232366,7 @@ Node text: ${this.#forgottenText}`; } }; } - var Expression = class extends Node20 { + var Expression = class extends Node21 { getContextualType() { return this._context.typeChecker.getContextualType(this); } @@ -232594,7 +232594,7 @@ Node text: ${this.#forgottenText}`; var NonNullExpressionBase = ExpressionedNode(LeftHandSideExpression); var NonNullExpression = class extends NonNullExpressionBase { }; - var ObjectLiteralElement = class extends Node20 { + var ObjectLiteralElement = class extends Node21 { remove() { removeCommaSeparatedChild(this); } @@ -232938,7 +232938,7 @@ Node text: ${this.#forgottenText}`; var YieldExpressionBase = ExpressionableNode(GeneratorableNode(Expression)); var YieldExpression = class extends YieldExpressionBase { }; - var StatementBase = ChildOrderableNode(Node20); + var StatementBase = ChildOrderableNode(Node21); var Statement = class extends StatementBase { remove() { removeStatementedNodeChild(this); @@ -232979,7 +232979,7 @@ Node text: ${this.#forgottenText}`; return getChildSyntaxList.call(this).insertChildText(index, writerFunction); function getChildSyntaxList() { const childSyntaxList = this.getChildSyntaxListOrThrow(); - if (Node20.isCaseClause(this) || Node20.isDefaultClause(this)) { + if (Node21.isCaseClause(this) || Node21.isDefaultClause(this)) { const block = childSyntaxList.getFirstChildIfKind(common.SyntaxKind.Block); if (block != null) return block.getChildSyntaxListOrThrow(); @@ -233019,7 +233019,7 @@ Node text: ${this.#forgottenText}`; }); } getClasses() { - return this.getStatements().filter(Node20.isClassDeclaration); + return this.getStatements().filter(Node21.isClassDeclaration); } getClass(nameOrFindFunction) { return getNodeByNameOrFindFunction(this.getClasses(), nameOrFindFunction); @@ -233049,7 +233049,7 @@ Node text: ${this.#forgottenText}`; }); } getEnums() { - return this.getStatements().filter(Node20.isEnumDeclaration); + return this.getStatements().filter(Node21.isEnumDeclaration); } getEnum(nameOrFindFunction) { return getNodeByNameOrFindFunction(this.getEnums(), nameOrFindFunction); @@ -233077,14 +233077,14 @@ Node text: ${this.#forgottenText}`; isAmbient: isNodeAmbientOrInAmbientContext(this) }).printTexts(writer, structures); }, { - previousNewLine: (previousMember) => structures[0].hasDeclareKeyword === true && Node20.isFunctionDeclaration(previousMember) && previousMember.getBody() == null, - nextNewLine: (nextMember) => structures[structures.length - 1].hasDeclareKeyword === true && Node20.isFunctionDeclaration(nextMember) && nextMember.getBody() == null + previousNewLine: (previousMember) => structures[0].hasDeclareKeyword === true && Node21.isFunctionDeclaration(previousMember) && previousMember.getBody() == null, + nextNewLine: (nextMember) => structures[structures.length - 1].hasDeclareKeyword === true && Node21.isFunctionDeclaration(nextMember) && nextMember.getBody() == null }); } }); } getFunctions() { - return this.getStatements().filter(Node20.isFunctionDeclaration).filter((f) => f.isAmbient() || f.isImplementation()); + return this.getStatements().filter(Node21.isFunctionDeclaration).filter((f) => f.isAmbient() || f.isImplementation()); } getFunction(nameOrFindFunction) { return getNodeByNameOrFindFunction(this.getFunctions(), nameOrFindFunction); @@ -233114,7 +233114,7 @@ Node text: ${this.#forgottenText}`; }); } getInterfaces() { - return this.getStatements().filter(Node20.isInterfaceDeclaration); + return this.getStatements().filter(Node21.isInterfaceDeclaration); } getInterface(nameOrFindFunction) { return getNodeByNameOrFindFunction(this.getInterfaces(), nameOrFindFunction); @@ -233144,7 +233144,7 @@ Node text: ${this.#forgottenText}`; }); } getModules() { - return this.getStatements().filter(Node20.isModuleDeclaration); + return this.getStatements().filter(Node21.isModuleDeclaration); } getModule(nameOrFindFunction) { return getNodeByNameOrFindFunction(this.getModules(), nameOrFindFunction); @@ -233170,14 +233170,14 @@ Node text: ${this.#forgottenText}`; this._standardWrite(writer, info, () => { this._context.structurePrinterFactory.forTypeAliasDeclaration().printTexts(writer, structures); }, { - previousNewLine: (previousMember) => Node20.isTypeAliasDeclaration(previousMember), - nextNewLine: (nextMember) => Node20.isTypeAliasDeclaration(nextMember) + previousNewLine: (previousMember) => Node21.isTypeAliasDeclaration(previousMember), + nextNewLine: (nextMember) => Node21.isTypeAliasDeclaration(nextMember) }); } }); } getTypeAliases() { - return this.getStatements().filter(Node20.isTypeAliasDeclaration); + return this.getStatements().filter(Node21.isTypeAliasDeclaration); } getTypeAlias(nameOrFindFunction) { return getNodeByNameOrFindFunction(this.getTypeAliases(), nameOrFindFunction); @@ -233186,7 +233186,7 @@ Node text: ${this.#forgottenText}`; return common.errors.throwIfNullOrUndefined(this.getTypeAlias(nameOrFindFunction), () => getNotFoundErrorMessageForNameOrFindFunction("type alias", nameOrFindFunction)); } getVariableStatements() { - return this.getStatements().filter(Node20.isVariableStatement); + return this.getStatements().filter(Node21.isVariableStatement); } getVariableStatement(nameOrFindFunction) { return this.getVariableStatements().find(getFindFunction()); @@ -233217,8 +233217,8 @@ Node text: ${this.#forgottenText}`; this._standardWrite(writer, info, () => { this._context.structurePrinterFactory.forVariableStatement().printTexts(writer, structures); }, { - previousNewLine: (previousMember) => Node20.isVariableStatement(previousMember), - nextNewLine: (nextMember) => Node20.isVariableStatement(nextMember) + previousNewLine: (previousMember) => Node21.isVariableStatement(previousMember), + nextNewLine: (nextMember) => Node21.isVariableStatement(nextMember) }); } }); @@ -233237,11 +233237,11 @@ Node text: ${this.#forgottenText}`; } getStructure() { const structure = {}; - if (Node20.isBodyable(this) && !this.hasBody()) + if (Node21.isBodyable(this) && !this.hasBody()) structure.statements = void 0; else { structure.statements = this.getStatements().map((s) => { - if (Node20._hasStructure(s)) + if (Node21._hasStructure(s)) return s.getStructure(); return s.getText({ trimLeadingIndentation: true }); }); @@ -233249,7 +233249,7 @@ Node text: ${this.#forgottenText}`; return callBaseGetStructure(Base.prototype, this, structure); } set(structure) { - if (Node20.isBodyable(this) && structure.statements == null && structure.hasOwnProperty(common.nameof(structure, "statements"))) + if (Node21.isBodyable(this) && structure.statements == null && structure.hasOwnProperty(common.nameof(structure, "statements"))) this.removeBody(); else if (structure.statements != null) { const statementCount = this._getCompilerStatementsWithComments().length; @@ -233270,17 +233270,17 @@ Node text: ${this.#forgottenText}`; } } _getCompilerStatementsContainer() { - if (Node20.isSourceFile(this) || Node20.isCaseClause(this) || Node20.isDefaultClause(this)) + if (Node21.isSourceFile(this) || Node21.isCaseClause(this) || Node21.isDefaultClause(this)) return this.compilerNode; - else if (Node20.isModuleDeclaration(this)) { + else if (Node21.isModuleDeclaration(this)) { const body = this._getInnerBody(); if (body == null) return void 0; else return body.compilerNode; - } else if (Node20.isBodyable(this) || Node20.isBodied(this)) + } else if (Node21.isBodyable(this) || Node21.isBodied(this)) return this.getBody()?.compilerNode; - else if (Node20.isBlock(this) || Node20.isModuleBlock(this)) + else if (Node21.isBlock(this) || Node21.isModuleBlock(this)) return this.compilerNode; else throw new common.errors.NotImplementedError(`Could not find the statements for node kind: ${this.getKindName()}, text: ${this.getText()}`); @@ -233297,7 +233297,7 @@ Node text: ${this.#forgottenText}`; }); } _standardWrite(writer, info, writeStructures, opts = {}) { - if (info.previousMember != null && (opts.previousNewLine == null || !opts.previousNewLine(info.previousMember)) && !Node20.isCommentNode(info.previousMember)) { + if (info.previousMember != null && (opts.previousNewLine == null || !opts.previousNewLine(info.previousMember)) && !Node21.isCommentNode(info.previousMember)) { writer.blankLine(); } else if (!info.isStartOfFile) { writer.newLineIfLastNot(); @@ -233311,7 +233311,7 @@ Node text: ${this.#forgottenText}`; }; } function addBodyIfNotExists(node) { - if (Node20.isBodyable(node) && !node.hasBody()) + if (Node21.isBodyable(node) && !node.hasBody()) node.addBody(); } var createBase$v = (ctor) => TextInsertableNode(StatementedNode(ctor)); @@ -233326,7 +233326,7 @@ Node text: ${this.#forgottenText}`; return common.errors.throwIfNullOrUndefined(this.getLabel(), message ?? "Expected to find a label.", this); } }; - var CaseBlockBase = TextInsertableNode(Node20); + var CaseBlockBase = TextInsertableNode(Node21); var CaseBlock = class extends CaseBlockBase { getClauses() { const clauses = this.compilerNode.clauses || []; @@ -233344,13 +233344,13 @@ Node text: ${this.#forgottenText}`; } }; var createBase$u = (ctor) => JSDocableNode(ExpressionedNode(TextInsertableNode(StatementedNode(ctor)))); - var CaseClauseBase = createBase$u(Node20); + var CaseClauseBase = createBase$u(Node21); var CaseClause = class extends CaseClauseBase { remove() { removeClausedNodeChild(this); } }; - var CatchClauseBase = Node20; + var CatchClauseBase = Node21; var CatchClause = class extends CatchClauseBase { getBlock() { return this._getNodeFromCompilerNode(this.compilerNode.block); @@ -233376,7 +233376,7 @@ Node text: ${this.#forgottenText}`; var DebuggerStatement = class extends DebuggerStatementBase { }; var createBase$t = (ctor) => TextInsertableNode(StatementedNode(ctor)); - var DefaultClauseBase = createBase$t(Node20); + var DefaultClauseBase = createBase$t(Node21); var DefaultClause = class extends DefaultClauseBase { remove() { removeClausedNodeChild(this); @@ -233439,7 +233439,7 @@ Node text: ${this.#forgottenText}`; } remove() { const nodes = []; - if (Node20.isIfStatement(this.getParentOrThrow())) + if (Node21.isIfStatement(this.getParentOrThrow())) nodes.push(this.getPreviousSiblingIfKindOrThrow(common.SyntaxKind.ElseKeyword)); nodes.push(this); removeStatementedNodeChildren(nodes); @@ -233553,7 +233553,7 @@ Node text: ${this.#forgottenText}`; } getNamespaceExport() { const exportClause = this.getNodeProperty("exportClause"); - return exportClause != null && Node20.isNamespaceExport(exportClause) ? exportClause : void 0; + return exportClause != null && Node21.isNamespaceExport(exportClause) ? exportClause : void 0; } getNamespaceExportOrThrow(message) { return common.errors.throwIfNullOrUndefined(this.getNamespaceExport(), message ?? "Expected to find a namespace export.", this); @@ -233571,7 +233571,7 @@ Node text: ${this.#forgottenText}`; textLength: 1 } }); - } else if (Node20.isNamespaceExport(exportClause)) + } else if (Node21.isNamespaceExport(exportClause)) exportClause.getNameNode().replaceWithText(name); else { insertIntoParentTextRange({ @@ -233609,7 +233609,7 @@ Node text: ${this.#forgottenText}`; const moduleSpecifier = this._getNodeFromCompilerNodeIfExists(this.compilerNode.moduleSpecifier); if (moduleSpecifier == null) return void 0; - if (!Node20.isStringLiteral(moduleSpecifier)) + if (!Node21.isStringLiteral(moduleSpecifier)) throw new common.errors.InvalidOperationError("Expected the module specifier to be a string literal."); return moduleSpecifier; } @@ -233628,7 +233628,7 @@ Node text: ${this.#forgottenText}`; if (symbol == null) return void 0; const declaration = symbol.getDeclarations()[0]; - return declaration != null && Node20.isSourceFile(declaration) ? declaration : void 0; + return declaration != null && Node21.isSourceFile(declaration) ? declaration : void 0; } isModuleSpecifierRelative() { const moduleSpecifierValue = this.getModuleSpecifierValue(); @@ -233809,7 +233809,7 @@ Node text: ${this.#forgottenText}`; } }); } - var ExportSpecifierBase = Node20; + var ExportSpecifierBase = Node21; var ExportSpecifier = class extends ExportSpecifierBase { setName(name) { const nameNode = this.getNameNode(); @@ -233942,7 +233942,7 @@ Node text: ${this.#forgottenText}`; } getStructure() { const alias = this.getAliasNode(); - return callBaseGetStructure(Node20.prototype, this, { + return callBaseGetStructure(Node21.prototype, this, { kind: exports2.StructureKind.ExportSpecifier, alias: alias ? alias.getText() : void 0, name: this.getNameNode().getText(), @@ -233950,14 +233950,14 @@ Node text: ${this.#forgottenText}`; }); } }; - var ExternalModuleReferenceBase = ExpressionableNode(Node20); + var ExternalModuleReferenceBase = ExpressionableNode(Node21); var ExternalModuleReference = class extends ExternalModuleReferenceBase { getReferencedSourceFileOrThrow(message) { return common.errors.throwIfNullOrUndefined(this.getReferencedSourceFile(), message ?? "Expected to find the referenced source file.", this); } isRelative() { const expression = this.getExpression(); - if (expression == null || !Node20.isStringLiteral(expression)) + if (expression == null || !Node21.isStringLiteral(expression)) return false; return ModuleUtils.isModuleSpecifierRelative(expression.getLiteralText()); } @@ -233971,7 +233971,7 @@ Node text: ${this.#forgottenText}`; return ModuleUtils.getReferencedSourceFileFromSymbol(symbol); } }; - var ImportAttributeBase = ImportAttributeNamedNode(Node20); + var ImportAttributeBase = ImportAttributeNamedNode(Node21); var ImportAttribute = class extends ImportAttributeBase { getValue() { return this._getNodeFromCompilerNode(this.compilerNode.value); @@ -233989,7 +233989,7 @@ Node text: ${this.#forgottenText}`; }); } }; - var ImportAttributesBase = Node20; + var ImportAttributesBase = Node21; var ImportAttributes = class extends ImportAttributesBase { setElements(elements) { this.replaceWithText((writer) => { @@ -234009,7 +234009,7 @@ Node text: ${this.#forgottenText}`; }); } }; - var ImportClauseBase = Node20; + var ImportClauseBase = Node21; var ImportClause = class extends ImportClauseBase { getPhaseModifier() { return this.compilerNode.phaseModifier; @@ -234075,13 +234075,13 @@ Node text: ${this.#forgottenText}`; } getNamespaceImport() { const namedBindings = this.getNamedBindings(); - if (namedBindings == null || !Node20.isNamespaceImport(namedBindings)) + if (namedBindings == null || !Node21.isNamespaceImport(namedBindings)) return void 0; return namedBindings.getNameNode(); } getNamedImports() { const namedBindings = this.getNamedBindings(); - if (namedBindings == null || !Node20.isNamedImports(namedBindings)) + if (namedBindings == null || !Node21.isNamedImports(namedBindings)) return []; return namedBindings.getElements(); } @@ -234126,7 +234126,7 @@ Node text: ${this.#forgottenText}`; } getModuleSpecifier() { const moduleSpecifier = this._getNodeFromCompilerNode(this.compilerNode.moduleSpecifier); - if (!Node20.isStringLiteral(moduleSpecifier)) + if (!Node21.isStringLiteral(moduleSpecifier)) throw new common.errors.InvalidOperationError("Expected the module specifier to be a string literal."); return moduleSpecifier; } @@ -234489,14 +234489,14 @@ Node text: ${this.#forgottenText}`; } isExternalModuleReferenceRelative() { const moduleReference = this.getModuleReference(); - if (!Node20.isExternalModuleReference(moduleReference)) + if (!Node21.isExternalModuleReference(moduleReference)) return false; return moduleReference.isRelative(); } setExternalModuleReference(textOrSourceFile) { const text2 = typeof textOrSourceFile === "string" ? textOrSourceFile : this._sourceFile.getRelativePathAsModuleSpecifierTo(textOrSourceFile); const moduleReference = this.getModuleReference(); - if (Node20.isExternalModuleReference(moduleReference) && moduleReference.getExpression() != null) + if (Node21.isExternalModuleReference(moduleReference) && moduleReference.getExpression() != null) moduleReference.getExpressionOrThrow().replaceWithText((writer) => writer.quote(text2)); else moduleReference.replaceWithText((writer) => writer.write("require(").quote(text2).write(")")); @@ -234507,12 +234507,12 @@ Node text: ${this.#forgottenText}`; } getExternalModuleReferenceSourceFile() { const moduleReference = this.getModuleReference(); - if (!Node20.isExternalModuleReference(moduleReference)) + if (!Node21.isExternalModuleReference(moduleReference)) return void 0; return moduleReference.getReferencedSourceFile(); } }; - var ImportSpecifierBase = Node20; + var ImportSpecifierBase = Node21; var ImportSpecifier = class extends ImportSpecifierBase { setName(name) { const nameNode = this.getNameNode(); @@ -234651,7 +234651,7 @@ Node text: ${this.#forgottenText}`; } getParentModule() { let parent = this.getParentOrThrow(); - if (!Node20.isModuleBlock(parent)) + if (!Node21.isModuleBlock(parent)) return void 0; while (parent.getParentOrThrow().getKind() === common.SyntaxKind.ModuleDeclaration) parent = parent.getParentOrThrow(); @@ -234710,7 +234710,7 @@ Node text: ${this.#forgottenText}`; } getNameNodes() { const name = this.getNameNode(); - if (Node20.isStringLiteral(name)) + if (Node21.isStringLiteral(name)) return name; else { const nodes = []; @@ -234782,7 +234782,7 @@ Node text: ${this.#forgottenText}`; } _getInnerBody() { let node = this.getBody(); - while (node != null && Node20.isBodyable(node) && node.compilerNode.statements == null) + while (node != null && Node21.isBodyable(node) && node.compilerNode.statements == null) node = node.getBody(); return node; } @@ -234797,19 +234797,19 @@ Node text: ${this.#forgottenText}`; if (!namespaceDec.hasDeclareKeyword()) namespaceDec.setHasDeclareKeyword(true); } - var NamedExportsBase = Node20; + var NamedExportsBase = Node21; var NamedExports = class extends NamedExportsBase { getElements() { return this.compilerNode.elements.map((e) => this._getNodeFromCompilerNode(e)); } }; - var NamedImportsBase = Node20; + var NamedImportsBase = Node21; var NamedImports = class extends NamedImportsBase { getElements() { return this.compilerNode.elements.map((e) => this._getNodeFromCompilerNode(e)); } }; - var NamespaceExportBase = RenameableNode(Node20); + var NamespaceExportBase = RenameableNode(Node21); var NamespaceExport = class extends NamespaceExportBase { setName(name) { const nameNode = this.getNameNode(); @@ -234832,7 +234832,7 @@ Node text: ${this.#forgottenText}`; return this._getNodeFromCompilerNode(this.compilerNode.name); } }; - var NamespaceImportBase = RenameableNode(Node20); + var NamespaceImportBase = RenameableNode(Node21); var NamespaceImport = class extends NamespaceImportBase { setName(name) { const nameNode = this.getNameNode(); @@ -234862,7 +234862,7 @@ Node text: ${this.#forgottenText}`; FileSystemRefreshResult[FileSystemRefreshResult["Updated"] = 1] = "Updated"; FileSystemRefreshResult[FileSystemRefreshResult["Deleted"] = 2] = "Deleted"; })(exports2.FileSystemRefreshResult || (exports2.FileSystemRefreshResult = {})); - var SourceFileBase = ModuledNode(TextInsertableNode(StatementedNode(Node20))); + var SourceFileBase = ModuledNode(TextInsertableNode(StatementedNode(Node21))); var SourceFile = (() => { let _classSuper = SourceFileBase; let _instanceExtraInitializers = []; @@ -235367,7 +235367,7 @@ Node text: ${this.#forgottenText}`; function getReferencingNodeFromStringLiteral(literal) { const parent = literal.getParentOrThrow(); const grandParent = parent.getParent(); - if (grandParent != null && Node20.isImportEqualsDeclaration(grandParent)) + if (grandParent != null && Node21.isImportEqualsDeclaration(grandParent)) return grandParent; else return parent; @@ -235580,7 +235580,7 @@ Node text: ${this.#forgottenText}`; var FunctionExpression = class extends FunctionExpressionBase { }; var createBase$m = (ctor) => OverrideableNode(QuestionTokenableNode(DecoratableNode(ScopeableNode(ReadonlyableNode(ModifierableNode(DotDotDotTokenableNode(TypedNode(InitializerExpressionableNode(BindingNamedNode(ctor)))))))))); - var ParameterDeclarationBase = createBase$m(Node20); + var ParameterDeclarationBase = createBase$m(Node21); var ParameterDeclaration = class extends ParameterDeclarationBase { isRestParameter() { return this.compilerNode.dotDotDotToken != null; @@ -235643,7 +235643,7 @@ Node text: ${this.#forgottenText}`; if (isParameterWithoutParens()) addParens(); function isParameterWithoutParens() { - return Node20.isArrowFunction(parent) && parent.compilerNode.parameters.length === 1 && parameter.getParentSyntaxListOrThrow().getPreviousSiblingIfKind(common.SyntaxKind.OpenParenToken) == null; + return Node21.isArrowFunction(parent) && parent.compilerNode.parameters.length === 1 && parameter.getParentSyntaxListOrThrow().getPreviousSiblingIfKind(common.SyntaxKind.OpenParenToken) == null; } function addParens() { const paramText = parameter.getText(); @@ -235660,14 +235660,14 @@ Node text: ${this.#forgottenText}`; }); } } - var ClassElement = class extends Node20 { + var ClassElement = class extends Node21 { remove() { const parent = this.getParentOrThrow(); - if (Node20.isClassDeclaration(parent) || Node20.isClassExpression(parent)) + if (Node21.isClassDeclaration(parent) || Node21.isClassExpression(parent)) removeClassMember(this); - else if (Node20.isObjectLiteralExpression(parent)) + else if (Node21.isObjectLiteralExpression(parent)) removeCommaSeparatedChild(this); - else if (Node20.isInterfaceDeclaration(parent)) + else if (Node21.isInterfaceDeclaration(parent)) removeInterfaceMember(this); else common.errors.throwNotImplementedForSyntaxKindError(parent.getKind()); @@ -235809,7 +235809,7 @@ Node text: ${this.#forgottenText}`; index, parent: this, write: (writer, info) => { - const previousMemberHasBody = !isAmbient && info.previousMember != null && Node20.isBodyable(info.previousMember) && info.previousMember.hasBody(); + const previousMemberHasBody = !isAmbient && info.previousMember != null && Node21.isBodyable(info.previousMember) && info.previousMember.hasBody(); const firstStructureHasBody = !isAmbient && members instanceof Array && structureHasBody(members[0]); if (previousMemberHasBody || info.previousMember != null && firstStructureHasBody) writer.blankLineIfLastNot(); @@ -235820,7 +235820,7 @@ Node text: ${this.#forgottenText}`; memberPrinter.printTexts(memberWriter, members); writer.write(memberWriter.toString()); const lastStructureHasBody = !isAmbient && members instanceof Array && structureHasBody(members[members.length - 1]); - const nextMemberHasBody = !isAmbient && info.nextMember != null && Node20.isBodyable(info.nextMember) && info.nextMember.hasBody(); + const nextMemberHasBody = !isAmbient && info.nextMember != null && Node21.isBodyable(info.nextMember) && info.nextMember.hasBody(); if (info.nextMember != null && lastStructureHasBody || nextMemberHasBody) writer.blankLineIfLastNot(); else @@ -235850,7 +235850,7 @@ Node text: ${this.#forgottenText}`; structures, expectedKind: common.SyntaxKind.Constructor, write: (writer, info) => { - if (!isAmbient && info.previousMember != null && !Node20.isCommentNode(info.previousMember)) + if (!isAmbient && info.previousMember != null && !Node21.isCommentNode(info.previousMember)) writer.blankLineIfLastNot(); else writer.newLineIfLastNot(); @@ -235863,7 +235863,7 @@ Node text: ${this.#forgottenText}`; }); } getConstructors() { - return this.getMembers().filter((m) => Node20.isConstructorDeclaration(m)); + return this.getMembers().filter((m) => Node21.isConstructorDeclaration(m)); } addStaticBlock(structure = {}) { return this.insertStaticBlock(getEndIndexFromArray(this.getMembersWithComments()), structure); @@ -235881,7 +235881,7 @@ Node text: ${this.#forgottenText}`; structures, expectedKind: common.SyntaxKind.ClassStaticBlockDeclaration, write: (writer, info) => { - if (!isAmbient && info.previousMember != null && !Node20.isCommentNode(info.previousMember)) + if (!isAmbient && info.previousMember != null && !Node21.isCommentNode(info.previousMember)) writer.blankLineIfLastNot(); else writer.newLineIfLastNot(); @@ -235894,7 +235894,7 @@ Node text: ${this.#forgottenText}`; }); } getStaticBlocks() { - return this.getMembers().filter((m) => Node20.isClassStaticBlockDeclaration(m)); + return this.getMembers().filter((m) => Node21.isClassStaticBlockDeclaration(m)); } addGetAccessor(structure) { return this.addGetAccessors([structure])[0]; @@ -235911,7 +235911,7 @@ Node text: ${this.#forgottenText}`; structures, expectedKind: common.SyntaxKind.GetAccessor, write: (writer, info) => { - if (info.previousMember != null && !Node20.isCommentNode(info.previousMember)) + if (info.previousMember != null && !Node21.isCommentNode(info.previousMember)) writer.blankLineIfLastNot(); else writer.newLineIfLastNot(); @@ -235940,7 +235940,7 @@ Node text: ${this.#forgottenText}`; structures, expectedKind: common.SyntaxKind.SetAccessor, write: (writer, info) => { - if (info.previousMember != null && !Node20.isCommentNode(info.previousMember)) + if (info.previousMember != null && !Node21.isCommentNode(info.previousMember)) writer.blankLineIfLastNot(); else writer.newLineIfLastNot(); @@ -235969,12 +235969,12 @@ Node text: ${this.#forgottenText}`; structures, expectedKind: common.SyntaxKind.PropertyDeclaration, write: (writer, info) => { - if (info.previousMember != null && Node20.hasBody(info.previousMember)) + if (info.previousMember != null && Node21.hasBody(info.previousMember)) writer.blankLineIfLastNot(); else writer.newLineIfLastNot(); this._context.structurePrinterFactory.forPropertyDeclaration().printTexts(writer, structures); - if (info.nextMember != null && Node20.hasBody(info.nextMember)) + if (info.nextMember != null && Node21.hasBody(info.nextMember)) writer.blankLineIfLastNot(); else writer.newLineIfLastNot(); @@ -235996,7 +235996,7 @@ Node text: ${this.#forgottenText}`; return insertChildren(this, { index, write: (writer, info) => { - if (!isAmbient && info.previousMember != null && !Node20.isCommentNode(info.previousMember)) + if (!isAmbient && info.previousMember != null && !Node21.isCommentNode(info.previousMember)) writer.blankLineIfLastNot(); else writer.newLineIfLastNot(); @@ -236035,7 +236035,7 @@ Node text: ${this.#forgottenText}`; return common.errors.throwIfNullOrUndefined(this.getProperty(nameOrFindFunction), () => getNotFoundErrorMessageForNameOrFindFunction("class property declaration", nameOrFindFunction)); } getProperties() { - return this.getMembers().filter((m) => Node20.isPropertyDeclaration(m)); + return this.getMembers().filter((m) => Node21.isPropertyDeclaration(m)); } getGetAccessor(nameOrFindFunction) { return getNodeByNameOrFindFunction(this.getGetAccessors(), nameOrFindFunction); @@ -236044,7 +236044,7 @@ Node text: ${this.#forgottenText}`; return common.errors.throwIfNullOrUndefined(this.getGetAccessor(nameOrFindFunction), () => getNotFoundErrorMessageForNameOrFindFunction("class getAccessor declaration", nameOrFindFunction)); } getGetAccessors() { - return this.getMembers().filter((m) => Node20.isGetAccessorDeclaration(m)); + return this.getMembers().filter((m) => Node21.isGetAccessorDeclaration(m)); } getSetAccessor(nameOrFindFunction) { return getNodeByNameOrFindFunction(this.getSetAccessors(), nameOrFindFunction); @@ -236053,7 +236053,7 @@ Node text: ${this.#forgottenText}`; return common.errors.throwIfNullOrUndefined(this.getSetAccessor(nameOrFindFunction), () => getNotFoundErrorMessageForNameOrFindFunction("class setAccessor declaration", nameOrFindFunction)); } getSetAccessors() { - return this.getMembers().filter((m) => Node20.isSetAccessorDeclaration(m)); + return this.getMembers().filter((m) => Node21.isSetAccessorDeclaration(m)); } getMethod(nameOrFindFunction) { return getNodeByNameOrFindFunction(this.getMethods(), nameOrFindFunction); @@ -236062,7 +236062,7 @@ Node text: ${this.#forgottenText}`; return common.errors.throwIfNullOrUndefined(this.getMethod(nameOrFindFunction), () => getNotFoundErrorMessageForNameOrFindFunction("class method declaration", nameOrFindFunction)); } getMethods() { - return this.getMembers().filter((m) => Node20.isMethodDeclaration(m)); + return this.getMembers().filter((m) => Node21.isMethodDeclaration(m)); } getInstanceMethod(nameOrFindFunction) { return getNodeByNameOrFindFunction(this.getInstanceMethods(), nameOrFindFunction); @@ -236090,9 +236090,9 @@ Node text: ${this.#forgottenText}`; } getInstanceMembers() { return this.getMembersWithParameterProperties().filter((m) => { - if (Node20.isConstructorDeclaration(m)) + if (Node21.isConstructorDeclaration(m)) return false; - return Node20.isParameterDeclaration(m) || !m.isStatic(); + return Node21.isParameterDeclaration(m) || !m.isStatic(); }); } getStaticMember(nameOrFindFunction) { @@ -236103,14 +236103,14 @@ Node text: ${this.#forgottenText}`; } getStaticMembers() { return this.getMembers().filter((m) => { - if (Node20.isConstructorDeclaration(m)) + if (Node21.isConstructorDeclaration(m)) return false; - return !Node20.isParameterDeclaration(m) && m.isStatic(); + return !Node21.isParameterDeclaration(m) && m.isStatic(); }); } getMembersWithParameterProperties() { const members = this.getMembers(); - const implementationCtors = members.filter((c) => Node20.isConstructorDeclaration(c) && c.isImplementation()); + const implementationCtors = members.filter((c) => Node21.isConstructorDeclaration(c) && c.isImplementation()); for (const ctor of implementationCtors) { let insertIndex = members.indexOf(ctor) + 1; for (const param of ctor.getParameters()) { @@ -236128,7 +236128,7 @@ Node text: ${this.#forgottenText}`; getMembersWithComments() { const compilerNode = this.compilerNode; const members = ExtendedParser.getContainerArray(compilerNode, this.getSourceFile().compilerNode); - return getAllMembers(this, members).filter((m) => isSupportedClassMember(m) || Node20.isCommentClassElement(m)); + return getAllMembers(this, members).filter((m) => isSupportedClassMember(m) || Node21.isCommentClassElement(m)); } getMember(nameOrFindFunction) { return getNodeByNameOrFindFunction(this.getMembers(), nameOrFindFunction); @@ -236166,9 +236166,9 @@ Node text: ${this.#forgottenText}`; const isAmbient = isNodeAmbientOrInAmbientContext(classDec); const members = compilerMembers.map((m) => classDec._getNodeFromCompilerNode(m)); return isAmbient ? members : members.filter((m) => { - if (!(Node20.isConstructorDeclaration(m) || Node20.isMethodDeclaration(m))) + if (!(Node21.isConstructorDeclaration(m) || Node21.isMethodDeclaration(m))) return true; - if (Node20.isMethodDeclaration(m) && m.isAbstract()) + if (Node21.isMethodDeclaration(m) && m.isAbstract()) return true; return m.isImplementation(); }); @@ -236194,10 +236194,10 @@ Node text: ${this.#forgottenText}`; return classes; } function isClassPropertyType(m) { - return Node20.isPropertyDeclaration(m) || Node20.isSetAccessorDeclaration(m) || Node20.isGetAccessorDeclaration(m) || Node20.isParameterDeclaration(m); + return Node21.isPropertyDeclaration(m) || Node21.isSetAccessorDeclaration(m) || Node21.isGetAccessorDeclaration(m) || Node21.isParameterDeclaration(m); } function isSupportedClassMember(m) { - return Node20.isMethodDeclaration(m) || Node20.isPropertyDeclaration(m) || Node20.isGetAccessorDeclaration(m) || Node20.isSetAccessorDeclaration(m) || Node20.isConstructorDeclaration(m) || Node20.isClassStaticBlockDeclaration(m); + return Node21.isMethodDeclaration(m) || Node21.isPropertyDeclaration(m) || Node21.isGetAccessorDeclaration(m) || Node21.isSetAccessorDeclaration(m) || Node21.isConstructorDeclaration(m) || Node21.isClassStaticBlockDeclaration(m); } function insertChildren(classDeclaration, opts) { return insertIntoBracesOrSourceFileWithGetChildren({ @@ -236265,7 +236265,7 @@ Node text: ${this.#forgottenText}`; typeParameters: this.getTypeParameters().map((p) => p.getStructure()), properties: [ ...parameterProperties.map((p) => { - const jsDocComment = p.getParentOrThrow().getJsDocs().map((j) => j.getTags()).flat().filter(Node20.isJSDocParameterTag).filter((t) => t.getTagName() === "param" && t.getName() === p.getName() && t.getComment() != null).map((t) => t.getCommentText().trim())[0]; + const jsDocComment = p.getParentOrThrow().getJsDocs().map((j) => j.getTags()).flat().filter(Node21.isJSDocParameterTag).filter((t) => t.getTagName() === "param" && t.getName() === p.getName() && t.getComment() != null).map((t) => t.getCommentText().trim())[0]; return { kind: exports2.StructureKind.PropertySignature, docs: jsDocComment == null ? [] : [{ kind: exports2.StructureKind.JSDoc, description: jsDocComment }], @@ -236340,7 +236340,7 @@ Node text: ${this.#forgottenText}`; name: getAndSet[0].getName(), type: getAndSet[0].getType().getText(getAndSet[0]), hasQuestionToken: false, - isReadonly: getAndSet.every(Node20.isGetAccessorDeclaration) + isReadonly: getAndSet.every(Node21.isGetAccessorDeclaration) }; } function getExtractedInterfaceMethodStructure(method) { @@ -236457,7 +236457,7 @@ Node text: ${this.#forgottenText}`; const thisName = this.getName(); const isStatic = this.isStatic(); return this.getParentOrThrow().forEachChild((sibling) => { - if (Node20.isSetAccessorDeclaration(sibling) && sibling.getName() === thisName && sibling.isStatic() === isStatic) + if (Node21.isSetAccessorDeclaration(sibling) && sibling.getName() === thisName && sibling.isStatic() === isStatic) return sibling; return void 0; }); @@ -236514,7 +236514,7 @@ Node text: ${this.#forgottenText}`; const thisName = this.getName(); const isStatic = this.isStatic(); return this.getParentOrThrow().forEachChild((sibling) => { - if (Node20.isGetAccessorDeclaration(sibling) && sibling.getName() === thisName && sibling.isStatic() === isStatic) + if (Node21.isGetAccessorDeclaration(sibling) && sibling.getName() === thisName && sibling.isStatic() === isStatic) return sibling; return void 0; }); @@ -236528,7 +236528,7 @@ Node text: ${this.#forgottenText}`; }); } }; - var DecoratorBase = LeftHandSideExpressionedNode(Node20); + var DecoratorBase = LeftHandSideExpressionedNode(Node21); var Decorator = class extends DecoratorBase { getName() { return this.getNameNode().getText(); @@ -236541,13 +236541,13 @@ Node text: ${this.#forgottenText}`; return getIdentifierFromName(this._getInnerExpression()); function getIdentifierFromName(expression) { const identifier = getNameFromExpression(expression); - if (!Node20.isIdentifier(identifier)) { + if (!Node21.isIdentifier(identifier)) { throw new common.errors.NotImplementedError(`Expected the decorator expression '${identifier.getText()}' to be an identifier. Please deal directly with 'getExpression()' on the decorator to handle more complex scenarios.`); } return identifier; } function getNameFromExpression(expression) { - if (Node20.isPropertyAccessExpression(expression)) + if (Node21.isPropertyAccessExpression(expression)) return expression.getNameNode(); return expression; } @@ -236559,7 +236559,7 @@ Node text: ${this.#forgottenText}`; return this.compilerNode.expression.getText(sourceFile.compilerNode); } isDecoratorFactory() { - return Node20.isCallExpression(this._getInnerExpression()); + return Node21.isCallExpression(this._getInnerExpression()); } setIsDecoratorFactory(isDecoratorFactory) { if (this.isDecoratorFactory() === isDecoratorFactory) @@ -236601,7 +236601,7 @@ Node text: ${this.#forgottenText}`; } getCallExpression() { const expression = this._getInnerExpression(); - return Node20.isCallExpression(expression) ? expression : void 0; + return Node21.isCallExpression(expression) ? expression : void 0; } getArguments() { return this.getCallExpression()?.getArguments() ?? []; @@ -236665,7 +236665,7 @@ Node text: ${this.#forgottenText}`; } _getInnerExpression() { let expr = this.getExpression(); - while (Node20.isParenthesizedExpression(expr)) + while (Node21.isParenthesizedExpression(expr)) expr = expr.getExpression(); return expr; } @@ -236754,7 +236754,7 @@ Node text: ${this.#forgottenText}`; return -1; } } - var JSDocBase = Node20; + var JSDocBase = Node21; var JSDoc = class extends JSDocBase { isMultiLine() { return this.getText().includes("\n"); @@ -236895,7 +236895,7 @@ Node text: ${this.#forgottenText}`; }); } }; - var TypeNode = class extends Node20 { + var TypeNode = class extends Node21 { }; var NodeWithTypeArgumentsBase = TypeArgumentedNode(TypeNode); var NodeWithTypeArguments = class extends NodeWithTypeArgumentsBase { @@ -236934,9 +236934,9 @@ Node text: ${this.#forgottenText}`; var ImportTypeNode = class extends NodeWithTypeArguments { setArgument(text2) { const arg = this.getArgument(); - if (Node20.isLiteralTypeNode(arg)) { + if (Node21.isLiteralTypeNode(arg)) { const literal = arg.getLiteral(); - if (Node20.isStringLiteral(literal)) { + if (Node21.isStringLiteral(literal)) { literal.setLiteralValue(text2); return this; } @@ -237115,7 +237115,7 @@ Node text: ${this.#forgottenText}`; TypeParameterVariance[TypeParameterVariance["InOut"] = 3] = "InOut"; })(exports2.TypeParameterVariance || (exports2.TypeParameterVariance = {})); var createBase$c = (ctor) => ModifierableNode(NamedNode(ctor)); - var TypeParameterDeclarationBase = createBase$c(Node20); + var TypeParameterDeclarationBase = createBase$c(Node21); var TypeParameterDeclaration = class extends TypeParameterDeclarationBase { isConst() { return this.hasModifier(common.SyntaxKind.ConstKeyword); @@ -237285,7 +237285,7 @@ Node text: ${this.#forgottenText}`; }; var JSDocAllType = class extends JSDocType { }; - var JSDocTagBase = Node20; + var JSDocTagBase = Node21; var JSDocTag = class extends JSDocTagBase { getTagName() { return this.getTagNameNode().getText(); @@ -237411,20 +237411,20 @@ Node text: ${this.#forgottenText}`; }; var JSDocImportTag = class extends JSDocTag { }; - var JSDocLink = class extends Node20 { + var JSDocLink = class extends Node21 { }; - var JSDocLinkCode = class extends Node20 { + var JSDocLinkCode = class extends Node21 { }; - var JSDocLinkPlain = class extends Node20 { + var JSDocLinkPlain = class extends Node21 { }; - var JSDocMemberName = class extends Node20 { + var JSDocMemberName = class extends Node21 { }; var JSDocNamepathType = class extends JSDocType { getTypeNode() { return this._getNodeFromCompilerNode(this.compilerNode.type); } }; - var JSDocNameReference = class extends Node20 { + var JSDocNameReference = class extends Node21 { getName() { return this._getNodeFromCompilerNode(this.compilerNode.name); } @@ -237507,7 +237507,7 @@ Node text: ${this.#forgottenText}`; return common.errors.throwIfNullOrUndefined(this.getConstraint(), message ?? "Expected to find the JS doc template tag's constraint.", this); } }; - var JSDocText = class extends Node20 { + var JSDocText = class extends Node21 { }; var JSDocThisTagBase = JSDocTypeExpressionableTag(JSDocTag); var JSDocThisTag = class extends JSDocThisTagBase { @@ -237547,7 +237547,7 @@ Node text: ${this.#forgottenText}`; return this._getNodeFromCompilerNode(this.compilerNode.type); } }; - var CommentEnumMember = class extends Node20 { + var CommentEnumMember = class extends Node21 { remove() { removeChildrenWithFormatting({ children: [this], @@ -237631,7 +237631,7 @@ Node text: ${this.#forgottenText}`; } }; var createBase$a = (ctor) => JSDocableNode(InitializerExpressionableNode(PropertyNamedNode(ctor))); - var EnumMemberBase = createBase$a(Node20); + var EnumMemberBase = createBase$a(Node21); var EnumMember = class extends EnumMemberBase { getValue() { return this._context.typeChecker.getConstantValue(this); @@ -237672,7 +237672,7 @@ Node text: ${this.#forgottenText}`; }); } }; - var HeritageClause = class extends Node20 { + var HeritageClause = class extends Node21 { getTypeNodes() { return this.compilerNode.types?.map((t) => this._getNodeFromCompilerNode(t)) ?? []; } @@ -237697,7 +237697,7 @@ Node text: ${this.#forgottenText}`; } } }; - var TypeElement = class extends Node20 { + var TypeElement = class extends Node21 { remove() { removeInterfaceMember(this); } @@ -237910,7 +237910,7 @@ Node text: ${this.#forgottenText}`; } }; } - var ComputedPropertyNameBase = ExpressionedNode(Node20); + var ComputedPropertyNameBase = ExpressionedNode(Node21); var ComputedPropertyName = class extends ComputedPropertyNameBase { }; var IdentifierBase = CommonIdentifierBase(ReferenceFindableNode(RenameableNode(PrimaryExpression))); @@ -237919,10 +237919,10 @@ Node text: ${this.#forgottenText}`; return this._context.languageService.getImplementations(this); } }; - var PrivateIdentifierBase = CommonIdentifierBase(ReferenceFindableNode(RenameableNode(Node20))); + var PrivateIdentifierBase = CommonIdentifierBase(ReferenceFindableNode(RenameableNode(Node21))); var PrivateIdentifier = class extends PrivateIdentifierBase { }; - var QualifiedName = class extends Node20 { + var QualifiedName = class extends Node21 { getLeft() { return this._getNodeFromCompilerNode(this.compilerNode.left); } @@ -237930,7 +237930,7 @@ Node text: ${this.#forgottenText}`; return this._getNodeFromCompilerNode(this.compilerNode.right); } }; - var JsxAttributeBase = Node20; + var JsxAttributeBase = Node21; var JsxAttribute = class extends JsxAttributeBase { getNameNode() { return this._getNodeFromCompilerNode(this.compilerNode.name); @@ -238007,7 +238007,7 @@ Node text: ${this.#forgottenText}`; } }; var createBase$3 = (ctor) => JsxTagNamedNode(ctor); - var JsxClosingElementBase = createBase$3(Node20); + var JsxClosingElementBase = createBase$3(Node21); var JsxClosingElement = class extends JsxClosingElementBase { }; var JsxClosingFragment = class extends Expression { @@ -238096,7 +238096,7 @@ Node text: ${this.#forgottenText}`; return this._getNodeFromCompilerNode(this.compilerNode.closingFragment); } }; - var JsxNamespacedNameBase = Node20; + var JsxNamespacedNameBase = Node21; var JsxNamespacedName = class extends JsxNamespacedNameBase { getNamespaceNode() { return this._getNodeFromCompilerNode(this.compilerNode.namespace); @@ -238135,7 +238135,7 @@ Node text: ${this.#forgottenText}`; }); } }; - var JsxSpreadAttributeBase = ExpressionedNode(Node20); + var JsxSpreadAttributeBase = ExpressionedNode(Node21); var JsxSpreadAttribute = class extends JsxSpreadAttributeBase { remove() { removeChildren({ @@ -238157,7 +238157,7 @@ Node text: ${this.#forgottenText}`; }); } }; - var JsxTextBase = LiteralLikeNode(Node20); + var JsxTextBase = LiteralLikeNode(Node21); var JsxText = class extends JsxTextBase { containsOnlyTriviaWhiteSpaces() { const oldCompilerNode = this.compilerNode; @@ -238247,8 +238247,8 @@ Node text: ${this.#forgottenText}`; getLiteralValue() { const pattern = /^\/(.*)\/([^\/]*)$/; const text2 = this.compilerNode.text; - const matches2 = pattern.exec(text2); - return new RegExp(matches2[1], matches2[2]); + const matches3 = pattern.exec(text2); + return new RegExp(matches3[1], matches3[2]); } setLiteralValue(regExpOrPattern, flags) { let pattern; @@ -238346,23 +238346,23 @@ Node text: ${this.#forgottenText}`; return parent.getChildAtIndex(childIndex); } }; - var TemplateHeadBase = LiteralLikeNode(Node20); + var TemplateHeadBase = LiteralLikeNode(Node21); var TemplateHead = class extends TemplateHeadBase { }; - var TemplateMiddleBase = LiteralLikeNode(Node20); + var TemplateMiddleBase = LiteralLikeNode(Node21); var TemplateMiddle = class extends TemplateMiddleBase { }; - var TemplateSpanBase = ExpressionedNode(Node20); + var TemplateSpanBase = ExpressionedNode(Node21); var TemplateSpan = class extends TemplateSpanBase { getLiteral() { return this._getNodeFromCompilerNode(this.compilerNode.literal); } }; - var TemplateTailBase = LiteralLikeNode(Node20); + var TemplateTailBase = LiteralLikeNode(Node21); var TemplateTail = class extends TemplateTailBase { }; var createBase = (ctor) => ExportGetableNode(ExclamationTokenableNode(TypedNode(InitializerExpressionableNode(BindingNamedNode(ctor))))); - var VariableDeclarationBase = createBase(Node20); + var VariableDeclarationBase = createBase(Node21); var VariableDeclaration = class extends VariableDeclarationBase { remove() { const parent = this.getParentOrThrow(); @@ -238400,7 +238400,7 @@ Node text: ${this.#forgottenText}`; } getVariableStatement() { const grandParent = this.getParentOrThrow().getParentOrThrow(); - return Node20.isVariableStatement(grandParent) ? grandParent : void 0; + return Node21.isVariableStatement(grandParent) ? grandParent : void 0; } set(structure) { callBaseSet(VariableDeclarationBase.prototype, this, structure); @@ -238412,7 +238412,7 @@ Node text: ${this.#forgottenText}`; }); } }; - var VariableDeclarationListBase = ModifierableNode(Node20); + var VariableDeclarationListBase = ModifierableNode(Node21); var VariableDeclarationList = class extends VariableDeclarationListBase { getDeclarations() { return this.compilerNode.declarations.map((d) => this._getNodeFromCompilerNode(d)); @@ -239873,7 +239873,7 @@ Node text: ${this.#forgottenText}`; if (symbol == null) return false; const valueDeclaration = symbol.getValueDeclaration(); - return valueDeclaration != null && Node20.isEnumDeclaration(valueDeclaration); + return valueDeclaration != null && Node21.isEnumDeclaration(valueDeclaration); } isInterface() { return this.#hasObjectFlag(common.ObjectFlags.Interface); @@ -239958,7 +239958,7 @@ Node text: ${this.#forgottenText}`; const declaration = symbol.getDeclarations()[0]; if (declaration == null) return void 0; - if (!Node20.isTypeParameterDeclaration(declaration)) + if (!Node21.isTypeParameterDeclaration(declaration)) return void 0; return declaration; } @@ -240942,13 +240942,13 @@ Node text: ${this.#forgottenText}`; [common.SyntaxKind.WhileStatement]: WhileStatement, [common.SyntaxKind.WithStatement]: WithStatement, [common.SyntaxKind.YieldExpression]: YieldExpression, - [common.SyntaxKind.SemicolonToken]: Node20, + [common.SyntaxKind.SemicolonToken]: Node21, [common.SyntaxKind.AnyKeyword]: Expression, [common.SyntaxKind.BooleanKeyword]: Expression, [common.SyntaxKind.FalseKeyword]: FalseLiteral, [common.SyntaxKind.ImportKeyword]: ImportExpression, - [common.SyntaxKind.InferKeyword]: Node20, - [common.SyntaxKind.NeverKeyword]: Node20, + [common.SyntaxKind.InferKeyword]: Node21, + [common.SyntaxKind.NeverKeyword]: Node21, [common.SyntaxKind.NullKeyword]: NullLiteral, [common.SyntaxKind.NumberKeyword]: Expression, [common.SyntaxKind.ObjectKeyword]: Expression, @@ -241112,7 +241112,7 @@ Node text: ${this.#forgottenText}`; return new CommentEnumMember(this.#context, compilerNode, sourceFile); return common.errors.throwNotImplementedForNeverValueError(compilerNode); } - const ctor = kindToWrapperMappings[compilerNode.kind] || Node20; + const ctor = kindToWrapperMappings[compilerNode.kind] || Node21; return new ctor(this.#context, compilerNode, sourceFile); } function isCommentNode(node) { @@ -241243,8 +241243,8 @@ Node text: ${this.#forgottenText}`; return this.#jsDocTagInfoCache.getOrCreate(jsDocTagInfo, () => new JSDocTagInfo(jsDocTagInfo)); } replaceCompilerNode(oldNode, newNode) { - const nodeToReplace = oldNode instanceof Node20 ? oldNode.compilerNode : oldNode; - const node = oldNode instanceof Node20 ? oldNode : this.#nodeCache.get(oldNode); + const nodeToReplace = oldNode instanceof Node21 ? oldNode.compilerNode : oldNode; + const node = oldNode instanceof Node21 ? oldNode : this.#nodeCache.get(oldNode); if (nodeToReplace.kind === common.SyntaxKind.SourceFile && nodeToReplace.fileName !== newNode.fileName) { const sourceFile = node; this.#removeCompilerNodeFromCache(nodeToReplace); @@ -241289,12 +241289,12 @@ Node text: ${this.#forgottenText}`; for (const node of nodes) this.#nodeCache.rememberNode(node); }); - if (Node20.isNode(result)) + if (Node21.isNode(result)) this.#nodeCache.rememberNode(result); if (isPromise(result)) { wasPromise = true; return result.then((value) => { - if (Node20.isNode(value)) + if (Node21.isNode(value)) this.#nodeCache.rememberNode(value); this.#nodeCache.forgetLastPoint(); return value; @@ -242422,7 +242422,7 @@ Node text: ${this.#forgottenText}`; exports2.NewExpressionBase = NewExpressionBase; exports2.NoSubstitutionTemplateLiteral = NoSubstitutionTemplateLiteral; exports2.NoSubstitutionTemplateLiteralBase = NoSubstitutionTemplateLiteralBase; - exports2.Node = Node20; + exports2.Node = Node21; exports2.NodeWithTypeArguments = NodeWithTypeArguments; exports2.NodeWithTypeArgumentsBase = NodeWithTypeArgumentsBase; exports2.NonNullExpression = NonNullExpression; @@ -243191,9 +243191,9 @@ var require_Alias = __commonJS({ var anchors = require_anchors(); var visit = require_visit(); var identity = require_identity(); - var Node20 = require_Node(); + var Node21 = require_Node(); var toJS = require_toJS(); - var Alias = class extends Node20.NodeBase { + var Alias = class extends Node21.NodeBase { constructor(source) { super(identity.ALIAS); this.source = source; @@ -243307,10 +243307,10 @@ var require_Scalar = __commonJS({ "node_modules/yaml/dist/nodes/Scalar.js"(exports2) { "use strict"; var identity = require_identity(); - var Node20 = require_Node(); + var Node21 = require_Node(); var toJS = require_toJS(); var isScalarValue = (value) => !value || typeof value !== "function" && typeof value !== "object"; - var Scalar = class extends Node20.NodeBase { + var Scalar = class extends Node21.NodeBase { constructor(value) { super(identity.SCALAR); this.value = value; @@ -243413,7 +243413,7 @@ var require_Collection = __commonJS({ "use strict"; var createNode = require_createNode(); var identity = require_identity(); - var Node20 = require_Node(); + var Node21 = require_Node(); function collectionFromPath(schema, path2, value) { let v = value; for (let i = path2.length - 1; i >= 0; --i) { @@ -243437,7 +243437,7 @@ var require_Collection = __commonJS({ }); } var isEmptyPath = (path2) => path2 == null || typeof path2 === "object" && !!path2[Symbol.iterator]().next().done; - var Collection = class extends Node20.NodeBase { + var Collection = class extends Node21.NodeBase { constructor(type, schema) { super(type); Object.defineProperty(this, "schema", { @@ -255678,11 +255678,11 @@ function assembleStyles() { }, hexToRgb: { value(hex) { - const matches2 = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16)); - if (!matches2) { + const matches3 = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16)); + if (!matches3) { return [0, 0, 0]; } - let [colorString] = matches2; + let [colorString] = matches3; if (colorString.length === 3) { colorString = [...colorString].map((character) => character + character).join(""); } @@ -257675,11 +257675,11 @@ function collectCalls(sourceFile, scope) { const path3 = literalText(first); const handlers2 = path3 === null ? call.getArguments() : rest; const middleware = []; - const mounts = []; + const mounts2 = []; for (const handler of handlers2) { const mounted = mountedRouter(handler, scope); if (mounted) { - mounts.push(mounted); + mounts2.push(mounted); continue; } middleware.push(handler.getText()); @@ -257698,7 +257698,7 @@ function collectCalls(sourceFile, scope) { position: call.getStart(), path: path3 ?? "", middleware, - mounts + mounts: mounts2 }); continue; } @@ -257761,12 +257761,12 @@ function routerMiddlewareBefore(uses, router, file, position) { ); } function inherited(router, uses, visiting) { - const mounts = uses.filter((use) => use.mounts.includes(router)); - if (mounts.length === 0 || visiting.has(router)) { + const mounts2 = uses.filter((use) => use.mounts.includes(router)); + if (mounts2.length === 0 || visiting.has(router)) { return { auth: false, validation: false, prefix: "" }; } visiting.add(router); - const paths = mounts.map((mount) => { + const paths = mounts2.map((mount) => { const parent = inherited(mount.router, uses, visiting); const protection = either( either(protects(mount.middleware), parent), @@ -259406,15 +259406,15 @@ var TEST_EXCLUDES = [ "**/*.spec.ts", "**/{test,tests,__tests__,e2e}/**" ]; -function helpersCalling(sourceFile, matches2) { +function helpersCalling(sourceFile, matches3) { const names = []; for (const declaration of sourceFile.getFunctions()) { const name = declaration.getName(); - if (name && matches2(declaration.getText())) names.push(name); + if (name && matches3(declaration.getText())) names.push(name); } for (const declaration of sourceFile.getVariableDeclarations()) { const initializer = declaration.getInitializer(); - if (initializer && matches2(initializer.getText())) { + if (initializer && matches3(initializer.getText())) { names.push(declaration.getName()); } } @@ -262867,10 +262867,243 @@ async function analyzeTesting(context) { } // src/analyzers/trpc-api.ts -var import_ts_morph20 = __toESM(require_ts_morph(), 1); +var import_ts_morph21 = __toESM(require_ts_morph(), 1); -// src/analyzers/trpc-procedures.ts +// src/analyzers/trpc-perimeter.ts var import_ts_morph19 = __toESM(require_ts_morph(), 1); +var ADAPTER_MODULE2 = /^@trpc\/server\/adapters\/|^@hono\/trpc-server$/; +var IDENTITY2 = /(?:session|token|auth|user|userId|identity|viewer|jwt)\b/i; +var REJECTION = /\bredirect\s*\(|\brewrite\s*\(|status\s*:\s*40[13]\b|\b40[13]\b/; +var NEXT_AUTH_MIDDLEWARE = "next-auth/middleware"; +var MIDDLEWARE_FILES = ["middleware", "src/middleware", "proxy", "src/proxy"]; +var MIDDLEWARE_EXTENSIONS = [".ts", ".js", ".mjs"]; +var SAMPLE_PROCEDURE = "post.create"; +var MAX_CONTEXT_DEPTH = 2; +function functionValue(index, node, depth = 0) { + if (!node || depth > MAX_CONTEXT_DEPTH + 2) return void 0; + const value = definition(index, node); + if (isFunctionLike(value)) return value; + if (import_ts_morph19.Node.isVariableDeclaration(value)) { + return functionValue(index, value.getInitializer(), depth + 1); + } + return void 0; +} +function guardsAnonymous(fn, rejects) { + return fn.getDescendantsOfKind(import_ts_morph19.SyntaxKind.IfStatement).some((statement) => { + const condition = unwrap(statement.getExpression()); + return import_ts_morph19.Node.isPrefixUnaryExpression(condition) && condition.getOperatorToken() === import_ts_morph19.SyntaxKind.ExclamationToken && IDENTITY2.test(condition.getOperand().getText()) && rejects(statement.getThenStatement()); + }); +} +function throws(branch) { + return import_ts_morph19.Node.isThrowStatement(branch) || branch.getDescendantsOfKind(import_ts_morph19.SyntaxKind.ThrowStatement).length > 0; +} +function rejectingFunction(index, fn, depth = 0) { + if (throwsAuthError(fn) || guardsAnonymous(fn, throws)) return fn; + if (depth >= MAX_CONTEXT_DEPTH) return void 0; + for (const call of fn.getDescendantsOfKind(import_ts_morph19.SyntaxKind.CallExpression)) { + const callee = functionValue(index, call.getExpression()); + const found = callee && rejectingFunction(index, callee, depth + 1); + if (found) return found; + } + return void 0; +} +function adapterCalls(index, sources) { + const calls = []; + for (const { file, sourceFile } of sources) { + if (!/@trpc\/server\/adapters\/|@hono\/trpc-server/.test( + sourceFile.getFullText() + )) { + continue; + } + for (const call of sourceFile.getDescendantsOfKind( + import_ts_morph19.SyntaxKind.CallExpression + )) { + const [first, second] = call.getArguments(); + if (ADAPTER_MODULE2.test(moduleOf(call.getExpression()) ?? "")) { + calls.push({ call, file, options: first }); + } else if ( + // fastify.register(fastifyTRPCPlugin, { trpcOptions: { createContext } }) + methodName(call) === "register" && first && ADAPTER_MODULE2.test(moduleOf(first) ?? "") + ) { + const options = objectLiteral(index, second); + calls.push({ + call, + file, + options: propertyValue(options?.getProperty("trpcOptions")) + }); + } + } + } + return calls; +} +function rejectingContext(index, adapters) { + const contexts = adapters.flatMap(({ options }) => { + const value = propertyValue( + objectLiteral(index, options)?.getProperty("createContext") + ); + const fn = functionValue(index, value); + return fn ? [fn] : []; + }); + const rejecting = contexts.map((fn) => rejectingFunction(index, fn)); + const [first] = rejecting; + if (!first || rejecting.some((fn) => fn === void 0)) return null; + return `createContext in ${index.paths.get(first.getSourceFile())} rejects anonymous callers`; +} +function middlewareName(node) { + const value = unwrap(node); + return import_ts_morph19.Node.isCallExpression(value) ? value.getExpression().getText() : value.getText(); +} +function isAuthMiddleware(node) { + return AUTH_NAMES4.test(middlewareName(node)); +} +function mounts(call) { + const parent = call.getParent(); + if (import_ts_morph19.Node.isCallExpression(parent) && methodName(parent) === "use") { + return [parent]; + } + if (!import_ts_morph19.Node.isVariableDeclaration(parent)) return []; + const name = parent.getName(); + return call.getSourceFile().getDescendantsOfKind(import_ts_morph19.SyntaxKind.CallExpression).filter( + (use) => methodName(use) === "use" && use.getArguments().some((argument) => argument.getText() === name) + ); +} +function mountAuth(use) { + const args = use.getArguments(); + const mountPath = literalText2(args[0]); + const handlers = mountPath === null ? args : args.slice(1); + const own = handlers.slice(0, -1).find(isAuthMiddleware); + if (own) return middlewareName(own); + const receiver = receiverOf(use).getText(); + for (const earlier of use.getSourceFile().getDescendantsOfKind(import_ts_morph19.SyntaxKind.CallExpression)) { + if (earlier.getStart() >= use.getStart()) break; + if (methodName(earlier) !== "use") continue; + if (receiverOf(earlier).getText() !== receiver) continue; + const [first, ...rest] = earlier.getArguments(); + const path2 = literalText2(first); + const applies = path2 === null || mountPath !== null && (path2 === "/" || `${mountPath}/`.startsWith(`${path2.replace(/\/$/, "")}/`)); + if (!applies) continue; + const middleware = (path2 === null ? earlier.getArguments() : rest).find( + isAuthMiddleware + ); + if (middleware) return middlewareName(middleware); + } + return null; +} +function expressMountAuth(adapters) { + const mounted = adapters.flatMap( + ({ call, file }) => mounts(call).map((use) => ({ use, file })) + ); + if (mounted.length === 0) return null; + const guarded = mounted.map(({ use, file }) => ({ + file, + middleware: mountAuth(use) + })); + if (!guarded.every(({ middleware }) => middleware !== null)) return null; + const [first] = guarded; + return first ? `${first.middleware} runs before the tRPC mount in ${first.file}` : null; +} +function routePath(file) { + const match2 = /^(?:src\/)?(app|pages)\/(.+)$/.exec(file); + if (!match2) return null; + const [, router, rest = ""] = match2; + const segments = rest.split("/"); + const last = segments.pop() ?? ""; + if (router === "app") { + if (!/^route\.[cm]?[jt]sx?$/.test(last)) return null; + } else { + segments.push(last.replace(/\.[cm]?[jt]sx?$/, "")); + } + const path2 = segments.filter((segment) => !/^\(.*\)$/.test(segment) && segment !== "index").map((segment) => /^\[.*\]$/.test(segment) ? SAMPLE_PROCEDURE : segment); + return `/${path2.join("/")}`; +} +function matches(pattern, path2) { + const source = pattern.replace(/\/:\w+\*/g, "(?:/.*)?").replace(/\/:\w+\+/g, "/.+").replace(/\/:\w+\?/g, "(?:/[^/]+)?").replace(/:\w+/g, "[^/]+"); + try { + return new RegExp(`^${source}$`).test(path2); + } catch { + return false; + } +} +function literalPatterns(index, node) { + if (!node) return null; + const value = unwrap(node); + const single = literalText2(value); + if (single !== null) return [single]; + if (!import_ts_morph19.Node.isArrayLiteralExpression(value)) return null; + const patterns = []; + for (const element of value.getElements()) { + const text2 = literalText2(element) ?? literalText2( + propertyValue(objectLiteral(index, element)?.getProperty("source")) + ); + if (text2 === null) return null; + patterns.push(text2); + } + return patterns; +} +function runsFor(index, sourceFile, path2) { + const config = sourceFile.getVariableDeclaration("config"); + if (!config?.isExported()) return true; + const matcher = objectLiteral(index, config.getInitializer())?.getProperty( + "matcher" + ); + if (!matcher) return true; + const patterns = literalPatterns(index, propertyValue(matcher)); + return patterns?.some((pattern) => matches(pattern, path2)) ?? false; +} +function protectsPath(index, sourceFile, path2) { + return sourceFile.getDescendantsOfKind(import_ts_morph19.SyntaxKind.CallExpression).filter((call) => methodName(call) === "protect").some((call) => { + const guard = call.getFirstAncestorByKind(import_ts_morph19.SyntaxKind.IfStatement); + if (!guard) return true; + let condition = unwrap(guard.getExpression()); + let negated = false; + if (import_ts_morph19.Node.isPrefixUnaryExpression(condition) && condition.getOperatorToken() === import_ts_morph19.SyntaxKind.ExclamationToken) { + negated = true; + condition = unwrap(condition.getOperand()); + } + if (!import_ts_morph19.Node.isCallExpression(condition)) return false; + const matcher = definition(index, condition.getExpression()); + const created = import_ts_morph19.Node.isVariableDeclaration(matcher) ? matcher.getInitializer() : void 0; + if (!import_ts_morph19.Node.isCallExpression(created)) return false; + const patterns = literalPatterns(index, created.getArguments()[0]); + if (patterns === null) return false; + const covered = patterns.some((pattern) => matches(pattern, path2)); + return negated ? !covered : covered; + }); +} +function middlewareRejects(index, sourceFile, path2) { + const nextAuth = [ + ...sourceFile.getExportDeclarations(), + ...sourceFile.getImportDeclarations() + ].some( + (declaration) => declaration.getModuleSpecifierValue() === NEXT_AUTH_MIDDLEWARE + ); + return nextAuth || protectsPath(index, sourceFile, path2) || guardsAnonymous(sourceFile, (branch) => REJECTION.test(branch.getText())); +} +function nextMiddlewareAuth(index, adapters) { + const paths = [ + ...new Set(adapters.flatMap(({ file: file2 }) => routePath(file2) ?? [])) + ]; + if (paths.length === 0) return null; + const file = MIDDLEWARE_FILES.flatMap( + (name) => MIDDLEWARE_EXTENSIONS.map((extension) => `${name}${extension}`) + ).find((candidate) => index.files.has(candidate)); + const sourceFile = file ? index.files.get(file) : void 0; + if (!file || !sourceFile) return null; + const covered = paths.every( + (path2) => runsFor(index, sourceFile, path2) && middlewareRejects(index, sourceFile, path2) + ); + if (!covered) return null; + const endpoint = (paths[0] ?? "").replace(`/${SAMPLE_PROCEDURE}`, ""); + return `${file} rejects unauthenticated requests to ${endpoint}`; +} +function perimeterAuth(index, sources) { + const adapters = adapterCalls(index, sources); + if (adapters.length === 0) return null; + return rejectingContext(index, adapters) ?? expressMountAuth(adapters) ?? nextMiddlewareAuth(index, adapters); +} + +// src/analyzers/trpc-procedures.ts +var import_ts_morph20 = __toESM(require_ts_morph(), 1); var PROCEDURE_KINDS = /* @__PURE__ */ new Set([ "mutation", "query", @@ -262914,7 +263147,7 @@ var TrpcResolver = class { for (const { file, sourceFile } of sources) { if (importsTrpcAdapter(sourceFile)) adapters++; for (const call of sourceFile.getDescendantsOfKind( - import_ts_morph19.SyntaxKind.CallExpression + import_ts_morph20.SyntaxKind.CallExpression )) { if (isCreateCall(call)) instances.push(call); else if (this.routerKind(call)) routers.push(call); @@ -262934,20 +263167,20 @@ var TrpcResolver = class { */ resolve(node) { const resolved = definition(this.index, node); - const pattern = import_ts_morph19.Node.isVariableDeclaration(resolved) ? resolved.getNameNode() : void 0; - if (!import_ts_morph19.Node.isObjectBindingPattern(pattern)) return resolved; + const pattern = import_ts_morph20.Node.isVariableDeclaration(resolved) ? resolved.getNameNode() : void 0; + if (!import_ts_morph20.Node.isObjectBindingPattern(pattern)) return resolved; const value = unwrap(node); - const declaration = import_ts_morph19.Node.isIdentifier(value) ? value.getSymbol()?.getDeclarations()[0] : void 0; + const declaration = import_ts_morph20.Node.isIdentifier(value) ? value.getSymbol()?.getDeclarations()[0] : void 0; const name = (declaration && importSource(declaration)?.name) ?? value.getText(); return pattern.getElements().find((element) => element.getName() === name); } /** The `initTRPC...create()` call an expression holds, if any. */ instance(node, depth = 0) { const value = unwrap(node); - if (import_ts_morph19.Node.isCallExpression(value)) return isCreateCall(value) ? value : null; + if (import_ts_morph20.Node.isCallExpression(value)) return isCreateCall(value) ? value : null; if (depth > MAX_RESOLUTION_DEPTH3) return null; const resolved = this.resolve(value); - const initializer = import_ts_morph19.Node.isVariableDeclaration(resolved) ? resolved.getInitializer() : void 0; + const initializer = import_ts_morph20.Node.isVariableDeclaration(resolved) ? resolved.getInitializer() : void 0; return initializer ? this.instance(initializer, depth + 1) : null; } /** @@ -262977,7 +263210,7 @@ var TrpcResolver = class { if (!options) return []; const exposures = []; const isDev = options.getProperty("isDev"); - if (import_ts_morph19.Node.isPropertyAssignment(isDev) && isDev.getInitializer()?.getKind() === import_ts_morph19.SyntaxKind.TrueKeyword) { + if (import_ts_morph20.Node.isPropertyAssignment(isDev) && isDev.getInitializer()?.getKind() === import_ts_morph20.SyntaxKind.TrueKeyword) { exposures.push({ node: isDev, via: "isDev" }); } const formatter = this.functionOf( @@ -262992,7 +263225,7 @@ var TrpcResolver = class { const resolver = call.getArguments().at(-1); if (!PROCEDURE_KINDS.has(kind) || !resolver) return; const value = unwrap(resolver); - if (!isFunctionLike(value) && !import_ts_morph19.Node.isIdentifier(value) && !import_ts_morph19.Node.isPropertyAccessExpression(value)) { + if (!isFunctionLike(value) && !import_ts_morph20.Node.isIdentifier(value) && !import_ts_morph20.Node.isPropertyAccessExpression(value)) { return; } const builder = this.builder(receiverOf(call)); @@ -263004,12 +263237,12 @@ var TrpcResolver = class { builder, path: localName(call), file, - line: import_ts_morph19.Node.isPropertyAccessExpression(callee) ? callee.getNameNode().getStartLineNumber() : call.getStartLineNumber() + line: import_ts_morph20.Node.isPropertyAccessExpression(callee) ? callee.getNameNode().getStartLineNumber() : call.getStartLineNumber() }); } evaluate(value, depth) { - if (import_ts_morph19.Node.isCallExpression(value)) return this.chained(value, depth); - if (import_ts_morph19.Node.isPropertyAccessExpression(value) && value.getName() === "procedure") { + if (import_ts_morph20.Node.isCallExpression(value)) return this.chained(value, depth); + if (import_ts_morph20.Node.isPropertyAccessExpression(value) && value.getName() === "procedure") { const instance = this.instance(value.getExpression(), depth + 1); return { middleware: [], @@ -263017,11 +263250,11 @@ var TrpcResolver = class { confidence: instance ? "high" : "low" }; } - if (!import_ts_morph19.Node.isIdentifier(value) && !import_ts_morph19.Node.isPropertyAccessExpression(value)) { + if (!import_ts_morph20.Node.isIdentifier(value) && !import_ts_morph20.Node.isPropertyAccessExpression(value)) { return null; } const resolved = this.resolve(value); - if (import_ts_morph19.Node.isVariableDeclaration(resolved)) { + if (import_ts_morph20.Node.isVariableDeclaration(resolved)) { const initializer = resolved.getInitializer(); const built = initializer && this.builder(initializer, depth + 1); if (built) return built; @@ -263060,9 +263293,9 @@ var TrpcResolver = class { } /** A builder known only by its name, such as an imported `protectedProcedure`. */ namedBuilder(value, resolved) { - const name = import_ts_morph19.Node.isPropertyAccessExpression(value) ? value.getName() : value.getText(); + const name = import_ts_morph20.Node.isPropertyAccessExpression(value) ? value.getName() : value.getText(); if (!BUILDER_NAMES.test(name)) return null; - const typed = import_ts_morph19.Node.isParameterDeclaration(resolved) ? resolved.getTypeNode()?.getText() ?? "" : ""; + const typed = import_ts_morph20.Node.isParameterDeclaration(resolved) ? resolved.getTypeNode()?.getText() ?? "" : ""; const named = PROTECTED_BUILDER_NAMES.test(name) || PROTECTED_BUILDER_NAMES.test(typed); return { middleware: named ? [{ auth: true, opaque: true }] : [], @@ -263087,13 +263320,13 @@ var TrpcResolver = class { if (isFunctionLike(value)) { return { auth: authenticates(value), opaque: false }; } - if (import_ts_morph19.Node.isCallExpression(value)) return this.judgeCall(value, depth); - if (!import_ts_morph19.Node.isIdentifier(value) && !import_ts_morph19.Node.isPropertyAccessExpression(value)) { + if (import_ts_morph20.Node.isCallExpression(value)) return this.judgeCall(value, depth); + if (!import_ts_morph20.Node.isIdentifier(value) && !import_ts_morph20.Node.isPropertyAccessExpression(value)) { return OPAQUE; } - const name = import_ts_morph19.Node.isPropertyAccessExpression(value) ? value.getName() : value.getText(); + const name = import_ts_morph20.Node.isPropertyAccessExpression(value) ? value.getName() : value.getText(); const resolved = this.resolve(value); - const target = import_ts_morph19.Node.isVariableDeclaration(resolved) ? resolved.getInitializer() : isFunctionLike(resolved) ? resolved : void 0; + const target = import_ts_morph20.Node.isVariableDeclaration(resolved) ? resolved.getInitializer() : isFunctionLike(resolved) ? resolved : void 0; const inner = target ? this.middleware(target, depth + 1) : OPAQUE; return { auth: inner.auth || AUTH_NAMES4.test(name), opaque: inner.opaque }; } @@ -263106,13 +263339,13 @@ var TrpcResolver = class { const callee = call.getExpression(); const name = methodName(call) || callee.getText(); const named = AUTH_NAMES4.test(name); - const resolved = import_ts_morph19.Node.isIdentifier(callee) ? this.resolve(callee) : void 0; - const initializer = import_ts_morph19.Node.isVariableDeclaration(resolved) ? resolved.getInitializer() : resolved; + const resolved = import_ts_morph20.Node.isIdentifier(callee) ? this.resolve(callee) : void 0; + const initializer = import_ts_morph20.Node.isVariableDeclaration(resolved) ? resolved.getInitializer() : resolved; const factory = initializer && unwrap(initializer); const parts = isFunctionLike(factory) ? returnedValues2(factory) : [ ...PIPE.has(name) ? [receiverOf(call)] : [], ...call.getArguments().filter( - (argument) => isFunctionLike(unwrap(argument)) || import_ts_morph19.Node.isIdentifier(unwrap(argument)) || import_ts_morph19.Node.isPropertyAccessExpression(unwrap(argument)) || import_ts_morph19.Node.isCallExpression(unwrap(argument)) + (argument) => isFunctionLike(unwrap(argument)) || import_ts_morph20.Node.isIdentifier(unwrap(argument)) || import_ts_morph20.Node.isPropertyAccessExpression(unwrap(argument)) || import_ts_morph20.Node.isCallExpression(unwrap(argument)) ) ]; const verdicts = parts.map((part) => this.middleware(part, depth + 1)); @@ -263127,16 +263360,16 @@ var TrpcResolver = class { if (isFunctionLike(value)) { return returnsArgument(value) ? { schema: null, confidence: "high" } : null; } - if (import_ts_morph19.Node.isIdentifier(value)) { + if (import_ts_morph20.Node.isIdentifier(value)) { const resolved = this.resolve(value); - const initializer = import_ts_morph19.Node.isVariableDeclaration(resolved) ? resolved.getInitializer() : void 0; + const initializer = import_ts_morph20.Node.isVariableDeclaration(resolved) ? resolved.getInitializer() : void 0; return initializer ? this.permissive(initializer, depth + 1) : null; } - if (!import_ts_morph19.Node.isCallExpression(value)) return null; + if (!import_ts_morph20.Node.isCallExpression(value)) return null; const name = methodName(value); if (SCHEMA_WRAPPERS.has(name)) { const receiver = unwrap(receiverOf(value)); - const wrapped = import_ts_morph19.Node.isCallExpression(receiver) ? receiver : value.getArguments()[0]; + const wrapped = import_ts_morph20.Node.isCallExpression(receiver) ? receiver : value.getArguments()[0]; return wrapped ? this.permissive(wrapped, depth + 1) : null; } if (!ANY_SCHEMAS.has(name) || value.getArguments().length > 0) return null; @@ -263149,9 +263382,9 @@ var TrpcResolver = class { /** The function a property or variable holds: `errorFormatter(opts) {}`. */ functionOf(node) { if (!node) return void 0; - if (import_ts_morph19.Node.isMethodDeclaration(node)) return node; + if (import_ts_morph20.Node.isMethodDeclaration(node)) return node; const value = this.resolve(node); - const target = import_ts_morph19.Node.isVariableDeclaration(value) ? value.getInitializer() : value; + const target = import_ts_morph20.Node.isVariableDeclaration(value) ? value.getInitializer() : value; const fn = target && unwrap(target); return isFunctionLike(fn) ? fn : void 0; } @@ -263174,16 +263407,16 @@ var TrpcResolver = class { } resolveFactory(value, depth) { const asKind = (name) => name === "router" ? "router" : name === "mergeRouters" ? "merge" : null; - if (import_ts_morph19.Node.isPropertyAccessExpression(value)) { + if (import_ts_morph20.Node.isPropertyAccessExpression(value)) { const kind2 = asKind(value.getName()); return kind2 && this.instance(value.getExpression(), depth + 1) ? kind2 : null; } - if (!import_ts_morph19.Node.isIdentifier(value) || !/router/i.test(value.getText())) { + if (!import_ts_morph20.Node.isIdentifier(value) || !/router/i.test(value.getText())) { return null; } if (trpcExport(value) === "mergeRouters") return "merge"; const resolved = this.resolve(value); - if (import_ts_morph19.Node.isVariableDeclaration(resolved)) { + if (import_ts_morph20.Node.isVariableDeclaration(resolved)) { const initializer = resolved.getInitializer(); return initializer ? this.factoryKind(initializer, depth + 1) : null; } @@ -263203,11 +263436,11 @@ var TrpcResolver = class { } recordMembers(record, depth) { return record.getProperties().flatMap((property) => { - if (import_ts_morph19.Node.isSpreadAssignment(property)) { + if (import_ts_morph20.Node.isSpreadAssignment(property)) { const spread = objectLiteral(this.index, property.getExpression()); return spread && depth < MAX_RESOLUTION_DEPTH3 ? this.recordMembers(spread, depth + 1) : []; } - if (!import_ts_morph19.Node.isPropertyAssignment(property) && !import_ts_morph19.Node.isShorthandPropertyAssignment(property)) { + if (!import_ts_morph20.Node.isPropertyAssignment(property) && !import_ts_morph20.Node.isShorthandPropertyAssignment(property)) { return []; } const node = propertyValue(property); @@ -263219,16 +263452,16 @@ var TrpcResolver = class { /** What a router entry holds: a procedure, a router, or a nested record. */ routerValue(node, depth) { const value = unwrap(node); - if (import_ts_morph19.Node.isCallExpression(value)) { + if (import_ts_morph20.Node.isCallExpression(value)) { if (this.procedures.has(value)) return { kind: "procedure", call: value }; return this.routerKind(value) ? { kind: "router", call: value } : null; } - if (import_ts_morph19.Node.isObjectLiteralExpression(value)) { + if (import_ts_morph20.Node.isObjectLiteralExpression(value)) { return { kind: "record", record: value }; } if (depth > MAX_RESOLUTION_DEPTH3) return null; const resolved = this.resolve(value); - const initializer = import_ts_morph19.Node.isVariableDeclaration(resolved) ? resolved.getInitializer() : void 0; + const initializer = import_ts_morph20.Node.isVariableDeclaration(resolved) ? resolved.getInitializer() : void 0; return initializer ? this.routerValue(initializer, depth + 1) : null; } /** @@ -263349,7 +263582,7 @@ function inputIssues(resolver, index, procedures) { ...fromRule("unvalidated-trpc-input"), message: `Input of ${procedure.kind} ${procedure.path} is not validated: ${parser.schema === null ? "the parser returns its argument without checking it" : `${parser.schema} accepts any value`}`, file: index.paths.get(call.getSourceFile()) ?? procedure.file, - line: import_ts_morph20.Node.isPropertyAccessExpression(callee) ? callee.getNameNode().getStartLineNumber() : call.getStartLineNumber(), + line: import_ts_morph21.Node.isPropertyAccessExpression(callee) ? callee.getNameNode().getStartLineNumber() : call.getStartLineNumber(), fix: "Pass a schema that describes the input, such as z.object({ ... }), or a parser function that throws on invalid values", confidence: parser.confidence }); @@ -263380,7 +263613,8 @@ function analyzeProcedures(context) { const project = resolver.collect(sources); const { procedures } = project; const mutations = procedures.filter(({ kind }) => kind === "mutation"); - const auth = authIssues(mutations); + const perimeter = mutations.length > 0 ? perimeterAuth(index, sources) : null; + const auth = perimeter ? [] : authIssues(mutations); const inputs = inputIssues(resolver, index, procedures); const stacks = project.instances.flatMap((create) => { const file = index.paths.get(create.getSourceFile()) ?? ""; @@ -263400,7 +263634,7 @@ function analyzeProcedures(context) { ({ path: path2 }) => path2.split(".").some((segment) => HEALTH_SEGMENT.test(segment)) ), points: authRate * AUTH_POINTS + validationRate * VALIDATION_POINTS + (stacks.length === 0 ? ERROR_POINTS : 0), - summary: `${plural2(procedures.length, "tRPC procedure")}${project.routers > 0 ? ` in ${plural2(project.routers, "router")}` : ""}` + summary: `${plural2(procedures.length, "tRPC procedure")}${project.routers > 0 ? ` in ${plural2(project.routers, "router")}` : ""}${perimeter ? `; ${perimeter}` : ""}` }; } function foundNothing(issue) { @@ -263645,7 +263879,7 @@ function parseDirectives(file, content) { }); return directives; } -function matches(directive, issue) { +function matches2(directive, issue) { if (issue.file !== directive.file) return false; if (!directive.rules.includes(issue.rule)) return false; return directive.scope === "file" || issue.line === directive.target; @@ -263690,7 +263924,7 @@ function applySuppressions(results, context) { const suppressed = results.map((result) => ({ ...result, issues: result.issues.map((issue) => { - const directive = valid.find((candidate) => matches(candidate, issue)); + const directive = valid.find((candidate) => matches2(candidate, issue)); if (!directive) return issue; directive.used = true; return { diff --git a/src/analyzers/trpc-api.ts b/src/analyzers/trpc-api.ts index 83ee952..7e4abf5 100644 --- a/src/analyzers/trpc-api.ts +++ b/src/analyzers/trpc-api.ts @@ -3,6 +3,7 @@ import type { ScanContext } from "../core/scan-context.js"; import { fromRule, getRule } from "../rules/registry.js"; import type { AnalyzerResult, DiagnosticIssue } from "../types.js"; import { type ModuleIndex, pathAliases } from "./fastify-plugins.js"; +import { perimeterAuth } from "./trpc-perimeter.js"; import { type Procedure, type StackExposure, @@ -170,7 +171,9 @@ function analyzeProcedures(context: ScanContext): TrpcAnalysis { const { procedures } = project; const mutations = procedures.filter(({ kind }) => kind === "mutation"); - const auth = authIssues(mutations); + // Auth in front of every procedure makes their own middleware optional. + const perimeter = mutations.length > 0 ? perimeterAuth(index, sources) : null; + const auth = perimeter ? [] : authIssues(mutations); const inputs = inputIssues(resolver, index, procedures); const stacks = project.instances.flatMap((create) => { const file = index.paths.get(create.getSourceFile()) ?? ""; @@ -202,7 +205,7 @@ function analyzeProcedures(context: ScanContext): TrpcAnalysis { (stacks.length === 0 ? ERROR_POINTS : 0), summary: `${plural(procedures.length, "tRPC procedure")}${ project.routers > 0 ? ` in ${plural(project.routers, "router")}` : "" - }`, + }${perimeter ? `; ${perimeter}` : ""}`, }; } diff --git a/src/analyzers/trpc-perimeter.ts b/src/analyzers/trpc-perimeter.ts new file mode 100644 index 0000000..2965cd4 --- /dev/null +++ b/src/analyzers/trpc-perimeter.ts @@ -0,0 +1,411 @@ +import { + type CallExpression, + Node, + type SourceFile, + SyntaxKind, +} from "ts-morph"; +import { + definition, + isFunctionLike, + literalText, + methodName, + type ModuleIndex, + moduleOf, + objectLiteral, + propertyValue, + receiverOf, + unwrap, +} from "./fastify-plugins.js"; +import { AUTH_NAMES, throwsAuthError } from "./trpc-syntax.js"; + +/** + * Authentication in front of every tRPC procedure rather than in their + * middleware: a createContext that rejects anonymous callers, auth + * middleware ahead of the Express mount that serves the router, or a + * Next.js middleware (`proxy` in Next.js 16) that rejects unauthenticated + * requests to the tRPC endpoint. Each is reported by where it was found. + */ + +type Sources = ReadonlyArray<{ file: string; sourceFile: SourceFile }>; + +const ADAPTER_MODULE = /^@trpc\/server\/adapters\/|^@hono\/trpc-server$/; +/** A value naming the caller: `session`, `token`, `req.auth`, `user`. */ +const IDENTITY = /(?:session|token|auth|user|userId|identity|viewer|jwt)\b/i; +/** A response that turns an anonymous request away. */ +const REJECTION = + /\bredirect\s*\(|\brewrite\s*\(|status\s*:\s*40[13]\b|\b40[13]\b/; +const NEXT_AUTH_MIDDLEWARE = "next-auth/middleware"; +const MIDDLEWARE_FILES = ["middleware", "src/middleware", "proxy", "src/proxy"]; +const MIDDLEWARE_EXTENSIONS = [".ts", ".js", ".mjs"]; +/** Stands in for a procedure path in a dynamic route segment; it has a dot, as real ones do. */ +const SAMPLE_PROCEDURE = "post.create"; +/** Bounds the functions followed from createContext into the project. */ +const MAX_CONTEXT_DEPTH = 2; + +/** The function a node holds, through variables and imports. */ +function functionValue( + index: ModuleIndex, + node: Node | undefined, + depth = 0, +): Node | undefined { + if (!node || depth > MAX_CONTEXT_DEPTH + 2) return undefined; + const value = definition(index, node); + if (isFunctionLike(value)) return value; + if (Node.isVariableDeclaration(value)) { + return functionValue(index, value.getInitializer(), depth + 1); + } + return undefined; +} + +/** `if (!session) throw ...` or `if (!token) return redirect(...)`. */ +function guardsAnonymous( + fn: Node, + rejects: (branch: Node) => boolean, +): boolean { + return fn.getDescendantsOfKind(SyntaxKind.IfStatement).some((statement) => { + const condition = unwrap(statement.getExpression()); + return ( + Node.isPrefixUnaryExpression(condition) && + condition.getOperatorToken() === SyntaxKind.ExclamationToken && + IDENTITY.test(condition.getOperand().getText()) && + rejects(statement.getThenStatement()) + ); + }); +} + +function throws(branch: Node): boolean { + return ( + Node.isThrowStatement(branch) || + branch.getDescendantsOfKind(SyntaxKind.ThrowStatement).length > 0 + ); +} + +/** The createContext function, or a project function it calls, that rejects anonymous callers. */ +function rejectingFunction( + index: ModuleIndex, + fn: Node, + depth = 0, +): Node | undefined { + if (throwsAuthError(fn) || guardsAnonymous(fn, throws)) return fn; + if (depth >= MAX_CONTEXT_DEPTH) return undefined; + for (const call of fn.getDescendantsOfKind(SyntaxKind.CallExpression)) { + const callee = functionValue(index, call.getExpression()); + const found = callee && rejectingFunction(index, callee, depth + 1); + if (found) return found; + } + return undefined; +} + +/** Calls to tRPC adapters and the options object each receives. */ +function adapterCalls( + index: ModuleIndex, + sources: Sources, +): Array<{ call: CallExpression; file: string; options: Node | undefined }> { + const calls: Array<{ + call: CallExpression; + file: string; + options: Node | undefined; + }> = []; + for (const { file, sourceFile } of sources) { + if ( + !/@trpc\/server\/adapters\/|@hono\/trpc-server/.test( + sourceFile.getFullText(), + ) + ) { + continue; + } + for (const call of sourceFile.getDescendantsOfKind( + SyntaxKind.CallExpression, + )) { + const [first, second] = call.getArguments(); + if (ADAPTER_MODULE.test(moduleOf(call.getExpression()) ?? "")) { + calls.push({ call, file, options: first }); + } else if ( + // fastify.register(fastifyTRPCPlugin, { trpcOptions: { createContext } }) + methodName(call) === "register" && + first && + ADAPTER_MODULE.test(moduleOf(first) ?? "") + ) { + const options = objectLiteral(index, second); + calls.push({ + call, + file, + options: propertyValue(options?.getProperty("trpcOptions")), + }); + } + } + } + return calls; +} + +function rejectingContext( + index: ModuleIndex, + adapters: ReturnType, +): string | null { + const contexts = adapters.flatMap(({ options }) => { + const value = propertyValue( + objectLiteral(index, options)?.getProperty("createContext"), + ); + const fn = functionValue(index, value); + return fn ? [fn] : []; + }); + // Every adapter must reject: one open handler serves the whole router. + const rejecting = contexts.map((fn) => rejectingFunction(index, fn)); + const [first] = rejecting; + if (!first || rejecting.some((fn) => fn === undefined)) return null; + // Name the function that rejects, not an inline arrow that calls it. + return `createContext in ${index.paths.get(first.getSourceFile())} rejects anonymous callers`; +} + +/** The middleware text a mount argument names: `requireAuth`, `passport.authenticate`. */ +function middlewareName(node: Node): string { + const value = unwrap(node); + return Node.isCallExpression(value) + ? value.getExpression().getText() + : value.getText(); +} + +function isAuthMiddleware(node: Node): boolean { + return AUTH_NAMES.test(middlewareName(node)); +} + +/** The `.use()` calls that mount an adapter, directly or through a variable. */ +function mounts(call: CallExpression): CallExpression[] { + const parent = call.getParent(); + if (Node.isCallExpression(parent) && methodName(parent) === "use") { + return [parent]; + } + if (!Node.isVariableDeclaration(parent)) return []; + const name = parent.getName(); + return call + .getSourceFile() + .getDescendantsOfKind(SyntaxKind.CallExpression) + .filter( + (use) => + methodName(use) === "use" && + use.getArguments().some((argument) => argument.getText() === name), + ); +} + +/** Auth middleware the request passes before reaching an Express mount. */ +function mountAuth(use: CallExpression): string | null { + const args = use.getArguments(); + const mountPath = literalText(args[0]); + const handlers = mountPath === null ? args : args.slice(1); + const own = handlers.slice(0, -1).find(isAuthMiddleware); + if (own) return middlewareName(own); + + const receiver = receiverOf(use).getText(); + for (const earlier of use + .getSourceFile() + .getDescendantsOfKind(SyntaxKind.CallExpression)) { + if (earlier.getStart() >= use.getStart()) break; + if (methodName(earlier) !== "use") continue; + if (receiverOf(earlier).getText() !== receiver) continue; + const [first, ...rest] = earlier.getArguments(); + const path = literalText(first); + // A global use(), or one whose path contains the tRPC mount. + const applies = + path === null || + (mountPath !== null && + (path === "/" || + `${mountPath}/`.startsWith(`${path.replace(/\/$/, "")}/`))); + if (!applies) continue; + const middleware = (path === null ? earlier.getArguments() : rest).find( + isAuthMiddleware, + ); + if (middleware) return middlewareName(middleware); + } + return null; +} + +function expressMountAuth( + adapters: ReturnType, +): string | null { + const mounted = adapters.flatMap(({ call, file }) => + mounts(call).map((use) => ({ use, file })), + ); + if (mounted.length === 0) return null; + const guarded = mounted.map(({ use, file }) => ({ + file, + middleware: mountAuth(use), + })); + if (!guarded.every(({ middleware }) => middleware !== null)) return null; + const [first] = guarded; + return first + ? `${first.middleware} runs before the tRPC mount in ${first.file}` + : null; +} + +/** The URL a Next.js route file serves, with a sample procedure for dynamic segments. */ +function routePath(file: string): string | null { + const match = /^(?:src\/)?(app|pages)\/(.+)$/.exec(file); + if (!match) return null; + const [, router, rest = ""] = match; + const segments = rest.split("/"); + const last = segments.pop() ?? ""; + if (router === "app") { + if (!/^route\.[cm]?[jt]sx?$/.test(last)) return null; + } else { + segments.push(last.replace(/\.[cm]?[jt]sx?$/, "")); + } + const path = segments + .filter((segment) => !/^\(.*\)$/.test(segment) && segment !== "index") + .map((segment) => (/^\[.*\]$/.test(segment) ? SAMPLE_PROCEDURE : segment)); + return `/${path.join("/")}`; +} + +/** + * Whether a Next.js matcher (path-to-regexp syntax with regex groups) + * matches a path. Only the fixed sample path is tested, so a pattern from the + * project cannot make matching slow. + */ +function matches(pattern: string, path: string): boolean { + const source = pattern + .replace(/\/:\w+\*/g, "(?:/.*)?") + .replace(/\/:\w+\+/g, "/.+") + .replace(/\/:\w+\?/g, "(?:/[^/]+)?") + .replace(/:\w+/g, "[^/]+"); + try { + return new RegExp(`^${source}$`).test(path); + } catch { + return false; + } +} + +/** String patterns in a literal, an array, or `{ source }` objects; null when not literal. */ +function literalPatterns( + index: ModuleIndex, + node: Node | undefined, +): string[] | null { + if (!node) return null; + const value = unwrap(node); + const single = literalText(value); + if (single !== null) return [single]; + if (!Node.isArrayLiteralExpression(value)) return null; + const patterns: string[] = []; + for (const element of value.getElements()) { + const text = + literalText(element) ?? + literalText( + propertyValue(objectLiteral(index, element)?.getProperty("source")), + ); + if (text === null) return null; + patterns.push(text); + } + return patterns; +} + +/** Whether the middleware runs for a path: no matcher means every path. */ +function runsFor( + index: ModuleIndex, + sourceFile: SourceFile, + path: string, +): boolean { + const config = sourceFile.getVariableDeclaration("config"); + if (!config?.isExported()) return true; + const matcher = objectLiteral(index, config.getInitializer())?.getProperty( + "matcher", + ); + if (!matcher) return true; + const patterns = literalPatterns(index, propertyValue(matcher)); + return patterns?.some((pattern) => matches(pattern, path)) ?? false; +} + +/** + * Clerk's `auth.protect()`: unconditional, or under `isProtected(req)` / + * `!isPublic(req)` whose createRouteMatcher patterns decide the path. + */ +function protectsPath( + index: ModuleIndex, + sourceFile: SourceFile, + path: string, +): boolean { + return sourceFile + .getDescendantsOfKind(SyntaxKind.CallExpression) + .filter((call) => methodName(call) === "protect") + .some((call) => { + const guard = call.getFirstAncestorByKind(SyntaxKind.IfStatement); + if (!guard) return true; + let condition = unwrap(guard.getExpression()); + let negated = false; + if ( + Node.isPrefixUnaryExpression(condition) && + condition.getOperatorToken() === SyntaxKind.ExclamationToken + ) { + negated = true; + condition = unwrap(condition.getOperand()); + } + if (!Node.isCallExpression(condition)) return false; + const matcher = definition(index, condition.getExpression()); + const created = Node.isVariableDeclaration(matcher) + ? matcher.getInitializer() + : undefined; + if (!Node.isCallExpression(created)) return false; + const patterns = literalPatterns(index, created.getArguments()[0]); + if (patterns === null) return false; + const covered = patterns.some((pattern) => matches(pattern, path)); + return negated ? !covered : covered; + }); +} + +/** True when a middleware turns anonymous requests to the path away. */ +function middlewareRejects( + index: ModuleIndex, + sourceFile: SourceFile, + path: string, +): boolean { + // next-auth v4's middleware redirects every request without a token. + const nextAuth = [ + ...sourceFile.getExportDeclarations(), + ...sourceFile.getImportDeclarations(), + ].some( + (declaration) => + declaration.getModuleSpecifierValue() === NEXT_AUTH_MIDDLEWARE, + ); + return ( + nextAuth || + protectsPath(index, sourceFile, path) || + guardsAnonymous(sourceFile, (branch) => REJECTION.test(branch.getText())) + ); +} + +function nextMiddlewareAuth( + index: ModuleIndex, + adapters: ReturnType, +): string | null { + const paths = [ + ...new Set(adapters.flatMap(({ file }) => routePath(file) ?? [])), + ]; + if (paths.length === 0) return null; + const file = MIDDLEWARE_FILES.flatMap((name) => + MIDDLEWARE_EXTENSIONS.map((extension) => `${name}${extension}`), + ).find((candidate) => index.files.has(candidate)); + const sourceFile = file ? index.files.get(file) : undefined; + if (!file || !sourceFile) return null; + const covered = paths.every( + (path) => + runsFor(index, sourceFile, path) && + middlewareRejects(index, sourceFile, path), + ); + if (!covered) return null; + const endpoint = (paths[0] ?? "").replace(`/${SAMPLE_PROCEDURE}`, ""); + return `${file} rejects unauthenticated requests to ${endpoint}`; +} + +/** + * Where every tRPC procedure is authenticated before it runs, or null. + * Procedures behind such a perimeter need no auth middleware of their own. + */ +export function perimeterAuth( + index: ModuleIndex, + sources: Sources, +): string | null { + const adapters = adapterCalls(index, sources); + if (adapters.length === 0) return null; + return ( + rejectingContext(index, adapters) ?? + expressMountAuth(adapters) ?? + nextMiddlewareAuth(index, adapters) + ); +} diff --git a/src/analyzers/trpc-syntax.ts b/src/analyzers/trpc-syntax.ts index e31e632..41cd39f 100644 --- a/src/analyzers/trpc-syntax.ts +++ b/src/analyzers/trpc-syntax.ts @@ -72,7 +72,8 @@ export function bindingSource(node: Node): Node | undefined { ?.getInitializer(); } -function throwsAuthError(fn: Node): boolean { +/** `throw new TRPCError({ code: "UNAUTHORIZED" })` or FORBIDDEN anywhere in a node. */ +export function throwsAuthError(fn: Node): boolean { return fn.getDescendantsOfKind(SyntaxKind.NewExpression).some((created) => { if (!/(?:^|\.)TRPCError$/.test(created.getExpression().getText())) { return false; diff --git a/tests/trpc-api.test.ts b/tests/trpc-api.test.ts index f4da97b..cf1ce92 100644 --- a/tests/trpc-api.test.ts +++ b/tests/trpc-api.test.ts @@ -614,3 +614,239 @@ test("tRPC procedures in test files are ignored", async () => { assert.equal(result, null); }); + +/** A router with one mutation on the public builder, served by an adapter. */ +const PERIMETER_ROUTER = { + "src/server/trpc.ts": TRPC_SETUP, + "src/server/router.ts": [ + 'import { z } from "zod";', + 'import { publicProcedure, router } from "./trpc";', + "export const appRouter = router({", + " createPost: publicProcedure", + " .input(z.object({ title: z.string() }))", + " .mutation(({ input }) => input),", + "});", + ].join("\n"), +}; +const UNPROTECTED = ["Mutation createPost has no recognizable auth middleware"]; + +test("a createContext that rejects anonymous callers protects every procedure", async () => { + const rejecting = [ + 'import { TRPCError } from "@trpc/server";', + 'import { getSession } from "./session";', + "export async function createContext({ req }) {", + " const session = await getSession(req);", + ' if (!session) throw new TRPCError({ code: "UNAUTHORIZED" });', + " return { user: session.user };", + "}", + ].join("\n"); + const standalone = await analyze({ + ...PERIMETER_ROUTER, + "src/server/context.ts": rejecting, + "src/server/index.ts": [ + 'import { createHTTPServer } from "@trpc/server/adapters/standalone";', + 'import { createContext } from "./context";', + 'import { appRouter } from "./router";', + "createHTTPServer({ router: appRouter, createContext }).listen(3000);", + ].join("\n"), + }); + assert.deepEqual(authMessages(standalone), []); + assert.match( + standalone?.summary ?? "", + /; createContext in src\/server\/context\.ts rejects anonymous callers$/, + ); + + // Followed through an inline arrow that calls the project's function. + const fetchAdapter = await analyze({ + ...PERIMETER_ROUTER, + "src/server/context.ts": rejecting, + "src/app/api/trpc/[trpc]/route.ts": [ + 'import { fetchRequestHandler } from "@trpc/server/adapters/fetch";', + 'import { createContext } from "../../../../server/context";', + 'import { appRouter } from "../../../../server/router";', + "const handler = (req: Request) =>", + " fetchRequestHandler({", + ' endpoint: "/api/trpc",', + " req,", + " router: appRouter,", + " createContext: () => createContext({ req }),", + " });", + "export { handler as GET, handler as POST };", + ].join("\n"), + }); + assert.deepEqual(authMessages(fetchAdapter), []); + + // Reading the session is not rejecting the caller (T3's createTRPCContext). + const reading = await analyze({ + ...PERIMETER_ROUTER, + "src/server/context.ts": [ + 'import { auth } from "./auth";', + "export async function createContext() {", + " const session = await auth();", + " return { session, user: session?.user ?? null };", + "}", + ].join("\n"), + "src/server/index.ts": [ + 'import { createHTTPServer } from "@trpc/server/adapters/standalone";', + 'import { createContext } from "./context";', + 'import { appRouter } from "./router";', + "createHTTPServer({ router: appRouter, createContext }).listen(3000);", + ].join("\n"), + }); + assert.deepEqual(authMessages(reading), UNPROTECTED); +}); + +test("auth middleware on the Express mount that serves tRPC protects its procedures", async () => { + const app = (...lines: string[]) => ({ + ...PERIMETER_ROUTER, + "src/app.ts": [ + 'import express from "express";', + 'import * as trpcExpress from "@trpc/server/adapters/express";', + 'import { requireAuth } from "./auth";', + 'import { appRouter } from "./server/router";', + "const app = express();", + "const trpc = trpcExpress.createExpressMiddleware({ router: appRouter });", + ...lines, + ].join("\n"), + }); + + const mounted = await analyze(app('app.use("/trpc", requireAuth, trpc);')); + assert.deepEqual(authMessages(mounted), []); + assert.match( + mounted?.summary ?? "", + /; requireAuth runs before the tRPC mount in src\/app\.ts$/, + ); + assert.deepEqual( + authMessages( + await analyze(app("app.use(requireAuth);", 'app.use("/trpc", trpc);')), + ), + [], + ); + + for (const lines of [ + ['app.use("/admin", requireAuth);', 'app.use("/trpc", trpc);'], + ['app.use("/trpc", trpc);', "app.use(requireAuth);"], + ['app.use("/trpc", trpc);'], + ]) { + assert.deepEqual(authMessages(await analyze(app(...lines))), UNPROTECTED); + } +}); + +test("a Next.js middleware that rejects anonymous requests to the tRPC endpoint protects it", async () => { + const project = (middleware: Record) => ({ + ...PERIMETER_ROUTER, + "src/app/api/trpc/[trpc]/route.ts": [ + 'import { fetchRequestHandler } from "@trpc/server/adapters/fetch";', + 'import { appRouter } from "../../../../server/router";', + "const handler = (req: Request) =>", + ' fetchRequestHandler({ endpoint: "/api/trpc", req, router: appRouter, createContext: () => ({ user: null }) });', + "export { handler as GET, handler as POST };", + ].join("\n"), + ...middleware, + }); + const custom = [ + 'import { NextResponse, type NextRequest } from "next/server";', + "export function middleware(request: NextRequest) {", + ' const token = request.cookies.get("session")?.value;', + " if (!token) {", + ' return NextResponse.redirect(new URL("/login", request.url));', + " }", + " return NextResponse.next();", + "}", + ]; + const protecting: Array<[string, Record]> = [ + [ + "custom middleware without a matcher", + { "src/middleware.ts": custom.join("\n") }, + ], + [ + "Next.js 16 proxy", + { + "proxy.ts": custom + .join("\n") + .replace("function middleware", "function proxy"), + }, + ], + [ + "next-auth/middleware with a tRPC matcher", + { + "middleware.ts": [ + 'export { default } from "next-auth/middleware";', + 'export const config = { matcher: ["/dashboard/:path*", "/api/trpc/:path*"] };', + ].join("\n"), + }, + ], + [ + "Clerk protecting a matched route", + { + "src/middleware.ts": [ + 'import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";', + 'const isProtectedRoute = createRouteMatcher(["/dashboard(.*)", "/api/trpc(.*)"]);', + "export default clerkMiddleware(async (auth, req) => {", + " if (isProtectedRoute(req)) await auth.protect();", + "});", + 'export const config = { matcher: ["/((?!_next|.*\\\\..*).*)", "/(api|trpc)(.*)"] };', + ].join("\n"), + }, + ], + ]; + for (const [name, files] of protecting) { + const result = await analyze(project(files)); + assert.deepEqual(authMessages(result), [], name); + assert.match(result?.summary ?? "", /middleware|proxy/, name); + } + + const leaving: Array<[string, Record]> = [ + ["no middleware", {}], + [ + "a matcher that excludes the API", + { + "middleware.ts": [ + 'export { default } from "next-auth/middleware";', + 'export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"] };', + ].join("\n"), + }, + ], + [ + "a matcher for other pages", + { + "middleware.ts": [ + 'export { default } from "next-auth/middleware";', + 'export const config = { matcher: ["/dashboard/:path*"] };', + ].join("\n"), + }, + ], + [ + "Clerk protecting other routes", + { + "middleware.ts": [ + 'import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";', + 'const isProtectedRoute = createRouteMatcher(["/dashboard(.*)"]);', + "export default clerkMiddleware(async (auth, req) => {", + " if (isProtectedRoute(req)) await auth.protect();", + "});", + ].join("\n"), + }, + ], + [ + "a middleware that only sets headers", + { + "middleware.ts": [ + 'import { NextResponse } from "next/server";', + "export function middleware() {", + " const response = NextResponse.next();", + ' response.headers.set("x-frame-options", "DENY");', + " return response;", + "}", + ].join("\n"), + }, + ], + ]; + for (const [name, files] of leaving) { + assert.deepEqual( + authMessages(await analyze(project(files))), + UNPROTECTED, + name, + ); + } +}); From c2a413b2a9a2112bd25fee9db02163b9ba89a4ca Mon Sep 17 00:00:00 2001 From: sabahattink Date: Fri, 30 Oct 2026 17:01:40 +0400 Subject: [PATCH 2/4] ci: rebuild the Action bundle on Dependabot pull requests Dependency updates that change dist/action.cjs failed bundle:check until someone rebuilt the bundle by hand. A read-only job now runs npm ci and the build on Dependabot pull requests; when the bundle changes, a second job that runs no repository or dependency code commits only the two bundle files and starts CI for the new commit through workflow_dispatch, since commits pushed with GITHUB_TOKEN do not trigger pull_request runs. Dependabot groups minor and patch npm updates into one pull request. --- .github/dependabot.yml | 4 ++ .github/workflows/ci.yml | 3 + .github/workflows/dependabot-bundle.yml | 75 +++++++++++++++++++++++++ CONTRIBUTING.md | 6 ++ 4 files changed, 88 insertions(+) create mode 100644 .github/workflows/dependabot-bundle.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e0f74d8..feb6913 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,6 +8,10 @@ updates: time: "06:00" timezone: Asia/Baku open-pull-requests-limit: 5 + # One pull request for routine updates; majors stay separate for review. + groups: + npm-minor-and-patch: + update-types: [minor, patch] ignore: # chalk 6 requires Node 22; CodeDiag supports Node 20.19 and newer. - dependency-name: chalk diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15cb4df..6e09a8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,9 @@ on: branches: [main] pull_request: branches: [main] + # Started by dependabot-bundle.yml after it commits a rebuilt bundle, since + # commits pushed with GITHUB_TOKEN do not trigger pull_request runs. + workflow_dispatch: jobs: build: diff --git a/.github/workflows/dependabot-bundle.yml b/.github/workflows/dependabot-bundle.yml new file mode 100644 index 0000000..3df1e74 --- /dev/null +++ b/.github/workflows/dependabot-bundle.yml @@ -0,0 +1,75 @@ +name: Dependabot bundle + +# A dependency update can change the committed Action bundle, which CI +# compares with a fresh build. This rebuilds it on Dependabot pull requests: +# the dependency's code runs only in a read-only job, and the job that +# pushes runs nothing from the repository or its dependencies. + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + build: + if: github.event.pull_request.user.login == 'dependabot[bot]' + runs-on: ubuntu-latest + outputs: + changed: ${{ steps.diff.outputs.changed }} + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + - uses: actions/setup-node@v7 + with: + node-version: 22 + - run: npm ci + - run: npm run build + - id: diff + run: | + if git diff --quiet -- dist/action.cjs dist/action.cjs.LEGAL.txt; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi + - if: steps.diff.outputs.changed == 'true' + uses: actions/upload-artifact@v7 + with: + name: action-bundle + path: | + dist/action.cjs + dist/action.cjs.LEGAL.txt + if-no-files-found: error + + commit: + needs: build + if: needs.build.outputs.changed == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + actions: write + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.ref }} + - uses: actions/download-artifact@v7 + with: + name: action-bundle + path: rebuilt + - name: Commit the rebuilt bundle + run: | + cp rebuilt/action.cjs rebuilt/action.cjs.LEGAL.txt dist/ + rm -rf rebuilt + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add dist/action.cjs dist/action.cjs.LEGAL.txt + git commit -m "chore(deps): rebuild the Action bundle" + git push + - name: Run CI for the new commit + env: + GH_TOKEN: ${{ github.token }} + BRANCH: ${{ github.event.pull_request.head.ref }} + run: gh workflow run ci.yml --repo "$GITHUB_REPOSITORY" --ref "$BRANCH" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6183f59..ce46d53 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,6 +76,12 @@ verification performed. Keep unrelated refactors in separate pull requests. 4. Move the Action's major tag (for example `v0`) to the release commit so `uses: sabahattink/codediag@v0` picks it up. +The GitHub Action runs the committed bundle in `dist/action.cjs`, and CI +fails when it differs from a fresh build. Dependabot pull requests that +change the bundle get it rebuilt by the Dependabot bundle workflow, which +builds without write access, commits only the two bundle files, and starts CI +for the new commit. + ## Reporting security issues Do not open a public issue for a suspected vulnerability. Follow the private From 206cc2fa5e86ce76f088d0b691f0edb3b2863f4d Mon Sep 17 00:00:00 2001 From: sabahattink Date: Fri, 30 Oct 2026 17:09:15 +0400 Subject: [PATCH 3/4] perf(dependencies): keep audits for an hour and run workspace audits concurrently Successful audits are written to node_modules/.cache/codediag, keyed by the audit command and the exact lock file and package.json contents, and reused for an hour by later scans and editor saves; entries are validated on read, failed audits are never kept, and nothing is written when the project has no node_modules. A reused result is named in the Dependencies summary with its age. --workspaces scans start every package's audit up front, four at a time, and the package scans reuse those outcomes, so the waiting overlaps instead of adding up. On a three-package npm workspace the scan takes 2.8 s, and 0.9 s once the audits are cached. --- dist/action.cjs | 1410 +++++++++++++++++++-------------- src/analyzers/audit-cache.ts | 144 ++++ src/analyzers/dependencies.ts | 209 ++++- src/core/workspace-scan.ts | 25 +- tests/dependencies.test.ts | 195 ++++- 5 files changed, 1355 insertions(+), 628 deletions(-) create mode 100644 src/analyzers/audit-cache.ts diff --git a/dist/action.cjs b/dist/action.cjs index 621c642..260669a 100644 --- a/dist/action.cjs +++ b/dist/action.cjs @@ -125164,7 +125164,7 @@ ${lanes.join("\n")} } } function createImportCallExpressionAMD(arg, containsLexicalThis) { - const resolve6 = factory2.createUniqueName("resolve"); + const resolve7 = factory2.createUniqueName("resolve"); const reject = factory2.createUniqueName("reject"); const parameters = [ factory2.createParameterDeclaration( @@ -125173,7 +125173,7 @@ ${lanes.join("\n")} /*dotDotDotToken*/ void 0, /*name*/ - resolve6 + resolve7 ), factory2.createParameterDeclaration( /*modifiers*/ @@ -125190,7 +125190,7 @@ ${lanes.join("\n")} factory2.createIdentifier("require"), /*typeArguments*/ void 0, - [factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]), resolve6, reject] + [factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]), resolve7, reject] ) ) ]); @@ -212101,8 +212101,8 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range installPackage(options) { this.packageInstallId++; const request2 = { kind: "installPackage", ...options, id: this.packageInstallId }; - const promise = new Promise((resolve6, reject) => { - (this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map())).set(this.packageInstallId, { resolve: resolve6, reject }); + const promise = new Promise((resolve7, reject) => { + (this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map())).set(this.packageInstallId, { resolve: resolve7, reject }); }); this.installer.send(request2); return promise; @@ -214430,7 +214430,7 @@ var require_path_browserify = __commonJS({ } var posix5 = { // path.resolve([from ...], to) - resolve: function resolve6() { + resolve: function resolve7() { var resolvedPath = ""; var resolvedAbsolute = false; var cwd; @@ -214477,7 +214477,7 @@ var require_path_browserify = __commonJS({ assertPath(path2); return path2.length > 0 && path2.charCodeAt(0) === 47; }, - join: function join11() { + join: function join12() { if (arguments.length === 0) return "."; var joined; @@ -214564,7 +214564,7 @@ var require_path_browserify = __commonJS({ _makeLong: function _makeLong(path2) { return path2; }, - dirname: function dirname6(path2) { + dirname: function dirname7(path2) { assertPath(path2); if (path2.length === 0) return "."; var code = path2.charCodeAt(0); @@ -218624,12 +218624,12 @@ ${nodeLocation}` : message; }; var NodeRuntimeFileSystem = class { delete(path3) { - return new Promise((resolve6, reject) => { + return new Promise((resolve7, reject) => { fs__namespace.rm(path3, { recursive: true }, (err) => { if (err) reject(err); else - resolve6(); + resolve7(); }); }); } @@ -218648,12 +218648,12 @@ ${nodeLocation}` : message; })); } readFile(filePath, encoding = "utf-8") { - return new Promise((resolve6, reject) => { + return new Promise((resolve7, reject) => { fs__namespace.readFile(filePath, encoding, (err, data) => { if (err) reject(err); else - resolve6(data); + resolve7(data); }); }); } @@ -218661,12 +218661,12 @@ ${nodeLocation}` : message; return fs__namespace.readFileSync(filePath, encoding); } async writeFile(filePath, fileText) { - await new Promise((resolve6, reject) => { + await new Promise((resolve7, reject) => { fs__namespace.writeFile(filePath, fileText, (err) => { if (err) reject(err); else - resolve6(); + resolve7(); }); }); } @@ -218680,12 +218680,12 @@ ${nodeLocation}` : message; fs__namespace.mkdirSync(dirPath, { recursive: true }); } move(srcPath, destPath) { - return new Promise((resolve6, reject) => { + return new Promise((resolve7, reject) => { fs__namespace.rename(srcPath, destPath, (err) => { if (err) reject(err); else - resolve6(); + resolve7(); }); }); } @@ -218693,12 +218693,12 @@ ${nodeLocation}` : message; fs__namespace.renameSync(srcPath, destPath); } copy(srcPath, destPath) { - return new Promise((resolve6, reject) => { + return new Promise((resolve7, reject) => { fs__namespace.copyFile(srcPath, destPath, (err) => { if (err) reject(err); else - resolve6(); + resolve7(); }); }); } @@ -218706,15 +218706,15 @@ ${nodeLocation}` : message; fs__namespace.copyFileSync(srcPath, destPath); } stat(path3) { - return new Promise((resolve6, reject) => { + return new Promise((resolve7, reject) => { fs__namespace.stat(path3, (err, stat) => { if (err) { if (err.code === "ENOENT" || err.code === "ENOTDIR") - resolve6(void 0); + resolve7(void 0); else reject(err); } else { - resolve6(stat); + resolve7(stat); } }); }); @@ -251588,8 +251588,8 @@ __export(action_exports, { runAction: () => runAction }); module.exports = __toCommonJS(action_exports); -var import_node_fs17 = require("fs"); -var import_node_path19 = require("path"); +var import_node_fs18 = require("fs"); +var import_node_path20 = require("path"); // src/analyzers/secret-history.ts var import_node_child_process2 = require("child_process"); @@ -255556,12 +255556,670 @@ function formatDelta(delta) { } // src/core/workspace-scan.ts -var import_node_fs14 = require("fs"); -var import_node_path15 = require("path"); +var import_node_fs15 = require("fs"); +var import_node_path16 = require("path"); + +// src/analyzers/dependencies.ts +var import_node_child_process3 = require("child_process"); +var import_node_crypto3 = require("crypto"); +var import_node_fs9 = require("fs"); +var import_node_path7 = require("path"); + +// src/analyzers/audit-advisories.ts +var MAX_LISTED = 5; +var SEVERITIES3 = /* @__PURE__ */ new Set(["critical", "high", "moderate", "low"]); +function isRecord4(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function text(value) { + return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0; +} +function advisory(name, severity, title, url) { + const packageName2 = text(name); + const level = typeof severity === "string" ? severity.toLowerCase() : ""; + if (!packageName2 || !SEVERITIES3.has(level)) return null; + const titleText = text(title); + const urlText = text(url); + return { + name: packageName2, + severity: level, + ...titleText ? { title: titleText } : {}, + ...urlText ? { url: urlText } : {} + }; +} +function npmAdvisories(document) { + if (!isRecord4(document.vulnerabilities)) return []; + return Object.entries(document.vulnerabilities).flatMap(([name, entry]) => { + if (!isRecord4(entry)) return []; + const source = Array.isArray(entry.via) ? entry.via.find(isRecord4) : void 0; + const found = advisory(name, entry.severity, source?.title, source?.url); + return found ? [found] : []; + }); +} +function advisoryMap(document) { + if (!isRecord4(document.advisories)) return []; + return Object.values(document.advisories).flatMap((entry) => { + if (!isRecord4(entry)) return []; + const found = advisory( + entry.module_name, + entry.severity, + entry.title, + entry.url + ); + return found ? [found] : []; + }); +} +function yarnAdvisory(line) { + if (line.type === "auditAdvisory" && isRecord4(line.data)) { + const data = isRecord4(line.data.advisory) ? line.data.advisory : void 0; + return data ? advisory(data.module_name, data.severity, data.title, data.url) : null; + } + if (isRecord4(line.children)) { + const { children } = line; + return advisory( + line.value, + children.Severity, + children.Issue, + children.URL + ); + } + return null; +} +function parseAuditAdvisories(output) { + const found = []; + const trimmed = output.trim(); + try { + const document = JSON.parse(trimmed); + if (isRecord4(document)) { + found.push(...npmAdvisories(document), ...advisoryMap(document)); + } + } catch { + for (const line of trimmed.split(/\r?\n/)) { + try { + const entry = JSON.parse(line); + const parsed = isRecord4(entry) ? yarnAdvisory(entry) : null; + if (parsed) found.push(parsed); + } catch { + } + } + } + const order = ["critical", "high", "moderate", "low"]; + const unique = /* @__PURE__ */ new Map(); + for (const entry of found) { + const key = `${entry.severity}\0${entry.name}`; + if (!unique.has(key)) unique.set(key, entry); + } + return [...unique.values()].sort( + (left, right) => order.indexOf(left.severity) - order.indexOf(right.severity) || left.name.localeCompare(right.name) + ); +} +function describeAdvisories(advisories, severity) { + const matching = advisories.filter((entry) => entry.severity === severity); + if (matching.length === 0) return ""; + const listed = matching.slice(0, MAX_LISTED).map((entry) => { + const details = [entry.title, entry.url].filter(Boolean).join(", "); + return details ? `${entry.name} (${details})` : entry.name; + }); + const more = matching.length - listed.length; + return ` Affected: ${listed.join("; ")}${more > 0 ? `; and ${more} more` : ""}.`; +} + +// src/analyzers/audit-cache.ts +var import_node_crypto2 = require("crypto"); +var import_node_fs8 = require("fs"); +var import_node_path6 = require("path"); +var AUDIT_CACHE_TTL_MS = 60 * 60 * 1e3; +var CACHE_VERSION = 1; +var SEVERITIES4 = /* @__PURE__ */ new Set(["critical", "high", "moderate", "low"]); +function isRecord5(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function isCount(value) { + return Number.isInteger(value) && value >= 0; +} +function auditCacheFile(lockDirectory, key) { + const modules = (0, import_node_path6.join)(lockDirectory, "node_modules"); + if (!(0, import_node_fs8.existsSync)(modules)) return null; + const name = (0, import_node_crypto2.createHash)("sha256").update(key).digest("hex"); + return (0, import_node_path6.join)(modules, ".cache", "codediag", "audit", `${name}.json`); +} +function readSummary(value) { + if (!isRecord5(value)) return null; + const { critical, high, moderate, low } = value; + return isCount(critical) && isCount(high) && isCount(moderate) && isCount(low) ? { critical, high, moderate, low } : null; +} +function readAdvisories(value) { + if (!Array.isArray(value)) return null; + const advisories = []; + for (const entry of value) { + if (!isRecord5(entry) || typeof entry.name !== "string" || !SEVERITIES4.has(String(entry.severity)) || entry.title !== void 0 && typeof entry.title !== "string" || entry.url !== void 0 && typeof entry.url !== "string") { + return null; + } + advisories.push({ + name: entry.name, + severity: entry.severity, + ...typeof entry.title === "string" ? { title: entry.title } : {}, + ...typeof entry.url === "string" ? { url: entry.url } : {} + }); + } + return advisories; +} +function readPersistedAudit(file, now = Date.now()) { + let value; + try { + value = JSON.parse((0, import_node_fs8.readFileSync)(file, "utf-8")); + } catch { + return null; + } + if (!isRecord5(value) || value.version !== CACHE_VERSION || typeof value.createdAt !== "string") { + return null; + } + const createdAt = Date.parse(value.createdAt); + if (!Number.isFinite(createdAt) || createdAt > now || now - createdAt >= AUDIT_CACHE_TTL_MS) { + return null; + } + const summary = readSummary(value.summary); + const advisories = readAdvisories(value.advisories); + return summary && advisories ? { summary, advisories, createdAt } : null; +} +function persistAudit(file, audit, now = Date.now()) { + try { + (0, import_node_fs8.mkdirSync)((0, import_node_path6.dirname)(file), { recursive: true }); + (0, import_node_fs8.writeFileSync)( + file, + `${JSON.stringify({ + version: CACHE_VERSION, + createdAt: new Date(now).toISOString(), + summary: audit.summary, + advisories: audit.advisories + })} +`, + "utf-8" + ); + } catch { + } +} +function auditAge(createdAt, now = Date.now()) { + const minutes = Math.floor((now - createdAt) / 6e4); + if (minutes < 1) return "less than a minute ago"; + return `${minutes} minute${minutes === 1 ? "" : "s"} ago`; +} + +// src/analyzers/dependencies.ts +var lockFileNames = { + npm: ["package-lock.json", "npm-shrinkwrap.json"], + pnpm: ["pnpm-lock.yaml"], + yarn: ["yarn.lock"] +}; +function findLockFile(projectPath, preferredManager) { + let directory = projectPath; + const managers = preferredManager ? [ + preferredManager, + ...["pnpm", "yarn", "npm"].filter( + (manager) => manager !== preferredManager + ) + ] : ["pnpm", "yarn", "npm"]; + for (let depth = 0; depth <= 3; depth++) { + for (const manager of managers) { + if (lockFileNames[manager].some((name) => (0, import_node_fs9.existsSync)((0, import_node_path7.join)(directory, name)))) { + return { manager, directory }; + } + } + const parent = (0, import_node_path7.dirname)(directory); + if (parent === directory) break; + directory = parent; + } + return null; +} +function declaredManager(value) { + const match2 = /^(npm|pnpm|yarn)@/i.exec(value ?? ""); + return match2?.[1]?.toLowerCase() ?? null; +} +function isModernYarn(projectPath, packageManager, lockDirectory) { + const version2 = /^yarn@(\d+)/i.exec(packageManager ?? "")?.[1]; + if (version2 && Number(version2) >= 2) return true; + let directory = projectPath; + for (let depth = 0; depth <= 3; depth++) { + if ((0, import_node_fs9.existsSync)((0, import_node_path7.join)(directory, ".yarnrc.yml"))) return true; + if (directory === lockDirectory) break; + const parent = (0, import_node_path7.dirname)(directory); + if (parent === directory) break; + directory = parent; + } + return false; +} +var PRODUCTION_FLAGS = { + npm: ["--omit=dev"], + pnpm: ["--prod"], + yarnClassic: ["--groups", "dependencies"], + yarnModern: ["--environment", "production"] +}; +function resolveAuditCommand(projectPath, packageManager, production = false) { + const only = (flags) => production ? [...flags] : []; + const preferredManager = declaredManager(packageManager); + const lockFile = findLockFile(projectPath, preferredManager); + const manager = preferredManager ?? lockFile?.manager ?? "npm"; + const executable = `${manager}${process.platform === "win32" ? ".cmd" : ""}`; + if (manager === "pnpm") { + return { + manager, + command: executable, + args: ["audit", "--json", ...only(PRODUCTION_FLAGS.pnpm)], + fixCommand: "pnpm audit --fix" + }; + } + if (manager === "yarn") { + const modern = isModernYarn( + projectPath, + packageManager, + lockFile?.directory + ); + return { + manager, + command: executable, + args: modern ? [ + "npm", + "audit", + "--json", + "--recursive", + ...only(PRODUCTION_FLAGS.yarnModern) + ] : ["audit", "--json", ...only(PRODUCTION_FLAGS.yarnClassic)], + fixCommand: modern ? "yarn up " : "yarn upgrade" + }; + } + return { + manager, + command: executable, + args: ["audit", "--json", ...only(PRODUCTION_FLAGS.npm)], + fixCommand: "npm audit fix" + }; +} +function readVulnerabilityCounts(value) { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + const record = value; + const names = [ + "critical", + "high", + "moderate", + "low" + ]; + if (!names.some((name) => name in record)) return null; + const summary = {}; + for (const name of names) { + const count = record[name]; + if (typeof count !== "number" || !Number.isFinite(count) || count < 0) { + throw new Error(`invalid ${name} vulnerability count`); + } + summary[name] = count; + } + return summary; +} +function findAuditSummary(value) { + if (!value || typeof value !== "object") return null; + if (Array.isArray(value)) { + for (const entry of value) { + const summary = findAuditSummary(entry); + if (summary) return summary; + } + return null; + } + const record = value; + const metadata = record.metadata; + const metadataSummary = readVulnerabilityCounts(metadata?.vulnerabilities); + if (metadataSummary) return metadataSummary; + if (record.type === "auditSummary") { + const data = record.data; + const yarnSummary = readVulnerabilityCounts(data?.vulnerabilities); + if (yarnSummary) return yarnSummary; + } + return null; +} +function parseAuditSummary(output) { + const trimmed = output.trim(); + if (!trimmed) throw new Error("audit returned no JSON"); + try { + const summary = findAuditSummary(JSON.parse(trimmed)); + if (summary) return summary; + } catch (error) { + if (error instanceof SyntaxError) { + } else { + throw error; + } + } + for (const line of trimmed.split(/\r?\n/)) { + if (!line.trim()) continue; + let entry; + try { + entry = JSON.parse(line); + } catch { + throw new Error("audit returned invalid JSON"); + } + const summary = findAuditSummary(entry); + if (summary) return summary; + } + throw new Error("audit JSON did not include a vulnerability summary"); +} +var AUDIT_TIMEOUT_MS = 3e4; +var DEFAULT_AUDIT_CONCURRENCY = 4; +var runAuditProcess = (command, cwd) => { + const result = (0, import_node_child_process3.spawnSync)(command.command, command.args, { + cwd, + timeout: AUDIT_TIMEOUT_MS, + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + shell: process.platform === "win32" + }); + return { + stdout: result.stdout ?? "", + stderr: result.stderr ?? "", + ...result.error ? { error: result.error } : {} + }; +}; +var runAuditProcessAsync = (command, cwd) => new Promise((resolve7) => { + (0, import_node_child_process3.execFile)( + command.command, + command.args, + { + cwd, + timeout: AUDIT_TIMEOUT_MS, + encoding: "utf-8", + maxBuffer: 64 * 1024 * 1024, + shell: process.platform === "win32", + windowsHide: true + }, + (error, stdout, stderr) => { + const failed = error !== null && (error.killed || typeof error.code !== "number"); + resolve7({ + stdout: stdout ?? "", + stderr: stderr ?? "", + ...failed ? { error } : {} + }); + } + ); +}); +var auditCache = /* @__PURE__ */ new Map(); +function hashFile(path2) { + try { + return (0, import_node_crypto3.createHash)("sha256").update((0, import_node_fs9.readFileSync)(path2)).digest("hex"); + } catch { + return "missing"; + } +} +function auditCacheKey(projectPath, command, lockFile) { + const lockPath = lockFileNames[lockFile.manager].map((name) => (0, import_node_path7.join)(lockFile.directory, name)).find((candidate) => (0, import_node_fs9.existsSync)(candidate)); + return [ + command.command, + ...command.args, + projectPath, + lockPath ? hashFile(lockPath) : "no-lock", + hashFile((0, import_node_path7.join)(projectPath, "package.json")) + ].join("\0"); +} +function persistedOutcome(file) { + const persisted = file ? readPersistedAudit(file) : null; + return persisted ? { + kind: "summary", + summary: persisted.summary, + advisories: persisted.advisories, + cachedAt: persisted.createdAt + } : void 0; +} +function persistOutcome(file, outcome) { + if (file && outcome.kind === "summary") { + persistAudit(file, { + summary: outcome.summary, + advisories: outcome.advisories ?? [] + }); + } +} +function auditOutcome(run, command) { + try { + if (run.error) throw run.error; + if (!run.stdout.trim()) { + throw new Error( + run.stderr.trim() || `${command.manager} audit returned no JSON` + ); + } + return { + kind: "summary", + summary: parseAuditSummary(run.stdout), + advisories: parseAuditAdvisories(run.stdout) + }; + } catch (error) { + return { + kind: "error", + message: error instanceof Error ? error.message : String(error) + }; + } +} +function runAudit(projectPath, command, runner, cacheFile) { + const outcome = auditOutcome(runner(command, projectPath), command); + persistOutcome(cacheFile, outcome); + return outcome; +} +function auditJob(target) { + const projectPath = (0, import_node_path7.resolve)(target.projectPath); + let pkg; + try { + pkg = JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path7.join)(projectPath, "package.json"), "utf-8")); + } catch { + return null; + } + const lockFile = findLockFile( + projectPath, + declaredManager(pkg.packageManager) + ); + if (!lockFile) return null; + const command = resolveAuditCommand( + projectPath, + pkg.packageManager, + target.production + ); + const key = auditCacheKey(projectPath, command, lockFile); + return { + projectPath, + command, + key, + cacheFile: auditCacheFile(lockFile.directory, key) + }; +} +async function prefetchAudits(targets, options = {}) { + const runner = options.runner ?? runAuditProcessAsync; + const jobs = [ + ...new Map( + targets.flatMap((target) => { + const job = auditJob(target); + return job ? [[job.key, job]] : []; + }) + ).values() + ]; + let next = 0; + const work = async () => { + for (let job = jobs[next++]; job; job = jobs[next++]) { + let outcome = persistedOutcome(job.cacheFile); + if (!outcome) { + outcome = auditOutcome( + await runner(job.command, job.projectPath), + job.command + ); + persistOutcome(job.cacheFile, outcome); + } + auditCache.set(job.key, outcome); + } + }; + const workers = Math.min( + options.concurrency ?? DEFAULT_AUDIT_CONCURRENCY, + jobs.length + ); + await Promise.all(Array.from({ length: workers }, work)); +} +function recordAudit(outcome, auditCommand, issues) { + if (outcome.kind === "error") { + issues.push({ + ...fromRule("audit-unavailable"), + message: `${auditCommand.manager} audit could not be evaluated: ${outcome.message}`, + fix: `Run ${[auditCommand.manager, ...auditCommand.args].join(" ")} and resolve the reported error` + }); + return false; + } + const { critical, high, moderate, low } = outcome.summary; + const affected = (severity) => describeAdvisories(outcome.advisories ?? [], severity); + if (critical > 0) + issues.push({ + ...fromRule("vuln-critical"), + message: `${critical} critical vulnerabilit${critical > 1 ? "ies" : "y"}`, + fix: `Run ${auditCommand.fixCommand}.${affected("critical")}` + }); + if (high > 0) + issues.push({ + ...fromRule("vuln-high"), + message: `${high} high severity vulnerabilit${high > 1 ? "ies" : "y"}`, + fix: `Run ${auditCommand.fixCommand}.${affected("high")}` + }); + if (moderate > 0) + issues.push({ + ...fromRule("vuln-moderate"), + message: `${moderate} moderate vulnerabilit${moderate > 1 ? "ies" : "y"}`, + fix: `Review ${auditCommand.manager} audit output.${affected("moderate")}` + }); + if (low > 0) { + const lowAffected = affected("low"); + issues.push({ + ...fromRule("vuln-low"), + message: `${low} low severity vulnerabilit${low > 1 ? "ies" : "y"}`, + ...lowAffected ? { fix: lowAffected.trim() } : {} + }); + } + return critical + high + moderate + low === 0; +} +async function analyzeDependencies(context, options = {}) { + const { projectPath } = context; + const issues = []; + let checksRun = 0; + let checksPassed = 0; + if (context.packageJsonStatus === "missing") { + return { + name: "Dependencies", + score: 0, + issues: [ + { + ...fromRule("no-package-json"), + message: "No package.json found" + } + ], + summary: "No package.json" + }; + } + const pkg = context.packageJson; + if (!pkg) { + return { + name: "Dependencies", + score: 0, + issues: [ + { + ...fromRule("invalid-package-json"), + message: "Cannot parse package.json" + } + ], + summary: "Invalid package.json" + }; + } + checksRun++; + const preferredManager = declaredManager(pkg.packageManager); + const lockFile = findLockFile(projectPath, preferredManager); + const auditCommand = resolveAuditCommand( + projectPath, + pkg.packageManager, + context.config.audit === "production" + ); + const hasLock = lockFile !== null && (preferredManager === null || lockFile.manager === auditCommand.manager); + if (hasLock) { + checksPassed++; + } else if (lockFile) { + issues.push({ + ...fromRule("lock-file-manager-mismatch"), + message: `packageManager selects ${auditCommand.manager}, but the detected lock file belongs to ${lockFile.manager}`, + fix: `Generate and commit the ${auditCommand.manager} lock file, then remove conflicting lock files` + }); + } else { + issues.push({ + ...fromRule("no-lock-file"), + message: "No lock file \u2014 builds are not reproducible and dependencies cannot be audited", + fix: `Run ${auditCommand.manager} install and commit the generated lock file` + }); + } + const auditMode = options.audit ?? context.config.audit !== false; + const cacheKey = lockFile ? auditCacheKey(projectPath, auditCommand, lockFile) : null; + const cacheFile = lockFile && cacheKey && auditMode !== false ? auditCacheFile(lockFile.directory, cacheKey) : null; + const persisted = persistedOutcome(cacheFile); + const cached = (cacheKey ? auditCache.get(cacheKey) : void 0) ?? persisted; + let reusedFrom; + if (auditMode === true || auditMode === "cached" && (cached || !lockFile)) { + checksRun++; + if (lockFile && cacheKey) { + const outcome = (auditMode === "cached" ? cached : persisted) ?? runAudit( + projectPath, + auditCommand, + options.runAudit ?? runAuditProcess, + cacheFile + ); + auditCache.set(cacheKey, outcome); + if (outcome.kind === "summary") reusedFrom = outcome.cachedAt; + if (recordAudit(outcome, auditCommand, issues)) checksPassed++; + } + } else { + issues.push({ + ...fromRule("audit-skipped"), + message: auditMode === "cached" ? "Dependency audit has not run for this lock file yet; known vulnerabilities were not checked" : "Dependency audit skipped; known vulnerabilities were not checked", + fix: `Run ${[auditCommand.manager, ...auditCommand.args].join(" ")} or scan without --no-audit` + }); + } + checksRun++; + if (pkg.engines?.node) { + checksPassed++; + } else { + issues.push({ + ...fromRule("no-engines"), + message: "No engines.node in package.json", + fix: 'Add "engines": { "node": ">=18.0.0" }' + }); + } + checksRun++; + if (pkg.scripts?.build && (pkg.scripts?.start || pkg.scripts?.dev)) { + checksPassed++; + } else { + issues.push({ + ...fromRule("missing-scripts"), + message: "Missing essential scripts (build, start/dev)", + fix: "Add build and start scripts" + }); + } + checksRun++; + const risky = ["request", "node-uuid", "nomnom", "coffee-script"]; + const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }; + const found = risky.filter((d) => d in allDeps); + if (found.length === 0) { + checksPassed++; + } else { + for (const dependency of found) { + issues.push({ + ...fromRule("deprecated-dep"), + message: `"${dependency}" is deprecated` + }); + } + } + const score = checksRun > 0 ? Math.round(checksPassed / checksRun * 100) : 0; + return { + name: "Dependencies", + score, + issues, + summary: `${Math.round(checksPassed)}/${checksRun} checks passed${reusedFrom === void 0 ? "" : `; audit reused from ${auditAge(reusedFrom)}`}` + }; +} // src/scanner.ts -var import_node_fs13 = require("fs"); -var import_node_path14 = require("path"); +var import_node_fs14 = require("fs"); +var import_node_path15 = require("path"); // node_modules/chalk/source/vendor/ansi-styles/index.js var ANSI_BACKGROUND_OFFSET = 10; @@ -256982,479 +257640,8 @@ function ora(options) { return new Ora(options); } -// src/analyzers/dependencies.ts -var import_node_child_process3 = require("child_process"); -var import_node_crypto2 = require("crypto"); -var import_node_fs8 = require("fs"); -var import_node_path6 = require("path"); - -// src/analyzers/audit-advisories.ts -var MAX_LISTED = 5; -var SEVERITIES3 = /* @__PURE__ */ new Set(["critical", "high", "moderate", "low"]); -function isRecord4(value) { - return typeof value === "object" && value !== null && !Array.isArray(value); -} -function text(value) { - return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0; -} -function advisory(name, severity, title, url) { - const packageName2 = text(name); - const level = typeof severity === "string" ? severity.toLowerCase() : ""; - if (!packageName2 || !SEVERITIES3.has(level)) return null; - const titleText = text(title); - const urlText = text(url); - return { - name: packageName2, - severity: level, - ...titleText ? { title: titleText } : {}, - ...urlText ? { url: urlText } : {} - }; -} -function npmAdvisories(document) { - if (!isRecord4(document.vulnerabilities)) return []; - return Object.entries(document.vulnerabilities).flatMap(([name, entry]) => { - if (!isRecord4(entry)) return []; - const source = Array.isArray(entry.via) ? entry.via.find(isRecord4) : void 0; - const found = advisory(name, entry.severity, source?.title, source?.url); - return found ? [found] : []; - }); -} -function advisoryMap(document) { - if (!isRecord4(document.advisories)) return []; - return Object.values(document.advisories).flatMap((entry) => { - if (!isRecord4(entry)) return []; - const found = advisory( - entry.module_name, - entry.severity, - entry.title, - entry.url - ); - return found ? [found] : []; - }); -} -function yarnAdvisory(line) { - if (line.type === "auditAdvisory" && isRecord4(line.data)) { - const data = isRecord4(line.data.advisory) ? line.data.advisory : void 0; - return data ? advisory(data.module_name, data.severity, data.title, data.url) : null; - } - if (isRecord4(line.children)) { - const { children } = line; - return advisory( - line.value, - children.Severity, - children.Issue, - children.URL - ); - } - return null; -} -function parseAuditAdvisories(output) { - const found = []; - const trimmed = output.trim(); - try { - const document = JSON.parse(trimmed); - if (isRecord4(document)) { - found.push(...npmAdvisories(document), ...advisoryMap(document)); - } - } catch { - for (const line of trimmed.split(/\r?\n/)) { - try { - const entry = JSON.parse(line); - const parsed = isRecord4(entry) ? yarnAdvisory(entry) : null; - if (parsed) found.push(parsed); - } catch { - } - } - } - const order = ["critical", "high", "moderate", "low"]; - const unique = /* @__PURE__ */ new Map(); - for (const entry of found) { - const key = `${entry.severity}\0${entry.name}`; - if (!unique.has(key)) unique.set(key, entry); - } - return [...unique.values()].sort( - (left, right) => order.indexOf(left.severity) - order.indexOf(right.severity) || left.name.localeCompare(right.name) - ); -} -function describeAdvisories(advisories, severity) { - const matching = advisories.filter((entry) => entry.severity === severity); - if (matching.length === 0) return ""; - const listed = matching.slice(0, MAX_LISTED).map((entry) => { - const details = [entry.title, entry.url].filter(Boolean).join(", "); - return details ? `${entry.name} (${details})` : entry.name; - }); - const more = matching.length - listed.length; - return ` Affected: ${listed.join("; ")}${more > 0 ? `; and ${more} more` : ""}.`; -} - -// src/analyzers/dependencies.ts -var lockFileNames = { - npm: ["package-lock.json", "npm-shrinkwrap.json"], - pnpm: ["pnpm-lock.yaml"], - yarn: ["yarn.lock"] -}; -function findLockFile(projectPath, preferredManager) { - let directory = projectPath; - const managers = preferredManager ? [ - preferredManager, - ...["pnpm", "yarn", "npm"].filter( - (manager) => manager !== preferredManager - ) - ] : ["pnpm", "yarn", "npm"]; - for (let depth = 0; depth <= 3; depth++) { - for (const manager of managers) { - if (lockFileNames[manager].some((name) => (0, import_node_fs8.existsSync)((0, import_node_path6.join)(directory, name)))) { - return { manager, directory }; - } - } - const parent = (0, import_node_path6.dirname)(directory); - if (parent === directory) break; - directory = parent; - } - return null; -} -function declaredManager(value) { - const match2 = /^(npm|pnpm|yarn)@/i.exec(value ?? ""); - return match2?.[1]?.toLowerCase() ?? null; -} -function isModernYarn(projectPath, packageManager, lockDirectory) { - const version2 = /^yarn@(\d+)/i.exec(packageManager ?? "")?.[1]; - if (version2 && Number(version2) >= 2) return true; - let directory = projectPath; - for (let depth = 0; depth <= 3; depth++) { - if ((0, import_node_fs8.existsSync)((0, import_node_path6.join)(directory, ".yarnrc.yml"))) return true; - if (directory === lockDirectory) break; - const parent = (0, import_node_path6.dirname)(directory); - if (parent === directory) break; - directory = parent; - } - return false; -} -var PRODUCTION_FLAGS = { - npm: ["--omit=dev"], - pnpm: ["--prod"], - yarnClassic: ["--groups", "dependencies"], - yarnModern: ["--environment", "production"] -}; -function resolveAuditCommand(projectPath, packageManager, production = false) { - const only = (flags) => production ? [...flags] : []; - const preferredManager = declaredManager(packageManager); - const lockFile = findLockFile(projectPath, preferredManager); - const manager = preferredManager ?? lockFile?.manager ?? "npm"; - const executable = `${manager}${process.platform === "win32" ? ".cmd" : ""}`; - if (manager === "pnpm") { - return { - manager, - command: executable, - args: ["audit", "--json", ...only(PRODUCTION_FLAGS.pnpm)], - fixCommand: "pnpm audit --fix" - }; - } - if (manager === "yarn") { - const modern = isModernYarn( - projectPath, - packageManager, - lockFile?.directory - ); - return { - manager, - command: executable, - args: modern ? [ - "npm", - "audit", - "--json", - "--recursive", - ...only(PRODUCTION_FLAGS.yarnModern) - ] : ["audit", "--json", ...only(PRODUCTION_FLAGS.yarnClassic)], - fixCommand: modern ? "yarn up " : "yarn upgrade" - }; - } - return { - manager, - command: executable, - args: ["audit", "--json", ...only(PRODUCTION_FLAGS.npm)], - fixCommand: "npm audit fix" - }; -} -function readVulnerabilityCounts(value) { - if (!value || typeof value !== "object" || Array.isArray(value)) return null; - const record = value; - const names = [ - "critical", - "high", - "moderate", - "low" - ]; - if (!names.some((name) => name in record)) return null; - const summary = {}; - for (const name of names) { - const count = record[name]; - if (typeof count !== "number" || !Number.isFinite(count) || count < 0) { - throw new Error(`invalid ${name} vulnerability count`); - } - summary[name] = count; - } - return summary; -} -function findAuditSummary(value) { - if (!value || typeof value !== "object") return null; - if (Array.isArray(value)) { - for (const entry of value) { - const summary = findAuditSummary(entry); - if (summary) return summary; - } - return null; - } - const record = value; - const metadata = record.metadata; - const metadataSummary = readVulnerabilityCounts(metadata?.vulnerabilities); - if (metadataSummary) return metadataSummary; - if (record.type === "auditSummary") { - const data = record.data; - const yarnSummary = readVulnerabilityCounts(data?.vulnerabilities); - if (yarnSummary) return yarnSummary; - } - return null; -} -function parseAuditSummary(output) { - const trimmed = output.trim(); - if (!trimmed) throw new Error("audit returned no JSON"); - try { - const summary = findAuditSummary(JSON.parse(trimmed)); - if (summary) return summary; - } catch (error) { - if (error instanceof SyntaxError) { - } else { - throw error; - } - } - for (const line of trimmed.split(/\r?\n/)) { - if (!line.trim()) continue; - let entry; - try { - entry = JSON.parse(line); - } catch { - throw new Error("audit returned invalid JSON"); - } - const summary = findAuditSummary(entry); - if (summary) return summary; - } - throw new Error("audit JSON did not include a vulnerability summary"); -} -var runAuditProcess = (command, cwd) => { - const result = (0, import_node_child_process3.spawnSync)(command.command, command.args, { - cwd, - timeout: 3e4, - encoding: "utf-8", - stdio: ["ignore", "pipe", "pipe"], - shell: process.platform === "win32" - }); - return { - stdout: result.stdout ?? "", - stderr: result.stderr ?? "", - ...result.error ? { error: result.error } : {} - }; -}; -var auditCache = /* @__PURE__ */ new Map(); -function hashFile(path2) { - try { - return (0, import_node_crypto2.createHash)("sha256").update((0, import_node_fs8.readFileSync)(path2)).digest("hex"); - } catch { - return "missing"; - } -} -function auditCacheKey(projectPath, command, lockFile) { - const lockPath = lockFileNames[lockFile.manager].map((name) => (0, import_node_path6.join)(lockFile.directory, name)).find((candidate) => (0, import_node_fs8.existsSync)(candidate)); - return [ - command.command, - ...command.args, - projectPath, - lockPath ? hashFile(lockPath) : "no-lock", - hashFile((0, import_node_path6.join)(projectPath, "package.json")) - ].join("\0"); -} -function runAudit(projectPath, command, runner) { - const run = runner(command, projectPath); - try { - if (run.error) throw run.error; - if (!run.stdout.trim()) { - throw new Error( - run.stderr.trim() || `${command.manager} audit returned no JSON` - ); - } - return { - kind: "summary", - summary: parseAuditSummary(run.stdout), - advisories: parseAuditAdvisories(run.stdout) - }; - } catch (error) { - return { - kind: "error", - message: error instanceof Error ? error.message : String(error) - }; - } -} -function recordAudit(outcome, auditCommand, issues) { - if (outcome.kind === "error") { - issues.push({ - ...fromRule("audit-unavailable"), - message: `${auditCommand.manager} audit could not be evaluated: ${outcome.message}`, - fix: `Run ${[auditCommand.manager, ...auditCommand.args].join(" ")} and resolve the reported error` - }); - return false; - } - const { critical, high, moderate, low } = outcome.summary; - const affected = (severity) => describeAdvisories(outcome.advisories ?? [], severity); - if (critical > 0) - issues.push({ - ...fromRule("vuln-critical"), - message: `${critical} critical vulnerabilit${critical > 1 ? "ies" : "y"}`, - fix: `Run ${auditCommand.fixCommand}.${affected("critical")}` - }); - if (high > 0) - issues.push({ - ...fromRule("vuln-high"), - message: `${high} high severity vulnerabilit${high > 1 ? "ies" : "y"}`, - fix: `Run ${auditCommand.fixCommand}.${affected("high")}` - }); - if (moderate > 0) - issues.push({ - ...fromRule("vuln-moderate"), - message: `${moderate} moderate vulnerabilit${moderate > 1 ? "ies" : "y"}`, - fix: `Review ${auditCommand.manager} audit output.${affected("moderate")}` - }); - if (low > 0) { - const lowAffected = affected("low"); - issues.push({ - ...fromRule("vuln-low"), - message: `${low} low severity vulnerabilit${low > 1 ? "ies" : "y"}`, - ...lowAffected ? { fix: lowAffected.trim() } : {} - }); - } - return critical + high + moderate + low === 0; -} -async function analyzeDependencies(context, options = {}) { - const { projectPath } = context; - const issues = []; - let checksRun = 0; - let checksPassed = 0; - if (context.packageJsonStatus === "missing") { - return { - name: "Dependencies", - score: 0, - issues: [ - { - ...fromRule("no-package-json"), - message: "No package.json found" - } - ], - summary: "No package.json" - }; - } - const pkg = context.packageJson; - if (!pkg) { - return { - name: "Dependencies", - score: 0, - issues: [ - { - ...fromRule("invalid-package-json"), - message: "Cannot parse package.json" - } - ], - summary: "Invalid package.json" - }; - } - checksRun++; - const preferredManager = declaredManager(pkg.packageManager); - const lockFile = findLockFile(projectPath, preferredManager); - const auditCommand = resolveAuditCommand( - projectPath, - pkg.packageManager, - context.config.audit === "production" - ); - const hasLock = lockFile !== null && (preferredManager === null || lockFile.manager === auditCommand.manager); - if (hasLock) { - checksPassed++; - } else if (lockFile) { - issues.push({ - ...fromRule("lock-file-manager-mismatch"), - message: `packageManager selects ${auditCommand.manager}, but the detected lock file belongs to ${lockFile.manager}`, - fix: `Generate and commit the ${auditCommand.manager} lock file, then remove conflicting lock files` - }); - } else { - issues.push({ - ...fromRule("no-lock-file"), - message: "No lock file \u2014 builds are not reproducible and dependencies cannot be audited", - fix: `Run ${auditCommand.manager} install and commit the generated lock file` - }); - } - const auditMode = options.audit ?? context.config.audit !== false; - const cacheKey = lockFile ? auditCacheKey(projectPath, auditCommand, lockFile) : null; - const cached = cacheKey ? auditCache.get(cacheKey) : void 0; - if (auditMode === true || auditMode === "cached" && (cached || !lockFile)) { - checksRun++; - if (lockFile && cacheKey) { - const outcome = auditMode === "cached" && cached ? cached : runAudit( - projectPath, - auditCommand, - options.runAudit ?? runAuditProcess - ); - auditCache.set(cacheKey, outcome); - if (recordAudit(outcome, auditCommand, issues)) checksPassed++; - } - } else { - issues.push({ - ...fromRule("audit-skipped"), - message: auditMode === "cached" ? "Dependency audit has not run for this lock file yet; known vulnerabilities were not checked" : "Dependency audit skipped; known vulnerabilities were not checked", - fix: `Run ${[auditCommand.manager, ...auditCommand.args].join(" ")} or scan without --no-audit` - }); - } - checksRun++; - if (pkg.engines?.node) { - checksPassed++; - } else { - issues.push({ - ...fromRule("no-engines"), - message: "No engines.node in package.json", - fix: 'Add "engines": { "node": ">=18.0.0" }' - }); - } - checksRun++; - if (pkg.scripts?.build && (pkg.scripts?.start || pkg.scripts?.dev)) { - checksPassed++; - } else { - issues.push({ - ...fromRule("missing-scripts"), - message: "Missing essential scripts (build, start/dev)", - fix: "Add build and start scripts" - }); - } - checksRun++; - const risky = ["request", "node-uuid", "nomnom", "coffee-script"]; - const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }; - const found = risky.filter((d) => d in allDeps); - if (found.length === 0) { - checksPassed++; - } else { - for (const dependency of found) { - issues.push({ - ...fromRule("deprecated-dep"), - message: `"${dependency}" is deprecated` - }); - } - } - const score = checksRun > 0 ? Math.round(checksPassed / checksRun * 100) : 0; - return { - name: "Dependencies", - score, - issues, - summary: `${Math.round(checksPassed)}/${checksRun} checks passed` - }; -} - // src/analyzers/express-api.ts -var import_node_path7 = require("path"); +var import_node_path8 = require("path"); var import_ts_morph2 = __toESM(require_ts_morph(), 1); var HTTP_METHODS = /* @__PURE__ */ new Set([ "all", @@ -257585,7 +257772,7 @@ function collectRouters(sourceFile, file) { } function resolveModule(fromFile, specifier, files) { if (!specifier.startsWith(".")) return null; - const base = import_node_path7.posix.normalize(import_node_path7.posix.join(import_node_path7.posix.dirname(fromFile), specifier)); + const base = import_node_path8.posix.normalize(import_node_path8.posix.join(import_node_path8.posix.dirname(fromFile), specifier)); const stem = base.replace(/\.(?:[cm]?js|jsx)$/, ""); const candidates = [ base, @@ -257915,11 +258102,11 @@ async function analyzeExpressApi(context) { var import_ts_morph5 = __toESM(require_ts_morph(), 1); // src/analyzers/fastify-autoload.ts -var import_node_path9 = require("path"); +var import_node_path10 = require("path"); var import_ts_morph4 = __toESM(require_ts_morph(), 1); // src/analyzers/fastify-plugins.ts -var import_node_path8 = require("path"); +var import_node_path9 = require("path"); var import_ts_morph3 = __toESM(require_ts_morph(), 1); var INSTANCE_NAMES = /(?:^|\.)(?:app|server|fastify|instance)$/i; var INSTANCE_TYPE = /^(?:[A-Za-z_$][\w$]*\.)?FastifyInstance\b/; @@ -258012,7 +258199,7 @@ function pathAliases(options, projectPath) { prefix, suffix, targets: targets.map( - (target) => (0, import_node_path8.relative)(projectPath, (0, import_node_path8.resolve)(base, target)).replace(/\\/g, "/") + (target) => (0, import_node_path9.relative)(projectPath, (0, import_node_path9.resolve)(base, target)).replace(/\\/g, "/") ) } ]; @@ -258020,7 +258207,7 @@ function pathAliases(options, projectPath) { } function importBases(index, fromFile, specifier) { if (specifier.startsWith(".")) { - return [import_node_path8.posix.normalize(import_node_path8.posix.join(import_node_path8.posix.dirname(fromFile), specifier))]; + return [import_node_path9.posix.normalize(import_node_path9.posix.join(import_node_path9.posix.dirname(fromFile), specifier))]; } return (index.aliases ?? []).flatMap(({ prefix, suffix, targets }) => { if (specifier.length < prefix.length + suffix.length || !specifier.startsWith(prefix) || !specifier.endsWith(suffix)) { @@ -258031,7 +258218,7 @@ function importBases(index, fromFile, specifier) { specifier.length - suffix.length ); return targets.map( - (target) => import_node_path8.posix.normalize(target.replace("*", matched)) + (target) => import_node_path9.posix.normalize(target.replace("*", matched)) ); }); } @@ -258243,7 +258430,7 @@ function fileOf(index, node) { return index.paths.get(node.getSourceFile()); } function within(base, segments) { - const joined = import_node_path9.posix.normalize(import_node_path9.posix.join(base || ".", ...segments)); + const joined = import_node_path10.posix.normalize(import_node_path10.posix.join(base || ".", ...segments)); if (joined === ".." || joined.startsWith("../")) return null; return joined === "." ? "" : joined.replace(/\/$/, ""); } @@ -258253,12 +258440,12 @@ function baseDirectory(index, node) { if (text2 === "process.cwd()") return ""; const file = fileOf(index, value); if (file === void 0) return null; - if (FILE_DIRECTORY.test(text2)) return import_node_path9.posix.dirname(file); + if (FILE_DIRECTORY.test(text2)) return import_node_path10.posix.dirname(file); const resolved = definition(index, value); if (!import_ts_morph4.Node.isVariableDeclaration(resolved)) return null; const initializer = resolved.getInitializer()?.getText() ?? ""; const declared = fileOf(index, resolved); - return /\bdirname\s*\(/.test(initializer) && initializer.includes("import.meta.url") && declared !== void 0 ? import_node_path9.posix.dirname(declared) : null; + return /\bdirname\s*\(/.test(initializer) && initializer.includes("import.meta.url") && declared !== void 0 ? import_node_path10.posix.dirname(declared) : null; } function calleeName(call) { const callee = call.getExpression(); @@ -258275,7 +258462,7 @@ function directoryOf(index, node, depth = 0) { const [path2, base] = value.getArguments(); const relative8 = literalText2(path2); const file = fileOf(index, value); - return relative8 !== null && base?.getText() === "import.meta.url" && file !== void 0 ? within(import_node_path9.posix.dirname(file), [relative8]) : null; + return relative8 !== null && base?.getText() === "import.meta.url" && file !== void 0 ? within(import_node_path10.posix.dirname(file), [relative8]) : null; } if (import_ts_morph4.Node.isCallExpression(value)) { const name = calleeName(value); @@ -258353,7 +258540,7 @@ function directories(index, options) { return found; } function hooksOf(path2, found, options) { - const parent = path2.includes("/") ? import_node_path9.posix.dirname(path2) : ""; + const parent = path2.includes("/") ? import_node_path10.posix.dirname(path2) : ""; const inherited3 = options.cascadeHooks && path2 !== "" ? hooksOf(parent, found, options) : []; const own = found.get(path2)?.hooks; if (!own) return inherited3; @@ -260768,8 +260955,8 @@ async function analyzeNextjsApi(context) { } // src/analyzers/security.ts -var import_node_fs9 = require("fs"); -var import_node_path10 = require("path"); +var import_node_fs10 = require("fs"); +var import_node_path11 = require("path"); // src/analyzers/security-sinks.ts var import_ts_morph17 = __toESM(require_ts_morph(), 1); @@ -262226,23 +262413,23 @@ async function analyzeSecurity(context, options = {}) { let foundGitignore = false; let directory = projectPath; for (let depth = 0; depth <= 3; depth++) { - const gitignorePath = (0, import_node_path10.join)(directory, ".gitignore"); - if ((0, import_node_fs9.existsSync)(gitignorePath)) { + const gitignorePath = (0, import_node_path11.join)(directory, ".gitignore"); + if ((0, import_node_fs10.existsSync)(gitignorePath)) { foundGitignore = true; - const content = (0, import_node_fs9.readFileSync)(gitignorePath, "utf-8"); + const content = (0, import_node_fs10.readFileSync)(gitignorePath, "utf-8"); if (gitignoreProtectsEnv(content)) { checksPassed++; } else { issues.push({ ...fromRule("env-not-gitignored"), message: ".env is not ignored by .gitignore \u2014 secrets may be committed", - file: (0, import_node_path10.relative)(projectPath, gitignorePath).replace(/\\/g, "/"), + file: (0, import_node_path11.relative)(projectPath, gitignorePath).replace(/\\/g, "/"), fix: "Add .env or .env* to your .gitignore" }); } break; } - const parent = (0, import_node_path10.dirname)(directory); + const parent = (0, import_node_path11.dirname)(directory); if (parent === directory) break; directory = parent; } @@ -262345,8 +262532,8 @@ async function analyzeSecurity(context, options = {}) { } // src/analyzers/structure.ts -var import_node_fs10 = require("fs"); -var import_node_path11 = require("path"); +var import_node_fs11 = require("fs"); +var import_node_path12 = require("path"); var LINTER_CONFIGS = [ "eslint.config.js", "eslint.config.cjs", @@ -262385,10 +262572,10 @@ function findConfigUp(projectPath, fileNames, maxParentDepth = 3) { let directory = projectPath; for (let depth = 0; depth <= maxParentDepth; depth++) { for (const fileName of fileNames) { - const candidate = (0, import_node_path11.join)(directory, fileName); - if ((0, import_node_fs10.existsSync)(candidate)) return candidate; + const candidate = (0, import_node_path12.join)(directory, fileName); + if ((0, import_node_fs11.existsSync)(candidate)) return candidate; } - const parent = (0, import_node_path11.dirname)(directory); + const parent = (0, import_node_path12.dirname)(directory); if (parent === directory) break; directory = parent; } @@ -262403,13 +262590,13 @@ function findReadme(projectPath) { "readme.txt", "readme" ]; - const entries = (0, import_node_fs10.readdirSync)(projectPath); + const entries = (0, import_node_fs11.readdirSync)(projectPath); const byLowerName = new Map( entries.map((entry) => [entry.toLowerCase(), entry]) ); for (const name of preferredNames) { const actualName = byLowerName.get(name); - if (actualName) return (0, import_node_path11.join)(projectPath, actualName); + if (actualName) return (0, import_node_path12.join)(projectPath, actualName); } } catch { return void 0; @@ -262420,11 +262607,11 @@ function meaningfulReadmeLength(content) { return content.replace(//g, " ").replace(/!\[[^\]]*\]\([^)]*\)/g, " ").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/<[^>]+>/g, " ").replace(/[`#>*_|~=-]/g, " ").replace(/\s+/g, " ").trim().length; } function displayPath(projectPath, filePath) { - return (0, import_node_path11.relative)(projectPath, filePath).replace(/\\/g, "/") || "."; + return (0, import_node_path12.relative)(projectPath, filePath).replace(/\\/g, "/") || "."; } function rootEnvFiles(projectPath) { try { - return (0, import_node_fs10.readdirSync)(projectPath).filter( + return (0, import_node_fs11.readdirSync)(projectPath).filter( (entry) => /^\.env(?:\..+)?$/.test(entry) && !ENV_TEMPLATES.includes(entry) ); } catch { @@ -262432,8 +262619,8 @@ function rootEnvFiles(projectPath) { } } function analyzeNestOrganization(context, issues) { - const srcPath = (0, import_node_path11.join)(context.projectPath, "src"); - if (!(0, import_node_fs10.existsSync)(srcPath)) { + const srcPath = (0, import_node_path12.join)(context.projectPath, "src"); + if (!(0, import_node_fs11.existsSync)(srcPath)) { issues.push({ ...fromRule("no-src-dir"), message: "No src/ directory found", @@ -262465,11 +262652,11 @@ function analyzeNestOrganization(context, issues) { return false; } const moduleDirectories = new Set( - moduleFiles.map((file) => (0, import_node_path11.dirname)(file).replace(/\\/g, "/")) + moduleFiles.map((file) => (0, import_node_path12.dirname)(file).replace(/\\/g, "/")) ); const featureDirectories = new Set( [...controllerFiles, ...serviceFiles].map( - (file) => (0, import_node_path11.dirname)(file).replace(/\\/g, "/") + (file) => (0, import_node_path12.dirname)(file).replace(/\\/g, "/") ) ); if (featureDirectories.size === 0) return true; @@ -262477,7 +262664,7 @@ function analyzeNestOrganization(context, issues) { let directory = featureDirectory; while (directory !== "src" && directory !== ".") { if (moduleDirectories.has(directory)) return true; - directory = (0, import_node_path11.dirname)(directory).replace(/\\/g, "/"); + directory = (0, import_node_path12.dirname)(directory).replace(/\\/g, "/"); } return featureDirectory === "src" && moduleDirectories.has("src"); }; @@ -262510,12 +262697,12 @@ async function analyzeStructure(context) { const pkg = context.packageJson ?? {}; const hasDependency = (name) => Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]); const isNestjs = hasDependency("@nestjs/core"); - const tsconfigPath = (0, import_node_path11.join)(projectPath, "tsconfig.json"); - const isTypescript = (0, import_node_fs10.existsSync)(tsconfigPath) || hasDependency("typescript"); + const tsconfigPath = (0, import_node_path12.join)(projectPath, "tsconfig.json"); + const isTypescript = (0, import_node_fs11.existsSync)(tsconfigPath) || hasDependency("typescript"); checksRun++; const readmePath = findReadme(projectPath); if (readmePath) { - const content = (0, import_node_fs10.readFileSync)(readmePath, "utf-8"); + const content = (0, import_node_fs11.readFileSync)(readmePath, "utf-8"); if (meaningfulReadmeLength(content) >= 100) { checksPassed++; } else { @@ -262599,7 +262786,7 @@ async function analyzeStructure(context) { checksRun++; const envFiles = rootEnvFiles(projectPath); const envTemplate = ENV_TEMPLATES.find( - (name) => (0, import_node_fs10.existsSync)((0, import_node_path11.join)(projectPath, name)) + (name) => (0, import_node_fs11.existsSync)((0, import_node_path12.join)(projectPath, name)) ); if (envFiles.length === 0 || envTemplate) { checksPassed++; @@ -262621,8 +262808,8 @@ async function analyzeStructure(context) { } // src/analyzers/testing.ts -var import_node_fs11 = require("fs"); -var import_node_path12 = require("path"); +var import_node_fs12 = require("fs"); +var import_node_path13 = require("path"); var import_ts_morph18 = __toESM(require_ts_morph(), 1); var TEST_CONFIG_FILES = ["jest", "vitest"].flatMap( (tool) => ["ts", "js", "mjs", "mts"].map((extension) => `${tool}.config.${extension}`) @@ -262646,11 +262833,11 @@ var COVERAGE_THRESHOLDS = { functions: 70, branches: 70 }; -function isRecord5(value) { +function isRecord6(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function parseCoverageMetric(value) { - if (!isRecord5(value)) return null; + if (!isRecord6(value)) return null; const total = value.total; const covered = value.covered; const pct = value.pct; @@ -262661,13 +262848,13 @@ function parseCoverageMetric(value) { } function readCoverageReport(projectPath) { const candidates = [ - (0, import_node_path12.join)(projectPath, "coverage", "coverage-summary.json"), - (0, import_node_path12.join)(projectPath, "coverage-summary.json") + (0, import_node_path13.join)(projectPath, "coverage", "coverage-summary.json"), + (0, import_node_path13.join)(projectPath, "coverage-summary.json") ]; - const reportPath = candidates.find((candidate) => (0, import_node_fs11.existsSync)(candidate)); + const reportPath = candidates.find((candidate) => (0, import_node_fs12.existsSync)(candidate)); if (!reportPath) return null; - const document = JSON.parse((0, import_node_fs11.readFileSync)(reportPath, "utf-8")); - if (!isRecord5(document) || !isRecord5(document.total)) { + const document = JSON.parse((0, import_node_fs12.readFileSync)(reportPath, "utf-8")); + if (!isRecord6(document) || !isRecord6(document.total)) { throw new Error("missing total coverage summary"); } const metrics = {}; @@ -262680,7 +262867,7 @@ function readCoverageReport(projectPath) { Object.values(metrics).reduce((sum, metric) => sum + metric.pct, 0) / Object.keys(metrics).length ); return { - file: (0, import_node_path12.relative)(projectPath, reportPath).replace(/\\/g, "/"), + file: (0, import_node_path13.relative)(projectPath, reportPath).replace(/\\/g, "/"), metrics, score }; @@ -262706,10 +262893,10 @@ function configDeclaresThreshold(context, file) { } function coverageRcDeclaresThreshold(projectPath) { return COVERAGE_RC_FILES.some((name) => { - const path2 = (0, import_node_path12.join)(projectPath, name); - if (!(0, import_node_fs11.existsSync)(path2)) return false; + const path2 = (0, import_node_path13.join)(projectPath, name); + if (!(0, import_node_fs12.existsSync)(path2)) return false; try { - const options = JSON.parse((0, import_node_fs11.readFileSync)(path2, "utf-8")); + const options = JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf-8")); return options?.["check-coverage"] === true || Object.keys(options ?? {}).some((key) => METRIC_KEYS.has(key)); } catch { return false; @@ -262728,7 +262915,7 @@ function hasCoverageThreshold(context) { return Boolean(jest?.coverageThreshold) || packageTool("c8") || packageTool("nyc") || Object.values(pkg?.scripts ?? {}).some( (script) => COVERAGE_SCRIPT.test(script) ) || coverageRcDeclaresThreshold(context.projectPath) || COVERAGE_CONFIG_FILES.some( - (file) => (0, import_node_fs11.existsSync)((0, import_node_path12.join)(context.projectPath, file)) && configDeclaresThreshold(context, file) + (file) => (0, import_node_fs12.existsSync)((0, import_node_path13.join)(context.projectPath, file)) && configDeclaresThreshold(context, file) ); } async function analyzeTesting(context) { @@ -262789,7 +262976,7 @@ async function analyzeTesting(context) { } checksRun++; const hasE2e = ["test", "tests", "e2e", "__tests__"].some( - (dir) => (0, import_node_fs11.existsSync)((0, import_node_path12.join)(projectPath, dir)) + (dir) => (0, import_node_fs12.existsSync)((0, import_node_path13.join)(projectPath, dir)) ); if (hasE2e) { checksPassed++; @@ -262801,7 +262988,7 @@ async function analyzeTesting(context) { }); } checksRun++; - const hasConfig = (0, import_node_fs11.existsSync)((0, import_node_path12.join)(projectPath, "jest.config.js")) || (0, import_node_fs11.existsSync)((0, import_node_path12.join)(projectPath, "jest.config.ts")) || (0, import_node_fs11.existsSync)((0, import_node_path12.join)(projectPath, "jest.config.mjs")) || (0, import_node_fs11.existsSync)((0, import_node_path12.join)(projectPath, "vitest.config.ts")) || (0, import_node_fs11.existsSync)((0, import_node_path12.join)(projectPath, "vitest.config.js")) || (0, import_node_fs11.existsSync)((0, import_node_path12.join)(projectPath, "vitest.config.mts")); + const hasConfig = (0, import_node_fs12.existsSync)((0, import_node_path13.join)(projectPath, "jest.config.js")) || (0, import_node_fs12.existsSync)((0, import_node_path13.join)(projectPath, "jest.config.ts")) || (0, import_node_fs12.existsSync)((0, import_node_path13.join)(projectPath, "jest.config.mjs")) || (0, import_node_fs12.existsSync)((0, import_node_path13.join)(projectPath, "vitest.config.ts")) || (0, import_node_fs12.existsSync)((0, import_node_path13.join)(projectPath, "vitest.config.js")) || (0, import_node_fs12.existsSync)((0, import_node_path13.join)(projectPath, "vitest.config.mts")); if (hasConfig || framework === "node:test") { checksPassed++; } else { @@ -262824,7 +263011,7 @@ async function analyzeTesting(context) { issues.push({ ...fromRule("invalid-coverage-report"), message: `Coverage summary could not be read: ${error instanceof Error ? error.message : String(error)}`, - file: (0, import_node_fs11.existsSync)((0, import_node_path12.join)(projectPath, "coverage", "coverage-summary.json")) ? "coverage/coverage-summary.json" : "coverage-summary.json", + file: (0, import_node_fs12.existsSync)((0, import_node_path13.join)(projectPath, "coverage", "coverage-summary.json")) ? "coverage/coverage-summary.json" : "coverage-summary.json", fix: "Regenerate coverage-summary.json with Jest, Vitest, or Istanbul" }); } @@ -263481,10 +263668,10 @@ var TrpcResolver = class { ]) ); const paths = /* @__PURE__ */ new Map(); - const join11 = (prefix, key) => [prefix, key].filter(Boolean).join("."); + const join12 = (prefix, key) => [prefix, key].filter(Boolean).join("."); const visit = (members, prefix, visiting) => { for (const { key, value } of members) { - const path2 = join11(prefix, key); + const path2 = join12(prefix, key); if (value.kind === "procedure") { if (!paths.has(value.call)) paths.set(value.call, path2); } else if (value.kind === "record") { @@ -263680,8 +263867,8 @@ var analyzeTrpcApi = withTrpcProcedures( // src/core/changed-files.ts var import_node_child_process4 = require("child_process"); -var import_node_fs12 = require("fs"); -var import_node_path13 = require("path"); +var import_node_fs13 = require("fs"); +var import_node_path14 = require("path"); var GIT_MAX_BUFFER = 64 * 1024 * 1024; function limitToFiles(results, files) { let excluded = 0; @@ -263697,7 +263884,7 @@ function limitToFiles(results, files) { } // src/core/fingerprint.ts -var import_node_crypto3 = require("crypto"); +var import_node_crypto4 = require("crypto"); var FINGERPRINT_KEY = "codediagFinding/v2"; function normalize(text2) { return text2.replace(/\s+/g, " ").trim(); @@ -263714,7 +263901,7 @@ function anchor(issue, lineText) { } function computeFingerprint(issue, lineText, occurrence = 0, namespace) { const file = namespace ? `${namespace}/${issue.file ?? ""}` : issue.file ?? ""; - return (0, import_node_crypto3.createHash)("sha256").update( + return (0, import_node_crypto4.createHash)("sha256").update( [issue.rule, file, anchor(issue, lineText), occurrence].join("\0") ).digest("hex"); } @@ -263982,7 +264169,7 @@ function calculateGrade(score) { return "F"; } async function scan(projectPath, config = loadConfig(projectPath), options = {}) { - if (!(0, import_node_fs13.existsSync)(projectPath)) { + if (!(0, import_node_fs14.existsSync)(projectPath)) { throw new Error(`Directory not found: ${projectPath}`); } const progress = createProgressReporter(options); @@ -264050,7 +264237,7 @@ async function scan(projectPath, config = loadConfig(projectPath), options = {}) const grade = calculateGrade(totalScore); const skipped = context.skipped(); return { - project: (0, import_node_path14.basename)(projectPath), + project: (0, import_node_path15.basename)(projectPath), stack, analyzers, totalScore, @@ -264083,13 +264270,13 @@ function packageIgnores(entries, path2) { }); } function packageConfig(root, path2, rootConfig) { - const directory = (0, import_node_path15.join)(root, path2); - const config = (0, import_node_fs14.existsSync)((0, import_node_path15.join)(directory, ".codediag.yml")) ? loadConfig(directory) : { + const directory = (0, import_node_path16.join)(root, path2); + const config = (0, import_node_fs15.existsSync)((0, import_node_path16.join)(directory, ".codediag.yml")) ? loadConfig(directory) : { ...rootConfig, ignore: packageIgnores(rootConfig.ignore, path2), analyzers: { ...rootConfig.analyzers } }; - if (!(0, import_node_fs14.existsSync)((0, import_node_path15.join)(directory, "package.json"))) { + if (!(0, import_node_fs15.existsSync)((0, import_node_path16.join)(directory, "package.json"))) { return { ...config, analyzers: { ...config.analyzers, dependencies: false } @@ -264110,7 +264297,7 @@ function packageScope(scope, path2) { function relocate(issue, root, path2) { if (issue.file) return { ...issue, file: `${path2}/${issue.file}` }; const manifest = ["package.json", "project.json"].find( - (file) => (0, import_node_fs14.existsSync)((0, import_node_path15.join)(root, path2, file)) + (file) => (0, import_node_fs15.existsSync)((0, import_node_path16.join)(root, path2, file)) ); return { ...issue, file: manifest ? `${path2}/${manifest}` : path2 }; } @@ -264193,7 +264380,7 @@ function combineWorkspace(root, info, scans, options) { 0 ); return { - project: (0, import_node_path15.basename)(root), + project: (0, import_node_path16.basename)(root), stack: detectStack(root), analyzers, totalScore, @@ -264234,11 +264421,22 @@ function combineWorkspace(root, info, scans, options) { } async function scanWorkspace(root, rootConfig, info, options = {}) { const { packageThreshold, ...scanOptions } = options; + const packages = info.packages.map((workspacePackage) => ({ + ...workspacePackage, + config: packageConfig(root, workspacePackage.path, rootConfig) + })); + const audits = (config) => config.analyzers.dependencies && (options.audit === void 0 ? config.audit !== false : options.audit === true); + await prefetchAudits( + packages.filter(({ config }) => audits(config)).map(({ path: path2, config }) => ({ + projectPath: (0, import_node_path16.join)(root, path2), + production: config.audit === "production" + })) + ); const scans = []; - for (const workspacePackage of info.packages) { - const config = packageConfig(root, workspacePackage.path, rootConfig); - const result = await scan((0, import_node_path15.join)(root, workspacePackage.path), config, { + for (const { config, ...workspacePackage } of packages) { + const result = await scan((0, import_node_path16.join)(root, workspacePackage.path), config, { ...scanOptions, + ...audits(config) ? { audit: "cached" } : {}, scope: packageScope(options.scope, workspacePackage.path), fingerprintNamespace: workspacePackage.path, label: workspacePackage.path @@ -264258,8 +264456,8 @@ async function scanWorkspace(root, rootConfig, info, options = {}) { } // src/core/workspaces.ts -var import_node_fs15 = require("fs"); -var import_node_path16 = require("path"); +var import_node_fs16 = require("fs"); +var import_node_path17 = require("path"); var import_yaml2 = __toESM(require_dist3(), 1); var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([ "node_modules", @@ -264270,12 +264468,12 @@ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([ ]); var MAX_DEPTH = 5; var DEFAULT_LERNA_PACKAGES = ["packages/*"]; -function isRecord6(value) { +function isRecord7(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function readJson(path2) { try { - return JSON.parse((0, import_node_fs15.readFileSync)(path2, "utf-8")); + return JSON.parse((0, import_node_fs16.readFileSync)(path2, "utf-8")); } catch { return void 0; } @@ -264284,25 +264482,25 @@ function strings(value) { return Array.isArray(value) ? value.filter((item) => typeof item === "string") : []; } function packageJsonPatterns(manifest) { - if (!isRecord6(manifest)) return []; + if (!isRecord7(manifest)) return []; const { workspaces } = manifest; - return isRecord6(workspaces) ? strings(workspaces.packages) : strings(workspaces); + return isRecord7(workspaces) ? strings(workspaces.packages) : strings(workspaces); } function pnpmPatterns(root) { - const path2 = (0, import_node_path16.join)(root, "pnpm-workspace.yaml"); - if (!(0, import_node_fs15.existsSync)(path2)) return []; + const path2 = (0, import_node_path17.join)(root, "pnpm-workspace.yaml"); + if (!(0, import_node_fs16.existsSync)(path2)) return []; try { - const document = (0, import_yaml2.parse)((0, import_node_fs15.readFileSync)(path2, "utf-8")); - return isRecord6(document) ? strings(document.packages) : []; + const document = (0, import_yaml2.parse)((0, import_node_fs16.readFileSync)(path2, "utf-8")); + return isRecord7(document) ? strings(document.packages) : []; } catch { return []; } } function lernaPatterns(root) { - const path2 = (0, import_node_path16.join)(root, "lerna.json"); - if (!(0, import_node_fs15.existsSync)(path2)) return null; + const path2 = (0, import_node_path17.join)(root, "lerna.json"); + if (!(0, import_node_fs16.existsSync)(path2)) return null; const document = readJson(path2); - const patterns = isRecord6(document) ? strings(document.packages) : []; + const patterns = isRecord7(document) ? strings(document.packages) : []; return patterns.length > 0 ? patterns : DEFAULT_LERNA_PACKAGES; } function candidateDirectories(root, markers) { @@ -264311,7 +264509,7 @@ function candidateDirectories(root, markers) { if (depth > MAX_DEPTH) return; let entries; try { - entries = (0, import_node_fs15.readdirSync)(directory, { withFileTypes: true }); + entries = (0, import_node_fs16.readdirSync)(directory, { withFileTypes: true }); } catch { return; } @@ -264324,7 +264522,7 @@ function candidateDirectories(root, markers) { continue; } walk( - (0, import_node_path16.join)(directory, entry.name), + (0, import_node_path17.join)(directory, entry.name), relative8 ? `${relative8}/${entry.name}` : entry.name, depth + 1 ); @@ -264343,23 +264541,23 @@ function matchPatterns(directories2, patterns) { } function packageName(root, path2) { for (const file of ["package.json", "project.json"]) { - const document = readJson((0, import_node_path16.join)(root, path2, file)); - if (isRecord6(document) && typeof document.name === "string") { + const document = readJson((0, import_node_path17.join)(root, path2, file)); + if (isRecord7(document) && typeof document.name === "string") { return document.name; } } return void 0; } function packageManagerTool(root, manifest) { - const declared = isRecord6(manifest) && typeof manifest.packageManager === "string" ? manifest.packageManager.split("@")[0] : void 0; + const declared = isRecord7(manifest) && typeof manifest.packageManager === "string" ? manifest.packageManager.split("@")[0] : void 0; if (declared === "pnpm" || declared === "yarn") return declared; - if ((0, import_node_fs15.existsSync)((0, import_node_path16.join)(root, "pnpm-lock.yaml"))) return "pnpm"; - if ((0, import_node_fs15.existsSync)((0, import_node_path16.join)(root, "yarn.lock"))) return "yarn"; + if ((0, import_node_fs16.existsSync)((0, import_node_path17.join)(root, "pnpm-lock.yaml"))) return "pnpm"; + if ((0, import_node_fs16.existsSync)((0, import_node_path17.join)(root, "yarn.lock"))) return "yarn"; return "npm"; } function detectWorkspaces(root) { - const manifest = readJson((0, import_node_path16.join)(root, "package.json")); - const isNx = (0, import_node_fs15.existsSync)((0, import_node_path16.join)(root, "nx.json")); + const manifest = readJson((0, import_node_path17.join)(root, "package.json")); + const isNx = (0, import_node_fs16.existsSync)((0, import_node_path17.join)(root, "nx.json")); const pnpm = pnpmPatterns(root); const lerna = lernaPatterns(root); const patterns = [ @@ -264379,7 +264577,7 @@ function detectWorkspaces(root) { return null; } if (paths.length === 0) return null; - const tool = isNx ? "nx" : (0, import_node_fs15.existsSync)((0, import_node_path16.join)(root, "turbo.json")) ? "turbo" : lerna ? "lerna" : pnpm.length > 0 ? "pnpm" : packageManagerTool(root, manifest); + const tool = isNx ? "nx" : (0, import_node_fs16.existsSync)((0, import_node_path17.join)(root, "turbo.json")) ? "turbo" : lerna ? "lerna" : pnpm.length > 0 ? "pnpm" : packageManagerTool(root, manifest); return { tool, packages: paths.map((path2) => { @@ -264390,10 +264588,10 @@ function detectWorkspaces(root) { } // src/core/github-pr.ts -var import_node_fs16 = require("fs"); +var import_node_fs17 = require("fs"); // src/reporters/pr-comment.ts -var import_node_path17 = require("path"); +var import_node_path18 = require("path"); var PR_COMMENT_MARKER = ""; var SCORE_MARKER = //; var MAX_LISTED_FINDINGS = 25; @@ -264453,7 +264651,7 @@ function renderPrComment(input) { const inChangedFiles = []; let elsewhere = 0; for (const issue of actionable) { - const path2 = issue.file ? import_node_path17.posix.join(projectDirectory, issue.file) : void 0; + const path2 = issue.file ? import_node_path18.posix.join(projectDirectory, issue.file) : void 0; if (path2 && changedFiles2.has(path2)) inChangedFiles.push({ issue, path: path2 }); else elsewhere += 1; } @@ -264500,7 +264698,7 @@ var MAX_PAGES = 30; function pullRequestContext(env2, token) { if (!PULL_REQUEST_EVENTS.has(env2.GITHUB_EVENT_NAME ?? "")) return null; if (!env2.GITHUB_EVENT_PATH || !env2.GITHUB_REPOSITORY) return null; - const event = JSON.parse((0, import_node_fs16.readFileSync)(env2.GITHUB_EVENT_PATH, "utf8")); + const event = JSON.parse((0, import_node_fs17.readFileSync)(env2.GITHUB_EVENT_PATH, "utf8")); const number = event.pull_request?.number; if (typeof number !== "number") return null; return { @@ -264584,7 +264782,7 @@ async function upsertComment(context, render, fetchImpl = fetch) { } // src/reporters/sarif.ts -var import_node_path18 = require("path"); +var import_node_path19 = require("path"); var import_node_url = require("url"); // package.json @@ -264618,7 +264816,7 @@ function sarifLevel(severity) { } } function artifactUri(file) { - if ((0, import_node_path18.isAbsolute)(file)) { + if ((0, import_node_path19.isAbsolute)(file)) { return (0, import_node_url.pathToFileURL)(file).href; } return file.replaceAll("\\", "/").split("/").map((segment) => encodeURIComponent(segment)).join("/"); @@ -264768,7 +264966,7 @@ function markdownEscape(value) { function writeOutput(name, value) { const outputFile = process.env.GITHUB_OUTPUT; if (outputFile) { - (0, import_node_fs17.appendFileSync)(outputFile, `${name}=${value} + (0, import_node_fs18.appendFileSync)(outputFile, `${name}=${value} `, "utf8"); } } @@ -264886,7 +265084,7 @@ async function commentOnPullRequest(result, threshold, failOn, workspace, projec return; } const files = await changedFiles(context); - const projectDirectory = (0, import_node_path19.relative)(workspace, projectPath).split(import_node_path19.sep).join("/"); + const projectDirectory = (0, import_node_path20.relative)(workspace, projectPath).split(import_node_path20.sep).join("/"); const outcome = await upsertComment( context, (previousBody) => renderPrComment({ @@ -264908,7 +265106,7 @@ async function commentOnPullRequest(result, threshold, failOn, workspace, projec } } function resolveWorkspacePath(workspace, value) { - return (0, import_node_path19.isAbsolute)(value) ? (0, import_node_path19.resolve)(value) : (0, import_node_path19.resolve)(workspace, value); + return (0, import_node_path20.isAbsolute)(value) ? (0, import_node_path20.resolve)(value) : (0, import_node_path20.resolve)(workspace, value); } function validatePathInput(name, value) { if (/[\r\n]/.test(value)) { @@ -264918,7 +265116,7 @@ function validatePathInput(name, value) { } async function runAction() { try { - const workspace = (0, import_node_path19.resolve)(process.env.GITHUB_WORKSPACE || process.cwd()); + const workspace = (0, import_node_path20.resolve)(process.env.GITHUB_WORKSPACE || process.cwd()); const projectPath = resolveWorkspacePath( workspace, validatePathInput("path", getInput("path", ".")) @@ -264974,18 +265172,18 @@ async function runAction() { ); } const result = previous ? compareWithPrevious(scanned, previous) : scanned; - (0, import_node_fs17.mkdirSync)((0, import_node_path19.dirname)(reportPath), { recursive: true }); - (0, import_node_fs17.writeFileSync)(reportPath, `${JSON.stringify(result, null, 2)} + (0, import_node_fs18.mkdirSync)((0, import_node_path20.dirname)(reportPath), { recursive: true }); + (0, import_node_fs18.writeFileSync)(reportPath, `${JSON.stringify(result, null, 2)} `, "utf8"); - (0, import_node_fs17.mkdirSync)((0, import_node_path19.dirname)(sarifPath), { recursive: true }); - (0, import_node_fs17.writeFileSync)(sarifPath, renderSarif(result), "utf8"); + (0, import_node_fs18.mkdirSync)((0, import_node_path20.dirname)(sarifPath), { recursive: true }); + (0, import_node_fs18.writeFileSync)(sarifPath, renderSarif(result), "utf8"); writeOutput("score", String(result.totalScore)); writeOutput("grade", result.grade); writeOutput("report", reportPath); writeOutput("sarif", sarifPath); emitAnnotations(result); if (process.env.GITHUB_STEP_SUMMARY) { - (0, import_node_fs17.appendFileSync)( + (0, import_node_fs18.appendFileSync)( process.env.GITHUB_STEP_SUMMARY, renderSummary(result, threshold, failOn), "utf8" diff --git a/src/analyzers/audit-cache.ts b/src/analyzers/audit-cache.ts new file mode 100644 index 0000000..1ef8088 --- /dev/null +++ b/src/analyzers/audit-cache.ts @@ -0,0 +1,144 @@ +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import type { AuditAdvisory } from "./audit-advisories.js"; + +/** + * Successful dependency audits kept on disk for an hour, so repeated scans + * and editor sessions reuse them without the network. Entries live in the + * project's node_modules/.cache, are keyed by the audit command and the + * exact lock file and package.json contents, and are validated on read like + * any other file the scan does not control. + */ + +export const AUDIT_CACHE_TTL_MS = 60 * 60 * 1000; +const CACHE_VERSION = 1; +const SEVERITIES = new Set(["critical", "high", "moderate", "low"]); + +export interface AuditCounts { + critical: number; + high: number; + moderate: number; + low: number; +} + +export interface PersistedAudit { + summary: AuditCounts; + advisories: AuditAdvisory[]; + /** When the audit ran, in milliseconds since the epoch. */ + createdAt: number; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function isCount(value: unknown): value is number { + return Number.isInteger(value) && (value as number) >= 0; +} + +/** The cache file for an audit, or null when the project has no node_modules. */ +export function auditCacheFile( + lockDirectory: string, + key: string, +): string | null { + const modules = join(lockDirectory, "node_modules"); + if (!existsSync(modules)) return null; + const name = createHash("sha256").update(key).digest("hex"); + return join(modules, ".cache", "codediag", "audit", `${name}.json`); +} + +function readSummary(value: unknown): AuditCounts | null { + if (!isRecord(value)) return null; + const { critical, high, moderate, low } = value; + return isCount(critical) && isCount(high) && isCount(moderate) && isCount(low) + ? { critical, high, moderate, low } + : null; +} + +function readAdvisories(value: unknown): AuditAdvisory[] | null { + if (!Array.isArray(value)) return null; + const advisories: AuditAdvisory[] = []; + for (const entry of value) { + if ( + !isRecord(entry) || + typeof entry.name !== "string" || + !SEVERITIES.has(String(entry.severity)) || + (entry.title !== undefined && typeof entry.title !== "string") || + (entry.url !== undefined && typeof entry.url !== "string") + ) { + return null; + } + advisories.push({ + name: entry.name, + severity: entry.severity as AuditAdvisory["severity"], + ...(typeof entry.title === "string" ? { title: entry.title } : {}), + ...(typeof entry.url === "string" ? { url: entry.url } : {}), + }); + } + return advisories; +} + +/** A cached audit younger than an hour, or null when absent, stale, or malformed. */ +export function readPersistedAudit( + file: string, + now = Date.now(), +): PersistedAudit | null { + let value: unknown; + try { + value = JSON.parse(readFileSync(file, "utf-8")); + } catch { + return null; + } + if ( + !isRecord(value) || + value.version !== CACHE_VERSION || + typeof value.createdAt !== "string" + ) { + return null; + } + const createdAt = Date.parse(value.createdAt); + if ( + !Number.isFinite(createdAt) || + createdAt > now || + now - createdAt >= AUDIT_CACHE_TTL_MS + ) { + return null; + } + const summary = readSummary(value.summary); + const advisories = readAdvisories(value.advisories); + return summary && advisories ? { summary, advisories, createdAt } : null; +} + +/** + * Records a successful audit. A cache that cannot be written, such as a + * read-only node_modules, only means the next scan audits again. + */ +export function persistAudit( + file: string, + audit: Omit, + now = Date.now(), +): void { + try { + mkdirSync(dirname(file), { recursive: true }); + writeFileSync( + file, + `${JSON.stringify({ + version: CACHE_VERSION, + createdAt: new Date(now).toISOString(), + summary: audit.summary, + advisories: audit.advisories, + })}\n`, + "utf-8", + ); + } catch { + // Best effort by design; see above. + } +} + +/** How long ago a cached audit ran, for the analyzer summary. */ +export function auditAge(createdAt: number, now = Date.now()): string { + const minutes = Math.floor((now - createdAt) / 60_000); + if (minutes < 1) return "less than a minute ago"; + return `${minutes} minute${minutes === 1 ? "" : "s"} ago`; +} diff --git a/src/analyzers/dependencies.ts b/src/analyzers/dependencies.ts index 001941f..9944568 100644 --- a/src/analyzers/dependencies.ts +++ b/src/analyzers/dependencies.ts @@ -1,7 +1,7 @@ -import { spawnSync } from "node:child_process"; +import { execFile, spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; -import { dirname, join } from "node:path"; +import { dirname, join, resolve } from "node:path"; import type { ScanContext } from "../core/scan-context.js"; import { fromRule } from "../rules/registry.js"; import type { AnalyzerResult, DiagnosticIssue, PackageJson } from "../types.js"; @@ -10,6 +10,12 @@ import { describeAdvisories, parseAuditAdvisories, } from "./audit-advisories.js"; +import { + auditAge, + auditCacheFile, + persistAudit, + readPersistedAudit, +} from "./audit-cache.js"; interface AuditSummary { critical: number; @@ -235,11 +241,19 @@ export interface AuditRun { } export type AuditRunner = (command: AuditCommand, cwd: string) => AuditRun; +export type AsyncAuditRunner = ( + command: AuditCommand, + cwd: string, +) => Promise; + +const AUDIT_TIMEOUT_MS = 30_000; +/** Workspace audits that run at once, so a large monorepo does not flood the registry. */ +const DEFAULT_AUDIT_CONCURRENCY = 4; const runAuditProcess: AuditRunner = (command, cwd) => { const result = spawnSync(command.command, command.args, { cwd, - timeout: 30000, + timeout: AUDIT_TIMEOUT_MS, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32", @@ -251,8 +265,42 @@ const runAuditProcess: AuditRunner = (command, cwd) => { }; }; +const runAuditProcessAsync: AsyncAuditRunner = (command, cwd) => + new Promise((resolve) => { + execFile( + command.command, + command.args, + { + cwd, + timeout: AUDIT_TIMEOUT_MS, + encoding: "utf-8", + maxBuffer: 64 * 1024 * 1024, + shell: process.platform === "win32", + windowsHide: true, + }, + (error, stdout, stderr) => { + // Audits exit non-zero when they find vulnerabilities; only a + // process that could not start or finish is a failure, as with + // spawnSync. + const failed = + error !== null && (error.killed || typeof error.code !== "number"); + resolve({ + stdout: stdout ?? "", + stderr: stderr ?? "", + ...(failed ? { error } : {}), + }); + }, + ); + }); + type AuditOutcome = - | { kind: "summary"; summary: AuditSummary; advisories?: AuditAdvisory[] } + | { + kind: "summary"; + summary: AuditSummary; + advisories?: AuditAdvisory[]; + /** Set when the result comes from the on-disk cache: when it ran. */ + cachedAt?: number; + } | { kind: "error"; message: string }; /** @@ -291,12 +339,30 @@ function auditCacheKey( ].join("\u0000"); } -function runAudit( - projectPath: string, - command: AuditCommand, - runner: AuditRunner, -): AuditOutcome { - const run = runner(command, projectPath); +/** A successful audit from the on-disk cache, if one is fresh. */ +function persistedOutcome(file: string | null): AuditOutcome | undefined { + const persisted = file ? readPersistedAudit(file) : null; + return persisted + ? { + kind: "summary", + summary: persisted.summary, + advisories: persisted.advisories, + cachedAt: persisted.createdAt, + } + : undefined; +} + +/** Keeps a successful audit for later scans; failures are retried instead. */ +function persistOutcome(file: string | null, outcome: AuditOutcome): void { + if (file && outcome.kind === "summary") { + persistAudit(file, { + summary: outcome.summary, + advisories: outcome.advisories ?? [], + }); + } +} + +function auditOutcome(run: AuditRun, command: AuditCommand): AuditOutcome { try { if (run.error) throw run.error; if (!run.stdout.trim()) { @@ -317,6 +383,98 @@ function runAudit( } } +function runAudit( + projectPath: string, + command: AuditCommand, + runner: AuditRunner, + cacheFile: string | null, +): AuditOutcome { + const outcome = auditOutcome(runner(command, projectPath), command); + persistOutcome(cacheFile, outcome); + return outcome; +} + +export interface AuditTarget { + projectPath: string; + /** Audit only production dependencies, as `audit: production` does. */ + production: boolean; +} + +interface AuditJob { + projectPath: string; + command: AuditCommand; + key: string; + cacheFile: string | null; +} + +function auditJob(target: AuditTarget): AuditJob | null { + const projectPath = resolve(target.projectPath); + let pkg: PackageJson; + try { + pkg = JSON.parse(readFileSync(join(projectPath, "package.json"), "utf-8")); + } catch { + // The scan reports a missing or invalid package.json itself. + return null; + } + const lockFile = findLockFile( + projectPath, + declaredManager(pkg.packageManager), + ); + if (!lockFile) return null; + const command = resolveAuditCommand( + projectPath, + pkg.packageManager, + target.production, + ); + const key = auditCacheKey(projectPath, command, lockFile); + return { + projectPath, + command, + key, + cacheFile: auditCacheFile(lockFile.directory, key), + }; +} + +/** + * Runs the audits of several projects concurrently, such as the packages of a + * workspace, and keeps the outcomes for scans in this process that use + * `audit: "cached"`. The audits are what a scan would run; only the waiting + * overlaps. + */ +export async function prefetchAudits( + targets: AuditTarget[], + options: { runner?: AsyncAuditRunner; concurrency?: number } = {}, +): Promise { + const runner = options.runner ?? runAuditProcessAsync; + const jobs = [ + ...new Map( + targets.flatMap((target) => { + const job = auditJob(target); + return job ? [[job.key, job] as const] : []; + }), + ).values(), + ]; + let next = 0; + const work = async (): Promise => { + for (let job = jobs[next++]; job; job = jobs[next++]) { + let outcome = persistedOutcome(job.cacheFile); + if (!outcome) { + outcome = auditOutcome( + await runner(job.command, job.projectPath), + job.command, + ); + persistOutcome(job.cacheFile, outcome); + } + auditCache.set(job.key, outcome); + } + }; + const workers = Math.min( + options.concurrency ?? DEFAULT_AUDIT_CONCURRENCY, + jobs.length, + ); + await Promise.all(Array.from({ length: workers }, work)); +} + /** Records an audit outcome's findings and reports whether it passed. */ function recordAudit( outcome: AuditOutcome, @@ -452,20 +610,29 @@ export async function analyzeDependencies( const cacheKey = lockFile ? auditCacheKey(projectPath, auditCommand, lockFile) : null; - const cached = cacheKey ? auditCache.get(cacheKey) : undefined; + const cacheFile = + lockFile && cacheKey && auditMode !== false + ? auditCacheFile(lockFile.directory, cacheKey) + : null; + const persisted = persistedOutcome(cacheFile); + const cached = (cacheKey ? auditCache.get(cacheKey) : undefined) ?? persisted; + let reusedFrom: number | undefined; // Without a lock file no audit can run in any mode; that is a failed check. if (auditMode === true || (auditMode === "cached" && (cached || !lockFile))) { checksRun++; if (lockFile && cacheKey) { + // A full scan audits again unless an audit of the same lock file ran + // within the hour; editor saves reuse any earlier result. const outcome = - auditMode === "cached" && cached - ? cached - : runAudit( - projectPath, - auditCommand, - options.runAudit ?? runAuditProcess, - ); + (auditMode === "cached" ? cached : persisted) ?? + runAudit( + projectPath, + auditCommand, + options.runAudit ?? runAuditProcess, + cacheFile, + ); auditCache.set(cacheKey, outcome); + if (outcome.kind === "summary") reusedFrom = outcome.cachedAt; if (recordAudit(outcome, auditCommand, issues)) checksPassed++; } } else { @@ -525,6 +692,10 @@ export async function analyzeDependencies( name: "Dependencies", score, issues, - summary: `${Math.round(checksPassed)}/${checksRun} checks passed`, + summary: `${Math.round(checksPassed)}/${checksRun} checks passed${ + reusedFrom === undefined + ? "" + : `; audit reused from ${auditAge(reusedFrom)}` + }`, }; } diff --git a/src/core/workspace-scan.ts b/src/core/workspace-scan.ts index b43311c..22860ae 100644 --- a/src/core/workspace-scan.ts +++ b/src/core/workspace-scan.ts @@ -1,5 +1,6 @@ import { existsSync } from "node:fs"; import { basename, join } from "node:path"; +import { prefetchAudits } from "../analyzers/dependencies.js"; import { loadConfig } from "../config.js"; import { detectStack } from "../detectors/stack-detector.js"; import { calculateGrade, type ScanOptions, scan } from "../scanner.js"; @@ -274,11 +275,31 @@ export async function scanWorkspace( options: WorkspaceScanOptions = {}, ): Promise { const { packageThreshold, ...scanOptions } = options; + const packages = info.packages.map((workspacePackage) => ({ + ...workspacePackage, + config: packageConfig(root, workspacePackage.path, rootConfig), + })); + // Packages that would audit now run their audits concurrently up front, + // and their scans reuse the outcomes instead of waiting one at a time. + const audits = (config: CodediagConfig): boolean => + config.analyzers.dependencies && + (options.audit === undefined + ? config.audit !== false + : options.audit === true); + await prefetchAudits( + packages + .filter(({ config }) => audits(config)) + .map(({ path, config }) => ({ + projectPath: join(root, path), + production: config.audit === "production", + })), + ); + const scans: PackageScan[] = []; - for (const workspacePackage of info.packages) { - const config = packageConfig(root, workspacePackage.path, rootConfig); + for (const { config, ...workspacePackage } of packages) { const result = await scan(join(root, workspacePackage.path), config, { ...scanOptions, + ...(audits(config) ? { audit: "cached" as const } : {}), scope: packageScope(options.scope, workspacePackage.path), fingerprintNamespace: workspacePackage.path, label: workspacePackage.path, diff --git a/tests/dependencies.test.ts b/tests/dependencies.test.ts index 1a338e8..bc69b0a 100644 --- a/tests/dependencies.test.ts +++ b/tests/dependencies.test.ts @@ -1,13 +1,23 @@ import assert from "node:assert/strict"; -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { + existsSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { + type AsyncAuditRunner, type AuditRunner, analyzeDependencies, clearAuditCache, parseAuditSummary, + prefetchAudits, resolveAuditCommand, } from "../src/analyzers/dependencies.js"; import { createScanContext } from "../src/core/scan-context.js"; @@ -289,3 +299,186 @@ test("cached audits reuse an earlier result for the same lock file", async () => rmSync(directory, { recursive: true, force: true }); } }); + +const HIGH_VULNERABILITIES = JSON.stringify({ + metadata: { + vulnerabilities: { critical: 0, high: 2, moderate: 0, low: 0 }, + }, +}); + +function auditProject(directory: string, withModules = true): void { + writeFileSync( + join(directory, "package.json"), + JSON.stringify({ + engines: { node: ">=20" }, + scripts: { build: "x", start: "y" }, + }), + ); + writeFileSync(join(directory, "package-lock.json"), '{"lockfileVersion":3}'); + if (withModules) mkdirSync(join(directory, "node_modules")); +} + +function persistedAudits(directory: string): string[] { + const cache = join(directory, "node_modules", ".cache", "codediag", "audit"); + return existsSync(cache) + ? readdirSync(cache).map((name) => join(cache, name)) + : []; +} + +test("successful audits persist for an hour in node_modules/.cache", async () => { + const directory = mkdtempSync(join(tmpdir(), "codediag-dependencies-")); + let runs = 0; + const counting: AuditRunner = () => { + runs++; + return { stdout: HIGH_VULNERABILITIES, stderr: "" }; + }; + const scanDependencies = (audit?: "cached") => { + // Each scan starts like a new process, with no audit in memory. + clearAuditCache(); + return analyzeDependencies(createScanContext(directory), { + runAudit: counting, + ...(audit ? { audit } : {}), + }); + }; + + try { + auditProject(directory); + const fresh = await scanDependencies(); + assert.deepEqual( + fresh.issues.map((issue) => issue.rule), + ["vuln-high"], + ); + assert.doesNotMatch(fresh.summary, /reused/); + assert.equal(runs, 1); + const [file] = persistedAudits(directory); + assert.ok(file, "the audit result is written to the cache"); + assert.equal( + readFileSync(file, "utf-8").includes("lockfileVersion"), + false, + ); + + const reused = await scanDependencies(); + assert.equal(runs, 1); + assert.deepEqual( + reused.issues.map((issue) => issue.rule), + ["vuln-high"], + ); + assert.match(reused.summary, /; audit reused from less than a minute ago$/); + // Editor saves, which never run the package manager, reuse it as well. + assert.deepEqual( + (await scanDependencies("cached")).issues.map((issue) => issue.rule), + ["vuln-high"], + ); + assert.equal(runs, 1); + + const entry = JSON.parse(readFileSync(file, "utf-8")); + writeFileSync( + file, + JSON.stringify({ + ...entry, + createdAt: new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString(), + }), + ); + await scanDependencies(); + assert.equal(runs, 2, "an entry older than an hour is not used"); + + writeFileSync(persistedAudits(directory)[0] ?? file, "{"); + await scanDependencies(); + assert.equal(runs, 3, "an unreadable entry is replaced"); + + writeFileSync( + persistedAudits(directory)[0] ?? file, + JSON.stringify({ + ...entry, + createdAt: new Date().toISOString(), + summary: { high: "2" }, + }), + ); + await scanDependencies(); + assert.equal(runs, 4, "an entry with the wrong shape is replaced"); + } finally { + clearAuditCache(); + rmSync(directory, { recursive: true, force: true }); + } +}); + +test("failed audits and projects without node_modules are not persisted", async () => { + const directory = mkdtempSync(join(tmpdir(), "codediag-dependencies-")); + let runs = 0; + const failing: AuditRunner = () => { + runs++; + return { stdout: "", stderr: "network unreachable" }; + }; + try { + auditProject(directory); + for (let scan = 0; scan < 2; scan++) { + clearAuditCache(); + const result = await analyzeDependencies(createScanContext(directory), { + runAudit: failing, + }); + assert.deepEqual( + result.issues.map((issue) => issue.rule), + ["audit-unavailable"], + ); + } + assert.equal(runs, 2); + assert.deepEqual(persistedAudits(directory), []); + + rmSync(join(directory, "node_modules"), { recursive: true }); + clearAuditCache(); + await analyzeDependencies(createScanContext(directory), { + runAudit: () => ({ stdout: HIGH_VULNERABILITIES, stderr: "" }), + }); + assert.equal(existsSync(join(directory, "node_modules")), false); + } finally { + clearAuditCache(); + rmSync(directory, { recursive: true, force: true }); + } +}); + +test("workspace audits are prefetched concurrently and reused by cached scans", async () => { + const root = mkdtempSync(join(tmpdir(), "codediag-dependencies-")); + const packages = ["a", "b", "c"].map((name) => join(root, name)); + let running = 0; + let mostAtOnce = 0; + const started: string[] = []; + const slow: AsyncAuditRunner = async (_command, cwd) => { + started.push(cwd); + running++; + mostAtOnce = Math.max(mostAtOnce, running); + await new Promise((resolve) => setTimeout(resolve, 20)); + running--; + return { stdout: HIGH_VULNERABILITIES, stderr: "" }; + }; + const unreachable: AuditRunner = () => { + throw new Error("prefetched scans must not run the package manager"); + }; + + try { + clearAuditCache(); + for (const directory of packages) { + mkdirSync(directory); + auditProject(directory, false); + } + await prefetchAudits( + packages.map((projectPath) => ({ projectPath, production: false })), + { runner: slow, concurrency: 2 }, + ); + assert.equal(started.length, 3); + assert.equal(mostAtOnce, 2); + + for (const directory of packages) { + const result = await analyzeDependencies(createScanContext(directory), { + audit: "cached", + runAudit: unreachable, + }); + assert.deepEqual( + result.issues.map((issue) => issue.rule), + ["vuln-high"], + ); + } + } finally { + clearAuditCache(); + rmSync(root, { recursive: true, force: true }); + } +}); From fc6bc9ed23fa81c10755eca1ff0de8dd72fd409b Mon Sep 17 00:00:00 2001 From: sabahattink Date: Fri, 30 Oct 2026 17:11:14 +0400 Subject: [PATCH 4/4] chore(release): CodeDiag 0.9.0 "Perimeter" --- CHANGELOG.md | 39 ++++++++++++++++++++++++++++++++++ README.md | 23 ++++++++++++++------ dist/action.cjs | 2 +- docs/github-action.md | 2 +- docs/index.html | 18 ++++++---------- docs/roadmap.md | 20 +++++++++-------- docs/vscode-extension.md | 4 ++-- extensions/vscode/CHANGELOG.md | 7 ++++++ extensions/vscode/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 11 files changed, 89 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fbf397..48659ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,45 @@ All notable changes to CodeDiag are documented in this file. +## [0.9.0] - 2026-10-30 + +The "Perimeter" release: tRPC apps that authenticate in front of their +procedures stop getting missing-auth warnings, dependency audits are cached +and run concurrently across monorepo packages, and Dependabot updates keep +the Action bundle in step. + +### Added + +- tRPC mutations are no longer reported as missing auth middleware when the + whole router is authenticated before procedures run: a `createContext` (or + a project function it calls) that throws `TRPCError` `UNAUTHORIZED` or + `FORBIDDEN`, or throws when the session is missing; auth middleware ahead + of the Express mount that serves the adapter; or a Next.js `middleware.ts` + (`proxy.ts` in Next.js 16) that rejects anonymous requests + (`next-auth/middleware`, Clerk's `auth.protect()` under a route matcher, or + a redirect or 401/403 when the session is missing) and whose `matcher` + covers the tRPC route. A `createContext` that only reads the session, as + T3's does, and the common `/((?!api|...).*)` matcher that skips the API do + not count. The API Health summary names where the perimeter was found. +- Successful dependency audits are kept for an hour in + `node_modules/.cache/codediag`, keyed by the audit command and the exact + lock file and `package.json` contents, and reused by later scans and + editor saves. Entries are validated on read, failed audits are never kept, + nothing is written without a `node_modules` directory, and the Dependencies + summary says when a result was reused. +- Dependabot pull requests that change the Action bundle get it rebuilt: a + read-only job builds it, and a job that runs no repository or dependency + code commits the two bundle files and starts CI for the new commit. + Dependabot groups minor and patch npm updates into one pull request. + +### Changed + +- `--workspaces` scans run every package's audit up front, four at a time, + and the package scans reuse those outcomes, so audit waits overlap instead + of adding up. +- VS Code extension 0.7.0: scans on save also reuse an audit cached on disk + within the hour. + ## [0.8.0] - 2026-10-30 The "Depth" release: tRPC procedures, request data followed through fields, diff --git a/README.md b/README.md index 70899c7..d3f102e 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,13 @@ AST analysis (ts-morph, not regex) of the routes your app actually exposes. routers across files (including `~/` path aliases); mutations without auth middleware (judged by what the middleware checks, not its name), `.input()` parsers that accept anything, and error formatters or `isDev: true` that - expose stack traces. The checks run inside whichever server hosts tRPC - (T3/Next.js, Express, Fastify), and a standalone tRPC server is its own - stack. + expose stack traces. Auth in front of every procedure counts too: a + `createContext` that rejects anonymous callers, auth middleware ahead of + the Express mount, or a Next.js `middleware.ts` (`proxy.ts` in Next.js 16) + whose matcher covers the tRPC route. The checks run inside whichever server + hosts tRPC (T3/Next.js, Express, Fastify), and a standalone tRPC server is + its own stack. An intentionally public mutation, such as sign-up, is + accepted with a reasoned `codediag-ignore-next-line` directive. ### Security @@ -119,7 +123,10 @@ Runs the matching npm, pnpm, Yarn Classic, or Yarn Berry audit (skipped with names the affected packages and their advisories in each vulnerability finding, checks the lock file and `packageManager` consistency, flags deprecated packages, and validates engine specs and essential scripts. -Monorepo lock files are found up to three parent directories. +Monorepo lock files are found up to three parent directories. A successful +audit is kept for an hour in `node_modules/.cache/codediag`, keyed by the lock +file and `package.json`, so repeated scans reuse it (the Dependencies summary +says when); delete that directory to force a fresh audit. ### Testing @@ -237,7 +244,8 @@ workspaces). Each package gets its own stack detection and score, using its own `.codediag.yml` if it has one and otherwise the root configuration, with root `ignore` paths rebased into the package. The combined report lists every package and averages each analyzer over the packages it ran in; the total -uses the usual analyzer weights. Findings are located relative to the root, +uses the usual analyzer weights. Package audits run four at a time before +the packages are analyzed. Findings are located relative to the root, and project-level findings point at the package's `package.json`, so baselines, `--compare`, `--changed-since`, SARIF, and the GitHub Action (`workspaces: true`) work across the whole repository. @@ -485,8 +493,11 @@ results in GitHub Code Scanning. callbacks, and injected providers; `@fastify/autoload` projects; secrets removed from the tree but kept in Git history; per-package thresholds and `--fail-on` gates; remediation quick fixes in VS Code. +- **0.9 "Perimeter":** tRPC auth in `createContext`, Express mounts, and + Next.js middleware; audits cached for an hour and run concurrently across + workspace packages; Dependabot updates that rebuild the Action bundle. - **Next:** see [docs/roadmap.md](docs/roadmap.md) for planned framework - support, editor improvements, and faster monorepo scans. + support, editor improvements, and deeper taint tracking. ## Contributing diff --git a/dist/action.cjs b/dist/action.cjs index 260669a..494e00d 100644 --- a/dist/action.cjs +++ b/dist/action.cjs @@ -264786,7 +264786,7 @@ var import_node_path19 = require("path"); var import_node_url = require("url"); // package.json -var version = "0.8.0"; +var version = "0.9.0"; // src/version.ts function getPackageVersion() { diff --git a/docs/github-action.md b/docs/github-action.md index 0ce624f..74bb934 100644 --- a/docs/github-action.md +++ b/docs/github-action.md @@ -33,7 +33,7 @@ jobs: ``` `@v0` follows the latest 0.x release. Pin an exact release such as -`@v0.8.0`, or a full commit SHA, when a workflow requires immutable +`@v0.9.0`, or a full commit SHA, when a workflow requires immutable third-party dependencies. ## Inputs diff --git a/docs/index.html b/docs/index.html index 8026e9b..e8f9aa8 100644 --- a/docs/index.html +++ b/docs/index.html @@ -371,7 +371,7 @@
  • Analyzers
  • Scoring
  • CI
  • -
  • New in 0.8
  • +
  • New in 0.9
  • @@ -722,18 +722,14 @@

    The same engine, inside VS Code

    -

    New in 0.8

    -

    tRPC, fastify-cli projects, secrets that history still holds, gates that a good average cannot hide, and fixes you apply from the editor.

    +

    New in 0.9

    +

    tRPC apps that authenticate in front of their procedures, and audits that stop making you wait twice.

      -
    • tRPC procedures

      Builders followed from initTRPC through .use() chains and merged routers across files: mutations whose middleware never rejects anonymous callers, .input() parsers that accept anything, and leaked stack traces. Procedure input is request data for the security rules.

    • -
    • Fastify autoload

      @fastify/autoload directories as they load: directory prefixes, autoPrefix, and autohooks that protect a directory’s routes.

    • -
    • Secrets in Git history

      Credentials that a commit added and a later one deleted, which every clone still holds. Compared by digest, never printed.

      --secret-history
    • -
    • Severity gate

      Fail on any active critical finding, whatever the score; suppressed and baselined findings do not count.

      --fail-on critical
    • -
    • Per-package thresholds

      Every monorepo package must reach its own score, so one weak package cannot hide in the average.

      --package-threshold 70
    • -
    • Editor remediations

      VS Code offers the rule’s own fix where it is mechanical: TLS verification on, JWT expiry checked, "none" out of the algorithm list.

    • -
    • Deeper taint

      Request data stored on this, passed to a constructor, or handed to forEach/map callbacks is followed to the sink.

    • -
    • Earlier in 0.7

      Cross-file taint, --workspaces monorepos, Hono, score history, base-branch deltas in pull requests, and audit advisory details.

    • +
    • tRPC perimeters

      Auth in createContext, ahead of the Express mount, or in a Next.js middleware.ts whose matcher covers the tRPC route protects every procedure. A matcher that skips /api does not.

    • +
    • Cached audits

      A successful audit is kept for an hour in node_modules/.cache, keyed by the lock file, so a second scan skips the network and says so in its summary.

    • +
    • Concurrent monorepo audits

      Every package’s audit starts up front, four at a time, so the waits overlap instead of adding up.

      --workspaces
    • +
    • Earlier in 0.8

      tRPC procedures, taint through fields and callbacks, Fastify autoload, secrets in Git history, --fail-on and per-package gates, and editor remediations.

    diff --git a/docs/roadmap.md b/docs/roadmap.md index d1287a1..5f25a05 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -8,7 +8,9 @@ across files and repositories (cross-file taint, monorepo workspaces, Hono, score history, base-branch deltas, and audit details); and 0.8 went deeper: tRPC, taint through fields, constructors, callbacks, and injected providers, `@fastify/autoload`, secrets kept in Git history, per-package and severity -gates, and remediation quick fixes. These are the next candidates, roughly in +gates, and remediation quick fixes; 0.9 recognized tRPC auth in front of the +procedures and made audits cached and concurrent. These are the next +candidates, roughly in priority order. Each one should ship with realistic and defect fixtures in `tests/fixtures/` and keep the benchmark tests green. @@ -19,13 +21,13 @@ priority order. Each one should ship with realistic and defect fixtures in from outside the class, `super()` calls, `Object.assign(this, ...)`, property writes on module objects, and factory providers (`useFactory`, `useValue`). -- **Auth outside tRPC procedures:** recognize authentication applied at the - adapter mount, in a Next.js `middleware.ts`, or by a `createContext` that - rejects anonymous callers, and an opt-out for intentionally public - mutations. +- **tRPC perimeters beyond Express and Next.js:** auth hooks around + `fastifyTRPCPlugin` and Hono's `trpcServer`, and Auth.js v5 middleware + whose `authorized` callback decides access. - **Git history beyond HEAD:** other branches and tags, and merge commits. -- **Persistent audit cache:** reuse dependency audit results across runs, - keyed by the lock file hash, so repeated and workspace scans stay fast. +- **One audit per lock file:** workspace packages that share a root lock + file still each run an audit (now concurrently and cached); one audit + reported once on the root would be faster and avoid repeated findings. ## Frameworks @@ -34,8 +36,8 @@ priority order. Each one should ship with realistic and defect fixtures in ## Monorepos and pull requests -- **Parallel workspace scans:** scan workspace packages concurrently and - share one dependency audit per lock file. +- **Parallel workspace analysis:** package audits already run + concurrently; the analysis itself still runs one package at a time. ## Editor diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index 53573ec..cae1249 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -77,8 +77,8 @@ not apply them. Each problem's rule ID also links to the rule reference. `codediag.scanOnSave` is off by default. Enabling it starts a debounced scan after saves. Scans started by saving a file never run the package manager: they reuse the dependency audit from the last command-triggered scan of the -same lock file, so scores stay consistent, and report `audit-skipped` until -one has run. Commands run from the Command Palette follow the project's +same lock file, or an audit of it cached on disk within the hour, so scores +stay consistent, and report `audit-skipped` until one has run. Commands run from the Command Palette follow the project's `audit` setting. ## Monorepos diff --git a/extensions/vscode/CHANGELOG.md b/extensions/vscode/CHANGELOG.md index 0dffafa..ee716e3 100644 --- a/extensions/vscode/CHANGELOG.md +++ b/extensions/vscode/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.7.0 + +- Run the CodeDiag 0.9 engine: tRPC auth in `createContext`, Express mounts, + and Next.js middleware no longer produces missing-auth problems. +- Scans on save also reuse a dependency audit of the same lock file cached on + disk within the last hour, so they keep their audit after VS Code restarts. + ## 0.6.0 - Remediation quick fixes, offered as the preferred fix where a rule has one diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index d842f84..2bd9c74 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -2,7 +2,7 @@ "name": "codediag-vscode", "displayName": "CodeDiag", "description": "Project health diagnostics for Node.js, NestJS, Express, Fastify, Hono, tRPC, and Next.js workspaces.", - "version": "0.6.0", + "version": "0.7.0", "publisher": "sabahattink", "license": "MIT", "engines": { diff --git a/package-lock.json b/package-lock.json index f86210c..d84f7e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sabahattink/codediag", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@sabahattink/codediag", - "version": "0.8.0", + "version": "0.9.0", "license": "MIT", "dependencies": { "chalk": "^5.3.0", diff --git a/package.json b/package.json index 0d8a073..50e72a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sabahattink/codediag", - "version": "0.8.0", + "version": "0.9.0", "description": "Automated project health scanner for Node.js, NestJS, Express, Fastify, Hono, tRPC, and Next.js projects.", "author": "Sabahattin Kalkan (https://sabahattinkalkan.com)", "license": "MIT",