@growthbook/proxy-eval 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "growthbook-edge-eval-cloudflare",
3
+ "version": "0.1.0",
4
+ "description": "GrowthBook edge evaluation for Cloudflare Workers",
5
+ "main": "src/index.ts",
6
+ "scripts": {
7
+ "dev": "wrangler dev",
8
+ "deploy": "wrangler deploy",
9
+ "build": "wrangler build"
10
+ },
11
+ "dependencies": {
12
+ "@growthbook/proxy-eval": "^1.1.1"
13
+ },
14
+ "devDependencies": {
15
+ "@cloudflare/workers-types": "^4.20250926.0",
16
+ "typescript": "^5.8.2",
17
+ "wrangler": "^4.40.1"
18
+ }
19
+ }
@@ -0,0 +1,119 @@
1
+ import { evaluateFeatures } from "@growthbook/proxy-eval";
2
+ import { StickyBucketService } from "@growthbook/growthbook";
3
+
4
+ interface Env {
5
+ ENVIRONMENT: string;
6
+
7
+ KV_GB_PAYLOAD: KVNamespace;
8
+ // NOTE: Be sure to connect a GrowthBook SDK Webhook to your KV store.
9
+ // Use the webhook type "Cloudflare KV" in the SDK webhook settings.
10
+ }
11
+
12
+ interface PostBody {
13
+ payload: any;
14
+ attributes: Record<string, any>;
15
+ forcedVariations?: Record<string, number>;
16
+ forcedFeatures?: Map<string, any>;
17
+ url?: string;
18
+ // NOTE: For advanced experimentation, you may want to connect a KV or cookie Sticky Bucket service
19
+ stickyBucketService?:
20
+ | (StickyBucketService & {
21
+ // For cookie-based service, should be a no-op:
22
+ connect: () => Promise<void>;
23
+ // For write-buffer flushes (ex: GB Proxy's implementation of RedisStickyBucketService):
24
+ onEvaluate?: () => Promise<void>;
25
+ })
26
+ | null;
27
+ ctx?: { verboseDebugging?: boolean };
28
+ }
29
+
30
+ const KV_KEY = "gb_payload";
31
+ const CACHE_TTL = 60 * 1000; // 1 min
32
+
33
+ // Cache payload from KV
34
+ let cachedPayload: any = null;
35
+ let lastFetch = 0;
36
+
37
+
38
+ export default {
39
+ fetch: async function (
40
+ request: Request,
41
+ env: Env,
42
+ ctx: ExecutionContext,
43
+ ): Promise<Response> {
44
+ // Handle CORS preflight requests
45
+ if (request.method === 'OPTIONS') {
46
+ return new Response(null, {
47
+ status: 204,
48
+ headers: getCORSHeaders(),
49
+ });
50
+ }
51
+
52
+ // Only allow POST requests
53
+ if (request.method !== 'POST') {
54
+ return new Response(null, {
55
+ status: 405,
56
+ headers: {
57
+ 'Allow': 'POST',
58
+ ...getCORSHeaders(),
59
+ }
60
+ });
61
+ }
62
+
63
+ try {
64
+ const body = await request.json<PostBody>().catch(() => null);
65
+ if (!body || typeof body !== "object") {
66
+ return handleInvalidRequest();
67
+ }
68
+
69
+ if (!cachedPayload || Date.now() - lastFetch > CACHE_TTL) {
70
+ cachedPayload = await env.KV_GB_PAYLOAD.get(KV_KEY, 'json');
71
+ lastFetch = Date.now();
72
+ }
73
+
74
+ const { attributes = {}, forcedVariations = {}, forcedFeatures = [], url = "" } = body;
75
+ const forcedFeaturesMap = new Map(forcedFeatures);
76
+
77
+ const evalResponse = await evaluateFeatures({
78
+ payload: cachedPayload,
79
+ attributes,
80
+ forcedVariations,
81
+ forcedFeatures: forcedFeaturesMap,
82
+ url,
83
+ // stickyBucketService,
84
+ });
85
+
86
+ // Return success response
87
+ return new Response(
88
+ JSON.stringify(evalResponse),
89
+ {
90
+ status: 200,
91
+ headers: {
92
+ 'Content-Type': 'application/json',
93
+ ...getCORSHeaders(),
94
+ }
95
+ }
96
+ );
97
+
98
+ } catch (error) {
99
+ console.error(error);
100
+ return handleInvalidRequest();
101
+ }
102
+ }
103
+ }
104
+
105
+ function getCORSHeaders(): Record<string, string> {
106
+ return {
107
+ 'Access-Control-Allow-Origin': '*', // Configure this appropriately for production
108
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
109
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
110
+ 'Access-Control-Max-Age': '86400',
111
+ };
112
+ }
113
+
114
+ function handleInvalidRequest(): Response {
115
+ return new Response(null, {
116
+ status: 500,
117
+ headers: getCORSHeaders(),
118
+ });
119
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022"],
5
+ "module": "ES2022",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true,
8
+ "esModuleInterop": true,
9
+ "allowJs": true,
10
+ "strict": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "noEmit": true,
16
+ "types": ["@cloudflare/workers-types"]
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }
@@ -0,0 +1,14 @@
1
+ name = "growthbook-edge-evaluator"
2
+ main = "src/index.ts"
3
+ compatibility_date = "2025-09-25"
4
+
5
+ # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables)
6
+ # Note: Use secrets to store sensitive data.
7
+ # Docs: https://developers.cloudflare.com/workers/platform/environment-variables
8
+
9
+ kv_namespaces = [
10
+ { binding = "KV_GB_PAYLOAD", id = "qwerty123" }
11
+ ]
12
+
13
+ [vars]
14
+ NODE_ENV="production"
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@growthbook/proxy-eval",
3
3
  "description": "Remote evaluation service for proxies, edge workers, or backend APIs",
4
- "version": "1.1.0",
4
+ "version": "1.1.1",
5
5
  "main": "dist/index.js",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -18,7 +18,7 @@
18
18
  "dev": "tsc --watch"
19
19
  },
20
20
  "dependencies": {
21
- "@growthbook/growthbook": "^1.6.1"
21
+ "@growthbook/growthbook": "^1.6.3"
22
22
  },
23
23
  "devDependencies": {
24
24
  "rimraf": "^6.0.1",