@sota-io/mcp 1.3.0 → 1.4.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.
Files changed (44) hide show
  1. package/CLAUDE.md +13 -5
  2. package/README.md +82 -1
  3. package/dist/auth/disposable-check.d.ts +2 -0
  4. package/dist/auth/disposable-check.d.ts.map +1 -0
  5. package/dist/auth/disposable-check.js +30 -0
  6. package/dist/auth/disposable-check.js.map +1 -0
  7. package/dist/auth/token-bridge.d.ts +28 -0
  8. package/dist/auth/token-bridge.d.ts.map +1 -0
  9. package/dist/auth/token-bridge.js +152 -0
  10. package/dist/auth/token-bridge.js.map +1 -0
  11. package/dist/auth/verify-jwt.d.ts +13 -0
  12. package/dist/auth/verify-jwt.d.ts.map +1 -0
  13. package/dist/auth/verify-jwt.js +84 -0
  14. package/dist/auth/verify-jwt.js.map +1 -0
  15. package/dist/http.d.ts +21 -0
  16. package/dist/http.d.ts.map +1 -0
  17. package/dist/http.js +259 -0
  18. package/dist/http.js.map +1 -0
  19. package/dist/tools/create-account.d.ts +23 -0
  20. package/dist/tools/create-account.d.ts.map +1 -0
  21. package/dist/tools/create-account.js +171 -0
  22. package/dist/tools/create-account.js.map +1 -0
  23. package/dist/tools/deploy.d.ts.map +1 -1
  24. package/dist/tools/deploy.js +2 -1
  25. package/dist/tools/deploy.js.map +1 -1
  26. package/dist/tools/domains.d.ts.map +1 -1
  27. package/dist/tools/domains.js +4 -0
  28. package/dist/tools/domains.js.map +1 -1
  29. package/dist/tools/env.d.ts.map +1 -1
  30. package/dist/tools/env.js +4 -2
  31. package/dist/tools/env.js.map +1 -1
  32. package/dist/tools/logs.d.ts.map +1 -1
  33. package/dist/tools/logs.js +1 -0
  34. package/dist/tools/logs.js.map +1 -1
  35. package/dist/tools/projects.d.ts.map +1 -1
  36. package/dist/tools/projects.js +3 -0
  37. package/dist/tools/projects.js.map +1 -1
  38. package/dist/tools/rollback.d.ts.map +1 -1
  39. package/dist/tools/rollback.js +1 -0
  40. package/dist/tools/rollback.js.map +1 -1
  41. package/dist/tools/status.d.ts.map +1 -1
  42. package/dist/tools/status.js +1 -0
  43. package/dist/tools/status.js.map +1 -1
  44. package/package.json +14 -7
package/CLAUDE.md CHANGED
@@ -39,11 +39,17 @@ You do NOT need to:
39
39
  - Configure connection strings
40
40
  - Set up SSL for the database (internal network, sslmode=disable is safe)
41
41
 
42
- If your app needs tables, run migrations on startup:
43
- - **Prisma**: `npx prisma migrate deploy` in your start script
44
- - **Drizzle**: `npx drizzle-kit push` or migrate on startup
45
- - **SQLAlchemy/Alembic**: `alembic upgrade head` in entrypoint
46
- - **Django**: `python manage.py migrate` before `gunicorn`
42
+ If your app needs tables, run migrations on startup — there is no shell access to containers. Chain the migration command before your server start:
43
+
44
+ - **Prisma**: Use `npx prisma db push && node server.js` as your start script. Note: use `db push` (not `migrate deploy`) because PgBouncer transaction mode blocks advisory locks. Add `?pgbouncer=true` to DATABASE_URL in your Prisma schema.
45
+ - **Drizzle**: `npx drizzle-kit push && node server.js` applies schema changes without migration files, works with PgBouncer.
46
+ - **TypeORM**: `npx typeorm migration:run -d dist/data-source.js && node dist/server.js`
47
+ - **Sequelize**: `npx sequelize-cli db:migrate && node server.js`
48
+ - **Knex**: `npx knex migrate:latest && node server.js`
49
+ - **Django**: `python manage.py migrate --noinput && gunicorn myproject.wsgi --bind 0.0.0.0:$PORT`
50
+ - **Alembic**: `alembic upgrade head && uvicorn app.main:app --host 0.0.0.0 --port $PORT`
51
+
52
+ Full migration guide: https://sota.io/docs/guides/postgresql
47
53
 
48
54
  ## Environment Variables
49
55
 
@@ -115,6 +121,8 @@ module.exports = { output: 'standalone' }
115
121
  | App not accessible | Use `get-status` — must show "running". Check health check (60s timeout). |
116
122
  | Env vars not applied | Changing env vars requires redeployment. Call `deploy` again. |
117
123
  | Next.js NEXT_PUBLIC_* empty | Set these BEFORE deploying (they're embedded at build time). |
124
+ | Migrations not running | Start script must chain migration before server. Example: `"start": "npx prisma db push && node server.js"` |
125
+ | `prepared statement already exists` | PgBouncer conflict — add `?pgbouncer=true` to DATABASE_URL in Prisma schema. |
118
126
 
119
127
  ## Links
120
128
 
package/README.md CHANGED
@@ -7,7 +7,33 @@ MCP server for [sota.io](https://sota.io) — deploy web apps via AI agents.
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
8
  [![Node](https://img.shields.io/badge/node-%3E%3D20-green)](https://nodejs.org)
9
9
 
10
- ## Quick Start
10
+ ## Two transports
11
+
12
+ Since v1.4.0 this package ships **two transports**:
13
+
14
+ - **`sota-mcp`** (stdio, default) — for Claude Code, Cursor, Windsurf, and any
15
+ MCP client that spawns a local process. Pass your `SOTA_API_KEY` via env var.
16
+ - **`sota-mcp-http`** (Streamable HTTP) — for self-hosting the remote endpoint
17
+ that powers `mcp.sota.io` (used by Claude Desktop and Claude.ai web). Reads
18
+ `SUPABASE_JWT_SECRET`, `DATABASE_URL`, etc. — most users do not need this;
19
+ it's the same code that runs on `mcp.sota.io` if you want to host your own.
20
+
21
+ Most users want the stdio transport.
22
+
23
+ ## One-click install for Claude Desktop / Claude.ai web
24
+
25
+ If you use Claude Desktop or Claude.ai (Pro / Max / Team / Enterprise plan),
26
+ the easiest install is **not** this npm package — it's the hosted remote
27
+ endpoint at `mcp.sota.io`. Click here:
28
+
29
+ [**Add to Claude →**](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=sota.io&connectorUrl=https%3A%2F%2Fmcp.sota.io%2Fmcp)
30
+
31
+ OAuth handles auth. New users can sign up entirely inside Claude via the
32
+ `create_account` tool — no browser tab switch.
33
+
34
+ See [https://sota.io/docs/integrations/claude](https://sota.io/docs/integrations/claude).
35
+
36
+ ## Quick Start (stdio — Claude Code, Cursor, Windsurf, …)
11
37
 
12
38
  1. Get an API key from [sota.io/dashboard/settings](https://sota.io/dashboard/settings)
13
39
  2. [Configure your IDE](#configuration)
@@ -130,6 +156,10 @@ Edit `~/.codeium/windsurf/mcp_config.json`:
130
156
  | `list-projects` | List all projects | *(none)* |
131
157
  | `create-project` | Create a new project | `name` |
132
158
  | `delete-project` | Delete a project permanently | `project_id` |
159
+ | `add-domain` | Add custom domain to project | `project_id`, `domain` |
160
+ | `list-domains` | List custom domains | `project_id` |
161
+ | `get-domain` | Get domain details and DNS status | `project_id`, `domain_id` |
162
+ | `remove-domain` | Remove custom domain | `project_id`, `domain_id` |
133
163
 
134
164
  ### `deploy`
135
165
 
@@ -241,6 +271,57 @@ Delete a project and all its deployments from sota.io. This action is permanent.
241
271
  "Delete my sota.io project abc123"
242
272
  ```
243
273
 
274
+ ### `add-domain`
275
+
276
+ Add a custom domain to a project. Returns DNS instructions for pointing the domain.
277
+
278
+ | Parameter | Type | Required | Description |
279
+ |-----------|------|----------|-------------|
280
+ | `project_id` | string | Yes | Project ID |
281
+ | `domain` | string | Yes | Domain name (e.g., "example.com" or "app.example.com") |
282
+
283
+ ```
284
+ "Add example.com as a custom domain to my project"
285
+ ```
286
+
287
+ ### `list-domains`
288
+
289
+ List all custom domains for a project.
290
+
291
+ | Parameter | Type | Required | Description |
292
+ |-----------|------|----------|-------------|
293
+ | `project_id` | string | Yes | Project ID |
294
+
295
+ ```
296
+ "Show all custom domains for my project"
297
+ ```
298
+
299
+ ### `get-domain`
300
+
301
+ Get domain details including DNS verification status and SSL state.
302
+
303
+ | Parameter | Type | Required | Description |
304
+ |-----------|------|----------|-------------|
305
+ | `project_id` | string | Yes | Project ID |
306
+ | `domain_id` | string | Yes | Domain ID |
307
+
308
+ ```
309
+ "Check the DNS status of my custom domain"
310
+ ```
311
+
312
+ ### `remove-domain`
313
+
314
+ Remove a custom domain from a project.
315
+
316
+ | Parameter | Type | Required | Description |
317
+ |-----------|------|----------|-------------|
318
+ | `project_id` | string | Yes | Project ID |
319
+ | `domain_id` | string | Yes | Domain ID to remove |
320
+
321
+ ```
322
+ "Remove the custom domain from my project"
323
+ ```
324
+
244
325
  ## Environment Variables
245
326
 
246
327
  | Variable | Required | Default | Description |
@@ -0,0 +1,2 @@
1
+ export declare function isDisposableEmailDomain(email: string): Promise<boolean>;
2
+ //# sourceMappingURL=disposable-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disposable-check.d.ts","sourceRoot":"","sources":["../../src/auth/disposable-check.ts"],"names":[],"mappings":"AAYA,wBAAsB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAgB7E"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Disposable-email domain check — server-side gate for create_account tool.
3
+ *
4
+ * The web /api/auth/check-email route uses a build-time-embedded TS module
5
+ * generated from the maintained github.com/disposable-email-domains list.
6
+ * Here in mcp-server we don't want to duplicate the 5500-domain list, so we
7
+ * just call the web's check endpoint (it's already public, no auth, returns
8
+ * 200/422). One round-trip per create_account Step 1.
9
+ */
10
+ const CHECK_URL = process.env.DISPOSABLE_EMAIL_CHECK_URL ?? "https://sota.io/api/auth/check-email";
11
+ export async function isDisposableEmailDomain(email) {
12
+ try {
13
+ const r = await fetch(CHECK_URL, {
14
+ method: "POST",
15
+ headers: { "Content-Type": "application/json" },
16
+ body: JSON.stringify({ email }),
17
+ signal: AbortSignal.timeout(5_000),
18
+ });
19
+ if (r.status === 422)
20
+ return true;
21
+ return false;
22
+ }
23
+ catch {
24
+ // Fail open on network error — we'd rather let a real user through
25
+ // than block them because our own /api/auth/check-email had a hiccup.
26
+ // Supabase OTP rate-limits will still catch volume abuse.
27
+ return false;
28
+ }
29
+ }
30
+ //# sourceMappingURL=disposable-check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"disposable-check.js","sourceRoot":"","sources":["../../src/auth/disposable-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,SAAS,GACb,OAAO,CAAC,GAAG,CAAC,0BAA0B,IAAI,sCAAsC,CAAC;AAEnF,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,KAAa;IACzD,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;YAC/B,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC;YAC/B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC;SACnC,CAAC,CAAC;QACH,IAAI,CAAC,CAAC,MAAM,KAAK,GAAG;YAAE,OAAO,IAAI,CAAC;QAClC,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,mEAAmE;QACnE,sEAAsE;QACtE,0DAA0D;QAC1D,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,28 @@
1
+ export interface BridgeResult {
2
+ /** The sota_xxx key plaintext to forward to sota-api on this request */
3
+ apiKey: string;
4
+ /** Whether a new key row was created (true on first sight of this user) */
5
+ created: boolean;
6
+ /** Whether the existing DB row was rotated (true on cold-start with pre-existing row) */
7
+ rotated: boolean;
8
+ }
9
+ /**
10
+ * Net-new user provisioning: called from the `create_account` tool after
11
+ * Supabase verified the OTP. Inserts the user_plans Free-tier row (if not
12
+ * already present) AND a `source='claude'` API key. Returns the key plaintext.
13
+ *
14
+ * Idempotent on user_plans (ON CONFLICT DO NOTHING) — covers the case where
15
+ * the user already exists with a plan but no Claude key (e.g. they signed up
16
+ * via the web first, then called create_account from Claude).
17
+ */
18
+ export declare function provisionPlanAndKey(userId: string): Promise<BridgeResult>;
19
+ /**
20
+ * Look up or provision a `source='claude'` API key for the given user.
21
+ *
22
+ * On cold start (no in-memory plaintext), if a DB row already exists we have
23
+ * no plaintext recoverable. Solution: rotate — delete the old row, insert a
24
+ * fresh key, return the new plaintext. The Claude integration continues to
25
+ * work transparently because sota-mcp does the lookup on every request.
26
+ */
27
+ export declare function getOrProvisionApiKey(userId: string): Promise<BridgeResult>;
28
+ //# sourceMappingURL=token-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-bridge.d.ts","sourceRoot":"","sources":["../../src/auth/token-bridge.ts"],"names":[],"mappings":"AAgGA,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAC;IACjB,yFAAyF;IACzF,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,YAAY,CAAC,CAqBvB;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,YAAY,CAAC,CAqCvB"}
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Token-to-Key bridge: maps a verified Claude JWT to an internal sota_xxx API key.
3
+ *
4
+ * Why: sota-api authenticates by `sota_xxx` keys (SHA-256 hashed in DB). Claude
5
+ * gives us a JWT. We don't pass the JWT through to sota-api — instead, on first
6
+ * sight of a JWT.sub, we provision a dedicated key tagged `source='claude'` and
7
+ * cache the mapping in-memory + persisted in DB.
8
+ *
9
+ * Benefits:
10
+ * - One revocation surface for Marx: "delete all source=claude keys for user X"
11
+ * immediately cuts off the Claude integration without touching the user's
12
+ * CLI/dashboard keys
13
+ * - Audit log can separate Claude-originated tool calls from CLI tool calls
14
+ * - Future plan-gating happens at the existing sota-api layer (no duplicate
15
+ * enforcement in sota-mcp)
16
+ *
17
+ * Lifecycle:
18
+ * 1. JWT in (verified, sub = user_id)
19
+ * 2. SELECT id, key_prefix FROM api_keys WHERE user_id=$1 AND source='claude'
20
+ * LIMIT 1 -- one Claude key per user, reused across requests
21
+ * 3. If not found:
22
+ * a. Generate sota_ + 40 hex (20 bytes random)
23
+ * b. SHA-256 the full key
24
+ * c. INSERT INTO api_keys (user_id, name='Claude MCP', key_hash, key_prefix,
25
+ * source='claude')
26
+ * d. Hold the plaintext only in memory for this request
27
+ * 4. Cache user_id → plaintext key for the process lifetime (5-min TTL; new
28
+ * keys reuse the in-memory cache after restart by regenerating on first
29
+ * hit — that's wasteful only on cold start, acceptable)
30
+ *
31
+ * IMPORTANT: we never get the plaintext back from the DB on subsequent hits.
32
+ * The plaintext is generated ONCE per user, cached in-memory while the
33
+ * process lives. After a process restart, we have no plaintext for users
34
+ * whose key already exists in DB → solution: REGENERATE (delete old row,
35
+ * insert new). That's safe because each Claude session re-binds to whatever
36
+ * key is current.
37
+ */
38
+ import crypto from "node:crypto";
39
+ import { Pool } from "pg";
40
+ const DATABASE_URL = process.env.DATABASE_URL ?? "";
41
+ let pool = null;
42
+ function getPool() {
43
+ if (!pool) {
44
+ if (!DATABASE_URL) {
45
+ throw new Error("DATABASE_URL not configured — sota-mcp cannot bridge tokens to API keys");
46
+ }
47
+ // Supabase pooler uses a CA chain pg can't verify by default. Strip
48
+ // ?sslmode= from the URL and force a permissive TLS config explicitly,
49
+ // matching what the rest of the sota.io stack does.
50
+ const cleanUrl = DATABASE_URL.replace(/[?&]sslmode=[^&]*/i, "");
51
+ pool = new Pool({
52
+ connectionString: cleanUrl,
53
+ max: 5,
54
+ idleTimeoutMillis: 30_000,
55
+ ssl: { rejectUnauthorized: false, require: true },
56
+ });
57
+ pool.on("error", (err) => {
58
+ // Best-effort logging; pool keeps working across single-conn errors
59
+ // eslint-disable-next-line no-console
60
+ console.error("pg pool error:", err.message);
61
+ });
62
+ }
63
+ return pool;
64
+ }
65
+ // In-memory cache: userId → { plaintextKey, expiresAt }
66
+ // TTL is short (5 min) because we'd rather rotate-on-restart than ever leak.
67
+ const cache = new Map();
68
+ const CACHE_TTL_MS = 5 * 60_000;
69
+ function cacheGet(userId) {
70
+ const entry = cache.get(userId);
71
+ if (!entry)
72
+ return null;
73
+ if (entry.expiresAt < Date.now()) {
74
+ cache.delete(userId);
75
+ return null;
76
+ }
77
+ return entry.plaintext;
78
+ }
79
+ function cacheSet(userId, plaintext) {
80
+ cache.set(userId, { plaintext, expiresAt: Date.now() + CACHE_TTL_MS });
81
+ }
82
+ function generateKey() {
83
+ const randBytes = crypto.randomBytes(20);
84
+ const plaintext = "sota_" + randBytes.toString("hex"); // sota_ + 40 hex
85
+ const hash = crypto.createHash("sha256").update(plaintext).digest("hex");
86
+ const prefix = plaintext.slice(0, 13); // "sota_" + first 8 hex
87
+ return { plaintext, hash, prefix };
88
+ }
89
+ /**
90
+ * Net-new user provisioning: called from the `create_account` tool after
91
+ * Supabase verified the OTP. Inserts the user_plans Free-tier row (if not
92
+ * already present) AND a `source='claude'` API key. Returns the key plaintext.
93
+ *
94
+ * Idempotent on user_plans (ON CONFLICT DO NOTHING) — covers the case where
95
+ * the user already exists with a plan but no Claude key (e.g. they signed up
96
+ * via the web first, then called create_account from Claude).
97
+ */
98
+ export async function provisionPlanAndKey(userId) {
99
+ const client = await getPool().connect();
100
+ try {
101
+ // Free-tier defaults (mirrors api/internal/repository/user_plan.go defaults)
102
+ await client.query(`INSERT INTO user_plans (
103
+ user_id, plan, max_projects, max_memory_mb, max_cpu_millicores,
104
+ max_databases, max_custom_domains, max_builds_per_day,
105
+ source, onboarded_at
106
+ ) VALUES ($1, 'free', 3, 256, 500, 1, 0, 10, 'claude', now())
107
+ ON CONFLICT (user_id) DO NOTHING`, [userId]);
108
+ }
109
+ catch (err) {
110
+ throw new Error(`user_plans provisioning failed: ${err instanceof Error ? err.message : String(err)}`);
111
+ }
112
+ finally {
113
+ client.release();
114
+ }
115
+ return getOrProvisionApiKey(userId);
116
+ }
117
+ /**
118
+ * Look up or provision a `source='claude'` API key for the given user.
119
+ *
120
+ * On cold start (no in-memory plaintext), if a DB row already exists we have
121
+ * no plaintext recoverable. Solution: rotate — delete the old row, insert a
122
+ * fresh key, return the new plaintext. The Claude integration continues to
123
+ * work transparently because sota-mcp does the lookup on every request.
124
+ */
125
+ export async function getOrProvisionApiKey(userId) {
126
+ const cached = cacheGet(userId);
127
+ if (cached)
128
+ return { apiKey: cached, created: false, rotated: false };
129
+ const client = await getPool().connect();
130
+ try {
131
+ const existing = await client.query(`SELECT id FROM api_keys WHERE user_id = $1 AND source = 'claude' LIMIT 1`, [userId]);
132
+ const { plaintext, hash, prefix } = generateKey();
133
+ if (existing.rowCount === 0) {
134
+ // First-time: simple INSERT
135
+ await client.query(`INSERT INTO api_keys (user_id, name, key_hash, key_prefix, source)
136
+ VALUES ($1, $2, $3, $4, 'claude')`, [userId, "Claude MCP", hash, prefix]);
137
+ cacheSet(userId, plaintext);
138
+ return { apiKey: plaintext, created: true, rotated: false };
139
+ }
140
+ // Cold-start rotation: replace the existing row's hash + prefix so the
141
+ // freshly minted plaintext becomes the active key for this user.
142
+ await client.query(`UPDATE api_keys
143
+ SET key_hash = $2, key_prefix = $3, last_used_at = now()
144
+ WHERE id = $1`, [existing.rows[0].id, hash, prefix]);
145
+ cacheSet(userId, plaintext);
146
+ return { apiKey: plaintext, created: false, rotated: true };
147
+ }
148
+ finally {
149
+ client.release();
150
+ }
151
+ }
152
+ //# sourceMappingURL=token-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-bridge.js","sourceRoot":"","sources":["../../src/auth/token-bridge.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export interface VerifiedJwt {
2
+ sub: string;
3
+ email?: string;
4
+ iss: string;
5
+ exp: number;
6
+ raw: string;
7
+ }
8
+ export declare class JwtVerificationError extends Error {
9
+ readonly code: string;
10
+ constructor(message: string, code: string);
11
+ }
12
+ export declare function verifyJwt(rawToken: string): Promise<VerifiedJwt>;
13
+ //# sourceMappingURL=verify-jwt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-jwt.d.ts","sourceRoot":"","sources":["../../src/auth/verify-jwt.ts"],"names":[],"mappings":"AA2CA,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,qBAAa,oBAAqB,SAAQ,KAAK;aACA,IAAI,EAAE,MAAM;gBAA7C,OAAO,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAG1D;AAED,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAoDtE"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * JWT validation for incoming Claude bearer tokens.
3
+ *
4
+ * Supabase OAuth Server (Phase 56) issues access tokens via HS256 — its JWKS
5
+ * endpoint is intentionally empty in May 2026 because tokens are signed with
6
+ * the project's shared JWT secret. We validate with that same secret.
7
+ *
8
+ * Acceptance criteria for a valid token (RFC 8707 audience binding partially):
9
+ * - signature verifies with the configured HS256 secret
10
+ * - exp not in the past, nbf not in the future
11
+ * - iss starts with the expected Supabase project issuer URL
12
+ * (we accept BOTH the project-ref URL and the custom-domain URL — Supabase
13
+ * Custom Domain doesn't rewrite the iss claim in May 2026, see Phase 56
14
+ * SUMMARY.md "Caveats" section)
15
+ * - sub is a non-empty UUID-shaped string
16
+ *
17
+ * NOT enforced in 57-02 (deferred to Phase 61 Pattern A migration where
18
+ * Anthropic-Held Credentials let us mint mcp.sota.io-audience tokens):
19
+ * - strict aud == "https://mcp.sota.io" check (Supabase tokens carry
20
+ * aud="authenticated", not our resource URL; rejecting on that would
21
+ * block every incoming token)
22
+ */
23
+ import { jwtVerify, errors as joseErrors } from "jose";
24
+ const SUPABASE_JWT_SECRET = process.env.SUPABASE_JWT_SECRET ?? "";
25
+ const ACCEPTED_ISSUERS = [
26
+ "https://wnwsdtqhywfxxlnyqafk.supabase.co/auth/v1",
27
+ "https://auth.sota.io/auth/v1",
28
+ ];
29
+ if (!SUPABASE_JWT_SECRET) {
30
+ // Server still boots, but every incoming token will fail validation.
31
+ // This keeps health checks alive while signalling a missing config.
32
+ // eslint-disable-next-line no-console
33
+ console.warn("WARN: SUPABASE_JWT_SECRET is not set — every Bearer token will be rejected.");
34
+ }
35
+ const secretKey = SUPABASE_JWT_SECRET
36
+ ? new TextEncoder().encode(SUPABASE_JWT_SECRET)
37
+ : null;
38
+ export class JwtVerificationError extends Error {
39
+ code;
40
+ constructor(message, code) {
41
+ super(message);
42
+ this.code = code;
43
+ }
44
+ }
45
+ export async function verifyJwt(rawToken) {
46
+ if (!secretKey) {
47
+ throw new JwtVerificationError("Server JWT secret not configured", "server_misconfigured");
48
+ }
49
+ try {
50
+ const { payload } = await jwtVerify(rawToken, secretKey, {
51
+ algorithms: ["HS256"],
52
+ });
53
+ const iss = typeof payload.iss === "string" ? payload.iss : "";
54
+ if (!ACCEPTED_ISSUERS.includes(iss)) {
55
+ throw new JwtVerificationError(`Unexpected issuer: ${iss}`, "invalid_issuer");
56
+ }
57
+ const sub = typeof payload.sub === "string" ? payload.sub : "";
58
+ if (!sub) {
59
+ throw new JwtVerificationError("Token has no sub claim", "no_subject");
60
+ }
61
+ return {
62
+ sub,
63
+ email: typeof payload.email === "string" ? payload.email : undefined,
64
+ iss,
65
+ exp: typeof payload.exp === "number" ? payload.exp : 0,
66
+ raw: rawToken,
67
+ };
68
+ }
69
+ catch (err) {
70
+ if (err instanceof JwtVerificationError)
71
+ throw err;
72
+ if (err instanceof joseErrors.JWTExpired) {
73
+ throw new JwtVerificationError("Token expired", "token_expired");
74
+ }
75
+ if (err instanceof joseErrors.JWTInvalid) {
76
+ throw new JwtVerificationError("Malformed JWT", "malformed_jwt");
77
+ }
78
+ if (err instanceof joseErrors.JWSSignatureVerificationFailed) {
79
+ throw new JwtVerificationError("JWT signature invalid", "invalid_signature");
80
+ }
81
+ throw new JwtVerificationError(err instanceof Error ? err.message : String(err), "unknown");
82
+ }
83
+ }
84
+ //# sourceMappingURL=verify-jwt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-jwt.js","sourceRoot":"","sources":["../../src/auth/verify-jwt.ts"],"names":[],"mappings":"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"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * sota.io MCP Server — HTTP Transport (remote)
4
+ *
5
+ * Streamable HTTP variant of the existing stdio MCP server. Same 13 tools,
6
+ * exposed via mcp.sota.io/mcp for one-click installation from Claude Desktop
7
+ * and Claude.ai.
8
+ *
9
+ * Phase 57-01 (this file): scaffold + transport + RFC 9728 metadata.
10
+ * Auth is currently a pass-through Bearer API key — clients send their
11
+ * existing sota_xxx key as `Authorization: Bearer <key>` and it gets
12
+ * forwarded to sota-api. No OAuth validation yet.
13
+ *
14
+ * Phase 57-02 (next): OAuth 2.1 + JWT validation against Supabase JWKS,
15
+ * token-to-API-key bridge with `source=claude` audit tagging.
16
+ *
17
+ * Phase 57-03 (next): `create_account` tool for net-new user signup from
18
+ * inside Claude + plan-based capability gating.
19
+ */
20
+ export {};
21
+ //# sourceMappingURL=http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;GAiBG"}