unnbound-events 3.0.0-beta.1 → 3.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,6 +86,8 @@ Return `undefined`, `null`, or omit properties to default to `204 No Content`.
86
86
 
87
87
  `c.request.body` used to vary by `content-type`. It is now always an `ArrayBuffer` of the exact request-content bytes after HTTP transport framing.
88
88
 
89
+ Queued (SQS) messages replay through the same router, so the table below applies to queue handlers too — migrate them alongside your HTTP handlers. The bytes differ only for queued JSON, which is re-serialized through the queue envelope and so arrives as equivalent JSON rather than the publisher's original bytes; every other queued content type is byte-exact.
90
+
89
91
  | `content-type` | 2.0.x | Current version |
90
92
  | --- | --- | --- |
91
93
  | `application/json` | parsed object | `ArrayBuffer` |
@@ -96,9 +98,10 @@ Return `undefined`, `null`, or omit properties to default to `204 No Content`.
96
98
 
97
99
  Handlers for the first four must decode explicitly — see [Handlers](#handlers) for the per-type recipes. Handlers that already received bytes (XML, EDI, CSV, binary) are unaffected.
98
100
 
99
- Two things to check when upgrading an existing workflow:
101
+ Three things to check when upgrading an existing workflow:
100
102
 
101
103
  - A malformed JSON body used to arrive as `undefined`. It now arrives as raw bytes and `JSON.parse` throws — handle it where you decode.
104
+ - A missing body used to arrive as `undefined`, so `if (!c.request.body)` guards fired. Over HTTP it is now a zero-length `ArrayBuffer`, which is truthy, and those guards silently stop firing. Check `c.request.body.byteLength` instead. Queued delivery is the exception: a bodyless async request replays as the four bytes `null`, because the queue envelope stores `null` and it is re-serialized on the way in.
102
105
  - `tsc` catches direct property access on the body, but not a body passed straight to a logger or serializer. Those compile clean and emit `{}` at runtime. Audit every `server.post` / `server.put` handler before deploying.
103
106
 
104
107
  ## Middleware
@@ -7,12 +7,7 @@ export interface S3Pointer {
7
7
  }
8
8
  export type S3PointerMessage = ['software.amazon.payloadoffloading.PayloadS3Pointer', S3Pointer];
9
9
  type SQSClientConfig = ConstructorParameters<typeof SQSClient>[0];
10
- /**
11
- * Internal SQS envelope before it re-enters the HTTP router. Inline bodies
12
- * remain decoded JSON values until QueueAdapter serializes them into request
13
- * bytes, so this must not claim the public IncomingRequest body contract.
14
- * This distinction changes only the type, not SQS delivery behavior.
15
- */
10
+ /** Queue envelopes hold decoded values until replay, so body is not yet an ArrayBuffer. */
16
11
  interface SqsEnvelopeRequest extends Omit<IncomingRequest, 'body'> {
17
12
  body: unknown;
18
13
  }
@@ -2,7 +2,6 @@ import type { Context } from 'hono';
2
2
  import type { EventHandlerResult } from '../event';
3
3
  import { type MiddlewareHandler } from './middleware';
4
4
  import type { EventEnvironment, EventVariables, MaybePromise } from './types';
5
- export { attachTraceHeaders, respondWith } from './response';
6
5
  export type Handler<I, R = EventHandlerResult> = (input: I) => MaybePromise<R>;
7
6
  export interface Endpoint<I> {
8
7
  (path: string, handler: Handler<I>): MaybePromise<void>;
@@ -1,13 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.patchEndpoint = exports.parseEndpointArguments = exports.buildEndpointArguments = exports.respondWith = exports.attachTraceHeaders = void 0;
3
+ exports.patchEndpoint = exports.parseEndpointArguments = exports.buildEndpointArguments = void 0;
4
4
  const unnbound_logger_sdk_1 = require("unnbound-logger-sdk");
5
5
  const utils_1 = require("../utils");
6
6
  const middleware_1 = require("./middleware");
7
7
  const response_1 = require("./response");
8
- var response_2 = require("./response");
9
- Object.defineProperty(exports, "attachTraceHeaders", { enumerable: true, get: function () { return response_2.attachTraceHeaders; } });
10
- Object.defineProperty(exports, "respondWith", { enumerable: true, get: function () { return response_2.respondWith; } });
11
8
  const buildEndpointArguments = (...args) => {
12
9
  const { path, middlewares, handler } = (0, exports.parseEndpointArguments)(...args);
13
10
  const patchedHandler = (0, exports.patchEndpoint)(handler);
@@ -27,7 +24,7 @@ const patchEndpoint = (handler) => {
27
24
  state.request.params = { ...state.request.params, ...context.req.param() };
28
25
  const result = await (0, unnbound_logger_sdk_1.startSpan)('Event request', async () => {
29
26
  const result = await handler(state);
30
- return { status: result?.status ?? 204, headers: result?.headers, body: result?.body };
27
+ return { status: (0, response_1.resolveStatus)(result), headers: result?.headers, body: result?.body };
31
28
  }, (o) => (0, utils_1.buildIncomingPayload)(state, o));
32
29
  return (0, response_1.respondWith)(context, result);
33
30
  };
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildMiddlewareArguments = exports.parseMiddlewareArguments = exports.patchMiddleware = void 0;
4
+ const unnbound_logger_sdk_1 = require("unnbound-logger-sdk");
5
+ const utils_1 = require("../utils");
4
6
  const response_1 = require("./response");
5
7
  const patchMiddleware = (handler) => {
6
8
  return async (context, next) => {
@@ -12,11 +14,10 @@ const patchMiddleware = (handler) => {
12
14
  // response contract as endpoint handlers.
13
15
  if (result == null)
14
16
  return;
15
- return (0, response_1.respondWith)(context, {
16
- status: result.status ?? 204,
17
- headers: result.headers,
18
- body: result.body,
19
- });
17
+ // Short-circuiting skips patchEndpoint, so nothing else emits the request
18
+ // span. Without this, denied requests are absent from workflow traces.
19
+ const traced = await (0, unnbound_logger_sdk_1.startSpan)('Event request', async () => ({ status: (0, response_1.resolveStatus)(result), headers: result.headers, body: result.body }), (o) => (0, utils_1.buildIncomingPayload)(state, o));
20
+ return (0, response_1.respondWith)(context, traced);
20
21
  };
21
22
  };
22
23
  exports.patchMiddleware = patchMiddleware;
@@ -1,4 +1,6 @@
1
1
  import type { Context } from 'hono';
2
- import type { StrictEventHandlerResult } from '../event';
2
+ import type { EventHandlerResult, StrictEventHandlerResult } from '../event';
3
+ export declare const isContentlessStatus: (status: number) => boolean;
4
+ export declare const resolveStatus: (result?: EventHandlerResult) => import("hono/utils/http-status").StatusCode;
3
5
  export declare const respondWith: (context: Context, result: StrictEventHandlerResult) => (Response & import("hono").TypedResponse<never, -1 | 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511, "json">) | (Response & import("hono").TypedResponse<null, -1 | 100 | 101 | 102 | 103 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511, "body">) | (Response & import("hono").TypedResponse<ArrayBuffer, -1 | 100 | 102 | 103 | 200 | 201 | 202 | 203 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 305 | 306 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511, "body">);
4
6
  export declare const attachTraceHeaders: (headers?: Record<string, string>) => Record<string, string> | undefined;
@@ -1,11 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.attachTraceHeaders = exports.respondWith = void 0;
3
+ exports.attachTraceHeaders = exports.respondWith = exports.resolveStatus = exports.isContentlessStatus = void 0;
4
4
  const unnbound_logger_sdk_1 = require("unnbound-logger-sdk");
5
+ // Response rejects bodies on these statuses; dropping one prevents a 500.
6
+ const CONTENTLESS_STATUSES = new Set([204, 205, 304]);
7
+ const isContentlessStatus = (status) => CONTENTLESS_STATUSES.has(status);
8
+ exports.isContentlessStatus = isContentlessStatus;
9
+ const resolveStatus = (result) => result?.status ?? (result?.body ? 200 : 204);
10
+ exports.resolveStatus = resolveStatus;
5
11
  const respondWith = (context, result) => {
6
12
  const headers = (0, exports.attachTraceHeaders)(result.headers);
7
- if (!result.body)
13
+ if (!result.body || CONTENTLESS_STATUSES.has(result.status)) {
8
14
  return context.body(null, result.status, headers);
15
+ }
9
16
  // Signed protocol responses such as AS2 MDNs must remain byte-exact.
10
17
  if (result.body instanceof Uint8Array || result.body instanceof ArrayBuffer) {
11
18
  const bytes = result.body instanceof ArrayBuffer ? result.body : Uint8Array.from(result.body).buffer;
package/dist/lib/utils.js CHANGED
@@ -6,6 +6,7 @@ const trace_1 = require("unnbound-logger-sdk/dist/trace");
6
6
  const types_1 = require("unnbound-logger-sdk/dist/types");
7
7
  const utils_1 = require("unnbound-logger-sdk/dist/utils");
8
8
  const error_1 = require("./error");
9
+ const response_1 = require("./routing/response");
9
10
  const normalizeHeaders = (headers) => {
10
11
  return Object.fromEntries(Object.entries(headers).map(([key, value]) => [
11
12
  key,
@@ -115,16 +116,18 @@ const buildResponsePayload = (result) => {
115
116
  const declaredHeaders = result?.headers
116
117
  ? Object.fromEntries(Object.entries((0, exports.normalizeHeaders)(result.headers)).map(([name, value]) => [name.toLowerCase(), value]))
117
118
  : undefined;
118
- // Byte responses go out with exactly the declared headers (no content-type
119
- // unless the handler set one); everything else is JSON-serialized on the
120
- // wire. The trace mirrors that, and the declared content-type drives the
121
- // display decode so byte responses labeled JSON/XML/text trace readably.
122
- const isByteBody = result?.body instanceof Uint8Array || result?.body instanceof ArrayBuffer;
119
+ // Binary responses keep declared headers; JSON gets the SDK default.
120
+ // The declared content type also controls trace decoding.
123
121
  const traceHeaders = headersForTrace(declaredHeaders ?? {});
122
+ const status = (0, response_1.resolveStatus)(result);
123
+ // Mirror the wire: respondWith drops the body on contentless statuses, so the
124
+ // trace must not claim one — nor a content type for a body never sent.
125
+ const body = (0, response_1.isContentlessStatus)(status) ? undefined : result?.body;
126
+ const isByteBody = body instanceof Uint8Array || body instanceof ArrayBuffer;
124
127
  return {
125
- status: result?.status ?? 204,
126
- headers: isByteBody ? traceHeaders : { 'content-type': 'application/json; charset=utf-8', ...traceHeaders },
127
- body: result?.body ? describeBody(result.body, declaredHeaders?.['content-type']) : undefined,
128
+ status,
129
+ headers: body && !isByteBody ? { 'content-type': 'application/json; charset=utf-8', ...traceHeaders } : traceHeaders,
130
+ body: body ? describeBody(body, declaredHeaders?.['content-type']) : undefined,
128
131
  };
129
132
  };
130
133
  const buildIncomingPayload = (event, result) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "unnbound-events",
3
3
  "description": "Unified events SDK to handle HTTP routes and queued messages with a single routing API.",
4
- "version": "3.0.0-beta.1",
4
+ "version": "3.0.0-beta.2",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {