Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
import { readdirSync, readFileSync, statSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { describe, expect, it } from "@suaveplan/testing/runner";
const PKG_DIR = resolve(__dirname, "..");
const pkg = JSON.parse(readFileSync(join(PKG_DIR, "package.json"), "utf8")) as {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
exports?: Record<string, unknown>;
};
const ALLOWED_DEPS = new Set([
"@opentelemetry/api",
"@suaveplan/api-plugin",
"@suaveplan/error",
"@suaveplan/events",
"@suaveplan/types",
"hono",
"tslib",
"zod",
]);
function walk(dir: string, out: string[] = []): string[] {
for (const name of readdirSync(dir)) {
const p = join(dir, name);
const st = statSync(p);
if (st.isDirectory()) {
if (name === "node_modules" || name === "dist") continue;
walk(p, out);
} else {
out.push(p);
}
}
return out;
}
describe("dep-graph", () => {
it("dependencies match the server allowlist", { timeout: 5000 }, () => {
const deps = Object.keys(pkg.dependencies ?? {});
for (const d of deps) {
expect(ALLOWED_DEPS.has(d)).toBe(true);
}
// Every allowed dep is present (no skip-by-omission).
for (const expected of [
"@suaveplan/error",
"@suaveplan/api-plugin",
"@suaveplan/events",
"@suaveplan/types",
"hono",
"tslib",
"zod",
"@opentelemetry/api",
]) {
expect(deps).toContain(expected);
}
});
it("subpath exports cover every feature", { timeout: 5000 }, () => {
const exports = pkg.exports ?? {};
const expected = [
".",
"./error-codes",
"./types/tenant",
"./resolver",
"./cache",
"./context",
"./middleware",
"./rate-limit",
"./lifecycle",
"./storage",
];
for (const e of expected) {
expect(Object.keys(exports)).toContain(e);
}
});
it("every impl .ts has a sibling .md", { timeout: 5000 }, () => {
const srcDir = join(PKG_DIR, "src");
const all = walk(srcDir);
const missing: string[] = [];
for (const file of all) {
if (!file.endsWith(".ts")) continue;
if (file.endsWith(".test.ts")) continue;
if (file.endsWith(".spec.ts")) continue;
if (file.endsWith(".types.ts")) continue;
const base = file.replace(/\.ts$/, "");
const fileName = base.split("/").pop();
if (fileName === "index") continue;
// The root index.ts is covered by the package README.
if (file === join(srcDir, "index.ts")) continue;
const md = `${base}.md`;
try {
statSync(md);
} catch {
missing.push(md.replace(`${PKG_DIR}/`, ""));
}
}
expect(missing).toEqual([]);
});
it("no biome suppressions in src/", { timeout: 5000 }, () => {
const all = walk(join(PKG_DIR, "src"));
const hits: string[] = [];
for (const f of all) {
if (!f.endsWith(".ts")) continue;
if (f.endsWith(".test.ts")) continue;
const txt = readFileSync(f, "utf8");
if (txt.includes("biome-ignore")) hits.push(f);
if (txt.includes("eslint-disable")) hits.push(f);
}
expect(hits).toEqual([]);
});
it("no banned patterns in src/", { timeout: 5000 }, () => {
const banned = [
"TODO",
"FIXME",
"ts-ignore",
"ts-expect-error",
"@deprecated",
"console.log",
];
const all = walk(join(PKG_DIR, "src"));
const hits: { file: string; pattern: string }[] = [];
for (const f of all) {
if (!f.endsWith(".ts")) continue;
if (f.endsWith(".test.ts")) continue;
const txt = readFileSync(f, "utf8");
for (const p of banned) {
if (txt.includes(p)) hits.push({ file: f, pattern: p });
}
}
expect(hits).toEqual([]);
});
it("no `export *` in feature barrels", { timeout: 5000 }, () => {
const featureBarrels = readdirSync(join(PKG_DIR, "src"))
.filter((entry) => {
const p = join(PKG_DIR, "src", entry);
try {
return statSync(p).isDirectory();
} catch {
return false;
}
})
.map((d) => join(PKG_DIR, "src", d, "index.ts"));
const hits: string[] = [];
for (const b of featureBarrels) {
try {
const txt = readFileSync(b, "utf8");
if (/^export\s+\*/m.test(txt)) hits.push(b);
} catch {
/* directory may have no index.ts */
}
}
expect(hits).toEqual([]);
});
});
// Guard against accidental flat-error-codes file showing up.
describe("layout invariants", () => {
it("error-codes lives at src/error-codes/error-codes.ts (directory form)", {
timeout: 5000,
}, () => {
const inner = join(PKG_DIR, "src", "error-codes", "error-codes.ts");
statSync(inner);
const dir = dirname(inner);
// No flat src/error-codes.ts collision.
const flat = join(PKG_DIR, "src", "error-codes.ts");
let flatExists = true;
try {
statSync(flat);
} catch {
flatExists = false;
}
expect(flatExists).toBe(false);
expect(dir.endsWith("/error-codes")).toBe(true);
});
});

View File

@@ -0,0 +1,151 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { Hono } from "hono";
import {
chainResolvers,
createTenantMiddleware,
headerResolver,
MemoryRateLimitStore,
subdomainResolver,
TENANT_CODES,
type Tenant,
TenantCache,
TenantError,
tenantRateLimit,
} from "../../src/index.js";
function makeTenant(id: string, over: Partial<Tenant> = {}): Tenant {
return {
id,
name: id,
slug: id,
status: "active",
settings: {},
enabledPlugins: [],
domains: [],
createdAt: new Date(0),
updatedAt: new Date(0),
...over,
};
}
describe("integration — tenant middleware roundtrip (real Hono + cache + store)", () => {
it("resolves via chain, caches positive lookup, enforces rate limit", {
timeout: 30000,
}, async () => {
const db = new Map<string, Tenant>([
["acme", makeTenant("acme")],
["beta", makeTenant("beta")],
]);
let dbCalls = 0;
const lookup = async (k: string): Promise<Tenant | null> => {
dbCalls += 1;
return db.get(k) ?? null;
};
const resolver = TenantCache.wrap(
chainResolvers(
headerResolver("x-tenant-id", lookup),
subdomainResolver("app.test", lookup)
),
{ ttlMs: 60_000 }
);
const store = new MemoryRateLimitStore(100);
const app = new Hono();
app.use("*", createTenantMiddleware({ resolver }));
app.use(
"*",
tenantRateLimit({
maxRequests: 2,
windowMs: 60_000,
store,
keyPrefix: "rl",
})
);
app.get("/me", (c) => c.json({ id: c.get("tenant").id }));
app.onError((err, c) => {
if (err instanceof TenantError) {
return c.json({ code: err.code }, 404);
}
return c.text("boom", 500);
});
// 1. First hit — cold cache, db touched.
const r1 = await app.request("/me", {
headers: { "x-tenant-id": "acme" },
});
expect(r1.status).toBe(200);
expect(dbCalls).toBe(1);
// 2. Second hit — cache hit, db untouched.
const r2 = await app.request("/me", {
headers: { "x-tenant-id": "acme" },
});
expect(r2.status).toBe(200);
expect(dbCalls).toBe(1);
// 3. Third hit — rate-limit exceeded.
const r3 = await app.request("/me", {
headers: { "x-tenant-id": "acme" },
});
expect(r3.status).toBe(429);
const body = (await r3.json()) as { code: string };
expect(body.code).toBe(TENANT_CODES.RATE_LIMITED);
expect(r3.headers.get("Retry-After")).not.toBeNull();
});
it("subdomain fallback resolves when header missing", {
timeout: 30000,
}, async () => {
const db = new Map<string, Tenant>([["sub", makeTenant("sub")]]);
const resolver = TenantCache.wrap(
chainResolvers(
headerResolver("x-tenant-id", async (k) => db.get(k) ?? null),
subdomainResolver("app.test", async (k) => db.get(k) ?? null)
),
{ ttlMs: 60_000, negativeTtlMs: 1000 }
);
const app = new Hono();
app.use("*", createTenantMiddleware({ resolver }));
app.get("/", (c) => c.text(c.get("tenant").id));
app.onError((err, c) =>
c.json(
{
code: err instanceof TenantError ? err.code : "x",
message: err.message,
},
500
)
);
const res = await app.request("http://sub.app.test/", {
headers: { host: "sub.app.test" },
});
expect(await res.text()).toBe("sub");
});
it("unknown tenant in chain → 404 via onError mapping", {
timeout: 30000,
}, async () => {
const resolver = TenantCache.wrap(
chainResolvers(
headerResolver("x-tenant-id", async () => null),
subdomainResolver("app.test", async () => null)
),
{ ttlMs: 60_000 }
);
const app = new Hono();
app.use("*", createTenantMiddleware({ resolver }));
app.get("/", (c) => c.text(c.get("tenant").id));
app.onError((err, c) => {
if (err instanceof TenantError)
return c.json({ code: err.code }, 404);
return c.text("", 500);
});
const res = await app.request("http://unknown.app.test/", {
headers: { host: "unknown.app.test" },
});
expect(res.status).toBe(404);
const body = (await res.json()) as { code: string };
expect(body.code).toBe(TENANT_CODES.NOT_FOUND);
});
});

View File

@@ -0,0 +1,66 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import fc from "fast-check";
import { Hono } from "hono";
import { headerResolver, type Tenant, TenantCache } from "../src/index.js";
function makeTenant(id: string): Tenant {
return {
id,
name: id,
slug: id,
status: "active",
settings: {},
enabledPlugins: [],
domains: [],
createdAt: new Date(0),
updatedAt: new Date(0),
};
}
describe("property — cache TTL boundary invariant", () => {
it("entry expires strictly after ttlMs boundary (fast-check 200 runs)", {
timeout: 30000,
}, async () => {
await fc.assert(
fc.asyncProperty(
fc.integer({ min: 10, max: 10_000 }),
fc.integer({ min: 0, max: 100_000 }),
async (ttlMs, baseNow) => {
let calls = 0;
const inner = headerResolver("x-tenant-id", async (id) => {
calls += 1;
return makeTenant(id);
});
let now = baseNow;
const cached = TenantCache.wrap(inner, {
ttlMs,
clock: () => now,
});
const app = new Hono();
app.get("/", async (c) => {
await cached.resolve(c);
return c.text("ok");
});
const hit = (): Promise<unknown> =>
app.request("/", {
headers: { "x-tenant-id": "k", host: "x" },
});
await hit(); // populate
expect(calls).toBe(1);
// Strictly inside TTL → cached.
now = baseNow + ttlMs - 1;
await hit();
expect(calls).toBe(1);
// At the boundary (expiresAt > now is false when equal) → re-lookup.
now = baseNow + ttlMs;
await hit();
expect(calls).toBe(2);
}
),
{ numRuns: 200 }
);
});
});

View File

@@ -0,0 +1,39 @@
/**
* Test-environment setup for @suaveplan/api-tenant.
*
* Wires a real in-memory OpenTelemetry `MeterProvider` BEFORE any
* source module is imported, so the meter resolved at module load
* is the real provider's meter — not the noop fallback. Tests that
* inspect emitted metrics can read them out of
* `testInMemoryMetricExporter`.
*
* Node ≥18 already exposes `crypto.subtle` and `crypto.getRandomValues`
* globally, so no crypto polyfill is required. Server-tier: `node:async_hooks`
* is allowed (used by the opt-in `withTenantStorage` builder).
*/
import { metrics } from "@opentelemetry/api";
import {
AggregationTemporality,
InMemoryMetricExporter,
MeterProvider,
PeriodicExportingMetricReader,
} from "@opentelemetry/sdk-metrics";
export const testInMemoryMetricExporter = new InMemoryMetricExporter(
AggregationTemporality.CUMULATIVE
);
const reader = new PeriodicExportingMetricReader({
exporter: testInMemoryMetricExporter,
exportIntervalMillis: 60_000,
});
export const testMeterProvider = new MeterProvider({ readers: [reader] });
metrics.setGlobalMeterProvider(testMeterProvider);
export async function flushTestMetrics() {
await testMeterProvider.forceFlush();
const collected = testInMemoryMetricExporter.getMetrics();
testInMemoryMetricExporter.reset();
return collected;
}