apiblaze 0.18.7 → 0.18.10
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 +45 -62
- package/dist/index.js +50 -11
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,67 +1,6 @@
|
|
|
1
1
|
# apiblaze
|
|
2
2
|
|
|
3
|
-
CLI for [APIblaze](https://apiblaze.com) —
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Drop-in API-key widget (React)
|
|
8
|
-
|
|
9
|
-
Let your users create and manage their API keys on your own site, in ~3 files. Your
|
|
10
|
-
server holds one control-plane key; the browser only ever talks to your own backend —
|
|
11
|
-
no secrets in the client, no cross-domain cookies.
|
|
12
|
-
|
|
13
|
-
**1. Install**
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm install apiblaze react
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
**2. One backend route** — it holds the key and reads your session (`app/api/apiblaze/keys/route.ts`):
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import { createApiblazeKeys } from 'apiblaze/server';
|
|
23
|
-
import { auth } from '@/auth'; // NextAuth, Clerk, Auth0 — anything
|
|
24
|
-
|
|
25
|
-
const keys = createApiblazeKeys({
|
|
26
|
-
cpKey: process.env.APIBLAZE_CP_KEY!, // from Dashboard → Developers → "Widget key"
|
|
27
|
-
getUser: async () => {
|
|
28
|
-
const s = await auth();
|
|
29
|
-
if (!s?.user) return null; // not logged in → 401
|
|
30
|
-
return {
|
|
31
|
-
tenant: s.user.orgId ?? s.user.id, // the customer COMPANY → isolation
|
|
32
|
-
userId: s.user.id, // the PERSON → owns their keys
|
|
33
|
-
// Which key types this person may create. Decided HERE, on your server:
|
|
34
|
-
// omit → ['call-only'] (default) list → those (widget shows a picker) false → no access
|
|
35
|
-
keyTypes: s.user.isEngineer ? ['manager', 'call-only'] : undefined,
|
|
36
|
-
};
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
export const GET = keys.handler;
|
|
41
|
-
export const POST = keys.handler;
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
**3. One component** — anywhere in your app:
|
|
45
|
-
|
|
46
|
-
```tsx
|
|
47
|
-
'use client';
|
|
48
|
-
import { ApiKeyWidget } from 'apiblaze/react';
|
|
49
|
-
|
|
50
|
-
export default function Settings() {
|
|
51
|
-
return <ApiKeyWidget theme={{ accent: '#e11d48' }} />;
|
|
52
|
-
}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
That's it. The widget calls `/api/apiblaze/keys` (same-origin, your session cookie rides
|
|
56
|
-
along); your route adds the control-plane key and talks to apiblaze server-to-server.
|
|
57
|
-
|
|
58
|
-
**White-label it.** Every font and color is a `theme` token (`accent`, `surface`,
|
|
59
|
-
`headerBackground`, `text`, `muted`, `border`, `danger`, `success`, `radius`,
|
|
60
|
-
`fontFamily`, `monoFontFamily`) — match any brand without forking the component.
|
|
61
|
-
|
|
62
|
-
**Security.** Eligibility is decided on your server from your session, never the browser —
|
|
63
|
-
a crafted request can't mint a type the user isn't allowed. Use a **Widget key** (Dashboard
|
|
64
|
-
→ Developers), not a full admin key, so the platform's subset rule is a real backstop.
|
|
3
|
+
CLI for [APIblaze](https://apiblaze.com) — chat with your APIs, run them as serverless proxies, and let your customers self-serve keys, users & groups from your own site.
|
|
65
4
|
|
|
66
5
|
---
|
|
67
6
|
|
|
@@ -103,10 +42,44 @@ npx apiblaze tenant
|
|
|
103
42
|
# Access your localhost through a proxied URL
|
|
104
43
|
npx apiblaze dev 3000
|
|
105
44
|
|
|
45
|
+
# Route a Next.js app's outbound fetch() through APIblaze
|
|
46
|
+
npx apiblaze sidecar
|
|
47
|
+
|
|
106
48
|
# Sign in to manage APIs under your team
|
|
107
49
|
npx apiblaze login
|
|
108
50
|
```
|
|
109
51
|
|
|
52
|
+
## Embeddable widgets (React)
|
|
53
|
+
|
|
54
|
+
Let your customers self-serve from **your** site — **API keys** (`<ApiKeyWidget/>`) and
|
|
55
|
+
**users, groups & admins** (`<UsersGroupsWidget/>`). Each is one server route holding your
|
|
56
|
+
**Widget key** (Dashboard → Developers) plus one component; the browser only ever talks to
|
|
57
|
+
your backend — no secrets in the client. One `getUser` maps your session
|
|
58
|
+
(`{ tenant, userId, email }`); **both widgets share the same key**.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// app/api/apiblaze/keys/route.ts → <ApiKeyWidget/>
|
|
62
|
+
import { createApiblazeKeys } from 'apiblaze/server';
|
|
63
|
+
const keys = createApiblazeKeys({ cpKey: process.env.APIBLAZE_CP_KEY!, getUser });
|
|
64
|
+
export const GET = keys.handler, POST = keys.handler;
|
|
65
|
+
|
|
66
|
+
// app/api/apiblaze/groups/route.ts → <UsersGroupsWidget/> (same key)
|
|
67
|
+
import { createApiblazeGroups } from 'apiblaze/server';
|
|
68
|
+
const groups = createApiblazeGroups({ cpKey: process.env.APIBLAZE_CP_KEY!, getUser });
|
|
69
|
+
export const GET = groups.handler, POST = groups.handler;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
'use client';
|
|
74
|
+
import { ApiKeyWidget, UsersGroupsWidget } from 'apiblaze/react';
|
|
75
|
+
|
|
76
|
+
<ApiKeyWidget theme={{ accent: '#7C3AED' }} />; // keys: create · rotate · revoke
|
|
77
|
+
<UsersGroupsWidget theme={{ accent: '#7C3AED' }} />; // users · nested groups · co-admins · seen-in-traffic
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Every color and font is a `theme` token — white-label to any brand. Eligibility and admin
|
|
81
|
+
rights are decided on **your** server from your session, never the browser.
|
|
82
|
+
|
|
110
83
|
## Help
|
|
111
84
|
|
|
112
85
|
```bash
|
|
@@ -162,6 +135,16 @@ Every chat turn shows its cost.
|
|
|
162
135
|
| `apiblaze tenant cors --tenant <slug> --origins <a,b>` | Set which websites can call it |
|
|
163
136
|
| `apiblaze tenant list / delete <slug>` | List / delete tenants |
|
|
164
137
|
|
|
138
|
+
### Producer — users, groups & authorization
|
|
139
|
+
|
|
140
|
+
| Command | What it does |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `apiblaze group create / list / delete <name>` | Manage a tenant's groups (also `<UsersGroupsWidget/>`) |
|
|
143
|
+
| `apiblaze group add-user / remove-user <user> <group>` | Put a user in a group (or take them out) |
|
|
144
|
+
| `apiblaze group add-group / remove-group <child> <parent>` | Nest a group inside a group |
|
|
145
|
+
| `apiblaze rule "<plain english>" <project> [--enforce]` | Author an object-level access rule in one shot (shadow, then `--enforce`) |
|
|
146
|
+
| `apiblaze agent authz <project>` | Design & turn on authorization interactively (chat) |
|
|
147
|
+
|
|
165
148
|
### Producer — change a proxy's config
|
|
166
149
|
|
|
167
150
|
| Command | What it does |
|
package/dist/index.js
CHANGED
|
@@ -703,7 +703,7 @@ var import_commander = require("commander");
|
|
|
703
703
|
var import_chalk41 = __toESM(require("chalk"));
|
|
704
704
|
|
|
705
705
|
// package.json
|
|
706
|
-
var version = "0.18.
|
|
706
|
+
var version = "0.18.10";
|
|
707
707
|
|
|
708
708
|
// src/index.ts
|
|
709
709
|
init_types();
|
|
@@ -2427,6 +2427,35 @@ async function runAgentChatRepl(opts) {
|
|
|
2427
2427
|
// src/lib/authz-apply.ts
|
|
2428
2428
|
var import_chalk15 = __toESM(require("chalk"));
|
|
2429
2429
|
init_api();
|
|
2430
|
+
async function gatherSampleIds(projectId, apiVersion, cap = 50) {
|
|
2431
|
+
try {
|
|
2432
|
+
const routesRes = await agentCall(`/projects/${projectId}/${apiVersion}/samples/routes`, "GET");
|
|
2433
|
+
const routes = routesRes?.data?.routes ?? [];
|
|
2434
|
+
const total = routes.reduce((n, r) => n + (Number(r.total) || 0), 0);
|
|
2435
|
+
if (!routes.length || total === 0) return { ids: [], total: 0 };
|
|
2436
|
+
const ordered = routes.filter((r) => r.route_hash).sort((a, b) => (Number(b.total) || 0) - (Number(a.total) || 0)).slice(0, 20);
|
|
2437
|
+
const ids = [];
|
|
2438
|
+
for (const r of ordered) {
|
|
2439
|
+
if (ids.length >= cap) break;
|
|
2440
|
+
const sRes = await agentCall(`/projects/${projectId}/${apiVersion}/samples/routes/${encodeURIComponent(r.route_hash)}/samples`, "GET");
|
|
2441
|
+
for (const s of sRes?.data?.samples ?? []) {
|
|
2442
|
+
if (ids.length >= cap) break;
|
|
2443
|
+
if (s.sample_id) ids.push(s.sample_id);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
return { ids, total };
|
|
2447
|
+
} catch {
|
|
2448
|
+
return { ids: [], total: 0 };
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
function maybeWarnNoTraffic(total, projectArg) {
|
|
2452
|
+
if (total >= 3) return;
|
|
2453
|
+
console.log(import_chalk15.default.yellow(total === 0 ? " \u26A0 No captured traffic for this API yet." : ` \u26A0 Only ${total} captured request(s) so far.`));
|
|
2454
|
+
console.log(import_chalk15.default.dim(" The authorization agent designs much better models & rules from REAL requests"));
|
|
2455
|
+
console.log(import_chalk15.default.dim(" (it sees who is allowed vs denied). Send some through your proxy first \u2014"));
|
|
2456
|
+
console.log(import_chalk15.default.dim(` curl a few endpoints, or `) + import_chalk15.default.cyan("npx apiblaze dev") + import_chalk15.default.dim(` \u2014 then re-run.
|
|
2457
|
+
`));
|
|
2458
|
+
}
|
|
2430
2459
|
async function applyProposal(opts) {
|
|
2431
2460
|
const log = opts.log ?? ((s) => console.log(s));
|
|
2432
2461
|
const { projectId, apiVersion, tenant: tenant2, proposal, enable } = opts;
|
|
@@ -2499,11 +2528,13 @@ async function runAuthz(projectArg, apiVersionArg) {
|
|
|
2499
2528
|
`));
|
|
2500
2529
|
}
|
|
2501
2530
|
}
|
|
2531
|
+
const { ids: sampleIds, total: sampleTotal } = await gatherSampleIds(projectId, apiVersion);
|
|
2532
|
+
maybeWarnNoTraffic(sampleTotal, projectArg);
|
|
2502
2533
|
await runAgentChatRepl({
|
|
2503
2534
|
title: `Authorization assistant \u2014 ${projectId} ${apiVersion}`,
|
|
2504
|
-
subtitle: `tenant ${tenant2} \xB7 discuss what authorization fits this API, then make it official.`,
|
|
2535
|
+
subtitle: sampleIds.length ? `tenant ${tenant2} \xB7 ${sampleIds.length} traffic sample(s) \xB7 discuss what fits, then make it official.` : `tenant ${tenant2} \xB7 discuss what authorization fits this API, then make it official.`,
|
|
2505
2536
|
endpoint: `/projects/${projectId}/${apiVersion}/authz/chat`,
|
|
2506
|
-
buildBody: () => ({ included_sample_ids:
|
|
2537
|
+
buildBody: () => ({ included_sample_ids: sampleIds, existing_model: null, existing_routes: [] }),
|
|
2507
2538
|
seedPrompt: "Analyze this API and tell me what authorization is feasible. If it is read-only, say so plainly. Then propose options \u2014 do not generate rules yet.",
|
|
2508
2539
|
summarizeProposal: (data) => {
|
|
2509
2540
|
const proposal = data.proposal;
|
|
@@ -2538,7 +2569,9 @@ async function runRule(ruleText, projectArg, opts) {
|
|
|
2538
2569
|
const projectId = match.projectId;
|
|
2539
2570
|
const apiVersion = opts.apiversion || match.apiVersion;
|
|
2540
2571
|
const tenant2 = match.tenant || projectId;
|
|
2541
|
-
const
|
|
2572
|
+
const { ids: sampleIds, total: sampleTotal } = await gatherSampleIds(projectId, apiVersion);
|
|
2573
|
+
maybeWarnNoTraffic(sampleTotal, projectArg);
|
|
2574
|
+
const spinner = (0, import_ora5.default)(sampleIds.length ? `Designing the rule from ${sampleIds.length} traffic sample(s)\u2026` : "Designing the rule\u2026").start();
|
|
2542
2575
|
const messages = [{
|
|
2543
2576
|
role: "user",
|
|
2544
2577
|
content: `Author this authorization rule and generate the COMPLETE proposal (OpenFGA model + per-route rules) NOW for every affected route. Do not ask clarifying questions; make reasonable assumptions and state them briefly. Rule: "${rule}"`
|
|
@@ -2546,7 +2579,7 @@ async function runRule(ruleText, projectArg, opts) {
|
|
|
2546
2579
|
const { status, data } = await agentCall(
|
|
2547
2580
|
`/projects/${projectId}/${apiVersion}/authz/chat`,
|
|
2548
2581
|
"POST",
|
|
2549
|
-
{ messages, included_sample_ids:
|
|
2582
|
+
{ messages, included_sample_ids: sampleIds, existing_model: null, existing_routes: [] }
|
|
2550
2583
|
);
|
|
2551
2584
|
if (status >= 400) {
|
|
2552
2585
|
spinner.fail(`Authorization agent error (${status}): ${data?.error ?? ""}`);
|
|
@@ -5626,7 +5659,7 @@ function freeBudgetWarning(billing, anon) {
|
|
|
5626
5659
|
if (typeof billing.free_turns_remaining === "number") {
|
|
5627
5660
|
const left2 = billing.free_turns_remaining;
|
|
5628
5661
|
if (left2 <= 0) return import_chalk36.default.yellow(" Free chats used up \u2014 `npx apiblaze login` (free) to keep going.");
|
|
5629
|
-
return import_chalk36.default.dim(` ${left2} free chat${left2 === 1 ? "" : "s"} left`);
|
|
5662
|
+
return import_chalk36.default.dim(` ${left2} free chat${left2 === 1 ? "" : "s"} left \xB7 /login to get more`);
|
|
5630
5663
|
}
|
|
5631
5664
|
if (typeof billing.free_remaining_cents !== "number") return null;
|
|
5632
5665
|
const perTurn = Math.max(billing.cents || 0, 0.02);
|
|
@@ -5715,7 +5748,7 @@ function renderUpsell(p, upsell) {
|
|
|
5715
5748
|
console.log("\n" + import_chalk36.default.yellow(` ${upsell.message || "This turn is not available right now."}`));
|
|
5716
5749
|
if (upsell.reason === "INSUFFICIENT" || upsell.reason === "BREAKER" || upsell.reason === "CAPPED" || upsell.reason === "PAUSED") {
|
|
5717
5750
|
if (!loggedIn) {
|
|
5718
|
-
console.log(import_chalk36.default.dim(" Options: `/login` for free
|
|
5751
|
+
console.log(import_chalk36.default.dim(" Options: `/login` for more free chats and requests, or `apiblaze llm set-key` to bring your own model key."));
|
|
5719
5752
|
} else {
|
|
5720
5753
|
console.log(import_chalk36.default.dim(" Options: top up your wallet, or `apiblaze llm set-key` to bring your own model key (bypasses platform limits)."));
|
|
5721
5754
|
}
|
|
@@ -5961,7 +5994,7 @@ async function runRepl(p, initialMessages) {
|
|
|
5961
5994
|
const llm2 = loadLlmConfig();
|
|
5962
5995
|
console.log(
|
|
5963
5996
|
import_chalk36.default.dim(
|
|
5964
|
-
llm2 ? `Using your local ${llm2.provider} key for the model. Type a question, or /exit. /login /claim manage your workspace.` : "Ask a question in plain English. /exit to quit \xB7 /login for
|
|
5997
|
+
llm2 ? `Using your local ${llm2.provider} key for the model. Type a question, or /exit. /login /claim manage your workspace.` : "Ask a question in plain English. /exit to quit \xB7 /login for more free chats \xB7 /claim to keep this workspace \xB7 `apiblaze llm set-key` for BYO models."
|
|
5965
5998
|
)
|
|
5966
5999
|
);
|
|
5967
6000
|
for (; ; ) {
|
|
@@ -5980,8 +6013,14 @@ async function runRepl(p, initialMessages) {
|
|
|
5980
6013
|
}
|
|
5981
6014
|
if (text === "/claim") {
|
|
5982
6015
|
if (!loadCredentials()) {
|
|
5983
|
-
console.log(import_chalk36.default.
|
|
5984
|
-
|
|
6016
|
+
console.log(import_chalk36.default.dim(" Logging in to claim your workspace\u2026"));
|
|
6017
|
+
try {
|
|
6018
|
+
await runLogin();
|
|
6019
|
+
} catch (err) {
|
|
6020
|
+
console.log(import_chalk36.default.red(` Login failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
6021
|
+
continue;
|
|
6022
|
+
}
|
|
6023
|
+
if (!loadCredentials()) continue;
|
|
5985
6024
|
}
|
|
5986
6025
|
try {
|
|
5987
6026
|
await runClaim(void 0, {});
|
|
@@ -6062,7 +6101,7 @@ async function runApichat(opts) {
|
|
|
6062
6101
|
if (p.proxyUrl) console.log(` ${import_chalk36.default.green("\u2713")} proxy ${import_chalk36.default.bold(p.proxyUrl)}`);
|
|
6063
6102
|
if (mcpUrl) console.log(` ${import_chalk36.default.green("\u2713")} mcp ${import_chalk36.default.bold(mcpUrl)}`);
|
|
6064
6103
|
if (p.anon) {
|
|
6065
|
-
console.log(import_chalk36.default.dim("\n Anonymous workspace \u2014 /claim inside the chat
|
|
6104
|
+
console.log(import_chalk36.default.dim("\n Anonymous workspace \u2014 /claim inside the chat to log in and keep it beyond 30 days."));
|
|
6066
6105
|
}
|
|
6067
6106
|
await runRepl(p);
|
|
6068
6107
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apiblaze",
|
|
3
|
-
"version": "0.18.
|
|
4
|
-
"description": "APIblaze CLI
|
|
3
|
+
"version": "0.18.10",
|
|
4
|
+
"description": "APIblaze CLI — Chat with your APIs, Manage your API keys, users and groups with the APIblaze serverless proxy",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"apiblaze",
|
|
7
7
|
"dev-tunnel",
|
|
@@ -90,4 +90,4 @@
|
|
|
90
90
|
"optional": true
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
}
|
|
93
|
+
}
|