初回コミット: AltStore PAL向けADPホスティングSaaS

- Fastify API・Node Worker・PostgreSQL・S3互換ストレージ構成
- ADP ZIPのマルチパートアップロードとManifest検証(パストラバーサル・ZIP爆弾等を拒否)
- manifest.json/signatureは再シリアライズせず元バイト列を保持
- Cloudflare風の運用向け管理ダッシュボード(shadcn/Radix・日本語UI・4ルート)
  概要/Sources/アプリ/リリース + ルートランディングページ
- 汎用OIDC SSO・Workspace単位の認可・demo mode
- 匿名配布: /sources/{slug}/source.json, /artifacts/{releaseId}/manifest.json
- Docker Compose対応
This commit is contained in:
amania-jailbreak
2026-08-05 10:09:58 +09:00
commit 93825ebc64
59 changed files with 16065 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
import { createHash, randomBytes } from "node:crypto";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import { createRemoteJWKSet, jwtVerify, SignJWT } from "jose";
import type { Identity } from "../../../packages/core/src/index";
import type { AppConfig } from "./config";
const stateCookie = "altdock_oidc_state";
const verifierCookie = "altdock_oidc_verifier";
const nonceCookie = "altdock_oidc_nonce";
const sessionCookie = "altdock_session";
function base64Url(value: Buffer) {
return value.toString("base64url");
}
function pkceChallenge(verifier: string) {
return createHash("sha256").update(verifier).digest("base64url");
}
export async function createSession(identity: Identity, appConfig: AppConfig) {
return new SignJWT({ ...identity })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("7d")
.sign(new TextEncoder().encode(appConfig.SESSION_SECRET));
}
export async function readSession(token: string | undefined, appConfig: AppConfig): Promise<Identity | null> {
if (!token) return null;
try {
const result = await jwtVerify(token, new TextEncoder().encode(appConfig.SESSION_SECRET), { algorithms: ["HS256"] });
const payload = result.payload;
if (typeof payload.subject !== "string" || typeof payload.email !== "string" || typeof payload.displayName !== "string") return null;
return { subject: payload.subject, email: payload.email, displayName: payload.displayName };
} catch {
return null;
}
}
export async function getIdentity(request: FastifyRequest, appConfig: AppConfig): Promise<Identity | null> {
if (appConfig.AUTH_MODE === "demo") {
const email = String(request.headers["x-demo-user"] || "demo@altdock.local");
return { subject: `demo:${email}`, email, displayName: email.split("@")[0] || "Demo Developer" };
}
return readSession(request.cookies[sessionCookie], appConfig);
}
function setSession(reply: FastifyReply, token: string) {
reply.setCookie(sessionCookie, token, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: 60 * 60 * 24 * 7,
});
}
async function oidcDiscovery(appConfig: AppConfig) {
if (!appConfig.OIDC_ISSUER_URL) throw new Error("OIDC_ISSUER_URL_REQUIRED");
const response = await fetch(`${appConfig.OIDC_ISSUER_URL.replace(/\/$/, "")}/.well-known/openid-configuration`);
if (!response.ok) throw new Error(`OIDC_DISCOVERY_FAILED:${response.status}`);
return response.json() as Promise<{ authorization_endpoint: string; token_endpoint: string; jwks_uri: string; issuer: string }>;
}
export async function registerAuthRoutes(app: FastifyInstance, appConfig: AppConfig) {
app.get("/auth/login", async (_request, reply) => {
if (appConfig.AUTH_MODE === "demo") return reply.redirect(appConfig.WEB_ORIGIN);
if (!appConfig.OIDC_CLIENT_ID || !appConfig.OIDC_REDIRECT_URI) throw new Error("OIDC_CONFIGURATION_REQUIRED");
const provider = await oidcDiscovery(appConfig);
const state = base64Url(randomBytes(24));
const verifier = base64Url(randomBytes(32));
const nonce = base64Url(randomBytes(24));
const params = new URLSearchParams({
client_id: appConfig.OIDC_CLIENT_ID,
redirect_uri: appConfig.OIDC_REDIRECT_URI,
response_type: "code",
scope: "openid profile email",
state,
nonce,
code_challenge: pkceChallenge(verifier),
code_challenge_method: "S256",
});
reply.setCookie(stateCookie, state, { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/auth" });
reply.setCookie(verifierCookie, verifier, { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/auth" });
reply.setCookie(nonceCookie, nonce, { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/auth" });
return reply.redirect(`${provider.authorization_endpoint}?${params.toString()}`);
});
app.get("/auth/callback", async (request, reply) => {
if (appConfig.AUTH_MODE === "demo") return reply.redirect(appConfig.WEB_ORIGIN);
if (!appConfig.OIDC_CLIENT_ID || !appConfig.OIDC_REDIRECT_URI || !appConfig.OIDC_CLIENT_SECRET) throw new Error("OIDC_CONFIGURATION_REQUIRED");
const query = request.query as { code?: string; state?: string; error?: string };
if (query.error) return reply.code(400).send({ error: "OIDC_LOGIN_FAILED", detail: query.error });
if (!query.code || query.state !== request.cookies[stateCookie]) return reply.code(400).send({ error: "OIDC_STATE_INVALID" });
const verifier = request.cookies[verifierCookie];
const nonce = request.cookies[nonceCookie];
if (!verifier || !nonce) return reply.code(400).send({ error: "OIDC_VERIFIER_MISSING" });
const provider = await oidcDiscovery(appConfig);
const tokenResponse = await fetch(provider.token_endpoint, {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ grant_type: "authorization_code", code: query.code, redirect_uri: appConfig.OIDC_REDIRECT_URI, client_id: appConfig.OIDC_CLIENT_ID, client_secret: appConfig.OIDC_CLIENT_SECRET, code_verifier: verifier }),
});
if (!tokenResponse.ok) return reply.code(400).send({ error: "OIDC_TOKEN_EXCHANGE_FAILED" });
const tokens = await tokenResponse.json() as { id_token?: string; access_token?: string };
if (!tokens.id_token) return reply.code(400).send({ error: "OIDC_ID_TOKEN_MISSING" });
const jwks = createRemoteJWKSet(new URL(provider.jwks_uri));
const verified = await jwtVerify(tokens.id_token, jwks, { issuer: provider.issuer, audience: appConfig.OIDC_CLIENT_ID });
const claims = verified.payload;
if (claims.nonce !== nonce) return reply.code(400).send({ error: "OIDC_NONCE_INVALID" });
const subject = String(claims.sub || "");
if (!subject) return reply.code(400).send({ error: "OIDC_SUBJECT_MISSING" });
const email = String(claims.email || `${subject}@oidc.local`);
const displayName = String(claims.name || claims.preferred_username || email.split("@")[0] || "Developer");
setSession(reply, await createSession({ subject, email, displayName }, appConfig));
reply.clearCookie(stateCookie, { path: "/auth" });
reply.clearCookie(verifierCookie, { path: "/auth" });
reply.clearCookie(nonceCookie, { path: "/auth" });
return reply.redirect(appConfig.WEB_ORIGIN);
});
app.get("/auth/logout", async (_request, reply) => {
reply.clearCookie(sessionCookie, { path: "/" });
return reply.redirect(appConfig.WEB_ORIGIN);
});
}