unnbound-events 2.1.0-beta.2 → 3.0.0-beta.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 +48 -6
- package/dist/index.d.ts +0 -2
- package/dist/index.js +1 -3
- package/dist/lib/adapters/extended-sqs-client.d.ts +16 -2
- package/dist/lib/enqueue.d.ts +2 -1
- package/dist/lib/event.d.ts +7 -6
- package/dist/lib/routing/endpoint.d.ts +3 -5
- package/dist/lib/routing/endpoint.js +10 -44
- package/dist/lib/routing/middleware.d.ts +5 -4
- package/dist/lib/routing/middleware.js +12 -2
- package/dist/lib/routing/response.d.ts +4 -0
- package/dist/lib/routing/response.js +28 -0
- package/dist/lib/server.js +3 -2
- package/dist/lib/utils.d.ts +7 -22
- package/dist/lib/utils.js +59 -74
- package/package.json +14 -13
- package/dist/lib/routing/as2.d.ts +0 -37
- package/dist/lib/routing/as2.js +0 -98
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Unnbound Events SDK
|
|
2
2
|
|
|
3
|
-
Unified HTTP and SQS queue routing with a single API. Register handlers once, handle requests from both transports.
|
|
3
|
+
Unified HTTP and SQS queue routing with a single API. Register handlers once, handle requests from both transports. `c.request.body` is always an `ArrayBuffer` of the exact request-content bytes after HTTP transport framing — the SDK never parses by content type or decompresses content encodings.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -18,7 +18,7 @@ const server = createServer();
|
|
|
18
18
|
// Register route handlers
|
|
19
19
|
server.post('/email/:userId', async (c) => {
|
|
20
20
|
const { userId } = c.request.params;
|
|
21
|
-
const
|
|
21
|
+
const payload = JSON.parse(Buffer.from(c.request.body).toString('utf8'));
|
|
22
22
|
|
|
23
23
|
return { status: 200, body: { sent: true } };
|
|
24
24
|
});
|
|
@@ -36,7 +36,7 @@ The server automatically:
|
|
|
36
36
|
|
|
37
37
|
## Handlers
|
|
38
38
|
|
|
39
|
-
Routes receive a context object with request details:
|
|
39
|
+
Routes receive a context object with request details. `c.request.body` is an `ArrayBuffer` containing the exact request-content bytes after HTTP transport framing, for **every** content type (zero-length when there is no body) — decode what you need inside the handler:
|
|
40
40
|
|
|
41
41
|
```ts
|
|
42
42
|
server.post('/users/:id', async (c) => {
|
|
@@ -44,21 +44,63 @@ server.post('/users/:id', async (c) => {
|
|
|
44
44
|
const { id } = c.request.params;
|
|
45
45
|
const query = c.request.query;
|
|
46
46
|
const headers = c.request.headers;
|
|
47
|
-
|
|
47
|
+
|
|
48
|
+
// Decode the raw bytes for the payload you expect:
|
|
49
|
+
|
|
50
|
+
// JSON
|
|
51
|
+
const payload = JSON.parse(Buffer.from(c.request.body).toString('utf8'));
|
|
52
|
+
|
|
53
|
+
// Text
|
|
54
|
+
const text = new TextDecoder().decode(c.request.body);
|
|
55
|
+
|
|
56
|
+
// Multipart / form-data (boundary comes from the original content-type header)
|
|
57
|
+
const form = await new Response(c.request.body, {
|
|
58
|
+
headers: { 'content-type': c.request.headers['content-type'] },
|
|
59
|
+
}).formData();
|
|
60
|
+
|
|
61
|
+
// URL-encoded forms
|
|
62
|
+
const params = new URLSearchParams(Buffer.from(c.request.body).toString('utf8'));
|
|
63
|
+
|
|
64
|
+
// XML, EDI, CSV: use the library the workflow already depends on (xml2js, csv, @ontemper/edi)
|
|
48
65
|
|
|
49
66
|
// Return response
|
|
50
67
|
return {
|
|
51
68
|
status: 200,
|
|
52
|
-
body: { id
|
|
69
|
+
body: { id },
|
|
53
70
|
headers: { 'x-custom': 'value' },
|
|
54
71
|
};
|
|
55
72
|
});
|
|
56
73
|
```
|
|
57
74
|
|
|
58
|
-
|
|
75
|
+
### Transport framing and content encoding
|
|
76
|
+
|
|
77
|
+
`c.request.body` contains request content, not HTTP transport framing. Content encodings such as gzip are preserved, so decompress them explicitly when needed.
|
|
78
|
+
|
|
79
|
+
**Response format:** `{ status?: number, body?: object | Uint8Array | ArrayBuffer, headers?: Record<string, string> }`
|
|
80
|
+
|
|
81
|
+
Objects are JSON-serialized. `Uint8Array`/`ArrayBuffer` bodies are sent verbatim as binary (set `content-type` explicitly) — required for protocols like AS2 whose responses must not be JSON-mangled.
|
|
59
82
|
|
|
60
83
|
Return `undefined`, `null`, or omit properties to default to `204 No Content`.
|
|
61
84
|
|
|
85
|
+
## Upgrading from 2.0.x
|
|
86
|
+
|
|
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
|
+
|
|
89
|
+
| `content-type` | 2.0.x | Current version |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| `application/json` | parsed object | `ArrayBuffer` |
|
|
92
|
+
| `application/x-www-form-urlencoded` | parsed object | `ArrayBuffer` |
|
|
93
|
+
| `multipart/form-data` | parsed object | `ArrayBuffer` |
|
|
94
|
+
| `text/plain` | `string` | `ArrayBuffer` |
|
|
95
|
+
| anything else | `ArrayBuffer` | `ArrayBuffer` |
|
|
96
|
+
|
|
97
|
+
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
|
+
|
|
99
|
+
Two things to check when upgrading an existing workflow:
|
|
100
|
+
|
|
101
|
+
- A malformed JSON body used to arrive as `undefined`. It now arrives as raw bytes and `JSON.parse` throws — handle it where you decode.
|
|
102
|
+
- `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
|
+
|
|
62
104
|
## Middleware
|
|
63
105
|
|
|
64
106
|
Add logic that runs before/after handlers. Works for both HTTP and queue requests.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,6 @@ export { createEnqueuer } from './lib/enqueue';
|
|
|
2
2
|
export type { ServerErrorOptions } from './lib/error';
|
|
3
3
|
export { ServerError } from './lib/error';
|
|
4
4
|
export type { EventMetadata, EventSource, IncomingEvent, IncomingRequest } from './lib/event';
|
|
5
|
-
export type { As2Prepared, As2RawRequest } from './lib/routing/as2';
|
|
6
|
-
export { as2Endpoint } from './lib/routing/as2';
|
|
7
5
|
export type { Handler } from './lib/routing/endpoint';
|
|
8
6
|
export type { MiddlewareHandler } from './lib/routing/middleware';
|
|
9
7
|
export type { EventVariables, MaybePromise } from './lib/routing/types';
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createServer = exports.
|
|
3
|
+
exports.createServer = exports.ServerError = exports.createEnqueuer = void 0;
|
|
4
4
|
var enqueue_1 = require("./lib/enqueue");
|
|
5
5
|
Object.defineProperty(exports, "createEnqueuer", { enumerable: true, get: function () { return enqueue_1.createEnqueuer; } });
|
|
6
6
|
var error_1 = require("./lib/error");
|
|
7
7
|
Object.defineProperty(exports, "ServerError", { enumerable: true, get: function () { return error_1.ServerError; } });
|
|
8
|
-
var as2_1 = require("./lib/routing/as2");
|
|
9
|
-
Object.defineProperty(exports, "as2Endpoint", { enumerable: true, get: function () { return as2_1.as2Endpoint; } });
|
|
10
8
|
var server_1 = require("./lib/server");
|
|
11
9
|
Object.defineProperty(exports, "createServer", { enumerable: true, get: function () { return server_1.createServer; } });
|
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import { type S3Client } from '@aws-sdk/client-s3';
|
|
2
2
|
import { type Message, type ReceiveMessageCommand, SQSClient } from '@aws-sdk/client-sqs';
|
|
3
|
-
import type {
|
|
3
|
+
import type { EventMetadata, IncomingRequest } from '../event';
|
|
4
4
|
export interface S3Pointer {
|
|
5
5
|
bucket: string;
|
|
6
6
|
key: string;
|
|
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
|
+
*/
|
|
16
|
+
interface SqsEnvelopeRequest extends Omit<IncomingRequest, 'body'> {
|
|
17
|
+
body: unknown;
|
|
18
|
+
}
|
|
19
|
+
interface SqsEnvelopeEvent {
|
|
20
|
+
request: SqsEnvelopeRequest;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
metadata: EventMetadata;
|
|
23
|
+
}
|
|
10
24
|
export interface ExtendedSqsMessage {
|
|
11
25
|
message: Message;
|
|
12
|
-
event:
|
|
26
|
+
event: SqsEnvelopeEvent;
|
|
13
27
|
}
|
|
14
28
|
export type ExtendedSQSClientConfig = SQSClientConfig & {
|
|
15
29
|
s3: S3Client;
|
package/dist/lib/enqueue.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { EventMetadata, IncomingRequest } from './event';
|
|
2
|
-
export interface EnqueuedRequest extends Omit<IncomingRequest, 'url' | 'headers' | 'query'> {
|
|
2
|
+
export interface EnqueuedRequest extends Omit<IncomingRequest, 'url' | 'headers' | 'query' | 'body'> {
|
|
3
3
|
query?: IncomingRequest['query'];
|
|
4
4
|
headers?: IncomingRequest['headers'];
|
|
5
|
+
body?: unknown;
|
|
5
6
|
}
|
|
6
7
|
/**
|
|
7
8
|
* Create an enqueuer that posts to the ingress service via HTTP.
|
package/dist/lib/event.d.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import type { StatusCode } from 'hono/utils/http-status';
|
|
2
2
|
import type { HttpMethod } from 'unnbound-logger-sdk';
|
|
3
|
-
export interface IncomingRequest
|
|
3
|
+
export interface IncomingRequest {
|
|
4
4
|
url: string;
|
|
5
5
|
method: HttpMethod;
|
|
6
6
|
path: string;
|
|
7
7
|
headers: Record<string, string>;
|
|
8
8
|
query: Record<string, any>;
|
|
9
|
-
|
|
9
|
+
/** Exact request-content bytes after HTTP transport framing; empty for bodyless requests. */
|
|
10
|
+
body: ArrayBuffer;
|
|
10
11
|
}
|
|
11
|
-
export interface MatchedIncomingRequest
|
|
12
|
-
params?: Record<string, string
|
|
12
|
+
export interface MatchedIncomingRequest extends IncomingRequest {
|
|
13
|
+
params?: Record<string, string>;
|
|
13
14
|
}
|
|
14
15
|
export type EventSource = 'http' | 'queue';
|
|
15
16
|
export interface EventMetadata {
|
|
@@ -31,6 +32,6 @@ export interface StrictEventHandlerResult {
|
|
|
31
32
|
* AS2 whose responses (signed S/MIME MDNs) must not be JSON-mangled. Set the
|
|
32
33
|
* `content-type` header explicitly when returning binary.
|
|
33
34
|
*/
|
|
34
|
-
body?: object | Uint8Array | ArrayBuffer |
|
|
35
|
+
body?: object | Uint8Array | ArrayBuffer | null;
|
|
35
36
|
}
|
|
36
|
-
export type EventHandlerResult = Partial<StrictEventHandlerResult> |
|
|
37
|
+
export type EventHandlerResult = Partial<StrictEventHandlerResult> | null | undefined;
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import type { Context } from 'hono';
|
|
2
|
-
import type { EventHandlerResult
|
|
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';
|
|
5
6
|
export type Handler<I, R = EventHandlerResult> = (input: I) => MaybePromise<R>;
|
|
6
|
-
export declare const markSelfTraced: <I>(handler: (input: I) => Promise<StrictEventHandlerResult>) => Handler<I>;
|
|
7
7
|
export interface Endpoint<I> {
|
|
8
8
|
(path: string, handler: Handler<I>): MaybePromise<void>;
|
|
9
9
|
(path: string, middleware: MiddlewareHandler<I>, handler: Handler<I>): MaybePromise<void>;
|
|
10
10
|
}
|
|
11
11
|
export type EndpointArgs<I> = [path: string, handler: Handler<I>] | [path: string, middleware: MiddlewareHandler<I>, handler: Handler<I>];
|
|
12
|
-
export declare const buildEndpointArguments: (...args: EndpointArgs<EventVariables<{}>>) => (string | Handler<Context<EventEnvironment<{}>, any, {}>, Response> | MiddlewareHandler<
|
|
12
|
+
export declare const buildEndpointArguments: (...args: EndpointArgs<EventVariables<{}>>) => (string | Handler<Context<EventEnvironment<{}>, any, {}>, Response> | import("hono").MiddlewareHandler<EventEnvironment<{}>>)[];
|
|
13
13
|
export declare const parseEndpointArguments: (...args: EndpointArgs<EventVariables<{}>>) => {
|
|
14
14
|
path: string;
|
|
15
15
|
middlewares: MiddlewareHandler<EventVariables<{}>>[];
|
|
16
16
|
handler: Handler<EventVariables<{}>, EventHandlerResult>;
|
|
17
17
|
};
|
|
18
18
|
export declare const patchEndpoint: <V extends object>(handler: Handler<EventVariables<V>>) => Handler<Context<EventEnvironment<V>>, Response>;
|
|
19
|
-
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">);
|
|
20
|
-
export declare const attachTraceHeaders: (headers?: Record<string, string>) => Record<string, string> | undefined;
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.patchEndpoint = exports.parseEndpointArguments = exports.buildEndpointArguments = exports.respondWith = exports.attachTraceHeaders = 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* wrapper (see routing/as2.ts) emits the request span itself and returns a strict result.
|
|
12
|
-
*/
|
|
13
|
-
const selfTraced = Symbol.for('unnbound-events.selfTraced');
|
|
14
|
-
const markSelfTraced = (handler) => {
|
|
15
|
-
return Object.assign(handler, { [selfTraced]: true });
|
|
16
|
-
};
|
|
17
|
-
exports.markSelfTraced = markSelfTraced;
|
|
18
|
-
const isSelfTraced = (handler) => selfTraced in handler;
|
|
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; } });
|
|
19
11
|
const buildEndpointArguments = (...args) => {
|
|
20
12
|
const { path, middlewares, handler } = (0, exports.parseEndpointArguments)(...args);
|
|
21
13
|
const patchedHandler = (0, exports.patchEndpoint)(handler);
|
|
@@ -33,37 +25,11 @@ const patchEndpoint = (handler) => {
|
|
|
33
25
|
const state = context.get('state');
|
|
34
26
|
// We have to recalculate the params because the handler path might contain url params
|
|
35
27
|
state.request.params = { ...state.request.params, ...context.req.param() };
|
|
36
|
-
const result =
|
|
37
|
-
|
|
38
|
-
:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}, (o) => (0, utils_1.buildIncomingPayload)(state, o));
|
|
42
|
-
return (0, exports.respondWith)(context, result);
|
|
28
|
+
const result = await (0, unnbound_logger_sdk_1.startSpan)('Event request', async () => {
|
|
29
|
+
const result = await handler(state);
|
|
30
|
+
return { status: result?.status ?? 204, headers: result?.headers, body: result?.body };
|
|
31
|
+
}, (o) => (0, utils_1.buildIncomingPayload)(state, o));
|
|
32
|
+
return (0, response_1.respondWith)(context, result);
|
|
43
33
|
};
|
|
44
34
|
};
|
|
45
35
|
exports.patchEndpoint = patchEndpoint;
|
|
46
|
-
const respondWith = (context, result) => {
|
|
47
|
-
const headers = (0, exports.attachTraceHeaders)(result.headers);
|
|
48
|
-
if (!result.body)
|
|
49
|
-
return context.body(null, result.status, headers);
|
|
50
|
-
// Signed protocol responses such as AS2 MDNs must remain byte-exact.
|
|
51
|
-
if (result.body instanceof Uint8Array || result.body instanceof ArrayBuffer) {
|
|
52
|
-
const bytes = result.body instanceof ArrayBuffer ? result.body : Uint8Array.from(result.body).buffer;
|
|
53
|
-
return context.body(bytes, result.status, headers);
|
|
54
|
-
}
|
|
55
|
-
return context.json(result.body, result.status, headers);
|
|
56
|
-
};
|
|
57
|
-
exports.respondWith = respondWith;
|
|
58
|
-
const attachTraceHeaders = (headers) => {
|
|
59
|
-
// storage can be undefined when CJS/ESM interop breaks re-exports (e.g. tsx in dev mode)
|
|
60
|
-
const { traceId, messageId } = unnbound_logger_sdk_1.storage?.getStore() ?? {};
|
|
61
|
-
if (!traceId || !messageId)
|
|
62
|
-
return headers;
|
|
63
|
-
return {
|
|
64
|
-
...headers,
|
|
65
|
-
[unnbound_logger_sdk_1.defaultTraceHeaderKey]: headers?.[unnbound_logger_sdk_1.defaultTraceHeaderKey] ?? traceId,
|
|
66
|
-
[unnbound_logger_sdk_1.defaultMessageHeaderKey]: headers?.[unnbound_logger_sdk_1.defaultMessageHeaderKey] ?? messageId,
|
|
67
|
-
};
|
|
68
|
-
};
|
|
69
|
-
exports.attachTraceHeaders = attachTraceHeaders;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MiddlewareHandler as HonoMiddlewareHandler, Next } from 'hono';
|
|
2
|
+
import type { EventHandlerResult } from '../event';
|
|
2
3
|
import type { EventEnvironment, EventVariables, MaybePromise } from './types';
|
|
3
|
-
export type MiddlewareHandler<I> = (input: I, next: Next) => MaybePromise<void>;
|
|
4
|
+
export type MiddlewareHandler<I> = (input: I, next: Next) => MaybePromise<EventHandlerResult | void>;
|
|
4
5
|
export interface Middleware<I> {
|
|
5
6
|
(...middlewares: MiddlewareHandler<I>[]): MaybePromise<void>;
|
|
6
7
|
(path: string, ...middlewares: MiddlewareHandler<I>[]): MaybePromise<void>;
|
|
7
8
|
}
|
|
8
9
|
export type MiddlewareArgs<I> = [...middlewares: MiddlewareHandler<I>[]] | [path: string, ...middlewares: MiddlewareHandler<I>[]];
|
|
9
|
-
export declare const patchMiddleware: (handler: MiddlewareHandler<EventVariables<{}>>) =>
|
|
10
|
+
export declare const patchMiddleware: (handler: MiddlewareHandler<EventVariables<{}>>) => HonoMiddlewareHandler<EventEnvironment<{}>>;
|
|
10
11
|
export declare const parseMiddlewareArguments: (...args: MiddlewareArgs<EventVariables<{}>>) => {
|
|
11
12
|
path: string;
|
|
12
13
|
middlewares: MiddlewareHandler<EventVariables<{}>>[];
|
|
@@ -14,4 +15,4 @@ export declare const parseMiddlewareArguments: (...args: MiddlewareArgs<EventVar
|
|
|
14
15
|
path: undefined;
|
|
15
16
|
middlewares: MiddlewareHandler<EventVariables<{}>>[];
|
|
16
17
|
};
|
|
17
|
-
export declare const buildMiddlewareArguments: (...args: MiddlewareArgs<EventVariables<{}>>) =>
|
|
18
|
+
export declare const buildMiddlewareArguments: (...args: MiddlewareArgs<EventVariables<{}>>) => HonoMiddlewareHandler<EventEnvironment<{}>>[] | readonly [string, ...HonoMiddlewareHandler<EventEnvironment<{}>>[]];
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildMiddlewareArguments = exports.parseMiddlewareArguments = exports.patchMiddleware = void 0;
|
|
4
|
+
const response_1 = require("./response");
|
|
4
5
|
const patchMiddleware = (handler) => {
|
|
5
|
-
return (context, next) => {
|
|
6
|
+
return async (context, next) => {
|
|
6
7
|
const state = context.get('state');
|
|
7
8
|
// We have to recalculate the params because middleware path might contain url params
|
|
8
9
|
state.request.params = { ...state.request.params, ...context.req.param() };
|
|
9
|
-
|
|
10
|
+
const result = await handler(state, next);
|
|
11
|
+
// Auth and validation middleware must be able to fail closed with the same
|
|
12
|
+
// response contract as endpoint handlers.
|
|
13
|
+
if (result == null)
|
|
14
|
+
return;
|
|
15
|
+
return (0, response_1.respondWith)(context, {
|
|
16
|
+
status: result.status ?? 204,
|
|
17
|
+
headers: result.headers,
|
|
18
|
+
body: result.body,
|
|
19
|
+
});
|
|
10
20
|
};
|
|
11
21
|
};
|
|
12
22
|
exports.patchMiddleware = patchMiddleware;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { StrictEventHandlerResult } from '../event';
|
|
3
|
+
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
|
+
export declare const attachTraceHeaders: (headers?: Record<string, string>) => Record<string, string> | undefined;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachTraceHeaders = exports.respondWith = void 0;
|
|
4
|
+
const unnbound_logger_sdk_1 = require("unnbound-logger-sdk");
|
|
5
|
+
const respondWith = (context, result) => {
|
|
6
|
+
const headers = (0, exports.attachTraceHeaders)(result.headers);
|
|
7
|
+
if (!result.body)
|
|
8
|
+
return context.body(null, result.status, headers);
|
|
9
|
+
// Signed protocol responses such as AS2 MDNs must remain byte-exact.
|
|
10
|
+
if (result.body instanceof Uint8Array || result.body instanceof ArrayBuffer) {
|
|
11
|
+
const bytes = result.body instanceof ArrayBuffer ? result.body : Uint8Array.from(result.body).buffer;
|
|
12
|
+
return context.body(bytes, result.status, headers);
|
|
13
|
+
}
|
|
14
|
+
return context.json(result.body, result.status, headers);
|
|
15
|
+
};
|
|
16
|
+
exports.respondWith = respondWith;
|
|
17
|
+
const attachTraceHeaders = (headers) => {
|
|
18
|
+
// storage can be undefined when CJS/ESM interop breaks re-exports (e.g. tsx in dev mode)
|
|
19
|
+
const { traceId, messageId } = unnbound_logger_sdk_1.storage?.getStore() ?? {};
|
|
20
|
+
if (!traceId || !messageId)
|
|
21
|
+
return headers;
|
|
22
|
+
return {
|
|
23
|
+
...headers,
|
|
24
|
+
[unnbound_logger_sdk_1.defaultTraceHeaderKey]: headers?.[unnbound_logger_sdk_1.defaultTraceHeaderKey] ?? traceId,
|
|
25
|
+
[unnbound_logger_sdk_1.defaultMessageHeaderKey]: headers?.[unnbound_logger_sdk_1.defaultMessageHeaderKey] ?? messageId,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
exports.attachTraceHeaders = attachTraceHeaders;
|
package/dist/lib/server.js
CHANGED
|
@@ -8,6 +8,7 @@ const queue_adapter_1 = require("./adapters/queue-adapter");
|
|
|
8
8
|
const error_1 = require("./error");
|
|
9
9
|
const endpoint_1 = require("./routing/endpoint");
|
|
10
10
|
const middleware_1 = require("./routing/middleware");
|
|
11
|
+
const response_1 = require("./routing/response");
|
|
11
12
|
const utils_1 = require("./utils");
|
|
12
13
|
class HttpQueueServer {
|
|
13
14
|
logger;
|
|
@@ -30,8 +31,8 @@ class HttpQueueServer {
|
|
|
30
31
|
}));
|
|
31
32
|
this.app.onError((err, c) => {
|
|
32
33
|
const error = error_1.ServerError.fromError(err);
|
|
33
|
-
|
|
34
|
-
return (0,
|
|
34
|
+
this.logger.error({ err: error.toError() }, error.message);
|
|
35
|
+
return (0, response_1.respondWith)(c, error.toJson());
|
|
35
36
|
});
|
|
36
37
|
this.http = new http_adapter_1.HttpAdapter({ app: this.app, logger: this.logger, ...options.http });
|
|
37
38
|
this.queueEnabled = shouldEnableQueue(options.queue);
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import type { HonoRequest } from 'hono';
|
|
2
2
|
import { type Maybe } from 'unnbound-logger-sdk/dist/types';
|
|
3
|
-
import type { EventHandlerResult, EventMetadata, EventSource, IncomingEvent, IncomingRequest, MatchedIncomingRequest
|
|
3
|
+
import type { EventHandlerResult, EventMetadata, EventSource, IncomingEvent, IncomingRequest, MatchedIncomingRequest } from './event';
|
|
4
4
|
export declare const normalizeHeaders: (headers: Record<string, string | string[]>) => Record<string, string>;
|
|
5
5
|
export declare const getIncomingEvent: (event: HonoRequest) => Promise<IncomingEvent<MatchedIncomingRequest>>;
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const getMetadataFromRequest: (request: IncomingRequest, source: EventSource) => EventMetadata;
|
|
6
|
+
export declare const getMetadataFromRequest: (request: Pick<IncomingRequest, 'headers'>, source: EventSource) => EventMetadata;
|
|
8
7
|
interface ForwardedMetadata {
|
|
9
8
|
source: EventSource;
|
|
10
9
|
timestamp: number;
|
|
@@ -26,27 +25,13 @@ export declare const buildIncomingPayload: (event: IncomingEvent<MatchedIncoming
|
|
|
26
25
|
incoming: boolean;
|
|
27
26
|
request: {
|
|
28
27
|
headers: Record<string, string>;
|
|
29
|
-
body:
|
|
28
|
+
body: unknown;
|
|
30
29
|
};
|
|
31
|
-
response?:
|
|
32
|
-
|
|
33
|
-
};
|
|
34
|
-
export declare const buildAs2IncomingPayload: (event: IncomingEvent<MatchedIncomingRequest>, result?: Maybe<EventHandlerResult>) => {
|
|
35
|
-
type: string;
|
|
36
|
-
source: EventSource;
|
|
37
|
-
delay: number | undefined;
|
|
38
|
-
http: {
|
|
39
|
-
url: string;
|
|
40
|
-
method: string;
|
|
41
|
-
query: Record<string, any>;
|
|
42
|
-
params: Record<string, string> | undefined;
|
|
43
|
-
ip: string | undefined;
|
|
44
|
-
incoming: boolean;
|
|
45
|
-
request: {
|
|
30
|
+
response?: {
|
|
31
|
+
status: import("hono/utils/http-status").StatusCode;
|
|
46
32
|
headers: Record<string, string>;
|
|
47
|
-
body:
|
|
48
|
-
};
|
|
49
|
-
response?: StrictEventHandlerResult | undefined;
|
|
33
|
+
body: unknown;
|
|
34
|
+
} | undefined;
|
|
50
35
|
};
|
|
51
36
|
};
|
|
52
37
|
export declare const batchArray: <T>(array: T[], size: number) => T[][];
|
package/dist/lib/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.batchArray = exports.
|
|
3
|
+
exports.batchArray = exports.buildIncomingPayload = exports.serializeMetadata = exports.getMetadataFromRequest = exports.getIncomingEvent = exports.normalizeHeaders = void 0;
|
|
4
4
|
const unnbound_logger_sdk_1 = require("unnbound-logger-sdk");
|
|
5
5
|
const trace_1 = require("unnbound-logger-sdk/dist/trace");
|
|
6
6
|
const types_1 = require("unnbound-logger-sdk/dist/types");
|
|
@@ -41,42 +41,23 @@ const getIncomingRequest = async (request) => {
|
|
|
41
41
|
path: request.path,
|
|
42
42
|
headers,
|
|
43
43
|
query: request.query(),
|
|
44
|
-
body: await
|
|
44
|
+
body: await readBody(request),
|
|
45
45
|
params: parseParams(request),
|
|
46
46
|
};
|
|
47
47
|
};
|
|
48
|
-
const
|
|
48
|
+
const readBody = async (request) => {
|
|
49
49
|
try {
|
|
50
|
-
|
|
51
|
-
return await request.json();
|
|
52
|
-
if (headers['content-type']?.includes('application/x-www-form-urlencoded'))
|
|
53
|
-
return await request.parseBody();
|
|
54
|
-
if (headers['content-type']?.includes('multipart/form-data'))
|
|
55
|
-
return await request.parseBody();
|
|
56
|
-
if (headers['content-type']?.includes('text/plain'))
|
|
57
|
-
return await request.text();
|
|
58
|
-
return request.arrayBuffer();
|
|
50
|
+
return await request.arrayBuffer();
|
|
59
51
|
}
|
|
60
|
-
catch {
|
|
61
|
-
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw new error_1.ServerError({ status: 400, message: 'Failed to read request body.', cause: error });
|
|
62
54
|
}
|
|
63
55
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const CANONICAL_TRACE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
70
|
-
const isCanonicalTraceId = (value) => typeof value === 'string' && CANONICAL_TRACE_ID.test(value);
|
|
71
|
-
exports.isCanonicalTraceId = isCanonicalTraceId;
|
|
72
|
-
const getMetadataFromRequest = (request, source) => {
|
|
73
|
-
const headerTraceId = request.headers[types_1.defaultTraceHeaderKey];
|
|
74
|
-
return {
|
|
75
|
-
traceId: (0, exports.isCanonicalTraceId)(headerTraceId) ? headerTraceId : (0, unnbound_logger_sdk_1.getTraceId)(),
|
|
76
|
-
messageId: request.headers[types_1.defaultMessageHeaderKey] ?? (0, trace_1.getMessageId)(),
|
|
77
|
-
source,
|
|
78
|
-
};
|
|
79
|
-
};
|
|
56
|
+
const getMetadataFromRequest = (request, source) => ({
|
|
57
|
+
traceId: request.headers[types_1.defaultTraceHeaderKey] ?? (0, unnbound_logger_sdk_1.getTraceId)(),
|
|
58
|
+
messageId: request.headers[types_1.defaultMessageHeaderKey] ?? (0, trace_1.getMessageId)(),
|
|
59
|
+
source,
|
|
60
|
+
});
|
|
80
61
|
exports.getMetadataFromRequest = getMetadataFromRequest;
|
|
81
62
|
const sourceKey = 'x-unnbound-source';
|
|
82
63
|
const timestampKey = 'x-unnbound-timestamp';
|
|
@@ -89,14 +70,34 @@ const deserializeMetadata = (request) => ({
|
|
|
89
70
|
source: request.headers[sourceKey],
|
|
90
71
|
timestamp: request.headers[timestampKey] ? Number.parseInt(request.headers[timestampKey], 10) : undefined,
|
|
91
72
|
});
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
73
|
+
const TRACE_BODY_MAX_BYTES = 32_768;
|
|
74
|
+
const getMediaType = (contentType) => contentType?.split(';')[0].trim().toLowerCase() || undefined;
|
|
75
|
+
const isTextishMediaType = (mediaType) => Boolean(mediaType &&
|
|
76
|
+
(mediaType.includes('json') ||
|
|
77
|
+
mediaType.startsWith('text/') ||
|
|
78
|
+
mediaType.includes('xml') ||
|
|
79
|
+
mediaType === 'application/x-www-form-urlencoded'));
|
|
80
|
+
const describeBody = (body, contentType) => {
|
|
81
|
+
if (!(body instanceof Uint8Array || body instanceof ArrayBuffer))
|
|
82
|
+
return (0, utils_1.safeJsonParse)(body);
|
|
83
|
+
const bytes = body instanceof Uint8Array ? body : new Uint8Array(body);
|
|
84
|
+
const byteLength = bytes.byteLength;
|
|
85
|
+
const mediaType = getMediaType(contentType);
|
|
86
|
+
if (!isTextishMediaType(mediaType))
|
|
87
|
+
return { binary: true, byteLength };
|
|
88
|
+
try {
|
|
89
|
+
const truncated = byteLength > TRACE_BODY_MAX_BYTES;
|
|
90
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, TRACE_BODY_MAX_BYTES), {
|
|
91
|
+
stream: truncated,
|
|
92
|
+
});
|
|
93
|
+
if (mediaType?.includes('json') && !truncated)
|
|
94
|
+
return (0, utils_1.safeJsonParse)(text);
|
|
95
|
+
return truncated ? { text, truncated: true, byteLength } : text;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { binary: true, byteLength };
|
|
95
99
|
}
|
|
96
|
-
return (0, utils_1.safeJsonParse)(body);
|
|
97
100
|
};
|
|
98
|
-
// Trace payloads are shipped to the logging pipeline; credentials must never ride along
|
|
99
|
-
// regardless of body shape (JSON, text, or binary — e.g. AS2's signed/encrypted MIME).
|
|
100
101
|
const REDACTED_TRACE_HEADERS = new Set([
|
|
101
102
|
'authorization',
|
|
102
103
|
'proxy-authorization',
|
|
@@ -110,38 +111,23 @@ const headersForTrace = (headers) => Object.fromEntries(Object.entries(headers).
|
|
|
110
111
|
name,
|
|
111
112
|
REDACTED_TRACE_HEADERS.has(name.toLowerCase()) ? '[redacted]' : value,
|
|
112
113
|
]));
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
'x-unnbound-trace-id',
|
|
131
|
-
]);
|
|
132
|
-
const as2HeadersForTrace = (headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [
|
|
133
|
-
name,
|
|
134
|
-
AS2_TRACE_HEADER_ALLOWLIST.has(name.toLowerCase()) ? value : '[redacted]',
|
|
135
|
-
]));
|
|
136
|
-
const buildResponsePayload = (headersPolicy, result) => ({
|
|
137
|
-
status: result?.status ?? 204,
|
|
138
|
-
headers: headersPolicy({
|
|
139
|
-
'content-type': 'application/json; charset=utf-8',
|
|
140
|
-
...(result?.headers ? (0, exports.normalizeHeaders)(result.headers) : undefined),
|
|
141
|
-
}),
|
|
142
|
-
body: result?.body ? describeBody(result.body) : undefined,
|
|
143
|
-
});
|
|
144
|
-
const buildIncomingPayloadWith = (headersPolicy) => (event, result) => {
|
|
114
|
+
const buildResponsePayload = (result) => {
|
|
115
|
+
const declaredHeaders = result?.headers
|
|
116
|
+
? Object.fromEntries(Object.entries((0, exports.normalizeHeaders)(result.headers)).map(([name, value]) => [name.toLowerCase(), value]))
|
|
117
|
+
: 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;
|
|
123
|
+
const traceHeaders = headersForTrace(declaredHeaders ?? {});
|
|
124
|
+
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
|
+
};
|
|
129
|
+
};
|
|
130
|
+
const buildIncomingPayload = (event, result) => {
|
|
145
131
|
return {
|
|
146
132
|
type: 'http',
|
|
147
133
|
source: event.metadata.source,
|
|
@@ -156,19 +142,18 @@ const buildIncomingPayloadWith = (headersPolicy) => (event, result) => {
|
|
|
156
142
|
event.request.headers['x-real-ip']),
|
|
157
143
|
incoming: true,
|
|
158
144
|
request: {
|
|
159
|
-
headers:
|
|
160
|
-
body: describeBody(event.request.body),
|
|
145
|
+
headers: headersForTrace(event.request.headers),
|
|
146
|
+
body: describeBody(event.request.body, event.request.headers['content-type']),
|
|
161
147
|
},
|
|
162
148
|
...(result && {
|
|
163
149
|
response: result.error
|
|
164
|
-
? buildResponsePayload(
|
|
165
|
-
: buildResponsePayload(
|
|
150
|
+
? buildResponsePayload(error_1.ServerError.fromError(result.error).toJson())
|
|
151
|
+
: buildResponsePayload(result.result),
|
|
166
152
|
}),
|
|
167
153
|
},
|
|
168
154
|
};
|
|
169
155
|
};
|
|
170
|
-
exports.buildIncomingPayload =
|
|
171
|
-
exports.buildAs2IncomingPayload = buildIncomingPayloadWith(as2HeadersForTrace);
|
|
156
|
+
exports.buildIncomingPayload = buildIncomingPayload;
|
|
172
157
|
const batchArray = (array, size) => {
|
|
173
158
|
return [...Array(Math.ceil(array.length / size))].map((_, i) => array.slice(i * size, i * size + size));
|
|
174
159
|
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
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": "
|
|
4
|
+
"version": "3.0.0-beta.1",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "vitest run",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"format": "biome format --write .",
|
|
12
|
+
"format:check": "biome format .",
|
|
13
|
+
"prepublishOnly": "pnpm run build",
|
|
14
|
+
"start:example": "tsx watch examples/node-server.ts",
|
|
15
|
+
"version:bump": "npm version patch"
|
|
16
|
+
},
|
|
7
17
|
"author": "Unnbound Team",
|
|
8
18
|
"license": "MIT",
|
|
9
19
|
"repository": {
|
|
@@ -20,7 +30,7 @@
|
|
|
20
30
|
"@hono/node-server": "^1.19.11",
|
|
21
31
|
"axios": "1.16.0",
|
|
22
32
|
"hono": "^4.12.21",
|
|
23
|
-
"unnbound-logger-sdk": "3.
|
|
33
|
+
"unnbound-logger-sdk": "^3.0.34"
|
|
24
34
|
},
|
|
25
35
|
"devDependencies": {
|
|
26
36
|
"@types/jest": "^29.5.12",
|
|
@@ -35,14 +45,5 @@
|
|
|
35
45
|
"engines": {
|
|
36
46
|
"node": ">=22"
|
|
37
47
|
},
|
|
38
|
-
"sideEffects": false
|
|
39
|
-
|
|
40
|
-
"build": "tsc",
|
|
41
|
-
"test": "vitest run",
|
|
42
|
-
"typecheck": "tsc --noEmit",
|
|
43
|
-
"format": "biome format --write .",
|
|
44
|
-
"format:check": "biome format .",
|
|
45
|
-
"start:example": "tsx watch examples/node-server.ts",
|
|
46
|
-
"version:bump": "npm version patch"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
48
|
+
"sideEffects": false
|
|
49
|
+
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { EventHandlerResult } from '../event';
|
|
2
|
-
import { type Handler } from './endpoint';
|
|
3
|
-
import type { EventVariables, MaybePromise } from './types';
|
|
4
|
-
/** Raw wire request handed to the platform verification call, byte-exact. */
|
|
5
|
-
export interface As2RawRequest {
|
|
6
|
-
headers: Record<string, string>;
|
|
7
|
-
body: ArrayBuffer | Uint8Array | string | undefined;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Result of the platform verification call. `as2.receive` results carry the exchange's
|
|
11
|
-
* canonical customer trace (recovered from the ledger for a loopback, server-minted for
|
|
12
|
-
* an external inbound); `as2.confirm` results deliberately do not — an async MDN
|
|
13
|
-
* callback is its own execution and stays on its own trace, linked by Message-ID.
|
|
14
|
-
*/
|
|
15
|
-
export interface As2Prepared {
|
|
16
|
-
customerTraceId?: string | null;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Verify-then-bind AS2 endpoint. The public `x-unnbound-trace-id` header is never AS2
|
|
20
|
-
* continuation authority: the route starts on a fresh trace, runs `prepare` (the hidden
|
|
21
|
-
* `as2.receive`/`as2.confirm` platform call) before any customer request span exists,
|
|
22
|
-
* and only a verified ledger-backed trace may re-bind the request. Unverified traffic
|
|
23
|
-
* fails on its own sanitized trace and can assert nothing. Request/response headers are
|
|
24
|
-
* logged through the AS2 allowlist — Receipt-Delivery-Option and Subject never appear.
|
|
25
|
-
*
|
|
26
|
-
* ```ts
|
|
27
|
-
* server.post('/as2/inbound', as2Endpoint(
|
|
28
|
-
* ({ headers, body }) => as2.receive({ headers, body: Buffer.from(body as ArrayBuffer), partner: 'AS2_PARTNER' }),
|
|
29
|
-
* async (result) => {
|
|
30
|
-
* if (result.kind !== 'received') return result.response;
|
|
31
|
-
* await processDocument(result.payload);
|
|
32
|
-
* return (await result.acknowledge()).response;
|
|
33
|
-
* },
|
|
34
|
-
* ));
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
export declare const as2Endpoint: <V extends object, P extends object>(prepare: (request: As2RawRequest, event: EventVariables<V>) => Promise<P>, handler: (prepared: P, event: EventVariables<V>) => MaybePromise<EventHandlerResult>) => Handler<EventVariables<V>>;
|
package/dist/lib/routing/as2.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.as2Endpoint = void 0;
|
|
4
|
-
const unnbound_logger_sdk_1 = require("unnbound-logger-sdk");
|
|
5
|
-
const error_1 = require("../error");
|
|
6
|
-
const utils_1 = require("../utils");
|
|
7
|
-
const endpoint_1 = require("./endpoint");
|
|
8
|
-
/**
|
|
9
|
-
* Transient platform failures a partner should re-send against. `As2SdkError` already computed
|
|
10
|
-
* these from the platform response, but it arrives here as a plain unknown — this package cannot
|
|
11
|
-
* import `@ontemper/as2` (the dependency runs the other way), so the shape is read defensively.
|
|
12
|
-
*/
|
|
13
|
-
const RETRYABLE_AS2_STATUSES = [502, 503, 504];
|
|
14
|
-
/**
|
|
15
|
-
* AS2 partners drive their re-send behaviour off the status code, so collapsing every failure to
|
|
16
|
-
* a flat 500 throws away the one signal telling them to try again — the same signal the
|
|
17
|
-
* in-flight/parked path already sends deliberately as a 503. Lift the transient statuses onto the
|
|
18
|
-
* wire; the message stays generic so no internal detail reaches the partner.
|
|
19
|
-
*/
|
|
20
|
-
const toWireFailure = (error) => {
|
|
21
|
-
if (error && typeof error === 'object') {
|
|
22
|
-
const { name, status } = error;
|
|
23
|
-
const retryable = RETRYABLE_AS2_STATUSES.find((candidate) => candidate === status);
|
|
24
|
-
if (name === 'As2SdkError' && retryable) {
|
|
25
|
-
return new error_1.ServerError({
|
|
26
|
-
message: 'AS2 message could not be processed; retry later',
|
|
27
|
-
status: retryable,
|
|
28
|
-
cause: error,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return error_1.ServerError.fromError(error);
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Verify-then-bind AS2 endpoint. The public `x-unnbound-trace-id` header is never AS2
|
|
36
|
-
* continuation authority: the route starts on a fresh trace, runs `prepare` (the hidden
|
|
37
|
-
* `as2.receive`/`as2.confirm` platform call) before any customer request span exists,
|
|
38
|
-
* and only a verified ledger-backed trace may re-bind the request. Unverified traffic
|
|
39
|
-
* fails on its own sanitized trace and can assert nothing. Request/response headers are
|
|
40
|
-
* logged through the AS2 allowlist — Receipt-Delivery-Option and Subject never appear.
|
|
41
|
-
*
|
|
42
|
-
* ```ts
|
|
43
|
-
* server.post('/as2/inbound', as2Endpoint(
|
|
44
|
-
* ({ headers, body }) => as2.receive({ headers, body: Buffer.from(body as ArrayBuffer), partner: 'AS2_PARTNER' }),
|
|
45
|
-
* async (result) => {
|
|
46
|
-
* if (result.kind !== 'received') return result.response;
|
|
47
|
-
* await processDocument(result.payload);
|
|
48
|
-
* return (await result.acknowledge()).response;
|
|
49
|
-
* },
|
|
50
|
-
* ));
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
53
|
-
// P is constrained to object, not As2Prepared: prepared results are unions whose
|
|
54
|
-
// members don't all carry customerTraceId (e.g. inFlight/parked), and TypeScript's
|
|
55
|
-
// weak-type check would reject those. The As2Prepared contract is read defensively.
|
|
56
|
-
const as2Endpoint = (prepare, handler) => {
|
|
57
|
-
return (0, endpoint_1.markSelfTraced)(async (event) => {
|
|
58
|
-
const requestTraceId = (0, unnbound_logger_sdk_1.getTraceId)();
|
|
59
|
-
let prepared;
|
|
60
|
-
try {
|
|
61
|
-
prepared = await (0, unnbound_logger_sdk_1.withTrace)(() => prepare({ headers: event.request.headers, body: event.request.body }, event), {
|
|
62
|
-
traceId: requestTraceId,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
return (0, unnbound_logger_sdk_1.withTrace)(async () => {
|
|
67
|
-
const failure = toWireFailure(error);
|
|
68
|
-
// Bookend the request on its own failure trace: the span records the sanitized
|
|
69
|
-
// AS2 payload and rethrows; the wire response carries no protocol claims.
|
|
70
|
-
await (0, unnbound_logger_sdk_1.startSpan)('Event request', () => Promise.reject(failure), (o) => (0, utils_1.buildAs2IncomingPayload)(event, o)).catch(() => undefined);
|
|
71
|
-
const result = failure.toJson();
|
|
72
|
-
return { ...result, headers: (0, endpoint_1.attachTraceHeaders)(result.headers) };
|
|
73
|
-
}, { traceId: requestTraceId });
|
|
74
|
-
}
|
|
75
|
-
const verified = prepared.customerTraceId;
|
|
76
|
-
const traceId = (0, utils_1.isCanonicalTraceId)(verified) ? verified : requestTraceId;
|
|
77
|
-
return (0, unnbound_logger_sdk_1.withTrace)(async () => {
|
|
78
|
-
// Response trace headers must be attached inside this scope: respondWith runs after
|
|
79
|
-
// the re-bound trace exits and would otherwise advertise the pre-bind trace.
|
|
80
|
-
try {
|
|
81
|
-
const result = await (0, unnbound_logger_sdk_1.startSpan)('Event request', async () => {
|
|
82
|
-
const result = await handler(prepared, event);
|
|
83
|
-
return { status: result?.status ?? 204, headers: result?.headers, body: result?.body };
|
|
84
|
-
}, (o) => (0, utils_1.buildAs2IncomingPayload)(event, o));
|
|
85
|
-
return { ...result, headers: (0, endpoint_1.attachTraceHeaders)(result.headers) };
|
|
86
|
-
}
|
|
87
|
-
catch (error) {
|
|
88
|
-
// The span above already logged the failure on the bound trace; respond with the
|
|
89
|
-
// sanitized shape instead of rethrowing into the header-derived middleware trace.
|
|
90
|
-
// `acknowledge()` runs in here too, so a transient platform failure keeps its
|
|
91
|
-
// retryable status rather than becoming an indistinguishable 500.
|
|
92
|
-
const result = toWireFailure(error).toJson();
|
|
93
|
-
return { ...result, headers: (0, endpoint_1.attachTraceHeaders)(result.headers) };
|
|
94
|
-
}
|
|
95
|
-
}, { traceId });
|
|
96
|
-
});
|
|
97
|
-
};
|
|
98
|
-
exports.as2Endpoint = as2Endpoint;
|