@marvalt/digivalt-core 0.1.7 → 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.
Files changed (44) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +32 -4
  3. package/bin/init.cjs +42 -7
  4. package/dist/config.cjs +520 -0
  5. package/dist/config.cjs.map +1 -0
  6. package/dist/config.d.ts +307 -0
  7. package/dist/config.esm.js +502 -0
  8. package/dist/config.esm.js.map +1 -0
  9. package/dist/generators.cjs +2481 -0
  10. package/dist/generators.cjs.map +1 -0
  11. package/dist/generators.d.ts +19 -0
  12. package/dist/generators.esm.js +2475 -0
  13. package/dist/generators.esm.js.map +1 -0
  14. package/dist/index.cjs +18 -7955
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.ts +18 -1196
  17. package/dist/index.esm.js +13 -7929
  18. package/dist/index.esm.js.map +1 -1
  19. package/dist/runtime/env.d.ts +4 -0
  20. package/dist/runtime/lazy.d.ts +1 -0
  21. package/dist/services/cf-wp-webhook.d.ts +2 -0
  22. package/dist/services/gravityForms.d.ts +3 -0
  23. package/dist/services/mautic.d.ts +5 -1
  24. package/dist/services/suitecrm.d.ts +2 -0
  25. package/dist/services.cjs +1339 -0
  26. package/dist/services.cjs.map +1 -0
  27. package/dist/services.d.ts +432 -0
  28. package/dist/services.esm.js +1322 -0
  29. package/dist/services.esm.js.map +1 -0
  30. package/dist/static/index.d.ts +4 -0
  31. package/dist/static.cjs +997 -0
  32. package/dist/static.cjs.map +1 -0
  33. package/dist/static.d.ts +410 -0
  34. package/dist/static.esm.js +962 -0
  35. package/dist/static.esm.js.map +1 -0
  36. package/dist/types.cjs +3 -0
  37. package/dist/types.cjs.map +1 -0
  38. package/dist/types.d.ts +134 -0
  39. package/dist/types.esm.js +2 -0
  40. package/dist/types.esm.js.map +1 -0
  41. package/package.json +30 -2
  42. package/template/DIGIVALT_SETUP.md +62 -0
  43. package/template/scripts/deploy-secrets.js +55 -23
  44. package/template/scripts/generate.ts +3 -17
@@ -0,0 +1,1322 @@
1
+ import { GravityFormsClient } from '@marvalt/wadapter';
2
+ import { MauticClient } from '@marvalt/madapter';
3
+
4
+ const getImportMetaEnv = () => {
5
+ try {
6
+ return typeof import.meta !== 'undefined' ? import.meta.env : undefined;
7
+ }
8
+ catch {
9
+ return undefined;
10
+ }
11
+ };
12
+ const getEnv = (key) => {
13
+ const viteEnv = getImportMetaEnv();
14
+ if (viteEnv && key in viteEnv) {
15
+ const value = viteEnv[key];
16
+ return value == null ? undefined : String(value);
17
+ }
18
+ if (typeof process !== 'undefined' && process.env && key in process.env) {
19
+ const value = process.env[key];
20
+ return value == null ? undefined : String(value);
21
+ }
22
+ return undefined;
23
+ };
24
+ const isDevEnvironment = () => {
25
+ const viteEnv = getImportMetaEnv();
26
+ if (viteEnv && 'DEV' in viteEnv) {
27
+ return Boolean(viteEnv.DEV);
28
+ }
29
+ return typeof process !== 'undefined' && process.env?.NODE_ENV === 'development';
30
+ };
31
+ const isLocalDevelopmentEnabled = () => {
32
+ return getEnv('VITE_LOCAL_DEVELOPMENT') === 'true';
33
+ };
34
+
35
+ const createLazyProxy = (factory) => new Proxy({}, {
36
+ get(_target, prop, receiver) {
37
+ const instance = factory();
38
+ const value = Reflect.get(instance, prop, receiver);
39
+ return typeof value === 'function' ? value.bind(instance) : value;
40
+ },
41
+ set(_target, prop, value, receiver) {
42
+ const instance = factory();
43
+ return Reflect.set(instance, prop, value, receiver);
44
+ },
45
+ has(_target, prop) {
46
+ const instance = factory();
47
+ return prop in instance;
48
+ },
49
+ ownKeys() {
50
+ const instance = factory();
51
+ return Reflect.ownKeys(instance);
52
+ },
53
+ getOwnPropertyDescriptor(_target, prop) {
54
+ const instance = factory();
55
+ return Object.getOwnPropertyDescriptor(instance, prop);
56
+ },
57
+ });
58
+
59
+ /**
60
+ * Gravity Forms Integration Service
61
+ * Uses @marvalt/wadapter package for secure form submissions
62
+ */
63
+ // Convention-based: Uses /api/gravity-forms-submit by default.
64
+ let gravityFormsClientInstance = null;
65
+ const createGravityFormsClient = () => {
66
+ const config = {
67
+ apiUrl: getEnv('VITE_WORDPRESS_API_URL'),
68
+ authMode: 'direct',
69
+ timeout: 30000,
70
+ retries: 3,
71
+ };
72
+ return new GravityFormsClient(config);
73
+ };
74
+ const getGravityFormsClient = () => {
75
+ if (!gravityFormsClientInstance) {
76
+ gravityFormsClientInstance = createGravityFormsClient();
77
+ }
78
+ return gravityFormsClientInstance;
79
+ };
80
+ const resetGravityFormsClient = () => {
81
+ gravityFormsClientInstance = null;
82
+ };
83
+ const gravityFormsClient = createLazyProxy(getGravityFormsClient);
84
+ // Legacy service wrapper for backwards compatibility
85
+ const gravityFormsService = {
86
+ /**
87
+ * Submit a Gravity Form (legacy interface)
88
+ * @param formId - Form ID as string or number
89
+ * @param entry - Entry data with field IDs as keys
90
+ * @param files - Optional files (not yet supported)
91
+ * @param useBasicApi - Ignored (always uses proxy)
92
+ */
93
+ async submitForm(formId, entry, files, useBasicApi) {
94
+ console.log('🔍 gravityFormsService.submitForm (legacy wrapper) called:', {
95
+ formId,
96
+ entry,
97
+ hasFiles: !!files,
98
+ useBasicApi,
99
+ });
100
+ if (files && Object.keys(files).length > 0) {
101
+ console.warn('⚠️ File uploads not yet supported in new Gravity Forms client');
102
+ }
103
+ // Convert to new API format
104
+ const formIdNumber = typeof formId === 'string' ? parseInt(formId, 10) : formId;
105
+ return getGravityFormsClient().submitForm(formIdNumber, {
106
+ form_id: formIdNumber,
107
+ field_values: entry,
108
+ });
109
+ },
110
+ // Pass through other methods
111
+ getForm: (id) => getGravityFormsClient().getForm(id),
112
+ getForms: () => getGravityFormsClient().getForms(),
113
+ getFormConfig: (id) => getGravityFormsClient().getFormConfig(id),
114
+ getHealth: () => getGravityFormsClient().getHealth(),
115
+ };
116
+
117
+ /**
118
+ * Mautic Integration Service (via @marvalt/madapter)
119
+ */
120
+ // @ts-ignore - internal workspace type resolution
121
+ // Convention-based: Uses /api/mautic-submit Pages Function for server-side OAuth2
122
+ // The Pages Function handles authentication securely without exposing credentials to the browser.
123
+ let mauticServiceInstance = null;
124
+ const createMauticService = () => {
125
+ const useWranglerProxy = isDevEnvironment() && isLocalDevelopmentEnabled();
126
+ return new MauticClient({
127
+ apiUrl: getEnv('VITE_MAUTIC_URL'),
128
+ proxyEndpoint: useWranglerProxy
129
+ ? 'http://127.0.0.1:8788/api/mautic-submit'
130
+ : '/api/mautic-submit',
131
+ });
132
+ };
133
+ const getMauticService = () => {
134
+ if (!mauticServiceInstance) {
135
+ mauticServiceInstance = createMauticService();
136
+ }
137
+ return mauticServiceInstance;
138
+ };
139
+ const resetMauticService = () => {
140
+ mauticServiceInstance = null;
141
+ };
142
+ const mauticService = createLazyProxy(getMauticService);
143
+
144
+ // Detect if we're running in a local development environment
145
+ const isLocalDevelopment = () => {
146
+ // Check if we're in a browser environment
147
+ if (typeof window !== 'undefined') {
148
+ // Check for local development indicators
149
+ const isLocalhost = window.location.hostname === 'localhost' ||
150
+ window.location.hostname === '127.0.0.1' ||
151
+ window.location.hostname.includes('192.168.') ||
152
+ window.location.hostname.includes('10.0.');
153
+ const isDevPort = window.location.port === '3000' ||
154
+ window.location.port === '5173' ||
155
+ window.location.port === '8080';
156
+ const hasLocalEnv = Boolean(getEnv('DEV')) || getEnv('VITE_LOCAL_DEVELOPMENT') === 'true';
157
+ return Boolean(isLocalhost && (isDevPort || hasLocalEnv));
158
+ }
159
+ else {
160
+ // Node.js environment - check environment variables
161
+ return getEnv('NODE_ENV') === 'development' ||
162
+ getEnv('VITE_LOCAL_DEVELOPMENT') === 'true';
163
+ }
164
+ };
165
+ // Get the appropriate environment configuration
166
+ const getEnvironmentConfig = () => {
167
+ const isLocal = isLocalDevelopment();
168
+ // Default to cloudflare_proxy (via Cloudflare Pages Functions)
169
+ // Use 'direct' only for local development with whitelisted IPs
170
+ const authModeRaw = getEnv('VITE_AUTH_MODE');
171
+ const authMode = authModeRaw || 'cloudflare_proxy';
172
+ // Reduced debug logging - only in development and only once
173
+ if (isDevEnvironment() && !authModeRaw) {
174
+ console.log('ℹ️ VITE_AUTH_MODE not set, defaulting to cloudflare_proxy (Cloudflare Pages Functions)');
175
+ }
176
+ // Validate auth mode value
177
+ if (authMode !== 'direct' && authMode !== 'cloudflare_proxy') {
178
+ console.error('❌ Invalid VITE_AUTH_MODE value. Must be "direct" or "cloudflare_proxy".');
179
+ return {
180
+ mode: 'error',
181
+ authMode: 'error',
182
+ dataSource: 'static',
183
+ baseUrl: '',
184
+ headers: {},
185
+ description: `Invalid configuration - VITE_AUTH_MODE="${authModeRaw}"`,
186
+ suiteCRM: {
187
+ url: undefined,
188
+ proxyUrl: undefined,
189
+ appId: undefined,
190
+ workerSecret: undefined,
191
+ oauth2: {
192
+ clientId: undefined,
193
+ clientSecret: undefined,
194
+ tokenUrl: undefined,
195
+ username: undefined,
196
+ password: undefined,
197
+ },
198
+ }
199
+ };
200
+ }
201
+ // Cloudflare proxy mode (uses Cloudflare Pages Functions by default)
202
+ // Worker URL is optional - Pages Functions handle routing automatically
203
+ if (authMode === 'cloudflare_proxy') {
204
+ const workerUrl = getEnv('VITE_CLOUDFLARE_WORKER_URL');
205
+ // Cloudflare Pages Functions work without explicit worker URL
206
+ // The worker URL is only needed for separate Cloudflare Workers
207
+ // Pages Functions automatically handle /api/* routes
208
+ const baseUrl = workerUrl || '/api';
209
+ return {
210
+ mode: isLocal ? 'local' : 'remote',
211
+ authMode: 'cloudflare_proxy',
212
+ dataSource: 'static',
213
+ baseUrl,
214
+ headers: {
215
+ 'Content-Type': 'application/json'
216
+ },
217
+ description: workerUrl
218
+ ? (isLocal ? 'Local development with Cloudflare Worker proxy' : 'Remote development with Cloudflare Worker proxy')
219
+ : (isLocal ? 'Local development with Cloudflare Pages Functions' : 'Production with Cloudflare Pages Functions'),
220
+ mautic: {
221
+ proxyUrl: getEnv('VITE_MAUTIC_PROXY_URL')
222
+ },
223
+ suiteCRM: {
224
+ url: getEnv('VITE_SUITECRM_URL'),
225
+ proxyUrl: getEnv('VITE_SUITECRM_PROXY_URL'),
226
+ appId: undefined,
227
+ workerSecret: undefined,
228
+ oauth2: {
229
+ clientId: getEnv('VITE_SUITECRM_CLIENT_ID'),
230
+ clientSecret: getEnv('VITE_SUITECRM_CLIENT_SECRET'),
231
+ tokenUrl: getEnv('VITE_SUITECRM_TOKEN_URL'),
232
+ username: getEnv('VITE_SUITECRM_USERNAME'),
233
+ password: getEnv('VITE_SUITECRM_PASSWORD'),
234
+ },
235
+ }
236
+ };
237
+ }
238
+ // Direct WordPress mode (legacy - for local development only)
239
+ if (authMode === 'direct') {
240
+ const wpApiUrl = getEnv('VITE_WORDPRESS_API_URL');
241
+ if (!wpApiUrl) {
242
+ console.warn('⚠️ Direct mode requires: VITE_WORDPRESS_API_URL');
243
+ return {
244
+ mode: 'error',
245
+ authMode: 'error',
246
+ dataSource: 'static',
247
+ baseUrl: '',
248
+ headers: {},
249
+ description: 'Invalid configuration - missing WordPress API URL',
250
+ suiteCRM: {
251
+ url: undefined,
252
+ proxyUrl: undefined,
253
+ appId: undefined,
254
+ workerSecret: undefined,
255
+ oauth2: {
256
+ clientId: undefined,
257
+ clientSecret: undefined,
258
+ tokenUrl: undefined,
259
+ username: undefined,
260
+ password: undefined,
261
+ },
262
+ }
263
+ };
264
+ }
265
+ if (isDevEnvironment()) {
266
+ console.log('🔍 Using direct WordPress mode (legacy - for local development only)');
267
+ }
268
+ const localDev = getEnv('VITE_LOCAL_DEVELOPMENT') === 'true';
269
+ return {
270
+ mode: localDev ? 'local' : 'remote',
271
+ authMode: 'direct',
272
+ dataSource: 'hybrid',
273
+ baseUrl: wpApiUrl,
274
+ headers: { 'Content-Type': 'application/json' },
275
+ description: localDev ? 'Direct (local dev) without Access' : 'Direct mode',
276
+ mautic: { url: getEnv('VITE_MAUTIC_URL') },
277
+ suiteCRM: {
278
+ url: getEnv('VITE_SUITECRM_URL'),
279
+ proxyUrl: getEnv('VITE_SUITECRM_PROXY_URL'),
280
+ appId: undefined,
281
+ workerSecret: undefined,
282
+ oauth2: {
283
+ clientId: getEnv('VITE_SUITECRM_CLIENT_ID'),
284
+ clientSecret: getEnv('VITE_SUITECRM_CLIENT_SECRET'),
285
+ tokenUrl: getEnv('VITE_SUITECRM_TOKEN_URL'),
286
+ username: getEnv('VITE_SUITECRM_USERNAME'),
287
+ password: getEnv('VITE_SUITECRM_PASSWORD'),
288
+ },
289
+ }
290
+ };
291
+ }
292
+ // This should never be reached, but just in case
293
+ return {
294
+ mode: 'error',
295
+ authMode: 'error',
296
+ dataSource: 'static',
297
+ baseUrl: '',
298
+ headers: {},
299
+ description: 'Invalid configuration - unknown error',
300
+ suiteCRM: {
301
+ url: undefined,
302
+ proxyUrl: undefined,
303
+ appId: undefined,
304
+ workerSecret: undefined,
305
+ oauth2: {
306
+ clientId: undefined,
307
+ clientSecret: undefined,
308
+ tokenUrl: undefined,
309
+ username: undefined,
310
+ password: undefined,
311
+ },
312
+ }
313
+ };
314
+ };
315
+ // Environment status and diagnostics
316
+ const getEnvironmentStatus = () => {
317
+ const config = getEnvironmentConfig();
318
+ const isLocal = isLocalDevelopment();
319
+ const authMode = getEnv('VITE_AUTH_MODE');
320
+ // Check if required credentials are available for each mode
321
+ const hasCloudflareCredentials = !!getEnv('VITE_CLOUDFLARE_WORKER_URL');
322
+ const hasWordPressApiUrl = !!getEnv('VITE_WORDPRESS_API_URL');
323
+ return {
324
+ currentConfig: config,
325
+ diagnostics: {
326
+ isLocalDevelopment: isLocal,
327
+ authMode: authMode,
328
+ hasCloudflareCredentials,
329
+ hasWordpressApiUrl: hasWordPressApiUrl,
330
+ cloudflareWorkerUrl: !!getEnv('VITE_CLOUDFLARE_WORKER_URL'),
331
+ cfAccessId: !!getEnv('VITE_CF_ACCESS_CLIENT_ID'),
332
+ cfAccessSecret: !!getEnv('VITE_CF_ACCESS_CLIENT_SECRET'),
333
+ mauticProxyUrl: !!getEnv('VITE_MAUTIC_PROXY_URL')
334
+ },
335
+ recommendations: {
336
+ // Only show recommendation if authMode is explicitly set to an invalid value
337
+ // Default is cloudflare_proxy, so no recommendation needed if not set
338
+ authMode: null, // Defaults to cloudflare_proxy, no warning needed
339
+ cloudflare: null // Pages Functions work without explicit worker URL
340
+ }
341
+ };
342
+ };
343
+ // Export current environment config as default
344
+ getEnvironmentConfig();
345
+ getEnvironmentStatus();
346
+
347
+ /**
348
+ * Integration Configuration
349
+ * Centralized configuration for all third-party integrations
350
+ */
351
+ /**
352
+ * Get integration configuration with environment variable fallbacks
353
+ */
354
+ function getIntegrationConfig() {
355
+ isDevEnvironment();
356
+ return {
357
+ wordpress: {
358
+ enabled: true,
359
+ apiUrl: getEnv('VITE_WORDPRESS_API_URL') || '',
360
+ authMode: getEnv('VITE_AUTH_MODE') || 'direct',
361
+ },
362
+ supabase: {
363
+ enabled: getEnv('VITE_AUTH_MODE') === 'supabase_proxy',
364
+ url: getEnv('VITE_SUPABASE_URL') || '',
365
+ anonKey: getEnv('VITE_SUPABASE_ANON_KEY') || '',
366
+ serviceRoleKey: getEnv('VITE_SUPABASE_SERVICE_ROLE_KEY'),
367
+ },
368
+ gravityForms: {
369
+ enabled: !!(getEnv('VITE_CLOUDFLARE_WORKER_URL') || getEnv('VITE_WORDPRESS_API_URL')),
370
+ url: getEnv('VITE_WORDPRESS_API_URL'),
371
+ proxyUrl: getEnv('VITE_CLOUDFLARE_WORKER_URL'),
372
+ consumerKey: getEnv('VITE_GF_CONSUMER_KEY'),
373
+ consumerSecret: getEnv('VITE_GF_CONSUMER_SECRET'),
374
+ forms: {
375
+ 'contact': {
376
+ name: 'Contact Form',
377
+ type: 'contact',
378
+ successMessage: 'Thank you for your message. We\'ll get back to you soon!',
379
+ },
380
+ 'newsletter': {
381
+ name: 'Newsletter Signup',
382
+ type: 'newsletter',
383
+ successMessage: 'Thank you for subscribing to our newsletter!',
384
+ },
385
+ 'support': {
386
+ name: 'Support Request',
387
+ type: 'support',
388
+ successMessage: 'Your support request has been submitted successfully.',
389
+ },
390
+ },
391
+ },
392
+ mautic: {
393
+ enabled: !!(getEnv('VITE_MAUTIC_PROXY_URL') || getEnv('VITE_MAUTIC_URL')),
394
+ url: getEnv('VITE_MAUTIC_URL') || '',
395
+ proxyUrl: getEnv('VITE_MAUTIC_PROXY_URL'),
396
+ forms: {
397
+ '1': {
398
+ name: 'Newsletter Signup',
399
+ type: 'newsletter',
400
+ successMessage: 'Thank you for subscribing to our newsletter!',
401
+ postAction: 'message',
402
+ postActionProperty: 'Thank you for your submission!'
403
+ },
404
+ '2': {
405
+ name: 'Contact Form',
406
+ type: 'contact',
407
+ successMessage: 'Thank you for contacting us! We\'ll get back to you soon.',
408
+ postAction: 'message',
409
+ postActionProperty: 'Thank you for your message!'
410
+ },
411
+ '3': {
412
+ name: 'Lead Capture',
413
+ type: 'lead',
414
+ successMessage: 'Thank you for your interest! We\'ll be in touch.',
415
+ postAction: 'message',
416
+ postActionProperty: 'Thank you for your submission!'
417
+ }
418
+ }
419
+ },
420
+ suiteCRM: {
421
+ enabled: !!(getEnv('VITE_SUITECRM_URL') && getEnv('VITE_SUITECRM_CLIENT_ID') && getEnv('VITE_SUITECRM_CLIENT_SECRET')),
422
+ url: getEnv('VITE_SUITECRM_URL'),
423
+ proxyUrl: getEnv('VITE_SUITECRM_PROXY_URL'),
424
+ appId: undefined,
425
+ workerSecret: undefined,
426
+ oauth2: {
427
+ clientId: getEnv('VITE_SUITECRM_CLIENT_ID'),
428
+ clientSecret: getEnv('VITE_SUITECRM_CLIENT_SECRET'),
429
+ username: getEnv('VITE_SUITECRM_USERNAME'),
430
+ password: getEnv('VITE_SUITECRM_PASSWORD'),
431
+ },
432
+ },
433
+ chatwoot: {
434
+ enabled: !!(getEnv('VITE_CHATWOOT_WEBSITE_TOKEN') && getEnv('VITE_CHATWOOT_BASE_URL')),
435
+ websiteToken: getEnv('VITE_CHATWOOT_WEBSITE_TOKEN') || '',
436
+ baseUrl: getEnv('VITE_CHATWOOT_BASE_URL') || '',
437
+ locale: 'en',
438
+ type: 'standard',
439
+ launcherTitle: 'Chat',
440
+ hideMessageBubble: false,
441
+ position: 'right',
442
+ showPopoutButton: false,
443
+ },
444
+ analytics: {
445
+ googleAnalytics: { enabled: false },
446
+ googleTagManager: { enabled: false },
447
+ facebookPixel: { enabled: false },
448
+ linkedinInsight: { enabled: false },
449
+ },
450
+ seo: {
451
+ defaultTitle: '',
452
+ defaultDescription: '',
453
+ defaultImage: '',
454
+ siteName: '',
455
+ siteUrl: '',
456
+ },
457
+ performance: {
458
+ enableLazyLoading: true,
459
+ enableImageOptimization: true,
460
+ enableCodeSplitting: true,
461
+ enableServiceWorker: false,
462
+ enableCaching: true,
463
+ }
464
+ };
465
+ }
466
+ function getSuiteCRMConfig() {
467
+ return getIntegrationConfig().suiteCRM;
468
+ }
469
+
470
+ // @ts-nocheck
471
+ /**
472
+ * Password utility functions for handling special characters
473
+ */
474
+ /**
475
+ * Safely encode a password for API requests
476
+ * Handles special characters like $, @, #, %, etc.
477
+ */
478
+ function encodePassword(password) {
479
+ if (!password)
480
+ return '';
481
+ // For JSON requests, we don't need to encode the password
482
+ // The JSON.stringify() will handle special characters properly
483
+ return password;
484
+ }
485
+ /**
486
+ * Validate password for common issues
487
+ */
488
+ function validatePassword(password) {
489
+ const issues = [];
490
+ if (!password) {
491
+ issues.push('Password is empty');
492
+ }
493
+ if (password.length < 1) {
494
+ issues.push('Password is too short');
495
+ }
496
+ // Check for potentially problematic characters
497
+ const problematicChars = ['$', '@', '#', '%', '&', '*', '(', ')', '[', ']', '{', '}', '|', '\\', '`', '~'];
498
+ const foundChars = problematicChars.filter(char => password.includes(char));
499
+ if (foundChars.length > 0) {
500
+ issues.push(`Password contains potentially problematic characters: ${foundChars.join(', ')}`);
501
+ }
502
+ return {
503
+ isValid: issues.length === 0,
504
+ issues
505
+ };
506
+ }
507
+ /**
508
+ * Get password encoding recommendations
509
+ */
510
+ function getPasswordRecommendations(password) {
511
+ const recommendations = [];
512
+ if (password.includes('$')) {
513
+ recommendations.push('The $ character in passwords can cause issues in some contexts. Consider using a different character or ensure proper encoding.');
514
+ }
515
+ if (password.includes('@')) {
516
+ recommendations.push('The @ character might be interpreted as email syntax in some contexts.');
517
+ }
518
+ if (password.includes('#')) {
519
+ recommendations.push('The # character might be interpreted as a URL fragment in some contexts.');
520
+ }
521
+ if (password.includes('%')) {
522
+ recommendations.push('The % character might be interpreted as URL encoding in some contexts.');
523
+ }
524
+ if (password.length < 8) {
525
+ recommendations.push('Consider using a longer password for better security.');
526
+ }
527
+ return recommendations;
528
+ }
529
+
530
+ class SuiteCRMService {
531
+ baseUrl;
532
+ proxyUrl;
533
+ appId;
534
+ workerSecret;
535
+ useProxy;
536
+ isConfigured;
537
+ oauth2Config;
538
+ accessToken;
539
+ tokenExpiresAt;
540
+ sessionId;
541
+ csrfToken;
542
+ constructor() {
543
+ // Get environment configuration
544
+ const envConfig = getEnvironmentConfig();
545
+ const suitecrm = getSuiteCRMConfig();
546
+ if (import.meta.env.DEV) {
547
+ console.log('🔍 SuiteCRM Service Auth Mode Debug:', {
548
+ authMode: envConfig.authMode,
549
+ hasSuiteCRMConfig: !!suitecrm,
550
+ hasEnvSuiteCRM: !!envConfig.suiteCRM,
551
+ suitecrmConfig: suitecrm ? {
552
+ hasUrl: !!suitecrm.url,
553
+ hasProxyUrl: !!suitecrm.proxyUrl,
554
+ hasAppId: !!suitecrm.appId,
555
+ hasWorkerSecret: !!suitecrm.workerSecret,
556
+ hasOAuth2: !!(suitecrm.oauth2?.clientId && suitecrm.oauth2?.clientSecret)
557
+ } : null,
558
+ envSuiteCRM: envConfig.suiteCRM ? {
559
+ hasUrl: !!envConfig.suiteCRM.url,
560
+ hasProxyUrl: !!envConfig.suiteCRM.proxyUrl,
561
+ hasAppId: !!envConfig.suiteCRM.appId,
562
+ hasWorkerSecret: !!envConfig.suiteCRM.workerSecret,
563
+ hasOAuth2: !!(envConfig.suiteCRM.oauth2?.clientId && envConfig.suiteCRM.oauth2?.clientSecret)
564
+ } : null
565
+ });
566
+ }
567
+ if (envConfig.authMode === 'cloudflare_proxy' && (suitecrm || envConfig.suiteCRM)) {
568
+ // Cloudflare Worker proxy (recommended - uses Secrets Store)
569
+ const config = suitecrm || envConfig.suiteCRM;
570
+ this.proxyUrl = config?.proxyUrl;
571
+ this.appId = undefined;
572
+ this.workerSecret = undefined;
573
+ this.useProxy = true;
574
+ this.isConfigured = !!this.proxyUrl;
575
+ if (import.meta.env.DEV) {
576
+ console.log('🔍 SuiteCRM Service: Using Cloudflare Worker proxy (Secrets Store)');
577
+ console.log('🔍 Constructor Debug:', {
578
+ proxyUrl: this.proxyUrl,
579
+ appId: this.appId,
580
+ hasWorkerSecret: !!this.workerSecret,
581
+ workerSecretLength: this.workerSecret?.length,
582
+ isConfigured: this.isConfigured
583
+ });
584
+ }
585
+ }
586
+ else if (envConfig.authMode === 'direct' && (suitecrm || envConfig.suiteCRM)) {
587
+ // Direct SuiteCRM API access with OAuth2
588
+ const config = suitecrm || envConfig.suiteCRM;
589
+ this.baseUrl = config?.url || '';
590
+ this.useProxy = false;
591
+ this.oauth2Config = config?.oauth2;
592
+ this.isConfigured = !!(this.baseUrl && config?.oauth2?.clientId && config?.oauth2?.clientSecret && config?.oauth2?.username && config?.oauth2?.password);
593
+ if (import.meta.env.DEV) {
594
+ console.log('🔍 SuiteCRM Service: Using direct SuiteCRM API access with OAuth2');
595
+ console.log('🔍 OAuth2 Config:', {
596
+ hasClientId: !!this.oauth2Config?.clientId,
597
+ hasClientSecret: !!this.oauth2Config?.clientSecret,
598
+ hasUsername: !!this.oauth2Config?.username,
599
+ hasPassword: !!this.oauth2Config?.password
600
+ });
601
+ }
602
+ }
603
+ else {
604
+ // Fallback or error state
605
+ this.useProxy = false;
606
+ this.isConfigured = false;
607
+ if (import.meta.env.DEV) {
608
+ console.warn('⚠️ SuiteCRM Service: No valid configuration found');
609
+ }
610
+ }
611
+ }
612
+ validateConfiguration() {
613
+ if (!this.isConfigured) {
614
+ throw new Error('SuiteCRM service is not properly configured');
615
+ }
616
+ }
617
+ async getSessionAndCSRFToken() {
618
+ // Check if we have valid session and CSRF token
619
+ if (this.sessionId && this.csrfToken) {
620
+ return { sessionId: this.sessionId, csrfToken: this.csrfToken };
621
+ }
622
+ if (import.meta.env.DEV) {
623
+ console.log('🔍 Starting session and CSRF token extraction...');
624
+ }
625
+ try {
626
+ // Step 1: Get initial session by accessing the login page
627
+ const loginResponse = await fetch(`${this.baseUrl}/`, {
628
+ method: 'GET',
629
+ headers: {
630
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
631
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
632
+ }
633
+ });
634
+ if (import.meta.env.DEV) {
635
+ console.log('📊 Login response status:', loginResponse.status);
636
+ console.log('📊 Login response ok:', loginResponse.ok);
637
+ }
638
+ if (!loginResponse.ok) {
639
+ throw new Error(`Failed to access SuiteCRM login page: ${loginResponse.status}`);
640
+ }
641
+ // Extract session ID from cookies
642
+ const cookies = loginResponse.headers.get('set-cookie');
643
+ if (import.meta.env.DEV) {
644
+ console.log('🍪 Set-Cookie header:', cookies);
645
+ }
646
+ if (cookies) {
647
+ // Try SCRMSESSID first (SuiteCRM specific)
648
+ const sessionMatch = cookies.match(/SCRMSESSID=([^;]+)/);
649
+ if (sessionMatch) {
650
+ this.sessionId = sessionMatch[1];
651
+ if (import.meta.env.DEV) {
652
+ console.log('✅ Found SCRMSESSID:', this.sessionId);
653
+ }
654
+ }
655
+ else {
656
+ if (import.meta.env.DEV) {
657
+ console.log('❌ SCRMSESSID not found in cookies');
658
+ }
659
+ // Fallback to PHPSESSID
660
+ const phpSessionMatch = cookies.match(/PHPSESSID=([^;]+)/);
661
+ if (phpSessionMatch) {
662
+ this.sessionId = phpSessionMatch[1];
663
+ if (import.meta.env.DEV) {
664
+ console.log('✅ Found PHPSESSID:', this.sessionId);
665
+ }
666
+ }
667
+ else {
668
+ if (import.meta.env.DEV) {
669
+ console.log('❌ PHPSESSID not found in cookies');
670
+ }
671
+ }
672
+ }
673
+ }
674
+ else {
675
+ if (import.meta.env.DEV) {
676
+ console.log('❌ No set-cookie header found');
677
+ }
678
+ }
679
+ // Step 2: Extract CSRF token from cookies (XSRF-TOKEN)
680
+ if (cookies) {
681
+ const xsrfMatch = cookies.match(/XSRF-TOKEN=([^;]+)/);
682
+ if (xsrfMatch) {
683
+ this.csrfToken = xsrfMatch[1];
684
+ if (import.meta.env.DEV) {
685
+ console.log('✅ Found XSRF-TOKEN:', this.csrfToken);
686
+ }
687
+ }
688
+ else {
689
+ if (import.meta.env.DEV) {
690
+ console.log('❌ XSRF-TOKEN not found in cookies');
691
+ }
692
+ }
693
+ }
694
+ // If not found in cookies, try HTML
695
+ if (!this.csrfToken) {
696
+ if (import.meta.env.DEV) {
697
+ console.log('🔍 Trying to extract CSRF token from HTML...');
698
+ }
699
+ const loginHtml = await loginResponse.text();
700
+ const csrfMatch = loginHtml.match(/name="csrf_token" value="([^"]+)"/);
701
+ if (csrfMatch) {
702
+ this.csrfToken = csrfMatch[1];
703
+ if (import.meta.env.DEV) {
704
+ console.log('✅ Found csrf_token in HTML:', this.csrfToken);
705
+ }
706
+ }
707
+ else {
708
+ if (import.meta.env.DEV) {
709
+ console.log('❌ csrf_token not found in HTML');
710
+ }
711
+ }
712
+ }
713
+ if (import.meta.env.DEV) {
714
+ console.log('📋 Final Summary:');
715
+ console.log('Session ID:', this.sessionId ? '✅ Found' : '❌ Not found');
716
+ console.log('CSRF Token:', this.csrfToken ? '✅ Found' : '❌ Not found');
717
+ }
718
+ if (!this.sessionId || !this.csrfToken) {
719
+ throw new Error('Failed to obtain session ID or CSRF token from SuiteCRM');
720
+ }
721
+ if (import.meta.env.DEV) {
722
+ console.log('🔑 Session and CSRF token obtained:', {
723
+ sessionId: this.sessionId ? 'SET' : 'NOT SET',
724
+ csrfToken: this.csrfToken ? 'SET' : 'NOT SET'
725
+ });
726
+ }
727
+ return { sessionId: this.sessionId, csrfToken: this.csrfToken };
728
+ }
729
+ catch (error) {
730
+ if (import.meta.env.DEV) {
731
+ console.error('❌ Error in getSessionAndCSRFToken:', error);
732
+ console.error('Error details:', {
733
+ message: error.message,
734
+ stack: error.stack,
735
+ name: error.name
736
+ });
737
+ }
738
+ throw error;
739
+ }
740
+ }
741
+ async getAccessToken() {
742
+ if (!this.oauth2Config) {
743
+ throw new Error('OAuth2 configuration not available');
744
+ }
745
+ // Check if we have a valid cached token
746
+ if (this.accessToken && this.tokenExpiresAt && Date.now() < this.tokenExpiresAt - 300000) {
747
+ return this.accessToken;
748
+ }
749
+ // Request new OAuth2 token using password grant
750
+ const tokenUrl = `${this.baseUrl}/Api/access_token`;
751
+ // Ensure password is properly handled for special characters
752
+ const rawPassword = this.oauth2Config.password || '';
753
+ const password = encodePassword(rawPassword);
754
+ // Validate password and log warnings
755
+ const passwordValidation = validatePassword(rawPassword);
756
+ if (!passwordValidation.isValid && import.meta.env.DEV) {
757
+ console.warn('⚠️ Password validation issues:', passwordValidation.issues);
758
+ }
759
+ const recommendations = getPasswordRecommendations(rawPassword);
760
+ if (recommendations.length > 0 && import.meta.env.DEV) {
761
+ console.warn('💡 Password recommendations:', recommendations);
762
+ }
763
+ const tokenData = {
764
+ grant_type: 'password',
765
+ client_id: this.oauth2Config.clientId,
766
+ client_secret: this.oauth2Config.clientSecret,
767
+ username: this.oauth2Config.username,
768
+ password: password,
769
+ };
770
+ if (import.meta.env.DEV) {
771
+ console.log('🔑 Requesting OAuth2 token with password grant:', {
772
+ tokenUrl,
773
+ clientId: tokenData.client_id,
774
+ username: tokenData.username,
775
+ hasPassword: !!tokenData.password
776
+ });
777
+ }
778
+ const response = await fetch(tokenUrl, {
779
+ method: 'POST',
780
+ headers: {
781
+ 'Content-Type': 'application/json',
782
+ },
783
+ body: JSON.stringify(tokenData),
784
+ });
785
+ if (!response.ok) {
786
+ const errorText = await response.text();
787
+ throw new Error(`OAuth2 token request failed: ${response.status} - ${errorText}`);
788
+ }
789
+ const tokenResponse = await response.json();
790
+ if (import.meta.env.DEV) {
791
+ console.log('🔑 OAuth2 token obtained:', {
792
+ tokenType: tokenResponse.token_type,
793
+ expiresIn: tokenResponse.expires_in
794
+ });
795
+ }
796
+ this.accessToken = tokenResponse.access_token || '';
797
+ this.tokenExpiresAt = Date.now() + (tokenResponse.expires_in * 1000);
798
+ return this.accessToken || '';
799
+ }
800
+ async getAuthHeaders() {
801
+ if (this.useProxy) {
802
+ return {
803
+ 'Content-Type': 'application/json'
804
+ };
805
+ }
806
+ else {
807
+ // Direct SuiteCRM API access with OAuth2 Bearer token
808
+ if (this.oauth2Config) {
809
+ const accessToken = await this.getAccessToken();
810
+ return {
811
+ 'Content-Type': 'application/json',
812
+ 'Accept': 'application/json',
813
+ 'Authorization': `Bearer ${accessToken}`
814
+ };
815
+ }
816
+ else {
817
+ // Fallback to session-based authentication
818
+ const { sessionId, csrfToken } = await this.getSessionAndCSRFToken();
819
+ return {
820
+ 'Content-Type': 'application/json',
821
+ 'Accept': 'application/json',
822
+ 'X-CSRF-Token': csrfToken,
823
+ 'Cookie': `SCRMSESSID=${sessionId}`
824
+ };
825
+ }
826
+ }
827
+ }
828
+ async makeRequest(endpoint, options = {}) {
829
+ this.validateConfiguration();
830
+ const headers = {
831
+ ...(await this.getAuthHeaders()),
832
+ ...options.headers
833
+ };
834
+ let url;
835
+ if (this.useProxy) {
836
+ // Use Cloudflare Worker proxy - pass endpoint as query parameter
837
+ const queryParams = new URLSearchParams({ endpoint });
838
+ url = `${this.proxyUrl}?${queryParams.toString()}`;
839
+ }
840
+ else {
841
+ // Direct SuiteCRM API access
842
+ const directUrl = `${this.baseUrl}/api${endpoint}`;
843
+ const localDev = import.meta.env.VITE_LOCAL_DEVELOPMENT === 'true';
844
+ const shouldUseAccess = !localDev && import.meta.env.VITE_AUTH_MODE === 'direct'
845
+ && !!import.meta.env.VITE_CF_ACCESS_CLIENT_ID && !!import.meta.env.VITE_CF_ACCESS_CLIENT_SECRET;
846
+ url = shouldUseAccess ? `/api/fetch-with-access?target=${encodeURIComponent(directUrl)}` : directUrl;
847
+ }
848
+ if (import.meta.env.DEV) {
849
+ console.log(`🔗 SuiteCRM API Request: ${options.method || 'GET'} ${url}`);
850
+ console.log('🔑 Headers:', Object.keys(headers).map(k => `${k}: ${headers[k] ? '***' : 'undefined'}`).join(', '));
851
+ }
852
+ const response = await fetch(url, {
853
+ ...options,
854
+ headers
855
+ });
856
+ if (!response.ok) {
857
+ const errorText = await response.text();
858
+ console.error('❌ SuiteCRM API Error:', response.status, errorText);
859
+ throw new Error(`SuiteCRM API error: ${response.status} - ${errorText}`);
860
+ }
861
+ const data = await response.json();
862
+ if (import.meta.env.DEV) {
863
+ console.log('✅ SuiteCRM API Response:', data);
864
+ }
865
+ return data;
866
+ }
867
+ // Check if SuiteCRM is configured
868
+ isServiceConfigured() {
869
+ return this.isConfigured;
870
+ }
871
+ // Lead Management
872
+ async createLead(lead) {
873
+ if (import.meta.env.DEV) {
874
+ console.log('🚀 Starting lead creation for:', lead.first_name, lead.last_name);
875
+ }
876
+ // Use REST API by default since GraphQL has permission issues
877
+ // GraphQL can be used later when permissions are resolved
878
+ return this.createLeadREST(lead);
879
+ }
880
+ async createLeadREST(lead) {
881
+ if (import.meta.env.DEV) {
882
+ console.log('🔍 Attempting REST API lead creation...');
883
+ }
884
+ const leadData = {
885
+ data: {
886
+ type: 'Leads',
887
+ attributes: {
888
+ first_name: lead.first_name,
889
+ last_name: lead.last_name,
890
+ email1: lead.email1,
891
+ phone_work: lead.phone_work,
892
+ lead_source: lead.lead_source || 'Website',
893
+ status: lead.status || 'New',
894
+ description: lead.description,
895
+ campaign_id: lead.campaign_id
896
+ }
897
+ }
898
+ };
899
+ const result = await this.makeRequest('/legacy/Api/V8/module', {
900
+ method: 'POST',
901
+ body: JSON.stringify(leadData),
902
+ });
903
+ if (import.meta.env.DEV) {
904
+ console.log('✅ REST API lead creation successful:', result);
905
+ }
906
+ return result;
907
+ }
908
+ async verifyLeadCreation(email, firstName, lastName) {
909
+ try {
910
+ if (import.meta.env.DEV) {
911
+ console.log('🔍 Verifying lead creation for:', email);
912
+ }
913
+ // Try to get all leads and filter client-side since SuiteCRM filter syntax is complex
914
+ const result = await this.makeRequest('/legacy/Api/V8/module/Leads');
915
+ if (result.data && Array.isArray(result.data)) {
916
+ // Check if any lead matches the email and name
917
+ const matchingLead = result.data.find((lead) => lead.attributes?.email1 === email &&
918
+ lead.attributes?.first_name === firstName &&
919
+ lead.attributes?.last_name === lastName);
920
+ if (import.meta.env.DEV) {
921
+ console.log('✅ Lead verification result:', !!matchingLead);
922
+ console.log('🔍 Found leads:', result.data.length);
923
+ console.log('🔍 Matching lead:', matchingLead);
924
+ }
925
+ return !!matchingLead;
926
+ }
927
+ return false;
928
+ }
929
+ catch (error) {
930
+ if (import.meta.env.DEV) {
931
+ console.log('❌ Lead verification failed:', error);
932
+ }
933
+ return false;
934
+ }
935
+ }
936
+ async getLead(leadId) {
937
+ return this.makeRequest(`/legacy/Api/V8/module/Leads/${leadId}`);
938
+ }
939
+ async updateLead(leadId, lead) {
940
+ const leadData = {
941
+ data: {
942
+ type: 'Leads',
943
+ id: leadId,
944
+ attributes: lead
945
+ }
946
+ };
947
+ return this.makeRequest(`/legacy/Api/V8/module/Leads/${leadId}`, {
948
+ method: 'PATCH',
949
+ body: JSON.stringify(leadData),
950
+ });
951
+ }
952
+ async getLeads(params = {}) {
953
+ const queryParams = new URLSearchParams();
954
+ if (params.page)
955
+ queryParams.append('page[number]', params.page.toString());
956
+ if (params.per_page)
957
+ queryParams.append('page[size]', params.per_page.toString());
958
+ if (params.filter)
959
+ queryParams.append('filter', params.filter);
960
+ if (params.sort)
961
+ queryParams.append('sort', params.sort);
962
+ const queryString = queryParams.toString();
963
+ const endpoint = queryString ? `/legacy/Api/V8/module/Leads?${queryString}` : '/legacy/Api/V8/module/Leads';
964
+ return this.makeRequest(endpoint);
965
+ }
966
+ // Contact Management
967
+ async createContact(contact) {
968
+ const contactData = {
969
+ data: {
970
+ type: 'Contacts',
971
+ attributes: contact
972
+ }
973
+ };
974
+ return this.makeRequest('/contacts', {
975
+ method: 'POST',
976
+ body: JSON.stringify(contactData),
977
+ });
978
+ }
979
+ async getContact(contactId) {
980
+ return this.makeRequest(`/contacts/${contactId}`);
981
+ }
982
+ async updateContact(contactId, contact) {
983
+ const contactData = {
984
+ data: {
985
+ type: 'Contacts',
986
+ id: contactId,
987
+ attributes: contact
988
+ }
989
+ };
990
+ return this.makeRequest(`/contacts/${contactId}`, {
991
+ method: 'PATCH',
992
+ body: JSON.stringify(contactData),
993
+ });
994
+ }
995
+ // Account Management
996
+ async createAccount(account) {
997
+ const accountData = {
998
+ data: {
999
+ type: 'Accounts',
1000
+ attributes: account
1001
+ }
1002
+ };
1003
+ return this.makeRequest('/accounts', {
1004
+ method: 'POST',
1005
+ body: JSON.stringify(accountData),
1006
+ });
1007
+ }
1008
+ async getAccount(accountId) {
1009
+ return this.makeRequest(`/accounts/${accountId}`);
1010
+ }
1011
+ // Opportunity Management
1012
+ async createOpportunity(opportunity) {
1013
+ const opportunityData = {
1014
+ data: {
1015
+ type: 'Opportunities',
1016
+ attributes: opportunity
1017
+ }
1018
+ };
1019
+ return this.makeRequest('/opportunities', {
1020
+ method: 'POST',
1021
+ body: JSON.stringify(opportunityData),
1022
+ });
1023
+ }
1024
+ async getOpportunity(opportunityId) {
1025
+ return this.makeRequest(`/opportunities/${opportunityId}`);
1026
+ }
1027
+ // Search and Filter
1028
+ async searchLeads(query) {
1029
+ const searchParams = new URLSearchParams({
1030
+ filter: `contains(first_name,"${query}") or contains(last_name,"${query}") or contains(email1,"${query}")`
1031
+ });
1032
+ return this.makeRequest(`/legacy/Api/V8/module/Leads?${searchParams.toString()}`);
1033
+ }
1034
+ // Utility Methods
1035
+ async getLeadSources() {
1036
+ return this.makeRequest('/enum/lead_sources');
1037
+ }
1038
+ async getLeadStatuses() {
1039
+ return this.makeRequest('/enum/lead_statuses');
1040
+ }
1041
+ // GraphQL support for SuiteCRM 8.8.0+
1042
+ async graphqlRequest(query, variables) {
1043
+ this.validateConfiguration();
1044
+ const headers = await this.getAuthHeaders();
1045
+ // Use the correct endpoint based on mode
1046
+ let url;
1047
+ if (this.useProxy) {
1048
+ // Use Cloudflare Worker proxy for GraphQL - pass endpoint as query parameter
1049
+ const queryParams = new URLSearchParams({ endpoint: '/Api/graphql' });
1050
+ url = `${this.proxyUrl}?${queryParams.toString()}`;
1051
+ }
1052
+ else {
1053
+ // Direct SuiteCRM API access
1054
+ url = `${this.baseUrl}/Api/graphql`;
1055
+ }
1056
+ if (import.meta.env.DEV) {
1057
+ console.log(`🔗 SuiteCRM GraphQL Request: POST ${url}`);
1058
+ console.log('📝 Query:', query);
1059
+ console.log('🔧 Variables:', variables);
1060
+ console.log('🔑 Headers:', Object.keys(headers).map(k => `${k}: ${headers[k] ? '***' : 'undefined'}`).join(', '));
1061
+ }
1062
+ const response = await fetch(url, {
1063
+ method: 'POST',
1064
+ headers,
1065
+ body: JSON.stringify({
1066
+ query,
1067
+ variables
1068
+ })
1069
+ });
1070
+ if (!response.ok) {
1071
+ const errorText = await response.text();
1072
+ console.error('❌ SuiteCRM GraphQL Error:', response.status, errorText);
1073
+ throw new Error(`SuiteCRM GraphQL error: ${response.status} - ${errorText}`);
1074
+ }
1075
+ const data = await response.json();
1076
+ if (import.meta.env.DEV) {
1077
+ console.log('✅ SuiteCRM GraphQL Response:', data);
1078
+ }
1079
+ return data;
1080
+ }
1081
+ // Test connection with GraphQL
1082
+ async testConnection() {
1083
+ try {
1084
+ // Try GraphQL first
1085
+ const graphqlQuery = `
1086
+ query TestConnection {
1087
+ __schema {
1088
+ types {
1089
+ name
1090
+ }
1091
+ }
1092
+ }
1093
+ `;
1094
+ await this.graphqlRequest(graphqlQuery);
1095
+ return true;
1096
+ }
1097
+ catch (graphqlError) {
1098
+ console.log('GraphQL test failed, trying REST API:', graphqlError);
1099
+ try {
1100
+ // Fallback to REST API
1101
+ await this.makeRequest('/leads?page[size]=1');
1102
+ return true;
1103
+ }
1104
+ catch (restError) {
1105
+ console.error('❌ SuiteCRM connection test failed:', { graphqlError, restError });
1106
+ return false;
1107
+ }
1108
+ }
1109
+ }
1110
+ // Simple Lead Creation - Create new lead directly
1111
+ async createLeadSimple(leadData) {
1112
+ if (import.meta.env.DEV) {
1113
+ console.log('🚀 Creating new lead for:', leadData.first_name, leadData.last_name, leadData.email1);
1114
+ }
1115
+ try {
1116
+ const result = await this.createLeadREST(leadData);
1117
+ if (import.meta.env.DEV) {
1118
+ console.log('✅ Lead created successfully');
1119
+ }
1120
+ return {
1121
+ data: result.data,
1122
+ meta: { action: 'lead_created' }
1123
+ };
1124
+ }
1125
+ catch (error) {
1126
+ if (import.meta.env.DEV) {
1127
+ console.error('❌ Lead creation failed:', error);
1128
+ }
1129
+ throw error;
1130
+ }
1131
+ }
1132
+ }
1133
+ let suitecrmServiceInstance = null;
1134
+ const getSuiteCRMService = () => {
1135
+ if (!suitecrmServiceInstance) {
1136
+ suitecrmServiceInstance = new SuiteCRMService();
1137
+ }
1138
+ return suitecrmServiceInstance;
1139
+ };
1140
+ const resetSuiteCRMService = () => {
1141
+ suitecrmServiceInstance = null;
1142
+ };
1143
+ const suitecrmService = createLazyProxy(getSuiteCRMService);
1144
+
1145
+ /**
1146
+ * Cloudflare WordPress Webhook Service
1147
+ * Handles webhook status and data regeneration monitoring
1148
+ */
1149
+ class CfWpWebhookService {
1150
+ baseUrl;
1151
+ refreshSecret;
1152
+ constructor() {
1153
+ this.baseUrl = getEnv('VITE_WORDPRESS_API_URL') || '';
1154
+ this.refreshSecret = getEnv('VITE_WP_REFRESH_SECRET') || '';
1155
+ if (!this.refreshSecret) {
1156
+ console.warn('⚠️ VITE_WP_REFRESH_SECRET not configured - webhook authentication will fail');
1157
+ }
1158
+ }
1159
+ /**
1160
+ * Get webhook status from WordPress
1161
+ */
1162
+ async getWebhookStatus() {
1163
+ try {
1164
+ const response = await fetch(`${this.baseUrl}/wp-json/cf-wp/v1/webhook-status`, {
1165
+ method: 'GET',
1166
+ headers: {
1167
+ 'Content-Type': 'application/json',
1168
+ 'Authorization': `Bearer ${this.refreshSecret}`
1169
+ }
1170
+ });
1171
+ if (!response.ok) {
1172
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1173
+ }
1174
+ return await response.json();
1175
+ }
1176
+ catch (error) {
1177
+ console.error('Failed to fetch webhook status:', error);
1178
+ return {
1179
+ last_triggered: null,
1180
+ last_success: null,
1181
+ last_error: error instanceof Error ? error.message : 'Unknown error',
1182
+ total_triggers: 0,
1183
+ success_rate: 0,
1184
+ status: 'error'
1185
+ };
1186
+ }
1187
+ }
1188
+ /**
1189
+ * Get webhook configuration from WordPress
1190
+ */
1191
+ async getWebhookConfig() {
1192
+ try {
1193
+ const response = await fetch(`${this.baseUrl}/wp-json/cf-wp/v1/webhook-config`, {
1194
+ method: 'GET',
1195
+ headers: {
1196
+ 'Content-Type': 'application/json',
1197
+ 'Authorization': `Bearer ${this.refreshSecret}`
1198
+ }
1199
+ });
1200
+ if (!response.ok) {
1201
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1202
+ }
1203
+ return await response.json();
1204
+ }
1205
+ catch (error) {
1206
+ console.error('Failed to fetch webhook config:', error);
1207
+ return null;
1208
+ }
1209
+ }
1210
+ /**
1211
+ * Test webhook endpoint
1212
+ */
1213
+ async testWebhook() {
1214
+ try {
1215
+ const testPayload = {
1216
+ post_id: 0,
1217
+ post_type: 'test',
1218
+ action: 'test',
1219
+ timestamp: new Date().toISOString(),
1220
+ frontend_id: 'test'
1221
+ };
1222
+ const response = await fetch('/api/webhook-test', {
1223
+ method: 'POST',
1224
+ headers: {
1225
+ 'Content-Type': 'application/json',
1226
+ 'Authorization': `Bearer ${this.refreshSecret}`
1227
+ },
1228
+ body: JSON.stringify(testPayload)
1229
+ });
1230
+ if (!response.ok) {
1231
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1232
+ }
1233
+ const result = await response.json();
1234
+ return {
1235
+ success: result.success,
1236
+ message: result.message || 'Webhook test completed'
1237
+ };
1238
+ }
1239
+ catch (error) {
1240
+ console.error('Webhook test failed:', error);
1241
+ return {
1242
+ success: false,
1243
+ message: error instanceof Error ? error.message : 'Unknown error'
1244
+ };
1245
+ }
1246
+ }
1247
+ /**
1248
+ * Get last webhook response
1249
+ */
1250
+ async getLastWebhookResponse() {
1251
+ try {
1252
+ const response = await fetch('/webhook-response.json');
1253
+ if (!response.ok) {
1254
+ return null;
1255
+ }
1256
+ return await response.json();
1257
+ }
1258
+ catch (error) {
1259
+ console.error('Failed to fetch last webhook response:', error);
1260
+ return null;
1261
+ }
1262
+ }
1263
+ /**
1264
+ * Manually trigger data regeneration
1265
+ */
1266
+ async triggerDataRegeneration() {
1267
+ try {
1268
+ const response = await fetch('/api/trigger-regeneration', {
1269
+ method: 'POST',
1270
+ headers: {
1271
+ 'Content-Type': 'application/json',
1272
+ 'Authorization': `Bearer ${this.refreshSecret}`
1273
+ }
1274
+ });
1275
+ if (!response.ok) {
1276
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1277
+ }
1278
+ const result = await response.json();
1279
+ return {
1280
+ success: result.success,
1281
+ message: result.message || 'Data regeneration triggered'
1282
+ };
1283
+ }
1284
+ catch (error) {
1285
+ console.error('Failed to trigger data regeneration:', error);
1286
+ return {
1287
+ success: false,
1288
+ message: error instanceof Error ? error.message : 'Unknown error'
1289
+ };
1290
+ }
1291
+ }
1292
+ /**
1293
+ * Check if webhook is properly configured
1294
+ */
1295
+ isConfigured() {
1296
+ return !!(this.baseUrl && this.refreshSecret);
1297
+ }
1298
+ /**
1299
+ * Get configuration status
1300
+ */
1301
+ getConfigurationStatus() {
1302
+ return {
1303
+ wordpress_url: !!this.baseUrl,
1304
+ refresh_secret: !!this.refreshSecret,
1305
+ fully_configured: !!(this.baseUrl && this.refreshSecret)
1306
+ };
1307
+ }
1308
+ }
1309
+ let cfWpWebhookServiceInstance = null;
1310
+ const getCfWpWebhookService = () => {
1311
+ if (!cfWpWebhookServiceInstance) {
1312
+ cfWpWebhookServiceInstance = new CfWpWebhookService();
1313
+ }
1314
+ return cfWpWebhookServiceInstance;
1315
+ };
1316
+ const resetCfWpWebhookService = () => {
1317
+ cfWpWebhookServiceInstance = null;
1318
+ };
1319
+ const cfWpWebhookService = createLazyProxy(getCfWpWebhookService);
1320
+
1321
+ export { cfWpWebhookService, createGravityFormsClient, createMauticService, getCfWpWebhookService, getGravityFormsClient, getMauticService, getSuiteCRMService, gravityFormsClient, gravityFormsService, mauticService, resetCfWpWebhookService, resetGravityFormsClient, resetMauticService, resetSuiteCRMService, suitecrmService };
1322
+ //# sourceMappingURL=services.esm.js.map