@memberjunction/server 5.27.1 → 5.29.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 (50) hide show
  1. package/dist/config.d.ts +151 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +15 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/generated/generated.d.ts +959 -5
  6. package/dist/generated/generated.d.ts.map +1 -1
  7. package/dist/generated/generated.js +4639 -280
  8. package/dist/generated/generated.js.map +1 -1
  9. package/dist/generic/ResolverBase.d.ts +14 -0
  10. package/dist/generic/ResolverBase.d.ts.map +1 -1
  11. package/dist/generic/ResolverBase.js +37 -3
  12. package/dist/generic/ResolverBase.js.map +1 -1
  13. package/dist/generic/RestoreContextInput.d.ts +27 -0
  14. package/dist/generic/RestoreContextInput.d.ts.map +1 -0
  15. package/dist/generic/RestoreContextInput.js +39 -0
  16. package/dist/generic/RestoreContextInput.js.map +1 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +21 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/resolvers/FeedbackResolver.d.ts +150 -0
  22. package/dist/resolvers/FeedbackResolver.d.ts.map +1 -0
  23. package/dist/resolvers/FeedbackResolver.js +876 -0
  24. package/dist/resolvers/FeedbackResolver.js.map +1 -0
  25. package/dist/resolvers/FileResolver.d.ts +27 -0
  26. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  27. package/dist/resolvers/FileResolver.js +32 -3
  28. package/dist/resolvers/FileResolver.js.map +1 -1
  29. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +18 -1
  30. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  31. package/dist/resolvers/IntegrationDiscoveryResolver.js +247 -22
  32. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  33. package/dist/resolvers/MCPResolver.d.ts +77 -0
  34. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  35. package/dist/resolvers/MCPResolver.js +300 -1
  36. package/dist/resolvers/MCPResolver.js.map +1 -1
  37. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  38. package/dist/resolvers/RunAIAgentResolver.js +87 -32
  39. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  40. package/package.json +68 -66
  41. package/src/config.ts +19 -0
  42. package/src/generated/generated.ts +3430 -281
  43. package/src/generic/ResolverBase.ts +41 -4
  44. package/src/generic/RestoreContextInput.ts +32 -0
  45. package/src/index.ts +22 -5
  46. package/src/resolvers/FeedbackResolver.ts +940 -0
  47. package/src/resolvers/FileResolver.ts +33 -4
  48. package/src/resolvers/IntegrationDiscoveryResolver.ts +224 -20
  49. package/src/resolvers/MCPResolver.ts +297 -1
  50. package/src/resolvers/RunAIAgentResolver.ts +89 -32
@@ -0,0 +1,876 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { Resolver, Mutation, Query, Arg, Ctx, Field, InputType, ObjectType } from 'type-graphql';
14
+ import { Octokit } from '@octokit/rest';
15
+ import { createAppAuth } from '@octokit/auth-app';
16
+ import { LogError, LogStatus } from '@memberjunction/core';
17
+ import { configInfo } from '../config.js';
18
+ import { z } from 'zod';
19
+ import { ChatParams, ChatMessageRole, GetAIAPIKey, BaseLLM } from '@memberjunction/ai';
20
+ import { AIEngine } from '@memberjunction/aiengine';
21
+ import { MJGlobal } from '@memberjunction/global';
22
+ import { readFileSync } from 'fs';
23
+ import { resolve } from 'path';
24
+ import { homedir } from 'os';
25
+ // Module-level cache — survives across resolver instances (type-graphql creates a new instance per request)
26
+ let cachedPrivateKey; // undefined = not yet read
27
+ let cachedOctokit = null;
28
+ let cachedAuthType = null;
29
+ // ============================================================================
30
+ // GraphQL Input/Output Types
31
+ // ============================================================================
32
+ /**
33
+ * Input type for feedback submission
34
+ */
35
+ let SubmitFeedbackInput = class SubmitFeedbackInput {
36
+ };
37
+ __decorate([
38
+ Field(),
39
+ __metadata("design:type", String)
40
+ ], SubmitFeedbackInput.prototype, "Title", void 0);
41
+ __decorate([
42
+ Field(),
43
+ __metadata("design:type", String)
44
+ ], SubmitFeedbackInput.prototype, "Description", void 0);
45
+ __decorate([
46
+ Field(),
47
+ __metadata("design:type", String)
48
+ ], SubmitFeedbackInput.prototype, "Category", void 0);
49
+ __decorate([
50
+ Field({ nullable: true }),
51
+ __metadata("design:type", String)
52
+ ], SubmitFeedbackInput.prototype, "StepsToReproduce", void 0);
53
+ __decorate([
54
+ Field({ nullable: true }),
55
+ __metadata("design:type", String)
56
+ ], SubmitFeedbackInput.prototype, "ExpectedBehavior", void 0);
57
+ __decorate([
58
+ Field({ nullable: true }),
59
+ __metadata("design:type", String)
60
+ ], SubmitFeedbackInput.prototype, "ActualBehavior", void 0);
61
+ __decorate([
62
+ Field({ nullable: true }),
63
+ __metadata("design:type", String)
64
+ ], SubmitFeedbackInput.prototype, "Severity", void 0);
65
+ __decorate([
66
+ Field({ nullable: true }),
67
+ __metadata("design:type", String)
68
+ ], SubmitFeedbackInput.prototype, "UseCase", void 0);
69
+ __decorate([
70
+ Field({ nullable: true }),
71
+ __metadata("design:type", String)
72
+ ], SubmitFeedbackInput.prototype, "ProposedSolution", void 0);
73
+ __decorate([
74
+ Field({ nullable: true }),
75
+ __metadata("design:type", String)
76
+ ], SubmitFeedbackInput.prototype, "Email", void 0);
77
+ __decorate([
78
+ Field({ nullable: true }),
79
+ __metadata("design:type", String)
80
+ ], SubmitFeedbackInput.prototype, "Name", void 0);
81
+ __decorate([
82
+ Field({ nullable: true }),
83
+ __metadata("design:type", String)
84
+ ], SubmitFeedbackInput.prototype, "Environment", void 0);
85
+ __decorate([
86
+ Field({ nullable: true }),
87
+ __metadata("design:type", String)
88
+ ], SubmitFeedbackInput.prototype, "AffectedArea", void 0);
89
+ __decorate([
90
+ Field({ nullable: true }),
91
+ __metadata("design:type", String)
92
+ ], SubmitFeedbackInput.prototype, "CurrentPage", void 0);
93
+ __decorate([
94
+ Field({ nullable: true }),
95
+ __metadata("design:type", String)
96
+ ], SubmitFeedbackInput.prototype, "UserAgent", void 0);
97
+ __decorate([
98
+ Field({ nullable: true }),
99
+ __metadata("design:type", String)
100
+ ], SubmitFeedbackInput.prototype, "ScreenSize", void 0);
101
+ __decorate([
102
+ Field({ nullable: true }),
103
+ __metadata("design:type", String)
104
+ ], SubmitFeedbackInput.prototype, "AppName", void 0);
105
+ __decorate([
106
+ Field({ nullable: true }),
107
+ __metadata("design:type", String)
108
+ ], SubmitFeedbackInput.prototype, "AppVersion", void 0);
109
+ __decorate([
110
+ Field({ nullable: true }),
111
+ __metadata("design:type", String)
112
+ ], SubmitFeedbackInput.prototype, "UserId", void 0);
113
+ __decorate([
114
+ Field({ nullable: true }),
115
+ __metadata("design:type", String)
116
+ ], SubmitFeedbackInput.prototype, "Timestamp", void 0);
117
+ __decorate([
118
+ Field({ nullable: true }),
119
+ __metadata("design:type", String)
120
+ ], SubmitFeedbackInput.prototype, "Metadata", void 0);
121
+ SubmitFeedbackInput = __decorate([
122
+ InputType()
123
+ ], SubmitFeedbackInput);
124
+ export { SubmitFeedbackInput };
125
+ /**
126
+ * Response type for feedback submission
127
+ */
128
+ let FeedbackResponseType = class FeedbackResponseType {
129
+ };
130
+ __decorate([
131
+ Field(),
132
+ __metadata("design:type", Boolean)
133
+ ], FeedbackResponseType.prototype, "Success", void 0);
134
+ __decorate([
135
+ Field({ nullable: true }),
136
+ __metadata("design:type", Number)
137
+ ], FeedbackResponseType.prototype, "IssueNumber", void 0);
138
+ __decorate([
139
+ Field({ nullable: true }),
140
+ __metadata("design:type", String)
141
+ ], FeedbackResponseType.prototype, "IssueUrl", void 0);
142
+ __decorate([
143
+ Field({ nullable: true }),
144
+ __metadata("design:type", String)
145
+ ], FeedbackResponseType.prototype, "Error", void 0);
146
+ FeedbackResponseType = __decorate([
147
+ ObjectType()
148
+ ], FeedbackResponseType);
149
+ export { FeedbackResponseType };
150
+ /**
151
+ * Input type for feedback classification
152
+ */
153
+ let ClassifyFeedbackInput = class ClassifyFeedbackInput {
154
+ };
155
+ __decorate([
156
+ Field(),
157
+ __metadata("design:type", String)
158
+ ], ClassifyFeedbackInput.prototype, "Title", void 0);
159
+ __decorate([
160
+ Field(),
161
+ __metadata("design:type", String)
162
+ ], ClassifyFeedbackInput.prototype, "Description", void 0);
163
+ ClassifyFeedbackInput = __decorate([
164
+ InputType()
165
+ ], ClassifyFeedbackInput);
166
+ export { ClassifyFeedbackInput };
167
+ /**
168
+ * Response type for feedback classification
169
+ */
170
+ let FeedbackClassificationResult = class FeedbackClassificationResult {
171
+ };
172
+ __decorate([
173
+ Field(),
174
+ __metadata("design:type", Boolean)
175
+ ], FeedbackClassificationResult.prototype, "Success", void 0);
176
+ __decorate([
177
+ Field({ nullable: true }),
178
+ __metadata("design:type", String)
179
+ ], FeedbackClassificationResult.prototype, "Category", void 0);
180
+ __decorate([
181
+ Field({ nullable: true }),
182
+ __metadata("design:type", String)
183
+ ], FeedbackClassificationResult.prototype, "Severity", void 0);
184
+ __decorate([
185
+ Field({ nullable: true }),
186
+ __metadata("design:type", String)
187
+ ], FeedbackClassificationResult.prototype, "Error", void 0);
188
+ FeedbackClassificationResult = __decorate([
189
+ ObjectType()
190
+ ], FeedbackClassificationResult);
191
+ export { FeedbackClassificationResult };
192
+ // ============================================================================
193
+ // Validation Schema
194
+ // ============================================================================
195
+ const FeedbackSubmissionSchema = z.object({
196
+ title: z
197
+ .string()
198
+ .min(5, 'Title must be at least 5 characters')
199
+ .max(256, 'Title must not exceed 256 characters'),
200
+ description: z
201
+ .string()
202
+ .min(20, 'Description must be at least 20 characters')
203
+ .max(10000, 'Description must not exceed 10000 characters'),
204
+ category: z.union([
205
+ z.literal('bug'),
206
+ z.literal('feature'),
207
+ z.literal('question'),
208
+ z.literal('other'),
209
+ z.string().max(50),
210
+ ]),
211
+ stepsToReproduce: z.string().max(5000).optional(),
212
+ expectedBehavior: z.string().max(5000).optional(),
213
+ actualBehavior: z.string().max(5000).optional(),
214
+ severity: z.enum(['critical', 'major', 'minor', 'trivial']).optional(),
215
+ useCase: z.string().max(5000).optional(),
216
+ proposedSolution: z.string().max(5000).optional(),
217
+ email: z.string().email().optional().or(z.literal('')),
218
+ name: z.string().max(100).optional(),
219
+ environment: z.enum(['production', 'staging', 'development', 'local']).optional(),
220
+ affectedArea: z.string().max(100).optional(),
221
+ currentPage: z.string().max(200).optional(),
222
+ userAgent: z.string().max(500).optional(),
223
+ screenSize: z.string().max(20).optional(),
224
+ appName: z.string().max(100).optional(),
225
+ appVersion: z.string().max(50).optional(),
226
+ userId: z.string().max(100).optional(),
227
+ timestamp: z.string().optional(),
228
+ metadata: z.record(z.unknown()).optional(),
229
+ });
230
+ // ============================================================================
231
+ // Display Labels
232
+ // ============================================================================
233
+ const categoryLabels = {
234
+ bug: 'Bug Report',
235
+ feature: 'Feature Request',
236
+ question: 'Question',
237
+ other: 'Other',
238
+ };
239
+ const severityLabels = {
240
+ critical: 'Critical',
241
+ major: 'Major',
242
+ minor: 'Minor',
243
+ trivial: 'Trivial',
244
+ };
245
+ const environmentLabels = {
246
+ production: 'Production',
247
+ staging: 'Staging',
248
+ development: 'Development',
249
+ local: 'Local',
250
+ };
251
+ // ============================================================================
252
+ // Resolver
253
+ // ============================================================================
254
+ let FeedbackResolver = class FeedbackResolver {
255
+ /**
256
+ * Submit feedback and create a GitHub issue
257
+ */
258
+ /**
259
+ * Check if the feedback feature is enabled for this org.
260
+ */
261
+ FeedbackEnabled() {
262
+ return this.isFeedbackEnabled();
263
+ }
264
+ /**
265
+ * Feedback is considered enabled only when both (a) the org kill switch
266
+ * is not explicitly off and (b) GitHub credentials are configured. Without
267
+ * (b) the feature cannot create issues, so we hide it rather than letting
268
+ * users submit into a broken pipe.
269
+ */
270
+ isFeedbackEnabled() {
271
+ if (configInfo.feedbackSettings.enabled === false) {
272
+ return false;
273
+ }
274
+ return this.resolveAuth() !== null;
275
+ }
276
+ async SubmitFeedback(input, ctx) {
277
+ try {
278
+ if (!this.isFeedbackEnabled()) {
279
+ return { Success: false, Error: 'Feedback is disabled for this organization.' };
280
+ }
281
+ // Get configuration
282
+ const config = this.getConfig();
283
+ if (!config) {
284
+ return {
285
+ Success: false,
286
+ Error: 'Feedback system is not configured. GITHUB_PAT environment variable is required.',
287
+ };
288
+ }
289
+ // Convert input to internal format
290
+ const submission = this.convertInputToSubmission(input, ctx);
291
+ // Validate submission
292
+ const validationResult = FeedbackSubmissionSchema.safeParse(submission);
293
+ if (!validationResult.success) {
294
+ const errorMessages = validationResult.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`);
295
+ return {
296
+ Success: false,
297
+ Error: `Validation failed: ${errorMessages.join('; ')}`,
298
+ };
299
+ }
300
+ // Create GitHub issue
301
+ const issue = await this.createGitHubIssue(submission, config);
302
+ LogStatus(`Feedback submitted: Issue #${issue.number} created`);
303
+ return {
304
+ Success: true,
305
+ IssueNumber: issue.number,
306
+ IssueUrl: issue.html_url,
307
+ };
308
+ }
309
+ catch (error) {
310
+ LogError('Error submitting feedback', undefined, error);
311
+ const message = error instanceof Error ? error.message : 'Unknown error occurred';
312
+ return {
313
+ Success: false,
314
+ Error: message,
315
+ };
316
+ }
317
+ }
318
+ /**
319
+ * Classify feedback using an LLM to suggest category and severity.
320
+ */
321
+ async ClassifyFeedback(input, ctx) {
322
+ try {
323
+ if (!this.isFeedbackEnabled()) {
324
+ return { Success: false, Error: 'Feedback is disabled for this organization.' };
325
+ }
326
+ // Get the current context user for AIEngine config
327
+ const contextUser = ctx.userPayload?.userRecord;
328
+ await AIEngine.Instance.Config(false, contextUser);
329
+ // Get active LLM models with valid API keys (guard against null DriverClass)
330
+ const allModels = AIEngine.Instance.Models.filter(m => m.AIModelType?.trim().toLowerCase() === 'llm' && m.IsActive && m.DriverClass);
331
+ const models = allModels.filter(m => {
332
+ try {
333
+ const key = GetAIAPIKey(m.DriverClass);
334
+ return key && key.trim().length > 0;
335
+ }
336
+ catch {
337
+ return false;
338
+ }
339
+ });
340
+ if (models.length === 0) {
341
+ return { Success: false, Error: 'No AI models configured' };
342
+ }
343
+ // Pick a fast, cheap model — prefer Groq, then OpenAI, then any available
344
+ const model = models.find(m => m.DriverClass === 'GroqLLM')
345
+ || models.find(m => m.DriverClass === 'OpenAILLM')
346
+ || models[0];
347
+ const apiKey = GetAIAPIKey(model.DriverClass);
348
+ const llm = MJGlobal.Instance.ClassFactory.CreateInstance(BaseLLM, model.DriverClass, apiKey);
349
+ if (!llm) {
350
+ return { Success: false, Error: 'Failed to create LLM instance' };
351
+ }
352
+ const chatParams = new ChatParams();
353
+ chatParams.model = model.APIName;
354
+ chatParams.messages = [
355
+ {
356
+ role: ChatMessageRole.system,
357
+ content: `Classify this user feedback. Respond with ONLY a JSON object, no other text:
358
+ {"category": "bug"|"feature"|"question"|"other", "severity": "critical"|"major"|"minor"|"trivial"}
359
+
360
+ Step 1 — CATEGORY. Choose ONE. Read the intent, not just keywords:
361
+ - "question" = user is SEEKING HELP or INFORMATION. Clues: "how do I", "where is", "can I", "is it possible", "I can't find", "where did ___ go", "used to see ___ but now", "help me". Even if they sound frustrated, if they're looking for guidance it's a question. A user who can't FIND something is asking a question, not reporting a bug.
362
+ - "feature" = user WANTS something NEW: "it would be nice", "can you add", "I wish", "please add", "would love to see"
363
+ - "bug" = something is OBJECTIVELY BROKEN: "error message", "crash", "500 error", "white screen", "data lost", "button does nothing when clicked", "stops responding". The system is malfunctioning, not just confusing.
364
+ - "other" = doesn't fit the above
365
+
366
+ Step 2 — SEVERITY. Strictly follow these rules:
367
+ - If category is "question" → severity MUST be "trivial"
368
+ - If category is "feature" → severity MUST be "minor"
369
+ - If category is "other" → severity MUST be "minor"
370
+ - If category is "bug":
371
+ - "critical" = data loss, security vulnerability, entire system unusable for ALL users
372
+ - "major" = user EXPLICITLY says words like "blocking me", "preventing me from", "can't complete my work", "unable to do my job". Without these exact phrases, do NOT use major.
373
+ - "trivial" = purely visual: typo, misspelling, alignment, spacing, colors, dark mode, theme, font, styling, cosmetic appearance
374
+ - "minor" = everything else. This is the DEFAULT for bugs. If unsure between trivial and minor, pick trivial. If unsure between minor and major, pick minor.`
375
+ },
376
+ {
377
+ role: ChatMessageRole.user,
378
+ content: `Title: ${input.Title}\nDescription: ${input.Description}`
379
+ }
380
+ ];
381
+ const result = await llm.ChatCompletion(chatParams);
382
+ const content = result?.data?.choices?.[0]?.message?.content?.trim();
383
+ if (!content) {
384
+ return { Success: false, Error: 'Empty response from AI' };
385
+ }
386
+ // Parse JSON from response (handle potential markdown code fences)
387
+ const jsonStr = content.replace(/```json?\s*/g, '').replace(/```/g, '').trim();
388
+ const parsed = JSON.parse(jsonStr);
389
+ return {
390
+ Success: true,
391
+ Category: parsed.category,
392
+ Severity: parsed.severity
393
+ };
394
+ }
395
+ catch (error) {
396
+ LogError('Error classifying feedback', undefined, error);
397
+ return { Success: false, Error: 'Classification failed' };
398
+ }
399
+ }
400
+ // ============================================================================
401
+ // Private Helper Methods
402
+ // ============================================================================
403
+ /**
404
+ * Get feedback configuration from environment/config.
405
+ * Supports GitHub App auth (preferred) or PAT fallback.
406
+ */
407
+ getConfig() {
408
+ const auth = this.resolveAuth();
409
+ if (!auth) {
410
+ return null;
411
+ }
412
+ const githubSettings = configInfo.feedbackSettings?.github;
413
+ return {
414
+ owner: githubSettings?.owner || process.env.GITHUB_FEEDBACK_OWNER || 'MemberJunction',
415
+ repo: githubSettings?.repo || process.env.GITHUB_FEEDBACK_REPO || 'MJ',
416
+ auth,
417
+ defaultLabels: githubSettings?.defaultLabels || ['user-submitted'],
418
+ categoryLabels: githubSettings?.categoryLabels || {
419
+ bug: 'bug',
420
+ feature: 'enhancement',
421
+ question: 'question',
422
+ other: 'triage',
423
+ },
424
+ severityLabels: githubSettings?.severityLabels || {
425
+ critical: 'priority: critical',
426
+ major: 'priority: high',
427
+ minor: 'priority: medium',
428
+ trivial: 'priority: low',
429
+ },
430
+ assignees: githubSettings?.assignees,
431
+ };
432
+ }
433
+ /**
434
+ * Resolve authentication — prefers GitHub App, falls back to PAT.
435
+ */
436
+ resolveAuth() {
437
+ // Try GitHub App first
438
+ const appId = process.env.GITHUB_APP_ID;
439
+ const installationId = process.env.GITHUB_APP_INSTALLATION_ID;
440
+ const privateKey = this.readPrivateKey();
441
+ if (appId && installationId && privateKey) {
442
+ return {
443
+ type: 'app',
444
+ appId: Number(appId),
445
+ installationId: Number(installationId),
446
+ privateKey
447
+ };
448
+ }
449
+ // Fall back to PAT
450
+ const token = process.env.GITHUB_PAT;
451
+ if (token) {
452
+ return { type: 'pat', token };
453
+ }
454
+ return null;
455
+ }
456
+ /**
457
+ * Read the GitHub App private key from file path or inline env var.
458
+ * Cached at module level — the key file doesn't change at runtime.
459
+ */
460
+ readPrivateKey() {
461
+ if (cachedPrivateKey !== undefined) {
462
+ return cachedPrivateKey;
463
+ }
464
+ // Option 1: File path
465
+ const keyPath = process.env.GITHUB_APP_PRIVATE_KEY_PATH;
466
+ if (keyPath) {
467
+ try {
468
+ const resolved = keyPath.startsWith('~/')
469
+ ? resolve(homedir(), keyPath.slice(2))
470
+ : resolve(keyPath);
471
+ cachedPrivateKey = readFileSync(resolved, 'utf-8');
472
+ return cachedPrivateKey;
473
+ }
474
+ catch (error) {
475
+ LogError('Failed to read GitHub App private key file', undefined, error);
476
+ cachedPrivateKey = null;
477
+ return null;
478
+ }
479
+ }
480
+ // Option 2: Inline key (with \n replaced)
481
+ const inlineKey = process.env.GITHUB_APP_PRIVATE_KEY;
482
+ if (inlineKey) {
483
+ cachedPrivateKey = inlineKey.replace(/\\n/g, '\n');
484
+ return cachedPrivateKey;
485
+ }
486
+ cachedPrivateKey = null;
487
+ return null;
488
+ }
489
+ /**
490
+ * Convert GraphQL input to internal submission format
491
+ */
492
+ convertInputToSubmission(input, ctx) {
493
+ const submission = {
494
+ title: input.Title,
495
+ description: input.Description,
496
+ category: input.Category,
497
+ stepsToReproduce: input.StepsToReproduce,
498
+ expectedBehavior: input.ExpectedBehavior,
499
+ actualBehavior: input.ActualBehavior,
500
+ severity: input.Severity,
501
+ useCase: input.UseCase,
502
+ proposedSolution: input.ProposedSolution,
503
+ email: input.Email,
504
+ name: input.Name,
505
+ environment: input.Environment,
506
+ affectedArea: input.AffectedArea,
507
+ currentPage: input.CurrentPage,
508
+ userAgent: input.UserAgent,
509
+ screenSize: input.ScreenSize,
510
+ appName: input.AppName,
511
+ appVersion: input.AppVersion,
512
+ userId: input.UserId || ctx.userPayload?.userRecord?.ID,
513
+ timestamp: input.Timestamp || new Date().toISOString(),
514
+ };
515
+ // Parse metadata if provided
516
+ if (input.Metadata) {
517
+ try {
518
+ submission.metadata = JSON.parse(input.Metadata);
519
+ }
520
+ catch {
521
+ // Ignore invalid JSON
522
+ }
523
+ }
524
+ return submission;
525
+ }
526
+ /**
527
+ * Get or create an authenticated Octokit instance.
528
+ * Cached at module level — createAppAuth handles token refresh internally.
529
+ */
530
+ getOctokit(config) {
531
+ if (cachedOctokit && cachedAuthType === config.auth.type) {
532
+ return cachedOctokit;
533
+ }
534
+ if (config.auth.type === 'app') {
535
+ cachedOctokit = new Octokit({
536
+ authStrategy: createAppAuth,
537
+ auth: {
538
+ appId: config.auth.appId,
539
+ privateKey: config.auth.privateKey,
540
+ installationId: config.auth.installationId,
541
+ },
542
+ });
543
+ }
544
+ else {
545
+ cachedOctokit = new Octokit({ auth: config.auth.token });
546
+ }
547
+ cachedAuthType = config.auth.type;
548
+ return cachedOctokit;
549
+ }
550
+ /**
551
+ * Create a GitHub issue from the feedback submission.
552
+ * If a screenshot is present, uploads it to the repo and embeds it in the issue.
553
+ */
554
+ async createGitHubIssue(submission, config) {
555
+ const octokit = this.getOctokit(config);
556
+ // Extract screenshot before formatting (formatScreenshotSection removes it from metadata)
557
+ const screenshotDataUrl = submission.metadata?.screenshot;
558
+ const labels = this.buildLabels(submission, config);
559
+ const body = this.formatIssueBody(submission);
560
+ // Create the issue first
561
+ const response = await octokit.issues.create({
562
+ owner: config.owner,
563
+ repo: config.repo,
564
+ title: submission.title,
565
+ body,
566
+ labels,
567
+ assignees: config.assignees,
568
+ });
569
+ const issueNumber = response.data.number;
570
+ // Upload screenshot and update issue body if present
571
+ if (screenshotDataUrl && typeof screenshotDataUrl === 'string') {
572
+ try {
573
+ const imageUrl = await this.uploadScreenshot(octokit, config.owner, config.repo, screenshotDataUrl, issueNumber);
574
+ if (imageUrl) {
575
+ const screenshotMarkdown = `\n## Screenshot\n\n![Screenshot](${imageUrl})\n`;
576
+ await octokit.issues.update({
577
+ owner: config.owner,
578
+ repo: config.repo,
579
+ issue_number: issueNumber,
580
+ body: body + screenshotMarkdown,
581
+ });
582
+ }
583
+ }
584
+ catch (error) {
585
+ // Screenshot upload is best-effort — don't fail the whole submission
586
+ LogError('Failed to upload feedback screenshot', undefined, error);
587
+ }
588
+ }
589
+ return {
590
+ number: issueNumber,
591
+ html_url: response.data.html_url,
592
+ };
593
+ }
594
+ /**
595
+ * Upload a base64 screenshot to the repo and return the raw content URL.
596
+ */
597
+ async uploadScreenshot(octokit, owner, repo, dataUrl, issueNumber) {
598
+ // Strip the data URL prefix (e.g., "data:image/jpeg;base64,")
599
+ const base64Match = dataUrl.match(/^data:image\/(\w+);base64,(.+)$/);
600
+ if (!base64Match)
601
+ return null;
602
+ const extension = base64Match[1] === 'jpeg' ? 'jpg' : base64Match[1];
603
+ const base64Content = base64Match[2];
604
+ const filename = `${issueNumber}-${Date.now()}.${extension}`;
605
+ const path = `.github/feedback-screenshots/${filename}`;
606
+ await octokit.repos.createOrUpdateFileContents({
607
+ owner,
608
+ repo,
609
+ path,
610
+ message: `feedback: add screenshot for issue #${issueNumber}`,
611
+ content: base64Content,
612
+ });
613
+ return `https://raw.githubusercontent.com/${owner}/${repo}/next/${path}`;
614
+ }
615
+ /**
616
+ * Build the labels array for the GitHub issue
617
+ */
618
+ buildLabels(submission, config) {
619
+ const labels = [];
620
+ if (config.defaultLabels) {
621
+ labels.push(...config.defaultLabels);
622
+ }
623
+ if (config.categoryLabels && submission.category in config.categoryLabels) {
624
+ labels.push(config.categoryLabels[submission.category]);
625
+ }
626
+ if (submission.severity && config.severityLabels && submission.severity in config.severityLabels) {
627
+ labels.push(config.severityLabels[submission.severity]);
628
+ }
629
+ return labels;
630
+ }
631
+ /**
632
+ * Format the feedback submission as a GitHub issue body in markdown.
633
+ * Orchestrates section-specific formatters and joins them.
634
+ */
635
+ formatIssueBody(submission) {
636
+ const sections = [
637
+ this.formatDescriptionSection(submission),
638
+ this.formatScreenshotSection(submission),
639
+ '---',
640
+ '',
641
+ this.formatDetailsTable(submission),
642
+ this.formatContactSection(submission),
643
+ this.formatTechnicalDetails(submission),
644
+ this.formatMetadataSection(submission),
645
+ ];
646
+ return sections.filter(s => s.length > 0).join('\n');
647
+ }
648
+ /**
649
+ * Format the description and category-specific sections (bug/feature)
650
+ */
651
+ formatDescriptionSection(submission) {
652
+ const lines = [];
653
+ lines.push('## Description', '', this.sanitizeMarkdown(submission.description), '');
654
+ if (submission.category === 'bug') {
655
+ if (submission.stepsToReproduce) {
656
+ lines.push('## Steps to Reproduce', '', this.sanitizeMarkdown(submission.stepsToReproduce), '');
657
+ }
658
+ if (submission.expectedBehavior) {
659
+ lines.push('## Expected Behavior', '', this.sanitizeMarkdown(submission.expectedBehavior), '');
660
+ }
661
+ if (submission.actualBehavior) {
662
+ lines.push('## Actual Behavior', '', this.sanitizeMarkdown(submission.actualBehavior), '');
663
+ }
664
+ }
665
+ if (submission.category === 'feature') {
666
+ if (submission.useCase) {
667
+ lines.push('## Use Case', '', this.sanitizeMarkdown(submission.useCase), '');
668
+ }
669
+ if (submission.proposedSolution) {
670
+ lines.push('## Proposed Solution', '', this.sanitizeMarkdown(submission.proposedSolution), '');
671
+ }
672
+ }
673
+ return lines.join('\n');
674
+ }
675
+ /**
676
+ * Format the submission details markdown table
677
+ */
678
+ formatDetailsTable(submission) {
679
+ const rows = [];
680
+ rows.push('### Submission Details', '', '| Field | Value |', '|-------|-------|');
681
+ if (submission.appName) {
682
+ const appInfo = submission.appVersion
683
+ ? `${this.sanitizeMarkdown(submission.appName)} v${this.sanitizeMarkdown(submission.appVersion)}`
684
+ : this.sanitizeMarkdown(submission.appName);
685
+ rows.push(`| **App** | ${appInfo} |`);
686
+ }
687
+ const categoryDisplay = categoryLabels[submission.category] || submission.category;
688
+ rows.push(`| **Category** | ${this.sanitizeMarkdown(categoryDisplay)} |`);
689
+ if (submission.severity) {
690
+ const severityDisplay = severityLabels[submission.severity] || submission.severity;
691
+ rows.push(`| **Severity** | ${this.sanitizeMarkdown(severityDisplay)} |`);
692
+ }
693
+ if (submission.environment) {
694
+ const envDisplay = environmentLabels[submission.environment] || submission.environment;
695
+ rows.push(`| **Environment** | ${this.sanitizeMarkdown(envDisplay)} |`);
696
+ }
697
+ if (submission.affectedArea) {
698
+ rows.push(`| **Affected Area** | ${this.sanitizeMarkdown(submission.affectedArea)} |`);
699
+ }
700
+ if (submission.timestamp) {
701
+ rows.push(`| **Submitted** | ${this.sanitizeMarkdown(submission.timestamp)} |`);
702
+ }
703
+ rows.push('');
704
+ return rows.join('\n');
705
+ }
706
+ /**
707
+ * Format the contact information section
708
+ */
709
+ formatContactSection(submission) {
710
+ if (!submission.name && !submission.email) {
711
+ return '';
712
+ }
713
+ const contactParts = [];
714
+ if (submission.name) {
715
+ contactParts.push(this.sanitizeMarkdown(submission.name));
716
+ }
717
+ if (submission.email) {
718
+ contactParts.push(this.sanitizeMarkdown(submission.email));
719
+ }
720
+ return ['### Contact', '', contactParts.join(' — '), ''].join('\n');
721
+ }
722
+ /**
723
+ * Format the technical details table (page, browser, screen, user)
724
+ */
725
+ formatTechnicalDetails(submission) {
726
+ if (!submission.currentPage && !submission.userAgent && !submission.screenSize && !submission.userId) {
727
+ return '';
728
+ }
729
+ const rows = [];
730
+ rows.push('### Technical Details', '', '| Field | Value |', '|-------|-------|');
731
+ if (submission.currentPage) {
732
+ rows.push(`| **Page/View** | ${this.sanitizeMarkdown(submission.currentPage)} |`);
733
+ }
734
+ if (submission.userAgent) {
735
+ const browserInfo = this.parseUserAgent(submission.userAgent);
736
+ rows.push(`| **Browser** | ${this.sanitizeMarkdown(browserInfo)} |`);
737
+ }
738
+ if (submission.screenSize) {
739
+ rows.push(`| **Screen Size** | ${this.sanitizeMarkdown(submission.screenSize)} |`);
740
+ }
741
+ if (submission.userId) {
742
+ rows.push(`| **User ID** | ${this.sanitizeMarkdown(submission.userId)} |`);
743
+ }
744
+ rows.push('');
745
+ return rows.join('\n');
746
+ }
747
+ /**
748
+ * Format the screenshot section. Embeds the base64 data in a collapsed block
749
+ * that AI agents can parse, even though GitHub won't render it visually.
750
+ * If the file upload succeeds (in createGitHubIssue), the issue body gets
751
+ * updated with a rendered image above this block.
752
+ */
753
+ formatScreenshotSection(submission) {
754
+ const screenshot = submission.metadata?.screenshot;
755
+ if (!screenshot || typeof screenshot !== 'string') {
756
+ return '';
757
+ }
758
+ // Remove from metadata so it doesn't also appear in the JSON section
759
+ delete submission.metadata.screenshot;
760
+ return [
761
+ '<details>',
762
+ '<summary>📷 Screenshot (for AI agents — not rendered by GitHub)</summary>',
763
+ '',
764
+ `![Screenshot](${screenshot})`,
765
+ '',
766
+ '</details>',
767
+ '',
768
+ ].join('\n');
769
+ }
770
+ /**
771
+ * Format the collapsible metadata JSON section
772
+ */
773
+ formatMetadataSection(submission) {
774
+ if (!submission.metadata || Object.keys(submission.metadata).length === 0) {
775
+ return '';
776
+ }
777
+ return [
778
+ '<details>',
779
+ '<summary>Additional Metadata</summary>',
780
+ '',
781
+ '```json',
782
+ JSON.stringify(submission.metadata, null, 2),
783
+ '```',
784
+ '</details>',
785
+ ].join('\n');
786
+ }
787
+ /**
788
+ * Sanitize a string for safe inclusion in markdown
789
+ */
790
+ sanitizeMarkdown(text) {
791
+ if (!text)
792
+ return '';
793
+ return text
794
+ .replace(/\\/g, '\\\\') // backslashes first (before other escapes add more)
795
+ .replace(/&/g, '&amp;')
796
+ .replace(/</g, '&lt;')
797
+ .replace(/>/g, '&gt;')
798
+ .replace(/`/g, '\\`') // inline code injection
799
+ .replace(/\[/g, '\\[')
800
+ .replace(/\]/g, '\\]')
801
+ .replace(/!\[/g, '!\\[')
802
+ .replace(/\(/g, '\\(') // link URL injection
803
+ .replace(/\)/g, '\\)')
804
+ .replace(/\|/g, '\\|');
805
+ }
806
+ /**
807
+ * Parse user agent string into a human-readable format
808
+ */
809
+ parseUserAgent(userAgent) {
810
+ let browser = 'Unknown Browser';
811
+ let os = 'Unknown OS';
812
+ if (userAgent.includes('Firefox/')) {
813
+ const match = userAgent.match(/Firefox\/(\d+)/);
814
+ browser = match ? `Firefox ${match[1]}` : 'Firefox';
815
+ }
816
+ else if (userAgent.includes('Edg/')) {
817
+ const match = userAgent.match(/Edg\/(\d+)/);
818
+ browser = match ? `Edge ${match[1]}` : 'Edge';
819
+ }
820
+ else if (userAgent.includes('Chrome/')) {
821
+ const match = userAgent.match(/Chrome\/(\d+)/);
822
+ browser = match ? `Chrome ${match[1]}` : 'Chrome';
823
+ }
824
+ else if (userAgent.includes('Safari/') && !userAgent.includes('Chrome')) {
825
+ const match = userAgent.match(/Version\/(\d+)/);
826
+ browser = match ? `Safari ${match[1]}` : 'Safari';
827
+ }
828
+ if (userAgent.includes('Windows NT 10')) {
829
+ os = 'Windows 10/11';
830
+ }
831
+ else if (userAgent.includes('Windows NT')) {
832
+ os = 'Windows';
833
+ }
834
+ else if (userAgent.includes('Mac OS X')) {
835
+ const match = userAgent.match(/Mac OS X (\d+[._]\d+)/);
836
+ os = match ? `macOS ${match[1].replace('_', '.')}` : 'macOS';
837
+ }
838
+ else if (userAgent.includes('Linux')) {
839
+ os = 'Linux';
840
+ }
841
+ else if (userAgent.includes('Android')) {
842
+ os = 'Android';
843
+ }
844
+ else if (userAgent.includes('iOS') || userAgent.includes('iPhone') || userAgent.includes('iPad')) {
845
+ os = 'iOS';
846
+ }
847
+ return `${browser} on ${os}`;
848
+ }
849
+ };
850
+ __decorate([
851
+ Query(() => Boolean),
852
+ __metadata("design:type", Function),
853
+ __metadata("design:paramtypes", []),
854
+ __metadata("design:returntype", Boolean)
855
+ ], FeedbackResolver.prototype, "FeedbackEnabled", null);
856
+ __decorate([
857
+ Mutation(() => FeedbackResponseType),
858
+ __param(0, Arg('input')),
859
+ __param(1, Ctx()),
860
+ __metadata("design:type", Function),
861
+ __metadata("design:paramtypes", [SubmitFeedbackInput, Object]),
862
+ __metadata("design:returntype", Promise)
863
+ ], FeedbackResolver.prototype, "SubmitFeedback", null);
864
+ __decorate([
865
+ Mutation(() => FeedbackClassificationResult),
866
+ __param(0, Arg('input')),
867
+ __param(1, Ctx()),
868
+ __metadata("design:type", Function),
869
+ __metadata("design:paramtypes", [ClassifyFeedbackInput, Object]),
870
+ __metadata("design:returntype", Promise)
871
+ ], FeedbackResolver.prototype, "ClassifyFeedback", null);
872
+ FeedbackResolver = __decorate([
873
+ Resolver()
874
+ ], FeedbackResolver);
875
+ export { FeedbackResolver };
876
+ //# sourceMappingURL=FeedbackResolver.js.map