@quantiya/codevibe-claude-plugin 1.0.11 → 1.0.12

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