@maroonedsoftware/koa 1.0.0 → 1.1.0
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/README.md +100 -53
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +68 -10
- package/dist/index.js.map +1 -1
- package/dist/middleware/router/body.parser.middleware.d.ts +15 -0
- package/dist/middleware/router/body.parser.middleware.d.ts.map +1 -1
- package/dist/middleware/server/cors.middleware.d.ts +19 -0
- package/dist/middleware/server/cors.middleware.d.ts.map +1 -0
- package/dist/middleware/server/error.middleware.d.ts +7 -0
- package/dist/middleware/server/error.middleware.d.ts.map +1 -1
- package/dist/middleware/server/rate.limiter.middleware.d.ts +11 -0
- package/dist/middleware/server/rate.limiter.middleware.d.ts.map +1 -0
- package/dist/middleware/server/serverkit.context.middleware.d.ts +13 -0
- package/dist/middleware/server/serverkit.context.middleware.d.ts.map +1 -0
- package/dist/serverkit.context.d.ts +18 -0
- package/dist/serverkit.context.d.ts.map +1 -1
- package/dist/serverkit.middleware.d.ts +8 -0
- package/dist/serverkit.middleware.d.ts.map +1 -1
- package/dist/serverkit.router.d.ts +8 -0
- package/dist/serverkit.router.d.ts.map +1 -1
- package/package.json +12 -9
- package/dist/middleware/server/injectkit.middleware.d.ts +0 -4
- package/dist/middleware/server/injectkit.middleware.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,91 +1,138 @@
|
|
|
1
|
-
# @maroonedsoftware/
|
|
1
|
+
# @maroonedsoftware/koa
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Koa utilities and middleware for ServerKit: typed context, router, CORS, error handling, rate limiting, body parsing, and request-scoped DI via [injectkit](https://www.npmjs.com/package/injectkit).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
pnpm add @maroonedsoftware/
|
|
8
|
+
pnpm add @maroonedsoftware/koa koa @koa/router @koa/cors
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
### Basic Usage
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
import { ConsoleLogger } from '@maroonedsoftware/logger';
|
|
11
|
+
Peer dependencies: `koa`, `@koa/router`, `@koa/cors`.
|
|
17
12
|
|
|
18
|
-
|
|
13
|
+
## Features
|
|
19
14
|
|
|
20
|
-
logger
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
logger
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
- **ServerKitContext** — Koa context extended with `container`, `logger`, `requestId`, `correlationId`, and related request metadata
|
|
16
|
+
- **ServerKitRouter** — Router typed for `ServerKitContext`
|
|
17
|
+
- **ServerKitMiddleware** — Middleware type bound to `ServerKitContext`
|
|
18
|
+
- **serverKitContextMiddleware** — Populates context with scoped container, logger, and request/correlation IDs
|
|
19
|
+
- **corsMiddleware** — CORS headers with `'*'`, string, or RegExp origin matching
|
|
20
|
+
- **errorMiddleware** — Central error handler; maps HTTP errors to status/body, 404 for unmatched routes, 500 for unknown errors
|
|
21
|
+
- **rateLimiterMiddleware** — Per-IP rate limiting via `rate-limiter-flexible` (429 when exceeded)
|
|
22
|
+
- **bodyParserMiddleware** — Parses JSON, form, text, multipart, or raw body by allowed content types
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
## Usage
|
|
28
25
|
|
|
29
|
-
|
|
26
|
+
### Basic setup
|
|
30
27
|
|
|
31
28
|
```typescript
|
|
32
|
-
import '
|
|
29
|
+
import Koa from 'koa';
|
|
33
30
|
import { InjectKitRegistry } from 'injectkit';
|
|
34
31
|
import { Logger, ConsoleLogger } from '@maroonedsoftware/logger';
|
|
32
|
+
import { ServerKitRouter, serverKitContextMiddleware, corsMiddleware, errorMiddleware, bodyParserMiddleware } from '@maroonedsoftware/koa';
|
|
35
33
|
|
|
36
|
-
// Set up dependency injection registry
|
|
37
34
|
const diRegistry = new InjectKitRegistry();
|
|
38
35
|
diRegistry.register(Logger).useClass(ConsoleLogger).asSingleton();
|
|
39
|
-
|
|
40
|
-
// Build the container
|
|
41
36
|
const container = diRegistry.build();
|
|
42
37
|
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
logger.info('Application started');
|
|
46
|
-
```
|
|
38
|
+
const app = new Koa();
|
|
39
|
+
const router = new ServerKitRouter();
|
|
47
40
|
|
|
48
|
-
|
|
41
|
+
app.use(errorMiddleware());
|
|
42
|
+
app.use(serverKitContextMiddleware(container));
|
|
43
|
+
app.use(corsMiddleware({ origin: ['*'] }));
|
|
49
44
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
router.post('/api/echo', bodyParserMiddleware(['application/json']), async ctx => {
|
|
46
|
+
ctx.body = { echoed: ctx.body, requestId: ctx.requestId };
|
|
47
|
+
});
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
class MyService {
|
|
56
|
-
constructor(private logger: Logger) {}
|
|
49
|
+
app.use(router.routes()).use(router.allowedMethods());
|
|
57
50
|
|
|
58
|
-
|
|
59
|
-
this.logger.info('Doing something');
|
|
60
|
-
}
|
|
61
|
-
}
|
|
51
|
+
app.listen(3000);
|
|
62
52
|
```
|
|
63
53
|
|
|
64
|
-
|
|
54
|
+
### Route handlers with ServerKitContext
|
|
55
|
+
|
|
56
|
+
Handlers receive `ctx` as `ServerKitContext` with `ctx.container`, `ctx.logger`, `ctx.requestId`, `ctx.correlationId`, and `ctx.userAgent`:
|
|
65
57
|
|
|
66
|
-
|
|
58
|
+
```typescript
|
|
59
|
+
router.get('/api/users/:id', async ctx => {
|
|
60
|
+
ctx.logger.info('Fetching user', { id: ctx.params.id });
|
|
61
|
+
const user = await ctx.container.get(UserService).findById(ctx.params.id);
|
|
62
|
+
if (!user) throw httpError(404);
|
|
63
|
+
ctx.body = user;
|
|
64
|
+
});
|
|
65
|
+
```
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
| --------------------------- | ----------------------------- |
|
|
70
|
-
| `error(message, ...params)` | Logs an error message |
|
|
71
|
-
| `warn(message, ...params)` | Logs a warning message |
|
|
72
|
-
| `info(message, ...params)` | Logs an informational message |
|
|
73
|
-
| `debug(message, ...params)` | Logs a debug message |
|
|
74
|
-
| `trace(message, ...params)` | Logs a trace message |
|
|
67
|
+
### CORS
|
|
75
68
|
|
|
76
|
-
|
|
69
|
+
```typescript
|
|
70
|
+
// Allow all origins
|
|
71
|
+
app.use(corsMiddleware({ origin: ['*'] }));
|
|
72
|
+
|
|
73
|
+
// Single origin
|
|
74
|
+
app.use(corsMiddleware({ origin: ['https://app.example.com'] }));
|
|
75
|
+
|
|
76
|
+
// Multiple origins or RegExps
|
|
77
|
+
app.use(
|
|
78
|
+
corsMiddleware({
|
|
79
|
+
origin: ['https://app.example.com', /^https:\/\/.*\.example\.com$/],
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
```
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
### Rate limiting
|
|
79
85
|
|
|
80
86
|
```typescript
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
import { RateLimiterMemory } from 'rate-limiter-flexible';
|
|
88
|
+
import { rateLimiterMiddleware } from '@maroonedsoftware/koa';
|
|
89
|
+
|
|
90
|
+
const rateLimiter = new RateLimiterMemory({
|
|
91
|
+
points: 100,
|
|
92
|
+
duration: 60,
|
|
93
|
+
});
|
|
94
|
+
app.use(rateLimiterMiddleware(rateLimiter));
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Body parser
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
Allow specific content types; empty array disallows any body. Supports JSON, urlencoded, text, multipart, and raw (e.g. PDF).
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
router.post('/api/upload', bodyParserMiddleware(['multipart/form-data']), async ctx => {
|
|
103
|
+
const body = ctx.body as MultipartBody;
|
|
104
|
+
// ...
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
router.post('/api/json', bodyParserMiddleware(['application/json']), async ctx => {
|
|
108
|
+
const data = ctx.body as Record<string, unknown>;
|
|
109
|
+
// ...
|
|
110
|
+
});
|
|
87
111
|
```
|
|
88
112
|
|
|
113
|
+
## API
|
|
114
|
+
|
|
115
|
+
### ServerKitContext
|
|
116
|
+
|
|
117
|
+
| Property | Type | Description |
|
|
118
|
+
| --------------- | ----------- | ------------------------------------ |
|
|
119
|
+
| `container` | `Container` | Request-scoped injectkit container |
|
|
120
|
+
| `logger` | `Logger` | Request-scoped logger |
|
|
121
|
+
| `loggerName` | `string` | Logger name (e.g. request path) |
|
|
122
|
+
| `userAgent` | `string` | `User-Agent` header value |
|
|
123
|
+
| `correlationId` | `string` | From `X-Correlation-Id` or generated |
|
|
124
|
+
| `requestId` | `string` | From `X-Request-Id` or generated |
|
|
125
|
+
|
|
126
|
+
### Middleware
|
|
127
|
+
|
|
128
|
+
| Middleware | Description |
|
|
129
|
+
| --------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
130
|
+
| `serverKitContextMiddleware(container)` | Sets `ctx.container`, `ctx.logger`, IDs; sets `X-Correlation-Id`, `X-Request-Id` response headers |
|
|
131
|
+
| `corsMiddleware(options?)` | CORS via `@koa/cors`; `origin`: `'*'`, string, or `(string \| RegExp)[]` |
|
|
132
|
+
| `errorMiddleware()` | Catches errors, maps HTTP errors to status/body, 404/500, emits app events |
|
|
133
|
+
| `rateLimiterMiddleware(rateLimiter)` | Consumes one token per request by IP; throws 429 when exceeded |
|
|
134
|
+
| `bodyParserMiddleware(contentTypes)` | Parses body by allowed MIME types; throws 400/411/415/422 on invalid input |
|
|
135
|
+
|
|
89
136
|
## License
|
|
90
137
|
|
|
91
138
|
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export * from './serverkit.context.js';
|
|
2
2
|
export * from './serverkit.middleware.js';
|
|
3
3
|
export * from './serverkit.router.js';
|
|
4
|
+
export * from './middleware/server/cors.middleware.js';
|
|
4
5
|
export * from './middleware/server/error.middleware.js';
|
|
5
|
-
export * from './middleware/server/
|
|
6
|
+
export * from './middleware/server/rate.limiter.middleware.js';
|
|
7
|
+
export * from './middleware/server/serverkit.context.middleware.js';
|
|
6
8
|
export * from './middleware/router/body.parser.middleware.js';
|
|
7
9
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,yCAAyC,CAAC;AACxD,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,wCAAwC,CAAC;AACvD,cAAc,yCAAyC,CAAC;AACxD,cAAc,gDAAgD,CAAC;AAC/D,cAAc,qDAAqD,CAAC;AACpE,cAAc,+CAA+C,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,40 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
|
|
|
5
5
|
import Router from "@koa/router";
|
|
6
6
|
var ServerKitRouter = /* @__PURE__ */ __name(() => new Router(), "ServerKitRouter");
|
|
7
7
|
|
|
8
|
+
// src/middleware/server/cors.middleware.ts
|
|
9
|
+
import cors from "@koa/cors";
|
|
10
|
+
var corsMiddleware = /* @__PURE__ */ __name((options) => {
|
|
11
|
+
const originMatcher = /* @__PURE__ */ __name((ctx) => {
|
|
12
|
+
const origin = ctx.get("origin");
|
|
13
|
+
const matchers = options?.origin ?? [
|
|
14
|
+
"*"
|
|
15
|
+
];
|
|
16
|
+
for (const matcher of matchers) {
|
|
17
|
+
if (matcher === "*") {
|
|
18
|
+
return origin;
|
|
19
|
+
}
|
|
20
|
+
if (typeof matcher === "string") {
|
|
21
|
+
if (matcher === origin) {
|
|
22
|
+
return origin;
|
|
23
|
+
}
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (matcher.test(origin)) {
|
|
27
|
+
return origin;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return "";
|
|
31
|
+
}, "originMatcher");
|
|
32
|
+
return cors({
|
|
33
|
+
...options,
|
|
34
|
+
origin: originMatcher,
|
|
35
|
+
allowMethods: options?.allowMethods ?? "GET,HEAD,PUT,POST,DELETE,PATCH",
|
|
36
|
+
secureContext: options?.secureContext ?? false,
|
|
37
|
+
keepHeadersOnError: options?.keepHeadersOnError ?? false,
|
|
38
|
+
privateNetworkAccess: options?.privateNetworkAccess ?? false
|
|
39
|
+
});
|
|
40
|
+
}, "corsMiddleware");
|
|
41
|
+
|
|
8
42
|
// src/middleware/server/error.middleware.ts
|
|
9
43
|
import { IsHttpError } from "@maroonedsoftware/errors";
|
|
10
44
|
var errorMiddleware = /* @__PURE__ */ __name(() => {
|
|
@@ -48,33 +82,55 @@ var errorMiddleware = /* @__PURE__ */ __name(() => {
|
|
|
48
82
|
};
|
|
49
83
|
}, "errorMiddleware");
|
|
50
84
|
|
|
51
|
-
// src/middleware/server/
|
|
85
|
+
// src/middleware/server/rate.limiter.middleware.ts
|
|
86
|
+
import { httpError } from "@maroonedsoftware/errors";
|
|
87
|
+
var rateLimiterMiddleware = /* @__PURE__ */ __name((rateLimiter) => {
|
|
88
|
+
return async (ctx, next) => {
|
|
89
|
+
try {
|
|
90
|
+
await rateLimiter.consume(ctx.ip);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw httpError(429).withCause(error);
|
|
93
|
+
}
|
|
94
|
+
await next();
|
|
95
|
+
};
|
|
96
|
+
}, "rateLimiterMiddleware");
|
|
97
|
+
|
|
98
|
+
// src/middleware/server/serverkit.context.middleware.ts
|
|
99
|
+
import crypto from "crypto";
|
|
52
100
|
import { Logger } from "@maroonedsoftware/logger";
|
|
53
|
-
var
|
|
101
|
+
var serverKitContextMiddleware = /* @__PURE__ */ __name((container) => {
|
|
54
102
|
return async (ctx, next) => {
|
|
55
103
|
ctx.container = container.createScopedContainer();
|
|
56
104
|
ctx.logger = container.get(Logger);
|
|
105
|
+
ctx.loggerName = ctx.path;
|
|
106
|
+
ctx.userAgent = ctx.get("user-agent") ?? "";
|
|
107
|
+
ctx.correlationId = ctx.get("x-correlation-id") ?? crypto.randomUUID();
|
|
108
|
+
ctx.requestId = ctx.get("x-request-id") ?? crypto.randomUUID();
|
|
109
|
+
ctx.headers["x-correlation-id"] = ctx.correlationId;
|
|
110
|
+
ctx.set("x-correlation-id", ctx.correlationId);
|
|
111
|
+
ctx.headers["x-request-id"] = ctx.requestId;
|
|
112
|
+
ctx.set("x-request-id", ctx.requestId);
|
|
57
113
|
await next();
|
|
58
114
|
};
|
|
59
|
-
}, "
|
|
115
|
+
}, "serverKitContextMiddleware");
|
|
60
116
|
|
|
61
117
|
// src/middleware/router/body.parser.middleware.ts
|
|
62
118
|
import coBody from "co-body";
|
|
63
|
-
import { httpError, IsHttpError as IsHttpError2 } from "@maroonedsoftware/errors";
|
|
119
|
+
import { httpError as httpError2, IsHttpError as IsHttpError2 } from "@maroonedsoftware/errors";
|
|
64
120
|
import { MultipartBody } from "@maroonedsoftware/multipart";
|
|
65
121
|
import rawBody from "raw-body";
|
|
66
122
|
var bodyParserMiddleware = /* @__PURE__ */ __name((contentTypes) => {
|
|
67
123
|
return async (ctx, next) => {
|
|
68
124
|
if (contentTypes.length === 0) {
|
|
69
125
|
if (ctx.request.length > 0) {
|
|
70
|
-
throw
|
|
126
|
+
throw httpError2(400).withDetails({
|
|
71
127
|
body: "Unexpected body"
|
|
72
128
|
});
|
|
73
129
|
}
|
|
74
130
|
} else {
|
|
75
131
|
if (ctx.request.length > 0) {
|
|
76
132
|
if (!ctx.request.is(contentTypes)) {
|
|
77
|
-
throw
|
|
133
|
+
throw httpError2(415).withDetails({
|
|
78
134
|
"content-type": `must be ${contentTypes.length > 1 ? "one of " : ""}${contentTypes.join(", ")}`,
|
|
79
135
|
value: ctx.request.type
|
|
80
136
|
});
|
|
@@ -91,7 +147,7 @@ var bodyParserMiddleware = /* @__PURE__ */ __name((contentTypes) => {
|
|
|
91
147
|
} else if (ctx.request.is("pdf")) {
|
|
92
148
|
ctx.body = await rawBody(ctx.req);
|
|
93
149
|
} else {
|
|
94
|
-
throw
|
|
150
|
+
throw httpError2(422).withDetails({
|
|
95
151
|
body: "Unsupported media type"
|
|
96
152
|
});
|
|
97
153
|
}
|
|
@@ -99,12 +155,12 @@ var bodyParserMiddleware = /* @__PURE__ */ __name((contentTypes) => {
|
|
|
99
155
|
if (IsHttpError2(error)) {
|
|
100
156
|
throw error;
|
|
101
157
|
}
|
|
102
|
-
throw
|
|
158
|
+
throw httpError2(422).withCause(error).withDetails({
|
|
103
159
|
body: "Invalid request body format"
|
|
104
160
|
});
|
|
105
161
|
}
|
|
106
162
|
} else {
|
|
107
|
-
throw
|
|
163
|
+
throw httpError2(411);
|
|
108
164
|
}
|
|
109
165
|
}
|
|
110
166
|
await next();
|
|
@@ -113,7 +169,9 @@ var bodyParserMiddleware = /* @__PURE__ */ __name((contentTypes) => {
|
|
|
113
169
|
export {
|
|
114
170
|
ServerKitRouter,
|
|
115
171
|
bodyParserMiddleware,
|
|
172
|
+
corsMiddleware,
|
|
116
173
|
errorMiddleware,
|
|
117
|
-
|
|
174
|
+
rateLimiterMiddleware,
|
|
175
|
+
serverKitContextMiddleware
|
|
118
176
|
};
|
|
119
177
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/serverkit.router.ts","../src/middleware/server/error.middleware.ts","../src/middleware/server/injectkit.middleware.ts","../src/middleware/router/body.parser.middleware.ts"],"sourcesContent":["import { DefaultState } from 'koa';\nimport Router from '@koa/router';\nimport { ServerKitContext } from './serverkit.context.js';\n\nexport const ServerKitRouter = <StateT = DefaultState, ContextT = ServerKitContext>() => new Router<StateT, ContextT>();\n","import { ServerKitMiddleware } from '../../serverkit.middleware.js';\nimport { IsHttpError } from '@maroonedsoftware/errors';\n\nexport const errorMiddleware = (): ServerKitMiddleware => {\n return async (ctx, next) => {\n try {\n await next();\n if (ctx.status === 404 && !ctx.body) {\n const body = {\n statusCode: 404,\n message: 'Not Found',\n details: { url: ctx.URL.toString() },\n };\n ctx.status = 404;\n ctx.body = body;\n ctx.app.emit('warn', body, ctx);\n }\n } catch (error) {\n if (IsHttpError(error)) {\n ctx.status = error.statusCode;\n ctx.body = {\n statusCode: error.statusCode,\n message: error.message,\n details: error.details,\n };\n if (error.headers) {\n for (const entry of Object.entries(error.headers)) {\n ctx.set(entry[0], entry[1]);\n }\n }\n } else {\n ctx.status = 500;\n ctx.body = {\n statusCode: 500,\n message: 'Internal Server Error',\n };\n }\n\n ctx.app.emit('error', error, ctx);\n }\n };\n};\n","import { Container } from 'injectkit';\nimport { Logger } from '@maroonedsoftware/logger';\nimport { ServerKitMiddleware } from '../../serverkit.middleware.js';\n\nexport const injectkitMiddleware = (container: Container): ServerKitMiddleware => {\n return async (ctx, next) => {\n ctx.container = container.createScopedContainer();\n ctx.logger = container.get(Logger);\n await next();\n };\n};\n","import coBody from 'co-body';\nimport { httpError, IsHttpError } from '@maroonedsoftware/errors';\nimport { MultipartBody } from '@maroonedsoftware/multipart';\nimport rawBody from 'raw-body';\nimport { ServerKitMiddleware } from '../../serverkit.middleware.js';\n\nexport const bodyParserMiddleware = (contentTypes: string[]): ServerKitMiddleware => {\n return async (ctx, next) => {\n if (contentTypes.length === 0) {\n if (ctx.request.length > 0) {\n throw httpError(400).withErrors({ body: 'Unexpected body' });\n }\n } else {\n if (ctx.request.length > 0) {\n if (!ctx.request.is(contentTypes)) {\n throw httpError(415).withErrors({\n 'content-type': `must be ${contentTypes.length > 1 ? 'one of ' : ''}${contentTypes.join(', ')}`,\n value: ctx.request.type,\n });\n }\n\n try {\n if (ctx.request.is('json', 'application/*+json')) {\n ctx.body = await coBody.json(ctx);\n } else if (ctx.request.is('urlencoded')) {\n ctx.body = await coBody.form(ctx);\n } else if (ctx.request.is('text/*')) {\n ctx.body = await coBody.text(ctx);\n } else if (ctx.request.is('multipart')) {\n ctx.body = new MultipartBody(ctx.req);\n } else if (ctx.request.is('pdf')) {\n ctx.body = await rawBody(ctx.req);\n } else {\n throw httpError(422).withErrors({ body: 'Unsupported media type' });\n }\n } catch (error) {\n if (IsHttpError(error)) {\n throw error;\n }\n throw httpError(422)\n .withCause(error as Error)\n .withErrors({ body: 'Invalid request body format' });\n }\n } else {\n throw httpError(411);\n }\n }\n await next();\n };\n};\n"],"mappings":";;;;AACA,OAAOA,YAAY;AAGZ,IAAMC,kBAAkB,6BAA0D,IAAIC,OAAAA,GAA9D;;;ACH/B,SAASC,mBAAmB;AAErB,IAAMC,kBAAkB,6BAAA;AAC7B,SAAO,OAAOC,KAAKC,SAAAA;AACjB,QAAI;AACF,YAAMA,KAAAA;AACN,UAAID,IAAIE,WAAW,OAAO,CAACF,IAAIG,MAAM;AACnC,cAAMA,OAAO;UACXC,YAAY;UACZC,SAAS;UACTC,SAAS;YAAEC,KAAKP,IAAIQ,IAAIC,SAAQ;UAAG;QACrC;AACAT,YAAIE,SAAS;AACbF,YAAIG,OAAOA;AACXH,YAAIU,IAAIC,KAAK,QAAQR,MAAMH,GAAAA;MAC7B;IACF,SAASY,OAAO;AACd,UAAIC,YAAYD,KAAAA,GAAQ;AACtBZ,YAAIE,SAASU,MAAMR;AACnBJ,YAAIG,OAAO;UACTC,YAAYQ,MAAMR;UAClBC,SAASO,MAAMP;UACfC,SAASM,MAAMN;QACjB;AACA,YAAIM,MAAME,SAAS;AACjB,qBAAWC,SAASC,OAAOC,QAAQL,MAAME,OAAO,GAAG;AACjDd,gBAAIkB,IAAIH,MAAM,CAAA,GAAIA,MAAM,CAAA,CAAE;UAC5B;QACF;MACF,OAAO;AACLf,YAAIE,SAAS;AACbF,YAAIG,OAAO;UACTC,YAAY;UACZC,SAAS;QACX;MACF;AAEAL,UAAIU,IAAIC,KAAK,SAASC,OAAOZ,GAAAA;IAC/B;EACF;AACF,GAtC+B;;;ACF/B,SAASmB,cAAc;AAGhB,IAAMC,sBAAsB,wBAACC,cAAAA;AAClC,SAAO,OAAOC,KAAKC,SAAAA;AACjBD,QAAID,YAAYA,UAAUG,sBAAqB;AAC/CF,QAAIG,SAASJ,UAAUK,IAAIC,MAAAA;AAC3B,UAAMJ,KAAAA;EACR;AACF,GANmC;;;ACJnC,OAAOK,YAAY;AACnB,SAASC,WAAWC,eAAAA,oBAAmB;AACvC,SAASC,qBAAqB;AAC9B,OAAOC,aAAa;AAGb,IAAMC,uBAAuB,wBAACC,iBAAAA;AACnC,SAAO,OAAOC,KAAKC,SAAAA;AACjB,QAAIF,aAAaG,WAAW,GAAG;AAC7B,UAAIF,IAAIG,QAAQD,SAAS,GAAG;AAC1B,cAAME,UAAU,GAAA,EAAKC,WAAW;UAAEC,MAAM;QAAkB,CAAA;MAC5D;IACF,OAAO;AACL,UAAIN,IAAIG,QAAQD,SAAS,GAAG;AAC1B,YAAI,CAACF,IAAIG,QAAQI,GAAGR,YAAAA,GAAe;AACjC,gBAAMK,UAAU,GAAA,EAAKC,WAAW;YAC9B,gBAAgB,WAAWN,aAAaG,SAAS,IAAI,YAAY,EAAA,GAAKH,aAAaS,KAAK,IAAA,CAAA;YACxFC,OAAOT,IAAIG,QAAQO;UACrB,CAAA;QACF;AAEA,YAAI;AACF,cAAIV,IAAIG,QAAQI,GAAG,QAAQ,oBAAA,GAAuB;AAChDP,gBAAIM,OAAO,MAAMK,OAAOC,KAAKZ,GAAAA;UAC/B,WAAWA,IAAIG,QAAQI,GAAG,YAAA,GAAe;AACvCP,gBAAIM,OAAO,MAAMK,OAAOE,KAAKb,GAAAA;UAC/B,WAAWA,IAAIG,QAAQI,GAAG,QAAA,GAAW;AACnCP,gBAAIM,OAAO,MAAMK,OAAOG,KAAKd,GAAAA;UAC/B,WAAWA,IAAIG,QAAQI,GAAG,WAAA,GAAc;AACtCP,gBAAIM,OAAO,IAAIS,cAAcf,IAAIgB,GAAG;UACtC,WAAWhB,IAAIG,QAAQI,GAAG,KAAA,GAAQ;AAChCP,gBAAIM,OAAO,MAAMW,QAAQjB,IAAIgB,GAAG;UAClC,OAAO;AACL,kBAAMZ,UAAU,GAAA,EAAKC,WAAW;cAAEC,MAAM;YAAyB,CAAA;UACnE;QACF,SAASY,OAAO;AACd,cAAIC,aAAYD,KAAAA,GAAQ;AACtB,kBAAMA;UACR;AACA,gBAAMd,UAAU,GAAA,EACbgB,UAAUF,KAAAA,EACVb,WAAW;YAAEC,MAAM;UAA8B,CAAA;QACtD;MACF,OAAO;AACL,cAAMF,UAAU,GAAA;MAClB;IACF;AACA,UAAMH,KAAAA;EACR;AACF,GA3CoC;","names":["Router","ServerKitRouter","Router","IsHttpError","errorMiddleware","ctx","next","status","body","statusCode","message","details","url","URL","toString","app","emit","error","IsHttpError","headers","entry","Object","entries","set","Logger","injectkitMiddleware","container","ctx","next","createScopedContainer","logger","get","Logger","coBody","httpError","IsHttpError","MultipartBody","rawBody","bodyParserMiddleware","contentTypes","ctx","next","length","request","httpError","withErrors","body","is","join","value","type","coBody","json","form","text","MultipartBody","req","rawBody","error","IsHttpError","withCause"]}
|
|
1
|
+
{"version":3,"sources":["../src/serverkit.router.ts","../src/middleware/server/cors.middleware.ts","../src/middleware/server/error.middleware.ts","../src/middleware/server/rate.limiter.middleware.ts","../src/middleware/server/serverkit.context.middleware.ts","../src/middleware/router/body.parser.middleware.ts"],"sourcesContent":["import { DefaultState } from 'koa';\nimport Router from '@koa/router';\nimport { ServerKitContext } from './serverkit.context.js';\n\n/**\n * Creates a new Koa router typed for ServerKit state and context.\n * Use with {@link ServerKitContext} for full typing of `ctx` in route handlers.\n *\n * @typeParam StateT - Koa state type (defaults to `DefaultState`).\n * @typeParam ContextT - Context type (defaults to `ServerKitContext`).\n * @returns A new {@link Router} instance.\n */\nexport const ServerKitRouter = <StateT = DefaultState, ContextT = ServerKitContext>() => new Router<StateT, ContextT>();\n","import cors from '@koa/cors';\nimport { ServerKitMiddleware } from '../../serverkit.middleware.js';\nimport { Context } from 'koa';\n\n/**\n * CORS options for {@link corsMiddleware}.\n * Extends `@koa/cors` options with an `origin` that may be a string or array of strings/RegExps.\n */\nexport interface CorsOptions extends Omit<cors.Options, 'origin'> {\n /** Allowed origin(s): `'*'`, a single origin string, or an array of strings/RegExps to match. */\n origin?: string | (string | RegExp)[];\n}\n\n/**\n * Adds CORS headers to responses using `@koa/cors` with ServerKit-compatible origin matching.\n * Supports `'*'`, exact string origins, and RegExp patterns.\n *\n * @param options - Optional {@link CorsOptions}; defaults to `GET,HEAD,PUT,POST,DELETE,PATCH` methods.\n * @returns {@link ServerKitMiddleware} that applies CORS headers.\n */\nexport const corsMiddleware = (options?: CorsOptions): ServerKitMiddleware => {\n // return the request origin as its own matcher to support RegExp\n const originMatcher = (ctx: Context): string => {\n const origin = ctx.get('origin');\n const matchers = options?.origin ?? ['*'];\n for (const matcher of matchers) {\n if (matcher === '*') {\n return origin;\n }\n\n if (typeof matcher === 'string') {\n if (matcher === origin) {\n return origin;\n }\n continue;\n }\n\n if (matcher.test(origin)) {\n return origin;\n }\n }\n\n // return the zero value to prevent matches\n return '';\n };\n\n return cors({\n ...options,\n origin: originMatcher,\n allowMethods: options?.allowMethods ?? 'GET,HEAD,PUT,POST,DELETE,PATCH',\n secureContext: options?.secureContext ?? false,\n keepHeadersOnError: options?.keepHeadersOnError ?? false,\n privateNetworkAccess: options?.privateNetworkAccess ?? false,\n });\n};\n","import { ServerKitMiddleware } from '../../serverkit.middleware.js';\nimport { IsHttpError } from '@maroonedsoftware/errors';\n\n/**\n * Central error handler: catches thrown errors, sets status/body from HTTP errors,\n * returns 404 for unmatched routes, and 500 for unknown errors.\n * Emits `error` or `warn` on the app for logging.\n *\n * @returns {@link ServerKitMiddleware} that wraps the stack in try/catch and normalizes responses.\n */\nexport const errorMiddleware = (): ServerKitMiddleware => {\n return async (ctx, next) => {\n try {\n await next();\n if (ctx.status === 404 && !ctx.body) {\n const body = {\n statusCode: 404,\n message: 'Not Found',\n details: { url: ctx.URL.toString() },\n };\n ctx.status = 404;\n ctx.body = body;\n ctx.app.emit('warn', body, ctx);\n }\n } catch (error) {\n if (IsHttpError(error)) {\n ctx.status = error.statusCode;\n ctx.body = {\n statusCode: error.statusCode,\n message: error.message,\n details: error.details,\n };\n if (error.headers) {\n for (const entry of Object.entries(error.headers)) {\n ctx.set(entry[0], entry[1]);\n }\n }\n } else {\n ctx.status = 500;\n ctx.body = {\n statusCode: 500,\n message: 'Internal Server Error',\n };\n }\n\n ctx.app.emit('error', error, ctx);\n }\n };\n};\n","import { RateLimiterAbstract } from 'rate-limiter-flexible';\nimport { ServerKitMiddleware } from '../../serverkit.middleware.js';\nimport { httpError } from '@maroonedsoftware/errors';\n\n/**\n * Enforces rate limiting per client IP using a `rate-limiter-flexible` instance.\n * Consumes one token per request; throws HTTP 429 when the limit is exceeded.\n *\n * @param rateLimiter - A {@link RateLimiterAbstract} instance (e.g. `RateLimiterMemory`, `RateLimiterRedis`).\n * @returns {@link ServerKitMiddleware} that consumes a token and continues or throws 429.\n */\nexport const rateLimiterMiddleware = (rateLimiter: RateLimiterAbstract): ServerKitMiddleware => {\n return async (ctx, next) => {\n try {\n await rateLimiter.consume(ctx.ip);\n } catch (error) {\n throw httpError(429).withCause(error as Error);\n }\n\n await next();\n };\n};\n","import crypto from 'crypto';\nimport { Container } from 'injectkit';\nimport { Logger } from '@maroonedsoftware/logger';\nimport { ServerKitMiddleware } from '../../serverkit.middleware.js';\n\n/**\n * Populates {@link ServerKitContext} for each request: scoped container, logger,\n * logger name, user-agent, correlation ID, and request ID.\n * Reads or generates `X-Correlation-Id` and `X-Request-Id` and sets response headers.\n * Should be applied early so downstream middleware and routes can use `ctx.container` and `ctx.logger`.\n *\n * @param container - Root injectkit {@link Container} used to create a scoped container and resolve {@link Logger}.\n * @returns {@link ServerKitMiddleware} that attaches ServerKit context to `ctx`.\n */\nexport const serverKitContextMiddleware = (container: Container): ServerKitMiddleware => {\n return async (ctx, next) => {\n ctx.container = container.createScopedContainer();\n ctx.logger = container.get(Logger);\n ctx.loggerName = ctx.path;\n\n ctx.userAgent = ctx.get('user-agent') ?? '';\n ctx.correlationId = ctx.get('x-correlation-id') ?? crypto.randomUUID();\n ctx.requestId = ctx.get('x-request-id') ?? crypto.randomUUID();\n\n ctx.headers['x-correlation-id'] = ctx.correlationId;\n ctx.set('x-correlation-id', ctx.correlationId);\n\n ctx.headers['x-request-id'] = ctx.requestId;\n ctx.set('x-request-id', ctx.requestId);\n\n await next();\n };\n};\n","import coBody from 'co-body';\nimport { httpError, IsHttpError } from '@maroonedsoftware/errors';\nimport { MultipartBody } from '@maroonedsoftware/multipart';\nimport rawBody from 'raw-body';\nimport { ServerKitMiddleware } from '../../serverkit.middleware.js';\n\n/**\n * Parses the request body based on `Content-Type` and assigns it to `ctx.body`.\n * Rejects requests with unexpected or unsupported content types.\n *\n * Supported types: JSON, URL-encoded form, text, multipart, PDF (raw buffer).\n * Requires a body when `contentTypes` is non-empty; otherwise rejects bodies.\n *\n * @param contentTypes - Allowed MIME types (e.g. `['application/json', 'application/x-www-form-urlencoded']`).\n * Use an empty array to disallow any request body.\n * @returns {@link ServerKitMiddleware} that parses the body and sets `ctx.body`.\n * @throws HTTP 400 if body is present when no content types are allowed.\n * @throws HTTP 411 if body is required but missing.\n * @throws HTTP 415 if `Content-Type` is not in `contentTypes`.\n * @throws HTTP 422 if body is invalid or media type is unsupported.\n */\nexport const bodyParserMiddleware = (contentTypes: string[]): ServerKitMiddleware => {\n return async (ctx, next) => {\n if (contentTypes.length === 0) {\n if (ctx.request.length > 0) {\n throw httpError(400).withDetails({ body: 'Unexpected body' });\n }\n } else {\n if (ctx.request.length > 0) {\n if (!ctx.request.is(contentTypes)) {\n throw httpError(415).withDetails({\n 'content-type': `must be ${contentTypes.length > 1 ? 'one of ' : ''}${contentTypes.join(', ')}`,\n value: ctx.request.type,\n });\n }\n\n try {\n if (ctx.request.is('json', 'application/*+json')) {\n ctx.body = await coBody.json(ctx);\n } else if (ctx.request.is('urlencoded')) {\n ctx.body = await coBody.form(ctx);\n } else if (ctx.request.is('text/*')) {\n ctx.body = await coBody.text(ctx);\n } else if (ctx.request.is('multipart')) {\n ctx.body = new MultipartBody(ctx.req);\n } else if (ctx.request.is('pdf')) {\n ctx.body = await rawBody(ctx.req);\n } else {\n throw httpError(422).withDetails({ body: 'Unsupported media type' });\n }\n } catch (error) {\n if (IsHttpError(error)) {\n throw error;\n }\n throw httpError(422)\n .withCause(error as Error)\n .withDetails({ body: 'Invalid request body format' });\n }\n } else {\n throw httpError(411);\n }\n }\n await next();\n };\n};\n"],"mappings":"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","names":["Router","ServerKitRouter","Router","cors","corsMiddleware","options","originMatcher","ctx","origin","get","matchers","matcher","test","cors","allowMethods","secureContext","keepHeadersOnError","privateNetworkAccess","IsHttpError","errorMiddleware","ctx","next","status","body","statusCode","message","details","url","URL","toString","app","emit","error","IsHttpError","headers","entry","Object","entries","set","httpError","rateLimiterMiddleware","rateLimiter","ctx","next","consume","ip","error","httpError","withCause","crypto","Logger","serverKitContextMiddleware","container","ctx","next","createScopedContainer","logger","get","Logger","loggerName","path","userAgent","correlationId","crypto","randomUUID","requestId","headers","set","coBody","httpError","IsHttpError","MultipartBody","rawBody","bodyParserMiddleware","contentTypes","ctx","next","length","request","httpError","withDetails","body","is","join","value","type","coBody","json","form","text","MultipartBody","req","rawBody","error","IsHttpError","withCause"]}
|
|
@@ -1,3 +1,18 @@
|
|
|
1
1
|
import { ServerKitMiddleware } from '../../serverkit.middleware.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parses the request body based on `Content-Type` and assigns it to `ctx.body`.
|
|
4
|
+
* Rejects requests with unexpected or unsupported content types.
|
|
5
|
+
*
|
|
6
|
+
* Supported types: JSON, URL-encoded form, text, multipart, PDF (raw buffer).
|
|
7
|
+
* Requires a body when `contentTypes` is non-empty; otherwise rejects bodies.
|
|
8
|
+
*
|
|
9
|
+
* @param contentTypes - Allowed MIME types (e.g. `['application/json', 'application/x-www-form-urlencoded']`).
|
|
10
|
+
* Use an empty array to disallow any request body.
|
|
11
|
+
* @returns {@link ServerKitMiddleware} that parses the body and sets `ctx.body`.
|
|
12
|
+
* @throws HTTP 400 if body is present when no content types are allowed.
|
|
13
|
+
* @throws HTTP 411 if body is required but missing.
|
|
14
|
+
* @throws HTTP 415 if `Content-Type` is not in `contentTypes`.
|
|
15
|
+
* @throws HTTP 422 if body is invalid or media type is unsupported.
|
|
16
|
+
*/
|
|
2
17
|
export declare const bodyParserMiddleware: (contentTypes: string[]) => ServerKitMiddleware;
|
|
3
18
|
//# sourceMappingURL=body.parser.middleware.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"body.parser.middleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/router/body.parser.middleware.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEpE,eAAO,MAAM,oBAAoB,GAAI,cAAc,MAAM,EAAE,KAAG,mBA2C7D,CAAC"}
|
|
1
|
+
{"version":3,"file":"body.parser.middleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/router/body.parser.middleware.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEpE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,oBAAoB,GAAI,cAAc,MAAM,EAAE,KAAG,mBA2C7D,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import cors from '@koa/cors';
|
|
2
|
+
import { ServerKitMiddleware } from '../../serverkit.middleware.js';
|
|
3
|
+
/**
|
|
4
|
+
* CORS options for {@link corsMiddleware}.
|
|
5
|
+
* Extends `@koa/cors` options with an `origin` that may be a string or array of strings/RegExps.
|
|
6
|
+
*/
|
|
7
|
+
export interface CorsOptions extends Omit<cors.Options, 'origin'> {
|
|
8
|
+
/** Allowed origin(s): `'*'`, a single origin string, or an array of strings/RegExps to match. */
|
|
9
|
+
origin?: string | (string | RegExp)[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Adds CORS headers to responses using `@koa/cors` with ServerKit-compatible origin matching.
|
|
13
|
+
* Supports `'*'`, exact string origins, and RegExp patterns.
|
|
14
|
+
*
|
|
15
|
+
* @param options - Optional {@link CorsOptions}; defaults to `GET,HEAD,PUT,POST,DELETE,PATCH` methods.
|
|
16
|
+
* @returns {@link ServerKitMiddleware} that applies CORS headers.
|
|
17
|
+
*/
|
|
18
|
+
export declare const corsMiddleware: (options?: CorsOptions) => ServerKitMiddleware;
|
|
19
|
+
//# sourceMappingURL=cors.middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cors.middleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/server/cors.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAGpE;;;GAGG;AACH,MAAM,WAAW,WAAY,SAAQ,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC;IAC/D,iGAAiG;IACjG,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;CACvC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,cAAc,GAAI,UAAU,WAAW,KAAG,mBAkCtD,CAAC"}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
import { ServerKitMiddleware } from '../../serverkit.middleware.js';
|
|
2
|
+
/**
|
|
3
|
+
* Central error handler: catches thrown errors, sets status/body from HTTP errors,
|
|
4
|
+
* returns 404 for unmatched routes, and 500 for unknown errors.
|
|
5
|
+
* Emits `error` or `warn` on the app for logging.
|
|
6
|
+
*
|
|
7
|
+
* @returns {@link ServerKitMiddleware} that wraps the stack in try/catch and normalizes responses.
|
|
8
|
+
*/
|
|
2
9
|
export declare const errorMiddleware: () => ServerKitMiddleware;
|
|
3
10
|
//# sourceMappingURL=error.middleware.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.middleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/server/error.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAGpE,eAAO,MAAM,eAAe,QAAO,mBAsClC,CAAC"}
|
|
1
|
+
{"version":3,"file":"error.middleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/server/error.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAGpE;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,QAAO,mBAsClC,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { RateLimiterAbstract } from 'rate-limiter-flexible';
|
|
2
|
+
import { ServerKitMiddleware } from '../../serverkit.middleware.js';
|
|
3
|
+
/**
|
|
4
|
+
* Enforces rate limiting per client IP using a `rate-limiter-flexible` instance.
|
|
5
|
+
* Consumes one token per request; throws HTTP 429 when the limit is exceeded.
|
|
6
|
+
*
|
|
7
|
+
* @param rateLimiter - A {@link RateLimiterAbstract} instance (e.g. `RateLimiterMemory`, `RateLimiterRedis`).
|
|
8
|
+
* @returns {@link ServerKitMiddleware} that consumes a token and continues or throws 429.
|
|
9
|
+
*/
|
|
10
|
+
export declare const rateLimiterMiddleware: (rateLimiter: RateLimiterAbstract) => ServerKitMiddleware;
|
|
11
|
+
//# sourceMappingURL=rate.limiter.middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate.limiter.middleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/server/rate.limiter.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAGpE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GAAI,aAAa,mBAAmB,KAAG,mBAUxE,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Container } from 'injectkit';
|
|
2
|
+
import { ServerKitMiddleware } from '../../serverkit.middleware.js';
|
|
3
|
+
/**
|
|
4
|
+
* Populates {@link ServerKitContext} for each request: scoped container, logger,
|
|
5
|
+
* logger name, user-agent, correlation ID, and request ID.
|
|
6
|
+
* Reads or generates `X-Correlation-Id` and `X-Request-Id` and sets response headers.
|
|
7
|
+
* Should be applied early so downstream middleware and routes can use `ctx.container` and `ctx.logger`.
|
|
8
|
+
*
|
|
9
|
+
* @param container - Root injectkit {@link Container} used to create a scoped container and resolve {@link Logger}.
|
|
10
|
+
* @returns {@link ServerKitMiddleware} that attaches ServerKit context to `ctx`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const serverKitContextMiddleware: (container: Container) => ServerKitMiddleware;
|
|
13
|
+
//# sourceMappingURL=serverkit.context.middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serverkit.context.middleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/server/serverkit.context.middleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEpE;;;;;;;;GAQG;AACH,eAAO,MAAM,0BAA0B,GAAI,WAAW,SAAS,KAAG,mBAkBjE,CAAC"}
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
import { Context } from 'koa';
|
|
2
2
|
import { Container } from 'injectkit';
|
|
3
3
|
import { Logger } from '@maroonedsoftware/logger';
|
|
4
|
+
/**
|
|
5
|
+
* Koa context extended with ServerKit request-scoped services and metadata.
|
|
6
|
+
* Populated by {@link serverKitContextMiddleware}.
|
|
7
|
+
* Use this as the context type for route handlers to get full typing of `ctx.container`, `ctx.logger`, and request IDs.
|
|
8
|
+
*
|
|
9
|
+
* @extends Context
|
|
10
|
+
* @see {@link serverKitContextMiddleware} – middleware that populates this context on each request
|
|
11
|
+
*/
|
|
4
12
|
export interface ServerKitContext extends Context {
|
|
13
|
+
/** Scoped injectkit container for this request; use for request-scoped DI. */
|
|
5
14
|
container: Container;
|
|
15
|
+
/** Request-scoped logger instance. */
|
|
6
16
|
logger: Logger;
|
|
17
|
+
/** Logger name for this request (e.g. request path or route identifier). */
|
|
18
|
+
loggerName: string;
|
|
19
|
+
/** Value of the `User-Agent` request header, or empty string if absent. */
|
|
20
|
+
userAgent: string;
|
|
21
|
+
/** Correlation ID for tracing; from `X-Correlation-Id` header or generated. */
|
|
22
|
+
correlationId: string;
|
|
23
|
+
/** Request ID; from `X-Request-Id` header or generated. */
|
|
24
|
+
requestId: string;
|
|
7
25
|
}
|
|
8
26
|
//# sourceMappingURL=serverkit.context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serverkit.context.d.ts","sourceRoot":"","sources":["../src/serverkit.context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAElD,MAAM,WAAW,gBAAiB,SAAQ,OAAO;IAC/C,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"serverkit.context.d.ts","sourceRoot":"","sources":["../src/serverkit.context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAElD;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAiB,SAAQ,OAAO;IAC/C,8EAA8E;IAC9E,SAAS,EAAE,SAAS,CAAC;IACrB,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,aAAa,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;CACnB"}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { DefaultState, Middleware } from 'koa';
|
|
2
2
|
import { ServerKitContext } from './serverkit.context.js';
|
|
3
|
+
/**
|
|
4
|
+
* Koa middleware type bound to {@link ServerKitContext}.
|
|
5
|
+
* Use this for middleware that relies on ServerKit context (container, logger, etc.).
|
|
6
|
+
*
|
|
7
|
+
* @typeParam ResponseBody - Type of the response body (defaults to `unknown`).
|
|
8
|
+
* @typeParam State - Koa state type (defaults to `DefaultState`).
|
|
9
|
+
* @typeParam Context - Context type (defaults to `ServerKitContext`).
|
|
10
|
+
*/
|
|
3
11
|
export type ServerKitMiddleware<ResponseBody = unknown, State = DefaultState, Context extends ServerKitContext = ServerKitContext> = Middleware<State, Context, ResponseBody>;
|
|
4
12
|
//# sourceMappingURL=serverkit.middleware.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serverkit.middleware.d.ts","sourceRoot":"","sources":["../src/serverkit.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,MAAM,MAAM,mBAAmB,CAAC,YAAY,GAAG,OAAO,EAAE,KAAK,GAAG,YAAY,EAAE,OAAO,SAAS,gBAAgB,GAAG,gBAAgB,IAAI,UAAU,CAC7I,KAAK,EACL,OAAO,EACP,YAAY,CACb,CAAC"}
|
|
1
|
+
{"version":3,"file":"serverkit.middleware.d.ts","sourceRoot":"","sources":["../src/serverkit.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,CAAC,YAAY,GAAG,OAAO,EAAE,KAAK,GAAG,YAAY,EAAE,OAAO,SAAS,gBAAgB,GAAG,gBAAgB,IAAI,UAAU,CAC7I,KAAK,EACL,OAAO,EACP,YAAY,CACb,CAAC"}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { DefaultState } from 'koa';
|
|
2
2
|
import Router from '@koa/router';
|
|
3
3
|
import { ServerKitContext } from './serverkit.context.js';
|
|
4
|
+
/**
|
|
5
|
+
* Creates a new Koa router typed for ServerKit state and context.
|
|
6
|
+
* Use with {@link ServerKitContext} for full typing of `ctx` in route handlers.
|
|
7
|
+
*
|
|
8
|
+
* @typeParam StateT - Koa state type (defaults to `DefaultState`).
|
|
9
|
+
* @typeParam ContextT - Context type (defaults to `ServerKitContext`).
|
|
10
|
+
* @returns A new {@link Router} instance.
|
|
11
|
+
*/
|
|
4
12
|
export declare const ServerKitRouter: <StateT = DefaultState, ContextT = ServerKitContext>() => Router<StateT, ContextT>;
|
|
5
13
|
//# sourceMappingURL=serverkit.router.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serverkit.router.d.ts","sourceRoot":"","sources":["../src/serverkit.router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,KAAK,CAAC;AACnC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,eAAO,MAAM,eAAe,GAAI,MAAM,GAAG,YAAY,EAAE,QAAQ,GAAG,gBAAgB,+BAAqC,CAAC"}
|
|
1
|
+
{"version":3,"file":"serverkit.router.d.ts","sourceRoot":"","sources":["../src/serverkit.router.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,KAAK,CAAC;AACnC,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,GAAG,YAAY,EAAE,QAAQ,GAAG,gBAAgB,+BAAqC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maroonedsoftware/koa",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Marooned Software",
|
|
@@ -32,23 +32,26 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"co-body": "^6.2.0",
|
|
34
34
|
"injectkit": "^1.0.3",
|
|
35
|
+
"rate-limiter-flexible": "^9.0.1",
|
|
35
36
|
"raw-body": "^3.0.2",
|
|
37
|
+
"@maroonedsoftware/multipart": "1.0.1",
|
|
36
38
|
"@maroonedsoftware/logger": "1.0.0",
|
|
37
|
-
"@maroonedsoftware/
|
|
38
|
-
"@maroonedsoftware/errors": "1.0.0"
|
|
39
|
+
"@maroonedsoftware/errors": "1.1.0"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
|
-
"koa": "^
|
|
42
|
-
"@koa/router": "^15.2.0"
|
|
42
|
+
"@koa/cors": "^5.0.0",
|
|
43
|
+
"@koa/router": "^15.2.0",
|
|
44
|
+
"koa": "^3.1.1"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
47
|
+
"@koa/cors": "^5.0.0",
|
|
48
|
+
"@koa/router": "^15.2.0",
|
|
45
49
|
"@types/co-body": "^6.1.3",
|
|
46
50
|
"@types/koa": "^3.0.1",
|
|
47
|
-
"@types/
|
|
51
|
+
"@types/koa__cors": "^5.0.1",
|
|
48
52
|
"koa": "^3.1.1",
|
|
49
|
-
"@
|
|
50
|
-
"@repo/config-eslint": "0.0.0"
|
|
51
|
-
"@repo/config-typescript": "0.0.0"
|
|
53
|
+
"@repo/config-typescript": "0.0.0",
|
|
54
|
+
"@repo/config-eslint": "0.0.0"
|
|
52
55
|
},
|
|
53
56
|
"scripts": {
|
|
54
57
|
"build": "tsup src/index.ts --format esm --sourcemap --dts && tsc --emitDeclarationOnly --declaration",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"injectkit.middleware.d.ts","sourceRoot":"","sources":["../../../src/middleware/server/injectkit.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEpE,eAAO,MAAM,mBAAmB,GAAI,WAAW,SAAS,KAAG,mBAM1D,CAAC"}
|