@genlobe/mcp-server 3.7.0 → 3.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +248 -40
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3504,11 +3504,11 @@ const ENTITY_SCHEMA_RECIPES = {
|
|
|
3504
3504
|
description: "Catalog product with price, stock, category, description.",
|
|
3505
3505
|
fields_definition: {
|
|
3506
3506
|
name: { type: "string", required: true, max_length: 200 },
|
|
3507
|
-
price: { type: "
|
|
3508
|
-
stock: { type: "
|
|
3507
|
+
price: { type: "float", required: true, min: 0 },
|
|
3508
|
+
stock: { type: "integer", required: true, min: 0, default: 0 },
|
|
3509
3509
|
currency: { type: "string", required: false, default: "USD", max_length: 3 },
|
|
3510
3510
|
category: { type: "string", required: false, max_length: 100 },
|
|
3511
|
-
description: { type: "
|
|
3511
|
+
description: { type: "text", required: false },
|
|
3512
3512
|
is_active: { type: "boolean", required: false, default: true },
|
|
3513
3513
|
},
|
|
3514
3514
|
bulk_seed_example: [
|
|
@@ -3525,15 +3525,15 @@ const ENTITY_SCHEMA_RECIPES = {
|
|
|
3525
3525
|
fields_definition: {
|
|
3526
3526
|
name: { type: "string", required: true, max_length: 200 },
|
|
3527
3527
|
phone: { type: "string", required: false, max_length: 50 },
|
|
3528
|
-
email: { type: "
|
|
3529
|
-
notes: { type: "
|
|
3530
|
-
tags: { type: "
|
|
3528
|
+
email: { type: "email", required: false, max_length: 200 },
|
|
3529
|
+
notes: { type: "text", required: false },
|
|
3530
|
+
tags: { type: "text", required: false },
|
|
3531
3531
|
},
|
|
3532
3532
|
bulk_seed_example: [
|
|
3533
3533
|
{ name: "Ana Pérez", phone: "+1-809-555-0100" },
|
|
3534
3534
|
{ name: "Walk-in", notes: "no-data anonymous buyer" },
|
|
3535
3535
|
],
|
|
3536
|
-
notes: "Common mistake: registering each store customer as a Genlobe end-user. Don't — that requires real email + SES delivery. Keep store customers as this custom entity unless they need to log into the storefront.",
|
|
3536
|
+
notes: "Common mistake: registering each store customer as a Genlobe end-user. Don't — that requires real email + SES delivery. Keep store customers as this custom entity unless they need to log into the storefront. `tags` is a comma-separated string (the schema validator does NOT support `array` as a field type — see the global notes at the top of every recipe).",
|
|
3537
3537
|
},
|
|
3538
3538
|
order: {
|
|
3539
3539
|
slug: "order",
|
|
@@ -3546,28 +3546,17 @@ const ENTITY_SCHEMA_RECIPES = {
|
|
|
3546
3546
|
required: false,
|
|
3547
3547
|
on_delete: "set_null",
|
|
3548
3548
|
},
|
|
3549
|
-
|
|
3550
|
-
type: "
|
|
3549
|
+
items_json: {
|
|
3550
|
+
type: "text",
|
|
3551
3551
|
required: true,
|
|
3552
|
-
item_schema: {
|
|
3553
|
-
product_id: {
|
|
3554
|
-
type: "reference",
|
|
3555
|
-
target_schema_slug: "product",
|
|
3556
|
-
required: true,
|
|
3557
|
-
on_delete: "restrict",
|
|
3558
|
-
},
|
|
3559
|
-
name_snapshot: { type: "string", required: true },
|
|
3560
|
-
price_snapshot: { type: "number", required: true, min: 0 },
|
|
3561
|
-
qty: { type: "number", required: true, min: 1 },
|
|
3562
|
-
},
|
|
3563
3552
|
},
|
|
3564
|
-
total: { type: "
|
|
3553
|
+
total: { type: "float", required: true, min: 0 },
|
|
3565
3554
|
currency: { type: "string", required: false, default: "USD" },
|
|
3566
3555
|
paid: { type: "boolean", required: true, default: false },
|
|
3567
3556
|
paid_at: { type: "datetime", required: false },
|
|
3568
|
-
notes: { type: "
|
|
3557
|
+
notes: { type: "text", required: false },
|
|
3569
3558
|
},
|
|
3570
|
-
notes: "
|
|
3559
|
+
notes: "**Line items modeling — important.** Genlobe's schema validator does NOT support `array` as a field type (validated types: string, integer, float, boolean, datetime, text, enum, email, url, phone, reference). Two ways to model line items:\n\n**Option A — simple, recommended for MVP**: store the items as a JSON string in `items_json` (type=text). Parse client-side. Each item should already include `product_id`, `name_snapshot`, `price_snapshot`, `qty`. Trade-off: you cannot search/filter by items via `POST /v1/entity/records/search` — items are opaque to the engine.\n\n**Option B — queryable, for production**: create a separate `order_item` schema with `order_id: reference->order`, `product_id: reference->product`, `name_snapshot`, `price_snapshot`, `qty`. Cross-schema joins (ADR-0009) make `\"top 5 products sold this month\"` a single search call.\n\nEither way: **always snapshot `name` and `price` at write time** so editing the product later does NOT mutate past orders.",
|
|
3571
3560
|
},
|
|
3572
3561
|
post: {
|
|
3573
3562
|
slug: "post",
|
|
@@ -3583,7 +3572,7 @@ const ENTITY_SCHEMA_RECIPES = {
|
|
|
3583
3572
|
enum: ["draft", "published", "archived"],
|
|
3584
3573
|
default: "draft",
|
|
3585
3574
|
},
|
|
3586
|
-
tags: { type: "
|
|
3575
|
+
tags: { type: "text", required: false },
|
|
3587
3576
|
published_at: { type: "datetime", required: false },
|
|
3588
3577
|
cover_image_url: { type: "string", required: false },
|
|
3589
3578
|
},
|
|
@@ -3658,7 +3647,7 @@ const ENTITY_SCHEMA_RECIPES = {
|
|
|
3658
3647
|
fields_definition: {
|
|
3659
3648
|
title: { type: "string", required: false, max_length: 200 },
|
|
3660
3649
|
body: { type: "string", required: true },
|
|
3661
|
-
tags: { type: "
|
|
3650
|
+
tags: { type: "text", required: false },
|
|
3662
3651
|
is_pinned: { type: "boolean", required: false, default: false },
|
|
3663
3652
|
},
|
|
3664
3653
|
notes: "Perfect for ADR-0007 row-level scope: regular end-users see only their own notes via `GET /v1/entity/records/mine` (no extra filter needed in the frontend).",
|
|
@@ -3685,6 +3674,14 @@ function ENTITY_SCHEMA_RECIPE(entityType) {
|
|
|
3685
3674
|
|
|
3686
3675
|
${recipe.description}
|
|
3687
3676
|
|
|
3677
|
+
## Schema field types — what the backend actually accepts
|
|
3678
|
+
|
|
3679
|
+
The Custom Entities validator only accepts these types in \`fields_definition\`:
|
|
3680
|
+
|
|
3681
|
+
\`string\`, \`text\`, \`integer\`, \`float\`, \`boolean\`, \`datetime\`, \`enum\`, \`email\`, \`url\`, \`phone\`, \`reference\` (ADR-0009).
|
|
3682
|
+
|
|
3683
|
+
**There is no \`array\` type and no \`number\` type.** Use \`integer\`/\`float\` instead of \`number\`. For list-shaped data either (a) store as a comma-separated \`text\` (simple, not queryable), or (b) model as a separate entity with a \`reference\` field pointing back (queryable, ADR-0009 cross-schema search).
|
|
3684
|
+
|
|
3688
3685
|
## Step 1 — Create the schema
|
|
3689
3686
|
|
|
3690
3687
|
\`\`\`http
|
|
@@ -3734,6 +3731,30 @@ End-to-end recipe to wire a chatbot agent that answers from a knowledge base
|
|
|
3734
3731
|
populated by a snapshot of your custom-entity catalog (typical "store
|
|
3735
3732
|
assistant" / "product Q&A" / "support deflection" use case).
|
|
3736
3733
|
|
|
3734
|
+
## Auth context — clarification (frequent confusion)
|
|
3735
|
+
|
|
3736
|
+
The end-user chat endpoint \`POST /v1/user/agents/{agent_id}/chat\` requires
|
|
3737
|
+
an end-user JWT. **There are two legitimate ways to obtain that JWT**, and a
|
|
3738
|
+
public-facing bot needs option (b):
|
|
3739
|
+
|
|
3740
|
+
- **(a) Real human end-user**: customer signs up via \`POST /v1/auth/register\`,
|
|
3741
|
+
logs in, and chats with the agent. The JWT is theirs.
|
|
3742
|
+
- **(b) Bot service account**: register **one real bot user** for the chat
|
|
3743
|
+
surface (use a real email you control, e.g. \`bot@yourshop.com\`), keep its
|
|
3744
|
+
credentials in your server env, log in server-side, attach the resulting
|
|
3745
|
+
JWT to incoming chat requests. This is what "register a bot user" means.
|
|
3746
|
+
|
|
3747
|
+
**What is NOT allowed** (and what some agents mis-read as "no LLM bot is
|
|
3748
|
+
possible"): inventing/synthesizing fake email addresses to bulk-create
|
|
3749
|
+
end-users on the fly. That bypass would route through SES and burn the
|
|
3750
|
+
sender reputation — exactly the incident pattern of #168. Registering one
|
|
3751
|
+
or a few real bot accounts that you actually own is fine.
|
|
3752
|
+
|
|
3753
|
+
If you cannot or do not want to register a bot user, an acceptable fallback
|
|
3754
|
+
for MVP is a deterministic keyword search over the catalog (no LLM, no JWT
|
|
3755
|
+
required) — useful when the storefront has to be live before you set up the
|
|
3756
|
+
LLM layer. The bot user path is still the canonical way to get a real LLM bot.
|
|
3757
|
+
|
|
3737
3758
|
## Architecture in one diagram
|
|
3738
3759
|
|
|
3739
3760
|
\`\`\`
|
|
@@ -3752,6 +3773,43 @@ assistant" / "product Q&A" / "support deflection" use case).
|
|
|
3752
3773
|
|
|
3753
3774
|
## Step-by-step
|
|
3754
3775
|
|
|
3776
|
+
### 0. Register the bot service-account user (only once per project)
|
|
3777
|
+
|
|
3778
|
+
The chat endpoint requires an end-user JWT. Register **one** real user that
|
|
3779
|
+
the server uses to obtain that JWT — this is not the same as inventing fake
|
|
3780
|
+
emails for bulk signups (which is what the security guide prohibits). Use a
|
|
3781
|
+
mailbox you actually own.
|
|
3782
|
+
|
|
3783
|
+
\`\`\`http
|
|
3784
|
+
POST /v1/auth/register
|
|
3785
|
+
Content-Type: application/json
|
|
3786
|
+
X-API-Key: <your pk_live_* ← register works with the public key>
|
|
3787
|
+
\`\`\`
|
|
3788
|
+
|
|
3789
|
+
Body:
|
|
3790
|
+
\`\`\`json
|
|
3791
|
+
{
|
|
3792
|
+
"email": "bot@yourshop.com",
|
|
3793
|
+
"password": "<32-char random string, store it once and never again in plain text>",
|
|
3794
|
+
"display_name": "Store Bot"
|
|
3795
|
+
}
|
|
3796
|
+
\`\`\`
|
|
3797
|
+
|
|
3798
|
+
Save the email and the password in your server-side env (\`.env.local\`):
|
|
3799
|
+
|
|
3800
|
+
\`\`\`
|
|
3801
|
+
BOT_USER_EMAIL=bot@yourshop.com
|
|
3802
|
+
BOT_USER_PASSWORD=<that random string>
|
|
3803
|
+
\`\`\`
|
|
3804
|
+
|
|
3805
|
+
From then on the server logs in this user on demand and caches the JWT for
|
|
3806
|
+
its lifetime. **Never** ship \`BOT_USER_PASSWORD\` to the browser. See the
|
|
3807
|
+
"Server-side TypeScript snippet" further down for the cached login helper.
|
|
3808
|
+
|
|
3809
|
+
If your project does not allow public chat (the bot is internal-only and
|
|
3810
|
+
every conversation already belongs to a logged-in human), skip this step
|
|
3811
|
+
and pass each user's own JWT to the chat endpoint.
|
|
3812
|
+
|
|
3755
3813
|
### 1. Create the KnowledgeBase
|
|
3756
3814
|
|
|
3757
3815
|
\`\`\`http
|
|
@@ -3905,11 +3963,28 @@ export async function refreshCatalogKB(productSchemaId: string, kbId: string, ex
|
|
|
3905
3963
|
});
|
|
3906
3964
|
}
|
|
3907
3965
|
|
|
3966
|
+
// Cached login for the bot service-account user. The JWT is reused until it
|
|
3967
|
+
// nears expiry; on next call we refresh server-side.
|
|
3968
|
+
let cachedBotJwt: { token: string; expiresAt: number } | null = null;
|
|
3969
|
+
|
|
3970
|
+
export async function getBotJwt(): Promise<string> {
|
|
3971
|
+
if (cachedBotJwt && cachedBotJwt.expiresAt > Date.now() + 60_000) {
|
|
3972
|
+
return cachedBotJwt.token;
|
|
3973
|
+
}
|
|
3974
|
+
const email = process.env.BOT_USER_EMAIL!;
|
|
3975
|
+
const password = process.env.BOT_USER_PASSWORD!;
|
|
3976
|
+
const r = await api('/v1/auth/login', { email, password });
|
|
3977
|
+
const ttl = (r.expires_in ?? 3600) * 1000;
|
|
3978
|
+
cachedBotJwt = { token: r.access_token, expiresAt: Date.now() + ttl };
|
|
3979
|
+
return r.access_token;
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3908
3982
|
export async function chatWithAgent(agentId: string, message: string, conversationId?: string) {
|
|
3983
|
+
const jwt = await getBotJwt();
|
|
3909
3984
|
return api(\`/v1/user/agents/\${agentId}/chat\`, {
|
|
3910
3985
|
message,
|
|
3911
3986
|
...(conversationId ? { conversation_id: conversationId } : {}),
|
|
3912
|
-
});
|
|
3987
|
+
}, jwt);
|
|
3913
3988
|
}
|
|
3914
3989
|
\`\`\`
|
|
3915
3990
|
|
|
@@ -3917,12 +3992,19 @@ export async function chatWithAgent(agentId: string, message: string, conversati
|
|
|
3917
3992
|
|
|
3918
3993
|
- \`POST /v1/agents\` is the Tenant Dashboard endpoint — when configuring from
|
|
3919
3994
|
your scaffolding script, use \`sk_live_*\`. End-user chat (\`/v1/user/agents/{id}/chat\`)
|
|
3920
|
-
is reached with \`pk_live_*\` + the end-user's
|
|
3995
|
+
is reached with \`pk_live_*\` (or \`sk_live_*\` on the server) + the end-user's
|
|
3996
|
+
JWT (or the bot's, if you registered one in Step 0).
|
|
3921
3997
|
- Tool calling vs RAG: for MVP / low-frequency catalog updates, RAG via KB is
|
|
3922
3998
|
simpler. Switch to tool calling when the catalog updates faster than the
|
|
3923
3999
|
refresh cadence you can sustain.
|
|
3924
4000
|
- The bot does not write to the catalog and does not take orders unless you
|
|
3925
|
-
add a separate tool / API path. Keep the system prompt strict
|
|
4001
|
+
add a separate tool / API path. Keep the system prompt strict.
|
|
4002
|
+
- **Fallback for MVP without an LLM** — if the project explicitly cannot
|
|
4003
|
+
register a bot user (no email domain available yet), a deterministic
|
|
4004
|
+
keyword-search bot over the catalog is acceptable. Implement \`/api/chat\` as
|
|
4005
|
+
a server-side \`POST /v1/entity/records/search\` with the user's query as an
|
|
4006
|
+
\`ilike\` filter. Not equivalent to the LLM bot but ships a working surface;
|
|
4007
|
+
swap it for the LLM later.`;
|
|
3926
4008
|
}
|
|
3927
4009
|
const APP_SCAFFOLDS = {
|
|
3928
4010
|
pos: {
|
|
@@ -3965,18 +4047,23 @@ const APP_SCAFFOLDS = {
|
|
|
3965
4047
|
},
|
|
3966
4048
|
],
|
|
3967
4049
|
build_order: [
|
|
3968
|
-
"
|
|
3969
|
-
"
|
|
3970
|
-
"
|
|
3971
|
-
"
|
|
3972
|
-
"
|
|
3973
|
-
"
|
|
3974
|
-
"
|
|
3975
|
-
"
|
|
3976
|
-
"
|
|
3977
|
-
"
|
|
4050
|
+
"**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.",
|
|
4051
|
+
"**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`.",
|
|
4052
|
+
"**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`.",
|
|
4053
|
+
"**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.",
|
|
4054
|
+
"**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.",
|
|
4055
|
+
"**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).",
|
|
4056
|
+
"**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).",
|
|
4057
|
+
"**7. Admin layout gate**: `app/(admin)/layout.tsx` reads `genlobe_jwt` cookie, redirects to `/admin/login` if missing.",
|
|
4058
|
+
"**8. Products vertical slice end-to-end first**: list, create, edit, delete. Confirms the full stack works before fanning out to other entities.",
|
|
4059
|
+
"**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).",
|
|
4060
|
+
"**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.",
|
|
4061
|
+
"**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.",
|
|
4062
|
+
"**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.",
|
|
4063
|
+
"**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.",
|
|
4064
|
+
"**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.",
|
|
3978
4065
|
],
|
|
3979
|
-
notes: "Stock decrement on sale is intentionally NOT automatic — for MVP
|
|
4066
|
+
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.",
|
|
3980
4067
|
},
|
|
3981
4068
|
crm: {
|
|
3982
4069
|
template: "crm",
|
|
@@ -4959,6 +5046,12 @@ Use search_endpoints tool to find available endpoints.`,
|
|
|
4959
5046
|
> 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.
|
|
4960
5047
|
> 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.
|
|
4961
5048
|
> 4. If the human says \"register a user with this email: ...\" — fine, that's an explicit instruction. Inventing one is not.
|
|
5049
|
+
>
|
|
5050
|
+
> ### Bot service-account user (LLM chat surface)
|
|
5051
|
+
>
|
|
5052
|
+
> **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.
|
|
5053
|
+
>
|
|
5054
|
+
> Registering **one** bot account is not the same thing as "inventing emails in bulk" — that distinction matters.
|
|
4962
5055
|
|
|
4963
5056
|
---
|
|
4964
5057
|
|
|
@@ -5186,7 +5279,122 @@ await fetch('/v1/auth/logout', {
|
|
|
5186
5279
|
|
|
5187
5280
|
localStorage.removeItem('access_token');
|
|
5188
5281
|
localStorage.removeItem('refresh_token');
|
|
5189
|
-
|
|
5282
|
+
\`\`\`
|
|
5283
|
+
|
|
5284
|
+
---
|
|
5285
|
+
|
|
5286
|
+
## 10. Server-side auth pattern (sk_live_* + cookies httpOnly) — Next.js App Router
|
|
5287
|
+
|
|
5288
|
+
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\`.
|
|
5289
|
+
|
|
5290
|
+
### Project layout (recommended)
|
|
5291
|
+
|
|
5292
|
+
\`\`\`
|
|
5293
|
+
app/
|
|
5294
|
+
├─ (admin)/ Owner dashboard, gated
|
|
5295
|
+
│ ├─ layout.tsx Reads cookie, redirects to /admin/login if missing
|
|
5296
|
+
│ ├─ admin/login/page.tsx
|
|
5297
|
+
│ └─ admin/page.tsx
|
|
5298
|
+
├─ (public)/ No auth, anonymous customer surface
|
|
5299
|
+
│ ├─ page.tsx Storefront / POS / catalog
|
|
5300
|
+
│ └─ chat/page.tsx Public chatbot UI
|
|
5301
|
+
├─ api/
|
|
5302
|
+
│ ├─ admin/login/route.ts POST → server-side call to /v1/auth/login, sets cookie
|
|
5303
|
+
│ ├─ admin/logout/route.ts Clears cookie
|
|
5304
|
+
│ └─ chat/route.ts POST → server-side call to /v1/user/agents/{id}/chat with bot JWT
|
|
5305
|
+
└─ middleware.ts Gates (admin)/*; refreshes bot JWT in background
|
|
5306
|
+
lib/
|
|
5307
|
+
└─ genlobe.server.ts 'server-only', wraps the API
|
|
5308
|
+
.env.local SAAS_API_URL, SAAS_API_KEY, SAAS_ORGANIZATION_ID, BOT_USER_EMAIL, BOT_USER_PASSWORD
|
|
5309
|
+
\`\`\`
|
|
5310
|
+
|
|
5311
|
+
### lib/genlobe.server.ts
|
|
5312
|
+
|
|
5313
|
+
\`\`\`typescript
|
|
5314
|
+
import 'server-only';
|
|
5315
|
+
|
|
5316
|
+
const API = process.env.SAAS_API_URL!; // e.g. http://localhost:8001
|
|
5317
|
+
const KEY = process.env.SAAS_API_KEY!; // sk_live_* — server only
|
|
5318
|
+
const ORG = process.env.SAAS_ORGANIZATION_ID!; // organization scope
|
|
5319
|
+
|
|
5320
|
+
export async function genlobeApi<T = any>(
|
|
5321
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
|
|
5322
|
+
path: string,
|
|
5323
|
+
body?: any,
|
|
5324
|
+
jwt?: string
|
|
5325
|
+
): Promise<T> {
|
|
5326
|
+
const res = await fetch(\`\${API}\${path}\`, {
|
|
5327
|
+
method,
|
|
5328
|
+
headers: {
|
|
5329
|
+
'Content-Type': 'application/json',
|
|
5330
|
+
'X-API-Key': KEY,
|
|
5331
|
+
'X-Organization-Id': ORG,
|
|
5332
|
+
...(jwt ? { Authorization: \`Bearer \${jwt}\` } : {}),
|
|
5333
|
+
},
|
|
5334
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
5335
|
+
cache: 'no-store',
|
|
5336
|
+
});
|
|
5337
|
+
if (!res.ok) throw new Error(\`Genlobe \${res.status}: \${await res.text()}\`);
|
|
5338
|
+
return res.json();
|
|
5339
|
+
}
|
|
5340
|
+
\`\`\`
|
|
5341
|
+
|
|
5342
|
+
### app/api/admin/login/route.ts
|
|
5343
|
+
|
|
5344
|
+
\`\`\`typescript
|
|
5345
|
+
import { NextResponse } from 'next/server';
|
|
5346
|
+
import { cookies } from 'next/headers';
|
|
5347
|
+
import { genlobeApi } from '@/lib/genlobe.server';
|
|
5348
|
+
|
|
5349
|
+
export async function POST(req: Request) {
|
|
5350
|
+
const { email, password } = await req.json();
|
|
5351
|
+
const { access_token, refresh_token, user } = await genlobeApi(
|
|
5352
|
+
'POST',
|
|
5353
|
+
'/v1/auth/login',
|
|
5354
|
+
{ email, password }
|
|
5355
|
+
);
|
|
5356
|
+
|
|
5357
|
+
const jar = cookies();
|
|
5358
|
+
jar.set('genlobe_jwt', access_token, {
|
|
5359
|
+
httpOnly: true, secure: process.env.NODE_ENV === 'production',
|
|
5360
|
+
sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24,
|
|
5361
|
+
});
|
|
5362
|
+
jar.set('genlobe_refresh', refresh_token, {
|
|
5363
|
+
httpOnly: true, secure: process.env.NODE_ENV === 'production',
|
|
5364
|
+
sameSite: 'lax', path: '/', maxAge: 60 * 60 * 24 * 30,
|
|
5365
|
+
});
|
|
5366
|
+
|
|
5367
|
+
return NextResponse.json({ user });
|
|
5368
|
+
}
|
|
5369
|
+
\`\`\`
|
|
5370
|
+
|
|
5371
|
+
### app/(admin)/layout.tsx (gate)
|
|
5372
|
+
|
|
5373
|
+
\`\`\`typescript
|
|
5374
|
+
import { cookies } from 'next/headers';
|
|
5375
|
+
import { redirect } from 'next/navigation';
|
|
5376
|
+
|
|
5377
|
+
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
|
5378
|
+
const jwt = cookies().get('genlobe_jwt')?.value;
|
|
5379
|
+
if (!jwt) redirect('/admin/login');
|
|
5380
|
+
return <>{children}</>;
|
|
5381
|
+
}
|
|
5382
|
+
\`\`\`
|
|
5383
|
+
|
|
5384
|
+
### .gitignore (critical)
|
|
5385
|
+
|
|
5386
|
+
\`\`\`
|
|
5387
|
+
.env*.local
|
|
5388
|
+
.env
|
|
5389
|
+
\`\`\`
|
|
5390
|
+
|
|
5391
|
+
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.
|
|
5392
|
+
|
|
5393
|
+
### Three rules that keep this safe
|
|
5394
|
+
|
|
5395
|
+
1. \`SAAS_API_KEY\` env var is **never** prefixed with \`NEXT_PUBLIC_\`. Same for \`BOT_USER_PASSWORD\` and \`SAAS_ORGANIZATION_ID\`.
|
|
5396
|
+
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.
|
|
5397
|
+
3. The browser only talks to *your* \`/api/*\` routes, never to the Genlobe host directly.`,
|
|
5190
5398
|
},
|
|
5191
5399
|
],
|
|
5192
5400
|
};
|
package/package.json
CHANGED