@vobase/core 0.10.0 → 0.12.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 (188) hide show
  1. package/package.json +7 -9
  2. package/src/__tests__/drizzle-introspection.test.ts +77 -0
  3. package/src/__tests__/e2e.test.ts +225 -0
  4. package/src/__tests__/permissions.test.ts +157 -0
  5. package/src/__tests__/rpc-types.test.ts +92 -0
  6. package/src/app.test.ts +99 -0
  7. package/src/app.ts +178 -0
  8. package/src/audit.test.ts +126 -0
  9. package/src/auth.test.ts +74 -0
  10. package/src/contracts/auth.ts +37 -0
  11. package/{dist/contracts/module.d.ts → src/contracts/module.ts} +6 -6
  12. package/src/contracts/notify.ts +47 -0
  13. package/src/contracts/permissions.ts +10 -0
  14. package/src/contracts/storage.ts +61 -0
  15. package/src/ctx.test.ts +162 -0
  16. package/src/ctx.ts +64 -0
  17. package/src/db/client.test.ts +75 -0
  18. package/src/db/client.ts +15 -0
  19. package/src/db/helpers.test.ts +147 -0
  20. package/src/db/helpers.ts +51 -0
  21. package/src/db/index.ts +8 -0
  22. package/{dist/index.d.ts → src/index.ts} +103 -6
  23. package/src/infra/circuit-breaker.test.ts +74 -0
  24. package/src/infra/circuit-breaker.ts +57 -0
  25. package/src/infra/errors.test.ts +175 -0
  26. package/src/infra/errors.ts +64 -0
  27. package/src/infra/http-client.test.ts +482 -0
  28. package/src/infra/http-client.ts +221 -0
  29. package/src/infra/index.ts +35 -0
  30. package/src/infra/job.test.ts +85 -0
  31. package/src/infra/job.ts +94 -0
  32. package/src/infra/logger.test.ts +65 -0
  33. package/src/infra/logger.ts +18 -0
  34. package/src/infra/queue.test.ts +46 -0
  35. package/src/infra/queue.ts +147 -0
  36. package/src/infra/throw-proxy.test.ts +34 -0
  37. package/src/infra/throw-proxy.ts +17 -0
  38. package/src/infra/webhooks-schema.ts +17 -0
  39. package/src/infra/webhooks.test.ts +364 -0
  40. package/src/infra/webhooks.ts +146 -0
  41. package/src/mcp/auth.test.ts +129 -0
  42. package/src/mcp/crud.test.ts +128 -0
  43. package/src/mcp/crud.ts +171 -0
  44. package/{dist/mcp/index.d.ts → src/mcp/index.ts} +0 -1
  45. package/src/mcp/server.test.ts +153 -0
  46. package/src/mcp/server.ts +178 -0
  47. package/src/middleware/audit.test.ts +169 -0
  48. package/src/module-registry.ts +18 -0
  49. package/src/module.test.ts +168 -0
  50. package/src/module.ts +111 -0
  51. package/src/modules/audit/index.ts +18 -0
  52. package/src/modules/audit/middleware.ts +33 -0
  53. package/src/modules/audit/schema.ts +35 -0
  54. package/src/modules/audit/track-changes.ts +70 -0
  55. package/src/modules/auth/audit-hooks.ts +74 -0
  56. package/src/modules/auth/index.ts +101 -0
  57. package/src/modules/auth/middleware.ts +51 -0
  58. package/src/modules/auth/permissions.ts +46 -0
  59. package/src/modules/auth/schema.ts +184 -0
  60. package/src/modules/credentials/encrypt.ts +95 -0
  61. package/src/modules/credentials/index.ts +15 -0
  62. package/src/modules/credentials/schema.ts +10 -0
  63. package/src/modules/notify/index.ts +90 -0
  64. package/src/modules/notify/notify.test.ts +145 -0
  65. package/src/modules/notify/providers/resend.ts +47 -0
  66. package/src/modules/notify/providers/smtp.ts +117 -0
  67. package/src/modules/notify/providers/waba.ts +82 -0
  68. package/src/modules/notify/schema.ts +27 -0
  69. package/src/modules/notify/service.ts +93 -0
  70. package/src/modules/sequences/index.ts +15 -0
  71. package/src/modules/sequences/next-sequence.ts +48 -0
  72. package/src/modules/sequences/schema.ts +12 -0
  73. package/src/modules/storage/index.ts +44 -0
  74. package/src/modules/storage/providers/local.ts +124 -0
  75. package/src/modules/storage/providers/s3.ts +83 -0
  76. package/src/modules/storage/routes.ts +76 -0
  77. package/src/modules/storage/schema.ts +26 -0
  78. package/src/modules/storage/service.ts +202 -0
  79. package/src/modules/storage/storage.test.ts +225 -0
  80. package/src/schemas.test.ts +44 -0
  81. package/src/schemas.ts +63 -0
  82. package/src/sequence.test.ts +56 -0
  83. package/dist/app.d.ts +0 -37
  84. package/dist/app.d.ts.map +0 -1
  85. package/dist/contracts/auth.d.ts +0 -35
  86. package/dist/contracts/auth.d.ts.map +0 -1
  87. package/dist/contracts/module.d.ts.map +0 -1
  88. package/dist/contracts/notify.d.ts +0 -46
  89. package/dist/contracts/notify.d.ts.map +0 -1
  90. package/dist/contracts/permissions.d.ts +0 -10
  91. package/dist/contracts/permissions.d.ts.map +0 -1
  92. package/dist/contracts/storage.d.ts +0 -54
  93. package/dist/contracts/storage.d.ts.map +0 -1
  94. package/dist/ctx.d.ts +0 -40
  95. package/dist/ctx.d.ts.map +0 -1
  96. package/dist/db/client.d.ts +0 -4
  97. package/dist/db/client.d.ts.map +0 -1
  98. package/dist/db/helpers.d.ts +0 -26
  99. package/dist/db/helpers.d.ts.map +0 -1
  100. package/dist/db/index.d.ts +0 -3
  101. package/dist/db/index.d.ts.map +0 -1
  102. package/dist/index.d.ts.map +0 -1
  103. package/dist/index.js +0 -98611
  104. package/dist/infra/circuit-breaker.d.ts +0 -17
  105. package/dist/infra/circuit-breaker.d.ts.map +0 -1
  106. package/dist/infra/errors.d.ts +0 -26
  107. package/dist/infra/errors.d.ts.map +0 -1
  108. package/dist/infra/http-client.d.ts +0 -31
  109. package/dist/infra/http-client.d.ts.map +0 -1
  110. package/dist/infra/index.d.ts +0 -11
  111. package/dist/infra/index.d.ts.map +0 -1
  112. package/dist/infra/job.d.ts +0 -14
  113. package/dist/infra/job.d.ts.map +0 -1
  114. package/dist/infra/logger.d.ts +0 -7
  115. package/dist/infra/logger.d.ts.map +0 -1
  116. package/dist/infra/queue.d.ts +0 -18
  117. package/dist/infra/queue.d.ts.map +0 -1
  118. package/dist/infra/throw-proxy.d.ts +0 -7
  119. package/dist/infra/throw-proxy.d.ts.map +0 -1
  120. package/dist/infra/webhooks-schema.d.ts +0 -60
  121. package/dist/infra/webhooks-schema.d.ts.map +0 -1
  122. package/dist/infra/webhooks.d.ts +0 -46
  123. package/dist/infra/webhooks.d.ts.map +0 -1
  124. package/dist/mcp/crud.d.ts +0 -12
  125. package/dist/mcp/crud.d.ts.map +0 -1
  126. package/dist/mcp/index.d.ts.map +0 -1
  127. package/dist/mcp/server.d.ts +0 -16
  128. package/dist/mcp/server.d.ts.map +0 -1
  129. package/dist/module-registry.d.ts +0 -3
  130. package/dist/module-registry.d.ts.map +0 -1
  131. package/dist/module.d.ts +0 -29
  132. package/dist/module.d.ts.map +0 -1
  133. package/dist/modules/audit/index.d.ts +0 -5
  134. package/dist/modules/audit/index.d.ts.map +0 -1
  135. package/dist/modules/audit/middleware.d.ts +0 -3
  136. package/dist/modules/audit/middleware.d.ts.map +0 -1
  137. package/dist/modules/audit/schema.d.ts +0 -247
  138. package/dist/modules/audit/schema.d.ts.map +0 -1
  139. package/dist/modules/audit/track-changes.d.ts +0 -3
  140. package/dist/modules/audit/track-changes.d.ts.map +0 -1
  141. package/dist/modules/auth/audit-hooks.d.ts +0 -6
  142. package/dist/modules/auth/audit-hooks.d.ts.map +0 -1
  143. package/dist/modules/auth/index.d.ts +0 -25
  144. package/dist/modules/auth/index.d.ts.map +0 -1
  145. package/dist/modules/auth/middleware.d.ts +0 -15
  146. package/dist/modules/auth/middleware.d.ts.map +0 -1
  147. package/dist/modules/auth/permissions.d.ts +0 -5
  148. package/dist/modules/auth/permissions.d.ts.map +0 -1
  149. package/dist/modules/auth/schema.d.ts +0 -2519
  150. package/dist/modules/auth/schema.d.ts.map +0 -1
  151. package/dist/modules/credentials/encrypt.d.ts +0 -12
  152. package/dist/modules/credentials/encrypt.d.ts.map +0 -1
  153. package/dist/modules/credentials/index.d.ts +0 -4
  154. package/dist/modules/credentials/index.d.ts.map +0 -1
  155. package/dist/modules/credentials/schema.d.ts +0 -56
  156. package/dist/modules/credentials/schema.d.ts.map +0 -1
  157. package/dist/modules/notify/index.d.ts +0 -36
  158. package/dist/modules/notify/index.d.ts.map +0 -1
  159. package/dist/modules/notify/providers/resend.d.ts +0 -7
  160. package/dist/modules/notify/providers/resend.d.ts.map +0 -1
  161. package/dist/modules/notify/providers/smtp.d.ts +0 -18
  162. package/dist/modules/notify/providers/smtp.d.ts.map +0 -1
  163. package/dist/modules/notify/providers/waba.d.ts +0 -12
  164. package/dist/modules/notify/providers/waba.d.ts.map +0 -1
  165. package/dist/modules/notify/schema.d.ts +0 -337
  166. package/dist/modules/notify/schema.d.ts.map +0 -1
  167. package/dist/modules/notify/service.d.ts +0 -22
  168. package/dist/modules/notify/service.d.ts.map +0 -1
  169. package/dist/modules/sequences/index.d.ts +0 -4
  170. package/dist/modules/sequences/index.d.ts.map +0 -1
  171. package/dist/modules/sequences/next-sequence.d.ts +0 -8
  172. package/dist/modules/sequences/next-sequence.d.ts.map +0 -1
  173. package/dist/modules/sequences/schema.d.ts +0 -72
  174. package/dist/modules/sequences/schema.d.ts.map +0 -1
  175. package/dist/modules/storage/index.d.ts +0 -24
  176. package/dist/modules/storage/index.d.ts.map +0 -1
  177. package/dist/modules/storage/providers/local.d.ts +0 -3
  178. package/dist/modules/storage/providers/local.d.ts.map +0 -1
  179. package/dist/modules/storage/providers/s3.d.ts +0 -3
  180. package/dist/modules/storage/providers/s3.d.ts.map +0 -1
  181. package/dist/modules/storage/routes.d.ts +0 -4
  182. package/dist/modules/storage/routes.d.ts.map +0 -1
  183. package/dist/modules/storage/schema.d.ts +0 -273
  184. package/dist/modules/storage/schema.d.ts.map +0 -1
  185. package/dist/modules/storage/service.d.ts +0 -35
  186. package/dist/modules/storage/service.d.ts.map +0 -1
  187. package/dist/schemas.d.ts +0 -19
  188. package/dist/schemas.d.ts.map +0 -1
@@ -0,0 +1,101 @@
1
+ import { Hono } from 'hono';
2
+ import { betterAuth } from 'better-auth';
3
+ import type { SocialProviders } from 'better-auth/social-providers';
4
+ import { drizzleAdapter } from 'better-auth/adapters/drizzle';
5
+ import { apiKey } from '@better-auth/api-key';
6
+ import { organization } from 'better-auth/plugins';
7
+
8
+ import type { AuthAdapter } from '../../contracts/auth';
9
+ import type { VobaseDb } from '../../db/client';
10
+ import { defineBuiltinModule } from '../../module';
11
+ import type { VobaseModule } from '../../module';
12
+ import { authSchema, apikeySchema, organizationSchema } from './schema';
13
+ import { createAuthAuditHooks } from './audit-hooks';
14
+ import { setOrganizationEnabled } from './permissions';
15
+
16
+ export interface AuthModuleConfig {
17
+ baseURL?: string;
18
+ trustedOrigins?: string[];
19
+ socialProviders?: SocialProviders;
20
+ /** Enable the organization plugin for multi-tenant support. Default: false */
21
+ organization?: boolean;
22
+ }
23
+
24
+ export type AuthModule = VobaseModule & {
25
+ adapter: AuthAdapter;
26
+ /** Validate an API key and return the owning user. Returns null if invalid. */
27
+ verifyApiKey: (key: string) => Promise<{ userId: string } | null>;
28
+ /** Whether the organization plugin is enabled */
29
+ organizationEnabled: boolean;
30
+ };
31
+
32
+ export function createAuthModule(db: VobaseDb, config?: AuthModuleConfig): AuthModule {
33
+ const baseURL = config?.baseURL ?? process.env.BETTER_AUTH_URL;
34
+ const orgEnabled = config?.organization ?? false;
35
+
36
+ // Tell the permission middleware whether org is enabled
37
+ setOrganizationEnabled(orgEnabled);
38
+
39
+ // Build plugin list
40
+ const plugins: any[] = [apiKey()];
41
+ if (orgEnabled) {
42
+ plugins.push(organization());
43
+ }
44
+
45
+ // Build schema for the adapter — always includes apikey, conditionally includes org
46
+ const adapterSchema = {
47
+ ...authSchema,
48
+ ...apikeySchema,
49
+ ...(orgEnabled ? organizationSchema : {}),
50
+ };
51
+
52
+ const auth = betterAuth({
53
+ database: drizzleAdapter(db, { provider: 'sqlite', schema: adapterSchema }),
54
+ ...(baseURL && { baseURL }),
55
+ emailAndPassword: {
56
+ enabled: true,
57
+ },
58
+ ...(config?.socialProviders && { socialProviders: config.socialProviders }),
59
+ user: {
60
+ additionalFields: {
61
+ role: {
62
+ type: 'string',
63
+ defaultValue: 'user',
64
+ input: false,
65
+ },
66
+ },
67
+ },
68
+ plugins,
69
+ hooks: createAuthAuditHooks(db),
70
+ ...(config?.trustedOrigins && { trustedOrigins: config.trustedOrigins }),
71
+ });
72
+
73
+ const adapter: AuthAdapter = {
74
+ getSession: (headers) => auth.api.getSession({ headers }),
75
+ handler: (request) => auth.handler(request),
76
+ };
77
+
78
+ const verifyApiKey = async (key: string): Promise<{ userId: string } | null> => {
79
+ try {
80
+ const result = await (auth.api as any).verifyApiKey({ body: { key } });
81
+ if (result && result.valid && result.key?.userId) {
82
+ return { userId: result.key.userId };
83
+ }
84
+ return null;
85
+ } catch {
86
+ return null;
87
+ }
88
+ };
89
+
90
+ const mod = defineBuiltinModule({
91
+ name: '_auth',
92
+ schema: adapterSchema,
93
+ routes: new Hono(),
94
+ });
95
+
96
+ return { ...mod, adapter, verifyApiKey, organizationEnabled: orgEnabled };
97
+ }
98
+
99
+ export { authSchema } from './schema';
100
+ export { sessionMiddleware, optionalSessionMiddleware } from './middleware';
101
+ export { createAuthAuditHooks } from './audit-hooks';
@@ -0,0 +1,51 @@
1
+ import { createMiddleware } from 'hono/factory';
2
+
3
+ import type { AuthAdapter } from '../../contracts/auth';
4
+ import { unauthorized } from '../../infra/errors';
5
+
6
+ declare module 'hono' {
7
+ interface ContextVariableMap {
8
+ user: { id: string; email: string; name: string; role: string; activeOrganizationId?: string } | null;
9
+ }
10
+ }
11
+
12
+ export function sessionMiddleware(adapter: AuthAdapter) {
13
+ return createMiddleware(async (c, next) => {
14
+ const session = await adapter.getSession(c.req.raw.headers);
15
+
16
+ if (!session) {
17
+ throw unauthorized();
18
+ }
19
+
20
+ c.set('user', {
21
+ id: session.user.id,
22
+ email: session.user.email,
23
+ name: session.user.name,
24
+ role: session.user.role ?? 'user',
25
+ activeOrganizationId: session.user.activeOrganizationId,
26
+ });
27
+
28
+ await next();
29
+ });
30
+ }
31
+
32
+ export function optionalSessionMiddleware(adapter: AuthAdapter) {
33
+ return createMiddleware(async (c, next) => {
34
+ const session = await adapter.getSession(c.req.raw.headers);
35
+
36
+ c.set(
37
+ 'user',
38
+ session
39
+ ? {
40
+ id: session.user.id,
41
+ email: session.user.email,
42
+ name: session.user.name,
43
+ role: session.user.role ?? 'user',
44
+ activeOrganizationId: session.user.activeOrganizationId,
45
+ }
46
+ : null,
47
+ );
48
+
49
+ await next();
50
+ });
51
+ }
@@ -0,0 +1,46 @@
1
+ import { createMiddleware } from 'hono/factory';
2
+ import { forbidden } from '../../infra/errors';
3
+
4
+ let _organizationEnabled = false;
5
+
6
+ export function setOrganizationEnabled(enabled: boolean) {
7
+ _organizationEnabled = enabled;
8
+ }
9
+
10
+ export function requireRole(...roles: string[]) {
11
+ return createMiddleware(async (c, next) => {
12
+ const user = c.get('user');
13
+ if (!user || !roles.includes(user.role)) {
14
+ throw forbidden('Insufficient role');
15
+ }
16
+ await next();
17
+ });
18
+ }
19
+
20
+ export function requirePermission(..._permissions: string[]) {
21
+ if (!_organizationEnabled) {
22
+ throw new Error(
23
+ 'Organization plugin required for permission-based auth. Use requireRole() instead or enable organization in config.',
24
+ );
25
+ }
26
+ return createMiddleware(async (c, next) => {
27
+ const user = c.get('user');
28
+ if (!user) throw forbidden('Authentication required');
29
+ await next();
30
+ });
31
+ }
32
+
33
+ export function requireOrg() {
34
+ if (!_organizationEnabled) {
35
+ throw new Error(
36
+ 'Organization plugin required. Enable organization in config.',
37
+ );
38
+ }
39
+ return createMiddleware(async (c, next) => {
40
+ const user = c.get('user');
41
+ if (!user?.activeOrganizationId) {
42
+ throw forbidden('Active organization required');
43
+ }
44
+ await next();
45
+ });
46
+ }
@@ -0,0 +1,184 @@
1
+ import { index, integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2
+
3
+ export const authUser = sqliteTable('user', {
4
+ id: text('id').primaryKey(),
5
+ name: text('name').notNull(),
6
+ email: text('email').notNull().unique(),
7
+ emailVerified: integer('email_verified', { mode: 'boolean' })
8
+ .notNull()
9
+ .default(false),
10
+ image: text('image'),
11
+ role: text('role').notNull().default('user'),
12
+ createdAt: integer('created_at', { mode: 'timestamp_ms' })
13
+ .notNull()
14
+ .$defaultFn(() => new Date()),
15
+ updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
16
+ .notNull()
17
+ .$defaultFn(() => new Date())
18
+ .$onUpdate(() => new Date()),
19
+ });
20
+
21
+ export const authSession = sqliteTable(
22
+ 'session',
23
+ {
24
+ id: text('id').primaryKey(),
25
+ expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
26
+ token: text('token').notNull().unique(),
27
+ createdAt: integer('created_at', { mode: 'timestamp_ms' })
28
+ .notNull()
29
+ .$defaultFn(() => new Date()),
30
+ updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
31
+ .notNull()
32
+ .$defaultFn(() => new Date())
33
+ .$onUpdate(() => new Date()),
34
+ ipAddress: text('ip_address'),
35
+ userAgent: text('user_agent'),
36
+ userId: text('user_id')
37
+ .notNull()
38
+ .references(() => authUser.id, { onDelete: 'cascade' }),
39
+ },
40
+ (table) => [index('session_user_id_idx').on(table.userId)],
41
+ );
42
+
43
+ export const authAccount = sqliteTable(
44
+ 'account',
45
+ {
46
+ id: text('id').primaryKey(),
47
+ accountId: text('account_id').notNull(),
48
+ providerId: text('provider_id').notNull(),
49
+ userId: text('user_id')
50
+ .notNull()
51
+ .references(() => authUser.id, { onDelete: 'cascade' }),
52
+ accessToken: text('access_token'),
53
+ refreshToken: text('refresh_token'),
54
+ idToken: text('id_token'),
55
+ accessTokenExpiresAt: integer('access_token_expires_at', {
56
+ mode: 'timestamp_ms',
57
+ }),
58
+ refreshTokenExpiresAt: integer('refresh_token_expires_at', {
59
+ mode: 'timestamp_ms',
60
+ }),
61
+ scope: text('scope'),
62
+ password: text('password'),
63
+ createdAt: integer('created_at', { mode: 'timestamp_ms' })
64
+ .notNull()
65
+ .$defaultFn(() => new Date()),
66
+ updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
67
+ .notNull()
68
+ .$defaultFn(() => new Date())
69
+ .$onUpdate(() => new Date()),
70
+ },
71
+ (table) => [index('account_user_id_idx').on(table.userId)],
72
+ );
73
+
74
+ export const authVerification = sqliteTable(
75
+ 'verification',
76
+ {
77
+ id: text('id').primaryKey(),
78
+ identifier: text('identifier').notNull(),
79
+ value: text('value').notNull(),
80
+ expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
81
+ createdAt: integer('created_at', { mode: 'timestamp_ms' })
82
+ .notNull()
83
+ .$defaultFn(() => new Date()),
84
+ updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
85
+ .notNull()
86
+ .$defaultFn(() => new Date())
87
+ .$onUpdate(() => new Date()),
88
+ },
89
+ (table) => [index('verification_identifier_idx').on(table.identifier)],
90
+ );
91
+
92
+ // API Key table (better-auth apiKey plugin)
93
+ export const authApikey = sqliteTable('apikey', {
94
+ id: text('id').primaryKey(),
95
+ name: text('name'),
96
+ start: text('start'),
97
+ prefix: text('prefix'),
98
+ key: text('key').notNull(),
99
+ userId: text('user_id')
100
+ .notNull()
101
+ .references(() => authUser.id, { onDelete: 'cascade' }),
102
+ refillInterval: text('refill_interval'),
103
+ refillAmount: integer('refill_amount'),
104
+ lastRefillAt: integer('last_refill_at', { mode: 'timestamp_ms' }),
105
+ enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
106
+ rateLimitEnabled: integer('rate_limit_enabled', { mode: 'boolean' })
107
+ .notNull()
108
+ .default(false),
109
+ rateLimitTimeWindow: integer('rate_limit_time_window'),
110
+ rateLimitMax: integer('rate_limit_max'),
111
+ requestCount: integer('request_count').notNull().default(0),
112
+ remaining: integer('remaining'),
113
+ lastRequest: integer('last_request', { mode: 'timestamp_ms' }),
114
+ expiresAt: integer('expires_at', { mode: 'timestamp_ms' }),
115
+ createdAt: integer('created_at', { mode: 'timestamp_ms' })
116
+ .notNull()
117
+ .$defaultFn(() => new Date()),
118
+ updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
119
+ .notNull()
120
+ .$defaultFn(() => new Date())
121
+ .$onUpdate(() => new Date()),
122
+ permissions: text('permissions'),
123
+ metadata: text('metadata'),
124
+ });
125
+
126
+ // Organization tables (better-auth organization plugin)
127
+ export const authOrganization = sqliteTable('organization', {
128
+ id: text('id').primaryKey(),
129
+ name: text('name').notNull(),
130
+ slug: text('slug').notNull().unique(),
131
+ logo: text('logo'),
132
+ metadata: text('metadata'),
133
+ createdAt: integer('created_at', { mode: 'timestamp_ms' })
134
+ .notNull()
135
+ .$defaultFn(() => new Date()),
136
+ });
137
+
138
+ export const authMember = sqliteTable('member', {
139
+ id: text('id').primaryKey(),
140
+ userId: text('user_id')
141
+ .notNull()
142
+ .references(() => authUser.id, { onDelete: 'cascade' }),
143
+ organizationId: text('organization_id')
144
+ .notNull()
145
+ .references(() => authOrganization.id, { onDelete: 'cascade' }),
146
+ role: text('role').notNull().default('member'),
147
+ createdAt: integer('created_at', { mode: 'timestamp_ms' })
148
+ .notNull()
149
+ .$defaultFn(() => new Date()),
150
+ });
151
+
152
+ export const authInvitation = sqliteTable('invitation', {
153
+ id: text('id').primaryKey(),
154
+ email: text('email').notNull(),
155
+ organizationId: text('organization_id')
156
+ .notNull()
157
+ .references(() => authOrganization.id, { onDelete: 'cascade' }),
158
+ inviterId: text('inviter_id')
159
+ .notNull()
160
+ .references(() => authUser.id, { onDelete: 'cascade' }),
161
+ role: text('role').notNull().default('member'),
162
+ status: text('status').notNull().default('pending'),
163
+ expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
164
+ createdAt: integer('created_at', { mode: 'timestamp_ms' })
165
+ .notNull()
166
+ .$defaultFn(() => new Date()),
167
+ });
168
+
169
+ export const authSchema = {
170
+ user: authUser,
171
+ session: authSession,
172
+ account: authAccount,
173
+ verification: authVerification,
174
+ };
175
+
176
+ export const apikeySchema = {
177
+ apikey: authApikey,
178
+ };
179
+
180
+ export const organizationSchema = {
181
+ organization: authOrganization,
182
+ member: authMember,
183
+ invitation: authInvitation,
184
+ };
@@ -0,0 +1,95 @@
1
+ import crypto from 'node:crypto';
2
+ import { eq } from 'drizzle-orm';
3
+
4
+ import type { VobaseDb } from '../../db/client';
5
+ import { credentialsTable } from './schema';
6
+
7
+ const ALGORITHM = 'aes-256-gcm';
8
+ const IV_LENGTH = 12;
9
+ const TAG_LENGTH = 16;
10
+
11
+ const KDF_SALT = Buffer.from('vobase-credential-store-v1');
12
+
13
+ function getEncryptionKey(): Buffer {
14
+ const secret = process.env.BETTER_AUTH_SECRET;
15
+ if (!secret)
16
+ throw new Error(
17
+ 'BETTER_AUTH_SECRET is required for credential encryption',
18
+ );
19
+ return crypto.scryptSync(secret, KDF_SALT, 32);
20
+ }
21
+
22
+ /** Encrypt a plaintext string using AES-256-GCM. Returns base64-encoded ciphertext. */
23
+ export function encrypt(plaintext: string): string {
24
+ const key = getEncryptionKey();
25
+ const iv = crypto.randomBytes(IV_LENGTH);
26
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
27
+ const encrypted = Buffer.concat([
28
+ cipher.update(plaintext, 'utf8'),
29
+ cipher.final(),
30
+ ]);
31
+ const tag = cipher.getAuthTag();
32
+ return Buffer.concat([iv, tag, encrypted]).toString('base64');
33
+ }
34
+
35
+ /** Decrypt a base64-encoded ciphertext using AES-256-GCM. */
36
+ export function decrypt(encoded: string): string {
37
+ const key = getEncryptionKey();
38
+ const data = Buffer.from(encoded, 'base64');
39
+ if (data.length < IV_LENGTH + TAG_LENGTH + 1) {
40
+ throw new Error('Invalid ciphertext: too short');
41
+ }
42
+ const iv = data.subarray(0, IV_LENGTH);
43
+ const tag = data.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
44
+ const ciphertext = data.subarray(IV_LENGTH + TAG_LENGTH);
45
+ const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
46
+ decipher.setAuthTag(tag);
47
+ return Buffer.concat([
48
+ decipher.update(ciphertext),
49
+ decipher.final(),
50
+ ]).toString('utf8');
51
+ }
52
+
53
+ /** Get a single credential value (decrypted). Returns null if not found or decryption fails. */
54
+ export async function getCredential(
55
+ db: VobaseDb,
56
+ key: string,
57
+ ): Promise<string | null> {
58
+ const rows = await db
59
+ .select()
60
+ .from(credentialsTable)
61
+ .where(eq(credentialsTable.key, key))
62
+ .limit(1);
63
+ if (!rows[0]) return null;
64
+ try {
65
+ return decrypt(rows[0].value);
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /** Set a single credential value (encrypted). Upserts on conflict. */
72
+ export async function setCredential(
73
+ db: VobaseDb,
74
+ key: string,
75
+ value: string,
76
+ ): Promise<void> {
77
+ const encrypted = encrypt(value);
78
+ await db
79
+ .insert(credentialsTable)
80
+ .values({ key, value: encrypted })
81
+ .onConflictDoUpdate({
82
+ target: credentialsTable.key,
83
+ set: { value: encrypted, updatedAt: new Date() },
84
+ });
85
+ }
86
+
87
+ /** Delete a single credential. */
88
+ export async function deleteCredential(
89
+ db: VobaseDb,
90
+ key: string,
91
+ ): Promise<void> {
92
+ await db
93
+ .delete(credentialsTable)
94
+ .where(eq(credentialsTable.key, key));
95
+ }
@@ -0,0 +1,15 @@
1
+ import { Hono } from 'hono';
2
+ import { defineBuiltinModule } from '../../module';
3
+ import { credentialsTable } from './schema';
4
+
5
+ export { credentialsTable } from './schema';
6
+ export { encrypt, decrypt, getCredential, setCredential, deleteCredential } from './encrypt';
7
+
8
+ export function createCredentialsModule() {
9
+ return defineBuiltinModule({
10
+ name: '_credentials',
11
+ schema: { credentialsTable },
12
+ routes: new Hono(),
13
+ init: () => {},
14
+ });
15
+ }
@@ -0,0 +1,10 @@
1
+ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
2
+
3
+ export const credentialsTable = sqliteTable('_credentials', {
4
+ key: text('key').primaryKey(),
5
+ value: text('value').notNull(),
6
+ updatedAt: integer('updated_at', { mode: 'timestamp_ms' })
7
+ .notNull()
8
+ .$defaultFn(() => new Date())
9
+ .$onUpdate(() => new Date()),
10
+ });
@@ -0,0 +1,90 @@
1
+ import { Hono } from 'hono';
2
+
3
+ import type { EmailProvider, WhatsAppProvider } from '../../contracts/notify';
4
+ import { defineBuiltinModule } from '../../module';
5
+ import { createResendProvider, type ResendConfig } from './providers/resend';
6
+ import { createSmtpProvider, type SmtpConfig } from './providers/smtp';
7
+ import { createWabaProvider } from './providers/waba';
8
+ import { createNotifyService } from './service';
9
+ import { notifySchema } from './schema';
10
+ import type { VobaseDb } from '../../db/client';
11
+
12
+ export interface EmailNotifyConfig {
13
+ provider: 'resend' | 'smtp';
14
+ from: string;
15
+ resend?: Omit<ResendConfig, 'from'>;
16
+ smtp?: Omit<SmtpConfig, 'from'>;
17
+ }
18
+
19
+ export interface WhatsAppNotifyConfig {
20
+ phoneNumberId: string;
21
+ accessToken: string;
22
+ apiVersion?: string;
23
+ }
24
+
25
+ export interface NotifyModuleConfig {
26
+ email?: EmailNotifyConfig;
27
+ whatsapp?: WhatsAppNotifyConfig;
28
+ }
29
+
30
+ export function createNotifyModule(db: VobaseDb, config: NotifyModuleConfig) {
31
+ let emailProvider: EmailProvider | undefined;
32
+ let emailProviderName: string | undefined;
33
+
34
+ if (config.email) {
35
+ if (config.email.provider === 'resend') {
36
+ if (!config.email.resend) {
37
+ throw new Error('Resend config required when provider is "resend"');
38
+ }
39
+ emailProvider = createResendProvider({
40
+ apiKey: config.email.resend.apiKey,
41
+ from: config.email.from,
42
+ });
43
+ emailProviderName = 'resend';
44
+ } else if (config.email.provider === 'smtp') {
45
+ if (!config.email.smtp) {
46
+ throw new Error('SMTP config required when provider is "smtp"');
47
+ }
48
+ emailProvider = createSmtpProvider({
49
+ ...config.email.smtp,
50
+ from: config.email.from,
51
+ });
52
+ emailProviderName = 'smtp';
53
+ }
54
+ }
55
+
56
+ let whatsappProvider: WhatsAppProvider | undefined;
57
+ let whatsappProviderName: string | undefined;
58
+
59
+ if (config.whatsapp) {
60
+ whatsappProvider = createWabaProvider({
61
+ phoneNumberId: config.whatsapp.phoneNumberId,
62
+ accessToken: config.whatsapp.accessToken,
63
+ apiVersion: config.whatsapp.apiVersion,
64
+ });
65
+ whatsappProviderName = 'waba';
66
+ }
67
+
68
+ const service = createNotifyService({
69
+ db,
70
+ emailProvider,
71
+ emailProviderName,
72
+ whatsappProvider,
73
+ whatsappProviderName,
74
+ });
75
+
76
+ const mod = defineBuiltinModule({
77
+ name: '_notify',
78
+ schema: notifySchema,
79
+ routes: new Hono(),
80
+ });
81
+
82
+ return { ...mod, service };
83
+ }
84
+
85
+ export { notifyLog, notifySchema } from './schema';
86
+ export { createNotifyService } from './service';
87
+ export type { NotifyService, EmailChannel, WhatsAppChannel } from './service';
88
+ export { createResendProvider, type ResendConfig } from './providers/resend';
89
+ export { createSmtpProvider, type SmtpConfig } from './providers/smtp';
90
+ export { createWabaProvider, type WabaConfig } from './providers/waba';