ai-saas-guard 0.27.2 → 0.28.1

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 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
+ 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.
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -41,6 +41,25 @@ AI can make a SaaS look finished while the real launch blockers sit in trust-bou
41
41
 
42
42
  `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.
43
43
 
44
+ ## See The Output
45
+
46
+ The report is designed to be read before launch or before merging an AI-heavy PR:
47
+
48
+ ```text
49
+ Launch Gate: review before launch
50
+ 4 findings: 1 high, 3 medium
51
+
52
+ HIGH stripe.webhook.missing-signature
53
+ File: app/api/stripe/webhook/route.ts
54
+ Why: billing access can be granted from a webhook path that does not verify Stripe signatures.
55
+ Verify: replay a webhook with an invalid signature and confirm the route rejects it.
56
+ Fix: read the raw body, call stripe.webhooks.constructEvent, and make event handling idempotent.
57
+
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.
61
+ ```
62
+
44
63
  ## What You Get
45
64
 
46
65
  One command returns a launch-readiness report with:
@@ -73,16 +92,16 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
73
92
  | Area | Status |
74
93
  | --- | --- |
75
94
  | Public GitHub repository | Available |
76
- | npm CLI | `ai-saas-guard@0.27.2` |
77
- | GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.27.2` |
95
+ | npm CLI | `ai-saas-guard@0.28.1` |
96
+ | GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.28.1` |
78
97
  | Outputs | Terminal, JSON, SARIF, and PR-focused markdown |
79
98
  | Project config | `.ai-saas-guard.json` rule toggles, severity overrides, suppressions, and fail thresholds |
80
99
  | Privacy model | Local-first, read-only scan commands, no LLM calls, no code upload |
81
- | Versioned Action tags | `v0.27.2`, `v0` |
82
- | Current release | `0.27.2` npm README metadata fix; launch-gate report summary remains current |
100
+ | Versioned Action tags | `v0.28.1`, `v0` |
101
+ | Current release | `0.28.1` discoverability polish, clearer first-screen output example, npm metadata sync, and hosted worker release line preservation |
83
102
  | npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
84
103
  | Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
85
- | Cloudflare hosted ingress | Deployed at `https://ai-saas-guard-hosted.zr9959.workers.dev`; Worker health and Check Run publisher configuration are live, but end-to-end GitHub App webhook delivery is still blocked pending private App settings verification |
104
+ | 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 |
86
105
  | 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) |
87
106
  | OpenSSF Best Practices | Passing badge, project `12955`; `.bestpractices.json` remains the conservative evidence record |
88
107
 
@@ -240,13 +259,15 @@ The hosted GitHub App deployment planner is documented in [docs/github-app-deplo
240
259
 
241
260
  The hosted production adapter layer is documented in [docs/hosted-production-adapters.md](docs/hosted-production-adapters.md). It exports `createHostedGitHubAppJwt`, `planHostedGitHubInstallationTokenRequest`, and `planHostedProductionWorkerExecution` from `ai-saas-guard/hosted/production-adapters`. It adds RS256 GitHub App JWT generation, selected-repository installation-token request plans, separate worker and Check Run token scopes, a fixed read-only worker command, bounded timeout and output budgets, compact JSON-only output, and cleanup plans for success, failure, timeout, and cancellation. It still does not expose a public hosted service by itself.
242
261
 
262
+ The hosted read-only checkout worker is exported from `ai-saas-guard/hosted/worker`. It creates a temporary checkout from trusted GitHub App identity, uses a runtime installation token only through git askpass, runs the fixed `ai-saas-guard pr-risk --json` command with bounded timeout/output, converts CLI JSON into compact findings, and deletes the checkout after success or failure. It does not return source, diffs, secrets, checkout paths, PR-authored commands, or installation tokens.
263
+
243
264
  The hosted Node/container app skeleton is documented in [docs/hosted-node-container-app.md](docs/hosted-node-container-app.md). It exports `createHostedHttpApp`, `createInMemoryHostedAppPlatform`, and `planHostedNodeContainerDeployment` from `ai-saas-guard/hosted/app`. It adds a safe `/healthz` route, signed `/github/webhook` ingress, one-job worker tick, in-memory provider adapters for tests, and deployment-plan validation for secret manager, queue, compact report store, worker sandbox, and GitHub Checks publisher references. It still does not deploy or expose a public hosted service by itself.
244
265
 
245
266
  The hosted staging deployment planner is documented in [docs/hosted-staging-deployment.md](docs/hosted-staging-deployment.md). It exports `planHostedProviderBinding`, `planHostedStagingDeployment`, and `planHostedGitHubAppPromotion` from `ai-saas-guard/hosted/staging`. It composes real provider references, the Node/container deployment plan, hosted operational release-gate evidence, and GitHub App deployment planning so staging and production promotion stay blocked until the required queue, store, worker sandbox, Check Run publisher, logs, metrics, rollback, and incident-response references are present. It still does not call a cloud provider, create a GitHub App, or expose a public hosted service by itself.
246
267
 
247
268
  The hosted staging harness is documented in [docs/hosted-staging-harness.md](docs/hosted-staging-harness.md). It exports `createFileBackedHostedStagingHarness` and `createHostedStagingHarnessEvidence` from `ai-saas-guard/hosted/staging-harness`. It runs signed webhook replay through the provider-independent hosted runtime with local file-backed queue, compact report, and Check Run adapters, then verifies worker sandbox cleanup. It is a staging rehearsal tool only; it does not call cloud providers, create a GitHub App, publish live Check Runs, or expose a public hosted service.
248
269
 
249
- The first live hosted ingress is deployed on Cloudflare Workers at `https://ai-saas-guard-hosted.zr9959.workers.dev` and documented in [hosted/cloudflare-worker/README.md](hosted/cloudflare-worker/README.md). It exposes `/healthz`, `/github/app/manifest-callback`, and signed `/github/webhook` intake backed by Cloudflare KV. A private staging GitHub App, `ai-saas-guard-hosted`, is installed on `zr9959/ai-saas-guard` with selected-repository access and the first-slice permission contract. The Worker can verify signatures, store compact pull request identity records, exchange a scoped installation token, fetch PR file metadata from GitHub, classify PR-risk hotspots, and publish a bounded Check Run summary. Current deployed evidence is tracked in [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md): health and Check Run publisher configuration pass, but end-to-end GitHub App webhook delivery is still blocked until the private App webhook settings are verified. It still does not run a full source checkout scan worker or store raw webhook payloads, PR title/body text, raw diffs, source, secrets, checkout paths, or installation tokens.
270
+ The first live hosted ingress is deployed on Cloudflare Workers at `https://ai-saas-guard-hosted.zr9959.workers.dev` and documented in [hosted/cloudflare-worker/README.md](hosted/cloudflare-worker/README.md). It exposes `/healthz`, `/github/app/manifest-callback`, and signed `/github/webhook` intake backed by Cloudflare KV. A private staging GitHub App, `ai-saas-guard-hosted`, is installed on `zr9959/ai-saas-guard` with selected-repository access and the first-slice permission contract. The Worker verifies signatures, stores compact pull request identity records, exchanges a scoped installation token, fetches PR file metadata from GitHub, classifies PR-risk hotspots, and publishes a bounded Check Run summary. Current deployed evidence is tracked in [docs/hosted-operations-evidence.md](docs/hosted-operations-evidence.md): health, signed webhook delivery, compact KV records, cleanup, and Check Run publication pass for the staging smoke. The Cloudflare Worker still does not run a full source checkout scan worker or store raw webhook payloads, PR title/body text, raw diffs, source, secrets, checkout paths, or installation tokens.
250
271
 
251
272
  The hosted operational release gate is documented in [docs/hosted-operational-release-gate.md](docs/hosted-operational-release-gate.md). It defines the hosted-specific CI, replay, queue, worker cleanup, privacy, monitoring, rollback, and incident-response evidence required before any hosted environment is exposed to users. The pure gate evaluator exported from `ai-saas-guard/hosted/contracts` blocks hosted exposure unless every P0 evidence item is fresh, a container digest is recorded, and release notes avoid pentest, certification, and full-audit claims.
252
273
 
@@ -254,7 +275,7 @@ Hosted uninstall and data deletion behavior is documented in [docs/hosted-uninst
254
275
 
255
276
  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.
256
277
 
257
- 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, a durable scan queue planner that reuses queued, running, and completed jobs for the same trusted scan key, a worker read-only scan planner that fixes the CLI command and requires repository `contents: read`, and a Check Run publication planner that requires repository `checks: write` and builds bounded check-only payloads from compact reports. They also cover queue-safe webhook event parsing, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, a retention/deletion cleanup planner, an operational release gate evaluator, the production adapter plans needed for GitHub App auth and bounded worker execution, the Node/container app skeleton needed for real provider wiring, the staging deployment planner needed before production GitHub App promotion, and the local staging harness needed to rehearse webhook replay, persistence, publication, and cleanup without cloud calls. The service runtime composes these contracts behind replaceable adapters. PR comments remain a later workflow or paid hosted feature, not part of the hosted MVP contract.
278
+ 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, a durable scan queue planner that reuses queued, running, and completed jobs for the same trusted scan key, a worker read-only scan planner that fixes the CLI command and requires repository `contents: read`, a concrete Node read-only checkout scan runner, and a Check Run publication planner that requires repository `checks: write` and builds bounded check-only payloads from compact reports. They also cover queue-safe webhook event parsing, bounded check-run summary rendering, idempotent queue cleanup planning, worker checkout cleanup planning, a retention/deletion cleanup planner, an operational release gate evaluator, the production adapter plans needed for GitHub App auth and bounded worker execution, the Node/container app skeleton needed for real provider wiring, the staging deployment planner needed before production GitHub App promotion, and the local staging harness needed to rehearse webhook replay, persistence, publication, and cleanup without cloud calls. The service runtime composes these contracts behind replaceable adapters. PR comments remain a later workflow or paid hosted feature, not part of the hosted MVP contract.
258
279
 
259
280
  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.
260
281
 
@@ -294,7 +315,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
294
315
 
295
316
  ## GitHub Action
296
317
 
297
- The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.27.2` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
318
+ The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.28.1` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
298
319
 
299
320
  ```yaml
300
321
  name: ai-saas-guard
@@ -426,8 +447,8 @@ Open-source core:
426
447
 
427
448
  Near-term priorities:
428
449
 
429
- - Use the hosted staging harness to rehearse webhook replay, Check Run publication, compact report persistence, and worker cleanup locally; then bind real provider references, deploy a staging artifact, and collect monitoring, rollback, and incident-response evidence from that artifact.
430
- - Keep hosted exposure blocked until the operational release gate has fresh evidence from a deployed artifact.
450
+ - Bind the Node/container read-only checkout worker to real provider references, deploy it behind staging queue/sandbox controls, and collect monitoring, rollback, incident-response, dependency, and container-scan evidence.
451
+ - Keep production hosted exposure blocked until the operational release gate has fresh evidence from the deployed checkout worker artifact.
431
452
 
432
453
  Potential paid layer later:
433
454
 
@@ -0,0 +1,43 @@
1
+ import type { HostedServiceScanRunner, HostedServiceScanRunnerInput, HostedServiceScanRunnerResult } from "./service.js";
2
+ export type HostedReadOnlyCheckoutCommandStage = "git_init" | "git_remote_add" | "git_fetch_head" | "git_fetch_base" | "git_checkout" | "cli_scan";
3
+ export interface HostedReadOnlyCheckoutCommand {
4
+ stage: HostedReadOnlyCheckoutCommandStage;
5
+ command: string;
6
+ args: string[];
7
+ cwd: string;
8
+ env: Record<string, string>;
9
+ timeoutMs: number;
10
+ maxOutputBytes: number;
11
+ shell: false;
12
+ }
13
+ export interface HostedReadOnlyCheckoutCommandResult {
14
+ stdout: string;
15
+ }
16
+ export type HostedReadOnlyCheckoutCommandRunner = (command: HostedReadOnlyCheckoutCommand) => Promise<HostedReadOnlyCheckoutCommandResult> | HostedReadOnlyCheckoutCommandResult;
17
+ export type HostedInstallationTokenProvider = (input: HostedServiceScanRunnerInput) => Promise<string> | string;
18
+ export interface HostedReadOnlyCheckoutScanRunnerOptions {
19
+ checkoutRoot?: string;
20
+ githubCloneBaseUrl?: string;
21
+ gitCommand?: string;
22
+ cliCommand?: string;
23
+ fetchDepth?: number;
24
+ timeoutMs?: number;
25
+ maxOutputBytes?: number;
26
+ installationTokenProvider: HostedInstallationTokenProvider;
27
+ commandRunner?: HostedReadOnlyCheckoutCommandRunner;
28
+ }
29
+ export type HostedReadOnlyCheckoutScanSafeReason = "invalid_worker_plan" | "invalid_repository_full_name" | "invalid_clone_base_url" | "missing_installation_token" | "git_init_failed" | "git_remote_add_failed" | "git_fetch_head_failed" | "git_fetch_base_failed" | "git_checkout_failed" | "cli_scan_failed" | "invalid_cli_output" | "cleanup_failed";
30
+ export declare class HostedReadOnlyCheckoutScanError extends Error {
31
+ readonly safeReason: HostedReadOnlyCheckoutScanSafeReason;
32
+ readonly privacy: {
33
+ readonly includesTemporaryCheckoutRoot: false;
34
+ readonly includesRawSource: false;
35
+ readonly includesRawDiffs: false;
36
+ readonly includesSecrets: false;
37
+ readonly includesCustomerPayloads: false;
38
+ readonly includesInstallationToken: false;
39
+ };
40
+ constructor(safeReason: HostedReadOnlyCheckoutScanSafeReason);
41
+ }
42
+ export declare function createHostedReadOnlyCheckoutScanRunner(options: HostedReadOnlyCheckoutScanRunnerOptions): HostedServiceScanRunner;
43
+ export declare function runHostedReadOnlyCheckoutScan(input: HostedServiceScanRunnerInput, options: HostedReadOnlyCheckoutScanRunnerOptions): Promise<HostedServiceScanRunnerResult>;
@@ -0,0 +1,267 @@
1
+ import { execFile } from "node:child_process";
2
+ import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { HOSTED_WORKER_DEFAULT_TIMEOUT_MS, HOSTED_WORKER_MAX_OUTPUT_BYTES, HOSTED_WORKER_MAX_TIMEOUT_MS } from "./production-adapters.js";
7
+ const execFileAsync = promisify(execFile);
8
+ const DEFAULT_GITHUB_CLONE_BASE_URL = "https://github.com";
9
+ const DEFAULT_FETCH_DEPTH = 100;
10
+ const MAX_FETCH_DEPTH = 1_000;
11
+ const MAX_COMPACT_FINDINGS = 200;
12
+ export class HostedReadOnlyCheckoutScanError extends Error {
13
+ safeReason;
14
+ privacy = {
15
+ includesTemporaryCheckoutRoot: false,
16
+ includesRawSource: false,
17
+ includesRawDiffs: false,
18
+ includesSecrets: false,
19
+ includesCustomerPayloads: false,
20
+ includesInstallationToken: false
21
+ };
22
+ constructor(safeReason) {
23
+ super(`hosted_read_only_checkout_scan_failed:${safeReason}`);
24
+ this.name = "HostedReadOnlyCheckoutScanError";
25
+ this.safeReason = safeReason;
26
+ }
27
+ }
28
+ export function createHostedReadOnlyCheckoutScanRunner(options) {
29
+ return (input) => runHostedReadOnlyCheckoutScan(input, options);
30
+ }
31
+ export async function runHostedReadOnlyCheckoutScan(input, options) {
32
+ const { plan } = input;
33
+ const { checkout, cli } = plan;
34
+ if (!plan.accepted || !plan.readOnly || !checkout || !cli || cli.writeMode !== "read_only") {
35
+ throw new HostedReadOnlyCheckoutScanError("invalid_worker_plan");
36
+ }
37
+ const repository = parseRepositoryFullName(checkout.repositoryFullName);
38
+ if (!repository) {
39
+ throw new HostedReadOnlyCheckoutScanError("invalid_repository_full_name");
40
+ }
41
+ const cloneBaseUrl = normalizeSafeCloneBaseUrl(options.githubCloneBaseUrl ?? DEFAULT_GITHUB_CLONE_BASE_URL);
42
+ const cloneUrl = `${cloneBaseUrl}/${repository.owner}/${repository.repo}.git`;
43
+ const timeoutMs = clampPositiveInteger(options.timeoutMs, HOSTED_WORKER_DEFAULT_TIMEOUT_MS, HOSTED_WORKER_MAX_TIMEOUT_MS);
44
+ const maxOutputBytes = clampPositiveInteger(options.maxOutputBytes, HOSTED_WORKER_MAX_OUTPUT_BYTES, HOSTED_WORKER_MAX_OUTPUT_BYTES);
45
+ const fetchDepth = clampPositiveInteger(options.fetchDepth, DEFAULT_FETCH_DEPTH, MAX_FETCH_DEPTH);
46
+ const checkoutRoot = options.checkoutRoot ?? join(tmpdir(), "ai-saas-guard-hosted-checkouts");
47
+ const token = await options.installationTokenProvider(input);
48
+ if (typeof token !== "string" || token.trim().length === 0) {
49
+ throw new HostedReadOnlyCheckoutScanError("missing_installation_token");
50
+ }
51
+ await mkdir(checkoutRoot, { recursive: true, mode: 0o700 });
52
+ const checkoutDir = await mkdtemp(join(checkoutRoot, "job-"));
53
+ let terminalError;
54
+ try {
55
+ await chmod(checkoutDir, 0o700);
56
+ const askpassPath = join(checkoutDir, ".git-askpass.sh");
57
+ await writeFile(askpassPath, [
58
+ "#!/bin/sh",
59
+ "case \"$1\" in",
60
+ "*Username*) printf '%s' 'x-access-token' ;;",
61
+ "*Password*) printf '%s' \"$AI_SAAS_GUARD_GITHUB_TOKEN\" ;;",
62
+ "*) printf '%s' '' ;;",
63
+ "esac",
64
+ ""
65
+ ].join("\n"), { mode: 0o700 });
66
+ const gitEnv = safeWorkerEnv(checkoutDir, {
67
+ GIT_ASKPASS: askpassPath,
68
+ GIT_TERMINAL_PROMPT: "0",
69
+ GIT_CONFIG_NOSYSTEM: "1",
70
+ GIT_CONFIG_GLOBAL: "/dev/null"
71
+ });
72
+ const gitSecretEnv = { AI_SAAS_GUARD_GITHUB_TOKEN: token };
73
+ const gitCommand = options.gitCommand ?? "git";
74
+ await runCommand(options, gitSecretEnv, commandSpec("git_init", gitCommand, ["init"], checkoutDir, gitEnv, timeoutMs, maxOutputBytes));
75
+ await runCommand(options, gitSecretEnv, commandSpec("git_remote_add", gitCommand, ["remote", "add", "origin", cloneUrl], checkoutDir, gitEnv, timeoutMs, maxOutputBytes));
76
+ await runCommand(options, gitSecretEnv, commandSpec("git_fetch_head", gitCommand, ["fetch", "--no-tags", "--depth", String(fetchDepth), "origin", checkout.targetCommitSha], checkoutDir, gitEnv, timeoutMs, maxOutputBytes));
77
+ await runCommand(options, gitSecretEnv, commandSpec("git_fetch_base", gitCommand, ["fetch", "--no-tags", "--depth", String(fetchDepth), "origin", checkout.baseSha], checkoutDir, gitEnv, timeoutMs, maxOutputBytes));
78
+ await runCommand(options, gitSecretEnv, commandSpec("git_checkout", gitCommand, ["checkout", "--detach", checkout.targetCommitSha], checkoutDir, gitEnv, timeoutMs, maxOutputBytes));
79
+ const cliEnv = safeWorkerEnv(checkoutDir);
80
+ const cliArgs = cli.args.map((arg) => arg === "<worker-checkout>" ? checkoutDir : arg);
81
+ const cliResult = await runCommand(options, {}, commandSpec("cli_scan", options.cliCommand ?? cli.command, cliArgs, checkoutDir, cliEnv, timeoutMs, maxOutputBytes));
82
+ return compactScanRunnerResult(cliResult.stdout);
83
+ }
84
+ catch (error) {
85
+ terminalError =
86
+ error instanceof HostedReadOnlyCheckoutScanError
87
+ ? error
88
+ : new HostedReadOnlyCheckoutScanError("cli_scan_failed");
89
+ throw terminalError;
90
+ }
91
+ finally {
92
+ try {
93
+ await rm(checkoutDir, { recursive: true, force: true });
94
+ }
95
+ catch {
96
+ if (!terminalError) {
97
+ throw new HostedReadOnlyCheckoutScanError("cleanup_failed");
98
+ }
99
+ }
100
+ }
101
+ }
102
+ function commandSpec(stage, command, args, cwd, env, timeoutMs, maxOutputBytes) {
103
+ return {
104
+ stage,
105
+ command,
106
+ args,
107
+ cwd,
108
+ env,
109
+ timeoutMs,
110
+ maxOutputBytes,
111
+ shell: false
112
+ };
113
+ }
114
+ async function runCommand(options, secretEnv, command) {
115
+ try {
116
+ if (options.commandRunner) {
117
+ return await options.commandRunner(command);
118
+ }
119
+ const { stdout } = await execFileAsync(command.command, command.args, {
120
+ cwd: command.cwd,
121
+ env: { ...command.env, ...secretEnv },
122
+ timeout: command.timeoutMs,
123
+ maxBuffer: command.maxOutputBytes,
124
+ encoding: "utf8",
125
+ shell: false
126
+ });
127
+ return { stdout };
128
+ }
129
+ catch {
130
+ throw new HostedReadOnlyCheckoutScanError(`${command.stage}_failed`);
131
+ }
132
+ }
133
+ function compactScanRunnerResult(stdout) {
134
+ try {
135
+ const report = JSON.parse(stdout);
136
+ const findings = Array.isArray(report.findings) ? report.findings : [];
137
+ return {
138
+ summaryCounts: normalizeSummaryCounts(report.summary),
139
+ findings: findings.slice(0, MAX_COMPACT_FINDINGS).flatMap(compactFinding)
140
+ };
141
+ }
142
+ catch {
143
+ throw new HostedReadOnlyCheckoutScanError("invalid_cli_output");
144
+ }
145
+ }
146
+ function compactFinding(value) {
147
+ if (!isRecord(value))
148
+ return [];
149
+ const ruleId = stringValue(value.ruleId);
150
+ const severity = stringValue(value.severity);
151
+ const evidence = Array.isArray(value.evidence) ? value.evidence.find(isRecord) : undefined;
152
+ const file = stringValue(evidence?.file) ?? stringValue(value.file);
153
+ if (!ruleId || !severity || !file)
154
+ return [];
155
+ const line = integerValue(evidence?.line) ?? integerValue(value.line);
156
+ return [
157
+ {
158
+ ruleId,
159
+ severity,
160
+ file,
161
+ ...(line === undefined ? {} : { line })
162
+ }
163
+ ];
164
+ }
165
+ function normalizeSummaryCounts(value) {
166
+ const record = isRecord(value) ? value : {};
167
+ const summary = {
168
+ critical: numberValue(record.critical),
169
+ high: numberValue(record.high),
170
+ medium: numberValue(record.medium),
171
+ low: numberValue(record.low),
172
+ info: numberValue(record.info)
173
+ };
174
+ summary.total = numberValue(record.total, summary.critical + summary.high + summary.medium + summary.low + summary.info);
175
+ return summary;
176
+ }
177
+ function safeWorkerEnv(checkoutDir, extra = {}) {
178
+ return {
179
+ PATH: process.env.PATH ?? "/usr/bin:/bin",
180
+ HOME: checkoutDir,
181
+ TMPDIR: checkoutDir,
182
+ CI: "true",
183
+ NO_COLOR: "1",
184
+ ...extra
185
+ };
186
+ }
187
+ function parseRepositoryFullName(value) {
188
+ const match = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(value);
189
+ if (!match)
190
+ return undefined;
191
+ return {
192
+ owner: match[1],
193
+ repo: match[2]
194
+ };
195
+ }
196
+ function normalizeSafeCloneBaseUrl(value) {
197
+ try {
198
+ const url = new URL(value);
199
+ if (url.protocol !== "https:" ||
200
+ url.username ||
201
+ url.password ||
202
+ url.search ||
203
+ url.hash ||
204
+ hasNonSlashPath(url.pathname) ||
205
+ isUnsafeHostedHostname(url.hostname)) {
206
+ throw new HostedReadOnlyCheckoutScanError("invalid_clone_base_url");
207
+ }
208
+ return `${url.protocol}//${url.host}`;
209
+ }
210
+ catch (error) {
211
+ if (error instanceof HostedReadOnlyCheckoutScanError)
212
+ throw error;
213
+ throw new HostedReadOnlyCheckoutScanError("invalid_clone_base_url");
214
+ }
215
+ }
216
+ function hasNonSlashPath(pathname) {
217
+ for (const character of pathname) {
218
+ if (character !== "/")
219
+ return true;
220
+ }
221
+ return false;
222
+ }
223
+ function isUnsafeHostedHostname(hostname) {
224
+ const normalized = hostname.toLowerCase().replace(/\.$/, "");
225
+ return (normalized === "localhost" ||
226
+ normalized.endsWith(".localhost") ||
227
+ isUnsafeIpv4Hostname(normalized) ||
228
+ normalized === "::1" ||
229
+ normalized.startsWith("fc") ||
230
+ normalized.startsWith("fd") ||
231
+ normalized.startsWith("fe80:"));
232
+ }
233
+ function isUnsafeIpv4Hostname(hostname) {
234
+ const parts = hostname.split(".");
235
+ if (parts.length !== 4 || !parts.every((part) => /^\d+$/.test(part)))
236
+ return false;
237
+ const octets = parts.map((part) => Number(part));
238
+ if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)) {
239
+ return false;
240
+ }
241
+ const [first, second] = octets;
242
+ return (first === 0 ||
243
+ first === 10 ||
244
+ first === 127 ||
245
+ (first === 169 && second === 254) ||
246
+ (first === 172 && second >= 16 && second <= 31) ||
247
+ (first === 192 && second === 168) ||
248
+ (first === 100 && second >= 64 && second <= 127) ||
249
+ first >= 224);
250
+ }
251
+ function clampPositiveInteger(value, fallback, maximum) {
252
+ if (value === undefined || !Number.isFinite(value))
253
+ return fallback;
254
+ return Math.min(maximum, Math.max(1, Math.floor(value)));
255
+ }
256
+ function isRecord(value) {
257
+ return typeof value === "object" && value !== null && !Array.isArray(value);
258
+ }
259
+ function stringValue(value) {
260
+ return typeof value === "string" && value.length > 0 ? value : undefined;
261
+ }
262
+ function integerValue(value) {
263
+ return typeof value === "number" && Number.isSafeInteger(value) ? value : undefined;
264
+ }
265
+ function numberValue(value, fallback = 0) {
266
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
267
+ }
@@ -5,7 +5,7 @@
5
5
  </p>
6
6
 
7
7
  <p align="center">
8
- ai-saas-guard 是面向 AI 构建的 SaaS 的本地优先上线 gate。它会优先指出 auth、billing、data access、secrets、MCP、deploy、CI 和“假成功”路径里最值得人工 review 的改动,让你在上线前知道该先看哪里。它本地运行、只读仓库、不上传代码。
8
+ ai-saas-guard 是面向 AI 构建的 Next.js、Supabase、Stripe、Vercel 和 MCP SaaS 的本地优先上线 gate。它会优先指出 auth、billing、data access、secrets、MCP、deploy、CI 和“假成功”路径里最值得人工 review 的改动,让你在上线前知道该先看哪里。它本地运行、只读仓库、不上传代码。
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -40,6 +40,25 @@ AI 能很快把一个 SaaS 做到“看起来能用”。真正危险的是上
40
40
 
41
41
  `ai-saas-guard` 是面向这个时刻的本地优先、review-first 上线预检工具。它不会证明你的应用绝对安全,也不是渗透测试、认证或完整安全审计。它的目标是给 founder、独立开发者、小团队和 reviewer 一份短而有证据的清单,告诉你上线或合并 PR 前最该先看哪里。
42
42
 
43
+ ## 输出长什么样
44
+
45
+ 报告是给上线前或合并 AI 大 PR 前快速阅读的:
46
+
47
+ ```text
48
+ Launch Gate: review before launch
49
+ 4 findings: 1 high, 3 medium
50
+
51
+ HIGH stripe.webhook.missing-signature
52
+ File: app/api/stripe/webhook/route.ts
53
+ Why: billing access can be granted from a webhook path that does not verify Stripe signatures.
54
+ Verify: replay a webhook with an invalid signature and confirm the route rejects it.
55
+ Fix: read the raw body, call stripe.webhooks.constructEvent, and make event handling idempotent.
56
+
57
+ MEDIUM supabase.rls.tenant-predicate-missing
58
+ File: supabase/migrations/20260524_accounts.sql
59
+ Verify: sign in as user A and user B; confirm neither can SELECT or UPDATE the other's rows.
60
+ ```
61
+
43
62
  ## 你会得到什么
44
63
 
45
64
  一个命令会返回一份上线前 review 队列:
@@ -67,21 +86,21 @@ AI 能很快把一个 SaaS 做到“看起来能用”。真正危险的是上
67
86
 
68
87
  这个仓库是公开 GitHub 仓库。
69
88
 
70
- CLI 已发布到 npm:`ai-saas-guard@0.27.2`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.27.2`。
89
+ CLI 已发布到 npm:`ai-saas-guard@0.28.1`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.28.1`。
71
90
 
72
91
  | 模块 | 状态 |
73
92
  | --- | --- |
74
93
  | 公开 GitHub 仓库 | 已可用 |
75
- | npm CLI | `ai-saas-guard@0.27.2` |
76
- | GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.27.2` |
94
+ | npm CLI | `ai-saas-guard@0.28.1` |
95
+ | GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.28.1` |
77
96
  | 输出格式 | Terminal、JSON、SARIF 和 PR markdown |
78
97
  | 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖、suppressions 和 fail threshold |
79
98
  | 隐私模型 | 本地优先、只读扫描、不调用 LLM、不上传代码 |
80
- | 当前版本 | `0.27.2` npm README metadata fix;CLI hosted Check Run launch-gate report summary 仍是当前功能 |
81
- | Action 标签 | `v0.27.2`、`v0` |
99
+ | 当前版本 | `0.28.1` discoverability polish、首页输出示例、npm metadata 同步,并保留 hosted worker release line |
100
+ | Action 标签 | `v0.28.1`、`v0` |
82
101
  | npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
83
102
  | 仓库可信度加固 | 严格 branch protection、Dependabot、CodeQL、fast-check fuzzing、signed release provenance assets、private vulnerability reporting、secret scanning 和 push protection |
84
- | Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`;Worker health Check Run publisher 配置已在线,但端到端 GitHub App webhook delivery 仍需要验证私有 App 设置 |
103
+ | Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`;签名 GitHub App webhook delivery compact Check Run staging smoke 已通过 |
85
104
  | 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) |
86
105
  | OpenSSF Best Practices | 已获得 passing badge,项目 `12955`;`.bestpractices.json` 继续作为保守证据记录 |
87
106
 
@@ -270,7 +289,7 @@ jobs:
270
289
 
271
290
  ## Hosted GitHub App 设计
272
291
 
273
- 当前仓库已经包含未来 Hosted GitHub App 的设计文档、纯契约测试,以及第一个真实 Cloudflare hosted ingress。私有 staging GitHub App `ai-saas-guard-hosted` 已安装到 `zr9959/ai-saas-guard`,Cloudflare 已配置所需的云端凭据绑定。Worker 代码已经能接收签名 webhook、写入 KV 队列、换取 scoped installation token、读取 GitHub PR file metadata、做 compact PR-risk classification,并发布有长度上限的 Check Run summary;但当前端到端 GitHub App webhook delivery smoke 还被私有 App webhook 设置阻断,证据记录在 [docs/hosted-operations-evidence.md](hosted-operations-evidence.md)。它还不是完整 source checkout scan worker。
292
+ 当前仓库已经包含未来 Hosted GitHub App 的设计文档、纯契约测试、第一个真实 Cloudflare hosted ingress,以及 Node/container read-only checkout scan runner。私有 staging GitHub App `ai-saas-guard-hosted` 已安装到 `zr9959/ai-saas-guard`,Cloudflare 已配置所需的云端凭据绑定。Worker 代码已经能接收签名 webhook、写入 KV 队列、换取 scoped installation token、读取 GitHub PR file metadata、做 compact PR-risk classification,并发布有长度上限的 Check Run summary;当前端到端 GitHub App webhook delivery smoke 已通过,证据记录在 [docs/hosted-operations-evidence.md](hosted-operations-evidence.md)。Cloudflare ingress 本身仍不是完整 source checkout scan worker。
274
293
 
275
294
  相关文档:
276
295
 
@@ -293,13 +312,14 @@ jobs:
293
312
  - pull request webhook intake planner:先验签,再解析 payload、生成可信 identity、校验 selected-repository scope,并默认只走 check-run-only 输出
294
313
  - durable scan queue planner:同一个 trusted scan key 的 queued/running/completed job 会复用,不重复排 worker,也不会把源码、diff、secret 或 PR 正文放进队列 payload
295
314
  - worker read-only scan planner:只用 trusted identity 规划临时 worker checkout,要求 repository `contents: read`,固定运行 `ai-saas-guard pr-risk --json`,并忽略 PR 正文里的 repo 名、token scope 或命令
315
+ - hosted read-only checkout worker:`ai-saas-guard/hosted/worker` 导出 `createHostedReadOnlyCheckoutScanRunner`,从 trusted GitHub App identity 创建临时 checkout,只通过 git askpass 使用 runtime installation token,运行固定 `ai-saas-guard pr-risk --json`,把 CLI JSON 转成 compact findings,并在成功或失败后删除 checkout;不会返回源码、diff、secret、checkout path、PR 里写的命令或 installation token
296
316
  - hosted service runtime:`ai-saas-guard/hosted/service` 导出 `createHostedServiceRuntime`,把签名 webhook intake、幂等 queue upsert、read-only worker 编排、compact report 存储、Check Run 发布 adapter 和 worker cleanup 串成可测试的服务核心;它本身不部署公开 hosted 环境
297
317
  - GitHub App deployment planner:`ai-saas-guard/hosted/github-app` 导出 `planHostedGitHubAppDeployment`,生成 first slice 最小权限 manifest,并在 release gate、公开 HTTPS URL、container digest、secret 引用、原始 secret 输入、permission 或 event 不安全时阻止创建
298
318
  - Hosted production adapter layer:`ai-saas-guard/hosted/production-adapters` 导出 `createHostedGitHubAppJwt`、`planHostedGitHubInstallationTokenRequest` 和 `planHostedProductionWorkerExecution`,用于 GitHub App RS256 JWT、selected-repository installation token 请求规划、worker/check-run 分离 token scope、固定只读 worker 命令、timeout/output 预算、compact JSON-only 输出,以及 success/failure/timeout/cancellation 的 cleanup 规划;它本身仍然不部署公开 hosted 服务
299
319
  - Hosted Node/container app skeleton:`ai-saas-guard/hosted/app` 导出 `createHostedHttpApp`、`createInMemoryHostedAppPlatform` 和 `planHostedNodeContainerDeployment`,提供安全 `/healthz`、签名 `/github/webhook` ingress、单 job worker tick、测试用 in-memory provider adapters,以及 secret manager、queue、compact report store、worker sandbox、GitHub Checks publisher 的部署引用校验;它本身仍然不部署或暴露公开 hosted 服务
300
320
  - Hosted staging deployment planner:`ai-saas-guard/hosted/staging` 导出 `planHostedProviderBinding`、`planHostedStagingDeployment` 和 `planHostedGitHubAppPromotion`,把真实 provider 引用、Node/container deployment plan、hosted operational release-gate evidence 和 GitHub App deployment planning 组合起来;缺少 queue、store、worker sandbox、Check Run publisher、logs、metrics、rollback 或 incident-response 引用时,会阻止 staging exposure 和 production promotion;它本身仍然不会调用云平台、创建 GitHub App 或暴露公开 hosted 服务
301
321
  - Hosted staging harness:`ai-saas-guard/hosted/staging-harness` 导出 `createFileBackedHostedStagingHarness` 和 `createHostedStagingHarnessEvidence`,可以在本地用 file-backed queue、compact report、Check Run request 和 worker sandbox 跑通签名 webhook replay、worker tick 和 cleanup 校验;它只是 staging 演练工具,不会调用云平台、创建 GitHub App、写真实 Check Run 或暴露公开 hosted 服务
302
- - Cloudflare hosted ingress:`hosted/cloudflare-worker` 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`,提供 `/healthz`、`/github/app/manifest-callback` 和签名 `/github/webhook` intake;Worker 已具备 compact pull request identity、file/category risk signal 和 Check Run metadata 路径;staging GitHub App ID 为 `3834787`,installation ID 为 `135085075`;真实 GitHub App webhook delivery、完整 source checkout scan worker、monitoring、rollback 和 incident-response evidence 仍需要通过 hosted operational release gate
322
+ - Cloudflare hosted ingress:`hosted/cloudflare-worker` 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`,提供 `/healthz`、`/github/app/manifest-callback` 和签名 `/github/webhook` intake;Worker 已具备 compact pull request identity、file/category risk signal 和 Check Run metadata 路径;staging GitHub App ID 为 `3834787`,installation ID 为 `135085075`;真实 GitHub App webhook delivery 和 Check Run smoke 已通过;完整 source checkout worker deployment、monitoring、rollback 和 incident-response evidence 仍需要通过 hosted operational release gate
303
323
  - webhook event parser
304
324
  - check-run summary renderer
305
325
  - Check Run publication planner:要求 repository `checks: write`,只从 compact report 生成有长度上限的 Check Run payload,包含 review categories、优先 review 文件、verification steps 和本地 CLI 复现命令;MVP 不发 PR comment
@@ -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.27.2` 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.28.1` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
6
6
 
7
7
  ## PR Summary
8
8
 
@@ -87,6 +87,6 @@ The repository can now produce and validate the deployment plan, and a private s
87
87
  - Webhook URL: `https://ai-saas-guard-hosted.zr9959.workers.dev/github/webhook`
88
88
  - Secret storage: Cloudflare Worker secrets for `WEBHOOK_SECRET` and `GITHUB_APP_PRIVATE_KEY`
89
89
 
90
- This is now a first-slice staging Worker deployment, not a complete hosted scanner. The Worker code verifies signatures, queues compact pull request identity records, exchanges scoped installation tokens, fetches PR file metadata, classifies PR-risk hotspots, and publishes bounded Check Runs. Current operations evidence is tracked in [hosted-operations-evidence.md](hosted-operations-evidence.md); health and publisher configuration pass, but end-to-end GitHub App webhook delivery is still blocked pending private App settings verification. It still does not run full source checkout scan workers, store raw diffs, store source code, or expose a production hosted service.
90
+ This is now a first-slice staging Worker deployment, not a complete hosted scanner. The Worker code verifies signatures, queues compact pull request identity records, exchanges scoped installation tokens, fetches PR file metadata, classifies PR-risk hotspots, and publishes bounded Check Runs. Current operations evidence is tracked in [hosted-operations-evidence.md](hosted-operations-evidence.md); health, signed webhook delivery, compact KV records, cleanup, and Check Run publication pass in staging. It still does not run full source checkout scan workers inside the Cloudflare Worker, store raw diffs, store source code, or expose a production hosted service.
91
91
 
92
92
  The next deployment stage should wire the hosted service runtime, production adapters, [Node/container app skeleton](hosted-node-container-app.md), and [staging deployment planner](hosted-staging-deployment.md) to a real platform queue, compact report store, GitHub installation authentication, worker isolation layer, Checks API publisher, logs, metrics, rollback, and incident-response evidence.
@@ -6,27 +6,28 @@ Passing these checks does not make the project a pentest, certification, or full
6
6
 
7
7
  ## Current Evidence
8
8
 
9
- Recorded on 2026-05-24 from the deployed Cloudflare Worker.
9
+ Recorded on 2026-05-24 from the deployed Cloudflare Worker and a temporary no-file-change GitHub PR smoke.
10
10
 
11
11
  | Check | Evidence | Result |
12
12
  | --- | --- | --- |
13
- | Cloudflare Worker health | `GET https://ai-saas-guard-hosted.zr9959.workers.dev/healthz` returned `ok: true`, `checkRunPublisher: "configured"`, `scannerVersion: "0.25.0"`, and all privacy flags set to false for raw payloads, PR text, source, diffs, secrets, customer payloads, checkout paths, and installation tokens | Passed |
14
- | Deployed Worker version | `wrangler deployments list` showed current version `bc4b87d9-420a-48bb-a058-8066b08abe03`, deployed at `2026-05-24T04:30:41.924Z` | Passed |
13
+ | Cloudflare Worker health | `GET https://ai-saas-guard-hosted.zr9959.workers.dev/healthz` returned `ok: true`, `checkRunPublisher: "configured"`, `scannerVersion: "0.28.0"`, and all privacy flags set to false for raw payloads, PR text, source, diffs, secrets, customer payloads, checkout paths, and installation tokens | Passed |
14
+ | Deployed Worker version | `wrangler deployments list` showed current version `531d2286-86c6-4327-bfd0-67cad8693c10`, deployed at `2026-05-24T09:01:25.706Z` | Passed |
15
15
  | KV cleanup | `wrangler kv key list --namespace-id fa5344fbd7944de6a776bf8731d58460 --remote` returned `[]` after smoke cleanup | Passed |
16
- | Temporary smoke PR cleanup | Temporary PR `#36` was closed and branch `codex/hosted-smoke-20260524123129` was deleted | Passed |
17
- | End-to-end GitHub App delivery | Temporary PR `#36` triggered normal GitHub Actions and CodeQL checks, but no `ai-saas-guard PR risk` Check Run appeared and no KV delivery record was created | Blocked |
16
+ | Temporary smoke PR cleanup | Temporary PR `#52` was closed, branch `codex/hosted-smoke-20260524170208` was deleted, and in-progress workflow run `26357038569` was cancelled | Passed |
17
+ | End-to-end GitHub App delivery | Temporary PR `#52` created `ai-saas-guard PR risk` from GitHub App `ai-saas-guard-hosted`; Check Run `77585561127` completed with conclusion `success` for head SHA `408925d2bf4df564082dabc3e1893a72c25bdd19` | Passed |
18
+ | Compact hosted record | KV scan record `scan:135085075:1247239389:52:408925d2bf4df564082dabc3e1893a72c25bdd19:0.28.0` completed with zero findings, `conclusion: "success"`, and all privacy flags set to false for raw payloads, PR text, source, diffs, secrets, customer payloads, checkout paths, and installation tokens | Passed |
18
19
 
19
- ## Current Blocker
20
+ ## Remaining Release Gate Gaps
20
21
 
21
- The deployed Worker is configured to publish compact PR-risk Check Runs when it receives a signed `pull_request` webhook. The temporary smoke PR did not create any Worker KV records, which means the GitHub App webhook did not reach the Worker.
22
+ The deployed Cloudflare Worker now receives signed GitHub App webhook delivery for pull request events and publishes bounded compact Check Runs. This is still staging evidence, not production hosted exposure.
22
23
 
23
- Before claiming live automatic PR checks, inspect the private GitHub App settings for `ai-saas-guard-hosted` and verify:
24
+ The hosted release gate still requires fresh deployed evidence for:
24
25
 
25
- - the webhook is active
26
- - the webhook URL is `https://ai-saas-guard-hosted.zr9959.workers.dev/github/webhook`
27
- - the webhook secret matches the Cloudflare `WEBHOOK_SECRET`
28
- - `pull_request` events are subscribed
29
- - the App installation still includes `zr9959/ai-saas-guard`
26
+ - full Node/container read-only checkout scan worker deployment
27
+ - worker sandbox network restrictions and cleanup evidence
28
+ - logs, metrics, alerting, rollback, and incident-response drills
29
+ - dependency and container artifact scanning for the deployed worker image
30
+ - retention and uninstall cleanup against the deployed provider stores
30
31
 
31
32
  ## Smoke Procedure
32
33
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  This document collects pure hosted contracts that can be tested before any hosted GitHub App service is deployed. These contracts keep the hosted design inspectable, local-first, and implementation-ready without adding network calls, credentials, queues, workers, or GitHub API writes. They are no network calls contracts by design.
4
4
 
5
- The helpers live in `src/hosted/contracts.ts` and are exported from `ai-saas-guard/hosted/contracts`. The production adapter plans live in `src/hosted/production-adapters.ts` and are exported from `ai-saas-guard/hosted/production-adapters`. The Node/container app skeleton lives in `src/hosted/app.ts` and is exported from `ai-saas-guard/hosted/app`. The staging deployment planner lives in `src/hosted/staging.ts` and is exported from `ai-saas-guard/hosted/staging`. The local staging harness lives in `src/hosted/staging-harness.ts` and is exported from `ai-saas-guard/hosted/staging-harness`.
5
+ The helpers live in `src/hosted/contracts.ts` and are exported from `ai-saas-guard/hosted/contracts`. The production adapter plans live in `src/hosted/production-adapters.ts` and are exported from `ai-saas-guard/hosted/production-adapters`. The Node/container app skeleton lives in `src/hosted/app.ts` and is exported from `ai-saas-guard/hosted/app`. The concrete read-only checkout worker runner lives in `src/hosted/worker.ts` and is exported from `ai-saas-guard/hosted/worker`. The staging deployment planner lives in `src/hosted/staging.ts` and is exported from `ai-saas-guard/hosted/staging`. The local staging harness lives in `src/hosted/staging-harness.ts` and is exported from `ai-saas-guard/hosted/staging-harness`.
6
6
 
7
7
  ## Pull Request Webhook Intake Planner
8
8
 
@@ -70,6 +70,29 @@ Trust boundaries:
70
70
 
71
71
  The exported helper is `planHostedWorkerReadOnlyScan`. It is intended to be the worker-provider-independent contract for the first real hosted worker implementation.
72
72
 
73
+ ## Read-Only Checkout Worker Runner
74
+
75
+ The read-only checkout worker runner turns the worker plan into a concrete Node/container runner without changing the product boundary. It is still a hosted building block, not a public hosted service by itself.
76
+
77
+ Default behavior:
78
+
79
+ - derive the GitHub clone URL only from trusted repository identity
80
+ - require a runtime installation token provider and keep the token out of command arguments, returned results, compact reports, and serialized plans
81
+ - pass the installation token to git only through a temporary askpass helper inside the worker checkout
82
+ - run `git init`, add the trusted remote, fetch the trusted head and base SHAs with bounded depth, and checkout the trusted head SHA
83
+ - run the fixed `ai-saas-guard pr-risk --root <worker-checkout> --base <baseSha> --json` command without shell parsing
84
+ - cap command timeout and output bytes
85
+ - parse only compact JSON fields from CLI output: summary counts, rule IDs, severity, file, and line
86
+ - delete the temporary checkout after success or failure
87
+
88
+ Privacy boundaries:
89
+
90
+ - do not return temporary checkout paths, raw source, raw diffs, evidence snippets, secrets, customer payloads, PR-authored commands, PR-authored repository names, or installation tokens
91
+ - do not persist source checkout contents beyond the worker run
92
+ - rely on the deployment sandbox for network egress restrictions around the CLI phase; the runner itself removes GitHub credentials before invoking the CLI
93
+
94
+ The exported helper is `createHostedReadOnlyCheckoutScanRunner`.
95
+
73
96
  ## Production Adapter Plans
74
97
 
75
98
  The production adapter layer turns the pure hosted contracts into a safer shape for real platform wiring. It is still provider-independent: it does not start a worker, call GitHub, request live installation tokens, write Check Runs, or upload source code.
@@ -381,6 +404,8 @@ Automated tests must cover:
381
404
  - worker read-only scan planning requires repository `contents: read` permissions
382
405
  - worker read-only scan planning uses trusted identity for checkout target and fixed CLI command
383
406
  - worker read-only scan planning does not persist raw source, raw diffs, secrets, customer payloads, checkout paths, PR-authored commands, or PR-authored token scopes
407
+ - read-only checkout worker runner uses trusted clone targets, bounded command execution, compact output parsing, and cleanup after success or failure
408
+ - read-only checkout worker runner does not expose installation tokens, checkout paths, raw source, raw diffs, secret values, customer payloads, or PR-authored commands
384
409
  - accepted pull request events build the expected trusted scan identity
385
410
  - unsupported actions are rejected
386
411
  - draft pull requests are rejected by default
@@ -5,11 +5,11 @@
5
5
  ## Current State
6
6
 
7
7
  - Package name: `ai-saas-guard`
8
- - Current published version: `0.27.2`
8
+ - Current published version: `0.28.1`
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.27.2`
12
+ - GitHub Release: `v0.28.1`
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.27.2`.
21
+ 1. Create and review a release tag such as `v0.28.1`.
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.
@@ -60,6 +60,7 @@ Implemented surfaces:
60
60
  - hosted service runtime document and provider-independent runtime core for signed webhook intake, idempotent queue upsert, read-only worker orchestration, compact report storage, Check Run publication adapters, and worker cleanup planning
61
61
  - hosted GitHub App deployment planner document and least-privilege manifest planner for required permissions, events, public HTTPS URLs, container digest, secret references, raw secret input blockers, and release-gate checks
62
62
  - hosted production adapter layer document and helpers for RS256 GitHub App JWT creation, selected-repository installation-token request planning, separate worker/check-run token scopes, fixed read-only worker execution, timeout/output budgets, and cleanup planning for success, failure, timeout, and cancellation
63
+ - hosted read-only checkout worker runner exported from `ai-saas-guard/hosted/worker`, with trusted clone targets, git askpass token handling, bounded command execution, compact CLI JSON parsing, and checkout cleanup after success or failure
63
64
  - hosted Node/container app skeleton document and helpers for safe health and webhook HTTP ingress, one-job worker ticks, in-memory provider adapters, provider reference validation, and the chosen `node_container` roles `webhook-ingress` and `scan-worker`
64
65
  - hosted staging deployment planner document and helpers for provider binding, staging release-gate evidence, Node/container deployment composition, and production GitHub App promotion gating
65
66
  - hosted staging harness document and helpers for local signed webhook replay, file-backed queue/report/Check Run artifacts, worker sandbox cleanup verification, and release-gate evidence fixtures without cloud calls
@@ -149,8 +150,8 @@ Hosted staging:
149
150
  - GitHub App ID: `3834787`
150
151
  - GitHub App installation ID: `135085075`
151
152
  - Installed repository: `zr9959/ai-saas-guard`
152
- - Current hosted mode: deployed Worker health and Check Run publisher configuration pass; code supports signed webhook ingress, compact queueing, scoped GitHub App token exchange, PR file risk classification, and bounded Check Run publishing
153
- - Not yet complete: end-to-end GitHub App webhook delivery settings verification, full source checkout scan worker execution, monitoring evidence, rollback evidence, incident-response evidence, production hosted exposure, and paid hosted workflow features
153
+ - Current hosted mode: deployed Worker health, signed GitHub App webhook delivery, compact KV records, cleanup, and Check Run publication pass in staging; code supports signed webhook ingress, compact queueing, scoped GitHub App token exchange, PR file risk classification, bounded Check Run publishing, and a Node/container read-only checkout worker runner
154
+ - Not yet complete: deployed full source checkout scan worker with sandbox evidence, monitoring evidence, rollback evidence, incident-response evidence, production hosted exposure, and paid hosted workflow features
154
155
 
155
156
  OpenSSF Best Practices:
156
157
 
@@ -160,7 +161,7 @@ OpenSSF Best Practices:
160
161
  Publishing:
161
162
 
162
163
  - npm package: `ai-saas-guard`
163
- - Current published release line: `v0.27.2`
164
+ - Current published release line: `v0.28.1` pending this branch release
164
165
  - Next source candidate: none
165
166
  - Publish workflow: `.github/workflows/npm-publish.yml`
166
167
  - Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
@@ -13,14 +13,14 @@ It is intentionally narrow:
13
13
  - Duplicate GitHub delivery IDs are accepted idempotently.
14
14
  - Responses and KV records do not include raw webhook payloads, PR title/body text, source code, diffs, secrets, customer payloads, checkout paths, or installation tokens.
15
15
 
16
- This Worker is a real hosted ingress with first-slice Check Run publishing code, not yet the complete scan worker. `shouldCreateCheckRun` is `true` only when the GitHub App bindings are present and the event passes installation scope checks. Current operations evidence is tracked in [docs/hosted-operations-evidence.md](../../docs/hosted-operations-evidence.md); the Worker health check passes, but end-to-end GitHub App webhook delivery still needs private App settings verification. Full source checkout scanning remains gated behind the hosted operational release gate.
16
+ This Worker is a real hosted ingress with first-slice Check Run publishing code, not yet the complete scan worker. `shouldCreateCheckRun` is `true` only when the GitHub App bindings are present and the event passes installation scope checks. Current operations evidence is tracked in [docs/hosted-operations-evidence.md](../../docs/hosted-operations-evidence.md); the Worker health check, signed webhook delivery, KV cleanup, and compact Check Run smoke pass in staging. Full source checkout scanning remains gated behind the hosted operational release gate and the Node/container checkout worker deployment path.
17
17
 
18
18
  ## Required Cloudflare Bindings
19
19
 
20
20
  - `HOSTED_EVENTS`: Cloudflare KV namespace for compact delivery and queued scan records.
21
21
  - `WEBHOOK_SECRET`: Worker secret matching the GitHub App webhook secret.
22
22
  - `GITHUB_APP_PRIVATE_KEY`: Worker secret for the staging GitHub App private key, used only in memory to sign short-lived GitHub App JWTs.
23
- - `SCANNER_VERSION`: public version string, currently `0.25.0`.
23
+ - `SCANNER_VERSION`: public version string, currently `0.28.0`.
24
24
  - `GITHUB_APP_ID`, `GITHUB_APP_SLUG`, `GITHUB_APP_INSTALLATION_ID`: public staging identifiers for the private GitHub App installation.
25
25
 
26
26
  ## Deployment
@@ -45,7 +45,7 @@ Current public staging endpoint:
45
45
  - GitHub App ID: `3834787`
46
46
  - GitHub App installation ID: `135085075`
47
47
  - Installed repository: `zr9959/ai-saas-guard`
48
- - Mode: signed webhook ingress and compact queueing only
48
+ - Mode: signed webhook ingress, compact queueing, PR file metadata classification, and bounded Check Run publishing
49
49
 
50
50
  ## Release Boundary
51
51
 
@@ -7,7 +7,7 @@
7
7
  "enabled": true
8
8
  },
9
9
  "vars": {
10
- "SCANNER_VERSION": "0.25.0",
10
+ "SCANNER_VERSION": "0.28.0",
11
11
  "GITHUB_APP_ID": "3834787",
12
12
  "GITHUB_APP_SLUG": "ai-saas-guard-hosted",
13
13
  "GITHUB_APP_INSTALLATION_ID": "135085075"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ai-saas-guard",
3
- "version": "0.27.2",
4
- "description": "Repo-local launch-readiness scanner for AI-built SaaS apps.",
3
+ "version": "0.28.1",
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",
7
7
  "homepage": "https://github.com/zr9959/ai-saas-guard#readme",
@@ -14,14 +14,25 @@
14
14
  },
15
15
  "keywords": [
16
16
  "ai",
17
+ "ai-code-review",
17
18
  "saas",
18
19
  "security",
19
20
  "launch",
20
21
  "preflight",
22
+ "launch-readiness",
23
+ "nextjs",
24
+ "vercel",
21
25
  "supabase",
26
+ "supabase-rls",
22
27
  "stripe",
28
+ "stripe-webhooks",
23
29
  "mcp",
30
+ "mcp-security",
24
31
  "github-action",
32
+ "github-actions",
33
+ "static-analysis",
34
+ "devsecops",
35
+ "local-first",
25
36
  "cli"
26
37
  ],
27
38
  "main": "./dist/index.js",
@@ -58,6 +69,10 @@
58
69
  "./hosted/staging-harness": {
59
70
  "types": "./dist/hosted/staging-harness.d.ts",
60
71
  "default": "./dist/hosted/staging-harness.js"
72
+ },
73
+ "./hosted/worker": {
74
+ "types": "./dist/hosted/worker.d.ts",
75
+ "default": "./dist/hosted/worker.js"
61
76
  }
62
77
  },
63
78
  "bin": {