mirror of
https://github.com/Twigpine/openlaunch.git
synced 2026-11-10 13:11:33 -10:00
- Metadata registration is INSERT-only for unsigned callers: the creator registers before broadcasting (salt still private); replaying the public launch params returns 409 and cannot overwrite image / description / website. Later changes go through the wallet-signed edit flow. (review finding 1) - The on-chain metadataURI is keyed by a client-chosen meta_key instead of the salt, so findSalt's result stays valid for USDG / stock quotes. Proven on the live Robinhood factory: 17/100 deterministic USDG launches reverted with the old scheme, 0/100 with the new one. (finding 2) - Swap and fee ingestion commit the event row and the launch totals in one transaction; a rebuild script recomputes totals from raw events if ever needed (production checked: no drift). (finding 3) - Fresh checkout: npm run migrate reads DATABASE_URL from .env.local/.env; fly-secrets.sh is self-contained (FLY_APP, SECRETS_ENV); README run + deploy steps corrected. - CI workflow (lint, typecheck, unit tests, production build; forge build + unit tests), CONTRIBUTING.md, SECURITY.md with private reporting.
80 lines
3.4 KiB
JavaScript
80 lines
3.4 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Apply db/schema.sql to DATABASE_URL inside ONE transaction. The file is
|
|
* idempotent (IF NOT EXISTS / CREATE OR REPLACE / ADD COLUMN IF NOT EXISTS), so
|
|
* this runs on every deploy as the Fly release_command (fly.toml) and locally
|
|
* after `docker run … postgres:16`. Records one bb_migrations row whenever the
|
|
* sha256 of the file differs from the last applied one.
|
|
*
|
|
* The whole file is sent as a single simple-protocol query (`sql.unsafe` with
|
|
* no parameters) — Postgres splits the statements itself, so `$$ … $$` function
|
|
* bodies never need client-side parsing. Any error rolls everything back and
|
|
* exits 1 (Fly then aborts the deploy).
|
|
*
|
|
* DATABASE_URL=postgres://… node scripts/migrate.mjs
|
|
*
|
|
* Only depends on `postgres` + node builtins so it runs from the standalone
|
|
* image (Dockerfile copies db/ + this file; next.config.ts keeps `postgres`
|
|
* external so it is traced into .next/standalone/node_modules).
|
|
*/
|
|
import { readFile } from "node:fs/promises";
|
|
import { createHash } from "node:crypto";
|
|
import { hostname } from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import postgres from "postgres";
|
|
|
|
// Fresh checkouts: pick DATABASE_URL up from .env.local / .env (never overriding a real env var).
|
|
if (!process.env.DATABASE_URL) {
|
|
const { readFileSync, existsSync } = await import("node:fs");
|
|
for (const f of [".env.local", ".env"]) {
|
|
if (!existsSync(f)) continue;
|
|
const m = readFileSync(f, "utf8").match(/^\s*DATABASE_URL\s*=\s*"?([^"\n#]+)"?/m);
|
|
if (m) {
|
|
process.env.DATABASE_URL = m[1].trim();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
const url = process.env.DATABASE_URL?.trim();
|
|
if (!url) {
|
|
console.error("migrate: DATABASE_URL is not set");
|
|
process.exit(2);
|
|
}
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const file = process.env.BASEBID_SCHEMA_FILE || path.resolve(here, "../db/schema.sql");
|
|
const text = await readFile(file, "utf8");
|
|
const hash = createHash("sha256").update(text).digest("hex");
|
|
const appliedBy = process.env.FLY_IMAGE_REF || process.env.FLY_MACHINE_ID || hostname();
|
|
|
|
const sql = postgres(url, { max: 1, connect_timeout: 15, idle_timeout: 5, onnotice: () => {} });
|
|
const t0 = Date.now();
|
|
let exitCode = 0;
|
|
try {
|
|
const { changed, history, tables } = await sql.begin(async (tx) => {
|
|
await tx.unsafe(text);
|
|
const [last] = await tx`select schema_sha256 from bb_migrations order by id desc limit 1`;
|
|
const changed = last?.schema_sha256 !== hash;
|
|
if (changed) {
|
|
await tx`insert into bb_migrations (schema_sha256, applied_by) values (${hash}, ${appliedBy})`;
|
|
}
|
|
const [{ history }] = await tx`select count(*)::int as history from bb_migrations`;
|
|
const [{ tables }] = await tx`
|
|
select count(*)::int as tables from information_schema.tables
|
|
where table_schema = current_schema() and table_name like 'bb\\_%'`;
|
|
return { changed, history, tables };
|
|
});
|
|
console.log(
|
|
`migrate: ok schema=${hash.slice(0, 12)} ${changed ? "applied (new hash)" : "re-applied (unchanged)"} ` +
|
|
`tables=${tables} history=${history} ${Date.now() - t0}ms`,
|
|
);
|
|
} catch (err) {
|
|
const e = /** @type {{ message?: string; position?: string; code?: string }} */ (err);
|
|
console.error(`migrate: FAILED ${e?.code ?? ""} ${e?.message ?? err}${e?.position ? ` (at char ${e.position})` : ""}`);
|
|
exitCode = 1;
|
|
} finally {
|
|
await sql.end({ timeout: 5 }).catch(() => {});
|
|
}
|
|
process.exit(exitCode);
|