@pallamart/pm-hono-setup 0.0.0-stage → 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 +32 -0
- package/README.md +21 -2
- package/dist/bindings/index.d.ts +1 -0
- package/dist/bindings/index.js +1 -0
- package/dist/bindings/service-bindings.d.ts +6 -0
- package/dist/bindings/service-bindings.js +1 -0
- package/dist/constants/api-key-auth-config.d.ts +8 -0
- package/dist/constants/api-key-auth-config.js +8 -0
- package/dist/constants/app-config.d.ts +15 -0
- package/dist/constants/app-config.js +21 -0
- package/dist/constants/openapi-config.d.ts +11 -0
- package/dist/constants/openapi-config.js +8 -0
- package/dist/constants/user-token-auth-config.d.ts +15 -0
- package/dist/constants/user-token-auth-config.js +15 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/middleware/api-key-auth.d.ts +3 -0
- package/dist/middleware/api-key-auth.js +17 -0
- package/dist/middleware/user-token-auth.d.ts +3 -0
- package/dist/middleware/user-token-auth.js +30 -0
- package/dist/models/api-key-auth-options.d.ts +4 -0
- package/dist/models/api-key-auth-options.js +1 -0
- package/dist/models/json-error-body-data.d.ts +5 -0
- package/dist/models/json-error-body-data.js +1 -0
- package/dist/models/json-error-data.d.ts +5 -0
- package/dist/models/json-error-data.js +1 -0
- package/dist/models/user-token-auth-options.d.ts +8 -0
- package/dist/models/user-token-auth-options.js +1 -0
- package/dist/services/json-error-service.d.ts +7 -0
- package/dist/services/json-error-service.js +8 -0
- package/dist/services/user-token-access-service.d.ts +4 -0
- package/dist/services/user-token-access-service.js +19 -0
- package/dist/services/user-token-refusal-service.d.ts +5 -0
- package/dist/services/user-token-refusal-service.js +10 -0
- package/dist/services/user-token-replay-service.d.ts +5 -0
- package/dist/services/user-token-replay-service.js +26 -0
- package/dist/setup/configure-openapi.d.ts +4 -0
- package/dist/setup/configure-openapi.js +23 -0
- package/dist/setup/create-app.d.ts +2 -0
- package/dist/setup/create-app.js +50 -0
- package/dist/setup/create-router.d.ts +3 -0
- package/dist/setup/create-router.js +4 -0
- package/dist/setup/index.d.ts +3 -0
- package/dist/setup/index.js +3 -0
- package/package.json +76 -5
package/LICENSE
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Pallamart Proprietary License
|
|
2
|
+
|
|
3
|
+
Version 1.0, 23-Jan-2024
|
|
4
|
+
|
|
5
|
+
1. Grant of License:
|
|
6
|
+
Pallamart grants you a non-exclusive, non-transferable, revocable license to use the software, solely for internal business purposes.
|
|
7
|
+
|
|
8
|
+
2. Restrictions:
|
|
9
|
+
You may not:
|
|
10
|
+
a. Distribute or sublicense the software to third parties.
|
|
11
|
+
b. Reverse engineer, decompile, or disassemble the software.
|
|
12
|
+
c. Remove or alter any proprietary notices or labels on the software.
|
|
13
|
+
|
|
14
|
+
3. Ownership:
|
|
15
|
+
Pallamart retains all right, title, and interest in and to the software, including all intellectual property rights.
|
|
16
|
+
|
|
17
|
+
4. Confidentiality:
|
|
18
|
+
You agree to keep the software confidential and not disclose it to third parties.
|
|
19
|
+
|
|
20
|
+
5. Support and Updates:
|
|
21
|
+
This license does not entitle you to support or updates. Pallamart may, at its discretion, provide support and updates.
|
|
22
|
+
|
|
23
|
+
6. Termination:
|
|
24
|
+
This license is effective until terminated. Pallamart may terminate the license at any time if you breach any terms. Upon termination, you must cease using the software.
|
|
25
|
+
|
|
26
|
+
7. Governing Law:
|
|
27
|
+
This license is governed by the Canadian Laws. Any disputes arising out of this license shall be resolved in the courts of Canada.
|
|
28
|
+
|
|
29
|
+
8. Contact Information:
|
|
30
|
+
For inquiries regarding this license, contact operations@pallamart.com.
|
|
31
|
+
|
|
32
|
+
[End of License]
|
package/README.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @pallamart/pm-hono-setup
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pallamart Hono app and router setup with OpenAPI, CSRF, rate limiting and JSON errors.
|
|
4
|
+
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
Use Node.js from `.nvmrc`, then install dependencies:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Available checks:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm run build
|
|
17
|
+
npm run format:check
|
|
18
|
+
npm run lint
|
|
19
|
+
npm test
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The package is published from `dist/`. Source code lives in `src/`; do not edit generated output directly.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './service-bindings.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './service-bindings.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { HttpStatus } from '@pallamart/pm-utilities';
|
|
2
|
+
export declare const ApiKeyAuthConfig: {
|
|
3
|
+
readonly header: "X-API-Key";
|
|
4
|
+
readonly missingKeysMessage: "No API keys are configured for this service";
|
|
5
|
+
readonly openPaths: readonly ["/health"];
|
|
6
|
+
readonly unauthorizedMessage: "Unauthorized";
|
|
7
|
+
readonly unauthorizedStatus: HttpStatus.Unauthorized;
|
|
8
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { HttpHeader, HttpStatus } from '@pallamart/pm-utilities';
|
|
2
|
+
export const ApiKeyAuthConfig = {
|
|
3
|
+
header: HttpHeader.XApiKey,
|
|
4
|
+
missingKeysMessage: 'No API keys are configured for this service',
|
|
5
|
+
openPaths: ['/health'],
|
|
6
|
+
unauthorizedMessage: 'Unauthorized',
|
|
7
|
+
unauthorizedStatus: HttpStatus.Unauthorized,
|
|
8
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HttpMethod, HttpStatus } from '@pallamart/pm-utilities';
|
|
2
|
+
export declare const AppConfig: {
|
|
3
|
+
readonly rateLimitWindowMs: number;
|
|
4
|
+
readonly rateLimitHeaders: "draft-6";
|
|
5
|
+
readonly missingRateLimitMessage: "No Rate Limiter value found";
|
|
6
|
+
readonly missingOriginMessage: "No CORS allowed origin found";
|
|
7
|
+
readonly originSeparator: ",";
|
|
8
|
+
readonly forwardedForHeader: "x-forwarded-for";
|
|
9
|
+
readonly cloudflareIpHeader: "cf-connecting-ip";
|
|
10
|
+
readonly upgradeInsecureHeader: "Upgrade-Insecure-Requests";
|
|
11
|
+
readonly corsMethods: readonly [HttpMethod.Get, HttpMethod.Post, HttpMethod.Put, HttpMethod.Delete, HttpMethod.Options];
|
|
12
|
+
readonly corsHeaders: readonly ["Content-Type"];
|
|
13
|
+
readonly exposedHeaders: readonly ["Content-Length"];
|
|
14
|
+
readonly notFoundStatus: HttpStatus.NotFound;
|
|
15
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { HttpHeader, HttpMethod, HttpStatus } from '@pallamart/pm-utilities';
|
|
2
|
+
export const AppConfig = {
|
|
3
|
+
rateLimitWindowMs: 60 * 1000,
|
|
4
|
+
rateLimitHeaders: 'draft-6',
|
|
5
|
+
missingRateLimitMessage: 'No Rate Limiter value found',
|
|
6
|
+
missingOriginMessage: 'No CORS allowed origin found',
|
|
7
|
+
originSeparator: ',',
|
|
8
|
+
forwardedForHeader: 'x-forwarded-for',
|
|
9
|
+
cloudflareIpHeader: 'cf-connecting-ip',
|
|
10
|
+
upgradeInsecureHeader: 'Upgrade-Insecure-Requests',
|
|
11
|
+
corsMethods: [
|
|
12
|
+
HttpMethod.Get,
|
|
13
|
+
HttpMethod.Post,
|
|
14
|
+
HttpMethod.Put,
|
|
15
|
+
HttpMethod.Delete,
|
|
16
|
+
HttpMethod.Options,
|
|
17
|
+
],
|
|
18
|
+
corsHeaders: [HttpHeader.ContentType],
|
|
19
|
+
exposedHeaders: ['Content-Length'],
|
|
20
|
+
notFoundStatus: HttpStatus.NotFound,
|
|
21
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const OpenApiConfig: {
|
|
2
|
+
readonly specPath: "/openapi.json";
|
|
3
|
+
readonly docsPath: "/";
|
|
4
|
+
readonly openapi: "3.0.0";
|
|
5
|
+
readonly theme: "bluePlanet";
|
|
6
|
+
readonly layout: "classic";
|
|
7
|
+
readonly httpClient: {
|
|
8
|
+
readonly targetKey: "js";
|
|
9
|
+
readonly clientKey: "fetch";
|
|
10
|
+
};
|
|
11
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HttpStatus } from '@pallamart/pm-utilities';
|
|
2
|
+
export declare const UserTokenAuthConfig: {
|
|
3
|
+
readonly header: "Authorization";
|
|
4
|
+
readonly scheme: "Bearer ";
|
|
5
|
+
readonly contextKey: "user";
|
|
6
|
+
readonly openPaths: readonly ["/health"];
|
|
7
|
+
readonly domainSeparator: "@";
|
|
8
|
+
readonly replayCacheLimit: 10000;
|
|
9
|
+
readonly missingSettingsMessage: "User token auth needs public keys, an issuer and an audience";
|
|
10
|
+
readonly missingAllowListMessage: "User token auth needs allowed emails or email domains";
|
|
11
|
+
readonly unauthorizedMessage: "Unauthorized";
|
|
12
|
+
readonly unauthorizedStatus: HttpStatus.Unauthorized;
|
|
13
|
+
readonly forbiddenMessage: "Forbidden";
|
|
14
|
+
readonly forbiddenStatus: HttpStatus.Forbidden;
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HttpAuthScheme, HttpHeader, HttpStatus } from '@pallamart/pm-utilities';
|
|
2
|
+
export const UserTokenAuthConfig = {
|
|
3
|
+
header: HttpHeader.Authorization,
|
|
4
|
+
scheme: HttpAuthScheme.bearerPrefix,
|
|
5
|
+
contextKey: 'user',
|
|
6
|
+
openPaths: ['/health'],
|
|
7
|
+
domainSeparator: '@',
|
|
8
|
+
replayCacheLimit: 10000,
|
|
9
|
+
missingSettingsMessage: 'User token auth needs public keys, an issuer and an audience',
|
|
10
|
+
missingAllowListMessage: 'User token auth needs allowed emails or email domains',
|
|
11
|
+
unauthorizedMessage: 'Unauthorized',
|
|
12
|
+
unauthorizedStatus: HttpStatus.Unauthorized,
|
|
13
|
+
forbiddenMessage: 'Forbidden',
|
|
14
|
+
forbiddenStatus: HttpStatus.Forbidden,
|
|
15
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * as Bindings from './bindings/index.js';
|
|
2
|
+
export * as Server from './setup/index.js';
|
|
3
|
+
export * from './constants/api-key-auth-config.js';
|
|
4
|
+
export * from './constants/user-token-auth-config.js';
|
|
5
|
+
export * from './middleware/api-key-auth.js';
|
|
6
|
+
export * from './middleware/user-token-auth.js';
|
|
7
|
+
export * from './models/api-key-auth-options.js';
|
|
8
|
+
export * from './models/json-error-body-data.js';
|
|
9
|
+
export * from './models/json-error-data.js';
|
|
10
|
+
export * from './models/user-token-auth-options.js';
|
|
11
|
+
export * from './services/json-error-service.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * as Bindings from './bindings/index.js';
|
|
2
|
+
export * as Server from './setup/index.js';
|
|
3
|
+
export * from './constants/api-key-auth-config.js';
|
|
4
|
+
export * from './constants/user-token-auth-config.js';
|
|
5
|
+
export * from './middleware/api-key-auth.js';
|
|
6
|
+
export * from './middleware/user-token-auth.js';
|
|
7
|
+
export * from './models/api-key-auth-options.js';
|
|
8
|
+
export * from './models/json-error-body-data.js';
|
|
9
|
+
export * from './models/json-error-data.js';
|
|
10
|
+
export * from './models/user-token-auth-options.js';
|
|
11
|
+
export * from './services/json-error-service.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { TimingSafeService } from '@pallamart/pm-utilities/server';
|
|
2
|
+
import { ApiKeyAuthConfig } from '../constants/api-key-auth-config.js';
|
|
3
|
+
import { JsonErrorService } from '../services/json-error-service.js';
|
|
4
|
+
export const apiKeyAuth = ({ keys, openPaths = [] }) => {
|
|
5
|
+
const allowed = keys.filter((key) => key !== undefined && key !== '');
|
|
6
|
+
if (allowed.length === 0)
|
|
7
|
+
throw new Error(ApiKeyAuthConfig.missingKeysMessage);
|
|
8
|
+
const open = [...ApiKeyAuthConfig.openPaths, ...openPaths];
|
|
9
|
+
return async (c, next) => {
|
|
10
|
+
if (open.some((path) => c.req.path === path || c.req.path.startsWith(`${path}/`)))
|
|
11
|
+
return await next();
|
|
12
|
+
const candidate = c.req.header(ApiKeyAuthConfig.header);
|
|
13
|
+
if (candidate === undefined || !allowed.some((key) => TimingSafeService.equals(candidate, key)))
|
|
14
|
+
return JsonErrorService.respond(c, ApiKeyAuthConfig.unauthorizedMessage, ApiKeyAuthConfig.unauthorizedStatus);
|
|
15
|
+
return await next();
|
|
16
|
+
};
|
|
17
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Context, Next } from 'hono';
|
|
2
|
+
import type { UserTokenAuthOptions } from '../models/user-token-auth-options.js';
|
|
3
|
+
export declare const userTokenAuth: ({ publicKeys, issuer, audience, allowedEmails, allowedEmailDomains, openPaths, }: UserTokenAuthOptions) => (c: Context, next: Next) => Promise<void | Response>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { UserTokenService } from '@pallamart/pm-utilities';
|
|
2
|
+
import { UserTokenAuthConfig } from '../constants/user-token-auth-config.js';
|
|
3
|
+
import { UserTokenAccessService } from '../services/user-token-access-service.js';
|
|
4
|
+
import { UserTokenRefusalService } from '../services/user-token-refusal-service.js';
|
|
5
|
+
import { UserTokenReplayService } from '../services/user-token-replay-service.js';
|
|
6
|
+
export const userTokenAuth = ({ publicKeys, issuer, audience, allowedEmails, allowedEmailDomains, openPaths = [], }) => {
|
|
7
|
+
const keys = publicKeys.filter((key) => key !== undefined && key !== '');
|
|
8
|
+
if (keys.length === 0 || !issuer || !audience)
|
|
9
|
+
throw new Error(UserTokenAuthConfig.missingSettingsMessage);
|
|
10
|
+
const emails = UserTokenAccessService.list(allowedEmails);
|
|
11
|
+
const domains = UserTokenAccessService.list(allowedEmailDomains);
|
|
12
|
+
if (emails.length === 0 && domains.length === 0)
|
|
13
|
+
throw new Error(UserTokenAuthConfig.missingAllowListMessage);
|
|
14
|
+
const open = [...UserTokenAuthConfig.openPaths, ...openPaths];
|
|
15
|
+
return async (c, next) => {
|
|
16
|
+
if (open.some((path) => c.req.path === path || c.req.path.startsWith(`${path}/`)))
|
|
17
|
+
return await next();
|
|
18
|
+
const header = c.req.header(UserTokenAuthConfig.header);
|
|
19
|
+
if (header === undefined || !header.startsWith(UserTokenAuthConfig.scheme))
|
|
20
|
+
return UserTokenRefusalService.unauthorized(c);
|
|
21
|
+
const url = new URL(c.req.url);
|
|
22
|
+
const claims = await UserTokenService.verify(header.slice(UserTokenAuthConfig.scheme.length).trim(), { method: c.req.method, path: `${url.pathname}${url.search}` }, { issuer, audience, publicKeys: keys });
|
|
23
|
+
if (claims === null || !UserTokenReplayService.claim(claims.jti, claims.exp))
|
|
24
|
+
return UserTokenRefusalService.unauthorized(c);
|
|
25
|
+
if (!UserTokenAccessService.isAllowed(claims.email, emails, domains))
|
|
26
|
+
return UserTokenRefusalService.forbidden(c);
|
|
27
|
+
c.set(UserTokenAuthConfig.contextKey, claims);
|
|
28
|
+
return await next();
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
3
|
+
import type { JsonErrorBodyData } from '../models/json-error-body-data.js';
|
|
4
|
+
export declare class JsonErrorService {
|
|
5
|
+
static body(message: string, status: ContentfulStatusCode): JsonErrorBodyData;
|
|
6
|
+
static respond(c: Context, message: string, status: ContentfulStatusCode): Response;
|
|
7
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { UserTokenAuthConfig } from '../constants/user-token-auth-config.js';
|
|
2
|
+
export class UserTokenAccessService {
|
|
3
|
+
static list(values) {
|
|
4
|
+
if (values === undefined)
|
|
5
|
+
return [];
|
|
6
|
+
return values
|
|
7
|
+
.filter((value) => typeof value === 'string')
|
|
8
|
+
.flatMap((value) => value.split(','))
|
|
9
|
+
.map((value) => value.trim().toLowerCase())
|
|
10
|
+
.filter((value) => value !== '');
|
|
11
|
+
}
|
|
12
|
+
static isAllowed(email, emails, domains) {
|
|
13
|
+
const address = email.trim().toLowerCase();
|
|
14
|
+
if (emails.includes(address))
|
|
15
|
+
return true;
|
|
16
|
+
const parts = address.split(UserTokenAuthConfig.domainSeparator);
|
|
17
|
+
return parts.length === 2 && parts[0] !== '' && domains.includes(parts[1]);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { UserTokenAuthConfig } from '../constants/user-token-auth-config.js';
|
|
2
|
+
import { JsonErrorService } from './json-error-service.js';
|
|
3
|
+
export class UserTokenRefusalService {
|
|
4
|
+
static unauthorized(c) {
|
|
5
|
+
return JsonErrorService.respond(c, UserTokenAuthConfig.unauthorizedMessage, UserTokenAuthConfig.unauthorizedStatus);
|
|
6
|
+
}
|
|
7
|
+
static forbidden(c) {
|
|
8
|
+
return JsonErrorService.respond(c, UserTokenAuthConfig.forbiddenMessage, UserTokenAuthConfig.forbiddenStatus);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { DateService, UserTokenConfig } from '@pallamart/pm-utilities';
|
|
2
|
+
import { UserTokenAuthConfig } from '../constants/user-token-auth-config.js';
|
|
3
|
+
export class UserTokenReplayService {
|
|
4
|
+
static claim(tokenId, expiresAt) {
|
|
5
|
+
const now = DateService.nowUnixSeconds();
|
|
6
|
+
this._forgetExpired(now);
|
|
7
|
+
if (this._seen.has(tokenId))
|
|
8
|
+
return false;
|
|
9
|
+
if (this._seen.size >= UserTokenAuthConfig.replayCacheLimit) {
|
|
10
|
+
const oldest = this._seen.keys().next().value;
|
|
11
|
+
if (oldest !== undefined)
|
|
12
|
+
this._seen.delete(oldest);
|
|
13
|
+
}
|
|
14
|
+
this._seen.set(tokenId, expiresAt + UserTokenConfig.clockSkewSeconds);
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
static _forgetExpired(now) {
|
|
18
|
+
const expired = [];
|
|
19
|
+
this._seen.forEach((expiresAt, tokenId) => {
|
|
20
|
+
if (expiresAt < now)
|
|
21
|
+
expired.push(tokenId);
|
|
22
|
+
});
|
|
23
|
+
expired.forEach((tokenId) => this._seen.delete(tokenId));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
UserTokenReplayService._seen = new Map();
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { OpenAPIV3 } from 'openapi-types';
|
|
3
|
+
import type { AppOpenAPI } from '../bindings/service-bindings.js';
|
|
4
|
+
export declare const configureOpenAPI: (app: AppOpenAPI, name: string, version: string, middleware?: MiddlewareHandler, security?: OpenAPIV3.SecurityRequirementObject[]) => void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { apiReference } from '@scalar/hono-api-reference';
|
|
2
|
+
import { OpenApiConfig } from '../constants/openapi-config.js';
|
|
3
|
+
export const configureOpenAPI = (app, name, version, middleware, security = []) => {
|
|
4
|
+
app.doc(OpenApiConfig.specPath, {
|
|
5
|
+
openapi: OpenApiConfig.openapi,
|
|
6
|
+
security,
|
|
7
|
+
info: {
|
|
8
|
+
version,
|
|
9
|
+
title: name,
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
const reference = apiReference({
|
|
13
|
+
theme: OpenApiConfig.theme,
|
|
14
|
+
layout: OpenApiConfig.layout,
|
|
15
|
+
defaultHttpClient: OpenApiConfig.httpClient,
|
|
16
|
+
hideDownloadButton: true,
|
|
17
|
+
url: OpenApiConfig.specPath,
|
|
18
|
+
});
|
|
19
|
+
if (middleware === undefined)
|
|
20
|
+
app.get(OpenApiConfig.docsPath, reference);
|
|
21
|
+
else
|
|
22
|
+
app.get(OpenApiConfig.docsPath, middleware, reference);
|
|
23
|
+
};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
export declare const createApp: (getIP: (c: Context) => string, getCustomHeaders?: () => string[], useCSRF?: boolean, csrfExemptPrefixes?: string[]) => import("@hono/zod-openapi").OpenAPIHono<import("../bindings/service-bindings.js").ServiceBindings, {}, "/">;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { rateLimiter } from 'hono-rate-limiter';
|
|
2
|
+
import { cors } from 'hono/cors';
|
|
3
|
+
import { csrf } from 'hono/csrf';
|
|
4
|
+
import { prettyJSON } from 'hono/pretty-json';
|
|
5
|
+
import { secureHeaders } from 'hono/secure-headers';
|
|
6
|
+
import { AppConfig } from '../constants/app-config.js';
|
|
7
|
+
import { JsonErrorService } from '../services/json-error-service.js';
|
|
8
|
+
import { createRouter } from './create-router.js';
|
|
9
|
+
export const createApp = (getIP, getCustomHeaders = () => [], useCSRF = true, csrfExemptPrefixes = []) => {
|
|
10
|
+
const app = createRouter();
|
|
11
|
+
if (!process.env.HONO_RATE_LIMIT)
|
|
12
|
+
throw new Error(AppConfig.missingRateLimitMessage);
|
|
13
|
+
const limit = parseInt(process.env.HONO_RATE_LIMIT);
|
|
14
|
+
app.use(rateLimiter({
|
|
15
|
+
windowMs: AppConfig.rateLimitWindowMs,
|
|
16
|
+
limit,
|
|
17
|
+
standardHeaders: AppConfig.rateLimitHeaders,
|
|
18
|
+
keyGenerator: (c) => c.req.header(AppConfig.forwardedForHeader) ||
|
|
19
|
+
c.req.header(AppConfig.cloudflareIpHeader) ||
|
|
20
|
+
getIP(c),
|
|
21
|
+
}));
|
|
22
|
+
if (!process.env.ALLOWED_ORIGIN)
|
|
23
|
+
throw new Error(AppConfig.missingOriginMessage);
|
|
24
|
+
const allowedOrigins = process.env.ALLOWED_ORIGIN.split(AppConfig.originSeparator);
|
|
25
|
+
app.use('*', cors({
|
|
26
|
+
origin: allowedOrigins,
|
|
27
|
+
allowHeaders: [
|
|
28
|
+
AppConfig.upgradeInsecureHeader,
|
|
29
|
+
...AppConfig.corsHeaders,
|
|
30
|
+
...getCustomHeaders(),
|
|
31
|
+
],
|
|
32
|
+
allowMethods: [...AppConfig.corsMethods],
|
|
33
|
+
exposeHeaders: [...AppConfig.exposedHeaders],
|
|
34
|
+
}));
|
|
35
|
+
if (useCSRF) {
|
|
36
|
+
const protect = csrf({ origin: allowedOrigins });
|
|
37
|
+
app.use(async (c, next) => {
|
|
38
|
+
if (csrfExemptPrefixes.some((prefix) => c.req.path.startsWith(prefix)))
|
|
39
|
+
return await next();
|
|
40
|
+
return await protect(c, next);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
app.use('*', secureHeaders({
|
|
44
|
+
xFrameOptions: false,
|
|
45
|
+
xXssProtection: true,
|
|
46
|
+
}));
|
|
47
|
+
app.use('*', prettyJSON());
|
|
48
|
+
app.notFound((c) => JsonErrorService.respond(c, `404 Not Found - ${c.req.path}`, AppConfig.notFoundStatus));
|
|
49
|
+
return app;
|
|
50
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,77 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@pallamart/pm-hono-setup",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"/dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"husky:prepare": "npx husky",
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"lint": "npm run lint:eslint",
|
|
23
|
+
"lint:fix": "npm run lint:eslint:fix",
|
|
24
|
+
"format:check": "npx prettier --check --ignore-unknown \"src/**/*.{ts,json,md}\"",
|
|
25
|
+
"format:prettier": "npx prettier --write \"src/**/*.{ts,json,md}\"",
|
|
26
|
+
"lint:eslint": "npx eslint \"src/**/*.ts\"",
|
|
27
|
+
"lint:eslint:fix": "npx eslint --fix \"src/**/*.ts\"",
|
|
28
|
+
"release": "semantic-release",
|
|
29
|
+
"test": "vitest run"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@commitlint/cli": "^20.1.0",
|
|
33
|
+
"@commitlint/config-conventional": "^20.0.0",
|
|
34
|
+
"@eslint/compat": "^2.0.0",
|
|
35
|
+
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
|
36
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
37
|
+
"@semantic-release/commit-analyzer": "^13.0.0",
|
|
38
|
+
"@semantic-release/git": "^10.0.1",
|
|
39
|
+
"@semantic-release/github": "^11.0.1",
|
|
40
|
+
"@semantic-release/npm": "^12.0.0",
|
|
41
|
+
"@semantic-release/release-notes-generator": "^14.0.1",
|
|
42
|
+
"@types/node": "^22.0.0",
|
|
43
|
+
"@typescript-eslint/eslint-plugin": "^8.48.0",
|
|
44
|
+
"@typescript-eslint/parser": "^8.48.0",
|
|
45
|
+
"commitlint": "^20.1.0",
|
|
46
|
+
"conventional-changelog-conventionalcommits": "^7.0.2",
|
|
47
|
+
"eslint": "^9.39.1",
|
|
48
|
+
"eslint-config-prettier": "^10.1.8",
|
|
49
|
+
"eslint-plugin-import": "^2.32.0",
|
|
50
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
51
|
+
"husky": "^9.1.7",
|
|
52
|
+
"lint-staged": "^16.2.7",
|
|
53
|
+
"prettier": "^3.6.2",
|
|
54
|
+
"semantic-release": "^24.2.0",
|
|
55
|
+
"ts-node": "^10.9.2",
|
|
56
|
+
"typescript": "^5.9.3",
|
|
57
|
+
"vitest": "^3.2.7"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public",
|
|
61
|
+
"registry": "https://registry.npmjs.org/"
|
|
62
|
+
},
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "git+https://github.com/pallamart/pm-hono-setup.git"
|
|
66
|
+
},
|
|
67
|
+
"description": "Pallamart Hono app and router setup with OpenAPI, CSRF, rate limiting and JSON errors",
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@hono/zod-openapi": "^1.1.5",
|
|
70
|
+
"@pallamart/pm-utilities": "^2.1.0",
|
|
71
|
+
"@scalar/hono-api-reference": "^0.9.48",
|
|
72
|
+
"hono": "^4.10.6",
|
|
73
|
+
"hono-rate-limiter": "^0.4.2",
|
|
74
|
+
"openapi-types": "^12.1.3",
|
|
75
|
+
"zod": "^4.1.13"
|
|
76
|
+
}
|
|
77
|
+
}
|