@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/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
- trustProxy = false,
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 `trustProxy` is false (default) the
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 `''`). When true,
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
- const ip = trustProxy ? getEdgeClientIp(request, true) : '';
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
- super(request, ip, runtime, { runTextStream, runSSEStream, runNDJSONStream });
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
- trustProxy = false,
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, trustProxy, env);
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 F-09: unused internally (superseded by `@nextrush/body-parser`'s
14
- * own content-type parsing). Kept for backward compatibility per the public-API
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 F-09: unused internally (superseded by `@nextrush/body-parser`'s
25
- * own content-length handling). Kept for backward compatibility per the
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');