unnbound-events 2.0.19 → 2.0.21-beta.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
@@ -30,7 +30,7 @@ server.start();
30
30
  The server automatically:
31
31
 
32
32
  - Starts HTTP server on `PORT` (default: `3000`)
33
- - Starts SQS long-polling if `UNNBOUND_SQS_URL` is set
33
+ - Starts SQS long-polling if `UNNBOUND_SQS_URL` is set. Set `queue.enabled: true` to require queue polling and fail fast when the queue URL is missing.
34
34
  - Provides `/healthcheck` endpoint (returns `200 { status: 'ok' }`)
35
35
  - Handles graceful shutdown on `SIGINT`/`SIGTERM`
36
36
 
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- export type { IncomingEvent, IncomingRequest, EventMetadata, EventSource } from './lib/event';
1
+ export { createEnqueuer } from './lib/enqueue';
2
2
  export type { ServerErrorOptions } from './lib/error';
3
- export type { EventServerOptions, EventServer } from './lib/server';
3
+ export { ServerError } from './lib/error';
4
+ export type { EventMetadata, EventSource, IncomingEvent, IncomingRequest } from './lib/event';
4
5
  export type { Handler } from './lib/routing/endpoint';
5
6
  export type { MiddlewareHandler } from './lib/routing/middleware';
6
7
  export type { EventVariables, MaybePromise } from './lib/routing/types';
8
+ export type { EventServer, EventServerOptions } from './lib/server';
7
9
  export { createServer } from './lib/server';
8
- export { ServerError } from './lib/error';
9
- export { createEnqueuer } from './lib/enqueue';
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createEnqueuer = exports.ServerError = exports.createServer = void 0;
4
- var server_1 = require("./lib/server");
5
- Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_1.createServer; } });
6
- var error_1 = require("./lib/error");
7
- Object.defineProperty(exports, "ServerError", { enumerable: true, get: function () { return error_1.ServerError; } });
3
+ exports.createServer = exports.ServerError = exports.createEnqueuer = void 0;
8
4
  var enqueue_1 = require("./lib/enqueue");
9
5
  Object.defineProperty(exports, "createEnqueuer", { enumerable: true, get: function () { return enqueue_1.createEnqueuer; } });
6
+ var error_1 = require("./lib/error");
7
+ Object.defineProperty(exports, "ServerError", { enumerable: true, get: function () { return error_1.ServerError; } });
8
+ var server_1 = require("./lib/server");
9
+ Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_1.createServer; } });
@@ -19,26 +19,26 @@ const types_1 = require("unnbound-logger-sdk/dist/types");
19
19
  * - Custom domain: proxy.domain.com/{shortId} → {shortId}
20
20
  */
21
21
  const getWorkspaceId = (url) => {
22
- url = url.replace(/^https?:\/\//, '');
22
+ const urlWithoutProtocol = url.replace(/^https?:\/\//, '');
23
23
  // Custom domain: extract path after domain (not gotemper.com or legacy unnbound.ai)
24
- if (!url.includes('gotemper.com') && !url.includes('unnbound.ai')) {
25
- return url
24
+ if (!urlWithoutProtocol.includes('gotemper.com') && !urlWithoutProtocol.includes('unnbound.ai')) {
25
+ return urlWithoutProtocol
26
26
  .replace(/^.+?\/(.+)$/, '$1')
27
27
  .replace(/^(sandbox|staging)\//, '')
28
28
  .replace(/^a?sync\//, '')
29
29
  .split('/')[0];
30
30
  }
31
31
  // New path-based format: {env}.workflow.gotemper.com/{sandbox/}{shortId} or workflow.gotemper.com/{sandbox/}{shortId}
32
- if (url.includes('.workflow.') || url.startsWith('workflow.')) {
32
+ if (urlWithoutProtocol.includes('.workflow.') || urlWithoutProtocol.startsWith('workflow.')) {
33
33
  // Extract everything after the domain, then remove sandbox/ and async/ prefixes
34
- const path = url.replace(/^.+?\/(.+)$/, '$1');
34
+ const path = urlWithoutProtocol.replace(/^.+?\/(.+)$/, '$1');
35
35
  return path
36
36
  .replace(/^(sandbox|staging)\//, '')
37
37
  .replace(/^a?sync\//, '')
38
38
  .split('/')[0];
39
39
  }
40
40
  // Legacy hostname format: {shortId}.workspaces.{env}.unnbound.ai
41
- return url.split('.')[0];
41
+ return urlWithoutProtocol.split('.')[0];
42
42
  };
43
43
  const TYPE_PATH_PREFIXES = {
44
44
  sandbox: '/sandbox',
@@ -101,6 +101,7 @@ const getWorkflowDomain = (url) => {
101
101
  // Custom domain - return hostname as-is
102
102
  return hostname;
103
103
  };
104
+ const getWorkflowProtocol = (url) => (url.startsWith('http://') ? 'http' : 'https');
104
105
  /**
105
106
  * Create an HTTP-based enqueuer that posts to the ingress service.
106
107
  * Used for legacy SQS mode or cross-workflow communication.
@@ -117,7 +118,7 @@ const createHttpEnqueuer = () => {
117
118
  const workspaceId = getWorkspaceId(url);
118
119
  const domain = getWorkflowDomain(url);
119
120
  const workflowType = getWorkflowType(url);
120
- const baseURL = `https://${domain}${TYPE_PATH_PREFIXES[workflowType]}/async/${workspaceId}`;
121
+ const baseURL = `${getWorkflowProtocol(url)}://${domain}${TYPE_PATH_PREFIXES[workflowType]}/async/${workspaceId}`;
121
122
  const client = (0, unnbound_logger_sdk_1.traceAxios)(axios_1.default.create({ baseURL }), { getPayload: internal_1.internal });
122
123
  return async (event, metadata) => {
123
124
  unnbound_logger_sdk_1.logger.info({ event, metadata }, 'Enqueuing event via HTTP...');
@@ -25,6 +25,12 @@ export interface IncomingEvent<R extends IncomingRequest = IncomingRequest> {
25
25
  export interface StrictEventHandlerResult {
26
26
  status: StatusCode;
27
27
  headers?: Record<string, string>;
28
- body?: object | undefined | null;
28
+ /**
29
+ * Response body. Objects are JSON-serialized; `Uint8Array` (including Buffer)
30
+ * and `ArrayBuffer` are sent verbatim as binary — required for protocols like
31
+ * AS2 whose responses (signed S/MIME MDNs) must not be JSON-mangled. Set the
32
+ * `content-type` header explicitly when returning binary.
33
+ */
34
+ body?: object | Uint8Array | ArrayBuffer | undefined | null;
29
35
  }
30
36
  export type EventHandlerResult = Partial<StrictEventHandlerResult> | undefined | null | undefined;
@@ -8,12 +8,12 @@ export interface Endpoint<I> {
8
8
  (path: string, middleware: MiddlewareHandler<I>, handler: Handler<I>): MaybePromise<void>;
9
9
  }
10
10
  export type EndpointArgs<I> = [path: string, handler: Handler<I>] | [path: string, middleware: MiddlewareHandler<I>, handler: Handler<I>];
11
- export declare const buildEndpointArguments: (...args: EndpointArgs<EventVariables<{}>>) => (string | MiddlewareHandler<Context<EventEnvironment<{}>, any, {}>> | Handler<Context<EventEnvironment<{}>, any, {}>, Response>)[];
11
+ export declare const buildEndpointArguments: (...args: EndpointArgs<EventVariables<{}>>) => (string | Handler<Context<EventEnvironment<{}>, any, {}>, Response> | MiddlewareHandler<Context<EventEnvironment<{}>, any, {}>>)[];
12
12
  export declare const parseEndpointArguments: (...args: EndpointArgs<EventVariables<{}>>) => {
13
13
  path: string;
14
14
  middlewares: MiddlewareHandler<EventVariables<{}>>[];
15
15
  handler: Handler<EventVariables<{}>, EventHandlerResult>;
16
16
  };
17
17
  export declare const patchEndpoint: <V extends object>(handler: Handler<EventVariables<V>>) => Handler<Context<EventEnvironment<V>>, Response>;
18
- export declare const respondWith: (context: Context, result: StrictEventHandlerResult) => (Response & import("hono").TypedResponse<null, 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 | -1, "body">) | (Response & import("hono").TypedResponse<never, 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 | -1, "json">);
18
+ 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">);
19
19
  export declare const attachTraceHeaders: (headers?: Record<string, string>) => Record<string, string> | undefined;
@@ -33,6 +33,15 @@ const respondWith = (context, result) => {
33
33
  const headers = (0, exports.attachTraceHeaders)(result.headers);
34
34
  if (!result.body)
35
35
  return context.body(null, result.status, headers);
36
+ // Binary bodies (Buffer/Uint8Array/ArrayBuffer) go out verbatim — JSON-serializing
37
+ // them corrupts wire protocols like AS2, whose signed S/MIME MDN response must be
38
+ // byte-exact. Buffers can be views over a pooled ArrayBuffer, so slice to the view.
39
+ if (result.body instanceof Uint8Array || result.body instanceof ArrayBuffer) {
40
+ const bytes = result.body instanceof ArrayBuffer
41
+ ? result.body
42
+ : result.body.buffer.slice(result.body.byteOffset, result.body.byteOffset + result.body.byteLength);
43
+ return context.body(bytes, result.status, headers);
44
+ }
36
45
  return context.json(result.body, result.status, headers);
37
46
  };
38
47
  exports.respondWith = respondWith;
@@ -4,6 +4,7 @@ import { type Middleware } from './routing/middleware';
4
4
  import type { EventVariables } from './routing/types';
5
5
  type HttpOptions = {};
6
6
  interface QueueOptions {
7
+ enabled?: boolean;
7
8
  maxMessages?: number;
8
9
  visibilityTimeout?: number;
9
10
  }
@@ -14,6 +14,7 @@ class HttpQueueServer {
14
14
  app = new hono_1.Hono();
15
15
  http;
16
16
  queue;
17
+ queueEnabled;
17
18
  dispose;
18
19
  constructor(options = {}) {
19
20
  this.logger = options.logger ?? unnbound_logger_sdk_1.logger;
@@ -33,12 +34,14 @@ class HttpQueueServer {
33
34
  return (0, endpoint_1.respondWith)(c, error.toJson());
34
35
  });
35
36
  this.http = new http_adapter_1.HttpAdapter({ app: this.app, logger: this.logger, ...options.http });
36
- // SQS adapter for async message processing
37
- this.queue = new queue_adapter_1.QueueAdapter({
38
- app: this.app,
39
- logger: this.logger,
40
- options: options.queue,
41
- });
37
+ this.queueEnabled = shouldEnableQueue(options.queue);
38
+ this.queue = this.queueEnabled
39
+ ? new queue_adapter_1.QueueAdapter({
40
+ app: this.app,
41
+ logger: this.logger,
42
+ options: options.queue,
43
+ })
44
+ : null;
42
45
  }
43
46
  get = (...args) => {
44
47
  // @ts-expect-error
@@ -73,17 +76,24 @@ class HttpQueueServer {
73
76
  process.on('SIGINT', createOnSignal('SIGINT'));
74
77
  process.on('SIGTERM', createOnSignal('SIGTERM'));
75
78
  this.logger.debug({}, 'Starting server...');
79
+ const disposables = [];
76
80
  try {
77
- const disposables = await Promise.all([this.http.listen(), this.queue.listen()]);
78
- this.dispose = async () => {
79
- await Promise.all(disposables.map((dispose) => dispose()))
80
- .then(() => void 0)
81
- .catch(console.error);
82
- };
81
+ disposables.push(await this.http.listen());
82
+ if (this.queue && this.queueEnabled) {
83
+ disposables.push(await this.queue.listen());
84
+ }
85
+ else {
86
+ this.logger.info({
87
+ runtimeTarget: process.env.UNNBOUND_RUNTIME_TARGET ?? 'default',
88
+ queueConfigured: Boolean(process.env.UNNBOUND_SQS_URL),
89
+ }, 'Queue listener disabled for this runtime.');
90
+ }
91
+ this.dispose = () => this.stopDisposables(disposables);
83
92
  this.logger.info('Server started.');
84
93
  return this.dispose;
85
94
  }
86
95
  catch (error) {
96
+ await this.stopDisposables(disposables);
87
97
  this.logger.error({ err: error }, 'Server failed to start.');
88
98
  throw error;
89
99
  }
@@ -95,6 +105,20 @@ class HttpQueueServer {
95
105
  await this.dispose();
96
106
  this.logger.debug({}, 'Server stopped.');
97
107
  }
108
+ async stopDisposables(disposables) {
109
+ const results = await Promise.allSettled(disposables.map((dispose) => dispose()));
110
+ for (const result of results) {
111
+ if (result.status === 'rejected') {
112
+ this.logger.error({ err: result.reason }, 'Server disposable failed to stop.');
113
+ }
114
+ }
115
+ }
98
116
  }
99
117
  const createServer = (options = {}) => new HttpQueueServer(options);
100
118
  exports.createServer = createServer;
119
+ function shouldEnableQueue(options) {
120
+ if (options?.enabled !== undefined) {
121
+ return options.enabled;
122
+ }
123
+ return Boolean(process.env.UNNBOUND_SQS_URL);
124
+ }
@@ -17,7 +17,6 @@ export declare const buildIncomingPayload: (event: IncomingEvent<MatchedIncoming
17
17
  source: EventSource;
18
18
  delay: number | undefined;
19
19
  http: {
20
- response?: StrictEventHandlerResult | undefined;
21
20
  url: string;
22
21
  method: string;
23
22
  query: Record<string, any>;
@@ -26,8 +25,9 @@ export declare const buildIncomingPayload: (event: IncomingEvent<MatchedIncoming
26
25
  incoming: boolean;
27
26
  request: {
28
27
  headers: Record<string, string>;
29
- body: any;
28
+ body: unknown;
30
29
  };
30
+ response?: StrictEventHandlerResult | undefined;
31
31
  };
32
32
  };
33
33
  export declare const batchArray: <T>(array: T[], size: number) => T[][];
package/dist/lib/utils.js CHANGED
@@ -78,13 +78,20 @@ const deserializeMetadata = (request) => ({
78
78
  source: request.headers[sourceKey],
79
79
  timestamp: request.headers[timestampKey] ? Number.parseInt(request.headers[timestampKey]) : undefined,
80
80
  });
81
+ /** Trace-safe body representation: binary bodies become a marker, not a byte dump. */
82
+ const describeBody = (body) => {
83
+ if (body instanceof Uint8Array || body instanceof ArrayBuffer) {
84
+ return { binary: true, byteLength: body.byteLength };
85
+ }
86
+ return (0, utils_1.safeJsonParse)(body);
87
+ };
81
88
  const buildResponsePayload = (result) => ({
82
89
  status: result?.status ?? 204,
83
90
  headers: {
84
91
  'content-type': 'application/json; charset=utf-8',
85
92
  ...(result?.headers ? (0, exports.normalizeHeaders)(result.headers) : undefined),
86
93
  },
87
- body: result?.body ? (0, utils_1.safeJsonParse)(result.body) : undefined,
94
+ body: result?.body ? describeBody(result.body) : undefined,
88
95
  });
89
96
  const buildIncomingPayload = (event, result) => {
90
97
  return {
@@ -102,7 +109,7 @@ const buildIncomingPayload = (event, result) => {
102
109
  incoming: true,
103
110
  request: {
104
111
  headers: event.request.headers,
105
- body: (0, utils_1.safeJsonParse)(event.request.body),
112
+ body: describeBody(event.request.body),
106
113
  },
107
114
  ...(result && {
108
115
  response: result.error
package/package.json CHANGED
@@ -1,19 +1,17 @@
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": "2.0.19",
4
+ "version": "2.0.21-beta.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
- "build": "bun x tsc",
9
- "test": "echo 'No tests'",
10
- "typecheck": "tsc -noEmit",
11
- "lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
12
- "lint:fix": "bun run lint --fix",
8
+ "build": "tsc",
9
+ "test": "vitest run",
10
+ "typecheck": "tsc --noEmit",
13
11
  "format": "biome format --write .",
14
12
  "format:check": "biome format .",
15
- "prepublishOnly": "bun run build",
16
- "start:example": "bun --watch examples/node-server.ts",
13
+ "prepublishOnly": "pnpm run build",
14
+ "start:example": "tsx watch examples/node-server.ts",
17
15
  "version:bump": "npm version patch"
18
16
  },
19
17
  "author": "Unnbound Team",
@@ -30,15 +28,14 @@
30
28
  "@aws-sdk/client-s3": "^3.0.0",
31
29
  "@aws-sdk/client-sqs": "^3.0.0",
32
30
  "@hono/node-server": "^1.19.11",
33
- "axios": "^1.12.2",
34
- "hono": "^4.12.7",
35
- "ioredis": "^5.4.1",
31
+ "axios": "1.16.0",
32
+ "hono": "^4.12.21",
36
33
  "unnbound-logger-sdk": "^3.0.34"
37
34
  },
38
35
  "devDependencies": {
39
36
  "@types/jest": "^29.5.12",
40
- "@types/node": "^22",
41
- "typescript": "^5.8.3"
37
+ "@types/node": "^24.12.2",
38
+ "vitest": "^4.0.15"
42
39
  },
43
40
  "files": [
44
41
  "dist/**/*",