@nextrush/adapter-edge 1.0.0-beta.0 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -4
- package/dist/index.d.ts +72 -19
- package/dist/index.js +63 -9
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/src/__tests__/boot-reuse-warning.test.ts +95 -0
- package/src/__tests__/context-ip-warning.test.ts +67 -0
- package/src/__tests__/context-platform.test.ts +41 -0
- package/src/__tests__/context.test.ts +40 -5
- package/src/__tests__/default-timeout.test.ts +5 -2
- package/src/__tests__/per-request-work-trim.test.ts +14 -14
- package/src/__tests__/timeout-attribution.test.ts +80 -0
- package/src/adapter.ts +84 -14
- package/src/context.ts +86 -10
- package/src/index.ts +1 -0
- package/src/utils.ts +4 -7
package/src/context.ts
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* @packageDocumentation
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { detectEdgeRuntime, getEdgeClientIp, WebContextBase } from '@nextrush/runtime';
|
|
19
|
+
import { detectEdgeRuntime, detectPlatform, getEdgeClientIp, WebContextBase } from '@nextrush/runtime';
|
|
20
|
+
import type { PlatformId, ProxyTrust } from '@nextrush/types';
|
|
20
21
|
import { runNDJSONStream, runSSEStream, runTextStream } from '@nextrush/stream';
|
|
21
22
|
|
|
22
23
|
/**
|
|
@@ -66,45 +67,118 @@ export class EdgeContext<Env = unknown> extends WebContextBase {
|
|
|
66
67
|
*/
|
|
67
68
|
readonly env?: Env;
|
|
68
69
|
|
|
70
|
+
/** Whether {@link waitUntil} has already warned once on this context (P2-4). */
|
|
71
|
+
private _waitUntilWarned = false;
|
|
72
|
+
/** Dev-mode gate for the {@link waitUntil} no-op warning (P2-4). */
|
|
73
|
+
private readonly _isProduction: boolean;
|
|
74
|
+
/** Whether the empty-`ctx.ip` warning has already fired once on this context (P2-3). */
|
|
75
|
+
private _ipWarned = false;
|
|
76
|
+
/** Whether the caller opted into trusting proxy headers for IP resolution. */
|
|
77
|
+
private readonly _proxy: ProxyTrust;
|
|
78
|
+
|
|
69
79
|
constructor(
|
|
70
80
|
request: Request,
|
|
71
81
|
executionContext?: EdgeExecutionContext,
|
|
72
|
-
|
|
73
|
-
env?: Env
|
|
82
|
+
proxy: ProxyTrust = false,
|
|
83
|
+
env?: Env,
|
|
84
|
+
isProduction = true,
|
|
85
|
+
platform?: PlatformId
|
|
74
86
|
) {
|
|
75
87
|
// Get client IP from CF headers or standard headers.
|
|
76
88
|
//
|
|
77
|
-
// HP-1 trim: Edge has no socket, so when `
|
|
89
|
+
// HP-1 trim: Edge has no socket, so when `proxy` is false (default) the
|
|
78
90
|
// client IP is `''` — returned directly, with no per-request header-lookup
|
|
79
91
|
// closure and no `getEdgeClientIp` policy call (byte-identical to
|
|
80
|
-
// `getEdgeClientIp(request, false)`, whose `directIp` is `''`).
|
|
92
|
+
// `getEdgeClientIp(request, false)`, whose `directIp` is `''`). Otherwise
|
|
81
93
|
// resolution still goes through `getEdgeClientIp`, preserving the Cloudflare
|
|
82
94
|
// `cf-connecting-ip` → `x-forwarded-for` → `x-real-ip` precedence.
|
|
83
|
-
|
|
95
|
+
//
|
|
96
|
+
// A `string[]` (CIDR peer list) trust is rejected here rather than at
|
|
97
|
+
// `createApp()` construction: Edge has no socket peer address to check
|
|
98
|
+
// that list against (RFC-030 §8.6), so the constraint is specific to this
|
|
99
|
+
// adapter, not the app-level `proxy` option in general.
|
|
100
|
+
if (Array.isArray(proxy)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
"proxy: ['<cidr>', ...] is not supported on the Edge adapter — there is no socket peer " +
|
|
103
|
+
'address to validate the trusted-peer list against. Use `proxy: <hopCount>` instead ' +
|
|
104
|
+
'(e.g. `proxy: 1` for one trusted reverse proxy such as Cloudflare).'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const ip = proxy !== false ? getEdgeClientIp(request, proxy) : '';
|
|
84
108
|
|
|
85
109
|
// Detect specific edge runtime
|
|
86
110
|
const runtime = detectEdgeRuntime().runtime;
|
|
87
111
|
|
|
88
|
-
|
|
112
|
+
// RFC-026: an explicitly-supplied platform (from a serverless Tier-1
|
|
113
|
+
// handler, which already knows its own identity) always wins over
|
|
114
|
+
// detection; otherwise fall back to the same three-branch edge-platform
|
|
115
|
+
// probe `detectEdgeRuntime()` already performs.
|
|
116
|
+
const resolvedPlatform = platform ?? detectPlatform().platform;
|
|
117
|
+
|
|
118
|
+
super(request, ip, runtime, { runTextStream, runSSEStream, runNDJSONStream }, resolvedPlatform);
|
|
89
119
|
|
|
90
120
|
this.executionContext = executionContext;
|
|
91
121
|
this.env = env;
|
|
122
|
+
this._isProduction = isProduction;
|
|
123
|
+
this._proxy = proxy;
|
|
92
124
|
}
|
|
93
125
|
|
|
94
126
|
// ===========================================================================
|
|
95
127
|
// Edge-Specific Methods
|
|
96
128
|
// ===========================================================================
|
|
97
129
|
|
|
130
|
+
/**
|
|
131
|
+
* The resolved client IP.
|
|
132
|
+
*
|
|
133
|
+
* @remarks
|
|
134
|
+
* `''` when `trustProxy` is `false` (the default) — Edge has no socket to
|
|
135
|
+
* fall back to, unlike Node. Outside production, the first read of this
|
|
136
|
+
* getter in that state warns once per context (P2-3): a silent empty
|
|
137
|
+
* string is indistinguishable from "resolution ran and found nothing,"
|
|
138
|
+
* which silently degrades IP-keyed middleware (rate limiting, audit
|
|
139
|
+
* logging) with no signal that `trustProxy` is the fix.
|
|
140
|
+
*/
|
|
141
|
+
override get ip(): string {
|
|
142
|
+
if (this._ip === '' && this._proxy === false && !this._isProduction && !this._ipWarned) {
|
|
143
|
+
this._ipWarned = true;
|
|
144
|
+
console.warn(
|
|
145
|
+
'[nextrush/edge] ctx.ip is an empty string because proxy is false (the default) — ' +
|
|
146
|
+
'Edge has no socket to fall back to, so no proxy headers means no IP. If this app runs ' +
|
|
147
|
+
'behind a trusted proxy (e.g. Cloudflare, which sets cf-connecting-ip), pass ' +
|
|
148
|
+
"{ proxy: 1 } to createApp() to resolve it from headers. (This message appears in " +
|
|
149
|
+
'development only.)'
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return this._ip;
|
|
153
|
+
}
|
|
154
|
+
|
|
98
155
|
/**
|
|
99
156
|
* Extend request lifetime for async operations
|
|
100
157
|
*
|
|
101
158
|
* @remarks
|
|
102
159
|
* Use this for fire-and-forget operations that should complete
|
|
103
160
|
* after the response is sent (logging, analytics, etc.)
|
|
161
|
+
*
|
|
162
|
+
* Silently does nothing when the platform provides no execution context
|
|
163
|
+
* (e.g. Vercel Edge, or a serverless invocation with no `waitUntil` support)
|
|
164
|
+
* — the same as always. Outside production, the first such call additionally
|
|
165
|
+
* warns once per context, since a dropped promise here is otherwise
|
|
166
|
+
* undebuggable (P2-4).
|
|
104
167
|
*/
|
|
105
168
|
waitUntil(promise: Promise<unknown>): void {
|
|
106
169
|
if (this.executionContext?.waitUntil) {
|
|
107
170
|
this.executionContext.waitUntil(promise);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!this._isProduction && !this._waitUntilWarned) {
|
|
174
|
+
this._waitUntilWarned = true;
|
|
175
|
+
console.warn(
|
|
176
|
+
'[nextrush/edge] ctx.waitUntil() was called, but this platform provided no execution ' +
|
|
177
|
+
'context — the promise was dropped without running. This is expected on platforms with ' +
|
|
178
|
+
'no background-task support (e.g. Vercel Edge); on Cloudflare Workers it usually means ' +
|
|
179
|
+
'`ctx` was not passed through to createCloudflareHandler. (This message appears in ' +
|
|
180
|
+
'development only.)'
|
|
181
|
+
);
|
|
108
182
|
}
|
|
109
183
|
}
|
|
110
184
|
}
|
|
@@ -115,8 +189,10 @@ export class EdgeContext<Env = unknown> extends WebContextBase {
|
|
|
115
189
|
export function createEdgeContext<Env = unknown>(
|
|
116
190
|
request: Request,
|
|
117
191
|
executionContext?: EdgeExecutionContext,
|
|
118
|
-
|
|
119
|
-
env?: Env
|
|
192
|
+
proxy: ProxyTrust = false,
|
|
193
|
+
env?: Env,
|
|
194
|
+
isProduction = true,
|
|
195
|
+
platform?: PlatformId
|
|
120
196
|
): EdgeContext<Env> {
|
|
121
|
-
return new EdgeContext<Env>(request, executionContext,
|
|
197
|
+
return new EdgeContext<Env>(request, executionContext, proxy, env, isProduction, platform);
|
|
122
198
|
}
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
export {
|
|
16
16
|
createCloudflareHandler,
|
|
17
17
|
createFetchHandler,
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- barrel re-export of a deprecated alias is not a usage site; kept for backward compatibility until the next major (P3-1).
|
|
18
19
|
createHandler,
|
|
19
20
|
createNetlifyHandler,
|
|
20
21
|
createVercelHandler,
|
package/src/utils.ts
CHANGED
|
@@ -10,9 +10,8 @@ export type { EdgeRuntimeInfo } from '@nextrush/runtime';
|
|
|
10
10
|
/**
|
|
11
11
|
* Get Content-Type header value
|
|
12
12
|
*
|
|
13
|
-
* @deprecated
|
|
14
|
-
*
|
|
15
|
-
* contract (deprecate-before-remove); will be removed in a future major.
|
|
13
|
+
* @deprecated Unused internally (superseded by `@nextrush/body-parser`'s own
|
|
14
|
+
* content-type parsing). Will be removed in `2.0.0`.
|
|
16
15
|
*/
|
|
17
16
|
export function getContentType(headers: Headers): string | undefined {
|
|
18
17
|
return headers.get('content-type') ?? undefined;
|
|
@@ -21,10 +20,8 @@ export function getContentType(headers: Headers): string | undefined {
|
|
|
21
20
|
/**
|
|
22
21
|
* Get Content-Length header as number
|
|
23
22
|
*
|
|
24
|
-
* @deprecated
|
|
25
|
-
*
|
|
26
|
-
* public-API contract (deprecate-before-remove); will be removed in a future
|
|
27
|
-
* major.
|
|
23
|
+
* @deprecated Unused internally (superseded by `@nextrush/body-parser`'s own
|
|
24
|
+
* content-length handling). Will be removed in `2.0.0`.
|
|
28
25
|
*/
|
|
29
26
|
export function getContentLength(headers: Headers): number | undefined {
|
|
30
27
|
const value = headers.get('content-length');
|