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(); }); });