@daloyjs/core 0.35.2 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -2
- package/bin/daloy.mjs +2 -0
- package/dist/adapters/bun.js +16 -9
- package/dist/adapters/deno.js +7 -1
- package/dist/adapters/node.d.ts +11 -0
- package/dist/adapters/node.js +24 -0
- package/dist/app.d.ts +223 -1
- package/dist/app.js +358 -8
- package/dist/asyncapi.d.ts +98 -0
- package/dist/asyncapi.js +212 -0
- package/dist/auto-ban.d.ts +205 -0
- package/dist/auto-ban.js +222 -0
- package/dist/bot-guard.d.ts +209 -0
- package/dist/bot-guard.js +291 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +88 -4
- package/dist/concurrency-limit.d.ts +135 -0
- package/dist/concurrency-limit.js +254 -0
- package/dist/docs.d.ts +57 -6
- package/dist/docs.js +34 -3
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +27 -0
- package/dist/fetch-guard.js +4 -0
- package/dist/fetch-resilience.d.ts +295 -0
- package/dist/fetch-resilience.js +485 -0
- package/dist/geo-block.d.ts +184 -0
- package/dist/geo-block.js +153 -0
- package/dist/hashing.d.ts +2 -1
- package/dist/hashing.js +12 -1
- package/dist/http-signatures.d.ts +303 -0
- package/dist/http-signatures.js +782 -0
- package/dist/idempotency.d.ts +204 -0
- package/dist/idempotency.js +341 -0
- package/dist/index.d.ts +38 -4
- package/dist/index.js +18 -1
- package/dist/ip-reputation.d.ts +198 -0
- package/dist/ip-reputation.js +253 -0
- package/dist/jwk.d.ts +15 -0
- package/dist/jwk.js +24 -2
- package/dist/load-shedding.d.ts +5 -0
- package/dist/logger.js +6 -2
- package/dist/metrics.d.ts +208 -0
- package/dist/metrics.js +452 -0
- package/dist/middleware.js +0 -10
- package/dist/mtls.d.ts +266 -0
- package/dist/mtls.js +488 -0
- package/dist/multipart.js +1 -1
- package/dist/openapi-diff.d.ts +79 -0
- package/dist/openapi-diff.js +246 -0
- package/dist/openapi.js +4 -1
- package/dist/pagination.d.ts +210 -0
- package/dist/pagination.js +353 -0
- package/dist/rate-limit-redis.d.ts +8 -0
- package/dist/rate-limit-redis.js +8 -0
- package/dist/request-decompression.d.ts +200 -0
- package/dist/request-decompression.js +363 -0
- package/dist/response-cache.d.ts +205 -0
- package/dist/response-cache.js +374 -0
- package/dist/router.d.ts +22 -0
- package/dist/router.js +64 -7
- package/dist/safe-redirect.d.ts +2 -2
- package/dist/safe-redirect.js +3 -8
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/scheduler.d.ts +315 -0
- package/dist/scheduler.js +546 -0
- package/dist/security.d.ts +27 -7
- package/dist/security.js +27 -7
- package/dist/session.js +3 -3
- package/dist/types.d.ts +33 -0
- package/dist/waf.d.ts +213 -0
- package/dist/waf.js +334 -0
- package/dist/webhook-delivery.d.ts +263 -0
- package/dist/webhook-delivery.js +311 -0
- package/dist/websocket.d.ts +52 -0
- package/dist/websocket.js +13 -0
- package/package.json +76 -2
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-class `Idempotency-Key` handling for DaloyJS.
|
|
3
|
+
*
|
|
4
|
+
* The {@link idempotency} middleware lets clients safely retry unsafe requests
|
|
5
|
+
* (`POST`, `PUT`, `PATCH`, `DELETE`) without risking duplicate side effects —
|
|
6
|
+
* the table-stakes guarantee for payment surfaces and serverless retries. It
|
|
7
|
+
* mirrors the IETF *The Idempotency-Key HTTP Header Field* draft and the
|
|
8
|
+
* conventions used by major payment processors:
|
|
9
|
+
*
|
|
10
|
+
* 1. The client sends a unique, client-generated `Idempotency-Key` header.
|
|
11
|
+
* 2. On the **first** request the handler runs normally; the framework
|
|
12
|
+
* fingerprints the request (method + path + body) and persists the final
|
|
13
|
+
* response keyed by the idempotency key.
|
|
14
|
+
* 3. On a **retry** carrying the same key and the same fingerprint, the stored
|
|
15
|
+
* response is replayed byte-for-byte (with an `Idempotency-Replayed: true`
|
|
16
|
+
* marker) — the handler never runs twice.
|
|
17
|
+
* 4. A retry that arrives **while the original is still in flight** gets a
|
|
18
|
+
* `409 Conflict` so the client backs off instead of racing.
|
|
19
|
+
* 5. Reusing a key with a **different** request body returns `422
|
|
20
|
+
* Unprocessable Content` — a key is permanently bound to its first payload.
|
|
21
|
+
*
|
|
22
|
+
* The store is pluggable via {@link IdempotencyStore}, mirroring the
|
|
23
|
+
* `SessionStore` / rate-limit-store pattern. The default
|
|
24
|
+
* {@link MemoryIdempotencyStore} is process-local; supply a shared backend
|
|
25
|
+
* (e.g. Redis) for multi-instance deployments.
|
|
26
|
+
*
|
|
27
|
+
* This module is dependency-free and uses only Web Crypto + Web Standard
|
|
28
|
+
* `Request`/`Response`, so it runs unchanged on Node, Bun, Deno, Cloudflare
|
|
29
|
+
* Workers, and Vercel Edge.
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
* @since 0.37.0
|
|
33
|
+
*/
|
|
34
|
+
import type { Hooks } from "./types.js";
|
|
35
|
+
/**
|
|
36
|
+
* Test-only helper that clears the process-wide shared stores used by
|
|
37
|
+
* `idempotency({ groupId })`. Not part of the documented public API.
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export declare function _resetSharedIdempotencyStoresForTests(): void;
|
|
42
|
+
/**
|
|
43
|
+
* A captured HTTP response persisted for replay. The body is stored as
|
|
44
|
+
* standard base64 so arbitrary binary payloads round-trip safely.
|
|
45
|
+
*/
|
|
46
|
+
export interface StoredIdempotentResponse {
|
|
47
|
+
/** HTTP status code of the original response. */
|
|
48
|
+
status: number;
|
|
49
|
+
/** Response headers as `[name, value]` pairs (lower-cased by `Headers`). */
|
|
50
|
+
headers: Array<[string, string]>;
|
|
51
|
+
/** Base64-encoded response body (empty string for a bodyless response). */
|
|
52
|
+
body: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A persisted idempotency entry. An entry is first written as `in-flight`
|
|
56
|
+
* while the handler runs, then upgraded to `completed` (carrying the captured
|
|
57
|
+
* {@link StoredIdempotentResponse}) once the response is produced.
|
|
58
|
+
*/
|
|
59
|
+
export interface IdempotencyRecord {
|
|
60
|
+
/** SHA-256 hex fingerprint of the originating request (method + path + body). */
|
|
61
|
+
fingerprint: string;
|
|
62
|
+
/** Lifecycle state of the reservation. */
|
|
63
|
+
status: "in-flight" | "completed";
|
|
64
|
+
/** Captured response, present only when `status` is `"completed"`. */
|
|
65
|
+
response?: StoredIdempotentResponse;
|
|
66
|
+
/** Creation time as ms since epoch. */
|
|
67
|
+
createdAt: number;
|
|
68
|
+
/** Absolute expiration as ms since epoch. */
|
|
69
|
+
expiresAt: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Pluggable persistence backend for {@link idempotency}. All methods may be
|
|
73
|
+
* synchronous or asynchronous. The contract mirrors `SessionStore` /
|
|
74
|
+
* `RateLimitStore`: implementations should treat an `expiresAt` in the past as
|
|
75
|
+
* "missing" and may lazily delete expired records.
|
|
76
|
+
*
|
|
77
|
+
* The {@link reserve} method MUST be atomic ("set if absent") so two
|
|
78
|
+
* concurrent requests carrying the same key cannot both win the reservation —
|
|
79
|
+
* exactly the `SET key value NX` semantics of a Redis backend.
|
|
80
|
+
*/
|
|
81
|
+
export interface IdempotencyStore {
|
|
82
|
+
/**
|
|
83
|
+
* Atomically reserve `key` for an in-flight request. If `key` is unused,
|
|
84
|
+
* persist `record` with the given TTL and return `null` (the caller now owns
|
|
85
|
+
* the key). If `key` already exists (in-flight or completed), return the
|
|
86
|
+
* existing record **without modifying it**.
|
|
87
|
+
*
|
|
88
|
+
* @param key - The (already namespaced) storage key.
|
|
89
|
+
* @param record - The in-flight record to persist on a successful reservation.
|
|
90
|
+
* @param ttlMs - Time-to-live in milliseconds for the reservation.
|
|
91
|
+
*/
|
|
92
|
+
reserve(key: string, record: IdempotencyRecord, ttlMs: number): IdempotencyRecord | null | Promise<IdempotencyRecord | null>;
|
|
93
|
+
/**
|
|
94
|
+
* Persist the final response for a previously reserved key, upgrading it to
|
|
95
|
+
* `completed` so subsequent retries replay it.
|
|
96
|
+
*
|
|
97
|
+
* @param key - The (already namespaced) storage key.
|
|
98
|
+
* @param record - The completed record carrying the captured response.
|
|
99
|
+
* @param ttlMs - Time-to-live in milliseconds for the stored response.
|
|
100
|
+
*/
|
|
101
|
+
complete(key: string, record: IdempotencyRecord, ttlMs: number): void | Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Release a reservation so the client may retry. Called when the handler
|
|
104
|
+
* produces a non-cacheable response (e.g. `5xx`) or throws.
|
|
105
|
+
*
|
|
106
|
+
* @param key - The (already namespaced) storage key.
|
|
107
|
+
*/
|
|
108
|
+
release(key: string): void | Promise<void>;
|
|
109
|
+
}
|
|
110
|
+
/** Options for the {@link idempotency} middleware. */
|
|
111
|
+
export interface IdempotencyOptions {
|
|
112
|
+
/** Pluggable persistence backend. Default: a fresh in-memory store. */
|
|
113
|
+
store?: IdempotencyStore;
|
|
114
|
+
/** How long a key (and its replayed response) lives, in seconds. Default: `86400` (24h). */
|
|
115
|
+
ttlSeconds?: number;
|
|
116
|
+
/** Request header carrying the key. Default: `"idempotency-key"`. */
|
|
117
|
+
headerName?: string;
|
|
118
|
+
/** Response header marking a replayed response. Default: `"idempotency-replayed"`. */
|
|
119
|
+
replayHeaderName?: string;
|
|
120
|
+
/**
|
|
121
|
+
* HTTP methods the middleware applies to. Requests with other methods pass
|
|
122
|
+
* through untouched even when they carry the header. Default:
|
|
123
|
+
* `["POST", "PUT", "PATCH", "DELETE"]`.
|
|
124
|
+
*/
|
|
125
|
+
methods?: string[];
|
|
126
|
+
/**
|
|
127
|
+
* Require the key on every applicable request, returning `400` when it is
|
|
128
|
+
* missing. Default: `false` (idempotency is opt-in per request).
|
|
129
|
+
*/
|
|
130
|
+
requireKey?: boolean;
|
|
131
|
+
/** Maximum accepted key length in characters. Default: `255`. */
|
|
132
|
+
maxKeyLength?: number;
|
|
133
|
+
/**
|
|
134
|
+
* Maximum response body size (bytes) the middleware will buffer and store.
|
|
135
|
+
* Larger responses are streamed through without caching and the reservation
|
|
136
|
+
* is released so a later retry can re-run the handler. Default: `1048576`
|
|
137
|
+
* (1 MiB). A guard against unbounded memory growth from large replies.
|
|
138
|
+
*/
|
|
139
|
+
maxResponseBytes?: number;
|
|
140
|
+
/**
|
|
141
|
+
* Decide whether a produced response should be cached for replay. Returning
|
|
142
|
+
* `false` releases the reservation so the client may retry. Default: cache
|
|
143
|
+
* any response with status `< 500` (server errors are retryable).
|
|
144
|
+
*/
|
|
145
|
+
cacheableStatus?: (status: number) => boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Share a single in-memory store across every `idempotency()` mount that
|
|
148
|
+
* declares the same `groupId`. Only meaningful for the default in-memory
|
|
149
|
+
* store; supply an explicit `store` to coordinate across processes.
|
|
150
|
+
*/
|
|
151
|
+
groupId?: string;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* In-memory {@link IdempotencyStore}. Suitable for tests and single-process
|
|
155
|
+
* deployments. Expired records are dropped on access; the map is opportunistically
|
|
156
|
+
* pruned so it cannot grow without bound.
|
|
157
|
+
*/
|
|
158
|
+
export declare class MemoryIdempotencyStore implements IdempotencyStore {
|
|
159
|
+
private readonly map;
|
|
160
|
+
/** @inheritDoc */
|
|
161
|
+
reserve(key: string, record: IdempotencyRecord): IdempotencyRecord | null;
|
|
162
|
+
/** @inheritDoc */
|
|
163
|
+
complete(key: string, record: IdempotencyRecord): void;
|
|
164
|
+
/** @inheritDoc */
|
|
165
|
+
release(key: string): void;
|
|
166
|
+
private read;
|
|
167
|
+
private prune;
|
|
168
|
+
/** Test helper. Remove every record. */
|
|
169
|
+
clear(): void;
|
|
170
|
+
/** Test helper. Number of stored records (including expired). */
|
|
171
|
+
size(): number;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Idempotency-key middleware. Mount it ahead of the routes that need
|
|
175
|
+
* exactly-once semantics under retries (typically the payment / write
|
|
176
|
+
* surface).
|
|
177
|
+
*
|
|
178
|
+
* Behavior for an applicable method (see {@link IdempotencyOptions.methods}):
|
|
179
|
+
*
|
|
180
|
+
* - **No key** → pass through (or `400` when {@link IdempotencyOptions.requireKey}).
|
|
181
|
+
* - **First key** → run the handler, then persist the response keyed by the
|
|
182
|
+
* request fingerprint for {@link IdempotencyOptions.ttlSeconds}.
|
|
183
|
+
* - **Same key + same body, completed** → replay the stored response with an
|
|
184
|
+
* `Idempotency-Replayed: true` header; the handler does not run.
|
|
185
|
+
* - **Same key, still in flight** → {@link ConflictError} (`409`).
|
|
186
|
+
* - **Same key + different body** → `422 Unprocessable Content` (a key is
|
|
187
|
+
* permanently bound to its first payload).
|
|
188
|
+
*
|
|
189
|
+
* Responses that fail {@link IdempotencyOptions.cacheableStatus} (server errors
|
|
190
|
+
* by default) or exceed {@link IdempotencyOptions.maxResponseBytes} are not
|
|
191
|
+
* cached and the reservation is released so the client can retry.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* import { idempotency } from "@daloyjs/core";
|
|
196
|
+
*
|
|
197
|
+
* app.use(idempotency({ ttlSeconds: 86_400 }));
|
|
198
|
+
* ```
|
|
199
|
+
*
|
|
200
|
+
* @param opts - Idempotency configuration.
|
|
201
|
+
* @returns A {@link Hooks} bundle ready for `app.use(...)`.
|
|
202
|
+
* @since 0.37.0
|
|
203
|
+
*/
|
|
204
|
+
export declare function idempotency(opts?: IdempotencyOptions): Hooks;
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* First-class `Idempotency-Key` handling for DaloyJS.
|
|
3
|
+
*
|
|
4
|
+
* The {@link idempotency} middleware lets clients safely retry unsafe requests
|
|
5
|
+
* (`POST`, `PUT`, `PATCH`, `DELETE`) without risking duplicate side effects —
|
|
6
|
+
* the table-stakes guarantee for payment surfaces and serverless retries. It
|
|
7
|
+
* mirrors the IETF *The Idempotency-Key HTTP Header Field* draft and the
|
|
8
|
+
* conventions used by major payment processors:
|
|
9
|
+
*
|
|
10
|
+
* 1. The client sends a unique, client-generated `Idempotency-Key` header.
|
|
11
|
+
* 2. On the **first** request the handler runs normally; the framework
|
|
12
|
+
* fingerprints the request (method + path + body) and persists the final
|
|
13
|
+
* response keyed by the idempotency key.
|
|
14
|
+
* 3. On a **retry** carrying the same key and the same fingerprint, the stored
|
|
15
|
+
* response is replayed byte-for-byte (with an `Idempotency-Replayed: true`
|
|
16
|
+
* marker) — the handler never runs twice.
|
|
17
|
+
* 4. A retry that arrives **while the original is still in flight** gets a
|
|
18
|
+
* `409 Conflict` so the client backs off instead of racing.
|
|
19
|
+
* 5. Reusing a key with a **different** request body returns `422
|
|
20
|
+
* Unprocessable Content` — a key is permanently bound to its first payload.
|
|
21
|
+
*
|
|
22
|
+
* The store is pluggable via {@link IdempotencyStore}, mirroring the
|
|
23
|
+
* `SessionStore` / rate-limit-store pattern. The default
|
|
24
|
+
* {@link MemoryIdempotencyStore} is process-local; supply a shared backend
|
|
25
|
+
* (e.g. Redis) for multi-instance deployments.
|
|
26
|
+
*
|
|
27
|
+
* This module is dependency-free and uses only Web Crypto + Web Standard
|
|
28
|
+
* `Request`/`Response`, so it runs unchanged on Node, Bun, Deno, Cloudflare
|
|
29
|
+
* Workers, and Vercel Edge.
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
* @since 0.37.0
|
|
33
|
+
*/
|
|
34
|
+
import { BadRequestError, ConflictError, HttpError } from "./errors.js";
|
|
35
|
+
const enc = new TextEncoder();
|
|
36
|
+
/** Internal `ctx.state` key carrying the reservation between hooks. */
|
|
37
|
+
const PENDING_STATE_KEY = "__idempotencyPending";
|
|
38
|
+
/**
|
|
39
|
+
* Process-wide registry of in-memory stores shared by
|
|
40
|
+
* {@link IdempotencyOptions.groupId}. Two `idempotency({ groupId: "payments" })`
|
|
41
|
+
* mounts receive the same store so a key reserved on one route is honored on
|
|
42
|
+
* the others.
|
|
43
|
+
*
|
|
44
|
+
* @internal
|
|
45
|
+
*/
|
|
46
|
+
const SHARED_IDEMPOTENCY_STORES = new Map();
|
|
47
|
+
/**
|
|
48
|
+
* Test-only helper that clears the process-wide shared stores used by
|
|
49
|
+
* `idempotency({ groupId })`. Not part of the documented public API.
|
|
50
|
+
*
|
|
51
|
+
* @internal
|
|
52
|
+
*/
|
|
53
|
+
export function _resetSharedIdempotencyStoresForTests() {
|
|
54
|
+
SHARED_IDEMPOTENCY_STORES.clear();
|
|
55
|
+
}
|
|
56
|
+
// ---------- Default store ----------
|
|
57
|
+
/**
|
|
58
|
+
* In-memory {@link IdempotencyStore}. Suitable for tests and single-process
|
|
59
|
+
* deployments. Expired records are dropped on access; the map is opportunistically
|
|
60
|
+
* pruned so it cannot grow without bound.
|
|
61
|
+
*/
|
|
62
|
+
export class MemoryIdempotencyStore {
|
|
63
|
+
map = new Map();
|
|
64
|
+
/** @inheritDoc */
|
|
65
|
+
reserve(key, record) {
|
|
66
|
+
const existing = this.read(key);
|
|
67
|
+
if (existing)
|
|
68
|
+
return existing;
|
|
69
|
+
this.map.set(key, record);
|
|
70
|
+
if (this.map.size > 10_000)
|
|
71
|
+
this.prune();
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
/** @inheritDoc */
|
|
75
|
+
complete(key, record) {
|
|
76
|
+
this.map.set(key, record);
|
|
77
|
+
}
|
|
78
|
+
/** @inheritDoc */
|
|
79
|
+
release(key) {
|
|
80
|
+
this.map.delete(key);
|
|
81
|
+
}
|
|
82
|
+
read(key) {
|
|
83
|
+
const rec = this.map.get(key);
|
|
84
|
+
if (!rec)
|
|
85
|
+
return null;
|
|
86
|
+
if (rec.expiresAt <= Date.now()) {
|
|
87
|
+
this.map.delete(key);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return rec;
|
|
91
|
+
}
|
|
92
|
+
prune() {
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
for (const [k, v] of this.map) {
|
|
95
|
+
if (v.expiresAt <= now)
|
|
96
|
+
this.map.delete(k);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Test helper. Remove every record. */
|
|
100
|
+
clear() {
|
|
101
|
+
this.map.clear();
|
|
102
|
+
}
|
|
103
|
+
/** Test helper. Number of stored records (including expired). */
|
|
104
|
+
size() {
|
|
105
|
+
return this.map.size;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function getSubtle() {
|
|
109
|
+
const c = globalThis.crypto;
|
|
110
|
+
if (!c?.subtle) {
|
|
111
|
+
throw new Error("idempotency(): Web Crypto (crypto.subtle) is required. Provide a polyfill in environments without it.");
|
|
112
|
+
}
|
|
113
|
+
return c.subtle;
|
|
114
|
+
}
|
|
115
|
+
const HEX = "0123456789abcdef";
|
|
116
|
+
function bytesToHex(bytes) {
|
|
117
|
+
let out = "";
|
|
118
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
119
|
+
const b = bytes[i];
|
|
120
|
+
out += HEX[b >> 4] + HEX[b & 0x0f];
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
function bytesToBase64(bytes) {
|
|
125
|
+
let bin = "";
|
|
126
|
+
const CHUNK = 0x8000;
|
|
127
|
+
for (let i = 0; i < bytes.length; i += CHUNK) {
|
|
128
|
+
bin += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
|
|
129
|
+
}
|
|
130
|
+
return btoa(bin);
|
|
131
|
+
}
|
|
132
|
+
function base64ToBytes(b64) {
|
|
133
|
+
const bin = atob(b64);
|
|
134
|
+
const out = new Uint8Array(bin.length);
|
|
135
|
+
for (let i = 0; i < bin.length; i++)
|
|
136
|
+
out[i] = bin.charCodeAt(i);
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Deterministic, key-order-insensitive serialization of a parsed request body
|
|
141
|
+
* so two logically-identical retries fingerprint the same.
|
|
142
|
+
*/
|
|
143
|
+
function stableStringify(value) {
|
|
144
|
+
if (value === null || value === undefined)
|
|
145
|
+
return "null";
|
|
146
|
+
if (typeof value !== "object")
|
|
147
|
+
return JSON.stringify(value) ?? "null";
|
|
148
|
+
if (Array.isArray(value))
|
|
149
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
150
|
+
const obj = value;
|
|
151
|
+
const keys = Object.keys(obj).sort();
|
|
152
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
153
|
+
}
|
|
154
|
+
async function computeFingerprint(method, ctx) {
|
|
155
|
+
const url = new URL(ctx.request.url);
|
|
156
|
+
const material = `${method}\n${url.pathname}${url.search}\n${stableStringify(ctx.body)}`;
|
|
157
|
+
const digest = new Uint8Array(await getSubtle().digest("SHA-256", enc.encode(material)));
|
|
158
|
+
return bytesToHex(digest);
|
|
159
|
+
}
|
|
160
|
+
// Printable ASCII only (no control chars / whitespace). Anchored + bounded to
|
|
161
|
+
// the character class, so this is linear-time and ReDoS-free.
|
|
162
|
+
const KEY_PATTERN = /^[\x21-\x7e]+$/;
|
|
163
|
+
function validateKey(key, headerName, maxLen) {
|
|
164
|
+
if (key.length === 0) {
|
|
165
|
+
throw new BadRequestError(`${headerName} header must not be empty.`);
|
|
166
|
+
}
|
|
167
|
+
if (key.length > maxLen) {
|
|
168
|
+
throw new BadRequestError(`${headerName} header must be at most ${maxLen} characters.`);
|
|
169
|
+
}
|
|
170
|
+
if (!KEY_PATTERN.test(key)) {
|
|
171
|
+
throw new BadRequestError(`${headerName} header contains invalid characters.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function captureResponse(res, maxBytes) {
|
|
175
|
+
const buf = new Uint8Array(await res.clone().arrayBuffer());
|
|
176
|
+
if (buf.byteLength > maxBytes)
|
|
177
|
+
return null;
|
|
178
|
+
const headers = [];
|
|
179
|
+
res.headers.forEach((value, name) => {
|
|
180
|
+
headers.push([name, value]);
|
|
181
|
+
});
|
|
182
|
+
return { status: res.status, headers, body: buf.byteLength ? bytesToBase64(buf) : "" };
|
|
183
|
+
}
|
|
184
|
+
function buildReplayResponse(stored, replayHeaderName) {
|
|
185
|
+
const headers = new Headers();
|
|
186
|
+
for (const [name, value] of stored.headers)
|
|
187
|
+
headers.set(name, value);
|
|
188
|
+
headers.set(replayHeaderName, "true");
|
|
189
|
+
const body = stored.body ? base64ToBytes(stored.body) : null;
|
|
190
|
+
return new Response(body, { status: stored.status, headers });
|
|
191
|
+
}
|
|
192
|
+
// ---------- Middleware ----------
|
|
193
|
+
/**
|
|
194
|
+
* Idempotency-key middleware. Mount it ahead of the routes that need
|
|
195
|
+
* exactly-once semantics under retries (typically the payment / write
|
|
196
|
+
* surface).
|
|
197
|
+
*
|
|
198
|
+
* Behavior for an applicable method (see {@link IdempotencyOptions.methods}):
|
|
199
|
+
*
|
|
200
|
+
* - **No key** → pass through (or `400` when {@link IdempotencyOptions.requireKey}).
|
|
201
|
+
* - **First key** → run the handler, then persist the response keyed by the
|
|
202
|
+
* request fingerprint for {@link IdempotencyOptions.ttlSeconds}.
|
|
203
|
+
* - **Same key + same body, completed** → replay the stored response with an
|
|
204
|
+
* `Idempotency-Replayed: true` header; the handler does not run.
|
|
205
|
+
* - **Same key, still in flight** → {@link ConflictError} (`409`).
|
|
206
|
+
* - **Same key + different body** → `422 Unprocessable Content` (a key is
|
|
207
|
+
* permanently bound to its first payload).
|
|
208
|
+
*
|
|
209
|
+
* Responses that fail {@link IdempotencyOptions.cacheableStatus} (server errors
|
|
210
|
+
* by default) or exceed {@link IdempotencyOptions.maxResponseBytes} are not
|
|
211
|
+
* cached and the reservation is released so the client can retry.
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```ts
|
|
215
|
+
* import { idempotency } from "@daloyjs/core";
|
|
216
|
+
*
|
|
217
|
+
* app.use(idempotency({ ttlSeconds: 86_400 }));
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* @param opts - Idempotency configuration.
|
|
221
|
+
* @returns A {@link Hooks} bundle ready for `app.use(...)`.
|
|
222
|
+
* @since 0.37.0
|
|
223
|
+
*/
|
|
224
|
+
export function idempotency(opts = {}) {
|
|
225
|
+
const ttlSeconds = opts.ttlSeconds ?? 86_400;
|
|
226
|
+
if (!Number.isInteger(ttlSeconds) || ttlSeconds <= 0) {
|
|
227
|
+
throw new Error("idempotency(): ttlSeconds must be a positive integer.");
|
|
228
|
+
}
|
|
229
|
+
const maxKeyLength = opts.maxKeyLength ?? 255;
|
|
230
|
+
if (!Number.isInteger(maxKeyLength) || maxKeyLength <= 0) {
|
|
231
|
+
throw new Error("idempotency(): maxKeyLength must be a positive integer.");
|
|
232
|
+
}
|
|
233
|
+
const maxResponseBytes = opts.maxResponseBytes ?? 1_048_576;
|
|
234
|
+
if (!Number.isInteger(maxResponseBytes) || maxResponseBytes <= 0) {
|
|
235
|
+
throw new Error("idempotency(): maxResponseBytes must be a positive integer.");
|
|
236
|
+
}
|
|
237
|
+
const headerName = (opts.headerName ?? "idempotency-key").toLowerCase();
|
|
238
|
+
const replayHeaderName = (opts.replayHeaderName ?? "idempotency-replayed").toLowerCase();
|
|
239
|
+
const methods = new Set((opts.methods ?? ["POST", "PUT", "PATCH", "DELETE"]).map((m) => m.toUpperCase()));
|
|
240
|
+
const requireKey = opts.requireKey === true;
|
|
241
|
+
const cacheableStatus = opts.cacheableStatus ?? ((status) => status < 500);
|
|
242
|
+
const ttlMs = ttlSeconds * 1_000;
|
|
243
|
+
let store;
|
|
244
|
+
if (opts.store) {
|
|
245
|
+
store = opts.store;
|
|
246
|
+
}
|
|
247
|
+
else if (opts.groupId) {
|
|
248
|
+
let shared = SHARED_IDEMPOTENCY_STORES.get(opts.groupId);
|
|
249
|
+
if (!shared) {
|
|
250
|
+
shared = new MemoryIdempotencyStore();
|
|
251
|
+
SHARED_IDEMPOTENCY_STORES.set(opts.groupId, shared);
|
|
252
|
+
}
|
|
253
|
+
store = shared;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
store = new MemoryIdempotencyStore();
|
|
257
|
+
}
|
|
258
|
+
const keyPrefix = opts.groupId ? `${opts.groupId}:` : "";
|
|
259
|
+
return {
|
|
260
|
+
async beforeHandle(ctx) {
|
|
261
|
+
const method = ctx.request.method.toUpperCase();
|
|
262
|
+
if (!methods.has(method))
|
|
263
|
+
return undefined;
|
|
264
|
+
const rawKey = ctx.request.headers.get(headerName);
|
|
265
|
+
if (rawKey === null || rawKey.trim() === "") {
|
|
266
|
+
if (requireKey) {
|
|
267
|
+
throw new BadRequestError(`Missing required ${headerName} header.`);
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
const key = rawKey.trim();
|
|
272
|
+
validateKey(key, headerName, maxKeyLength);
|
|
273
|
+
const fingerprint = await computeFingerprint(method, ctx);
|
|
274
|
+
const storeKey = `${keyPrefix}${key}`;
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
const record = {
|
|
277
|
+
fingerprint,
|
|
278
|
+
status: "in-flight",
|
|
279
|
+
createdAt: now,
|
|
280
|
+
expiresAt: now + ttlMs,
|
|
281
|
+
};
|
|
282
|
+
const reserveResult = store.reserve(storeKey, record, ttlMs);
|
|
283
|
+
const existing = isPromiseLike(reserveResult) ? await reserveResult : reserveResult;
|
|
284
|
+
if (existing) {
|
|
285
|
+
if (existing.fingerprint !== fingerprint) {
|
|
286
|
+
throw new HttpError(422, {
|
|
287
|
+
type: "https://daloyjs.dev/errors/idempotency-key-reuse",
|
|
288
|
+
title: "Unprocessable Content",
|
|
289
|
+
detail: `The ${headerName} header was already used with a different request payload.`,
|
|
290
|
+
}, { "cache-control": "no-store" });
|
|
291
|
+
}
|
|
292
|
+
if (existing.status === "in-flight") {
|
|
293
|
+
throw new ConflictError(`A request with this ${headerName} is still being processed. Retry after it completes.`);
|
|
294
|
+
}
|
|
295
|
+
// Completed: replay the stored response verbatim.
|
|
296
|
+
return buildReplayResponse(existing.response, replayHeaderName);
|
|
297
|
+
}
|
|
298
|
+
ctx.state[PENDING_STATE_KEY] = {
|
|
299
|
+
storeKey,
|
|
300
|
+
fingerprint,
|
|
301
|
+
};
|
|
302
|
+
return undefined;
|
|
303
|
+
},
|
|
304
|
+
async onSend(res, ctx) {
|
|
305
|
+
if (!ctx)
|
|
306
|
+
return undefined;
|
|
307
|
+
const state = ctx.state;
|
|
308
|
+
const pending = state[PENDING_STATE_KEY];
|
|
309
|
+
if (!pending)
|
|
310
|
+
return undefined;
|
|
311
|
+
// Consume once: a replayed response on a later retry must not re-store.
|
|
312
|
+
delete state[PENDING_STATE_KEY];
|
|
313
|
+
if (!cacheableStatus(res.status)) {
|
|
314
|
+
await store.release(pending.storeKey);
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
const captured = await captureResponse(res, maxResponseBytes);
|
|
318
|
+
if (captured === null) {
|
|
319
|
+
// Body too large to cache safely: drop the reservation so retries work.
|
|
320
|
+
await store.release(pending.storeKey);
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
const now = Date.now();
|
|
324
|
+
const completeResult = store.complete(pending.storeKey, {
|
|
325
|
+
fingerprint: pending.fingerprint,
|
|
326
|
+
status: "completed",
|
|
327
|
+
response: captured,
|
|
328
|
+
createdAt: now,
|
|
329
|
+
expiresAt: now + ttlMs,
|
|
330
|
+
}, ttlMs);
|
|
331
|
+
if (isPromiseLike(completeResult))
|
|
332
|
+
await completeResult;
|
|
333
|
+
return undefined;
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function isPromiseLike(value) {
|
|
338
|
+
return (value !== null &&
|
|
339
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
340
|
+
typeof value.then === "function");
|
|
341
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { createApp } from "./app.js";
|
|
|
3
3
|
export { _resetPackageJsonCacheForTests } from "./app.js";
|
|
4
4
|
export { _resetCrashHandlersForTests } from "./app.js";
|
|
5
5
|
export { _resetInsecureDefaultsLogForTests } from "./app.js";
|
|
6
|
-
export type { AppOptions, AppOpenAPIOptions, DocsRouteOptions, HealthRouteOptions, CspReportRouteOptions, IntrospectedRoute, PluginInstalledEvent, PluginExtension, ShutdownEvent, } from "./app.js";
|
|
6
|
+
export type { AppOptions, AppOpenAPIOptions, DocsRouteOptions, HealthRouteOptions, CspReportRouteOptions, MetricsRouteOptions, IntrospectedRoute, PluginInstalledEvent, PluginExtension, ShutdownEvent, SecurityPreset, } from "./app.js";
|
|
7
7
|
export { getConnInfo, setConnInfo, assertBehindProxy, resolveClientIp, readRemoteAddress, readRemotePort, pickForwardedForByHops, } from "./conn-info.js";
|
|
8
8
|
export type { BehindProxyConfig, ConnInfo } from "./conn-info.js";
|
|
9
9
|
export { subdomains, PSL_SNAPSHOT_DATE, PSL_PUBLIC_SUFFIXES, MAX_SNAPSHOT_AGE_DAYS, } from "./subdomains.js";
|
|
@@ -11,10 +11,12 @@ export type { SubdomainsOptions, SubdomainsResult } from "./subdomains.js";
|
|
|
11
11
|
export { defineDependency, DEPENDENCY_MARKER } from "./dependency.js";
|
|
12
12
|
export type { DependencyHooks, DependencyOptions, } from "./dependency.js";
|
|
13
13
|
export type { RouteDefinition, HttpMethod, PathString, RequestSchemas, ResponsesMap, ResponseSpec, AuthSpec, Hooks, BaseContext, AppState, AuthScheme, AuthContext, HandlerReturn, InferRequest, ParamsOf, PathParams, CallbackDefinition, CallbackMap, CallbackOperation, RouteExample, RouteMeta, } from "./types.js";
|
|
14
|
-
export { HttpError, BadRequestError, ValidationError, NotFoundError, UnauthorizedError, ForbiddenError, MethodNotAllowedError, PayloadTooLargeError, UnsupportedMediaTypeError, TooManyRequestsError, RequestTimeoutError, InternalError, MessageLeakError, httpError, SAFE_CUSTOM_ERROR_RESPONSE_HEADERS, checkCustomErrorResponseHeaders, } from "./errors.js";
|
|
14
|
+
export { HttpError, BadRequestError, ValidationError, NotFoundError, ConflictError, UnauthorizedError, ForbiddenError, MethodNotAllowedError, PayloadTooLargeError, UnsupportedMediaTypeError, TooManyRequestsError, RequestTimeoutError, InternalError, MessageLeakError, httpError, SAFE_CUSTOM_ERROR_RESPONSE_HEADERS, checkCustomErrorResponseHeaders, } from "./errors.js";
|
|
15
15
|
export type { ProblemDetails, ProblemRenderOptions, HttpErrorOptions } from "./errors.js";
|
|
16
16
|
export type { StandardSchemaV1 } from "./schema.js";
|
|
17
17
|
export { validate, isStandardSchema } from "./schema.js";
|
|
18
|
+
export { diffOpenAPI, hasBreakingChanges } from "./openapi-diff.js";
|
|
19
|
+
export type { ChangeSeverity, OpenAPIChange, OpenAPIDiffResult, } from "./openapi-diff.js";
|
|
18
20
|
export { readBodyLimited, safeJsonParse, isForbiddenObjectKey, sanitizeHeaderName, sanitizeHeaderValue, timingSafeEqual, randomId, assertNoDuplicateSingletonHeaders, assertNoReservedInternalHeaders, RESERVED_INBOUND_HEADER_PREFIXES, SMUGGLING_SINGLETON_HEADERS, verifyWebhookSignature, signWebhookPayload, WEBHOOK_DEFAULT_TOLERANCE_SECONDS, assertStrongSecret, MIN_PROD_SECRET_BYTES, WEAK_SECRET_STRINGS, sanitizeFilename, assertSafeRelativePath, hasMongoOperatorKeys, assertNoMongoOperators, } from "./security.js";
|
|
19
21
|
export type { WebhookHmacAlgorithm } from "./security.js";
|
|
20
22
|
export { requestId, secureHeaders, SECURE_HEADERS_MARKER, cors, CORS_HOOK_MARKER, CORS_ORIGIN_ALLOW_MARKER, CORS_WILDCARD_ORIGIN_MARKER, rateLimit, loginThrottle, timing, bearerAuth, basicAuth, csrf, CSRF_HOOK_MARKER, fetchMetadata, requireScopes, REQUIRE_SCOPES_AGGREGATE_KEY, REQUIRE_SCOPES_HOOK_MARKER, _resetSharedRateLimitStoresForTests, } from "./middleware.js";
|
|
@@ -36,6 +38,30 @@ export { ipRestriction } from "./ip-restriction.js";
|
|
|
36
38
|
export type { IpRestrictionOptions } from "./ip-restriction.js";
|
|
37
39
|
export { fetchGuard, SsrfBlockedError } from "./fetch-guard.js";
|
|
38
40
|
export type { FetchGuardOptions, SsrfBlockReason } from "./fetch-guard.js";
|
|
41
|
+
export { resilientFetch, CircuitBreaker, CircuitOpenError, FetchTimeoutError, } from "./fetch-resilience.js";
|
|
42
|
+
export type { ResilientFetchOptions, CircuitBreakerOptions, CircuitState, RetryContext, } from "./fetch-resilience.js";
|
|
43
|
+
export { createWebhookSender, MemoryWebhookDeadLetterSink, } from "./webhook-delivery.js";
|
|
44
|
+
export type { WebhookEvent, WebhookSenderOptions, WebhookDeliveryResult, WebhookDeadLetter, WebhookDeadLetterSink, WebhookAttempt, } from "./webhook-delivery.js";
|
|
45
|
+
export { Scheduler, CronParseError, parseCron, nextCronRun, } from "./scheduler.js";
|
|
46
|
+
export type { SchedulerOptions, SchedulerLogger, TimerFns, TaskDefinition, TaskHandler, TaskRunContext, TaskErrorInfo, TaskState, CronFields, } from "./scheduler.js";
|
|
47
|
+
export { clientCertAuth, setClientCertificate, getClientCertificate, normalizePeerCertificate, parseForwardedClientCert, } from "./mtls.js";
|
|
48
|
+
export type { ClientCertificate, ClientCertificateSource, ClientCertAuthOptions, ClientCertHeaderConfig, PeerCertificateLike, } from "./mtls.js";
|
|
49
|
+
export { signMessage, signRequest, verifyMessage, verifyRequest, httpSignatureAuth, contentDigest, verifyContentDigest, DEFAULT_SIGNATURE_LABEL, DEFAULT_MAX_SIGNATURE_AGE_SECONDS, DEFAULT_SIGNATURE_CLOCK_SKEW_SECONDS, } from "./http-signatures.js";
|
|
50
|
+
export type { HttpSignatureAlgorithm, HttpSignatureKeyMaterial, HttpSignatureKey, SignMessageOptions, SignRequestOptions, MessageSignature, VerifyMessageOptions, VerifyResult, VerifySuccess, VerifyFailure, KeyResolutionInfo, HttpSignatureAuthOptions, ContentDigestAlgorithm, } from "./http-signatures.js";
|
|
51
|
+
export { autoBan, MemoryAutoBanStore, _resetAutoBanStoresForTests, } from "./auto-ban.js";
|
|
52
|
+
export type { AutoBanOptions, AutoBanStore, AutoBanRecord, AutoBanEvent, AutoBanStrikeEvent, } from "./auto-ban.js";
|
|
53
|
+
export { botGuard, GOOGLEBOT, BINGBOT, WELL_KNOWN_BOTS } from "./bot-guard.js";
|
|
54
|
+
export type { BotGuardOptions, BotGuardEvent, BotResolver, VerifiedBotRule, } from "./bot-guard.js";
|
|
55
|
+
export { ipReputation, urlFeed } from "./ip-reputation.js";
|
|
56
|
+
export type { IpReputationOptions, IpReputationFeed, IpReputationMatch, IpReputationController, UrlFeedOptions, } from "./ip-reputation.js";
|
|
57
|
+
export { geoBlock } from "./geo-block.js";
|
|
58
|
+
export type { GeoBlockOptions, GeoBlockDecision, GeoBlockReason, GeoState, CountryFromIp, CountryFromContext, } from "./geo-block.js";
|
|
59
|
+
export { concurrencyLimit } from "./concurrency-limit.js";
|
|
60
|
+
export type { ConcurrencyLimitOptions, ConcurrencyRejection, } from "./concurrency-limit.js";
|
|
61
|
+
export { requestDecompression, decompressRequestBody, DecompressionBombError, UnsupportedContentEncodingError, MalformedCompressedBodyError, _resetRequestDecompressionProbeForTests, } from "./request-decompression.js";
|
|
62
|
+
export type { RequestDecompressionOptions, RequestDecompressionEncoding, DecompressionBombInfo, } from "./request-decompression.js";
|
|
63
|
+
export { waf } from "./waf.js";
|
|
64
|
+
export type { WafOptions, WafMode, WafRuleId, WafRuleConfig, WafInspectConfig, WafMatch, WafEvent, WafInspectionLocation, } from "./waf.js";
|
|
39
65
|
export { safeRedirect, OpenRedirectBlockedError } from "./safe-redirect.js";
|
|
40
66
|
export type { SafeRedirectOptions, SafeRedirectStatus, SafeRedirectBlockReason, } from "./safe-redirect.js";
|
|
41
67
|
export { loadShedding, LOAD_SHEDDING_MARKER } from "./load-shedding.js";
|
|
@@ -46,7 +72,7 @@ export type { RequestIdOptions, SecureHeadersOptions, CspDirectivesOptions, Cors
|
|
|
46
72
|
export type { BearerAuthOptions, BearerAuthVerifyHook } from "./middleware.js";
|
|
47
73
|
export { createLogger, noopLogger, DEFAULT_REDACT_KEYS } from "./logger.js";
|
|
48
74
|
export type { Logger, LogLevel, ConsoleLoggerOptions, LoggerRedactionOptions, } from "./logger.js";
|
|
49
|
-
export type { ScalarJsonPrimitive, ScalarJsonValue, ScalarReferenceConfiguration, ScalarTheme, } from "./docs.js";
|
|
75
|
+
export type { ScalarJsonPrimitive, ScalarJsonValue, ScalarReferenceConfiguration, ScalarTheme, DocsAssetOptions, } from "./docs.js";
|
|
50
76
|
export { formatStartupBanner, printStartupBanner } from "./banner.js";
|
|
51
77
|
export type { StartupBannerLink, StartupBannerOptions } from "./banner.js";
|
|
52
78
|
export { sseStream, sseResponse, ndjsonStream, ndjsonResponse, } from "./streaming.js";
|
|
@@ -57,9 +83,17 @@ export { discriminator, discriminatedUnion } from "./discriminator.js";
|
|
|
57
83
|
export type { DiscriminatorObject, DiscriminatedUnion, DiscriminatedUnionOptions, } from "./discriminator.js";
|
|
58
84
|
export { session, rotateSession, signValue, verifySignedValue, MemorySessionStore, SESSION_HOOK_MARKER, SESSION_SECRETS_MARKER, } from "./session.js";
|
|
59
85
|
export type { SessionOptions, SessionCookieOptions, SessionContext, SessionRecord, SessionStore, SessionState, RotateSessionOptions, } from "./session.js";
|
|
86
|
+
export { idempotency, MemoryIdempotencyStore, _resetSharedIdempotencyStoresForTests, } from "./idempotency.js";
|
|
87
|
+
export type { IdempotencyOptions, IdempotencyStore, IdempotencyRecord, StoredIdempotentResponse, } from "./idempotency.js";
|
|
88
|
+
export { responseCache, MemoryResponseCacheStore, _resetSharedResponseCacheStoresForTests, } from "./response-cache.js";
|
|
89
|
+
export type { ResponseCacheOptions, ResponseCacheStore, CachedResponse, } from "./response-cache.js";
|
|
90
|
+
export { encodeCursor, decodeCursor, buildLinkHeader, buildPageLinks, paginationQuery, MAX_CURSOR_LENGTH, } from "./pagination.js";
|
|
91
|
+
export type { PaginationLink, PageLinkOptions, PageLinks, PaginationQueryOptions, PaginationParams, PaginationQuerySchema, } from "./pagination.js";
|
|
92
|
+
export { MetricsRegistry, Counter, Gauge, Histogram, httpMetrics, DEFAULT_DURATION_BUCKETS, PROMETHEUS_CONTENT_TYPE, } from "./metrics.js";
|
|
93
|
+
export type { MetricLabels, MetricsRegistryOptions, HttpMetricsOptions, } from "./metrics.js";
|
|
60
94
|
export { fileField, multipartObject, isFileFieldSchema, isMultipartObjectSchema, } from "./multipart.js";
|
|
61
95
|
export type { FileFieldSchema, FileFieldOptions, FileMagicBytesOption, FileMagicBytesSignature, MultipartObjectOptions, MultipartShape, UploadedFile, } from "./multipart.js";
|
|
62
96
|
export { otelTracing, TRACING_SPAN_KIND_SERVER, TRACING_SPAN_STATUS_UNSET, TRACING_SPAN_STATUS_OK, TRACING_SPAN_STATUS_ERROR, } from "./tracing.js";
|
|
63
97
|
export type { OtelTracingOptions, TracingAttributes, TracingAttributeValue, TracingSpan, TracingStartSpanOptions, TracingTracer, } from "./tracing.js";
|
|
64
98
|
export { defineWebSocket, WebSocketRegistry, WebSocketProtocolError, WebSocketPayloadTooLargeError, WS_GUID, WS_READY_STATE, WS_OPCODE, WS_CLOSE_CODE, WS_MAX_CONTROL_PAYLOAD, DEFAULT_WS_BACKPRESSURE_LIMIT, DEFAULT_WS_MAX_PAYLOAD_LENGTH, DEFAULT_WS_IDLE_TIMEOUT_SECONDS, computeAcceptKey, parseSubprotocols, validateSelectedSubprotocol, validateUpgrade, checkWebSocketOrigin, parseFrame, encodeFrame, encodeClosePayload, decodeClosePayload, encodeSendPayload, normalizeWebSocketOptions, wsRateLimit, FrameSink, FRAME_INCOMPLETE, } from "./websocket.js";
|
|
65
|
-
export type { WebSocketConnection, WebSocketContext, WebSocketHandler, WebSocketRouteEntry, NormalizedWebSocketOptions, WebSocketBeforeUpgrade, HandshakeResult, ParsedFrame, MessageEvent as WebSocketMessageEvent, FrameSinkEvents, } from "./websocket.js";
|
|
99
|
+
export type { WebSocketConnection, WebSocketContext, WebSocketHandler, WebSocketMeta, WebSocketRouteEntry, NormalizedWebSocketOptions, WebSocketBeforeUpgrade, HandshakeResult, ParsedFrame, MessageEvent as WebSocketMessageEvent, FrameSinkEvents, } from "./websocket.js";
|