sourcecode 1.35.35__tar.gz → 1.35.36__tar.gz
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.
- {sourcecode-1.35.35 → sourcecode-1.35.36}/PKG-INFO +3 -3
- {sourcecode-1.35.35 → sourcecode-1.35.36}/README.md +2 -2
- {sourcecode-1.35.35 → sourcecode-1.35.36}/pyproject.toml +1 -1
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/license.py +6 -1
- sourcecode-1.35.36/supabase/functions/README.md +35 -0
- sourcecode-1.35.36/supabase/functions/get-license/index.ts +83 -0
- sourcecode-1.35.36/supabase/functions/lemonsqueezy-webhook/index.ts +163 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/.gitignore +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/.ruff.toml +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/CHANGELOG.md +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/CONTRIBUTING.md +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/LICENSE +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/SECURITY.md +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/raw +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/cli.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/workspace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.35.
|
|
3
|
+
Version: 1.35.36
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
|
|
41
41
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
42
42
|
|
|
43
|
-

|
|
44
44
|

|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -114,7 +114,7 @@ pipx install sourcecode
|
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
sourcecode version
|
|
117
|
-
# sourcecode 1.35.
|
|
117
|
+
# sourcecode 1.35.36
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
---
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -76,7 +76,7 @@ pipx install sourcecode
|
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
sourcecode version
|
|
79
|
-
# sourcecode 1.35.
|
|
79
|
+
# sourcecode 1.35.36
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
---
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sourcecode"
|
|
7
|
-
version = "1.35.
|
|
7
|
+
version = "1.35.36"
|
|
8
8
|
description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -40,10 +40,15 @@ from typing import Optional
|
|
|
40
40
|
# Supabase endpoint config — hardcoded for production; override via env for dev
|
|
41
41
|
# ---------------------------------------------------------------------------
|
|
42
42
|
_DEFAULT_SUPABASE_URL: str = "https://qkndlmyekvujjdgthtmz.supabase.co"
|
|
43
|
+
# Public anon/publishable key — safe to ship in client code. RLS plus the
|
|
44
|
+
# service-role key (server-only, in the Edge Function secrets) protect the data.
|
|
45
|
+
# Paste your project's anon key here so `sourcecode activate` works out of the
|
|
46
|
+
# box; env var still overrides for testing against another project.
|
|
47
|
+
_DEFAULT_SUPABASE_ANON_KEY: str = "sb_publishable_qiJFLWjbBbTqjg-fb0mAGA_cl8PBOKH"
|
|
43
48
|
_SUPABASE_URL: str = os.environ.get("SOURCECODE_SUPABASE_URL", _DEFAULT_SUPABASE_URL)
|
|
44
49
|
_SUPABASE_ANON_KEY: str = os.environ.get(
|
|
45
50
|
"SOURCECODE_SUPABASE_ANON_KEY",
|
|
46
|
-
|
|
51
|
+
_DEFAULT_SUPABASE_ANON_KEY,
|
|
47
52
|
)
|
|
48
53
|
if _SUPABASE_URL != _DEFAULT_SUPABASE_URL:
|
|
49
54
|
sys.stderr.write(
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Supabase Edge Functions
|
|
2
|
+
|
|
3
|
+
Backend for the Pro license flow. The CLI side lives in
|
|
4
|
+
`src/sourcecode/license.py`.
|
|
5
|
+
|
|
6
|
+
## Functions
|
|
7
|
+
|
|
8
|
+
| Function | Purpose | JWT |
|
|
9
|
+
|----------|---------|-----|
|
|
10
|
+
| `get-license` | Validates a license key for `sourcecode activate` and the 30-min revalidation. Returns `{valid, plan, status, features, email}`. | `--no-verify-jwt` |
|
|
11
|
+
| `lemonsqueezy-webhook` | Lemon Squeezy purchase/subscription webhook. Stores the LS native key, sets plan/status, handles revocation. | `--no-verify-jwt` |
|
|
12
|
+
|
|
13
|
+
Both deploy with JWT verification OFF: the CLI authenticates with the public
|
|
14
|
+
publishable key (not a JWT), and the webhook authenticates via HMAC signature.
|
|
15
|
+
|
|
16
|
+
## Secrets (Supabase dashboard -> Edge Functions -> Secrets)
|
|
17
|
+
|
|
18
|
+
- `SUPABASE_URL`
|
|
19
|
+
- `SUPABASE_SERVICE_ROLE_KEY`
|
|
20
|
+
- `LEMON_SQUEEZY_WEBHOOK_SECRET` (webhook only)
|
|
21
|
+
|
|
22
|
+
## Deploy
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
supabase functions deploy get-license --no-verify-jwt
|
|
26
|
+
supabase functions deploy lemonsqueezy-webhook --no-verify-jwt
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Lemon Squeezy config
|
|
30
|
+
|
|
31
|
+
- Keep **Generate license keys** ON for every Pro variant (LS emails the key;
|
|
32
|
+
the webhook stores that same native key — single key system).
|
|
33
|
+
- Subscribe the webhook to: `license_key_created`, `order_created`,
|
|
34
|
+
`subscription_created/updated/resumed/unpaused`,
|
|
35
|
+
`subscription_payment_success`, `subscription_expired`, `subscription_paused`.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
|
|
2
|
+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
|
3
|
+
|
|
4
|
+
// License validation endpoint hit by the CLI's `sourcecode activate <key>` and
|
|
5
|
+
// by the 30-min background revalidation (license.py: _call_get_license).
|
|
6
|
+
// Deploy with --no-verify-jwt: the CLI authenticates with the public
|
|
7
|
+
// publishable key, which is not a legacy-secret JWT. Protection is the exact
|
|
8
|
+
// license_key the caller must present + service-role lookup, not a JWT.
|
|
9
|
+
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
|
|
10
|
+
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
|
11
|
+
|
|
12
|
+
// Same format the CLI validates (license.py:72)
|
|
13
|
+
const LICENSE_KEY_RE = /^[A-Za-z0-9_\-]{1,200}$/;
|
|
14
|
+
|
|
15
|
+
const json = (body: unknown, status = 200) =>
|
|
16
|
+
new Response(JSON.stringify(body), {
|
|
17
|
+
status,
|
|
18
|
+
headers: { "Content-Type": "application/json" },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
serve(async (req: Request) => {
|
|
22
|
+
if (req.method !== "POST") {
|
|
23
|
+
return json({ valid: false, error: "method_not_allowed" }, 405);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let payload: Record<string, unknown>;
|
|
27
|
+
try {
|
|
28
|
+
payload = await req.json();
|
|
29
|
+
} catch {
|
|
30
|
+
return json({ valid: false, error: "invalid_json" }, 400);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const licenseKey = ((payload.license_key as string) ?? "").trim();
|
|
34
|
+
if (!licenseKey || !LICENSE_KEY_RE.test(licenseKey)) {
|
|
35
|
+
return json({ valid: false, error: "invalid_license_format" });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
|
|
39
|
+
|
|
40
|
+
const { data: user, error } = await supabase
|
|
41
|
+
.from("users")
|
|
42
|
+
.select("email, plan, status, features")
|
|
43
|
+
.eq("license_key", licenseKey)
|
|
44
|
+
.maybeSingle();
|
|
45
|
+
|
|
46
|
+
if (error) {
|
|
47
|
+
console.error("DB error", error);
|
|
48
|
+
return json({ valid: false, error: "db_error" }, 500);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!user) {
|
|
52
|
+
return json({ valid: false, error: "license_not_found" });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const active = (user.status ?? "active") === "active";
|
|
56
|
+
const isPro = user.plan === "pro";
|
|
57
|
+
|
|
58
|
+
// Revocation: status != active OR plan != pro -> valid:false.
|
|
59
|
+
// The CLI revalidates every 30 min and clears its cache on this response.
|
|
60
|
+
if (!active || !isPro) {
|
|
61
|
+
return json({
|
|
62
|
+
valid: false,
|
|
63
|
+
error: !isPro ? "not_pro" : "inactive",
|
|
64
|
+
plan: user.plan ?? "free",
|
|
65
|
+
status: user.status ?? "inactive",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// features may arrive as jsonb (array) or as a JSON string — normalize
|
|
70
|
+
let features = user.features as unknown;
|
|
71
|
+
if (typeof features === "string") {
|
|
72
|
+
try { features = JSON.parse(features); } catch { features = []; }
|
|
73
|
+
}
|
|
74
|
+
if (!Array.isArray(features)) features = [];
|
|
75
|
+
|
|
76
|
+
return json({
|
|
77
|
+
valid: true,
|
|
78
|
+
plan: user.plan,
|
|
79
|
+
status: user.status ?? "active",
|
|
80
|
+
features,
|
|
81
|
+
email: user.email ?? "",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { serve } from "https://deno.land/std@0.177.0/http/server.ts";
|
|
2
|
+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
|
3
|
+
|
|
4
|
+
// Lemon Squeezy webhook. Source of truth for plan/status and the license key.
|
|
5
|
+
// Deploy with --no-verify-jwt (Lemon Squeezy does not send a Supabase JWT;
|
|
6
|
+
// HMAC signature is the authentication). Keep "Generate license keys" ON on
|
|
7
|
+
// every Pro variant: LS emails the key to the customer and we store that same
|
|
8
|
+
// native key here, so there is a single key system end to end.
|
|
9
|
+
const LEMON_SQUEEZY_WEBHOOK_SECRET = Deno.env.get("LEMON_SQUEEZY_WEBHOOK_SECRET")!;
|
|
10
|
+
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
|
|
11
|
+
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
|
|
12
|
+
|
|
13
|
+
const PRO_FEATURES = ["impact", "review-pr", "generate-tests", "mcp"];
|
|
14
|
+
|
|
15
|
+
// The license key is delivered by this event (LS generates + emails it):
|
|
16
|
+
const LICENSE_EVENTS = ["license_key_created"];
|
|
17
|
+
// Activate / keep Pro:
|
|
18
|
+
const ACTIVATE_EVENTS = [
|
|
19
|
+
"order_created",
|
|
20
|
+
"subscription_created",
|
|
21
|
+
"subscription_updated",
|
|
22
|
+
"subscription_resumed",
|
|
23
|
+
"subscription_unpaused",
|
|
24
|
+
"subscription_payment_success",
|
|
25
|
+
];
|
|
26
|
+
// Revocation — real end of access. NOT subscription_cancelled: that keeps
|
|
27
|
+
// access until period end; LS sends subscription_expired when it actually ends.
|
|
28
|
+
const REVOKE_EVENTS = [
|
|
29
|
+
"subscription_expired",
|
|
30
|
+
"subscription_paused",
|
|
31
|
+
];
|
|
32
|
+
const HANDLED_EVENTS = [...LICENSE_EVENTS, ...ACTIVATE_EVENTS, ...REVOKE_EVENTS];
|
|
33
|
+
|
|
34
|
+
async function verifySignature(rawBody: string, signature: string): Promise<boolean> {
|
|
35
|
+
if (!signature) return false;
|
|
36
|
+
const encoder = new TextEncoder();
|
|
37
|
+
const key = await crypto.subtle.importKey(
|
|
38
|
+
"raw",
|
|
39
|
+
encoder.encode(LEMON_SQUEEZY_WEBHOOK_SECRET),
|
|
40
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
41
|
+
false,
|
|
42
|
+
["sign"],
|
|
43
|
+
);
|
|
44
|
+
const expected = await crypto.subtle.sign("HMAC", key, encoder.encode(rawBody));
|
|
45
|
+
const expectedHex = Array.from(new Uint8Array(expected))
|
|
46
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
47
|
+
.join("");
|
|
48
|
+
return expectedHex === signature;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const json = (body: unknown, status = 200) =>
|
|
52
|
+
new Response(JSON.stringify(body), {
|
|
53
|
+
status,
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
serve(async (req: Request) => {
|
|
58
|
+
if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });
|
|
59
|
+
|
|
60
|
+
const rawBody = await req.text();
|
|
61
|
+
const signature =
|
|
62
|
+
req.headers.get("X-Signature") ?? req.headers.get("x-signature") ?? "";
|
|
63
|
+
|
|
64
|
+
if (!(await verifySignature(rawBody, signature))) {
|
|
65
|
+
console.error("Invalid webhook signature");
|
|
66
|
+
return new Response("Unauthorized", { status: 401 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let payload: Record<string, unknown>;
|
|
70
|
+
try {
|
|
71
|
+
payload = JSON.parse(rawBody);
|
|
72
|
+
} catch {
|
|
73
|
+
return new Response("Bad request: invalid JSON", { status: 400 });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const meta = payload.meta as Record<string, unknown>;
|
|
77
|
+
const data = payload.data as Record<string, unknown>;
|
|
78
|
+
const eventName = meta?.event_name as string;
|
|
79
|
+
const eventId = meta?.event_id as string | undefined;
|
|
80
|
+
|
|
81
|
+
if (!HANDLED_EVENTS.includes(eventName)) {
|
|
82
|
+
return json({ received: true, skipped: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const attributes = data?.attributes as Record<string, unknown>;
|
|
86
|
+
const email = ((attributes?.user_email ?? attributes?.customer_email) as string ?? "")
|
|
87
|
+
.toLowerCase();
|
|
88
|
+
|
|
89
|
+
if (!email || !email.includes("@")) {
|
|
90
|
+
console.error("No valid email in payload", { eventName });
|
|
91
|
+
return new Response("Bad request: no email", { status: 400 });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
|
|
95
|
+
|
|
96
|
+
// Idempotency
|
|
97
|
+
if (eventId) {
|
|
98
|
+
const { data: existing } = await supabase
|
|
99
|
+
.from("license_events").select("id").eq("event_id", eventId).maybeSingle();
|
|
100
|
+
if (existing) return json({ received: true, duplicate: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { data: existingUser } = await supabase
|
|
104
|
+
.from("users").select("id, license_key").eq("email", email).maybeSingle();
|
|
105
|
+
|
|
106
|
+
let userId = existingUser?.id;
|
|
107
|
+
const now = new Date().toISOString();
|
|
108
|
+
|
|
109
|
+
// #3 license_key_created -> store the native Lemon Squeezy key
|
|
110
|
+
if (LICENSE_EVENTS.includes(eventName)) {
|
|
111
|
+
const lsKey = attributes?.key as string;
|
|
112
|
+
if (!lsKey) {
|
|
113
|
+
console.error("license_key_created without attributes.key");
|
|
114
|
+
return new Response("Bad request: no key", { status: 400 });
|
|
115
|
+
}
|
|
116
|
+
const { data: up, error } = await supabase.from("users").upsert(
|
|
117
|
+
{ email, plan: "pro", status: "active", features: PRO_FEATURES,
|
|
118
|
+
license_key: lsKey, updated_at: now },
|
|
119
|
+
{ onConflict: "email", ignoreDuplicates: false },
|
|
120
|
+
).select("id").single();
|
|
121
|
+
if (error) { console.error("upsert key", error); return json({ error: "DB" }, 500); }
|
|
122
|
+
userId = up?.id ?? userId;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// #4 Revocation -> status inactive (does NOT touch license_key or plan)
|
|
126
|
+
else if (REVOKE_EVENTS.includes(eventName)) {
|
|
127
|
+
const { error } = await supabase.from("users")
|
|
128
|
+
.update({ status: "inactive", updated_at: now }).eq("email", email);
|
|
129
|
+
if (error) console.error("revoke", error);
|
|
130
|
+
if (userId) {
|
|
131
|
+
await supabase.from("subscriptions").update({ status: "inactive" }).eq("user_id", userId);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Activation -> plan pro + active (preserves existing license_key)
|
|
136
|
+
else {
|
|
137
|
+
const { data: up, error } = await supabase.from("users").upsert(
|
|
138
|
+
{ email, plan: "pro", status: "active", features: PRO_FEATURES, updated_at: now },
|
|
139
|
+
{ onConflict: "email", ignoreDuplicates: false },
|
|
140
|
+
).select("id").single();
|
|
141
|
+
if (error) { console.error("upsert activate", error); return json({ error: "DB" }, 500); }
|
|
142
|
+
userId = up?.id ?? userId;
|
|
143
|
+
|
|
144
|
+
const periodEnd = (attributes?.renews_at ?? attributes?.ends_at ?? null) as string | null;
|
|
145
|
+
await supabase.from("subscriptions").upsert(
|
|
146
|
+
{ user_id: userId, provider: "lemonsqueezy", status: "active",
|
|
147
|
+
current_period_end: periodEnd, created_at: now },
|
|
148
|
+
{ onConflict: "user_id" },
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Audit
|
|
153
|
+
const { error: evErr } = await supabase.from("license_events").insert({
|
|
154
|
+
user_id: userId ?? null,
|
|
155
|
+
event_type: eventName,
|
|
156
|
+
event_id: eventId ?? null,
|
|
157
|
+
payload: JSON.parse(JSON.stringify(payload)),
|
|
158
|
+
});
|
|
159
|
+
if (evErr) console.error("license_event insert", evErr);
|
|
160
|
+
|
|
161
|
+
console.log(`Processed ${eventName} for ${email}`);
|
|
162
|
+
return json({ received: true, email, event: eventName });
|
|
163
|
+
});
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|