@guard-rail/core 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -36
- package/dist/adapters/supabase/auth.js +1 -2
- package/dist/adapters/supabase/auth.js.map +1 -1
- package/dist/adapters/supabase/client.js +6 -1
- package/dist/adapters/supabase/client.js.map +1 -1
- package/dist/adapters/supabase/confirm.d.ts +11 -11
- package/dist/adapters/supabase/confirm.js +14 -18
- package/dist/adapters/supabase/confirm.js.map +1 -1
- package/dist/adapters/supabase/index.d.ts +3 -1
- package/dist/adapters/supabase/index.js +5 -0
- package/dist/adapters/supabase/index.js.map +1 -1
- package/dist/adapters/supabase/rateLimit.d.ts +24 -0
- package/dist/adapters/supabase/rateLimit.js +37 -0
- package/dist/adapters/supabase/rateLimit.js.map +1 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +1046 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/init-db.js +23 -5
- package/dist/cli/init-db.js.map +1 -1
- package/dist/core/adapter-types.d.ts +71 -1
- package/dist/core/config.d.ts +12 -1
- package/dist/core/config.js +29 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/csp.d.ts +32 -0
- package/dist/core/csp.js +26 -0
- package/dist/core/csp.js.map +1 -0
- package/dist/core/validate.d.ts +47 -0
- package/dist/core/validate.js +118 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/index.client.d.ts +2 -1
- package/dist/index.client.js +3 -0
- package/dist/index.client.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/react/Admin.d.ts +5 -3
- package/dist/react/Admin.js +29 -41
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/AuthDrawer.js +22 -34
- package/dist/react/AuthDrawer.js.map +1 -1
- package/dist/react/AuthRedirectSync.js +28 -22
- package/dist/react/AuthRedirectSync.js.map +1 -1
- package/dist/react/GRail/client.js +27 -47
- package/dist/react/GRail/client.js.map +1 -1
- package/dist/react/GRail/render.js +3 -2
- package/dist/react/GRail/render.js.map +1 -1
- package/dist/react/MediaPicker.js +12 -16
- package/dist/react/MediaPicker.js.map +1 -1
- package/dist/react/Repeater/client.js +12 -13
- package/dist/react/Repeater/client.js.map +1 -1
- package/dist/react/Team.d.ts +1 -7
- package/dist/react/Team.js +21 -46
- package/dist/react/Team.js.map +1 -1
- package/dist/react/guardrail.css +563 -0
- package/dist/server/actions.js +52 -31
- package/dist/server/actions.js.map +1 -1
- package/dist/server/loader.js +4 -4
- package/dist/server/loader.js.map +1 -1
- package/dist/server/rateLimit.d.ts +21 -0
- package/dist/server/rateLimit.js +94 -0
- package/dist/server/rateLimit.js.map +1 -0
- package/dist/server/repeaterActions.js +9 -1
- package/dist/server/repeaterActions.js.map +1 -1
- package/dist/server/wrapper.js +18 -1
- package/dist/server/wrapper.js.map +1 -1
- package/package.json +11 -9
- package/supabase/admin/check-grants.sql +41 -0
- package/supabase/migrations/20261028000000_require_confirmed_email.sql +53 -0
- package/supabase/migrations/20261028010000_rate_limits.sql +67 -0
- package/supabase/migrations/20261028020000_member_and_bucket_guards.sql +96 -0
- package/dist/react/styles.d.ts +0 -504
- package/dist/react/styles.js +0 -500
- package/dist/react/styles.js.map +0 -1
- package/scripts/postinstall.mjs +0 -48
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/cli/doctor.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * guardrail-doctor — checks that a project is set up the way this version of\n * Guardrail expects, and (with --fix) repairs what can be repaired locally.\n *\n * It ships inside @guard-rail/core on purpose, so the checks always match the\n * installed version: when a release adds a requirement (a migration, an\n * environment variable, a header), the same release adds the check for it here.\n *\n * Three layers, each opt-in beyond the first:\n * 1. Static + read-only (default): project files, environment, the database's\n * shape and the Supabase project's sign-up settings. Changes nothing.\n * 2. --app-url: asks a running copy of the site for its response headers.\n * 3. --e2e: creates a throwaway user and rows on the real database, exercises\n * sign-in, invite claiming, row-level security, the admin guard, rate\n * limiting and storage the way a signed-in editor would, then deletes it\n * all. Never runs without saying what it will touch.\n *\n * Standalone like the other CLIs: plain Node, local imports end in \".js\", and\n * nothing here imports React or Next.\n */\nimport { spawnSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport {\n existsSync,\n mkdirSync,\n copyFileSync,\n readdirSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport path from \"node:path\";\nimport { createRequire } from \"node:module\";\nimport { createInterface } from \"node:readline/promises\";\nimport { fileURLToPath } from \"node:url\";\nimport { GR_SCHEMA, GR_MEMBERS_TABLE } from \"../core/defs.js\";\nimport { cspString, guardrailCspDirectives } from \"../core/csp.js\";\n\n/* ------------------------------------------------------------ constants --- */\n\n/**\n * Oldest Next.js release known to carry the fixes for the advisories published\n * so far, by major line. Only the offline fallback: normally the doctor asks the\n * npm registry's advisory service about the installed version, which is always\n * current. Update alongside the dependency bumps; a major not listed gets a\n * \"check for security releases\" note instead of a verdict.\n */\nconst NEXT_MIN_PATCHED: Record<number, [number, number, number]> = {\n 15: [15, 5, 28],\n 16: [16, 4, 1],\n};\n\n/**\n * Kept in step with the block create-guardrail writes into a new project's\n * next.config.ts — the same headers, so a scaffolded and a repaired site match.\n */\nconst SECURITY_HEADERS_BLOCK = ` // Baseline response headers. frame-ancestors keeps another site from framing\n // this one (the editing toolbar included) in a clickjacking overlay. A full\n // Content-Security-Policy is specific to what your pages load, so add yours\n // alongside it.\n async headers() {\n return [\n {\n source: \"/:path*\",\n headers: [\n { key: \"Content-Security-Policy\", value: \"frame-ancestors 'self'\" },\n { key: \"X-Frame-Options\", value: \"SAMEORIGIN\" },\n { key: \"X-Content-Type-Options\", value: \"nosniff\" },\n { key: \"Referrer-Policy\", value: \"strict-origin-when-cross-origin\" },\n { key: \"Permissions-Policy\", value: \"camera=(), microphone=(), geolocation=()\" },\n // Only honored over HTTPS; browsers ignore it on plain HTTP.\n { key: \"Strict-Transport-Security\", value: \"max-age=63072000; includeSubDomains\" },\n ],\n },\n ];\n },\n`;\n\nconst SITE_URL_ENV_NOTE = `\n# The site's public address. Every emailed invite/reset link points here, and in\n# production nothing is sent without it (links are never built from the request).\n# GUARDRAIL_SITE_URL=https://example.com\n`;\n\nconst USAGE = `Usage:\n guardrail-doctor [options]\n\nChecks this project's Guardrail setup. Read-only unless you ask otherwise.\n\nOptions:\n --fix Repair what can be repaired in local files: copy missing\n migrations, add a GUARDRAIL_SITE_URL note to your env\n files, add baseline security headers to next.config.\n Never touches secrets or Supabase dashboard settings.\n --push With --fix: also run \\`supabase db push\\` after copying.\n --app-url=<url> Also check the response headers of a running copy of the\n site (e.g. --app-url=http://localhost:3000).\n --e2e Run the end-to-end suite against the database: creates a\n throwaway user and rows, exercises sign-in, invite claims,\n row-level security, the admin guard, rate limiting and\n storage, then deletes everything it made.\n --yes Don't ask before --e2e touches a non-local database.\n --offline Don't contact the npm registry. By default the installed\n versions of next, @supabase/* and @guard-rail/core are\n checked against its published security advisories (only\n package names and versions are sent, as \\`pnpm audit\\` does).\n --csp Print a starter Content-Security-Policy-Report-Only header\n for this site (built from SUPABASE_URL) and exit. Add it\n report-only first, watch the reports, then enforce.\n --production Treat production-only requirements (like\n GUARDRAIL_SITE_URL) as failures rather than warnings.\n --strict Exit non-zero on warnings too.\n --json Print results as JSON.\n -h, --help This text.\n\nReads SUPABASE_URL, SUPABASE_ANON_KEY and SUPABASE_SERVICE_ROLE_KEY (or\nSERVICE_KEY) from the environment, then .env.local and .env.`;\n\n/* -------------------------------------------------------------- results --- */\n\ntype Status = \"pass\" | \"warn\" | \"fail\" | \"skip\" | \"info\";\n\ninterface Result {\n group: string;\n status: Status;\n title: string;\n detail?: string;\n hint?: string;\n}\n\nconst results: Result[] = [];\nlet currentGroup = \"\";\n\nfunction group(name: string): void {\n currentGroup = name;\n}\n\nfunction record(status: Status, title: string, detail?: string, hint?: string): void {\n results.push({ group: currentGroup, status, title, ...(detail ? { detail } : {}), ...(hint ? { hint } : {}) });\n}\n\nconst pass = (t: string, d?: string) => record(\"pass\", t, d);\nconst warn = (t: string, d?: string, h?: string) => record(\"warn\", t, d, h);\nconst fail = (t: string, d?: string, h?: string) => record(\"fail\", t, d, h);\nconst skip = (t: string, d?: string) => record(\"skip\", t, d);\nconst info = (t: string, d?: string) => record(\"info\", t, d);\n\n/* ------------------------------------------------------------- helpers --- */\n\nfunction parseFlags(argv: string[]) {\n const flags = new Set<string>();\n const values: Record<string, string> = {};\n const unknown: string[] = [];\n for (const arg of argv) {\n const m = arg.match(/^--([^=]+)=([\\s\\S]*)$/);\n if (m) {\n values[m[1]!] = m[2]!;\n } else if (arg.startsWith(\"-\")) {\n flags.add(arg.replace(/^-+/, \"\"));\n } else {\n unknown.push(arg);\n }\n }\n const known = new Set([\"fix\", \"push\", \"e2e\", \"yes\", \"production\", \"strict\", \"json\", \"offline\", \"csp\", \"help\", \"h\"]);\n for (const f of flags) if (!known.has(f)) unknown.push(`--${f}`);\n for (const k of Object.keys(values)) if (k !== \"app-url\") unknown.push(`--${k}`);\n return { flags, values, unknown };\n}\n\nfunction readEnvFiles(cwd: string): Record<string, string> {\n const out: Record<string, string> = {};\n // .env wins last-in, so read it first: .env.local overrides it, as in Next.\n for (const file of [\".env\", \".env.local\"]) {\n const p = path.join(cwd, file);\n if (!existsSync(p)) continue;\n for (const line of readFileSync(p, \"utf8\").split(/\\r?\\n/)) {\n const m = line.match(/^\\s*(?:export\\s+)?([A-Za-z_][A-Za-z0-9_]*)\\s*=\\s*(.*)$/);\n if (!m) continue;\n const value = m[2]!.trim().replace(/^(['\"])(.*)\\1$/, \"$2\");\n if (value) out[m[1]!] = value;\n }\n }\n return out;\n}\n\nfunction parseVersion(v: string): [number, number, number] | null {\n const m = v.match(/^(\\d+)\\.(\\d+)\\.(\\d+)/);\n return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;\n}\n\nfunction atLeast(v: [number, number, number], min: [number, number, number]): boolean {\n for (let i = 0; i < 3; i++) {\n if (v[i]! !== min[i]!) return v[i]! > min[i]!;\n }\n return true;\n}\n\n/** Decodes a JWT's payload without verifying it — just to read which role a key claims. */\nfunction jwtRole(key: string): string | null {\n const parts = key.split(\".\");\n if (parts.length !== 3) return null;\n try {\n const payload = JSON.parse(Buffer.from(parts[1]!, \"base64url\").toString(\"utf8\"));\n return typeof payload.role === \"string\" ? payload.role : null;\n } catch {\n return null;\n }\n}\n\nfunction isLocalHost(host: string): boolean {\n return host === \"localhost\" || host === \"127.0.0.1\" || host === \"::1\" || host === \"[::1]\" || host.endsWith(\".localhost\");\n}\n\nfunction commandExists(cmd: string): boolean {\n const res = process.platform === \"win32\" ? spawnSync(\"where\", [cmd]) : spawnSync(\"which\", [cmd]);\n return res.status === 0;\n}\n\nfunction findNextConfig(cwd: string): string | null {\n for (const name of [\"next.config.ts\", \"next.config.mjs\", \"next.config.js\", \"next.config.cjs\", \"next.config.mts\"]) {\n const p = path.join(cwd, name);\n if (existsSync(p)) return p;\n }\n return null;\n}\n\nfunction bundledMigrationsDir(): string {\n const here = path.dirname(fileURLToPath(import.meta.url));\n return path.join(here, \"..\", \"..\", \"supabase\", \"migrations\");\n}\n\n/* ----------------------------------------------------------------- fixes --- */\n\nconst applied: string[] = [];\n\nfunction fixMigrations(cwd: string): void {\n const src = bundledMigrationsDir();\n if (!existsSync(src)) return;\n const dest = path.join(cwd, \"supabase\", \"migrations\");\n mkdirSync(dest, { recursive: true });\n const have = new Set(readdirSync(dest));\n for (const file of readdirSync(src)) {\n if (have.has(file)) continue;\n copyFileSync(path.join(src, file), path.join(dest, file));\n applied.push(`Copied supabase/migrations/${file}`);\n }\n}\n\nfunction fixEnvNotes(cwd: string): void {\n for (const file of [\".env.example\", \".env.local\"]) {\n const p = path.join(cwd, file);\n if (!existsSync(p)) continue;\n const content = readFileSync(p, \"utf8\");\n if (/GUARDRAIL_SITE_URL/.test(content)) continue;\n writeFileSync(p, content.replace(/\\n*$/, \"\\n\") + SITE_URL_ENV_NOTE);\n applied.push(`Added a GUARDRAIL_SITE_URL note to ${file}`);\n }\n}\n\nconst CACHE_COMPONENTS_LINE = /^[ \\t]*(?:cacheComponents|partialPrefetching)\\s*:\\s*true\\s*,?[ \\t]*\\r?\\n/gm;\n\n/** Removes Cache Components from next.config. Returns true if it changed the file. */\nfunction fixCacheComponents(cwd: string): boolean {\n const configPath = findNextConfig(cwd);\n if (!configPath) return false;\n const content = readFileSync(configPath, \"utf8\");\n const next = content.replace(CACHE_COMPONENTS_LINE, \"\");\n if (next === content) return false;\n writeFileSync(configPath, next);\n applied.push(`Turned off Cache Components in ${path.basename(configPath)}`);\n return true;\n}\n\n/** Inserts the baseline headers into the config object. Returns false if the file isn't a shape it can patch. */\nfunction fixHeaders(cwd: string): \"patched\" | \"present\" | \"manual\" | \"none\" {\n const configPath = findNextConfig(cwd);\n if (!configPath) return \"none\";\n const content = readFileSync(configPath, \"utf8\");\n if (/frame-ancestors|x-frame-options/i.test(content)) return \"present\";\n // An existing headers() can't be merged into safely from here.\n if (/\\bheaders\\s*\\(/.test(content)) return \"manual\";\n\n const openers = [\n /(const\\s+nextConfig\\s*(?::\\s*[A-Za-z<>[\\]| ]+)?=\\s*\\{[ \\t]*\\r?\\n)/,\n /(export\\s+default\\s*\\{[ \\t]*\\r?\\n)/,\n /(module\\.exports\\s*=\\s*\\{[ \\t]*\\r?\\n)/,\n ];\n for (const re of openers) {\n if (!re.test(content)) continue;\n writeFileSync(configPath, content.replace(re, `$1${SECURITY_HEADERS_BLOCK}`));\n applied.push(`Added baseline security headers to ${path.basename(configPath)}`);\n return \"patched\";\n }\n return \"manual\";\n}\n\n/* ------------------------------------------------------------ advisories --- */\n\ninterface Advisory {\n title: string;\n severity: string;\n url: string;\n}\n\n/**\n * Published security advisories for exact package versions, from the npm\n * registry's bulk advisory service (the one `pnpm audit` uses). Null when it\n * can't be reached, so callers can fall back instead of guessing.\n */\nasync function fetchAdvisories(versions: Record<string, string>): Promise<Record<string, Advisory[]> | null> {\n const body: Record<string, string[]> = {};\n for (const [name, v] of Object.entries(versions)) body[name] = [v];\n if (Object.keys(body).length === 0) return {};\n try {\n const res = await fetch(\"https://registry.npmjs.org/-/npm/v1/security/advisories/bulk\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: AbortSignal.timeout(8000),\n });\n if (!res.ok) return null;\n return (await res.json()) as Record<string, Advisory[]>;\n } catch {\n return null;\n }\n}\n\n/* ---------------------------------------------------------------- checks --- */\n\ninterface Ctx {\n cwd: string;\n env: Record<string, string>;\n production: boolean;\n offline: boolean;\n url?: string;\n anonKey?: string;\n serviceKey?: string;\n}\n\nasync function checkProject(ctx: Ctx): Promise<void> {\n group(\"Project\");\n const pkgPath = path.join(ctx.cwd, \"package.json\");\n if (!existsSync(pkgPath)) {\n fail(\"No package.json here\", \"Run this from the root of your Next.js project.\");\n return;\n }\n\n const ownPkg = JSON.parse(readFileSync(path.join(bundledMigrationsDir(), \"..\", \"..\", \"package.json\"), \"utf8\"));\n pass(`@guard-rail/core ${ownPkg.version}`);\n\n const require = createRequire(pkgPath);\n const installed = (name: string): string | undefined => {\n try {\n return JSON.parse(readFileSync(require.resolve(`${name}/package.json`), \"utf8\")).version;\n } catch {\n return undefined;\n }\n };\n const nextVersion = installed(\"next\");\n if (!nextVersion) warn(\"Next.js isn't installed here\", \"Couldn't resolve `next` from this directory.\");\n\n // Ask the registry about what's actually installed; only fall back to the\n // built-in table for Next.js when that isn't possible.\n const checked: Record<string, string> = {};\n for (const name of [\"next\", \"@supabase/supabase-js\", \"@supabase/ssr\"]) {\n const v = installed(name);\n if (v) checked[name] = v;\n }\n checked[\"@guard-rail/core\"] = ownPkg.version;\n const advisories = ctx.offline ? null : await fetchAdvisories(checked);\n\n if (advisories) {\n let clean = 0;\n for (const [name, version] of Object.entries(checked)) {\n const found = advisories[name] ?? [];\n const serious = found.filter((a) => a.severity === \"critical\" || a.severity === \"high\");\n if (found.length === 0) {\n clean++;\n continue;\n }\n const lines = found.slice(0, 3).map((a) => `${a.severity}: ${a.title} (${a.url})`);\n if (found.length > 3) lines.push(`…and ${found.length - 3} more`);\n (serious.length ? fail : warn)(\n `${name} ${version} has ${found.length} known security advisor${found.length === 1 ? \"y\" : \"ies\"}`,\n lines.join(\"\\n \"),\n `Upgrade: pnpm add ${name}@latest`,\n );\n }\n if (clean) pass(`No known security advisories for ${clean} checked package${clean === 1 ? \"\" : \"s\"}`, Object.entries(checked).filter(([n]) => (advisories[n] ?? []).length === 0).map(([n, v]) => `${n} ${v}`).join(\", \"));\n } else if (nextVersion) {\n // Offline, or the registry couldn't be reached: the built-in table.\n const v = parseVersion(nextVersion);\n const floor = v ? NEXT_MIN_PATCHED[v[0]] : undefined;\n const why = ctx.offline ? \"offline\" : \"the advisory service couldn't be reached\";\n if (!v) {\n info(`Next.js ${nextVersion}`);\n } else if (floor && !atLeast(v, floor)) {\n fail(\n `Next.js ${nextVersion} has known security vulnerabilities`,\n `Releases before ${floor.join(\".\")} carry published advisories (checked against this tool's built-in list — ${why}).`,\n `Upgrade: pnpm add next@latest`,\n );\n } else if (floor) {\n pass(`Next.js ${nextVersion}`, `Checked against this tool's built-in list (${why}); it may not know about newer advisories.`);\n } else {\n warn(\n `Next.js ${nextVersion}`,\n `This tool doesn't know the patched release for the ${v[0]}.x line (${why}).`,\n `Make sure you're on its latest patch, or move to the current major.`,\n );\n }\n }\n\n for (const dep of [\"@supabase/supabase-js\", \"@supabase/ssr\"]) {\n try {\n require.resolve(`${dep}/package.json`);\n pass(`${dep} installed`);\n } catch {\n fail(`${dep} isn't installed`, undefined, `pnpm add ${dep}`);\n }\n }\n}\n\nfunction checkEnvironment(ctx: Ctx): void {\n group(\"Environment\");\n const { env } = ctx;\n\n if (!ctx.url) {\n fail(\"SUPABASE_URL isn't set\", \"Not in the environment, .env.local or .env.\");\n } else {\n try {\n const u = new URL(ctx.url);\n if (u.protocol !== \"https:\" && u.protocol !== \"http:\") throw new Error();\n if (u.protocol === \"http:\" && !isLocalHost(u.hostname)) {\n warn(\"SUPABASE_URL uses plain http\", \"Use the project's https:// address.\");\n } else {\n pass(\"SUPABASE_URL\", u.host);\n }\n } catch {\n fail(\"SUPABASE_URL isn't a valid URL\", \"Expected https://<project>.supabase.co.\");\n }\n }\n\n const anon = ctx.anonKey;\n if (!anon) {\n fail(\"SUPABASE_ANON_KEY isn't set\");\n } else {\n const role = jwtRole(anon);\n if (role === \"service_role\" || anon.startsWith(\"sb_secret_\")) {\n fail(\n \"SUPABASE_ANON_KEY holds the service-role (secret) key\",\n \"The anon key is meant to be public; the service-role key bypasses every access rule.\",\n \"Swap them: put the anon / publishable key here.\",\n );\n } else {\n pass(\"SUPABASE_ANON_KEY\");\n }\n }\n\n const svc = ctx.serviceKey;\n if (!svc) {\n warn(\n \"No service-role key\",\n \"Invites, password resets and database-backed rate limits need SUPABASE_SERVICE_ROLE_KEY (server-only).\",\n );\n } else {\n const role = jwtRole(svc);\n if (role === \"anon\" || svc.startsWith(\"sb_publishable_\")) {\n fail(\"SUPABASE_SERVICE_ROLE_KEY holds the anon (publishable) key\", undefined, \"Use the service-role / secret key here.\");\n } else {\n pass(\"Service-role key present\");\n }\n }\n\n // A secret behind NEXT_PUBLIC_ is compiled into the browser bundle.\n const leaked = Object.keys(env).filter((k) => /^NEXT_PUBLIC_.*(SERVICE|SECRET|RESEND|PRIVATE)/i.test(k));\n if (leaked.length) {\n fail(\n \"Secret behind a NEXT_PUBLIC_ name\",\n `${leaked.join(\", \")} would be published to every visitor's browser.`,\n \"Rename it without the NEXT_PUBLIC_ prefix and read it only in server code.\",\n );\n } else {\n pass(\"No secrets behind NEXT_PUBLIC_ names\");\n }\n\n const siteUrl = env.GUARDRAIL_SITE_URL;\n if (!siteUrl) {\n (ctx.production ? fail : warn)(\n \"GUARDRAIL_SITE_URL isn't set\",\n \"Production sends no invite or password-reset emails without it — and \\\"Forgot password?\\\" fails silently.\",\n \"Set it to your public address, e.g. https://example.com (or pass siteUrl to withGuardrail()).\",\n );\n } else {\n try {\n const u = new URL(siteUrl);\n if (u.protocol !== \"https:\" && u.protocol !== \"http:\") throw new Error();\n if (u.protocol === \"http:\" && !isLocalHost(u.hostname)) {\n warn(\"GUARDRAIL_SITE_URL uses plain http\", \"Emailed links should point at an https address.\");\n } else {\n pass(\"GUARDRAIL_SITE_URL\", u.origin);\n }\n } catch {\n fail(\"GUARDRAIL_SITE_URL isn't a valid http(s) URL\", siteUrl);\n }\n }\n\n const hasKey = Boolean(env.RESEND_API_KEY);\n const hasFrom = Boolean(env.GUARDRAIL_EMAIL_FROM);\n if (hasKey && hasFrom) {\n pass(\"Resend email configured\");\n } else if (hasKey !== hasFrom) {\n fail(\n `${hasKey ? \"GUARDRAIL_EMAIL_FROM\" : \"RESEND_API_KEY\"} is missing`,\n \"Email through Resend needs both RESEND_API_KEY and GUARDRAIL_EMAIL_FROM.\",\n );\n } else {\n info(\"No Resend settings in the environment\", \"Fine if you pass your own onInvite / onPasswordReset, or only use consoleEmails() in development.\");\n }\n\n if (env.GUARDRAIL_SITE_ID) pass(\"GUARDRAIL_SITE_ID\", env.GUARDRAIL_SITE_ID);\n else info(\"GUARDRAIL_SITE_ID isn't set\", 'Sites default to the id \"default\".');\n}\n\nfunction checkFiles(ctx: Ctx): void {\n group(\"Files\");\n\n // Migrations\n const src = bundledMigrationsDir();\n const dest = path.join(ctx.cwd, \"supabase\", \"migrations\");\n if (existsSync(src)) {\n const shipped = readdirSync(src);\n const have = existsSync(dest) ? new Set(readdirSync(dest)) : new Set<string>();\n const missing = shipped.filter((f) => !have.has(f));\n if (missing.length === 0) {\n pass(\"All Guardrail migrations are in supabase/migrations/\");\n } else {\n fail(\n `${missing.length} migration${missing.length === 1 ? \"\" : \"s\"} not copied into supabase/migrations/`,\n missing.join(\", \"),\n \"Run: pnpm exec guardrail-doctor --fix (or guardrail-init-db)\",\n );\n }\n }\n\n // Local Supabase config exposes the schema\n const configToml = path.join(ctx.cwd, \"supabase\", \"config.toml\");\n if (existsSync(configToml)) {\n const toml = readFileSync(configToml, \"utf8\");\n const schemas = toml.match(/\\[api\\][\\s\\S]*?schemas\\s*=\\s*\\[([^\\]]*)\\]/)?.[1] ?? \"\";\n if (schemas.includes('\"guardrail\"')) pass('supabase/config.toml exposes the \"guardrail\" schema');\n else fail('supabase/config.toml doesn\\'t expose the \"guardrail\" schema', undefined, \"Run: pnpm exec guardrail-init-db\");\n }\n\n // Root layout wiring\n const layouts = [\"app/layout.tsx\", \"src/app/layout.tsx\", \"app/layout.jsx\", \"src/app/layout.jsx\", \"app/layout.js\", \"src/app/layout.js\"];\n const layout = layouts.map((l) => path.join(ctx.cwd, l)).find(existsSync);\n const wiredIn = (file: string) => /withGuardrail/.test(readFileSync(file, \"utf8\"));\n if (!layout) {\n skip(\"Root layout not found\", \"Looked for app/layout and src/app/layout.\");\n } else if (wiredIn(layout)) {\n pass(\"Root layout uses withGuardrail()\");\n } else {\n warn(\n \"Root layout doesn't mention withGuardrail()\",\n `${path.relative(ctx.cwd, layout)} — fine if it's wrapped where the layout is exported from another file.`,\n );\n }\n\n // Security headers\n const configPath = findNextConfig(ctx.cwd);\n if (!configPath) {\n skip(\"next.config not found\");\n } else {\n const content = readFileSync(configPath, \"utf8\");\n if (/^[ \\t]*cacheComponents\\s*:\\s*true/m.test(content)) {\n fail(\n \"Cache Components is turned on\",\n \"Guardrail reads the session cookie to decide what to render, which Next's Cache Components mode refuses outside <Suspense> — the site won't pass `next build`.\",\n \"Run: pnpm exec guardrail-doctor --fix (removes `cacheComponents: true` and `partialPrefetching: true`)\",\n );\n }\n if (/frame-ancestors|x-frame-options/i.test(content)) {\n pass(`${path.basename(configPath)} sets anti-framing headers`);\n } else {\n warn(\n `${path.basename(configPath)} sets no security headers`,\n \"Without frame-ancestors, another site can frame yours — including the editing toolbar — under a clickjacking overlay.\",\n \"Run: pnpm exec guardrail-doctor --fix (skip if your host or middleware sets them; verify with --app-url)\",\n );\n }\n }\n\n // Env files must be ignored by git\n const gitignore = path.join(ctx.cwd, \".gitignore\");\n const hasEnvFile = [\".env.local\", \".env\"].some((f) => existsSync(path.join(ctx.cwd, f)));\n if (hasEnvFile) {\n const ignores = existsSync(gitignore) ? readFileSync(gitignore, \"utf8\") : \"\";\n if (/^\\s*\\.env(\\*|\\.local|\\.\\*)?\\s*$/m.test(ignores) || /^\\s*\\.env\\*/m.test(ignores)) {\n pass(\"Env files are gitignored\");\n } else {\n fail(\".env files aren't gitignored\", \"Your Supabase keys could be committed.\", \"Add `.env*` (and `!.env.example`) to .gitignore.\");\n }\n }\n}\n\ntype SupabaseModule = typeof import(\"@supabase/supabase-js\");\ntype SupabaseClient = import(\"@supabase/supabase-js\").SupabaseClient;\n\nasync function loadSupabase(): Promise<SupabaseModule | null> {\n try {\n return (await import(\"@supabase/supabase-js\")) as SupabaseModule;\n } catch {\n return null;\n }\n}\n\nasync function checkDatabase(ctx: Ctx, sb: SupabaseModule | null): Promise<boolean> {\n group(\"Database\");\n if (!ctx.url || !ctx.anonKey) {\n skip(\"Skipped\", \"Needs SUPABASE_URL and SUPABASE_ANON_KEY.\");\n return false;\n }\n if (!sb) {\n skip(\"Skipped\", \"@supabase/supabase-js isn't installed.\");\n return false;\n }\n const base = ctx.url.replace(/\\/$/, \"\");\n\n // Reachability + schema exposure, with the public key only\n try {\n const res = await fetch(`${base}/rest/v1/gr_content?select=path_key&limit=1`, {\n headers: { apikey: ctx.anonKey, Authorization: `Bearer ${ctx.anonKey}`, \"Accept-Profile\": GR_SCHEMA },\n });\n const body = res.ok ? \"\" : await res.text();\n if (res.ok) {\n pass(`\"${GR_SCHEMA}\" schema is exposed and readable`);\n } else if (/PGRST106|Invalid schema/i.test(body)) {\n fail(\n `The \"${GR_SCHEMA}\" schema isn't exposed`,\n \"PostgREST refuses requests for it.\",\n \"Supabase dashboard > Settings > API > Exposed schemas: add guardrail.\",\n );\n return false;\n } else if (/PGRST205|42P01|does not exist|schema cache/i.test(body)) {\n fail(\"Guardrail's tables don't exist\", \"The migration hasn't been applied to this project.\", \"Run: pnpm exec guardrail-init-db --push\");\n return false;\n } else if (res.status === 401 || res.status === 403) {\n fail(\"The anon key was rejected\", `HTTP ${res.status}`, \"Check SUPABASE_ANON_KEY belongs to this project.\");\n return false;\n } else {\n fail(\"Unexpected response reading content\", `HTTP ${res.status}: ${body.slice(0, 160)}`);\n return false;\n }\n } catch (e) {\n fail(\"Couldn't reach Supabase\", e instanceof Error ? e.message : String(e), \"Check SUPABASE_URL, and that the project isn't paused.\");\n return false;\n }\n\n // Sign-up settings: the one thing that decides whether an invite can be claimed by someone else\n try {\n const res = await fetch(`${base}/auth/v1/settings`, { headers: { apikey: ctx.anonKey } });\n if (res.ok) {\n const s = (await res.json()) as {\n disable_signup?: boolean;\n mailer_autoconfirm?: boolean;\n external?: { email?: boolean };\n };\n if (s.external?.email === false) {\n fail(\"Email sign-in is disabled in Supabase\", undefined, \"Authentication > Sign In / Providers: enable Email.\");\n } else if (s.disable_signup) {\n pass(\"Public sign-ups are off\");\n } else if (s.mailer_autoconfirm) {\n warn(\n \"Anyone can sign up, and addresses count as confirmed instantly\",\n \"Someone could register an invited person's address before they do, and claim the invitation.\",\n \"Authentication > Sign In / Providers > Email: turn on Confirm email (or turn off public sign-ups).\",\n );\n } else {\n pass(\"Sign-ups require a confirmed email\");\n }\n } else {\n skip(\"Couldn't read the project's sign-up settings\", `HTTP ${res.status}`);\n }\n } catch {\n skip(\"Couldn't read the project's sign-up settings\");\n }\n\n if (!ctx.serviceKey) {\n skip(\"Service-role checks skipped\", \"No service-role key to inspect the database's functions with.\");\n return true;\n }\n\n const admin = sb.createClient(ctx.url, ctx.serviceKey, { auth: { persistSession: false, autoRefreshToken: false } });\n const gr = () => admin.schema(GR_SCHEMA);\n\n for (const table of [GR_MEMBERS_TABLE, \"gr_content\", \"gr_repeater_items\", \"gr_rate_limits\"]) {\n const { error } = await gr().from(table).select(\"*\", { head: true, count: \"exact\" }).limit(1);\n if (!error) pass(`Table ${table}`);\n else if (table === \"gr_rate_limits\") warn(\"Table gr_rate_limits is missing\", \"Rate limits are counted in memory until the migration is applied.\", \"Run: pnpm exec guardrail-init-db --push\");\n else fail(`Table ${table} isn't readable`, error.message, \"Run: pnpm exec guardrail-init-db --push\");\n }\n\n // Functions: a permission error means it exists but this role can't run it; \"not found\" means it's absent.\n const probes: Array<[string, Record<string, unknown>, string]> = [\n [\"gr_confirmed_email\", {}, \"Confirmed-email identity check\"],\n [\"gr_claim_invite\", { target_site: \"doctor-probe\" }, \"Invite-claim function\"],\n [\"gr_ensure_bucket\", { target_bucket: \"gr-doctor-probe\" }, \"Media bucket function\"],\n ];\n for (const [fn, args, label] of probes) {\n const { error } = await gr().rpc(fn, args);\n if (!error || error.code === \"42501\" || /permission denied/i.test(error.message)) {\n pass(label);\n } else if (error.code === \"PGRST202\" || error.code === \"42883\" || /could not find the function/i.test(error.message)) {\n fail(`${label} is missing`, `Function ${fn}() doesn't exist.`, \"Apply the latest migrations: pnpm exec guardrail-init-db --push\");\n } else {\n // Exists, but failed for another reason with these dummy arguments.\n pass(label);\n }\n }\n\n const probeKey = `doctor-${randomBytes(3).toString(\"hex\")}`;\n const { data, error } = await gr().rpc(\"gr_rate_hit\", { p_name: \"doctor:probe\", p_key: probeKey, p_limit: 1000, p_window_seconds: 60 });\n if (!error && data) {\n pass(\"Database-backed rate limiting\");\n await gr().from(\"gr_rate_limits\").delete().eq(\"bucket\", `doctor:probe|${probeKey}`);\n } else if (error?.code === \"PGRST202\" || error?.code === \"42883\") {\n warn(\"Rate-limit function is missing\", \"Limits are counted in memory, which doesn't hold across serverless instances.\", \"Apply the latest migrations: pnpm exec guardrail-init-db --push\");\n } else if (error) {\n warn(\"Rate-limit function failed\", error.message);\n }\n\n info(\"Admin guard (can't delete the last admin)\", \"Verified by --e2e; it's a trigger, which can't be probed read-only.\");\n return true;\n}\n\n/* -------------------------------------------------------------------- csp --- */\n\ntype Csp = Record<string, string[]>;\n\nfunction parseCsp(value: string): Csp {\n const out: Csp = {};\n for (const part of value.split(\";\")) {\n const [name, ...sources] = part.trim().split(/\\s+/);\n if (name) out[name.toLowerCase()] = sources;\n }\n return out;\n}\n\n/** The sources a directive effectively allows: its own list, else default-src's; null if neither (unrestricted). */\nfunction effectiveSources(csp: Csp, name: string): string[] | null {\n return csp[name] ?? csp[\"default-src\"] ?? null;\n}\n\n/** Whether a source list lets the page reach `origin` (a different origin from the site's own). */\nfunction allowsOrigin(sources: string[] | null, origin: string): boolean {\n if (sources === null) return true;\n const target = new URL(origin);\n return sources.some((raw) => {\n const src = raw.toLowerCase();\n if (src === \"*\") return true;\n if (src === `${target.protocol}`) return true; // scheme only, e.g. https:\n if (src === origin.toLowerCase()) return true;\n const wild = src.match(/^(https?:)\\/\\/\\*\\.(.+?)(?::(\\d+))?$/);\n if (wild) {\n const [, scheme, domain, port] = wild;\n return scheme === target.protocol && target.hostname.endsWith(`.${domain}`) && (!port || port === target.port);\n }\n return false;\n });\n}\n\n/** A starter policy for this site: Guardrail's needs plus a conservative baseline for everything else. */\nfunction starterPolicy(supabaseUrl?: string): string {\n const gr = guardrailCspDirectives(supabaseUrl ? { supabaseUrl } : {});\n return cspString({\n \"default-src\": [\"'self'\"],\n \"script-src\": [\"'self'\", \"'nonce-REPLACE_PER_REQUEST'\", \"'strict-dynamic'\"],\n \"style-src\": gr[\"style-src\"]!,\n \"img-src\": [...gr[\"img-src\"]!, \"data:\"],\n \"connect-src\": gr[\"connect-src\"]!,\n \"font-src\": [\"'self'\", \"data:\"],\n \"object-src\": [\"'none'\"],\n \"base-uri\": [\"'self'\"],\n \"form-action\": [\"'self'\"],\n \"frame-ancestors\": gr[\"frame-ancestors\"]!,\n });\n}\n\nfunction printStarterCsp(ctx: Ctx): void {\n console.log(`Starter Content-Security-Policy\n\nContent-Security-Policy-Report-Only: ${starterPolicy(ctx.url)}\n\nHow to use it\n - Send it as Content-Security-Policy-Report-Only first. Browsers report what it\n would block without blocking anything; tune it for the things your site\n loads itself (analytics, fonts, embeds), then switch the header name to\n Content-Security-Policy.\n - Guardrail needs nothing beyond 'self' for styles and scripts. Its stylesheet is\n a real file and it renders no inline scripts, so it needs no 'unsafe-inline' and no\n nonce. The only extras are your Supabase origin in connect-src (image\n uploads) and img-src (the media picker's thumbnails).${ctx.url ? \"\" : \"\\n (Set SUPABASE_URL and run this again to include your Supabase origin.)\"}\n - script-src above uses a per-request nonce, which Next.js sets from a proxy\n (see \"Content Security Policy\" in the Next.js docs). With only a static header\n in next.config you can't use a nonce — use 'self' 'unsafe-inline' for scripts\n instead, which is a weaker policy but still a useful one.\n - guardrailCspDirectives() from @guard-rail/core returns Guardrail's directives\n if you build the header in code.\n`);\n}\n\nasync function checkLiveSite(appUrl: string, ctx: Ctx): Promise<void> {\n group(\"Live site\");\n let res: Response;\n try {\n res = await fetch(appUrl, { redirect: \"manual\" });\n } catch (e) {\n fail(`Couldn't reach ${appUrl}`, e instanceof Error ? e.message : String(e), \"Is the site running?\");\n return;\n }\n if (res.status >= 500) {\n fail(`${appUrl} answered HTTP ${res.status}`);\n return;\n }\n pass(`${appUrl} answered HTTP ${res.status}`);\n\n const h = res.headers;\n const csp = h.get(\"content-security-policy\") ?? \"\";\n if (/frame-ancestors/i.test(csp) || h.get(\"x-frame-options\")) pass(\"Can't be framed by another site\");\n else fail(\"Can be framed by another site\", \"No frame-ancestors or X-Frame-Options header.\", \"Add the baseline headers: pnpm exec guardrail-doctor --fix\");\n\n if (/nosniff/i.test(h.get(\"x-content-type-options\") ?? \"\")) pass(\"X-Content-Type-Options: nosniff\");\n else warn(\"X-Content-Type-Options header missing\");\n\n if (h.get(\"referrer-policy\")) pass(`Referrer-Policy: ${h.get(\"referrer-policy\")}`);\n else warn(\"Referrer-Policy header missing\");\n\n if (new URL(appUrl).protocol === \"https:\") {\n if (h.get(\"strict-transport-security\")) pass(\"Strict-Transport-Security set\");\n else warn(\"Strict-Transport-Security header missing\", \"Browsers won't insist on HTTPS for this site.\");\n } else {\n skip(\"Strict-Transport-Security\", \"Only meaningful over HTTPS.\");\n }\n\n const setCookie = typeof h.getSetCookie === \"function\" ? h.getSetCookie() : [];\n const session = setCookie.filter((c) => /^sb-.*-auth-token/i.test(c));\n if (session.length) fail(\"A session cookie was set for an anonymous request\", session.map((c) => c.split(\"=\")[0]).join(\", \"));\n\n // Guardrail's stylesheet: the chrome is styled by a CSS file the package imports\n // itself. A bundler that doesn't pick up CSS from node_modules would leave it\n // silently unstyled, so confirm the CSS actually reached the page.\n if (res.status === 200) {\n const html = await res.text().catch(() => \"\");\n const MARKER = /--gr-bg\\b|\\.gr-banner\\b/;\n const inline = [...html.matchAll(/<style[^>]*>([\\s\\S]*?)<\\/style>/gi)].map((m) => m[1]).join(\"\\n\");\n let found = MARKER.test(inline);\n const hrefs = [...html.matchAll(/<link\\b[^>]*rel=[\"']stylesheet[\"'][^>]*>/gi)]\n .map((m) => m[0].match(/href=[\"']([^\"']+)[\"']/i)?.[1])\n .filter((x): x is string => Boolean(x))\n .slice(0, 8);\n for (const href of hrefs) {\n if (found) break;\n try {\n const css = await fetch(new URL(href, appUrl), { signal: AbortSignal.timeout(8000) });\n if (css.ok && MARKER.test(await css.text())) found = true;\n } catch {\n // an unreachable stylesheet is the site's own problem; keep looking\n }\n }\n if (found) pass(\"Guardrail's stylesheet is on the page\");\n else if (hrefs.length === 0 && !/<style/i.test(html)) skip(\"Guardrail's stylesheet\", \"The page has no stylesheets to inspect.\");\n else {\n warn(\n \"Guardrail's stylesheet isn't on the page\",\n \"Its editing chrome would render unstyled. The package imports its own CSS, which your bundler may not be picking up.\",\n 'Import it once in your root layout: import \"@guard-rail/core/styles.css\";',\n );\n }\n }\n\n // Content-Security-Policy: is there a real one, and does it let Guardrail work?\n const enforced = h.get(\"content-security-policy\");\n const reportOnly = h.get(\"content-security-policy-report-only\");\n const policy = enforced ?? reportOnly;\n if (!policy) {\n warn(\n \"No Content-Security-Policy\",\n \"Nothing limits where scripts and styles can load from, so an injected script would run.\",\n \"Run: pnpm exec guardrail-doctor --csp (prints a starter report-only policy)\",\n );\n return;\n }\n const parsed = parseCsp(policy);\n const label = enforced ? \"\" : \" (report-only)\";\n if (!parsed[\"default-src\"] && !parsed[\"script-src\"]) {\n info(`Content-Security-Policy only sets ${Object.keys(parsed).join(\", \") || \"nothing\"}`, \"It doesn't restrict scripts. Run: pnpm exec guardrail-doctor --csp for a fuller starter.\");\n return;\n }\n const style = effectiveSources(parsed, \"style-src\");\n if (style && style.map((x) => x.toLowerCase()).includes(\"'unsafe-inline'\")) {\n info(`style-src allows 'unsafe-inline'${label}`, \"Guardrail no longer needs it — its chrome is a stylesheet. Remove it unless something else on the site does.\");\n } else {\n pass(`Policy${label} doesn't need 'unsafe-inline' for Guardrail's styles`);\n }\n if (ctx.url) {\n let supabase: string | undefined;\n try {\n supabase = new URL(ctx.url).origin;\n } catch {\n // reported under Environment\n }\n if (supabase && supabase !== new URL(appUrl).origin) {\n for (const [directive, why] of [\n [\"connect-src\", \"image uploads go straight from the browser to Supabase storage\"],\n [\"img-src\", \"uploaded images and the media picker's thumbnails load from Supabase storage\"],\n ] as const) {\n if (allowsOrigin(effectiveSources(parsed, directive), supabase)) pass(`${directive}${label} allows ${supabase}`);\n else warn(`${directive}${label} blocks ${supabase}`, `Needed because ${why}.`, `Add ${supabase} to ${directive}.`);\n }\n }\n }\n}\n\n/* ------------------------------------------------------------------ e2e --- */\n\n// A 1x1 transparent PNG.\nconst TINY_PNG = Buffer.from(\n \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==\",\n \"base64\",\n);\n\nasync function confirmE2E(ctx: Ctx, yes: boolean): Promise<boolean> {\n const host = new URL(ctx.url!).host;\n const local = isLocalHost(new URL(ctx.url!).hostname);\n console.log(\n `\\nThe end-to-end suite will create a throwaway user, a few rows and one test image on ${host},\\nexercise them as a signed-in editor would, then delete everything it made.\\n`,\n );\n if (local || yes) return true;\n if (!process.stdin.isTTY) {\n console.log(\"That's not a local database. Re-run with --yes to allow it.\");\n return false;\n }\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n const answer = (await rl.question(`${host} is not a local database. Continue? [y/N] `)).trim().toLowerCase();\n rl.close();\n return answer === \"y\" || answer === \"yes\";\n}\n\nasync function runE2E(ctx: Ctx, sb: SupabaseModule): Promise<void> {\n group(\"End-to-end\");\n const url = ctx.url!;\n const admin = sb.createClient(url, ctx.serviceKey!, { auth: { persistSession: false, autoRefreshToken: false } });\n const user = sb.createClient(url, ctx.anonKey!, { auth: { persistSession: false, autoRefreshToken: false } });\n const anon = sb.createClient(url, ctx.anonKey!, { auth: { persistSession: false, autoRefreshToken: false } });\n const gr = (c: SupabaseClient) => c.schema(GR_SCHEMA);\n\n const tag = randomBytes(4).toString(\"hex\");\n const siteId = `doctor-${tag}`;\n const otherSite = `doctor-${tag}-other`;\n const email = `guardrail-doctor-${tag}@example.com`;\n const password = randomBytes(18).toString(\"base64url\");\n const objectPath = `${siteId}/doctor.png`;\n let userId: string | undefined;\n let uploaded = false;\n let storageChecked = false;\n\n // `why` explains a failure and is only shown when the check fails; `note` is\n // shown when it passes.\n const expect = (ok: boolean, title: string, why?: string, hint?: string, note?: string) =>\n ok ? pass(title, note) : fail(title, why, hint);\n\n try {\n // 1. A throwaway account, and a link the way an invite or reset would mint one\n const created = await admin.auth.admin.createUser({ email, password, email_confirm: true });\n if (created.error || !created.data.user) {\n fail(\"Create a test account\", created.error?.message ?? \"No user returned.\", \"Needs a valid service-role key and Email sign-in enabled.\");\n return;\n }\n userId = created.data.user.id;\n pass(\"Create a test account (service-role key works)\");\n\n const link = await admin.auth.admin.generateLink({ type: \"recovery\", email });\n expect(\n !link.error && Boolean(link.data?.properties?.hashed_token),\n \"Mint a password-reset link\",\n link.error?.message,\n \"Invites and resets need this to work; check Email sign-in is enabled.\",\n );\n\n // 2. Invite claim: a pending membership becomes active for the signed-in person\n const seeded = await gr(admin).from(GR_MEMBERS_TABLE).insert({ site_id: siteId, email, role: \"admin\", status: \"pending\" });\n if (seeded.error) {\n fail(\"Record a pending invitation\", seeded.error.message);\n return;\n }\n const signedIn = await user.auth.signInWithPassword({ email, password });\n if (signedIn.error) {\n fail(\"Sign in with a password\", signedIn.error.message);\n return;\n }\n pass(\"Sign in with a password\");\n\n const claim = await gr(user).rpc(\"gr_claim_invite\", { target_site: siteId });\n const after = await gr(user).from(GR_MEMBERS_TABLE).select(\"status,role\").eq(\"site_id\", siteId).eq(\"email\", email).maybeSingle();\n expect(\n !claim.error && after.data?.status === \"active\" && after.data?.role === \"admin\",\n \"Claim an invitation\",\n claim.error?.message ?? (after.data ? undefined : \"The membership row wasn't visible after claiming.\"),\n \"Apply the latest migrations: pnpm exec guardrail-init-db --push\",\n );\n\n // 3. Content: an admin can write their own site, anyone can read it, nobody can write another site\n const write = await gr(user).from(\"gr_content\").insert({\n site_id: siteId,\n path_key: \"doctor.check\",\n type: \"text\",\n value: { type: \"text\", text: \"doctor\" },\n });\n expect(!write.error, \"Editor can save content to their site\", write.error?.message);\n\n const read = await gr(anon).from(\"gr_content\").select(\"path_key\").eq(\"site_id\", siteId).eq(\"path_key\", \"doctor.check\");\n expect(!read.error && (read.data?.length ?? 0) === 1, \"Visitors can read saved content\", read.error?.message);\n\n const cross = await gr(user).from(\"gr_content\").insert({\n site_id: otherSite,\n path_key: \"doctor.check\",\n type: \"text\",\n value: { type: \"text\", text: \"nope\" },\n });\n expect(Boolean(cross.error), \"Editor can't write to a site they don't belong to\", cross.error ? undefined : \"The write was allowed — row-level security is not protecting content.\");\n\n const anonWrite = await gr(anon).from(\"gr_content\").insert({\n site_id: siteId,\n path_key: \"doctor.anon\",\n type: \"text\",\n value: { type: \"text\", text: \"nope\" },\n });\n expect(Boolean(anonWrite.error), \"Visitors can't write content\", anonWrite.error ? undefined : \"An unauthenticated write was allowed.\");\n\n // 4. Admin guard: the last admin can't be demoted or removed through the API\n const demote = await gr(user).from(GR_MEMBERS_TABLE).update({ role: \"editor\" }).eq(\"site_id\", siteId).eq(\"email\", email);\n expect(\n Boolean(demote.error),\n \"The last admin can't be demoted\",\n demote.error ? undefined : \"The demotion succeeded — the admin guard trigger isn't installed.\",\n \"Apply the latest migrations: pnpm exec guardrail-init-db --push\",\n );\n // Put the admin back first, so this check doesn't depend on the last one\n // having been blocked.\n await gr(admin).from(GR_MEMBERS_TABLE).upsert(\n { site_id: siteId, email, user_id: userId, role: \"admin\", status: \"active\" },\n { onConflict: \"site_id,email\" },\n );\n const remove = await gr(user).from(GR_MEMBERS_TABLE).delete().eq(\"site_id\", siteId).eq(\"email\", email);\n const stillThere = await gr(admin).from(GR_MEMBERS_TABLE).select(\"email\").eq(\"site_id\", siteId).eq(\"email\", email);\n expect(\n Boolean(remove.error) || (stillThere.data?.length ?? 0) === 1,\n \"The last admin can't be removed\",\n remove.error ? undefined : \"The row was deleted — the admin guard trigger isn't installed.\",\n \"Apply the latest migrations: pnpm exec guardrail-init-db --push\",\n );\n\n // Whatever the last two checks did, the storage checks below need the admin back.\n await gr(admin).from(GR_MEMBERS_TABLE).upsert(\n { site_id: siteId, email, user_id: userId, role: \"admin\", status: \"active\" },\n { onConflict: \"site_id,email\" },\n );\n\n // 5. Rate limiting counts and then refuses\n const hits: boolean[] = [];\n let rateErr: string | undefined;\n for (let i = 0; i < 3; i++) {\n const r = await gr(admin).rpc(\"gr_rate_hit\", { p_name: \"doctor:e2e\", p_key: siteId, p_limit: 2, p_window_seconds: 60 });\n if (r.error) {\n rateErr = r.error.message;\n break;\n }\n const row = Array.isArray(r.data) ? r.data[0] : r.data;\n hits.push(Boolean(row?.allowed));\n }\n expect(\n !rateErr && hits.join() === \"true,true,false\",\n \"Rate limiter allows up to the limit, then refuses\",\n rateErr ?? (hits.length ? `Got ${hits.join(\", \")} instead of true, true, false` : undefined),\n \"Apply the latest migrations: pnpm exec guardrail-init-db --push\",\n `Allowed ${hits.filter(Boolean).length} of ${hits.length} attempts at a limit of 2`,\n );\n\n // 6. Storage, when the site uses it\n const buckets = await admin.storage.listBuckets();\n const mediaBucket = buckets.data?.find((b) => b.id === \"gr-media\");\n if (!mediaBucket) {\n skip(\"Storage\", 'No \"gr-media\" bucket — skipped (this site may not use editable images).');\n } else {\n storageChecked = true;\n const up = await user.storage.from(\"gr-media\").upload(objectPath, TINY_PNG, { contentType: \"image/png\" });\n uploaded = !up.error;\n expect(!up.error, \"Editor can upload an image to their site's folder\", up.error?.message, \"Check the storage policies from the migration are applied.\");\n if (uploaded) {\n const { data } = user.storage.from(\"gr-media\").getPublicUrl(objectPath);\n const res = await fetch(data.publicUrl).catch(() => null);\n expect(Boolean(res?.ok), \"Uploaded images are publicly readable\", res ? `HTTP ${res.status}` : \"Request failed.\", \"next/image also needs the storage host in remotePatterns.\", `HTTP ${res?.status}`);\n }\n const elsewhere = await user.storage.from(\"gr-media\").upload(`${otherSite}/doctor.png`, TINY_PNG, { contentType: \"image/png\" });\n if (!elsewhere.error) await admin.storage.from(\"gr-media\").remove([`${otherSite}/doctor.png`]);\n expect(Boolean(elsewhere.error), \"Editor can't upload into another site's folder\", elsewhere.error ? undefined : \"The upload was allowed.\");\n }\n } catch (e) {\n fail(\"The end-to-end run stopped unexpectedly\", e instanceof Error ? e.message : String(e));\n } finally {\n // Always clean up, and say so if something is left behind.\n const leftovers: string[] = [];\n const attempt = async (what: string, run: () => PromiseLike<{ error: { message: string } | null }>) => {\n try {\n const { error } = await run();\n if (error) leftovers.push(`${what} (${error.message})`);\n } catch (e) {\n leftovers.push(`${what} (${e instanceof Error ? e.message : String(e)})`);\n }\n };\n if (storageChecked && uploaded) await attempt(`storage object ${objectPath}`, () => admin.storage.from(\"gr-media\").remove([objectPath]));\n await attempt(\"content rows\", () => gr(admin).from(\"gr_content\").delete().in(\"site_id\", [siteId, otherSite]));\n await attempt(\"membership rows\", () => gr(admin).from(GR_MEMBERS_TABLE).delete().eq(\"site_id\", siteId));\n await attempt(\"rate-limit row\", () => gr(admin).from(\"gr_rate_limits\").delete().eq(\"bucket\", `doctor:e2e|${siteId}`));\n if (userId) await attempt(`test account ${email}`, () => admin.auth.admin.deleteUser(userId!));\n if (leftovers.length) {\n warn(\"Cleanup left something behind\", leftovers.join(\"; \"), `Everything it made is tagged \"${siteId}\" / ${email}; delete it by hand.`);\n } else {\n pass(\"Cleaned up everything the test created\");\n }\n }\n}\n\n/* --------------------------------------------------------------- output --- */\n\nconst COLOR = process.stdout.isTTY && !process.env.NO_COLOR;\nconst paint = (code: number, s: string) => (COLOR ? `\\u001b[${code}m${s}\\u001b[0m` : s);\nconst ICON: Record<Status, string> = {\n pass: paint(32, \"✓\"),\n warn: paint(33, \"!\"),\n fail: paint(31, \"✗\"),\n skip: paint(90, \"–\"),\n info: paint(36, \"i\"),\n};\n\nfunction printReport(): void {\n let last = \"\";\n for (const r of results) {\n if (r.group !== last) {\n console.log(`\\n${paint(1, r.group)}`);\n last = r.group;\n }\n console.log(` ${ICON[r.status]} ${r.title}`);\n if (r.detail) console.log(` ${paint(90, r.detail)}`);\n if (r.hint && (r.status === \"fail\" || r.status === \"warn\")) console.log(` ${paint(90, \"→ \" + r.hint)}`);\n }\n const count = (s: Status) => results.filter((r) => r.status === s).length;\n const parts = [`${count(\"pass\")} passed`, count(\"warn\") ? paint(33, `${count(\"warn\")} to review`) : null, count(\"fail\") ? paint(31, `${count(\"fail\")} failed`) : null].filter(Boolean);\n console.log(`\\n${parts.join(\", \")}\\n`);\n}\n\n/* ----------------------------------------------------------------- main --- */\n\nasync function main(): Promise<number> {\n const { flags, values, unknown } = parseFlags(process.argv.slice(2));\n if (flags.has(\"help\") || flags.has(\"h\")) {\n console.log(USAGE);\n return 0;\n }\n if (unknown.length) {\n console.error(`Unknown option${unknown.length === 1 ? \"\" : \"s\"}: ${unknown.join(\", \")}\\n\\n${USAGE}`);\n return 2;\n }\n\n const cwd = process.cwd();\n const fileEnv = readEnvFiles(cwd);\n const env: Record<string, string> = { ...fileEnv };\n for (const [k, v] of Object.entries(process.env)) if (v) env[k] = v;\n\n const ctx: Ctx = {\n cwd,\n env,\n production: flags.has(\"production\"),\n offline: flags.has(\"offline\"),\n url: env.SUPABASE_URL,\n anonKey: env.SUPABASE_ANON_KEY,\n serviceKey: env.SUPABASE_SERVICE_ROLE_KEY ?? env.SERVICE_KEY,\n };\n\n if (flags.has(\"csp\")) {\n printStarterCsp(ctx);\n return 0;\n }\n\n const json = flags.has(\"json\");\n if (!json) console.log(\"Guardrail doctor\");\n\n if (flags.has(\"fix\")) {\n fixMigrations(cwd);\n fixEnvNotes(cwd);\n fixCacheComponents(cwd);\n const headers = fixHeaders(cwd);\n if (headers === \"manual\") {\n applied.push(\"next.config has a shape I can't patch safely — add the headers from the README's \\\"Before you deploy\\\" yourself\");\n }\n if (flags.has(\"push\")) {\n if (!commandExists(\"supabase\")) {\n applied.push(\"Supabase CLI not found — apply the migrations yourself (`supabase db push`, or paste them into the SQL editor)\");\n } else {\n // A local Docker stack takes pending migrations with `migration up`;\n // `db push` is for a linked hosted project.\n const local = ctx.url ? isLocalHost(new URL(ctx.url).hostname) : false;\n const cmd = local ? [\"migration\", \"up\", \"--local\"] : [\"db\", \"push\"];\n const res = spawnSync(\"supabase\", cmd, { stdio: json ? \"ignore\" : \"inherit\" });\n applied.push(\n res.status === 0\n ? `Applied migrations with \\`supabase ${cmd.slice(0, 2).join(\" \")}\\``\n : local\n ? \"`supabase migration up` failed — is the local stack running? (`supabase start`)\"\n : \"`supabase db push` failed — if the project isn't linked yet, run `supabase link` first\",\n );\n }\n }\n if (!json) {\n console.log(`\\n${paint(1, \"Fixes\")}`);\n if (applied.length === 0) console.log(\" Nothing to fix locally.\");\n for (const a of applied) console.log(` • ${a}`);\n }\n } else if (flags.has(\"push\")) {\n console.error(\"--push only applies together with --fix.\");\n return 2;\n }\n\n await checkProject(ctx);\n checkEnvironment(ctx);\n checkFiles(ctx);\n\n const sb = await loadSupabase();\n const dbReachable = await checkDatabase(ctx, sb);\n\n if (values[\"app-url\"]) await checkLiveSite(values[\"app-url\"], ctx);\n\n if (flags.has(\"e2e\")) {\n group(\"End-to-end\");\n if (!dbReachable || !sb) {\n skip(\"Skipped\", \"The database checks above need to pass first.\");\n } else if (!ctx.serviceKey) {\n skip(\"Skipped\", \"The end-to-end suite needs the service-role key to create and remove its test account.\");\n } else if (await confirmE2E(ctx, flags.has(\"yes\"))) {\n await runE2E(ctx, sb);\n } else {\n skip(\"Skipped\", \"Not confirmed.\");\n }\n }\n\n if (json) {\n console.log(JSON.stringify({ applied, results }, null, 2));\n } else {\n printReport();\n if (results.some((r) => r.status === \"fail\")) {\n console.log(\"Fix the failures above, then run guardrail-doctor again.\\n\");\n }\n }\n\n const failed = results.some((r) => r.status === \"fail\");\n const warned = results.some((r) => r.status === \"warn\");\n return failed || (flags.has(\"strict\") && warned) ? 1 : 0;\n}\n\nmain().then(\n (code) => process.exit(code),\n (err) => {\n console.error(err);\n process.exit(1);\n },\n);\n"],"mappings":"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","names":["require","error"]}
|
package/dist/cli/init-db.js
CHANGED
|
@@ -34,9 +34,11 @@ Copies Guardrail's migration(s) from this package into ./supabase/migrations/
|
|
|
34
34
|
has no supabase/config.toml yet, and adds "guardrail" to its exposed schemas
|
|
35
35
|
for local dev.
|
|
36
36
|
|
|
37
|
-
--push Also
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
--push Also apply the migrations once the files are in place: \`supabase db push\`
|
|
38
|
+
for a hosted project, \`supabase migration up\` when SUPABASE_URL points
|
|
39
|
+
at a local stack. Without it, the migration is copied but not applied \u2014
|
|
40
|
+
run \`supabase db push\` yourself (after \`supabase link\`, if this
|
|
41
|
+
project isn't linked yet).`;
|
|
40
42
|
function patchExposedSchemas(configPath) {
|
|
41
43
|
const content = readFileSync(configPath, "utf8");
|
|
42
44
|
const patched = content.replace(
|
|
@@ -56,6 +58,21 @@ function patchExposedSchemas(configPath) {
|
|
|
56
58
|
}
|
|
57
59
|
writeFileSync(configPath, patched);
|
|
58
60
|
}
|
|
61
|
+
function isLocalSupabase() {
|
|
62
|
+
let url = process.env.SUPABASE_URL;
|
|
63
|
+
for (const file of [".env.local", ".env"]) {
|
|
64
|
+
if (url) break;
|
|
65
|
+
const p = path.join(process.cwd(), file);
|
|
66
|
+
if (!existsSync(p)) continue;
|
|
67
|
+
url = readFileSync(p, "utf8").match(/^SUPABASE_URL=(.*)$/m)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const host = new URL(url ?? "").hostname;
|
|
71
|
+
return host === "localhost" || host === "127.0.0.1" || host === "[::1]" || host.endsWith(".localhost");
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
59
76
|
async function main() {
|
|
60
77
|
const args = process.argv.slice(2);
|
|
61
78
|
if (args.includes("--help") || args.includes("-h")) {
|
|
@@ -106,11 +123,12 @@ async function main() {
|
|
|
106
123
|
);
|
|
107
124
|
return;
|
|
108
125
|
}
|
|
126
|
+
const local = isLocalSupabase();
|
|
109
127
|
try {
|
|
110
|
-
run("supabase", ["db", "push"]);
|
|
128
|
+
run("supabase", local ? ["migration", "up", "--local"] : ["db", "push"]);
|
|
111
129
|
} catch {
|
|
112
130
|
console.warn(
|
|
113
|
-
"\n`supabase db push` failed \u2014 if this project isn't linked yet, run `supabase link` first, then `supabase db push`."
|
|
131
|
+
local ? "\n`supabase migration up` failed \u2014 is the local stack running? Start it with `supabase start`." : "\n`supabase db push` failed \u2014 if this project isn't linked yet, run `supabase link` first, then `supabase db push`."
|
|
114
132
|
);
|
|
115
133
|
}
|
|
116
134
|
}
|
package/dist/cli/init-db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/init-db.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Copies Guardrail's SQL migration(s) into the current project's own\n * supabase/migrations/, then (optionally) applies them — the step\n * create-guardrail already automates for a brand-new scaffold (see\n * copyMigration()/maybeLinkAndPush() in that package), but has no\n * equivalent for an existing Next.js + Supabase app that just ran\n * `npm install @guard-rail/core`. Until now, postinstall.mjs pointed at\n * doing this by hand: \"Copy\n * node_modules/@guard-rail/core/supabase/migrations/*.sql into your own\n * supabase/migrations/, then `supabase db push`.\"\n *\n * Copies by filename, skipping anything already present in the destination\n * — safe to re-run after upgrading the package: it only ever adds\n * migrations this version ships that a prior run didn't already copy, and\n * never overwrites one that's already there.\n *\n * Deliberately non-interactive (flags only, no prompts), unlike\n * create-guardrail's wizard-style prompts — this is meant to be a single\n * command a developer (or a script) runs once against a project that\n * already exists, not a guided setup flow.\n */\nimport { spawnSync } from \"node:child_process\";\nimport {\n existsSync,\n mkdirSync,\n copyFileSync,\n readdirSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nfunction commandExists(cmd: string): boolean {\n // See create-guardrail's util.ts for why `which`/`where` over `command -v`\n // with shell:true — no shell needed, no Node deprecation warning.\n const res =\n process.platform === \"win32\" ? spawnSync(\"where\", [cmd]) : spawnSync(\"which\", [cmd]);\n return res.status === 0;\n}\n\n/** Throws with the failing command's name — every call site decides whether that's fatal. */\nfunction run(cmd: string, args: string[]): void {\n const res = spawnSync(cmd, args, { stdio: \"inherit\" });\n if (res.status !== 0) {\n throw new Error(`${cmd} ${args.join(\" \")} exited with code ${res.status}`);\n }\n}\n\nfunction fail(message: string): never {\n console.error(`\\n${message}\\n`);\n process.exit(1);\n}\n\nconst USAGE = `Usage:\n guardrail-init-db [--push]\n\nCopies Guardrail's migration(s) from this package into ./supabase/migrations/\n(skipping any already present there), runs \\`supabase init\\` if this project\nhas no supabase/config.toml yet, and adds \"guardrail\" to its exposed schemas\nfor local dev.\n\n--push Also run \\`supabase db push\\` once the files are in place. Without it,\n the migration is copied but not applied — run \\`supabase db push\\`\n yourself (after \\`supabase link\\`, if this project isn't linked yet).`;\n\n/**\n * Idempotent by design (a no-op if \"guardrail\" is already listed) — safe to\n * run on every invocation rather than only the first, including against a\n * config.toml a developer has since hand-edited.\n */\nfunction patchExposedSchemas(configPath: string): void {\n const content = readFileSync(configPath, \"utf8\");\n const patched = content.replace(\n /(\\[api\\][\\s\\S]*?schemas\\s*=\\s*\\[)([^\\]]*)(\\])/,\n (match, pre: string, inner: string, post: string) => {\n if (inner.includes('\"guardrail\"')) return match;\n const trimmed = inner.trimEnd();\n const sep = trimmed === \"\" || trimmed.endsWith(\",\") ? \" \" : \", \";\n return `${pre}${inner}${sep}\"guardrail\"${post}`;\n },\n );\n\n if (patched === content && !content.includes('\"guardrail\"')) {\n console.warn(\n 'Could not find the [api] schemas setting in supabase/config.toml to patch ' +\n 'automatically — add \"guardrail\" to it yourself before `supabase start`.',\n );\n return;\n }\n writeFileSync(configPath, patched);\n}\n\nasync function main() {\n const args = process.argv.slice(2);\n if (args.includes(\"--help\") || args.includes(\"-h\")) {\n console.log(USAGE);\n return;\n }\n const push = args.includes(\"--push\");\n\n // This file's own location, not the invoking project's node_modules —\n // resolves the migrations this installed package version ships,\n // regardless of how the command was invoked (npx, a package.json script,\n // a workspace link).\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const pkgRoot = path.join(__dirname, \"..\", \"..\");\n const srcDir = path.join(pkgRoot, \"supabase\", \"migrations\");\n if (!existsSync(srcDir)) {\n fail(`No bundled migrations found at ${srcDir} — is @guard-rail/core installed correctly?`);\n }\n\n const destDir = path.join(process.cwd(), \"supabase\", \"migrations\");\n mkdirSync(destDir, { recursive: true });\n const existing = new Set(readdirSync(destDir));\n\n const copied: string[] = [];\n for (const file of readdirSync(srcDir)) {\n if (existing.has(file)) continue;\n copyFileSync(path.join(srcDir, file), path.join(destDir, file));\n copied.push(file);\n }\n\n if (copied.length === 0) {\n console.log(\"supabase/migrations/ is already up to date — nothing to copy.\");\n } else {\n console.log(`Copied ${copied.length} migration${copied.length === 1 ? \"\" : \"s\"}:`);\n for (const file of copied) console.log(` supabase/migrations/${file}`);\n }\n\n if (!commandExists(\"supabase\")) {\n console.log(\n \"\\nSupabase CLI not found — apply the migration yourself (paste it into the SQL \" +\n \"editor, or install the CLI and run `supabase db push`).\",\n );\n return;\n }\n\n const configPath = path.join(process.cwd(), \"supabase\", \"config.toml\");\n if (!existsSync(configPath)) {\n try {\n run(\"supabase\", [\"init\"]);\n } catch {\n console.warn(\"\\n`supabase init` failed — you can run it yourself.\");\n return;\n }\n }\n if (existsSync(configPath)) patchExposedSchemas(configPath);\n\n if (!push) {\n console.log(\n \"\\nRun `guardrail-init-db --push` (or `supabase db push` yourself) to apply it.\",\n );\n return;\n }\n\n try {\n run(\"supabase\", [\"db\", \"push\"]);\n } catch {\n console.warn(\n \"\\n`supabase db push` failed — if this project isn't linked yet, run `supabase \" +\n \"link` first, then `supabase db push`.\",\n );\n }\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/cli/init-db.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * Copies Guardrail's SQL migration(s) into the current project's own\n * supabase/migrations/, then (optionally) applies them — the step\n * create-guardrail already automates for a brand-new scaffold (see\n * copyMigration()/maybeLinkAndPush() in that package), but has no\n * equivalent for an existing Next.js + Supabase app that just ran\n * `pnpm add @guard-rail/core`. Without it, this would be done by hand: copy\n * node_modules/@guard-rail/core/supabase/migrations/*.sql into your own\n * supabase/migrations/, then `supabase db push`.\n *\n * Copies by filename, skipping anything already present in the destination\n * — safe to re-run after upgrading the package: it only ever adds\n * migrations this version ships that a prior run didn't already copy, and\n * never overwrites one that's already there.\n *\n * Deliberately non-interactive (flags only, no prompts), unlike\n * create-guardrail's wizard-style prompts — this is meant to be a single\n * command a developer (or a script) runs once against a project that\n * already exists, not a guided setup flow.\n */\nimport { spawnSync } from \"node:child_process\";\nimport {\n existsSync,\n mkdirSync,\n copyFileSync,\n readdirSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nfunction commandExists(cmd: string): boolean {\n // See create-guardrail's util.ts for why `which`/`where` over `command -v`\n // with shell:true — no shell needed, no Node deprecation warning.\n const res =\n process.platform === \"win32\" ? spawnSync(\"where\", [cmd]) : spawnSync(\"which\", [cmd]);\n return res.status === 0;\n}\n\n/** Throws with the failing command's name — every call site decides whether that's fatal. */\nfunction run(cmd: string, args: string[]): void {\n const res = spawnSync(cmd, args, { stdio: \"inherit\" });\n if (res.status !== 0) {\n throw new Error(`${cmd} ${args.join(\" \")} exited with code ${res.status}`);\n }\n}\n\nfunction fail(message: string): never {\n console.error(`\\n${message}\\n`);\n process.exit(1);\n}\n\nconst USAGE = `Usage:\n guardrail-init-db [--push]\n\nCopies Guardrail's migration(s) from this package into ./supabase/migrations/\n(skipping any already present there), runs \\`supabase init\\` if this project\nhas no supabase/config.toml yet, and adds \"guardrail\" to its exposed schemas\nfor local dev.\n\n--push Also apply the migrations once the files are in place: \\`supabase db push\\`\n for a hosted project, \\`supabase migration up\\` when SUPABASE_URL points\n at a local stack. Without it, the migration is copied but not applied —\n run \\`supabase db push\\` yourself (after \\`supabase link\\`, if this\n project isn't linked yet).`;\n\n/**\n * Idempotent by design (a no-op if \"guardrail\" is already listed) — safe to\n * run on every invocation rather than only the first, including against a\n * config.toml a developer has since hand-edited.\n */\nfunction patchExposedSchemas(configPath: string): void {\n const content = readFileSync(configPath, \"utf8\");\n const patched = content.replace(\n /(\\[api\\][\\s\\S]*?schemas\\s*=\\s*\\[)([^\\]]*)(\\])/,\n (match, pre: string, inner: string, post: string) => {\n if (inner.includes('\"guardrail\"')) return match;\n const trimmed = inner.trimEnd();\n const sep = trimmed === \"\" || trimmed.endsWith(\",\") ? \" \" : \", \";\n return `${pre}${inner}${sep}\"guardrail\"${post}`;\n },\n );\n\n if (patched === content && !content.includes('\"guardrail\"')) {\n console.warn(\n 'Could not find the [api] schemas setting in supabase/config.toml to patch ' +\n 'automatically — add \"guardrail\" to it yourself before `supabase start`.',\n );\n return;\n }\n writeFileSync(configPath, patched);\n}\n\n/** True when this project's SUPABASE_URL (environment, .env.local or .env) points at a local stack. */\nfunction isLocalSupabase(): boolean {\n let url = process.env.SUPABASE_URL;\n for (const file of [\".env.local\", \".env\"]) {\n if (url) break;\n const p = path.join(process.cwd(), file);\n if (!existsSync(p)) continue;\n url = readFileSync(p, \"utf8\").match(/^SUPABASE_URL=(.*)$/m)?.[1]?.trim().replace(/^['\"]|['\"]$/g, \"\");\n }\n try {\n const host = new URL(url ?? \"\").hostname;\n return host === \"localhost\" || host === \"127.0.0.1\" || host === \"[::1]\" || host.endsWith(\".localhost\");\n } catch {\n return false;\n }\n}\n\nasync function main() {\n const args = process.argv.slice(2);\n if (args.includes(\"--help\") || args.includes(\"-h\")) {\n console.log(USAGE);\n return;\n }\n const push = args.includes(\"--push\");\n\n // This file's own location, not the invoking project's node_modules —\n // resolves the migrations this installed package version ships,\n // regardless of how the command was invoked (npx, a package.json script,\n // a workspace link).\n const __dirname = path.dirname(fileURLToPath(import.meta.url));\n const pkgRoot = path.join(__dirname, \"..\", \"..\");\n const srcDir = path.join(pkgRoot, \"supabase\", \"migrations\");\n if (!existsSync(srcDir)) {\n fail(`No bundled migrations found at ${srcDir} — is @guard-rail/core installed correctly?`);\n }\n\n const destDir = path.join(process.cwd(), \"supabase\", \"migrations\");\n mkdirSync(destDir, { recursive: true });\n const existing = new Set(readdirSync(destDir));\n\n const copied: string[] = [];\n for (const file of readdirSync(srcDir)) {\n if (existing.has(file)) continue;\n copyFileSync(path.join(srcDir, file), path.join(destDir, file));\n copied.push(file);\n }\n\n if (copied.length === 0) {\n console.log(\"supabase/migrations/ is already up to date — nothing to copy.\");\n } else {\n console.log(`Copied ${copied.length} migration${copied.length === 1 ? \"\" : \"s\"}:`);\n for (const file of copied) console.log(` supabase/migrations/${file}`);\n }\n\n if (!commandExists(\"supabase\")) {\n console.log(\n \"\\nSupabase CLI not found — apply the migration yourself (paste it into the SQL \" +\n \"editor, or install the CLI and run `supabase db push`).\",\n );\n return;\n }\n\n const configPath = path.join(process.cwd(), \"supabase\", \"config.toml\");\n if (!existsSync(configPath)) {\n try {\n run(\"supabase\", [\"init\"]);\n } catch {\n console.warn(\"\\n`supabase init` failed — you can run it yourself.\");\n return;\n }\n }\n if (existsSync(configPath)) patchExposedSchemas(configPath);\n\n if (!push) {\n console.log(\n \"\\nRun `guardrail-init-db --push` (or `supabase db push` yourself) to apply it.\",\n );\n return;\n }\n\n // A local Docker stack takes pending migrations with `migration up`; `db push`\n // is for a linked hosted project.\n const local = isLocalSupabase();\n try {\n run(\"supabase\", local ? [\"migration\", \"up\", \"--local\"] : [\"db\", \"push\"]);\n } catch {\n console.warn(\n local\n ? \"\\n`supabase migration up` failed — is the local stack running? Start it with `supabase start`.\"\n : \"\\n`supabase db push` failed — if this project isn't linked yet, run `supabase \" +\n \"link` first, then `supabase db push`.\",\n );\n }\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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","names":[]}
|
|
@@ -302,6 +302,24 @@ interface GRPasswordResetEvent {
|
|
|
302
302
|
/** "self" for someone who asked from the sign-in bar; otherwise the admin who sent it. */
|
|
303
303
|
requestedBy: string;
|
|
304
304
|
}
|
|
305
|
+
/** One counted attempt: bump `key`'s counter in `name`'s bucket and say whether it is still within `limit` per `windowSeconds`. */
|
|
306
|
+
interface GRRateLimitHit {
|
|
307
|
+
/** Which limit this is, e.g. "login:account". Namespace your counter keys with it. */
|
|
308
|
+
name: string;
|
|
309
|
+
/** Who is being counted: an address, an email, or a user id. */
|
|
310
|
+
key: string;
|
|
311
|
+
limit: number;
|
|
312
|
+
windowSeconds: number;
|
|
313
|
+
}
|
|
314
|
+
type GRRateLimiter = (hit: GRRateLimitHit) => {
|
|
315
|
+
allowed: boolean;
|
|
316
|
+
retryAfterSeconds?: number;
|
|
317
|
+
} | Promise<{
|
|
318
|
+
allowed: boolean;
|
|
319
|
+
retryAfterSeconds?: number;
|
|
320
|
+
}>;
|
|
321
|
+
/** The limits Guardrail applies by default; each can be tuned with `rateLimitRules`. */
|
|
322
|
+
type GRRateLimitName = "login:pair" | "login:account" | "login:ip" | "forgot-password:ip" | "forgot-password:account" | "auth-link:ip" | "invite:user" | "password-reset:user" | "upload:user";
|
|
305
323
|
interface GRConfig {
|
|
306
324
|
db: GRDBAdapter;
|
|
307
325
|
auth: GRAuthAdapter;
|
|
@@ -311,6 +329,58 @@ interface GRConfig {
|
|
|
311
329
|
/** Optional — only needed when a page uses <GRepeater>. */
|
|
312
330
|
repeaters?: GRRepeaterAdapter;
|
|
313
331
|
siteId: string | (() => string | Promise<string>);
|
|
332
|
+
/**
|
|
333
|
+
* The public address of this site (`https://example.com`), which every emailed
|
|
334
|
+
* sign-in and password-reset link points back to. Falls back to the
|
|
335
|
+
* GUARDRAIL_SITE_URL environment variable.
|
|
336
|
+
*
|
|
337
|
+
* Required in production: a link built from the request's Host/Origin headers
|
|
338
|
+
* can be steered at an attacker's domain (anyone can request a reset for any
|
|
339
|
+
* address), so without this, invites and resets refuse to send. In development
|
|
340
|
+
* the request's own address is used.
|
|
341
|
+
*/
|
|
342
|
+
siteUrl?: string | (() => string | Promise<string>);
|
|
343
|
+
/**
|
|
344
|
+
* Counts attempts on the endpoints that can be abused: sign-in, "Forgot
|
|
345
|
+
* password?", the emailed-link exchange, and the admin actions that send mail
|
|
346
|
+
* or create uploads. Called with one `hit` per limit; return whether it is
|
|
347
|
+
* still under its ceiling.
|
|
348
|
+
*
|
|
349
|
+
* The default keeps counters in memory, which only holds on a long-running
|
|
350
|
+
* server — serverless instances don't share memory, so a deployment there
|
|
351
|
+
* should pass a limiter backed by shared state (Upstash/Redis, or your
|
|
352
|
+
* database). Pass `false` to turn limiting off. A limiter that throws blocks
|
|
353
|
+
* the request.
|
|
354
|
+
*/
|
|
355
|
+
rateLimit?: false | GRRateLimiter;
|
|
356
|
+
/**
|
|
357
|
+
* Overrides for individual rate limits, e.g. a larger `login:account` ceiling
|
|
358
|
+
* for a team that signs in from one shared office address. Only the numbers
|
|
359
|
+
* given are changed; anything invalid (not a positive whole number) is
|
|
360
|
+
* ignored and the default stays. The defaults are:
|
|
361
|
+
*
|
|
362
|
+
* login:pair 8 / 15 min one account from one address
|
|
363
|
+
* login:account 40 / 15 min one account from anywhere
|
|
364
|
+
* login:ip 30 / 15 min one address, any account
|
|
365
|
+
* forgot-password:ip 10 / hour, forgot-password:account 5 / hour
|
|
366
|
+
* auth-link:ip 30 / 15 min
|
|
367
|
+
* invite:user 30 / hour, password-reset:user 30 / hour, upload:user 60 / 10 min
|
|
368
|
+
*/
|
|
369
|
+
rateLimitRules?: Partial<Record<GRRateLimitName, {
|
|
370
|
+
limit?: number;
|
|
371
|
+
windowSeconds?: number;
|
|
372
|
+
}>>;
|
|
373
|
+
/**
|
|
374
|
+
* Who is calling, for the per-address rate limits. By default the first
|
|
375
|
+
* `X-Forwarded-For` entry (then `X-Real-IP`), which is right behind a proxy
|
|
376
|
+
* that overwrites the header — Vercel, Cloudflare and most load balancers —
|
|
377
|
+
* and spoofable on a server reached directly, or behind one that only
|
|
378
|
+
* appends. Pass a function when you know better, e.g. to read a header your
|
|
379
|
+
* CDN sets (`CF-Connecting-IP`), or to take the right-most entry. Return
|
|
380
|
+
* undefined to fall back to the default. The per-account limits don't depend
|
|
381
|
+
* on this.
|
|
382
|
+
*/
|
|
383
|
+
clientIp?: (headers: Headers) => string | undefined | Promise<string | undefined>;
|
|
314
384
|
/**
|
|
315
385
|
* How an invitation gets delivered. Guardrail records the pending membership
|
|
316
386
|
* and — when the auth adapter can (createInviteLink) — creates the account and
|
|
@@ -353,4 +423,4 @@ interface GRConfig {
|
|
|
353
423
|
prefetch?: false | GRPrefetch;
|
|
354
424
|
}
|
|
355
425
|
|
|
356
|
-
export type { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget };
|
|
426
|
+
export type { AuthRedirectParams, AuthResult, GRAuthAdapter, GRConfig, GRDBAdapter, GRInviteEvent, GRMembershipAdapter, GRPasswordResetEvent, GRPrefetch, GRRateLimitHit, GRRateLimitName, GRRateLimiter, GRRepeaterAdapter, GRSession, GRStorageAdapter, GRStorageRow, GRUploadTarget };
|
package/dist/core/config.d.ts
CHANGED
|
@@ -29,6 +29,17 @@ declare function getPrefetch(): {
|
|
|
29
29
|
} | null;
|
|
30
30
|
declare function getInviteHandler(): GRConfig["onInvite"];
|
|
31
31
|
declare function getPasswordResetHandler(): GRConfig["onPasswordReset"];
|
|
32
|
+
/** undefined: use the built-in in-memory limiter. false: limiting is off. */
|
|
33
|
+
declare function getRateLimiter(): GRConfig["rateLimit"];
|
|
34
|
+
declare function getRateLimitRules(): GRConfig["rateLimitRules"];
|
|
35
|
+
declare function getClientIpResolver(): GRConfig["clientIp"];
|
|
32
36
|
declare function resolveSiteId(): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* The origin emailed links must point at, from `siteUrl` / GUARDRAIL_SITE_URL.
|
|
39
|
+
* Null when none is set; the caller decides whether a request-derived address
|
|
40
|
+
* is acceptable (development only). Throws on a value that isn't an http(s)
|
|
41
|
+
* address, so a typo fails loudly instead of mailing out dead links.
|
|
42
|
+
*/
|
|
43
|
+
declare function resolveSiteUrl(): Promise<string | null>;
|
|
33
44
|
|
|
34
|
-
export { DEFAULT_PREFETCH_LIMIT, configureGuardrail, getAuth, getDB, getInviteHandler, getMembers, getPasswordResetHandler, getPrefetch, getRepeaters, getStorage, resolveSiteId };
|
|
45
|
+
export { DEFAULT_PREFETCH_LIMIT, configureGuardrail, getAuth, getClientIpResolver, getDB, getInviteHandler, getMembers, getPasswordResetHandler, getPrefetch, getRateLimitRules, getRateLimiter, getRepeaters, getStorage, resolveSiteId, resolveSiteUrl };
|
package/dist/core/config.js
CHANGED
|
@@ -41,21 +41,49 @@ function getInviteHandler() {
|
|
|
41
41
|
function getPasswordResetHandler() {
|
|
42
42
|
return _config?.onPasswordReset;
|
|
43
43
|
}
|
|
44
|
+
function getRateLimiter() {
|
|
45
|
+
return _config?.rateLimit;
|
|
46
|
+
}
|
|
47
|
+
function getRateLimitRules() {
|
|
48
|
+
return _config?.rateLimitRules;
|
|
49
|
+
}
|
|
50
|
+
function getClientIpResolver() {
|
|
51
|
+
return _config?.clientIp;
|
|
52
|
+
}
|
|
44
53
|
async function resolveSiteId() {
|
|
45
54
|
const sid = _config?.siteId ?? "default";
|
|
46
55
|
return typeof sid === "function" ? await sid() : sid || "default";
|
|
47
56
|
}
|
|
57
|
+
async function resolveSiteUrl() {
|
|
58
|
+
const configured = _config?.siteUrl ?? process.env.GUARDRAIL_SITE_URL;
|
|
59
|
+
const raw = typeof configured === "function" ? await configured() : configured;
|
|
60
|
+
if (!raw) return null;
|
|
61
|
+
let url;
|
|
62
|
+
try {
|
|
63
|
+
url = new URL(raw);
|
|
64
|
+
} catch {
|
|
65
|
+
throw new Error(`Guardrail: siteUrl "${raw}" isn't a valid URL (expected https://example.com).`);
|
|
66
|
+
}
|
|
67
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
68
|
+
throw new Error(`Guardrail: siteUrl must be an http(s) address, got "${raw}".`);
|
|
69
|
+
}
|
|
70
|
+
return url.origin;
|
|
71
|
+
}
|
|
48
72
|
export {
|
|
49
73
|
DEFAULT_PREFETCH_LIMIT,
|
|
50
74
|
configureGuardrail,
|
|
51
75
|
getAuth,
|
|
76
|
+
getClientIpResolver,
|
|
52
77
|
getDB,
|
|
53
78
|
getInviteHandler,
|
|
54
79
|
getMembers,
|
|
55
80
|
getPasswordResetHandler,
|
|
56
81
|
getPrefetch,
|
|
82
|
+
getRateLimitRules,
|
|
83
|
+
getRateLimiter,
|
|
57
84
|
getRepeaters,
|
|
58
85
|
getStorage,
|
|
59
|
-
resolveSiteId
|
|
86
|
+
resolveSiteId,
|
|
87
|
+
resolveSiteUrl
|
|
60
88
|
};
|
|
61
89
|
//# sourceMappingURL=config.js.map
|
package/dist/core/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/config.ts"],"sourcesContent":["import type {\n GRConfig,\n GRAuthAdapter,\n GRDBAdapter,\n GRMembershipAdapter,\n GRRepeaterAdapter,\n GRStorageAdapter,\n} from \"./adapter-types\";\n\nlet _config: GRConfig | null = null;\n\n/**\n * Registers the adapters Guardrail should use.\n *\n * Idempotent, and re-applied on every render by withGuardrail: Next.js does not\n * guarantee one shared module instance across the SSR graph, the server-action\n * graph, and the workers used for static generation, so config registered once\n * at startup is simply absent in some of them.\n *\n * Core has no knowledge of any particular backend — there is no default and no\n * fallback. An app names its adapters explicitly or Guardrail refuses to run.\n */\nexport function configureGuardrail(config: GRConfig): void {\n _config = config;\n}\n\nfunction required<K extends keyof GRConfig>(key: K): GRConfig[K] {\n const value = _config?.[key];\n if (!value) {\n throw new Error(\n `Guardrail: no ${String(key)} adapter configured. Pass one to ` +\n `withGuardrail(Layout, { db, auth, members }).`,\n );\n }\n return value;\n}\n\nexport function getDB(): GRDBAdapter {\n return required(\"db\") as GRDBAdapter;\n}\n\nexport function getAuth(): GRAuthAdapter {\n return required(\"auth\") as GRAuthAdapter;\n}\n\nexport function getMembers(): GRMembershipAdapter {\n return required(\"members\") as GRMembershipAdapter;\n}\n\n/** Optional — sites without editable images need no storage. */\nexport function getStorage(): GRStorageAdapter | undefined {\n return _config?.storage;\n}\n\n/** Throws with a clear message if no page on this site uses <GRepeater>. */\nexport function getRepeaters(): GRRepeaterAdapter {\n return required(\"repeaters\") as GRRepeaterAdapter;\n}\n\nexport const DEFAULT_PREFETCH_LIMIT = 500;\n\n/** Normalized preload settings, or null when preloading is off. */\nexport function getPrefetch(): { prefix?: string[]; limit: number } | null {\n const p = _config?.prefetch;\n if (p === false) return null;\n return {\n ...(p?.prefix?.length ? { prefix: p.prefix } : {}),\n limit: p?.limit ?? DEFAULT_PREFETCH_LIMIT,\n };\n}\n\nexport function getInviteHandler(): GRConfig[\"onInvite\"] {\n return _config?.onInvite;\n}\n\nexport function getPasswordResetHandler(): GRConfig[\"onPasswordReset\"] {\n return _config?.onPasswordReset;\n}\n\nexport async function resolveSiteId(): Promise<string> {\n const sid = _config?.siteId ?? \"default\";\n return typeof sid === \"function\" ? await sid() : sid || \"default\";\n}\n"],"mappings":"AASA,IAAI,UAA2B;AAaxB,SAAS,mBAAmB,QAAwB;AACzD,YAAU;AACZ;AAEA,SAAS,SAAmC,KAAqB;AAC/D,QAAM,QAAQ,UAAU,GAAG;AAC3B,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR,iBAAiB,OAAO,GAAG,CAAC;AAAA,IAE9B;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,QAAqB;AACnC,SAAO,SAAS,IAAI;AACtB;AAEO,SAAS,UAAyB;AACvC,SAAO,SAAS,MAAM;AACxB;AAEO,SAAS,aAAkC;AAChD,SAAO,SAAS,SAAS;AAC3B;AAGO,SAAS,aAA2C;AACzD,SAAO,SAAS;AAClB;AAGO,SAAS,eAAkC;AAChD,SAAO,SAAS,WAAW;AAC7B;AAEO,MAAM,yBAAyB;AAG/B,SAAS,cAA2D;AACzE,QAAM,IAAI,SAAS;AACnB,MAAI,MAAM,MAAO,QAAO;AACxB,SAAO;AAAA,IACL,GAAI,GAAG,QAAQ,SAAS,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IAChD,OAAO,GAAG,SAAS;AAAA,EACrB;AACF;AAEO,SAAS,mBAAyC;AACvD,SAAO,SAAS;AAClB;AAEO,SAAS,0BAAuD;AACrE,SAAO,SAAS;AAClB;AAEA,eAAsB,gBAAiC;AACrD,QAAM,MAAM,SAAS,UAAU;AAC/B,SAAO,OAAO,QAAQ,aAAa,MAAM,IAAI,IAAI,OAAO;AAC1D;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/config.ts"],"sourcesContent":["import type {\n GRConfig,\n GRAuthAdapter,\n GRDBAdapter,\n GRMembershipAdapter,\n GRRepeaterAdapter,\n GRStorageAdapter,\n} from \"./adapter-types\";\n\nlet _config: GRConfig | null = null;\n\n/**\n * Registers the adapters Guardrail should use.\n *\n * Idempotent, and re-applied on every render by withGuardrail: Next.js does not\n * guarantee one shared module instance across the SSR graph, the server-action\n * graph, and the workers used for static generation, so config registered once\n * at startup is simply absent in some of them.\n *\n * Core has no knowledge of any particular backend — there is no default and no\n * fallback. An app names its adapters explicitly or Guardrail refuses to run.\n */\nexport function configureGuardrail(config: GRConfig): void {\n _config = config;\n}\n\nfunction required<K extends keyof GRConfig>(key: K): GRConfig[K] {\n const value = _config?.[key];\n if (!value) {\n throw new Error(\n `Guardrail: no ${String(key)} adapter configured. Pass one to ` +\n `withGuardrail(Layout, { db, auth, members }).`,\n );\n }\n return value;\n}\n\nexport function getDB(): GRDBAdapter {\n return required(\"db\") as GRDBAdapter;\n}\n\nexport function getAuth(): GRAuthAdapter {\n return required(\"auth\") as GRAuthAdapter;\n}\n\nexport function getMembers(): GRMembershipAdapter {\n return required(\"members\") as GRMembershipAdapter;\n}\n\n/** Optional — sites without editable images need no storage. */\nexport function getStorage(): GRStorageAdapter | undefined {\n return _config?.storage;\n}\n\n/** Throws with a clear message if no page on this site uses <GRepeater>. */\nexport function getRepeaters(): GRRepeaterAdapter {\n return required(\"repeaters\") as GRRepeaterAdapter;\n}\n\nexport const DEFAULT_PREFETCH_LIMIT = 500;\n\n/** Normalized preload settings, or null when preloading is off. */\nexport function getPrefetch(): { prefix?: string[]; limit: number } | null {\n const p = _config?.prefetch;\n if (p === false) return null;\n return {\n ...(p?.prefix?.length ? { prefix: p.prefix } : {}),\n limit: p?.limit ?? DEFAULT_PREFETCH_LIMIT,\n };\n}\n\nexport function getInviteHandler(): GRConfig[\"onInvite\"] {\n return _config?.onInvite;\n}\n\nexport function getPasswordResetHandler(): GRConfig[\"onPasswordReset\"] {\n return _config?.onPasswordReset;\n}\n\n/** undefined: use the built-in in-memory limiter. false: limiting is off. */\nexport function getRateLimiter(): GRConfig[\"rateLimit\"] {\n return _config?.rateLimit;\n}\n\nexport function getRateLimitRules(): GRConfig[\"rateLimitRules\"] {\n return _config?.rateLimitRules;\n}\n\nexport function getClientIpResolver(): GRConfig[\"clientIp\"] {\n return _config?.clientIp;\n}\n\nexport async function resolveSiteId(): Promise<string> {\n const sid = _config?.siteId ?? \"default\";\n return typeof sid === \"function\" ? await sid() : sid || \"default\";\n}\n\n/**\n * The origin emailed links must point at, from `siteUrl` / GUARDRAIL_SITE_URL.\n * Null when none is set; the caller decides whether a request-derived address\n * is acceptable (development only). Throws on a value that isn't an http(s)\n * address, so a typo fails loudly instead of mailing out dead links.\n */\nexport async function resolveSiteUrl(): Promise<string | null> {\n const configured = _config?.siteUrl ?? process.env.GUARDRAIL_SITE_URL;\n const raw = typeof configured === \"function\" ? await configured() : configured;\n if (!raw) return null;\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw new Error(`Guardrail: siteUrl \"${raw}\" isn't a valid URL (expected https://example.com).`);\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n throw new Error(`Guardrail: siteUrl must be an http(s) address, got \"${raw}\".`);\n }\n return url.origin;\n}\n"],"mappings":"AASA,IAAI,UAA2B;AAaxB,SAAS,mBAAmB,QAAwB;AACzD,YAAU;AACZ;AAEA,SAAS,SAAmC,KAAqB;AAC/D,QAAM,QAAQ,UAAU,GAAG;AAC3B,MAAI,CAAC,OAAO;AACV,UAAM,IAAI;AAAA,MACR,iBAAiB,OAAO,GAAG,CAAC;AAAA,IAE9B;AAAA,EACF;AACA,SAAO;AACT;AAEO,SAAS,QAAqB;AACnC,SAAO,SAAS,IAAI;AACtB;AAEO,SAAS,UAAyB;AACvC,SAAO,SAAS,MAAM;AACxB;AAEO,SAAS,aAAkC;AAChD,SAAO,SAAS,SAAS;AAC3B;AAGO,SAAS,aAA2C;AACzD,SAAO,SAAS;AAClB;AAGO,SAAS,eAAkC;AAChD,SAAO,SAAS,WAAW;AAC7B;AAEO,MAAM,yBAAyB;AAG/B,SAAS,cAA2D;AACzE,QAAM,IAAI,SAAS;AACnB,MAAI,MAAM,MAAO,QAAO;AACxB,SAAO;AAAA,IACL,GAAI,GAAG,QAAQ,SAAS,EAAE,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,IAChD,OAAO,GAAG,SAAS;AAAA,EACrB;AACF;AAEO,SAAS,mBAAyC;AACvD,SAAO,SAAS;AAClB;AAEO,SAAS,0BAAuD;AACrE,SAAO,SAAS;AAClB;AAGO,SAAS,iBAAwC;AACtD,SAAO,SAAS;AAClB;AAEO,SAAS,oBAAgD;AAC9D,SAAO,SAAS;AAClB;AAEO,SAAS,sBAA4C;AAC1D,SAAO,SAAS;AAClB;AAEA,eAAsB,gBAAiC;AACrD,QAAM,MAAM,SAAS,UAAU;AAC/B,SAAO,OAAO,QAAQ,aAAa,MAAM,IAAI,IAAI,OAAO;AAC1D;AAQA,eAAsB,iBAAyC;AAC7D,QAAM,aAAa,SAAS,WAAW,QAAQ,IAAI;AACnD,QAAM,MAAM,OAAO,eAAe,aAAa,MAAM,WAAW,IAAI;AACpE,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,GAAG;AAAA,EACnB,QAAQ;AACN,UAAM,IAAI,MAAM,uBAAuB,GAAG,qDAAqD;AAAA,EACjG;AACA,MAAI,IAAI,aAAa,YAAY,IAAI,aAAa,SAAS;AACzD,UAAM,IAAI,MAAM,uDAAuD,GAAG,IAAI;AAAA,EAChF;AACA,SAAO,IAAI;AACb;","names":[]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Guardrail needs from a site's Content-Security-Policy, so a site can
|
|
3
|
+
* adopt a strict policy without trial and error.
|
|
4
|
+
*
|
|
5
|
+
* The editing chrome ships as a stylesheet (not inline styles) and renders no
|
|
6
|
+
* inline scripts, so it needs no 'unsafe-inline' and no nonce. The only things
|
|
7
|
+
* a strict policy has to allow for it are the two places the *browser* talks to
|
|
8
|
+
* or loads from Supabase:
|
|
9
|
+
*
|
|
10
|
+
* connect-src — the signed-upload PUT that sends an image straight to storage
|
|
11
|
+
* img-src — public storage URLs, shown in the media picker's thumbnails
|
|
12
|
+
*
|
|
13
|
+
* Everything else (reads, writes, sign-in) goes through the site's own server
|
|
14
|
+
* actions, i.e. 'self'. Pure, so it can be called from next.config.ts or a
|
|
15
|
+
* proxy/middleware that builds the header per request.
|
|
16
|
+
*/
|
|
17
|
+
type GRCspDirectives = Record<string, string[]>;
|
|
18
|
+
interface GRCspOptions {
|
|
19
|
+
/** Your Supabase project URL (SUPABASE_URL). Omit if the site has no editable images. */
|
|
20
|
+
supabaseUrl?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The directives Guardrail needs, as sources to merge into your own policy:
|
|
24
|
+
*
|
|
25
|
+
* const gr = guardrailCspDirectives({ supabaseUrl: process.env.SUPABASE_URL });
|
|
26
|
+
* // gr["connect-src"] -> ["'self'", "https://abc.supabase.co"]
|
|
27
|
+
*/
|
|
28
|
+
declare function guardrailCspDirectives(options?: GRCspOptions): GRCspDirectives;
|
|
29
|
+
/** Serializes directives into a header value: `name a b; name2 c`. */
|
|
30
|
+
declare function cspString(directives: GRCspDirectives): string;
|
|
31
|
+
|
|
32
|
+
export { type GRCspDirectives, type GRCspOptions, cspString, guardrailCspDirectives };
|
package/dist/core/csp.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
function guardrailCspDirectives(options = {}) {
|
|
2
|
+
const origin = options.supabaseUrl ? supabaseOrigin(options.supabaseUrl) : void 0;
|
|
3
|
+
const withStorage = (...base) => origin ? [...base, origin] : base;
|
|
4
|
+
return {
|
|
5
|
+
"style-src": ["'self'"],
|
|
6
|
+
"img-src": withStorage("'self'"),
|
|
7
|
+
"connect-src": withStorage("'self'"),
|
|
8
|
+
"frame-ancestors": ["'self'"]
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function cspString(directives) {
|
|
12
|
+
return Object.entries(directives).filter(([, sources]) => sources.length > 0).map(([name, sources]) => `${name} ${[...new Set(sources)].join(" ")}`).join("; ");
|
|
13
|
+
}
|
|
14
|
+
function supabaseOrigin(url) {
|
|
15
|
+
try {
|
|
16
|
+
const u = new URL(url);
|
|
17
|
+
return u.protocol === "http:" || u.protocol === "https:" ? u.origin : void 0;
|
|
18
|
+
} catch {
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
cspString,
|
|
24
|
+
guardrailCspDirectives
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=csp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/core/csp.ts"],"sourcesContent":["/**\n * What Guardrail needs from a site's Content-Security-Policy, so a site can\n * adopt a strict policy without trial and error.\n *\n * The editing chrome ships as a stylesheet (not inline styles) and renders no\n * inline scripts, so it needs no 'unsafe-inline' and no nonce. The only things\n * a strict policy has to allow for it are the two places the *browser* talks to\n * or loads from Supabase:\n *\n * connect-src — the signed-upload PUT that sends an image straight to storage\n * img-src — public storage URLs, shown in the media picker's thumbnails\n *\n * Everything else (reads, writes, sign-in) goes through the site's own server\n * actions, i.e. 'self'. Pure, so it can be called from next.config.ts or a\n * proxy/middleware that builds the header per request.\n */\n\nexport type GRCspDirectives = Record<string, string[]>;\n\nexport interface GRCspOptions {\n /** Your Supabase project URL (SUPABASE_URL). Omit if the site has no editable images. */\n supabaseUrl?: string;\n}\n\n/**\n * The directives Guardrail needs, as sources to merge into your own policy:\n *\n * const gr = guardrailCspDirectives({ supabaseUrl: process.env.SUPABASE_URL });\n * // gr[\"connect-src\"] -> [\"'self'\", \"https://abc.supabase.co\"]\n */\nexport function guardrailCspDirectives(options: GRCspOptions = {}): GRCspDirectives {\n const origin = options.supabaseUrl ? supabaseOrigin(options.supabaseUrl) : undefined;\n const withStorage = (...base: string[]) => (origin ? [...base, origin] : base);\n return {\n \"style-src\": [\"'self'\"],\n \"img-src\": withStorage(\"'self'\"),\n \"connect-src\": withStorage(\"'self'\"),\n \"frame-ancestors\": [\"'self'\"],\n };\n}\n\n/** Serializes directives into a header value: `name a b; name2 c`. */\nexport function cspString(directives: GRCspDirectives): string {\n return Object.entries(directives)\n .filter(([, sources]) => sources.length > 0)\n .map(([name, sources]) => `${name} ${[...new Set(sources)].join(\" \")}`)\n .join(\"; \");\n}\n\nfunction supabaseOrigin(url: string): string | undefined {\n try {\n const u = new URL(url);\n return u.protocol === \"http:\" || u.protocol === \"https:\" ? u.origin : undefined;\n } catch {\n return undefined;\n }\n}\n"],"mappings":"AA8BO,SAAS,uBAAuB,UAAwB,CAAC,GAAoB;AAClF,QAAM,SAAS,QAAQ,cAAc,eAAe,QAAQ,WAAW,IAAI;AAC3E,QAAM,cAAc,IAAI,SAAoB,SAAS,CAAC,GAAG,MAAM,MAAM,IAAI;AACzE,SAAO;AAAA,IACL,aAAa,CAAC,QAAQ;AAAA,IACtB,WAAW,YAAY,QAAQ;AAAA,IAC/B,eAAe,YAAY,QAAQ;AAAA,IACnC,mBAAmB,CAAC,QAAQ;AAAA,EAC9B;AACF;AAGO,SAAS,UAAU,YAAqC;AAC7D,SAAO,OAAO,QAAQ,UAAU,EAC7B,OAAO,CAAC,CAAC,EAAE,OAAO,MAAM,QAAQ,SAAS,CAAC,EAC1C,IAAI,CAAC,CAAC,MAAM,OAAO,MAAM,GAAG,IAAI,IAAI,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,EACrE,KAAK,IAAI;AACd;AAEA,SAAS,eAAe,KAAiC;AACvD,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,GAAG;AACrB,WAAO,EAAE,aAAa,WAAW,EAAE,aAAa,WAAW,EAAE,SAAS;AAAA,EACxE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { OverrideType, GRRole, OverrideValue } from './types.js';
|
|
2
|
+
import 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Checks on field values before they are stored and again before they are
|
|
6
|
+
* rendered. Pure and dependency-free, so the same rules run on the server (what
|
|
7
|
+
* may be written) and in the renderer (what rows already in the table may do).
|
|
8
|
+
*
|
|
9
|
+
* Why it matters: an editor types these, a visitor's browser then acts on them.
|
|
10
|
+
* A link whose href is `javascript:...` is a script that runs for whoever
|
|
11
|
+
* clicks it, so a low-privilege editor account must not be able to store one.
|
|
12
|
+
*/
|
|
13
|
+
declare const OVERRIDE_TYPES: readonly OverrideType[];
|
|
14
|
+
declare const LIMITS: {
|
|
15
|
+
readonly pathKey: 200;
|
|
16
|
+
readonly text: 20000;
|
|
17
|
+
readonly url: 2048;
|
|
18
|
+
readonly alt: 1000;
|
|
19
|
+
readonly dimension: 20000;
|
|
20
|
+
};
|
|
21
|
+
/** The link, if it is a relative path, an anchor, or http(s)/mailto/tel; otherwise null. */
|
|
22
|
+
declare function safeHref(raw: unknown): string | null;
|
|
23
|
+
/** The image address, if it is relative or http(s); otherwise null. */
|
|
24
|
+
declare function safeImageSrc(raw: unknown): string | null;
|
|
25
|
+
declare function validPathKey(raw: unknown): string | null;
|
|
26
|
+
type Checked = {
|
|
27
|
+
ok: true;
|
|
28
|
+
value: OverrideValue;
|
|
29
|
+
} | {
|
|
30
|
+
ok: false;
|
|
31
|
+
error: string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Validates a value from the browser and returns a clean copy: only known
|
|
35
|
+
* fields, only the right shapes, bounded sizes. Anything else is an error, not
|
|
36
|
+
* silently dropped, so a bad write fails visibly.
|
|
37
|
+
*/
|
|
38
|
+
declare function validateOverrideValue(input: unknown): Checked;
|
|
39
|
+
/** A normalized (trimmed, lower-cased) address, or null if it isn't a plausible one. RFC 5321 caps an address at 254 characters. */
|
|
40
|
+
declare function cleanEmail(raw: unknown): string | null;
|
|
41
|
+
declare function isRole(raw: unknown): raw is GRRole;
|
|
42
|
+
/** An identifier from the browser (a repeater or item id): a bounded string without control characters. */
|
|
43
|
+
declare function validId(raw: unknown): string | null;
|
|
44
|
+
/** A list of ids: an array of valid ids, no longer than `max`. */
|
|
45
|
+
declare function validIdList(raw: unknown, max?: number): string[] | null;
|
|
46
|
+
|
|
47
|
+
export { LIMITS, OVERRIDE_TYPES, cleanEmail, isRole, safeHref, safeImageSrc, validId, validIdList, validPathKey, validateOverrideValue };
|