@terreno/api 0.0.18 → 0.2.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 +73 -3
- package/dist/api.d.ts +96 -3
- package/dist/api.js +159 -11
- package/dist/api.test.js +906 -2
- package/dist/auth.js +3 -1
- package/dist/betterAuth.d.ts +91 -0
- package/dist/betterAuth.js +8 -0
- package/dist/betterAuth.test.d.ts +1 -0
- package/dist/betterAuth.test.js +181 -0
- package/dist/betterAuthApp.d.ts +22 -0
- package/dist/betterAuthApp.js +38 -0
- package/dist/betterAuthApp.test.d.ts +1 -0
- package/dist/betterAuthApp.test.js +242 -0
- package/dist/betterAuthSetup.d.ts +60 -0
- package/dist/betterAuthSetup.js +278 -0
- package/dist/betterAuthSetup.test.d.ts +1 -0
- package/dist/betterAuthSetup.test.js +684 -0
- package/dist/errors.js +14 -11
- package/dist/example.js +7 -7
- package/dist/expressServer.js +2 -2
- package/dist/githubAuth.test.js +3 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/openApi.test.js +8 -5
- package/dist/openApiBuilder.d.ts +69 -1
- package/dist/openApiBuilder.js +109 -5
- package/dist/openApiValidator.d.ts +296 -0
- package/dist/openApiValidator.js +698 -0
- package/dist/openApiValidator.test.d.ts +1 -0
- package/dist/openApiValidator.test.js +346 -0
- package/dist/plugins.test.js +3 -3
- package/dist/terrenoApp.d.ts +189 -0
- package/dist/terrenoApp.js +352 -0
- package/dist/terrenoApp.test.d.ts +1 -0
- package/dist/terrenoApp.test.js +264 -0
- package/dist/terrenoPlugin.d.ts +38 -0
- package/dist/terrenoPlugin.js +2 -0
- package/dist/tests.js +34 -24
- package/package.json +8 -2
- package/src/__snapshots__/openApi.test.ts.snap +399 -0
- package/src/__snapshots__/openApiBuilder.test.ts.snap +108 -0
- package/src/api.test.ts +743 -2
- package/src/api.ts +270 -6
- package/src/auth.ts +3 -1
- package/src/betterAuth.test.ts +160 -0
- package/src/betterAuth.ts +104 -0
- package/src/betterAuthApp.test.ts +114 -0
- package/src/betterAuthApp.ts +60 -0
- package/src/betterAuthSetup.test.ts +485 -0
- package/src/betterAuthSetup.ts +251 -0
- package/src/errors.ts +14 -11
- package/src/example.ts +7 -7
- package/src/expressServer.ts +4 -5
- package/src/githubAuth.test.ts +3 -3
- package/src/index.ts +6 -0
- package/src/openApi.test.ts +8 -5
- package/src/openApiBuilder.ts +188 -15
- package/src/openApiValidator.test.ts +241 -0
- package/src/openApiValidator.ts +860 -0
- package/src/plugins.test.ts +3 -3
- package/src/terrenoApp.test.ts +201 -0
- package/src/terrenoApp.ts +347 -0
- package/src/terrenoPlugin.ts +39 -0
- package/src/tests.ts +34 -24
- package/.cursorrules +0 -107
- package/.windsurfrules +0 -107
- package/AGENTS.md +0 -313
- package/dist/response.d.ts +0 -0
- package/dist/response.js +0 -1
- package/index.ts +0 -1
- package/src/response.ts +0 -0
package/src/api.ts
CHANGED
|
@@ -18,6 +18,12 @@ import {
|
|
|
18
18
|
listOpenApiMiddleware,
|
|
19
19
|
patchOpenApiMiddleware,
|
|
20
20
|
} from "./openApi";
|
|
21
|
+
import {
|
|
22
|
+
buildQuerySchemaFromFields,
|
|
23
|
+
type ModelRouterValidationOptions,
|
|
24
|
+
validateModelRequestBody,
|
|
25
|
+
validateQueryParams,
|
|
26
|
+
} from "./openApiValidator";
|
|
21
27
|
import {checkPermissions, permissionMiddleware, type RESTPermissions} from "./permissions";
|
|
22
28
|
import type {PopulatePath} from "./populate";
|
|
23
29
|
import {
|
|
@@ -263,6 +269,19 @@ export interface ModelRouterOptions<T> {
|
|
|
263
269
|
* that you want to be documented and typed in the SDK.
|
|
264
270
|
*/
|
|
265
271
|
openApiExtraModelProperties?: any;
|
|
272
|
+
/**
|
|
273
|
+
* Enable runtime validation of request bodies against the OpenAPI schema.
|
|
274
|
+
* When enabled, requests that don't match the documented schema will return 400 errors.
|
|
275
|
+
*
|
|
276
|
+
* Can be set to:
|
|
277
|
+
* - `true`: Enable validation for create and update operations
|
|
278
|
+
* - `false`: Disable validation (default)
|
|
279
|
+
* - Object with `validateCreate` and `validateUpdate` booleans for fine-grained control
|
|
280
|
+
*
|
|
281
|
+
* Note: Global validation can be enabled via `configureOpenApiValidator()`.
|
|
282
|
+
* This option overrides the global setting for this specific router.
|
|
283
|
+
*/
|
|
284
|
+
validation?: boolean | ModelRouterValidationOptions;
|
|
266
285
|
}
|
|
267
286
|
|
|
268
287
|
// Ensures query params are allowed. Also checks nested query params when using $and/$or.
|
|
@@ -309,13 +328,143 @@ function checkQueryParamAllowed(
|
|
|
309
328
|
// return result;
|
|
310
329
|
// }
|
|
311
330
|
|
|
331
|
+
// Helper to determine if validation should be enabled for a specific operation.
|
|
332
|
+
// When options.validation is not set, returns true — the middleware's own
|
|
333
|
+
// isConfigured check will decide whether to actually validate.
|
|
334
|
+
function shouldValidate(
|
|
335
|
+
options: ModelRouterOptions<any>,
|
|
336
|
+
operation: "create" | "update" | "query"
|
|
337
|
+
): boolean {
|
|
338
|
+
// Check route-specific validation option first
|
|
339
|
+
if (options.validation !== undefined) {
|
|
340
|
+
if (typeof options.validation === "boolean") {
|
|
341
|
+
return options.validation;
|
|
342
|
+
}
|
|
343
|
+
if (operation === "create") {
|
|
344
|
+
return options.validation.validateCreate ?? true;
|
|
345
|
+
}
|
|
346
|
+
if (operation === "update") {
|
|
347
|
+
return options.validation.validateUpdate ?? true;
|
|
348
|
+
}
|
|
349
|
+
return options.validation.validateQuery ?? true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Default: let middleware's isConfigured check decide
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Get body validation middleware if validation is enabled
|
|
357
|
+
function getBodyValidationMiddleware<T>(
|
|
358
|
+
model: Model<T>,
|
|
359
|
+
options: ModelRouterOptions<T>,
|
|
360
|
+
operation: "create" | "update"
|
|
361
|
+
): (req: Request, res: Response, next: NextFunction) => void {
|
|
362
|
+
const validationOptions: import("./openApiValidator").RequestBodyValidatorOptions = {};
|
|
363
|
+
if (!shouldValidate(options, operation)) {
|
|
364
|
+
validationOptions.enabled = false;
|
|
365
|
+
}
|
|
366
|
+
if (typeof options.validation === "object") {
|
|
367
|
+
if (options.validation.onError) {
|
|
368
|
+
validationOptions.onError = options.validation.onError;
|
|
369
|
+
}
|
|
370
|
+
if (options.validation.onAdditionalPropertiesRemoved) {
|
|
371
|
+
validationOptions.onAdditionalPropertiesRemoved =
|
|
372
|
+
options.validation.onAdditionalPropertiesRemoved;
|
|
373
|
+
}
|
|
374
|
+
const excludeFields =
|
|
375
|
+
operation === "create"
|
|
376
|
+
? options.validation.excludeFromCreate
|
|
377
|
+
: options.validation.excludeFromUpdate;
|
|
378
|
+
if (excludeFields?.length) {
|
|
379
|
+
validationOptions.excludeFields = excludeFields;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return validateModelRequestBody(model, validationOptions);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Get query validation middleware if validation is enabled
|
|
387
|
+
function getQueryValidationMiddleware<T>(
|
|
388
|
+
model: Model<T>,
|
|
389
|
+
options: ModelRouterOptions<T>
|
|
390
|
+
): (req: Request, res: Response, next: NextFunction) => void {
|
|
391
|
+
const querySchema = buildQuerySchemaFromFields(model, options.queryFields);
|
|
392
|
+
const validationOptions: import("./openApiValidator").QueryValidatorOptions = {};
|
|
393
|
+
if (!shouldValidate(options, "query")) {
|
|
394
|
+
validationOptions.enabled = false;
|
|
395
|
+
}
|
|
396
|
+
if (typeof options.validation === "object" && options.validation.onError) {
|
|
397
|
+
validationOptions.onError = options.validation.onError;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return validateQueryParams(querySchema, validationOptions);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Registration object returned by modelRouter when called with a path.
|
|
405
|
+
*
|
|
406
|
+
* Used with `TerrenoApp.register()` to mount model routers at specific paths.
|
|
407
|
+
* Contains the Express router and the path it should be mounted at.
|
|
408
|
+
*
|
|
409
|
+
* @see modelRouter for creating registrations
|
|
410
|
+
* @see TerrenoApp for registering routers
|
|
411
|
+
*/
|
|
412
|
+
export interface ModelRouterRegistration {
|
|
413
|
+
/** Internal type discriminator for registration detection */
|
|
414
|
+
__type: "modelRouter";
|
|
415
|
+
/** The path where the router should be mounted (e.g., "/todos") */
|
|
416
|
+
path: string;
|
|
417
|
+
/** The Express router containing CRUD endpoints */
|
|
418
|
+
router: express.Router;
|
|
419
|
+
}
|
|
420
|
+
|
|
312
421
|
/**
|
|
313
422
|
* Create a set of CRUD routes given a Mongoose model and configuration options.
|
|
314
423
|
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
424
|
+
* When called with a path as the first argument, returns a `ModelRouterRegistration` that can be
|
|
425
|
+
* passed to `TerrenoApp.register()`.
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* // Traditional usage (returns express.Router):
|
|
429
|
+
* router.use("/todos", modelRouter(Todo, options));
|
|
430
|
+
*
|
|
431
|
+
* // Registration usage (returns ModelRouterRegistration):
|
|
432
|
+
* const todoRouter = modelRouter("/todos", Todo, options);
|
|
433
|
+
* app.register(todoRouter);
|
|
317
434
|
*/
|
|
318
|
-
export function modelRouter<T>(
|
|
435
|
+
export function modelRouter<T>(
|
|
436
|
+
path: string,
|
|
437
|
+
model: Model<T>,
|
|
438
|
+
options: ModelRouterOptions<T>
|
|
439
|
+
): ModelRouterRegistration;
|
|
440
|
+
export function modelRouter<T>(model: Model<T>, options: ModelRouterOptions<T>): express.Router;
|
|
441
|
+
export function modelRouter<T>(
|
|
442
|
+
pathOrModel: string | Model<T>,
|
|
443
|
+
modelOrOptions: Model<T> | ModelRouterOptions<T>,
|
|
444
|
+
maybeOptions?: ModelRouterOptions<T>
|
|
445
|
+
): express.Router | ModelRouterRegistration {
|
|
446
|
+
let model: Model<T>;
|
|
447
|
+
let options: ModelRouterOptions<T>;
|
|
448
|
+
let path: string | undefined;
|
|
449
|
+
|
|
450
|
+
if (typeof pathOrModel === "string") {
|
|
451
|
+
path = pathOrModel;
|
|
452
|
+
model = modelOrOptions as Model<T>;
|
|
453
|
+
options = maybeOptions as ModelRouterOptions<T>;
|
|
454
|
+
} else {
|
|
455
|
+
model = pathOrModel;
|
|
456
|
+
options = modelOrOptions as ModelRouterOptions<T>;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const router = _buildModelRouter(model, options);
|
|
460
|
+
|
|
461
|
+
if (path !== undefined) {
|
|
462
|
+
return {__type: "modelRouter", path, router};
|
|
463
|
+
}
|
|
464
|
+
return router;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function _buildModelRouter<T>(model: Model<T>, options: ModelRouterOptions<T>): express.Router {
|
|
319
468
|
const router = express.Router();
|
|
320
469
|
|
|
321
470
|
// Do before the other router options so endpoints take priority.
|
|
@@ -325,12 +474,18 @@ export function modelRouter<T>(model: Model<T>, options: ModelRouterOptions<T>):
|
|
|
325
474
|
|
|
326
475
|
const responseHandler = options.responseHandler ?? defaultResponseHandler;
|
|
327
476
|
|
|
477
|
+
// Always install validation middleware — they are no-ops until configureOpenApiValidator() is called
|
|
478
|
+
const createValidation = getBodyValidationMiddleware(model, options, "create");
|
|
479
|
+
const updateValidation = getBodyValidationMiddleware(model, options, "update");
|
|
480
|
+
const queryValidation = getQueryValidationMiddleware(model, options);
|
|
481
|
+
|
|
328
482
|
router.post(
|
|
329
483
|
"/",
|
|
330
484
|
[
|
|
331
485
|
authenticateMiddleware(options.allowAnonymous),
|
|
332
486
|
createOpenApiMiddleware(model, options),
|
|
333
487
|
permissionMiddleware(model, options),
|
|
488
|
+
createValidation,
|
|
334
489
|
],
|
|
335
490
|
asyncHandler(async (req: Request, res: Response) => {
|
|
336
491
|
let body: Partial<T> | (Partial<T> | undefined)[] | null | undefined;
|
|
@@ -439,6 +594,7 @@ export function modelRouter<T>(model: Model<T>, options: ModelRouterOptions<T>):
|
|
|
439
594
|
authenticateMiddleware(options.allowAnonymous),
|
|
440
595
|
permissionMiddleware(model, options),
|
|
441
596
|
listOpenApiMiddleware(model, options),
|
|
597
|
+
queryValidation,
|
|
442
598
|
],
|
|
443
599
|
asyncHandler(async (req: Request, res: Response) => {
|
|
444
600
|
let query: any = {};
|
|
@@ -625,6 +781,7 @@ export function modelRouter<T>(model: Model<T>, options: ModelRouterOptions<T>):
|
|
|
625
781
|
authenticateMiddleware(options.allowAnonymous),
|
|
626
782
|
patchOpenApiMiddleware(model, options),
|
|
627
783
|
permissionMiddleware(model, options),
|
|
784
|
+
updateValidation,
|
|
628
785
|
],
|
|
629
786
|
asyncHandler(async (req: Request, res: Response) => {
|
|
630
787
|
let doc: mongoose.Document & T = (req as any).obj;
|
|
@@ -984,9 +1141,116 @@ export function modelRouter<T>(model: Model<T>, options: ModelRouterOptions<T>):
|
|
|
984
1141
|
return router;
|
|
985
1142
|
}
|
|
986
1143
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1144
|
+
/**
|
|
1145
|
+
* Options for the asyncHandler function.
|
|
1146
|
+
*/
|
|
1147
|
+
export interface AsyncHandlerOptions {
|
|
1148
|
+
/**
|
|
1149
|
+
* Schema for validating request body.
|
|
1150
|
+
* When provided and validation is enabled, the request body will be validated
|
|
1151
|
+
* against this schema before the handler runs.
|
|
1152
|
+
*/
|
|
1153
|
+
bodySchema?: Record<string, import("./openApiBuilder").OpenApiSchemaProperty>;
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Schema for validating query parameters.
|
|
1157
|
+
* When provided and validation is enabled, query params will be validated
|
|
1158
|
+
* against this schema before the handler runs.
|
|
1159
|
+
*/
|
|
1160
|
+
querySchema?: Record<string, import("./openApiBuilder").OpenApiSchemaProperty>;
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Override global validation setting for this handler.
|
|
1164
|
+
* - `true`: Enable validation regardless of global setting
|
|
1165
|
+
* - `false`: Disable validation regardless of global setting
|
|
1166
|
+
* - `undefined`: Use global setting
|
|
1167
|
+
*/
|
|
1168
|
+
validate?: boolean;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Wraps async route handlers to properly catch and forward errors.
|
|
1173
|
+
*
|
|
1174
|
+
* Since Express doesn't handle async routes well, wrap them with this function.
|
|
1175
|
+
* Optionally supports integrated request validation.
|
|
1176
|
+
*
|
|
1177
|
+
* @param fn - The async route handler function
|
|
1178
|
+
* @param options - Optional configuration for validation
|
|
1179
|
+
* @returns Express middleware function
|
|
1180
|
+
*
|
|
1181
|
+
* @example
|
|
1182
|
+
* ```typescript
|
|
1183
|
+
* // Basic usage without validation
|
|
1184
|
+
* router.post("/users", asyncHandler(async (req, res) => {
|
|
1185
|
+
* // handler code
|
|
1186
|
+
* }));
|
|
1187
|
+
*
|
|
1188
|
+
* // With integrated validation
|
|
1189
|
+
* router.post("/users", asyncHandler(async (req, res) => {
|
|
1190
|
+
* // handler code - body is already validated
|
|
1191
|
+
* }, {
|
|
1192
|
+
* bodySchema: {
|
|
1193
|
+
* name: {type: "string", required: true},
|
|
1194
|
+
* email: {type: "string", format: "email", required: true},
|
|
1195
|
+
* },
|
|
1196
|
+
* validate: true,
|
|
1197
|
+
* }));
|
|
1198
|
+
* ```
|
|
1199
|
+
*/
|
|
1200
|
+
export const asyncHandler = (fn: any, options?: AsyncHandlerOptions) => {
|
|
1201
|
+
// If no validation options, return simple handler
|
|
1202
|
+
if (!options?.bodySchema && !options?.querySchema) {
|
|
1203
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
1204
|
+
return Promise.resolve(fn(req, res, next)).catch(next);
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Import validation functions dynamically to avoid circular deps at module load
|
|
1209
|
+
const {
|
|
1210
|
+
validateRequestBody,
|
|
1211
|
+
validateQueryParams,
|
|
1212
|
+
getOpenApiValidatorConfig,
|
|
1213
|
+
} = require("./openApiValidator");
|
|
1214
|
+
|
|
1215
|
+
// Build validation middleware
|
|
1216
|
+
const validators: ((req: Request, res: Response, next: NextFunction) => void)[] = [];
|
|
1217
|
+
|
|
1218
|
+
// Determine if validation should be enabled
|
|
1219
|
+
const shouldValidate = options.validate ?? getOpenApiValidatorConfig().validateRequests ?? false;
|
|
1220
|
+
|
|
1221
|
+
if (shouldValidate) {
|
|
1222
|
+
if (options.bodySchema) {
|
|
1223
|
+
validators.push(validateRequestBody(options.bodySchema, {enabled: true}));
|
|
1224
|
+
}
|
|
1225
|
+
if (options.querySchema) {
|
|
1226
|
+
validators.push(validateQueryParams(options.querySchema, {enabled: true}));
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
return (req: Request, res: Response, next: NextFunction) => {
|
|
1231
|
+
// Run validators sequentially, then the handler
|
|
1232
|
+
const runValidators = (index: number): void => {
|
|
1233
|
+
if (index >= validators.length) {
|
|
1234
|
+
// All validators passed, run the actual handler
|
|
1235
|
+
Promise.resolve(fn(req, res, next)).catch(next);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
try {
|
|
1240
|
+
validators[index](req, res, (err?: any) => {
|
|
1241
|
+
if (err) {
|
|
1242
|
+
next(err);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
runValidators(index + 1);
|
|
1246
|
+
});
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
next(err);
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
runValidators(0);
|
|
1253
|
+
};
|
|
990
1254
|
};
|
|
991
1255
|
|
|
992
1256
|
// For backwards compatibility with the old names.
|
package/src/auth.ts
CHANGED
|
@@ -299,7 +299,9 @@ export function addAuthRoutes(
|
|
|
299
299
|
logger.warn(`Invalid login: ${info}`);
|
|
300
300
|
return res.status(401).json({message: info?.message});
|
|
301
301
|
}
|
|
302
|
-
|
|
302
|
+
if (process.env.NODE_ENV !== "test") {
|
|
303
|
+
logger.info(`User logged in: ${user._id}, type: ${(user as any).type || "N/A"}`);
|
|
304
|
+
}
|
|
303
305
|
const tokens = await generateTokens(user, authOptions);
|
|
304
306
|
return res.json({
|
|
305
307
|
data: {refreshToken: tokens.refreshToken, token: tokens.token, userId: user?._id},
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {describe, expect, it} from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type {AuthProvider, BetterAuthConfig, BetterAuthOAuthProvider} from "./betterAuth";
|
|
4
|
+
|
|
5
|
+
describe("Better Auth types", () => {
|
|
6
|
+
it("defines BetterAuthOAuthProvider interface correctly", () => {
|
|
7
|
+
const provider: BetterAuthOAuthProvider = {
|
|
8
|
+
clientId: "test-client-id",
|
|
9
|
+
clientSecret: "test-client-secret",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
expect(provider.clientId).toBe("test-client-id");
|
|
13
|
+
expect(provider.clientSecret).toBe("test-client-secret");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("defines BetterAuthConfig interface correctly", () => {
|
|
17
|
+
const config: BetterAuthConfig = {
|
|
18
|
+
basePath: "/api/auth",
|
|
19
|
+
baseURL: "http://localhost:3000",
|
|
20
|
+
enabled: true,
|
|
21
|
+
githubOAuth: {
|
|
22
|
+
clientId: "github-client-id",
|
|
23
|
+
clientSecret: "github-client-secret",
|
|
24
|
+
},
|
|
25
|
+
googleOAuth: {
|
|
26
|
+
clientId: "google-client-id",
|
|
27
|
+
clientSecret: "google-client-secret",
|
|
28
|
+
},
|
|
29
|
+
secret: "test-secret",
|
|
30
|
+
trustedOrigins: ["terreno://", "exp://"],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
expect(config.enabled).toBe(true);
|
|
34
|
+
expect(config.googleOAuth?.clientId).toBe("google-client-id");
|
|
35
|
+
expect(config.githubOAuth?.clientId).toBe("github-client-id");
|
|
36
|
+
expect(config.trustedOrigins).toContain("terreno://");
|
|
37
|
+
expect(config.basePath).toBe("/api/auth");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("allows minimal BetterAuthConfig", () => {
|
|
41
|
+
const minimalConfig: BetterAuthConfig = {
|
|
42
|
+
enabled: false,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
expect(minimalConfig.enabled).toBe(false);
|
|
46
|
+
expect(minimalConfig.googleOAuth).toBeUndefined();
|
|
47
|
+
expect(minimalConfig.basePath).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("defines AuthProvider type correctly", () => {
|
|
51
|
+
const jwtProvider: AuthProvider = "jwt";
|
|
52
|
+
const betterAuthProvider: AuthProvider = "better-auth";
|
|
53
|
+
|
|
54
|
+
expect(jwtProvider).toBe("jwt");
|
|
55
|
+
expect(betterAuthProvider).toBe("better-auth");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("Better Auth setup", () => {
|
|
60
|
+
it("syncBetterAuthUser creates a new user when not found", async () => {
|
|
61
|
+
// This test would require mocking MongoDB which is complex
|
|
62
|
+
// For now we test the interface structure
|
|
63
|
+
const betterAuthUser = {
|
|
64
|
+
createdAt: new Date(),
|
|
65
|
+
email: "test@example.com",
|
|
66
|
+
emailVerified: true,
|
|
67
|
+
id: "ba-user-123",
|
|
68
|
+
image: null,
|
|
69
|
+
name: "Test User",
|
|
70
|
+
updatedAt: new Date(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
expect(betterAuthUser.id).toBe("ba-user-123");
|
|
74
|
+
expect(betterAuthUser.email).toBe("test@example.com");
|
|
75
|
+
expect(betterAuthUser.name).toBe("Test User");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("BetterAuthSession has correct structure", () => {
|
|
79
|
+
const session = {
|
|
80
|
+
createdAt: new Date(),
|
|
81
|
+
expiresAt: new Date(Date.now() + 3600000),
|
|
82
|
+
id: "session-123",
|
|
83
|
+
ipAddress: "127.0.0.1",
|
|
84
|
+
updatedAt: new Date(),
|
|
85
|
+
userAgent: "Mozilla/5.0",
|
|
86
|
+
userId: "user-456",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
expect(session.id).toBe("session-123");
|
|
90
|
+
expect(session.userId).toBe("user-456");
|
|
91
|
+
expect(session.expiresAt.getTime()).toBeGreaterThan(Date.now());
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("BetterAuthSessionData combines session and user", () => {
|
|
95
|
+
const sessionData = {
|
|
96
|
+
session: {
|
|
97
|
+
createdAt: new Date(),
|
|
98
|
+
expiresAt: new Date(),
|
|
99
|
+
id: "session-123",
|
|
100
|
+
ipAddress: null,
|
|
101
|
+
updatedAt: new Date(),
|
|
102
|
+
userAgent: null,
|
|
103
|
+
userId: "user-456",
|
|
104
|
+
},
|
|
105
|
+
user: {
|
|
106
|
+
createdAt: new Date(),
|
|
107
|
+
email: "test@example.com",
|
|
108
|
+
emailVerified: false,
|
|
109
|
+
id: "user-456",
|
|
110
|
+
image: null,
|
|
111
|
+
name: "Test",
|
|
112
|
+
updatedAt: new Date(),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
expect(sessionData.session.userId).toBe(sessionData.user.id);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("Better Auth config validation", () => {
|
|
121
|
+
it("basePath defaults to /api/auth when not specified", () => {
|
|
122
|
+
const config: BetterAuthConfig = {
|
|
123
|
+
enabled: true,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const basePath = config.basePath ?? "/api/auth";
|
|
127
|
+
expect(basePath).toBe("/api/auth");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("trustedOrigins defaults to empty array when not specified", () => {
|
|
131
|
+
const config: BetterAuthConfig = {
|
|
132
|
+
enabled: true,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const trustedOrigins = config.trustedOrigins ?? [];
|
|
136
|
+
expect(trustedOrigins).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("supports multiple OAuth providers simultaneously", () => {
|
|
140
|
+
const config: BetterAuthConfig = {
|
|
141
|
+
appleOAuth: {
|
|
142
|
+
clientId: "apple-id",
|
|
143
|
+
clientSecret: "apple-secret",
|
|
144
|
+
},
|
|
145
|
+
enabled: true,
|
|
146
|
+
githubOAuth: {
|
|
147
|
+
clientId: "github-id",
|
|
148
|
+
clientSecret: "github-secret",
|
|
149
|
+
},
|
|
150
|
+
googleOAuth: {
|
|
151
|
+
clientId: "google-id",
|
|
152
|
+
clientSecret: "google-secret",
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
expect(config.googleOAuth).toBeDefined();
|
|
157
|
+
expect(config.githubOAuth).toBeDefined();
|
|
158
|
+
expect(config.appleOAuth).toBeDefined();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Better Auth types and configuration interfaces for @terreno/api.
|
|
3
|
+
*
|
|
4
|
+
* These types support optional Better Auth integration alongside the existing
|
|
5
|
+
* JWT/Passport authentication system.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* OAuth provider configuration for Better Auth.
|
|
10
|
+
*/
|
|
11
|
+
export interface BetterAuthOAuthProvider {
|
|
12
|
+
clientId: string;
|
|
13
|
+
clientSecret: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configuration options for Better Auth integration.
|
|
18
|
+
*/
|
|
19
|
+
export interface BetterAuthConfig {
|
|
20
|
+
/**
|
|
21
|
+
* Whether Better Auth is enabled for this server.
|
|
22
|
+
*/
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Google OAuth provider configuration.
|
|
27
|
+
*/
|
|
28
|
+
googleOAuth?: BetterAuthOAuthProvider;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Apple OAuth provider configuration.
|
|
32
|
+
*/
|
|
33
|
+
appleOAuth?: BetterAuthOAuthProvider;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* GitHub OAuth provider configuration.
|
|
37
|
+
*/
|
|
38
|
+
githubOAuth?: BetterAuthOAuthProvider;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Trusted origins for CORS and redirect validation.
|
|
42
|
+
* Include your app's deep link schemes (e.g., "terreno://", "exp://").
|
|
43
|
+
*/
|
|
44
|
+
trustedOrigins?: string[];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Base path for Better Auth routes.
|
|
48
|
+
* @default "/api/auth"
|
|
49
|
+
*/
|
|
50
|
+
basePath?: string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Secret key for Better Auth session encryption.
|
|
54
|
+
* If not provided, falls back to BETTER_AUTH_SECRET environment variable.
|
|
55
|
+
*/
|
|
56
|
+
secret?: string;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Base URL for the auth server.
|
|
60
|
+
* If not provided, falls back to BETTER_AUTH_URL environment variable.
|
|
61
|
+
*/
|
|
62
|
+
baseURL?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Auth provider selection for setupServer.
|
|
67
|
+
* - "jwt": Traditional JWT/Passport authentication (default)
|
|
68
|
+
* - "better-auth": Better Auth with OAuth support
|
|
69
|
+
*/
|
|
70
|
+
export type AuthProvider = "jwt" | "better-auth";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* User data from Better Auth session.
|
|
74
|
+
*/
|
|
75
|
+
export interface BetterAuthUser {
|
|
76
|
+
id: string;
|
|
77
|
+
email: string;
|
|
78
|
+
name: string | null;
|
|
79
|
+
image: string | null;
|
|
80
|
+
emailVerified: boolean;
|
|
81
|
+
createdAt: Date;
|
|
82
|
+
updatedAt: Date;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Session data from Better Auth.
|
|
87
|
+
*/
|
|
88
|
+
export interface BetterAuthSession {
|
|
89
|
+
id: string;
|
|
90
|
+
userId: string;
|
|
91
|
+
expiresAt: Date;
|
|
92
|
+
ipAddress: string | null;
|
|
93
|
+
userAgent: string | null;
|
|
94
|
+
createdAt: Date;
|
|
95
|
+
updatedAt: Date;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Combined session and user data from Better Auth.
|
|
100
|
+
*/
|
|
101
|
+
export interface BetterAuthSessionData {
|
|
102
|
+
session: BetterAuthSession;
|
|
103
|
+
user: BetterAuthUser;
|
|
104
|
+
}
|