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: 5 additions & 0 deletions src/ui/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const ALLOWLIST: Record<string, string> = {
'index.html': 'text/html; charset=utf-8',
'app.css': 'text/css; charset=utf-8',
'app.js': 'text/javascript; charset=utf-8',
// The brand artwork, served as files rather than inlined as data URIs so the
// browser caches them and the CSS stays readable. Same-origin, so the
// no-external-requests rule holds.
'mark.png': 'image/png',
'lockup.png': 'image/png',
};

export interface Asset {
Expand Down
26 changes: 19 additions & 7 deletions src/ui/public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,25 @@ button, input, select, textarea { font: inherit; color: inherit; }

/* ── Mark ─────────────────────────────────────────────────────────────────── */

.mark { display: block; width: 100%; height: 100%; }
.mark-shield, .mark-split, .mark-dial { stroke: var(--dim); fill: none; }
.mark-split { opacity: 0.55; }
.mark-hinge { fill: var(--dim); }
.mark-key { fill: var(--brand); }
/*
* Brand artwork, served from /assets. Raster rather than SVG because the source
* is a rendered image with gradients and a glow that a monoline redraw loses.
* Both files are same-origin, so the no-external-requests rule still holds.
*/
.login-lockup {
display: block;
width: 280px;
max-width: 100%;
height: auto;
margin: 0 auto 1.5rem;
}

.login-mark { width: 52px; height: 52px; }
.brand-mark { display: block; width: 28px; height: 28px; }
.brand-mark {
display: block;
width: 30px;
height: auto;
flex: none;
}

/*
* Wordmark. The reference artwork uses a wide geometric face; a webfont is out
Expand Down Expand Up @@ -144,6 +155,7 @@ button, input, select, textarea { font: inherit; color: inherit; }

.login-sub {
margin: 0 0 2rem;
text-align: center;
color: var(--faint);
font-size: 0.9375rem;
}
Expand Down
11 changes: 0 additions & 11 deletions src/ui/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,6 @@ function clear(node) {
while (node.firstChild) node.removeChild(node.firstChild);
}

/** Stamp the inline SVG mark into every [data-mark] slot. */
function renderMarks() {
const template = $('mark-template');
for (const slot of document.querySelectorAll('[data-mark]')) {
if (slot.firstChild) continue;
slot.appendChild(template.content.cloneNode(true));
}
}

let toastTimer;
function toast(message, isError = false) {
const node = $('toast');
Expand Down Expand Up @@ -899,7 +890,6 @@ async function refresh() {

async function start(user) {
showApp(user);
renderMarks();
renderLegend();
const consumerData = await api('/api/consumers');
state.consumers = consumerData.consumers || [];
Expand Down Expand Up @@ -982,7 +972,6 @@ $('tag-filter').addEventListener('change', async (event) => {
// ── Boot ────────────────────────────────────────────────────────────────────

(async function boot() {
renderMarks();
try {
await start((await api('/api/session')).user);
} catch {
Expand Down
29 changes: 3 additions & 26 deletions src/ui/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,11 @@
</head>
<body>

<!--
The mark: a vault door set in a shield, split down the middle — steel on the
left, the lit keyhole on the right.

Drawn as inline SVG rather than embedding the raster artwork: this page is
served by the process holding every credential, on a network that may have no
route out, so the mark has to be self-contained, a few hundred bytes, and able
to inherit the theme's colours. The full-resolution artwork belongs in the
README and the favicon, where its weight costs nothing.
-->
<template id="mark-template">
<svg class="mark" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<path class="mark-shield" d="M16 2.6 27.2 7v9.2c0 6.7-4.7 10.8-11.2 12.9C9.5 27 4.8 22.9 4.8 16.2V7z"
stroke-width="1.9" stroke-linejoin="round" />
<path class="mark-split" d="M16 3.4v25" stroke-width="1.5" />
<circle class="mark-dial" cx="10.4" cy="15.4" r="3" stroke-width="1.5" />
<circle class="mark-hinge" cx="10.4" cy="15.4" r="0.85" />
<circle class="mark-key" cx="21.4" cy="14.2" r="2.15" />
<path class="mark-key" d="M20.6 15.9h1.6l.55 4.1h-2.7z" />
</svg>
</template>

<!-- ── Login ──────────────────────────────────────────────────────────────── -->
<section id="login-view" class="login-view" hidden>
<form id="login-form" class="login-card" autocomplete="off">
<div class="login-mark" data-mark></div>
<h1 class="wordmark"><span class="wm-a">Cipher</span><span class="wm-b">Gate</span></h1>
<p class="login-sub">Secrets gateway for agents. Every secret in this store is reachable from here.</p>
<img class="login-lockup" src="/assets/lockup.png" alt="CipherGate" width="280" height="221">
<p class="login-sub">Every secret in this store is reachable from here.</p>

<label class="field">
<span class="field-label">User</span>
Expand All @@ -57,7 +34,7 @@ <h1 class="wordmark"><span class="wm-a">Cipher</span><span class="wm-b">Gate</sp
<section id="app-view" class="app-view" hidden>
<header class="topbar">
<div class="brand">
<span class="brand-mark" data-mark></span>
<img class="brand-mark" src="/assets/mark.png" alt="" width="30" height="31">
<span class="brand-name wordmark"><span class="wm-a">Cipher</span><span class="wm-b">Gate</span></span>
</div>
<div class="topbar-right">
Expand Down
Binary file added src/ui/public/lockup.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 added src/ui/public/mark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions tests/ui-assets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ describe('UI assets', () => {
}
});

it('serves the brand artwork as intact PNG bytes', async () => {
ctx = await createCtx();
for (const name of ['mark.png', 'lockup.png']) {
const res = await ctx.app.inject({ method: 'GET', url: `/assets/${name}` });
expect(res.statusCode).toBe(200);
expect(res.headers['content-type']).toContain('image/png');
// The PNG signature, to prove the binary survived the read and the send
// rather than being mangled by a text encoding somewhere.
expect([...res.rawPayload.subarray(0, 4)]).toEqual([0x89, 0x50, 0x4e, 0x47]);
expect(res.rawPayload.length).toBeGreaterThan(1000);
}
});

it('404s an asset outside the allowlist', async () => {
ctx = await createCtx();
const res = await ctx.app.inject({ method: 'GET', url: '/assets/secrets.db' });
Expand Down
Loading