ai-saas-guard 0.29.1 → 0.29.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -10
- package/docs/README.zh-CN.md +27 -10
- package/docs/demo-quickstart.md +60 -0
- package/docs/npm-publishing.md +3 -3
- package/examples/demo-risky-saas/.github/workflows/ci.yml +13 -0
- package/examples/demo-risky-saas/README.md +25 -0
- package/examples/demo-risky-saas/app/api/billing/checkout/route.ts +14 -0
- package/examples/demo-risky-saas/app/api/stripe/webhook/route.ts +14 -0
- package/examples/demo-risky-saas/package.json +5 -0
- package/examples/demo-risky-saas/supabase/migrations/001_accounts.sql +14 -0
- package/examples/demo-safe-saas/.env.example +3 -0
- package/examples/demo-safe-saas/.github/workflows/ci.yml +32 -0
- package/examples/demo-safe-saas/README.md +17 -0
- package/examples/demo-safe-saas/app/api/stripe/webhook/route.ts +89 -0
- package/examples/demo-safe-saas/app/api/tenant/[tenantId]/route.ts +18 -0
- package/examples/demo-safe-saas/app/dashboard/[tenantId]/page.tsx +11 -0
- package/examples/demo-safe-saas/next.config.js +22 -0
- package/examples/demo-safe-saas/package.json +5 -0
- package/examples/demo-safe-saas/supabase/migrations/001_projects.sql +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,23 +60,40 @@ npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --
|
|
|
60
60
|
|
|
61
61
|
You get rule IDs, severity, file evidence, why it matters, how to verify it manually, and a concrete fix direction. The scanner is deterministic, read-only, and does not call an LLM.
|
|
62
62
|
|
|
63
|
+
## Try The Demo Fixtures
|
|
64
|
+
|
|
65
|
+
Want to see the report before scanning your own repo?
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
git clone https://github.com/zr9959/ai-saas-guard.git
|
|
69
|
+
cd ai-saas-guard
|
|
70
|
+
npx ai-saas-guard@latest scan --root examples/demo-risky-saas
|
|
71
|
+
npx ai-saas-guard@latest scan --root examples/demo-safe-saas
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The risky demo currently returns 19 intentional findings across Stripe, Supabase, silent-success paths, Next/Vercel deploy hints, and GitHub Actions. The safe demo returns 0 findings for the same broad surfaces with safer static patterns. See [docs/demo-quickstart.md](docs/demo-quickstart.md).
|
|
75
|
+
|
|
63
76
|
## See The Output
|
|
64
77
|
|
|
65
78
|
The report is designed to be read before launch or before merging an AI-heavy PR. A longer copy-paste example is in [docs/sample-launch-report.md](docs/sample-launch-report.md).
|
|
66
79
|
|
|
67
80
|
```text
|
|
68
81
|
Launch Gate: review before launch
|
|
69
|
-
|
|
82
|
+
19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
|
|
70
83
|
|
|
71
|
-
|
|
84
|
+
CRITICAL stripe.webhook.missing-signature
|
|
72
85
|
File: app/api/stripe/webhook/route.ts
|
|
73
86
|
Why: billing access can be granted from a webhook path that does not verify Stripe signatures.
|
|
74
87
|
Verify: replay a webhook with an invalid signature and confirm the route rejects it.
|
|
75
88
|
Fix: read the raw body, call stripe.webhooks.constructEvent, and make event handling idempotent.
|
|
76
89
|
|
|
77
|
-
|
|
78
|
-
File:
|
|
79
|
-
Verify:
|
|
90
|
+
HIGH silent-success.swallowed-error
|
|
91
|
+
File: app/api/billing/checkout/route.ts
|
|
92
|
+
Verify: force the upstream billing call to fail and confirm the route returns an error, not fake success.
|
|
93
|
+
|
|
94
|
+
MEDIUM deploy.next.missing-security-headers
|
|
95
|
+
File: app/api/billing/checkout/route.ts
|
|
96
|
+
Verify: inspect production response headers for auth, billing, and API pages.
|
|
80
97
|
```
|
|
81
98
|
|
|
82
99
|
## What You Get
|
|
@@ -162,13 +179,13 @@ The CLI is published on npm as `ai-saas-guard`, and the GitHub Action is availab
|
|
|
162
179
|
| Area | Status |
|
|
163
180
|
| --- | --- |
|
|
164
181
|
| Public GitHub repository | Available |
|
|
165
|
-
| npm CLI | `ai-saas-guard@0.29.
|
|
166
|
-
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.29.
|
|
182
|
+
| npm CLI | `ai-saas-guard@0.29.2` |
|
|
183
|
+
| GitHub Action | `zr9959/ai-saas-guard@v0` or fixed tag `v0.29.2` |
|
|
167
184
|
| Outputs | Terminal, JSON, SARIF, and PR-focused markdown |
|
|
168
185
|
| Project config | `.ai-saas-guard.json` rule toggles, severity overrides, suppressions, and fail thresholds |
|
|
169
186
|
| Privacy model | Local-first, read-only scan commands, no LLM calls, no code upload |
|
|
170
|
-
| Versioned Action tags | `v0.29.
|
|
171
|
-
| Current release | `0.29.
|
|
187
|
+
| Versioned Action tags | `v0.29.2`, `v0` |
|
|
188
|
+
| Current release | `0.29.2` publishes public risky/safe demo fixtures, a demo quickstart, quickstart feedback template, and refreshed first-run README guidance |
|
|
172
189
|
| npm publishing | Trusted Publisher/OIDC, no long-lived publish token |
|
|
173
190
|
| Repository trust hardening | Strict branch protection, Dependabot, CodeQL, fast-check fuzzing, signed release provenance assets, private vulnerability reporting, secret scanning, and push protection |
|
|
174
191
|
| Cloudflare hosted ingress | Deployed at `https://ai-saas-guard-hosted.zr9959.workers.dev`; signed GitHub App webhook delivery and compact Check Run smoke now pass in staging |
|
|
@@ -335,7 +352,7 @@ Use `suppressions` for narrower false-positive handling when one rule is noisy o
|
|
|
335
352
|
|
|
336
353
|
## GitHub Action
|
|
337
354
|
|
|
338
|
-
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.29.
|
|
355
|
+
The repo includes a composite Action. Use `v0` for the latest compatible pre-1.0 Action, a specific release tag such as `v0.29.2` for controlled upgrades, or pin a reviewed commit SHA for stricter supply-chain control:
|
|
339
356
|
|
|
340
357
|
```yaml
|
|
341
358
|
name: ai-saas-guard
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -59,23 +59,40 @@ npx ai-saas-guard@latest pr-risk --root /path/to/your-saas --base origin/main --
|
|
|
59
59
|
|
|
60
60
|
你会得到 rule ID、severity、文件证据、为什么重要、如何人工验证,以及具体修复方向。扫描是 deterministic、只读的,不调用 LLM。
|
|
61
61
|
|
|
62
|
+
## 先试公开 demo
|
|
63
|
+
|
|
64
|
+
如果你还不想先扫自己的私有仓库,可以先跑公开 fixture:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/zr9959/ai-saas-guard.git
|
|
68
|
+
cd ai-saas-guard
|
|
69
|
+
npx ai-saas-guard@latest scan --root examples/demo-risky-saas
|
|
70
|
+
npx ai-saas-guard@latest scan --root examples/demo-safe-saas
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
risky demo 当前会故意触发 19 个 finding,覆盖 Stripe、Supabase、silent-success、Next/Vercel deploy 提示和 GitHub Actions。safe demo 在同类风险面上使用更安全的静态写法,当前返回 0 个 finding。说明见 [demo-quickstart.md](demo-quickstart.md)。
|
|
74
|
+
|
|
62
75
|
## 输出长什么样
|
|
63
76
|
|
|
64
77
|
报告是给上线前或合并 AI 大 PR 前快速阅读的。更完整的可复制样例见 [docs/sample-launch-report.md](sample-launch-report.md)。
|
|
65
78
|
|
|
66
79
|
```text
|
|
67
80
|
Launch Gate: review before launch
|
|
68
|
-
|
|
81
|
+
19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
|
|
69
82
|
|
|
70
|
-
|
|
83
|
+
CRITICAL stripe.webhook.missing-signature
|
|
71
84
|
File: app/api/stripe/webhook/route.ts
|
|
72
85
|
Why: billing access can be granted from a webhook path that does not verify Stripe signatures.
|
|
73
86
|
Verify: replay a webhook with an invalid signature and confirm the route rejects it.
|
|
74
87
|
Fix: read the raw body, call stripe.webhooks.constructEvent, and make event handling idempotent.
|
|
75
88
|
|
|
76
|
-
|
|
77
|
-
File:
|
|
78
|
-
Verify:
|
|
89
|
+
HIGH silent-success.swallowed-error
|
|
90
|
+
File: app/api/billing/checkout/route.ts
|
|
91
|
+
Verify: force the upstream billing call to fail and confirm the route returns an error, not fake success.
|
|
92
|
+
|
|
93
|
+
MEDIUM deploy.next.missing-security-headers
|
|
94
|
+
File: app/api/billing/checkout/route.ts
|
|
95
|
+
Verify: inspect production response headers for auth, billing, and API pages.
|
|
79
96
|
```
|
|
80
97
|
|
|
81
98
|
## 你会得到什么
|
|
@@ -144,18 +161,18 @@ node dist/cli.js scan --root /path/to/your-saas
|
|
|
144
161
|
|
|
145
162
|
这个仓库是公开 GitHub 仓库。
|
|
146
163
|
|
|
147
|
-
CLI 已发布到 npm:`ai-saas-guard@0.29.
|
|
164
|
+
CLI 已发布到 npm:`ai-saas-guard@0.29.2`。GitHub Action 支持 `v0` 浮动标签,也支持固定版本标签,例如 `v0.29.2`。
|
|
148
165
|
|
|
149
166
|
| 模块 | 状态 |
|
|
150
167
|
| --- | --- |
|
|
151
168
|
| 公开 GitHub 仓库 | 已可用 |
|
|
152
|
-
| npm CLI | `ai-saas-guard@0.29.
|
|
153
|
-
| GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.29.
|
|
169
|
+
| npm CLI | `ai-saas-guard@0.29.2` |
|
|
170
|
+
| GitHub Action | `zr9959/ai-saas-guard@v0` 或固定标签 `v0.29.2` |
|
|
154
171
|
| 输出格式 | Terminal、JSON、SARIF 和 PR markdown |
|
|
155
172
|
| 项目配置 | `.ai-saas-guard.json` 支持规则开关、severity 覆盖、suppressions 和 fail threshold |
|
|
156
173
|
| 隐私模型 | 本地优先、只读扫描、不调用 LLM、不上传代码 |
|
|
157
|
-
| 当前版本 | `0.29.
|
|
158
|
-
| Action 标签 | `v0.29.
|
|
174
|
+
| 当前版本 | `0.29.2` 发布公开 risky/safe demo fixtures、demo quickstart、quickstart 反馈模板,并刷新首次试用 README 指引 |
|
|
175
|
+
| Action 标签 | `v0.29.2`、`v0` |
|
|
159
176
|
| npm 发布 | GitHub Actions Trusted Publisher/OIDC,无需长期 npm token |
|
|
160
177
|
| 仓库可信度加固 | 严格 branch protection、Dependabot、CodeQL、fast-check fuzzing、signed release provenance assets、private vulnerability reporting、secret scanning 和 push protection |
|
|
161
178
|
| Cloudflare hosted ingress | 已部署到 `https://ai-saas-guard-hosted.zr9959.workers.dev`;签名 GitHub App webhook delivery 和 compact Check Run staging smoke 已通过 |
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Demo Quickstart
|
|
2
|
+
|
|
3
|
+
Use these public fixtures when you want to understand `ai-saas-guard` before pointing it at a private repository.
|
|
4
|
+
|
|
5
|
+
## Risky Demo
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx ai-saas-guard@latest scan --root examples/demo-risky-saas
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The risky demo intentionally includes unsigned Stripe webhook handling, a silent-success billing fallback, broad Supabase RLS, and overpowered GitHub Actions permissions.
|
|
12
|
+
|
|
13
|
+
Expected summary:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
19 findings: 2 critical, 6 high, 7 medium, 3 low, 1 info
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The first findings should point at trust-boundary files such as:
|
|
20
|
+
|
|
21
|
+
- `app/api/stripe/webhook/route.ts` for missing Stripe signature verification and idempotency
|
|
22
|
+
- `supabase/migrations/001_accounts.sql` for broad RLS and missing tenant predicates
|
|
23
|
+
- `app/api/billing/checkout/route.ts` for a silent-success billing fallback
|
|
24
|
+
- `.github/workflows/ci.yml` for launch-related workflow hygiene hints
|
|
25
|
+
|
|
26
|
+
For local development from this repository checkout:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm ci
|
|
30
|
+
npm run build
|
|
31
|
+
node dist/cli.js scan --root examples/demo-risky-saas
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Safe Demo
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx ai-saas-guard@latest scan --root examples/demo-safe-saas
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The safe demo keeps the same broad surfaces but uses safer static patterns: Stripe signature verification and idempotency hints, scoped RLS, security headers, documented env variables, request IDs, and bounded GitHub Actions permissions.
|
|
41
|
+
|
|
42
|
+
Expected summary:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
0 findings
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For local development from this repository checkout:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
node dist/cli.js scan --root examples/demo-safe-saas
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## What To Look For
|
|
55
|
+
|
|
56
|
+
- Every finding has a rule ID, severity, file evidence, why it matters, a manual verification step, and a fix direction.
|
|
57
|
+
- The risky demo is intentionally noisy enough to show the report shape.
|
|
58
|
+
- The safe demo is intentionally small; it is not a complete SaaS template and does not certify a real app.
|
|
59
|
+
|
|
60
|
+
Do not paste real API keys, customer data, private source code, webhook secrets, or production URLs into public issues when sharing output.
|
package/docs/npm-publishing.md
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
## Current State
|
|
6
6
|
|
|
7
7
|
- Package name: `ai-saas-guard`
|
|
8
|
-
- Current published version: `0.29.
|
|
8
|
+
- Current published version: `0.29.2`
|
|
9
9
|
- Next source candidate: none
|
|
10
10
|
- npm registry state: published at <https://www.npmjs.com/package/ai-saas-guard>
|
|
11
11
|
- First npm-published version: `0.1.1`
|
|
12
|
-
- GitHub Release: `v0.29.
|
|
12
|
+
- GitHub Release: `v0.29.2`
|
|
13
13
|
- Publish workflow: `.github/workflows/npm-publish.yml`
|
|
14
14
|
- Trusted Publisher: GitHub Actions, `zr9959/ai-saas-guard`, workflow `npm-publish.yml`, allowed action `npm publish`
|
|
15
15
|
- Long-lived npm publish token: not required
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
Use GitHub Actions with npm Trusted Publisher/OIDC:
|
|
20
20
|
|
|
21
|
-
1. Create and review a release tag such as `v0.29.
|
|
21
|
+
1. Create and review a release tag such as `v0.29.2`.
|
|
22
22
|
2. Publish from the GitHub Release or run the `Publish npm` workflow manually with `ref` set to that tag.
|
|
23
23
|
3. Keep `permissions.id-token: write` in the workflow so npm can exchange the GitHub Actions OIDC identity for a short-lived publish credential.
|
|
24
24
|
4. Run `npm publish --access public` from the workflow. Trusted publishing automatically generates provenance for this public package from this public repository.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Risky Demo SaaS
|
|
2
|
+
|
|
3
|
+
This is a tiny public fixture that intentionally contains common AI-built SaaS launch risks.
|
|
4
|
+
|
|
5
|
+
Run from the repository root:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
node dist/cli.js scan --root examples/demo-risky-saas
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with the published package:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx ai-saas-guard@latest scan --root examples/demo-risky-saas
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Expected themes:
|
|
18
|
+
|
|
19
|
+
- unsigned Stripe webhook handling
|
|
20
|
+
- silent-success billing fallback
|
|
21
|
+
- broad Supabase RLS policy
|
|
22
|
+
- over-broad GitHub Actions permissions
|
|
23
|
+
- stale PR workflow risk
|
|
24
|
+
|
|
25
|
+
This fixture uses inert placeholder code only. It does not contain real secrets, customer data, or production URLs.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export async function POST() {
|
|
2
|
+
try {
|
|
3
|
+
return Response.json({ checkoutUrl: await createCheckout() });
|
|
4
|
+
} catch {
|
|
5
|
+
return Response.json({
|
|
6
|
+
success: true,
|
|
7
|
+
checkoutUrl: "/billing/demo-success"
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function createCheckout() {
|
|
13
|
+
throw new Error("provider unavailable");
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export async function POST(req: Request) {
|
|
2
|
+
const event = await req.json();
|
|
3
|
+
|
|
4
|
+
if (event.type === "checkout.session.completed") {
|
|
5
|
+
await grantAccess(event.data.object.customer);
|
|
6
|
+
return Response.json({ success: true });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return Response.json({ ok: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function grantAccess(customerId: string) {
|
|
13
|
+
console.log("granting access", customerId);
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
create table public.accounts (
|
|
2
|
+
id uuid primary key,
|
|
3
|
+
tenant_id uuid not null,
|
|
4
|
+
user_id uuid not null,
|
|
5
|
+
plan text not null
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
alter table public.accounts enable row level security;
|
|
9
|
+
|
|
10
|
+
create policy "public read accounts"
|
|
11
|
+
on public.accounts
|
|
12
|
+
for select
|
|
13
|
+
to public
|
|
14
|
+
using (true);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Safe demo CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
paths-ignore:
|
|
6
|
+
- "docs/**"
|
|
7
|
+
- "*.md"
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
env:
|
|
20
|
+
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
|
|
23
|
+
with:
|
|
24
|
+
fetch-depth: 0
|
|
25
|
+
persist-credentials: false
|
|
26
|
+
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
|
|
27
|
+
with:
|
|
28
|
+
node-version: 20
|
|
29
|
+
- run: test -n "${STRIPE_SECRET_KEY:-}"
|
|
30
|
+
- run: npm ci
|
|
31
|
+
- run: npx ai-saas-guard pr-risk --base origin/main
|
|
32
|
+
- run: npm test
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Safe Demo SaaS
|
|
2
|
+
|
|
3
|
+
This is a tiny public fixture with safer launch-readiness patterns for the same broad surfaces as the risky demo.
|
|
4
|
+
|
|
5
|
+
Run from the repository root:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
node dist/cli.js scan --root examples/demo-safe-saas
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Expected result:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
No heuristic launch-readiness risks found by this command.
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The fixture is intentionally small. It is not a complete SaaS template and does not prove a real app is secure.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import Stripe from "stripe";
|
|
2
|
+
|
|
3
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
|
|
4
|
+
|
|
5
|
+
export async function POST(req: Request) {
|
|
6
|
+
const requestId = req.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
7
|
+
await rateLimit(req);
|
|
8
|
+
const body = await req.text();
|
|
9
|
+
const signature = req.headers.get("stripe-signature");
|
|
10
|
+
const event = stripe.webhooks.constructEvent(
|
|
11
|
+
body,
|
|
12
|
+
signature!,
|
|
13
|
+
process.env.STRIPE_WEBHOOK_KEY!
|
|
14
|
+
);
|
|
15
|
+
const tenant_id = await resolveTenantForStripeCustomer(event.data.object.customer);
|
|
16
|
+
|
|
17
|
+
console.info("stripe webhook received", { requestId, eventId: event.id, tenant_id });
|
|
18
|
+
|
|
19
|
+
if (await hasProcessedEvent(event.id)) {
|
|
20
|
+
return new Response("ok");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
switch (event.type) {
|
|
24
|
+
case "checkout.session.completed":
|
|
25
|
+
await syncEntitlement(event.data.object.customer);
|
|
26
|
+
break;
|
|
27
|
+
case "invoice.payment_failed":
|
|
28
|
+
await markPastDue(event.data.object.customer);
|
|
29
|
+
break;
|
|
30
|
+
case "invoice.payment_action_required":
|
|
31
|
+
await requestPaymentAction(event.data.object.customer);
|
|
32
|
+
break;
|
|
33
|
+
case "customer.subscription.updated":
|
|
34
|
+
await syncSubscription(event.data.object);
|
|
35
|
+
break;
|
|
36
|
+
case "customer.subscription.deleted":
|
|
37
|
+
await revokeEntitlement(event.data.object.customer);
|
|
38
|
+
break;
|
|
39
|
+
case "charge.refunded":
|
|
40
|
+
await reviewRefund(event.data.object.customer);
|
|
41
|
+
break;
|
|
42
|
+
default:
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await recordProcessedEvent(event.id);
|
|
47
|
+
return new Response("ok");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function hasProcessedEvent(eventId: string) {
|
|
51
|
+
return eventId.length === 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function rateLimit(req: Request) {
|
|
55
|
+
console.log(req.headers.get("x-forwarded-for") ?? "local");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function resolveTenantForStripeCustomer(customerId: unknown) {
|
|
59
|
+
console.log(customerId);
|
|
60
|
+
return String(customerId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function recordProcessedEvent(eventId: string) {
|
|
64
|
+
console.log(eventId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function syncEntitlement(customerId: string) {
|
|
68
|
+
console.log(customerId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function markPastDue(customerId: string) {
|
|
72
|
+
console.log(customerId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function requestPaymentAction(customerId: string) {
|
|
76
|
+
console.log(customerId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function syncSubscription(subscription: unknown) {
|
|
80
|
+
console.log(subscription);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function revokeEntitlement(customerId: string) {
|
|
84
|
+
console.log(customerId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function reviewRefund(customerId: string) {
|
|
88
|
+
console.log(customerId);
|
|
89
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export async function POST(request: Request) {
|
|
2
|
+
const requestId = request.headers.get("x-request-id") ?? crypto.randomUUID();
|
|
3
|
+
await rateLimit(request);
|
|
4
|
+
const payload = await request.json();
|
|
5
|
+
|
|
6
|
+
console.info("tenant update", { requestId, tenantId: payload.tenantId });
|
|
7
|
+
await updateTenantBilling(payload.tenantId, process.env.STRIPE_SECRET_KEY);
|
|
8
|
+
|
|
9
|
+
return Response.json({ ok: true, requestId });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function updateTenantBilling(tenantId: string, stripeKey?: string) {
|
|
13
|
+
console.log(tenantId, Boolean(stripeKey));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function rateLimit(request: Request) {
|
|
17
|
+
console.log(request.headers.get("x-forwarded-for") ?? "local");
|
|
18
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Image from "next/image";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
|
|
4
|
+
export default function TenantDashboard() {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
<Image src="https://cdn.example.com/logo.png" width={240} height={120} alt="" />
|
|
8
|
+
<Link prefetch={false} href="/dashboard/settings">Open settings</Link>
|
|
9
|
+
</>
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
async headers() {
|
|
3
|
+
return [
|
|
4
|
+
{
|
|
5
|
+
source: "/(.*)",
|
|
6
|
+
headers: [
|
|
7
|
+
{ key: "X-Frame-Options", value: "DENY" },
|
|
8
|
+
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
9
|
+
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
];
|
|
13
|
+
},
|
|
14
|
+
images: {
|
|
15
|
+
remotePatterns: [
|
|
16
|
+
{
|
|
17
|
+
protocol: "https",
|
|
18
|
+
hostname: "cdn.example.com"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
create table public.projects (
|
|
2
|
+
id uuid primary key,
|
|
3
|
+
user_id uuid not null,
|
|
4
|
+
name text
|
|
5
|
+
);
|
|
6
|
+
|
|
7
|
+
alter table public.projects enable row level security;
|
|
8
|
+
|
|
9
|
+
create policy "users read own projects"
|
|
10
|
+
on public.projects
|
|
11
|
+
for select
|
|
12
|
+
using (auth.uid() = user_id);
|
|
13
|
+
|
|
14
|
+
create policy "users write own projects"
|
|
15
|
+
on public.projects
|
|
16
|
+
for all
|
|
17
|
+
using (auth.uid() = user_id)
|
|
18
|
+
with check (auth.uid() = user_id);
|
package/package.json
CHANGED