/**
* 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 };