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
4 changes: 2 additions & 2 deletions docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ receive the code, so `https:` and non-loopback `http:` are both refused.

### Minting a token directly

Open <https://mcp.ikodo.dev/auth> in a browser and press the button. The token is blurred
until you point at it, and one click selects the whole value to copy.
Open <https://mcp.ikodo.dev/auth> in a browser and press the button. The token is covered by
asterisks until you point at it, and one click selects the whole value to copy.

The endpoint is built for that page and always answers in HTML — `POST` mints and returns
`201` with the token in the page body; there is no JSON representation. A script therefore
Expand Down
30 changes: 20 additions & 10 deletions src/http/token-issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,21 @@ button:hover{filter:brightness(1.08)}
:focus-visible{outline:2px solid var(--warn);outline-offset:3px}
.token{margin:26px 0;border:1px solid var(--rule-2);background:var(--sheet);padding:18px 20px;
font-family:var(--mono);font-size:14px;color:var(--ink);word-break:break-all;user-select:all}
/* The secret stays unreadable until asked for, so a screen share or a
screenshot of the rest of the page does not carry it away. The blur is
presentation only — the text underneath is still selectable, which is what
lets one click select the whole value with no script on the page. */
.token.secret{filter:blur(5px);transition:filter .12s ease-out;cursor:pointer}
.token.secret:hover,.token.secret:focus,.token.secret:active{filter:none}
/* The secret stays covered until asked for, so a screen share or a screenshot
of the rest of the page does not carry it away. Only one of the two spans is
ever in the layout: the stand-in is dropped on reveal so it can never land in
a copy, and the value is dropped while covered so it cannot be read. The
swap hangs off :hover and :focus because this page bans script and has no
other way to respond to a reader. */
.token.secret{cursor:pointer}
.token.secret .real{display:none}
.token.secret:hover .mask,.token.secret:focus .mask,.token.secret:active .mask{display:none}
.token.secret:hover .real,.token.secret:focus .real,.token.secret:active .real{display:inline}
.mask{color:var(--ink-2)}
.token::selection{background:var(--warn);color:var(--stock)}
.hint{font-size:13px;margin:-18px 0 26px}
.hint kbd{font-family:var(--mono);font-size:11px;border:1px solid var(--rule-2);border-radius:2px;
padding:1px 5px;color:var(--ink)}
@media (prefers-reduced-motion:reduce){.token.secret{transition:none}}
.once{border-left:2px solid var(--warn);background:color-mix(in srgb,var(--warn) 5%,var(--sheet));
padding:16px 20px;margin-bottom:26px}
.once b{display:block;font-family:var(--mono);font-size:10px;text-transform:uppercase;
Expand All @@ -82,17 +86,23 @@ comparing Base pools. <b>No wallet, no account, no email.</b></p>
<form method="post" action="${ISSUE_PATH}"><button type="submit">Create a token</button></form>`,
);

/** Matches the value's width so revealing it does not reflow the page. */
function cover(value: string): string {
return "*".repeat(value.length);
}

function issuedPage(token: string): string {
const stars = cover(token);
return page(
"Your DeepTrace token",
`<h1>Your token</h1>
<div class="once"><b>Shown once</b>Copy it now. It is stored only as a hash, so it cannot be
shown again. Losing it costs nothing &mdash; come back and take another.</div>
<div class="token secret" tabindex="0">${token}</div>
<p class="hint">Blurred until you point at it. One click selects the whole token, then
<div class="token secret" tabindex="0"><span class="mask" aria-hidden="true">${stars}</span><span class="real">${token}</span></div>
<p class="hint">Hidden until you point at it. One click selects the whole token, then
<kbd>Cmd</kbd>/<kbd>Ctrl</kbd>+<kbd>C</kbd> copies it.</p>
<p>Export it, then follow the <a href="/">setup guide</a> for your client:</p>
<div class="token secret" tabindex="0">export DEEPTRACE_TOKEN=&quot;${token}&quot;</div>
<div class="token secret" tabindex="0">export DEEPTRACE_TOKEN=&quot;<span class="mask" aria-hidden="true">${stars}</span><span class="real">${token}</span>&quot;</div>
<p>Never paste it into a chat, put it in a URL, or commit it.</p>`,
);
}
Expand Down
22 changes: 14 additions & 8 deletions tests/unit/token-issue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,20 +130,26 @@ describe("self-serve token issuance", () => {
}
});

it("keeps the issued token blurred until the reader asks for it", async () => {
it("covers the issued token with asterisks until the reader asks for it", async () => {
const { runtime, origin } = await startServer();
try {
const body = await (await fetch(`${origin}/auth`, { method: "POST" })).text();
const token = extractToken(body);

// Blur is presentation only. The value must stay selectable underneath,
// or the click-then-copy path this page documents stops working, and it
// must stay script-free so the page keeps its own CSP.
expect(body).toContain('class="token secret"');
expect(body).toContain("filter:blur(");
// Only one of the two spans is ever laid out: the asterisks leave on
// reveal so they can never land in a copy, and the value stays out of the
// layout while covered so it cannot be read off a screen share. Both
// swaps have to stay script-free, or the page loses its own CSP.
expect(token).not.toBe("");
expect(body).toContain(
`<span class="mask" aria-hidden="true">${"*".repeat(token.length)}</span>`,
);
expect(body).toContain(`<span class="real">${token}</span>`);
expect(body).toContain(".token.secret .real{display:none}");
expect(body).toMatch(/\.token\.secret:hover \.mask[^{]*\{display:none\}/u);
expect(body).toMatch(/\.token\.secret:hover \.real[^{]*\{display:inline\}/u);
expect(body).toContain("user-select:all");
expect(body).toMatch(/\.token\.secret:hover[^{]*\{filter:none\}/u);
expect(body).not.toMatch(/<script/u);
expect(extractToken(body)).not.toBe("");
} finally {
await runtime.close();
}
Expand Down