@queue-it/fastly 1.0.4 → 2.0.0-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.
- package/LICENSE +21 -21
- package/README.md +129 -91
- package/package.json +16 -21
- package/{assembly → src}/contextProvider.ts +120 -122
- package/{assembly → src}/helper.ts +1 -1
- package/{assembly → src}/index.ts +3 -3
- package/{assembly → src}/integrationConfigProvider.ts +20 -23
- package/{assembly → src}/requestResponseHandler.ts +131 -137
- package/{assembly → src}/sdk/HttpContextProvider.ts +23 -24
- package/src/sdk/IntegrationConfig/CustomerIntegrationDecodingHandler.ts +57 -0
- package/{assembly → src}/sdk/IntegrationConfig/IntegrationConfigHelpers.ts +230 -232
- package/{assembly → src}/sdk/IntegrationConfig/IntegrationConfigModel.ts +93 -93
- package/{assembly → src}/sdk/KnownUser.ts +393 -395
- package/{assembly → src}/sdk/Models.ts +105 -105
- package/{assembly → src}/sdk/QueueITHelpers.ts +259 -263
- package/{assembly → src}/sdk/UserInQueueService.ts +245 -245
- package/{assembly → src}/sdk/UserInQueueStateCookieRepository.ts +188 -189
- package/src/sdk/helpers/Uri.ts +4 -0
- package/{assembly → src}/sdk/helpers/crypto.ts +338 -340
- package/assembly/sdk/IntegrationConfig/CustomerIntegrationDecodingHandler.ts +0 -198
- package/assembly/sdk/helpers/Uri.ts +0 -308
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { Fastly, Headers, Request } from "@fastly/as-compute";
|
|
2
1
|
import { RequestLogger } from "./helper";
|
|
3
2
|
|
|
4
|
-
export function getIntegrationConfig(
|
|
3
|
+
export async function getIntegrationConfig(
|
|
5
4
|
details: IntegrationDetails,
|
|
6
5
|
endpointProvider: IntegrationEndpointProvider
|
|
7
|
-
): string {
|
|
6
|
+
): Promise<string> {
|
|
8
7
|
const headers = new Headers();
|
|
9
8
|
headers.set("api-key", details.apiKey);
|
|
10
9
|
headers.set("host", endpointProvider.getHostname(details.customerId));
|
|
@@ -16,19 +15,16 @@ export function getIntegrationConfig(
|
|
|
16
15
|
headers: headers,
|
|
17
16
|
}
|
|
18
17
|
);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (cacheConf.maxAge
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (cacheConf.staleWhileRevalidate != -1) {
|
|
25
|
-
cacheOverride.setSWR(cacheConf.staleWhileRevalidate);
|
|
26
|
-
}
|
|
18
|
+
const cacheConf = endpointProvider.getCacheConfig();
|
|
19
|
+
const cacheInit: { ttl?: number; swr?: number } = {};
|
|
20
|
+
if (cacheConf.maxAge !== -1) cacheInit.ttl = cacheConf.maxAge;
|
|
21
|
+
if (cacheConf.staleWhileRevalidate !== -1) cacheInit.swr = cacheConf.staleWhileRevalidate;
|
|
22
|
+
const cacheOverride = new CacheOverride("override", cacheInit);
|
|
27
23
|
|
|
28
|
-
|
|
24
|
+
const beresp = await fetch(request, {
|
|
29
25
|
backend: details.queueItOrigin,
|
|
30
26
|
cacheOverride: cacheOverride,
|
|
31
|
-
})
|
|
27
|
+
});
|
|
32
28
|
|
|
33
29
|
if (!(details.logger instanceof MockLogger)) {
|
|
34
30
|
let cacheState = beresp.headers.get("x-cache");
|
|
@@ -41,7 +37,7 @@ export function getIntegrationConfig(
|
|
|
41
37
|
if (beresp.status != 200) {
|
|
42
38
|
return "";
|
|
43
39
|
}
|
|
44
|
-
return beresp.text();
|
|
40
|
+
return await beresp.text();
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
const integrationCustomerId = "customerId",
|
|
@@ -52,19 +48,20 @@ const integrationCustomerId = "customerId",
|
|
|
52
48
|
workerHost = "workerHost";
|
|
53
49
|
|
|
54
50
|
export function resolveIntegrationDetails(): IntegrationDetails | null {
|
|
55
|
-
const dict = new
|
|
51
|
+
const dict = new ConfigStore(integrationDictionary);
|
|
56
52
|
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
dict.get(integrationCustomerId) === null ||
|
|
54
|
+
dict.get(integrationApiKey) === null ||
|
|
55
|
+
dict.get(integrationSecret) === null ||
|
|
56
|
+
dict.get(integrationQueueItOrigin) === null
|
|
61
57
|
) {
|
|
62
58
|
return null;
|
|
63
59
|
}
|
|
64
60
|
|
|
65
61
|
let workerHostValue = "";
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
const workerHostVal = dict.get(workerHost);
|
|
63
|
+
if (workerHostVal !== null) {
|
|
64
|
+
workerHostValue = workerHostVal;
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
return new IntegrationDetails(
|
|
@@ -77,8 +74,8 @@ export function resolveIntegrationDetails(): IntegrationDetails | null {
|
|
|
77
74
|
}
|
|
78
75
|
|
|
79
76
|
export class IntegrationEndpointCacheConfig {
|
|
80
|
-
maxAge:
|
|
81
|
-
staleWhileRevalidate:
|
|
77
|
+
maxAge: number = -1;
|
|
78
|
+
staleWhileRevalidate: number = -1;
|
|
82
79
|
}
|
|
83
80
|
|
|
84
81
|
export interface IntegrationEndpointProvider {
|
|
@@ -1,137 +1,131 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "./
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
validationResultPair.first
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (contextHeaderKeys[i].length == 0) continue;
|
|
133
|
-
let value = contextHeaders.get(contextHeaderKeys[i]);
|
|
134
|
-
if (value != null && value!.length > 0)
|
|
135
|
-
res.headers.append(contextHeaderKeys[i], value!);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
1
|
+
import { KnownUser } from "./sdk/KnownUser";
|
|
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
|
+
import { Utils } from "./sdk/QueueITHelpers";
|
|
11
|
+
|
|
12
|
+
const QUEUEIT_FAILED_HEADERNAME = "x-queueit-failed";
|
|
13
|
+
let httpProvider: FastlyHttpContextProvider | null = null;
|
|
14
|
+
|
|
15
|
+
export async function onQueueITRequest(
|
|
16
|
+
req: Request,
|
|
17
|
+
conf: IntegrationDetails | null = null
|
|
18
|
+
): Promise<Response | null> {
|
|
19
|
+
if (conf == null) {
|
|
20
|
+
conf = resolveIntegrationDetails();
|
|
21
|
+
}
|
|
22
|
+
if (conf == null) {
|
|
23
|
+
return new Response("No integration details found.", {
|
|
24
|
+
headers: new Headers(),
|
|
25
|
+
status: 404,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const integrationProvider =
|
|
30
|
+
conf.provider == null
|
|
31
|
+
? new QueueItIntegrationEndpointProvider()
|
|
32
|
+
: conf.provider;
|
|
33
|
+
QueueITHelper.configureKnownUserHashing();
|
|
34
|
+
httpProvider = getHttpHandler(req);
|
|
35
|
+
|
|
36
|
+
let integrationConfigJson = await getIntegrationConfig(conf, integrationProvider);
|
|
37
|
+
const requestUrl: string = conf.resolveWorkerRequestUrl(req.url);
|
|
38
|
+
|
|
39
|
+
const queueItToken = Utils.getParameterByName(
|
|
40
|
+
requestUrl,
|
|
41
|
+
KnownUser.QueueITTokenKey
|
|
42
|
+
);
|
|
43
|
+
const requestUrlWithoutToken: string = Utils.removeQueueItToken(requestUrl);
|
|
44
|
+
|
|
45
|
+
// The requestUrlWithoutToken is used to match Triggers and as the Target url (where to return the users to).
|
|
46
|
+
// It is therefor important that this is exactly the url of the users browsers. So, if your webserver is
|
|
47
|
+
// behind e.g. a load balancer that modifies the host name or port, reformat requestUrlWithoutToken before proceeding.
|
|
48
|
+
const validationResultPair = KnownUser.validateRequestByIntegrationConfig(
|
|
49
|
+
requestUrlWithoutToken,
|
|
50
|
+
queueItToken,
|
|
51
|
+
integrationConfigJson,
|
|
52
|
+
conf.customerId,
|
|
53
|
+
conf.secretKey,
|
|
54
|
+
httpProvider!
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
validationResultPair.first != null &&
|
|
59
|
+
validationResultPair.first!.doRedirect()
|
|
60
|
+
) {
|
|
61
|
+
const validationResult = validationResultPair.first!;
|
|
62
|
+
|
|
63
|
+
if (validationResult.isAjaxResult) {
|
|
64
|
+
let response = new Response(null, {
|
|
65
|
+
status: 200,
|
|
66
|
+
headers: httpProvider!.getHttpResponse().getHeaders(),
|
|
67
|
+
});
|
|
68
|
+
// 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
|
|
69
|
+
response.headers.set("Access-Control-Expose-Headers", validationResult.getAjaxQueueRedirectHeaderKey());
|
|
70
|
+
response.headers.set(
|
|
71
|
+
validationResult.getAjaxQueueRedirectHeaderKey(),
|
|
72
|
+
QueueITHelper.addKUPlatformVersion(
|
|
73
|
+
validationResult.getAjaxRedirectUrl()
|
|
74
|
+
)
|
|
75
|
+
);
|
|
76
|
+
Utils.addNoCacheHeaders(response);
|
|
77
|
+
return response;
|
|
78
|
+
} else {
|
|
79
|
+
let response = new Response(null, {
|
|
80
|
+
status: 302,
|
|
81
|
+
headers: httpProvider!.getHttpResponse().getHeaders(),
|
|
82
|
+
});
|
|
83
|
+
// Send the user to the queue - either because hash was missing or because is was invalid
|
|
84
|
+
response.headers.set(
|
|
85
|
+
"Location",
|
|
86
|
+
QueueITHelper.addKUPlatformVersion(validationResult.redirectUrl)
|
|
87
|
+
);
|
|
88
|
+
Utils.addNoCacheHeaders(response);
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
} else if (validationResultPair.first != null) {
|
|
92
|
+
const validationResult = validationResultPair.first!;
|
|
93
|
+
// Request can continue - we remove queueittoken form querystring parameter to avoid sharing of user specific token
|
|
94
|
+
// Support mobile scenario adding the condition !validationResult.isAjaxResult
|
|
95
|
+
if (
|
|
96
|
+
queueItToken != "" &&
|
|
97
|
+
!validationResult.isAjaxResult &&
|
|
98
|
+
validationResult.actionType == "Queue"
|
|
99
|
+
) {
|
|
100
|
+
let response = new Response(null, {
|
|
101
|
+
status: 302,
|
|
102
|
+
headers: httpProvider!.getHttpResponse().getHeaders(),
|
|
103
|
+
});
|
|
104
|
+
response.headers.set("Location", requestUrlWithoutToken);
|
|
105
|
+
Utils.addNoCacheHeaders(response);
|
|
106
|
+
return response;
|
|
107
|
+
} else {
|
|
108
|
+
// lets caller decide the next step, or just serve the request normally
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
} else if (validationResultPair.second != null) {
|
|
112
|
+
httpProvider!.isError = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
//Fill in the Queue-it headers
|
|
119
|
+
export function onQueueITResponse(res: Response): void {
|
|
120
|
+
const contextHeaders = httpProvider!.getHttpResponse().getHeaders();
|
|
121
|
+
|
|
122
|
+
if (httpProvider!.isError) {
|
|
123
|
+
res.headers.append(QUEUEIT_FAILED_HEADERNAME, "true");
|
|
124
|
+
}
|
|
125
|
+
for (const key of contextHeaders.keys()) {
|
|
126
|
+
if (key.length == 0) continue;
|
|
127
|
+
const value = contextHeaders.get(key);
|
|
128
|
+
if (value != null && value.length > 0)
|
|
129
|
+
res.headers.append(key, value);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
1
|
+
|
|
2
|
+
export interface IHttpRequest {
|
|
3
|
+
getUserAgent(): string;
|
|
4
|
+
getHeader(name: string): string;
|
|
5
|
+
getAbsoluteUri(): string;
|
|
6
|
+
getUserHostAddress(): string;
|
|
7
|
+
getCookieValue(cookieKey: string): string;
|
|
8
|
+
getRequestBodyAsString(): string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IHttpResponse {
|
|
12
|
+
setCookie(cookieName: string, cookieValue: string, domain: string, expiration: number): void;
|
|
13
|
+
getHeaders(): Headers;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IHttpContextProvider {
|
|
17
|
+
getHttpRequest(): IHttpRequest;
|
|
18
|
+
getHttpResponse(): IHttpResponse;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IDateTimeProvider {
|
|
22
|
+
getCurrentTime(): Date
|
|
23
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CustomerIntegration,
|
|
3
|
+
IntegrationConfigModel,
|
|
4
|
+
TriggerModel,
|
|
5
|
+
TriggerPart
|
|
6
|
+
} from "./IntegrationConfigModel";
|
|
7
|
+
|
|
8
|
+
export class CustomerIntegrationDecodingHandler {
|
|
9
|
+
static deserialize(integrationsConfigString: string): CustomerIntegration {
|
|
10
|
+
const result = new CustomerIntegration();
|
|
11
|
+
if (!integrationsConfigString) return result;
|
|
12
|
+
|
|
13
|
+
const parsed = JSON.parse(integrationsConfigString);
|
|
14
|
+
result.Version = parsed.Version ?? 0;
|
|
15
|
+
result.Description = parsed.Description ?? '';
|
|
16
|
+
|
|
17
|
+
if (Array.isArray(parsed.Integrations)) {
|
|
18
|
+
result.Integrations = parsed.Integrations.map((item: any) => {
|
|
19
|
+
const model = new IntegrationConfigModel();
|
|
20
|
+
model.Name = item.Name ?? '';
|
|
21
|
+
model.EventId = item.EventId ?? '';
|
|
22
|
+
model.CookieDomain = item.CookieDomain ?? '';
|
|
23
|
+
model.LayoutName = item.LayoutName ?? '';
|
|
24
|
+
model.Culture = item.Culture ?? '';
|
|
25
|
+
model.ExtendCookieValidity = item.ExtendCookieValidity ?? false;
|
|
26
|
+
model.CookieValidityMinute = item.CookieValidityMinute ?? 0;
|
|
27
|
+
model.QueueDomain = item.QueueDomain ?? '';
|
|
28
|
+
model.RedirectLogic = item.RedirectLogic ?? '';
|
|
29
|
+
model.ForcedTargetUrl = item.ForcedTargetUrl ?? '';
|
|
30
|
+
model.ActionType = item.ActionType ?? '';
|
|
31
|
+
|
|
32
|
+
model.Triggers = (item.Triggers ?? []).map((trigger: any) => {
|
|
33
|
+
const t = new TriggerModel();
|
|
34
|
+
t.LogicalOperator = trigger.LogicalOperator ?? '';
|
|
35
|
+
t.TriggerParts = (trigger.TriggerParts ?? []).map((part: any) => {
|
|
36
|
+
const tp = new TriggerPart();
|
|
37
|
+
tp.ValidatorType = part.ValidatorType ?? '';
|
|
38
|
+
tp.Operator = part.Operator ?? '';
|
|
39
|
+
tp.ValueToCompare = part.ValueToCompare ?? '';
|
|
40
|
+
tp.ValuesToCompare = part.ValuesToCompare ?? [];
|
|
41
|
+
tp.IsNegative = part.IsNegative ?? false;
|
|
42
|
+
tp.IsIgnoreCase = part.IsIgnoreCase ?? false;
|
|
43
|
+
tp.UrlPart = part.UrlPart ?? '';
|
|
44
|
+
tp.CookieName = part.CookieName ?? '';
|
|
45
|
+
tp.HttpHeaderName = part.HttpHeaderName ?? '';
|
|
46
|
+
return tp;
|
|
47
|
+
});
|
|
48
|
+
return t;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return model;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
}
|