te.js 2.1.3 → 2.1.4

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/docs/ammo.md CHANGED
@@ -123,16 +123,36 @@ ammo.fire('Hello, World!');
123
123
  ammo.fire(200, '<h1>Hello</h1>', 'text/html');
124
124
  ```
125
125
 
126
+ **Response structure (default, recommended)**
127
+
128
+ Tejas wraps responses in a consistent envelope so clients always get the same shape. This is **enabled by default** and applies to every `fire()` call (and to `ammo.throw()`, `ammo.unauthorized()`, and other error methods, since they use `fire()` internally):
129
+
130
+ ```javascript
131
+ ammo.fire(200, { id: 1, name: 'Alice' });
132
+ // Wire: { "data": { "id": 1, "name": "Alice" } }
133
+
134
+ ammo.fire(400, 'Email is required');
135
+ // Wire: { "error": "Email is required" }
136
+ ```
137
+
138
+ - **2xx** → body is wrapped as `{ data: <payload> }`
139
+ - **4xx/5xx** → body is wrapped as `{ error: <message> }`
140
+ - **204** and **3xx** (e.g. redirects) → no wrapping
141
+
142
+ You can disable wrapping or customize the keys (`data` / `error`) via the [response config](./configuration.md#response-structure) in `tejas.config.json` or environment variables.
143
+
126
144
  **All signatures:**
127
145
 
128
- | Call | Status | Body | Content-Type |
129
- |------|--------|------|-------------|
146
+ | Call | Status | Wire Body | Content-Type |
147
+ |------|--------|-----------|-------------|
130
148
  | `fire()` | 204 | *(empty)* | — |
131
- | `fire("text")` | 200 | `text` | `text/plain` |
132
- | `fire({ json })` | 200 | JSON string | `application/json` |
133
- | `fire(201)` | 201 | status message | `text/plain` |
134
- | `fire(201, data)` | 201 | `data` | auto-detected |
135
- | `fire(200, html, "text/html")` | 200 | `html` | `text/html` |
149
+ | `fire("text")` | 200 | `{ "data": "text" }` | `application/json` |
150
+ | `fire({ json })` | 200 | `{ "data": { ... } }` | `application/json` |
151
+ | `fire(201)` | 201 | `{ "data": "<status message>" }` | `application/json` |
152
+ | `fire(201, data)` | 201 | `{ "data": <data> }` | `application/json` |
153
+ | `fire(200, html, "text/html")` | 200 | `html` *(no envelope)* | `text/html` |
154
+
155
+ When response structure is disabled, the Body column matches what you pass (no envelope). The table above reflects the default behaviour.
136
156
 
137
157
  After `fire()` is called, the sent data is available as `ammo.dispatchedData`.
138
158
 
@@ -243,8 +263,8 @@ import { Target, TejError } from 'te.js';
243
263
 
244
264
  const users = new Target('/users');
245
265
 
246
- // GET /users - List all
247
- // POST /users - Create new
266
+ // GET /users - List all → { "data": { users, page, limit } }
267
+ // POST /users - Create new → { "data": user }
248
268
  users.register('/', async (ammo) => {
249
269
  if (ammo.GET) {
250
270
  const { page = 1, limit = 10 } = ammo.payload;
@@ -256,7 +276,7 @@ users.register('/', async (ammo) => {
256
276
  const { name, email } = ammo.payload;
257
277
 
258
278
  if (!name || !email) {
259
- throw new TejError(400, 'Name and email are required');
279
+ throw new TejError(400, 'Name and email are required'); // → { "error": "..." }
260
280
  }
261
281
 
262
282
  const user = await createUser({ name, email });
@@ -266,15 +286,15 @@ users.register('/', async (ammo) => {
266
286
  ammo.notAllowed();
267
287
  });
268
288
 
269
- // GET /users/:id - Get one
270
- // PUT /users/:id - Update
271
- // DELETE /users/:id - Delete
289
+ // GET /users/:id - Get one → { "data": user }
290
+ // PUT /users/:id - Update → { "data": user }
291
+ // DELETE /users/:id - Delete → 204 (no body)
272
292
  users.register('/:id', async (ammo) => {
273
293
  const { id } = ammo.payload;
274
294
 
275
295
  if (ammo.GET) {
276
296
  const user = await getUser(id);
277
- if (!user) throw new TejError(404, 'User not found');
297
+ if (!user) throw new TejError(404, 'User not found'); // → { "error": "User not found" }
278
298
  return ammo.fire(user);
279
299
  }
280
300
 
@@ -36,7 +36,7 @@ import Tejas from 'te.js';
36
36
 
37
37
  const app = new Tejas({
38
38
  port: 3000,
39
- log: { http_requests: true }
39
+ log: { http_requests: true },
40
40
  });
41
41
 
42
42
  app.takeoff();
@@ -46,35 +46,45 @@ app.takeoff();
46
46
 
47
47
  ### Core
48
48
 
49
- | Config Key | Env Variable | Type | Default | Description |
50
- |------------|-------------|------|---------|-------------|
51
- | `entry` | `ENTRY` | string | *(auto-resolved)* | Entry file for `tejas fly`. Falls back to `package.json` `main`, then `index.js` / `app.js` / `server.js` |
52
- | `port` | `PORT` | number | `1403` | Server port |
53
- | `dir.targets` | `DIR_TARGETS` | string | `"targets"` | Directory containing `.target.js` files for auto-discovery |
49
+ | Config Key | Env Variable | Type | Default | Description |
50
+ | ------------- | ------------- | ------ | ----------------- | --------------------------------------------------------------------------------------------------------- |
51
+ | `entry` | `ENTRY` | string | _(auto-resolved)_ | Entry file for `tejas fly`. Falls back to `package.json` `main`, then `index.js` / `app.js` / `server.js` |
52
+ | `port` | `PORT` | number | `1403` | Server port |
53
+ | `dir.targets` | `DIR_TARGETS` | string | `"targets"` | Directory containing `.target.js` files for auto-discovery |
54
54
 
55
55
  ### Logging
56
56
 
57
- | Config Key | Env Variable | Type | Default | Description |
58
- |------------|-------------|------|---------|-------------|
57
+ | Config Key | Env Variable | Type | Default | Description |
58
+ | ------------------- | ------------------- | ------- | ------- | ------------------------------------------------------- |
59
59
  | `log.http_requests` | `LOG_HTTP_REQUESTS` | boolean | `false` | Log incoming HTTP requests (method, path, status, time) |
60
- | `log.exceptions` | `LOG_EXCEPTIONS` | boolean | `false` | Log unhandled exceptions and errors |
60
+ | `log.exceptions` | `LOG_EXCEPTIONS` | boolean | `false` | Log unhandled exceptions and errors |
61
+
62
+ ### Response Structure {#response-structure}
63
+
64
+ By default, Tejas wraps all success responses in `{ data: ... }` and all error responses in `{ error: ... }`. This gives clients a consistent envelope. See [Ammo — fire()](./ammo.md#fire----send-response) for examples. Disable or customize via the options below.
65
+
66
+ | Config Key | Env Variable | Type | Default | Description |
67
+ | -------------------------- | --------------------------- | ------- | --------- | ---------------------------------------------------------------------------------------- |
68
+ | `response.envelopeEnabled` | `RESPONSE_ENVELOPE_ENABLED` | boolean | `true` | Enable response envelope: wrap success in `{ data: ... }` and errors in `{ error: ... }` |
69
+ | `response.successKey` | `RESPONSE_SUCCESSKEY` | string | `"data"` | Key used to wrap 2xx response bodies |
70
+ | `response.errorKey` | `RESPONSE_ERRORKEY` | string | `"error"` | Key used to wrap 4xx/5xx response bodies |
61
71
 
62
72
  ### Developer warnings
63
73
 
64
74
  When an endpoint is called and it has no allowed methods defined (see [Routing — Endpoint Metadata](./routing.md#endpoint-metadata)), the framework logs a warning once per path so you can restrict methods for security (405 and `Allow` header). To disable this warning:
65
75
 
66
- | Config Key | Env Variable | Type | Default | Description |
67
- |------------|-------------|------|---------|-------------|
68
- | `warn_missing_allowed_methods` | `WARN_MISSING_ALLOWED_METHODS` | boolean/string | *(warn)* | Set to `false` to disable the runtime warning for endpoints without allowed methods. |
76
+ | Config Key | Env Variable | Type | Default | Description |
77
+ | ------------------------------ | ------------------------------ | -------------- | -------- | ------------------------------------------------------------------------------------ |
78
+ | `warn_missing_allowed_methods` | `WARN_MISSING_ALLOWED_METHODS` | boolean/string | _(warn)_ | Set to `false` to disable the runtime warning for endpoints without allowed methods. |
69
79
 
70
80
  Example: in `tejas.config.json` use `"warn_missing_allowed_methods": false`, or in `.env` use `WARN_MISSING_ALLOWED_METHODS=false`.
71
81
 
72
82
  ### Request Body
73
83
 
74
- | Config Key | Env Variable | Type | Default | Description |
75
- |------------|-------------|------|---------|-------------|
76
- | `body.max_size` | `BODY_MAX_SIZE` | number | `10485760` (10 MB) | Maximum request body size in bytes. Requests exceeding this receive a 413 error |
77
- | `body.timeout` | `BODY_TIMEOUT` | number | `30000` (30 s) | Body parsing timeout in milliseconds. Requests exceeding this receive a 408 error |
84
+ | Config Key | Env Variable | Type | Default | Description |
85
+ | --------------- | --------------- | ------ | ------------------ | --------------------------------------------------------------------------------- |
86
+ | `body.max_size` | `BODY_MAX_SIZE` | number | `10485760` (10 MB) | Maximum request body size in bytes. Requests exceeding this receive a 413 error |
87
+ | `body.timeout` | `BODY_TIMEOUT` | number | `30000` (30 s) | Body parsing timeout in milliseconds. Requests exceeding this receive a 408 error |
78
88
 
79
89
  ### LLM configuration (feature as parent, LLM inside each feature)
80
90
 
@@ -84,31 +94,31 @@ Tejas uses a **feature-as-parent** pattern: each feature that needs an LLM has i
84
94
 
85
95
  These options configure the `tejas generate:docs` CLI command and the auto-documentation system. The **`docs.llm`** block is the LLM configuration for this feature. See [Auto-Documentation](./auto-docs.md) for full details.
86
96
 
87
- | Config Key | Env Variable | Type | Default | Description |
88
- |------------|-------------|------|---------|-------------|
89
- | `docs.dirTargets` | `DOCS_DIR_TARGETS` | string | `"targets"` | Target directory for doc generation (can differ from `dir.targets`) |
90
- | `docs.output` | — | string | `"./openapi.json"` | Output path for the generated OpenAPI spec |
91
- | `docs.title` | — | string | `"API"` | API title in the OpenAPI `info` block |
92
- | `docs.version` | — | string | `"1.0.0"` | API version in the OpenAPI `info` block |
93
- | `docs.description` | — | string | `""` | API description |
94
- | `docs.level` | — | number | `1` | LLM enhancement level (1–3). Higher = better docs, more tokens |
95
- | `docs.llm.baseURL` | `DOCS_LLM_BASE_URL` or `LLM_BASE_URL` | string | `"https://api.openai.com/v1"` | LLM provider endpoint for auto-docs |
96
- | `docs.llm.apiKey` | `DOCS_LLM_API_KEY` or `LLM_API_KEY` | string | — | LLM provider API key for auto-docs |
97
- | `docs.llm.model` | `DOCS_LLM_MODEL` or `LLM_MODEL` | string | `"gpt-4o-mini"` | LLM model for auto-docs |
98
- | `docs.overviewPath` | — | string | `"./API_OVERVIEW.md"` | Path for the generated overview page (level 3 only) |
99
- | `docs.productionBranch` | `DOCS_PRODUCTION_BRANCH` | string | `"main"` | Git branch that triggers `docs:on-push` |
97
+ | Config Key | Env Variable | Type | Default | Description |
98
+ | ----------------------- | ------------------------------------- | ------ | ----------------------------- | ------------------------------------------------------------------- |
99
+ | `docs.dirTargets` | `DOCS_DIR_TARGETS` | string | `"targets"` | Target directory for doc generation (can differ from `dir.targets`) |
100
+ | `docs.output` | — | string | `"./openapi.json"` | Output path for the generated OpenAPI spec |
101
+ | `docs.title` | — | string | `"API"` | API title in the OpenAPI `info` block |
102
+ | `docs.version` | — | string | `"1.0.0"` | API version in the OpenAPI `info` block |
103
+ | `docs.description` | — | string | `""` | API description |
104
+ | `docs.level` | — | number | `1` | LLM enhancement level (1–3). Higher = better docs, more tokens |
105
+ | `docs.llm.baseURL` | `DOCS_LLM_BASE_URL` or `LLM_BASE_URL` | string | `"https://api.openai.com/v1"` | LLM provider endpoint for auto-docs |
106
+ | `docs.llm.apiKey` | `DOCS_LLM_API_KEY` or `LLM_API_KEY` | string | — | LLM provider API key for auto-docs |
107
+ | `docs.llm.model` | `DOCS_LLM_MODEL` or `LLM_MODEL` | string | `"gpt-4o-mini"` | LLM model for auto-docs |
108
+ | `docs.overviewPath` | — | string | `"./API_OVERVIEW.md"` | Path for the generated overview page (level 3 only) |
109
+ | `docs.productionBranch` | `DOCS_PRODUCTION_BRANCH` | string | `"main"` | Git branch that triggers `docs:on-push` |
100
110
 
101
111
  ### Error handling (LLM-inferred errors)
102
112
 
103
113
  When [LLM-inferred error codes and messages](./error-handling.md#llm-inferred-errors) are enabled, the **`errors.llm`** block configures the LLM used for inferring status code and message when you call `ammo.throw()` without explicit code or message. Unset values fall back to `LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL`. You can also enable (and optionally set connection options) by calling **`app.withLLMErrors(config?)`** before `takeoff()` — e.g. `app.withLLMErrors()` to use env/config for baseURL, apiKey, model, or `app.withLLMErrors({ baseURL, apiKey, model, messageType })` to override in code.
104
114
 
105
- | Config Key | Env Variable | Type | Default | Description |
106
- |------------|-------------|------|---------|-------------|
107
- | `errors.llm.enabled` | `ERRORS_LLM_ENABLED` or `LLM_*` (for connection) | boolean | `false` | Enable LLM-inferred error code and message for `ammo.throw()` |
108
- | `errors.llm.baseURL` | `ERRORS_LLM_BASE_URL` or `LLM_BASE_URL` | string | `"https://api.openai.com/v1"` | LLM provider endpoint for error inference |
109
- | `errors.llm.apiKey` | `ERRORS_LLM_API_KEY` or `LLM_API_KEY` | string | — | LLM provider API key for error inference |
110
- | `errors.llm.model` | `ERRORS_LLM_MODEL` or `LLM_MODEL` | string | `"gpt-4o-mini"` | LLM model for error inference |
111
- | `errors.llm.messageType` | `ERRORS_LLM_MESSAGE_TYPE` or `LLM_MESSAGE_TYPE` | `"endUser"` \| `"developer"` | `"endUser"` | Default tone for LLM-generated message: `endUser` (safe for clients) or `developer` (technical detail). Overridable per `ammo.throw()` call. |
115
+ | Config Key | Env Variable | Type | Default | Description |
116
+ | ------------------------ | ------------------------------------------------ | ---------------------------- | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
117
+ | `errors.llm.enabled` | `ERRORS_LLM_ENABLED` or `LLM_*` (for connection) | boolean | `false` | Enable LLM-inferred error code and message for `ammo.throw()` |
118
+ | `errors.llm.baseURL` | `ERRORS_LLM_BASE_URL` or `LLM_BASE_URL` | string | `"https://api.openai.com/v1"` | LLM provider endpoint for error inference |
119
+ | `errors.llm.apiKey` | `ERRORS_LLM_API_KEY` or `LLM_API_KEY` | string | — | LLM provider API key for error inference |
120
+ | `errors.llm.model` | `ERRORS_LLM_MODEL` or `LLM_MODEL` | string | `"gpt-4o-mini"` | LLM model for error inference |
121
+ | `errors.llm.messageType` | `ERRORS_LLM_MESSAGE_TYPE` or `LLM_MESSAGE_TYPE` | `"endUser"` \| `"developer"` | `"endUser"` | Default tone for LLM-generated message: `endUser` (safe for clients) or `developer` (technical detail). Overridable per `ammo.throw()` call. |
112
122
 
113
123
  When enabled, the same behaviour applies whether you call `ammo.throw()` or the framework calls it when it catches an error — one mechanism, no separate config.
114
124
 
@@ -127,6 +137,11 @@ Create a `tejas.config.json` in your project root:
127
137
  "http_requests": true,
128
138
  "exceptions": true
129
139
  },
140
+ "response": {
141
+ "envelopeEnabled": true,
142
+ "successKey": "data",
143
+ "errorKey": "error"
144
+ },
130
145
  "body": {
131
146
  "max_size": 5242880,
132
147
  "timeout": 15000
@@ -165,6 +180,11 @@ PORT=3000
165
180
  LOG_HTTP_REQUESTS=true
166
181
  LOG_EXCEPTIONS=true
167
182
 
183
+ # Response envelope (default: enabled; 2xx → { data }, 4xx/5xx → { error })
184
+ # RESPONSE_ENVELOPE_ENABLED=true
185
+ # RESPONSE_SUCCESSKEY=data
186
+ # RESPONSE_ERRORKEY=error
187
+
168
188
  # Body limits
169
189
  BODY_MAX_SIZE=5242880
170
190
  BODY_TIMEOUT=15000
@@ -204,12 +224,12 @@ const app = new Tejas({
204
224
  port: 3000,
205
225
  log: {
206
226
  http_requests: true,
207
- exceptions: true
227
+ exceptions: true,
208
228
  },
209
229
  body: {
210
230
  max_size: 10 * 1024 * 1024,
211
- timeout: 30000
212
- }
231
+ timeout: 30000,
232
+ },
213
233
  });
214
234
  ```
215
235
 
@@ -239,7 +259,7 @@ import { env } from 'tej-env';
239
259
  target.register('/info', (ammo) => {
240
260
  ammo.fire({
241
261
  port: env('PORT'),
242
- maxBodySize: env('BODY_MAX_SIZE')
262
+ maxBodySize: env('BODY_MAX_SIZE'),
243
263
  });
244
264
  });
245
265
  ```
@@ -274,7 +294,7 @@ Database connections are configured via `takeoff()` options, not through the con
274
294
  ```javascript
275
295
  app.takeoff({
276
296
  withRedis: { url: 'redis://localhost:6379' },
277
- withMongo: { uri: 'mongodb://localhost:27017/myapp' }
297
+ withMongo: { uri: 'mongodb://localhost:27017/myapp' },
278
298
  });
279
299
  ```
280
300
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "te.js",
3
- "version": "2.1.3",
3
+ "version": "2.1.4",
4
4
  "description": "AI Native Node.js Framework",
5
5
  "type": "module",
6
6
  "main": "te.js",
@@ -1,4 +1,5 @@
1
1
  import status from 'statuses';
2
+ import { getResponseConfig } from '../../utils/response-config.js';
2
3
 
3
4
  const formattedData = (data) => {
4
5
  if (data === null || data === undefined) return '';
@@ -17,64 +18,72 @@ const formattedData = (data) => {
17
18
  return String(data);
18
19
  };
19
20
 
21
+ /**
22
+ * Apply response structure envelope when enabled.
23
+ * 2xx → { [successKey]: data }; 4xx/5xx → { [errorKey]: data }; 204 and when disabled → pass through.
24
+ * @param {number} statusCode
25
+ * @param {unknown} data
26
+ * @returns {unknown}
27
+ */
28
+ const applyResponseStructure = (statusCode, data) => {
29
+ const { enabled, successKey, errorKey } = getResponseConfig();
30
+ if (!enabled) return data;
31
+ if (statusCode === 204) return data;
32
+ if (statusCode >= 200 && statusCode < 300) {
33
+ return { [successKey]: data };
34
+ }
35
+ if (statusCode >= 400) {
36
+ return { [errorKey]: data };
37
+ }
38
+ return data;
39
+ };
40
+
20
41
  const statusAndData = (args) => {
42
+ let statusCode;
43
+ let rawData;
44
+ let customContentType = null;
45
+
21
46
  // Handle no arguments
22
47
  if (!args || args.length === 0) {
23
- return {
24
- statusCode: 204,
25
- data: status(204),
26
- contentType: 'text/plain',
27
- };
28
- }
29
-
30
- // Handle single argument
31
- if (args.length === 1) {
48
+ statusCode = 204;
49
+ rawData = status(204);
50
+ } else if (args.length === 1) {
32
51
  const arg = args[0];
33
52
 
34
53
  // If it's a number, treat as status code
35
54
  if (typeof arg === 'number') {
36
- return {
37
- statusCode: arg,
38
- data: status(arg) || String(arg),
39
- contentType: 'text/plain',
40
- };
55
+ statusCode = arg;
56
+ rawData = status(arg) || String(arg);
57
+ } else {
58
+ // Otherwise treat as data
59
+ statusCode = 200;
60
+ rawData = arg;
41
61
  }
42
-
43
- // Otherwise treat as data
44
- return {
45
- statusCode: 200,
46
- data: formattedData(arg),
47
- contentType: contentType(arg),
48
- };
49
- }
50
-
51
- // Handle multiple arguments
52
- let statusCode = 200;
53
- let data = args[0];
54
-
55
- // If first argument is a number, treat as status code
56
- if (typeof args[0] === 'number') {
57
- statusCode = args[0];
58
- data = args[1];
59
62
  } else {
60
- // If first argument is not a number, check if second is
61
- if (typeof args[1] === 'number') {
63
+ // Handle multiple arguments
64
+ statusCode = 200;
65
+ rawData = args[0];
66
+
67
+ if (typeof args[0] === 'number') {
68
+ statusCode = args[0];
69
+ rawData = args[1];
70
+ } else if (typeof args[1] === 'number') {
62
71
  statusCode = args[1];
63
72
  }
64
- }
65
73
 
66
- // If data is undefined, use status message
67
- if (data === undefined) {
68
- data = status[statusCode] || String(statusCode);
74
+ if (rawData === undefined) {
75
+ rawData = status[statusCode] || String(statusCode);
76
+ }
77
+
78
+ customContentType = args.length > 2 ? args[2] : null;
69
79
  }
70
80
 
71
- // If third argument is provided, it's the content type
72
- const customContentType = args.length > 2 ? args[2] : null;
81
+ const wrapped = applyResponseStructure(statusCode, rawData);
73
82
 
74
83
  return {
75
84
  statusCode,
76
- data: formattedData(data),
77
- contentType: customContentType || contentType(data),
85
+ data: formattedData(wrapped),
86
+ contentType: customContentType || contentType(wrapped),
78
87
  };
79
88
  };
80
89
 
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Resolve response structure configuration.
3
+ * Uses RESPONSE_* env vars (populated from tejas.config.json response section).
4
+ */
5
+
6
+ import { env } from 'tej-env';
7
+
8
+ /**
9
+ * Resolve response config from env.
10
+ * @returns {{ enabled: boolean, successKey: string, errorKey: string }}
11
+ */
12
+ export function getResponseConfig() {
13
+ // Response envelope: from .env use RESPONSE_ENVELOPE_ENABLED; from config file → RESPONSE_ENVELOPEENABLED; legacy names supported
14
+ const enabledRaw =
15
+ env('RESPONSE_ENVELOPE_ENABLED') ??
16
+ env('RESPONSE_ENVELOPEENABLED') ??
17
+ env('RESPONSE_FORMAT_ENABLED') ??
18
+ env('RESPONSE_FORMATENABLED') ??
19
+ env('RESPONSE_ENABLED') ??
20
+ '';
21
+ const enabled =
22
+ enabledRaw === true ||
23
+ enabledRaw === 'true' ||
24
+ enabledRaw === '1' ||
25
+ enabledRaw === 1;
26
+
27
+ const successKey =
28
+ env('RESPONSE_SUCCESSKEY') ?? env('RESPONSE_SUCCESS_KEY') ?? 'data';
29
+ const errorKey =
30
+ env('RESPONSE_ERRORKEY') ?? env('RESPONSE_ERROR_KEY') ?? 'error';
31
+
32
+ return {
33
+ enabled:
34
+ enabledRaw === undefined || enabledRaw === '' ? true : Boolean(enabled),
35
+ successKey: String(successKey ?? 'data').trim() || 'data',
36
+ errorKey: String(errorKey ?? 'error').trim() || 'error',
37
+ };
38
+ }