webspresso 0.0.99 → 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 (60) hide show
  1. package/.agents/AGENTS.md +2 -2
  2. package/.agents/AUTH.md +246 -0
  3. package/.agents/CLI.md +47 -0
  4. package/.agents/ERRORS.md +59 -0
  5. package/.agents/ORM.md +90 -0
  6. package/.agents/PLUGINS.md +44 -7
  7. package/.agents/QUEUE.md +18 -10
  8. package/.agents/ROUTING.md +33 -0
  9. package/.agents/STREAMING.md +35 -0
  10. package/AGENTS.md +1 -0
  11. package/README.md +145 -93
  12. package/bin/commands/add-docker.js +467 -0
  13. package/bin/commands/deploy-check.js +339 -0
  14. package/bin/webspresso.js +4 -0
  15. package/core/auth/index.js +63 -1
  16. package/core/auth/magic-link.js +170 -0
  17. package/core/auth/manager.js +343 -3
  18. package/core/auth/middleware.js +30 -5
  19. package/core/auth/session-manager.js +255 -0
  20. package/core/auth/sessions.js +351 -0
  21. package/core/auth/tokens.js +1 -0
  22. package/core/auth/totp.js +4 -1
  23. package/core/auth/user-agent.js +161 -0
  24. package/core/orm/index.js +2 -1
  25. package/core/orm/model.js +51 -1
  26. package/core/orm/repository.js +283 -13
  27. package/core/orm/scopes.js +3 -2
  28. package/core/orm/types.js +6 -1
  29. package/core/queue/cron.js +252 -39
  30. package/core/queue/manager.js +20 -2
  31. package/core/queue/scheduler.js +115 -59
  32. package/core/ssr/stream.js +62 -2
  33. package/index.d.ts +274 -14
  34. package/index.js +4 -0
  35. package/package.json +7 -5
  36. package/plugins/admin-panel/modules/admin-users.js +1 -1
  37. package/plugins/audit-log/admin-component.js +54 -54
  38. package/plugins/audit-log/index.js +2 -2
  39. package/plugins/content/client/inline-edit.js +2 -46
  40. package/plugins/cors.js +13 -2
  41. package/plugins/email/auth-bridge.js +66 -0
  42. package/plugins/email/bundled-templates.js +15 -0
  43. package/plugins/email/templates/auth-magic-link.mjml +13 -0
  44. package/plugins/file-manager/admin-component.js +49 -49
  45. package/plugins/file-manager/index.js +4 -4
  46. package/plugins/index.js +2 -0
  47. package/plugins/webhooks/admin-component.js +608 -0
  48. package/plugins/webhooks/api-handlers.js +237 -0
  49. package/plugins/webhooks/crypto.js +144 -0
  50. package/plugins/webhooks/dispatcher.js +708 -0
  51. package/plugins/webhooks/index.js +263 -0
  52. package/plugins/webhooks/matcher.js +113 -0
  53. package/plugins/webhooks/migration-template.js +75 -0
  54. package/plugins/webhooks/models.js +98 -0
  55. package/plugins/webhooks/sender.js +176 -0
  56. package/src/bundle/index.js +2 -1
  57. package/src/file-router.js +173 -4
  58. package/src/helpers.js +7 -1
  59. package/src/njk-frontmatter.js +3 -0
  60. package/src/routing/cascading.js +112 -0
package/.agents/AGENTS.md CHANGED
@@ -44,11 +44,11 @@ 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 19 built-in official plugins (including `apiKeyPlugin`, `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 20 built-in official plugins (including `webhooksPlugin`, `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).
51
- 13. **[CLI Tooling & Commands](.agents/CLI.md)** — `webspresso dev`, `build`, `doctor`, `db:migrate`, `db:seed`, `polar:migrate`, `favicon:generate`, `add:tailwind`, `agents:init`.
51
+ 13. **[CLI Tooling & Commands](CLI.md)** — `webspresso dev`, `build`, `doctor`, `db:migrate`, `db:seed`, `polar:migrate`, `favicon:generate`, `add:tailwind`, `add:docker`, `deploy:check`, `agents:init`.
52
52
  14. **[Development & Testing Workflow](.agents/WORKFLOW.md)** — CLI commands, Vitest unit/integration testing, Playwright E2E tests, TypeScript type checking, and zero regression policy.
53
53
  15. **[Coding Standards & Conventions](.agents/CONVENTIONS.md)** — Zero external dependencies policy, strict function signature & schema verification, clean component separation, and log traceback inspection rules.
54
54
  16. **[Background Job Queue](.agents/QUEUE.md)** — In-Memory, Knex Database, and Distributed Redis queue adapters, job auto-discovery (`jobs/`), retries, and graceful draining.
package/.agents/AUTH.md CHANGED
@@ -45,6 +45,10 @@ Attached to Express request during auth middleware execution:
45
45
  | `req.auth.refreshAccessToken(refreshToken, options)` | Refresh access token & rotate refresh token |
46
46
  | `req.auth.check()` / `req.auth.guest()` | Check if user is authenticated or guest |
47
47
  | `req.auth.user()` / `req.auth.id()` | Get current user object or ID |
48
+ | `req.auth.sessions()` | List active sessions & device info for current user |
49
+ | `req.auth.revokeSession(sessionId)` | Revoke a specific session (remotely log out device) |
50
+ | `req.auth.revokeOtherSessions()` | Revoke all sessions except the current active session |
51
+ | `req.auth.revokeAllSessions(userId?)` | Revoke all active sessions (force logout from all devices) |
48
52
 
49
53
  ---
50
54
 
@@ -269,4 +273,246 @@ module.exports = {
269
273
  };
270
274
  ```
271
275
 
276
+ ---
277
+
278
+ ## 7. Magic Link & Passwordless Authentication
279
+
280
+ Webspresso includes zero-dependency, cryptographically secure **Magic Link / Passwordless Authentication**:
281
+ - **Single-Use Timed Tokens**: 32-byte cryptographically secure random tokens stored hashed (`SHA-256`) in the `auth_tokens` table (`type = 'magic_link'`) with a default 15-minute TTL.
282
+ - **Anti-Enumeration Protection**: `requestMagicLink` returns `{ sent: true, userFound: false }` for unknown emails by default so attackers cannot probe user existence.
283
+ - **Auto-Verification**: Successfully signing in via a magic link automatically marks `email_verified_at` if the user is not yet verified.
284
+ - **Dual Session & JWT Support**: Seamlessly establishes web sessions (`req.auth.login`) and/or issues JWT access/refresh token pairs.
285
+ - **Native Email Plugin Integration**: Built-in `auth-magic-link` MJML template, automated delivery via `plugins/email`'s `wireAuthEmails`, and pre-wired `/api/auth/magic-link` routes.
286
+
287
+ ### 7.1 Setup in `quickAuth`
288
+ ```javascript
289
+ const { quickAuth } = require('webspresso/core/auth');
290
+
291
+ const auth = quickAuth({
292
+ db,
293
+ userModel: 'User',
294
+ identifierField: 'email',
295
+ magicLink: true, // Automatically provisions Knex auth_tokens adapter
296
+ });
297
+ ```
298
+
299
+ ### 7.2 Requesting a Magic Link (`pages/api/auth/magic-link.post.js`)
300
+ ```javascript
301
+ const { defineApi } = require('webspresso');
302
+
303
+ module.exports = defineApi({
304
+ schema: ({ z }) => ({
305
+ body: z.object({
306
+ email: z.string().email(),
307
+ }),
308
+ }),
309
+ async handler(req, res) {
310
+ const { email } = req.input.body;
311
+
312
+ // Dispatches email notification via plugins/email (if active)
313
+ await req.auth.requestMagicLink(email, {
314
+ tokenTtlMs: 15 * 60 * 1000, // 15 mins (default)
315
+ path: '/auth/magic-link/verify',
316
+ });
317
+
318
+ return res.json({
319
+ ok: true,
320
+ message: 'If an account exists with this email, a sign-in link has been sent.',
321
+ });
322
+ },
323
+ });
324
+ ```
325
+
326
+ ### 7.3 Verifying & Logging In (`pages/api/auth/magic-link/verify.get.js`)
327
+ ```javascript
328
+ const { defineApi } = require('webspresso');
329
+
330
+ module.exports = defineApi({
331
+ schema: ({ z }) => ({
332
+ query: z.object({
333
+ token: z.string().min(1),
334
+ }),
335
+ }),
336
+ async handler(req, res) {
337
+ const { token } = req.input.query;
338
+
339
+ // Verifies token, consumes it, marks email verified, logs into session and/or generates JWTs
340
+ const result = await req.auth.loginWithMagicLink(token, {
341
+ remember: true,
342
+ jwt: false, // Set to true to receive accessToken and refreshToken
343
+ });
344
+
345
+ // For browser requests, redirect to dashboard:
346
+ if (req.accepts('html')) {
347
+ return res.redirect('/dashboard');
348
+ }
349
+
350
+ return res.json({ ok: true, user: result.user });
351
+ },
352
+ });
353
+ ```
354
+
355
+ ### 7.4 Using Built-in Route Middlewares
356
+ You can also mount Webspresso's pre-configured Express middlewares directly:
357
+ ```javascript
358
+ const {
359
+ createMagicLinkRequestHandler,
360
+ createMagicLinkVerifyMiddleware,
361
+ } = require('webspresso/core/auth');
362
+
363
+ // Request link: POST /auth/magic-link
364
+ app.post('/auth/magic-link', createMagicLinkRequestHandler(authManager));
365
+
366
+ // Verify & Login: GET /auth/magic-link/verify
367
+ app.get('/auth/magic-link/verify', createMagicLinkVerifyMiddleware(authManager, {
368
+ redirectTo: '/dashboard',
369
+ failureRedirect: '/login?error=invalid_magic_link',
370
+ jwt: false,
371
+ }));
372
+ ```
373
+
374
+ ### 7.5 AuthManager Magic Link Events
375
+ `AuthManager` extends `EventEmitter`, allowing you to hook into magic link lifecycle events:
376
+ ```javascript
377
+ authManager.on('magicLink:requested', ({ userId, email, expiresAt }) => {
378
+ console.log(`Magic link requested for ${email}, expires at ${expiresAt}`);
379
+ });
380
+
381
+ authManager.on('magicLink:verified', ({ userId }) => {
382
+ console.log(`User ${userId} signed in via magic link`);
383
+ });
384
+ ```
385
+
386
+ ---
387
+
388
+ ## 8. Active Devices & Remote Session Revocation
389
+
390
+ Webspresso provides a zero-dependency session management and device tracking system (`webspresso/core/auth`) allowing users to view all active logins, detect browser/OS/hardware types, and remotely revoke unwanted sessions.
391
+
392
+ ### 8.1 Database Migration
393
+ Create the `user_sessions` table using the built-in migration helper:
394
+ ```javascript
395
+ const { createUserSessionsTable } = require('webspresso/core/auth');
396
+
397
+ exports.up = async function(knex) {
398
+ await createUserSessionsTable(knex);
399
+ };
400
+
401
+ exports.down = async function(knex) {
402
+ const { dropUserSessionsTable } = require('webspresso/core/auth');
403
+ await dropUserSessionsTable(knex);
404
+ };
405
+ ```
406
+
407
+ Schema columns:
408
+ - `id`: Bigint primary key
409
+ - `session_id`: Unique Express session ID
410
+ - `user_id`: User ID reference
411
+ - `ip_address`: Proxy-safe client IP (`x-forwarded-for`, `x-real-ip`, or socket)
412
+ - `user_agent`: Raw User-Agent string
413
+ - `browser`: Detected browser and version (e.g. `Chrome 128`, `Safari 17`, `Firefox 125`, `Edge 122`)
414
+ - `os`: Detected operating system (e.g. `macOS 14`, `Windows 10/11`, `iOS 17.4`, `Android 14`, `Ubuntu Linux`)
415
+ - `device`: Device form factor (`desktop`, `mobile`, `tablet`, `bot`)
416
+ - `last_active_at`: Last seen timestamp (throttled to 60s by default to protect DB performance)
417
+ - `created_at`: Initial login timestamp
418
+
419
+ ### 8.2 Enabling Session Tracking
420
+ Enable session tracking in `createAuth` or `quickAuth`:
421
+ ```javascript
422
+ const { quickAuth } = require('webspresso/core/auth');
423
+
424
+ const auth = quickAuth({
425
+ db,
426
+ sessionTracking: true, // Automatically enables Knex session tracker and active session verification
427
+ });
428
+ ```
429
+
430
+ Or manually with custom options:
431
+ ```javascript
432
+ const { createAuth, createKnexSessionTracker } = require('webspresso/core/auth');
433
+
434
+ const auth = createAuth({
435
+ db,
436
+ sessionTracking: true,
437
+ sessionTracker: createKnexSessionTracker(db),
438
+ sessionTouchThrottleMs: 60000, // Touch session at most once per minute
439
+ // ...other config
440
+ });
441
+ ```
442
+
443
+ ### 8.3 File-Based API Routes (`pages/api/*`)
444
+
445
+ #### List Active Devices (`pages/api/auth/sessions.get.js`)
446
+ ```javascript
447
+ const { defineApi } = require('webspresso');
448
+
449
+ module.exports = defineApi({
450
+ middleware: ['auth'],
451
+ async handler(req, res) {
452
+ // Returns array of active sessions with is_current flag for current device
453
+ const sessions = await req.auth.sessions();
454
+ return res.json({ ok: true, sessions });
455
+ },
456
+ });
457
+ ```
458
+
459
+ #### Revoke Session / Remote Logout (`pages/api/auth/sessions/revoke.post.js`)
460
+ ```javascript
461
+ const { defineApi } = require('webspresso');
462
+
463
+ module.exports = defineApi({
464
+ middleware: ['auth'],
465
+ schema: ({ z }) => ({
466
+ body: z.object({
467
+ sessionId: z.string().optional(),
468
+ allOthers: z.boolean().optional(),
469
+ }),
470
+ }),
471
+ async handler(req, res) {
472
+ const { sessionId, allOthers } = req.input.body;
473
+
474
+ if (allOthers) {
475
+ // Revokes all sessions except current device
476
+ const count = await req.auth.revokeOtherSessions();
477
+ return res.json({ ok: true, revokedCount: count, message: 'All other sessions revoked' });
478
+ }
479
+
480
+ if (sessionId) {
481
+ // Revokes specified session (logs out current user if it is the current session)
482
+ await req.auth.revokeSession(sessionId);
483
+ return res.json({ ok: true, message: 'Session revoked' });
484
+ }
485
+
486
+ return res.status(400).json({ error: 'sessionId or allOthers is required' });
487
+ },
488
+ });
489
+ ```
490
+
491
+ ### 8.4 Automatic Invalidation in Middleware
492
+ When a session is revoked remotely, Webspresso's `authenticate` middleware automatically detects it on the next incoming request from that device:
493
+ - The revoked session is destroyed in `req.sessionStore`.
494
+ - `req.user` is set to `null`.
495
+ - Any subsequent `requireAuth` or `middleware: ['auth']` guard immediately redirects or rejects the request with `401 Unauthorized`.
496
+
497
+ ### 8.5 Session Lifecycle Events
498
+ `AuthManager` emits EventEmitter events for monitoring and security audits:
499
+ ```javascript
500
+ auth.on('session:created', ({ sessionId, userId, ip, browser, os, device }) => {
501
+ console.log(`New session created for user ${userId} from ${device} (${browser} on ${os}) at ${ip}`);
502
+ });
503
+
504
+ auth.on('session:revoked', ({ sessionId, userId, revokedBy }) => {
505
+ console.log(`Session ${sessionId} was revoked`);
506
+ });
507
+
508
+ auth.on('sessions:revoked_others', ({ userId, currentSessionId, count }) => {
509
+ console.log(`User ${userId} revoked ${count} other sessions`);
510
+ });
511
+
512
+ auth.on('sessions:revoked_all', ({ userId, count }) => {
513
+ console.log(`All ${count} sessions revoked for user ${userId}`);
514
+ });
515
+ ```
516
+
517
+
272
518
 
package/.agents/CLI.md CHANGED
@@ -302,8 +302,55 @@ npx webspresso typegen --out types/webspresso.d.ts --no-env-file
302
302
  - **Typed Services (`req.service('name', input)`)**: Auto-completes registered service names and enforces validated input payloads.
303
303
  - **Typed Route Table (`AppRoute`, `AppRoutePath`)**: Union types for all discovered SSR and API routes.
304
304
 
305
+ ---
306
+
307
+ ## 16. Containerization & Docker Setup (`add:docker` / `docker`)
308
+
309
+ Generate production-ready, security-hardened, multi-stage `Dockerfile`, `.dockerignore`, and `docker-compose.yml`:
310
+
311
+ ```bash
312
+ # Generate Dockerfile, .dockerignore, and docker-compose.yml
313
+ npx webspresso add:docker
314
+
315
+ # Customize Node.js version, database service, or container port
316
+ npx webspresso add:docker --node 22-alpine --db postgres --redis --port 3000
305
317
 
318
+ # Overwrite existing files
319
+ npx webspresso add:docker --force
320
+ ```
306
321
 
322
+ ### Generated Artifacts:
323
+ - **Multi-Stage `Dockerfile`**:
324
+ - `deps` stage: caches production dependencies with native build tools (`python3`, `make`, `g++`).
325
+ - `builder` stage: runs asset build scripts (`npm run build`).
326
+ - `runner` stage: runs as unprivileged user `webspresso` (UID 1001), health check with `/health` and `/healthz`, and `SIGTERM` signal handling.
327
+ - **`.dockerignore`**: Excludes `node_modules`, `.env`, local `.sqlite` databases, test files, and git metadata while preserving upload directories.
328
+ - **`docker-compose.yml`**: Configures app container with persistent upload volumes, database services (PostgreSQL, MySQL, or SQLite), and optional Redis.
329
+ - **`package.json` Scripts**: Adds `docker:build`, `docker:run`, `docker:up`, and `docker:down`.
307
330
 
331
+ ---
308
332
 
333
+ ## 17. Production Deployment Preflight Check (`deploy:check` / `check:deploy`)
334
+
335
+ Run comprehensive pre-flight verification before deploying to Fly.io, Railway, Render, VPS, or Kubernetes:
336
+
337
+ ```bash
338
+ # Run production preflight verification
339
+ npx webspresso deploy:check
340
+
341
+ # Run against a specific environment (.env.staging or .env.production)
342
+ npx webspresso deploy:check --env staging
343
+
344
+ # Strict mode: fail with exit code 1 on warnings as well as errors (ideal for CI/CD)
345
+ npx webspresso deploy:check --strict
346
+
347
+ # Machine-readable JSON output for automated deployment pipelines
348
+ npx webspresso deploy:check --json
349
+ ```
309
350
 
351
+ ### Audited Verification Areas:
352
+ 1. **Secrets & Cryptography**: Ensures `SESSION_SECRET` or `APP_KEY` is set, >= 32 characters, and not using weak placeholder defaults.
353
+ 2. **Git & Leak Prevention**: Verifies `.env` and local database files are excluded in `.gitignore`.
354
+ 3. **Container Hardening**: Ensures `Dockerfile` runs as a non-root `USER`, configures a `HEALTHCHECK`, and uses multi-stage caching.
355
+ 4. **Database & Migrations**: Audits for pending unapplied migrations and warns against ephemeral SQLite storage.
356
+ 5. **Server Optimizations**: Validates HTTP compression (`compression: true`), graceful shutdown, and health check endpoints.
package/.agents/ERRORS.md CHANGED
@@ -199,3 +199,62 @@ In server logs, 500 errors are automatically highlighted with visual banners pin
199
199
  Phase: handler
200
200
  ```
201
201
 
202
+ ---
203
+
204
+ ## 6. Directory-Level Error Boundaries (`_error.njk` & `_error.js`)
205
+
206
+ In addition to API JSON errors and the top-level 500 error page, Webspresso provides **Directory-Level Error Boundaries** for SSR pages (similar to Next.js `error.js` or Nuxt `error.vue`):
207
+
208
+ ```
209
+ pages/
210
+ ├── _layout.njk # Root site layout
211
+ ├── _error.njk # Global site error fallback
212
+ └── dashboard/
213
+ ├── _layout.njk # Dashboard layout (sidebar, header)
214
+ ├── _error.njk # Dashboard-scoped error boundary
215
+ ├── _error.js # Optional companion loader
216
+ ├── billing/
217
+ │ └── invoices.njk # If load() fails here...
218
+ └── settings.njk
219
+ ```
220
+
221
+ ### 6.1 Isolated Error Containment
222
+ When an unhandled error is thrown in a page route's `load()`, template rendering, or lifecycle hooks:
223
+ 1. Webspresso searches up the directory hierarchy (innermost leaf to root `pages/`) for the nearest `_error.njk`.
224
+ 2. The error boundary is rendered **inside** the ancestor cascading layouts (`_layout.njk`), ensuring that headers, sidebars, and navigation remain visible while displaying the isolated error state.
225
+ 3. The HTTP response status is preserved from `error.status || error.statusCode || 500` (e.g. throwing `NotFoundError` sets HTTP 404).
226
+
227
+ ### 6.2 Companion Loader (`_error.js`)
228
+ An optional companion `_error.js` can prefetch support data, report errors to monitoring services, or enrich the context:
229
+
230
+ ```javascript
231
+ // pages/dashboard/_error.js
232
+ module.exports = {
233
+ load: async ({ error, req, res, ctx, db }) => {
234
+ return {
235
+ supportEmail: 'support@example.com',
236
+ incidentId: req.id,
237
+ };
238
+ },
239
+ };
240
+ ```
241
+
242
+ ### 6.3 Template Context Variables
243
+ Inside `_error.njk`, the following variables are injected:
244
+ - `error`: `{ message, status, statusCode, code, name, details, fields, stack }` (stack is only exposed when `isDev === true`).
245
+ - `route`: `{ routePath, file }`.
246
+ - `req`: `{ path, query, params, method, url }`.
247
+ - `fsy`: Template helper suite.
248
+ - Plus any data returned by companion `_error.js` loader.
249
+
250
+ ### 6.4 Layout Opt-Out
251
+ To render a full-page custom error view without the parent layout, set `layout: false` in `_error.js` or frontmatter:
252
+ ```yaml
253
+ ---
254
+ layout: false
255
+ ---
256
+ <div class="standalone-error-screen">
257
+ <h1>{{ error.status }} - {{ error.message }}</h1>
258
+ </div>
259
+ ```
260
+
package/.agents/ORM.md CHANGED
@@ -311,4 +311,94 @@ const mixed = await articleRepo.query()
311
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
312
  | **MySQL** | `'fts'` | `MATCH(...) AGAINST(? IN BOOLEAN MODE)` | Requires FULLTEXT index on table. |
313
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
+
314
404
 
@@ -112,7 +112,44 @@ Zero-dependency B2B & Developer API Key Management Plugin with standard prefix f
112
112
  - `getMigrationTemplate(tableName)`: Knex migration script creating `api_keys` table.
113
113
  - `createApiKeyModel(tableName)`: Webspresso ORM model definition with hidden `key_hash`.
114
114
 
115
- ### 2.2 `adminPanelPlugin` (`plugins/admin-panel`)
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`)
116
153
  Mithril.js SPA Admin Panel mounted at `/_admin`.
117
154
  - **Options**: `{ path: '/_admin', db, title: 'Webspresso Admin' }`
118
155
  - **Features**: Auto CRUD screens for ORM models, user staff auth, custom page registration, widgets, field renderers.
@@ -131,14 +168,14 @@ Multipart file upload manager with local / cloud storage providers.
131
168
 
132
169
  ### 2.5 `fileManagerPlugin` (`plugins/file-manager`)
133
170
  Filesystem-first media library, directory explorer, and modal asset picker for Admin Panel.
134
- - **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 }`
135
172
  - **Features**:
136
173
  - Directory tree navigation (breadcrumbs), safe subfolder creation (`mkdir`), item rename, delete, and move.
137
174
  - Path traversal attack protection (`resolveSafePath`).
138
175
  - Grid (thumbnail gallery) & List (table) view modes.
139
176
  - Search by filename and type filter (Images, Documents, Videos, Audio, Archives, Code).
140
177
  - Media preview modal with copy URL and direct playback/preview.
141
- - "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.
142
179
  - Built-in services: `fileManager.list`, `fileManager.mkdir`, `fileManager.rename`, `fileManager.delete`, `fileManager.move`, `fileManager.upload`.
143
180
 
144
181
  ### 2.6 `corsPlugin` (`plugins/cors`)
@@ -256,19 +293,19 @@ Admin mutation tracking, record change timeline, and audit history with schedule
256
293
  - **Options**:
257
294
  - `db`: Database instance (must expose `.knex`).
258
295
  - `tableName`: Database table name (default `'audit_logs'`).
259
- - `registerRecordAction`: Register built-in "Zaman Tüneli" action on all admin models (default `true`).
296
+ - `registerRecordAction`: Register built-in "Timeline" action on all admin models (default `true`).
260
297
  - `retentionDays`: Days to retain audit trail entries (default `90`).
261
298
  - `cleanupIntervalMs`: Background cleanup timer frequency (default 24h).
262
299
  - `autoCleanup`: Enable scheduled background purge (default `true`).
263
300
  - `cleanupOnStart`: Run initial purge shortly after boot (default `true`).
264
301
  - `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)**.
302
+ - **Timeline UI Features**:
303
+ - **Dual Mode**: Switch seamlessly between **Timeline View** and **Table View**.
267
304
  - **Record Focus Mode**: Clicking any `Model #ID` tag instantly filters and displays that specific record's revision timeline.
268
305
  - **Visual Spine & Status Icons**: Color-coded action nodes for `create` (emerald +), `update` (blue ✏️), `delete` (rose 🗑️), and `restore` (amber ↺).
269
306
  - **Changed Fields Diff**: Clearly lists modified fields (e.g. `title`, `price`) on every update event.
270
307
  - **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.
308
+ - **Built-in Record Action**: One-click "Timeline" button on record lists and edit forms jumps straight to that record's audit history.
272
309
  - **API & Admin Panel Endpoints**:
273
310
  - `GET /_admin/api/audit-logs`: List paginated logs with filters (`model`, `recordId`, `action`, `search`, `from`, `to`).
274
311
  - `GET /_admin/api/audit-logs/timeline/:model/:id`: Dedicated chronological record change timeline with summary stats.
package/.agents/QUEUE.md CHANGED
@@ -162,27 +162,30 @@ Webspresso includes a native, zero-dependency task scheduler with a POSIX-compli
162
162
 
163
163
  ### 1. Cron Expression Syntax
164
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`
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
168
  - Lists: `1,15,30`, `MON,WED,FRI`
169
169
  - Descriptive aliases:
170
170
  - `@yearly`, `@annually` (`0 0 1 1 *`)
171
171
  - `@monthly` (`0 0 1 * *`)
172
172
  - `@weekly` (`0 0 * * 0`)
173
+ - `@weekdays`, `@workdays` (`0 0 * * 1-5`)
174
+ - `@weekends` (`0 0 * * 6,0`)
173
175
  - `@daily`, `@midnight` (`0 0 * * *`)
174
- - `@hourly` (`0 * * * *`)
176
+ - `@hourly`, `@every_hour` (`0 * * * *`)
175
177
  - `@every_minute`, `@minutely` (`* * * * *`)
176
178
  - `@every <N>m` (e.g. `@every 5m` → `*/5 * * * *`)
177
179
  - `@every <N>h` (e.g. `@every 2h` → `0 */2 * * *`)
180
+ - `@every <N>d` (e.g. `@every 2d` → `0 0 */2 * *`)
178
181
 
179
182
  ### 2. Programmatic Scheduling
180
183
  ```js
181
- // Schedule daily digest at 09:00 AM
184
+ // Schedule daily digest at 09:00 AM with Timezone support
182
185
  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
+ 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
186
189
  });
187
190
 
188
191
  // Or use the .cron() alias
@@ -190,9 +193,12 @@ queue.cron('@every 15m', 'metrics.collect');
190
193
 
191
194
  // Query schedules
192
195
  const schedules = queue.getSchedules();
193
- // [{ id, name, cronExpression, nextRunAt, lastRunAt, isRunning, runsCount, ... }]
196
+ // [{ id, name, cronExpression, nextRunAt, lastRunAt, isRunning, activeJobId, activeJobIds, runsCount, ... }]
194
197
 
195
- // Unschedule by schedule ID or job name
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)
196
202
  queue.unschedule('metrics.collect');
197
203
  ```
198
204
 
@@ -201,8 +207,10 @@ queue.unschedule('metrics.collect');
201
207
  | :--- | :--- | :--- | :--- |
202
208
  | `id` | `string` | `schedule:${job}:${cron}` | Custom unique identifier |
203
209
  | `overlap` | `boolean` | `false` | When `false`, skips tick if a previous job run is still processing |
210
+ | `preventOverlap` | `boolean` | `true` | Alias for `overlap: false` |
204
211
  | `maxRuns` | `number` | `null` | Automatically unschedules task after N runs |
205
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'`) |
206
214
  | `runImmediately` | `boolean` | `false` | Triggers a run right after scheduling |
207
215
 
208
216
  ### 4. File-Based Cron Discovery (`jobs/`)