@opensaas/stack-auth 0.39.2 → 0.41.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.
Files changed (104) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +153 -0
  3. package/CLAUDE.md +140 -22
  4. package/dist/client/index.d.ts +0 -16
  5. package/dist/client/index.d.ts.map +1 -1
  6. package/dist/config/adopt-better-auth-tables.d.ts +4 -49
  7. package/dist/config/adopt-better-auth-tables.d.ts.map +1 -1
  8. package/dist/config/adopt-better-auth-tables.js +4 -51
  9. package/dist/config/adopt-better-auth-tables.js.map +1 -1
  10. package/dist/config/derive-auth-lists.d.ts +42 -37
  11. package/dist/config/derive-auth-lists.d.ts.map +1 -1
  12. package/dist/config/derive-auth-lists.js +579 -223
  13. package/dist/config/derive-auth-lists.js.map +1 -1
  14. package/dist/config/index.d.ts +0 -3
  15. package/dist/config/index.d.ts.map +1 -1
  16. package/dist/config/index.js +4 -34
  17. package/dist/config/index.js.map +1 -1
  18. package/dist/config/plugin.d.ts.map +1 -1
  19. package/dist/config/plugin.js +20 -90
  20. package/dist/config/plugin.js.map +1 -1
  21. package/dist/config/types.d.ts +89 -67
  22. package/dist/config/types.d.ts.map +1 -1
  23. package/dist/index.d.ts +1 -9
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -13
  26. package/dist/index.js.map +1 -1
  27. package/dist/lists/index.d.ts +9 -5
  28. package/dist/lists/index.d.ts.map +1 -1
  29. package/dist/lists/index.js +9 -6
  30. package/dist/lists/index.js.map +1 -1
  31. package/dist/mcp/better-auth.d.ts +6 -19
  32. package/dist/mcp/better-auth.d.ts.map +1 -1
  33. package/dist/mcp/better-auth.js +6 -20
  34. package/dist/mcp/better-auth.js.map +1 -1
  35. package/dist/plugins/index.d.ts +4 -4
  36. package/dist/plugins/index.d.ts.map +1 -1
  37. package/dist/plugins/index.js +4 -4
  38. package/dist/plugins/index.js.map +1 -1
  39. package/dist/runtime/types.d.ts +2 -9
  40. package/dist/runtime/types.d.ts.map +1 -1
  41. package/dist/runtime/types.js +0 -4
  42. package/dist/runtime/types.js.map +1 -1
  43. package/dist/server/index.d.ts +5 -12
  44. package/dist/server/index.d.ts.map +1 -1
  45. package/dist/server/index.js +14 -62
  46. package/dist/server/index.js.map +1 -1
  47. package/dist/ui/components/ForgotPasswordForm.d.ts +0 -3
  48. package/dist/ui/components/ForgotPasswordForm.d.ts.map +1 -1
  49. package/dist/ui/components/ForgotPasswordForm.js +0 -3
  50. package/dist/ui/components/ForgotPasswordForm.js.map +1 -1
  51. package/dist/ui/components/ResetPasswordForm.d.ts +0 -3
  52. package/dist/ui/components/ResetPasswordForm.d.ts.map +1 -1
  53. package/dist/ui/components/ResetPasswordForm.js +0 -5
  54. package/dist/ui/components/ResetPasswordForm.js.map +1 -1
  55. package/dist/ui/components/SignInForm.d.ts +0 -3
  56. package/dist/ui/components/SignInForm.d.ts.map +1 -1
  57. package/dist/ui/components/SignInForm.js +0 -4
  58. package/dist/ui/components/SignInForm.js.map +1 -1
  59. package/dist/ui/components/SignUpForm.d.ts +0 -3
  60. package/dist/ui/components/SignUpForm.d.ts.map +1 -1
  61. package/dist/ui/components/SignUpForm.js +0 -5
  62. package/dist/ui/components/SignUpForm.js.map +1 -1
  63. package/dist/ui/lib/clean-error-message.d.ts +3 -6
  64. package/dist/ui/lib/clean-error-message.d.ts.map +1 -1
  65. package/dist/ui/lib/clean-error-message.js +3 -6
  66. package/dist/ui/lib/clean-error-message.js.map +1 -1
  67. package/dist/ui/types.d.ts +2 -15
  68. package/dist/ui/types.d.ts.map +1 -1
  69. package/dist/ui/types.js +2 -7
  70. package/dist/ui/types.js.map +1 -1
  71. package/package.json +12 -5
  72. package/src/client/index.ts +0 -17
  73. package/src/config/adopt-better-auth-tables.ts +4 -51
  74. package/src/config/derive-auth-lists.ts +685 -252
  75. package/src/config/index.ts +4 -34
  76. package/src/config/plugin.ts +21 -91
  77. package/src/config/types.ts +90 -67
  78. package/src/index.ts +3 -14
  79. package/src/lists/index.ts +17 -5
  80. package/src/mcp/better-auth.ts +6 -25
  81. package/src/plugins/index.ts +4 -5
  82. package/src/runtime/types.ts +2 -10
  83. package/src/server/index.ts +17 -70
  84. package/src/ui/components/ForgotPasswordForm.tsx +0 -3
  85. package/src/ui/components/ResetPasswordForm.tsx +0 -5
  86. package/src/ui/components/SignInForm.tsx +0 -4
  87. package/src/ui/components/SignUpForm.tsx +0 -5
  88. package/src/ui/lib/clean-error-message.ts +3 -6
  89. package/src/ui/types.ts +2 -15
  90. package/tests/auth-lists-drift.test.ts +480 -0
  91. package/tests/config.test.ts +3 -0
  92. package/tests/credential-field-read-deny-e2e.test.ts +295 -0
  93. package/tests/derive-auth-lists.test.ts +263 -6
  94. package/tests/generated-fk-shape.test.ts +432 -9
  95. package/tests/mcp-oauth-cascade-e2e.test.ts +267 -0
  96. package/tests/plugin-derived-keys.test.ts +39 -0
  97. package/tests/plugin-table-derivation.test.ts +270 -0
  98. package/tsconfig.tsbuildinfo +1 -1
  99. package/dist/server/schema-converter.d.ts +0 -81
  100. package/dist/server/schema-converter.d.ts.map +0 -1
  101. package/dist/server/schema-converter.js +0 -157
  102. package/dist/server/schema-converter.js.map +0 -1
  103. package/src/server/schema-converter.ts +0 -243
  104. 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
- }