ai-saas-guard 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,7 +41,7 @@ It is intentionally evidence-first. Findings include a rule ID, severity, file e
41
41
 
42
42
  This repository is public on GitHub.
43
43
 
44
- The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is available through versioned release tags. If you need stricter supply-chain pinning in CI, pin the GitHub Action to a reviewed commit SHA instead of a mutable tag.
44
+ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is available through versioned release tags. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag for controlled upgrades, or a reviewed commit SHA for stricter supply-chain pinning.
45
45
 
46
46
  | Area | Status |
47
47
  | --- | --- |
@@ -50,8 +50,9 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
50
50
  | Local CLI from source | Available for development |
51
51
  | JSON and SARIF output | Available |
52
52
  | Composite GitHub Action | Available |
53
- | Versioned Action tags | `v0.1.2` |
54
- | npm package | `ai-saas-guard@0.1.2` |
53
+ | Versioned Action tags | `v0.2.0`, `v0` |
54
+ | npm package | `ai-saas-guard@0.2.0` |
55
+ | npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
55
56
 
56
57
  ## Quick Start
57
58
 
@@ -75,6 +76,7 @@ Machine-readable output:
75
76
  ```bash
76
77
  npx ai-saas-guard@latest scan --root /path/to/your-saas --json
77
78
  npx ai-saas-guard@latest scan --root /path/to/your-saas --sarif > ai-saas-guard.sarif
79
+ npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown > ai-saas-guard-pr.md
78
80
  npx ai-saas-guard@latest scan --root /path/to/your-saas --fail-on high
79
81
  ```
80
82
 
@@ -146,24 +148,29 @@ AI-generated PRs often combine unrelated work:
146
148
  - review-first checklist
147
149
  - suggested PR split
148
150
  - required tests or manual verification
151
+ - explicit git-diff diagnostics when a base ref or shallow checkout prevents PR classification
152
+ - PR-focused markdown for GitHub step summaries or PR comments
149
153
 
150
154
  ```bash
151
155
  node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --json
156
+ node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --markdown
152
157
  ```
153
158
 
159
+ If `--base` cannot be resolved, `pr-risk` emits `pr-risk.diff-unavailable` instead of silently reporting a clean or empty diff. In GitHub Actions, use `actions/checkout` with `fetch-depth: 0` when you need merge-base comparison against `origin/main`.
160
+
154
161
  ## Commands
155
162
 
156
163
  | Command | Purpose |
157
164
  | --- | --- |
158
165
  | `scan` | Broad local launch preflight across secrets, Stripe, Supabase, MCP, API routes, and deploy config |
159
- | `pr-risk` | Classify the current git diff or a base branch diff for review priority |
166
+ | `pr-risk` | Classify the current git diff or a base branch diff for review priority; supports JSON, SARIF, and PR-focused markdown |
160
167
  | `check-supabase` | Inspect migrations and policy files for RLS and ownership risks |
161
168
  | `check-stripe` | Inspect webhook handlers and billing lifecycle coverage |
162
169
  | `check-mcp` | Inventory MCP configs and classify side effects |
163
170
 
164
171
  ## GitHub Action
165
172
 
166
- The repo includes a composite Action. Use the latest release tag or pin a reviewed commit SHA for stricter supply-chain control:
173
+ The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.2.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
167
174
 
168
175
  ```yaml
169
176
  name: ai-saas-guard
@@ -181,7 +188,7 @@ jobs:
181
188
  - uses: actions/checkout@v6.0.2
182
189
  with:
183
190
  fetch-depth: 0
184
- - uses: zr9959/ai-saas-guard@v0.1.2
191
+ - uses: zr9959/ai-saas-guard@v0
185
192
  with:
186
193
  command: pr-risk
187
194
  root: ${{ github.workspace }}
@@ -192,7 +199,7 @@ jobs:
192
199
  For SARIF upload:
193
200
 
194
201
  ```yaml
195
- - uses: zr9959/ai-saas-guard@v0.1.2
202
+ - uses: zr9959/ai-saas-guard@v0
196
203
  with:
197
204
  command: scan
198
205
  format: sarif
@@ -202,7 +209,22 @@ For SARIF upload:
202
209
  sarif_file: ai-saas-guard.sarif
203
210
  ```
204
211
 
205
- For maximum reproducibility, replace `v0.1.2` with the full commit SHA from the release notes.
212
+ For PR-readable markdown in the Actions run:
213
+
214
+ ```yaml
215
+ - uses: zr9959/ai-saas-guard@v0
216
+ with:
217
+ command: pr-risk
218
+ root: ${{ github.workspace }}
219
+ base: origin/main
220
+ format: markdown
221
+ output: ai-saas-guard-pr.md
222
+ - run: cat ai-saas-guard-pr.md >> "$GITHUB_STEP_SUMMARY"
223
+ ```
224
+
225
+ Use markdown for reviewer-facing PR triage and SARIF for GitHub code scanning alerts. See [docs/github-action.md](docs/github-action.md) for copy-paste workflows and trade-offs.
226
+
227
+ For maximum reproducibility, replace `v0` with the full commit SHA from the release notes.
206
228
 
207
229
  ## Ignore File
208
230
 
@@ -270,18 +292,16 @@ Open-source core:
270
292
  - local CLI
271
293
  - deterministic scanner rules
272
294
  - vulnerable and safe fixtures
273
- - JSON and SARIF output
295
+ - JSON, SARIF, and PR-focused markdown output
274
296
  - GitHub Action wrapper
275
297
  - rule documentation
276
298
 
277
299
  Near-term priorities:
278
300
 
279
- - npm trusted publishing and provenance
280
- - PR comment summary mode
281
301
  - configurable severity and rule toggles
282
302
  - expanded Supabase RLS fixtures
283
303
  - Stripe webhook replay cookbook
284
- - SARIF upload workflow example
304
+ - launch-readiness checklist content
285
305
 
286
306
  Potential paid layer later:
287
307
 
@@ -301,4 +321,4 @@ Please read [SECURITY.md](SECURITY.md) before reporting vulnerabilities. Do not
301
321
 
302
322
  ## npm Publishing
303
323
 
304
- The package is published as [`ai-saas-guard`](https://www.npmjs.com/package/ai-saas-guard). See [docs/npm-publishing.md](docs/npm-publishing.md) for the GitHub Actions provenance workflow, the first-publish token history, and the trusted-publisher follow-up.
324
+ The package is published as [`ai-saas-guard`](https://www.npmjs.com/package/ai-saas-guard). See [docs/npm-publishing.md](docs/npm-publishing.md) for the GitHub Actions Trusted Publisher workflow, provenance notes, and first-publish token history.
package/action.yml CHANGED
@@ -12,7 +12,7 @@ inputs:
12
12
  required: false
13
13
  default: ${{ github.workspace }}
14
14
  format:
15
- description: "Output format: terminal, json, or sarif."
15
+ description: "Output format: terminal, json, sarif, or markdown."
16
16
  required: false
17
17
  default: terminal
18
18
  fail-on:
@@ -62,7 +62,7 @@ runs:
62
62
  esac
63
63
 
64
64
  case "${INPUT_FORMAT}" in
65
- terminal|json|sarif) ;;
65
+ terminal|json|sarif|markdown) ;;
66
66
  *)
67
67
  echo "Invalid format input: ${INPUT_FORMAT}" >&2
68
68
  exit 2
@@ -83,6 +83,8 @@ runs:
83
83
  args+=("--json")
84
84
  elif [ "${INPUT_FORMAT}" = "sarif" ]; then
85
85
  args+=("--sarif")
86
+ elif [ "${INPUT_FORMAT}" = "markdown" ]; then
87
+ args+=("--markdown")
86
88
  fi
87
89
 
88
90
  if [ "${INPUT_FAIL_ON}" != "none" ]; then
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { resolve } from "node:path";
3
3
  import { checkMcp, checkStripe, checkSupabase, classifyPrRisk, scanRepository } from "./index.js";
4
4
  import { formatJsonReport } from "./report/json.js";
5
+ import { formatMarkdownReport } from "./report/markdown.js";
5
6
  import { formatSarifReport } from "./report/sarif.js";
6
7
  import { formatTerminalReport } from "./report/terminal.js";
7
8
  async function main(argv) {
@@ -57,10 +58,14 @@ function parseArgs(argv) {
57
58
  result.format = "sarif";
58
59
  continue;
59
60
  }
61
+ if (arg === "--markdown") {
62
+ result.format = "markdown";
63
+ continue;
64
+ }
60
65
  if (arg === "--format") {
61
66
  const value = argv[index + 1];
62
- if (value !== "terminal" && value !== "json" && value !== "sarif") {
63
- throw new Error("--format requires terminal, json, or sarif");
67
+ if (value !== "terminal" && value !== "json" && value !== "sarif" && value !== "markdown") {
68
+ throw new Error("--format requires terminal, json, sarif, or markdown");
64
69
  }
65
70
  result.format = value;
66
71
  index += 1;
@@ -108,6 +113,8 @@ function formatReport(report, format) {
108
113
  return formatJsonReport(report);
109
114
  if (format === "sarif")
110
115
  return formatSarifReport(report);
116
+ if (format === "markdown")
117
+ return formatMarkdownReport(report);
111
118
  return `${formatTerminalReport(report)}\n`;
112
119
  }
113
120
  function shouldFail(report, failOn) {
@@ -138,7 +145,7 @@ Usage:
138
145
  ai-saas-guard check-supabase [--root <repo>] [--json|--sarif] [--fail-on <severity>]
139
146
  ai-saas-guard check-stripe [--root <repo>] [--json|--sarif] [--fail-on <severity>]
140
147
  ai-saas-guard check-mcp [--root <repo>] [--json|--sarif] [--fail-on <severity>]
141
- ai-saas-guard pr-risk [--root <repo>] [--base <branch>] [--json|--sarif] [--fail-on <severity>]
148
+ ai-saas-guard pr-risk [--root <repo>] [--base <branch>] [--json|--sarif|--markdown] [--fail-on <severity>]
142
149
 
143
150
  Defaults:
144
151
  - read-only
@@ -146,6 +153,7 @@ Defaults:
146
153
  - no account or login required
147
154
  - terminal output by default, JSON with --json
148
155
  - SARIF output for GitHub code scanning with --sarif
156
+ - PR-focused markdown summary with --markdown
149
157
  `;
150
158
  }
151
159
  main(process.argv.slice(2)).then((code) => {
@@ -0,0 +1,2 @@
1
+ import type { BaseReport } from "../types.js";
2
+ export declare function formatMarkdownReport(report: BaseReport): string;
@@ -0,0 +1,83 @@
1
+ export function formatMarkdownReport(report) {
2
+ if (report.command === "pr-risk")
3
+ return `${formatPrRiskMarkdown(report)}\n`;
4
+ return `${formatGenericMarkdown(report)}\n`;
5
+ }
6
+ function formatPrRiskMarkdown(report) {
7
+ const lines = [];
8
+ lines.push("## ai-saas-guard PR risk summary");
9
+ lines.push("");
10
+ lines.push(summaryLine(report));
11
+ if (report.categories.length > 0) {
12
+ lines.push("");
13
+ lines.push(`**Risk categories:** ${report.categories.map((category) => `\`${category}\``).join(", ")}`);
14
+ }
15
+ lines.push("");
16
+ lines.push("### Review first");
17
+ if (report.topRiskyFiles.length === 0) {
18
+ lines.push("");
19
+ lines.push("No changed trust-boundary files were classified by `pr-risk`.");
20
+ }
21
+ else {
22
+ lines.push("");
23
+ lines.push("| File | Score | Categories | Diff |");
24
+ lines.push("| --- | ---: | --- | ---: |");
25
+ for (const file of report.topRiskyFiles.slice(0, 10)) {
26
+ lines.push(`| \`${escapeMarkdownTableCell(file.path)}\` | ${file.score} | ${file.categories.map((category) => `\`${escapeMarkdownTableCell(category)}\``).join("<br>")} | +${file.added} / -${file.removed} |`);
27
+ }
28
+ }
29
+ lines.push("");
30
+ lines.push("### Required verification");
31
+ appendList(lines, report.requiredTests.length > 0 ? report.requiredTests : report.reviewChecklist);
32
+ lines.push("");
33
+ lines.push("### Suggested PR split");
34
+ appendList(lines, report.suggestedSplit.length > 0 ? report.suggestedSplit : ["No split suggestion from the current diff."]);
35
+ lines.push("");
36
+ lines.push("### Findings");
37
+ appendFindings(lines, report.findings);
38
+ return lines.join("\n");
39
+ }
40
+ function formatGenericMarkdown(report) {
41
+ const lines = [];
42
+ lines.push(`## ai-saas-guard ${report.command}`);
43
+ lines.push("");
44
+ lines.push(summaryLine(report));
45
+ lines.push("");
46
+ lines.push("### Findings");
47
+ appendFindings(lines, report.findings);
48
+ return lines.join("\n");
49
+ }
50
+ function summaryLine(report) {
51
+ return `**Findings:** ${report.summary.total} total | critical ${report.summary.critical} | high ${report.summary.high} | medium ${report.summary.medium} | low ${report.summary.low} | info ${report.summary.info}`;
52
+ }
53
+ function appendList(lines, items) {
54
+ for (const item of items) {
55
+ lines.push(`- ${item}`);
56
+ }
57
+ }
58
+ function appendFindings(lines, findings) {
59
+ if (findings.length === 0) {
60
+ lines.push("");
61
+ lines.push("No heuristic launch-readiness risks found by this command.");
62
+ return;
63
+ }
64
+ for (const [index, finding] of findings.entries()) {
65
+ lines.push("");
66
+ lines.push(`${index + 1}. **[${finding.severity.toUpperCase()}] ${finding.title}**`);
67
+ lines.push(` - Rule: \`${finding.ruleId}\``);
68
+ lines.push(` - Evidence: ${formatEvidence(finding.evidence[0])}`);
69
+ lines.push(` - Why: ${finding.why}`);
70
+ lines.push(` - Verify: ${finding.suggestedVerification}`);
71
+ lines.push(` - Fix direction: ${finding.suggestedFix}`);
72
+ }
73
+ }
74
+ function formatEvidence(evidence) {
75
+ if (!evidence)
76
+ return "`none`";
77
+ const location = evidence.line ? `${evidence.file}:${evidence.line}` : evidence.file;
78
+ const detail = evidence.snippet ?? evidence.match;
79
+ return detail ? `\`${location}\` - ${detail}` : `\`${location}\``;
80
+ }
81
+ function escapeMarkdownTableCell(value) {
82
+ return value.replaceAll("|", "\\|").replaceAll("\n", " ");
83
+ }
@@ -195,6 +195,13 @@ export const RULE_CATALOG = {
195
195
  why: "AI-generated PRs often bury trust-boundary changes inside larger diffs.",
196
196
  stability: "default"
197
197
  },
198
+ "pr-risk.diff-unavailable": {
199
+ ruleId: "pr-risk.diff-unavailable",
200
+ severity: "info",
201
+ title: "Git diff could not be read",
202
+ why: "PR classification can be misleading when the requested base ref or Git history is unavailable.",
203
+ stability: "default"
204
+ },
198
205
  "pr-risk.no-diff": {
199
206
  ruleId: "pr-risk.no-diff",
200
207
  severity: "info",
@@ -14,10 +14,13 @@ const categoryWeights = {
14
14
  "large AI-generated/refactor-like diff": 18
15
15
  };
16
16
  export async function classifyPrRisk(options) {
17
- const diffText = options.diffText ?? (await readGitDiff(options.rootDir, options.base));
17
+ const diffResult = options.diffText === undefined
18
+ ? await readGitDiff(options.rootDir, options.base)
19
+ : { diffText: options.diffText, diagnostics: [] };
20
+ const { diffText } = diffResult;
18
21
  const files = parseDiffFiles(diffText);
19
22
  const categories = new Set();
20
- const findings = [];
23
+ const findings = [...diffResult.diagnostics];
21
24
  for (const file of files) {
22
25
  for (const category of file.categories) {
23
26
  categories.add(category);
@@ -44,7 +47,7 @@ export async function classifyPrRisk(options) {
44
47
  suggestedFix: "Split unrelated UI/refactor work away from trust-boundary changes and add focused tests before merge."
45
48
  }));
46
49
  }
47
- if (diffText.trim().length === 0) {
50
+ if (diffText.trim().length === 0 && diffResult.diagnostics.length === 0) {
48
51
  findings.push(finding({
49
52
  ruleId: "pr-risk.no-diff",
50
53
  title: "No git diff found",
@@ -68,13 +71,17 @@ async function readGitDiff(rootDir, base) {
68
71
  if (base) {
69
72
  try {
70
73
  const { stdout } = await execFileAsync("git", ["diff", `${base}...HEAD`], { cwd: rootDir, maxBuffer: 20 * 1024 * 1024 });
71
- return stdout;
74
+ return { diffText: stdout, diagnostics: [] };
72
75
  }
73
- catch {
74
- return "";
76
+ catch (error) {
77
+ return {
78
+ diffText: "",
79
+ diagnostics: [buildGitDiffFailureFinding(rootDir, ["git", "diff", `${base}...HEAD`], error, base)]
80
+ };
75
81
  }
76
82
  }
77
83
  const parts = [];
84
+ const failures = [];
78
85
  for (const args of [
79
86
  ["diff", "--cached"],
80
87
  ["diff"]
@@ -83,11 +90,62 @@ async function readGitDiff(rootDir, base) {
83
90
  const { stdout } = await execFileAsync("git", args, { cwd: rootDir, maxBuffer: 20 * 1024 * 1024 });
84
91
  parts.push(stdout);
85
92
  }
86
- catch {
87
- continue;
93
+ catch (error) {
94
+ failures.push({ args: ["git", ...args], error });
88
95
  }
89
96
  }
90
- return parts.join("\n");
97
+ if (parts.length === 0 && failures.length > 0) {
98
+ return {
99
+ diffText: "",
100
+ diagnostics: [buildGitDiffFailureFinding(rootDir, failures[0].args, failures[0].error)]
101
+ };
102
+ }
103
+ return { diffText: parts.join("\n"), diagnostics: [] };
104
+ }
105
+ function buildGitDiffFailureFinding(rootDir, command, error, base) {
106
+ const errorText = redactRootPath(getGitErrorText(error), rootDir);
107
+ const lowerError = errorText.toLowerCase();
108
+ let suggestedVerification = "Run `git status` and confirm the target path is inside a Git repository.";
109
+ let suggestedFix = "Run `pr-risk` from a Git checkout, or pass explicit diff text through the API.";
110
+ if (base) {
111
+ suggestedVerification = `Run \`${buildFetchCommand(base)}\`, then \`git rev-parse --verify ${base}\` to confirm the base ref exists locally.`;
112
+ suggestedFix = "Fetch the branch or pass an existing local base ref, for example `--base origin/main`.";
113
+ }
114
+ if (base && (lowerError.includes("no merge base") || lowerError.includes("shallow"))) {
115
+ suggestedVerification = "Run `git rev-parse --is-shallow-repository` and confirm CI checks out full history before `pr-risk`.";
116
+ suggestedFix = "Use `fetch-depth: 0` in `actions/checkout`, or run `git fetch --unshallow` before invoking `pr-risk`.";
117
+ }
118
+ return finding({
119
+ ruleId: "pr-risk.diff-unavailable",
120
+ title: base ? `Could not read git diff for base ${base}` : "Could not read git diff",
121
+ severity: "info",
122
+ evidence: [
123
+ {
124
+ file: ".",
125
+ match: command.join(" "),
126
+ snippet: errorText.slice(0, 500)
127
+ }
128
+ ],
129
+ why: "PR risk classification needs a readable git diff, but the git command failed for the target repository.",
130
+ suggestedVerification,
131
+ suggestedFix
132
+ });
133
+ }
134
+ function buildFetchCommand(base) {
135
+ const remoteRef = /^([^/\s]+)\/(.+)$/.exec(base);
136
+ if (remoteRef)
137
+ return `git fetch ${remoteRef[1]} ${remoteRef[2]}`;
138
+ return `git fetch origin ${base}`;
139
+ }
140
+ function redactRootPath(text, rootDir) {
141
+ return rootDir ? text.replaceAll(rootDir, ".") : text;
142
+ }
143
+ function getGitErrorText(error) {
144
+ const candidate = error;
145
+ return [candidate.stderr, candidate.stdout, candidate.message]
146
+ .filter((value) => Boolean(value?.trim()))
147
+ .join("\n")
148
+ .trim() || "git diff exited with a non-zero status.";
91
149
  }
92
150
  function parseDiffFiles(diffText) {
93
151
  const files = [];
@@ -0,0 +1,69 @@
1
+ # GitHub Action Usage
2
+
3
+ `ai-saas-guard` ships as a composite GitHub Action for pull request and code scanning workflows.
4
+
5
+ Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.2.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
6
+
7
+ ## PR Summary
8
+
9
+ Use markdown when reviewers need a short, evidence-first summary of risky files, required verification, and suggested PR split.
10
+
11
+ ```yaml
12
+ name: ai-saas-guard-pr-summary
13
+
14
+ on:
15
+ pull_request:
16
+
17
+ permissions:
18
+ contents: read
19
+
20
+ jobs:
21
+ pr-summary:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v6.0.2
25
+ with:
26
+ fetch-depth: 0
27
+ - uses: zr9959/ai-saas-guard@v0
28
+ with:
29
+ command: pr-risk
30
+ root: ${{ github.workspace }}
31
+ base: origin/main
32
+ format: markdown
33
+ output: ai-saas-guard-pr.md
34
+ - run: cat ai-saas-guard-pr.md >> "$GITHUB_STEP_SUMMARY"
35
+ ```
36
+
37
+ Use markdown for PR review triage. It is intentionally short enough for a GitHub step summary or a PR comment created by your own workflow. It does not require a hosted service.
38
+
39
+ ## SARIF Upload
40
+
41
+ Use SARIF when you want findings to appear in GitHub code scanning alerts.
42
+
43
+ ```yaml
44
+ name: ai-saas-guard-sarif
45
+
46
+ on:
47
+ pull_request:
48
+
49
+ permissions:
50
+ contents: read
51
+ security-events: write
52
+
53
+ jobs:
54
+ code-scanning:
55
+ runs-on: ubuntu-latest
56
+ steps:
57
+ - uses: actions/checkout@v6.0.2
58
+ - uses: zr9959/ai-saas-guard@v0
59
+ with:
60
+ command: scan
61
+ root: ${{ github.workspace }}
62
+ format: sarif
63
+ output: ai-saas-guard.sarif
64
+ - uses: github/codeql-action/upload-sarif@v3
65
+ with:
66
+ sarif_file: ai-saas-guard.sarif
67
+ ```
68
+
69
+ Use SARIF for tracking alerts over time. Use markdown for reviewer guidance on a specific PR. Many teams should run both: markdown for quick review order, SARIF for code scanning visibility.
@@ -5,27 +5,25 @@
5
5
  ## Current State
6
6
 
7
7
  - Package name: `ai-saas-guard`
8
- - Current version: `0.1.2`
8
+ - Current version: `0.2.0`
9
9
  - npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
10
10
  - First npm-published version: `0.1.1`
11
- - GitHub Release: `v0.1.2`
11
+ - GitHub Release: `v0.2.0`
12
12
  - Publish workflow: `.github/workflows/npm-publish.yml`
13
+ - Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
14
+ - Long-lived npm publish token: not required
13
15
 
14
16
  ## Preferred Path
15
17
 
16
- Use GitHub Actions with npm provenance:
18
+ Use GitHub Actions with npm Trusted Publisher/OIDC:
17
19
 
18
- 1. Create and review a release tag such as `v0.1.2`.
19
- 2. Run the `Publish npm` workflow manually with `ref` set to that tag.
20
- 3. Configure npm Trusted Publisher for future releases:
21
- - Provider: GitHub Actions
22
- - Organization or user: `zr9959`
23
- - Repository: `ai-saas-guard`
24
- - Workflow filename: `npm-publish.yml`
25
- - Allowed action: `npm publish`
26
- 4. Once trusted publishing is verified, remove or rotate any long-lived npm publish token.
20
+ 1. Create and review a release tag such as `v0.2.0`.
21
+ 2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
22
+ 3. Keep `permissions.id-token: write` in the workflow so npm can exchange the GitHub Actions OIDC identity for a short-lived publish credential.
23
+ 4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
24
+ 5. Keep npm package publishing access set to require 2FA and disallow traditional tokens. Trusted publishers continue to work because they use OIDC instead of npm auth tokens.
27
25
 
28
- The first npm publish used a temporary granular access token because npm requires a 2FA-bypass token until trusted publishing is configured. The workflow sets `id-token: write`, uses Node 24, and runs `npm publish --provenance --access public`, so it is ready for npm Trusted Publisher OIDC publishing.
26
+ The first npm publish used a temporary granular access token because npm requires a 2FA-bypass token until trusted publishing is configured. That temporary automation token and the GitHub `NPM_TOKEN` secret were removed after the Trusted Publisher migration.
29
27
 
30
28
  ## Release Gate
31
29
 
@@ -45,9 +45,12 @@ Implemented surfaces:
45
45
  - MCP config side-effect and secret-bearing risk inventory
46
46
  - Next/Vercel deploy and runtime footguns
47
47
  - PR diff risk triage for auth, billing, RLS, env, tests removed, and large mixed diffs
48
+ - PR diff diagnostics when a base ref or shallow checkout prevents comparison
49
+ - PR-focused markdown summary output for GitHub step summaries or PR comments
48
50
  - JSON output
49
51
  - SARIF output
50
52
  - composite GitHub Action wrapper
53
+ - npm publishing through GitHub Actions Trusted Publisher/OIDC
51
54
 
52
55
  Existing commands:
53
56
 
@@ -96,13 +99,9 @@ GitHub Project:
96
99
  Current issue set:
97
100
 
98
101
  - #1 Add launch-readiness checklist content
99
- - #2 Add GitHub Action release packaging
100
102
  - #3 Add configurable rule severity and rule toggles
101
- - #4 Add PR comment summary mode
102
103
  - #5 Write Stripe webhook replay cookbook
103
- - #6 Add SARIF upload workflow example
104
104
  - #7 Expand Supabase RLS fixtures and ownership patterns
105
- - #8 Publish ai-saas-guard to npm
106
105
 
107
106
  CI:
108
107
 
@@ -111,6 +110,14 @@ CI:
111
110
  - Uses `permissions: contents: read`
112
111
  - Latest verified run after setup succeeded
113
112
 
113
+ Publishing:
114
+
115
+ - npm package: `ai-saas-guard`
116
+ - Current release line: `v0.2.0`
117
+ - Publish workflow: `.github/workflows/npm-publish.yml`
118
+ - Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
119
+ - Long-lived npm publish tokens should not be required.
120
+
114
121
  ## Repository Boundaries
115
122
 
116
123
  Allowed in this public repository:
@@ -133,14 +140,11 @@ Not allowed:
133
140
 
134
141
  Recommended order:
135
142
 
136
- 1. Prepare npm publishing plan with trusted publishing/provenance.
137
- 2. Add GitHub Action release packaging and example workflow.
138
- 3. Add PR comment summary mode.
139
- 4. Add configurable severity and rule toggles.
140
- 5. Expand Supabase RLS fixtures and ownership patterns.
141
- 6. Write Stripe webhook replay cookbook.
142
- 7. Add SARIF upload workflow example.
143
- 8. Improve false-positive suppression and rule stability labels.
143
+ 1. Add configurable severity and rule toggles.
144
+ 2. Expand Supabase RLS fixtures and ownership patterns.
145
+ 3. Write Stripe webhook replay cookbook.
146
+ 4. Improve false-positive suppression and rule stability labels.
147
+ 5. Add a GitHub App design note for the potential hosted layer.
144
148
 
145
149
  For every feature, keep the scanner evidence-first:
146
150
 
package/docs/rules.md CHANGED
@@ -61,6 +61,7 @@ Rule metadata is centralized in `src/rules/catalog.ts` and covered by tests so S
61
61
  | Rule ID | Severity | Why it exists |
62
62
  | --- | --- | --- |
63
63
  | `pr-risk.sensitive-surface` | medium/high | Highlights files reviewers should inspect before cosmetic or refactor files. |
64
+ | `pr-risk.diff-unavailable` | info | Explains when the requested base ref or Git history prevents PR diff classification. |
64
65
  | `pr-risk.no-diff` | info | Explains that PR classification needs a diff. |
65
66
 
66
67
  ## Adding Rules
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-saas-guard",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "Repo-local launch-readiness scanner for AI-built SaaS apps.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/zr9959/ai-saas-guard#readme",