@rebasepro/types 0.16.0 → 0.16.1-canary.g0d7af95

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 (57) hide show
  1. package/dist/call_context.d.ts +5 -5
  2. package/dist/controllers/auth_state.d.ts +1 -1
  3. package/dist/controllers/client.d.ts +22 -9
  4. package/dist/controllers/collection_registry.d.ts +2 -2
  5. package/dist/controllers/data.d.ts +4 -12
  6. package/dist/controllers/data_driver.d.ts +7 -7
  7. package/dist/controllers/email.d.ts +54 -2
  8. package/dist/controllers/index.d.ts +8 -9
  9. package/dist/controllers/storage.d.ts +4 -4
  10. package/dist/index.d.ts +5 -5
  11. package/dist/index.es.js +433 -3
  12. package/dist/index.es.js.map +1 -1
  13. package/dist/types/admin_block.d.ts +1 -1
  14. package/dist/types/auth_adapter.d.ts +18 -9
  15. package/dist/types/backend.d.ts +23 -10
  16. package/dist/types/collection_contract.d.ts +1 -1
  17. package/dist/types/collections.d.ts +34 -9
  18. package/dist/types/component_ref.d.ts +3 -2
  19. package/dist/types/cron.d.ts +1 -25
  20. package/dist/types/data_source.d.ts +1 -1
  21. package/dist/types/database_adapter.d.ts +5 -5
  22. package/dist/types/entities.d.ts +1 -1
  23. package/dist/types/entity_callbacks.d.ts +4 -4
  24. package/dist/types/index.d.ts +33 -29
  25. package/dist/types/indexes.d.ts +179 -0
  26. package/dist/types/project_manifest.d.ts +132 -27
  27. package/dist/types/properties.d.ts +58 -6
  28. package/dist/types/relations.d.ts +1 -1
  29. package/dist/types/resource_kinds.d.ts +189 -0
  30. package/dist/types/resources.d.ts +197 -0
  31. package/dist/types/schema_editing.d.ts +127 -0
  32. package/dist/types/schema_version.d.ts +1 -1
  33. package/dist/types/security_rules.d.ts +1 -1
  34. package/dist/types/storage_source.d.ts +27 -0
  35. package/dist/users/index.d.ts +1 -1
  36. package/package.json +2 -2
  37. package/src/controllers/client.ts +14 -1
  38. package/src/controllers/data.ts +0 -9
  39. package/src/controllers/email.ts +55 -2
  40. package/src/controllers/index.ts +0 -1
  41. package/src/controllers/storage.ts +4 -4
  42. package/src/types/admin_block.ts +1 -2
  43. package/src/types/auth_adapter.ts +18 -10
  44. package/src/types/backend.ts +18 -1
  45. package/src/types/collections.ts +28 -2
  46. package/src/types/component_ref.ts +3 -2
  47. package/src/types/cron.ts +0 -24
  48. package/src/types/index.ts +4 -0
  49. package/src/types/indexes.ts +180 -0
  50. package/src/types/project_manifest.ts +139 -26
  51. package/src/types/properties.ts +54 -0
  52. package/src/types/resource_kinds.ts +324 -0
  53. package/src/types/resources.ts +368 -0
  54. package/src/types/schema_editing.ts +154 -0
  55. package/src/types/storage_source.ts +28 -0
  56. package/dist/controllers/database_admin.d.ts +0 -11
  57. package/src/controllers/database_admin.ts +0 -22
@@ -0,0 +1,197 @@
1
+ /**
2
+ * The resource graph: one declaration site for every named thing a project needs.
3
+ *
4
+ * ## The rule
5
+ *
6
+ * **Every named resource is declared with a constructor in config code.** A
7
+ * database, a bucket, a topic and whatever kind comes next are all spelled the
8
+ * same way, so "where do I declare my second one" has one answer instead of one
9
+ * answer per kind.
10
+ *
11
+ * ```ts
12
+ * export const main = database("main");
13
+ * export const media = bucket("media", { transport: "direct" });
14
+ * export const signups = topic<SignupEvent>("signups");
15
+ * ```
16
+ *
17
+ * ## Declaration is not binding
18
+ *
19
+ * A declaration says a resource *exists* and what shape it has. It never says
20
+ * how to reach it — that is a property of the environment, not of the project,
21
+ * and it differs between a laptop, a self-hosted box and a tenant in the cloud.
22
+ * Binding lives in `@rebasepro/server`'s boot path, reading environment
23
+ * variables or an infrastructure config file, and it keys off the logical name
24
+ * declared here.
25
+ *
26
+ * This split is the whole point. Before it, storage topology was hand-written
27
+ * into `rebase.json` while database topology lived in TypeScript, and the
28
+ * boundary between them was a fact about what the control plane could read
29
+ * before a build — a platform implementation detail that a developer had no way
30
+ * to derive. Worse, storage could be declared in *both* places, and the merge
31
+ * silently kept the JSON's engine and discarded the code's.
32
+ *
33
+ * ## Why a registry rather than a fixed union
34
+ *
35
+ * Kinds register themselves. Adding pub/sub, a cache or a search index must not
36
+ * require editing a manifest schema, a validator and three switch statements —
37
+ * that cost is exactly why the last two kinds ended up in different homes.
38
+ */
39
+ /** How a client reaches a resource. */
40
+ export type ResourceTransport =
41
+ /** Through the backend. The default, and the only one that needs no client SDK. */
42
+ "server"
43
+ /** A provider SDK talks to the resource directly; the backend is not in the path. */
44
+ | "direct";
45
+ /**
46
+ * A resource kind, as registered.
47
+ *
48
+ * `engines` is an allowlist rather than documentation. An unrecognised engine
49
+ * used to be a free string that passed every check and failed later, further
50
+ * from the typo that caused it — `"s2"` for `"s3"` reached the runtime. Anything
51
+ * genuinely outside the list is spelled `custom:<id>`, which says so at the call
52
+ * site instead of looking like a typo.
53
+ */
54
+ export interface ResourceKindSpec {
55
+ /** The kind's name, as it appears in a declaration and in the graph. */
56
+ kind: string;
57
+ /** Engines this kind ships with. `custom:<id>` is always additionally valid. */
58
+ engines: readonly string[];
59
+ /** Used when a declaration names none. */
60
+ defaultEngine: string;
61
+ /**
62
+ * Environment variable base names this kind binds from, in the order a
63
+ * binder should try them. A resource keyed `analytics` reads
64
+ * `<BASE>__ANALYTICS`; the default-keyed resource reads `<BASE>` unsuffixed,
65
+ * so a single-resource project configured the obvious way declares nothing.
66
+ */
67
+ envBases: readonly string[];
68
+ /**
69
+ * The subset of `envBases` that matters for a given engine.
70
+ *
71
+ * The binder reads every base and takes whichever is set — harmless, and it
72
+ * keeps binding tolerant. A GENERATOR cannot be that relaxed: `rebase eject
73
+ * infra` writing S3_BUCKET, GCS_BUCKET, STORAGE_BUCKET and
74
+ * STORAGE_PUBLIC_URL for a `local` bucket hands somebody four variables of
75
+ * which three are noise, and a config file full of irrelevant keys is one
76
+ * nobody reads carefully.
77
+ *
78
+ * Keyed by engine; an engine with no entry falls back to all of them, which
79
+ * is the honest answer for one this package has never heard of.
80
+ */
81
+ envBasesByEngine?: Readonly<Record<string, readonly string[]>>;
82
+ /** Option keys this kind accepts beyond the common ones, for validation. */
83
+ optionKeys?: readonly string[];
84
+ /**
85
+ * Whether a project implicitly has one of these even when it declares
86
+ * nothing. True for databases — a backend without one is not a backend —
87
+ * and false for topics, where zero is the normal number.
88
+ */
89
+ implicitDefault?: boolean;
90
+ }
91
+ /** The key a resource takes when a project declares only one of its kind. */
92
+ export declare const DEFAULT_RESOURCE_KEY = "(default)";
93
+ /** A declared resource, as it appears in the graph. */
94
+ export interface ResourceDeclaration {
95
+ kind: string;
96
+ /** Unique within its kind. What a binder looks up and what an env suffix is built from. */
97
+ key: string;
98
+ engine: string;
99
+ transport: ResourceTransport;
100
+ label?: string;
101
+ /** Kind-specific options, validated against the kind's `optionKeys`. */
102
+ options: Readonly<Record<string, unknown>>;
103
+ }
104
+ /**
105
+ * The value a constructor returns.
106
+ *
107
+ * Carries its own declaration so config code can hold it and pass it around,
108
+ * and stringifies to its key so it drops into the places that still take one.
109
+ * Collections name a data source by string today; a handle works there without
110
+ * the collection API having to change, which keeps this a config redesign
111
+ * rather than a rewrite of the data layer.
112
+ */
113
+ export interface ResourceHandle extends ResourceDeclaration {
114
+ toString(): string;
115
+ }
116
+ /** Whether a value is a resource handle rather than a plain string key. */
117
+ export declare function isResourceHandle(value: unknown): value is ResourceHandle;
118
+ /** The key a resource reference names, whether it is a handle or already a key. */
119
+ export declare function resourceKeyOf(ref: string | ResourceHandle): string;
120
+ /** Register a resource kind. Idempotent for an identical spec; throws on a conflicting one. */
121
+ export declare function registerResourceKind(spec: ResourceKindSpec): void;
122
+ /** Every registered kind, for validators and for `rebase doctor`. */
123
+ export declare function resourceKinds(): ResourceKindSpec[];
124
+ /** One registered kind, or undefined. */
125
+ export declare function resourceKind(kind: string): ResourceKindSpec | undefined;
126
+ /** Options every kind accepts. */
127
+ export interface DeclareOptions {
128
+ engine?: string;
129
+ transport?: ResourceTransport;
130
+ label?: string;
131
+ [option: string]: unknown;
132
+ }
133
+ /** Whether an engine is one the kind knows, or an explicit `custom:` opt-out. */
134
+ export declare function isValidEngine(spec: ResourceKindSpec, engine: string): boolean;
135
+ /**
136
+ * Declare a resource. The primitive every kind's constructor is built from.
137
+ *
138
+ * Redeclaring the same `kind:key` with a *different* shape throws rather than
139
+ * merging. Merging is what the old storage path did, and it silently discarded
140
+ * one of the two engines — a declaration accepted and then ignored, which is
141
+ * the failure this whole model exists to remove. Redeclaring it identically is
142
+ * fine: a config module evaluated twice must not be an error.
143
+ */
144
+ export declare function declareResource(kind: string, key?: string, options?: DeclareOptions): ResourceHandle;
145
+ /** Every declared resource, in declaration order, optionally filtered by kind. */
146
+ export declare function declaredResources(kind?: string): ResourceDeclaration[];
147
+ /**
148
+ * Forget every declaration, keeping registered kinds.
149
+ *
150
+ * For tests and for a CLI that evaluates more than one project in a process.
151
+ * Kinds survive because they are registered by module import, which will not
152
+ * happen a second time.
153
+ */
154
+ export declare function resetDeclaredResources(): void;
155
+ /**
156
+ * The env-var suffix a resource's bindings use: `__ANALYTICS` for `analytics`,
157
+ * and nothing at all for the default-keyed one.
158
+ *
159
+ * The default takes no suffix so that a project with one database configured
160
+ * through plain `DATABASE_URL` keeps working having declared nothing — the
161
+ * overwhelmingly common project must not have to say so.
162
+ */
163
+ export declare function resourceEnvSuffix(key: string): string;
164
+ /**
165
+ * Two resources of a kind whose keys differ but whose env suffixes do not.
166
+ *
167
+ * `media-files` and `media_files` both become `__MEDIA_FILES`, so one would
168
+ * silently read the other's configuration. Returned rather than thrown so the
169
+ * caller can report it with the rest of a validation pass.
170
+ */
171
+ export declare function findEnvSuffixCollision(keys: readonly string[]): {
172
+ a: string;
173
+ b: string;
174
+ suffix: string;
175
+ } | null;
176
+ /**
177
+ * The whole graph, as recorded in a manifest and read by a host.
178
+ *
179
+ * `version` is the graph format, not the project's. A host reading a graph it
180
+ * does not understand must say so rather than provision half of it.
181
+ */
182
+ export interface ResourceGraph {
183
+ version: 1;
184
+ resources: ResourceDeclaration[];
185
+ }
186
+ /** The current graph format version. */
187
+ export declare const RESOURCE_GRAPH_VERSION: 1;
188
+ /** Build a graph from the current declarations, sorted for a stable diff. */
189
+ export declare function buildResourceGraph(): ResourceGraph;
190
+ /**
191
+ * The environment variables worth writing for a resource, given its engine.
192
+ *
193
+ * Falls back to every base the kind reads when the engine is unknown — a
194
+ * `custom:` engine gets the full list rather than an empty one, because
195
+ * guessing narrow would silently omit the variable it actually needs.
196
+ */
197
+ export declare function envBasesForResource(declaration: ResourceDeclaration): readonly string[];
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The vocabulary a live schema change is described in.
3
+ *
4
+ * Declared here, and nowhere else, because two packages that must not import
5
+ * each other both need it: `@rebasepro/server-postgres` decides what a change
6
+ * means and renders the files it needs, while `@rebasepro/server` commits those
7
+ * files and serves the routes. Neither can reach the other — the server is
8
+ * engine-agnostic by design — so the shared kernel holds the shapes and the
9
+ * driver is detected structurally through {@link SchemaEditingAdmin}.
10
+ *
11
+ * Nothing here executes anything. These are the nouns.
12
+ */
13
+ /**
14
+ * What a change will do to a live database.
15
+ *
16
+ * - `safe` — the boot-time ensure path expresses it, and the result matches the
17
+ * configuration.
18
+ * - `diverges` — the ensure path applies *something*, but the database will not
19
+ * match what the configuration declares, and nothing reports it. This is the
20
+ * category worth having: adding a required property to a populated table
21
+ * yields a nullable column, and adding a value to an existing enum yields
22
+ * nothing at all. Both read as success.
23
+ * - `needs-migration` — the ensure path cannot express it. Dropping anything,
24
+ * changing a type, moving a primary key.
25
+ */
26
+ export type SchemaChangeVerdict = "safe" | "diverges" | "needs-migration";
27
+ export type SchemaChangeKind = "add-collection" | "remove-collection" | "add-property" | "remove-property" | "change-property-type" | "rename-column" | "add-enum-value" | "remove-enum-value" | "change-required" | "change-primary-key";
28
+ export interface SchemaChange {
29
+ kind: SchemaChangeKind;
30
+ verdict: SchemaChangeVerdict;
31
+ /** Collection slug. */
32
+ collection: string;
33
+ /** Property name, where the change is to one. */
34
+ property?: string;
35
+ /** One line, specific: what changed and what it will do. */
36
+ detail: string;
37
+ /** What to do instead, when the verdict is not `safe`. */
38
+ remedy?: string;
39
+ }
40
+ export interface ClassifiedSchemaChanges {
41
+ changes: SchemaChange[];
42
+ /** The worst verdict present, or `safe` for an empty diff. */
43
+ verdict: SchemaChangeVerdict;
44
+ /** True only when every change is `safe` — the one case an editor may apply. */
45
+ applicable: boolean;
46
+ }
47
+ /**
48
+ * Where a project's generated schema artifacts live, relative to the **project**
49
+ * root — which is the repository root only when the project is the whole
50
+ * repository.
51
+ *
52
+ * Here rather than in the Postgres package because it is a contract, not an
53
+ * engine detail: `@rebasepro/server` has to derive these for a project in a
54
+ * subdirectory, and it cannot import a driver to do it.
55
+ */
56
+ export interface SchemaCommitPaths {
57
+ /** Drizzle schema, imported by the backend. */
58
+ schemaFile: string;
59
+ /** Declarative DDL, what `db push` applies and Atlas diffs against. */
60
+ ddlFile: string;
61
+ policiesFile: string;
62
+ searchFile: string;
63
+ }
64
+ export declare const DEFAULT_COMMIT_PATHS: SchemaCommitPaths;
65
+ /** One file the commit writes, as content rather than as a path on a disk. */
66
+ export interface SchemaChangeFile {
67
+ path: string;
68
+ contents: string;
69
+ }
70
+ /**
71
+ * Everything a change needs written and run.
72
+ *
73
+ * Computed without touching a disk or a network. The database is *read* — what
74
+ * a change means depends on what is already there, and a plan that guessed
75
+ * would be guessing about whether the statements it returns will be accepted.
76
+ */
77
+ export interface SchemaChangePlan {
78
+ /** Every file the commit writes — collection source and generated artifacts. */
79
+ files: SchemaChangeFile[];
80
+ /** The additive DDL this change adds, in dependency order. */
81
+ statements: string[];
82
+ classified: ClassifiedSchemaChanges;
83
+ /** A commit message describing the change rather than announcing one. */
84
+ message: string;
85
+ /**
86
+ * Constraints the configuration asks for that these statements do not
87
+ * carry, and why.
88
+ *
89
+ * Almost always empty. When it is not, it is the part the person confirming
90
+ * needs to read: the change will apply, and the database will still not
91
+ * enforce something the configuration says — a required property over a
92
+ * table that already holds rows with no value for it. Optional so a plan
93
+ * from an engine that does not distinguish these cases stays valid.
94
+ */
95
+ withheldConstraints?: WithheldSchemaConstraint[];
96
+ }
97
+ /** A constraint a plan asks for and does not apply. */
98
+ export interface WithheldSchemaConstraint {
99
+ /** `schema.table.column`. */
100
+ target: string;
101
+ kind: "not-null";
102
+ /** What is in the way, naming the obstacle rather than the rule. */
103
+ reason: string;
104
+ /** What would make it applicable. */
105
+ remedy: string;
106
+ }
107
+ /**
108
+ * An admin that can plan a schema change.
109
+ *
110
+ * Planning only. Applying is `executeSql`, which every SQL admin already has,
111
+ * and committing belongs to whatever holds the repository — keeping those three
112
+ * apart is what lets the same plan be committed locally on a developer's machine
113
+ * and through a GitHub App from a cloud tenant.
114
+ *
115
+ * @group Admin
116
+ */
117
+ export interface SchemaEditingAdmin {
118
+ /**
119
+ * Decide what the change means and render everything it needs.
120
+ *
121
+ * Rejects when the change is not applicable, carrying the classification so
122
+ * a caller can say which change was the problem.
123
+ */
124
+ planSchemaChange(before: unknown[], after: unknown[], options?: {
125
+ paths?: Partial<SchemaCommitPaths>;
126
+ }): Promise<SchemaChangePlan>;
127
+ }
@@ -1,4 +1,4 @@
1
- import type { CollectionConfig } from "./collections";
1
+ import type { CollectionConfig } from "./collections.js";
2
2
  /**
3
3
  * Compute the canonical string a schema version hashes.
4
4
  *
@@ -15,7 +15,7 @@
15
15
  * them with `getPolicyNamesForRule`/`getEffectiveSecurityRules` rather than
16
16
  * matching on `rule.name`.
17
17
  */
18
- import type { PolicyExpression } from "./policy";
18
+ import type { PolicyExpression } from "./policy.js";
19
19
  /**
20
20
  * SQL operation that a policy applies to.
21
21
  * @group Models
@@ -54,6 +54,33 @@ export interface StorageSourceDefinition {
54
54
  * `"gcs"`, `"firebase"`, `"azure"`, or a custom id).
55
55
  */
56
56
  engine: string;
57
+ /**
58
+ * The credential set this source signs with, when several sources share one.
59
+ *
60
+ * ## What it is for
61
+ *
62
+ * Every binding a bucket needs is read per key — `S3_BUCKET__MEDIA`,
63
+ * `S3_ACCESS_KEY_ID__MEDIA`, and so on. That is right for the bucket *name*,
64
+ * which is different for every source by definition, and wrong for the
65
+ * credentials, which usually are not: fifteen buckets on one MinIO install
66
+ * meant fifteen copies of the same endpoint, access key and secret — ninety
67
+ * variables where eighteen would do, and one key rotation became fifteen
68
+ * paired edits where a single missed one fails at upload time with an opaque
69
+ * signing error.
70
+ *
71
+ * Naming an account here lets the *account-scoped* bindings fall back to
72
+ * `<BASE>__<ACCOUNT>` when no per-key value is set. The bucket name never
73
+ * falls back: it is what distinguishes one source from another.
74
+ *
75
+ * ## Why it does not fall back to the bare variable
76
+ *
77
+ * A source with no `account` reads only its own suffixed names, exactly as
78
+ * before — so every project that predates this is wire-identical. The
79
+ * unsuffixed `S3_ACCESS_KEY_ID` belongs to the *default* source, and letting
80
+ * a named bucket inherit it would mean a typo'd key silently signs with
81
+ * another source's credentials. Two forms, both explicit, opt-in.
82
+ */
83
+ account?: string;
57
84
  /**
58
85
  * How the frontend reaches this storage. Defaults to `"server"`.
59
86
  *
@@ -1 +1 @@
1
- export * from "./user";
1
+ export * from "./user.js";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/types",
3
3
  "type": "module",
4
- "version": "0.16.0",
4
+ "version": "0.16.1-canary.g0d7af95",
5
5
  "description": "Rebase type definitions — shared interfaces and controller types",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -88,7 +88,7 @@
88
88
  },
89
89
  "scripts": {
90
90
  "watch": "vite build --watch",
91
- "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../scripts/assert-build-output.mjs",
91
+ "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json && node ../../tooling/scripts/add-dts-extensions.mjs dist && node ../../tooling/scripts/assert-build-output.mjs",
92
92
  "test:lint": "eslint \"src/**\" --quiet",
93
93
  "test": "jest --passWithNoTests",
94
94
  "clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f"
@@ -300,7 +300,7 @@ export interface RebaseClient<DB = unknown> {
300
300
  /** Unified Authentication layer */
301
301
  auth: AuthClient;
302
302
 
303
- /** Unified Storage layer (default storage source, backward-compatible) */
303
+ /** Unified Storage layer — the default storage source. */
304
304
  storage?: StorageSource;
305
305
 
306
306
  /** Registry of all named storage sources for multi-backend support */
@@ -437,6 +437,19 @@ export interface RebaseServerClient<DB = unknown> extends Omit<RebaseClient<DB>,
437
437
  * Execute raw SQL against the database. Always present server-side for SQL
438
438
  * engines. Values interpolated into the query should be passed via
439
439
  * `params`, referenced as `$1`, `$2`, … placeholders in the query text.
440
+ *
441
+ * **Runtime note.** This is the one accessor on this object that is not
442
+ * portable. It runs on the database owner connection over a TCP socket, so
443
+ * it is available wherever the framework holds that connection — every Node
444
+ * deployment, self-hosted or managed — and not on a host that has no
445
+ * sockets and no business holding owner credentials.
446
+ *
447
+ * Nothing about that is a problem for a Node deployment, and it is not a
448
+ * reason to avoid it there. It is a reason not to build a function's *only*
449
+ * data path on it if that function may later move: `c.get("driver")` and
450
+ * `rebase.dataAsAdmin` go over the same wire wherever they run. A function
451
+ * that genuinely needs raw SQL can ask `runtimeKey()` and degrade, rather
452
+ * than discovering it at the call.
440
453
  */
441
454
  sql(query: string, options?: { database?: string; role?: string; params?: unknown[] }): Promise<Record<string, unknown>[]>;
442
455
  }
@@ -3,15 +3,6 @@ import type { ComputedSortField, SearchMatch } from "../types/search";
3
3
  import { Entity, EntityValues } from "../types/entities";
4
4
  import { WhereFilterOp, FieldPath, FilterValues, OrderBySpec } from "../types/filter-operators";
5
5
 
6
- /**
7
- * Operator-blind filter value: whatever the column holds, a list of it, or null.
8
- *
9
- * @deprecated Superseded by {@link WhereValueFor}, which correlates the value
10
- * with the operator. Kept exported because it is public API and downstream code
11
- * annotates with it; every `where()` overload in this file uses `WhereValueFor`.
12
- */
13
- export type WhereValue<T> = T | T[] | null;
14
-
15
6
  /**
16
7
  * The element type of an array column, and the column's own type otherwise.
17
8
  *
@@ -20,6 +20,51 @@ export interface EmailSendOptions {
20
20
  text?: string;
21
21
  /** Optional reply-to address. */
22
22
  replyTo?: string;
23
+ /**
24
+ * Additional headers, verbatim.
25
+ *
26
+ * The reason this exists is that several things a real sender must do are
27
+ * only expressible as headers, and without a passthrough an application had
28
+ * to choose between not doing them and not using this interface:
29
+ *
30
+ * - `List-Unsubscribe` and `List-Unsubscribe-Post`, which give a mail client
31
+ * its own one-click opt-out. The large providers weigh their presence when
32
+ * deciding whether bulk mail reaches an inbox at all.
33
+ * - `In-Reply-To` and `References`, without which a reply is a new thread.
34
+ *
35
+ * **Values are validated, not escaped.** A value containing CR or LF is
36
+ * rejected rather than sanitised, because a newline in a header value ends
37
+ * the header and starts a new one — so a field built from user input is an
38
+ * injection point for `Bcc:` and anything else. Rejecting is the only safe
39
+ * response: silently stripping the newline would deliver a message the
40
+ * caller did not write, and neither would tell them.
41
+ */
42
+ headers?: Record<string, string>;
43
+ }
44
+
45
+ /**
46
+ * What the provider reported about a message it accepted.
47
+ *
48
+ * Every field is optional because not every backend reports them: a custom
49
+ * `sendEmail` function that posts to an HTTP API may know nothing beyond "no
50
+ * error". An absent `messageId` therefore means "not reported", never "not
51
+ * sent" — the absence of an id is not a delivery failure, which is signalled by
52
+ * a thrown error.
53
+ */
54
+ export interface EmailSendResult {
55
+ /**
56
+ * The message's RFC 5322 Message-ID, **without** angle brackets.
57
+ *
58
+ * Stripped because this is an identifier to store and compare — against a
59
+ * reply's `In-Reply-To`, most often — and a value that sometimes carries
60
+ * brackets and sometimes does not is a bug waiting in every comparison.
61
+ * Re-add them when writing it into a header: `<${messageId}>`.
62
+ */
63
+ messageId?: string;
64
+ /** Recipients the provider accepted, when it says. */
65
+ accepted?: string[];
66
+ /** Recipients the provider refused, when it says. A non-empty list is not an error. */
67
+ rejected?: string[];
23
68
  }
24
69
 
25
70
  /**
@@ -29,8 +74,16 @@ export interface EmailSendOptions {
29
74
  * provider — consumers only interact through this interface.
30
75
  */
31
76
  export interface EmailService {
32
- /** Send a single email. */
33
- send(options: EmailSendOptions): Promise<void>;
77
+ /**
78
+ * Send a single email.
79
+ *
80
+ * Resolves with what the provider reported (see {@link EmailSendResult});
81
+ * throws on failure. It returned `void` before 0.17: an application that
82
+ * sent a message had no way to learn the id the server assigned it, so
83
+ * threading a reply back to the message that prompted it was impossible
84
+ * through this interface. Callers that do not care may still ignore it.
85
+ */
86
+ send(options: EmailSendOptions): Promise<EmailSendResult>;
34
87
  /** Returns `true` when the service has valid credentials / is ready to send. */
35
88
  isConfigured(): boolean;
36
89
  /** Verify connection/credentials with the email provider. */
@@ -2,7 +2,6 @@ export * from "./collection_registry";
2
2
  export * from "./auth_state";
3
3
 
4
4
  export * from "./data";
5
- export * from "./database_admin";
6
5
  export * from "./data_driver";
7
6
 
8
7
  export * from "./effective_role";
@@ -68,11 +68,11 @@ export interface UploadFileResult {
68
68
  /**
69
69
  * Fully qualified storage URL for the uploaded file.
70
70
  *
71
- * For example: `s3://my-bucket/path/to/file.png`.
72
- *
73
- * This is optional for backwards compatibility.
71
+ * For example: `s3://my-bucket/path/to/file.png`. Every controller in the
72
+ * framework returns one — S3, GCS and local alike — and a caller that stores
73
+ * the reference needs it, so it is part of the result rather than a maybe.
74
74
  */
75
- storageUrl?: string;
75
+ storageUrl: string;
76
76
  }
77
77
 
78
78
  /**
@@ -71,8 +71,7 @@ export const ADMIN_COLLECTION_KEYS = [
71
71
  "selectionController",
72
72
  "selectionEnabled",
73
73
  "sideDialogWidth",
74
- "sort",
75
- "titleProperty"
74
+ "sort"
76
75
  ] as const;
77
76
 
78
77
  /** A key of a collection's `admin` block. @group Models */
@@ -83,8 +83,16 @@ export interface AuthAdapterCapabilities {
83
83
 
84
84
  /** Supports email/password login. */
85
85
  emailPasswordLogin: boolean;
86
- /** Supports new user registration. */
87
- registration: boolean;
86
+ /**
87
+ * Whether self-registration is open **right now**.
88
+ *
89
+ * A runtime answer, not a static feature list: the built-in adapter also
90
+ * reports `true` during the first-user bootstrap window (an empty user
91
+ * table) and `false` the moment `disableSelfRegistration` is set. Whatever
92
+ * this says, `POST /auth/register` does — both read the same predicate, so
93
+ * the UI can never be sent to a form that can only 403.
94
+ */
95
+ registrationEnabled: boolean;
88
96
  /**
89
97
  * Supports the end-user password reset flow (emailing a reset link).
90
98
  *
@@ -113,12 +121,15 @@ export interface AuthAdapterCapabilities {
113
121
  /** Supports passwordless magic link login. */
114
122
  magicLink: boolean;
115
123
  /**
116
- * Whether `POST /auth/anonymous` will mint a credential-less session.
124
+ * Supports passwordless sign-in with a six-digit code sent by email.
117
125
  *
118
- * Optional so an external adapter that predates the key keeps typechecking;
119
- * absent means "this adapter does not offer anonymous sign-in".
126
+ * Optional so that an external adapter written before this existed still
127
+ * satisfies the interface; absent reads as "no", which is what an adapter
128
+ * that has never heard of the flow means.
120
129
  */
121
- anonymousLogin?: boolean;
130
+ emailOtp?: boolean;
131
+ /** Whether `POST /auth/anonymous` will mint a credential-less session. */
132
+ anonymousLogin: boolean;
122
133
  /** List of enabled OAuth provider IDs (e.g. `["google", "github"]`). */
123
134
  enabledProviders: string[];
124
135
 
@@ -134,9 +145,6 @@ export interface AuthAdapterCapabilities {
134
145
  * Only applicable for built-in auth.
135
146
  */
136
147
  needsSetup?: boolean;
137
-
138
- /** Whether new user registration is enabled (may differ from `registration` capability at runtime). */
139
- registrationEnabled?: boolean;
140
148
  }
141
149
 
142
150
  // ─── User Management ────────────────────────────────────────────────────────
@@ -311,7 +319,7 @@ export interface TransformAuthResponseContext {
311
319
  /** The authenticated user's ID. */
312
320
  uid: string;
313
321
  /** The auth method that triggered this response. */
314
- method: "login" | "register" | "oauth" | "refresh" | "anonymous" | "magic-link" | "mfa";
322
+ method: "login" | "register" | "oauth" | "refresh" | "anonymous" | "magic-link" | "otp" | "mfa";
315
323
  /** The raw HTTP request (for reading headers, IP, etc.). */
316
324
  request: Request;
317
325
  }
@@ -4,6 +4,7 @@ import type { LogicalCondition } from "../controllers/data";
4
4
  import type { AuthAdapter } from "./auth_adapter";
5
5
  import type { HistoryConfig } from "../controllers/client";
6
6
  import type { ChannelBusSetting } from "./channel_bus";
7
+ import type { SchemaEditingAdmin } from "./schema_editing";
7
8
 
8
9
  // =============================================================================
9
10
  // DATABASE CONNECTION INTERFACES
@@ -564,7 +565,23 @@ export interface BranchAdmin {
564
565
  *
565
566
  * @group Admin
566
567
  */
567
- export type DatabaseAdmin = Partial<SQLAdmin> & Partial<DocumentAdmin> & Partial<SchemaAdmin> & Partial<BranchAdmin>;
568
+ export type DatabaseAdmin = Partial<SQLAdmin> & Partial<DocumentAdmin> & Partial<SchemaAdmin>
569
+ & Partial<BranchAdmin> & Partial<SchemaEditingAdmin>;
570
+
571
+ /**
572
+ * Type guard: can this admin plan a live schema change?
573
+ *
574
+ * Planning is engine-specific — it renders DDL, a Drizzle schema and the
575
+ * declarative SQL artifacts — so the implementation lives in the driver
576
+ * package. The server detects the capability structurally, exactly as it does
577
+ * for SQL, rather than importing an engine it is supposed to know nothing
578
+ * about.
579
+ *
580
+ * @group Admin
581
+ */
582
+ export function isSchemaEditingAdmin(admin: DatabaseAdmin | undefined): admin is SchemaEditingAdmin {
583
+ return !!admin && typeof (admin as SchemaEditingAdmin).planSchemaChange === "function";
584
+ }
568
585
 
569
586
  /**
570
587
  * Type guard: does this admin support SQL operations?