mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 06:15:39 -04:00
feat: add public-api app and route update checks through it
- Add `apps/public-api` (`@sofa/public-api`) — minimal Hono app
deployable to Vercel; `GET /v1/version` fetches the latest GitHub
release and returns `{ version, releaseUrl }` with a 15-minute
CDN cache (`s-maxage=900, stale-while-revalidate=3600`)
- Update `packages/core/src/update-check.ts` to call
`PUBLIC_API_URL/v1/version` instead of the GitHub API directly;
`PUBLIC_API_URL` defaults to `https://public-api.sofa.watch`
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "@sofa/public-api",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "PORT=3002 bun --watch src/app.ts",
|
||||||
|
"start": "PORT=3002 bun src/app.ts",
|
||||||
|
"lint": "biome check",
|
||||||
|
"format": "biome format --write",
|
||||||
|
"check-types": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "4.12.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "catalog:",
|
||||||
|
"typescript": "catalog:"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Hono } from "hono";
|
||||||
|
import { cors } from "hono/cors";
|
||||||
|
import { getLatestVersion } from "./lib/github";
|
||||||
|
|
||||||
|
const app = new Hono();
|
||||||
|
|
||||||
|
app.use("*", cors());
|
||||||
|
|
||||||
|
app.get("/health", (c) => c.json({ status: "healthy" }));
|
||||||
|
|
||||||
|
app.get("/v1/version", async (c) => {
|
||||||
|
try {
|
||||||
|
const latest = await getLatestVersion();
|
||||||
|
|
||||||
|
c.header(
|
||||||
|
"Cache-Control",
|
||||||
|
"public, s-maxage=900, stale-while-revalidate=3600",
|
||||||
|
);
|
||||||
|
|
||||||
|
return c.json(latest);
|
||||||
|
} catch (e) {
|
||||||
|
return c.json(
|
||||||
|
{ error: e instanceof Error ? e.message : "Failed to fetch version" },
|
||||||
|
502,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
const GITHUB_RELEASES_URL =
|
||||||
|
"https://api.github.com/repos/jakejarvis/sofa/releases/latest";
|
||||||
|
|
||||||
|
export interface VersionInfo {
|
||||||
|
version: string;
|
||||||
|
releaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLatestVersion(): Promise<VersionInfo> {
|
||||||
|
const res = await fetch(GITHUB_RELEASES_URL, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github.v3+json",
|
||||||
|
"User-Agent": "sofa-public-api",
|
||||||
|
...(process.env.GITHUB_TOKEN && {
|
||||||
|
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(10_000),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`GitHub API ${res.status}`);
|
||||||
|
|
||||||
|
const data = (await res.json()) as {
|
||||||
|
tag_name: string;
|
||||||
|
html_url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: data.tag_name.replace(/^v/, ""),
|
||||||
|
releaseUrl: data.html_url,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["esnext"],
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": ["bun"]
|
||||||
|
},
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"bunVersion": "1.x"
|
||||||
|
}
|
||||||
@@ -85,6 +85,17 @@
|
|||||||
"typescript": "catalog:",
|
"typescript": "catalog:",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"apps/public-api": {
|
||||||
|
"name": "@sofa/public-api",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "4.12.7",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "catalog:",
|
||||||
|
"typescript": "catalog:",
|
||||||
|
},
|
||||||
|
},
|
||||||
"apps/server": {
|
"apps/server": {
|
||||||
"name": "@sofa/server",
|
"name": "@sofa/server",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
@@ -997,6 +1008,8 @@
|
|||||||
|
|
||||||
"@sofa/native": ["@sofa/native@workspace:apps/native"],
|
"@sofa/native": ["@sofa/native@workspace:apps/native"],
|
||||||
|
|
||||||
|
"@sofa/public-api": ["@sofa/public-api@workspace:apps/public-api"],
|
||||||
|
|
||||||
"@sofa/server": ["@sofa/server@workspace:apps/server"],
|
"@sofa/server": ["@sofa/server@workspace:apps/server"],
|
||||||
|
|
||||||
"@sofa/tmdb": ["@sofa/tmdb@workspace:packages/tmdb"],
|
"@sofa/tmdb": ["@sofa/tmdb@workspace:packages/tmdb"],
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ const APP_VERSION = process.env.APP_VERSION || "0.0.0";
|
|||||||
|
|
||||||
const log = createLogger("update-check");
|
const log = createLogger("update-check");
|
||||||
|
|
||||||
const GITHUB_RELEASES_URL =
|
const PUBLIC_API_URL =
|
||||||
"https://api.github.com/repos/jakejarvis/sofa/releases/latest";
|
process.env.PUBLIC_API_URL || "https://public-api.sofa.watch";
|
||||||
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
||||||
|
|
||||||
export interface UpdateCheckResult {
|
export interface UpdateCheckResult {
|
||||||
@@ -70,20 +70,17 @@ export async function performUpdateCheck(): Promise<UpdateCheckResult> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(GITHUB_RELEASES_URL, {
|
const res = await fetch(`${PUBLIC_API_URL}/v1/version`, {
|
||||||
headers: {
|
headers: { "User-Agent": "sofa-update-check" },
|
||||||
Accept: "application/vnd.github.v3+json",
|
|
||||||
"User-Agent": "sofa-update-check",
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(10_000),
|
signal: AbortSignal.timeout(10_000),
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error(`GitHub API ${res.status}`);
|
if (!res.ok) throw new Error(`Public API ${res.status}`);
|
||||||
|
|
||||||
const data = (await res.json()) as { tag_name: string; html_url: string };
|
const data = (await res.json()) as { version: string; releaseUrl: string };
|
||||||
const version = data.tag_name.replace(/^v/, "");
|
const version = data.version;
|
||||||
|
|
||||||
setSetting("updateCheckLatestVersion", version);
|
setSetting("updateCheckLatestVersion", version);
|
||||||
setSetting("updateCheckReleaseUrl", data.html_url);
|
setSetting("updateCheckReleaseUrl", data.releaseUrl);
|
||||||
setSetting("updateCheckLastCheckedAt", new Date().toISOString());
|
setSetting("updateCheckLastCheckedAt", new Date().toISOString());
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
@@ -94,7 +91,7 @@ export async function performUpdateCheck(): Promise<UpdateCheckResult> {
|
|||||||
updateAvailable: isNewerVersion(version, APP_VERSION),
|
updateAvailable: isNewerVersion(version, APP_VERSION),
|
||||||
currentVersion: APP_VERSION,
|
currentVersion: APP_VERSION,
|
||||||
latestVersion: version,
|
latestVersion: version,
|
||||||
releaseUrl: data.html_url,
|
releaseUrl: data.releaseUrl,
|
||||||
lastCheckedAt: new Date().toISOString(),
|
lastCheckedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
Reference in New Issue
Block a user