@riffyh/server 1.0.1 → 1.0.3

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
@@ -1,5 +1,4 @@
1
1
  import { Elysia } from "elysia";
2
- import * as _$_riffyh_commons0 from "@riffyh/commons";
3
2
 
4
3
  //#region src/index.d.ts
5
4
  declare const server: Elysia<"", {
@@ -16,6 +15,9 @@ declare const server: Elysia<"", {
16
15
  } & {
17
16
  typebox: {};
18
17
  error: {};
18
+ } & {
19
+ typebox: {};
20
+ error: {};
19
21
  }, {
20
22
  schema: {};
21
23
  standaloneSchema: {};
@@ -36,6 +38,13 @@ declare const server: Elysia<"", {
36
38
  macroFn: {};
37
39
  parser: {};
38
40
  response: {};
41
+ } & {
42
+ schema: {};
43
+ standaloneSchema: {};
44
+ macro: {};
45
+ macroFn: {};
46
+ parser: {};
47
+ response: {};
39
48
  }, {
40
49
  get: {
41
50
  body: unknown;
@@ -66,20 +75,9 @@ declare const server: Elysia<"", {
66
75
  query: unknown;
67
76
  headers: unknown;
68
77
  response: {
69
- 200: {
70
- key: string;
71
- name: string;
72
- iconUrl: string;
73
- }[];
74
- 422: {
75
- type: "validation";
76
- on: string;
77
- summary?: string;
78
- message?: string;
79
- found?: unknown;
80
- property?: string;
81
- expected?: string;
82
- };
78
+ [x: string]: any;
79
+ [x: number]: any;
80
+ [x: symbol]: any;
83
81
  };
84
82
  };
85
83
  };
@@ -90,45 +88,13 @@ declare const server: Elysia<"", {
90
88
  params: {};
91
89
  query: {
92
90
  id: string;
93
- dataSource: string;
91
+ dataSource: never;
94
92
  };
95
93
  headers: unknown;
96
94
  response: {
97
- 200: {
98
- tags: {
99
- key: string;
100
- name: string;
101
- id: string;
102
- slug: string;
103
- type: _$_riffyh_commons0.TagType;
104
- }[];
105
- key: string;
106
- id: string;
107
- title: {
108
- display: string;
109
- original: string | null;
110
- };
111
- cover: {
112
- src: string;
113
- width: number;
114
- height: number;
115
- };
116
- pages: {
117
- src: string;
118
- width: number;
119
- height: number;
120
- order: number;
121
- }[];
122
- };
123
- 422: {
124
- type: "validation";
125
- on: string;
126
- summary?: string;
127
- message?: string;
128
- found?: unknown;
129
- property?: string;
130
- expected?: string;
131
- };
95
+ [x: string]: any;
96
+ [x: number]: any;
97
+ [x: symbol]: any;
132
98
  };
133
99
  };
134
100
  };
@@ -139,36 +105,61 @@ declare const server: Elysia<"", {
139
105
  params: {};
140
106
  query: {
141
107
  query?: string | undefined;
142
- page: number;
143
108
  dataSource: never;
109
+ page: number;
144
110
  };
145
111
  headers: unknown;
146
112
  response: {
147
- 200: {
148
- galleries: {
149
- key: string;
150
- id: string;
151
- title: {
152
- display: string;
153
- original: string | null;
154
- };
155
- cover: {
156
- src: string;
157
- width: number;
158
- height: number;
159
- };
160
- }[];
161
- currentPage: number;
162
- maximumPages: number;
113
+ [x: string]: any;
114
+ [x: number]: any;
115
+ [x: symbol]: any;
116
+ };
117
+ };
118
+ };
119
+ } & {
120
+ collection: {
121
+ export: {
122
+ post: {
123
+ body: string;
124
+ params: {};
125
+ query: unknown;
126
+ headers: unknown;
127
+ response: {
128
+ 200: string;
129
+ 422: {
130
+ type: "validation";
131
+ on: string;
132
+ summary?: string;
133
+ message?: string;
134
+ found?: unknown;
135
+ property?: string;
136
+ expected?: string;
137
+ };
163
138
  };
164
- 422: {
165
- type: "validation";
166
- on: string;
167
- summary?: string;
168
- message?: string;
169
- found?: unknown;
170
- property?: string;
171
- expected?: string;
139
+ };
140
+ };
141
+ };
142
+ } & {
143
+ collection: {
144
+ import: {
145
+ get: {
146
+ body: unknown;
147
+ params: {};
148
+ query: {
149
+ key: string;
150
+ };
151
+ headers: unknown;
152
+ response: {
153
+ 200: string;
154
+ 422: {
155
+ type: "validation";
156
+ on: string;
157
+ summary?: string;
158
+ message?: string;
159
+ found?: unknown;
160
+ property?: string;
161
+ expected?: string;
162
+ };
172
163
  };
173
164
  };
174
165
  };
@@ -179,9 +170,10 @@ declare const server: Elysia<"", {
179
170
  body: unknown;
180
171
  params: {};
181
172
  query: {
182
- url: string;
183
173
  dataSource: never;
184
- format: "webp" | "jpg";
174
+ url: string;
175
+ format: "webp" | "jpeg";
176
+ type: "page" | "cover";
185
177
  };
186
178
  headers: unknown;
187
179
  response: {
@@ -221,6 +213,12 @@ declare const server: Elysia<"", {
221
213
  schema: {};
222
214
  standaloneSchema: {};
223
215
  response: {};
216
+ } & {
217
+ derive: {};
218
+ resolve: {};
219
+ schema: {};
220
+ standaloneSchema: {};
221
+ response: {};
224
222
  }>;
225
223
  type Server = typeof server;
226
224
  //#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: t.String()
78
+ dataSource: dataSourceKeys
36
79
  }),
37
80
  response: galleryModel
38
81
  }).get("/listing", async ({ query }) => {
@@ -49,24 +92,37 @@ 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: { "Content-Type": `image/${query.format}` } });
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: { "Content-Type": `image/${query.format}` } });
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([t.Literal("webp"), t.Literal("jpg")]),
119
+ format: t.Union([t.Literal("webp"), t.Literal("jpeg")]),
120
+ type: t.Union([t.Literal("cover"), t.Literal("page")]),
68
121
  dataSource: dataSourceKeys
69
- }) }).listen(3e3);
122
+ }) }).listen({
123
+ hostname: config.hostname ?? "0.0.0.0",
124
+ port: config.port ?? 3e3
125
+ });
70
126
  console.log(`🦊 Elysia is running at http://${server.server?.hostname}:${server.server?.port}`);
71
127
  //#endregion
72
128
  export {};
@@ -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(\"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,11 +1,10 @@
1
1
  {
2
2
  "name": "@riffyh/server",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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
  ],
@@ -17,23 +16,27 @@
17
16
  "registry": "https://registry.npmjs.org"
18
17
  },
19
18
  "dependencies": {
20
- "@elysiajs/cors": "1.4.1",
19
+ "@elysiajs/cors": "1.4.2",
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
- "elysia": "1.4.28",
26
- "sharp": "^0.34.5",
27
- "@riffyh/commons": "2.1.0"
25
+ "elysia": "1.4.29",
26
+ "sharp": "0.35.2",
27
+ "tweetnacl": "1.0.3",
28
+ "tweetnacl-util": "0.15.1",
29
+ "@riffyh/commons": "2.1.2"
28
30
  },
29
31
  "devDependencies": {
30
32
  "@types/debug": "4.1.13",
31
- "tsdown": "0.21.7"
33
+ "tsdown": "0.22.3"
32
34
  },
33
35
  "scripts": {
34
- "test": "echo \"Error: no test specified\" && exit 1",
35
- "dev": "bun run --watch src/index.ts",
36
- "build": "tsdown",
37
- "start": "bun run dist/index.mjs"
36
+ "dev": "tsdown --watch",
37
+ "build": "tsdown"
38
+ },
39
+ "bin": {
40
+ "server": "dist/index.mjs"
38
41
  }
39
42
  }