@vexcms/better-auth 0.1.0-alpha.2 → 0.1.0-alpha.21

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @vexcms/better-auth
2
2
 
3
- [Better Auth](https://www.better-auth.com/) adapter for [VEX CMS](https://github.com/ianyimi/vex). Automatically extracts Better Auth's authentication tables and converts them into VEX collection definitions, enabling admin panel management of auth data.
3
+ [Better Auth](https://www.better-auth.com/) integration for [VEX CMS](https://github.com/ianyimi/vex). Converts Better Auth's user/session/account/verification tables — and any registered plugin's tables — into standard Vex collections, so auth data is manageable through the same admin panel as everything else.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,35 +8,115 @@
8
8
  pnpm add @vexcms/better-auth
9
9
  ```
10
10
 
11
+ ## Two entry points
12
+
13
+ VexCMS configuration is split into a client-safe `vex.config.ts` and a server-only
14
+ `vex.config.server.ts`, and this package mirrors that split:
15
+
16
+ | Entry point | Where it's imported | What it does |
17
+ |---|---|---|
18
+ | `@vexcms/better-auth` (root) | `vex.config.server.ts`, Convex functions | `betterAuthAdapter()` — instantiates the live adapter from your real Better Auth options. Re-exports the Convex DB adapter, so it pulls in `convex/server`. **Never import this from client code.** |
19
+ | `@vexcms/better-auth/client` | `vex.config.ts` | `betterAuthCollections()` — derives the same collections from a plain-data schema description. No plugin factories, no environment reads, no server SDKs (~1 KB gzip marginal). |
20
+
11
21
  ## Usage
12
22
 
13
- ```typescript
14
- import { defineConfig } from "@vexcms/core"
15
- import { vexBetterAuth } from "@vexcms/better-auth"
23
+ Author a client-safe module holding everything about your Better Auth setup that determines
24
+ which collections exist and which fields they carry — model names, `user.additionalFields`,
25
+ and which plugins are registered:
16
26
 
17
- export default defineConfig({
18
- auth: vexBetterAuth({
19
- config: {
20
- user: { modelName: "users" },
21
- plugins: [admin()],
27
+ ```ts
28
+ // src/auth/schema.ts — no server imports, safe in the browser bundle
29
+ import type { BetterAuthSchemaInput } from "@vexcms/better-auth/client";
30
+
31
+ export const authSchema = {
32
+ user: {
33
+ additionalFields: {
34
+ roles: { type: "string[]", defaultValue: ["user"], required: true },
22
35
  },
23
- }),
36
+ },
37
+ plugins: {
38
+ admin: true,
39
+ anonymous: true,
40
+ apiKey: true,
41
+ convex: true,
42
+ organization: { teams: true },
43
+ },
44
+ } satisfies BetterAuthSchemaInput;
45
+ ```
46
+
47
+ ```ts
48
+ // vex.config.ts (client)
49
+ import { betterAuthCollections } from "@vexcms/better-auth/client";
50
+ import { defineConfig } from "@vexcms/core";
51
+
52
+ import { authSchema } from "./auth/schema";
53
+
54
+ export default defineConfig({
55
+ authCollections: betterAuthCollections(authSchema),
24
56
  collections: [/* ... */],
25
- })
57
+ });
58
+ ```
59
+
60
+ Your real Better Auth options spread the same schema module and add everything server-only —
61
+ the secret, base URL, and real plugin instances. `plugins` on `authSchema` is a descriptor map
62
+ for `betterAuthCollections()`, not plugin instances, so it's dropped by name rather than
63
+ shadowed by the override:
64
+
65
+ ```ts
66
+ // src/auth/options.ts (server)
67
+ import type { BetterAuthOptions } from "better-auth";
68
+ import { admin, anonymous, organization } from "better-auth/plugins";
69
+ import { apiKey } from "@better-auth/api-key";
70
+ import { convex } from "@convex-dev/better-auth/plugins";
71
+
72
+ import { authSchema } from "./schema";
73
+
74
+ const { plugins: _pluginDescriptors, ...schemaOptions } = authSchema;
75
+
76
+ export const authOptions: BetterAuthOptions = {
77
+ ...schemaOptions,
78
+ secret: process.env.BETTER_AUTH_SECRET,
79
+ plugins: [admin(), anonymous(), apiKey(), convex(), organization({ teams: true })],
80
+ };
26
81
  ```
27
82
 
83
+ ```ts
84
+ // vex.config.server.ts (server)
85
+ import { betterAuthAdapter } from "@vexcms/better-auth";
86
+ import { defineServerConfig } from "@vexcms/core";
87
+
88
+ import { authOptions } from "./auth/options";
89
+
90
+ import vexConfig from "./vex.config";
91
+
92
+ export default defineServerConfig({
93
+ config: vexConfig,
94
+ server: {
95
+ auth: { adapter: betterAuthAdapter({ config: authOptions }) },
96
+ },
97
+ });
98
+ ```
99
+
100
+ `defineServerConfig` compares the client config's `authCollections` against the live
101
+ adapter's `collections` and throws `VexAuthConfigError` naming the first divergence — e.g.
102
+ `collection "user" is missing field(s) "phone"` — the moment `authSchema` and `authOptions`
103
+ fall out of sync. Registering the adapter (`server.auth`) is optional: omit it for a project
104
+ with no auth, or to skip the check.
105
+
28
106
  ## What It Does
29
107
 
30
- `vexBetterAuth()` takes your Better Auth configuration and:
108
+ Both entry points convert Better Auth's schema into Vex collections through the same pipeline:
31
109
 
32
- 1. **Extracts auth tables** — Uses Better Auth's `getAuthTables()` to resolve the complete schema including plugin-contributed fields
33
- 2. **Converts fields** — Maps Better Auth field types to VEX field types (string → text, boolean → checkbox, references → relationship, etc.)
34
- 3. **Configures admin UI** — Sets appropriate field visibility:
35
- - **Editable**: user name, image, role, ban status
36
- - **Hidden**: sensitive fields (tokens, secrets, verification values)
37
- - **Read-only**: everything else
38
- 4. **Creates indexes** — Generates database indexes for indexed/unique fields
39
- 5. **Groups in sidebar** — All auth collections appear under the "Auth" group
110
+ 1. **Resolves the full schema** — `betterAuthAdapter()` runs Better Auth's `getAuthTables()`
111
+ over your real options; `betterAuthCollections()` runs it over plain `{ id, schema }` plugin
112
+ stubs resolved from a committed schema snapshot, so both produce identical collections
113
+ 2. **Converts fields** — maps Better Auth field types to Vex field types (string → text,
114
+ boolean → checkbox, references → relationship, etc.)
115
+ 3. **Configures admin UI** — sets field visibility: editable (name, image, role, ban status),
116
+ hidden (tokens, secrets, verification values), read-only (everything else)
117
+ 4. **Creates indexes** — generates database indexes for indexed/unique fields
118
+ 5. **Protects system collections** — every auth collection except `user` is `protected`, so a
119
+ user-defined collection can't override its slug
40
120
 
41
121
  ## Supported Auth Tables
42
122
 
@@ -47,36 +127,63 @@ export default defineConfig({
47
127
  | `account` | OAuth/credential accounts linked to users |
48
128
  | `verification` | Email verification and password reset tokens |
49
129
 
50
- Plugin-contributed tables (e.g., from `admin()`, `apiKey()`) are automatically included.
130
+ ## Modelled plugins
131
+
132
+ `betterAuthCollections()` models 11 plugins' schemas directly — `admin`, `anonymous`,
133
+ `apiKey`, `convex`, `deviceAuthorization`, `jwt`, `mcp`, `oidcProvider`, `organization`
134
+ (`teams` changes which tables exist), `phoneNumber`, `siwe`, `twoFactor`. Each descriptor
135
+ accepts `true` or schema-affecting options only (`{ schema: { ... } }` to rename columns or
136
+ add fields) — runtime options (roles, mailers, secrets) are rejected by the type, since
137
+ accepting them would risk a server callback ending up in the browser bundle.
138
+
139
+ Naming any other `better-auth/plugins` export in `authSchema.plugins` is a type error pointing
140
+ at the fix: read that plugin's `schema` off its real instance on the server, and hand-copy the
141
+ plain-data value into `extraSchema`:
142
+
143
+ ```ts
144
+ // src/auth/schema.ts
145
+ export const authSchema = {
146
+ plugins: { admin: true },
147
+ // `magicLink` isn't modelled — copy its `schema` shape from the real plugin
148
+ // instance rather than importing `better-auth/plugins` here.
149
+ extraSchema: {
150
+ verification: { fields: {} },
151
+ },
152
+ } satisfies BetterAuthSchemaInput;
153
+ ```
51
154
 
52
- ## Plugin Support
155
+ `betterAuthAdapter()` (server-side) accepts any registered plugin directly — it instantiates
156
+ your real options, so it needs no such list.
53
157
 
54
- Better Auth plugins that add fields or tables are fully supported:
158
+ ## Custom Model Names
55
159
 
56
- ```typescript
57
- import { admin } from "better-auth/plugins"
160
+ Custom `modelName` values are respected and propagated to relationship references:
58
161
 
59
- vexBetterAuth({
60
- config: {
61
- plugins: [admin()], // Adds role, banned, banReason fields to user
62
- },
63
- })
162
+ ```ts
163
+ export const authSchema = {
164
+ user: { modelName: "users" },
165
+ session: { modelName: "sessions" },
166
+ } satisfies BetterAuthSchemaInput;
64
167
  ```
65
168
 
66
- ## Custom Model Names
169
+ ## Keeping the plugin schema snapshot fresh
67
170
 
68
- Custom `modelName` values are respected and propagated to relationship references:
171
+ `betterAuthCollections()` reads plugin schemas from a snapshot committed to this package
172
+ (`src/pluginSchemas.generated.ts`), so it never instantiates a real plugin factory.
173
+ Regenerate it after a Better Auth upgrade:
69
174
 
70
- ```typescript
71
- vexBetterAuth({
72
- config: {
73
- user: { modelName: "users" },
74
- session: { modelName: "sessions" },
75
- },
76
- })
175
+ ```bash
176
+ pnpm --filter @vexcms/better-auth gen:auth-schemas
77
177
  ```
78
178
 
179
+ `pluginSchemas.test.ts` fails whenever the committed snapshot and the installed `better-auth`
180
+ version disagree, and a new upstream plugin fails this package's typecheck via an
181
+ exhaustiveness assertion until it's classified as modelled or explicitly routed to
182
+ `extraSchema`. Projects consuming this package never run this — it's this package's own
183
+ maintenance task.
184
+
79
185
  ## Peer Dependencies
80
186
 
81
187
  - `@vexcms/core` — Core VEX CMS types
82
- - `better-auth` — Better Auth (>=1.4.9 <1.5.0)
188
+ - `better-auth` — Better Auth (>=1.6.23 <1.7.0)
189
+ - `convex` — Convex backend (>=1.44.0 <2)
package/dist/adapter.d.ts CHANGED
@@ -16,7 +16,12 @@ export interface BetterAuthAdapterOptions {
16
16
  *
17
17
  * Introspects Better Auth's full merged schema (base fields + plugin fields
18
18
  * + additionalFields) via `getAuthTables()` and converts each table into a
19
- * standard Vex `CollectionConfig` using Vex field builders.
19
+ * standard Vex `CollectionConfig` using Vex field builders. Server-only: pass
20
+ * the resulting `VexAuthAdapter` to `defineServerConfig({ server: { auth: { adapter } } })`,
21
+ * which also uses its `collections` as the source of truth to verify that the client config's
22
+ * `authCollections` — built by {@link better-auth/src!betterAuthCollections} from
23
+ * `@vexcms/better-auth/client` — have not drifted, throwing `VexAuthConfigError` naming the
24
+ * first divergence.
20
25
  *
21
26
  * Mappings from Better Auth `DBFieldAttribute` to Vex fields:
22
27
  * - `type` → Vex field type (`string`→text, `boolean`→checkbox, `number`→number,
@@ -31,16 +36,23 @@ export interface BetterAuthAdapterOptions {
31
36
  * @param props — Adapter options; pass your Better Auth config object wrapped
32
37
  * in `{ config: authOptions }`. Optional — when omitted, uses default
33
38
  * Better Auth tables with no plugins or additional fields.
34
- * @returns A `VexAuthAdapter` ready for `defineConfig({ auth: … })`.
39
+ * @returns A `VexAuthAdapter` ready for `defineServerConfig({ server: { auth: { adapter } } })`.
35
40
  *
36
41
  * @example
37
42
  * ```ts
43
+ * // vex.config.server.ts
38
44
  * import { betterAuthAdapter } from "@vexcms/better-auth";
45
+ * import { defineServerConfig } from "@vexcms/core";
46
+ *
39
47
  * import { authOptions } from "~/auth/server";
40
48
  *
41
- * export default defineConfig({
42
- * auth: betterAuthAdapter({ config: authOptions }),
43
- * collections: [posts],
49
+ * import vexConfig from "./vex.config";
50
+ *
51
+ * export default defineServerConfig({
52
+ * config: vexConfig,
53
+ * server: {
54
+ * auth: { adapter: betterAuthAdapter({ config: authOptions }) },
55
+ * },
44
56
  * });
45
57
  * ```
46
58
  */
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EAKpB,cAAc,EACf,MAAM,cAAc,CAAC;AAWtB,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGvE;;;;;;;GAOG;AACH,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AA+BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,CAAC,EAAE,wBAAwB,GAC/B,cAAc,CAmGhB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,QAYA"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EAKpB,cAAc,EACf,MAAM,cAAc,CAAC;AAWtB,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGvE;;;;;;;GAOG;AACH,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AA+BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,EAAE,wBAAwB,GAAG,cAAc,CAmGlF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IACvC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB,QAOA"}
@@ -0,0 +1,196 @@
1
+ // src/adapter.ts
2
+ import {
3
+ array,
4
+ checkbox,
5
+ date,
6
+ defineCollection,
7
+ number,
8
+ select,
9
+ text
10
+ } from "@vexcms/core";
11
+ import { getAuthTables } from "better-auth/db";
12
+ var AUTH_COLLECTION_TYPES = {
13
+ user: "user",
14
+ session: "session",
15
+ account: "account",
16
+ verification: "verification"
17
+ };
18
+ var EDITABLE_FIELDS = /* @__PURE__ */ new Set([
19
+ "name",
20
+ "email",
21
+ "image",
22
+ "role",
23
+ "banned",
24
+ "banReason",
25
+ "banExpires"
26
+ ]);
27
+ var HIDDEN_FIELDS = /* @__PURE__ */ new Set([
28
+ "hashedPassword",
29
+ "password",
30
+ "twoFactorSecret",
31
+ "twoFactorBackupCodes",
32
+ "token",
33
+ "secret",
34
+ "code"
35
+ ]);
36
+ function betterAuthAdapter(props) {
37
+ const tables = getAuthTables(props?.config ?? {});
38
+ const collections = [];
39
+ const authCollections = [
40
+ { type: AUTH_COLLECTION_TYPES.user, slug: props?.config?.user?.modelName },
41
+ {
42
+ type: AUTH_COLLECTION_TYPES.session,
43
+ slug: props?.config?.session?.modelName
44
+ },
45
+ {
46
+ type: AUTH_COLLECTION_TYPES.account,
47
+ slug: props?.config?.account?.modelName
48
+ },
49
+ {
50
+ type: AUTH_COLLECTION_TYPES.verification,
51
+ slug: props?.config?.verification?.modelName
52
+ }
53
+ ];
54
+ for (const [tableKey, tableDef] of Object.entries(tables)) {
55
+ const slug = tableDef.modelName ?? tableKey;
56
+ const fields = {};
57
+ addAuthCollectionFields({
58
+ attributes: tableDef.fields,
59
+ slug,
60
+ fields,
61
+ config: props?.config,
62
+ extractId: true
63
+ });
64
+ const authCollectionType = authCollections.find((ac) => ac.slug === slug)?.type;
65
+ if (authCollectionType) {
66
+ switch (authCollectionType) {
67
+ case "user": {
68
+ const additionalFields = props?.config?.user?.additionalFields;
69
+ addAuthCollectionFields({
70
+ attributes: additionalFields,
71
+ slug,
72
+ fields,
73
+ config: props?.config
74
+ });
75
+ break;
76
+ }
77
+ case "session": {
78
+ const additionalFields = props?.config?.session?.additionalFields;
79
+ addAuthCollectionFields({
80
+ attributes: additionalFields,
81
+ slug,
82
+ fields,
83
+ config: props?.config
84
+ });
85
+ break;
86
+ }
87
+ case "account": {
88
+ const additionalFields = props?.config?.account?.additionalFields;
89
+ addAuthCollectionFields({
90
+ attributes: additionalFields,
91
+ slug,
92
+ fields,
93
+ config: props?.config
94
+ });
95
+ break;
96
+ }
97
+ case "verification": {
98
+ const additionalFields = props?.config?.verification?.additionalFields;
99
+ addAuthCollectionFields({
100
+ attributes: additionalFields,
101
+ slug,
102
+ fields,
103
+ config: props?.config
104
+ });
105
+ break;
106
+ }
107
+ default:
108
+ break;
109
+ }
110
+ }
111
+ const isProtected = slug !== "user" && slug !== "users";
112
+ collections.push(
113
+ defineCollection({
114
+ slug,
115
+ fields,
116
+ admin: {
117
+ icon: "Lock"
118
+ },
119
+ meta: isProtected ? { protected: true } : void 0
120
+ })
121
+ );
122
+ }
123
+ const userSlug = tables.user?.modelName ?? "user";
124
+ return {
125
+ name: "better-auth",
126
+ collections,
127
+ userCollection: userSlug
128
+ };
129
+ }
130
+ function addAuthCollectionFields(props) {
131
+ if (!props.attributes) return;
132
+ for (const [fieldName, attr] of Object.entries(props.attributes)) {
133
+ if (props.extractId === true && fieldName === "id") continue;
134
+ const field = betterAuthAttrToVexField(fieldName, attr, props.slug, props?.config);
135
+ if (field) props.fields[fieldName] = field;
136
+ }
137
+ }
138
+ function betterAuthAttrToVexField(fieldName, attr, tableSlug, authOptions) {
139
+ if (fieldName === "id") return null;
140
+ const isUserTable = tableSlug === authOptions?.user?.modelName || tableSlug === "user" || tableSlug === "users";
141
+ const isEditable = isUserTable && EDITABLE_FIELDS.has(fieldName);
142
+ const isHidden = HIDDEN_FIELDS.has(fieldName);
143
+ const admin = {};
144
+ if (!isEditable) admin.readOnly = true;
145
+ if (isHidden) admin.hidden = true;
146
+ const baseOptions = {
147
+ defaultValue: void 0
148
+ };
149
+ if (Object.keys(admin).length > 0) baseOptions.admin = admin;
150
+ if (!isEditable) baseOptions.meta = { locked: true };
151
+ if (attr.required) baseOptions.required = attr.required;
152
+ if (attr.defaultValue !== void 0 && typeof attr.defaultValue !== "function") {
153
+ baseOptions.defaultValue = attr.defaultValue;
154
+ }
155
+ if (attr.unique || attr.index) {
156
+ baseOptions.index = `by_${fieldName}`;
157
+ }
158
+ if (attr.references) {
159
+ return text(baseOptions);
160
+ }
161
+ if (Array.isArray(attr.type)) {
162
+ return select({
163
+ options: attr.type.map((v) => ({ value: v, label: v })),
164
+ ...baseOptions
165
+ });
166
+ }
167
+ switch (attr.type) {
168
+ case "string":
169
+ return text(baseOptions);
170
+ case "string[]":
171
+ return array({
172
+ ...baseOptions,
173
+ items: text(baseOptions)
174
+ });
175
+ case "boolean":
176
+ return checkbox(baseOptions);
177
+ case "number":
178
+ return number(baseOptions);
179
+ case "number[]":
180
+ return array({
181
+ ...baseOptions,
182
+ items: number(baseOptions)
183
+ });
184
+ case "date":
185
+ return date(baseOptions);
186
+ case "json":
187
+ return text(baseOptions);
188
+ default:
189
+ return null;
190
+ }
191
+ }
192
+
193
+ export {
194
+ betterAuthAdapter
195
+ };
196
+ //# sourceMappingURL=chunk-YPIEILO5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/adapter.ts"],"sourcesContent":["import type {\n AuthCollectionConfig,\n AuthFieldMeta,\n BaseFieldInput,\n CollectionSlug,\n FieldAdminConfigInput,\n VexAuthAdapter,\n} from \"@vexcms/core\";\nimport {\n array,\n AuthCollectionMeta,\n checkbox,\n date,\n defineCollection,\n number,\n select,\n text,\n} from \"@vexcms/core\";\nimport type { BetterAuthOptions, DBFieldAttribute } from \"better-auth\";\nimport { getAuthTables } from \"better-auth/db\";\n\n/**\n * Options for `betterAuthAdapter()`.\n *\n * Accepts the same `BetterAuthOptions` object you pass to `betterAuth()`\n * on the server. Only schema-affecting properties (modelNames,\n * additionalFields, plugins) are read. Runtime options (database, secret,\n * baseURL) are ignored.\n */\nexport interface BetterAuthAdapterOptions {\n config: BetterAuthOptions;\n}\n\nconst AUTH_COLLECTION_TYPES = {\n user: \"user\",\n session: \"session\",\n account: \"account\",\n verification: \"verification\",\n} as const;\n\n/** Fields on the user table that end-users are allowed to edit. */\nconst EDITABLE_FIELDS = new Set([\n \"name\",\n \"email\",\n \"image\",\n \"role\",\n \"banned\",\n \"banReason\",\n \"banExpires\",\n]);\n\n/** Fields that should be hidden in the admin UI for security. */\nconst HIDDEN_FIELDS = new Set([\n \"hashedPassword\",\n \"password\",\n \"twoFactorSecret\",\n \"twoFactorBackupCodes\",\n \"token\",\n \"secret\",\n \"code\",\n]);\n\n/**\n * Creates a Vex auth adapter from a Better Auth configuration.\n *\n * Introspects Better Auth's full merged schema (base fields + plugin fields\n * + additionalFields) via `getAuthTables()` and converts each table into a\n * standard Vex `CollectionConfig` using Vex field builders. Server-only: pass\n * the resulting `VexAuthAdapter` to `defineServerConfig({ server: { auth: { adapter } } })`,\n * which also uses its `collections` as the source of truth to verify that the client config's\n * `authCollections` — built by {@link better-auth/src!betterAuthCollections} from\n * `@vexcms/better-auth/client` — have not drifted, throwing `VexAuthConfigError` naming the\n * first divergence.\n *\n * Mappings from Better Auth `DBFieldAttribute` to Vex fields:\n * - `type` → Vex field type (`string`→text, `boolean`→checkbox, `number`→number,\n * `date`/`timestamp`→date, `json`→text fallback, array→select)\n * - `required` → field `required`\n * - `defaultValue` → field `defaultValue`\n * - `unique`/`index` → field `index` (index name: `by_<field>_unique` or `by_<field>`)\n * - `references` → `relationship` field\n * - System fields get `admin.readOnly: true` and `meta.locked: true`\n * - Sensitive fields get `admin.hidden: true`\n *\n * @param props — Adapter options; pass your Better Auth config object wrapped\n * in `{ config: authOptions }`. Optional — when omitted, uses default\n * Better Auth tables with no plugins or additional fields.\n * @returns A `VexAuthAdapter` ready for `defineServerConfig({ server: { auth: { adapter } } })`.\n *\n * @example\n * ```ts\n * // vex.config.server.ts\n * import { betterAuthAdapter } from \"@vexcms/better-auth\";\n * import { defineServerConfig } from \"@vexcms/core\";\n *\n * import { authOptions } from \"~/auth/server\";\n *\n * import vexConfig from \"./vex.config\";\n *\n * export default defineServerConfig({\n * config: vexConfig,\n * server: {\n * auth: { adapter: betterAuthAdapter({ config: authOptions }) },\n * },\n * });\n * ```\n */\nexport function betterAuthAdapter(props?: BetterAuthAdapterOptions): VexAuthAdapter {\n const tables = getAuthTables(props?.config ?? {});\n const collections: AuthCollectionConfig[] = [];\n const authCollections = [\n { type: AUTH_COLLECTION_TYPES.user, slug: props?.config?.user?.modelName },\n {\n type: AUTH_COLLECTION_TYPES.session,\n slug: props?.config?.session?.modelName,\n },\n {\n type: AUTH_COLLECTION_TYPES.account,\n slug: props?.config?.account?.modelName,\n },\n {\n type: AUTH_COLLECTION_TYPES.verification,\n slug: props?.config?.verification?.modelName,\n },\n ] as const;\n\n for (const [tableKey, tableDef] of Object.entries(tables)) {\n const slug = tableDef.modelName ?? tableKey;\n const fields: AuthCollectionConfig[\"fields\"] = {};\n\n addAuthCollectionFields({\n attributes: tableDef.fields,\n slug,\n fields,\n config: props?.config,\n extractId: true,\n });\n\n const authCollectionType = authCollections.find((ac) => ac.slug === slug)?.type;\n if (authCollectionType) {\n switch (authCollectionType) {\n case \"user\": {\n const additionalFields = props?.config?.user?.additionalFields;\n addAuthCollectionFields({\n attributes: additionalFields,\n slug,\n fields,\n config: props?.config,\n });\n break;\n }\n case \"session\": {\n const additionalFields = props?.config?.session?.additionalFields;\n addAuthCollectionFields({\n attributes: additionalFields,\n slug,\n fields,\n config: props?.config,\n });\n break;\n }\n case \"account\": {\n const additionalFields = props?.config?.account?.additionalFields;\n addAuthCollectionFields({\n attributes: additionalFields,\n slug,\n fields,\n config: props?.config,\n });\n break;\n }\n case \"verification\": {\n const additionalFields = props?.config?.verification?.additionalFields;\n addAuthCollectionFields({\n attributes: additionalFields,\n slug,\n fields,\n config: props?.config,\n });\n break;\n }\n default:\n break;\n }\n }\n\n const isProtected = slug !== \"user\" && slug !== \"users\";\n\n collections.push(\n defineCollection<AuthFieldMeta, AuthCollectionMeta>({\n slug: slug as CollectionSlug,\n fields,\n admin: {\n icon: \"Lock\",\n },\n meta: isProtected ? { protected: true } : undefined,\n }),\n );\n }\n\n const userSlug = tables.user?.modelName ?? \"user\";\n return {\n name: \"better-auth\",\n collections,\n userCollection: userSlug as CollectionSlug,\n };\n}\n\n/**\n * Converts a set of Better Auth field attributes into Vex field definitions\n * and appends them to the given collection `fields` map.\n *\n * Iterates over `attributes`, calls {@link betterAuthAttrToVexField} for each\n * entry, and mutates `props.fields` in place. When `extractId` is `true`,\n * the `id` field is skipped because Convex auto-generates `_id`.\n *\n * @param props — Field extraction options.\n * @param props.attributes — Better Auth `DBFieldAttribute` map to convert.\n * @param props.slug — Collection slug (used for relationship target resolution).\n * @param props.fields — Mutable Vex collection fields object to populate.\n * @param props.config — Better Auth options (for model name / additional fields lookups).\n * @param props.extractId — When `true`, skips the `id` field.\n *\n * @internal\n */\nexport function addAuthCollectionFields(props: {\n attributes?: Record<string, DBFieldAttribute>;\n slug: string;\n fields: AuthCollectionConfig[\"fields\"];\n config?: BetterAuthOptions;\n extractId?: boolean;\n}) {\n if (!props.attributes) return;\n for (const [fieldName, attr] of Object.entries(props.attributes)) {\n if (props.extractId === true && fieldName === \"id\") continue;\n const field = betterAuthAttrToVexField(fieldName, attr, props.slug, props?.config);\n if (field) props.fields[fieldName] = field;\n }\n}\n\n/**\n * Maps a single Better Auth `DBFieldAttribute` to a Vex field builder call.\n *\n * Handles all Better Auth scalar types: `string`, `boolean`, `number`, `date`,\n * `timestamp`, `json`, and enum arrays. Maps overlapping properties\n * (`required`, `defaultValue`, `unique`/`index`, `references`) to their\n * Vex equivalents. Sets `admin.readOnly` and `meta.locked` on system\n * fields, and `admin.hidden` on sensitive fields.\n *\n * @param fieldName — The field name from Better Auth.\n * @param attr — The Better Auth field attribute.\n * @param tableSlug — The collection slug (for relationship targets).\n * @param authOptions -- The betterAuth options\n * @returns A Vex field instance, or `null` if the type is unsupported.\n *\n * @internal\n */\nfunction betterAuthAttrToVexField(\n fieldName: string,\n attr: DBFieldAttribute,\n tableSlug: string,\n authOptions?: BetterAuthOptions,\n) {\n // Skip id — Convex auto-generates _id\n if (fieldName === \"id\") return null;\n\n const isUserTable =\n tableSlug === authOptions?.user?.modelName || tableSlug === \"user\" || tableSlug === \"users\";\n const isEditable = isUserTable && EDITABLE_FIELDS.has(fieldName);\n const isHidden = HIDDEN_FIELDS.has(fieldName);\n\n const admin: FieldAdminConfigInput = {};\n if (!isEditable) admin.readOnly = true;\n if (isHidden) admin.hidden = true;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const baseOptions: { defaultValue: any } & BaseFieldInput<AuthFieldMeta> = {\n defaultValue: undefined,\n };\n if (Object.keys(admin).length > 0) baseOptions.admin = admin;\n if (!isEditable) baseOptions.meta = { locked: true };\n if (attr.required) baseOptions.required = attr.required;\n if (attr.defaultValue !== undefined && typeof attr.defaultValue !== \"function\") {\n baseOptions.defaultValue = attr.defaultValue;\n }\n if (attr.unique || attr.index) {\n baseOptions.index = `by_${fieldName}`;\n }\n\n if (attr.references) {\n return text(baseOptions);\n }\n\n if (Array.isArray(attr.type)) {\n return select({\n options: attr.type.map((v: string) => ({ value: v, label: v })),\n ...baseOptions,\n });\n }\n\n switch (attr.type) {\n case \"string\":\n return text(baseOptions);\n case \"string[]\":\n return array({\n ...baseOptions,\n items: text(baseOptions),\n });\n case \"boolean\":\n return checkbox(baseOptions);\n case \"number\":\n return number(baseOptions);\n case \"number[]\":\n return array({\n ...baseOptions,\n items: number(baseOptions),\n });\n case \"date\":\n return date(baseOptions);\n case \"json\":\n // Fall back to text until a dedicated json field type is wired\n // TODO: create json field\n return text(baseOptions);\n default:\n // Unknown type — skip rather than crash\n return null;\n }\n}\n"],"mappings":";AAQA;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,qBAAqB;AAc9B,IAAM,wBAAwB;AAAA,EAC5B,MAAM;AAAA,EACN,SAAS;AAAA,EACT,SAAS;AAAA,EACT,cAAc;AAChB;AAGA,IAAM,kBAAkB,oBAAI,IAAI;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAGD,IAAM,gBAAgB,oBAAI,IAAI;AAAA,EAC5B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AA+CM,SAAS,kBAAkB,OAAkD;AAClF,QAAM,SAAS,cAAc,OAAO,UAAU,CAAC,CAAC;AAChD,QAAM,cAAsC,CAAC;AAC7C,QAAM,kBAAkB;AAAA,IACtB,EAAE,MAAM,sBAAsB,MAAM,MAAM,OAAO,QAAQ,MAAM,UAAU;AAAA,IACzE;AAAA,MACE,MAAM,sBAAsB;AAAA,MAC5B,MAAM,OAAO,QAAQ,SAAS;AAAA,IAChC;AAAA,IACA;AAAA,MACE,MAAM,sBAAsB;AAAA,MAC5B,MAAM,OAAO,QAAQ,SAAS;AAAA,IAChC;AAAA,IACA;AAAA,MACE,MAAM,sBAAsB;AAAA,MAC5B,MAAM,OAAO,QAAQ,cAAc;AAAA,IACrC;AAAA,EACF;AAEA,aAAW,CAAC,UAAU,QAAQ,KAAK,OAAO,QAAQ,MAAM,GAAG;AACzD,UAAM,OAAO,SAAS,aAAa;AACnC,UAAM,SAAyC,CAAC;AAEhD,4BAAwB;AAAA,MACtB,YAAY,SAAS;AAAA,MACrB;AAAA,MACA;AAAA,MACA,QAAQ,OAAO;AAAA,MACf,WAAW;AAAA,IACb,CAAC;AAED,UAAM,qBAAqB,gBAAgB,KAAK,CAAC,OAAO,GAAG,SAAS,IAAI,GAAG;AAC3E,QAAI,oBAAoB;AACtB,cAAQ,oBAAoB;AAAA,QAC1B,KAAK,QAAQ;AACX,gBAAM,mBAAmB,OAAO,QAAQ,MAAM;AAC9C,kCAAwB;AAAA,YACtB,YAAY;AAAA,YACZ;AAAA,YACA;AAAA,YACA,QAAQ,OAAO;AAAA,UACjB,CAAC;AACD;AAAA,QACF;AAAA,QACA,KAAK,WAAW;AACd,gBAAM,mBAAmB,OAAO,QAAQ,SAAS;AACjD,kCAAwB;AAAA,YACtB,YAAY;AAAA,YACZ;AAAA,YACA;AAAA,YACA,QAAQ,OAAO;AAAA,UACjB,CAAC;AACD;AAAA,QACF;AAAA,QACA,KAAK,WAAW;AACd,gBAAM,mBAAmB,OAAO,QAAQ,SAAS;AACjD,kCAAwB;AAAA,YACtB,YAAY;AAAA,YACZ;AAAA,YACA;AAAA,YACA,QAAQ,OAAO;AAAA,UACjB,CAAC;AACD;AAAA,QACF;AAAA,QACA,KAAK,gBAAgB;AACnB,gBAAM,mBAAmB,OAAO,QAAQ,cAAc;AACtD,kCAAwB;AAAA,YACtB,YAAY;AAAA,YACZ;AAAA,YACA;AAAA,YACA,QAAQ,OAAO;AAAA,UACjB,CAAC;AACD;AAAA,QACF;AAAA,QACA;AACE;AAAA,MACJ;AAAA,IACF;AAEA,UAAM,cAAc,SAAS,UAAU,SAAS;AAEhD,gBAAY;AAAA,MACV,iBAAoD;AAAA,QAClD;AAAA,QACA;AAAA,QACA,OAAO;AAAA,UACL,MAAM;AAAA,QACR;AAAA,QACA,MAAM,cAAc,EAAE,WAAW,KAAK,IAAI;AAAA,MAC5C,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,WAAW,OAAO,MAAM,aAAa;AAC3C,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,gBAAgB;AAAA,EAClB;AACF;AAmBO,SAAS,wBAAwB,OAMrC;AACD,MAAI,CAAC,MAAM,WAAY;AACvB,aAAW,CAAC,WAAW,IAAI,KAAK,OAAO,QAAQ,MAAM,UAAU,GAAG;AAChE,QAAI,MAAM,cAAc,QAAQ,cAAc,KAAM;AACpD,UAAM,QAAQ,yBAAyB,WAAW,MAAM,MAAM,MAAM,OAAO,MAAM;AACjF,QAAI,MAAO,OAAM,OAAO,SAAS,IAAI;AAAA,EACvC;AACF;AAmBA,SAAS,yBACP,WACA,MACA,WACA,aACA;AAEA,MAAI,cAAc,KAAM,QAAO;AAE/B,QAAM,cACJ,cAAc,aAAa,MAAM,aAAa,cAAc,UAAU,cAAc;AACtF,QAAM,aAAa,eAAe,gBAAgB,IAAI,SAAS;AAC/D,QAAM,WAAW,cAAc,IAAI,SAAS;AAE5C,QAAM,QAA+B,CAAC;AACtC,MAAI,CAAC,WAAY,OAAM,WAAW;AAClC,MAAI,SAAU,OAAM,SAAS;AAG7B,QAAM,cAAqE;AAAA,IACzE,cAAc;AAAA,EAChB;AACA,MAAI,OAAO,KAAK,KAAK,EAAE,SAAS,EAAG,aAAY,QAAQ;AACvD,MAAI,CAAC,WAAY,aAAY,OAAO,EAAE,QAAQ,KAAK;AACnD,MAAI,KAAK,SAAU,aAAY,WAAW,KAAK;AAC/C,MAAI,KAAK,iBAAiB,UAAa,OAAO,KAAK,iBAAiB,YAAY;AAC9E,gBAAY,eAAe,KAAK;AAAA,EAClC;AACA,MAAI,KAAK,UAAU,KAAK,OAAO;AAC7B,gBAAY,QAAQ,MAAM,SAAS;AAAA,EACrC;AAEA,MAAI,KAAK,YAAY;AACnB,WAAO,KAAK,WAAW;AAAA,EACzB;AAEA,MAAI,MAAM,QAAQ,KAAK,IAAI,GAAG;AAC5B,WAAO,OAAO;AAAA,MACZ,SAAS,KAAK,KAAK,IAAI,CAAC,OAAe,EAAE,OAAO,GAAG,OAAO,EAAE,EAAE;AAAA,MAC9D,GAAG;AAAA,IACL,CAAC;AAAA,EACH;AAEA,UAAQ,KAAK,MAAM;AAAA,IACjB,KAAK;AACH,aAAO,KAAK,WAAW;AAAA,IACzB,KAAK;AACH,aAAO,MAAM;AAAA,QACX,GAAG;AAAA,QACH,OAAO,KAAK,WAAW;AAAA,MACzB,CAAC;AAAA,IACH,KAAK;AACH,aAAO,SAAS,WAAW;AAAA,IAC7B,KAAK;AACH,aAAO,OAAO,WAAW;AAAA,IAC3B,KAAK;AACH,aAAO,MAAM;AAAA,QACX,GAAG;AAAA,QACH,OAAO,OAAO,WAAW;AAAA,MAC3B,CAAC;AAAA,IACH,KAAK;AACH,aAAO,KAAK,WAAW;AAAA,IACzB,KAAK;AAGH,aAAO,KAAK,WAAW;AAAA,IACzB;AAEE,aAAO;AAAA,EACX;AACF;","names":[]}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Client-safe entry point for `@vexcms/better-auth`.
3
+ *
4
+ * The browser imports this — never the package root, which re-exports the
5
+ * Convex DB adapter and pulls `convex/server` into the graph. Everything here
6
+ * is pure data transformation: `@vexcms/core` plus Better Auth's
7
+ * `getAuthTables`/`mergeSchema` (~1 KB gzipped), with no plugin factories, no
8
+ * environment reads, and no server SDKs.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // src/vex.config.ts
13
+ * import { betterAuthCollections } from "@vexcms/better-auth/client";
14
+ * import { authSchema } from "./auth/schema";
15
+ *
16
+ * export default defineConfig({ authCollections: betterAuthCollections(authSchema) });
17
+ * ```
18
+ *
19
+ * @module
20
+ */
21
+ export { betterAuthCollections } from "./collections";
22
+ export type { BetterAuthCoreTableInput, BetterAuthSchemaInput, } from "./collections";
23
+ export type { BetterAuthPluginDescriptors, BetterAuthPluginName, ModelledPluginDescriptors, ModelledPluginName, PluginDBSchema, PluginSchemaOverride, } from "./pluginSchemas";
24
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,YAAY,EACV,wBAAwB,EACxB,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,YAAY,EACV,2BAA2B,EAC3B,oBAAoB,EACpB,yBAAyB,EACzB,kBAAkB,EAClB,cAAc,EACd,oBAAoB,GACrB,MAAM,iBAAiB,CAAC"}