Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
57a6eb5
feat: Mike for Mac — native desktop shell prototype (Electron)
amal66 Aug 9, 2026
79ab794
feat(desktop): product-mark app icon + packaged-app e2e against the r…
amal66 Aug 9, 2026
3e6b494
fix(desktop): make the shell honor the web app's real navigation, pop…
amal66 Aug 16, 2026
3b1431e
test(desktop): prove every fixed flow against the real packaged app +…
amal66 Aug 16, 2026
cd88176
fix(backend): keep window.opener alive on the MCP OAuth callback popup
amal66 Aug 16, 2026
5b36bfc
fix(backend): sign presigned download URLs against a browser-reachabl…
amal66 Aug 16, 2026
0891e67
docs(desktop): wire the signed-release build and document signing end…
amal66 Aug 16, 2026
a30848f
test(desktop): match the captured external URL by whole line, not sub…
amal66 Aug 17, 2026
17f9346
feat(desktop): default the shell to the hosted service so a download …
amal66 Aug 18, 2026
491cfc2
docs(desktop): plan for a fully self-contained Mike.app (local stack …
amal66 Aug 18, 2026
c696050
feat(backend): filesystem storage driver with expiring blob-token sig…
amal66 Aug 18, 2026
91e109a
feat(frontend): opt-in Next standalone output (NEXT_OUTPUT_STANDALONE=1)
amal66 Aug 18, 2026
9a711ca
feat(desktop): self-contained local mode — the whole Mike stack insid…
amal66 Aug 18, 2026
ec234e2
feat(desktop): first-launch chooser — cloud, this Mac, or your own se…
amal66 Aug 18, 2026
11b6d9b
feat(desktop): packaged self-contained build proven; signed-release w…
amal66 Aug 18, 2026
aade2ab
fix(desktop): reset native drag regions after navigation so the produ…
amal66 Aug 19, 2026
9194e80
feat(desktop): guest mode — one click from the login page into the lo…
amal66 Aug 19, 2026
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
6 changes: 6 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ R2_ENDPOINT_URL=https://your-account-id.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=your-r2-access-key
R2_SECRET_ACCESS_KEY=your-r2-secret-key
R2_BUCKET_NAME=mike
# Only needed when the storage endpoint above is NOT reachable from the user's
# browser (e.g. a compose-internal hostname). Presigned download URLs are
# signed against this instead. Cloud R2/S3 endpoints are already public, so
# leave it unset there. NOTE: for the docker-compose stack this must be set in
# the compose-root .env or the shell, not here — see docker-compose.yml.
# R2_PUBLIC_ENDPOINT_URL=https://files.your-domain.com

GEMINI_API_KEY=your-gemini-key
ANTHROPIC_API_KEY=your-anthropic-key
Expand Down
71 changes: 71 additions & 0 deletions backend/src/__tests__/integration/user.routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,77 @@ describe("user.routes", () => {
});
});

// ── GET /user/mcp-connectors/oauth/callback (popup hand-off page) ─────
describe("GET /user/mcp-connectors/oauth/callback", () => {
// The 400 path (missing state/code) renders the same popup HTML via
// the same header helper as the success path, without needing any
// real OAuth machinery — so it is the regression probe for both.
it("relaxes COOP so window.opener survives, alongside the nonce CSP", async () => {
const log = vi
.spyOn(console, "error")
.mockImplementation(() => undefined);

const res = await request(app).get(
"/user/mcp-connectors/oauth/callback",
);

expect(res.status).toBe(400);
// Load-bearing: helmet's default COOP of same-origin would sever
// window.opener the moment the popup returns from the
// cross-origin consent page, silently breaking the postMessage
// hand-off. This must hold through the full app assembly (helmet
// runs on this very request), not just on the bare router.
expect(res.headers["cross-origin-opener-policy"]).toBe(
"unsafe-none",
);
// The route-scoped CSP (with the per-response script nonce) must
// survive alongside the COOP relaxation.
expect(res.headers["content-security-policy"]).toContain(
"script-src 'nonce-",
);
log.mockRestore();
});

it("keeps helmet's default COOP on every other route", async () => {
// Contrast probe: the relaxation must stay scoped to the popup
// page. If it ever leaks app-wide, this fails.
supabaseState.tables.user_profiles = {
data: profileRow(),
error: null,
};

const res = await request(app).get("/user/profile").set(...AUTH);

expect(res.headers["cross-origin-opener-policy"]).toBe(
"same-origin",
);
});

it("neutralizes a </script> breakout in the attacker-controlled error detail", async () => {
const log = vi
.spyOn(console, "error")
.mockImplementation(() => undefined);

// ?error= flows into the inline script's JSON literal.
// JSON.stringify leaves "<" alone, so an unescaped payload of
// </script><script>… would close the legitimate script element
// and inject markup.
const res = await request(app).get(
"/user/mcp-connectors/oauth/callback?error=" +
encodeURIComponent("</script><script>evil()</script>"),
);

expect(res.status).toBe(400);
// No breakout: the only </script> left is the page's own closing
// tag, and the payload's "<" chars were escaped to \u003c inside
// the JS string literal.
expect(res.text).not.toContain("</script><script");
expect(res.text.match(/<\/script>/g)).toHaveLength(1);
expect(res.text).toContain("\\u003c/script>");
log.mockRestore();
});
});

// ── PATCH /user/security/mfa-login (factor-gated, MFA-guarded) ────────
describe("PATCH /user/security/mfa-login", () => {
it("returns 400 when enabling without a verified TOTP factor", async () => {
Expand Down
131 changes: 131 additions & 0 deletions backend/src/lib/__tests__/storageFs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import fs from "fs/promises";
import os from "os";
import path from "path";

// STORAGE_DRIVER / STORAGE_FS_ROOT are captured at module load in ../storage,
// so each case configures process.env BEFORE importing a fresh copy (same
// reset-then-dynamic-import pattern as storagePresign.test.ts).
async function loadFsStorage(root: string) {
vi.resetModules();
process.env.STORAGE_DRIVER = "fs";
process.env.STORAGE_FS_ROOT = root;
process.env.DOWNLOAD_SIGNING_SECRET = "test-signing-secret";
process.env.BACKEND_PUBLIC_URL = "http://localhost:3001";
delete process.env.R2_ENDPOINT_URL;
delete process.env.R2_ACCESS_KEY_ID;
delete process.env.R2_SECRET_ACCESS_KEY;
return import("../storage");
}

let root: string;

beforeEach(async () => {
root = await fs.mkdtemp(path.join(os.tmpdir(), "mike-storage-fs-"));
});

afterEach(async () => {
delete process.env.STORAGE_DRIVER;
delete process.env.STORAGE_FS_ROOT;
delete process.env.BACKEND_PUBLIC_URL;
await fs.rm(root, { recursive: true, force: true });
});

describe("filesystem storage driver", () => {
it("is enabled by STORAGE_DRIVER=fs without any R2 config", async () => {
const { storageEnabled } = await loadFsStorage(root);
expect(storageEnabled).toBe(true);
});

it("round-trips upload → download → delete", async () => {
const storage = await loadFsStorage(root);
const key = "documents/u1/d1/source.pdf";
const content = new TextEncoder().encode("pdf bytes").buffer as ArrayBuffer;

await storage.uploadFile(key, content, "application/pdf");
const back = await storage.downloadFile(key);
expect(back).not.toBeNull();
expect(Buffer.from(back!).toString()).toBe("pdf bytes");

await storage.deleteFile(key);
expect(await storage.downloadFile(key)).toBeNull();
});

it("deleteFile tolerates a missing key (S3 delete semantics)", async () => {
const storage = await loadFsStorage(root);
await expect(storage.deleteFile("documents/u1/gone.bin")).resolves
.toBeUndefined();
});

it("listFiles matches S3 string-prefix semantics, not directories", async () => {
const storage = await loadFsStorage(root);
const enc = (s: string) => new TextEncoder().encode(s).buffer as ArrayBuffer;
await storage.uploadFile("documents/u1/d1/source.pdf", enc("a"), "x");
await storage.uploadFile("documents/u1/d1/versions/v1.docx", enc("b"), "x");
await storage.uploadFile("documents/u1/d2/source.pdf", enc("c"), "x");
await storage.uploadFile("generated/u1/d1/generated.docx", enc("d"), "x");

// Whole-directory prefix
expect(await storage.listFiles("documents/u1/d1/")).toEqual([
"documents/u1/d1/source.pdf",
"documents/u1/d1/versions/v1.docx",
]);
// Partial-segment prefix must match d1 AND d2, like S3 would
expect(await storage.listFiles("documents/u1/d")).toEqual([
"documents/u1/d1/source.pdf",
"documents/u1/d1/versions/v1.docx",
"documents/u1/d2/source.pdf",
]);
expect(await storage.listFiles("nope/")).toEqual([]);
});

it("getSignedUrl returns an expiring blob-token URL on the backend", async () => {
const storage = await loadFsStorage(root);
const url = await storage.getSignedUrl(
"documents/u1/d1/source.pdf",
3600,
"Contract v2.pdf",
);
expect(url).toMatch(
/^http:\/\/localhost:3001\/download\/signed\/[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/,
);

const { verifyBlobToken } = await import("../downloadTokens");
const token = url!.split("/download/signed/")[1];
expect(verifyBlobToken(token)).toEqual({
path: "documents/u1/d1/source.pdf",
filename: "Contract v2.pdf",
});
});

it("rejects keys that escape the storage root", async () => {
const storage = await loadFsStorage(root);
await expect(
storage.uploadFile(
"../outside.bin",
new ArrayBuffer(1),
"application/octet-stream",
),
).rejects.toThrow(/escapes STORAGE_FS_ROOT/);
});
});

describe("blob tokens", () => {
it("expired tokens verify as null", async () => {
await loadFsStorage(root);
const { signBlobToken, verifyBlobToken } = await import("../downloadTokens");
const token = signBlobToken("documents/u1/d1/source.pdf", "a.pdf", -5);
expect(verifyBlobToken(token)).toBeNull();
});

it("blob and permanent download tokens are not interchangeable", async () => {
await loadFsStorage(root);
const { signBlobToken, signDownload, verifyBlobToken, verifyDownload } =
await import("../downloadTokens");
// A permanent token must not pass the blob verifier (it has no expiry),
// and a blob token must not pass the permanent verifier — the HMACs are
// domain-separated so one capability can't be replayed as the other.
expect(verifyBlobToken(signDownload("p", "f"))).toBeNull();
expect(verifyDownload(signBlobToken("p", "f", 60))).toBeNull();
});
});
57 changes: 57 additions & 0 deletions backend/src/lib/__tests__/storagePresign.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { describe, expect, it, vi } from "vitest";

// These tests intentionally do NOT mock @aws-sdk/s3-request-presigner: the
// behavior under test is which endpoint the real presigner signs against.
// Presigning is pure local crypto (no network), so letting the real SDK
// produce the URL and parsing its host keeps the test honest — a mock would
// happily "sign" against whatever client the code handed it.
//
// PRESIGN_ENDPOINT is captured at module load in ../storage, so each case
// must configure process.env BEFORE importing a fresh copy of the module
// (same reset-then-dynamic-import pattern as storageErrors.test.ts).
async function loadStorage(publicEndpoint?: string) {
vi.resetModules();
process.env.R2_ENDPOINT_URL = "http://storage:9000";
process.env.R2_ACCESS_KEY_ID = "test-access-key";
process.env.R2_SECRET_ACCESS_KEY = "test-secret-key";
process.env.R2_BUCKET_NAME = "mike";
if (publicEndpoint === undefined) {
delete process.env.R2_PUBLIC_ENDPOINT_URL;
} else {
process.env.R2_PUBLIC_ENDPOINT_URL = publicEndpoint;
}
return import("../storage");
}

describe("getSignedUrl presign endpoint split", () => {
it("signs against R2_PUBLIC_ENDPOINT_URL, not the internal endpoint", async () => {
// Self-hosted deploys reach storage at a compose-internal hostname
// (http://storage:9000) that a browser can never resolve. The presigned
// URL is handed to the browser, so it must be signed against the
// host-published endpoint — and an S3 signature is bound to the host it
// was signed for, so this cannot be fixed up after the fact.
const { getSignedUrl } = await loadStorage("http://localhost:9100");

const url = await getSignedUrl("some/key");

expect(url).not.toBeNull();
const parsed = new URL(url!);
expect(parsed.host).toBe("localhost:9100");
expect(parsed.host).not.toBe("storage:9000");
// Still a real path-style presigned GET for the requested object.
expect(parsed.pathname).toBe("/mike/some/key");
expect(parsed.searchParams.get("X-Amz-Signature")).toBeTruthy();
});

it("falls back to the R2_ENDPOINT_URL host when no public endpoint is set", async () => {
// Cloud R2/S3 endpoints are already publicly reachable; without
// R2_PUBLIC_ENDPOINT_URL the presigner must keep using the one
// configured endpoint unchanged.
const { getSignedUrl } = await loadStorage(undefined);

const url = await getSignedUrl("some/key");

expect(url).not.toBeNull();
expect(new URL(url!).host).toBe("storage:9000");
});
});
56 changes: 56 additions & 0 deletions backend/src/lib/downloadTokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,59 @@ export function verifyDownload(
export function buildDownloadUrl(path: string, filename: string): string {
return `/download/${signDownload(path, filename)}`;
}

/**
* Expiring blob tokens — the filesystem storage driver's stand-in for S3
* presigned URLs.
*
* A presigned URL is a capability: whoever holds it can fetch that one object
* until it expires, with no session attached (the browser follows it as a
* plain <a> click, so no Authorization header is available). These tokens
* reproduce exactly that contract: HMAC over {path, filename, exp}, verified
* by the unauthenticated `/download/signed/:token` route. Access control
* happened when the token was minted — the same moment it would have happened
* for a presigned URL.
*
* Unlike `signDownload` above, these DO expire: they replace URLs that always
* carried an expiry, and the routes that mint them re-check access on every
* request, so a short lifetime costs nothing.
*/
export function signBlobToken(
path: string,
filename: string,
expiresInSeconds: number,
): string {
const exp = Math.floor(Date.now() / 1000) + expiresInSeconds;
const payload = JSON.stringify({ p: path, f: filename, e: exp });
const enc = b64urlEncode(Buffer.from(payload, "utf8"));
const sig = crypto
.createHmac("sha256", getSecret())
.update("blob:" + enc)
.digest();
return `${enc}.${b64urlEncode(sig)}`;
}

export function verifyBlobToken(
token: string,
): { path: string; filename: string } | null {
const parts = token.split(".");
if (parts.length !== 2) return null;
const [enc, sigEnc] = parts;
const expected = crypto
.createHmac("sha256", getSecret())
.update("blob:" + enc)
.digest();
if (!timingSafeEqStr(sigEnc, b64urlEncode(expected))) return null;
try {
const parsed = JSON.parse(b64urlDecode(enc).toString("utf8")) as {
p: string;
f: string;
e: number;
};
if (!parsed?.p || !parsed?.f || typeof parsed.e !== "number") return null;
if (parsed.e < Math.floor(Date.now() / 1000)) return null;
return { path: parsed.p, filename: parsed.f };
} catch {
return null;
}
}
Loading
Loading