ai-saas-guard 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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.4.0`, `v0` |
55
- | npm package | `ai-saas-guard@0.4.0` |
54
+ | Versioned Action tags | `v0.5.0`, `v0` |
55
+ | npm package | `ai-saas-guard@0.5.0` |
56
56
  | npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
57
57
 
58
58
  ## Quick Start
@@ -170,6 +170,10 @@ 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
+ ## Stripe Webhook Replay
174
+
175
+ 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.
176
+
173
177
  ## Project Configuration
174
178
 
175
179
  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 +195,7 @@ Add `.ai-saas-guard.json` at the repository root to tune findings without changi
191
195
 
192
196
  ## GitHub Action
193
197
 
194
- The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.4.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
198
+ The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.5.0` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
195
199
 
196
200
  ```yaml
197
201
  name: ai-saas-guard
@@ -320,7 +324,6 @@ Open-source core:
320
324
 
321
325
  Near-term priorities:
322
326
 
323
- - Stripe webhook replay cookbook
324
327
  - launch-readiness checklist content
325
328
  - false-positive suppression and rule stability labels
326
329
  - GitHub App design note for the potential hosted layer
@@ -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 files = context.files;
14
- const webhookFiles = files.filter((file) => {
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 = files.filter((file) => !/\.(md|txt)$/i.test(file.path) && !file.path.startsWith("docs/"));
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
+ }
@@ -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.4.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
5
+ Use `zr9959/ai-saas-guard@v0` for the latest compatible pre-1.0 Action. Use a specific tag such as `v0.5.0` or a reviewed commit SHA when reproducibility is more important than automatic minor updates.
6
6
 
7
7
  ## PR Summary
8
8
 
@@ -5,10 +5,10 @@
5
5
  ## Current State
6
6
 
7
7
  - Package name: `ai-saas-guard`
8
- - Current version: `0.4.0`
8
+ - Current version: `0.5.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.4.0`
11
+ - GitHub Release: `v0.5.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.4.0`.
20
+ 1. Create and review a release tag such as `v0.5.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.
@@ -40,6 +40,7 @@ Implemented surfaces:
40
40
 
41
41
  - secret-like values and risky public env exposure
42
42
  - Stripe webhook signature, raw body, idempotency, and lifecycle handler heuristics
43
+ - Stripe webhook replay cookbook for checkout, renewal failure, updates, cancellation, refunds, duplicate delivery, and out-of-order review
43
44
  - Supabase RLS, tenant membership, ownership filter, weak `WITH CHECK`, and storage object policy heuristics
44
45
  - sensitive API route heuristics
45
46
  - MCP config side-effect and secret-bearing risk inventory
@@ -100,7 +101,6 @@ GitHub Project:
100
101
  Current issue set:
101
102
 
102
103
  - #1 Add launch-readiness checklist content
103
- - #5 Write Stripe webhook replay cookbook
104
104
 
105
105
  CI:
106
106
 
@@ -112,7 +112,7 @@ CI:
112
112
  Publishing:
113
113
 
114
114
  - npm package: `ai-saas-guard`
115
- - Current release line: `v0.4.0`
115
+ - Current release line: `v0.5.0`
116
116
  - Publish workflow: `.github/workflows/npm-publish.yml`
117
117
  - Trusted Publisher: GitHub Actions for `zr9959/ai-saas-guard`, workflow `npm-publish.yml`
118
118
  - Long-lived npm publish tokens should not be required.
@@ -139,10 +139,9 @@ Not allowed:
139
139
 
140
140
  Recommended order:
141
141
 
142
- 1. Write Stripe webhook replay cookbook.
143
- 2. Add launch-readiness checklist content.
144
- 3. Improve false-positive suppression and rule stability labels.
145
- 4. Add a GitHub App design note for the potential hosted layer.
142
+ 1. Add launch-readiness checklist content.
143
+ 2. Improve false-positive suppression and rule stability labels.
144
+ 3. 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-saas-guard",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Repo-local launch-readiness scanner for AI-built SaaS apps.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/zr9959/ai-saas-guard#readme",