@rebasepro/server-core 0.4.0 → 0.5.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/README.md +62 -25
- package/dist/common/src/util/permissions.d.ts +14 -6
- package/dist/index.es.js +75 -33
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +75 -33
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/errors.d.ts +15 -0
- package/dist/server-core/src/auth/jwt.d.ts +10 -0
- package/dist/types/src/types/backend.d.ts +36 -1
- package/dist/types/src/types/collections.d.ts +21 -1
- package/dist/types/src/types/properties.d.ts +0 -8
- package/package.json +5 -5
- package/src/api/errors.ts +20 -1
- package/src/api/openapi-generator.ts +1 -1
- package/src/api/rest/api-generator.ts +71 -15
- package/src/api/server.ts +1 -1
- package/src/auth/builtin-auth-adapter.ts +4 -15
- package/src/auth/jwt.ts +10 -0
- package/src/auth/routes.ts +5 -9
- package/src/init.ts +12 -20
- package/test.ts +0 -6
|
@@ -28,6 +28,21 @@ export interface ErrorResponse {
|
|
|
28
28
|
details?: unknown;
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* General shape of errors that flow through the API error handler.
|
|
33
|
+
* Extends Error with optional HTTP status, error code, and details.
|
|
34
|
+
*/
|
|
35
|
+
export interface RebaseApiError extends Error {
|
|
36
|
+
statusCode?: number;
|
|
37
|
+
code?: string;
|
|
38
|
+
details?: unknown;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Type guard for errors that carry optional API metadata (statusCode, code, details).
|
|
42
|
+
* Returns true for any Error instance — the optional properties are then
|
|
43
|
+
* checked via normal property access.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isRebaseApiError(error: unknown): error is RebaseApiError;
|
|
31
46
|
/**
|
|
32
47
|
* Hono error-handling middleware (`app.onError`).
|
|
33
48
|
* Converts any error into the canonical `{ error: { message, code } }` shape.
|
|
@@ -9,6 +9,16 @@ export interface AccessTokenPayload {
|
|
|
9
9
|
uid?: string;
|
|
10
10
|
/** Authentication Assurance Level: aal1 = password/oauth, aal2 = MFA verified */
|
|
11
11
|
aal?: "aal1" | "aal2";
|
|
12
|
+
/** Email claim from the JWT, if present */
|
|
13
|
+
email?: string;
|
|
14
|
+
/** Display name claim from the JWT, if present */
|
|
15
|
+
displayName?: string;
|
|
16
|
+
/** Photo URL claim from the JWT, if present */
|
|
17
|
+
photoURL?: string;
|
|
18
|
+
/** Whether MFA has been verified for this session */
|
|
19
|
+
mfa_verified?: boolean;
|
|
20
|
+
/** Authentication Methods Reference — list of methods used (e.g. 'pwd', 'otp') */
|
|
21
|
+
amr?: string[];
|
|
12
22
|
}
|
|
13
23
|
/**
|
|
14
24
|
* Configure JWT settings - call this during initialization.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Entity } from "./entities";
|
|
2
2
|
import type { EntityCollection, FilterValues, WhereFilterOp } from "./collections";
|
|
3
|
+
import type { AuthAdapter } from "./auth_adapter";
|
|
3
4
|
/**
|
|
4
5
|
* Abstract database connection interface.
|
|
5
6
|
* Represents a connection to any database system.
|
|
@@ -182,6 +183,24 @@ export interface RealtimeProvider {
|
|
|
182
183
|
* Notify all relevant subscribers of an entity update
|
|
183
184
|
*/
|
|
184
185
|
notifyEntityUpdate(path: string, entityId: string, entity: Entity | null, databaseId?: string): Promise<void>;
|
|
186
|
+
/**
|
|
187
|
+
* Called when the HTTP server is ready and listening.
|
|
188
|
+
* Useful for providers that need the server address for callbacks.
|
|
189
|
+
*/
|
|
190
|
+
onServerReady?(serverInfo: {
|
|
191
|
+
port: number;
|
|
192
|
+
hostname?: string;
|
|
193
|
+
}): void;
|
|
194
|
+
/**
|
|
195
|
+
* Gracefully shut down the realtime provider.
|
|
196
|
+
* Called during server shutdown to clean up resources.
|
|
197
|
+
*/
|
|
198
|
+
destroy?(): Promise<void>;
|
|
199
|
+
/**
|
|
200
|
+
* Stop the internal LISTEN client (e.g., PostgreSQL LISTEN/NOTIFY).
|
|
201
|
+
* Called during graceful shutdown before closing database connections.
|
|
202
|
+
*/
|
|
203
|
+
stopListening?(): Promise<void>;
|
|
185
204
|
}
|
|
186
205
|
/**
|
|
187
206
|
* Abstract collection registry interface.
|
|
@@ -464,6 +483,22 @@ export interface BackendBootstrapper {
|
|
|
464
483
|
* (e.g., `"postgres"`, `"mongodb"`, `"mysql"`).
|
|
465
484
|
*/
|
|
466
485
|
type: string;
|
|
486
|
+
/**
|
|
487
|
+
* Unique identifier for this bootstrapper instance.
|
|
488
|
+
* Used to register the driver in the driver registry.
|
|
489
|
+
* Defaults to `type` if not set.
|
|
490
|
+
*/
|
|
491
|
+
id?: string;
|
|
492
|
+
/**
|
|
493
|
+
* Whether this bootstrapper provides the default driver.
|
|
494
|
+
* When true, the coordinator uses this driver as the primary one.
|
|
495
|
+
*/
|
|
496
|
+
isDefault?: boolean;
|
|
497
|
+
/**
|
|
498
|
+
* Run database migrations for this driver.
|
|
499
|
+
* Called by the coordinator after all drivers are initialized.
|
|
500
|
+
*/
|
|
501
|
+
runMigrations?(config: unknown, driverResult: InitializedDriver): Promise<void>;
|
|
467
502
|
/**
|
|
468
503
|
* Create a DataDriver from the given config.
|
|
469
504
|
* This is the only **required** method.
|
|
@@ -498,7 +533,7 @@ export interface BackendBootstrapper {
|
|
|
498
533
|
/**
|
|
499
534
|
* Initialize WebSocket server for realtime operations.
|
|
500
535
|
*/
|
|
501
|
-
initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import("../controllers/data_driver").DataDriver, config?: unknown): Promise<void> | void;
|
|
536
|
+
initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import("../controllers/data_driver").DataDriver, config?: unknown, authAdapter?: AuthAdapter): Promise<void> | void;
|
|
502
537
|
}
|
|
503
538
|
/**
|
|
504
539
|
* Result of `BackendBootstrapper.initializeDriver()`.
|
|
@@ -343,6 +343,23 @@ export interface BaseEntityCollection<M extends Record<string, unknown> = Record
|
|
|
343
343
|
* Builder for the collection actions rendered in the toolbar
|
|
344
344
|
*/
|
|
345
345
|
Actions?: ComponentRef<CollectionActionsProps>[];
|
|
346
|
+
/**
|
|
347
|
+
* The database table name for this collection.
|
|
348
|
+
* Automatically set for PostgreSQL collections.
|
|
349
|
+
* For non-SQL backends, this may be undefined.
|
|
350
|
+
*/
|
|
351
|
+
table?: string;
|
|
352
|
+
/**
|
|
353
|
+
* Relations defined for this collection.
|
|
354
|
+
* Populated at normalization time from inline relation properties
|
|
355
|
+
* or explicit relation definitions.
|
|
356
|
+
*/
|
|
357
|
+
relations?: Relation[];
|
|
358
|
+
/**
|
|
359
|
+
* Security rules for this collection (Row Level Security).
|
|
360
|
+
* When defined, the backend enforces access control policies.
|
|
361
|
+
*/
|
|
362
|
+
securityRules?: SecurityRule[];
|
|
346
363
|
}
|
|
347
364
|
/**
|
|
348
365
|
* A collection backed by PostgreSQL (or any SQL database).
|
|
@@ -436,7 +453,10 @@ export interface MongoDBCollection<M extends Record<string, unknown> = Record<st
|
|
|
436
453
|
* @group Models
|
|
437
454
|
*/
|
|
438
455
|
export type EntityCollection<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> = PostgresCollection<M, USER> | FirebaseCollection<M, USER> | MongoDBCollection<M, USER>;
|
|
439
|
-
/**
|
|
456
|
+
/**
|
|
457
|
+
* An EntityCollection that supports SQL-style relations (e.g. Postgres).
|
|
458
|
+
* @deprecated Use `EntityCollection` directly — `table`, `relations`, and `securityRules` are now on `BaseEntityCollection`.
|
|
459
|
+
*/
|
|
440
460
|
export type CollectionWithRelations<M extends Record<string, unknown> = Record<string, unknown>> = EntityCollection<M> & {
|
|
441
461
|
table?: string;
|
|
442
462
|
relations?: Relation[];
|
|
@@ -641,14 +641,6 @@ export interface MapProperty extends BaseProperty {
|
|
|
641
641
|
* Properties that are displayed when rendered as a preview
|
|
642
642
|
*/
|
|
643
643
|
previewProperties?: string[];
|
|
644
|
-
/**
|
|
645
|
-
* Allow the user to add only some keys in this map.
|
|
646
|
-
* By default, all properties of the map have the corresponding field in
|
|
647
|
-
* the form view. Setting this flag to true allows to pick only some.
|
|
648
|
-
* Useful for map that can have a lot of sub-properties that may not be
|
|
649
|
-
* needed
|
|
650
|
-
*/
|
|
651
|
-
pickOnlySomeKeys?: boolean;
|
|
652
644
|
/**
|
|
653
645
|
* Render this map as a key-value table that allows to use
|
|
654
646
|
* arbitrary keys. You don't need to define the properties in this case.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/server-core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "Database-Agnostic Backend Core for Rebase",
|
|
6
6
|
"funding": {
|
|
7
7
|
"url": "https://github.com/sponsors/rebaseco"
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"ts-morph": "27.0.2",
|
|
55
55
|
"ws": "^8.20.1",
|
|
56
56
|
"zod": "^3.25.76",
|
|
57
|
-
"@rebasepro/
|
|
58
|
-
"@rebasepro/
|
|
59
|
-
"@rebasepro/
|
|
60
|
-
"@rebasepro/
|
|
57
|
+
"@rebasepro/common": "0.5.0",
|
|
58
|
+
"@rebasepro/client": "0.5.0",
|
|
59
|
+
"@rebasepro/types": "0.5.0",
|
|
60
|
+
"@rebasepro/utils": "0.5.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/jest": "^29.5.14",
|
package/src/api/errors.ts
CHANGED
|
@@ -70,13 +70,32 @@ export interface ErrorResponse {
|
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* General shape of errors that flow through the API error handler.
|
|
75
|
+
* Extends Error with optional HTTP status, error code, and details.
|
|
76
|
+
*/
|
|
77
|
+
export interface RebaseApiError extends Error {
|
|
78
|
+
statusCode?: number;
|
|
79
|
+
code?: string;
|
|
80
|
+
details?: unknown;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Type guard for errors that carry optional API metadata (statusCode, code, details).
|
|
85
|
+
* Returns true for any Error instance — the optional properties are then
|
|
86
|
+
* checked via normal property access.
|
|
87
|
+
*/
|
|
88
|
+
export function isRebaseApiError(error: unknown): error is RebaseApiError {
|
|
89
|
+
return error instanceof Error;
|
|
90
|
+
}
|
|
91
|
+
|
|
73
92
|
/**
|
|
74
93
|
* Hono error-handling middleware (`app.onError`).
|
|
75
94
|
* Converts any error into the canonical `{ error: { message, code } }` shape.
|
|
76
95
|
*/
|
|
77
96
|
export const errorHandler: ErrorHandler = (err, c) => {
|
|
78
97
|
// Typecast custom error properties
|
|
79
|
-
const error = err
|
|
98
|
+
const error: RebaseApiError = err;
|
|
80
99
|
|
|
81
100
|
if (error instanceof ApiError || error.name === "ApiError") {
|
|
82
101
|
// Operational errors — log at warn level
|
|
@@ -314,7 +314,7 @@ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResp
|
|
|
314
314
|
};
|
|
315
315
|
|
|
316
316
|
// ── Subcollection routes ──────────────────────────────────────
|
|
317
|
-
const relations =
|
|
317
|
+
const relations = collection.relations;
|
|
318
318
|
if (relations && relations.length > 0) {
|
|
319
319
|
for (const relation of relations) {
|
|
320
320
|
const relationName = relation.relationName;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { DataDriver, Entity, EntityCollection, FetchCollectionProps, DataHooks, BackendHookContext, RestFetchService } from "@rebasepro/types";
|
|
3
3
|
import { QueryOptions, HonoEnv } from "../types";
|
|
4
|
-
import { ApiError } from "../errors";
|
|
4
|
+
import { ApiError, isRebaseApiError } from "../errors";
|
|
5
5
|
import { parseQueryOptions } from "./query-parser";
|
|
6
6
|
import { httpMethodToOperation, isOperationAllowed } from "../../auth/api-keys/api-key-permission-guard";
|
|
7
7
|
import type { ApiKeyMasked } from "../../auth/api-keys/api-key-types";
|
|
@@ -234,9 +234,10 @@ export class RestApiGenerator {
|
|
|
234
234
|
|
|
235
235
|
return c.json(response, 201);
|
|
236
236
|
} catch (error) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
if (isRebaseApiError(error) && !error.code) {
|
|
238
|
+
error.code = "BAD_REQUEST";
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
240
241
|
}
|
|
241
242
|
});
|
|
242
243
|
|
|
@@ -282,9 +283,10 @@ export class RestApiGenerator {
|
|
|
282
283
|
|
|
283
284
|
return c.json(response);
|
|
284
285
|
} catch (error) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
if (isRebaseApiError(error) && !error.code) {
|
|
287
|
+
error.code = "BAD_REQUEST";
|
|
288
|
+
}
|
|
289
|
+
throw error;
|
|
288
290
|
}
|
|
289
291
|
});
|
|
290
292
|
|
|
@@ -386,6 +388,8 @@ entityId };
|
|
|
386
388
|
|
|
387
389
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
388
390
|
|
|
391
|
+
const hookCtx = this.buildHookContext(c, "GET");
|
|
392
|
+
|
|
389
393
|
if (parsed.entityId === "count") {
|
|
390
394
|
// GET /parent/:parentId/child/count — count child entities
|
|
391
395
|
const queryDict = c.req.queries();
|
|
@@ -406,7 +410,12 @@ entityId };
|
|
|
406
410
|
entityId: parsed.entityId
|
|
407
411
|
});
|
|
408
412
|
if (!entity) throw ApiError.notFound("Entity not found");
|
|
409
|
-
|
|
413
|
+
|
|
414
|
+
const flatResult = this.flattenEntity(entity);
|
|
415
|
+
const hookResult = await this.applyAfterRead(c.req.param("parent"), flatResult, hookCtx);
|
|
416
|
+
if (!hookResult) throw ApiError.notFound("Entity not found");
|
|
417
|
+
|
|
418
|
+
return c.json(hookResult);
|
|
410
419
|
} else {
|
|
411
420
|
// GET /parent/:parentId/child — list entities
|
|
412
421
|
const queryDict = c.req.queries();
|
|
@@ -420,13 +429,23 @@ entityId };
|
|
|
420
429
|
order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
|
|
421
430
|
searchString
|
|
422
431
|
});
|
|
432
|
+
|
|
433
|
+
let flatEntities = entities.map(e => this.flattenEntity(e));
|
|
434
|
+
flatEntities = await this.applyAfterReadBatch(c.req.param("parent"), flatEntities, hookCtx);
|
|
435
|
+
|
|
436
|
+
const total = driver.countEntities ? await driver.countEntities({
|
|
437
|
+
path: parsed.collectionPath,
|
|
438
|
+
filter: queryOptions.where as FetchCollectionProps["filter"],
|
|
439
|
+
searchString
|
|
440
|
+
}) : flatEntities.length;
|
|
441
|
+
|
|
423
442
|
return c.json({
|
|
424
|
-
data:
|
|
443
|
+
data: flatEntities,
|
|
425
444
|
meta: {
|
|
426
|
-
total
|
|
445
|
+
total,
|
|
427
446
|
limit: queryOptions.limit,
|
|
428
447
|
offset: queryOptions.offset,
|
|
429
|
-
hasMore:
|
|
448
|
+
hasMore: (queryOptions.offset || 0) + flatEntities.length < total
|
|
430
449
|
}
|
|
431
450
|
});
|
|
432
451
|
}
|
|
@@ -441,9 +460,14 @@ entityId };
|
|
|
441
460
|
if (!parsed || parsed.entityId) return next();
|
|
442
461
|
|
|
443
462
|
const driver = c.get("driver") || this.driver;
|
|
463
|
+
const hookCtx = this.buildHookContext(c, "POST");
|
|
444
464
|
|
|
445
465
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
446
|
-
|
|
466
|
+
let body = await c.req.json().catch(() => ({}));
|
|
467
|
+
|
|
468
|
+
if (this.dataHooks?.beforeSave) {
|
|
469
|
+
body = await this.dataHooks.beforeSave(parsed.collectionPath, body, undefined, hookCtx);
|
|
470
|
+
}
|
|
447
471
|
|
|
448
472
|
const entity = await driver.saveEntity({
|
|
449
473
|
path: parsed.collectionPath,
|
|
@@ -451,7 +475,15 @@ entityId };
|
|
|
451
475
|
status: "new"
|
|
452
476
|
});
|
|
453
477
|
|
|
454
|
-
|
|
478
|
+
const response = this.formatResponse(entity);
|
|
479
|
+
|
|
480
|
+
if (this.dataHooks?.afterSave) {
|
|
481
|
+
Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
482
|
+
console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return c.json(response, 201);
|
|
455
487
|
});
|
|
456
488
|
|
|
457
489
|
// PUT /<subcollection-path>/:id — update entity
|
|
@@ -463,10 +495,15 @@ entityId };
|
|
|
463
495
|
if (!parsed || !parsed.entityId) return next();
|
|
464
496
|
|
|
465
497
|
const driver = c.get("driver") || this.driver;
|
|
498
|
+
const hookCtx = this.buildHookContext(c, "PUT");
|
|
466
499
|
|
|
467
500
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
468
501
|
|
|
469
|
-
|
|
502
|
+
let body = await c.req.json().catch(() => ({}));
|
|
503
|
+
|
|
504
|
+
if (this.dataHooks?.beforeSave) {
|
|
505
|
+
body = await this.dataHooks.beforeSave(parsed.collectionPath, body, parsed.entityId, hookCtx);
|
|
506
|
+
}
|
|
470
507
|
|
|
471
508
|
const entity = await driver.saveEntity({
|
|
472
509
|
path: parsed.collectionPath,
|
|
@@ -475,7 +512,15 @@ entityId };
|
|
|
475
512
|
status: "existing"
|
|
476
513
|
});
|
|
477
514
|
|
|
478
|
-
|
|
515
|
+
const response = this.formatResponse(entity);
|
|
516
|
+
|
|
517
|
+
if (this.dataHooks?.afterSave) {
|
|
518
|
+
Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response as Record<string, unknown>, hookCtx)).catch(err => {
|
|
519
|
+
console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return c.json(response);
|
|
479
524
|
});
|
|
480
525
|
|
|
481
526
|
// DELETE /<subcollection-path>/:id — delete entity
|
|
@@ -487,6 +532,7 @@ entityId };
|
|
|
487
532
|
if (!parsed || !parsed.entityId) return next();
|
|
488
533
|
|
|
489
534
|
const driver = c.get("driver") || this.driver;
|
|
535
|
+
const hookCtx = this.buildHookContext(c, "DELETE");
|
|
490
536
|
|
|
491
537
|
this.enforceApiKeyPermission(c, c.req.param("parent"));
|
|
492
538
|
|
|
@@ -497,8 +543,18 @@ entityId };
|
|
|
497
543
|
|
|
498
544
|
if (!existingEntity) throw ApiError.notFound("Entity not found");
|
|
499
545
|
|
|
546
|
+
if (this.dataHooks?.beforeDelete) {
|
|
547
|
+
await this.dataHooks.beforeDelete(parsed.collectionPath, parsed.entityId, hookCtx);
|
|
548
|
+
}
|
|
549
|
+
|
|
500
550
|
await driver.deleteEntity({ entity: existingEntity });
|
|
501
551
|
|
|
552
|
+
if (this.dataHooks?.afterDelete) {
|
|
553
|
+
Promise.resolve(this.dataHooks.afterDelete(parsed.collectionPath, parsed.entityId, hookCtx)).catch(err => {
|
|
554
|
+
console.error("[BackendHooks] data.afterDelete error:", err instanceof Error ? err.message : err);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
502
558
|
return new Response(null, { status: 204 });
|
|
503
559
|
});
|
|
504
560
|
}
|
package/src/api/server.ts
CHANGED
|
@@ -120,7 +120,7 @@ export class RebaseApiServer {
|
|
|
120
120
|
singularName: col.singularName,
|
|
121
121
|
description: col.description,
|
|
122
122
|
properties: Object.keys(col.properties),
|
|
123
|
-
relations:
|
|
123
|
+
relations: col.relations?.map((r: Relation) => ({
|
|
124
124
|
relationName: r.relationName,
|
|
125
125
|
target: typeof r.target === "function" ? r.target().slug : r.target,
|
|
126
126
|
cardinality: r.cardinality,
|
|
@@ -119,12 +119,6 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
119
119
|
return null;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// The decoded JWT may contain additional claims beyond the typed payload
|
|
123
|
-
const extendedPayload = payload as AccessTokenPayload & {
|
|
124
|
-
email?: string;
|
|
125
|
-
displayName?: string;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
122
|
// Resolve roles from the repository
|
|
129
123
|
let roles: string[] = payload.roles || [];
|
|
130
124
|
try {
|
|
@@ -137,8 +131,8 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
137
131
|
|
|
138
132
|
return {
|
|
139
133
|
uid: payload.userId,
|
|
140
|
-
email:
|
|
141
|
-
displayName:
|
|
134
|
+
email: payload.email ?? "",
|
|
135
|
+
displayName: payload.displayName ?? null,
|
|
142
136
|
roles,
|
|
143
137
|
isAdmin,
|
|
144
138
|
rawToken: token,
|
|
@@ -163,11 +157,6 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
163
157
|
return null;
|
|
164
158
|
}
|
|
165
159
|
|
|
166
|
-
const extendedPayload = payload as AccessTokenPayload & {
|
|
167
|
-
email?: string;
|
|
168
|
-
displayName?: string;
|
|
169
|
-
};
|
|
170
|
-
|
|
171
160
|
let roles: string[] = payload.roles || [];
|
|
172
161
|
try {
|
|
173
162
|
roles = await authRepository.getUserRoleIds(payload.userId);
|
|
@@ -179,8 +168,8 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
179
168
|
|
|
180
169
|
return {
|
|
181
170
|
uid: payload.userId,
|
|
182
|
-
email:
|
|
183
|
-
displayName:
|
|
171
|
+
email: payload.email ?? "",
|
|
172
|
+
displayName: payload.displayName ?? null,
|
|
184
173
|
roles,
|
|
185
174
|
isAdmin,
|
|
186
175
|
rawToken: token,
|
package/src/auth/jwt.ts
CHANGED
|
@@ -13,6 +13,16 @@ export interface AccessTokenPayload {
|
|
|
13
13
|
uid?: string;
|
|
14
14
|
/** Authentication Assurance Level: aal1 = password/oauth, aal2 = MFA verified */
|
|
15
15
|
aal?: "aal1" | "aal2";
|
|
16
|
+
/** Email claim from the JWT, if present */
|
|
17
|
+
email?: string;
|
|
18
|
+
/** Display name claim from the JWT, if present */
|
|
19
|
+
displayName?: string;
|
|
20
|
+
/** Photo URL claim from the JWT, if present */
|
|
21
|
+
photoURL?: string;
|
|
22
|
+
/** Whether MFA has been verified for this session */
|
|
23
|
+
mfa_verified?: boolean;
|
|
24
|
+
/** Authentication Methods Reference — list of methods used (e.g. 'pwd', 'otp') */
|
|
25
|
+
amr?: string[];
|
|
16
26
|
}
|
|
17
27
|
|
|
18
28
|
let jwtConfig: JwtConfig = {
|
package/src/auth/routes.ts
CHANGED
|
@@ -1171,18 +1171,14 @@ message: "Session revoked successfully" });
|
|
|
1171
1171
|
throw ApiError.notFound("MFA factor not found");
|
|
1172
1172
|
}
|
|
1173
1173
|
|
|
1174
|
-
//
|
|
1175
|
-
const
|
|
1176
|
-
let isValid =
|
|
1174
|
+
// Try TOTP verification first (standard 6-digit codes)
|
|
1175
|
+
const secretBuffer = base32Decode(factor.secretEncrypted);
|
|
1176
|
+
let isValid = verifyTotp(secretBuffer, code);
|
|
1177
1177
|
|
|
1178
|
-
if
|
|
1179
|
-
|
|
1178
|
+
// Fall back to recovery code verification if TOTP didn't match
|
|
1179
|
+
if (!isValid) {
|
|
1180
1180
|
const codeHash = hashRecoveryCode(code);
|
|
1181
1181
|
isValid = await authRepo.useRecoveryCode(userCtx.userId, codeHash);
|
|
1182
|
-
} else {
|
|
1183
|
-
// Verify TOTP
|
|
1184
|
-
const secretBuffer = base32Decode(factor.secretEncrypted);
|
|
1185
|
-
isValid = verifyTotp(secretBuffer, code);
|
|
1186
1182
|
}
|
|
1187
1183
|
|
|
1188
1184
|
if (!isValid) {
|
package/src/init.ts
CHANGED
|
@@ -395,7 +395,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
395
395
|
|
|
396
396
|
// 1. Initialize all drivers
|
|
397
397
|
for (const bootstrapper of bootstrappers) {
|
|
398
|
-
const b = bootstrapper
|
|
398
|
+
const b = bootstrapper;
|
|
399
399
|
logger.info("Running bootstrapper for driver", { driverId: b.id || bootstrapper.type });
|
|
400
400
|
if (b.isDefault) {
|
|
401
401
|
defaultDriverId = b.id || bootstrapper.type;
|
|
@@ -413,7 +413,9 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
413
413
|
|
|
414
414
|
if (bootstrapper.initializeRealtime) {
|
|
415
415
|
const realtime = await bootstrapper.initializeRealtime({}, driverResult);
|
|
416
|
-
|
|
416
|
+
if (realtime) {
|
|
417
|
+
realtimeServices[b.id || bootstrapper.type] = realtime;
|
|
418
|
+
}
|
|
417
419
|
}
|
|
418
420
|
}
|
|
419
421
|
|
|
@@ -424,9 +426,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
424
426
|
if (!defaultDriver || !defaultDriverResult) {
|
|
425
427
|
throw new Error("Default driver not initialized by bootstrappers");
|
|
426
428
|
}
|
|
427
|
-
const defaultBootstrapper = bootstrappers.find(b =>
|
|
428
|
-
id?: string
|
|
429
|
-
}).id === defaultDriverId || b.type === defaultDriverId) || bootstrappers[0];
|
|
429
|
+
const defaultBootstrapper = bootstrappers.find(b => b.id === defaultDriverId || b.type === defaultDriverId) || bootstrappers[0];
|
|
430
430
|
const defaultRealtimeService = defaultDriverResult.realtimeProvider;
|
|
431
431
|
|
|
432
432
|
// 2. Initialize Auth & History via the default driver's bootstrapper
|
|
@@ -721,7 +721,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
721
721
|
// maxFileSize (default 50MB), which overrides the global API body limit.
|
|
722
722
|
const storageMaxSize = (
|
|
723
723
|
config.storage && typeof config.storage === "object" && "type" in config.storage
|
|
724
|
-
? (config.storage as BackendStorageConfig
|
|
724
|
+
? (config.storage as BackendStorageConfig).maxFileSize
|
|
725
725
|
: undefined
|
|
726
726
|
) ?? 50 * 1024 * 1024;
|
|
727
727
|
|
|
@@ -998,12 +998,8 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
998
998
|
}
|
|
999
999
|
}
|
|
1000
1000
|
|
|
1001
|
-
if (
|
|
1002
|
-
initializeWebsockets
|
|
1003
|
-
}).initializeWebsockets) {
|
|
1004
|
-
await (defaultBootstrapper as BackendBootstrapper & {
|
|
1005
|
-
initializeWebsockets: (...args: unknown[]) => unknown
|
|
1006
|
-
}).initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth, authAdapter);
|
|
1001
|
+
if (defaultBootstrapper.initializeWebsockets && defaultRealtimeService) {
|
|
1002
|
+
await defaultBootstrapper.initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth, authAdapter);
|
|
1007
1003
|
}
|
|
1008
1004
|
|
|
1009
1005
|
logger.info("Rebase Backend Initialized");
|
|
@@ -1061,15 +1057,11 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
1061
1057
|
// timer callbacks don't fire against a closed pool.
|
|
1062
1058
|
for (const [key, rt] of Object.entries(realtimeServices)) {
|
|
1063
1059
|
try {
|
|
1064
|
-
|
|
1065
|
-
destroy
|
|
1066
|
-
stopListening?: () => Promise<void>
|
|
1067
|
-
};
|
|
1068
|
-
if (typeof rtWithLifecycle.destroy === "function") {
|
|
1069
|
-
await rtWithLifecycle.destroy();
|
|
1060
|
+
if (typeof rt.destroy === "function") {
|
|
1061
|
+
await rt.destroy();
|
|
1070
1062
|
logger.info(`Realtime service "${key}" destroyed`);
|
|
1071
|
-
} else if (typeof
|
|
1072
|
-
await
|
|
1063
|
+
} else if (typeof rt.stopListening === "function") {
|
|
1064
|
+
await rt.stopListening();
|
|
1073
1065
|
logger.info(`Realtime service "${key}" LISTEN client stopped`);
|
|
1074
1066
|
}
|
|
1075
1067
|
} catch (err) {
|