@rebasepro/cli 0.10.1-canary.d8d45b2 → 0.10.1-canary.ed78a2c

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/cli",
3
- "version": "0.10.1-canary.d8d45b2",
3
+ "version": "0.10.1-canary.ed78a2c",
4
4
  "description": "Developer tools for Rebase projects",
5
5
  "main": "./dist/index.es.js",
6
6
  "module": "./dist/index.es.js",
@@ -31,12 +31,12 @@
31
31
  "execa": "^9.6.1",
32
32
  "inquirer": "14.0.2",
33
33
  "jiti": "^2.7.0",
34
- "@rebasepro/agent-skills": "0.10.1-canary.d8d45b2",
35
- "@rebasepro/client": "0.10.1-canary.d8d45b2",
36
- "@rebasepro/codegen": "0.10.1-canary.d8d45b2",
37
- "@rebasepro/types": "0.10.1-canary.d8d45b2",
38
- "@rebasepro/server-postgres": "0.10.1-canary.d8d45b2",
39
- "@rebasepro/server": "0.10.1-canary.d8d45b2"
34
+ "@rebasepro/agent-skills": "0.10.1-canary.ed78a2c",
35
+ "@rebasepro/client": "0.10.1-canary.ed78a2c",
36
+ "@rebasepro/server": "0.10.1-canary.ed78a2c",
37
+ "@rebasepro/server-postgres": "0.10.1-canary.ed78a2c",
38
+ "@rebasepro/codegen": "0.10.1-canary.ed78a2c",
39
+ "@rebasepro/types": "0.10.1-canary.ed78a2c"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "^25.9.3",
@@ -15,6 +15,7 @@ import {
15
15
  } from "@rebasepro/server";
16
16
  import { createPostgresDatabaseConnection, createPostgresAdapter } from "@rebasepro/server-postgres";
17
17
  import { env } from "./env.js";
18
+ import { storageAuthorize } from "./storage.js";
18
19
 
19
20
  const __filename = fileURLToPath(import.meta.url);
20
21
  const __dirname = path.dirname(__filename);
@@ -153,6 +154,12 @@ pass: env.SMTP_PASS! }
153
154
  type: "local",
154
155
  basePath: env.STORAGE_PATH || path.resolve(__dirname, "../../uploads")
155
156
  },
157
+ // Storage is not under row-level security, so this hook IS its access
158
+ // model — the server refuses to boot in production without one, because
159
+ // "signed in" would otherwise be the only thing between a caller and every
160
+ // file in the bucket. See storage.ts; the default scopes each caller to
161
+ // `users/<uid>/` and is meant to be replaced with your own rule.
162
+ storageAuthorize,
156
163
  history: true,
157
164
  enableSwagger: true
158
165
  });
@@ -0,0 +1,71 @@
1
+ import { isPublicStoragePath, type StorageAuthorize } from "@rebasepro/types";
2
+
3
+ /**
4
+ * Who may do what to files in storage.
5
+ *
6
+ * Storage is **not** under row-level security. Tables are: a request runs as
7
+ * `rebase_user` and Postgres decides row by row. Object storage has no equivalent,
8
+ * so keys share one flat namespace and this hook is the whole authorization model.
9
+ * Without it the server refuses to boot in production, because "authenticated"
10
+ * would be the only thing between a signed-in caller and every file in the bucket —
11
+ * they can `GET /storage/list?prefix=` to enumerate every key, then read, overwrite
12
+ * or delete any of them.
13
+ *
14
+ * It lives in `backend/src/` rather than a config package because BaaS mode has no
15
+ * config package: collections come from the live database, so there is nothing to
16
+ * declare and nowhere else to put this.
17
+ *
18
+ * ## The default this starter ships
19
+ *
20
+ * A deliberately conservative one, because a BaaS project's storage semantics are
21
+ * whatever *your* application says they are and this file cannot know:
22
+ *
23
+ * | operation | who |
24
+ * | --- | --- |
25
+ * | `read` of `public/…` | anyone, signed in or not |
26
+ * | everything else | a signed-in caller under their own `users/<uid>/` prefix |
27
+ *
28
+ * Gating `list` on the prefix is the important half. Enumeration is what turns "keys
29
+ * are hard to guess" into "keys are known", and it is step one of the attack the
30
+ * boot guard exists to prevent.
31
+ *
32
+ * ## Replace this with your model
33
+ *
34
+ * Prefix rules are a starting point, not a general answer: they work for writes
35
+ * because a write *puts* the object there, but anything already in the bucket needs
36
+ * the row that owns it consulted. `ctx.data` is handed in for exactly that — trusted,
37
+ * RLS-bypassing reads, because the hook *is* the authorization decision and running
38
+ * it through a reader already narrowed by the caller's permissions would be circular:
39
+ *
40
+ * ```ts
41
+ * export const storageAuthorize: StorageAuthorize = async ({ key, user, operation, data }) => {
42
+ * if (operation === "read" && isPublicStoragePath(key)) return true;
43
+ * if (!user) return false;
44
+ * const row = (await data?.collection("attachments").find({
45
+ * where: { storage_key: ["==", key] }, limit: 1
46
+ * }))?.data?.[0];
47
+ * return row?.owner_id === user.uid;
48
+ * };
49
+ * ```
50
+ *
51
+ * If your bucket really is a public read-only CDN, `storagePublicRead: true` says so
52
+ * instead. If every signed-in caller is genuinely trusted with every file,
53
+ * `storageInsecureAllowAnyAuthenticated: true` says *that* — it is named to be read
54
+ * twice. Both are set on `initializeRebaseBackend` in `index.ts`.
55
+ */
56
+ export const storageAuthorize: StorageAuthorize = ({ key, user, operation }) => {
57
+ // The `public/` prefix is the framework's convention for world-readable objects.
58
+ if (operation === "read" && isPublicStoragePath(key)) {
59
+ return true;
60
+ }
61
+
62
+ // `user` is null on routes that allow unauthenticated access.
63
+ if (!user) {
64
+ return false;
65
+ }
66
+
67
+ // Each caller gets their own namespace. A listing is scoped the same way, so a
68
+ // prefix outside it — including the empty prefix, which would enumerate the
69
+ // whole bucket — is denied.
70
+ return key.startsWith(`users/${user.uid}/`);
71
+ };
@@ -34,6 +34,9 @@ DATABASE_URL=postgresql://rebase:changeme@localhost:5432/rebase?options=-c%20sea
34
34
  # DB_POOL_CONNECT_TIMEOUT=10000
35
35
 
36
36
  # ── Server ────────────────────────────────────────────────────────────────────
37
+ # Used by `rebase start` (production). `rebase dev` IGNORES this and binds a
38
+ # port derived from the project path, so several projects can run at once —
39
+ # trust the URL `rebase dev` prints, or pin one with `rebase dev --port 3001`.
37
40
  PORT=3001
38
41
  NODE_ENV=development
39
42
  # Allow connecting to localhost / 127.0.0.1 in production mode (for local testing of prod builds)
@@ -74,6 +77,8 @@ FRONTEND_URL=http://localhost:5173
74
77
  # GOOGLE_CLIENT_ID=
75
78
 
76
79
  # ── Frontend (Vite) ──────────────────────────────────────────────────────────
80
+ # For production builds. In dev, `rebase dev` injects the backend port it
81
+ # actually bound, overriding this — see the PORT note above.
77
82
  VITE_API_URL=http://localhost:3001
78
83
  # VITE_GOOGLE_CLIENT_ID=
79
84
 
@@ -43,9 +43,14 @@ pnpm run db:push # or: npm run db:push
43
43
  pnpm dev # or: npm run dev
44
44
  ```
45
45
 
46
- Open **http://localhost:5173** — the admin panel. The first account you
47
- register becomes the admin. The backend API (Hono + PostgreSQL) runs on
48
- port 3001; the frontend (Vite + React) on port 5173.
46
+ `rebase dev` prints the two URLs it actually bound and opens the admin panel
47
+ there. The first account you register becomes the admin.
48
+
49
+ Ports are **derived from this project's path**, not fixed at 3001/5173, so
50
+ several Rebase projects can run at once without colliding. That is why the URL
51
+ in your terminal is the one to trust — and why the `PORT` and `VITE_API_URL` in
52
+ `.env` are ignored by `rebase dev` (they apply to `rebase start`, the production
53
+ server). Pin a port with `rebase dev --port 3001` if you need a stable one.
49
54
 
50
55
  > The `db:push` step is what creates the tables for the example `posts`,
51
56
  > `authors`, and `tags` collections. Skip it and the admin panel still opens,
@@ -16,6 +16,7 @@ import {
16
16
  } from "@rebasepro/server";
17
17
  import { createPostgresDatabaseConnection, createPostgresAdapter } from "@rebasepro/server-postgres";
18
18
  import { enums, relations, tables } from "./schema.generated.js";
19
+ import { storageAuthorize } from "../../config/storage.js";
19
20
  import { env } from "./env.js";
20
21
  import usersCollection from "../../config/collections/users.js";
21
22
 
@@ -154,6 +155,12 @@ pass: env.SMTP_PASS! }
154
155
  type: "local",
155
156
  basePath: env.STORAGE_PATH || path.resolve(__dirname, "../../uploads")
156
157
  },
158
+ // Storage is not under row-level security, so this hook IS its access
159
+ // model — the server refuses to boot in production without one, because
160
+ // "signed in" would otherwise be the only thing between a visitor and
161
+ // every file in the bucket. See config/storage.ts, which also shows the
162
+ // multi-tenant (per-owner) shape and the two escape hatches.
163
+ storageAuthorize,
157
164
  history: true
158
165
  });
159
166
 
@@ -0,0 +1,9 @@
1
+ /// <reference types="@rebasepro/admin-types" />
2
+
3
+ // One line, once per project. `@rebasepro/admin-types` declares the `admin` block on
4
+ // `CollectionConfig` and on every property type by declaration merging, and an
5
+ // augmentation applies to the whole *program* — so this reference is what makes
6
+ // `admin: { … }` legal in every collection file here.
7
+ //
8
+ // A BaaS project has no admin panel and no reason for this file. Without it, `admin` on
9
+ // a collection or a property is a type error, which is the guarantee.
@@ -1,11 +1,10 @@
1
- import { CollectionConfig } from "@rebasepro/types";
1
+ import { defineCollection } from "@rebasepro/admin-types";
2
2
 
3
- const authorsCollection: CollectionConfig = {
3
+ const authorsCollection = defineCollection({
4
4
  name: "Authors",
5
5
  singularName: "Author",
6
6
  slug: "authors",
7
7
  table: "authors",
8
- icon: "Person",
9
8
  properties: {
10
9
  id: {
11
10
  name: "ID",
@@ -34,12 +33,15 @@ const authorsCollection: CollectionConfig = {
34
33
  }
35
34
  }
36
35
  },
37
- propertiesOrder: [
38
- "id",
39
- "name",
40
- "email",
41
- "picture"
42
- ]
43
- };
36
+ admin: {
37
+ icon: "Person",
38
+ propertiesOrder: [
39
+ "id",
40
+ "name",
41
+ "email",
42
+ "picture"
43
+ ]
44
+ }
45
+ });
44
46
 
45
47
  export default authorsCollection;
@@ -1,13 +1,12 @@
1
- import { CollectionConfig } from "@rebasepro/types";
1
+ import { defineCollection } from "@rebasepro/admin-types";
2
2
  import authorsCollection from "./authors.js";
3
3
  import tagsCollection from "./tags.js";
4
4
 
5
- const postsCollection: CollectionConfig = {
5
+ const postsCollection = defineCollection({
6
6
  name: "Posts",
7
7
  singularName: "Post",
8
8
  slug: "posts",
9
9
  table: "posts",
10
- icon: "Article",
11
10
  properties: {
12
11
  id: {
13
12
  name: "ID",
@@ -24,7 +23,7 @@ const postsCollection: CollectionConfig = {
24
23
  content: {
25
24
  name: "Content",
26
25
  type: "string",
27
- ui: {
26
+ admin: {
28
27
  markdown: true
29
28
  }
30
29
  },
@@ -52,20 +51,25 @@ const postsCollection: CollectionConfig = {
52
51
  author: {
53
52
  name: "Author",
54
53
  type: "relation",
55
- relationName: "author",
56
- target: () => authorsCollection,
57
- cardinality: "one",
58
- direction: "owning"
54
+ relation: {
55
+ kind: "belongsTo",
56
+ target: () => authorsCollection,
57
+ relationName: "author",
58
+ }
59
59
  },
60
60
  tags: {
61
61
  name: "Tags",
62
62
  type: "relation",
63
- relationName: "tags",
64
- target: () => tagsCollection,
65
- cardinality: "many",
66
- direction: "owning"
63
+ relation: {
64
+ kind: "manyToMany",
65
+ target: () => tagsCollection,
66
+ relationName: "tags",
67
+ }
67
68
  }
69
+ },
70
+ admin: {
71
+ icon: "Article"
68
72
  }
69
- };
73
+ });
70
74
 
71
75
  export default postsCollection;
@@ -1,11 +1,10 @@
1
- import { CollectionConfig } from "@rebasepro/types";
1
+ import { defineCollection } from "@rebasepro/admin-types";
2
2
 
3
- const categoriesCollection: CollectionConfig = {
3
+ const categoriesCollection = defineCollection({
4
4
  name: "Categories",
5
5
  singularName: "Category",
6
6
  slug: "categories",
7
7
  table: "categories",
8
- icon: "Category",
9
8
  properties: {
10
9
  id: {
11
10
  name: "ID",
@@ -25,7 +24,7 @@ const categoriesCollection: CollectionConfig = {
25
24
  description: {
26
25
  name: "Description",
27
26
  type: "string",
28
- ui: {
27
+ admin: {
29
28
  multiline: true
30
29
  }
31
30
  },
@@ -34,7 +33,10 @@ const categoriesCollection: CollectionConfig = {
34
33
  type: "string"
35
34
  }
36
35
  },
37
- propertiesOrder: ["id", "name", "slug", "description", "icon"]
38
- };
36
+ admin: {
37
+ icon: "Category",
38
+ propertiesOrder: ["id", "name", "slug", "description", "icon"]
39
+ }
40
+ });
39
41
 
40
42
  export default categoriesCollection;
@@ -1,11 +1,10 @@
1
- import { CollectionConfig } from "@rebasepro/types";
1
+ import { defineCollection } from "@rebasepro/admin-types";
2
2
 
3
- const ordersCollection: CollectionConfig = {
3
+ const ordersCollection = defineCollection({
4
4
  name: "Orders",
5
5
  singularName: "Order",
6
6
  slug: "orders",
7
7
  table: "orders",
8
- icon: "Receipt",
9
8
  properties: {
10
9
  id: {
11
10
  name: "ID",
@@ -22,21 +21,31 @@ const ordersCollection: CollectionConfig = {
22
21
  name: "Status",
23
22
  type: "string",
24
23
  enum: [
25
- { id: "pending",
26
- label: "Pending",
27
- color: "gray" },
28
- { id: "processing",
29
- label: "Processing",
30
- color: "blue" },
31
- { id: "shipped",
32
- label: "Shipped",
33
- color: "orange" },
34
- { id: "delivered",
35
- label: "Delivered",
36
- color: "green" },
37
- { id: "cancelled",
38
- label: "Cancelled",
39
- color: "red" }
24
+ {
25
+ id: "pending",
26
+ label: "Pending",
27
+ color: "gray"
28
+ },
29
+ {
30
+ id: "processing",
31
+ label: "Processing",
32
+ color: "blue"
33
+ },
34
+ {
35
+ id: "shipped",
36
+ label: "Shipped",
37
+ color: "orange"
38
+ },
39
+ {
40
+ id: "delivered",
41
+ label: "Delivered",
42
+ color: "green"
43
+ },
44
+ {
45
+ id: "cancelled",
46
+ label: "Cancelled",
47
+ color: "red"
48
+ }
40
49
  ]
41
50
  },
42
51
  total: {
@@ -47,7 +56,7 @@ color: "red" }
47
56
  notes: {
48
57
  name: "Notes",
49
58
  type: "string",
50
- ui: {
59
+ admin: {
51
60
  multiline: true
52
61
  }
53
62
  },
@@ -56,11 +65,14 @@ color: "red" }
56
65
  type: "date",
57
66
  columnName: "created_at",
58
67
  autoValue: "on_create",
59
- ui: {
68
+ admin: {
60
69
  readOnly: true
61
70
  }
62
71
  }
72
+ },
73
+ admin: {
74
+ icon: "Receipt"
63
75
  }
64
- };
76
+ });
65
77
 
66
78
  export default ordersCollection;
@@ -1,12 +1,11 @@
1
- import { CollectionConfig } from "@rebasepro/types";
1
+ import { defineCollection } from "@rebasepro/admin-types";
2
2
  import categoriesCollection from "./categories.js";
3
3
 
4
- const productsCollection: CollectionConfig = {
4
+ const productsCollection = defineCollection({
5
5
  name: "Products",
6
6
  singularName: "Product",
7
7
  slug: "products",
8
8
  table: "products",
9
- icon: "ShoppingCart",
10
9
  properties: {
11
10
  id: {
12
11
  name: "ID",
@@ -21,7 +20,7 @@ const productsCollection: CollectionConfig = {
21
20
  description: {
22
21
  name: "Description",
23
22
  type: "string",
24
- ui: {
23
+ admin: {
25
24
  markdown: true
26
25
  }
27
26
  },
@@ -39,26 +38,36 @@ const productsCollection: CollectionConfig = {
39
38
  name: "Status",
40
39
  type: "string",
41
40
  enum: [
42
- { id: "draft",
43
- label: "Draft",
44
- color: "gray" },
45
- { id: "active",
46
- label: "Active",
47
- color: "green" },
48
- { id: "archived",
49
- label: "Archived",
50
- color: "orange" }
41
+ {
42
+ id: "draft",
43
+ label: "Draft",
44
+ color: "gray"
45
+ },
46
+ {
47
+ id: "active",
48
+ label: "Active",
49
+ color: "green"
50
+ },
51
+ {
52
+ id: "archived",
53
+ label: "Archived",
54
+ color: "orange"
55
+ }
51
56
  ]
52
57
  },
53
58
  category: {
54
59
  name: "Category",
55
60
  type: "relation",
56
- relationName: "category",
57
- target: () => categoriesCollection,
58
- cardinality: "one",
59
- direction: "owning"
61
+ relation: {
62
+ kind: "belongsTo",
63
+ target: () => categoriesCollection,
64
+ relationName: "category",
65
+ }
60
66
  }
67
+ },
68
+ admin: {
69
+ icon: "ShoppingCart"
61
70
  }
62
- };
71
+ });
63
72
 
64
73
  export default productsCollection;
@@ -1,11 +1,10 @@
1
- import { CollectionConfig } from "@rebasepro/types";
1
+ import { defineCollection } from "@rebasepro/admin-types";
2
2
 
3
- const tagsCollection: CollectionConfig = {
3
+ const tagsCollection = defineCollection({
4
4
  name: "Tags",
5
5
  singularName: "Tag",
6
6
  slug: "tags",
7
7
  table: "tags",
8
- icon: "Tag",
9
8
  properties: {
10
9
  id: {
11
10
  name: "ID",
@@ -19,7 +18,10 @@ const tagsCollection: CollectionConfig = {
19
18
  required: true
20
19
  }
21
20
  }
21
+ },
22
+ admin: {
23
+ icon: "Tag"
22
24
  }
23
- };
25
+ });
24
26
 
25
27
  export default tagsCollection;
@@ -1,29 +1,28 @@
1
- import type { CollectionConfig } from "@rebasepro/types";
1
+ import { defineCollection } from "@rebasepro/admin-types";
2
2
 
3
- const usersCollection: CollectionConfig = {
3
+ const usersCollection = defineCollection({
4
4
  name: "Users",
5
5
  singularName: "User",
6
6
  slug: "users",
7
7
  auth: { enabled: true },
8
8
  table: "users",
9
9
  schema: "rebase",
10
- icon: "Users",
11
- group: "Settings",
12
- openEntityMode: "dialog",
13
- disableDefaultActions: ["copy"],
14
10
  securityRules: [
15
- { operation: "select",
16
- roles: ["admin"] },
17
- { operations: ["insert", "update", "delete"],
18
- roles: ["admin"] }
11
+ {
12
+ operation: "select",
13
+ roles: ["admin"]
14
+ },
15
+ {
16
+ operations: ["insert", "update", "delete"],
17
+ roles: ["admin"]
18
+ }
19
19
  ],
20
- sort: ["createdAt", "desc"],
21
20
  properties: {
22
21
  id: {
23
22
  name: "ID",
24
23
  type: "string",
25
24
  isId: "uuid",
26
- ui: {
25
+ admin: {
27
26
  readOnly: true
28
27
  }
29
28
  },
@@ -44,9 +43,8 @@ roles: ["admin"] }
44
43
  name: "Photo URL",
45
44
  type: "string",
46
45
  columnName: "photo_url",
47
- ui: {
48
- url: "image"
49
- }
46
+ url: true,
47
+ admin: { urlPreview: "image" }
50
48
  },
51
49
  roles: {
52
50
  name: "Roles",
@@ -67,7 +65,7 @@ roles: ["admin"] }
67
65
  type: "string",
68
66
  columnName: "password_hash",
69
67
  excludeFromApi: true,
70
- ui: {
68
+ admin: {
71
69
  hideFromCollection: true,
72
70
  disabled: { hidden: true }
73
71
  }
@@ -77,7 +75,7 @@ roles: ["admin"] }
77
75
  type: "boolean",
78
76
  columnName: "email_verified",
79
77
  defaultValue: false,
80
- ui: {
78
+ admin: {
81
79
  hideFromCollection: true,
82
80
  disabled: { hidden: true }
83
81
  }
@@ -87,7 +85,7 @@ roles: ["admin"] }
87
85
  type: "string",
88
86
  columnName: "email_verification_token",
89
87
  excludeFromApi: true,
90
- ui: {
88
+ admin: {
91
89
  hideFromCollection: true,
92
90
  disabled: { hidden: true }
93
91
  }
@@ -96,7 +94,7 @@ roles: ["admin"] }
96
94
  name: "Email Verification Sent At",
97
95
  type: "date",
98
96
  columnName: "email_verification_sent_at",
99
- ui: {
97
+ admin: {
100
98
  hideFromCollection: true,
101
99
  disabled: { hidden: true }
102
100
  }
@@ -107,7 +105,7 @@ roles: ["admin"] }
107
105
  keyValue: true,
108
106
  properties: {},
109
107
  defaultValue: {},
110
- ui: {
108
+ admin: {
111
109
  hideFromCollection: true,
112
110
  disabled: { hidden: true }
113
111
  }
@@ -117,7 +115,7 @@ roles: ["admin"] }
117
115
  type: "date",
118
116
  columnName: "created_at",
119
117
  autoValue: "on_create",
120
- ui: {
118
+ admin: {
121
119
  readOnly: true
122
120
  }
123
121
  },
@@ -126,19 +124,26 @@ roles: ["admin"] }
126
124
  type: "date",
127
125
  columnName: "updated_at",
128
126
  autoValue: "on_update",
129
- ui: {
127
+ admin: {
130
128
  hideFromCollection: true,
131
129
  disabled: { hidden: true }
132
130
  }
133
131
  }
134
132
  },
135
- propertiesOrder: [
136
- "id",
137
- "email",
138
- "displayName",
139
- "roles",
140
- "createdAt"
141
- ]
142
- };
133
+ admin: {
134
+ icon: "Users",
135
+ group: "Settings",
136
+ openEntityMode: "dialog",
137
+ disableDefaultActions: ["copy"],
138
+ sort: ["createdAt", "desc"],
139
+ propertiesOrder: [
140
+ "id",
141
+ "email",
142
+ "displayName",
143
+ "roles",
144
+ "createdAt"
145
+ ]
146
+ }
147
+ });
143
148
 
144
149
  export default usersCollection;