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.
Files changed (112) hide show
  1. {sourcecode-1.35.35 → sourcecode-1.35.36}/PKG-INFO +3 -3
  2. {sourcecode-1.35.35 → sourcecode-1.35.36}/README.md +2 -2
  3. {sourcecode-1.35.35 → sourcecode-1.35.36}/pyproject.toml +1 -1
  4. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/license.py +6 -1
  6. sourcecode-1.35.36/supabase/functions/README.md +35 -0
  7. sourcecode-1.35.36/supabase/functions/get-license/index.ts +83 -0
  8. sourcecode-1.35.36/supabase/functions/lemonsqueezy-webhook/index.ts +163 -0
  9. {sourcecode-1.35.35 → sourcecode-1.35.36}/.github/workflows/build-windows.yml +0 -0
  10. {sourcecode-1.35.35 → sourcecode-1.35.36}/.gitignore +0 -0
  11. {sourcecode-1.35.35 → sourcecode-1.35.36}/.ruff.toml +0 -0
  12. {sourcecode-1.35.35 → sourcecode-1.35.36}/CHANGELOG.md +0 -0
  13. {sourcecode-1.35.35 → sourcecode-1.35.36}/CONTRIBUTING.md +0 -0
  14. {sourcecode-1.35.35 → sourcecode-1.35.36}/LICENSE +0 -0
  15. {sourcecode-1.35.35 → sourcecode-1.35.36}/SECURITY.md +0 -0
  16. {sourcecode-1.35.35 → sourcecode-1.35.36}/raw +0 -0
  17. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/adaptive_scanner.py +0 -0
  18. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/architecture_analyzer.py +0 -0
  19. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/architecture_summary.py +0 -0
  20. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/ast_extractor.py +0 -0
  21. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/cache.py +0 -0
  22. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/canonical_ir.py +0 -0
  23. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/cir_graphs.py +0 -0
  24. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/classifier.py +0 -0
  25. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/cli.py +0 -0
  26. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/code_notes_analyzer.py +0 -0
  27. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/confidence_analyzer.py +0 -0
  28. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/context_scorer.py +0 -0
  29. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/context_summarizer.py +0 -0
  30. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/contract_model.py +0 -0
  31. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/contract_pipeline.py +0 -0
  32. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/coverage_parser.py +0 -0
  33. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/dependency_analyzer.py +0 -0
  34. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/__init__.py +0 -0
  35. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/base.py +0 -0
  36. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/csproj_parser.py +0 -0
  37. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/dart.py +0 -0
  38. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/dotnet.py +0 -0
  39. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/elixir.py +0 -0
  40. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/go.py +0 -0
  41. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/heuristic.py +0 -0
  42. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/hybrid.py +0 -0
  43. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/java.py +0 -0
  44. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/jvm_ext.py +0 -0
  45. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/nodejs.py +0 -0
  46. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/parsers.py +0 -0
  47. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/php.py +0 -0
  48. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/project.py +0 -0
  49. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/python.py +0 -0
  50. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/ruby.py +0 -0
  51. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/rust.py +0 -0
  52. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/systems.py +0 -0
  53. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/terraform.py +0 -0
  54. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/detectors/tooling.py +0 -0
  55. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/doc_analyzer.py +0 -0
  56. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/entrypoint_classifier.py +0 -0
  57. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/env_analyzer.py +0 -0
  58. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/error_schema.py +0 -0
  59. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/explain.py +0 -0
  60. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/file_chunker.py +0 -0
  61. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/file_classifier.py +0 -0
  62. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/flow_analyzer.py +0 -0
  63. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/fqn_utils.py +0 -0
  64. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/git_analyzer.py +0 -0
  65. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/graph_analyzer.py +0 -0
  66. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/__init__.py +0 -0
  67. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  68. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  69. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  70. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  71. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  72. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/orchestrator.py +0 -0
  73. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/registry.py +0 -0
  74. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/runner.py +0 -0
  75. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp/server.py +0 -0
  76. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/mcp_nudge.py +0 -0
  77. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/metrics_analyzer.py +0 -0
  78. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/migrate_check.py +0 -0
  79. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/output_budget.py +0 -0
  80. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/path_filters.py +0 -0
  81. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/pr_comment_renderer.py +0 -0
  82. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/pr_impact.py +0 -0
  83. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/prepare_context.py +0 -0
  84. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/progress.py +0 -0
  85. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/ranking_engine.py +0 -0
  86. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/redactor.py +0 -0
  87. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/relevance_scorer.py +0 -0
  88. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/rename_refactor.py +0 -0
  89. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/repo_classifier.py +0 -0
  90. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/repository_ir.py +0 -0
  91. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/ris.py +0 -0
  92. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/runtime_classifier.py +0 -0
  93. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/scanner.py +0 -0
  94. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/schema.py +0 -0
  95. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/semantic_analyzer.py +0 -0
  96. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/serializer.py +0 -0
  97. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_event_topology.py +0 -0
  98. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_findings.py +0 -0
  99. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_impact.py +0 -0
  100. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_model.py +0 -0
  101. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_security_audit.py +0 -0
  102. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_semantic.py +0 -0
  103. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/spring_tx_analyzer.py +0 -0
  104. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/summarizer.py +0 -0
  105. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/telemetry/__init__.py +0 -0
  106. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/telemetry/config.py +0 -0
  107. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/telemetry/consent.py +0 -0
  108. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/telemetry/events.py +0 -0
  109. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/telemetry/filters.py +0 -0
  110. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/telemetry/transport.py +0 -0
  111. {sourcecode-1.35.35 → sourcecode-1.35.36}/src/sourcecode/tree_utils.py +0 -0
  112. {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.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
- ![Version](https://img.shields.io/badge/version-1.35.35-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.35.36-blue)
44
44
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
45
45
 
46
46
  ---
@@ -114,7 +114,7 @@ pipx install sourcecode
114
114
 
115
115
  ```bash
116
116
  sourcecode version
117
- # sourcecode 1.35.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
- ![Version](https://img.shields.io/badge/version-1.35.35-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.35.36-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
7
7
 
8
8
  ---
@@ -76,7 +76,7 @@ pipx install sourcecode
76
76
 
77
77
  ```bash
78
78
  sourcecode version
79
- # sourcecode 1.35.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.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"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.35.35"
3
+ __version__ = "1.35.36"
@@ -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
- "", # Set SOURCECODE_SUPABASE_ANON_KEY to your project anon key
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