@queue-it/fastly 3.6.0 → 4.4.3-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +156 -58
  2. package/package.json +23 -16
  3. package/src/contextProvider.ts +175 -0
  4. package/src/fastlyCryptoProvider.ts +8 -0
  5. package/src/helper.ts +11 -0
  6. package/{assembly/sdk → src}/helpers/crypto.ts +338 -340
  7. package/src/index.ts +3 -0
  8. package/src/integrationConfigProvider.ts +199 -0
  9. package/src/requestResponseHandler.ts +200 -0
  10. package/README-INTERNAL.md +0 -21
  11. package/asconfig.json +0 -17
  12. package/assembly/__tests__/CustomerIntegrationDecodingHandler.spec.ts +0 -1086
  13. package/assembly/__tests__/IntegrationConfigHelpersTest.spec.ts +0 -574
  14. package/assembly/__tests__/KnownUserTest.spec.ts +0 -1894
  15. package/assembly/__tests__/Mocks.ts +0 -321
  16. package/assembly/__tests__/QueueParameterHelper.spec.ts +0 -59
  17. package/assembly/__tests__/UserInQueueService.spec.ts +0 -418
  18. package/assembly/__tests__/UserInQueueStateCookieRepository.spec.ts +0 -337
  19. package/assembly/__tests__/as-pect.config.js +0 -57
  20. package/assembly/__tests__/as-pect.d.ts +0 -1
  21. package/assembly/contextProvider.ts +0 -123
  22. package/assembly/helper.ts +0 -23
  23. package/assembly/index.ts +0 -10
  24. package/assembly/integrationConfigProvider.ts +0 -32
  25. package/assembly/requestResponseHandler.ts +0 -92
  26. package/assembly/sdk/HttpContextProvider.ts +0 -24
  27. package/assembly/sdk/IntegrationConfig/CustomerIntegrationDecodingHandler.ts +0 -198
  28. package/assembly/sdk/IntegrationConfig/IntegrationConfigHelpers.ts +0 -232
  29. package/assembly/sdk/IntegrationConfig/IntegrationConfigModel.ts +0 -93
  30. package/assembly/sdk/KnownUser.ts +0 -396
  31. package/assembly/sdk/Models.ts +0 -105
  32. package/assembly/sdk/QueueITHelpers.ts +0 -263
  33. package/assembly/sdk/UserInQueueService.ts +0 -245
  34. package/assembly/sdk/UserInQueueStateCookieRepository.ts +0 -189
  35. package/assembly/sdk/helpers/Date.ts +0 -194
  36. package/assembly/sdk/helpers/Uri.ts +0 -308
  37. package/assembly/tsconfig.json +0 -6
  38. package/index.js +0 -5
  39. package/pipelines/ci.yaml +0 -28
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export {IntegrationDetails, EnqueueTokenProviderFactory, IntegrationEndpointProvider, IntegrationEndpointCacheConfig, resolveIntegrationDetails} from "./integrationConfigProvider"
2
+ export {onQueueITRequest, onQueueITResponse} from "./requestResponseHandler";
3
+ export {RequestLogger} from "./helper";
@@ -0,0 +1,199 @@
1
+ import { ConfigStore } from "fastly:config-store";
2
+ import { IEnqueueTokenProvider } from "@queue-it/connector-javascript";
3
+ import { RequestLogger } from "./helper";
4
+
5
+ export type EnqueueTokenProviderFactory = (
6
+ customerId: string,
7
+ secretKey: string,
8
+ validityTime: number,
9
+ clientIp: string,
10
+ withKey: boolean
11
+ ) => IEnqueueTokenProvider;
12
+
13
+ export async function getIntegrationConfig(
14
+ details: IntegrationDetails,
15
+ endpointProvider: IntegrationEndpointProvider
16
+ ): Promise<string> {
17
+ const headers = new Headers();
18
+ headers.set("api-key", details.apiKey);
19
+ headers.set("host", endpointProvider.getHostname(details.customerId));
20
+ const request = new Request(
21
+ endpointProvider.getEndpoint(details.customerId),
22
+ {
23
+ method: "GET",
24
+ body: null,
25
+ headers: headers,
26
+ }
27
+ );
28
+ const cacheConf = endpointProvider.getCacheConfig();
29
+ const cacheInit: { ttl?: number; swr?: number } = {};
30
+ if (cacheConf.maxAge !== -1) cacheInit.ttl = cacheConf.maxAge;
31
+ if (cacheConf.staleWhileRevalidate !== -1) cacheInit.swr = cacheConf.staleWhileRevalidate;
32
+ const cacheOverride = new CacheOverride("override", cacheInit);
33
+
34
+ const beresp = await fetch(request, {
35
+ backend: details.queueItOrigin,
36
+ cacheOverride: cacheOverride,
37
+ });
38
+
39
+ if (!(details.logger instanceof MockLogger)) {
40
+ let cacheState = beresp.headers.get("x-cache");
41
+ let hits = beresp.headers.get("x-cache-hits");
42
+ if (hits == null) hits = "-1";
43
+ if (cacheState == null) cacheState = "n";
44
+ details.logger.log("IgnFetch:" + cacheState! + ":" + hits!);
45
+ }
46
+
47
+ if (beresp.status != 200) {
48
+ return "";
49
+ }
50
+ return await beresp.text();
51
+ }
52
+
53
+ const integrationCustomerId = "customerId",
54
+ integrationApiKey = "apiKey",
55
+ integrationSecret = "secret",
56
+ integrationQueueItOrigin = "queueItOrigin",
57
+ integrationDictionary = "IntegrationConfiguration",
58
+ workerHost = "workerHost",
59
+ enqueueTokenEnabledKey = "enqueueTokenEnabled",
60
+ enqueueTokenValidityTimeKey = "enqueueTokenValidityTime",
61
+ enqueueTokenKeyEnabledKey = "enqueueTokenKeyEnabled",
62
+ requestBodyEnabledKey = "requestBodyEnabled";
63
+
64
+ export function resolveIntegrationDetails(): IntegrationDetails | null {
65
+ const dict = new ConfigStore(integrationDictionary);
66
+ if (
67
+ dict.get(integrationCustomerId) === null ||
68
+ dict.get(integrationApiKey) === null ||
69
+ dict.get(integrationSecret) === null ||
70
+ dict.get(integrationQueueItOrigin) === null
71
+ ) {
72
+ return null;
73
+ }
74
+
75
+ let workerHostValue = "";
76
+ const workerHostVal = dict.get(workerHost);
77
+ if (workerHostVal !== null) {
78
+ workerHostValue = workerHostVal;
79
+ }
80
+
81
+ let enqueueTokenEnabled = true;
82
+ const enqueueTokenEnabledVal = dict.get(enqueueTokenEnabledKey);
83
+ if (enqueueTokenEnabledVal !== null) {
84
+ enqueueTokenEnabled = enqueueTokenEnabledVal.toLowerCase() === "true";
85
+ }
86
+
87
+ let enqueueTokenValidityTime = 240000;
88
+ const enqueueTokenValidityTimeVal = dict.get(enqueueTokenValidityTimeKey);
89
+ if (enqueueTokenValidityTimeVal !== null) {
90
+ enqueueTokenValidityTime = parseInt(enqueueTokenValidityTimeVal, 10);
91
+ }
92
+
93
+ let enqueueTokenKeyEnabled = false;
94
+ const enqueueTokenKeyEnabledVal = dict.get(enqueueTokenKeyEnabledKey);
95
+ if (enqueueTokenKeyEnabledVal !== null) {
96
+ enqueueTokenKeyEnabled = enqueueTokenKeyEnabledVal.toLowerCase() === "true";
97
+ }
98
+
99
+ let requestBodyEnabled = false;
100
+ const requestBodyEnabledVal = dict.get(requestBodyEnabledKey);
101
+ if (requestBodyEnabledVal !== null) {
102
+ requestBodyEnabled = requestBodyEnabledVal.toLowerCase() === "true";
103
+ }
104
+
105
+ return new IntegrationDetails(
106
+ dict.get(integrationQueueItOrigin)!,
107
+ dict.get(integrationCustomerId)!,
108
+ dict.get(integrationSecret)!,
109
+ dict.get(integrationApiKey)!,
110
+ workerHostValue,
111
+ enqueueTokenEnabled,
112
+ enqueueTokenValidityTime,
113
+ enqueueTokenKeyEnabled,
114
+ requestBodyEnabled
115
+ );
116
+ }
117
+
118
+ export class IntegrationEndpointCacheConfig {
119
+ maxAge: number = -1;
120
+ staleWhileRevalidate: number = -1;
121
+ }
122
+
123
+ export interface IntegrationEndpointProvider {
124
+ getHostname(customerId: string): string;
125
+
126
+ getEndpoint(customerId: string): string;
127
+
128
+ getCacheConfig(): IntegrationEndpointCacheConfig;
129
+ }
130
+
131
+ export class QueueItIntegrationEndpointProvider
132
+ implements IntegrationEndpointProvider
133
+ {
134
+ private readonly cacheConfig: IntegrationEndpointCacheConfig;
135
+
136
+ constructor() {
137
+ this.cacheConfig = new IntegrationEndpointCacheConfig();
138
+ this.cacheConfig.maxAge = 60 * 5;
139
+ this.cacheConfig.staleWhileRevalidate = 60 * 5;
140
+ }
141
+
142
+ getCacheConfig(): IntegrationEndpointCacheConfig {
143
+ return this.cacheConfig;
144
+ }
145
+
146
+ getHostname(customerId: string): string {
147
+ return customerId + ".queue-it.net";
148
+ }
149
+
150
+ getEndpoint(customerId: string): string {
151
+ const host = this.getHostname(customerId);
152
+ return "https://" + host + "/status/integrationconfig/secure/" + customerId;
153
+ }
154
+ }
155
+
156
+ class MockLogger implements RequestLogger {
157
+ log(message: string): void {}
158
+ }
159
+
160
+ export class IntegrationDetails {
161
+ public enqueueTokenProviderFactory: EnqueueTokenProviderFactory | null = null;
162
+
163
+ /**
164
+ * The client IP address of the end user. In Fastly Compute, the client IP is not available
165
+ * via request headers (unlike Cloudflare's cf-connecting-ip or Akamai's True-Client-IP).
166
+ * Instead, it must be obtained from event.client.address in the fetch event handler and
167
+ * passed here. Used by the SDK for IP-based trigger matching and enqueue token generation.
168
+ */
169
+ public clientIp: string = '';
170
+
171
+ constructor(
172
+ public queueItOrigin: string,
173
+ public customerId: string,
174
+ public secretKey: string,
175
+ public apiKey: string,
176
+ public workerHost: string,
177
+ public enqueueTokenEnabled: boolean = true,
178
+ public enqueueTokenValidityTime: number = 240000,
179
+ public enqueueTokenKeyEnabled: boolean = false,
180
+ public requestBodyEnabled: boolean = false,
181
+ public provider: IntegrationEndpointProvider = new QueueItIntegrationEndpointProvider(),
182
+ public logger: RequestLogger = new MockLogger()
183
+ ) {}
184
+
185
+ resolveWorkerRequestUrl(pureUrl: string): string {
186
+ if (this.workerHost == "") {
187
+ return pureUrl;
188
+ }
189
+ const protoEnding = pureUrl.indexOf("://") + 3;
190
+ const protocol = pureUrl.substr(0, protoEnding);
191
+ const urlWithoutProto = pureUrl.substr(protoEnding);
192
+ const pathAndQuery =
193
+ urlWithoutProto.indexOf("/") != -1
194
+ ? urlWithoutProto.substr(urlWithoutProto.indexOf("/"))
195
+ : "";
196
+ const rewrittenUrl = protocol + this.workerHost + pathAndQuery;
197
+ return rewrittenUrl;
198
+ }
199
+ }
@@ -0,0 +1,200 @@
1
+ import { KnownUser } from "@queue-it/connector-javascript";
2
+ import { QueueITHelper } from "./helper";
3
+ import { FastlyHttpContextProvider, getHttpHandler } from "./contextProvider";
4
+ import {
5
+ getIntegrationConfig,
6
+ resolveIntegrationDetails,
7
+ IntegrationDetails,
8
+ QueueItIntegrationEndpointProvider,
9
+ } from "./integrationConfigProvider";
10
+
11
+ const QUEUEIT_FAILED_HEADERNAME = "x-queueit-failed";
12
+ const QUEUEIT_CONNECTOR_EXECUTED_HEADER_NAME = "x-queueit-connector";
13
+
14
+ let httpProvider: FastlyHttpContextProvider | null = null;
15
+ let sendNoCacheHeaders: boolean = false;
16
+
17
+ export async function onQueueITRequest(
18
+ req: Request,
19
+ conf: IntegrationDetails | null = null
20
+ ): Promise<Response | null> {
21
+ if (conf == null) {
22
+ conf = resolveIntegrationDetails();
23
+ }
24
+ if (conf == null) {
25
+ return new Response("No integration details found.", {
26
+ headers: new Headers(),
27
+ status: 404,
28
+ });
29
+ }
30
+
31
+ const integrationProvider =
32
+ conf.provider == null
33
+ ? new QueueItIntegrationEndpointProvider()
34
+ : conf.provider;
35
+
36
+ let bodyString = '';
37
+ if (conf.requestBodyEnabled) {
38
+ bodyString = ((await req.clone().text()) || '').substring(0, 2048);
39
+ }
40
+
41
+ httpProvider = getHttpHandler(req, conf.clientIp, bodyString);
42
+ sendNoCacheHeaders = false;
43
+
44
+ if (conf.enqueueTokenEnabled) {
45
+ const clientIp = httpProvider.getHttpRequest().getUserHostAddress();
46
+ if (conf.enqueueTokenProviderFactory) {
47
+ httpProvider.setCustomEnqueueTokenProvider(
48
+ conf.enqueueTokenProviderFactory(
49
+ conf.customerId,
50
+ conf.secretKey,
51
+ conf.enqueueTokenValidityTime,
52
+ clientIp,
53
+ conf.enqueueTokenKeyEnabled
54
+ )
55
+ );
56
+ } else {
57
+ httpProvider.setEnqueueTokenProvider(
58
+ conf.customerId,
59
+ conf.secretKey,
60
+ conf.enqueueTokenValidityTime,
61
+ clientIp,
62
+ conf.enqueueTokenKeyEnabled
63
+ );
64
+ }
65
+ }
66
+
67
+ try {
68
+ const integrationConfigJson = await getIntegrationConfig(conf, integrationProvider);
69
+ const requestUrl: string = conf.resolveWorkerRequestUrl(req.url);
70
+
71
+ const queueItToken = getQueueItToken(requestUrl, httpProvider);
72
+ const requestUrlWithoutToken = requestUrl.replace(
73
+ new RegExp("([?&])(" + KnownUser.QueueITTokenKey + "=[^&]*)", "i"),
74
+ ""
75
+ );
76
+
77
+ // The requestUrlWithoutToken is used to match Triggers and as the Target url (where to return the users to).
78
+ // It is therefor important that this is exactly the url of the users browsers. So, if your webserver is
79
+ // behind e.g. a load balancer that modifies the host name or port, reformat requestUrlWithoutToken before proceeding.
80
+
81
+ const validationResult = await KnownUser.validateRequestByIntegrationConfig(
82
+ requestUrlWithoutToken,
83
+ queueItToken,
84
+ integrationConfigJson,
85
+ conf.customerId,
86
+ conf.secretKey,
87
+ httpProvider!
88
+ );
89
+
90
+ if (validationResult.doRedirect()) {
91
+ if (validationResult.isAjaxResult) {
92
+ const response = new Response(null, {
93
+ status: 200,
94
+ headers: httpProvider!.getResponseHeaders(),
95
+ });
96
+ const headerKey = validationResult.getAjaxQueueRedirectHeaderKey();
97
+ const queueRedirectUrl = validationResult.getAjaxRedirectUrl();
98
+
99
+ // In case of ajax call send the user to the queue by sending a custom queue-it header and redirecting user to queue from javascript
100
+ response.headers.set(
101
+ headerKey,
102
+ QueueITHelper.addKUPlatformVersion(queueRedirectUrl)
103
+ );
104
+ response.headers.set("Access-Control-Expose-Headers", headerKey);
105
+ sendNoCacheHeaders = true;
106
+ return response;
107
+ } else {
108
+ const response = new Response(null, {
109
+ status: 302,
110
+ headers: httpProvider!.getResponseHeaders(),
111
+ });
112
+ // Send the user to the queue - either because hash was missing or because is was invalid
113
+ response.headers.set(
114
+ "Location",
115
+ QueueITHelper.addKUPlatformVersion(validationResult.redirectUrl)
116
+ );
117
+ sendNoCacheHeaders = true;
118
+ return response;
119
+ }
120
+ } else {
121
+ // Request can continue - we remove queueittoken from querystring parameter to avoid sharing of user specific token
122
+ if (
123
+ requestUrl !== requestUrlWithoutToken &&
124
+ validationResult.actionType === "Queue"
125
+ ) {
126
+ const response = new Response(null, {
127
+ status: 302,
128
+ headers: httpProvider!.getResponseHeaders(),
129
+ });
130
+ response.headers.set("Location", requestUrlWithoutToken);
131
+ sendNoCacheHeaders = true;
132
+ return response;
133
+ } else {
134
+ // lets caller decide the next step, or just serve the request normally
135
+ return null;
136
+ }
137
+ }
138
+ } catch (e) {
139
+ if (console && console.log) {
140
+ console.log("ERROR:" + e);
141
+ }
142
+ httpProvider!.isError = true;
143
+ return null;
144
+ }
145
+ }
146
+
147
+ // Fill in the Queue-it headers
148
+ export function onQueueITResponse(res: Response): void {
149
+ res.headers.set(QUEUEIT_CONNECTOR_EXECUTED_HEADER_NAME, "fastly");
150
+
151
+ if (httpProvider) {
152
+ const contextHeaders = httpProvider.getResponseHeaders();
153
+ for (const key of contextHeaders.keys()) {
154
+ if (key.length == 0) continue;
155
+ const value = contextHeaders.get(key);
156
+ if (value != null && value.length > 0)
157
+ res.headers.append(key, value);
158
+ }
159
+
160
+ if (httpProvider.isError) {
161
+ res.headers.append(QUEUEIT_FAILED_HEADERNAME, "true");
162
+ }
163
+ }
164
+
165
+ if (sendNoCacheHeaders) {
166
+ addNoCacheHeaders(res);
167
+ }
168
+ }
169
+
170
+ function addNoCacheHeaders(res: Response): void {
171
+ res.headers.set('Cache-Control', 'no-cache, no-store, must-revalidate, max-age=0');
172
+ res.headers.set('Pragma', 'no-cache');
173
+ res.headers.set('Expires', 'Fri, 01 Jan 1990 00:00:00 GMT');
174
+ }
175
+
176
+ function getQueueItToken(
177
+ requestUrl: string,
178
+ httpContext: FastlyHttpContextProvider
179
+ ): string {
180
+ const queueItToken = getParameterByName(requestUrl, KnownUser.QueueITTokenKey);
181
+ if (queueItToken) {
182
+ return queueItToken;
183
+ }
184
+
185
+ const tokenHeaderName = `x-${KnownUser.QueueITTokenKey}`;
186
+ return httpContext.getHttpRequest().getHeader(tokenHeaderName);
187
+ }
188
+
189
+ function getParameterByName(url: string, name: string): string {
190
+ name = name.replace(/[\[\]]/g, '\\$&');
191
+ const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)');
192
+ const results = regex.exec(url);
193
+ if (!results) return '';
194
+ if (!results[2]) return '';
195
+ try {
196
+ return decodeURIComponent(results[2].replace(/\+/g, ' '));
197
+ } catch {
198
+ return results[2];
199
+ }
200
+ }
@@ -1,21 +0,0 @@
1
- ## Getting started
2
- - Install the [Fastly CLI](https://developer.fastly.com/reference/cli/)
3
- - Configure the CLI with the Fastly token.
4
- You can get a token in the Fastly platform.
5
- To configure the CLI run `fastly configure` and fill in the details.
6
- - You can list the Fastly services with `fastly service list`
7
-
8
- ## Development workflow
9
- - Build and test locally
10
- - Align version in package.json and UserInQueueService.ts
11
- - Commit changes
12
- - Run the CI pipeline
13
- - Update Sample site with new changes.
14
- This can be done by running `fastly compute build && fastly compute deploy`.
15
- - Sync with github
16
-
17
- ## Deploying to the sample site
18
- - Make sure you have the right configuration in `fastly.toml`
19
-
20
- ## Limitations
21
- - Since dictionary item values have a limit of 8k chars we poll the integration config with a 5 min TTL.
package/asconfig.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "targets": {
3
- "debug": {
4
- "binaryFile": "bin/untouched.wasm",
5
- "textFile": "bin/untouched.wat",
6
- "sourceMap": true,
7
- "debug": true
8
- },
9
- "release": {
10
- "binaryFile": "bin/main.wasm",
11
- "textFile": "bin/main.wat",
12
- "sourceMap": true,
13
- "optimize": true
14
- }
15
- },
16
- "options": {}
17
- }