odac 1.3.0 → 1.4.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 (42) hide show
  1. package/.agent/rules/memory.md +7 -1
  2. package/.github/workflows/release.yml +0 -4
  3. package/AGENTS.md +47 -0
  4. package/CHANGELOG.md +32 -0
  5. package/README.md +1 -1
  6. package/bin/odac.js +169 -6
  7. package/client/odac.js +15 -11
  8. package/docs/ai/README.md +49 -0
  9. package/docs/ai/skills/SKILL.md +39 -0
  10. package/docs/ai/skills/backend/authentication.md +67 -0
  11. package/docs/ai/skills/backend/config.md +32 -0
  12. package/docs/ai/skills/backend/controllers.md +62 -0
  13. package/docs/ai/skills/backend/cron.md +50 -0
  14. package/docs/ai/skills/backend/database.md +21 -0
  15. package/docs/ai/skills/backend/forms.md +19 -0
  16. package/docs/ai/skills/backend/ipc.md +55 -0
  17. package/docs/ai/skills/backend/mail.md +34 -0
  18. package/docs/ai/skills/backend/request_response.md +35 -0
  19. package/docs/ai/skills/backend/routing.md +51 -0
  20. package/docs/ai/skills/backend/storage.md +43 -0
  21. package/docs/ai/skills/backend/streaming.md +34 -0
  22. package/docs/ai/skills/backend/structure.md +57 -0
  23. package/docs/ai/skills/backend/translations.md +42 -0
  24. package/docs/ai/skills/backend/utilities.md +24 -0
  25. package/docs/ai/skills/backend/validation.md +53 -0
  26. package/docs/ai/skills/backend/views.md +61 -0
  27. package/docs/ai/skills/frontend/core.md +66 -0
  28. package/docs/ai/skills/frontend/forms.md +21 -0
  29. package/docs/ai/skills/frontend/navigation.md +20 -0
  30. package/docs/ai/skills/frontend/realtime.md +47 -0
  31. package/docs/backend/10-authentication/01-user-logins-with-authjs.md +2 -0
  32. package/docs/backend/10-authentication/05-session-management.md +25 -3
  33. package/package.json +1 -1
  34. package/src/Auth.js +100 -15
  35. package/src/Route/Internal.js +21 -18
  36. package/src/Route/MimeTypes.js +56 -0
  37. package/src/Route.js +40 -63
  38. package/src/View/Form.js +91 -51
  39. package/src/View.js +8 -3
  40. package/test/Auth.test.js +249 -0
  41. package/test/Client.test.js +29 -0
  42. package/test/View/Form.test.js +37 -0
@@ -0,0 +1,20 @@
1
+ # Frontend Navigation & SPA Skill
2
+
3
+ Smooth transitions and single-page application behavior using `odac.js`.
4
+
5
+ ## Rules
6
+ 1. **Selection**: Enable via `Odac.action({ navigate: 'main' })`.
7
+ 2. **Exclusion**: Use `data-navigate="false"` or `.no-navigate` class for full reloads.
8
+ 3. **Lifecycle**: Use `load` and `page` events to run code after navigation.
9
+
10
+ ## Patterns
11
+ ```javascript
12
+ Odac.action({
13
+ navigate: {
14
+ update: 'main',
15
+ on: function(page, vars) {
16
+ console.log('Navigated to:', page);
17
+ }
18
+ }
19
+ });
20
+ ```
@@ -0,0 +1,47 @@
1
+ # Frontend Realtime & WebSocket Skill
2
+
3
+ Real-time bidirectional communication and server-sent events with high efficiency.
4
+
5
+ ## Architectural Approach
6
+ ODAC prioritizes connection efficiency. `Odac.ws()` provides shared WebSocket connections across multiple browser tabs using `SharedWorker`, significantly reducing server load.
7
+
8
+ ## Core Rules
9
+ 1. **Shared Connections**: Always prefer `Odac.ws(url, { shared: true })` for scalable real-time apps.
10
+ 2. **Auto-Reconnect**: Enabled by default; the client handles network drops automatically.
11
+ 3. **SSE (Streaming)**: Use `new EventSource(url)` for one-way streams (e.g., live logs, notifications).
12
+ 4. **JSON Native**: Messages sent and received via `Odac.ws` are automatically parsed/stringified.
13
+
14
+ ## Reference Patterns
15
+
16
+ ### 1. Shared WebSocket (Cross-Tab)
17
+ ```javascript
18
+ // One connection shared across all open tabs
19
+ const ws = Odac.ws('/chat', { shared: true });
20
+
21
+ ws.on('message', (data) => {
22
+ console.log('Update received in all tabs:', data);
23
+ });
24
+
25
+ // Sends message from current tab; others will receive the response
26
+ ws.send({ type: 'chat', text: 'Hello World' });
27
+ ```
28
+
29
+ ### 2. Standard WebSocket (Per-Tab)
30
+ ```javascript
31
+ const ws = Odac.ws('/game', { shared: false });
32
+ ws.on('open', () => console.log('Game connected'));
33
+ ```
34
+
35
+ ### 3. Server-Sent Events (SSE)
36
+ ```javascript
37
+ const source = new EventSource('/api/events');
38
+ source.onmessage = (event) => {
39
+ const data = JSON.parse(event.data);
40
+ updateUI(data);
41
+ };
42
+ ```
43
+
44
+ ## Best Practices
45
+ - **Resource Management**: Shared WebSocket automatically closes when the last tab using it is closed.
46
+ - **Fallback**: If `SharedWorker` is not supported (e.g., Safari), `Odac.ws` automatically falls back to a standard WebSocket.
47
+ - **Server-Side Hubs**: Ensure the backend uses `Odac.Hub` to send messages to the correct rooms or users.
@@ -11,6 +11,8 @@ The `Odac.Auth` service is your bouncer, managing who gets in and who stays out.
11
11
 
12
12
  When you call this, `Auth` creates a secure session for the user.
13
13
 
14
+ > **💡 Enterprise Security:** ODAC automatically handles **Token Rotation** every 15 minutes (configurable) and includes built-in **CSRF protection** for all forms. Sessions are persistent across browser restarts by default.
15
+
14
16
  #### Checking the Guest List
15
17
 
16
18
  * `Odac.Auth.isLogin()`: Is the current user logged in? Returns `true` or `false`.
@@ -29,6 +29,20 @@ Sessions use a **sliding window** approach (similar to NextAuth.js):
29
29
  3. User inactive for 30 days, session expires
30
30
  4. Active users stay logged in indefinitely (up to 30 days of inactivity)
31
31
 
32
+ ### Token Rotation (Enterprise Grade)
33
+
34
+ Odac implements a non-blocking **Refresh Token Rotation** mechanism to enhance security:
35
+
36
+ - **rotationAge**: How often to rotate tokens (default: 15 minutes)
37
+ - **Grace Period**: When a token is rotated, the old token remains valid for **60 seconds** to prevent race conditions in Single Page Applications (SPAs) making concurrent requests.
38
+
39
+ **How it works:**
40
+ 1. A request is made with an active token older than `rotationAge`.
41
+ 2. Odac issues a brand new token set and sends them as cookies.
42
+ 3. The old token is marked as "rotated" and assigned a 60-second lütuf (grace) period.
43
+ 4. Subsequent concurrent requests using the old token still pass within those 60 seconds.
44
+ 5. After 60 seconds, the old token is naturally expired.
45
+
32
46
  ### Configuration
33
47
 
34
48
  Configure session behavior in `odac.json`:
@@ -39,21 +53,29 @@ Configure session behavior in `odac.json`:
39
53
  "table": "users",
40
54
  "token": "user_tokens",
41
55
  "maxAge": 2592000000,
42
- "updateAge": 86400000
56
+ "updateAge": 86400000,
57
+ "rotationAge": 900000,
58
+ "rotation": true
43
59
  }
44
60
  }
45
61
  ```
46
62
 
47
63
  **Options:**
48
64
 
49
- - `maxAge` (milliseconds): Maximum inactivity period before session expires
65
+ - `maxAge` (milliseconds): Maximum inactivity period before session expires. **Note:** Cookies also use this value for persistence.
50
66
  - Default: `2592000000` (30 days)
51
67
  - Example: `604800000` (7 days)
52
68
 
53
- - `updateAge` (milliseconds): How often to update the session timestamp
69
+ - `updateAge` (milliseconds): How often to update the session timestamp (heartbeat)
54
70
  - Default: `86400000` (1 day)
55
71
  - Example: `3600000` (1 hour)
56
72
 
73
+ - `rotationAge` (milliseconds): How often to rotate the session tokens
74
+ - Default: `900000` (15 minutes)
75
+
76
+ - `rotation` (boolean): Enable or disable token rotation
77
+ - Default: `true`
78
+
57
79
  ### Common Configurations
58
80
 
59
81
  **Short sessions (banking apps):**
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "email": "mail@emre.red",
8
8
  "url": "https://emre.red"
9
9
  },
10
- "version": "1.3.0",
10
+ "version": "1.4.0",
11
11
  "license": "MIT",
12
12
  "engines": {
13
13
  "node": ">=18.0.0"
package/src/Auth.js CHANGED
@@ -1,8 +1,11 @@
1
1
  const nodeCrypto = require('crypto')
2
+ const ROTATED_TOKEN_EPOCH_THRESHOLD_MS = 31536000000
3
+ const TOKEN_ROTATION_GRACE_PERIOD_MS = 60 * 1000
2
4
  class Auth {
3
5
  #request = null
4
6
  #table = null
5
7
  #user = null
8
+ static #migrationCache = new Set()
6
9
 
7
10
  constructor(request) {
8
11
  this.#request = request
@@ -98,7 +101,10 @@ class Auth {
98
101
 
99
102
  // Code First Migration: Ensure token table exists and clean up old tokens
100
103
  try {
101
- await this.#ensureTokenTableV2(tokenTable)
104
+ if (!Auth.#migrationCache.has(tokenTable)) {
105
+ await this.#ensureTokenTableV2(tokenTable)
106
+ Auth.#migrationCache.add(tokenTable)
107
+ }
102
108
  } catch (e) {
103
109
  console.error('Odac Auth Error: Failed to ensure token table exists:', e.message)
104
110
  }
@@ -112,13 +118,21 @@ class Auth {
112
118
 
113
119
  const maxAge = Odac.Config.auth?.maxAge || 30 * 24 * 60 * 60 * 1000
114
120
  const updateAge = Odac.Config.auth?.updateAge || 24 * 60 * 60 * 1000
121
+ const rotationAge = Odac.Config.auth?.rotationAge || 15 * 60 * 1000 // Default 15 mins for rotation
122
+ const shouldRotate = Odac.Config.auth?.rotation !== false // Allow disabling rotation
115
123
  const now = Date.now()
116
124
 
117
125
  // Active comes as Date object usually from drivers
118
126
  const lastActive = new Date(sql_token[0].active).getTime()
127
+ const tokenDate = new Date(sql_token[0].date).getTime()
119
128
  const inactiveAge = now - lastActive
129
+ const tokenAge = now - tokenDate
130
+
131
+ // If date is before 1971, it's a marker for a rotated (grace period) token
132
+ const isRotated = tokenDate < ROTATED_TOKEN_EPOCH_THRESHOLD_MS
120
133
 
121
134
  if (inactiveAge > maxAge) {
135
+ // Naturally cleans up expired tokens and rotated tokens after grace period
122
136
  await Odac.DB[tokenTable].where('id', sql_token[0].id).delete()
123
137
  return false
124
138
  }
@@ -126,12 +140,63 @@ class Auth {
126
140
  this.#user = await Odac.DB[this.#table].where(primaryKey, sql_token[0].user).first()
127
141
  if (!this.#user) return false
128
142
 
129
- if (inactiveAge > updateAge) {
130
- // Use update instead of set for Knex
131
- Odac.DB[tokenTable]
132
- .where('id', sql_token[0].id)
133
- .update({active: new Date()}) // knex uses .update
134
- .catch(() => {})
143
+ if (!isRotated) {
144
+ if (shouldRotate && tokenAge > rotationAge) {
145
+ // --- Token Rotation ---
146
+ const newTokenX = nodeCrypto.randomBytes(32).toString('hex')
147
+ const newTokenY = nodeCrypto.randomBytes(32).toString('hex')
148
+ const newToken = {
149
+ id: Odac.DB.nanoid(),
150
+ user: sql_token[0].user,
151
+ token_x: newTokenX,
152
+ token_y: Odac.Var(newTokenY).hash(),
153
+ browser: sql_token[0].browser,
154
+ ip: this.#request.ip,
155
+ date: new Date(),
156
+ active: new Date()
157
+ }
158
+
159
+ // 1. Persist new token (await to ensure it exists before client uses new cookies)
160
+ const insertOk = await Odac.DB[tokenTable].insert(newToken).catch(e => {
161
+ console.error('Odac Auth Error: Token rotation failed', e.message)
162
+ return false
163
+ })
164
+
165
+ if (insertOk !== false) {
166
+ // 2. Mark old token as rotated and set exactly 60 seconds grace period
167
+ // Non-blocking I/O (Fire & Forget) -> High Throughput
168
+ const rotatedActiveDate = new Date(now - maxAge + TOKEN_ROTATION_GRACE_PERIOD_MS)
169
+ const epochDate = new Date(0)
170
+
171
+ Odac.DB[tokenTable]
172
+ .where('id', sql_token[0].id)
173
+ .update({
174
+ active: rotatedActiveDate,
175
+ date: epochDate
176
+ })
177
+ .catch(() => {})
178
+
179
+ // 3. Issue new cookies immediately
180
+ this.#request.cookie('odac_x', newTokenX, {
181
+ httpOnly: true,
182
+ secure: true,
183
+ sameSite: 'Lax',
184
+ maxAge: maxAge
185
+ })
186
+ this.#request.cookie('odac_y', newTokenY, {
187
+ httpOnly: true,
188
+ secure: true,
189
+ sameSite: 'Lax',
190
+ maxAge: maxAge
191
+ })
192
+ }
193
+ } else if (inactiveAge > updateAge) {
194
+ // Fallback simple active update if rotation is not triggered
195
+ Odac.DB[tokenTable]
196
+ .where('id', sql_token[0].id)
197
+ .update({active: new Date()})
198
+ .catch(() => {})
199
+ }
135
200
  }
136
201
 
137
202
  return true
@@ -143,11 +208,13 @@ class Auth {
143
208
  let user = await this.check(where)
144
209
  if (!user) return false
145
210
 
146
- if (!Odac.Config.auth) Odac.Config.auth = {}
147
211
  let key = Odac.Config.auth.key || 'id'
148
212
  let token = Odac.Config.auth.token || 'odac_auth'
149
213
 
150
- await this.#ensureTokenTableV2(token)
214
+ if (!Auth.#migrationCache.has(token)) {
215
+ await this.#ensureTokenTableV2(token)
216
+ Auth.#migrationCache.add(token)
217
+ }
151
218
 
152
219
  this.#cleanupExpiredTokens(token)
153
220
 
@@ -165,12 +232,20 @@ class Auth {
165
232
  ip: this.#request.ip
166
233
  }
167
234
 
235
+ const maxAge = Odac.Config.auth?.maxAge || 30 * 24 * 60 * 60 * 1000
236
+
168
237
  this.#request.cookie('odac_x', cookie.token_x, {
169
238
  httpOnly: true,
170
239
  secure: true,
171
- sameSite: 'Lax'
240
+ sameSite: 'Lax',
241
+ maxAge: maxAge
242
+ })
243
+ this.#request.cookie('odac_y', token_y, {
244
+ httpOnly: true,
245
+ secure: true,
246
+ sameSite: 'Lax',
247
+ maxAge: maxAge
172
248
  })
173
- this.#request.cookie('odac_y', token_y, {httpOnly: true, secure: true, sameSite: 'Lax'})
174
249
 
175
250
  // Knex insert returns ids on some dbs, promise resolves to result
176
251
  const result = await Odac.DB[token].insert(cookie)
@@ -199,7 +274,10 @@ class Auth {
199
274
  const uniqueFields = options.uniqueFields || ['email']
200
275
 
201
276
  try {
202
- await this.#ensureUserTableV2(this.#table, primaryKey, passwordField, uniqueFields, data)
277
+ if (!Auth.#migrationCache.has(this.#table)) {
278
+ await this.#ensureUserTableV2(this.#table, primaryKey, passwordField, uniqueFields, data)
279
+ Auth.#migrationCache.add(this.#table)
280
+ }
203
281
  } catch (e) {
204
282
  // If DB not configured or connection failed
205
283
  console.error('Odac Auth Error:', e.message)
@@ -302,12 +380,16 @@ class Auth {
302
380
  if (!this.#user) return false
303
381
 
304
382
  if (!Odac.Config.auth) Odac.Config.auth = {}
305
- const token = Odac.Config.auth.token || 'user_tokens'
383
+ const tokenTable = Odac.Config.auth.token || 'user_tokens'
384
+ const primaryKey = Odac.Config.auth.key || 'id'
306
385
  const odacX = this.#request.cookie('odac_x')
307
386
  const browser = this.#request.header('user-agent')
308
387
 
309
388
  if (odacX && browser) {
310
- await Odac.DB[token].where('token_x', odacX).where('browser', browser).delete()
389
+ // Delete current token AND any rotated grace-period tokens for this user+browser
390
+ // Why: After rotation, the old token stays alive for ~60s. Explicit logout must kill it too.
391
+ const userId = this.#user[primaryKey]
392
+ await Odac.DB[tokenTable].where('user', userId).where('browser', browser).delete()
311
393
  }
312
394
 
313
395
  this.#request.cookie('odac_x', '', {maxAge: -1})
@@ -326,7 +408,10 @@ class Auth {
326
408
 
327
409
  // Ensure magic table exists
328
410
  try {
329
- await this.#ensureMagicLinkTable(magicTable)
411
+ if (!Auth.#migrationCache.has(magicTable)) {
412
+ await this.#ensureMagicLinkTable(magicTable)
413
+ Auth.#migrationCache.add(magicTable)
414
+ }
330
415
  } catch (e) {
331
416
  console.error('Failed to ensure magic link table exists:', e)
332
417
  // Consider returning an error here to prevent further execution.
@@ -541,23 +541,13 @@ class Internal {
541
541
 
542
542
  if (Odac.formConfig.action) {
543
543
  const actionParts = Odac.formConfig.action.split('.')
544
- if (actionParts.length === 2) {
544
+ if (actionParts.length >= 2) {
545
545
  const controllerName = actionParts[0]
546
- const methodName = actionParts[1]
547
-
548
- // Dynamically load controller
549
- // We need to access Odac.Route.class to find the controller path/module
550
- // Or use require directly if we know the path structure.
551
- // Since we are in framework/src/Route/Internal.js, controllers are in framework/controller/ OR app/controller/
552
- // Ideally Odac.Route.class has the loaded controllers.
553
546
 
554
547
  let controllerModule = null
555
548
 
556
549
  if (Odac.Route && Odac.Route.class && Odac.Route.class[controllerName]) {
557
550
  controllerModule = Odac.Route.class[controllerName].module
558
- } else {
559
- // Try to require it if not loaded (though Route.js should have loaded it)
560
- // This fallback might be tricky with absolute paths, relying on Route.class is safer.
561
551
  }
562
552
 
563
553
  if (controllerModule) {
@@ -605,16 +595,29 @@ class Internal {
605
595
  }
606
596
  }
607
597
 
608
- // Handle Class-based Controller
598
+ let method = controllerModule
599
+ let context = null
600
+ let isClass = false
601
+
609
602
  if (typeof controllerModule === 'function' && controllerModule.prototype) {
610
- const instance = new controllerModule(Odac)
611
- if (typeof instance[methodName] === 'function') {
612
- return await instance[methodName](formHelper)
603
+ isClass = true
604
+ context = new controllerModule(Odac)
605
+ method = context
606
+ }
607
+
608
+ for (let i = 1; i < actionParts.length; i++) {
609
+ if (method) {
610
+ context = method
611
+ method = method[actionParts[i]]
613
612
  }
614
613
  }
615
- // Handle Object-based Controller (Backwards Compatibility)
616
- else if (typeof controllerModule[methodName] === 'function') {
617
- return await controllerModule[methodName](Odac, formHelper)
614
+
615
+ if (typeof method === 'function') {
616
+ if (isClass) {
617
+ return await method.call(context, formHelper)
618
+ } else {
619
+ return await method.call(context, Odac, formHelper)
620
+ }
618
621
  }
619
622
  } catch (e) {
620
623
  console.error(e)
@@ -0,0 +1,56 @@
1
+ module.exports = {
2
+ html: 'text/html',
3
+ css: 'text/css',
4
+ js: 'text/javascript',
5
+ json: 'application/json',
6
+ png: 'image/png',
7
+ jpg: 'image/jpg',
8
+ jpeg: 'image/jpeg',
9
+ svg: 'image/svg+xml',
10
+ ico: 'image/x-icon',
11
+ mp3: 'audio/mpeg',
12
+ mp4: 'video/mp4',
13
+ webm: 'video/webm',
14
+ woff: 'font/woff',
15
+ woff2: 'font/woff2',
16
+ ttf: 'font/ttf',
17
+ otf: 'font/otf',
18
+ eot: 'font/eot',
19
+ pdf: 'application/pdf',
20
+ zip: 'application/zip',
21
+ tar: 'application/x-tar',
22
+ gz: 'application/gzip',
23
+ rar: 'application/x-rar-compressed',
24
+ '7z': 'application/x-7z-compressed',
25
+ txt: 'text/plain',
26
+ log: 'text/plain',
27
+ csv: 'text/csv',
28
+ xml: 'text/xml',
29
+ rss: 'application/rss+xml',
30
+ atom: 'application/atom+xml',
31
+ yaml: 'application/x-yaml',
32
+ sh: 'application/x-sh',
33
+ bat: 'application/x-bat',
34
+ exe: 'application/x-exe',
35
+ bin: 'application/x-binary',
36
+ doc: 'application/msword',
37
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
38
+ xls: 'application/vnd.ms-excel',
39
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
40
+ ppt: 'application/vnd.ms-powerpoint',
41
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
42
+ avi: 'video/x-msvideo',
43
+ wmv: 'video/x-ms-wmv',
44
+ flv: 'video/x-flv',
45
+ webp: 'image/webp',
46
+ gif: 'image/gif',
47
+ bmp: 'image/bmp',
48
+ tiff: 'image/tiff',
49
+ tif: 'image/tiff',
50
+ weba: 'audio/webm',
51
+ wav: 'audio/wav',
52
+ ogg: 'audio/ogg',
53
+ flac: 'audio/flac',
54
+ aac: 'audio/aac',
55
+ midi: 'audio/midi'
56
+ }
package/src/Route.js CHANGED
@@ -7,62 +7,7 @@ const MiddlewareChain = require('./Route/Middleware.js')
7
7
  const {WebSocketServer} = require('./WebSocket.js')
8
8
 
9
9
  var routes2 = {}
10
- const mime = {
11
- html: 'text/html',
12
- css: 'text/css',
13
- js: 'text/javascript',
14
- json: 'application/json',
15
- png: 'image/png',
16
- jpg: 'image/jpg',
17
- jpeg: 'image/jpeg',
18
- svg: 'image/svg+xml',
19
- ico: 'image/x-icon',
20
- mp3: 'audio/mpeg',
21
- mp4: 'video/mp4',
22
- webm: 'video/webm',
23
- woff: 'font/woff',
24
- woff2: 'font/woff2',
25
- ttf: 'font/ttf',
26
- otf: 'font/otf',
27
- eot: 'font/eot',
28
- pdf: 'application/pdf',
29
- zip: 'application/zip',
30
- tar: 'application/x-tar',
31
- gz: 'application/gzip',
32
- rar: 'application/x-rar-compressed',
33
- '7z': 'application/x-7z-compressed',
34
- txt: 'text/plain',
35
- log: 'text/plain',
36
- csv: 'text/csv',
37
- xml: 'text/xml',
38
- rss: 'application/rss+xml',
39
- atom: 'application/atom+xml',
40
- yaml: 'application/x-yaml',
41
- sh: 'application/x-sh',
42
- bat: 'application/x-bat',
43
- exe: 'application/x-exe',
44
- bin: 'application/x-binary',
45
- doc: 'application/msword',
46
- docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
47
- xls: 'application/vnd.ms-excel',
48
- xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
49
- ppt: 'application/vnd.ms-powerpoint',
50
- pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
51
- avi: 'video/x-msvideo',
52
- wmv: 'video/x-ms-wmv',
53
- flv: 'video/x-flv',
54
- webp: 'image/webp',
55
- gif: 'image/gif',
56
- bmp: 'image/bmp',
57
- tiff: 'image/tiff',
58
- tif: 'image/tiff',
59
- weba: 'audio/webm',
60
- wav: 'audio/wav',
61
- ogg: 'audio/ogg',
62
- flac: 'audio/flac',
63
- aac: 'audio/aac',
64
- midi: 'audio/midi'
65
- }
10
+ const mime = require('./Route/MimeTypes.js')
66
11
 
67
12
  class Route {
68
13
  loading = false
@@ -122,15 +67,37 @@ class Route {
122
67
  if (middlewareResult !== undefined) return middlewareResult
123
68
 
124
69
  if (controller.action) {
125
- const ControllerClass = controller.cache
70
+ const ControllerModule = controller.cache
71
+ const actionParts = controller.action.split('.')
72
+
126
73
  try {
127
- const instance = new ControllerClass(Odac)
128
- if (typeof instance[controller.action] === 'function') {
129
- return instance[controller.action](Odac)
74
+ const instance = new ControllerModule(Odac)
75
+ let method = instance
76
+ let context = instance
77
+
78
+ for (const segment of actionParts) {
79
+ if (method) {
80
+ context = method
81
+ method = method[segment]
82
+ }
83
+ }
84
+
85
+ if (typeof method === 'function') {
86
+ return method.call(context, Odac)
130
87
  }
131
88
  } catch {
132
- if (typeof ControllerClass[controller.action] === 'function') {
133
- return ControllerClass[controller.action](Odac)
89
+ let method = ControllerModule
90
+ let context = ControllerModule
91
+
92
+ for (const segment of actionParts) {
93
+ if (method) {
94
+ context = method
95
+ method = method[segment]
96
+ }
97
+ }
98
+
99
+ if (typeof method === 'function') {
100
+ return method.call(context, Odac)
134
101
  }
135
102
  }
136
103
  return Odac.Request.abort(500)
@@ -144,6 +111,9 @@ class Route {
144
111
  async check(Odac) {
145
112
  let url = Odac.Request.url.split('?')[0]
146
113
  if (url.endsWith('/')) url = url.slice(0, -1)
114
+ // Global Auth Check: Load user if valid tokens exist to simplify DX
115
+ // This allows calling Odac.Auth.user() anywhere without manual await Odac.Auth.check()
116
+ if (Odac.Auth) await Odac.Auth.check()
147
117
 
148
118
  if (url.startsWith('/_odac/')) {
149
119
  Odac.Request.route = '_odac_internal'
@@ -882,8 +852,15 @@ class Route {
882
852
  }
883
853
  })
884
854
 
855
+ // Global Auth Check: Load user if valid tokens exist to simplify DX
856
+ // This allows calling Odac.Auth.user() anywhere without manual await Odac.Auth.check()
857
+ if (Odac.Auth) await Odac.Auth.check()
858
+
885
859
  if (requireAuth) {
886
- const isAuthenticated = await Odac.Auth.check()
860
+ let isAuthenticated = false
861
+ if (Odac.Auth) {
862
+ isAuthenticated = await Odac.Auth.check()
863
+ }
887
864
  if (!isAuthenticated) {
888
865
  ws.close(4001, 'Unauthorized')
889
866
  return