@genlobe/mcp-server 2.2.0 → 3.1.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.
- package/README.md +86 -116
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2203 -1474
- package/package.json +41 -41
package/dist/index.js
CHANGED
|
@@ -13,17 +13,23 @@
|
|
|
13
13
|
* the End-user endpoints, NOT the Tenant/Dashboard endpoints.
|
|
14
14
|
*
|
|
15
15
|
* Usage:
|
|
16
|
-
* npx @
|
|
16
|
+
* npx @genlobe/mcp-server
|
|
17
17
|
*
|
|
18
18
|
* Or configure in VS Code MCP settings with SAAS_API_URL environment variable.
|
|
19
19
|
*/
|
|
20
20
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
21
21
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
22
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
23
|
+
import { createRequire } from "module";
|
|
23
24
|
// =============================================================================
|
|
24
25
|
// Configuration
|
|
25
26
|
// =============================================================================
|
|
26
|
-
|
|
27
|
+
// Read version from package.json so every surface (banner, API_OVERVIEW,
|
|
28
|
+
// MCP Server metadata) stays in sync with whatever was last published.
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const pkg = require("../package.json");
|
|
31
|
+
const SERVER_VERSION = pkg.version;
|
|
32
|
+
const API_URL = process.env.SAAS_API_URL || process.env.API_URL || "http://localhost:8001";
|
|
27
33
|
const API_KEY = process.env.SAAS_API_KEY || process.env.API_KEY || "";
|
|
28
34
|
// Cache for API responses
|
|
29
35
|
const cache = new Map();
|
|
@@ -37,50 +43,65 @@ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
|
37
43
|
*/
|
|
38
44
|
const API_OVERVIEW = {
|
|
39
45
|
name: "Multi-tenant SaaS API",
|
|
40
|
-
version:
|
|
41
|
-
description: `
|
|
42
|
-
This is a Backend-as-a-Service API for building SaaS applications.
|
|
43
|
-
|
|
44
|
-
## Architecture Overview
|
|
45
|
-
|
|
46
|
-
There are TWO types of authentication contexts:
|
|
47
|
-
|
|
48
|
-
### 1. TENANT CONTEXT (Dashboard)
|
|
49
|
-
- Used by: Tenant owners/admins managing their SaaS account
|
|
50
|
-
- Authentication: JWT from /v1/tenant-auth/login
|
|
51
|
-
- Purpose: Manage API keys, team members, view analytics, configure settings
|
|
52
|
-
- These endpoints are NOT for end-user frontends
|
|
53
|
-
|
|
54
|
-
### 2. END-USER CONTEXT (API)
|
|
55
|
-
- Used by: End-users of applications built on top of this API
|
|
56
|
-
- Authentication: API Key (X-API-Key header) + optional User JWT
|
|
57
|
-
- Purpose: User auth, organizations, subscriptions, billing
|
|
58
|
-
- These endpoints ARE for building end-user frontends
|
|
59
|
-
|
|
60
|
-
## API Key Types
|
|
61
|
-
- \`pk_live_*\`: Public key - safe to expose in frontend code (LIMITED to auth endpoints only)
|
|
62
|
-
- \`sk_live_*\`: Secret key - backend only, never expose (FULL API access)
|
|
63
|
-
|
|
64
|
-
##
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
46
|
+
version: SERVER_VERSION,
|
|
47
|
+
description: `
|
|
48
|
+
This is a Backend-as-a-Service API for building SaaS applications.
|
|
49
|
+
|
|
50
|
+
## Architecture Overview
|
|
51
|
+
|
|
52
|
+
There are TWO types of authentication contexts:
|
|
53
|
+
|
|
54
|
+
### 1. TENANT CONTEXT (Dashboard)
|
|
55
|
+
- Used by: Tenant owners/admins managing their SaaS account
|
|
56
|
+
- Authentication: JWT from /v1/tenant-auth/login
|
|
57
|
+
- Purpose: Manage API keys, team members, view analytics, configure settings
|
|
58
|
+
- These endpoints are NOT for end-user frontends
|
|
59
|
+
|
|
60
|
+
### 2. END-USER CONTEXT (API)
|
|
61
|
+
- Used by: End-users of applications built on top of this API
|
|
62
|
+
- Authentication: API Key (X-API-Key header) + optional User JWT
|
|
63
|
+
- Purpose: User auth, organizations, subscriptions, billing
|
|
64
|
+
- These endpoints ARE for building end-user frontends
|
|
65
|
+
|
|
66
|
+
## API Key Types
|
|
67
|
+
- \`pk_live_*\`: Public key - safe to expose in frontend code (LIMITED to auth endpoints only)
|
|
68
|
+
- \`sk_live_*\`: Secret key - backend only, never expose (FULL API access)
|
|
69
|
+
|
|
70
|
+
## API Key Organization Scope (B2B2C)
|
|
71
|
+
|
|
72
|
+
Every API key is associated with exactly one organization inside a tenant:
|
|
73
|
+
|
|
74
|
+
- **Root-org key** (tenant-wide): the key inherits the tenant's root organization. Callers MUST send \`X-Organization-Id\` to target a non-root customer organization.
|
|
75
|
+
- **Customer-org key** (scoped): the key is bound to a specific customer organization. The organization context is resolved automatically from the key — the \`X-Organization-Id\` header is ignored for these keys.
|
|
76
|
+
|
|
77
|
+
**Implication for end-user endpoints that list "X-Organization-Id required":**
|
|
78
|
+
- If the request uses a root-org key → the header IS required.
|
|
79
|
+
- If the request uses a customer-org-scoped key → the header is optional (the key's org wins). Sending a different org_id in the header has no effect; the scope of the key is authoritative.
|
|
80
|
+
|
|
81
|
+
**Implication for \`POST /v1/auth/register\`:** the new end-user joins the API key's organization automatically as role=\`member\`. The platform no longer creates a "<email>'s Organization" per signup — that auto-spawn behavior was removed. Do not try to pass \`organization_id\` in the request body; it isn't part of the schema. The X-API-Key alone determines where the user lands.
|
|
82
|
+
|
|
83
|
+
Tenants assign keys to customer organizations from their dashboard.
|
|
84
|
+
|
|
85
|
+
## Security Features (for public keys)
|
|
86
|
+
|
|
87
|
+
### Origin Validation
|
|
88
|
+
Public keys (pk_live_*) require Origin header validation:
|
|
89
|
+
- When creating a public key, the tenant specifies allowed origins
|
|
90
|
+
- Requests from other origins are rejected
|
|
91
|
+
- This prevents stolen API keys from being used on other domains
|
|
92
|
+
|
|
93
|
+
### Rate Limiting (per IP)
|
|
94
|
+
- Register: 3/hour (blocked 30 min after exceeding)
|
|
95
|
+
- Login: 10/15 min (blocked 15 min after exceeding)
|
|
96
|
+
- Forgot password: 3/hour (blocked 30 min after exceeding)
|
|
97
|
+
- Resend verification: 3/hour (blocked 30 min after exceeding)
|
|
98
|
+
|
|
99
|
+
## When Building a Frontend
|
|
100
|
+
If you're building a frontend application for end-users, you should:
|
|
101
|
+
1. Use ONLY the End-user endpoints (not Tenant/Dashboard endpoints)
|
|
102
|
+
2. Use the public API key (pk_live_*) for client-side requests
|
|
103
|
+
3. Include X-API-Key header in ALL requests
|
|
104
|
+
4. After login, also include Authorization: Bearer <jwt> for authenticated requests
|
|
84
105
|
`,
|
|
85
106
|
base_url: API_URL,
|
|
86
107
|
documentation_urls: {
|
|
@@ -128,12 +149,12 @@ const END_USER_ENDPOINTS = {
|
|
|
128
149
|
message: "Registration successful. Please check your email to verify your account.",
|
|
129
150
|
verification_required: "true"
|
|
130
151
|
},
|
|
131
|
-
note: "The response depends on whether the tenant has email verification enabled. When verification IS required, NO tokens are returned — the user must verify their email first, then login. Check for 'verification_required: true' in the response to show the appropriate UI.",
|
|
132
|
-
example_request: `{
|
|
133
|
-
"email": "user@example.com",
|
|
134
|
-
"password": "SecurePass123",
|
|
135
|
-
"display_name": "John Doe",
|
|
136
|
-
"return_to": "https://myapp.com/verify-email"
|
|
152
|
+
note: "Two important things:\n\n1) The response depends on whether the tenant has email verification enabled. When verification IS required, NO tokens are returned — the user must verify their email first, then login. Check for 'verification_required: true' in the response to show the appropriate UI.\n\n2) Organization assignment is automatic from the API key (B2B2C). The new end-user joins the organization the API key is scoped to (root-org or a customer-org) as role=member. NO per-user 'personal' organization is auto-spawned anymore. Do not pass any organization_id in the body — it isn't accepted, and the routing happens server-side from the X-API-Key.",
|
|
153
|
+
example_request: `{
|
|
154
|
+
"email": "user@example.com",
|
|
155
|
+
"password": "SecurePass123",
|
|
156
|
+
"display_name": "John Doe",
|
|
157
|
+
"return_to": "https://myapp.com/verify-email"
|
|
137
158
|
}`
|
|
138
159
|
},
|
|
139
160
|
{
|
|
@@ -147,9 +168,9 @@ const END_USER_ENDPOINTS = {
|
|
|
147
168
|
password: "string (required)"
|
|
148
169
|
},
|
|
149
170
|
response: "Same as register",
|
|
150
|
-
example_request: `{
|
|
151
|
-
"email": "user@example.com",
|
|
152
|
-
"password": "SecurePass123"
|
|
171
|
+
example_request: `{
|
|
172
|
+
"email": "user@example.com",
|
|
173
|
+
"password": "SecurePass123"
|
|
153
174
|
}`
|
|
154
175
|
},
|
|
155
176
|
{
|
|
@@ -317,10 +338,10 @@ const END_USER_ENDPOINTS = {
|
|
|
317
338
|
slug: "string (required) - URL-friendly identifier",
|
|
318
339
|
description: "string (optional)"
|
|
319
340
|
},
|
|
320
|
-
example_request: `{
|
|
321
|
-
"name": "My Company",
|
|
322
|
-
"slug": "my-company",
|
|
323
|
-
"description": "Our awesome company"
|
|
341
|
+
example_request: `{
|
|
342
|
+
"name": "My Company",
|
|
343
|
+
"slug": "my-company",
|
|
344
|
+
"description": "Our awesome company"
|
|
324
345
|
}`
|
|
325
346
|
},
|
|
326
347
|
{
|
|
@@ -554,26 +575,26 @@ const END_USER_ENDPOINTS = {
|
|
|
554
575
|
updated_at: "ISO datetime"
|
|
555
576
|
}
|
|
556
577
|
],
|
|
557
|
-
example_response: `[
|
|
558
|
-
{
|
|
559
|
-
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
560
|
-
"name": "Pro Plan",
|
|
561
|
-
"description": "For growing teams",
|
|
562
|
-
"price_monthly": 29.99,
|
|
563
|
-
"price_yearly": 299.99,
|
|
564
|
-
"currency": "usd",
|
|
565
|
-
"features": ["API Access", "Priority Support", "Advanced Analytics"],
|
|
566
|
-
"is_active": true,
|
|
567
|
-
"is_free": false,
|
|
568
|
-
"max_cost_usd_per_month": 100.00,
|
|
569
|
-
"max_messages_per_month": 1000,
|
|
570
|
-
"max_tokens_per_month": null,
|
|
571
|
-
"stripe_product_id": "prod_xxxxx",
|
|
572
|
-
"stripe_price_id_monthly": "price_xxxxx",
|
|
573
|
-
"stripe_price_id_yearly": "price_xxxxx",
|
|
574
|
-
"created_at": "2024-01-15T10:30:00Z",
|
|
575
|
-
"updated_at": "2024-01-15T10:30:00Z"
|
|
576
|
-
}
|
|
578
|
+
example_response: `[
|
|
579
|
+
{
|
|
580
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
581
|
+
"name": "Pro Plan",
|
|
582
|
+
"description": "For growing teams",
|
|
583
|
+
"price_monthly": 29.99,
|
|
584
|
+
"price_yearly": 299.99,
|
|
585
|
+
"currency": "usd",
|
|
586
|
+
"features": ["API Access", "Priority Support", "Advanced Analytics"],
|
|
587
|
+
"is_active": true,
|
|
588
|
+
"is_free": false,
|
|
589
|
+
"max_cost_usd_per_month": 100.00,
|
|
590
|
+
"max_messages_per_month": 1000,
|
|
591
|
+
"max_tokens_per_month": null,
|
|
592
|
+
"stripe_product_id": "prod_xxxxx",
|
|
593
|
+
"stripe_price_id_monthly": "price_xxxxx",
|
|
594
|
+
"stripe_price_id_yearly": "price_xxxxx",
|
|
595
|
+
"created_at": "2024-01-15T10:30:00Z",
|
|
596
|
+
"updated_at": "2024-01-15T10:30:00Z"
|
|
597
|
+
}
|
|
577
598
|
]`
|
|
578
599
|
},
|
|
579
600
|
{
|
|
@@ -600,24 +621,24 @@ const END_USER_ENDPOINTS = {
|
|
|
600
621
|
created_at: "ISO datetime",
|
|
601
622
|
updated_at: "ISO datetime"
|
|
602
623
|
},
|
|
603
|
-
example_response: `{
|
|
604
|
-
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
605
|
-
"plan_id": "660e8400-e29b-41d4-a716-446655440001",
|
|
606
|
-
"organization_id": "770e8400-e29b-41d4-a716-446655440002",
|
|
607
|
-
"tenant_id": "880e8400-e29b-41d4-a716-446655440003",
|
|
608
|
-
"stripe_customer_id": "cus_xxxxx",
|
|
609
|
-
"stripe_subscription_id": "sub_xxxxx",
|
|
610
|
-
"billing_cycle": "monthly",
|
|
611
|
-
"status": "active",
|
|
612
|
-
"current_period_start": "2024-01-01T00:00:00Z",
|
|
613
|
-
"current_period_end": "2024-02-01T00:00:00Z",
|
|
614
|
-
"cancel_at_period_end": false,
|
|
615
|
-
"canceled_at": null,
|
|
616
|
-
"trial_start": null,
|
|
617
|
-
"trial_end": null,
|
|
618
|
-
"subscription_metadata": {},
|
|
619
|
-
"created_at": "2024-01-01T00:00:00Z",
|
|
620
|
-
"updated_at": "2024-01-01T00:00:00Z"
|
|
624
|
+
example_response: `{
|
|
625
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
626
|
+
"plan_id": "660e8400-e29b-41d4-a716-446655440001",
|
|
627
|
+
"organization_id": "770e8400-e29b-41d4-a716-446655440002",
|
|
628
|
+
"tenant_id": "880e8400-e29b-41d4-a716-446655440003",
|
|
629
|
+
"stripe_customer_id": "cus_xxxxx",
|
|
630
|
+
"stripe_subscription_id": "sub_xxxxx",
|
|
631
|
+
"billing_cycle": "monthly",
|
|
632
|
+
"status": "active",
|
|
633
|
+
"current_period_start": "2024-01-01T00:00:00Z",
|
|
634
|
+
"current_period_end": "2024-02-01T00:00:00Z",
|
|
635
|
+
"cancel_at_period_end": false,
|
|
636
|
+
"canceled_at": null,
|
|
637
|
+
"trial_start": null,
|
|
638
|
+
"trial_end": null,
|
|
639
|
+
"subscription_metadata": {},
|
|
640
|
+
"created_at": "2024-01-01T00:00:00Z",
|
|
641
|
+
"updated_at": "2024-01-01T00:00:00Z"
|
|
621
642
|
}`,
|
|
622
643
|
note: "Does NOT include plan_name or price. Use plan_id to fetch plan details if needed."
|
|
623
644
|
},
|
|
@@ -797,12 +818,12 @@ const END_USER_ENDPOINTS = {
|
|
|
797
818
|
price: "string - Display price",
|
|
798
819
|
message: "string | null"
|
|
799
820
|
},
|
|
800
|
-
example_request: `{
|
|
801
|
-
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
802
|
-
"organization_id": "660e8400-e29b-41d4-a716-446655440001",
|
|
803
|
-
"plan_id": "770e8400-e29b-41d4-a716-446655440002",
|
|
804
|
-
"success_url": "https://myapp.com/billing/success",
|
|
805
|
-
"cancel_url": "https://myapp.com/billing/cancel"
|
|
821
|
+
example_request: `{
|
|
822
|
+
"user_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
823
|
+
"organization_id": "660e8400-e29b-41d4-a716-446655440001",
|
|
824
|
+
"plan_id": "770e8400-e29b-41d4-a716-446655440002",
|
|
825
|
+
"success_url": "https://myapp.com/billing/success",
|
|
826
|
+
"cancel_url": "https://myapp.com/billing/cancel"
|
|
806
827
|
}`
|
|
807
828
|
},
|
|
808
829
|
{
|
|
@@ -824,9 +845,9 @@ const END_USER_ENDPOINTS = {
|
|
|
824
845
|
cancel_at_period_end: "boolean | null"
|
|
825
846
|
},
|
|
826
847
|
note: "Upgrades create Stripe prorations. Downgrades cancel at period end and schedule the new plan.",
|
|
827
|
-
example_request: `{
|
|
828
|
-
"subscription_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
829
|
-
"new_plan_id": "660e8400-e29b-41d4-a716-446655440001"
|
|
848
|
+
example_request: `{
|
|
849
|
+
"subscription_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
850
|
+
"new_plan_id": "660e8400-e29b-41d4-a716-446655440001"
|
|
830
851
|
}`
|
|
831
852
|
},
|
|
832
853
|
{
|
|
@@ -899,31 +920,31 @@ const END_USER_ENDPOINTS = {
|
|
|
899
920
|
},
|
|
900
921
|
error: "string | null - Error message if any"
|
|
901
922
|
},
|
|
902
|
-
example_response: `{
|
|
903
|
-
"period_start": "2024-01-01T00:00:00Z",
|
|
904
|
-
"period_end": "2024-02-01T00:00:00Z",
|
|
905
|
-
"plan_name": "Pro Plan",
|
|
906
|
-
"plan_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
907
|
-
"usage": {
|
|
908
|
-
"messages": {
|
|
909
|
-
"current": 450,
|
|
910
|
-
"max": 1000,
|
|
911
|
-
"remaining": 550,
|
|
912
|
-
"percentage": 45.0
|
|
913
|
-
},
|
|
914
|
-
"cost_usd": {
|
|
915
|
-
"current": 23.50,
|
|
916
|
-
"max": 100.0,
|
|
917
|
-
"remaining": 76.50,
|
|
918
|
-
"percentage": 23.5
|
|
919
|
-
},
|
|
920
|
-
"tokens": {
|
|
921
|
-
"total": 125000,
|
|
922
|
-
"input": 75000,
|
|
923
|
-
"output": 50000
|
|
924
|
-
}
|
|
925
|
-
},
|
|
926
|
-
"error": null
|
|
923
|
+
example_response: `{
|
|
924
|
+
"period_start": "2024-01-01T00:00:00Z",
|
|
925
|
+
"period_end": "2024-02-01T00:00:00Z",
|
|
926
|
+
"plan_name": "Pro Plan",
|
|
927
|
+
"plan_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
928
|
+
"usage": {
|
|
929
|
+
"messages": {
|
|
930
|
+
"current": 450,
|
|
931
|
+
"max": 1000,
|
|
932
|
+
"remaining": 550,
|
|
933
|
+
"percentage": 45.0
|
|
934
|
+
},
|
|
935
|
+
"cost_usd": {
|
|
936
|
+
"current": 23.50,
|
|
937
|
+
"max": 100.0,
|
|
938
|
+
"remaining": 76.50,
|
|
939
|
+
"percentage": 23.5
|
|
940
|
+
},
|
|
941
|
+
"tokens": {
|
|
942
|
+
"total": 125000,
|
|
943
|
+
"input": 75000,
|
|
944
|
+
"output": 50000
|
|
945
|
+
}
|
|
946
|
+
},
|
|
947
|
+
"error": null
|
|
927
948
|
}`
|
|
928
949
|
},
|
|
929
950
|
{
|
|
@@ -1077,26 +1098,26 @@ const END_USER_ENDPOINTS = {
|
|
|
1077
1098
|
execution_metadata: "object | null",
|
|
1078
1099
|
created_at: "ISO datetime"
|
|
1079
1100
|
},
|
|
1080
|
-
example_request: `{
|
|
1081
|
-
"messages": [
|
|
1082
|
-
{"role": "user", "content": "Hello, how can you help me?"}
|
|
1083
|
-
],
|
|
1084
|
-
"temperature": 0.7
|
|
1101
|
+
example_request: `{
|
|
1102
|
+
"messages": [
|
|
1103
|
+
{"role": "user", "content": "Hello, how can you help me?"}
|
|
1104
|
+
],
|
|
1105
|
+
"temperature": 0.7
|
|
1085
1106
|
}`,
|
|
1086
|
-
example_response: `{
|
|
1087
|
-
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
1088
|
-
"agent_id": "660e8400-e29b-41d4-a716-446655440001",
|
|
1089
|
-
"tenant_id": "770e8400-e29b-41d4-a716-446655440002",
|
|
1090
|
-
"user_id": "880e8400-e29b-41d4-a716-446655440003",
|
|
1091
|
-
"organization_id": "990e8400-e29b-41d4-a716-446655440004",
|
|
1092
|
-
"input_text": "Hello, how can you help me?",
|
|
1093
|
-
"output_text": "I'm an AI assistant designed to help you with...",
|
|
1094
|
-
"tokens_used": 150,
|
|
1095
|
-
"execution_time_ms": 1234,
|
|
1096
|
-
"status": "SUCCESS",
|
|
1097
|
-
"error_message": null,
|
|
1098
|
-
"execution_metadata": {"model": "gpt-4o", "temperature": 0.7},
|
|
1099
|
-
"created_at": "2024-01-30T10:30:00Z"
|
|
1107
|
+
example_response: `{
|
|
1108
|
+
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
1109
|
+
"agent_id": "660e8400-e29b-41d4-a716-446655440001",
|
|
1110
|
+
"tenant_id": "770e8400-e29b-41d4-a716-446655440002",
|
|
1111
|
+
"user_id": "880e8400-e29b-41d4-a716-446655440003",
|
|
1112
|
+
"organization_id": "990e8400-e29b-41d4-a716-446655440004",
|
|
1113
|
+
"input_text": "Hello, how can you help me?",
|
|
1114
|
+
"output_text": "I'm an AI assistant designed to help you with...",
|
|
1115
|
+
"tokens_used": 150,
|
|
1116
|
+
"execution_time_ms": 1234,
|
|
1117
|
+
"status": "SUCCESS",
|
|
1118
|
+
"error_message": null,
|
|
1119
|
+
"execution_metadata": {"model": "gpt-4o", "temperature": 0.7},
|
|
1120
|
+
"created_at": "2024-01-30T10:30:00Z"
|
|
1100
1121
|
}`,
|
|
1101
1122
|
note: "Usage is tracked against your plan limits. Use /v1/api/usage/current to check remaining usage."
|
|
1102
1123
|
},
|
|
@@ -1507,13 +1528,13 @@ const END_USER_ENDPOINTS = {
|
|
|
1507
1528
|
order_dir: "'asc' | 'desc' (default 'desc')"
|
|
1508
1529
|
},
|
|
1509
1530
|
notes: "Supported operators: eq, ne, gt, gte, lt, lte, like, ilike, in, not_in",
|
|
1510
|
-
example_request: `{
|
|
1511
|
-
"schema_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
1512
|
-
"query": {
|
|
1513
|
-
"price": { "operator": "gt", "value": 100 },
|
|
1514
|
-
"category": "electronics",
|
|
1515
|
-
"status": { "operator": "in", "value": ["active", "pending"] }
|
|
1516
|
-
}
|
|
1531
|
+
example_request: `{
|
|
1532
|
+
"schema_id": "550e8400-e29b-41d4-a716-446655440000",
|
|
1533
|
+
"query": {
|
|
1534
|
+
"price": { "operator": "gt", "value": 100 },
|
|
1535
|
+
"category": "electronics",
|
|
1536
|
+
"status": { "operator": "in", "value": ["active", "pending"] }
|
|
1537
|
+
}
|
|
1517
1538
|
}`
|
|
1518
1539
|
}
|
|
1519
1540
|
]
|
|
@@ -1706,7 +1727,7 @@ const END_USER_ENDPOINTS = {
|
|
|
1706
1727
|
endpoints: [
|
|
1707
1728
|
{
|
|
1708
1729
|
method: "POST",
|
|
1709
|
-
path: "/v1/external-agents/{agent_id}/calls",
|
|
1730
|
+
path: "/v1/user/external-agents/{agent_id}/calls",
|
|
1710
1731
|
summary: "Register an external agent call",
|
|
1711
1732
|
auth: { api_key: "sk_live_*", jwt: true },
|
|
1712
1733
|
rate_limit: "120 per minute per IP",
|
|
@@ -1721,6 +1742,7 @@ const END_USER_ENDPOINTS = {
|
|
|
1721
1742
|
total_tokens: "integer (optional) - Total tokens. If not provided, calculated as input + output",
|
|
1722
1743
|
cost_usd: "number (optional, default: 0.0) - Cost in USD",
|
|
1723
1744
|
duration_ms: "integer (optional, default: 0) - Call duration in milliseconds",
|
|
1745
|
+
model_name: "string (optional) - Model used (e.g., openai/gpt-4o). If provided, cost is calculated from provider rates instead of flat rate",
|
|
1724
1746
|
status: "string (optional, default: 'SUCCESS') - 'SUCCESS' or 'ERROR'",
|
|
1725
1747
|
error_message: "string (optional) - Error message if status is ERROR",
|
|
1726
1748
|
call_metadata: "object (optional) - Arbitrary metadata about the call"
|
|
@@ -1741,7 +1763,60 @@ const END_USER_ENDPOINTS = {
|
|
|
1741
1763
|
call_metadata: "object | null",
|
|
1742
1764
|
created_at: "ISO datetime"
|
|
1743
1765
|
},
|
|
1744
|
-
note: "
|
|
1766
|
+
note: "Adds a fixed cost (EXTERNAL_AGENT_REQUEST_COST env var, default $0.01) to tenant billing. If model_name is provided with tokens > 0, cost is calculated from provider rates instead."
|
|
1767
|
+
},
|
|
1768
|
+
{
|
|
1769
|
+
method: "POST",
|
|
1770
|
+
path: "/v1/user/external-agents/{agent_id}/usage",
|
|
1771
|
+
summary: "Register usage metrics for an external agent",
|
|
1772
|
+
auth: { api_key: "sk_live_*", jwt: true },
|
|
1773
|
+
rate_limit: "120 per minute per IP",
|
|
1774
|
+
path_params: {
|
|
1775
|
+
agent_id: "uuid (required) - The external agent ID"
|
|
1776
|
+
},
|
|
1777
|
+
request_body: {
|
|
1778
|
+
input_tokens: "integer (optional, default: 0) - Number of input tokens consumed",
|
|
1779
|
+
output_tokens: "integer (optional, default: 0) - Number of output tokens consumed",
|
|
1780
|
+
cost_usd: "number (optional) - Explicit cost in USD. If omitted, calculated from model rates or flat rate",
|
|
1781
|
+
model_name: "string (optional) - Model used (e.g., openai/gpt-4o)",
|
|
1782
|
+
provider: "string (optional) - Provider name (e.g., openai, anthropic). Defaults to 'external_agent'",
|
|
1783
|
+
metadata: "object (optional) - Arbitrary metadata"
|
|
1784
|
+
},
|
|
1785
|
+
response: {
|
|
1786
|
+
tracked: "boolean - Whether usage was tracked successfully",
|
|
1787
|
+
tokens: "integer - Total tokens tracked",
|
|
1788
|
+
cost_usd: "number - Cost tracked in USD",
|
|
1789
|
+
model_name: "string - Model name used for tracking",
|
|
1790
|
+
provider: "string - Provider name used for tracking"
|
|
1791
|
+
},
|
|
1792
|
+
note: "Use this for batch usage reporting or updating token counts without creating a full call record. Tracks tokens and cost in tenant billing."
|
|
1793
|
+
},
|
|
1794
|
+
{
|
|
1795
|
+
method: "POST",
|
|
1796
|
+
path: "/v1/user/external-agents/{agent_id}/execute",
|
|
1797
|
+
summary: "Execute an external agent via its configured webhook",
|
|
1798
|
+
auth: { api_key: "sk_live_*", jwt: true },
|
|
1799
|
+
rate_limit: "60 per minute per IP",
|
|
1800
|
+
path_params: {
|
|
1801
|
+
agent_id: "uuid (required) - The external agent ID"
|
|
1802
|
+
},
|
|
1803
|
+
request_body: {
|
|
1804
|
+
input: "string (required) - Input text forwarded to the external agent",
|
|
1805
|
+
metadata: "object (optional) - Arbitrary context forwarded to the webhook"
|
|
1806
|
+
},
|
|
1807
|
+
response: {
|
|
1808
|
+
output: "string - Agent response text",
|
|
1809
|
+
input_tokens: "integer - Input tokens reported by the webhook",
|
|
1810
|
+
output_tokens: "integer - Output tokens reported by the webhook",
|
|
1811
|
+
total_tokens: "integer - input + output",
|
|
1812
|
+
duration_ms: "integer - Execution time measured by Genlobe",
|
|
1813
|
+
cost_usd: "number - Platform fee registered for this call",
|
|
1814
|
+
model_name: "string | null - Model reported by the webhook",
|
|
1815
|
+
status: "string - SUCCESS or ERROR",
|
|
1816
|
+
error_message: "string | null",
|
|
1817
|
+
call_id: "uuid | null - Registered call record"
|
|
1818
|
+
},
|
|
1819
|
+
note: "Requires the external agent to have webhook_url configured. Genlobe POSTs {agent_id, input, metadata} to the webhook and expects {output, input_tokens?, output_tokens?, model_name?} in the JSON response."
|
|
1745
1820
|
}
|
|
1746
1821
|
]
|
|
1747
1822
|
}
|
|
@@ -1749,619 +1824,619 @@ const END_USER_ENDPOINTS = {
|
|
|
1749
1824
|
// =============================================================================
|
|
1750
1825
|
// TypeScript/JavaScript SDK Template
|
|
1751
1826
|
// =============================================================================
|
|
1752
|
-
const SDK_TEMPLATE_TYPESCRIPT = `/**
|
|
1753
|
-
* Multi-tenant SaaS API Client
|
|
1754
|
-
*
|
|
1755
|
-
* Usage:
|
|
1756
|
-
* const api = new ApiClient('pk_live_your_key_here', 'https://api.yoursaas.com');
|
|
1757
|
-
*
|
|
1758
|
-
* // Login
|
|
1759
|
-
* const { access_token, user } = await api.auth.login('email@example.com', 'password');
|
|
1760
|
-
*
|
|
1761
|
-
* // Set token for authenticated requests
|
|
1762
|
-
* api.setAccessToken(access_token);
|
|
1763
|
-
*
|
|
1764
|
-
* // Get user's organizations
|
|
1765
|
-
* const orgs = await api.organizations.getMyOrganizations();
|
|
1766
|
-
*/
|
|
1767
|
-
|
|
1768
|
-
interface TokenResponse {
|
|
1769
|
-
access_token: string;
|
|
1770
|
-
refresh_token: string;
|
|
1771
|
-
token_type: string;
|
|
1772
|
-
expires_in: number;
|
|
1773
|
-
user: {
|
|
1774
|
-
id: string;
|
|
1775
|
-
email: string;
|
|
1776
|
-
display_name: string | null;
|
|
1777
|
-
avatar_url: string | null;
|
|
1778
|
-
is_active: boolean;
|
|
1779
|
-
created_at: string;
|
|
1780
|
-
};
|
|
1781
|
-
}
|
|
1782
|
-
|
|
1783
|
-
interface Organization {
|
|
1784
|
-
id: string;
|
|
1785
|
-
name: string;
|
|
1786
|
-
slug: string;
|
|
1787
|
-
description?: string;
|
|
1788
|
-
logo_url?: string;
|
|
1789
|
-
role: 'owner' | 'admin' | 'member' | 'developer';
|
|
1790
|
-
created_at: string;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
interface ApiError {
|
|
1794
|
-
detail: string;
|
|
1795
|
-
error?: string;
|
|
1796
|
-
error_description?: string;
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
class ApiClient {
|
|
1800
|
-
private apiKey: string;
|
|
1801
|
-
private baseUrl: string;
|
|
1802
|
-
private accessToken: string | null = null;
|
|
1803
|
-
private refreshToken: string | null = null;
|
|
1804
|
-
|
|
1805
|
-
constructor(apiKey: string, baseUrl: string) {
|
|
1806
|
-
if (!apiKey) throw new Error('API key is required');
|
|
1807
|
-
if (!baseUrl) throw new Error('Base URL is required');
|
|
1808
|
-
this.apiKey = apiKey;
|
|
1809
|
-
this.baseUrl = baseUrl.replace(/\\/$/, ''); // Remove trailing slash
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
setAccessToken(token: string) {
|
|
1813
|
-
this.accessToken = token;
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
setRefreshToken(token: string) {
|
|
1817
|
-
this.refreshToken = token;
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
private organizationId: string | null = null;
|
|
1821
|
-
|
|
1822
|
-
setOrganizationId(orgId: string) {
|
|
1823
|
-
this.organizationId = orgId;
|
|
1824
|
-
}
|
|
1825
|
-
|
|
1826
|
-
private async request<T>(
|
|
1827
|
-
method: string,
|
|
1828
|
-
endpoint: string,
|
|
1829
|
-
body?: any,
|
|
1830
|
-
requiresAuth = false
|
|
1831
|
-
): Promise<T> {
|
|
1832
|
-
const headers: Record<string, string> = {
|
|
1833
|
-
'X-API-Key': this.apiKey,
|
|
1834
|
-
'Content-Type': 'application/json',
|
|
1835
|
-
};
|
|
1836
|
-
|
|
1837
|
-
if (requiresAuth && this.accessToken) {
|
|
1838
|
-
headers['Authorization'] = \`Bearer \${this.accessToken}\`;
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
if (this.organizationId) {
|
|
1842
|
-
headers['X-Organization-Id'] = this.organizationId;
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
const response = await fetch(\`\${this.baseUrl}\${endpoint}\`, {
|
|
1846
|
-
method,
|
|
1847
|
-
headers,
|
|
1848
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
1849
|
-
});
|
|
1850
|
-
|
|
1851
|
-
if (!response.ok) {
|
|
1852
|
-
const error: ApiError = await response.json();
|
|
1853
|
-
throw new Error(error.detail || \`Request failed: \${response.status}\`);
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
return response.json();
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
// Auth endpoints
|
|
1860
|
-
auth = {
|
|
1861
|
-
register: async (email: string, password: string, displayName?: string) => {
|
|
1862
|
-
const result = await this.request<TokenResponse>('POST', '/v1/auth/register', {
|
|
1863
|
-
email,
|
|
1864
|
-
password,
|
|
1865
|
-
display_name: displayName,
|
|
1866
|
-
});
|
|
1867
|
-
this.accessToken = result.access_token;
|
|
1868
|
-
this.refreshToken = result.refresh_token;
|
|
1869
|
-
return result;
|
|
1870
|
-
},
|
|
1871
|
-
|
|
1872
|
-
login: async (email: string, password: string) => {
|
|
1873
|
-
const result = await this.request<TokenResponse>('POST', '/v1/auth/login', {
|
|
1874
|
-
email,
|
|
1875
|
-
password,
|
|
1876
|
-
});
|
|
1877
|
-
this.accessToken = result.access_token;
|
|
1878
|
-
this.refreshToken = result.refresh_token;
|
|
1879
|
-
return result;
|
|
1880
|
-
},
|
|
1881
|
-
|
|
1882
|
-
logout: async () => {
|
|
1883
|
-
await this.request('POST', '/v1/auth/logout', undefined, true);
|
|
1884
|
-
this.accessToken = null;
|
|
1885
|
-
this.refreshToken = null;
|
|
1886
|
-
},
|
|
1887
|
-
|
|
1888
|
-
me: async () => {
|
|
1889
|
-
return this.request<TokenResponse['user']>('GET', '/v1/auth/me', undefined, true);
|
|
1890
|
-
},
|
|
1891
|
-
|
|
1892
|
-
refresh: async () => {
|
|
1893
|
-
if (!this.refreshToken) throw new Error('No refresh token available');
|
|
1894
|
-
const result = await this.request<TokenResponse>('POST', '/v1/auth/refresh', {
|
|
1895
|
-
refresh_token: this.refreshToken,
|
|
1896
|
-
});
|
|
1897
|
-
this.accessToken = result.access_token;
|
|
1898
|
-
return result;
|
|
1899
|
-
},
|
|
1900
|
-
|
|
1901
|
-
forgotPassword: async (email: string) => {
|
|
1902
|
-
return this.request('POST', '/v1/auth/forgot-password', { email });
|
|
1903
|
-
},
|
|
1904
|
-
|
|
1905
|
-
resetPassword: async (token: string, newPassword: string) => {
|
|
1906
|
-
return this.request('POST', '/v1/auth/reset-password', {
|
|
1907
|
-
token,
|
|
1908
|
-
new_password: newPassword,
|
|
1909
|
-
});
|
|
1910
|
-
},
|
|
1911
|
-
|
|
1912
|
-
changePassword: async (currentPassword: string, newPassword: string) => {
|
|
1913
|
-
return this.request('POST', '/v1/auth/change-password', {
|
|
1914
|
-
current_password: currentPassword,
|
|
1915
|
-
new_password: newPassword,
|
|
1916
|
-
});
|
|
1917
|
-
},
|
|
1918
|
-
|
|
1919
|
-
verifyEmail: async (token: string) => {
|
|
1920
|
-
return this.request('POST', '/v1/auth/verify-email', { token });
|
|
1921
|
-
},
|
|
1922
|
-
|
|
1923
|
-
resendVerification: async (email: string) => {
|
|
1924
|
-
return this.request('POST', '/v1/auth/resend-verification', { email });
|
|
1925
|
-
},
|
|
1926
|
-
|
|
1927
|
-
getProviders: async () => {
|
|
1928
|
-
return this.request<{ providers: Array<{ provider: string; name: string }> }>(
|
|
1929
|
-
'GET', '/v1/auth/providers'
|
|
1930
|
-
);
|
|
1931
|
-
},
|
|
1932
|
-
|
|
1933
|
-
getGoogleAuthUrl: async (redirectUri: string) => {
|
|
1934
|
-
return this.request<{ authorization_url: string }>(
|
|
1935
|
-
'GET', \`/v1/auth/google/url?redirect_uri=\${encodeURIComponent(redirectUri)}\`
|
|
1936
|
-
);
|
|
1937
|
-
},
|
|
1938
|
-
};
|
|
1939
|
-
|
|
1940
|
-
// Agent endpoints
|
|
1941
|
-
agents = {
|
|
1942
|
-
getAvailable: async (skip = 0, limit = 50) => {
|
|
1943
|
-
return this.request(
|
|
1944
|
-
'GET',
|
|
1945
|
-
\`/v1/user/agents/available?skip=\${skip}&limit=\${limit}\`,
|
|
1946
|
-
undefined,
|
|
1947
|
-
true
|
|
1948
|
-
);
|
|
1949
|
-
},
|
|
1950
|
-
|
|
1951
|
-
get: async (agentId: string) => {
|
|
1952
|
-
return this.request('GET', \`/v1/user/agents/\${agentId}\`, undefined, true);
|
|
1953
|
-
},
|
|
1954
|
-
|
|
1955
|
-
chat: async (agentId: string, messages: Array<{ role: string; content: string }>, options?: { temperature?: number; model?: string; conversation_id?: string }) => {
|
|
1956
|
-
return this.request('POST', \`/v1/user/agents/\${agentId}/chat\`, {
|
|
1957
|
-
messages,
|
|
1958
|
-
...options,
|
|
1959
|
-
}, true);
|
|
1960
|
-
},
|
|
1961
|
-
|
|
1962
|
-
getHistory: async (agentId: string, skip = 0, limit = 20) => {
|
|
1963
|
-
return this.request(
|
|
1964
|
-
'GET',
|
|
1965
|
-
\`/v1/user/agents/\${agentId}/history?skip=\${skip}&limit=\${limit}\`,
|
|
1966
|
-
undefined,
|
|
1967
|
-
true
|
|
1968
|
-
);
|
|
1969
|
-
},
|
|
1970
|
-
|
|
1971
|
-
createConversation: async (agentId: string, title?: string) => {
|
|
1972
|
-
return this.request('POST', \`/v1/user/agents/\${agentId}/conversations\`, {
|
|
1973
|
-
title,
|
|
1974
|
-
}, true);
|
|
1975
|
-
},
|
|
1976
|
-
|
|
1977
|
-
listConversations: async (agentId: string, skip = 0, limit = 20) => {
|
|
1978
|
-
return this.request(
|
|
1979
|
-
'GET',
|
|
1980
|
-
\`/v1/user/agents/\${agentId}/conversations?skip=\${skip}&limit=\${limit}\`,
|
|
1981
|
-
undefined,
|
|
1982
|
-
true
|
|
1983
|
-
);
|
|
1984
|
-
},
|
|
1985
|
-
|
|
1986
|
-
getConversation: async (agentId: string, conversationId: string) => {
|
|
1987
|
-
return this.request(
|
|
1988
|
-
'GET',
|
|
1989
|
-
\`/v1/user/agents/\${agentId}/conversations/\${conversationId}\`,
|
|
1990
|
-
undefined,
|
|
1991
|
-
true
|
|
1992
|
-
);
|
|
1993
|
-
},
|
|
1994
|
-
|
|
1995
|
-
endConversation: async (agentId: string, conversationId: string) => {
|
|
1996
|
-
return this.request(
|
|
1997
|
-
'DELETE',
|
|
1998
|
-
\`/v1/user/agents/\${agentId}/conversations/\${conversationId}\`,
|
|
1999
|
-
undefined,
|
|
2000
|
-
true
|
|
2001
|
-
);
|
|
2002
|
-
},
|
|
2003
|
-
};
|
|
2004
|
-
|
|
2005
|
-
// Organization endpoints
|
|
2006
|
-
organizations = {
|
|
2007
|
-
getMyOrganizations: async () => {
|
|
2008
|
-
return this.request<{ organizations: Organization[] }>(
|
|
2009
|
-
'GET',
|
|
2010
|
-
'/v1/organizations/my-organizations',
|
|
2011
|
-
undefined,
|
|
2012
|
-
true
|
|
2013
|
-
);
|
|
2014
|
-
},
|
|
2015
|
-
|
|
2016
|
-
create: async (name: string, slug: string, description?: string) => {
|
|
2017
|
-
return this.request<Organization>(
|
|
2018
|
-
'POST',
|
|
2019
|
-
'/v1/organizations',
|
|
2020
|
-
{ name, slug, description },
|
|
2021
|
-
true
|
|
2022
|
-
);
|
|
2023
|
-
},
|
|
2024
|
-
|
|
2025
|
-
get: async (id: string) => {
|
|
2026
|
-
return this.request<Organization>(
|
|
2027
|
-
'GET',
|
|
2028
|
-
\`/v1/organizations/\${id}\`,
|
|
2029
|
-
undefined,
|
|
2030
|
-
true
|
|
2031
|
-
);
|
|
2032
|
-
},
|
|
2033
|
-
|
|
2034
|
-
update: async (id: string, data: { name?: string; description?: string }) => {
|
|
2035
|
-
return this.request<Organization>(
|
|
2036
|
-
'PUT',
|
|
2037
|
-
\`/v1/organizations/\${id}\`,
|
|
2038
|
-
data,
|
|
2039
|
-
true
|
|
2040
|
-
);
|
|
2041
|
-
},
|
|
2042
|
-
|
|
2043
|
-
getMembers: async (id: string) => {
|
|
2044
|
-
return this.request(
|
|
2045
|
-
'GET',
|
|
2046
|
-
\`/v1/organizations/\${id}/members\`,
|
|
2047
|
-
undefined,
|
|
2048
|
-
true
|
|
2049
|
-
);
|
|
2050
|
-
},
|
|
2051
|
-
|
|
2052
|
-
invite: async (id: string, email: string, role: 'admin' | 'member' | 'developer') => {
|
|
2053
|
-
return this.request(
|
|
2054
|
-
'POST',
|
|
2055
|
-
\`/v1/organizations/\${id}/invite\`,
|
|
2056
|
-
{ email, role },
|
|
2057
|
-
true
|
|
2058
|
-
);
|
|
2059
|
-
},
|
|
2060
|
-
|
|
2061
|
-
acceptInvitation: async (token: string) => {
|
|
2062
|
-
return this.request(
|
|
2063
|
-
'POST',
|
|
2064
|
-
'/v1/organizations/accept-invitation',
|
|
2065
|
-
{ token },
|
|
2066
|
-
true
|
|
2067
|
-
);
|
|
2068
|
-
},
|
|
2069
|
-
|
|
2070
|
-
rejectInvitation: async (token: string) => {
|
|
2071
|
-
return this.request(
|
|
2072
|
-
'POST',
|
|
2073
|
-
'/v1/organizations/reject-invitation',
|
|
2074
|
-
{ token }
|
|
2075
|
-
);
|
|
2076
|
-
},
|
|
2077
|
-
|
|
2078
|
-
updateMemberRole: async (orgId: string, userId: string, role: 'admin' | 'member' | 'developer') => {
|
|
2079
|
-
return this.request(
|
|
2080
|
-
'PUT',
|
|
2081
|
-
\`/v1/organizations/\${orgId}/members/\${userId}\`,
|
|
2082
|
-
{ role },
|
|
2083
|
-
true
|
|
2084
|
-
);
|
|
2085
|
-
},
|
|
2086
|
-
|
|
2087
|
-
removeMember: async (orgId: string, userId: string) => {
|
|
2088
|
-
return this.request(
|
|
2089
|
-
'DELETE',
|
|
2090
|
-
\`/v1/organizations/\${orgId}/members/\${userId}\`,
|
|
2091
|
-
undefined,
|
|
2092
|
-
true
|
|
2093
|
-
);
|
|
2094
|
-
},
|
|
2095
|
-
|
|
2096
|
-
deactivateMember: async (orgId: string, userId: string) => {
|
|
2097
|
-
return this.request(
|
|
2098
|
-
'POST',
|
|
2099
|
-
\`/v1/organizations/\${orgId}/members/\${userId}/deactivate\`,
|
|
2100
|
-
undefined,
|
|
2101
|
-
true
|
|
2102
|
-
);
|
|
2103
|
-
},
|
|
2104
|
-
|
|
2105
|
-
setDefault: async (orgId: string) => {
|
|
2106
|
-
return this.request(
|
|
2107
|
-
'POST',
|
|
2108
|
-
\`/v1/organizations/\${orgId}/set-default\`,
|
|
2109
|
-
undefined,
|
|
2110
|
-
true
|
|
2111
|
-
);
|
|
2112
|
-
},
|
|
2113
|
-
|
|
2114
|
-
getSubscription: async (orgId: string) => {
|
|
2115
|
-
return this.request('GET', \`/v1/organizations/\${orgId}/subscription\`, undefined, true);
|
|
2116
|
-
},
|
|
2117
|
-
|
|
2118
|
-
subscribe: async (orgId: string, planId: string) => {
|
|
2119
|
-
return this.request('POST', \`/v1/organizations/\${orgId}/subscription/\${planId}\`, undefined, true);
|
|
2120
|
-
},
|
|
2121
|
-
|
|
2122
|
-
cancelSubscription: async (orgId: string) => {
|
|
2123
|
-
return this.request('DELETE', \`/v1/organizations/\${orgId}/subscription\`, undefined, true);
|
|
2124
|
-
},
|
|
2125
|
-
|
|
2126
|
-
getUsage: async (orgId: string) => {
|
|
2127
|
-
return this.request('GET', \`/v1/organizations/\${orgId}/usage\`, undefined, true);
|
|
2128
|
-
},
|
|
2129
|
-
};
|
|
2130
|
-
|
|
2131
|
-
// Subscription endpoints
|
|
2132
|
-
subscriptions = {
|
|
2133
|
-
getPlans: async () => {
|
|
2134
|
-
return this.request('GET', '/v1/subscriptions/plans', undefined, true);
|
|
2135
|
-
},
|
|
2136
|
-
|
|
2137
|
-
getCurrent: async () => {
|
|
2138
|
-
return this.request('GET', '/v1/subscriptions/current', undefined, true);
|
|
2139
|
-
},
|
|
2140
|
-
|
|
2141
|
-
checkout: async (planId: string, successUrl: string, cancelUrl: string) => {
|
|
2142
|
-
return this.request<{ checkout_url: string; session_id: string }>(
|
|
2143
|
-
'POST',
|
|
2144
|
-
'/v1/subscriptions/checkout',
|
|
2145
|
-
{
|
|
2146
|
-
plan_id: planId,
|
|
2147
|
-
success_url: successUrl,
|
|
2148
|
-
cancel_url: cancelUrl,
|
|
2149
|
-
},
|
|
2150
|
-
true
|
|
2151
|
-
);
|
|
2152
|
-
},
|
|
2153
|
-
|
|
2154
|
-
getById: async (subscriptionId: string) => {
|
|
2155
|
-
return this.request('GET', \`/v1/subscriptions/subscriptions/\${subscriptionId}\`, undefined, true);
|
|
2156
|
-
},
|
|
2157
|
-
|
|
2158
|
-
cancel: async (subscriptionId: string, cancelAtPeriodEnd = true) => {
|
|
2159
|
-
return this.request(
|
|
2160
|
-
'DELETE',
|
|
2161
|
-
\`/v1/subscriptions/subscriptions/\${subscriptionId}?cancel_at_period_end=\${cancelAtPeriodEnd}\`,
|
|
2162
|
-
undefined,
|
|
2163
|
-
true
|
|
2164
|
-
);
|
|
2165
|
-
},
|
|
2166
|
-
};
|
|
2167
|
-
|
|
2168
|
-
// Billing endpoints (sk_live_* only)
|
|
2169
|
-
billing = {
|
|
2170
|
-
getAvailablePlans: async () => {
|
|
2171
|
-
return this.request('GET', '/v1/billing/available-plans', undefined, true);
|
|
2172
|
-
},
|
|
2173
|
-
|
|
2174
|
-
createCheckoutSession: async (data: { user_id: string; organization_id: string; plan_id: string; success_url: string; cancel_url: string }) => {
|
|
2175
|
-
return this.request<{ checkout_url: string; session_id: string; plan_name: string; price: string }>('POST', '/v1/billing/create-checkout-session', data, true);
|
|
2176
|
-
},
|
|
2177
|
-
|
|
2178
|
-
changePlan: async (data: { subscription_id: string; new_plan_id: string }) => {
|
|
2179
|
-
return this.request('POST', '/v1/billing/change-plan', data, true);
|
|
2180
|
-
},
|
|
2181
|
-
|
|
2182
|
-
cancelSubscription: async (subscriptionId: string, cancelAtPeriodEnd = true) => {
|
|
2183
|
-
return this.request('POST', \`/v1/billing/cancel-subscription?subscription_id=\${subscriptionId}&cancel_at_period_end=\${cancelAtPeriodEnd}\`, undefined, true);
|
|
2184
|
-
},
|
|
2185
|
-
|
|
2186
|
-
getBillingHistory: async (organizationId: string, limit = 20) => {
|
|
2187
|
-
return this.request('GET', \`/v1/billing/billing-history-org/\${organizationId}?limit=\${limit}\`, undefined, true);
|
|
2188
|
-
},
|
|
2189
|
-
};
|
|
2190
|
-
|
|
2191
|
-
// Plans endpoints (JWT required)
|
|
2192
|
-
plans = {
|
|
2193
|
-
list: async (skip = 0, limit = 100, includeInactive = false) => {
|
|
2194
|
-
return this.request('GET', \`/v1/plans?skip=\${skip}&limit=\${limit}&include_inactive=\${includeInactive}\`, undefined, true);
|
|
2195
|
-
},
|
|
2196
|
-
|
|
2197
|
-
getActive: async () => {
|
|
2198
|
-
return this.request('GET', '/v1/plans/active', undefined, true);
|
|
2199
|
-
},
|
|
2200
|
-
|
|
2201
|
-
getFree: async () => {
|
|
2202
|
-
return this.request('GET', '/v1/plans/free', undefined, true);
|
|
2203
|
-
},
|
|
2204
|
-
|
|
2205
|
-
getById: async (planId: string) => {
|
|
2206
|
-
return this.request('GET', \`/v1/plans/\${planId}\`, undefined, true);
|
|
2207
|
-
},
|
|
2208
|
-
};
|
|
2209
|
-
|
|
2210
|
-
// User management endpoints
|
|
2211
|
-
users = {
|
|
2212
|
-
list: async (page = 1, perPage = 20, includeInactive = false) => {
|
|
2213
|
-
return this.request('GET', \`/v1/users?page=\${page}&per_page=\${perPage}&include_inactive=\${includeInactive}\`, undefined, true);
|
|
2214
|
-
},
|
|
2215
|
-
|
|
2216
|
-
stats: async () => {
|
|
2217
|
-
return this.request('GET', '/v1/users/stats', undefined, true);
|
|
2218
|
-
},
|
|
2219
|
-
|
|
2220
|
-
get: async (userId: string, includeSettings = false) => {
|
|
2221
|
-
return this.request('GET', \`/v1/users/\${userId}?include_settings=\${includeSettings}\`, undefined, true);
|
|
2222
|
-
},
|
|
2223
|
-
|
|
2224
|
-
update: async (userId: string, data: { display_name?: string; avatar_url?: string; locale?: string }) => {
|
|
2225
|
-
return this.request('PUT', \`/v1/users/\${userId}\`, data, true);
|
|
2226
|
-
},
|
|
2227
|
-
|
|
2228
|
-
updateSettings: async (userId: string, settings: any) => {
|
|
2229
|
-
return this.request('PUT', \`/v1/users/\${userId}/settings\`, settings, true);
|
|
2230
|
-
},
|
|
2231
|
-
|
|
2232
|
-
deactivate: async (userId: string) => {
|
|
2233
|
-
return this.request('POST', \`/v1/users/\${userId}/deactivate\`, undefined, true);
|
|
2234
|
-
},
|
|
2235
|
-
|
|
2236
|
-
activate: async (userId: string) => {
|
|
2237
|
-
return this.request('POST', \`/v1/users/\${userId}/activate\`, undefined, true);
|
|
2238
|
-
},
|
|
2239
|
-
|
|
2240
|
-
delete: async (userId: string) => {
|
|
2241
|
-
return this.request('DELETE', \`/v1/users/\${userId}\`, undefined, true);
|
|
2242
|
-
},
|
|
2243
|
-
};
|
|
2244
|
-
|
|
2245
|
-
// Usage endpoints
|
|
2246
|
-
usage = {
|
|
2247
|
-
getCurrent: async (organizationId: string) => {
|
|
2248
|
-
return this.request('GET', \`/v1/api/usage/current?organization_id=\${organizationId}\`, undefined, true);
|
|
2249
|
-
},
|
|
2250
|
-
|
|
2251
|
-
getHistory: async (organizationId: string, limit = 20, offset = 0) => {
|
|
2252
|
-
return this.request(
|
|
2253
|
-
'GET',
|
|
2254
|
-
\`/v1/api/usage/history?organization_id=\${organizationId}&limit=\${limit}&offset=\${offset}\`,
|
|
2255
|
-
undefined,
|
|
2256
|
-
true
|
|
2257
|
-
);
|
|
2258
|
-
},
|
|
2259
|
-
|
|
2260
|
-
getEntityUsage: async (organizationId: string) => {
|
|
2261
|
-
return this.request('GET', \`/v1/api/usage/entity-usage?organization_id=\${organizationId}\`, undefined, true);
|
|
2262
|
-
},
|
|
2263
|
-
|
|
2264
|
-
getStats: async (organizationId: string) => {
|
|
2265
|
-
return this.request('GET', \`/v1/usage/stats?organization_id=\${organizationId}\`, undefined, true);
|
|
2266
|
-
},
|
|
2267
|
-
|
|
2268
|
-
check: async (data: { tokens: number; usage_type?: string; user_id: string; organization_id: string }) => {
|
|
2269
|
-
return this.request('POST', '/v1/usage/check', data, true);
|
|
2270
|
-
},
|
|
2271
|
-
|
|
2272
|
-
consume: async (data: { tokens: number; usage_type?: string; user_id: string; organization_id: string }) => {
|
|
2273
|
-
return this.request('POST', '/v1/usage/consume', data, true);
|
|
2274
|
-
},
|
|
2275
|
-
};
|
|
2276
|
-
|
|
2277
|
-
// Entity Schema endpoints
|
|
2278
|
-
entitySchemas = {
|
|
2279
|
-
list: async (page = 1, size = 20) => {
|
|
2280
|
-
return this.request('GET', \`/v1/entity/schemas?page=\${page}&size=\${size}\`, undefined, true);
|
|
2281
|
-
},
|
|
2282
|
-
|
|
2283
|
-
create: async (data: { name: string; slug: string; description?: string; fields_definition: any }) => {
|
|
2284
|
-
return this.request('POST', '/v1/entity/schemas', data, true);
|
|
2285
|
-
},
|
|
2286
|
-
|
|
2287
|
-
get: async (schemaId: string) => {
|
|
2288
|
-
return this.request('GET', \`/v1/entity/schemas/\${schemaId}\`, undefined, true);
|
|
2289
|
-
},
|
|
2290
|
-
|
|
2291
|
-
update: async (schemaId: string, data: { name?: string; description?: string; fields_definition?: any; is_active?: boolean }) => {
|
|
2292
|
-
return this.request('PUT', \`/v1/entity/schemas/\${schemaId}\`, data, true);
|
|
2293
|
-
},
|
|
2294
|
-
|
|
2295
|
-
delete: async (schemaId: string) => {
|
|
2296
|
-
return this.request('DELETE', \`/v1/entity/schemas/\${schemaId}\`, undefined, true);
|
|
2297
|
-
},
|
|
2298
|
-
};
|
|
2299
|
-
|
|
2300
|
-
// Entity Record endpoints
|
|
2301
|
-
entityRecords = {
|
|
2302
|
-
create: async (schemaId: string, data: any) => {
|
|
2303
|
-
return this.request('POST', '/v1/entity/records', { schema_id: schemaId, data }, true);
|
|
2304
|
-
},
|
|
2305
|
-
|
|
2306
|
-
get: async (recordId: string) => {
|
|
2307
|
-
return this.request('GET', \`/v1/entity/records/\${recordId}\`, undefined, true);
|
|
2308
|
-
},
|
|
2309
|
-
|
|
2310
|
-
update: async (recordId: string, data: any) => {
|
|
2311
|
-
return this.request('PUT', \`/v1/entity/records/\${recordId}\`, { data }, true);
|
|
2312
|
-
},
|
|
2313
|
-
|
|
2314
|
-
delete: async (recordId: string) => {
|
|
2315
|
-
return this.request('DELETE', \`/v1/entity/records/\${recordId}\`, undefined, true);
|
|
2316
|
-
},
|
|
2317
|
-
|
|
2318
|
-
search: async (searchReq: { schema_id?: string; query?: any; page?: number; size?: number; order_by?: string; order_dir?: string }) => {
|
|
2319
|
-
const params = new URLSearchParams();
|
|
2320
|
-
if (searchReq.page) params.set('page', String(searchReq.page));
|
|
2321
|
-
if (searchReq.size) params.set('size', String(searchReq.size));
|
|
2322
|
-
if (searchReq.order_by) params.set('order_by', searchReq.order_by);
|
|
2323
|
-
if (searchReq.order_dir) params.set('order_dir', searchReq.order_dir);
|
|
2324
|
-
return this.request('POST', \`/v1/entity/records/search?\${params}\`, { schema_id: searchReq.schema_id, query: searchReq.query }, true);
|
|
2325
|
-
},
|
|
2326
|
-
};
|
|
2327
|
-
|
|
2328
|
-
// Department endpoints
|
|
2329
|
-
departments = {
|
|
2330
|
-
get: async (departmentId: string, userId: string) => {
|
|
2331
|
-
return this.request('GET', \`/v1/departments/\${departmentId}?user_id=\${userId}\`, undefined, true);
|
|
2332
|
-
},
|
|
2333
|
-
};
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
export { ApiClient, TokenResponse, Organization, ApiError };
|
|
1827
|
+
const SDK_TEMPLATE_TYPESCRIPT = `/**
|
|
1828
|
+
* Multi-tenant SaaS API Client
|
|
1829
|
+
*
|
|
1830
|
+
* Usage:
|
|
1831
|
+
* const api = new ApiClient('pk_live_your_key_here', 'https://api.yoursaas.com');
|
|
1832
|
+
*
|
|
1833
|
+
* // Login
|
|
1834
|
+
* const { access_token, user } = await api.auth.login('email@example.com', 'password');
|
|
1835
|
+
*
|
|
1836
|
+
* // Set token for authenticated requests
|
|
1837
|
+
* api.setAccessToken(access_token);
|
|
1838
|
+
*
|
|
1839
|
+
* // Get user's organizations
|
|
1840
|
+
* const orgs = await api.organizations.getMyOrganizations();
|
|
1841
|
+
*/
|
|
1842
|
+
|
|
1843
|
+
interface TokenResponse {
|
|
1844
|
+
access_token: string;
|
|
1845
|
+
refresh_token: string;
|
|
1846
|
+
token_type: string;
|
|
1847
|
+
expires_in: number;
|
|
1848
|
+
user: {
|
|
1849
|
+
id: string;
|
|
1850
|
+
email: string;
|
|
1851
|
+
display_name: string | null;
|
|
1852
|
+
avatar_url: string | null;
|
|
1853
|
+
is_active: boolean;
|
|
1854
|
+
created_at: string;
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
interface Organization {
|
|
1859
|
+
id: string;
|
|
1860
|
+
name: string;
|
|
1861
|
+
slug: string;
|
|
1862
|
+
description?: string;
|
|
1863
|
+
logo_url?: string;
|
|
1864
|
+
role: 'owner' | 'admin' | 'member' | 'developer';
|
|
1865
|
+
created_at: string;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
interface ApiError {
|
|
1869
|
+
detail: string;
|
|
1870
|
+
error?: string;
|
|
1871
|
+
error_description?: string;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
class ApiClient {
|
|
1875
|
+
private apiKey: string;
|
|
1876
|
+
private baseUrl: string;
|
|
1877
|
+
private accessToken: string | null = null;
|
|
1878
|
+
private refreshToken: string | null = null;
|
|
1879
|
+
|
|
1880
|
+
constructor(apiKey: string, baseUrl: string) {
|
|
1881
|
+
if (!apiKey) throw new Error('API key is required');
|
|
1882
|
+
if (!baseUrl) throw new Error('Base URL is required');
|
|
1883
|
+
this.apiKey = apiKey;
|
|
1884
|
+
this.baseUrl = baseUrl.replace(/\\/$/, ''); // Remove trailing slash
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
setAccessToken(token: string) {
|
|
1888
|
+
this.accessToken = token;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
setRefreshToken(token: string) {
|
|
1892
|
+
this.refreshToken = token;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
private organizationId: string | null = null;
|
|
1896
|
+
|
|
1897
|
+
setOrganizationId(orgId: string) {
|
|
1898
|
+
this.organizationId = orgId;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
private async request<T>(
|
|
1902
|
+
method: string,
|
|
1903
|
+
endpoint: string,
|
|
1904
|
+
body?: any,
|
|
1905
|
+
requiresAuth = false
|
|
1906
|
+
): Promise<T> {
|
|
1907
|
+
const headers: Record<string, string> = {
|
|
1908
|
+
'X-API-Key': this.apiKey,
|
|
1909
|
+
'Content-Type': 'application/json',
|
|
1910
|
+
};
|
|
1911
|
+
|
|
1912
|
+
if (requiresAuth && this.accessToken) {
|
|
1913
|
+
headers['Authorization'] = \`Bearer \${this.accessToken}\`;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
if (this.organizationId) {
|
|
1917
|
+
headers['X-Organization-Id'] = this.organizationId;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
const response = await fetch(\`\${this.baseUrl}\${endpoint}\`, {
|
|
1921
|
+
method,
|
|
1922
|
+
headers,
|
|
1923
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
1924
|
+
});
|
|
1925
|
+
|
|
1926
|
+
if (!response.ok) {
|
|
1927
|
+
const error: ApiError = await response.json();
|
|
1928
|
+
throw new Error(error.detail || \`Request failed: \${response.status}\`);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
return response.json();
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// Auth endpoints
|
|
1935
|
+
auth = {
|
|
1936
|
+
register: async (email: string, password: string, displayName?: string) => {
|
|
1937
|
+
const result = await this.request<TokenResponse>('POST', '/v1/auth/register', {
|
|
1938
|
+
email,
|
|
1939
|
+
password,
|
|
1940
|
+
display_name: displayName,
|
|
1941
|
+
});
|
|
1942
|
+
this.accessToken = result.access_token;
|
|
1943
|
+
this.refreshToken = result.refresh_token;
|
|
1944
|
+
return result;
|
|
1945
|
+
},
|
|
1946
|
+
|
|
1947
|
+
login: async (email: string, password: string) => {
|
|
1948
|
+
const result = await this.request<TokenResponse>('POST', '/v1/auth/login', {
|
|
1949
|
+
email,
|
|
1950
|
+
password,
|
|
1951
|
+
});
|
|
1952
|
+
this.accessToken = result.access_token;
|
|
1953
|
+
this.refreshToken = result.refresh_token;
|
|
1954
|
+
return result;
|
|
1955
|
+
},
|
|
1956
|
+
|
|
1957
|
+
logout: async () => {
|
|
1958
|
+
await this.request('POST', '/v1/auth/logout', undefined, true);
|
|
1959
|
+
this.accessToken = null;
|
|
1960
|
+
this.refreshToken = null;
|
|
1961
|
+
},
|
|
1962
|
+
|
|
1963
|
+
me: async () => {
|
|
1964
|
+
return this.request<TokenResponse['user']>('GET', '/v1/auth/me', undefined, true);
|
|
1965
|
+
},
|
|
1966
|
+
|
|
1967
|
+
refresh: async () => {
|
|
1968
|
+
if (!this.refreshToken) throw new Error('No refresh token available');
|
|
1969
|
+
const result = await this.request<TokenResponse>('POST', '/v1/auth/refresh', {
|
|
1970
|
+
refresh_token: this.refreshToken,
|
|
1971
|
+
});
|
|
1972
|
+
this.accessToken = result.access_token;
|
|
1973
|
+
return result;
|
|
1974
|
+
},
|
|
1975
|
+
|
|
1976
|
+
forgotPassword: async (email: string) => {
|
|
1977
|
+
return this.request('POST', '/v1/auth/forgot-password', { email });
|
|
1978
|
+
},
|
|
1979
|
+
|
|
1980
|
+
resetPassword: async (token: string, newPassword: string) => {
|
|
1981
|
+
return this.request('POST', '/v1/auth/reset-password', {
|
|
1982
|
+
token,
|
|
1983
|
+
new_password: newPassword,
|
|
1984
|
+
});
|
|
1985
|
+
},
|
|
1986
|
+
|
|
1987
|
+
changePassword: async (currentPassword: string, newPassword: string) => {
|
|
1988
|
+
return this.request('POST', '/v1/auth/change-password', {
|
|
1989
|
+
current_password: currentPassword,
|
|
1990
|
+
new_password: newPassword,
|
|
1991
|
+
});
|
|
1992
|
+
},
|
|
1993
|
+
|
|
1994
|
+
verifyEmail: async (token: string) => {
|
|
1995
|
+
return this.request('POST', '/v1/auth/verify-email', { token });
|
|
1996
|
+
},
|
|
1997
|
+
|
|
1998
|
+
resendVerification: async (email: string) => {
|
|
1999
|
+
return this.request('POST', '/v1/auth/resend-verification', { email });
|
|
2000
|
+
},
|
|
2001
|
+
|
|
2002
|
+
getProviders: async () => {
|
|
2003
|
+
return this.request<{ providers: Array<{ provider: string; name: string }> }>(
|
|
2004
|
+
'GET', '/v1/auth/providers'
|
|
2005
|
+
);
|
|
2006
|
+
},
|
|
2007
|
+
|
|
2008
|
+
getGoogleAuthUrl: async (redirectUri: string) => {
|
|
2009
|
+
return this.request<{ authorization_url: string }>(
|
|
2010
|
+
'GET', \`/v1/auth/google/url?redirect_uri=\${encodeURIComponent(redirectUri)}\`
|
|
2011
|
+
);
|
|
2012
|
+
},
|
|
2013
|
+
};
|
|
2014
|
+
|
|
2015
|
+
// Agent endpoints
|
|
2016
|
+
agents = {
|
|
2017
|
+
getAvailable: async (skip = 0, limit = 50) => {
|
|
2018
|
+
return this.request(
|
|
2019
|
+
'GET',
|
|
2020
|
+
\`/v1/user/agents/available?skip=\${skip}&limit=\${limit}\`,
|
|
2021
|
+
undefined,
|
|
2022
|
+
true
|
|
2023
|
+
);
|
|
2024
|
+
},
|
|
2025
|
+
|
|
2026
|
+
get: async (agentId: string) => {
|
|
2027
|
+
return this.request('GET', \`/v1/user/agents/\${agentId}\`, undefined, true);
|
|
2028
|
+
},
|
|
2029
|
+
|
|
2030
|
+
chat: async (agentId: string, messages: Array<{ role: string; content: string }>, options?: { temperature?: number; model?: string; conversation_id?: string }) => {
|
|
2031
|
+
return this.request('POST', \`/v1/user/agents/\${agentId}/chat\`, {
|
|
2032
|
+
messages,
|
|
2033
|
+
...options,
|
|
2034
|
+
}, true);
|
|
2035
|
+
},
|
|
2036
|
+
|
|
2037
|
+
getHistory: async (agentId: string, skip = 0, limit = 20) => {
|
|
2038
|
+
return this.request(
|
|
2039
|
+
'GET',
|
|
2040
|
+
\`/v1/user/agents/\${agentId}/history?skip=\${skip}&limit=\${limit}\`,
|
|
2041
|
+
undefined,
|
|
2042
|
+
true
|
|
2043
|
+
);
|
|
2044
|
+
},
|
|
2045
|
+
|
|
2046
|
+
createConversation: async (agentId: string, title?: string) => {
|
|
2047
|
+
return this.request('POST', \`/v1/user/agents/\${agentId}/conversations\`, {
|
|
2048
|
+
title,
|
|
2049
|
+
}, true);
|
|
2050
|
+
},
|
|
2051
|
+
|
|
2052
|
+
listConversations: async (agentId: string, skip = 0, limit = 20) => {
|
|
2053
|
+
return this.request(
|
|
2054
|
+
'GET',
|
|
2055
|
+
\`/v1/user/agents/\${agentId}/conversations?skip=\${skip}&limit=\${limit}\`,
|
|
2056
|
+
undefined,
|
|
2057
|
+
true
|
|
2058
|
+
);
|
|
2059
|
+
},
|
|
2060
|
+
|
|
2061
|
+
getConversation: async (agentId: string, conversationId: string) => {
|
|
2062
|
+
return this.request(
|
|
2063
|
+
'GET',
|
|
2064
|
+
\`/v1/user/agents/\${agentId}/conversations/\${conversationId}\`,
|
|
2065
|
+
undefined,
|
|
2066
|
+
true
|
|
2067
|
+
);
|
|
2068
|
+
},
|
|
2069
|
+
|
|
2070
|
+
endConversation: async (agentId: string, conversationId: string) => {
|
|
2071
|
+
return this.request(
|
|
2072
|
+
'DELETE',
|
|
2073
|
+
\`/v1/user/agents/\${agentId}/conversations/\${conversationId}\`,
|
|
2074
|
+
undefined,
|
|
2075
|
+
true
|
|
2076
|
+
);
|
|
2077
|
+
},
|
|
2078
|
+
};
|
|
2079
|
+
|
|
2080
|
+
// Organization endpoints
|
|
2081
|
+
organizations = {
|
|
2082
|
+
getMyOrganizations: async () => {
|
|
2083
|
+
return this.request<{ organizations: Organization[] }>(
|
|
2084
|
+
'GET',
|
|
2085
|
+
'/v1/organizations/my-organizations',
|
|
2086
|
+
undefined,
|
|
2087
|
+
true
|
|
2088
|
+
);
|
|
2089
|
+
},
|
|
2090
|
+
|
|
2091
|
+
create: async (name: string, slug: string, description?: string) => {
|
|
2092
|
+
return this.request<Organization>(
|
|
2093
|
+
'POST',
|
|
2094
|
+
'/v1/organizations',
|
|
2095
|
+
{ name, slug, description },
|
|
2096
|
+
true
|
|
2097
|
+
);
|
|
2098
|
+
},
|
|
2099
|
+
|
|
2100
|
+
get: async (id: string) => {
|
|
2101
|
+
return this.request<Organization>(
|
|
2102
|
+
'GET',
|
|
2103
|
+
\`/v1/organizations/\${id}\`,
|
|
2104
|
+
undefined,
|
|
2105
|
+
true
|
|
2106
|
+
);
|
|
2107
|
+
},
|
|
2108
|
+
|
|
2109
|
+
update: async (id: string, data: { name?: string; description?: string }) => {
|
|
2110
|
+
return this.request<Organization>(
|
|
2111
|
+
'PUT',
|
|
2112
|
+
\`/v1/organizations/\${id}\`,
|
|
2113
|
+
data,
|
|
2114
|
+
true
|
|
2115
|
+
);
|
|
2116
|
+
},
|
|
2117
|
+
|
|
2118
|
+
getMembers: async (id: string) => {
|
|
2119
|
+
return this.request(
|
|
2120
|
+
'GET',
|
|
2121
|
+
\`/v1/organizations/\${id}/members\`,
|
|
2122
|
+
undefined,
|
|
2123
|
+
true
|
|
2124
|
+
);
|
|
2125
|
+
},
|
|
2126
|
+
|
|
2127
|
+
invite: async (id: string, email: string, role: 'admin' | 'member' | 'developer') => {
|
|
2128
|
+
return this.request(
|
|
2129
|
+
'POST',
|
|
2130
|
+
\`/v1/organizations/\${id}/invite\`,
|
|
2131
|
+
{ email, role },
|
|
2132
|
+
true
|
|
2133
|
+
);
|
|
2134
|
+
},
|
|
2135
|
+
|
|
2136
|
+
acceptInvitation: async (token: string) => {
|
|
2137
|
+
return this.request(
|
|
2138
|
+
'POST',
|
|
2139
|
+
'/v1/organizations/accept-invitation',
|
|
2140
|
+
{ token },
|
|
2141
|
+
true
|
|
2142
|
+
);
|
|
2143
|
+
},
|
|
2144
|
+
|
|
2145
|
+
rejectInvitation: async (token: string) => {
|
|
2146
|
+
return this.request(
|
|
2147
|
+
'POST',
|
|
2148
|
+
'/v1/organizations/reject-invitation',
|
|
2149
|
+
{ token }
|
|
2150
|
+
);
|
|
2151
|
+
},
|
|
2152
|
+
|
|
2153
|
+
updateMemberRole: async (orgId: string, userId: string, role: 'admin' | 'member' | 'developer') => {
|
|
2154
|
+
return this.request(
|
|
2155
|
+
'PUT',
|
|
2156
|
+
\`/v1/organizations/\${orgId}/members/\${userId}\`,
|
|
2157
|
+
{ role },
|
|
2158
|
+
true
|
|
2159
|
+
);
|
|
2160
|
+
},
|
|
2161
|
+
|
|
2162
|
+
removeMember: async (orgId: string, userId: string) => {
|
|
2163
|
+
return this.request(
|
|
2164
|
+
'DELETE',
|
|
2165
|
+
\`/v1/organizations/\${orgId}/members/\${userId}\`,
|
|
2166
|
+
undefined,
|
|
2167
|
+
true
|
|
2168
|
+
);
|
|
2169
|
+
},
|
|
2170
|
+
|
|
2171
|
+
deactivateMember: async (orgId: string, userId: string) => {
|
|
2172
|
+
return this.request(
|
|
2173
|
+
'POST',
|
|
2174
|
+
\`/v1/organizations/\${orgId}/members/\${userId}/deactivate\`,
|
|
2175
|
+
undefined,
|
|
2176
|
+
true
|
|
2177
|
+
);
|
|
2178
|
+
},
|
|
2179
|
+
|
|
2180
|
+
setDefault: async (orgId: string) => {
|
|
2181
|
+
return this.request(
|
|
2182
|
+
'POST',
|
|
2183
|
+
\`/v1/organizations/\${orgId}/set-default\`,
|
|
2184
|
+
undefined,
|
|
2185
|
+
true
|
|
2186
|
+
);
|
|
2187
|
+
},
|
|
2188
|
+
|
|
2189
|
+
getSubscription: async (orgId: string) => {
|
|
2190
|
+
return this.request('GET', \`/v1/organizations/\${orgId}/subscription\`, undefined, true);
|
|
2191
|
+
},
|
|
2192
|
+
|
|
2193
|
+
subscribe: async (orgId: string, planId: string) => {
|
|
2194
|
+
return this.request('POST', \`/v1/organizations/\${orgId}/subscription/\${planId}\`, undefined, true);
|
|
2195
|
+
},
|
|
2196
|
+
|
|
2197
|
+
cancelSubscription: async (orgId: string) => {
|
|
2198
|
+
return this.request('DELETE', \`/v1/organizations/\${orgId}/subscription\`, undefined, true);
|
|
2199
|
+
},
|
|
2200
|
+
|
|
2201
|
+
getUsage: async (orgId: string) => {
|
|
2202
|
+
return this.request('GET', \`/v1/organizations/\${orgId}/usage\`, undefined, true);
|
|
2203
|
+
},
|
|
2204
|
+
};
|
|
2205
|
+
|
|
2206
|
+
// Subscription endpoints
|
|
2207
|
+
subscriptions = {
|
|
2208
|
+
getPlans: async () => {
|
|
2209
|
+
return this.request('GET', '/v1/subscriptions/plans', undefined, true);
|
|
2210
|
+
},
|
|
2211
|
+
|
|
2212
|
+
getCurrent: async () => {
|
|
2213
|
+
return this.request('GET', '/v1/subscriptions/current', undefined, true);
|
|
2214
|
+
},
|
|
2215
|
+
|
|
2216
|
+
checkout: async (planId: string, successUrl: string, cancelUrl: string) => {
|
|
2217
|
+
return this.request<{ checkout_url: string; session_id: string }>(
|
|
2218
|
+
'POST',
|
|
2219
|
+
'/v1/subscriptions/checkout',
|
|
2220
|
+
{
|
|
2221
|
+
plan_id: planId,
|
|
2222
|
+
success_url: successUrl,
|
|
2223
|
+
cancel_url: cancelUrl,
|
|
2224
|
+
},
|
|
2225
|
+
true
|
|
2226
|
+
);
|
|
2227
|
+
},
|
|
2228
|
+
|
|
2229
|
+
getById: async (subscriptionId: string) => {
|
|
2230
|
+
return this.request('GET', \`/v1/subscriptions/subscriptions/\${subscriptionId}\`, undefined, true);
|
|
2231
|
+
},
|
|
2232
|
+
|
|
2233
|
+
cancel: async (subscriptionId: string, cancelAtPeriodEnd = true) => {
|
|
2234
|
+
return this.request(
|
|
2235
|
+
'DELETE',
|
|
2236
|
+
\`/v1/subscriptions/subscriptions/\${subscriptionId}?cancel_at_period_end=\${cancelAtPeriodEnd}\`,
|
|
2237
|
+
undefined,
|
|
2238
|
+
true
|
|
2239
|
+
);
|
|
2240
|
+
},
|
|
2241
|
+
};
|
|
2242
|
+
|
|
2243
|
+
// Billing endpoints (sk_live_* only)
|
|
2244
|
+
billing = {
|
|
2245
|
+
getAvailablePlans: async () => {
|
|
2246
|
+
return this.request('GET', '/v1/billing/available-plans', undefined, true);
|
|
2247
|
+
},
|
|
2248
|
+
|
|
2249
|
+
createCheckoutSession: async (data: { user_id: string; organization_id: string; plan_id: string; success_url: string; cancel_url: string }) => {
|
|
2250
|
+
return this.request<{ checkout_url: string; session_id: string; plan_name: string; price: string }>('POST', '/v1/billing/create-checkout-session', data, true);
|
|
2251
|
+
},
|
|
2252
|
+
|
|
2253
|
+
changePlan: async (data: { subscription_id: string; new_plan_id: string }) => {
|
|
2254
|
+
return this.request('POST', '/v1/billing/change-plan', data, true);
|
|
2255
|
+
},
|
|
2256
|
+
|
|
2257
|
+
cancelSubscription: async (subscriptionId: string, cancelAtPeriodEnd = true) => {
|
|
2258
|
+
return this.request('POST', \`/v1/billing/cancel-subscription?subscription_id=\${subscriptionId}&cancel_at_period_end=\${cancelAtPeriodEnd}\`, undefined, true);
|
|
2259
|
+
},
|
|
2260
|
+
|
|
2261
|
+
getBillingHistory: async (organizationId: string, limit = 20) => {
|
|
2262
|
+
return this.request('GET', \`/v1/billing/billing-history-org/\${organizationId}?limit=\${limit}\`, undefined, true);
|
|
2263
|
+
},
|
|
2264
|
+
};
|
|
2265
|
+
|
|
2266
|
+
// Plans endpoints (JWT required)
|
|
2267
|
+
plans = {
|
|
2268
|
+
list: async (skip = 0, limit = 100, includeInactive = false) => {
|
|
2269
|
+
return this.request('GET', \`/v1/plans?skip=\${skip}&limit=\${limit}&include_inactive=\${includeInactive}\`, undefined, true);
|
|
2270
|
+
},
|
|
2271
|
+
|
|
2272
|
+
getActive: async () => {
|
|
2273
|
+
return this.request('GET', '/v1/plans/active', undefined, true);
|
|
2274
|
+
},
|
|
2275
|
+
|
|
2276
|
+
getFree: async () => {
|
|
2277
|
+
return this.request('GET', '/v1/plans/free', undefined, true);
|
|
2278
|
+
},
|
|
2279
|
+
|
|
2280
|
+
getById: async (planId: string) => {
|
|
2281
|
+
return this.request('GET', \`/v1/plans/\${planId}\`, undefined, true);
|
|
2282
|
+
},
|
|
2283
|
+
};
|
|
2284
|
+
|
|
2285
|
+
// User management endpoints
|
|
2286
|
+
users = {
|
|
2287
|
+
list: async (page = 1, perPage = 20, includeInactive = false) => {
|
|
2288
|
+
return this.request('GET', \`/v1/users?page=\${page}&per_page=\${perPage}&include_inactive=\${includeInactive}\`, undefined, true);
|
|
2289
|
+
},
|
|
2290
|
+
|
|
2291
|
+
stats: async () => {
|
|
2292
|
+
return this.request('GET', '/v1/users/stats', undefined, true);
|
|
2293
|
+
},
|
|
2294
|
+
|
|
2295
|
+
get: async (userId: string, includeSettings = false) => {
|
|
2296
|
+
return this.request('GET', \`/v1/users/\${userId}?include_settings=\${includeSettings}\`, undefined, true);
|
|
2297
|
+
},
|
|
2298
|
+
|
|
2299
|
+
update: async (userId: string, data: { display_name?: string; avatar_url?: string; locale?: string }) => {
|
|
2300
|
+
return this.request('PUT', \`/v1/users/\${userId}\`, data, true);
|
|
2301
|
+
},
|
|
2302
|
+
|
|
2303
|
+
updateSettings: async (userId: string, settings: any) => {
|
|
2304
|
+
return this.request('PUT', \`/v1/users/\${userId}/settings\`, settings, true);
|
|
2305
|
+
},
|
|
2306
|
+
|
|
2307
|
+
deactivate: async (userId: string) => {
|
|
2308
|
+
return this.request('POST', \`/v1/users/\${userId}/deactivate\`, undefined, true);
|
|
2309
|
+
},
|
|
2310
|
+
|
|
2311
|
+
activate: async (userId: string) => {
|
|
2312
|
+
return this.request('POST', \`/v1/users/\${userId}/activate\`, undefined, true);
|
|
2313
|
+
},
|
|
2314
|
+
|
|
2315
|
+
delete: async (userId: string) => {
|
|
2316
|
+
return this.request('DELETE', \`/v1/users/\${userId}\`, undefined, true);
|
|
2317
|
+
},
|
|
2318
|
+
};
|
|
2319
|
+
|
|
2320
|
+
// Usage endpoints
|
|
2321
|
+
usage = {
|
|
2322
|
+
getCurrent: async (organizationId: string) => {
|
|
2323
|
+
return this.request('GET', \`/v1/api/usage/current?organization_id=\${organizationId}\`, undefined, true);
|
|
2324
|
+
},
|
|
2325
|
+
|
|
2326
|
+
getHistory: async (organizationId: string, limit = 20, offset = 0) => {
|
|
2327
|
+
return this.request(
|
|
2328
|
+
'GET',
|
|
2329
|
+
\`/v1/api/usage/history?organization_id=\${organizationId}&limit=\${limit}&offset=\${offset}\`,
|
|
2330
|
+
undefined,
|
|
2331
|
+
true
|
|
2332
|
+
);
|
|
2333
|
+
},
|
|
2334
|
+
|
|
2335
|
+
getEntityUsage: async (organizationId: string) => {
|
|
2336
|
+
return this.request('GET', \`/v1/api/usage/entity-usage?organization_id=\${organizationId}\`, undefined, true);
|
|
2337
|
+
},
|
|
2338
|
+
|
|
2339
|
+
getStats: async (organizationId: string) => {
|
|
2340
|
+
return this.request('GET', \`/v1/usage/stats?organization_id=\${organizationId}\`, undefined, true);
|
|
2341
|
+
},
|
|
2342
|
+
|
|
2343
|
+
check: async (data: { tokens: number; usage_type?: string; user_id: string; organization_id: string }) => {
|
|
2344
|
+
return this.request('POST', '/v1/usage/check', data, true);
|
|
2345
|
+
},
|
|
2346
|
+
|
|
2347
|
+
consume: async (data: { tokens: number; usage_type?: string; user_id: string; organization_id: string }) => {
|
|
2348
|
+
return this.request('POST', '/v1/usage/consume', data, true);
|
|
2349
|
+
},
|
|
2350
|
+
};
|
|
2351
|
+
|
|
2352
|
+
// Entity Schema endpoints
|
|
2353
|
+
entitySchemas = {
|
|
2354
|
+
list: async (page = 1, size = 20) => {
|
|
2355
|
+
return this.request('GET', \`/v1/entity/schemas?page=\${page}&size=\${size}\`, undefined, true);
|
|
2356
|
+
},
|
|
2357
|
+
|
|
2358
|
+
create: async (data: { name: string; slug: string; description?: string; fields_definition: any }) => {
|
|
2359
|
+
return this.request('POST', '/v1/entity/schemas', data, true);
|
|
2360
|
+
},
|
|
2361
|
+
|
|
2362
|
+
get: async (schemaId: string) => {
|
|
2363
|
+
return this.request('GET', \`/v1/entity/schemas/\${schemaId}\`, undefined, true);
|
|
2364
|
+
},
|
|
2365
|
+
|
|
2366
|
+
update: async (schemaId: string, data: { name?: string; description?: string; fields_definition?: any; is_active?: boolean }) => {
|
|
2367
|
+
return this.request('PUT', \`/v1/entity/schemas/\${schemaId}\`, data, true);
|
|
2368
|
+
},
|
|
2369
|
+
|
|
2370
|
+
delete: async (schemaId: string) => {
|
|
2371
|
+
return this.request('DELETE', \`/v1/entity/schemas/\${schemaId}\`, undefined, true);
|
|
2372
|
+
},
|
|
2373
|
+
};
|
|
2374
|
+
|
|
2375
|
+
// Entity Record endpoints
|
|
2376
|
+
entityRecords = {
|
|
2377
|
+
create: async (schemaId: string, data: any) => {
|
|
2378
|
+
return this.request('POST', '/v1/entity/records', { schema_id: schemaId, data }, true);
|
|
2379
|
+
},
|
|
2380
|
+
|
|
2381
|
+
get: async (recordId: string) => {
|
|
2382
|
+
return this.request('GET', \`/v1/entity/records/\${recordId}\`, undefined, true);
|
|
2383
|
+
},
|
|
2384
|
+
|
|
2385
|
+
update: async (recordId: string, data: any) => {
|
|
2386
|
+
return this.request('PUT', \`/v1/entity/records/\${recordId}\`, { data }, true);
|
|
2387
|
+
},
|
|
2388
|
+
|
|
2389
|
+
delete: async (recordId: string) => {
|
|
2390
|
+
return this.request('DELETE', \`/v1/entity/records/\${recordId}\`, undefined, true);
|
|
2391
|
+
},
|
|
2392
|
+
|
|
2393
|
+
search: async (searchReq: { schema_id?: string; query?: any; page?: number; size?: number; order_by?: string; order_dir?: string }) => {
|
|
2394
|
+
const params = new URLSearchParams();
|
|
2395
|
+
if (searchReq.page) params.set('page', String(searchReq.page));
|
|
2396
|
+
if (searchReq.size) params.set('size', String(searchReq.size));
|
|
2397
|
+
if (searchReq.order_by) params.set('order_by', searchReq.order_by);
|
|
2398
|
+
if (searchReq.order_dir) params.set('order_dir', searchReq.order_dir);
|
|
2399
|
+
return this.request('POST', \`/v1/entity/records/search?\${params}\`, { schema_id: searchReq.schema_id, query: searchReq.query }, true);
|
|
2400
|
+
},
|
|
2401
|
+
};
|
|
2402
|
+
|
|
2403
|
+
// Department endpoints
|
|
2404
|
+
departments = {
|
|
2405
|
+
get: async (departmentId: string, userId: string) => {
|
|
2406
|
+
return this.request('GET', \`/v1/departments/\${departmentId}?user_id=\${userId}\`, undefined, true);
|
|
2407
|
+
},
|
|
2408
|
+
};
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
export { ApiClient, TokenResponse, Organization, ApiError };
|
|
2337
2412
|
`;
|
|
2338
2413
|
// =============================================================================
|
|
2339
2414
|
// Request Headers Helper
|
|
2340
2415
|
// =============================================================================
|
|
2341
|
-
const REQUEST_HEADERS = `
|
|
2342
|
-
## Required Headers for ALL Requests
|
|
2343
|
-
|
|
2344
|
-
\`\`\`
|
|
2345
|
-
X-API-Key: pk_live_xxxxx
|
|
2346
|
-
Content-Type: application/json
|
|
2347
|
-
\`\`\`
|
|
2348
|
-
|
|
2349
|
-
## For Authenticated Requests (after login)
|
|
2350
|
-
|
|
2351
|
-
\`\`\`
|
|
2352
|
-
X-API-Key: pk_live_xxxxx
|
|
2353
|
-
Content-Type: application/json
|
|
2354
|
-
Authorization: Bearer <access_token>
|
|
2355
|
-
\`\`\`
|
|
2356
|
-
|
|
2357
|
-
## Header Details
|
|
2358
|
-
|
|
2359
|
-
| Header | Required | Description |
|
|
2360
|
-
|--------|----------|-------------|
|
|
2361
|
-
| X-API-Key | Always | Your API key (pk_live_* for frontend) |
|
|
2362
|
-
| Content-Type | For POST/PUT/PATCH | Always use application/json |
|
|
2363
|
-
| Authorization | After login | Bearer token from login response |
|
|
2364
|
-
| X-Organization-Id | For org-scoped endpoints | Organization UUID (entities, usage) |
|
|
2416
|
+
const REQUEST_HEADERS = `
|
|
2417
|
+
## Required Headers for ALL Requests
|
|
2418
|
+
|
|
2419
|
+
\`\`\`
|
|
2420
|
+
X-API-Key: pk_live_xxxxx
|
|
2421
|
+
Content-Type: application/json
|
|
2422
|
+
\`\`\`
|
|
2423
|
+
|
|
2424
|
+
## For Authenticated Requests (after login)
|
|
2425
|
+
|
|
2426
|
+
\`\`\`
|
|
2427
|
+
X-API-Key: pk_live_xxxxx
|
|
2428
|
+
Content-Type: application/json
|
|
2429
|
+
Authorization: Bearer <access_token>
|
|
2430
|
+
\`\`\`
|
|
2431
|
+
|
|
2432
|
+
## Header Details
|
|
2433
|
+
|
|
2434
|
+
| Header | Required | Description |
|
|
2435
|
+
|--------|----------|-------------|
|
|
2436
|
+
| X-API-Key | Always | Your API key (pk_live_* for frontend) |
|
|
2437
|
+
| Content-Type | For POST/PUT/PATCH | Always use application/json |
|
|
2438
|
+
| Authorization | After login | Bearer token from login response |
|
|
2439
|
+
| X-Organization-Id | For org-scoped endpoints | Organization UUID (entities, usage) |
|
|
2365
2440
|
`;
|
|
2366
2441
|
// =============================================================================
|
|
2367
2442
|
// API Client
|
|
@@ -2395,12 +2470,390 @@ async function fetchFromApi(endpoint) {
|
|
|
2395
2470
|
throw error;
|
|
2396
2471
|
}
|
|
2397
2472
|
}
|
|
2473
|
+
function detectKeyType(key) {
|
|
2474
|
+
if (!key)
|
|
2475
|
+
return { type: null, prefix: "" };
|
|
2476
|
+
// Mask everything past the prefix so logs don't leak.
|
|
2477
|
+
const prefix = key.slice(0, 12) + "…";
|
|
2478
|
+
if (key.startsWith("pk_"))
|
|
2479
|
+
return { type: "public", prefix };
|
|
2480
|
+
if (key.startsWith("sk_"))
|
|
2481
|
+
return { type: "secret", prefix };
|
|
2482
|
+
return { type: null, prefix };
|
|
2483
|
+
}
|
|
2484
|
+
function SECURITY_GUIDE(detected) {
|
|
2485
|
+
if (detected === null) {
|
|
2486
|
+
return `# API key security guide
|
|
2487
|
+
|
|
2488
|
+
No \`SAAS_API_KEY\` is configured in the MCP env, so this guide is generic.
|
|
2489
|
+
Configure a key first (\`validate_credentials\` will tell you which one
|
|
2490
|
+
you have) and rerun this tool to get the targeted guide.
|
|
2491
|
+
|
|
2492
|
+
## Two key types — pick the right one BEFORE writing code
|
|
2493
|
+
|
|
2494
|
+
### Public key (\`pk_live_*\`)
|
|
2495
|
+
|
|
2496
|
+
- **Surface:** safe to ship in browser / mobile bundles, **only when
|
|
2497
|
+
paired with allowed-origins restrictions configured at key creation**.
|
|
2498
|
+
- **Allowed endpoints:** auth-only (\`/v1/auth/register\`, \`login\`,
|
|
2499
|
+
\`refresh\`, \`logout\`, \`me\`, \`forgot-password\`, \`reset-password\`).
|
|
2500
|
+
- **Forbidden:** any admin / data-listing endpoint. The backend rejects
|
|
2501
|
+
these calls at the middleware level — no surprises.
|
|
2502
|
+
|
|
2503
|
+
### Secret key (\`sk_live_*\`)
|
|
2504
|
+
|
|
2505
|
+
- **Surface:** server-side ONLY. Never put in a browser bundle, mobile
|
|
2506
|
+
app, public repo, or any environment a user can inspect.
|
|
2507
|
+
- **Allowed endpoints:** every end-user endpoint, including admin
|
|
2508
|
+
listings (\`/v1/auth/users\`, \`/v1/dashboard/*\` patterns through
|
|
2509
|
+
appropriate flows).
|
|
2510
|
+
- **Forbidden:** including the literal string in client-side code.
|
|
2511
|
+
|
|
2512
|
+
## Common leak vectors to watch out for
|
|
2513
|
+
|
|
2514
|
+
- Hardcoding into Git history (rotate the key if it slipped — \`git\`
|
|
2515
|
+
history is forever).
|
|
2516
|
+
- \`NEXT_PUBLIC_*\` / \`VITE_*\` / \`PUBLIC_*\` env vars: these get baked
|
|
2517
|
+
into the client bundle. Only public keys belong here.
|
|
2518
|
+
- CI logs that echo env vars.
|
|
2519
|
+
- Browser DevTools "Network" tab: every request leaks its headers to
|
|
2520
|
+
whoever has the device. With public keys + allowed-origins this is
|
|
2521
|
+
fine; with secret keys it is a breach.`;
|
|
2522
|
+
}
|
|
2523
|
+
if (detected === "public") {
|
|
2524
|
+
return `# Security guide for your **public** key (\`pk_live_*\`)
|
|
2525
|
+
|
|
2526
|
+
Your configured key is a public key. The platform allows it in browser
|
|
2527
|
+
bundles and mobile apps **only if** you set its \`allowed_origins\` at
|
|
2528
|
+
creation. Verify that with the dashboard or with \`get_endpoint_details\`
|
|
2529
|
+
for \`POST /v1/dashboard/api-keys\`.
|
|
2530
|
+
|
|
2531
|
+
## ✅ Where it's safe to put this key
|
|
2532
|
+
|
|
2533
|
+
- A React/Vue/Svelte SPA bundle, served from one of the allowed origins.
|
|
2534
|
+
- A Next.js / Remix / SvelteKit app, in either client or server code.
|
|
2535
|
+
- A mobile app (React Native / Flutter / Swift / Kotlin).
|
|
2536
|
+
- An \`.env\` file committed to a private repo, prefixed
|
|
2537
|
+
\`NEXT_PUBLIC_*\` / \`VITE_*\` / \`PUBLIC_*\` so the bundler exposes it.
|
|
2538
|
+
|
|
2539
|
+
## ❌ Endpoints this key CAN'T call
|
|
2540
|
+
|
|
2541
|
+
The backend will reject calls to admin / management endpoints with this
|
|
2542
|
+
key. Trying to do \`/v1/auth/users\` (the user list) or any \`/v1/dashboard/*\`
|
|
2543
|
+
endpoint with a public key returns 401/403. Use a secret key on a server
|
|
2544
|
+
for those.
|
|
2545
|
+
|
|
2546
|
+
## Allowed endpoints (full list)
|
|
2547
|
+
|
|
2548
|
+
- \`POST /v1/auth/register\`
|
|
2549
|
+
- \`POST /v1/auth/login\`
|
|
2550
|
+
- \`POST /v1/auth/refresh\`
|
|
2551
|
+
- \`POST /v1/auth/logout\`
|
|
2552
|
+
- \`GET /v1/auth/me\`
|
|
2553
|
+
- \`POST /v1/auth/forgot-password\`
|
|
2554
|
+
- \`POST /v1/auth/reset-password\`
|
|
2555
|
+
- \`POST /v1/auth/verify-email\`
|
|
2556
|
+
- \`POST /v1/auth/resend-verification\`
|
|
2557
|
+
- \`GET /v1/auth/providers\`
|
|
2558
|
+
- \`GET /v1/auth/google/url\` (and equivalents per provider)
|
|
2559
|
+
|
|
2560
|
+
## Allowed-origins rule of thumb
|
|
2561
|
+
|
|
2562
|
+
- Always include every domain you serve the app from, including
|
|
2563
|
+
\`http://localhost:3000\` for dev. Origins that aren't on the list
|
|
2564
|
+
produce a 403 even if the key is correct.
|
|
2565
|
+
- Wildcards are not allowed. Listing the exact origins is the point.
|
|
2566
|
+
|
|
2567
|
+
## Things that are still secret even with a public key
|
|
2568
|
+
|
|
2569
|
+
- The end-user's **JWT** issued after \`/v1/auth/login\`. Treat it like a
|
|
2570
|
+
session token: keep it in memory or HTTP-only cookies, never in
|
|
2571
|
+
localStorage if XSS is a real threat for your app.
|
|
2572
|
+
|
|
2573
|
+
## Recommended next step
|
|
2574
|
+
|
|
2575
|
+
Run \`recommend_stack\` with the kind of app you want to build to get a
|
|
2576
|
+
matching framework + architecture. For a public key the answer is
|
|
2577
|
+
usually \`react / vue / svelte\` for SPA, or \`next / remix / sveltekit\`
|
|
2578
|
+
for an app that also needs SSR.`;
|
|
2579
|
+
}
|
|
2580
|
+
// secret
|
|
2581
|
+
return `# Security guide for your **secret** key (\`sk_live_*\`)
|
|
2582
|
+
|
|
2583
|
+
Your configured key has full end-user API access on behalf of the
|
|
2584
|
+
tenant. **It must never reach a browser, a mobile bundle, a public
|
|
2585
|
+
repo, or anywhere a user can inspect.**
|
|
2586
|
+
|
|
2587
|
+
## ✅ Where it's safe to put this key
|
|
2588
|
+
|
|
2589
|
+
- A server you run: Node.js / Python / Go / Rust / Ruby — anywhere the
|
|
2590
|
+
source isn't shipped to clients.
|
|
2591
|
+
- A Next.js / Remix / SvelteKit app, **only inside server-only files**
|
|
2592
|
+
(\`app/api/**\`, server components, route handlers, server actions,
|
|
2593
|
+
\`+page.server.ts\`, route loaders). Pair with environment variables
|
|
2594
|
+
read **without** a \`NEXT_PUBLIC_\` / \`VITE_\` / \`PUBLIC_\` prefix so
|
|
2595
|
+
the bundler does not embed it.
|
|
2596
|
+
- Edge functions (Vercel Edge / Cloudflare Workers) where the secret
|
|
2597
|
+
lives in the platform's secret store, not in code.
|
|
2598
|
+
- CI/CD secrets stores: GitHub Actions encrypted secrets, Doppler,
|
|
2599
|
+
AWS Secrets Manager, etc.
|
|
2600
|
+
|
|
2601
|
+
## ❌ Where it MUST NOT go
|
|
2602
|
+
|
|
2603
|
+
- React/Vue/Svelte client components that the bundler ships to the browser.
|
|
2604
|
+
- \`NEXT_PUBLIC_*\`, \`VITE_*\`, \`PUBLIC_*\` env vars (they end up in the
|
|
2605
|
+
bundle).
|
|
2606
|
+
- Mobile apps — the binary can be reverse-engineered, the key extracted.
|
|
2607
|
+
- Git history, including \`.env\` accidentally committed before
|
|
2608
|
+
\`.gitignore\` was set up.
|
|
2609
|
+
- Browser DevTools / sw caches (don't forward it through service workers).
|
|
2610
|
+
- Logs that aren't access-controlled.
|
|
2611
|
+
|
|
2612
|
+
## What this key can do
|
|
2613
|
+
|
|
2614
|
+
- Every end-user-facing endpoint, including listings that public keys
|
|
2615
|
+
can't touch (\`GET /v1/auth/users\`, \`GET /v1/auth/providers\`,
|
|
2616
|
+
user-by-id lookups, billing operations on behalf of end-users, etc.).
|
|
2617
|
+
- Acts on behalf of the tenant — every call is attributed to the
|
|
2618
|
+
organization the key is scoped to.
|
|
2619
|
+
|
|
2620
|
+
## Architecture pattern that keeps it safe
|
|
2621
|
+
|
|
2622
|
+
Don't expose this key to the client. Put a thin server in front and
|
|
2623
|
+
proxy the requests:
|
|
2624
|
+
|
|
2625
|
+
\`\`\`
|
|
2626
|
+
[Browser app] → POST /api/login → [Your server] → POST /v1/auth/login → [Genlobe]
|
|
2627
|
+
(no key) (X-API-Key: sk_live_…)
|
|
2628
|
+
\`\`\`
|
|
2629
|
+
|
|
2630
|
+
Your server holds the key in env. The browser never sees it. The
|
|
2631
|
+
end-user JWT that comes back from \`/v1/auth/login\` is what your
|
|
2632
|
+
browser app holds for that session.
|
|
2633
|
+
|
|
2634
|
+
## Common leak vectors (each one is a real incident pattern)
|
|
2635
|
+
|
|
2636
|
+
- "Just for testing" hardcoded into a React component → committed →
|
|
2637
|
+
pushed → key on GitHub forever (rotate immediately).
|
|
2638
|
+
- A \`fetch\` call inside \`useEffect\` that sends the secret in the
|
|
2639
|
+
Authorization header — it shows in the user's Network tab. Move that
|
|
2640
|
+
call to your server.
|
|
2641
|
+
- An LLM coding agent pasting the key into a comment to "remember" it.
|
|
2642
|
+
Refuse — the key belongs in env.
|
|
2643
|
+
- Logging \`console.log(headers)\` in middleware on a public-facing app.
|
|
2644
|
+
|
|
2645
|
+
## Recommended next step
|
|
2646
|
+
|
|
2647
|
+
Run \`recommend_stack\` with your app_type to get a server-aware framework
|
|
2648
|
+
recommendation. For a secret key that almost always means **Next.js /
|
|
2649
|
+
Remix / SvelteKit / Nuxt**, all of which give you a clear server boundary
|
|
2650
|
+
where this key can live.`;
|
|
2651
|
+
}
|
|
2652
|
+
function STACK_RECOMMENDATION(appType, detected) {
|
|
2653
|
+
const keyLine = detected === "secret"
|
|
2654
|
+
? "🔴 You have a **secret key** (\`sk_live_*\`). It must stay server-side."
|
|
2655
|
+
: detected === "public"
|
|
2656
|
+
? "🟢 You have a **public key** (\`pk_live_*\`). It can ship to clients with allowed-origins set."
|
|
2657
|
+
: "⚪ No key configured yet. Pick one first (`validate_credentials`).";
|
|
2658
|
+
const sections = {
|
|
2659
|
+
fullstack: detected === "secret" ? `## Recommendation: **Next.js (App Router)**
|
|
2660
|
+
|
|
2661
|
+
A fullstack project with a secret key needs a clear server boundary.
|
|
2662
|
+
Next.js App Router is the simplest framework that gives you:
|
|
2663
|
+
|
|
2664
|
+
- **Server Components** + **Server Actions** + **Route Handlers** where
|
|
2665
|
+
the key lives in \`process.env.SAAS_API_KEY\` and never reaches the
|
|
2666
|
+
client.
|
|
2667
|
+
- File-based routing for the user-facing pages.
|
|
2668
|
+
- Built-in support for env-based config without leaking it to the bundle.
|
|
2669
|
+
|
|
2670
|
+
### Project layout
|
|
2671
|
+
|
|
2672
|
+
\`\`\`
|
|
2673
|
+
app/
|
|
2674
|
+
├─ layout.tsx
|
|
2675
|
+
├─ page.tsx (RSC, talks to SaaS via server action)
|
|
2676
|
+
├─ (auth)/
|
|
2677
|
+
│ ├─ login/page.tsx (client form → posts to server action)
|
|
2678
|
+
│ └─ register/page.tsx
|
|
2679
|
+
├─ api/
|
|
2680
|
+
│ ├─ auth/login/route.ts (Route handler, sk_live_* used here)
|
|
2681
|
+
│ ├─ auth/register/route.ts
|
|
2682
|
+
│ └─ proxy/[...path]/route.ts (catch-all server proxy if you need it)
|
|
2683
|
+
└─ lib/saas-client.ts ('server-only', wraps fetch + sk_live)
|
|
2684
|
+
\`\`\`
|
|
2685
|
+
|
|
2686
|
+
### Three rules that keep the key safe
|
|
2687
|
+
|
|
2688
|
+
1. \`SAAS_API_KEY\` env var is **never** prefixed with \`NEXT_PUBLIC_\`.
|
|
2689
|
+
2. The wrapper file imports \`'server-only'\` at the top. If a client
|
|
2690
|
+
component ever imports it, the build fails — that's the contract.
|
|
2691
|
+
3. The browser only ever talks to *your* \`/api/*\` routes, never to
|
|
2692
|
+
\`api.your-saas.com\` directly.
|
|
2693
|
+
|
|
2694
|
+
### Equivalents in other frameworks
|
|
2695
|
+
|
|
2696
|
+
- **Remix** — \`loader\` and \`action\` functions are server-only by
|
|
2697
|
+
design; same shape works.
|
|
2698
|
+
- **SvelteKit** — \`+page.server.ts\` / \`+server.ts\` files.
|
|
2699
|
+
- **Nuxt** — \`server/api/*.ts\` route files, \`useNitroApp\`.
|
|
2700
|
+
- **Astro** — server-rendered routes (\`output: 'server'\`).
|
|
2701
|
+
|
|
2702
|
+
Pick the one your team is most comfortable with. Next.js is the most
|
|
2703
|
+
documented for this exact pattern.` : `## Recommendation: lighter SPA + your own backend
|
|
2704
|
+
|
|
2705
|
+
A fullstack project with a public key is unusual — public keys are
|
|
2706
|
+
restricted to auth endpoints. If you want to read or write data beyond
|
|
2707
|
+
register/login/me, you'll need a secret key on the server.
|
|
2708
|
+
|
|
2709
|
+
For now you have two paths:
|
|
2710
|
+
|
|
2711
|
+
**A. Keep it client-only.** Use Vite + React/Vue/Svelte. Allowed origins
|
|
2712
|
+
on the public key restrict who can call. Limited to auth, but works for
|
|
2713
|
+
the "user signs up, signs in" surface.
|
|
2714
|
+
|
|
2715
|
+
**B. Add a thin server.** Generate a secret key from the dashboard,
|
|
2716
|
+
stash it in a Next.js/Remix server, run \`recommend_stack\` again with
|
|
2717
|
+
the secret key configured. That moves you to the server-side pattern
|
|
2718
|
+
with full API access.`,
|
|
2719
|
+
frontend_only: detected === "public" ? `## Recommendation: **Vite + React** (or Vue / Svelte / SolidJS)
|
|
2720
|
+
|
|
2721
|
+
A frontend-only app with a public key is the textbook case. Use any
|
|
2722
|
+
SPA-style framework that lets you embed the key in the bundle.
|
|
2723
|
+
|
|
2724
|
+
\`\`\`
|
|
2725
|
+
src/
|
|
2726
|
+
├─ App.tsx
|
|
2727
|
+
├─ pages/
|
|
2728
|
+
│ ├─ Login.tsx
|
|
2729
|
+
│ ├─ Register.tsx
|
|
2730
|
+
│ └─ Dashboard.tsx
|
|
2731
|
+
├─ lib/
|
|
2732
|
+
│ └─ api.ts (fetch wrapper that adds X-API-Key)
|
|
2733
|
+
└─ main.tsx
|
|
2734
|
+
|
|
2735
|
+
.env
|
|
2736
|
+
└─ VITE_API_KEY=pk_live_…
|
|
2737
|
+
└─ VITE_API_URL=https://api.your-saas.com
|
|
2738
|
+
\`\`\`
|
|
2739
|
+
|
|
2740
|
+
### Three rules
|
|
2741
|
+
|
|
2742
|
+
1. The key only goes into \`import.meta.env.VITE_API_KEY\` (Vite) or
|
|
2743
|
+
\`process.env.NEXT_PUBLIC_API_KEY\` (Next.js client). Never paste the
|
|
2744
|
+
raw string in a component.
|
|
2745
|
+
2. The deployed origin must be on the key's allowed-origins list, or
|
|
2746
|
+
every request returns 403.
|
|
2747
|
+
3. The end-user JWT (the result of \`/v1/auth/login\`) is what your app
|
|
2748
|
+
holds in memory or HTTP-only cookies — *not* the public key. JWTs
|
|
2749
|
+
are scoped to the user; the public key is shared by everyone.
|
|
2750
|
+
|
|
2751
|
+
If you ever need to call admin-style endpoints (list users, etc.), you
|
|
2752
|
+
need to upgrade to a secret key on a server (option B above).` : `## Recommendation: don't ship the secret key to the client
|
|
2753
|
+
|
|
2754
|
+
Your key is secret. A frontend-only architecture cannot use it without
|
|
2755
|
+
leaking it. Two paths:
|
|
2756
|
+
|
|
2757
|
+
**A. Generate a public key from the dashboard** for the frontend, keep
|
|
2758
|
+
the secret key for any server you might need later. This is the
|
|
2759
|
+
canonical setup. Re-run \`recommend_stack\` with \`app_type=frontend_only\`
|
|
2760
|
+
once you swap.
|
|
2761
|
+
|
|
2762
|
+
**B. Add a small server** (Vercel Functions, Cloudflare Workers, a Node
|
|
2763
|
+
service) and proxy. Then call it "fullstack" and use the Next.js
|
|
2764
|
+
recommendation above.`,
|
|
2765
|
+
backend_only: detected === "secret" ? `## Recommendation: any server runtime, secret in env
|
|
2766
|
+
|
|
2767
|
+
A backend-only consumer (a worker, a webhook handler, a cron) is the
|
|
2768
|
+
simplest case. Any language works:
|
|
2769
|
+
|
|
2770
|
+
- **Node** (\`fastify\`, \`express\`, \`hono\`)
|
|
2771
|
+
- **Python** (\`fastapi\`, \`flask\`)
|
|
2772
|
+
- **Go** (\`net/http\`, \`echo\`)
|
|
2773
|
+
- **Rust** (\`axum\`, \`actix-web\`)
|
|
2774
|
+
|
|
2775
|
+
\`\`\`bash
|
|
2776
|
+
# .env (loaded via dotenv-cli, doppler, or your platform's secret store)
|
|
2777
|
+
SAAS_API_URL=https://api.your-saas.com
|
|
2778
|
+
SAAS_API_KEY=sk_live_…
|
|
2779
|
+
\`\`\`
|
|
2780
|
+
|
|
2781
|
+
\`\`\`ts
|
|
2782
|
+
// minimal Node example
|
|
2783
|
+
const r = await fetch(\`\${process.env.SAAS_API_URL}/v1/auth/login\`, {
|
|
2784
|
+
method: 'POST',
|
|
2785
|
+
headers: {
|
|
2786
|
+
'Content-Type': 'application/json',
|
|
2787
|
+
'X-API-Key': process.env.SAAS_API_KEY!,
|
|
2788
|
+
},
|
|
2789
|
+
body: JSON.stringify({ email, password }),
|
|
2790
|
+
});
|
|
2791
|
+
\`\`\`
|
|
2792
|
+
|
|
2793
|
+
The secret never leaves the host process; rotate via the dashboard if
|
|
2794
|
+
the host or env is ever compromised.` : `A backend with a public key works only for auth endpoints. If your
|
|
2795
|
+
backend needs admin operations (list users, billing on behalf of a
|
|
2796
|
+
tenant), generate a secret key from the dashboard and re-run this tool.`,
|
|
2797
|
+
mobile: detected === "public" ? `## Recommendation: native or React Native + public key in app config
|
|
2798
|
+
|
|
2799
|
+
Mobile apps with a public key behave like SPAs. Bundle the key, set
|
|
2800
|
+
allowed-origins to whatever your auth callback host is.
|
|
2801
|
+
|
|
2802
|
+
- **React Native / Expo** — \`react-native-config\` reads
|
|
2803
|
+
\`SAAS_API_KEY\` from \`.env\` and exposes it to JS.
|
|
2804
|
+
- **Flutter** — \`flutter_dotenv\` or \`--dart-define\` flags.
|
|
2805
|
+
- **Swift / Kotlin** — \`xcconfig\` / \`local.properties\` + \`BuildConfig\`.
|
|
2806
|
+
|
|
2807
|
+
Even though the key is in the binary, public keys are *designed* for
|
|
2808
|
+
this. The risk is identical to a website's public key.` : `## Recommendation: don't ship a secret key in a mobile binary
|
|
2809
|
+
|
|
2810
|
+
Mobile apps cannot keep a secret. Reverse-engineering the binary is
|
|
2811
|
+
trivial. Two paths:
|
|
2812
|
+
|
|
2813
|
+
**A. Switch to a public key** for the mobile client and keep the
|
|
2814
|
+
secret key on a server you control. Your server proxies the calls
|
|
2815
|
+
that need elevated permissions.
|
|
2816
|
+
|
|
2817
|
+
**B. Architecture A is non-negotiable for production.** The only way
|
|
2818
|
+
to use a secret key safely from a mobile app is *not to use it from
|
|
2819
|
+
the mobile app*.`,
|
|
2820
|
+
cli: detected === "secret" ? `## Recommendation: any language, secret in user's local env
|
|
2821
|
+
|
|
2822
|
+
A CLI is essentially a backend running on the user's machine. Standard
|
|
2823
|
+
pattern:
|
|
2824
|
+
|
|
2825
|
+
- Read \`SAAS_API_KEY\` from \`process.env\` (Node), \`os.environ\`
|
|
2826
|
+
(Python), etc.
|
|
2827
|
+
- Print a clear error if missing: \`"Set SAAS_API_KEY (get one from
|
|
2828
|
+
https://app.your-saas.com/api-keys)"\`.
|
|
2829
|
+
- For multi-tenant CLIs, store the key in OS keychain (\`keytar\` on
|
|
2830
|
+
Node, \`keyring\` on Python) instead of plain \`.env\`.
|
|
2831
|
+
|
|
2832
|
+
Frameworks: \`commander\` / \`oclif\` (Node), \`typer\` / \`click\`
|
|
2833
|
+
(Python), \`cobra\` (Go).` : `A CLI with a public key works only for auth flows. For most useful CLI
|
|
2834
|
+
behaviors (managing data, running admin commands) you want a secret key.`,
|
|
2835
|
+
unsure: `## Pick the audience first, the stack second
|
|
2836
|
+
|
|
2837
|
+
Tell me one of:
|
|
2838
|
+
|
|
2839
|
+
- **fullstack** — one app that needs both UI and server logic
|
|
2840
|
+
- **frontend_only** — only the browser, no server you own
|
|
2841
|
+
- **backend_only** — server, worker, cron, webhook
|
|
2842
|
+
- **mobile** — iOS / Android app
|
|
2843
|
+
- **cli** — a command-line tool
|
|
2844
|
+
|
|
2845
|
+
…then call \`recommend_stack\` again with \`app_type=<picked>\`. Each path
|
|
2846
|
+
has a different shape because of where the API key can safely live.`,
|
|
2847
|
+
};
|
|
2848
|
+
const body = sections[appType] ?? sections["unsure"];
|
|
2849
|
+
return `# Stack recommendation\n\n${keyLine}\n\n${body}\n\n---\n\nRun \`get_security_guide\` for the full security cheat-sheet that matches your key type.`;
|
|
2850
|
+
}
|
|
2398
2851
|
// =============================================================================
|
|
2399
2852
|
// MCP Server Implementation
|
|
2400
2853
|
// =============================================================================
|
|
2401
2854
|
const server = new Server({
|
|
2402
2855
|
name: "saas-multi-agent-api",
|
|
2403
|
-
version:
|
|
2856
|
+
version: SERVER_VERSION,
|
|
2404
2857
|
}, {
|
|
2405
2858
|
capabilities: {
|
|
2406
2859
|
tools: {},
|
|
@@ -2412,20 +2865,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
2412
2865
|
tools: [
|
|
2413
2866
|
{
|
|
2414
2867
|
name: "get_api_overview",
|
|
2415
|
-
description: `Get a high-level overview of the Multi-tenant SaaS API.
|
|
2416
|
-
|
|
2417
|
-
IMPORTANT: This API has TWO types of endpoints:
|
|
2418
|
-
1. TENANT/DASHBOARD endpoints - For tenant admins (NOT for end-user frontends)
|
|
2419
|
-
2. END-USER endpoints - For building frontend applications
|
|
2420
|
-
|
|
2421
|
-
KEY FEATURES:
|
|
2422
|
-
- User authentication (login, register, password reset)
|
|
2423
|
-
- Organizations and team management
|
|
2424
|
-
- Subscriptions and billing (Stripe)
|
|
2425
|
-
- AI Agents: Chat with AI agents created by the tenant
|
|
2426
|
-
- External Agents: Track usage from external agent integrations (e.g., N8N)
|
|
2427
|
-
- Usage tracking
|
|
2428
|
-
|
|
2868
|
+
description: `Get a high-level overview of the Multi-tenant SaaS API.
|
|
2869
|
+
|
|
2870
|
+
IMPORTANT: This API has TWO types of endpoints:
|
|
2871
|
+
1. TENANT/DASHBOARD endpoints - For tenant admins (NOT for end-user frontends)
|
|
2872
|
+
2. END-USER endpoints - For building frontend applications
|
|
2873
|
+
|
|
2874
|
+
KEY FEATURES:
|
|
2875
|
+
- User authentication (login, register, password reset)
|
|
2876
|
+
- Organizations and team management
|
|
2877
|
+
- Subscriptions and billing (Stripe)
|
|
2878
|
+
- AI Agents: Chat with AI agents created by the tenant
|
|
2879
|
+
- External Agents: Track usage from external agent integrations (e.g., N8N)
|
|
2880
|
+
- Usage tracking
|
|
2881
|
+
|
|
2429
2882
|
Call this first to understand the API architecture and authentication model.`,
|
|
2430
2883
|
inputSchema: {
|
|
2431
2884
|
type: "object",
|
|
@@ -2435,12 +2888,12 @@ Call this first to understand the API architecture and authentication model.`,
|
|
|
2435
2888
|
},
|
|
2436
2889
|
{
|
|
2437
2890
|
name: "get_end_user_endpoints",
|
|
2438
|
-
description: `Get detailed documentation for END-USER API endpoints.
|
|
2439
|
-
|
|
2440
|
-
Use this when building a frontend for end-users. These endpoints use:
|
|
2441
|
-
- API Key (X-API-Key header) - required for ALL requests
|
|
2442
|
-
- JWT token (Authorization: Bearer) - required AFTER login
|
|
2443
|
-
|
|
2891
|
+
description: `Get detailed documentation for END-USER API endpoints.
|
|
2892
|
+
|
|
2893
|
+
Use this when building a frontend for end-users. These endpoints use:
|
|
2894
|
+
- API Key (X-API-Key header) - required for ALL requests
|
|
2895
|
+
- JWT token (Authorization: Bearer) - required AFTER login
|
|
2896
|
+
|
|
2444
2897
|
Categories: authentication, organizations, subscriptions, billing, usage, agents, users, plans, ai, entities, departments, external_agents`,
|
|
2445
2898
|
inputSchema: {
|
|
2446
2899
|
type: "object",
|
|
@@ -2465,15 +2918,15 @@ Categories: authentication, organizations, subscriptions, billing, usage, agents
|
|
|
2465
2918
|
},
|
|
2466
2919
|
{
|
|
2467
2920
|
name: "get_sdk_template",
|
|
2468
|
-
description: `Get a complete TypeScript/JavaScript SDK template for the API.
|
|
2469
|
-
|
|
2470
|
-
This template includes:
|
|
2471
|
-
- Typed interfaces for all responses
|
|
2472
|
-
- Auth methods (login, register, refresh, logout)
|
|
2473
|
-
- Organization methods
|
|
2474
|
-
- Subscription methods
|
|
2475
|
-
- Usage methods
|
|
2476
|
-
|
|
2921
|
+
description: `Get a complete TypeScript/JavaScript SDK template for the API.
|
|
2922
|
+
|
|
2923
|
+
This template includes:
|
|
2924
|
+
- Typed interfaces for all responses
|
|
2925
|
+
- Auth methods (login, register, refresh, logout)
|
|
2926
|
+
- Organization methods
|
|
2927
|
+
- Subscription methods
|
|
2928
|
+
- Usage methods
|
|
2929
|
+
|
|
2477
2930
|
You can use this as a starting point for your frontend.`,
|
|
2478
2931
|
inputSchema: {
|
|
2479
2932
|
type: "object",
|
|
@@ -2525,15 +2978,15 @@ You can use this as a starting point for your frontend.`,
|
|
|
2525
2978
|
},
|
|
2526
2979
|
{
|
|
2527
2980
|
name: "get_authentication_flow",
|
|
2528
|
-
description: `Get a step-by-step guide for implementing authentication in your frontend.
|
|
2529
|
-
|
|
2530
|
-
Covers:
|
|
2531
|
-
- Registration flow (with conditional email verification)
|
|
2532
|
-
- Login flow
|
|
2533
|
-
- Token refresh
|
|
2534
|
-
- Password reset
|
|
2535
|
-
- Email verification flow
|
|
2536
|
-
- Google OAuth flow
|
|
2981
|
+
description: `Get a step-by-step guide for implementing authentication in your frontend.
|
|
2982
|
+
|
|
2983
|
+
Covers:
|
|
2984
|
+
- Registration flow (with conditional email verification)
|
|
2985
|
+
- Login flow
|
|
2986
|
+
- Token refresh
|
|
2987
|
+
- Password reset
|
|
2988
|
+
- Email verification flow
|
|
2989
|
+
- Google OAuth flow
|
|
2537
2990
|
- Logout`,
|
|
2538
2991
|
inputSchema: {
|
|
2539
2992
|
type: "object",
|
|
@@ -2543,14 +2996,14 @@ Covers:
|
|
|
2543
2996
|
},
|
|
2544
2997
|
{
|
|
2545
2998
|
name: "get_common_patterns",
|
|
2546
|
-
description: `Get common implementation patterns for frontend development.
|
|
2547
|
-
|
|
2548
|
-
Includes:
|
|
2549
|
-
- Token storage best practices
|
|
2550
|
-
- Auto token refresh
|
|
2551
|
-
- Error handling
|
|
2552
|
-
- API request wrapper
|
|
2553
|
-
- OAuth callback handling
|
|
2999
|
+
description: `Get common implementation patterns for frontend development.
|
|
3000
|
+
|
|
3001
|
+
Includes:
|
|
3002
|
+
- Token storage best practices
|
|
3003
|
+
- Auto token refresh
|
|
3004
|
+
- Error handling
|
|
3005
|
+
- API request wrapper
|
|
3006
|
+
- OAuth callback handling
|
|
2554
3007
|
- Environment variables`,
|
|
2555
3008
|
inputSchema: {
|
|
2556
3009
|
type: "object",
|
|
@@ -2558,6 +3011,66 @@ Includes:
|
|
|
2558
3011
|
required: [],
|
|
2559
3012
|
},
|
|
2560
3013
|
},
|
|
3014
|
+
{
|
|
3015
|
+
name: "validate_credentials",
|
|
3016
|
+
description: `Verify the SAAS_API_KEY currently configured by hitting the live API.
|
|
3017
|
+
Returns the detected key type (public pk_live_* or secret sk_live_*),
|
|
3018
|
+
whether the key authenticates correctly, the API URL it points at, and
|
|
3019
|
+
the kind of operations the key is allowed to perform. Run this first
|
|
3020
|
+
when starting a new project so the agent knows what's safe to build.`,
|
|
3021
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
3022
|
+
},
|
|
3023
|
+
{
|
|
3024
|
+
name: "list_end_users",
|
|
3025
|
+
description: `List the tenant's end-users via GET /v1/auth/users. Requires a
|
|
3026
|
+
secret API key (sk_live_*). Useful when scaffolding admin views or
|
|
3027
|
+
debugging a signup flow. Returns an array of {id, email, display_name,
|
|
3028
|
+
is_active, created_at}.`,
|
|
3029
|
+
inputSchema: {
|
|
3030
|
+
type: "object",
|
|
3031
|
+
properties: {
|
|
3032
|
+
skip: { type: "number", description: "Pagination offset (default 0)" },
|
|
3033
|
+
limit: { type: "number", description: "Page size (default 20, max 100)" },
|
|
3034
|
+
},
|
|
3035
|
+
required: [],
|
|
3036
|
+
},
|
|
3037
|
+
},
|
|
3038
|
+
{
|
|
3039
|
+
name: "list_oauth_providers",
|
|
3040
|
+
description: `List the OAuth providers (Google, GitHub, etc.) the tenant has
|
|
3041
|
+
configured for end-user social login. Useful when wiring up a "Sign in
|
|
3042
|
+
with Google" button — if the provider isn't in the list, the button
|
|
3043
|
+
won't work. Calls GET /v1/auth/providers.`,
|
|
3044
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
3045
|
+
},
|
|
3046
|
+
{
|
|
3047
|
+
name: "get_security_guide",
|
|
3048
|
+
description: `Get key-type-specific security best practices. Reads SAAS_API_KEY
|
|
3049
|
+
to detect public (pk_live_*) vs secret (sk_live_*) and returns the
|
|
3050
|
+
matching guide: where it's safe to put the key, common leak vectors,
|
|
3051
|
+
allowed-origins setup, and what NOT to do. Always consult this before
|
|
3052
|
+
generating production code.`,
|
|
3053
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
3054
|
+
},
|
|
3055
|
+
{
|
|
3056
|
+
name: "recommend_stack",
|
|
3057
|
+
description: `Recommend a stack and high-level architecture given the configured
|
|
3058
|
+
API key type and the kind of app the developer wants to build. Picks
|
|
3059
|
+
between Next.js / Remix / SvelteKit (server-side) for secret keys vs
|
|
3060
|
+
React/Vue SPA for public keys, and explains why. Use this whenever the
|
|
3061
|
+
developer says "I want to build X" before writing code.`,
|
|
3062
|
+
inputSchema: {
|
|
3063
|
+
type: "object",
|
|
3064
|
+
properties: {
|
|
3065
|
+
app_type: {
|
|
3066
|
+
type: "string",
|
|
3067
|
+
enum: ["fullstack", "frontend_only", "backend_only", "mobile", "cli", "unsure"],
|
|
3068
|
+
description: "What kind of app are you building?",
|
|
3069
|
+
},
|
|
3070
|
+
},
|
|
3071
|
+
required: ["app_type"],
|
|
3072
|
+
},
|
|
3073
|
+
},
|
|
2561
3074
|
],
|
|
2562
3075
|
};
|
|
2563
3076
|
});
|
|
@@ -2571,30 +3084,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2571
3084
|
content: [
|
|
2572
3085
|
{
|
|
2573
3086
|
type: "text",
|
|
2574
|
-
text: `# Multi-tenant SaaS API Overview
|
|
2575
|
-
|
|
2576
|
-
**API URL:** ${API_URL}
|
|
2577
|
-
**Version:**
|
|
2578
|
-
|
|
2579
|
-
${API_OVERVIEW.description}
|
|
2580
|
-
|
|
2581
|
-
## Quick Links
|
|
2582
|
-
- Swagger UI: ${API_URL}/docs
|
|
2583
|
-
- ReDoc: ${API_URL}/redoc
|
|
2584
|
-
- OpenAPI JSON: ${API_URL}/v1/developer/openapi.json
|
|
2585
|
-
|
|
2586
|
-
##
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
3087
|
+
text: `# Multi-tenant SaaS API Overview
|
|
3088
|
+
|
|
3089
|
+
**API URL:** ${API_URL}
|
|
3090
|
+
**Version:** ${SERVER_VERSION}
|
|
3091
|
+
|
|
3092
|
+
${API_OVERVIEW.description}
|
|
3093
|
+
|
|
3094
|
+
## Quick Links
|
|
3095
|
+
- Swagger UI: ${API_URL}/docs
|
|
3096
|
+
- ReDoc: ${API_URL}/redoc
|
|
3097
|
+
- OpenAPI JSON: ${API_URL}/v1/developer/openapi.json
|
|
3098
|
+
|
|
3099
|
+
## Recommended first calls (vibecoding flow)
|
|
3100
|
+
|
|
3101
|
+
Before generating any code for the user, run these in order:
|
|
3102
|
+
|
|
3103
|
+
1. **\`validate_credentials\`** — confirms the configured API key works and tells you whether it's a public (\`pk_live_*\`) or secret (\`sk_live_*\`) key. The rest of your decisions depend on the answer.
|
|
3104
|
+
2. **\`get_security_guide\`** — returns the safe-usage rules for the *detected* key type (where it can run, what it can call, leak vectors).
|
|
3105
|
+
3. **\`recommend_stack\`** — given the kind of app the user wants (frontend_only, fullstack, backend_service, etc.), returns the framework + project layout that won't leak the key.
|
|
3106
|
+
|
|
3107
|
+
Skipping these is how secret keys end up shipped to a browser bundle. Don't skip them.
|
|
3108
|
+
|
|
3109
|
+
## Getting Started (manual)
|
|
3110
|
+
|
|
3111
|
+
1. Get your API key from the tenant dashboard
|
|
3112
|
+
2. Use \`pk_live_*\` key for frontend-only apps; use \`sk_live_*\` only on a server (Next.js route handler, backend, etc.)
|
|
3113
|
+
3. Include \`X-API-Key\` header in ALL requests
|
|
3114
|
+
4. After login, include \`Authorization: Bearer <token>\` for authenticated requests
|
|
3115
|
+
|
|
3116
|
+
## Important Notes
|
|
3117
|
+
|
|
3118
|
+
⚠️ **When building a frontend for end-users:**
|
|
3119
|
+
- ONLY use endpoints from the "End-user" category
|
|
3120
|
+
- DO NOT use /v1/tenant-auth/* or /v1/dashboard/* endpoints
|
|
2598
3121
|
- Those are for tenant admin dashboards, not end-user apps`,
|
|
2599
3122
|
},
|
|
2600
3123
|
],
|
|
@@ -2610,115 +3133,115 @@ ${API_OVERVIEW.description}
|
|
|
2610
3133
|
content: [
|
|
2611
3134
|
{
|
|
2612
3135
|
type: "text",
|
|
2613
|
-
text: `# End-User API Endpoints${category ? ` - ${category}` : ''}
|
|
2614
|
-
|
|
2615
|
-
These endpoints are for building frontend applications for end-users.
|
|
2616
|
-
|
|
2617
|
-
${JSON.stringify(endpoints, null, 2)}
|
|
2618
|
-
|
|
2619
|
-
## Quick Reference
|
|
2620
|
-
|
|
2621
|
-
### Authentication (No JWT required)
|
|
2622
|
-
- POST /v1/auth/register - Register new user
|
|
2623
|
-
- POST /v1/auth/login - Login user
|
|
2624
|
-
- POST /v1/auth/refresh - Refresh access token
|
|
2625
|
-
- POST /v1/auth/forgot-password - Request password reset
|
|
2626
|
-
- POST /v1/auth/reset-password - Reset password
|
|
2627
|
-
- POST /v1/auth/verify-email - Verify email (with API Key)
|
|
2628
|
-
- POST /v1/auth/verify-email-with-token - Verify email with token only (no API Key)
|
|
2629
|
-
- POST /v1/auth/resend-verification - Resend verification email
|
|
2630
|
-
- GET /v1/auth/providers - List enabled OAuth providers (e.g. Google)
|
|
2631
|
-
- GET /v1/auth/google/url?redirect_uri=... - Get Google OAuth URL
|
|
2632
|
-
- GET /v1/auth/google/callback - OAuth callback (redirects with tokens in URL fragment #)
|
|
2633
|
-
|
|
2634
|
-
### Authenticated Endpoints (JWT required)
|
|
2635
|
-
- GET /v1/auth/me - Get current user
|
|
2636
|
-
- POST /v1/auth/logout - Logout
|
|
2637
|
-
- POST /v1/auth/change-password - Change password (authenticated)
|
|
2638
|
-
- GET /v1/organizations/my-organizations - Get user's organizations
|
|
2639
|
-
- POST /v1/organizations - Create organization
|
|
2640
|
-
- GET /v1/organizations/{id}/departments - List departments
|
|
2641
|
-
- GET /v1/organizations/{id}/members - List members
|
|
2642
|
-
- PUT /v1/organizations/{id}/members/{user_id} - Update member role
|
|
2643
|
-
- DELETE /v1/organizations/{id}/members/{user_id} - Remove member
|
|
2644
|
-
- POST /v1/organizations/{id}/invite - Invite member
|
|
2645
|
-
- POST /v1/organizations/accept-invitation - Accept invitation
|
|
2646
|
-
- POST /v1/organizations/reject-invitation - Reject invitation
|
|
2647
|
-
- GET /v1/organizations/invitations/pending?email= - Check pending invitations
|
|
2648
|
-
- GET /v1/organizations/invitations/{token} - Get invitation details
|
|
2649
|
-
- POST /v1/organizations/{id}/set-default - Set default org
|
|
2650
|
-
- GET /v1/organizations/{id}/subscription - Get org subscription
|
|
2651
|
-
- POST /v1/organizations/{id}/subscription/{plan_id} - Subscribe to plan
|
|
2652
|
-
- DELETE /v1/organizations/{id}/subscription - Cancel subscription
|
|
2653
|
-
- GET /v1/organizations/{id}/usage - Get org usage
|
|
2654
|
-
|
|
2655
|
-
### Plans (JWT required)
|
|
2656
|
-
- GET /v1/plans - List all plans (paginated)
|
|
2657
|
-
- GET /v1/plans/active - List active plans
|
|
2658
|
-
- GET /v1/plans/free - List free plans
|
|
2659
|
-
- GET /v1/plans/{plan_id} - Get plan details
|
|
2660
|
-
|
|
2661
|
-
### Subscriptions (JWT required)
|
|
2662
|
-
- GET /v1/subscriptions/plans - List plans
|
|
2663
|
-
- GET /v1/subscriptions/plans/{plan_id} - Get plan by ID
|
|
2664
|
-
- GET /v1/subscriptions/current - Get current subscription
|
|
2665
|
-
- POST /v1/subscriptions/subscriptions - Create subscription
|
|
2666
|
-
- GET /v1/subscriptions/subscriptions/{id} - Get subscription by ID
|
|
2667
|
-
- PUT /v1/subscriptions/subscriptions/{id} - Update subscription
|
|
2668
|
-
- DELETE /v1/subscriptions/subscriptions/{id} - Cancel subscription
|
|
2669
|
-
- POST /v1/subscriptions/checkout - Start checkout
|
|
2670
|
-
- GET /v1/subscriptions/users/{user_id}/usage - Get user plan usage
|
|
2671
|
-
- GET /v1/subscriptions/users/{user_id}/usage/limits - Check user limits
|
|
2672
|
-
- PUT /v1/subscriptions/users/{user_id}/plan/{plan_id} - Assign plan
|
|
2673
|
-
- POST /v1/subscriptions/organizations/{org_id}/resume - Resume org subscription (sk_live_*)
|
|
2674
|
-
- GET /v1/subscriptions/organizations/{org_id}/invoices - Get org invoices (sk_live_*)
|
|
2675
|
-
- DELETE /v1/subscriptions/organizations/{org_id}/subscription - Cancel org subscription (sk_live_*)
|
|
2676
|
-
|
|
2677
|
-
### Billing (sk_live_* ONLY)
|
|
2678
|
-
- GET /v1/billing/available-plans - List available plans
|
|
2679
|
-
- POST /v1/billing/create-checkout-session - Create checkout session
|
|
2680
|
-
- POST /v1/billing/change-plan - Change plan (upgrade/downgrade)
|
|
2681
|
-
- POST /v1/billing/cancel-subscription - Cancel subscription
|
|
2682
|
-
- GET /v1/billing/billing-history-org/{id} - Billing history
|
|
2683
|
-
|
|
2684
|
-
### Users
|
|
2685
|
-
- POST /v1/users - Create user (sk_live_* + JWT)
|
|
2686
|
-
- GET /v1/users - List users (sk_live_* only, paginated)
|
|
2687
|
-
- GET /v1/users/stats - User stats (sk_live_* only)
|
|
2688
|
-
- GET /v1/users/{id} - Get user (self or sk_live_*)
|
|
2689
|
-
- PUT /v1/users/{id} - Update user
|
|
2690
|
-
- PUT /v1/users/{id}/settings - Update settings
|
|
2691
|
-
- POST /v1/users/{id}/deactivate - Deactivate user (soft delete)
|
|
2692
|
-
- POST /v1/users/{id}/activate - Activate user (sk_live_* only)
|
|
2693
|
-
- DELETE /v1/users/{id} - Delete user (sk_live_* only, soft delete)
|
|
2694
|
-
|
|
2695
|
-
### Usage (JWT required)
|
|
2696
|
-
- GET /v1/api/usage/current - Current usage stats
|
|
2697
|
-
- GET /v1/api/usage/history - Usage history (limit/offset pagination)
|
|
2698
|
-
- GET /v1/api/usage/entity-usage - Entity usage stats
|
|
2699
|
-
- GET /v1/usage/stats - Comprehensive usage statistics
|
|
2700
|
-
- POST /v1/usage/check - Check if usage allowed (without consuming)
|
|
2701
|
-
- POST /v1/usage/consume - Consume usage tokens/messages
|
|
2702
|
-
|
|
2703
|
-
### AI Agents (JWT required)
|
|
2704
|
-
- GET /v1/user/agents/available - List available agents
|
|
2705
|
-
- GET /v1/user/agents/{agent_id} - Get agent details
|
|
2706
|
-
- POST /v1/user/agents/{agent_id}/chat - Chat with an agent
|
|
2707
|
-
- GET /v1/user/agents/{agent_id}/history - Get conversation history
|
|
2708
|
-
|
|
2709
|
-
### Custom Entities (API Key + JWT + X-Organization-Id)
|
|
2710
|
-
- GET /v1/entity/schemas - List schemas
|
|
2711
|
-
- POST /v1/entity/schemas - Create schema
|
|
2712
|
-
- GET /v1/entity/schemas/{id} - Get schema
|
|
2713
|
-
- PUT /v1/entity/schemas/{id} - Update schema
|
|
2714
|
-
- DELETE /v1/entity/schemas/{id} - Delete schema
|
|
2715
|
-
- POST /v1/entity/records - Create record
|
|
2716
|
-
- GET /v1/entity/records/{id} - Get record
|
|
2717
|
-
- PUT /v1/entity/records/{id} - Update record
|
|
2718
|
-
- DELETE /v1/entity/records/{id} - Delete record
|
|
2719
|
-
- POST /v1/entity/records/search - Search records with filters
|
|
2720
|
-
|
|
2721
|
-
### Departments
|
|
3136
|
+
text: `# End-User API Endpoints${category ? ` - ${category}` : ''}
|
|
3137
|
+
|
|
3138
|
+
These endpoints are for building frontend applications for end-users.
|
|
3139
|
+
|
|
3140
|
+
${JSON.stringify(endpoints, null, 2)}
|
|
3141
|
+
|
|
3142
|
+
## Quick Reference
|
|
3143
|
+
|
|
3144
|
+
### Authentication (No JWT required)
|
|
3145
|
+
- POST /v1/auth/register - Register new user
|
|
3146
|
+
- POST /v1/auth/login - Login user
|
|
3147
|
+
- POST /v1/auth/refresh - Refresh access token
|
|
3148
|
+
- POST /v1/auth/forgot-password - Request password reset
|
|
3149
|
+
- POST /v1/auth/reset-password - Reset password
|
|
3150
|
+
- POST /v1/auth/verify-email - Verify email (with API Key)
|
|
3151
|
+
- POST /v1/auth/verify-email-with-token - Verify email with token only (no API Key)
|
|
3152
|
+
- POST /v1/auth/resend-verification - Resend verification email
|
|
3153
|
+
- GET /v1/auth/providers - List enabled OAuth providers (e.g. Google)
|
|
3154
|
+
- GET /v1/auth/google/url?redirect_uri=... - Get Google OAuth URL
|
|
3155
|
+
- GET /v1/auth/google/callback - OAuth callback (redirects with tokens in URL fragment #)
|
|
3156
|
+
|
|
3157
|
+
### Authenticated Endpoints (JWT required)
|
|
3158
|
+
- GET /v1/auth/me - Get current user
|
|
3159
|
+
- POST /v1/auth/logout - Logout
|
|
3160
|
+
- POST /v1/auth/change-password - Change password (authenticated)
|
|
3161
|
+
- GET /v1/organizations/my-organizations - Get user's organizations
|
|
3162
|
+
- POST /v1/organizations - Create organization
|
|
3163
|
+
- GET /v1/organizations/{id}/departments - List departments
|
|
3164
|
+
- GET /v1/organizations/{id}/members - List members
|
|
3165
|
+
- PUT /v1/organizations/{id}/members/{user_id} - Update member role
|
|
3166
|
+
- DELETE /v1/organizations/{id}/members/{user_id} - Remove member
|
|
3167
|
+
- POST /v1/organizations/{id}/invite - Invite member
|
|
3168
|
+
- POST /v1/organizations/accept-invitation - Accept invitation
|
|
3169
|
+
- POST /v1/organizations/reject-invitation - Reject invitation
|
|
3170
|
+
- GET /v1/organizations/invitations/pending?email= - Check pending invitations
|
|
3171
|
+
- GET /v1/organizations/invitations/{token} - Get invitation details
|
|
3172
|
+
- POST /v1/organizations/{id}/set-default - Set default org
|
|
3173
|
+
- GET /v1/organizations/{id}/subscription - Get org subscription
|
|
3174
|
+
- POST /v1/organizations/{id}/subscription/{plan_id} - Subscribe to plan
|
|
3175
|
+
- DELETE /v1/organizations/{id}/subscription - Cancel subscription
|
|
3176
|
+
- GET /v1/organizations/{id}/usage - Get org usage
|
|
3177
|
+
|
|
3178
|
+
### Plans (JWT required)
|
|
3179
|
+
- GET /v1/plans - List all plans (paginated)
|
|
3180
|
+
- GET /v1/plans/active - List active plans
|
|
3181
|
+
- GET /v1/plans/free - List free plans
|
|
3182
|
+
- GET /v1/plans/{plan_id} - Get plan details
|
|
3183
|
+
|
|
3184
|
+
### Subscriptions (JWT required)
|
|
3185
|
+
- GET /v1/subscriptions/plans - List plans
|
|
3186
|
+
- GET /v1/subscriptions/plans/{plan_id} - Get plan by ID
|
|
3187
|
+
- GET /v1/subscriptions/current - Get current subscription
|
|
3188
|
+
- POST /v1/subscriptions/subscriptions - Create subscription
|
|
3189
|
+
- GET /v1/subscriptions/subscriptions/{id} - Get subscription by ID
|
|
3190
|
+
- PUT /v1/subscriptions/subscriptions/{id} - Update subscription
|
|
3191
|
+
- DELETE /v1/subscriptions/subscriptions/{id} - Cancel subscription
|
|
3192
|
+
- POST /v1/subscriptions/checkout - Start checkout
|
|
3193
|
+
- GET /v1/subscriptions/users/{user_id}/usage - Get user plan usage
|
|
3194
|
+
- GET /v1/subscriptions/users/{user_id}/usage/limits - Check user limits
|
|
3195
|
+
- PUT /v1/subscriptions/users/{user_id}/plan/{plan_id} - Assign plan
|
|
3196
|
+
- POST /v1/subscriptions/organizations/{org_id}/resume - Resume org subscription (sk_live_*)
|
|
3197
|
+
- GET /v1/subscriptions/organizations/{org_id}/invoices - Get org invoices (sk_live_*)
|
|
3198
|
+
- DELETE /v1/subscriptions/organizations/{org_id}/subscription - Cancel org subscription (sk_live_*)
|
|
3199
|
+
|
|
3200
|
+
### Billing (sk_live_* ONLY)
|
|
3201
|
+
- GET /v1/billing/available-plans - List available plans
|
|
3202
|
+
- POST /v1/billing/create-checkout-session - Create checkout session
|
|
3203
|
+
- POST /v1/billing/change-plan - Change plan (upgrade/downgrade)
|
|
3204
|
+
- POST /v1/billing/cancel-subscription - Cancel subscription
|
|
3205
|
+
- GET /v1/billing/billing-history-org/{id} - Billing history
|
|
3206
|
+
|
|
3207
|
+
### Users
|
|
3208
|
+
- POST /v1/users - Create user (sk_live_* + JWT)
|
|
3209
|
+
- GET /v1/users - List users (sk_live_* only, paginated)
|
|
3210
|
+
- GET /v1/users/stats - User stats (sk_live_* only)
|
|
3211
|
+
- GET /v1/users/{id} - Get user (self or sk_live_*)
|
|
3212
|
+
- PUT /v1/users/{id} - Update user
|
|
3213
|
+
- PUT /v1/users/{id}/settings - Update settings
|
|
3214
|
+
- POST /v1/users/{id}/deactivate - Deactivate user (soft delete)
|
|
3215
|
+
- POST /v1/users/{id}/activate - Activate user (sk_live_* only)
|
|
3216
|
+
- DELETE /v1/users/{id} - Delete user (sk_live_* only, soft delete)
|
|
3217
|
+
|
|
3218
|
+
### Usage (JWT required)
|
|
3219
|
+
- GET /v1/api/usage/current - Current usage stats
|
|
3220
|
+
- GET /v1/api/usage/history - Usage history (limit/offset pagination)
|
|
3221
|
+
- GET /v1/api/usage/entity-usage - Entity usage stats
|
|
3222
|
+
- GET /v1/usage/stats - Comprehensive usage statistics
|
|
3223
|
+
- POST /v1/usage/check - Check if usage allowed (without consuming)
|
|
3224
|
+
- POST /v1/usage/consume - Consume usage tokens/messages
|
|
3225
|
+
|
|
3226
|
+
### AI Agents (JWT required)
|
|
3227
|
+
- GET /v1/user/agents/available - List available agents
|
|
3228
|
+
- GET /v1/user/agents/{agent_id} - Get agent details
|
|
3229
|
+
- POST /v1/user/agents/{agent_id}/chat - Chat with an agent
|
|
3230
|
+
- GET /v1/user/agents/{agent_id}/history - Get conversation history
|
|
3231
|
+
|
|
3232
|
+
### Custom Entities (API Key + JWT + X-Organization-Id)
|
|
3233
|
+
- GET /v1/entity/schemas - List schemas
|
|
3234
|
+
- POST /v1/entity/schemas - Create schema
|
|
3235
|
+
- GET /v1/entity/schemas/{id} - Get schema
|
|
3236
|
+
- PUT /v1/entity/schemas/{id} - Update schema
|
|
3237
|
+
- DELETE /v1/entity/schemas/{id} - Delete schema
|
|
3238
|
+
- POST /v1/entity/records - Create record
|
|
3239
|
+
- GET /v1/entity/records/{id} - Get record
|
|
3240
|
+
- PUT /v1/entity/records/{id} - Update record
|
|
3241
|
+
- DELETE /v1/entity/records/{id} - Delete record
|
|
3242
|
+
- POST /v1/entity/records/search - Search records with filters
|
|
3243
|
+
|
|
3244
|
+
### Departments
|
|
2722
3245
|
- GET /v1/departments/{department_id} - Get department details`,
|
|
2723
3246
|
},
|
|
2724
3247
|
],
|
|
@@ -2739,33 +3262,33 @@ ${JSON.stringify(endpoints, null, 2)}
|
|
|
2739
3262
|
content: [
|
|
2740
3263
|
{
|
|
2741
3264
|
type: "text",
|
|
2742
|
-
text: `# TypeScript/JavaScript SDK Template
|
|
2743
|
-
|
|
2744
|
-
\`\`\`typescript
|
|
2745
|
-
${SDK_TEMPLATE_TYPESCRIPT}
|
|
2746
|
-
\`\`\`
|
|
2747
|
-
|
|
2748
|
-
## Usage Example
|
|
2749
|
-
|
|
2750
|
-
\`\`\`typescript
|
|
2751
|
-
import { ApiClient } from './api-client';
|
|
2752
|
-
|
|
2753
|
-
const api = new ApiClient('pk_live_your_key', 'https://api.yoursaas.com');
|
|
2754
|
-
|
|
2755
|
-
// Register
|
|
2756
|
-
const { user } = await api.auth.register('email@example.com', 'Password123', 'John');
|
|
2757
|
-
|
|
2758
|
-
// Login
|
|
2759
|
-
const { access_token, user } = await api.auth.login('email@example.com', 'Password123');
|
|
2760
|
-
|
|
2761
|
-
// Get organizations
|
|
2762
|
-
const { organizations } = await api.organizations.getMyOrganizations();
|
|
2763
|
-
|
|
2764
|
-
// Create organization
|
|
2765
|
-
const org = await api.organizations.create('My Org', 'my-org');
|
|
2766
|
-
|
|
2767
|
-
// Get subscription plans
|
|
2768
|
-
const plans = await api.subscriptions.getPlans();
|
|
3265
|
+
text: `# TypeScript/JavaScript SDK Template
|
|
3266
|
+
|
|
3267
|
+
\`\`\`typescript
|
|
3268
|
+
${SDK_TEMPLATE_TYPESCRIPT}
|
|
3269
|
+
\`\`\`
|
|
3270
|
+
|
|
3271
|
+
## Usage Example
|
|
3272
|
+
|
|
3273
|
+
\`\`\`typescript
|
|
3274
|
+
import { ApiClient } from './api-client';
|
|
3275
|
+
|
|
3276
|
+
const api = new ApiClient('pk_live_your_key', 'https://api.yoursaas.com');
|
|
3277
|
+
|
|
3278
|
+
// Register
|
|
3279
|
+
const { user } = await api.auth.register('email@example.com', 'Password123', 'John');
|
|
3280
|
+
|
|
3281
|
+
// Login
|
|
3282
|
+
const { access_token, user } = await api.auth.login('email@example.com', 'Password123');
|
|
3283
|
+
|
|
3284
|
+
// Get organizations
|
|
3285
|
+
const { organizations } = await api.organizations.getMyOrganizations();
|
|
3286
|
+
|
|
3287
|
+
// Create organization
|
|
3288
|
+
const org = await api.organizations.create('My Org', 'my-org');
|
|
3289
|
+
|
|
3290
|
+
// Get subscription plans
|
|
3291
|
+
const plans = await api.subscriptions.getPlans();
|
|
2769
3292
|
\`\`\``,
|
|
2770
3293
|
},
|
|
2771
3294
|
],
|
|
@@ -2788,10 +3311,10 @@ const plans = await api.subscriptions.getPlans();
|
|
|
2788
3311
|
content: [
|
|
2789
3312
|
{
|
|
2790
3313
|
type: "text",
|
|
2791
|
-
text: `Error fetching OpenAPI spec: ${error}
|
|
2792
|
-
|
|
2793
|
-
The API might not be accessible. Try accessing ${API_URL}/docs directly.
|
|
2794
|
-
|
|
3314
|
+
text: `Error fetching OpenAPI spec: ${error}
|
|
3315
|
+
|
|
3316
|
+
The API might not be accessible. Try accessing ${API_URL}/docs directly.
|
|
3317
|
+
|
|
2795
3318
|
In the meantime, use the get_end_user_endpoints tool for endpoint documentation.`,
|
|
2796
3319
|
},
|
|
2797
3320
|
],
|
|
@@ -2821,12 +3344,12 @@ In the meantime, use the get_end_user_endpoints tool for endpoint documentation.
|
|
|
2821
3344
|
content: [
|
|
2822
3345
|
{
|
|
2823
3346
|
type: "text",
|
|
2824
|
-
text: `No endpoints found matching "${query}".
|
|
2825
|
-
|
|
2826
|
-
Try searching for:
|
|
2827
|
-
- auth, login, register, password
|
|
2828
|
-
- organization, member, invite
|
|
2829
|
-
- subscription, plan, checkout
|
|
3347
|
+
text: `No endpoints found matching "${query}".
|
|
3348
|
+
|
|
3349
|
+
Try searching for:
|
|
3350
|
+
- auth, login, register, password
|
|
3351
|
+
- organization, member, invite
|
|
3352
|
+
- subscription, plan, checkout
|
|
2830
3353
|
- billing, usage`,
|
|
2831
3354
|
},
|
|
2832
3355
|
],
|
|
@@ -2836,12 +3359,12 @@ Try searching for:
|
|
|
2836
3359
|
content: [
|
|
2837
3360
|
{
|
|
2838
3361
|
type: "text",
|
|
2839
|
-
text: `# Endpoints matching "${query}"
|
|
2840
|
-
|
|
2841
|
-
${results.map(r => `## ${r.method} ${r.path}
|
|
2842
|
-
**Category:** ${r.category}
|
|
2843
|
-
**Summary:** ${r.summary}
|
|
2844
|
-
**Auth:** API Key: ${r.auth.api_key}, JWT: ${r.auth.jwt ? 'Required' : 'Not required'}
|
|
3362
|
+
text: `# Endpoints matching "${query}"
|
|
3363
|
+
|
|
3364
|
+
${results.map(r => `## ${r.method} ${r.path}
|
|
3365
|
+
**Category:** ${r.category}
|
|
3366
|
+
**Summary:** ${r.summary}
|
|
3367
|
+
**Auth:** API Key: ${r.auth.api_key}, JWT: ${r.auth.jwt ? 'Required' : 'Not required'}
|
|
2845
3368
|
`).join('\n')}`,
|
|
2846
3369
|
},
|
|
2847
3370
|
],
|
|
@@ -2849,55 +3372,67 @@ ${results.map(r => `## ${r.method} ${r.path}
|
|
|
2849
3372
|
}
|
|
2850
3373
|
case "get_endpoint_details": {
|
|
2851
3374
|
const path = args.path;
|
|
2852
|
-
const
|
|
2853
|
-
|
|
3375
|
+
const methodArg = args.method;
|
|
3376
|
+
const method = methodArg ? methodArg.toUpperCase() : null;
|
|
3377
|
+
const matches = [];
|
|
2854
3378
|
for (const [category, data] of Object.entries(END_USER_ENDPOINTS)) {
|
|
2855
3379
|
for (const endpoint of data.endpoints || []) {
|
|
2856
|
-
if (endpoint.path === path && endpoint.method === method) {
|
|
2857
|
-
|
|
2858
|
-
content: [
|
|
2859
|
-
{
|
|
2860
|
-
type: "text",
|
|
2861
|
-
text: `# ${method} ${path}
|
|
2862
|
-
|
|
2863
|
-
**Category:** ${category}
|
|
2864
|
-
**Summary:** ${endpoint.summary}
|
|
2865
|
-
|
|
2866
|
-
## Authentication
|
|
2867
|
-
- API Key: ${endpoint.auth.api_key}
|
|
2868
|
-
- JWT Token: ${endpoint.auth.jwt ? 'Required' : 'Not required'}
|
|
2869
|
-
|
|
2870
|
-
${endpoint.request_body ? `## Request Body
|
|
2871
|
-
\`\`\`json
|
|
2872
|
-
${JSON.stringify(endpoint.request_body, null, 2)}
|
|
2873
|
-
\`\`\`` : ''}
|
|
2874
|
-
|
|
2875
|
-
${endpoint.example_request ? `## Example Request
|
|
2876
|
-
\`\`\`json
|
|
2877
|
-
${endpoint.example_request}
|
|
2878
|
-
\`\`\`` : ''}
|
|
2879
|
-
|
|
2880
|
-
${endpoint.response ? `## Response
|
|
2881
|
-
\`\`\`json
|
|
2882
|
-
${JSON.stringify(endpoint.response, null, 2)}
|
|
2883
|
-
\`\`\`` : ''}
|
|
2884
|
-
|
|
2885
|
-
${endpoint.note ? `## Note
|
|
2886
|
-
${endpoint.note}` : ''}`,
|
|
2887
|
-
},
|
|
2888
|
-
],
|
|
2889
|
-
};
|
|
3380
|
+
if (endpoint.path === path && (method === null || endpoint.method === method)) {
|
|
3381
|
+
matches.push({ category, endpoint });
|
|
2890
3382
|
}
|
|
2891
3383
|
}
|
|
2892
3384
|
}
|
|
3385
|
+
const renderEndpoint = (m) => {
|
|
3386
|
+
const ep = m.endpoint;
|
|
3387
|
+
return `# ${ep.method} ${ep.path}
|
|
3388
|
+
|
|
3389
|
+
**Category:** ${m.category}
|
|
3390
|
+
**Summary:** ${ep.summary}
|
|
3391
|
+
|
|
3392
|
+
## Authentication
|
|
3393
|
+
- API Key: ${ep.auth.api_key}
|
|
3394
|
+
- JWT Token: ${ep.auth.jwt ? 'Required' : 'Not required'}
|
|
3395
|
+
|
|
3396
|
+
${ep.request_body ? `## Request Body
|
|
3397
|
+
\`\`\`json
|
|
3398
|
+
${JSON.stringify(ep.request_body, null, 2)}
|
|
3399
|
+
\`\`\`` : ''}
|
|
3400
|
+
|
|
3401
|
+
${ep.example_request ? `## Example Request
|
|
3402
|
+
\`\`\`json
|
|
3403
|
+
${ep.example_request}
|
|
3404
|
+
\`\`\`` : ''}
|
|
3405
|
+
|
|
3406
|
+
${ep.response ? `## Response
|
|
3407
|
+
\`\`\`json
|
|
3408
|
+
${JSON.stringify(ep.response, null, 2)}
|
|
3409
|
+
\`\`\`` : ''}
|
|
3410
|
+
|
|
3411
|
+
${ep.note ? `## Note
|
|
3412
|
+
${ep.note}` : ''}`;
|
|
3413
|
+
};
|
|
3414
|
+
if (matches.length === 1) {
|
|
3415
|
+
return { content: [{ type: "text", text: renderEndpoint(matches[0]) }] };
|
|
3416
|
+
}
|
|
3417
|
+
if (matches.length > 1) {
|
|
3418
|
+
// When several methods share the path, render them all.
|
|
3419
|
+
return {
|
|
3420
|
+
content: [
|
|
3421
|
+
{
|
|
3422
|
+
type: "text",
|
|
3423
|
+
text: matches.map(renderEndpoint).join("\n\n---\n\n"),
|
|
3424
|
+
},
|
|
3425
|
+
],
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
2893
3428
|
return {
|
|
2894
3429
|
content: [
|
|
2895
3430
|
{
|
|
2896
3431
|
type: "text",
|
|
2897
|
-
text: `Endpoint "${method}
|
|
2898
|
-
|
|
2899
|
-
Make sure you're using the correct path format (e.g., /v1/auth/login).
|
|
2900
|
-
|
|
3432
|
+
text: `Endpoint "${method ? method + " " : ""}${path}" not found in end-user endpoints.
|
|
3433
|
+
|
|
3434
|
+
Make sure you're using the correct path format (e.g., /v1/auth/login).
|
|
3435
|
+
|
|
2901
3436
|
Use search_endpoints tool to find available endpoints.`,
|
|
2902
3437
|
},
|
|
2903
3438
|
],
|
|
@@ -2908,232 +3443,232 @@ Use search_endpoints tool to find available endpoints.`,
|
|
|
2908
3443
|
content: [
|
|
2909
3444
|
{
|
|
2910
3445
|
type: "text",
|
|
2911
|
-
text: `# Authentication Flow Guide
|
|
2912
|
-
|
|
2913
|
-
## 1. Registration Flow
|
|
2914
|
-
|
|
2915
|
-
\`\`\`typescript
|
|
2916
|
-
// 1. Register user
|
|
2917
|
-
const response = await fetch('/v1/auth/register', {
|
|
2918
|
-
method: 'POST',
|
|
2919
|
-
headers: {
|
|
2920
|
-
'X-API-Key': 'pk_live_your_key',
|
|
2921
|
-
'Content-Type': 'application/json',
|
|
2922
|
-
},
|
|
2923
|
-
body: JSON.stringify({
|
|
2924
|
-
email: 'user@example.com',
|
|
2925
|
-
password: 'SecurePass123', // min 8 chars, 1 upper, 1 lower, 1 digit
|
|
2926
|
-
display_name: 'John Doe', // optional
|
|
2927
|
-
}),
|
|
2928
|
-
});
|
|
2929
|
-
|
|
2930
|
-
const { access_token, refresh_token, user } = await response.json();
|
|
2931
|
-
|
|
2932
|
-
// 2. Store tokens securely
|
|
2933
|
-
localStorage.setItem('access_token', access_token);
|
|
2934
|
-
localStorage.setItem('refresh_token', refresh_token);
|
|
2935
|
-
|
|
2936
|
-
// User is now logged in
|
|
2937
|
-
\`\`\`
|
|
2938
|
-
|
|
2939
|
-
## 2. Login Flow
|
|
2940
|
-
|
|
2941
|
-
\`\`\`typescript
|
|
2942
|
-
const response = await fetch('/v1/auth/login', {
|
|
2943
|
-
method: 'POST',
|
|
2944
|
-
headers: {
|
|
2945
|
-
'X-API-Key': 'pk_live_your_key',
|
|
2946
|
-
'Content-Type': 'application/json',
|
|
2947
|
-
},
|
|
2948
|
-
body: JSON.stringify({
|
|
2949
|
-
email: 'user@example.com',
|
|
2950
|
-
password: 'SecurePass123',
|
|
2951
|
-
}),
|
|
2952
|
-
});
|
|
2953
|
-
|
|
2954
|
-
if (!response.ok) {
|
|
2955
|
-
const error = await response.json();
|
|
2956
|
-
// Handle error: error.detail contains the message
|
|
2957
|
-
throw new Error(error.detail);
|
|
2958
|
-
}
|
|
2959
|
-
|
|
2960
|
-
const { access_token, refresh_token, user } = await response.json();
|
|
2961
|
-
\`\`\`
|
|
2962
|
-
|
|
2963
|
-
## 3. Token Refresh Flow
|
|
2964
|
-
|
|
2965
|
-
\`\`\`typescript
|
|
2966
|
-
// Call this when access_token expires (expires_in seconds from login)
|
|
2967
|
-
const response = await fetch('/v1/auth/refresh', {
|
|
2968
|
-
method: 'POST',
|
|
2969
|
-
headers: {
|
|
2970
|
-
'X-API-Key': 'pk_live_your_key',
|
|
2971
|
-
'Content-Type': 'application/json',
|
|
2972
|
-
},
|
|
2973
|
-
body: JSON.stringify({
|
|
2974
|
-
refresh_token: localStorage.getItem('refresh_token'),
|
|
2975
|
-
}),
|
|
2976
|
-
});
|
|
2977
|
-
|
|
2978
|
-
const { access_token } = await response.json();
|
|
2979
|
-
localStorage.setItem('access_token', access_token);
|
|
2980
|
-
\`\`\`
|
|
2981
|
-
|
|
2982
|
-
## 4. Password Reset Flow
|
|
2983
|
-
|
|
2984
|
-
\`\`\`typescript
|
|
2985
|
-
// Step 1: Request reset email
|
|
2986
|
-
await fetch('/v1/auth/forgot-password', {
|
|
2987
|
-
method: 'POST',
|
|
2988
|
-
headers: {
|
|
2989
|
-
'X-API-Key': 'pk_live_your_key',
|
|
2990
|
-
'Content-Type': 'application/json',
|
|
2991
|
-
},
|
|
2992
|
-
body: JSON.stringify({ email: 'user@example.com' }),
|
|
2993
|
-
});
|
|
2994
|
-
// Always succeeds (security)
|
|
2995
|
-
|
|
2996
|
-
// Step 2: User clicks link in email with token
|
|
2997
|
-
|
|
2998
|
-
// Step 3: Reset password with token
|
|
2999
|
-
await fetch('/v1/auth/reset-password', {
|
|
3000
|
-
method: 'POST',
|
|
3001
|
-
headers: {
|
|
3002
|
-
'X-API-Key': 'pk_live_your_key',
|
|
3003
|
-
'Content-Type': 'application/json',
|
|
3004
|
-
},
|
|
3005
|
-
body: JSON.stringify({
|
|
3006
|
-
token: 'token-from-email-link',
|
|
3007
|
-
new_password: 'NewSecurePass123',
|
|
3008
|
-
}),
|
|
3009
|
-
});
|
|
3010
|
-
\`\`\`
|
|
3011
|
-
|
|
3012
|
-
## 5. Change Password Flow (Authenticated)
|
|
3013
|
-
|
|
3014
|
-
\`\`\`typescript
|
|
3015
|
-
await fetch('/v1/auth/change-password', {
|
|
3016
|
-
method: 'POST',
|
|
3017
|
-
headers: {
|
|
3018
|
-
'X-API-Key': 'pk_live_your_key',
|
|
3019
|
-
'Authorization': 'Bearer your_jwt_token',
|
|
3020
|
-
'Content-Type': 'application/json',
|
|
3021
|
-
},
|
|
3022
|
-
body: JSON.stringify({
|
|
3023
|
-
current_password: 'OldPass123!',
|
|
3024
|
-
new_password: 'NewPass456!',
|
|
3025
|
-
}),
|
|
3026
|
-
});
|
|
3027
|
-
\`\`\`
|
|
3028
|
-
|
|
3029
|
-
## 6. Email Verification Flow
|
|
3030
|
-
|
|
3031
|
-
\`\`\`typescript
|
|
3032
|
-
// Registration may require email verification depending on tenant config.
|
|
3033
|
-
// Check the register response for verification_required: true
|
|
3034
|
-
|
|
3035
|
-
const registerResult = await response.json();
|
|
3036
|
-
|
|
3037
|
-
if (registerResult.verification_required) {
|
|
3038
|
-
// 1. Show "Check your email" message — NO tokens returned
|
|
3039
|
-
// 2. User clicks link in email → opens /verify-email?token=xxx
|
|
3040
|
-
|
|
3041
|
-
// 3. Verify the email token
|
|
3042
|
-
const verifyResponse = await fetch('/v1/auth/verify-email', {
|
|
3043
|
-
method: 'POST',
|
|
3044
|
-
headers: {
|
|
3045
|
-
'X-API-Key': 'pk_live_your_key',
|
|
3046
|
-
'Content-Type': 'application/json',
|
|
3047
|
-
},
|
|
3048
|
-
body: JSON.stringify({ token: tokenFromUrl }),
|
|
3049
|
-
});
|
|
3050
|
-
// Returns: { message: "Email verified successfully" }
|
|
3051
|
-
|
|
3052
|
-
// 4. If token expired, user can request a new one
|
|
3053
|
-
await fetch('/v1/auth/resend-verification', {
|
|
3054
|
-
method: 'POST',
|
|
3055
|
-
headers: {
|
|
3056
|
-
'X-API-Key': 'pk_live_your_key',
|
|
3057
|
-
'Content-Type': 'application/json',
|
|
3058
|
-
},
|
|
3059
|
-
body: JSON.stringify({ email: 'user@example.com' }),
|
|
3060
|
-
});
|
|
3061
|
-
// Rate limited: 3/hour
|
|
3062
|
-
|
|
3063
|
-
// 5. After verification, user can login normally via /v1/auth/login
|
|
3064
|
-
} else {
|
|
3065
|
-
// No verification required — tokens returned directly from register
|
|
3066
|
-
localStorage.setItem('access_token', registerResult.access_token);
|
|
3067
|
-
localStorage.setItem('refresh_token', registerResult.refresh_token);
|
|
3068
|
-
}
|
|
3069
|
-
\`\`\`
|
|
3070
|
-
|
|
3071
|
-
## 7. Google OAuth Flow
|
|
3072
|
-
|
|
3073
|
-
\`\`\`typescript
|
|
3074
|
-
// 1. Check if Google OAuth is enabled for this tenant
|
|
3075
|
-
const providersRes = await fetch('/v1/auth/providers', {
|
|
3076
|
-
headers: { 'X-API-Key': 'pk_live_your_key' },
|
|
3077
|
-
});
|
|
3078
|
-
const { providers } = await providersRes.json();
|
|
3079
|
-
const googleEnabled = providers.some((p: any) => p.provider === 'google');
|
|
3080
|
-
|
|
3081
|
-
// 2. Get Google authorization URL
|
|
3082
|
-
const redirectUri = window.location.origin + '/oauth/callback';
|
|
3083
|
-
const urlRes = await fetch(
|
|
3084
|
-
\`/v1/auth/google/url?redirect_uri=\${encodeURIComponent(redirectUri)}\`,
|
|
3085
|
-
{ headers: { 'X-API-Key': 'pk_live_your_key' } }
|
|
3086
|
-
);
|
|
3087
|
-
const { authorization_url } = await urlRes.json();
|
|
3088
|
-
|
|
3089
|
-
// 3. Redirect user to Google
|
|
3090
|
-
window.location.href = authorization_url;
|
|
3091
|
-
|
|
3092
|
-
// 4. Google redirects to /v1/auth/google/callback (backend handles this)
|
|
3093
|
-
// 5. Backend redirects to YOUR redirect_uri with tokens in URL FRAGMENT (#)
|
|
3094
|
-
|
|
3095
|
-
// 6. In your /oauth/callback page, read tokens from hash (NOT query params)
|
|
3096
|
-
const hash = window.location.hash.substring(1);
|
|
3097
|
-
const params = new URLSearchParams(hash);
|
|
3098
|
-
const accessToken = params.get('access_token');
|
|
3099
|
-
const refreshToken = params.get('refresh_token');
|
|
3100
|
-
const expiresIn = params.get('expires_in');
|
|
3101
|
-
|
|
3102
|
-
// Also check query params for errors
|
|
3103
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
3104
|
-
const error = urlParams.get('error');
|
|
3105
|
-
if (error) {
|
|
3106
|
-
console.error('OAuth failed:', urlParams.get('error_description'));
|
|
3107
|
-
}
|
|
3108
|
-
\`\`\`
|
|
3109
|
-
|
|
3110
|
-
## 8. Authenticated Requests
|
|
3111
|
-
|
|
3112
|
-
\`\`\`typescript
|
|
3113
|
-
// After login, include Authorization header
|
|
3114
|
-
const response = await fetch('/v1/auth/me', {
|
|
3115
|
-
headers: {
|
|
3116
|
-
'X-API-Key': 'pk_live_your_key',
|
|
3117
|
-
'Authorization': \`Bearer \${localStorage.getItem('access_token')}\`,
|
|
3118
|
-
},
|
|
3119
|
-
});
|
|
3120
|
-
|
|
3121
|
-
const user = await response.json();
|
|
3122
|
-
\`\`\`
|
|
3123
|
-
|
|
3124
|
-
## 9. Logout
|
|
3125
|
-
|
|
3126
|
-
\`\`\`typescript
|
|
3127
|
-
await fetch('/v1/auth/logout', {
|
|
3128
|
-
method: 'POST',
|
|
3129
|
-
headers: {
|
|
3130
|
-
'X-API-Key': 'pk_live_your_key',
|
|
3131
|
-
'Authorization': \`Bearer \${localStorage.getItem('access_token')}\`,
|
|
3132
|
-
},
|
|
3133
|
-
});
|
|
3134
|
-
|
|
3135
|
-
localStorage.removeItem('access_token');
|
|
3136
|
-
localStorage.removeItem('refresh_token');
|
|
3446
|
+
text: `# Authentication Flow Guide
|
|
3447
|
+
|
|
3448
|
+
## 1. Registration Flow
|
|
3449
|
+
|
|
3450
|
+
\`\`\`typescript
|
|
3451
|
+
// 1. Register user
|
|
3452
|
+
const response = await fetch('/v1/auth/register', {
|
|
3453
|
+
method: 'POST',
|
|
3454
|
+
headers: {
|
|
3455
|
+
'X-API-Key': 'pk_live_your_key',
|
|
3456
|
+
'Content-Type': 'application/json',
|
|
3457
|
+
},
|
|
3458
|
+
body: JSON.stringify({
|
|
3459
|
+
email: 'user@example.com',
|
|
3460
|
+
password: 'SecurePass123', // min 8 chars, 1 upper, 1 lower, 1 digit
|
|
3461
|
+
display_name: 'John Doe', // optional
|
|
3462
|
+
}),
|
|
3463
|
+
});
|
|
3464
|
+
|
|
3465
|
+
const { access_token, refresh_token, user } = await response.json();
|
|
3466
|
+
|
|
3467
|
+
// 2. Store tokens securely
|
|
3468
|
+
localStorage.setItem('access_token', access_token);
|
|
3469
|
+
localStorage.setItem('refresh_token', refresh_token);
|
|
3470
|
+
|
|
3471
|
+
// User is now logged in
|
|
3472
|
+
\`\`\`
|
|
3473
|
+
|
|
3474
|
+
## 2. Login Flow
|
|
3475
|
+
|
|
3476
|
+
\`\`\`typescript
|
|
3477
|
+
const response = await fetch('/v1/auth/login', {
|
|
3478
|
+
method: 'POST',
|
|
3479
|
+
headers: {
|
|
3480
|
+
'X-API-Key': 'pk_live_your_key',
|
|
3481
|
+
'Content-Type': 'application/json',
|
|
3482
|
+
},
|
|
3483
|
+
body: JSON.stringify({
|
|
3484
|
+
email: 'user@example.com',
|
|
3485
|
+
password: 'SecurePass123',
|
|
3486
|
+
}),
|
|
3487
|
+
});
|
|
3488
|
+
|
|
3489
|
+
if (!response.ok) {
|
|
3490
|
+
const error = await response.json();
|
|
3491
|
+
// Handle error: error.detail contains the message
|
|
3492
|
+
throw new Error(error.detail);
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
const { access_token, refresh_token, user } = await response.json();
|
|
3496
|
+
\`\`\`
|
|
3497
|
+
|
|
3498
|
+
## 3. Token Refresh Flow
|
|
3499
|
+
|
|
3500
|
+
\`\`\`typescript
|
|
3501
|
+
// Call this when access_token expires (expires_in seconds from login)
|
|
3502
|
+
const response = await fetch('/v1/auth/refresh', {
|
|
3503
|
+
method: 'POST',
|
|
3504
|
+
headers: {
|
|
3505
|
+
'X-API-Key': 'pk_live_your_key',
|
|
3506
|
+
'Content-Type': 'application/json',
|
|
3507
|
+
},
|
|
3508
|
+
body: JSON.stringify({
|
|
3509
|
+
refresh_token: localStorage.getItem('refresh_token'),
|
|
3510
|
+
}),
|
|
3511
|
+
});
|
|
3512
|
+
|
|
3513
|
+
const { access_token } = await response.json();
|
|
3514
|
+
localStorage.setItem('access_token', access_token);
|
|
3515
|
+
\`\`\`
|
|
3516
|
+
|
|
3517
|
+
## 4. Password Reset Flow
|
|
3518
|
+
|
|
3519
|
+
\`\`\`typescript
|
|
3520
|
+
// Step 1: Request reset email
|
|
3521
|
+
await fetch('/v1/auth/forgot-password', {
|
|
3522
|
+
method: 'POST',
|
|
3523
|
+
headers: {
|
|
3524
|
+
'X-API-Key': 'pk_live_your_key',
|
|
3525
|
+
'Content-Type': 'application/json',
|
|
3526
|
+
},
|
|
3527
|
+
body: JSON.stringify({ email: 'user@example.com' }),
|
|
3528
|
+
});
|
|
3529
|
+
// Always succeeds (security)
|
|
3530
|
+
|
|
3531
|
+
// Step 2: User clicks link in email with token
|
|
3532
|
+
|
|
3533
|
+
// Step 3: Reset password with token
|
|
3534
|
+
await fetch('/v1/auth/reset-password', {
|
|
3535
|
+
method: 'POST',
|
|
3536
|
+
headers: {
|
|
3537
|
+
'X-API-Key': 'pk_live_your_key',
|
|
3538
|
+
'Content-Type': 'application/json',
|
|
3539
|
+
},
|
|
3540
|
+
body: JSON.stringify({
|
|
3541
|
+
token: 'token-from-email-link',
|
|
3542
|
+
new_password: 'NewSecurePass123',
|
|
3543
|
+
}),
|
|
3544
|
+
});
|
|
3545
|
+
\`\`\`
|
|
3546
|
+
|
|
3547
|
+
## 5. Change Password Flow (Authenticated)
|
|
3548
|
+
|
|
3549
|
+
\`\`\`typescript
|
|
3550
|
+
await fetch('/v1/auth/change-password', {
|
|
3551
|
+
method: 'POST',
|
|
3552
|
+
headers: {
|
|
3553
|
+
'X-API-Key': 'pk_live_your_key',
|
|
3554
|
+
'Authorization': 'Bearer your_jwt_token',
|
|
3555
|
+
'Content-Type': 'application/json',
|
|
3556
|
+
},
|
|
3557
|
+
body: JSON.stringify({
|
|
3558
|
+
current_password: 'OldPass123!',
|
|
3559
|
+
new_password: 'NewPass456!',
|
|
3560
|
+
}),
|
|
3561
|
+
});
|
|
3562
|
+
\`\`\`
|
|
3563
|
+
|
|
3564
|
+
## 6. Email Verification Flow
|
|
3565
|
+
|
|
3566
|
+
\`\`\`typescript
|
|
3567
|
+
// Registration may require email verification depending on tenant config.
|
|
3568
|
+
// Check the register response for verification_required: true
|
|
3569
|
+
|
|
3570
|
+
const registerResult = await response.json();
|
|
3571
|
+
|
|
3572
|
+
if (registerResult.verification_required) {
|
|
3573
|
+
// 1. Show "Check your email" message — NO tokens returned
|
|
3574
|
+
// 2. User clicks link in email → opens /verify-email?token=xxx
|
|
3575
|
+
|
|
3576
|
+
// 3. Verify the email token
|
|
3577
|
+
const verifyResponse = await fetch('/v1/auth/verify-email', {
|
|
3578
|
+
method: 'POST',
|
|
3579
|
+
headers: {
|
|
3580
|
+
'X-API-Key': 'pk_live_your_key',
|
|
3581
|
+
'Content-Type': 'application/json',
|
|
3582
|
+
},
|
|
3583
|
+
body: JSON.stringify({ token: tokenFromUrl }),
|
|
3584
|
+
});
|
|
3585
|
+
// Returns: { message: "Email verified successfully" }
|
|
3586
|
+
|
|
3587
|
+
// 4. If token expired, user can request a new one
|
|
3588
|
+
await fetch('/v1/auth/resend-verification', {
|
|
3589
|
+
method: 'POST',
|
|
3590
|
+
headers: {
|
|
3591
|
+
'X-API-Key': 'pk_live_your_key',
|
|
3592
|
+
'Content-Type': 'application/json',
|
|
3593
|
+
},
|
|
3594
|
+
body: JSON.stringify({ email: 'user@example.com' }),
|
|
3595
|
+
});
|
|
3596
|
+
// Rate limited: 3/hour
|
|
3597
|
+
|
|
3598
|
+
// 5. After verification, user can login normally via /v1/auth/login
|
|
3599
|
+
} else {
|
|
3600
|
+
// No verification required — tokens returned directly from register
|
|
3601
|
+
localStorage.setItem('access_token', registerResult.access_token);
|
|
3602
|
+
localStorage.setItem('refresh_token', registerResult.refresh_token);
|
|
3603
|
+
}
|
|
3604
|
+
\`\`\`
|
|
3605
|
+
|
|
3606
|
+
## 7. Google OAuth Flow
|
|
3607
|
+
|
|
3608
|
+
\`\`\`typescript
|
|
3609
|
+
// 1. Check if Google OAuth is enabled for this tenant
|
|
3610
|
+
const providersRes = await fetch('/v1/auth/providers', {
|
|
3611
|
+
headers: { 'X-API-Key': 'pk_live_your_key' },
|
|
3612
|
+
});
|
|
3613
|
+
const { providers } = await providersRes.json();
|
|
3614
|
+
const googleEnabled = providers.some((p: any) => p.provider === 'google');
|
|
3615
|
+
|
|
3616
|
+
// 2. Get Google authorization URL
|
|
3617
|
+
const redirectUri = window.location.origin + '/oauth/callback';
|
|
3618
|
+
const urlRes = await fetch(
|
|
3619
|
+
\`/v1/auth/google/url?redirect_uri=\${encodeURIComponent(redirectUri)}\`,
|
|
3620
|
+
{ headers: { 'X-API-Key': 'pk_live_your_key' } }
|
|
3621
|
+
);
|
|
3622
|
+
const { authorization_url } = await urlRes.json();
|
|
3623
|
+
|
|
3624
|
+
// 3. Redirect user to Google
|
|
3625
|
+
window.location.href = authorization_url;
|
|
3626
|
+
|
|
3627
|
+
// 4. Google redirects to /v1/auth/google/callback (backend handles this)
|
|
3628
|
+
// 5. Backend redirects to YOUR redirect_uri with tokens in URL FRAGMENT (#)
|
|
3629
|
+
|
|
3630
|
+
// 6. In your /oauth/callback page, read tokens from hash (NOT query params)
|
|
3631
|
+
const hash = window.location.hash.substring(1);
|
|
3632
|
+
const params = new URLSearchParams(hash);
|
|
3633
|
+
const accessToken = params.get('access_token');
|
|
3634
|
+
const refreshToken = params.get('refresh_token');
|
|
3635
|
+
const expiresIn = params.get('expires_in');
|
|
3636
|
+
|
|
3637
|
+
// Also check query params for errors
|
|
3638
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
3639
|
+
const error = urlParams.get('error');
|
|
3640
|
+
if (error) {
|
|
3641
|
+
console.error('OAuth failed:', urlParams.get('error_description'));
|
|
3642
|
+
}
|
|
3643
|
+
\`\`\`
|
|
3644
|
+
|
|
3645
|
+
## 8. Authenticated Requests
|
|
3646
|
+
|
|
3647
|
+
\`\`\`typescript
|
|
3648
|
+
// After login, include Authorization header
|
|
3649
|
+
const response = await fetch('/v1/auth/me', {
|
|
3650
|
+
headers: {
|
|
3651
|
+
'X-API-Key': 'pk_live_your_key',
|
|
3652
|
+
'Authorization': \`Bearer \${localStorage.getItem('access_token')}\`,
|
|
3653
|
+
},
|
|
3654
|
+
});
|
|
3655
|
+
|
|
3656
|
+
const user = await response.json();
|
|
3657
|
+
\`\`\`
|
|
3658
|
+
|
|
3659
|
+
## 9. Logout
|
|
3660
|
+
|
|
3661
|
+
\`\`\`typescript
|
|
3662
|
+
await fetch('/v1/auth/logout', {
|
|
3663
|
+
method: 'POST',
|
|
3664
|
+
headers: {
|
|
3665
|
+
'X-API-Key': 'pk_live_your_key',
|
|
3666
|
+
'Authorization': \`Bearer \${localStorage.getItem('access_token')}\`,
|
|
3667
|
+
},
|
|
3668
|
+
});
|
|
3669
|
+
|
|
3670
|
+
localStorage.removeItem('access_token');
|
|
3671
|
+
localStorage.removeItem('refresh_token');
|
|
3137
3672
|
\`\`\``,
|
|
3138
3673
|
},
|
|
3139
3674
|
],
|
|
@@ -3144,225 +3679,412 @@ localStorage.removeItem('refresh_token');
|
|
|
3144
3679
|
content: [
|
|
3145
3680
|
{
|
|
3146
3681
|
type: "text",
|
|
3147
|
-
text: `# Common Implementation Patterns
|
|
3148
|
-
|
|
3149
|
-
## 1. API Request Wrapper
|
|
3150
|
-
|
|
3151
|
-
\`\`\`typescript
|
|
3152
|
-
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
|
3153
|
-
const API_KEY = process.env.NEXT_PUBLIC_API_KEY; // pk_live_*
|
|
3154
|
-
|
|
3155
|
-
interface RequestOptions {
|
|
3156
|
-
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
3157
|
-
body?: any;
|
|
3158
|
-
requiresAuth?: boolean;
|
|
3159
|
-
}
|
|
3160
|
-
|
|
3161
|
-
async function apiRequest<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
|
|
3162
|
-
const { method = 'GET', body, requiresAuth = false } = options;
|
|
3163
|
-
|
|
3164
|
-
const headers: Record<string, string> = {
|
|
3165
|
-
'X-API-Key': API_KEY,
|
|
3166
|
-
'Content-Type': 'application/json',
|
|
3167
|
-
};
|
|
3168
|
-
|
|
3169
|
-
if (requiresAuth) {
|
|
3170
|
-
const token = localStorage.getItem('access_token');
|
|
3171
|
-
if (token) {
|
|
3172
|
-
headers['Authorization'] = \`Bearer \${token}\`;
|
|
3173
|
-
}
|
|
3174
|
-
}
|
|
3175
|
-
|
|
3176
|
-
const response = await fetch(\`\${API_URL}\${endpoint}\`, {
|
|
3177
|
-
method,
|
|
3178
|
-
headers,
|
|
3179
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
3180
|
-
});
|
|
3181
|
-
|
|
3182
|
-
if (response.status === 401 && requiresAuth) {
|
|
3183
|
-
// Try to refresh token
|
|
3184
|
-
const refreshed = await refreshToken();
|
|
3185
|
-
if (refreshed) {
|
|
3186
|
-
// Retry request with new token
|
|
3187
|
-
headers['Authorization'] = \`Bearer \${localStorage.getItem('access_token')}\`;
|
|
3188
|
-
const retryResponse = await fetch(\`\${API_URL}\${endpoint}\`, {
|
|
3189
|
-
method,
|
|
3190
|
-
headers,
|
|
3191
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
3192
|
-
});
|
|
3193
|
-
if (!retryResponse.ok) throw new Error('Request failed after refresh');
|
|
3194
|
-
return retryResponse.json();
|
|
3195
|
-
}
|
|
3196
|
-
// Redirect to login
|
|
3197
|
-
window.location.href = '/login';
|
|
3198
|
-
throw new Error('Session expired');
|
|
3199
|
-
}
|
|
3200
|
-
|
|
3201
|
-
if (!response.ok) {
|
|
3202
|
-
const error = await response.json();
|
|
3203
|
-
throw new Error(error.detail || 'Request failed');
|
|
3204
|
-
}
|
|
3205
|
-
|
|
3206
|
-
return response.json();
|
|
3207
|
-
}
|
|
3208
|
-
\`\`\`
|
|
3209
|
-
|
|
3210
|
-
## 2. Auto Token Refresh
|
|
3211
|
-
|
|
3212
|
-
\`\`\`typescript
|
|
3213
|
-
async function refreshToken(): Promise<boolean> {
|
|
3214
|
-
const refreshToken = localStorage.getItem('refresh_token');
|
|
3215
|
-
if (!refreshToken) return false;
|
|
3216
|
-
|
|
3217
|
-
try {
|
|
3218
|
-
const response = await fetch(\`\${API_URL}/v1/auth/refresh\`, {
|
|
3219
|
-
method: 'POST',
|
|
3220
|
-
headers: {
|
|
3221
|
-
'X-API-Key': API_KEY,
|
|
3222
|
-
'Content-Type': 'application/json',
|
|
3223
|
-
},
|
|
3224
|
-
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
3225
|
-
});
|
|
3226
|
-
|
|
3227
|
-
if (!response.ok) return false;
|
|
3228
|
-
|
|
3229
|
-
const data = await response.json();
|
|
3230
|
-
localStorage.setItem('access_token', data.access_token);
|
|
3231
|
-
return true;
|
|
3232
|
-
} catch {
|
|
3233
|
-
return false;
|
|
3234
|
-
}
|
|
3235
|
-
}
|
|
3236
|
-
\`\`\`
|
|
3237
|
-
|
|
3238
|
-
## 3. React Hook Example
|
|
3239
|
-
|
|
3240
|
-
\`\`\`typescript
|
|
3241
|
-
import { useState, useEffect } from 'react';
|
|
3242
|
-
|
|
3243
|
-
function useAuth() {
|
|
3244
|
-
const [user, setUser] = useState(null);
|
|
3245
|
-
const [loading, setLoading] = useState(true);
|
|
3246
|
-
|
|
3247
|
-
useEffect(() => {
|
|
3248
|
-
const token = localStorage.getItem('access_token');
|
|
3249
|
-
if (!token) {
|
|
3250
|
-
setLoading(false);
|
|
3251
|
-
return;
|
|
3252
|
-
}
|
|
3253
|
-
|
|
3254
|
-
apiRequest('/v1/auth/me', { requiresAuth: true })
|
|
3255
|
-
.then(setUser)
|
|
3256
|
-
.catch(() => {
|
|
3257
|
-
localStorage.removeItem('access_token');
|
|
3258
|
-
localStorage.removeItem('refresh_token');
|
|
3259
|
-
})
|
|
3260
|
-
.finally(() => setLoading(false));
|
|
3261
|
-
}, []);
|
|
3262
|
-
|
|
3263
|
-
const login = async (email: string, password: string) => {
|
|
3264
|
-
const data = await apiRequest('/v1/auth/login', {
|
|
3265
|
-
method: 'POST',
|
|
3266
|
-
body: { email, password },
|
|
3267
|
-
});
|
|
3268
|
-
localStorage.setItem('access_token', data.access_token);
|
|
3269
|
-
localStorage.setItem('refresh_token', data.refresh_token);
|
|
3270
|
-
setUser(data.user);
|
|
3271
|
-
return data;
|
|
3272
|
-
};
|
|
3273
|
-
|
|
3274
|
-
const logout = async () => {
|
|
3275
|
-
await apiRequest('/v1/auth/logout', { method: 'POST', requiresAuth: true });
|
|
3276
|
-
localStorage.removeItem('access_token');
|
|
3277
|
-
localStorage.removeItem('refresh_token');
|
|
3278
|
-
setUser(null);
|
|
3279
|
-
};
|
|
3280
|
-
|
|
3281
|
-
return { user, loading, login, logout };
|
|
3282
|
-
}
|
|
3283
|
-
\`\`\`
|
|
3284
|
-
|
|
3285
|
-
## 4. Error Handling
|
|
3286
|
-
|
|
3287
|
-
\`\`\`typescript
|
|
3288
|
-
interface ApiError {
|
|
3289
|
-
detail: string;
|
|
3290
|
-
error?: string;
|
|
3291
|
-
error_description?: string;
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
|
-
function handleApiError(error: ApiError): string {
|
|
3295
|
-
// Common error messages
|
|
3296
|
-
const errorMessages: Record<string, string> = {
|
|
3297
|
-
'Invalid email or password': 'The email or password you entered is incorrect.',
|
|
3298
|
-
'User already exists': 'An account with this email already exists.',
|
|
3299
|
-
'Invalid token': 'Your session has expired. Please log in again.',
|
|
3300
|
-
'Rate limit exceeded': 'Too many requests. Please try again later.',
|
|
3301
|
-
};
|
|
3302
|
-
|
|
3303
|
-
return errorMessages[error.detail] || error.detail || 'An error occurred';
|
|
3304
|
-
}
|
|
3305
|
-
\`\`\`
|
|
3306
|
-
|
|
3307
|
-
## 5. OAuth Callback Handling
|
|
3308
|
-
|
|
3309
|
-
\`\`\`typescript
|
|
3310
|
-
// After Google OAuth, tokens arrive in the URL FRAGMENT (#), not query params (?)
|
|
3311
|
-
// This is because fragments are never sent to the server — they stay client-side only
|
|
3312
|
-
|
|
3313
|
-
function parseOAuthCallback(): { access_token: string; refresh_token: string; expires_in: string } | null {
|
|
3314
|
-
// Check for errors in query params first
|
|
3315
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
3316
|
-
const error = urlParams.get('error');
|
|
3317
|
-
if (error) {
|
|
3318
|
-
console.error('OAuth error:', urlParams.get('error_description'));
|
|
3319
|
-
return null;
|
|
3320
|
-
}
|
|
3321
|
-
|
|
3322
|
-
// Parse tokens from URL fragment
|
|
3323
|
-
const hash = window.location.hash.substring(1); // Remove leading #
|
|
3324
|
-
const params = new URLSearchParams(hash);
|
|
3325
|
-
|
|
3326
|
-
const access_token = params.get('access_token');
|
|
3327
|
-
const refresh_token = params.get('refresh_token');
|
|
3328
|
-
const expires_in = params.get('expires_in');
|
|
3329
|
-
|
|
3330
|
-
if (!access_token || !refresh_token) return null;
|
|
3331
|
-
|
|
3332
|
-
// Clean up the URL (remove fragment)
|
|
3333
|
-
window.history.replaceState({}, '', window.location.pathname);
|
|
3334
|
-
|
|
3335
|
-
return { access_token, refresh_token, expires_in: expires_in || '3600' };
|
|
3336
|
-
}
|
|
3337
|
-
|
|
3338
|
-
// Usage in your OAuth callback page:
|
|
3339
|
-
// const tokens = parseOAuthCallback();
|
|
3340
|
-
// if (tokens) {
|
|
3341
|
-
// localStorage.setItem('access_token', tokens.access_token);
|
|
3342
|
-
// localStorage.setItem('refresh_token', tokens.refresh_token);
|
|
3343
|
-
// navigate('/dashboard');
|
|
3344
|
-
// }
|
|
3345
|
-
\`\`\`
|
|
3346
|
-
|
|
3347
|
-
## 6. Environment Variables (Vite / React)
|
|
3348
|
-
|
|
3349
|
-
\`\`\`env
|
|
3350
|
-
# .env
|
|
3351
|
-
VITE_API_URL=https://api.yoursaas.com
|
|
3352
|
-
VITE_API_KEY=pk_live_your_public_key_here
|
|
3353
|
-
\`\`\`
|
|
3354
|
-
|
|
3355
|
-
## 7. Environment Variables (Next.js)
|
|
3356
|
-
|
|
3357
|
-
\`\`\`env
|
|
3358
|
-
# .env.local
|
|
3359
|
-
NEXT_PUBLIC_API_URL=https://api.yoursaas.com
|
|
3360
|
-
NEXT_PUBLIC_API_KEY=pk_live_your_public_key_here
|
|
3682
|
+
text: `# Common Implementation Patterns
|
|
3683
|
+
|
|
3684
|
+
## 1. API Request Wrapper
|
|
3685
|
+
|
|
3686
|
+
\`\`\`typescript
|
|
3687
|
+
const API_URL = process.env.NEXT_PUBLIC_API_URL;
|
|
3688
|
+
const API_KEY = process.env.NEXT_PUBLIC_API_KEY; // pk_live_*
|
|
3689
|
+
|
|
3690
|
+
interface RequestOptions {
|
|
3691
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
3692
|
+
body?: any;
|
|
3693
|
+
requiresAuth?: boolean;
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3696
|
+
async function apiRequest<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
|
|
3697
|
+
const { method = 'GET', body, requiresAuth = false } = options;
|
|
3698
|
+
|
|
3699
|
+
const headers: Record<string, string> = {
|
|
3700
|
+
'X-API-Key': API_KEY,
|
|
3701
|
+
'Content-Type': 'application/json',
|
|
3702
|
+
};
|
|
3703
|
+
|
|
3704
|
+
if (requiresAuth) {
|
|
3705
|
+
const token = localStorage.getItem('access_token');
|
|
3706
|
+
if (token) {
|
|
3707
|
+
headers['Authorization'] = \`Bearer \${token}\`;
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
const response = await fetch(\`\${API_URL}\${endpoint}\`, {
|
|
3712
|
+
method,
|
|
3713
|
+
headers,
|
|
3714
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
3715
|
+
});
|
|
3716
|
+
|
|
3717
|
+
if (response.status === 401 && requiresAuth) {
|
|
3718
|
+
// Try to refresh token
|
|
3719
|
+
const refreshed = await refreshToken();
|
|
3720
|
+
if (refreshed) {
|
|
3721
|
+
// Retry request with new token
|
|
3722
|
+
headers['Authorization'] = \`Bearer \${localStorage.getItem('access_token')}\`;
|
|
3723
|
+
const retryResponse = await fetch(\`\${API_URL}\${endpoint}\`, {
|
|
3724
|
+
method,
|
|
3725
|
+
headers,
|
|
3726
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
3727
|
+
});
|
|
3728
|
+
if (!retryResponse.ok) throw new Error('Request failed after refresh');
|
|
3729
|
+
return retryResponse.json();
|
|
3730
|
+
}
|
|
3731
|
+
// Redirect to login
|
|
3732
|
+
window.location.href = '/login';
|
|
3733
|
+
throw new Error('Session expired');
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
if (!response.ok) {
|
|
3737
|
+
const error = await response.json();
|
|
3738
|
+
throw new Error(error.detail || 'Request failed');
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
return response.json();
|
|
3742
|
+
}
|
|
3743
|
+
\`\`\`
|
|
3744
|
+
|
|
3745
|
+
## 2. Auto Token Refresh
|
|
3746
|
+
|
|
3747
|
+
\`\`\`typescript
|
|
3748
|
+
async function refreshToken(): Promise<boolean> {
|
|
3749
|
+
const refreshToken = localStorage.getItem('refresh_token');
|
|
3750
|
+
if (!refreshToken) return false;
|
|
3751
|
+
|
|
3752
|
+
try {
|
|
3753
|
+
const response = await fetch(\`\${API_URL}/v1/auth/refresh\`, {
|
|
3754
|
+
method: 'POST',
|
|
3755
|
+
headers: {
|
|
3756
|
+
'X-API-Key': API_KEY,
|
|
3757
|
+
'Content-Type': 'application/json',
|
|
3758
|
+
},
|
|
3759
|
+
body: JSON.stringify({ refresh_token: refreshToken }),
|
|
3760
|
+
});
|
|
3761
|
+
|
|
3762
|
+
if (!response.ok) return false;
|
|
3763
|
+
|
|
3764
|
+
const data = await response.json();
|
|
3765
|
+
localStorage.setItem('access_token', data.access_token);
|
|
3766
|
+
return true;
|
|
3767
|
+
} catch {
|
|
3768
|
+
return false;
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
\`\`\`
|
|
3772
|
+
|
|
3773
|
+
## 3. React Hook Example
|
|
3774
|
+
|
|
3775
|
+
\`\`\`typescript
|
|
3776
|
+
import { useState, useEffect } from 'react';
|
|
3777
|
+
|
|
3778
|
+
function useAuth() {
|
|
3779
|
+
const [user, setUser] = useState(null);
|
|
3780
|
+
const [loading, setLoading] = useState(true);
|
|
3781
|
+
|
|
3782
|
+
useEffect(() => {
|
|
3783
|
+
const token = localStorage.getItem('access_token');
|
|
3784
|
+
if (!token) {
|
|
3785
|
+
setLoading(false);
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
apiRequest('/v1/auth/me', { requiresAuth: true })
|
|
3790
|
+
.then(setUser)
|
|
3791
|
+
.catch(() => {
|
|
3792
|
+
localStorage.removeItem('access_token');
|
|
3793
|
+
localStorage.removeItem('refresh_token');
|
|
3794
|
+
})
|
|
3795
|
+
.finally(() => setLoading(false));
|
|
3796
|
+
}, []);
|
|
3797
|
+
|
|
3798
|
+
const login = async (email: string, password: string) => {
|
|
3799
|
+
const data = await apiRequest('/v1/auth/login', {
|
|
3800
|
+
method: 'POST',
|
|
3801
|
+
body: { email, password },
|
|
3802
|
+
});
|
|
3803
|
+
localStorage.setItem('access_token', data.access_token);
|
|
3804
|
+
localStorage.setItem('refresh_token', data.refresh_token);
|
|
3805
|
+
setUser(data.user);
|
|
3806
|
+
return data;
|
|
3807
|
+
};
|
|
3808
|
+
|
|
3809
|
+
const logout = async () => {
|
|
3810
|
+
await apiRequest('/v1/auth/logout', { method: 'POST', requiresAuth: true });
|
|
3811
|
+
localStorage.removeItem('access_token');
|
|
3812
|
+
localStorage.removeItem('refresh_token');
|
|
3813
|
+
setUser(null);
|
|
3814
|
+
};
|
|
3815
|
+
|
|
3816
|
+
return { user, loading, login, logout };
|
|
3817
|
+
}
|
|
3818
|
+
\`\`\`
|
|
3819
|
+
|
|
3820
|
+
## 4. Error Handling
|
|
3821
|
+
|
|
3822
|
+
\`\`\`typescript
|
|
3823
|
+
interface ApiError {
|
|
3824
|
+
detail: string;
|
|
3825
|
+
error?: string;
|
|
3826
|
+
error_description?: string;
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
function handleApiError(error: ApiError): string {
|
|
3830
|
+
// Common error messages
|
|
3831
|
+
const errorMessages: Record<string, string> = {
|
|
3832
|
+
'Invalid email or password': 'The email or password you entered is incorrect.',
|
|
3833
|
+
'User already exists': 'An account with this email already exists.',
|
|
3834
|
+
'Invalid token': 'Your session has expired. Please log in again.',
|
|
3835
|
+
'Rate limit exceeded': 'Too many requests. Please try again later.',
|
|
3836
|
+
};
|
|
3837
|
+
|
|
3838
|
+
return errorMessages[error.detail] || error.detail || 'An error occurred';
|
|
3839
|
+
}
|
|
3840
|
+
\`\`\`
|
|
3841
|
+
|
|
3842
|
+
## 5. OAuth Callback Handling
|
|
3843
|
+
|
|
3844
|
+
\`\`\`typescript
|
|
3845
|
+
// After Google OAuth, tokens arrive in the URL FRAGMENT (#), not query params (?)
|
|
3846
|
+
// This is because fragments are never sent to the server — they stay client-side only
|
|
3847
|
+
|
|
3848
|
+
function parseOAuthCallback(): { access_token: string; refresh_token: string; expires_in: string } | null {
|
|
3849
|
+
// Check for errors in query params first
|
|
3850
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
3851
|
+
const error = urlParams.get('error');
|
|
3852
|
+
if (error) {
|
|
3853
|
+
console.error('OAuth error:', urlParams.get('error_description'));
|
|
3854
|
+
return null;
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
// Parse tokens from URL fragment
|
|
3858
|
+
const hash = window.location.hash.substring(1); // Remove leading #
|
|
3859
|
+
const params = new URLSearchParams(hash);
|
|
3860
|
+
|
|
3861
|
+
const access_token = params.get('access_token');
|
|
3862
|
+
const refresh_token = params.get('refresh_token');
|
|
3863
|
+
const expires_in = params.get('expires_in');
|
|
3864
|
+
|
|
3865
|
+
if (!access_token || !refresh_token) return null;
|
|
3866
|
+
|
|
3867
|
+
// Clean up the URL (remove fragment)
|
|
3868
|
+
window.history.replaceState({}, '', window.location.pathname);
|
|
3869
|
+
|
|
3870
|
+
return { access_token, refresh_token, expires_in: expires_in || '3600' };
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
// Usage in your OAuth callback page:
|
|
3874
|
+
// const tokens = parseOAuthCallback();
|
|
3875
|
+
// if (tokens) {
|
|
3876
|
+
// localStorage.setItem('access_token', tokens.access_token);
|
|
3877
|
+
// localStorage.setItem('refresh_token', tokens.refresh_token);
|
|
3878
|
+
// navigate('/dashboard');
|
|
3879
|
+
// }
|
|
3880
|
+
\`\`\`
|
|
3881
|
+
|
|
3882
|
+
## 6. Environment Variables (Vite / React)
|
|
3883
|
+
|
|
3884
|
+
\`\`\`env
|
|
3885
|
+
# .env
|
|
3886
|
+
VITE_API_URL=https://api.yoursaas.com
|
|
3887
|
+
VITE_API_KEY=pk_live_your_public_key_here
|
|
3888
|
+
\`\`\`
|
|
3889
|
+
|
|
3890
|
+
## 7. Environment Variables (Next.js)
|
|
3891
|
+
|
|
3892
|
+
\`\`\`env
|
|
3893
|
+
# .env.local
|
|
3894
|
+
NEXT_PUBLIC_API_URL=https://api.yoursaas.com
|
|
3895
|
+
NEXT_PUBLIC_API_KEY=pk_live_your_public_key_here
|
|
3361
3896
|
\`\`\``,
|
|
3362
3897
|
},
|
|
3363
3898
|
],
|
|
3364
3899
|
};
|
|
3365
3900
|
}
|
|
3901
|
+
case "validate_credentials": {
|
|
3902
|
+
const detected = detectKeyType(API_KEY);
|
|
3903
|
+
if (!detected.type) {
|
|
3904
|
+
return {
|
|
3905
|
+
content: [
|
|
3906
|
+
{
|
|
3907
|
+
type: "text",
|
|
3908
|
+
text: `# Credentials check
|
|
3909
|
+
|
|
3910
|
+
❌ **No SAAS_API_KEY configured.**
|
|
3911
|
+
|
|
3912
|
+
Most authenticated tools (\`list_end_users\`, \`list_oauth_providers\`, etc.)
|
|
3913
|
+
will fail. Add the key to your MCP client config:
|
|
3914
|
+
|
|
3915
|
+
\`\`\`json
|
|
3916
|
+
{
|
|
3917
|
+
"mcpServers": {
|
|
3918
|
+
"genlobe": {
|
|
3919
|
+
"command": "node",
|
|
3920
|
+
"args": ["…/dist/index.js"],
|
|
3921
|
+
"env": {
|
|
3922
|
+
"SAAS_API_URL": "${API_URL}",
|
|
3923
|
+
"SAAS_API_KEY": "sk_live_…"
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
\`\`\`
|
|
3929
|
+
|
|
3930
|
+
Then restart the MCP. Run \`get_security_guide\` for guidance on which key
|
|
3931
|
+
type to pick.`,
|
|
3932
|
+
},
|
|
3933
|
+
],
|
|
3934
|
+
};
|
|
3935
|
+
}
|
|
3936
|
+
// Probe with a known sk_live-only endpoint. /v1/auth/users is a safe
|
|
3937
|
+
// smoke test for secret keys; for public keys this will 401, which
|
|
3938
|
+
// is itself useful information.
|
|
3939
|
+
let probeResult;
|
|
3940
|
+
try {
|
|
3941
|
+
const r = await fetch(`${API_URL}/v1/auth/users?limit=1`, {
|
|
3942
|
+
headers: { "X-API-Key": API_KEY },
|
|
3943
|
+
});
|
|
3944
|
+
probeResult = { ok: r.ok, status: r.status };
|
|
3945
|
+
if (!r.ok) {
|
|
3946
|
+
try {
|
|
3947
|
+
probeResult.detail = (await r.json()).detail;
|
|
3948
|
+
}
|
|
3949
|
+
catch { }
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
catch (err) {
|
|
3953
|
+
probeResult = { ok: false, status: 0, detail: String(err) };
|
|
3954
|
+
}
|
|
3955
|
+
const lines = [
|
|
3956
|
+
`# Credentials check`,
|
|
3957
|
+
``,
|
|
3958
|
+
`**Configured API URL:** \`${API_URL}\``,
|
|
3959
|
+
`**Detected key type:** ${detected.type === "public" ? "🟢 Public (`pk_live_*`)" : "🔴 Secret (`sk_live_*`)"}`,
|
|
3960
|
+
`**Key prefix:** \`${detected.prefix}\``,
|
|
3961
|
+
``,
|
|
3962
|
+
];
|
|
3963
|
+
if (detected.type === "secret") {
|
|
3964
|
+
if (probeResult.ok) {
|
|
3965
|
+
lines.push(`✅ Probe call \`GET /v1/auth/users\` succeeded — the key authenticates.`, ``, `**You can use this key for:** every end-user endpoint that accepts API key auth, including admin-style listings (users, orgs) and end-user signup/login on behalf of users.`, ``, `**You CANNOT use this key for:** anything that runs in a browser. Run \`get_security_guide\` before writing frontend code.`);
|
|
3966
|
+
}
|
|
3967
|
+
else {
|
|
3968
|
+
lines.push(`❌ Probe call \`GET /v1/auth/users\` failed: HTTP ${probeResult.status}${probeResult.detail ? ` — ${probeResult.detail}` : ""}.`, ``, `Possible causes: key revoked, wrong tenant, wrong API URL, network blocked, backend down.`);
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
else {
|
|
3972
|
+
if (probeResult.status === 401 || probeResult.status === 403) {
|
|
3973
|
+
lines.push(`✅ Behavior matches a public key: \`/v1/auth/users\` returned HTTP ${probeResult.status} as expected. Public keys can only call auth endpoints (\`/v1/auth/register\`, \`/v1/auth/login\`, etc.).`, ``, `Run \`get_security_guide\` for the full list of allowed endpoints.`);
|
|
3974
|
+
}
|
|
3975
|
+
else if (probeResult.ok) {
|
|
3976
|
+
lines.push(`⚠️ Probe unexpectedly succeeded with what looks like a public key. Verify the prefix; if it really is a secret key the prefix detection is wrong.`);
|
|
3977
|
+
}
|
|
3978
|
+
else {
|
|
3979
|
+
lines.push(`❌ Probe failed unexpectedly: HTTP ${probeResult.status}${probeResult.detail ? ` — ${probeResult.detail}` : ""}.`);
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
3983
|
+
}
|
|
3984
|
+
case "list_end_users": {
|
|
3985
|
+
if (!API_KEY) {
|
|
3986
|
+
return {
|
|
3987
|
+
content: [
|
|
3988
|
+
{
|
|
3989
|
+
type: "text",
|
|
3990
|
+
text: `❌ Cannot list end-users: SAAS_API_KEY is not configured. Add a secret key (\`sk_live_*\`) to the MCP env. Use \`validate_credentials\` to verify.`,
|
|
3991
|
+
},
|
|
3992
|
+
],
|
|
3993
|
+
};
|
|
3994
|
+
}
|
|
3995
|
+
const skip = args.skip ?? 0;
|
|
3996
|
+
const limit = args.limit ?? 20;
|
|
3997
|
+
try {
|
|
3998
|
+
const r = await fetch(`${API_URL}/v1/auth/users?skip=${skip}&limit=${Math.min(limit, 100)}`, { headers: { "X-API-Key": API_KEY } });
|
|
3999
|
+
const body = await r.json();
|
|
4000
|
+
if (!r.ok) {
|
|
4001
|
+
return {
|
|
4002
|
+
content: [
|
|
4003
|
+
{
|
|
4004
|
+
type: "text",
|
|
4005
|
+
text: `HTTP ${r.status} from /v1/auth/users — ${body.detail ?? JSON.stringify(body)}.\n\nIf the key is \`pk_live_*\`, this endpoint is restricted to secret keys. Run \`get_security_guide\` for details.`,
|
|
4006
|
+
},
|
|
4007
|
+
],
|
|
4008
|
+
};
|
|
4009
|
+
}
|
|
4010
|
+
return {
|
|
4011
|
+
content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
4014
|
+
catch (err) {
|
|
4015
|
+
return {
|
|
4016
|
+
content: [{ type: "text", text: `Network error: ${err}` }],
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
case "list_oauth_providers": {
|
|
4021
|
+
if (!API_KEY) {
|
|
4022
|
+
return {
|
|
4023
|
+
content: [
|
|
4024
|
+
{
|
|
4025
|
+
type: "text",
|
|
4026
|
+
text: `❌ Cannot list providers: SAAS_API_KEY not configured.`,
|
|
4027
|
+
},
|
|
4028
|
+
],
|
|
4029
|
+
};
|
|
4030
|
+
}
|
|
4031
|
+
try {
|
|
4032
|
+
const r = await fetch(`${API_URL}/v1/auth/providers`, {
|
|
4033
|
+
headers: { "X-API-Key": API_KEY },
|
|
4034
|
+
});
|
|
4035
|
+
const body = await r.json();
|
|
4036
|
+
if (!r.ok) {
|
|
4037
|
+
return {
|
|
4038
|
+
content: [
|
|
4039
|
+
{
|
|
4040
|
+
type: "text",
|
|
4041
|
+
text: `HTTP ${r.status} from /v1/auth/providers — ${body.detail ?? JSON.stringify(body)}.`,
|
|
4042
|
+
},
|
|
4043
|
+
],
|
|
4044
|
+
};
|
|
4045
|
+
}
|
|
4046
|
+
const providers = body.providers ?? [];
|
|
4047
|
+
if (providers.length === 0) {
|
|
4048
|
+
return {
|
|
4049
|
+
content: [
|
|
4050
|
+
{
|
|
4051
|
+
type: "text",
|
|
4052
|
+
text: `No OAuth providers configured for this tenant. The "Sign in with Google" / GitHub / etc. buttons would not work — only email/password login is wired up. Configure providers from the tenant dashboard before generating social-login UIs.`,
|
|
4053
|
+
},
|
|
4054
|
+
],
|
|
4055
|
+
};
|
|
4056
|
+
}
|
|
4057
|
+
const list = providers.map(p => `- **${p.name}** (\`${p.provider}\`)`).join("\n");
|
|
4058
|
+
return {
|
|
4059
|
+
content: [
|
|
4060
|
+
{
|
|
4061
|
+
type: "text",
|
|
4062
|
+
text: `# OAuth providers configured for this tenant\n\n${list}\n\nWire each one with \`GET /v1/auth/{provider}/url\` to get the authorization URL.`,
|
|
4063
|
+
},
|
|
4064
|
+
],
|
|
4065
|
+
};
|
|
4066
|
+
}
|
|
4067
|
+
catch (err) {
|
|
4068
|
+
return {
|
|
4069
|
+
content: [{ type: "text", text: `Network error: ${err}` }],
|
|
4070
|
+
};
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
4073
|
+
case "get_security_guide": {
|
|
4074
|
+
const detected = detectKeyType(API_KEY);
|
|
4075
|
+
return {
|
|
4076
|
+
content: [{ type: "text", text: SECURITY_GUIDE(detected.type) }],
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
4079
|
+
case "recommend_stack": {
|
|
4080
|
+
const appType = args.app_type;
|
|
4081
|
+
const detected = detectKeyType(API_KEY);
|
|
4082
|
+
return {
|
|
4083
|
+
content: [
|
|
4084
|
+
{ type: "text", text: STACK_RECOMMENDATION(appType, detected.type) },
|
|
4085
|
+
],
|
|
4086
|
+
};
|
|
4087
|
+
}
|
|
3366
4088
|
default:
|
|
3367
4089
|
throw new Error(`Unknown tool: ${name}`);
|
|
3368
4090
|
}
|
|
@@ -3381,10 +4103,14 @@ NEXT_PUBLIC_API_KEY=pk_live_your_public_key_here
|
|
|
3381
4103
|
});
|
|
3382
4104
|
// Start the server
|
|
3383
4105
|
async function main() {
|
|
3384
|
-
console.error(`🚀 Multi-tenant SaaS API MCP Server
|
|
4106
|
+
console.error(`🚀 Multi-tenant SaaS API MCP Server v${SERVER_VERSION}`);
|
|
3385
4107
|
console.error(`📡 API URL: ${API_URL}`);
|
|
3386
|
-
console.error(`🔑 API Key: ${API_KEY ? "Configured" : "Not configured"}`);
|
|
3387
|
-
console.error(`\
|
|
4108
|
+
console.error(`🔑 API Key: ${API_KEY ? "Configured" : "Not configured (set SAAS_API_KEY to enable authenticated calls)"}`);
|
|
4109
|
+
console.error(`\n👉 Recommended first calls (vibecoding flow):`);
|
|
4110
|
+
console.error(` 1. validate_credentials — verify key + detect type (pk_live_* vs sk_live_*)`);
|
|
4111
|
+
console.error(` 2. get_security_guide — safe-usage rules for the detected key type`);
|
|
4112
|
+
console.error(` 3. recommend_stack — pick a framework that won't leak your key`);
|
|
4113
|
+
console.error(`\nReference tools:`);
|
|
3388
4114
|
console.error(` - get_api_overview: Understand the API architecture`);
|
|
3389
4115
|
console.error(` - get_end_user_endpoints: Get endpoint documentation`);
|
|
3390
4116
|
console.error(` - get_sdk_template: Get TypeScript SDK template`);
|
|
@@ -3393,6 +4119,9 @@ async function main() {
|
|
|
3393
4119
|
console.error(` - search_endpoints: Search for endpoints`);
|
|
3394
4120
|
console.error(` - get_endpoint_details: Get specific endpoint info`);
|
|
3395
4121
|
console.error(` - get_openapi_spec: Get full OpenAPI spec`);
|
|
4122
|
+
console.error(`\nAuthenticated helpers:`);
|
|
4123
|
+
console.error(` - list_end_users (sk_live_* required)`);
|
|
4124
|
+
console.error(` - list_oauth_providers (any key)`);
|
|
3396
4125
|
const transport = new StdioServerTransport();
|
|
3397
4126
|
await server.connect(transport);
|
|
3398
4127
|
}
|