@asterflow/response 1.0.10 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,202 +10,52 @@
10
10
 
11
11
  </div>
12
12
 
13
- > Unified HTTP response adapter system for AsterFlow applications.
13
+ > Type-safe HTTP response builder with status-code helpers, header/cookie management, and binary file responses.
14
14
 
15
15
  ## 📦 Installation
16
16
 
17
17
  ```bash
18
- npm install @asterflow/response
19
- # or
20
18
  bun install @asterflow/response
21
19
  ```
22
20
 
23
- ## 💡 About
21
+ ### Features
24
22
 
25
- @asterflow/response provides a type-safe, unified response system for AsterFlow applications. It standardizes HTTP response handling across different runtimes while maintaining full type safety and providing convenient helper methods for common HTTP status codes.
23
+ - **Typed status codes** `response.status(code)` narrows the accepted body type to what that code expects
24
+ - **Shorthand helpers** — `success`, `created`, `noContent`, `badRequest`, `unauthorized`, `forbidden`, `notFound`, `validationError`, `internalServerError`
25
+ - **JSON responses** — `.json(data)` sends the body and sets `Content-Type: application/json`
26
+ - **Binary/file responses** — `.file(data, contentType?)` sends a `Uint8Array` as raw bytes; without a `contentType`, it's detected from the data's magic bytes (PNG, JPEG, GIF, PDF, BMP, ICO, GZIP, ZIP, WEBP), falling back to `application/octet-stream`
27
+ - **Headers and cookies** — `setHeader`/`setCookie`, each chainable and reflected in the response's type
28
+ - **Runtime conversion** — `toResponse()` for the standard Web `Response`, `toServerResponse(res)` for Node's `http.ServerResponse`
26
29
 
27
- ## Features
30
+ ## How to Use
28
31
 
29
- - **Complete Type Safety:** Full TypeScript support with type inference for responses, status codes, and body types
30
- - **Status Code Helpers:** Built-in methods for common HTTP status codes (200, 201, 400, 404, etc.)
31
- - **Header Management:** Type-safe header manipulation with method chaining
32
- - **Cookie Support:** Integrated cookie management system
33
- - **Content Type Detection:** Automatic content type detection and JSON serialization
34
- - **Multi-Runtime Support:** Compatible with standard Response and Node.js ServerResponse
35
- - **Immutable Design:** Response objects are immutable, promoting safer code patterns
36
- - **Method Chaining:** Fluent API for building complex responses
32
+ ```ts
33
+ import { AsterResponse } from '@asterflow/response'
37
34
 
38
- ## 🚀 Usage
39
-
40
- ### Basic Response
41
-
42
- ```typescript
43
- import { Response } from '@asterflow/response'
44
-
45
- // Simple text response
46
- const response = new Response()
47
- .success({ message: 'Hello World!' })
48
-
49
- // With status code
50
- const response = new Response()
35
+ const response = new AsterResponse()
51
36
  .status(201)
52
- .send({ id: 1, name: 'User' })
53
- ```
54
-
55
- ### Status Code Helpers
56
-
57
- ```typescript
58
- import { Response } from '@asterflow/response'
59
-
60
- // Success responses
61
- const success = new Response().success({ data: 'Success!' })
62
- const created = new Response().created({ id: 1 })
63
- const noContent = new Response().noContent()
64
-
65
- // Error responses
66
- const badRequest = new Response().badRequest({ error: 'Invalid input' })
67
- const unauthorized = new Response().unauthorized({ error: 'Not authenticated' })
68
- const forbidden = new Response().forbidden({ error: 'Access denied' })
69
- const notFound = new Response().notFound({ error: 'Resource not found' })
70
- const zodError = new Response().zodError({ errors: ['Validation failed'] })
71
- ```
72
-
73
- ### Headers and Cookies
74
-
75
- ```typescript
76
- import { Response } from '@asterflow/response'
77
-
78
- const response = new Response()
79
- .success({ message: 'Hello!' })
80
- .setHeader('X-Custom-Header', 'custom-value')
81
- .setHeader('Cache-Control', 'no-cache')
82
- .setCookie('session', 'abc123')
83
- .setCookie('theme', 'dark')
84
-
85
- // Headers and cookies are fully typed
86
- console.log(response.header) // Map with typed entries
87
- console.log(response.cookies) // Map with typed entries
37
+ .setHeader('X-Request-Id', 'abc123')
38
+ .json({ id: 1, name: 'User' })
88
39
  ```
89
40
 
90
- ### JSON Responses
41
+ Sending a file works the same way — pass the bytes, and the content type is sniffed automatically if you don't already know it:
91
42
 
92
- ```typescript
93
- import { Response } from '@asterflow/response'
43
+ ```ts
44
+ import { readFile } from 'fs/promises'
45
+ import { AsterResponse } from '@asterflow/response'
94
46
 
95
- // Automatic JSON serialization
96
- const jsonResponse = new Response()
97
- .json({
98
- users: [
99
- { id: 1, name: 'John' },
100
- { id: 2, name: 'Jane' }
101
- ]
102
- })
103
-
104
- // Content-Type automatically set to application/json
47
+ const data = await readFile('avatar.png')
48
+ const response = new AsterResponse().file(data) // Content-Type: image/png
105
49
  ```
106
50
 
107
- ### Integration with Different Runtimes
108
-
109
- #### Standard Web API
110
-
111
- ```typescript
112
- import { Response } from '@asterflow/response'
113
-
114
- const response = new Response()
115
- .success({ message: 'Hello World!' })
116
-
117
- // Convert to standard Response
118
- const webResponse = response.toResponse()
119
- ```
120
-
121
- #### Node.js HTTP Server
122
-
123
- ```typescript
124
- import { Response } from '@asterflow/response'
125
- import { createServer } from 'http'
126
-
127
- createServer((req, res) => {
128
- const response = new Response()
129
- .success({ message: 'Hello from Node.js!' })
130
-
131
- // Convert to ServerResponse
132
- response.toServerResponse(res)
133
- })
134
- ```
135
-
136
- ### Advanced Type Safety
137
-
138
- ```typescript
139
- import { Response } from '@asterflow/response'
140
-
141
- // Define custom response types
142
- type MyResponders = {
143
- 200: { message: string; data: unknown }
144
- 201: { id: number; message: string }
145
- 400: { error: string; details?: string[] }
146
- 404: { error: string }
147
- }
148
-
149
- const response = new Response<MyResponders>()
150
- .success({ message: 'Success!', data: { id: 1 } }) // Fully typed
151
-
152
- // TypeScript will enforce the correct shape for each status code
153
- ```
154
-
155
- ### Method Chaining
156
-
157
- ```typescript
158
- import { Response } from '@asterflow/response'
159
-
160
- const response = new Response()
161
- .status(201)
162
- .setHeader('Location', '/users/123')
163
- .setHeader('X-Request-ID', 'req-123')
164
- .setCookie('last-action', 'create-user')
165
- .json({
166
- id: 123,
167
- message: 'User created successfully'
168
- })
169
- ```
170
-
171
- ## 🔧 API Reference
172
-
173
- ### Core Methods
174
-
175
- - `status(code)` - Set HTTP status code
176
- - `getStatus()` - Get current status code
177
- - `send(data)` - Send response with data
178
- - `json(data)` - Send JSON response with proper Content-Type
179
-
180
- ### Status Code Helpers
181
-
182
- - `success(data)` - 200 OK
183
- - `created(data)` - 201 Created
184
- - `noContent(data)` - 204 No Content
185
- - `badRequest(data)` - 400 Bad Request
186
- - `unauthorized(data)` - 401 Unauthorized
187
- - `forbidden(data)` - 403 Forbidden
188
- - `notFound(data)` - 404 Not Found
189
- - `zodError(data)` - 422 Unprocessable Entity
190
-
191
- ### Header and Cookie Management
192
-
193
- - `setHeader(name, value)` - Add/update header
194
- - `setCookie(name, value)` - Add/update cookie
195
-
196
- ### Runtime Conversion
197
-
198
- - `toResponse()` - Convert to standard Web API Response
199
- - `toServerResponse(serverRes)` - Write to Node.js ServerResponse
51
+ Convert the finished response for whichever runtime you're on: `response.toResponse()` or `response.toServerResponse(res)`.
200
52
 
201
53
  ## 🔗 Related Packages
202
54
 
203
- - [asterflow](https://www.npmjs.com/package/asterflow) - Core framework
204
- - [@asterflow/router](https://www.npmjs.com/package/@asterflow/router) - Type-safe routing system
205
- - [@asterflow/adapter](https://www.npmjs.com/package/@asterflow/adapter) - HTTP adapters for different runtimes
206
- - [@asterflow/request](https://www.npmjs.com/package/@asterflow/request) - Unified HTTP request system
207
- - [@asterflow/plugin](https://www.npmjs.com/package/@asterflow/plugin) - A modular and typed plugin system
55
+ - Depended on by [@asterflow/adapter](https://www.npmjs.com/package/@asterflow/adapter) builds error responses and passes `AsterResponse` to its runtime adapters
56
+ - Depended on by [@asterflow/multipart](https://www.npmjs.com/package/@asterflow/multipart) returns `AsterResponse` errors for malformed multipart requests
57
+ - Depended on by [asterflow](https://www.npmjs.com/package/asterflow) the core framework's request handler works with `AsterResponse`
208
58
 
209
59
  ## 📄 License
210
60
 
211
- MIT - See [LICENSE](https://github.com/AsterFlow/AsterFlow/blob/main/LICENSE) for more details.
61
+ This project is licensed under the [MIT License](../../LICENSE).
@@ -1,26 +1,53 @@
1
1
  "use strict";
2
- var d = Object.defineProperty;
3
- var i = Object.getOwnPropertyDescriptor;
4
- var h = Object.getOwnPropertyNames;
5
- var c = Object.prototype.hasOwnProperty;
6
- var y = (o, e) => {
2
+ var i = Object.defineProperty;
3
+ var c = Object.getOwnPropertyDescriptor;
4
+ var x = Object.getOwnPropertyNames;
5
+ var m = Object.prototype.hasOwnProperty;
6
+ var u = (s, e) => {
7
7
  for (var t in e)
8
- d(o, t, { get: e[t], enumerable: !0 });
9
- }, p = (o, e, t, n) => {
8
+ i(s, t, { get: e[t], enumerable: !0 });
9
+ }, S = (s, e, t, o) => {
10
10
  if (e && typeof e == "object" || typeof e == "function")
11
- for (let s of h(e))
12
- !c.call(o, s) && s !== t && d(o, s, { get: () => e[s], enumerable: !(n = i(e, s)) || n.enumerable });
13
- return o;
11
+ for (let n of x(e))
12
+ !m.call(s, n) && n !== t && i(s, n, { get: () => e[n], enumerable: !(o = c(e, n)) || o.enumerable });
13
+ return s;
14
14
  };
15
- var S = (o) => p(d({}, "__esModule", { value: !0 }), o);
15
+ var C = (s) => S(i({}, "__esModule", { value: !0 }), s);
16
16
  // packages/response/src/index.ts
17
- var u = {};
18
- y(u, {
19
- AsterResponse: () => r
17
+ var B = {};
18
+ u(B, {
19
+ AsterResponse: () => y,
20
+ sniffContentType: () => d
20
21
  });
21
- module.exports = S(u);
22
+ module.exports = C(B);
23
+ // packages/response/src/utils/sniffContentType.ts
24
+ var R = [
25
+ { mimeType: "image/png", bytes: [137, 80, 78, 71, 13, 10, 26, 10] },
26
+ { mimeType: "image/jpeg", bytes: [255, 216, 255] },
27
+ { mimeType: "image/gif", bytes: [71, 73, 70, 56] },
28
+ { mimeType: "application/pdf", bytes: [37, 80, 68, 70] },
29
+ { mimeType: "image/bmp", bytes: [66, 77] },
30
+ { mimeType: "image/x-icon", bytes: [0, 0, 1, 0] },
31
+ { mimeType: "application/gzip", bytes: [31, 139] },
32
+ { mimeType: "application/zip", bytes: [80, 75, 3, 4] },
33
+ { mimeType: "image/webp", bytes: [87, 69, 66, 80], offset: 8 }
34
+ ], f = [82, 73, 70, 70];
35
+ function h(s, e, t = 0) {
36
+ if (s.length < t + e.length) return !1;
37
+ for (let o = 0; o < e.length; o++)
38
+ if (s[t + o] !== e[o]) return !1;
39
+ return !0;
40
+ }
41
+ function d(s) {
42
+ for (let e of R)
43
+ if (!(e.mimeType === "image/webp" && !h(s, f)) && h(s, e.bytes, e.offset))
44
+ return e.mimeType;
45
+ }
22
46
  // packages/response/src/controllers/Response.ts
23
- var r = class o {
47
+ function p(s) {
48
+ return s instanceof Uint8Array;
49
+ }
50
+ var y = class s {
24
51
  _status;
25
52
  body;
26
53
  context;
@@ -48,6 +75,15 @@ var r = class o {
48
75
  }
49
76
  }, this;
50
77
  }
78
+ file(e, t) {
79
+ return this.send(e), this.context = {
80
+ ...this.context,
81
+ header: {
82
+ ...this.context.header,
83
+ "Content-Type": t ?? d(e) ?? "application/octet-stream"
84
+ }
85
+ }, this;
86
+ }
51
87
  success(e) {
52
88
  return this.status(200), this.send(e), this;
53
89
  }
@@ -96,11 +132,12 @@ var r = class o {
96
132
  toResponse() {
97
133
  let e = new Headers();
98
134
  e.set("Content-Type", "text/plain");
99
- for (let [n, s] of Object.entries(this.context.header)) e.set(n, s);
100
- (typeof this.body == "object" || Array.isArray(this.body)) && e.set("Content-Type", "application/json");
101
- for (let [n, s] of Object.entries(this.context.cookies)) e.append("Set-Cookie", `${n}=${s}`);
102
- let t = e.get("Content-Type") === "application/json" ? JSON.stringify(this.body) : String(this.body);
103
- return new globalThis.Response(t, {
135
+ for (let [n, a] of Object.entries(this.context.header)) e.set(n, a);
136
+ let t = p(this.body);
137
+ t ? "Content-Type" in this.context.header || e.set("Content-Type", "application/octet-stream") : (typeof this.body == "object" || Array.isArray(this.body)) && e.set("Content-Type", "application/json");
138
+ for (let [n, a] of Object.entries(this.context.cookies)) e.append("Set-Cookie", `${n}=${a}`);
139
+ let o = t ? this.body : e.get("Content-Type") === "application/json" ? JSON.stringify(this.body) : String(this.body);
140
+ return new globalThis.Response(o, {
104
141
  status: this._status,
105
142
  headers: e
106
143
  });
@@ -108,16 +145,18 @@ var r = class o {
108
145
  toServerResponse(e) {
109
146
  let t = new Headers();
110
147
  t.set("Content-Type", "text/plain");
111
- for (let [s, a] of Object.entries(this.context.header)) t.set(s, a);
112
- (typeof this.body == "object" || Array.isArray(this.body)) && t.set("Content-Type", "application/json");
113
- for (let [s, a] of Object.entries(this.context.cookies)) t.append("Set-Cookie", `${s}=${a}`);
114
- let n = t.get("Content-Type") === "application/json" ? JSON.stringify(this.body) : String(this.body);
148
+ for (let [a, r] of Object.entries(this.context.header)) t.set(a, r);
149
+ let o = p(this.body);
150
+ o ? "Content-Type" in this.context.header || t.set("Content-Type", "application/octet-stream") : (typeof this.body == "object" || Array.isArray(this.body)) && t.set("Content-Type", "application/json");
151
+ for (let [a, r] of Object.entries(this.context.cookies)) t.append("Set-Cookie", `${a}=${r}`);
152
+ let n = o ? this.body : t.get("Content-Type") === "application/json" ? JSON.stringify(this.body) : String(this.body);
115
153
  e.writeHead(this._status, Object.fromEntries(t)), e.end(n);
116
154
  }
117
155
  static create() {
118
- return new o();
156
+ return new s();
119
157
  }
120
158
  };
121
159
  0 && (module.exports = {
122
- AsterResponse
160
+ AsterResponse,
161
+ sniffContentType
123
162
  });
package/dist/mjs/index.js CHANGED
@@ -1,5 +1,31 @@
1
+ // packages/response/src/utils/sniffContentType.ts
2
+ var p = [
3
+ { mimeType: "image/png", bytes: [137, 80, 78, 71, 13, 10, 26, 10] },
4
+ { mimeType: "image/jpeg", bytes: [255, 216, 255] },
5
+ { mimeType: "image/gif", bytes: [71, 73, 70, 56] },
6
+ { mimeType: "application/pdf", bytes: [37, 80, 68, 70] },
7
+ { mimeType: "image/bmp", bytes: [66, 77] },
8
+ { mimeType: "image/x-icon", bytes: [0, 0, 1, 0] },
9
+ { mimeType: "application/gzip", bytes: [31, 139] },
10
+ { mimeType: "application/zip", bytes: [80, 75, 3, 4] },
11
+ { mimeType: "image/webp", bytes: [87, 69, 66, 80], offset: 8 }
12
+ ], c = [82, 73, 70, 70];
13
+ function i(s, e, t = 0) {
14
+ if (s.length < t + e.length) return !1;
15
+ for (let o = 0; o < e.length; o++)
16
+ if (s[t + o] !== e[o]) return !1;
17
+ return !0;
18
+ }
19
+ function d(s) {
20
+ for (let e of p)
21
+ if (!(e.mimeType === "image/webp" && !i(s, c)) && i(s, e.bytes, e.offset))
22
+ return e.mimeType;
23
+ }
1
24
  // packages/response/src/controllers/Response.ts
2
- var a = class d {
25
+ function y(s) {
26
+ return s instanceof Uint8Array;
27
+ }
28
+ var h = class s {
3
29
  _status;
4
30
  body;
5
31
  context;
@@ -27,6 +53,15 @@ var a = class d {
27
53
  }
28
54
  }, this;
29
55
  }
56
+ file(e, t) {
57
+ return this.send(e), this.context = {
58
+ ...this.context,
59
+ header: {
60
+ ...this.context.header,
61
+ "Content-Type": t ?? d(e) ?? "application/octet-stream"
62
+ }
63
+ }, this;
64
+ }
30
65
  success(e) {
31
66
  return this.status(200), this.send(e), this;
32
67
  }
@@ -75,11 +110,12 @@ var a = class d {
75
110
  toResponse() {
76
111
  let e = new Headers();
77
112
  e.set("Content-Type", "text/plain");
78
- for (let [o, s] of Object.entries(this.context.header)) e.set(o, s);
79
- (typeof this.body == "object" || Array.isArray(this.body)) && e.set("Content-Type", "application/json");
80
- for (let [o, s] of Object.entries(this.context.cookies)) e.append("Set-Cookie", `${o}=${s}`);
81
- let t = e.get("Content-Type") === "application/json" ? JSON.stringify(this.body) : String(this.body);
82
- return new globalThis.Response(t, {
113
+ for (let [a, n] of Object.entries(this.context.header)) e.set(a, n);
114
+ let t = y(this.body);
115
+ t ? "Content-Type" in this.context.header || e.set("Content-Type", "application/octet-stream") : (typeof this.body == "object" || Array.isArray(this.body)) && e.set("Content-Type", "application/json");
116
+ for (let [a, n] of Object.entries(this.context.cookies)) e.append("Set-Cookie", `${a}=${n}`);
117
+ let o = t ? this.body : e.get("Content-Type") === "application/json" ? JSON.stringify(this.body) : String(this.body);
118
+ return new globalThis.Response(o, {
83
119
  status: this._status,
84
120
  headers: e
85
121
  });
@@ -87,16 +123,18 @@ var a = class d {
87
123
  toServerResponse(e) {
88
124
  let t = new Headers();
89
125
  t.set("Content-Type", "text/plain");
90
- for (let [s, n] of Object.entries(this.context.header)) t.set(s, n);
91
- (typeof this.body == "object" || Array.isArray(this.body)) && t.set("Content-Type", "application/json");
92
- for (let [s, n] of Object.entries(this.context.cookies)) t.append("Set-Cookie", `${s}=${n}`);
93
- let o = t.get("Content-Type") === "application/json" ? JSON.stringify(this.body) : String(this.body);
94
- e.writeHead(this._status, Object.fromEntries(t)), e.end(o);
126
+ for (let [n, r] of Object.entries(this.context.header)) t.set(n, r);
127
+ let o = y(this.body);
128
+ o ? "Content-Type" in this.context.header || t.set("Content-Type", "application/octet-stream") : (typeof this.body == "object" || Array.isArray(this.body)) && t.set("Content-Type", "application/json");
129
+ for (let [n, r] of Object.entries(this.context.cookies)) t.append("Set-Cookie", `${n}=${r}`);
130
+ let a = o ? this.body : t.get("Content-Type") === "application/json" ? JSON.stringify(this.body) : String(this.body);
131
+ e.writeHead(this._status, Object.fromEntries(t)), e.end(a);
95
132
  }
96
133
  static create() {
97
- return new d();
134
+ return new s();
98
135
  }
99
136
  };
100
137
  export {
101
- a as AsterResponse
138
+ h as AsterResponse,
139
+ d as sniffContentType
102
140
  };
@@ -16,6 +16,25 @@ export declare class AsterResponse<RawResponder extends Responders = Responders,
16
16
  }>;
17
17
  cookies: Context["cookies"];
18
18
  }>;
19
+ /**
20
+ * Sends a raw `Buffer`/`Uint8Array` body (e.g. a file read from disk).
21
+ * Unlike `send`/`json`, the body is written to the response exactly as
22
+ * given - never `JSON.stringify`'d or `String()`'d - so the client
23
+ * receives just the file's bytes, no wrapping object.
24
+ *
25
+ * `contentType` is optional: pass it when you already know the MIME type
26
+ * (cheapest - skips detection entirely). Omitted, it's detected straight
27
+ * from `data`'s leading magic bytes via `sniffContentType` - O(1)
28
+ * relative to the payload size, it inspects at most ~12 bytes and never
29
+ * scans the buffer. Falls back to `application/octet-stream` only if
30
+ * nothing matches.
31
+ */
32
+ file(data: Uint8Array, contentType?: string): AsterResponse<RawResponder, BodySchema, StatusCode, {
33
+ header: Prettify<Context["header"] & {
34
+ "Content-Type": string;
35
+ }>;
36
+ cookies: Context["cookies"];
37
+ }>;
19
38
  success(data: BodySchema[200]): AsterResponse<RawResponder, BodySchema, 200, Context>;
20
39
  created(data: BodySchema[201]): AsterResponse<RawResponder, BodySchema, 201, Context>;
21
40
  noContent(data: BodySchema[204]): AsterResponse<RawResponder, BodySchema, 204, Context>;
@@ -1,3 +1,4 @@
1
1
  export * from './types/response';
2
2
  export * from './types/utils';
3
3
  export * from './controllers/Response';
4
+ export * from './utils/sniffContentType';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Detects a binary payload's MIME type from its leading magic bytes -
3
+ * O(1) relative to payload size (inspects at most ~12 bytes, never scans
4
+ * the buffer), synchronous, no dependency. Covers the formats an upload
5
+ * endpoint commonly deals with; returns `undefined` when nothing matches so
6
+ * callers can fall back to `application/octet-stream` themselves.
7
+ */
8
+ export declare function sniffContentType(data: Uint8Array): string | undefined;
package/package.json CHANGED
@@ -1,12 +1,27 @@
1
1
  {
2
2
  "name": "@asterflow/response",
3
- "version": "1.0.10",
3
+ "version": "1.1.0",
4
+ "description": "Type-safe HTTP response builder with status-code helpers, header/cookie management, and binary file responses.",
5
+ "keywords": [
6
+ "asterflow",
7
+ "response",
8
+ "http"
9
+ ],
4
10
  "main": "dist/cjs/index.cjs",
5
11
  "module": "dist/mjs/index.js",
6
12
  "types": "dist/types/index.d.ts",
7
13
  "typings": "dist/types/index.d.ts",
8
14
  "type": "module",
9
15
  "license": "MIT",
16
+ "author": "Ashu11-A",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/AsterFlow/AsterFlow.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/AsterFlow/AsterFlow/issues"
23
+ },
24
+ "homepage": "https://github.com/AsterFlow/AsterFlow",
10
25
  "exports": {
11
26
  ".": {
12
27
  "types": "./dist/types/index.d.ts",