@quantiya/codevibe-codex-plugin 1.0.7 → 1.0.9

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