@genlobe/mcp-server 3.7.1 → 3.8.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/dist/index.js +370 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3731,18 +3731,44 @@ End-to-end recipe to wire a chatbot agent that answers from a knowledge base
|
|
|
3731
3731
|
populated by a snapshot of your custom-entity catalog (typical "store
|
|
3732
3732
|
assistant" / "product Q&A" / "support deflection" use case).
|
|
3733
3733
|
|
|
3734
|
-
##
|
|
3734
|
+
## ⭐ Public chatbots: use service-mode (ADR-0013) — the simple path
|
|
3735
3735
|
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3736
|
+
For a **public chatbot** (anonymous visitors, no human login), call the
|
|
3737
|
+
service-mode endpoint with your \`sk_live_*\` ONLY — no end-user JWT, no bot
|
|
3738
|
+
user, no email verification:
|
|
3739
|
+
|
|
3740
|
+
\`\`\`http
|
|
3741
|
+
POST /v1/server/agents/{agent_id}/chat
|
|
3742
|
+
X-API-Key: <your sk_live_*> ← server-side only
|
|
3743
|
+
X-Organization-Id: <org_id> (or organization_id in the body)
|
|
3744
|
+
\`\`\`
|
|
3745
|
+
|
|
3746
|
+
Body:
|
|
3747
|
+
\`\`\`json
|
|
3748
|
+
{ "messages": [{"role":"user","content":"do you have rice?"}], "session_id": "anon-visitor-abc" }
|
|
3749
|
+
\`\`\`
|
|
3750
|
+
|
|
3751
|
+
Usage is billed to the Tenant + Organization; \`user_id\` on the execution is
|
|
3752
|
+
NULL (no human). \`session_id\` (optional) groups one anonymous conversation.
|
|
3753
|
+
This is the **recommended path** for storefront/support bots — it removes the
|
|
3754
|
+
old bot-user + email-verification wall entirely.
|
|
3755
|
+
|
|
3756
|
+
Your Next.js \`/api/chat\` route handler calls this server-side; the browser
|
|
3757
|
+
only talks to \`/api/chat\`, never to Genlobe directly.
|
|
3758
|
+
|
|
3759
|
+
## Per-user chat (when there IS a logged-in human)
|
|
3760
|
+
|
|
3761
|
+
If the chat belongs to an authenticated end-user (e.g. an in-app assistant),
|
|
3762
|
+
use \`POST /v1/user/agents/{agent_id}/chat\` with that user's JWT instead — it
|
|
3763
|
+
attributes usage per user and applies their plan limits. Two ways to get the
|
|
3764
|
+
JWT:
|
|
3739
3765
|
|
|
3740
3766
|
- **(a) Real human end-user**: customer signs up via \`POST /v1/auth/register\`,
|
|
3741
3767
|
logs in, and chats with the agent. The JWT is theirs.
|
|
3742
|
-
- **(b) Bot service account**: register **one real bot user**
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3768
|
+
- **(b) Bot service account (legacy fallback)**: register **one real bot user**
|
|
3769
|
+
(real email you control), log in server-side, attach the JWT. Only needed if
|
|
3770
|
+
you specifically want per-"user" attribution for the bot. For public bots,
|
|
3771
|
+
prefer service-mode above — it is simpler.
|
|
3746
3772
|
|
|
3747
3773
|
**What is NOT allowed** (and what some agents mis-read as "no LLM bot is
|
|
3748
3774
|
possible"): inventing/synthesizing fake email addresses to bulk-create
|
|
@@ -3773,6 +3799,43 @@ LLM layer. The bot user path is still the canonical way to get a real LLM bot.
|
|
|
3773
3799
|
|
|
3774
3800
|
## Step-by-step
|
|
3775
3801
|
|
|
3802
|
+
### 0. Register the bot service-account user (only once per project)
|
|
3803
|
+
|
|
3804
|
+
The chat endpoint requires an end-user JWT. Register **one** real user that
|
|
3805
|
+
the server uses to obtain that JWT — this is not the same as inventing fake
|
|
3806
|
+
emails for bulk signups (which is what the security guide prohibits). Use a
|
|
3807
|
+
mailbox you actually own.
|
|
3808
|
+
|
|
3809
|
+
\`\`\`http
|
|
3810
|
+
POST /v1/auth/register
|
|
3811
|
+
Content-Type: application/json
|
|
3812
|
+
X-API-Key: <your pk_live_* ← register works with the public key>
|
|
3813
|
+
\`\`\`
|
|
3814
|
+
|
|
3815
|
+
Body:
|
|
3816
|
+
\`\`\`json
|
|
3817
|
+
{
|
|
3818
|
+
"email": "bot@yourshop.com",
|
|
3819
|
+
"password": "<32-char random string, store it once and never again in plain text>",
|
|
3820
|
+
"display_name": "Store Bot"
|
|
3821
|
+
}
|
|
3822
|
+
\`\`\`
|
|
3823
|
+
|
|
3824
|
+
Save the email and the password in your server-side env (\`.env.local\`):
|
|
3825
|
+
|
|
3826
|
+
\`\`\`
|
|
3827
|
+
BOT_USER_EMAIL=bot@yourshop.com
|
|
3828
|
+
BOT_USER_PASSWORD=<that random string>
|
|
3829
|
+
\`\`\`
|
|
3830
|
+
|
|
3831
|
+
From then on the server logs in this user on demand and caches the JWT for
|
|
3832
|
+
its lifetime. **Never** ship \`BOT_USER_PASSWORD\` to the browser. See the
|
|
3833
|
+
"Server-side TypeScript snippet" further down for the cached login helper.
|
|
3834
|
+
|
|
3835
|
+
If your project does not allow public chat (the bot is internal-only and
|
|
3836
|
+
every conversation already belongs to a logged-in human), skip this step
|
|
3837
|
+
and pass each user's own JWT to the chat endpoint.
|
|
3838
|
+
|
|
3776
3839
|
### 1. Create the KnowledgeBase
|
|
3777
3840
|
|
|
3778
3841
|
\`\`\`http
|
|
@@ -3926,11 +3989,28 @@ export async function refreshCatalogKB(productSchemaId: string, kbId: string, ex
|
|
|
3926
3989
|
});
|
|
3927
3990
|
}
|
|
3928
3991
|
|
|
3992
|
+
// Cached login for the bot service-account user. The JWT is reused until it
|
|
3993
|
+
// nears expiry; on next call we refresh server-side.
|
|
3994
|
+
let cachedBotJwt: { token: string; expiresAt: number } | null = null;
|
|
3995
|
+
|
|
3996
|
+
export async function getBotJwt(): Promise<string> {
|
|
3997
|
+
if (cachedBotJwt && cachedBotJwt.expiresAt > Date.now() + 60_000) {
|
|
3998
|
+
return cachedBotJwt.token;
|
|
3999
|
+
}
|
|
4000
|
+
const email = process.env.BOT_USER_EMAIL!;
|
|
4001
|
+
const password = process.env.BOT_USER_PASSWORD!;
|
|
4002
|
+
const r = await api('/v1/auth/login', { email, password });
|
|
4003
|
+
const ttl = (r.expires_in ?? 3600) * 1000;
|
|
4004
|
+
cachedBotJwt = { token: r.access_token, expiresAt: Date.now() + ttl };
|
|
4005
|
+
return r.access_token;
|
|
4006
|
+
}
|
|
4007
|
+
|
|
3929
4008
|
export async function chatWithAgent(agentId: string, message: string, conversationId?: string) {
|
|
4009
|
+
const jwt = await getBotJwt();
|
|
3930
4010
|
return api(\`/v1/user/agents/\${agentId}/chat\`, {
|
|
3931
4011
|
message,
|
|
3932
4012
|
...(conversationId ? { conversation_id: conversationId } : {}),
|
|
3933
|
-
});
|
|
4013
|
+
}, jwt);
|
|
3934
4014
|
}
|
|
3935
4015
|
\`\`\`
|
|
3936
4016
|
|
|
@@ -3938,12 +4018,19 @@ export async function chatWithAgent(agentId: string, message: string, conversati
|
|
|
3938
4018
|
|
|
3939
4019
|
- \`POST /v1/agents\` is the Tenant Dashboard endpoint — when configuring from
|
|
3940
4020
|
your scaffolding script, use \`sk_live_*\`. End-user chat (\`/v1/user/agents/{id}/chat\`)
|
|
3941
|
-
is reached with \`pk_live_*\` + the end-user's
|
|
4021
|
+
is reached with \`pk_live_*\` (or \`sk_live_*\` on the server) + the end-user's
|
|
4022
|
+
JWT (or the bot's, if you registered one in Step 0).
|
|
3942
4023
|
- Tool calling vs RAG: for MVP / low-frequency catalog updates, RAG via KB is
|
|
3943
4024
|
simpler. Switch to tool calling when the catalog updates faster than the
|
|
3944
4025
|
refresh cadence you can sustain.
|
|
3945
4026
|
- The bot does not write to the catalog and does not take orders unless you
|
|
3946
|
-
add a separate tool / API path. Keep the system prompt strict
|
|
4027
|
+
add a separate tool / API path. Keep the system prompt strict.
|
|
4028
|
+
- **Fallback for MVP without an LLM** — if the project explicitly cannot
|
|
4029
|
+
register a bot user (no email domain available yet), a deterministic
|
|
4030
|
+
keyword-search bot over the catalog is acceptable. Implement \`/api/chat\` as
|
|
4031
|
+
a server-side \`POST /v1/entity/records/search\` with the user's query as an
|
|
4032
|
+
\`ilike\` filter. Not equivalent to the LLM bot but ships a working surface;
|
|
4033
|
+
swap it for the LLM later.`;
|
|
3947
4034
|
}
|
|
3948
4035
|
const APP_SCAFFOLDS = {
|
|
3949
4036
|
pos: {
|
|
@@ -3986,18 +4073,23 @@ const APP_SCAFFOLDS = {
|
|
|
3986
4073
|
},
|
|
3987
4074
|
],
|
|
3988
4075
|
build_order: [
|
|
3989
|
-
"
|
|
3990
|
-
"
|
|
3991
|
-
"
|
|
3992
|
-
"
|
|
3993
|
-
"
|
|
3994
|
-
"
|
|
3995
|
-
"
|
|
3996
|
-
"
|
|
3997
|
-
"
|
|
3998
|
-
"
|
|
4076
|
+
"**0. Pre-flight.** Call `validate_credentials` to confirm sk_live_* and the org context. Restart any long-running uvicorn on the Genlobe backend if it predates today — the bulk endpoint is only available on backends started after the v3.6.2 merge.",
|
|
4077
|
+
"**1. Scaffold a Next.js 16 app router project under `vendy-app/` (or similar).** Tailwind + shadcn. `pnpm dev -p 3100` so it doesn't collide with the Genlobe Tenant UI on 3000. Verify `.gitignore` lists `.env*.local`, `.env`, `node_modules`, `.next`.",
|
|
4078
|
+
"**2. Create `lib/genlobe.server.ts` with `import 'server-only';` at the top.** Wrap fetch with X-API-Key + X-Organization-Id + optional JWT — see Section 10 of `get_authentication_flow`.",
|
|
4079
|
+
"**3. `.env.local`**: SAAS_API_URL, SAAS_API_KEY (sk_live_*), SAAS_ORGANIZATION_ID, BOT_USER_EMAIL, BOT_USER_PASSWORD, STORE_NAME, STORE_CURRENCY. Once schemas are created in step 5, append PRODUCT_SCHEMA_ID, CUSTOMER_SCHEMA_ID, ORDER_SCHEMA_ID.",
|
|
4080
|
+
"**4. Bot service-account user (Step 0 of `get_chatbot_setup_recipe`).** Register `bot@<your-shop>` via `POST /v1/auth/register`. Save credentials in `.env.local`. The server logs in this user on demand to obtain the chat JWT — never the human's.",
|
|
4081
|
+
"**5. Bootstrap script (`scripts/bootstrap.ts`)**: idempotent. Reads each schema slug from `.env.local`; if missing, calls `get_entity_schema_recipe({entity_type:'product|customer|order'})` shape and `POST /v1/entity/schemas`; appends the returned id to `.env.local`. Then `POST /v1/entity/records/bulk` with 5-10 sample products in ONE call (this is the saboreo demo of bulk insert).",
|
|
4082
|
+
"**6. Owner login**: `/admin/login` → server-side `POST /v1/auth/login` → set `genlobe_jwt` cookie httpOnly. Use the email the human gave you (NOT the bot user).",
|
|
4083
|
+
"**7. Admin layout gate**: `app/(admin)/layout.tsx` reads `genlobe_jwt` cookie, redirects to `/admin/login` if missing.",
|
|
4084
|
+
"**8. Products vertical slice end-to-end first**: list, create, edit, delete. Confirms the full stack works before fanning out to other entities.",
|
|
4085
|
+
"**9. Sales (`/admin/sales/new`)**: customer autocomplete (search customers with `ilike`), product picker (paginated), line items with `name_snapshot` + `price_snapshot` per item, total auto-calc. POST one `order` record. Stock decrement is best-effort (continue if it fails — the sale is recorded).",
|
|
4086
|
+
"**10. Sales list (`/admin/sales`) + per-customer history (`/admin/customers/{id}`)**: cross-schema join (`POST /v1/entity/records/search` with `join` clause on `customer_id`/`product_id`) to pull customer / product names in the same call.",
|
|
4087
|
+
"**11. Dashboard KPIs (`/admin`)**: today's revenue, top 3 products, new customers this week. Client-side sums for MVP; flag any KPI taking >300ms as a candidate for server-side aggregation later.",
|
|
4088
|
+
"**12. KB + bot setup (`scripts/sync-catalog-kb.ts`)**: snapshot the catalog to plain text, create KB with `rag_role: 'product_catalog'`, upload document, create the Store Assistant agent with the same `rag_role`. Add a button in `/admin` that re-runs this script when the catalog changes.",
|
|
4089
|
+
"**13. Public storefront (`/`) + `/chat`**: storefront uses the catalog directly via a server route (no auth on the customer); `/chat` UI posts to `/api/chat` which calls `chatWithAgent(agentId, message, conversationId)` — JWT handled server-side by the cached bot login.",
|
|
4090
|
+
"**14. Smoke**: open `http://localhost:3100`, place one test order, confirm it shows up in `/admin/sales`, ask the bot \"¿tienen X?\", verify it answers from the KB.",
|
|
3999
4091
|
],
|
|
4000
|
-
notes: "Stock decrement on sale is intentionally NOT automatic — for MVP
|
|
4092
|
+
notes: "Stock decrement on sale is intentionally NOT automatic to the level of a DB transaction — for MVP it is best-effort. If the stock UPDATE fails, the order is still saved. The owner reconciles manually. This avoids the double-decrement and partial-failure bugs that a naive transactional implementation hits without a row-lock primitive.\n\nReal-life pitfall (Vendy build, 2026-05-26): an agent built this 95% successfully but skipped the LLM bot setup because it mis-read the security guidance as 'no bot users allowed'. The Step 0 in `get_chatbot_setup_recipe` and the bot service-account clarification in `get_authentication_flow` exist to prevent that exact regression. If you find yourself building a keyword-search fallback because LLM 'isn't possible', re-read both — the LLM bot IS possible with one registered bot user, and the recipes spell out the ceremony.",
|
|
4001
4093
|
},
|
|
4002
4094
|
crm: {
|
|
4003
4095
|
template: "crm",
|
|
@@ -4233,6 +4325,87 @@ ${s.notes ? `## Notes\n\n${s.notes}\n` : ""}
|
|
|
4233
4325
|
- For the auth shape (sk_live_* / pk_live_* + cookies), call \`get_authentication_flow()\` and \`get_security_guide()\`.`;
|
|
4234
4326
|
}
|
|
4235
4327
|
// =============================================================================
|
|
4328
|
+
// Supabase → Genlobe migration recipe (G10 from the DX audit).
|
|
4329
|
+
// =============================================================================
|
|
4330
|
+
function SUPABASE_MIGRATION_RECIPE() {
|
|
4331
|
+
return `# Supabase → Genlobe migration recipe
|
|
4332
|
+
|
|
4333
|
+
Move a Supabase project's data into Genlobe. The tricky part is **users +
|
|
4334
|
+
foreign keys**; data tables are the easy part (bulk insert). Do it in this
|
|
4335
|
+
order so foreign keys never dangle.
|
|
4336
|
+
|
|
4337
|
+
## Mental model (ADR-0012)
|
|
4338
|
+
|
|
4339
|
+
- Genlobe \`users\` (native, auth) ← Supabase \`auth.users\`. Auth stays in the
|
|
4340
|
+
native table; do NOT model users as a custom entity.
|
|
4341
|
+
- Genlobe custom entities ← Supabase \`public.*\` data tables.
|
|
4342
|
+
- One Genlobe **Organization = one namespace** (≈ a Supabase project). Your
|
|
4343
|
+
app's *customers* are rows in a custom entity, NOT Organizations.
|
|
4344
|
+
|
|
4345
|
+
## Step 1 — Import users FIRST (preserve UUIDs)
|
|
4346
|
+
|
|
4347
|
+
\`\`\`http
|
|
4348
|
+
POST /v1/server/users/import (sk_live_* — server/agent callable)
|
|
4349
|
+
\`\`\`
|
|
4350
|
+
\`\`\`json
|
|
4351
|
+
{
|
|
4352
|
+
"organization_id": "<target org>",
|
|
4353
|
+
"users": [
|
|
4354
|
+
{
|
|
4355
|
+
"id": "<the Supabase auth.users.id — PRESERVE IT>",
|
|
4356
|
+
"email": "ada@shop.com",
|
|
4357
|
+
"password_hash": "<Supabase encrypted_password — bcrypt, imported as-is>",
|
|
4358
|
+
"profile_data": { "...": "from raw_user_meta_data" },
|
|
4359
|
+
"is_email_verified": true,
|
|
4360
|
+
"status": "active"
|
|
4361
|
+
}
|
|
4362
|
+
]
|
|
4363
|
+
}
|
|
4364
|
+
\`\`\`
|
|
4365
|
+
|
|
4366
|
+
- **Preserve \`id\`**: pass the Supabase UUID so every foreign key that pointed
|
|
4367
|
+
at it stays valid. This is the single most important step.
|
|
4368
|
+
- **\`password_hash\`**: Supabase and Genlobe both use bcrypt, so the hash is
|
|
4369
|
+
imported as-is — users keep their password, NO reset.
|
|
4370
|
+
- Capped at 500 per call, all-or-nothing. Chunk larger sets.
|
|
4371
|
+
|
|
4372
|
+
## Step 2 — Create the entity schemas for the data tables
|
|
4373
|
+
|
|
4374
|
+
For each Supabase \`public.<table>\`, define a custom entity schema. Use
|
|
4375
|
+
\`get_entity_schema_recipe\` for common shapes. Map a column that was a FK to
|
|
4376
|
+
\`auth.users\` as a \`reference\` field with \`target_schema_slug: "users"\`
|
|
4377
|
+
(ADR-0009) — it validates against the native users table.
|
|
4378
|
+
|
|
4379
|
+
\`POST /v1/entity/schemas\` (one per table). Valid field types: string, text,
|
|
4380
|
+
integer, float, boolean, datetime, enum, email, url, phone, reference.
|
|
4381
|
+
(No \`array\`, no \`number\` — see get_entity_schema_recipe.)
|
|
4382
|
+
|
|
4383
|
+
## Step 3 — Bulk-insert the rows
|
|
4384
|
+
|
|
4385
|
+
\`\`\`http
|
|
4386
|
+
POST /v1/entity/records/bulk
|
|
4387
|
+
\`\`\`
|
|
4388
|
+
Because you preserved user UUIDs in Step 1, any \`reference→users\` field in
|
|
4389
|
+
these rows already points at a valid user. For FKs between data tables,
|
|
4390
|
+
preserve those source ids too (store them in a field) or rewrite them with a
|
|
4391
|
+
mapping you keep while importing.
|
|
4392
|
+
|
|
4393
|
+
## Step 4 — Verify
|
|
4394
|
+
|
|
4395
|
+
- Spot-check: a migrated user can log in with their old password.
|
|
4396
|
+
- A migrated row's \`reference→users\` resolves (the user exists).
|
|
4397
|
+
- Counts match the Supabase source.
|
|
4398
|
+
|
|
4399
|
+
## Auth for the import endpoints
|
|
4400
|
+
|
|
4401
|
+
All three steps accept \`sk_live_*\` from a server/agent:
|
|
4402
|
+
- Users: \`POST /v1/server/users/import\` (server-scoped, sk_live_*). The
|
|
4403
|
+
dashboard variant \`POST /v1/dashboard/organizations/users/import\` (TenantMember
|
|
4404
|
+
auth) does the same thing for a human in the dashboard.
|
|
4405
|
+
- Schemas: \`POST /v1/entity/schemas\`.
|
|
4406
|
+
- Rows: \`POST /v1/entity/records/bulk\`.`;
|
|
4407
|
+
}
|
|
4408
|
+
// =============================================================================
|
|
4236
4409
|
// MCP Server Implementation
|
|
4237
4410
|
// =============================================================================
|
|
4238
4411
|
const server = new Server({
|
|
@@ -4570,6 +4743,24 @@ developer says "I want to build X" before writing code.`,
|
|
|
4570
4743
|
required: ["template"],
|
|
4571
4744
|
},
|
|
4572
4745
|
},
|
|
4746
|
+
{
|
|
4747
|
+
name: "get_supabase_migration_recipe",
|
|
4748
|
+
description: `Step-by-step recipe to migrate a Supabase project into Genlobe (G10). Covers the tricky part — users + foreign keys — in the correct order: import users FIRST preserving their UUIDs (so FKs stay valid) and their bcrypt password hash (Supabase + Genlobe both use bcrypt — no password reset), then create entity schemas mapping FK columns to reference fields (target_schema_slug: "users"), then bulk-insert rows. Explains the ADR-0012 mental model (Organization = namespace; your app's customers are rows, not Organizations). Pure data; no network call.`,
|
|
4749
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
4750
|
+
},
|
|
4751
|
+
{
|
|
4752
|
+
name: "invite_organization_owner",
|
|
4753
|
+
description: `Invite the owner of an Organization (F2/F4). Creates the owner end-user and EMAILS them their sign-in credentials. The agent NEVER sees the password — it is delivered by email only (MCP "no raw secrets" invariant). Ask the human for the owner's REAL email; never invent one (a bounced address gets SES-suppressed — incident #168). Requires sk_live_*. Returns { user_id, email, email_sent }. If email_sent is false, tell the human to grab the temporary credential from the Tenant Dashboard. Calls POST /v1/server/organizations/invite-owner.`,
|
|
4754
|
+
inputSchema: {
|
|
4755
|
+
type: "object",
|
|
4756
|
+
properties: {
|
|
4757
|
+
organization_id: { type: "string", description: "UUID of the Organization the owner will administer." },
|
|
4758
|
+
email: { type: "string", description: "The owner's REAL email address. Ask the human — never fabricate it." },
|
|
4759
|
+
display_name: { type: "string", description: "Optional display name for the owner." },
|
|
4760
|
+
},
|
|
4761
|
+
required: ["organization_id", "email"],
|
|
4762
|
+
},
|
|
4763
|
+
},
|
|
4573
4764
|
],
|
|
4574
4765
|
};
|
|
4575
4766
|
});
|
|
@@ -4980,6 +5171,12 @@ Use search_endpoints tool to find available endpoints.`,
|
|
|
4980
5171
|
> 2. **Do NOT register a new end-user with an email you generated.** Inventing an email (e.g. \`admin@<their-domain>\`, \`seed@<their-domain>\`) leads to bounces. AWS SES then suppresses the address at the account level and locks the tenant out of email delivery for that destination — recovery requires manual operator action. This is exactly how production incident #168 happened.
|
|
4981
5172
|
> 3. **Schemas don't need a JWT.** \`/v1/entity/schemas\` is a tenant-admin operation: \`sk_live_*\` alone is enough (see issue #177). Read \`get_endpoint_details\` for the schemas endpoint before assuming you need to register a user.
|
|
4982
5173
|
> 4. If the human says \"register a user with this email: ...\" — fine, that's an explicit instruction. Inventing one is not.
|
|
5174
|
+
>
|
|
5175
|
+
> ### Bot service-account user (LLM chat surface)
|
|
5176
|
+
>
|
|
5177
|
+
> **Public chatbot surfaces are an exception that is fully permitted**: you can — and should — register **one real bot service-account user** to obtain the JWT the agent chat endpoint requires. The human is implicitly authorizing this when they ask for "an LLM chatbot the customers can use". Use a real email you control (e.g. \`bot@<their-shop-domain>\` or a personal mailbox). Save the credentials in the server-side env (\`.env.local\` with \`BOT_USER_EMAIL\` + \`BOT_USER_PASSWORD\`), log in server-side, attach the JWT to incoming chat requests. See \`get_chatbot_setup_recipe()\` Step 0 for the full ceremony.
|
|
5178
|
+
>
|
|
5179
|
+
> Registering **one** bot account is not the same thing as "inventing emails in bulk" — that distinction matters.
|
|
4983
5180
|
|
|
4984
5181
|
---
|
|
4985
5182
|
|
|
@@ -5207,7 +5404,122 @@ await fetch('/v1/auth/logout', {
|
|
|
5207
5404
|
|
|
5208
5405
|
localStorage.removeItem('access_token');
|
|
5209
5406
|
localStorage.removeItem('refresh_token');
|
|
5210
|
-
|
|
5407
|
+
\`\`\`
|
|
5408
|
+
|
|
5409
|
+
---
|
|
5410
|
+
|
|
5411
|
+
## 10. Server-side auth pattern (sk_live_* + cookies httpOnly) — Next.js App Router
|
|
5412
|
+
|
|
5413
|
+
When the app uses a **secret key** (\`sk_live_*\`), the browser must never see the key OR the API base URL. All Genlobe calls go through your own \`/api/*\` routes on the Next.js server. End-user sessions are kept in **httpOnly cookies**, not \`localStorage\`.
|
|
5414
|
+
|
|
5415
|
+
### Project layout (recommended)
|
|
5416
|
+
|
|
5417
|
+
\`\`\`
|
|
5418
|
+
app/
|
|
5419
|
+
├─ (admin)/ Owner dashboard, gated
|
|
5420
|
+
│ ├─ layout.tsx Reads cookie, redirects to /admin/login if missing
|
|
5421
|
+
│ ├─ admin/login/page.tsx
|
|
5422
|
+
│ └─ admin/page.tsx
|
|
5423
|
+
├─ (public)/ No auth, anonymous customer surface
|
|
5424
|
+
│ ├─ page.tsx Storefront / POS / catalog
|
|
5425
|
+
│ └─ chat/page.tsx Public chatbot UI
|
|
5426
|
+
├─ api/
|
|
5427
|
+
│ ├─ admin/login/route.ts POST → server-side call to /v1/auth/login, sets cookie
|
|
5428
|
+
│ ├─ admin/logout/route.ts Clears cookie
|
|
5429
|
+
│ └─ chat/route.ts POST → server-side call to /v1/user/agents/{id}/chat with bot JWT
|
|
5430
|
+
└─ middleware.ts Gates (admin)/*; refreshes bot JWT in background
|
|
5431
|
+
lib/
|
|
5432
|
+
└─ genlobe.server.ts 'server-only', wraps the API
|
|
5433
|
+
.env.local SAAS_API_URL, SAAS_API_KEY, SAAS_ORGANIZATION_ID, BOT_USER_EMAIL, BOT_USER_PASSWORD
|
|
5434
|
+
\`\`\`
|
|
5435
|
+
|
|
5436
|
+
### lib/genlobe.server.ts
|
|
5437
|
+
|
|
5438
|
+
\`\`\`typescript
|
|
5439
|
+
import 'server-only';
|
|
5440
|
+
|
|
5441
|
+
const API = process.env.SAAS_API_URL!; // e.g. http://localhost:8001
|
|
5442
|
+
const KEY = process.env.SAAS_API_KEY!; // sk_live_* — server only
|
|
5443
|
+
const ORG = process.env.SAAS_ORGANIZATION_ID!; // organization scope
|
|
5444
|
+
|
|
5445
|
+
export async function genlobeApi<T = any>(
|
|
5446
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
5447
|
+
path: string,
|
|
5448
|
+
body?: any,
|
|
5449
|
+
jwt?: string
|
|
5450
|
+
): Promise<T> {
|
|
5451
|
+
const res = await fetch(\`\${API}\${path}\`, {
|
|
5452
|
+
method,
|
|
5453
|
+
headers: {
|
|
5454
|
+
'Content-Type': 'application/json',
|
|
5455
|
+
'X-API-Key': KEY,
|
|
5456
|
+
'X-Organization-Id': ORG,
|
|
5457
|
+
...(jwt ? { Authorization: \`Bearer \${jwt}\` } : {}),
|
|
5458
|
+
},
|
|
5459
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
5460
|
+
cache: 'no-store',
|
|
5461
|
+
});
|
|
5462
|
+
if (!res.ok) throw new Error(\`Genlobe \${res.status}: \${await res.text()}\`);
|
|
5463
|
+
return res.json();
|
|
5464
|
+
}
|
|
5465
|
+
\`\`\`
|
|
5466
|
+
|
|
5467
|
+
### app/api/admin/login/route.ts
|
|
5468
|
+
|
|
5469
|
+
\`\`\`typescript
|
|
5470
|
+
import { NextResponse } from 'next/server';
|
|
5471
|
+
import { cookies } from 'next/headers';
|
|
5472
|
+
import { genlobeApi } from '@/lib/genlobe.server';
|
|
5473
|
+
|
|
5474
|
+
export async function POST(req: Request) {
|
|
5475
|
+
const { email, password } = await req.json();
|
|
5476
|
+
const { access_token, refresh_token, user } = await genlobeApi(
|
|
5477
|
+
'POST',
|
|
5478
|
+
'/v1/auth/login',
|
|
5479
|
+
{ email, password }
|
|
5480
|
+
);
|
|
5481
|
+
|
|
5482
|
+
const jar = cookies();
|
|
5483
|
+
jar.set('genlobe_jwt', access_token, {
|
|
5484
|
+
httpOnly: true, secure: process.env.NODE_ENV === 'production',
|
|
5485
|
+
sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24,
|
|
5486
|
+
});
|
|
5487
|
+
jar.set('genlobe_refresh', refresh_token, {
|
|
5488
|
+
httpOnly: true, secure: process.env.NODE_ENV === 'production',
|
|
5489
|
+
sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24 * 30,
|
|
5490
|
+
});
|
|
5491
|
+
|
|
5492
|
+
return NextResponse.json({ user });
|
|
5493
|
+
}
|
|
5494
|
+
\`\`\`
|
|
5495
|
+
|
|
5496
|
+
### app/(admin)/layout.tsx (gate)
|
|
5497
|
+
|
|
5498
|
+
\`\`\`typescript
|
|
5499
|
+
import { cookies } from 'next/headers';
|
|
5500
|
+
import { redirect } from 'next/navigation';
|
|
5501
|
+
|
|
5502
|
+
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
|
5503
|
+
const jwt = cookies().get('genlobe_jwt')?.value;
|
|
5504
|
+
if (!jwt) redirect('/admin/login');
|
|
5505
|
+
return <>{children}</>;
|
|
5506
|
+
}
|
|
5507
|
+
\`\`\`
|
|
5508
|
+
|
|
5509
|
+
### .gitignore (critical)
|
|
5510
|
+
|
|
5511
|
+
\`\`\`
|
|
5512
|
+
.env*.local
|
|
5513
|
+
.env
|
|
5514
|
+
\`\`\`
|
|
5515
|
+
|
|
5516
|
+
Next.js boilerplate puts this in by default if you used \`create-next-app\`. **Always verify it before the first commit** — the \`sk_live_*\` and \`BOT_USER_PASSWORD\` live in \`.env.local\` and must never leave your machine.
|
|
5517
|
+
|
|
5518
|
+
### Three rules that keep this safe
|
|
5519
|
+
|
|
5520
|
+
1. \`SAAS_API_KEY\` env var is **never** prefixed with \`NEXT_PUBLIC_\`. Same for \`BOT_USER_PASSWORD\` and \`SAAS_ORGANIZATION_ID\`.
|
|
5521
|
+
2. The wrapper file (\`lib/genlobe.server.ts\`) starts with \`import 'server-only'\`. If a client component ever imports it, the Next.js build fails with a clear message — that's the contract.
|
|
5522
|
+
3. The browser only talks to *your* \`/api/*\` routes, never to the Genlobe host directly.`,
|
|
5211
5523
|
},
|
|
5212
5524
|
],
|
|
5213
5525
|
};
|
|
@@ -5640,6 +5952,41 @@ type to pick.`,
|
|
|
5640
5952
|
content: [{ type: "text", text: APP_SCAFFOLD(template) }],
|
|
5641
5953
|
};
|
|
5642
5954
|
}
|
|
5955
|
+
case "get_supabase_migration_recipe": {
|
|
5956
|
+
return {
|
|
5957
|
+
content: [{ type: "text", text: SUPABASE_MIGRATION_RECIPE() }],
|
|
5958
|
+
};
|
|
5959
|
+
}
|
|
5960
|
+
case "invite_organization_owner": {
|
|
5961
|
+
if (!API_KEY) {
|
|
5962
|
+
return {
|
|
5963
|
+
content: [{ type: "text", text: `❌ Cannot invite owner: SAAS_API_KEY is not configured. A secret key (sk_live_*) is required.` }],
|
|
5964
|
+
};
|
|
5965
|
+
}
|
|
5966
|
+
const { organization_id, email, display_name } = args;
|
|
5967
|
+
try {
|
|
5968
|
+
const r = await fetch(`${API_URL}/v1/server/organizations/invite-owner`, {
|
|
5969
|
+
method: "POST",
|
|
5970
|
+
headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
|
|
5971
|
+
body: JSON.stringify({ organization_id, email, display_name }),
|
|
5972
|
+
});
|
|
5973
|
+
const body = await r.json();
|
|
5974
|
+
if (!r.ok) {
|
|
5975
|
+
return {
|
|
5976
|
+
content: [{ type: "text", text: `HTTP ${r.status} from /v1/server/organizations/invite-owner — ${body.detail ?? JSON.stringify(body)}.\n\nNeeds a secret key (sk_live_*). The org must belong to this key's tenant.` }],
|
|
5977
|
+
};
|
|
5978
|
+
}
|
|
5979
|
+
const note = body.email_sent
|
|
5980
|
+
? "✅ The owner has been emailed their sign-in credentials. They can now sign in at the Org Owner Dashboard and change their password."
|
|
5981
|
+
: "⚠️ Owner created, but the credentials email could NOT be sent (SMTP not configured). Ask the human to retrieve the temporary credential from the Tenant Dashboard → Organizations → invite owner. The password is never exposed here.";
|
|
5982
|
+
return {
|
|
5983
|
+
content: [{ type: "text", text: `Owner invited.\n${JSON.stringify(body, null, 2)}\n\n${note}` }],
|
|
5984
|
+
};
|
|
5985
|
+
}
|
|
5986
|
+
catch (err) {
|
|
5987
|
+
return { content: [{ type: "text", text: `Network error: ${err}` }] };
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5643
5990
|
default:
|
|
5644
5991
|
throw new Error(`Unknown tool: ${name}`);
|
|
5645
5992
|
}
|
package/package.json
CHANGED