/** * apiblaze/server — SHARED widget-server substrate. * * One CP credential, one getUser contract, one ensure-tenant/ensure-user path — * consumed by BOTH widget faces (`createApiblazeKeys` and `createApiblazeGroups`). * Factored per iam_toolkit_spec ("share, don't fork" — copy-forking this * bootstrap is a spec failure). * * Trust model: the CP key stays server-side and vouches for the CHANNEL; the * acting end-user rides X-End-User-Id / X-End-User-Email. On the apikeys plane * the end-user header only narrows (self-scoped key ops); on the iam plane it * IS the principal (iam-worker independently checks the resolved user's * apiblaze_admins membership before any group op). */ interface AppUser { /** The producer's stable id for the consumer COMPANY — becomes the tenant (isolation). */ tenant: string; /** The producer's stable id for the PERSON — the acting end-user. */ userId: string; /** Optional; REQUIRED in practice for the groups widget — tenant-admin status * is granted by email (the tenant's admin-emails allowlist). */ email?: string; /** Optional display name (cosmetic). */ label?: string; /** Key-widget eligibility (see createApiblazeKeys). Ignored by the groups widget. */ keyTypes?: string[] | false; /** Any extra fields your eligibility logic wants (e.g. isEngineer). */ [k: string]: unknown; } declare class PlaneError extends Error { status: number; detail: string; constructor(status: number, detail: string); } /** * createApiblazeGroups — the server-side half of the Users & Groups widget * (), riding the SAME shared substrate (core.ts) and the * SAME one CP widget credential as the API-key widget. * * Usage (Next.js App Router — app/api/apiblaze/groups/route.ts): * * import { createApiblazeGroups } from 'apiblaze/server'; * const groups = createApiblazeGroups({ * cpKey: process.env.APIBLAZE_CP_KEY!, // the SAME key the key widget uses * getUser: async () => { ...same contract... return { tenant, userId, email } }, * }); * export const GET = groups.handler; * export const POST = groups.handler; * * AUTHORIZATION MODEL (iam_toolkit_spec): unlike key ops (self-scoped), group * admin ops are tenant-wide — so the acting END-USER IS THE PRINCIPAL. This * server forwards the session user via X-End-User-Id/-Email; apiblaze's iam * plane independently verifies that user is a TENANT ADMIN (apiblaze_admins * member, granted via the tenant's admin-emails allowlist) before any op. A * non-admin gets `access: 'pending'` — nothing here can elevate them, and no * producer-side flag can either. The CP key is only the channel. * * Strictly users + groups — NO authorization-rule authoring (that stays in * `apiblaze authz` / the dashboard). */ interface ApiblazeGroupsConfig { /** The producer CP key — the SAME widget key the API-key widget uses. Server-only. */ cpKey: string; /** Return the logged-in user from the request/session, or null if unauthenticated. * `email` is how tenant-admin status is granted (admin-emails allowlist) — pass it. */ getUser: (req: Request) => Promise | AppUser | null; /** Override the apikeys plane base (tenant/user provisioning). Default https://apikeys.apiblaze.com */ base?: string; /** Override the iam plane base. Default https://iam.apiblaze.com */ iamBase?: string; } type GroupsAction = { action: 'snapshot'; } | { action: 'list-groups'; offset?: number; q?: string; } | { action: 'search-users'; q?: string; limit?: number; offset?: number; } | { action: 'list-observed'; offset?: number; q?: string; } | { action: 'group-members'; groupId: string; offset?: number; } | { action: 'create-group'; name: string; description?: string; } | { action: 'rename-group'; groupId: string; name?: string; description?: string; } | { action: 'delete-group'; groupId: string; } | { action: 'add-member'; groupId: string; abzSub: string; role?: 'member' | 'admin'; } | { action: 'change-role'; groupId: string; abzSub: string; role: 'member' | 'admin'; } | { action: 'remove-member'; groupId: string; abzSub: string; } | { action: 'add-subgroup'; groupId: string; childGroupId: string; } | { action: 'remove-subgroup'; groupId: string; childGroupId: string; } | { action: 'group-detail'; groupId: string; } | { action: 'provision-observed'; consumerUserId: string; email?: string; displayName?: string; groups?: string[]; } | { action: 'create-user'; ref: string; displayName?: string; } | { action: 'list-admins'; } | { action: 'add-admin'; email: string; } | { action: 'remove-admin'; email: string; }; declare function createApiblazeGroups(cfg: ApiblazeGroupsConfig): { handler: (req: Request) => Promise; run: (user: AppUser, op: GroupsAction) => Promise<{ status: number; data: unknown; }>; }; /** * createApiblazeChat — the server-side half of for API-KEY-door * proxies (chatwidget_prd.md, Mode 2 "relay"). JWT-door proxies don't need this * file at all: the widget sends the end user's own session token straight to * the proxy (Mode 1) and per-person quotas/authorization fall out for free. * * What the relay does — and ALL it does: * 1. resolves YOUR user from YOUR session (the same getUser contract as * createApiblazeKeys / createApiblazeGroups), * 2. attaches the producer DP key (server-only) + X-End-User-Id, * 3. PIPES the SSE stream through untouched (streams pass through Next.js * route handlers natively — never buffer, never re-frame). * * DOCUMENTED TRADEOFFS (say them, don't bury them): behind ONE relay key, * · the proxy's chat quota meters ALL your end users into ONE daily bucket; * · tool calls carry that key's authority (specs/chat_funding.MD: metering * keys on the credential; the asserted X-End-User-Id proves nothing); * · chat is ONE TURN AT A TIME site-wide — the anti-burst in-flight lock is * per credential, so while one user's turn streams (seconds), another * user's send is politely refused ("a previous message is still being * processed"). Fine for a demo or a low-traffic page; NOT for a busy site. * Want per-person buckets, per-person authorization, and real concurrency? * Use Mode 1 (register your issuer on the proxy — the user's own JWT is the * credential), or mint per-user keys via the key-widget plane. * * // app/api/apiblaze/chat/route.ts * import { createApiblazeChat } from 'apiblaze/server'; * const chat = createApiblazeChat({ * project: 'ninopizza', * apiKey: process.env.APIBLAZE_DP_KEY!, // a DP key for that proxy — server-only * getUser: async (req) => ({ userId: (await session(req)).userId }), * }); * export const POST = chat.handler; */ interface ChatRelayUser { /** Your stable id for the PERSON — forwarded as X-End-User-Id so the proxy's * identity gates (identified_traffic_only / pre-approval) and the upstream's * x-end-user-id contract see it. */ userId: string; /** Optional display email/label — NOT forwarded; reserved for future use. */ email?: string; } interface ApiblazeChatConfig { /** Project handle (the proxy's subdomain). */ project: string; /** A DP api key for that proxy. SERVER-ONLY — never expose it to the browser; * the whole point of this relay is that the browser never holds it. */ apiKey: string; /** Resolve the acting end user from YOUR request/session. Return null when * nobody is signed in (the relay then answers 401 rather than letting an * anonymous visitor chat on your key). Return {userId: ''} to explicitly * allow UNIDENTIFIED chat (the proxy must not require identification). */ getUser: (req: Request) => Promise | ChatRelayUser | null; /** Defaults: '1.0.0' / 'prod' / 'abz.run'. */ apiVersion?: string; environment?: string; host?: string; /** Cap on the request body we relay, bytes (default 262144 = the proxy's own * non-BYO cap — reject junk before spending upstream bandwidth on it). */ maxBodyBytes?: number; } declare function createApiblazeChat(cfg: ApiblazeChatConfig): { handler: (req: Request) => Promise; upstream: string; }; /** * apiblaze/server — the server-side half of the apiblaze widgets. * * Holds the producer's CP key (server-only, never shipped to the browser) and turns * a logged-in user into apiblaze operations. The browser widgets (`apiblaze/react`) * talk ONLY to your own backend; your backend talks to apiblaze — so there are no * cross-domain cookies, no secrets in the client, and no auth wiring beyond * "read my session." * * TWO faces over ONE shared substrate (core.ts — one CP credential, one * ensure-tenant/ensure-user path): * - createApiblazeKeys → API-key management () * - createApiblazeGroups → users & groups IAM () * * Usage (Next.js App Router — app/api/apiblaze/keys/route.ts): * * import { createApiblazeKeys } from 'apiblaze/server'; * import { auth } from '@/auth'; // NextAuth, Clerk, anything * * const keys = createApiblazeKeys({ * cpKey: process.env.APIBLAZE_CP_KEY!, * getUser: async () => { * const s = await auth(); * if (!s?.user) return null; * return { * tenant: s.user.orgId ?? s.user.id, * userId: s.user.id, * email: s.user.email ?? undefined, * // Which key types this person may create. Omit → ['call-only']. * // false → no API access. A list of 2+ → the widget shows a picker. * keyTypes: s.user.isEngineer ? ['manager', 'call-only'] : undefined, * }; * }, * }); * export const GET = keys.handler; * export const POST = keys.handler; * * SECURITY: eligibility is decided HERE, on your server, from your session — * never from the browser. When a user is eligible for more than one type the * browser sends its pick, but this handler only honors it if it's in the user's * allowed set; otherwise it's rejected. `keyTypes: false` is enforced server-side * (403) before anything is provisioned, not merely hidden in the UI. */ interface ApiblazeKeysConfig { /** The producer CP key from the apiblaze dashboard Developers section. Server-only. * Use a purpose-built "widget" key (not a full admin key) so the platform's own * subset rule is a real backstop under your eligibility logic. */ cpKey: string; /** Return the logged-in user from the request/session, or null if unauthenticated. */ getUser: (req: Request) => Promise | AppUser | null; /** Optional override for eligibility, if you'd rather compute it here than inline * in getUser. Same contract as AppUser.keyTypes (undefined → default, false/[] → deny). * Takes precedence over user.keyTypes when provided. */ resolveKeyTypes?: (user: AppUser) => (string[] | false) | Promise; /** Key lifetime in seconds. Omit for a DURABLE key (no expiry) — the right * default for a production API key a developer embeds, so it never silently * stops working. A durable key is shown ONCE at creation (not re-revealable); * set an expiry if you want keys that stay revealable until they expire. */ keyExpiresInSeconds?: number; /** Override the apiblaze base (tests/self-host). Default https://apikeys.apiblaze.com */ base?: string; } type Action = { action: 'list'; } | { action: 'create'; keyType?: string; } | { action: 'reveal'; keyId: string; } | { action: 'rotate'; keyId: string; } | { action: 'revoke'; keyId: string; }; declare function createApiblazeKeys(cfg: ApiblazeKeysConfig): { handler: (req: Request) => Promise; run: (user: AppUser, op: Action) => Promise<{ status: number; data: unknown; }>; }; export { type ApiblazeChatConfig, type ApiblazeGroupsConfig, type ApiblazeKeysConfig, type AppUser, type ChatRelayUser, type GroupsAction, PlaneError, createApiblazeChat, createApiblazeGroups, createApiblazeKeys };