@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.
Files changed (45) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +21 -2
  3. package/dist/bindings/index.d.ts +1 -0
  4. package/dist/bindings/index.js +1 -0
  5. package/dist/bindings/service-bindings.d.ts +6 -0
  6. package/dist/bindings/service-bindings.js +1 -0
  7. package/dist/constants/api-key-auth-config.d.ts +8 -0
  8. package/dist/constants/api-key-auth-config.js +8 -0
  9. package/dist/constants/app-config.d.ts +15 -0
  10. package/dist/constants/app-config.js +21 -0
  11. package/dist/constants/openapi-config.d.ts +11 -0
  12. package/dist/constants/openapi-config.js +8 -0
  13. package/dist/constants/user-token-auth-config.d.ts +15 -0
  14. package/dist/constants/user-token-auth-config.js +15 -0
  15. package/dist/index.d.ts +11 -0
  16. package/dist/index.js +11 -0
  17. package/dist/middleware/api-key-auth.d.ts +3 -0
  18. package/dist/middleware/api-key-auth.js +17 -0
  19. package/dist/middleware/user-token-auth.d.ts +3 -0
  20. package/dist/middleware/user-token-auth.js +30 -0
  21. package/dist/models/api-key-auth-options.d.ts +4 -0
  22. package/dist/models/api-key-auth-options.js +1 -0
  23. package/dist/models/json-error-body-data.d.ts +5 -0
  24. package/dist/models/json-error-body-data.js +1 -0
  25. package/dist/models/json-error-data.d.ts +5 -0
  26. package/dist/models/json-error-data.js +1 -0
  27. package/dist/models/user-token-auth-options.d.ts +8 -0
  28. package/dist/models/user-token-auth-options.js +1 -0
  29. package/dist/services/json-error-service.d.ts +7 -0
  30. package/dist/services/json-error-service.js +8 -0
  31. package/dist/services/user-token-access-service.d.ts +4 -0
  32. package/dist/services/user-token-access-service.js +19 -0
  33. package/dist/services/user-token-refusal-service.d.ts +5 -0
  34. package/dist/services/user-token-refusal-service.js +10 -0
  35. package/dist/services/user-token-replay-service.d.ts +5 -0
  36. package/dist/services/user-token-replay-service.js +26 -0
  37. package/dist/setup/configure-openapi.d.ts +4 -0
  38. package/dist/setup/configure-openapi.js +23 -0
  39. package/dist/setup/create-app.d.ts +2 -0
  40. package/dist/setup/create-app.js +50 -0
  41. package/dist/setup/create-router.d.ts +3 -0
  42. package/dist/setup/create-router.js +4 -0
  43. package/dist/setup/index.d.ts +3 -0
  44. package/dist/setup/index.js +3 -0
  45. 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
- # Temporary Holding Version
1
+ # @pallamart/pm-hono-setup
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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,6 @@
1
+ import type { OpenAPIHono } from '@hono/zod-openapi';
2
+ export interface ServiceBindings {
3
+ Bindings: NodeJS.ProcessEnv;
4
+ Variables: Record<string, string | number | boolean>;
5
+ }
6
+ export type AppOpenAPI = OpenAPIHono<ServiceBindings>;
@@ -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,8 @@
1
+ export const OpenApiConfig = {
2
+ specPath: '/openapi.json',
3
+ docsPath: '/',
4
+ openapi: '3.0.0',
5
+ theme: 'bluePlanet',
6
+ layout: 'classic',
7
+ httpClient: { targetKey: 'js', clientKey: 'fetch' },
8
+ };
@@ -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
+ };
@@ -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,3 @@
1
+ import type { Context, Next } from 'hono';
2
+ import type { ApiKeyAuthOptions } from '../models/api-key-auth-options.js';
3
+ export declare const apiKeyAuth: ({ keys, openPaths }: ApiKeyAuthOptions) => (c: Context, next: Next) => Promise<void | Response>;
@@ -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,4 @@
1
+ export interface ApiKeyAuthOptions {
2
+ keys: (string | undefined)[];
3
+ openPaths?: string[];
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { JsonErrorData } from './json-error-data';
2
+ export interface JsonErrorBodyData {
3
+ success: false;
4
+ error: JsonErrorData;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ import type { ContentfulStatusCode } from 'hono/utils/http-status';
2
+ export interface JsonErrorData {
3
+ message: string;
4
+ status: ContentfulStatusCode;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ export interface UserTokenAuthOptions {
2
+ publicKeys: (string | undefined)[];
3
+ issuer: string | undefined;
4
+ audience: string | undefined;
5
+ allowedEmails?: (string | undefined)[];
6
+ allowedEmailDomains?: (string | undefined)[];
7
+ openPaths?: string[];
8
+ }
@@ -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,8 @@
1
+ export class JsonErrorService {
2
+ static body(message, status) {
3
+ return { success: false, error: { message, status } };
4
+ }
5
+ static respond(c, message, status) {
6
+ return c.json(JsonErrorService.body(message, status), status);
7
+ }
8
+ }
@@ -0,0 +1,4 @@
1
+ export declare class UserTokenAccessService {
2
+ static list(values: (string | undefined)[] | undefined): string[];
3
+ static isAllowed(email: string, emails: string[], domains: string[]): boolean;
4
+ }
@@ -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,5 @@
1
+ import type { Context } from 'hono';
2
+ export declare class UserTokenRefusalService {
3
+ static unauthorized(c: Context): Response;
4
+ static forbidden(c: Context): Response;
5
+ }
@@ -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,5 @@
1
+ export declare class UserTokenReplayService {
2
+ private static _seen;
3
+ static claim(tokenId: string, expiresAt: number): boolean;
4
+ private static _forgetExpired;
5
+ }
@@ -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
+ };
@@ -0,0 +1,3 @@
1
+ import { OpenAPIHono } from '@hono/zod-openapi';
2
+ import type { ServiceBindings } from '../bindings/service-bindings.js';
3
+ export declare const createRouter: () => OpenAPIHono<ServiceBindings, {}, "/">;
@@ -0,0 +1,4 @@
1
+ import { OpenAPIHono } from '@hono/zod-openapi';
2
+ export const createRouter = () => new OpenAPIHono({
3
+ strict: false,
4
+ });
@@ -0,0 +1,3 @@
1
+ export * from './configure-openapi.js';
2
+ export * from './create-app.js';
3
+ export * from './create-router.js';
@@ -0,0 +1,3 @@
1
+ export * from './configure-openapi.js';
2
+ export * from './create-app.js';
3
+ export * from './create-router.js';
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
- "name": "@pallamart/pm-hono-setup",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }