项目文件夹

文件
wehub-resource-sync 98e40dac97
CLI Smoke Test / smoke-test-linux (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-linux (24) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (24) (push) Has been cancelled
Expo App TypeScript typecheck / typecheck (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:49 +08:00

220 行
7.6 KiB
TypeScript

import "reflect-metadata";
// Patch crypto.subtle.importKey to normalize base64 → base64url in JWK data.
// privacy-kit uses standard base64 for Ed25519 JWK keys, but Bun (correctly per spec)
// requires base64url. Node.js is lenient about this, Bun is not.
const origImportKey = crypto.subtle.importKey.bind(crypto.subtle);
crypto.subtle.importKey = function (format: any, keyData: any, algorithm: any, extractable: any, keyUsages: any) {
if (format === 'jwk' && keyData && typeof keyData === 'object') {
const fixed = { ...keyData };
for (const field of ['d', 'x', 'y', 'n', 'e', 'p', 'q', 'dp', 'dq', 'qi', 'k']) {
if (typeof fixed[field] === 'string') {
fixed[field] = fixed[field].replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
}
return origImportKey(format, fixed, algorithm, extractable, keyUsages);
}
return origImportKey(format, keyData, algorithm, extractable, keyUsages);
} as any;
import * as fs from "fs";
import * as path from "path";
import { createPGlite } from "./storage/pgliteLoader";
const dataDir = process.env.DATA_DIR || "./data";
const pgliteDir = process.env.PGLITE_DIR || path.join(dataDir, "pglite");
export async function runMigrations(opts: { pgliteDir: string; migrationsDir?: string } = { pgliteDir }) {
const targetPgliteDir = opts.pgliteDir;
console.log(`Migrating database in ${targetPgliteDir}...`);
fs.mkdirSync(targetPgliteDir, { recursive: true });
const pg = createPGlite(targetPgliteDir);
// Create migrations tracking table
await pg.exec(`
CREATE TABLE IF NOT EXISTS "_prisma_migrations" (
"id" TEXT PRIMARY KEY,
"migration_name" TEXT NOT NULL UNIQUE,
"finished_at" TIMESTAMPTZ,
"started_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
"applied_steps_count" INTEGER NOT NULL DEFAULT 0,
"logs" TEXT
);
`);
// Find migrations directory - explicit arg wins; fall back to defaults.
let migrationsDirResolved = "";
const candidates: string[] = [];
if (opts.migrationsDir) candidates.push(opts.migrationsDir);
candidates.push(
path.join(process.cwd(), "prisma", "migrations"),
path.join(process.cwd(), "packages", "happy-server", "prisma", "migrations"),
path.join(path.dirname(process.execPath), "prisma", "migrations"),
);
for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
migrationsDirResolved = candidate;
break;
}
}
if (!migrationsDirResolved) {
throw new Error(`Could not find prisma/migrations directory. Tried: ${candidates.join(", ")}`);
}
// Get all migration directories sorted
const dirs = fs.readdirSync(migrationsDirResolved)
.filter(d => fs.statSync(path.join(migrationsDirResolved, d)).isDirectory())
.sort();
// Get already applied migrations
const applied = await pg.query<{ migration_name: string }>(
`SELECT "migration_name" FROM "_prisma_migrations" WHERE "finished_at" IS NOT NULL`
);
const appliedSet = new Set(applied.rows.map(r => r.migration_name));
let appliedCount = 0;
for (const dir of dirs) {
if (appliedSet.has(dir)) {
continue;
}
const sqlFile = path.join(migrationsDirResolved, dir, "migration.sql");
if (!fs.existsSync(sqlFile)) {
continue;
}
console.log(` Applying ${dir}...`);
const sql = fs.readFileSync(sqlFile, "utf-8");
try {
await pg.exec(sql);
await pg.query(
`INSERT INTO "_prisma_migrations" ("id", "migration_name", "finished_at", "applied_steps_count") VALUES ($1, $2, now(), 1)`,
[crypto.randomUUID(), dir]
);
appliedCount++;
} catch (e: any) {
throw new Error(`Failed to apply ${dir}: ${e.message}`);
}
}
if (appliedCount === 0) {
console.log("No new migrations to apply.");
} else {
console.log(`Applied ${appliedCount} migration(s).`);
}
await pg.close();
}
async function serve() {
// Ensure DB_PROVIDER is set for db.ts
process.env.DB_PROVIDER = process.env.DB_PROVIDER || "pglite";
process.env.PGLITE_DIR = process.env.PGLITE_DIR || pgliteDir;
const masterSecret = process.env.HANDY_MASTER_SECRET;
if (!masterSecret) {
throw new Error("HANDY_MASTER_SECRET is required");
}
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3005;
const host = process.env.HOST || "0.0.0.0";
const staticDir = findStaticDir();
let injectHtmlConfig: Record<string, unknown> | undefined;
if (process.env.HAPPY_INJECT_HTML_CONFIG) {
try {
injectHtmlConfig = JSON.parse(process.env.HAPPY_INJECT_HTML_CONFIG);
} catch {
// ignore malformed input
}
}
const { startServer } = await import("./index");
await startServer({
pgliteDir: process.env.PGLITE_DIR!,
masterSecret,
port,
host,
staticDir,
injectHtmlConfig,
});
// Block until shutdown so the process stays alive.
const { awaitShutdown } = await import("./utils/shutdown");
await awaitShutdown();
process.exit(0);
}
function findStaticDir(): string | undefined {
const candidates = [
process.env.HAPPY_STATIC_DIR,
path.join(process.cwd(), "webapp"),
path.join(path.dirname(process.execPath), "webapp"),
].filter(Boolean) as string[];
for (const candidate of candidates) {
if (fs.existsSync(path.join(candidate, "index.html"))) {
return candidate;
}
}
return undefined;
}
// CLI — only when this file is invoked directly, not when imported as a library.
const standaloneEntrypoints = new Set([
"standalone.ts",
"standalone.js",
"standalone.mjs",
"standalone.cjs",
"happy-server",
"happy-server.exe",
]);
export function isStandaloneEntrypoint(invokedFile: string): boolean {
// win32.basename splits on both "/" and "\", so a Windows-style argv[1] is
// parsed correctly even on a POSIX host (and vice-versa). The POSIX basename
// would leave backslashes intact and miss Windows entrypoints like
// happy-server.exe when tests or tooling run cross-platform.
return standaloneEntrypoints.has(path.win32.basename(invokedFile).toLowerCase());
}
const invokedFile = process.argv[1] || "";
const isDirectInvocation = isStandaloneEntrypoint(invokedFile);
if (isDirectInvocation) {
const command = process.argv[2];
switch (command) {
case "migrate":
runMigrations({ pgliteDir }).catch(e => {
console.error(e);
process.exit(1);
});
break;
case "serve":
serve().catch(e => {
console.error(e);
process.exit(1);
});
break;
default:
console.log(`happy-server - portable distribution
Usage:
happy-server migrate Apply database migrations
happy-server serve Start the server
Environment variables:
DATA_DIR Base data directory (default: ./data)
PGLITE_DIR PGlite database directory (default: DATA_DIR/pglite)
DATABASE_URL PostgreSQL URL (if set, uses external Postgres instead of PGlite)
REDIS_URL Redis URL (optional, not required for standalone)
PORT Server port (default: 3005)
HANDY_MASTER_SECRET Required: master secret for auth/encryption
`);
process.exit(command === "--help" || command === "-h" ? 0 : 1);
}
}