@ysk-kit/api-http 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 YSK Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,14 @@
1
+ export type EnvelopeErrorJson = {
2
+ status: number;
3
+ body: {
4
+ ok: false;
5
+ error: {
6
+ code: string;
7
+ message: string;
8
+ details?: unknown;
9
+ requestId: string;
10
+ };
11
+ };
12
+ };
13
+ export declare const envelopeError: (error: unknown, requestId: string) => EnvelopeErrorJson;
14
+ //# sourceMappingURL=envelope-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope-error.d.ts","sourceRoot":"","sources":["../src/envelope-error.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE;QACJ,EAAE,EAAE,KAAK,CAAC;QACV,KAAK,EAAE;YACL,IAAI,EAAE,MAAM,CAAC;YACb,OAAO,EAAE,MAAM,CAAC;YAChB,OAAO,CAAC,EAAE,OAAO,CAAC;YAClB,SAAS,EAAE,MAAM,CAAC;SACnB,CAAC;KACH,CAAC;CACH,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,EAAE,WAAW,MAAM,KAAG,iBAwCjE,CAAC"}
@@ -0,0 +1,45 @@
1
+ import { ERROR_MESSAGE } from '@ysk-kit/contracts';
2
+ import { AppError } from '@ysk-kit/domain-kernel';
3
+ import { ZodError } from 'zod';
4
+ export const envelopeError = (error, requestId) => {
5
+ if (error instanceof ZodError) {
6
+ return {
7
+ status: 422,
8
+ body: {
9
+ ok: false,
10
+ error: {
11
+ code: 'VALIDATION_FAILED',
12
+ message: ERROR_MESSAGE.VALIDATION_FAILED['zh-HK'],
13
+ details: error.flatten(),
14
+ requestId,
15
+ },
16
+ },
17
+ };
18
+ }
19
+ if (error instanceof AppError) {
20
+ return {
21
+ status: error.status,
22
+ body: {
23
+ ok: false,
24
+ error: {
25
+ code: error.code,
26
+ message: error.message,
27
+ ...(error.details !== undefined ? { details: error.details } : {}),
28
+ requestId,
29
+ },
30
+ },
31
+ };
32
+ }
33
+ return {
34
+ status: 500,
35
+ body: {
36
+ ok: false,
37
+ error: {
38
+ code: 'INTERNAL',
39
+ message: ERROR_MESSAGE.INTERNAL['zh-HK'],
40
+ requestId,
41
+ },
42
+ },
43
+ };
44
+ };
45
+ //# sourceMappingURL=envelope-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"envelope-error.js","sourceRoot":"","sources":["../src/envelope-error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,KAAK,CAAC;AAe/B,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAc,EAAE,SAAiB,EAAqB,EAAE;IACpF,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;QAC9B,OAAO;YACL,MAAM,EAAE,GAAG;YACX,IAAI,EAAE;gBACJ,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE;oBACL,IAAI,EAAE,mBAAmB;oBACzB,OAAO,EAAE,aAAa,CAAC,iBAAiB,CAAC,OAAO,CAAC;oBACjD,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE;oBACxB,SAAS;iBACV;aACF;SACF,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;QAC9B,OAAO;YACL,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,IAAI,EAAE;gBACJ,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE;oBACL,IAAI,EAAE,KAAK,CAAC,IAAI;oBAChB,OAAO,EAAE,KAAK,CAAC,OAAO;oBACtB,GAAG,CAAC,KAAK,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAClE,SAAS;iBACV;aACF;SACF,CAAC;IACJ,CAAC;IACD,OAAO;QACL,MAAM,EAAE,GAAG;QACX,IAAI,EAAE;YACJ,EAAE,EAAE,KAAK;YACT,KAAK,EAAE;gBACL,IAAI,EAAE,UAAU;gBAChB,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC,OAAO,CAAC;gBACxC,SAAS;aACV;SACF;KACF,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { z } from 'zod';
2
+ export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
3
+ export type ContractRoute = {
4
+ method: HttpMethod;
5
+ path: string;
6
+ query?: z.ZodType;
7
+ body?: z.ZodType;
8
+ pathParams?: z.ZodType;
9
+ };
10
+ export type ContractRouter = {
11
+ [key: string]: ContractRoute | ContractRouter;
12
+ };
13
+ export declare const flattenContract: (contract: ContractRouter) => {
14
+ key: string;
15
+ route: ContractRoute;
16
+ }[];
17
+ //# sourceMappingURL=flatten.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flatten.d.ts","sourceRoot":"","sources":["../src/flatten.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,cAAc,CAAC;CAC/C,CAAC;AA2BF,eAAO,MAAM,eAAe,GAC1B,UAAU,cAAc,KACvB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,EAIvC,CAAC"}
@@ -0,0 +1,23 @@
1
+ const isRoute = (value) => Boolean(value &&
2
+ typeof value === 'object' &&
3
+ 'method' in value &&
4
+ 'path' in value &&
5
+ typeof value.method === 'string' &&
6
+ typeof value.path === 'string');
7
+ const walk = (node, prefix, acc) => {
8
+ for (const [key, value] of Object.entries(node)) {
9
+ const nextKey = prefix ? `${prefix}.${key}` : key;
10
+ if (isRoute(value)) {
11
+ acc.push({ key: nextKey, route: value });
12
+ }
13
+ else if (value && typeof value === 'object') {
14
+ walk(value, nextKey, acc);
15
+ }
16
+ }
17
+ };
18
+ export const flattenContract = (contract) => {
19
+ const acc = [];
20
+ walk(contract, '', acc);
21
+ return acc;
22
+ };
23
+ //# sourceMappingURL=flatten.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flatten.js","sourceRoot":"","sources":["../src/flatten.ts"],"names":[],"mappings":"AAgBA,MAAM,OAAO,GAAG,CAAC,KAAc,EAA0B,EAAE,CACzD,OAAO,CACL,KAAK;IACH,OAAO,KAAK,KAAK,QAAQ;IACzB,QAAQ,IAAI,KAAK;IACjB,MAAM,IAAI,KAAK;IACf,OAAQ,KAAuB,CAAC,MAAM,KAAK,QAAQ;IACnD,OAAQ,KAAuB,CAAC,IAAI,KAAK,QAAQ,CACpD,CAAC;AAEJ,MAAM,IAAI,GAAG,CACX,IAAoB,EACpB,MAAc,EACd,GAA4C,EACtC,EAAE;IACR,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAChD,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC;QAClD,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACnB,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3C,CAAC;aAAM,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9C,IAAI,CAAC,KAAuB,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,QAAwB,EACiB,EAAE;IAC3C,MAAM,GAAG,GAA4C,EAAE,CAAC;IACxD,IAAI,CAAC,QAAQ,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;IACxB,OAAO,GAAG,CAAC;AACb,CAAC,CAAC"}
@@ -0,0 +1,8 @@
1
+ export { type EnvelopeErrorJson, envelopeError } from './envelope-error';
2
+ export { type ContractRoute, type ContractRouter, flattenContract, type HttpMethod, } from './flatten';
3
+ export { buildOpenApiDocument, type OpenApiDocument, scalarDocsHtml, } from './openapi';
4
+ export { clientIp, createMemoryRateLimit, isRateLimitSkipped, type RateLimitOpts, } from './rate-limit';
5
+ export { REQUEST_ID_HEADER } from './request-id';
6
+ export { applySecurityHeaders, securityHeaders } from './security-headers';
7
+ export { type HttpCtx, type HttpHandler, type HttpResult, headerValue } from './types';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,iBAAiB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,eAAe,EACf,KAAK,UAAU,GAChB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,oBAAoB,EACpB,KAAK,eAAe,EACpB,cAAc,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,QAAQ,EACR,qBAAqB,EACrB,kBAAkB,EAClB,KAAK,aAAa,GACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { envelopeError } from './envelope-error';
2
+ export { flattenContract, } from './flatten';
3
+ export { buildOpenApiDocument, scalarDocsHtml, } from './openapi';
4
+ export { clientIp, createMemoryRateLimit, isRateLimitSkipped, } from './rate-limit';
5
+ export { REQUEST_ID_HEADER } from './request-id';
6
+ export { applySecurityHeaders, securityHeaders } from './security-headers';
7
+ export { headerValue } from './types';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAGL,eAAe,GAEhB,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,oBAAoB,EAEpB,cAAc,GACf,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,QAAQ,EACR,qBAAqB,EACrB,kBAAkB,GAEnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EAAmD,WAAW,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { type ContractRouter } from './flatten';
2
+ export type OpenApiDocument = {
3
+ openapi: string;
4
+ info: {
5
+ title: string;
6
+ version: string;
7
+ };
8
+ paths: Record<string, Record<string, unknown>>;
9
+ };
10
+ export declare const buildOpenApiDocument: (contract: ContractRouter) => OpenApiDocument;
11
+ export declare const scalarDocsHtml: (specUrl?: string) => string;
12
+ //# sourceMappingURL=openapi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,cAAc,EAAmB,MAAM,WAAW,CAAC;AAUjE,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAChD,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,UAAU,cAAc,KAAG,eAkC/D,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,gBAAyB,KAAG,MAY1D,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { z } from 'zod';
2
+ import { flattenContract } from './flatten';
3
+ const toJsonSchema = (schema) => {
4
+ try {
5
+ return z.toJSONSchema(schema);
6
+ }
7
+ catch {
8
+ return { type: 'object' };
9
+ }
10
+ };
11
+ export const buildOpenApiDocument = (contract) => {
12
+ const paths = {};
13
+ for (const { key, route } of flattenContract(contract)) {
14
+ const path = route.path.replace(/:([A-Za-z0-9_]+)/g, '{$1}');
15
+ const method = route.method.toLowerCase();
16
+ const operation = {
17
+ operationId: key,
18
+ responses: {
19
+ '200': { description: 'OK' },
20
+ },
21
+ };
22
+ if (route.body) {
23
+ operation.requestBody = {
24
+ required: true,
25
+ content: { 'application/json': { schema: toJsonSchema(route.body) } },
26
+ };
27
+ }
28
+ if (route.query) {
29
+ operation.parameters = [
30
+ {
31
+ in: 'query',
32
+ name: 'query',
33
+ schema: toJsonSchema(route.query),
34
+ },
35
+ ];
36
+ }
37
+ paths[path] ??= {};
38
+ paths[path][method] = operation;
39
+ }
40
+ return {
41
+ openapi: '3.1.0',
42
+ info: { title: 'YSK Kit API', version: '0.1.0' },
43
+ paths,
44
+ };
45
+ };
46
+ export const scalarDocsHtml = (specUrl = '/openapi.json') => `<!doctype html>
47
+ <html lang="en">
48
+ <head>
49
+ <meta charset="utf-8" />
50
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
51
+ <title>YSK Kit API</title>
52
+ </head>
53
+ <body>
54
+ <script id="api-reference" data-url="${specUrl}"></script>
55
+ <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
56
+ </body>
57
+ </html>
58
+ `;
59
+ //# sourceMappingURL=openapi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.js","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAuB,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjE,MAAM,YAAY,GAAG,CAAC,MAAiB,EAA2B,EAAE;IAClE,IAAI,CAAC;QACH,OAAO,CAAC,CAAC,YAAY,CAAC,MAAM,CAA4B,CAAC;IAC3D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAC5B,CAAC;AACH,CAAC,CAAC;AAQF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,QAAwB,EAAmB,EAAE;IAChF,MAAM,KAAK,GAA4C,EAAE,CAAC;IAC1D,KAAK,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAC1C,MAAM,SAAS,GAA4B;YACzC,WAAW,EAAE,GAAG;YAChB,SAAS,EAAE;gBACT,KAAK,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE;aAC7B;SACF,CAAC;QACF,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;YACf,SAAS,CAAC,WAAW,GAAG;gBACtB,QAAQ,EAAE,IAAI;gBACd,OAAO,EAAE,EAAE,kBAAkB,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE;aACtE,CAAC;QACJ,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAChB,SAAS,CAAC,UAAU,GAAG;gBACrB;oBACE,EAAE,EAAE,OAAO;oBACX,IAAI,EAAE,OAAO;oBACb,MAAM,EAAE,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC;iBAClC;aACF,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QACnB,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IAClC,CAAC;IACD,OAAO;QACL,OAAO,EAAE,OAAO;QAChB,IAAI,EAAE,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE;QAChD,KAAK;KACN,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,OAAO,GAAG,eAAe,EAAU,EAAE,CAAC;;;;;;;;yCAQ5B,OAAO;;;;CAI/C,CAAC"}
@@ -0,0 +1,11 @@
1
+ export type RateLimitOpts = {
2
+ windowMs: number;
3
+ max: number;
4
+ skipPath?: (path: string) => boolean;
5
+ };
6
+ export declare const clientIp: (forwarded: string | string[] | undefined, remote: string | undefined) => string;
7
+ export declare const isRateLimitSkipped: (path: string) => boolean;
8
+ export declare const createMemoryRateLimit: (opts: RateLimitOpts) => {
9
+ check(ip: string, path: string): void;
10
+ };
11
+ //# sourceMappingURL=rate-limit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACtC,CAAC;AAEF,eAAO,MAAM,QAAQ,GACnB,WAAW,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EACxC,QAAQ,MAAM,GAAG,SAAS,KACzB,MAKF,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,MAAM,MAAM,KAAG,OACc,CAAC;AAEjE,eAAO,MAAM,qBAAqB,GAAI,MAAM,aAAa;cAK3C,MAAM,QAAQ,MAAM,GAAG,IAAI;CAaxC,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { AppError } from '@ysk-kit/domain-kernel';
2
+ export const clientIp = (forwarded, remote) => {
3
+ const raw = Array.isArray(forwarded) ? forwarded[0] : forwarded;
4
+ const first = raw?.split(',')[0]?.trim();
5
+ if (first && first.length > 0)
6
+ return first;
7
+ return remote ?? 'unknown';
8
+ };
9
+ export const isRateLimitSkipped = (path) => path === '/metrics' || path === '/health' || path === '/ready';
10
+ export const createMemoryRateLimit = (opts) => {
11
+ const hits = new Map();
12
+ const skip = opts.skipPath ?? isRateLimitSkipped;
13
+ return {
14
+ check(ip, path) {
15
+ if (skip(path))
16
+ return;
17
+ const now = Date.now();
18
+ const windowStart = now - opts.windowMs;
19
+ const recent = (hits.get(ip) ?? []).filter((at) => at > windowStart);
20
+ if (recent.length >= opts.max) {
21
+ hits.set(ip, recent);
22
+ throw new AppError('RATE_LIMITED');
23
+ }
24
+ recent.push(now);
25
+ hits.set(ip, recent);
26
+ },
27
+ };
28
+ };
29
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAQlD,MAAM,CAAC,MAAM,QAAQ,GAAG,CACtB,SAAwC,EACxC,MAA0B,EAClB,EAAE;IACV,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAChE,MAAM,KAAK,GAAG,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;IACzC,IAAI,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,MAAM,IAAI,SAAS,CAAC;AAC7B,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,IAAY,EAAW,EAAE,CAC1D,IAAI,KAAK,UAAU,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,QAAQ,CAAC;AAEjE,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,IAAmB,EAAE,EAAE;IAC3D,MAAM,IAAI,GAAG,IAAI,GAAG,EAAoB,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,IAAI,kBAAkB,CAAC;IAEjD,OAAO;QACL,KAAK,CAAC,EAAU,EAAE,IAAY;YAC5B,IAAI,IAAI,CAAC,IAAI,CAAC;gBAAE,OAAO;YACvB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACvB,MAAM,WAAW,GAAG,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC;YACxC,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,GAAG,WAAW,CAAC,CAAC;YACrE,IAAI,MAAM,CAAC,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC9B,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;gBACrB,MAAM,IAAI,QAAQ,CAAC,cAAc,CAAC,CAAC;YACrC,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACjB,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export declare const REQUEST_ID_HEADER = "x-request-id";
2
+ //# sourceMappingURL=request-id.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-id.d.ts","sourceRoot":"","sources":["../src/request-id.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,iBAAiB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export const REQUEST_ID_HEADER = 'x-request-id';
2
+ //# sourceMappingURL=request-id.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-id.js","sourceRoot":"","sources":["../src/request-id.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,iBAAiB,GAAG,cAAc,CAAC"}
@@ -0,0 +1,9 @@
1
+ export declare const securityHeaders: (opts: {
2
+ production: boolean;
3
+ docs?: boolean;
4
+ }) => Record<string, string>;
5
+ export declare const applySecurityHeaders: (set: (name: string, value: string) => void, opts: {
6
+ production: boolean;
7
+ docs?: boolean;
8
+ }) => void;
9
+ //# sourceMappingURL=security-headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-headers.d.ts","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,GAAI,MAAM;IACpC,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAgBxB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,EAC1C,MAAM;IAAE,UAAU,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,KAC5C,IAIF,CAAC"}
@@ -0,0 +1,23 @@
1
+ export const securityHeaders = (opts) => {
2
+ const csp = opts.docs
3
+ ? "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' https://cdn.jsdelivr.net; connect-src 'self'; frame-ancestors 'none'"
4
+ : "default-src 'none'; frame-ancestors 'none'";
5
+ const headers = {
6
+ 'X-Content-Type-Options': 'nosniff',
7
+ 'X-Frame-Options': 'DENY',
8
+ 'Referrer-Policy': 'no-referrer',
9
+ 'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
10
+ 'Cross-Origin-Opener-Policy': 'same-origin',
11
+ 'Content-Security-Policy': csp,
12
+ };
13
+ if (opts.production) {
14
+ headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains';
15
+ }
16
+ return headers;
17
+ };
18
+ export const applySecurityHeaders = (set, opts) => {
19
+ for (const [name, value] of Object.entries(securityHeaders(opts))) {
20
+ set(name, value);
21
+ }
22
+ };
23
+ //# sourceMappingURL=security-headers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-headers.js","sourceRoot":"","sources":["../src/security-headers.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAG/B,EAA0B,EAAE;IAC3B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI;QACnB,CAAC,CAAC,uOAAuO;QACzO,CAAC,CAAC,4CAA4C,CAAC;IACjD,MAAM,OAAO,GAA2B;QACtC,wBAAwB,EAAE,SAAS;QACnC,iBAAiB,EAAE,MAAM;QACzB,iBAAiB,EAAE,aAAa;QAChC,oBAAoB,EAAE,0CAA0C;QAChE,4BAA4B,EAAE,aAAa;QAC3C,yBAAyB,EAAE,GAAG;KAC/B,CAAC;IACF,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QACpB,OAAO,CAAC,2BAA2B,CAAC,GAAG,qCAAqC,CAAC;IAC/E,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,GAA0C,EAC1C,IAA6C,EACvC,EAAE;IACR,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAClE,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACnB,CAAC;AACH,CAAC,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { AccessClaims } from '@ysk-kit/auth';
2
+ import type { Permission } from '@ysk-kit/contracts';
3
+ export type HttpCtx = {
4
+ auth?: AccessClaims;
5
+ body: unknown;
6
+ query: unknown;
7
+ params: unknown;
8
+ requestId: string;
9
+ headers: Record<string, string | string[] | undefined>;
10
+ };
11
+ export type HttpResult = {
12
+ status: number;
13
+ body: unknown;
14
+ };
15
+ export type HttpHandler = {
16
+ auth?: 'required';
17
+ permission?: Permission;
18
+ handle: (ctx: HttpCtx) => Promise<HttpResult>;
19
+ };
20
+ export declare const headerValue: (headers: HttpCtx["headers"], name: string) => string | undefined;
21
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAErD,MAAM,MAAM,OAAO,GAAG;IACpB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;CACxD,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,MAAM,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;CAC/C,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,SAAS,OAAO,CAAC,SAAS,CAAC,EAAE,MAAM,MAAM,KAAG,MAAM,GAAG,SAIhF,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ export const headerValue = (headers, name) => {
2
+ const value = headers[name] ?? headers[name.toLowerCase()];
3
+ if (Array.isArray(value))
4
+ return value[0];
5
+ return value;
6
+ };
7
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAuBA,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,OAA2B,EAAE,IAAY,EAAsB,EAAE;IAC3F,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAC3D,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1C,OAAO,KAAK,CAAC;AACf,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@ysk-kit/api-http",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "dependencies": {
14
+ "@ysk-kit/auth": "1.0.0",
15
+ "@ysk-kit/contracts": "1.0.0",
16
+ "@ysk-kit/domain-kernel": "1.0.0",
17
+ "zod": "^4.6.5"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "^24.5.2",
21
+ "typescript": "^6.0.3",
22
+ "vitest": "^5.0.2"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "publishConfig": {
28
+ "registry": "https://registry.npmjs.org",
29
+ "access": "public"
30
+ },
31
+ "scripts": {
32
+ "typecheck": "tsc --noEmit -p tsconfig.json",
33
+ "test": "vitest run --passWithNoTests",
34
+ "build": "tsc -p tsconfig.build.json"
35
+ }
36
+ }