Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions src/lib/server/services/vault-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,21 @@ function slugify(name: string): string {
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}

function normalizeDomain(domain: string): string {
const trimmed = domain.trim();
if (!trimmed) return "";

try {
const url = new URL(trimmed.includes("://") ? trimmed : `https://${trimmed}`);
return url.host;
} catch {
return trimmed.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
}
}

function deriveAllowedOrigins(domain: string): string[] {
return [`https://${domain}`, `https://*.${domain}`];
const normalizedDomain = normalizeDomain(domain);
return [`https://${normalizedDomain}`, `https://*.${normalizedDomain}`];
}

type FieldInput = { name: string; value: string; sensitive?: boolean };
Expand All @@ -19,12 +32,13 @@ export async function createItem(
data: { name: string; domain?: string; description?: string; allowedOrigins?: string[]; fields?: FieldInput[] },
) {
const slug = slugify(data.name);
const allowedOrigins = data.allowedOrigins ?? (data.domain ? deriveAllowedOrigins(data.domain) : null);
const domain = data.domain ? normalizeDomain(data.domain) : null;
const allowedOrigins = data.allowedOrigins ?? (domain ? deriveAllowedOrigins(domain) : null);

let item;
try {
[item] = await db.insert(vaultItems).values({
vaultId, name: data.name, slug, domain: data.domain ?? null,
vaultId, name: data.name, slug, domain,
description: data.description ?? null, allowedOrigins,
}).returning();
} catch (err: unknown) {
Expand Down Expand Up @@ -74,7 +88,15 @@ export async function updateItem(
id: string,
data: { name?: string; domain?: string | null; description?: string | null; allowedOrigins?: string[] | null },
) {
const [row] = await db.update(vaultItems).set({ ...data, updatedAt: new Date() })
const updates = { ...data };

if ("domain" in updates) {
const domain = updates.domain ? normalizeDomain(updates.domain) : null;
updates.domain = domain;
updates.allowedOrigins ??= domain ? deriveAllowedOrigins(domain) : null;
}

const [row] = await db.update(vaultItems).set({ ...updates, updatedAt: new Date() })
.where(eq(vaultItems.id, id)).returning();
return row ?? null;
}
Expand Down
5 changes: 4 additions & 1 deletion tests/integration/vault-field-value.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { auditLogs } from "$lib/server/db/schema";
import { eq, desc } from "drizzle-orm";

describe("vault field value with origin validation", () => {
beforeEach(async () => { await truncateAll(); });
beforeEach(async () => {
process.env.VAULT_ENCRYPTION_KEY = Buffer.from("a]3Fq!9Lp@2Xw#7Yz&5Bv*8Cn$4Dm%6E").toString("base64");
await truncateAll();
});

it("returns value when origin matches allowedOrigins", async () => {
const { token } = await createTestToken();
Expand Down
47 changes: 47 additions & 0 deletions tests/integration/vaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,53 @@ describe("vault items service", () => {
expect(item.allowedOrigins).toContain("https://*.ing.nl");
});

it("normalizes URL domains before deriving allowedOrigins", async () => {
const item = await createItem(vaultId, {
name: "Webgains",
domain: "https://platform.webgains.io/path",
fields: [],
});

expect(item.domain).toBe("platform.webgains.io");
expect(item.allowedOrigins).toEqual([
"https://platform.webgains.io",
"https://*.platform.webgains.io",
]);
});

it("updates allowedOrigins when domain changes", async () => {
const item = await createItem(vaultId, { name: "Login", domain: "github.com", fields: [] });

const updated = await updateItem(item.id, { domain: "https://platform.webgains.io/" });

expect(updated?.domain).toBe("platform.webgains.io");
expect(updated?.allowedOrigins).toEqual([
"https://platform.webgains.io",
"https://*.platform.webgains.io",
]);
});

it("clears allowedOrigins when domain is cleared", async () => {
const item = await createItem(vaultId, { name: "Login", domain: "github.com", fields: [] });

const updated = await updateItem(item.id, { domain: null });

expect(updated?.domain).toBeNull();
expect(updated?.allowedOrigins).toBeNull();
});

it("preserves explicit allowedOrigins when domain changes", async () => {
const item = await createItem(vaultId, { name: "Login", domain: "github.com", fields: [] });

const updated = await updateItem(item.id, {
domain: "webgains.io",
allowedOrigins: ["https://login.webgains.io"],
});

expect(updated?.domain).toBe("webgains.io");
expect(updated?.allowedOrigins).toEqual(["https://login.webgains.io"]);
});

it("deletes item and cascades to fields", async () => {
const item = await createItem(vaultId, {
name: "To Delete",
Expand Down
Loading