diff --git a/scripts/arch-scan/arch.db b/scripts/arch-scan/arch.db index 13eb54c..05cebea 100644 Binary files a/scripts/arch-scan/arch.db and b/scripts/arch-scan/arch.db differ diff --git a/scripts/arch-scan/query.test.ts b/scripts/arch-scan/query.test.ts index 7f8adb4..e14aa20 100644 --- a/scripts/arch-scan/query.test.ts +++ b/scripts/arch-scan/query.test.ts @@ -103,4 +103,17 @@ describe("queryViolations", () => { ); expect(missing).toBeDefined(); }); + + it("should exclude @public Server Actions from violations", () => { + const db = setupTestDb(); + // 插入一个带 @public 标记的 Server Action(豁免权限校验) + db.prepare( + "INSERT INTO symbols (file_id, name, kind, is_exported, is_async, is_server_action, is_public, start_line, end_line) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" + ).run(1, "publicLoginAction", "function", 1, 1, 1, 1, 60, 100); + const violations = queryViolations(db); + const missing = violations.server_actions_without_permission.find( + (v) => v.name === "publicLoginAction" + ); + expect(missing).toBeUndefined(); + }); }); diff --git a/scripts/arch-scan/query.ts b/scripts/arch-scan/query.ts index 11ae3c5..24e925f 100644 --- a/scripts/arch-scan/query.ts +++ b/scripts/arch-scan/query.ts @@ -141,6 +141,7 @@ export function queryViolations(db: Database.Database): Violations { `SELECT s.name, f.path FROM symbols s JOIN files f ON s.file_id = f.id WHERE s.is_server_action = 1 + AND s.is_public = 0 AND NOT EXISTS ( SELECT 1 FROM calls c JOIN symbols cs ON c.callee_id = cs.id diff --git a/scripts/arch-scan/scanner.ts b/scripts/arch-scan/scanner.ts index 8294266..797e74f 100644 --- a/scripts/arch-scan/scanner.ts +++ b/scripts/arch-scan/scanner.ts @@ -185,9 +185,24 @@ export function scanSymbols(db: Database.Database): void { .prepare("SELECT id, path FROM files") .all() as { id: number; path: string }[]; const insertSymbol = db.prepare( - `INSERT OR IGNORE INTO symbols (file_id, name, kind, is_exported, is_async, is_server_action, signature, start_line, end_line) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` + `INSERT OR IGNORE INTO symbols (file_id, name, kind, is_exported, is_async, is_server_action, is_public, signature, start_line, end_line) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ); + const hasPublicTag = (node: Node): boolean => { + try { + // ts-morph JSDoc 仅在 JSDocableNode 上可用 + const anyNode = node as unknown as { + getJsDocs?: () => Array<{ getTags: () => Array<{ getTagName: () => string }> }>; + }; + if (typeof anyNode.getJsDocs !== "function") return false; + const docs = anyNode.getJsDocs(); + return docs.some((doc) => + doc.getTags().some((tag) => tag.getTagName() === "public") + ); + } catch { + return false; + } + }; for (const file of files) { let sourceFile; try { @@ -223,6 +238,7 @@ export function scanSymbols(db: Database.Database): void { const startLine = node.getStartLineNumber(); const endLine = node.getEndLineNumber(); const isServerAction = isServerFile && exported && async; + const isPublic = hasPublicTag(node) ? 1 : 0; insertSymbol.run( file.id, name, @@ -230,6 +246,7 @@ export function scanSymbols(db: Database.Database): void { exported ? 1 : 0, async ? 1 : 0, isServerAction ? 1 : 0, + isPublic, signature, startLine, endLine @@ -250,6 +267,7 @@ export function scanSymbols(db: Database.Database): void { const signature = extractSignature(decl); const startLine = decl.getStartLineNumber(); const endLine = decl.getEndLineNumber(); + const isPublic = hasPublicTag(node) ? 1 : 0; insertSymbol.run( file.id, name, @@ -257,6 +275,7 @@ export function scanSymbols(db: Database.Database): void { exported ? 1 : 0, 0, 0, + isPublic, signature, startLine, endLine diff --git a/scripts/arch-scan/schema.ts b/scripts/arch-scan/schema.ts index 4c1f074..af73bff 100644 --- a/scripts/arch-scan/schema.ts +++ b/scripts/arch-scan/schema.ts @@ -27,6 +27,7 @@ const DDL_STATEMENTS = [ is_exported INTEGER DEFAULT 0, is_async INTEGER DEFAULT 0, is_server_action INTEGER DEFAULT 0, + is_public INTEGER DEFAULT 0, signature TEXT, start_line INTEGER, end_line INTEGER,