@bgord/bun 0.1.0 → 0.2.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/dist/index.d.ts CHANGED
@@ -1,9 +1,16 @@
1
- export * from "./graceful-shutdown";
1
+ export * from "./api-key-shield";
2
2
  export * from "./api-version";
3
- export * from "./time-zone-offset";
3
+ export * from "./auth-shield";
4
4
  export * from "./context";
5
5
  export * from "./etag-extractor";
6
+ export * from "./graceful-shutdown";
7
+ export * from "./healthcheck";
6
8
  export * from "./http-logger";
7
- export * from "./api-key-shield";
8
9
  export * from "./rate-limit-shield";
9
- export * from "./healthcheck";
10
+ export * from "./time-zone-offset";
11
+ export * from "./cache-static-files";
12
+ export * from "./cache-response";
13
+ export * from "./download-file";
14
+ export * from "./i18n";
15
+ export * from "./file-uploader";
16
+ export * from "./image-processor";
@@ -4,8 +4,5 @@ declare type RateLimitShieldOptionsType = {
4
4
  ms: bg.Schema.TimestampType;
5
5
  };
6
6
  export declare const TooManyRequestsError: HTTPException;
7
- export declare const rateLimitShield: (options: RateLimitShieldOptionsType) => import("hono/types").MiddlewareHandler<{
8
- Bindings: any;
9
- Variables: any;
10
- }, string, {}>;
7
+ export declare const rateLimitShield: (options: RateLimitShieldOptionsType) => import("hono").MiddlewareHandler<any, string, {}>;
11
8
  export {};
@@ -8,10 +8,7 @@ export declare type TimeZoneOffsetVariables = {
8
8
  };
9
9
  export declare class TimeZoneOffset {
10
10
  static TIME_ZONE_OFFSET_HEADER_NAME: string;
11
- static attach: import("hono/types").MiddlewareHandler<{
12
- Bindings: any;
13
- Variables: any;
14
- }, string, {}>;
11
+ static attach: import("hono").MiddlewareHandler<any, string, {}>;
15
12
  static adjustTimestamp(timestamp: bg.Schema.TimestampType, timeZoneOffsetMs: bg.Schema.TimeZoneOffsetValueType): bg.Schema.TimestampType;
16
13
  static adjustDate(timestamp: bg.Schema.TimestampType, timeZoneOffsetMs: bg.Schema.TimeZoneOffsetValueType): Date;
17
14
  }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.0",
2
+ "version": "0.2.0",
3
3
  "license": "MIT",
4
4
  "name": "@bgord/bun",
5
5
  "type": "module",
@@ -28,9 +28,9 @@
28
28
  "@commitlint/config-conventional": "19.5.0",
29
29
  "@types/bun": "1.1.11",
30
30
  "@types/lodash": "4.17.10",
31
- "cspell": "8.14.4",
31
+ "cspell": "8.15.3",
32
32
  "husky": "9.1.6",
33
- "knip": "5.33.2",
33
+ "knip": "5.33.3",
34
34
  "microbundle": "0.15.1",
35
35
  "only-allow": "1.2.1",
36
36
  "shellcheck": "3.0.0",
@@ -39,8 +39,9 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@bgord/node": "0.82.0",
42
- "hono": "4.6.4",
43
- "lodash": "4.17.21"
42
+ "hono": "4.6.5",
43
+ "lodash": "4.17.21",
44
+ "sharp": "0.33.5"
44
45
  },
45
46
  "resolutions": {
46
47
  "typescript": "4.7.4"
@@ -0,0 +1,152 @@
1
+ import hono from "hono";
2
+ import * as bgn from "@bgord/node";
3
+ import { Lucia } from "lucia";
4
+ import { createMiddleware } from "hono/factory";
5
+ import { HTTPException } from "hono/http-exception";
6
+
7
+ class SessionId {
8
+ private value: string | null;
9
+
10
+ constructor(cookie: string | undefined, lucia: Lucia) {
11
+ this.value = lucia.readSessionCookie(cookie ?? "");
12
+ }
13
+
14
+ get(): SessionId["value"] {
15
+ return this.value;
16
+ }
17
+ }
18
+
19
+ type AuthShieldConfigType<T> = {
20
+ Username: typeof bgn.Username;
21
+ Password: typeof bgn.Password;
22
+ HashedPassword: typeof bgn.HashedPassword;
23
+ lucia: Lucia;
24
+ findUniqueUserOrThrow: (username: bgn.Username) => Promise<T>;
25
+ };
26
+
27
+ export const AccessDeniedAuthShieldError = new HTTPException(403, {
28
+ message: "access_denied_auth_shield",
29
+ });
30
+
31
+ export class AuthShield<
32
+ T extends { password: bgn.PasswordType; id: bgn.IdType }
33
+ > {
34
+ private readonly config: AuthShieldConfigType<T>;
35
+
36
+ constructor(
37
+ overrides: Omit<
38
+ AuthShieldConfigType<T>,
39
+ "Username" | "Password" | "HashedPassword"
40
+ > & {
41
+ Username?: typeof bgn.Username;
42
+ Password?: typeof bgn.Password;
43
+ HashedPassword?: typeof bgn.HashedPassword;
44
+ }
45
+ ) {
46
+ const config = {
47
+ Username: overrides.Username ?? bgn.Username,
48
+ Password: overrides.Password ?? bgn.Password,
49
+ HashedPassword: overrides.HashedPassword ?? bgn.HashedPassword,
50
+ lucia: overrides.lucia,
51
+ findUniqueUserOrThrow: overrides.findUniqueUserOrThrow,
52
+ };
53
+
54
+ this.config = config;
55
+ }
56
+
57
+ verify = createMiddleware(async (c: hono.Context, next: hono.Next) => {
58
+ const user = c.get("user");
59
+
60
+ if (!user) {
61
+ throw AccessDeniedAuthShieldError;
62
+ }
63
+
64
+ return next();
65
+ });
66
+
67
+ reverse = createMiddleware(async (c: hono.Context, next: hono.Next) => {
68
+ const user = c.get("user");
69
+
70
+ if (user) {
71
+ throw AccessDeniedAuthShieldError;
72
+ }
73
+
74
+ return next();
75
+ });
76
+
77
+ detach = createMiddleware(async (c: hono.Context, next: hono.Next) => {
78
+ const cookie = c.req.header("cookie");
79
+
80
+ const sessionId = new SessionId(cookie, this.config.lucia).get();
81
+
82
+ if (!sessionId) return next();
83
+
84
+ await this.config.lucia.invalidateSession(sessionId);
85
+
86
+ return next();
87
+ });
88
+
89
+ build = createMiddleware(async (c: hono.Context, next: hono.Next) => {
90
+ const cookie = c.req.header("cookie");
91
+
92
+ const sessionId = new SessionId(cookie, this.config.lucia).get();
93
+
94
+ if (!sessionId) {
95
+ c.set("user", null);
96
+ c.set("session", null);
97
+
98
+ return next();
99
+ }
100
+
101
+ const { session, user } = await this.config.lucia.validateSession(
102
+ sessionId
103
+ );
104
+
105
+ if (!session) {
106
+ c.res.headers.set(
107
+ "Set-Cookie",
108
+ this.config.lucia.createBlankSessionCookie().serialize()
109
+ );
110
+ c.set("user", null);
111
+ c.set("session", null);
112
+
113
+ return next();
114
+ }
115
+
116
+ if (session.fresh) {
117
+ c.res.headers.set(
118
+ "Set-Cookie",
119
+ this.config.lucia.createSessionCookie(session.id).serialize()
120
+ );
121
+ }
122
+ c.set("user", user);
123
+ c.set("session", session);
124
+
125
+ return next();
126
+ });
127
+
128
+ attach = createMiddleware(async (c: hono.Context, next: hono.Next) => {
129
+ try {
130
+ const body = await c.req.raw.clone().formData();
131
+
132
+ const username = new this.config.Username(body.get("username") as string);
133
+ const password = new this.config.Password(body.get("password") as string);
134
+
135
+ const user = await this.config.findUniqueUserOrThrow(username);
136
+
137
+ const hashedPassword = await this.config.HashedPassword.fromHash(
138
+ user.password
139
+ );
140
+ await hashedPassword.matchesOrThrow(password);
141
+
142
+ const session = await this.config.lucia.createSession(user.id, {});
143
+ const sessionCookie = this.config.lucia.createSessionCookie(session.id);
144
+
145
+ c.res.headers.set("Set-Cookie", sessionCookie.serialize());
146
+
147
+ return next();
148
+ } catch (error) {
149
+ throw AccessDeniedAuthShieldError;
150
+ }
151
+ });
152
+ }
@@ -0,0 +1,35 @@
1
+ import { createMiddleware } from "hono/factory";
2
+ import NodeCache from "node-cache";
3
+ import _ from "lodash";
4
+
5
+ export enum CacheHitEnum {
6
+ hit = "hit",
7
+ miss = "miss",
8
+ }
9
+
10
+ export class CacheResponse {
11
+ static readonly CACHE_HIT_HEADER = "Cache-Hit";
12
+
13
+ constructor(private readonly cache: NodeCache) {}
14
+
15
+ handle = createMiddleware(async (c, next) => {
16
+ const url = _.escape(c.req.url);
17
+
18
+ if (this.cache.has(url)) {
19
+ c.res.headers.set(CacheResponse.CACHE_HIT_HEADER, CacheHitEnum.hit);
20
+
21
+ // @ts-ignore
22
+ return c.json(this.cache.get(url));
23
+ }
24
+
25
+ c.res.headers.set(CacheResponse.CACHE_HIT_HEADER, CacheHitEnum.miss);
26
+
27
+ return next();
28
+ });
29
+
30
+ clear = createMiddleware(async (_c, next) => {
31
+ this.cache.flushAll();
32
+
33
+ return next();
34
+ });
35
+ }
@@ -0,0 +1,34 @@
1
+ import * as bg from "@bgord/node";
2
+ import { createMiddleware } from "hono/factory";
3
+
4
+ export enum CacheStaticFilesStrategy {
5
+ never = "never",
6
+ always = "always",
7
+ five_minutes = "five_minutes",
8
+ }
9
+
10
+ export class CacheStaticFiles {
11
+ static handle(strategy: CacheStaticFilesStrategy) {
12
+ return createMiddleware(async (c, next) => {
13
+ if (strategy === CacheStaticFilesStrategy.never) {
14
+ c.res.headers.set(
15
+ "cache-control",
16
+ "private, no-cache, no-store, must-revalidate"
17
+ );
18
+ }
19
+ if (strategy === CacheStaticFilesStrategy.always) {
20
+ c.res.headers.set(
21
+ "cache-control",
22
+ `public, max-age=${bg.Time.Days(365).seconds}, immutable`
23
+ );
24
+ }
25
+ if (strategy === CacheStaticFilesStrategy.five_minutes) {
26
+ c.res.headers.set(
27
+ "cache-control",
28
+ `public, max-age=${bg.Time.Minutes(5).seconds}`
29
+ );
30
+ }
31
+ return next();
32
+ });
33
+ }
34
+ }
@@ -0,0 +1,15 @@
1
+ import * as bgn from "@bgord/node";
2
+ import type { PathLike } from "node:fs";
3
+
4
+ export type DownloadFileConfigType = { filename: PathLike; mime: bgn.Mime };
5
+
6
+ export class DownloadFile {
7
+ static attach(config: DownloadFileConfigType) {
8
+ return {
9
+ headers: new Headers({
10
+ "Content-Disposition": `attachment; filename="${config.filename}"`,
11
+ "Content-Type": config.mime.raw,
12
+ }),
13
+ };
14
+ }
15
+ }
@@ -0,0 +1,48 @@
1
+ import * as bgn from "@bgord/node";
2
+ import { createMiddleware } from "hono/factory";
3
+ import { HTTPException } from "hono/http-exception";
4
+ import { bodyLimit } from "hono/body-limit";
5
+
6
+ export const InvalidFileMimeTypeError = new HTTPException(400, {
7
+ message: "invalid_file_mime_type_error",
8
+ });
9
+
10
+ export const FileTooBigError = new HTTPException(400, {
11
+ message: "file_too_big_error",
12
+ });
13
+
14
+ type FileUploaderConfigType = {
15
+ mimeTypes: string[];
16
+ maxFilesSize: bgn.SizeValueType;
17
+ };
18
+
19
+ export class FileUploader {
20
+ static validate(config: FileUploaderConfigType) {
21
+ return [
22
+ bodyLimit({
23
+ maxSize: config.maxFilesSize,
24
+ onError: () => {
25
+ throw FileTooBigError;
26
+ },
27
+ }),
28
+
29
+ createMiddleware(async (c, next) => {
30
+ const body = await c.req.raw.clone().formData();
31
+
32
+ const file = body.get("file");
33
+
34
+ if (!(file instanceof File)) {
35
+ throw InvalidFileMimeTypeError;
36
+ }
37
+
38
+ const contentType = new bgn.Mime(file.type);
39
+ const accepted = config.mimeTypes.some((acceptedMimeType) =>
40
+ new bgn.Mime(acceptedMimeType).isSatisfiedBy(contentType)
41
+ );
42
+
43
+ if (!accepted) throw InvalidFileMimeTypeError;
44
+ return next();
45
+ }),
46
+ ];
47
+ }
48
+ }
@@ -52,13 +52,13 @@ export class HttpLogger {
52
52
  let body: any;
53
53
 
54
54
  try {
55
- body = await c.req.json();
55
+ body = await c.req.raw.clone().json();
56
56
  } catch (error) {}
57
57
 
58
58
  const httpRequestBeforeMetadata = {
59
59
  params: c.req.param(),
60
60
  headers: _.omit(
61
- c.req.raw.headers.toJSON(),
61
+ c.req.raw.clone().headers.toJSON(),
62
62
  HttpLogger.uninformativeHeaders
63
63
  ),
64
64
  body,
@@ -80,9 +80,9 @@ export class HttpLogger {
80
80
 
81
81
  await next();
82
82
 
83
- const cacheHitHeader = c.res.headers.get(
84
- bg.CacheResponse.CACHE_HIT_HEADER
85
- );
83
+ const cacheHitHeader = c.res
84
+ .clone()
85
+ .headers.get(bg.CacheResponse.CACHE_HIT_HEADER);
86
86
 
87
87
  const cacheHit =
88
88
  cacheHitHeader === bg.CacheHitEnum.hit
@@ -99,7 +99,7 @@ export class HttpLogger {
99
99
  cacheHit,
100
100
  };
101
101
 
102
- const serverTimingMs = c.res.headers.get("Server-Timing");
102
+ const serverTimingMs = c.res.clone().headers.get("Server-Timing");
103
103
 
104
104
  const durationMsMatch =
105
105
  serverTimingMs?.match(/dur=([0-9]*\.?[0-9]+)/) ?? undefined;
package/src/i18n.ts ADDED
@@ -0,0 +1,110 @@
1
+ import * as bgn from "@bgord/node";
2
+ import path from "node:path";
3
+ import { createMiddleware } from "hono/factory";
4
+ import { getCookie } from "hono/cookie";
5
+
6
+ export type TranslationsKeyType = string;
7
+ export type TranslationsValueType = string;
8
+ export type TranslationsType = Record<
9
+ TranslationsKeyType,
10
+ TranslationsValueType
11
+ >;
12
+
13
+ export type TranslationPlaceholderType = string;
14
+ export type TranslationPlaceholderValueType = string | number;
15
+ export type TranslationVariableType = Record<
16
+ TranslationPlaceholderType,
17
+ TranslationPlaceholderValueType
18
+ >;
19
+
20
+ export type I18nConfigType = {
21
+ translationsPath?: bgn.Schema.PathType;
22
+ defaultLanguage?: bgn.Schema.LanguageType;
23
+ supportedLanguages: Record<string, bgn.Schema.LanguageType>;
24
+ };
25
+
26
+ export type I18nVariablesType = {
27
+ language: bgn.Schema.LanguageType;
28
+ supportedLanguages: bgn.Schema.LanguageType[];
29
+ translationsPath: bgn.Schema.PathType;
30
+ };
31
+
32
+ export class I18n {
33
+ static LANGUAGE_COOKIE_NAME = "accept-language";
34
+
35
+ static DEFAULT_TRANSLATIONS_PATH =
36
+ bgn.Schema.Path.parse("infra/translations");
37
+
38
+ static FALLBACK_LANGUAGE = "en";
39
+
40
+ static applyTo(config: I18nConfigType) {
41
+ return createMiddleware(async (c, next) => {
42
+ const translationsPath =
43
+ config?.translationsPath ?? I18n.DEFAULT_TRANSLATIONS_PATH;
44
+
45
+ const defaultLanguage = config?.defaultLanguage ?? I18n.FALLBACK_LANGUAGE;
46
+
47
+ const chosenLanguage =
48
+ getCookie(c, I18n.LANGUAGE_COOKIE_NAME) ?? defaultLanguage;
49
+
50
+ const language = Object.keys(config.supportedLanguages).find(
51
+ (language) => language === chosenLanguage
52
+ )
53
+ ? chosenLanguage
54
+ : I18n.FALLBACK_LANGUAGE;
55
+
56
+ c.set("supportedLanguages", Object.keys(config.supportedLanguages));
57
+ c.set("language", language);
58
+ c.set("translationsPath", translationsPath);
59
+
60
+ return next();
61
+ });
62
+ }
63
+
64
+ static async getTranslations(
65
+ language: bgn.Schema.LanguageType,
66
+ translationsPath: bgn.Schema.PathType
67
+ ): Promise<TranslationsType> {
68
+ try {
69
+ return Bun.file(
70
+ I18n.getTranslationPathForLanguage(language, translationsPath)
71
+ ).json();
72
+ } catch (error) {
73
+ // biome-ignore lint: lint/suspicious/noConsoleLog
74
+ console.log("I18n#getTranslations", error);
75
+
76
+ return {};
77
+ }
78
+ }
79
+
80
+ static useTranslations(translations: TranslationsType) {
81
+ return function translate(
82
+ key: TranslationsKeyType,
83
+ variables?: TranslationVariableType
84
+ ) {
85
+ const translation = translations[key];
86
+
87
+ if (!translation) {
88
+ console.warn(`[@bgord/node] missing translation for key: ${key}`);
89
+ return key;
90
+ }
91
+
92
+ if (!variables) return translation;
93
+
94
+ return Object.entries(variables).reduce(
95
+ (result, [placeholder, value]) =>
96
+ result.replace(`{{${placeholder}}}`, String(value)),
97
+ translation
98
+ );
99
+ };
100
+ }
101
+
102
+ static getTranslationPathForLanguage(
103
+ language: bgn.Schema.LanguageType,
104
+ translationsPath = I18n.DEFAULT_TRANSLATIONS_PATH
105
+ ): bgn.Schema.PathType {
106
+ return bgn.Schema.Path.parse(
107
+ path.join(translationsPath, `${language}.json`)
108
+ );
109
+ }
110
+ }
@@ -0,0 +1,5 @@
1
+ import sharp from "sharp";
2
+
3
+ export class ImageProcessor {
4
+ static sharp = sharp;
5
+ }
package/src/index.ts CHANGED
@@ -1,9 +1,16 @@
1
- export * from "./graceful-shutdown";
1
+ export * from "./api-key-shield";
2
2
  export * from "./api-version";
3
- export * from "./time-zone-offset";
3
+ export * from "./auth-shield";
4
4
  export * from "./context";
5
5
  export * from "./etag-extractor";
6
+ export * from "./graceful-shutdown";
7
+ export * from "./healthcheck";
6
8
  export * from "./http-logger";
7
- export * from "./api-key-shield";
8
9
  export * from "./rate-limit-shield";
9
- export * from "./healthcheck";
10
+ export * from "./time-zone-offset";
11
+ export * from "./cache-static-files";
12
+ export * from "./cache-response";
13
+ export * from "./download-file";
14
+ export * from "./i18n";
15
+ export * from "./file-uploader";
16
+ export * from "./image-processor";