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
51 changes: 49 additions & 2 deletions src/auth/callback-page.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { describe, expect, test } from "bun:test";

import {
PRODUCT_GITHUB_LABEL,
PRODUCT_GITHUB_URL,
PRODUCT_SITE_LABEL,
PRODUCT_SITE_URL,
} from "../branding.js";
import { callbackPageHtml, humanizeIdentifier } from "./callback-page.js";

describe("humanizeIdentifier", () => {
Expand Down Expand Up @@ -42,8 +48,49 @@ describe("callbackPageHtml", () => {
);
});

test("the page reaches for nothing off the machine", () => {
test("the footer links to the product site and the GitHub org", () => {
const html = callbackPageHtml({ subject: "linear" });
expect(html).not.toMatch(/https?:\/\/(?!www\.w3\.org)/);
const link = (url: string, label: string) =>
`<a href="${url}" target="_blank" rel="noopener noreferrer">${label}</a>`;
expect(html).toContain(link(PRODUCT_SITE_URL, PRODUCT_SITE_LABEL));
expect(html).toContain(link(PRODUCT_GITHUB_URL, PRODUCT_GITHUB_LABEL));
});

test("each footer label names the destination its URL actually points at", () => {
expect(PRODUCT_SITE_URL).toContain(PRODUCT_SITE_LABEL);
expect(PRODUCT_GITHUB_URL).toContain(PRODUCT_GITHUB_LABEL);
});

// An allowlist rather than a shape match: an unexpected origin fails loudly
// instead of passing because it happened to be wrapped in an anchor tag.
const allowedOrigins = new Set([
PRODUCT_SITE_URL,
PRODUCT_GITHUB_URL,
// The SVG namespace the wordmark declares; a URI, never fetched.
"http://www.w3.org/2000/svg",
]);

const offMachineOrigins = (html: string): readonly string[] => {
// Scheme-qualified and protocol-relative alike, since either would load.
const found =
html.match(
/(?:[a-z][a-z0-9+.-]*:)?\/\/[a-z0-9-]+(?:\.[a-z0-9-]+)+[^"'`)\s<>]*/gi,
) ?? [];
return found.filter(
(ref) => ![...allowedOrigins].some((origin) => ref.startsWith(origin)),
);
};

for (const [outcome, page] of [
["success", { subject: "linear" }],
["failure", { subject: "linear", error: "access_denied" }],
] as const) {
test(`the ${outcome} page names no off-machine origin beyond the footer links`, () => {
const html = callbackPageHtml(page);
expect(offMachineOrigins(html)).toEqual([]);
expect(html).not.toMatch(
/\b(?:fetch|XMLHttpRequest|WebSocket|EventSource|sendBeacon|importScripts)\s*\(/,
);
});
}
});
34 changes: 32 additions & 2 deletions src/auth/callback-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
* authorization callback.
*/

import { PRODUCT_NAME } from "../branding.js";
import {
PRODUCT_GITHUB_LABEL,
PRODUCT_GITHUB_URL,
PRODUCT_NAME,
PRODUCT_SITE_LABEL,
PRODUCT_SITE_URL,
} from "../branding.js";

/** Corbits wordmark, background layers stripped so it inherits `currentColor`. */
const WORDMARK = `<svg class="wordmark" viewBox="0 0 1000 400" role="img" aria-label="Corbits" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M195.057 192.125L160.182 225.625C157.016 214.042 153.099 205.042 148.432 198.625C143.849 192.208 138.974 189 133.807 189C129.724 189 126.557 190.75 124.307 194.25C122.141 197.75 121.057 202.75 121.057 209.25C121.057 224.583 124.307 237.167 130.807 247C137.307 256.833 145.641 261.75 155.807 261.75C161.391 261.75 166.682 260.333 171.682 257.5C176.766 254.667 181.141 250.708 184.807 245.625L196.557 254.25C189.807 268.667 181.266 279.667 170.932 287.25C160.599 294.833 149.016 298.625 136.182 298.625C120.016 298.625 106.724 293.083 96.3072 282C85.9739 270.833 80.8072 256.667 80.8072 239.5C80.8072 219.583 87.7239 202.333 101.557 187.75C115.391 173.083 131.349 165.75 149.432 165.75C158.682 165.75 167.182 168 174.932 172.5C182.682 177 189.391 183.542 195.057 192.125ZM303.705 248.125C303.705 231.292 300.288 216.542 293.455 203.875C286.705 191.208 279.371 184.875 271.455 184.875C266.371 184.875 262.413 187.458 259.58 192.625C256.746 197.792 255.33 205.125 255.33 214.625C255.33 232.208 258.621 247.417 265.205 260.25C271.788 273 279.08 279.375 287.08 279.375C292.496 279.375 296.621 276.75 299.455 271.5C302.288 266.167 303.705 258.375 303.705 248.125ZM279.705 165.75C300.038 165.75 316.913 172.042 330.33 184.625C343.83 197.208 350.58 213.083 350.58 232.25C350.58 251.417 343.871 267.292 330.455 279.875C317.121 292.375 300.205 298.625 279.705 298.625C259.121 298.625 242.08 292.375 228.58 279.875C215.163 267.375 208.455 251.5 208.455 232.25C208.455 213 215.163 197.125 228.58 184.625C242.08 172.042 259.121 165.75 279.705 165.75ZM464.727 164.5L478.352 169.25L463.227 212C458.311 210.083 454.144 208.667 450.727 207.75C447.394 206.833 444.561 206.375 442.227 206.375C437.144 206.375 433.144 208.125 430.227 211.625C427.311 215.042 425.852 219.792 425.852 225.875V279.125H438.852V295H371.352V279.125H380.227V214.875C380.227 211.542 379.936 209.292 379.352 208.125C378.769 206.958 377.686 205.875 376.102 204.875L371.352 208.75L362.102 198.5L389.727 165.75C397.477 169.333 403.602 173.208 408.102 177.375C412.686 181.542 415.936 186.292 417.852 191.625C425.102 180.542 430.477 173.458 433.977 170.375C437.561 167.292 441.311 165.75 445.227 165.75C447.394 165.75 449.894 166.167 452.727 167C455.644 167.75 458.727 168.875 461.977 170.375L464.727 164.5ZM540.875 248.875C540.875 257.292 542.417 263.75 545.5 268.25C548.583 272.75 552.958 275 558.625 275C564.625 275 569.375 272.25 572.875 266.75C576.458 261.25 578.25 253.667 578.25 244C578.25 229.75 575.75 218.125 570.75 209.125C565.833 200.125 559.542 195.625 551.875 195.625C550.125 195.625 548.417 195.917 546.75 196.5C545.083 197 543.125 197.917 540.875 199.25V248.875ZM550.375 118.375V134.5L540.875 137.5V181.875C548.792 176.208 555.75 172.125 561.75 169.625C567.75 167.042 573.417 165.75 578.75 165.75C590.667 165.75 600.542 170.417 608.375 179.75C616.292 189.083 620.25 200.792 620.25 214.875C620.25 226.458 617.958 237.417 613.375 247.75C608.875 258 602.208 267.25 593.375 275.5C585.458 283.083 576.958 288.833 567.875 292.75C558.792 296.667 549.375 298.625 539.625 298.625C532.208 298.625 524.458 297.333 516.375 294.75C508.292 292.083 501.208 288.583 495.125 284.25V152.125L486.375 155V138.875L550.375 118.375ZM694.898 165.75H696.648C696.231 171.167 695.898 175.625 695.648 179.125C695.481 182.542 695.398 185.417 695.398 187.75V279.125H704.273V295H640.898V279.125H649.773V193.375L640.898 194.875V180.625L694.898 165.75ZM677.148 126C683.231 126 688.106 127.417 691.773 130.25C695.523 133.083 697.398 136.708 697.398 141.125C697.398 147.125 694.314 152.25 688.148 156.5C682.064 160.75 674.731 162.875 666.148 162.875C660.898 162.875 656.689 161.542 653.523 158.875C650.356 156.208 648.773 152.667 648.773 148.25C648.773 142.5 651.648 137.375 657.398 132.875C663.148 128.292 669.731 126 677.148 126ZM771.67 137.25L784.545 140.125V170.625H806.795L804.17 185.375C803.837 187.875 803.42 189.417 802.92 190C802.42 190.5 801.587 190.75 800.42 190.75H784.545V255.375C784.545 261.792 785.378 266.333 787.045 269C788.712 271.667 791.545 273 795.545 273C797.045 273 798.628 272.708 800.295 272.125C802.045 271.542 803.962 270.625 806.045 269.375L812.67 279.125C806.503 285.458 799.92 290.292 792.92 293.625C785.92 296.958 778.92 298.625 771.92 298.625C760.253 298.625 751.795 295.875 746.545 290.375C741.295 284.792 738.67 275.75 738.67 263.25V190.75H726.92V176.25C738.253 172.917 747.628 168 755.045 161.5C762.545 155 768.087 146.917 771.67 137.25ZM916.693 179.75L898.068 209.25C890.818 200.667 884.401 194.333 878.818 190.25C873.318 186.167 868.401 184.125 864.068 184.125C861.651 184.125 859.693 184.833 858.193 186.25C856.776 187.583 856.068 189.375 856.068 191.625C856.068 194.292 857.443 196.875 860.193 199.375C863.026 201.875 868.651 205.25 877.068 209.5C895.651 218.833 907.484 226.417 912.568 232.25C917.734 238.083 920.318 245.083 920.318 253.25C920.318 265.833 914.984 276.542 904.318 285.375C893.734 294.208 880.568 298.625 864.818 298.625C855.484 298.625 846.568 297.125 838.068 294.125C829.651 291.042 821.651 286.458 814.068 280.375L835.068 247.875C844.151 257.875 852.276 265.417 859.443 270.5C866.609 275.583 872.651 278.125 877.568 278.125C880.401 278.125 882.568 277.458 884.068 276.125C885.568 274.708 886.318 272.708 886.318 270.125C886.318 265.708 878.693 259.458 863.443 251.375L863.318 251.25C862.401 250.75 861.151 250.083 859.568 249.25C836.734 236.917 825.318 223.333 825.318 208.5C825.318 195.833 829.734 185.542 838.568 177.625C847.484 169.708 859.193 165.75 873.693 165.75C881.276 165.75 888.609 166.917 895.693 169.25C902.776 171.583 909.776 175.083 916.693 179.75Z" fill="currentColor"/> </svg>`;
Expand Down Expand Up @@ -103,7 +109,21 @@ footer {
font-size: 0.75rem;
letter-spacing: 0.02em;
color: var(--ink-faint);
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem 0.75rem;
}
footer a {
color: var(--ink-dim);
text-decoration: none;
border-bottom: 1px solid transparent;
}
footer a:hover {
color: var(--ink);
border-bottom-color: var(--rule);
}
footer .sep { color: var(--rule); }
@keyframes rise {
from { opacity: 0; transform: translateY(0.5rem); }
to { opacity: 1; transform: none; }
Expand Down Expand Up @@ -227,6 +247,16 @@ export function humanizeIdentifier(raw: string): string {
return words.charAt(0).toUpperCase() + words.slice(1);
}

/**
* A footer link, preceded by its separator.
*
* Opens in a new tab so the operator keeps the tab telling them the
* authorization finished and this window is safe to close.
*/
function footerLink(url: string, label: string): string {
return `<span class="sep" aria-hidden="true">·</span><a href="${url}" target="_blank" rel="noopener noreferrer">${label}</a>`;
}

export type CallbackPage = {
/** What was being authorized: an MCP server or provider name. */
readonly subject?: string;
Expand Down Expand Up @@ -274,7 +304,7 @@ export function callbackPageHtml(page: CallbackPage = {}): string {
`<h1>${heading}</h1>`,
`<p class="body">${body}</p>`,
"<hr>",
`<footer>${PRODUCT_NAME}</footer>`,
`<footer>${PRODUCT_NAME}${footerLink(PRODUCT_SITE_URL, PRODUCT_SITE_LABEL)}${footerLink(PRODUCT_GITHUB_URL, PRODUCT_GITHUB_LABEL)}</footer>`,
"</div>",
"</main></body>",
`<script>${SCRIPT}</script>`,
Expand Down
8 changes: 8 additions & 0 deletions src/branding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export const PRODUCT_NAME = "Corbits Code";
// Short attribution form used in compact UI (status lines, footers).
export const PRODUCT_SHORT_NAME = "Corbits";

// Canonical public URLs, each paired with the text shown in its place. The label
// is spelled out rather than stripped from the URL so it can diverge from the
// bare host later without a rendering helper having to special-case it.
export const PRODUCT_SITE_URL = "https://corbits.dev";
export const PRODUCT_SITE_LABEL = "corbits.dev";
export const PRODUCT_GITHUB_URL = "https://github.com/corbitsdev";
export const PRODUCT_GITHUB_LABEL = "github.com/corbitsdev";

export const COMMAND_NAME = "corbits";

export const SETTINGS_DIR_NAME = ".corbits";
Expand Down
Loading