@rotorsoft/act-http 1.1.0 → 1.2.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 +12 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/api/actor.d.ts +20 -0
- package/dist/@types/api/actor.d.ts.map +1 -0
- package/dist/@types/api/errors.d.ts +73 -0
- package/dist/@types/api/errors.d.ts.map +1 -0
- package/dist/@types/api/idempotency.d.ts +36 -0
- package/dist/@types/api/idempotency.d.ts.map +1 -0
- package/dist/@types/api/index.d.ts +39 -0
- package/dist/@types/api/index.d.ts.map +1 -0
- package/dist/@types/receiver/start.d.ts +1 -1
- package/dist/@types/sse/apply-patch.d.ts +3 -3
- package/dist/@types/sse/broadcast.d.ts +6 -6
- package/dist/@types/sse/broadcast.d.ts.map +1 -1
- package/dist/@types/sse/presence.d.ts +7 -7
- package/dist/@types/sse/presence.d.ts.map +1 -1
- package/dist/@types/webhook/classify.d.ts +6 -6
- package/dist/@types/webhook/classify.d.ts.map +1 -1
- package/dist/@types/webhook/index.d.ts +1 -1
- package/dist/@types/webhook/index.d.ts.map +1 -1
- package/dist/@types/webhook/sign.d.ts +1 -1
- package/dist/@types/webhook/sign.d.ts.map +1 -1
- package/dist/api/index.cjs +85 -0
- package/dist/api/index.cjs.map +1 -0
- package/dist/api/index.js +62 -0
- package/dist/api/index.js.map +1 -0
- package/dist/{chunk-NOIXOF2I.js → chunk-4CGAUB5H.js} +13 -13
- package/dist/chunk-4CGAUB5H.js.map +1 -0
- package/dist/{chunk-F7VWYZ37.js → chunk-K4HAOBRF.js} +4 -4
- package/dist/{chunk-F7VWYZ37.js.map → chunk-K4HAOBRF.js.map} +1 -1
- package/dist/receiver/express/index.cjs +14 -14
- package/dist/receiver/express/index.cjs.map +1 -1
- package/dist/receiver/express/index.js +3 -3
- package/dist/receiver/express/index.js.map +1 -1
- package/dist/receiver/fastify/index.cjs +12 -12
- package/dist/receiver/fastify/index.cjs.map +1 -1
- package/dist/receiver/fastify/index.js +1 -1
- package/dist/receiver/hono/index.cjs +14 -14
- package/dist/receiver/hono/index.cjs.map +1 -1
- package/dist/receiver/hono/index.js +2 -2
- package/dist/receiver/index.cjs +19 -2746
- package/dist/receiver/index.cjs.map +1 -1
- package/dist/receiver/index.js +5 -2077
- package/dist/receiver/index.js.map +1 -1
- package/dist/receiver/trpc/index.cjs +12 -12
- package/dist/receiver/trpc/index.cjs.map +1 -1
- package/dist/receiver/trpc/index.js +1 -1
- package/dist/sse/index.cjs +21 -21
- package/dist/sse/index.cjs.map +1 -1
- package/dist/sse/index.js +24 -24
- package/dist/sse/index.js.map +1 -1
- package/dist/webhook/index.cjs +14 -34
- package/dist/webhook/index.cjs.map +1 -1
- package/dist/webhook/index.js +14 -32
- package/dist/webhook/index.js.map +1 -1
- package/package.json +34 -12
- package/dist/chunk-NOIXOF2I.js.map +0 -1
- package/dist/dist-NWMJQI4E.js +0 -647
- package/dist/dist-NWMJQI4E.js.map +0 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Actor } from "@rotorsoft/act";
|
|
2
|
+
/**
|
|
3
|
+
* Extractor function the host supplies to resolve an {@link Actor}
|
|
4
|
+
* from an incoming request. The framework keeps auth out of the
|
|
5
|
+
* package — JWT vs session vs API key is the host's call — and asks
|
|
6
|
+
* for this single closure that every transport (tRPC, Hono, OpenAPI
|
|
7
|
+
* docs) composes against.
|
|
8
|
+
*
|
|
9
|
+
* The `request` argument is intentionally generic. Each transport
|
|
10
|
+
* narrows it at the call site (`IncomingMessage` for Hono, the tRPC
|
|
11
|
+
* context object for tRPC, etc.) — keeping the contract here
|
|
12
|
+
* transport-agnostic means one extractor implementation plugs into
|
|
13
|
+
* every adapter unchanged.
|
|
14
|
+
*
|
|
15
|
+
* Async is allowed so the extractor can verify a JWT against a
|
|
16
|
+
* remote JWKS endpoint without forcing every host to synchronously
|
|
17
|
+
* cache.
|
|
18
|
+
*/
|
|
19
|
+
export type ActorExtractor = (request: unknown) => Actor | Promise<Actor>;
|
|
20
|
+
//# sourceMappingURL=actor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actor.d.ts","sourceRoot":"","sources":["../../../src/api/actor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uniform error envelope shipped over the wire by every act-http
|
|
3
|
+
* transport. Hosts get the same shape from REST, tRPC, and OpenAPI —
|
|
4
|
+
* a client that talks to two transports doesn't have to invent two
|
|
5
|
+
* error parsers.
|
|
6
|
+
*
|
|
7
|
+
* - `error` — the framework error name (`"ValidationError"`,
|
|
8
|
+
* `"InvariantError"`, …). Stable identifier, safe to switch on.
|
|
9
|
+
* - `detail` — the framework's message text. Human-readable; not
|
|
10
|
+
* parsed by clients.
|
|
11
|
+
* - `code` — a machine-readable status code from {@link ERROR_MAP}
|
|
12
|
+
* for clients that prefer enum-style branching over name strings.
|
|
13
|
+
*/
|
|
14
|
+
export type ApiError = {
|
|
15
|
+
error: string;
|
|
16
|
+
detail?: string;
|
|
17
|
+
code?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Status + code pair for one known framework error.
|
|
21
|
+
*/
|
|
22
|
+
export type ErrorMapEntry = {
|
|
23
|
+
status: number;
|
|
24
|
+
code: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The single table that maps framework error types to HTTP status
|
|
28
|
+
* codes and machine-readable codes. One table, three consumers
|
|
29
|
+
* (Hono, tRPC, OpenAPI) — cross-transport consistency by
|
|
30
|
+
* construction.
|
|
31
|
+
*
|
|
32
|
+
* Operators wanting different mappings wrap the generated transport
|
|
33
|
+
* rather than mutating this — the consistency is the load-bearing
|
|
34
|
+
* property, not the specific status codes.
|
|
35
|
+
*/
|
|
36
|
+
export declare const ERROR_MAP: {
|
|
37
|
+
readonly ValidationError: {
|
|
38
|
+
readonly status: 422;
|
|
39
|
+
readonly code: "VALIDATION";
|
|
40
|
+
};
|
|
41
|
+
readonly InvariantError: {
|
|
42
|
+
readonly status: 409;
|
|
43
|
+
readonly code: "INVARIANT";
|
|
44
|
+
};
|
|
45
|
+
readonly ConcurrencyError: {
|
|
46
|
+
readonly status: 412;
|
|
47
|
+
readonly code: "CONCURRENCY";
|
|
48
|
+
};
|
|
49
|
+
readonly StreamClosedError: {
|
|
50
|
+
readonly status: 410;
|
|
51
|
+
readonly code: "STREAM_CLOSED";
|
|
52
|
+
};
|
|
53
|
+
readonly NonRetryableError: {
|
|
54
|
+
readonly status: 400;
|
|
55
|
+
readonly code: "NON_RETRYABLE";
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Translate an unknown thrown value into the canonical
|
|
60
|
+
* {@link ApiError} envelope plus HTTP status. Each transport's error
|
|
61
|
+
* boundary calls this once and forwards the result to the wire.
|
|
62
|
+
*
|
|
63
|
+
* Known framework errors map per {@link ERROR_MAP}. Everything else
|
|
64
|
+
* surfaces as a 500 with `code: "INTERNAL"`; the `detail` field is
|
|
65
|
+
* populated when the throw was an `Error` instance, omitted
|
|
66
|
+
* otherwise (a thrown string or object doesn't get to leak its
|
|
67
|
+
* payload to the client).
|
|
68
|
+
*/
|
|
69
|
+
export declare function toApiError(err: unknown): {
|
|
70
|
+
status: number;
|
|
71
|
+
body: ApiError;
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/api/errors.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;CAM4B,CAAC;AAanD;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,CAuB3E"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { IdempotencyStore } from "@rotorsoft/act-ops/idempotency";
|
|
2
|
+
/**
|
|
3
|
+
* Result of a {@link withIdempotency} call.
|
|
4
|
+
*
|
|
5
|
+
* - `{ deduped: false, result }` — the claim was fresh; the handler
|
|
6
|
+
* ran and produced `result`.
|
|
7
|
+
* - `{ deduped: true }` — the claim was already taken; the handler
|
|
8
|
+
* was *not* invoked. The caller decides how to respond (typically
|
|
9
|
+
* a 2xx with no body, matching the receiver-side convention).
|
|
10
|
+
*
|
|
11
|
+
* Note: the contract does not cache the previous response. A
|
|
12
|
+
* duplicate call returns the deduped marker only — replaying the
|
|
13
|
+
* original handler's output would require a response-caching
|
|
14
|
+
* adapter, which is out of scope here. The receiver-side convention
|
|
15
|
+
* (and the convention the generated transports follow) is "ack the
|
|
16
|
+
* duplicate; do nothing else."
|
|
17
|
+
*/
|
|
18
|
+
export type IdempotencyResult<T> = {
|
|
19
|
+
deduped: false;
|
|
20
|
+
result: T;
|
|
21
|
+
} | {
|
|
22
|
+
deduped: true;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Wrap an action handler so the framework honors `Idempotency-Key`
|
|
26
|
+
* dedup. Acquires the key via {@link IdempotencyStore.claim}, runs
|
|
27
|
+
* the handler exactly when the claim was fresh, and skips the
|
|
28
|
+
* handler entirely on a duplicate.
|
|
29
|
+
*
|
|
30
|
+
* Reuses the contract `@rotorsoft/act-ops/idempotency` already
|
|
31
|
+
* defines for the receiver-side `Idempotency-Key` story. A single
|
|
32
|
+
* `IdempotencyStore` implementation covers both halves of the "Act
|
|
33
|
+
* over the wire" surface — receiver and generated API.
|
|
34
|
+
*/
|
|
35
|
+
export declare function withIdempotency<T>(store: IdempotencyStore, key: string, handler: () => Promise<T>): Promise<IdempotencyResult<T>>;
|
|
36
|
+
//# sourceMappingURL=idempotency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../../src/api/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAEvE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAC3B;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,GAC7B;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,CAAC;AAEtB;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACrC,KAAK,EAAE,gBAAgB,EACvB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAM/B"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
* @module act-http/api
|
|
4
|
+
*
|
|
5
|
+
* Shared utilities for the act-http auto-generated API surfaces.
|
|
6
|
+
* Three concerns that every transport (tRPC, Hono, OpenAPI) has to
|
|
7
|
+
* address — actor extraction, error envelope mapping,
|
|
8
|
+
* `Idempotency-Key` wiring — defined once here and composed by each
|
|
9
|
+
* transport sibling subpath.
|
|
10
|
+
*
|
|
11
|
+
* - {@link ActorExtractor} — the host-supplied closure that resolves
|
|
12
|
+
* an `Actor` from an incoming request. Auth (JWT, session, API
|
|
13
|
+
* key) stays in the host; the package only asks for this one
|
|
14
|
+
* function.
|
|
15
|
+
* - {@link ApiError}, {@link ERROR_MAP}, {@link toApiError} — the
|
|
16
|
+
* uniform error envelope and the status/code mapping every
|
|
17
|
+
* transport uses. Cross-transport consistency by construction.
|
|
18
|
+
* - {@link withIdempotency} — the helper that wraps action handlers
|
|
19
|
+
* in an `Idempotency-Key` claim. Reuses the
|
|
20
|
+
* `@rotorsoft/act-ops/idempotency` contract that
|
|
21
|
+
* `@rotorsoft/act-http/receiver` already speaks, so receivers and
|
|
22
|
+
* generated APIs share one `IdempotencyStore` implementation.
|
|
23
|
+
*
|
|
24
|
+
* Sibling subpaths in the same package consume the utilities here:
|
|
25
|
+
*
|
|
26
|
+
* - `@rotorsoft/act-http/trpc` — tRPC adapter (#843).
|
|
27
|
+
* - `@rotorsoft/act-http/hono` — Hono adapter (#844).
|
|
28
|
+
* - `@rotorsoft/act-http/openapi` — OpenAPI emitter (#845).
|
|
29
|
+
*
|
|
30
|
+
* Existing siblings unrelated to the generated-API work:
|
|
31
|
+
*
|
|
32
|
+
* - `@rotorsoft/act-http/webhook` — outbound POST delivery.
|
|
33
|
+
* - `@rotorsoft/act-http/sse` — incremental state broadcast.
|
|
34
|
+
* - `@rotorsoft/act-http/receiver` — inbound webhook ingestion.
|
|
35
|
+
*/
|
|
36
|
+
export type { ActorExtractor } from "./actor.js";
|
|
37
|
+
export { type ApiError, ERROR_MAP, type ErrorMapEntry, toApiError, } from "./errors.js";
|
|
38
|
+
export { type IdempotencyResult, withIdempotency } from "./idempotency.js";
|
|
39
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACL,KAAK,QAAQ,EACb,SAAS,EACT,KAAK,aAAa,EAClB,UAAU,GACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,KAAK,iBAAiB,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -19,7 +19,7 @@ import type { ReceiverBuilder, ReceiverOptions } from "@rotorsoft/act-ops/receiv
|
|
|
19
19
|
* }), async (event, ctx) => {
|
|
20
20
|
* // event.orderId and event.total are typed
|
|
21
21
|
* // ctx.key is the deduplicated Idempotency-Key
|
|
22
|
-
* await
|
|
22
|
+
* await process_order(event.orderId, event.total);
|
|
23
23
|
* })
|
|
24
24
|
* .build();
|
|
25
25
|
*
|
|
@@ -22,12 +22,12 @@ export type ApplyResult<S extends BroadcastState = BroadcastState> = {
|
|
|
22
22
|
*
|
|
23
23
|
* ```typescript
|
|
24
24
|
* onData: (msg) => {
|
|
25
|
-
* const cached = utils.
|
|
25
|
+
* const cached = utils.get_state.get_data({ streamId });
|
|
26
26
|
* const result = applyPatchMessage(msg, cached);
|
|
27
27
|
* if (result.ok) {
|
|
28
|
-
* utils.
|
|
28
|
+
* utils.get_state.setData({ streamId }, result.state);
|
|
29
29
|
* } else if (result.reason === "behind") {
|
|
30
|
-
* utils.
|
|
30
|
+
* utils.get_state.invalidate({ streamId }); // trigger full refetch
|
|
31
31
|
* }
|
|
32
32
|
* // "stale" → no-op, client already has newer state
|
|
33
33
|
* }
|
|
@@ -25,7 +25,7 @@ import type { BroadcastState, PatchMessage, Subscriber } from "./types.js";
|
|
|
25
25
|
* });
|
|
26
26
|
*
|
|
27
27
|
* // Initial state for reconnects:
|
|
28
|
-
* const cached = broadcast.
|
|
28
|
+
* const cached = broadcast.get_state(streamId);
|
|
29
29
|
* ```
|
|
30
30
|
*
|
|
31
31
|
* ## Version Contract
|
|
@@ -36,9 +36,9 @@ import type { BroadcastState, PatchMessage, Subscriber } from "./types.js";
|
|
|
36
36
|
*/
|
|
37
37
|
export declare class BroadcastChannel<S extends BroadcastState = BroadcastState> {
|
|
38
38
|
private channels;
|
|
39
|
-
private
|
|
39
|
+
private state_cache;
|
|
40
40
|
constructor(options?: {
|
|
41
|
-
|
|
41
|
+
cache_size?: number;
|
|
42
42
|
});
|
|
43
43
|
/**
|
|
44
44
|
* Publish domain patches from a commit.
|
|
@@ -54,16 +54,16 @@ export declare class BroadcastChannel<S extends BroadcastState = BroadcastState>
|
|
|
54
54
|
* (e.g. presence overlay, computed field refresh).
|
|
55
55
|
* Uses the same version as the cached state, single entry.
|
|
56
56
|
*/
|
|
57
|
-
|
|
57
|
+
publish_overlay(streamId: string, overlay_patch: Partial<S>): PatchMessage<S> | undefined;
|
|
58
58
|
/**
|
|
59
59
|
* Subscribe to broadcast messages for a stream.
|
|
60
60
|
* Returns a cleanup function that removes the subscription.
|
|
61
61
|
*/
|
|
62
62
|
subscribe(streamId: string, cb: Subscriber<S>): () => void;
|
|
63
63
|
/** Get the number of subscribers for a stream. */
|
|
64
|
-
|
|
64
|
+
get_subscriber_count(streamId: string): number;
|
|
65
65
|
/** Get the cached state for a stream (for reconnects / initial SSE yield). */
|
|
66
|
-
|
|
66
|
+
get_state(streamId: string): S | undefined;
|
|
67
67
|
/** Direct access to the state cache (for app-specific reads like presence). */
|
|
68
68
|
get cache(): StateCache<S>;
|
|
69
69
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"broadcast.d.ts","sourceRoot":"","sources":["../../../src/sse/broadcast.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,qBAAa,gBAAgB,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc;IACrE,OAAO,CAAC,QAAQ,CAAyC;IACzD,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"broadcast.d.ts","sourceRoot":"","sources":["../../../src/sse/broadcast.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,qBAAa,gBAAgB,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc;IACrE,OAAO,CAAC,QAAQ,CAAyC;IACzD,OAAO,CAAC,WAAW,CAAgB;gBAEvB,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAI7C;;;;;;;OAOG;IACH,OAAO,CACL,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,CAAC,EACR,OAAO,GAAE,OAAO,CAAC,CAAC,CAAC,EAAO,GACzB,YAAY,CAAC,CAAC,CAAC;IAgBlB;;;;OAIG;IACH,eAAe,CACb,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC,GACxB,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS;IAe9B;;;OAGG;IACH,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,MAAM,IAAI;IAW1D,kDAAkD;IAClD,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAI9C,8EAA8E;IAC9E,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAI1C,+EAA+E;IAC/E,IAAI,KAAK,IAAI,UAAU,CAAC,CAAC,CAAC,CAEzB;CACF"}
|
|
@@ -11,24 +11,24 @@
|
|
|
11
11
|
* const presence = new PresenceTracker();
|
|
12
12
|
*
|
|
13
13
|
* // On SSE connect:
|
|
14
|
-
* presence.add(
|
|
14
|
+
* presence.add(game_id, player_id);
|
|
15
15
|
*
|
|
16
16
|
* // On SSE disconnect:
|
|
17
|
-
* presence.remove(
|
|
17
|
+
* presence.remove(game_id, player_id);
|
|
18
18
|
*
|
|
19
19
|
* // Query:
|
|
20
|
-
* presence.
|
|
20
|
+
* presence.get_online(game_id); // Set<string>
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
export declare class PresenceTracker {
|
|
24
24
|
private streams;
|
|
25
25
|
/** Increment ref count for an identity on a stream. */
|
|
26
|
-
add(streamId: string,
|
|
26
|
+
add(streamId: string, identity_id: string): void;
|
|
27
27
|
/** Decrement ref count. Removes the identity when count reaches 0. */
|
|
28
|
-
remove(streamId: string,
|
|
28
|
+
remove(streamId: string, identity_id: string): void;
|
|
29
29
|
/** Get the set of online identity IDs for a stream. */
|
|
30
|
-
|
|
30
|
+
get_online(streamId: string): Set<string>;
|
|
31
31
|
/** Check if a specific identity is online for a stream. */
|
|
32
|
-
|
|
32
|
+
is_online(streamId: string, identity_id: string): boolean;
|
|
33
33
|
}
|
|
34
34
|
//# sourceMappingURL=presence.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../../src/sse/presence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAA0C;IAEzD,uDAAuD;IACvD,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"presence.d.ts","sourceRoot":"","sources":["../../../src/sse/presence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAA0C;IAEzD,uDAAuD;IACvD,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAMhD,sEAAsE;IACtE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IASnD,uDAAuD;IACvD,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC;IAKzC,2DAA2D;IAC3D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO;CAG1D"}
|
|
@@ -22,14 +22,14 @@ export type HttpDisposition = "ok" | "retry" | "block";
|
|
|
22
22
|
* SDK-based reactions, etc.) can apply the same retry semantics
|
|
23
23
|
* without inventing a parallel rule.
|
|
24
24
|
*/
|
|
25
|
-
export declare function
|
|
26
|
-
/** Options for {@link
|
|
25
|
+
export declare function classify_http_response(response: Response): HttpDisposition;
|
|
26
|
+
/** Options for {@link try_ok}. */
|
|
27
27
|
export type TryOkOptions = {
|
|
28
28
|
/** The endpoint that received the request. Surfaced on the thrown error and in its message. */
|
|
29
29
|
url: string;
|
|
30
30
|
/**
|
|
31
31
|
* Label prefixed onto the error message — typically the
|
|
32
|
-
* integration's identity (`"webhook"`, `"
|
|
32
|
+
* integration's identity (`"webhook"`, `"my_sdk"`, `"grpc"`).
|
|
33
33
|
* Default: `"request"`.
|
|
34
34
|
*/
|
|
35
35
|
label?: string;
|
|
@@ -43,8 +43,8 @@ export type TryOkOptions = {
|
|
|
43
43
|
*
|
|
44
44
|
* ```ts
|
|
45
45
|
* .on("OrderConfirmed").do(async (event) => {
|
|
46
|
-
* const response = await
|
|
47
|
-
* await
|
|
46
|
+
* const response = await my_sdk.deliver(event);
|
|
47
|
+
* await try_ok(response, { url: my_sdk.url, label: "my_sdk" });
|
|
48
48
|
* // ...response was 2xx; continue with downstream work...
|
|
49
49
|
* });
|
|
50
50
|
* ```
|
|
@@ -55,5 +55,5 @@ export type TryOkOptions = {
|
|
|
55
55
|
* here, so `instanceof RetryableHttpError` matches both webhook and
|
|
56
56
|
* custom-integration errors uniformly.
|
|
57
57
|
*/
|
|
58
|
-
export declare function
|
|
58
|
+
export declare function try_ok(response: Response, options: TryOkOptions): Promise<void>;
|
|
59
59
|
//# sourceMappingURL=classify.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../../src/webhook/classify.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,OAAO,GAAG,OAAO,CAAC;AAEvD;;;;;;GAMG;AACH,wBAAgB,
|
|
1
|
+
{"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../../src/webhook/classify.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,OAAO,GAAG,OAAO,CAAC;AAEvD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,QAAQ,GAAG,eAAe,CAI1E;AAED,kCAAkC;AAClC,MAAM,MAAM,YAAY,GAAG;IACzB,+FAA+F;IAC/F,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,MAAM,CAC1B,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,IAAI,CAAC,CAmBf"}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { ReactionHandler, Schemas } from "@rotorsoft/act";
|
|
28
28
|
import { type WebhookConfig } from "./types.js";
|
|
29
|
-
export {
|
|
29
|
+
export type { HttpDisposition } from "./classify.js";
|
|
30
30
|
export type { HttpDeliveryErrorInit, WebhookBody, WebhookConfig, WebhookResolver, } from "./types.js";
|
|
31
31
|
export { NonRetryableHttpError, NonRetryableWebhookError, RetryableHttpError, WebhookError, } from "./types.js";
|
|
32
32
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/webhook/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAa,eAAe,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAG1E,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,YAAY,CAAC;AAEpB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/webhook/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAa,eAAe,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAG1E,OAAO,EAEL,KAAK,aAAa,EAEnB,MAAM,YAAY,CAAC;AAEpB,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,YAAY,EACV,qBAAqB,EACrB,WAAW,EACX,aAAa,EACb,eAAe,GAChB,MAAM,YAAY,CAAC;AACpB,OAAO,EACL,qBAAqB,EACrB,wBAAwB,EACxB,kBAAkB,EAClB,YAAY,GACb,MAAM,YAAY,CAAC;AAsBpB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,OAAO,SAAS,OAAO,GAAG,OAAO,EACvD,MAAM,EAAE,aAAa,CAAC,OAAO,CAAC,GAC7B,eAAe,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,CA+EzC"}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* from the package's `./webhook` entry — the webhook helper calls
|
|
19
19
|
* it internally, and operators don't need it directly.
|
|
20
20
|
*/
|
|
21
|
-
export declare function
|
|
21
|
+
export declare function sign_request(body: string, secret: string, now?: number): {
|
|
22
22
|
signature: string;
|
|
23
23
|
timestamp: string;
|
|
24
24
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../../../src/webhook/sign.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,
|
|
1
|
+
{"version":3,"file":"sign.d.ts","sourceRoot":"","sources":["../../../src/webhook/sign.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,MAAsC,GAC1C;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAK1C"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/api/index.ts
|
|
21
|
+
var api_exports = {};
|
|
22
|
+
__export(api_exports, {
|
|
23
|
+
ERROR_MAP: () => ERROR_MAP,
|
|
24
|
+
toApiError: () => toApiError,
|
|
25
|
+
withIdempotency: () => withIdempotency
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(api_exports);
|
|
28
|
+
|
|
29
|
+
// src/api/errors.ts
|
|
30
|
+
var import_act = require("@rotorsoft/act");
|
|
31
|
+
var ERROR_MAP = {
|
|
32
|
+
ValidationError: { status: 422, code: "VALIDATION" },
|
|
33
|
+
InvariantError: { status: 409, code: "INVARIANT" },
|
|
34
|
+
ConcurrencyError: { status: 412, code: "CONCURRENCY" },
|
|
35
|
+
StreamClosedError: { status: 410, code: "STREAM_CLOSED" },
|
|
36
|
+
NonRetryableError: { status: 400, code: "NON_RETRYABLE" }
|
|
37
|
+
};
|
|
38
|
+
var lookup_known = (err) => {
|
|
39
|
+
if (err instanceof import_act.ValidationError) return { name: "ValidationError" };
|
|
40
|
+
if (err instanceof import_act.InvariantError) return { name: "InvariantError" };
|
|
41
|
+
if (err instanceof import_act.ConcurrencyError) return { name: "ConcurrencyError" };
|
|
42
|
+
if (err instanceof import_act.StreamClosedError) return { name: "StreamClosedError" };
|
|
43
|
+
if (err instanceof import_act.NonRetryableError) return { name: "NonRetryableError" };
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
function toApiError(err) {
|
|
47
|
+
const known = lookup_known(err);
|
|
48
|
+
if (known) {
|
|
49
|
+
const entry = ERROR_MAP[known.name];
|
|
50
|
+
return {
|
|
51
|
+
status: entry.status,
|
|
52
|
+
body: {
|
|
53
|
+
error: known.name,
|
|
54
|
+
detail: err.message,
|
|
55
|
+
code: entry.code
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (err instanceof Error) {
|
|
60
|
+
return {
|
|
61
|
+
status: 500,
|
|
62
|
+
body: { error: "InternalError", detail: err.message, code: "INTERNAL" }
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
status: 500,
|
|
67
|
+
body: { error: "InternalError", code: "INTERNAL" }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/api/idempotency.ts
|
|
72
|
+
async function withIdempotency(store, key, handler) {
|
|
73
|
+
const fresh = await store.claim(key);
|
|
74
|
+
if (!fresh) {
|
|
75
|
+
return { deduped: true };
|
|
76
|
+
}
|
|
77
|
+
return { deduped: false, result: await handler() };
|
|
78
|
+
}
|
|
79
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
80
|
+
0 && (module.exports = {
|
|
81
|
+
ERROR_MAP,
|
|
82
|
+
toApiError,
|
|
83
|
+
withIdempotency
|
|
84
|
+
});
|
|
85
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/index.ts","../../src/api/errors.ts","../../src/api/idempotency.ts"],"sourcesContent":["/**\n * @packageDocumentation\n * @module act-http/api\n *\n * Shared utilities for the act-http auto-generated API surfaces.\n * Three concerns that every transport (tRPC, Hono, OpenAPI) has to\n * address — actor extraction, error envelope mapping,\n * `Idempotency-Key` wiring — defined once here and composed by each\n * transport sibling subpath.\n *\n * - {@link ActorExtractor} — the host-supplied closure that resolves\n * an `Actor` from an incoming request. Auth (JWT, session, API\n * key) stays in the host; the package only asks for this one\n * function.\n * - {@link ApiError}, {@link ERROR_MAP}, {@link toApiError} — the\n * uniform error envelope and the status/code mapping every\n * transport uses. Cross-transport consistency by construction.\n * - {@link withIdempotency} — the helper that wraps action handlers\n * in an `Idempotency-Key` claim. Reuses the\n * `@rotorsoft/act-ops/idempotency` contract that\n * `@rotorsoft/act-http/receiver` already speaks, so receivers and\n * generated APIs share one `IdempotencyStore` implementation.\n *\n * Sibling subpaths in the same package consume the utilities here:\n *\n * - `@rotorsoft/act-http/trpc` — tRPC adapter (#843).\n * - `@rotorsoft/act-http/hono` — Hono adapter (#844).\n * - `@rotorsoft/act-http/openapi` — OpenAPI emitter (#845).\n *\n * Existing siblings unrelated to the generated-API work:\n *\n * - `@rotorsoft/act-http/webhook` — outbound POST delivery.\n * - `@rotorsoft/act-http/sse` — incremental state broadcast.\n * - `@rotorsoft/act-http/receiver` — inbound webhook ingestion.\n */\n\nexport type { ActorExtractor } from \"./actor.js\";\nexport {\n type ApiError,\n ERROR_MAP,\n type ErrorMapEntry,\n toApiError,\n} from \"./errors.js\";\nexport { type IdempotencyResult, withIdempotency } from \"./idempotency.js\";\n","import {\n ConcurrencyError,\n InvariantError,\n NonRetryableError,\n StreamClosedError,\n ValidationError,\n} from \"@rotorsoft/act\";\n\n/**\n * Uniform error envelope shipped over the wire by every act-http\n * transport. Hosts get the same shape from REST, tRPC, and OpenAPI —\n * a client that talks to two transports doesn't have to invent two\n * error parsers.\n *\n * - `error` — the framework error name (`\"ValidationError\"`,\n * `\"InvariantError\"`, …). Stable identifier, safe to switch on.\n * - `detail` — the framework's message text. Human-readable; not\n * parsed by clients.\n * - `code` — a machine-readable status code from {@link ERROR_MAP}\n * for clients that prefer enum-style branching over name strings.\n */\nexport type ApiError = {\n error: string;\n detail?: string;\n code?: string;\n};\n\n/**\n * Status + code pair for one known framework error.\n */\nexport type ErrorMapEntry = {\n status: number;\n code: string;\n};\n\n/**\n * The single table that maps framework error types to HTTP status\n * codes and machine-readable codes. One table, three consumers\n * (Hono, tRPC, OpenAPI) — cross-transport consistency by\n * construction.\n *\n * Operators wanting different mappings wrap the generated transport\n * rather than mutating this — the consistency is the load-bearing\n * property, not the specific status codes.\n */\nexport const ERROR_MAP = {\n ValidationError: { status: 422, code: \"VALIDATION\" },\n InvariantError: { status: 409, code: \"INVARIANT\" },\n ConcurrencyError: { status: 412, code: \"CONCURRENCY\" },\n StreamClosedError: { status: 410, code: \"STREAM_CLOSED\" },\n NonRetryableError: { status: 400, code: \"NON_RETRYABLE\" },\n} as const satisfies Record<string, ErrorMapEntry>;\n\nconst lookup_known = (\n err: unknown\n): { name: keyof typeof ERROR_MAP } | null => {\n if (err instanceof ValidationError) return { name: \"ValidationError\" };\n if (err instanceof InvariantError) return { name: \"InvariantError\" };\n if (err instanceof ConcurrencyError) return { name: \"ConcurrencyError\" };\n if (err instanceof StreamClosedError) return { name: \"StreamClosedError\" };\n if (err instanceof NonRetryableError) return { name: \"NonRetryableError\" };\n return null;\n};\n\n/**\n * Translate an unknown thrown value into the canonical\n * {@link ApiError} envelope plus HTTP status. Each transport's error\n * boundary calls this once and forwards the result to the wire.\n *\n * Known framework errors map per {@link ERROR_MAP}. Everything else\n * surfaces as a 500 with `code: \"INTERNAL\"`; the `detail` field is\n * populated when the throw was an `Error` instance, omitted\n * otherwise (a thrown string or object doesn't get to leak its\n * payload to the client).\n */\nexport function toApiError(err: unknown): { status: number; body: ApiError } {\n const known = lookup_known(err);\n if (known) {\n const entry = ERROR_MAP[known.name];\n return {\n status: entry.status,\n body: {\n error: known.name,\n detail: (err as Error).message,\n code: entry.code,\n },\n };\n }\n if (err instanceof Error) {\n return {\n status: 500,\n body: { error: \"InternalError\", detail: err.message, code: \"INTERNAL\" },\n };\n }\n return {\n status: 500,\n body: { error: \"InternalError\", code: \"INTERNAL\" },\n };\n}\n","import type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\n\n/**\n * Result of a {@link withIdempotency} call.\n *\n * - `{ deduped: false, result }` — the claim was fresh; the handler\n * ran and produced `result`.\n * - `{ deduped: true }` — the claim was already taken; the handler\n * was *not* invoked. The caller decides how to respond (typically\n * a 2xx with no body, matching the receiver-side convention).\n *\n * Note: the contract does not cache the previous response. A\n * duplicate call returns the deduped marker only — replaying the\n * original handler's output would require a response-caching\n * adapter, which is out of scope here. The receiver-side convention\n * (and the convention the generated transports follow) is \"ack the\n * duplicate; do nothing else.\"\n */\nexport type IdempotencyResult<T> =\n | { deduped: false; result: T }\n | { deduped: true };\n\n/**\n * Wrap an action handler so the framework honors `Idempotency-Key`\n * dedup. Acquires the key via {@link IdempotencyStore.claim}, runs\n * the handler exactly when the claim was fresh, and skips the\n * handler entirely on a duplicate.\n *\n * Reuses the contract `@rotorsoft/act-ops/idempotency` already\n * defines for the receiver-side `Idempotency-Key` story. A single\n * `IdempotencyStore` implementation covers both halves of the \"Act\n * over the wire\" surface — receiver and generated API.\n */\nexport async function withIdempotency<T>(\n store: IdempotencyStore,\n key: string,\n handler: () => Promise<T>\n): Promise<IdempotencyResult<T>> {\n const fresh = await store.claim(key);\n if (!fresh) {\n return { deduped: true };\n }\n return { deduped: false, result: await handler() };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,iBAMO;AAuCA,IAAM,YAAY;AAAA,EACvB,iBAAiB,EAAE,QAAQ,KAAK,MAAM,aAAa;AAAA,EACnD,gBAAgB,EAAE,QAAQ,KAAK,MAAM,YAAY;AAAA,EACjD,kBAAkB,EAAE,QAAQ,KAAK,MAAM,cAAc;AAAA,EACrD,mBAAmB,EAAE,QAAQ,KAAK,MAAM,gBAAgB;AAAA,EACxD,mBAAmB,EAAE,QAAQ,KAAK,MAAM,gBAAgB;AAC1D;AAEA,IAAM,eAAe,CACnB,QAC4C;AAC5C,MAAI,eAAe,2BAAiB,QAAO,EAAE,MAAM,kBAAkB;AACrE,MAAI,eAAe,0BAAgB,QAAO,EAAE,MAAM,iBAAiB;AACnE,MAAI,eAAe,4BAAkB,QAAO,EAAE,MAAM,mBAAmB;AACvE,MAAI,eAAe,6BAAmB,QAAO,EAAE,MAAM,oBAAoB;AACzE,MAAI,eAAe,6BAAmB,QAAO,EAAE,MAAM,oBAAoB;AACzE,SAAO;AACT;AAaO,SAAS,WAAW,KAAkD;AAC3E,QAAM,QAAQ,aAAa,GAAG;AAC9B,MAAI,OAAO;AACT,UAAM,QAAQ,UAAU,MAAM,IAAI;AAClC,WAAO;AAAA,MACL,QAAQ,MAAM;AAAA,MACd,MAAM;AAAA,QACJ,OAAO,MAAM;AAAA,QACb,QAAS,IAAc;AAAA,QACvB,MAAM,MAAM;AAAA,MACd;AAAA,IACF;AAAA,EACF;AACA,MAAI,eAAe,OAAO;AACxB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM,EAAE,OAAO,iBAAiB,QAAQ,IAAI,SAAS,MAAM,WAAW;AAAA,IACxE;AAAA,EACF;AACA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,MAAM,EAAE,OAAO,iBAAiB,MAAM,WAAW;AAAA,EACnD;AACF;;;ACjEA,eAAsB,gBACpB,OACA,KACA,SAC+B;AAC/B,QAAM,QAAQ,MAAM,MAAM,MAAM,GAAG;AACnC,MAAI,CAAC,OAAO;AACV,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AACA,SAAO,EAAE,SAAS,OAAO,QAAQ,MAAM,QAAQ,EAAE;AACnD;","names":[]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/api/errors.ts
|
|
2
|
+
import {
|
|
3
|
+
ConcurrencyError,
|
|
4
|
+
InvariantError,
|
|
5
|
+
NonRetryableError,
|
|
6
|
+
StreamClosedError,
|
|
7
|
+
ValidationError
|
|
8
|
+
} from "@rotorsoft/act";
|
|
9
|
+
var ERROR_MAP = {
|
|
10
|
+
ValidationError: { status: 422, code: "VALIDATION" },
|
|
11
|
+
InvariantError: { status: 409, code: "INVARIANT" },
|
|
12
|
+
ConcurrencyError: { status: 412, code: "CONCURRENCY" },
|
|
13
|
+
StreamClosedError: { status: 410, code: "STREAM_CLOSED" },
|
|
14
|
+
NonRetryableError: { status: 400, code: "NON_RETRYABLE" }
|
|
15
|
+
};
|
|
16
|
+
var lookup_known = (err) => {
|
|
17
|
+
if (err instanceof ValidationError) return { name: "ValidationError" };
|
|
18
|
+
if (err instanceof InvariantError) return { name: "InvariantError" };
|
|
19
|
+
if (err instanceof ConcurrencyError) return { name: "ConcurrencyError" };
|
|
20
|
+
if (err instanceof StreamClosedError) return { name: "StreamClosedError" };
|
|
21
|
+
if (err instanceof NonRetryableError) return { name: "NonRetryableError" };
|
|
22
|
+
return null;
|
|
23
|
+
};
|
|
24
|
+
function toApiError(err) {
|
|
25
|
+
const known = lookup_known(err);
|
|
26
|
+
if (known) {
|
|
27
|
+
const entry = ERROR_MAP[known.name];
|
|
28
|
+
return {
|
|
29
|
+
status: entry.status,
|
|
30
|
+
body: {
|
|
31
|
+
error: known.name,
|
|
32
|
+
detail: err.message,
|
|
33
|
+
code: entry.code
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (err instanceof Error) {
|
|
38
|
+
return {
|
|
39
|
+
status: 500,
|
|
40
|
+
body: { error: "InternalError", detail: err.message, code: "INTERNAL" }
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
status: 500,
|
|
45
|
+
body: { error: "InternalError", code: "INTERNAL" }
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/api/idempotency.ts
|
|
50
|
+
async function withIdempotency(store, key, handler) {
|
|
51
|
+
const fresh = await store.claim(key);
|
|
52
|
+
if (!fresh) {
|
|
53
|
+
return { deduped: true };
|
|
54
|
+
}
|
|
55
|
+
return { deduped: false, result: await handler() };
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
ERROR_MAP,
|
|
59
|
+
toApiError,
|
|
60
|
+
withIdempotency
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/api/errors.ts","../../src/api/idempotency.ts"],"sourcesContent":["import {\n ConcurrencyError,\n InvariantError,\n NonRetryableError,\n StreamClosedError,\n ValidationError,\n} from \"@rotorsoft/act\";\n\n/**\n * Uniform error envelope shipped over the wire by every act-http\n * transport. Hosts get the same shape from REST, tRPC, and OpenAPI —\n * a client that talks to two transports doesn't have to invent two\n * error parsers.\n *\n * - `error` — the framework error name (`\"ValidationError\"`,\n * `\"InvariantError\"`, …). Stable identifier, safe to switch on.\n * - `detail` — the framework's message text. Human-readable; not\n * parsed by clients.\n * - `code` — a machine-readable status code from {@link ERROR_MAP}\n * for clients that prefer enum-style branching over name strings.\n */\nexport type ApiError = {\n error: string;\n detail?: string;\n code?: string;\n};\n\n/**\n * Status + code pair for one known framework error.\n */\nexport type ErrorMapEntry = {\n status: number;\n code: string;\n};\n\n/**\n * The single table that maps framework error types to HTTP status\n * codes and machine-readable codes. One table, three consumers\n * (Hono, tRPC, OpenAPI) — cross-transport consistency by\n * construction.\n *\n * Operators wanting different mappings wrap the generated transport\n * rather than mutating this — the consistency is the load-bearing\n * property, not the specific status codes.\n */\nexport const ERROR_MAP = {\n ValidationError: { status: 422, code: \"VALIDATION\" },\n InvariantError: { status: 409, code: \"INVARIANT\" },\n ConcurrencyError: { status: 412, code: \"CONCURRENCY\" },\n StreamClosedError: { status: 410, code: \"STREAM_CLOSED\" },\n NonRetryableError: { status: 400, code: \"NON_RETRYABLE\" },\n} as const satisfies Record<string, ErrorMapEntry>;\n\nconst lookup_known = (\n err: unknown\n): { name: keyof typeof ERROR_MAP } | null => {\n if (err instanceof ValidationError) return { name: \"ValidationError\" };\n if (err instanceof InvariantError) return { name: \"InvariantError\" };\n if (err instanceof ConcurrencyError) return { name: \"ConcurrencyError\" };\n if (err instanceof StreamClosedError) return { name: \"StreamClosedError\" };\n if (err instanceof NonRetryableError) return { name: \"NonRetryableError\" };\n return null;\n};\n\n/**\n * Translate an unknown thrown value into the canonical\n * {@link ApiError} envelope plus HTTP status. Each transport's error\n * boundary calls this once and forwards the result to the wire.\n *\n * Known framework errors map per {@link ERROR_MAP}. Everything else\n * surfaces as a 500 with `code: \"INTERNAL\"`; the `detail` field is\n * populated when the throw was an `Error` instance, omitted\n * otherwise (a thrown string or object doesn't get to leak its\n * payload to the client).\n */\nexport function toApiError(err: unknown): { status: number; body: ApiError } {\n const known = lookup_known(err);\n if (known) {\n const entry = ERROR_MAP[known.name];\n return {\n status: entry.status,\n body: {\n error: known.name,\n detail: (err as Error).message,\n code: entry.code,\n },\n };\n }\n if (err instanceof Error) {\n return {\n status: 500,\n body: { error: \"InternalError\", detail: err.message, code: \"INTERNAL\" },\n };\n }\n return {\n status: 500,\n body: { error: \"InternalError\", code: \"INTERNAL\" },\n };\n}\n","import type { IdempotencyStore } from \"@rotorsoft/act-ops/idempotency\";\n\n/**\n * Result of a {@link withIdempotency} call.\n *\n * - `{ deduped: false, result }` — the claim was fresh; the handler\n * ran and produced `result`.\n * - `{ deduped: true }` — the claim was already taken; the handler\n * was *not* invoked. The caller decides how to respond (typically\n * a 2xx with no body, matching the receiver-side convention).\n *\n * Note: the contract does not cache the previous response. A\n * duplicate call returns the deduped marker only — replaying the\n * original handler's output would require a response-caching\n * adapter, which is out of scope here. The receiver-side convention\n * (and the convention the generated transports follow) is \"ack the\n * duplicate; do nothing else.\"\n */\nexport type IdempotencyResult<T> =\n | { deduped: false; result: T }\n | { deduped: true };\n\n/**\n * Wrap an action handler so the framework honors `Idempotency-Key`\n * dedup. Acquires the key via {@link IdempotencyStore.claim}, runs\n * the handler exactly when the claim was fresh, and skips the\n * handler entirely on a duplicate.\n *\n * Reuses the contract `@rotorsoft/act-ops/idempotency` already\n * defines for the receiver-side `Idempotency-Key` story. A single\n * `IdempotencyStore` implementation covers both halves of the \"Act\n * over the wire\" surface — receiver and generated API.\n */\nexport async function withIdempotency<T>(\n store: IdempotencyStore,\n key: string,\n handler: () => Promise<T>\n): Promise<IdempotencyResult<T>> {\n const fresh = await store.claim(key);\n if (!fresh) {\n return { deduped: true };\n }\n return { deduped: false, result: await handler() };\n}\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAuCA,IAAM,YAAY;AAAA,EACvB,iBAAiB,EAAE,QAAQ,KAAK,MAAM,aAAa;AAAA,EACnD,gBAAgB,EAAE,QAAQ,KAAK,MAAM,YAAY;AAAA,EACjD,kBAAkB,EAAE,QAAQ,KAAK,MAAM,cAAc;AAAA,EACrD,mBAAmB,EAAE,QAAQ,KAAK,MAAM,gBAAgB;AAAA,EACxD,mBAAmB,EAAE,QAAQ,KAAK,MAAM,gBAAgB;AAC1D;AAEA,IAAM,eAAe,CACnB,QAC4C;AAC5C,MAAI,eAAe,gBAAiB,QAAO,EAAE,MAAM,kBAAkB;AACrE,MAAI,eAAe,eAAgB,QAAO,EAAE,MAAM,iBAAiB;AACnE,MAAI,eAAe,iBAAkB,QAAO,EAAE,MAAM,mBAAmB;AACvE,MAAI,eAAe,kBAAmB,QAAO,EAAE,MAAM,oBAAoB;AACzE,MAAI,eAAe,kBAAmB,QAAO,EAAE,MAAM,oBAAoB;AACzE,SAAO;AACT;AAaO,SAAS,WAAW,KAAkD;AAC3E,QAAM,QAAQ,aAAa,GAAG;AAC9B,MAAI,OAAO;AACT,UAAM,QAAQ,UAAU,MAAM,IAAI;AAClC,WAAO;AAAA,MACL,QAAQ,MAAM;AAAA,MACd,MAAM;AAAA,QACJ,OAAO,MAAM;AAAA,QACb,QAAS,IAAc;AAAA,QACvB,MAAM,MAAM;AAAA,MACd;AAAA,IACF;AAAA,EACF;AACA,MAAI,eAAe,OAAO;AACxB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM,EAAE,OAAO,iBAAiB,QAAQ,IAAI,SAAS,MAAM,WAAW;AAAA,IACxE;AAAA,EACF;AACA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,MAAM,EAAE,OAAO,iBAAiB,MAAM,WAAW;AAAA,EACnD;AACF;;;ACjEA,eAAsB,gBACpB,OACA,KACA,SAC+B;AAC/B,QAAM,QAAQ,MAAM,MAAM,MAAM,GAAG;AACnC,MAAI,CAAC,OAAO;AACV,WAAO,EAAE,SAAS,KAAK;AAAA,EACzB;AACA,SAAO,EAAE,SAAS,OAAO,QAAQ,MAAM,QAAQ,EAAE;AACnD;","names":[]}
|
|
@@ -14,12 +14,12 @@ import { createHmac, timingSafeEqual } from "crypto";
|
|
|
14
14
|
function verifyWebhook(headers, body, secret, options) {
|
|
15
15
|
const maxAgeSeconds = options?.maxAgeSeconds ?? 300;
|
|
16
16
|
const now = options?.now ?? Math.floor(Date.now() / 1e3);
|
|
17
|
-
const signature =
|
|
17
|
+
const signature = pick_header(headers, "x-webhook-signature");
|
|
18
18
|
if (!signature) return { ok: false, reason: "missing-signature" };
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
21
|
-
const timestamp = Number.parseInt(
|
|
22
|
-
if (Number.isNaN(timestamp) || String(timestamp) !==
|
|
19
|
+
const timestamp_str = pick_header(headers, "x-webhook-timestamp");
|
|
20
|
+
if (!timestamp_str) return { ok: false, reason: "missing-timestamp" };
|
|
21
|
+
const timestamp = Number.parseInt(timestamp_str, 10);
|
|
22
|
+
if (Number.isNaN(timestamp) || String(timestamp) !== timestamp_str) {
|
|
23
23
|
return { ok: false, reason: "missing-timestamp" };
|
|
24
24
|
}
|
|
25
25
|
const delta = now - timestamp;
|
|
@@ -28,21 +28,21 @@ function verifyWebhook(headers, body, secret, options) {
|
|
|
28
28
|
if (!signature.startsWith("sha256=")) {
|
|
29
29
|
return { ok: false, reason: "bad-signature" };
|
|
30
30
|
}
|
|
31
|
-
const
|
|
32
|
-
if (!/^[0-9a-fA-F]{64}$/.test(
|
|
31
|
+
const provided_hex = signature.slice("sha256=".length);
|
|
32
|
+
if (!/^[0-9a-fA-F]{64}$/.test(provided_hex)) {
|
|
33
33
|
return { ok: false, reason: "bad-signature" };
|
|
34
34
|
}
|
|
35
|
-
const
|
|
36
|
-
const a = Buffer.from(
|
|
37
|
-
const b = Buffer.from(
|
|
35
|
+
const expected_hex = createHmac("sha256", secret).update(`${timestamp_str}.${body}`).digest("hex");
|
|
36
|
+
const a = Buffer.from(provided_hex, "hex");
|
|
37
|
+
const b = Buffer.from(expected_hex, "hex");
|
|
38
38
|
if (!timingSafeEqual(a, b)) {
|
|
39
39
|
return { ok: false, reason: "bad-signature" };
|
|
40
40
|
}
|
|
41
41
|
return { ok: true };
|
|
42
42
|
}
|
|
43
|
-
function
|
|
43
|
+
function pick_header(headers, lower_name) {
|
|
44
44
|
for (const [name, value] of Object.entries(headers)) {
|
|
45
|
-
if (name.toLowerCase() !==
|
|
45
|
+
if (name.toLowerCase() !== lower_name) continue;
|
|
46
46
|
if (Array.isArray(value) || value === void 0 || value === "") {
|
|
47
47
|
return void 0;
|
|
48
48
|
}
|
|
@@ -75,4 +75,4 @@ export {
|
|
|
75
75
|
verifyWebhook,
|
|
76
76
|
checkWebhook
|
|
77
77
|
};
|
|
78
|
-
//# sourceMappingURL=chunk-
|
|
78
|
+
//# sourceMappingURL=chunk-4CGAUB5H.js.map
|