@flightdev/edge 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-2026 Flight Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # @flight-framework/edge
2
+
3
+ Edge Runtime handlers for Flight Framework. Deploy to any edge provider with a unified API.
4
+
5
+ ## Philosophy
6
+
7
+ **Flight doesn't impose** - you choose your edge provider. All adapters are optional.
8
+
9
+ ## Features
10
+
11
+ - **Unified geo API** - Same geo data interface across all providers
12
+ - **Cloudflare Workers** - Full cf object access, KV, R2, D1
13
+ - **Vercel Edge** - Next.js compatible middleware and routes
14
+ - **Deno Deploy** - Native Deno.serve() integration
15
+ - **Provider portable** - Switch providers without code changes
16
+ - **Zero lock-in** - Standard Request/Response objects
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @flight-framework/edge
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```typescript
27
+ import { createEdgeHandler } from '@flight-framework/edge';
28
+
29
+ const handler = createEdgeHandler((request, ctx) => {
30
+ const { country, city } = ctx.geo;
31
+
32
+ // Fire-and-forget logging
33
+ ctx.waitUntil(logAnalytics(request));
34
+
35
+ return Response.json({ country, city });
36
+ });
37
+
38
+ export default handler;
39
+ ```
40
+
41
+ ## Adapters
42
+
43
+ ### Cloudflare Workers
44
+
45
+ ```typescript
46
+ import { createEdgeHandler } from '@flight-framework/edge';
47
+ import { createCloudflareHandler } from '@flight-framework/edge/cloudflare';
48
+
49
+ const handler = createEdgeHandler((req, ctx) => {
50
+ // Full geo data from Cloudflare
51
+ const { country, city, timezone } = ctx.geo;
52
+
53
+ // Cloudflare-specific properties
54
+ const isBot = ctx.cf?.isBot;
55
+ const colo = ctx.cf?.colo;
56
+
57
+ // Access KV, R2, D1 via env
58
+ const kv = ctx.env?.MY_KV;
59
+
60
+ return Response.json({ country, city, isBot });
61
+ });
62
+
63
+ export default createCloudflareHandler(handler);
64
+ ```
65
+
66
+ ### Vercel Edge
67
+
68
+ ```typescript
69
+ // app/api/geo/route.ts
70
+ import { createEdgeHandler } from '@flight-framework/edge';
71
+ import { createVercelHandler } from '@flight-framework/edge/vercel';
72
+
73
+ const handler = createEdgeHandler((req, ctx) => {
74
+ return Response.json({ country: ctx.geo.country });
75
+ });
76
+
77
+ export const GET = createVercelHandler(handler);
78
+ export const runtime = 'edge';
79
+ ```
80
+
81
+ ### Deno Deploy
82
+
83
+ ```typescript
84
+ import { createEdgeHandler } from '@flight-framework/edge';
85
+ import { serve } from '@flight-framework/edge/deno';
86
+
87
+ const handler = createEdgeHandler((req, ctx) => {
88
+ return new Response(`Hello from ${ctx.geo.country}!`);
89
+ });
90
+
91
+ serve(handler, { port: 8000 });
92
+ ```
93
+
94
+ ## EdgeContext API
95
+
96
+ ```typescript
97
+ interface EdgeContext {
98
+ // Geolocation data
99
+ geo: {
100
+ country?: string; // ISO country code (AR, US, DE)
101
+ city?: string; // City name
102
+ region?: string; // Region/state
103
+ latitude?: string;
104
+ longitude?: string;
105
+ timezone?: string; // IANA timezone
106
+ };
107
+
108
+ // Cloudflare-specific (only with CF adapter)
109
+ cf?: {
110
+ colo?: string; // Data center
111
+ isBot?: boolean;
112
+ asn?: number;
113
+ // ... more
114
+ };
115
+
116
+ // Environment bindings
117
+ env?: Record<string, unknown>;
118
+
119
+ // Lifecycle methods
120
+ waitUntil: (promise: Promise<unknown>) => void;
121
+ passThroughOnException?: () => void;
122
+ }
123
+ ```
124
+
125
+ ## Utilities
126
+
127
+ ```typescript
128
+ import {
129
+ isEdgeRuntime,
130
+ getEdgeRuntime,
131
+ getGeoFromRequest,
132
+ } from '@flight-framework/edge';
133
+
134
+ // Check runtime
135
+ if (isEdgeRuntime()) {
136
+ const runtime = getEdgeRuntime(); // 'cloudflare' | 'vercel' | 'deno'
137
+ }
138
+
139
+ // Extract geo from any request
140
+ const geo = getGeoFromRequest(request);
141
+ ```
142
+
143
+ ## License
144
+
145
+ MIT
@@ -0,0 +1,101 @@
1
+ import { EdgeHandler } from '../index.js';
2
+ import 'undici';
3
+
4
+ /**
5
+ * Cloudflare Workers Adapter
6
+ *
7
+ * Convert a Flight edge handler to a Cloudflare Workers ExportedHandler.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // src/worker.ts
12
+ * import { createEdgeHandler } from '@flightdev/edge';
13
+ * import { createCloudflareHandler } from '@flightdev/edge/cloudflare';
14
+ *
15
+ * const handler = createEdgeHandler((request, ctx) => {
16
+ * return new Response(`Hello from ${ctx.geo.country}!`);
17
+ * });
18
+ *
19
+ * export default createCloudflareHandler(handler);
20
+ * ```
21
+ */
22
+
23
+ interface CloudflareExecutionContext {
24
+ waitUntil(promise: Promise<unknown>): void;
25
+ passThroughOnException(): void;
26
+ }
27
+ type CloudflareEnv = Record<string, unknown>;
28
+ interface CloudflareExportedHandler {
29
+ fetch(request: Request, env: CloudflareEnv, ctx: CloudflareExecutionContext): Response | Promise<Response>;
30
+ }
31
+ /**
32
+ * Create a Cloudflare Workers compatible handler from a Flight edge handler.
33
+ *
34
+ * This adapter:
35
+ * - Extracts geo data from the `cf` object on the request
36
+ * - Provides `waitUntil` and `passThroughOnException` from execution context
37
+ * - Exposes environment bindings
38
+ *
39
+ * @param handler - Flight edge handler
40
+ * @returns Cloudflare ExportedHandler object
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * import { createEdgeHandler } from '@flightdev/edge';
45
+ * import { createCloudflareHandler } from '@flightdev/edge/cloudflare';
46
+ *
47
+ * const handler = createEdgeHandler(async (request, ctx) => {
48
+ * // Geo data from Cloudflare
49
+ * const { country, city, timezone } = ctx.geo;
50
+ *
51
+ * // Cloudflare-specific properties
52
+ * const isBot = ctx.cf?.isBot;
53
+ * const colo = ctx.cf?.colo;
54
+ *
55
+ * // Access environment bindings (KV, R2, D1, etc.)
56
+ * const kv = ctx.env?.MY_KV as KVNamespace;
57
+ *
58
+ * // Fire and forget analytics
59
+ * ctx.waitUntil(trackAnalytics({ country, path: request.url }));
60
+ *
61
+ * return Response.json({ country, city, isBot, colo });
62
+ * });
63
+ *
64
+ * export default createCloudflareHandler(handler);
65
+ * ```
66
+ */
67
+ declare function createCloudflareHandler(handler: EdgeHandler): CloudflareExportedHandler;
68
+ /**
69
+ * Create a Cloudflare Pages Function handler.
70
+ * Similar to Worker but adapted for Pages Functions context.
71
+ *
72
+ * @param handler - Flight edge handler
73
+ * @returns Pages Function handler
74
+ */
75
+ declare function createPagesHandler(handler: EdgeHandler): (context: {
76
+ request: Request;
77
+ env: CloudflareEnv;
78
+ waitUntil: (promise: Promise<unknown>) => void;
79
+ passThroughToOrigin: () => void;
80
+ }) => Promise<Response>;
81
+ /**
82
+ * Check if running in Cloudflare Workers environment.
83
+ */
84
+ declare function isCloudflareWorker(): boolean;
85
+ /**
86
+ * Get the colo (data center) that served the request.
87
+ * Useful for debugging and analytics.
88
+ */
89
+ declare function getColo(request: Request): string | undefined;
90
+ /**
91
+ * Check if the request is from a known bot.
92
+ * Uses Cloudflare's bot detection.
93
+ */
94
+ declare function isBot(request: Request): boolean;
95
+ /**
96
+ * Get bot score (0-100). Only available on Enterprise plans.
97
+ * Lower score = more likely to be a bot.
98
+ */
99
+ declare function getBotScore(request: Request): number | undefined;
100
+
101
+ export { createCloudflareHandler, createPagesHandler, getBotScore, getColo, isBot, isCloudflareWorker };
@@ -0,0 +1,76 @@
1
+ // src/adapters/cloudflare.ts
2
+ function createCloudflareHandler(handler) {
3
+ return {
4
+ async fetch(request, env, ctx) {
5
+ const cf = request.cf;
6
+ const geo = {
7
+ country: cf?.country,
8
+ city: cf?.city,
9
+ region: cf?.region ?? cf?.regionCode,
10
+ latitude: cf?.latitude,
11
+ longitude: cf?.longitude,
12
+ timezone: cf?.timezone,
13
+ continent: cf?.continent,
14
+ postalCode: cf?.postalCode,
15
+ metroCode: cf?.metroCode
16
+ };
17
+ const cfProperties = {
18
+ asn: cf?.asn,
19
+ asOrganization: cf?.asOrganization,
20
+ colo: cf?.colo,
21
+ httpProtocol: cf?.httpProtocol,
22
+ requestPriority: cf?.requestPriority,
23
+ tlsCipher: cf?.tlsCipher,
24
+ tlsVersion: cf?.tlsVersion,
25
+ isBot: cf?.isBot,
26
+ botScore: cf?.botScore,
27
+ verifiedBotCategory: cf?.verifiedBotCategory
28
+ };
29
+ const edgeContext = {
30
+ geo,
31
+ cf: cfProperties,
32
+ env,
33
+ waitUntil: ctx.waitUntil.bind(ctx),
34
+ passThroughOnException: ctx.passThroughOnException.bind(ctx)
35
+ };
36
+ return handler(request, edgeContext);
37
+ }
38
+ };
39
+ }
40
+ function createPagesHandler(handler) {
41
+ return async (context) => {
42
+ const cf = context.request.cf;
43
+ const geo = {
44
+ country: cf?.country,
45
+ city: cf?.city,
46
+ region: cf?.region,
47
+ latitude: cf?.latitude,
48
+ longitude: cf?.longitude,
49
+ timezone: cf?.timezone
50
+ };
51
+ const edgeContext = {
52
+ geo,
53
+ cf,
54
+ env: context.env,
55
+ waitUntil: context.waitUntil,
56
+ passThroughOnException: context.passThroughToOrigin
57
+ };
58
+ return handler(context.request, edgeContext);
59
+ };
60
+ }
61
+ function isCloudflareWorker() {
62
+ return typeof globalThis.WebSocketPair !== "undefined" && typeof globalThis.caches !== "undefined";
63
+ }
64
+ function getColo(request) {
65
+ return request.cf?.colo;
66
+ }
67
+ function isBot(request) {
68
+ return request.cf?.isBot ?? false;
69
+ }
70
+ function getBotScore(request) {
71
+ return request.cf?.botScore;
72
+ }
73
+
74
+ export { createCloudflareHandler, createPagesHandler, getBotScore, getColo, isBot, isCloudflareWorker };
75
+ //# sourceMappingURL=cloudflare.js.map
76
+ //# sourceMappingURL=cloudflare.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/cloudflare.ts"],"names":[],"mappings":";AAuGO,SAAS,wBAAwB,OAAA,EAAiD;AACrF,EAAA,OAAO;AAAA,IACH,MAAM,KAAA,CACF,OAAA,EACA,GAAA,EACA,GAAA,EACiB;AAEjB,MAAA,MAAM,KAAM,OAAA,CAAgB,EAAA;AAG5B,MAAA,MAAM,GAAA,GAAe;AAAA,QACjB,SAAS,EAAA,EAAI,OAAA;AAAA,QACb,MAAM,EAAA,EAAI,IAAA;AAAA,QACV,MAAA,EAAQ,EAAA,EAAI,MAAA,IAAU,EAAA,EAAI,UAAA;AAAA,QAC1B,UAAU,EAAA,EAAI,QAAA;AAAA,QACd,WAAW,EAAA,EAAI,SAAA;AAAA,QACf,UAAU,EAAA,EAAI,QAAA;AAAA,QACd,WAAW,EAAA,EAAI,SAAA;AAAA,QACf,YAAY,EAAA,EAAI,UAAA;AAAA,QAChB,WAAW,EAAA,EAAI;AAAA,OACnB;AAGA,MAAA,MAAM,YAAA,GAAqC;AAAA,QACvC,KAAK,EAAA,EAAI,GAAA;AAAA,QACT,gBAAgB,EAAA,EAAI,cAAA;AAAA,QACpB,MAAM,EAAA,EAAI,IAAA;AAAA,QACV,cAAc,EAAA,EAAI,YAAA;AAAA,QAClB,iBAAiB,EAAA,EAAI,eAAA;AAAA,QACrB,WAAW,EAAA,EAAI,SAAA;AAAA,QACf,YAAY,EAAA,EAAI,UAAA;AAAA,QAChB,OAAO,EAAA,EAAI,KAAA;AAAA,QACX,UAAU,EAAA,EAAI,QAAA;AAAA,QACd,qBAAqB,EAAA,EAAI;AAAA,OAC7B;AAGA,MAAA,MAAM,WAAA,GAA2B;AAAA,QAC7B,GAAA;AAAA,QACA,EAAA,EAAI,YAAA;AAAA,QACJ,GAAA;AAAA,QACA,SAAA,EAAW,GAAA,CAAI,SAAA,CAAU,IAAA,CAAK,GAAG,CAAA;AAAA,QACjC,sBAAA,EAAwB,GAAA,CAAI,sBAAA,CAAuB,IAAA,CAAK,GAAG;AAAA,OAC/D;AAEA,MAAA,OAAO,OAAA,CAAQ,SAAS,WAAW,CAAA;AAAA,IACvC;AAAA,GACJ;AACJ;AASO,SAAS,mBAAmB,OAAA,EAAsB;AACrD,EAAA,OAAO,OAAO,OAAA,KAKW;AACrB,IAAA,MAAM,EAAA,GAAM,QAAQ,OAAA,CAAgB,EAAA;AAEpC,IAAA,MAAM,GAAA,GAAe;AAAA,MACjB,SAAS,EAAA,EAAI,OAAA;AAAA,MACb,MAAM,EAAA,EAAI,IAAA;AAAA,MACV,QAAQ,EAAA,EAAI,MAAA;AAAA,MACZ,UAAU,EAAA,EAAI,QAAA;AAAA,MACd,WAAW,EAAA,EAAI,SAAA;AAAA,MACf,UAAU,EAAA,EAAI;AAAA,KAClB;AAEA,IAAA,MAAM,WAAA,GAA2B;AAAA,MAC7B,GAAA;AAAA,MACA,EAAA;AAAA,MACA,KAAK,OAAA,CAAQ,GAAA;AAAA,MACb,WAAW,OAAA,CAAQ,SAAA;AAAA,MACnB,wBAAwB,OAAA,CAAQ;AAAA,KACpC;AAEA,IAAA,OAAO,OAAA,CAAQ,OAAA,CAAQ,OAAA,EAAS,WAAW,CAAA;AAAA,EAC/C,CAAA;AACJ;AASO,SAAS,kBAAA,GAA8B;AAC1C,EAAA,OAAO,OAAQ,UAAA,CAAmB,aAAA,KAAkB,WAAA,IAChD,OAAQ,WAAmB,MAAA,KAAW,WAAA;AAC9C;AAMO,SAAS,QAAQ,OAAA,EAAsC;AAC1D,EAAA,OAAS,QAAgB,EAAA,EAAoC,IAAA;AACjE;AAMO,SAAS,MAAM,OAAA,EAA2B;AAC7C,EAAA,OAAS,OAAA,CAAgB,IAAoC,KAAA,IAAS,KAAA;AAC1E;AAMO,SAAS,YAAY,OAAA,EAAsC;AAC9D,EAAA,OAAS,QAAgB,EAAA,EAAoC,QAAA;AACjE","file":"cloudflare.js","sourcesContent":["/**\r\n * Cloudflare Workers Adapter\r\n * \r\n * Convert a Flight edge handler to a Cloudflare Workers ExportedHandler.\r\n * \r\n * @example\r\n * ```typescript\r\n * // src/worker.ts\r\n * import { createEdgeHandler } from '@flightdev/edge';\r\n * import { createCloudflareHandler } from '@flightdev/edge/cloudflare';\r\n * \r\n * const handler = createEdgeHandler((request, ctx) => {\r\n * return new Response(`Hello from ${ctx.geo.country}!`);\r\n * });\r\n * \r\n * export default createCloudflareHandler(handler);\r\n * ```\r\n */\r\n\r\nimport type { EdgeHandler, EdgeContext, EdgeGeo, CloudflareProperties } from '../index';\r\n\r\n// =============================================================================\r\n// Cloudflare Types (no external dependency required)\r\n// =============================================================================\r\n\r\ninterface IncomingRequestCfProperties {\r\n asn?: number;\r\n asOrganization?: string;\r\n city?: string;\r\n colo?: string;\r\n continent?: string;\r\n country?: string;\r\n httpProtocol?: string;\r\n latitude?: string;\r\n longitude?: string;\r\n metroCode?: string;\r\n postalCode?: string;\r\n region?: string;\r\n regionCode?: string;\r\n timezone?: string;\r\n isBot?: boolean;\r\n botScore?: number;\r\n verifiedBotCategory?: string;\r\n requestPriority?: string;\r\n tlsCipher?: string;\r\n tlsVersion?: string;\r\n}\r\n\r\ninterface CloudflareExecutionContext {\r\n waitUntil(promise: Promise<unknown>): void;\r\n passThroughOnException(): void;\r\n}\r\n\r\ntype CloudflareEnv = Record<string, unknown>;\r\n\r\ninterface CloudflareExportedHandler {\r\n fetch(\r\n request: Request,\r\n env: CloudflareEnv,\r\n ctx: CloudflareExecutionContext\r\n ): Response | Promise<Response>;\r\n}\r\n\r\n// =============================================================================\r\n// Adapter Implementation\r\n// =============================================================================\r\n\r\n/**\r\n * Create a Cloudflare Workers compatible handler from a Flight edge handler.\r\n * \r\n * This adapter:\r\n * - Extracts geo data from the `cf` object on the request\r\n * - Provides `waitUntil` and `passThroughOnException` from execution context\r\n * - Exposes environment bindings\r\n * \r\n * @param handler - Flight edge handler\r\n * @returns Cloudflare ExportedHandler object\r\n * \r\n * @example\r\n * ```typescript\r\n * import { createEdgeHandler } from '@flightdev/edge';\r\n * import { createCloudflareHandler } from '@flightdev/edge/cloudflare';\r\n * \r\n * const handler = createEdgeHandler(async (request, ctx) => {\r\n * // Geo data from Cloudflare\r\n * const { country, city, timezone } = ctx.geo;\r\n * \r\n * // Cloudflare-specific properties\r\n * const isBot = ctx.cf?.isBot;\r\n * const colo = ctx.cf?.colo;\r\n * \r\n * // Access environment bindings (KV, R2, D1, etc.)\r\n * const kv = ctx.env?.MY_KV as KVNamespace;\r\n * \r\n * // Fire and forget analytics\r\n * ctx.waitUntil(trackAnalytics({ country, path: request.url }));\r\n * \r\n * return Response.json({ country, city, isBot, colo });\r\n * });\r\n * \r\n * export default createCloudflareHandler(handler);\r\n * ```\r\n */\r\nexport function createCloudflareHandler(handler: EdgeHandler): CloudflareExportedHandler {\r\n return {\r\n async fetch(\r\n request: Request,\r\n env: CloudflareEnv,\r\n ctx: CloudflareExecutionContext\r\n ): Promise<Response> {\r\n // Extract cf properties from request\r\n const cf = (request as any).cf as IncomingRequestCfProperties | undefined;\r\n\r\n // Build geo object from cf data\r\n const geo: EdgeGeo = {\r\n country: cf?.country,\r\n city: cf?.city,\r\n region: cf?.region ?? cf?.regionCode,\r\n latitude: cf?.latitude,\r\n longitude: cf?.longitude,\r\n timezone: cf?.timezone,\r\n continent: cf?.continent,\r\n postalCode: cf?.postalCode,\r\n metroCode: cf?.metroCode,\r\n };\r\n\r\n // Build Cloudflare-specific properties\r\n const cfProperties: CloudflareProperties = {\r\n asn: cf?.asn,\r\n asOrganization: cf?.asOrganization,\r\n colo: cf?.colo,\r\n httpProtocol: cf?.httpProtocol,\r\n requestPriority: cf?.requestPriority,\r\n tlsCipher: cf?.tlsCipher,\r\n tlsVersion: cf?.tlsVersion,\r\n isBot: cf?.isBot,\r\n botScore: cf?.botScore,\r\n verifiedBotCategory: cf?.verifiedBotCategory,\r\n };\r\n\r\n // Create unified EdgeContext\r\n const edgeContext: EdgeContext = {\r\n geo,\r\n cf: cfProperties,\r\n env,\r\n waitUntil: ctx.waitUntil.bind(ctx),\r\n passThroughOnException: ctx.passThroughOnException.bind(ctx),\r\n };\r\n\r\n return handler(request, edgeContext);\r\n },\r\n };\r\n}\r\n\r\n/**\r\n * Create a Cloudflare Pages Function handler.\r\n * Similar to Worker but adapted for Pages Functions context.\r\n * \r\n * @param handler - Flight edge handler\r\n * @returns Pages Function handler\r\n */\r\nexport function createPagesHandler(handler: EdgeHandler) {\r\n return async (context: {\r\n request: Request;\r\n env: CloudflareEnv;\r\n waitUntil: (promise: Promise<unknown>) => void;\r\n passThroughToOrigin: () => void;\r\n }): Promise<Response> => {\r\n const cf = (context.request as any).cf as IncomingRequestCfProperties | undefined;\r\n\r\n const geo: EdgeGeo = {\r\n country: cf?.country,\r\n city: cf?.city,\r\n region: cf?.region,\r\n latitude: cf?.latitude,\r\n longitude: cf?.longitude,\r\n timezone: cf?.timezone,\r\n };\r\n\r\n const edgeContext: EdgeContext = {\r\n geo,\r\n cf: cf as CloudflareProperties,\r\n env: context.env,\r\n waitUntil: context.waitUntil,\r\n passThroughOnException: context.passThroughToOrigin,\r\n };\r\n\r\n return handler(context.request, edgeContext);\r\n };\r\n}\r\n\r\n// =============================================================================\r\n// Utilities\r\n// =============================================================================\r\n\r\n/**\r\n * Check if running in Cloudflare Workers environment.\r\n */\r\nexport function isCloudflareWorker(): boolean {\r\n return typeof (globalThis as any).WebSocketPair !== 'undefined' &&\r\n typeof (globalThis as any).caches !== 'undefined';\r\n}\r\n\r\n/**\r\n * Get the colo (data center) that served the request.\r\n * Useful for debugging and analytics.\r\n */\r\nexport function getColo(request: Request): string | undefined {\r\n return ((request as any).cf as IncomingRequestCfProperties)?.colo;\r\n}\r\n\r\n/**\r\n * Check if the request is from a known bot.\r\n * Uses Cloudflare's bot detection.\r\n */\r\nexport function isBot(request: Request): boolean {\r\n return ((request as any).cf as IncomingRequestCfProperties)?.isBot ?? false;\r\n}\r\n\r\n/**\r\n * Get bot score (0-100). Only available on Enterprise plans.\r\n * Lower score = more likely to be a bot.\r\n */\r\nexport function getBotScore(request: Request): number | undefined {\r\n return ((request as any).cf as IncomingRequestCfProperties)?.botScore;\r\n}\r\n"]}
@@ -0,0 +1,96 @@
1
+ import { EdgeHandler } from '../index.js';
2
+ import 'undici';
3
+
4
+ /**
5
+ * Deno Deploy Adapter
6
+ *
7
+ * Convert a Flight edge handler to work with Deno.serve().
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // server.ts
12
+ * import { createEdgeHandler } from '@flightdev/edge';
13
+ * import { createDenoHandler, serve } from '@flightdev/edge/deno';
14
+ *
15
+ * const handler = createEdgeHandler((request, ctx) => {
16
+ * return new Response(`Hello from ${ctx.geo.country}!`);
17
+ * });
18
+ *
19
+ * serve(createDenoHandler(handler), { port: 8000 });
20
+ * ```
21
+ */
22
+
23
+ interface DenoServeHandlerInfo {
24
+ remoteAddr: {
25
+ hostname: string;
26
+ port: number;
27
+ transport: 'tcp' | 'udp';
28
+ };
29
+ }
30
+ interface DenoServeOptions {
31
+ port?: number;
32
+ hostname?: string;
33
+ signal?: AbortSignal;
34
+ onListen?: (params: {
35
+ hostname: string;
36
+ port: number;
37
+ }) => void;
38
+ onError?: (error: Error) => Response | Promise<Response>;
39
+ }
40
+ /**
41
+ * Create a Deno.serve compatible handler from a Flight edge handler.
42
+ *
43
+ * Note: Deno Deploy provides limited geo data compared to Cloudflare.
44
+ * Geo extraction relies on third-party services or custom headers.
45
+ *
46
+ * @param handler - Flight edge handler
47
+ * @returns Deno-compatible fetch handler
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * import { createEdgeHandler } from '@flightdev/edge';
52
+ * import { createDenoHandler } from '@flightdev/edge/deno';
53
+ *
54
+ * const handler = createEdgeHandler((req, ctx) => {
55
+ * // Geo may be limited on Deno Deploy
56
+ * const { country } = ctx.geo;
57
+ *
58
+ * ctx.waitUntil(logRequest(req));
59
+ *
60
+ * return Response.json({ message: 'Hello from Deno!', country });
61
+ * });
62
+ *
63
+ * Deno.serve(createDenoHandler(handler));
64
+ * ```
65
+ */
66
+ declare function createDenoHandler(handler: EdgeHandler): (request: Request, info?: DenoServeHandlerInfo) => Promise<Response>;
67
+ /**
68
+ * Convenience wrapper for Deno.serve with Flight handler.
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * import { createEdgeHandler } from '@flightdev/edge';
73
+ * import { serve } from '@flightdev/edge/deno';
74
+ *
75
+ * const handler = createEdgeHandler((req, ctx) => {
76
+ * return new Response('Hello!');
77
+ * });
78
+ *
79
+ * serve(handler, { port: 8000 });
80
+ * ```
81
+ */
82
+ declare function serve(handler: EdgeHandler, options?: DenoServeOptions): void;
83
+ /**
84
+ * Check if running in Deno runtime.
85
+ */
86
+ declare function isDeno(): boolean;
87
+ /**
88
+ * Check if running in Deno Deploy (as opposed to local Deno).
89
+ */
90
+ declare function isDenoDeployment(): boolean;
91
+ /**
92
+ * Get the deployment region on Deno Deploy.
93
+ */
94
+ declare function getDenoRegion(): string | undefined;
95
+
96
+ export { createDenoHandler, getDenoRegion, isDeno, isDenoDeployment, serve };
@@ -0,0 +1,69 @@
1
+ // src/adapters/deno.ts
2
+ function createDenoHandler(handler) {
3
+ const pendingPromises = [];
4
+ return async (request, info) => {
5
+ const geo = extractGeoFromHeaders(request.headers);
6
+ const edgeContext = {
7
+ geo,
8
+ waitUntil: (promise) => {
9
+ pendingPromises.push(promise);
10
+ }
11
+ };
12
+ try {
13
+ const response = await handler(request, edgeContext);
14
+ if (pendingPromises.length > 0) {
15
+ Promise.allSettled(pendingPromises).catch(console.error);
16
+ pendingPromises.length = 0;
17
+ }
18
+ return response;
19
+ } catch (error) {
20
+ console.error("[Deno Edge] Handler error:", error);
21
+ return new Response("Internal Server Error", { status: 500 });
22
+ }
23
+ };
24
+ }
25
+ function serve(handler, options = {}) {
26
+ const denoHandler = createDenoHandler(handler);
27
+ if (typeof globalThis.Deno === "undefined") {
28
+ throw new Error("Deno runtime is not available. Are you running in Deno?");
29
+ }
30
+ const Deno = globalThis.Deno;
31
+ Deno.serve({
32
+ port: options.port ?? 8e3,
33
+ hostname: options.hostname ?? "0.0.0.0",
34
+ signal: options.signal,
35
+ onListen: options.onListen ?? (({ hostname, port }) => {
36
+ console.log(`[Flight Edge] Server running at http://${hostname}:${port}`);
37
+ }),
38
+ onError: options.onError ?? ((error) => {
39
+ console.error("[Flight Edge] Server error:", error);
40
+ return new Response("Internal Server Error", { status: 500 });
41
+ })
42
+ }, denoHandler);
43
+ }
44
+ function extractGeoFromHeaders(headers) {
45
+ return {
46
+ // Common geo headers from reverse proxies
47
+ country: headers.get("cf-ipcountry") ?? headers.get("x-country-code") ?? headers.get("x-geo-country") ?? void 0,
48
+ city: headers.get("cf-ipcity") ?? headers.get("x-city") ?? headers.get("x-geo-city") ?? void 0,
49
+ region: headers.get("cf-region") ?? headers.get("x-region") ?? headers.get("x-geo-region") ?? void 0,
50
+ latitude: headers.get("x-latitude") ?? headers.get("x-geo-latitude") ?? void 0,
51
+ longitude: headers.get("x-longitude") ?? headers.get("x-geo-longitude") ?? void 0,
52
+ timezone: headers.get("x-timezone") ?? headers.get("x-geo-timezone") ?? void 0
53
+ };
54
+ }
55
+ function isDeno() {
56
+ return typeof globalThis.Deno !== "undefined";
57
+ }
58
+ function isDenoDeployment() {
59
+ const Deno = globalThis.Deno;
60
+ return Deno?.env?.get("DENO_DEPLOYMENT_ID") !== void 0;
61
+ }
62
+ function getDenoRegion() {
63
+ const Deno = globalThis.Deno;
64
+ return Deno?.env?.get("DENO_REGION");
65
+ }
66
+
67
+ export { createDenoHandler, getDenoRegion, isDeno, isDenoDeployment, serve };
68
+ //# sourceMappingURL=deno.js.map
69
+ //# sourceMappingURL=deno.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/deno.ts"],"names":[],"mappings":"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","file":"deno.js","sourcesContent":["/**\r\n * Deno Deploy Adapter\r\n * \r\n * Convert a Flight edge handler to work with Deno.serve().\r\n * \r\n * @example\r\n * ```typescript\r\n * // server.ts\r\n * import { createEdgeHandler } from '@flightdev/edge';\r\n * import { createDenoHandler, serve } from '@flightdev/edge/deno';\r\n * \r\n * const handler = createEdgeHandler((request, ctx) => {\r\n * return new Response(`Hello from ${ctx.geo.country}!`);\r\n * });\r\n * \r\n * serve(createDenoHandler(handler), { port: 8000 });\r\n * ```\r\n */\r\n\r\nimport type { EdgeHandler, EdgeContext, EdgeGeo } from '../index';\r\n\r\n// =============================================================================\r\n// Deno Types\r\n// =============================================================================\r\n\r\ninterface DenoServeHandlerInfo {\r\n remoteAddr: {\r\n hostname: string;\r\n port: number;\r\n transport: 'tcp' | 'udp';\r\n };\r\n}\r\n\r\ninterface DenoServeOptions {\r\n port?: number;\r\n hostname?: string;\r\n signal?: AbortSignal;\r\n onListen?: (params: { hostname: string; port: number }) => void;\r\n onError?: (error: Error) => Response | Promise<Response>;\r\n}\r\n\r\n// =============================================================================\r\n// Adapter Implementation\r\n// =============================================================================\r\n\r\n/**\r\n * Create a Deno.serve compatible handler from a Flight edge handler.\r\n * \r\n * Note: Deno Deploy provides limited geo data compared to Cloudflare.\r\n * Geo extraction relies on third-party services or custom headers.\r\n * \r\n * @param handler - Flight edge handler\r\n * @returns Deno-compatible fetch handler\r\n * \r\n * @example\r\n * ```typescript\r\n * import { createEdgeHandler } from '@flightdev/edge';\r\n * import { createDenoHandler } from '@flightdev/edge/deno';\r\n * \r\n * const handler = createEdgeHandler((req, ctx) => {\r\n * // Geo may be limited on Deno Deploy\r\n * const { country } = ctx.geo;\r\n * \r\n * ctx.waitUntil(logRequest(req));\r\n * \r\n * return Response.json({ message: 'Hello from Deno!', country });\r\n * });\r\n * \r\n * Deno.serve(createDenoHandler(handler));\r\n * ```\r\n */\r\nexport function createDenoHandler(handler: EdgeHandler) {\r\n // Store pending promises for waitUntil\r\n const pendingPromises: Promise<unknown>[] = [];\r\n\r\n return async (\r\n request: Request,\r\n info?: DenoServeHandlerInfo\r\n ): Promise<Response> => {\r\n // Extract geo from headers (if provided by reverse proxy)\r\n const geo: EdgeGeo = extractGeoFromHeaders(request.headers);\r\n\r\n // Create EdgeContext\r\n const edgeContext: EdgeContext = {\r\n geo,\r\n waitUntil: (promise: Promise<unknown>) => {\r\n pendingPromises.push(promise);\r\n },\r\n };\r\n\r\n try {\r\n const response = await handler(request, edgeContext);\r\n\r\n // Wait for all pending promises after response\r\n if (pendingPromises.length > 0) {\r\n // Don't await - let them run in background\r\n Promise.allSettled(pendingPromises).catch(console.error);\r\n pendingPromises.length = 0;\r\n }\r\n\r\n return response;\r\n } catch (error) {\r\n console.error('[Deno Edge] Handler error:', error);\r\n return new Response('Internal Server Error', { status: 500 });\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * Convenience wrapper for Deno.serve with Flight handler.\r\n * \r\n * @example\r\n * ```typescript\r\n * import { createEdgeHandler } from '@flightdev/edge';\r\n * import { serve } from '@flightdev/edge/deno';\r\n * \r\n * const handler = createEdgeHandler((req, ctx) => {\r\n * return new Response('Hello!');\r\n * });\r\n * \r\n * serve(handler, { port: 8000 });\r\n * ```\r\n */\r\nexport function serve(\r\n handler: EdgeHandler,\r\n options: DenoServeOptions = {}\r\n): void {\r\n const denoHandler = createDenoHandler(handler);\r\n\r\n // Check if Deno is available\r\n if (typeof (globalThis as any).Deno === 'undefined') {\r\n throw new Error('Deno runtime is not available. Are you running in Deno?');\r\n }\r\n\r\n const Deno = (globalThis as any).Deno;\r\n\r\n Deno.serve({\r\n port: options.port ?? 8000,\r\n hostname: options.hostname ?? '0.0.0.0',\r\n signal: options.signal,\r\n onListen: options.onListen ?? (({ hostname, port }: { hostname: string; port: number }) => {\r\n console.log(`[Flight Edge] Server running at http://${hostname}:${port}`);\r\n }),\r\n onError: options.onError ?? ((error: Error) => {\r\n console.error('[Flight Edge] Server error:', error);\r\n return new Response('Internal Server Error', { status: 500 });\r\n }),\r\n }, denoHandler);\r\n}\r\n\r\n// =============================================================================\r\n// Utilities\r\n// =============================================================================\r\n\r\n/**\r\n * Extract geo from headers.\r\n * Deno Deploy doesn't provide geo by default - these come from reverse proxy.\r\n */\r\nfunction extractGeoFromHeaders(headers: Headers): EdgeGeo {\r\n return {\r\n // Common geo headers from reverse proxies\r\n country: headers.get('cf-ipcountry') ??\r\n headers.get('x-country-code') ??\r\n headers.get('x-geo-country') ??\r\n undefined,\r\n city: headers.get('cf-ipcity') ??\r\n headers.get('x-city') ??\r\n headers.get('x-geo-city') ??\r\n undefined,\r\n region: headers.get('cf-region') ??\r\n headers.get('x-region') ??\r\n headers.get('x-geo-region') ??\r\n undefined,\r\n latitude: headers.get('x-latitude') ??\r\n headers.get('x-geo-latitude') ??\r\n undefined,\r\n longitude: headers.get('x-longitude') ??\r\n headers.get('x-geo-longitude') ??\r\n undefined,\r\n timezone: headers.get('x-timezone') ??\r\n headers.get('x-geo-timezone') ??\r\n undefined,\r\n };\r\n}\r\n\r\n/**\r\n * Check if running in Deno runtime.\r\n */\r\nexport function isDeno(): boolean {\r\n return typeof (globalThis as any).Deno !== 'undefined';\r\n}\r\n\r\n/**\r\n * Check if running in Deno Deploy (as opposed to local Deno).\r\n */\r\nexport function isDenoDeployment(): boolean {\r\n const Deno = (globalThis as any).Deno;\r\n return Deno?.env?.get('DENO_DEPLOYMENT_ID') !== undefined;\r\n}\r\n\r\n/**\r\n * Get the deployment region on Deno Deploy.\r\n */\r\nexport function getDenoRegion(): string | undefined {\r\n const Deno = (globalThis as any).Deno;\r\n return Deno?.env?.get('DENO_REGION');\r\n}\r\n"]}
@@ -0,0 +1,111 @@
1
+ import { EdgeHandler } from '../index.js';
2
+ import 'undici';
3
+
4
+ /**
5
+ * Vercel Edge Adapter
6
+ *
7
+ * Convert a Flight edge handler to work with Vercel Edge Runtime.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // app/api/geo/route.ts
12
+ * import { createEdgeHandler } from '@flightdev/edge';
13
+ * import { createVercelHandler } from '@flightdev/edge/vercel';
14
+ *
15
+ * const handler = createEdgeHandler((request, ctx) => {
16
+ * return Response.json({ country: ctx.geo.country });
17
+ * });
18
+ *
19
+ * export const GET = createVercelHandler(handler);
20
+ * export const runtime = 'edge';
21
+ * ```
22
+ */
23
+
24
+ interface VercelGeo {
25
+ city?: string;
26
+ country?: string;
27
+ countryRegion?: string;
28
+ latitude?: string;
29
+ longitude?: string;
30
+ region?: string;
31
+ }
32
+ interface VercelRequestContext {
33
+ geo?: VercelGeo;
34
+ ip?: string;
35
+ waitUntil?: (promise: Promise<unknown>) => void;
36
+ }
37
+ /**
38
+ * Create a Vercel Edge Runtime compatible handler from a Flight edge handler.
39
+ *
40
+ * This adapter:
41
+ * - Extracts geo data from Vercel's geo headers
42
+ * - Provides waitUntil when available
43
+ * - Works with Next.js Edge API Routes and Middleware
44
+ *
45
+ * @param handler - Flight edge handler
46
+ * @returns Vercel-compatible fetch handler
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * // Next.js App Router
51
+ * // app/api/location/route.ts
52
+ * import { createEdgeHandler } from '@flightdev/edge';
53
+ * import { createVercelHandler } from '@flightdev/edge/vercel';
54
+ *
55
+ * const handler = createEdgeHandler((req, ctx) => {
56
+ * const { country, city, region } = ctx.geo;
57
+ *
58
+ * // Redirect based on country
59
+ * if (country === 'AR') {
60
+ * return Response.redirect('https://ar.example.com');
61
+ * }
62
+ *
63
+ * return Response.json({ country, city, region });
64
+ * });
65
+ *
66
+ * export const GET = createVercelHandler(handler);
67
+ * export const runtime = 'edge';
68
+ * ```
69
+ */
70
+ declare function createVercelHandler(handler: EdgeHandler): (request: Request, context?: VercelRequestContext) => Promise<Response>;
71
+ /**
72
+ * Create a Vercel Edge Middleware compatible handler.
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * // middleware.ts
77
+ * import { createEdgeHandler } from '@flightdev/edge';
78
+ * import { createMiddlewareHandler } from '@flightdev/edge/vercel';
79
+ *
80
+ * const handler = createEdgeHandler((req, ctx) => {
81
+ * // Block certain countries
82
+ * if (ctx.geo.country === 'XX') {
83
+ * return new Response('Access Denied', { status: 403 });
84
+ * }
85
+ *
86
+ * // Continue to next middleware/route
87
+ * return null as any; // Return NextResponse.next() in actual usage
88
+ * });
89
+ *
90
+ * export default createMiddlewareHandler(handler);
91
+ *
92
+ * export const config = {
93
+ * matcher: '/api/:path*',
94
+ * };
95
+ * ```
96
+ */
97
+ declare function createMiddlewareHandler(handler: EdgeHandler): (request: Request) => Promise<Response>;
98
+ /**
99
+ * Check if running in Vercel Edge Runtime.
100
+ */
101
+ declare function isVercelEdge(): boolean;
102
+ /**
103
+ * Get the deployment region.
104
+ */
105
+ declare function getDeploymentRegion(request: Request): string | undefined;
106
+ /**
107
+ * Get client IP address.
108
+ */
109
+ declare function getClientIP(request: Request): string | undefined;
110
+
111
+ export { createMiddlewareHandler, createVercelHandler, getClientIP, getDeploymentRegion, isVercelEdge };
@@ -0,0 +1,53 @@
1
+ // src/adapters/vercel.ts
2
+ function createVercelHandler(handler) {
3
+ return async (request, context) => {
4
+ const geo = extractGeoFromHeaders(request.headers);
5
+ if (context?.geo) {
6
+ geo.city = context.geo.city ?? geo.city;
7
+ geo.country = context.geo.country ?? geo.country;
8
+ geo.region = context.geo.countryRegion ?? context.geo.region ?? geo.region;
9
+ geo.latitude = context.geo.latitude ?? geo.latitude;
10
+ geo.longitude = context.geo.longitude ?? geo.longitude;
11
+ }
12
+ const edgeContext = {
13
+ geo,
14
+ waitUntil: context?.waitUntil ?? (() => {
15
+ })
16
+ };
17
+ return handler(request, edgeContext);
18
+ };
19
+ }
20
+ function createMiddlewareHandler(handler) {
21
+ return async (request) => {
22
+ const geo = extractGeoFromHeaders(request.headers);
23
+ const edgeContext = {
24
+ geo,
25
+ waitUntil: () => {
26
+ }
27
+ };
28
+ return handler(request, edgeContext);
29
+ };
30
+ }
31
+ function extractGeoFromHeaders(headers) {
32
+ return {
33
+ country: headers.get("x-vercel-ip-country") ?? void 0,
34
+ city: headers.get("x-vercel-ip-city") ? decodeURIComponent(headers.get("x-vercel-ip-city")) : void 0,
35
+ region: headers.get("x-vercel-ip-country-region") ?? void 0,
36
+ latitude: headers.get("x-vercel-ip-latitude") ?? void 0,
37
+ longitude: headers.get("x-vercel-ip-longitude") ?? void 0,
38
+ timezone: headers.get("x-vercel-ip-timezone") ?? void 0
39
+ };
40
+ }
41
+ function isVercelEdge() {
42
+ return typeof globalThis.EdgeRuntime !== "undefined";
43
+ }
44
+ function getDeploymentRegion(request) {
45
+ return request.headers.get("x-vercel-deployment-url")?.split(".")[0];
46
+ }
47
+ function getClientIP(request) {
48
+ return request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? request.headers.get("x-real-ip") ?? void 0;
49
+ }
50
+
51
+ export { createMiddlewareHandler, createVercelHandler, getClientIP, getDeploymentRegion, isVercelEdge };
52
+ //# sourceMappingURL=vercel.js.map
53
+ //# sourceMappingURL=vercel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/adapters/vercel.ts"],"names":[],"mappings":";AA8EO,SAAS,oBAAoB,OAAA,EAAsB;AACtD,EAAA,OAAO,OACH,SACA,OAAA,KACoB;AAEpB,IAAA,MAAM,GAAA,GAAe,qBAAA,CAAsB,OAAA,CAAQ,OAAO,CAAA;AAG1D,IAAA,IAAI,SAAS,GAAA,EAAK;AACd,MAAA,GAAA,CAAI,IAAA,GAAO,OAAA,CAAQ,GAAA,CAAI,IAAA,IAAQ,GAAA,CAAI,IAAA;AACnC,MAAA,GAAA,CAAI,OAAA,GAAU,OAAA,CAAQ,GAAA,CAAI,OAAA,IAAW,GAAA,CAAI,OAAA;AACzC,MAAA,GAAA,CAAI,SAAS,OAAA,CAAQ,GAAA,CAAI,iBAAiB,OAAA,CAAQ,GAAA,CAAI,UAAU,GAAA,CAAI,MAAA;AACpE,MAAA,GAAA,CAAI,QAAA,GAAW,OAAA,CAAQ,GAAA,CAAI,QAAA,IAAY,GAAA,CAAI,QAAA;AAC3C,MAAA,GAAA,CAAI,SAAA,GAAY,OAAA,CAAQ,GAAA,CAAI,SAAA,IAAa,GAAA,CAAI,SAAA;AAAA,IACjD;AAGA,IAAA,MAAM,WAAA,GAA2B;AAAA,MAC7B,GAAA;AAAA,MACA,SAAA,EAAW,OAAA,EAAS,SAAA,KAAc,MAAM;AAAA,MAAE,CAAA;AAAA,KAC9C;AAEA,IAAA,OAAO,OAAA,CAAQ,SAAS,WAAW,CAAA;AAAA,EACvC,CAAA;AACJ;AA4BO,SAAS,wBAAwB,OAAA,EAAsB;AAC1D,EAAA,OAAO,OAAO,OAAA,KAAwC;AAClD,IAAA,MAAM,GAAA,GAAM,qBAAA,CAAsB,OAAA,CAAQ,OAAO,CAAA;AAEjD,IAAA,MAAM,WAAA,GAA2B;AAAA,MAC7B,GAAA;AAAA,MACA,WAAW,MAAM;AAAA,MAAE;AAAA,KACvB;AAEA,IAAA,OAAO,OAAA,CAAQ,SAAS,WAAW,CAAA;AAAA,EACvC,CAAA;AACJ;AASA,SAAS,sBAAsB,OAAA,EAA2B;AACtD,EAAA,OAAO;AAAA,IACH,OAAA,EAAS,OAAA,CAAQ,GAAA,CAAI,qBAAqB,CAAA,IAAK,MAAA;AAAA,IAC/C,IAAA,EAAM,OAAA,CAAQ,GAAA,CAAI,kBAAkB,CAAA,GAC9B,mBAAmB,OAAA,CAAQ,GAAA,CAAI,kBAAkB,CAAE,CAAA,GACnD,MAAA;AAAA,IACN,MAAA,EAAQ,OAAA,CAAQ,GAAA,CAAI,4BAA4B,CAAA,IAAK,MAAA;AAAA,IACrD,QAAA,EAAU,OAAA,CAAQ,GAAA,CAAI,sBAAsB,CAAA,IAAK,MAAA;AAAA,IACjD,SAAA,EAAW,OAAA,CAAQ,GAAA,CAAI,uBAAuB,CAAA,IAAK,MAAA;AAAA,IACnD,QAAA,EAAU,OAAA,CAAQ,GAAA,CAAI,sBAAsB,CAAA,IAAK;AAAA,GACrD;AACJ;AAKO,SAAS,YAAA,GAAwB;AACpC,EAAA,OAAO,OAAQ,WAAmB,WAAA,KAAgB,WAAA;AACtD;AAKO,SAAS,oBAAoB,OAAA,EAAsC;AACtE,EAAA,OAAO,OAAA,CAAQ,QAAQ,GAAA,CAAI,yBAAyB,GAAG,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA;AACvE;AAKO,SAAS,YAAY,OAAA,EAAsC;AAC9D,EAAA,OAAO,QAAQ,OAAA,CAAQ,GAAA,CAAI,iBAAiB,CAAA,EAAG,MAAM,GAAG,CAAA,CAAE,CAAC,CAAA,EAAG,MAAK,IAC/D,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,WAAW,CAAA,IAC/B,MAAA;AACR","file":"vercel.js","sourcesContent":["/**\r\n * Vercel Edge Adapter\r\n * \r\n * Convert a Flight edge handler to work with Vercel Edge Runtime.\r\n * \r\n * @example\r\n * ```typescript\r\n * // app/api/geo/route.ts\r\n * import { createEdgeHandler } from '@flightdev/edge';\r\n * import { createVercelHandler } from '@flightdev/edge/vercel';\r\n * \r\n * const handler = createEdgeHandler((request, ctx) => {\r\n * return Response.json({ country: ctx.geo.country });\r\n * });\r\n * \r\n * export const GET = createVercelHandler(handler);\r\n * export const runtime = 'edge';\r\n * ```\r\n */\r\n\r\nimport type { EdgeHandler, EdgeContext, EdgeGeo } from '../index';\r\n\r\n// =============================================================================\r\n// Vercel Types\r\n// =============================================================================\r\n\r\ninterface VercelGeo {\r\n city?: string;\r\n country?: string;\r\n countryRegion?: string;\r\n latitude?: string;\r\n longitude?: string;\r\n region?: string;\r\n}\r\n\r\ninterface VercelRequestContext {\r\n geo?: VercelGeo;\r\n ip?: string;\r\n waitUntil?: (promise: Promise<unknown>) => void;\r\n}\r\n\r\n// =============================================================================\r\n// Adapter Implementation\r\n// =============================================================================\r\n\r\n/**\r\n * Create a Vercel Edge Runtime compatible handler from a Flight edge handler.\r\n * \r\n * This adapter:\r\n * - Extracts geo data from Vercel's geo headers\r\n * - Provides waitUntil when available\r\n * - Works with Next.js Edge API Routes and Middleware\r\n * \r\n * @param handler - Flight edge handler\r\n * @returns Vercel-compatible fetch handler\r\n * \r\n * @example\r\n * ```typescript\r\n * // Next.js App Router\r\n * // app/api/location/route.ts\r\n * import { createEdgeHandler } from '@flightdev/edge';\r\n * import { createVercelHandler } from '@flightdev/edge/vercel';\r\n * \r\n * const handler = createEdgeHandler((req, ctx) => {\r\n * const { country, city, region } = ctx.geo;\r\n * \r\n * // Redirect based on country\r\n * if (country === 'AR') {\r\n * return Response.redirect('https://ar.example.com');\r\n * }\r\n * \r\n * return Response.json({ country, city, region });\r\n * });\r\n * \r\n * export const GET = createVercelHandler(handler);\r\n * export const runtime = 'edge';\r\n * ```\r\n */\r\nexport function createVercelHandler(handler: EdgeHandler) {\r\n return async (\r\n request: Request,\r\n context?: VercelRequestContext\r\n ): Promise<Response> => {\r\n // Extract geo from headers (Vercel injects these)\r\n const geo: EdgeGeo = extractGeoFromHeaders(request.headers);\r\n\r\n // Override with context geo if available\r\n if (context?.geo) {\r\n geo.city = context.geo.city ?? geo.city;\r\n geo.country = context.geo.country ?? geo.country;\r\n geo.region = context.geo.countryRegion ?? context.geo.region ?? geo.region;\r\n geo.latitude = context.geo.latitude ?? geo.latitude;\r\n geo.longitude = context.geo.longitude ?? geo.longitude;\r\n }\r\n\r\n // Create EdgeContext\r\n const edgeContext: EdgeContext = {\r\n geo,\r\n waitUntil: context?.waitUntil ?? (() => { }),\r\n };\r\n\r\n return handler(request, edgeContext);\r\n };\r\n}\r\n\r\n/**\r\n * Create a Vercel Edge Middleware compatible handler.\r\n * \r\n * @example\r\n * ```typescript\r\n * // middleware.ts\r\n * import { createEdgeHandler } from '@flightdev/edge';\r\n * import { createMiddlewareHandler } from '@flightdev/edge/vercel';\r\n * \r\n * const handler = createEdgeHandler((req, ctx) => {\r\n * // Block certain countries\r\n * if (ctx.geo.country === 'XX') {\r\n * return new Response('Access Denied', { status: 403 });\r\n * }\r\n * \r\n * // Continue to next middleware/route\r\n * return null as any; // Return NextResponse.next() in actual usage\r\n * });\r\n * \r\n * export default createMiddlewareHandler(handler);\r\n * \r\n * export const config = {\r\n * matcher: '/api/:path*',\r\n * };\r\n * ```\r\n */\r\nexport function createMiddlewareHandler(handler: EdgeHandler) {\r\n return async (request: Request): Promise<Response> => {\r\n const geo = extractGeoFromHeaders(request.headers);\r\n\r\n const edgeContext: EdgeContext = {\r\n geo,\r\n waitUntil: () => { },\r\n };\r\n\r\n return handler(request, edgeContext);\r\n };\r\n}\r\n\r\n// =============================================================================\r\n// Utilities\r\n// =============================================================================\r\n\r\n/**\r\n * Extract geo data from Vercel's geo headers.\r\n */\r\nfunction extractGeoFromHeaders(headers: Headers): EdgeGeo {\r\n return {\r\n country: headers.get('x-vercel-ip-country') ?? undefined,\r\n city: headers.get('x-vercel-ip-city')\r\n ? decodeURIComponent(headers.get('x-vercel-ip-city')!)\r\n : undefined,\r\n region: headers.get('x-vercel-ip-country-region') ?? undefined,\r\n latitude: headers.get('x-vercel-ip-latitude') ?? undefined,\r\n longitude: headers.get('x-vercel-ip-longitude') ?? undefined,\r\n timezone: headers.get('x-vercel-ip-timezone') ?? undefined,\r\n };\r\n}\r\n\r\n/**\r\n * Check if running in Vercel Edge Runtime.\r\n */\r\nexport function isVercelEdge(): boolean {\r\n return typeof (globalThis as any).EdgeRuntime !== 'undefined';\r\n}\r\n\r\n/**\r\n * Get the deployment region.\r\n */\r\nexport function getDeploymentRegion(request: Request): string | undefined {\r\n return request.headers.get('x-vercel-deployment-url')?.split('.')[0];\r\n}\r\n\r\n/**\r\n * Get client IP address.\r\n */\r\nexport function getClientIP(request: Request): string | undefined {\r\n return request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??\r\n request.headers.get('x-real-ip') ??\r\n undefined;\r\n}\r\n"]}
@@ -0,0 +1,178 @@
1
+ export { Request, Response } from 'undici';
2
+
3
+ /**
4
+ * @flightdev/edge
5
+ *
6
+ * Edge Runtime handlers for Flight Framework.
7
+ * Deploy to any edge provider - Cloudflare Workers, Vercel Edge, Deno Deploy.
8
+ *
9
+ * Philosophy: Flight doesn't impose - you choose your edge provider.
10
+ * All adapters are optional, use only what you need.
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import { createEdgeHandler } from '@flightdev/edge';
15
+ *
16
+ * export default createEdgeHandler((request, context) => {
17
+ * const country = context.geo.country;
18
+ * return new Response(`Hello from ${country}!`);
19
+ * });
20
+ * ```
21
+ */
22
+ /**
23
+ * Geolocation data available at the edge.
24
+ * Availability varies by provider - always check for undefined.
25
+ */
26
+ interface EdgeGeo {
27
+ /** ISO 3166-1 alpha-2 country code (e.g., "US", "AR", "DE") */
28
+ country?: string;
29
+ /** City name (e.g., "Buenos Aires", "New York") */
30
+ city?: string;
31
+ /** Region/state code (e.g., "CA", "TX") */
32
+ region?: string;
33
+ /** Latitude as string */
34
+ latitude?: string;
35
+ /** Longitude as string */
36
+ longitude?: string;
37
+ /** IANA timezone (e.g., "America/Argentina/Buenos_Aires") */
38
+ timezone?: string;
39
+ /** Continent code (e.g., "NA", "SA", "EU") */
40
+ continent?: string;
41
+ /** Postal code */
42
+ postalCode?: string;
43
+ /** Metro code (US only) */
44
+ metroCode?: string;
45
+ }
46
+ /**
47
+ * Cloudflare-specific properties.
48
+ * Only available when using Cloudflare Workers adapter.
49
+ */
50
+ interface CloudflareProperties {
51
+ /** ASN of the request */
52
+ asn?: number;
53
+ /** ASN organization */
54
+ asOrganization?: string;
55
+ /** Colo (data center) that served the request */
56
+ colo?: string;
57
+ /** HTTP protocol version */
58
+ httpProtocol?: string;
59
+ /** Request priority */
60
+ requestPriority?: string;
61
+ /** TLS cipher */
62
+ tlsCipher?: string;
63
+ /** TLS version */
64
+ tlsVersion?: string;
65
+ /** Is the connection from a known bot */
66
+ isBot?: boolean;
67
+ /** Bot score (Enterprise only) */
68
+ botScore?: number;
69
+ /** Verified bot category */
70
+ verifiedBotCategory?: string;
71
+ }
72
+ /**
73
+ * Context provided to edge handlers.
74
+ * Contains geo data, provider-specific info, and lifecycle methods.
75
+ */
76
+ interface EdgeContext {
77
+ /** Geolocation data derived from request IP */
78
+ geo: EdgeGeo;
79
+ /** Cloudflare-specific properties (only with Cloudflare adapter) */
80
+ cf?: CloudflareProperties;
81
+ /** Environment variables/bindings (provider-specific) */
82
+ env?: Record<string, unknown>;
83
+ /**
84
+ * Extend the lifetime of the request handler.
85
+ * Use for fire-and-forget operations like logging or analytics.
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * context.waitUntil(logAnalytics(request));
90
+ * return new Response('OK'); // Returns immediately
91
+ * ```
92
+ */
93
+ waitUntil: (promise: Promise<unknown>) => void;
94
+ /**
95
+ * Cloudflare only: Pass through to origin on exception.
96
+ * Allows graceful degradation to your origin server.
97
+ */
98
+ passThroughOnException?: () => void;
99
+ }
100
+ /**
101
+ * Edge handler function signature.
102
+ * Receives a standard Request and EdgeContext, returns a Response.
103
+ */
104
+ type EdgeHandler = (request: Request, context: EdgeContext) => Response | Promise<Response>;
105
+ /**
106
+ * Configuration options for edge handlers.
107
+ */
108
+ interface EdgeHandlerOptions {
109
+ /**
110
+ * Enable request logging.
111
+ * @default false
112
+ */
113
+ logging?: boolean;
114
+ /**
115
+ * Custom error handler for uncaught exceptions.
116
+ */
117
+ onError?: (error: Error, request: Request) => Response | Promise<Response>;
118
+ /**
119
+ * Transform request before handler.
120
+ */
121
+ beforeRequest?: (request: Request) => Request | Promise<Request>;
122
+ /**
123
+ * Transform response after handler.
124
+ */
125
+ afterResponse?: (response: Response, request: Request) => Response | Promise<Response>;
126
+ }
127
+ /**
128
+ * Create an edge handler with unified context.
129
+ *
130
+ * This is a lightweight wrapper that normalizes the edge runtime environment
131
+ * across different providers. Use the provider-specific adapters for deployment.
132
+ *
133
+ * @param handler - Your edge handler function
134
+ * @param options - Optional configuration
135
+ * @returns Wrapped handler
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * import { createEdgeHandler } from '@flightdev/edge';
140
+ *
141
+ * const handler = createEdgeHandler(async (request, ctx) => {
142
+ * // Access geo data (available on all providers)
143
+ * const { country, city } = ctx.geo;
144
+ *
145
+ * // Fire-and-forget logging
146
+ * ctx.waitUntil(logRequest(request));
147
+ *
148
+ * return Response.json({ country, city });
149
+ * });
150
+ *
151
+ * export default handler;
152
+ * ```
153
+ */
154
+ declare function createEdgeHandler(handler: EdgeHandler, options?: EdgeHandlerOptions): EdgeHandler;
155
+ /**
156
+ * Check if code is running in an edge runtime.
157
+ * Useful for conditional logic between edge and Node.js.
158
+ */
159
+ declare function isEdgeRuntime(): boolean;
160
+ /**
161
+ * Get the current edge runtime name.
162
+ * Returns undefined if not in an edge runtime.
163
+ */
164
+ declare function getEdgeRuntime(): 'cloudflare' | 'vercel' | 'deno' | undefined;
165
+ /**
166
+ * Create an empty EdgeContext for testing or fallback.
167
+ */
168
+ declare function createEmptyContext(): EdgeContext;
169
+ /**
170
+ * Extract geo data from a request.
171
+ * Attempts to read from various header formats used by different providers.
172
+ *
173
+ * @param request - The incoming request
174
+ * @returns Geo data object
175
+ */
176
+ declare function getGeoFromRequest(request: Request): EdgeGeo;
177
+
178
+ export { type CloudflareProperties, type EdgeContext, type EdgeGeo, type EdgeHandler, type EdgeHandlerOptions, createEdgeHandler, createEmptyContext, getEdgeRuntime, getGeoFromRequest, isEdgeRuntime };
package/dist/index.js ADDED
@@ -0,0 +1,74 @@
1
+ // src/index.ts
2
+ function createEdgeHandler(handler, options = {}) {
3
+ return async (request, context) => {
4
+ try {
5
+ const processedRequest = options.beforeRequest ? await options.beforeRequest(request) : request;
6
+ if (options.logging) {
7
+ const { method, url } = processedRequest;
8
+ const { country, city } = context.geo;
9
+ console.log(`[Edge] ${method} ${url} - ${city || "Unknown"}, ${country || "Unknown"}`);
10
+ }
11
+ const response = await handler(processedRequest, context);
12
+ return options.afterResponse ? await options.afterResponse(response, processedRequest) : response;
13
+ } catch (error) {
14
+ if (options.onError) {
15
+ return options.onError(error, request);
16
+ }
17
+ console.error("[Edge] Handler error:", error);
18
+ return new Response(
19
+ JSON.stringify({ error: "Internal Server Error" }),
20
+ {
21
+ status: 500,
22
+ headers: { "Content-Type": "application/json" }
23
+ }
24
+ );
25
+ }
26
+ };
27
+ }
28
+ function isEdgeRuntime() {
29
+ if (typeof globalThis.caches !== "undefined" && typeof globalThis.WebSocketPair !== "undefined") {
30
+ return true;
31
+ }
32
+ if (typeof globalThis.EdgeRuntime !== "undefined") {
33
+ return true;
34
+ }
35
+ if (typeof globalThis.Deno !== "undefined") {
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+ function getEdgeRuntime() {
41
+ if (typeof globalThis.WebSocketPair !== "undefined") {
42
+ return "cloudflare";
43
+ }
44
+ if (typeof globalThis.EdgeRuntime !== "undefined") {
45
+ return "vercel";
46
+ }
47
+ if (typeof globalThis.Deno !== "undefined") {
48
+ return "deno";
49
+ }
50
+ return void 0;
51
+ }
52
+ function createEmptyContext() {
53
+ return {
54
+ geo: {},
55
+ waitUntil: () => {
56
+ }
57
+ };
58
+ }
59
+ function getGeoFromRequest(request) {
60
+ const headers = request.headers;
61
+ return {
62
+ // Cloudflare headers
63
+ country: headers.get("cf-ipcountry") ?? headers.get("x-vercel-ip-country") ?? void 0,
64
+ city: headers.get("cf-ipcity") ?? headers.get("x-vercel-ip-city") ?? void 0,
65
+ region: headers.get("cf-region") ?? headers.get("x-vercel-ip-country-region") ?? void 0,
66
+ latitude: headers.get("cf-iplatitude") ?? headers.get("x-vercel-ip-latitude") ?? void 0,
67
+ longitude: headers.get("cf-iplongitude") ?? headers.get("x-vercel-ip-longitude") ?? void 0,
68
+ timezone: headers.get("cf-timezone") ?? headers.get("x-vercel-ip-timezone") ?? void 0
69
+ };
70
+ }
71
+
72
+ export { createEdgeHandler, createEmptyContext, getEdgeRuntime, getGeoFromRequest, isEdgeRuntime };
73
+ //# sourceMappingURL=index.js.map
74
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\r\n * @flightdev/edge\r\n * \r\n * Edge Runtime handlers for Flight Framework.\r\n * Deploy to any edge provider - Cloudflare Workers, Vercel Edge, Deno Deploy.\r\n * \r\n * Philosophy: Flight doesn't impose - you choose your edge provider.\r\n * All adapters are optional, use only what you need.\r\n * \r\n * @example\r\n * ```typescript\r\n * import { createEdgeHandler } from '@flightdev/edge';\r\n * \r\n * export default createEdgeHandler((request, context) => {\r\n * const country = context.geo.country;\r\n * return new Response(`Hello from ${country}!`);\r\n * });\r\n * ```\r\n */\r\n\r\n// =============================================================================\r\n// Types\r\n// =============================================================================\r\n\r\n/**\r\n * Geolocation data available at the edge.\r\n * Availability varies by provider - always check for undefined.\r\n */\r\nexport interface EdgeGeo {\r\n /** ISO 3166-1 alpha-2 country code (e.g., \"US\", \"AR\", \"DE\") */\r\n country?: string;\r\n /** City name (e.g., \"Buenos Aires\", \"New York\") */\r\n city?: string;\r\n /** Region/state code (e.g., \"CA\", \"TX\") */\r\n region?: string;\r\n /** Latitude as string */\r\n latitude?: string;\r\n /** Longitude as string */\r\n longitude?: string;\r\n /** IANA timezone (e.g., \"America/Argentina/Buenos_Aires\") */\r\n timezone?: string;\r\n /** Continent code (e.g., \"NA\", \"SA\", \"EU\") */\r\n continent?: string;\r\n /** Postal code */\r\n postalCode?: string;\r\n /** Metro code (US only) */\r\n metroCode?: string;\r\n}\r\n\r\n/**\r\n * Cloudflare-specific properties.\r\n * Only available when using Cloudflare Workers adapter.\r\n */\r\nexport interface CloudflareProperties {\r\n /** ASN of the request */\r\n asn?: number;\r\n /** ASN organization */\r\n asOrganization?: string;\r\n /** Colo (data center) that served the request */\r\n colo?: string;\r\n /** HTTP protocol version */\r\n httpProtocol?: string;\r\n /** Request priority */\r\n requestPriority?: string;\r\n /** TLS cipher */\r\n tlsCipher?: string;\r\n /** TLS version */\r\n tlsVersion?: string;\r\n /** Is the connection from a known bot */\r\n isBot?: boolean;\r\n /** Bot score (Enterprise only) */\r\n botScore?: number;\r\n /** Verified bot category */\r\n verifiedBotCategory?: string;\r\n}\r\n\r\n/**\r\n * Context provided to edge handlers.\r\n * Contains geo data, provider-specific info, and lifecycle methods.\r\n */\r\nexport interface EdgeContext {\r\n /** Geolocation data derived from request IP */\r\n geo: EdgeGeo;\r\n\r\n /** Cloudflare-specific properties (only with Cloudflare adapter) */\r\n cf?: CloudflareProperties;\r\n\r\n /** Environment variables/bindings (provider-specific) */\r\n env?: Record<string, unknown>;\r\n\r\n /**\r\n * Extend the lifetime of the request handler.\r\n * Use for fire-and-forget operations like logging or analytics.\r\n * \r\n * @example\r\n * ```typescript\r\n * context.waitUntil(logAnalytics(request));\r\n * return new Response('OK'); // Returns immediately\r\n * ```\r\n */\r\n waitUntil: (promise: Promise<unknown>) => void;\r\n\r\n /**\r\n * Cloudflare only: Pass through to origin on exception.\r\n * Allows graceful degradation to your origin server.\r\n */\r\n passThroughOnException?: () => void;\r\n}\r\n\r\n/**\r\n * Edge handler function signature.\r\n * Receives a standard Request and EdgeContext, returns a Response.\r\n */\r\nexport type EdgeHandler = (\r\n request: Request,\r\n context: EdgeContext\r\n) => Response | Promise<Response>;\r\n\r\n/**\r\n * Configuration options for edge handlers.\r\n */\r\nexport interface EdgeHandlerOptions {\r\n /**\r\n * Enable request logging.\r\n * @default false\r\n */\r\n logging?: boolean;\r\n\r\n /**\r\n * Custom error handler for uncaught exceptions.\r\n */\r\n onError?: (error: Error, request: Request) => Response | Promise<Response>;\r\n\r\n /**\r\n * Transform request before handler.\r\n */\r\n beforeRequest?: (request: Request) => Request | Promise<Request>;\r\n\r\n /**\r\n * Transform response after handler.\r\n */\r\n afterResponse?: (response: Response, request: Request) => Response | Promise<Response>;\r\n}\r\n\r\n// =============================================================================\r\n// Core Implementation\r\n// =============================================================================\r\n\r\n/**\r\n * Create an edge handler with unified context.\r\n * \r\n * This is a lightweight wrapper that normalizes the edge runtime environment\r\n * across different providers. Use the provider-specific adapters for deployment.\r\n * \r\n * @param handler - Your edge handler function\r\n * @param options - Optional configuration\r\n * @returns Wrapped handler\r\n * \r\n * @example\r\n * ```typescript\r\n * import { createEdgeHandler } from '@flightdev/edge';\r\n * \r\n * const handler = createEdgeHandler(async (request, ctx) => {\r\n * // Access geo data (available on all providers)\r\n * const { country, city } = ctx.geo;\r\n * \r\n * // Fire-and-forget logging\r\n * ctx.waitUntil(logRequest(request));\r\n * \r\n * return Response.json({ country, city });\r\n * });\r\n * \r\n * export default handler;\r\n * ```\r\n */\r\nexport function createEdgeHandler(\r\n handler: EdgeHandler,\r\n options: EdgeHandlerOptions = {}\r\n): EdgeHandler {\r\n return async (request: Request, context: EdgeContext): Promise<Response> => {\r\n try {\r\n // Before request hook\r\n const processedRequest = options.beforeRequest\r\n ? await options.beforeRequest(request)\r\n : request;\r\n\r\n // Logging\r\n if (options.logging) {\r\n const { method, url } = processedRequest;\r\n const { country, city } = context.geo;\r\n console.log(`[Edge] ${method} ${url} - ${city || 'Unknown'}, ${country || 'Unknown'}`);\r\n }\r\n\r\n // Execute handler\r\n const response = await handler(processedRequest, context);\r\n\r\n // After response hook\r\n return options.afterResponse\r\n ? await options.afterResponse(response, processedRequest)\r\n : response;\r\n\r\n } catch (error) {\r\n // Custom error handler\r\n if (options.onError) {\r\n return options.onError(error as Error, request);\r\n }\r\n\r\n // Default error response\r\n console.error('[Edge] Handler error:', error);\r\n return new Response(\r\n JSON.stringify({ error: 'Internal Server Error' }),\r\n {\r\n status: 500,\r\n headers: { 'Content-Type': 'application/json' },\r\n }\r\n );\r\n }\r\n };\r\n}\r\n\r\n// =============================================================================\r\n// Utilities\r\n// =============================================================================\r\n\r\n/**\r\n * Check if code is running in an edge runtime.\r\n * Useful for conditional logic between edge and Node.js.\r\n */\r\nexport function isEdgeRuntime(): boolean {\r\n // Cloudflare Workers\r\n if (typeof (globalThis as any).caches !== 'undefined' &&\r\n typeof (globalThis as any).WebSocketPair !== 'undefined') {\r\n return true;\r\n }\r\n\r\n // Vercel Edge\r\n if (typeof (globalThis as any).EdgeRuntime !== 'undefined') {\r\n return true;\r\n }\r\n\r\n // Deno\r\n if (typeof (globalThis as any).Deno !== 'undefined') {\r\n return true;\r\n }\r\n\r\n return false;\r\n}\r\n\r\n/**\r\n * Get the current edge runtime name.\r\n * Returns undefined if not in an edge runtime.\r\n */\r\nexport function getEdgeRuntime(): 'cloudflare' | 'vercel' | 'deno' | undefined {\r\n if (typeof (globalThis as any).WebSocketPair !== 'undefined') {\r\n return 'cloudflare';\r\n }\r\n if (typeof (globalThis as any).EdgeRuntime !== 'undefined') {\r\n return 'vercel';\r\n }\r\n if (typeof (globalThis as any).Deno !== 'undefined') {\r\n return 'deno';\r\n }\r\n return undefined;\r\n}\r\n\r\n/**\r\n * Create an empty EdgeContext for testing or fallback.\r\n */\r\nexport function createEmptyContext(): EdgeContext {\r\n return {\r\n geo: {},\r\n waitUntil: () => { },\r\n };\r\n}\r\n\r\n/**\r\n * Extract geo data from a request.\r\n * Attempts to read from various header formats used by different providers.\r\n * \r\n * @param request - The incoming request\r\n * @returns Geo data object\r\n */\r\nexport function getGeoFromRequest(request: Request): EdgeGeo {\r\n const headers = request.headers;\r\n\r\n return {\r\n // Cloudflare headers\r\n country: headers.get('cf-ipcountry') ??\r\n headers.get('x-vercel-ip-country') ??\r\n undefined,\r\n city: headers.get('cf-ipcity') ??\r\n headers.get('x-vercel-ip-city') ??\r\n undefined,\r\n region: headers.get('cf-region') ??\r\n headers.get('x-vercel-ip-country-region') ??\r\n undefined,\r\n latitude: headers.get('cf-iplatitude') ??\r\n headers.get('x-vercel-ip-latitude') ??\r\n undefined,\r\n longitude: headers.get('cf-iplongitude') ??\r\n headers.get('x-vercel-ip-longitude') ??\r\n undefined,\r\n timezone: headers.get('cf-timezone') ??\r\n headers.get('x-vercel-ip-timezone') ??\r\n undefined,\r\n };\r\n}\r\n\r\n// =============================================================================\r\n// Re-exports for convenience\r\n// =============================================================================\r\n\r\nexport type { Request, Response } from './types';\r\n"]}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@flightdev/edge",
3
+ "version": "0.2.0",
4
+ "description": "Edge Runtime handlers for Flight Framework - deploy to any edge provider",
5
+ "keywords": [
6
+ "flight",
7
+ "edge",
8
+ "cloudflare",
9
+ "vercel",
10
+ "deno",
11
+ "workers",
12
+ "serverless"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "Flight Contributors",
16
+ "type": "module",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./cloudflare": {
23
+ "types": "./dist/adapters/cloudflare.d.ts",
24
+ "import": "./dist/adapters/cloudflare.js"
25
+ },
26
+ "./vercel": {
27
+ "types": "./dist/adapters/vercel.d.ts",
28
+ "import": "./dist/adapters/vercel.js"
29
+ },
30
+ "./deno": {
31
+ "types": "./dist/adapters/deno.d.ts",
32
+ "import": "./dist/adapters/deno.js"
33
+ }
34
+ },
35
+ "main": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "files": [
38
+ "dist"
39
+ ],
40
+ "devDependencies": {
41
+ "@types/node": "^22.0.0",
42
+ "rimraf": "^6.0.0",
43
+ "tsup": "^8.0.0",
44
+ "typescript": "^5.7.0",
45
+ "vitest": "^2.0.0"
46
+ },
47
+ "peerDependencies": {
48
+ "@cloudflare/workers-types": "^4.0.0"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "@cloudflare/workers-types": {
52
+ "optional": true
53
+ }
54
+ },
55
+ "scripts": {
56
+ "build": "tsup",
57
+ "dev": "tsup --watch",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest",
60
+ "lint": "eslint src/",
61
+ "clean": "rimraf dist",
62
+ "typecheck": "tsc --noEmit"
63
+ }
64
+ }