@fluojs/http 1.1.2 → 3.0.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.ko.md +430 -15
- package/README.md +437 -15
- package/dist/access-log-observer.d.ts +77 -0
- package/dist/access-log-observer.d.ts.map +1 -0
- package/dist/access-log-observer.js +171 -0
- package/dist/adapter.d.ts +9 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +15 -1
- package/dist/adapters/binding.d.ts.map +1 -1
- package/dist/adapters/binding.js +18 -16
- package/dist/adapters/dto-binding-plan.d.ts +2 -2
- package/dist/adapters/dto-binding-plan.d.ts.map +1 -1
- package/dist/adapters/dto-binding-plan.js +13 -4
- package/dist/adapters/dto-validation-adapter.d.ts +2 -1
- package/dist/adapters/dto-validation-adapter.d.ts.map +1 -1
- package/dist/adapters/dto-validation-adapter.js +2 -2
- package/dist/byte-range-response.d.ts +71 -0
- package/dist/byte-range-response.d.ts.map +1 -0
- package/dist/byte-range-response.js +347 -0
- package/dist/client-identity.d.ts +8 -4
- package/dist/client-identity.d.ts.map +1 -1
- package/dist/client-identity.js +12 -93
- package/dist/compiled-route-identity.d.ts +17 -0
- package/dist/compiled-route-identity.d.ts.map +1 -0
- package/dist/compiled-route-identity.js +29 -0
- package/dist/connection.d.ts +67 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +480 -0
- package/dist/context/request-context-node-store.d.ts +6 -0
- package/dist/context/request-context-node-store.d.ts.map +1 -1
- package/dist/context/request-context-node-store.js +14 -0
- package/dist/context/request-context.d.ts +5 -5
- package/dist/context/request-context.d.ts.map +1 -1
- package/dist/context/request-context.js +8 -83
- package/dist/context/request-id.d.ts +10 -0
- package/dist/context/request-id.d.ts.map +1 -0
- package/dist/context/request-id.js +27 -0
- package/dist/context/sse.d.ts +13 -0
- package/dist/context/sse.d.ts.map +1 -1
- package/dist/context/sse.js +36 -6
- package/dist/cookie-helpers.d.ts +47 -0
- package/dist/cookie-helpers.d.ts.map +1 -0
- package/dist/cookie-helpers.js +166 -0
- package/dist/decorators.d.ts +52 -19
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +57 -10
- package/dist/dispatch/conditional-request-policy.d.ts +67 -0
- package/dist/dispatch/conditional-request-policy.d.ts.map +1 -0
- package/dist/dispatch/conditional-request-policy.js +297 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts +8 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts.map +1 -1
- package/dist/dispatch/dispatch-content-negotiation.js +89 -42
- package/dist/dispatch/dispatch-error-negotiation.d.ts +26 -0
- package/dist/dispatch/dispatch-error-negotiation.d.ts.map +1 -0
- package/dist/dispatch/dispatch-error-negotiation.js +110 -0
- package/dist/dispatch/dispatch-error-policy.d.ts +1 -10
- package/dist/dispatch/dispatch-error-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-error-policy.js +1 -33
- package/dist/dispatch/dispatch-error-representation.d.ts +18 -0
- package/dist/dispatch/dispatch-error-representation.d.ts.map +1 -0
- package/dist/dispatch/dispatch-error-representation.js +125 -0
- package/dist/dispatch/dispatch-response-metadata.d.ts +10 -0
- package/dist/dispatch/dispatch-response-metadata.d.ts.map +1 -0
- package/dist/dispatch/dispatch-response-metadata.js +12 -0
- package/dist/dispatch/dispatch-response-policy.d.ts +33 -5
- package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-response-policy.js +149 -18
- package/dist/dispatch/dispatcher.d.ts +5 -1
- package/dist/dispatch/dispatcher.d.ts.map +1 -1
- package/dist/dispatch/dispatcher.js +178 -107
- package/dist/dispatch/fast-path/debug-visibility.d.ts.map +1 -1
- package/dist/dispatch/fast-path/debug-visibility.js +6 -3
- package/dist/dispatch/fast-path/dispatcher-state.d.ts +10 -0
- package/dist/dispatch/fast-path/dispatcher-state.d.ts.map +1 -0
- package/dist/dispatch/fast-path/dispatcher-state.js +52 -0
- package/dist/dispatch/fast-path/eligibility-checker.d.ts.map +1 -1
- package/dist/dispatch/fast-path/eligibility-checker.js +27 -15
- package/dist/dispatch/fast-path/eligibility.d.ts +16 -10
- package/dist/dispatch/fast-path/eligibility.d.ts.map +1 -1
- package/dist/dispatch/fast-path/eligibility.js +10 -4
- package/dist/dispatch/fast-path/fast-path-executor.d.ts +14 -1
- package/dist/dispatch/fast-path/fast-path-executor.d.ts.map +1 -1
- package/dist/dispatch/fast-path/fast-path-executor.js +23 -6
- package/dist/dispatch/request-abort.d.ts +22 -0
- package/dist/dispatch/request-abort.d.ts.map +1 -0
- package/dist/dispatch/request-abort.js +30 -0
- package/dist/dispatch/response-integration.d.ts +55 -0
- package/dist/dispatch/response-integration.d.ts.map +1 -0
- package/dist/dispatch/response-integration.js +57 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +19 -0
- package/dist/guards.d.ts +1 -1
- package/dist/guards.d.ts.map +1 -1
- package/dist/header-helpers.d.ts +59 -0
- package/dist/header-helpers.d.ts.map +1 -0
- package/dist/header-helpers.js +185 -0
- package/dist/index.d.ts +1 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -15
- package/dist/index.portable.d.ts +25 -0
- package/dist/index.portable.d.ts.map +1 -0
- package/dist/index.portable.js +21 -0
- package/dist/internal.d.ts +3 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -0
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +100 -15
- package/dist/middleware/correlation.d.ts +9 -1
- package/dist/middleware/correlation.d.ts.map +1 -1
- package/dist/middleware/correlation.js +25 -14
- package/dist/middleware/cors.d.ts.map +1 -1
- package/dist/middleware/cors.js +7 -8
- package/dist/middleware/middleware.d.ts +3 -3
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/rate-limit.d.ts +13 -2
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/rate-limit.js +1 -0
- package/dist/static-assets.d.ts +77 -0
- package/dist/static-assets.d.ts.map +1 -0
- package/dist/static-assets.js +270 -0
- package/dist/types.d.ts +225 -17
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +71 -1
- package/package.json +19 -9
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { readFirstNonEmptyRequestHeaderValue } from './header-helpers.js';
|
|
2
|
+
import { matchesIfRange } from './dispatch/conditional-request-policy.js';
|
|
3
|
+
import { registerFrameworkResponseWriter } from './dispatch/response-integration.js';
|
|
4
|
+
const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Input accepted by {@link createByteRangeResponse}.
|
|
8
|
+
*
|
|
9
|
+
* A streamed representation needs its exact full byte length so the response
|
|
10
|
+
* can evaluate `Range`, `If-Range`, and `HEAD` before consuming the stream.
|
|
11
|
+
* Pass a stream factory when `HEAD` must not construct the stream.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Options that describe a range-capable byte representation. */
|
|
15
|
+
|
|
16
|
+
/** Internal byte-representation data shared by response writers. */
|
|
17
|
+
|
|
18
|
+
/** Inputs for writing a byte representation through a portable response facade. */
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a response entry with RFC single-byte-range semantics.
|
|
22
|
+
*
|
|
23
|
+
* `Uint8Array` and `ArrayBuffer` values can be returned directly from a
|
|
24
|
+
* handler. Use this helper when returning a portable `ReadableStream`.
|
|
25
|
+
*
|
|
26
|
+
* @param source Byte representation or stream factory to expose with range support.
|
|
27
|
+
* @param options Optional content type and required stream size metadata.
|
|
28
|
+
* @returns A registered response entry that applies single-byte-range semantics.
|
|
29
|
+
*/
|
|
30
|
+
export function createByteRangeResponse(source, options = {}) {
|
|
31
|
+
const bytes = toBytes(source);
|
|
32
|
+
const size = bytes?.byteLength ?? options.size;
|
|
33
|
+
if (typeof size !== 'number' || !Number.isSafeInteger(size) || size < 0) {
|
|
34
|
+
throw new TypeError('A readable byte-range response requires a non-negative integer size.');
|
|
35
|
+
}
|
|
36
|
+
const entry = {
|
|
37
|
+
source,
|
|
38
|
+
contentType: options.contentType ?? DEFAULT_CONTENT_TYPE,
|
|
39
|
+
size
|
|
40
|
+
};
|
|
41
|
+
return registerFrameworkResponseWriter(entry, async context => {
|
|
42
|
+
await writeByteRangeResponse({
|
|
43
|
+
applySuccessResponseMetadata: context.applySuccessResponseMetadata,
|
|
44
|
+
entry,
|
|
45
|
+
request: context.request,
|
|
46
|
+
response: context.response,
|
|
47
|
+
validators: context.validators
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Returns whether a normal handler result is an automatically range-capable byte value.
|
|
54
|
+
*
|
|
55
|
+
* @param value Value to test for a byte representation.
|
|
56
|
+
* @returns `true` when the value is an `ArrayBuffer` or `Uint8Array`; otherwise `false`.
|
|
57
|
+
*/
|
|
58
|
+
export function isByteRangeByteSource(value) {
|
|
59
|
+
return value instanceof ArrayBuffer || value instanceof Uint8Array;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Determines whether an ordinary byte result requires byte-range processing.
|
|
64
|
+
*
|
|
65
|
+
* Explicit byte-range responses retain their full-response metadata behavior;
|
|
66
|
+
* this only guards automatic range handling for plain handler values.
|
|
67
|
+
*
|
|
68
|
+
* @param request Incoming request whose `Range` and `If-Range` fields are evaluated.
|
|
69
|
+
* @param validators Selected response validators used to evaluate `If-Range`.
|
|
70
|
+
* @returns `true` when the request has a valid byte range and `If-Range` permits it.
|
|
71
|
+
*/
|
|
72
|
+
export function shouldApplyByteRange(request, validators) {
|
|
73
|
+
return isByteRangeRequestMethod(request.method) && parseByteRangeHeader(readFirstNonEmptyRequestHeaderValue(request, 'range')) !== undefined && matchesIfRange(request, validators);
|
|
74
|
+
}
|
|
75
|
+
function resolveByteRange(rangeHeader, size, ifRangeMatches) {
|
|
76
|
+
const match = parseByteRangeHeader(rangeHeader);
|
|
77
|
+
if (!match || !ifRangeMatches) {
|
|
78
|
+
return {
|
|
79
|
+
kind: 'full'
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const [, startText, endText] = match;
|
|
83
|
+
if ((startText?.length ?? 0) === 0) {
|
|
84
|
+
const suffixLength = Number(endText);
|
|
85
|
+
if (!Number.isSafeInteger(suffixLength) || suffixLength <= 0 || size === 0) {
|
|
86
|
+
return {
|
|
87
|
+
kind: 'unsatisfiable'
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
end: size - 1,
|
|
92
|
+
kind: 'partial',
|
|
93
|
+
start: Math.max(size - suffixLength, 0)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const start = Number(startText);
|
|
97
|
+
if (!Number.isSafeInteger(start) || start < 0 || start >= size) {
|
|
98
|
+
return {
|
|
99
|
+
kind: 'unsatisfiable'
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if ((endText?.length ?? 0) === 0) {
|
|
103
|
+
return {
|
|
104
|
+
end: size - 1,
|
|
105
|
+
kind: 'partial',
|
|
106
|
+
start
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const requestedEnd = Number(endText);
|
|
110
|
+
if (!Number.isSafeInteger(requestedEnd) || requestedEnd < start) {
|
|
111
|
+
return {
|
|
112
|
+
kind: 'unsatisfiable'
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
end: Math.min(requestedEnd, size - 1),
|
|
117
|
+
kind: 'partial',
|
|
118
|
+
start
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function parseByteRangeHeader(rangeHeader) {
|
|
122
|
+
if (!rangeHeader) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
|
|
126
|
+
return match && (match[1]?.length ?? 0) + (match[2]?.length ?? 0) > 0 ? match : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Writes one byte representation while preserving range and cancellation semantics.
|
|
131
|
+
*
|
|
132
|
+
* @param options Request, response, validators, and representation metadata.
|
|
133
|
+
* @returns A promise that settles after the representation completes or fails.
|
|
134
|
+
*/
|
|
135
|
+
export async function writeByteRangeResponse(options) {
|
|
136
|
+
const {
|
|
137
|
+
entry,
|
|
138
|
+
request,
|
|
139
|
+
response,
|
|
140
|
+
validators
|
|
141
|
+
} = options;
|
|
142
|
+
if (response.committed) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const acceptsByteRange = isByteRangeRequestMethod(request.method);
|
|
146
|
+
const range = resolveByteRange(acceptsByteRange ? readFirstNonEmptyRequestHeaderValue(request, 'range') : undefined, entry.size, acceptsByteRange && matchesIfRange(request, validators));
|
|
147
|
+
const isHead = request.method.toUpperCase() === 'HEAD';
|
|
148
|
+
const bytes = toBytes(entry.source);
|
|
149
|
+
if (range.kind === 'unsatisfiable') {
|
|
150
|
+
options.applySuccessResponseMetadata();
|
|
151
|
+
if (acceptsByteRange) {
|
|
152
|
+
response.setHeader('Accept-Ranges', 'bytes');
|
|
153
|
+
}
|
|
154
|
+
if (!hasHeader(response, 'content-type')) {
|
|
155
|
+
response.setHeader('Content-Type', entry.contentType);
|
|
156
|
+
}
|
|
157
|
+
response.setStatus(416);
|
|
158
|
+
response.setHeader('Content-Length', '0');
|
|
159
|
+
response.setHeader('Content-Range', `bytes */${entry.size}`);
|
|
160
|
+
await response.send(undefined, sendOptions(options.compression));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
let reader;
|
|
164
|
+
let stream;
|
|
165
|
+
if (!isHead && !bytes) {
|
|
166
|
+
const streamSource = openByteRangeStream(entry.source);
|
|
167
|
+
if (!streamSource) {
|
|
168
|
+
throw new TypeError('A byte-range response must contain bytes or a portable readable stream.');
|
|
169
|
+
}
|
|
170
|
+
reader = streamSource.getReader();
|
|
171
|
+
try {
|
|
172
|
+
stream = response.stream;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
await cancelAndReleaseReader(reader);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
if (!stream) {
|
|
178
|
+
await cancelAndReleaseReader(reader);
|
|
179
|
+
throw new TypeError('The active HTTP adapter cannot write a portable byte stream.');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
options.applySuccessResponseMetadata();
|
|
183
|
+
if (acceptsByteRange) {
|
|
184
|
+
response.setHeader('Accept-Ranges', 'bytes');
|
|
185
|
+
}
|
|
186
|
+
if (!hasHeader(response, 'content-type')) {
|
|
187
|
+
response.setHeader('Content-Type', entry.contentType);
|
|
188
|
+
}
|
|
189
|
+
if (range.kind === 'partial') {
|
|
190
|
+
response.setStatus(206);
|
|
191
|
+
response.setHeader('Content-Length', String(range.end - range.start + 1));
|
|
192
|
+
response.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${entry.size}`);
|
|
193
|
+
} else {
|
|
194
|
+
response.setHeader('Content-Length', String(entry.size));
|
|
195
|
+
}
|
|
196
|
+
if (isHead) {
|
|
197
|
+
await response.send(undefined, sendOptions(options.compression));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (bytes) {
|
|
201
|
+
await response.send(range.kind === 'partial' ? bytes.slice(range.start, range.end + 1) : bytes, sendOptions(options.compression));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!reader || !stream) {
|
|
205
|
+
throw new TypeError('A byte-range response must contain an opened portable readable stream.');
|
|
206
|
+
}
|
|
207
|
+
response.committed = true;
|
|
208
|
+
await writeReadableStream(reader, range.kind === 'partial' ? range.start : 0, range.kind === 'partial' ? range.end : entry.size - 1, request, stream);
|
|
209
|
+
}
|
|
210
|
+
function sendOptions(compression) {
|
|
211
|
+
return compression === false ? {
|
|
212
|
+
compression: false
|
|
213
|
+
} : undefined;
|
|
214
|
+
}
|
|
215
|
+
function hasHeader(response, name) {
|
|
216
|
+
const lowerName = name.toLowerCase();
|
|
217
|
+
return Object.keys(response.headers).some(header => header.toLowerCase() === lowerName);
|
|
218
|
+
}
|
|
219
|
+
function toBytes(source) {
|
|
220
|
+
if (source instanceof Uint8Array) {
|
|
221
|
+
return source;
|
|
222
|
+
}
|
|
223
|
+
if (source instanceof ArrayBuffer) {
|
|
224
|
+
return new Uint8Array(source);
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
function isByteRangeRequestMethod(method) {
|
|
229
|
+
const normalizedMethod = method.toUpperCase();
|
|
230
|
+
return normalizedMethod === 'GET' || normalizedMethod === 'HEAD';
|
|
231
|
+
}
|
|
232
|
+
function isReadableStream(source) {
|
|
233
|
+
return typeof source === 'object' && source !== null && 'getReader' in source && typeof source.getReader === 'function';
|
|
234
|
+
}
|
|
235
|
+
function openByteRangeStream(source) {
|
|
236
|
+
const stream = typeof source === 'function' ? source() : source;
|
|
237
|
+
return isReadableStream(stream) ? stream : undefined;
|
|
238
|
+
}
|
|
239
|
+
async function cancelAndReleaseReader(reader) {
|
|
240
|
+
void reader.cancel().catch(() => undefined);
|
|
241
|
+
reader.releaseLock();
|
|
242
|
+
}
|
|
243
|
+
async function writeReadableStream(reader, start, end, request, stream) {
|
|
244
|
+
let skip = start;
|
|
245
|
+
let remaining = end - start + 1;
|
|
246
|
+
let stopped = false;
|
|
247
|
+
let hasTransportFailure = false;
|
|
248
|
+
let transportFailure;
|
|
249
|
+
let resolveStop = () => {};
|
|
250
|
+
let resolveTransportFailure = () => {};
|
|
251
|
+
let cancellation;
|
|
252
|
+
const stopPromise = new Promise(resolve => {
|
|
253
|
+
resolveStop = resolve;
|
|
254
|
+
});
|
|
255
|
+
const transportFailurePromise = new Promise(resolve => {
|
|
256
|
+
resolveTransportFailure = resolve;
|
|
257
|
+
});
|
|
258
|
+
const cancel = () => {
|
|
259
|
+
cancellation ??= reader.cancel();
|
|
260
|
+
void cancellation.catch(() => undefined);
|
|
261
|
+
return cancellation;
|
|
262
|
+
};
|
|
263
|
+
const stop = () => {
|
|
264
|
+
if (!stopped) {
|
|
265
|
+
stopped = true;
|
|
266
|
+
resolveStop();
|
|
267
|
+
}
|
|
268
|
+
void cancel();
|
|
269
|
+
};
|
|
270
|
+
const fail = error => {
|
|
271
|
+
if (!hasTransportFailure) {
|
|
272
|
+
hasTransportFailure = true;
|
|
273
|
+
transportFailure = error;
|
|
274
|
+
resolveTransportFailure();
|
|
275
|
+
}
|
|
276
|
+
stop();
|
|
277
|
+
};
|
|
278
|
+
const removeCloseListener = stream.onClose?.(stop);
|
|
279
|
+
const removeErrorListener = stream.onError?.(fail);
|
|
280
|
+
stream.disableCompression?.();
|
|
281
|
+
request.signal?.addEventListener('abort', stop, {
|
|
282
|
+
once: true
|
|
283
|
+
});
|
|
284
|
+
if (isByteRangeRequestAborted(request)) {
|
|
285
|
+
stop();
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
while (remaining > 0 && !stopped && !stream.closed) {
|
|
289
|
+
if (isByteRangeRequestAborted(request)) {
|
|
290
|
+
stop();
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
const result = await raceWithStop(reader.read(), stopPromise, transportFailurePromise);
|
|
294
|
+
if (!result || stopped || hasTransportFailure || isByteRangeRequestAborted(request)) {
|
|
295
|
+
stop();
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
const {
|
|
299
|
+
done,
|
|
300
|
+
value
|
|
301
|
+
} = result;
|
|
302
|
+
if (done) {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
if (skip >= value.byteLength) {
|
|
306
|
+
skip -= value.byteLength;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const chunkStart = skip;
|
|
310
|
+
skip = 0;
|
|
311
|
+
const chunkEnd = Math.min(value.byteLength, chunkStart + remaining);
|
|
312
|
+
const chunk = value.subarray(chunkStart, chunkEnd);
|
|
313
|
+
remaining -= chunk.byteLength;
|
|
314
|
+
if (chunk.byteLength > 0 && stream.write(chunk) === false) {
|
|
315
|
+
const drain = stream.waitForDrain?.();
|
|
316
|
+
if (drain) {
|
|
317
|
+
await raceWithStop(drain, stopPromise, transportFailurePromise);
|
|
318
|
+
}
|
|
319
|
+
if (isByteRangeRequestAborted(request)) {
|
|
320
|
+
stop();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
} finally {
|
|
325
|
+
request.signal?.removeEventListener('abort', stop);
|
|
326
|
+
removeCloseListener?.();
|
|
327
|
+
removeErrorListener?.();
|
|
328
|
+
void cancel();
|
|
329
|
+
reader.releaseLock();
|
|
330
|
+
if (!stream.closed && !hasTransportFailure) {
|
|
331
|
+
stream.close();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (hasTransportFailure) {
|
|
335
|
+
throw transportFailure;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function isByteRangeRequestAborted(request) {
|
|
339
|
+
return request.signal?.aborted === true || request.isAborted?.() === true;
|
|
340
|
+
}
|
|
341
|
+
async function raceWithStop(operation, stop, transportFailure) {
|
|
342
|
+
const terminalSignals = [operation, stop.then(() => undefined)];
|
|
343
|
+
if (transportFailure) {
|
|
344
|
+
terminalSignals.push(transportFailure.then(() => undefined));
|
|
345
|
+
}
|
|
346
|
+
return await Promise.race([...terminalSignals]);
|
|
347
|
+
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import type { FrameworkRequest } from './types.js';
|
|
2
|
+
import { type TrustProxyPolicy } from './connection.js';
|
|
2
3
|
interface ClientIdentityResolutionOptions {
|
|
4
|
+
trustProxy?: TrustProxyPolicy;
|
|
3
5
|
trustProxyHeaders?: boolean;
|
|
4
6
|
}
|
|
5
7
|
/**
|
|
6
8
|
* Resolve one stable client identity from the normalized request contract.
|
|
7
9
|
*
|
|
8
10
|
* By default, resolution uses only the raw socket's `remoteAddress`. When
|
|
9
|
-
* `trustProxyHeaders`
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* `trustProxyHeaders` enables the broad legacy compatibility path: it accepts
|
|
12
|
+
* the complete forwarding chain from `Forwarded`, `X-Forwarded-For`, or
|
|
13
|
+
* `X-Real-IP` without a deployment-specific peer boundary. Prefer
|
|
14
|
+
* `trustProxy` for new deployments. If no trusted identity is available,
|
|
15
|
+
* callers must provide an explicit resolver because falling back to a shared
|
|
16
|
+
* `unknown` bucket is not safe in proxied or serverless environments.
|
|
13
17
|
*
|
|
14
18
|
* @param request Adapter-normalized request whose headers/raw transport state should be inspected.
|
|
15
19
|
* @param options Client-identity trust settings for proxy-header handling.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-identity.d.ts","sourceRoot":"","sources":["../src/client-identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"client-identity.d.ts","sourceRoot":"","sources":["../src/client-identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,KAAK,gBAAgB,EAAyB,MAAM,iBAAiB,CAAC;AAE/E,UAAU,+BAA+B;IACvC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,gBAAgB,EACzB,OAAO,GAAE,+BAAoC,GAC5C,MAAM,CAaR"}
|
package/dist/client-identity.js
CHANGED
|
@@ -1,97 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
const X_FORWARDED_FOR_HEADER = 'x-forwarded-for';
|
|
3
|
-
const X_REAL_IP_HEADER = 'x-real-ip';
|
|
4
|
-
function readHeader(headers, name) {
|
|
5
|
-
const direct = headers[name];
|
|
6
|
-
if (typeof direct === 'string') {
|
|
7
|
-
return direct;
|
|
8
|
-
}
|
|
9
|
-
if (Array.isArray(direct)) {
|
|
10
|
-
return direct.find(value => value.trim().length > 0);
|
|
11
|
-
}
|
|
12
|
-
const match = Object.entries(headers).find(([key]) => key.toLowerCase() === name);
|
|
13
|
-
const value = match?.[1];
|
|
14
|
-
if (typeof value === 'string') {
|
|
15
|
-
return value;
|
|
16
|
-
}
|
|
17
|
-
return value?.find(entry => entry.trim().length > 0);
|
|
18
|
-
}
|
|
19
|
-
function normalizeClientIdentity(value) {
|
|
20
|
-
if (!value) {
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
let normalized = value.trim();
|
|
24
|
-
if (!normalized || normalized.toLowerCase() === 'unknown') {
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
if (normalized.startsWith('"') && normalized.endsWith('"')) {
|
|
28
|
-
normalized = normalized.slice(1, -1).trim();
|
|
29
|
-
}
|
|
30
|
-
const bracketedHostPort = normalized.match(/^\[(.+)]:(\d+)$/);
|
|
31
|
-
if (bracketedHostPort) {
|
|
32
|
-
return bracketedHostPort[1]?.trim() || undefined;
|
|
33
|
-
}
|
|
34
|
-
if (normalized.startsWith('[') && normalized.endsWith(']')) {
|
|
35
|
-
normalized = normalized.slice(1, -1).trim();
|
|
36
|
-
}
|
|
37
|
-
const ipv4HostPort = normalized.match(/^((?:\d{1,3}\.){3}\d{1,3}):(\d+)$/);
|
|
38
|
-
if (ipv4HostPort) {
|
|
39
|
-
return ipv4HostPort[1]?.trim() || undefined;
|
|
40
|
-
}
|
|
41
|
-
return normalized || undefined;
|
|
42
|
-
}
|
|
43
|
-
function resolveForwardedClientIdentity(headers) {
|
|
44
|
-
const forwarded = readHeader(headers, FORWARDED_HEADER);
|
|
45
|
-
if (!forwarded) {
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
for (const field of forwarded.split(',')) {
|
|
49
|
-
for (const part of field.split(';')) {
|
|
50
|
-
const separator = part.indexOf('=');
|
|
51
|
-
if (separator === -1) {
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
const key = part.slice(0, separator).trim().toLowerCase();
|
|
55
|
-
if (key !== 'for') {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
const normalized = normalizeClientIdentity(part.slice(separator + 1));
|
|
59
|
-
if (normalized) {
|
|
60
|
-
return normalized;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return undefined;
|
|
65
|
-
}
|
|
66
|
-
function resolveCommaSeparatedClientIdentity(headers, headerName) {
|
|
67
|
-
const headerValue = readHeader(headers, headerName);
|
|
68
|
-
if (!headerValue) {
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
for (const value of headerValue.split(',')) {
|
|
72
|
-
const normalized = normalizeClientIdentity(value);
|
|
73
|
-
if (normalized) {
|
|
74
|
-
return normalized;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
function resolveSocketClientIdentity(raw) {
|
|
80
|
-
if (!raw || typeof raw !== 'object') {
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
const socket = raw.socket;
|
|
84
|
-
return typeof socket?.remoteAddress === 'string' ? normalizeClientIdentity(socket.remoteAddress) : undefined;
|
|
85
|
-
}
|
|
86
|
-
|
|
1
|
+
import { resolveHttpConnection } from './connection.js';
|
|
87
2
|
/**
|
|
88
3
|
* Resolve one stable client identity from the normalized request contract.
|
|
89
4
|
*
|
|
90
5
|
* By default, resolution uses only the raw socket's `remoteAddress`. When
|
|
91
|
-
* `trustProxyHeaders`
|
|
92
|
-
* `
|
|
93
|
-
*
|
|
94
|
-
*
|
|
6
|
+
* `trustProxyHeaders` enables the broad legacy compatibility path: it accepts
|
|
7
|
+
* the complete forwarding chain from `Forwarded`, `X-Forwarded-For`, or
|
|
8
|
+
* `X-Real-IP` without a deployment-specific peer boundary. Prefer
|
|
9
|
+
* `trustProxy` for new deployments. If no trusted identity is available,
|
|
10
|
+
* callers must provide an explicit resolver because falling back to a shared
|
|
11
|
+
* `unknown` bucket is not safe in proxied or serverless environments.
|
|
95
12
|
*
|
|
96
13
|
* @param request Adapter-normalized request whose headers/raw transport state should be inspected.
|
|
97
14
|
* @param options Client-identity trust settings for proxy-header handling.
|
|
@@ -99,10 +16,12 @@ function resolveSocketClientIdentity(raw) {
|
|
|
99
16
|
* @throws Error When the request exposes no trusted proxy header or socket identity.
|
|
100
17
|
*/
|
|
101
18
|
export function resolveClientIdentity(request, options = {}) {
|
|
102
|
-
const
|
|
103
|
-
|
|
19
|
+
const clientIdentity = resolveHttpConnection(request, {
|
|
20
|
+
allowForwardedWithoutPeer: options.trustProxyHeaders === true,
|
|
21
|
+
trustProxy: options.trustProxy ?? (options.trustProxyHeaders ? Number.MAX_SAFE_INTEGER : false)
|
|
22
|
+
}).clientAddress;
|
|
104
23
|
if (clientIdentity) {
|
|
105
24
|
return clientIdentity;
|
|
106
25
|
}
|
|
107
|
-
throw new Error('Unable to resolve client identity from the trusted request transport.
|
|
26
|
+
throw new Error('Unable to resolve client identity from the trusted request transport. Configure resolveHttpConnection(...) with an explicit trust policy, or provide an explicit keyResolver/keyGenerator for this environment.');
|
|
108
27
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HandlerDescriptor } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Attach the deterministic compiler identity to a handler descriptor.
|
|
4
|
+
*
|
|
5
|
+
* @param descriptor Handler descriptor produced by route compilation.
|
|
6
|
+
* @param identity Stable identity assigned to the compiled route.
|
|
7
|
+
* @returns The descriptor with its non-writable compiler identity attached.
|
|
8
|
+
*/
|
|
9
|
+
export declare function attachCompiledRouteIdentity<Descriptor extends HandlerDescriptor>(descriptor: Descriptor, identity: string): Descriptor;
|
|
10
|
+
/**
|
|
11
|
+
* Read the deterministic identity assigned by the HTTP route compiler.
|
|
12
|
+
*
|
|
13
|
+
* @param descriptor Compiled handler descriptor produced by `createHandlerMapping(...)`.
|
|
14
|
+
* @returns The compiler identity, or `undefined` for a manually authored descriptor.
|
|
15
|
+
*/
|
|
16
|
+
export declare function getCompiledRouteIdentity(descriptor: HandlerDescriptor): string | undefined;
|
|
17
|
+
//# sourceMappingURL=compiled-route-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compiled-route-identity.d.ts","sourceRoot":"","sources":["../src/compiled-route-identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAIpD;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,UAAU,SAAS,iBAAiB,EAC9E,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,MAAM,GACf,UAAU,CASZ;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,UAAU,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,CAI1F"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const COMPILED_ROUTE_IDENTITY_SYMBOL = Symbol.for('fluo.http.compiledRouteIdentity');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Attach the deterministic compiler identity to a handler descriptor.
|
|
5
|
+
*
|
|
6
|
+
* @param descriptor Handler descriptor produced by route compilation.
|
|
7
|
+
* @param identity Stable identity assigned to the compiled route.
|
|
8
|
+
* @returns The descriptor with its non-writable compiler identity attached.
|
|
9
|
+
*/
|
|
10
|
+
export function attachCompiledRouteIdentity(descriptor, identity) {
|
|
11
|
+
Object.defineProperty(descriptor, COMPILED_ROUTE_IDENTITY_SYMBOL, {
|
|
12
|
+
configurable: false,
|
|
13
|
+
enumerable: true,
|
|
14
|
+
value: identity,
|
|
15
|
+
writable: false
|
|
16
|
+
});
|
|
17
|
+
return descriptor;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read the deterministic identity assigned by the HTTP route compiler.
|
|
22
|
+
*
|
|
23
|
+
* @param descriptor Compiled handler descriptor produced by `createHandlerMapping(...)`.
|
|
24
|
+
* @returns The compiler identity, or `undefined` for a manually authored descriptor.
|
|
25
|
+
*/
|
|
26
|
+
export function getCompiledRouteIdentity(descriptor) {
|
|
27
|
+
const identity = Reflect.get(descriptor, COMPILED_ROUTE_IDENTITY_SYMBOL);
|
|
28
|
+
return typeof identity === 'string' ? identity : undefined;
|
|
29
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { FrameworkRequest } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Function that decides whether one proxy hop is trusted.
|
|
4
|
+
*
|
|
5
|
+
* @param address Proxy address being considered, starting with the direct transport peer.
|
|
6
|
+
* @param index Zero-based distance from the direct transport peer.
|
|
7
|
+
* @returns Whether the proxy hop may supply forwarding metadata.
|
|
8
|
+
*/
|
|
9
|
+
export type TrustProxyPredicate = (address: string, index: number) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Explicit policy that permits forwarding metadata from known proxy hops.
|
|
12
|
+
*
|
|
13
|
+
* `false` disables proxy trust, a number trusts that many nearest hops, an
|
|
14
|
+
* address/CIDR list matches nearest hops, and a predicate receives each hop
|
|
15
|
+
* from nearest to furthest.
|
|
16
|
+
*/
|
|
17
|
+
export type TrustProxyPolicy = false | number | readonly string[] | TrustProxyPredicate;
|
|
18
|
+
/**
|
|
19
|
+
* Options used to derive a trusted connection snapshot.
|
|
20
|
+
*/
|
|
21
|
+
export interface ResolveHttpConnectionOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Proxy trust boundary. The default is `false`, so forwarded headers never
|
|
24
|
+
* override the direct transport identity without an explicit policy.
|
|
25
|
+
*/
|
|
26
|
+
trustProxy?: TrustProxyPolicy;
|
|
27
|
+
/**
|
|
28
|
+
* Retains the legacy header-only migration path for integrations that do not
|
|
29
|
+
* expose a transport peer. New integrations must leave this disabled.
|
|
30
|
+
*/
|
|
31
|
+
allowForwardedWithoutPeer?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Immutable, runtime-neutral view of the connection identity selected for a request.
|
|
35
|
+
*/
|
|
36
|
+
export interface HttpConnection {
|
|
37
|
+
/** First untrusted address in the client-to-server chain, when available. */
|
|
38
|
+
readonly clientAddress: string | undefined;
|
|
39
|
+
/** Public host selected from trusted forwarding metadata or the request host header. */
|
|
40
|
+
readonly host: string | undefined;
|
|
41
|
+
/** Host without an optional port selected from `host`. */
|
|
42
|
+
readonly hostname: string | undefined;
|
|
43
|
+
/** Numeric port selected from `host`, when present and valid. */
|
|
44
|
+
readonly port: number | undefined;
|
|
45
|
+
/** `http` or `https` selected from transport or trusted forwarding metadata. */
|
|
46
|
+
readonly protocol: 'http' | 'https';
|
|
47
|
+
/** Trusted proxy addresses between `clientAddress` and `remoteAddress`. */
|
|
48
|
+
readonly proxyChain: readonly string[];
|
|
49
|
+
/** Direct adapter transport peer address, when available. */
|
|
50
|
+
readonly remoteAddress: string | undefined;
|
|
51
|
+
/** Whether `protocol` is `https`. */
|
|
52
|
+
readonly secure: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve an immutable connection snapshot using a direct transport identity
|
|
56
|
+
* and only proxy metadata allowed by the explicit trust policy.
|
|
57
|
+
*
|
|
58
|
+
* Malformed forwarding input is ignored as a whole, preserving the direct
|
|
59
|
+
* transport identity. Adapters without a socket address produce an undefined
|
|
60
|
+
* identity rather than trusting a forwarding header by default.
|
|
61
|
+
*
|
|
62
|
+
* @param request Adapter-normalized request whose headers and raw transport are inspected.
|
|
63
|
+
* @param options Explicit proxy trust policy for the current deployment.
|
|
64
|
+
* @returns Immutable connection identity, protocol, host, and trusted proxy chain.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveHttpConnection(request: FrameworkRequest, options?: ResolveHttpConnectionOptions): HttpConnection;
|
|
67
|
+
//# sourceMappingURL=connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AA4BnD;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;AAE9E;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,mBAAmB,CAAC;AAExF;;GAEG;AACH,MAAM,WAAW,4BAA4B;IAC3C;;;OAGG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B;;;OAGG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,wFAAwF;IACxF,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,gFAAgF;IAChF,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IACpC,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,6DAA6D;IAC7D,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,qCAAqC;IACrC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AA8gBD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,gBAAgB,EACzB,OAAO,GAAE,4BAAiC,GACzC,cAAc,CAmDhB"}
|