@rebasepro/server-core 0.2.3 → 0.2.5
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/dist/common/src/collections/default-collections.d.ts +9 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/util/permissions.d.ts +1 -0
- package/dist/{index-BZoAtuqi.js → index-Cr1D21av.js} +11 -3
- package/dist/index-Cr1D21av.js.map +1 -0
- package/dist/index.es.js +2309 -370
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2298 -355
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/logs-routes.d.ts +37 -0
- package/dist/server-core/src/api/rest/api-generator.d.ts +6 -0
- package/dist/server-core/src/api/types.d.ts +6 -1
- package/dist/server-core/src/auth/adapter-middleware.d.ts +7 -3
- package/dist/server-core/src/auth/admin-routes.d.ts +3 -3
- package/dist/server-core/src/auth/api-keys/api-key-middleware.d.ts +39 -0
- package/dist/server-core/src/auth/api-keys/api-key-permission-guard.d.ts +32 -0
- package/dist/server-core/src/auth/api-keys/api-key-routes.d.ts +20 -0
- package/dist/server-core/src/auth/api-keys/api-key-store.d.ts +35 -0
- package/dist/server-core/src/auth/api-keys/api-key-types.d.ts +88 -0
- package/dist/server-core/src/auth/api-keys/index.d.ts +17 -0
- package/dist/server-core/src/auth/{auth-overrides.d.ts → auth-hooks.d.ts} +69 -11
- package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +3 -3
- package/dist/server-core/src/auth/index.d.ts +5 -3
- package/dist/server-core/src/auth/interfaces.d.ts +93 -3
- package/dist/server-core/src/auth/jwt.d.ts +3 -1
- package/dist/server-core/src/auth/mfa.d.ts +49 -0
- package/dist/server-core/src/auth/middleware.d.ts +7 -0
- package/dist/server-core/src/auth/rate-limiter.d.ts +19 -0
- package/dist/server-core/src/auth/routes.d.ts +3 -3
- package/dist/server-core/src/env.d.ts +6 -0
- package/dist/server-core/src/index.d.ts +1 -0
- package/dist/server-core/src/init.d.ts +3 -3
- package/dist/server-core/src/services/webhook-service.d.ts +29 -0
- package/dist/server-core/src/storage/image-transform.d.ts +48 -0
- package/dist/server-core/src/storage/index.d.ts +3 -0
- package/dist/server-core/src/storage/tus-handler.d.ts +51 -0
- package/dist/types/src/controllers/auth.d.ts +4 -26
- package/dist/types/src/controllers/client.d.ts +25 -43
- package/dist/types/src/controllers/collection_registry.d.ts +1 -1
- package/dist/types/src/controllers/data.d.ts +4 -0
- package/dist/types/src/controllers/data_driver.d.ts +23 -0
- package/dist/types/src/controllers/registry.d.ts +5 -4
- package/dist/types/src/rebase_context.d.ts +1 -1
- package/dist/types/src/types/auth_adapter.d.ts +5 -60
- package/dist/types/src/types/backend.d.ts +2 -2
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- package/dist/types/src/types/collections.d.ts +0 -4
- package/dist/types/src/types/component_ref.d.ts +1 -1
- package/dist/types/src/types/cron.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +1 -0
- package/dist/types/src/types/export_import.d.ts +1 -1
- package/dist/types/src/types/formex.d.ts +2 -2
- package/dist/types/src/types/properties.d.ts +9 -7
- package/dist/types/src/types/translations.d.ts +28 -12
- package/dist/types/src/types/user_management_delegate.d.ts +22 -57
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -1
- package/package.json +8 -6
- package/src/api/ast-schema-editor.ts +4 -4
- package/src/api/errors.ts +16 -7
- package/src/api/logs-routes.ts +129 -0
- package/src/api/rest/api-generator.ts +42 -2
- package/src/api/rest/query-parser.ts +37 -1
- package/src/api/types.ts +6 -1
- package/src/auth/adapter-middleware.ts +20 -4
- package/src/auth/admin-routes.ts +36 -100
- package/src/auth/api-keys/api-key-middleware.ts +126 -0
- package/src/auth/api-keys/api-key-permission-guard.ts +64 -0
- package/src/auth/api-keys/api-key-routes.ts +183 -0
- package/src/auth/api-keys/api-key-store.ts +317 -0
- package/src/auth/api-keys/api-key-types.ts +94 -0
- package/src/auth/api-keys/index.ts +37 -0
- package/src/auth/{auth-overrides.ts → auth-hooks.ts} +81 -14
- package/src/auth/builtin-auth-adapter.ts +35 -73
- package/src/auth/custom-auth-adapter.ts +1 -1
- package/src/auth/index.ts +7 -3
- package/src/auth/interfaces.ts +111 -3
- package/src/auth/jwt.ts +19 -5
- package/src/auth/mfa.ts +160 -0
- package/src/auth/middleware.ts +20 -1
- package/src/auth/rate-limiter.ts +92 -0
- package/src/auth/routes.ts +455 -24
- package/src/cron/cron-loader.ts +5 -10
- package/src/cron/cron-scheduler.ts +11 -12
- package/src/cron/cron-store.ts +8 -7
- package/src/env.ts +2 -0
- package/src/functions/function-loader.ts +6 -9
- package/src/index.ts +1 -2
- package/src/init.ts +56 -8
- package/src/serve-spa.ts +5 -4
- package/src/services/webhook-service.ts +155 -0
- package/src/storage/image-transform.ts +202 -0
- package/src/storage/index.ts +3 -0
- package/src/storage/routes.ts +56 -3
- package/src/storage/tus-handler.ts +315 -0
- package/src/utils/dev-port.ts +14 -0
- package/src/utils/logging.ts +9 -7
- package/test/admin-routes.test.ts +62 -164
- package/test/api-generator.test.ts +0 -1
- package/test/api-key-permission-guard.test.ts +132 -0
- package/test/ast-schema-editor.test.ts +26 -0
- package/test/auth-routes.test.ts +1 -2
- package/test/backend-hooks-admin.test.ts +3 -29
- package/test/custom-auth-adapter.test.ts +2 -10
- package/test/email-templates.test.ts +169 -0
- package/test/function-loader.test.ts +124 -0
- package/test/jwt.test.ts +4 -2
- package/test/mfa.test.ts +197 -0
- package/test/middleware.test.ts +10 -5
- package/test/webhook-service.test.ts +249 -0
- package/vite.config.ts +3 -2
- package/dist/index-BZoAtuqi.js.map +0 -1
- package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
- package/dist/types/src/users/roles.d.ts +0 -22
- package/src/bootstrappers/index.ts +0 -1
- package/src/singleton.test.ts +0 -28
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { MiddlewareHandler } from "hono";
|
|
3
|
+
import type { HonoEnv } from "./types";
|
|
4
|
+
|
|
5
|
+
export interface LogEntry {
|
|
6
|
+
id: string;
|
|
7
|
+
timestamp: string;
|
|
8
|
+
level: "debug" | "info" | "warn" | "error";
|
|
9
|
+
source: "api" | "auth" | "storage" | "realtime" | "system";
|
|
10
|
+
message: string;
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class LogRingBuffer {
|
|
15
|
+
private buffer: LogEntry[] = [];
|
|
16
|
+
private maxSize: number;
|
|
17
|
+
private idCounter = 0;
|
|
18
|
+
|
|
19
|
+
constructor(maxSize = 10000) {
|
|
20
|
+
this.maxSize = maxSize;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
push(entry: Omit<LogEntry, "id">): void {
|
|
24
|
+
const id = `log_${++this.idCounter}`;
|
|
25
|
+
this.buffer.push({ ...entry, id });
|
|
26
|
+
if (this.buffer.length > this.maxSize) {
|
|
27
|
+
this.buffer.shift();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
query(options: {
|
|
32
|
+
level?: string;
|
|
33
|
+
source?: string;
|
|
34
|
+
search?: string;
|
|
35
|
+
limit?: number;
|
|
36
|
+
offset?: number;
|
|
37
|
+
since?: string;
|
|
38
|
+
}): { entries: LogEntry[]; total: number } {
|
|
39
|
+
let filtered = this.buffer;
|
|
40
|
+
|
|
41
|
+
if (options.level) {
|
|
42
|
+
filtered = filtered.filter(e => e.level === options.level);
|
|
43
|
+
}
|
|
44
|
+
if (options.source) {
|
|
45
|
+
filtered = filtered.filter(e => e.source === options.source);
|
|
46
|
+
}
|
|
47
|
+
if (options.search) {
|
|
48
|
+
const searchLower = options.search.toLowerCase();
|
|
49
|
+
filtered = filtered.filter(e => e.message.toLowerCase().includes(searchLower));
|
|
50
|
+
}
|
|
51
|
+
if (options.since) {
|
|
52
|
+
const sinceValue = options.since;
|
|
53
|
+
filtered = filtered.filter(e => e.timestamp >= sinceValue);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Newest first
|
|
57
|
+
const sorted = [...filtered].reverse();
|
|
58
|
+
const total = sorted.length;
|
|
59
|
+
const limit = options.limit || 100;
|
|
60
|
+
const offset = options.offset || 0;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
entries: sorted.slice(offset, offset + limit),
|
|
64
|
+
total
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getLatest(count = 50): LogEntry[] {
|
|
69
|
+
return this.buffer.slice(-count).reverse();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Global singleton
|
|
74
|
+
export const logBuffer = new LogRingBuffer();
|
|
75
|
+
|
|
76
|
+
/** Add a log entry */
|
|
77
|
+
export function addLog(
|
|
78
|
+
level: LogEntry["level"],
|
|
79
|
+
source: LogEntry["source"],
|
|
80
|
+
message: string,
|
|
81
|
+
metadata?: Record<string, unknown>
|
|
82
|
+
): void {
|
|
83
|
+
logBuffer.push({
|
|
84
|
+
timestamp: new Date().toISOString(),
|
|
85
|
+
level,
|
|
86
|
+
source,
|
|
87
|
+
message,
|
|
88
|
+
metadata
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Hono middleware to log API requests */
|
|
93
|
+
export function logMiddleware(): MiddlewareHandler<HonoEnv> {
|
|
94
|
+
return async (c, next) => {
|
|
95
|
+
const start = Date.now();
|
|
96
|
+
await next();
|
|
97
|
+
const duration = Date.now() - start;
|
|
98
|
+
addLog("info", "api", `${c.req.method} ${c.req.path} ${c.res.status} ${duration}ms`, {
|
|
99
|
+
method: c.req.method,
|
|
100
|
+
path: c.req.path,
|
|
101
|
+
status: c.res.status,
|
|
102
|
+
duration
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const app = new Hono<HonoEnv>();
|
|
108
|
+
|
|
109
|
+
// GET /api/logs — Query logs
|
|
110
|
+
app.get("/", (c) => {
|
|
111
|
+
const query = c.req.query();
|
|
112
|
+
const result = logBuffer.query({
|
|
113
|
+
level: query.level,
|
|
114
|
+
source: query.source,
|
|
115
|
+
search: query.search,
|
|
116
|
+
limit: query.limit ? parseInt(query.limit) : undefined,
|
|
117
|
+
offset: query.offset ? parseInt(query.offset) : undefined,
|
|
118
|
+
since: query.since
|
|
119
|
+
});
|
|
120
|
+
return c.json(result);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// GET /api/logs/latest — Get latest logs (for real-time)
|
|
124
|
+
app.get("/latest", (c) => {
|
|
125
|
+
const count = parseInt(c.req.query("count") || "50");
|
|
126
|
+
return c.json({ entries: logBuffer.getLatest(count) });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export default app;
|
|
@@ -3,6 +3,8 @@ import { DataDriver, Entity, EntityCollection, FetchCollectionProps, DataHooks,
|
|
|
3
3
|
import { QueryOptions, HonoEnv } from "../types";
|
|
4
4
|
import { ApiError } from "../errors";
|
|
5
5
|
import { parseQueryOptions } from "./query-parser";
|
|
6
|
+
import { httpMethodToOperation, isOperationAllowed } from "../../auth/api-keys/api-key-permission-guard";
|
|
7
|
+
import type { ApiKeyMasked } from "../../auth/api-keys/api-key-types";
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
/**
|
|
@@ -47,6 +49,27 @@ export class RestApiGenerator {
|
|
|
47
49
|
return this.router;
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Check API key permissions for a collection operation.
|
|
54
|
+
* Throws 403 if the key doesn't have the required permission.
|
|
55
|
+
* No-ops if the request is not authenticated via an API key.
|
|
56
|
+
*/
|
|
57
|
+
private enforceApiKeyPermission(
|
|
58
|
+
c: { get: (key: string) => unknown; req: { method: string } },
|
|
59
|
+
collectionSlug: string,
|
|
60
|
+
): void {
|
|
61
|
+
const apiKey = c.get("apiKey") as ApiKeyMasked | undefined;
|
|
62
|
+
if (!apiKey) return; // Not an API key request — skip
|
|
63
|
+
|
|
64
|
+
const operation = httpMethodToOperation(c.req.method);
|
|
65
|
+
if (!isOperationAllowed(apiKey.permissions, collectionSlug, operation)) {
|
|
66
|
+
throw ApiError.forbidden(
|
|
67
|
+
`API key does not have "${operation}" permission for collection "${collectionSlug}"`,
|
|
68
|
+
"API_KEY_FORBIDDEN",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
50
73
|
/**
|
|
51
74
|
* Get the typed RestFetchService from a driver if it exposes one (for include support).
|
|
52
75
|
*/
|
|
@@ -63,6 +86,7 @@ export class RestApiGenerator {
|
|
|
63
86
|
|
|
64
87
|
// GET /collection/count - Count entities (with optional filters)
|
|
65
88
|
this.router.get(`${basePath}/count`, async (c) => {
|
|
89
|
+
this.enforceApiKeyPermission(c, collection.slug);
|
|
66
90
|
const queryDict = c.req.query();
|
|
67
91
|
const queryOptions = parseQueryOptions(queryDict);
|
|
68
92
|
const searchString = queryDict.searchString as string | undefined;
|
|
@@ -74,6 +98,7 @@ export class RestApiGenerator {
|
|
|
74
98
|
|
|
75
99
|
// GET /collection - List entities
|
|
76
100
|
this.router.get(basePath, async (c) => {
|
|
101
|
+
this.enforceApiKeyPermission(c, collection.slug);
|
|
77
102
|
const queryDict = c.req.query();
|
|
78
103
|
const queryOptions = parseQueryOptions(queryDict);
|
|
79
104
|
const searchString = queryDict.searchString as string | undefined;
|
|
@@ -93,7 +118,8 @@ export class RestApiGenerator {
|
|
|
93
118
|
offset: queryOptions.offset,
|
|
94
119
|
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
95
120
|
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
96
|
-
searchString
|
|
121
|
+
searchString,
|
|
122
|
+
vectorSearch: queryOptions.vectorSearch
|
|
97
123
|
},
|
|
98
124
|
queryOptions.include
|
|
99
125
|
);
|
|
@@ -133,6 +159,7 @@ export class RestApiGenerator {
|
|
|
133
159
|
|
|
134
160
|
// GET /collection/:id - Get single entity
|
|
135
161
|
this.router.get(`${basePath}/:id`, async (c) => {
|
|
162
|
+
this.enforceApiKeyPermission(c, collection.slug);
|
|
136
163
|
const id = c.req.param("id");
|
|
137
164
|
const queryDict = c.req.query();
|
|
138
165
|
const queryOptions = parseQueryOptions(queryDict);
|
|
@@ -179,6 +206,7 @@ export class RestApiGenerator {
|
|
|
179
206
|
// POST /collection - Create entity
|
|
180
207
|
this.router.post(basePath, async (c) => {
|
|
181
208
|
try {
|
|
209
|
+
this.enforceApiKeyPermission(c, collection.slug);
|
|
182
210
|
const driver = c.get("driver") || this.driver;
|
|
183
211
|
const path = collection.slug;
|
|
184
212
|
const hookCtx = this.buildHookContext(c, "POST");
|
|
@@ -215,6 +243,7 @@ export class RestApiGenerator {
|
|
|
215
243
|
// PUT /collection/:id - Update entity
|
|
216
244
|
this.router.put(`${basePath}/:id`, async (c) => {
|
|
217
245
|
try {
|
|
246
|
+
this.enforceApiKeyPermission(c, collection.slug);
|
|
218
247
|
const id = c.req.param("id");
|
|
219
248
|
const driver = c.get("driver") || this.driver;
|
|
220
249
|
const hookCtx = this.buildHookContext(c, "PUT");
|
|
@@ -261,6 +290,7 @@ export class RestApiGenerator {
|
|
|
261
290
|
|
|
262
291
|
// DELETE /collection/:id - Delete entity
|
|
263
292
|
this.router.delete(`${basePath}/:id`, async (c) => {
|
|
293
|
+
this.enforceApiKeyPermission(c, collection.slug);
|
|
264
294
|
const id = c.req.param("id");
|
|
265
295
|
const driver = c.get("driver") || this.driver;
|
|
266
296
|
const hookCtx = this.buildHookContext(c, "DELETE");
|
|
@@ -354,6 +384,8 @@ entityId };
|
|
|
354
384
|
|
|
355
385
|
const driver = c.get("driver") || this.driver;
|
|
356
386
|
|
|
387
|
+
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
388
|
+
|
|
357
389
|
if (parsed.entityId === "count") {
|
|
358
390
|
// GET /parent/:parentId/child/count — count child entities
|
|
359
391
|
const queryDict = c.req.query();
|
|
@@ -407,6 +439,8 @@ entityId };
|
|
|
407
439
|
if (!parsed || parsed.entityId) return next();
|
|
408
440
|
|
|
409
441
|
const driver = c.get("driver") || this.driver;
|
|
442
|
+
|
|
443
|
+
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
410
444
|
const body = await c.req.json().catch(() => ({}));
|
|
411
445
|
|
|
412
446
|
const entity = await driver.saveEntity({
|
|
@@ -427,6 +461,9 @@ entityId };
|
|
|
427
461
|
if (!parsed || !parsed.entityId) return next();
|
|
428
462
|
|
|
429
463
|
const driver = c.get("driver") || this.driver;
|
|
464
|
+
|
|
465
|
+
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
466
|
+
|
|
430
467
|
const body = await c.req.json().catch(() => ({}));
|
|
431
468
|
|
|
432
469
|
const entity = await driver.saveEntity({
|
|
@@ -449,6 +486,8 @@ entityId };
|
|
|
449
486
|
|
|
450
487
|
const driver = c.get("driver") || this.driver;
|
|
451
488
|
|
|
489
|
+
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
490
|
+
|
|
452
491
|
const existingEntity = await driver.fetchEntity({
|
|
453
492
|
path: parsed.collectionPath,
|
|
454
493
|
entityId: parsed.entityId
|
|
@@ -520,7 +559,8 @@ entityId };
|
|
|
520
559
|
orderBy: queryOptions.orderBy?.[0]?.field,
|
|
521
560
|
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
522
561
|
startAfter: queryOptions.offset ? String(queryOptions.offset) : undefined,
|
|
523
|
-
searchString
|
|
562
|
+
searchString,
|
|
563
|
+
vectorSearch: queryOptions.vectorSearch
|
|
524
564
|
});
|
|
525
565
|
|
|
526
566
|
return entities.map(entity => this.flattenEntity(entity));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { VectorSearchParams } from "@rebasepro/types";
|
|
1
2
|
import { QueryOptions } from "../types";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -39,7 +40,7 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
|
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
// PostgREST-style filtering: ?field=op.value
|
|
42
|
-
const reservedQueryKeys = ["limit", "offset", "page", "orderBy", "include", "fields", "searchString"];
|
|
43
|
+
const reservedQueryKeys = ["limit", "offset", "page", "orderBy", "include", "fields", "searchString", "vector_search", "vector", "vector_distance", "vector_threshold"];
|
|
43
44
|
for (const [key, rawValue] of Object.entries(query)) {
|
|
44
45
|
if (reservedQueryKeys.includes(key)) continue;
|
|
45
46
|
|
|
@@ -133,5 +134,40 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
|
|
|
133
134
|
options.fields = fieldsStr.split(",").map(s => s.trim()).filter(Boolean);
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
// Vector similarity search
|
|
138
|
+
if (query.vector_search && query.vector) {
|
|
139
|
+
const vectorStr = String(query.vector);
|
|
140
|
+
let queryVector: number[];
|
|
141
|
+
try {
|
|
142
|
+
queryVector = JSON.parse(vectorStr) as number[];
|
|
143
|
+
if (!Array.isArray(queryVector) || !queryVector.every(v => typeof v === "number")) {
|
|
144
|
+
throw new Error("Expected array of numbers");
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
throw new Error("Invalid vector format. Expected JSON array of numbers, e.g. [0.1,0.2,0.3]");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const distanceParam = query.vector_distance ? String(query.vector_distance) : "cosine";
|
|
151
|
+
if (distanceParam !== "cosine" && distanceParam !== "l2" && distanceParam !== "inner_product") {
|
|
152
|
+
throw new Error(`Invalid vector_distance: ${distanceParam}. Expected: cosine, l2, or inner_product`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const vectorSearch: VectorSearchParams = {
|
|
156
|
+
property: String(query.vector_search),
|
|
157
|
+
vector: queryVector,
|
|
158
|
+
distance: distanceParam,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
if (query.vector_threshold) {
|
|
162
|
+
const threshold = parseFloat(String(query.vector_threshold));
|
|
163
|
+
if (isNaN(threshold)) {
|
|
164
|
+
throw new Error("Invalid vector_threshold. Expected a number.");
|
|
165
|
+
}
|
|
166
|
+
vectorSearch.threshold = threshold;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
options.vectorSearch = vectorSearch;
|
|
170
|
+
}
|
|
171
|
+
|
|
136
172
|
return options;
|
|
137
173
|
}
|
package/src/api/types.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { EntityCollection } from "@rebasepro/types";
|
|
1
|
+
import { EntityCollection, VectorSearchParams } from "@rebasepro/types";
|
|
2
2
|
import { AuthResult } from "../auth/middleware";
|
|
3
3
|
import { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
4
4
|
import { DataDriver } from "@rebasepro/types";
|
|
5
|
+
import type { ApiKeyMasked } from "../auth/api-keys/api-key-types";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Hono Environment Variables
|
|
@@ -11,6 +12,8 @@ export type HonoEnv = {
|
|
|
11
12
|
Variables: {
|
|
12
13
|
user?: AuthResult | { userId?: string, roles?: string[] };
|
|
13
14
|
driver?: DataDriver;
|
|
15
|
+
/** Set when the request is authenticated via a Service API Key. */
|
|
16
|
+
apiKey?: ApiKeyMasked;
|
|
14
17
|
}
|
|
15
18
|
};
|
|
16
19
|
|
|
@@ -78,6 +81,8 @@ export interface QueryOptions {
|
|
|
78
81
|
include?: string[];
|
|
79
82
|
/** Columns to return in the response (field-level selection) */
|
|
80
83
|
fields?: string[];
|
|
84
|
+
/** Vector similarity search configuration */
|
|
85
|
+
vectorSearch?: VectorSearchParams;
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
/**
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* an `AuthAdapter` to `initializeRebaseBackend()`.
|
|
7
7
|
*
|
|
8
8
|
* The middleware:
|
|
9
|
-
* 1.
|
|
10
|
-
* 2.
|
|
11
|
-
* 3.
|
|
9
|
+
* 1. Checks for API key tokens (`rk_` prefix) first — these are Rebase-level
|
|
10
|
+
* 2. Falls back to `adapter.verifyRequest(request)` to resolve the user
|
|
11
|
+
* 3. Scopes the DataDriver via `withAuth()` for RLS
|
|
12
|
+
* 4. Enforces auth (401) when `requireAuth` is true and no user is found
|
|
12
13
|
*
|
|
13
14
|
* The behavior is identical to `createAuthMiddleware()` — only the
|
|
14
15
|
* token verification strategy is pluggable.
|
|
@@ -17,7 +18,9 @@
|
|
|
17
18
|
import type { MiddlewareHandler } from "hono";
|
|
18
19
|
import type { DataDriver, AuthAdapter } from "@rebasepro/types";
|
|
19
20
|
import type { HonoEnv } from "../api/types";
|
|
21
|
+
import type { ApiKeyStore } from "./api-keys/api-key-store";
|
|
20
22
|
import { scopeDataDriver } from "./rls-scope";
|
|
23
|
+
import { validateApiKey } from "./api-keys/api-key-middleware";
|
|
21
24
|
|
|
22
25
|
export interface AdapterAuthMiddlewareOptions {
|
|
23
26
|
/** The auth adapter to delegate verification to. */
|
|
@@ -29,15 +32,28 @@ export interface AdapterAuthMiddlewareOptions {
|
|
|
29
32
|
* Defaults to `true` (secure by default).
|
|
30
33
|
*/
|
|
31
34
|
requireAuth?: boolean;
|
|
35
|
+
/** Optional API key store — when provided, `rk_` bearer tokens are accepted. */
|
|
36
|
+
apiKeyStore?: ApiKeyStore;
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
/**
|
|
35
40
|
* Create a Hono middleware that uses an `AuthAdapter` for request verification.
|
|
36
41
|
*/
|
|
37
42
|
export function createAdapterAuthMiddleware(options: AdapterAuthMiddlewareOptions): MiddlewareHandler<HonoEnv> {
|
|
38
|
-
const { adapter, driver, requireAuth: enforceAuth = true } = options;
|
|
43
|
+
const { adapter, driver, requireAuth: enforceAuth = true, apiKeyStore } = options;
|
|
39
44
|
|
|
40
45
|
return async (c, next) => {
|
|
46
|
+
// ── API Key check (Rebase-level, independent of auth adapter) ────
|
|
47
|
+
if (apiKeyStore) {
|
|
48
|
+
const authHeader = c.req.header("authorization") || "";
|
|
49
|
+
const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
50
|
+
if (token.startsWith("rk_")) {
|
|
51
|
+
const result = await validateApiKey(c, token, { store: apiKeyStore, driver });
|
|
52
|
+
if (result === true) return next();
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
let authenticatedUser = null;
|
|
42
58
|
|
|
43
59
|
try {
|
package/src/auth/admin-routes.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { Hono } from "hono";
|
|
|
2
2
|
import { ApiError, errorHandler } from "../api/errors";
|
|
3
3
|
import type { AuthRepository } from "./interfaces";
|
|
4
4
|
import { requireAuth, requireAdmin, createRequireAuth } from "./middleware";
|
|
5
|
-
import type {
|
|
6
|
-
import {
|
|
5
|
+
import type { AuthHooks } from "./auth-hooks";
|
|
6
|
+
import { resolveAuthHooks } from "./auth-hooks";
|
|
7
7
|
import { AuthModuleConfig } from "./routes";
|
|
8
|
-
import type { BackendHooks, AdminUser,
|
|
8
|
+
import type { BackendHooks, AdminUser, BackendHookContext } from "@rebasepro/types";
|
|
9
9
|
|
|
10
10
|
interface AdminRouteOptions extends AuthModuleConfig {
|
|
11
11
|
serviceKey?: string;
|
|
@@ -19,10 +19,10 @@ interface AdminRouteOptions extends AuthModuleConfig {
|
|
|
19
19
|
*/
|
|
20
20
|
hooks?: BackendHooks;
|
|
21
21
|
/**
|
|
22
|
-
* Auth
|
|
22
|
+
* Auth hooks for customizing password hashing, credential
|
|
23
23
|
* verification, lifecycle hooks, etc.
|
|
24
24
|
*/
|
|
25
|
-
|
|
25
|
+
authHooks?: AuthHooks;
|
|
26
26
|
}
|
|
27
27
|
import { HonoEnv } from "../api/types";
|
|
28
28
|
import { randomBytes, createHash } from "crypto";
|
|
@@ -74,8 +74,8 @@ function hashToken(token: string): string {
|
|
|
74
74
|
export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
|
|
75
75
|
const router = new Hono<HonoEnv>();
|
|
76
76
|
const authRepo = config.authRepo;
|
|
77
|
-
const { emailService, emailConfig, hooks,
|
|
78
|
-
const ops =
|
|
77
|
+
const { emailService, emailConfig, hooks, authHooks } = config;
|
|
78
|
+
const ops = resolveAuthHooks(authHooks);
|
|
79
79
|
|
|
80
80
|
/** Build a BackendHookContext from Hono's context object */
|
|
81
81
|
function buildHookContext(c: { get: (key: string) => unknown }, method: BackendHookContext["method"]): BackendHookContext {
|
|
@@ -99,12 +99,7 @@ export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
|
|
|
99
99
|
return results.filter((u): u is AdminUser => u !== null);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
async function applyRoleAfterReadBatch(roles: AdminRole[], ctx: BackendHookContext): Promise<AdminRole[]> {
|
|
104
|
-
if (!hooks?.roles?.afterRead) return roles;
|
|
105
|
-
const results = await Promise.all(roles.map(r => hooks!.roles!.afterRead!(r, ctx)));
|
|
106
|
-
return results.filter((r): r is AdminRole => r !== null);
|
|
107
|
-
}
|
|
102
|
+
|
|
108
103
|
|
|
109
104
|
/** Convert a DB user record + role IDs into the AdminUser API shape */
|
|
110
105
|
function toAdminUser(u: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; createdAt?: Date | string; updatedAt?: Date | string }, roles: string[]): AdminUser {
|
|
@@ -174,6 +169,10 @@ export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
|
|
|
174
169
|
if (!userId) {
|
|
175
170
|
throw ApiError.unauthorized("User ID not found in auth context");
|
|
176
171
|
}
|
|
172
|
+
const caller = await authRepo.getUserById(userId);
|
|
173
|
+
if (!caller) {
|
|
174
|
+
throw ApiError.notFound("Authenticated user does not exist in the database. Please sign out and sign in/register again.", "USER_NOT_FOUND");
|
|
175
|
+
}
|
|
177
176
|
await authRepo.setUserRoles(userId, ["admin"]);
|
|
178
177
|
|
|
179
178
|
// ── Set persistent flag ──────────────────────────────────────
|
|
@@ -456,6 +455,19 @@ displayName: existing.displayName }, appName);
|
|
|
456
455
|
}
|
|
457
456
|
|
|
458
457
|
if (roles !== undefined && Array.isArray(roles)) {
|
|
458
|
+
const currentRoles = await authRepo.getUserRoleIds(userId);
|
|
459
|
+
const wasAdmin = currentRoles.includes("admin");
|
|
460
|
+
const willBeAdmin = roles.includes("admin");
|
|
461
|
+
|
|
462
|
+
if (wasAdmin && !willBeAdmin) {
|
|
463
|
+
const adminUsers = await authRepo.listUsersPaginated({
|
|
464
|
+
roleId: "admin",
|
|
465
|
+
limit: 1
|
|
466
|
+
});
|
|
467
|
+
if (adminUsers.total <= 1) {
|
|
468
|
+
throw ApiError.forbidden("Cannot demote the last administrator", "LAST_ADMIN");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
459
471
|
await authRepo.setUserRoles(userId, roles);
|
|
460
472
|
}
|
|
461
473
|
|
|
@@ -487,6 +499,17 @@ displayName: existing.displayName }, appName);
|
|
|
487
499
|
throw ApiError.notFound("User not found");
|
|
488
500
|
}
|
|
489
501
|
|
|
502
|
+
const roles = await authRepo.getUserRoleIds(userId);
|
|
503
|
+
if (roles.includes("admin")) {
|
|
504
|
+
const adminUsers = await authRepo.listUsersPaginated({
|
|
505
|
+
roleId: "admin",
|
|
506
|
+
limit: 1
|
|
507
|
+
});
|
|
508
|
+
if (adminUsers.total <= 1) {
|
|
509
|
+
throw ApiError.forbidden("Cannot delete the last administrator", "LAST_ADMIN");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
490
513
|
// Apply beforeDelete hook (throw to abort)
|
|
491
514
|
const hookCtx = buildHookContext(c, "DELETE");
|
|
492
515
|
if (hooks?.users?.beforeDelete) {
|
|
@@ -505,94 +528,7 @@ displayName: existing.displayName }, appName);
|
|
|
505
528
|
return c.json({ success: true });
|
|
506
529
|
});
|
|
507
530
|
|
|
508
|
-
router.get("/roles", requireAdmin, async (c) => {
|
|
509
|
-
const roles = await authRepo.listRoles();
|
|
510
|
-
const hookCtx = buildHookContext(c, "GET");
|
|
511
|
-
|
|
512
|
-
let adminRoles: AdminRole[] = roles.map(r => ({
|
|
513
|
-
id: r.id,
|
|
514
|
-
name: r.name,
|
|
515
|
-
isAdmin: r.isAdmin,
|
|
516
|
-
defaultPermissions: r.defaultPermissions,
|
|
517
|
-
config: r.config
|
|
518
|
-
}));
|
|
519
|
-
|
|
520
|
-
adminRoles = await applyRoleAfterReadBatch(adminRoles, hookCtx);
|
|
521
|
-
|
|
522
|
-
return c.json({ roles: adminRoles });
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
router.get("/roles/:roleId", requireAdmin, async (c) => {
|
|
526
|
-
const roleId = c.req.param("roleId");
|
|
527
|
-
const role = await authRepo.getRoleById(roleId);
|
|
528
|
-
|
|
529
|
-
if (!role) {
|
|
530
|
-
throw ApiError.notFound("Role not found");
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
return c.json({ role });
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
router.post("/roles", requireAdmin, async (c) => {
|
|
537
|
-
const body = await c.req.json();
|
|
538
|
-
const { id, name, isAdmin, defaultPermissions, config } = body;
|
|
539
|
-
|
|
540
|
-
if (!id || !name) {
|
|
541
|
-
throw ApiError.badRequest("Role ID and name are required", "INVALID_INPUT");
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const existing = await authRepo.getRoleById(id);
|
|
545
|
-
if (existing) {
|
|
546
|
-
throw ApiError.conflict("Role already exists", "ROLE_EXISTS");
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
const role = await authRepo.createRole({
|
|
550
|
-
id,
|
|
551
|
-
name,
|
|
552
|
-
isAdmin: isAdmin ?? false,
|
|
553
|
-
defaultPermissions: defaultPermissions ?? null,
|
|
554
|
-
config: config ?? null
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
return c.json({ role }, 201);
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
router.put("/roles/:roleId", requireAdmin, async (c) => {
|
|
561
|
-
const roleId = c.req.param("roleId");
|
|
562
|
-
const body = await c.req.json();
|
|
563
|
-
const { name, isAdmin, defaultPermissions, config } = body;
|
|
564
|
-
|
|
565
|
-
const existing = await authRepo.getRoleById(roleId);
|
|
566
|
-
if (!existing) {
|
|
567
|
-
throw ApiError.notFound("Role not found");
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
const role = await authRepo.updateRole(roleId, {
|
|
571
|
-
name,
|
|
572
|
-
isAdmin,
|
|
573
|
-
defaultPermissions,
|
|
574
|
-
config
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
return c.json({ role });
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
router.delete("/roles/:roleId", requireAdmin, async (c) => {
|
|
581
|
-
const roleId = c.req.param("roleId");
|
|
582
|
-
|
|
583
|
-
if (["admin", "editor", "viewer"].includes(roleId)) {
|
|
584
|
-
throw ApiError.badRequest("Cannot delete built-in roles", "BUILTIN_ROLE");
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
const existing = await authRepo.getRoleById(roleId);
|
|
588
|
-
if (!existing) {
|
|
589
|
-
throw ApiError.notFound("Role not found");
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
await authRepo.deleteRole(roleId);
|
|
593
531
|
|
|
594
|
-
return c.json({ success: true });
|
|
595
|
-
});
|
|
596
532
|
|
|
597
533
|
return router;
|
|
598
534
|
}
|