@gxp-dev/tools 2.0.6 → 2.0.7

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 (99) hide show
  1. package/browser-extensions/README.md +1 -0
  2. package/browser-extensions/chrome/background.js +857 -0
  3. package/browser-extensions/chrome/content.js +51 -0
  4. package/browser-extensions/chrome/devtools.html +9 -0
  5. package/browser-extensions/chrome/devtools.js +23 -0
  6. package/browser-extensions/chrome/icons/gx_off_128.png +0 -0
  7. package/browser-extensions/chrome/icons/gx_off_16.png +0 -0
  8. package/browser-extensions/chrome/icons/gx_off_32.png +0 -0
  9. package/browser-extensions/chrome/icons/gx_off_64.png +0 -0
  10. package/browser-extensions/chrome/icons/gx_on_128.png +0 -0
  11. package/browser-extensions/chrome/icons/gx_on_16.png +0 -0
  12. package/browser-extensions/chrome/icons/gx_on_32.png +0 -0
  13. package/browser-extensions/chrome/icons/gx_on_64.png +0 -0
  14. package/browser-extensions/chrome/inspector.js +1087 -0
  15. package/browser-extensions/chrome/manifest.json +70 -0
  16. package/browser-extensions/chrome/panel.html +638 -0
  17. package/browser-extensions/chrome/panel.js +862 -0
  18. package/browser-extensions/chrome/popup.html +399 -0
  19. package/browser-extensions/chrome/popup.js +515 -0
  20. package/browser-extensions/chrome/rules.json +1 -0
  21. package/browser-extensions/chrome/test-chrome.html +145 -0
  22. package/browser-extensions/chrome/test-mixed-content.html +190 -0
  23. package/browser-extensions/chrome/test-uri-pattern.html +199 -0
  24. package/browser-extensions/firefox/README.md +134 -0
  25. package/browser-extensions/firefox/background.js +804 -0
  26. package/browser-extensions/firefox/content.js +120 -0
  27. package/browser-extensions/firefox/debug-errors.html +229 -0
  28. package/browser-extensions/firefox/debug-https.html +113 -0
  29. package/browser-extensions/firefox/devtools.html +9 -0
  30. package/browser-extensions/firefox/devtools.js +24 -0
  31. package/browser-extensions/firefox/icons/gx_off_128.png +0 -0
  32. package/browser-extensions/firefox/icons/gx_off_16.png +0 -0
  33. package/browser-extensions/firefox/icons/gx_off_32.png +0 -0
  34. package/browser-extensions/firefox/icons/gx_off_64.png +0 -0
  35. package/browser-extensions/firefox/icons/gx_on_128.png +0 -0
  36. package/browser-extensions/firefox/icons/gx_on_16.png +0 -0
  37. package/browser-extensions/firefox/icons/gx_on_32.png +0 -0
  38. package/browser-extensions/firefox/icons/gx_on_64.png +0 -0
  39. package/browser-extensions/firefox/inspector.js +1087 -0
  40. package/browser-extensions/firefox/manifest.json +67 -0
  41. package/browser-extensions/firefox/panel.html +638 -0
  42. package/browser-extensions/firefox/panel.js +862 -0
  43. package/browser-extensions/firefox/popup.html +525 -0
  44. package/browser-extensions/firefox/popup.js +536 -0
  45. package/browser-extensions/firefox/test-gramercy.html +126 -0
  46. package/browser-extensions/firefox/test-imports.html +58 -0
  47. package/browser-extensions/firefox/test-masking.html +147 -0
  48. package/browser-extensions/firefox/test-uri-pattern.html +199 -0
  49. package/package.json +7 -2
  50. package/runtime/PortalContainer.vue +326 -0
  51. package/runtime/dev-tools/DevToolsModal.vue +217 -0
  52. package/runtime/dev-tools/LayoutSwitcher.vue +221 -0
  53. package/runtime/dev-tools/MockDataEditor.vue +621 -0
  54. package/runtime/dev-tools/SocketSimulator.vue +562 -0
  55. package/runtime/dev-tools/StoreInspector.vue +644 -0
  56. package/runtime/dev-tools/index.js +6 -0
  57. package/runtime/gxpStringsPlugin.js +428 -0
  58. package/runtime/index.html +22 -0
  59. package/runtime/main.js +32 -0
  60. package/runtime/mock-api/auth-middleware.js +97 -0
  61. package/runtime/mock-api/image-generator.js +221 -0
  62. package/runtime/mock-api/index.js +197 -0
  63. package/runtime/mock-api/response-generator.js +394 -0
  64. package/runtime/mock-api/route-generator.js +323 -0
  65. package/runtime/mock-api/socket-triggers.js +371 -0
  66. package/runtime/mock-api/spec-loader.js +300 -0
  67. package/runtime/server.js +180 -0
  68. package/runtime/stores/gxpPortalConfigStore.js +554 -0
  69. package/runtime/stores/index.js +6 -0
  70. package/runtime/vite-inspector-plugin.js +749 -0
  71. package/runtime/vite-source-tracker-plugin.js +232 -0
  72. package/runtime/vite.config.js +402 -0
  73. package/scripts/launch-chrome.js +90 -0
  74. package/scripts/pack-chrome.js +91 -0
  75. package/socket-events/AiSessionMessageCreated.json +18 -0
  76. package/socket-events/SocialStreamPostCreated.json +24 -0
  77. package/socket-events/SocialStreamPostVariantCompleted.json +23 -0
  78. package/template/README.md +332 -0
  79. package/template/app-manifest.json +32 -0
  80. package/template/dev-assets/images/avatar-placeholder.png +0 -0
  81. package/template/dev-assets/images/background-placeholder.jpg +0 -0
  82. package/template/dev-assets/images/banner-placeholder.jpg +0 -0
  83. package/template/dev-assets/images/icon-placeholder.png +0 -0
  84. package/template/dev-assets/images/logo-placeholder.png +0 -0
  85. package/template/dev-assets/images/product-placeholder.jpg +0 -0
  86. package/template/dev-assets/images/thumbnail-placeholder.jpg +0 -0
  87. package/template/env.example +51 -0
  88. package/template/gitignore +53 -0
  89. package/template/index.html +22 -0
  90. package/template/main.js +28 -0
  91. package/template/src/DemoPage.vue +459 -0
  92. package/template/src/Plugin.vue +38 -0
  93. package/template/src/stores/index.js +9 -0
  94. package/template/src/stores/test-data.json +173 -0
  95. package/template/theme-layouts/AdditionalStyling.css +0 -0
  96. package/template/theme-layouts/PrivateLayout.vue +39 -0
  97. package/template/theme-layouts/PublicLayout.vue +39 -0
  98. package/template/theme-layouts/SystemLayout.vue +39 -0
  99. package/template/vite.config.js +333 -0
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Socket Triggers
3
+ *
4
+ * Parses AsyncAPI specs for x-triggered-by extensions and emits
5
+ * Socket.IO events after matching API calls complete.
6
+ */
7
+
8
+ /**
9
+ * Parse AsyncAPI spec for socket triggers
10
+ * @param {object} asyncApiSpec - AsyncAPI specification
11
+ * @returns {object} Map of operation keys to trigger definitions
12
+ */
13
+ function parseSocketTriggers(asyncApiSpec) {
14
+ const triggers = {};
15
+
16
+ if (!asyncApiSpec) {
17
+ return triggers;
18
+ }
19
+
20
+ // Handle AsyncAPI 2.x format
21
+ if (asyncApiSpec.channels) {
22
+ parseChannels(asyncApiSpec.channels, asyncApiSpec, triggers);
23
+ }
24
+
25
+ // Handle AsyncAPI 3.x format
26
+ if (asyncApiSpec.operations) {
27
+ parseOperations(asyncApiSpec.operations, asyncApiSpec, triggers);
28
+ }
29
+
30
+ // Also check components/messages directly
31
+ if (asyncApiSpec.components?.messages) {
32
+ parseMessages(asyncApiSpec.components.messages, triggers);
33
+ }
34
+
35
+ const triggerCount = Object.keys(triggers).length;
36
+ if (triggerCount > 0) {
37
+ console.log(`📡 Parsed ${triggerCount} socket trigger definitions`);
38
+ }
39
+
40
+ return triggers;
41
+ }
42
+
43
+ /**
44
+ * Parse channels (AsyncAPI 2.x)
45
+ * @param {object} channels - Channels object
46
+ * @param {object} spec - Full spec
47
+ * @param {object} triggers - Triggers map to populate
48
+ */
49
+ function parseChannels(channels, spec, triggers) {
50
+ for (const [channelName, channel] of Object.entries(channels)) {
51
+ // Check publish/subscribe operations
52
+ const operations = [
53
+ channel.publish,
54
+ channel.subscribe,
55
+ ].filter(Boolean);
56
+
57
+ for (const operation of operations) {
58
+ const message = operation.message;
59
+ if (message) {
60
+ extractTriggersFromMessage(channelName, message, spec, triggers);
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Parse operations (AsyncAPI 3.x)
68
+ * @param {object} operations - Operations object
69
+ * @param {object} spec - Full spec
70
+ * @param {object} triggers - Triggers map to populate
71
+ */
72
+ function parseOperations(operations, spec, triggers) {
73
+ for (const [, operation] of Object.entries(operations)) {
74
+ if (operation.messages) {
75
+ for (const message of operation.messages) {
76
+ const channelName = operation.channel?.$ref || "default";
77
+ extractTriggersFromMessage(channelName, message, spec, triggers);
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Parse messages directly from components
85
+ * @param {object} messages - Messages object
86
+ * @param {object} triggers - Triggers map to populate
87
+ */
88
+ function parseMessages(messages, triggers) {
89
+ for (const [messageName, message] of Object.entries(messages)) {
90
+ if (message["x-triggered-by"]) {
91
+ const triggerDefs = Array.isArray(message["x-triggered-by"])
92
+ ? message["x-triggered-by"]
93
+ : [message["x-triggered-by"]];
94
+
95
+ for (const triggerDef of triggerDefs) {
96
+ const operationKey = triggerDef.operation;
97
+ if (!operationKey) continue;
98
+
99
+ if (!triggers[operationKey]) {
100
+ triggers[operationKey] = [];
101
+ }
102
+
103
+ triggers[operationKey].push({
104
+ event: message.name || messageName,
105
+ channel: triggerDef.channel,
106
+ delay: triggerDef.delay || 0,
107
+ condition: triggerDef.condition,
108
+ payload: triggerDef.payload || message.payload,
109
+ });
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Extract triggers from a message definition
117
+ * @param {string} channelName - Channel name
118
+ * @param {object} message - Message object
119
+ * @param {object} spec - Full spec for $ref resolution
120
+ * @param {object} triggers - Triggers map to populate
121
+ */
122
+ function extractTriggersFromMessage(channelName, message, spec, triggers) {
123
+ // Resolve $ref if needed
124
+ let resolvedMessage = message;
125
+ if (message.$ref) {
126
+ resolvedMessage = resolveRef(message.$ref, spec) || message;
127
+ }
128
+
129
+ // Check for x-triggered-by extension
130
+ const triggerDefs = resolvedMessage["x-triggered-by"];
131
+ if (!triggerDefs) return;
132
+
133
+ const triggerArray = Array.isArray(triggerDefs) ? triggerDefs : [triggerDefs];
134
+
135
+ for (const triggerDef of triggerArray) {
136
+ const operationKey = triggerDef.operation;
137
+ if (!operationKey) continue;
138
+
139
+ if (!triggers[operationKey]) {
140
+ triggers[operationKey] = [];
141
+ }
142
+
143
+ triggers[operationKey].push({
144
+ event: resolvedMessage.name || resolvedMessage.messageId || "unknown",
145
+ channel: triggerDef.channel || channelName,
146
+ delay: triggerDef.delay || 0,
147
+ condition: triggerDef.condition,
148
+ payload: triggerDef.payload || resolvedMessage.payload,
149
+ });
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Resolve a $ref reference
155
+ * @param {string} ref - Reference string
156
+ * @param {object} spec - Full spec
157
+ * @returns {object|null} Resolved object
158
+ */
159
+ function resolveRef(ref, spec) {
160
+ if (!ref || !ref.startsWith("#/")) {
161
+ return null;
162
+ }
163
+
164
+ const parts = ref.slice(2).split("/");
165
+ let current = spec;
166
+
167
+ for (const part of parts) {
168
+ if (current && typeof current === "object" && part in current) {
169
+ current = current[part];
170
+ } else {
171
+ return null;
172
+ }
173
+ }
174
+
175
+ return current;
176
+ }
177
+
178
+ /**
179
+ * Template a payload with request/response data
180
+ * @param {object} payload - Payload template
181
+ * @param {object} context - Context with request and response data
182
+ * @returns {object} Templated payload
183
+ */
184
+ function templatePayload(payload, context) {
185
+ if (!payload) return {};
186
+
187
+ const payloadStr = JSON.stringify(payload);
188
+
189
+ // Replace template variables
190
+ const templated = payloadStr.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
191
+ const value = resolvePath(path.trim(), context);
192
+
193
+ if (value === undefined) {
194
+ return match; // Keep original if not found
195
+ }
196
+
197
+ // Handle different types
198
+ if (typeof value === "string") {
199
+ return value;
200
+ }
201
+
202
+ return JSON.stringify(value);
203
+ });
204
+
205
+ try {
206
+ return JSON.parse(templated);
207
+ } catch {
208
+ // If JSON parse fails, return original
209
+ return payload;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Resolve a dot-notation path in an object
215
+ * @param {string} path - Dot-notation path (e.g., "response.body.id")
216
+ * @param {object} obj - Object to resolve in
217
+ * @returns {*} Resolved value
218
+ */
219
+ function resolvePath(path, obj) {
220
+ // Handle special values
221
+ if (path === "now") {
222
+ return new Date().toISOString();
223
+ }
224
+
225
+ if (path === "timestamp") {
226
+ return Date.now();
227
+ }
228
+
229
+ const parts = path.split(".");
230
+ let current = obj;
231
+
232
+ for (const part of parts) {
233
+ if (current && typeof current === "object" && part in current) {
234
+ current = current[part];
235
+ } else {
236
+ return undefined;
237
+ }
238
+ }
239
+
240
+ return current;
241
+ }
242
+
243
+ /**
244
+ * Evaluate a condition expression
245
+ * @param {string} condition - Condition expression
246
+ * @param {object} context - Context with request and response data
247
+ * @returns {boolean} Condition result
248
+ */
249
+ function evaluateCondition(condition, context) {
250
+ if (!condition) return true;
251
+
252
+ try {
253
+ // Simple condition parsing (e.g., "response.status == 200")
254
+ const match = condition.match(/^([^\s]+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/);
255
+
256
+ if (!match) return true;
257
+
258
+ const [, path, operator, valueStr] = match;
259
+ const actualValue = resolvePath(path, context);
260
+
261
+ // Parse the expected value
262
+ let expectedValue;
263
+ try {
264
+ expectedValue = JSON.parse(valueStr);
265
+ } catch {
266
+ expectedValue = valueStr;
267
+ }
268
+
269
+ switch (operator) {
270
+ case "==":
271
+ return actualValue == expectedValue;
272
+ case "!=":
273
+ return actualValue != expectedValue;
274
+ case ">":
275
+ return actualValue > expectedValue;
276
+ case "<":
277
+ return actualValue < expectedValue;
278
+ case ">=":
279
+ return actualValue >= expectedValue;
280
+ case "<=":
281
+ return actualValue <= expectedValue;
282
+ default:
283
+ return true;
284
+ }
285
+ } catch {
286
+ return true;
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Trigger socket events for a completed API operation
292
+ * @param {object} io - Socket.IO server instance
293
+ * @param {object} socketTriggers - Parsed trigger definitions
294
+ * @param {string} operationKey - Operation key (e.g., "POST /events/{eventId}/checkin")
295
+ * @param {object} context - Request/response context
296
+ */
297
+ function triggerSocketEvents(io, socketTriggers, operationKey, context) {
298
+ if (!io || !socketTriggers) return;
299
+
300
+ const triggers = socketTriggers[operationKey];
301
+ if (!triggers || triggers.length === 0) return;
302
+
303
+ for (const trigger of triggers) {
304
+ // Evaluate condition
305
+ if (!evaluateCondition(trigger.condition, context)) {
306
+ console.log(` ⏭️ Skipped socket event (condition not met): ${trigger.event}`);
307
+ continue;
308
+ }
309
+
310
+ // Template the payload
311
+ const payload = templatePayload(trigger.payload, context);
312
+
313
+ // Template the channel name
314
+ let channel = trigger.channel || "";
315
+ channel = channel.replace(/\{([^}]+)\}/g, (match, path) => {
316
+ const value = resolvePath(`request.params.${path}`, context);
317
+ return value !== undefined ? value : match;
318
+ });
319
+
320
+ // Schedule the emit
321
+ const delay = trigger.delay || 0;
322
+
323
+ const emit = () => {
324
+ console.log(` 📡 Emitting socket event: ${trigger.event}`);
325
+ if (channel) {
326
+ console.log(` Channel: ${channel}`);
327
+ }
328
+
329
+ io.emit(trigger.event, payload);
330
+ };
331
+
332
+ if (delay > 0) {
333
+ console.log(` ⏱️ Scheduling socket event: ${trigger.event} (${delay}ms delay)`);
334
+ setTimeout(emit, delay);
335
+ } else {
336
+ emit();
337
+ }
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Get trigger statistics
343
+ * @param {object} socketTriggers - Parsed trigger definitions
344
+ * @returns {object} Statistics
345
+ */
346
+ function getTriggerStats(socketTriggers) {
347
+ if (!socketTriggers) {
348
+ return { total: 0, operations: [] };
349
+ }
350
+
351
+ const operations = [];
352
+ let total = 0;
353
+
354
+ for (const [operation, triggers] of Object.entries(socketTriggers)) {
355
+ total += triggers.length;
356
+ operations.push({
357
+ operation,
358
+ events: triggers.map((t) => t.event),
359
+ });
360
+ }
361
+
362
+ return { total, operations };
363
+ }
364
+
365
+ module.exports = {
366
+ parseSocketTriggers,
367
+ triggerSocketEvents,
368
+ templatePayload,
369
+ evaluateCondition,
370
+ getTriggerStats,
371
+ };
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Spec Loader
3
+ *
4
+ * Fetches and caches OpenAPI, AsyncAPI, and Webhook specs from the platform.
5
+ * Falls back to local files when the platform API is unreachable.
6
+ */
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const axios = require("axios");
11
+ const https = require("https");
12
+
13
+ // Import environment URLs from constants
14
+ let ENVIRONMENT_URLS;
15
+ try {
16
+ // When running from node_modules
17
+ ENVIRONMENT_URLS = require("../../bin/lib/constants").ENVIRONMENT_URLS;
18
+ } catch {
19
+ // Fallback for direct execution
20
+ ENVIRONMENT_URLS = require(path.join(
21
+ __dirname,
22
+ "../../bin/lib/constants"
23
+ )).ENVIRONMENT_URLS;
24
+ }
25
+
26
+ // Spec cache
27
+ const cache = {
28
+ openApi: null,
29
+ asyncApi: null,
30
+ webhooks: null,
31
+ lastFetch: null,
32
+ };
33
+
34
+ // Default cache TTL (5 minutes)
35
+ const DEFAULT_CACHE_TTL = 5 * 60 * 1000;
36
+
37
+ /**
38
+ * Get cache TTL from environment or use default
39
+ */
40
+ function getCacheTTL() {
41
+ return parseInt(process.env.MOCK_API_CACHE_TTL) || DEFAULT_CACHE_TTL;
42
+ }
43
+
44
+ /**
45
+ * Get environment configuration based on API_ENV
46
+ * @param {string} env - Environment name (defaults to 'production')
47
+ * @returns {object} Environment URLs configuration
48
+ */
49
+ function getEnvironmentConfig(env) {
50
+ const envName = env || process.env.API_ENV || "production";
51
+ const config = ENVIRONMENT_URLS[envName];
52
+
53
+ if (!config) {
54
+ console.warn(
55
+ `⚠️ Unknown environment "${envName}", falling back to production`
56
+ );
57
+ return ENVIRONMENT_URLS.production;
58
+ }
59
+
60
+ return config;
61
+ }
62
+
63
+ /**
64
+ * Create axios instance with SSL handling for local development
65
+ */
66
+ function createHttpClient() {
67
+ return axios.create({
68
+ timeout: 10000,
69
+ httpsAgent: new https.Agent({
70
+ rejectUnauthorized: process.env.NODE_ENV === "production",
71
+ }),
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Fetch a spec from a URL
77
+ * @param {string} url - URL to fetch
78
+ * @param {string} specName - Name for logging
79
+ * @returns {object|null} Parsed JSON spec or null on error
80
+ */
81
+ async function fetchSpec(url, specName) {
82
+ const client = createHttpClient();
83
+
84
+ try {
85
+ console.log(`📡 Fetching ${specName} from ${url}`);
86
+ const response = await client.get(url);
87
+ console.log(`✅ ${specName} loaded successfully`);
88
+ return response.data;
89
+ } catch (error) {
90
+ const message = error.response
91
+ ? `HTTP ${error.response.status}`
92
+ : error.message;
93
+ console.warn(`⚠️ Failed to fetch ${specName}: ${message}`);
94
+ return null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Load a spec from a local file
100
+ * @param {string} projectRoot - Project root directory
101
+ * @param {string} filename - Filename to load
102
+ * @param {string} specName - Name for logging
103
+ * @returns {object|null} Parsed JSON spec or null if not found
104
+ */
105
+ function loadLocalSpec(projectRoot, filename, specName) {
106
+ const filePath = path.join(projectRoot, filename);
107
+
108
+ if (!fs.existsSync(filePath)) {
109
+ return null;
110
+ }
111
+
112
+ try {
113
+ console.log(`📁 Loading local ${specName} from ${filename}`);
114
+ const content = fs.readFileSync(filePath, "utf-8");
115
+ const spec = JSON.parse(content);
116
+ console.log(`✅ Local ${specName} loaded successfully`);
117
+ return spec;
118
+ } catch (error) {
119
+ console.warn(`⚠️ Failed to parse local ${specName}: ${error.message}`);
120
+ return null;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Fetch OpenAPI spec from platform or local file
126
+ * @param {string} projectRoot - Project root for local fallback
127
+ * @returns {object|null} OpenAPI spec
128
+ */
129
+ async function fetchOpenApiSpec(projectRoot) {
130
+ const config = getEnvironmentConfig();
131
+
132
+ // Try remote first
133
+ let spec = await fetchSpec(config.openApiSpec, "OpenAPI spec");
134
+
135
+ // Fallback to local
136
+ if (!spec && projectRoot) {
137
+ spec = loadLocalSpec(projectRoot, "openapi.json", "OpenAPI spec");
138
+ }
139
+
140
+ return spec;
141
+ }
142
+
143
+ /**
144
+ * Fetch AsyncAPI spec from platform or local file
145
+ * @param {string} projectRoot - Project root for local fallback
146
+ * @returns {object|null} AsyncAPI spec
147
+ */
148
+ async function fetchAsyncApiSpec(projectRoot) {
149
+ const config = getEnvironmentConfig();
150
+
151
+ // Try remote first
152
+ let spec = await fetchSpec(config.asyncApiSpec, "AsyncAPI spec");
153
+
154
+ // Fallback to local
155
+ if (!spec && projectRoot) {
156
+ spec = loadLocalSpec(projectRoot, "asyncapi.json", "AsyncAPI spec");
157
+ }
158
+
159
+ return spec;
160
+ }
161
+
162
+ /**
163
+ * Fetch Webhook spec from platform or local file
164
+ * @param {string} projectRoot - Project root for local fallback
165
+ * @returns {object|null} Webhook spec
166
+ */
167
+ async function fetchWebhookSpec(projectRoot) {
168
+ const config = getEnvironmentConfig();
169
+
170
+ // Try remote first
171
+ let spec = await fetchSpec(config.webhookSpec, "Webhook spec");
172
+
173
+ // Fallback to local
174
+ if (!spec && projectRoot) {
175
+ spec = loadLocalSpec(projectRoot, "webhooks.json", "Webhook spec");
176
+ }
177
+
178
+ return spec;
179
+ }
180
+
181
+ /**
182
+ * Check if cache is still valid
183
+ * @returns {boolean} True if cache is valid
184
+ */
185
+ function isCacheValid() {
186
+ if (!cache.lastFetch) {
187
+ return false;
188
+ }
189
+
190
+ const elapsed = Date.now() - cache.lastFetch;
191
+ return elapsed < getCacheTTL();
192
+ }
193
+
194
+ /**
195
+ * Load all specs with caching
196
+ * @param {string} projectRoot - Project root for local fallback
197
+ * @param {boolean} forceRefresh - Force refresh even if cache is valid
198
+ * @returns {object} Object containing all specs
199
+ */
200
+ async function loadSpecs(projectRoot, forceRefresh = false) {
201
+ // Return cached if valid and not forcing refresh
202
+ if (!forceRefresh && isCacheValid()) {
203
+ console.log("📦 Using cached specs");
204
+ return {
205
+ openApi: cache.openApi,
206
+ asyncApi: cache.asyncApi,
207
+ webhooks: cache.webhooks,
208
+ };
209
+ }
210
+
211
+ const root = projectRoot || process.cwd();
212
+
213
+ console.log("🔄 Loading API specs...");
214
+ console.log(` Environment: ${process.env.API_ENV || "production"}`);
215
+
216
+ // Fetch all specs in parallel
217
+ const [openApi, asyncApi, webhooks] = await Promise.all([
218
+ fetchOpenApiSpec(root),
219
+ fetchAsyncApiSpec(root),
220
+ fetchWebhookSpec(root),
221
+ ]);
222
+
223
+ // Update cache
224
+ cache.openApi = openApi;
225
+ cache.asyncApi = asyncApi;
226
+ cache.webhooks = webhooks;
227
+ cache.lastFetch = Date.now();
228
+
229
+ // Log summary
230
+ const loaded = [];
231
+ if (openApi) loaded.push("OpenAPI");
232
+ if (asyncApi) loaded.push("AsyncAPI");
233
+ if (webhooks) loaded.push("Webhooks");
234
+
235
+ if (loaded.length > 0) {
236
+ console.log(`✅ Loaded specs: ${loaded.join(", ")}`);
237
+ } else {
238
+ console.warn("⚠️ No specs were loaded");
239
+ }
240
+
241
+ return { openApi, asyncApi, webhooks };
242
+ }
243
+
244
+ /**
245
+ * Clear the spec cache and refetch
246
+ * @param {string} projectRoot - Project root for local fallback
247
+ * @returns {object} Freshly loaded specs
248
+ */
249
+ async function refreshSpecs(projectRoot) {
250
+ console.log("🔄 Refreshing API specs...");
251
+ cache.openApi = null;
252
+ cache.asyncApi = null;
253
+ cache.webhooks = null;
254
+ cache.lastFetch = null;
255
+
256
+ return loadSpecs(projectRoot, true);
257
+ }
258
+
259
+ /**
260
+ * Get current cache status
261
+ * @returns {object} Cache status info
262
+ */
263
+ function getCacheStatus() {
264
+ const ttl = getCacheTTL();
265
+ const elapsed = cache.lastFetch ? Date.now() - cache.lastFetch : null;
266
+
267
+ return {
268
+ hasOpenApi: !!cache.openApi,
269
+ hasAsyncApi: !!cache.asyncApi,
270
+ hasWebhooks: !!cache.webhooks,
271
+ lastFetch: cache.lastFetch ? new Date(cache.lastFetch).toISOString() : null,
272
+ cacheValid: isCacheValid(),
273
+ ttlMs: ttl,
274
+ expiresIn: elapsed !== null ? Math.max(0, ttl - elapsed) : null,
275
+ };
276
+ }
277
+
278
+ /**
279
+ * Get the raw cached specs (for direct access)
280
+ * @returns {object} Cached specs
281
+ */
282
+ function getCachedSpecs() {
283
+ return {
284
+ openApi: cache.openApi,
285
+ asyncApi: cache.asyncApi,
286
+ webhooks: cache.webhooks,
287
+ };
288
+ }
289
+
290
+ module.exports = {
291
+ getEnvironmentConfig,
292
+ fetchOpenApiSpec,
293
+ fetchAsyncApiSpec,
294
+ fetchWebhookSpec,
295
+ loadSpecs,
296
+ refreshSpecs,
297
+ getCacheStatus,
298
+ getCachedSpecs,
299
+ isCacheValid,
300
+ };