ai-saas-guard 0.10.0 → 0.12.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
@@ -1,11 +1,19 @@
1
1
  <h1 align="center">ai-saas-guard</h1>
2
2
 
3
3
  <p align="center">
4
- <strong>Local-first launch preflight for AI-built SaaS apps.</strong>
4
+ <strong>You used AI to build your SaaS. Now you need to know what is risky before launch.</strong>
5
5
  </p>
6
6
 
7
7
  <p align="center">
8
- Find the auth, billing, data-access, secret, MCP, and deploy surfaces a human should review before launch or merge.
8
+ ai-saas-guard points reviewers to the auth, billing, data access, secrets, MCP, and deploy changes that deserve human attention first. It runs locally, reads your repo only, and does not upload code.
9
+ </p>
10
+
11
+ <p align="center">
12
+ It is not a pentest. It is a practical review checklist for launch-risk hotspots.
13
+ </p>
14
+
15
+ <p align="center">
16
+ English | <a href="README.zh-CN.md">中文 README</a>
9
17
  </p>
10
18
 
11
19
  <p align="center">
@@ -18,13 +26,29 @@
18
26
 
19
27
  ---
20
28
 
21
- ## What It Does
29
+ ## The Problem It Solves
22
30
 
23
- `ai-saas-guard` is a command-line launch preflight for founders, solo builders, and reviewers shipping SaaS apps with AI coding tools.
31
+ AI can turn an idea into a working SaaS quickly. The harder question is whether the app is ready for real users.
24
32
 
25
- It answers one narrow question:
33
+ The risky parts are often not the obvious UI bugs. They are the small changes that decide who can see data, who gets paid access, where secrets are exposed, and what an AI tool is allowed to do:
26
34
 
27
- > What changed in auth, billing, data access, secrets, MCP tools, or deploy config that deserves human review first?
35
+ - Can one customer read another customer's data?
36
+ - Can a Stripe webhook grant access twice, miss a failed payment, or trust an unsigned request?
37
+ - Did a public environment variable expose a secret?
38
+ - Did an MCP tool get shell, database, or broad filesystem access?
39
+ - Did a pull request hide auth, billing, or deploy changes inside a large AI-generated diff?
40
+
41
+ `ai-saas-guard` is a local-first, review-first preflight for that moment. It does not try to prove your app is secure. It is not a pentest, certification, or full audit. It gives founders, solo builders, small teams, and reviewers a short, evidence-backed list of what to check before launch or merge.
42
+
43
+ ## What You Get
44
+
45
+ Run it against a repository or pull request and get findings with:
46
+
47
+ - the rule that matched
48
+ - severity and file evidence
49
+ - why the issue matters in a SaaS launch
50
+ - how to verify it manually
51
+ - a practical fix direction
28
52
 
29
53
  It is built for common AI-SaaS stacks:
30
54
 
@@ -35,8 +59,6 @@ It is built for common AI-SaaS stacks:
35
59
  - MCP server configuration
36
60
  - AI-generated pull requests with large mixed diffs
37
61
 
38
- It is intentionally evidence-first. Findings include a rule ID, severity, file evidence, why it matters, how to verify it, and a fix direction.
39
-
40
62
  ## Current Status
41
63
 
42
64
  This repository is public on GitHub.
@@ -51,8 +73,8 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
51
73
  | JSON and SARIF output | Available |
52
74
  | Composite GitHub Action | Available |
53
75
  | Project config | `.ai-saas-guard.json` rule toggles, severity overrides, and fail thresholds |
54
- | Versioned Action tags | `v0.10.0`, `v0` |
55
- | npm package | `ai-saas-guard@0.10.0` |
76
+ | Versioned Action tags | `v0.12.0`, `v0` |
77
+ | npm package | `ai-saas-guard@0.12.0` |
56
78
  | npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
57
79
 
58
80
  ## Quick Start
@@ -192,13 +214,13 @@ Hosted uninstall and data deletion behavior is documented in [docs/hosted-uninst
192
214
 
193
215
  Hosted pricing and packaging boundaries are documented in [docs/hosted-pricing-packaging.md](docs/hosted-pricing-packaging.md). Core local scanning stays useful without an account; hosted plans may add workflow convenience, saved reports, team policy, and optional human review, but they do not gate local CLI scanning.
194
216
 
195
- Hosted pre-implementation pure contracts are documented in [docs/hosted-preimplementation-contracts.md](docs/hosted-preimplementation-contracts.md). They cover queue-safe webhook event parsing, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, and other service-free helpers exported from `ai-saas-guard/hosted/contracts`.
217
+ Hosted pre-implementation pure contracts are documented in [docs/hosted-preimplementation-contracts.md](docs/hosted-preimplementation-contracts.md). They now include a pull request webhook intake planner that verifies signatures before parsing or queueing, plus a durable scan queue planner that reuses queued, running, and completed jobs for the same trusted scan key. They also cover queue-safe webhook event parsing, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, and other service-free helpers exported from `ai-saas-guard/hosted/contracts`.
196
218
 
197
219
  A public hosted compact report schema fixture is available at [examples/hosted-compact-report.json](examples/hosted-compact-report.json). It is synthetic and public-safe: compact evidence only, no raw source, raw diffs, secrets, webhook payload bodies, customer payloads, private URLs, or worker checkout paths.
198
220
 
199
221
  The proposed hosted app permission boundary is intentionally narrow: repository contents read, pull requests read, checks write, and metadata read for the first version. Optional PR comments require repository policy opt-in, and broad permissions such as administration, deployments, Actions write, and repository secrets are out of scope.
200
222
 
201
- The repository also includes pure pre-implementation hosted contract helpers and tests for webhook verification, installation token scoping, queue idempotency, compact reports, and retention limits. These helpers do not implement or deploy a hosted service.
223
+ The repository also includes pure pre-implementation hosted contract helpers and tests for webhook intake order, webhook verification, installation token scoping, durable queue idempotency, compact reports, and retention limits. These helpers do not implement or deploy a hosted service.
202
224
 
203
225
  Users should prefer the local CLI for private repositories, offline review, or no-account workflows where hosted code processing is not acceptable.
204
226
 
@@ -232,7 +254,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
232
254
 
233
255
  ## GitHub Action
234
256
 
235
- The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.10.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
257
+ The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.12.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
236
258
 
237
259
  ```yaml
238
260
  name: ai-saas-guard
@@ -0,0 +1,294 @@
1
+ <h1 align="center">ai-saas-guard</h1>
2
+
3
+ <p align="center">
4
+ <strong>你用 AI 把 SaaS 做出来了。现在要知道上线前哪里最容易出事。</strong>
5
+ </p>
6
+
7
+ <p align="center">
8
+ ai-saas-guard 会优先指出 auth、billing、data access、secrets、MCP 和 deploy 里最值得人工 review 的改动。它本地运行、只读仓库、不上传代码。
9
+ </p>
10
+
11
+ <p align="center">
12
+ 它不是渗透测试,而是一份面向上线风险点的实用 review 清单。
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="README.md">English README</a> | 中文
17
+ </p>
18
+
19
+ ---
20
+
21
+ ## 它解决什么问题
22
+
23
+ AI 能很快把一个 SaaS 从想法做成可运行的产品。真正难的是:它能不能放心给真实用户用。
24
+
25
+ 上线前最危险的通常不是界面小 bug,而是那些会影响用户数据、付费权限、密钥暴露和 AI 工具权限的小改动:
26
+
27
+ - 一个用户会不会看到另一个客户的数据?
28
+ - Stripe webhook 会不会重复开通权限、漏处理付款失败,或者信任未签名请求?
29
+ - `NEXT_PUBLIC_*` 里是不是不小心暴露了 secret?
30
+ - MCP 工具是不是拿到了 shell、数据库或过宽的文件系统权限?
31
+ - AI 生成的大 PR 里,是不是把 auth、billing 或 deploy 改动藏在 UI 调整中?
32
+
33
+ `ai-saas-guard` 是面向这个时刻的本地优先、review-first 上线预检工具。它不会证明你的应用绝对安全,也不是渗透测试、认证或完整安全审计。它的目标是给 founder、独立开发者、小团队和 reviewer 一份短而有证据的清单,告诉你上线或合并 PR 前最该先看哪里。
34
+
35
+ ## 你会得到什么
36
+
37
+ 对仓库或 PR 运行后,它会给出:
38
+
39
+ - 命中的 rule
40
+ - severity 和文件证据
41
+ - 为什么这个问题会影响 SaaS 上线
42
+ - 如何人工验证
43
+ - 实际修复方向
44
+
45
+ 它适合常见 AI 构建的 SaaS 技术栈:
46
+
47
+ - Next.js 和 Vercel
48
+ - Supabase RLS、storage policy、SQL migration
49
+ - Stripe checkout、subscription、webhook
50
+ - Prisma 或 SQL migration
51
+ - MCP server 配置
52
+ - AI 生成的大型混合 PR
53
+
54
+ ## 当前状态
55
+
56
+ 这个仓库是公开 GitHub 仓库。
57
+
58
+ CLI 已发布到 npm:`ai-saas-guard@0.12.0`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.12.0`。
59
+
60
+ | 模块 | 状态 |
61
+ | --- | --- |
62
+ | 公开 GitHub 仓库 | 已可用 |
63
+ | npm CLI | 已发布为 `ai-saas-guard` |
64
+ | 本地源码运行 | 已可用 |
65
+ | JSON 和 SARIF 输出 | 已可用 |
66
+ | Markdown PR summary | 已可用 |
67
+ | GitHub Action | 已可用 |
68
+ | 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖和 fail threshold |
69
+ | 当前版本 | `0.12.0` |
70
+ | Action 标签 | `v0.12.0`、`v0` |
71
+ | npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
72
+
73
+ ## 快速开始
74
+
75
+ 无需全局安装,直接运行:
76
+
77
+ ```bash
78
+ npx ai-saas-guard@latest scan --root /path/to/your-saas
79
+ ```
80
+
81
+ 运行专项检查:
82
+
83
+ ```bash
84
+ npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main
85
+ npx ai-saas-guard@latest check-supabase --root /path/to/your-saas
86
+ npx ai-saas-guard@latest check-stripe --root /path/to/your-saas
87
+ npx ai-saas-guard@latest check-mcp --root /path/to/your-saas
88
+ ```
89
+
90
+ 机器可读输出:
91
+
92
+ ```bash
93
+ npx ai-saas-guard@latest scan --root /path/to/your-saas --json
94
+ npx ai-saas-guard@latest scan --root /path/to/your-saas --sarif > ai-saas-guard.sarif
95
+ npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --markdown > ai-saas-guard-pr.md
96
+ ```
97
+
98
+ 本地开发:
99
+
100
+ ```bash
101
+ git clone https://github.com/zr9959/ai-saas-guard.git
102
+ cd ai-saas-guard
103
+ npm ci
104
+ npm run build
105
+ node dist/cli.js scan --root /path/to/your-saas
106
+ ```
107
+
108
+ ## 主要命令
109
+
110
+ | 命令 | 用途 |
111
+ | --- | --- |
112
+ | `scan` | 对 secrets、Stripe、Supabase、MCP、API routes、deploy config 做整体上线预检 |
113
+ | `pr-risk` | 分析当前 git diff 或指定 base branch diff,判断哪些文件和风险面应该先 review |
114
+ | `check-supabase` | 检查 migration 和 policy 文件里的 RLS、ownership、storage policy 风险 |
115
+ | `check-stripe` | 检查 webhook 签名、raw body、幂等、订阅生命周期和 entitlement 更新路径 |
116
+ | `check-mcp` | 检查 MCP 配置里的 secret、非 localhost 绑定、shell/db/filesystem 等副作用 |
117
+
118
+ ## 它会检查什么
119
+
120
+ | 风险面 | 例子 |
121
+ | --- | --- |
122
+ | Secrets 和 env | 类似密钥的字符串、危险的 `NEXT_PUBLIC_*` 暴露 |
123
+ | Stripe | webhook 缺失、未验证签名、raw body 签名风险、缺幂等、缺失败/取消/退款/更新处理 |
124
+ | Supabase | 敏感表没启用 RLS、policy 过宽、缺少 ownership filter、`WITH CHECK` 过弱、storage object policy 过宽 |
125
+ | API routes | 有 auth 但缺少明显 ownership guard,敏感 mutation route 缺少 rate-limit 提示 |
126
+ | MCP | 明文 secret、非 localhost 绑定、过宽文件系统权限、shell 工具、raw SQL 工具 |
127
+ | Deploy config | Next static export 和 API route 冲突、Edge runtime 使用 Node-only API、关键 env 文档缺失 |
128
+ | PR risk | auth、billing、RLS、env、deploy、API、storage、测试删除、大型混合 diff |
129
+
130
+ 完整规则请看 [docs/rules.md](docs/rules.md)。
131
+
132
+ ## PR 风险分流
133
+
134
+ `scan` 可以扫整个仓库,但这个项目更锋利的入口是 PR review。
135
+
136
+ AI 生成的 PR 经常把很多东西混在一起:
137
+
138
+ - UI 调整
139
+ - auth/session 改动
140
+ - database migration
141
+ - Stripe checkout 或 webhook 改动
142
+ - Supabase policy
143
+ - Vercel 配置
144
+ - 测试被删除或削弱
145
+
146
+ `pr-risk` 会输出:
147
+
148
+ - 最应该先 review 的文件
149
+ - PR 触碰到的敏感类别
150
+ - review-first checklist
151
+ - 建议拆分 PR 的方向
152
+ - 必要测试或人工验证步骤
153
+ - 当 base ref 或 shallow checkout 导致无法比较时,给出明确诊断
154
+
155
+ ```bash
156
+ node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --json
157
+ node dist/cli.js pr-risk --root /path/to/your-saas --base origin/main --markdown
158
+ ```
159
+
160
+ ## 目标用户
161
+
162
+ 这个工具主要面向:
163
+
164
+ - 用 AI 编程工具快速做 SaaS MVP 的 founder 或独立开发者
165
+ - 没有专职安全工程师的小团队
166
+ - 需要 review 大型 AI-generated PR 的 reviewer
167
+ - 给客户交付 SaaS 的开发者或 agency
168
+ - 希望在 CI 里加一道轻量上线预检的小团队
169
+
170
+ 它的目标不是吓人,也不是制造大量噪音,而是帮你把 review 时间用在最值得看的地方。
171
+
172
+ ## GitHub Action
173
+
174
+ 可以在 GitHub Actions 里直接使用:
175
+
176
+ ```yaml
177
+ name: ai-saas-guard
178
+
179
+ on:
180
+ pull_request:
181
+
182
+ permissions:
183
+ contents: read
184
+
185
+ jobs:
186
+ preflight:
187
+ runs-on: ubuntu-latest
188
+ steps:
189
+ - uses: actions/checkout@v6.0.2
190
+ with:
191
+ fetch-depth: 0
192
+ - uses: zr9959/ai-saas-guard@v0
193
+ with:
194
+ command: pr-risk
195
+ root: ${{ github.workspace }}
196
+ base: origin/main
197
+ fail-on: high
198
+ config: .ai-saas-guard.json
199
+ ```
200
+
201
+ 更多 GitHub Action 示例请看 [docs/github-action.md](docs/github-action.md)。
202
+
203
+ ## 项目配置
204
+
205
+ 在仓库根目录添加 `.ai-saas-guard.json` 可以调整规则:
206
+
207
+ ```json
208
+ {
209
+ "failOn": "high",
210
+ "rules": {
211
+ "stripe.webhook.missing-signature": "off",
212
+ "stripe.webhook.missing-idempotency": "critical",
213
+ "deploy.env.example-missing": "info"
214
+ },
215
+ "suppressions": [
216
+ {
217
+ "ruleId": "stripe.webhook.missing-idempotency",
218
+ "paths": ["app/api/stripe/webhook/route.ts"],
219
+ "reason": "Temporary launch exception with duplicate-event coverage in integration tests."
220
+ }
221
+ ]
222
+ }
223
+ ```
224
+
225
+ `rules` 可以关闭规则或覆盖 severity。`suppressions` 适合处理某个具体路径上的 false positive。`failOn` 用于设置 CI 失败阈值。
226
+
227
+ ## 隐私模型
228
+
229
+ `ai-saas-guard` 设计上适合在私有本地仓库中运行。
230
+
231
+ - 本地运行
232
+ - 读取仓库文件和 git diff
233
+ - scan 命令无网络调用
234
+ - 不上传代码
235
+ - 不需要账号或登录
236
+ - 不修改被扫描仓库
237
+ - 对类似 secret 的 evidence 做 redaction
238
+
239
+ ## Hosted GitHub App 设计
240
+
241
+ 当前仓库已经包含未来 Hosted GitHub App 的设计文档和纯契约测试,但还没有部署真实 hosted 服务。
242
+
243
+ 相关文档:
244
+
245
+ - [docs/github-app-design.md](docs/github-app-design.md)
246
+ - [docs/hosted-first-service-slice.md](docs/hosted-first-service-slice.md)
247
+ - [docs/hosted-deployment-model.md](docs/hosted-deployment-model.md)
248
+ - [docs/hosted-operational-release-gate.md](docs/hosted-operational-release-gate.md)
249
+ - [docs/hosted-uninstall-data-deletion.md](docs/hosted-uninstall-data-deletion.md)
250
+ - [docs/hosted-pricing-packaging.md](docs/hosted-pricing-packaging.md)
251
+ - [docs/hosted-preimplementation-contracts.md](docs/hosted-preimplementation-contracts.md)
252
+
253
+ 已经实现的 hosted 预实现纯契约包括:
254
+
255
+ - pull request webhook intake planner:先验签,再解析 payload、生成可信 identity、校验 selected-repository scope,并默认只走 check-run-only 输出
256
+ - durable scan queue planner:同一个 trusted scan key 的 queued/running/completed job 会复用,不重复排 worker,也不会把源码、diff、secret 或 PR 正文放进队列 payload
257
+ - webhook event parser
258
+ - check-run summary renderer
259
+ - queue cleanup planner
260
+ - worker checkout cleanup planner
261
+ - hosted compact report fixture:[examples/hosted-compact-report.json](examples/hosted-compact-report.json)
262
+
263
+ 这些 helper 不会启动服务、不会调用 GitHub API、不会请求 installation token、不会写 check run,也不会上传源码。
264
+
265
+ ## 它不是什么
266
+
267
+ 这个项目刻意避免过度安全承诺。
268
+
269
+ - 不是渗透测试
270
+ - 不是完整 SAST 平台
271
+ - 不能证明你的应用绝对安全
272
+ - 不能替代两账号权限测试
273
+ - 不执行 Stripe、Supabase、Vercel 或浏览器流程
274
+ - 不检查没有体现在本地文件里的生产设置
275
+ - 不替代 Semgrep、Gitleaks、TruffleHog、Bearer、CodeQL 或人工 review
276
+
277
+ 正确使用方式是:把它当成上线前和 PR review 前的 preflight,帮助你决定应该先把人工注意力放在哪里。
278
+
279
+ ## 开发
280
+
281
+ ```bash
282
+ npm ci
283
+ npm test
284
+ npm run build
285
+ node dist/cli.js scan --root .
286
+ ```
287
+
288
+ 发布 CLI、GitHub Action、npm package 或任何公开仓库更新前,必须按照 [docs/release-quality-knowledge-base.md](docs/release-quality-knowledge-base.md) 的 release gate 执行。
289
+
290
+ 以后更新英文 `README.md` 时,也要同步检查并更新本中文 `README.zh-CN.md`。
291
+
292
+ ## 安全报告
293
+
294
+ 报告漏洞前请阅读 [SECURITY.md](SECURITY.md)。不要在公开 issue 中发布真实 API key、客户数据、私有源码或生产 URL。
@@ -46,6 +46,43 @@ export interface HostedPullRequestEventDecision {
46
46
  action?: string;
47
47
  identity?: HostedScanIdentity;
48
48
  }
49
+ export type HostedPullRequestWebhookIntakeStage = "signature" | "payload" | "event" | "installation_scope" | "queue";
50
+ export type HostedPullRequestWebhookIntakeRejectReason = WebhookRejectReason | PullRequestEventRejectReason | InstallationScopeRejectReason | "invalid_json" | "missing_delivery_id";
51
+ export interface HostedPullRequestWebhookIntakeInput {
52
+ payload: string | Buffer;
53
+ signatureHeader?: string;
54
+ signingKey: string | Buffer;
55
+ deliveryId?: string;
56
+ seenDeliveryIds?: Set<string>;
57
+ scannerVersion: string;
58
+ selectedRepositoryIds: number[];
59
+ removedRepositoryIds?: number[];
60
+ queue: Map<string, HostedScanJobState>;
61
+ allowDraft?: boolean;
62
+ supportedActions?: string[];
63
+ manualRerun?: boolean;
64
+ }
65
+ export interface HostedPullRequestWebhookIntakeDecision {
66
+ accepted: boolean;
67
+ stage: HostedPullRequestWebhookIntakeStage;
68
+ reason?: HostedPullRequestWebhookIntakeRejectReason;
69
+ action?: string;
70
+ deliveryId?: string;
71
+ identity?: HostedScanIdentity;
72
+ job?: HostedScanJobDecision;
73
+ shouldQueueScanJob: boolean;
74
+ shouldFetchRepository: boolean;
75
+ shouldCreateCheckRun: boolean;
76
+ shouldCreatePrComment: false;
77
+ privacy: {
78
+ includesRawWebhookPayload: false;
79
+ includesUntrustedPrText: false;
80
+ includesRawSource: false;
81
+ includesRawDiffs: false;
82
+ includesSecrets: false;
83
+ includesCustomerPayloads: false;
84
+ };
85
+ }
49
86
  export type InstallationScopeRejectReason = "installation_mismatch" | "repository_not_installed" | "repository_removed_from_installation";
50
87
  export interface InstallationScopeInput {
51
88
  identity: HostedScanIdentity;
@@ -77,6 +114,57 @@ export interface HostedScanJobDecision {
77
114
  shouldCreatePrComment: boolean;
78
115
  }
79
116
  export type HostedQueueJobStatus = "queued" | "running" | "completed" | "failed" | "cancelled";
117
+ export interface HostedScanQueueRecord {
118
+ key: string;
119
+ identity: HostedScanIdentity;
120
+ status: HostedQueueJobStatus;
121
+ attempt: number;
122
+ deliveryIds: string[];
123
+ createdAt: string;
124
+ updatedAt: string;
125
+ reportId?: string;
126
+ }
127
+ export interface HostedScanQueuePayload {
128
+ key: string;
129
+ identity: HostedScanIdentity;
130
+ deliveryId: string;
131
+ attempt: number;
132
+ requestedAt: string;
133
+ source: "github_pull_request";
134
+ }
135
+ export interface HostedScanQueueUpsertInput {
136
+ identity: HostedScanIdentity;
137
+ deliveryId: string;
138
+ requestedAt: string;
139
+ queue: Map<string, HostedScanQueueRecord>;
140
+ manualRerun?: boolean;
141
+ rawSource?: string;
142
+ rawDiff?: string;
143
+ secretValues?: string[];
144
+ untrustedPrText?: string;
145
+ customerPayload?: unknown;
146
+ }
147
+ export interface HostedScanQueueUpsertDecision {
148
+ key: string;
149
+ idempotent: true;
150
+ created: boolean;
151
+ reusedExistingJob: boolean;
152
+ existingStatus?: HostedQueueJobStatus;
153
+ attempt: number;
154
+ queueRecord: HostedScanQueueRecord;
155
+ queuePayload: HostedScanQueuePayload;
156
+ shouldEnqueueWorker: boolean;
157
+ shouldReuseCompletedReport: boolean;
158
+ shouldCreateCheckRun: boolean;
159
+ shouldCreatePrComment: false;
160
+ privacy: {
161
+ includesRawSource: false;
162
+ includesRawDiffs: false;
163
+ includesSecrets: false;
164
+ includesUntrustedPrText: false;
165
+ includesCustomerPayloads: false;
166
+ };
167
+ }
80
168
  export type HostedQueueCleanupTrigger = "repository_removed" | "installation_deleted" | "repeated_cleanup";
81
169
  export interface HostedQueueCleanupJobState {
82
170
  key: string;
@@ -260,11 +348,13 @@ export declare const HOSTED_PRIVACY_DEFAULTS: {
260
348
  readonly deleteWorkerCheckout: "after_scan_completion";
261
349
  };
262
350
  export declare function verifyGitHubWebhook(input: GitHubWebhookInput): GitHubWebhookDecision;
351
+ export declare function planHostedPullRequestWebhookIntake(input: HostedPullRequestWebhookIntakeInput): HostedPullRequestWebhookIntakeDecision;
263
352
  export declare function buildHostedScanIdentity(input: HostedScanIdentityInput): HostedScanIdentity;
264
353
  export declare function parseHostedPullRequestEvent(input: HostedPullRequestEventInput): HostedPullRequestEventDecision;
265
354
  export declare function authorizeInstallationTokenScope(input: InstallationScopeInput): InstallationScopeDecision;
266
355
  export declare function getHostedScanIdempotencyKey(identity: HostedScanIdentity): string;
267
356
  export declare function upsertHostedScanJob(queue: Map<string, HostedScanJobState>, input: HostedScanJobInput): HostedScanJobDecision;
357
+ export declare function planHostedScanQueueUpsert(input: HostedScanQueueUpsertInput): HostedScanQueueUpsertDecision;
268
358
  export declare function getHostedQueueCleanupIdempotencyKey(input: {
269
359
  trigger: HostedQueueCleanupTrigger;
270
360
  installationId: number;
@@ -42,6 +42,65 @@ export function verifyGitHubWebhook(input) {
42
42
  deliveryId
43
43
  };
44
44
  }
45
+ export function planHostedPullRequestWebhookIntake(input) {
46
+ const signatureDecision = verifyGitHubWebhook({
47
+ payload: input.payload,
48
+ signatureHeader: input.signatureHeader,
49
+ signingKey: input.signingKey,
50
+ deliveryId: input.deliveryId,
51
+ seenDeliveryIds: input.seenDeliveryIds
52
+ });
53
+ if (!signatureDecision.accepted) {
54
+ return rejectPullRequestWebhookIntake("signature", signatureDecision.reason ?? "invalid_signature", input.deliveryId);
55
+ }
56
+ if (!input.deliveryId) {
57
+ return rejectPullRequestWebhookIntake("event", "missing_delivery_id");
58
+ }
59
+ const payload = parseJsonPayload(input.payload);
60
+ if (!payload) {
61
+ return rejectPullRequestWebhookIntake("payload", "invalid_json", input.deliveryId);
62
+ }
63
+ const eventDecision = parseHostedPullRequestEvent({
64
+ payload,
65
+ scannerVersion: input.scannerVersion,
66
+ allowDraft: input.allowDraft,
67
+ supportedActions: input.supportedActions
68
+ });
69
+ if (!eventDecision.accepted || !eventDecision.identity) {
70
+ return rejectPullRequestWebhookIntake("event", eventDecision.reason ?? "missing_required_field", input.deliveryId, eventDecision.action);
71
+ }
72
+ const scopeDecision = authorizeInstallationTokenScope({
73
+ identity: eventDecision.identity,
74
+ installationId: eventDecision.identity.installationId,
75
+ selectedRepositoryIds: input.selectedRepositoryIds,
76
+ removedRepositoryIds: input.removedRepositoryIds
77
+ });
78
+ if (!scopeDecision.authorized) {
79
+ return rejectPullRequestWebhookIntake("installation_scope", scopeDecision.reason ?? "repository_not_installed", input.deliveryId, eventDecision.action, eventDecision.identity);
80
+ }
81
+ const queueDecision = upsertHostedScanJob(input.queue, {
82
+ identity: eventDecision.identity,
83
+ deliveryId: input.deliveryId,
84
+ manualRerun: input.manualRerun
85
+ });
86
+ const checkRunOnlyQueueDecision = {
87
+ ...queueDecision,
88
+ shouldCreatePrComment: false
89
+ };
90
+ return {
91
+ accepted: true,
92
+ stage: "queue",
93
+ action: eventDecision.action,
94
+ deliveryId: input.deliveryId,
95
+ identity: eventDecision.identity,
96
+ job: checkRunOnlyQueueDecision,
97
+ shouldQueueScanJob: queueDecision.created || input.manualRerun === true,
98
+ shouldFetchRepository: queueDecision.shouldCreateCheckRun,
99
+ shouldCreateCheckRun: queueDecision.shouldCreateCheckRun,
100
+ shouldCreatePrComment: false,
101
+ privacy: hostedWebhookIntakePrivacy()
102
+ };
103
+ }
45
104
  export function buildHostedScanIdentity(input) {
46
105
  return {
47
106
  installationId: input.installationId,
@@ -145,7 +204,7 @@ export function upsertHostedScanJob(queue, input) {
145
204
  reusedExistingReport: false,
146
205
  attempt: 1,
147
206
  shouldCreateCheckRun: true,
148
- shouldCreatePrComment: true
207
+ shouldCreatePrComment: false
149
208
  };
150
209
  }
151
210
  if (!existing.deliveryIds.includes(input.deliveryId)) {
@@ -163,6 +222,76 @@ export function upsertHostedScanJob(queue, input) {
163
222
  shouldCreatePrComment: false
164
223
  };
165
224
  }
225
+ export function planHostedScanQueueUpsert(input) {
226
+ const key = getHostedScanIdempotencyKey(input.identity);
227
+ const existing = input.queue.get(key);
228
+ if (!existing) {
229
+ const record = {
230
+ key,
231
+ identity: input.identity,
232
+ status: "queued",
233
+ attempt: 1,
234
+ deliveryIds: [input.deliveryId],
235
+ createdAt: input.requestedAt,
236
+ updatedAt: input.requestedAt
237
+ };
238
+ input.queue.set(key, record);
239
+ return {
240
+ key,
241
+ idempotent: true,
242
+ created: true,
243
+ reusedExistingJob: false,
244
+ attempt: record.attempt,
245
+ queueRecord: { ...record, deliveryIds: [...record.deliveryIds] },
246
+ queuePayload: createHostedScanQueuePayload(record, input.deliveryId, input.requestedAt),
247
+ shouldEnqueueWorker: true,
248
+ shouldReuseCompletedReport: false,
249
+ shouldCreateCheckRun: true,
250
+ shouldCreatePrComment: false,
251
+ privacy: hostedScanQueuePrivacy()
252
+ };
253
+ }
254
+ if (!existing.deliveryIds.includes(input.deliveryId)) {
255
+ existing.deliveryIds.push(input.deliveryId);
256
+ }
257
+ const existingStatus = existing.status;
258
+ if (input.manualRerun) {
259
+ existing.attempt += 1;
260
+ existing.status = "queued";
261
+ existing.updatedAt = input.requestedAt;
262
+ return {
263
+ key,
264
+ idempotent: true,
265
+ created: false,
266
+ reusedExistingJob: false,
267
+ existingStatus,
268
+ attempt: existing.attempt,
269
+ queueRecord: cloneHostedScanQueueRecord(existing),
270
+ queuePayload: createHostedScanQueuePayload(existing, input.deliveryId, input.requestedAt),
271
+ shouldEnqueueWorker: true,
272
+ shouldReuseCompletedReport: false,
273
+ shouldCreateCheckRun: true,
274
+ shouldCreatePrComment: false,
275
+ privacy: hostedScanQueuePrivacy()
276
+ };
277
+ }
278
+ existing.updatedAt = input.requestedAt;
279
+ return {
280
+ key,
281
+ idempotent: true,
282
+ created: false,
283
+ reusedExistingJob: true,
284
+ existingStatus,
285
+ attempt: existing.attempt,
286
+ queueRecord: cloneHostedScanQueueRecord(existing),
287
+ queuePayload: createHostedScanQueuePayload(existing, input.deliveryId, input.requestedAt),
288
+ shouldEnqueueWorker: false,
289
+ shouldReuseCompletedReport: existingStatus === "completed",
290
+ shouldCreateCheckRun: false,
291
+ shouldCreatePrComment: false,
292
+ privacy: hostedScanQueuePrivacy()
293
+ };
294
+ }
166
295
  export function getHostedQueueCleanupIdempotencyKey(input) {
167
296
  return ["queue-cleanup", input.trigger, input.installationId, input.repositoryId ?? "all"].join(":");
168
297
  }
@@ -357,6 +486,65 @@ function rejectInstallationScope(reason) {
357
486
  reason
358
487
  };
359
488
  }
489
+ function rejectPullRequestWebhookIntake(stage, reason, deliveryId, action, identity) {
490
+ return {
491
+ accepted: false,
492
+ stage,
493
+ reason,
494
+ ...(deliveryId === undefined ? {} : { deliveryId }),
495
+ ...(action === undefined ? {} : { action }),
496
+ ...(identity === undefined ? {} : { identity }),
497
+ shouldQueueScanJob: false,
498
+ shouldFetchRepository: false,
499
+ shouldCreateCheckRun: false,
500
+ shouldCreatePrComment: false,
501
+ privacy: hostedWebhookIntakePrivacy()
502
+ };
503
+ }
504
+ function hostedWebhookIntakePrivacy() {
505
+ return {
506
+ includesRawWebhookPayload: false,
507
+ includesUntrustedPrText: false,
508
+ includesRawSource: false,
509
+ includesRawDiffs: false,
510
+ includesSecrets: false,
511
+ includesCustomerPayloads: false
512
+ };
513
+ }
514
+ function createHostedScanQueuePayload(record, deliveryId, requestedAt) {
515
+ return {
516
+ key: record.key,
517
+ identity: record.identity,
518
+ deliveryId,
519
+ attempt: record.attempt,
520
+ requestedAt,
521
+ source: "github_pull_request"
522
+ };
523
+ }
524
+ function cloneHostedScanQueueRecord(record) {
525
+ return {
526
+ ...record,
527
+ identity: { ...record.identity },
528
+ deliveryIds: [...record.deliveryIds]
529
+ };
530
+ }
531
+ function hostedScanQueuePrivacy() {
532
+ return {
533
+ includesRawSource: false,
534
+ includesRawDiffs: false,
535
+ includesSecrets: false,
536
+ includesUntrustedPrText: false,
537
+ includesCustomerPayloads: false
538
+ };
539
+ }
540
+ function parseJsonPayload(payload) {
541
+ try {
542
+ return JSON.parse(Buffer.isBuffer(payload) ? payload.toString("utf8") : payload);
543
+ }
544
+ catch {
545
+ return undefined;
546
+ }
547
+ }
360
548
  function queueCleanupMatches(job, input, scope) {
361
549
  if (job.identity.installationId !== input.installationId) {
362
550
  return false;
@@ -2,7 +2,7 @@
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.10.0` 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.12.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
6
6
 
7
7
  ## PR Summary
8
8
 
@@ -4,6 +4,49 @@ This document collects pure hosted contracts that can be tested before any hoste
4
4
 
5
5
  The helpers live in `src/hosted/contracts.ts` and are exported from `ai-saas-guard/hosted/contracts`.
6
6
 
7
+ ## Pull Request Webhook Intake Planner
8
+
9
+ The pull request webhook intake planner is the first pure implementation slice for the future hosted service. It composes the earlier contracts into one safe order without starting a server or calling GitHub APIs.
10
+
11
+ Default behavior:
12
+
13
+ - verify `X-Hub-Signature-256` before parsing JSON, queueing work, authorizing token scope, or planning repository fetches
14
+ - reject invalid, missing, malformed, or replayed signatures before payload parsing
15
+ - parse only signed pull request payloads
16
+ - derive scan identity from trusted GitHub event fields through the webhook event parser
17
+ - authorize selected-repository installation scope before any fetch is planned
18
+ - upsert one idempotent scan job by installation, repository, pull request, head SHA, and scanner version
19
+ - default to check-run-only output; PR comments remain disabled for the first hosted slice
20
+
21
+ Privacy boundaries:
22
+
23
+ - return only trusted identity, queue metadata, stage, reason, and booleans needed by an ingress or worker
24
+ - do not return raw webhook payloads, untrusted PR text, raw source, raw diffs, secrets, or customer payloads
25
+ - keep local CLI usage independent from the hosted service
26
+
27
+ The exported helper is `planHostedPullRequestWebhookIntake`. It is intentionally service-free: callers still need a real webhook server, queue provider, installation token lookup, worker checkout, scanner execution, compact report storage, and GitHub Checks API writer before any hosted environment exists.
28
+
29
+ ## Durable Scan Queue Planner
30
+
31
+ The durable scan queue planner defines how the future hosted service should create or reuse scan jobs once a signed pull request webhook has produced trusted scan identity. It is a pure planner only: it does not connect to a queue provider, run a worker, fetch source, write reports, or call GitHub APIs.
32
+
33
+ Default behavior:
34
+
35
+ - compute the same logical scan key from installation ID, repository ID, pull request number, head SHA, and scanner version
36
+ - create one queued job when no matching job exists
37
+ - reuse existing queued, running, or completed jobs for duplicate deliveries
38
+ - reuse completed compact reports instead of enqueueing duplicate worker work
39
+ - allow manual reruns to increment `attempt` while keeping the same logical scan key
40
+ - keep the first hosted slice check-run-only; PR comments remain disabled
41
+
42
+ Queue payload boundaries:
43
+
44
+ - include only scan identity, job key, delivery ID, attempt, requested time, and source
45
+ - do not include raw source, raw diffs, secret values, untrusted PR text, webhook payload bodies, customer payloads, private URLs, or worker checkout paths
46
+ - return safe queue metadata that can be stored by a durable queue without leaking source code
47
+
48
+ The exported helper is `planHostedScanQueueUpsert`. It is intended to be the queue-provider-independent contract for the first real hosted queue implementation.
49
+
7
50
  ## Webhook Event Parser
8
51
 
9
52
  The webhook event parser runs after webhook signature verification. It converts a reduced GitHub `pull_request` webhook payload into a queue-safe scan request identity.
@@ -139,6 +182,13 @@ These contracts do not:
139
182
 
140
183
  Automated tests must cover:
141
184
 
185
+ - signed pull request webhook intake verifies signatures before JSON parsing or queueing
186
+ - accepted pull request webhook intake queues one check-run-only scan request from trusted fields
187
+ - rejected installation scope stops before repository fetch planning
188
+ - durable scan queue planning creates one queued job for a new trusted scan key
189
+ - duplicate deliveries reuse queued, running, and completed jobs without enqueueing duplicate worker work
190
+ - completed duplicate jobs reuse compact reports
191
+ - manual reruns increment attempt without changing the logical scan key
142
192
  - accepted pull request events build the expected trusted scan identity
143
193
  - unsupported actions are rejected
144
194
  - draft pull requests are rejected by default
@@ -5,10 +5,10 @@
5
5
  ## Current State
6
6
 
7
7
  - Package name: `ai-saas-guard`
8
- - Current version: `0.10.0`
8
+ - Current version: `0.12.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.10.0`
11
+ - GitHub Release: `v0.12.0`
12
12
  - Publish workflow: `.github/workflows/npm-publish.yml`
13
13
  - Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
14
14
  - Long-lived npm publish token: not required
@@ -17,7 +17,7 @@
17
17
 
18
18
  Use GitHub Actions with npm Trusted Publisher/OIDC:
19
19
 
20
- 1. Create and review a release tag such as `v0.10.0`.
20
+ 1. Create and review a release tag such as `v0.12.0`.
21
21
  2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
22
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
23
  4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
@@ -57,9 +57,9 @@ Implemented surfaces:
57
57
  - hosted operational release gate document requiring hosted CI, webhook replay, dependency and container scanning, privacy and retention verification, worker cleanup, monitoring and alerting, manual rollback, and incident response evidence before exposure
58
58
  - hosted uninstall and data deletion document defining repository removal, full app uninstall, compact report deletion, queue cancellation, audit record retention, repeated cleanup idempotency, and user-facing deletion wording
59
59
  - hosted pricing and packaging document defining open-source CLI boundaries, free/public repo hosted behavior, private repo hosted behavior, PR comments, saved reports, team policy, optional Launch Review, and no pentest/certification/full-audit claims
60
- - hosted pre-implementation contracts document, hosted compact report fixture, and pure helpers for queue-safe pull request event parsing from trusted GitHub event fields, bounded check-run summary rendering, idempotent queue cleanup planning, and worker checkout cleanup planning
60
+ - hosted pre-implementation contracts document, hosted compact report fixture, and pure helpers for pull request webhook intake planning, durable scan queue upsert planning, queue-safe pull request event parsing from trusted GitHub event fields, bounded check-run summary rendering, idempotent queue cleanup planning, and worker checkout cleanup planning
61
61
  - implementation-ready hosted GitHub App permission contract for required permissions, optional PR comment permissions, selected repository installation, and out-of-scope broad permissions
62
- - pure hosted GitHub App contract helpers and tests for webhook verification, installation token scoping, scan queue idempotency, compact reports, and retention limits
62
+ - pure hosted GitHub App contract helpers and tests for webhook intake order, webhook verification, installation token scoping, durable scan queue idempotency, compact reports, and retention limits
63
63
  - GitHub issue templates for bug reports, false positives, false negatives, rule requests, and public-safe security reports
64
64
  - CODEOWNERS for source, tests, docs, workflows, Action, and package metadata
65
65
  - JSON output
@@ -113,7 +113,8 @@ GitHub Project:
113
113
 
114
114
  Current issue set:
115
115
 
116
- - No open roadmap issues after the hosted pre-implementation contract batch.
116
+ - Closed hosted MVP issue: #24 webhook intake.
117
+ - Open hosted MVP roadmap issues: #25 idempotent queue contract, #26 read-only worker checkout, #27 Check summaries, #28 retention/uninstall cleanup, and #29 hosted operational release gate.
117
118
 
118
119
  CI:
119
120
 
@@ -125,7 +126,7 @@ CI:
125
126
  Publishing:
126
127
 
127
128
  - npm package: `ai-saas-guard`
128
- - Current release line: `v0.10.0`
129
+ - Current release line: `v0.12.0`
129
130
  - Publish workflow: `.github/workflows/npm-publish.yml`
130
131
  - Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
131
132
  - Long-lived npm publish tokens should not be required.
@@ -137,6 +138,7 @@ Allowed in this public repository:
137
138
  - CLI source code
138
139
  - tests and intentionally vulnerable fixtures
139
140
  - public docs
141
+ - English README and Chinese README; when `README.md` changes, review and update `README.zh-CN.md` in the same change
140
142
  - GitHub Action wrapper
141
143
  - examples that contain only inert fake data
142
144
  - release-quality process docs
@@ -235,6 +235,7 @@ P0:
235
235
  - `types` points to generated declaration files if package exports TypeScript API.
236
236
  - `exports` is accurate.
237
237
  - README install examples match the published package name.
238
+ - If `README.md` changes, `README.zh-CN.md` must be reviewed and updated or explicitly confirmed still current.
238
239
  - Version follows semver.
239
240
  - Release notes state breaking changes, new checks, false-positive changes, and migration notes.
240
241
  - Publish with npm trusted publishing/OIDC when possible.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-saas-guard",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",
@@ -52,7 +52,8 @@
52
52
  "docs",
53
53
  "dist",
54
54
  "examples",
55
- "README.md"
55
+ "README.md",
56
+ "README.zh-CN.md"
56
57
  ],
57
58
  "license": "MIT",
58
59
  "devDependencies": {