@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.
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 +3 -3
  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 +5 -6
  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,347 @@
1
+ import * as Sentry from "@sentry/bun";
2
+ import openapi from "@wesleytodd/openapi";
3
+ import cors from "cors";
4
+ import express from "express";
5
+ import qs from "qs";
6
+
7
+ import type {ModelRouterRegistration} from "./api";
8
+ import {addAuthRoutes, addMeRoutes, setupAuth, type UserModel as UserMongooseModel} from "./auth";
9
+ import {apiErrorMiddleware, apiUnauthorizedMiddleware} from "./errors";
10
+ import {type AuthOptions, logRequests} from "./expressServer";
11
+ import {addGitHubAuthRoutes, type GitHubAuthOptions, setupGitHubAuth} from "./githubAuth";
12
+ import {type LoggingOptions, logger, setupLogging} from "./logger";
13
+ import {openApiEtagMiddleware} from "./openApiEtag";
14
+ import type {TerrenoPlugin} from "./terrenoPlugin";
15
+
16
+ type CorsOrigin =
17
+ | string
18
+ | boolean
19
+ | RegExp
20
+ | Array<boolean | string | RegExp>
21
+ | ((
22
+ requestOrigin: string | undefined,
23
+ callback: (
24
+ err: Error | null,
25
+ origin?: boolean | string | RegExp | Array<boolean | string | RegExp>
26
+ ) => void
27
+ ) => void);
28
+
29
+ /**
30
+ * Configuration options for TerrenoApp.
31
+ */
32
+ export interface TerrenoAppOptions {
33
+ /** Mongoose User model with passport-local-mongoose plugin */
34
+ userModel: UserMongooseModel;
35
+ /** CORS origin configuration (default: "*") */
36
+ corsOrigin?: CorsOrigin;
37
+ /** Logging configuration options */
38
+ loggingOptions?: LoggingOptions;
39
+ /** Authentication configuration options */
40
+ authOptions?: AuthOptions;
41
+ /** GitHub OAuth configuration (enables GitHub authentication if provided) */
42
+ githubAuth?: GitHubAuthOptions;
43
+ /** Skip calling app.listen() in start() method (useful for testing) */
44
+ skipListen?: boolean;
45
+ /** Sentry configuration options */
46
+ sentryOptions?: Sentry.BunOptions;
47
+ /** Maximum number of array items in query parameters (default: 200) */
48
+ arrayLimit?: number;
49
+ /** Whether to log all incoming requests (default: true) */
50
+ logRequests?: boolean;
51
+ }
52
+
53
+ /**
54
+ * Fluent API for building Express applications with Terreno framework.
55
+ *
56
+ * TerrenoApp provides an alternative to `setupServer` using a registration
57
+ * pattern instead of callbacks. Build applications by registering model
58
+ * routers and plugins, then calling `start()` to begin listening.
59
+ *
60
+ * The middleware stack is configured in this order:
61
+ * 1. CORS
62
+ * 2. Custom middleware (via addMiddleware)
63
+ * 3. JSON body parser
64
+ * 4. Auth routes (/auth/login, /auth/signup, etc.)
65
+ * 5. JWT authentication setup
66
+ * 6. Request logging
67
+ * 7. Sentry scopes
68
+ * 8. OpenAPI middleware
69
+ * 9. /auth/me routes
70
+ * 10. GitHub OAuth routes (if enabled)
71
+ * 11. Registered model routers and plugins
72
+ * 12. Error handling middleware
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * // Basic usage with model routers
77
+ * const todoRouter = modelRouter("/todos", Todo, {
78
+ * permissions: { list: [Permissions.IsAuthenticated], ... },
79
+ * });
80
+ *
81
+ * const app = new TerrenoApp({ userModel: User })
82
+ * .register(todoRouter)
83
+ * .register(new HealthApp())
84
+ * .start();
85
+ * ```
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * // With custom middleware
90
+ * const app = new TerrenoApp({
91
+ * userModel: User,
92
+ * corsOrigin: ["https://app.example.com"],
93
+ * loggingOptions: { logRequests: true },
94
+ * githubAuth: {
95
+ * clientId: process.env.GITHUB_CLIENT_ID!,
96
+ * clientSecret: process.env.GITHUB_CLIENT_SECRET!,
97
+ * callbackURL: process.env.GITHUB_CALLBACK_URL!,
98
+ * },
99
+ * })
100
+ * .addMiddleware((req, res, next) => {
101
+ * res.setHeader("X-Custom-Header", "value");
102
+ * next();
103
+ * })
104
+ * .register(todoRouter)
105
+ * .register(userRouter)
106
+ * .start();
107
+ * ```
108
+ *
109
+ * @see setupServer for the callback-based alternative
110
+ * @see TerrenoPlugin for creating reusable plugins
111
+ * @see modelRouter for creating CRUD route registrations
112
+ */
113
+ export class TerrenoApp {
114
+ private options: TerrenoAppOptions;
115
+ private registrations: (ModelRouterRegistration | TerrenoPlugin)[] = [];
116
+ private middlewareFns: (express.RequestHandler | ((app: express.Application) => void))[] = [];
117
+
118
+ /**
119
+ * Create a new TerrenoApp builder.
120
+ *
121
+ * @param options - Application configuration options including user model and auth settings
122
+ */
123
+ constructor(options: TerrenoAppOptions) {
124
+ this.options = options;
125
+ }
126
+
127
+ /**
128
+ * Register a model router or plugin with the application.
129
+ *
130
+ * Model routers are created with `modelRouter("/path", Model, options)` and
131
+ * provide CRUD endpoints. Plugins implement `TerrenoPlugin` interface and
132
+ * can register custom routes and middleware.
133
+ *
134
+ * Registrations are mounted in the order they are added.
135
+ *
136
+ * @param registration - A ModelRouterRegistration from modelRouter() or a TerrenoPlugin instance
137
+ * @returns This TerrenoApp instance for method chaining
138
+ *
139
+ * @example
140
+ * ```typescript
141
+ * const todoRouter = modelRouter("/todos", Todo, options);
142
+ * const healthPlugin = new HealthApp({ path: "/health" });
143
+ *
144
+ * app.register(todoRouter).register(healthPlugin);
145
+ * ```
146
+ */
147
+ register(registration: ModelRouterRegistration | TerrenoPlugin): this {
148
+ this.registrations.push(registration);
149
+ return this;
150
+ }
151
+
152
+ /**
153
+ * Add custom Express middleware to the application.
154
+ *
155
+ * Middleware is added BEFORE JSON body parsing and authentication setup,
156
+ * allowing you to modify incoming requests early in the middleware stack.
157
+ *
158
+ * @param fn - Express middleware function or a function that configures the app
159
+ * @returns This TerrenoApp instance for method chaining
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * app.addMiddleware((req, res, next) => {
164
+ * res.setHeader("X-Request-ID", req.id);
165
+ * next();
166
+ * });
167
+ * ```
168
+ */
169
+ addMiddleware(fn: express.RequestHandler | ((app: express.Application) => void)): this {
170
+ this.middlewareFns.push(fn);
171
+ return this;
172
+ }
173
+
174
+ /**
175
+ * Build the Express application without starting the server.
176
+ *
177
+ * Configures the complete middleware stack including:
178
+ * - CORS, JSON parsing, authentication, logging, Sentry, OpenAPI
179
+ * - All registered model routers and plugins
180
+ * - Error handling middleware
181
+ *
182
+ * Use this method when you need the Express app instance for testing
183
+ * or custom server setup. For normal use, call `start()` instead.
184
+ *
185
+ * @returns Configured Express application instance
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * const app = new TerrenoApp({ userModel: User })
190
+ * .register(todoRouter)
191
+ * .build();
192
+ *
193
+ * // Use app for testing with supertest
194
+ * await request(app).get("/todos").expect(200);
195
+ * ```
196
+ */
197
+ build(): express.Application {
198
+ setupLogging(this.options.loggingOptions);
199
+
200
+ const app = express();
201
+ const options = this.options;
202
+
203
+ app.set("query parser", (str: string) =>
204
+ qs.parse(str, {arrayLimit: options.arrayLimit ?? 200})
205
+ );
206
+
207
+ app.use(cors({origin: options.corsOrigin ?? "*"}));
208
+
209
+ // Apply custom middleware before JSON parsing
210
+ for (const fn of this.middlewareFns) {
211
+ if (fn.length <= 3) {
212
+ // express.RequestHandler (req, res, next)
213
+ app.use(fn as express.RequestHandler);
214
+ } else {
215
+ // Function that receives the app
216
+ (fn as (app: express.Application) => void)(app);
217
+ }
218
+ }
219
+
220
+ app.use(express.json());
221
+
222
+ // Auth routes (login/signup/refresh_token) before JWT middleware
223
+ addAuthRoutes(app, options.userModel as any, options.authOptions);
224
+ setupAuth(app as any, options.userModel as any);
225
+
226
+ if (options.logRequests !== false) {
227
+ app.use(logRequests);
228
+ }
229
+
230
+ // Store logging options on the response locals
231
+ app.use((_req, res, next) => {
232
+ res.locals.loggingOptions = options.loggingOptions;
233
+ next();
234
+ });
235
+
236
+ // Sentry scopes
237
+ app.all("*", (req: any, _res: any, next: any) => {
238
+ const transactionId = req.header("X-Transaction-ID");
239
+ const sessionId = req.header("X-Session-ID");
240
+ if (transactionId) {
241
+ Sentry.getCurrentScope().setTag("transaction_id", transactionId);
242
+ }
243
+ if (sessionId) {
244
+ Sentry.getCurrentScope().setTag("session_id", sessionId);
245
+ }
246
+ if (req.user?._id) {
247
+ Sentry.getCurrentScope().setTag("user", req.user._id);
248
+ }
249
+ next();
250
+ });
251
+
252
+ // OpenAPI
253
+ app.use(openApiEtagMiddleware);
254
+ const oapi = openapi({
255
+ info: {
256
+ description: "Generated docs from an Express api",
257
+ title: "Express Application",
258
+ version: "1.0.0",
259
+ },
260
+ openapi: "3.0.0",
261
+ });
262
+ app.use(oapi);
263
+
264
+ if (process.env.ENABLE_SWAGGER === "true") {
265
+ app.use("/swagger", oapi.swaggerui());
266
+ }
267
+
268
+ addMeRoutes(app, options.userModel as any, options.authOptions);
269
+
270
+ // GitHub OAuth
271
+ if (options.githubAuth) {
272
+ setupGitHubAuth(app, options.userModel as any, options.githubAuth);
273
+ addGitHubAuthRoutes(app, options.userModel as any, options.githubAuth, options.authOptions);
274
+ }
275
+
276
+ // Mount registered model routers and plugins
277
+ for (const registration of this.registrations) {
278
+ if (this.isModelRouterRegistration(registration)) {
279
+ app.use(registration.path, registration.router);
280
+ } else {
281
+ registration.register(app);
282
+ }
283
+ }
284
+
285
+ // Inject openApi into model router options for registered routers
286
+ // The openApi middleware handles this via the oapi instance already mounted on the app
287
+
288
+ Sentry.setupExpressErrorHandler(app);
289
+
290
+ // Error middleware
291
+ app.use(apiUnauthorizedMiddleware);
292
+ app.use(apiErrorMiddleware);
293
+
294
+ app.use(function onError(err: any, _req: any, res: any, _next: any) {
295
+ logger.error(`Fallthrough error: ${err}${err?.stack ? `\n${err.stack}` : ""}}`);
296
+ Sentry.captureException(err);
297
+ res.statusCode = 500;
298
+ res.end(`${res.sentry}\n`);
299
+ });
300
+
301
+ return app;
302
+ }
303
+
304
+ /**
305
+ * Build the Express application and start listening on the configured port.
306
+ *
307
+ * Calls `build()` to configure the application, then starts an HTTP server
308
+ * listening on the port specified by the `PORT` environment variable (default: 9000).
309
+ * If `skipListen` option is true, the app is built but the server is not started.
310
+ *
311
+ * @returns Configured Express application instance
312
+ *
313
+ * @throws Process exits with code 1 if the server fails to start
314
+ *
315
+ * @example
316
+ * ```typescript
317
+ * // Start server on port 3000
318
+ * process.env.PORT = "3000";
319
+ * const app = new TerrenoApp({ userModel: User })
320
+ * .register(todoRouter)
321
+ * .start();
322
+ * ```
323
+ */
324
+ start(): express.Application {
325
+ const app = this.build();
326
+
327
+ if (!this.options.skipListen) {
328
+ const port = process.env.PORT || "9000";
329
+ try {
330
+ app.listen(port, () => {
331
+ logger.info(`Listening on port ${port}`);
332
+ });
333
+ } catch (error) {
334
+ logger.error(`Error trying to start HTTP server: ${error}\n${(error as any).stack}`);
335
+ process.exit(1);
336
+ }
337
+ }
338
+
339
+ return app;
340
+ }
341
+
342
+ private isModelRouterRegistration(
343
+ registration: ModelRouterRegistration | TerrenoPlugin
344
+ ): registration is ModelRouterRegistration {
345
+ return (registration as ModelRouterRegistration).__type === "modelRouter";
346
+ }
347
+ }
@@ -1,5 +1,39 @@
1
1
  import type express from "express";
2
2
 
3
+ /**
4
+ * Interface for plugins that can be registered with TerrenoApp.
5
+ *
6
+ * Implement this interface to create reusable plugins that encapsulate
7
+ * routes, middleware, or other Express application setup. Plugins are
8
+ * registered via `TerrenoApp.register()` and are mounted after core
9
+ * authentication and OpenAPI middleware.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * class MyPlugin implements TerrenoPlugin {
14
+ * register(app: express.Application): void {
15
+ * app.get("/my-route", (req, res) => {
16
+ * res.json({ status: "ok" });
17
+ * });
18
+ * }
19
+ * }
20
+ *
21
+ * const app = new TerrenoApp({ userModel: User })
22
+ * .register(new MyPlugin())
23
+ * .start();
24
+ * ```
25
+ *
26
+ * @see TerrenoApp for the application builder that consumes plugins
27
+ * @see HealthApp for a built-in plugin example
28
+ */
3
29
  export interface TerrenoPlugin {
30
+ /**
31
+ * Register routes and middleware with the Express application.
32
+ *
33
+ * Called during `TerrenoApp.build()` after core middleware has been
34
+ * configured but before error handling middleware is added.
35
+ *
36
+ * @param app - The Express application instance to register with
37
+ */
4
38
  register(app: express.Application): void;
5
39
  }
@@ -1,204 +0,0 @@
1
- # Terreno
2
-
3
- A monorepo containing shared packages for building full-stack applications with React Native and Express/Mongoose.
4
-
5
- ## Packages
6
-
7
- - **api/** - REST API framework built on Express/Mongoose (`@terreno/api`)
8
- - **ui/** - React Native UI component library (`@terreno/ui`)
9
- - **rtk/** - Redux Toolkit Query utilities for API backends (`@terreno/rtk`)
10
- - **demo/** - Demo app for showcasing and testing UI components
11
- - **example-frontend/** - Example Expo app demonstrating full stack usage
12
- - **example-backend/** - Example Express backend using @terreno/api
13
-
14
- ## Development
15
-
16
- Uses [Bun](https://bun.sh/) as the package manager.
17
-
18
- ```bash
19
- bun install # Install dependencies
20
- bun run compile # Compile all packages
21
- bun run lint # Lint all packages
22
- bun run lint:fix # Fix lint issues
23
- bun run test # Run tests in api and ui
24
- ```
25
-
26
- ### Package-specific commands
27
-
28
- ```bash
29
- bun run api:test # Test API package
30
- bun run ui:test # Test UI package
31
- bun run demo:start # Start demo app
32
- bun run frontend:web # Start frontend example
33
- bun run backend:dev # Start backend example
34
- ```
35
-
36
- ## How the Packages Work Together
37
-
38
- The three core packages form a complete full-stack framework:
39
-
40
- ```
41
- BACKEND
42
- @terreno/api
43
- - Mongoose models with modelRouter -> CRUD endpoints
44
- - Built-in auth (JWT + Passport)
45
- - Automatic OpenAPI spec generation
46
- |
47
- /openapi.json
48
- |
49
- RTK Query SDK Codegen
50
- |
51
- FRONTEND
52
- @terreno/rtk
53
- - Generated hooks from OpenAPI spec
54
- - Auth slice with JWT token management
55
- - Automatic token refresh
56
- +
57
- @terreno/ui
58
- - React Native components (Box, Button, TextField, etc.)
59
- - TerrenoProvider for theming
60
- ```
61
-
62
- ### Integration Flow
63
-
64
- 1. **Backend (api)**: Define Mongoose models, use `modelRouter` to create CRUD endpoints with permissions
65
- 2. **OpenAPI Generation**: `setupServer` automatically generates `/openapi.json`
66
- 3. **SDK Codegen**: Frontend runs `bun run sdk` to generate RTK Query hooks from OpenAPI spec
67
- 4. **Frontend (rtk + ui)**: Use generated hooks with UI components for type-safe API calls
68
-
69
- ## Example Apps (Keep These Updated!)
70
-
71
- The `example-frontend/` and `example-backend/` directories serve as both documentation and integration tests. When adding features to api, ui, or rtk:
72
-
73
- 1. **Add examples** demonstrating new features
74
- 2. **Update SDK** after backend changes: `cd example-frontend && bun run sdk`
75
- 3. **Verify integration** by running both examples together
76
-
77
- ### Running the Full Stack
78
-
79
- ```bash
80
- # Terminal 1: Start backend
81
- bun run backend:dev
82
-
83
- # Terminal 2: Start frontend
84
- bun run frontend:web
85
- ```
86
-
87
- ## Code Style
88
-
89
- ### TypeScript/JavaScript
90
- - Use ES module syntax and TypeScript for all code
91
- - Prefer interfaces over types; avoid enums, use maps
92
- - Prefer const arrow functions over `function` keyword
93
- - Use descriptive variable names with auxiliary verbs (e.g., `isLoading`)
94
- - Use camelCase directories (e.g., `components/authWizard`)
95
- - Favor named exports
96
- - Use the RORO pattern (Receive an Object, Return an Object)
97
-
98
- ### Dates and Time
99
- - Always use Luxon instead of Date or dayjs
100
-
101
- ### Error Handling
102
- - Check error conditions at start of functions and return early
103
- - Limit nested if statements
104
- - Use multiline syntax with curly braces for all conditionals
105
-
106
- ### Testing
107
- - Use bun test with expect for testing
108
-
109
- ### Logging
110
- - Frontend: Use `console.info`, `console.debug`, `console.warn`, or `console.error` for permanent logs
111
- - Backend: Use `logger.info/warn/error/debug` for permanent logs
112
- - Use `console.log` only for debugging (to be removed)
113
-
114
- ### Development Practices
115
- - Don't apologize for errors: fix them
116
- - Prioritize modularity, DRY, performance, and security
117
- - Focus on readability over performance
118
- - Write complete, functional code without TODOs when possible
119
- - Comments should describe purpose, not effect
120
-
121
- ## Package Reference
122
-
123
- ### @terreno/api
124
-
125
- REST API framework providing:
126
-
127
- - **modelRouter**: Auto-generates CRUD endpoints for Mongoose models
128
- - **Permissions**: `IsAuthenticated`, `IsOwner`, `IsAdmin`, `IsAuthenticatedOrReadOnly`
129
- - **Query Filters**: `OwnerQueryFilter` for filtering list queries by owner
130
- - **setupServer**: Express server setup with auth, OpenAPI, and middleware
131
- - **APIError**: Standardized error handling
132
- - **logger**: Winston-based logging
133
-
134
- Key imports:
135
- ```typescript
136
- import {
137
- modelRouter,
138
- setupServer,
139
- Permissions,
140
- OwnerQueryFilter,
141
- APIError,
142
- logger,
143
- asyncHandler,
144
- authenticateMiddleware,
145
- } from "@terreno/api";
146
- ```
147
-
148
- ### @terreno/ui
149
-
150
- React Native component library with 88+ components:
151
-
152
- - **Layout**: Box, Page, SplitPage, Card
153
- - **Forms**: TextField, SelectField, DateTimeField, CheckBox
154
- - **Display**: Text, Heading, Badge, DataTable
155
- - **Actions**: Button, IconButton, Link
156
- - **Feedback**: Spinner, Modal, Toast
157
- - **Theming**: TerrenoProvider, useTheme
158
-
159
- Key imports:
160
- ```typescript
161
- import {
162
- Box,
163
- Button,
164
- Card,
165
- Page,
166
- Text,
167
- TextField,
168
- TerrenoProvider,
169
- } from "@terreno/ui";
170
- ```
171
-
172
- ### @terreno/rtk
173
-
174
- Redux Toolkit Query integration:
175
-
176
- - **generateAuthSlice**: Creates auth reducer and middleware with JWT handling
177
- - **emptyApi**: Base RTK Query API for code generation
178
- - **Platform utilities**: Secure token storage (expo-secure-store for native, AsyncStorage for web)
179
-
180
- Key imports:
181
- ```typescript
182
- import {generateAuthSlice} from "@terreno/rtk";
183
- ```
184
-
185
- ## CI/CD Workflows
186
-
187
- ### Required Secret Validation
188
-
189
- GitHub Actions workflows that use secrets or environment variables must validate all required variables are set before using them. Add a validation step early in the job that fails fast with a clear error message listing any missing variables.
190
-
191
- ```yaml
192
- - name: Validate required secrets
193
- run: |
194
- missing=()
195
- if [ -z "$VAR_NAME" ]; then missing+=("VAR_NAME"); fi
196
- if [ ${#missing[@]} -ne 0 ]; then
197
- echo "::error::Missing required secrets: ${missing[*]}"
198
- exit 1
199
- fi
200
- ```
201
-
202
- ## Dependency Management
203
-
204
- Uses [Bun Catalogs](https://bun.sh/docs/install/catalogs) - shared versions defined in root `package.json` under `catalog`. Reference with `catalog:` in workspace packages.