@vexcms/better-auth 0.1.0-alpha.2 → 0.1.0-alpha.20
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 +147 -40
- package/dist/adapter.d.ts +17 -5
- package/dist/adapter.d.ts.map +1 -1
- package/dist/chunk-YPIEILO5.js +196 -0
- package/dist/chunk-YPIEILO5.js.map +1 -0
- package/dist/client.d.ts +24 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +997 -0
- package/dist/client.js.map +1 -0
- package/dist/collections.d.ts +88 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/convex/anonRole.d.ts +45 -0
- package/dist/convex/anonRole.d.ts.map +1 -0
- package/dist/convex/getAuth.d.ts +7 -4
- package/dist/convex/getAuth.d.ts.map +1 -1
- package/dist/convex/index.d.ts +4 -3
- package/dist/convex/index.d.ts.map +1 -1
- package/dist/index.d.ts +25 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -209
- package/dist/index.js.map +1 -1
- package/dist/pluginSchemas.d.ts +159 -0
- package/dist/pluginSchemas.d.ts.map +1 -0
- package/dist/pluginSchemas.generated.d.ts +4 -0
- package/dist/pluginSchemas.generated.d.ts.map +1 -0
- package/package.json +12 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @vexcms/better-auth
|
|
2
2
|
|
|
3
|
-
[Better Auth](https://www.better-auth.com/)
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
108
|
+
Both entry points convert Better Auth's schema into Vex collections through the same pipeline:
|
|
31
109
|
|
|
32
|
-
1. **
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
+
`betterAuthAdapter()` (server-side) accepts any registered plugin directly — it instantiates
|
|
156
|
+
your real options, so it needs no such list.
|
|
53
157
|
|
|
54
|
-
|
|
158
|
+
## Custom Model Names
|
|
55
159
|
|
|
56
|
-
|
|
57
|
-
import { admin } from "better-auth/plugins"
|
|
160
|
+
Custom `modelName` values are respected and propagated to relationship references:
|
|
58
161
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
},
|
|
63
|
-
}
|
|
162
|
+
```ts
|
|
163
|
+
export const authSchema = {
|
|
164
|
+
user: { modelName: "users" },
|
|
165
|
+
session: { modelName: "sessions" },
|
|
166
|
+
} satisfies BetterAuthSchemaInput;
|
|
64
167
|
```
|
|
65
168
|
|
|
66
|
-
##
|
|
169
|
+
## Keeping the plugin schema snapshot fresh
|
|
67
170
|
|
|
68
|
-
|
|
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
|
-
```
|
|
71
|
-
|
|
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.
|
|
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 `
|
|
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
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
*/
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -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
|
|
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":"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","names":[]}
|
package/dist/client.d.ts
ADDED
|
@@ -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"}
|