@opensaas/stack-auth 0.39.2 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +151 -0
- package/CLAUDE.md +140 -22
- package/dist/client/index.d.ts +0 -16
- package/dist/client/index.d.ts.map +1 -1
- package/dist/config/adopt-better-auth-tables.d.ts +4 -49
- package/dist/config/adopt-better-auth-tables.d.ts.map +1 -1
- package/dist/config/adopt-better-auth-tables.js +4 -51
- package/dist/config/adopt-better-auth-tables.js.map +1 -1
- package/dist/config/derive-auth-lists.d.ts +42 -37
- package/dist/config/derive-auth-lists.d.ts.map +1 -1
- package/dist/config/derive-auth-lists.js +579 -223
- package/dist/config/derive-auth-lists.js.map +1 -1
- package/dist/config/index.d.ts +0 -3
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +4 -34
- package/dist/config/index.js.map +1 -1
- package/dist/config/plugin.d.ts.map +1 -1
- package/dist/config/plugin.js +20 -90
- package/dist/config/plugin.js.map +1 -1
- package/dist/config/types.d.ts +89 -67
- package/dist/config/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -13
- package/dist/index.js.map +1 -1
- package/dist/lists/index.d.ts +9 -5
- package/dist/lists/index.d.ts.map +1 -1
- package/dist/lists/index.js +9 -6
- package/dist/lists/index.js.map +1 -1
- package/dist/mcp/better-auth.d.ts +6 -19
- package/dist/mcp/better-auth.d.ts.map +1 -1
- package/dist/mcp/better-auth.js +6 -20
- package/dist/mcp/better-auth.js.map +1 -1
- package/dist/plugins/index.d.ts +4 -4
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +4 -4
- package/dist/plugins/index.js.map +1 -1
- package/dist/runtime/types.d.ts +2 -9
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js +0 -4
- package/dist/runtime/types.js.map +1 -1
- package/dist/server/index.d.ts +5 -12
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +14 -62
- package/dist/server/index.js.map +1 -1
- package/dist/ui/components/ForgotPasswordForm.d.ts +0 -3
- package/dist/ui/components/ForgotPasswordForm.d.ts.map +1 -1
- package/dist/ui/components/ForgotPasswordForm.js +0 -3
- package/dist/ui/components/ForgotPasswordForm.js.map +1 -1
- package/dist/ui/components/ResetPasswordForm.d.ts +0 -3
- package/dist/ui/components/ResetPasswordForm.d.ts.map +1 -1
- package/dist/ui/components/ResetPasswordForm.js +0 -5
- package/dist/ui/components/ResetPasswordForm.js.map +1 -1
- package/dist/ui/components/SignInForm.d.ts +0 -3
- package/dist/ui/components/SignInForm.d.ts.map +1 -1
- package/dist/ui/components/SignInForm.js +0 -4
- package/dist/ui/components/SignInForm.js.map +1 -1
- package/dist/ui/components/SignUpForm.d.ts +0 -3
- package/dist/ui/components/SignUpForm.d.ts.map +1 -1
- package/dist/ui/components/SignUpForm.js +0 -5
- package/dist/ui/components/SignUpForm.js.map +1 -1
- package/dist/ui/lib/clean-error-message.d.ts +3 -6
- package/dist/ui/lib/clean-error-message.d.ts.map +1 -1
- package/dist/ui/lib/clean-error-message.js +3 -6
- package/dist/ui/lib/clean-error-message.js.map +1 -1
- package/dist/ui/types.d.ts +2 -15
- package/dist/ui/types.d.ts.map +1 -1
- package/dist/ui/types.js +2 -7
- package/dist/ui/types.js.map +1 -1
- package/package.json +12 -5
- package/src/client/index.ts +0 -17
- package/src/config/adopt-better-auth-tables.ts +4 -51
- package/src/config/derive-auth-lists.ts +685 -252
- package/src/config/index.ts +4 -34
- package/src/config/plugin.ts +21 -91
- package/src/config/types.ts +90 -67
- package/src/index.ts +3 -14
- package/src/lists/index.ts +17 -5
- package/src/mcp/better-auth.ts +6 -25
- package/src/plugins/index.ts +4 -5
- package/src/runtime/types.ts +2 -10
- package/src/server/index.ts +17 -70
- package/src/ui/components/ForgotPasswordForm.tsx +0 -3
- package/src/ui/components/ResetPasswordForm.tsx +0 -5
- package/src/ui/components/SignInForm.tsx +0 -4
- package/src/ui/components/SignUpForm.tsx +0 -5
- package/src/ui/lib/clean-error-message.ts +3 -6
- package/src/ui/types.ts +2 -15
- package/tests/auth-lists-drift.test.ts +480 -0
- package/tests/config.test.ts +3 -0
- package/tests/credential-field-read-deny-e2e.test.ts +295 -0
- package/tests/derive-auth-lists.test.ts +263 -6
- package/tests/generated-fk-shape.test.ts +432 -9
- package/tests/mcp-oauth-cascade-e2e.test.ts +267 -0
- package/tests/plugin-derived-keys.test.ts +39 -0
- package/tests/plugin-table-derivation.test.ts +270 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/server/schema-converter.d.ts +0 -81
- package/dist/server/schema-converter.d.ts.map +0 -1
- package/dist/server/schema-converter.js +0 -157
- package/dist/server/schema-converter.js.map +0 -1
- package/src/server/schema-converter.ts +0 -243
- package/tests/schema-converter.test.ts +0 -399
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convert Better Auth database schema to OpenSaaS list configs
|
|
3
|
-
* Allows dynamic list generation from Better Auth plugins
|
|
4
|
-
*/
|
|
5
|
-
import type { ListConfig } from '@opensaas/stack-core';
|
|
6
|
-
/**
|
|
7
|
-
* Better Auth field attribute structure
|
|
8
|
-
* Inferred from better-auth internal types
|
|
9
|
-
*/
|
|
10
|
-
type BetterAuthFieldAttribute = {
|
|
11
|
-
type: string | string[];
|
|
12
|
-
required?: boolean;
|
|
13
|
-
unique?: boolean;
|
|
14
|
-
references?: {
|
|
15
|
-
model: string;
|
|
16
|
-
field: string;
|
|
17
|
-
onDelete?: 'no action' | 'restrict' | 'cascade' | 'set null' | 'set default';
|
|
18
|
-
};
|
|
19
|
-
defaultValue?: unknown;
|
|
20
|
-
returned?: boolean;
|
|
21
|
-
input?: boolean;
|
|
22
|
-
/**
|
|
23
|
-
* "If the field should be a bigint on the database instead of integer" —
|
|
24
|
-
* only meaningful alongside `type: 'number'`. Modelled here so the `number`
|
|
25
|
-
* branch below can read it; better-auth's rate limiter is the only shipped
|
|
26
|
-
* example (`lastRequest`, a millisecond epoch that overflows a 32-bit
|
|
27
|
-
* `Int`), but any plugin can declare it (issue #917).
|
|
28
|
-
*/
|
|
29
|
-
bigint?: boolean;
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Better Auth table schema structure
|
|
33
|
-
*/
|
|
34
|
-
type BetterAuthTableSchema = {
|
|
35
|
-
modelName?: string;
|
|
36
|
-
fields: Record<string, BetterAuthFieldAttribute>;
|
|
37
|
-
};
|
|
38
|
-
/**
|
|
39
|
-
* Convert Better Auth table schema to OpenSaaS ListConfig.
|
|
40
|
-
*
|
|
41
|
-
* Per ADR-0013, plugin-injected lists ship **closed** — no access is set here,
|
|
42
|
-
* so the list denies every operation until the application grants it. For a
|
|
43
|
-
* table that resolves onto one of the four base auth models (user/session/
|
|
44
|
-
* account/verification), that means `authPlugin({ access: { ... } })`; for any
|
|
45
|
-
* other better-auth-plugin-declared table (e.g. OAuth client tables from the
|
|
46
|
-
* `mcp` plugin), the application must declare the list itself under the same
|
|
47
|
-
* derived key so the plugin's field-only extend path can merge in (its access
|
|
48
|
-
* then stands untouched).
|
|
49
|
-
*/
|
|
50
|
-
export declare function convertTableToList(tableName: string, tableSchema: BetterAuthTableSchema): ListConfig<any>;
|
|
51
|
-
/**
|
|
52
|
-
* Base better-auth model keys — the tables the auth plugin's own model config
|
|
53
|
-
* (`user`/`session`/`account`/`verification`, plus `rateLimit` when
|
|
54
|
-
* `rateLimit.storage: 'database'` derives it) can remap via `modelName`.
|
|
55
|
-
*/
|
|
56
|
-
type BaseAuthModelKey = 'user' | 'session' | 'account' | 'verification' | 'rateLimit';
|
|
57
|
-
/**
|
|
58
|
-
* Resolved list key for each base better-auth model, as derived by
|
|
59
|
-
* {@link deriveAuthLists} from the plugin's configured model-key remap (e.g.
|
|
60
|
-
* `user.modelName: 'AuthUser'`). Passed to {@link convertBetterAuthSchema} so a
|
|
61
|
-
* provider plugin's schema extension of a base model (e.g. `user`) lands on the
|
|
62
|
-
* same adopted Auth list rather than being re-derived from the plugin's own
|
|
63
|
-
* table name.
|
|
64
|
-
*/
|
|
65
|
-
export type BaseAuthModelKeys = Partial<Record<BaseAuthModelKey, string>>;
|
|
66
|
-
/**
|
|
67
|
-
* Convert all Better Auth tables to OpenSaaS list configs
|
|
68
|
-
* This is called by authPlugin() to generate lists from Better Auth + plugins
|
|
69
|
-
*
|
|
70
|
-
* @param tables - The better-auth plugin's declared schema extension
|
|
71
|
-
* @param baseModelKeys - The resolved list keys for the four base auth models
|
|
72
|
-
* (`user`/`session`/`account`/`verification`), from the same model-key remap
|
|
73
|
-
* the Auth lists themselves use. When a table name matches one of these keys
|
|
74
|
-
* (case-insensitively), its list key is taken from here instead of being
|
|
75
|
-
* re-derived from the table name — so a schema extension targeting `user`
|
|
76
|
-
* lands on the adopted Auth list (e.g. `AuthUser`) rather than a re-derived
|
|
77
|
-
* `User`, even when that key collides with an unrelated host list.
|
|
78
|
-
*/
|
|
79
|
-
export declare function convertBetterAuthSchema(tables: Record<string, BetterAuthTableSchema>, baseModelKeys?: BaseAuthModelKeys): Record<string, ListConfig<any>>;
|
|
80
|
-
export {};
|
|
81
|
-
//# sourceMappingURL=schema-converter.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"schema-converter.d.ts","sourceRoot":"","sources":["../../src/server/schema-converter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAe,MAAM,sBAAsB,CAAA;AAEnE;;;GAGG;AACH,KAAK,wBAAwB,GAAG;IAC9B,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,UAAU,CAAC,EAAE;QACX,KAAK,EAAE,MAAM,CAAA;QACb,KAAK,EAAE,MAAM,CAAA;QACb,QAAQ,CAAC,EAAE,WAAW,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,aAAa,CAAA;KAC7E,CAAA;IACD,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAA;AAED;;GAEG;AACH,KAAK,qBAAqB,GAAG;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAA;CACjD,CAAA;AAqED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,qBAAqB,GAEjC,UAAU,CAAC,GAAG,CAAC,CA4BjB;AAED;;;;GAIG;AACH,KAAK,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,GAAG,WAAW,CAAA;AAErF;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAA;AA4BzE;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,EAC7C,aAAa,GAAE,iBAAsB,GAEpC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAejC"}
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convert Better Auth database schema to OpenSaaS list configs
|
|
3
|
-
* Allows dynamic list generation from Better Auth plugins
|
|
4
|
-
*/
|
|
5
|
-
import { list } from '@opensaas/stack-core';
|
|
6
|
-
import { text, timestamp, checkbox, integer, bigInt } from '@opensaas/stack-core/fields';
|
|
7
|
-
/**
|
|
8
|
-
* Convert Better Auth field type to OpenSaaS field config
|
|
9
|
-
*/
|
|
10
|
-
function convertField(fieldName, betterAuthField) {
|
|
11
|
-
const { type, required, unique, defaultValue, references, bigint } = betterAuthField;
|
|
12
|
-
// System fields are auto-generated by OpenSaaS
|
|
13
|
-
if (fieldName === 'id' || fieldName === 'createdAt' || fieldName === 'updatedAt') {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
// Handle references (relationships)
|
|
17
|
-
if (references) {
|
|
18
|
-
// Relationships are handled separately
|
|
19
|
-
// Return null here and handle in relationship pass
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
// Map Better Auth types to OpenSaaS field types
|
|
23
|
-
switch (type) {
|
|
24
|
-
case 'string':
|
|
25
|
-
return text({
|
|
26
|
-
validation: { isRequired: required },
|
|
27
|
-
isIndexed: unique ? 'unique' : undefined,
|
|
28
|
-
defaultValue: typeof defaultValue === 'string' ? defaultValue : undefined,
|
|
29
|
-
});
|
|
30
|
-
case 'number':
|
|
31
|
-
// A millisecond epoch (e.g. Date.now()) overflows a 32-bit Int, so a
|
|
32
|
-
// field declaring `bigint: true` must generate a BigInt column instead
|
|
33
|
-
// (issue #917) — matching what deriveAuthLists produces for the same
|
|
34
|
-
// upstream declaration (e.g. the rate limiter's `lastRequest`).
|
|
35
|
-
if (bigint) {
|
|
36
|
-
return bigInt({
|
|
37
|
-
validation: { isRequired: required },
|
|
38
|
-
defaultValue: typeof defaultValue === 'number' ? defaultValue : undefined,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
return integer({
|
|
42
|
-
validation: { isRequired: required },
|
|
43
|
-
defaultValue: typeof defaultValue === 'number' ? defaultValue : undefined,
|
|
44
|
-
});
|
|
45
|
-
case 'boolean':
|
|
46
|
-
return checkbox({
|
|
47
|
-
defaultValue: typeof defaultValue === 'boolean' ? defaultValue : false,
|
|
48
|
-
});
|
|
49
|
-
case 'date':
|
|
50
|
-
return timestamp({
|
|
51
|
-
defaultValue: defaultValue === 'now' ? { kind: 'now' } : undefined,
|
|
52
|
-
});
|
|
53
|
-
default:
|
|
54
|
-
// Unknown type - default to text
|
|
55
|
-
console.warn(`[stack-auth] Unknown Better Auth field type "${type}" for field "${fieldName}", defaulting to text field`);
|
|
56
|
-
return text({
|
|
57
|
-
validation: { isRequired: required },
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Convert Better Auth table schema to OpenSaaS ListConfig.
|
|
63
|
-
*
|
|
64
|
-
* Per ADR-0013, plugin-injected lists ship **closed** — no access is set here,
|
|
65
|
-
* so the list denies every operation until the application grants it. For a
|
|
66
|
-
* table that resolves onto one of the four base auth models (user/session/
|
|
67
|
-
* account/verification), that means `authPlugin({ access: { ... } })`; for any
|
|
68
|
-
* other better-auth-plugin-declared table (e.g. OAuth client tables from the
|
|
69
|
-
* `mcp` plugin), the application must declare the list itself under the same
|
|
70
|
-
* derived key so the plugin's field-only extend path can merge in (its access
|
|
71
|
-
* then stands untouched).
|
|
72
|
-
*/
|
|
73
|
-
export function convertTableToList(tableName, tableSchema) {
|
|
74
|
-
const fields = {};
|
|
75
|
-
// First pass: convert regular fields
|
|
76
|
-
for (const [fieldName, fieldAttr] of Object.entries(tableSchema.fields)) {
|
|
77
|
-
const fieldConfig = convertField(fieldName, fieldAttr);
|
|
78
|
-
if (fieldConfig) {
|
|
79
|
-
fields[fieldName] = fieldConfig;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// Second pass: add relationships
|
|
83
|
-
// NOTE: Better Auth uses one-way references, but OpenSaaS requires bidirectional relationships
|
|
84
|
-
// We skip relationship conversion for now and rely on the foreign key fields
|
|
85
|
-
// Users can manually add bidirectional relationships if needed by extending the User list
|
|
86
|
-
for (const [fieldName, fieldAttr] of Object.entries(tableSchema.fields)) {
|
|
87
|
-
if (fieldAttr.references) {
|
|
88
|
-
// For now, treat reference fields as regular text fields (foreign keys)
|
|
89
|
-
// This preserves the userId, clientId, etc. fields that Better Auth expects
|
|
90
|
-
if (!fields[fieldName]) {
|
|
91
|
-
fields[fieldName] = text({
|
|
92
|
-
validation: { isRequired: fieldAttr.required },
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return list({ fields });
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Look up the configured remap for a table name, if it names one of the four
|
|
101
|
-
* base better-auth models (case-insensitively). Written as an explicit switch
|
|
102
|
-
* rather than an indexed lookup so no type assertion is needed to narrow the
|
|
103
|
-
* table name into {@link BaseAuthModelKey}.
|
|
104
|
-
*/
|
|
105
|
-
function resolveBaseModelKey(tableName, baseModelKeys) {
|
|
106
|
-
switch (tableName.toLowerCase()) {
|
|
107
|
-
case 'user':
|
|
108
|
-
return baseModelKeys.user;
|
|
109
|
-
case 'session':
|
|
110
|
-
return baseModelKeys.session;
|
|
111
|
-
case 'account':
|
|
112
|
-
return baseModelKeys.account;
|
|
113
|
-
case 'verification':
|
|
114
|
-
return baseModelKeys.verification;
|
|
115
|
-
case 'ratelimit':
|
|
116
|
-
return baseModelKeys.rateLimit;
|
|
117
|
-
default:
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Convert all Better Auth tables to OpenSaaS list configs
|
|
123
|
-
* This is called by authPlugin() to generate lists from Better Auth + plugins
|
|
124
|
-
*
|
|
125
|
-
* @param tables - The better-auth plugin's declared schema extension
|
|
126
|
-
* @param baseModelKeys - The resolved list keys for the four base auth models
|
|
127
|
-
* (`user`/`session`/`account`/`verification`), from the same model-key remap
|
|
128
|
-
* the Auth lists themselves use. When a table name matches one of these keys
|
|
129
|
-
* (case-insensitively), its list key is taken from here instead of being
|
|
130
|
-
* re-derived from the table name — so a schema extension targeting `user`
|
|
131
|
-
* lands on the adopted Auth list (e.g. `AuthUser`) rather than a re-derived
|
|
132
|
-
* `User`, even when that key collides with an unrelated host list.
|
|
133
|
-
*/
|
|
134
|
-
export function convertBetterAuthSchema(tables, baseModelKeys = {}) {
|
|
135
|
-
const lists = {}; // eslint-disable-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
136
|
-
for (const [tableName, tableSchema] of Object.entries(tables)) {
|
|
137
|
-
// A base-model remap always wins: it reflects the actual configured list
|
|
138
|
-
// key for that model, whereas `tableSchema.modelName`/the table name are
|
|
139
|
-
// just how the provider plugin happens to describe its own extension.
|
|
140
|
-
const listKey = resolveBaseModelKey(tableName, baseModelKeys) ||
|
|
141
|
-
tableSchema.modelName ||
|
|
142
|
-
toPascalCase(tableName);
|
|
143
|
-
lists[listKey] = convertTableToList(tableName, tableSchema);
|
|
144
|
-
}
|
|
145
|
-
return lists;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Convert table name to PascalCase
|
|
149
|
-
* e.g., "oauth_application" -> "OauthApplication"
|
|
150
|
-
*/
|
|
151
|
-
function toPascalCase(str) {
|
|
152
|
-
return str
|
|
153
|
-
.split(/[_-]/)
|
|
154
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
155
|
-
.join('');
|
|
156
|
-
}
|
|
157
|
-
//# sourceMappingURL=schema-converter.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"schema-converter.js","sourceRoot":"","sources":["../../src/server/schema-converter.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convert Better Auth database schema to OpenSaaS list configs
|
|
3
|
-
* Allows dynamic list generation from Better Auth plugins
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { list } from '@opensaas/stack-core'
|
|
7
|
-
import { text, timestamp, checkbox, integer, bigInt } from '@opensaas/stack-core/fields'
|
|
8
|
-
import type { ListConfig, FieldConfig } from '@opensaas/stack-core'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Better Auth field attribute structure
|
|
12
|
-
* Inferred from better-auth internal types
|
|
13
|
-
*/
|
|
14
|
-
type BetterAuthFieldAttribute = {
|
|
15
|
-
type: string | string[] // 'string' | 'number' | 'boolean' | 'date' | etc., or an enum array
|
|
16
|
-
required?: boolean
|
|
17
|
-
unique?: boolean
|
|
18
|
-
references?: {
|
|
19
|
-
model: string
|
|
20
|
-
field: string
|
|
21
|
-
onDelete?: 'no action' | 'restrict' | 'cascade' | 'set null' | 'set default'
|
|
22
|
-
}
|
|
23
|
-
defaultValue?: unknown
|
|
24
|
-
returned?: boolean
|
|
25
|
-
input?: boolean
|
|
26
|
-
/**
|
|
27
|
-
* "If the field should be a bigint on the database instead of integer" —
|
|
28
|
-
* only meaningful alongside `type: 'number'`. Modelled here so the `number`
|
|
29
|
-
* branch below can read it; better-auth's rate limiter is the only shipped
|
|
30
|
-
* example (`lastRequest`, a millisecond epoch that overflows a 32-bit
|
|
31
|
-
* `Int`), but any plugin can declare it (issue #917).
|
|
32
|
-
*/
|
|
33
|
-
bigint?: boolean
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Better Auth table schema structure
|
|
38
|
-
*/
|
|
39
|
-
type BetterAuthTableSchema = {
|
|
40
|
-
modelName?: string
|
|
41
|
-
fields: Record<string, BetterAuthFieldAttribute>
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Convert Better Auth field type to OpenSaaS field config
|
|
46
|
-
*/
|
|
47
|
-
function convertField(
|
|
48
|
-
fieldName: string,
|
|
49
|
-
betterAuthField: BetterAuthFieldAttribute,
|
|
50
|
-
): FieldConfig | null {
|
|
51
|
-
const { type, required, unique, defaultValue, references, bigint } = betterAuthField
|
|
52
|
-
|
|
53
|
-
// System fields are auto-generated by OpenSaaS
|
|
54
|
-
if (fieldName === 'id' || fieldName === 'createdAt' || fieldName === 'updatedAt') {
|
|
55
|
-
return null
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Handle references (relationships)
|
|
59
|
-
if (references) {
|
|
60
|
-
// Relationships are handled separately
|
|
61
|
-
// Return null here and handle in relationship pass
|
|
62
|
-
return null
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Map Better Auth types to OpenSaaS field types
|
|
66
|
-
switch (type) {
|
|
67
|
-
case 'string':
|
|
68
|
-
return text({
|
|
69
|
-
validation: { isRequired: required },
|
|
70
|
-
isIndexed: unique ? 'unique' : undefined,
|
|
71
|
-
defaultValue: typeof defaultValue === 'string' ? defaultValue : undefined,
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
case 'number':
|
|
75
|
-
// A millisecond epoch (e.g. Date.now()) overflows a 32-bit Int, so a
|
|
76
|
-
// field declaring `bigint: true` must generate a BigInt column instead
|
|
77
|
-
// (issue #917) — matching what deriveAuthLists produces for the same
|
|
78
|
-
// upstream declaration (e.g. the rate limiter's `lastRequest`).
|
|
79
|
-
if (bigint) {
|
|
80
|
-
return bigInt({
|
|
81
|
-
validation: { isRequired: required },
|
|
82
|
-
defaultValue: typeof defaultValue === 'number' ? defaultValue : undefined,
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
return integer({
|
|
86
|
-
validation: { isRequired: required },
|
|
87
|
-
defaultValue: typeof defaultValue === 'number' ? defaultValue : undefined,
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
case 'boolean':
|
|
91
|
-
return checkbox({
|
|
92
|
-
defaultValue: typeof defaultValue === 'boolean' ? defaultValue : false,
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
case 'date':
|
|
96
|
-
return timestamp({
|
|
97
|
-
defaultValue: defaultValue === 'now' ? { kind: 'now' } : undefined,
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
default:
|
|
101
|
-
// Unknown type - default to text
|
|
102
|
-
console.warn(
|
|
103
|
-
`[stack-auth] Unknown Better Auth field type "${type}" for field "${fieldName}", defaulting to text field`,
|
|
104
|
-
)
|
|
105
|
-
return text({
|
|
106
|
-
validation: { isRequired: required },
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Convert Better Auth table schema to OpenSaaS ListConfig.
|
|
113
|
-
*
|
|
114
|
-
* Per ADR-0013, plugin-injected lists ship **closed** — no access is set here,
|
|
115
|
-
* so the list denies every operation until the application grants it. For a
|
|
116
|
-
* table that resolves onto one of the four base auth models (user/session/
|
|
117
|
-
* account/verification), that means `authPlugin({ access: { ... } })`; for any
|
|
118
|
-
* other better-auth-plugin-declared table (e.g. OAuth client tables from the
|
|
119
|
-
* `mcp` plugin), the application must declare the list itself under the same
|
|
120
|
-
* derived key so the plugin's field-only extend path can merge in (its access
|
|
121
|
-
* then stands untouched).
|
|
122
|
-
*/
|
|
123
|
-
export function convertTableToList(
|
|
124
|
-
tableName: string,
|
|
125
|
-
tableSchema: BetterAuthTableSchema,
|
|
126
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
127
|
-
): ListConfig<any> {
|
|
128
|
-
const fields: Record<string, FieldConfig> = {}
|
|
129
|
-
|
|
130
|
-
// First pass: convert regular fields
|
|
131
|
-
for (const [fieldName, fieldAttr] of Object.entries(tableSchema.fields)) {
|
|
132
|
-
const fieldConfig = convertField(fieldName, fieldAttr)
|
|
133
|
-
if (fieldConfig) {
|
|
134
|
-
fields[fieldName] = fieldConfig
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Second pass: add relationships
|
|
139
|
-
// NOTE: Better Auth uses one-way references, but OpenSaaS requires bidirectional relationships
|
|
140
|
-
// We skip relationship conversion for now and rely on the foreign key fields
|
|
141
|
-
// Users can manually add bidirectional relationships if needed by extending the User list
|
|
142
|
-
for (const [fieldName, fieldAttr] of Object.entries(tableSchema.fields)) {
|
|
143
|
-
if (fieldAttr.references) {
|
|
144
|
-
// For now, treat reference fields as regular text fields (foreign keys)
|
|
145
|
-
// This preserves the userId, clientId, etc. fields that Better Auth expects
|
|
146
|
-
if (!fields[fieldName]) {
|
|
147
|
-
fields[fieldName] = text({
|
|
148
|
-
validation: { isRequired: fieldAttr.required },
|
|
149
|
-
})
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return list({ fields })
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Base better-auth model keys — the tables the auth plugin's own model config
|
|
159
|
-
* (`user`/`session`/`account`/`verification`, plus `rateLimit` when
|
|
160
|
-
* `rateLimit.storage: 'database'` derives it) can remap via `modelName`.
|
|
161
|
-
*/
|
|
162
|
-
type BaseAuthModelKey = 'user' | 'session' | 'account' | 'verification' | 'rateLimit'
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Resolved list key for each base better-auth model, as derived by
|
|
166
|
-
* {@link deriveAuthLists} from the plugin's configured model-key remap (e.g.
|
|
167
|
-
* `user.modelName: 'AuthUser'`). Passed to {@link convertBetterAuthSchema} so a
|
|
168
|
-
* provider plugin's schema extension of a base model (e.g. `user`) lands on the
|
|
169
|
-
* same adopted Auth list rather than being re-derived from the plugin's own
|
|
170
|
-
* table name.
|
|
171
|
-
*/
|
|
172
|
-
export type BaseAuthModelKeys = Partial<Record<BaseAuthModelKey, string>>
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Look up the configured remap for a table name, if it names one of the four
|
|
176
|
-
* base better-auth models (case-insensitively). Written as an explicit switch
|
|
177
|
-
* rather than an indexed lookup so no type assertion is needed to narrow the
|
|
178
|
-
* table name into {@link BaseAuthModelKey}.
|
|
179
|
-
*/
|
|
180
|
-
function resolveBaseModelKey(
|
|
181
|
-
tableName: string,
|
|
182
|
-
baseModelKeys: BaseAuthModelKeys,
|
|
183
|
-
): string | undefined {
|
|
184
|
-
switch (tableName.toLowerCase()) {
|
|
185
|
-
case 'user':
|
|
186
|
-
return baseModelKeys.user
|
|
187
|
-
case 'session':
|
|
188
|
-
return baseModelKeys.session
|
|
189
|
-
case 'account':
|
|
190
|
-
return baseModelKeys.account
|
|
191
|
-
case 'verification':
|
|
192
|
-
return baseModelKeys.verification
|
|
193
|
-
case 'ratelimit':
|
|
194
|
-
return baseModelKeys.rateLimit
|
|
195
|
-
default:
|
|
196
|
-
return undefined
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Convert all Better Auth tables to OpenSaaS list configs
|
|
202
|
-
* This is called by authPlugin() to generate lists from Better Auth + plugins
|
|
203
|
-
*
|
|
204
|
-
* @param tables - The better-auth plugin's declared schema extension
|
|
205
|
-
* @param baseModelKeys - The resolved list keys for the four base auth models
|
|
206
|
-
* (`user`/`session`/`account`/`verification`), from the same model-key remap
|
|
207
|
-
* the Auth lists themselves use. When a table name matches one of these keys
|
|
208
|
-
* (case-insensitively), its list key is taken from here instead of being
|
|
209
|
-
* re-derived from the table name — so a schema extension targeting `user`
|
|
210
|
-
* lands on the adopted Auth list (e.g. `AuthUser`) rather than a re-derived
|
|
211
|
-
* `User`, even when that key collides with an unrelated host list.
|
|
212
|
-
*/
|
|
213
|
-
export function convertBetterAuthSchema(
|
|
214
|
-
tables: Record<string, BetterAuthTableSchema>,
|
|
215
|
-
baseModelKeys: BaseAuthModelKeys = {},
|
|
216
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
217
|
-
): Record<string, ListConfig<any>> {
|
|
218
|
-
const lists: Record<string, ListConfig<any>> = {} // eslint-disable-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
219
|
-
|
|
220
|
-
for (const [tableName, tableSchema] of Object.entries(tables)) {
|
|
221
|
-
// A base-model remap always wins: it reflects the actual configured list
|
|
222
|
-
// key for that model, whereas `tableSchema.modelName`/the table name are
|
|
223
|
-
// just how the provider plugin happens to describe its own extension.
|
|
224
|
-
const listKey =
|
|
225
|
-
resolveBaseModelKey(tableName, baseModelKeys) ||
|
|
226
|
-
tableSchema.modelName ||
|
|
227
|
-
toPascalCase(tableName)
|
|
228
|
-
lists[listKey] = convertTableToList(tableName, tableSchema)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return lists
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Convert table name to PascalCase
|
|
236
|
-
* e.g., "oauth_application" -> "OauthApplication"
|
|
237
|
-
*/
|
|
238
|
-
function toPascalCase(str: string): string {
|
|
239
|
-
return str
|
|
240
|
-
.split(/[_-]/)
|
|
241
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
242
|
-
.join('')
|
|
243
|
-
}
|