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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,9 @@ auto-clear; the server still sees only ciphertext.
| `docs/` | Architecture and security docs. |

Read the full design in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md), the
threat model in [`docs/SECURITY.md`](docs/SECURITY.md), and a screenshot
walkthrough of every screen in [`docs/USER_GUIDE.md`](docs/USER_GUIDE.md).
threat model in [`docs/SECURITY.md`](docs/SECURITY.md), a screenshot walkthrough
of every screen in [`docs/USER_GUIDE.md`](docs/USER_GUIDE.md), and the
white-label branding guide in [`docs/BRANDING.md`](docs/BRANDING.md).

## Installation

Expand Down
123 changes: 123 additions & 0 deletions docs/BRANDING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Branding & white-label customisation

Passman ships unbranded by default. Operators who want to deploy it
under their own name and colours edit a single file —
`packages/web/public/branding.json` — without rebuilding the web bundle.

The same web image can therefore serve any number of customers; mount a
different `branding.json` per deployment via Kubernetes ConfigMap, a
Docker bind mount, or a templated CI step.

## What you can customise

| Field | Type | Effect |
| ---------------- | ------ | -------------------------------------------------------------------------- |
| `appName` | string | Shown in the sidebar, browser tab title, and login/register heroes. |
| `tagline` | string | One-liner displayed under "Unlock your vault" / "Create your vault". |
| `logoUrl` | string | Same-origin path / `https://` URL / `data:` URL. Empty = default mark. |
| `brandColor` | hex | Primary accent (#RRGGBB, #RGB, or #RRGGBBAA). Drives the Connect button, focus rings, every accent surface. |
| `brandColorDark` | hex | Hover / gradient stop — defaults to a darkened brandColor. |
| `supportEmail` | string | Adds a `Support` mailto link in the user card. Empty hides it. |
| `footerText` | string | Small footer line in the sidebar (e.g. "© Acme Corp · Internal use only"). |

Every field is optional — anything you leave out falls through to the
defaults baked into `DEFAULT_BRANDING` in
[`packages/web/src/branding/defaults.ts`](../packages/web/src/branding/defaults.ts).

## Quick start (60 seconds)

1. **Replace the logo** — drop your SVG (or PNG, but SVG renders crisper) into
`packages/web/public/branding/logo.svg`. There's an example at
[`packages/web/public/branding/logo.example.svg`](../packages/web/public/branding/logo.example.svg)
you can copy as a starting point.

2. **Edit `packages/web/public/branding.json`:**

```jsonc
{
"appName": "Acme Vault",
"tagline": "Your team's credentials, safely off the cloud.",
"logoUrl": "/branding/logo.svg",
"brandColor": "#0a66c2",
"brandColorDark": "#084d92",
"supportEmail": "vault-support@acme.example",
"footerText": "© Acme Corp · Internal use only"
}
```

A reference is committed at
[`packages/web/public/branding.example.json`](../packages/web/public/branding.example.json).

3. **Reload the page.** Vite serves both files from `/`, the React app
fetches `/branding.json` on boot, and the document re-themes
immediately. No build step needed in dev.

4. **For production**, the same files are copied to `dist/` by `vite
build`. To swap branding without rebuilding, mount your own
`branding.json` and `branding/logo.svg` over the served `dist/`
directory at deploy time.

## CSP & security

The default index.html has a strict Content-Security-Policy. Logo URLs
must satisfy `img-src 'self' data:` — i.e.:

- **Same-origin paths** (`/branding/logo.svg`) — works out of the box.
- **`data:` URLs** — also allowed; useful for inlining a small SVG
directly in `branding.json` if you can't serve a separate file.
- **Cross-origin URLs (`https://cdn.example/...`)** — blocked by the
default CSP. Either widen `img-src` in
[`packages/web/index.html`](../packages/web/index.html) or proxy the
asset under your own origin.

The branding loader rejects unsafe schemes (`javascript:`, `file://`,
`ftp://` …) silently and falls back to the default mark — a safety net
for the case where `branding.json` is ever filled by user input rather
than an operator.

## What is NOT customised (and why)

- **Encryption defaults** — Argon2id parameters, AES-256-GCM, the
zero-knowledge protocol. These are security-critical; changing them
belongs in the server's config, not a customer-facing brand file.
- **Per-user themes** — `branding.json` is instance-wide. A customer
who needs per-user theming should layer that on top of this feature
(e.g. let a user pick from a list the operator has approved).
- **Dark / light mode toggle** — Passman is dark-first by design. The
brand colour is the only colour token an operator overrides today.
- **Extension popup** — the Manifest V3 popup is a separate context
with its own bundle. Branding the extension is tracked as a
follow-up; for now the popup keeps the default mark.

## Testing your override locally

```bash
# in repo root
npm run dev --workspace=@passman/web
# open http://localhost:5173 — your branding.json is served at /branding.json
```

The login page should now show your logo + appName + tagline above the
form, and the brand colour should drive the Unlock button + every
accent in the vault.

If the change doesn't appear, open DevTools → Network and confirm
`/branding.json` returns your override. A failed fetch (404, network
error, malformed JSON) silently falls back to defaults.

## How it works

The branding system is implemented in
[`packages/web/src/branding/`](../packages/web/src/branding/):

- `types.ts` — the `Branding` shape and partial `BrandingOverride`
- `defaults.ts` — frozen `DEFAULT_BRANDING`
- `load.ts` — fetch + sanitise + merge, plus `applyBranding()` that
writes `--brand` / `--brand-2` / `--brand-soft` / `--brand-line` CSS
variables on `:root` so any already-painted accent re-themes
- `BrandingProvider.tsx` — React context provider that fetches once on
mount; components read it with `useBranding()`

The provider does NOT block render — the first paint uses defaults and
the customer's branding takes effect on the second render. That keeps
the app booting under any failure mode.
26 changes: 25 additions & 1 deletion docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,31 @@ matches the URL stored on the item.

---

## 7. Tips
## 7. White-label branding

Passman ships unbranded by default, but a single `branding.json` file
swaps the logo, app name, tagline, and accent colour app-wide. No
rebuild required — operators edit one file (or mount it as a
ConfigMap) and the same web bundle serves any company.

The login page below shows what the default and a custom override look
like side by side:

| Default | White-labelled |
| ------------------------------ | --------------------------------------- |
| ![Default Passman](img/login.png) | ![Acme Vault override](img/login-whitelabel.png) |

The brand colour cascades to every accent in the UI — the Connect
button on every grid row, the focus rings, the protocol pills, the
selection highlight. Setting `brandColor` once re-themes the whole
product.

See [`docs/BRANDING.md`](BRANDING.md) for the full field reference,
CSP notes, and a 60-second quick-start.

---

## 8. Tips

- **Use the protocol field.** Even if Passman can infer the protocol
from the port, declaring it explicitly is what unlocks the right
Expand Down
Binary file added docs/img/login-whitelabel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/img/login.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/img/register.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion docs/preview/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
<h1>Passman screenshots</h1>
<ul>
<li><a href="register.html">Register</a></li>
<li><a href="login.html">Login</a></li>
<li><a href="login.html">Login &mdash; default branding</a></li>
<li><a href="login-whitelabel.html">Login &mdash; with white-label override (Acme)</a></li>
<li><a href="vault.html">Vault &mdash; sidebar dashboard</a></li>
<li><a href="vault-connect.html">Vault &mdash; Connect dialog (JDBC / SSH / RDP / copy-command)</a></li>
<li><a href="vault-add.html">Vault &mdash; add credential form</a></li>
Expand Down
43 changes: 43 additions & 0 deletions docs/preview/login-whitelabel.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Acme Vault — Unlock</title>
<link rel="stylesheet" href="../../packages/web/src/styles.css" />
<style>
/* Equivalent to applyBranding({ brandColor: "#0a66c2" }) at runtime — */
/* the React app sets these in JS so any already-painted accent re-themes. */
:root {
--brand: #0a66c2;
--brand-2: #084d92;
--brand-soft: rgba(10, 102, 194, 0.12);
--brand-line: rgba(10, 102, 194, 0.32);
}
</style>
</head>
<body>
<main class="auth-container">
<div class="auth-brand">
<img class="brand-logo" width="28" height="28"
src="../../packages/web/public/branding/logo.example.svg"
alt="Acme Vault logo" />
<span class="auth-brand-name">Acme Vault</span>
</div>
<h1>Unlock your vault</h1>
<p class="auth-tagline">Your team's credentials, safely off the cloud.</p>
<form>
<label>
Email
<input type="email" value="ops@acme.example" />
</label>
<label>
Master password
<input type="password" value="............" />
</label>
<button type="submit">Unlock</button>
</form>
<p>New here? <a href="register.html">Create a vault</a></p>
</main>
</body>
</html>
5 changes: 5 additions & 0 deletions docs/preview/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
</head>
<body>
<main class="auth-container">
<div class="auth-brand">
<div class="brand-mark brand-mark-lg"></div>
<span class="auth-brand-name">Passman</span>
</div>
<h1>Unlock your vault</h1>
<p class="auth-tagline">Zero-knowledge password manager</p>
<form>
<label>
Email
Expand Down
5 changes: 5 additions & 0 deletions docs/preview/register.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
</head>
<body>
<main class="auth-container">
<div class="auth-brand">
<div class="brand-mark brand-mark-lg"></div>
<span class="auth-brand-name">Passman</span>
</div>
<h1>Create your vault</h1>
<p class="auth-tagline">Zero-knowledge password manager</p>
<p class="warning">
⚠️ Your master password is the only key. We can't recover it. If you
forget it, your vault is permanently inaccessible.
Expand Down
9 changes: 9 additions & 0 deletions packages/web/public/branding.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"appName": "Acme Vault",
"tagline": "Your team's credentials, safely off the cloud.",
"logoUrl": "/branding/logo.svg",
"brandColor": "#0a66c2",
"brandColorDark": "#084d92",
"supportEmail": "vault-support@acme.example",
"footerText": "© Acme Corp · Internal use only"
}
1 change: 1 addition & 0 deletions packages/web/public/branding.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
10 changes: 10 additions & 0 deletions packages/web/public/branding/logo.example.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions packages/web/src/branding/BrandingProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { createContext, useContext, useEffect, useState } from "react";

import { DEFAULT_BRANDING } from "./defaults.js";
import { applyBranding, loadBranding } from "./load.js";
import type { Branding } from "./types.js";

const BrandingContext = createContext<Branding>(DEFAULT_BRANDING);

interface Props {
children: React.ReactNode;
}

/**
* Fetches `/branding.json` once, applies the resulting brand to the
* document (page title + CSS variables), and exposes the merged branding
* via React context. Components read it with `useBranding()`.
*
* The first paint uses defaults — overrides take effect on the second
* render once the fetch resolves. We deliberately do NOT block render
* on the fetch: the app should boot even if branding.json is unreachable.
*/
export function BrandingProvider({ children }: Props) {
const [brand, setBrand] = useState<Branding>(DEFAULT_BRANDING);

useEffect(() => {
let cancelled = false;
void loadBranding().then((b) => {
if (cancelled) return;
setBrand(b);
applyBranding(b);
});
return () => {
cancelled = true;
};
}, []);

return (
<BrandingContext.Provider value={brand}>{children}</BrandingContext.Provider>
);
}

export function useBranding(): Branding {
return useContext(BrandingContext);
}
21 changes: 21 additions & 0 deletions packages/web/src/branding/defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Branding } from "./types.js";

/**
* Default brand identity. These are the values an unbranded `passman` ships
* with and the fallback when `branding.json` is missing, malformed, or fails
* to fetch. Anything left out of an operator's `branding.json` falls through
* to these.
*
* Hex colour values mirror the design tokens in `styles.css`. Keep them in
* sync — the loader writes the `--brand` / `--brand-2` CSS variables at
* runtime, but the rest of the design system is defined in CSS.
*/
export const DEFAULT_BRANDING: Branding = Object.freeze({
appName: "Passman",
tagline: "Zero-knowledge password manager",
logoUrl: "",
brandColor: "#3ECF8E",
brandColorDark: "#14B884",
supportEmail: "",
footerText: "",
});
4 changes: 4 additions & 0 deletions packages/web/src/branding/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { BrandingProvider, useBranding } from "./BrandingProvider.js";
export { DEFAULT_BRANDING } from "./defaults.js";
export { applyBranding, loadBranding, mergeBranding } from "./load.js";
export type { Branding, BrandingOverride } from "./types.js";
Loading
Loading