apiblaze 0.4.13 → 0.5.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 +206 -19
- package/dist/sidecar/index.d.mts +50 -0
- package/dist/sidecar/index.d.ts +50 -0
- package/dist/sidecar/index.js +192 -0
- package/dist/sidecar/index.mjs +166 -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_chalk29 = __toESM(require("chalk"));
|
|
29
29
|
|
|
30
30
|
// package.json
|
|
31
|
-
var version = "0.
|
|
31
|
+
var version = "0.5.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;
|
|
@@ -3037,6 +3037,192 @@ 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 {
|
|
3056
|
+
found: hasConfig || hasDep || appDir || pagesDir,
|
|
3057
|
+
router: appDir ? "app" : pagesDir ? "pages" : null
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
function upsertEnvLocal(root, token, tenant2) {
|
|
3061
|
+
const p = path3.join(root, ".env.local");
|
|
3062
|
+
let existing = "";
|
|
3063
|
+
try {
|
|
3064
|
+
existing = fs6.readFileSync(p, "utf8");
|
|
3065
|
+
} catch {
|
|
3066
|
+
}
|
|
3067
|
+
const hadToken = /^APIBLAZE_TOKEN=/m.test(existing);
|
|
3068
|
+
let next = existing;
|
|
3069
|
+
const set = (k, v) => {
|
|
3070
|
+
if (new RegExp(`^${k}=`, "m").test(next)) next = next.replace(new RegExp(`^${k}=.*$`, "m"), `${k}=${v}`);
|
|
3071
|
+
else next = (next && !next.endsWith("\n") ? next + "\n" : next) + `${k}=${v}
|
|
3072
|
+
`;
|
|
3073
|
+
};
|
|
3074
|
+
set("APIBLAZE_TOKEN", token);
|
|
3075
|
+
set("APIBLAZE_TENANT", tenant2);
|
|
3076
|
+
fs6.writeFileSync(p, next);
|
|
3077
|
+
return hadToken ? "rotated" : existing ? "reused" : "created";
|
|
3078
|
+
}
|
|
3079
|
+
function ensureGitignored(root) {
|
|
3080
|
+
const p = path3.join(root, ".gitignore");
|
|
3081
|
+
let content = "";
|
|
3082
|
+
try {
|
|
3083
|
+
content = fs6.readFileSync(p, "utf8");
|
|
3084
|
+
} catch {
|
|
3085
|
+
}
|
|
3086
|
+
if (!/^\.env\.local$/m.test(content) && !/^\.env\*/m.test(content)) {
|
|
3087
|
+
fs6.writeFileSync(p, (content && !content.endsWith("\n") ? content + "\n" : content) + ".env.local\n");
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
function wireInstrumentation(root) {
|
|
3091
|
+
const candidates = ["instrumentation.ts", "instrumentation.js", path3.join("src", "instrumentation.ts")];
|
|
3092
|
+
const existing = candidates.map((c) => path3.join(root, c)).find((f) => fs6.existsSync(f));
|
|
3093
|
+
const body = `import { register as apiblaze } from "apiblaze/sidecar";
|
|
3094
|
+
|
|
3095
|
+
export function register() {
|
|
3096
|
+
apiblaze();
|
|
3097
|
+
}
|
|
3098
|
+
`;
|
|
3099
|
+
if (!existing) {
|
|
3100
|
+
fs6.writeFileSync(path3.join(root, "instrumentation.ts"), body);
|
|
3101
|
+
return "created";
|
|
3102
|
+
}
|
|
3103
|
+
const cur = fs6.readFileSync(existing, "utf8");
|
|
3104
|
+
if (cur.includes("apiblaze/sidecar")) return "present";
|
|
3105
|
+
if (/export\s+function\s+register\s*\(/.test(cur)) {
|
|
3106
|
+
const patched = `import { register as apiblaze } from "apiblaze/sidecar";
|
|
3107
|
+
` + cur.replace(/export\s+function\s+register\s*\(\s*\)\s*\{/, (m) => `${m}
|
|
3108
|
+
apiblaze();`);
|
|
3109
|
+
fs6.writeFileSync(existing, patched);
|
|
3110
|
+
return "patched";
|
|
3111
|
+
}
|
|
3112
|
+
fs6.writeFileSync(existing, `import { register as apiblaze } from "apiblaze/sidecar";
|
|
3113
|
+
${cur}
|
|
3114
|
+
// apiblaze(): call apiblaze() inside your register() export.
|
|
3115
|
+
`);
|
|
3116
|
+
return "patched";
|
|
3117
|
+
}
|
|
3118
|
+
var INSPECTOR_PAGE = `// AUTO-GENERATED by \`apiblaze sidecar\` \u2014 dev-only smoke test. Safe to delete.
|
|
3119
|
+
// Proves the whole lazy path: interception \u2192 edge auth \u2192 auto-provision \u2192
|
|
3120
|
+
// header transform \u2192 convergence. Fires a fetch at httpbingo THROUGH the sidecar
|
|
3121
|
+
// and echoes what the upstream actually received.
|
|
3122
|
+
export const dynamic = "force-dynamic";
|
|
3123
|
+
|
|
3124
|
+
async function probe() {
|
|
3125
|
+
const res = await fetch("https://httpbingo.org/headers", {
|
|
3126
|
+
headers: { "x-api-key": "demo-secret-value" },
|
|
3127
|
+
cache: "no-store",
|
|
3128
|
+
});
|
|
3129
|
+
const body = await res.json().catch(() => ({}));
|
|
3130
|
+
return { status: res.status, sidecar: res.headers.get("x-abz-sidecar"), echo: body };
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
export default async function Page() {
|
|
3134
|
+
if (process.env.NODE_ENV === "production") {
|
|
3135
|
+
return <main style={{ padding: 24 }}>Inspector is disabled in production.</main>;
|
|
3136
|
+
}
|
|
3137
|
+
let result: any = null, error: string | null = null;
|
|
3138
|
+
try { result = await probe(); } catch (e: any) { error = e?.message ?? String(e); }
|
|
3139
|
+
const ok = result && result.status === 200;
|
|
3140
|
+
return (
|
|
3141
|
+
<main style={{ fontFamily: "ui-monospace, monospace", padding: 24, lineHeight: 1.6 }}>
|
|
3142
|
+
<h1>APIblaze sidecar inspector</h1>
|
|
3143
|
+
<p>{ok ? "\u2705 intercepted + relayed" : "\u26A0\uFE0F check the result below"}</p>
|
|
3144
|
+
<p>edge path: <b>{result?.sidecar ?? "(no x-abz-sidecar header \u2014 was the fetch intercepted?)"}</b>
|
|
3145
|
+
{" "}(first call \u2192 "miss"; after ~60s of propagation, provisioned path)</p>
|
|
3146
|
+
<p>The upstream echoes the headers it received \u2014 you should see <code>x-api-key</code>
|
|
3147
|
+
(the marker suffix stripped), and NOT <code>x-abz-target</code> or any APIblaze identity header.</p>
|
|
3148
|
+
{error && <pre style={{ color: "crimson" }}>{error}</pre>}
|
|
3149
|
+
<pre>{JSON.stringify(result?.echo ?? {}, null, 2)}</pre>
|
|
3150
|
+
<form><button formAction="" style={{ padding: "8px 16px" }}>Run again</button></form>
|
|
3151
|
+
<p style={{ opacity: 0.6 }}>Delete app/abz-inspector/ (or pages/abz-inspector) before shipping.</p>
|
|
3152
|
+
</main>
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
`;
|
|
3156
|
+
function generateInspector(root, router) {
|
|
3157
|
+
try {
|
|
3158
|
+
if (router === "pages") {
|
|
3159
|
+
const dir2 = fs6.existsSync(path3.join(root, "src", "pages")) ? path3.join(root, "src", "pages") : path3.join(root, "pages");
|
|
3160
|
+
const f2 = path3.join(dir2, "abz-inspector.tsx");
|
|
3161
|
+
fs6.writeFileSync(f2, INSPECTOR_PAGE);
|
|
3162
|
+
return path3.relative(root, f2);
|
|
3163
|
+
}
|
|
3164
|
+
const base = fs6.existsSync(path3.join(root, "src", "app")) ? path3.join(root, "src", "app") : path3.join(root, "app");
|
|
3165
|
+
const dir = path3.join(base, "abz-inspector");
|
|
3166
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
3167
|
+
const f = path3.join(dir, "page.tsx");
|
|
3168
|
+
fs6.writeFileSync(f, INSPECTOR_PAGE);
|
|
3169
|
+
return path3.relative(root, f);
|
|
3170
|
+
} catch {
|
|
3171
|
+
return null;
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
async function runSidecar(opts) {
|
|
3175
|
+
const root = path3.resolve(opts.dir ?? process.cwd());
|
|
3176
|
+
if (!loadCredentials()) {
|
|
3177
|
+
console.log(import_chalk28.default.dim("Not logged in \u2014 starting APIblaze login first..."));
|
|
3178
|
+
await runLogin();
|
|
3179
|
+
}
|
|
3180
|
+
const detected = detectNextProject(root);
|
|
3181
|
+
if (!detected.found) {
|
|
3182
|
+
console.log(import_chalk28.default.yellow(`No Next.js project detected in ${root}.`));
|
|
3183
|
+
console.log("Create one first (e.g. `npx create-next-app`), then re-run `apiblaze sidecar` inside it.");
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
const { teamId, teamName } = await resolveTeam(opts.team);
|
|
3187
|
+
const spinner = (0, import_ora13.default)("Minting a scoped sidecar key...").start();
|
|
3188
|
+
let key, tenant2, keyId;
|
|
3189
|
+
try {
|
|
3190
|
+
const out = await admin({
|
|
3191
|
+
method: "POST",
|
|
3192
|
+
path: `/teams/${encodeURIComponent(teamId)}/developer-keys`,
|
|
3193
|
+
body: { role: "sidecar", description: `sidecar key for ${path3.basename(root)}` },
|
|
3194
|
+
summary: `Mint a sidecar data-plane key for team ${teamName ?? teamId}`
|
|
3195
|
+
});
|
|
3196
|
+
key = out.key;
|
|
3197
|
+
tenant2 = out.tenant;
|
|
3198
|
+
keyId = out.key_id;
|
|
3199
|
+
spinner.succeed("Sidecar key minted.");
|
|
3200
|
+
} catch (err) {
|
|
3201
|
+
spinner.fail("Key mint failed.");
|
|
3202
|
+
throw err;
|
|
3203
|
+
}
|
|
3204
|
+
const envState = upsertEnvLocal(root, key, tenant2);
|
|
3205
|
+
ensureGitignored(root);
|
|
3206
|
+
console.log(` ${import_chalk28.default.green("\u2713")} .env.local ${envState} (APIBLAZE_TOKEN, APIBLAZE_TENANT) \u2014 gitignored`);
|
|
3207
|
+
const wireState = wireInstrumentation(root);
|
|
3208
|
+
console.log(` ${import_chalk28.default.green("\u2713")} instrumentation.ts ${wireState} (register apiblaze/sidecar)`);
|
|
3209
|
+
console.log(` ${import_chalk28.default.dim("\u2022")} install the runtime dep: ${import_chalk28.default.cyan("npm install apiblaze")}`);
|
|
3210
|
+
let inspectorPath = null;
|
|
3211
|
+
if (!opts.noInspector) {
|
|
3212
|
+
inspectorPath = generateInspector(root, detected.router);
|
|
3213
|
+
if (inspectorPath) console.log(` ${import_chalk28.default.green("\u2713")} inspector at ${inspectorPath} \u2192 visit /abz-inspector (dev only)`);
|
|
3214
|
+
}
|
|
3215
|
+
console.log("");
|
|
3216
|
+
console.log(import_chalk28.default.bold("Done. Next steps:"));
|
|
3217
|
+
console.log(` 1. ${import_chalk28.default.cyan("npm install apiblaze")}`);
|
|
3218
|
+
console.log(` 2. ${import_chalk28.default.cyan("npm run dev")} then open ${import_chalk28.default.underline("http://localhost:3000/abz-inspector")}`);
|
|
3219
|
+
console.log(` 3. Your external https fetches now route through APIblaze \u2014 see them in the dashboard (badged "auto-provisioned").`);
|
|
3220
|
+
console.log("");
|
|
3221
|
+
console.log(import_chalk28.default.dim(` Key id ${keyId} \xB7 revoke anytime: apiblaze apikeys revoke ${keyId}`));
|
|
3222
|
+
if (inspectorPath) console.log(import_chalk28.default.dim(` Remove the inspector before shipping: rm -rf ${path3.dirname(inspectorPath)}`));
|
|
3223
|
+
console.log(import_chalk28.default.dim(" The control-plane login stays in ~/.apiblaze \u2014 it never entered this project."));
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3040
3226
|
// src/index.ts
|
|
3041
3227
|
var program = new import_commander.Command();
|
|
3042
3228
|
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 +3262,12 @@ var agent = program.command("agent").description("Chat with an assistant that bu
|
|
|
3076
3262
|
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
3263
|
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
3264
|
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)));
|
|
3265
|
+
program.command("sidecar").description("Wire a Next.js app to route its external fetches through APIblaze (one command)").option("--team <id|name>", "Team to provision under (defaults to your active team)").option("--dir <path>", "Project directory (defaults to cwd)").option("--no-inspector", "Skip generating the dev-only /abz-inspector smoke-test page").option("-y, --yes", "Skip prompts").action(action((opts) => runSidecar({ team: opts.team, dir: opts.dir, yes: opts.yes, noInspector: opts.inspector === false })));
|
|
3079
3266
|
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
3267
|
try {
|
|
3081
3268
|
const resolved = parseInt(port ?? opts.port, 10);
|
|
3082
3269
|
if (Number.isNaN(resolved)) {
|
|
3083
|
-
console.error(
|
|
3270
|
+
console.error(import_chalk29.default.red(`Invalid port: ${port ?? opts.port}`));
|
|
3084
3271
|
process.exit(1);
|
|
3085
3272
|
}
|
|
3086
3273
|
await runDev({ port: resolved, captureFile: opts.captureFile });
|
|
@@ -3127,7 +3314,7 @@ program.command("projects").description("List the projects in your team").action
|
|
|
3127
3314
|
}
|
|
3128
3315
|
});
|
|
3129
3316
|
program.command("delete").description("Delete a proxy and everything under it (asks first)").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[version]", "API version (defaults to the first match)").option("--team <id|name>", "Team the project is in (defaults to active team)").option("-y, --yes", "Skip the confirmation prompt").option("--json", "Output machine-readable JSON").action(action((project, version2, opts) => runDelete(project, version2, opts)));
|
|
3130
|
-
program.command("export").description("Export
|
|
3317
|
+
program.command("export").description("Export config and data for migration out of APIblaze (Kong, ...)").argument("<project>", "Project name or id (see `apiblaze projects`)").argument("[version]", "API version (defaults to the first match)").option("--kong", "Produce a runnable Kong OSS bundle (decK config + plugins + docker-compose)").option("--data", "Plain data export (default)").option("--secrets", "Include decrypted producer-supplied secrets (member role; audit-logged)").option("--keys <mode>", "API-key export: hashes (default, consumers keep keys) | mint | none").option("--no-consumers", "Skip the end-user lane (users, groups, keys)").option("-o, --out <file>", "Output zip path").option("--team <id|name>", "Team the project is in (defaults to active team)").action(action((project, version2, opts) => runExport(project, version2, { ...opts, noConsumers: opts.consumers === false })));
|
|
3131
3318
|
program.command("target").description("Change where a proxy forwards requests").argument("<project>", "Project name or id").requiredOption("--url <url>", "Target URL to forward to").option("--env <env>", "Environment to scope the target to (e.g. prod, dev)").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version (defaults to the first match)").option("--json", "Output machine-readable JSON").action(action((project, opts) => runTargetSet(project, opts)));
|
|
3132
3319
|
program.command("throttle").description("Set rate limits and quotas for a proxy").argument("<project>", "Project name or id").option("--rate <n>", "User rate limit (requests/sec)").option("--end-user-rate <n>", "Per-end-user rate limit (requests/sec)").option("--quota <n>", "Proxy quota (requests/period)").option("--period <p>", "Quota period: daily | weekly | monthly").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runThrottleSet(project, opts)));
|
|
3133
3320
|
program.command("rename").description("Change a proxy's display name").argument("<project>", "Project name or id").requiredOption("--display-name <name>", "New human-friendly display name").option("--team <id|name>", "Team the project is in").option("--apiversion <version>", "API version").option("--json", "Output machine-readable JSON").action(action((project, opts) => runRename(project, opts)));
|
|
@@ -3153,8 +3340,8 @@ spec.command("get").description("Print the current OpenAPI document").argument("
|
|
|
3153
3340
|
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
3341
|
var HELP_GROUPS = [
|
|
3155
3342
|
{ title: "Chat", commands: ["agent"] },
|
|
3156
|
-
{ title: "Setup", commands: ["login", "create", "dev", "claim", "team", "whoami", "logout"] },
|
|
3157
|
-
{ title: "Control plane commands", commands: ["projects", "tenant", "domain", "delete", "target", "throttle", "rename", "spec"] },
|
|
3343
|
+
{ title: "Setup", commands: ["login", "create", "sidecar", "dev", "claim", "team", "whoami", "logout"] },
|
|
3344
|
+
{ title: "Control plane commands", commands: ["projects", "tenant", "domain", "delete", "target", "throttle", "rename", "spec", "export"] },
|
|
3158
3345
|
{ title: "Data plane commands", commands: [
|
|
3159
3346
|
{ parent: "consumer", sub: "login" },
|
|
3160
3347
|
{ parent: "consumer", sub: "apikeys" }
|
|
@@ -3173,7 +3360,7 @@ function groupedCommandHelp() {
|
|
|
3173
3360
|
const sub = byName.get(e.parent)?.commands.find((s) => s.name() === e.sub);
|
|
3174
3361
|
return sub ? ` ${helpLabel(e).padEnd(width)}${sub.description()}` : "";
|
|
3175
3362
|
}).filter(Boolean).join("\n");
|
|
3176
|
-
return `${
|
|
3363
|
+
return `${import_chalk29.default.bold(g.title)}
|
|
3177
3364
|
${rows}`;
|
|
3178
3365
|
}).join("\n\n");
|
|
3179
3366
|
}
|
|
@@ -3199,13 +3386,13 @@ Examples:
|
|
|
3199
3386
|
`);
|
|
3200
3387
|
function printError(err) {
|
|
3201
3388
|
if (err instanceof ApiError) {
|
|
3202
|
-
console.error(
|
|
3389
|
+
console.error(import_chalk29.default.red(`
|
|
3203
3390
|
API error (${err.status}): ${err.message}`));
|
|
3204
3391
|
} else if (err instanceof Error) {
|
|
3205
|
-
console.error(
|
|
3392
|
+
console.error(import_chalk29.default.red(`
|
|
3206
3393
|
Error: ${err.message}`));
|
|
3207
3394
|
} else {
|
|
3208
|
-
console.error(
|
|
3395
|
+
console.error(import_chalk29.default.red("\nUnknown error"));
|
|
3209
3396
|
}
|
|
3210
3397
|
}
|
|
3211
3398
|
program.parse(process.argv);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* apiblaze/sidecar — the egress interceptor.
|
|
3
|
+
* Spec: specs/sidecar/apiblaze-sidecar-implementation-spec.md §11.
|
|
4
|
+
*
|
|
5
|
+
* Import in a Next.js backend to route outbound fetch() calls through APIblaze:
|
|
6
|
+
*
|
|
7
|
+
* // instrumentation.ts
|
|
8
|
+
* import { register as apiblaze } from "apiblaze/sidecar";
|
|
9
|
+
* export function register() { apiblaze(); }
|
|
10
|
+
*
|
|
11
|
+
* // or next.config.js
|
|
12
|
+
* import { withApiblaze } from "apiblaze/sidecar";
|
|
13
|
+
* export default withApiblaze({ /* your next config *\/ });
|
|
14
|
+
*
|
|
15
|
+
* Reads APIBLAZE_TOKEN + APIBLAZE_TENANT from env. STATELESS — it never learns
|
|
16
|
+
* whether a proxy exists; it applies the same transform on call 1 and call N.
|
|
17
|
+
* All state lives at the edge. If either env var is missing it no-ops with a
|
|
18
|
+
* single boot warning and NEVER breaks the app.
|
|
19
|
+
*/
|
|
20
|
+
interface SidecarOptions {
|
|
21
|
+
token?: string;
|
|
22
|
+
tenant?: string;
|
|
23
|
+
/** Extra origin globs to never intercept (in addition to the noise denylist). */
|
|
24
|
+
exclude?: string[];
|
|
25
|
+
/** Only intercept these origin globs (when set, everything else passes direct). */
|
|
26
|
+
include?: string[];
|
|
27
|
+
/** Suppress the boot log line. */
|
|
28
|
+
quiet?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Install the interceptor. Wraps the current globalThis.fetch (which Next has
|
|
32
|
+
* already patched for cache semantics) so caching survives — we only reroute the
|
|
33
|
+
* actual egress. Idempotent: safe to call from both next.config and
|
|
34
|
+
* instrumentation without double-wrapping.
|
|
35
|
+
*/
|
|
36
|
+
declare function register(opts?: SidecarOptions): void;
|
|
37
|
+
/**
|
|
38
|
+
* next.config wrapper: injects instrumentation registration. Returns the config
|
|
39
|
+
* with `instrumentationHook` enabled (Next <15) — on Next 15+ instrumentation is
|
|
40
|
+
* always on, so the flag is a harmless no-op. The actual register() call still
|
|
41
|
+
* needs an instrumentation.ts (the scaffolder writes one); this wrapper mainly
|
|
42
|
+
* exists as the documented most-zero entry point and to enable the hook.
|
|
43
|
+
*/
|
|
44
|
+
declare function withApiblaze<T extends Record<string, unknown>>(nextConfig?: T): T;
|
|
45
|
+
declare const _default: {
|
|
46
|
+
register: typeof register;
|
|
47
|
+
withApiblaze: typeof withApiblaze;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export { type SidecarOptions, _default as default, register, withApiblaze };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* apiblaze/sidecar — the egress interceptor.
|
|
3
|
+
* Spec: specs/sidecar/apiblaze-sidecar-implementation-spec.md §11.
|
|
4
|
+
*
|
|
5
|
+
* Import in a Next.js backend to route outbound fetch() calls through APIblaze:
|
|
6
|
+
*
|
|
7
|
+
* // instrumentation.ts
|
|
8
|
+
* import { register as apiblaze } from "apiblaze/sidecar";
|
|
9
|
+
* export function register() { apiblaze(); }
|
|
10
|
+
*
|
|
11
|
+
* // or next.config.js
|
|
12
|
+
* import { withApiblaze } from "apiblaze/sidecar";
|
|
13
|
+
* export default withApiblaze({ /* your next config *\/ });
|
|
14
|
+
*
|
|
15
|
+
* Reads APIBLAZE_TOKEN + APIBLAZE_TENANT from env. STATELESS — it never learns
|
|
16
|
+
* whether a proxy exists; it applies the same transform on call 1 and call N.
|
|
17
|
+
* All state lives at the edge. If either env var is missing it no-ops with a
|
|
18
|
+
* single boot warning and NEVER breaks the app.
|
|
19
|
+
*/
|
|
20
|
+
interface SidecarOptions {
|
|
21
|
+
token?: string;
|
|
22
|
+
tenant?: string;
|
|
23
|
+
/** Extra origin globs to never intercept (in addition to the noise denylist). */
|
|
24
|
+
exclude?: string[];
|
|
25
|
+
/** Only intercept these origin globs (when set, everything else passes direct). */
|
|
26
|
+
include?: string[];
|
|
27
|
+
/** Suppress the boot log line. */
|
|
28
|
+
quiet?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Install the interceptor. Wraps the current globalThis.fetch (which Next has
|
|
32
|
+
* already patched for cache semantics) so caching survives — we only reroute the
|
|
33
|
+
* actual egress. Idempotent: safe to call from both next.config and
|
|
34
|
+
* instrumentation without double-wrapping.
|
|
35
|
+
*/
|
|
36
|
+
declare function register(opts?: SidecarOptions): void;
|
|
37
|
+
/**
|
|
38
|
+
* next.config wrapper: injects instrumentation registration. Returns the config
|
|
39
|
+
* with `instrumentationHook` enabled (Next <15) — on Next 15+ instrumentation is
|
|
40
|
+
* always on, so the flag is a harmless no-op. The actual register() call still
|
|
41
|
+
* needs an instrumentation.ts (the scaffolder writes one); this wrapper mainly
|
|
42
|
+
* exists as the documented most-zero entry point and to enable the hook.
|
|
43
|
+
*/
|
|
44
|
+
declare function withApiblaze<T extends Record<string, unknown>>(nextConfig?: T): T;
|
|
45
|
+
declare const _default: {
|
|
46
|
+
register: typeof register;
|
|
47
|
+
withApiblaze: typeof withApiblaze;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export { type SidecarOptions, _default as default, register, withApiblaze };
|
|
@@ -0,0 +1,192 @@
|
|
|
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 ABZ_TARGET_HEADER = "x-abz-target";
|
|
29
|
+
var MARK_SUFFIX = "-abz-target";
|
|
30
|
+
var DATA_PLANE = "abz.run";
|
|
31
|
+
var NOISE_DENYLIST = [
|
|
32
|
+
"google-analytics.com",
|
|
33
|
+
"googletagmanager.com",
|
|
34
|
+
"segment.io",
|
|
35
|
+
"segment.com",
|
|
36
|
+
"sentry.io",
|
|
37
|
+
"ingest.sentry.io",
|
|
38
|
+
"posthog.com",
|
|
39
|
+
"mixpanel.com",
|
|
40
|
+
"amplitude.com",
|
|
41
|
+
"datadoghq.com",
|
|
42
|
+
"newrelic.com",
|
|
43
|
+
"nr-data.net",
|
|
44
|
+
"honeycomb.io",
|
|
45
|
+
"launchdarkly.com",
|
|
46
|
+
"statsigapi.net",
|
|
47
|
+
"plausible.io",
|
|
48
|
+
"hotjar.com",
|
|
49
|
+
"fullstory.com",
|
|
50
|
+
"intercom.io",
|
|
51
|
+
"avatars.githubusercontent.com",
|
|
52
|
+
"gravatar.com",
|
|
53
|
+
"googleusercontent.com",
|
|
54
|
+
"cloudfront.net",
|
|
55
|
+
"akamaihd.net",
|
|
56
|
+
"fastly.net",
|
|
57
|
+
"imgix.net",
|
|
58
|
+
"cloudinary.com",
|
|
59
|
+
"unsplash.com",
|
|
60
|
+
"twimg.com",
|
|
61
|
+
"fbcdn.net",
|
|
62
|
+
"registry.npmjs.org",
|
|
63
|
+
"pypi.org",
|
|
64
|
+
"github.io"
|
|
65
|
+
];
|
|
66
|
+
var installed = false;
|
|
67
|
+
function canonicalizeOrigin(input) {
|
|
68
|
+
try {
|
|
69
|
+
const u = new URL(input);
|
|
70
|
+
if (u.protocol !== "https:") return null;
|
|
71
|
+
const port = u.port && u.port !== "443" ? `:${u.port}` : "";
|
|
72
|
+
return `https://${u.hostname.toLowerCase()}${port}`;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function sha256Hex(s) {
|
|
78
|
+
const data = new TextEncoder().encode(s);
|
|
79
|
+
const buf = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
80
|
+
return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
81
|
+
}
|
|
82
|
+
async function sidecarHost(tenant, canonicalOrigin) {
|
|
83
|
+
const h = await sha256Hex(`${tenant}|${canonicalOrigin}`);
|
|
84
|
+
return `sc${h.slice(0, 16)}.${DATA_PLANE}`;
|
|
85
|
+
}
|
|
86
|
+
function hostMatchesGlob(host, glob) {
|
|
87
|
+
const g = glob.toLowerCase();
|
|
88
|
+
if (g.startsWith("*.")) {
|
|
89
|
+
const base = g.slice(2);
|
|
90
|
+
return host === base || host.endsWith(`.${base}`);
|
|
91
|
+
}
|
|
92
|
+
return host === g;
|
|
93
|
+
}
|
|
94
|
+
function isNoise(host) {
|
|
95
|
+
return NOISE_DENYLIST.some((d) => host === d || host.endsWith(`.${d}`));
|
|
96
|
+
}
|
|
97
|
+
function isPrivateOrLocal(host) {
|
|
98
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
|
|
99
|
+
if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host)) return true;
|
|
100
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return true;
|
|
101
|
+
if (/^169\.254\./.test(host)) return true;
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
function shouldIntercept(cfg, url) {
|
|
105
|
+
if (url.protocol !== "https:") return false;
|
|
106
|
+
const host = url.hostname.toLowerCase();
|
|
107
|
+
if (host.endsWith(DATA_PLANE) || host.endsWith("tryabz.run") || host.endsWith("apiblaze.com")) return false;
|
|
108
|
+
if (isPrivateOrLocal(host)) return false;
|
|
109
|
+
if (isNoise(host)) return false;
|
|
110
|
+
if (cfg.exclude.some((g) => hostMatchesGlob(host, g))) return false;
|
|
111
|
+
if (cfg.include.length > 0 && !cfg.include.some((g) => hostMatchesGlob(host, g))) return false;
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
async function rewrite(cfg, input) {
|
|
115
|
+
const url = new URL(input.url);
|
|
116
|
+
if (!shouldIntercept(cfg, url)) return null;
|
|
117
|
+
const origin = canonicalizeOrigin(url.origin);
|
|
118
|
+
if (!origin) return null;
|
|
119
|
+
const host = await sidecarHost(cfg.tenant, origin);
|
|
120
|
+
const newUrl = new URL(input.url);
|
|
121
|
+
newUrl.protocol = "https:";
|
|
122
|
+
newUrl.host = host;
|
|
123
|
+
newUrl.pathname = `/1.0.0/prod${url.pathname === "/" ? "" : url.pathname}`;
|
|
124
|
+
const headers = new Headers(input.headers);
|
|
125
|
+
const marked = new Headers();
|
|
126
|
+
headers.forEach((value, name) => {
|
|
127
|
+
const n = name.toLowerCase();
|
|
128
|
+
if (n === "authorization") {
|
|
129
|
+
marked.set(`authorization${MARK_SUFFIX}`, value);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (n.startsWith("x-") && !n.startsWith("x-abz-")) {
|
|
133
|
+
marked.set(`${n}${MARK_SUFFIX}`, value);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
marked.set(n, value);
|
|
137
|
+
});
|
|
138
|
+
marked.set(ABZ_TARGET_HEADER, origin);
|
|
139
|
+
marked.set("x-api-key", cfg.token);
|
|
140
|
+
return new Request(newUrl.toString(), {
|
|
141
|
+
method: input.method,
|
|
142
|
+
headers: marked,
|
|
143
|
+
body: input.body,
|
|
144
|
+
redirect: "manual",
|
|
145
|
+
// @ts-expect-error duplex needed for streaming bodies on some runtimes
|
|
146
|
+
duplex: input.body ? "half" : void 0
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
function resolveConfig(opts) {
|
|
150
|
+
const token = opts?.token ?? process.env.APIBLAZE_TOKEN;
|
|
151
|
+
const tenant = opts?.tenant ?? process.env.APIBLAZE_TENANT;
|
|
152
|
+
if (!token || !tenant) {
|
|
153
|
+
if (!opts?.quiet) {
|
|
154
|
+
console.warn("[apiblaze/sidecar] APIBLAZE_TOKEN and APIBLAZE_TENANT are required \u2014 interceptor is OFF (fetches pass through unchanged).");
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return { token, tenant, exclude: opts?.exclude ?? [], include: opts?.include ?? [] };
|
|
159
|
+
}
|
|
160
|
+
function register(opts) {
|
|
161
|
+
if (installed) return;
|
|
162
|
+
const cfg = resolveConfig(opts);
|
|
163
|
+
if (!cfg) return;
|
|
164
|
+
const original = globalThis.fetch;
|
|
165
|
+
if (typeof original !== "function") return;
|
|
166
|
+
const wrapped = async (input, init) => {
|
|
167
|
+
try {
|
|
168
|
+
const req = new Request(input, init);
|
|
169
|
+
const rewritten = await rewrite(cfg, req);
|
|
170
|
+
if (rewritten) return original(rewritten);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (!opts?.quiet) console.warn("[apiblaze/sidecar] passthrough after interceptor error:", err?.message);
|
|
173
|
+
}
|
|
174
|
+
return original(input, init);
|
|
175
|
+
};
|
|
176
|
+
wrapped.__apiblaze = true;
|
|
177
|
+
globalThis.fetch = wrapped;
|
|
178
|
+
installed = true;
|
|
179
|
+
if (!opts?.quiet) console.log(`[apiblaze/sidecar] active \u2014 external fetches route through ${cfg.tenant}'s APIblaze plane.`);
|
|
180
|
+
}
|
|
181
|
+
function withApiblaze(nextConfig = {}) {
|
|
182
|
+
register();
|
|
183
|
+
const experimental = { ...nextConfig.experimental };
|
|
184
|
+
if (experimental.instrumentationHook === void 0) experimental.instrumentationHook = true;
|
|
185
|
+
return { ...nextConfig, experimental };
|
|
186
|
+
}
|
|
187
|
+
var sidecar_default = { register, withApiblaze };
|
|
188
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
189
|
+
0 && (module.exports = {
|
|
190
|
+
register,
|
|
191
|
+
withApiblaze
|
|
192
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// src/sidecar/index.ts
|
|
2
|
+
var ABZ_TARGET_HEADER = "x-abz-target";
|
|
3
|
+
var MARK_SUFFIX = "-abz-target";
|
|
4
|
+
var DATA_PLANE = "abz.run";
|
|
5
|
+
var NOISE_DENYLIST = [
|
|
6
|
+
"google-analytics.com",
|
|
7
|
+
"googletagmanager.com",
|
|
8
|
+
"segment.io",
|
|
9
|
+
"segment.com",
|
|
10
|
+
"sentry.io",
|
|
11
|
+
"ingest.sentry.io",
|
|
12
|
+
"posthog.com",
|
|
13
|
+
"mixpanel.com",
|
|
14
|
+
"amplitude.com",
|
|
15
|
+
"datadoghq.com",
|
|
16
|
+
"newrelic.com",
|
|
17
|
+
"nr-data.net",
|
|
18
|
+
"honeycomb.io",
|
|
19
|
+
"launchdarkly.com",
|
|
20
|
+
"statsigapi.net",
|
|
21
|
+
"plausible.io",
|
|
22
|
+
"hotjar.com",
|
|
23
|
+
"fullstory.com",
|
|
24
|
+
"intercom.io",
|
|
25
|
+
"avatars.githubusercontent.com",
|
|
26
|
+
"gravatar.com",
|
|
27
|
+
"googleusercontent.com",
|
|
28
|
+
"cloudfront.net",
|
|
29
|
+
"akamaihd.net",
|
|
30
|
+
"fastly.net",
|
|
31
|
+
"imgix.net",
|
|
32
|
+
"cloudinary.com",
|
|
33
|
+
"unsplash.com",
|
|
34
|
+
"twimg.com",
|
|
35
|
+
"fbcdn.net",
|
|
36
|
+
"registry.npmjs.org",
|
|
37
|
+
"pypi.org",
|
|
38
|
+
"github.io"
|
|
39
|
+
];
|
|
40
|
+
var installed = false;
|
|
41
|
+
function canonicalizeOrigin(input) {
|
|
42
|
+
try {
|
|
43
|
+
const u = new URL(input);
|
|
44
|
+
if (u.protocol !== "https:") return null;
|
|
45
|
+
const port = u.port && u.port !== "443" ? `:${u.port}` : "";
|
|
46
|
+
return `https://${u.hostname.toLowerCase()}${port}`;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function sha256Hex(s) {
|
|
52
|
+
const data = new TextEncoder().encode(s);
|
|
53
|
+
const buf = await globalThis.crypto.subtle.digest("SHA-256", data);
|
|
54
|
+
return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
55
|
+
}
|
|
56
|
+
async function sidecarHost(tenant, canonicalOrigin) {
|
|
57
|
+
const h = await sha256Hex(`${tenant}|${canonicalOrigin}`);
|
|
58
|
+
return `sc${h.slice(0, 16)}.${DATA_PLANE}`;
|
|
59
|
+
}
|
|
60
|
+
function hostMatchesGlob(host, glob) {
|
|
61
|
+
const g = glob.toLowerCase();
|
|
62
|
+
if (g.startsWith("*.")) {
|
|
63
|
+
const base = g.slice(2);
|
|
64
|
+
return host === base || host.endsWith(`.${base}`);
|
|
65
|
+
}
|
|
66
|
+
return host === g;
|
|
67
|
+
}
|
|
68
|
+
function isNoise(host) {
|
|
69
|
+
return NOISE_DENYLIST.some((d) => host === d || host.endsWith(`.${d}`));
|
|
70
|
+
}
|
|
71
|
+
function isPrivateOrLocal(host) {
|
|
72
|
+
if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host.endsWith(".internal")) return true;
|
|
73
|
+
if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host)) return true;
|
|
74
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return true;
|
|
75
|
+
if (/^169\.254\./.test(host)) return true;
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
function shouldIntercept(cfg, url) {
|
|
79
|
+
if (url.protocol !== "https:") return false;
|
|
80
|
+
const host = url.hostname.toLowerCase();
|
|
81
|
+
if (host.endsWith(DATA_PLANE) || host.endsWith("tryabz.run") || host.endsWith("apiblaze.com")) return false;
|
|
82
|
+
if (isPrivateOrLocal(host)) return false;
|
|
83
|
+
if (isNoise(host)) return false;
|
|
84
|
+
if (cfg.exclude.some((g) => hostMatchesGlob(host, g))) return false;
|
|
85
|
+
if (cfg.include.length > 0 && !cfg.include.some((g) => hostMatchesGlob(host, g))) return false;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
async function rewrite(cfg, input) {
|
|
89
|
+
const url = new URL(input.url);
|
|
90
|
+
if (!shouldIntercept(cfg, url)) return null;
|
|
91
|
+
const origin = canonicalizeOrigin(url.origin);
|
|
92
|
+
if (!origin) return null;
|
|
93
|
+
const host = await sidecarHost(cfg.tenant, origin);
|
|
94
|
+
const newUrl = new URL(input.url);
|
|
95
|
+
newUrl.protocol = "https:";
|
|
96
|
+
newUrl.host = host;
|
|
97
|
+
newUrl.pathname = `/1.0.0/prod${url.pathname === "/" ? "" : url.pathname}`;
|
|
98
|
+
const headers = new Headers(input.headers);
|
|
99
|
+
const marked = new Headers();
|
|
100
|
+
headers.forEach((value, name) => {
|
|
101
|
+
const n = name.toLowerCase();
|
|
102
|
+
if (n === "authorization") {
|
|
103
|
+
marked.set(`authorization${MARK_SUFFIX}`, value);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (n.startsWith("x-") && !n.startsWith("x-abz-")) {
|
|
107
|
+
marked.set(`${n}${MARK_SUFFIX}`, value);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
marked.set(n, value);
|
|
111
|
+
});
|
|
112
|
+
marked.set(ABZ_TARGET_HEADER, origin);
|
|
113
|
+
marked.set("x-api-key", cfg.token);
|
|
114
|
+
return new Request(newUrl.toString(), {
|
|
115
|
+
method: input.method,
|
|
116
|
+
headers: marked,
|
|
117
|
+
body: input.body,
|
|
118
|
+
redirect: "manual",
|
|
119
|
+
// @ts-expect-error duplex needed for streaming bodies on some runtimes
|
|
120
|
+
duplex: input.body ? "half" : void 0
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function resolveConfig(opts) {
|
|
124
|
+
const token = opts?.token ?? process.env.APIBLAZE_TOKEN;
|
|
125
|
+
const tenant = opts?.tenant ?? process.env.APIBLAZE_TENANT;
|
|
126
|
+
if (!token || !tenant) {
|
|
127
|
+
if (!opts?.quiet) {
|
|
128
|
+
console.warn("[apiblaze/sidecar] APIBLAZE_TOKEN and APIBLAZE_TENANT are required \u2014 interceptor is OFF (fetches pass through unchanged).");
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return { token, tenant, exclude: opts?.exclude ?? [], include: opts?.include ?? [] };
|
|
133
|
+
}
|
|
134
|
+
function register(opts) {
|
|
135
|
+
if (installed) return;
|
|
136
|
+
const cfg = resolveConfig(opts);
|
|
137
|
+
if (!cfg) return;
|
|
138
|
+
const original = globalThis.fetch;
|
|
139
|
+
if (typeof original !== "function") return;
|
|
140
|
+
const wrapped = async (input, init) => {
|
|
141
|
+
try {
|
|
142
|
+
const req = new Request(input, init);
|
|
143
|
+
const rewritten = await rewrite(cfg, req);
|
|
144
|
+
if (rewritten) return original(rewritten);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (!opts?.quiet) console.warn("[apiblaze/sidecar] passthrough after interceptor error:", err?.message);
|
|
147
|
+
}
|
|
148
|
+
return original(input, init);
|
|
149
|
+
};
|
|
150
|
+
wrapped.__apiblaze = true;
|
|
151
|
+
globalThis.fetch = wrapped;
|
|
152
|
+
installed = true;
|
|
153
|
+
if (!opts?.quiet) console.log(`[apiblaze/sidecar] active \u2014 external fetches route through ${cfg.tenant}'s APIblaze plane.`);
|
|
154
|
+
}
|
|
155
|
+
function withApiblaze(nextConfig = {}) {
|
|
156
|
+
register();
|
|
157
|
+
const experimental = { ...nextConfig.experimental };
|
|
158
|
+
if (experimental.instrumentationHook === void 0) experimental.instrumentationHook = true;
|
|
159
|
+
return { ...nextConfig, experimental };
|
|
160
|
+
}
|
|
161
|
+
var sidecar_default = { register, withApiblaze };
|
|
162
|
+
export {
|
|
163
|
+
sidecar_default as default,
|
|
164
|
+
register,
|
|
165
|
+
withApiblaze
|
|
166
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apiblaze",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.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"
|