@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 +21 -0
- package/README.md +145 -0
- package/dist/adapters/cloudflare.d.ts +101 -0
- package/dist/adapters/cloudflare.js +76 -0
- package/dist/adapters/cloudflare.js.map +1 -0
- package/dist/adapters/deno.d.ts +96 -0
- package/dist/adapters/deno.js +69 -0
- package/dist/adapters/deno.js.map +1 -0
- package/dist/adapters/vercel.d.ts +111 -0
- package/dist/adapters/vercel.js +53 -0
- package/dist/adapters/vercel.js.map +1 -0
- package/dist/index.d.ts +178 -0
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
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"]}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|