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,184 @@
import { OpenAPIHono } from "@hono/zod-openapi";
import {
createOpenAPIRegistry,
definePluginRoutes,
} from "@suaveplan/api-openapi";
import type { Principal } from "@suaveplan/api-plugin-auth";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { VAULT_PLUGIN_CODES } from "../../src/error-codes/error-codes.js";
import { defaultUnlockExtractor } from "../../src/plugin/unlock.js";
import { buildVaultRoutes } from "../../src/routes/build-routes.js";
import { createVaultService } from "../../src/services/vault-service.js";
import type { VaultPluginConfig } from "../../src/types/types.js";
import { createMemoryDrive } from "../setup/memory-drive.js";
const noopLogger = {
debug: () => undefined,
info: () => undefined,
warn: () => undefined,
error: () => undefined,
};
function buildApp(principal: Principal) {
const drive = createMemoryDrive();
const service = createVaultService({
storage: drive,
serverPepper: "test-pepper",
logger: noopLogger,
});
const config: VaultPluginConfig = {
storage: drive,
routePrefix: "/vault",
maxItemSizeBytes: 10 * 1024 * 1024,
applySchemaOnInit: false,
unlockExtractor: defaultUnlockExtractor,
serverPepper: "test-pepper",
};
const app = new OpenAPIHono();
app.use("*", async (c, next) => {
c.set("user", principal);
await next();
});
const registry = createOpenAPIRegistry({
info: { title: "t", version: "0.1" },
});
definePluginRoutes(buildVaultRoutes(service, config), {
app,
registry,
pluginName: "vault",
});
app.onError((err, c) => {
const status =
(err as { context?: { statusCode?: number } }).context
?.statusCode ?? 500;
return c.json(
{
success: false,
error: {
code: (err as { code?: string }).code ?? "X",
message: (err as Error).message,
timestamp: new Date().toISOString(),
},
},
status as 400 | 401 | 403 | 404 | 500
);
});
return { app, service, drive };
}
describe("cross-user-isolation", () => {
it("user A cannot see / modify user B's items", {
timeout: 120_000,
}, async () => {
const alice: Principal = {
id: "user:alice",
emailVerified: true,
roles: [],
};
const bob: Principal = {
id: "user:bob",
emailVerified: true,
roles: [],
};
// Alice's setup shares the drive with Bob's app — but each
// principal opens its own vault container.
const drive = createMemoryDrive();
function makeApp(principal: Principal) {
const service = createVaultService({
storage: drive,
serverPepper: "test-pepper",
logger: noopLogger,
});
const config: VaultPluginConfig = {
storage: drive,
routePrefix: "/vault",
maxItemSizeBytes: 10 * 1024 * 1024,
applySchemaOnInit: false,
unlockExtractor: defaultUnlockExtractor,
serverPepper: "test-pepper",
};
const app = new OpenAPIHono();
app.use("*", async (c, next) => {
c.set("user", principal);
await next();
});
const registry = createOpenAPIRegistry({
info: { title: "t", version: "0.1" },
});
definePluginRoutes(buildVaultRoutes(service, config), {
app,
registry,
pluginName: "vault",
});
app.onError((err, c) => {
const status =
(err as { context?: { statusCode?: number } }).context
?.statusCode ?? 500;
return c.json(
{
success: false,
error: {
code: (err as { code?: string }).code ?? "X",
message: (err as Error).message,
timestamp: new Date().toISOString(),
},
},
status as 400 | 401 | 403 | 404 | 500
);
});
return { app };
}
// Bob creates an item.
const bobApp = makeApp(bob).app;
const bobCreate = await bobApp.request("/vault/items", {
method: "POST",
headers: {
"X-Vault-Unlock": "BobPwd-2026",
"Content-Type": "application/json",
},
body: JSON.stringify({
title: "bob-secret",
kind: "credential",
body: "bob-content",
}),
});
expect(bobCreate.status).toBe(201);
const bobItem = (await bobCreate.json()) as {
data: { id: string };
};
// Alice tries to read Bob's item. Even though Alice supplies
// a valid X-Vault-Unlock header, her vault container is
// distinct from Bob's, so the lookup yields ITEM_NOT_FOUND.
const aliceApp = makeApp(alice).app;
const aliceRead = await aliceApp.request(
`/vault/items/${bobItem.data.id}`,
{
headers: { "X-Vault-Unlock": "AlicePwd-2026" },
}
);
expect(aliceRead.status).toBe(404);
const json = (await aliceRead.json()) as { error: { code: string } };
expect(json.error.code).toBe(VAULT_PLUGIN_CODES.ITEM_NOT_FOUND);
// Alice's own listing returns zero items.
const aliceList = await aliceApp.request("/vault/items", {
headers: { "X-Vault-Unlock": "AlicePwd-2026" },
});
const listJson = (await aliceList.json()) as {
meta: { total: number };
};
expect(listJson.meta.total).toBe(0);
});
it("makeApp helper is exported (sanity)", { timeout: 5000 }, () => {
const result = buildApp({
id: "x",
emailVerified: true,
roles: [],
});
expect(result.app).toBeDefined();
expect(result.service).toBeDefined();
});
});

View File

@@ -0,0 +1,216 @@
import { OpenAPIHono } from "@hono/zod-openapi";
import { createOpenAPIRegistry } from "@suaveplan/api-openapi";
import { createPluginContext } from "@suaveplan/api-plugin";
import type { AuthService } from "@suaveplan/api-plugin-auth";
import { createAuthPlugin } from "@suaveplan/api-plugin-auth";
import { betterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb";
import { EventBus } from "@suaveplan/events";
import { SurrealAdapter } from "@suaveplan/surrealdb";
import {
afterAll,
beforeAll,
describe,
expect,
it,
} from "@suaveplan/testing/runner";
import { SurrealDBFixture } from "@suaveplan/testing-integration-surrealdb";
import { createVaultPlugin } from "../../src/plugin/plugin.js";
import { createMemoryDrive } from "../setup/memory-drive.js";
const noopLogger = {
debug: () => undefined,
info: () => undefined,
warn: () => undefined,
error: () => undefined,
};
let fixture: SurrealDBFixture | null = null;
let client: SurrealAdapter | null = null;
beforeAll(async () => {
fixture = new SurrealDBFixture();
const cfg = await fixture.start();
client = new SurrealAdapter({
url: cfg.url,
namespace: cfg.namespace,
database: cfg.database,
username: cfg.username,
password: cfg.password,
});
await client.connect();
await client.execute("DEFINE TABLE OVERWRITE user SCHEMAFULL;");
await client.execute(
"DEFINE FIELD OVERWRITE email ON user TYPE option<string>;"
);
await client.execute(
"DEFINE FIELD OVERWRITE phoneNumber ON user TYPE option<string>;"
);
await client.execute(
"DEFINE FIELD OVERWRITE createdAt ON user TYPE option<datetime> DEFAULT time::now() VALUE $before OR time::now();"
);
await client.execute(
"DEFINE FIELD OVERWRITE updatedAt ON user TYPE option<datetime> DEFAULT time::now() VALUE time::now();"
);
}, 180_000);
afterAll(async () => {
if (client) {
await client.disconnect();
client = null;
}
if (fixture) {
await fixture.stop();
fixture = null;
}
}, 60_000);
describe("full integration boot — auth + vault against real SurrealDB", () => {
it("sign-up → POST → GET → PATCH → DELETE /vault/items roundtrip against real SurrealDB", {
timeout: 180_000,
}, async () => {
if (client === null) throw new Error("client not ready");
const app = new OpenAPIHono();
const registry = createOpenAPIRegistry({
info: { title: "vault-integration", version: "0.1.0" },
});
const services = new Map<string, unknown>();
services.set("app", app);
services.set("openAPIRegistry", registry);
services.set("db", client);
// 1. Auth plugin — applies schema + registers authService +
// installs session middleware on the app.
const authPlugin = createAuthPlugin({
betterAuth: {
database: betterAuthSurrealAdapter({ client }),
secret: "test-secret-at-least-32-chars-long-for-better-auth",
baseURL: "http://localhost:3000/auth",
emailAndPassword: { enabled: true },
},
});
await authPlugin.onInit?.(
createPluginContext({
logger: noopLogger,
eventBus: new EventBus(),
signal: new AbortController().signal,
config: authPlugin.defaults as never,
services,
})
);
// 2. Vault plugin — applies vault schema + registers vaultService
// + mounts 5 routes.
const drive = createMemoryDrive();
const vaultPlugin = createVaultPlugin({
storage: drive,
applySchemaOnInit: true,
});
await vaultPlugin.onInit?.(
createPluginContext({
logger: noopLogger,
eventBus: new EventBus(),
signal: new AbortController().signal,
config: vaultPlugin.defaults as never,
services,
})
);
// Error-handler so VaultPluginError → JSON envelope w/ statusCode.
app.onError((err, c) => {
const status =
(err as { context?: { statusCode?: number } }).context
?.statusCode ?? 500;
return c.json(
{
success: false,
error: {
code: (err as { code?: string }).code ?? "X",
message: (err as Error).message,
timestamp: new Date().toISOString(),
},
},
status as 400 | 401 | 403 | 404 | 413 | 500
);
});
// 3. Sign up via Better-Auth.
const authService = services.get("authService") as AuthService;
expect(authService).toBeDefined();
const auth = authService.getBetterAuthInstance();
const signUp = (await auth.api.signUpEmail({
body: {
email: "vault-integration@example.test",
password: "Passw0rd!2026",
name: "Vault Integration",
},
asResponse: true,
})) as Response;
expect(signUp.status).toBeLessThan(400);
const cookie = signUp.headers.get("set-cookie") ?? "";
// 4. POST /vault/items
const createRes = await app.request("/vault/items", {
method: "POST",
headers: {
"X-Vault-Unlock": "MyVaultPwd-2026",
"Content-Type": "application/json",
Cookie: cookie.split(";")[0] ?? "",
},
body: JSON.stringify({
title: "API key",
kind: "credential",
body: "secret-body-value",
}),
});
expect(createRes.status).toBe(201);
const created = (await createRes.json()) as {
data: { id: string; title: string; body: string };
};
expect(created.data.body).toBe("secret-body-value");
const itemId = created.data.id;
// 5. GET /vault/items
const listRes = await app.request("/vault/items", {
headers: {
"X-Vault-Unlock": "MyVaultPwd-2026",
Cookie: cookie.split(";")[0] ?? "",
},
});
expect(listRes.status).toBe(200);
const list = (await listRes.json()) as {
meta: { total: number };
};
expect(list.meta.total).toBe(1);
// 6. PATCH /vault/items/:id
const patchRes = await app.request(`/vault/items/${itemId}`, {
method: "PATCH",
headers: {
"X-Vault-Unlock": "MyVaultPwd-2026",
"Content-Type": "application/json",
Cookie: cookie.split(";")[0] ?? "",
},
body: JSON.stringify({ title: "Renamed" }),
});
expect(patchRes.status).toBe(200);
// 7. DELETE /vault/items/:id
const delRes = await app.request(`/vault/items/${itemId}`, {
method: "DELETE",
headers: {
"X-Vault-Unlock": "MyVaultPwd-2026",
Cookie: cookie.split(";")[0] ?? "",
},
});
expect(delRes.status).toBe(200);
// 8. Final list should be empty.
const finalRes = await app.request("/vault/items", {
headers: {
"X-Vault-Unlock": "MyVaultPwd-2026",
Cookie: cookie.split(";")[0] ?? "",
},
});
const final = (await finalRes.json()) as { meta: { total: number } };
expect(final.meta.total).toBe(0);
});
});

View File

@@ -0,0 +1,90 @@
import { OpenAPIHono } from "@hono/zod-openapi";
import { createOpenAPIRegistry } from "@suaveplan/api-openapi";
import { createPluginContext } from "@suaveplan/api-plugin";
import { EventBus } from "@suaveplan/events";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { createVaultPlugin } from "../../src/plugin/plugin.js";
import { createMemoryDrive } from "../setup/memory-drive.js";
const noopLogger = {
debug: () => undefined,
info: () => undefined,
warn: () => undefined,
error: () => undefined,
};
describe("openapi-doc", () => {
it("served doc lists 5 /vault/items routes", {
timeout: 30_000,
}, async () => {
const app = new OpenAPIHono();
const registry = createOpenAPIRegistry({
info: { title: "vault-test", version: "0.1.0" },
});
const services = new Map<string, unknown>();
services.set("app", app);
services.set("openAPIRegistry", registry);
const plugin = createVaultPlugin({
storage: createMemoryDrive(),
applySchemaOnInit: false,
});
await plugin.onInit?.(
createPluginContext({
logger: noopLogger,
eventBus: new EventBus(),
signal: new AbortController().signal,
config: plugin.defaults as never,
services,
})
);
const routes = registry.getRoutesByPlugin("vault");
expect(routes.length).toBe(5);
registry.applyTo(app, "/openapi.json");
const res = await app.request("/openapi.json");
const doc = (await res.json()) as {
paths: Record<string, Record<string, unknown>>;
};
const vaultPaths = Object.keys(doc.paths).filter((p) =>
p.startsWith("/vault/")
);
expect(vaultPaths.length).toBeGreaterThanOrEqual(2);
expect(doc.paths["/vault/items"]).toBeDefined();
expect(doc.paths["/vault/items/{id}"]).toBeDefined();
});
it("served /openapi.json includes 5 /vault/items routes", {
timeout: 30_000,
}, async () => {
const app = new OpenAPIHono();
const registry = createOpenAPIRegistry({
info: { title: "vault-test", version: "0.1.0" },
});
const services = new Map<string, unknown>();
services.set("app", app);
services.set("openAPIRegistry", registry);
const plugin = createVaultPlugin({
storage: createMemoryDrive(),
applySchemaOnInit: false,
});
await plugin.onInit?.(
createPluginContext({
logger: noopLogger,
eventBus: new EventBus(),
signal: new AbortController().signal,
config: plugin.defaults as never,
services,
})
);
const methods = registry
.getRoutesByPlugin("vault")
.map((r) => `${r.method.toUpperCase()} ${r.path}`);
expect(methods).toEqual([
"GET /vault/items",
"POST /vault/items",
"GET /vault/items/{id}",
"PATCH /vault/items/{id}",
"DELETE /vault/items/{id}",
]);
});
});