webspresso 0.0.98 → 0.0.99

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 (56) hide show
  1. package/.agents/ADMIN.md +11 -1
  2. package/.agents/AGENTS.md +1 -1
  3. package/.agents/AUTH.md +169 -0
  4. package/.agents/ORM.md +88 -4
  5. package/.agents/PLUGINS.md +73 -5
  6. package/.agents/QUEUE.md +84 -0
  7. package/.agents/ROUTING.md +127 -7
  8. package/AGENTS.md +1 -0
  9. package/core/auth/index.js +60 -0
  10. package/core/auth/manager.js +134 -0
  11. package/core/auth/middleware.js +88 -0
  12. package/core/auth/oauth.js +517 -0
  13. package/core/auth/qrcode.js +499 -0
  14. package/core/auth/totp.js +504 -0
  15. package/core/orm/eager-loader.js +144 -10
  16. package/core/orm/index.js +8 -0
  17. package/core/orm/model.js +4 -0
  18. package/core/orm/query-builder.js +90 -4
  19. package/core/orm/repository.js +44 -3
  20. package/core/orm/search.js +213 -0
  21. package/core/queue/cron.js +331 -0
  22. package/core/queue/index.js +8 -1
  23. package/core/queue/manager.js +82 -1
  24. package/core/queue/scheduler.js +359 -0
  25. package/core/sse/index.js +19 -0
  26. package/core/sse/stream.js +430 -0
  27. package/index.d.ts +528 -0
  28. package/index.js +44 -0
  29. package/package.json +1 -1
  30. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
  31. package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
  32. package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
  33. package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
  34. package/plugins/admin-panel/field-renderers/index.js +2 -0
  35. package/plugins/api-key/crypto.js +101 -0
  36. package/plugins/api-key/index.js +288 -0
  37. package/plugins/api-key/middleware.js +461 -0
  38. package/plugins/api-key/migration-template.js +48 -0
  39. package/plugins/api-key/model.js +50 -0
  40. package/plugins/api-key/scopes.js +125 -0
  41. package/plugins/audit-log/admin-component.js +487 -70
  42. package/plugins/audit-log/api-handlers.js +114 -0
  43. package/plugins/audit-log/index.js +24 -0
  44. package/plugins/file-manager/admin-component.js +20 -4
  45. package/plugins/file-manager/index.js +21 -4
  46. package/plugins/index.js +6 -0
  47. package/plugins/oauth/index.js +338 -0
  48. package/plugins/queue/index.js +4 -0
  49. package/src/api/api-loader.js +1 -0
  50. package/src/discovery/file-route-parser.js +44 -1
  51. package/src/discovery/scan-routes.js +2 -2
  52. package/src/file-router.js +137 -100
  53. package/src/helpers.js +9 -0
  54. package/src/routing/cascading.js +378 -0
  55. package/src/routing/middleware-resolver.js +92 -0
  56. package/src/server.js +6 -0
package/.agents/ADMIN.md CHANGED
@@ -225,7 +225,7 @@ registry.registerWidget('recent-orders', {
225
225
  - Top tables by row count with visual progress percentage bars
226
226
  - Direct navigation links to model CRUD screens
227
227
  - **`quick-actions`**: Fast shortcuts to create new records across enabled models.
228
- - **`recent-activity`**: Timeline of recent administrative mutations (creates, updates, deletes).
228
+ - **`recent-activity`**: Timeline of recent administrative mutations (creates, updates, deletes) powered by `audit_logs` / `admin_activity_logs` with direct links to record changes.
229
229
 
230
230
  ---
231
231
 
@@ -245,6 +245,16 @@ registry.registerAction('Product', {
245
245
  },
246
246
  });
247
247
 
248
+ // Record action with navigation redirect (e.g. audit-timeline)
249
+ registry.registerAction('Product', {
250
+ id: 'view-history',
251
+ label: 'History',
252
+ icon: 'clock',
253
+ handler: async (record, model) => {
254
+ return { redirect: `/_admin/audit-log?model=${model.name}&recordId=${record.id}` };
255
+ },
256
+ });
257
+
248
258
  // Bulk multi-select table action
249
259
  registry.registerBulkAction('Product', {
250
260
  id: 'bulk-archive',
package/.agents/AGENTS.md CHANGED
@@ -44,7 +44,7 @@ This `.agents/` customization root is organized into modular topic guides:
44
44
  6. **[HTTP Response Compression](.agents/COMPRESSION.md)** — Native streaming zlib compression (`server.compression`), Brotli/Gzip/Deflate negotiation, threshold handling, and `res.compress(false)` route opt-out.
45
45
  7. **[Dual Authentication System](.agents/AUTH.md)** — Stateful Session + Cookie & Stateless HS256 JWT + Refresh Token Rotation, `req.auth` helpers, and `middleware: ['jwt']` guard integrations.
46
46
  8. **[ORM & Database Layer](.agents/ORM.md)** — Model definitions with `defineModel`, `zdb` schemas, Repositories API, Query Caching, Relations, Soft Delete, Scopes & Migrations.
47
- 9. **[Plugin Ecosystem Guide](.agents/PLUGINS.md)** — Lifecycle hooks (`register`, `onRoutesReady`), CSP headers, and detailed reference for all 18 built-in official plugins (including `xlsxPlugin`, `csvPlugin`, `fileManagerPlugin`, `realtimePlugin`, and `basicAuthPlugin`).
47
+ 9. **[Plugin Ecosystem Guide](.agents/PLUGINS.md)** — Lifecycle hooks (`register`, `onRoutesReady`), CSP headers, and detailed reference for all 19 built-in official plugins (including `apiKeyPlugin`, `xlsxPlugin`, `csvPlugin`, `fileManagerPlugin`, `realtimePlugin`, and `basicAuthPlugin`).
48
48
  10. **[Realtime Layer & Adapters](.agents/REALTIME.md)** — Framework-agnostic realtime layer (`core/realtime`), generic WebSocket, SSE, and Socket.IO adapters, subscription identity, backoff reconnect, and auth lifecycle.
49
49
  11. **[Email Plugin Guide](.agents/EMAIL.md)** — MJML template compilation, Nodemailer transport, DB delivery logs, Auth Email bridge, and Admin UI.
50
50
  12. **[Admin Panel Customization](.agents/ADMIN.md)** — Extending Mithril.js Admin Panel SPA (Custom pages `component.js`, custom field renderers, widgets, single & bulk actions).
package/.agents/AUTH.md CHANGED
@@ -101,3 +101,172 @@ module.exports = {
101
101
  },
102
102
  };
103
103
  ```
104
+
105
+ ---
106
+
107
+ ## 5. Zero-Dependency OAuth 2.0 & Social Login (`plugins/oauth`, `core/auth/oauth`)
108
+
109
+ Webspresso includes a built-in, zero-dependency OAuth 2.0 client and plugin with RFC 7636 PKCE, HMAC-SHA256 signed anti-CSRF state tokens, profile normalization, and seamless Dual Auth integration:
110
+
111
+ ### 5.1 Plugin Configuration
112
+ ```javascript
113
+ const { createApp, oauthPlugin } = require('webspresso');
114
+
115
+ const app = createApp({
116
+ plugins: [
117
+ oauthPlugin({
118
+ secret: process.env.SESSION_SECRET,
119
+ providers: {
120
+ google: {
121
+ clientId: process.env.GOOGLE_CLIENT_ID,
122
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
123
+ // redirectUri is optional; auto-inferred from request host if omitted
124
+ },
125
+ github: {
126
+ clientId: process.env.GITHUB_CLIENT_ID,
127
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
128
+ },
129
+ },
130
+ // Optional hook to find or create the user in your database
131
+ async findOrCreateUser({ provider, profile, tokens, db }) {
132
+ let user = await db.getRepository('User').findOne({ email: profile.email });
133
+ if (!user) {
134
+ user = await db.getRepository('User').create({
135
+ email: profile.email,
136
+ name: profile.name,
137
+ avatar_url: profile.avatarUrl,
138
+ });
139
+ }
140
+ return user;
141
+ },
142
+ defaultReturnTo: '/dashboard',
143
+ }),
144
+ ],
145
+ });
146
+ ```
147
+
148
+ ### 5.2 Automatic Routes
149
+ - `GET /auth/:provider` (e.g. `/auth/google?returnTo=/dashboard`): Generates PKCE code challenge and HMAC-signed state token with embedded return URL, redirecting the browser to the provider's authorization page.
150
+ - `GET /auth/:provider/callback`: Validates anti-CSRF state, exchanges authorization code for tokens, fetches normalized profile, calls `findOrCreateUser`, automatically establishes session via `req.auth.login(user)`, and redirects to `returnTo`. For API/mobile clients (`format=json` or `Accept: application/json`), returns `{ success: true, user, token }`.
151
+
152
+ ### 5.3 Template Helper (`fsy.oauthUrl`)
153
+ In Nunjucks templates:
154
+ ```html
155
+ <a href="{{ fsy.oauthUrl('google') }}" class="btn">Sign in with Google</a>
156
+ <a href="{{ fsy.oauthUrl('github', { returnTo: '/settings' }) }}" class="btn">Connect GitHub</a>
157
+ ```
158
+
159
+ ---
160
+
161
+ ## 6. Zero-Dependency Two-Factor Authentication (2FA / TOTP)
162
+
163
+ Webspresso provides an enterprise-grade, zero-dependency 2FA/TOTP suite adhering strictly to **RFC 6238 (TOTP)**, **RFC 4226 (HOTP)**, and **RFC 4648 (Base32)**. It requires NO external npm libraries and features offline SVG QR code generation to prevent leaking secret keys to third-party APIs.
164
+
165
+ ### 6.1 Key Capabilities
166
+ - **RFC 6238 Time-Based OTP**: Standard 30s step, 6/8 digits, HMAC-SHA1/SHA256/SHA512.
167
+ - **Time Drift Tolerance**: Built-in clock drift window (`window: 1` verifies $t \pm 30s$) and detailed delta analysis.
168
+ - **Offline QR Code Generator**: Generates crisp vector SVG strings and data URIs directly in pure Node.js.
169
+ - **Single-Use Recovery Codes**: Cryptographically secure alphanumeric codes, SHA-256 hashed storage, and timing-safe single-use burning.
170
+ - **Session & Guard Integration**: `req.auth.verify2Fa()`, `req.auth.is2FaVerified()`, and `middleware: ['2fa']`.
171
+
172
+ ### 6.2 Initializing 2FA Setup for a User
173
+ ```javascript
174
+ const { totp } = require('webspresso');
175
+
176
+ // In an API route: POST /api/user/2fa/setup
177
+ module.exports = {
178
+ middleware: ['auth'],
179
+ async handler(req, res) {
180
+ const setup = totp.createTotpSetup({
181
+ user: req.user,
182
+ issuer: 'My SaaS App',
183
+ recoveryCodeCount: 8,
184
+ });
185
+
186
+ // 1. Temporarily save secret and hashed recovery codes to DB for confirmation
187
+ await req.db.getRepository('User').update(req.user.id, {
188
+ temp_totp_secret: setup.secret,
189
+ temp_recovery_codes: JSON.stringify(setup.recoveryCodesHashed),
190
+ });
191
+
192
+ // 2. Return QR SVG and plaintext recovery codes for user to record
193
+ return res.json({
194
+ secret: setup.secret,
195
+ qrSvg: setup.qrSvg,
196
+ qrDataUri: setup.qrDataUri,
197
+ recoveryCodes: setup.recoveryCodes,
198
+ });
199
+ },
200
+ };
201
+ ```
202
+
203
+ ### 6.3 Confirming 2FA Activation
204
+ ```javascript
205
+ // In an API route: POST /api/user/2fa/confirm
206
+ module.exports = {
207
+ middleware: ['auth'],
208
+ schema: ({ z }) => ({
209
+ body: z.object({ token: z.string().min(6).max(8) }),
210
+ }),
211
+ async handler(req, res) {
212
+ const user = req.user;
213
+ const { token } = req.input.body;
214
+
215
+ const isValid = totp.verifyTotp(token, user.temp_totp_secret);
216
+ if (!isValid) {
217
+ return res.status(400).json({ error: 'Invalid verification code' });
218
+ }
219
+
220
+ // Activate 2FA on user record
221
+ await req.db.getRepository('User').update(user.id, {
222
+ two_factor_enabled: true,
223
+ totp_secret: user.temp_totp_secret,
224
+ recovery_codes: user.temp_recovery_codes,
225
+ temp_totp_secret: null,
226
+ temp_recovery_codes: null,
227
+ });
228
+
229
+ req.auth.set2FaVerified(true);
230
+ return res.json({ success: true, message: '2FA enabled successfully' });
231
+ },
232
+ };
233
+ ```
234
+
235
+ ### 6.4 2FA Login Flow & Guard Middleware
236
+ ```javascript
237
+ // Step 1: Initial password login
238
+ const user = await req.auth.findUserByCredentials(email, password);
239
+ if (user.two_factor_enabled) {
240
+ // Mark session as 2FA pending without full login
241
+ req.auth.set2FaPending(user);
242
+ return res.json({ require2Fa: true });
243
+ }
244
+ await req.auth.login(user);
245
+
246
+ // Step 2: 2FA Verification Endpoint (accepts 6-digit TOTP token OR recovery code)
247
+ const verification = req.auth.verify2Fa(tokenOrRecoveryCode, user);
248
+ if (verification.success) {
249
+ if (verification.type === 'recovery_code') {
250
+ // Burn used recovery code
251
+ await req.db.getRepository('User').update(user.id, {
252
+ recovery_codes: JSON.stringify(verification.remainingHashedCodes),
253
+ });
254
+ }
255
+ await req.auth.login(user);
256
+ req.auth.set2FaVerified(true);
257
+ return res.json({ success: true });
258
+ }
259
+ ```
260
+
261
+ ### 6.5 Route Protection with `middleware: ['2fa']`
262
+ ```javascript
263
+ // In pages/api/account/billing.get.js or pages/security/index.njk
264
+ module.exports = {
265
+ middleware: ['auth', '2fa'], // Rejects unverified users with 403 or redirects to /2fa
266
+ async handler(req, res) {
267
+ return res.json({ secureData: true });
268
+ },
269
+ };
270
+ ```
271
+
272
+
package/.agents/ORM.md CHANGED
@@ -122,17 +122,32 @@ const result = await productRepo.query()
122
122
 
123
123
  ---
124
124
 
125
- ## 3. Relations & Eager Loading
125
+ ## 3. Relations & Nested Eager Loading
126
126
 
127
- Support for `belongsTo`, `hasMany`, and `hasOne`:
127
+ Support for `belongsTo`, `hasMany`, and `hasOne` with arbitrary-depth batch nested loading:
128
128
 
129
129
  ```js
130
- // Eager-load relations using include
131
- const productsWithCategory = await productRepo.query()
130
+ // 1. Direct eager loading
131
+ const products = await productRepo.query()
132
132
  .include('category', 'reviews')
133
133
  .find();
134
+
135
+ // 2. Nested eager loading (dot notation)
136
+ const posts = await postRepo.query()
137
+ .include('author.profile', 'author.company.country', 'comments.author')
138
+ .find();
139
+
140
+ // 3. Eager loading via repository methods
141
+ const post = await postRepo.findById(id, { include: ['author.profile'] });
142
+ const list = await postRepo.find({ active: true }, { include: ['author.company'] });
143
+
144
+ // 4. Shorthand repository chaining
145
+ const result = await postRepo.include('author.profile').where('user_id', 1).find();
134
146
  ```
135
147
 
148
+ > [!TIP]
149
+ > Nested relations are loaded using optimized batch queries (`whereIn`) with zero N+1 queries. Shared object references and JSON columns on related models are automatically handled.
150
+
136
151
  ---
137
152
 
138
153
  ## 4. Ambient Transactions & AsyncLocalStorage
@@ -228,3 +243,72 @@ defineModel({
228
243
  - **Include Guards**: Excessive `query().with(...)` or `?include=...` relations beyond `maxIncludes` throw `QueryComplexityError` or safely cap relations.
229
244
  - **REST Resources Protection**: Automatically validates client parameters (`?perPage=5000` or `?include=...`) against model complexity boundaries.
230
245
 
246
+ ---
247
+
248
+ ## 8. Universal Full-Text Search (FTS)
249
+
250
+ Webspresso ORM provides database-agnostic full-text and keyword search with transparent dialect optimizations for SQLite, PostgreSQL, and MySQL.
251
+
252
+ ### 8.1 Model Configuration (`searchable`)
253
+
254
+ Declare default searchable columns on your model:
255
+
256
+ ```javascript
257
+ defineModel({
258
+ name: 'Article',
259
+ table: 'articles',
260
+ schema: zdb.schema({
261
+ id: zdb.id(),
262
+ title: zdb.string(),
263
+ content: zdb.text(),
264
+ author: zdb.string(),
265
+ }),
266
+ searchable: ['title', 'content'], // Default columns for .search()
267
+ });
268
+ ```
269
+
270
+ *(Note: If `searchable` is not defined, `.search()` automatically falls back to `admin.searchFields` if available.)*
271
+
272
+ ### 8.2 Using Search in Application Code
273
+
274
+ Search is available both on repository instances (`repo.search`) and on query builders (`query().search()` / `query().orSearch()`):
275
+
276
+ ```javascript
277
+ const articleRepo = req.db.getRepository('Article');
278
+
279
+ // 1. Basic search across default searchable columns
280
+ const results = await articleRepo.search('webspresso framework').get();
281
+
282
+ // 2. Chained with filtering, ordering, and pagination
283
+ const page = await articleRepo
284
+ .search(req.query.q) // Safe no-op when req.query.q is empty or undefined
285
+ .where('status', 'published')
286
+ .orderBy('created_at', 'desc')
287
+ .paginate({ page: 1, limit: 10 });
288
+
289
+ // 3. Dynamic column override
290
+ const byAuthor = await articleRepo.search('Alice', ['author']).get();
291
+
292
+ // 4. Exact phrase matching
293
+ const exact = await articleRepo.search('zero dependency sprawl', null, { exact: true }).get();
294
+
295
+ // 5. OR matching between words (default is AND)
296
+ const anyWord = await articleRepo.search('vue react angular', null, { matchAllWords: false }).get();
297
+
298
+ // 6. Disjoined search conditions with orSearch()
299
+ const mixed = await articleRepo.query()
300
+ .where('author', 'Bob')
301
+ .orSearch('featured article', ['title'])
302
+ .get();
303
+ ```
304
+
305
+ ### 8.3 Dialect Features & Modes
306
+
307
+ | Dialect | Mode (`options.mode`) | SQL Strategy | Notes |
308
+ | :--- | :--- | :--- | :--- |
309
+ | **SQLite** | `'auto'` (default) | Tokenized case-insensitive `LIKE` | Safe out-of-the-box, no virtual table required. |
310
+ | **SQLite** | `'fts'` | `articles MATCH ?` or virtual table subquery | Set `{ mode: 'fts', ftsTable: 'articles_fts' }` for FTS5 tables. |
311
+ | **PostgreSQL** | `'auto'` / `'fts'` | `to_tsvector @@ plainto_tsquery` / `websearch_to_tsquery` | Native Postgres tsvector search. Pass `{ rank: true }` to order by `ts_rank` relevance. |
312
+ | **MySQL** | `'fts'` | `MATCH(...) AGAINST(? IN BOOLEAN MODE)` | Requires FULLTEXT index on table. |
313
+
314
+
@@ -77,6 +77,41 @@ Zero-dependency HTTP Basic Authentication (RFC 7617) plugin with timing-safe equ
77
77
  - `onAuthenticated`: Callback hook `(req, user) => ...`.
78
78
  - **Named Middleware**: Registers `ctx.middlewares.basicAuth` for route files.
79
79
 
80
+ ### 2.2 `apiKeyPlugin` (`plugins/api-key`)
81
+ Zero-dependency B2B & Developer API Key Management Plugin with standard prefix format (`wsp_live_...`), SHA-256 secure hash storage, timing-safe constant-time verification (`crypto.timingSafeEqual`), granular scopes/permissions with wildcard support, non-blocking `last_used_at` usage tracking, and programmatic creation/revocation APIs.
82
+ - **Options**:
83
+ - `db` / `knex`: Webspresso Database or Knex instance.
84
+ - `tableName`: Database table name (default `'api_keys'`).
85
+ - `prefix`: Default key prefix (default `'wsp_live_'`).
86
+ - `byteLength`: Random byte count for key generation (default `24`).
87
+ - `global`: Boolean, whether to protect routes globally (default `false`).
88
+ - `routes`: Route prefix(es) to protect when `global: true`.
89
+ - `skipPaths` / `skip`: Exclusion paths or predicate function `(req) => boolean`.
90
+ - `keys`: Static in-memory keys for testing / serverless / simple setups (`[{ key, name, scopes }]` or map).
91
+ - `trackUsage`: Boolean (default `true`), whether to automatically update `last_used_at` on verified keys.
92
+ - `updateLastUsedThrottleMs`: Throttle period in ms to avoid DB write thrashing (default `60000`).
93
+ - `allowQueryParam`: Boolean (default `false`), allow extracting API key via `?apiKey=...`.
94
+ - `findKeyByHash` / `verify`: Custom key lookup or verification hook.
95
+ - `getUser`: Custom async user resolver `(apiKeyRecord, req) => Promise<User | null>`.
96
+ - `onAuthenticated`: Callback hook `(req, apiKeyRecord) => void`.
97
+ - **Named Middleware**:
98
+ - `middleware: ['apiKey']`: Requires valid API key via `X-API-Key`, `api-key`, or `Authorization: Bearer <key>`.
99
+ - `middleware: [['apiKey', { scopes: ['reports:read', 'orders:*'] }]]`: Enforces required granular scopes with prefix wildcard support (`*`, `namespace:*`).
100
+ - `middleware: [['apiKey', { optional: true }]]`: Non-blocking authentication (populates `req.apiKey` if present, passes through if absent).
101
+ - **Request Context Injected**:
102
+ - `req.apiKey`: The authenticated API key record (including name, key_hint, scopes, metadata).
103
+ - `req.auth.apiKey`: Equivalent reference inside `req.auth`.
104
+ - `req.auth.user` & `req.user`: Associated user record (if `user_id` or `getUser` resolved).
105
+ - **Programmatic API (`apiKeyPlugin.api`)**:
106
+ - `generateKey(options)`: Generate fresh key with hash and masked hint (`wsp_live_...4f2a`).
107
+ - `hashKey(rawKey)`: Compute SHA-256 hex string.
108
+ - `maskKey(rawKey, prefix)`: Mask raw key for logs/UI.
109
+ - `createApiKey(data, options)`: Generates key, hashes it, and stores in database table (returns raw key once).
110
+ - `verifyApiKey(rawKey, options)`: Verify raw key against DB, checking expiration and revocation.
111
+ - `revokeApiKey(idOrHash, options)`: Mark key as revoked in DB (`revoked_at = now()`).
112
+ - `getMigrationTemplate(tableName)`: Knex migration script creating `api_keys` table.
113
+ - `createApiKeyModel(tableName)`: Webspresso ORM model definition with hidden `key_hash`.
114
+
80
115
  ### 2.2 `adminPanelPlugin` (`plugins/admin-panel`)
81
116
  Mithril.js SPA Admin Panel mounted at `/_admin`.
82
117
  - **Options**: `{ path: '/_admin', db, title: 'Webspresso Admin' }`
@@ -217,23 +252,37 @@ Polar.sh subscription billing — checkout, customer portal, webhooks, Customer
217
252
  - [docs/guides/polar-production-checklist.md](../docs/guides/polar-production-checklist.md)
218
253
 
219
254
  ### 2.12 `auditLogPlugin` (`plugins/audit-log`)
220
- Admin mutation tracking and audit history with scheduled retention and manual purge API.
255
+ Admin mutation tracking, record change timeline, and audit history with scheduled retention and manual purge API.
221
256
  - **Options**:
222
257
  - `db`: Database instance (must expose `.knex`).
223
258
  - `tableName`: Database table name (default `'audit_logs'`).
259
+ - `registerRecordAction`: Register built-in "Zaman Tüneli" action on all admin models (default `true`).
224
260
  - `retentionDays`: Days to retain audit trail entries (default `90`).
225
261
  - `cleanupIntervalMs`: Background cleanup timer frequency (default 24h).
226
262
  - `autoCleanup`: Enable scheduled background purge (default `true`).
227
263
  - `cleanupOnStart`: Run initial purge shortly after boot (default `true`).
228
264
  - `retention`: `{ enabled, days: 90, intervalMs: 86400000, runOnStart: true }` (or `false` to disable).
265
+ - **Zaman Tüneli (Timeline UI) Features**:
266
+ - **Dual Mode**: Switch seamlessly between **Zaman Tüneli (Timeline View)** and **Tablo Görünümü (Table View)**.
267
+ - **Record Focus Mode**: Clicking any `Model #ID` tag instantly filters and displays that specific record's revision timeline.
268
+ - **Visual Spine & Status Icons**: Color-coded action nodes for `create` (emerald +), `update` (blue ✏️), `delete` (rose 🗑️), and `restore` (amber ↺).
269
+ - **Changed Fields Diff**: Clearly lists modified fields (e.g. `title`, `price`) on every update event.
270
+ - **Expandable Details**: Inspect full HTTP request path, actor IP, user agent, and pretty-printed JSON metadata.
271
+ - **Built-in Record Action**: One-click "Zaman Tüneli" button on record lists and edit forms jumps straight to that record's audit history.
272
+ - **API & Admin Panel Endpoints**:
273
+ - `GET /_admin/api/audit-logs`: List paginated logs with filters (`model`, `recordId`, `action`, `search`, `from`, `to`).
274
+ - `GET /_admin/api/audit-logs/timeline/:model/:id`: Dedicated chronological record change timeline with summary stats.
275
+ - `POST /_admin/api/audit-logs/purge`: `{ days: 90 }` or `{ olderThan: '2026-01-01' }`.
276
+ - **Programmatic Plugin API**:
277
+ ```javascript
278
+ const audit = ctx.usePlugin('audit-log');
279
+ const history = await audit.queryTimeline('Post', 42);
280
+ const logs = await audit.queryLogs({ model: 'User', action: 'delete' });
281
+ ```
229
282
  - **Pruning CLI**:
230
283
  ```bash
231
284
  webspresso audit:prune --days 90
232
285
  ```
233
- - **API & Admin Panel**:
234
- - `POST /_admin/api/audit-logs/purge`: `{ days: 90 }` or `{ olderThan: '2026-01-01' }`
235
- - "Purge 90d" button directly in the Admin Panel audit log viewer.
236
- - Lifecycle integration: background timers use `.unref()` and clean up on server shutdown.
237
286
 
238
287
  ### 2.13 `ormCacheAdminPlugin` (`plugins/orm-cache-admin`)
239
288
  ORM Cache inspection and invalidation dashboard.
@@ -558,6 +607,25 @@ Interactive Webspresso Developer Dashboard SPA mounted at `/_webspresso` and Flo
558
607
  - **💻 System Tab**: Process uptime, memory usage, environment, and Node platform metrics.
559
608
  - **Zero Production Overhead**: When `process.env.NODE_ENV === 'production'`, `devToolbar()` immediately returns `""` with zero overhead.
560
609
 
610
+ ### 2.19 `oauthPlugin` (`plugins/oauth`)
611
+ Zero-dependency OAuth 2.0 & Social Login plugin supporting Google, GitHub, and custom providers with RFC 7636 PKCE, HMAC-signed anti-CSRF state tokens, and Dual Auth integration.
612
+ - **Options**:
613
+ - `secret`: Secret key for HMAC state signing (`process.env.OAUTH_SECRET || process.env.SESSION_SECRET`).
614
+ - `pathPrefix`: Base path for OAuth routes (default `'/auth'`).
615
+ - `providers`: Provider configuration dictionary (`google`, `github`, or custom).
616
+ - `defaultReturnTo`: Fallback redirect path after successful login (default `'/'`).
617
+ - `failureRedirect`: Redirect path on OAuth failure (default `'/login'`).
618
+ - `findOrCreateUser`: `async ({ provider, profile, tokens, db, req, res }) => user`.
619
+ - `onSuccess`: `async ({ provider, user, profile, tokens, stateData, req, res, client }) => void`.
620
+ - `onError`: `async ({ error, provider, req, res, client }) => void`.
621
+ - **Automatic Routes**:
622
+ - `GET /auth/:provider`: Initiates OAuth authorization flow with PKCE and signed state.
623
+ - `GET /auth/:provider/callback`: Exchanges authorization code, fetches normalized profile, links or creates user, and logs into session or returns JWT token.
624
+ - **Template Helper**:
625
+ - `fsy.oauthUrl('google')` ➔ `"/auth/google"`
626
+ - `fsy.oauthUrl('github', { returnTo: '/dashboard' })` ➔ `"/auth/github?returnTo=%2Fdashboard"`
627
+
628
+
561
629
 
562
630
 
563
631
 
package/.agents/QUEUE.md CHANGED
@@ -154,3 +154,87 @@ webspresso queue:prune --days 7
154
154
  webspresso queue:prune --days 14 --status completed,failed
155
155
  ```
156
156
 
157
+ ---
158
+
159
+ ## 6. Cron & Recurring Task Scheduler
160
+
161
+ Webspresso includes a native, zero-dependency task scheduler with a POSIX-compliant 5-field cron parser (`core/queue/cron.js` & `core/queue/scheduler.js`).
162
+
163
+ ### 1. Cron Expression Syntax
164
+ - Standard 5 fields: `minute hour day-of-month month day-of-week`
165
+ - Wildcards: `*`
166
+ - Steps: `*/5`, `10-30/5`
167
+ - Ranges: `1-5`, `MON-FRI`, `JAN-DEC`
168
+ - Lists: `1,15,30`, `MON,WED,FRI`
169
+ - Descriptive aliases:
170
+ - `@yearly`, `@annually` (`0 0 1 1 *`)
171
+ - `@monthly` (`0 0 1 * *`)
172
+ - `@weekly` (`0 0 * * 0`)
173
+ - `@daily`, `@midnight` (`0 0 * * *`)
174
+ - `@hourly` (`0 * * * *`)
175
+ - `@every_minute`, `@minutely` (`* * * * *`)
176
+ - `@every <N>m` (e.g. `@every 5m` → `*/5 * * * *`)
177
+ - `@every <N>h` (e.g. `@every 2h` → `0 */2 * * *`)
178
+
179
+ ### 2. Programmatic Scheduling
180
+ ```js
181
+ // Schedule daily digest at 09:00 AM
182
+ queue.schedule('0 9 * * *', 'reports.daily-digest', { channel: 'slack' }, {
183
+ overlap: false, // Prevent concurrent execution if previous run is still active (default: false)
184
+ maxRuns: 10, // Optional maximum executions before auto-unscheduling
185
+ utc: false, // Use local timezone or UTC
186
+ });
187
+
188
+ // Or use the .cron() alias
189
+ queue.cron('@every 15m', 'metrics.collect');
190
+
191
+ // Query schedules
192
+ const schedules = queue.getSchedules();
193
+ // [{ id, name, cronExpression, nextRunAt, lastRunAt, isRunning, runsCount, ... }]
194
+
195
+ // Unschedule by schedule ID or job name
196
+ queue.unschedule('metrics.collect');
197
+ ```
198
+
199
+ ### 3. Schedule Options
200
+ | Option | Type | Default | Description |
201
+ | :--- | :--- | :--- | :--- |
202
+ | `id` | `string` | `schedule:${job}:${cron}` | Custom unique identifier |
203
+ | `overlap` | `boolean` | `false` | When `false`, skips tick if a previous job run is still processing |
204
+ | `maxRuns` | `number` | `null` | Automatically unschedules task after N runs |
205
+ | `utc` | `boolean` | `false` | Evaluate cron expressions in UTC instead of local timezone |
206
+ | `runImmediately` | `boolean` | `false` | Triggers a run right after scheduling |
207
+
208
+ ### 4. File-Based Cron Discovery (`jobs/`)
209
+ Jobs in the `jobs/` folder can automatically declare their recurrence in two ways:
210
+
211
+ #### Option A: `cron` / `schedule` property in job module
212
+ ```js
213
+ // jobs/reports/summary.js
214
+ module.exports = async function(job, ctx) {
215
+ // run summary logic...
216
+ };
217
+
218
+ module.exports.cron = '0 8 * * 1-5'; // Every weekday at 8:00 AM
219
+ module.exports.data = { reportType: 'executive' };
220
+ module.exports.options = {
221
+ overlap: false,
222
+ };
223
+ ```
224
+
225
+ #### Option B: `.cron.js` file naming
226
+ ```js
227
+ // jobs/cleanup-logs.cron.js (auto-discovers job name 'cleanup-logs')
228
+ module.exports = {
229
+ cron: '@daily',
230
+ handler: async function(job, ctx) {
231
+ // perform cleanup
232
+ },
233
+ };
234
+ ```
235
+
236
+ ### 5. Lifecycle & Overlap Management
237
+ - The scheduler automatically pauses when `queue.pause()` is called and resumes with `queue.resume()`.
238
+ - On `queue.drain()` or `queue.stop()`, recurring schedules are stopped and active dispatched jobs are given time to complete.
239
+ - Overlap prevention (`overlap: false`) hooks into `job:completed` and `job:failed` events, ensuring slow asynchronous tasks never collide or duplicate.
240
+