@riffyh/server 1.0.3 → 1.2.1

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.mts CHANGED
@@ -75,9 +75,20 @@ declare const server: Elysia<"", {
75
75
  query: unknown;
76
76
  headers: unknown;
77
77
  response: {
78
- [x: string]: any;
79
- [x: number]: any;
80
- [x: symbol]: any;
78
+ 200: {
79
+ key: string;
80
+ name: string;
81
+ iconUrl: string;
82
+ }[];
83
+ 422: {
84
+ type: "validation";
85
+ on: string;
86
+ summary?: string;
87
+ message?: string;
88
+ found?: unknown;
89
+ property?: string;
90
+ expected?: string;
91
+ };
81
92
  };
82
93
  };
83
94
  };
@@ -92,9 +103,42 @@ declare const server: Elysia<"", {
92
103
  };
93
104
  headers: unknown;
94
105
  response: {
95
- [x: string]: any;
96
- [x: number]: any;
97
- [x: symbol]: any;
106
+ 200: {
107
+ tags: {
108
+ key: string;
109
+ name: string;
110
+ id: string;
111
+ slug: string;
112
+ type: import("@riffyh/commons").TagType;
113
+ }[];
114
+ key: string;
115
+ id: string;
116
+ title: {
117
+ display: string;
118
+ original: string | null;
119
+ };
120
+ language: "en" | "jp" | "cn" | "kr" | "th" | null;
121
+ cover: {
122
+ src: string;
123
+ width: number;
124
+ height: number;
125
+ };
126
+ pages: {
127
+ src: string;
128
+ width: number;
129
+ height: number;
130
+ order: number;
131
+ }[];
132
+ };
133
+ 422: {
134
+ type: "validation";
135
+ on: string;
136
+ summary?: string;
137
+ message?: string;
138
+ found?: unknown;
139
+ property?: string;
140
+ expected?: string;
141
+ };
98
142
  };
99
143
  };
100
144
  };
@@ -105,14 +149,80 @@ declare const server: Elysia<"", {
105
149
  params: {};
106
150
  query: {
107
151
  query?: string | undefined;
152
+ page: number;
108
153
  dataSource: never;
154
+ };
155
+ headers: unknown;
156
+ response: {
157
+ 200: {
158
+ galleries: {
159
+ key: string;
160
+ id: string;
161
+ title: {
162
+ display: string;
163
+ original: string | null;
164
+ };
165
+ language: "en" | "jp" | "cn" | "kr" | "th" | null;
166
+ cover: {
167
+ src: string;
168
+ width: number;
169
+ height: number;
170
+ };
171
+ }[];
172
+ currentPage: number;
173
+ maximumPages: number;
174
+ };
175
+ 422: {
176
+ type: "validation";
177
+ on: string;
178
+ summary?: string;
179
+ message?: string;
180
+ found?: unknown;
181
+ property?: string;
182
+ expected?: string;
183
+ };
184
+ };
185
+ };
186
+ };
187
+ } & {
188
+ tag: {
189
+ get: {
190
+ body: unknown;
191
+ params: {};
192
+ query: {
109
193
  page: number;
194
+ id: string;
195
+ dataSource: never;
110
196
  };
111
197
  headers: unknown;
112
198
  response: {
113
- [x: string]: any;
114
- [x: number]: any;
115
- [x: symbol]: any;
199
+ 200: {
200
+ galleries: {
201
+ key: string;
202
+ id: string;
203
+ title: {
204
+ display: string;
205
+ original: string | null;
206
+ };
207
+ language: "en" | "jp" | "cn" | "kr" | "th" | null;
208
+ cover: {
209
+ src: string;
210
+ width: number;
211
+ height: number;
212
+ };
213
+ }[];
214
+ currentPage: number;
215
+ maximumPages: number;
216
+ };
217
+ 422: {
218
+ type: "validation";
219
+ on: string;
220
+ summary?: string;
221
+ message?: string;
222
+ found?: unknown;
223
+ property?: string;
224
+ expected?: string;
225
+ };
116
226
  };
117
227
  };
118
228
  };
@@ -170,10 +280,10 @@ declare const server: Elysia<"", {
170
280
  body: unknown;
171
281
  params: {};
172
282
  query: {
173
- dataSource: never;
283
+ type: "page" | "cover";
174
284
  url: string;
285
+ dataSource: never;
175
286
  format: "webp" | "jpeg";
176
- type: "page" | "cover";
177
287
  };
178
288
  headers: unknown;
179
289
  response: {
package/dist/index.mjs CHANGED
@@ -60,6 +60,7 @@ try {
60
60
  }
61
61
  log(`loaded configuration with ${config.dataSources.length} data sources`);
62
62
  const dataSourceKeys = t.Union(config.dataSources.map((o) => t.Literal(o.key)));
63
+ const isStoreExist = config.dataSources.find((dataSource) => dataSource.key === "store");
63
64
  const server = new Elysia().use(swagger({ exclude: ["/_image"] })).use(toon()).use(cors()).get("/", ({ redirect }) => redirect("/swagger")).get("/health", () => "healthy").get("/dataSources", () => config.dataSources.map((o) => ({
64
65
  key: o.key,
65
66
  name: o.name,
@@ -69,6 +70,10 @@ const server = new Elysia().use(swagger({ exclude: ["/_image"] })).use(toon()).u
69
70
  name: t.String(),
70
71
  iconUrl: t.String()
71
72
  })) }).get("/gallery", async ({ query }) => {
73
+ if (isStoreExist) {
74
+ const gallery = await config.dataSources.find((o) => o.key === "store").getGallery({ id: `${query.dataSource};${query.id}` }).catch(() => null);
75
+ if (gallery !== null) return gallery;
76
+ }
72
77
  const dataSource = config.dataSources.find((o) => o.key === query.dataSource);
73
78
  if (dataSource === void 0) throw new Error(`data source ${query.dataSource} not found`);
74
79
  return dataSource.getGallery({ id: query.id });
@@ -92,6 +97,20 @@ const server = new Elysia().use(swagger({ exclude: ["/_image"] })).use(toon()).u
92
97
  dataSource: dataSourceKeys
93
98
  }),
94
99
  response: listingResultModel
100
+ }).get("/tag", async ({ query }) => {
101
+ const dataSource = config.dataSources.find((o) => o.key === query.dataSource);
102
+ if (dataSource === void 0) throw new Error(`data source ${query.dataSource} not found`);
103
+ return dataSource.getTagListing({
104
+ id: query.id,
105
+ page: query.page
106
+ });
107
+ }, {
108
+ query: t.Object({
109
+ id: t.String(),
110
+ page: t.Number(),
111
+ dataSource: dataSourceKeys
112
+ }),
113
+ response: listingResultModel
95
114
  }).post("/collection/export", ({ body }) => upload(body, config.secretboxKey), { body: t.String() }).get("/collection/import", async ({ query }) => download(query.key, config.secretboxKey), { query: t.Object({ key: t.String() }) }).get("/image", async ({ query }) => {
96
115
  const cacheKeys = [
97
116
  query.dataSource,
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/bytebin.ts","../src/index.ts"],"sourcesContent":["import { secretbox, randomBytes } from \"tweetnacl\";\nimport { decodeUTF8, encodeUTF8, encodeBase64, decodeBase64 } from \"tweetnacl-util\";\n\nexport const upload = async (content: string, key: string): Promise<string> => {\n const nonce = randomBytes(secretbox.nonceLength);\n const keyUint8Array = decodeBase64(key);\n const messageUint8 = decodeUTF8(content);\n const box = secretbox(messageUint8, nonce, keyUint8Array);\n\n const fullMessage = new Uint8Array(nonce.length + box.length);\n fullMessage.set(nonce);\n fullMessage.set(box, nonce.length);\n\n const base64FullMessage = encodeBase64(fullMessage);\n const response = await fetch(\"https://api.pastes.dev/post\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"text/plain\",\n \"User-Agent\": \"RiffyH (github.com/rayriffy/rayriffy-h)\",\n },\n body: base64FullMessage,\n });\n\n if (!response.ok) throw new Error(\"failed to upload content\");\n\n const data = response.json() as Promise<{ key: string }>;\n return (await data).key;\n};\n\nexport const download = async (code: string, key: string): Promise<string> => {\n const response = await fetch(`https://api.pastes.dev/${code}`);\n\n if (!response.ok) throw new Error(`failed to download content with code ${code}`);\n\n const encryptedContent = await response.text();\n\n const keyUint8Array = decodeBase64(key);\n const messageWithNonceAsUint8Array = decodeBase64(encryptedContent);\n const nonce = messageWithNonceAsUint8Array.slice(0, secretbox.nonceLength);\n const message = messageWithNonceAsUint8Array.slice(\n secretbox.nonceLength,\n encryptedContent.length,\n );\n\n const decrypted = secretbox.open(message, nonce, keyUint8Array);\n\n if (!decrypted) throw new Error(\"could not decrypt message\");\n\n return encodeUTF8(decrypted);\n};\n","#!/usr/bin/env bun\n\nimport { t, Elysia } from \"elysia\";\nimport { swagger } from \"@elysiajs/swagger\";\nimport { cors } from \"@elysiajs/cors\";\nimport { toon } from \"@toon-tools/elysia\";\n\nimport { defineCacheInstance } from \"@rayriffy/filesystem\";\nimport sharp from \"sharp\";\nimport { galleryModel, listingResultModel, type Config } from \"@riffyh/commons\";\nimport debug from \"debug\";\nimport path from \"node:path\";\nimport { download, upload } from \"./bytebin\";\nimport { secretbox, randomBytes } from \"tweetnacl\";\nimport { encodeBase64, decodeBase64 } from \"tweetnacl-util\";\n\nconst log = debug(\"riffyh:server\");\nconst cache = defineCacheInstance();\n\nlog(\"warming up server...\");\n\nconst configFile = process.env.RIFFYH_CONFIG_PATH || \"./riffyh.config.ts\";\nconst configPath = path.resolve(configFile);\nlog(`resolving config file at ${configPath}...`);\nconst config: Config = await import(configPath).then((o) => o.default);\n\ntry {\n if (\n typeof config.secretboxKey !== \"string\" ||\n decodeBase64(config.secretboxKey).length !== secretbox.keyLength\n )\n throw new Error(\"key length mismatch\");\n // oxlint-disable-next-line no-unused-vars\n} catch (_) {\n const generatedKey = encodeBase64(randomBytes(secretbox.keyLength));\n console.error(\n `unable to parse secret key. please either generate key via https://tweetnacl.js.org/#/secretbox or copy following key to configuration: ${generatedKey}`,\n );\n process.exit(1);\n}\n\nlog(`loaded configuration with ${config.dataSources.length} data sources`);\n\nconst dataSourceKeys = t.Union(config.dataSources.map((o) => t.Literal(o.key)));\n\nconst server = new Elysia()\n .use(\n swagger({\n exclude: [\"/_image\"],\n }),\n )\n .use(toon())\n .use(cors())\n .get(\"/\", ({ redirect }) => redirect(\"/swagger\"))\n .get(\"/health\", () => \"healthy\")\n .get(\n \"/dataSources\",\n () =>\n config.dataSources.map((o) => ({\n key: o.key,\n name: o.name,\n iconUrl: o.iconUrl,\n })),\n {\n response: t.Array(\n t.Object({\n key: t.String(),\n name: t.String(),\n iconUrl: t.String(),\n }),\n ),\n },\n )\n .get(\n \"/gallery\",\n async ({ query }) => {\n const dataSource = config.dataSources.find((o) => o.key === query.dataSource);\n if (dataSource === undefined) throw new Error(`data source ${query.dataSource} not found`);\n\n return dataSource.getGallery({\n id: query.id,\n });\n },\n {\n query: t.Object({\n id: t.String(),\n dataSource: dataSourceKeys,\n }),\n response: galleryModel,\n },\n )\n .get(\n \"/listing\",\n async ({ query }) => {\n const dataSource = config.dataSources.find((o) => o.key === query.dataSource);\n if (dataSource === undefined) throw new Error(`data source ${query.dataSource} not found`);\n\n return dataSource.getListing({\n searchQuery: query.query || null,\n page: query.page,\n });\n },\n {\n query: t.Object({\n query: t.Optional(t.String()),\n page: t.Number(),\n dataSource: dataSourceKeys,\n }),\n response: listingResultModel,\n },\n )\n .post(\"/collection/export\", ({ body }) => upload(body, config.secretboxKey), {\n body: t.String(),\n })\n .get(\"/collection/import\", async ({ query }) => download(query.key, config.secretboxKey), {\n query: t.Object({\n key: t.String(),\n }),\n })\n .get(\n \"/image\",\n async ({ query }) => {\n const cacheKeys = [query.dataSource, query.url, query.format, query.type];\n\n const cachedImage = await cache.read<Buffer>(cacheKeys);\n if (cachedImage !== null)\n return new Response(Buffer.from(cachedImage.data), {\n headers: {\n \"Content-Type\": `image/${query.format}`,\n \"Cache-Control\": \"public, max-age=86400000\",\n },\n });\n\n const dataSource = config.dataSources.find((o) => o.key === query.dataSource);\n if (dataSource === undefined) throw new Error(`data source ${query.dataSource} not found`);\n\n const fetchedImage = await dataSource.getImage({\n url: query.url,\n });\n const resizedImage = await sharp(fetchedImage)\n .resize({\n width: query.type === \"cover\" ? 640 : 1280,\n })\n .toFormat(query.format, {\n quality: 72,\n })\n .toBuffer();\n await cache.write(\n cacheKeys,\n resizedImage,\n 86_400_000, // 1 month\n );\n\n return new Response(resizedImage, {\n headers: {\n \"Content-Type\": `image/${query.format}`,\n \"Cache-Control\": \"public, max-age=86400\",\n \"CDN-Cache-Control\": \"public, max-age=2592000\",\n \"Cloudflare-CDN-Cache-Control\": \"public, max-age=2592000\",\n },\n });\n },\n {\n query: t.Object({\n url: t.String(),\n format: t.Union([t.Literal(\"webp\"), t.Literal(\"jpeg\")]),\n type: t.Union([t.Literal(\"cover\"), t.Literal(\"page\")]),\n dataSource: dataSourceKeys,\n }),\n },\n )\n .listen({\n hostname: config.hostname ?? \"0.0.0.0\",\n port: config.port ?? 3000,\n });\n\nexport type Server = typeof server;\n\nconsole.log(`🦊 Elysia is running at http://${server.server?.hostname}:${server.server?.port}`);\n"],"mappings":";;;;;;;;;;;;;AAGA,MAAa,SAAS,OAAO,SAAiB,QAAiC;CAC7E,MAAM,QAAQ,YAAY,UAAU,WAAW;CAC/C,MAAM,gBAAgB,aAAa,GAAG;CAEtC,MAAM,MAAM,UADS,WAAW,OACC,GAAG,OAAO,aAAa;CAExD,MAAM,cAAc,IAAI,WAAW,MAAM,SAAS,IAAI,MAAM;CAC5D,YAAY,IAAI,KAAK;CACrB,YAAY,IAAI,KAAK,MAAM,MAAM;CAEjC,MAAM,oBAAoB,aAAa,WAAW;CAClD,MAAM,WAAW,MAAM,MAAM,+BAA+B;EAC1D,QAAQ;EACR,SAAS;GACP,gBAAgB;GAChB,cAAc;EAChB;EACA,MAAM;CACR,CAAC;CAED,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,MAAM,0BAA0B;CAG5D,QAAQ,MADK,SAAS,KACL,EAAA,CAAG;AACtB;AAEA,MAAa,WAAW,OAAO,MAAc,QAAiC;CAC5E,MAAM,WAAW,MAAM,MAAM,0BAA0B,MAAM;CAE7D,IAAI,CAAC,SAAS,IAAI,MAAM,IAAI,MAAM,wCAAwC,MAAM;CAEhF,MAAM,mBAAmB,MAAM,SAAS,KAAK;CAE7C,MAAM,gBAAgB,aAAa,GAAG;CACtC,MAAM,+BAA+B,aAAa,gBAAgB;CAClE,MAAM,QAAQ,6BAA6B,MAAM,GAAG,UAAU,WAAW;CACzE,MAAM,UAAU,6BAA6B,MAC3C,UAAU,aACV,iBAAiB,MACnB;CAEA,MAAM,YAAY,UAAU,KAAK,SAAS,OAAO,aAAa;CAE9D,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,2BAA2B;CAE3D,OAAO,WAAW,SAAS;AAC7B;;;ACjCA,MAAM,MAAM,MAAM,eAAe;AACjC,MAAM,QAAQ,oBAAoB;AAElC,IAAI,sBAAsB;AAE1B,MAAM,aAAa,QAAQ,IAAI,sBAAsB;AACrD,MAAM,aAAa,KAAK,QAAQ,UAAU;AAC1C,IAAI,4BAA4B,WAAW,IAAI;AAC/C,MAAM,SAAiB,MAAM,OAAO,WAAW,CAAC,MAAM,MAAM,EAAE,OAAO;AAErE,IAAI;CACF,IACE,OAAO,OAAO,iBAAiB,YAC/B,aAAa,OAAO,YAAY,CAAC,CAAC,WAAW,UAAU,WAEvD,MAAM,IAAI,MAAM,qBAAqB;AAEzC,SAAS,GAAG;CACV,MAAM,eAAe,aAAa,YAAY,UAAU,SAAS,CAAC;CAClE,QAAQ,MACN,2IAA2I,cAC7I;CACA,QAAQ,KAAK,CAAC;AAChB;AAEA,IAAI,6BAA6B,OAAO,YAAY,OAAO,cAAc;AAEzE,MAAM,iBAAiB,EAAE,MAAM,OAAO,YAAY,KAAK,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC;AAE9E,MAAM,SAAS,IAAI,OAAO,CAAC,CACxB,IACC,QAAQ,EACN,SAAS,CAAC,SAAS,EACrB,CAAC,CACH,CAAC,CACA,IAAI,KAAK,CAAC,CAAC,CACX,IAAI,KAAK,CAAC,CAAC,CACX,IAAI,MAAM,EAAE,eAAe,SAAS,UAAU,CAAC,CAAC,CAChD,IAAI,iBAAiB,SAAS,CAAC,CAC/B,IACC,sBAEE,OAAO,YAAY,KAAK,OAAO;CAC7B,KAAK,EAAE;CACP,MAAM,EAAE;CACR,SAAS,EAAE;AACb,EAAE,GACJ,EACE,UAAU,EAAE,MACV,EAAE,OAAO;CACP,KAAK,EAAE,OAAO;CACd,MAAM,EAAE,OAAO;CACf,SAAS,EAAE,OAAO;AACpB,CAAC,CACH,EACF,CACF,CAAC,CACA,IACC,YACA,OAAO,EAAE,YAAY;CACnB,MAAM,aAAa,OAAO,YAAY,MAAM,MAAM,EAAE,QAAQ,MAAM,UAAU;CAC5E,IAAI,eAAe,KAAA,GAAW,MAAM,IAAI,MAAM,eAAe,MAAM,WAAW,WAAW;CAEzF,OAAO,WAAW,WAAW,EAC3B,IAAI,MAAM,GACZ,CAAC;AACH,GACA;CACE,OAAO,EAAE,OAAO;EACd,IAAI,EAAE,OAAO;EACb,YAAY;CACd,CAAC;CACD,UAAU;AACZ,CACF,CAAC,CACA,IACC,YACA,OAAO,EAAE,YAAY;CACnB,MAAM,aAAa,OAAO,YAAY,MAAM,MAAM,EAAE,QAAQ,MAAM,UAAU;CAC5E,IAAI,eAAe,KAAA,GAAW,MAAM,IAAI,MAAM,eAAe,MAAM,WAAW,WAAW;CAEzF,OAAO,WAAW,WAAW;EAC3B,aAAa,MAAM,SAAS;EAC5B,MAAM,MAAM;CACd,CAAC;AACH,GACA;CACE,OAAO,EAAE,OAAO;EACd,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC;EAC5B,MAAM,EAAE,OAAO;EACf,YAAY;CACd,CAAC;CACD,UAAU;AACZ,CACF,CAAC,CACA,KAAK,uBAAuB,EAAE,WAAW,OAAO,MAAM,OAAO,YAAY,GAAG,EAC3E,MAAM,EAAE,OAAO,EACjB,CAAC,CAAC,CACD,IAAI,sBAAsB,OAAO,EAAE,YAAY,SAAS,MAAM,KAAK,OAAO,YAAY,GAAG,EACxF,OAAO,EAAE,OAAO,EACd,KAAK,EAAE,OAAO,EAChB,CAAC,EACH,CAAC,CAAC,CACD,IACC,UACA,OAAO,EAAE,YAAY;CACnB,MAAM,YAAY;EAAC,MAAM;EAAY,MAAM;EAAK,MAAM;EAAQ,MAAM;CAAI;CAExE,MAAM,cAAc,MAAM,MAAM,KAAa,SAAS;CACtD,IAAI,gBAAgB,MAClB,OAAO,IAAI,SAAS,OAAO,KAAK,YAAY,IAAI,GAAG,EACjD,SAAS;EACP,gBAAgB,SAAS,MAAM;EAC/B,iBAAiB;CACnB,EACF,CAAC;CAEH,MAAM,aAAa,OAAO,YAAY,MAAM,MAAM,EAAE,QAAQ,MAAM,UAAU;CAC5E,IAAI,eAAe,KAAA,GAAW,MAAM,IAAI,MAAM,eAAe,MAAM,WAAW,WAAW;CAKzF,MAAM,eAAe,MAAM,MAAM,MAHN,WAAW,SAAS,EAC7C,KAAK,MAAM,IACb,CAAC,CAC4C,CAAC,CAC3C,OAAO,EACN,OAAO,MAAM,SAAS,UAAU,MAAM,KACxC,CAAC,CAAC,CACD,SAAS,MAAM,QAAQ,EACtB,SAAS,GACX,CAAC,CAAC,CACD,SAAS;CACZ,MAAM,MAAM,MACV,WACA,cACA,KACF;CAEA,OAAO,IAAI,SAAS,cAAc,EAChC,SAAS;EACP,gBAAgB,SAAS,MAAM;EAC/B,iBAAiB;EACjB,qBAAqB;EACrB,gCAAgC;CAClC,EACF,CAAC;AACH,GACA,EACE,OAAO,EAAE,OAAO;CACd,KAAK,EAAE,OAAO;CACd,QAAQ,EAAE,MAAM,CAAC,EAAE,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,CAAC,CAAC;CACtD,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,MAAM,CAAC,CAAC;CACrD,YAAY;AACd,CAAC,EACH,CACF,CAAC,CACA,OAAO;CACN,UAAU,OAAO,YAAY;CAC7B,MAAM,OAAO,QAAQ;AACvB,CAAC;AAIH,QAAQ,IAAI,kCAAkC,OAAO,QAAQ,SAAS,GAAG,OAAO,QAAQ,MAAM"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/bytebin.ts","../src/index.ts"],"sourcesContent":["import { secretbox, randomBytes } from \"tweetnacl\";\nimport { decodeUTF8, encodeUTF8, encodeBase64, decodeBase64 } from \"tweetnacl-util\";\n\nexport const upload = async (content: string, key: string): Promise<string> => {\n const nonce = randomBytes(secretbox.nonceLength);\n const keyUint8Array = decodeBase64(key);\n const messageUint8 = decodeUTF8(content);\n const box = secretbox(messageUint8, nonce, keyUint8Array);\n\n const fullMessage = new Uint8Array(nonce.length + box.length);\n fullMessage.set(nonce);\n fullMessage.set(box, nonce.length);\n\n const base64FullMessage = encodeBase64(fullMessage);\n const response = await fetch(\"https://api.pastes.dev/post\", {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"text/plain\",\n \"User-Agent\": \"RiffyH (github.com/rayriffy/rayriffy-h)\",\n },\n body: base64FullMessage,\n });\n\n if (!response.ok) throw new Error(\"failed to upload content\");\n\n const data = response.json() as Promise<{ key: string }>;\n return (await data).key;\n};\n\nexport const download = async (code: string, key: string): Promise<string> => {\n const response = await fetch(`https://api.pastes.dev/${code}`);\n\n if (!response.ok) throw new Error(`failed to download content with code ${code}`);\n\n const encryptedContent = await response.text();\n\n const keyUint8Array = decodeBase64(key);\n const messageWithNonceAsUint8Array = decodeBase64(encryptedContent);\n const nonce = messageWithNonceAsUint8Array.slice(0, secretbox.nonceLength);\n const message = messageWithNonceAsUint8Array.slice(\n secretbox.nonceLength,\n encryptedContent.length,\n );\n\n const decrypted = secretbox.open(message, nonce, keyUint8Array);\n\n if (!decrypted) throw new Error(\"could not decrypt message\");\n\n return encodeUTF8(decrypted);\n};\n","#!/usr/bin/env bun\n\nimport { t, Elysia } from \"elysia\";\nimport { swagger } from \"@elysiajs/swagger\";\nimport { cors } from \"@elysiajs/cors\";\nimport { toon } from \"@toon-tools/elysia\";\n\nimport { defineCacheInstance } from \"@rayriffy/filesystem\";\nimport sharp from \"sharp\";\nimport { galleryModel, listingResultModel, type Config } from \"@riffyh/commons\";\nimport debug from \"debug\";\nimport path from \"node:path\";\nimport { download, upload } from \"./bytebin\";\nimport { secretbox, randomBytes } from \"tweetnacl\";\nimport { encodeBase64, decodeBase64 } from \"tweetnacl-util\";\n\nconst log = debug(\"riffyh:server\");\nconst cache = defineCacheInstance();\n\nlog(\"warming up server...\");\n\nconst configFile = process.env.RIFFYH_CONFIG_PATH || \"./riffyh.config.ts\";\nconst configPath = path.resolve(configFile);\nlog(`resolving config file at ${configPath}...`);\nconst config: Config = await import(configPath).then((o) => o.default);\n\ntry {\n if (\n typeof config.secretboxKey !== \"string\" ||\n decodeBase64(config.secretboxKey).length !== secretbox.keyLength\n )\n throw new Error(\"key length mismatch\");\n // oxlint-disable-next-line no-unused-vars\n} catch (_) {\n const generatedKey = encodeBase64(randomBytes(secretbox.keyLength));\n console.error(\n `unable to parse secret key. please either generate key via https://tweetnacl.js.org/#/secretbox or copy following key to configuration: ${generatedKey}`,\n );\n process.exit(1);\n}\n\nlog(`loaded configuration with ${config.dataSources.length} data sources`);\n\nconst dataSourceKeys = t.Union(config.dataSources.map((o) => t.Literal(o.key)));\nconst isStoreExist = config.dataSources.find((dataSource) => dataSource.key === \"store\");\n\nconst server = new Elysia()\n .use(\n swagger({\n exclude: [\"/_image\"],\n }),\n )\n .use(toon())\n .use(cors())\n .get(\"/\", ({ redirect }) => redirect(\"/swagger\"))\n .get(\"/health\", () => \"healthy\")\n .get(\n \"/dataSources\",\n () =>\n config.dataSources.map((o) => ({\n key: o.key,\n name: o.name,\n iconUrl: o.iconUrl,\n })),\n {\n response: t.Array(\n t.Object({\n key: t.String(),\n name: t.String(),\n iconUrl: t.String(),\n }),\n ),\n },\n )\n .get(\n \"/gallery\",\n async ({ query }) => {\n if (isStoreExist) {\n const storeDataSource = config.dataSources.find((o) => o.key === \"store\")!;\n const gallery = await storeDataSource\n .getGallery({\n id: `${query.dataSource};${query.id}`,\n })\n .catch(() => null);\n\n if (gallery !== null) return gallery;\n }\n\n const dataSource = config.dataSources.find((o) => o.key === query.dataSource);\n if (dataSource === undefined) throw new Error(`data source ${query.dataSource} not found`);\n\n return dataSource.getGallery({\n id: query.id,\n });\n },\n {\n query: t.Object({\n id: t.String(),\n dataSource: dataSourceKeys,\n }),\n response: galleryModel,\n },\n )\n .get(\n \"/listing\",\n async ({ query }) => {\n const dataSource = config.dataSources.find((o) => o.key === query.dataSource);\n if (dataSource === undefined) throw new Error(`data source ${query.dataSource} not found`);\n\n return dataSource.getListing({\n searchQuery: query.query || null,\n page: query.page,\n });\n },\n {\n query: t.Object({\n query: t.Optional(t.String()),\n page: t.Number(),\n dataSource: dataSourceKeys,\n }),\n response: listingResultModel,\n },\n )\n .get(\n \"/tag\",\n async ({ query }) => {\n const dataSource = config.dataSources.find((o) => o.key === query.dataSource);\n if (dataSource === undefined) throw new Error(`data source ${query.dataSource} not found`);\n\n return dataSource.getTagListing({\n id: query.id,\n page: query.page,\n });\n },\n {\n query: t.Object({\n id: t.String(),\n page: t.Number(),\n dataSource: dataSourceKeys,\n }),\n response: listingResultModel,\n },\n )\n .post(\"/collection/export\", ({ body }) => upload(body, config.secretboxKey), {\n body: t.String(),\n })\n .get(\"/collection/import\", async ({ query }) => download(query.key, config.secretboxKey), {\n query: t.Object({\n key: t.String(),\n }),\n })\n .get(\n \"/image\",\n async ({ query }) => {\n const cacheKeys = [query.dataSource, query.url, query.format, query.type];\n\n const cachedImage = await cache.read<Buffer>(cacheKeys);\n if (cachedImage !== null)\n return new Response(Buffer.from(cachedImage.data), {\n headers: {\n \"Content-Type\": `image/${query.format}`,\n \"Cache-Control\": \"public, max-age=86400000\",\n },\n });\n\n const dataSource = config.dataSources.find((o) => o.key === query.dataSource);\n if (dataSource === undefined) throw new Error(`data source ${query.dataSource} not found`);\n\n const fetchedImage = await dataSource.getImage({\n url: query.url,\n });\n const resizedImage = await sharp(fetchedImage)\n .resize({\n width: query.type === \"cover\" ? 640 : 1280,\n })\n .toFormat(query.format, {\n quality: 72,\n })\n .toBuffer();\n await cache.write(\n cacheKeys,\n resizedImage,\n 86_400_000, // 1 month\n );\n\n return new Response(resizedImage, {\n headers: {\n \"Content-Type\": `image/${query.format}`,\n \"Cache-Control\": \"public, max-age=86400\",\n \"CDN-Cache-Control\": \"public, max-age=2592000\",\n \"Cloudflare-CDN-Cache-Control\": \"public, max-age=2592000\",\n },\n });\n },\n {\n query: t.Object({\n url: t.String(),\n format: t.Union([t.Literal(\"webp\"), t.Literal(\"jpeg\")]),\n type: t.Union([t.Literal(\"cover\"), t.Literal(\"page\")]),\n dataSource: dataSourceKeys,\n }),\n },\n )\n .listen({\n hostname: config.hostname ?? \"0.0.0.0\",\n port: config.port ?? 3000,\n });\n\nexport type Server = typeof server;\n\nconsole.log(`🦊 Elysia is running at http://${server.server?.hostname}:${server.server?.port}`);\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riffyh/server",
3
- "version": "1.0.3",
3
+ "version": "1.2.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rayriffy/rayriffy-h.git"
@@ -26,7 +26,7 @@
26
26
  "sharp": "0.35.2",
27
27
  "tweetnacl": "1.0.3",
28
28
  "tweetnacl-util": "0.15.1",
29
- "@riffyh/commons": "2.1.2"
29
+ "@riffyh/commons": "2.2.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/debug": "4.1.13",