apiblaze 0.4.15 → 0.6.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/README.md +32 -1
- package/dist/index.js +245 -20
- package/dist/sidecar/index.d.mts +28 -0
- package/dist/sidecar/index.d.ts +28 -0
- package/dist/sidecar/index.js +179 -0
- package/dist/sidecar/index.mjs +153 -0
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ Every chat turn shows its cost.
|
|
|
69
69
|
| Command | What it does |
|
|
70
70
|
|---|---|
|
|
71
71
|
| `apiblaze create --target <url>` | Make an API from a backend (no account needed) |
|
|
72
|
+
| `apiblaze sidecar` | Route a Next.js app's external `fetch()` calls through APIblaze (one command) |
|
|
72
73
|
| `apiblaze dev [port]` | Put your localhost behind a public URL |
|
|
73
74
|
| `apiblaze login` / `logout` | Sign in / out (logout asks producer or consumer) |
|
|
74
75
|
| `apiblaze whoami` | Who am I — both Producer and Consumer |
|
|
@@ -130,7 +131,37 @@ shows both, and `logout` asks which to drop.
|
|
|
130
131
|
|
|
131
132
|
On Ctrl+C the tunnel is cleanly deregistered.
|
|
132
133
|
|
|
133
|
-
|
|
134
|
+
## Sidecar — proxy a Next.js app's egress
|
|
135
|
+
|
|
136
|
+
`apiblaze sidecar` wires your Next.js backend so its outbound `fetch()` calls
|
|
137
|
+
transparently route through APIblaze. Each distinct upstream origin (Stripe, an
|
|
138
|
+
internal API, anything) lazily becomes a first-class APIblaze proxy on first use
|
|
139
|
+
— so you get identity, rate-limits, quotas, and audit wrapped around APIs that
|
|
140
|
+
have none of it, with no per-call changes.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
cd my-next-app
|
|
144
|
+
npx apiblaze sidecar # logs you in, mints a scoped key, wires it up
|
|
145
|
+
npm install apiblaze
|
|
146
|
+
npm run dev # then open http://localhost:3000/abz-inspector
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The command mints a **scoped data-plane key** (create/use proxies for one team,
|
|
150
|
+
nothing else) into gitignored `.env.local` as `APIBLAZE_TOKEN` + `APIBLAZE_TENANT`.
|
|
151
|
+
Your control-plane login stays in `~/.apiblaze` and never enters the project. It
|
|
152
|
+
also writes `instrumentation.ts`:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { register as apiblaze } from "apiblaze/sidecar";
|
|
156
|
+
export function register() { apiblaze(); }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The interceptor is stateless and fail-open: if it can't reach APIblaze it falls
|
|
160
|
+
back to a direct fetch, so it never breaks your app. External HTTPS origins are
|
|
161
|
+
intercepted; same-origin, localhost/private, and known analytics/CDN noise are
|
|
162
|
+
left alone.
|
|
163
|
+
|
|
164
|
+
## How it works
|
|
134
165
|
|
|
135
166
|
- **No project yet?** If none of your projects point at this machine, `apiblaze dev`
|
|
136
167
|
offers to spin up a throwaway dev proxy (random name like `braveotter42`) pointed at
|
package/dist/index.js
CHANGED
|
@@ -25,10 +25,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
|
|
26
26
|
// src/index.ts
|
|
27
27
|
var import_commander = require("commander");
|
|
28
|
-
var
|
|
28
|
+
var import_chalk30 = __toESM(require("chalk"));
|
|
29
29
|
|
|
30
30
|
// package.json
|
|
31
|
-
var version = "0.
|
|
31
|
+
var version = "0.6.0";
|
|
32
32
|
|
|
33
33
|
// src/types.ts
|
|
34
34
|
var ApiError = class extends Error {
|
|
@@ -105,9 +105,9 @@ async function createProxyAnonymous(body) {
|
|
|
105
105
|
}
|
|
106
106
|
return res.json();
|
|
107
107
|
}
|
|
108
|
-
async function apiFetch(
|
|
108
|
+
async function apiFetch(path4, options = {}) {
|
|
109
109
|
const token = getAccessToken();
|
|
110
|
-
const url = `${DASHBOARD_BASE}${
|
|
110
|
+
const url = `${DASHBOARD_BASE}${path4}`;
|
|
111
111
|
const res = await fetch(url, {
|
|
112
112
|
...options,
|
|
113
113
|
headers: {
|
|
@@ -132,12 +132,12 @@ async function apiFetch(path3, options = {}) {
|
|
|
132
132
|
}
|
|
133
133
|
return res.json();
|
|
134
134
|
}
|
|
135
|
-
async function agentCall(
|
|
135
|
+
async function agentCall(path4, method, body) {
|
|
136
136
|
const token = getAccessToken();
|
|
137
137
|
const res = await fetch(`${DASHBOARD_BASE}/api/cli/agents`, {
|
|
138
138
|
method: "POST",
|
|
139
139
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
140
|
-
body: JSON.stringify({ path:
|
|
140
|
+
body: JSON.stringify({ path: path4, method, body })
|
|
141
141
|
});
|
|
142
142
|
let data = null;
|
|
143
143
|
try {
|
|
@@ -420,11 +420,11 @@ function decodeJwt(token) {
|
|
|
420
420
|
return null;
|
|
421
421
|
}
|
|
422
422
|
}
|
|
423
|
-
function maskPath(
|
|
424
|
-
const q =
|
|
425
|
-
if (q < 0) return
|
|
426
|
-
const base =
|
|
427
|
-
const query =
|
|
423
|
+
function maskPath(path4) {
|
|
424
|
+
const q = path4.indexOf("?");
|
|
425
|
+
if (q < 0) return path4;
|
|
426
|
+
const base = path4.slice(0, q);
|
|
427
|
+
const query = path4.slice(q + 1);
|
|
428
428
|
const masked = query.split("&").map((pair) => {
|
|
429
429
|
const eq = pair.indexOf("=");
|
|
430
430
|
if (eq < 0) return pair;
|
|
@@ -2485,15 +2485,15 @@ async function runTenantCors(opts) {
|
|
|
2485
2485
|
process.exit(1);
|
|
2486
2486
|
}
|
|
2487
2487
|
const { teamId } = await resolveTeam(opts.team);
|
|
2488
|
-
const
|
|
2489
|
-
const cors =
|
|
2488
|
+
const origins2 = (opts.origins ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
2489
|
+
const cors = origins2.length ? { allowed_origins: origins2 } : null;
|
|
2490
2490
|
const spinner = (0, import_ora8.default)("Updating CORS...").start();
|
|
2491
2491
|
try {
|
|
2492
2492
|
await admin({
|
|
2493
2493
|
method: "PUT",
|
|
2494
2494
|
path: `/teams/${encodeURIComponent(teamId)}/tenants/${encodeURIComponent(opts.tenant)}/cors`,
|
|
2495
2495
|
body: { cors },
|
|
2496
|
-
summary: `Set CORS for tenant ${opts.tenant} \u2192 ${
|
|
2496
|
+
summary: `Set CORS for tenant ${opts.tenant} \u2192 ${origins2.length ? origins2.join(", ") : "(cleared)"}`
|
|
2497
2497
|
});
|
|
2498
2498
|
spinner.succeed(`CORS updated for ${opts.tenant}.`);
|
|
2499
2499
|
} catch (err) {
|
|
@@ -3037,6 +3037,227 @@ async function runConsumerApikeys(opts) {
|
|
|
3037
3037
|
else console.log(import_chalk27.default.dim(" Key created; run `apiblaze consumer apikeys` to reveal it if it expires."));
|
|
3038
3038
|
}
|
|
3039
3039
|
|
|
3040
|
+
// src/commands/sidecar.ts
|
|
3041
|
+
var import_chalk28 = __toESM(require("chalk"));
|
|
3042
|
+
var import_ora13 = __toESM(require("ora"));
|
|
3043
|
+
var fs6 = __toESM(require("fs"));
|
|
3044
|
+
var path3 = __toESM(require("path"));
|
|
3045
|
+
function detectNextProject(root) {
|
|
3046
|
+
const hasConfig = ["next.config.js", "next.config.mjs", "next.config.ts"].some((f) => fs6.existsSync(path3.join(root, f)));
|
|
3047
|
+
let hasDep = false;
|
|
3048
|
+
try {
|
|
3049
|
+
const pkg = JSON.parse(fs6.readFileSync(path3.join(root, "package.json"), "utf8"));
|
|
3050
|
+
hasDep = !!(pkg.dependencies?.next || pkg.devDependencies?.next);
|
|
3051
|
+
} catch {
|
|
3052
|
+
}
|
|
3053
|
+
const appDir = fs6.existsSync(path3.join(root, "app")) || fs6.existsSync(path3.join(root, "src", "app"));
|
|
3054
|
+
const pagesDir = fs6.existsSync(path3.join(root, "pages")) || fs6.existsSync(path3.join(root, "src", "pages"));
|
|
3055
|
+
return { found: hasConfig || hasDep || appDir || pagesDir, router: appDir ? "app" : pagesDir ? "pages" : null };
|
|
3056
|
+
}
|
|
3057
|
+
function upsertEnvLocal(root, token) {
|
|
3058
|
+
const p = path3.join(root, ".env.local");
|
|
3059
|
+
let existing = "";
|
|
3060
|
+
try {
|
|
3061
|
+
existing = fs6.readFileSync(p, "utf8");
|
|
3062
|
+
} catch {
|
|
3063
|
+
}
|
|
3064
|
+
const had = /^APIBLAZE_TOKEN=/m.test(existing);
|
|
3065
|
+
const next = had ? existing.replace(/^APIBLAZE_TOKEN=.*$/m, `APIBLAZE_TOKEN=${token}`) : (existing && !existing.endsWith("\n") ? existing + "\n" : existing) + `APIBLAZE_TOKEN=${token}
|
|
3066
|
+
`;
|
|
3067
|
+
fs6.writeFileSync(p, next);
|
|
3068
|
+
return had ? "rotated" : "created";
|
|
3069
|
+
}
|
|
3070
|
+
function ensureGitignored(root) {
|
|
3071
|
+
const p = path3.join(root, ".gitignore");
|
|
3072
|
+
let c = "";
|
|
3073
|
+
try {
|
|
3074
|
+
c = fs6.readFileSync(p, "utf8");
|
|
3075
|
+
} catch {
|
|
3076
|
+
}
|
|
3077
|
+
if (!/^\.env\.local$/m.test(c) && !/^\.env\*/m.test(c)) fs6.writeFileSync(p, (c && !c.endsWith("\n") ? c + "\n" : c) + ".env.local\n");
|
|
3078
|
+
}
|
|
3079
|
+
function wireInstrumentation(root) {
|
|
3080
|
+
const existing = ["instrumentation.ts", "instrumentation.js", path3.join("src", "instrumentation.ts")].map((c) => path3.join(root, c)).find((f) => fs6.existsSync(f));
|
|
3081
|
+
const body = `import { register as apiblaze } from "apiblaze/sidecar";
|
|
3082
|
+
|
|
3083
|
+
export function register() {
|
|
3084
|
+
apiblaze();
|
|
3085
|
+
}
|
|
3086
|
+
`;
|
|
3087
|
+
if (!existing) {
|
|
3088
|
+
fs6.writeFileSync(path3.join(root, "instrumentation.ts"), body);
|
|
3089
|
+
return "created";
|
|
3090
|
+
}
|
|
3091
|
+
const cur = fs6.readFileSync(existing, "utf8");
|
|
3092
|
+
if (cur.includes("apiblaze/sidecar")) return "present";
|
|
3093
|
+
if (/export\s+function\s+register\s*\(/.test(cur)) {
|
|
3094
|
+
fs6.writeFileSync(existing, `import { register as apiblaze } from "apiblaze/sidecar";
|
|
3095
|
+
` + cur.replace(/export\s+function\s+register\s*\(\s*\)\s*\{/, (m) => `${m}
|
|
3096
|
+
apiblaze();`));
|
|
3097
|
+
return "patched";
|
|
3098
|
+
}
|
|
3099
|
+
fs6.writeFileSync(existing, `import { register as apiblaze } from "apiblaze/sidecar";
|
|
3100
|
+
${cur}
|
|
3101
|
+
// call apiblaze() inside your register() export.
|
|
3102
|
+
`);
|
|
3103
|
+
return "patched";
|
|
3104
|
+
}
|
|
3105
|
+
var INSPECTOR_PAGE = `// AUTO-GENERATED by \`apiblaze init\` \u2014 dev-only. Safe to delete (rm this folder).
|
|
3106
|
+
// Proves the observe\u2192approve model: an approved origin routes + keeps its auth;
|
|
3107
|
+
// an un-approved origin goes direct and shows up as a candidate to approve.
|
|
3108
|
+
export const dynamic = "force-dynamic";
|
|
3109
|
+
|
|
3110
|
+
async function probe(u: string) {
|
|
3111
|
+
try {
|
|
3112
|
+
const res = await fetch(u, { headers: { "x-api-key": "demo-secret-value" }, cache: "no-store" });
|
|
3113
|
+
return { url: u, status: res.status, echo: await res.json().catch(() => ({})) };
|
|
3114
|
+
} catch (e: any) { return { url: u, error: e?.message ?? String(e) }; }
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
export default async function Page() {
|
|
3118
|
+
if (process.env.NODE_ENV === "production") return <main style={{padding:24}}>Inspector disabled in production.</main>;
|
|
3119
|
+
const r = await probe("https://httpbingo.org/headers");
|
|
3120
|
+
return (
|
|
3121
|
+
<main style={{ fontFamily: "ui-monospace, monospace", padding: 24, lineHeight: 1.6 }}>
|
|
3122
|
+
<h1>APIblaze sidecar inspector</h1>
|
|
3123
|
+
<p>Fetched httpbingo through the interceptor. If httpbingo.org is <b>not yet approved</b>, this went
|
|
3124
|
+
<b> direct</b> and now appears as a candidate in your dashboard \u2014 approve it, wait ~5 min, reload,
|
|
3125
|
+
and it will route through APIblaze (the echo below will show it arrived via your proxy).</p>
|
|
3126
|
+
<pre>{JSON.stringify(r, null, 2)}</pre>
|
|
3127
|
+
<p style={{opacity:.6}}>Delete this folder before shipping.</p>
|
|
3128
|
+
</main>
|
|
3129
|
+
);
|
|
3130
|
+
}
|
|
3131
|
+
`;
|
|
3132
|
+
function generateInspector(root, router) {
|
|
3133
|
+
try {
|
|
3134
|
+
if (router === "pages") {
|
|
3135
|
+
const dir2 = fs6.existsSync(path3.join(root, "src", "pages")) ? path3.join(root, "src", "pages") : path3.join(root, "pages");
|
|
3136
|
+
const f2 = path3.join(dir2, "abz-inspector.tsx");
|
|
3137
|
+
fs6.writeFileSync(f2, INSPECTOR_PAGE);
|
|
3138
|
+
return path3.relative(root, f2);
|
|
3139
|
+
}
|
|
3140
|
+
const base = fs6.existsSync(path3.join(root, "src", "app")) ? path3.join(root, "src", "app") : path3.join(root, "app");
|
|
3141
|
+
const dir = path3.join(base, "abz-inspector");
|
|
3142
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
3143
|
+
const f = path3.join(dir, "page.tsx");
|
|
3144
|
+
fs6.writeFileSync(f, INSPECTOR_PAGE);
|
|
3145
|
+
return path3.relative(root, f);
|
|
3146
|
+
} catch {
|
|
3147
|
+
return null;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
async function runSidecar(opts) {
|
|
3151
|
+
const root = path3.resolve(opts.dir ?? process.cwd());
|
|
3152
|
+
if (!loadCredentials()) {
|
|
3153
|
+
console.log(import_chalk28.default.dim("Not logged in \u2014 starting APIblaze login..."));
|
|
3154
|
+
await runLogin();
|
|
3155
|
+
}
|
|
3156
|
+
const detected = detectNextProject(root);
|
|
3157
|
+
if (!detected.found) {
|
|
3158
|
+
console.log(import_chalk28.default.yellow(`No Next.js project detected in ${root}.`));
|
|
3159
|
+
console.log("Create one (e.g. `npx create-next-app`) and re-run `apiblaze init` inside it.");
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
const { teamId, teamName } = await resolveTeam(opts.team);
|
|
3163
|
+
const spinner = (0, import_ora13.default)("Setting up the sidecar (tenant + non-expiring invoke key)...").start();
|
|
3164
|
+
let token;
|
|
3165
|
+
try {
|
|
3166
|
+
const out = await admin({
|
|
3167
|
+
method: "POST",
|
|
3168
|
+
path: `/teams/${encodeURIComponent(teamId)}/sidecar/setup`,
|
|
3169
|
+
summary: `Set up sidecar for team ${teamName ?? teamId}`
|
|
3170
|
+
});
|
|
3171
|
+
token = out.token;
|
|
3172
|
+
spinner.succeed("Sidecar ready.");
|
|
3173
|
+
} catch (err) {
|
|
3174
|
+
spinner.fail("Setup failed.");
|
|
3175
|
+
throw err;
|
|
3176
|
+
}
|
|
3177
|
+
const envState = upsertEnvLocal(root, token);
|
|
3178
|
+
ensureGitignored(root);
|
|
3179
|
+
console.log(` ${import_chalk28.default.green("\u2713")} .env.local ${envState} (APIBLAZE_TOKEN) \u2014 gitignored`);
|
|
3180
|
+
const wireState = wireInstrumentation(root);
|
|
3181
|
+
console.log(` ${import_chalk28.default.green("\u2713")} instrumentation.ts ${wireState}`);
|
|
3182
|
+
let inspectorPath = null;
|
|
3183
|
+
if (!opts.noInspector) {
|
|
3184
|
+
inspectorPath = generateInspector(root, detected.router);
|
|
3185
|
+
if (inspectorPath) console.log(` ${import_chalk28.default.green("\u2713")} inspector at ${inspectorPath}`);
|
|
3186
|
+
}
|
|
3187
|
+
console.log("");
|
|
3188
|
+
console.log(import_chalk28.default.bold("Done. What happens next:"));
|
|
3189
|
+
console.log(` 1. ${import_chalk28.default.cyan("npm install apiblaze")}`);
|
|
3190
|
+
console.log(` 2. ${import_chalk28.default.cyan("npm run dev")} and use your app \u2014 it works exactly as before (all calls go direct).`);
|
|
3191
|
+
console.log(` 3. The origins your app calls appear as ${import_chalk28.default.bold("candidates")} \u2014 list them: ${import_chalk28.default.cyan("apiblaze origins")}`);
|
|
3192
|
+
console.log(` 4. Approve the ones to route: ${import_chalk28.default.cyan("apiblaze origins approve api.stripe.com")} (or in the dashboard)`);
|
|
3193
|
+
console.log(` \u2026within ~5 min your app starts routing that origin through APIblaze.`);
|
|
3194
|
+
if (inspectorPath) console.log(` \u2022 Try it now: open ${import_chalk28.default.underline("http://localhost:3000/abz-inspector")} (dev only; rm ${path3.dirname(inspectorPath)} before shipping)`);
|
|
3195
|
+
console.log("");
|
|
3196
|
+
console.log(import_chalk28.default.yellow(" \u26A0 APIBLAZE_TOKEN is long-lived and lets a holder call your team's proxies. Never commit it."));
|
|
3197
|
+
console.log(import_chalk28.default.dim(" Your control-plane login stays in ~/.apiblaze \u2014 it never entered this project."));
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
// src/commands/origins.ts
|
|
3201
|
+
var import_chalk29 = __toESM(require("chalk"));
|
|
3202
|
+
var import_ora14 = __toESM(require("ora"));
|
|
3203
|
+
async function runOriginsList(opts) {
|
|
3204
|
+
const { teamId, teamName } = await resolveTeam(opts.team);
|
|
3205
|
+
const out = await admin({
|
|
3206
|
+
method: "GET",
|
|
3207
|
+
path: `/teams/${encodeURIComponent(teamId)}/sidecar/candidates`,
|
|
3208
|
+
summary: `List sidecar origins for team ${teamName ?? teamId}`
|
|
3209
|
+
});
|
|
3210
|
+
if (opts.json) {
|
|
3211
|
+
console.log(JSON.stringify(out));
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
const routed = out.routed ?? [];
|
|
3215
|
+
const candidates = out.candidates ?? [];
|
|
3216
|
+
console.log(import_chalk29.default.bold(`
|
|
3217
|
+
Routed through APIblaze (${routed.length})`));
|
|
3218
|
+
if (!routed.length) console.log(import_chalk29.default.dim(" none yet"));
|
|
3219
|
+
for (const r of routed) console.log(` ${import_chalk29.default.green("\u25CF")} ${r.sidecar_origin} ${import_chalk29.default.dim(`\u2192 ${r.project_id}`)}`);
|
|
3220
|
+
console.log(import_chalk29.default.bold(`
|
|
3221
|
+
Candidates \u2014 going direct, not yet approved (${candidates.length})`));
|
|
3222
|
+
if (!candidates.length) console.log(import_chalk29.default.dim(" none \u2014 run your app to discover the origins it calls"));
|
|
3223
|
+
for (const c of candidates) {
|
|
3224
|
+
console.log(` ${import_chalk29.default.yellow("\u25CB")} ${c.origin} ${import_chalk29.default.dim(`seen ${c.request_count}\xD7, last ${c.last_seen}`)}`);
|
|
3225
|
+
}
|
|
3226
|
+
if (candidates.length) {
|
|
3227
|
+
console.log(import_chalk29.default.dim(`
|
|
3228
|
+
Approve: apiblaze origins approve ${candidates[0].origin.replace("https://", "")}`));
|
|
3229
|
+
console.log(import_chalk29.default.dim(` Dismiss: apiblaze origins deny ${candidates[0].origin.replace("https://", "")}`));
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
async function runOriginsApprove(origin, opts) {
|
|
3233
|
+
const { teamId } = await resolveTeam(opts.team);
|
|
3234
|
+
const spinner = (0, import_ora14.default)(`Approving ${origin}...`).start();
|
|
3235
|
+
try {
|
|
3236
|
+
const out = await admin({
|
|
3237
|
+
method: "POST",
|
|
3238
|
+
path: `/teams/${encodeURIComponent(teamId)}/sidecar/approve`,
|
|
3239
|
+
body: { origin },
|
|
3240
|
+
summary: `Approve sidecar origin ${origin}`
|
|
3241
|
+
});
|
|
3242
|
+
spinner.succeed(`Approved ${origin} \u2192 proxy ${out.project_id}. Your app will route it within ~5 min.`);
|
|
3243
|
+
if (opts.json) console.log(JSON.stringify(out));
|
|
3244
|
+
} catch (err) {
|
|
3245
|
+
spinner.fail("Approve failed.");
|
|
3246
|
+
throw err;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
async function runOriginsDeny(origin, opts) {
|
|
3250
|
+
const { teamId } = await resolveTeam(opts.team);
|
|
3251
|
+
const spinner = (0, import_ora14.default)(`Dismissing ${origin}...`).start();
|
|
3252
|
+
try {
|
|
3253
|
+
await admin({ method: "POST", path: `/teams/${encodeURIComponent(teamId)}/sidecar/dismiss`, body: { origin }, summary: `Dismiss sidecar origin ${origin}` });
|
|
3254
|
+
spinner.succeed(`Dismissed ${origin}. It won't be suggested again.`);
|
|
3255
|
+
} catch (err) {
|
|
3256
|
+
spinner.fail("Dismiss failed.");
|
|
3257
|
+
throw err;
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3040
3261
|
// src/index.ts
|
|
3041
3262
|
var program = new import_commander.Command();
|
|
3042
3263
|
program.name("apiblaze").description("APIblaze CLI \u2014 create & manage API proxies and run dev tunnels").version(version).option("-v, --verbose", "Print the exact series of API calls each command makes (curl-equivalent you could run yourself)");
|
|
@@ -3076,11 +3297,15 @@ var agent = program.command("agent").description("Chat with an assistant that bu
|
|
|
3076
3297
|
agent.command("authz").description("Chat to design and turn on access rules for an API").argument("<project>", "Project name or id").argument("[apiVersion]", "API version (defaults to the project's)").action(action((project, apiVersion) => runAuthz(project, apiVersion)));
|
|
3077
3298
|
agent.command("openapi").description("Chat to build your API spec from real traffic").argument("<project>", "Project name or id").argument("[apiVersion]", "API version (defaults to the project's)").action(action((project, apiVersion) => runOpenapi(project, apiVersion)));
|
|
3078
3299
|
agent.command("mcp").description("Chat to build an MCP server for an API").argument("<project>", "Project name or id").argument("[apiVersion]", "API version (defaults to the project's)").option("--environment <env>", "Environment to publish (default: prod)").action(action((project, apiVersion, opts) => runMcp(project, apiVersion, opts)));
|
|
3300
|
+
program.command("init").aliases(["sidecar"]).description("Wire a Next.js app to route its external fetches through APIblaze (one command)").option("--team <id|name>", "Team to set up under (defaults to your active team)").option("--dir <path>", "Project directory (defaults to cwd)").option("--no-inspector", "Skip generating the dev-only /abz-inspector page").option("-y, --yes", "Skip prompts").action(action((opts) => runSidecar({ team: opts.team, dir: opts.dir, yes: opts.yes, noInspector: opts.inspector === false })));
|
|
3301
|
+
var origins = program.command("origins").description("See which external origins your app calls; approve the ones to route through APIblaze").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Machine-readable output").action(action((opts) => runOriginsList(opts)));
|
|
3302
|
+
origins.command("approve").description("Route an origin through APIblaze (creates its proxy)").argument("<origin>", "Origin, e.g. api.stripe.com").option("--team <id|name>", "Team (defaults to active team)").option("--json", "Machine-readable output").action(action((origin, opts) => runOriginsApprove(origin, opts)));
|
|
3303
|
+
origins.command("deny").description("Dismiss a candidate origin so it stops being suggested").argument("<origin>", "Origin, e.g. sentry.io").option("--team <id|name>", "Team (defaults to active team)").action(action((origin, opts) => runOriginsDeny(origin, opts)));
|
|
3079
3304
|
program.command("dev").description("Put your localhost behind a public URL (dev tunnel)").argument("[port]", "Local port to tunnel (positional; overrides --port)").option("-p, --port <number>", "Local port to tunnel", "3000").option("-o, --capture-file <path>", "Stream full request/response traffic to a file (JSON lines)").action(async (port, opts) => {
|
|
3080
3305
|
try {
|
|
3081
3306
|
const resolved = parseInt(port ?? opts.port, 10);
|
|
3082
3307
|
if (Number.isNaN(resolved)) {
|
|
3083
|
-
console.error(
|
|
3308
|
+
console.error(import_chalk30.default.red(`Invalid port: ${port ?? opts.port}`));
|
|
3084
3309
|
process.exit(1);
|
|
3085
3310
|
}
|
|
3086
3311
|
await runDev({ port: resolved, captureFile: opts.captureFile });
|
|
@@ -3153,7 +3378,7 @@ spec.command("get").description("Print the current OpenAPI document").argument("
|
|
|
3153
3378
|
spec.command("set").description("Replace the stored OpenAPI spec from a local file").argument("<project>", "Project name or id").requiredOption("--file <path>", "OpenAPI JSON or YAML file to upload").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runSpecSet(project, opts)));
|
|
3154
3379
|
var HELP_GROUPS = [
|
|
3155
3380
|
{ title: "Chat", commands: ["agent"] },
|
|
3156
|
-
{ title: "Setup", commands: ["login", "create", "dev", "claim", "team", "whoami", "logout"] },
|
|
3381
|
+
{ title: "Setup", commands: ["login", "create", "init", "origins", "dev", "claim", "team", "whoami", "logout"] },
|
|
3157
3382
|
{ title: "Control plane commands", commands: ["projects", "tenant", "domain", "delete", "target", "throttle", "rename", "spec", "export"] },
|
|
3158
3383
|
{ title: "Data plane commands", commands: [
|
|
3159
3384
|
{ parent: "consumer", sub: "login" },
|
|
@@ -3173,7 +3398,7 @@ function groupedCommandHelp() {
|
|
|
3173
3398
|
const sub = byName.get(e.parent)?.commands.find((s) => s.name() === e.sub);
|
|
3174
3399
|
return sub ? ` ${helpLabel(e).padEnd(width)}${sub.description()}` : "";
|
|
3175
3400
|
}).filter(Boolean).join("\n");
|
|
3176
|
-
return `${
|
|
3401
|
+
return `${import_chalk30.default.bold(g.title)}
|
|
3177
3402
|
${rows}`;
|
|
3178
3403
|
}).join("\n\n");
|
|
3179
3404
|
}
|
|
@@ -3199,13 +3424,13 @@ Examples:
|
|
|
3199
3424
|
`);
|
|
3200
3425
|
function printError(err) {
|
|
3201
3426
|
if (err instanceof ApiError) {
|
|
3202
|
-
console.error(
|
|
3427
|
+
console.error(import_chalk30.default.red(`
|
|
3203
3428
|
API error (${err.status}): ${err.message}`));
|
|
3204
3429
|
} else if (err instanceof Error) {
|
|
3205
|
-
console.error(
|
|
3430
|
+
console.error(import_chalk30.default.red(`
|
|
3206
3431
|
Error: ${err.message}`));
|
|
3207
3432
|
} else {
|
|
3208
|
-
console.error(
|
|
3433
|
+
console.error(import_chalk30.default.red("\nUnknown error"));
|
|
3209
3434
|
}
|
|
3210
3435
|
}
|
|
3211
3436
|
program.parse(process.argv);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* apiblaze/sidecar — the egress interceptor (observe → approve model).
|
|
3
|
+
* Spec: specs/sidecar/apiblaze-sidecar-spec.md §3.
|
|
4
|
+
*
|
|
5
|
+
* // instrumentation.ts
|
|
6
|
+
* import { register as apiblaze } from "apiblaze/sidecar";
|
|
7
|
+
* export function register() { apiblaze(); }
|
|
8
|
+
*
|
|
9
|
+
* At boot it loads the team's APPROVED origins (GET sidecar.abz.run/routes) into
|
|
10
|
+
* an in-memory map. Per request: if the origin is approved it routes through that
|
|
11
|
+
* origin's proxy (carrying the call's own auth); otherwise it goes DIRECT,
|
|
12
|
+
* untouched, and reports the origin once as a candidate. Reads only APIBLAZE_TOKEN.
|
|
13
|
+
* Fail-open: any error → direct. Never breaks the app.
|
|
14
|
+
*/
|
|
15
|
+
interface SidecarOptions {
|
|
16
|
+
token?: string;
|
|
17
|
+
exclude?: string[];
|
|
18
|
+
include?: string[];
|
|
19
|
+
quiet?: boolean;
|
|
20
|
+
}
|
|
21
|
+
declare function register(opts?: SidecarOptions): void;
|
|
22
|
+
declare function withApiblaze<T extends Record<string, unknown>>(nextConfig?: T): T;
|
|
23
|
+
declare const _default: {
|
|
24
|
+
register: typeof register;
|
|
25
|
+
withApiblaze: typeof withApiblaze;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export { type SidecarOptions, _default as default, register, withApiblaze };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* apiblaze/sidecar — the egress interceptor (observe → approve model).
|
|
3
|
+
* Spec: specs/sidecar/apiblaze-sidecar-spec.md §3.
|
|
4
|
+
*
|
|
5
|
+
* // instrumentation.ts
|
|
6
|
+
* import { register as apiblaze } from "apiblaze/sidecar";
|
|
7
|
+
* export function register() { apiblaze(); }
|
|
8
|
+
*
|
|
9
|
+
* At boot it loads the team's APPROVED origins (GET sidecar.abz.run/routes) into
|
|
10
|
+
* an in-memory map. Per request: if the origin is approved it routes through that
|
|
11
|
+
* origin's proxy (carrying the call's own auth); otherwise it goes DIRECT,
|
|
12
|
+
* untouched, and reports the origin once as a candidate. Reads only APIBLAZE_TOKEN.
|
|
13
|
+
* Fail-open: any error → direct. Never breaks the app.
|
|
14
|
+
*/
|
|
15
|
+
interface SidecarOptions {
|
|
16
|
+
token?: string;
|
|
17
|
+
exclude?: string[];
|
|
18
|
+
include?: string[];
|
|
19
|
+
quiet?: boolean;
|
|
20
|
+
}
|
|
21
|
+
declare function register(opts?: SidecarOptions): void;
|
|
22
|
+
declare function withApiblaze<T extends Record<string, unknown>>(nextConfig?: T): T;
|
|
23
|
+
declare const _default: {
|
|
24
|
+
register: typeof register;
|
|
25
|
+
withApiblaze: typeof withApiblaze;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export { type SidecarOptions, _default as default, register, withApiblaze };
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/sidecar/index.ts
|
|
21
|
+
var sidecar_exports = {};
|
|
22
|
+
__export(sidecar_exports, {
|
|
23
|
+
default: () => sidecar_default,
|
|
24
|
+
register: () => register,
|
|
25
|
+
withApiblaze: () => withApiblaze
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(sidecar_exports);
|
|
28
|
+
var SIDECAR_HOST = "https://sidecar.abz.run";
|
|
29
|
+
var REFRESH_MS = 5 * 60 * 1e3;
|
|
30
|
+
var NOISE_DENYLIST = [
|
|
31
|
+
"google-analytics.com",
|
|
32
|
+
"googletagmanager.com",
|
|
33
|
+
"segment.io",
|
|
34
|
+
"sentry.io",
|
|
35
|
+
"ingest.sentry.io",
|
|
36
|
+
"posthog.com",
|
|
37
|
+
"mixpanel.com",
|
|
38
|
+
"amplitude.com",
|
|
39
|
+
"datadoghq.com",
|
|
40
|
+
"newrelic.com",
|
|
41
|
+
"nr-data.net",
|
|
42
|
+
"launchdarkly.com",
|
|
43
|
+
"statsigapi.net",
|
|
44
|
+
"plausible.io",
|
|
45
|
+
"avatars.githubusercontent.com",
|
|
46
|
+
"gravatar.com",
|
|
47
|
+
"googleusercontent.com",
|
|
48
|
+
"cloudfront.net",
|
|
49
|
+
"akamaihd.net",
|
|
50
|
+
"fastly.net",
|
|
51
|
+
"imgix.net",
|
|
52
|
+
"cloudinary.com",
|
|
53
|
+
"registry.npmjs.org",
|
|
54
|
+
"pypi.org"
|
|
55
|
+
];
|
|
56
|
+
var installed = false;
|
|
57
|
+
function canonicalOrigin(input) {
|
|
58
|
+
try {
|
|
59
|
+
const u = new URL(input);
|
|
60
|
+
if (u.protocol !== "https:") return null;
|
|
61
|
+
const port = u.port && u.port !== "443" ? `:${u.port}` : "";
|
|
62
|
+
return `https://${u.hostname.toLowerCase()}${port}`;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function hostMatchesGlob(host, glob) {
|
|
68
|
+
const g = glob.toLowerCase();
|
|
69
|
+
if (g.startsWith("*.")) {
|
|
70
|
+
const b = g.slice(2);
|
|
71
|
+
return host === b || host.endsWith(`.${b}`);
|
|
72
|
+
}
|
|
73
|
+
return host === g;
|
|
74
|
+
}
|
|
75
|
+
function isNoise(host) {
|
|
76
|
+
return NOISE_DENYLIST.some((d) => host === d || host.endsWith(`.${d}`));
|
|
77
|
+
}
|
|
78
|
+
function isPrivateOrLocal(host) {
|
|
79
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
|
|
80
|
+
if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^169\.254\./.test(host)) return true;
|
|
81
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return true;
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
function register(opts) {
|
|
85
|
+
if (installed) return;
|
|
86
|
+
const token = opts?.token ?? process.env.APIBLAZE_TOKEN;
|
|
87
|
+
if (!token) {
|
|
88
|
+
if (!opts?.quiet) console.warn("[apiblaze/sidecar] APIBLAZE_TOKEN not set \u2014 interceptor OFF (fetches pass through unchanged).");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const original = globalThis.fetch;
|
|
92
|
+
if (typeof original !== "function") return;
|
|
93
|
+
const exclude = opts?.exclude ?? [];
|
|
94
|
+
const include = opts?.include ?? [];
|
|
95
|
+
let routes = {};
|
|
96
|
+
const seen = /* @__PURE__ */ new Set();
|
|
97
|
+
const refresh = async () => {
|
|
98
|
+
try {
|
|
99
|
+
const res = await original(`${SIDECAR_HOST}/routes`, { headers: { "x-api-key": token }, cache: "no-store" });
|
|
100
|
+
if (res.ok) routes = await res.json();
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
void refresh();
|
|
105
|
+
const timer = setInterval(refresh, REFRESH_MS);
|
|
106
|
+
if (typeof timer?.unref === "function") timer.unref();
|
|
107
|
+
const reportObserved = (origin) => {
|
|
108
|
+
original(`${SIDECAR_HOST}/observed`, {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "x-api-key": token, "content-type": "application/json" },
|
|
111
|
+
body: JSON.stringify({ origin })
|
|
112
|
+
}).catch(() => {
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
const shouldConsider = (url) => {
|
|
116
|
+
if (url.protocol !== "https:") return false;
|
|
117
|
+
const host = url.hostname.toLowerCase();
|
|
118
|
+
if (host.endsWith("abz.run") || host.endsWith("tryabz.run") || host.endsWith("apiblaze.com")) return false;
|
|
119
|
+
if (isPrivateOrLocal(host) || isNoise(host)) return false;
|
|
120
|
+
if (exclude.some((g) => hostMatchesGlob(host, g))) return false;
|
|
121
|
+
if (include.length > 0 && !include.some((g) => hostMatchesGlob(host, g))) return false;
|
|
122
|
+
return true;
|
|
123
|
+
};
|
|
124
|
+
const wrapped = async (input, init) => {
|
|
125
|
+
try {
|
|
126
|
+
const req = new Request(input, init);
|
|
127
|
+
const url = new URL(req.url);
|
|
128
|
+
if (shouldConsider(url)) {
|
|
129
|
+
const origin = canonicalOrigin(url.origin);
|
|
130
|
+
if (origin) {
|
|
131
|
+
const base = routes[origin];
|
|
132
|
+
if (base) {
|
|
133
|
+
if (!seen.has(origin)) {
|
|
134
|
+
seen.add(origin);
|
|
135
|
+
if (!opts?.quiet) console.log(`[apiblaze/sidecar] routed \u2192 ${origin}`);
|
|
136
|
+
}
|
|
137
|
+
const headers = new Headers(req.headers);
|
|
138
|
+
const appKey = headers.get("x-api-key");
|
|
139
|
+
if (appKey) headers.set("x-abz-fwd-x-api-key", appKey);
|
|
140
|
+
headers.set("x-api-key", token);
|
|
141
|
+
const target = `${base}${url.pathname}${url.search}`;
|
|
142
|
+
return original(target, {
|
|
143
|
+
method: req.method,
|
|
144
|
+
headers,
|
|
145
|
+
body: req.body,
|
|
146
|
+
redirect: "manual",
|
|
147
|
+
duplex: req.body ? "half" : void 0
|
|
148
|
+
// streaming request bodies
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (!seen.has(origin)) {
|
|
152
|
+
seen.add(origin);
|
|
153
|
+
if (!opts?.quiet) console.log(`[apiblaze/sidecar] direct (not approved) \u2192 ${origin} \xB7 approve at your dashboard to route it`);
|
|
154
|
+
reportObserved(origin);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (err) {
|
|
159
|
+
if (!opts?.quiet) console.warn("[apiblaze/sidecar] passthrough after interceptor error:", err?.message);
|
|
160
|
+
}
|
|
161
|
+
return original(input, init);
|
|
162
|
+
};
|
|
163
|
+
wrapped.__apiblaze = true;
|
|
164
|
+
globalThis.fetch = wrapped;
|
|
165
|
+
installed = true;
|
|
166
|
+
if (!opts?.quiet) console.log("[apiblaze/sidecar] active \u2014 approved origins route through APIblaze; the rest go direct.");
|
|
167
|
+
}
|
|
168
|
+
function withApiblaze(nextConfig = {}) {
|
|
169
|
+
register();
|
|
170
|
+
const experimental = { ...nextConfig.experimental };
|
|
171
|
+
if (experimental.instrumentationHook === void 0) experimental.instrumentationHook = true;
|
|
172
|
+
return { ...nextConfig, experimental };
|
|
173
|
+
}
|
|
174
|
+
var sidecar_default = { register, withApiblaze };
|
|
175
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
176
|
+
0 && (module.exports = {
|
|
177
|
+
register,
|
|
178
|
+
withApiblaze
|
|
179
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// src/sidecar/index.ts
|
|
2
|
+
var SIDECAR_HOST = "https://sidecar.abz.run";
|
|
3
|
+
var REFRESH_MS = 5 * 60 * 1e3;
|
|
4
|
+
var NOISE_DENYLIST = [
|
|
5
|
+
"google-analytics.com",
|
|
6
|
+
"googletagmanager.com",
|
|
7
|
+
"segment.io",
|
|
8
|
+
"sentry.io",
|
|
9
|
+
"ingest.sentry.io",
|
|
10
|
+
"posthog.com",
|
|
11
|
+
"mixpanel.com",
|
|
12
|
+
"amplitude.com",
|
|
13
|
+
"datadoghq.com",
|
|
14
|
+
"newrelic.com",
|
|
15
|
+
"nr-data.net",
|
|
16
|
+
"launchdarkly.com",
|
|
17
|
+
"statsigapi.net",
|
|
18
|
+
"plausible.io",
|
|
19
|
+
"avatars.githubusercontent.com",
|
|
20
|
+
"gravatar.com",
|
|
21
|
+
"googleusercontent.com",
|
|
22
|
+
"cloudfront.net",
|
|
23
|
+
"akamaihd.net",
|
|
24
|
+
"fastly.net",
|
|
25
|
+
"imgix.net",
|
|
26
|
+
"cloudinary.com",
|
|
27
|
+
"registry.npmjs.org",
|
|
28
|
+
"pypi.org"
|
|
29
|
+
];
|
|
30
|
+
var installed = false;
|
|
31
|
+
function canonicalOrigin(input) {
|
|
32
|
+
try {
|
|
33
|
+
const u = new URL(input);
|
|
34
|
+
if (u.protocol !== "https:") return null;
|
|
35
|
+
const port = u.port && u.port !== "443" ? `:${u.port}` : "";
|
|
36
|
+
return `https://${u.hostname.toLowerCase()}${port}`;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function hostMatchesGlob(host, glob) {
|
|
42
|
+
const g = glob.toLowerCase();
|
|
43
|
+
if (g.startsWith("*.")) {
|
|
44
|
+
const b = g.slice(2);
|
|
45
|
+
return host === b || host.endsWith(`.${b}`);
|
|
46
|
+
}
|
|
47
|
+
return host === g;
|
|
48
|
+
}
|
|
49
|
+
function isNoise(host) {
|
|
50
|
+
return NOISE_DENYLIST.some((d) => host === d || host.endsWith(`.${d}`));
|
|
51
|
+
}
|
|
52
|
+
function isPrivateOrLocal(host) {
|
|
53
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
|
|
54
|
+
if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^169\.254\./.test(host)) return true;
|
|
55
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return true;
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
function register(opts) {
|
|
59
|
+
if (installed) return;
|
|
60
|
+
const token = opts?.token ?? process.env.APIBLAZE_TOKEN;
|
|
61
|
+
if (!token) {
|
|
62
|
+
if (!opts?.quiet) console.warn("[apiblaze/sidecar] APIBLAZE_TOKEN not set \u2014 interceptor OFF (fetches pass through unchanged).");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const original = globalThis.fetch;
|
|
66
|
+
if (typeof original !== "function") return;
|
|
67
|
+
const exclude = opts?.exclude ?? [];
|
|
68
|
+
const include = opts?.include ?? [];
|
|
69
|
+
let routes = {};
|
|
70
|
+
const seen = /* @__PURE__ */ new Set();
|
|
71
|
+
const refresh = async () => {
|
|
72
|
+
try {
|
|
73
|
+
const res = await original(`${SIDECAR_HOST}/routes`, { headers: { "x-api-key": token }, cache: "no-store" });
|
|
74
|
+
if (res.ok) routes = await res.json();
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
void refresh();
|
|
79
|
+
const timer = setInterval(refresh, REFRESH_MS);
|
|
80
|
+
if (typeof timer?.unref === "function") timer.unref();
|
|
81
|
+
const reportObserved = (origin) => {
|
|
82
|
+
original(`${SIDECAR_HOST}/observed`, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "x-api-key": token, "content-type": "application/json" },
|
|
85
|
+
body: JSON.stringify({ origin })
|
|
86
|
+
}).catch(() => {
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
const shouldConsider = (url) => {
|
|
90
|
+
if (url.protocol !== "https:") return false;
|
|
91
|
+
const host = url.hostname.toLowerCase();
|
|
92
|
+
if (host.endsWith("abz.run") || host.endsWith("tryabz.run") || host.endsWith("apiblaze.com")) return false;
|
|
93
|
+
if (isPrivateOrLocal(host) || isNoise(host)) return false;
|
|
94
|
+
if (exclude.some((g) => hostMatchesGlob(host, g))) return false;
|
|
95
|
+
if (include.length > 0 && !include.some((g) => hostMatchesGlob(host, g))) return false;
|
|
96
|
+
return true;
|
|
97
|
+
};
|
|
98
|
+
const wrapped = async (input, init) => {
|
|
99
|
+
try {
|
|
100
|
+
const req = new Request(input, init);
|
|
101
|
+
const url = new URL(req.url);
|
|
102
|
+
if (shouldConsider(url)) {
|
|
103
|
+
const origin = canonicalOrigin(url.origin);
|
|
104
|
+
if (origin) {
|
|
105
|
+
const base = routes[origin];
|
|
106
|
+
if (base) {
|
|
107
|
+
if (!seen.has(origin)) {
|
|
108
|
+
seen.add(origin);
|
|
109
|
+
if (!opts?.quiet) console.log(`[apiblaze/sidecar] routed \u2192 ${origin}`);
|
|
110
|
+
}
|
|
111
|
+
const headers = new Headers(req.headers);
|
|
112
|
+
const appKey = headers.get("x-api-key");
|
|
113
|
+
if (appKey) headers.set("x-abz-fwd-x-api-key", appKey);
|
|
114
|
+
headers.set("x-api-key", token);
|
|
115
|
+
const target = `${base}${url.pathname}${url.search}`;
|
|
116
|
+
return original(target, {
|
|
117
|
+
method: req.method,
|
|
118
|
+
headers,
|
|
119
|
+
body: req.body,
|
|
120
|
+
redirect: "manual",
|
|
121
|
+
duplex: req.body ? "half" : void 0
|
|
122
|
+
// streaming request bodies
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (!seen.has(origin)) {
|
|
126
|
+
seen.add(origin);
|
|
127
|
+
if (!opts?.quiet) console.log(`[apiblaze/sidecar] direct (not approved) \u2192 ${origin} \xB7 approve at your dashboard to route it`);
|
|
128
|
+
reportObserved(origin);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
if (!opts?.quiet) console.warn("[apiblaze/sidecar] passthrough after interceptor error:", err?.message);
|
|
134
|
+
}
|
|
135
|
+
return original(input, init);
|
|
136
|
+
};
|
|
137
|
+
wrapped.__apiblaze = true;
|
|
138
|
+
globalThis.fetch = wrapped;
|
|
139
|
+
installed = true;
|
|
140
|
+
if (!opts?.quiet) console.log("[apiblaze/sidecar] active \u2014 approved origins route through APIblaze; the rest go direct.");
|
|
141
|
+
}
|
|
142
|
+
function withApiblaze(nextConfig = {}) {
|
|
143
|
+
register();
|
|
144
|
+
const experimental = { ...nextConfig.experimental };
|
|
145
|
+
if (experimental.instrumentationHook === void 0) experimental.instrumentationHook = true;
|
|
146
|
+
return { ...nextConfig, experimental };
|
|
147
|
+
}
|
|
148
|
+
var sidecar_default = { register, withApiblaze };
|
|
149
|
+
export {
|
|
150
|
+
sidecar_default as default,
|
|
151
|
+
register,
|
|
152
|
+
withApiblaze
|
|
153
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apiblaze",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "APIblaze CLI + sidecar — manage API proxies, run dev tunnels, and route a Next.js app's egress through APIblaze with one command",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"apiblaze",
|
|
7
7
|
"dev-tunnel",
|
|
8
8
|
"cloudflared",
|
|
9
9
|
"api",
|
|
10
|
-
"proxy"
|
|
10
|
+
"proxy",
|
|
11
|
+
"sidecar",
|
|
12
|
+
"nextjs",
|
|
13
|
+
"egress"
|
|
11
14
|
],
|
|
12
15
|
"license": "MIT",
|
|
13
16
|
"author": "APIblaze",
|
|
@@ -15,6 +18,15 @@
|
|
|
15
18
|
"apiblaze": "dist/index.js"
|
|
16
19
|
},
|
|
17
20
|
"main": "./dist/index.js",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./dist/index.js",
|
|
23
|
+
"./sidecar": {
|
|
24
|
+
"types": "./dist/sidecar/index.d.ts",
|
|
25
|
+
"import": "./dist/sidecar/index.mjs",
|
|
26
|
+
"require": "./dist/sidecar/index.js",
|
|
27
|
+
"default": "./dist/sidecar/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
18
30
|
"files": [
|
|
19
31
|
"dist/",
|
|
20
32
|
"README.md"
|