@rebasepro/server-core 0.2.3 → 0.2.4

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 (109) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +12 -0
  2. package/dist/common/src/collections/index.d.ts +1 -0
  3. package/dist/common/src/util/permissions.d.ts +1 -0
  4. package/dist/{index-BZoAtuqi.js → index-Cr1D21av.js} +11 -3
  5. package/dist/index-Cr1D21av.js.map +1 -0
  6. package/dist/index.es.js +2166 -208
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +2155 -193
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/server-core/src/api/logs-routes.d.ts +37 -0
  11. package/dist/server-core/src/api/rest/api-generator.d.ts +6 -0
  12. package/dist/server-core/src/api/types.d.ts +6 -1
  13. package/dist/server-core/src/auth/adapter-middleware.d.ts +7 -3
  14. package/dist/server-core/src/auth/admin-routes.d.ts +3 -3
  15. package/dist/server-core/src/auth/api-keys/api-key-middleware.d.ts +39 -0
  16. package/dist/server-core/src/auth/api-keys/api-key-permission-guard.d.ts +32 -0
  17. package/dist/server-core/src/auth/api-keys/api-key-routes.d.ts +20 -0
  18. package/dist/server-core/src/auth/api-keys/api-key-store.d.ts +35 -0
  19. package/dist/server-core/src/auth/api-keys/api-key-types.d.ts +88 -0
  20. package/dist/server-core/src/auth/api-keys/index.d.ts +17 -0
  21. package/dist/server-core/src/auth/{auth-overrides.d.ts → auth-hooks.d.ts} +69 -11
  22. package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +3 -3
  23. package/dist/server-core/src/auth/index.d.ts +5 -3
  24. package/dist/server-core/src/auth/interfaces.d.ts +93 -3
  25. package/dist/server-core/src/auth/jwt.d.ts +3 -1
  26. package/dist/server-core/src/auth/mfa.d.ts +49 -0
  27. package/dist/server-core/src/auth/middleware.d.ts +7 -0
  28. package/dist/server-core/src/auth/rate-limiter.d.ts +19 -0
  29. package/dist/server-core/src/auth/routes.d.ts +3 -3
  30. package/dist/server-core/src/env.d.ts +6 -0
  31. package/dist/server-core/src/index.d.ts +1 -0
  32. package/dist/server-core/src/init.d.ts +3 -3
  33. package/dist/server-core/src/services/webhook-service.d.ts +29 -0
  34. package/dist/server-core/src/storage/image-transform.d.ts +48 -0
  35. package/dist/server-core/src/storage/index.d.ts +3 -0
  36. package/dist/server-core/src/storage/tus-handler.d.ts +51 -0
  37. package/dist/types/src/controllers/auth.d.ts +2 -24
  38. package/dist/types/src/controllers/client.d.ts +0 -3
  39. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  40. package/dist/types/src/controllers/data_driver.d.ts +18 -0
  41. package/dist/types/src/controllers/registry.d.ts +5 -4
  42. package/dist/types/src/rebase_context.d.ts +1 -1
  43. package/dist/types/src/types/auth_adapter.d.ts +2 -4
  44. package/dist/types/src/types/collections.d.ts +0 -4
  45. package/dist/types/src/types/component_ref.d.ts +1 -1
  46. package/dist/types/src/types/cron.d.ts +1 -1
  47. package/dist/types/src/types/entity_views.d.ts +1 -0
  48. package/dist/types/src/types/export_import.d.ts +1 -1
  49. package/dist/types/src/types/formex.d.ts +2 -2
  50. package/dist/types/src/types/properties.d.ts +2 -2
  51. package/dist/types/src/types/translations.d.ts +28 -12
  52. package/dist/types/src/types/user_management_delegate.d.ts +6 -4
  53. package/dist/types/src/users/roles.d.ts +0 -8
  54. package/package.json +8 -6
  55. package/src/api/ast-schema-editor.ts +4 -4
  56. package/src/api/errors.ts +16 -7
  57. package/src/api/logs-routes.ts +129 -0
  58. package/src/api/rest/api-generator.ts +42 -2
  59. package/src/api/rest/query-parser.ts +37 -1
  60. package/src/api/types.ts +6 -1
  61. package/src/auth/adapter-middleware.ts +20 -4
  62. package/src/auth/admin-routes.ts +39 -14
  63. package/src/auth/api-keys/api-key-middleware.ts +126 -0
  64. package/src/auth/api-keys/api-key-permission-guard.ts +64 -0
  65. package/src/auth/api-keys/api-key-routes.ts +183 -0
  66. package/src/auth/api-keys/api-key-store.ts +317 -0
  67. package/src/auth/api-keys/api-key-types.ts +94 -0
  68. package/src/auth/api-keys/index.ts +37 -0
  69. package/src/auth/{auth-overrides.ts → auth-hooks.ts} +81 -14
  70. package/src/auth/builtin-auth-adapter.ts +31 -19
  71. package/src/auth/index.ts +7 -3
  72. package/src/auth/interfaces.ts +111 -3
  73. package/src/auth/jwt.ts +19 -5
  74. package/src/auth/mfa.ts +160 -0
  75. package/src/auth/middleware.ts +20 -1
  76. package/src/auth/rate-limiter.ts +92 -0
  77. package/src/auth/routes.ts +455 -24
  78. package/src/cron/cron-loader.ts +5 -10
  79. package/src/cron/cron-scheduler.ts +11 -12
  80. package/src/cron/cron-store.ts +8 -7
  81. package/src/env.ts +2 -0
  82. package/src/functions/function-loader.ts +6 -9
  83. package/src/index.ts +1 -2
  84. package/src/init.ts +37 -7
  85. package/src/serve-spa.ts +5 -4
  86. package/src/services/webhook-service.ts +155 -0
  87. package/src/storage/image-transform.ts +202 -0
  88. package/src/storage/index.ts +3 -0
  89. package/src/storage/routes.ts +56 -3
  90. package/src/storage/tus-handler.ts +315 -0
  91. package/src/utils/dev-port.ts +14 -0
  92. package/src/utils/logging.ts +9 -7
  93. package/test/admin-routes.test.ts +74 -7
  94. package/test/api-generator.test.ts +0 -1
  95. package/test/api-key-permission-guard.test.ts +132 -0
  96. package/test/ast-schema-editor.test.ts +26 -0
  97. package/test/auth-routes.test.ts +1 -2
  98. package/test/backend-hooks-admin.test.ts +3 -4
  99. package/test/email-templates.test.ts +169 -0
  100. package/test/function-loader.test.ts +124 -0
  101. package/test/jwt.test.ts +4 -2
  102. package/test/mfa.test.ts +197 -0
  103. package/test/middleware.test.ts +10 -5
  104. package/test/webhook-service.test.ts +249 -0
  105. package/vite.config.ts +3 -2
  106. package/dist/index-BZoAtuqi.js.map +0 -1
  107. package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
  108. package/src/bootstrappers/index.ts +0 -1
  109. package/src/singleton.test.ts +0 -28
@@ -105,6 +105,12 @@ export interface RebaseTranslations {
105
105
  navigation_drawer: string;
106
106
  collapse: string;
107
107
  expand: string;
108
+ /** Tooltip for the language switcher in the drawer footer */
109
+ change_language?: string;
110
+ /** Tooltip for the theme toggle in the drawer footer */
111
+ toggle_theme?: string;
112
+ /** Aria label for the user menu trigger in the drawer footer */
113
+ user_menu?: string;
108
114
  error: string;
109
115
  error_uploading_file: string;
110
116
  error_deleting: string;
@@ -426,18 +432,28 @@ export interface RebaseTranslations {
426
432
  deleted: string;
427
433
  select_reference: string;
428
434
  select_references: string;
429
- account_settings: string;
430
- profile: string;
431
- sessions: string;
432
- display_name: string;
433
- photo_url: string;
434
- save_profile: string;
435
- saving: string;
436
- no_active_sessions: string;
437
- revoking: string;
438
- revoke_all_sessions: string;
439
- unknown_device: string;
440
- current: string;
435
+ account_settings?: string;
436
+ profile?: string;
437
+ sessions?: string;
438
+ security?: string;
439
+ change_password?: string;
440
+ current_password?: string;
441
+ new_password?: string;
442
+ confirm_password?: string;
443
+ password_changed?: string;
444
+ passwords_dont_match?: string;
445
+ password_too_short?: string;
446
+ password_change_not_available?: string;
447
+ changing_password?: string;
448
+ display_name?: string;
449
+ photo_url?: string;
450
+ save_profile?: string;
451
+ saving?: string;
452
+ no_active_sessions?: string;
453
+ revoking?: string;
454
+ revoke_all_sessions?: string;
455
+ unknown_device?: string;
456
+ current?: string;
441
457
  role_id: string;
442
458
  role_name: string;
443
459
  add_reference: string;
@@ -104,15 +104,17 @@ export interface UserManagementDelegate<USER extends User = User> {
104
104
  * If true, the UI will allow the user to create the default roles (admin, editor, viewer).
105
105
  */
106
106
  allowDefaultRolesCreation?: boolean;
107
- /**
108
- * Should collection config permissions be included?
109
- */
110
- includeCollectionConfigPermissions?: boolean;
111
107
  /**
112
108
  * Optionally define roles for a given user. This is useful when the roles
113
109
  * are coming from a separate provider than the one issuing the tokens.
114
110
  */
115
111
  defineRolesFor?: (user: USER) => Promise<Role[] | undefined> | Role[] | undefined;
112
+ /**
113
+ * Whether any admin users exist. Used by the bootstrap banner to decide
114
+ * whether to prompt. Populated via a lightweight check (e.g. `limit=1`
115
+ * query) instead of loading all users.
116
+ */
117
+ hasAdminUsers?: boolean;
116
118
  /**
117
119
  * Optional function to bootstrap an admin user.
118
120
  * Often used when the database is empty.
@@ -11,12 +11,4 @@ export type Role = {
11
11
  * If this flag is true, the user can perform any action
12
12
  */
13
13
  isAdmin?: boolean;
14
- /**
15
- * Permissions related to editing the collections
16
- */
17
- config?: {
18
- createCollections?: boolean;
19
- editCollections?: boolean | "own";
20
- deleteCollections?: boolean | "own";
21
- };
22
14
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/server-core",
3
3
  "type": "module",
4
- "version": "0.2.3",
4
+ "version": "0.2.4",
5
5
  "description": "Database-Agnostic Backend Core for Rebase",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -50,13 +50,14 @@
50
50
  "hono": "^4.12.21",
51
51
  "jsonwebtoken": "^9.0.3",
52
52
  "nodemailer": "^6.10.1",
53
+ "sharp": "^0.34.5",
53
54
  "ts-morph": "27.0.2",
54
55
  "ws": "^8.20.1",
55
56
  "zod": "^3.25.76",
56
- "@rebasepro/common": "0.2.3",
57
- "@rebasepro/client": "0.2.3",
58
- "@rebasepro/types": "0.2.3",
59
- "@rebasepro/utils": "0.2.3"
57
+ "@rebasepro/client": "0.2.4",
58
+ "@rebasepro/types": "0.2.4",
59
+ "@rebasepro/utils": "0.2.4",
60
+ "@rebasepro/common": "0.2.4"
60
61
  },
61
62
  "devDependencies": {
62
63
  "@types/jest": "^29.5.14",
@@ -67,6 +68,7 @@
67
68
  "@types/react-dom": "^19.2.3",
68
69
  "@types/ws": "^8.18.1",
69
70
  "@vitejs/plugin-react": "^4.7.0",
71
+ "cross-env": "^7.0.3",
70
72
  "jest": "^29.7.0",
71
73
  "ts-jest": "29.4.1",
72
74
  "typescript": "^5.9.3",
@@ -80,7 +82,7 @@
80
82
  "watch": "vite build --watch",
81
83
  "build": "vite build && tsc --emitDeclarationOnly -p tsconfig.prod.json",
82
84
  "test:lint": "eslint \"src/**\" --quiet",
83
- "test": "jest --passWithNoTests --forceExit",
85
+ "test": "cross-env NODE_OPTIONS=\"--experimental-vm-modules\" jest --passWithNoTests --forceExit",
84
86
  "clean": "rm -rf dist && find ./src -name '*.js' -type f | xargs rm -f"
85
87
  }
86
88
  }
@@ -89,7 +89,7 @@ export class AstSchemaEditor {
89
89
  return "undefined";
90
90
  }
91
91
  if (typeof obj === "string") {
92
- return `"${obj.replace(/"/g, '\\"')}"`;
92
+ return JSON.stringify(obj);
93
93
  }
94
94
  if (typeof obj === "number" || typeof obj === "boolean") {
95
95
  return String(obj);
@@ -127,7 +127,7 @@ export class AstSchemaEditor {
127
127
 
128
128
  if (isCode || name === "target" || name === "callbacks" || name === "permissions" || name === "securityRules") {
129
129
  // Preserve this property exactly as it was
130
- const keyStr = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : `"${name}"`;
130
+ const keyStr = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
131
131
  preservedProps.push(`${keyStr}: ${init.getText()}`);
132
132
  }
133
133
  }
@@ -139,7 +139,7 @@ export class AstSchemaEditor {
139
139
  if (keys.length === 0 && preservedProps.length === 0) return "{}";
140
140
 
141
141
  const props = keys.map(key => {
142
- const keyStr = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : `"${key}"`;
142
+ const keyStr = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
143
143
 
144
144
  // If the value is an object, pass the old AST node to recurse
145
145
  let childAstNode: ObjectLiteralExpression | undefined;
@@ -192,7 +192,7 @@ export class AstSchemaEditor {
192
192
  }
193
193
  } else {
194
194
  propsObj.addPropertyAssignment({
195
- name: /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(propertyKey) ? propertyKey : `"${propertyKey}"`,
195
+ name: /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(propertyKey) ? propertyKey : JSON.stringify(propertyKey),
196
196
  initializer: newInitializer
197
197
  });
198
198
  }
package/src/api/errors.ts CHANGED
@@ -115,15 +115,24 @@ export const errorHandler: ErrorHandler = (err, c) => {
115
115
  logMessage = `Database schema mismatch (${issue} missing): ${error.message}. Did you forget to run migrations ('pnpm db:push' or 'pnpm db:migrate')?`;
116
116
  }
117
117
 
118
- // Unexpected errors — log at error level
119
- console.error(
120
- `❌ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}`
121
- );
122
-
123
- // Suppress the huge stack trace for known DB errors (it's noisy and leaks SQL)
124
118
  const causePg = (error.cause && typeof error.cause === "object") ? (error.cause as PgLikeError) : undefined;
125
119
  const pgErrorCode = causePg?.code || error.code;
126
- const suppressStack = pgErrorCode === "42703" || pgErrorCode === "42P01" || (statusCode < 500 && code === "BAD_REQUEST");
120
+ const isDbSchemaMismatch = pgErrorCode === "42703" || pgErrorCode === "42P01";
121
+
122
+ if (isDbSchemaMismatch) {
123
+ // Database schema mismatch is logged as a warning instead of a fatal error
124
+ console.warn(
125
+ `⚠️ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}`
126
+ );
127
+ } else {
128
+ // Unexpected errors — log at error level
129
+ console.error(
130
+ `❌ [API] ${c.req.method} ${c.req.path} → ${statusCode} ${code}: ${logMessage}`
131
+ );
132
+ }
133
+
134
+ // Suppress the huge stack trace for known DB errors (it's noisy and leaks SQL)
135
+ const suppressStack = isDbSchemaMismatch || (statusCode < 500 && code === "BAD_REQUEST");
127
136
  if (!suppressStack) {
128
137
  console.error(error.stack || error);
129
138
  }
@@ -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. Calls `adapter.verifyRequest(request)` to resolve the user
10
- * 2. Scopes the DataDriver via `withAuth()` for RLS
11
- * 3. Enforces auth (401) when `requireAuth` is true and no user is found
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 {