@rebasepro/cli 0.25.0 → 0.26.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.
Files changed (190) hide show
  1. package/README.md +2 -1
  2. package/bin/rebase.js +143 -4
  3. package/dist/{agent-setup-B5AKHOgt.js → agent-setup-D9mHRZ5R.js} +125 -3
  4. package/dist/agent-setup-D9mHRZ5R.js.map +1 -0
  5. package/dist/{api-keys-Tm7zvAPe.js → api-keys-CyJ26KEJ.js} +41 -10
  6. package/dist/api-keys-CyJ26KEJ.js.map +1 -0
  7. package/dist/{apps-BsU_ILbP.js → apps-CnLDV5b6.js} +6 -7
  8. package/dist/{apps-BsU_ILbP.js.map → apps-CnLDV5b6.js.map} +1 -1
  9. package/dist/{atlas-dev-database-OWtqnG7W.js → atlas-dev-database-BODqdCWt.js} +4 -4
  10. package/dist/{atlas-dev-database-OWtqnG7W.js.map → atlas-dev-database-BODqdCWt.js.map} +1 -1
  11. package/dist/{auth-Vl2hFKXP.js → auth-dII2jCO1.js} +10 -8
  12. package/dist/auth-dII2jCO1.js.map +1 -0
  13. package/dist/{branch-pointer-9DnwkFJJ.js → branch-pointer-BQUF6lYa.js} +1 -1
  14. package/dist/{build-BBgT8EJZ.js → build-Cza7ZQrp.js} +6 -7
  15. package/dist/{build-BBgT8EJZ.js.map → build-Cza7ZQrp.js.map} +1 -1
  16. package/dist/{bundle-DtCCCr4w.js → bundle-C6yEtMJ-.js} +53 -5
  17. package/dist/bundle-C6yEtMJ-.js.map +1 -0
  18. package/dist/bundle.d.ts +14 -0
  19. package/dist/{cloud-BpH0_bmV.js → cloud-BdnmqQaZ.js} +8528 -4342
  20. package/dist/cloud-BdnmqQaZ.js.map +1 -0
  21. package/dist/collections-dir-DlvDhime.js +69 -0
  22. package/dist/collections-dir-DlvDhime.js.map +1 -0
  23. package/dist/commands/agent-setup.d.ts +58 -0
  24. package/dist/commands/api-keys.d.ts +13 -0
  25. package/dist/commands/cloud/activity.d.ts +25 -0
  26. package/dist/commands/cloud/app-key.d.ts +18 -0
  27. package/dist/commands/cloud/auth.d.ts +1 -0
  28. package/dist/commands/cloud/billing-invoices.d.ts +39 -0
  29. package/dist/commands/cloud/claim.d.ts +7 -0
  30. package/dist/commands/cloud/context.d.ts +115 -6
  31. package/dist/commands/cloud/deploy-summary.d.ts +162 -0
  32. package/dist/commands/cloud/deploy.d.ts +64 -0
  33. package/dist/commands/cloud/env.d.ts +24 -0
  34. package/dist/commands/cloud/index.d.ts +13 -1
  35. package/dist/commands/cloud/init.d.ts +143 -0
  36. package/dist/commands/cloud/login-flow.d.ts +51 -0
  37. package/dist/commands/cloud/notifications.d.ts +28 -0
  38. package/dist/commands/cloud/orgs.d.ts +13 -0
  39. package/dist/commands/cloud/previews.d.ts +123 -0
  40. package/dist/commands/cloud/resources.d.ts +68 -14
  41. package/dist/commands/cloud/runtime-logs.d.ts +176 -0
  42. package/dist/commands/cloud/service-key.d.ts +15 -0
  43. package/dist/commands/cloud/token-capabilities.d.ts +61 -8
  44. package/dist/commands/cloud/tokens.d.ts +18 -3
  45. package/dist/commands/cloud/usage.d.ts +63 -0
  46. package/dist/commands/dev.d.ts +22 -0
  47. package/dist/commands/doctor.d.ts +1 -1
  48. package/dist/commands/generate-view.d.ts +94 -0
  49. package/dist/commands/generate_sdk.d.ts +19 -2
  50. package/dist/commands/init.d.ts +20 -0
  51. package/dist/commands/skills.d.ts +4 -3
  52. package/dist/commands/start.d.ts +10 -0
  53. package/dist/commands/storage.d.ts +4 -0
  54. package/dist/commands/upgrade.d.ts +12 -0
  55. package/dist/commands/verify.d.ts +96 -0
  56. package/dist/{consent-ClRHmcxf.js → consent-BD7F29T3.js} +2 -2
  57. package/dist/{consent-ClRHmcxf.js.map → consent-BD7F29T3.js.map} +1 -1
  58. package/dist/{console-CEGm0XNc.js → console-BXB6-8GT.js} +12 -6
  59. package/dist/console-BXB6-8GT.js.map +1 -0
  60. package/dist/{context-CyflUrKg.js → context-GUxkZm-S.js} +482 -26
  61. package/dist/context-GUxkZm-S.js.map +1 -0
  62. package/dist/{daemon-CknkDjd6.js → daemon-Cp7oNXth.js} +160 -5
  63. package/dist/daemon-Cp7oNXth.js.map +1 -0
  64. package/dist/{daemon-entry-CbLRrgzy.js → daemon-entry-zOC1revO.js} +6 -3
  65. package/dist/daemon-entry-zOC1revO.js.map +1 -0
  66. package/dist/{db-CRNpA5mH.js → db-KzlqddGD.js} +101 -54
  67. package/dist/db-KzlqddGD.js.map +1 -0
  68. package/dist/{derive-CsoFGAnK.js → derive-ae9UOoOB.js} +2 -2
  69. package/dist/{derive-CsoFGAnK.js.map → derive-ae9UOoOB.js.map} +1 -1
  70. package/dist/{dev-C0pbMmhS.js → dev-DTqVMgB3.js} +231 -44
  71. package/dist/dev-DTqVMgB3.js.map +1 -0
  72. package/dist/dev-db/daemon.d.ts +41 -0
  73. package/dist/dev-db/pull.d.ts +33 -5
  74. package/dist/dev-db/state.d.ts +9 -0
  75. package/dist/{dev-preflight-unHpiYO1.js → dev-preflight-Cxv3TiD1.js} +73 -27
  76. package/dist/dev-preflight-Cxv3TiD1.js.map +1 -0
  77. package/dist/{dev-server-B1UYifZJ.js → dev-server-OJsUGZEB.js} +14 -3
  78. package/dist/dev-server-OJsUGZEB.js.map +1 -0
  79. package/dist/{doctor-CATRdWm6.js → doctor-BRWOlqeG.js} +14 -12
  80. package/dist/doctor-BRWOlqeG.js.map +1 -0
  81. package/dist/{eject-DuasTr-F.js → eject-DABcDnPN.js} +3 -4
  82. package/dist/{eject-DuasTr-F.js.map → eject-DABcDnPN.js.map} +1 -1
  83. package/dist/{fold-static-hDHtDfTq.js → fold-static-BnAK-anQ.js} +3 -3
  84. package/dist/{fold-static-hDHtDfTq.js.map → fold-static-BnAK-anQ.js.map} +1 -1
  85. package/dist/generate-view-IFwCADvb.js +662 -0
  86. package/dist/generate-view-IFwCADvb.js.map +1 -0
  87. package/dist/{generate_sdk-Cvg5jjJF.js → generate_sdk-BvcWvoUh.js} +49 -13
  88. package/dist/generate_sdk-BvcWvoUh.js.map +1 -0
  89. package/dist/index.es.js +58 -40
  90. package/dist/index.es.js.map +1 -1
  91. package/dist/{init-BWAwHPtz.js → init-CaU6GhwI.js} +50 -62
  92. package/dist/init-CaU6GhwI.js.map +1 -0
  93. package/dist/invoked-command-CG_rIa_o.js +55 -0
  94. package/dist/invoked-command-CG_rIa_o.js.map +1 -0
  95. package/dist/last-dev-database-DpW5dfoX.js +2 -0
  96. package/dist/{manifest-DRp0UVTj.js → manifest-BtzDy7KQ.js} +36 -3
  97. package/dist/{manifest-DRp0UVTj.js.map → manifest-BtzDy7KQ.js.map} +1 -1
  98. package/dist/{normalize-imports-BoBVh5tU.js → normalize-imports-D0zXlM6g.js} +3 -3
  99. package/dist/{normalize-imports-BoBVh5tU.js.map → normalize-imports-D0zXlM6g.js.map} +1 -1
  100. package/dist/{path-flags-BFgG9V6y.js → path-flags-DNvtEi2-.js} +5 -4
  101. package/dist/path-flags-DNvtEi2-.js.map +1 -0
  102. package/dist/{prepare-DdnU70z8.js → prepare-CTw9NEkh.js} +5 -4
  103. package/dist/{prepare-DdnU70z8.js.map → prepare-CTw9NEkh.js.map} +1 -1
  104. package/dist/project-CasK56vN.js +701 -0
  105. package/dist/project-CasK56vN.js.map +1 -0
  106. package/dist/{pull-C3PXduzl.js → pull-DDMpsZ5r.js} +131 -24
  107. package/dist/pull-DDMpsZ5r.js.map +1 -0
  108. package/dist/refusal-ReeeGP8a.js +26 -0
  109. package/dist/refusal-ReeeGP8a.js.map +1 -0
  110. package/dist/resolve-D647F1LA.js +2 -0
  111. package/dist/{resources-BdNRDEAy.js → resources-CPSC5Kcu.js} +4 -5
  112. package/dist/{resources-BdNRDEAy.js.map → resources-CPSC5Kcu.js.map} +1 -1
  113. package/dist/{schema-B0-p6T1O.js → schema-BSXwZSiI.js} +9 -8
  114. package/dist/schema-BSXwZSiI.js.map +1 -0
  115. package/dist/{skills-DfnTeyRz.js → skills-BU2flMOy.js} +52 -8
  116. package/dist/skills-BU2flMOy.js.map +1 -0
  117. package/dist/{start-BmEZkS0n.js → start-mVb6upwY.js} +20 -7
  118. package/dist/start-mVb6upwY.js.map +1 -0
  119. package/dist/{state-CCB-FAQ9.js → state-CYdBvnwA.js} +3 -2
  120. package/dist/state-CYdBvnwA.js.map +1 -0
  121. package/dist/static-syntax-check.d.ts +64 -0
  122. package/dist/{status-BJ26yZOo.js → status-BREvtzJm.js} +8 -9
  123. package/dist/{status-BJ26yZOo.js.map → status-BREvtzJm.js.map} +1 -1
  124. package/dist/{storage-DKPUBTWG.js → storage-DtOXGflp.js} +159 -16
  125. package/dist/storage-DtOXGflp.js.map +1 -0
  126. package/dist/{telemetry-EMFRVRQi.js → telemetry-CgwJPVMO.js} +2 -2
  127. package/dist/{telemetry-EMFRVRQi.js.map → telemetry-CgwJPVMO.js.map} +1 -1
  128. package/dist/{telemetry-m9czTNGR.js → telemetry-DwK5RQJl.js} +4 -4
  129. package/dist/{telemetry-m9czTNGR.js.map → telemetry-DwK5RQJl.js.map} +1 -1
  130. package/dist/{upgrade-DjihWuZL.js → upgrade-B7sVwL8S.js} +26 -4
  131. package/dist/{upgrade-DjihWuZL.js.map → upgrade-B7sVwL8S.js.map} +1 -1
  132. package/dist/utils/args.d.ts +32 -3
  133. package/dist/utils/boot-retry.d.ts +41 -0
  134. package/dist/utils/collections-dir.d.ts +14 -0
  135. package/dist/utils/dev-preflight.d.ts +34 -1
  136. package/dist/utils/dev-server.d.ts +11 -0
  137. package/dist/utils/path-flags.d.ts +2 -1
  138. package/dist/utils/project.d.ts +5 -1
  139. package/dist/utils/tool-stdio.d.ts +1 -1
  140. package/dist/utils/unknown-command.d.ts +4 -0
  141. package/dist/verify-D7UsV_qe.js +730 -0
  142. package/dist/verify-D7UsV_qe.js.map +1 -0
  143. package/package.json +9 -7
  144. package/templates/eject/backend/src/index.ts +9 -1
  145. package/templates/generated-schemas/ecommerce.ts +3 -0
  146. package/templates/overlays/baas/ai-instructions.md +13 -0
  147. package/templates/resource-graphs/blank.json +25 -0
  148. package/templates/resource-graphs/blog.json +28 -0
  149. package/templates/resource-graphs/ecommerce.json +28 -0
  150. package/templates/resource-graphs/headless.json +22 -0
  151. package/templates/template/.claude/hooks/post-edit-collection.sh +99 -0
  152. package/templates/template/.claude/settings.json +17 -0
  153. package/templates/template/.env.example +15 -5
  154. package/templates/template/ai-instructions.md +21 -1
  155. package/templates/template/config/collections/presets/ecommerce/index.ts +23 -0
  156. package/templates/template/docker-compose.yml +4 -1
  157. package/templates/template/frontend/package.json +1 -0
  158. package/templates/template/frontend/src/App.tsx +10 -9
  159. package/templates/views/AppView.tsx.tmpl +138 -0
  160. package/templates/views/EntityView.tsx.tmpl +64 -0
  161. package/templates/views/index.ts.tmpl +8 -0
  162. package/dist/agent-setup-B5AKHOgt.js.map +0 -1
  163. package/dist/api-keys-Tm7zvAPe.js.map +0 -1
  164. package/dist/args-BX0YD14a.js +0 -266
  165. package/dist/args-BX0YD14a.js.map +0 -1
  166. package/dist/auth-Vl2hFKXP.js.map +0 -1
  167. package/dist/bundle-DtCCCr4w.js.map +0 -1
  168. package/dist/cloud-BpH0_bmV.js.map +0 -1
  169. package/dist/console-CEGm0XNc.js.map +0 -1
  170. package/dist/context-CyflUrKg.js.map +0 -1
  171. package/dist/daemon-CknkDjd6.js.map +0 -1
  172. package/dist/daemon-entry-CbLRrgzy.js.map +0 -1
  173. package/dist/db-CRNpA5mH.js.map +0 -1
  174. package/dist/dev-C0pbMmhS.js.map +0 -1
  175. package/dist/dev-preflight-unHpiYO1.js.map +0 -1
  176. package/dist/dev-server-B1UYifZJ.js.map +0 -1
  177. package/dist/doctor-CATRdWm6.js.map +0 -1
  178. package/dist/generate_sdk-Cvg5jjJF.js.map +0 -1
  179. package/dist/init-BWAwHPtz.js.map +0 -1
  180. package/dist/last-dev-database-c_dKz1S3.js +0 -2
  181. package/dist/path-flags-BFgG9V6y.js.map +0 -1
  182. package/dist/project-Wp2zoyeN.js +0 -349
  183. package/dist/project-Wp2zoyeN.js.map +0 -1
  184. package/dist/pull-C3PXduzl.js.map +0 -1
  185. package/dist/resolve-CNQjsatH.js +0 -2
  186. package/dist/schema-B0-p6T1O.js.map +0 -1
  187. package/dist/skills-DfnTeyRz.js.map +0 -1
  188. package/dist/start-BmEZkS0n.js.map +0 -1
  189. package/dist/state-CCB-FAQ9.js.map +0 -1
  190. package/dist/storage-DKPUBTWG.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify-D7UsV_qe.js","names":[],"sources":["../src/commands/verify.ts"],"sourcesContent":["/**\n * CLI command: rebase verify\n *\n * One command that says whether the collections are safe to ship, fast enough\n * to run after every edit. An agent writing `config/collections/*.ts` gets three\n * things wrong more than anything else — an option that does not exist, a UI\n * widget outside `admin: { … }`, and no `securityRules` — and every one of them\n * used to surface late: at boot, in review, or never. The project's Claude Code\n * hook (`.claude/hooks/post-edit-collection.sh`, written by `rebase init` and\n * `rebase skills --hooks`) runs this after each edit to a collection file, and\n * CI runs it on every push.\n *\n * Four steps, each reported on its own:\n *\n * 1. **Collection config** — every file under `config/collections/` loads and\n * passes `findCollectionConfigProblems` with unknown keys as errors: the same\n * validator the backend runs at boot, at its strictest.\n * 2. **RLS coverage** — a collection that declares no `securityRules` and\n * inherits none, or one with a `user_id` / `owner_id` / `organization_id` /\n * `tenant_id` column that leans on directory defaults instead of its own\n * rules; and a rule that lets a role read every row of such a table.\n * 3. **Migration freshness** — whether `drizzle/migrations` still builds the\n * tables and columns the collections declare (offline), and with `--live`\n * whether the database has applied every migration.\n * 4. **Live RLS audit** — with `--live`, the `@rebasepro/rls-check` catalogue\n * against the project's database, resolved as `db *` resolves it (the\n * managed one unless something names another).\n *\n * `--json` prints one object an agent can act on: every finding carries the\n * file, the dotted path into the config and the fix.\n */\nimport chalk from \"chalk\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport {\n parseEnvBoolean,\n isFirebaseCollectionConfig,\n isRelationalCollectionConfig,\n type CollectionConfig,\n type Property,\n type SecurityRule\n} from \"@rebasepro/types\";\nimport { parseCommandArgs, wantsHelp } from \"../utils/args\";\nimport { failAsJson, findBackendDir, getActiveBackendPlugin, readEnvFile, requireProjectRoot } from \"../utils/project\";\nimport { COLLECTIONS_FLAG_HELP, missingCollectionsPath } from \"../utils/path-flags\";\nimport { isLoadedCollectionFile } from \"../utils/collection-drift\";\nimport { resolveCollectionsDir } from \"../utils/collections-dir\";\n\n/** The flags `rebase verify` takes. */\nexport const VERIFY_FLAGS = {\n \"--collections\": String,\n \"-c\": \"--collections\",\n \"--json\": Boolean,\n \"--quiet\": Boolean,\n \"-q\": \"--quiet\",\n \"--live\": Boolean,\n \"--database-url\": String,\n \"--docker\": Boolean\n} as const;\n\nconst VERIFY_FLAG_HELP: Record<string, string> = {\n \"--collections, -c\": COLLECTIONS_FLAG_HELP,\n \"--json\": \"Print one JSON object: every step, and every finding with its file, path and fix\",\n \"--quiet, -q\": \"Print nothing when everything passes; only the problems when something fails\",\n \"--live\": \"Also connect to the project's database: check migrations are applied and run the rls-check audit\",\n \"--database-url\": \"With --live: audit this database instead of the project's\",\n \"--docker\": \"With --live: use the database from docker compose, starting it if needed\"\n};\n\nexport type VerifyStepId = \"config\" | \"rls\" | \"migrations\" | \"live\";\n\nexport interface VerifyFinding {\n step: VerifyStepId;\n /** Stable, kebab-case: what kind of problem. An agent can branch on it. */\n check: string;\n severity: \"error\" | \"warning\";\n message: string;\n /** What to change, when there is one thing to change. */\n fix?: string;\n /** The collection's slug. */\n collection?: string;\n /** The file to open, relative to the project root. */\n file?: string;\n /** Dotted path into the collection config — `posts.properties.title.widget`. */\n path?: string;\n}\n\nexport interface VerifyStep {\n id: VerifyStepId;\n title: string;\n status: \"passed\" | \"failed\" | \"warnings\" | \"skipped\";\n /** Why a step was skipped, or what it covered. */\n detail?: string;\n}\n\nexport interface VerifyReport {\n ok: boolean;\n /** Relative to the project root. */\n collectionsDir: string;\n steps: VerifyStep[];\n findings: VerifyFinding[];\n summary: { errors: number; warnings: number };\n}\n\nexport interface VerifyOptions {\n projectRoot: string;\n collectionsDir: string;\n /** The backend package, where `drizzle/migrations` lives. Null for a project without one. */\n backendDir: string | null;\n live: boolean;\n databaseUrl?: string;\n /** Why no `databaseUrl` could be resolved for `--live`, when resolving it failed. */\n databaseUnavailable?: string;\n /**\n * Whether the backend refuses a request with no token (`AUTH_REQUIRE`, on\n * unless set to `false`). Decides how a Rebase-compiled public read reads.\n * Defaults to true, the runtime's default.\n */\n authRequired?: boolean;\n}\n\nconst STEP_TITLES: Record<VerifyStepId, string> = {\n config: \"Collection config\",\n rls: \"RLS coverage\",\n migrations: \"Migration freshness\",\n live: \"Live RLS audit\"\n};\n\n/**\n * Columns that say \"this row belongs to someone\". A table carrying one and\n * served without a row filter is the classic multi-tenant leak.\n */\nconst OWNER_COLUMNS: readonly string[] = [\"user_id\", \"owner_id\", \"organization_id\", \"tenant_id\"];\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** What a collection file must default-export to be loaded as one. */\nfunction isCollection(value: unknown): value is CollectionConfig {\n return isRecord(value) && typeof value.slug === \"string\" && isRecord(value.properties);\n}\n\n/** `organizationId` → `organization_id`, as the generator derives a column from a key. */\nfunction snake(key: string): string {\n return key.replace(/([a-z0-9])([A-Z])/g, \"$1_$2\").replace(/[\\s-]+/g, \"_\").toLowerCase();\n}\n\n// ─── Loading ──────────────────────────────────────────────────────────────────\n\ninterface LoadedCollections {\n collections: CollectionConfig[];\n /** Parallel to `collections`: the file each one came from, relative to the project root. */\n sources: string[];\n /** Each collection's `securityRules` as its file declares them, before directory defaults. */\n ownRules: (readonly SecurityRule[] | undefined)[];\n /** `defaultSecurityRules` from the directory's `index.ts`. */\n defaults: readonly SecurityRule[] | undefined;\n defaultsFile?: string;\n failures: VerifyFinding[];\n}\n\n/**\n * Load the collections the way the backend finds them: every top-level file\n * the loader reads (`isLoadedCollectionFile`), each default-exporting one\n * collection, plus `defaultSecurityRules` from the directory's `index.ts`.\n *\n * Through jiti, with its caches off: the CLI is plain Node and the files are\n * TypeScript, and a verifier that answered from the previous edit's module\n * would be worse than none. A file that does not load is a finding, not a\n * crash — after an agent's edit it is the most likely problem of all.\n */\nasync function loadCollectionFiles(projectRoot: string, collectionsDir: string): Promise<LoadedCollections> {\n const { createJiti } = await import(\"jiti\");\n const jiti = createJiti(path.join(collectionsDir, \"index.ts\"), {\n interopDefault: true,\n moduleCache: false,\n fsCache: false\n });\n const relative = (file: string) => path.relative(projectRoot, file);\n\n const loaded: LoadedCollections = { collections: [], sources: [], ownRules: [], defaults: undefined, failures: [] };\n\n for (const name of fs.readdirSync(collectionsDir).filter(isLoadedCollectionFile).sort()) {\n const file = path.join(collectionsDir, name);\n try {\n const exported = await jiti.import<unknown>(file, { default: true });\n if (!isCollection(exported)) {\n loaded.failures.push({\n step: \"config\",\n check: \"config-no-collection\",\n severity: \"error\",\n file: relative(file),\n message: `${name} does not default-export a collection (an object with a \\`slug\\` and \\`properties\\`).`,\n fix: \"End the file with `export default defineCollection({ slug, name, properties, … })` — the backend refuses to boot on a collection file that exports anything else.\"\n });\n continue;\n }\n loaded.collections.push(exported);\n loaded.sources.push(relative(file));\n loaded.ownRules.push(exported.securityRules);\n } catch (error) {\n loaded.failures.push({\n step: \"config\",\n check: \"config-load\",\n severity: \"error\",\n file: relative(file),\n message: `${name} does not load: ${error instanceof Error ? error.message : String(error)}`,\n fix: \"Fix the syntax or import error. The backend refuses to boot on the same file.\"\n });\n }\n }\n\n for (const index of [\"index.ts\", \"index.js\"]) {\n const file = path.join(collectionsDir, index);\n if (!fs.existsSync(file)) continue;\n try {\n const mod = await jiti.import<unknown>(file);\n const rules = isRecord(mod) ? mod.defaultSecurityRules : undefined;\n if (Array.isArray(rules)) {\n loaded.defaults = rules;\n loaded.defaultsFile = relative(file);\n }\n } catch (error) {\n loaded.failures.push({\n step: \"config\",\n check: \"config-load\",\n severity: \"error\",\n file: relative(file),\n message: `${index} does not load: ${error instanceof Error ? error.message : String(error)}`,\n fix: \"Fix the file, or delete it if the project declares no `defaultSecurityRules`. The backend refuses to boot without reading it.\"\n });\n }\n break;\n }\n\n return loaded;\n}\n\n// ─── Step 1: collection config ────────────────────────────────────────────────\n\nasync function checkConfig(loaded: LoadedCollections): Promise<VerifyFinding[]> {\n const { findCollectionConfigProblems } = await import(\"@rebasepro/server\");\n const fileBySlug = new Map(loaded.collections.map((c, i) => [c.slug, loaded.sources[i]]));\n const problems = findCollectionConfigProblems(loaded.collections, {\n unknownKeys: \"error\",\n sources: loaded.sources\n });\n return problems.map((problem): VerifyFinding => {\n const slug = problem.path.split(\".\")[0];\n const file = fileBySlug.get(slug);\n return {\n step: \"config\",\n check: problem.kind === \"key\" ? \"config-key\" : \"config-incoherent\",\n severity: problem.severity,\n message: problem.message,\n path: problem.path,\n ...(fileBySlug.has(slug) && { collection: slug }),\n ...(file && { file })\n };\n });\n}\n\n// ─── Step 2: RLS coverage ─────────────────────────────────────────────────────\n\n/** An owner-ish column a collection's table carries, and the property that declares it. */\ninterface OwnerColumn {\n column: string;\n key: string;\n}\n\n/** The owner-ish columns a collection's table carries: plain columns and `belongsTo` keys. */\nfunction ownerColumnsOf(collection: CollectionConfig): OwnerColumn[] {\n const found: OwnerColumn[] = [];\n const consider = (column: string, key: string) => {\n if (OWNER_COLUMNS.includes(column)) found.push({ column, key });\n };\n for (const [key, property] of Object.entries<Property>(collection.properties ?? {})) {\n if (!isRecord(property)) continue;\n if (property.type === \"relation\") {\n const relation = property.relation;\n if (isRecord(relation) && relation.kind === \"belongsTo\") {\n // As the generator names it (`generateForeignKeyName` in\n // `@rebasepro/utils`, which the CLI does not depend on): from\n // the relation's name, which is the property key only when\n // `relationName` is not set.\n const name = typeof relation.relationName === \"string\" && relation.relationName ? relation.relationName : key;\n const singular = /[^s]s$/.test(name) ? name.slice(0, -1) : name;\n consider(typeof relation.localKey === \"string\" ? relation.localKey : `${snake(singular)}_id`, key);\n }\n continue;\n }\n consider(typeof property.columnName === \"string\" ? property.columnName : snake(key), key);\n }\n return found;\n}\n\n/** Whether a rule applies to reads. */\nfunction coversSelect(rule: SecurityRule): boolean {\n const operations = rule.operations ?? [rule.operation ?? \"all\"];\n return operations.includes(\"select\") || operations.includes(\"all\");\n}\n\n/**\n * A permissive read rule with no row filter: every row, to everyone the rule\n * names. `access: \"public\"` is the same thing said on purpose, so it is not\n * this; `roles: [\"admin\"]` restates the default admin read and adds nothing.\n */\nfunction isUnfilteredRead(rule: SecurityRule): boolean {\n if (!coversSelect(rule) || rule.mode === \"restrictive\") return false;\n if (rule.access === \"public\" || rule.ownerField || rule.condition) return false;\n if (rule.using !== undefined && !/^\\s*\\(?\\s*true\\s*\\)?\\s*$/i.test(rule.using)) return false;\n // A write-only check (`withCheck` with no `using`) filters nothing on read,\n // and it is not a read rule either.\n if (rule.using === undefined && rule.withCheck !== undefined) return false;\n const roles = rule.roles ?? [];\n return !(roles.length > 0 && roles.every(role => role === \"admin\"));\n}\n\n/** A read rule that returns every row to someone: an unfiltered one, or `access: \"public\"`. */\nfunction opensEveryRow(rule: SecurityRule): boolean {\n return isUnfilteredRead(rule) || (rule.access === \"public\" && coversSelect(rule) && rule.mode !== \"restrictive\");\n}\n\nfunction describeRuleTarget(rule: Pick<SecurityRule, \"roles\">): string {\n return rule.roles?.length ? `role${rule.roles.length > 1 ? \"s\" : \"\"} ${rule.roles.map(r => `\"${r}\"`).join(\", \")}` : \"every signed-in user\";\n}\n\nfunction checkRls(loaded: LoadedCollections): VerifyFinding[] {\n const findings: VerifyFinding[] = [];\n loaded.collections.forEach((collection, i) => {\n // Firestore does not implement `securityRules` at all — its own rules\n // language governs it, and nothing here can say anything about that.\n if (isFirebaseCollectionConfig(collection)) return;\n\n const slug = collection.slug;\n const file = loaded.sources[i];\n const own = loaded.ownRules[i] ?? [];\n const owners = ownerColumnsOf(collection);\n const tenantScoped = isRelationalCollectionConfig(collection) && isRecord(collection.tenant);\n const ownerField = owners[0]?.column ?? \"user_id\";\n\n if (own.length === 0) {\n // An auth collection gets a self-read policy and an admin-only write\n // gate from the framework; it is never served unfiltered.\n if (collection.auth) return;\n\n if (owners.length > 0 && !tenantScoped) {\n const perUser = ownerField === \"user_id\" || ownerField === \"owner_id\";\n const fix = perUser\n ? `Add \\`securityRules: [{ operation: \"all\", ownerField: \"${ownerField}\" }]\\` so each user reaches only their own rows. ` +\n \"A table meant to be readable by everyone says so: `{ operation: \\\"select\\\", access: \\\"public\\\" }`.\"\n : `Scope it with \\`tenant: { field: \"${owners[0].key}\", from: { claim: \"${ownerField}\" } }\\` (the claim that carries the caller's tenant), ` +\n \"plus the `securityRules` that grant access inside a tenant — e.g. `[{ operation: \\\"select\\\", access: \\\"public\\\" }]`.\";\n const inherited = loaded.defaults ?? [];\n if (inherited.length === 0) {\n findings.push({\n step: \"rls\",\n check: \"rls-missing\",\n severity: \"error\",\n collection: slug,\n file,\n path: `${slug}.securityRules`,\n message: `\"${slug}\" has a \\`${ownerField}\\` column but declares no \\`securityRules\\`, and inherits none: nothing says which rows belong to whom (by default only admins can read or write it).`,\n fix\n });\n } else if (inherited.some(opensEveryRow)) {\n // Directory defaults are written for every table. On a\n // tenant column, inheriting an all-rows read is the\n // cross-tenant leak; on a per-user column it is often a\n // public profile or author list, so it is said, not failed.\n findings.push({\n step: \"rls\",\n check: perUser ? \"rls-inherited-read\" : \"rls-missing\",\n severity: perUser ? \"warning\" : \"error\",\n collection: slug,\n file,\n path: `${slug}.securityRules`,\n message: `\"${slug}\" has a \\`${ownerField}\\` column but declares no \\`securityRules\\` of its own, so it inherits \\`defaultSecurityRules\\` from ${loaded.defaultsFile}, which let ${describeRuleTarget(inherited.find(opensEveryRow) ?? {})} read every row regardless of \\`${ownerField}\\`.`,\n fix\n });\n }\n return;\n }\n\n if (!loaded.defaults?.length) {\n findings.push({\n step: \"rls\",\n check: \"rls-missing\",\n severity: \"error\",\n collection: slug,\n file,\n path: `${slug}.securityRules`,\n message: `\"${slug}\" declares no \\`securityRules\\` and inherits none, so nothing says who may read or write it (by default only admins can).`,\n fix: \"Declare its access: `securityRules: [{ operation: \\\"select\\\", access: \\\"public\\\" }, { operations: [\\\"insert\\\", \\\"update\\\", \\\"delete\\\"], roles: [\\\"admin\\\"] }]` \" +\n \"for a public table, `{ operation: \\\"all\\\", ownerField: \\\"user_id\\\" }` for per-user rows, \" +\n \"or `{ operation: \\\"all\\\", roles: [\\\"admin\\\"] }` to keep it admin-only on purpose.\"\n });\n }\n return;\n }\n\n if (owners.length === 0 || tenantScoped) return;\n own.forEach((rule, index) => {\n if (!isUnfilteredRead(rule)) return;\n findings.push({\n step: \"rls\",\n check: \"rls-unfiltered-read\",\n severity: \"warning\",\n collection: slug,\n file,\n path: `${slug}.securityRules[${index}]`,\n message: `\"${slug}\" has a \\`${ownerField}\\` column, and securityRules[${index}] lets ${describeRuleTarget(rule)} read every row of it with no row filter.`,\n fix: `Filter the rule — \\`ownerField: \"${ownerField}\"\\` or a \\`condition\\` — or, if every row really is meant for ${describeRuleTarget(rule)}, keep it and ignore this warning.`\n });\n });\n });\n return findings;\n}\n\n// ─── Step 3: migration freshness ──────────────────────────────────────────────\n\nconst POSTGRES_PLUGIN = \"@rebasepro/server-postgres\";\n\n/** One row of `rebase.atlas_schema_revisions`, as far as \"applied\" needs it. */\nexport interface AppliedRevision {\n version: string;\n /** Atlas's revision type, a bit set: 1 baseline, 2 executed, 4 resolved (`migrate set`). */\n type: number;\n}\n\n/** Atlas's baseline bit in `atlas_schema_revisions.type`. */\nconst REVISION_TYPE_BASELINE = 1;\n\n/**\n * The migration files the database has not run, as Atlas's own `migrate status`\n * counts them.\n *\n * A file is applied when its version has a row, or when it is at or behind the\n * newest *baseline* row: `migrate apply --baseline V` records V alone, and\n * means \"everything up to V is already here\". `rebase db migrate --baseline` is\n * what the CLI prescribes for a booted database, so set membership alone\n * reported the earlier migrations of every baselined database as not applied —\n * and prescribed `rebase db migrate`, which answers \"No migration files to\n * execute\". That run failed forever.\n */\nexport function pendingMigrations(\n files: readonly string[],\n revisions: readonly AppliedRevision[],\n compare: (a: string, b: string) => number,\n versionOf: (file: string) => string\n): string[] {\n const recorded = new Set(revisions.map(row => row.version));\n const baseline = revisions\n .filter(row => (Number(row.type) & REVISION_TYPE_BASELINE) !== 0)\n .map(row => row.version)\n .reduce<string | null>((newest, version) => (newest === null || compare(version, newest) > 0 ? version : newest), null);\n return files.filter((file) => {\n const version = versionOf(file);\n if (recorded.has(version)) return false;\n return baseline === null || compare(version, baseline) > 0;\n });\n}\n\nasync function checkMigrations(\n options: VerifyOptions,\n loaded: LoadedCollections,\n step: VerifyStep\n): Promise<VerifyFinding[]> {\n const skip = (detail: string): VerifyFinding[] => {\n step.status = \"skipped\";\n step.detail = detail;\n return [];\n };\n if (!options.backendDir) return skip(\"no backend/ directory\");\n const plugin = getActiveBackendPlugin(options.backendDir);\n if (plugin !== POSTGRES_PLUGIN) {\n return skip(`migrations are checked for ${POSTGRES_PLUGIN} projects${plugin ? `; this one uses ${plugin}` : \"\"}`);\n }\n const migrationsDir = path.join(options.backendDir, \"drizzle\", \"migrations\");\n const files = fs.existsSync(migrationsDir)\n ? fs.readdirSync(migrationsDir).filter(f => f.endsWith(\".sql\")).sort()\n : [];\n if (files.length === 0) {\n return skip(\"no migrations yet — the schema is applied with `rebase db push`; `rebase db generate` starts versioned migrations\");\n }\n\n const relativeDir = path.relative(options.projectRoot, migrationsDir);\n const { findUnmigratedSchema } = await import(\"@rebasepro/server-postgres\");\n const findings: VerifyFinding[] = [];\n const fix = \"Run `rebase db generate` to write the migration, then `rebase db migrate` to apply it.\";\n const fileBySlug = new Map(loaded.collections.map((c, i) => [c.slug, loaded.sources[i]]));\n\n let changes: ReturnType<typeof findUnmigratedSchema>;\n try {\n changes = findUnmigratedSchema(\n loaded.collections,\n files.map(f => fs.readFileSync(path.join(migrationsDir, f), \"utf-8\"))\n );\n } catch (error) {\n return [{\n step: \"migrations\",\n check: \"migration-plan\",\n severity: \"error\",\n message: `Could not derive the schema from the collections: ${error instanceof Error ? error.message : String(error)}`,\n fix: \"`rebase schema generate` reports the same problem with more context.\"\n }];\n }\n\n for (const change of changes) {\n const where = change.column ? `${change.table}.${change.column}` : change.table;\n const message = change.kind === \"missing-table\"\n ? `Table ${where} is declared by the collections but no migration in ${relativeDir} creates it.`\n : change.kind === \"missing-column\"\n ? `Column ${where} is declared by the collections but no migration in ${relativeDir} adds it.`\n : `Column ${where} is no longer declared by the collections, but the migrations in ${relativeDir} still create it.`;\n const file = change.slug ? fileBySlug.get(change.slug) : undefined;\n findings.push({\n step: \"migrations\",\n check: change.kind === \"removed-column\" ? \"migration-stale-column\" : \"migration-missing\",\n severity: \"error\",\n message,\n fix,\n ...(change.slug && { collection: change.slug }),\n ...(file && { file })\n });\n }\n\n step.detail = `${files.length} migration${files.length === 1 ? \"\" : \"s\"} in ${relativeDir}`;\n\n if (options.live && options.databaseUrl) {\n findings.push(...await checkAppliedMigrations(options.databaseUrl, files));\n }\n return findings;\n}\n\n/** Which migration files the database has not run, from Atlas's revision table. */\nasync function checkAppliedMigrations(databaseUrl: string, files: string[]): Promise<VerifyFinding[]> {\n const pg = await import(\"pg\");\n const client = new pg.default.Client({ connectionString: databaseUrl, connectionTimeoutMillis: 5_000 });\n try {\n await client.connect();\n const present = await client.query<{ present: boolean }>(\n \"SELECT to_regclass('rebase.atlas_schema_revisions') IS NOT NULL AS present\"\n );\n if (!present.rows[0]?.present) {\n return [{\n step: \"migrations\",\n check: \"migration-unapplied\",\n severity: \"warning\",\n message: \"The database has no migration history (rebase.atlas_schema_revisions) — it was provisioned with `rebase db push`, or never migrated.\",\n fix: \"`rebase db migrate --baseline <version>` adopts an existing database; `rebase db migrate` applies the migrations to a new one.\"\n }];\n }\n const applied = await client.query<{ version: string; type: string | number }>(\n \"SELECT version, type FROM rebase.atlas_schema_revisions WHERE error IS NULL OR error = ''\"\n );\n const { compareMigrationVersions, migrationVersionOf } = await import(\"@rebasepro/server-postgres\");\n const pending = pendingMigrations(\n files,\n applied.rows.map(row => ({ version: row.version, type: Number(row.type) })),\n compareMigrationVersions,\n migrationVersionOf\n );\n if (pending.length === 0) return [];\n return [{\n step: \"migrations\",\n check: \"migration-unapplied\",\n severity: \"error\",\n message: `${pending.length} migration${pending.length === 1 ? \" has\" : \"s have\"} not been applied to the database: ${pending.join(\", \")}.`,\n fix: \"Run `rebase db migrate`.\"\n }];\n } catch (error) {\n return [{\n step: \"migrations\",\n check: \"migration-unapplied\",\n severity: \"warning\",\n message: `Could not read the database's migration history: ${error instanceof Error ? error.message : String(error)}`\n }];\n } finally {\n await client.end().catch(() => undefined);\n }\n}\n\n// ─── Step 4: live RLS audit ───────────────────────────────────────────────────\n\n/** rls-check's severities, folded onto this command's two. `info` is not reported. */\nfunction liveSeverity(severity: string): VerifyFinding[\"severity\"] | null {\n if (severity === \"critical\" || severity === \"high\") return \"error\";\n if (severity === \"medium\" || severity === \"low\") return \"warning\";\n return null;\n}\n\n/**\n * Why the database `--live` was pointed at cannot be opened, or `null` when it\n * can. Asked once, before the live steps, so an unreachable database is one\n * error rather than a skipped audit and a migration warning: under `--live`\n * the audit was asked for, and a CI run in which it never ran is not green.\n */\nasync function databaseUnreachable(databaseUrl: string): Promise<string | null> {\n const pg = await import(\"pg\");\n const client = new pg.default.Client({ connectionString: databaseUrl, connectionTimeoutMillis: 5_000 });\n try {\n await client.connect();\n return null;\n } catch (error) {\n return error instanceof Error ? error.message : String(error);\n } finally {\n await client.end().catch(() => undefined);\n }\n}\n\n/**\n * A `USING (true)` read policy Rebase compiled from an `access: \"public\"`\n * select rule, on a backend that refuses a request with no token.\n *\n * rls-check reads the policy alone, and for the role every signed-in request\n * runs as it is \"every row to everyone\": critical. But the API answers 401\n * before RLS is consulted unless `AUTH_REQUIRE=false`, so what it grants is\n * every row to every *signed-in* user — exactly what step 2 accepts as a rule\n * said on purpose, and what boot reports as an info line. A fresh blog\n * scaffold failed its own `verify --live` with three such errors. It is a\n * warning here, as an unfiltered read is in step 2; with `AUTH_REQUIRE=false`\n * the rows really are public and it stays an error.\n */\nfunction isTokenGatedPublicRead(\n finding: { id: string; target: { table?: string; policy?: string } },\n authRequired: boolean,\n isGeneratedPolicyName: (name: string, table: string) => boolean\n): boolean {\n if (!authRequired || finding.id !== \"policy-always-true\") return false;\n const { table, policy } = finding.target;\n if (!table || !policy) return false;\n return isGeneratedPolicyName(policy, table) && policy.startsWith(`${table}_select_`);\n}\n\nasync function checkLive(databaseUrl: string, step: VerifyStep, authRequired: boolean): Promise<VerifyFinding[]> {\n const { scan, CHECKS } = await import(\"@rebasepro/rls-check\");\n const { isGeneratedPolicyName } = await import(\"@rebasepro/server-postgres\");\n let result: Awaited<ReturnType<typeof scan>>;\n try {\n result = await scan({ connectionString: databaseUrl, statementTimeoutMs: 10_000 });\n } catch (error) {\n step.detail = \"the audit did not run\";\n return [{\n step: \"live\",\n check: \"live-scan-failed\",\n severity: \"error\",\n message: `The live RLS audit failed: ${error instanceof Error ? error.message : String(error)}`,\n fix: \"`npx @rebasepro/rls-check <database-url>` runs the same audit on its own and reports the failure in full.\"\n }];\n }\n step.detail = `${CHECKS.length} rls-check checks against ${result.database.host}/${result.database.name}`;\n\n const findings: VerifyFinding[] = [];\n for (const finding of result.findings) {\n const severity = liveSeverity(finding.severity);\n if (!severity) continue;\n const tokenGated = severity === \"error\" && isTokenGatedPublicRead(finding, authRequired, isGeneratedPolicyName);\n findings.push({\n step: \"live\",\n check: finding.id,\n severity: tokenGated ? \"warning\" : severity,\n message: tokenGated\n ? `${finding.title} — compiled from an \\`access: \"public\"\\` read rule: every signed-in user reads every row. ` +\n \"A request with no token is refused by the API (AUTH_REQUIRE is on), so it is not an anonymous read.\"\n : finding.title,\n ...(finding.fix && { fix: finding.fix }),\n path: finding.target.table ? `${finding.target.schema}.${finding.target.table}` : finding.target.schema\n });\n }\n for (const degraded of result.diagnostics.degraded) {\n findings.push({\n step: \"live\",\n check: \"rls-check-degraded\",\n severity: \"warning\",\n message: `The audit could not read ${degraded.what}: ${degraded.error}. A clean result does not cover it.`\n });\n }\n return findings;\n}\n\n// ─── The run ──────────────────────────────────────────────────────────────────\n\nfunction settle(step: VerifyStep, findings: VerifyFinding[]): VerifyStep {\n if (step.status === \"skipped\") return step;\n const mine = findings.filter(f => f.step === step.id);\n step.status = mine.some(f => f.severity === \"error\") ? \"failed\" : mine.length > 0 ? \"warnings\" : \"passed\";\n return step;\n}\n\n/**\n * Run every step and return the report. Prints nothing and never exits, so a\n * caller — the command, a test — decides what to do with it.\n */\nexport async function runVerify(options: VerifyOptions): Promise<VerifyReport> {\n const findings: VerifyFinding[] = [];\n const steps: VerifyStep[] = ([\"config\", \"rls\", \"migrations\", \"live\"] as const).map(id => ({\n id,\n title: STEP_TITLES[id],\n status: \"passed\"\n }));\n const [configStep, rlsStep, migrationsStep, liveStep] = steps;\n const collectionsDir = path.relative(options.projectRoot, options.collectionsDir) || \".\";\n\n if (!fs.existsSync(options.collectionsDir) || !fs.statSync(options.collectionsDir).isDirectory()) {\n for (const step of steps) {\n step.status = \"skipped\";\n step.detail = `no collections directory at ${collectionsDir}`;\n }\n return { ok: true, collectionsDir, steps, findings, summary: { errors: 0, warnings: 0 } };\n }\n\n const loaded = await loadCollectionFiles(options.projectRoot, options.collectionsDir);\n findings.push(...loaded.failures);\n\n if (loaded.collections.length > 0) {\n findings.push(...await checkConfig(loaded));\n }\n const counted = `${loaded.collections.length} collection${loaded.collections.length === 1 ? \"\" : \"s\"}`;\n configStep.detail = `${counted}, unknown keys are errors`;\n rlsStep.detail = counted;\n\n // Directory defaults reach a collection the way the loader applies them, so\n // the migration step plans the same policies the backend would.\n if (loaded.defaults?.length) {\n const { applyCollectionDefaults } = await import(\"@rebasepro/server\");\n applyCollectionDefaults(loaded.collections, { defaultSecurityRules: [...loaded.defaults] });\n }\n\n findings.push(...checkRls(loaded));\n\n // The database the live steps read, once it is known to answer. Under\n // `--live` a database that is missing or does not answer is an error: the\n // audit was asked for, and exit 0 without it is a green CI run over nothing.\n let liveUrl: string | undefined;\n if (options.live) {\n if (!options.databaseUrl) {\n liveStep.detail = \"no database to audit\";\n findings.push({\n step: \"live\",\n check: \"live-no-database\",\n severity: \"error\",\n message: options.databaseUnavailable\n ? `--live was asked for, and the database could not be prepared: ${options.databaseUnavailable}`\n : \"--live was asked for, and no database is configured to audit.\",\n fix: \"Pass --database-url <url> (or set DATABASE_URL), or run without --live for the offline checks.\"\n });\n } else {\n const unreachable = await databaseUnreachable(options.databaseUrl);\n if (unreachable) {\n liveStep.detail = \"the database did not answer\";\n findings.push({\n step: \"live\",\n check: \"live-unreachable\",\n severity: \"error\",\n message: `--live was asked for, and the database could not be reached: ${unreachable}. Neither the migration check nor the RLS audit ran.`,\n fix: \"Check the database is running and the URL is right (`rebase db url` prints the one this project resolves), or run without --live for the offline checks.\"\n });\n } else {\n liveUrl = options.databaseUrl;\n }\n }\n }\n\n if (findings.some(f => f.step === \"config\" && f.severity === \"error\")) {\n migrationsStep.status = \"skipped\";\n migrationsStep.detail = \"fix the collection config errors first\";\n } else {\n findings.push(...await checkMigrations({ ...options, databaseUrl: liveUrl }, loaded, migrationsStep));\n }\n\n if (!options.live) {\n liveStep.status = \"skipped\";\n liveStep.detail = \"pass --live to audit the database\";\n } else if (liveUrl) {\n findings.push(...await checkLive(liveUrl, liveStep, options.authRequired ?? true));\n }\n\n steps.forEach(step => settle(step, findings));\n const errors = findings.filter(f => f.severity === \"error\").length;\n return {\n ok: errors === 0,\n collectionsDir,\n steps,\n findings,\n summary: { errors, warnings: findings.length - errors }\n };\n}\n\n// ─── Output ───────────────────────────────────────────────────────────────────\n\nfunction printFinding(finding: VerifyFinding): void {\n const mark = finding.severity === \"error\" ? chalk.red(\"✗\") : chalk.yellow(\"!\");\n const where = [finding.file, finding.path].filter(Boolean).join(\" → \");\n console.log(` ${mark} ${where ? `${chalk.bold(where)} ` : \"\"}${finding.message}`);\n if (finding.fix) console.log(chalk.gray(` fix: ${finding.fix}`));\n}\n\nfunction summaryLine(report: VerifyReport): string {\n const { errors, warnings } = report.summary;\n return `${errors} error${errors === 1 ? \"\" : \"s\"}, ${warnings} warning${warnings === 1 ? \"\" : \"s\"}`;\n}\n\nexport function printReport(report: VerifyReport, quiet: boolean): void {\n if (quiet) {\n if (report.findings.length === 0) return;\n if (!report.ok) console.log(chalk.red(`rebase verify: ${summaryLine(report)}`));\n for (const finding of report.findings) printFinding(finding);\n return;\n }\n\n console.log(\"\");\n console.log(` ${chalk.bold(\"rebase verify\")} ${chalk.gray(report.collectionsDir)}`);\n console.log(\"\");\n for (const step of report.steps) {\n const mark = step.status === \"passed\" ? chalk.green(\"✓\")\n : step.status === \"failed\" ? chalk.red(\"✗\")\n : step.status === \"warnings\" ? chalk.yellow(\"!\")\n : chalk.gray(\"–\");\n const detail = step.detail ? chalk.gray(step.status === \"skipped\" ? `skipped: ${step.detail}` : step.detail) : \"\";\n console.log(` ${mark} ${step.title.padEnd(22)} ${detail}`);\n }\n if (report.findings.length > 0) {\n console.log(\"\");\n for (const finding of report.findings) printFinding(finding);\n }\n console.log(\"\");\n console.log(report.ok ? chalk.green(` ✓ ${summaryLine(report)}`) : chalk.red(` ✗ ${summaryLine(report)}`));\n console.log(\"\");\n}\n\nfunction printVerifyHelp(): void {\n console.log(`\n${chalk.bold(\"rebase verify\")} — Check the collections are safe to ship\n\n${chalk.green.bold(\"Usage\")}\n rebase verify [options]\n\n${chalk.green.bold(\"Options\")}\n${Object.entries(VERIFY_FLAG_HELP).map(([flag, help]) => ` ${chalk.blue(flag.padEnd(20))} ${help}`).join(\"\\n\")}\n\n${chalk.green.bold(\"Steps\")}\n 1. Collection config every file loads and passes the boot validator, unknown keys as errors\n 2. RLS coverage securityRules are declared, and owner/tenant columns are filtered on\n 3. Migration freshness drizzle/migrations builds every declared table and column\n 4. Live RLS audit with --live: the rls-check catalogue against the project's database\n\nExits 1 when any step reports an error; warnings do not fail. Under --live, a\ndatabase that is missing or does not answer is an error. Fast and offline\nwithout --live: for CI, and for the Claude Code hook that ${chalk.cyan(\"rebase skills --hooks\")} installs.\n`);\n}\n\n/**\n * Whether the backend refuses a request with no token: `AUTH_REQUIRE` from the\n * environment, else `.env`, read as every boolean env var is read\n * (`parseEnvBoolean`: `false`/`0`/`no`/`off` turn it off). On unless one of\n * those says otherwise — the runtime's default. Compared raw, `AUTH_REQUIRE=0`\n * read as \"on\", and a compiled public read was reported as token-gated on a\n * backend that serves it to anyone.\n */\nexport function authRequiredFrom(\n environment: Record<string, string | undefined>,\n envFile: Record<string, string | undefined>\n): boolean {\n return parseEnvBoolean(environment.AUTH_REQUIRE ?? envFile.AUTH_REQUIRE) !== false;\n}\n\n/**\n * The database `--live` audits, resolved as every other database door resolves\n * it (`db *`, `console`, `doctor`): `--database-url`, `DATABASE_URL`, `.env`,\n * `--docker` or the last `rebase dev --docker`, and otherwise the managed\n * development database — started if it is not running. Reading only\n * `DATABASE_URL` meant the audit never ran on a fresh scaffold, whose database\n * is the managed one.\n *\n * Progress goes to stderr, so `--json` keeps stdout to the report.\n */\nasync function resolveLiveDatabase(\n projectRoot: string,\n rawArgs: readonly string[],\n options: { flagUrl: string | null; flagDocker: boolean; quiet: boolean }\n): Promise<Pick<VerifyOptions, \"databaseUrl\" | \"databaseUnavailable\">> {\n try {\n const { ensureDockerDatabase, databaseUrlOf } = await import(\"./db\");\n await ensureDockerDatabase(projectRoot, rawArgs);\n const { prepareDatabaseEnv } = await import(\"../dev-db/prepare\");\n const prepared = await prepareDatabaseEnv(projectRoot, {\n flagUrl: options.flagUrl,\n flagDocker: options.flagDocker,\n quiet: options.quiet,\n onProgress: options.quiet ? undefined : (line) => console.error(chalk.gray(` ${line}`))\n });\n return { databaseUrl: databaseUrlOf(prepared, readEnvFile(projectRoot)) };\n } catch (error) {\n return { databaseUnavailable: error instanceof Error ? error.message : String(error) };\n }\n}\n\nexport async function verifyCommand(rawArgs: string[]): Promise<void> {\n if (wantsHelp(rawArgs)) {\n printVerifyHelp();\n return;\n }\n const { flags } = parseCommandArgs({\n spec: VERIFY_FLAGS,\n rawArgs,\n commandWords: 1,\n command: \"verify\",\n maxPositionals: 0\n });\n\n const projectRoot = requireProjectRoot();\n const backendDir = findBackendDir(projectRoot);\n if (flags[\"--collections\"]) {\n // A mistyped `-c` used to skip every step and print \"0 errors\" with\n // exit 0 — a green CI run over nothing. Refused as `doctor` and\n // `schema` refuse it. Without the flag, a project with no collections\n // directory (a headless one) is still a skip, not a failure.\n const refusal = missingCollectionsPath(rawArgs, process.cwd(), backendDir ?? projectRoot, \"verify\");\n if (refusal) {\n if (flags[\"--json\"]) {\n failAsJson(\n `Collections path not found: \"${flags[\"--collections\"]}\" (resolved to ${path.resolve(process.cwd(), flags[\"--collections\"])}).`,\n \"collections_not_found\",\n \"Leave --collections out (config/collections is the default), or pass a directory that exists; relative paths are resolved from where you run the command.\"\n );\n }\n console.error(\"\");\n for (const line of refusal) console.error(line);\n console.error(\"\");\n process.exit(1);\n }\n }\n const collectionsDir = flags[\"--collections\"]\n ? path.resolve(process.cwd(), flags[\"--collections\"])\n : resolveCollectionsDir(projectRoot);\n const live = flags[\"--live\"] === true;\n const database = live\n ? await resolveLiveDatabase(projectRoot, rawArgs, {\n flagUrl: flags[\"--database-url\"] ?? null,\n flagDocker: flags[\"--docker\"] === true,\n quiet: flags[\"--quiet\"] === true\n })\n : {};\n\n const report = await runVerify({\n projectRoot,\n collectionsDir,\n backendDir,\n live,\n ...database,\n authRequired: authRequiredFrom(process.env, readEnvFile(projectRoot))\n });\n\n if (flags[\"--json\"]) {\n console.log(JSON.stringify(report, null, 2));\n } else {\n printReport(report, flags[\"--quiet\"] === true);\n }\n if (!report.ok) process.exitCode = 1;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/cli",
3
- "version": "0.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "Developer tools for Rebase projects",
5
5
  "keywords": [
6
6
  "cli",
@@ -36,6 +36,7 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
+ "acorn": "^8.18.0",
39
40
  "arg": "^5.0.2",
40
41
  "chalk": "^5.6.2",
41
42
  "dotenv": "^17.4.2",
@@ -43,12 +44,13 @@
43
44
  "inquirer": "14.0.2",
44
45
  "jiti": "^2.7.0",
45
46
  "pg": "^8.22.0",
46
- "@rebasepro/codegen": "0.25.0",
47
- "@rebasepro/client": "0.25.0",
48
- "@rebasepro/agent-skills": "0.25.0",
49
- "@rebasepro/server": "0.25.0",
50
- "@rebasepro/types": "0.25.0",
51
- "@rebasepro/server-postgres": "0.25.0"
47
+ "@rebasepro/agent-skills": "0.26.0",
48
+ "@rebasepro/client": "0.26.0",
49
+ "@rebasepro/rls-check": "0.26.0",
50
+ "@rebasepro/server": "0.26.0",
51
+ "@rebasepro/server-postgres": "0.26.0",
52
+ "@rebasepro/codegen": "0.26.0",
53
+ "@rebasepro/types": "0.26.0"
52
54
  },
53
55
  "devDependencies": {
54
56
  "@types/node": "^26.1.2",
@@ -15,6 +15,7 @@ import {
15
15
  HonoEnv,
16
16
  listenWithPortRetry,
17
17
  cleanupDevPortFile,
18
+ overridableSecurityHeaders,
18
19
  initializeDataSources,
19
20
  resolveDataSources,
20
21
  resolveStorageSources,
@@ -117,7 +118,14 @@ app.use("/*", cors({
117
118
  ]
118
119
  }));
119
120
 
120
- app.use("/*", secureHeaders());
121
+ // Its `no-referrer` would hide the origin Google sign-in checks, and it would
122
+ // overwrite the MCP consent page's own `no-referrer` and `X-Frame-Options: DENY`.
123
+ // The runtime's defaults for those two leave a route's own alone.
124
+ app.use("/*", secureHeaders({
125
+ referrerPolicy: false,
126
+ xFrameOptions: false
127
+ }));
128
+ app.use("/*", overridableSecurityHeaders());
121
129
 
122
130
  // ─── Databases ───────────────────────────────────────────────────────
123
131
  // One connection per declared database, resolved from `DATABASE_URL` for the
@@ -15,6 +15,7 @@ export const categories = pgTable("categories", {
15
15
  description: text("description"),
16
16
  icon: text("icon")
17
17
  }, (table) => ([
18
+ pgPolicy("categories_all_c924b8e", { as: "permissive", for: "all", to: ["public"], using: sql`(SELECT string_to_array(rebase.roles(), ',') && ARRAY['admin'])`, withCheck: sql`(SELECT string_to_array(rebase.roles(), ',') && ARRAY['admin'])` }),
18
19
  pgPolicy("categories_default_admin_read", { as: "permissive", for: "select", to: ["public"], using: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))` }),
19
20
  pgPolicy("categories_default_admin_write_insert", { as: "permissive", for: "insert", to: ["public"], withCheck: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))` }),
20
21
  pgPolicy("categories_default_admin_write_update", { as: "permissive", for: "update", to: ["public"], using: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))`, withCheck: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))` }),
@@ -29,6 +30,7 @@ export const orders = pgTable("orders", {
29
30
  notes: text("notes"),
30
31
  createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`now()`)
31
32
  }, (table) => ([
33
+ pgPolicy("orders_all_c924b8e", { as: "permissive", for: "all", to: ["public"], using: sql`(SELECT string_to_array(rebase.roles(), ',') && ARRAY['admin'])`, withCheck: sql`(SELECT string_to_array(rebase.roles(), ',') && ARRAY['admin'])` }),
32
34
  pgPolicy("orders_default_admin_read", { as: "permissive", for: "select", to: ["public"], using: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))` }),
33
35
  pgPolicy("orders_default_admin_write_insert", { as: "permissive", for: "insert", to: ["public"], withCheck: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))` }),
34
36
  pgPolicy("orders_default_admin_write_update", { as: "permissive", for: "update", to: ["public"], using: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))`, withCheck: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))` }),
@@ -45,6 +47,7 @@ export const products = pgTable("products", {
45
47
  categoryId: integer("category_id").references((): AnyPgColumn => categories.id, { onDelete: "set null" })
46
48
  }, (table) => ([
47
49
  index("products_category_id_ix_066548f").on(table.categoryId.asc().nullsLast()),
50
+ pgPolicy("products_all_c924b8e", { as: "permissive", for: "all", to: ["public"], using: sql`(SELECT string_to_array(rebase.roles(), ',') && ARRAY['admin'])`, withCheck: sql`(SELECT string_to_array(rebase.roles(), ',') && ARRAY['admin'])` }),
48
51
  pgPolicy("products_default_admin_read", { as: "permissive", for: "select", to: ["public"], using: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))` }),
49
52
  pgPolicy("products_default_admin_write_insert", { as: "permissive", for: "insert", to: ["public"], withCheck: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))` }),
50
53
  pgPolicy("products_default_admin_write_update", { as: "permissive", for: "update", to: ["public"], using: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))`, withCheck: sql`(SELECT (rebase.uid() IS NULL) OR (string_to_array(rebase.roles(), ',') && ARRAY['admin']))` }),
@@ -38,3 +38,16 @@ This is a **headless** Rebase project: a REST API, auth, storage and realtime ov
38
38
  6. **In `backend/functions/`, always import from `@rebasepro/server/functions`** — never from `@rebasepro/server`. Both work today; the subpath is the portable one, and it also gives you the typed context accessors (`getUser`, `getDriver`, `requireDriver`) instead of casting `c.get("user")`. The package root is for a server entrypoint, not for route handlers.
39
39
  7. **Never read `process.env` at the top of a function file.** A module-scope read that comes back undefined throws at import time, and the loader reports that as a *skipped function* — the route simply 404s with no error attached to it. Read configuration inside the handler with `requireEnv(c, "NAME")`, or build a client once with `lazyResource(env => new Client(env.KEY))`.
40
40
  8. **Work that outlives the response goes in `waitUntil(c, promise)`**, not a floating promise. A floating promise is dropped when the process shuts down mid-deploy; `waitUntil` is what a graceful shutdown waits for.
41
+
42
+ ## Backend runtime (`runtime: "managed"` on Rebase Cloud)
43
+
44
+ `rebase.json` declares the backend as `runtime: "managed"`: Rebase Cloud runs this bundle on its own image, and nothing about that image can be changed. Plan backend features against these limits, from [Managed backend runtime](https://rebase.pro/docs/deployment/managed-runtime/). Where that page marks something **not settled**, say so instead of guessing.
45
+
46
+ - **Node 22 on Debian slim, `linux`/`x64`; no native modules.** A native module (`sharp`, `canvas`, `bcrypt`, anything with a `.node` binary) makes `rebase cloud deploy` refuse the bundle. WebAssembly works, and so does `child_process` for a `linux`/`x64` binary an npm package ships inside its tarball. Install scripts never run, so a binary a package downloads in `postinstall` never arrives, and the image has no `ffmpeg`, Python or compiler. For work that needs one, call a hosted API, or ask the human about `runtime: "custom"`.
47
+ - **Private npm packages** install through the project's `.npmrc`, with every token written as `${NPM_TOKEN}` and set by the human with `rebase cloud env set NPM_TOKEN=… --secret`. A literal token in `.npmrc` fails the build.
48
+ - **Memory and disk belong to one instance, until the next deploy.** A module-level `new Map()` cache survives between requests on one instance, but is not shared between instances and is lost on every deploy and restart. `/tmp` is writable, with the same limits. Keep anything worth keeping in the database or in storage.
49
+ - **Requests:** a function times out after 30 seconds (`504 FUNCTION_TIMEOUT`, `REBASE_FUNCTIONS_TIMEOUT_MS`, at most 10 minutes), and a request body over 10 MiB gets a `413` (`REBASE_MAX_BODY_SIZE`). Pass `getSignal(c)` to `fetch`. Put longer work in a job or in `waitUntil`.
50
+ - **Instances:** at least one always runs; nothing scales a backend to zero.
51
+ - **Outbound network:** HTTP and HTTPS (ports 80 and 443) to anywhere, plus 465, 587 and 993 for mail. Every other port is blocked, so a database or Redis outside Rebase Cloud cannot be reached. There is no fixed outbound IP to allowlist.
52
+ - **Configuration:** the human sets variables with `rebase cloud env set NAME=value` (`--secret` makes a value write-only). A change applies at the next deploy. Read variables inside the handler, with `requireEnv(c, "NAME")`.
53
+ - **Generated files** (audio, images, exports) go to storage: `rebase.storage.putObject({ file, key })`. A key under `public/` is served to anyone at `/api/storage/file/<key>`. Any other key needs a download token. The project needs `rebase cloud storage create` once, plus a `storageAuthorize` hook that allows the write.
@@ -0,0 +1,25 @@
1
+ {
2
+ "$generated": "Generated from config by `rebase resources`. Edit the declarations, not this file.",
3
+ "version": 1,
4
+ "resources": [
5
+ {
6
+ "kind": "database",
7
+ "key": "(default)",
8
+ "engine": "postgres",
9
+ "transport": "server",
10
+ "usedBy": [
11
+ "collection:users"
12
+ ]
13
+ },
14
+ {
15
+ "kind": "function",
16
+ "key": "hello",
17
+ "engine": "http",
18
+ "transport": "server",
19
+ "options": {
20
+ "portable": true,
21
+ "file": "backend/functions/hello.ts"
22
+ }
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$generated": "Generated from config by `rebase resources`. Edit the declarations, not this file.",
3
+ "version": 1,
4
+ "resources": [
5
+ {
6
+ "kind": "database",
7
+ "key": "(default)",
8
+ "engine": "postgres",
9
+ "transport": "server",
10
+ "usedBy": [
11
+ "collection:authors",
12
+ "collection:posts",
13
+ "collection:tags",
14
+ "collection:users"
15
+ ]
16
+ },
17
+ {
18
+ "kind": "function",
19
+ "key": "hello",
20
+ "engine": "http",
21
+ "transport": "server",
22
+ "options": {
23
+ "portable": true,
24
+ "file": "backend/functions/hello.ts"
25
+ }
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$generated": "Generated from config by `rebase resources`. Edit the declarations, not this file.",
3
+ "version": 1,
4
+ "resources": [
5
+ {
6
+ "kind": "database",
7
+ "key": "(default)",
8
+ "engine": "postgres",
9
+ "transport": "server",
10
+ "usedBy": [
11
+ "collection:categories",
12
+ "collection:orders",
13
+ "collection:products",
14
+ "collection:users"
15
+ ]
16
+ },
17
+ {
18
+ "kind": "function",
19
+ "key": "hello",
20
+ "engine": "http",
21
+ "transport": "server",
22
+ "options": {
23
+ "portable": true,
24
+ "file": "backend/functions/hello.ts"
25
+ }
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "$generated": "Generated from config by `rebase resources`. Edit the declarations, not this file.",
3
+ "version": 1,
4
+ "resources": [
5
+ {
6
+ "kind": "database",
7
+ "key": "(default)",
8
+ "engine": "postgres",
9
+ "transport": "server"
10
+ },
11
+ {
12
+ "kind": "function",
13
+ "key": "hello",
14
+ "engine": "http",
15
+ "transport": "server",
16
+ "options": {
17
+ "portable": true,
18
+ "file": "backend/functions/hello.ts"
19
+ }
20
+ }
21
+ ]
22
+ }
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env bash
2
+ # Rebase guardrail for Claude Code, installed by `rebase init` and `rebase skills --hooks`.
3
+ #
4
+ # After Claude Code edits a collection file — under config/collections/, or
5
+ # <config>/collections/ when rebase.json moves the backend's config — this runs
6
+ # `rebase verify --quiet` and hands any problem straight back to the agent:
7
+ # an option that does not exist, a UI widget written outside `admin: { ... }`,
8
+ # a collection with no `securityRules`, a migration the collections have moved
9
+ # past. Exit code 2 is how a PostToolUse hook talks to the agent: Claude Code
10
+ # shows it what this prints on stderr, and the agent fixes it before going on.
11
+ #
12
+ # Edits anywhere else return at once. Safe to delete; `rebase skills --hooks`
13
+ # puts it back.
14
+ set -u
15
+
16
+ input="$(cat)"
17
+
18
+ # The edited file, from the tool call Claude Code passes on stdin, and the
19
+ # backend's config directory, from rebase.json (`apps.<backend>.config`, by
20
+ # default `config`) — the runtime and `rebase verify` read the collections from
21
+ # there, so a project that moved them is still checked. Node rather than jq:
22
+ # every Rebase project has Node, not every machine has jq.
23
+ parsed="$(printf '%s' "$input" | node -e '
24
+ let raw = "";
25
+ process.stdin.on("data", chunk => { raw += chunk; });
26
+ process.stdin.on("end", () => {
27
+ let config = "config";
28
+ try {
29
+ const manifest = JSON.parse(require("fs").readFileSync(require("path").join(process.env.CLAUDE_PROJECT_DIR || ".", "rebase.json"), "utf8"));
30
+ const backend = Object.values(manifest.apps || {}).find(app => app && app.type === "backend");
31
+ if (backend && typeof backend.config === "string" && backend.config) config = backend.config.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
32
+ } catch {
33
+ // No rebase.json, or one this cannot read: the conventional layout.
34
+ }
35
+ let file = "";
36
+ try {
37
+ const toolInput = JSON.parse(raw).tool_input || {};
38
+ // Forward slashes on every OS: on Windows the path arrives as
39
+ // C:\Users\me\shop\config\collections\posts.ts, which the case below never matched.
40
+ file = String(toolInput.file_path || toolInput.notebook_path || "").replace(/\\/g, "/");
41
+ } catch {
42
+ // Not a tool call this hook understands: nothing to check.
43
+ }
44
+ process.stdout.write(config + "\n" + file);
45
+ });
46
+ ')"
47
+ config_dir="${parsed%%$'\n'*}"
48
+ file="${parsed#*$'\n'}"
49
+
50
+ case "$file" in
51
+ */"$config_dir"/collections/* | "$config_dir"/collections/*) ;;
52
+ *) exit 0 ;;
53
+ esac
54
+
55
+ cd "${CLAUDE_PROJECT_DIR:-.}" || exit 0
56
+
57
+ # The project's own CLI, pinned with the rest of its @rebasepro packages.
58
+ #
59
+ # Not installed yet is the state `rebase init --yes` leaves, and it is not a
60
+ # problem in the collections the agent just edited, so it never blocks the
61
+ # edit. There is no `npx` fallback: `npx --no rebase` with nothing installed
62
+ # exits 1 ("could not determine executable to run"), which read as a failed
63
+ # verify and told the agent to fix collections that were fine.
64
+ if [ ! -x node_modules/.bin/rebase ]; then
65
+ echo "Rebase guardrail skipped: this project's dependencies are not installed (no node_modules/.bin/rebase). Run the install — pnpm install, or your package manager's — to check collections after each edit." >&2
66
+ exit 0
67
+ fi
68
+ output="$(node_modules/.bin/rebase verify --quiet 2>&1)"
69
+ status=$?
70
+
71
+ if [ "$status" -eq 0 ]; then
72
+ exit 0
73
+ fi
74
+
75
+ # A project pinned to a CLI from before `rebase verify` answers "Unknown
76
+ # command", and a CLI whose interpreter is missing cannot run at all (127).
77
+ # Neither is a problem in the collections the agent just edited, so neither
78
+ # blocks the edit: say why the check was skipped, and go on.
79
+ case "$output" in
80
+ *"Unknown command"*verify*)
81
+ echo "Rebase guardrail skipped: this project's @rebasepro/cli predates \`rebase verify\`. Upgrade it with \`rebase upgrade\` to check collections after each edit." >&2
82
+ exit 0
83
+ ;;
84
+ esac
85
+ if [ "$status" -eq 127 ]; then
86
+ echo "Rebase guardrail skipped: this project's rebase CLI could not be run. Reinstall the dependencies to check collections after each edit." >&2
87
+ exit 0
88
+ fi
89
+
90
+ {
91
+ project_dir="${CLAUDE_PROJECT_DIR:-}"
92
+ echo "rebase verify failed after editing ${file#"${project_dir//\\//}/"}:"
93
+ echo ""
94
+ echo "$output"
95
+ echo ""
96
+ echo "Each problem names its file, the path inside the collection config, and the fix."
97
+ echo "Fix them in ${config_dir}/collections/ before moving on; run \`rebase verify\` for the full report."
98
+ } >&2
99
+ exit 2
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
+ "hooks": {
4
+ "PostToolUse": [
5
+ {
6
+ "matcher": "Edit|MultiEdit|Write",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/post-edit-collection.sh\"",
11
+ "timeout": 60
12
+ }
13
+ ]
14
+ }
15
+ ]
16
+ }
17
+ }
@@ -169,10 +169,11 @@ VITE_API_URL=
169
169
  # STORAGE_TYPE=local
170
170
 
171
171
  # --- Local filesystem (default — great for dev and single-server deployments) --
172
- # Unset, files land in <project>/uploads, under `rebase dev` and in the server
173
- # `rebase eject` writes alike. A relative path is read from the directory the
174
- # server runs in: the project root under `rebase dev`, backend/ after
175
- # `rebase eject`. So ./uploads names two places, and an absolute path one.
172
+ # Unset, files land in <project>/uploads, under `rebase dev`, `rebase start`
173
+ # and in the server `rebase eject` writes alike. A relative path is read from
174
+ # the directory the server runs in: the project root under `rebase dev`,
175
+ # backend/ after `rebase eject`. So ./uploads names two places, and an absolute
176
+ # path one.
176
177
  # STORAGE_PATH=/absolute/path/to/uploads
177
178
  # FORCE_LOCAL_STORAGE=true # Only with a durable volume holding STORAGE_PATH
178
179
 
@@ -242,6 +243,9 @@ VITE_API_URL=
242
243
  # send that key. Leave it unset under `rebase dev`: they find the server it
243
244
  # started, and send nothing while it is stopped. Set it when the server runs
244
245
  # some other way — `rebase start`, a container, another host.
246
+ # `rebase cloud init --claim` sets it to the cloud project's address, for the
247
+ # app, the SDK and the MCP server; these commands then go there too and say
248
+ # so. Set it in the shell to reach a local server instead.
245
249
  # REBASE_BASE_URL=http://localhost:3001
246
250
 
247
251
  # Disable switching to database-level roles (e.g. 'admin') for Studio SQL execution (optional)
@@ -266,9 +270,15 @@ REBASE_VERSION=latest
266
270
 
267
271
  # Whether boot provisions the schema your collections describe.
268
272
  # ensure create missing tables, columns and enum types, additively (the default)
269
- # none leave the schema alone
273
+ # none leave your collections' schema alone (auth's tables are still kept current)
270
274
  # Any other value refuses to boot. Boot never runs migration files: the ones
271
275
  # `rebase db generate` writes to `backend/drizzle/migrations` are applied with
272
276
  # `rebase db migrate`.
273
277
  # REBASE_MIGRATE_ON_BOOT=ensure
274
278
 
279
+ # `false`: change nothing in the database at all — no table, column, policy,
280
+ # role or grant, auth's included — for a database only your own change process
281
+ # may change. Its schema then comes from a provisioning run that process
282
+ # controls (REBASE_PROVISION_ONLY=true). Only `true` and `false` are accepted.
283
+ # REBASE_PROVISION=true
284
+
@@ -16,6 +16,7 @@
16
16
  | `pnpm skills:install` | Install the Rebase skills for your assistant |
17
17
  | `pnpm example` | Run `scripts/example.ts` against the running backend — the SDK, end to end |
18
18
  | `rebase doctor` | What disagrees between collections, generated schema and the live database — run this before guessing |
19
+ | `rebase verify` | After every collection edit, and in CI: options that do not exist, UI options outside `admin`, missing `securityRules`, migrations behind the collections. `--json` names each file, path and fix. Claude Code runs it for you through `.claude/hooks/post-edit-collection.sh` |
19
20
  | `rebase resources --write` | After declaring a database, bucket or topic in `config/resources.ts`: regenerate `rebase.resources.json` and commit it (`pnpm build` does this too) |
20
21
  | `pnpm deploy` | Deploys this project. Never run it; see below |
21
22
 
@@ -37,4 +38,23 @@
37
38
  5. **In `backend/functions/`, always import from `@rebasepro/server/functions`** — never from `@rebasepro/server`. Both work today; the subpath is the portable one, and it also gives you the typed context accessors (`getUser`, `getDriver`, `requireDriver`) instead of casting `c.get("user")`. The package root is for a server entrypoint, not for route handlers.
38
39
  6. **Never read `process.env` at the top of a function file.** A module-scope read that comes back undefined throws at import time, and the loader reports that as a *skipped function* — the route simply 404s with no error attached to it. Read configuration inside the handler with `requireEnv(c, "NAME")`, or build a client once with `lazyResource(env => new Client(env.KEY))`.
39
40
  7. **Work that outlives the response goes in `waitUntil(c, promise)`**, not a floating promise. A floating promise is dropped when the process shuts down mid-deploy; `waitUntil` is what a graceful shutdown waits for.
40
- 8. **Build UI from the kit, never from scratch**: any custom view, home page, dashboard or entity tab must be composed from `@rebasepro/ui` components (`Card`, `Typography`, `Button`, `Chip`, `Alert`, …) and the theme's tokens. Backgrounds and borders take a surface by role — `bg-surface-frame`, `bg-surface-sheet`, `bg-surface-card`, `bg-surface-raised`, `bg-surface-field`, `bg-surface-well`, `border-hairline` — and those switch with the theme on their own, so they need no `dark:` pair; text takes `<Typography>` or `text-text-primary dark:text-text-primary-dark`. Do **not** invent a palette, a type scale, or hand-written CSS: a hardcoded colour like `#111` is invisible in one of the two themes and nothing will catch it. The roles and their reasoning are in `@rebasepro/ui/theme.css` in your `node_modules`; the live reference is `import { UIReferenceView } from "@rebasepro/app/debug"` — mount it on a route and look before building a view. See [Styling Custom UI](https://rebase.pro/docs/frontend/styling).
41
+
42
+ ## Backend runtime (`runtime: "managed"` on Rebase Cloud)
43
+
44
+ `rebase.json` declares the backend as `runtime: "managed"`: Rebase Cloud runs this bundle on its own image, and nothing about that image can be changed. Plan backend features against these limits, from [Managed backend runtime](https://rebase.pro/docs/deployment/managed-runtime/). Where that page marks something **not settled**, say so instead of guessing.
45
+
46
+ - **Node 22 on Debian slim, `linux`/`x64`; no native modules.** A native module (`sharp`, `canvas`, `bcrypt`, anything with a `.node` binary) makes `rebase cloud deploy` refuse the bundle. WebAssembly works, and so does `child_process` for a `linux`/`x64` binary an npm package ships inside its tarball. Install scripts never run, so a binary a package downloads in `postinstall` never arrives, and the image has no `ffmpeg`, Python or compiler. For work that needs one, call a hosted API, or ask the human about `runtime: "custom"`.
47
+ - **Private npm packages** install through the project's `.npmrc`, with every token written as `${NPM_TOKEN}` and set by the human with `rebase cloud env set NPM_TOKEN=… --secret`. A literal token in `.npmrc` fails the build.
48
+ - **Memory and disk belong to one instance, until the next deploy.** A module-level `new Map()` cache survives between requests on one instance, but is not shared between instances and is lost on every deploy and restart. `/tmp` is writable, with the same limits. Keep anything worth keeping in the database or in storage.
49
+ - **Requests:** a function times out after 30 seconds (`504 FUNCTION_TIMEOUT`, `REBASE_FUNCTIONS_TIMEOUT_MS`, at most 10 minutes), and a request body over 10 MiB gets a `413` (`REBASE_MAX_BODY_SIZE`). Pass `getSignal(c)` to `fetch`. Put longer work in a job or in `waitUntil`.
50
+ - **Instances:** at least one always runs; nothing scales a backend to zero.
51
+ - **Outbound network:** HTTP and HTTPS (ports 80 and 443) to anywhere, plus 465, 587 and 993 for mail. Every other port is blocked, so a database or Redis outside Rebase Cloud cannot be reached. There is no fixed outbound IP to allowlist.
52
+ - **Configuration:** the human sets variables with `rebase cloud env set NAME=value` (`--secret` makes a value write-only). A change applies at the next deploy. Read variables inside the handler, with `requireEnv(c, "NAME")`.
53
+ - **Generated files** (audio, images, exports) go to storage: `rebase.storage.putObject({ file, key })`. A key under `public/` is served to anyone at `/api/storage/file/<key>`. Any other key needs a download token. The project needs `rebase cloud storage create` once, plus a `storageAuthorize` hook that allows the write.
54
+
55
+ ## Admin UI — only when you build it
56
+
57
+ These rules apply to views inside the Rebase admin panel: custom views, home pages, dashboards and entity tabs. A public site or a static app this project serves (an `apps` entry with `"type": "static"`) is not the admin panel. Style it the way that app is already styled. It does not need `@rebasepro/ui`.
58
+
59
+ - **Build admin UI from the kit, never from scratch**: every one of those views is composed from `@rebasepro/ui` components (`Card`, `Typography`, `Button`, `Chip`, `Alert`, …) and the theme's tokens. Backgrounds and borders take a surface by role — `bg-surface-frame`, `bg-surface-sheet`, `bg-surface-card`, `bg-surface-raised`, `bg-surface-field`, `bg-surface-well`, `border-hairline` — and those switch with the theme on their own, so they need no `dark:` pair; text takes `<Typography>` or `text-text-primary dark:text-text-primary-dark`. Do **not** invent a palette, a type scale, or hand-written CSS: a hardcoded colour like `#111` is invisible in one of the two themes and nothing will catch it. The roles and their reasoning are in `@rebasepro/ui/theme.css` in your `node_modules`; the live reference is `import { UIReferenceView } from "@rebasepro/app/debug"` — mount it on a route and look before building a view. See [Styling Custom UI](https://rebase.pro/docs/frontend/styling).
60
+ - **Set `admin.span` on every property you write, from what it holds.** The admin form puts every field at full width unless the property says otherwise, pairing only two neighbouring fields with the same small editor (two numbers, two switches, two dates, two short selects); it never pairs free text and never looks at the title. Pair two short values that belong together (`2` + `2`: start / end date, provider / region; `1` for numbers, booleans and short codes beside their siblings), keep anything that reads long at `4` (names, URLs, emails, ids, descriptions, markdown, files), make each row add up to 4, and declare pairs next to each other. See the `rebase-collections` skill, "Form layout".
@@ -4,5 +4,28 @@ import ordersCollection from "./orders";
4
4
  // Resolves after `rebase init` copies this file into config/collections/,
5
5
  // next to the shared users.ts — not from inside presets/, which never runs.
6
6
  import usersCollection from "./users";
7
+ import type { SecurityRule } from "@rebasepro/types";
8
+
9
+ /**
10
+ * Applied to any collection in this directory that declares no
11
+ * `securityRules` of its own: admins read and write, nobody else.
12
+ *
13
+ * Said out loud rather than left to the framework's admin-only default, so
14
+ * `rebase verify` (and the reader) can tell a decision from an omission. A
15
+ * storefront opens tables up one at a time: `{ operation: "select", access:
16
+ * "public" }` on products and categories for a public catalogue, and
17
+ * `ownerField` on orders once they carry the customer's user id.
18
+ */
19
+ export const defaultSecurityRules: SecurityRule[] = [
20
+ { operation: "all", roles: ["admin"] }
21
+ ];
7
22
 
8
23
  export const collections = [productsCollection, categoriesCollection, ordersCollection, usersCollection];
24
+
25
+ // What `rebase dev`, `rebase db push` and the admin panel do when they read this
26
+ // directory, done here as well: a collection with no `securityRules` of its own
27
+ // gets `defaultSecurityRules`, on the collection object itself, so a relation's
28
+ // target carries them too.
29
+ for (const collection of collections) {
30
+ if (!collection.securityRules?.length) collection.securityRules = defaultSecurityRules;
31
+ }
@@ -138,8 +138,11 @@ services:
138
138
  #
139
139
  # `rebase db push` is still what applies the rest: any change that is not
140
140
  # purely additive.
141
- # Set this to `none` to take a deployment out of provisioning entirely.
141
+ # Set this to `none` to leave your collections' schema alone; auth's
142
+ # tables are still kept current. REBASE_PROVISION=false changes nothing
143
+ # in the database at all.
142
144
  REBASE_MIGRATE_ON_BOOT: ${REBASE_MIGRATE_ON_BOOT:-ensure}
145
+ REBASE_PROVISION: ${REBASE_PROVISION:-true}
143
146
 
144
147
  # Uploads land on the named volume below, which survives restarts. That is
145
148
  # the case FORCE_LOCAL_STORAGE exists to acknowledge — without a durable