@marvalt/digivalt-core 0.1.0 → 0.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.
- package/bin/init.cjs +42 -0
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +4 -0
- package/dist/index.esm.js.map +1 -1
- package/package.json +7 -2
- package/template/.dev.vars.example +11 -0
- package/template/functions/api/fetch-with-access.ts +41 -0
- package/template/functions/api/gravity-forms-submit.ts +291 -0
- package/template/functions/api/mautic-submit.ts +19 -0
- package/template/functions/api/webhook.js +78 -0
- package/template/wrangler.toml +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marvalt/digivalt-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Core glue logic and shared context for DigiVAlt frontend applications",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -13,11 +13,16 @@
|
|
|
13
13
|
"require": "./dist/index.cjs"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"digivalt-init": "./bin/init.cjs"
|
|
18
|
+
},
|
|
16
19
|
"files": [
|
|
17
20
|
"dist",
|
|
18
21
|
"README.md",
|
|
19
22
|
"LICENSE",
|
|
20
|
-
"CHANGELOG.md"
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"bin",
|
|
25
|
+
"template"
|
|
21
26
|
],
|
|
22
27
|
"type": "module",
|
|
23
28
|
"sideEffects": false,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# DigiVAlt Cloudflare Worker Secrets
|
|
2
|
+
# Copy this to .dev.vars and populate for local testing via Wrangler
|
|
3
|
+
|
|
4
|
+
# Add the specific API keys needed by your functions/ proxy layers
|
|
5
|
+
MAUTIC_USERNAME=
|
|
6
|
+
MAUTIC_PASSWORD=
|
|
7
|
+
SUITECRM_CLIENT_ID=
|
|
8
|
+
SUITECRM_CLIENT_SECRET=
|
|
9
|
+
GRAVITY_FORMS_CONSUMER_KEY=
|
|
10
|
+
GRAVITY_FORMS_CONSUMER_SECRET=
|
|
11
|
+
WP_APPLICATION_PASSWORD=
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export async function onRequest(context: any) {
|
|
2
|
+
const { request, env } = context;
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
const url = new URL(request.url);
|
|
6
|
+
const target = url.searchParams.get('target');
|
|
7
|
+
if (!target) return new Response('Missing target', { status: 400 });
|
|
8
|
+
|
|
9
|
+
// Server-side secrets (do NOT expose to browser). Prefer non-VITE names.
|
|
10
|
+
const clientId = env.CF_ACCESS_CLIENT_ID || env.VITE_CF_ACCESS_CLIENT_ID;
|
|
11
|
+
const clientSecret = env.CF_ACCESS_CLIENT_SECRET || env.VITE_CF_ACCESS_CLIENT_SECRET;
|
|
12
|
+
|
|
13
|
+
if (!clientId || !clientSecret) {
|
|
14
|
+
return new Response('Access credentials not configured', { status: 500 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const method = request.method;
|
|
18
|
+
const incomingHeaders = new Headers(request.headers);
|
|
19
|
+
// Strip hop-by-hop and sensitive headers
|
|
20
|
+
['host', 'origin', 'referer', 'cookie', 'authorization'].forEach((h) => incomingHeaders.delete(h));
|
|
21
|
+
|
|
22
|
+
// Inject Cloudflare Access service token headers
|
|
23
|
+
incomingHeaders.set('CF-Access-Client-Id', clientId);
|
|
24
|
+
incomingHeaders.set('CF-Access-Client-Secret', clientSecret);
|
|
25
|
+
|
|
26
|
+
const init: RequestInit = { method, headers: incomingHeaders };
|
|
27
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
28
|
+
init.body = await request.arrayBuffer();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const resp = await fetch(target, init);
|
|
32
|
+
const respHeaders = new Headers(resp.headers);
|
|
33
|
+
['set-cookie', 'cf-ray', 'server'].forEach((h) => respHeaders.delete(h));
|
|
34
|
+
|
|
35
|
+
return new Response(resp.body, { status: resp.status, headers: respHeaders });
|
|
36
|
+
} catch (e: any) {
|
|
37
|
+
return new Response(`fetch-with-access error: ${e?.message || 'unknown'}`, { status: 500 });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Pages Function for Gravity Forms API proxy
|
|
3
|
+
*
|
|
4
|
+
* This function handles WordPress Basic Auth server-side to keep credentials secure.
|
|
5
|
+
* It provides the same security layers as the Mautic proxy.
|
|
6
|
+
*
|
|
7
|
+
* Required environment variables:
|
|
8
|
+
* - VITE_WORDPRESS_API_URL or WORDPRESS_API_URL: WordPress instance URL
|
|
9
|
+
* - VITE_WP_API_USERNAME or WP_API_USERNAME: WordPress username
|
|
10
|
+
* - VITE_WP_APP_PASSWORD or WP_APP_PASSWORD: WordPress application password
|
|
11
|
+
* - VITE_CF_ACCESS_CLIENT_ID or CF_ACCESS_CLIENT_ID: (Optional) Cloudflare Access client ID
|
|
12
|
+
* - VITE_CF_ACCESS_CLIENT_SECRET or CF_ACCESS_CLIENT_SECRET: (Optional) Cloudflare Access client secret
|
|
13
|
+
* - ALLOWED_ORIGINS or VITE_ALLOWED_ORIGINS: Comma-separated list of allowed origins
|
|
14
|
+
* - TURNSTILE_SECRET_KEY or VITE_TURNSTILE_SECRET_KEY: (Optional) Cloudflare Turnstile secret key
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Verifies a Cloudflare Turnstile token server-side.
|
|
19
|
+
*/
|
|
20
|
+
async function verifyTurnstile(token: string, secretKey: string): Promise<boolean> {
|
|
21
|
+
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: {
|
|
24
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
25
|
+
},
|
|
26
|
+
body: `secret=${encodeURIComponent(secretKey)}&response=${encodeURIComponent(token)}`,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
return data.success;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function onRequest(context: any) {
|
|
34
|
+
const { request, env } = context;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const url = new URL(request.url);
|
|
38
|
+
const endpoint = url.searchParams.get('endpoint');
|
|
39
|
+
|
|
40
|
+
// ============================================
|
|
41
|
+
// CORS Headers - Always include for OPTIONS preflight
|
|
42
|
+
// ============================================
|
|
43
|
+
const origin = request.headers.get('Origin');
|
|
44
|
+
const referer = request.headers.get('Referer');
|
|
45
|
+
|
|
46
|
+
const allowedOriginsStr = env.ALLOWED_ORIGINS || env.VITE_ALLOWED_ORIGINS || '';
|
|
47
|
+
const allowedOrigins = allowedOriginsStr
|
|
48
|
+
.split(',')
|
|
49
|
+
.map((o: string) => o.trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
|
|
52
|
+
if (allowedOrigins.length === 0) {
|
|
53
|
+
allowedOrigins.push('http://localhost:8080', 'http://localhost:5173');
|
|
54
|
+
console.log('⚠️ No ALLOWED_ORIGINS configured, defaulting to localhost');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check if origin is in allowed list OR is localhost (development)
|
|
58
|
+
const isLocalhost = origin && (origin.includes('localhost') || origin.includes('127.0.0.1'));
|
|
59
|
+
const isAllowedOrigin = allowedOrigins.some((allowed: string) =>
|
|
60
|
+
origin?.startsWith(allowed) || referer?.startsWith(allowed)
|
|
61
|
+
) || isLocalhost; // Allow localhost in development
|
|
62
|
+
|
|
63
|
+
// Build CORS headers - always include these
|
|
64
|
+
const corsHeaders: Record<string, string> = {
|
|
65
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
66
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, CF-Access-Client-Id, CF-Access-Client-Secret',
|
|
67
|
+
'Access-Control-Max-Age': '86400',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Determine the origin to allow
|
|
71
|
+
let allowedOrigin: string | null = null;
|
|
72
|
+
if (origin && isAllowedOrigin) {
|
|
73
|
+
allowedOrigin = origin;
|
|
74
|
+
if (isLocalhost) {
|
|
75
|
+
console.log('🔓 Allowing localhost origin in development:', origin);
|
|
76
|
+
}
|
|
77
|
+
} else if (referer && isAllowedOrigin) {
|
|
78
|
+
// Fallback to referer if origin is not present
|
|
79
|
+
try {
|
|
80
|
+
allowedOrigin = new URL(referer).origin;
|
|
81
|
+
} catch (e) {
|
|
82
|
+
// Invalid referer URL, ignore
|
|
83
|
+
}
|
|
84
|
+
} else if (!origin && !referer) {
|
|
85
|
+
// Allow if no origin/referer (same-origin or direct request)
|
|
86
|
+
allowedOrigin = '*';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Set Access-Control-Allow-Origin header
|
|
90
|
+
if (allowedOrigin) {
|
|
91
|
+
corsHeaders['Access-Control-Allow-Origin'] = allowedOrigin;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle OPTIONS preflight request
|
|
95
|
+
if (request.method === 'OPTIONS') {
|
|
96
|
+
return new Response(null, {
|
|
97
|
+
status: 204,
|
|
98
|
+
headers: corsHeaders,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!endpoint) {
|
|
103
|
+
return new Response('Missing endpoint parameter', {
|
|
104
|
+
status: 400,
|
|
105
|
+
headers: corsHeaders,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Validate origin for non-OPTIONS requests (use the same check that includes localhost)
|
|
110
|
+
if ((origin || referer) && !isAllowedOrigin) {
|
|
111
|
+
console.warn('🚫 Blocked request from unauthorized origin:', origin || referer);
|
|
112
|
+
return new Response(JSON.stringify({
|
|
113
|
+
error: 'Forbidden origin',
|
|
114
|
+
message: 'This endpoint can only be accessed from authorized domains'
|
|
115
|
+
}), {
|
|
116
|
+
status: 403,
|
|
117
|
+
headers: {
|
|
118
|
+
'Content-Type': 'application/json',
|
|
119
|
+
...corsHeaders,
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================
|
|
125
|
+
// SECURITY LAYER 2: Endpoint Whitelisting
|
|
126
|
+
// ============================================
|
|
127
|
+
const allowedPatterns = [
|
|
128
|
+
/^\/forms\/\d+\/submit$/, // Form submissions only
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const isAllowedEndpoint = allowedPatterns.some(pattern => pattern.test(endpoint));
|
|
132
|
+
|
|
133
|
+
if (!isAllowedEndpoint) {
|
|
134
|
+
console.warn('🚫 Blocked unauthorized endpoint:', endpoint);
|
|
135
|
+
return new Response(JSON.stringify({
|
|
136
|
+
error: 'Forbidden endpoint',
|
|
137
|
+
message: 'Only form submission endpoints are allowed'
|
|
138
|
+
}), {
|
|
139
|
+
status: 403,
|
|
140
|
+
headers: { 'Content-Type': 'application/json' }
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ============================================
|
|
145
|
+
// SECURITY LAYER 3: Turnstile Verification
|
|
146
|
+
// ============================================
|
|
147
|
+
const turnstileSecretKey = env.TURNSTILE_SECRET_KEY || env.VITE_TURNSTILE_SECRET_KEY;
|
|
148
|
+
const turnstileToken = request.headers.get('cf-turnstile-response');
|
|
149
|
+
|
|
150
|
+
// Only verify Turnstile if:
|
|
151
|
+
// 1. Secret key is configured AND
|
|
152
|
+
// 2. Request is POST AND
|
|
153
|
+
// 3. Client sent a token (indicating Turnstile is enabled on the form)
|
|
154
|
+
if (turnstileSecretKey && request.method === 'POST' && turnstileToken) {
|
|
155
|
+
const isValid = await verifyTurnstile(turnstileToken, turnstileSecretKey);
|
|
156
|
+
|
|
157
|
+
if (!isValid) {
|
|
158
|
+
console.warn('🚫 Invalid Turnstile token');
|
|
159
|
+
return new Response(JSON.stringify({
|
|
160
|
+
error: 'Verification failed',
|
|
161
|
+
message: 'Bot verification failed'
|
|
162
|
+
}), {
|
|
163
|
+
status: 403,
|
|
164
|
+
headers: { 'Content-Type': 'application/json' }
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
console.log('✅ Turnstile verification passed');
|
|
169
|
+
} else if (turnstileSecretKey && request.method === 'POST' && !turnstileToken) {
|
|
170
|
+
// Warn but allow (Turnstile is optional for Gravity Forms)
|
|
171
|
+
console.warn('⚠️ Turnstile secret key configured but no token provided - allowing request');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================
|
|
175
|
+
// SECURITY LAYER 4: WordPress Basic Auth
|
|
176
|
+
// ============================================
|
|
177
|
+
const wpUrl = env.VITE_WORDPRESS_API_URL || env.WORDPRESS_API_URL;
|
|
178
|
+
const username = env.VITE_WP_API_USERNAME || env.WP_API_USERNAME;
|
|
179
|
+
const password = env.VITE_WP_APP_PASSWORD || env.WP_APP_PASSWORD;
|
|
180
|
+
|
|
181
|
+
if (!wpUrl || !username || !password) {
|
|
182
|
+
console.error('❌ WordPress credentials not configured', {
|
|
183
|
+
wpUrl: !!wpUrl,
|
|
184
|
+
username: !!username,
|
|
185
|
+
password: !!password
|
|
186
|
+
});
|
|
187
|
+
return new Response('WordPress credentials not configured', { status: 500 });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Create Basic Auth header
|
|
191
|
+
const authHeader = 'Basic ' + btoa(`${username}:${password}`);
|
|
192
|
+
|
|
193
|
+
const headers: Record<string, string> = {
|
|
194
|
+
'Authorization': authHeader,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Preserve Content-Type from original request
|
|
198
|
+
const contentType = request.headers.get('Content-Type');
|
|
199
|
+
if (contentType) {
|
|
200
|
+
headers['Content-Type'] = contentType;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ============================================
|
|
204
|
+
// SECURITY LAYER 5: CF Access (Optional)
|
|
205
|
+
// ============================================
|
|
206
|
+
const cfAccessClientId = env.CF_ACCESS_CLIENT_ID || env.VITE_CF_ACCESS_CLIENT_ID;
|
|
207
|
+
const cfAccessClientSecret = env.CF_ACCESS_CLIENT_SECRET || env.VITE_CF_ACCESS_CLIENT_SECRET;
|
|
208
|
+
|
|
209
|
+
if (cfAccessClientId && cfAccessClientSecret) {
|
|
210
|
+
headers['CF-Access-Client-Id'] = cfAccessClientId;
|
|
211
|
+
headers['CF-Access-Client-Secret'] = cfAccessClientSecret;
|
|
212
|
+
console.log('🔐 Added CF Access headers to WordPress request');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Build target URL
|
|
216
|
+
const targetUrl = `${wpUrl}/wp-json/gf-api/v1${endpoint}`;
|
|
217
|
+
|
|
218
|
+
const init: RequestInit = {
|
|
219
|
+
method: request.method,
|
|
220
|
+
headers,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
224
|
+
init.body = await request.text();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(`📤 Proxying ${request.method} request to WordPress:`, {
|
|
228
|
+
endpoint,
|
|
229
|
+
targetUrl,
|
|
230
|
+
bodyPreview: init.body ? init.body.substring(0, 200) : 'no body'
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const response = await fetch(targetUrl, init);
|
|
234
|
+
|
|
235
|
+
const responseBody = await response.text();
|
|
236
|
+
console.log(`📥 WordPress response: ${response.status} ${response.statusText}`, {
|
|
237
|
+
bodyPreview: responseBody.substring(0, 500)
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return new Response(responseBody, {
|
|
241
|
+
status: response.status,
|
|
242
|
+
statusText: response.statusText,
|
|
243
|
+
headers: {
|
|
244
|
+
'Content-Type': response.headers.get('Content-Type') || 'application/json',
|
|
245
|
+
...corsHeaders,
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
} catch (error: any) {
|
|
250
|
+
console.error('❌ Gravity Forms proxy error:', error);
|
|
251
|
+
// Build CORS headers for error response (reuse logic from above)
|
|
252
|
+
const errorCorsHeaders: Record<string, string> = {
|
|
253
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
254
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, CF-Access-Client-Id, CF-Access-Client-Secret',
|
|
255
|
+
'Access-Control-Max-Age': '86400',
|
|
256
|
+
};
|
|
257
|
+
const errorOrigin = request.headers.get('Origin');
|
|
258
|
+
const errorReferer = request.headers.get('Referer');
|
|
259
|
+
const errorAllowedOriginsStr = env.ALLOWED_ORIGINS || env.VITE_ALLOWED_ORIGINS || '';
|
|
260
|
+
const errorAllowedOrigins = errorAllowedOriginsStr
|
|
261
|
+
.split(',')
|
|
262
|
+
.map((o: string) => o.trim())
|
|
263
|
+
.filter(Boolean);
|
|
264
|
+
if (errorAllowedOrigins.length === 0) {
|
|
265
|
+
errorAllowedOrigins.push('http://localhost:8080', 'http://localhost:5173');
|
|
266
|
+
}
|
|
267
|
+
const isErrorAllowedOrigin = errorAllowedOrigins.some((allowed: string) =>
|
|
268
|
+
errorOrigin?.startsWith(allowed) || errorReferer?.startsWith(allowed)
|
|
269
|
+
);
|
|
270
|
+
if (errorOrigin && isErrorAllowedOrigin) {
|
|
271
|
+
errorCorsHeaders['Access-Control-Allow-Origin'] = errorOrigin;
|
|
272
|
+
} else if (errorReferer && isErrorAllowedOrigin) {
|
|
273
|
+
const errorRefererOrigin = new URL(errorReferer).origin;
|
|
274
|
+
errorCorsHeaders['Access-Control-Allow-Origin'] = errorRefererOrigin;
|
|
275
|
+
} else if (!errorOrigin && !errorReferer) {
|
|
276
|
+
errorCorsHeaders['Access-Control-Allow-Origin'] = '*';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return new Response(JSON.stringify({
|
|
280
|
+
success: false,
|
|
281
|
+
error: error?.message || 'Unknown error',
|
|
282
|
+
}), {
|
|
283
|
+
status: 500,
|
|
284
|
+
headers: {
|
|
285
|
+
'Content-Type': 'application/json',
|
|
286
|
+
...errorCorsHeaders,
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Pages Function for Mautic API proxy
|
|
3
|
+
* Auto-generated by @marvalt/madapter
|
|
4
|
+
*
|
|
5
|
+
* This function handles OAuth2 authentication server-side to keep credentials secure.
|
|
6
|
+
* It is automatically installed when you install @marvalt/madapter.
|
|
7
|
+
*
|
|
8
|
+
* Required environment variables (in .env.local):
|
|
9
|
+
* - VITE_MAUTIC_URL: Your Mautic instance URL
|
|
10
|
+
* - VITE_MAUTIC_API_PUBLIC_KEY: OAuth2 client ID
|
|
11
|
+
* - VITE_MAUTIC_API_SECRET_KEY: OAuth2 client secret
|
|
12
|
+
* - VITE_CF_ACCESS_CLIENT_ID: (Optional) Cloudflare Access client ID
|
|
13
|
+
* - VITE_CF_ACCESS_CLIENT_SECRET: (Optional) Cloudflare Access client secret
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { handleMauticProxy } from '@marvalt/madapter/server';
|
|
17
|
+
|
|
18
|
+
export const onRequest = handleMauticProxy;
|
|
19
|
+
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export async function onRequestPost(context) {
|
|
2
|
+
const { request, env } = context;
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
console.log('Webhook received:', {
|
|
6
|
+
method: request.method,
|
|
7
|
+
url: request.url,
|
|
8
|
+
headers: Object.fromEntries(request.headers.entries())
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Verify authentication using VITE_FRONTEND_SECRET
|
|
12
|
+
const authHeader = request.headers.get('Authorization');
|
|
13
|
+
const expectedSecret = env.VITE_FRONTEND_SECRET;
|
|
14
|
+
|
|
15
|
+
if (!authHeader || !expectedSecret) {
|
|
16
|
+
console.log('Missing authentication:', {
|
|
17
|
+
hasAuthHeader: !!authHeader,
|
|
18
|
+
hasExpectedSecret: !!expectedSecret
|
|
19
|
+
});
|
|
20
|
+
return new Response('Unauthorized', { status: 401 });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check if the Authorization header matches the expected secret
|
|
24
|
+
if (authHeader !== `Bearer ${expectedSecret}`) {
|
|
25
|
+
console.log('Invalid authentication token');
|
|
26
|
+
return new Response('Unauthorized', { status: 401 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Parse the webhook payload
|
|
30
|
+
const payload = await request.json();
|
|
31
|
+
console.log('Webhook payload:', payload);
|
|
32
|
+
|
|
33
|
+
// Validate required fields
|
|
34
|
+
if (!payload.frontend_id || !payload.post_id || !payload.post_type) {
|
|
35
|
+
console.log('Missing required fields:', payload);
|
|
36
|
+
return new Response('Bad Request - Missing required fields', { status: 400 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Log the webhook details
|
|
40
|
+
console.log('Webhook processed successfully:', {
|
|
41
|
+
frontend_id: payload.frontend_id,
|
|
42
|
+
post_id: payload.post_id,
|
|
43
|
+
post_type: payload.post_type,
|
|
44
|
+
action: payload.action || 'update',
|
|
45
|
+
timestamp: new Date().toISOString()
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// For now, we'll just log the webhook
|
|
49
|
+
// In the future, this could trigger a rebuild or cache invalidation
|
|
50
|
+
// For Cloudflare Pages, you might want to trigger a deployment webhook
|
|
51
|
+
|
|
52
|
+
return new Response(JSON.stringify({
|
|
53
|
+
success: true,
|
|
54
|
+
message: 'Webhook processed successfully',
|
|
55
|
+
timestamp: new Date().toISOString()
|
|
56
|
+
}), {
|
|
57
|
+
status: 200,
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json'
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Webhook error:', error);
|
|
65
|
+
return new Response(JSON.stringify({
|
|
66
|
+
success: false,
|
|
67
|
+
error: error.message
|
|
68
|
+
}), {
|
|
69
|
+
status: 500,
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json'
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|