ai-saas-guard 0.29.0 → 0.29.2

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
@@ -5,7 +5,7 @@
5
5
  </p>
6
6
 
7
7
  <p align="center">
8
- ai-saas-guard is a local-first launch gate for AI-built Next.js, Supabase, Stripe, Vercel, and MCP SaaS apps. It focuses on auth, billing, data access, secrets, MCP, and deploy decisions, plus CI and fake-success paths, so you know what to review before launch or merge. It runs locally, reads your repo only, and does not upload code.
8
+ A local-first launch gate for AI-built Next.js, Supabase, Stripe, Vercel, GitHub Actions, and MCP SaaS apps. It focuses on auth, billing, data access, secrets, MCP, and deploy paths, then turns risky files into a short review queue before launch or merge. It runs locally, reads your repo only, and does not upload code.
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -27,9 +27,11 @@
27
27
 
28
28
  ---
29
29
 
30
- ## The Launch Problem
30
+ ## Before You Invite Users
31
31
 
32
- AI can make a SaaS look finished while the real launch blockers sit in trust-boundary code. These are the failures that hurt after real users arrive:
32
+ AI can make a SaaS look finished: login works, checkout opens, the dashboard loads, and tests are green. The launch risk is usually hidden in trust-boundary code that decides who gets access, who pays, what data they can see, and whether failures are visible.
33
+
34
+ These are the failures that hurt after real users arrive:
33
35
 
34
36
  - one customer can see or change another customer's data
35
37
  - Stripe grants access from an unsigned, duplicated, missing, or failed webhook path
@@ -42,23 +44,56 @@ AI can make a SaaS look finished while the real launch blockers sit in trust-bou
42
44
 
43
45
  `ai-saas-guard` gives you a short local review queue for those risks. It does not prove the app is secure, certify a release, or replace human review. It tells founders, solo builders, small teams, and reviewers what deserves attention first.
44
46
 
47
+ ## 60-Second Local Check
48
+
49
+ Run it against your app without installing anything globally:
50
+
51
+ ```bash
52
+ npx ai-saas-guard@latest scan --root /path/to/your-saas
53
+ ```
54
+
55
+ For an AI-heavy pull request:
56
+
57
+ ```bash
58
+ npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown
59
+ ```
60
+
61
+ You get rule IDs, severity, file evidence, why it matters, how to verify it manually, and a concrete fix direction. The scanner is deterministic, read-only, and does not call an LLM.
62
+
63
+ ## Try The Demo Fixtures
64
+
65
+ Want to see the report before scanning your own repo?
66
+
67
+ ```bash
68
+ git clone https://github.com/zr9959/ai-saas-guard.git
69
+ cd ai-saas-guard
70
+ npx ai-saas-guard@latest scan --root examples/demo-risky-saas
71
+ npx ai-saas-guard@latest scan --root examples/demo-safe-saas
72
+ ```
73
+
74
+ The risky demo currently returns 19 intentional findings across Stripe, Supabase, silent-success paths, Next/Vercel deploy hints, and GitHub Actions. The safe demo returns 0 findings for the same broad surfaces with safer static patterns. See [docs/demo-quickstart.md](docs/demo-quickstart.md).
75
+
45
76
  ## See The Output
46
77
 
47
78
  The report is designed to be read before launch or before merging an AI-heavy PR. A longer copy-paste example is in [docs/sample-launch-report.md](docs/sample-launch-report.md).
48
79
 
49
80
  ```text
50
81
  Launch Gate: review before launch
51
- 4 findings: 1 high, 3 medium
82
+ 19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
52
83
 
53
- HIGH stripe.webhook.missing-signature
84
+ CRITICAL stripe.webhook.missing-signature
54
85
  File: app/api/stripe/webhook/route.ts
55
86
  Why: billing access can be granted from a webhook path that does not verify Stripe signatures.
56
87
  Verify: replay a webhook with an invalid signature and confirm the route rejects it.
57
88
  Fix: read the raw body, call stripe.webhooks.constructEvent, and make event handling idempotent.
58
89
 
59
- MEDIUM supabase.rls.tenant-predicate-missing
60
- File: supabase/migrations/20260524_accounts.sql
61
- Verify: sign in as user A and user B; confirm neither can SELECT or UPDATE the other's rows.
90
+ HIGH silent-success.swallowed-error
91
+ File: app/api/billing/checkout/route.ts
92
+ Verify: force the upstream billing call to fail and confirm the route returns an error, not fake success.
93
+
94
+ MEDIUM deploy.next.missing-security-headers
95
+ File: app/api/billing/checkout/route.ts
96
+ Verify: inspect production response headers for auth, billing, and API pages.
62
97
  ```
63
98
 
64
99
  ## What You Get
@@ -85,28 +120,6 @@ One command returns a launch-readiness report with:
85
120
  | Are tools and CI overpowered? | MCP side-effect classes, local policy/receipt templates, GitHub Actions permissions, concurrency, checkout depth, action pinning |
86
121
  | Can reviewers trust the PR? | `pr-risk` ranking for auth, billing, RLS, deploy, API, storage, tests, silent-success paths, missing spec context, and large AI diffs |
87
122
 
88
- ## Current Status
89
-
90
- This repository is public on GitHub.
91
-
92
- 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.
93
-
94
- | Area | Status |
95
- | --- | --- |
96
- | Public GitHub repository | Available |
97
- | npm CLI | `ai-saas-guard@0.29.0` |
98
- | GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.29.0` |
99
- | Outputs | Terminal, JSON, SARIF, and PR-focused markdown |
100
- | Project config | `.ai-saas-guard.json` rule toggles, severity overrides, suppressions, and fail thresholds |
101
- | Privacy model | Local-first, read-only scan commands, no LLM calls, no code upload |
102
- | Versioned Action tags | `v0.29.0`, `v0` |
103
- | Current release | `0.29.0` hosted Node checkout platform composition, Clerk unsafe metadata rule, Prisma tenant-scope rule, Vercel cron guard rule, sample launch report, and Marketplace wrapper decision |
104
- | npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
105
- | Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
106
- | Cloudflare hosted ingress | Deployed at `https://ai-saas-guard-hosted.zr9959.workers.dev`; signed GitHub App webhook delivery and compact Check Run smoke now pass in staging |
107
- | Hosted GitHub App staging | Private App `ai-saas-guard-hosted` (`3834787`) installed on `zr9959/ai-saas-guard`; hosted operations evidence is in [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md) |
108
- | OpenSSF Best Practices | Passing badge, project `12955`; `.bestpractices.json` remains the conservative evidence record |
109
-
110
123
  ## Quick Start
111
124
 
112
125
  Run the published CLI without installing it globally:
@@ -157,6 +170,28 @@ node dist/cli.js check-mcp --root /path/to/your-saas
157
170
  node dist/cli.js check-actions --root /path/to/your-saas
158
171
  ```
159
172
 
173
+ ## Current Status
174
+
175
+ This repository is public on GitHub.
176
+
177
+ 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.
178
+
179
+ | Area | Status |
180
+ | --- | --- |
181
+ | Public GitHub repository | Available |
182
+ | npm CLI | `ai-saas-guard@0.29.2` |
183
+ | GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.29.2` |
184
+ | Outputs | Terminal, JSON, SARIF, and PR-focused markdown |
185
+ | Project config | `.ai-saas-guard.json` rule toggles, severity overrides, suppressions, and fail thresholds |
186
+ | Privacy model | Local-first, read-only scan commands, no LLM calls, no code upload |
187
+ | Versioned Action tags | `v0.29.2`, `v0` |
188
+ | Current release | `0.29.2` publishes public risky/safe demo fixtures, a demo quickstart, quickstart feedback template, and refreshed first-run README guidance |
189
+ | npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
190
+ | Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
191
+ | Cloudflare hosted ingress | Deployed at `https://ai-saas-guard-hosted.zr9959.workers.dev`; signed GitHub App webhook delivery and compact Check Run smoke now pass in staging |
192
+ | Hosted GitHub App staging | Private App `ai-saas-guard-hosted` (`3834787`) installed on `zr9959/ai-saas-guard`; hosted operations evidence is in [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md) |
193
+ | OpenSSF Best Practices | Passing badge, project `12955`; `.bestpractices.json` remains the conservative evidence record |
194
+
160
195
  ## Example Finding
161
196
 
162
197
  Terminal output is designed to be useful to a reviewer, not just a scanner dashboard.
@@ -317,7 +352,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
317
352
 
318
353
  ## GitHub Action
319
354
 
320
- The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.29.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
355
+ The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.29.2` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
321
356
 
322
357
  ```yaml
323
358
  name: ai-saas-guard
package/action.yml CHANGED
@@ -8,11 +8,11 @@ inputs:
8
8
  required: false
9
9
  default: scan
10
10
  root:
11
- description: Repository path to scan.
11
+ description: "Repository path to scan. Usually github.workspace."
12
12
  required: false
13
13
  default: ${{ github.workspace }}
14
14
  format:
15
- description: "Output format: terminal, json, sarif, or markdown."
15
+ description: "Output format: terminal, json, sarif, or markdown. Use markdown for PR summaries and sarif for code scanning."
16
16
  required: false
17
17
  default: terminal
18
18
  fail-on:
@@ -20,15 +20,15 @@ inputs:
20
20
  required: false
21
21
  default: none
22
22
  base:
23
- description: Base ref for pr-risk.
23
+ description: "Base ref for pr-risk. Use origin/main with actions/checkout fetch-depth: 0."
24
24
  required: false
25
25
  default: ""
26
26
  config:
27
- description: Optional ai-saas-guard JSON config path.
27
+ description: "Optional ai-saas-guard JSON config path, relative to the workflow workspace or root."
28
28
  required: false
29
29
  default: ""
30
30
  output:
31
- description: Optional path to write output while also keeping the command exit code.
31
+ description: "Optional path to write output while also keeping the command exit code."
32
32
  required: false
33
33
  default: ""
34
34
 
@@ -56,7 +56,7 @@ runs:
56
56
  INPUT_CONFIG: ${{ inputs.config }}
57
57
  INPUT_OUTPUT: ${{ inputs.output }}
58
58
  run: |
59
- set -o pipefail
59
+ set -euo pipefail
60
60
 
61
61
  case "${INPUT_COMMAND}" in
62
62
  scan|check-supabase|check-stripe|check-mcp|check-actions|pr-risk) ;;
@@ -82,6 +82,16 @@ runs:
82
82
  ;;
83
83
  esac
84
84
 
85
+ if [ ! -d "${INPUT_ROOT}" ]; then
86
+ echo "Root path does not exist or is not a directory: ${INPUT_ROOT}" >&2
87
+ exit 2
88
+ fi
89
+
90
+ if [ -n "${INPUT_CONFIG}" ] && [ ! -f "${INPUT_CONFIG}" ] && [ ! -f "${INPUT_ROOT%/}/${INPUT_CONFIG}" ]; then
91
+ echo "Config file not found: ${INPUT_CONFIG}" >&2
92
+ exit 2
93
+ fi
94
+
85
95
  args=("${INPUT_COMMAND}" "--root" "${INPUT_ROOT}")
86
96
 
87
97
  if [ "${INPUT_FORMAT}" = "json" ]; then
@@ -105,6 +115,7 @@ runs:
105
115
  fi
106
116
 
107
117
  if [ -n "${INPUT_OUTPUT}" ]; then
118
+ mkdir -p -- "$(dirname -- "${INPUT_OUTPUT}")"
108
119
  node "${GITHUB_ACTION_PATH}/dist/cli.js" "${args[@]}" | tee -- "${INPUT_OUTPUT}"
109
120
  else
110
121
  node "${GITHUB_ACTION_PATH}/dist/cli.js" "${args[@]}"
@@ -5,7 +5,7 @@
5
5
  </p>
6
6
 
7
7
  <p align="center">
8
- ai-saas-guard 是面向 AI 构建的 Next.js、Supabase、Stripe、Vercel 和 MCP SaaS 的本地优先上线 gate。它会优先指出 auth、billing、data access、secrets、MCPdeploy、CI 和“假成功”路径里最值得人工 review 的改动,让你在上线前知道该先看哪里。它本地运行、只读仓库、不上传代码。
8
+ 面向 AI 构建的 Next.js、Supabase、Stripe、Vercel、GitHub Actions 和 MCP SaaS 的本地优先上线 gate。它聚焦 auth、billing、data access、secrets、MCPdeploy,把仓库里最容易出事的风险路径变成一份短 review 队列,让你在上线前或合并 PR 前知道该先看哪里。它本地运行、只读仓库、不上传代码。
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -26,9 +26,11 @@
26
26
 
27
27
  ---
28
28
 
29
- ## 它解决什么问题
29
+ ## 邀请真实用户前先看这里
30
30
 
31
- AI 能很快把一个 SaaS 做到“看起来能用”。真正危险的是上线后才暴露的信任边界问题:
31
+ AI 能很快把一个 SaaS 做到“看起来能用”:能登录、能打开 checkout、dashboard 能加载、测试也是绿的。真正危险的是信任边界代码,它决定谁有权限、谁付了钱、谁能看哪些数据,以及服务失败时会不会被悄悄伪装成成功。
32
+
33
+ 这些问题通常会在真实用户来了以后才变痛:
32
34
 
33
35
  - 一个用户能看到或修改另一个客户的数据
34
36
  - Stripe webhook 因为未签名、重复、漏处理失败事件而错误开通权限
@@ -41,23 +43,56 @@ AI 能很快把一个 SaaS 做到“看起来能用”。真正危险的是上
41
43
 
42
44
  `ai-saas-guard` 是面向这个时刻的本地优先、review-first 上线预检工具。它不会证明你的应用绝对安全,也不是渗透测试、认证或完整安全审计。它的目标是给 founder、独立开发者、小团队和 reviewer 一份短而有证据的清单,告诉你上线或合并 PR 前最该先看哪里。
43
45
 
46
+ ## 60 秒本地检查
47
+
48
+ 无需全局安装,直接扫你的应用:
49
+
50
+ ```bash
51
+ npx ai-saas-guard@latest scan --root /path/to/your-saas
52
+ ```
53
+
54
+ 如果是 AI 生成的大 PR:
55
+
56
+ ```bash
57
+ npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown
58
+ ```
59
+
60
+ 你会得到 rule ID、severity、文件证据、为什么重要、如何人工验证,以及具体修复方向。扫描是 deterministic、只读的,不调用 LLM。
61
+
62
+ ## 先试公开 demo
63
+
64
+ 如果你还不想先扫自己的私有仓库,可以先跑公开 fixture:
65
+
66
+ ```bash
67
+ git clone https://github.com/zr9959/ai-saas-guard.git
68
+ cd ai-saas-guard
69
+ npx ai-saas-guard@latest scan --root examples/demo-risky-saas
70
+ npx ai-saas-guard@latest scan --root examples/demo-safe-saas
71
+ ```
72
+
73
+ risky demo 当前会故意触发 19 个 finding,覆盖 Stripe、Supabase、silent-success、Next/Vercel deploy 提示和 GitHub Actions。safe demo 在同类风险面上使用更安全的静态写法,当前返回 0 个 finding。说明见 [demo-quickstart.md](demo-quickstart.md)。
74
+
44
75
  ## 输出长什么样
45
76
 
46
77
  报告是给上线前或合并 AI 大 PR 前快速阅读的。更完整的可复制样例见 [docs/sample-launch-report.md](sample-launch-report.md)。
47
78
 
48
79
  ```text
49
80
  Launch Gate: review before launch
50
- 4 findings: 1 high, 3 medium
81
+ 19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
51
82
 
52
- HIGH stripe.webhook.missing-signature
83
+ CRITICAL stripe.webhook.missing-signature
53
84
  File: app/api/stripe/webhook/route.ts
54
85
  Why: billing access can be granted from a webhook path that does not verify Stripe signatures.
55
86
  Verify: replay a webhook with an invalid signature and confirm the route rejects it.
56
87
  Fix: read the raw body, call stripe.webhooks.constructEvent, and make event handling idempotent.
57
88
 
58
- MEDIUM supabase.rls.tenant-predicate-missing
59
- File: supabase/migrations/20260524_accounts.sql
60
- Verify: sign in as user A and user B; confirm neither can SELECT or UPDATE the other's rows.
89
+ HIGH silent-success.swallowed-error
90
+ File: app/api/billing/checkout/route.ts
91
+ Verify: force the upstream billing call to fail and confirm the route returns an error, not fake success.
92
+
93
+ MEDIUM deploy.next.missing-security-headers
94
+ File: app/api/billing/checkout/route.ts
95
+ Verify: inspect production response headers for auth, billing, and API pages.
61
96
  ```
62
97
 
63
98
  ## 你会得到什么
@@ -84,28 +119,6 @@ Verify: sign in as user A and user B; confirm neither can SELECT or UPDATE the o
84
119
  | 工具和 CI 权限是不是过大? | MCP side-effect 分类、本地 policy/receipt 模板、GitHub Actions 权限、concurrency、checkout depth、Action pinning |
85
120
  | reviewer 能不能看懂 AI PR? | `pr-risk` 对 auth、billing、RLS、deploy、API、storage、测试、silent-success、缺 spec context 和大型 diff 排序 |
86
121
 
87
- ## 当前状态
88
-
89
- 这个仓库是公开 GitHub 仓库。
90
-
91
- CLI 已发布到 npm:`ai-saas-guard@0.29.0`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.29.0`。
92
-
93
- | 模块 | 状态 |
94
- | --- | --- |
95
- | 公开 GitHub 仓库 | 已可用 |
96
- | npm CLI | `ai-saas-guard@0.29.0` |
97
- | GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.29.0` |
98
- | 输出格式 | Terminal、JSON、SARIF 和 PR markdown |
99
- | 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖、suppressions 和 fail threshold |
100
- | 隐私模型 | 本地优先、只读扫描、不调用 LLM、不上传代码 |
101
- | 当前版本 | `0.29.0` hosted Node checkout platform 组合入口、Clerk unsafe metadata 规则、Prisma tenant-scope 规则、Vercel cron guard 规则、sample launch report 和 Marketplace wrapper 决策 |
102
- | Action 标签 | `v0.29.0`、`v0` |
103
- | npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
104
- | 仓库可信度加固 | 严格 branch protection、Dependabot、CodeQL、fast-check fuzzing、signed release provenance assets、private vulnerability reporting、secret scanning 和 push protection |
105
- | Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`;签名 GitHub App webhook delivery 和 compact Check Run staging smoke 已通过 |
106
- | Hosted GitHub App staging | 私有 App `ai-saas-guard-hosted`(`3834787`)已安装到 `zr9959/ai-saas-guard`;hosted operations evidence 见 [docs/hosted-operations-evidence.md](hosted-operations-evidence.md) |
107
- | OpenSSF Best Practices | 已获得 passing badge,项目 `12955`;`.bestpractices.json` 继续作为保守证据记录 |
108
-
109
122
  ## 快速开始
110
123
 
111
124
  无需全局安装,直接运行:
@@ -144,6 +157,28 @@ npm run build
144
157
  node dist/cli.js scan --root /path/to/your-saas
145
158
  ```
146
159
 
160
+ ## 当前状态
161
+
162
+ 这个仓库是公开 GitHub 仓库。
163
+
164
+ CLI 已发布到 npm:`ai-saas-guard@0.29.2`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.29.2`。
165
+
166
+ | 模块 | 状态 |
167
+ | --- | --- |
168
+ | 公开 GitHub 仓库 | 已可用 |
169
+ | npm CLI | `ai-saas-guard@0.29.2` |
170
+ | GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.29.2` |
171
+ | 输出格式 | Terminal、JSON、SARIF 和 PR markdown |
172
+ | 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖、suppressions 和 fail threshold |
173
+ | 隐私模型 | 本地优先、只读扫描、不调用 LLM、不上传代码 |
174
+ | 当前版本 | `0.29.2` 发布公开 risky/safe demo fixtures、demo quickstart、quickstart 反馈模板,并刷新首次试用 README 指引 |
175
+ | Action 标签 | `v0.29.2`、`v0` |
176
+ | npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
177
+ | 仓库可信度加固 | 严格 branch protection、Dependabot、CodeQL、fast-check fuzzing、signed release provenance assets、private vulnerability reporting、secret scanning 和 push protection |
178
+ | Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`;签名 GitHub App webhook delivery 和 compact Check Run staging smoke 已通过 |
179
+ | Hosted GitHub App staging | 私有 App `ai-saas-guard-hosted`(`3834787`)已安装到 `zr9959/ai-saas-guard`;hosted operations evidence 见 [docs/hosted-operations-evidence.md](hosted-operations-evidence.md) |
180
+ | OpenSSF Best Practices | 已获得 passing badge,项目 `12955`;`.bestpractices.json` 继续作为保守证据记录 |
181
+
147
182
  ## 主要命令
148
183
 
149
184
  | 命令 | 用途 |
@@ -0,0 +1,60 @@
1
+ # Demo Quickstart
2
+
3
+ Use these public fixtures when you want to understand `ai-saas-guard` before pointing it at a private repository.
4
+
5
+ ## Risky Demo
6
+
7
+ ```bash
8
+ npx ai-saas-guard@latest scan --root examples/demo-risky-saas
9
+ ```
10
+
11
+ The risky demo intentionally includes unsigned Stripe webhook handling, a silent-success billing fallback, broad Supabase RLS, and overpowered GitHub Actions permissions.
12
+
13
+ Expected summary:
14
+
15
+ ```text
16
+ 19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
17
+ ```
18
+
19
+ The first findings should point at trust-boundary files such as:
20
+
21
+ - `app/api/stripe/webhook/route.ts` for missing Stripe signature verification and idempotency
22
+ - `supabase/migrations/001_accounts.sql` for broad RLS and missing tenant predicates
23
+ - `app/api/billing/checkout/route.ts` for a silent-success billing fallback
24
+ - `.github/workflows/ci.yml` for launch-related workflow hygiene hints
25
+
26
+ For local development from this repository checkout:
27
+
28
+ ```bash
29
+ npm ci
30
+ npm run build
31
+ node dist/cli.js scan --root examples/demo-risky-saas
32
+ ```
33
+
34
+ ## Safe Demo
35
+
36
+ ```bash
37
+ npx ai-saas-guard@latest scan --root examples/demo-safe-saas
38
+ ```
39
+
40
+ The safe demo keeps the same broad surfaces but uses safer static patterns: Stripe signature verification and idempotency hints, scoped RLS, security headers, documented env variables, request IDs, and bounded GitHub Actions permissions.
41
+
42
+ Expected summary:
43
+
44
+ ```text
45
+ 0 findings
46
+ ```
47
+
48
+ For local development from this repository checkout:
49
+
50
+ ```bash
51
+ node dist/cli.js scan --root examples/demo-safe-saas
52
+ ```
53
+
54
+ ## What To Look For
55
+
56
+ - Every finding has a rule ID, severity, file evidence, why it matters, a manual verification step, and a fix direction.
57
+ - The risky demo is intentionally noisy enough to show the report shape.
58
+ - The safe demo is intentionally small; it is not a complete SaaS template and does not certify a real app.
59
+
60
+ Do not paste real API keys, customer data, private source code, webhook secrets, or production URLs into public issues when sharing output.
@@ -2,7 +2,9 @@
2
2
 
3
3
  `ai-saas-guard` ships as a composite GitHub Action for pull request and code scanning workflows.
4
4
 
5
- Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.28.1` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
5
+ Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.29.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
6
+
7
+ The Action runs the same local scanner inside the GitHub-hosted runner. It reads the checked-out repository, does not call an LLM, and does not upload source code. For `pr-risk`, always use `actions/checkout` with `fetch-depth: 0` so the base branch comparison is available.
6
8
 
7
9
  ## PR Summary
8
10
 
@@ -5,11 +5,11 @@
5
5
  ## Current State
6
6
 
7
7
  - Package name: `ai-saas-guard`
8
- - Current published version: `0.29.0`
8
+ - Current published version: `0.29.2`
9
9
  - Next source candidate: none
10
10
  - npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
11
11
  - First npm-published version: `0.1.1`
12
- - GitHub Release: `v0.29.0`
12
+ - GitHub Release: `v0.29.2`
13
13
  - Publish workflow: `.github/workflows/npm-publish.yml`
14
14
  - Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
15
15
  - Long-lived npm publish token: not required
@@ -18,7 +18,7 @@
18
18
 
19
19
  Use GitHub Actions with npm Trusted Publisher/OIDC:
20
20
 
21
- 1. Create and review a release tag such as `v0.29.0`.
21
+ 1. Create and review a release tag such as `v0.29.2`.
22
22
  2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
23
23
  3. Keep `permissions.id-token: write` in the workflow so npm can exchange the GitHub Actions OIDC identity for a short-lived publish credential.
24
24
  4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
@@ -0,0 +1,13 @@
1
+ name: Risky demo CI
2
+
3
+ on:
4
+ pull_request:
5
+
6
+ permissions: write-all
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v6.0.2
13
+ - run: npm test
@@ -0,0 +1,25 @@
1
+ # Risky Demo SaaS
2
+
3
+ This is a tiny public fixture that intentionally contains common AI-built SaaS launch risks.
4
+
5
+ Run from the repository root:
6
+
7
+ ```bash
8
+ node dist/cli.js scan --root examples/demo-risky-saas
9
+ ```
10
+
11
+ Or with the published package:
12
+
13
+ ```bash
14
+ npx ai-saas-guard@latest scan --root examples/demo-risky-saas
15
+ ```
16
+
17
+ Expected themes:
18
+
19
+ - unsigned Stripe webhook handling
20
+ - silent-success billing fallback
21
+ - broad Supabase RLS policy
22
+ - over-broad GitHub Actions permissions
23
+ - stale PR workflow risk
24
+
25
+ This fixture uses inert placeholder code only. It does not contain real secrets, customer data, or production URLs.
@@ -0,0 +1,14 @@
1
+ export async function POST() {
2
+ try {
3
+ return Response.json({ checkoutUrl: await createCheckout() });
4
+ } catch {
5
+ return Response.json({
6
+ success: true,
7
+ checkoutUrl: "/billing/demo-success"
8
+ });
9
+ }
10
+ }
11
+
12
+ async function createCheckout() {
13
+ throw new Error("provider unavailable");
14
+ }
@@ -0,0 +1,14 @@
1
+ export async function POST(req: Request) {
2
+ const event = await req.json();
3
+
4
+ if (event.type === "checkout.session.completed") {
5
+ await grantAccess(event.data.object.customer);
6
+ return Response.json({ success: true });
7
+ }
8
+
9
+ return Response.json({ ok: true });
10
+ }
11
+
12
+ async function grantAccess(customerId: string) {
13
+ console.log("granting access", customerId);
14
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "ai-saas-guard-demo-risky-saas",
3
+ "private": true,
4
+ "type": "module"
5
+ }
@@ -0,0 +1,14 @@
1
+ create table public.accounts (
2
+ id uuid primary key,
3
+ tenant_id uuid not null,
4
+ user_id uuid not null,
5
+ plan text not null
6
+ );
7
+
8
+ alter table public.accounts enable row level security;
9
+
10
+ create policy "public read accounts"
11
+ on public.accounts
12
+ for select
13
+ to public
14
+ using (true);
@@ -0,0 +1,3 @@
1
+ STRIPE_SECRET_KEY=
2
+ STRIPE_WEBHOOK_KEY=
3
+ NEXT_PUBLIC_SUPABASE_URL=
@@ -0,0 +1,32 @@
1
+ name: Safe demo CI
2
+
3
+ on:
4
+ pull_request:
5
+ paths-ignore:
6
+ - "docs/**"
7
+ - "*.md"
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ci-${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ test:
18
+ runs-on: ubuntu-latest
19
+ env:
20
+ STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
21
+ steps:
22
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
23
+ with:
24
+ fetch-depth: 0
25
+ persist-credentials: false
26
+ - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
27
+ with:
28
+ node-version: 20
29
+ - run: test -n "${STRIPE_SECRET_KEY:-}"
30
+ - run: npm ci
31
+ - run: npx ai-saas-guard pr-risk --base origin/main
32
+ - run: npm test
@@ -0,0 +1,17 @@
1
+ # Safe Demo SaaS
2
+
3
+ This is a tiny public fixture with safer launch-readiness patterns for the same broad surfaces as the risky demo.
4
+
5
+ Run from the repository root:
6
+
7
+ ```bash
8
+ node dist/cli.js scan --root examples/demo-safe-saas
9
+ ```
10
+
11
+ Expected result:
12
+
13
+ ```text
14
+ No heuristic launch-readiness risks found by this command.
15
+ ```
16
+
17
+ The fixture is intentionally small. It is not a complete SaaS template and does not prove a real app is secure.
@@ -0,0 +1,89 @@
1
+ import Stripe from "stripe";
2
+
3
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
4
+
5
+ export async function POST(req: Request) {
6
+ const requestId = req.headers.get("x-request-id") ?? crypto.randomUUID();
7
+ await rateLimit(req);
8
+ const body = await req.text();
9
+ const signature = req.headers.get("stripe-signature");
10
+ const event = stripe.webhooks.constructEvent(
11
+ body,
12
+ signature!,
13
+ process.env.STRIPE_WEBHOOK_KEY!
14
+ );
15
+ const tenant_id = await resolveTenantForStripeCustomer(event.data.object.customer);
16
+
17
+ console.info("stripe webhook received", { requestId, eventId: event.id, tenant_id });
18
+
19
+ if (await hasProcessedEvent(event.id)) {
20
+ return new Response("ok");
21
+ }
22
+
23
+ switch (event.type) {
24
+ case "checkout.session.completed":
25
+ await syncEntitlement(event.data.object.customer);
26
+ break;
27
+ case "invoice.payment_failed":
28
+ await markPastDue(event.data.object.customer);
29
+ break;
30
+ case "invoice.payment_action_required":
31
+ await requestPaymentAction(event.data.object.customer);
32
+ break;
33
+ case "customer.subscription.updated":
34
+ await syncSubscription(event.data.object);
35
+ break;
36
+ case "customer.subscription.deleted":
37
+ await revokeEntitlement(event.data.object.customer);
38
+ break;
39
+ case "charge.refunded":
40
+ await reviewRefund(event.data.object.customer);
41
+ break;
42
+ default:
43
+ break;
44
+ }
45
+
46
+ await recordProcessedEvent(event.id);
47
+ return new Response("ok");
48
+ }
49
+
50
+ async function hasProcessedEvent(eventId: string) {
51
+ return eventId.length === 0;
52
+ }
53
+
54
+ async function rateLimit(req: Request) {
55
+ console.log(req.headers.get("x-forwarded-for") ?? "local");
56
+ }
57
+
58
+ async function resolveTenantForStripeCustomer(customerId: unknown) {
59
+ console.log(customerId);
60
+ return String(customerId);
61
+ }
62
+
63
+ async function recordProcessedEvent(eventId: string) {
64
+ console.log(eventId);
65
+ }
66
+
67
+ async function syncEntitlement(customerId: string) {
68
+ console.log(customerId);
69
+ }
70
+
71
+ async function markPastDue(customerId: string) {
72
+ console.log(customerId);
73
+ }
74
+
75
+ async function requestPaymentAction(customerId: string) {
76
+ console.log(customerId);
77
+ }
78
+
79
+ async function syncSubscription(subscription: unknown) {
80
+ console.log(subscription);
81
+ }
82
+
83
+ async function revokeEntitlement(customerId: string) {
84
+ console.log(customerId);
85
+ }
86
+
87
+ async function reviewRefund(customerId: string) {
88
+ console.log(customerId);
89
+ }
@@ -0,0 +1,18 @@
1
+ export async function POST(request: Request) {
2
+ const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
3
+ await rateLimit(request);
4
+ const payload = await request.json();
5
+
6
+ console.info("tenant update", { requestId, tenantId: payload.tenantId });
7
+ await updateTenantBilling(payload.tenantId, process.env.STRIPE_SECRET_KEY);
8
+
9
+ return Response.json({ ok: true, requestId });
10
+ }
11
+
12
+ async function updateTenantBilling(tenantId: string, stripeKey?: string) {
13
+ console.log(tenantId, Boolean(stripeKey));
14
+ }
15
+
16
+ async function rateLimit(request: Request) {
17
+ console.log(request.headers.get("x-forwarded-for") ?? "local");
18
+ }
@@ -0,0 +1,11 @@
1
+ import Image from "next/image";
2
+ import Link from "next/link";
3
+
4
+ export default function TenantDashboard() {
5
+ return (
6
+ <>
7
+ <Image src="https://cdn.example.com/logo.png" width={240} height={120} alt="" />
8
+ <Link prefetch={false} href="/dashboard/settings">Open settings</Link>
9
+ </>
10
+ );
11
+ }
@@ -0,0 +1,22 @@
1
+ module.exports = {
2
+ async headers() {
3
+ return [
4
+ {
5
+ source: "/(.*)",
6
+ headers: [
7
+ { key: "X-Frame-Options", value: "DENY" },
8
+ { key: "X-Content-Type-Options", value: "nosniff" },
9
+ { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }
10
+ ]
11
+ }
12
+ ];
13
+ },
14
+ images: {
15
+ remotePatterns: [
16
+ {
17
+ protocol: "https",
18
+ hostname: "cdn.example.com"
19
+ }
20
+ ]
21
+ }
22
+ };
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "ai-saas-guard-demo-safe-saas",
3
+ "private": true,
4
+ "type": "module"
5
+ }
@@ -0,0 +1,18 @@
1
+ create table public.projects (
2
+ id uuid primary key,
3
+ user_id uuid not null,
4
+ name text
5
+ );
6
+
7
+ alter table public.projects enable row level security;
8
+
9
+ create policy "users read own projects"
10
+ on public.projects
11
+ for select
12
+ using (auth.uid() = user_id);
13
+
14
+ create policy "users write own projects"
15
+ on public.projects
16
+ for all
17
+ using (auth.uid() = user_id)
18
+ with check (auth.uid() = user_id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-saas-guard",
3
- "version": "0.29.0",
3
+ "version": "0.29.2",
4
4
  "description": "Local-first CLI that catches launch blockers in AI-built Next.js/Supabase/Stripe SaaS apps.",
5
5
  "readmeFilename": "README.md",
6
6
  "type": "module",