webspresso 0.0.104 → 0.0.106
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 +21 -2
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +220 -8
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/FLAGS.md +118 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/LOGGING.md +93 -0
- package/.agents/METRICS.md +127 -0
- package/.agents/MIGRATION_JOBS.md +95 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +38 -3
- package/.agents/QUEUE.md +42 -0
- package/.agents/RATE_LIMIT.md +147 -0
- package/.agents/RESILIENCE.md +235 -0
- package/.agents/SCALE_OUT.md +109 -0
- package/.agents/SECRETS.md +117 -0
- package/AGENTS.md +16 -0
- package/README.md +2 -2
- package/bin/commands/add-aws.js +396 -0
- package/bin/commands/add-ci.js +523 -0
- package/bin/commands/add-cloudflare.js +240 -0
- package/bin/commands/add-cloudrun.js +381 -0
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/add-migration-job.js +396 -0
- package/bin/commands/assets-sync.js +209 -0
- package/bin/commands/client-generate.js +52 -0
- package/bin/commands/console.js +53 -0
- package/bin/commands/db-migrate.js +47 -2
- package/bin/commands/db-studio.js +84 -0
- package/bin/commands/deploy-check.js +242 -13
- package/bin/commands/dev.js +1 -1
- package/bin/commands/export.js +87 -0
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/secrets-pull.js +208 -0
- package/bin/commands/start.js +1 -1
- package/bin/utils/db.js +7 -1
- package/bin/utils/project.js +2 -2
- package/bin/webspresso.js +22 -0
- package/core/app.js +421 -0
- package/core/auth/authenticate.js +93 -0
- package/core/auth/hash.js +5 -4
- package/core/auth/index.js +34 -4
- package/core/auth/jwt.js +5 -54
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +53 -10
- package/core/auth/middleware.js +16 -68
- package/core/auth/oauth.js +8 -17
- package/core/auth/redis-session.js +1008 -0
- package/core/auth/sessions.js +156 -0
- package/core/auth/totp.js +2 -1
- package/core/cache/byte-size.js +123 -0
- package/core/cache/cdn.js +296 -0
- package/core/cache/index.js +33 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +354 -0
- package/core/commands/registry.js +6 -20
- package/core/console/commands.js +148 -0
- package/core/console/context.js +260 -0
- package/core/console/eval.js +70 -0
- package/core/console/index.js +22 -0
- package/core/console/repl.js +102 -0
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +49 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +9 -7
- package/core/events/signal.js +7 -0
- package/core/export/discovery.js +191 -0
- package/core/export/exporter.js +277 -0
- package/core/export/index.js +24 -0
- package/core/flags/evaluator.js +252 -0
- package/core/flags/hasher.js +37 -0
- package/core/flags/index.js +21 -0
- package/core/flags/manager.js +263 -0
- package/core/flags/store.js +126 -0
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/http/adapter.js +74 -0
- package/core/http/express-adapter.js +46 -0
- package/core/http/hono-adapter.js +531 -0
- package/core/http/index.js +41 -0
- package/core/http/pipeline.js +485 -0
- package/core/index.js +336 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +325 -0
- package/core/limits/normalize.js +222 -0
- package/core/logger/formatters.js +264 -0
- package/core/logger/index.js +260 -0
- package/core/logger/trace.js +137 -0
- package/core/metrics/collectors.js +125 -0
- package/core/metrics/index.js +45 -0
- package/core/metrics/middleware.js +230 -0
- package/core/metrics/registry.js +366 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/d1.js +265 -0
- package/core/orm/index.js +176 -13
- package/core/orm/json-fields.js +22 -8
- package/core/orm/migrate-cloud.js +194 -0
- package/core/orm/model.js +10 -0
- package/core/orm/pool.js +97 -0
- package/core/orm/query-builder.js +72 -9
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/cloudflare.js +131 -0
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/adapters/pubsub.js +400 -0
- package/core/queue/adapters/sqs.js +419 -0
- package/core/queue/adapters/wait-until.js +77 -0
- package/core/queue/index.js +8 -0
- package/core/queue/manager.js +66 -41
- package/core/rate-limit/index.js +22 -0
- package/core/rate-limit/limiter.js +219 -0
- package/core/rate-limit/stores/memory.js +129 -0
- package/core/rate-limit/stores/redis.js +151 -0
- package/core/rate-limit/utils.js +112 -0
- package/core/resilience/circuit-breaker.js +422 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/secrets/index.js +113 -0
- package/core/secrets/manager.js +308 -0
- package/core/secrets/providers/aws.js +288 -0
- package/core/secrets/providers/env.js +44 -0
- package/core/secrets/providers/gcp.js +307 -0
- package/core/secrets/providers/memory.js +76 -0
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +129 -19
- package/core/stream/index.js +19 -0
- package/core/stream/text-stream.js +326 -0
- package/core/studio/index.js +43 -0
- package/core/studio/inspector.js +258 -0
- package/core/studio/server.js +325 -0
- package/core/studio/ui.js +681 -0
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +118 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +6 -5
- package/core/utils/fs.js +144 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +1549 -2
- package/index.js +272 -2
- package/package.json +3 -2
- package/plugins/admin-panel/core/api-extensions.js +2 -5
- package/plugins/admin-panel/index.js +13 -6
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -3
- package/plugins/api-key/index.js +3 -2
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +6 -15
- package/plugins/audit-log/index.js +3 -1
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +3 -2
- package/plugins/cors.js +3 -2
- package/plugins/csrf/index.js +9 -12
- package/plugins/csv/index.js +8 -5
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/email/index.js +11 -9
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +5 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/polar/src/api.js +2 -1
- package/plugins/polar/src/webhooks.js +5 -6
- package/plugins/queue/index.js +18 -2
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/redirect/index.js +16 -9
- package/plugins/rls/index.js +170 -0
- package/plugins/seo-checker/index.js +2 -2
- package/plugins/site-analytics/index.js +3 -1
- package/plugins/site-analytics/tracking.js +2 -2
- package/plugins/sitemap.js +3 -2
- package/plugins/swagger.js +12 -5
- package/plugins/upload/gcs-provider.js +148 -0
- package/plugins/upload/index.js +136 -77
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/upload/s3-provider.js +257 -0
- package/plugins/webhooks/crypto.js +6 -11
- package/plugins/webhooks/dispatcher.js +8 -8
- package/plugins/webhooks/index.js +33 -23
- package/plugins/webhooks/matcher.js +7 -11
- package/plugins/webhooks/sender.js +8 -11
- package/plugins/xlsx/index.js +7 -5
- package/src/api/api-loader.js +12 -95
- package/src/api/define-api.js +1 -0
- package/src/app-context.js +41 -5
- package/src/client-runtime/constants.js +10 -0
- package/src/client-runtime/mount.js +2 -3
- package/src/dev/banner.js +6 -2
- package/src/dev/client-gen.js +458 -0
- package/src/discovery/scan-routes.js +5 -2
- package/src/file-router.js +3 -3
- package/src/helpers.js +212 -24
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +13 -154
- package/src/plugin-manager.js +99 -18
- package/src/server.js +335 -133
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +95 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +15 -1
- package/src/services/validator.js +38 -20
- package/src/template/precompile.js +8 -19
package/.agents/ORM.md
CHANGED
|
@@ -116,11 +116,33 @@ const product = await productRepo.findById(1);
|
|
|
116
116
|
const activeProducts = await productRepo.find({ active: true });
|
|
117
117
|
const singleProduct = await productRepo.findOne({ slug: 'laptop' });
|
|
118
118
|
|
|
119
|
-
// 2. Insert, Update, Delete
|
|
119
|
+
// 2. Insert, Update, Delete & Batch Operations
|
|
120
120
|
const newProduct = await productRepo.create({ name: 'Laptop', price: 999, category_id: 1 });
|
|
121
121
|
const updated = await productRepo.update(newProduct.id, { price: 899 });
|
|
122
122
|
await productRepo.delete(newProduct.id); // Soft or hard delete based on model config
|
|
123
123
|
|
|
124
|
+
// 2.1 Batch Insert (Auto-Chunked with SQL Variable Limit Protection)
|
|
125
|
+
// Automatically chunks rows to stay safely within SQLite/Postgres parameter limits (prevents 'too many SQL variables' error)
|
|
126
|
+
// and executes inside an atomic transaction (all succeed or all rollback).
|
|
127
|
+
const products = await productRepo.createBatch([
|
|
128
|
+
{ name: 'Tablet', price: 499, category_id: 1 },
|
|
129
|
+
{ name: 'Phone', price: 799, category_id: 1 },
|
|
130
|
+
], { chunkSize: 500, transaction: true });
|
|
131
|
+
|
|
132
|
+
// 2.2 Upsert & Batch Upsert (onConflict Merge)
|
|
133
|
+
// Inserts new records or updates existing records on conflict target (e.g. sku or primaryKey)
|
|
134
|
+
const upserted = await productRepo.upsert(
|
|
135
|
+
{ sku: 'SKU-001', name: 'Keyboard', price: 59 },
|
|
136
|
+
'sku',
|
|
137
|
+
['name', 'price']
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const batchUpserted = await productRepo.upsertBatch(
|
|
141
|
+
catalogItems,
|
|
142
|
+
'sku',
|
|
143
|
+
{ updateColumns: ['name', 'price', 'updated_at'] }
|
|
144
|
+
);
|
|
145
|
+
|
|
124
146
|
// 3. Query Builder & Pagination
|
|
125
147
|
const result = await productRepo.query()
|
|
126
148
|
.where('price', '>', 500)
|
|
@@ -468,5 +490,137 @@ const user = await UserFactory.create({ db });
|
|
|
468
490
|
const admins = await UserFactory.count(3).apply('admin').create({ db });
|
|
469
491
|
```
|
|
470
492
|
|
|
493
|
+
---
|
|
494
|
+
|
|
495
|
+
## 12. Dynamic Model Row-Level Security (RLS)
|
|
496
|
+
|
|
497
|
+
Webspresso ORM features zero-dependency, declarative Row-Level Security (RLS) directly on model definitions, ensuring data isolation across users, organizations, and multi-tenant systems.
|
|
498
|
+
|
|
499
|
+
### 12.1 Declaring RLS Policies on Models
|
|
500
|
+
|
|
501
|
+
Models declare row-level access rules with `rls` inside `defineModel`:
|
|
502
|
+
|
|
503
|
+
```javascript
|
|
504
|
+
const { defineModel, zdb } = require('webspresso');
|
|
505
|
+
|
|
506
|
+
module.exports = defineModel({
|
|
507
|
+
name: 'Document',
|
|
508
|
+
table: 'documents',
|
|
509
|
+
schema: zdb.schema({
|
|
510
|
+
id: zdb.id(),
|
|
511
|
+
user_id: zdb.integer(),
|
|
512
|
+
tenant_id: zdb.string().optional(),
|
|
513
|
+
title: zdb.string(),
|
|
514
|
+
content: zdb.text(),
|
|
515
|
+
status: zdb.string().default('draft'),
|
|
516
|
+
is_public: zdb.boolean().default(false),
|
|
517
|
+
}),
|
|
518
|
+
rls: {
|
|
519
|
+
// Shorthand: automatically scopes queries and mutations to user.id
|
|
520
|
+
userColumn: 'user_id',
|
|
521
|
+
|
|
522
|
+
// Multi-tenant shorthand: scopes to user.tenant_id or scopeContext.tenantId
|
|
523
|
+
tenantColumn: 'tenant_id',
|
|
524
|
+
|
|
525
|
+
// Role-based bypass: admins and superadmins bypass RLS by default
|
|
526
|
+
bypassRoles: ['admin', 'superadmin'],
|
|
527
|
+
|
|
528
|
+
// Granular action rules:
|
|
529
|
+
// Read / Select: public documents are visible to anyone, drafts only to author
|
|
530
|
+
read: (user, qb) => {
|
|
531
|
+
if (!user) return { is_public: true };
|
|
532
|
+
return (q) => q.where('user_id', user.id).orWhere('is_public', true);
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
// Update: only allow modifying draft documents
|
|
536
|
+
update: (user, qb) => {
|
|
537
|
+
if (!user) return false;
|
|
538
|
+
return (q) => q.where('user_id', user.id).where('status', 'draft');
|
|
539
|
+
},
|
|
540
|
+
|
|
541
|
+
// Delete: completely disabled or owner only
|
|
542
|
+
delete: (user, qb) => {
|
|
543
|
+
if (!user) return false;
|
|
544
|
+
return { user_id: user.id };
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
// Create: auto-populates user_id, validates permissions or modifies payload
|
|
548
|
+
create: (user, data) => {
|
|
549
|
+
if (!user) return false; // Throws UnauthorizedError/ForbiddenError
|
|
550
|
+
return { status: 'draft' }; // Enforce default state
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### 12.2 Scoping Repositories and Query Builders
|
|
557
|
+
|
|
558
|
+
Repositories and query builders provide fluent scoping methods:
|
|
559
|
+
|
|
560
|
+
```javascript
|
|
561
|
+
const repo = db.getRepository('Document');
|
|
562
|
+
|
|
563
|
+
// Scoped to specific user:
|
|
564
|
+
const userRepo = repo.forUser(currentUser);
|
|
565
|
+
const myDocs = await userRepo.findAll();
|
|
566
|
+
const myDoc = await userRepo.findById(docId); // null if not owned
|
|
567
|
+
|
|
568
|
+
// Auto-populates user_id and rejects spoofed user IDs:
|
|
569
|
+
const newDoc = await userRepo.create({ title: 'My Report' });
|
|
570
|
+
|
|
571
|
+
// QueryBuilder chaining:
|
|
572
|
+
const count = await repo.query().forUser(currentUser).where('status', 'draft').count();
|
|
573
|
+
const paged = await repo.query().forUser(currentUser).paginate(1, 10);
|
|
574
|
+
|
|
575
|
+
// Explicit bypass (system/admin operations):
|
|
576
|
+
const allDocs = await repo.withoutRls().findAll();
|
|
577
|
+
const total = await repo.query().withoutRls().count();
|
|
578
|
+
|
|
579
|
+
// Multi-tenant scoping:
|
|
580
|
+
const tenantRepo = repo.forTenant('org_acme');
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
### 12.3 Database Scoped Accessors
|
|
584
|
+
|
|
585
|
+
```javascript
|
|
586
|
+
// Scoped database instance:
|
|
587
|
+
const userDb = db.forUser(currentUser);
|
|
588
|
+
const docs = await userDb.getRepository('Document').findAll();
|
|
589
|
+
const drafts = await userDb.query('Document').where('status', 'draft').list();
|
|
590
|
+
|
|
591
|
+
// Ambient transaction scoping:
|
|
592
|
+
await db.transaction(async (trx) => {
|
|
593
|
+
const noteRepo = trx.forUser(currentUser).getRepository('Document');
|
|
594
|
+
await noteRepo.create({ title: 'Transaction Document' });
|
|
595
|
+
});
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### 12.4 Automatic Express Integration (`rlsPlugin`)
|
|
599
|
+
|
|
600
|
+
Register `rlsPlugin()` to automatically wrap `req.db.getRepository()` with `req.user` in API endpoints and SSR loaders:
|
|
601
|
+
|
|
602
|
+
```javascript
|
|
603
|
+
const { createApp, rlsPlugin } = require('webspresso');
|
|
604
|
+
|
|
605
|
+
const app = createApp({
|
|
606
|
+
plugins: [
|
|
607
|
+
rlsPlugin({
|
|
608
|
+
bypassRoles: ['admin', 'superadmin'], // default
|
|
609
|
+
getUser: (req) => req.user, // default
|
|
610
|
+
}),
|
|
611
|
+
],
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// Inside any API route (pages/api/documents.get.js):
|
|
615
|
+
module.exports = defineApi({
|
|
616
|
+
handler: async (req, res) => {
|
|
617
|
+
// req.db.getRepository('Document') is automatically scoped to req.user!
|
|
618
|
+
const docs = await req.db.getRepository('Document').findAll();
|
|
619
|
+
return res.json(docs);
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
|
|
471
625
|
|
|
472
626
|
|
package/.agents/PLUGINS.md
CHANGED
|
@@ -161,12 +161,19 @@ Schema-driven headless CMS & inline content editing.
|
|
|
161
161
|
- **Features**: Dynamic content types & entries, public API (`/api/content/:type/:slug`), visual inline editor widget.
|
|
162
162
|
|
|
163
163
|
### 2.4 `uploadPlugin` (`plugins/upload`)
|
|
164
|
-
Multipart file upload manager with local
|
|
165
|
-
- **Options**: `{ path: '/api/upload', local: { destDir, publicBasePath }, maxBytes: 10000000, mimeAllowlist: ['image/png', 'image/jpeg'] }`
|
|
166
|
-
- **
|
|
164
|
+
Multipart file upload manager with local disk, AWS S3, Google Cloud Storage (GCS), and Cloudflare R2 drivers.
|
|
165
|
+
- **Options**: `{ path: '/api/upload', provider, local: { destDir, publicBasePath }, maxBytes: 10000000, mimeAllowlist: ['image/png', 'image/jpeg'], presigned: true, presignedRoute: '/api/upload/presign' }`
|
|
166
|
+
- **Supported Storage Drivers**:
|
|
167
|
+
- `createLocalFileProvider({ destDir, publicBasePath })`: Local disk storage (monolith / VPS).
|
|
168
|
+
- `createS3FileProvider({ s3Client, bucket, publicBaseUrl, prefix })`: AWS S3, MinIO, or S3-compatible endpoints with direct presigned PUT URLs (`createSignedUploadUrl`).
|
|
169
|
+
- `createGcsFileProvider({ bucket, storageClient, publicBaseUrl, prefix })`: Google Cloud Storage with V4 signed URLs for serverless direct uploads.
|
|
170
|
+
- `createR2FileProvider({ bucket, publicBaseUrl, prefix })`: Cloudflare Workers native R2 binding.
|
|
171
|
+
- **Direct-to-Cloud Presigned Uploads**: Set `presigned: true` to mount `POST /api/upload/presign`, allowing web browsers to upload directly to S3 or GCS without passing large payloads through memory-limited or ephemeral containers.
|
|
172
|
+
- **Static Asset Sync**: `webspresso assets:sync [--bucket <name>] [--provider s3|gcs|r2] [--dry-run]` to upload compiled JS/CSS bundles to cloud storage with `Cache-Control: public, max-age=31536000, immutable`.
|
|
167
173
|
- **Orphan Pruning**: `scanOrphanedUploads({ db, destDir })` and `pruneOrphanedUploads({ db, destDir, dryRun })` to detect and remove disk files no longer referenced by any database model, accompanied by CLI command `webspresso upload:prune [--dry-run]`.
|
|
168
174
|
|
|
169
175
|
|
|
176
|
+
|
|
170
177
|
### 2.5 `fileManagerPlugin` (`plugins/file-manager`)
|
|
171
178
|
Filesystem-first media library, directory explorer, and modal asset picker for Admin Panel.
|
|
172
179
|
- **Options**: `{ baseDir: './public/uploads', publicBasePath: '/uploads', apiPrefix: '/files', pageTitle: 'File Manager', maxUploadBytes: 10485760, allowedExtensions, allowedMimeTypes, includePage: true, lazy: false }`
|
|
@@ -692,3 +699,31 @@ Zero-dependency on-demand plugin hydration wrapper that defers heavy plugin init
|
|
|
692
699
|
- Mounts a lightweight Express router proxy at `prefix`.
|
|
693
700
|
- Stubs placeholder services into `ServiceRegistry` and proxy helpers into Nunjucks context.
|
|
694
701
|
- 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.
|
|
702
|
+
|
|
703
|
+
### 2.21 `rlsPlugin` (`plugins/rls`)
|
|
704
|
+
Zero-dependency Dynamic Model Row-Level Security (RLS) plugin that bridges request authentication (`req.user`, `req.auth`, `req.tenantId`) to ORM repositories with automatic query scoping, role-based bypass, and route-level overrides.
|
|
705
|
+
- **Why**: Eliminates manual `where({ user_id: req.user.id })` boilerplate across all API endpoints, prevents accidental cross-tenant/cross-user data leakage, and provides declarative data access boundaries.
|
|
706
|
+
- **Options**:
|
|
707
|
+
- `bypassRoles`: Global roles that bypass RLS across all models (default: `['admin', 'superadmin']`). Set to `false` to disable.
|
|
708
|
+
- `getUser`: Custom extractor for current user from `req` (default: `req.user || req.adminUser || req.auth?.user`).
|
|
709
|
+
- `getTenantId`: Custom extractor for current tenant ID from `req` (default: `req.tenantId || req.user?.tenant_id`).
|
|
710
|
+
- `global`: Mount global middleware wrapping `req.db` (default: `true`).
|
|
711
|
+
- `enabled`: Toggle RLS plugin active state (default: `true`).
|
|
712
|
+
- **Usage**:
|
|
713
|
+
```javascript
|
|
714
|
+
const { createApp, rlsPlugin } = require('webspresso');
|
|
715
|
+
|
|
716
|
+
const app = createApp({
|
|
717
|
+
plugins: [
|
|
718
|
+
rlsPlugin({
|
|
719
|
+
bypassRoles: ['admin', 'superadmin'],
|
|
720
|
+
}),
|
|
721
|
+
],
|
|
722
|
+
});
|
|
723
|
+
```
|
|
724
|
+
- **How It Works**:
|
|
725
|
+
- Automatically wraps `req.db.getRepository()` and `req.db.query()` with the active `user` and `tenantId`.
|
|
726
|
+
- Admins bypass filters automatically unless model specifies `bypassRoles: false`.
|
|
727
|
+
- Endpoints can explicitly bypass RLS when needed via `req.db.getRepository('Model').withoutRls()`.
|
|
728
|
+
- Named middleware `middleware: ['rls']` available for selective route application.
|
|
729
|
+
|
package/.agents/QUEUE.md
CHANGED
|
@@ -92,6 +92,48 @@ queuePlugin({
|
|
|
92
92
|
});
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
### 4. `SqsQueueAdapter` (AWS SQS)
|
|
96
|
+
Zero-dependency AWS SQS adapter powered by native Node.js HTTP and standard AWS SigV4 request signing. Supports long-polling worker loops (`dequeue`), direct enqueue with delay seconds calculation, and AWS Lambda push batch execution (`processBatch`).
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
const { SqsQueueAdapter } = require('webspresso');
|
|
100
|
+
|
|
101
|
+
// Worker daemon or long-polling runner:
|
|
102
|
+
const adapter = new SqsQueueAdapter({
|
|
103
|
+
queueUrl: process.env.AWS_SQS_QUEUE_URL,
|
|
104
|
+
region: process.env.AWS_REGION || 'us-east-1',
|
|
105
|
+
waitTimeSeconds: 10,
|
|
106
|
+
visibilityTimeout: 30,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// AWS Lambda SQS Event Handler:
|
|
110
|
+
exports.handler = async (event) => {
|
|
111
|
+
return adapter.processBatch(event.Records, queueManager);
|
|
112
|
+
};
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 5. `PubSubQueueAdapter` (Google Cloud Pub/Sub)
|
|
116
|
+
Zero-dependency Google Cloud Pub/Sub adapter using standard REST API (`:publish`, `:pull`, `:acknowledge`, `:modifyAckDeadline`) with base64 payload serialization. Supports both background pull workers and Cloud Run / App Engine Push Subscription HTTP webhooks (`processPushRequest`).
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
const { PubSubQueueAdapter } = require('webspresso');
|
|
120
|
+
|
|
121
|
+
const adapter = new PubSubQueueAdapter({
|
|
122
|
+
projectId: process.env.GOOGLE_CLOUD_PROJECT,
|
|
123
|
+
topic: 'app-jobs',
|
|
124
|
+
subscription: 'app-jobs-sub',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Cloud Run Push Subscription Webhook Route:
|
|
128
|
+
app.post('/api/queue/push', async (req, res) => {
|
|
129
|
+
await adapter.processPushRequest(req, res, req.queue);
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 6. `WaitUntilQueueAdapter` & `CloudflareQueueAdapter`
|
|
134
|
+
- `WaitUntilQueueAdapter`: Serverless & Edge execution (Cloudflare Workers `ctx.waitUntil`, Vercel Edge).
|
|
135
|
+
- `CloudflareQueueAdapter`: Cloudflare Queues integration (`env.MY_QUEUE.send`).
|
|
136
|
+
|
|
95
137
|
---
|
|
96
138
|
|
|
97
139
|
## 4. File-Based Auto-Discovery (`jobs/`)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Native Zero-Dependency Rate Limiting Engine (`core/rate-limit`)
|
|
2
|
+
|
|
3
|
+
Webspresso features a high-performance, zero-dependency Rate Limiting Engine located in `core/rate-limit/`. It works seamlessly across both traditional Express middleware pipelines and modern edge/serverless contexts (such as Hono), providing standard RFC 6585 and IETF Draft rate limit headers with memory leak-free in-memory and distributed Redis sliding-window stores.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🎯 Native Core vs. Plugin Comparison
|
|
8
|
+
|
|
9
|
+
| Feature | Core Rate Limiter (`core/rate-limit`) | Rate Limit Plugin (`plugins/rate-limit`) |
|
|
10
|
+
| :--- | :--- | :--- |
|
|
11
|
+
| **Dependencies** | **Zero external dependencies** (pure Node.js) | Requires `express-rate-limit` peer dependency |
|
|
12
|
+
| **Import** | `const { rateLimit, RateLimiter } = require('webspresso')` | `const { rateLimitPlugin } = require('webspresso')` |
|
|
13
|
+
| **Framework Agnostic** | ✅ Express `(req, res, next)` & Hono `(c, next)` | Express only |
|
|
14
|
+
| **Stores** | Native Sliding Window `MemoryRateLimitStore` & `RedisRateLimitStore` | `express-rate-limit` memory or external store |
|
|
15
|
+
| **Headers** | RFC 6585 `RateLimit-*`, `Retry-After`, legacy `X-RateLimit-*` | `standardHeaders: 'draft-7'`, `legacyHeaders` |
|
|
16
|
+
| **File Route Usage** | Universal middleware in API routes and standalone apps | Named middleware via `ctx.middlewares.rateLimit` |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 🚀 Quick Start
|
|
21
|
+
|
|
22
|
+
### 1. Basic Express Route Limiting
|
|
23
|
+
|
|
24
|
+
```javascript
|
|
25
|
+
const { rateLimit } = require('webspresso');
|
|
26
|
+
|
|
27
|
+
// 60 requests per minute
|
|
28
|
+
const apiLimiter = rateLimit({
|
|
29
|
+
window: '1m',
|
|
30
|
+
limit: 60,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Use as standard middleware
|
|
34
|
+
app.use('/api', apiLimiter);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. File-Based API Route (`pages/api/...`)
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
const { defineApi, rateLimit } = require('webspresso');
|
|
41
|
+
|
|
42
|
+
const strictLimiter = rateLimit({
|
|
43
|
+
window: '10s',
|
|
44
|
+
limit: 5,
|
|
45
|
+
message: { error: 'Too many requests. Please slow down.' },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
module.exports = defineApi({
|
|
49
|
+
middleware: [strictLimiter],
|
|
50
|
+
handler: async (req, res) => {
|
|
51
|
+
return res.json({ success: true });
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. Distributed Redis Rate Limiting (Scale-Out)
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
const { rateLimit, RedisRateLimitStore } = require('webspresso');
|
|
60
|
+
const Redis = require('ioredis');
|
|
61
|
+
|
|
62
|
+
const redis = new Redis(process.env.REDIS_URL);
|
|
63
|
+
|
|
64
|
+
const limiter = rateLimit({
|
|
65
|
+
window: '15m',
|
|
66
|
+
limit: 100,
|
|
67
|
+
store: new RedisRateLimitStore({
|
|
68
|
+
client: redis,
|
|
69
|
+
prefix: 'rl:auth:',
|
|
70
|
+
passOnStoreError: true, // Fail-open if Redis drops
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 4. Edge / Hono Application Usage
|
|
76
|
+
|
|
77
|
+
The middleware automatically detects Hono's `(c, next)` context signature:
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
const { Hono } = require('hono');
|
|
81
|
+
const { rateLimit } = require('webspresso');
|
|
82
|
+
|
|
83
|
+
const app = new Hono();
|
|
84
|
+
const limiter = rateLimit({ limit: 100, window: '1m' });
|
|
85
|
+
|
|
86
|
+
app.use('*', (c, next) => limiter(c, next));
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## ⚙️ Options Reference
|
|
92
|
+
|
|
93
|
+
| Option | Type | Default | Description |
|
|
94
|
+
| :--- | :--- | :--- | :--- |
|
|
95
|
+
| `limit` / `max` | `number` | `60` | Maximum number of allowed requests within the time window. |
|
|
96
|
+
| `window` / `windowMs`| `string\|number` | `'1m'` | Window duration in ms or string shorthand (`'500ms'`, `'10s'`, `'1m'`, `'1h'`, `'1d'`). |
|
|
97
|
+
| `store` | `RateLimitStore` | `MemoryRateLimitStore` | Underlying storage engine. Defaults to sliding window in-memory store. |
|
|
98
|
+
| `keyGenerator` | `(req) => string` | `extractClientIp` | Function to identify client. Extracts Cloudflare, X-Forwarded-For, or socket IP by default. |
|
|
99
|
+
| `skip` | `(req) => boolean` | `() => false` | Condition to skip rate limiting (e.g. internal healthchecks). |
|
|
100
|
+
| `standardHeaders` | `boolean` | `true` | Send standard RFC headers (`RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`, `RateLimit-Policy`). |
|
|
101
|
+
| `legacyHeaders` | `boolean` | `false` | Send legacy `X-RateLimit-*` headers. |
|
|
102
|
+
| `skipFailedRequests` | `boolean` | `false` | When `true`, failed requests (`statusCode >= 400`) decrement hit count upon finish. |
|
|
103
|
+
| `skipSuccessfulRequests`| `boolean` | `false` | When `true`, successful requests (`statusCode < 400`) decrement hit count upon finish. |
|
|
104
|
+
| `statusCode` | `number` | `429` | HTTP status code returned when limit is exceeded. |
|
|
105
|
+
| `message` | `string\|object` | `{ error: 'Too Many Requests', ... }` | Response payload on limit exceeded. |
|
|
106
|
+
| `handler` | `Function` | `null` | Custom callback when limit is exceeded `(req, res, next, info) => void`. |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 📡 HTTP Response Headers
|
|
111
|
+
|
|
112
|
+
When enabled, the rate limiter emits standard headers:
|
|
113
|
+
|
|
114
|
+
```http
|
|
115
|
+
HTTP/1.1 200 OK
|
|
116
|
+
RateLimit-Limit: 60
|
|
117
|
+
RateLimit-Remaining: 58
|
|
118
|
+
RateLimit-Reset: 42
|
|
119
|
+
RateLimit-Policy: 60;w=60
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
When the limit is exceeded (HTTP 429 Too Many Requests):
|
|
123
|
+
|
|
124
|
+
```http
|
|
125
|
+
HTTP/1.1 429 Too Many Requests
|
|
126
|
+
Retry-After: 42
|
|
127
|
+
RateLimit-Limit: 60
|
|
128
|
+
RateLimit-Remaining: 0
|
|
129
|
+
RateLimit-Reset: 42
|
|
130
|
+
RateLimit-Policy: 60;w=60
|
|
131
|
+
Content-Type: application/json
|
|
132
|
+
|
|
133
|
+
{"error":"Too Many Requests","message":"Too many requests, please try again later.","statusCode":429}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 🛡️ Reverse Proxy & Client IP Extraction
|
|
139
|
+
|
|
140
|
+
`extractClientIp(req)` extracts client IP in the following priority order:
|
|
141
|
+
1. `req.ip` (Express `trust proxy`)
|
|
142
|
+
2. `CF-Connecting-IP` (Cloudflare)
|
|
143
|
+
3. `Fastly-Client-IP` / `True-Client-IP` (Fastly / Akamai)
|
|
144
|
+
4. `X-Forwarded-For` (First client IP in comma-separated list)
|
|
145
|
+
5. `X-Real-IP` (Nginx / HAProxy)
|
|
146
|
+
6. `socket.remoteAddress` (Normalized IPv4 / IPv6)
|
|
147
|
+
7. Fallback `127.0.0.1`
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Enterprise Resilience, Circuit Breaker & Retry Engine (`core/resilience`)
|
|
2
|
+
|
|
3
|
+
Webspresso includes a zero-dependency, enterprise-grade resilience and fault tolerance engine built directly on native Node.js primitives (`events`, `perf_hooks`, `async_hooks`). It prevents cascading downstream outages, protects critical services, and recovers automatically from transient network spikes.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🚦 Architectural Overview
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
┌───────────────────────────────────────┐
|
|
11
|
+
│ Incoming Request │
|
|
12
|
+
└───────────────────┬───────────────────┘
|
|
13
|
+
│
|
|
14
|
+
▼
|
|
15
|
+
┌───────────────────────────────────────┐
|
|
16
|
+
│ Circuit Breaker (Fast Fail / OPEN) │
|
|
17
|
+
└───────┬───────────────────────▲───────┘
|
|
18
|
+
(Closed) │ │ (Trips on failure)
|
|
19
|
+
▼ │
|
|
20
|
+
┌───────────────────────────────┴───────┐
|
|
21
|
+
│ Retry Engine (Backoff + Jitter) │
|
|
22
|
+
└───────┬───────────────────────▲───────┘
|
|
23
|
+
(Attempt)│ │ (Transient Error)
|
|
24
|
+
▼ │
|
|
25
|
+
┌───────────────────────────────┴───────┐
|
|
26
|
+
│ Service Handler / Remote Upstream │
|
|
27
|
+
└───────────────────────────────────────┘
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
1. **State Machine (`CircuitBreaker`)**:
|
|
31
|
+
- `CLOSED`: Normal operation. Requests pass through. Failures are tracked in a rolling time window.
|
|
32
|
+
- `OPEN`: Downstream is degraded/unhealthy. Subsequent calls **fail fast immediately** with `CircuitBreakerOpenError` (HTTP 503) without hitting the downstream resource, or return a predefined `fallback`.
|
|
33
|
+
- `HALF_OPEN`: Trial recovery state entered automatically once `resetTimeout` elapses. A controlled number of probe requests (`successThreshold`) test the downstream. If they succeed, the breaker resets to `CLOSED`. If any probe fails, it trips back to `OPEN`.
|
|
34
|
+
|
|
35
|
+
2. **Retry with Exponential Backoff & Full Jitter (`retry`, `withRetry`)**:
|
|
36
|
+
- Automatically retries transient network errors (`ECONNRESET`, `ETIMEDOUT`, `ENOTFOUND`, 429 Too Many Requests, and 5xx Gateway errors).
|
|
37
|
+
- Skips client-side 4xx errors (e.g. 400 Bad Request, 401, 403, 404, 422) since retrying client errors produces identical failures.
|
|
38
|
+
- Full jitter prevents synchronization spikes (the "thundering herd" problem) against recovering third-party services.
|
|
39
|
+
- Respects `AbortSignal` for cancellation.
|
|
40
|
+
|
|
41
|
+
3. **Seamless Services Layer Integration**:
|
|
42
|
+
- Declare `circuitBreaker` and `retry` declaratively inside `defineService({ ... })`.
|
|
43
|
+
- Automatically protects service calls from SSR pages, API routes, or worker jobs.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 🛠️ Quick Start & Usage Examples
|
|
48
|
+
|
|
49
|
+
### 1. Declarative Service Protection (`defineService`)
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
const { defineService } = require('webspresso');
|
|
53
|
+
|
|
54
|
+
module.exports = defineService({
|
|
55
|
+
// Automatic retry on transient network errors
|
|
56
|
+
retry: {
|
|
57
|
+
retries: 3,
|
|
58
|
+
delay: '100ms',
|
|
59
|
+
factor: 2,
|
|
60
|
+
jitter: true,
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Circuit breaker with fast-fail and graceful fallback
|
|
64
|
+
circuitBreaker: {
|
|
65
|
+
failureThreshold: 3, // Open after 3 consecutive failures
|
|
66
|
+
failureRateThreshold: 0.5, // Or if >= 50% fail in rolling window
|
|
67
|
+
rollingWindow: '10s', // 10-second sample window
|
|
68
|
+
resetTimeout: '15s', // Stay open for 15s before trial recovery
|
|
69
|
+
timeout: '3s', // Per-call execution timeout (ms or string)
|
|
70
|
+
fallback: (err, input) => {
|
|
71
|
+
// Graceful fallback returned when OPEN or when all retries fail
|
|
72
|
+
return { fromCache: true, id: input.id, data: null, degraded: true };
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
schema: ({ z }) => ({
|
|
77
|
+
id: z.string(),
|
|
78
|
+
}),
|
|
79
|
+
|
|
80
|
+
handler: async (input, ctx) => {
|
|
81
|
+
// Call external payment or inventory API
|
|
82
|
+
const response = await fetch(`https://api.external.com/items/${input.id}`);
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
const err = new Error(`Upstream API responded with status ${response.status}`);
|
|
85
|
+
err.statusCode = response.status;
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
return await response.json();
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 2. Standalone Circuit Breaker Usage
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
const { createCircuitBreaker } = require('webspresso');
|
|
97
|
+
|
|
98
|
+
const paymentBreaker = createCircuitBreaker({
|
|
99
|
+
name: 'stripe-gateway',
|
|
100
|
+
failureThreshold: 5,
|
|
101
|
+
resetTimeout: '30s',
|
|
102
|
+
timeout: '5s',
|
|
103
|
+
fallback: (err) => {
|
|
104
|
+
return { status: 'queued_offline', message: 'Payment queued for processing' };
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Execute async function wrapped by breaker
|
|
109
|
+
const result = await paymentBreaker.execute(async () => {
|
|
110
|
+
return await stripeClient.charges.create({ ... });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Or wrap a function
|
|
114
|
+
const safeCharge = paymentBreaker.wrap(chargeFunction);
|
|
115
|
+
await safeCharge(args);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 3. Standalone Retry Engine (`retry`)
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
const { retry, isTransientError } = require('webspresso');
|
|
122
|
+
|
|
123
|
+
const data = await retry(
|
|
124
|
+
async (attempt) => {
|
|
125
|
+
return await fetchRemoteData();
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
retries: 3,
|
|
129
|
+
delay: 200, // Initial 200ms delay
|
|
130
|
+
factor: 2, // 200ms -> 400ms -> 800ms
|
|
131
|
+
jitter: true, // Randomized jitter
|
|
132
|
+
onRetry: ({ attempt, error, delayMs }) => {
|
|
133
|
+
console.warn(`Retry attempt ${attempt} after error: ${error.message} (waiting ${delayMs}ms)`);
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 4. Accessing Breakers & Retry from Route Handlers & Context
|
|
140
|
+
|
|
141
|
+
In API routes and SSR loaders:
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
// Inside API route handler or SSR loader
|
|
145
|
+
export async function handler(req, res) {
|
|
146
|
+
// Named circuit breaker from central registry
|
|
147
|
+
const crmBreaker = req.breaker('hubspot-api', { failureThreshold: 3 });
|
|
148
|
+
|
|
149
|
+
// Retry engine available on req
|
|
150
|
+
const lead = await req.retry(async () => {
|
|
151
|
+
return await pushLeadToCrm(req.body);
|
|
152
|
+
}, { retries: 2 });
|
|
153
|
+
|
|
154
|
+
return res.json(lead);
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
In services handlers (`ctx`):
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
module.exports = defineService({
|
|
162
|
+
handler: async (input, ctx) => {
|
|
163
|
+
// Access circuit breaker
|
|
164
|
+
const breaker = ctx.breaker('custom-api');
|
|
165
|
+
|
|
166
|
+
// Access retry helper
|
|
167
|
+
return await ctx.retry(async (attempt) => {
|
|
168
|
+
return await queryDownstream();
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## ⚙️ Configuration Reference
|
|
177
|
+
|
|
178
|
+
### `CircuitBreakerOptions`
|
|
179
|
+
|
|
180
|
+
| Option | Type | Default | Description |
|
|
181
|
+
| :--- | :--- | :--- | :--- |
|
|
182
|
+
| `name` | `string` | `'default'` | Identifier for logging and registry lookup. |
|
|
183
|
+
| `failureThreshold` | `number` | `5` | Consecutive failures before opening circuit. |
|
|
184
|
+
| `failureRateThreshold` | `number` | `null` | Failure rate (0.01 - 1.0) in rolling window to open circuit (e.g. `0.5` = 50%). |
|
|
185
|
+
| `minimumRequests` | `number` | `5` | Minimum calls in rolling window before failure rate check activates. |
|
|
186
|
+
| `rollingWindow` | `number \| string` | `10000` (10s) | Rolling time window for metrics sample pruning. |
|
|
187
|
+
| `successThreshold` | `number` | `2` | Consecutive successes in `HALF_OPEN` state required to close circuit. |
|
|
188
|
+
| `resetTimeout` | `number \| string` | `10000` (10s) | Duration circuit stays `OPEN` before attempting `HALF_OPEN` probe. |
|
|
189
|
+
| `timeout` | `number \| string` | `0` (disabled) | Per-call execution timeout in milliseconds or duration string (`'2s'`). |
|
|
190
|
+
| `fallback` | `Function` | `null` | Fallback handler `(error, ...args) => any` called when `OPEN` or call fails. |
|
|
191
|
+
| `isFailure` | `Function` | `defaultIsFailure` | Predicate `(error) => boolean`. By default ignores 4xx client errors (except 429). |
|
|
192
|
+
| `onStateChange` | `Function` | `null` | Callback `(fromState, toState, breaker) => void`. |
|
|
193
|
+
|
|
194
|
+
### `RetryOptions`
|
|
195
|
+
|
|
196
|
+
| Option | Type | Default | Description |
|
|
197
|
+
| :--- | :--- | :--- | :--- |
|
|
198
|
+
| `retries` | `number` | `3` | Maximum retry attempts before giving up. |
|
|
199
|
+
| `delay` / `minTimeout` | `number \| string` | `100` | Initial delay in ms or duration string (`'100ms'`). |
|
|
200
|
+
| `maxTimeout` | `number \| string` | `10000` (10s) | Maximum capped delay for exponential backoff. |
|
|
201
|
+
| `factor` | `number` | `2` | Exponential backoff multiplier. |
|
|
202
|
+
| `jitter` | `boolean` | `true` | Apply full randomized jitter (`Math.random() * delay`). |
|
|
203
|
+
| `shouldRetry` | `Function` | `isTransientError` | Predicate `(error, attempt) => boolean`. Retries network/5xx/429 errors. |
|
|
204
|
+
| `onRetry` | `Function` | `null` | Hook `({ attempt, error, delayMs }) => void`. |
|
|
205
|
+
| `signal` | `AbortSignal` | `null` | Optional signal to cancel pending retry delays immediately. |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 📊 Telemetry & Monitoring
|
|
210
|
+
|
|
211
|
+
Inspect health and failure metrics at any time:
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
// Breaker instance stats
|
|
215
|
+
const stats = breaker.getStats();
|
|
216
|
+
// {
|
|
217
|
+
// name: 'stripe-gateway',
|
|
218
|
+
// state: 'CLOSED',
|
|
219
|
+
// totalCalls: 120,
|
|
220
|
+
// successfulCalls: 118,
|
|
221
|
+
// failedCalls: 2,
|
|
222
|
+
// rejectedCalls: 0,
|
|
223
|
+
// timedOutCalls: 0,
|
|
224
|
+
// fallbackCalls: 0,
|
|
225
|
+
// consecutiveFailures: 0,
|
|
226
|
+
// consecutiveSuccesses: 118,
|
|
227
|
+
// failureRate: 0.016,
|
|
228
|
+
// lastFailureTime: 1713430000000,
|
|
229
|
+
// lastSuccessTime: 1713430001000,
|
|
230
|
+
// lastStateChange: 1713429000000
|
|
231
|
+
// }
|
|
232
|
+
|
|
233
|
+
// Global registry stats across all breakers
|
|
234
|
+
const allStats = app.breakers.getStats();
|
|
235
|
+
```
|