@rebasepro/types 0.9.1-canary.fd3754b → 0.10.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.
@@ -94,7 +94,7 @@ export interface AuthAdapterCapabilities {
94
94
  */
95
95
  passwordReset: boolean;
96
96
  /**
97
- * Whether the adapter exposes `POST /admin/users/:userId/reset-password`,
97
+ * Whether the adapter exposes `POST /admin/users/:uid/reset-password`,
98
98
  * letting an admin reset another user's password.
99
99
  *
100
100
  * Independent of `passwordReset`: the built-in adapter supports this even
@@ -198,8 +198,8 @@ export interface UserManagementAdapter {
198
198
  createUser(data: AuthCreateUserData): Promise<AuthUserData>;
199
199
  updateUser(id: string, data: Partial<AuthCreateUserData>): Promise<AuthUserData | null>;
200
200
  deleteUser(id: string): Promise<void>;
201
- getUserRoles(userId: string): Promise<string[]>;
202
- setUserRoles(userId: string, roleIds: string[]): Promise<void>;
201
+ getUserRoles(uid: string): Promise<string[]>;
202
+ setUserRoles(uid: string, roleIds: string[]): Promise<void>;
203
203
  }
204
204
 
205
205
  // ─── User Creation Lifecycle ─────────────────────────────────────────────────
@@ -223,6 +223,27 @@ export interface UserCreationPrepareResult {
223
223
  invitationSent: boolean;
224
224
  }
225
225
 
226
+ /**
227
+ * What a create body for an auth collection may name beyond the collection's
228
+ * own declared fields — see `AuthAdapter.describeUserCreationContract()`.
229
+ *
230
+ * @group Auth
231
+ */
232
+ export interface UserCreationWriteContract {
233
+ /**
234
+ * Whether to check the body for fields neither the collection nor
235
+ * {@link extraFields} declares. `false` skips the check entirely.
236
+ */
237
+ validate: boolean;
238
+ /**
239
+ * Credential and provider keys the adapter consumes itself, which the
240
+ * collection therefore does not declare as columns. `password` is the
241
+ * canonical one: `prepareUserCreation` hashes it into `passwordHash` and
242
+ * deletes it before the row is ever built.
243
+ */
244
+ extraFields: string[];
245
+ }
246
+
226
247
  /**
227
248
  * Result of `AuthAdapter.finalizeUserCreation()`.
228
249
  *
@@ -281,7 +302,7 @@ export interface AuthResponsePayload {
281
302
  */
282
303
  export interface TransformAuthResponseContext {
283
304
  /** The authenticated user's ID. */
284
- userId: string;
305
+ uid: string;
285
306
  /** The auth method that triggered this response. */
286
307
  method: "login" | "register" | "oauth" | "refresh" | "anonymous" | "magic-link" | "mfa";
287
308
  /** The raw HTTP request (for reading headers, IP, etc.). */
@@ -425,6 +446,33 @@ export interface AuthAdapter {
425
446
  collectionAuth?: unknown
426
447
  ): Promise<UserCreationPrepareResult>;
427
448
 
449
+ /**
450
+ * Describe what a create body for this auth collection is allowed to name,
451
+ * so unknown-field validation can run on it.
452
+ *
453
+ * A signup body is not the collection's shape: it carries credential fields
454
+ * like `password` that the users table never declares as columns, and
455
+ * `prepareUserCreation` maps them onto real ones. Validating the raw body
456
+ * against the collection alone would reject every legitimate signup — which
457
+ * is why the check used to be skipped outright for auth collections. That
458
+ * skip was total, so an undeclared field was silently dropped and the write
459
+ * still returned 201, while the same typo on a normal collection was a 400.
460
+ *
461
+ * This narrows the exemption to the fields the adapter actually consumes.
462
+ *
463
+ * `validate: false` disables the check for this collection, and is the right
464
+ * answer when a custom `onCreateUser` hook is configured: the body is then
465
+ * the hook's contract, not the collection's, and this layer cannot know what
466
+ * the hook accepts.
467
+ *
468
+ * If not implemented, validation is skipped — the pre-existing behaviour.
469
+ *
470
+ * @param collectionAuth - The parsed `AuthCollectionConfig` from the collection (if `auth` is an object).
471
+ */
472
+ describeUserCreationContract?(
473
+ collectionAuth?: unknown
474
+ ): UserCreationWriteContract;
475
+
428
476
  /**
429
477
  * Finalize a user creation after the entity has been persisted.
430
478
  *
@@ -245,6 +245,42 @@ export interface SingleSubscriptionConfig {
245
245
  id: string | number;
246
246
  }
247
247
 
248
+ /**
249
+ * Opt-in retention for one set of broadcast channels.
250
+ *
251
+ * Retention is configured on the server and nowhere else. A channel is created
252
+ * by whoever names it, so letting a client ask for its own history depth would
253
+ * let any visitor commit the backend to unbounded storage; and presence-only or
254
+ * notification-only channels — the overwhelming majority — must not pay for a
255
+ * feature they never use. With no rules configured nothing is written, no table
256
+ * is created, and broadcast behaves exactly as it did before history existed.
257
+ */
258
+ export interface ChannelRetentionRule {
259
+ /**
260
+ * Channel name to match. Either exact (`"doc:42"`) or a trailing-`*` prefix
261
+ * (`"doc:*"`). Deliberately not a full glob or RegExp: this decides what
262
+ * gets written to disk, and a rule whose blast radius is not obvious at a
263
+ * glance is the wrong shape for that.
264
+ */
265
+ match: string;
266
+ /** Keep at most this many of the most recent messages per channel. */
267
+ limit?: number;
268
+ /**
269
+ * Keep messages for at most this long. Accepts a millisecond count or a
270
+ * short duration string (`"30s"`, `"15m"`, `"24h"`, `"7d"`).
271
+ */
272
+ ttl?: number | string;
273
+ }
274
+
275
+ /** Server-side realtime options. */
276
+ export interface RealtimeChannelsConfig {
277
+ /**
278
+ * Retention rules, most specific first — the first match wins. Omitted or
279
+ * empty means no channel retains anything.
280
+ */
281
+ channels?: ChannelRetentionRule[];
282
+ }
283
+
248
284
  /**
249
285
  * Abstract realtime provider interface.
250
286
  * Handles real-time subscriptions and notifications for entity changes.
@@ -380,10 +416,24 @@ export interface SQLAdmin {
380
416
  fetchAvailableDatabases?(): Promise<string[]>;
381
417
 
382
418
  /**
383
- * Fetch the available database roles.
419
+ * Fetch the available *native PostgreSQL* database roles (from `pg_roles`).
420
+ *
421
+ * These are connection-level roles — what the SQL editor can `SET ROLE` to,
422
+ * and what `SecurityRule.pgRoles` targets. They are NOT application roles;
423
+ * for those use {@link fetchApplicationRoles}.
384
424
  */
385
425
  fetchAvailableRoles?(): Promise<string[]>;
386
426
 
427
+ /**
428
+ * Fetch the *application-level* roles in use in this project.
429
+ *
430
+ * These are the strings stored on the users table's `roles` column and
431
+ * exposed to policies as `auth.roles()` — what `SecurityRule.roles`
432
+ * matches against. Distinct from {@link fetchAvailableRoles}; the two are
433
+ * not interchangeable.
434
+ */
435
+ fetchApplicationRoles?(): Promise<string[]>;
436
+
387
437
  /**
388
438
  * Fetch the current database name.
389
439
  */
@@ -382,6 +382,18 @@ export interface BaseCollectionConfig<M extends Record<string, unknown> = Record
382
382
  */
383
383
  history?: boolean;
384
384
 
385
+ /**
386
+ * Whether a write naming a field this collection does not declare is
387
+ * rejected with a 400. Defaults to `true`.
388
+ *
389
+ * Set to `false` to let unknown keys through to the database, which is what
390
+ * happened before this existed: a typo reached the INSERT and came back as
391
+ * a Postgres error about a column, or — where a column really does exist
392
+ * that the config never declared, populated by a trigger or a default —
393
+ * quietly worked. The second case is the reason for the escape hatch.
394
+ */
395
+ strictWrites?: boolean;
396
+
385
397
  /**
386
398
  * Should local changes be backed up in local storage, to prevent data loss on
387
399
  * accidental navigations.
@@ -1059,9 +1071,14 @@ export interface SecurityRuleBase {
1059
1071
  * **Shortcut.** Restrict this rule to users that have one of these
1060
1072
  * application-level roles.
1061
1073
  *
1062
- * **Important:** These are NOT native PostgreSQL database roles. They are
1063
- * application roles managed by Rebase, stored in the `rebase.user_roles`
1064
- * table, and injected into each transaction via `auth.roles()`.
1074
+ * **Important:** These are NOT native PostgreSQL database roles names
1075
+ * like `public`, `anon` or `authenticated` belong to {@link pgRoles} and
1076
+ * produce a condition no user can satisfy if used here. These are
1077
+ * application roles managed by Rebase, stored as an inline `roles TEXT[]`
1078
+ * column on the users table, and injected into each transaction as
1079
+ * `app.user_roles` — which `auth.roles()` reads.
1080
+ *
1081
+ * There is no roles registry: a role exists once it is assigned to a user.
1065
1082
  *
1066
1083
  * Generates a safe array-overlap condition — the user passes if they hold
1067
1084
  * *any* of the listed roles:
@@ -1093,10 +1110,10 @@ export interface SecurityRuleBase {
1093
1110
  * every database connection). This is correct for most setups where
1094
1111
  * a single database role is used for all connections.
1095
1112
  *
1096
- * **Important:** These are NOT the same as the application-level `roles`
1097
- * (admin, editor, viewer, etc.) — those are enforced in the USING/WITH
1098
- * CHECK clauses via `auth.roles()`. This field controls the PostgreSQL
1099
- * `TO` clause in `CREATE POLICY ... TO role_name`.
1113
+ * **Important:** These are NOT the same as the application-level
1114
+ * {@link roles} (admin, editor, viewer, etc.) — those are enforced in the
1115
+ * USING/WITH CHECK clauses via `auth.roles()`. This field controls the
1116
+ * PostgreSQL `TO` clause in `CREATE POLICY ... TO role_name`.
1100
1117
  *
1101
1118
  * Use this if you have dedicated PostgreSQL roles (e.g. `app_read`,
1102
1119
  * `app_write`) and want policies to target specific ones.
@@ -1327,7 +1344,7 @@ export interface AuthCollectionConfig {
1327
1344
  * Override for custom reset flows.
1328
1345
  */
1329
1346
  onResetPassword?: (
1330
- userId: string,
1347
+ uid: string,
1331
1348
  ctx: AuthCollectionContext
1332
1349
  ) => Promise<AuthCollectionResetResult>;
1333
1350
 
@@ -91,7 +91,7 @@ export interface AfterReadProps<M extends Record<string, unknown> = Record<strin
91
91
  path: string;
92
92
 
93
93
  /**
94
- * Fetched row (flat — `{ id, ...columns }`)
94
+ * Fetched row (flat — the table's columns)
95
95
  */
96
96
  row: Record<string, unknown>
97
97
 
@@ -185,7 +185,7 @@ export interface BeforeDeleteProps<M extends Record<string, unknown> = Record<st
185
185
  id: string | number;
186
186
 
187
187
  /**
188
- * Deleted row (flat — `{ id, ...columns }`)
188
+ * Deleted row (flat — the table's columns)
189
189
  */
190
190
  row: Record<string, unknown>;
191
191
 
@@ -217,7 +217,7 @@ export interface AfterDeleteProps<M extends Record<string, unknown> = Record<str
217
217
  id: string | number;
218
218
 
219
219
  /**
220
- * Deleted row (flat — `{ id, ...columns }`)
220
+ * Deleted row (flat — the table's columns)
221
221
  */
222
222
  row: Record<string, unknown>;
223
223
 
@@ -27,9 +27,28 @@ export type PolicyExpression =
27
27
  | RolesOverlapPolicyExpression
28
28
  | RolesContainPolicyExpression
29
29
  | AuthenticatedPolicyExpression
30
+ | ServerContextPolicyExpression
30
31
  | ExistsInPolicyExpression
31
32
  | RawPolicyExpression;
32
33
 
34
+ /**
35
+ * The id a request without a logged-in user reports as `auth.uid()`.
36
+ *
37
+ * A user-context request always sets `app.uid`: blank would read back as
38
+ * `NULL`, and `NULL` is how the trusted server context is recognised, so an
39
+ * anonymous visitor would be promoted to server privileges. The driver
40
+ * therefore substitutes this sentinel at the single chokepoint where the GUC
41
+ * is set.
42
+ *
43
+ * The consequence for policy authors is that **`auth.uid() IS NOT NULL` is a
44
+ * tautology on the user path** — it is true for anonymous visitors too. Use
45
+ * {@link policy.authenticated} (or `auth.uid() <> 'anonymous'`) to mean "signed
46
+ * in", and {@link policy.serverContext} to mean "the trusted server context".
47
+ *
48
+ * @group Models
49
+ */
50
+ export const ANONYMOUS_USER_ID = "anonymous";
51
+
33
52
  /** Always allows. Compiles to `true`. @group Models */
34
53
  export interface TruePolicyExpression {
35
54
  kind: "true";
@@ -93,13 +112,42 @@ export interface RolesContainPolicyExpression {
93
112
  }
94
113
 
95
114
  /**
96
- * True when there is an authenticated user (`auth.uid() IS NOT NULL`).
115
+ * True when a signed-in user is making the request. Compiles to
116
+ * `auth.uid() IS NOT NULL AND auth.uid() <> 'anonymous'`.
117
+ *
118
+ * Both halves are load-bearing. `IS NOT NULL` excludes the server context;
119
+ * the {@link ANONYMOUS_USER_ID} comparison excludes anonymous visitors, who
120
+ * *do* carry a non-null `auth.uid()`. Checking only `IS NOT NULL` grants to
121
+ * everyone — see {@link ANONYMOUS_USER_ID}.
122
+ *
123
+ * `policy.not(policy.authenticated())` therefore means "anonymous visitor or
124
+ * the server context". To single out the server context, use
125
+ * {@link ServerContextPolicyExpression}.
97
126
  * @group Models
98
127
  */
99
128
  export interface AuthenticatedPolicyExpression {
100
129
  kind: "authenticated";
101
130
  }
102
131
 
132
+ /**
133
+ * True only in the trusted **server context** — the built-in flows that run
134
+ * without a user (signup, migrations, `dataAsAdmin`) set no user GUC, so
135
+ * `auth.uid()` is `NULL` for them and only for them. Compiles to
136
+ * `auth.uid() IS NULL`.
137
+ *
138
+ * This is what lets the owner connection satisfy a policy even under FORCE RLS.
139
+ * It is deliberately a primitive rather than `not(authenticated())`: the two
140
+ * meant the same thing while `authenticated` ignored {@link ANONYMOUS_USER_ID},
141
+ * and conflating them is what turns a server-only grant into an anonymous one.
142
+ *
143
+ * The JavaScript evaluator always returns `false` for this node — a client is
144
+ * never the server context.
145
+ * @group Models
146
+ */
147
+ export interface ServerContextPolicyExpression {
148
+ kind: "serverContext";
149
+ }
150
+
103
151
  /**
104
152
  * Membership / relational access: true when at least one row exists in another
105
153
  * collection (a join/membership table) matching `where`. This is what lets you
@@ -218,6 +266,7 @@ export const policy = {
218
266
  rolesOverlap: (roles: readonly string[]): RolesOverlapPolicyExpression => ({ kind: "rolesOverlap", roles: roles as string[] }),
219
267
  rolesContain: (roles: readonly string[]): RolesContainPolicyExpression => ({ kind: "rolesContain", roles: roles as string[] }),
220
268
  authenticated: (): AuthenticatedPolicyExpression => ({ kind: "authenticated" }),
269
+ serverContext: (): ServerContextPolicyExpression => ({ kind: "serverContext" }),
221
270
  existsIn: (args: { collection: string; where: PolicyExpression }): ExistsInPolicyExpression =>
222
271
  ({ kind: "existsIn", collection: args.collection, where: args.where }),
223
272
  raw: (sql: string): RawPolicyExpression => ({ kind: "raw", sql }),
@@ -231,6 +231,20 @@ export interface BaseProperty<CustomProps = unknown> {
231
231
  */
232
232
  validation?: PropertyValidationSchema;
233
233
 
234
+ /**
235
+ * Never include this column in an API response.
236
+ *
237
+ * For secrets the server must store and read but no client should ever
238
+ * receive — password hashes, verification tokens. The value is still
239
+ * written and queryable server-side; it is stripped from every row the API
240
+ * serves, for every caller, including admins and service keys.
241
+ *
242
+ * This is a server-side guarantee, unlike `ui.hideFromCollection`, which
243
+ * only stops the admin panel from *rendering* a field and leaves it in the
244
+ * JSON payload.
245
+ */
246
+ excludeFromApi?: boolean;
247
+
234
248
  // NOTE: `defaultValue` is intentionally NOT on BaseProperty.
235
249
  // Each concrete property type (StringProperty, NumberProperty, etc.)
236
250
  // defines its own typed `defaultValue` for compile-time safety.
@@ -12,12 +12,40 @@ export interface WebSocketMessage {
12
12
  rows?: Record<string, unknown>[];
13
13
  row?: Record<string, unknown> | null;
14
14
  error?: string;
15
+ /**
16
+ * Channel name, on broadcast and presence frames.
17
+ *
18
+ * These are addressed by channel rather than by `requestId` or
19
+ * `subscriptionId`, so this is the only field that routes them.
20
+ */
21
+ channel?: string;
15
22
  }
16
23
 
24
+ /**
25
+ * The key columns a collection's rows are addressed by.
26
+ *
27
+ * A row is exactly its columns and carries no address, so a subscriber that has
28
+ * to recognise one — to patch it, or to keep its reference across a refetch —
29
+ * derives the address from these. The SDK is usable with no collections
30
+ * declared at all, so the server is the only side that knows them.
31
+ *
32
+ * Undefined when the server cannot resolve them: a table with no primary key
33
+ * and no `id` column has no address, and rows of it cannot be recognised by
34
+ * anyone.
35
+ */
36
+ export type WirePrimaryKeys = { fieldName: string; type: "string" | "number"; isUUID?: boolean }[];
37
+
17
38
  export interface CollectionUpdateMessage extends WebSocketMessage {
18
39
  type: "collection_update";
19
40
  subscriptionId: string;
20
41
  rows: Record<string, unknown>[];
42
+ /**
43
+ * See {@link WirePrimaryKeys}. Sent with the rows themselves — and not only
44
+ * with a patch — because a CDC-originated change sends no patch at all: it
45
+ * invalidates and goes straight to a refetch, and the merge that preserves
46
+ * unchanged rows' references needs an address to match them by.
47
+ */
48
+ pks?: WirePrimaryKeys;
21
49
  }
22
50
 
23
51
  export interface SingleUpdateMessage extends WebSocketMessage {
@@ -35,9 +63,57 @@ export interface SingleUpdateMessage extends WebSocketMessage {
35
63
  export interface CollectionPatchMessage extends WebSocketMessage {
36
64
  type: "collection_patch";
37
65
  subscriptionId: string;
66
+ /** The address of the row this patch refers to — derived, never read off it. */
38
67
  id: string;
39
68
  /** The updated row, or null if deleted */
40
69
  row: Record<string, unknown> | null;
70
+ /** See {@link WirePrimaryKeys}: how the subscriber finds {@link id} in its cache. */
71
+ pks?: WirePrimaryKeys;
72
+ }
73
+
74
+ /**
75
+ * One retained broadcast, as it travels on the wire.
76
+ *
77
+ * `seq` is per-channel, dense and monotonically increasing — it is the only
78
+ * thing a reconnecting client needs to say where it got to. See
79
+ * {@link ChannelHistoryMessage}.
80
+ */
81
+ export interface ChannelHistoryEntry {
82
+ seq: number;
83
+ event: string;
84
+ payload: unknown;
85
+ /**
86
+ * The server-side client id of whoever sent it, for information only.
87
+ *
88
+ * Deliberately not used to filter a client's own messages out of a replay:
89
+ * a reconnect assigns a brand-new client id, so the very case replay exists
90
+ * for is the case where this would fail to match. Consumers that cannot
91
+ * tolerate re-applying their own operations must make them idempotent.
92
+ */
93
+ senderId?: string;
94
+ /** When the server accepted it, ISO-8601. */
95
+ at: string;
96
+ }
97
+
98
+ /**
99
+ * Server → client: the retained messages a client missed.
100
+ *
101
+ * `retained: false` means the channel has no retention rule configured, so
102
+ * there is no history to replay and there never will be — an explicit answer
103
+ * rather than an empty one, so a client can tell "nothing missed" apart from
104
+ * "this channel does not keep history".
105
+ */
106
+ export interface ChannelHistoryMessage extends WebSocketMessage {
107
+ type: "channel_history";
108
+ channel: string;
109
+ messages: ChannelHistoryEntry[];
110
+ retained: boolean;
111
+ /**
112
+ * The highest seq the server holds for this channel, whether or not it was
113
+ * returned. Lets a client that capped its request with `limit` see that it
114
+ * is still behind, and decide to resync wholesale instead of paging.
115
+ */
116
+ latestSeq?: number;
41
117
  }
42
118
 
43
119
  /**