apiblaze 0.5.0 → 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/dist/index.js +124 -86
- package/dist/sidecar/index.d.mts +7 -29
- package/dist/sidecar/index.d.ts +7 -29
- package/dist/sidecar/index.js +75 -88
- package/dist/sidecar/index.mjs +75 -88
- package/package.json +1 -1
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 {
|
|
@@ -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) {
|
|
@@ -3052,44 +3052,32 @@ function detectNextProject(root) {
|
|
|
3052
3052
|
}
|
|
3053
3053
|
const appDir = fs6.existsSync(path3.join(root, "app")) || fs6.existsSync(path3.join(root, "src", "app"));
|
|
3054
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
|
-
};
|
|
3055
|
+
return { found: hasConfig || hasDep || appDir || pagesDir, router: appDir ? "app" : pagesDir ? "pages" : null };
|
|
3059
3056
|
}
|
|
3060
|
-
function upsertEnvLocal(root, token
|
|
3057
|
+
function upsertEnvLocal(root, token) {
|
|
3061
3058
|
const p = path3.join(root, ".env.local");
|
|
3062
3059
|
let existing = "";
|
|
3063
3060
|
try {
|
|
3064
3061
|
existing = fs6.readFileSync(p, "utf8");
|
|
3065
3062
|
} catch {
|
|
3066
3063
|
}
|
|
3067
|
-
const
|
|
3068
|
-
|
|
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}
|
|
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}
|
|
3072
3066
|
`;
|
|
3073
|
-
};
|
|
3074
|
-
set("APIBLAZE_TOKEN", token);
|
|
3075
|
-
set("APIBLAZE_TENANT", tenant2);
|
|
3076
3067
|
fs6.writeFileSync(p, next);
|
|
3077
|
-
return
|
|
3068
|
+
return had ? "rotated" : "created";
|
|
3078
3069
|
}
|
|
3079
3070
|
function ensureGitignored(root) {
|
|
3080
3071
|
const p = path3.join(root, ".gitignore");
|
|
3081
|
-
let
|
|
3072
|
+
let c = "";
|
|
3082
3073
|
try {
|
|
3083
|
-
|
|
3074
|
+
c = fs6.readFileSync(p, "utf8");
|
|
3084
3075
|
} catch {
|
|
3085
3076
|
}
|
|
3086
|
-
if (!/^\.env\.local$/m.test(
|
|
3087
|
-
fs6.writeFileSync(p, (content && !content.endsWith("\n") ? content + "\n" : content) + ".env.local\n");
|
|
3088
|
-
}
|
|
3077
|
+
if (!/^\.env\.local$/m.test(c) && !/^\.env\*/m.test(c)) fs6.writeFileSync(p, (c && !c.endsWith("\n") ? c + "\n" : c) + ".env.local\n");
|
|
3089
3078
|
}
|
|
3090
3079
|
function wireInstrumentation(root) {
|
|
3091
|
-
const
|
|
3092
|
-
const existing = candidates.map((c) => path3.join(root, c)).find((f) => fs6.existsSync(f));
|
|
3080
|
+
const existing = ["instrumentation.ts", "instrumentation.js", path3.join("src", "instrumentation.ts")].map((c) => path3.join(root, c)).find((f) => fs6.existsSync(f));
|
|
3093
3081
|
const body = `import { register as apiblaze } from "apiblaze/sidecar";
|
|
3094
3082
|
|
|
3095
3083
|
export function register() {
|
|
@@ -3103,52 +3091,40 @@ export function register() {
|
|
|
3103
3091
|
const cur = fs6.readFileSync(existing, "utf8");
|
|
3104
3092
|
if (cur.includes("apiblaze/sidecar")) return "present";
|
|
3105
3093
|
if (/export\s+function\s+register\s*\(/.test(cur)) {
|
|
3106
|
-
|
|
3094
|
+
fs6.writeFileSync(existing, `import { register as apiblaze } from "apiblaze/sidecar";
|
|
3107
3095
|
` + cur.replace(/export\s+function\s+register\s*\(\s*\)\s*\{/, (m) => `${m}
|
|
3108
|
-
apiblaze();`);
|
|
3109
|
-
fs6.writeFileSync(existing, patched);
|
|
3096
|
+
apiblaze();`));
|
|
3110
3097
|
return "patched";
|
|
3111
3098
|
}
|
|
3112
3099
|
fs6.writeFileSync(existing, `import { register as apiblaze } from "apiblaze/sidecar";
|
|
3113
3100
|
${cur}
|
|
3114
|
-
//
|
|
3101
|
+
// call apiblaze() inside your register() export.
|
|
3115
3102
|
`);
|
|
3116
3103
|
return "patched";
|
|
3117
3104
|
}
|
|
3118
|
-
var INSPECTOR_PAGE = `// AUTO-GENERATED by \`apiblaze
|
|
3119
|
-
// Proves the
|
|
3120
|
-
//
|
|
3121
|
-
// and echoes what the upstream actually received.
|
|
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.
|
|
3122
3108
|
export const dynamic = "force-dynamic";
|
|
3123
3109
|
|
|
3124
|
-
async function probe() {
|
|
3125
|
-
|
|
3126
|
-
headers: { "x-api-key": "demo-secret-value" },
|
|
3127
|
-
|
|
3128
|
-
});
|
|
3129
|
-
const body = await res.json().catch(() => ({}));
|
|
3130
|
-
return { status: res.status, sidecar: res.headers.get("x-abz-sidecar"), echo: body };
|
|
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) }; }
|
|
3131
3115
|
}
|
|
3132
3116
|
|
|
3133
3117
|
export default async function Page() {
|
|
3134
|
-
if (process.env.NODE_ENV === "production") {
|
|
3135
|
-
|
|
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;
|
|
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");
|
|
3140
3120
|
return (
|
|
3141
3121
|
<main style={{ fontFamily: "ui-monospace, monospace", padding: 24, lineHeight: 1.6 }}>
|
|
3142
3122
|
<h1>APIblaze sidecar inspector</h1>
|
|
3143
|
-
<p>
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
<
|
|
3147
|
-
|
|
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>
|
|
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>
|
|
3152
3128
|
</main>
|
|
3153
3129
|
);
|
|
3154
3130
|
}
|
|
@@ -3174,53 +3150,112 @@ function generateInspector(root, router) {
|
|
|
3174
3150
|
async function runSidecar(opts) {
|
|
3175
3151
|
const root = path3.resolve(opts.dir ?? process.cwd());
|
|
3176
3152
|
if (!loadCredentials()) {
|
|
3177
|
-
console.log(import_chalk28.default.dim("Not logged in \u2014 starting APIblaze login
|
|
3153
|
+
console.log(import_chalk28.default.dim("Not logged in \u2014 starting APIblaze login..."));
|
|
3178
3154
|
await runLogin();
|
|
3179
3155
|
}
|
|
3180
3156
|
const detected = detectNextProject(root);
|
|
3181
3157
|
if (!detected.found) {
|
|
3182
3158
|
console.log(import_chalk28.default.yellow(`No Next.js project detected in ${root}.`));
|
|
3183
|
-
console.log("Create one
|
|
3159
|
+
console.log("Create one (e.g. `npx create-next-app`) and re-run `apiblaze init` inside it.");
|
|
3184
3160
|
return;
|
|
3185
3161
|
}
|
|
3186
3162
|
const { teamId, teamName } = await resolveTeam(opts.team);
|
|
3187
|
-
const spinner = (0, import_ora13.default)("
|
|
3188
|
-
let
|
|
3163
|
+
const spinner = (0, import_ora13.default)("Setting up the sidecar (tenant + non-expiring invoke key)...").start();
|
|
3164
|
+
let token;
|
|
3189
3165
|
try {
|
|
3190
3166
|
const out = await admin({
|
|
3191
3167
|
method: "POST",
|
|
3192
|
-
path: `/teams/${encodeURIComponent(teamId)}/
|
|
3193
|
-
|
|
3194
|
-
summary: `Mint a sidecar data-plane key for team ${teamName ?? teamId}`
|
|
3168
|
+
path: `/teams/${encodeURIComponent(teamId)}/sidecar/setup`,
|
|
3169
|
+
summary: `Set up sidecar for team ${teamName ?? teamId}`
|
|
3195
3170
|
});
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
keyId = out.key_id;
|
|
3199
|
-
spinner.succeed("Sidecar key minted.");
|
|
3171
|
+
token = out.token;
|
|
3172
|
+
spinner.succeed("Sidecar ready.");
|
|
3200
3173
|
} catch (err) {
|
|
3201
|
-
spinner.fail("
|
|
3174
|
+
spinner.fail("Setup failed.");
|
|
3202
3175
|
throw err;
|
|
3203
3176
|
}
|
|
3204
|
-
const envState = upsertEnvLocal(root,
|
|
3177
|
+
const envState = upsertEnvLocal(root, token);
|
|
3205
3178
|
ensureGitignored(root);
|
|
3206
|
-
console.log(` ${import_chalk28.default.green("\u2713")} .env.local ${envState} (APIBLAZE_TOKEN
|
|
3179
|
+
console.log(` ${import_chalk28.default.green("\u2713")} .env.local ${envState} (APIBLAZE_TOKEN) \u2014 gitignored`);
|
|
3207
3180
|
const wireState = wireInstrumentation(root);
|
|
3208
|
-
console.log(` ${import_chalk28.default.green("\u2713")} instrumentation.ts ${wireState}
|
|
3209
|
-
console.log(` ${import_chalk28.default.dim("\u2022")} install the runtime dep: ${import_chalk28.default.cyan("npm install apiblaze")}`);
|
|
3181
|
+
console.log(` ${import_chalk28.default.green("\u2713")} instrumentation.ts ${wireState}`);
|
|
3210
3182
|
let inspectorPath = null;
|
|
3211
3183
|
if (!opts.noInspector) {
|
|
3212
3184
|
inspectorPath = generateInspector(root, detected.router);
|
|
3213
|
-
if (inspectorPath) console.log(` ${import_chalk28.default.green("\u2713")} inspector at ${inspectorPath}
|
|
3185
|
+
if (inspectorPath) console.log(` ${import_chalk28.default.green("\u2713")} inspector at ${inspectorPath}`);
|
|
3214
3186
|
}
|
|
3215
3187
|
console.log("");
|
|
3216
|
-
console.log(import_chalk28.default.bold("Done.
|
|
3188
|
+
console.log(import_chalk28.default.bold("Done. What happens next:"));
|
|
3217
3189
|
console.log(` 1. ${import_chalk28.default.cyan("npm install apiblaze")}`);
|
|
3218
|
-
console.log(` 2. ${import_chalk28.default.cyan("npm run dev")}
|
|
3219
|
-
console.log(` 3.
|
|
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)`);
|
|
3220
3195
|
console.log("");
|
|
3221
|
-
console.log(import_chalk28.default.
|
|
3222
|
-
|
|
3223
|
-
|
|
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
|
+
}
|
|
3224
3259
|
}
|
|
3225
3260
|
|
|
3226
3261
|
// src/index.ts
|
|
@@ -3262,12 +3297,15 @@ var agent = program.command("agent").description("Chat with an assistant that bu
|
|
|
3262
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)));
|
|
3263
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)));
|
|
3264
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)));
|
|
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
|
|
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)));
|
|
3266
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) => {
|
|
3267
3305
|
try {
|
|
3268
3306
|
const resolved = parseInt(port ?? opts.port, 10);
|
|
3269
3307
|
if (Number.isNaN(resolved)) {
|
|
3270
|
-
console.error(
|
|
3308
|
+
console.error(import_chalk30.default.red(`Invalid port: ${port ?? opts.port}`));
|
|
3271
3309
|
process.exit(1);
|
|
3272
3310
|
}
|
|
3273
3311
|
await runDev({ port: resolved, captureFile: opts.captureFile });
|
|
@@ -3340,7 +3378,7 @@ spec.command("get").description("Print the current OpenAPI document").argument("
|
|
|
3340
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)));
|
|
3341
3379
|
var HELP_GROUPS = [
|
|
3342
3380
|
{ title: "Chat", commands: ["agent"] },
|
|
3343
|
-
{ title: "Setup", commands: ["login", "create", "
|
|
3381
|
+
{ title: "Setup", commands: ["login", "create", "init", "origins", "dev", "claim", "team", "whoami", "logout"] },
|
|
3344
3382
|
{ title: "Control plane commands", commands: ["projects", "tenant", "domain", "delete", "target", "throttle", "rename", "spec", "export"] },
|
|
3345
3383
|
{ title: "Data plane commands", commands: [
|
|
3346
3384
|
{ parent: "consumer", sub: "login" },
|
|
@@ -3360,7 +3398,7 @@ function groupedCommandHelp() {
|
|
|
3360
3398
|
const sub = byName.get(e.parent)?.commands.find((s) => s.name() === e.sub);
|
|
3361
3399
|
return sub ? ` ${helpLabel(e).padEnd(width)}${sub.description()}` : "";
|
|
3362
3400
|
}).filter(Boolean).join("\n");
|
|
3363
|
-
return `${
|
|
3401
|
+
return `${import_chalk30.default.bold(g.title)}
|
|
3364
3402
|
${rows}`;
|
|
3365
3403
|
}).join("\n\n");
|
|
3366
3404
|
}
|
|
@@ -3386,13 +3424,13 @@ Examples:
|
|
|
3386
3424
|
`);
|
|
3387
3425
|
function printError(err) {
|
|
3388
3426
|
if (err instanceof ApiError) {
|
|
3389
|
-
console.error(
|
|
3427
|
+
console.error(import_chalk30.default.red(`
|
|
3390
3428
|
API error (${err.status}): ${err.message}`));
|
|
3391
3429
|
} else if (err instanceof Error) {
|
|
3392
|
-
console.error(
|
|
3430
|
+
console.error(import_chalk30.default.red(`
|
|
3393
3431
|
Error: ${err.message}`));
|
|
3394
3432
|
} else {
|
|
3395
|
-
console.error(
|
|
3433
|
+
console.error(import_chalk30.default.red("\nUnknown error"));
|
|
3396
3434
|
}
|
|
3397
3435
|
}
|
|
3398
3436
|
program.parse(process.argv);
|
package/dist/sidecar/index.d.mts
CHANGED
|
@@ -1,46 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* apiblaze/sidecar — the egress interceptor.
|
|
3
|
-
* Spec: specs/sidecar/apiblaze-sidecar-
|
|
4
|
-
*
|
|
5
|
-
* Import in a Next.js backend to route outbound fetch() calls through APIblaze:
|
|
2
|
+
* apiblaze/sidecar — the egress interceptor (observe → approve model).
|
|
3
|
+
* Spec: specs/sidecar/apiblaze-sidecar-spec.md §3.
|
|
6
4
|
*
|
|
7
5
|
* // instrumentation.ts
|
|
8
6
|
* import { register as apiblaze } from "apiblaze/sidecar";
|
|
9
7
|
* export function register() { apiblaze(); }
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
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.
|
|
19
14
|
*/
|
|
20
15
|
interface SidecarOptions {
|
|
21
16
|
token?: string;
|
|
22
|
-
tenant?: string;
|
|
23
|
-
/** Extra origin globs to never intercept (in addition to the noise denylist). */
|
|
24
17
|
exclude?: string[];
|
|
25
|
-
/** Only intercept these origin globs (when set, everything else passes direct). */
|
|
26
18
|
include?: string[];
|
|
27
|
-
/** Suppress the boot log line. */
|
|
28
19
|
quiet?: boolean;
|
|
29
20
|
}
|
|
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
21
|
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
22
|
declare function withApiblaze<T extends Record<string, unknown>>(nextConfig?: T): T;
|
|
45
23
|
declare const _default: {
|
|
46
24
|
register: typeof register;
|
package/dist/sidecar/index.d.ts
CHANGED
|
@@ -1,46 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* apiblaze/sidecar — the egress interceptor.
|
|
3
|
-
* Spec: specs/sidecar/apiblaze-sidecar-
|
|
4
|
-
*
|
|
5
|
-
* Import in a Next.js backend to route outbound fetch() calls through APIblaze:
|
|
2
|
+
* apiblaze/sidecar — the egress interceptor (observe → approve model).
|
|
3
|
+
* Spec: specs/sidecar/apiblaze-sidecar-spec.md §3.
|
|
6
4
|
*
|
|
7
5
|
* // instrumentation.ts
|
|
8
6
|
* import { register as apiblaze } from "apiblaze/sidecar";
|
|
9
7
|
* export function register() { apiblaze(); }
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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.
|
|
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.
|
|
19
14
|
*/
|
|
20
15
|
interface SidecarOptions {
|
|
21
16
|
token?: string;
|
|
22
|
-
tenant?: string;
|
|
23
|
-
/** Extra origin globs to never intercept (in addition to the noise denylist). */
|
|
24
17
|
exclude?: string[];
|
|
25
|
-
/** Only intercept these origin globs (when set, everything else passes direct). */
|
|
26
18
|
include?: string[];
|
|
27
|
-
/** Suppress the boot log line. */
|
|
28
19
|
quiet?: boolean;
|
|
29
20
|
}
|
|
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
21
|
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
22
|
declare function withApiblaze<T extends Record<string, unknown>>(nextConfig?: T): T;
|
|
45
23
|
declare const _default: {
|
|
46
24
|
register: typeof register;
|
package/dist/sidecar/index.js
CHANGED
|
@@ -25,14 +25,12 @@ __export(sidecar_exports, {
|
|
|
25
25
|
withApiblaze: () => withApiblaze
|
|
26
26
|
});
|
|
27
27
|
module.exports = __toCommonJS(sidecar_exports);
|
|
28
|
-
var
|
|
29
|
-
var
|
|
30
|
-
var DATA_PLANE = "abz.run";
|
|
28
|
+
var SIDECAR_HOST = "https://sidecar.abz.run";
|
|
29
|
+
var REFRESH_MS = 5 * 60 * 1e3;
|
|
31
30
|
var NOISE_DENYLIST = [
|
|
32
31
|
"google-analytics.com",
|
|
33
32
|
"googletagmanager.com",
|
|
34
33
|
"segment.io",
|
|
35
|
-
"segment.com",
|
|
36
34
|
"sentry.io",
|
|
37
35
|
"ingest.sentry.io",
|
|
38
36
|
"posthog.com",
|
|
@@ -41,13 +39,9 @@ var NOISE_DENYLIST = [
|
|
|
41
39
|
"datadoghq.com",
|
|
42
40
|
"newrelic.com",
|
|
43
41
|
"nr-data.net",
|
|
44
|
-
"honeycomb.io",
|
|
45
42
|
"launchdarkly.com",
|
|
46
43
|
"statsigapi.net",
|
|
47
44
|
"plausible.io",
|
|
48
|
-
"hotjar.com",
|
|
49
|
-
"fullstory.com",
|
|
50
|
-
"intercom.io",
|
|
51
45
|
"avatars.githubusercontent.com",
|
|
52
46
|
"gravatar.com",
|
|
53
47
|
"googleusercontent.com",
|
|
@@ -56,15 +50,11 @@ var NOISE_DENYLIST = [
|
|
|
56
50
|
"fastly.net",
|
|
57
51
|
"imgix.net",
|
|
58
52
|
"cloudinary.com",
|
|
59
|
-
"unsplash.com",
|
|
60
|
-
"twimg.com",
|
|
61
|
-
"fbcdn.net",
|
|
62
53
|
"registry.npmjs.org",
|
|
63
|
-
"pypi.org"
|
|
64
|
-
"github.io"
|
|
54
|
+
"pypi.org"
|
|
65
55
|
];
|
|
66
56
|
var installed = false;
|
|
67
|
-
function
|
|
57
|
+
function canonicalOrigin(input) {
|
|
68
58
|
try {
|
|
69
59
|
const u = new URL(input);
|
|
70
60
|
if (u.protocol !== "https:") return null;
|
|
@@ -74,20 +64,11 @@ function canonicalizeOrigin(input) {
|
|
|
74
64
|
return null;
|
|
75
65
|
}
|
|
76
66
|
}
|
|
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
67
|
function hostMatchesGlob(host, glob) {
|
|
87
68
|
const g = glob.toLowerCase();
|
|
88
69
|
if (g.startsWith("*.")) {
|
|
89
|
-
const
|
|
90
|
-
return host ===
|
|
70
|
+
const b = g.slice(2);
|
|
71
|
+
return host === b || host.endsWith(`.${b}`);
|
|
91
72
|
}
|
|
92
73
|
return host === g;
|
|
93
74
|
}
|
|
@@ -96,78 +77,84 @@ function isNoise(host) {
|
|
|
96
77
|
}
|
|
97
78
|
function isPrivateOrLocal(host) {
|
|
98
79
|
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;
|
|
80
|
+
if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^169\.254\./.test(host)) return true;
|
|
100
81
|
if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return true;
|
|
101
|
-
if (/^169\.254\./.test(host)) return true;
|
|
102
82
|
return false;
|
|
103
83
|
}
|
|
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
84
|
function register(opts) {
|
|
161
85
|
if (installed) return;
|
|
162
|
-
const
|
|
163
|
-
if (!
|
|
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
|
+
}
|
|
164
91
|
const original = globalThis.fetch;
|
|
165
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
|
+
};
|
|
166
124
|
const wrapped = async (input, init) => {
|
|
167
125
|
try {
|
|
168
126
|
const req = new Request(input, init);
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
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
|
+
}
|
|
171
158
|
} catch (err) {
|
|
172
159
|
if (!opts?.quiet) console.warn("[apiblaze/sidecar] passthrough after interceptor error:", err?.message);
|
|
173
160
|
}
|
|
@@ -176,7 +163,7 @@ function register(opts) {
|
|
|
176
163
|
wrapped.__apiblaze = true;
|
|
177
164
|
globalThis.fetch = wrapped;
|
|
178
165
|
installed = true;
|
|
179
|
-
if (!opts?.quiet) console.log(
|
|
166
|
+
if (!opts?.quiet) console.log("[apiblaze/sidecar] active \u2014 approved origins route through APIblaze; the rest go direct.");
|
|
180
167
|
}
|
|
181
168
|
function withApiblaze(nextConfig = {}) {
|
|
182
169
|
register();
|
package/dist/sidecar/index.mjs
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
// src/sidecar/index.ts
|
|
2
|
-
var
|
|
3
|
-
var
|
|
4
|
-
var DATA_PLANE = "abz.run";
|
|
2
|
+
var SIDECAR_HOST = "https://sidecar.abz.run";
|
|
3
|
+
var REFRESH_MS = 5 * 60 * 1e3;
|
|
5
4
|
var NOISE_DENYLIST = [
|
|
6
5
|
"google-analytics.com",
|
|
7
6
|
"googletagmanager.com",
|
|
8
7
|
"segment.io",
|
|
9
|
-
"segment.com",
|
|
10
8
|
"sentry.io",
|
|
11
9
|
"ingest.sentry.io",
|
|
12
10
|
"posthog.com",
|
|
@@ -15,13 +13,9 @@ var NOISE_DENYLIST = [
|
|
|
15
13
|
"datadoghq.com",
|
|
16
14
|
"newrelic.com",
|
|
17
15
|
"nr-data.net",
|
|
18
|
-
"honeycomb.io",
|
|
19
16
|
"launchdarkly.com",
|
|
20
17
|
"statsigapi.net",
|
|
21
18
|
"plausible.io",
|
|
22
|
-
"hotjar.com",
|
|
23
|
-
"fullstory.com",
|
|
24
|
-
"intercom.io",
|
|
25
19
|
"avatars.githubusercontent.com",
|
|
26
20
|
"gravatar.com",
|
|
27
21
|
"googleusercontent.com",
|
|
@@ -30,15 +24,11 @@ var NOISE_DENYLIST = [
|
|
|
30
24
|
"fastly.net",
|
|
31
25
|
"imgix.net",
|
|
32
26
|
"cloudinary.com",
|
|
33
|
-
"unsplash.com",
|
|
34
|
-
"twimg.com",
|
|
35
|
-
"fbcdn.net",
|
|
36
27
|
"registry.npmjs.org",
|
|
37
|
-
"pypi.org"
|
|
38
|
-
"github.io"
|
|
28
|
+
"pypi.org"
|
|
39
29
|
];
|
|
40
30
|
var installed = false;
|
|
41
|
-
function
|
|
31
|
+
function canonicalOrigin(input) {
|
|
42
32
|
try {
|
|
43
33
|
const u = new URL(input);
|
|
44
34
|
if (u.protocol !== "https:") return null;
|
|
@@ -48,20 +38,11 @@ function canonicalizeOrigin(input) {
|
|
|
48
38
|
return null;
|
|
49
39
|
}
|
|
50
40
|
}
|
|
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
41
|
function hostMatchesGlob(host, glob) {
|
|
61
42
|
const g = glob.toLowerCase();
|
|
62
43
|
if (g.startsWith("*.")) {
|
|
63
|
-
const
|
|
64
|
-
return host ===
|
|
44
|
+
const b = g.slice(2);
|
|
45
|
+
return host === b || host.endsWith(`.${b}`);
|
|
65
46
|
}
|
|
66
47
|
return host === g;
|
|
67
48
|
}
|
|
@@ -70,78 +51,84 @@ function isNoise(host) {
|
|
|
70
51
|
}
|
|
71
52
|
function isPrivateOrLocal(host) {
|
|
72
53
|
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;
|
|
54
|
+
if (/^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^169\.254\./.test(host)) return true;
|
|
74
55
|
if (/^172\.(1[6-9]|2\d|3[01])\./.test(host)) return true;
|
|
75
|
-
if (/^169\.254\./.test(host)) return true;
|
|
76
56
|
return false;
|
|
77
57
|
}
|
|
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
58
|
function register(opts) {
|
|
135
59
|
if (installed) return;
|
|
136
|
-
const
|
|
137
|
-
if (!
|
|
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
|
+
}
|
|
138
65
|
const original = globalThis.fetch;
|
|
139
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
|
+
};
|
|
140
98
|
const wrapped = async (input, init) => {
|
|
141
99
|
try {
|
|
142
100
|
const req = new Request(input, init);
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
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
|
+
}
|
|
145
132
|
} catch (err) {
|
|
146
133
|
if (!opts?.quiet) console.warn("[apiblaze/sidecar] passthrough after interceptor error:", err?.message);
|
|
147
134
|
}
|
|
@@ -150,7 +137,7 @@ function register(opts) {
|
|
|
150
137
|
wrapped.__apiblaze = true;
|
|
151
138
|
globalThis.fetch = wrapped;
|
|
152
139
|
installed = true;
|
|
153
|
-
if (!opts?.quiet) console.log(
|
|
140
|
+
if (!opts?.quiet) console.log("[apiblaze/sidecar] active \u2014 approved origins route through APIblaze; the rest go direct.");
|
|
154
141
|
}
|
|
155
142
|
function withApiblaze(nextConfig = {}) {
|
|
156
143
|
register();
|
package/package.json
CHANGED