@riffyh/server 1.0.1 → 1.0.2
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 +69 -4
- package/dist/index.mjs +69 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -8
package/dist/index.d.mts
CHANGED
|
@@ -16,6 +16,9 @@ declare const server: Elysia<"", {
|
|
|
16
16
|
} & {
|
|
17
17
|
typebox: {};
|
|
18
18
|
error: {};
|
|
19
|
+
} & {
|
|
20
|
+
typebox: {};
|
|
21
|
+
error: {};
|
|
19
22
|
}, {
|
|
20
23
|
schema: {};
|
|
21
24
|
standaloneSchema: {};
|
|
@@ -36,6 +39,13 @@ declare const server: Elysia<"", {
|
|
|
36
39
|
macroFn: {};
|
|
37
40
|
parser: {};
|
|
38
41
|
response: {};
|
|
42
|
+
} & {
|
|
43
|
+
schema: {};
|
|
44
|
+
standaloneSchema: {};
|
|
45
|
+
macro: {};
|
|
46
|
+
macroFn: {};
|
|
47
|
+
parser: {};
|
|
48
|
+
response: {};
|
|
39
49
|
}, {
|
|
40
50
|
get: {
|
|
41
51
|
body: unknown;
|
|
@@ -90,7 +100,7 @@ declare const server: Elysia<"", {
|
|
|
90
100
|
params: {};
|
|
91
101
|
query: {
|
|
92
102
|
id: string;
|
|
93
|
-
dataSource:
|
|
103
|
+
dataSource: never;
|
|
94
104
|
};
|
|
95
105
|
headers: unknown;
|
|
96
106
|
response: {
|
|
@@ -103,11 +113,11 @@ declare const server: Elysia<"", {
|
|
|
103
113
|
type: _$_riffyh_commons0.TagType;
|
|
104
114
|
}[];
|
|
105
115
|
key: string;
|
|
106
|
-
id: string;
|
|
107
116
|
title: {
|
|
108
117
|
display: string;
|
|
109
118
|
original: string | null;
|
|
110
119
|
};
|
|
120
|
+
id: string;
|
|
111
121
|
cover: {
|
|
112
122
|
src: string;
|
|
113
123
|
width: number;
|
|
@@ -147,11 +157,11 @@ declare const server: Elysia<"", {
|
|
|
147
157
|
200: {
|
|
148
158
|
galleries: {
|
|
149
159
|
key: string;
|
|
150
|
-
id: string;
|
|
151
160
|
title: {
|
|
152
161
|
display: string;
|
|
153
162
|
original: string | null;
|
|
154
163
|
};
|
|
164
|
+
id: string;
|
|
155
165
|
cover: {
|
|
156
166
|
src: string;
|
|
157
167
|
width: number;
|
|
@@ -173,15 +183,64 @@ declare const server: Elysia<"", {
|
|
|
173
183
|
};
|
|
174
184
|
};
|
|
175
185
|
};
|
|
186
|
+
} & {
|
|
187
|
+
collection: {
|
|
188
|
+
export: {
|
|
189
|
+
post: {
|
|
190
|
+
body: string;
|
|
191
|
+
params: {};
|
|
192
|
+
query: unknown;
|
|
193
|
+
headers: unknown;
|
|
194
|
+
response: {
|
|
195
|
+
200: string;
|
|
196
|
+
422: {
|
|
197
|
+
type: "validation";
|
|
198
|
+
on: string;
|
|
199
|
+
summary?: string;
|
|
200
|
+
message?: string;
|
|
201
|
+
found?: unknown;
|
|
202
|
+
property?: string;
|
|
203
|
+
expected?: string;
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
} & {
|
|
210
|
+
collection: {
|
|
211
|
+
import: {
|
|
212
|
+
get: {
|
|
213
|
+
body: unknown;
|
|
214
|
+
params: {};
|
|
215
|
+
query: {
|
|
216
|
+
key: string;
|
|
217
|
+
};
|
|
218
|
+
headers: unknown;
|
|
219
|
+
response: {
|
|
220
|
+
200: string;
|
|
221
|
+
422: {
|
|
222
|
+
type: "validation";
|
|
223
|
+
on: string;
|
|
224
|
+
summary?: string;
|
|
225
|
+
message?: string;
|
|
226
|
+
found?: unknown;
|
|
227
|
+
property?: string;
|
|
228
|
+
expected?: string;
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
};
|
|
233
|
+
};
|
|
176
234
|
} & {
|
|
177
235
|
image: {
|
|
178
236
|
get: {
|
|
179
237
|
body: unknown;
|
|
180
238
|
params: {};
|
|
181
239
|
query: {
|
|
240
|
+
type: "page" | "cover";
|
|
182
241
|
url: string;
|
|
183
242
|
dataSource: never;
|
|
184
|
-
format: "webp" | "jpg";
|
|
243
|
+
format: "avif" | "webp" | "jpg";
|
|
185
244
|
};
|
|
186
245
|
headers: unknown;
|
|
187
246
|
response: {
|
|
@@ -221,6 +280,12 @@ declare const server: Elysia<"", {
|
|
|
221
280
|
schema: {};
|
|
222
281
|
standaloneSchema: {};
|
|
223
282
|
response: {};
|
|
283
|
+
} & {
|
|
284
|
+
derive: {};
|
|
285
|
+
resolve: {};
|
|
286
|
+
schema: {};
|
|
287
|
+
standaloneSchema: {};
|
|
288
|
+
response: {};
|
|
224
289
|
}>;
|
|
225
290
|
type Server = typeof server;
|
|
226
291
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -2,11 +2,47 @@
|
|
|
2
2
|
import { Elysia, t } from "elysia";
|
|
3
3
|
import { swagger } from "@elysiajs/swagger";
|
|
4
4
|
import { cors } from "@elysiajs/cors";
|
|
5
|
+
import { toon } from "@toon-tools/elysia";
|
|
5
6
|
import { defineCacheInstance } from "@rayriffy/filesystem";
|
|
6
7
|
import sharp from "sharp";
|
|
7
8
|
import { galleryModel, listingResultModel } from "@riffyh/commons";
|
|
8
9
|
import debug from "debug";
|
|
9
10
|
import path from "node:path";
|
|
11
|
+
import { randomBytes, secretbox } from "tweetnacl";
|
|
12
|
+
import { decodeBase64, decodeUTF8, encodeBase64, encodeUTF8 } from "tweetnacl-util";
|
|
13
|
+
//#region src/bytebin.ts
|
|
14
|
+
const upload = async (content, key) => {
|
|
15
|
+
const nonce = randomBytes(secretbox.nonceLength);
|
|
16
|
+
const keyUint8Array = decodeBase64(key);
|
|
17
|
+
const box = secretbox(decodeUTF8(content), nonce, keyUint8Array);
|
|
18
|
+
const fullMessage = new Uint8Array(nonce.length + box.length);
|
|
19
|
+
fullMessage.set(nonce);
|
|
20
|
+
fullMessage.set(box, nonce.length);
|
|
21
|
+
const base64FullMessage = encodeBase64(fullMessage);
|
|
22
|
+
const response = await fetch("https://api.pastes.dev/post", {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: {
|
|
25
|
+
"Content-Type": "text/plain",
|
|
26
|
+
"User-Agent": "RiffyH (github.com/rayriffy/rayriffy-h)"
|
|
27
|
+
},
|
|
28
|
+
body: base64FullMessage
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) throw new Error("failed to upload content");
|
|
31
|
+
return (await response.json()).key;
|
|
32
|
+
};
|
|
33
|
+
const download = async (code, key) => {
|
|
34
|
+
const response = await fetch(`https://api.pastes.dev/${code}`);
|
|
35
|
+
if (!response.ok) throw new Error(`failed to download content with code ${code}`);
|
|
36
|
+
const encryptedContent = await response.text();
|
|
37
|
+
const keyUint8Array = decodeBase64(key);
|
|
38
|
+
const messageWithNonceAsUint8Array = decodeBase64(encryptedContent);
|
|
39
|
+
const nonce = messageWithNonceAsUint8Array.slice(0, secretbox.nonceLength);
|
|
40
|
+
const message = messageWithNonceAsUint8Array.slice(secretbox.nonceLength, encryptedContent.length);
|
|
41
|
+
const decrypted = secretbox.open(message, nonce, keyUint8Array);
|
|
42
|
+
if (!decrypted) throw new Error("could not decrypt message");
|
|
43
|
+
return encodeUTF8(decrypted);
|
|
44
|
+
};
|
|
45
|
+
//#endregion
|
|
10
46
|
//#region src/index.ts
|
|
11
47
|
const log = debug("riffyh:server");
|
|
12
48
|
const cache = defineCacheInstance();
|
|
@@ -15,9 +51,16 @@ const configFile = process.env.RIFFYH_CONFIG_PATH || "./riffyh.config.ts";
|
|
|
15
51
|
const configPath = path.resolve(configFile);
|
|
16
52
|
log(`resolving config file at ${configPath}...`);
|
|
17
53
|
const config = await import(configPath).then((o) => o.default);
|
|
54
|
+
try {
|
|
55
|
+
if (typeof config.secretboxKey !== "string" || decodeBase64(config.secretboxKey).length !== secretbox.keyLength) throw new Error("key length mismatch");
|
|
56
|
+
} catch (_) {
|
|
57
|
+
const generatedKey = encodeBase64(randomBytes(secretbox.keyLength));
|
|
58
|
+
console.error(`unable to parse secret key. please either generate key via https://tweetnacl.js.org/#/secretbox or copy following key to configuration: ${generatedKey}`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
18
61
|
log(`loaded configuration with ${config.dataSources.length} data sources`);
|
|
19
62
|
const dataSourceKeys = t.Union(config.dataSources.map((o) => t.Literal(o.key)));
|
|
20
|
-
const server = new Elysia().use(swagger({ exclude: ["/_image"] })).use(cors()).get("/", ({ redirect }) => redirect("/swagger")).get("/health", () => "healthy").get("/dataSources", () => config.dataSources.map((o) => ({
|
|
63
|
+
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) => ({
|
|
21
64
|
key: o.key,
|
|
22
65
|
name: o.name,
|
|
23
66
|
iconUrl: o.iconUrl
|
|
@@ -32,7 +75,7 @@ const server = new Elysia().use(swagger({ exclude: ["/_image"] })).use(cors()).g
|
|
|
32
75
|
}, {
|
|
33
76
|
query: t.Object({
|
|
34
77
|
id: t.String(),
|
|
35
|
-
dataSource:
|
|
78
|
+
dataSource: dataSourceKeys
|
|
36
79
|
}),
|
|
37
80
|
response: galleryModel
|
|
38
81
|
}).get("/listing", async ({ query }) => {
|
|
@@ -49,24 +92,41 @@ const server = new Elysia().use(swagger({ exclude: ["/_image"] })).use(cors()).g
|
|
|
49
92
|
dataSource: dataSourceKeys
|
|
50
93
|
}),
|
|
51
94
|
response: listingResultModel
|
|
52
|
-
}).get("/image", async ({ query }) => {
|
|
95
|
+
}).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 }) => {
|
|
53
96
|
const cacheKeys = [
|
|
54
97
|
query.dataSource,
|
|
55
98
|
query.url,
|
|
56
|
-
query.format
|
|
99
|
+
query.format,
|
|
100
|
+
query.type
|
|
57
101
|
];
|
|
58
102
|
const cachedImage = await cache.read(cacheKeys);
|
|
59
|
-
if (cachedImage !== null) return new Response(Buffer.from(cachedImage.data), { headers: {
|
|
103
|
+
if (cachedImage !== null) return new Response(Buffer.from(cachedImage.data), { headers: {
|
|
104
|
+
"Content-Type": `image/${query.format}`,
|
|
105
|
+
"Cache-Control": "public, max-age=86400000"
|
|
106
|
+
} });
|
|
60
107
|
const dataSource = config.dataSources.find((o) => o.key === query.dataSource);
|
|
61
108
|
if (dataSource === void 0) throw new Error(`data source ${query.dataSource} not found`);
|
|
62
|
-
const resizedImage = await sharp(await dataSource.getImage({ url: query.url })).resize({ width: 1280 }).toFormat(query.format, { quality: 72 }).toBuffer();
|
|
109
|
+
const resizedImage = await sharp(await dataSource.getImage({ url: query.url })).resize({ width: query.type === "cover" ? 640 : 1280 }).toFormat(query.format, { quality: 72 }).toBuffer();
|
|
63
110
|
await cache.write(cacheKeys, resizedImage, 864e5);
|
|
64
|
-
return new Response(resizedImage, { headers: {
|
|
111
|
+
return new Response(resizedImage, { headers: {
|
|
112
|
+
"Content-Type": `image/${query.format}`,
|
|
113
|
+
"Cache-Control": "public, max-age=86400",
|
|
114
|
+
"CDN-Cache-Control": "public, max-age=2592000",
|
|
115
|
+
"Cloudflare-CDN-Cache-Control": "public, max-age=2592000"
|
|
116
|
+
} });
|
|
65
117
|
}, { query: t.Object({
|
|
66
118
|
url: t.String(),
|
|
67
|
-
format: t.Union([
|
|
119
|
+
format: t.Union([
|
|
120
|
+
t.Literal("avif"),
|
|
121
|
+
t.Literal("webp"),
|
|
122
|
+
t.Literal("jpg")
|
|
123
|
+
]),
|
|
124
|
+
type: t.Union([t.Literal("cover"), t.Literal("page")]),
|
|
68
125
|
dataSource: dataSourceKeys
|
|
69
|
-
}) }).listen(
|
|
126
|
+
}) }).listen({
|
|
127
|
+
hostname: config.hostname ?? "0.0.0.0",
|
|
128
|
+
port: config.port ?? 3e3
|
|
129
|
+
});
|
|
70
130
|
console.log(`🦊 Elysia is running at http://${server.server?.hostname}:${server.server?.port}`);
|
|
71
131
|
//#endregion
|
|
72
132
|
export {};
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env bun\nimport { t, Elysia } from \"elysia\";\nimport { swagger } from \"@elysiajs/swagger\";\nimport { cors } from \"@elysiajs/cors\";\nimport { defineCacheInstance } from \"@rayriffy/filesystem\";\nimport sharp from \"sharp\";\n\nimport { galleryModel, listingResultModel, type Config } from \"@riffyh/commons\";\nimport debug from \"debug\";\nimport path from \"node:path\";\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\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(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: t.String(),\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 \"/image\",\n async ({ query }) => {\n const cacheKeys = [query.dataSource, query.url, query.format];\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 },\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: 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 },\n });\n },\n {\n query: t.Object({\n url: t.String(),\n format: t.Union([t.Literal(\"webp\"), t.Literal(\"jpg\")]),\n dataSource: dataSourceKeys,\n }),\n },\n )\n .listen(3000);\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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"}
|
|
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(\"avif\"), t.Literal(\"webp\"), t.Literal(\"jpg\")]),\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,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@riffyh/server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/rayriffy/rayriffy-h.git"
|
|
7
7
|
},
|
|
8
|
-
"bin": "dist/index.mjs",
|
|
9
8
|
"files": [
|
|
10
9
|
"dist/"
|
|
11
10
|
],
|
|
@@ -21,19 +20,23 @@
|
|
|
21
20
|
"@elysiajs/swagger": "1.3.1",
|
|
22
21
|
"@rayriffy/filesystem": "1.0.1",
|
|
23
22
|
"@sinclair/typebox": "0.34.49",
|
|
23
|
+
"@toon-tools/elysia": "1.1.0",
|
|
24
24
|
"debug": "4.4.3",
|
|
25
25
|
"elysia": "1.4.28",
|
|
26
|
-
"sharp": "
|
|
27
|
-
"
|
|
26
|
+
"sharp": "0.34.5",
|
|
27
|
+
"tweetnacl": "1.0.3",
|
|
28
|
+
"tweetnacl-util": "0.15.1",
|
|
29
|
+
"@riffyh/commons": "2.1.1"
|
|
28
30
|
},
|
|
29
31
|
"devDependencies": {
|
|
30
32
|
"@types/debug": "4.1.13",
|
|
31
33
|
"tsdown": "0.21.7"
|
|
32
34
|
},
|
|
33
35
|
"scripts": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
"dev": "tsdown --watch",
|
|
37
|
+
"build": "tsdown"
|
|
38
|
+
},
|
|
39
|
+
"bin": {
|
|
40
|
+
"server": "dist/index.mjs"
|
|
38
41
|
}
|
|
39
42
|
}
|