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.
- package/.agents/ADMIN.md +11 -1
- package/.agents/AGENTS.md +1 -1
- package/.agents/AUTH.md +169 -0
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +73 -5
- package/.agents/QUEUE.md +84 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -0
- package/core/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +8 -0
- package/core/orm/model.js +4 -0
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- package/core/queue/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +82 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/index.d.ts +528 -0
- package/index.js +44 -0
- package/package.json +1 -1
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
- package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -70
- package/plugins/audit-log/api-handlers.js +114 -0
- package/plugins/audit-log/index.js +24 -0
- package/plugins/file-manager/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +6 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/queue/index.js +4 -0
- package/src/api/api-loader.js +1 -0
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +137 -100
- package/src/helpers.js +9 -0
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- 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
|
|
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
|
-
//
|
|
131
|
-
const
|
|
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
|
+
|
package/.agents/PLUGINS.md
CHANGED
|
@@ -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
|
+
|