@dimah-s3/server 0.3.2 → 0.4.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/README.md CHANGED
@@ -26,13 +26,23 @@ export const s3 = dimahS3({
26
26
  });
27
27
  ```
28
28
 
29
+ Mount with a framework adapter:
30
+
29
31
  ```ts
32
+ // Next.js
30
33
  import { toNextJsHandler } from "@dimah-s3/server/next";
31
- import { s3 } from "@/lib/s3";
32
-
33
34
  export const { GET, POST, DELETE } = toNextJsHandler(s3);
35
+
36
+ // Express — @dimah-s3/server/express → toExpressHandler
37
+ // Hono — @dimah-s3/server/hono → toHonoHandler
38
+ // Fastify — @dimah-s3/server/fastify → toFastifyHandler
39
+ // Elysia — @dimah-s3/server/elysia → toElysiaHandler
40
+ // SvelteKit — @dimah-s3/server/svelte-kit → toSvelteKitHandler
41
+ // Node http — @dimah-s3/server/node → toNodeHandler
34
42
  ```
35
43
 
44
+ See [Adapters](https://dimah-s3.vercel.app/docs/server/adapters) for full examples.
45
+
36
46
  Server-side (no HTTP):
37
47
 
38
48
  ```ts
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adapters.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapters.test.d.ts","sourceRoot":"","sources":["../../src/adapters/adapters.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,22 @@
1
+ import type { DimahS3HandlerSource } from "./types";
2
+ /** Structural Elysia context — avoids a hard dependency on `elysia`. */
3
+ type ElysiaContext = {
4
+ request: Request;
5
+ };
6
+ /**
7
+ * Adapt a dimah-s3 instance to an Elysia route handler.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { Elysia } from "elysia";
12
+ * import { toElysiaHandler } from "@dimah-s3/server/elysia";
13
+ * import { s3 } from "./s3";
14
+ *
15
+ * new Elysia()
16
+ * .all("/api/s3/*", toElysiaHandler(s3))
17
+ * .listen(3000);
18
+ * ```
19
+ */
20
+ export declare function toElysiaHandler(s3: DimahS3HandlerSource): (ctx: ElysiaContext) => Promise<Response>;
21
+ export {};
22
+ //# sourceMappingURL=elysia.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elysia.d.ts","sourceRoot":"","sources":["../../src/adapters/elysia.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAEpD,wEAAwE;AACxE,KAAK,aAAa,GAAG;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,oBAAoB,SACzC,aAAa,uBAC3B"}
@@ -0,0 +1,8 @@
1
+ // src/adapters/elysia.ts
2
+ function toElysiaHandler(s3) {
3
+ return (ctx) => s3.handler(ctx.request);
4
+ }
5
+
6
+ export { toElysiaHandler };
7
+ //# sourceMappingURL=elysia.js.map
8
+ //# sourceMappingURL=elysia.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/elysia.ts"],"names":[],"mappings":";AAqBO,SAAS,gBAAgB,EAAA,EAA0B;AACxD,EAAA,OAAO,CAAC,GAAA,KAAuB,EAAA,CAAG,OAAA,CAAQ,IAAI,OAAO,CAAA;AACvD","file":"elysia.js","sourcesContent":["import type { DimahS3HandlerSource } from \"./types\";\n\n/** Structural Elysia context — avoids a hard dependency on `elysia`. */\ntype ElysiaContext = {\n request: Request;\n};\n\n/**\n * Adapt a dimah-s3 instance to an Elysia route handler.\n *\n * @example\n * ```ts\n * import { Elysia } from \"elysia\";\n * import { toElysiaHandler } from \"@dimah-s3/server/elysia\";\n * import { s3 } from \"./s3\";\n *\n * new Elysia()\n * .all(\"/api/s3/*\", toElysiaHandler(s3))\n * .listen(3000);\n * ```\n */\nexport function toElysiaHandler(s3: DimahS3HandlerSource) {\n return (ctx: ElysiaContext) => s3.handler(ctx.request);\n}\n"]}
@@ -0,0 +1,24 @@
1
+ import { fromNodeHeaders } from "./node";
2
+ import type { DimahS3HandlerSource } from "./types";
3
+ export { fromNodeHeaders };
4
+ /**
5
+ * Adapt a dimah-s3 instance to an Express / Connect handler.
6
+ *
7
+ * Mount **before** `express.json()` so the request body is not consumed early.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import express from "express";
12
+ * import { toExpressHandler } from "@dimah-s3/server/express";
13
+ * import { s3 } from "./s3";
14
+ *
15
+ * const app = express();
16
+ * // Express 4
17
+ * app.all("/api/s3/*", toExpressHandler(s3));
18
+ * // Express 5 — named wildcard
19
+ * // app.all("/api/s3/*splat", toExpressHandler(s3));
20
+ * app.use(express.json());
21
+ * ```
22
+ */
23
+ export declare function toExpressHandler(s3: DimahS3HandlerSource): (req: import("node:http").IncomingMessage, res: import("node:http").ServerResponse) => Promise<void>;
24
+ //# sourceMappingURL=express.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/adapters/express.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAiB,MAAM,QAAQ,CAAC;AACxD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAEpD,OAAO,EAAE,eAAe,EAAE,CAAC;AAE3B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,oBAAoB,wGAExD"}
@@ -0,0 +1,125 @@
1
+ import { DimahS3Error, S3_ERROR_CODES } from '@dimah-s3/core';
2
+
3
+ // src/errors.ts
4
+ var errors = {
5
+ notFound: () => new DimahS3Error("Not Found", 404, { code: S3_ERROR_CODES.NOT_FOUND }),
6
+ invalidJson: () => new DimahS3Error("Invalid JSON payload", 400, {
7
+ code: S3_ERROR_CODES.INVALID_JSON
8
+ }),
9
+ forbidden: () => new DimahS3Error("Forbidden", 403, { code: S3_ERROR_CODES.FORBIDDEN }),
10
+ internalError: () => new DimahS3Error("Internal server error", 500, {
11
+ code: S3_ERROR_CODES.INTERNAL_ERROR
12
+ }),
13
+ objectNotFound: () => new DimahS3Error("Object not found", 404, {
14
+ code: S3_ERROR_CODES.OBJECT_NOT_FOUND
15
+ }),
16
+ s3NetworkError: (code) => new DimahS3Error(
17
+ `S3 endpoint unreachable (${code}): check your endpoint URL and network connectivity`,
18
+ 502,
19
+ { code: S3_ERROR_CODES.S3_NETWORK_ERROR, params: { code } }
20
+ ),
21
+ fieldRequired: (name) => new DimahS3Error(`${name} is required`, 400, {
22
+ code: S3_ERROR_CODES.FIELD_REQUIRED,
23
+ params: { name }
24
+ }),
25
+ /** Missing `key` on query-string routes (download / delete / list-parts). */
26
+ keyRequired: () => new DimahS3Error("key query parameter is required", 400, {
27
+ code: S3_ERROR_CODES.KEY_REQUIRED
28
+ }),
29
+ uploadIdRequired: () => new DimahS3Error("uploadId is required", 400, {
30
+ code: S3_ERROR_CODES.UPLOAD_ID_REQUIRED
31
+ }),
32
+ partNumberInvalid: () => new DimahS3Error("partNumber must be a positive integer", 400, {
33
+ code: S3_ERROR_CODES.PART_NUMBER_INVALID
34
+ }),
35
+ partsRequired: () => new DimahS3Error("At least one valid part is required", 400, {
36
+ code: S3_ERROR_CODES.PARTS_REQUIRED
37
+ }),
38
+ fileSizeRequiredUpload: () => new DimahS3Error(
39
+ "fileSize is required when upload.requireFileSize is enabled",
40
+ 400,
41
+ { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_UPLOAD }
42
+ ),
43
+ fileSizeRequiredMultipart: () => new DimahS3Error(
44
+ "fileSize is required when multipart.requireFileSize is enabled",
45
+ 400,
46
+ { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART }
47
+ )
48
+ };
49
+
50
+ // src/adapters/node.ts
51
+ function fromNodeHeaders(nodeHeaders) {
52
+ const headers = new Headers();
53
+ for (const [key, value] of Object.entries(nodeHeaders)) {
54
+ if (value == null) continue;
55
+ if (Array.isArray(value)) {
56
+ for (const v of value) headers.append(key, v);
57
+ } else {
58
+ headers.set(key, value);
59
+ }
60
+ }
61
+ return headers;
62
+ }
63
+ function collectBody(req) {
64
+ return new Promise((resolve, reject) => {
65
+ const chunks = [];
66
+ req.on("data", (chunk) => {
67
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
68
+ });
69
+ req.on("end", () => resolve(Buffer.concat(chunks)));
70
+ req.on("error", reject);
71
+ });
72
+ }
73
+ function toWebRequest(req, body) {
74
+ const host = req.headers.host ?? "localhost";
75
+ const protocol = req.socket.encrypted === true ? "https" : "http";
76
+ const url = new URL(req.url ?? "/", `${protocol}://${host}`);
77
+ const headers = fromNodeHeaders(req.headers);
78
+ const method = (req.method ?? "GET").toUpperCase();
79
+ const init = { method, headers };
80
+ if (method !== "GET" && method !== "HEAD") {
81
+ init.body = new Uint8Array(body);
82
+ init.duplex = "half";
83
+ }
84
+ return new Request(url, init);
85
+ }
86
+ async function writeWebResponse(res, response) {
87
+ res.statusCode = response.status;
88
+ response.headers.forEach((value, key) => {
89
+ res.setHeader(key, value);
90
+ });
91
+ const buffer = Buffer.from(await response.arrayBuffer());
92
+ res.end(buffer);
93
+ }
94
+ function toNodeHandler(s3) {
95
+ return async (req, res) => {
96
+ try {
97
+ const body = await collectBody(req);
98
+ const request = toWebRequest(req, body);
99
+ const response = await s3.handler(request);
100
+ await writeWebResponse(res, response);
101
+ } catch (err) {
102
+ console.error("[S3 API]", err);
103
+ if (!res.headersSent) {
104
+ const fallback = errors.internalError();
105
+ res.statusCode = fallback.status;
106
+ res.setHeader("Content-Type", "application/json");
107
+ res.end(
108
+ JSON.stringify({
109
+ message: fallback.message,
110
+ code: fallback.code
111
+ })
112
+ );
113
+ }
114
+ }
115
+ };
116
+ }
117
+
118
+ // src/adapters/express.ts
119
+ function toExpressHandler(s3) {
120
+ return toNodeHandler(s3);
121
+ }
122
+
123
+ export { fromNodeHeaders, toExpressHandler };
124
+ //# sourceMappingURL=express.js.map
125
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/adapters/node.ts","../../src/adapters/express.ts"],"names":[],"mappings":";;;AAKO,IAAM,MAAA,GAAS;AAAA,EACpB,QAAA,EAAU,MACR,IAAI,YAAA,CAAa,WAAA,EAAa,KAAK,EAAE,IAAA,EAAM,cAAA,CAAe,SAAA,EAAW,CAAA;AAAA,EAEvE,WAAA,EAAa,MACX,IAAI,YAAA,CAAa,wBAAwB,GAAA,EAAK;AAAA,IAC5C,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,SAAA,EAAW,MACT,IAAI,YAAA,CAAa,WAAA,EAAa,KAAK,EAAE,IAAA,EAAM,cAAA,CAAe,SAAA,EAAW,CAAA;AAAA,EAEvE,aAAA,EAAe,MACb,IAAI,YAAA,CAAa,yBAAyB,GAAA,EAAK;AAAA,IAC7C,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,cAAA,EAAgB,MACd,IAAI,YAAA,CAAa,oBAAoB,GAAA,EAAK;AAAA,IACxC,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,cAAA,EAAgB,CAAC,IAAA,KACf,IAAI,YAAA;AAAA,IACF,4BAA4B,IAAI,CAAA,mDAAA,CAAA;AAAA,IAChC,GAAA;AAAA,IACA,EAAE,IAAA,EAAM,cAAA,CAAe,kBAAkB,MAAA,EAAQ,EAAE,MAAK;AAAE,GAC5D;AAAA,EAEF,aAAA,EAAe,CAAC,IAAA,KACd,IAAI,aAAa,CAAA,EAAG,IAAI,gBAAgB,GAAA,EAAK;AAAA,IAC3C,MAAM,cAAA,CAAe,cAAA;AAAA,IACrB,MAAA,EAAQ,EAAE,IAAA;AAAK,GAChB,CAAA;AAAA;AAAA,EAGH,WAAA,EAAa,MACX,IAAI,YAAA,CAAa,mCAAmC,GAAA,EAAK;AAAA,IACvD,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,gBAAA,EAAkB,MAChB,IAAI,YAAA,CAAa,wBAAwB,GAAA,EAAK;AAAA,IAC5C,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,iBAAA,EAAmB,MACjB,IAAI,YAAA,CAAa,yCAAyC,GAAA,EAAK;AAAA,IAC7D,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,aAAA,EAAe,MACb,IAAI,YAAA,CAAa,uCAAuC,GAAA,EAAK;AAAA,IAC3D,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,sBAAA,EAAwB,MACtB,IAAI,YAAA;AAAA,IACF,6DAAA;AAAA,IACA,GAAA;AAAA,IACA,EAAE,IAAA,EAAM,cAAA,CAAe,yBAAA;AAA0B,GACnD;AAAA,EAEF,yBAAA,EAA2B,MACzB,IAAI,YAAA;AAAA,IACF,gEAAA;AAAA,IACA,GAAA;AAAA,IACA,EAAE,IAAA,EAAM,cAAA,CAAe,4BAAA;AAA6B;AAE1D,CAAA;;;ACvDO,SAAS,gBAAgB,WAAA,EAA2C;AACzE,EAAA,MAAM,OAAA,GAAU,IAAI,OAAA,EAAQ;AAC5B,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,WAAW,CAAA,EAAG;AACtD,IAAA,IAAI,SAAS,IAAA,EAAM;AACnB,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,MAAA,KAAA,MAAW,CAAA,IAAK,KAAA,EAAO,OAAA,CAAQ,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,IAC9C,CAAA,MAAO;AACL,MAAA,OAAA,CAAQ,GAAA,CAAI,KAAK,KAAK,CAAA;AAAA,IACxB;AAAA,EACF;AACA,EAAA,OAAO,OAAA;AACT;AAEA,SAAS,YAAY,GAAA,EAAuC;AAC1D,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,MAAM,SAAmB,EAAC;AAC1B,IAAA,GAAA,CAAI,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAA2B;AACzC,MAAA,MAAA,CAAO,IAAA,CAAK,OAAO,KAAA,KAAU,QAAA,GAAW,OAAO,IAAA,CAAK,KAAK,IAAI,KAAK,CAAA;AAAA,IACpE,CAAC,CAAA;AACD,IAAA,GAAA,CAAI,EAAA,CAAG,OAAO,MAAM,OAAA,CAAQ,OAAO,MAAA,CAAO,MAAM,CAAC,CAAC,CAAA;AAClD,IAAA,GAAA,CAAI,EAAA,CAAG,SAAS,MAAM,CAAA;AAAA,EACxB,CAAC,CAAA;AACH;AAEA,SAAS,YAAA,CAAa,KAAsB,IAAA,EAAuB;AACjE,EAAA,MAAM,IAAA,GAAO,GAAA,CAAI,OAAA,CAAQ,IAAA,IAAQ,WAAA;AACjC,EAAA,MAAM,QAAA,GACH,GAAA,CAAI,MAAA,CAAmC,SAAA,KAAc,OAClD,OAAA,GACA,MAAA;AACN,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,GAAA,CAAI,GAAA,IAAO,KAAK,CAAA,EAAG,QAAQ,CAAA,GAAA,EAAM,IAAI,CAAA,CAAE,CAAA;AAE3D,EAAA,MAAM,OAAA,GAAU,eAAA,CAAgB,GAAA,CAAI,OAAO,CAAA;AAC3C,EAAA,MAAM,MAAA,GAAA,CAAU,GAAA,CAAI,MAAA,IAAU,KAAA,EAAO,WAAA,EAAY;AACjD,EAAA,MAAM,IAAA,GAAoB,EAAE,MAAA,EAAQ,OAAA,EAAQ;AAC5C,EAAA,IAAI,MAAA,KAAW,KAAA,IAAS,MAAA,KAAW,MAAA,EAAQ;AACzC,IAAA,IAAA,CAAK,IAAA,GAAO,IAAI,UAAA,CAAW,IAAI,CAAA;AAE/B,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AAEA,EAAA,OAAO,IAAI,OAAA,CAAQ,GAAA,EAAK,IAAI,CAAA;AAC9B;AAEA,eAAe,gBAAA,CACb,KACA,QAAA,EACe;AACf,EAAA,GAAA,CAAI,aAAa,QAAA,CAAS,MAAA;AAC1B,EAAA,QAAA,CAAS,OAAA,CAAQ,OAAA,CAAQ,CAAC,KAAA,EAAO,GAAA,KAAQ;AACvC,IAAA,GAAA,CAAI,SAAA,CAAU,KAAK,KAAK,CAAA;AAAA,EAC1B,CAAC,CAAA;AACD,EAAA,MAAM,SAAS,MAAA,CAAO,IAAA,CAAK,MAAM,QAAA,CAAS,aAAa,CAAA;AACvD,EAAA,GAAA,CAAI,IAAI,MAAM,CAAA;AAChB;AAcO,SAAS,cAAc,EAAA,EAA0B;AACtD,EAAA,OAAO,OAAO,KAAsB,GAAA,KAAuC;AACzE,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,GAAO,MAAM,WAAA,CAAY,GAAG,CAAA;AAClC,MAAA,MAAM,OAAA,GAAU,YAAA,CAAa,GAAA,EAAK,IAAI,CAAA;AACtC,MAAA,MAAM,QAAA,GAAW,MAAM,EAAA,CAAG,OAAA,CAAQ,OAAO,CAAA;AACzC,MAAA,MAAM,gBAAA,CAAiB,KAAK,QAAQ,CAAA;AAAA,IACtC,SAAS,GAAA,EAAK;AACZ,MAAA,OAAA,CAAQ,KAAA,CAAM,YAAY,GAAG,CAAA;AAC7B,MAAA,IAAI,CAAC,IAAI,WAAA,EAAa;AACpB,QAAA,MAAM,QAAA,GAAW,OAAO,aAAA,EAAc;AACtC,QAAA,GAAA,CAAI,aAAa,QAAA,CAAS,MAAA;AAC1B,QAAA,GAAA,CAAI,SAAA,CAAU,gBAAgB,kBAAkB,CAAA;AAChD,QAAA,GAAA,CAAI,GAAA;AAAA,UACF,KAAK,SAAA,CAAU;AAAA,YACb,SAAS,QAAA,CAAS,OAAA;AAAA,YAClB,MAAM,QAAA,CAAS;AAAA,WAChB;AAAA,SACH;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAA;AACF;;;ACrFO,SAAS,iBAAiB,EAAA,EAA0B;AACzD,EAAA,OAAO,cAAc,EAAE,CAAA;AACzB","file":"express.js","sourcesContent":["import { DimahS3Error, S3_ERROR_CODES, type S3ErrorCode } from \"@dimah-s3/core\";\n\nexport { DimahS3Error };\n\n/** English API errors with stable codes for client-side localization. */\nexport const errors = {\n notFound: () =>\n new DimahS3Error(\"Not Found\", 404, { code: S3_ERROR_CODES.NOT_FOUND }),\n\n invalidJson: () =>\n new DimahS3Error(\"Invalid JSON payload\", 400, {\n code: S3_ERROR_CODES.INVALID_JSON,\n }),\n\n forbidden: () =>\n new DimahS3Error(\"Forbidden\", 403, { code: S3_ERROR_CODES.FORBIDDEN }),\n\n internalError: () =>\n new DimahS3Error(\"Internal server error\", 500, {\n code: S3_ERROR_CODES.INTERNAL_ERROR,\n }),\n\n objectNotFound: () =>\n new DimahS3Error(\"Object not found\", 404, {\n code: S3_ERROR_CODES.OBJECT_NOT_FOUND,\n }),\n\n s3NetworkError: (code: string) =>\n new DimahS3Error(\n `S3 endpoint unreachable (${code}): check your endpoint URL and network connectivity`,\n 502,\n { code: S3_ERROR_CODES.S3_NETWORK_ERROR, params: { code } },\n ),\n\n fieldRequired: (name: string) =>\n new DimahS3Error(`${name} is required`, 400, {\n code: S3_ERROR_CODES.FIELD_REQUIRED,\n params: { name },\n }),\n\n /** Missing `key` on query-string routes (download / delete / list-parts). */\n keyRequired: () =>\n new DimahS3Error(\"key query parameter is required\", 400, {\n code: S3_ERROR_CODES.KEY_REQUIRED,\n }),\n\n uploadIdRequired: () =>\n new DimahS3Error(\"uploadId is required\", 400, {\n code: S3_ERROR_CODES.UPLOAD_ID_REQUIRED,\n }),\n\n partNumberInvalid: () =>\n new DimahS3Error(\"partNumber must be a positive integer\", 400, {\n code: S3_ERROR_CODES.PART_NUMBER_INVALID,\n }),\n\n partsRequired: () =>\n new DimahS3Error(\"At least one valid part is required\", 400, {\n code: S3_ERROR_CODES.PARTS_REQUIRED,\n }),\n\n fileSizeRequiredUpload: () =>\n new DimahS3Error(\n \"fileSize is required when upload.requireFileSize is enabled\",\n 400,\n { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_UPLOAD },\n ),\n\n fileSizeRequiredMultipart: () =>\n new DimahS3Error(\n \"fileSize is required when multipart.requireFileSize is enabled\",\n 400,\n { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART },\n ),\n} as const;\n\nexport type ServerErrors = typeof errors;\n\n/** Trim and require a non-empty string; throws {@link errors.fieldRequired}. */\nexport function requireString(value: unknown, name: string): string {\n const trimmed = typeof value === \"string\" ? value.trim() : \"\";\n if (!trimmed) throw errors.fieldRequired(name);\n return trimmed;\n}\n\nfunction statusOf(err: unknown, fallback: number): number {\n return typeof (err as { status?: unknown })?.status === \"number\"\n ? (err as { status: number }).status\n : fallback;\n}\n\n/**\n * Convert hook/S3 throws into {@link DimahS3Error}.\n *\n * - Existing {@link DimahS3Error} instances are preserved (including subclasses).\n * - Plain `Error`s keep their message; `code` is only applied when provided\n * (omit it so clients show the custom English message instead of a mapped\n * library string).\n */\nexport function toDimahS3Error(\n err: unknown,\n fallbackMessage: string,\n fallbackStatus = 403,\n options?: { code?: S3ErrorCode | string },\n): DimahS3Error {\n if (err instanceof DimahS3Error) return err;\n\n const message =\n err instanceof Error && err.message.trim() ? err.message : fallbackMessage;\n const status = statusOf(err, fallbackStatus);\n\n return new DimahS3Error(message, status, {\n cause: err instanceof Error ? err : undefined,\n ...(options?.code !== undefined ? { code: options.code } : {}),\n });\n}\n","import type {\n IncomingHttpHeaders,\n IncomingMessage,\n ServerResponse,\n} from \"node:http\";\nimport { errors } from \"../errors\";\nimport type { DimahS3HandlerSource } from \"./types\";\n\n/**\n * Convert Node.js / Express request headers into a Web {@link Headers} object.\n * Useful when calling `s3.api.*` from a Node handler.\n *\n * @example\n * ```ts\n * import { fromNodeHeaders } from \"@dimah-s3/server/node\";\n *\n * await s3.api.download(key, { headers: fromNodeHeaders(req.headers) });\n * ```\n */\nexport function fromNodeHeaders(nodeHeaders: IncomingHttpHeaders): Headers {\n const headers = new Headers();\n for (const [key, value] of Object.entries(nodeHeaders)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const v of value) headers.append(key, v);\n } else {\n headers.set(key, value);\n }\n }\n return headers;\n}\n\nfunction collectBody(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer | string) => {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n\nfunction toWebRequest(req: IncomingMessage, body: Buffer): Request {\n const host = req.headers.host ?? \"localhost\";\n const protocol =\n (req.socket as { encrypted?: boolean }).encrypted === true\n ? \"https\"\n : \"http\";\n const url = new URL(req.url ?? \"/\", `${protocol}://${host}`);\n\n const headers = fromNodeHeaders(req.headers);\n const method = (req.method ?? \"GET\").toUpperCase();\n const init: RequestInit = { method, headers };\n if (method !== \"GET\" && method !== \"HEAD\") {\n init.body = new Uint8Array(body);\n // @ts-expect-error Node fetch duplex for streaming bodies\n init.duplex = \"half\";\n }\n\n return new Request(url, init);\n}\n\nasync function writeWebResponse(\n res: ServerResponse,\n response: Response,\n): Promise<void> {\n res.statusCode = response.status;\n response.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n const buffer = Buffer.from(await response.arrayBuffer());\n res.end(buffer);\n}\n\n/**\n * Adapt a dimah-s3 instance to Node.js `http` / Express-style handlers.\n *\n * @example\n * ```ts\n * import { createServer } from \"node:http\";\n * import { toNodeHandler } from \"@dimah-s3/server/node\";\n * import { s3 } from \"./s3\";\n *\n * createServer(toNodeHandler(s3)).listen(3000);\n * ```\n */\nexport function toNodeHandler(s3: DimahS3HandlerSource) {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n try {\n const body = await collectBody(req);\n const request = toWebRequest(req, body);\n const response = await s3.handler(request);\n await writeWebResponse(res, response);\n } catch (err) {\n console.error(\"[S3 API]\", err);\n if (!res.headersSent) {\n const fallback = errors.internalError();\n res.statusCode = fallback.status;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n message: fallback.message,\n code: fallback.code,\n }),\n );\n }\n }\n };\n}\n","import { fromNodeHeaders, toNodeHandler } from \"./node\";\nimport type { DimahS3HandlerSource } from \"./types\";\n\nexport { fromNodeHeaders };\n\n/**\n * Adapt a dimah-s3 instance to an Express / Connect handler.\n *\n * Mount **before** `express.json()` so the request body is not consumed early.\n *\n * @example\n * ```ts\n * import express from \"express\";\n * import { toExpressHandler } from \"@dimah-s3/server/express\";\n * import { s3 } from \"./s3\";\n *\n * const app = express();\n * // Express 4\n * app.all(\"/api/s3/*\", toExpressHandler(s3));\n * // Express 5 — named wildcard\n * // app.all(\"/api/s3/*splat\", toExpressHandler(s3));\n * app.use(express.json());\n * ```\n */\nexport function toExpressHandler(s3: DimahS3HandlerSource) {\n return toNodeHandler(s3);\n}\n"]}
@@ -0,0 +1,30 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { DimahS3HandlerSource } from "./types";
3
+ /** Structural Fastify request/reply — avoids a hard dependency on `fastify`. */
4
+ type FastifyRequestLike = {
5
+ raw: IncomingMessage;
6
+ };
7
+ type FastifyReplyLike = {
8
+ raw: ServerResponse;
9
+ hijack: () => void;
10
+ };
11
+ /**
12
+ * Adapt a dimah-s3 instance to a Fastify route handler.
13
+ *
14
+ * Uses `reply.hijack()` and the Node adapter so Fastify does not touch the
15
+ * response stream. Mount this route **before** body parsers (or disable JSON
16
+ * parsing for `/api/s3/*`) so the request body stays readable.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * import Fastify from "fastify";
21
+ * import { toFastifyHandler } from "@dimah-s3/server/fastify";
22
+ * import { s3 } from "./s3";
23
+ *
24
+ * const app = Fastify();
25
+ * app.all("/api/s3/*", toFastifyHandler(s3));
26
+ * ```
27
+ */
28
+ export declare function toFastifyHandler(s3: DimahS3HandlerSource): (req: FastifyRequestLike, reply: FastifyReplyLike) => Promise<void>;
29
+ export {};
30
+ //# sourceMappingURL=fastify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../../src/adapters/fastify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAEpD,gFAAgF;AAChF,KAAK,kBAAkB,GAAG;IACxB,GAAG,EAAE,eAAe,CAAC;CACtB,CAAC;AAEF,KAAK,gBAAgB,GAAG;IACtB,GAAG,EAAE,cAAc,CAAC;IACpB,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,oBAAoB,SAEpC,kBAAkB,SAAS,gBAAgB,mBAI/D"}
@@ -0,0 +1,129 @@
1
+ import { DimahS3Error, S3_ERROR_CODES } from '@dimah-s3/core';
2
+
3
+ // src/errors.ts
4
+ var errors = {
5
+ notFound: () => new DimahS3Error("Not Found", 404, { code: S3_ERROR_CODES.NOT_FOUND }),
6
+ invalidJson: () => new DimahS3Error("Invalid JSON payload", 400, {
7
+ code: S3_ERROR_CODES.INVALID_JSON
8
+ }),
9
+ forbidden: () => new DimahS3Error("Forbidden", 403, { code: S3_ERROR_CODES.FORBIDDEN }),
10
+ internalError: () => new DimahS3Error("Internal server error", 500, {
11
+ code: S3_ERROR_CODES.INTERNAL_ERROR
12
+ }),
13
+ objectNotFound: () => new DimahS3Error("Object not found", 404, {
14
+ code: S3_ERROR_CODES.OBJECT_NOT_FOUND
15
+ }),
16
+ s3NetworkError: (code) => new DimahS3Error(
17
+ `S3 endpoint unreachable (${code}): check your endpoint URL and network connectivity`,
18
+ 502,
19
+ { code: S3_ERROR_CODES.S3_NETWORK_ERROR, params: { code } }
20
+ ),
21
+ fieldRequired: (name) => new DimahS3Error(`${name} is required`, 400, {
22
+ code: S3_ERROR_CODES.FIELD_REQUIRED,
23
+ params: { name }
24
+ }),
25
+ /** Missing `key` on query-string routes (download / delete / list-parts). */
26
+ keyRequired: () => new DimahS3Error("key query parameter is required", 400, {
27
+ code: S3_ERROR_CODES.KEY_REQUIRED
28
+ }),
29
+ uploadIdRequired: () => new DimahS3Error("uploadId is required", 400, {
30
+ code: S3_ERROR_CODES.UPLOAD_ID_REQUIRED
31
+ }),
32
+ partNumberInvalid: () => new DimahS3Error("partNumber must be a positive integer", 400, {
33
+ code: S3_ERROR_CODES.PART_NUMBER_INVALID
34
+ }),
35
+ partsRequired: () => new DimahS3Error("At least one valid part is required", 400, {
36
+ code: S3_ERROR_CODES.PARTS_REQUIRED
37
+ }),
38
+ fileSizeRequiredUpload: () => new DimahS3Error(
39
+ "fileSize is required when upload.requireFileSize is enabled",
40
+ 400,
41
+ { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_UPLOAD }
42
+ ),
43
+ fileSizeRequiredMultipart: () => new DimahS3Error(
44
+ "fileSize is required when multipart.requireFileSize is enabled",
45
+ 400,
46
+ { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART }
47
+ )
48
+ };
49
+
50
+ // src/adapters/node.ts
51
+ function fromNodeHeaders(nodeHeaders) {
52
+ const headers = new Headers();
53
+ for (const [key, value] of Object.entries(nodeHeaders)) {
54
+ if (value == null) continue;
55
+ if (Array.isArray(value)) {
56
+ for (const v of value) headers.append(key, v);
57
+ } else {
58
+ headers.set(key, value);
59
+ }
60
+ }
61
+ return headers;
62
+ }
63
+ function collectBody(req) {
64
+ return new Promise((resolve, reject) => {
65
+ const chunks = [];
66
+ req.on("data", (chunk) => {
67
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
68
+ });
69
+ req.on("end", () => resolve(Buffer.concat(chunks)));
70
+ req.on("error", reject);
71
+ });
72
+ }
73
+ function toWebRequest(req, body) {
74
+ const host = req.headers.host ?? "localhost";
75
+ const protocol = req.socket.encrypted === true ? "https" : "http";
76
+ const url = new URL(req.url ?? "/", `${protocol}://${host}`);
77
+ const headers = fromNodeHeaders(req.headers);
78
+ const method = (req.method ?? "GET").toUpperCase();
79
+ const init = { method, headers };
80
+ if (method !== "GET" && method !== "HEAD") {
81
+ init.body = new Uint8Array(body);
82
+ init.duplex = "half";
83
+ }
84
+ return new Request(url, init);
85
+ }
86
+ async function writeWebResponse(res, response) {
87
+ res.statusCode = response.status;
88
+ response.headers.forEach((value, key) => {
89
+ res.setHeader(key, value);
90
+ });
91
+ const buffer = Buffer.from(await response.arrayBuffer());
92
+ res.end(buffer);
93
+ }
94
+ function toNodeHandler(s3) {
95
+ return async (req, res) => {
96
+ try {
97
+ const body = await collectBody(req);
98
+ const request = toWebRequest(req, body);
99
+ const response = await s3.handler(request);
100
+ await writeWebResponse(res, response);
101
+ } catch (err) {
102
+ console.error("[S3 API]", err);
103
+ if (!res.headersSent) {
104
+ const fallback = errors.internalError();
105
+ res.statusCode = fallback.status;
106
+ res.setHeader("Content-Type", "application/json");
107
+ res.end(
108
+ JSON.stringify({
109
+ message: fallback.message,
110
+ code: fallback.code
111
+ })
112
+ );
113
+ }
114
+ }
115
+ };
116
+ }
117
+
118
+ // src/adapters/fastify.ts
119
+ function toFastifyHandler(s3) {
120
+ const handler = toNodeHandler(s3);
121
+ return async (req, reply) => {
122
+ reply.hijack();
123
+ await handler(req.raw, reply.raw);
124
+ };
125
+ }
126
+
127
+ export { toFastifyHandler };
128
+ //# sourceMappingURL=fastify.js.map
129
+ //# sourceMappingURL=fastify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/adapters/node.ts","../../src/adapters/fastify.ts"],"names":[],"mappings":"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","file":"fastify.js","sourcesContent":["import { DimahS3Error, S3_ERROR_CODES, type S3ErrorCode } from \"@dimah-s3/core\";\n\nexport { DimahS3Error };\n\n/** English API errors with stable codes for client-side localization. */\nexport const errors = {\n notFound: () =>\n new DimahS3Error(\"Not Found\", 404, { code: S3_ERROR_CODES.NOT_FOUND }),\n\n invalidJson: () =>\n new DimahS3Error(\"Invalid JSON payload\", 400, {\n code: S3_ERROR_CODES.INVALID_JSON,\n }),\n\n forbidden: () =>\n new DimahS3Error(\"Forbidden\", 403, { code: S3_ERROR_CODES.FORBIDDEN }),\n\n internalError: () =>\n new DimahS3Error(\"Internal server error\", 500, {\n code: S3_ERROR_CODES.INTERNAL_ERROR,\n }),\n\n objectNotFound: () =>\n new DimahS3Error(\"Object not found\", 404, {\n code: S3_ERROR_CODES.OBJECT_NOT_FOUND,\n }),\n\n s3NetworkError: (code: string) =>\n new DimahS3Error(\n `S3 endpoint unreachable (${code}): check your endpoint URL and network connectivity`,\n 502,\n { code: S3_ERROR_CODES.S3_NETWORK_ERROR, params: { code } },\n ),\n\n fieldRequired: (name: string) =>\n new DimahS3Error(`${name} is required`, 400, {\n code: S3_ERROR_CODES.FIELD_REQUIRED,\n params: { name },\n }),\n\n /** Missing `key` on query-string routes (download / delete / list-parts). */\n keyRequired: () =>\n new DimahS3Error(\"key query parameter is required\", 400, {\n code: S3_ERROR_CODES.KEY_REQUIRED,\n }),\n\n uploadIdRequired: () =>\n new DimahS3Error(\"uploadId is required\", 400, {\n code: S3_ERROR_CODES.UPLOAD_ID_REQUIRED,\n }),\n\n partNumberInvalid: () =>\n new DimahS3Error(\"partNumber must be a positive integer\", 400, {\n code: S3_ERROR_CODES.PART_NUMBER_INVALID,\n }),\n\n partsRequired: () =>\n new DimahS3Error(\"At least one valid part is required\", 400, {\n code: S3_ERROR_CODES.PARTS_REQUIRED,\n }),\n\n fileSizeRequiredUpload: () =>\n new DimahS3Error(\n \"fileSize is required when upload.requireFileSize is enabled\",\n 400,\n { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_UPLOAD },\n ),\n\n fileSizeRequiredMultipart: () =>\n new DimahS3Error(\n \"fileSize is required when multipart.requireFileSize is enabled\",\n 400,\n { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART },\n ),\n} as const;\n\nexport type ServerErrors = typeof errors;\n\n/** Trim and require a non-empty string; throws {@link errors.fieldRequired}. */\nexport function requireString(value: unknown, name: string): string {\n const trimmed = typeof value === \"string\" ? value.trim() : \"\";\n if (!trimmed) throw errors.fieldRequired(name);\n return trimmed;\n}\n\nfunction statusOf(err: unknown, fallback: number): number {\n return typeof (err as { status?: unknown })?.status === \"number\"\n ? (err as { status: number }).status\n : fallback;\n}\n\n/**\n * Convert hook/S3 throws into {@link DimahS3Error}.\n *\n * - Existing {@link DimahS3Error} instances are preserved (including subclasses).\n * - Plain `Error`s keep their message; `code` is only applied when provided\n * (omit it so clients show the custom English message instead of a mapped\n * library string).\n */\nexport function toDimahS3Error(\n err: unknown,\n fallbackMessage: string,\n fallbackStatus = 403,\n options?: { code?: S3ErrorCode | string },\n): DimahS3Error {\n if (err instanceof DimahS3Error) return err;\n\n const message =\n err instanceof Error && err.message.trim() ? err.message : fallbackMessage;\n const status = statusOf(err, fallbackStatus);\n\n return new DimahS3Error(message, status, {\n cause: err instanceof Error ? err : undefined,\n ...(options?.code !== undefined ? { code: options.code } : {}),\n });\n}\n","import type {\n IncomingHttpHeaders,\n IncomingMessage,\n ServerResponse,\n} from \"node:http\";\nimport { errors } from \"../errors\";\nimport type { DimahS3HandlerSource } from \"./types\";\n\n/**\n * Convert Node.js / Express request headers into a Web {@link Headers} object.\n * Useful when calling `s3.api.*` from a Node handler.\n *\n * @example\n * ```ts\n * import { fromNodeHeaders } from \"@dimah-s3/server/node\";\n *\n * await s3.api.download(key, { headers: fromNodeHeaders(req.headers) });\n * ```\n */\nexport function fromNodeHeaders(nodeHeaders: IncomingHttpHeaders): Headers {\n const headers = new Headers();\n for (const [key, value] of Object.entries(nodeHeaders)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const v of value) headers.append(key, v);\n } else {\n headers.set(key, value);\n }\n }\n return headers;\n}\n\nfunction collectBody(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer | string) => {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n\nfunction toWebRequest(req: IncomingMessage, body: Buffer): Request {\n const host = req.headers.host ?? \"localhost\";\n const protocol =\n (req.socket as { encrypted?: boolean }).encrypted === true\n ? \"https\"\n : \"http\";\n const url = new URL(req.url ?? \"/\", `${protocol}://${host}`);\n\n const headers = fromNodeHeaders(req.headers);\n const method = (req.method ?? \"GET\").toUpperCase();\n const init: RequestInit = { method, headers };\n if (method !== \"GET\" && method !== \"HEAD\") {\n init.body = new Uint8Array(body);\n // @ts-expect-error Node fetch duplex for streaming bodies\n init.duplex = \"half\";\n }\n\n return new Request(url, init);\n}\n\nasync function writeWebResponse(\n res: ServerResponse,\n response: Response,\n): Promise<void> {\n res.statusCode = response.status;\n response.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n const buffer = Buffer.from(await response.arrayBuffer());\n res.end(buffer);\n}\n\n/**\n * Adapt a dimah-s3 instance to Node.js `http` / Express-style handlers.\n *\n * @example\n * ```ts\n * import { createServer } from \"node:http\";\n * import { toNodeHandler } from \"@dimah-s3/server/node\";\n * import { s3 } from \"./s3\";\n *\n * createServer(toNodeHandler(s3)).listen(3000);\n * ```\n */\nexport function toNodeHandler(s3: DimahS3HandlerSource) {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n try {\n const body = await collectBody(req);\n const request = toWebRequest(req, body);\n const response = await s3.handler(request);\n await writeWebResponse(res, response);\n } catch (err) {\n console.error(\"[S3 API]\", err);\n if (!res.headersSent) {\n const fallback = errors.internalError();\n res.statusCode = fallback.status;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n message: fallback.message,\n code: fallback.code,\n }),\n );\n }\n }\n };\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { toNodeHandler } from \"./node\";\nimport type { DimahS3HandlerSource } from \"./types\";\n\n/** Structural Fastify request/reply — avoids a hard dependency on `fastify`. */\ntype FastifyRequestLike = {\n raw: IncomingMessage;\n};\n\ntype FastifyReplyLike = {\n raw: ServerResponse;\n hijack: () => void;\n};\n\n/**\n * Adapt a dimah-s3 instance to a Fastify route handler.\n *\n * Uses `reply.hijack()` and the Node adapter so Fastify does not touch the\n * response stream. Mount this route **before** body parsers (or disable JSON\n * parsing for `/api/s3/*`) so the request body stays readable.\n *\n * @example\n * ```ts\n * import Fastify from \"fastify\";\n * import { toFastifyHandler } from \"@dimah-s3/server/fastify\";\n * import { s3 } from \"./s3\";\n *\n * const app = Fastify();\n * app.all(\"/api/s3/*\", toFastifyHandler(s3));\n * ```\n */\nexport function toFastifyHandler(s3: DimahS3HandlerSource) {\n const handler = toNodeHandler(s3);\n return async (req: FastifyRequestLike, reply: FastifyReplyLike) => {\n reply.hijack();\n await handler(req.raw, reply.raw);\n };\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import type { DimahS3HandlerSource } from "./types";
2
+ /** Structural Hono context — avoids a hard dependency on `hono`. */
3
+ type HonoContext = {
4
+ req: {
5
+ raw: Request;
6
+ };
7
+ };
8
+ /**
9
+ * Adapt a dimah-s3 instance to a Hono route handler.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { Hono } from "hono";
14
+ * import { toHonoHandler } from "@dimah-s3/server/hono";
15
+ * import { s3 } from "./s3";
16
+ *
17
+ * const app = new Hono();
18
+ * app.on(["GET", "POST", "DELETE"], "/api/s3/*", toHonoHandler(s3));
19
+ * ```
20
+ */
21
+ export declare function toHonoHandler(s3: DimahS3HandlerSource): (c: HonoContext) => Promise<Response>;
22
+ export {};
23
+ //# sourceMappingURL=hono.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hono.d.ts","sourceRoot":"","sources":["../../src/adapters/hono.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAEpD,oEAAoE;AACpE,KAAK,WAAW,GAAG;IACjB,GAAG,EAAE;QAAE,GAAG,EAAE,OAAO,CAAA;KAAE,CAAC;CACvB,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,oBAAoB,OACzC,WAAW,uBACvB"}
@@ -0,0 +1,8 @@
1
+ // src/adapters/hono.ts
2
+ function toHonoHandler(s3) {
3
+ return (c) => s3.handler(c.req.raw);
4
+ }
5
+
6
+ export { toHonoHandler };
7
+ //# sourceMappingURL=hono.js.map
8
+ //# sourceMappingURL=hono.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/hono.ts"],"names":[],"mappings":";AAoBO,SAAS,cAAc,EAAA,EAA0B;AACtD,EAAA,OAAO,CAAC,CAAA,KAAmB,EAAA,CAAG,OAAA,CAAQ,CAAA,CAAE,IAAI,GAAG,CAAA;AACjD","file":"hono.js","sourcesContent":["import type { DimahS3HandlerSource } from \"./types\";\n\n/** Structural Hono context — avoids a hard dependency on `hono`. */\ntype HonoContext = {\n req: { raw: Request };\n};\n\n/**\n * Adapt a dimah-s3 instance to a Hono route handler.\n *\n * @example\n * ```ts\n * import { Hono } from \"hono\";\n * import { toHonoHandler } from \"@dimah-s3/server/hono\";\n * import { s3 } from \"./s3\";\n *\n * const app = new Hono();\n * app.on([\"GET\", \"POST\", \"DELETE\"], \"/api/s3/*\", toHonoHandler(s3));\n * ```\n */\nexport function toHonoHandler(s3: DimahS3HandlerSource) {\n return (c: HonoContext) => s3.handler(c.req.raw);\n}\n"]}
@@ -1,6 +1,6 @@
1
- import type { DimahS3 } from "../dimah-s3";
1
+ import type { DimahS3HandlerSource } from "./types";
2
2
  /**
3
- * Adapt a {@link DimahS3} instance to Next.js App Router route handlers.
3
+ * Adapt a dimah-s3 instance to Next.js App Router route handlers.
4
4
  *
5
5
  * @example
6
6
  * ```ts
@@ -10,7 +10,7 @@ import type { DimahS3 } from "../dimah-s3";
10
10
  * export const { GET, POST, DELETE } = toNextJsHandler(s3);
11
11
  * ```
12
12
  */
13
- export declare function toNextJsHandler(s3: Pick<DimahS3<Record<string, unknown>>, "handler">): {
13
+ export declare function toNextJsHandler(s3: DimahS3HandlerSource): {
14
14
  GET: (request: Request) => Promise<Response>;
15
15
  POST: (request: Request) => Promise<Response>;
16
16
  DELETE: (request: Request) => Promise<Response>;
@@ -1 +1 @@
1
- {"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../../src/adapters/next.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC;IAGnD,GAAG;IACH,IAAI;IACJ,MAAM;EAET"}
1
+ {"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../../src/adapters/next.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,oBAAoB;IAEpD,GAAG;IACH,IAAI;IACJ,MAAM;EAET"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adapters/next.ts"],"names":[],"mappings":";AAaO,SAAS,gBACd,EAAA,EACA;AACA,EAAA,OAAO;AAAA,IACL,KAAK,EAAA,CAAG,OAAA;AAAA,IACR,MAAM,EAAA,CAAG,OAAA;AAAA,IACT,QAAQ,EAAA,CAAG;AAAA,GACb;AACF","file":"next.js","sourcesContent":["import type { DimahS3 } from \"../dimah-s3\";\n\n/**\n * Adapt a {@link DimahS3} instance to Next.js App Router route handlers.\n *\n * @example\n * ```ts\n * import { toNextJsHandler } from \"@dimah-s3/server/next\";\n * import { s3 } from \"@/lib/s3\";\n *\n * export const { GET, POST, DELETE } = toNextJsHandler(s3);\n * ```\n */\nexport function toNextJsHandler(\n s3: Pick<DimahS3<Record<string, unknown>>, \"handler\">,\n) {\n return {\n GET: s3.handler,\n POST: s3.handler,\n DELETE: s3.handler,\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/adapters/next.ts"],"names":[],"mappings":";AAaO,SAAS,gBAAgB,EAAA,EAA0B;AACxD,EAAA,OAAO;AAAA,IACL,KAAK,EAAA,CAAG,OAAA;AAAA,IACR,MAAM,EAAA,CAAG,OAAA;AAAA,IACT,QAAQ,EAAA,CAAG;AAAA,GACb;AACF","file":"next.js","sourcesContent":["import type { DimahS3HandlerSource } from \"./types\";\n\n/**\n * Adapt a dimah-s3 instance to Next.js App Router route handlers.\n *\n * @example\n * ```ts\n * import { toNextJsHandler } from \"@dimah-s3/server/next\";\n * import { s3 } from \"@/lib/s3\";\n *\n * export const { GET, POST, DELETE } = toNextJsHandler(s3);\n * ```\n */\nexport function toNextJsHandler(s3: DimahS3HandlerSource) {\n return {\n GET: s3.handler,\n POST: s3.handler,\n DELETE: s3.handler,\n };\n}\n"]}
@@ -1,7 +1,19 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
2
- import type { DimahS3 } from "../dimah-s3";
1
+ import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
2
+ import type { DimahS3HandlerSource } from "./types";
3
3
  /**
4
- * Adapt a {@link DimahS3} instance to Node.js `http` / Express-style handlers.
4
+ * Convert Node.js / Express request headers into a Web {@link Headers} object.
5
+ * Useful when calling `s3.api.*` from a Node handler.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { fromNodeHeaders } from "@dimah-s3/server/node";
10
+ *
11
+ * await s3.api.download(key, { headers: fromNodeHeaders(req.headers) });
12
+ * ```
13
+ */
14
+ export declare function fromNodeHeaders(nodeHeaders: IncomingHttpHeaders): Headers;
15
+ /**
16
+ * Adapt a dimah-s3 instance to Node.js `http` / Express-style handlers.
5
17
  *
6
18
  * @example
7
19
  * ```ts
@@ -12,5 +24,5 @@ import type { DimahS3 } from "../dimah-s3";
12
24
  * createServer(toNodeHandler(s3)).listen(3000);
13
25
  * ```
14
26
  */
15
- export declare function toNodeHandler(s3: Pick<DimahS3<Record<string, unknown>>, "handler">): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
27
+ export declare function toNodeHandler(s3: DimahS3HandlerSource): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
16
28
  //# sourceMappingURL=node.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/adapters/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAuD3C;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAC3B,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS,CAAC,SAElC,eAAe,OAAO,cAAc,KAAG,OAAO,CAAC,IAAI,CAAC,CAqBxE"}
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/adapters/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,eAAe,EACf,cAAc,EACf,MAAM,WAAW,CAAC;AAEnB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,mBAAmB,GAAG,OAAO,CAWzE;AA6CD;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,oBAAoB,SACjC,eAAe,OAAO,cAAc,KAAG,OAAO,CAAC,IAAI,CAAC,CAqBxE"}
@@ -48,6 +48,18 @@ var errors = {
48
48
  };
49
49
 
50
50
  // src/adapters/node.ts
51
+ function fromNodeHeaders(nodeHeaders) {
52
+ const headers = new Headers();
53
+ for (const [key, value] of Object.entries(nodeHeaders)) {
54
+ if (value == null) continue;
55
+ if (Array.isArray(value)) {
56
+ for (const v of value) headers.append(key, v);
57
+ } else {
58
+ headers.set(key, value);
59
+ }
60
+ }
61
+ return headers;
62
+ }
51
63
  function collectBody(req) {
52
64
  return new Promise((resolve, reject) => {
53
65
  const chunks = [];
@@ -62,15 +74,7 @@ function toWebRequest(req, body) {
62
74
  const host = req.headers.host ?? "localhost";
63
75
  const protocol = req.socket.encrypted === true ? "https" : "http";
64
76
  const url = new URL(req.url ?? "/", `${protocol}://${host}`);
65
- const headers = new Headers();
66
- for (const [key, value] of Object.entries(req.headers)) {
67
- if (value == null) continue;
68
- if (Array.isArray(value)) {
69
- for (const v of value) headers.append(key, v);
70
- } else {
71
- headers.set(key, value);
72
- }
73
- }
77
+ const headers = fromNodeHeaders(req.headers);
74
78
  const method = (req.method ?? "GET").toUpperCase();
75
79
  const init = { method, headers };
76
80
  if (method !== "GET" && method !== "HEAD") {
@@ -111,6 +115,6 @@ function toNodeHandler(s3) {
111
115
  };
112
116
  }
113
117
 
114
- export { toNodeHandler };
118
+ export { fromNodeHeaders, toNodeHandler };
115
119
  //# sourceMappingURL=node.js.map
116
120
  //# sourceMappingURL=node.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/errors.ts","../../src/adapters/node.ts"],"names":[],"mappings":"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","file":"node.js","sourcesContent":["import { DimahS3Error, S3_ERROR_CODES, type S3ErrorCode } from \"@dimah-s3/core\";\n\nexport { DimahS3Error };\n\n/** English API errors with stable codes for client-side localization. */\nexport const errors = {\n notFound: () =>\n new DimahS3Error(\"Not Found\", 404, { code: S3_ERROR_CODES.NOT_FOUND }),\n\n invalidJson: () =>\n new DimahS3Error(\"Invalid JSON payload\", 400, {\n code: S3_ERROR_CODES.INVALID_JSON,\n }),\n\n forbidden: () =>\n new DimahS3Error(\"Forbidden\", 403, { code: S3_ERROR_CODES.FORBIDDEN }),\n\n internalError: () =>\n new DimahS3Error(\"Internal server error\", 500, {\n code: S3_ERROR_CODES.INTERNAL_ERROR,\n }),\n\n objectNotFound: () =>\n new DimahS3Error(\"Object not found\", 404, {\n code: S3_ERROR_CODES.OBJECT_NOT_FOUND,\n }),\n\n s3NetworkError: (code: string) =>\n new DimahS3Error(\n `S3 endpoint unreachable (${code}): check your endpoint URL and network connectivity`,\n 502,\n { code: S3_ERROR_CODES.S3_NETWORK_ERROR, params: { code } },\n ),\n\n fieldRequired: (name: string) =>\n new DimahS3Error(`${name} is required`, 400, {\n code: S3_ERROR_CODES.FIELD_REQUIRED,\n params: { name },\n }),\n\n /** Missing `key` on query-string routes (download / delete / list-parts). */\n keyRequired: () =>\n new DimahS3Error(\"key query parameter is required\", 400, {\n code: S3_ERROR_CODES.KEY_REQUIRED,\n }),\n\n uploadIdRequired: () =>\n new DimahS3Error(\"uploadId is required\", 400, {\n code: S3_ERROR_CODES.UPLOAD_ID_REQUIRED,\n }),\n\n partNumberInvalid: () =>\n new DimahS3Error(\"partNumber must be a positive integer\", 400, {\n code: S3_ERROR_CODES.PART_NUMBER_INVALID,\n }),\n\n partsRequired: () =>\n new DimahS3Error(\"At least one valid part is required\", 400, {\n code: S3_ERROR_CODES.PARTS_REQUIRED,\n }),\n\n fileSizeRequiredUpload: () =>\n new DimahS3Error(\n \"fileSize is required when upload.requireFileSize is enabled\",\n 400,\n { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_UPLOAD },\n ),\n\n fileSizeRequiredMultipart: () =>\n new DimahS3Error(\n \"fileSize is required when multipart.requireFileSize is enabled\",\n 400,\n { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART },\n ),\n} as const;\n\nexport type ServerErrors = typeof errors;\n\n/** Trim and require a non-empty string; throws {@link errors.fieldRequired}. */\nexport function requireString(value: unknown, name: string): string {\n const trimmed = typeof value === \"string\" ? value.trim() : \"\";\n if (!trimmed) throw errors.fieldRequired(name);\n return trimmed;\n}\n\nfunction statusOf(err: unknown, fallback: number): number {\n return typeof (err as { status?: unknown })?.status === \"number\"\n ? (err as { status: number }).status\n : fallback;\n}\n\n/**\n * Convert hook/S3 throws into {@link DimahS3Error}.\n *\n * - Existing {@link DimahS3Error} instances are preserved (including subclasses).\n * - Plain `Error`s keep their message; `code` is only applied when provided\n * (omit it so clients show the custom English message instead of a mapped\n * library string).\n */\nexport function toDimahS3Error(\n err: unknown,\n fallbackMessage: string,\n fallbackStatus = 403,\n options?: { code?: S3ErrorCode | string },\n): DimahS3Error {\n if (err instanceof DimahS3Error) return err;\n\n const message =\n err instanceof Error && err.message.trim() ? err.message : fallbackMessage;\n const status = statusOf(err, fallbackStatus);\n\n return new DimahS3Error(message, status, {\n cause: err instanceof Error ? err : undefined,\n ...(options?.code !== undefined ? { code: options.code } : {}),\n });\n}\n","import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { DimahS3 } from \"../dimah-s3\";\nimport { errors } from \"../errors\";\n\nfunction collectBody(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer | string) => {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n\nfunction toWebRequest(req: IncomingMessage, body: Buffer): Request {\n const host = req.headers.host ?? \"localhost\";\n const protocol =\n (req.socket as { encrypted?: boolean }).encrypted === true\n ? \"https\"\n : \"http\";\n const url = new URL(req.url ?? \"/\", `${protocol}://${host}`);\n\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const v of value) headers.append(key, v);\n } else {\n headers.set(key, value);\n }\n }\n\n const method = (req.method ?? \"GET\").toUpperCase();\n const init: RequestInit = { method, headers };\n if (method !== \"GET\" && method !== \"HEAD\") {\n init.body = new Uint8Array(body);\n // @ts-expect-error Node fetch duplex for streaming bodies\n init.duplex = \"half\";\n }\n\n return new Request(url, init);\n}\n\nasync function writeWebResponse(\n res: ServerResponse,\n response: Response,\n): Promise<void> {\n res.statusCode = response.status;\n response.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n const buffer = Buffer.from(await response.arrayBuffer());\n res.end(buffer);\n}\n\n/**\n * Adapt a {@link DimahS3} instance to Node.js `http` / Express-style handlers.\n *\n * @example\n * ```ts\n * import { createServer } from \"node:http\";\n * import { toNodeHandler } from \"@dimah-s3/server/node\";\n * import { s3 } from \"./s3\";\n *\n * createServer(toNodeHandler(s3)).listen(3000);\n * ```\n */\nexport function toNodeHandler(\n s3: Pick<DimahS3<Record<string, unknown>>, \"handler\">,\n) {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n try {\n const body = await collectBody(req);\n const request = toWebRequest(req, body);\n const response = await s3.handler(request);\n await writeWebResponse(res, response);\n } catch (err) {\n console.error(\"[S3 API]\", err);\n if (!res.headersSent) {\n const fallback = errors.internalError();\n res.statusCode = fallback.status;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n message: fallback.message,\n code: fallback.code,\n }),\n );\n }\n }\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/errors.ts","../../src/adapters/node.ts"],"names":[],"mappings":";;;AAKO,IAAM,MAAA,GAAS;AAAA,EACpB,QAAA,EAAU,MACR,IAAI,YAAA,CAAa,WAAA,EAAa,KAAK,EAAE,IAAA,EAAM,cAAA,CAAe,SAAA,EAAW,CAAA;AAAA,EAEvE,WAAA,EAAa,MACX,IAAI,YAAA,CAAa,wBAAwB,GAAA,EAAK;AAAA,IAC5C,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,SAAA,EAAW,MACT,IAAI,YAAA,CAAa,WAAA,EAAa,KAAK,EAAE,IAAA,EAAM,cAAA,CAAe,SAAA,EAAW,CAAA;AAAA,EAEvE,aAAA,EAAe,MACb,IAAI,YAAA,CAAa,yBAAyB,GAAA,EAAK;AAAA,IAC7C,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,cAAA,EAAgB,MACd,IAAI,YAAA,CAAa,oBAAoB,GAAA,EAAK;AAAA,IACxC,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,cAAA,EAAgB,CAAC,IAAA,KACf,IAAI,YAAA;AAAA,IACF,4BAA4B,IAAI,CAAA,mDAAA,CAAA;AAAA,IAChC,GAAA;AAAA,IACA,EAAE,IAAA,EAAM,cAAA,CAAe,kBAAkB,MAAA,EAAQ,EAAE,MAAK;AAAE,GAC5D;AAAA,EAEF,aAAA,EAAe,CAAC,IAAA,KACd,IAAI,aAAa,CAAA,EAAG,IAAI,gBAAgB,GAAA,EAAK;AAAA,IAC3C,MAAM,cAAA,CAAe,cAAA;AAAA,IACrB,MAAA,EAAQ,EAAE,IAAA;AAAK,GAChB,CAAA;AAAA;AAAA,EAGH,WAAA,EAAa,MACX,IAAI,YAAA,CAAa,mCAAmC,GAAA,EAAK;AAAA,IACvD,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,gBAAA,EAAkB,MAChB,IAAI,YAAA,CAAa,wBAAwB,GAAA,EAAK;AAAA,IAC5C,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,iBAAA,EAAmB,MACjB,IAAI,YAAA,CAAa,yCAAyC,GAAA,EAAK;AAAA,IAC7D,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,aAAA,EAAe,MACb,IAAI,YAAA,CAAa,uCAAuC,GAAA,EAAK;AAAA,IAC3D,MAAM,cAAA,CAAe;AAAA,GACtB,CAAA;AAAA,EAEH,sBAAA,EAAwB,MACtB,IAAI,YAAA;AAAA,IACF,6DAAA;AAAA,IACA,GAAA;AAAA,IACA,EAAE,IAAA,EAAM,cAAA,CAAe,yBAAA;AAA0B,GACnD;AAAA,EAEF,yBAAA,EAA2B,MACzB,IAAI,YAAA;AAAA,IACF,gEAAA;AAAA,IACA,GAAA;AAAA,IACA,EAAE,IAAA,EAAM,cAAA,CAAe,4BAAA;AAA6B;AAE1D,CAAA;;;ACvDO,SAAS,gBAAgB,WAAA,EAA2C;AACzE,EAAA,MAAM,OAAA,GAAU,IAAI,OAAA,EAAQ;AAC5B,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,WAAW,CAAA,EAAG;AACtD,IAAA,IAAI,SAAS,IAAA,EAAM;AACnB,IAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,MAAA,KAAA,MAAW,CAAA,IAAK,KAAA,EAAO,OAAA,CAAQ,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,IAC9C,CAAA,MAAO;AACL,MAAA,OAAA,CAAQ,GAAA,CAAI,KAAK,KAAK,CAAA;AAAA,IACxB;AAAA,EACF;AACA,EAAA,OAAO,OAAA;AACT;AAEA,SAAS,YAAY,GAAA,EAAuC;AAC1D,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,EAAS,MAAA,KAAW;AACtC,IAAA,MAAM,SAAmB,EAAC;AAC1B,IAAA,GAAA,CAAI,EAAA,CAAG,MAAA,EAAQ,CAAC,KAAA,KAA2B;AACzC,MAAA,MAAA,CAAO,IAAA,CAAK,OAAO,KAAA,KAAU,QAAA,GAAW,OAAO,IAAA,CAAK,KAAK,IAAI,KAAK,CAAA;AAAA,IACpE,CAAC,CAAA;AACD,IAAA,GAAA,CAAI,EAAA,CAAG,OAAO,MAAM,OAAA,CAAQ,OAAO,MAAA,CAAO,MAAM,CAAC,CAAC,CAAA;AAClD,IAAA,GAAA,CAAI,EAAA,CAAG,SAAS,MAAM,CAAA;AAAA,EACxB,CAAC,CAAA;AACH;AAEA,SAAS,YAAA,CAAa,KAAsB,IAAA,EAAuB;AACjE,EAAA,MAAM,IAAA,GAAO,GAAA,CAAI,OAAA,CAAQ,IAAA,IAAQ,WAAA;AACjC,EAAA,MAAM,QAAA,GACH,GAAA,CAAI,MAAA,CAAmC,SAAA,KAAc,OAClD,OAAA,GACA,MAAA;AACN,EAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,GAAA,CAAI,GAAA,IAAO,KAAK,CAAA,EAAG,QAAQ,CAAA,GAAA,EAAM,IAAI,CAAA,CAAE,CAAA;AAE3D,EAAA,MAAM,OAAA,GAAU,eAAA,CAAgB,GAAA,CAAI,OAAO,CAAA;AAC3C,EAAA,MAAM,MAAA,GAAA,CAAU,GAAA,CAAI,MAAA,IAAU,KAAA,EAAO,WAAA,EAAY;AACjD,EAAA,MAAM,IAAA,GAAoB,EAAE,MAAA,EAAQ,OAAA,EAAQ;AAC5C,EAAA,IAAI,MAAA,KAAW,KAAA,IAAS,MAAA,KAAW,MAAA,EAAQ;AACzC,IAAA,IAAA,CAAK,IAAA,GAAO,IAAI,UAAA,CAAW,IAAI,CAAA;AAE/B,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AAEA,EAAA,OAAO,IAAI,OAAA,CAAQ,GAAA,EAAK,IAAI,CAAA;AAC9B;AAEA,eAAe,gBAAA,CACb,KACA,QAAA,EACe;AACf,EAAA,GAAA,CAAI,aAAa,QAAA,CAAS,MAAA;AAC1B,EAAA,QAAA,CAAS,OAAA,CAAQ,OAAA,CAAQ,CAAC,KAAA,EAAO,GAAA,KAAQ;AACvC,IAAA,GAAA,CAAI,SAAA,CAAU,KAAK,KAAK,CAAA;AAAA,EAC1B,CAAC,CAAA;AACD,EAAA,MAAM,SAAS,MAAA,CAAO,IAAA,CAAK,MAAM,QAAA,CAAS,aAAa,CAAA;AACvD,EAAA,GAAA,CAAI,IAAI,MAAM,CAAA;AAChB;AAcO,SAAS,cAAc,EAAA,EAA0B;AACtD,EAAA,OAAO,OAAO,KAAsB,GAAA,KAAuC;AACzE,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,GAAO,MAAM,WAAA,CAAY,GAAG,CAAA;AAClC,MAAA,MAAM,OAAA,GAAU,YAAA,CAAa,GAAA,EAAK,IAAI,CAAA;AACtC,MAAA,MAAM,QAAA,GAAW,MAAM,EAAA,CAAG,OAAA,CAAQ,OAAO,CAAA;AACzC,MAAA,MAAM,gBAAA,CAAiB,KAAK,QAAQ,CAAA;AAAA,IACtC,SAAS,GAAA,EAAK;AACZ,MAAA,OAAA,CAAQ,KAAA,CAAM,YAAY,GAAG,CAAA;AAC7B,MAAA,IAAI,CAAC,IAAI,WAAA,EAAa;AACpB,QAAA,MAAM,QAAA,GAAW,OAAO,aAAA,EAAc;AACtC,QAAA,GAAA,CAAI,aAAa,QAAA,CAAS,MAAA;AAC1B,QAAA,GAAA,CAAI,SAAA,CAAU,gBAAgB,kBAAkB,CAAA;AAChD,QAAA,GAAA,CAAI,GAAA;AAAA,UACF,KAAK,SAAA,CAAU;AAAA,YACb,SAAS,QAAA,CAAS,OAAA;AAAA,YAClB,MAAM,QAAA,CAAS;AAAA,WAChB;AAAA,SACH;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAA;AACF","file":"node.js","sourcesContent":["import { DimahS3Error, S3_ERROR_CODES, type S3ErrorCode } from \"@dimah-s3/core\";\n\nexport { DimahS3Error };\n\n/** English API errors with stable codes for client-side localization. */\nexport const errors = {\n notFound: () =>\n new DimahS3Error(\"Not Found\", 404, { code: S3_ERROR_CODES.NOT_FOUND }),\n\n invalidJson: () =>\n new DimahS3Error(\"Invalid JSON payload\", 400, {\n code: S3_ERROR_CODES.INVALID_JSON,\n }),\n\n forbidden: () =>\n new DimahS3Error(\"Forbidden\", 403, { code: S3_ERROR_CODES.FORBIDDEN }),\n\n internalError: () =>\n new DimahS3Error(\"Internal server error\", 500, {\n code: S3_ERROR_CODES.INTERNAL_ERROR,\n }),\n\n objectNotFound: () =>\n new DimahS3Error(\"Object not found\", 404, {\n code: S3_ERROR_CODES.OBJECT_NOT_FOUND,\n }),\n\n s3NetworkError: (code: string) =>\n new DimahS3Error(\n `S3 endpoint unreachable (${code}): check your endpoint URL and network connectivity`,\n 502,\n { code: S3_ERROR_CODES.S3_NETWORK_ERROR, params: { code } },\n ),\n\n fieldRequired: (name: string) =>\n new DimahS3Error(`${name} is required`, 400, {\n code: S3_ERROR_CODES.FIELD_REQUIRED,\n params: { name },\n }),\n\n /** Missing `key` on query-string routes (download / delete / list-parts). */\n keyRequired: () =>\n new DimahS3Error(\"key query parameter is required\", 400, {\n code: S3_ERROR_CODES.KEY_REQUIRED,\n }),\n\n uploadIdRequired: () =>\n new DimahS3Error(\"uploadId is required\", 400, {\n code: S3_ERROR_CODES.UPLOAD_ID_REQUIRED,\n }),\n\n partNumberInvalid: () =>\n new DimahS3Error(\"partNumber must be a positive integer\", 400, {\n code: S3_ERROR_CODES.PART_NUMBER_INVALID,\n }),\n\n partsRequired: () =>\n new DimahS3Error(\"At least one valid part is required\", 400, {\n code: S3_ERROR_CODES.PARTS_REQUIRED,\n }),\n\n fileSizeRequiredUpload: () =>\n new DimahS3Error(\n \"fileSize is required when upload.requireFileSize is enabled\",\n 400,\n { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_UPLOAD },\n ),\n\n fileSizeRequiredMultipart: () =>\n new DimahS3Error(\n \"fileSize is required when multipart.requireFileSize is enabled\",\n 400,\n { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART },\n ),\n} as const;\n\nexport type ServerErrors = typeof errors;\n\n/** Trim and require a non-empty string; throws {@link errors.fieldRequired}. */\nexport function requireString(value: unknown, name: string): string {\n const trimmed = typeof value === \"string\" ? value.trim() : \"\";\n if (!trimmed) throw errors.fieldRequired(name);\n return trimmed;\n}\n\nfunction statusOf(err: unknown, fallback: number): number {\n return typeof (err as { status?: unknown })?.status === \"number\"\n ? (err as { status: number }).status\n : fallback;\n}\n\n/**\n * Convert hook/S3 throws into {@link DimahS3Error}.\n *\n * - Existing {@link DimahS3Error} instances are preserved (including subclasses).\n * - Plain `Error`s keep their message; `code` is only applied when provided\n * (omit it so clients show the custom English message instead of a mapped\n * library string).\n */\nexport function toDimahS3Error(\n err: unknown,\n fallbackMessage: string,\n fallbackStatus = 403,\n options?: { code?: S3ErrorCode | string },\n): DimahS3Error {\n if (err instanceof DimahS3Error) return err;\n\n const message =\n err instanceof Error && err.message.trim() ? err.message : fallbackMessage;\n const status = statusOf(err, fallbackStatus);\n\n return new DimahS3Error(message, status, {\n cause: err instanceof Error ? err : undefined,\n ...(options?.code !== undefined ? { code: options.code } : {}),\n });\n}\n","import type {\n IncomingHttpHeaders,\n IncomingMessage,\n ServerResponse,\n} from \"node:http\";\nimport { errors } from \"../errors\";\nimport type { DimahS3HandlerSource } from \"./types\";\n\n/**\n * Convert Node.js / Express request headers into a Web {@link Headers} object.\n * Useful when calling `s3.api.*` from a Node handler.\n *\n * @example\n * ```ts\n * import { fromNodeHeaders } from \"@dimah-s3/server/node\";\n *\n * await s3.api.download(key, { headers: fromNodeHeaders(req.headers) });\n * ```\n */\nexport function fromNodeHeaders(nodeHeaders: IncomingHttpHeaders): Headers {\n const headers = new Headers();\n for (const [key, value] of Object.entries(nodeHeaders)) {\n if (value == null) continue;\n if (Array.isArray(value)) {\n for (const v of value) headers.append(key, v);\n } else {\n headers.set(key, value);\n }\n }\n return headers;\n}\n\nfunction collectBody(req: IncomingMessage): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on(\"data\", (chunk: Buffer | string) => {\n chunks.push(typeof chunk === \"string\" ? Buffer.from(chunk) : chunk);\n });\n req.on(\"end\", () => resolve(Buffer.concat(chunks)));\n req.on(\"error\", reject);\n });\n}\n\nfunction toWebRequest(req: IncomingMessage, body: Buffer): Request {\n const host = req.headers.host ?? \"localhost\";\n const protocol =\n (req.socket as { encrypted?: boolean }).encrypted === true\n ? \"https\"\n : \"http\";\n const url = new URL(req.url ?? \"/\", `${protocol}://${host}`);\n\n const headers = fromNodeHeaders(req.headers);\n const method = (req.method ?? \"GET\").toUpperCase();\n const init: RequestInit = { method, headers };\n if (method !== \"GET\" && method !== \"HEAD\") {\n init.body = new Uint8Array(body);\n // @ts-expect-error Node fetch duplex for streaming bodies\n init.duplex = \"half\";\n }\n\n return new Request(url, init);\n}\n\nasync function writeWebResponse(\n res: ServerResponse,\n response: Response,\n): Promise<void> {\n res.statusCode = response.status;\n response.headers.forEach((value, key) => {\n res.setHeader(key, value);\n });\n const buffer = Buffer.from(await response.arrayBuffer());\n res.end(buffer);\n}\n\n/**\n * Adapt a dimah-s3 instance to Node.js `http` / Express-style handlers.\n *\n * @example\n * ```ts\n * import { createServer } from \"node:http\";\n * import { toNodeHandler } from \"@dimah-s3/server/node\";\n * import { s3 } from \"./s3\";\n *\n * createServer(toNodeHandler(s3)).listen(3000);\n * ```\n */\nexport function toNodeHandler(s3: DimahS3HandlerSource) {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n try {\n const body = await collectBody(req);\n const request = toWebRequest(req, body);\n const response = await s3.handler(request);\n await writeWebResponse(res, response);\n } catch (err) {\n console.error(\"[S3 API]\", err);\n if (!res.headersSent) {\n const fallback = errors.internalError();\n res.statusCode = fallback.status;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(\n JSON.stringify({\n message: fallback.message,\n code: fallback.code,\n }),\n );\n }\n }\n };\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import type { DimahS3HandlerSource } from "./types";
2
+ /** Structural SvelteKit event — avoids a hard dependency on `@sveltejs/kit`. */
3
+ type SvelteKitRequestEvent = {
4
+ request: Request;
5
+ };
6
+ /**
7
+ * Adapt a dimah-s3 instance to a SvelteKit request handler.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * // src/routes/api/s3/[...path]/+server.ts
12
+ * import { toSvelteKitHandler } from "@dimah-s3/server/svelte-kit";
13
+ * import { s3 } from "$lib/s3";
14
+ *
15
+ * const handler = toSvelteKitHandler(s3);
16
+ * export const GET = handler;
17
+ * export const POST = handler;
18
+ * export const DELETE = handler;
19
+ * ```
20
+ */
21
+ export declare function toSvelteKitHandler(s3: DimahS3HandlerSource): (event: SvelteKitRequestEvent) => Promise<Response>;
22
+ export {};
23
+ //# sourceMappingURL=svelte-kit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svelte-kit.d.ts","sourceRoot":"","sources":["../../src/adapters/svelte-kit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAEpD,gFAAgF;AAChF,KAAK,qBAAqB,GAAG;IAC3B,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,oBAAoB,WAC1C,qBAAqB,uBACrC"}
@@ -0,0 +1,8 @@
1
+ // src/adapters/svelte-kit.ts
2
+ function toSvelteKitHandler(s3) {
3
+ return (event) => s3.handler(event.request);
4
+ }
5
+
6
+ export { toSvelteKitHandler };
7
+ //# sourceMappingURL=svelte-kit.js.map
8
+ //# sourceMappingURL=svelte-kit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/svelte-kit.ts"],"names":[],"mappings":";AAsBO,SAAS,mBAAmB,EAAA,EAA0B;AAC3D,EAAA,OAAO,CAAC,KAAA,KAAiC,EAAA,CAAG,OAAA,CAAQ,MAAM,OAAO,CAAA;AACnE","file":"svelte-kit.js","sourcesContent":["import type { DimahS3HandlerSource } from \"./types\";\n\n/** Structural SvelteKit event — avoids a hard dependency on `@sveltejs/kit`. */\ntype SvelteKitRequestEvent = {\n request: Request;\n};\n\n/**\n * Adapt a dimah-s3 instance to a SvelteKit request handler.\n *\n * @example\n * ```ts\n * // src/routes/api/s3/[...path]/+server.ts\n * import { toSvelteKitHandler } from \"@dimah-s3/server/svelte-kit\";\n * import { s3 } from \"$lib/s3\";\n *\n * const handler = toSvelteKitHandler(s3);\n * export const GET = handler;\n * export const POST = handler;\n * export const DELETE = handler;\n * ```\n */\nexport function toSvelteKitHandler(s3: DimahS3HandlerSource) {\n return (event: SvelteKitRequestEvent) => s3.handler(event.request);\n}\n"]}
@@ -0,0 +1,4 @@
1
+ import type { DimahS3 } from "../dimah-s3";
2
+ /** Minimal surface adapters need from a dimah-s3 instance. */
3
+ export type DimahS3HandlerSource = Pick<DimahS3<Record<string, unknown>>, "handler">;
4
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAE3C,8DAA8D;AAC9D,MAAM,MAAM,oBAAoB,GAAG,IAAI,CACrC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAChC,SAAS,CACV,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dimah-s3/server",
3
- "version": "0.3.2",
3
+ "version": "0.4.1",
4
4
  "description": "Presigned S3 server — dimahS3() with HTTP handler and direct api for upload, download, delete, and multipart",
5
5
  "keywords": [
6
6
  "s3",
@@ -36,6 +36,26 @@
36
36
  "./node": {
37
37
  "types": "./dist/adapters/node.d.ts",
38
38
  "default": "./dist/adapters/node.js"
39
+ },
40
+ "./express": {
41
+ "types": "./dist/adapters/express.d.ts",
42
+ "default": "./dist/adapters/express.js"
43
+ },
44
+ "./hono": {
45
+ "types": "./dist/adapters/hono.d.ts",
46
+ "default": "./dist/adapters/hono.js"
47
+ },
48
+ "./fastify": {
49
+ "types": "./dist/adapters/fastify.d.ts",
50
+ "default": "./dist/adapters/fastify.js"
51
+ },
52
+ "./elysia": {
53
+ "types": "./dist/adapters/elysia.d.ts",
54
+ "default": "./dist/adapters/elysia.js"
55
+ },
56
+ "./svelte-kit": {
57
+ "types": "./dist/adapters/svelte-kit.d.ts",
58
+ "default": "./dist/adapters/svelte-kit.js"
39
59
  }
40
60
  },
41
61
  "files": [
@@ -44,7 +64,7 @@
44
64
  "dependencies": {
45
65
  "@aws-sdk/s3-presigned-post": "^3.1104.0",
46
66
  "@aws-sdk/s3-request-presigner": "^3.1104.0",
47
- "@dimah-s3/core": "0.3.2"
67
+ "@dimah-s3/core": "0.4.1"
48
68
  },
49
69
  "peerDependencies": {
50
70
  "@aws-sdk/client-s3": ">=3"
@@ -57,8 +77,8 @@
57
77
  "tsup": "^8.5.1",
58
78
  "typescript": "7.0.2",
59
79
  "vitest": "^4.1.10",
60
- "@workspace/typescript-config": "0.0.1",
61
- "@workspace/eslint-config": "0.0.1"
80
+ "@workspace/eslint-config": "0.0.1",
81
+ "@workspace/typescript-config": "0.0.1"
62
82
  },
63
83
  "publishConfig": {
64
84
  "access": "public"