diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 8378a981..6a970e5c 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -5,6 +5,9 @@ on:
branches:
- main
+permissions:
+ contents: read
+
concurrency:
group: deploy-sites
cancel-in-progress: true
diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml
index a6babbe1..37b7c2bb 100644
--- a/.github/workflows/pr-checks.yml
+++ b/.github/workflows/pr-checks.yml
@@ -3,6 +3,9 @@ name: PR Checks
on:
pull_request:
+permissions:
+ contents: read
+
concurrency:
group: pr-checks-${{ github.ref }}
cancel-in-progress: true
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 80fd8be7..89c51b5e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -6,6 +6,9 @@ on:
- main
pull_request:
+permissions:
+ contents: read
+
concurrency:
group: tests-${{ github.ref }}
cancel-in-progress: true
diff --git a/packages/admin-ui/src/pages/Branding.tsx b/packages/admin-ui/src/pages/Branding.tsx
index 42e66721..9aefd437 100644
--- a/packages/admin-ui/src/pages/Branding.tsx
+++ b/packages/admin-ui/src/pages/Branding.tsx
@@ -30,6 +30,8 @@ type BrandingTab = ThemeMode | "language";
type PreviewScreen = "login" | "apps" | "security" | "authorize" | "profile";
type BrandingImage = { data: string | null; mimeType: string | null };
type BrandingIdentity = { title: string; tagline: string };
+const allowedBrandingImageTypes = new Set(["image/png", "image/jpeg", "image/x-icon"]);
+const brandingImageAccept = "image/png,image/jpeg,image/x-icon";
const colorFields = [
{ key: "brandColor", label: "Brand Color" },
@@ -376,7 +378,7 @@ export default function Branding() {
if (event.origin !== window.location.origin) return;
if (event.source !== iframeRef.current?.contentWindow) return;
const data = event.data as { type?: string; theme?: unknown } | null;
- if (!data || data.type !== "da:theme-changed") return;
+ if (data?.type !== "da:theme-changed") return;
if (data.theme === "light" || data.theme === "dark") setActiveMode(data.theme);
};
window.addEventListener("message", onMessage);
@@ -388,6 +390,10 @@ export default function Branding() {
toast({ title: "Image too large (max 2MB)", variant: "destructive" });
return;
}
+ if (!allowedBrandingImageTypes.has(file.type)) {
+ toast({ title: "Invalid image type", variant: "destructive" });
+ return;
+ }
try {
const buf = await file.arrayBuffer();
setter({ data: arrayBufferToBase64(buf), mimeType: file.type });
@@ -512,7 +518,7 @@ export default function Branding() {
{
const f = e.target.files?.[0];
if (f) onPickImage(f, setLogo);
@@ -551,7 +557,7 @@ export default function Branding() {
{
const f = e.target.files?.[0];
if (f) onPickImage(f, setFavicon);
diff --git a/packages/api/src/controllers/admin/settingsUpdate.ts b/packages/api/src/controllers/admin/settingsUpdate.ts
index 69c8935d..8be50bcf 100644
--- a/packages/api/src/controllers/admin/settingsUpdate.ts
+++ b/packages/api/src/controllers/admin/settingsUpdate.ts
@@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
import { z } from "zod/v4";
import { ForbiddenError, ValidationError } from "../../errors.ts";
import { genericErrors } from "../../http/openapi-helpers.ts";
+import { validateImageBase64 } from "../../services/branding.ts";
import { isEmailSendingAvailable } from "../../services/email.ts";
import { requireSession } from "../../services/sessions.ts";
import { getSetting, setSetting } from "../../services/settings.ts";
@@ -86,6 +87,8 @@ async function updateSettingsHandler(
await validatePasswordResetEnable(context, data.value);
} else if (data.key === "email.smtp.enabled") {
await validateSmtpEnable(context, data.value);
+ } else if (isBrandingImageSetting(data.key)) {
+ validateBrandingImageSetting(data.value);
}
// Get the old value for audit logging
@@ -113,6 +116,33 @@ async function updateSettingsHandler(
});
}
+function isBrandingImageSetting(key: string): boolean {
+ return [
+ "branding.logo",
+ "branding.logo_dark",
+ "branding.favicon",
+ "branding.favicon_dark",
+ ].includes(key);
+}
+
+function validateBrandingImageSetting(value: unknown): void {
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
+ throw new ValidationError("Branding image must be an object");
+ }
+ const image = value as { data?: unknown; mimeType?: unknown };
+ const hasData = typeof image.data === "string" && image.data.length > 0;
+ const hasMimeType = typeof image.mimeType === "string" && image.mimeType.length > 0;
+ if (!hasData && !hasMimeType) return;
+ if (!hasData || !hasMimeType) {
+ throw new ValidationError("Branding image data and MIME type are required");
+ }
+ try {
+ validateImageBase64(image.data as string, image.mimeType as string);
+ } catch (error) {
+ throw new ValidationError(error instanceof Error ? error.message : "Invalid image");
+ }
+}
+
function validateSmtpPort(value: unknown): void {
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 65535) {
throw new ValidationError("SMTP port must be between 1 and 65535");
diff --git a/packages/api/src/controllers/user/usersDirectory.ts b/packages/api/src/controllers/user/usersDirectory.ts
index c209b783..4cdfd646 100644
--- a/packages/api/src/controllers/user/usersDirectory.ts
+++ b/packages/api/src/controllers/user/usersDirectory.ts
@@ -11,6 +11,7 @@ import type { Context, ControllerSchema } from "../../types.ts";
import { sendJson } from "../../utils/http.ts";
const usersReadPermissionKey = "darkauth.users:read";
+const maxAuthorizationHeaderLength = 16_384;
export function hasRequiredPermission(permissions: unknown): boolean {
return (
@@ -63,17 +64,50 @@ function hasMatchingAudience(payload: Record, clientId: string)
return false;
}
+function isBearerSeparator(charCode: number): boolean {
+ return charCode === 0x20 || charCode === 0x09;
+}
+
+function isBearerAuthAttempt(auth: string): boolean {
+ if (!auth.startsWith("Bearer")) return false;
+ return auth.length === "Bearer".length || isBearerSeparator(auth.charCodeAt("Bearer".length));
+}
+
+export function parseBearerToken(auth: string): string | null {
+ if (auth.length === 0 || auth.length > maxAuthorizationHeaderLength) return null;
+ if (!auth.startsWith("Bearer")) return null;
+
+ let tokenStart = "Bearer".length;
+ if (tokenStart >= auth.length || !isBearerSeparator(auth.charCodeAt(tokenStart))) return null;
+
+ while (tokenStart < auth.length && isBearerSeparator(auth.charCodeAt(tokenStart))) {
+ tokenStart += 1;
+ }
+
+ if (tokenStart >= auth.length) return null;
+
+ for (let index = tokenStart; index < auth.length; index += 1) {
+ if (isBearerSeparator(auth.charCodeAt(index))) return null;
+ }
+
+ return auth.slice(tokenStart);
+}
+
async function requireUsersReadPermission(
context: Context,
request: IncomingMessage
): Promise {
const auth = request.headers.authorization || "";
- const tokenMatch = /^Bearer\s+(.+)$/.exec(auth);
+ const bearerToken = parseBearerToken(auth);
+
+ if (!bearerToken && isBearerAuthAttempt(auth)) {
+ throw new UnauthorizedError("Invalid bearer token");
+ }
- if (tokenMatch?.[1]) {
+ if (bearerToken) {
const JWKS = createRemoteJWKSet(new URL(`${context.config.issuer}/.well-known/jwks.json`));
try {
- const verified = await jwtVerify(tokenMatch[1], JWKS, { issuer: context.config.issuer });
+ const verified = await jwtVerify(bearerToken, JWKS, { issuer: context.config.issuer });
const payload = verified.payload as Record;
const mode = resolveUsersReadModeFromPayload(payload);
diff --git a/packages/api/src/controllers/usersDirectory.test.ts b/packages/api/src/controllers/usersDirectory.test.ts
index a90d54de..8f5363dc 100644
--- a/packages/api/src/controllers/usersDirectory.test.ts
+++ b/packages/api/src/controllers/usersDirectory.test.ts
@@ -3,6 +3,7 @@ import { test } from "node:test";
import {
hasRequiredPermission,
hasRequiredScope,
+ parseBearerToken,
resolveUsersReadModeFromPayload,
} from "./user/usersDirectory.ts";
@@ -39,3 +40,17 @@ test("resolveUsersReadModeFromPayload returns null when missing required permiss
});
assert.equal(mode, null);
});
+
+test("parseBearerToken extracts a bounded bearer token without regex parsing", () => {
+ assert.equal(parseBearerToken("Bearer access-token"), "access-token");
+ assert.equal(parseBearerToken("Bearer access-token"), "access-token");
+ assert.equal(parseBearerToken("Bearer\taccess-token"), "access-token");
+});
+
+test("parseBearerToken rejects missing, padded, and oversized tokens", () => {
+ assert.equal(parseBearerToken(""), null);
+ assert.equal(parseBearerToken("Bearer"), null);
+ assert.equal(parseBearerToken("Bearer "), null);
+ assert.equal(parseBearerToken("Bearer access-token "), null);
+ assert.equal(parseBearerToken(`Bearer ${"a".repeat(16_379)}`), null);
+});
diff --git a/packages/api/src/models/federation.test.ts b/packages/api/src/models/federation.test.ts
index f4ef588c..e3d53f44 100644
--- a/packages/api/src/models/federation.test.ts
+++ b/packages/api/src/models/federation.test.ts
@@ -62,7 +62,7 @@ async function createContext() {
return { context, cleanup };
}
-const publicLookup = async () => [{ address: "203.0.113.10" }];
+const publicLookup = async () => [{ address: "93.184.216.34" }];
const metadata = {
issuer: "https://idp.example.com",
@@ -394,3 +394,41 @@ test("OIDC discovery blocks local and private issuer hosts before fetch", async
assert.equal(fetchImpl.mock.callCount(), 0);
});
+
+test("OIDC discovery blocks non-public resolved and translated addresses before fetch", async () => {
+ const fetchImpl = mock.fn(async () => ({ ok: true, json: async () => metadata }) as Response);
+
+ await assert.rejects(
+ () =>
+ discoverOidcMetadata("https://idp.example.com", fetchImpl, async () => [
+ { address: "100.64.0.1" },
+ ]),
+ /issuer host is not allowed/
+ );
+ await assert.rejects(
+ () => discoverOidcMetadata("https://[64:ff9b::7f00:1]", fetchImpl, publicLookup),
+ /issuer host is not allowed/
+ );
+
+ assert.equal(fetchImpl.mock.callCount(), 0);
+});
+
+test("OIDC discovery rejects ambiguous issuer URLs before fetch", async () => {
+ const fetchImpl = mock.fn(async () => ({ ok: true, json: async () => metadata }) as Response);
+
+ await assert.rejects(
+ () => discoverOidcMetadata("https://user:pass@idp.example.com", fetchImpl, publicLookup),
+ /issuer must not include credentials/
+ );
+ await assert.rejects(
+ () =>
+ discoverOidcMetadata(
+ "https://idp.example.com?next=https://127.0.0.1",
+ fetchImpl,
+ publicLookup
+ ),
+ /issuer must not include a query string/
+ );
+
+ assert.equal(fetchImpl.mock.callCount(), 0);
+});
diff --git a/packages/api/src/models/federation.ts b/packages/api/src/models/federation.ts
index b27c200e..0411c7f3 100644
--- a/packages/api/src/models/federation.ts
+++ b/packages/api/src/models/federation.ts
@@ -798,14 +798,60 @@ function isBlockedAddress(address: string) {
const normalized = address.toLowerCase().replace(/^\[|\]$/g, "");
if (isBlockedIpv4(normalized)) return true;
if (isIP(normalized) !== 6) return false;
- const first = Number.parseInt(normalized.split(":")[0] || "0", 16);
- return (
- normalized === "::" ||
- normalized === "::1" ||
- normalized.startsWith("::ffff:") ||
- (first >= 0xfc00 && first <= 0xfdff) ||
- (first >= 0xfe80 && first <= 0xfebf)
- );
+ const value = parseIpv6(normalized);
+ if (value === null) return true;
+ return IPV6_BLOCKED_RANGES.some(([base, bits]) => isIpv6InRange(value, base, bits));
+}
+
+const IPV6_BLOCKED_RANGES = [
+ [parseIpv6("::") as bigint, 96],
+ [parseIpv6("::ffff:0:0") as bigint, 96],
+ [parseIpv6("64:ff9b::") as bigint, 96],
+ [parseIpv6("64:ff9b:1::") as bigint, 48],
+ [parseIpv6("100::") as bigint, 64],
+ [parseIpv6("2001::") as bigint, 23],
+ [parseIpv6("2002::") as bigint, 16],
+ [parseIpv6("fc00::") as bigint, 7],
+ [parseIpv6("fe80::") as bigint, 10],
+ [parseIpv6("ff00::") as bigint, 8],
+] as const;
+
+function isIpv6InRange(value: bigint, base: bigint, bits: number) {
+ const shift = 128n - BigInt(bits);
+ return value >> shift === base >> shift;
+}
+
+function parseIpv6(address: string) {
+ const withoutZone = address.split("%")[0] || "";
+ const expanded = expandEmbeddedIpv4(withoutZone);
+ if (!expanded) return null;
+ const halves = expanded.split("::");
+ if (halves.length > 2) return null;
+ const left = halves[0] ? halves[0].split(":") : [];
+ const right = halves[1] ? halves[1].split(":") : [];
+ const missing = halves.length === 1 ? 0 : 8 - left.length - right.length;
+ if (missing < 0) return null;
+ const groups = [...left, ...Array.from({ length: missing }, () => "0"), ...right];
+ if (groups.length !== 8) return null;
+ let value = 0n;
+ for (const group of groups) {
+ if (!/^[0-9a-f]{1,4}$/i.test(group)) return null;
+ value = (value << 16n) + BigInt(Number.parseInt(group, 16));
+ }
+ return value;
+}
+
+function expandEmbeddedIpv4(address: string) {
+ if (!address.includes(".")) return address;
+ const index = address.lastIndexOf(":");
+ if (index < 0) return null;
+ const ipv4 = address.slice(index + 1);
+ if (isIP(ipv4) !== 4) return null;
+ const parts = ipv4.split(".").map((part) => Number.parseInt(part, 10));
+ const [a = 0, b = 0, c = 0, d = 0] = parts;
+ const high = (a << 8) + b;
+ const low = (c << 8) + d;
+ return `${address.slice(0, index)}:${high.toString(16)}:${low.toString(16)}`;
}
function isBlockedIpv4(address: string) {
@@ -815,10 +861,14 @@ function isBlockedIpv4(address: string) {
return (
a === 0 ||
a === 10 ||
+ (a === 100 && b >= 64 && b <= 127) ||
a === 127 ||
(a === 169 && b === 254) ||
(a === 172 && b >= 16 && b <= 31) ||
- (a === 192 && b === 168)
+ (a === 192 && b === 0) ||
+ (a === 192 && b === 168) ||
+ (a === 198 && (b === 18 || b === 19)) ||
+ a >= 224
);
}
@@ -1250,6 +1300,7 @@ async function encryptSecret(context: Context, secret: string | null | undefined
function normalizeIssuer(value: string) {
const url = normalizeHttpsUrl(value, "issuer");
+ if (new URL(url).search) throw new ValidationError("issuer must not include a query string");
return url.endsWith("/") ? url.slice(0, -1) : url;
}
@@ -1258,6 +1309,8 @@ function normalizeHttpsUrl(value: string, name: string) {
const url = new URL(value);
const host = url.hostname.toLowerCase();
const local = host === "localhost" || host === "127.0.0.1" || host === "::1";
+ if (url.username || url.password)
+ throw new ValidationError(`${name} must not include credentials`);
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) {
throw new ValidationError(`${name} must use https`);
}
diff --git a/packages/api/src/models/passwordResetTokens.test.ts b/packages/api/src/models/passwordResetTokens.test.ts
index 99e92f0a..de14818b 100644
--- a/packages/api/src/models/passwordResetTokens.test.ts
+++ b/packages/api/src/models/passwordResetTokens.test.ts
@@ -11,6 +11,7 @@ import type { Context } from "../types.ts";
import {
createPasswordResetToken,
getActivePasswordResetToken,
+ hashPasswordResetToken,
invalidateActivePasswordResetTokens,
type PasswordResetTokenRow,
} from "./passwordResetTokens.ts";
@@ -63,7 +64,11 @@ async function findActiveToken(
test("createPasswordResetToken stores only token hashes and invalidates older active tokens", async () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "darkauth-password-reset-token-test-"));
const { db, client, close } = await createPglite(directory);
- const context = { db, logger: createLogger() } as Context;
+ const context = {
+ db,
+ logger: createLogger(),
+ config: { kekPassphrase: "test-password-reset-pepper" },
+ } as Context;
try {
await ensurePasswordResetTokenTable(client);
@@ -92,6 +97,8 @@ test("createPasswordResetToken stores only token hashes and invalidates older ac
assert.equal(rows.filter((row) => row.consumedAt === null).length, 1);
assert.ok(rows.every((row) => row.tokenHash !== first.token));
assert.ok(rows.every((row) => row.tokenHash !== second.token));
+ assert.ok(rows.some((row) => row.tokenHash === hashPasswordResetToken(context, first.token)));
+ assert.ok(rows.some((row) => row.tokenHash === hashPasswordResetToken(context, second.token)));
assert.equal(await findActiveToken(context, first.token), null);
const active = await findActiveToken(context, second.token);
@@ -106,7 +113,11 @@ test("createPasswordResetToken stores only token hashes and invalidates older ac
test("invalidateActivePasswordResetTokens and expiry prevent token validation", async () => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "darkauth-password-reset-token-test-"));
const { db, client, close } = await createPglite(directory);
- const context = { db, logger: createLogger() } as Context;
+ const context = {
+ db,
+ logger: createLogger(),
+ config: { kekPassphrase: "test-password-reset-pepper" },
+ } as Context;
try {
await ensurePasswordResetTokenTable(client);
diff --git a/packages/api/src/models/passwordResetTokens.ts b/packages/api/src/models/passwordResetTokens.ts
index fa986106..90623e30 100644
--- a/packages/api/src/models/passwordResetTokens.ts
+++ b/packages/api/src/models/passwordResetTokens.ts
@@ -16,7 +16,8 @@ export interface PasswordResetTokenRow {
}
export function hashPasswordResetToken(context: Context, token: string): string {
- const pepper = context.config?.kekPassphrase || context.config?.installToken || "DarkAuth";
+ const pepper = context.config?.kekPassphrase;
+ if (!pepper) throw new ValidationError("Password reset token pepper is not configured");
return createHmac("sha256", pepper).update(token).digest("base64url");
}
diff --git a/packages/api/src/models/trustedDevices.ts b/packages/api/src/models/trustedDevices.ts
index 6d0cf2f9..e01e8897 100644
--- a/packages/api/src/models/trustedDevices.ts
+++ b/packages/api/src/models/trustedDevices.ts
@@ -1,9 +1,9 @@
-import type { webcrypto } from "node:crypto";
+import { randomInt, type webcrypto } from "node:crypto";
import { and, eq, gt, isNull } from "drizzle-orm";
import { deviceApprovalRequests, keyEnvelopes, trustedDevices } from "../db/schema.ts";
import { ConflictError, ForbiddenError, NotFoundError, ValidationError } from "../errors.ts";
import type { Context } from "../types.ts";
-import { generateRandomBytes, generateRandomString, sha256Base64Url } from "../utils/crypto.ts";
+import { generateRandomString, sha256Base64Url } from "../utils/crypto.ts";
import { assertScimTrustedDeviceApprovalPolicy } from "./scimPolicy.ts";
const DEVICE_APPROVAL_STATUSES = new Set(["pending", "approved", "consumed", "denied"]);
@@ -318,7 +318,7 @@ function validateCiphertext(value: Buffer, name: string) {
}
function generateVerificationCode() {
- const value = generateRandomBytes(4).readUInt32BE(0) % 1_000_000;
+ const value = randomInt(0, 1_000_000);
return value.toString().padStart(6, "0");
}
diff --git a/packages/api/src/services/branding.test.ts b/packages/api/src/services/branding.test.ts
new file mode 100644
index 00000000..eabc2aac
--- /dev/null
+++ b/packages/api/src/services/branding.test.ts
@@ -0,0 +1,15 @@
+import assert from "node:assert/strict";
+import { test } from "node:test";
+import { validateImageBase64 } from "./branding.ts";
+
+test("validateImageBase64 rejects svg branding images", () => {
+ const svg = Buffer.from("").toString("base64");
+
+ assert.throws(() => validateImageBase64(svg, "image/svg+xml"), /Invalid image type/);
+});
+
+test("validateImageBase64 keeps supported raster branding images", () => {
+ const data = Buffer.from("image-bytes").toString("base64");
+
+ assert.doesNotThrow(() => validateImageBase64(data, "image/png"));
+});
diff --git a/packages/api/src/services/branding.ts b/packages/api/src/services/branding.ts
index 817bc347..30b43e05 100644
--- a/packages/api/src/services/branding.ts
+++ b/packages/api/src/services/branding.ts
@@ -80,22 +80,12 @@ export function sanitizeCSS(css: string): string {
return sanitized;
}
-export function sanitizeSvg(svg: string): string {
- let s = svg;
- s = s.replace(/
\nBody`
+ );
+
+ assert.equal(
+ parsed.title,
+ "<ScRiPt>alert(1)</ScRiPt><img src=x onerror=alert(2)>"
+ );
+ assert.equal(parsed.content, "Body");
+});
+
+test("parseDecryptedNoteContent escapes malformed fallback title tags", () => {
+ const parsed = parseDecryptedNoteContent(`# \nBody`);
+
+ assert.equal(parsed.title, "<script>alert(1)</SCRIPT<script>");
+});
+
+test("getPreviewFromNoteContent escapes markup and event attributes", () => {
+ const preview = getPreviewFromNoteContent(`
Hello\nworld`);
+
+ assert.equal(preview, "<img src="x" onerror='alert(1)'> Hello world");
+});
+
+test("getPreviewFromNoteContent escapes repeated script fragments", () => {
+ const preview = getPreviewFromNoteContent(`javajavascript:script:`);
+
+ assert.equal(preview, "javajavascript:script:<ScRiPt>alert(1)</ScRiPt>");
+});
diff --git a/packages/test-suite/fixtures/testData.ts b/packages/test-suite/fixtures/testData.ts
index 7d8605f1..b00174fa 100644
--- a/packages/test-suite/fixtures/testData.ts
+++ b/packages/test-suite/fixtures/testData.ts
@@ -1,3 +1,5 @@
+import { randomBytes } from 'node:crypto';
+
export interface TestUser {
username: string;
email: string;
@@ -12,7 +14,7 @@ export interface TestAdmin {
}
function randomId(): string {
- return Math.random().toString(36).slice(2, 8);
+ return randomBytes(4).toString('hex');
}
export function createTestAdmin(overrides: Partial = {}): TestAdmin {
diff --git a/packages/test-suite/tests/admin/branding/branding.spec.ts b/packages/test-suite/tests/admin/branding/branding.spec.ts
index 5810b0c4..e9fd09e8 100644
--- a/packages/test-suite/tests/admin/branding/branding.spec.ts
+++ b/packages/test-suite/tests/admin/branding/branding.spec.ts
@@ -195,20 +195,26 @@ test.describe('Admin - Branding Settings', () => {
test('can upload and save a large logo without corrupting base64 data', async ({ page }) => {
await page.waitForSelector('text="Brand Color"', { timeout: 10000 });
- const svg = ``;
- const expectedLogo = Buffer.from(svg).toString('base64');
+ const png = Buffer.concat([
+ Buffer.from(
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mM88R8AAtUB6C8zHs0AAAAASUVORK5CYII=',
+ 'base64'
+ ),
+ Buffer.alloc(200_000, 0),
+ ]);
+ const expectedLogo = png.toString('base64');
await page.locator('input[type="file"]').first().setInputFiles({
- name: 'large-logo.svg',
- mimeType: 'image/svg+xml',
- buffer: Buffer.from(svg)
+ name: 'large-logo.png',
+ mimeType: 'image/png',
+ buffer: png,
});
const logoPreview = page.getByAltText('Logo preview');
await expect(logoPreview).toBeVisible();
await expect
.poll(async () => await logoPreview.getAttribute('src'), { timeout: 5000 })
- .toBe(`data:image/svg+xml;base64,${expectedLogo}`);
+ .toBe(`data:image/png;base64,${expectedLogo}`);
await page.locator('button:has-text("Save")').first().click();
await expect(page.getByText('Branding saved').first()).toBeVisible({ timeout: 5000 });
@@ -216,7 +222,7 @@ test.describe('Admin - Branding Settings', () => {
await page.locator('button:has-text("Reload")').first().click();
await expect(page.getByAltText('Logo preview')).toHaveAttribute(
'src',
- `data:image/svg+xml;base64,${expectedLogo}`
+ `data:image/png;base64,${expectedLogo}`
);
});
});
diff --git a/packages/test-suite/tests/admin/user-key-management-admin-ui.spec.ts b/packages/test-suite/tests/admin/user-key-management-admin-ui.spec.ts
index c54b3ea7..78f93ab5 100644
--- a/packages/test-suite/tests/admin/user-key-management-admin-ui.spec.ts
+++ b/packages/test-suite/tests/admin/user-key-management-admin-ui.spec.ts
@@ -1,3 +1,4 @@
+import { randomUUID } from 'node:crypto';
import { expect, test, type Locator, type Page } from '@playwright/test';
import { createAccountKey, createKeyEnvelope } from '@DarkAuth/api/src/models/keybag.ts';
import { createTrustedDevice } from '@DarkAuth/api/src/models/trustedDevices.ts';
@@ -12,7 +13,7 @@ const adminCred = {
};
function uniqueId(prefix: string) {
- return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
+ return `${prefix}-${Date.now()}-${randomUUID()}`;
}
function escapeXPathText(value: string) {