@muxoai/platform 1.3.0 → 1.3.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/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +19 -4
- package/.codex/config.toml +9 -2
- package/README.md +63 -20
- package/dist/clerk.d.ts +33 -0
- package/dist/clerk.js +198 -0
- package/dist/cli.js +1181 -99
- package/dist/engine.d.ts +22 -1
- package/dist/engine.js +284 -14
- package/dist/mcp.d.ts +6 -1
- package/dist/mcp.js +109 -22
- package/dist/nl.js +60 -36
- package/dist/policy.d.ts +9 -0
- package/dist/policy.js +73 -0
- package/dist/providers/composio-instant.d.ts +4 -0
- package/dist/providers/composio-instant.js +634 -534
- package/dist/providers/composio-sdk.d.ts +1 -0
- package/dist/providers/composio-sdk.js +6 -2
- package/dist/providers/microsoft-governance.d.ts +61 -2
- package/dist/providers/microsoft-governance.js +458 -24
- package/dist/providers/stripe-billing.js +14 -4
- package/dist/providers/stripe-projects.d.ts +4 -1
- package/dist/providers/stripe-projects.js +123 -12
- package/dist/providers/supermemory.d.ts +28 -5
- package/dist/providers/supermemory.js +183 -55
- package/dist/sdk.d.ts +167 -1
- package/dist/sdk.js +113 -2
- package/dist/server.js +258 -40
- package/dist/store.d.ts +26 -1
- package/dist/store.js +255 -6
- package/dist/types.d.ts +86 -0
- package/dist/ui/console-html.d.ts +16 -0
- package/dist/ui/console-html.js +2427 -2206
- package/dist/ui/design-tokens.d.ts +177 -0
- package/dist/ui/design-tokens.js +210 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +2 -2
- package/plugins/muxo/.app.json +1 -1
- package/plugins/muxo/.claude-plugin/plugin.json +2 -2
- package/plugins/muxo/.codex-plugin/plugin.json +7 -5
- package/plugins/muxo/.mcp.json +1 -33
- package/plugins/muxo/assets/logo.png +0 -0
- package/plugins/muxo/hooks/session-start.sh +1 -1
- package/plugins/muxo/plugin.json +63 -6
- package/plugins/muxo/skills/muxo/SKILL.md +92 -14
- package/plugins/muxo/skills/muxo/agents/openai.yaml +12 -0
- package/plugins/muxo/skills/muxo/references/cli.md +4 -2
- package/plugins/muxo/skills/muxo/references/mcp.md +15 -9
- package/plugins/muxo/skills/setup/SKILL.md +8 -2
- package/plugins/muxo/skills/setup/agents/openai.yaml +12 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"plugins": [
|
|
7
7
|
{
|
|
8
8
|
"name": "muxo",
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.3.1",
|
|
10
10
|
"description": "Governed execution across 1,000+ Connected Apps, Instant Tools (search, scrape, crawl, SEO audit), Cloud Infrastructure provisioning, Supermemory v5 recall, and Microsoft Agent Governance Ed25519 receipts.",
|
|
11
11
|
"source": {
|
|
12
12
|
"source": "local",
|
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muxo",
|
|
3
|
+
"description": "Official Muxo plugin marketplace for governed execution across Connected Apps, Instant Tools, Cloud Infrastructure, and Workspace Memory.",
|
|
3
4
|
"owner": {
|
|
4
5
|
"name": "Muxo",
|
|
5
6
|
"email": "founders@muxo.ai",
|
|
6
7
|
"url": "https://muxo.ai"
|
|
7
8
|
},
|
|
9
|
+
"metadata": {
|
|
10
|
+
"description": "Official Muxo plugin marketplace for governed execution across Connected Apps, Instant Tools, Cloud Infrastructure, and Workspace Memory.",
|
|
11
|
+
"version": "1.3.1"
|
|
12
|
+
},
|
|
8
13
|
"plugins": [
|
|
9
14
|
{
|
|
10
15
|
"name": "muxo",
|
|
11
|
-
"source":
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
"source": {
|
|
17
|
+
"source": "npm",
|
|
18
|
+
"package": "@muxoai/cli"
|
|
19
|
+
},
|
|
20
|
+
"strict": false,
|
|
21
|
+
"description": "Governed execution across 1,000+ Connected Apps, Instant Tools (search, scrape, crawl, SEO audit), Cloud Infrastructure provisioning, Workspace Memory recall, and verifiable audit records.",
|
|
22
|
+
"version": "1.3.1",
|
|
23
|
+
"category": "Productivity",
|
|
24
|
+
"skills": [
|
|
25
|
+
"./plugins/muxo/skills/muxo",
|
|
26
|
+
"./plugins/muxo/skills/setup"
|
|
27
|
+
],
|
|
28
|
+
"hooks": "./plugins/muxo/hooks/hooks.json",
|
|
29
|
+
"mcpServers": "./plugins/muxo/.mcp.json"
|
|
15
30
|
}
|
|
16
31
|
]
|
|
17
32
|
}
|
package/.codex/config.toml
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
# OpenAI Codex
|
|
2
|
-
# Reference: https://developers.openai.com/
|
|
1
|
+
# OpenAI Codex Configuration for Muxo
|
|
2
|
+
# Reference: https://developers.openai.com/codex/plugins
|
|
3
3
|
|
|
4
4
|
[plugins."muxo@muxo"]
|
|
5
5
|
enabled = true
|
|
6
|
+
|
|
7
|
+
# TOML MCP Alternative (~/.codex/config.toml):
|
|
8
|
+
[mcp_servers.muxo]
|
|
9
|
+
url = "https://api.muxo.ai/mcp"
|
|
10
|
+
# Or for local stdio proxy (requires Node.js 22+):
|
|
11
|
+
# command = "npx"
|
|
12
|
+
# args = ["-y", "@muxoai/cli@latest", "mcp"]
|
package/README.md
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
# Muxo (`@muxoai/platform` / `@muxoai/cli` / `@muxoai/sdk`)
|
|
2
2
|
|
|
3
|
-
Muxo (`v1.3.
|
|
3
|
+
Muxo (`v1.3.2`) is the unified execution, cloud infrastructure provisioning, contextual memory, and cryptographic governance platform for AI agents and applications. Customers and their agents interact exclusively with **Muxo** — either via structured tool slugs or free-form **Natural Language** (`muxo do`, `POST /api/v1/do`, `client.do()`, and MCP `plan_action`).
|
|
4
4
|
|
|
5
5
|
See [`ARCHITECTURE.md`](./ARCHITECTURE.md) for the complete internal engineering blueprint, open-source ecosystem Keep/Adopt/Drop matrix, and 4-pillar architecture.
|
|
6
6
|
|
|
7
7
|
Under the hood, Muxo unifies **four core pillars** plus **Natural Language Routing** and **BYOK (Bring Your Own Key) 0-Credit Execution** behind one **19-operation MCP contract**, OpenAI Plugin (`agent-plugins.org/1.0.0`), REST API, Interactive CLI/TUI (`@muxoai/cli`), TypeScript SDK (`@muxoai/sdk`), and Web Console (`https://app.muxo.ai`):
|
|
8
8
|
|
|
9
|
-
1. **
|
|
10
|
-
2. **
|
|
11
|
-
3. **
|
|
12
|
-
4. **
|
|
9
|
+
1. **Connected Apps (`app.*`) & Instant Tools (`instant.*`)** — Managed OAuth SaaS integrations (`app.github.*`, `app.gmail.*`, `app.slack.*`, `app.notion.*`, `app.linear.*`) with live browser OAuth handoff (`wait_for_connection`), zero-OAuth instant capabilities (`instant.web.search`, `instant.web.scrape`, `instant.web.crawl`, `instant.seo.audit`, `instant.hackernews.top_stories`, `instant.github.repo_intel`, `instant.dns.lookup`, `instant.composio.workbench`), plus **CLI-first Autonomous 2-Call Agent Signup & Claim** (`POST /api/v1/agents/signup` + `POST /api/v1/agents/claim`).
|
|
10
|
+
2. **Cloud Infrastructure (`resource.*`) + Unified Credit Billing (`muxo_funded`) & BYOK (`byok`)** — Governed cloud resource provisioning across 60+ providers (`Turso`, `Neon`, `Supabase`, `Cloudflare`, `Vercel`, `Upstash`, `Resend`, `Railway`, `Render`, etc.) with automatic companion plan resolution, AES-256-GCM encrypted secret custody (`muxo env pull`), unified credit settlement (`muxo_funded`), and **0-credit BYOK mode** (`muxo byok set <provider>`) when you bring your own supplier or LLM API keys.
|
|
11
|
+
3. **Workspace Memory (`/ns/muxo-v5-*`)** — Isolated per-workspace long-term static facts, dynamic session context, and hybrid semantic search recalled before every tool plan (`plan_action` & `get_run_context`), with strict secret redaction before memory ingestion.
|
|
12
|
+
4. **Safety & Approvals (Muxo Guardrails)** — Every planned and executed operation passes through prompt-injection security scanning, workspace policy evaluation, cryptographic agent identity (`did:agent:muxo:*`), and produces a verifiable execution audit record.
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
## Install & Quickstart (Humans, Harnesses & Agents)
|
|
17
17
|
|
|
18
|
+
> **Requirement:** `@muxoai/cli` requires **Node.js 22+** (`node >= 22.0.0`).
|
|
19
|
+
|
|
18
20
|
```bash
|
|
19
|
-
# 1. Install the Muxo CLI or run directly via npx
|
|
21
|
+
# 1. Install the Muxo CLI or run directly via npx (requires Node.js 22+)
|
|
20
22
|
npm install -g @muxoai/cli@latest
|
|
21
23
|
|
|
22
|
-
# 2.
|
|
24
|
+
# 2. CLI-first autonomous signup against the hosted workspace (creates ~/.muxo/config.json with mk_live_* key + 250 starter credits)
|
|
23
25
|
muxo login --signup --name "my-agent"
|
|
24
26
|
|
|
25
27
|
# 3. Launch the Interactive Terminal TUI (Natural Language bar, BYOK Vault, Billing & Status)
|
|
@@ -40,23 +42,64 @@ muxo billing status
|
|
|
40
42
|
muxo billing topup 1000
|
|
41
43
|
muxo billing upgrade
|
|
42
44
|
|
|
43
|
-
# 7. Connect an OAuth SaaS App & Provision Live Cloud Infrastructure
|
|
45
|
+
# 7. Connect an OAuth SaaS App (browser consent handoff) & Provision Live Cloud Infrastructure
|
|
44
46
|
muxo link gmail --wait
|
|
45
47
|
muxo provision turso --input '{"database_name":"my-agent-db"}' --approve
|
|
46
48
|
muxo env pull --out .env.muxo
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Official OpenAI Plugin & MCP Server (`https://api.muxo.ai/mcp`)
|
|
50
49
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
# 8. Check your setup, memory, and safety controls
|
|
51
|
+
muxo harness # API, MCP, auth, kill switch, audit chain, catalog (exit 1 on failure)
|
|
52
|
+
muxo memory facts # Facts with history; add --current for current facts only
|
|
53
|
+
muxo memory forget "old staging database" # Delete matching memories and retire their facts
|
|
54
|
+
muxo governance scan "text to check" # Prompt-injection check before storing or forwarding text
|
|
55
|
+
muxo governance pause "incident review" # Kill switch: stop all agent actions in the workspace
|
|
56
|
+
muxo governance resume
|
|
57
|
+
muxo governance policy --ring 2 --block-destructive-sql true --require-approval financial,destructive
|
|
58
|
+
```
|
|
59
|
+
## Official Plugins, Marketplaces & MCP Setup (`https://api.muxo.ai/mcp`)
|
|
60
|
+
|
|
61
|
+
Muxo ships a portable OpenAI Plugin (`plugins/muxo/plugin.json` & `plugins/muxo/mcp.json`), Claude Code Plugin (`plugins/muxo/.claude-plugin/plugin.json`), and Streamable HTTP MCP Server:
|
|
62
|
+
|
|
63
|
+
- **Streamable HTTP MCP Endpoints**: `https://api.muxo.ai/mcp` and `https://app.muxo.ai/mcp`
|
|
64
|
+
- **Local Stdio MCP Command** (requires Node.js 22+, proxies to hosted workspace): `npx -y @muxoai/cli@latest mcp`
|
|
65
|
+
- **Claude Code Marketplace (`muxo@muxo`)**:
|
|
66
|
+
```bash
|
|
67
|
+
claude plugin marketplace add https://api.muxo.ai/.claude-plugin/marketplace.json
|
|
68
|
+
claude plugin install muxo@muxo
|
|
69
|
+
```
|
|
70
|
+
- **OpenAI Codex Marketplace & TOML MCP Alternative**:
|
|
71
|
+
- **Marketplace (Git Repository or Local Directory)**: Add a Git repository or local directory containing `.agents/plugins/marketplace.json`, then enable `[plugins."muxo@muxo"]` in `.codex/config.toml`.
|
|
72
|
+
- **TOML MCP Alternative (`~/.codex/config.toml`)**:
|
|
73
|
+
```toml
|
|
74
|
+
[mcp_servers.muxo]
|
|
75
|
+
url = "https://api.muxo.ai/mcp"
|
|
76
|
+
```
|
|
77
|
+
Or for local stdio proxy (requires Node.js 22+):
|
|
78
|
+
```toml
|
|
79
|
+
[mcp_servers.muxo]
|
|
80
|
+
command = "npx"
|
|
81
|
+
args = ["-y", "@muxoai/cli@latest", "mcp"]
|
|
82
|
+
```
|
|
83
|
+
- **Claude Desktop**:
|
|
84
|
+
- **Hosted MCP (Custom Connectors)**: Open **Settings → Connectors → Add custom connector** and enter `https://api.muxo.ai/mcp`.
|
|
85
|
+
- **Local Stdio Server (`claude_desktop_config.json`, requires Node.js 22+)**:
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"muxo": {
|
|
90
|
+
"command": "npx",
|
|
91
|
+
"args": ["-y", "@muxoai/cli@latest", "mcp"]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
- **ChatGPT**:
|
|
97
|
+
- Open **Plugins → + → Add custom MCP server** (at `https://chatgpt.com/plugins`) and enter `https://api.muxo.ai/mcp`.
|
|
98
|
+
- **OAuth 2.1 Discovery (CIMD or DCR + PKCE)**:
|
|
99
|
+
- `https://api.muxo.ai/.well-known/oauth-protected-resource/mcp` (RFC 9728)
|
|
100
|
+
- `https://api.muxo.ai/.well-known/oauth-authorization-server` (RFC 8414)
|
|
101
|
+
- Client registration can use **Client ID Metadata Documents (CIMD)** or **Dynamic Client Registration (DCR)** (`POST https://api.muxo.ai/oauth/register`); `/oauth/register` is supported when a client uses DCR rather than running on every flow.
|
|
102
|
+
- `GET https://api.muxo.ai/oauth/authorize` & `POST https://api.muxo.ai/oauth/token` (PKCE `S256`)
|
|
60
103
|
- **SEP-2640 Skills Import Extension (`io.modelcontextprotocol/skills`)**: `skills/list`, `skills/get`, and `resources/read` serving `skill://muxo/muxo/SKILL.md` and `skill://muxo/setup/SKILL.md` with `sha256` integrity digests.
|
|
61
104
|
- **Tool Annotations & Multi-Account Profile**: All 19 tools include `title` and `annotations` (`readOnlyHint`, `destructiveHint`, `openWorldHint`), plus `_meta["openai/profile"]: true` on `get_profile` and Company Knowledge `search`/`fetch` compatibility.
|
|
62
105
|
|
package/dist/clerk.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface ClerkConfig {
|
|
2
|
+
publishableKey: string;
|
|
3
|
+
frontendApi: string;
|
|
4
|
+
authorizedParties: string[];
|
|
5
|
+
jwtKey?: string;
|
|
6
|
+
secretKey?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ClerkSession {
|
|
9
|
+
userId: string;
|
|
10
|
+
sessionId: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
emailVerified?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ClerkProfile {
|
|
15
|
+
email: string | null;
|
|
16
|
+
emailVerified: boolean;
|
|
17
|
+
name: string | null;
|
|
18
|
+
}
|
|
19
|
+
export interface ClerkEnv {
|
|
20
|
+
CLERK_PUBLISHABLE_KEY?: string;
|
|
21
|
+
CLERK_SECRET_KEY?: string;
|
|
22
|
+
CLERK_JWT_KEY?: string;
|
|
23
|
+
CLERK_AUTHORIZED_PARTIES?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Derives the Clerk Frontend API origin encoded in a publishable key (pk_live_/pk_test_). */
|
|
26
|
+
export declare function clerkFrontendApi(publishableKey: string): string | null;
|
|
27
|
+
export declare function readClerkConfig(env: ClerkEnv, fallbackAuthorizedParty?: string): ClerkConfig | null;
|
|
28
|
+
export declare function looksLikeJwt(token: string): boolean;
|
|
29
|
+
/** Verifies a Clerk session JWT (RS256) and returns the session identity, or null if invalid. */
|
|
30
|
+
export declare function verifyClerkSessionToken(token: string, config: ClerkConfig, nowSeconds?: number): Promise<ClerkSession | null>;
|
|
31
|
+
/** Reads the primary email and name for a Clerk user through the Backend API. */
|
|
32
|
+
export declare function fetchClerkProfile(userId: string, secretKey: string): Promise<ClerkProfile | null>;
|
|
33
|
+
export declare function clerkBrowserScriptUrl(frontendApi: string): string;
|
package/dist/clerk.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
const RS256 = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
|
2
|
+
const JWKS_TTL_MS = 10 * 60 * 1000;
|
|
3
|
+
const JWKS_RETRY_MS = 30 * 1000;
|
|
4
|
+
const CLOCK_SKEW_SECONDS = 5;
|
|
5
|
+
function base64UrlToBytes(input) {
|
|
6
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
7
|
+
const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
|
|
8
|
+
const binary = atob(padded);
|
|
9
|
+
const bytes = new Uint8Array(new ArrayBuffer(binary.length));
|
|
10
|
+
for (let i = 0; i < binary.length; i++)
|
|
11
|
+
bytes[i] = binary.charCodeAt(i);
|
|
12
|
+
return bytes;
|
|
13
|
+
}
|
|
14
|
+
function decodeJsonSegment(segment) {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(new TextDecoder().decode(base64UrlToBytes(segment)));
|
|
17
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Derives the Clerk Frontend API origin encoded in a publishable key (pk_live_/pk_test_). */
|
|
24
|
+
export function clerkFrontendApi(publishableKey) {
|
|
25
|
+
const match = /^pk_(?:live|test)_([A-Za-z0-9+/=_-]{4,512})$/.exec(publishableKey.trim());
|
|
26
|
+
if (!match)
|
|
27
|
+
return null;
|
|
28
|
+
let decoded;
|
|
29
|
+
try {
|
|
30
|
+
decoded = new TextDecoder().decode(base64UrlToBytes(match[1]));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (!decoded.endsWith("$"))
|
|
36
|
+
return null;
|
|
37
|
+
const host = decoded.slice(0, -1);
|
|
38
|
+
if (!/^(?=.{4,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i.test(host))
|
|
39
|
+
return null;
|
|
40
|
+
return `https://${host.toLowerCase()}`;
|
|
41
|
+
}
|
|
42
|
+
export function readClerkConfig(env, fallbackAuthorizedParty) {
|
|
43
|
+
const publishableKey = (env.CLERK_PUBLISHABLE_KEY || "").trim();
|
|
44
|
+
if (!publishableKey)
|
|
45
|
+
return null;
|
|
46
|
+
const frontendApi = clerkFrontendApi(publishableKey);
|
|
47
|
+
if (!frontendApi)
|
|
48
|
+
return null;
|
|
49
|
+
const configured = (env.CLERK_AUTHORIZED_PARTIES || "")
|
|
50
|
+
.split(",")
|
|
51
|
+
.map((s) => s.trim().replace(/\/+$/, ""))
|
|
52
|
+
.filter(Boolean);
|
|
53
|
+
const authorizedParties = configured.length
|
|
54
|
+
? configured
|
|
55
|
+
: fallbackAuthorizedParty
|
|
56
|
+
? [fallbackAuthorizedParty.replace(/\/+$/, "")]
|
|
57
|
+
: [];
|
|
58
|
+
return {
|
|
59
|
+
publishableKey,
|
|
60
|
+
frontendApi,
|
|
61
|
+
authorizedParties,
|
|
62
|
+
jwtKey: (env.CLERK_JWT_KEY || "").trim() || undefined,
|
|
63
|
+
secretKey: (env.CLERK_SECRET_KEY || "").trim() || undefined,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export function looksLikeJwt(token) {
|
|
67
|
+
return /^eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token);
|
|
68
|
+
}
|
|
69
|
+
async function importPemKey(pem) {
|
|
70
|
+
const body = pem
|
|
71
|
+
.replace(/\\n/g, "\n")
|
|
72
|
+
.replace(/-----BEGIN PUBLIC KEY-----/, "")
|
|
73
|
+
.replace(/-----END PUBLIC KEY-----/, "")
|
|
74
|
+
.replace(/\s+/g, "");
|
|
75
|
+
return crypto.subtle.importKey("spki", base64UrlToBytes(body), RS256, false, ["verify"]);
|
|
76
|
+
}
|
|
77
|
+
// Only public verification keys are cached; never tokens or user data.
|
|
78
|
+
const jwksCache = new Map();
|
|
79
|
+
const pemCache = new Map();
|
|
80
|
+
async function loadJwks(frontendApi) {
|
|
81
|
+
const res = await fetch(`${frontendApi}/.well-known/jwks.json`, {
|
|
82
|
+
headers: { Accept: "application/json" },
|
|
83
|
+
redirect: "manual",
|
|
84
|
+
});
|
|
85
|
+
if (res.status !== 200)
|
|
86
|
+
throw new Error(`Clerk JWKS unavailable (HTTP ${res.status})`);
|
|
87
|
+
const doc = (await res.json());
|
|
88
|
+
const keys = new Map();
|
|
89
|
+
for (const jwk of doc.keys || []) {
|
|
90
|
+
if (jwk.kty !== "RSA" || typeof jwk.kid !== "string")
|
|
91
|
+
continue;
|
|
92
|
+
if (jwk.use && jwk.use !== "sig")
|
|
93
|
+
continue;
|
|
94
|
+
const key = await crypto.subtle.importKey("jwk", { kty: "RSA", n: String(jwk.n), e: String(jwk.e), alg: "RS256", ext: true }, RS256, false, ["verify"]);
|
|
95
|
+
keys.set(jwk.kid, key);
|
|
96
|
+
}
|
|
97
|
+
return keys;
|
|
98
|
+
}
|
|
99
|
+
async function verificationKey(config, kid) {
|
|
100
|
+
if (config.jwtKey) {
|
|
101
|
+
let key = pemCache.get(config.jwtKey);
|
|
102
|
+
if (!key) {
|
|
103
|
+
key = await importPemKey(config.jwtKey);
|
|
104
|
+
pemCache.set(config.jwtKey, key);
|
|
105
|
+
}
|
|
106
|
+
return key;
|
|
107
|
+
}
|
|
108
|
+
if (!kid)
|
|
109
|
+
return null;
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
const cached = jwksCache.get(config.frontendApi);
|
|
112
|
+
const fresh = cached && now - cached.fetchedAt < JWKS_TTL_MS;
|
|
113
|
+
if (cached?.keys.has(kid) && fresh)
|
|
114
|
+
return cached.keys.get(kid);
|
|
115
|
+
// Refetch for unknown kids (key rotation), but not more than once per retry window.
|
|
116
|
+
if (!cached || !fresh || now - cached.fetchedAt > JWKS_RETRY_MS) {
|
|
117
|
+
const keys = await loadJwks(config.frontendApi);
|
|
118
|
+
jwksCache.set(config.frontendApi, { keys, fetchedAt: now });
|
|
119
|
+
return keys.get(kid) || null;
|
|
120
|
+
}
|
|
121
|
+
return cached.keys.get(kid) || null;
|
|
122
|
+
}
|
|
123
|
+
/** Verifies a Clerk session JWT (RS256) and returns the session identity, or null if invalid. */
|
|
124
|
+
export async function verifyClerkSessionToken(token, config, nowSeconds = Math.floor(Date.now() / 1000)) {
|
|
125
|
+
if (!looksLikeJwt(token) || token.length > 16_384)
|
|
126
|
+
return null;
|
|
127
|
+
const [headerPart, payloadPart, signaturePart] = token.split(".");
|
|
128
|
+
const header = decodeJsonSegment(headerPart);
|
|
129
|
+
const payload = decodeJsonSegment(payloadPart);
|
|
130
|
+
if (!header || !payload || header.alg !== "RS256")
|
|
131
|
+
return null;
|
|
132
|
+
let key;
|
|
133
|
+
try {
|
|
134
|
+
key = await verificationKey(config, typeof header.kid === "string" ? header.kid : undefined);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
if (!key)
|
|
140
|
+
return null;
|
|
141
|
+
let valid = false;
|
|
142
|
+
try {
|
|
143
|
+
valid = await crypto.subtle.verify(RS256, key, base64UrlToBytes(signaturePart), new TextEncoder().encode(`${headerPart}.${payloadPart}`));
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if (!valid)
|
|
149
|
+
return null;
|
|
150
|
+
if (payload.iss !== config.frontendApi)
|
|
151
|
+
return null;
|
|
152
|
+
if (typeof payload.exp !== "number" || payload.exp + CLOCK_SKEW_SECONDS < nowSeconds)
|
|
153
|
+
return null;
|
|
154
|
+
if (typeof payload.nbf === "number" && payload.nbf - CLOCK_SKEW_SECONDS > nowSeconds)
|
|
155
|
+
return null;
|
|
156
|
+
if (typeof payload.iat === "number" && payload.iat - CLOCK_SKEW_SECONDS > nowSeconds)
|
|
157
|
+
return null;
|
|
158
|
+
if (typeof payload.azp === "string" &&
|
|
159
|
+
config.authorizedParties.length > 0 &&
|
|
160
|
+
!config.authorizedParties.includes(payload.azp.replace(/\/+$/, ""))) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (payload.sts !== undefined && payload.sts !== "active")
|
|
164
|
+
return null;
|
|
165
|
+
if (typeof payload.sub !== "string" || !/^user_[A-Za-z0-9]{1,128}$/.test(payload.sub))
|
|
166
|
+
return null;
|
|
167
|
+
return {
|
|
168
|
+
userId: payload.sub,
|
|
169
|
+
sessionId: typeof payload.sid === "string" ? payload.sid : "",
|
|
170
|
+
email: typeof payload.email === "string" ? payload.email.toLowerCase() : undefined,
|
|
171
|
+
emailVerified: payload.email_verified === true,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/** Reads the primary email and name for a Clerk user through the Backend API. */
|
|
175
|
+
export async function fetchClerkProfile(userId, secretKey) {
|
|
176
|
+
try {
|
|
177
|
+
const res = await fetch(`https://api.clerk.com/v1/users/${encodeURIComponent(userId)}`, {
|
|
178
|
+
headers: { Authorization: `Bearer ${secretKey}`, Accept: "application/json" },
|
|
179
|
+
});
|
|
180
|
+
if (!res.ok)
|
|
181
|
+
return null;
|
|
182
|
+
const user = (await res.json());
|
|
183
|
+
const addresses = user.email_addresses || [];
|
|
184
|
+
const primary = addresses.find((e) => e.id === user.primary_email_address_id) || addresses[0];
|
|
185
|
+
const name = [user.first_name, user.last_name].filter(Boolean).join(" ").trim() || user.username || null;
|
|
186
|
+
return {
|
|
187
|
+
email: primary?.email_address ? primary.email_address.toLowerCase() : null,
|
|
188
|
+
emailVerified: primary?.verification?.status === "verified",
|
|
189
|
+
name,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
export function clerkBrowserScriptUrl(frontendApi) {
|
|
197
|
+
return `${frontendApi}/npm/@clerk/clerk-js@5/dist/clerk.browser.js`;
|
|
198
|
+
}
|