@rebasepro/types 0.10.0 → 0.10.1-canary.14e53ae
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/dist/controllers/client.d.ts +3 -1
- package/dist/controllers/data_driver.d.ts +29 -0
- package/dist/index.es.js +270 -1
- package/dist/index.es.js.map +1 -1
- package/dist/types/backend.d.ts +30 -1
- package/dist/types/channel_bus.d.ts +192 -0
- package/dist/types/collection_contract.d.ts +43 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/project_manifest.d.ts +336 -0
- package/dist/types/schema_version.d.ts +18 -0
- package/dist/types/storage_authorize.d.ts +74 -0
- package/package.json +1 -1
- package/src/controllers/client.ts +3 -2
- package/src/controllers/data_driver.ts +56 -0
- package/src/types/backend.ts +33 -1
- package/src/types/channel_bus.ts +202 -0
- package/src/types/collection_contract.ts +249 -0
- package/src/types/index.ts +5 -0
- package/src/types/policy.ts +25 -11
- package/src/types/project_manifest.ts +362 -0
- package/src/types/schema_version.ts +112 -0
- package/src/types/storage_authorize.ts +77 -0
package/src/types/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./modify_collections";
|
|
|
16
16
|
export * from "./formex";
|
|
17
17
|
export * from "./websockets";
|
|
18
18
|
export * from "./backend";
|
|
19
|
+
export * from "./channel_bus";
|
|
19
20
|
export * from "./translations";
|
|
20
21
|
export * from "./plugins";
|
|
21
22
|
export * from "./builders";
|
|
@@ -33,4 +34,8 @@ export * from "./database_adapter";
|
|
|
33
34
|
export * from "./breadcrumbs";
|
|
34
35
|
export * from "./component_overrides";
|
|
35
36
|
export * from "./api_keys";
|
|
37
|
+
export * from "./project_manifest";
|
|
38
|
+
export * from "./collection_contract";
|
|
39
|
+
export * from "./schema_version";
|
|
40
|
+
export * from "./storage_authorize";
|
|
36
41
|
|
package/src/types/policy.ts
CHANGED
|
@@ -258,21 +258,35 @@ export interface AuthRolesPolicyOperand {
|
|
|
258
258
|
export const policy = {
|
|
259
259
|
true: (): TruePolicyExpression => ({ kind: "true" }),
|
|
260
260
|
false: (): FalsePolicyExpression => ({ kind: "false" }),
|
|
261
|
-
and: (...operands: readonly PolicyExpression[]): AndPolicyExpression => ({ kind: "and",
|
|
262
|
-
|
|
263
|
-
|
|
261
|
+
and: (...operands: readonly PolicyExpression[]): AndPolicyExpression => ({ kind: "and",
|
|
262
|
+
operands: operands as PolicyExpression[] }),
|
|
263
|
+
or: (...operands: readonly PolicyExpression[]): OrPolicyExpression => ({ kind: "or",
|
|
264
|
+
operands: operands as PolicyExpression[] }),
|
|
265
|
+
not: (operand: PolicyExpression): NotPolicyExpression => ({ kind: "not",
|
|
266
|
+
operand }),
|
|
264
267
|
compare: (left: PolicyOperand, op: PolicyCompareOperator, right: PolicyOperand): ComparePolicyExpression =>
|
|
265
|
-
({ kind: "compare",
|
|
266
|
-
|
|
267
|
-
|
|
268
|
+
({ kind: "compare",
|
|
269
|
+
op,
|
|
270
|
+
left,
|
|
271
|
+
right }),
|
|
272
|
+
rolesOverlap: (roles: readonly string[]): RolesOverlapPolicyExpression => ({ kind: "rolesOverlap",
|
|
273
|
+
roles: roles as string[] }),
|
|
274
|
+
rolesContain: (roles: readonly string[]): RolesContainPolicyExpression => ({ kind: "rolesContain",
|
|
275
|
+
roles: roles as string[] }),
|
|
268
276
|
authenticated: (): AuthenticatedPolicyExpression => ({ kind: "authenticated" }),
|
|
269
277
|
serverContext: (): ServerContextPolicyExpression => ({ kind: "serverContext" }),
|
|
270
278
|
existsIn: (args: { collection: string; where: PolicyExpression }): ExistsInPolicyExpression =>
|
|
271
|
-
({ kind: "existsIn",
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
279
|
+
({ kind: "existsIn",
|
|
280
|
+
collection: args.collection,
|
|
281
|
+
where: args.where }),
|
|
282
|
+
raw: (sql: string): RawPolicyExpression => ({ kind: "raw",
|
|
283
|
+
sql }),
|
|
284
|
+
field: (name: string): FieldPolicyOperand => ({ kind: "field",
|
|
285
|
+
name }),
|
|
286
|
+
outerField: (name: string): OuterFieldPolicyOperand => ({ kind: "outerField",
|
|
287
|
+
name }),
|
|
288
|
+
literal: (value: string | number | boolean | null): LiteralPolicyOperand => ({ kind: "literal",
|
|
289
|
+
value }),
|
|
276
290
|
authUid: (): AuthUidPolicyOperand => ({ kind: "authUid" }),
|
|
277
291
|
authRoles: (): AuthRolesPolicyOperand => ({ kind: "authRoles" })
|
|
278
292
|
};
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project manifest (`rebase.json`) and the build artifacts derived from it.
|
|
3
|
+
*
|
|
4
|
+
* Three separate documents live in this file, and keeping them distinct matters:
|
|
5
|
+
*
|
|
6
|
+
* 1. {@link RebaseProjectManifest} — `rebase.json`. **Authored** by the developer,
|
|
7
|
+
* committed to the repository. Declares topology only: which runtime major the
|
|
8
|
+
* project targets, and which apps *this repository* contributes to the project.
|
|
9
|
+
* Schema, security rules, hooks and functions stay in TypeScript under the
|
|
10
|
+
* config package — nothing that needs a type system belongs here.
|
|
11
|
+
*
|
|
12
|
+
* 2. {@link RebaseProjectLink} — the per-checkout link (`.rebase/cloud.json`).
|
|
13
|
+
* **Not committed**, because it is per-developer like a git remote. Says which
|
|
14
|
+
* deployed project this working copy points at, whether that is a Rebase Cloud
|
|
15
|
+
* project or the base URL of a self-hosted backend.
|
|
16
|
+
*
|
|
17
|
+
* 3. {@link RebaseBundleManifest} — `manifest.json` inside a built bundle.
|
|
18
|
+
* **Generated**, never hand-edited. It is the lockfile analogue: the exact
|
|
19
|
+
* contract a built artifact claims to satisfy, which the runtime validates
|
|
20
|
+
* before it boots and a control plane validates before it deploys.
|
|
21
|
+
*
|
|
22
|
+
* A repository declares only the apps it contains. The set of apps belonging to a
|
|
23
|
+
* project is held by the project itself, which is what makes multi-repo projects
|
|
24
|
+
* work: two repositories never need to know about each other, only about the
|
|
25
|
+
* project.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Which kind of thing an app is.
|
|
30
|
+
*
|
|
31
|
+
* - `backend` — the collections/hooks/functions that define the project's API.
|
|
32
|
+
* Exactly one per *project* (not per repository); the registry enforces it.
|
|
33
|
+
* - `static` — a pre-built client bundle (SPA, static site) served over CDN.
|
|
34
|
+
* - `admin` — the Rebase admin panel, either hosted by the platform or built
|
|
35
|
+
* into this repository.
|
|
36
|
+
* - `mobile` — a native app. Registration only: it gets client credentials and
|
|
37
|
+
* configuration, and is never built or hosted here.
|
|
38
|
+
* - `custom` — an arbitrary container image built from a Dockerfile. The eject
|
|
39
|
+
* hatch: full control, no managed-runtime guarantees.
|
|
40
|
+
*/
|
|
41
|
+
export type RebaseAppType = "backend" | "static" | "admin" | "mobile" | "custom";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The backend app: the project's API surface.
|
|
45
|
+
*
|
|
46
|
+
* Paths are relative to the directory holding `rebase.json`. The defaults match
|
|
47
|
+
* the layout `rebase init` scaffolds, so a stock project may declare simply
|
|
48
|
+
* `{ "type": "backend" }`.
|
|
49
|
+
*/
|
|
50
|
+
export interface RebaseBackendAppConfig {
|
|
51
|
+
type: "backend";
|
|
52
|
+
/** Directory of the config package (collections + index). Default `config`. */
|
|
53
|
+
config?: string;
|
|
54
|
+
/** Directory of server functions. Default `backend/functions`. */
|
|
55
|
+
functions?: string;
|
|
56
|
+
/** Directory of cron job definitions. Default `backend/crons` when present. */
|
|
57
|
+
crons?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Path to the generated Drizzle schema module (tables/enums/relations).
|
|
60
|
+
* Default `backend/src/schema.generated.ts`.
|
|
61
|
+
*/
|
|
62
|
+
schema?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Which collections source the runtime uses.
|
|
65
|
+
*
|
|
66
|
+
* - `cms` (default) — collections come from the config package.
|
|
67
|
+
* - `baas` — collections are introspected from the live database at boot and
|
|
68
|
+
* the config package is not required.
|
|
69
|
+
*/
|
|
70
|
+
mode?: "cms" | "baas";
|
|
71
|
+
/**
|
|
72
|
+
* Module path (relative to `config`) exporting the auth users collection as
|
|
73
|
+
* its default export. Default `collections/users`.
|
|
74
|
+
*/
|
|
75
|
+
usersCollection?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A static client bundle — SPA or static site — built here and served from CDN.
|
|
80
|
+
*/
|
|
81
|
+
export interface RebaseStaticAppConfig {
|
|
82
|
+
type: "static";
|
|
83
|
+
/** Package directory containing the client sources. */
|
|
84
|
+
root: string;
|
|
85
|
+
/** Command that produces `output`. Run from the repository root. */
|
|
86
|
+
build?: string;
|
|
87
|
+
/** Directory of built assets, relative to the repository root. */
|
|
88
|
+
output: string;
|
|
89
|
+
/**
|
|
90
|
+
* Serve `index.html` for unmatched paths (client-side routing).
|
|
91
|
+
* Default `true` — the overwhelmingly common case for a client app, and a
|
|
92
|
+
* static *site* generator emits real files for its routes anyway.
|
|
93
|
+
*/
|
|
94
|
+
spa?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The admin panel.
|
|
99
|
+
*
|
|
100
|
+
* `hosted` is the default and means the platform serves it — nothing is built
|
|
101
|
+
* into this repository and nothing ships in the bundle. `bundled` builds it here,
|
|
102
|
+
* which is what a self-hosted or air-gapped deployment wants.
|
|
103
|
+
*/
|
|
104
|
+
export interface RebaseAdminAppConfig {
|
|
105
|
+
type: "admin";
|
|
106
|
+
mode?: "hosted" | "bundled";
|
|
107
|
+
/** Only for `bundled`: package directory containing the admin sources. */
|
|
108
|
+
root?: string;
|
|
109
|
+
/** Only for `bundled`: build command. */
|
|
110
|
+
build?: string;
|
|
111
|
+
/** Only for `bundled`: directory of built assets. */
|
|
112
|
+
output?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A native app. Registered for credentials and configuration; never built here.
|
|
117
|
+
*/
|
|
118
|
+
export interface RebaseMobileAppConfig {
|
|
119
|
+
type: "mobile";
|
|
120
|
+
platform: "ios" | "android" | "other";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* An app built from a Dockerfile into an arbitrary image.
|
|
125
|
+
*
|
|
126
|
+
* This is the deliberate escape hatch. A project containing one is not eligible
|
|
127
|
+
* for the managed runtime — the platform cannot make guarantees about an image
|
|
128
|
+
* it did not build — but it still deploys, and nothing else about the project
|
|
129
|
+
* changes.
|
|
130
|
+
*/
|
|
131
|
+
export interface RebaseCustomAppConfig {
|
|
132
|
+
type: "custom";
|
|
133
|
+
/** Dockerfile path relative to the repository root. */
|
|
134
|
+
dockerfile?: string;
|
|
135
|
+
/** Build context relative to the repository root. Default `.`. */
|
|
136
|
+
context?: string;
|
|
137
|
+
/** Port the container listens on. Default 8080. */
|
|
138
|
+
port?: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type RebaseAppConfig =
|
|
142
|
+
| RebaseBackendAppConfig
|
|
143
|
+
| RebaseStaticAppConfig
|
|
144
|
+
| RebaseAdminAppConfig
|
|
145
|
+
| RebaseMobileAppConfig
|
|
146
|
+
| RebaseCustomAppConfig;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* `rebase.json` — the authored project manifest.
|
|
150
|
+
*/
|
|
151
|
+
export interface RebaseProjectManifest {
|
|
152
|
+
/** JSON Schema URL, for editor completion. Ignored by the tooling. */
|
|
153
|
+
$schema?: string;
|
|
154
|
+
/**
|
|
155
|
+
* The runtime **major** this project targets, as a semver range
|
|
156
|
+
* (e.g. `^1`, `~1.4`, or an exact `1.4.2` to pin).
|
|
157
|
+
*
|
|
158
|
+
* The platform upgrades patches and minors underneath a project without
|
|
159
|
+
* asking; it never crosses a major. See {@link RUNTIME_CONTRACT_VERSION}.
|
|
160
|
+
*/
|
|
161
|
+
runtime: string;
|
|
162
|
+
/**
|
|
163
|
+
* Apps this repository contributes, keyed by app name. The key is the app's
|
|
164
|
+
* identity within the project: it is what `rebase deploy <app>` names, what
|
|
165
|
+
* client credentials are issued against, and what a second repository must
|
|
166
|
+
* not collide with.
|
|
167
|
+
*/
|
|
168
|
+
apps: Record<string, RebaseAppConfig>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The per-checkout project link.
|
|
173
|
+
*
|
|
174
|
+
* Deliberately separate from `rebase.json`: the manifest is committed and shared,
|
|
175
|
+
* while the link is per-developer. Keeping them in one file would mean either
|
|
176
|
+
* committing someone's project id or gitignoring the topology.
|
|
177
|
+
*/
|
|
178
|
+
export interface RebaseProjectLink {
|
|
179
|
+
/**
|
|
180
|
+
* A Rebase Cloud project id, or the base URL of any running Rebase backend
|
|
181
|
+
* (`https://api.example.com`). Both are first-class: every command that
|
|
182
|
+
* accepts a project reference accepts either, so a self-hosted project has
|
|
183
|
+
* the same tooling as a cloud one.
|
|
184
|
+
*/
|
|
185
|
+
project: string;
|
|
186
|
+
/** Organization slug. Cloud projects only. */
|
|
187
|
+
org?: string;
|
|
188
|
+
/** Explicit API base URL, when it differs from the project's default. */
|
|
189
|
+
apiUrl?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Whether a project can run on the managed runtime, and if not, precisely why.
|
|
194
|
+
*
|
|
195
|
+
* The reasons are returned rather than summarised so tooling can print something
|
|
196
|
+
* a developer can act on. "Not eligible" is never a dead end — it selects the
|
|
197
|
+
* custom-runtime path, which still deploys.
|
|
198
|
+
*/
|
|
199
|
+
export interface ManagedCompatibility {
|
|
200
|
+
eligible: boolean;
|
|
201
|
+
reasons: string[];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
205
|
+
// Bundle
|
|
206
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Version of the bundle *format* itself.
|
|
210
|
+
*
|
|
211
|
+
* Bumped only when the on-disk layout changes in a way an older runtime could
|
|
212
|
+
* not read. A runtime accepts any bundle whose `bundleFormat` is less than or
|
|
213
|
+
* equal to its own — old bundles keep booting on new runtimes, which is the
|
|
214
|
+
* whole point of separating the artifact from the engine.
|
|
215
|
+
*/
|
|
216
|
+
export const BUNDLE_FORMAT_VERSION = 1;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The runtime contract major.
|
|
220
|
+
*
|
|
221
|
+
* Distinct from the `@rebasepro/server` package version: the package may release
|
|
222
|
+
* any number of minors and patches while this stays put. It changes only when
|
|
223
|
+
* the bundle/runtime contract breaks compatibility, and a project's
|
|
224
|
+
* `manifest.runtime` range is matched against *this*.
|
|
225
|
+
*/
|
|
226
|
+
export const RUNTIME_CONTRACT_VERSION = 1;
|
|
227
|
+
|
|
228
|
+
/** Where the runtime finds each part of the bundle. Paths are bundle-relative. */
|
|
229
|
+
export interface RebaseBundleEntrypoints {
|
|
230
|
+
/** Compiled config package directory (collections live under it). */
|
|
231
|
+
config?: string;
|
|
232
|
+
/** Compiled collections directory, when it differs from `<config>/collections`. */
|
|
233
|
+
collections?: string;
|
|
234
|
+
/** Compiled functions directory. */
|
|
235
|
+
functions?: string;
|
|
236
|
+
/** Compiled crons directory. */
|
|
237
|
+
crons?: string;
|
|
238
|
+
/** Compiled Drizzle schema module. */
|
|
239
|
+
schema?: string;
|
|
240
|
+
/** Module exporting the auth users collection (default export). */
|
|
241
|
+
usersCollection?: string;
|
|
242
|
+
/** Built admin assets, when the admin panel is bundled rather than hosted. */
|
|
243
|
+
admin?: string;
|
|
244
|
+
/** Built static assets to serve from the runtime, when not on a CDN. */
|
|
245
|
+
static?: string;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* A native module found in the dependency closure.
|
|
250
|
+
*
|
|
251
|
+
* Recorded rather than merely counted so a rejection can name the offending
|
|
252
|
+
* package instead of saying "something here is native".
|
|
253
|
+
*/
|
|
254
|
+
export interface NativeDependency {
|
|
255
|
+
name: string;
|
|
256
|
+
/** Why it was flagged — a `.node` binary, a gyp build, or an install script. */
|
|
257
|
+
reason: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* `manifest.json` — generated, and the document the runtime and control plane
|
|
262
|
+
* both validate against.
|
|
263
|
+
*/
|
|
264
|
+
export interface RebaseBundleManifest {
|
|
265
|
+
/** @see BUNDLE_FORMAT_VERSION */
|
|
266
|
+
bundleFormat: number;
|
|
267
|
+
runtime: {
|
|
268
|
+
/** The `runtime` range copied from `rebase.json`. */
|
|
269
|
+
range: string;
|
|
270
|
+
/** Exact `@rebasepro/server` version this bundle was built against. */
|
|
271
|
+
builtAgainst: string;
|
|
272
|
+
/** Runtime contract major this bundle requires. */
|
|
273
|
+
contract: number;
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Hash of the compiled collection definitions.
|
|
277
|
+
*
|
|
278
|
+
* This is the contract stamp. A generated SDK records the value it was built
|
|
279
|
+
* from, a client sends it back, and a mismatch is what lets the platform say
|
|
280
|
+
* "this app was built against an older schema" instead of failing mysteriously
|
|
281
|
+
* at the first request. It covers collections only — a hook edit does not
|
|
282
|
+
* change a client's contract, so it must not invalidate every SDK.
|
|
283
|
+
*/
|
|
284
|
+
schemaVersion: string;
|
|
285
|
+
/** Which app in `rebase.json` this bundle was built from. */
|
|
286
|
+
app: string;
|
|
287
|
+
/**
|
|
288
|
+
* What the runtime does with this bundle.
|
|
289
|
+
*
|
|
290
|
+
* - `cms` — a backend with declared collections; the runtime provisions their
|
|
291
|
+
* tables and serves the data API.
|
|
292
|
+
* - `baas` — a backend that introspects an existing database rather than
|
|
293
|
+
* declaring collections.
|
|
294
|
+
* - `static` — no backend at all: the bundle is a built SPA (`entry.static`),
|
|
295
|
+
* and the runtime only serves those assets. No database, no data sources —
|
|
296
|
+
* this is how a `static`/`admin` app runs on the same image as the backend.
|
|
297
|
+
*/
|
|
298
|
+
mode: "cms" | "baas" | "static";
|
|
299
|
+
entry: RebaseBundleEntrypoints;
|
|
300
|
+
/** Collection slugs contained in the bundle, for quick inspection. */
|
|
301
|
+
collections?: string[];
|
|
302
|
+
hooks: {
|
|
303
|
+
/**
|
|
304
|
+
* Whether the dependency closure contains native code.
|
|
305
|
+
*
|
|
306
|
+
* The managed runtime refuses these: a prebuilt binary cannot be run on
|
|
307
|
+
* an image the platform did not build it for, and the honest failure is
|
|
308
|
+
* at deploy time rather than at 3am in a crash loop.
|
|
309
|
+
*/
|
|
310
|
+
native: boolean;
|
|
311
|
+
nativeModules?: NativeDependency[];
|
|
312
|
+
};
|
|
313
|
+
/**
|
|
314
|
+
* What the bundle's config says about storage access control.
|
|
315
|
+
*
|
|
316
|
+
* Storage is not under RLS and its keys share one flat namespace, so a
|
|
317
|
+
* deployment with file storage enabled and no access model serves every
|
|
318
|
+
* user's files to every signed-in user. The runtime refuses to boot in that
|
|
319
|
+
* state — which, on a hosted platform that enables storage from the *console*
|
|
320
|
+
* rather than from the bundle, surfaces as a crash loop the developer cannot
|
|
321
|
+
* read.
|
|
322
|
+
*
|
|
323
|
+
* Recording it here lets a host reject the deploy with the reason instead.
|
|
324
|
+
* Absent on bundles built before this field existed.
|
|
325
|
+
*/
|
|
326
|
+
storage?: {
|
|
327
|
+
/** Whether the config package exports a `storageAuthorize` hook. */
|
|
328
|
+
authorize: boolean;
|
|
329
|
+
};
|
|
330
|
+
deps: {
|
|
331
|
+
/** Runtime dependencies of user code, as declared. */
|
|
332
|
+
declared: Record<string, string>;
|
|
333
|
+
};
|
|
334
|
+
build: {
|
|
335
|
+
/** `@rebasepro/cli` version that produced this bundle. */
|
|
336
|
+
cli: string;
|
|
337
|
+
/** Node major the bundle was compiled on. */
|
|
338
|
+
node: string;
|
|
339
|
+
/** ISO-8601. */
|
|
340
|
+
createdAt: string;
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** The contract a running backend serves at `GET /api/meta/contract`. */
|
|
345
|
+
export interface RebaseProjectContract {
|
|
346
|
+
/** Matches {@link RebaseBundleManifest.schemaVersion}. */
|
|
347
|
+
schemaVersion: string;
|
|
348
|
+
runtime: {
|
|
349
|
+
/** `@rebasepro/server` version currently running. */
|
|
350
|
+
version: string;
|
|
351
|
+
contract: number;
|
|
352
|
+
};
|
|
353
|
+
mode: "cms" | "baas";
|
|
354
|
+
/** Full collection definitions, serialized — the input to SDK generation. */
|
|
355
|
+
collections: unknown[];
|
|
356
|
+
/** Collection slugs, for cheap inspection without parsing the definitions. */
|
|
357
|
+
collectionSlugs: string[];
|
|
358
|
+
generatedAt: string;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Header carrying the schema version an SDK was generated from. */
|
|
362
|
+
export const SCHEMA_VERSION_HEADER = "x-rebase-schema";
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { CollectionConfig } from "./collections";
|
|
2
|
+
import { serializeCollections } from "./collection_contract";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The schema version stamp.
|
|
6
|
+
*
|
|
7
|
+
* One function, used in three places that must agree or the whole drift-detection
|
|
8
|
+
* story is noise: `rebase build` writes it into a bundle manifest, the runtime
|
|
9
|
+
* serves it from the contract endpoint, and a generated SDK records the value it
|
|
10
|
+
* was built from. If any two of those computed it differently, every client would
|
|
11
|
+
* look permanently out of date.
|
|
12
|
+
*
|
|
13
|
+
* It covers **collections only** — the client's contract is the shape of the
|
|
14
|
+
* data, so editing a hook or a server function must not invalidate every SDK in
|
|
15
|
+
* every repository. That is a deliberate narrowing, not an oversight.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** Stable stringify: object keys sorted at every level, so key order cannot alter the hash. */
|
|
19
|
+
function canonicalize(value: unknown): string {
|
|
20
|
+
if (value === null || typeof value !== "object") {
|
|
21
|
+
return JSON.stringify(value) ?? "null";
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
return `[${value.map(canonicalize).join(",")}]`;
|
|
25
|
+
}
|
|
26
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
27
|
+
.filter(([, v]) => v !== undefined)
|
|
28
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
29
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`).join(",")}}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reduce a collection to the parts a generated client is actually built from.
|
|
34
|
+
*
|
|
35
|
+
* The version answers one question — "is this SDK stale?" — so it must change
|
|
36
|
+
* exactly when the generated types could change, and never otherwise. Hashing a
|
|
37
|
+
* whole collection fails both halves of that:
|
|
38
|
+
*
|
|
39
|
+
* - Security rules, callbacks, icons, groups and UI settings do not appear in a
|
|
40
|
+
* generated client, so including them reports perfectly current SDKs as stale.
|
|
41
|
+
* - Worse, they are not stable *inputs*. The runtime applies default security
|
|
42
|
+
* rules when it loads collections, so the same source hashed before and after
|
|
43
|
+
* loading produced two different answers — a build-time stamp that could never
|
|
44
|
+
* match the server that served it.
|
|
45
|
+
*
|
|
46
|
+
* Codegen reads the slug (for the `Database` key and type names), the properties,
|
|
47
|
+
* and the relations. That is the projection.
|
|
48
|
+
*/
|
|
49
|
+
function projectForCodegen(collection: CollectionConfig): Record<string, unknown> {
|
|
50
|
+
const source = collection as CollectionConfig & {
|
|
51
|
+
relations?: unknown;
|
|
52
|
+
subcollections?: CollectionConfig[];
|
|
53
|
+
path?: string;
|
|
54
|
+
engine?: unknown;
|
|
55
|
+
dataSource?: unknown;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
slug: collection.slug ?? source.path,
|
|
60
|
+
properties: collection.properties,
|
|
61
|
+
relations: source.relations,
|
|
62
|
+
// The engine decides whether relations are resolved at all: codegen asks
|
|
63
|
+
// `getDataSourceCapabilities(collection.engine).supportsRelations`, and an
|
|
64
|
+
// engine that answers no drops every foreign-key column from the
|
|
65
|
+
// generated Row/Insert/Update types. Moving a collection to such an
|
|
66
|
+
// engine is a real change to the generated types, so it has to move the
|
|
67
|
+
// version. `dataSource` is what resolves to `engine`, so it counts too.
|
|
68
|
+
engine: source.engine,
|
|
69
|
+
dataSource: source.dataSource,
|
|
70
|
+
subcollections: source.subcollections?.map(projectForCodegen)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compute the canonical string a schema version hashes.
|
|
76
|
+
*
|
|
77
|
+
* Exposed separately so the hashing itself can differ by environment: Node has
|
|
78
|
+
* `crypto`, and callers without it can still compare canonical forms directly.
|
|
79
|
+
*/
|
|
80
|
+
export function canonicalSchemaPayload(collections: CollectionConfig[]): string {
|
|
81
|
+
const projected = serializeCollections(collections)
|
|
82
|
+
.map(collection => projectForCodegen(collection as CollectionConfig));
|
|
83
|
+
return canonicalize(projected);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A short, non-cryptographic digest of the canonical payload.
|
|
88
|
+
*
|
|
89
|
+
* FNV-1a style, 64 bits, as two 32-bit halves. This is an identity, not a
|
|
90
|
+
* security boundary: nothing trusts a schema version to prove anything, it only
|
|
91
|
+
* answers "is this the same schema as before". A hand-rolled hash keeps this
|
|
92
|
+
* module free of `node:crypto`, so the identical function runs in the browser,
|
|
93
|
+
* in the CLI, and in the runtime — which is the property that actually matters.
|
|
94
|
+
*/
|
|
95
|
+
export function computeSchemaVersion(collections: CollectionConfig[]): string {
|
|
96
|
+
const payload = canonicalSchemaPayload(collections);
|
|
97
|
+
|
|
98
|
+
let h1 = 0x811c9dc5;
|
|
99
|
+
let h2 = 0x01000193;
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < payload.length; i++) {
|
|
102
|
+
const code = payload.charCodeAt(i);
|
|
103
|
+
h1 ^= code;
|
|
104
|
+
// Multiply by the FNV prime using shifts to stay in 32-bit integer math.
|
|
105
|
+
h1 = (h1 + ((h1 << 1) + (h1 << 4) + (h1 << 7) + (h1 << 8) + (h1 << 24))) >>> 0;
|
|
106
|
+
h2 ^= code + i;
|
|
107
|
+
h2 = (h2 + ((h2 << 1) + (h2 << 5) + (h2 << 9) + (h2 << 15) + (h2 << 24))) >>> 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const hex = (n: number): string => n.toString(16).padStart(8, "0");
|
|
111
|
+
return `v1:${hex(h1)}${hex(h2)}`;
|
|
112
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The storage access-control contract.
|
|
3
|
+
*
|
|
4
|
+
* Lives here rather than in `@rebasepro/server` because a project declares its
|
|
5
|
+
* `storageAuthorize` hook from its **config package**, and a config package
|
|
6
|
+
* depends on `@rebasepro/types` alone. A type the contract requires but the
|
|
7
|
+
* contract's author cannot import is not much of a contract.
|
|
8
|
+
*
|
|
9
|
+
* `@rebasepro/server` re-exports all of this, so existing imports keep working.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type StorageOperation = "read" | "write" | "delete" | "list";
|
|
13
|
+
|
|
14
|
+
/** The caller, as resolved by whichever auth middleware ran. */
|
|
15
|
+
export interface StorageAuthorizeUser {
|
|
16
|
+
uid: string;
|
|
17
|
+
email?: string;
|
|
18
|
+
roles?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface StorageAuthorizeContext {
|
|
22
|
+
/** Object key, bucket prefix stripped and traversal already sanitized. */
|
|
23
|
+
key: string;
|
|
24
|
+
bucket: string;
|
|
25
|
+
operation: StorageOperation;
|
|
26
|
+
/** Null when the route allows unauthenticated access. */
|
|
27
|
+
user: StorageAuthorizeUser | null;
|
|
28
|
+
/** Named backend the request targeted, when one was given. */
|
|
29
|
+
storageId?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Trusted, RLS-bypassing data access, so the hook can answer the question it
|
|
32
|
+
* actually has to answer: *who owns this object?*
|
|
33
|
+
*
|
|
34
|
+
* Without it the hook can only do prefix arithmetic on the key, which
|
|
35
|
+
* expresses no real multi-tenant rule — ownership lives in a row, not in a
|
|
36
|
+
* string. And it cannot simply import the server to get one: a project
|
|
37
|
+
* declares this hook from its **config** package, which depends on
|
|
38
|
+
* `@rebasepro/types` alone and cannot resolve `@rebasepro/server` at
|
|
39
|
+
* runtime. So the accessor is handed in.
|
|
40
|
+
*
|
|
41
|
+
* It bypasses row-level security on purpose. The hook IS the authorization
|
|
42
|
+
* decision; asking it to make that decision through a reader that has
|
|
43
|
+
* already been narrowed by the caller's own permissions is circular.
|
|
44
|
+
*/
|
|
45
|
+
data?: StorageAuthorizeData;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The slice of the data API a storage hook needs: read a collection, in the
|
|
50
|
+
* trusted server context.
|
|
51
|
+
*
|
|
52
|
+
* Deliberately read-only and deliberately tiny. A hook that can write is a hook
|
|
53
|
+
* that can be tricked into writing, and an authorization check has no business
|
|
54
|
+
* mutating anything.
|
|
55
|
+
*/
|
|
56
|
+
export interface StorageAuthorizeData {
|
|
57
|
+
collection(slug: string): {
|
|
58
|
+
find(query?: Record<string, unknown>): Promise<{ data: Record<string, unknown>[] }>;
|
|
59
|
+
findById(id: string): Promise<Record<string, unknown> | null>;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Per-object access control for storage, the analogue of an RLS policy on a
|
|
65
|
+
* collection.
|
|
66
|
+
*
|
|
67
|
+
* Without it, storage routes authenticate but do not authorize: `requireAuth`
|
|
68
|
+
* and `publicRead` are global switches, so any signed-in user could read any
|
|
69
|
+
* key they could name. That is fine for a single-tenant app and useless for a
|
|
70
|
+
* multi-tenant one, where the only thing separating two tenants' files would be
|
|
71
|
+
* key unguessability — not an access-control model. Multi-tenant apps were
|
|
72
|
+
* left to route every byte through a custom function to get an ownership check
|
|
73
|
+
* in, and each of them had to invent it.
|
|
74
|
+
*
|
|
75
|
+
* Return false (or throw) to deny. Denials surface as 403.
|
|
76
|
+
*/
|
|
77
|
+
export type StorageAuthorize = (ctx: StorageAuthorizeContext) => boolean | Promise<boolean>;
|