@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.
Files changed (116) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +9 -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 +2309 -370
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +2298 -355
  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 +4 -26
  38. package/dist/types/src/controllers/client.d.ts +25 -43
  39. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  40. package/dist/types/src/controllers/data.d.ts +4 -0
  41. package/dist/types/src/controllers/data_driver.d.ts +23 -0
  42. package/dist/types/src/controllers/registry.d.ts +5 -4
  43. package/dist/types/src/rebase_context.d.ts +1 -1
  44. package/dist/types/src/types/auth_adapter.d.ts +5 -60
  45. package/dist/types/src/types/backend.d.ts +2 -2
  46. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  47. package/dist/types/src/types/collections.d.ts +0 -4
  48. package/dist/types/src/types/component_ref.d.ts +1 -1
  49. package/dist/types/src/types/cron.d.ts +1 -1
  50. package/dist/types/src/types/entity_views.d.ts +1 -0
  51. package/dist/types/src/types/export_import.d.ts +1 -1
  52. package/dist/types/src/types/formex.d.ts +2 -2
  53. package/dist/types/src/types/properties.d.ts +9 -7
  54. package/dist/types/src/types/translations.d.ts +28 -12
  55. package/dist/types/src/types/user_management_delegate.d.ts +22 -57
  56. package/dist/types/src/users/index.d.ts +0 -1
  57. package/dist/types/src/users/user.d.ts +0 -1
  58. package/package.json +8 -6
  59. package/src/api/ast-schema-editor.ts +4 -4
  60. package/src/api/errors.ts +16 -7
  61. package/src/api/logs-routes.ts +129 -0
  62. package/src/api/rest/api-generator.ts +42 -2
  63. package/src/api/rest/query-parser.ts +37 -1
  64. package/src/api/types.ts +6 -1
  65. package/src/auth/adapter-middleware.ts +20 -4
  66. package/src/auth/admin-routes.ts +36 -100
  67. package/src/auth/api-keys/api-key-middleware.ts +126 -0
  68. package/src/auth/api-keys/api-key-permission-guard.ts +64 -0
  69. package/src/auth/api-keys/api-key-routes.ts +183 -0
  70. package/src/auth/api-keys/api-key-store.ts +317 -0
  71. package/src/auth/api-keys/api-key-types.ts +94 -0
  72. package/src/auth/api-keys/index.ts +37 -0
  73. package/src/auth/{auth-overrides.ts → auth-hooks.ts} +81 -14
  74. package/src/auth/builtin-auth-adapter.ts +35 -73
  75. package/src/auth/custom-auth-adapter.ts +1 -1
  76. package/src/auth/index.ts +7 -3
  77. package/src/auth/interfaces.ts +111 -3
  78. package/src/auth/jwt.ts +19 -5
  79. package/src/auth/mfa.ts +160 -0
  80. package/src/auth/middleware.ts +20 -1
  81. package/src/auth/rate-limiter.ts +92 -0
  82. package/src/auth/routes.ts +455 -24
  83. package/src/cron/cron-loader.ts +5 -10
  84. package/src/cron/cron-scheduler.ts +11 -12
  85. package/src/cron/cron-store.ts +8 -7
  86. package/src/env.ts +2 -0
  87. package/src/functions/function-loader.ts +6 -9
  88. package/src/index.ts +1 -2
  89. package/src/init.ts +56 -8
  90. package/src/serve-spa.ts +5 -4
  91. package/src/services/webhook-service.ts +155 -0
  92. package/src/storage/image-transform.ts +202 -0
  93. package/src/storage/index.ts +3 -0
  94. package/src/storage/routes.ts +56 -3
  95. package/src/storage/tus-handler.ts +315 -0
  96. package/src/utils/dev-port.ts +14 -0
  97. package/src/utils/logging.ts +9 -7
  98. package/test/admin-routes.test.ts +62 -164
  99. package/test/api-generator.test.ts +0 -1
  100. package/test/api-key-permission-guard.test.ts +132 -0
  101. package/test/ast-schema-editor.test.ts +26 -0
  102. package/test/auth-routes.test.ts +1 -2
  103. package/test/backend-hooks-admin.test.ts +3 -29
  104. package/test/custom-auth-adapter.test.ts +2 -10
  105. package/test/email-templates.test.ts +169 -0
  106. package/test/function-loader.test.ts +124 -0
  107. package/test/jwt.test.ts +4 -2
  108. package/test/mfa.test.ts +197 -0
  109. package/test/middleware.test.ts +10 -5
  110. package/test/webhook-service.test.ts +249 -0
  111. package/vite.config.ts +3 -2
  112. package/dist/index-BZoAtuqi.js.map +0 -1
  113. package/dist/server-core/src/bootstrappers/index.d.ts +0 -0
  114. package/dist/types/src/users/roles.d.ts +0 -22
  115. package/src/bootstrappers/index.ts +0 -1
  116. package/src/singleton.test.ts +0 -28
package/src/auth/jwt.ts CHANGED
@@ -11,6 +11,8 @@ export interface AccessTokenPayload {
11
11
  userId: string;
12
12
  roles: string[];
13
13
  uid?: string;
14
+ /** Authentication Assurance Level: aal1 = password/oauth, aal2 = MFA verified */
15
+ aal?: "aal1" | "aal2";
14
16
  }
15
17
 
16
18
  let jwtConfig: JwtConfig = {
@@ -70,13 +72,22 @@ export function configureJwt(config: JwtConfig): void {
70
72
  /**
71
73
  * Generate an access token (short-lived, 1 hour by default)
72
74
  */
73
- export function generateAccessToken(userId: string, roles: string[]): string {
75
+ export function generateAccessToken(
76
+ userId: string,
77
+ roles: string[],
78
+ aal: "aal1" | "aal2" = "aal1",
79
+ customClaims?: Record<string, unknown>
80
+ ): string {
74
81
  if (!jwtConfig.secret) {
75
82
  throw new Error("JWT secret not configured. Call configureJwt() first.");
76
83
  }
77
84
 
78
- const payload: AccessTokenPayload = { userId,
79
- roles };
85
+ const payload: Record<string, unknown> = {
86
+ userId,
87
+ roles,
88
+ aal,
89
+ ...customClaims
90
+ };
80
91
 
81
92
  return jwt.sign(payload, jwtConfig.secret, {
82
93
  expiresIn: jwtConfig.accessExpiresIn as jwt.SignOptions["expiresIn"],
@@ -124,16 +135,19 @@ export function verifyAccessToken(token: string): AccessTokenPayload | null {
124
135
  }
125
136
 
126
137
  try {
127
- const decoded = jwt.verify(token, jwtConfig.secret, { algorithms: ["HS256"] }) as { userId?: string; uid?: string; sub?: string; roles?: string[] };
138
+ const decoded = jwt.verify(token, jwtConfig.secret, { algorithms: ["HS256"] }) as { userId?: string; uid?: string; sub?: string; roles?: string[]; aal?: string };
128
139
  const id = decoded.userId || decoded.uid || decoded.sub;
129
140
  if (!id) {
130
141
  console.error("[JWT] Verification failed: missing id in payload", decoded);
131
142
  return null;
132
143
  }
133
144
 
145
+ const aal = (decoded.aal === "aal1" || decoded.aal === "aal2") ? decoded.aal : "aal1";
146
+
134
147
  return {
135
148
  userId: id,
136
- roles: decoded.roles || []
149
+ roles: decoded.roles || [],
150
+ aal
137
151
  };
138
152
  } catch (error) {
139
153
  console.error("[JWT] Verification failed:", error, "Token start:", token.substring(0, 15));
@@ -0,0 +1,160 @@
1
+ /**
2
+ * TOTP (Time-based One-Time Password) implementation.
3
+ *
4
+ * Pure Node.js crypto implementation — no external dependencies required.
5
+ * Implements RFC 6238 (TOTP) and RFC 4226 (HOTP).
6
+ */
7
+
8
+ import { createHmac, randomBytes, createHash } from "crypto";
9
+
10
+ // ─── Base32 Encoding/Decoding ────────────────────────────────────────────────
11
+
12
+ const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
13
+
14
+ /**
15
+ * Encode a Buffer to Base32 string (RFC 4648)
16
+ */
17
+ export function base32Encode(buffer: Buffer): string {
18
+ let bits = 0;
19
+ let value = 0;
20
+ let output = "";
21
+
22
+ for (let i = 0; i < buffer.length; i++) {
23
+ value = (value << 8) | buffer[i];
24
+ bits += 8;
25
+
26
+ while (bits >= 5) {
27
+ output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
28
+ bits -= 5;
29
+ }
30
+ }
31
+
32
+ if (bits > 0) {
33
+ output += BASE32_ALPHABET[(value << (5 - bits)) & 31];
34
+ }
35
+
36
+ return output;
37
+ }
38
+
39
+ /**
40
+ * Decode a Base32 string to Buffer
41
+ */
42
+ export function base32Decode(encoded: string): Buffer {
43
+ const cleanInput = encoded.replace(/=+$/, "").toUpperCase();
44
+ const bytes: number[] = [];
45
+ let bits = 0;
46
+ let value = 0;
47
+
48
+ for (let i = 0; i < cleanInput.length; i++) {
49
+ const index = BASE32_ALPHABET.indexOf(cleanInput[i]);
50
+ if (index === -1) continue;
51
+
52
+ value = (value << 5) | index;
53
+ bits += 5;
54
+
55
+ if (bits >= 8) {
56
+ bytes.push((value >>> (bits - 8)) & 255);
57
+ bits -= 8;
58
+ }
59
+ }
60
+
61
+ return Buffer.from(bytes);
62
+ }
63
+
64
+ // ─── HOTP/TOTP ───────────────────────────────────────────────────────────────
65
+
66
+ /**
67
+ * Generate an HOTP value per RFC 4226
68
+ */
69
+ function generateHotp(secret: Buffer, counter: bigint): string {
70
+ const hmac = createHmac("sha1", secret);
71
+ const counterBuffer = Buffer.alloc(8);
72
+ counterBuffer.writeBigInt64BE(counter);
73
+ hmac.update(counterBuffer);
74
+ const hash = hmac.digest();
75
+
76
+ const offset = hash[hash.length - 1] & 0x0f;
77
+ const code =
78
+ (((hash[offset] & 0x7f) << 24) |
79
+ (hash[offset + 1] << 16) |
80
+ (hash[offset + 2] << 8) |
81
+ hash[offset + 3]) %
82
+ 1000000;
83
+
84
+ return code.toString().padStart(6, "0");
85
+ }
86
+
87
+ /**
88
+ * Generate a TOTP value for the current time step
89
+ */
90
+ export function generateTotp(secret: Buffer, timeStep = 30): string {
91
+ const counter = BigInt(Math.floor(Date.now() / 1000 / timeStep));
92
+ return generateHotp(secret, counter);
93
+ }
94
+
95
+ /**
96
+ * Verify a TOTP token with a configurable time window
97
+ *
98
+ * @param secret - The shared secret as a Buffer
99
+ * @param token - The 6-digit TOTP code to verify
100
+ * @param window - Number of time steps to check on each side (default: 1)
101
+ * @returns true if the token is valid within the window
102
+ */
103
+ export function verifyTotp(secret: Buffer, token: string, window = 1): boolean {
104
+ const timeStep = 30;
105
+ const counter = BigInt(Math.floor(Date.now() / 1000 / timeStep));
106
+ for (let i = -window; i <= window; i++) {
107
+ if (generateHotp(secret, counter + BigInt(i)) === token) {
108
+ return true;
109
+ }
110
+ }
111
+ return false;
112
+ }
113
+
114
+ // ─── Secret Generation ───────────────────────────────────────────────────────
115
+
116
+ /**
117
+ * Generate a new TOTP secret and return the setup information
118
+ *
119
+ * @param issuer - The issuer name (app name) for the QR code
120
+ * @param accountName - The account identifier (usually email)
121
+ * @returns Object with base32 secret and otpauth URI
122
+ */
123
+ export function generateTotpSecret(issuer: string, accountName: string): {
124
+ secret: string;
125
+ uri: string;
126
+ } {
127
+ // Generate a 20-byte random secret (160 bits, recommended by RFC 4226)
128
+ const secretBuffer = randomBytes(20);
129
+ const secret = base32Encode(secretBuffer);
130
+
131
+ // Build otpauth:// URI for QR code generation
132
+ const encodedIssuer = encodeURIComponent(issuer);
133
+ const encodedAccount = encodeURIComponent(accountName);
134
+ const uri = `otpauth://totp/${encodedIssuer}:${encodedAccount}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=6&period=30`;
135
+
136
+ return { secret, uri };
137
+ }
138
+
139
+ // ─── Recovery Codes ──────────────────────────────────────────────────────────
140
+
141
+ /**
142
+ * Generate a set of one-time recovery codes
143
+ *
144
+ * @param count - Number of recovery codes to generate (default: 10)
145
+ * @returns Array of formatted recovery code strings (e.g. "A1B2C-D3E4F")
146
+ */
147
+ export function generateRecoveryCodes(count = 10): string[] {
148
+ return Array.from({ length: count }, () => {
149
+ const raw = randomBytes(5).toString("hex").toUpperCase();
150
+ const parts = raw.match(/.{1,5}/g);
151
+ return parts ? parts.join("-") : raw;
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Hash a recovery code for storage
157
+ */
158
+ export function hashRecoveryCode(code: string): string {
159
+ return createHash("sha256").update(code.replace(/-/g, "").toUpperCase()).digest("hex");
160
+ }
@@ -4,6 +4,8 @@ import { verifyAccessToken, AccessTokenPayload } from "./jwt";
4
4
  import { HonoEnv } from "../api/types";
5
5
  import { scopeDataDriver } from "./rls-scope";
6
6
  import { safeCompare } from "./crypto-utils";
7
+ import { isApiKeyToken, validateApiKey } from "./api-keys/api-key-middleware";
8
+ import type { ApiKeyStore } from "./api-keys/api-key-store";
7
9
 
8
10
  /**
9
11
  * Result from a custom auth validator.
@@ -44,6 +46,12 @@ export interface AuthMiddlewareOptions {
44
46
  * timing attacks. The key must be at least 32 characters.
45
47
  */
46
48
  serviceKey?: string;
49
+ /**
50
+ * API key store for authenticating `rk_` prefixed tokens.
51
+ * When set, tokens starting with `rk_` are validated against the
52
+ * database instead of being treated as JWTs.
53
+ */
54
+ apiKeyStore?: ApiKeyStore;
47
55
  }
48
56
 
49
57
  /**
@@ -223,7 +231,7 @@ export function extractUserFromToken(token: string): AccessTokenPayload | null {
223
231
  */
224
232
 
225
233
  export function createAuthMiddleware(options: AuthMiddlewareOptions): MiddlewareHandler<HonoEnv> {
226
- const { driver, requireAuth: enforceAuth = true, validator, serviceKey } = options;
234
+ const { driver, requireAuth: enforceAuth = true, validator, serviceKey, apiKeyStore } = options;
227
235
 
228
236
  return async (c, next) => {
229
237
  if (validator) {
@@ -291,6 +299,17 @@ code: "UNAUTHORIZED" } }, 401);
291
299
  return c.json({ error: { message: "Internal authentication error",
292
300
  code: "INTERNAL_ERROR" } }, 500);
293
301
  }
302
+ } else if (apiKeyStore && isApiKeyToken(token)) {
303
+ // ── API Key verification ──────────────────────────────
304
+ // Tokens starting with `rk_` are validated against the
305
+ // api_keys table instead of JWT verification.
306
+ const result = await validateApiKey(c, token, {
307
+ store: apiKeyStore,
308
+ driver,
309
+ });
310
+ if (result !== true) {
311
+ return result;
312
+ }
294
313
  } else {
295
314
  // ── JWT verification ───────────────────────────────────
296
315
  const payload = extractUserFromToken(token);
@@ -131,3 +131,95 @@ export const strictAuthLimiter = createRateLimiter({
131
131
  limit: 50,
132
132
  message: "Too many requests to this sensitive endpoint, please try again later."
133
133
  });
134
+
135
+ /**
136
+ * Key generator for API-key-based rate limiting.
137
+ *
138
+ * Uses the API key ID (from `c.get("apiKey")`) as the rate limit key.
139
+ * Falls back to IP-based keying when the request is not authenticated
140
+ * via an API key.
141
+ */
142
+ export function apiKeyKeyGenerator(c: Parameters<MiddlewareHandler<HonoEnv>>[0]): string {
143
+ const apiKey = c.get("apiKey") as { id: string } | undefined;
144
+ if (apiKey) {
145
+ return `api-key:${apiKey.id}`;
146
+ }
147
+ return defaultKeyGenerator(c);
148
+ }
149
+
150
+ /**
151
+ * Create a rate limiter specifically for API key requests.
152
+ *
153
+ * When a request is authenticated via an API key that has a `rate_limit`
154
+ * configured, this limiter enforces per-key limits using the key's ID
155
+ * as the rate limit bucket.
156
+ *
157
+ * @param defaultLimit - Fallback limit when the key has no `rate_limit` set.
158
+ * @param windowMs - Time window in milliseconds (default: 15 minutes).
159
+ */
160
+ export function createApiKeyRateLimiter(
161
+ defaultLimit = 1000,
162
+ windowMs = 15 * 60 * 1000,
163
+ ): MiddlewareHandler<HonoEnv> {
164
+ // We maintain a single shared store keyed by API key ID.
165
+ // The actual limit is resolved per-request from the key's metadata.
166
+ const store = new Map<string, { timestamps: number[] }>();
167
+
168
+ const cleanupInterval = setInterval(() => {
169
+ const now = Date.now();
170
+ for (const [key, entry] of store.entries()) {
171
+ entry.timestamps = entry.timestamps.filter(t => now - t < windowMs);
172
+ if (entry.timestamps.length === 0) {
173
+ store.delete(key);
174
+ }
175
+ }
176
+ }, windowMs);
177
+
178
+ if (cleanupInterval.unref) {
179
+ cleanupInterval.unref();
180
+ }
181
+
182
+ return async (c, next) => {
183
+ const apiKey = c.get("apiKey") as { id: string; rate_limit: number | null } | undefined;
184
+ if (!apiKey) {
185
+ // Not an API key request — skip this limiter
186
+ return next();
187
+ }
188
+
189
+ const limit = apiKey.rate_limit ?? defaultLimit;
190
+ const key = `api-key:${apiKey.id}`;
191
+ const now = Date.now();
192
+
193
+ let entry = store.get(key);
194
+ if (!entry) {
195
+ entry = { timestamps: [] };
196
+ store.set(key, entry);
197
+ }
198
+
199
+ entry.timestamps = entry.timestamps.filter(t => now - t < windowMs);
200
+
201
+ if (entry.timestamps.length >= limit) {
202
+ const retryAfterMs = entry.timestamps[0] + windowMs - now;
203
+ const retryAfterSec = Math.ceil(retryAfterMs / 1000);
204
+
205
+ c.header("Retry-After", String(retryAfterSec));
206
+ c.header("X-RateLimit-Limit", String(limit));
207
+ c.header("X-RateLimit-Remaining", "0");
208
+ c.header("X-RateLimit-Reset", String(Math.ceil((now + retryAfterMs) / 1000)));
209
+
210
+ return c.json({
211
+ error: {
212
+ message: "API key rate limit exceeded, please try again later.",
213
+ code: "RATE_LIMITED"
214
+ }
215
+ }, 429);
216
+ }
217
+
218
+ entry.timestamps.push(now);
219
+
220
+ c.header("X-RateLimit-Limit", String(limit));
221
+ c.header("X-RateLimit-Remaining", String(limit - entry.timestamps.length));
222
+
223
+ return next();
224
+ };
225
+ }