webspresso 0.0.95 → 0.0.96

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 (73) hide show
  1. package/.agents/ADMIN.md +225 -0
  2. package/.agents/AGENTS.md +72 -0
  3. package/.agents/ARCHITECTURE.md +119 -0
  4. package/.agents/AUTH.md +103 -0
  5. package/.agents/CLI.md +115 -0
  6. package/.agents/COMPRESSION.md +59 -0
  7. package/.agents/CONVENTIONS.md +46 -0
  8. package/.agents/EMAIL.md +141 -0
  9. package/.agents/ERRORS.md +201 -0
  10. package/.agents/MCP.md +103 -0
  11. package/.agents/ORM.md +230 -0
  12. package/.agents/PLUGINS.md +419 -0
  13. package/.agents/QUEUE.md +113 -0
  14. package/.agents/REALTIME.md +213 -0
  15. package/.agents/ROUTING.md +292 -0
  16. package/.agents/SERVICES.md +341 -0
  17. package/.agents/SHUTDOWN.md +67 -0
  18. package/.agents/STREAMING.md +73 -0
  19. package/.agents/WORKFLOW.md +27 -0
  20. package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
  21. package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
  22. package/.cursorrules +14 -0
  23. package/AGENTS.md +175 -0
  24. package/CLAUDE.md +54 -0
  25. package/README.md +18 -14
  26. package/bin/commands/agents-init.js +169 -0
  27. package/bin/commands/api.js +114 -48
  28. package/bin/commands/doctor.js +35 -0
  29. package/bin/commands/new.js +13 -2
  30. package/bin/commands/page.js +75 -89
  31. package/bin/commands/service.js +77 -0
  32. package/bin/webspresso.js +2 -2
  33. package/core/compileSchema.js +1 -1
  34. package/core/errors/middleware.js +22 -2
  35. package/core/errors/normalize.js +29 -2
  36. package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
  37. package/index.d.ts +306 -0
  38. package/index.js +35 -1
  39. package/package.json +5 -3
  40. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
  41. package/plugins/csrf/index.js +3 -2
  42. package/plugins/csv/engine.js +447 -0
  43. package/plugins/csv/index.js +94 -0
  44. package/plugins/csv/middleware.js +80 -0
  45. package/plugins/csv/services.js +138 -0
  46. package/plugins/file-manager/admin-component.js +613 -0
  47. package/plugins/file-manager/api-handlers.js +261 -0
  48. package/plugins/file-manager/core.js +601 -0
  49. package/plugins/file-manager/index.js +139 -0
  50. package/plugins/file-manager/security.js +207 -0
  51. package/plugins/index.js +8 -0
  52. package/plugins/xlsx/engine.js +397 -0
  53. package/plugins/xlsx/index.js +91 -0
  54. package/plugins/xlsx/middleware.js +87 -0
  55. package/plugins/xlsx/services.js +118 -0
  56. package/src/api/api-loader.js +233 -0
  57. package/src/api/define-api.js +35 -0
  58. package/src/discovery/file-route-parser.js +201 -0
  59. package/src/discovery/scan-routes.js +265 -0
  60. package/src/file-router.js +6 -0
  61. package/src/modules/define-module.js +99 -0
  62. package/src/modules/module-discovery.js +117 -0
  63. package/src/pages/define-page.js +36 -0
  64. package/src/pages/page-loader.js +214 -0
  65. package/src/routing/mount-discovered-routes.js +78 -0
  66. package/src/routing/route-table.js +191 -0
  67. package/src/server.js +112 -15
  68. package/src/services/builtins/file-manager.js +178 -0
  69. package/src/services/index.js +2 -0
  70. package/bin/commands/skill.js +0 -187
  71. package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
  72. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
  73. package/templates/skills/webspresso-usage/SKILL.md +0 -48
@@ -0,0 +1,113 @@
1
+ # Webspresso Background Job Queue Guide
2
+
3
+ Webspresso features a high-performance, zero-dependency background job queue engine (`core/queue`) and plugin (`plugins/queue`), supporting In-Memory, Knex Database, and Distributed Redis adapters, automatic retries with exponential backoff, priority scheduling, and graceful shutdown draining.
4
+
5
+ ---
6
+
7
+ ## 1. Quick Start
8
+
9
+ ### Basic Usage with `core/queue`
10
+ ```js
11
+ const { createQueueManager } = require('webspresso/core/queue');
12
+
13
+ const queue = createQueueManager({
14
+ concurrency: 4,
15
+ pollInterval: 200,
16
+ });
17
+
18
+ // 1. Define job handler
19
+ queue.define('email.send', async (job, ctx) => {
20
+ const { to, subject, body } = job.data;
21
+ job.progress(20, 'Preparing template');
22
+ // ... send email ...
23
+ job.progress(100, 'Sent');
24
+ return { delivered: true };
25
+ }, {
26
+ attempts: 3,
27
+ backoff: { type: 'exponential', delay: 1000 },
28
+ timeout: 10000,
29
+ priority: 20,
30
+ });
31
+
32
+ // 2. Dispatch job
33
+ await queue.dispatch('email.send', {
34
+ to: 'user@example.com',
35
+ subject: 'Welcome!',
36
+ }, {
37
+ priority: 50,
38
+ delay: 5000, // delay 5 seconds
39
+ });
40
+ ```
41
+
42
+ ---
43
+
44
+ ## 2. Using the Plugin (`plugins/queue`)
45
+
46
+ ```js
47
+ const { createApp, queuePlugin } = require('webspresso');
48
+
49
+ const app = createApp({
50
+ plugins: [
51
+ queuePlugin({
52
+ adapter: 'memory', // 'memory' | 'database' | 'redis'
53
+ concurrency: 4,
54
+ jobsDir: './jobs', // Auto-discovers jobs/ directory
55
+ }),
56
+ ],
57
+ });
58
+
59
+ // In Express route handlers:
60
+ app.post('/api/export', async (req, res) => {
61
+ const job = await req.queue.dispatch('export.users', { format: 'csv' });
62
+ res.json({ jobId: job.id, status: job.status });
63
+ });
64
+ ```
65
+
66
+ ---
67
+
68
+ ## 3. Adapters
69
+
70
+ ### 1. `MemoryQueueAdapter` (Default)
71
+ Zero-dependency in-memory priority queue. Ideal for lightweight background tasks, single-process apps, and development.
72
+
73
+ ### 2. `DatabaseQueueAdapter`
74
+ Persistent queue backed by Knex database table (`webspresso_jobs`) supporting SQLite, PostgreSQL, and MySQL with row-level locking.
75
+
76
+ ```js
77
+ queuePlugin({
78
+ adapter: 'database',
79
+ db: appDb,
80
+ tableName: 'webspresso_jobs',
81
+ });
82
+ ```
83
+
84
+ ### 3. `RedisQueueAdapter`
85
+ Distributed queue backed by Redis for multi-instance PM2/Kubernetes cluster deployments.
86
+
87
+ ```js
88
+ queuePlugin({
89
+ adapter: 'redis',
90
+ redisClient: redis,
91
+ prefix: 'app:queue:',
92
+ });
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 4. File-Based Auto-Discovery (`jobs/`)
98
+
99
+ Files placed in `jobs/` are automatically mapped to dot-separated job names:
100
+ - `jobs/email/send.js` → `'email.send'`
101
+ - `jobs/user-report.js` → `'user-report'`
102
+
103
+ ```js
104
+ // jobs/email/send.js
105
+ module.exports = async function(job, ctx) {
106
+ // job.data
107
+ };
108
+
109
+ module.exports.options = {
110
+ attempts: 5,
111
+ backoff: { type: 'exponential', delay: 2000 },
112
+ };
113
+ ```
@@ -0,0 +1,213 @@
1
+ # Webspresso Realtime Layer & Adapters Guide
2
+
3
+ [← Back to AGENTS.md](.agents/AGENTS.md)
4
+
5
+ Webspresso features a framework-agnostic, plugin-based, adapter-driven Realtime layer (`core/realtime`) designed for universal browser execution with zero SSR hazards.
6
+
7
+ ---
8
+
9
+ ## 1. Architectural Overview
10
+
11
+ ```text
12
+ Webspresso App / Client
13
+ │
14
+ ├── AuthManager (Token / Cookie / Custom / None)
15
+ ├── SubscriptionRegistry (identifier::params composite identity)
16
+ ├── ReconnectManager (exponential backoff + jitter)
17
+ │
18
+ └── Realtime Core
19
+ │
20
+ ├── Generic WebSocket Adapter (`core/realtime/adapters/websocket`)
21
+ ├── SSE Adapter (`core/realtime/adapters/sse`)
22
+ ├── Socket.IO Adapter (`core/realtime/adapters/socket-io`)
23
+ └── Redis Pub/Sub Adapter (`core/realtime/adapters/redis`)
24
+ ```
25
+
26
+ ### Core Tenets:
27
+ 1. **Zero SSR Network Hazards**: Server-side rendering execution never attempts socket connections or touches browser globals.
28
+ 2. **Unified Subscription Model**: All realtime technologies share identical public subscription interfaces (`subscribe`, `send`, `perform`, `unsubscribe`).
29
+ 3. **Deterministic Subscription Registry**: Subscriptions with identical identifiers and query parameters share channel instances without duplication.
30
+ 4. **Secret Sanitization**: Auth tokens and credentials are automatically stripped from error logs (`token=***`).
31
+ 5. **Distributed Cluster Sync**: Redis adapter allows multi-process / multi-container instances (PM2 cluster, K8s, Docker) to broadcast across all connected clients.
32
+
33
+ ---
34
+
35
+ ## 2. Core API Reference
36
+
37
+ ### 2.1 Factory Functions (`core/realtime`, `plugins/realtime`)
38
+ ```js
39
+ const { createRealtime, realtime, realtimePlugin, redis, websocket, sse, socketIo } = require('webspresso');
40
+ // Or via plugins
41
+ const { realtimePlugin, redis, websocket, sse, socketIo } = require('webspresso/plugins');
42
+ ```
43
+
44
+ - `createRealtime(options)`: Creates a standalone `RealtimeClient`.
45
+ - `realtime(options)`: Universal helper returning `RealtimeClient` with `.plugin` property attached.
46
+ - `realtimePlugin(options)`: Webspresso plugin mounting `app.realtime` and handling shutdown disposers.
47
+
48
+ ### 2.2 Options Configuration
49
+ | Option | Type | Default | Description |
50
+ |---|---|---|---|
51
+ | `adapter` | `RealtimeAdapter` | **Required** | Transport adapter (`websocket()`, `sse()`, `socketIo()`, `redis()`). |
52
+ | `auth` | `Object` \| `false` | `false` | Strategy config: `{ strategy: 'token'|'cookie'|'custom' }`. |
53
+ | `autoConnect` | `boolean` | `true` | Automatically connect when browser environment is ready. |
54
+ | `reconnect` | `Object` | `{ enabled: true }` | Backoff settings (`baseDelay`, `maxDelay`, `factor`, `jitter`). |
55
+ | `isBrowser` | `boolean` | `null` | Force browser mode (testing override). |
56
+
57
+ ---
58
+
59
+ ## 3. Adapters
60
+
61
+ ### 3.1 Generic WebSocket Adapter (`websocket`)
62
+ ```js
63
+ const { websocket } = require('webspresso');
64
+
65
+ const adapter = websocket({
66
+ url: 'wss://api.example.com/realtime', // string or async (ctx) => url
67
+ authTransport: 'query', // 'query' | 'header' | 'protocol' | 'message'
68
+ queryParamName: 'token',
69
+ messages: {
70
+ subscribe: ({ identifier, params }) => ({ type: 'subscribe', identifier, params }),
71
+ send: ({ identifier, data }) => ({ type: 'message', identifier, data }),
72
+ perform: ({ identifier, action, data }) => ({ type: 'perform', identifier, action, data }),
73
+ },
74
+ });
75
+ ```
76
+
77
+ ### 3.2 Server-Sent Events (SSE) Adapter (`sse`)
78
+ ```js
79
+ const { sse } = require('webspresso');
80
+
81
+ const adapter = sse({
82
+ url: '/api/realtime/events',
83
+ queryParamName: 'token',
84
+ });
85
+ ```
86
+ *Note: SSE adapter is unidirectional and read-only. Calling `sub.send()` or `sub.perform()` will throw a clear `UNSUPPORTED_CAPABILITY` error.*
87
+
88
+ ### 3.3 Socket.IO Adapter (`socketIo`)
89
+ ```js
90
+ const { socketIo } = require('webspresso');
91
+
92
+ const adapter = socketIo({
93
+ url: 'https://socket.example.com',
94
+ path: '/socket.io',
95
+ socketOptions: { transports: ['websocket'] },
96
+ });
97
+ ```
98
+
99
+ ### 3.4 Redis Pub/Sub Distributed Adapter (`redis`)
100
+ Enables cross-process distributed event synchronization across PM2 clusters, Docker containers, and Kubernetes pods:
101
+
102
+ ```js
103
+ const { createApp, realtimePlugin, redis } = require('webspresso');
104
+ const Redis = require('ioredis');
105
+
106
+ // Option A: Single client instance (adapter will duplicate for subClient)
107
+ const pubsub = new Redis(process.env.REDIS_URL);
108
+ const adapter = redis({
109
+ redis: pubsub,
110
+ channelPrefix: 'myapp:rt:',
111
+ });
112
+
113
+ // Option B: Explicit dual clients
114
+ const pubClient = new Redis(process.env.REDIS_URL);
115
+ const subClient = new Redis(process.env.REDIS_URL);
116
+ const adapter = redis({
117
+ pubClient,
118
+ subClient,
119
+ channelPrefix: 'myapp:rt:',
120
+ });
121
+
122
+ // Option C: In-memory fallback (useful for local development & unit tests)
123
+ const localAdapter = redis({ inMemory: true });
124
+
125
+ const { app } = createApp({
126
+ plugins: [
127
+ realtimePlugin({
128
+ adapter,
129
+ autoConnect: true,
130
+ }),
131
+ ],
132
+ });
133
+
134
+ // Server-side broadcasting to all cluster nodes:
135
+ await app.realtime.adapter.publish('room:general', { message: 'Cluster-wide notification' });
136
+ ```
137
+
138
+
139
+ ---
140
+
141
+ ## 4. Usage Recipes
142
+
143
+ ### 4.1 Webspresso Plugin Integration
144
+ ```js
145
+ const { createApp } = require('webspresso');
146
+ const { realtimePlugin, websocket } = require('webspresso/plugins');
147
+
148
+ const { app } = createApp({
149
+ plugins: [
150
+ realtimePlugin({
151
+ adapter: websocket({ url: '/realtime' }),
152
+ auth: { strategy: 'cookie' },
153
+ autoConnect: true,
154
+ }),
155
+ ],
156
+ });
157
+ ```
158
+
159
+ ### 4.2 Token Authentication & Automatic Refresh
160
+ ```js
161
+ const { realtime, websocket } = require('webspresso');
162
+
163
+ const client = realtime({
164
+ adapter: websocket({ url: 'wss://api.example.com/realtime' }),
165
+ auth: {
166
+ strategy: 'token',
167
+ getToken: async () => localStorage.getItem('jwt_token'),
168
+ refreshToken: async () => {
169
+ const res = await fetch('/api/auth/refresh', { method: 'POST' });
170
+ const { token } = await res.json();
171
+ localStorage.setItem('jwt_token', token);
172
+ return token;
173
+ },
174
+ onUnauthorized: (err) => {
175
+ window.location.href = '/login';
176
+ },
177
+ },
178
+ });
179
+ ```
180
+
181
+ ### 4.3 Subscriptions & Remote Actions
182
+ ```js
183
+ // Subscribe to channel
184
+ const sub = app.realtime.subscribe('project:123', { role: 'editor' }, {
185
+ connected() {
186
+ console.log('Connected to project:123 channel');
187
+ },
188
+ disconnected() {
189
+ console.log('Temporarily disconnected');
190
+ },
191
+ received(data) {
192
+ console.log('New event:', data);
193
+ },
194
+ rejected(error) {
195
+ console.error('Subscription rejected:', error);
196
+ },
197
+ });
198
+
199
+ // Send raw message
200
+ sub.send({ text: 'Hello!' });
201
+
202
+ // Perform named action (RPC)
203
+ sub.perform('cursor_move', { x: 120, y: 340 });
204
+
205
+ // Unsubscribe
206
+ sub.unsubscribe();
207
+ ```
208
+
209
+ ### 4.4 Re-authentication Workflow
210
+ ```js
211
+ // Refresh credentials and restore all active subscriptions seamlessly
212
+ await app.realtime.reauthenticate();
213
+ ```
@@ -0,0 +1,292 @@
1
+ # Webspresso File-Based Routing & API Routes
2
+
3
+ [← Back to AGENTS.md](AGENTS.md)
4
+
5
+ Webspresso is a full-stack Node.js framework powered by **100% File-Based Routing** (similar to Next.js App Router / Nuxt 3).
6
+
7
+ ---
8
+
9
+ ## ⚠️ STRICT ANTI-PATTERNS (DO NOT DO THIS)
10
+
11
+ ### ❌ BAD vs ✅ GOOD Code Comparison
12
+
13
+ ```javascript
14
+ // ❌ WRONG (Express / Legacy reflex — NEVER DO THIS)
15
+ // File: src/routes/users.js
16
+ const express = require('express');
17
+ const router = express.Router();
18
+ router.get('/users', async (req, res) => {
19
+ const users = await db('users').select('*'); // Raw query bypasses ORM!
20
+ res.json(users);
21
+ });
22
+ module.exports = router;
23
+
24
+ // ✅ CORRECT (Webspresso 100% File-Based Routing)
25
+ // File: pages/api/users.get.js
26
+ 'use strict';
27
+ module.exports = {
28
+ schema: ({ z }) => ({
29
+ query: z.object({ limit: z.coerce.number().optional().default(20) }),
30
+ }),
31
+ middleware: ['auth'], // optional
32
+ handler: async (req, res) => {
33
+ const users = await req.db.getRepository('User').find(req.input.query);
34
+ return res.json({ success: true, data: users });
35
+ },
36
+ };
37
+ ```
38
+
39
+ 1. **NEVER create a `routes/` or `src/routes/` folder.**
40
+ 2. **NEVER use `express.Router()` or `require('express').Router()`.**
41
+ 3. **NEVER call `app.get()`, `app.post()`, `app.put()`, or `app.delete()` for application endpoints.**
42
+ 4. **NEVER use `createApp({ setupRoutes: (app) => ... })` to mount domain/application routes.**
43
+ 5. **ALL application routes MUST live inside the `pages/` directory.**
44
+
45
+ ---
46
+
47
+ ## 1. Directory Structure & URL Mapping
48
+
49
+ All routes are automatically discovered and registered from the `pages/` directory:
50
+
51
+ | File Path | HTTP Method | URL Route | Description |
52
+ | :--- | :--- | :--- | :--- |
53
+ | `pages/index.njk` | `GET` | `/` | Home page (SSR Nunjucks) |
54
+ | `pages/about.njk` | `GET` | `/about` | Static SSR page |
55
+ | `pages/notes/[id].njk` | `GET` | `/notes/:id` | Dynamic SSR route with param |
56
+ | `pages/api/notes.get.js` | `GET` | `/api/notes` | List notes API endpoint |
57
+ | `pages/api/notes.post.js` | `POST` | `/api/notes` | Create note API endpoint |
58
+ | `pages/api/notes/[id].get.js` | `GET` | `/api/notes/:id` | Get single note API endpoint |
59
+ | `pages/api/notes/[id].put.js` | `PUT` | `/api/notes/:id` | Update note API endpoint |
60
+ | `pages/api/notes/[id].delete.js` | `DELETE` | `/api/notes/:id` | Delete note API endpoint |
61
+
62
+ ### Method Extension Suffixes
63
+ API route filenames **MUST** declare their HTTP method before the `.js` extension:
64
+ - `.get.js`
65
+ - `.post.js`
66
+ - `.put.js`
67
+ - `.patch.js`
68
+ - `.delete.js`
69
+
70
+ If no method suffix is provided (e.g. `pages/api/notes.js`), it defaults to `GET`.
71
+
72
+ ---
73
+
74
+ ## 2. Standard API Route Contract
75
+
76
+ Every API route module exports an object with optional `schema`, optional `middleware`, and a `handler` function:
77
+
78
+ ```javascript
79
+ // pages/api/notes.post.js
80
+ 'use strict';
81
+
82
+ module.exports = {
83
+ // 1. Declarative Input Validation (Zod)
84
+ schema: ({ z }) => ({
85
+ body: z.object({
86
+ title: z.string().min(1, 'Title is required').max(200),
87
+ content: z.string().min(1, 'Content is required'),
88
+ isPublic: z.boolean().optional().default(false),
89
+ }),
90
+ }),
91
+
92
+ // 2. Named Middleware Guards (resolved from ctx.middlewares or global plugins)
93
+ middleware: ['auth'], // e.g. 'auth', 'jwt', 'basicAuth', or inline (req, res, next) => ...
94
+
95
+ // 3. Request Handler
96
+ handler: async (req, res) => {
97
+ // req.db: Injected database repository factory
98
+ // req.service(): Injected service registry invocation helper
99
+ // req.input.body: Type-safe, validated request payload
100
+ // req.auth / req.session: Authenticated user session
101
+
102
+ const noteRepo = req.db.getRepository('Note');
103
+ const note = await noteRepo.create({
104
+ ...req.input.body,
105
+ userId: req.session?.user?.id || req.auth?.user?.id,
106
+ });
107
+
108
+ return res.status(201).json({
109
+ success: true,
110
+ data: note,
111
+ });
112
+ },
113
+ };
114
+ ```
115
+
116
+ > [!IMPORTANT]
117
+ > **NEVER use raw Knex queries** like `req.db('notes')` in route handlers. **ALWAYS use repositories**: `req.db.getRepository('Note')`. This guarantees schema validation, lifecycle hooks, soft deletes, and query caching are respected.
118
+
119
+ ---
120
+
121
+ ## 3. Dynamic Route Parameters (`[param]`)
122
+
123
+ Dynamic path segments use square brackets:
124
+
125
+ ```javascript
126
+ // pages/api/notes/[id].get.js
127
+ 'use strict';
128
+
129
+ module.exports = {
130
+ schema: ({ z }) => ({
131
+ params: z.object({
132
+ id: z.string().min(1),
133
+ }),
134
+ }),
135
+
136
+ handler: async (req, res) => {
137
+ const { id } = req.params;
138
+ const note = await req.db.getRepository('Note').findById(id);
139
+
140
+ if (!note) {
141
+ return res.status(404).json({ error: 'Note not found' });
142
+ }
143
+
144
+ return res.json({ success: true, data: note });
145
+ },
146
+ };
147
+ ```
148
+
149
+ ---
150
+
151
+ ## 4. SSR Page Routes (`load()` Loader)
152
+
153
+ For server-rendered Nunjucks HTML pages, use `.njk` templates and companion `.js` loader files (or inline `async function load` in a `.js` file):
154
+
155
+ ```javascript
156
+ // pages/notes/[id].js (Companion data loader for pages/notes/[id].njk)
157
+ 'use strict';
158
+
159
+ module.exports = {
160
+ // Runs server-side before rendering the template
161
+ async load({ req, res, db, ctx }) {
162
+ const note = await db.getRepository('Note').findById(req.params.id);
163
+ if (!note) {
164
+ return { status: 404, notFound: true };
165
+ }
166
+
167
+ return {
168
+ note,
169
+ title: note.title,
170
+ };
171
+ },
172
+ };
173
+ ```
174
+
175
+ ---
176
+
177
+ ## 5. Module & File-Based Discovery (`src/pages`, `src/api`, `modules/{name}`)
178
+
179
+ Webspresso supports clean, domain-driven module discovery alongside root-level pages and APIs:
180
+
181
+ ```txt
182
+ src/
183
+ pages/ -> Global UI pages (definePage or Nunjucks)
184
+ index.js -> GET /
185
+ about.js -> GET /about
186
+ docs/[...path].js -> GET /docs/*path
187
+
188
+ api/ -> Global API endpoints (defineApi)
189
+ health.get.js -> GET /api/health
190
+ users/[id].patch.js -> PATCH /api/users/:id
191
+
192
+ modules/ -> Feature-driven modules
193
+ auth/
194
+ auth.module.js -> defineModule({ name: 'auth', ... })
195
+ pages/
196
+ login.js -> GET /auth/login
197
+ register.js -> GET /auth/register
198
+ api/
199
+ login.post.js -> POST /api/auth/login
200
+ me.get.js -> GET /api/auth/me
201
+ services/
202
+ login.js -> ctx.service('auth.login', input)
203
+ middleware/
204
+ auth-guard.js -> module-local named middleware 'authGuard'
205
+ ```
206
+
207
+ ### 5.1 `definePage()` Contract
208
+ ```javascript
209
+ const { definePage } = require('webspresso');
210
+
211
+ module.exports = definePage({
212
+ middleware: ['authGuard'], // module or global middleware
213
+ load: async (ctx) => {
214
+ // ctx: { req, res, params, query, service, db, redirect, fsy, locale, t }
215
+ if (!ctx.req.user) {
216
+ return ctx.redirect('/auth/login', 302);
217
+ }
218
+ const profile = await ctx.service('user.profile', { id: ctx.req.user.id });
219
+ return { profile };
220
+ },
221
+ head: (data) => ({
222
+ title: `${data.profile.name}'s Profile`,
223
+ }),
224
+ render: (data, ctx) => `
225
+ <main>
226
+ <h1>${data.profile.name}</h1>
227
+ <p>Email: ${data.profile.email}</p>
228
+ </main>
229
+ `,
230
+ });
231
+ ```
232
+
233
+ ### 5.2 `defineApi()` Contract
234
+ ```javascript
235
+ const { defineApi, z } = require('webspresso');
236
+
237
+ module.exports = defineApi({
238
+ description: 'Authenticate user and return session',
239
+ tags: ['Auth'],
240
+ middleware: ['rateLimit'],
241
+ schema: {
242
+ body: z.object({
243
+ email: z.string().email(),
244
+ password: z.string().min(8),
245
+ }),
246
+ },
247
+ handler: async (req, res, ctx) => {
248
+ // req.input.body contains validated payload
249
+ const result = await ctx.service('auth.login', req.input.body);
250
+ return res.json(result);
251
+ },
252
+ });
253
+ ```
254
+
255
+ ### 5.3 `defineModule()` Contract
256
+ ```javascript
257
+ const { defineModule } = require('webspresso');
258
+
259
+ module.exports = defineModule({
260
+ name: 'auth',
261
+ version: '1.0.0',
262
+ pages: { prefix: '/auth' }, // default: /auth
263
+ api: { prefix: '/api/auth' }, // default: /api/auth
264
+ middlewares: {
265
+ authGuard: (req, res, next) => {
266
+ if (!req.user) return res.status(401).json({ error: 'Unauthorized' });
267
+ next();
268
+ },
269
+ },
270
+ });
271
+ ```
272
+
273
+ ### 5.4 Route Introspection (`app.routes.list()`)
274
+ Compiled routes are accessible at runtime for tooling, CLI inspection, and documentation:
275
+ ```javascript
276
+ const routeList = app.routes.list();
277
+ // [
278
+ // { method: 'GET', path: '/', type: 'page', source: 'src/pages/index.js' },
279
+ // { method: 'POST', path: '/api/auth/login', type: 'api', module: 'auth', description: 'Authenticate user' }
280
+ // ]
281
+ ```
282
+
283
+ ---
284
+
285
+ ## 6. Summary Checklist for Agents
286
+
287
+ When a user asks to add or modify endpoints:
288
+ 1. Identify the URL and method (e.g. `POST /api/auth/register` or `POST /auth/login`).
289
+ 2. Create in `pages/api/...`, `src/api/...`, or `modules/{module}/api/...` with HTTP method extension (`.post.js`).
290
+ 3. Use `defineApi()` or `{ schema, middleware, handler }`.
291
+ 4. Use `req.db.getRepository(...)` or `ctx.service(...)`.
292
+ 5. **NEVER touch `routes/` or create manual Express router files.**