webspresso 0.0.101 → 0.0.103
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/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +11 -0
- package/.agents/CONVENTIONS.md +4 -0
- package/.agents/ORM.md +13 -4
- package/.agents/PLUGINS.md +32 -6
- package/.agents/ROUTING.md +99 -2
- package/.agents/skills/webspresso-framework/SKILL.md +32 -0
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +4 -5
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
- package/AGENTS.md +6 -0
- package/README.md +20 -7
- package/bin/commands/add-tailwind.js +10 -0
- package/bin/commands/build.js +34 -0
- package/bin/commands/routes.js +8 -0
- package/core/applySchema.js +18 -1
- package/core/auth/jwt.js +10 -17
- package/core/auth/user-agent.js +61 -42
- package/core/compression/compression-stream.js +2 -2
- package/core/compression/middleware.js +1 -1
- package/core/compression/negotiate.js +11 -4
- package/core/events/dispatcher.js +56 -33
- package/core/openapi/orm-components.js +2 -3
- package/core/orm/cache/listeners.js +12 -2
- package/core/orm/json-fields.js +7 -1
- package/core/orm/model.js +99 -8
- package/core/orm/query-builder.js +71 -25
- package/core/orm/repository.js +42 -17
- package/core/orm/schema-helpers.js +49 -0
- package/core/orm/scopes.js +40 -9
- package/core/orm/types.js +12 -1
- package/core/queue/manager.js +5 -0
- package/core/validation/index.js +9 -3
- package/index.d.ts +178 -6
- package/index.js +71 -16
- package/package.json +6 -5
- package/plugins/admin-panel/index.js +12 -0
- package/plugins/audit-log/middleware.js +39 -4
- package/plugins/csrf/index.js +1 -1
- package/plugins/email/service.js +32 -1
- package/plugins/file-manager/api-handlers.js +10 -17
- package/plugins/file-manager/index.js +24 -9
- package/plugins/file-manager/security.js +18 -6
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +258 -0
- package/plugins/site-analytics/bot-patterns.js +3 -0
- package/plugins/site-analytics/client-error-handler.js +3 -1
- package/plugins/site-analytics/index.js +10 -1
- package/plugins/site-analytics/tracking.js +245 -46
- package/src/api/api-loader.js +18 -2
- package/src/api/validation-i18n.js +231 -0
- package/src/discovery/route-manifest.js +197 -0
- package/src/discovery/scan-routes.js +84 -8
- package/src/file-router.js +215 -97
- package/src/helpers.js +139 -26
- package/src/njk-frontmatter.js +17 -1
- package/src/plugin-manager.js +46 -13
- package/src/routing/route-table.js +15 -5
- package/src/server.js +86 -15
- package/src/services/builtins/system.js +4 -6
- package/src/services/registry.js +12 -2
- package/src/services/validator.js +57 -32
- package/src/template/precompile.js +247 -0
package/.agents/AGENTS.md
CHANGED
|
@@ -38,7 +38,7 @@ Every coding agent working on Webspresso **MUST** follow this 4-step execution d
|
|
|
38
38
|
This `.agents/` customization root is organized into modular topic guides:
|
|
39
39
|
|
|
40
40
|
1. **[File-Based Routing & API Routes](.agents/ROUTING.md)** — File-based page routes (`pages/`), method-suffixed API routes (`pages/api/*.get.js`, `*.post.js`), parameter brackets (`[id]`), Zod request schemas, and the strict prohibition of manual `routes/` directories or `app.get()`.
|
|
41
|
-
2. **[Architecture & Structure](.agents/ARCHITECTURE.md)** — File-based routing (`pages/`), Asset Management & Versioning (`AssetManager`, `fsy.asset`), ORM models & `zdb` schemas (`core/orm`), Mithril.js Admin Panel SPA (`plugins/admin-panel`), and Plugin Ecosystem.
|
|
41
|
+
2. **[Architecture & Structure](.agents/ARCHITECTURE.md)** — File-based routing (`pages/`), Cold Start & AOT Optimizations (`discoverAppRoutes`, `lazyRoutes: true`, `routes.manifest.json`, `templates.compiled.js`, `lazyPlugin`), Asset Management & Versioning (`AssetManager`, `fsy.asset`), ORM models & `zdb` schemas (`core/orm`), Mithril.js Admin Panel SPA (`plugins/admin-panel`), and Plugin Ecosystem.
|
|
42
42
|
3. **[Services Layer](.agents/SERVICES.md)** — File-based auto-discovery (`services/`), Zod validation, declarative `auth` & RBAC guards, automatic ACID transaction propagation (`transaction: true`), timeouts, and memoization.
|
|
43
43
|
4. **[Exceptions & Error Handling](.agents/ERRORS.md)** — Django-inspired framework exception hierarchy (`WebspressoError`, `HttpError`, `ValidationError`, `SecurityError`, `RequestAbortedError`), central error boundary, `app.setErrorHandler()`, and production error masking.
|
|
44
44
|
5. **[Graceful Shutdown & Lifecycle](.agents/SHUTDOWN.md)** — `ShutdownManager`, `NodeHttpAdapter`, connection draining, force close mode, and reverse-order plugin disposer cleanup.
|
package/.agents/ARCHITECTURE.md
CHANGED
|
@@ -23,6 +23,12 @@
|
|
|
23
23
|
- Discovered routes are compiled and sorted in deterministic priority order: Static $\to$ Deep Dynamic $\to$ Shallow Dynamic $\to$ Catch-All (`*`).
|
|
24
24
|
- Detects `METHOD + PATH` collision conflicts at startup and throws descriptive errors.
|
|
25
25
|
- Inspectable via `app.routes.list()` for CLI tools, documentation generation, and debugging.
|
|
26
|
+
- **High-Performance Architecture & Cold-Start Optimization**:
|
|
27
|
+
- **Single-Pass Unified Route Discovery (`discoverAppRoutes`)**: Discovers Page routes, API routes, and Route Groups in a single filesystem traversal pass, avoiding redundant tree walks during startup.
|
|
28
|
+
- **Dev-Mode On-Demand (JIT) Route Evaluation (`lazyRoutes: true`)**: In development (`NODE_ENV !== 'production'`), route handler modules (`pages/**/*.js`) are loaded on first incoming HTTP request instead of eager-evaluating all routes at boot, reducing dev boot time to ~15ms on large projects.
|
|
29
|
+
- **AOT Route Manifest (`webspresso build` & `routesManifest`)**: Production builds generate `.webspresso/routes.manifest.json`. When present (or configured via `createApp({ routesManifest: true })`), zero directory scanning or route sorting occurs at runtime.
|
|
30
|
+
- **AOT Nunjucks Template Precompilation (`precompileTemplates` & `precompiledTemplates`)**: `webspresso build` compiles all `.njk` templates in `pages/` and `views/` into `.webspresso/templates.compiled.js`. In production, templates execute without runtime parser/AST generation or disk reads.
|
|
31
|
+
- **On-Demand Lazy Plugins (`lazyPlugin`, `defineLazyPlugin`)**: Heavy plugins (e.g. `adminPanelPlugin({ lazy: true })`, `fileManagerPlugin({ lazy: true })`) mount Express proxy routers and hydrate in memory only when a matching HTTP request or service call arrives.
|
|
26
32
|
- **Templating & Helpers**: Nunjucks (`.njk`) templates rendered with layouts (e.g. `views/layout.njk`). Access helpers via `fsy` object in templates.
|
|
27
33
|
- **Asset Management & Cache-Busting (`assets`, `AssetManager`, `fsy`)**:
|
|
28
34
|
- Configured via `createApp({ assets: { version, manifestPath, prefix, publicDir } })` or `configureAssets()`.
|
|
@@ -115,6 +121,11 @@ Plugins expose `name`, `version`, `dependencies`, and lifecycle hooks (`register
|
|
|
115
121
|
| `restResourcePlugin` | ORM / REST API | Auto RESTful CRUD endpoints from models with eager loading & sanitization |
|
|
116
122
|
| `xlsxPlugin` | Excel / Export | Excel spreadsheet generation, parsing, ORM streaming export (`res.xlsx`), and services |
|
|
117
123
|
| `csvPlugin` | CSV / Export | RFC 4180 CSV generator, parser, Excel UTF-8 BOM, streaming writer (`res.csv`), and services |
|
|
124
|
+
| `lazyPlugin` | Core / Performance | On-demand plugin hydration wrapper deferring plugin setup until first request/call |
|
|
125
|
+
|
|
126
|
+
- **On-Demand Lazy Plugins (`lazyPlugin`, `defineLazyPlugin`)**:
|
|
127
|
+
- Wrap any plugin via `lazyPlugin(() => heavyPlugin(), { prefix: '/path', services: ['service.name'] })`.
|
|
128
|
+
- Built-in shorthand support on resource-heavy plugins: `adminPanelPlugin({ db, lazy: true })` and `fileManagerPlugin({ lazy: true })`.
|
|
118
129
|
|
|
119
130
|
---
|
|
120
131
|
|
package/.agents/CONVENTIONS.md
CHANGED
|
@@ -44,3 +44,7 @@
|
|
|
44
44
|
- **ALWAYS** use published `package.json` export paths: `require('webspresso/core/auth')`, `require('webspresso/core/orm')`, `require('webspresso/plugins/polar/src/webhooks')`.
|
|
45
45
|
- **NEVER** rely on deep relative paths from `node_modules/webspresso/...` or append `/index.js` unless documenting a pre-0.0.95 workaround.
|
|
46
46
|
- Smoke test: `tests/unit/package-exports.test.js`.
|
|
47
|
+
14. **Tailwind CSS v4 Standard (NO v3 PostCSS Boilerplate)**:
|
|
48
|
+
- **ALWAYS** prefer Tailwind CSS v4 using `npx webspresso add tailwind --v4` (powered by Oxide and `@tailwindcss/cli`).
|
|
49
|
+
- **ALWAYS** use CSS-first `@import "tailwindcss";` in `src/input.css` and compile to `public/css/style.css`.
|
|
50
|
+
- **NEVER** create redundant `tailwind.config.js` or `postcss.config.js` files unless custom legacy PostCSS plugins are specifically required.
|
package/.agents/ORM.md
CHANGED
|
@@ -51,7 +51,9 @@ import { defineModel, zdb } from 'webspresso';
|
|
|
51
51
|
export default defineModel({
|
|
52
52
|
name: 'Product',
|
|
53
53
|
table: 'products',
|
|
54
|
-
|
|
54
|
+
timestamps: true, // auto-injects createdAt & updatedAt into schema & scopes
|
|
55
|
+
softDelete: true, // auto-injects deletedAt & manages soft-delete scope
|
|
56
|
+
schema: {
|
|
55
57
|
id: zdb.id(),
|
|
56
58
|
name: zdb.string().min(2),
|
|
57
59
|
slug: zdb.string().unique(),
|
|
@@ -61,9 +63,7 @@ export default defineModel({
|
|
|
61
63
|
metadata: zdb.json({ optional: true }),
|
|
62
64
|
category_id: zdb.integer(),
|
|
63
65
|
active: zdb.boolean({ default: true }),
|
|
64
|
-
|
|
65
|
-
updated_at: zdb.timestamp({ auto: 'update' }),
|
|
66
|
-
}),
|
|
66
|
+
},
|
|
67
67
|
relations: {
|
|
68
68
|
category: { type: 'belongsTo', model: 'Category', foreignKey: 'category_id' },
|
|
69
69
|
reviews: { type: 'hasMany', model: 'Review', foreignKey: 'product_id' },
|
|
@@ -81,6 +81,13 @@ export default defineModel({
|
|
|
81
81
|
});
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
### Automatic Timestamps & Soft Deletes (`timestamps`, `softDelete`)
|
|
85
|
+
Instead of manually defining boilerplate timestamp and soft delete columns in `schema`, configure them declaratively:
|
|
86
|
+
- **Top-level / Options:** `timestamps: true` (or `options: { timestamps: true }`) automatically injects `createdAt` and `updatedAt` into the Zod schema, validation rules, column map, and Knex migrations.
|
|
87
|
+
- **Soft Deletes:** `softDelete: true` (or `softDeletes: true`) automatically injects `deletedAt` (nullable/optional) into the schema and activates the repository `whereNull('deletedAt')` soft-delete scope.
|
|
88
|
+
- **Custom column names & snake_case:** `timestamps: { snakeCase: true }` or `timestamps: { createdAt: 'created_at', updatedAt: 'updated_at' }`, `softDelete: { deletedAt: 'deleted_at' }`.
|
|
89
|
+
- **Explicit Schema Spread:** `...zdb.timestamps()` and `...zdb.softDeletes()` are also supported for explicit schema definitions.
|
|
90
|
+
|
|
84
91
|
### Supported `zdb` Data Types
|
|
85
92
|
- `zdb.id()` — Primary key (BigInt / Auto-increment)
|
|
86
93
|
- `zdb.string({ max, unique, optional, sortable })` — String / VarChar
|
|
@@ -90,6 +97,8 @@ export default defineModel({
|
|
|
90
97
|
- `zdb.file()` — File asset URL / upload path (non-sortable by default)
|
|
91
98
|
- `zdb.json()` — Serialized JSON column (non-sortable by default)
|
|
92
99
|
- `zdb.timestamp({ auto, sortable })` — Timestamp (`auto: 'create'` / `auto: 'update'`)
|
|
100
|
+
- `zdb.timestamps({ snakeCase })` — Timestamp pair (`createdAt`, `updatedAt`)
|
|
101
|
+
- `zdb.softDelete()`, `zdb.softDeletes({ snakeCase })` — Soft delete timestamp (`deletedAt`)
|
|
93
102
|
|
|
94
103
|
*Note: All `zdb` types support chainable `.config({ sortable: boolean })` or options object `{ sortable: boolean }`.*
|
|
95
104
|
|
package/.agents/PLUGINS.md
CHANGED
|
@@ -151,8 +151,9 @@ Outgoing Webhook Dispatcher and Management Plugin with zero external dependencie
|
|
|
151
151
|
|
|
152
152
|
### 2.4 `adminPanelPlugin` (`plugins/admin-panel`)
|
|
153
153
|
Mithril.js SPA Admin Panel mounted at `/_admin`.
|
|
154
|
-
- **Options**: `{ path: '/_admin', db, title: 'Webspresso Admin' }`
|
|
154
|
+
- **Options**: `{ path: '/_admin', db, title: 'Webspresso Admin', lazy: false }`
|
|
155
155
|
- **Features**: Auto CRUD screens for ORM models, user staff auth, custom page registration, widgets, field renderers.
|
|
156
|
+
- **On-Demand Hydration (`lazy: true`)**: When set to `true`, the admin panel plugin wraps itself with `lazyPlugin()`, deferring route mounting and internal setup until the first HTTP request hits `/_admin`.
|
|
156
157
|
|
|
157
158
|
### 2.3 `contentPlugin` (`plugins/content`)
|
|
158
159
|
Schema-driven headless CMS & inline content editing.
|
|
@@ -168,11 +169,12 @@ Multipart file upload manager with local / cloud storage providers.
|
|
|
168
169
|
|
|
169
170
|
### 2.5 `fileManagerPlugin` (`plugins/file-manager`)
|
|
170
171
|
Filesystem-first media library, directory explorer, and modal asset picker for Admin Panel.
|
|
171
|
-
- **Options**: `{ baseDir: './public/uploads', publicBasePath: '/uploads', apiPrefix: '/files', pageTitle: 'File Manager', maxUploadBytes: 10485760, allowedExtensions, allowedMimeTypes, includePage: true }`
|
|
172
|
+
- **Options**: `{ baseDir: './public/uploads', publicBasePath: '/uploads', apiPrefix: '/files', pageTitle: 'File Manager', maxUploadBytes: 10485760, allowedExtensions, allowedMimeTypes, includePage: true, lazy: false }`
|
|
172
173
|
- **Features**:
|
|
173
174
|
- Directory tree navigation (breadcrumbs), safe subfolder creation (`mkdir`), item rename, delete, and move.
|
|
174
175
|
- Path traversal attack protection (`resolveSafePath`).
|
|
175
176
|
- Grid (thumbnail gallery) & List (table) view modes.
|
|
177
|
+
- On-demand hydration via `lazy: true`.
|
|
176
178
|
- Search by filename and type filter (Images, Documents, Videos, Audio, Archives, Code).
|
|
177
179
|
- Media preview modal with copy URL and direct playback/preview.
|
|
178
180
|
- "Choose from Media Library" modal picker integrated with `FileUploadField` and `zdb.file()` form fields.
|
|
@@ -662,7 +664,31 @@ Zero-dependency OAuth 2.0 & Social Login plugin supporting Google, GitHub, and c
|
|
|
662
664
|
- `fsy.oauthUrl('google')` ➔ `"/auth/google"`
|
|
663
665
|
- `fsy.oauthUrl('github', { returnTo: '/dashboard' })` ➔ `"/auth/github?returnTo=%2Fdashboard"`
|
|
664
666
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
667
|
+
### 2.20 `lazyPlugin` / `defineLazyPlugin` (`plugins/lazy`)
|
|
668
|
+
Zero-dependency on-demand plugin hydration wrapper that defers heavy plugin initialization, route mounting, and service registration until the first matching HTTP request or service invocation.
|
|
669
|
+
- **Why**: Drastically slashes application cold start time and peak memory footprint when mounting heavy modules (such as admin panels, media libraries, documentation portals, or internal diagnostic tools) that are not needed on every server startup.
|
|
670
|
+
- **Usage**:
|
|
671
|
+
```javascript
|
|
672
|
+
const { lazyPlugin, adminPanelPlugin, fileManagerPlugin } = require('webspresso/plugins');
|
|
673
|
+
|
|
674
|
+
// Option 1: Using built-in lazy option
|
|
675
|
+
adminPanelPlugin({ db, lazy: true });
|
|
676
|
+
fileManagerPlugin({ lazy: true });
|
|
677
|
+
|
|
678
|
+
// Option 2: Wrapping custom or 3rd-party plugins
|
|
679
|
+
const heavyPlugin = lazyPlugin(
|
|
680
|
+
() => require('./my-heavy-plugin')({ config: 123 }),
|
|
681
|
+
{
|
|
682
|
+
name: 'my-heavy-plugin',
|
|
683
|
+
prefix: '/heavy', // Hydrates when request path starts with /heavy
|
|
684
|
+
services: ['heavy.process'], // Hydrates when ctx.service('heavy.process') is called
|
|
685
|
+
helpers: ['heavyHelper'], // Hydrates when fsy.heavyHelper() is evaluated
|
|
686
|
+
events: ['heavy.*'], // Hydrates when matching event is dispatched
|
|
687
|
+
onHydrate: (plugin) => console.log('Plugin hydrated!'),
|
|
688
|
+
}
|
|
689
|
+
);
|
|
690
|
+
```
|
|
691
|
+
- **How It Works**:
|
|
692
|
+
- Mounts a lightweight Express router proxy at `prefix`.
|
|
693
|
+
- Stubs placeholder services into `ServiceRegistry` and proxy helpers into Nunjucks context.
|
|
694
|
+
- Intercepts incoming requests/calls, executes the plugin factory function, runs `register(ctx)` and `onRoutesReady(ctx)`, replaces proxies seamlessly with the real router/services, and forwards the initial call with zero dropped requests.
|
package/.agents/ROUTING.md
CHANGED
|
@@ -119,6 +119,73 @@ module.exports = {
|
|
|
119
119
|
> [!IMPORTANT]
|
|
120
120
|
> **NEVER use raw Knex queries** like `req.db('notes')` in route handlers. **ALWAYS use repositories**: `req.db.getRepository('Note')`. This guarantees schema validation, lifecycle hooks, soft deletes, and query caching are respected.
|
|
121
121
|
|
|
122
|
+
### 2.1 Smart API Validation i18n & Error Translation
|
|
123
|
+
|
|
124
|
+
Webspresso features built-in **Smart Dual-Layer Auto-Translation** for Zod schema validation errors with **zero runtime overhead on valid requests**.
|
|
125
|
+
|
|
126
|
+
#### How It Works:
|
|
127
|
+
1. **Explicit Custom Keys**: Define dictionary translation keys directly inside Zod schemas (e.g. `z.string({ message: 'auth.email_required' })` or `z.string().min(5, 'auth.password_min')`).
|
|
128
|
+
2. **Dynamic Fallbacks**: If no explicit key is matched, the engine falls back to convention-based candidate keys:
|
|
129
|
+
- `validation.fields.<field>.<code/type>` (e.g., `validation.fields.email.invalid_string`)
|
|
130
|
+
- `validation.<code/type>` (e.g., `validation.too_small`, `validation.required`, `validation.invalid_string.email`)
|
|
131
|
+
- `zod.<code/type>`
|
|
132
|
+
3. **Parameter Interpolation**: Dynamic schema parameters (e.g. `{{ min }}`, `{{ max }}`, `{{ expected }}`, `{{ received }}`) are automatically interpolated into translated messages or inline custom template strings.
|
|
133
|
+
4. **Context Injection**: `apiCtx` provides `{ locale, t, service, db, sse }` to route handlers for immediate localized responses.
|
|
134
|
+
|
|
135
|
+
#### Example Usage:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
// pages/locales/tr.json
|
|
139
|
+
{
|
|
140
|
+
"auth": {
|
|
141
|
+
"email_required": "Lütfen geçerli bir e-posta adresi giriniz"
|
|
142
|
+
},
|
|
143
|
+
"validation": {
|
|
144
|
+
"required": "Bu alan zorunludur",
|
|
145
|
+
"too_small": "En az {{ min }} karakter olmalıdır"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
// pages/api/auth/register.post.js
|
|
152
|
+
'use strict';
|
|
153
|
+
const { defineApi } = require('webspresso');
|
|
154
|
+
|
|
155
|
+
module.exports = defineApi({
|
|
156
|
+
schema: ({ z }) => ({
|
|
157
|
+
body: z.object({
|
|
158
|
+
// Explicit dictionary translation key
|
|
159
|
+
email: z.string({ message: 'auth.email_required' }).email(),
|
|
160
|
+
// Automatic fallback to validation.too_small with {{ min }} interpolation
|
|
161
|
+
password: z.string().min(8),
|
|
162
|
+
}),
|
|
163
|
+
}),
|
|
164
|
+
handler: async (req, res, { t, locale }) => {
|
|
165
|
+
// req.locale and t() are injected into handler context
|
|
166
|
+
return res.status(201).json({
|
|
167
|
+
success: true,
|
|
168
|
+
message: t('auth.registered_success', { email: req.input.body.email }),
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Client requests sending `Accept-Language: tr` or query param `?lang=tr` will automatically receive fully localized validation errors in the 400 Bad Request response:
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"error": "Validation failed",
|
|
178
|
+
"issues": [
|
|
179
|
+
{
|
|
180
|
+
"field": "password",
|
|
181
|
+
"code": "too_small",
|
|
182
|
+
"key": "validation.too_small",
|
|
183
|
+
"message": "En az 8 karakter olmalıdır"
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
122
189
|
---
|
|
123
190
|
|
|
124
191
|
## 3. Dynamic Route Parameters (`[param]`)
|
|
@@ -326,7 +393,37 @@ const routeList = app.routes.list();
|
|
|
326
393
|
// ]
|
|
327
394
|
```
|
|
328
395
|
|
|
329
|
-
### 6.5
|
|
396
|
+
### 6.5 Cold Start & High-Performance Routing Architecture
|
|
397
|
+
|
|
398
|
+
Webspresso incorporates multi-tier optimizations to ensure sub-millisecond cold starts in production and instant dev-server boot times:
|
|
399
|
+
|
|
400
|
+
#### 1. Single-Pass Unified Route Discovery (`discoverAppRoutes`)
|
|
401
|
+
Instead of performing multiple independent directory traversals, `discoverAppRoutes(rootDir, options)` discovers Page routes, API routes, and Route Groups in a single traversal pass:
|
|
402
|
+
```javascript
|
|
403
|
+
const { discoverAppRoutes } = require('webspresso');
|
|
404
|
+
|
|
405
|
+
const { routeTable, pageRoutes, apiRoutes } = discoverAppRoutes(__dirname);
|
|
406
|
+
```
|
|
407
|
+
Passing `discoveredRoutes` to `createApp({ discoveredRoutes })` skips internal discovery completely.
|
|
408
|
+
|
|
409
|
+
#### 2. Dev-Mode On-Demand (JIT) Route Evaluation (`lazyRoutes: true`)
|
|
410
|
+
In development (`NODE_ENV !== 'production'`), `lazyRoutes: true` is enabled by default:
|
|
411
|
+
- Route paths and HTTP methods are registered immediately with lightweight proxy handlers.
|
|
412
|
+
- Route handler modules (`pages/**/*.js` / `pages/api/**/*.js`) are required and evaluated **on-demand on the first incoming HTTP request**.
|
|
413
|
+
- Result: Dev server boots in ~15ms even in applications with hundreds of routes.
|
|
414
|
+
- Can be explicitly controlled via `createApp({ lazyRoutes: false })` to force eager evaluation.
|
|
415
|
+
|
|
416
|
+
#### 3. Production Ahead-of-Time (AOT) Route Manifest (`routes.manifest.json`)
|
|
417
|
+
For maximum production performance, compile your route table during the build step:
|
|
418
|
+
```bash
|
|
419
|
+
npx webspresso build
|
|
420
|
+
```
|
|
421
|
+
This generates `.webspresso/routes.manifest.json`. When `createApp({ routesManifest: true })` is used (or when `.webspresso/routes.manifest.json` exists in `NODE_ENV === 'production'`), Webspresso:
|
|
422
|
+
- Bypasses filesystem directory scanning (`pages/`) entirely.
|
|
423
|
+
- Bypasses regex path compilation and route sorting at boot.
|
|
424
|
+
- Instantly binds pre-sorted routes from the manifest.
|
|
425
|
+
|
|
426
|
+
### 6.6 Server-Sent Events (SSE) Streaming (`res.sse`)
|
|
330
427
|
For real-time event streaming and LLM token outputs, use the native `res.sse()` helper (or `{ sse }` from `ctx`):
|
|
331
428
|
|
|
332
429
|
```javascript
|
|
@@ -352,7 +449,7 @@ module.exports = defineApi({
|
|
|
352
449
|
});
|
|
353
450
|
```
|
|
354
451
|
|
|
355
|
-
### 6.
|
|
452
|
+
### 6.7 Cascading Middleware (`_middleware.js`) & Nested Layouts (`_layout.njk`)
|
|
356
453
|
|
|
357
454
|
Webspresso supports folder-level cascading middleware inheritance and nested layouts across directory trees:
|
|
358
455
|
|
|
@@ -42,11 +42,43 @@ webspresso start # Start production server
|
|
|
42
42
|
webspresso doctor # Run system sanity checks
|
|
43
43
|
webspresso db:migrate # Run pending Knex migrations
|
|
44
44
|
webspresso db:seed # Run database seeders
|
|
45
|
+
webspresso add:tailwind # Add Tailwind CSS v4 (Oxide engine, zero-config)
|
|
45
46
|
webspresso agents:init # Scaffold AI Agent rules and skills
|
|
46
47
|
```
|
|
47
48
|
|
|
48
49
|
---
|
|
49
50
|
|
|
51
|
+
## 🎨 Tailwind CSS v4 Integration
|
|
52
|
+
|
|
53
|
+
Webspresso natively supports **Tailwind CSS v4** (Oxide engine, LightningCSS, zero-config CSS-first architecture).
|
|
54
|
+
|
|
55
|
+
### Setup & Commands
|
|
56
|
+
```bash
|
|
57
|
+
# Add Tailwind CSS v4 (recommended, zero-config CSS-first)
|
|
58
|
+
npx webspresso add tailwind --v4
|
|
59
|
+
|
|
60
|
+
# Or upgrade an existing Tailwind v3 project to v4
|
|
61
|
+
npx webspresso add tailwind --upgrade
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Architecture & Conventions
|
|
65
|
+
- **CSS-First Directives**: `src/input.css` uses modern v4 `@import`:
|
|
66
|
+
```css
|
|
67
|
+
@import "tailwindcss";
|
|
68
|
+
```
|
|
69
|
+
- **Zero Configuration**: No `tailwind.config.js`, `postcss.config.js`, or `autoprefixer` required.
|
|
70
|
+
- **Compiled Output**: Compiles into `public/css/style.css` via `@tailwindcss/cli`.
|
|
71
|
+
- **Scripts in `package.json`**:
|
|
72
|
+
- `"build:css"`: `@tailwindcss/cli -i ./src/input.css -o ./public/css/style.css --minify`
|
|
73
|
+
- `"watch:css"`: `@tailwindcss/cli -i ./src/input.css -o ./public/css/style.css --watch`
|
|
74
|
+
- `"dev"`: Automatically runs CSS watcher alongside the server: `npm run watch:css & node --watch server.js`
|
|
75
|
+
- **Template Integration**: Link stylesheet in Nunjucks layouts (`views/layout.njk`):
|
|
76
|
+
```nunjucks
|
|
77
|
+
<link rel="stylesheet" href="{{ fsy.asset('/css/style.css') }}">
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
50
82
|
## 🏗️ Core Architecture Overview
|
|
51
83
|
|
|
52
84
|
- **`pages/` & `pages/api/`**: File-based SSR templates (`.njk`), companion data loaders (`.js`), and method-suffixed API endpoints (`.get.js`, `.post.js`).
|
|
@@ -21,8 +21,10 @@ module.exports = defineModel({
|
|
|
21
21
|
name: 'Product',
|
|
22
22
|
table: 'products',
|
|
23
23
|
primaryKey: 'id',
|
|
24
|
+
timestamps: true, // auto-injects createdAt & updatedAt into schema & scopes
|
|
25
|
+
softDelete: true, // auto-injects deletedAt & enables soft-delete scope
|
|
24
26
|
|
|
25
|
-
schema:
|
|
27
|
+
schema: {
|
|
26
28
|
id: zdb.id(),
|
|
27
29
|
name: zdb.string().min(1).max(255),
|
|
28
30
|
slug: zdb.string().unique(),
|
|
@@ -30,10 +32,7 @@ module.exports = defineModel({
|
|
|
30
32
|
in_stock: zdb.boolean().default(true),
|
|
31
33
|
metadata: zdb.json().optional(),
|
|
32
34
|
user_id: zdb.foreignKey('users.id'),
|
|
33
|
-
|
|
34
|
-
updated_at: zdb.timestamp().autoUpdate(),
|
|
35
|
-
deleted_at: zdb.timestamp().nullable(),
|
|
36
|
-
}),
|
|
35
|
+
},
|
|
37
36
|
|
|
38
37
|
relations: {
|
|
39
38
|
user: {
|
|
@@ -85,10 +85,10 @@ module.exports = definePage({
|
|
|
85
85
|
{% extends "layout.njk" %}
|
|
86
86
|
|
|
87
87
|
{% block content %}
|
|
88
|
-
<div class="
|
|
89
|
-
<h1>{{ product.name }}</h1>
|
|
90
|
-
<p class="
|
|
91
|
-
<p>{{ product.description }}</p>
|
|
88
|
+
<div class="max-w-4xl mx-auto p-6 bg-white dark:bg-gray-900 rounded-xl shadow-sm">
|
|
89
|
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{{ product.name }}</h1>
|
|
90
|
+
<p class="text-lg font-semibold text-emerald-600 mt-2">{{ fsy.formatCurrency(product.price) }}</p>
|
|
91
|
+
<p class="text-gray-600 dark:text-gray-300 mt-4 leading-relaxed">{{ product.description }}</p>
|
|
92
92
|
</div>
|
|
93
93
|
{% endblock %}
|
|
94
94
|
```
|
package/AGENTS.md
CHANGED
|
@@ -58,6 +58,12 @@ Every coding agent working on Webspresso **MUST** follow this 4-step execution d
|
|
|
58
58
|
- **Graceful Shutdown & Lifecycle Management**: Configured via `createApp({ server: { shutdown: { enabled: true, mode: 'graceful'|'force', timeout: 10000 } } })`. Socket connection tracking, request draining with `Connection: close`, and reverse-order plugin disposer cleanup.
|
|
59
59
|
- **HTTP Response Compression**: Zero-dependency streaming zlib compression configured via `createApp({ server: { compression: true } })` with Brotli (`br`), Gzip (`gzip`), Deflate (`deflate`), threshold buffering (1 KB), and route-level opt-out (`res.compress(false)`).
|
|
60
60
|
- **Custom Error & 404 Pages**: Configured via `createApp({ errorPages: { notFound, serverError, timeout } })`. Supports template paths (e.g. `notFound: '404.njk'`) or custom handler functions, passing `{ fsy, locale, isDev, url, method }` to templates.
|
|
61
|
+
- **Cold Start & High-Performance Optimizations**:
|
|
62
|
+
- Unified single-pass route scanner (`discoverAppRoutes`).
|
|
63
|
+
- Dev-mode on-demand (JIT) route evaluation (`lazyRoutes: true`, dev boot ~15ms).
|
|
64
|
+
- Ahead-of-time (AOT) route table manifest (`webspresso build` -> `.webspresso/routes.manifest.json`).
|
|
65
|
+
- Ahead-of-time (AOT) Nunjucks template precompilation (`webspresso build` -> `.webspresso/templates.compiled.js`).
|
|
66
|
+
- On-demand lazy plugin hydration (`lazyPlugin`, `adminPanelPlugin({ lazy: true })`, `fileManagerPlugin({ lazy: true })`).
|
|
61
67
|
- **Middleware & Hooks**: Route lifecycle hooks can be declared per-page or globally via `pages/_hooks.js`.
|
|
62
68
|
- **API Endpoints & Routing Architecture**:
|
|
63
69
|
- ⚠️ **CRITICAL ANTI-PATTERN: NEVER create a `routes/` or `src/routes/` directory. NEVER use `express.Router()`, `app.get()`, or `app.post()` for application endpoints.**
|
package/README.md
CHANGED
|
@@ -31,11 +31,12 @@ Open **`http://localhost:3000`** in your browser.
|
|
|
31
31
|
| Layer | Capabilities |
|
|
32
32
|
| :--- | :--- |
|
|
33
33
|
| **Routing & SSR** | 100% file-based routing (`pages/`), method suffixes (`.get.js`, `.post.js`), nested layouts (`_layout.njk`), directory-level error boundaries (`_error.njk`), loading skeletons (`_loading.njk`), and out-of-order chunked response streaming (`res.renderStream`). |
|
|
34
|
+
| **Performance & Cold Start** | Single-pass unified route discovery, dev mode JIT on-demand route evaluation, production AOT route manifest (`webspresso build`), AOT Nunjucks template precompilation (zero AST parsing at runtime), and on-demand lazy plugin mounting. |
|
|
34
35
|
| **Database & ORM** | Built-in Knex ORM with `defineModel`, `zdb` schema builder, Repository pattern (`db.getRepository('Model')`), relations (`belongsTo`, `hasMany`), cascade soft-delete & restore, and ambient transactions via `AsyncLocalStorage`. |
|
|
35
36
|
| **Dual Auth & Sessions** | Stateful cookies for SSR + stateless HS256 JWT rotation for APIs, magic links, active device management & remote session revocation, CSRF protection, and zero external auth dependencies. |
|
|
36
37
|
| **Services & Queues** | File-based auto-discovery in `services/` with declarative Zod validation, role-based access guards, and ACID transaction propagation. Background worker queues in `jobs/`. |
|
|
37
|
-
| **Admin Panel SPA** | Integrated Mithril.js single-page admin panel mounted at `/_admin` with auto-layout wrapping, CRUD views, custom dashboard widgets, and custom field renderers. |
|
|
38
|
-
| **Plugin Ecosystem** | 20+ official plugins: Polar.sh billing, Webhooks dispatcher with HMAC signatures, Realtime (WebSocket/SSE), Site Analytics, API Keys, File Manager, CSV/Excel generation, and more. |
|
|
38
|
+
| **Admin Panel SPA** | Integrated Mithril.js single-page admin panel mounted at `/_admin` with auto-layout wrapping, CRUD views, custom dashboard widgets, and custom field renderers. Supports on-demand lazy mounting (`adminPanelPlugin({ lazy: true })`). |
|
|
39
|
+
| **Plugin Ecosystem** | 20+ official plugins: Polar.sh billing, Webhooks dispatcher with HMAC signatures, Realtime (WebSocket/SSE), Site Analytics, API Keys, File Manager, CSV/Excel generation, On-demand Lazy Plugins (`lazyPlugin`), and more. |
|
|
39
40
|
| **DevOps & Production** | Built-in streaming zlib compression (`brotli`/`gzip`), socket connection draining & graceful shutdown, `webspresso deploy:check` production readiness auditor, and Docker scaffolding. |
|
|
40
41
|
|
|
41
42
|
---
|
|
@@ -114,13 +115,13 @@ const { defineModel, zdb } = require('webspresso');
|
|
|
114
115
|
module.exports = defineModel({
|
|
115
116
|
name: 'Note',
|
|
116
117
|
table: 'notes',
|
|
118
|
+
timestamps: true, // auto-injects createdAt & updatedAt into schema & scopes
|
|
119
|
+
softDelete: true, // auto-injects deletedAt & enables soft-delete scope
|
|
117
120
|
schema: {
|
|
118
121
|
id: zdb.id(),
|
|
119
122
|
title: zdb.string(200),
|
|
120
123
|
content: zdb.text(),
|
|
121
124
|
userId: zdb.integer(),
|
|
122
|
-
...zdb.timestamps(),
|
|
123
|
-
...zdb.softDeletes(),
|
|
124
125
|
},
|
|
125
126
|
relations: {
|
|
126
127
|
user: { type: 'belongsTo', model: 'User', foreignKey: 'userId' },
|
|
@@ -154,13 +155,25 @@ app.listen(PORT, () => {
|
|
|
154
155
|
|
|
155
156
|
---
|
|
156
157
|
|
|
158
|
+
## ⚡ High-Performance Architecture (Cold Start & AOT)
|
|
159
|
+
|
|
160
|
+
Webspresso is designed for blazing-fast cold starts and minimal resource consumption in both development and production:
|
|
161
|
+
|
|
162
|
+
1. **Unified Single-Pass Route Discovery (`discoverAppRoutes`)**: Traverses the filesystem only once at startup, building the `RouteTable` and companion loaders in a single pass (saving 30-40% mount time).
|
|
163
|
+
2. **Dev-Mode On-Demand (JIT) Route Evaluation**: In development, API routes and SSR companion `.js` configs are resolved on-demand when first requested. The dev server boots in **<100ms** regardless of project size.
|
|
164
|
+
3. **Production AOT Route Manifest (`webspresso build`)**: The build step generates `.webspresso/routes.manifest.json`. In production, the server mounts pre-indexed routes with **zero filesystem directory scans**.
|
|
165
|
+
4. **Nunjucks Template Precompilation (AOT / Zero-AST Parse)**: `webspresso build` precompiles `.njk` templates into JavaScript render functions (`.webspresso/templates.compiled.js`). Eliminates runtime disk I/O and template parsing overhead for ultra-low TTFB.
|
|
166
|
+
5. **On-Demand Lazy Plugins (`lazyPlugin`)**: Heavy plugins (e.g. `adminPanelPlugin({ lazy: true })`, `fileManagerPlugin({ lazy: true })`) defer initialization, session setup, and route mounting until first access to their URL prefix or service invocation.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
157
170
|
## 🧰 Essential CLI Commands
|
|
158
171
|
|
|
159
172
|
```bash
|
|
160
173
|
# Development & Builds
|
|
161
|
-
npx webspresso dev # Start dev server with watch & live reload
|
|
162
|
-
npx webspresso build # Compile production assets
|
|
163
|
-
npx webspresso start # Run production server
|
|
174
|
+
npx webspresso dev # Start dev server with watch & JIT live reload
|
|
175
|
+
npx webspresso build # Compile production assets, AOT route manifest & precompiled templates
|
|
176
|
+
npx webspresso start # Run production server with zero-FS discovery
|
|
164
177
|
|
|
165
178
|
# Database & Migrations
|
|
166
179
|
npx webspresso db:migrate # Run pending database migrations
|
|
@@ -171,6 +171,16 @@ module.exports = {
|
|
|
171
171
|
// Create placeholder CSS
|
|
172
172
|
fs.writeFileSync('public/css/style.css', '/* Run npm run build:css */\n');
|
|
173
173
|
console.log('✅ Created public/css/style.css');
|
|
174
|
+
|
|
175
|
+
// Ensure public/css/style.css is in .gitignore
|
|
176
|
+
if (fs.existsSync('.gitignore')) {
|
|
177
|
+
let gitignore = fs.readFileSync('.gitignore', 'utf-8');
|
|
178
|
+
if (!gitignore.includes('public/css/style.css')) {
|
|
179
|
+
gitignore = gitignore.trimEnd() + '\npublic/css/style.css\n';
|
|
180
|
+
fs.writeFileSync('.gitignore', gitignore);
|
|
181
|
+
console.log('✅ Added public/css/style.css to .gitignore');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
174
184
|
|
|
175
185
|
// Try to build CSS if tailwindcss / @tailwindcss/cli is already installed
|
|
176
186
|
const tailwindBin = path.join(process.cwd(), 'node_modules', '.bin', 'tailwindcss');
|
package/bin/commands/build.js
CHANGED
|
@@ -15,6 +15,8 @@ function registerCommand(program) {
|
|
|
15
15
|
.description('Build production assets (Tailwind CSS and esbuild JS with manifest.json)')
|
|
16
16
|
.option('--no-css', 'Skip CSS build')
|
|
17
17
|
.option('--no-js', 'Skip JS bundle build')
|
|
18
|
+
.option('--no-routes', 'Skip generating AOT route manifest')
|
|
19
|
+
.option('--no-templates', 'Skip precompiling Nunjucks templates')
|
|
18
20
|
.option('--no-hash', 'Disable content hashing in asset filenames')
|
|
19
21
|
.action(async (options = {}) => {
|
|
20
22
|
const cwd = process.cwd();
|
|
@@ -33,6 +35,8 @@ function registerCommand(program) {
|
|
|
33
35
|
|
|
34
36
|
let cssBuilt = false;
|
|
35
37
|
let jsBuilt = false;
|
|
38
|
+
let routesBuilt = false;
|
|
39
|
+
let routeCount = 0;
|
|
36
40
|
|
|
37
41
|
// 1. Build CSS (Tailwind)
|
|
38
42
|
if (options.css !== false) {
|
|
@@ -90,9 +94,39 @@ function registerCommand(program) {
|
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
// 3. Build AOT Route Manifest (.webspresso/routes.manifest.json)
|
|
98
|
+
if (options.routes !== false) {
|
|
99
|
+
try {
|
|
100
|
+
const { generateRouteManifest } = require('../../src/discovery/route-manifest');
|
|
101
|
+
const result = generateRouteManifest({ rootDir: cwd });
|
|
102
|
+
routesBuilt = true;
|
|
103
|
+
routeCount = result.routeCount;
|
|
104
|
+
console.log(`🗺️ AOT route manifest generated (${result.routeCount} routes) at: ${path.relative(cwd, result.manifestPath)}`);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error('❌ Route manifest generation failed:', err.message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 4. Precompile Nunjucks Templates (.webspresso/templates.compiled.js)
|
|
111
|
+
let templatesBuilt = false;
|
|
112
|
+
let templateCount = 0;
|
|
113
|
+
if (options.templates !== false) {
|
|
114
|
+
try {
|
|
115
|
+
const { precompileTemplates } = require('../../src/template/precompile');
|
|
116
|
+
const result = precompileTemplates({ rootDir: cwd });
|
|
117
|
+
templatesBuilt = true;
|
|
118
|
+
templateCount = result.templateCount;
|
|
119
|
+
console.log(`📑 Precompiled Nunjucks templates (${result.templateCount} templates) at: ${path.relative(cwd, result.manifestPath)}`);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error('❌ Template precompilation failed:', err.message);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
93
125
|
console.log('\n✨ Asset build complete!\n');
|
|
94
126
|
if (cssBuilt) console.log(' ✓ CSS: public/css/style.css');
|
|
95
127
|
if (jsBuilt) console.log(' ✓ JS: public/dist/ & manifest.json');
|
|
128
|
+
if (routesBuilt) console.log(` ✓ Routes: .webspresso/routes.manifest.json (${routeCount} routes)`);
|
|
129
|
+
if (templatesBuilt) console.log(` ✓ Templates: .webspresso/templates.compiled.js (${templateCount} templates)`);
|
|
96
130
|
});
|
|
97
131
|
}
|
|
98
132
|
|
package/bin/commands/routes.js
CHANGED
|
@@ -19,10 +19,18 @@ function registerCommand(program) {
|
|
|
19
19
|
.option('-a, --api', 'Filter to only API endpoints')
|
|
20
20
|
.option('-p, --pages', 'Filter to only SSR page routes')
|
|
21
21
|
.option('-j, --json', 'Output routes as JSON')
|
|
22
|
+
.option('-m, --manifest', 'Generate or update .webspresso/routes.manifest.json')
|
|
22
23
|
.option('--root <dir>', 'Root directory of the project', process.cwd())
|
|
23
24
|
.action((options) => {
|
|
24
25
|
const rootDir = path.resolve(options.root || process.cwd());
|
|
25
26
|
|
|
27
|
+
if (options.manifest) {
|
|
28
|
+
const { generateRouteManifest } = require('../../src/discovery/route-manifest');
|
|
29
|
+
const result = generateRouteManifest({ rootDir });
|
|
30
|
+
console.log(`✅ AOT route manifest written to: ${path.relative(rootDir, result.manifestPath)} (${result.routeCount} routes)`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
26
34
|
const descriptors = scanRoutes({
|
|
27
35
|
rootDir,
|
|
28
36
|
pages: fs.existsSync(path.join(rootDir, 'pages')) ? { dir: 'pages' } : true,
|
package/core/applySchema.js
CHANGED
|
@@ -49,8 +49,25 @@ function applySchema(req, compiledSchema) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
let _translateValidationIssues = null;
|
|
53
|
+
function getTranslateValidationIssues() {
|
|
54
|
+
if (!_translateValidationIssues) {
|
|
55
|
+
try {
|
|
56
|
+
_translateValidationIssues = require('../src/api/validation-i18n').translateValidationIssues;
|
|
57
|
+
} catch {
|
|
58
|
+
_translateValidationIssues = (issues) => issues;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return _translateValidationIssues;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function translateValidationIssues(issues, options) {
|
|
65
|
+
return getTranslateValidationIssues()(issues, options);
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
module.exports = {
|
|
53
|
-
applySchema
|
|
69
|
+
applySchema,
|
|
70
|
+
translateValidationIssues,
|
|
54
71
|
};
|
|
55
72
|
|
|
56
73
|
|
package/core/auth/jwt.js
CHANGED
|
@@ -6,18 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
|
|
9
|
+
const HS256_HEADER_BASE64URL = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9';
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Base64URL encode string or Buffer
|
|
11
13
|
* @param {string|Buffer} input
|
|
12
14
|
* @returns {string}
|
|
13
15
|
*/
|
|
14
16
|
function base64UrlEncode(input) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
.toString('
|
|
18
|
-
.replace(/=/g, '')
|
|
19
|
-
.replace(/\+/g, '-')
|
|
20
|
-
.replace(/\//g, '_');
|
|
17
|
+
return Buffer.isBuffer(input)
|
|
18
|
+
? input.toString('base64url')
|
|
19
|
+
: Buffer.from(String(input), 'utf8').toString('base64url');
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
/**
|
|
@@ -26,11 +25,7 @@ function base64UrlEncode(input) {
|
|
|
26
25
|
* @returns {string}
|
|
27
26
|
*/
|
|
28
27
|
function base64UrlDecode(input) {
|
|
29
|
-
|
|
30
|
-
while (base64.length % 4 !== 0) {
|
|
31
|
-
base64 += '=';
|
|
32
|
-
}
|
|
33
|
-
return Buffer.from(base64, 'base64').toString('utf8');
|
|
28
|
+
return Buffer.from(String(input), 'base64url').toString('utf8');
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
/**
|
|
@@ -110,16 +105,14 @@ function signJwt(payload, secret, options = {}) {
|
|
|
110
105
|
...payload,
|
|
111
106
|
};
|
|
112
107
|
|
|
113
|
-
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
108
|
+
const encodedHeader = algorithm === 'HS256' ? HS256_HEADER_BASE64URL : base64UrlEncode(JSON.stringify(header));
|
|
114
109
|
const encodedPayload = base64UrlEncode(JSON.stringify(fullPayload));
|
|
115
110
|
|
|
116
111
|
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
117
|
-
const
|
|
112
|
+
const encodedSignature = crypto
|
|
118
113
|
.createHmac('sha256', secret)
|
|
119
114
|
.update(signingInput)
|
|
120
|
-
.digest();
|
|
121
|
-
|
|
122
|
-
const encodedSignature = base64UrlEncode(signature);
|
|
115
|
+
.digest('base64url');
|
|
123
116
|
|
|
124
117
|
return `${signingInput}.${encodedSignature}`;
|
|
125
118
|
}
|
|
@@ -177,7 +170,7 @@ function verifyJwt(token, secret, options = {}) {
|
|
|
177
170
|
.update(signingInput)
|
|
178
171
|
.digest();
|
|
179
172
|
|
|
180
|
-
const actualSignatureBuf = Buffer.from(encodedSignature
|
|
173
|
+
const actualSignatureBuf = Buffer.from(encodedSignature, 'base64url');
|
|
181
174
|
|
|
182
175
|
if (
|
|
183
176
|
expectedSignatureBuf.length !== actualSignatureBuf.length ||
|