@vobase/core 0.10.0 → 0.11.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/package.json +7 -9
- package/src/__tests__/drizzle-introspection.test.ts +77 -0
- package/src/__tests__/e2e.test.ts +225 -0
- package/src/__tests__/permissions.test.ts +157 -0
- package/src/__tests__/rpc-types.test.ts +92 -0
- package/src/app.test.ts +99 -0
- package/src/app.ts +178 -0
- package/src/audit.test.ts +126 -0
- package/src/auth.test.ts +74 -0
- package/src/contracts/auth.ts +37 -0
- package/{dist/contracts/module.d.ts → src/contracts/module.ts} +6 -6
- package/src/contracts/notify.ts +47 -0
- package/src/contracts/permissions.ts +10 -0
- package/src/contracts/storage.ts +61 -0
- package/src/ctx.test.ts +162 -0
- package/src/ctx.ts +64 -0
- package/src/db/client.test.ts +75 -0
- package/src/db/client.ts +15 -0
- package/src/db/helpers.test.ts +147 -0
- package/src/db/helpers.ts +51 -0
- package/src/db/index.ts +8 -0
- package/{dist/index.d.ts → src/index.ts} +103 -6
- package/src/infra/circuit-breaker.test.ts +74 -0
- package/src/infra/circuit-breaker.ts +57 -0
- package/src/infra/errors.test.ts +175 -0
- package/src/infra/errors.ts +64 -0
- package/src/infra/http-client.test.ts +482 -0
- package/src/infra/http-client.ts +221 -0
- package/src/infra/index.ts +35 -0
- package/src/infra/job.test.ts +85 -0
- package/src/infra/job.ts +94 -0
- package/src/infra/logger.test.ts +65 -0
- package/src/infra/logger.ts +18 -0
- package/src/infra/queue.test.ts +46 -0
- package/src/infra/queue.ts +147 -0
- package/src/infra/throw-proxy.test.ts +34 -0
- package/src/infra/throw-proxy.ts +17 -0
- package/src/infra/webhooks-schema.ts +17 -0
- package/src/infra/webhooks.test.ts +364 -0
- package/src/infra/webhooks.ts +146 -0
- package/src/mcp/auth.test.ts +129 -0
- package/src/mcp/crud.test.ts +128 -0
- package/src/mcp/crud.ts +171 -0
- package/{dist/mcp/index.d.ts → src/mcp/index.ts} +0 -1
- package/src/mcp/server.test.ts +153 -0
- package/src/mcp/server.ts +178 -0
- package/src/middleware/audit.test.ts +169 -0
- package/src/module-registry.ts +18 -0
- package/src/module.test.ts +168 -0
- package/src/module.ts +111 -0
- package/src/modules/audit/index.ts +18 -0
- package/src/modules/audit/middleware.ts +33 -0
- package/src/modules/audit/schema.ts +35 -0
- package/src/modules/audit/track-changes.ts +70 -0
- package/src/modules/auth/audit-hooks.ts +74 -0
- package/src/modules/auth/index.ts +101 -0
- package/src/modules/auth/middleware.ts +51 -0
- package/src/modules/auth/permissions.ts +46 -0
- package/src/modules/auth/schema.ts +184 -0
- package/src/modules/credentials/encrypt.ts +95 -0
- package/src/modules/credentials/index.ts +15 -0
- package/src/modules/credentials/schema.ts +10 -0
- package/src/modules/notify/index.ts +90 -0
- package/src/modules/notify/notify.test.ts +145 -0
- package/src/modules/notify/providers/resend.ts +47 -0
- package/src/modules/notify/providers/smtp.ts +117 -0
- package/src/modules/notify/providers/waba.ts +82 -0
- package/src/modules/notify/schema.ts +27 -0
- package/src/modules/notify/service.ts +93 -0
- package/src/modules/sequences/index.ts +15 -0
- package/src/modules/sequences/next-sequence.ts +48 -0
- package/src/modules/sequences/schema.ts +12 -0
- package/src/modules/storage/index.ts +44 -0
- package/src/modules/storage/providers/local.ts +124 -0
- package/src/modules/storage/providers/s3.ts +83 -0
- package/src/modules/storage/routes.ts +76 -0
- package/src/modules/storage/schema.ts +26 -0
- package/src/modules/storage/service.ts +202 -0
- package/src/modules/storage/storage.test.ts +225 -0
- package/src/schemas.test.ts +44 -0
- package/src/schemas.ts +63 -0
- package/src/sequence.test.ts +56 -0
- package/dist/app.d.ts +0 -37
- package/dist/app.d.ts.map +0 -1
- package/dist/contracts/auth.d.ts +0 -35
- package/dist/contracts/auth.d.ts.map +0 -1
- package/dist/contracts/module.d.ts.map +0 -1
- package/dist/contracts/notify.d.ts +0 -46
- package/dist/contracts/notify.d.ts.map +0 -1
- package/dist/contracts/permissions.d.ts +0 -10
- package/dist/contracts/permissions.d.ts.map +0 -1
- package/dist/contracts/storage.d.ts +0 -54
- package/dist/contracts/storage.d.ts.map +0 -1
- package/dist/ctx.d.ts +0 -40
- package/dist/ctx.d.ts.map +0 -1
- package/dist/db/client.d.ts +0 -4
- package/dist/db/client.d.ts.map +0 -1
- package/dist/db/helpers.d.ts +0 -26
- package/dist/db/helpers.d.ts.map +0 -1
- package/dist/db/index.d.ts +0 -3
- package/dist/db/index.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -98611
- package/dist/infra/circuit-breaker.d.ts +0 -17
- package/dist/infra/circuit-breaker.d.ts.map +0 -1
- package/dist/infra/errors.d.ts +0 -26
- package/dist/infra/errors.d.ts.map +0 -1
- package/dist/infra/http-client.d.ts +0 -31
- package/dist/infra/http-client.d.ts.map +0 -1
- package/dist/infra/index.d.ts +0 -11
- package/dist/infra/index.d.ts.map +0 -1
- package/dist/infra/job.d.ts +0 -14
- package/dist/infra/job.d.ts.map +0 -1
- package/dist/infra/logger.d.ts +0 -7
- package/dist/infra/logger.d.ts.map +0 -1
- package/dist/infra/queue.d.ts +0 -18
- package/dist/infra/queue.d.ts.map +0 -1
- package/dist/infra/throw-proxy.d.ts +0 -7
- package/dist/infra/throw-proxy.d.ts.map +0 -1
- package/dist/infra/webhooks-schema.d.ts +0 -60
- package/dist/infra/webhooks-schema.d.ts.map +0 -1
- package/dist/infra/webhooks.d.ts +0 -46
- package/dist/infra/webhooks.d.ts.map +0 -1
- package/dist/mcp/crud.d.ts +0 -12
- package/dist/mcp/crud.d.ts.map +0 -1
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/server.d.ts +0 -16
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/module-registry.d.ts +0 -3
- package/dist/module-registry.d.ts.map +0 -1
- package/dist/module.d.ts +0 -29
- package/dist/module.d.ts.map +0 -1
- package/dist/modules/audit/index.d.ts +0 -5
- package/dist/modules/audit/index.d.ts.map +0 -1
- package/dist/modules/audit/middleware.d.ts +0 -3
- package/dist/modules/audit/middleware.d.ts.map +0 -1
- package/dist/modules/audit/schema.d.ts +0 -247
- package/dist/modules/audit/schema.d.ts.map +0 -1
- package/dist/modules/audit/track-changes.d.ts +0 -3
- package/dist/modules/audit/track-changes.d.ts.map +0 -1
- package/dist/modules/auth/audit-hooks.d.ts +0 -6
- package/dist/modules/auth/audit-hooks.d.ts.map +0 -1
- package/dist/modules/auth/index.d.ts +0 -25
- package/dist/modules/auth/index.d.ts.map +0 -1
- package/dist/modules/auth/middleware.d.ts +0 -15
- package/dist/modules/auth/middleware.d.ts.map +0 -1
- package/dist/modules/auth/permissions.d.ts +0 -5
- package/dist/modules/auth/permissions.d.ts.map +0 -1
- package/dist/modules/auth/schema.d.ts +0 -2519
- package/dist/modules/auth/schema.d.ts.map +0 -1
- package/dist/modules/credentials/encrypt.d.ts +0 -12
- package/dist/modules/credentials/encrypt.d.ts.map +0 -1
- package/dist/modules/credentials/index.d.ts +0 -4
- package/dist/modules/credentials/index.d.ts.map +0 -1
- package/dist/modules/credentials/schema.d.ts +0 -56
- package/dist/modules/credentials/schema.d.ts.map +0 -1
- package/dist/modules/notify/index.d.ts +0 -36
- package/dist/modules/notify/index.d.ts.map +0 -1
- package/dist/modules/notify/providers/resend.d.ts +0 -7
- package/dist/modules/notify/providers/resend.d.ts.map +0 -1
- package/dist/modules/notify/providers/smtp.d.ts +0 -18
- package/dist/modules/notify/providers/smtp.d.ts.map +0 -1
- package/dist/modules/notify/providers/waba.d.ts +0 -12
- package/dist/modules/notify/providers/waba.d.ts.map +0 -1
- package/dist/modules/notify/schema.d.ts +0 -337
- package/dist/modules/notify/schema.d.ts.map +0 -1
- package/dist/modules/notify/service.d.ts +0 -22
- package/dist/modules/notify/service.d.ts.map +0 -1
- package/dist/modules/sequences/index.d.ts +0 -4
- package/dist/modules/sequences/index.d.ts.map +0 -1
- package/dist/modules/sequences/next-sequence.d.ts +0 -8
- package/dist/modules/sequences/next-sequence.d.ts.map +0 -1
- package/dist/modules/sequences/schema.d.ts +0 -72
- package/dist/modules/sequences/schema.d.ts.map +0 -1
- package/dist/modules/storage/index.d.ts +0 -24
- package/dist/modules/storage/index.d.ts.map +0 -1
- package/dist/modules/storage/providers/local.d.ts +0 -3
- package/dist/modules/storage/providers/local.d.ts.map +0 -1
- package/dist/modules/storage/providers/s3.d.ts +0 -3
- package/dist/modules/storage/providers/s3.d.ts.map +0 -1
- package/dist/modules/storage/routes.d.ts +0 -4
- package/dist/modules/storage/routes.d.ts.map +0 -1
- package/dist/modules/storage/schema.d.ts +0 -273
- package/dist/modules/storage/schema.d.ts.map +0 -1
- package/dist/modules/storage/service.d.ts +0 -35
- package/dist/modules/storage/service.d.ts.map +0 -1
- package/dist/schemas.d.ts +0 -19
- package/dist/schemas.d.ts.map +0 -1
package/src/app.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
|
|
3
|
+
import { contextMiddleware } from './ctx';
|
|
4
|
+
import { createDatabase } from './db/client';
|
|
5
|
+
import { createHttpClient, type HttpClientOptions } from './infra/http-client';
|
|
6
|
+
import { errorHandler } from './infra/errors';
|
|
7
|
+
import { createWorker } from './infra/job';
|
|
8
|
+
import { logger } from './infra/logger';
|
|
9
|
+
import { createMcpHandler } from './mcp/server';
|
|
10
|
+
import { createAuditModule } from './modules/audit';
|
|
11
|
+
import { createAuthModule, optionalSessionMiddleware, type AuthModuleConfig } from './modules/auth';
|
|
12
|
+
import { createCredentialsModule } from './modules/credentials';
|
|
13
|
+
import { createNotifyModule, type NotifyModuleConfig } from './modules/notify';
|
|
14
|
+
import type { NotifyService } from './modules/notify/service';
|
|
15
|
+
import { createSequencesModule } from './modules/sequences';
|
|
16
|
+
import { createStorageModule, type StorageModuleConfig } from './modules/storage';
|
|
17
|
+
import type { StorageService } from './modules/storage/service';
|
|
18
|
+
import type { VobaseModule } from './module';
|
|
19
|
+
import { createScheduler } from './infra/queue';
|
|
20
|
+
import { createThrowProxy } from './infra/throw-proxy';
|
|
21
|
+
import { createWebhookRoutes, type WebhookConfig } from './infra/webhooks';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_QUEUE_DB_PATH = '/data/bunqueue.db';
|
|
24
|
+
const LOCAL_QUEUE_DB_PATH = './data/bunqueue.db';
|
|
25
|
+
|
|
26
|
+
function deriveQueueDbPath(databasePath: string): string {
|
|
27
|
+
if (databasePath !== ':memory:' && databasePath.endsWith('.db')) {
|
|
28
|
+
return databasePath.replace(/\.db$/, '-queue.db');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return DEFAULT_QUEUE_DB_PATH;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function createSchedulerWithFallback(queueDbPath: string) {
|
|
35
|
+
try {
|
|
36
|
+
return {
|
|
37
|
+
scheduler: await createScheduler({ dbPath: queueDbPath }),
|
|
38
|
+
effectiveQueueDbPath: queueDbPath,
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (queueDbPath !== DEFAULT_QUEUE_DB_PATH) {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logger.warn('Falling back to local queue database path', {
|
|
46
|
+
queueDbPath,
|
|
47
|
+
fallbackQueueDbPath: LOCAL_QUEUE_DB_PATH,
|
|
48
|
+
error: error instanceof Error ? error.message : String(error),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
scheduler: await createScheduler({ dbPath: LOCAL_QUEUE_DB_PATH }),
|
|
53
|
+
effectiveQueueDbPath: LOCAL_QUEUE_DB_PATH,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CreateAppConfig {
|
|
59
|
+
modules: VobaseModule[];
|
|
60
|
+
database: string;
|
|
61
|
+
storage?: StorageModuleConfig;
|
|
62
|
+
notify?: NotifyModuleConfig;
|
|
63
|
+
http?: HttpClientOptions;
|
|
64
|
+
webhooks?: Record<string, WebhookConfig>;
|
|
65
|
+
mcp?: { enabled?: boolean };
|
|
66
|
+
trustedOrigins?: string[];
|
|
67
|
+
auth?: Omit<AuthModuleConfig, 'trustedOrigins'>;
|
|
68
|
+
/** Enable the credentials module (encrypted credential store). Default: false */
|
|
69
|
+
credentials?: { enabled: boolean };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function createApp(config: CreateAppConfig) {
|
|
73
|
+
const db = createDatabase(config.database);
|
|
74
|
+
|
|
75
|
+
const queueDbPath = deriveQueueDbPath(config.database);
|
|
76
|
+
const { scheduler, effectiveQueueDbPath } =
|
|
77
|
+
await createSchedulerWithFallback(queueDbPath);
|
|
78
|
+
|
|
79
|
+
const http = createHttpClient(config.http);
|
|
80
|
+
|
|
81
|
+
// === Auth Module (always active) ===
|
|
82
|
+
const authMod = createAuthModule(db, {
|
|
83
|
+
...config.auth,
|
|
84
|
+
trustedOrigins: config.trustedOrigins,
|
|
85
|
+
});
|
|
86
|
+
const authAdapter = authMod.adapter;
|
|
87
|
+
|
|
88
|
+
// === Storage Module (config-driven) ===
|
|
89
|
+
let storageMod: ReturnType<typeof createStorageModule> | undefined;
|
|
90
|
+
let storageService: StorageService;
|
|
91
|
+
if (config.storage) {
|
|
92
|
+
storageMod = createStorageModule(db, config.storage);
|
|
93
|
+
storageService = storageMod.service;
|
|
94
|
+
} else {
|
|
95
|
+
storageService = createThrowProxy<StorageService>('storage');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// === Notify Module (config-driven) ===
|
|
99
|
+
let notifyMod: ReturnType<typeof createNotifyModule> | undefined;
|
|
100
|
+
let notifyService: NotifyService;
|
|
101
|
+
if (config.notify) {
|
|
102
|
+
notifyMod = createNotifyModule(db, config.notify);
|
|
103
|
+
notifyService = notifyMod.service;
|
|
104
|
+
} else {
|
|
105
|
+
notifyService = createThrowProxy<NotifyService>('notify');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// === Built-in Module Init ===
|
|
109
|
+
const initCtx = { db, scheduler, http, storage: storageService, notify: notifyService };
|
|
110
|
+
|
|
111
|
+
const auditMod = createAuditModule();
|
|
112
|
+
auditMod.init?.(initCtx);
|
|
113
|
+
|
|
114
|
+
const seqMod = createSequencesModule();
|
|
115
|
+
seqMod.init?.(initCtx);
|
|
116
|
+
|
|
117
|
+
let credMod: VobaseModule | undefined;
|
|
118
|
+
if (config.credentials?.enabled) {
|
|
119
|
+
credMod = createCredentialsModule();
|
|
120
|
+
credMod.init?.(initCtx);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// === Base app with middleware ===
|
|
124
|
+
const base = new Hono();
|
|
125
|
+
base.onError(errorHandler);
|
|
126
|
+
base.use('*', contextMiddleware({ db, scheduler, storage: storageService, notify: notifyService, http }));
|
|
127
|
+
base.use('/api/*', optionalSessionMiddleware(authAdapter));
|
|
128
|
+
base.on(['POST', 'GET'], '/api/auth/*', (c) => authAdapter.handler(c.req.raw));
|
|
129
|
+
|
|
130
|
+
// Mount via chaining to preserve route types for hc<AppType>
|
|
131
|
+
const app = base
|
|
132
|
+
.get('/health', (c) => c.json({ status: 'ok', uptime: process.uptime() }));
|
|
133
|
+
|
|
134
|
+
// === Storage routes ===
|
|
135
|
+
if (storageMod) {
|
|
136
|
+
(app as Hono).route('/api/storage', storageMod.routes);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// === User Modules ===
|
|
140
|
+
const userModules = config.modules;
|
|
141
|
+
for (const mod of userModules) {
|
|
142
|
+
mod.init?.(initCtx);
|
|
143
|
+
(app as Hono).route(`/api/${mod.name}`, mod.routes);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// === Webhooks ===
|
|
147
|
+
if (config.webhooks && Object.keys(config.webhooks).length > 0) {
|
|
148
|
+
const webhookRouter = createWebhookRoutes(config.webhooks, { db, scheduler });
|
|
149
|
+
(app as Hono).route('', webhookRouter);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// === MCP + Jobs ===
|
|
153
|
+
const builtInModules: VobaseModule[] = [authMod, auditMod, seqMod];
|
|
154
|
+
if (storageMod) builtInModules.push(storageMod);
|
|
155
|
+
if (notifyMod) builtInModules.push(notifyMod);
|
|
156
|
+
if (credMod) builtInModules.push(credMod);
|
|
157
|
+
const allModules = [...builtInModules, ...userModules];
|
|
158
|
+
|
|
159
|
+
if (config.mcp?.enabled) {
|
|
160
|
+
const mcpHandler = createMcpHandler({
|
|
161
|
+
db,
|
|
162
|
+
modules: allModules,
|
|
163
|
+
verifyApiKey: authMod.verifyApiKey,
|
|
164
|
+
organizationEnabled: authMod.organizationEnabled,
|
|
165
|
+
});
|
|
166
|
+
(app as Hono).all('/mcp', async (c) => {
|
|
167
|
+
const response = await mcpHandler(c.req.raw);
|
|
168
|
+
return response;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const allJobs = allModules.flatMap((module) => module.jobs ?? []);
|
|
173
|
+
if (allJobs.length > 0) {
|
|
174
|
+
await createWorker(allJobs, { dbPath: effectiveQueueDbPath });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return app;
|
|
178
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
3
|
+
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
|
4
|
+
|
|
5
|
+
import type { VobaseDb } from './db';
|
|
6
|
+
import { trackChanges } from './modules/audit/track-changes';
|
|
7
|
+
import * as schema from './modules/audit/schema';
|
|
8
|
+
|
|
9
|
+
interface AuditRow {
|
|
10
|
+
tableName: string;
|
|
11
|
+
recordId: string;
|
|
12
|
+
oldData: string | null;
|
|
13
|
+
newData: string | null;
|
|
14
|
+
changedBy: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('trackChanges()', () => {
|
|
18
|
+
let sqlite: Database;
|
|
19
|
+
let db: VobaseDb;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
sqlite = new Database(':memory:');
|
|
23
|
+
sqlite.run('PRAGMA journal_mode=WAL');
|
|
24
|
+
sqlite.exec(`
|
|
25
|
+
CREATE TABLE _record_audits (
|
|
26
|
+
id TEXT PRIMARY KEY,
|
|
27
|
+
table_name TEXT NOT NULL,
|
|
28
|
+
record_id TEXT NOT NULL,
|
|
29
|
+
old_data TEXT,
|
|
30
|
+
new_data TEXT,
|
|
31
|
+
changed_by TEXT,
|
|
32
|
+
created_at INTEGER NOT NULL
|
|
33
|
+
)
|
|
34
|
+
`);
|
|
35
|
+
|
|
36
|
+
db = drizzle({ client: sqlite, schema }) as unknown as VobaseDb;
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
sqlite.close();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function getRows(): AuditRow[] {
|
|
44
|
+
return sqlite
|
|
45
|
+
.prepare(
|
|
46
|
+
`
|
|
47
|
+
SELECT
|
|
48
|
+
table_name AS tableName,
|
|
49
|
+
record_id AS recordId,
|
|
50
|
+
old_data AS oldData,
|
|
51
|
+
new_data AS newData,
|
|
52
|
+
changed_by AS changedBy
|
|
53
|
+
FROM _record_audits
|
|
54
|
+
ORDER BY rowid ASC
|
|
55
|
+
`,
|
|
56
|
+
)
|
|
57
|
+
.all() as AuditRow[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
it('stores full new data for create events', () => {
|
|
61
|
+
trackChanges(
|
|
62
|
+
db,
|
|
63
|
+
'invoices',
|
|
64
|
+
'inv_1',
|
|
65
|
+
null,
|
|
66
|
+
{ status: 'draft', total: 100 },
|
|
67
|
+
'user_1',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const rows = getRows();
|
|
71
|
+
expect(rows).toHaveLength(1);
|
|
72
|
+
expect(rows[0]).toEqual({
|
|
73
|
+
tableName: 'invoices',
|
|
74
|
+
recordId: 'inv_1',
|
|
75
|
+
oldData: null,
|
|
76
|
+
newData: JSON.stringify({ status: 'draft', total: 100 }),
|
|
77
|
+
changedBy: 'user_1',
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('stores only changed fields for update events', () => {
|
|
82
|
+
trackChanges(
|
|
83
|
+
db,
|
|
84
|
+
'invoices',
|
|
85
|
+
'inv_2',
|
|
86
|
+
{ status: 'draft', total: 100, note: 'A' },
|
|
87
|
+
{ status: 'sent', total: 100, note: 'B' },
|
|
88
|
+
'user_2',
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const rows = getRows();
|
|
92
|
+
expect(rows).toHaveLength(1);
|
|
93
|
+
expect(rows[0]?.oldData).toBe(
|
|
94
|
+
JSON.stringify({ status: 'draft', note: 'A' }),
|
|
95
|
+
);
|
|
96
|
+
expect(rows[0]?.newData).toBe(
|
|
97
|
+
JSON.stringify({ status: 'sent', note: 'B' }),
|
|
98
|
+
);
|
|
99
|
+
expect(rows[0]?.changedBy).toBe('user_2');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('stores full old data for delete events', () => {
|
|
103
|
+
trackChanges(db, 'invoices', 'inv_3', { status: 'void', total: 20 }, null);
|
|
104
|
+
|
|
105
|
+
const rows = getRows();
|
|
106
|
+
expect(rows).toHaveLength(1);
|
|
107
|
+
expect(rows[0]?.oldData).toBe(
|
|
108
|
+
JSON.stringify({ status: 'void', total: 20 }),
|
|
109
|
+
);
|
|
110
|
+
expect(rows[0]?.newData).toBeNull();
|
|
111
|
+
expect(rows[0]?.changedBy).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('does not store an audit row when values are unchanged', () => {
|
|
115
|
+
trackChanges(
|
|
116
|
+
db,
|
|
117
|
+
'invoices',
|
|
118
|
+
'inv_4',
|
|
119
|
+
{ status: 'draft', total: 100 },
|
|
120
|
+
{ status: 'draft', total: 100 },
|
|
121
|
+
'user_3',
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(getRows()).toHaveLength(0);
|
|
125
|
+
});
|
|
126
|
+
});
|
package/src/auth.test.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import { describe, expect, it, mock } from 'bun:test';
|
|
3
|
+
import type { Context, Next } from 'hono';
|
|
4
|
+
|
|
5
|
+
import type { AuthAdapter, AuthSession } from './contracts/auth';
|
|
6
|
+
import { createAuthModule } from './modules/auth';
|
|
7
|
+
import { sessionMiddleware, optionalSessionMiddleware } from './modules/auth/middleware';
|
|
8
|
+
import { createDatabase, type VobaseDb } from './db';
|
|
9
|
+
import { VobaseError } from './infra/errors';
|
|
10
|
+
|
|
11
|
+
type DbWithClient = VobaseDb & { $client: Database };
|
|
12
|
+
|
|
13
|
+
function createMockContext() {
|
|
14
|
+
const values = new Map<string, unknown>();
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
req: {
|
|
18
|
+
raw: {
|
|
19
|
+
headers: new Headers(),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
set: (key: string, value: unknown) => {
|
|
23
|
+
values.set(key, value);
|
|
24
|
+
},
|
|
25
|
+
get: (key: string) => values.get(key),
|
|
26
|
+
} as unknown as Context;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createMockAdapter(getSession: () => Promise<AuthSession | null>): AuthAdapter {
|
|
30
|
+
return {
|
|
31
|
+
getSession,
|
|
32
|
+
handler: async () => new Response('ok'),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('createAuthModule', () => {
|
|
37
|
+
it('returns a module with adapter having handler and getSession', () => {
|
|
38
|
+
const db = createDatabase(':memory:') as DbWithClient;
|
|
39
|
+
const authMod = createAuthModule(db);
|
|
40
|
+
|
|
41
|
+
expect(authMod).toHaveProperty('adapter');
|
|
42
|
+
expect(authMod.adapter).toHaveProperty('handler');
|
|
43
|
+
expect(authMod.adapter).toHaveProperty('getSession');
|
|
44
|
+
expect(authMod.name).toBe('_auth');
|
|
45
|
+
|
|
46
|
+
db.$client.close();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('session middleware', () => {
|
|
51
|
+
it('sessionMiddleware throws unauthorized when no session exists', async () => {
|
|
52
|
+
const adapter = createMockAdapter(async () => null);
|
|
53
|
+
const c = createMockContext();
|
|
54
|
+
|
|
55
|
+
await expect(
|
|
56
|
+
sessionMiddleware(adapter)(c, async () => {}),
|
|
57
|
+
).rejects.toBeInstanceOf(VobaseError);
|
|
58
|
+
|
|
59
|
+
await expect(
|
|
60
|
+
sessionMiddleware(adapter)(c, async () => {}),
|
|
61
|
+
).rejects.toMatchObject({ code: 'UNAUTHORIZED' });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('optionalSessionMiddleware sets user to null and continues', async () => {
|
|
65
|
+
const next = mock(async () => {}) as unknown as Next;
|
|
66
|
+
const adapter = createMockAdapter(async () => null);
|
|
67
|
+
const c = createMockContext();
|
|
68
|
+
|
|
69
|
+
await optionalSessionMiddleware(adapter)(c, next);
|
|
70
|
+
|
|
71
|
+
expect(c.get('user')).toBeNull();
|
|
72
|
+
expect(next).toHaveBeenCalledTimes(1);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core contract for authentication. The auth adapter translates an incoming
|
|
3
|
+
* request into a user identity. Core uses this in the session middleware
|
|
4
|
+
* to populate ctx.user. The built-in auth module implements this via
|
|
5
|
+
* better-auth; users can swap in their own implementation.
|
|
6
|
+
*/
|
|
7
|
+
export interface AuthAdapter {
|
|
8
|
+
/**
|
|
9
|
+
* Extract user identity from a request. Returns null if unauthenticated.
|
|
10
|
+
* Must not throw — return null for invalid/expired sessions.
|
|
11
|
+
*/
|
|
12
|
+
getSession(headers: Headers): Promise<AuthSession | null>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The raw request handler for auth routes (sign-in, sign-up, etc.)
|
|
16
|
+
* Mounted at /api/auth/* by the auth module.
|
|
17
|
+
*/
|
|
18
|
+
handler: (request: Request) => Promise<Response>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AuthSession {
|
|
22
|
+
user: AuthUser;
|
|
23
|
+
session: {
|
|
24
|
+
id: string;
|
|
25
|
+
expiresAt: Date;
|
|
26
|
+
token: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface AuthUser {
|
|
31
|
+
id: string;
|
|
32
|
+
email: string;
|
|
33
|
+
name: string;
|
|
34
|
+
role: string;
|
|
35
|
+
/** Set when the user has an active organization (better-auth organization plugin) */
|
|
36
|
+
activeOrganizationId?: string;
|
|
37
|
+
}
|
|
@@ -3,15 +3,15 @@ import type { HttpClient } from '../infra/http-client';
|
|
|
3
3
|
import type { NotifyService } from '../modules/notify/service';
|
|
4
4
|
import type { StorageService } from '../modules/storage/service';
|
|
5
5
|
import type { Scheduler } from '../infra/queue';
|
|
6
|
+
|
|
6
7
|
/**
|
|
7
8
|
* Context passed to a module's `init` hook during app boot.
|
|
8
9
|
* Provides access to core infrastructure services.
|
|
9
10
|
*/
|
|
10
11
|
export interface ModuleInitContext {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
db: VobaseDb;
|
|
13
|
+
scheduler: Scheduler;
|
|
14
|
+
storage: StorageService;
|
|
15
|
+
http: HttpClient;
|
|
16
|
+
notify: NotifyService;
|
|
16
17
|
}
|
|
17
|
-
//# sourceMappingURL=module.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider interfaces for the notify module. Each channel (email, WhatsApp)
|
|
3
|
+
* has its own provider interface with channel-specific options.
|
|
4
|
+
*/
|
|
5
|
+
export interface EmailProvider {
|
|
6
|
+
send(message: EmailMessage): Promise<EmailResult>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface EmailMessage {
|
|
10
|
+
to: string | string[];
|
|
11
|
+
subject: string;
|
|
12
|
+
html?: string;
|
|
13
|
+
text?: string;
|
|
14
|
+
from?: string; // overrides default from in config
|
|
15
|
+
cc?: string[];
|
|
16
|
+
bcc?: string[];
|
|
17
|
+
replyTo?: string;
|
|
18
|
+
attachments?: EmailAttachment[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface EmailAttachment {
|
|
22
|
+
filename: string;
|
|
23
|
+
content: Buffer | Uint8Array;
|
|
24
|
+
contentType?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface EmailResult {
|
|
28
|
+
success: boolean;
|
|
29
|
+
messageId?: string;
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WhatsAppProvider {
|
|
34
|
+
send(message: WhatsAppMessage): Promise<WhatsAppResult>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WhatsAppMessage {
|
|
38
|
+
to: string; // E.164 phone number
|
|
39
|
+
template?: { name: string; language: string; parameters?: string[] };
|
|
40
|
+
text?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface WhatsAppResult {
|
|
44
|
+
success: boolean;
|
|
45
|
+
messageId?: string;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider interface for storage backends. Each provider implements
|
|
3
|
+
* the physical operations; the storage module handles bucket resolution,
|
|
4
|
+
* metadata tracking, and access control on top.
|
|
5
|
+
*/
|
|
6
|
+
export interface StorageProvider {
|
|
7
|
+
upload(fullKey: string, data: Buffer | Uint8Array, opts?: UploadOptions): Promise<void>;
|
|
8
|
+
download(fullKey: string): Promise<Uint8Array>;
|
|
9
|
+
delete(fullKey: string): Promise<void>;
|
|
10
|
+
exists(fullKey: string): Promise<boolean>;
|
|
11
|
+
presign(fullKey: string, opts: PresignOptions): string;
|
|
12
|
+
list(prefix: string, opts?: ListOptions): Promise<StorageListResult>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UploadOptions {
|
|
16
|
+
contentType?: string;
|
|
17
|
+
metadata?: Record<string, string>;
|
|
18
|
+
/** Max upload size in bytes. Enforced server-side for direct uploads. */
|
|
19
|
+
maxSize?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PresignOptions {
|
|
23
|
+
expiresIn?: number; // seconds, default 3600
|
|
24
|
+
method?: 'GET' | 'PUT'; // default 'GET'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ListOptions {
|
|
28
|
+
cursor?: string;
|
|
29
|
+
limit?: number; // default 100
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface StorageListResult {
|
|
33
|
+
objects: StorageObjectInfo[];
|
|
34
|
+
cursor?: string; // undefined = no more results
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface StorageObjectInfo {
|
|
38
|
+
key: string;
|
|
39
|
+
size: number;
|
|
40
|
+
contentType: string;
|
|
41
|
+
lastModified: Date;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Local filesystem provider */
|
|
45
|
+
export interface LocalProviderConfig {
|
|
46
|
+
type: 'local';
|
|
47
|
+
basePath: string; // e.g. './data/files'
|
|
48
|
+
baseUrl?: string; // for presign proxy URLs, default '/api/storage'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** S3-compatible provider (AWS, R2, MinIO) using Bun native S3 */
|
|
52
|
+
export interface S3ProviderConfig {
|
|
53
|
+
type: 's3';
|
|
54
|
+
bucket: string;
|
|
55
|
+
region?: string;
|
|
56
|
+
endpoint?: string; // for R2, MinIO
|
|
57
|
+
accessKeyId: string;
|
|
58
|
+
secretAccessKey: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type StorageProviderConfig = LocalProviderConfig | S3ProviderConfig;
|