@terreno/api 0.1.0 → 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.
Files changed (40) hide show
  1. package/dist/api.d.ts +28 -2
  2. package/dist/api.js +20 -7
  3. package/dist/betterAuth.d.ts +91 -0
  4. package/dist/betterAuth.js +8 -0
  5. package/dist/betterAuth.test.d.ts +1 -0
  6. package/dist/betterAuth.test.js +181 -0
  7. package/dist/betterAuthApp.d.ts +22 -0
  8. package/dist/betterAuthApp.js +38 -0
  9. package/dist/betterAuthApp.test.d.ts +1 -0
  10. package/dist/betterAuthApp.test.js +242 -0
  11. package/dist/betterAuthSetup.d.ts +60 -0
  12. package/dist/betterAuthSetup.js +278 -0
  13. package/dist/betterAuthSetup.test.d.ts +1 -0
  14. package/dist/betterAuthSetup.test.js +684 -0
  15. package/dist/expressServer.js +2 -2
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.js +4 -0
  18. package/dist/terrenoApp.d.ts +189 -0
  19. package/dist/terrenoApp.js +352 -0
  20. package/dist/terrenoApp.test.d.ts +1 -0
  21. package/dist/terrenoApp.test.js +264 -0
  22. package/dist/terrenoPlugin.d.ts +34 -0
  23. package/package.json +6 -3
  24. package/src/api.ts +61 -3
  25. package/src/betterAuth.test.ts +160 -0
  26. package/src/betterAuth.ts +104 -0
  27. package/src/betterAuthApp.test.ts +114 -0
  28. package/src/betterAuthApp.ts +60 -0
  29. package/src/betterAuthSetup.test.ts +485 -0
  30. package/src/betterAuthSetup.ts +251 -0
  31. package/src/expressServer.ts +4 -5
  32. package/src/index.ts +4 -0
  33. package/src/openApiValidator.ts +1 -1
  34. package/src/terrenoApp.test.ts +201 -0
  35. package/src/terrenoApp.ts +347 -0
  36. package/src/terrenoPlugin.ts +34 -0
  37. package/.claude/CLAUDE.local.md +0 -204
  38. package/.cursor/rules/00-root.mdc +0 -338
  39. package/.github/copilot-instructions.md +0 -333
  40. package/AGENTS.md +0 -333
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15
+ return new (P || (P = Promise))(function (resolve, reject) {
16
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
20
+ });
21
+ };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
49
+ var __read = (this && this.__read) || function (o, n) {
50
+ var m = typeof Symbol === "function" && o[Symbol.iterator];
51
+ if (!m) return o;
52
+ var i = m.call(o), r, ar = [], e;
53
+ try {
54
+ while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
55
+ }
56
+ catch (error) { e = { error: error }; }
57
+ finally {
58
+ try {
59
+ if (r && !r.done && (m = i["return"])) m.call(i);
60
+ }
61
+ finally { if (e) throw e.error; }
62
+ }
63
+ return ar;
64
+ };
65
+ var __importDefault = (this && this.__importDefault) || function (mod) {
66
+ return (mod && mod.__esModule) ? mod : { "default": mod };
67
+ };
68
+ Object.defineProperty(exports, "__esModule", { value: true });
69
+ var bun_test_1 = require("bun:test");
70
+ var supertest_1 = __importDefault(require("supertest"));
71
+ var api_1 = require("./api");
72
+ var permissions_1 = require("./permissions");
73
+ var terrenoApp_1 = require("./terrenoApp");
74
+ var tests_1 = require("./tests");
75
+ (0, bun_test_1.describe)("TerrenoApp", function () {
76
+ var originalEnv = process.env;
77
+ (0, bun_test_1.beforeEach)(function () {
78
+ process.env = __assign(__assign({}, originalEnv), { REFRESH_TOKEN_SECRET: "test-refresh-secret", SESSION_SECRET: "test-session-secret", TOKEN_EXPIRES_IN: "1h", TOKEN_ISSUER: "test-issuer", TOKEN_SECRET: "test-secret" });
79
+ });
80
+ (0, bun_test_1.afterEach)(function () {
81
+ process.env = originalEnv;
82
+ });
83
+ (0, bun_test_1.describe)("build", function () {
84
+ (0, bun_test_1.it)("returns an express application without listening", function () {
85
+ var app = new terrenoApp_1.TerrenoApp({
86
+ skipListen: true,
87
+ userModel: tests_1.UserModel,
88
+ }).build();
89
+ (0, bun_test_1.expect)(app).toBeDefined();
90
+ });
91
+ (0, bun_test_1.it)("creates server with custom corsOrigin", function () {
92
+ var app = new terrenoApp_1.TerrenoApp({
93
+ corsOrigin: "https://example.com",
94
+ skipListen: true,
95
+ userModel: tests_1.UserModel,
96
+ }).build();
97
+ (0, bun_test_1.expect)(app).toBeDefined();
98
+ });
99
+ });
100
+ (0, bun_test_1.describe)("start", function () {
101
+ (0, bun_test_1.it)("returns an express application with skipListen", function () {
102
+ var app = new terrenoApp_1.TerrenoApp({
103
+ skipListen: true,
104
+ userModel: tests_1.UserModel,
105
+ }).start();
106
+ (0, bun_test_1.expect)(app).toBeDefined();
107
+ });
108
+ });
109
+ (0, bun_test_1.describe)("register with modelRouter", function () {
110
+ var admin;
111
+ (0, bun_test_1.beforeEach)(function () { return __awaiter(void 0, void 0, void 0, function () {
112
+ var _a;
113
+ return __generator(this, function (_b) {
114
+ switch (_b.label) {
115
+ case 0: return [4 /*yield*/, (0, tests_1.setupDb)()];
116
+ case 1:
117
+ _a = __read.apply(void 0, [_b.sent(), 1]), admin = _a[0];
118
+ return [2 /*return*/];
119
+ }
120
+ });
121
+ }); });
122
+ (0, bun_test_1.it)("mounts model router at the specified path", function () { return __awaiter(void 0, void 0, void 0, function () {
123
+ var foodRegistration, app, agent, res;
124
+ return __generator(this, function (_a) {
125
+ switch (_a.label) {
126
+ case 0:
127
+ foodRegistration = (0, api_1.modelRouter)("/food", tests_1.FoodModel, {
128
+ allowAnonymous: true,
129
+ permissions: {
130
+ create: [permissions_1.Permissions.IsAny],
131
+ delete: [permissions_1.Permissions.IsAny],
132
+ list: [permissions_1.Permissions.IsAny],
133
+ read: [permissions_1.Permissions.IsAny],
134
+ update: [permissions_1.Permissions.IsAny],
135
+ },
136
+ sort: "-created",
137
+ });
138
+ (0, bun_test_1.expect)(foodRegistration.__type).toBe("modelRouter");
139
+ (0, bun_test_1.expect)(foodRegistration.path).toBe("/food");
140
+ app = new terrenoApp_1.TerrenoApp({
141
+ skipListen: true,
142
+ userModel: tests_1.UserModel,
143
+ })
144
+ .register(foodRegistration)
145
+ .build();
146
+ return [4 /*yield*/, tests_1.FoodModel.create({
147
+ calories: 100,
148
+ name: "Apple",
149
+ ownerId: admin._id,
150
+ source: { name: "Nature" },
151
+ })];
152
+ case 1:
153
+ _a.sent();
154
+ return [4 /*yield*/, (0, tests_1.authAsUser)(app, "admin")];
155
+ case 2:
156
+ agent = _a.sent();
157
+ return [4 /*yield*/, agent.get("/food").expect(200)];
158
+ case 3:
159
+ res = _a.sent();
160
+ (0, bun_test_1.expect)(res.body.data).toHaveLength(1);
161
+ (0, bun_test_1.expect)(res.body.data[0].name).toBe("Apple");
162
+ return [2 /*return*/];
163
+ }
164
+ });
165
+ }); });
166
+ (0, bun_test_1.it)("supports chaining multiple registrations", function () { return __awaiter(void 0, void 0, void 0, function () {
167
+ var foodRegistration, app;
168
+ return __generator(this, function (_a) {
169
+ foodRegistration = (0, api_1.modelRouter)("/food", tests_1.FoodModel, {
170
+ allowAnonymous: true,
171
+ permissions: {
172
+ create: [permissions_1.Permissions.IsAny],
173
+ delete: [permissions_1.Permissions.IsAny],
174
+ list: [permissions_1.Permissions.IsAny],
175
+ read: [permissions_1.Permissions.IsAny],
176
+ update: [permissions_1.Permissions.IsAny],
177
+ },
178
+ });
179
+ app = new terrenoApp_1.TerrenoApp({
180
+ skipListen: true,
181
+ userModel: tests_1.UserModel,
182
+ })
183
+ .register(foodRegistration)
184
+ .build();
185
+ (0, bun_test_1.expect)(app).toBeDefined();
186
+ return [2 /*return*/];
187
+ });
188
+ }); });
189
+ });
190
+ (0, bun_test_1.describe)("register with plugin", function () {
191
+ (0, bun_test_1.it)("calls plugin.register with the express app", function () {
192
+ var registerFn = (0, bun_test_1.mock)(function () { });
193
+ var plugin = {
194
+ register: registerFn,
195
+ };
196
+ var app = new terrenoApp_1.TerrenoApp({
197
+ skipListen: true,
198
+ userModel: tests_1.UserModel,
199
+ })
200
+ .register(plugin)
201
+ .build();
202
+ (0, bun_test_1.expect)(registerFn).toHaveBeenCalledTimes(1);
203
+ // Verify the plugin received the express app
204
+ var calledWith = registerFn.mock.calls[0][0];
205
+ (0, bun_test_1.expect)(calledWith).toBe(app);
206
+ });
207
+ });
208
+ (0, bun_test_1.describe)("addMiddleware", function () {
209
+ (0, bun_test_1.it)("runs request handler middleware", function () { return __awaiter(void 0, void 0, void 0, function () {
210
+ var middlewareCalled, middleware, app;
211
+ return __generator(this, function (_a) {
212
+ switch (_a.label) {
213
+ case 0:
214
+ middlewareCalled = false;
215
+ middleware = function (_req, _res, next) {
216
+ middlewareCalled = true;
217
+ next();
218
+ };
219
+ app = new terrenoApp_1.TerrenoApp({
220
+ skipListen: true,
221
+ userModel: tests_1.UserModel,
222
+ })
223
+ .addMiddleware(middleware)
224
+ .build();
225
+ return [4 /*yield*/, (0, supertest_1.default)(app).get("/nonexistent").expect(404)];
226
+ case 1:
227
+ _a.sent();
228
+ (0, bun_test_1.expect)(middlewareCalled).toBe(true);
229
+ return [2 /*return*/];
230
+ }
231
+ });
232
+ }); });
233
+ });
234
+ (0, bun_test_1.describe)("modelRouter overload", function () {
235
+ (0, bun_test_1.it)("returns ModelRouterRegistration when path is provided", function () {
236
+ var result = (0, api_1.modelRouter)("/food", tests_1.FoodModel, {
237
+ permissions: {
238
+ create: [permissions_1.Permissions.IsAny],
239
+ delete: [permissions_1.Permissions.IsAny],
240
+ list: [permissions_1.Permissions.IsAny],
241
+ read: [permissions_1.Permissions.IsAny],
242
+ update: [permissions_1.Permissions.IsAny],
243
+ },
244
+ });
245
+ (0, bun_test_1.expect)(result.__type).toBe("modelRouter");
246
+ (0, bun_test_1.expect)(result.path).toBe("/food");
247
+ (0, bun_test_1.expect)(result.router).toBeDefined();
248
+ });
249
+ (0, bun_test_1.it)("returns express.Router when no path is provided", function () {
250
+ var result = (0, api_1.modelRouter)(tests_1.FoodModel, {
251
+ permissions: {
252
+ create: [permissions_1.Permissions.IsAny],
253
+ delete: [permissions_1.Permissions.IsAny],
254
+ list: [permissions_1.Permissions.IsAny],
255
+ read: [permissions_1.Permissions.IsAny],
256
+ update: [permissions_1.Permissions.IsAny],
257
+ },
258
+ });
259
+ // Should be a regular router (function), not a ModelRouterRegistration
260
+ (0, bun_test_1.expect)(typeof result).toBe("function");
261
+ (0, bun_test_1.expect)(result.__type).toBeUndefined();
262
+ });
263
+ });
264
+ });
@@ -1,4 +1,38 @@
1
1
  import type express from "express";
2
+ /**
3
+ * Interface for plugins that can be registered with TerrenoApp.
4
+ *
5
+ * Implement this interface to create reusable plugins that encapsulate
6
+ * routes, middleware, or other Express application setup. Plugins are
7
+ * registered via `TerrenoApp.register()` and are mounted after core
8
+ * authentication and OpenAPI middleware.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * class MyPlugin implements TerrenoPlugin {
13
+ * register(app: express.Application): void {
14
+ * app.get("/my-route", (req, res) => {
15
+ * res.json({ status: "ok" });
16
+ * });
17
+ * }
18
+ * }
19
+ *
20
+ * const app = new TerrenoApp({ userModel: User })
21
+ * .register(new MyPlugin())
22
+ * .start();
23
+ * ```
24
+ *
25
+ * @see TerrenoApp for the application builder that consumes plugins
26
+ * @see HealthApp for a built-in plugin example
27
+ */
2
28
  export interface TerrenoPlugin {
29
+ /**
30
+ * Register routes and middleware with the Express application.
31
+ *
32
+ * Called during `TerrenoApp.build()` after core middleware has been
33
+ * configured but before error handling middleware is added.
34
+ *
35
+ * @param app - The Express application instance to register with
36
+ */
3
37
  register(app: express.Application): void;
4
38
  }
package/package.json CHANGED
@@ -5,10 +5,11 @@
5
5
  },
6
6
  "dependencies": {
7
7
  "@sentry/bun": "^10.25.0",
8
+ "better-auth": "^1.2.8",
8
9
  "@sentry/profiling-node": "^10.25.0",
9
10
  "@types/qs": "^6.14.0",
10
11
  "@wesleytodd/openapi": "^1.1.0",
11
- "ajv": "^8.17.1",
12
+ "ajv": "8.18.0",
12
13
  "ajv-formats": "^3.0.1",
13
14
  "axios": "^1.13.2",
14
15
  "cors": "^2.8.5",
@@ -17,7 +18,7 @@
17
18
  "express": "^4.21.2",
18
19
  "generaterr": "^1.5.0",
19
20
  "jsonwebtoken": "^9.0.2",
20
- "lodash": "^4.17.21",
21
+ "lodash": "^4.17.23",
21
22
  "luxon": "^3.7.2",
22
23
  "mongoose": "8.18.1",
23
24
  "mongoose-to-swagger": "^1.4.0",
@@ -43,6 +44,7 @@
43
44
  "@types/express": "^4.17.21",
44
45
  "@types/jsonwebtoken": "^9.0.9",
45
46
  "@types/lodash": "^4.17.15",
47
+ "@types/mongodb": "^4.0.7",
46
48
  "@types/node": "^22.13.5",
47
49
  "@types/on-finished": "^2.3.4",
48
50
  "@types/passport": "^1.0.17",
@@ -52,6 +54,7 @@
52
54
  "@types/passport-local": "^1.0.38",
53
55
  "@types/sinon": "^17.0.4",
54
56
  "@types/supertest": "^6.0.2",
57
+ "mongodb-memory-server": "^11.0.1",
55
58
  "sinon": "^19.0.2",
56
59
  "supertest": "^7.0.0",
57
60
  "typedoc": "~0.27.9",
@@ -90,5 +93,5 @@
90
93
  "updateSnapshot": "bun test --update-snapshots"
91
94
  },
92
95
  "types": "dist/index.d.ts",
93
- "version": "0.1.0"
96
+ "version": "0.2.0"
94
97
  }
package/src/api.ts CHANGED
@@ -400,13 +400,71 @@ function getQueryValidationMiddleware<T>(
400
400
  return validateQueryParams(querySchema, validationOptions);
401
401
  }
402
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
+
403
421
  /**
404
422
  * Create a set of CRUD routes given a Mongoose model and configuration options.
405
423
  *
406
- * @param model A Mongoose Model
407
- * @param options Options for configuring the REST API, such as permissions, transformers, and hooks.
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);
408
434
  */
409
- export function modelRouter<T>(model: Model<T>, options: ModelRouterOptions<T>): express.Router {
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 {
410
468
  const router = express.Router();
411
469
 
412
470
  // Do before the other router options so endpoints take priority.
@@ -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
+ }