aws-appsync-subscription-link 3.1.2 → 3.1.4

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ <a name="3.1.4"></a>
7
+ ## [3.1.4](https://github.com/awslabs/aws-mobile-appsync-sdk-js/compare/aws-appsync-subscription-link@3.1.3...aws-appsync-subscription-link@3.1.4) (2026-03-24)
8
+
9
+
10
+ ### Bug Fixes
11
+
12
+ * **subscription-link:** move auth credentials from URL query string to Sec-WebSocket-Protocol header ([748b45a](https://github.com/awslabs/aws-mobile-appsync-sdk-js/commit/748b45a))
13
+
14
+
15
+
16
+
17
+ <a name="3.1.3"></a>
18
+ ## [3.1.3](https://github.com/awslabs/aws-mobile-appsync-sdk-js/compare/aws-appsync-subscription-link@3.1.2...aws-appsync-subscription-link@3.1.3) (2024-01-23)
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * exception due to illegal access to item in an empty map ([#747](https://github.com/awslabs/aws-mobile-appsync-sdk-js/issues/747)) ([82cb58e](https://github.com/awslabs/aws-mobile-appsync-sdk-js/commit/82cb58e))
24
+
25
+
26
+
27
+
6
28
  <a name="3.1.2"></a>
7
29
  ## [3.1.2](https://github.com/awslabs/aws-mobile-appsync-sdk-js/compare/aws-appsync-subscription-link@3.1.1...aws-appsync-subscription-link@3.1.2) (2022-11-17)
8
30
 
@@ -8,6 +8,20 @@ jest.mock('uuid', () => ({ v4: jest.fn() }));
8
8
 
9
9
  const query = gql`subscription { someSubscription { aField } }`
10
10
 
11
+ /**
12
+ * Helper to decode a base64url-encoded header from the Sec-WebSocket-Protocol value.
13
+ * The protocol value is prefixed with "header-".
14
+ */
15
+ function decodeProtocolHeader(protocols: string | string[]): Record<string, string> {
16
+ const arr = Array.isArray(protocols) ? protocols : [protocols];
17
+ const headerProtocol = arr.find(p => p.startsWith("header-"));
18
+ if (!headerProtocol) throw new Error("No header- protocol found");
19
+ const base64url = headerProtocol.slice("header-".length);
20
+ // Convert base64url back to standard base64
21
+ const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
22
+ return JSON.parse(Buffer.from(base64, "base64").toString());
23
+ }
24
+
11
25
  class myWebSocket implements WebSocket {
12
26
  binaryType: BinaryType;
13
27
  bufferedAmount: number;
@@ -75,13 +89,17 @@ describe("RealTime subscription link", () => {
75
89
  });
76
90
 
77
91
  test("Initialize WebSocket correctly for API KEY", (done) => {
78
- expect.assertions(2);
92
+ expect.assertions(3);
79
93
  jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
80
94
  return "2019-11-13T18:47:04.733Z";
81
95
  }));
82
96
  AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
83
- expect(url).toBe('wss://apikeytesturl1234567890123.appsync-realtime-api.us-west-2.amazonaws.com/graphql?header=eyJob3N0IjoiYXBpa2V5dGVzdHVybDEyMzQ1Njc4OTAxMjMuYXBwc3luYy1hcGkudXMtd2VzdC0yLmFtYXpvbmF3cy5jb20iLCJ4LWFtei1kYXRlIjoiMjAxOTExMTNUMTg0NzA0WiIsIngtYXBpLWtleSI6Inh4eHh4In0=&payload=e30=');
84
- expect(protocol).toBe('graphql-ws');
97
+ // URL should be clean — no query string with credentials
98
+ expect(url).toBe('wss://apikeytesturl1234567890123.appsync-realtime-api.us-west-2.amazonaws.com/graphql');
99
+ // Protocol should be an array with graphql-ws and header- prefix
100
+ expect(Array.isArray(protocol)).toBe(true);
101
+ const header = decodeProtocolHeader(protocol);
102
+ expect(header["x-api-key"]).toBe("xxxxx");
85
103
  done();
86
104
  return new myWebSocket();
87
105
  });
@@ -112,13 +130,15 @@ describe("RealTime subscription link", () => {
112
130
  });
113
131
 
114
132
  test("Initialize WebSocket correctly for API KEY with custom domain", (done) => {
115
- expect.assertions(2);
133
+ expect.assertions(3);
116
134
  jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
117
135
  return "2019-11-13T18:47:04.733Z";
118
136
  }));
119
137
  AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
120
- expect(url).toBe('wss://apikeytest.testcustomdomain.com/graphql/realtime?header=eyJob3N0IjoiYXBpa2V5dGVzdC50ZXN0Y3VzdG9tZG9tYWluLmNvbSIsIngtYW16LWRhdGUiOiIyMDE5MTExM1QxODQ3MDRaIiwieC1hcGkta2V5IjoieHh4eHgifQ==&payload=e30=');
121
- expect(protocol).toBe('graphql-ws');
138
+ expect(url).toBe('wss://apikeytest.testcustomdomain.com/graphql/realtime');
139
+ expect(Array.isArray(protocol)).toBe(true);
140
+ const header = decodeProtocolHeader(protocol);
141
+ expect(header["x-api-key"]).toBe("xxxxx");
122
142
  done();
123
143
  return new myWebSocket();
124
144
  });
@@ -149,13 +169,15 @@ describe("RealTime subscription link", () => {
149
169
  });
150
170
 
151
171
  test("Initialize WebSocket correctly for COGNITO USER POOLS", (done) => {
152
- expect.assertions(2);
172
+ expect.assertions(3);
153
173
  jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
154
174
  return "2019-11-13T18:47:04.733Z";
155
175
  }));
156
176
  AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
157
- expect(url).toBe('wss://cognitouserpooltesturl1234.appsync-realtime-api.us-west-2.amazonaws.com/graphql?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0IjoiY29nbml0b3VzZXJwb29sdGVzdHVybDEyMzQuYXBwc3luYy1hcGkudXMtd2VzdC0yLmFtYXpvbmF3cy5jb20ifQ==&payload=e30=');
158
- expect(protocol).toBe('graphql-ws');
177
+ expect(url).toBe('wss://cognitouserpooltesturl1234.appsync-realtime-api.us-west-2.amazonaws.com/graphql');
178
+ expect(Array.isArray(protocol)).toBe(true);
179
+ const header = decodeProtocolHeader(protocol);
180
+ expect(header["Authorization"]).toBe("token");
159
181
  done();
160
182
  return new myWebSocket();
161
183
  });
@@ -186,13 +208,15 @@ describe("RealTime subscription link", () => {
186
208
  });
187
209
 
188
210
  test("Initialize WebSocket correctly for COGNITO USER POOLS with custom domain", (done) => {
189
- expect.assertions(2);
211
+ expect.assertions(3);
190
212
  jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
191
213
  return "2019-11-13T18:47:04.733Z";
192
214
  }));
193
215
  AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
194
- expect(url).toBe('wss://cognitouserpools.testcustomdomain.com/graphql/realtime?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0IjoiY29nbml0b3VzZXJwb29scy50ZXN0Y3VzdG9tZG9tYWluLmNvbSJ9&payload=e30=');
195
- expect(protocol).toBe('graphql-ws');
216
+ expect(url).toBe('wss://cognitouserpools.testcustomdomain.com/graphql/realtime');
217
+ expect(Array.isArray(protocol)).toBe(true);
218
+ const header = decodeProtocolHeader(protocol);
219
+ expect(header["Authorization"]).toBe("token");
196
220
  done();
197
221
  return new myWebSocket();
198
222
  });
@@ -223,13 +247,15 @@ describe("RealTime subscription link", () => {
223
247
  });
224
248
 
225
249
  test("Initialize WebSocket correctly for OPENID_CONNECT", (done) => {
226
- expect.assertions(2);
250
+ expect.assertions(3);
227
251
  jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
228
252
  return "2019-11-13T18:47:04.733Z";
229
253
  }));
230
254
  AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
231
- expect(url).toBe('wss://openidconnecttesturl123456.appsync-realtime-api.us-west-2.amazonaws.com/graphql?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0Ijoib3BlbmlkY29ubmVjdHRlc3R1cmwxMjM0NTYuYXBwc3luYy1hcGkudXMtd2VzdC0yLmFtYXpvbmF3cy5jb20ifQ==&payload=e30=');
232
- expect(protocol).toBe('graphql-ws');
255
+ expect(url).toBe('wss://openidconnecttesturl123456.appsync-realtime-api.us-west-2.amazonaws.com/graphql');
256
+ expect(Array.isArray(protocol)).toBe(true);
257
+ const header = decodeProtocolHeader(protocol);
258
+ expect(header["Authorization"]).toBe("token");
233
259
  done();
234
260
  return new myWebSocket();
235
261
  });
@@ -260,13 +286,15 @@ describe("RealTime subscription link", () => {
260
286
  });
261
287
 
262
288
  test("Initialize WebSocket correctly for OPENID_CONNECT with custom domain", (done) => {
263
- expect.assertions(2);
289
+ expect.assertions(3);
264
290
  jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
265
291
  return "2019-11-13T18:47:04.733Z";
266
292
  }));
267
293
  AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
268
- expect(url).toBe('wss://openidconnecttesturl.testcustomdomain.com/graphql/realtime?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0Ijoib3BlbmlkY29ubmVjdHRlc3R1cmwudGVzdGN1c3RvbWRvbWFpbi5jb20ifQ==&payload=e30=');
269
- expect(protocol).toBe('graphql-ws');
294
+ expect(url).toBe('wss://openidconnecttesturl.testcustomdomain.com/graphql/realtime');
295
+ expect(Array.isArray(protocol)).toBe(true);
296
+ const header = decodeProtocolHeader(protocol);
297
+ expect(header["Authorization"]).toBe("token");
270
298
  done();
271
299
  return new myWebSocket();
272
300
  });
@@ -297,13 +325,15 @@ describe("RealTime subscription link", () => {
297
325
  });
298
326
 
299
327
  test('Initialize WebSocket correctly for AWS_LAMBDA', (done) => {
300
- expect.assertions(2);
328
+ expect.assertions(3);
301
329
  jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
302
330
  return "2019-11-13T18:47:04.733Z";
303
331
  }));
304
332
  AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
305
- expect(url).toBe('wss://awslambdatesturl1234567890.appsync-realtime-api.us-west-2.amazonaws.com/graphql?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0IjoiYXdzbGFtYmRhdGVzdHVybDEyMzQ1Njc4OTAuYXBwc3luYy1hcGkudXMtd2VzdC0yLmFtYXpvbmF3cy5jb20ifQ==&payload=e30=');
306
- expect(protocol).toBe('graphql-ws');
333
+ expect(url).toBe('wss://awslambdatesturl1234567890.appsync-realtime-api.us-west-2.amazonaws.com/graphql');
334
+ expect(Array.isArray(protocol)).toBe(true);
335
+ const header = decodeProtocolHeader(protocol);
336
+ expect(header["Authorization"]).toBe("token");
307
337
  done();
308
338
  return new myWebSocket();
309
339
  });
@@ -331,13 +361,15 @@ describe("RealTime subscription link", () => {
331
361
  })
332
362
 
333
363
  test('Initialize WebSocket correctly for AWS_LAMBDA with custom domain', (done) => {
334
- expect.assertions(2);
364
+ expect.assertions(3);
335
365
  jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
336
366
  return "2019-11-13T18:47:04.733Z";
337
367
  }));
338
368
  AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
339
- expect(url).toBe('wss://awslambdatesturl.testcustomdomain.com/graphql/realtime?header=eyJBdXRob3JpemF0aW9uIjoidG9rZW4iLCJob3N0IjoiYXdzbGFtYmRhdGVzdHVybC50ZXN0Y3VzdG9tZG9tYWluLmNvbSJ9&payload=e30=');
340
- expect(protocol).toBe('graphql-ws');
369
+ expect(url).toBe('wss://awslambdatesturl.testcustomdomain.com/graphql/realtime');
370
+ expect(Array.isArray(protocol)).toBe(true);
371
+ const header = decodeProtocolHeader(protocol);
372
+ expect(header["Authorization"]).toBe("token");
341
373
  done();
342
374
  return new myWebSocket();
343
375
  });
@@ -373,8 +405,8 @@ describe("RealTime subscription link", () => {
373
405
  return "2019-11-13T18:47:04.733Z";
374
406
  }));
375
407
  AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
376
- expect(url).toBe('wss://apikeytest.testcustomdomain.com/graphql/realtime?header=eyJob3N0IjoiYXBpa2V5dGVzdC50ZXN0Y3VzdG9tZG9tYWluLmNvbSIsIngtYW16LWRhdGUiOiIyMDE5MTExM1QxODQ3MDRaIiwieC1hcGkta2V5IjoieHh4eHgifQ==&payload=e30=');
377
- expect(protocol).toBe('graphql-ws');
408
+ expect(url).toBe('wss://apikeytest.testcustomdomain.com/graphql/realtime');
409
+ expect(Array.isArray(protocol)).toBe(true);
378
410
  const socket = new myWebSocket();
379
411
 
380
412
  setTimeout(() => {
@@ -452,8 +484,8 @@ describe("RealTime subscription link", () => {
452
484
  return "2019-11-13T18:47:04.733Z";
453
485
  }));
454
486
  AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
455
- expect(url).toBe('wss://apikeytest.testcustomdomain.com/graphql/realtime?header=eyJob3N0IjoiYXBpa2V5dGVzdC50ZXN0Y3VzdG9tZG9tYWluLmNvbSIsIngtYW16LWRhdGUiOiIyMDE5MTExM1QxODQ3MDRaIiwieC1hcGkta2V5IjoieHh4eHgifQ==&payload=e30=');
456
- expect(protocol).toBe('graphql-ws');
487
+ expect(url).toBe('wss://apikeytest.testcustomdomain.com/graphql/realtime');
488
+ expect(Array.isArray(protocol)).toBe(true);
457
489
  const socket = new myWebSocket();
458
490
 
459
491
  setTimeout(() => {
@@ -521,5 +553,34 @@ describe("RealTime subscription link", () => {
521
553
  });
522
554
  });
523
555
 
556
+ test("URL does not contain credentials in query string", (done) => {
557
+ expect.assertions(2);
558
+ jest.spyOn(Date.prototype, 'toISOString').mockImplementation(jest.fn(() => {
559
+ return "2019-11-13T18:47:04.733Z";
560
+ }));
561
+ AppSyncRealTimeSubscriptionHandshakeLink.createWebSocket = jest.fn((url, protocol) => {
562
+ // The URL must not contain any query parameters with auth material
563
+ expect(url.includes("?")).toBe(false);
564
+ // Auth should be in the protocol header instead
565
+ const header = decodeProtocolHeader(protocol);
566
+ expect(header["x-api-key"]).toBe("my-secret-key");
567
+ done();
568
+ return new myWebSocket();
569
+ });
570
+ const link = new AppSyncRealTimeSubscriptionHandshakeLink({
571
+ auth: {
572
+ type: AUTH_TYPE.API_KEY,
573
+ apiKey: "my-secret-key",
574
+ },
575
+ region: "us-west-2",
576
+ url: "https://securitytesturl12345678901.appsync-api.us-west-2.amazonaws.com/graphql",
577
+ });
578
+
579
+ execute(link, { query }).subscribe({
580
+ error: () => { fail; },
581
+ next: () => { done(); },
582
+ complete: () => { done(); },
583
+ });
584
+ });
524
585
 
525
586
  });
@@ -0,0 +1,44 @@
1
+ import { ApolloLink } from '@apollo/client/core';
2
+ import { AuthOptions } from 'aws-appsync-auth-link';
3
+
4
+ /*!
5
+ * Copyright 2017-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
6
+ * SPDX-License-Identifier: Apache-2.0
7
+ */
8
+
9
+ declare const CONTROL_EVENTS_KEY = "@@controlEvents";
10
+
11
+ type UrlInfo = {
12
+ url: string;
13
+ auth: AuthOptions;
14
+ region: string;
15
+ };
16
+ /**
17
+ * Route connections through a proxy (e.g. CloudFront) instead of connecting
18
+ * directly to AppSync. The library will send traffic to the proxy URL while
19
+ * using the original AppSync host for authentication.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * {
24
+ * url: 'https://xxx.appsync-api.us-east-1.amazonaws.com/graphql',
25
+ * region: 'us-east-1',
26
+ * auth: { type: AUTH_TYPE.API_KEY, apiKey: 'da2-xxx' },
27
+ * proxy: { url: 'https://d111111abcdef8.cloudfront.net/graphql' }
28
+ * }
29
+ * ```
30
+ */
31
+ type ProxyConfig = {
32
+ /** The proxy endpoint that sits in front of AppSync (e.g. a CloudFront distribution URL). */
33
+ url: string;
34
+ };
35
+ type AppSyncRealTimeSubscriptionConfig = UrlInfo & {
36
+ keepAliveTimeoutMs?: number;
37
+ /** Optional proxy configuration for routing through CloudFront or another CDN. */
38
+ proxy?: ProxyConfig;
39
+ };
40
+
41
+ declare function createSubscriptionHandshakeLink(args: AppSyncRealTimeSubscriptionConfig, resultsFetcherLink?: ApolloLink): ApolloLink;
42
+ declare function createSubscriptionHandshakeLink(url: string, resultsFetcherLink?: ApolloLink): ApolloLink;
43
+
44
+ export { CONTROL_EVENTS_KEY, createSubscriptionHandshakeLink };
package/lib/index.js CHANGED
@@ -11,8 +11,9 @@ var __rest = (this && this.__rest) || function (s, e) {
11
11
  return t;
12
12
  };
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.createSubscriptionHandshakeLink = exports.CONTROL_EVENTS_KEY = void 0;
14
15
  var subscription_handshake_link_1 = require("./subscription-handshake-link");
15
- exports.CONTROL_EVENTS_KEY = subscription_handshake_link_1.CONTROL_EVENTS_KEY;
16
+ Object.defineProperty(exports, "CONTROL_EVENTS_KEY", { enumerable: true, get: function () { return subscription_handshake_link_1.CONTROL_EVENTS_KEY; } });
16
17
  var core_1 = require("@apollo/client/core");
17
18
  var http_1 = require("@apollo/client/link/http");
18
19
  var utilities_1 = require("@apollo/client/utilities");
@@ -22,7 +23,7 @@ function createSubscriptionHandshakeLink(infoOrUrl, theResultsFetcherLink) {
22
23
  var resultsFetcherLink, subscriptionLinks;
23
24
  if (typeof infoOrUrl === "string") {
24
25
  resultsFetcherLink =
25
- theResultsFetcherLink || http_1.createHttpLink({ uri: infoOrUrl });
26
+ theResultsFetcherLink || (0, http_1.createHttpLink)({ uri: infoOrUrl });
26
27
  subscriptionLinks = core_1.ApolloLink.from([
27
28
  new non_terminating_link_1.NonTerminatingLink("controlMessages", {
28
29
  link: new core_1.ApolloLink(function (operation, _forward) {
@@ -43,12 +44,12 @@ function createSubscriptionHandshakeLink(infoOrUrl, theResultsFetcherLink) {
43
44
  }
44
45
  else {
45
46
  var url = infoOrUrl.url;
46
- resultsFetcherLink = theResultsFetcherLink || http_1.createHttpLink({ uri: url });
47
+ resultsFetcherLink = theResultsFetcherLink || (0, http_1.createHttpLink)({ uri: url });
47
48
  subscriptionLinks = new realtime_subscription_handshake_link_1.AppSyncRealTimeSubscriptionHandshakeLink(infoOrUrl);
48
49
  }
49
50
  return core_1.ApolloLink.split(function (operation) {
50
51
  var query = operation.query;
51
- var _a = utilities_1.getMainDefinition(query), kind = _a.kind, graphqlOperation = _a.operation;
52
+ var _a = (0, utilities_1.getMainDefinition)(query), kind = _a.kind, graphqlOperation = _a.operation;
52
53
  var isSubscription = kind === "OperationDefinition" && graphqlOperation === "subscription";
53
54
  return isSubscription;
54
55
  }, subscriptionLinks, resultsFetcherLink);