webspresso 0.0.98 → 0.0.100

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 (88) hide show
  1. package/.agents/ADMIN.md +11 -1
  2. package/.agents/AGENTS.md +2 -2
  3. package/.agents/AUTH.md +415 -0
  4. package/.agents/CLI.md +47 -0
  5. package/.agents/ERRORS.md +59 -0
  6. package/.agents/ORM.md +178 -4
  7. package/.agents/PLUGINS.md +113 -8
  8. package/.agents/QUEUE.md +92 -0
  9. package/.agents/ROUTING.md +160 -7
  10. package/.agents/STREAMING.md +35 -0
  11. package/AGENTS.md +2 -0
  12. package/README.md +145 -93
  13. package/bin/commands/add-docker.js +467 -0
  14. package/bin/commands/deploy-check.js +339 -0
  15. package/bin/webspresso.js +4 -0
  16. package/core/auth/index.js +123 -1
  17. package/core/auth/magic-link.js +170 -0
  18. package/core/auth/manager.js +477 -3
  19. package/core/auth/middleware.js +118 -5
  20. package/core/auth/oauth.js +517 -0
  21. package/core/auth/qrcode.js +499 -0
  22. package/core/auth/session-manager.js +255 -0
  23. package/core/auth/sessions.js +351 -0
  24. package/core/auth/tokens.js +1 -0
  25. package/core/auth/totp.js +507 -0
  26. package/core/auth/user-agent.js +161 -0
  27. package/core/orm/eager-loader.js +144 -10
  28. package/core/orm/index.js +10 -1
  29. package/core/orm/model.js +55 -1
  30. package/core/orm/query-builder.js +90 -4
  31. package/core/orm/repository.js +327 -16
  32. package/core/orm/scopes.js +3 -2
  33. package/core/orm/search.js +213 -0
  34. package/core/orm/types.js +6 -1
  35. package/core/queue/cron.js +544 -0
  36. package/core/queue/index.js +8 -1
  37. package/core/queue/manager.js +100 -1
  38. package/core/queue/scheduler.js +415 -0
  39. package/core/sse/index.js +19 -0
  40. package/core/sse/stream.js +430 -0
  41. package/core/ssr/stream.js +62 -2
  42. package/index.d.ts +794 -6
  43. package/index.js +48 -0
  44. package/package.json +7 -5
  45. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
  46. package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
  47. package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
  48. package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
  49. package/plugins/admin-panel/field-renderers/index.js +2 -0
  50. package/plugins/admin-panel/modules/admin-users.js +1 -1
  51. package/plugins/api-key/crypto.js +101 -0
  52. package/plugins/api-key/index.js +288 -0
  53. package/plugins/api-key/middleware.js +461 -0
  54. package/plugins/api-key/migration-template.js +48 -0
  55. package/plugins/api-key/model.js +50 -0
  56. package/plugins/api-key/scopes.js +125 -0
  57. package/plugins/audit-log/admin-component.js +487 -70
  58. package/plugins/audit-log/api-handlers.js +114 -0
  59. package/plugins/audit-log/index.js +24 -0
  60. package/plugins/content/client/inline-edit.js +2 -46
  61. package/plugins/cors.js +13 -2
  62. package/plugins/email/auth-bridge.js +66 -0
  63. package/plugins/email/bundled-templates.js +15 -0
  64. package/plugins/email/templates/auth-magic-link.mjml +13 -0
  65. package/plugins/file-manager/admin-component.js +69 -53
  66. package/plugins/file-manager/index.js +25 -8
  67. package/plugins/index.js +8 -0
  68. package/plugins/oauth/index.js +338 -0
  69. package/plugins/queue/index.js +4 -0
  70. package/plugins/webhooks/admin-component.js +608 -0
  71. package/plugins/webhooks/api-handlers.js +237 -0
  72. package/plugins/webhooks/crypto.js +144 -0
  73. package/plugins/webhooks/dispatcher.js +708 -0
  74. package/plugins/webhooks/index.js +263 -0
  75. package/plugins/webhooks/matcher.js +113 -0
  76. package/plugins/webhooks/migration-template.js +75 -0
  77. package/plugins/webhooks/models.js +98 -0
  78. package/plugins/webhooks/sender.js +176 -0
  79. package/src/api/api-loader.js +1 -0
  80. package/src/bundle/index.js +2 -1
  81. package/src/discovery/file-route-parser.js +44 -1
  82. package/src/discovery/scan-routes.js +2 -2
  83. package/src/file-router.js +310 -104
  84. package/src/helpers.js +16 -1
  85. package/src/njk-frontmatter.js +3 -0
  86. package/src/routing/cascading.js +490 -0
  87. package/src/routing/middleware-resolver.js +92 -0
  88. package/src/server.js +6 -0
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,162 @@ 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
+ ---
315
+
316
+ ## 9. Relational Cascading Soft-Delete & Selective Restore (`cascade: true`)
317
+
318
+ Webspresso ORM supports automated relational cascade operations for `hasMany` and `hasOne` relationships. When a parent record is soft-deleted or restored, child records are synchronized automatically.
319
+
320
+ ### 9.1 Relation Configuration
321
+
322
+ Configure `cascade` directly inside your model relations:
323
+
324
+ ```javascript
325
+ defineModel({
326
+ name: 'Author',
327
+ table: 'authors',
328
+ schema: zdb.schema({
329
+ id: zdb.id(),
330
+ name: zdb.string(),
331
+ deleted_at: zdb.timestamp({ nullable: true }),
332
+ }),
333
+ relations: {
334
+ // 1. Full cascade (soft-delete, hard-delete, and restore)
335
+ posts: {
336
+ type: 'hasMany',
337
+ model: () => 'Post', // or () => Post
338
+ foreignKey: 'author_id',
339
+ cascade: true,
340
+ },
341
+ // 2. hasOne cascade
342
+ profile: {
343
+ type: 'hasOne',
344
+ model: () => 'Profile',
345
+ foreignKey: 'author_id',
346
+ cascade: true,
347
+ },
348
+ // 3. Delete-only cascade (child is deleted with parent, but NOT restored)
349
+ tokens: {
350
+ type: 'hasMany',
351
+ model: () => 'ApiToken',
352
+ foreignKey: 'author_id',
353
+ cascade: 'delete', // or { delete: true, restore: false }
354
+ },
355
+ },
356
+ scopes: {
357
+ softDelete: true,
358
+ },
359
+ });
360
+ ```
361
+
362
+ ### 9.2 Cascade Deletion Behavior
363
+
364
+ - **Soft Delete (`repo.delete(id)`)**:
365
+ When an Author is soft-deleted, all active child records in cascading relations (`hasMany`, `hasOne`) are soft-deleted with the **exact same `deleted_at` timestamp**.
366
+ Cascade is recursive: If `Post` also defines `comments` with `cascade: true`, all comments under those posts are cascaded automatically.
367
+ - **Hard Delete (`repo.forceDelete(id)`)**:
368
+ When `forceDelete` is called (or when parent is a non-softDelete model), cascading child records are deleted **before** the parent to avoid foreign-key constraint violations (`FOREIGN KEY constraint failed`).
369
+ - **Cache Invalidation**:
370
+ The ORM query cache for all affected related models is automatically invalidated.
371
+
372
+ ### 9.3 Selective Restore (Smart Timestamp Matching)
373
+
374
+ A classic pitfall in soft-delete architectures is resurrecting child records that the user deliberately deleted prior to deleting the parent account:
375
+
376
+ ```
377
+ T1 (10:00): User manually deletes Post #1 (deleted_at = 10:00).
378
+ T2 (12:00): User account is soft-deleted (deleted_at = 12:00).
379
+ Post #2 and Post #3 receive deleted_at = 12:00.
380
+ T3 (15:00): User account is restored.
381
+ ```
382
+
383
+ By default, Webspresso ORM executes **selective restore**:
384
+ - It restores only child records where `child.deleted_at === parent.deleted_at` (Post #2 and Post #3).
385
+ - Post #1 remains soft-deleted because its `deleted_at` was `10:00`!
386
+
387
+ ```javascript
388
+ // Default: Safe selective restore (protects previously deleted children)
389
+ await authorRepo.restore(authorId);
390
+
391
+ // Optional: Force restore ALL children regardless of when they were deleted
392
+ await authorRepo.restore(authorId, { allChildren: true });
393
+
394
+ // Optional: Bypass cascading for a specific operation
395
+ await authorRepo.delete(authorId, { cascade: false });
396
+ await authorRepo.restore(authorId, { cascade: false });
397
+ ```
398
+
399
+ ### 9.4 Cycle & Recursion Safety
400
+
401
+ If your data model contains circular relations (e.g. `User.partner` <-> `Partner.user`), Webspresso ORM uses an internal visited set (`model:id`) to terminate recursion cleanly without stack overflows.
402
+
403
+
404
+
@@ -77,7 +77,79 @@ 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 `adminPanelPlugin` (`plugins/admin-panel`)
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
+
115
+ ### 2.3 `webhooksPlugin` (`plugins/webhooks`)
116
+ Outgoing Webhook Dispatcher and Management Plugin with zero external dependencies. Dispatches events to subscribed external HTTP endpoints, signs payloads with HMAC-SHA256 (`X-Webspresso-Signature` and standard timestamped `webhook-signature`), filters events with wildcards (`*`, `order.*`), automatically retries failed deliveries using exponential backoff, hooks directly into Webspresso Queue (`core/queue`), and provides an interactive Mithril.js Admin Panel management UI.
117
+ - **Options**:
118
+ - `db` / `knex`: Webspresso Database or Knex instance.
119
+ - `endpointsTable`: Database table for endpoints (default `'webhook_endpoints'`).
120
+ - `deliveriesTable`: Database table for delivery logs (default `'webhook_deliveries'`).
121
+ - `maxAttempts`: Maximum retry attempts on delivery failure (default `3`).
122
+ - `backoffBaseMs`: Base delay for exponential backoff in milliseconds (default `1000`).
123
+ - `timeoutMs`: HTTP request timeout in milliseconds (default `10000`).
124
+ - `wrapPayload`: Boolean (default `true`), wrap data in standard `{ id, event, timestamp, data }` event envelope.
125
+ - `queue`: Custom `QueueManager` instance (auto-detected from `ctx.app.queue` if present).
126
+ - `endpoints`: Static in-memory endpoints array (useful for testing or serverless setups).
127
+ - `apiPrefix`: Admin REST API prefix (default `'/webhooks'`).
128
+ - `includeDefaultPage`: Register Admin UI page and sidebar menu item (default `true`).
129
+ - `pageTitle`: Admin page title (default `'Webhooks'`).
130
+ - `menuOrder`: Admin sidebar order (default `92`).
131
+ - **Request & Application Context Injected**:
132
+ - `req.webhooks`: Direct reference to the `WebhookDispatcher` instance.
133
+ - `app.webhooks`: Direct reference on the Webspresso Application instance.
134
+ - **Built-in Services Registered**:
135
+ - `'webhook.dispatch'`: Dispatch events via `ctx.service('webhook.dispatch', { event, payload })`.
136
+ - `'webhook.ping'`: Test endpoint connectivity via `ctx.service('webhook.ping', { endpointId })`.
137
+ - `'webhook.retry'`: Manually retry a delivery via `ctx.service('webhook.retry', { deliveryId })`.
138
+ - **Programmatic API (`webhooksPlugin.api` / `dispatcher`)**:
139
+ - `dispatch(event, payload, options)`: Match endpoints and deliver payload asynchronously (or await with `wait: true`).
140
+ - `pingEndpoint(endpointId)`: Send a test `ping` event to verify destination connectivity.
141
+ - `retryDelivery(deliveryId)`: Re-send a failed delivery with incremented attempt count.
142
+ - `createEndpoint(data)`: Register a new webhook endpoint with custom or auto-generated `whsec_` secret.
143
+ - `updateEndpoint(id, data)` / `deleteEndpoint(id)`: Update or delete endpoint records.
144
+ - `listEndpoints(filters)`: List endpoints with delivery statistics.
145
+ - `listDeliveries(filters)`: Query delivery logs with filtering by endpoint, event, and success status.
146
+ - `purgeDeliveries(options)`: Purge logs older than specified retention days.
147
+ - `signPayload(payload, secret, options)`: Compute HMAC-SHA256 signature (`sha256=...` and `t=...,v1=...`).
148
+ - `verifySignature(payload, header, secret, options)`: Constant-time signature verification with drift tolerance.
149
+ - `getMigrationTemplate()`: Knex migration script for `webhook_endpoints` and `webhook_deliveries`.
150
+ - `getModels()`: Webspresso ORM model definitions for `WebhookEndpoint` and `WebhookDelivery`.
151
+
152
+ ### 2.4 `adminPanelPlugin` (`plugins/admin-panel`)
81
153
  Mithril.js SPA Admin Panel mounted at `/_admin`.
82
154
  - **Options**: `{ path: '/_admin', db, title: 'Webspresso Admin' }`
83
155
  - **Features**: Auto CRUD screens for ORM models, user staff auth, custom page registration, widgets, field renderers.
@@ -96,14 +168,14 @@ Multipart file upload manager with local / cloud storage providers.
96
168
 
97
169
  ### 2.5 `fileManagerPlugin` (`plugins/file-manager`)
98
170
  Filesystem-first media library, directory explorer, and modal asset picker for Admin Panel.
99
- - **Options**: `{ baseDir: './public/uploads', publicBasePath: '/uploads', apiPrefix: '/files', pageTitle: 'Dosya Yöneticisi', maxUploadBytes: 10485760, allowedExtensions, allowedMimeTypes, includePage: true }`
171
+ - **Options**: `{ baseDir: './public/uploads', publicBasePath: '/uploads', apiPrefix: '/files', pageTitle: 'File Manager', maxUploadBytes: 10485760, allowedExtensions, allowedMimeTypes, includePage: true }`
100
172
  - **Features**:
101
173
  - Directory tree navigation (breadcrumbs), safe subfolder creation (`mkdir`), item rename, delete, and move.
102
174
  - Path traversal attack protection (`resolveSafePath`).
103
175
  - Grid (thumbnail gallery) & List (table) view modes.
104
176
  - Search by filename and type filter (Images, Documents, Videos, Audio, Archives, Code).
105
177
  - Media preview modal with copy URL and direct playback/preview.
106
- - "Kütüphaneden Seç" modal picker integrated with `FileUploadField` and `zdb.file()` form fields.
178
+ - "Choose from Media Library" modal picker integrated with `FileUploadField` and `zdb.file()` form fields.
107
179
  - Built-in services: `fileManager.list`, `fileManager.mkdir`, `fileManager.rename`, `fileManager.delete`, `fileManager.move`, `fileManager.upload`.
108
180
 
109
181
  ### 2.6 `corsPlugin` (`plugins/cors`)
@@ -217,23 +289,37 @@ Polar.sh subscription billing — checkout, customer portal, webhooks, Customer
217
289
  - [docs/guides/polar-production-checklist.md](../docs/guides/polar-production-checklist.md)
218
290
 
219
291
  ### 2.12 `auditLogPlugin` (`plugins/audit-log`)
220
- Admin mutation tracking and audit history with scheduled retention and manual purge API.
292
+ Admin mutation tracking, record change timeline, and audit history with scheduled retention and manual purge API.
221
293
  - **Options**:
222
294
  - `db`: Database instance (must expose `.knex`).
223
295
  - `tableName`: Database table name (default `'audit_logs'`).
296
+ - `registerRecordAction`: Register built-in "Timeline" action on all admin models (default `true`).
224
297
  - `retentionDays`: Days to retain audit trail entries (default `90`).
225
298
  - `cleanupIntervalMs`: Background cleanup timer frequency (default 24h).
226
299
  - `autoCleanup`: Enable scheduled background purge (default `true`).
227
300
  - `cleanupOnStart`: Run initial purge shortly after boot (default `true`).
228
301
  - `retention`: `{ enabled, days: 90, intervalMs: 86400000, runOnStart: true }` (or `false` to disable).
302
+ - **Timeline UI Features**:
303
+ - **Dual Mode**: Switch seamlessly between **Timeline View** and **Table View**.
304
+ - **Record Focus Mode**: Clicking any `Model #ID` tag instantly filters and displays that specific record's revision timeline.
305
+ - **Visual Spine & Status Icons**: Color-coded action nodes for `create` (emerald +), `update` (blue ✏️), `delete` (rose 🗑️), and `restore` (amber ↺).
306
+ - **Changed Fields Diff**: Clearly lists modified fields (e.g. `title`, `price`) on every update event.
307
+ - **Expandable Details**: Inspect full HTTP request path, actor IP, user agent, and pretty-printed JSON metadata.
308
+ - **Built-in Record Action**: One-click "Timeline" button on record lists and edit forms jumps straight to that record's audit history.
309
+ - **API & Admin Panel Endpoints**:
310
+ - `GET /_admin/api/audit-logs`: List paginated logs with filters (`model`, `recordId`, `action`, `search`, `from`, `to`).
311
+ - `GET /_admin/api/audit-logs/timeline/:model/:id`: Dedicated chronological record change timeline with summary stats.
312
+ - `POST /_admin/api/audit-logs/purge`: `{ days: 90 }` or `{ olderThan: '2026-01-01' }`.
313
+ - **Programmatic Plugin API**:
314
+ ```javascript
315
+ const audit = ctx.usePlugin('audit-log');
316
+ const history = await audit.queryTimeline('Post', 42);
317
+ const logs = await audit.queryLogs({ model: 'User', action: 'delete' });
318
+ ```
229
319
  - **Pruning CLI**:
230
320
  ```bash
231
321
  webspresso audit:prune --days 90
232
322
  ```
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
323
 
238
324
  ### 2.13 `ormCacheAdminPlugin` (`plugins/orm-cache-admin`)
239
325
  ORM Cache inspection and invalidation dashboard.
@@ -558,6 +644,25 @@ Interactive Webspresso Developer Dashboard SPA mounted at `/_webspresso` and Flo
558
644
  - **💻 System Tab**: Process uptime, memory usage, environment, and Node platform metrics.
559
645
  - **Zero Production Overhead**: When `process.env.NODE_ENV === 'production'`, `devToolbar()` immediately returns `""` with zero overhead.
560
646
 
647
+ ### 2.19 `oauthPlugin` (`plugins/oauth`)
648
+ 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.
649
+ - **Options**:
650
+ - `secret`: Secret key for HMAC state signing (`process.env.OAUTH_SECRET || process.env.SESSION_SECRET`).
651
+ - `pathPrefix`: Base path for OAuth routes (default `'/auth'`).
652
+ - `providers`: Provider configuration dictionary (`google`, `github`, or custom).
653
+ - `defaultReturnTo`: Fallback redirect path after successful login (default `'/'`).
654
+ - `failureRedirect`: Redirect path on OAuth failure (default `'/login'`).
655
+ - `findOrCreateUser`: `async ({ provider, profile, tokens, db, req, res }) => user`.
656
+ - `onSuccess`: `async ({ provider, user, profile, tokens, stateData, req, res, client }) => void`.
657
+ - `onError`: `async ({ error, provider, req, res, client }) => void`.
658
+ - **Automatic Routes**:
659
+ - `GET /auth/:provider`: Initiates OAuth authorization flow with PKCE and signed state.
660
+ - `GET /auth/:provider/callback`: Exchanges authorization code, fetches normalized profile, links or creates user, and logs into session or returns JWT token.
661
+ - **Template Helper**:
662
+ - `fsy.oauthUrl('google')` ➔ `"/auth/google"`
663
+ - `fsy.oauthUrl('github', { returnTo: '/dashboard' })` ➔ `"/auth/github?returnTo=%2Fdashboard"`
664
+
665
+
561
666
 
562
667
 
563
668
 
package/.agents/QUEUE.md CHANGED
@@ -154,3 +154,95 @@ 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: `*`, `?` (supported in day-of-month and day-of-week)
166
+ - Steps & Offsets: `*/5`, `10/15` (starting at minute 10, every 15 min), `10-30/5`
167
+ - Ranges: `1-5`, `5-7` (Fri-Sun), `MON-FRI`, `MON-SUN`, `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
+ - `@weekdays`, `@workdays` (`0 0 * * 1-5`)
174
+ - `@weekends` (`0 0 * * 6,0`)
175
+ - `@daily`, `@midnight` (`0 0 * * *`)
176
+ - `@hourly`, `@every_hour` (`0 * * * *`)
177
+ - `@every_minute`, `@minutely` (`* * * * *`)
178
+ - `@every <N>m` (e.g. `@every 5m` → `*/5 * * * *`)
179
+ - `@every <N>h` (e.g. `@every 2h` → `0 */2 * * *`)
180
+ - `@every <N>d` (e.g. `@every 2d` → `0 0 */2 * *`)
181
+
182
+ ### 2. Programmatic Scheduling
183
+ ```js
184
+ // Schedule daily digest at 09:00 AM with Timezone support
185
+ queue.schedule('0 9 * * *', 'reports.daily-digest', { channel: 'slack' }, {
186
+ timezone: 'Europe/Istanbul', // IANA timezone support ('Europe/Istanbul', 'America/New_York')
187
+ overlap: false, // Prevent concurrent execution if previous run is still active (default: false)
188
+ maxRuns: 10, // Optional maximum executions before auto-unscheduling
189
+ });
190
+
191
+ // Or use the .cron() alias
192
+ queue.cron('@every 15m', 'metrics.collect');
193
+
194
+ // Query schedules
195
+ const schedules = queue.getSchedules();
196
+ // [{ id, name, cronExpression, nextRunAt, lastRunAt, isRunning, activeJobId, activeJobIds, runsCount, ... }]
197
+
198
+ // Manually trigger a schedule immediately
199
+ await queue.triggerSchedule('reports.daily-digest');
200
+
201
+ // Unschedule by schedule ID or job name (removes all schedules matching the job name)
202
+ queue.unschedule('metrics.collect');
203
+ ```
204
+
205
+ ### 3. Schedule Options
206
+ | Option | Type | Default | Description |
207
+ | :--- | :--- | :--- | :--- |
208
+ | `id` | `string` | `schedule:${job}:${cron}` | Custom unique identifier |
209
+ | `overlap` | `boolean` | `false` | When `false`, skips tick if a previous job run is still processing |
210
+ | `preventOverlap` | `boolean` | `true` | Alias for `overlap: false` |
211
+ | `maxRuns` | `number` | `null` | Automatically unschedules task after N runs |
212
+ | `utc` | `boolean` | `false` | Evaluate cron expressions in UTC instead of local timezone |
213
+ | `timezone` (`tz`) | `string` | `null` | IANA timezone name (e.g. `'Europe/Istanbul'`, `'America/New_York'`) |
214
+ | `runImmediately` | `boolean` | `false` | Triggers a run right after scheduling |
215
+
216
+ ### 4. File-Based Cron Discovery (`jobs/`)
217
+ Jobs in the `jobs/` folder can automatically declare their recurrence in two ways:
218
+
219
+ #### Option A: `cron` / `schedule` property in job module
220
+ ```js
221
+ // jobs/reports/summary.js
222
+ module.exports = async function(job, ctx) {
223
+ // run summary logic...
224
+ };
225
+
226
+ module.exports.cron = '0 8 * * 1-5'; // Every weekday at 8:00 AM
227
+ module.exports.data = { reportType: 'executive' };
228
+ module.exports.options = {
229
+ overlap: false,
230
+ };
231
+ ```
232
+
233
+ #### Option B: `.cron.js` file naming
234
+ ```js
235
+ // jobs/cleanup-logs.cron.js (auto-discovers job name 'cleanup-logs')
236
+ module.exports = {
237
+ cron: '@daily',
238
+ handler: async function(job, ctx) {
239
+ // perform cleanup
240
+ },
241
+ };
242
+ ```
243
+
244
+ ### 5. Lifecycle & Overlap Management
245
+ - The scheduler automatically pauses when `queue.pause()` is called and resumes with `queue.resume()`.
246
+ - On `queue.drain()` or `queue.stop()`, recurring schedules are stopped and active dispatched jobs are given time to complete.
247
+ - Overlap prevention (`overlap: false`) hooks into `job:completed` and `job:failed` events, ensuring slow asynchronous tasks never collide or duplicate.
248
+