185 lines
6.3 KiB
TypeScript
185 lines
6.3 KiB
TypeScript
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();
|
|
});
|
|
});
|