@arcis/node 1.4.4 → 1.5.1
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/LICENSE +21 -0
- package/README.md +36 -6
- package/dist/astro/index.js +6141 -0
- package/dist/astro/index.js.map +1 -0
- package/dist/astro/index.mjs +6136 -0
- package/dist/astro/index.mjs.map +1 -0
- package/dist/bun/index.js +6195 -0
- package/dist/bun/index.js.map +1 -0
- package/dist/bun/index.mjs +6189 -0
- package/dist/bun/index.mjs.map +1 -0
- package/dist/core/constants.d.ts +3 -2
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/index.js +4 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +4 -3
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/types.d.ts +32 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/fastify/index.js +6160 -0
- package/dist/fastify/index.js.map +1 -0
- package/dist/fastify/index.mjs +6155 -0
- package/dist/fastify/index.mjs.map +1 -0
- package/dist/guards.d.ts +156 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/hono/index.js +6159 -0
- package/dist/hono/index.js.map +1 -0
- package/dist/hono/index.mjs +6154 -0
- package/dist/hono/index.mjs.map +1 -0
- package/dist/index.d.ts +23 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7126 -178
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7088 -179
- package/dist/index.mjs.map +1 -1
- package/dist/koa/index.js +6158 -0
- package/dist/koa/index.js.map +1 -0
- package/dist/koa/index.mjs +6153 -0
- package/dist/koa/index.mjs.map +1 -0
- package/dist/logging/index.js.map +1 -1
- package/dist/logging/index.mjs.map +1 -1
- package/dist/logging/redactor.d.ts.map +1 -1
- package/dist/middleware/astro.d.ts +64 -0
- package/dist/middleware/astro.d.ts.map +1 -0
- package/dist/middleware/bot-detection.d.ts.map +1 -1
- package/dist/middleware/bun.d.ts +75 -0
- package/dist/middleware/bun.d.ts.map +1 -0
- package/dist/middleware/csrf.d.ts.map +1 -1
- package/dist/middleware/error-handler.d.ts.map +1 -1
- package/dist/middleware/fastify.d.ts +89 -0
- package/dist/middleware/fastify.d.ts.map +1 -0
- package/dist/middleware/graphql.d.ts +35 -0
- package/dist/middleware/graphql.d.ts.map +1 -0
- package/dist/middleware/hono.d.ts +63 -0
- package/dist/middleware/hono.d.ts.map +1 -0
- package/dist/middleware/index.d.ts +12 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +6469 -119
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +6459 -120
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/middleware/koa.d.ts +84 -0
- package/dist/middleware/koa.d.ts.map +1 -0
- package/dist/middleware/main.d.ts +0 -30
- package/dist/middleware/main.d.ts.map +1 -1
- package/dist/middleware/mass-assign.d.ts +81 -0
- package/dist/middleware/mass-assign.d.ts.map +1 -0
- package/dist/middleware/method-allowlist.d.ts +66 -0
- package/dist/middleware/method-allowlist.d.ts.map +1 -0
- package/dist/middleware/nestjs.d.ts +62 -0
- package/dist/middleware/nestjs.d.ts.map +1 -0
- package/dist/middleware/nextjs.d.ts +102 -0
- package/dist/middleware/nextjs.d.ts.map +1 -0
- package/dist/middleware/nuxt.d.ts +61 -0
- package/dist/middleware/nuxt.d.ts.map +1 -0
- package/dist/middleware/overload.d.ts +92 -0
- package/dist/middleware/overload.d.ts.map +1 -0
- package/dist/middleware/protect.d.ts +91 -0
- package/dist/middleware/protect.d.ts.map +1 -0
- package/dist/middleware/rate-limit-sliding.d.ts.map +1 -1
- package/dist/middleware/rate-limit-token.d.ts.map +1 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/response-splitting.d.ts +83 -0
- package/dist/middleware/response-splitting.d.ts.map +1 -0
- package/dist/middleware/sveltekit.d.ts +68 -0
- package/dist/middleware/sveltekit.d.ts.map +1 -0
- package/dist/middleware/token-budget.d.ts +75 -0
- package/dist/middleware/token-budget.d.ts.map +1 -0
- package/dist/nestjs/index.js +1724 -0
- package/dist/nestjs/index.js.map +1 -0
- package/dist/nestjs/index.mjs +1717 -0
- package/dist/nestjs/index.mjs.map +1 -0
- package/dist/nextjs/index.js +6184 -0
- package/dist/nextjs/index.js.map +1 -0
- package/dist/nextjs/index.mjs +6178 -0
- package/dist/nextjs/index.mjs.map +1 -0
- package/dist/nuxt/index.js +6141 -0
- package/dist/nuxt/index.js.map +1 -0
- package/dist/nuxt/index.mjs +6136 -0
- package/dist/nuxt/index.mjs.map +1 -0
- package/dist/sanitizers/encode.d.ts.map +1 -1
- package/dist/sanitizers/graphql.d.ts +72 -0
- package/dist/sanitizers/graphql.d.ts.map +1 -0
- package/dist/sanitizers/headers.d.ts +18 -0
- package/dist/sanitizers/headers.d.ts.map +1 -1
- package/dist/sanitizers/index.d.ts +4 -1
- package/dist/sanitizers/index.d.ts.map +1 -1
- package/dist/sanitizers/index.js +140 -66
- package/dist/sanitizers/index.js.map +1 -1
- package/dist/sanitizers/index.mjs +135 -67
- package/dist/sanitizers/index.mjs.map +1 -1
- package/dist/sanitizers/prompt-injection.d.ts +62 -0
- package/dist/sanitizers/prompt-injection.d.ts.map +1 -0
- package/dist/sanitizers/sanitize.d.ts +1 -1
- package/dist/sanitizers/sanitize.d.ts.map +1 -1
- package/dist/sanitizers/xpath.d.ts +37 -0
- package/dist/sanitizers/xpath.d.ts.map +1 -0
- package/dist/stores/index.js +4 -4
- package/dist/stores/index.js.map +1 -1
- package/dist/stores/index.mjs +4 -4
- package/dist/stores/index.mjs.map +1 -1
- package/dist/stores/redis.d.ts +7 -1
- package/dist/stores/redis.d.ts.map +1 -1
- package/dist/sveltekit/index.js +6142 -0
- package/dist/sveltekit/index.js.map +1 -0
- package/dist/sveltekit/index.mjs +6137 -0
- package/dist/sveltekit/index.mjs.map +1 -0
- package/dist/validation/index.d.ts +2 -0
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/index.js +137 -12
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/index.mjs +116 -13
- package/dist/validation/index.mjs.map +1 -1
- package/dist/validation/redirect.d.ts.map +1 -1
- package/dist/validation/schema.d.ts.map +1 -1
- package/dist/validation/url-async.d.ts +137 -0
- package/dist/validation/url-async.d.ts.map +1 -0
- package/package.json +57 -12
- package/scripts/postinstall.cjs +26 -0
- package/dist/cli/arcis.d.ts +0 -23
- package/dist/cli/arcis.d.ts.map +0 -1
- package/dist/cli/arcis.js +0 -312
- package/dist/cli/arcis.js.map +0 -1
- package/dist/cli/arcis.mjs +0 -309
- package/dist/cli/arcis.mjs.map +0 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arcis/node/koa
|
|
3
|
+
*
|
|
4
|
+
* Koa adapter for Arcis. Returns a Koa middleware function suitable for
|
|
5
|
+
* `app.use(...)`. Rate-limit + bot detection run BEFORE `next()` (the
|
|
6
|
+
* handler); security headers are applied AFTER `next()` so they ride on
|
|
7
|
+
* the buffered response that Koa flushes on its own.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import Koa from 'koa';
|
|
11
|
+
* import { arcisKoa } from '@arcis/node/koa';
|
|
12
|
+
*
|
|
13
|
+
* const app = new Koa();
|
|
14
|
+
* app.use(arcisKoa({
|
|
15
|
+
* rateLimit: { max: 100, windowMs: 60_000 },
|
|
16
|
+
* bot: true,
|
|
17
|
+
* }));
|
|
18
|
+
* app.use(async (ctx) => { ctx.body = { ok: true }; });
|
|
19
|
+
* app.listen(3000);
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* No runtime dependency on `koa` — its types are duck-typed enough to
|
|
23
|
+
* satisfy Koa's actual `Context` shape. Real `Context` is assignable
|
|
24
|
+
* into `KoaContextLike` without imports.
|
|
25
|
+
*/
|
|
26
|
+
import type { HeaderOptions, RateLimitOptions } from '../core/types';
|
|
27
|
+
import { type BotProtectionOptions } from './bot-detection';
|
|
28
|
+
export interface KoaRequestLike {
|
|
29
|
+
headers: Record<string, string | string[] | undefined>;
|
|
30
|
+
ip?: string;
|
|
31
|
+
url?: string;
|
|
32
|
+
method?: string;
|
|
33
|
+
socket?: {
|
|
34
|
+
remoteAddress?: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export interface KoaResponseLike {
|
|
38
|
+
status: number;
|
|
39
|
+
body: unknown;
|
|
40
|
+
set(name: string, value: string): void;
|
|
41
|
+
}
|
|
42
|
+
export interface KoaContextLike {
|
|
43
|
+
request: KoaRequestLike;
|
|
44
|
+
response: KoaResponseLike;
|
|
45
|
+
/**
|
|
46
|
+
* Koa attaches IP at `ctx.ip` (delegating to `ctx.request.ip`). We read
|
|
47
|
+
* either; whichever is set wins.
|
|
48
|
+
*/
|
|
49
|
+
ip?: string;
|
|
50
|
+
/** `ctx.set` shortcuts to `ctx.response.set` in real Koa. */
|
|
51
|
+
set(name: string, value: string): void;
|
|
52
|
+
/** Setting `ctx.status` shortcuts to `ctx.response.status`. */
|
|
53
|
+
status: number;
|
|
54
|
+
/** Setting `ctx.body` shortcuts to `ctx.response.body`. */
|
|
55
|
+
body: unknown;
|
|
56
|
+
}
|
|
57
|
+
export type KoaNext = () => Promise<unknown>;
|
|
58
|
+
export type KoaMiddleware = (ctx: KoaContextLike, next: KoaNext) => Promise<void>;
|
|
59
|
+
export interface ArcisKoaOptions {
|
|
60
|
+
/** Security headers configuration. Default: enabled. Pass `false` to disable. */
|
|
61
|
+
headers?: boolean | HeaderOptions;
|
|
62
|
+
/** Rate limiter configuration. Default: 100 req/60s in-memory. Pass `false` to disable. */
|
|
63
|
+
rateLimit?: boolean | RateLimitOptions;
|
|
64
|
+
/**
|
|
65
|
+
* Bot protection. Default: disabled (opt-in to avoid surprising behavior on
|
|
66
|
+
* legitimate crawlers). Pass `true` for sensible defaults or an options
|
|
67
|
+
* object for full control.
|
|
68
|
+
*/
|
|
69
|
+
bot?: boolean | BotProtectionOptions;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Build a Koa middleware that applies Arcis protections on each request:
|
|
73
|
+
* rate-limit (returns 429 if exceeded), bot detection (returns
|
|
74
|
+
* `botStatusCode` if denied), then `await next()`, then security headers
|
|
75
|
+
* on the buffered response. Order matches the SvelteKit / Fastify /
|
|
76
|
+
* Next.js adapters so cross-framework behavior is consistent.
|
|
77
|
+
*
|
|
78
|
+
* Setting `ctx.status` and `ctx.body` before returning is Koa's idiomatic
|
|
79
|
+
* way to short-circuit a response — we don't call `next()` on the deny
|
|
80
|
+
* path so downstream middleware doesn't run.
|
|
81
|
+
*/
|
|
82
|
+
export declare function arcisKoa(options?: ArcisKoaOptions): KoaMiddleware;
|
|
83
|
+
export default arcisKoa;
|
|
84
|
+
//# sourceMappingURL=koa.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"koa.d.ts","sourceRoot":"","sources":["../../src/middleware/koa.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,KAAK,EACV,aAAa,EAEb,gBAAgB,EACjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,KAAK,oBAAoB,EAE1B,MAAM,iBAAiB,CAAC;AAMzB,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrC;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,cAAc,CAAC;IACxB,QAAQ,EAAE,eAAe,CAAC;IAC1B;;;OAGG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;AAC7C,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAIlF,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,GAAG,aAAa,CAAC;IAClC,2FAA2F;IAC3F,SAAS,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;IACvC;;;;OAIG;IACH,GAAG,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;CACtC;AAgKD;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,aAAa,CAkErE;AAED,eAAe,QAAQ,CAAC"}
|
|
@@ -3,36 +3,6 @@
|
|
|
3
3
|
* Main arcis() middleware factory
|
|
4
4
|
*/
|
|
5
5
|
import type { ArcisOptions, ArcisFunction, ArcisMiddlewareStack } from '../core/types';
|
|
6
|
-
/**
|
|
7
|
-
* Create Arcis middleware with all protections enabled.
|
|
8
|
-
*
|
|
9
|
-
* @param options - Configuration options
|
|
10
|
-
* @returns Array of Express middleware
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* // Full protection (recommended)
|
|
14
|
-
* app.use(arcis());
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
17
|
-
* // Custom configuration
|
|
18
|
-
* app.use(arcis({
|
|
19
|
-
* rateLimit: { max: 50 },
|
|
20
|
-
* headers: { frameOptions: 'SAMEORIGIN' }
|
|
21
|
-
* }));
|
|
22
|
-
*
|
|
23
|
-
* @example
|
|
24
|
-
* // Disable specific features
|
|
25
|
-
* app.use(arcis({
|
|
26
|
-
* rateLimit: false,
|
|
27
|
-
* sanitize: { sql: false }
|
|
28
|
-
* }));
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* // Cleanup on shutdown
|
|
32
|
-
* const middleware = arcis();
|
|
33
|
-
* app.use(middleware);
|
|
34
|
-
* process.on('SIGTERM', () => middleware.close());
|
|
35
|
-
*/
|
|
36
6
|
export declare function arcis(options?: ArcisOptions): ArcisMiddlewareStack;
|
|
37
7
|
declare const arcisWithMethods: ArcisFunction;
|
|
38
8
|
export { arcisWithMethods as arcisFunction };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/middleware/main.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,oBAAoB,
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/middleware/main.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,oBAAoB,EAKrB,MAAM,eAAe,CAAC;AA8KvB,wBAAgB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,oBAAoB,CA6EtE;AAGD,QAAA,MAAM,gBAAgB,EAAY,aAAa,CAAC;AAQhD,OAAO,EAAE,gBAAgB,IAAI,aAAa,EAAE,CAAC;AAC7C,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arcis/node/middleware/mass-assign
|
|
3
|
+
*
|
|
4
|
+
* Mass-assignment runtime guard (sdk-vectors.md tier 1 #25).
|
|
5
|
+
*
|
|
6
|
+
* The classic mass-assignment vulnerability:
|
|
7
|
+
*
|
|
8
|
+
* ```js
|
|
9
|
+
* const user = await User.findOne({ id });
|
|
10
|
+
* Object.assign(user, req.body); // attacker sets req.body.is_admin = true
|
|
11
|
+
* await user.save();
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* This middleware filters `req.body` to a per-route allowlist before
|
|
15
|
+
* the handler runs. Two modes:
|
|
16
|
+
*
|
|
17
|
+
* - `'strip'` (default) — silently drop disallowed keys, continue.
|
|
18
|
+
* - `'reject'` — return 400 with the offending key names.
|
|
19
|
+
*
|
|
20
|
+
* Pair it with the audit rule (`MASS-ASSIGN` in `arcis audit`) for the
|
|
21
|
+
* static-analysis side and the route-level middleware for the runtime
|
|
22
|
+
* side. Audit catches `Object.assign(target, req.body)` patterns at
|
|
23
|
+
* build time; this middleware catches the runtime data flow.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { massAssign } from '@arcis/node';
|
|
27
|
+
*
|
|
28
|
+
* app.post('/users',
|
|
29
|
+
* massAssign({ allow: ['email', 'password', 'name'] }),
|
|
30
|
+
* async (req, res) => {
|
|
31
|
+
* // req.body has been filtered — is_admin / role / created_at all gone.
|
|
32
|
+
* const user = await User.create(req.body);
|
|
33
|
+
* res.json(user);
|
|
34
|
+
* },
|
|
35
|
+
* );
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* Default scope is top-level keys only. Nested objects pass through
|
|
39
|
+
* untouched — that's deliberate: nested allowlists encourage
|
|
40
|
+
* `allow: ['profile.bio', 'profile.avatar']` style strings which
|
|
41
|
+
* become a parser, not a guard. Use a schema validator (Zod / Joi /
|
|
42
|
+
* Arcis's `validate`) when nested filtering is required; this
|
|
43
|
+
* middleware handles the 80% case of "filter req.body for an ORM
|
|
44
|
+
* mass-assign call".
|
|
45
|
+
*/
|
|
46
|
+
import type { RequestHandler } from 'express';
|
|
47
|
+
export interface MassAssignOptions {
|
|
48
|
+
/**
|
|
49
|
+
* Allowlist of permitted top-level keys on `req.body`. Required —
|
|
50
|
+
* a missing or empty array would silently strip every key, almost
|
|
51
|
+
* certainly a configuration mistake.
|
|
52
|
+
*/
|
|
53
|
+
allow: readonly string[];
|
|
54
|
+
/**
|
|
55
|
+
* Behavior when `req.body` contains a key NOT in `allow`:
|
|
56
|
+
* - `'strip'` (default): silently drop the key, continue.
|
|
57
|
+
* - `'reject'`: return `statusCode` (default 400) with a JSON
|
|
58
|
+
* body listing the disallowed keys.
|
|
59
|
+
*/
|
|
60
|
+
mode?: 'strip' | 'reject';
|
|
61
|
+
/** Status code for the reject path. Default: 400. */
|
|
62
|
+
statusCode?: number;
|
|
63
|
+
/** Error message in the reject body. Default: "Disallowed fields". */
|
|
64
|
+
message?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Skip the filter when `req.body` is not a plain object (string,
|
|
67
|
+
* array, FormData, etc.). Default: true. Set to false to surface
|
|
68
|
+
* a 400 ("body must be an object") on those payloads — useful for
|
|
69
|
+
* routes that should ONLY accept JSON objects.
|
|
70
|
+
*/
|
|
71
|
+
passThroughNonObjects?: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build a mass-assignment guard middleware. Runs against `req.body`
|
|
75
|
+
* before the route handler — must be installed AFTER body-parsing
|
|
76
|
+
* middleware (`express.json()` / `express.urlencoded()`) so `req.body`
|
|
77
|
+
* is already populated.
|
|
78
|
+
*/
|
|
79
|
+
export declare function massAssign(options: MassAssignOptions): RequestHandler;
|
|
80
|
+
export default massAssign;
|
|
81
|
+
//# sourceMappingURL=mass-assign.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mass-assign.d.ts","sourceRoot":"","sources":["../../src/middleware/mass-assign.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EAAW,cAAc,EAA0B,MAAM,SAAS,CAAC;AAE/E,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC1B,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AASD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,cAAc,CAgFrE;AAED,eAAe,UAAU,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arcis/node/middleware/method-allowlist
|
|
3
|
+
*
|
|
4
|
+
* HTTP method tampering protection (sdk-vectors.md tier 1 #26).
|
|
5
|
+
*
|
|
6
|
+
* Two related threats:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Disallowed methods** — TRACE leaks Authorization headers (XST);
|
|
9
|
+
* CONNECT is for proxies and shouldn't reach an application server;
|
|
10
|
+
* custom verbs slip past route-handlers that only check `if (req.method
|
|
11
|
+
* === 'POST')`. The middleware rejects anything outside an allowlist
|
|
12
|
+
* with 405.
|
|
13
|
+
*
|
|
14
|
+
* 2. **Method-override bypass** — frameworks that respect
|
|
15
|
+
* `X-HTTP-Method-Override` let an attacker turn a GET into a POST or
|
|
16
|
+
* DELETE, bypassing route-level method checks. The middleware strips
|
|
17
|
+
* these headers BEFORE the route handler sees them.
|
|
18
|
+
*
|
|
19
|
+
* Pair with the bundle middleware:
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { arcis } from '@arcis/node';
|
|
23
|
+
* import { methodAllowlist } from '@arcis/node/middleware/method-allowlist';
|
|
24
|
+
*
|
|
25
|
+
* app.use(methodAllowlist());
|
|
26
|
+
* app.use(arcis());
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* Or standalone for fine-grained mounts:
|
|
30
|
+
*
|
|
31
|
+
* ```ts
|
|
32
|
+
* app.use('/api', methodAllowlist({ allow: ['GET', 'POST'] }));
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
import type { RequestHandler } from 'express';
|
|
36
|
+
export interface MethodAllowlistOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Methods to permit. Each entry is uppercased before comparison so
|
|
39
|
+
* `['get', 'post']` works the same as `['GET', 'POST']`. Defaults to
|
|
40
|
+
* the full standard CRUD set: GET, POST, PUT, DELETE, HEAD, OPTIONS,
|
|
41
|
+
* PATCH. TRACE and CONNECT are intentionally excluded — the former
|
|
42
|
+
* leaks Authorization (XST), the latter is for proxies.
|
|
43
|
+
*/
|
|
44
|
+
allow?: readonly string[];
|
|
45
|
+
/**
|
|
46
|
+
* Strip method-override headers (`X-HTTP-Method-Override`,
|
|
47
|
+
* `X-Method-Override`, `X-HTTP-Method`) before the request reaches the
|
|
48
|
+
* route handler. Default: true. Set to false only if your stack
|
|
49
|
+
* legitimately uses one of these headers for client-method tunnelling
|
|
50
|
+
* AND you've verified each override target is auth-checked
|
|
51
|
+
* independently.
|
|
52
|
+
*/
|
|
53
|
+
stripOverrideHeaders?: boolean;
|
|
54
|
+
/** HTTP status code for the deny response. Default: 405 Method Not Allowed. */
|
|
55
|
+
statusCode?: number;
|
|
56
|
+
/** Error message body. Default: "Method not allowed". */
|
|
57
|
+
message?: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build a method-allowlist middleware. Uppercase-matches `req.method`
|
|
61
|
+
* against the allow set; strips override headers so downstream code
|
|
62
|
+
* can't be tricked into running a different method's logic.
|
|
63
|
+
*/
|
|
64
|
+
export declare function methodAllowlist(options?: MethodAllowlistOptions): RequestHandler;
|
|
65
|
+
export default methodAllowlist;
|
|
66
|
+
//# sourceMappingURL=method-allowlist.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"method-allowlist.d.ts","sourceRoot":"","sources":["../../src/middleware/method-allowlist.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAuB9C,MAAM,WAAW,sBAAsB;IACrC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAE1B;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAE/B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,yDAAyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B,GAAG,cAAc,CAgCpF;AAED,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arcis/node/nestjs
|
|
3
|
+
*
|
|
4
|
+
* NestJS adapter for Arcis.
|
|
5
|
+
*
|
|
6
|
+
* Two ways to wire this up:
|
|
7
|
+
*
|
|
8
|
+
* 1. Global functional middleware (zero NestJS knowledge required):
|
|
9
|
+
* ```ts
|
|
10
|
+
* // main.ts
|
|
11
|
+
* import { NestFactory } from '@nestjs/core';
|
|
12
|
+
* import { arcis } from '@arcis/node';
|
|
13
|
+
* const app = await NestFactory.create(AppModule);
|
|
14
|
+
* app.use(arcis({ block: true }));
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* 2. DI-aware module + class middleware (per-route control):
|
|
18
|
+
* ```ts
|
|
19
|
+
* // app.module.ts
|
|
20
|
+
* import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
|
|
21
|
+
* import { ArcisModule, ArcisMiddleware } from '@arcis/node/nestjs';
|
|
22
|
+
*
|
|
23
|
+
* @Module({ imports: [ArcisModule.forRoot({ block: true })] })
|
|
24
|
+
* export class AppModule implements NestModule {
|
|
25
|
+
* configure(consumer: MiddlewareConsumer) {
|
|
26
|
+
* consumer.apply(ArcisMiddleware).forRoutes('*');
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* No runtime dependency on `@nestjs/common`: the only NestJS reference is a
|
|
32
|
+
* type-only import erased at compile time. NestJS users already have
|
|
33
|
+
* `@nestjs/common` installed; non-NestJS users pay nothing.
|
|
34
|
+
*/
|
|
35
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
36
|
+
import type { DynamicModule } from '@nestjs/common';
|
|
37
|
+
import type { ArcisOptions } from '../core/types';
|
|
38
|
+
/** Injection token for `ArcisOptions` consumed by `ArcisMiddleware`'s factory. */
|
|
39
|
+
export declare const ARCIS_OPTIONS: unique symbol;
|
|
40
|
+
/**
|
|
41
|
+
* NestJS-compatible class middleware. Implements the structural shape NestJS
|
|
42
|
+
* expects (`.use(req, res, next)`) without importing `NestMiddleware` at
|
|
43
|
+
* runtime. Internally builds the same handler stack as `arcis()` and walks it
|
|
44
|
+
* sequentially, propagating errors and short-circuit responses correctly.
|
|
45
|
+
*/
|
|
46
|
+
export declare class ArcisMiddleware {
|
|
47
|
+
private readonly handlers;
|
|
48
|
+
constructor(options?: ArcisOptions);
|
|
49
|
+
use(req: Request, res: Response, next: NextFunction): void;
|
|
50
|
+
/** Release rate-limiter intervals etc. Call from `OnApplicationShutdown`. */
|
|
51
|
+
close(): void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* NestJS dynamic module. `ArcisModule.forRoot(options)` is the entry point.
|
|
55
|
+
* Returns a plain `DynamicModule` literal so `@Module({})` is unnecessary on
|
|
56
|
+
* `ArcisModule` itself; this keeps `@nestjs/common` purely a type-only import.
|
|
57
|
+
*/
|
|
58
|
+
export declare class ArcisModule {
|
|
59
|
+
static forRoot(options?: ArcisOptions): DynamicModule;
|
|
60
|
+
}
|
|
61
|
+
export default ArcisModule;
|
|
62
|
+
//# sourceMappingURL=nestjs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nestjs.d.ts","sourceRoot":"","sources":["../../src/middleware/nestjs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAkB,MAAM,SAAS,CAAC;AAC/E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,OAAO,KAAK,EAAE,YAAY,EAAwB,MAAM,eAAe,CAAC;AAExE,kFAAkF;AAClF,eAAO,MAAM,aAAa,eAA0B,CAAC;AAErD;;;;;GAKG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;gBAEpC,OAAO,GAAE,YAAiB;IAItC,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI;IAsB1D,6EAA6E;IAC7E,KAAK,IAAI,IAAI;CAGd;AAED;;;;GAIG;AACH,qBAAa,WAAW;IACtB,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,YAAiB,GAAG,aAAa;CAc1D;AAED,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arcis/node/nextjs
|
|
3
|
+
*
|
|
4
|
+
* Next.js adapter for Arcis. Two entry points covering the modern Next.js
|
|
5
|
+
* stack (Edge Middleware + App Router route handlers):
|
|
6
|
+
*
|
|
7
|
+
* **1. Edge Middleware (`middleware.ts` at the project root):**
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { arcisMiddleware } from '@arcis/node/nextjs';
|
|
11
|
+
* import { NextResponse } from 'next/server';
|
|
12
|
+
*
|
|
13
|
+
* const arcis = arcisMiddleware({
|
|
14
|
+
* rateLimit: { max: 100, windowMs: 60_000 },
|
|
15
|
+
* bot: true,
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* export default async function middleware(request: Request) {
|
|
19
|
+
* const blocked = await arcis(request);
|
|
20
|
+
* if (blocked) return blocked;
|
|
21
|
+
* return NextResponse.next();
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* The returned function inspects the request and returns either:
|
|
26
|
+
* - a `Response` (rate-limited 429 / bot-blocked 403) to short-circuit, or
|
|
27
|
+
* - `undefined` to let the request proceed.
|
|
28
|
+
*
|
|
29
|
+
* The caller decides what "proceed" means — `NextResponse.next()` for Edge
|
|
30
|
+
* Middleware, or a re-thrown handler call for custom plumbing. Keeping the
|
|
31
|
+
* allow-path explicit avoids importing `next/server` from the adapter.
|
|
32
|
+
*
|
|
33
|
+
* **2. App Router route handlers (`app/api/.../route.ts`):**
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { arcisProtect } from '@arcis/node/nextjs';
|
|
37
|
+
*
|
|
38
|
+
* export const POST = arcisProtect(
|
|
39
|
+
* async (request: Request) => Response.json({ ok: true }),
|
|
40
|
+
* { rateLimit: { max: 100 }, bot: true },
|
|
41
|
+
* );
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* The wrapper runs the same allow / deny pipeline as `arcisMiddleware`,
|
|
45
|
+
* then on the allow path calls the handler, mutates the resulting
|
|
46
|
+
* Response's headers with security defaults, and returns it.
|
|
47
|
+
*
|
|
48
|
+
* Pages-router API routes (`pages/api/...`) use Node-style req/res rather
|
|
49
|
+
* than the Web Fetch shape; for those, drop the standard Express adapter
|
|
50
|
+
* (`arcis()` from the package root) into `app.use(...)` of a custom server,
|
|
51
|
+
* or migrate to the App Router for first-party support.
|
|
52
|
+
*
|
|
53
|
+
* No runtime dependency on `next` — the adapter speaks Web Fetch
|
|
54
|
+
* `Request`/`Response` directly. NextRequest extends Request and
|
|
55
|
+
* NextResponse extends Response, so both are assignable into / out of this
|
|
56
|
+
* surface without imports.
|
|
57
|
+
*/
|
|
58
|
+
import type { HeaderOptions, RateLimitOptions } from '../core/types';
|
|
59
|
+
import { type BotProtectionOptions } from './bot-detection';
|
|
60
|
+
export interface ArcisNextOptions {
|
|
61
|
+
/** Security headers configuration. Default: enabled. Pass `false` to disable. */
|
|
62
|
+
headers?: boolean | HeaderOptions;
|
|
63
|
+
/** Rate limiter configuration. Default: 100 req/60s in-memory. Pass `false` to disable. */
|
|
64
|
+
rateLimit?: boolean | RateLimitOptions;
|
|
65
|
+
/**
|
|
66
|
+
* Bot protection. Default: disabled (opt-in to avoid surprising behavior on
|
|
67
|
+
* legitimate crawlers). Pass `true` for sensible defaults or an options
|
|
68
|
+
* object for full control.
|
|
69
|
+
*/
|
|
70
|
+
bot?: boolean | BotProtectionOptions;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build a Next.js Edge Middleware factory. The returned function runs the
|
|
74
|
+
* Arcis allow / deny pipeline against the request and returns a `Response`
|
|
75
|
+
* to short-circuit OR `undefined` to indicate "let the request proceed."
|
|
76
|
+
*
|
|
77
|
+
* The caller is responsible for the proceed-path (typically
|
|
78
|
+
* `NextResponse.next()` from `next/server`). This split keeps the adapter
|
|
79
|
+
* dependency-free of `next/server` while preserving clean Edge Middleware
|
|
80
|
+
* ergonomics.
|
|
81
|
+
*
|
|
82
|
+
* Security headers are NOT applied here — Edge Middleware can't easily
|
|
83
|
+
* mutate the response body's headers without consuming the body. For
|
|
84
|
+
* security-headers-on-response, use `arcisProtect` on the route handler.
|
|
85
|
+
*/
|
|
86
|
+
export declare function arcisMiddleware(options?: ArcisNextOptions): (request: Request) => Promise<Response | undefined>;
|
|
87
|
+
/**
|
|
88
|
+
* Wrap an App Router route handler with the Arcis pipeline. Runs rate-limit
|
|
89
|
+
* + bot checks BEFORE the handler, then security headers on the response
|
|
90
|
+
* AFTER. The `...args` spread preserves the second-arg shape route handlers
|
|
91
|
+
* receive, e.g. `(request, { params })` for dynamic routes.
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* export const GET = arcisProtect(
|
|
95
|
+
* async (request, { params }) => Response.json({ id: params.id }),
|
|
96
|
+
* { rateLimit: { max: 50 } },
|
|
97
|
+
* );
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export declare function arcisProtect<TArgs extends unknown[]>(handler: (request: Request, ...args: TArgs) => Promise<Response> | Response, options?: ArcisNextOptions): (request: Request, ...args: TArgs) => Promise<Response>;
|
|
101
|
+
export default arcisMiddleware;
|
|
102
|
+
//# sourceMappingURL=nextjs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nextjs.d.ts","sourceRoot":"","sources":["../../src/middleware/nextjs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AAIH,OAAO,KAAK,EACV,aAAa,EAEb,gBAAgB,EACjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,KAAK,oBAAoB,EAE1B,MAAM,iBAAiB,CAAC;AAIzB,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,GAAG,aAAa,CAAC;IAClC,2FAA2F;IAC3F,SAAS,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;IACvC;;;;OAIG;IACH,GAAG,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;CACtC;AA4PD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,OAAO,GAAE,gBAAqB,GAC7B,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAMrD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,KAAK,SAAS,OAAO,EAAE,EAClD,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,EAC3E,OAAO,GAAE,gBAAqB,GAC7B,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,QAAQ,CAAC,CAezD;AAED,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arcis/node/nuxt
|
|
3
|
+
*
|
|
4
|
+
* Nuxt (h3) adapter for Arcis. Drop into a server middleware file:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* // server/middleware/arcis.ts
|
|
8
|
+
* import { defineEventHandler } from 'h3';
|
|
9
|
+
* import { arcisHandler } from '@arcis/node/nuxt';
|
|
10
|
+
* export default defineEventHandler(arcisHandler({ rateLimit: { max: 100 } }));
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Or compose with other handlers — each Nuxt server middleware file runs in
|
|
14
|
+
* the order Nitro discovers them (alphabetical by default).
|
|
15
|
+
*
|
|
16
|
+
* The adapter operates against h3's Node-compat layer (`event.node.req` /
|
|
17
|
+
* `event.node.res`) so it works on the standard Nuxt server. There is no
|
|
18
|
+
* runtime dependency on `h3` — its event shape is duck-typed.
|
|
19
|
+
*/
|
|
20
|
+
import type { HeaderOptions, RateLimitOptions } from '../core/types';
|
|
21
|
+
import { type BotProtectionOptions } from './bot-detection';
|
|
22
|
+
interface NodeIncomingMessageLike {
|
|
23
|
+
headers: Record<string, string | string[] | undefined>;
|
|
24
|
+
socket?: {
|
|
25
|
+
remoteAddress?: string;
|
|
26
|
+
};
|
|
27
|
+
method?: string;
|
|
28
|
+
url?: string;
|
|
29
|
+
}
|
|
30
|
+
interface NodeServerResponseLike {
|
|
31
|
+
statusCode: number;
|
|
32
|
+
writableEnded?: boolean;
|
|
33
|
+
setHeader(name: string, value: string | number | string[]): void;
|
|
34
|
+
removeHeader(name: string): void;
|
|
35
|
+
end(body?: string): void;
|
|
36
|
+
}
|
|
37
|
+
export interface H3EventLike {
|
|
38
|
+
node: {
|
|
39
|
+
req: NodeIncomingMessageLike;
|
|
40
|
+
res: NodeServerResponseLike;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export type ArcisH3Handler = (event: H3EventLike) => Promise<void> | void;
|
|
44
|
+
export interface ArcisNuxtOptions {
|
|
45
|
+
/** Security headers configuration. Default: enabled. Pass `false` to disable. */
|
|
46
|
+
headers?: boolean | HeaderOptions;
|
|
47
|
+
/** Rate limiter configuration. Default: 100 req/60s in-memory. Pass `false` to disable. */
|
|
48
|
+
rateLimit?: boolean | RateLimitOptions;
|
|
49
|
+
/** Bot protection. Default: disabled (opt-in). */
|
|
50
|
+
bot?: boolean | BotProtectionOptions;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build an h3 event handler that applies Arcis protections on each request.
|
|
54
|
+
* Wrap it with `defineEventHandler` in your Nuxt server middleware. When a
|
|
55
|
+
* request is rate-limited or denied, the handler writes the 429/403 response
|
|
56
|
+
* directly via `event.node.res` and returns; otherwise it sets security
|
|
57
|
+
* headers and falls through to the next handler.
|
|
58
|
+
*/
|
|
59
|
+
export declare function arcisHandler(options?: ArcisNuxtOptions): ArcisH3Handler;
|
|
60
|
+
export default arcisHandler;
|
|
61
|
+
//# sourceMappingURL=nuxt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nuxt.d.ts","sourceRoot":"","sources":["../../src/middleware/nuxt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,EACV,aAAa,EAEb,gBAAgB,EACjB,MAAM,eAAe,CAAC;AACvB,OAAO,EAEL,KAAK,oBAAoB,EAE1B,MAAM,iBAAiB,CAAC;AAIzB,UAAU,uBAAuB;IAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,UAAU,sBAAsB;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IACjE,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE;QACJ,GAAG,EAAE,uBAAuB,CAAC;QAC7B,GAAG,EAAE,sBAAsB,CAAC;KAC7B,CAAC;CACH;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAI1E,MAAM,WAAW,gBAAgB;IAC/B,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,GAAG,aAAa,CAAC;IAClC,2FAA2F;IAC3F,SAAS,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;IACvC,kDAAkD;IAClD,GAAG,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAC;CACtC;AA8HD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,gBAAqB,GAAG,cAAc,CA8D3E;AAED,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arcis/node/middleware/overload
|
|
3
|
+
*
|
|
4
|
+
* Event-loop overload protection (sdk-vectors.md tier 1 #30, issue #51).
|
|
5
|
+
*
|
|
6
|
+
* Rate limiting caps per-client load; this middleware caps **total server
|
|
7
|
+
* load** by sampling event-loop lag and shedding new requests with 503
|
|
8
|
+
* when the loop is saturated. Pairs with rate-limit (per-client) +
|
|
9
|
+
* `methodAllowlist` (per-route) to give Arcis three orthogonal layers
|
|
10
|
+
* of "this server is healthy enough to do work" gating.
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { eventLoopProtection } from '@arcis/node';
|
|
14
|
+
*
|
|
15
|
+
* app.use(eventLoopProtection({
|
|
16
|
+
* maxLagMs: 500, // 503 above this smoothed lag
|
|
17
|
+
* sampleIntervalMs: 250, // measure every 250ms
|
|
18
|
+
* }));
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Sampling strategy:
|
|
22
|
+
* - `setInterval(fn, sampleIntervalMs)` is scheduled; the callback
|
|
23
|
+
* compares wall-clock elapsed against the configured interval. Lag
|
|
24
|
+
* IS the difference: when the loop is busy, the timer fires late.
|
|
25
|
+
* - The interval handle is `.unref()`-ed so the process can exit
|
|
26
|
+
* cleanly even if the caller forgets to call `close()`.
|
|
27
|
+
* - Exponential moving average (`smoothed = 0.7 * smoothed + 0.3 *
|
|
28
|
+
* measured`) smooths out single-sample spikes — a 600ms GC pause
|
|
29
|
+
* shouldn't cause every request to 503 for the next sample window.
|
|
30
|
+
*
|
|
31
|
+
* Implementation deliberately picks `setTimeout` measurement over
|
|
32
|
+
* `perf_hooks.monitorEventLoopDelay` (Node 12+ alternative) for two
|
|
33
|
+
* reasons: (1) it works on every supported Node version with no feature
|
|
34
|
+
* detection branch, (2) the perf_hooks histogram returns nanosecond
|
|
35
|
+
* lag from a 10ms-resolution sampler, but applying the spec's EMA
|
|
36
|
+
* formula on that signal would skew toward unreal lag values that the
|
|
37
|
+
* sampler smoothed away. Keeping the sampler the spec calls for keeps
|
|
38
|
+
* the math testable.
|
|
39
|
+
*/
|
|
40
|
+
import type { RequestHandler } from 'express';
|
|
41
|
+
export interface EventLoopProtectionOptions {
|
|
42
|
+
/** Smoothed lag threshold in ms above which the middleware returns 503. Default: 500. */
|
|
43
|
+
maxLagMs?: number;
|
|
44
|
+
/** Sample frequency in ms. Default: 250. Lower = more responsive, higher = less overhead. */
|
|
45
|
+
sampleIntervalMs?: number;
|
|
46
|
+
/** Status code to return when overloaded. Default: 503. */
|
|
47
|
+
statusCode?: number;
|
|
48
|
+
/** Error message in the response body. Default: "Server overloaded, please retry". */
|
|
49
|
+
message?: string;
|
|
50
|
+
/** Retry-After header value in seconds. Default: 5. */
|
|
51
|
+
retryAfterSeconds?: number;
|
|
52
|
+
/**
|
|
53
|
+
* EMA smoothing factor for the new measurement. Default: 0.3 (per
|
|
54
|
+
* issue #51 spec). Must be in (0, 1]; lower values smooth harder
|
|
55
|
+
* (longer memory of past samples), higher values track more reactively.
|
|
56
|
+
*/
|
|
57
|
+
alpha?: number;
|
|
58
|
+
/**
|
|
59
|
+
* When true, every response gets `X-EventLoop-Lag: <ms>` so monitoring
|
|
60
|
+
* can graph saturation independent of the deny decision. Off by default
|
|
61
|
+
* because most apps don't need it and an extra header on every response
|
|
62
|
+
* adds noise.
|
|
63
|
+
*/
|
|
64
|
+
exposeLagHeader?: boolean;
|
|
65
|
+
}
|
|
66
|
+
export interface EventLoopProtectionMiddleware extends RequestHandler {
|
|
67
|
+
/**
|
|
68
|
+
* Stop the sampler. Call from a SIGTERM handler so the interval doesn't
|
|
69
|
+
* keep a misconfigured process alive. Idempotent: subsequent calls are
|
|
70
|
+
* no-ops.
|
|
71
|
+
*/
|
|
72
|
+
close(): void;
|
|
73
|
+
/**
|
|
74
|
+
* Read the current smoothed lag in ms. Useful for tests that want to
|
|
75
|
+
* assert the smoothing math without mocking timers, and for callers
|
|
76
|
+
* who want to expose the value through a different surface (Prometheus,
|
|
77
|
+
* dashboard panel, etc.).
|
|
78
|
+
*/
|
|
79
|
+
currentLagMs(): number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build an event-loop protection middleware. Returns a request handler
|
|
83
|
+
* with `close()` and `currentLagMs()` attached (Pattern 6 in the
|
|
84
|
+
* monorepo's middleware conventions: factories return a callable
|
|
85
|
+
* augmented with cleanup helpers).
|
|
86
|
+
*/
|
|
87
|
+
export declare function eventLoopProtection(options?: EventLoopProtectionOptions): EventLoopProtectionMiddleware;
|
|
88
|
+
export default eventLoopProtection;
|
|
89
|
+
export declare const __test: {
|
|
90
|
+
ema(prior: number, measured: number, alpha: number): number;
|
|
91
|
+
};
|
|
92
|
+
//# sourceMappingURL=overload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overload.d.ts","sourceRoot":"","sources":["../../src/middleware/overload.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAY,MAAM,SAAS,CAAC;AAExD,MAAM,WAAW,0BAA0B;IACzC,yFAAyF;IACzF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6FAA6F;IAC7F,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,2DAA2D;IAC3D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sFAAsF;IACtF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,6BAA8B,SAAQ,cAAc;IACnE;;;;OAIG;IACH,KAAK,IAAI,IAAI,CAAC;IACd;;;;;OAKG;IACH,YAAY,IAAI,MAAM,CAAC;CACxB;AAWD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,0BAA+B,GACvC,6BAA6B,CAoE/B;AAED,eAAe,mBAAmB,CAAC;AAInC,eAAO,MAAM,MAAM;eACN,MAAM,YAAY,MAAM,SAAS,MAAM,GAAG,MAAM;CAG5D,CAAC"}
|