@quantiya/codevibe-gemini-plugin 1.0.5 → 1.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 (54) hide show
  1. package/README.md +57 -80
  2. package/dist/server.js +13 -1424
  3. package/package.json +10 -12
  4. package/dist/appsync-client.d.ts +0 -66
  5. package/dist/appsync-client.d.ts.map +0 -1
  6. package/dist/appsync-client.js +0 -819
  7. package/dist/appsync-client.js.map +0 -1
  8. package/dist/auth-cli.d.ts +0 -18
  9. package/dist/auth-cli.d.ts.map +0 -1
  10. package/dist/auth-cli.js +0 -472
  11. package/dist/auth-cli.js.map +0 -1
  12. package/dist/command-executor.d.ts +0 -20
  13. package/dist/command-executor.d.ts.map +0 -1
  14. package/dist/command-executor.js +0 -127
  15. package/dist/command-executor.js.map +0 -1
  16. package/dist/config.d.ts +0 -25
  17. package/dist/config.d.ts.map +0 -1
  18. package/dist/config.js +0 -106
  19. package/dist/config.js.map +0 -1
  20. package/dist/crypto-service.d.ts +0 -115
  21. package/dist/crypto-service.d.ts.map +0 -1
  22. package/dist/crypto-service.js +0 -278
  23. package/dist/crypto-service.js.map +0 -1
  24. package/dist/http-api.d.ts +0 -63
  25. package/dist/http-api.d.ts.map +0 -1
  26. package/dist/http-api.js +0 -582
  27. package/dist/http-api.js.map +0 -1
  28. package/dist/key-manager.d.ts +0 -87
  29. package/dist/key-manager.d.ts.map +0 -1
  30. package/dist/key-manager.js +0 -287
  31. package/dist/key-manager.js.map +0 -1
  32. package/dist/logger.d.ts +0 -2
  33. package/dist/logger.d.ts.map +0 -1
  34. package/dist/logger.js +0 -18
  35. package/dist/logger.js.map +0 -1
  36. package/dist/prompt-responder.d.ts +0 -22
  37. package/dist/prompt-responder.d.ts.map +0 -1
  38. package/dist/prompt-responder.js +0 -132
  39. package/dist/prompt-responder.js.map +0 -1
  40. package/dist/server.d.ts +0 -9
  41. package/dist/server.d.ts.map +0 -1
  42. package/dist/server.js.map +0 -1
  43. package/dist/token-storage.d.ts +0 -39
  44. package/dist/token-storage.d.ts.map +0 -1
  45. package/dist/token-storage.js +0 -169
  46. package/dist/token-storage.js.map +0 -1
  47. package/dist/transcript-watcher.d.ts +0 -111
  48. package/dist/transcript-watcher.d.ts.map +0 -1
  49. package/dist/transcript-watcher.js +0 -324
  50. package/dist/transcript-watcher.js.map +0 -1
  51. package/dist/types.d.ts +0 -119
  52. package/dist/types.d.ts.map +0 -1
  53. package/dist/types.js +0 -16
  54. package/dist/types.js.map +0 -1
@@ -1,819 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.AppSyncClient = void 0;
7
- const ws_1 = __importDefault(require("ws"));
8
- const uuid_1 = require("uuid");
9
- const config_1 = require("./config");
10
- const logger_1 = require("./logger");
11
- const token_storage_1 = require("./token-storage");
12
- const types_1 = require("./types");
13
- // GraphQL queries and mutations
14
- const createSessionMutation = /* GraphQL */ `
15
- mutation CreateSession($input: CreateSessionInput!) {
16
- createSession(input: $input) {
17
- sessionId
18
- userId
19
- agentType
20
- projectPath
21
- status
22
- createdAt
23
- updatedAt
24
- }
25
- }
26
- `;
27
- const updateSessionMutation = /* GraphQL */ `
28
- mutation UpdateSession($input: UpdateSessionInput!) {
29
- updateSession(input: $input) {
30
- sessionId
31
- status
32
- updatedAt
33
- }
34
- }
35
- `;
36
- const createEventMutation = /* GraphQL */ `
37
- mutation CreateEvent($input: CreateEventInput!) {
38
- createEvent(input: $input) {
39
- eventId
40
- sessionId
41
- type
42
- source
43
- content
44
- timestamp
45
- metadata
46
- promptId
47
- deliveryStatus
48
- deliveredAt
49
- executedAt
50
- }
51
- }
52
- `;
53
- const updateEventStatusMutation = /* GraphQL */ `
54
- mutation UpdateEventStatus($input: UpdateEventStatusInput!) {
55
- updateEventStatus(input: $input) {
56
- eventId
57
- sessionId
58
- type
59
- source
60
- content
61
- timestamp
62
- metadata
63
- promptId
64
- deliveryStatus
65
- deliveredAt
66
- executedAt
67
- }
68
- }
69
- `;
70
- const createFileChangeMutation = /* GraphQL */ `
71
- mutation CreateFileChange($input: CreateFileChangeInput!) {
72
- createFileChange(input: $input) {
73
- changeId
74
- sessionId
75
- filePath
76
- action
77
- diff
78
- timestamp
79
- }
80
- }
81
- `;
82
- const getSessionQuery = /* GraphQL */ `
83
- query GetSession($sessionId: ID!) {
84
- getSession(sessionId: $sessionId) {
85
- sessionId
86
- userId
87
- projectPath
88
- status
89
- createdAt
90
- updatedAt
91
- metadata
92
- }
93
- }
94
- `;
95
- const listEventsQuery = /* GraphQL */ `
96
- query ListEvents($sessionId: ID!, $source: EventSource) {
97
- listEvents(sessionId: $sessionId, source: $source) {
98
- items {
99
- eventId
100
- sessionId
101
- type
102
- source
103
- content
104
- timestamp
105
- metadata
106
- promptId
107
- }
108
- nextToken
109
- }
110
- }
111
- `;
112
- const onEventCreatedSubscription = /* GraphQL */ `
113
- subscription OnEventCreated($sessionId: ID!) {
114
- onEventCreated(sessionId: $sessionId) {
115
- eventId
116
- sessionId
117
- type
118
- source
119
- content
120
- timestamp
121
- metadata
122
- promptId
123
- attachments {
124
- id
125
- type
126
- filename
127
- s3Key
128
- size
129
- width
130
- height
131
- isEncrypted
132
- }
133
- deliveryStatus
134
- deliveredAt
135
- executedAt
136
- isEncrypted
137
- }
138
- }
139
- `;
140
- const getAttachmentDownloadUrlMutation = /* GraphQL */ `
141
- mutation GetAttachmentDownloadUrl($s3Key: String!) {
142
- getAttachmentDownloadUrl(s3Key: $s3Key) {
143
- downloadUrl
144
- expiresAt
145
- }
146
- }
147
- `;
148
- const listUserDeviceKeysQuery = /* GraphQL */ `
149
- query ListUserDeviceKeys {
150
- listUserDeviceKeys {
151
- userId
152
- deviceId
153
- publicKey
154
- platform
155
- deviceName
156
- createdAt
157
- lastUsedAt
158
- }
159
- }
160
- `;
161
- // Reconnection configuration
162
- const RECONNECT_CONFIG = {
163
- maxAttempts: 10,
164
- baseDelayMs: 1000, // Start with 1 second
165
- maxDelayMs: 60000, // Max 1 minute between attempts
166
- backoffMultiplier: 2, // Double delay each attempt
167
- };
168
- class AppSyncClient {
169
- constructor() {
170
- this.authenticated = false;
171
- this.currentUserId = null;
172
- this.currentEmail = null;
173
- this.storedTokens = null;
174
- this.activeSubscriptions = new Map();
175
- logger_1.logger.info('AppSync client initialized (Cognito User Pool auth via OAuth)');
176
- }
177
- // Get the current authenticated user ID
178
- getCurrentUserId() {
179
- return this.currentUserId || 'default-user';
180
- }
181
- // Get the current authenticated user email
182
- getCurrentUserEmail() {
183
- return this.currentEmail;
184
- }
185
- /**
186
- * Authenticate using stored OAuth tokens from 'codevibe-gemini login'
187
- * Returns true if successfully authenticated, false otherwise
188
- */
189
- async authenticateWithStoredTokens() {
190
- try {
191
- // Load tokens from storage
192
- const tokens = (0, token_storage_1.loadTokens)();
193
- if (!tokens) {
194
- logger_1.logger.debug('No stored tokens found');
195
- return false;
196
- }
197
- logger_1.logger.info('Found stored OAuth tokens', {
198
- userId: tokens.userId,
199
- email: tokens.email,
200
- expired: (0, token_storage_1.isTokenExpired)(tokens),
201
- });
202
- // If tokens are expired, try to refresh them
203
- if ((0, token_storage_1.isTokenExpired)(tokens)) {
204
- logger_1.logger.info('Stored tokens are expired, attempting refresh...');
205
- const refreshed = await this.refreshStoredTokens(tokens);
206
- if (!refreshed) {
207
- logger_1.logger.warn('Token refresh failed - user needs to re-authenticate');
208
- return false;
209
- }
210
- }
211
- // Use the stored tokens
212
- this.storedTokens = tokens;
213
- this.currentUserId = tokens.userId;
214
- this.currentEmail = tokens.email;
215
- this.authenticated = true;
216
- logger_1.logger.info('Authenticated with stored OAuth tokens', {
217
- userId: this.currentUserId,
218
- email: this.currentEmail,
219
- });
220
- return true;
221
- }
222
- catch (error) {
223
- logger_1.logger.error('Failed to authenticate with stored tokens:', error);
224
- return false;
225
- }
226
- }
227
- /**
228
- * Refresh expired tokens using the refresh token
229
- */
230
- async refreshStoredTokens(tokens) {
231
- try {
232
- const tokenUrl = `https://${config_1.config.aws.cognitoDomain}/oauth2/token`;
233
- const params = new URLSearchParams({
234
- grant_type: 'refresh_token',
235
- client_id: config_1.config.aws.cognitoClientId,
236
- refresh_token: tokens.refreshToken,
237
- });
238
- const response = await fetch(tokenUrl, {
239
- method: 'POST',
240
- headers: {
241
- 'Content-Type': 'application/x-www-form-urlencoded',
242
- },
243
- body: params.toString(),
244
- });
245
- if (!response.ok) {
246
- const errorText = await response.text();
247
- logger_1.logger.error('Token refresh failed:', { status: response.status, error: errorText });
248
- return false;
249
- }
250
- const data = await response.json();
251
- // Update stored tokens
252
- const updatedTokens = {
253
- ...tokens,
254
- accessToken: data.access_token,
255
- idToken: data.id_token,
256
- expiresAt: Date.now() + (data.expires_in * 1000),
257
- };
258
- (0, token_storage_1.saveTokens)(updatedTokens);
259
- this.storedTokens = updatedTokens;
260
- logger_1.logger.info('Tokens refreshed successfully', {
261
- expiresAt: new Date(updatedTokens.expiresAt).toISOString(),
262
- });
263
- return true;
264
- }
265
- catch (error) {
266
- logger_1.logger.error('Token refresh error:', error);
267
- return false;
268
- }
269
- }
270
- /**
271
- * Check if stored tokens exist and are valid
272
- */
273
- hasValidStoredTokens() {
274
- const tokens = (0, token_storage_1.loadTokens)();
275
- return tokens !== null && !(0, token_storage_1.isTokenExpired)(tokens);
276
- }
277
- // Sign out - clears local state (token removal handled by auth-cli)
278
- signOutUser() {
279
- this.authenticated = false;
280
- this.storedTokens = null;
281
- this.currentUserId = null;
282
- this.currentEmail = null;
283
- logger_1.logger.info('Signed out successfully');
284
- }
285
- /**
286
- * Make a direct GraphQL request to AppSync using fetch
287
- * Uses stored OAuth tokens for Cognito User Pool authentication
288
- * Automatically refreshes token and retries on 401 errors
289
- */
290
- async graphqlRequest(query, variables, isRetry = false) {
291
- const headers = {
292
- 'Content-Type': 'application/json',
293
- };
294
- // Add authorization header using stored OAuth tokens
295
- if (this.storedTokens?.idToken) {
296
- // Cognito User Pool expects raw JWT in Authorization header (no Bearer prefix)
297
- headers['Authorization'] = this.storedTokens.idToken;
298
- }
299
- else {
300
- throw new Error('No valid auth tokens available. Run "codevibe-gemini login" first.');
301
- }
302
- logger_1.logger.debug('Making GraphQL request', {
303
- hasAuth: !!headers['Authorization'],
304
- isRetry,
305
- });
306
- const response = await fetch(config_1.config.aws.appsyncUrl, {
307
- method: 'POST',
308
- headers,
309
- body: JSON.stringify({ query, variables }),
310
- });
311
- const result = await response.json();
312
- // Check for 401 Unauthorized - try to refresh token and retry once
313
- if (response.status === 401 && !isRetry && this.storedTokens) {
314
- logger_1.logger.info('Received 401 Unauthorized, attempting token refresh...');
315
- const refreshed = await this.refreshStoredTokens(this.storedTokens);
316
- if (refreshed) {
317
- logger_1.logger.info('Token refreshed successfully, retrying request...');
318
- return this.graphqlRequest(query, variables, true);
319
- }
320
- else {
321
- logger_1.logger.error('Token refresh failed - user needs to re-authenticate with "codevibe-gemini login"');
322
- throw new Error('Token expired and refresh failed. Run "codevibe-gemini login" to re-authenticate.');
323
- }
324
- }
325
- if (!response.ok) {
326
- logger_1.logger.error('GraphQL request failed', { status: response.status, errors: result.errors });
327
- throw new Error(`GraphQL request failed: ${response.status}`);
328
- }
329
- if (result.errors && result.errors.length > 0) {
330
- logger_1.logger.error('GraphQL errors', { errors: result.errors });
331
- throw new Error(`GraphQL error: ${result.errors[0].message}`);
332
- }
333
- return result;
334
- }
335
- // Create a new session
336
- async createSession(input) {
337
- try {
338
- logger_1.logger.info('Creating session with input', {
339
- sessionId: input.sessionId,
340
- userId: input.userId,
341
- agentType: input.agentType,
342
- projectPath: input.projectPath,
343
- status: input.status,
344
- });
345
- // Stringify metadata for AWSJSON type
346
- const preparedInput = {
347
- ...input,
348
- metadata: input.metadata ? JSON.stringify(input.metadata) : undefined,
349
- };
350
- logger_1.logger.info('Prepared input for GraphQL', { preparedInput });
351
- const response = await this.graphqlRequest(createSessionMutation, { input: preparedInput });
352
- const session = response.data.createSession;
353
- logger_1.logger.info('Session created', { sessionId: session.sessionId });
354
- return session;
355
- }
356
- catch (error) {
357
- logger_1.logger.error('Failed to create session:', error);
358
- throw error;
359
- }
360
- }
361
- // Update an existing session
362
- async updateSession(input) {
363
- try {
364
- logger_1.logger.debug('Updating session', input);
365
- // Stringify metadata for AWSJSON type
366
- const preparedInput = {
367
- ...input,
368
- metadata: input.metadata ? JSON.stringify(input.metadata) : undefined,
369
- };
370
- const response = await this.graphqlRequest(updateSessionMutation, { input: preparedInput });
371
- const session = response.data.updateSession;
372
- logger_1.logger.info('Session updated', { sessionId: session.sessionId });
373
- return session;
374
- }
375
- catch (error) {
376
- logger_1.logger.error('Failed to update session:', error);
377
- throw error;
378
- }
379
- }
380
- // Create an event
381
- async createEvent(input) {
382
- try {
383
- logger_1.logger.debug('Creating event', {
384
- sessionId: input.sessionId,
385
- type: input.type,
386
- source: input.source,
387
- });
388
- // Stringify metadata for AWSJSON type
389
- const preparedInput = {
390
- ...input,
391
- metadata: input.metadata ? JSON.stringify(input.metadata) : undefined,
392
- };
393
- const response = await this.graphqlRequest(createEventMutation, { input: preparedInput });
394
- const event = response.data.createEvent;
395
- logger_1.logger.info('Event created', {
396
- eventId: event.eventId,
397
- sessionId: event.sessionId,
398
- type: event.type,
399
- });
400
- return event;
401
- }
402
- catch (error) {
403
- logger_1.logger.error('Failed to create event:', error);
404
- throw error;
405
- }
406
- }
407
- // Update event delivery status (for double checkmark feature)
408
- async updateEventStatus(input) {
409
- try {
410
- logger_1.logger.debug('Updating event status', {
411
- eventId: input.eventId,
412
- sessionId: input.sessionId,
413
- deliveryStatus: input.deliveryStatus,
414
- });
415
- const response = await this.graphqlRequest(updateEventStatusMutation, { input });
416
- const event = response.data.updateEventStatus;
417
- logger_1.logger.info('Event status updated', {
418
- eventId: event.eventId,
419
- deliveryStatus: event.deliveryStatus,
420
- });
421
- return event;
422
- }
423
- catch (error) {
424
- logger_1.logger.error('Failed to update event status:', error);
425
- throw error;
426
- }
427
- }
428
- // Create a file change
429
- async createFileChange(input) {
430
- try {
431
- logger_1.logger.debug('Creating file change', {
432
- sessionId: input.sessionId,
433
- filePath: input.filePath,
434
- action: input.action,
435
- });
436
- // Stringify metadata for AWSJSON type
437
- const preparedInput = {
438
- ...input,
439
- metadata: input.metadata ? JSON.stringify(input.metadata) : undefined,
440
- };
441
- const response = await this.graphqlRequest(createFileChangeMutation, { input: preparedInput });
442
- const fileChange = response.data.createFileChange;
443
- logger_1.logger.info('File change created', {
444
- changeId: fileChange.changeId,
445
- filePath: fileChange.filePath,
446
- });
447
- return fileChange;
448
- }
449
- catch (error) {
450
- logger_1.logger.error('Failed to create file change:', error);
451
- throw error;
452
- }
453
- }
454
- // Get a session
455
- async getSession(sessionId) {
456
- try {
457
- logger_1.logger.debug('Getting session', { sessionId });
458
- const response = await this.graphqlRequest(getSessionQuery, { sessionId });
459
- return response.data.getSession;
460
- }
461
- catch (error) {
462
- logger_1.logger.error('Failed to get session:', error);
463
- throw error;
464
- }
465
- }
466
- // List events for a session
467
- async listEvents(sessionId, source) {
468
- try {
469
- logger_1.logger.debug('Listing events', { sessionId, source });
470
- const response = await this.graphqlRequest(listEventsQuery, { sessionId, source });
471
- return response.data.listEvents.items;
472
- }
473
- catch (error) {
474
- logger_1.logger.error('Failed to list events:', error);
475
- throw error;
476
- }
477
- }
478
- // Subscribe to events for a session with automatic reconnection
479
- subscribeToEvents(sessionId, onEvent, onError) {
480
- logger_1.logger.info('Subscribing to events', { sessionId });
481
- // Clean up existing subscription for this sessionId if any
482
- const existingState = this.activeSubscriptions.get(sessionId);
483
- if (existingState) {
484
- logger_1.logger.info('Cleaning up existing subscription before creating new one', { sessionId });
485
- this.cleanupSubscriptionState(existingState);
486
- this.activeSubscriptions.delete(sessionId);
487
- }
488
- // Create subscription state
489
- const state = {
490
- ws: null,
491
- subscriptionId: (0, uuid_1.v4)(),
492
- sessionId,
493
- onEvent,
494
- onError,
495
- reconnectAttempts: 0,
496
- isReconnecting: false,
497
- };
498
- // Store state before creating subscription
499
- this.activeSubscriptions.set(sessionId, state);
500
- // Create the actual subscription
501
- this.createSubscription(state);
502
- // Return unsubscribe function
503
- return () => {
504
- logger_1.logger.info('Unsubscribing from events', { sessionId });
505
- this.cleanupSubscriptionState(state);
506
- this.activeSubscriptions.delete(sessionId);
507
- };
508
- }
509
- /**
510
- * Build the AppSync real-time WebSocket URL with authorization
511
- */
512
- buildRealtimeUrl() {
513
- // Convert AppSync URL to real-time URL
514
- // https://xxx.appsync-api.region.amazonaws.com/graphql -> wss://xxx.appsync-realtime-api.region.amazonaws.com/graphql
515
- const realtimeUrl = config_1.config.aws.appsyncUrl
516
- .replace('https://', 'wss://')
517
- .replace('appsync-api', 'appsync-realtime-api');
518
- // Build authorization header using stored OAuth token
519
- const authHeader = {
520
- host: new URL(config_1.config.aws.appsyncUrl).host,
521
- };
522
- if (this.storedTokens?.idToken) {
523
- authHeader['Authorization'] = this.storedTokens.idToken;
524
- }
525
- // Encode headers as base64 for URL
526
- const headerBase64 = Buffer.from(JSON.stringify(authHeader)).toString('base64');
527
- const payloadBase64 = Buffer.from(JSON.stringify({})).toString('base64');
528
- return `${realtimeUrl}?header=${headerBase64}&payload=${payloadBase64}`;
529
- }
530
- /**
531
- * Create a custom WebSocket subscription to AppSync
532
- * This bypasses Amplify which doesn't work with externally stored tokens
533
- */
534
- createSubscription(state) {
535
- const { sessionId, subscriptionId, onEvent, onError } = state;
536
- try {
537
- const wsUrl = this.buildRealtimeUrl();
538
- logger_1.logger.info('Creating WebSocket subscription', { sessionId, subscriptionId });
539
- const ws = new ws_1.default(wsUrl, ['graphql-ws']);
540
- ws.on('open', () => {
541
- logger_1.logger.info('WebSocket connected, sending connection_init', { sessionId });
542
- // Send connection_init
543
- ws.send(JSON.stringify({
544
- type: 'connection_init',
545
- }));
546
- });
547
- ws.on('message', (data) => {
548
- try {
549
- const message = JSON.parse(data.toString());
550
- switch (message.type) {
551
- case 'connection_ack':
552
- logger_1.logger.info('WebSocket connection acknowledged', {
553
- sessionId,
554
- connectionTimeout: message.payload?.connectionTimeoutMs
555
- });
556
- // Start the subscription
557
- this.sendSubscriptionStart(ws, state);
558
- break;
559
- case 'start_ack':
560
- logger_1.logger.info('Subscription started successfully', { sessionId, subscriptionId });
561
- state.isReconnecting = false;
562
- if (state.reconnectAttempts > 0) {
563
- logger_1.logger.info('Subscription reconnected successfully', {
564
- sessionId,
565
- afterAttempts: state.reconnectAttempts
566
- });
567
- state.reconnectAttempts = 0;
568
- }
569
- break;
570
- case 'data':
571
- // Reset keep-alive timer on data
572
- this.resetKeepAliveTimer(state);
573
- const event = message.payload?.data?.onEventCreated;
574
- if (event) {
575
- logger_1.logger.debug('Event received from subscription', {
576
- eventId: event.eventId,
577
- type: event.type,
578
- source: event.source,
579
- });
580
- // Filter out desktop events (we only want mobile events)
581
- if (event.source === types_1.EventSource.MOBILE) {
582
- onEvent(event);
583
- }
584
- }
585
- break;
586
- case 'ka':
587
- // Keep-alive message - reset timer
588
- this.resetKeepAliveTimer(state);
589
- logger_1.logger.debug('Keep-alive received', { sessionId });
590
- break;
591
- case 'error':
592
- logger_1.logger.error('Subscription error from server', {
593
- sessionId,
594
- errors: message.payload?.errors
595
- });
596
- const errorMsg = message.payload?.errors?.[0]?.message || 'Unknown subscription error';
597
- this.handleSubscriptionError(state, new Error(errorMsg));
598
- break;
599
- case 'complete':
600
- logger_1.logger.info('Subscription completed by server', { sessionId });
601
- break;
602
- default:
603
- logger_1.logger.debug('Unknown WebSocket message type', { type: message.type, sessionId });
604
- }
605
- }
606
- catch (parseError) {
607
- logger_1.logger.error('Failed to parse WebSocket message', { error: parseError, data: data.toString() });
608
- }
609
- });
610
- ws.on('error', (error) => {
611
- logger_1.logger.error('WebSocket error:', { sessionId, error: error.message });
612
- this.handleSubscriptionError(state, error);
613
- });
614
- ws.on('close', (code, reason) => {
615
- logger_1.logger.info('WebSocket closed', {
616
- sessionId,
617
- code,
618
- reason: reason.toString()
619
- });
620
- // Clear keep-alive timer
621
- if (state.keepAliveTimer) {
622
- clearTimeout(state.keepAliveTimer);
623
- state.keepAliveTimer = undefined;
624
- }
625
- // Attempt reconnection if not intentionally closed
626
- if (code !== 1000 && this.activeSubscriptions.has(sessionId)) {
627
- this.handleSubscriptionError(state, new Error(`WebSocket closed: ${code} ${reason.toString()}`));
628
- }
629
- });
630
- state.ws = ws;
631
- // Set initial keep-alive timer (AppSync sends ka every ~240 seconds)
632
- this.resetKeepAliveTimer(state);
633
- }
634
- catch (error) {
635
- logger_1.logger.error('Failed to create WebSocket subscription:', { sessionId, error });
636
- this.handleSubscriptionError(state, error);
637
- }
638
- }
639
- /**
640
- * Send subscription start message
641
- */
642
- sendSubscriptionStart(ws, state) {
643
- const { sessionId, subscriptionId } = state;
644
- // Build authorization for the subscription using stored OAuth token
645
- const authHeader = {
646
- host: new URL(config_1.config.aws.appsyncUrl).host,
647
- };
648
- if (this.storedTokens?.idToken) {
649
- authHeader['Authorization'] = this.storedTokens.idToken;
650
- }
651
- const startMessage = {
652
- id: subscriptionId,
653
- type: 'start',
654
- payload: {
655
- data: JSON.stringify({
656
- query: onEventCreatedSubscription,
657
- variables: { sessionId },
658
- }),
659
- extensions: {
660
- authorization: authHeader,
661
- },
662
- },
663
- };
664
- logger_1.logger.debug('Sending subscription start', { sessionId, subscriptionId });
665
- ws.send(JSON.stringify(startMessage));
666
- }
667
- /**
668
- * Reset the keep-alive timer - if no message received in 5 minutes, reconnect
669
- */
670
- resetKeepAliveTimer(state) {
671
- if (state.keepAliveTimer) {
672
- clearTimeout(state.keepAliveTimer);
673
- }
674
- // AppSync sends keep-alive every ~240 seconds, timeout after 5 minutes
675
- state.keepAliveTimer = setTimeout(() => {
676
- logger_1.logger.warn('Keep-alive timeout, reconnecting subscription', { sessionId: state.sessionId });
677
- this.handleSubscriptionError(state, new Error('Keep-alive timeout'));
678
- }, 5 * 60 * 1000);
679
- }
680
- // Handle subscription errors with reconnection logic
681
- handleSubscriptionError(state, error) {
682
- const { sessionId, onError } = state;
683
- // Don't reconnect if we're already trying or if session was removed
684
- if (state.isReconnecting || !this.activeSubscriptions.has(sessionId)) {
685
- return;
686
- }
687
- // Check if we've exceeded max attempts
688
- if (state.reconnectAttempts >= RECONNECT_CONFIG.maxAttempts) {
689
- logger_1.logger.error('Max reconnection attempts reached, giving up', {
690
- sessionId,
691
- attempts: state.reconnectAttempts
692
- });
693
- if (onError) {
694
- onError(new Error(`Subscription failed after ${state.reconnectAttempts} reconnection attempts: ${error.message}`));
695
- }
696
- return;
697
- }
698
- state.isReconnecting = true;
699
- state.reconnectAttempts++;
700
- // Calculate delay with exponential backoff
701
- const delay = Math.min(RECONNECT_CONFIG.baseDelayMs * Math.pow(RECONNECT_CONFIG.backoffMultiplier, state.reconnectAttempts - 1), RECONNECT_CONFIG.maxDelayMs);
702
- logger_1.logger.info('Scheduling subscription reconnection', {
703
- sessionId,
704
- attempt: state.reconnectAttempts,
705
- maxAttempts: RECONNECT_CONFIG.maxAttempts,
706
- delayMs: delay
707
- });
708
- // Clean up old WebSocket if it exists
709
- if (state.ws) {
710
- try {
711
- state.ws.close(1000, 'Reconnecting');
712
- }
713
- catch (e) {
714
- // Ignore errors when closing failed WebSocket
715
- }
716
- state.ws = null;
717
- }
718
- // Clear keep-alive timer
719
- if (state.keepAliveTimer) {
720
- clearTimeout(state.keepAliveTimer);
721
- state.keepAliveTimer = undefined;
722
- }
723
- // Schedule reconnection
724
- state.reconnectTimer = setTimeout(() => {
725
- // Verify session still exists before reconnecting
726
- if (this.activeSubscriptions.has(sessionId)) {
727
- logger_1.logger.info('Attempting subscription reconnection', {
728
- sessionId,
729
- attempt: state.reconnectAttempts
730
- });
731
- // Generate new subscription ID for reconnection
732
- state.subscriptionId = (0, uuid_1.v4)();
733
- this.createSubscription(state);
734
- }
735
- }, delay);
736
- }
737
- // Clean up subscription state
738
- cleanupSubscriptionState(state) {
739
- // Clear any pending reconnection timer
740
- if (state.reconnectTimer) {
741
- clearTimeout(state.reconnectTimer);
742
- state.reconnectTimer = undefined;
743
- }
744
- // Clear keep-alive timer
745
- if (state.keepAliveTimer) {
746
- clearTimeout(state.keepAliveTimer);
747
- state.keepAliveTimer = undefined;
748
- }
749
- // Close WebSocket if active
750
- if (state.ws) {
751
- try {
752
- // Send stop message for the subscription before closing
753
- if (state.ws.readyState === ws_1.default.OPEN) {
754
- state.ws.send(JSON.stringify({
755
- id: state.subscriptionId,
756
- type: 'stop',
757
- }));
758
- state.ws.close(1000, 'Unsubscribing');
759
- }
760
- }
761
- catch (e) {
762
- // Ignore errors
763
- }
764
- state.ws = null;
765
- }
766
- }
767
- // Cleanup all subscriptions
768
- cleanupSubscriptions() {
769
- logger_1.logger.info('Cleaning up all subscriptions', {
770
- count: this.activeSubscriptions.size,
771
- });
772
- this.activeSubscriptions.forEach((state, sessionId) => {
773
- logger_1.logger.debug('Unsubscribing from session', { sessionId });
774
- this.cleanupSubscriptionState(state);
775
- });
776
- this.activeSubscriptions.clear();
777
- }
778
- // Get pre-signed download URL for an attachment
779
- async getAttachmentDownloadUrl(s3Key) {
780
- try {
781
- logger_1.logger.debug('Getting attachment download URL', { s3Key });
782
- const response = await this.graphqlRequest(getAttachmentDownloadUrlMutation, { s3Key });
783
- const result = response.data.getAttachmentDownloadUrl;
784
- logger_1.logger.info('Got attachment download URL', {
785
- s3Key,
786
- expiresAt: result.expiresAt,
787
- });
788
- return result;
789
- }
790
- catch (error) {
791
- logger_1.logger.error('Failed to get attachment download URL:', error);
792
- throw error;
793
- }
794
- }
795
- // List all device keys for the current user (for E2E encryption)
796
- async listUserDeviceKeys() {
797
- try {
798
- logger_1.logger.debug('Listing user device keys');
799
- const response = await this.graphqlRequest(listUserDeviceKeysQuery, {});
800
- // listUserDeviceKeys returns [DeviceKey!]! directly, not a connection type
801
- const items = response.data.listUserDeviceKeys || [];
802
- logger_1.logger.info('Listed user device keys', { count: items.length });
803
- return items.map((item) => ({
804
- deviceId: item.deviceId,
805
- publicKey: item.publicKey,
806
- }));
807
- }
808
- catch (error) {
809
- logger_1.logger.error('Failed to list user device keys:', error);
810
- throw error;
811
- }
812
- }
813
- // Check if authenticated
814
- isAuthenticated() {
815
- return this.authenticated;
816
- }
817
- }
818
- exports.AppSyncClient = AppSyncClient;
819
- //# sourceMappingURL=appsync-client.js.map