Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
4f164dd
Fix SSR build on Vercel
kojibai Jan 26, 2026
c42f346
Merge pull request #280 from kojibai/codex/fix-ssr-function-error-on-…
kojibai Jan 26, 2026
3599610
Fix navigator credentials access in WebAuthn
kojibai Jan 26, 2026
a33071d
Merge pull request #281 from kojibai/codex/fix-typescript-errors-rega…
kojibai Jan 26, 2026
6048e90
Harden SSR fallback and debug output
kojibai Jan 26, 2026
6203234
Merge pull request #282 from kojibai/codex/fix-ssr-issues-after-updates
kojibai Jan 26, 2026
5b290e9
Keep SSR debug output visible
kojibai Jan 26, 2026
ade6fe6
Merge pull request #283 from kojibai/codex/fix-ssr-to-display-debug-m…
kojibai Jan 26, 2026
f5aaf24
Fix SSR entry bundle resolution
kojibai Jan 26, 2026
94b0213
Merge pull request #284 from kojibai/codex/fix-module-not-found-error…
kojibai Jan 26, 2026
1c90d5c
Fix SSR bundle lookup for Vercel
kojibai Jan 26, 2026
b7fb7d2
Merge pull request #285 from kojibai/codex/fix-ssr-entry-server-bundl…
kojibai Jan 26, 2026
c0cf3aa
Fix SSR build output cleanup
kojibai Jan 26, 2026
1d06e0d
Merge pull request #286 from kojibai/codex/fix-ssr-not-found-in-dist/…
kojibai Jan 26, 2026
1721dcb
Preserve SSR server outputs during client build
kojibai Jan 26, 2026
702ae43
Merge pull request #287 from kojibai/codex/fix-ssr-entry-server-bundl…
kojibai Jan 26, 2026
22856c1
Avoid warm-up 404s for shell routes
kojibai Jan 26, 2026
2f68062
Merge pull request #288 from kojibai/codex/investigate-404-resource-l…
kojibai Jan 26, 2026
f2e159b
Fix owner signer verification flow
kojibai Jan 26, 2026
e7eba4f
Fix WebAuthn signer verification flow
kojibai Jan 26, 2026
abd9107
Fix ownership attestation status for origin glyphs
kojibai Jan 26, 2026
433ec19
Fix KAS badge and provenance display for receive glyphs
kojibai Jan 26, 2026
b6cad10
Fix audit auth signer for receive glyphs
kojibai Jan 26, 2026
d8e87f8
Merge pull request #294 from kojibai/codex/verify-receive-glyph-auth-…
kojibai Jan 26, 2026
2124add
update verifypage
kojibai Jan 26, 2026
ab7fbf7
Fix owner auth phiKey handling and receive guards
kojibai Jan 26, 2026
b7e6a5a
Fix receive owner auth phiKey and shared receipt results
kojibai Jan 26, 2026
cc6d171
Use receipt capsule phiKey for embedded raw value
kojibai Jan 26, 2026
a365cf5
Refine verify-only audit flow
kojibai Jan 26, 2026
8a01257
Adjust verify audit provenance display
kojibai Jan 26, 2026
39d506b
Show provenance verified for origin
kojibai Jan 26, 2026
2ff6d74
Update sovereign Word choice
kojibai Jan 26, 2026
b9413e4
Merge pull request #298 from kojibai/codex/implement-verifypage-chang…
kojibai Jan 26, 2026
021002e
Merge pull request #297 from kojibai/codex/implement-four-fixes-in-ve…
kojibai Jan 26, 2026
e99cb83
Merge pull request #293 from kojibai/codex/fix-signer-mismatch-logic-…
kojibai Jan 26, 2026
f9ae845
Merge pull request #292 from kojibai/codex/implement-verifypage-auth-…
kojibai Jan 26, 2026
cd304d9
Hide KAS UI when signature missing
kojibai Jan 26, 2026
c825886
Fix KAS guards typing
kojibai Jan 26, 2026
6822905
Show receive KAS verification without owner sig
kojibai Jan 26, 2026
77aa393
Merge pull request #300 from kojibai/codex/add-conditional-kas-ui-ren…
kojibai Jan 26, 2026
60304fd
v42.1.0
kojibai Jan 26, 2026
ca1ddb5
Enhance verified card export with PNG metadata
kojibai Jan 26, 2026
b4820f5
Enhance verified card PNG export and receipt PNG import (embedded pro…
kojibai Jan 26, 2026
a62ce1b
Enhance verified card PNG export and receipt PNG import (embedded pro…
kojibai Jan 26, 2026
42e6393
Enhance verified card PNG export and receipt PNG import (embedded pro…
kojibai Jan 26, 2026
b92b952
Merge pull request #301 from kojibai/codex/upgrade-save-verified-card…
kojibai Jan 26, 2026
ba66814
Update Verifypage
kojibai Jan 26, 2026
51b83c6
Update verified card seal and QR layout
kojibai Jan 26, 2026
7e89a9e
Refine verified card seal layout and gradients
kojibai Jan 26, 2026
895caca
Tweak verified card badge spacing
kojibai Jan 26, 2026
5731867
Adjust badge sizing and seal seeding
kojibai Jan 26, 2026
102015f
Merge pull request #303 from kojibai/codex/implement-upgrades-to-rece…
kojibai Jan 26, 2026
965b70a
Polish verified card layout
kojibai Jan 26, 2026
d74c7bb
Merge pull request #304 from kojibai/codex/finalize-verified-image-ex…
kojibai Jan 26, 2026
bc65d37
v42.2.0
kojibai Jan 26, 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
144 changes: 115 additions & 29 deletions api/ssr.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// api/ssr.ts
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import { PassThrough } from "node:stream";
import { randomUUID } from "node:crypto";
import type { IncomingMessage, ServerResponse } from "node:http";
import type { PipeableStream } from "react-dom/server";

Expand Down Expand Up @@ -31,6 +32,8 @@ type ManifestEntry = {
src?: string;
};

const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");

function pickRender(mod: unknown): RenderFn | null {
if (!mod || typeof mod !== "object") return null;
const m = mod as SsrModule;
Expand All @@ -49,14 +52,15 @@ function toPublicPath(file?: string): string | null {

function buildSsrHead(): string {
const parts: string[] = [];
const manifestPath = path.join(process.cwd(), "dist", "client", ".vite", "manifest.json");
const manifestPaths = [
path.join(ROOT_DIR, "dist", ".vite", "manifest.json"),
path.join(ROOT_DIR, "dist", "client", ".vite", "manifest.json"),
];

try {
if (fs.existsSync(manifestPath)) {
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as Record<
string,
ManifestEntry
>;
const manifestPath = manifestPaths.find((candidate) => fs.existsSync(candidate));
if (manifestPath) {
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as Record<string, ManifestEntry>;
const entryKey =
Object.keys(manifest).find((key) => manifest[key]?.src?.includes("entry-client")) ??
Object.keys(manifest).find((key) => manifest[key]?.isEntry);
Expand Down Expand Up @@ -127,28 +131,107 @@ function formatErrorForDebug(err: unknown): string {
}
}

export default async function handler(req: IncomingMessage, res: ServerResponse) {
const ABORT_DELAY_MS = 10_000;
type TemplateParts = { head: string; tail: string };

try {
const url = absoluteUrl(req);
const DEBUG = url.searchParams.has("__ssr_debug");
function minimalTemplate(): TemplateParts {
return {
head:
"<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"></head><body><div id=\"root\">",
tail: "</div></body></html>",
};
}

const templatePath = path.join(process.cwd(), "dist", "server", "template.html");
function loadTemplate(): TemplateParts {
const templatePath = path.join(ROOT_DIR, "dist", "server", "template.html");
try {
const template = fs.readFileSync(templatePath, "utf8");
const templateWithHead = template.replace("<!--ssr-head-->", buildSsrHead());
const { head, tail } = splitTemplate(templateWithHead);
return splitTemplate(templateWithHead);
} catch (err) {
console.error("SSR template load failed:", err);
return minimalTemplate();
}
}

function resolveEntryServerPath(): string | null {
const serverDir = path.join(ROOT_DIR, "dist", "server");
const candidates = ["entry-server.js", "entry-server.mjs", "entry-server.cjs"];

for (const name of candidates) {
const candidate = path.join(serverDir, name);
if (fs.existsSync(candidate)) return candidate;
}

try {
const files = fs.readdirSync(serverDir);
const match = files.find((file) => /^entry-server\.(mjs|cjs|js)$/i.test(file));
return match ? path.join(serverDir, match) : null;
} catch {
return null;
}
}

function respondWithShell(
res: ServerResponse,
parts: TemplateParts,
requestId: string,
opts: { debug: boolean; error?: unknown }
) {
if (!res.headersSent) {
res.statusCode = 200;
res.setHeader("content-type", "text/html; charset=utf-8");
res.setHeader("cache-control", "no-store");
res.setHeader("x-ssr", "0");
res.setHeader("x-ssr-request-id", requestId);
if (opts.debug) {
res.setHeader("x-ssr-debug", "1");
}
}

const entryPath = path.join(process.cwd(), "dist", "server", "entry-server.js");
if (opts.debug && opts.error) {
const payload = escapeHtml(formatErrorForDebug(opts.error));
const debugTemplate = minimalTemplate();
const style =
"<style>body{font-family:ui-monospace,Menlo,Monaco,Consolas,monospace;padding:24px;background:#0b0b0c;color:#f2f2f2;}pre{white-space:pre-wrap;word-break:break-word;background:#151519;border:1px solid #2a2a33;padding:16px;border-radius:8px;}</style>";
res.end(`${debugTemplate.head}${style}<pre>${payload}</pre>${debugTemplate.tail}`);
return;
}

res.end(parts.head + parts.tail);
}

export default async function handler(req: IncomingMessage, res: ServerResponse) {
const ABORT_DELAY_MS = 10_000;
const requestId = randomUUID();

try {
const url = absoluteUrl(req);
const DEBUG = url.searchParams.has("__ssr_debug") || req.headers["x-ssr-debug"] === "1";
const templateParts = loadTemplate();
const { head, tail } = templateParts;

const entryPath = resolveEntryServerPath();
if (!entryPath) {
const err = new Error("SSR entry-server bundle not found in dist/server");
console.error(`[SSR ${requestId}] entry not found`);
respondWithShell(res, templateParts, requestId, { debug: DEBUG, error: err });
return;
}
const entryUrl = pathToFileURL(entryPath).href;

const mod = (await import(entryUrl)) as unknown;
const render = pickRender(mod);
let render: RenderFn | null = null;
try {
const mod = (await import(entryUrl)) as unknown;
render = pickRender(mod);
} catch (err) {
console.error(`[SSR ${requestId}] entry import failed:`, err);
respondWithShell(res, templateParts, requestId, { debug: DEBUG, error: err });
return;
}

if (!render) {
res.statusCode = 500;
res.setHeader("content-type", "text/plain; charset=utf-8");
res.end("SSR entry missing render() export");
console.error(`[SSR ${requestId}] entry missing render() export`);
respondWithShell(res, templateParts, requestId, { debug: DEBUG });
return;
}

Expand All @@ -158,6 +241,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
res.setHeader("content-type", "text/html; charset=utf-8");
res.setHeader("cache-control", "no-store");
res.setHeader("x-ssr", "1");
res.setHeader("x-ssr-request-id", requestId);

let shellFlushed = false;
let pipeable: PipeableStream | null = null;
Expand Down Expand Up @@ -213,10 +297,8 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
// If shell failed before onShellReady, we still want to return an HTML document.
// Debug mode: show error details in the browser.
if (!shellFlushed) {
if (DEBUG) {
if (!res.writableEnded) res.end(`<pre>${escapeHtml(formatErrorForDebug(err))}</pre>`);
} else {
if (!res.writableEnded) res.end(head + tail);
if (!res.writableEnded) {
respondWithShell(res, templateParts, requestId, { debug: DEBUG, error: err });
}
return;
}
Expand All @@ -233,12 +315,16 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
};

// Start React SSR stream
pipeable = render(url.pathname + url.search, null, opts);
try {
pipeable = render(url.pathname + url.search, null, opts);
} catch (err) {
console.error(`[SSR ${requestId}] render crashed:`, err);
respondWithShell(res, templateParts, requestId, { debug: DEBUG, error: err });
}
} catch (err) {
console.error("SSR function crashed:", err);
res.statusCode = 500;
res.setHeader("content-type", "text/plain; charset=utf-8");
res.end("SSR function crashed");
console.error(`[SSR ${requestId}] function crashed:`, err);
const DEBUG = req.headers["x-ssr-debug"] === "1";
respondWithShell(res, loadTemplate(), requestId, { debug: DEBUG, error: err });
}
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"dev:ssr": "NODE_ENV=development node server.mjs",
"build:clean": "rm -rf dist .vite && pnpm run build:ssr",
"build": "tsc -b && vite build",
"build:ssr": "tsc -b && vite build && vite build --ssr src/entry-server.tsx --outDir dist/server && vite build --ssr src/entry-server-exports.ts --outDir dist/server && node scripts/ssr-pack.mjs",
"build:ssr": "rm -rf dist/server && tsc -b && vite build && vite build --ssr src/entry-server.tsx --outDir dist/server && vite build --ssr src/entry-server-exports.ts --outDir dist/server && node scripts/ssr-pack.mjs",
"lint": "eslint .",
"preview": "NODE_ENV=production node server.mjs",
"test": "node --test"
Expand Down
9 changes: 8 additions & 1 deletion public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

// Update this version string manually to keep the app + cache versions in sync.
// The value is forwarded to the UI via the service worker "SW_ACTIVATED" message.
const APP_VERSION = "42.0.0"; // update on release
const APP_VERSION = "42.2.0"; // update on release
const VERSION = new URL(self.location.href).searchParams.get("v") || APP_VERSION; // derived from build
const PREFIX = "PHINETWORK";

Expand Down Expand Up @@ -168,6 +168,13 @@ async function warmUrls(urls, { mapShell = false } = {}) {
const url = normalizeWarmUrl(raw);
if (!url) return;

const hasExtension = url.pathname.split("/").pop()?.includes(".");

if (mapShell && shell && !hasExtension) {
await mapShellToRoute(url.href, shell);
return;
}

const cacheName = cacheBucketFor(url);
const req = new Request(url.href, { cache: "reload" });

Expand Down
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1219,7 +1219,7 @@ export function AppChrome(): React.JSX.Element {
});

await Promise.all(
[...OFFLINE_ASSETS_TO_WARM, ...SHELL_ROUTES_TO_WARM].map(async (url) => {
[...OFFLINE_ASSETS_TO_WARM, ...APP_SHELL_HINTS].map(async (url) => {
try {
await fetch(url, { cache: "no-cache", signal: aborter.signal });
} catch {
Expand Down
Loading