@terreno/api 0.1.0 → 0.3.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/dist/api.d.ts +28 -2
- package/dist/api.js +20 -7
- 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/expressServer.js +3 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- 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 +34 -0
- package/package.json +6 -3
- package/src/api.ts +61 -3
- 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/expressServer.ts +5 -6
- package/src/index.ts +4 -0
- package/src/openApiValidator.ts +1 -1
- package/src/terrenoApp.test.ts +201 -0
- package/src/terrenoApp.ts +347 -0
- package/src/terrenoPlugin.ts +34 -0
- package/.claude/CLAUDE.local.md +0 -204
- package/.cursor/rules/00-root.mdc +0 -338
- package/.github/copilot-instructions.md +0 -333
- 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
|
+
});
|
package/dist/terrenoPlugin.d.ts
CHANGED
|
@@ -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": "
|
|
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
|
+
"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.
|
|
96
|
+
"version": "0.3.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
|
-
*
|
|
407
|
-
*
|
|
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>(
|
|
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
|
+
}
|