ai-saas-guard 0.4.0 → 0.6.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 +11 -5
- package/dist/scanners/stripe.js +7 -3
- package/docs/github-action.md +1 -1
- package/docs/launch-readiness-checklist.md +191 -0
- package/docs/npm-publishing.md +3 -3
- package/docs/project-handoff.md +6 -7
- package/docs/stripe-webhook-replay.md +206 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,8 +51,8 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
51
51
|
| JSON and SARIF output | Available |
|
|
52
52
|
| Composite GitHub Action | Available |
|
|
53
53
|
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, and fail thresholds |
|
|
54
|
-
| Versioned Action tags | `v0.
|
|
55
|
-
| npm package | `ai-saas-guard@0.
|
|
54
|
+
| Versioned Action tags | `v0.6.0`, `v0` |
|
|
55
|
+
| npm package | `ai-saas-guard@0.6.0` |
|
|
56
56
|
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
57
57
|
|
|
58
58
|
## Quick Start
|
|
@@ -170,6 +170,14 @@ If `--base` cannot be resolved, `pr-risk` emits `pr-risk.diff-unavailable` inste
|
|
|
170
170
|
| `check-stripe` | Inspect webhook handlers and billing lifecycle coverage |
|
|
171
171
|
| `check-mcp` | Inventory MCP configs and classify side effects |
|
|
172
172
|
|
|
173
|
+
## Launch Readiness Checklist
|
|
174
|
+
|
|
175
|
+
Use [docs/launch-readiness-checklist.md](docs/launch-readiness-checklist.md) when an app is close to inviting real users. It explains how to combine `ai-saas-guard` output with manual two-account authorization testing, Stripe webhook verification, MCP config review, Supabase policy review, deploy checks, rollback planning, and a clear reminder that this is not a full security audit.
|
|
176
|
+
|
|
177
|
+
## Stripe Webhook Replay
|
|
178
|
+
|
|
179
|
+
Use [docs/stripe-webhook-replay.md](docs/stripe-webhook-replay.md) after `check-stripe` flags missing signature verification, idempotency, lifecycle handlers, or entitlement updates. The cookbook maps findings to concrete `stripe listen` and `stripe trigger` commands for checkout success, failed renewal, subscription update, cancellation, refund, duplicate delivery, and out-of-order event review.
|
|
180
|
+
|
|
173
181
|
## Project Configuration
|
|
174
182
|
|
|
175
183
|
Add `.ai-saas-guard.json` at the repository root to tune findings without changing scanner code. The CLI auto-loads this file from `--root` when it exists. Use `--config <file>` to point to a different JSON file.
|
|
@@ -191,7 +199,7 @@ Add `.ai-saas-guard.json` at the repository root to tune findings without changi
|
|
|
191
199
|
|
|
192
200
|
## GitHub Action
|
|
193
201
|
|
|
194
|
-
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.
|
|
202
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.6.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
195
203
|
|
|
196
204
|
```yaml
|
|
197
205
|
name: ai-saas-guard
|
|
@@ -320,8 +328,6 @@ Open-source core:
|
|
|
320
328
|
|
|
321
329
|
Near-term priorities:
|
|
322
330
|
|
|
323
|
-
- Stripe webhook replay cookbook
|
|
324
|
-
- launch-readiness checklist content
|
|
325
331
|
- false-positive suppression and rule stability labels
|
|
326
332
|
- GitHub App design note for the potential hosted layer
|
|
327
333
|
|
package/dist/scanners/stripe.js
CHANGED
|
@@ -10,8 +10,8 @@ const criticalEvents = [
|
|
|
10
10
|
const eventPattern = /["']((?:checkout\.session\.completed|invoice\.payment_failed|customer\.subscription\.(?:deleted|updated|created)|charge\.(?:refunded|dispute\.created)|refund\.(?:created|updated)))["']/g;
|
|
11
11
|
export async function checkStripe(input) {
|
|
12
12
|
const context = await resolveScanContext(input);
|
|
13
|
-
const
|
|
14
|
-
const webhookFiles =
|
|
13
|
+
const runtimeFiles = context.files.filter((file) => !isDocumentationFile(file.path));
|
|
14
|
+
const webhookFiles = runtimeFiles.filter((file) => {
|
|
15
15
|
const path = file.path.toLowerCase();
|
|
16
16
|
const content = file.content.toLowerCase();
|
|
17
17
|
return ((path.includes("stripe") && path.includes("webhook")) ||
|
|
@@ -19,7 +19,7 @@ export async function checkStripe(input) {
|
|
|
19
19
|
content.includes("stripe-signature") ||
|
|
20
20
|
content.includes("checkout.session.completed"));
|
|
21
21
|
});
|
|
22
|
-
const stripeSignalFiles =
|
|
22
|
+
const stripeSignalFiles = runtimeFiles;
|
|
23
23
|
const usesStripe = webhookFiles.length > 0 ||
|
|
24
24
|
stripeSignalFiles.some((file) => /STRIPE_SECRET_KEY|STRIPE_WEBHOOK_SECRET|from\s+["']stripe["']|require\(["']stripe["']\)|new\s+Stripe|stripe\.webhooks|checkout\.session\.completed/i.test(`${file.path}\n${file.content}`));
|
|
25
25
|
const findings = [];
|
|
@@ -164,3 +164,7 @@ function firstSnippetMatching(content, pattern) {
|
|
|
164
164
|
const line = firstLineMatching(content, pattern);
|
|
165
165
|
return line ? lineAt(content, line) : undefined;
|
|
166
166
|
}
|
|
167
|
+
function isDocumentationFile(path) {
|
|
168
|
+
const normalizedPath = path.replace(/\\/g, "/").toLowerCase();
|
|
169
|
+
return normalizedPath.startsWith("docs/") || /\.(md|mdx|rst|txt)$/i.test(normalizedPath);
|
|
170
|
+
}
|
package/docs/github-action.md
CHANGED
|
@@ -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.
|
|
5
|
+
Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.6.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
|
|
6
6
|
|
|
7
7
|
## PR Summary
|
|
8
8
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Launch Readiness Checklist
|
|
2
|
+
|
|
3
|
+
Use this checklist when an AI-built SaaS app is close to launch, a founder is about to invite real users, or a reviewer needs a practical pre-merge review path.
|
|
4
|
+
|
|
5
|
+
This is not a full security audit, penetration test, compliance review, or proof that the app is secure. It is a founder-readable launch preflight that combines `ai-saas-guard` findings with manual verification for the most common SaaS launch blockers.
|
|
6
|
+
|
|
7
|
+
## Start With The Local Preflight
|
|
8
|
+
|
|
9
|
+
Run from the app repository:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx ai-saas-guard@latest scan --root .
|
|
13
|
+
npx ai-saas-guard@latest pr-risk --root . --base origin/main
|
|
14
|
+
npx ai-saas-guard@latest check-supabase --root .
|
|
15
|
+
npx ai-saas-guard@latest check-stripe --root .
|
|
16
|
+
npx ai-saas-guard@latest check-mcp --root .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Treat every finding as a review queue item. The tool is read-only and local-first, so it can show where to inspect, but it cannot confirm production settings, account ownership, live Stripe dashboard state, or every possible authorization path.
|
|
20
|
+
|
|
21
|
+
## Launch Blocker Rules
|
|
22
|
+
|
|
23
|
+
Use these labels while reviewing:
|
|
24
|
+
|
|
25
|
+
| Level | Meaning | Examples |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| Launch blocker | Do not ship until fixed or explicitly disabled for the product. | Any user can read another tenant's data, unsigned Stripe webhooks grant access, real secrets are committed. |
|
|
28
|
+
| Must verify | Shipping may be reasonable only after a named manual test passes. | Rate limits, billing lifecycle handling, storage object scope, rollback path. |
|
|
29
|
+
| Follow-up | Track after launch when the current product risk is bounded. | Extra docs, sharper false-positive suppression, non-critical workflow polish. |
|
|
30
|
+
|
|
31
|
+
If a finding affects auth, billing, user data, secrets, file storage, production deploy config, or MCP tools with side effects, assume it is at least "must verify" until a human has tested the exact path.
|
|
32
|
+
|
|
33
|
+
## Two-Account Authorization Testing
|
|
34
|
+
|
|
35
|
+
Two-account authorization testing is the fastest manual check for broken SaaS isolation.
|
|
36
|
+
|
|
37
|
+
Prepare two ordinary test accounts:
|
|
38
|
+
|
|
39
|
+
- User A in organization or workspace A.
|
|
40
|
+
- User B in organization or workspace B.
|
|
41
|
+
- At least one private object owned by each account or workspace: project, document, invoice, file, message, API key, or team member.
|
|
42
|
+
|
|
43
|
+
Verify read isolation:
|
|
44
|
+
|
|
45
|
+
- Log in as User A and open User A's private objects.
|
|
46
|
+
- Try to load User B's object by changing route parameters, object IDs, slugs, search filters, API URLs, and browser history entries.
|
|
47
|
+
- Repeat the same checks through direct API calls if the app exposes JSON routes.
|
|
48
|
+
- Confirm User A receives a 403 or 404 and no object metadata leaks in the response.
|
|
49
|
+
|
|
50
|
+
Verify write isolation:
|
|
51
|
+
|
|
52
|
+
- As User A, try to update, delete, invite users to, export, or share User B's objects by tampering with IDs.
|
|
53
|
+
- Confirm the server rejects the request, not only the UI.
|
|
54
|
+
- Confirm no partial write, audit entry, billing change, storage object, notification, or background job was created.
|
|
55
|
+
|
|
56
|
+
Verify tenant switching:
|
|
57
|
+
|
|
58
|
+
- If a user can belong to multiple workspaces, switch active workspaces and repeat the same read/write checks.
|
|
59
|
+
- Confirm server-side ownership checks use the selected tenant membership, not only a client-side workspace ID.
|
|
60
|
+
|
|
61
|
+
For Supabase apps, compare these manual checks with row-level security policy evidence from `check-supabase`. RLS should protect sensitive tables even when a route, query builder, or generated client code is wrong.
|
|
62
|
+
|
|
63
|
+
## Stripe Webhook Verification
|
|
64
|
+
|
|
65
|
+
Stripe webhook verification is required for subscription or credit products because checkout redirects are not billing truth.
|
|
66
|
+
|
|
67
|
+
Minimum checks:
|
|
68
|
+
|
|
69
|
+
- The webhook route reads the raw request body.
|
|
70
|
+
- The handler verifies the `Stripe-Signature` header with the server-only webhook secret before any database write.
|
|
71
|
+
- The signing secret is never exposed through `NEXT_PUBLIC_*`, client bundles, public logs, issue comments, or screenshots.
|
|
72
|
+
- The handler stores or dedupes `event.id`.
|
|
73
|
+
- Entitlement state is updated from Stripe webhook reconciliation, not only from checkout success pages.
|
|
74
|
+
|
|
75
|
+
Replay the critical billing paths with the companion cookbook:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
stripe listen --forward-to localhost:3000/api/stripe/webhook
|
|
79
|
+
stripe trigger checkout.session.completed
|
|
80
|
+
stripe trigger invoice.payment_failed
|
|
81
|
+
stripe trigger customer.subscription.updated
|
|
82
|
+
stripe trigger customer.subscription.deleted
|
|
83
|
+
stripe trigger charge.refunded
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Expected evidence:
|
|
87
|
+
|
|
88
|
+
- Checkout success grants the right user or tenant access only after signature verification.
|
|
89
|
+
- Failed invoice creates the intended past-due or grace state.
|
|
90
|
+
- Subscription updates reconcile plan, quantity, status, cancellation, and period fields.
|
|
91
|
+
- Cancellation revokes or downgrades paid access.
|
|
92
|
+
- Refund handling is explicit, even if the product requires manual review.
|
|
93
|
+
- Duplicate delivery of the same event ID is a no-op.
|
|
94
|
+
- Out-of-order events reconcile to one durable entitlement state.
|
|
95
|
+
|
|
96
|
+
See [stripe-webhook-replay.md](stripe-webhook-replay.md) for the full command cookbook.
|
|
97
|
+
|
|
98
|
+
## MCP Config Review
|
|
99
|
+
|
|
100
|
+
MCP config review matters because local tools can turn prompt injection into filesystem, shell, database, or network side effects.
|
|
101
|
+
|
|
102
|
+
Inventory every MCP server used by the app, agent, or development workflow:
|
|
103
|
+
|
|
104
|
+
- Config files such as `.mcp.json`, `.cursor/mcp.json`, `claude_desktop_config.json`, or tool-specific equivalents.
|
|
105
|
+
- Tool descriptions that expose shell commands, raw SQL, browser automation, filesystem writes, or deployment actions.
|
|
106
|
+
- Environment variables and credentials passed to MCP servers.
|
|
107
|
+
- Bind addresses and transport URLs.
|
|
108
|
+
|
|
109
|
+
Review questions:
|
|
110
|
+
|
|
111
|
+
- Does any MCP server bind to `0.0.0.0` or a non-localhost host?
|
|
112
|
+
- Does any tool allow broad filesystem read/write access outside the intended repository?
|
|
113
|
+
- Does any tool run arbitrary shell commands or raw SQL against production-like data?
|
|
114
|
+
- Are credentials stored in plaintext config files?
|
|
115
|
+
- Are secret-bearing config files group-readable or world-readable?
|
|
116
|
+
- Can the tool mutate billing, user access, customer data, or deploy state without a human approval step?
|
|
117
|
+
|
|
118
|
+
Launch blocker examples:
|
|
119
|
+
|
|
120
|
+
- A production database URL is stored in an MCP config.
|
|
121
|
+
- A broad filesystem tool can write outside the app repository.
|
|
122
|
+
- A shell or SQL tool is exposed to untrusted prompts without environment separation.
|
|
123
|
+
|
|
124
|
+
Use `check-mcp` findings as the starting inventory, then manually inspect any tool that can read secrets or change state.
|
|
125
|
+
|
|
126
|
+
## Supabase And Storage
|
|
127
|
+
|
|
128
|
+
For Supabase apps, launch only after the data model has an ownership story.
|
|
129
|
+
|
|
130
|
+
Check:
|
|
131
|
+
|
|
132
|
+
- Sensitive tables have RLS enabled.
|
|
133
|
+
- Policies use `auth.uid()` or tenant membership checks tied to the row.
|
|
134
|
+
- Write policies use `WITH CHECK` so users cannot insert or move rows into another tenant.
|
|
135
|
+
- Storage buckets and `storage.objects` policies are scoped by owner, tenant, or object path.
|
|
136
|
+
- Service-role keys stay server-only and are not used in browser code.
|
|
137
|
+
|
|
138
|
+
Manual verification should still use the two-account flow above. Scanner findings can point to weak policies, but the actual product workflow determines whether access is correctly isolated.
|
|
139
|
+
|
|
140
|
+
## Secrets, Env, And Deploy
|
|
141
|
+
|
|
142
|
+
Before launch:
|
|
143
|
+
|
|
144
|
+
- Rotate any real secret that was committed, pasted into a public issue, or printed in a public log.
|
|
145
|
+
- Confirm examples use inert placeholders, not real-looking provider tokens.
|
|
146
|
+
- Confirm `NEXT_PUBLIC_*` variables contain only values that are safe for browsers.
|
|
147
|
+
- Check `.env.example` or deploy docs include required production variables without revealing secret values.
|
|
148
|
+
- Confirm Vercel, Netlify, or other deploy settings match runtime expectations: API routes are not accidentally static, and Node-only libraries are not deployed to incompatible Edge runtimes.
|
|
149
|
+
|
|
150
|
+
## CI And PR Review
|
|
151
|
+
|
|
152
|
+
Minimum repository workflow:
|
|
153
|
+
|
|
154
|
+
- CI runs tests on pull requests and pushes to the default branch.
|
|
155
|
+
- `ai-saas-guard pr-risk` runs with enough git history to compare against the base branch.
|
|
156
|
+
- SARIF or markdown output is used when reviewers need scan results in GitHub.
|
|
157
|
+
- Large AI-generated pull requests are split when they combine UI, auth, database, billing, and deploy changes.
|
|
158
|
+
|
|
159
|
+
Review the first files named by `pr-risk` before reviewing cosmetic changes. If a base ref is missing, fix checkout depth or fetch history before trusting the PR risk result.
|
|
160
|
+
|
|
161
|
+
## Rollback And Incident Readiness
|
|
162
|
+
|
|
163
|
+
Before inviting real users, define:
|
|
164
|
+
|
|
165
|
+
- How to disable paid access changes without deleting customer data.
|
|
166
|
+
- How to revoke or rotate leaked keys.
|
|
167
|
+
- How to roll back a failed deployment.
|
|
168
|
+
- How to pause new signups or billing flows.
|
|
169
|
+
- Where webhook delivery failures, auth errors, and database policy denials are logged.
|
|
170
|
+
- Who decides whether a finding is a launch blocker or an accepted risk.
|
|
171
|
+
|
|
172
|
+
The goal is not a perfect process. The goal is that the founder can answer what happens when auth, billing, deploy, or data isolation fails on launch day.
|
|
173
|
+
|
|
174
|
+
## Founder Sign-Off
|
|
175
|
+
|
|
176
|
+
A launch-ready review should leave behind short evidence, not just a feeling.
|
|
177
|
+
|
|
178
|
+
Use this minimal sign-off:
|
|
179
|
+
|
|
180
|
+
| Area | Evidence to keep |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| CLI preflight | Command output or CI link for `scan` and focused checks |
|
|
183
|
+
| Two-account authorization | User A/User B notes with tested object types and rejected actions |
|
|
184
|
+
| Stripe | Webhook replay notes for success, failure, update, cancellation, refund, duplicate, and out-of-order paths |
|
|
185
|
+
| Supabase | RLS and storage policy review notes |
|
|
186
|
+
| MCP | Reviewed MCP config files and disabled or constrained risky tools |
|
|
187
|
+
| Secrets | Rotation notes or confirmation that only placeholders were found |
|
|
188
|
+
| Deploy | Production env and runtime checks |
|
|
189
|
+
| Rollback | Known rollback command or deploy platform rollback path |
|
|
190
|
+
|
|
191
|
+
If any row is blank for a product surface the app actually uses, treat it as a must-verify item before launch.
|
package/docs/npm-publishing.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
## Current State
|
|
6
6
|
|
|
7
7
|
- Package name: `ai-saas-guard`
|
|
8
|
-
- Current version: `0.
|
|
8
|
+
- Current version: `0.6.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.
|
|
11
|
+
- GitHub Release: `v0.6.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.
|
|
20
|
+
1. Create and review a release tag such as `v0.6.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.
|
package/docs/project-handoff.md
CHANGED
|
@@ -39,7 +39,9 @@ Do not market it as a full pentest, full SAST platform, or proof that an app is
|
|
|
39
39
|
Implemented surfaces:
|
|
40
40
|
|
|
41
41
|
- secret-like values and risky public env exposure
|
|
42
|
+
- founder-readable launch-readiness checklist for two-account authorization, Stripe webhook verification, MCP config review, Supabase, deploy, CI, and rollback checks
|
|
42
43
|
- Stripe webhook signature, raw body, idempotency, and lifecycle handler heuristics
|
|
44
|
+
- Stripe webhook replay cookbook for checkout, renewal failure, updates, cancellation, refunds, duplicate delivery, and out-of-order review
|
|
43
45
|
- Supabase RLS, tenant membership, ownership filter, weak `WITH CHECK`, and storage object policy heuristics
|
|
44
46
|
- sensitive API route heuristics
|
|
45
47
|
- MCP config side-effect and secret-bearing risk inventory
|
|
@@ -99,8 +101,7 @@ GitHub Project:
|
|
|
99
101
|
|
|
100
102
|
Current issue set:
|
|
101
103
|
|
|
102
|
-
-
|
|
103
|
-
- #5 Write Stripe webhook replay cookbook
|
|
104
|
+
- No open roadmap issues after the `v0.6.0` checklist release.
|
|
104
105
|
|
|
105
106
|
CI:
|
|
106
107
|
|
|
@@ -112,7 +113,7 @@ CI:
|
|
|
112
113
|
Publishing:
|
|
113
114
|
|
|
114
115
|
- npm package: `ai-saas-guard`
|
|
115
|
-
- Current release line: `v0.
|
|
116
|
+
- Current release line: `v0.6.0`
|
|
116
117
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
117
118
|
- Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
|
|
118
119
|
- Long-lived npm publish tokens should not be required.
|
|
@@ -139,10 +140,8 @@ Not allowed:
|
|
|
139
140
|
|
|
140
141
|
Recommended order:
|
|
141
142
|
|
|
142
|
-
1.
|
|
143
|
-
2. Add
|
|
144
|
-
3. Improve false-positive suppression and rule stability labels.
|
|
145
|
-
4. Add a GitHub App design note for the potential hosted layer.
|
|
143
|
+
1. Improve false-positive suppression and rule stability labels.
|
|
144
|
+
2. Add a GitHub App design note for the potential hosted layer.
|
|
146
145
|
|
|
147
146
|
For every feature, keep the scanner evidence-first:
|
|
148
147
|
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Stripe Webhook Replay Cookbook
|
|
2
|
+
|
|
3
|
+
Use this cookbook after `ai-saas-guard check-stripe` flags missing signature checks, missing idempotency, missing lifecycle handlers, or unclear entitlement updates.
|
|
4
|
+
|
|
5
|
+
It is a local test workflow for reviewers. It does not prove the production integration is secure, and it does not replace Stripe Dashboard checks, production endpoint configuration review, or two-account authorization tests.
|
|
6
|
+
|
|
7
|
+
## Preconditions
|
|
8
|
+
|
|
9
|
+
- Run against a sandbox or test-mode Stripe account.
|
|
10
|
+
- Start the app locally with the same webhook route code that will be deployed.
|
|
11
|
+
- Use fake local users and test subscriptions only.
|
|
12
|
+
- Do not paste real API keys, signing secrets, customer data, or production URLs into issue comments or logs.
|
|
13
|
+
- Make sure your handler verifies the `Stripe-Signature` header with Stripe's raw request body before changing billing state.
|
|
14
|
+
|
|
15
|
+
Start local forwarding in one terminal:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
stripe listen --forward-to localhost:3000/api/stripe/webhook
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Copy the signing secret printed by `stripe listen` into your local server-only environment variable. Keep it out of client bundles and public logs.
|
|
22
|
+
|
|
23
|
+
In another terminal, run the scanner first:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx ai-saas-guard@latest check-stripe --root .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use the findings as the review queue. Each finding should map to at least one replay below.
|
|
30
|
+
|
|
31
|
+
## What To Observe
|
|
32
|
+
|
|
33
|
+
For every replay, record these four facts:
|
|
34
|
+
|
|
35
|
+
| Question | Expected evidence |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| Did the webhook return `2xx` only after verification and reconciliation? | Server log or test assertion |
|
|
38
|
+
| Was the Stripe event ID stored or deduped? | `event.id` row, unique key, or idempotency log |
|
|
39
|
+
| Did app entitlement state change correctly? | Plan, access, seat, credit, or subscription status row |
|
|
40
|
+
| Is the user-facing state consistent after refresh? | Dashboard, gated route, API response, or account page |
|
|
41
|
+
|
|
42
|
+
Entitlement reconciliation means the app derives access from Stripe's current billing truth, then writes one durable local state. Typical examples are `plan`, `subscription_status`, `current_period_end`, `cancel_at_period_end`, active seats, credit balance, or an access table keyed by user, organization, customer, or subscription.
|
|
43
|
+
|
|
44
|
+
## Replay Matrix
|
|
45
|
+
|
|
46
|
+
Run the commands one at a time. Watch both the `stripe listen` terminal and your app server logs.
|
|
47
|
+
|
|
48
|
+
| Scenario | Command | What to verify |
|
|
49
|
+
| --- | --- | --- |
|
|
50
|
+
| Checkout success | `stripe trigger checkout.session.completed` | The app maps the Checkout Session to the correct user or tenant and grants access only after signature verification. |
|
|
51
|
+
| Failed renewal | `stripe trigger invoice.payment_failed` | The app marks the subscription past-due, starts a grace path if intended, and does not leave unrestricted paid access forever. |
|
|
52
|
+
| Subscription update | `stripe trigger customer.subscription.updated` | Plan, quantity, cancel-at-period-end, period end, and status changes reconcile into local entitlement state. |
|
|
53
|
+
| Cancellation | `stripe trigger customer.subscription.deleted` | Access is revoked or downgraded deterministically for the correct customer or tenant. |
|
|
54
|
+
| Refund | `stripe trigger charge.refunded` | Refund handling does not accidentally grant access, double-credit an account, or ignore a required downgrade workflow. |
|
|
55
|
+
|
|
56
|
+
If a command is not available in your installed Stripe CLI, run `stripe trigger --help` or the event category help such as `stripe trigger customer.subscription --help`, then use the closest supported test event for the same billing state transition.
|
|
57
|
+
|
|
58
|
+
## Checkout Success
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
stripe trigger checkout.session.completed
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Review checklist:
|
|
65
|
+
|
|
66
|
+
- The handler rejects unsigned requests before any database write.
|
|
67
|
+
- The handler stores or checks the Stripe `event.id`.
|
|
68
|
+
- The session is linked to a local user, organization, or tenant through metadata, customer ID, or subscription ID.
|
|
69
|
+
- The app does not grant access only from the success redirect page.
|
|
70
|
+
- Refreshing the app shows access from database state, not from a one-time URL parameter.
|
|
71
|
+
|
|
72
|
+
`ai-saas-guard` findings this can validate:
|
|
73
|
+
|
|
74
|
+
- `stripe.webhook.missing-signature`
|
|
75
|
+
- `stripe.webhook.no-entitlement-path`
|
|
76
|
+
- `stripe.webhook.missing-idempotency`
|
|
77
|
+
|
|
78
|
+
## Failed Invoice
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
stripe trigger invoice.payment_failed
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Review checklist:
|
|
85
|
+
|
|
86
|
+
- The local subscription is not left as fully active without a documented grace policy.
|
|
87
|
+
- The app records failure state in the same entitlement system used by normal access checks.
|
|
88
|
+
- Customer notification or billing portal recovery is queued if that is part of the product flow.
|
|
89
|
+
- A later recovery event can move the account back to the intended state without manual database edits.
|
|
90
|
+
|
|
91
|
+
`ai-saas-guard` findings this can validate:
|
|
92
|
+
|
|
93
|
+
- `stripe.webhook.missing-critical-event`
|
|
94
|
+
- `stripe.webhook.no-entitlement-path`
|
|
95
|
+
|
|
96
|
+
## Subscription Update
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
stripe trigger customer.subscription.updated
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Review checklist:
|
|
103
|
+
|
|
104
|
+
- Plan upgrades and downgrades update the local plan or entitlement rows.
|
|
105
|
+
- Seat quantity changes do not leave stale access.
|
|
106
|
+
- `cancel_at_period_end` and period boundaries are persisted if the app uses them.
|
|
107
|
+
- The handler reconciles from Stripe identifiers instead of trusting a client-provided user ID.
|
|
108
|
+
|
|
109
|
+
`ai-saas-guard` findings this can validate:
|
|
110
|
+
|
|
111
|
+
- `stripe.webhook.missing-critical-event`
|
|
112
|
+
- `stripe.webhook.no-entitlement-path`
|
|
113
|
+
|
|
114
|
+
## Cancellation
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
stripe trigger customer.subscription.deleted
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Review checklist:
|
|
121
|
+
|
|
122
|
+
- The correct user, organization, or tenant loses paid access.
|
|
123
|
+
- Shared team access is downgraded consistently, not only the account owner.
|
|
124
|
+
- The app handles repeated cancellation events without throwing or creating inconsistent rows.
|
|
125
|
+
- Historical records remain available only according to product policy.
|
|
126
|
+
|
|
127
|
+
`ai-saas-guard` findings this can validate:
|
|
128
|
+
|
|
129
|
+
- `stripe.webhook.missing-critical-event`
|
|
130
|
+
- `stripe.webhook.missing-idempotency`
|
|
131
|
+
|
|
132
|
+
## Refund
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
stripe trigger charge.refunded
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Review checklist:
|
|
139
|
+
|
|
140
|
+
- Refund handling is explicit, even if the intended action is "record and review manually."
|
|
141
|
+
- Credits, invoices, or access extensions are not applied twice.
|
|
142
|
+
- Refund events do not bypass subscription status checks.
|
|
143
|
+
- Support/admin workflows have enough evidence to understand which customer, invoice, and subscription were involved.
|
|
144
|
+
|
|
145
|
+
`ai-saas-guard` findings this can validate:
|
|
146
|
+
|
|
147
|
+
- `stripe.webhook.missing-critical-event`
|
|
148
|
+
- `stripe.webhook.no-entitlement-path`
|
|
149
|
+
|
|
150
|
+
## Duplicate Event Replay
|
|
151
|
+
|
|
152
|
+
Stripe can retry events, and the same event can reach your handler more than once. Your handler should be idempotent around the Stripe event ID and the domain object it mutates.
|
|
153
|
+
|
|
154
|
+
Practical replay:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
stripe trigger checkout.session.completed
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Then replay the same captured event through your own test harness, fixture, or integration test. The important assertion is not that the Stripe CLI creates the same event twice; it is that your app has a test path where the same `event.id` is processed twice and the second attempt is a no-op.
|
|
161
|
+
|
|
162
|
+
Review checklist:
|
|
163
|
+
|
|
164
|
+
- `event.id` is stored with a unique constraint or equivalent dedupe guard.
|
|
165
|
+
- The second delivery returns success without repeating fulfillment.
|
|
166
|
+
- Access grants, invoices, credits, seats, and emails are not duplicated.
|
|
167
|
+
- The handler is safe if the first attempt partially wrote state and then crashed.
|
|
168
|
+
|
|
169
|
+
`ai-saas-guard` findings this can validate:
|
|
170
|
+
|
|
171
|
+
- `stripe.webhook.missing-idempotency`
|
|
172
|
+
|
|
173
|
+
## Out-of-Order Event Questions
|
|
174
|
+
|
|
175
|
+
Stripe events should be treated as signals to reconcile state, not as a guarantee that the app saw every prior transition in the expected order.
|
|
176
|
+
|
|
177
|
+
Use these questions during review:
|
|
178
|
+
|
|
179
|
+
- What happens if `customer.subscription.updated` arrives before the app processed `checkout.session.completed`?
|
|
180
|
+
- What happens if `invoice.payment_failed` arrives after a manual admin upgrade?
|
|
181
|
+
- What happens if `customer.subscription.deleted` arrives after a refund workflow already changed local access?
|
|
182
|
+
- Does the handler fetch or derive current subscription/customer state before writing final entitlement state?
|
|
183
|
+
- Is the local write guarded by Stripe customer, subscription, and tenant ownership, not just by event type?
|
|
184
|
+
|
|
185
|
+
When possible, add tests that call the entitlement reconciliation function directly with events in a different order. The durable result should match Stripe's current billing truth and the product's explicit grace/cancellation policy.
|
|
186
|
+
|
|
187
|
+
## Minimal Acceptance Checklist
|
|
188
|
+
|
|
189
|
+
Before launch or merge, a reviewer should be able to answer yes to each item:
|
|
190
|
+
|
|
191
|
+
- Unsigned webhook requests are rejected before database writes.
|
|
192
|
+
- Raw request body is used for signature verification.
|
|
193
|
+
- Every handled event stores or dedupes `event.id`.
|
|
194
|
+
- `checkout.session.completed` grants access through webhook reconciliation, not only redirect success.
|
|
195
|
+
- `invoice.payment_failed` has an explicit past-due or grace behavior.
|
|
196
|
+
- `customer.subscription.updated` updates plan, quantity, period, cancellation, and status fields used by access checks.
|
|
197
|
+
- `customer.subscription.deleted` revokes or downgrades access for the correct user or tenant.
|
|
198
|
+
- `charge.refunded` has an explicit record, downgrade, or manual review path.
|
|
199
|
+
- Duplicate deliveries are no-ops after the first successful reconciliation.
|
|
200
|
+
- Out-of-order events reconcile to one durable local entitlement state.
|
|
201
|
+
|
|
202
|
+
## Source Links
|
|
203
|
+
|
|
204
|
+
- Stripe CLI trigger docs: https://docs.stripe.com/stripe-cli/triggers
|
|
205
|
+
- Stripe CLI forwarding docs: https://docs.stripe.com/stripe-cli/use-cli
|
|
206
|
+
- Stripe webhook signature docs: https://docs.stripe.com/webhooks/signature
|