@pallasoft/ps-utilities 2.18.0 → 2.19.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/bin/ps-worker-bundle-check.js +4 -0
- package/dist/build/index.d.ts +6 -0
- package/dist/build/index.js +18 -0
- package/dist/build/worker-bundle-cli.d.ts +5 -0
- package/dist/build/worker-bundle-cli.js +40 -0
- package/dist/build/worker-bundle-service.d.ts +10 -0
- package/dist/build/worker-bundle-service.js +100 -0
- package/dist/esm/build/index.js +2 -0
- package/dist/esm/build/worker-bundle-cli.js +33 -0
- package/dist/esm/build/worker-bundle-service.js +80 -0
- package/dist/esm/client/constants/device-config.js +6 -0
- package/dist/esm/client/debounce-service.js +34 -0
- package/dist/esm/client/device-service.js +15 -0
- package/dist/esm/client/http-service.js +51 -0
- package/dist/esm/client/index.js +3 -0
- package/dist/esm/common/amount-service.js +37 -0
- package/dist/esm/common/base64-service.js +17 -0
- package/dist/esm/common/color-service.js +8 -0
- package/dist/esm/common/constants/amount-config.js +8 -0
- package/dist/esm/common/constants/amount-direction.js +6 -0
- package/dist/esm/common/constants/email-preview-config.js +30 -0
- package/dist/esm/common/constants/error-response-config.js +4 -0
- package/dist/esm/common/constants/file-size-config.js +5 -0
- package/dist/esm/common/constants/http-auth-scheme.js +3 -0
- package/dist/esm/common/constants/http-content-type.js +8 -0
- package/dist/esm/common/constants/http-header.js +23 -0
- package/dist/esm/common/constants/http-method.js +10 -0
- package/dist/esm/common/constants/index.js +19 -0
- package/dist/esm/common/constants/phone-config.js +6 -0
- package/dist/esm/common/constants/portable-text-config.js +17 -0
- package/dist/esm/common/constants/postal-code-formats.js +10 -0
- package/dist/esm/common/constants/relative-time-config.js +11 -0
- package/dist/esm/common/constants/request-client-config.js +6 -0
- package/dist/esm/common/constants/signed-token-config.js +11 -0
- package/dist/esm/common/constants/socket-token-config.js +4 -0
- package/dist/esm/common/constants/user-agent-config.js +18 -0
- package/dist/esm/common/constants/user-token-config.js +5 -0
- package/dist/esm/common/constants/validation-config.js +8 -0
- package/dist/esm/common/crypto-service.js +128 -0
- package/dist/esm/common/date-service.js +101 -0
- package/dist/esm/common/email-preview-service.js +142 -0
- package/dist/esm/common/encrypted-fields-service.js +49 -0
- package/dist/esm/common/envelope-crypto-service.js +48 -0
- package/dist/esm/common/error-response-service.js +20 -0
- package/dist/esm/common/file-size-service.js +15 -0
- package/dist/esm/common/index.js +27 -0
- package/dist/esm/common/json-service.js +16 -0
- package/dist/esm/common/number-format-service.js +22 -0
- package/dist/esm/common/phone-service.js +70 -0
- package/dist/esm/common/portable-text-service.js +97 -0
- package/dist/esm/common/postal-code-service.js +34 -0
- package/dist/esm/common/recaptcha-service.js +23 -0
- package/dist/esm/common/relative-time-service.js +18 -0
- package/dist/esm/common/request-client-service.js +20 -0
- package/dist/esm/common/signed-token-service.js +111 -0
- package/dist/esm/common/socket-token-service.js +22 -0
- package/dist/esm/common/string-service.js +5 -0
- package/dist/esm/common/transform-service.js +12 -0
- package/dist/esm/common/url-service.js +15 -0
- package/dist/esm/common/user-agent-service.js +26 -0
- package/dist/esm/common/user-token-service.js +59 -0
- package/dist/esm/common/validation-service.js +29 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/models/coded-error-data.js +1 -0
- package/dist/esm/models/device-data.js +1 -0
- package/dist/esm/models/email-preview-input.js +1 -0
- package/dist/esm/models/encrypted-value-payload.js +1 -0
- package/dist/esm/models/json-value-guard.js +1 -0
- package/dist/esm/models/phone-country-data.js +1 -0
- package/dist/esm/models/phone-display-data.js +1 -0
- package/dist/esm/models/postal-code-format-data.js +1 -0
- package/dist/esm/models/relative-time-options.js +1 -0
- package/dist/esm/models/request-client-data.js +1 -0
- package/dist/esm/models/request-client-options.js +1 -0
- package/dist/esm/models/signed-token-header.js +1 -0
- package/dist/esm/models/signed-token-key-pair.js +1 -0
- package/dist/esm/models/socket-client-options.js +1 -0
- package/dist/esm/models/socket-token-claims.js +1 -0
- package/dist/esm/models/user-token-claims.js +1 -0
- package/dist/esm/models/user-token-issue-options.js +1 -0
- package/dist/esm/models/user-token-request.js +1 -0
- package/dist/esm/models/user-token-subject.js +1 -0
- package/dist/esm/models/user-token-verify-options.js +1 -0
- package/dist/esm/models/worker-bundle-budget-data.js +1 -0
- package/dist/esm/models/worker-bundle-file-data.js +1 -0
- package/dist/esm/models/worker-bundle-options-data.js +1 -0
- package/dist/esm/models/worker-bundle-report-data.js +1 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/server/file-service.js +41 -0
- package/dist/esm/server/index.js +2 -0
- package/dist/esm/server/timing-safe-service.js +8 -0
- package/dist/esm/sockets/index.js +1 -0
- package/dist/esm/sockets/socket-client-service.js +10 -0
- package/dist/index.d.ts +2 -4
- package/dist/index.js +3 -5
- package/dist/models/worker-bundle-budget-data.d.ts +5 -0
- package/dist/models/worker-bundle-budget-data.js +2 -0
- package/dist/models/worker-bundle-file-data.d.ts +6 -0
- package/dist/models/worker-bundle-file-data.js +2 -0
- package/dist/models/worker-bundle-options-data.d.ts +6 -0
- package/dist/models/worker-bundle-options-data.js +2 -0
- package/dist/models/worker-bundle-report-data.d.ts +8 -0
- package/dist/models/worker-bundle-report-data.js +2 -0
- package/package.json +16 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { AsYouType, getCountries, getCountryCallingCode, parsePhoneNumberFromString, } from 'libphonenumber-js';
|
|
2
|
+
import { PhoneConfig } from './constants/phone-config';
|
|
3
|
+
export class PhoneService {
|
|
4
|
+
static isCountry(code) {
|
|
5
|
+
return getCountries().includes(code);
|
|
6
|
+
}
|
|
7
|
+
static countries(preferred, locale = PhoneConfig.locale) {
|
|
8
|
+
const names = new Intl.DisplayNames([locale], { type: PhoneConfig.displayNamesType });
|
|
9
|
+
const options = getCountries().map((code) => {
|
|
10
|
+
const name = names.of(code);
|
|
11
|
+
return {
|
|
12
|
+
callingCode: getCountryCallingCode(code),
|
|
13
|
+
code,
|
|
14
|
+
label: name !== undefined ? name : code,
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
return options.sort((left, right) => {
|
|
18
|
+
if (left.code === preferred)
|
|
19
|
+
return -1;
|
|
20
|
+
if (right.code === preferred)
|
|
21
|
+
return 1;
|
|
22
|
+
return left.label.localeCompare(right.label);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
static display(value, fallbackCountry) {
|
|
26
|
+
const parsed = value.startsWith(PhoneConfig.plus)
|
|
27
|
+
? parsePhoneNumberFromString(value)
|
|
28
|
+
: undefined;
|
|
29
|
+
if (parsed !== undefined && parsed.country !== undefined)
|
|
30
|
+
return { country: parsed.country, national: parsed.formatNational() };
|
|
31
|
+
return { country: fallbackCountry, national: value };
|
|
32
|
+
}
|
|
33
|
+
static format(national, country) {
|
|
34
|
+
if (!PhoneService.isCountry(country))
|
|
35
|
+
return national;
|
|
36
|
+
return new AsYouType(country).input(national);
|
|
37
|
+
}
|
|
38
|
+
static formatForDisplay(value, country) {
|
|
39
|
+
const parsed = PhoneService.parse(value, country);
|
|
40
|
+
if (parsed === undefined || !parsed.isValid())
|
|
41
|
+
return value.trim();
|
|
42
|
+
return parsed.country === country ? parsed.formatNational() : parsed.formatInternational();
|
|
43
|
+
}
|
|
44
|
+
static toStored(national, country) {
|
|
45
|
+
const trimmed = national.trim();
|
|
46
|
+
if (trimmed === '' || !PhoneService.isCountry(country))
|
|
47
|
+
return trimmed;
|
|
48
|
+
const parsed = parsePhoneNumberFromString(trimmed, country);
|
|
49
|
+
return parsed !== undefined ? parsed.number : trimmed;
|
|
50
|
+
}
|
|
51
|
+
static isValid(national, country) {
|
|
52
|
+
if (national.trim() === '')
|
|
53
|
+
return true;
|
|
54
|
+
const parsed = PhoneService.parse(national, country);
|
|
55
|
+
return parsed !== undefined && parsed.isValid();
|
|
56
|
+
}
|
|
57
|
+
static callingCode(country) {
|
|
58
|
+
return PhoneService.isCountry(country) ? getCountryCallingCode(country) : '';
|
|
59
|
+
}
|
|
60
|
+
static parse(value, country) {
|
|
61
|
+
const trimmed = value.trim();
|
|
62
|
+
if (trimmed === '')
|
|
63
|
+
return undefined;
|
|
64
|
+
if (trimmed.startsWith(PhoneConfig.plus))
|
|
65
|
+
return parsePhoneNumberFromString(trimmed);
|
|
66
|
+
return PhoneService.isCountry(country)
|
|
67
|
+
? parsePhoneNumberFromString(trimmed, country)
|
|
68
|
+
: undefined;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Common } from '@pallasoft/ps-models';
|
|
2
|
+
import { escapeHTML, toHTML } from '@portabletext/to-html';
|
|
3
|
+
import { PortableTextConfig } from './constants/portable-text-config';
|
|
4
|
+
export class PortableTextService {
|
|
5
|
+
static toPlainText(blocks) {
|
|
6
|
+
const lines = [];
|
|
7
|
+
const counters = {};
|
|
8
|
+
let previousWasList = false;
|
|
9
|
+
blocks.forEach((block) => {
|
|
10
|
+
if (block._type !== Common.PortableTextConfig.blockType)
|
|
11
|
+
return;
|
|
12
|
+
const text = this.blockText(block);
|
|
13
|
+
const level = typeof block.level === 'number' ? block.level : 1;
|
|
14
|
+
const isNumber = block.listItem === PortableTextConfig.numberItem;
|
|
15
|
+
const isBullet = block.listItem === PortableTextConfig.bulletItem;
|
|
16
|
+
const isList = isNumber || isBullet;
|
|
17
|
+
if (!isList) {
|
|
18
|
+
counters[level] = 0;
|
|
19
|
+
if (lines.length > 0)
|
|
20
|
+
lines.push(PortableTextConfig.paragraphSeparator);
|
|
21
|
+
lines.push(block.style === PortableTextConfig.quoteStyle
|
|
22
|
+
? PortableTextConfig.quotePrefix + text
|
|
23
|
+
: text);
|
|
24
|
+
previousWasList = false;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
counters[level] = isNumber
|
|
28
|
+
? (typeof counters[level] === 'number' ? counters[level] : 0) + 1
|
|
29
|
+
: 0;
|
|
30
|
+
const prefix = isNumber
|
|
31
|
+
? `${counters[level]}${PortableTextConfig.numberSuffix}`
|
|
32
|
+
: PortableTextConfig.bulletPrefix;
|
|
33
|
+
const indent = ' '.repeat(level - 1);
|
|
34
|
+
if (lines.length > 0)
|
|
35
|
+
lines.push(previousWasList ? PortableTextConfig.lineSeparator : PortableTextConfig.paragraphSeparator);
|
|
36
|
+
lines.push(indent + prefix + text);
|
|
37
|
+
previousWasList = true;
|
|
38
|
+
});
|
|
39
|
+
return lines.join('');
|
|
40
|
+
}
|
|
41
|
+
static fromPlainText(text) {
|
|
42
|
+
return text
|
|
43
|
+
.replace(PortableTextConfig.lineBreakPattern, PortableTextConfig.lineSeparator)
|
|
44
|
+
.split(PortableTextConfig.paragraphSplitPattern)
|
|
45
|
+
.map((paragraph) => paragraph.trim())
|
|
46
|
+
.filter((paragraph) => paragraph.length > 0)
|
|
47
|
+
.map((paragraph, index) => ({
|
|
48
|
+
_type: Common.PortableTextConfig.blockType,
|
|
49
|
+
_key: `${PortableTextConfig.keyPrefix.block}${index}`,
|
|
50
|
+
style: PortableTextConfig.normalStyle,
|
|
51
|
+
markDefs: [],
|
|
52
|
+
children: [
|
|
53
|
+
{
|
|
54
|
+
_type: Common.PortableTextConfig.spanType,
|
|
55
|
+
_key: `${PortableTextConfig.keyPrefix.span}${index}`,
|
|
56
|
+
text: paragraph,
|
|
57
|
+
marks: [],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
static isEmpty(blocks) {
|
|
63
|
+
return (!Array.isArray(blocks) || blocks.every((block) => this.blockText(block).trim().length === 0));
|
|
64
|
+
}
|
|
65
|
+
static toHtml(blocks) {
|
|
66
|
+
return toHTML(blocks, {
|
|
67
|
+
onMissingComponent: false,
|
|
68
|
+
components: {
|
|
69
|
+
marks: {
|
|
70
|
+
link: ({ children, value }) => {
|
|
71
|
+
const href = typeof value?.href === 'string' ? value.href : '';
|
|
72
|
+
return this.isSafeLink(href)
|
|
73
|
+
? `<a href="${escapeHTML(href)}" rel="${PortableTextConfig.linkRel}" target="${PortableTextConfig.linkTarget}">${children}</a>`
|
|
74
|
+
: children;
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
static blockText(block) {
|
|
81
|
+
return Array.isArray(block.children)
|
|
82
|
+
? block.children
|
|
83
|
+
.map((child) => child._type === Common.PortableTextConfig.spanType && typeof child.text === 'string'
|
|
84
|
+
? child.text
|
|
85
|
+
: '')
|
|
86
|
+
.join('')
|
|
87
|
+
: '';
|
|
88
|
+
}
|
|
89
|
+
static isSafeLink(href) {
|
|
90
|
+
try {
|
|
91
|
+
return Common.PortableTextConfig.linkProtocols.includes(new URL(href).protocol);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { PostalCodeConfig, PostalCodeFormats } from './constants/postal-code-formats';
|
|
2
|
+
export class PostalCodeService {
|
|
3
|
+
static format(raw, country) {
|
|
4
|
+
const upper = raw.toUpperCase();
|
|
5
|
+
const format = PostalCodeFormats[country];
|
|
6
|
+
if (format === undefined)
|
|
7
|
+
return upper.replace(PostalCodeConfig.fallbackAllowed, '');
|
|
8
|
+
const compact = upper.replace(format.allowed, '');
|
|
9
|
+
if (format.groups.length === 0)
|
|
10
|
+
return upper.replace(PostalCodeConfig.fallbackAllowed, '');
|
|
11
|
+
const parts = [];
|
|
12
|
+
let cursor = 0;
|
|
13
|
+
for (const size of format.groups) {
|
|
14
|
+
if (cursor >= compact.length)
|
|
15
|
+
break;
|
|
16
|
+
parts.push(compact.slice(cursor, cursor + size));
|
|
17
|
+
cursor += size;
|
|
18
|
+
}
|
|
19
|
+
return parts.join(format.separator);
|
|
20
|
+
}
|
|
21
|
+
static maxLength(country) {
|
|
22
|
+
const format = PostalCodeFormats[country];
|
|
23
|
+
if (format === undefined || format.groups.length === 0)
|
|
24
|
+
return undefined;
|
|
25
|
+
return (format.groups.reduce((sum, size) => sum + size, 0) +
|
|
26
|
+
format.separator.length * (format.groups.length - 1));
|
|
27
|
+
}
|
|
28
|
+
static isValid(raw, country) {
|
|
29
|
+
const format = PostalCodeFormats[country];
|
|
30
|
+
if (raw.trim() === '' || format === undefined || format.groups.length === 0)
|
|
31
|
+
return true;
|
|
32
|
+
return PostalCodeService.format(raw, country).length === PostalCodeService.maxLength(country);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export class RecaptchaService {
|
|
2
|
+
static async verify(token, secretKey, remoteIp) {
|
|
3
|
+
if (!process.env.RECAPTCHA_VERIFY_URL) {
|
|
4
|
+
throw new Error('No reCAPTCHA verify URL found');
|
|
5
|
+
}
|
|
6
|
+
const params = new URLSearchParams({ secret: secretKey, response: token });
|
|
7
|
+
if (remoteIp)
|
|
8
|
+
params.set('remoteip', remoteIp);
|
|
9
|
+
const response = await fetch(process.env.RECAPTCHA_VERIFY_URL, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
12
|
+
body: params.toString(),
|
|
13
|
+
});
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
return { success: false, errorCodes: ['verification-request-failed'] };
|
|
16
|
+
}
|
|
17
|
+
const data = (await response.json());
|
|
18
|
+
return {
|
|
19
|
+
success: !!data.success,
|
|
20
|
+
errorCodes: data['error-codes'] ? data['error-codes'] : [],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { RelativeTimeConfig } from './constants/relative-time-config';
|
|
2
|
+
import { DateService } from './date-service';
|
|
3
|
+
export class RelativeTimeService {
|
|
4
|
+
static format(value, now, options) {
|
|
5
|
+
if (!DateService.isValid(value))
|
|
6
|
+
return value;
|
|
7
|
+
const seconds = (now.getTime() - DateService.getDate(value).valueOf()) /
|
|
8
|
+
RelativeTimeConfig.millisecondsPerSecond;
|
|
9
|
+
if (seconds < -RelativeTimeConfig.futureToleranceSeconds)
|
|
10
|
+
return options.formatDate(value);
|
|
11
|
+
if (seconds < RelativeTimeConfig.justNowSeconds)
|
|
12
|
+
return options.justNow;
|
|
13
|
+
const step = RelativeTimeConfig.units.find((unit) => seconds < unit.limitSeconds);
|
|
14
|
+
return step === undefined
|
|
15
|
+
? options.formatDate(value)
|
|
16
|
+
: new Intl.RelativeTimeFormat(options.locale, RelativeTimeConfig.format).format(-Math.max(1, Math.round(seconds / step.seconds)), step.unit);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { RequestClientConfig } from './constants/request-client-config';
|
|
2
|
+
export class RequestClientService {
|
|
3
|
+
static from(headers, options = {}) {
|
|
4
|
+
const address = RequestClientConfig.addressHeaders
|
|
5
|
+
.map((name) => headers.get(name))
|
|
6
|
+
.find((value) => value !== null && value.trim() !== '');
|
|
7
|
+
const agent = headers.get(RequestClientConfig.agentHeader);
|
|
8
|
+
return {
|
|
9
|
+
ipAddress: address === undefined
|
|
10
|
+
? null
|
|
11
|
+
: this._limit(address.split(RequestClientConfig.listSeparator)[0].trim(), options.ipAddressMaxLength),
|
|
12
|
+
userAgent: agent === null || agent.trim() === ''
|
|
13
|
+
? null
|
|
14
|
+
: this._limit(agent.trim(), options.userAgentMaxLength),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
static _limit(value, maxLength) {
|
|
18
|
+
return typeof maxLength === 'number' ? value.slice(0, maxLength) : value;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Base64Service } from './base64-service';
|
|
2
|
+
import { SignedTokenConfig } from './constants/signed-token-config';
|
|
3
|
+
export class SignedTokenService {
|
|
4
|
+
static async generateKeyPair() {
|
|
5
|
+
const pair = (await crypto.subtle.generateKey(SignedTokenConfig.keyAlgorithm, true, [
|
|
6
|
+
'sign',
|
|
7
|
+
'verify',
|
|
8
|
+
]));
|
|
9
|
+
const privateKey = await crypto.subtle.exportKey(SignedTokenConfig.privateKeyFormat, pair.privateKey);
|
|
10
|
+
const publicKey = await crypto.subtle.exportKey(SignedTokenConfig.publicKeyFormat, pair.publicKey);
|
|
11
|
+
return {
|
|
12
|
+
privateKey: Base64Service.encode(new Uint8Array(privateKey)),
|
|
13
|
+
publicKey: Base64Service.encode(new Uint8Array(publicKey)),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
static async sign(claims, privateKey) {
|
|
17
|
+
const header = {
|
|
18
|
+
alg: SignedTokenConfig.algorithm,
|
|
19
|
+
typ: SignedTokenConfig.type,
|
|
20
|
+
};
|
|
21
|
+
const signingInput = `${this._encodeJson(header)}${SignedTokenConfig.separator}${this._encodeJson(claims)}`;
|
|
22
|
+
const signature = await crypto.subtle.sign(SignedTokenConfig.signAlgorithm, await this._privateKey(privateKey), this._textEncoder.encode(signingInput));
|
|
23
|
+
return `${signingInput}${SignedTokenConfig.separator}${this._toBase64Url(new Uint8Array(signature))}`;
|
|
24
|
+
}
|
|
25
|
+
static async verify(token, publicKeys) {
|
|
26
|
+
const parts = token.split(SignedTokenConfig.separator);
|
|
27
|
+
if (parts.length !== SignedTokenConfig.partCount)
|
|
28
|
+
return null;
|
|
29
|
+
const [encodedHeader, encodedClaims, encodedSignature] = parts;
|
|
30
|
+
const header = this._decodeJson(encodedHeader);
|
|
31
|
+
if (header === null ||
|
|
32
|
+
header.alg !== SignedTokenConfig.algorithm ||
|
|
33
|
+
header.typ !== SignedTokenConfig.type)
|
|
34
|
+
return null;
|
|
35
|
+
const signature = this._fromBase64Url(encodedSignature);
|
|
36
|
+
if (signature === null)
|
|
37
|
+
return null;
|
|
38
|
+
const signingInput = this._textEncoder.encode(`${encodedHeader}${SignedTokenConfig.separator}${encodedClaims}`);
|
|
39
|
+
for (const publicKey of publicKeys) {
|
|
40
|
+
const key = await this._publicKey(publicKey).catch(() => null);
|
|
41
|
+
if (key === null)
|
|
42
|
+
continue;
|
|
43
|
+
const isValid = await crypto.subtle
|
|
44
|
+
.verify(SignedTokenConfig.signAlgorithm, key, signature, signingInput)
|
|
45
|
+
.catch(() => false);
|
|
46
|
+
if (isValid)
|
|
47
|
+
return this._decodeJson(encodedClaims);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
static async _privateKey(privateKey) {
|
|
52
|
+
const cached = this._privateKeys.get(privateKey);
|
|
53
|
+
if (cached !== undefined)
|
|
54
|
+
return cached;
|
|
55
|
+
const key = await crypto.subtle.importKey(SignedTokenConfig.privateKeyFormat, Base64Service.decode(privateKey), SignedTokenConfig.keyAlgorithm, false, ['sign']);
|
|
56
|
+
this._remember(this._privateKeys, privateKey, key);
|
|
57
|
+
return key;
|
|
58
|
+
}
|
|
59
|
+
static async _publicKey(publicKey) {
|
|
60
|
+
const cached = this._publicKeys.get(publicKey);
|
|
61
|
+
if (cached !== undefined)
|
|
62
|
+
return cached;
|
|
63
|
+
const key = await crypto.subtle.importKey(SignedTokenConfig.publicKeyFormat, Base64Service.decode(publicKey), SignedTokenConfig.keyAlgorithm, false, ['verify']);
|
|
64
|
+
this._remember(this._publicKeys, publicKey, key);
|
|
65
|
+
return key;
|
|
66
|
+
}
|
|
67
|
+
static _remember(cache, source, key) {
|
|
68
|
+
if (cache.size >= SignedTokenConfig.keyCacheLimit) {
|
|
69
|
+
const oldest = cache.keys().next().value;
|
|
70
|
+
if (oldest !== undefined)
|
|
71
|
+
cache.delete(oldest);
|
|
72
|
+
}
|
|
73
|
+
cache.set(source, key);
|
|
74
|
+
}
|
|
75
|
+
static _encodeJson(value) {
|
|
76
|
+
return this._toBase64Url(this._textEncoder.encode(JSON.stringify(value)));
|
|
77
|
+
}
|
|
78
|
+
static _decodeJson(encoded) {
|
|
79
|
+
const bytes = this._fromBase64Url(encoded);
|
|
80
|
+
if (bytes === null)
|
|
81
|
+
return null;
|
|
82
|
+
try {
|
|
83
|
+
const value = JSON.parse(this._textDecoder.decode(bytes));
|
|
84
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
85
|
+
? value
|
|
86
|
+
: null;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
static _toBase64Url(bytes) {
|
|
93
|
+
return Base64Service.encode(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
94
|
+
}
|
|
95
|
+
static _fromBase64Url(value) {
|
|
96
|
+
if (!/^[A-Za-z0-9_-]*$/.test(value))
|
|
97
|
+
return null;
|
|
98
|
+
const base64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
99
|
+
const padding = base64.length % 4 === 0 ? '' : '='.repeat(4 - (base64.length % 4));
|
|
100
|
+
try {
|
|
101
|
+
return Base64Service.decode(`${base64}${padding}`);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
SignedTokenService._textEncoder = new TextEncoder();
|
|
109
|
+
SignedTokenService._textDecoder = new TextDecoder();
|
|
110
|
+
SignedTokenService._privateKeys = new Map();
|
|
111
|
+
SignedTokenService._publicKeys = new Map();
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SocketTokenConfig } from './constants/socket-token-config';
|
|
2
|
+
import { DateService } from './date-service';
|
|
3
|
+
import { SignedTokenService } from './signed-token-service';
|
|
4
|
+
export class SocketTokenService {
|
|
5
|
+
static async issue(scope, privateKey, seconds = SocketTokenConfig.defaultSeconds) {
|
|
6
|
+
const claims = {
|
|
7
|
+
scope,
|
|
8
|
+
exp: DateService.nowUnixSeconds() + seconds,
|
|
9
|
+
};
|
|
10
|
+
return SignedTokenService.sign(claims, privateKey);
|
|
11
|
+
}
|
|
12
|
+
static async verify(token, publicKeys) {
|
|
13
|
+
const claims = await SignedTokenService.verify(token, publicKeys).catch(() => null);
|
|
14
|
+
if (claims === null ||
|
|
15
|
+
typeof claims.scope !== 'string' ||
|
|
16
|
+
claims.scope === '' ||
|
|
17
|
+
typeof claims.exp !== 'number' ||
|
|
18
|
+
claims.exp * SocketTokenConfig.millisecondsPerSecond < DateService.nowMilliseconds())
|
|
19
|
+
return null;
|
|
20
|
+
return claims.scope;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class URLService {
|
|
2
|
+
static saveToQueryParams(key, value) {
|
|
3
|
+
const url = this.updatePath(key, value.toString(), true);
|
|
4
|
+
window.history.replaceState({}, '', url);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
URLService.updatePath = (key, value, doRemoveParams) => {
|
|
8
|
+
const queryParams = new URLSearchParams(location.search);
|
|
9
|
+
if (doRemoveParams && !value) {
|
|
10
|
+
queryParams.delete(key);
|
|
11
|
+
}
|
|
12
|
+
else
|
|
13
|
+
queryParams.set(key, value);
|
|
14
|
+
return `${location.pathname}?${queryParams.toString()}`;
|
|
15
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { UserAgentConfig } from './constants/user-agent-config';
|
|
2
|
+
export class UserAgentService {
|
|
3
|
+
static browser(userAgent) {
|
|
4
|
+
if (userAgent === null)
|
|
5
|
+
return null;
|
|
6
|
+
const match = UserAgentConfig.browsers.find((browser) => browser.pattern.test(userAgent));
|
|
7
|
+
return match === undefined ? null : match.name;
|
|
8
|
+
}
|
|
9
|
+
static system(userAgent) {
|
|
10
|
+
if (userAgent === null)
|
|
11
|
+
return null;
|
|
12
|
+
const match = UserAgentConfig.systems.find((system) => system.pattern.test(userAgent));
|
|
13
|
+
return match === undefined ? null : match.name;
|
|
14
|
+
}
|
|
15
|
+
static describe(userAgent) {
|
|
16
|
+
const browser = UserAgentService.browser(userAgent);
|
|
17
|
+
const system = UserAgentService.system(userAgent);
|
|
18
|
+
if (browser === null && system === null)
|
|
19
|
+
return UserAgentConfig.unknown;
|
|
20
|
+
if (browser === null)
|
|
21
|
+
return String(system);
|
|
22
|
+
if (system === null)
|
|
23
|
+
return browser;
|
|
24
|
+
return `${browser}${UserAgentConfig.joiner}${system}`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { UserTokenConfig } from './constants/user-token-config';
|
|
2
|
+
import { DateService } from './date-service';
|
|
3
|
+
import { SignedTokenService } from './signed-token-service';
|
|
4
|
+
export class UserTokenService {
|
|
5
|
+
static async issue(subject, request, options) {
|
|
6
|
+
const issuedAt = this._now();
|
|
7
|
+
const lifetime = typeof options.lifetimeSeconds === 'number'
|
|
8
|
+
? Math.min(options.lifetimeSeconds, UserTokenConfig.maxLifetimeSeconds)
|
|
9
|
+
: UserTokenConfig.defaultLifetimeSeconds;
|
|
10
|
+
const claims = {
|
|
11
|
+
iss: options.issuer,
|
|
12
|
+
aud: options.audience,
|
|
13
|
+
sub: subject.id,
|
|
14
|
+
email: subject.email.trim().toLowerCase(),
|
|
15
|
+
name: subject.displayName,
|
|
16
|
+
htm: request.method.toUpperCase(),
|
|
17
|
+
htu: request.path,
|
|
18
|
+
iat: issuedAt,
|
|
19
|
+
exp: issuedAt + lifetime,
|
|
20
|
+
jti: crypto.randomUUID(),
|
|
21
|
+
};
|
|
22
|
+
return SignedTokenService.sign(claims, options.privateKey);
|
|
23
|
+
}
|
|
24
|
+
static async verify(token, request, options) {
|
|
25
|
+
const claims = await SignedTokenService.verify(token, options.publicKeys);
|
|
26
|
+
if (claims === null || !this._isWellFormed(claims))
|
|
27
|
+
return null;
|
|
28
|
+
const now = this._now();
|
|
29
|
+
const maxLifetime = typeof options.maxLifetimeSeconds === 'number'
|
|
30
|
+
? options.maxLifetimeSeconds
|
|
31
|
+
: UserTokenConfig.maxLifetimeSeconds;
|
|
32
|
+
if (claims.iss !== options.issuer ||
|
|
33
|
+
claims.aud !== options.audience ||
|
|
34
|
+
claims.htm !== request.method.toUpperCase() ||
|
|
35
|
+
claims.htu !== request.path ||
|
|
36
|
+
claims.iat > now + UserTokenConfig.clockSkewSeconds ||
|
|
37
|
+
claims.exp <= now - UserTokenConfig.clockSkewSeconds ||
|
|
38
|
+
claims.exp - claims.iat > maxLifetime)
|
|
39
|
+
return null;
|
|
40
|
+
return claims;
|
|
41
|
+
}
|
|
42
|
+
static _isWellFormed(claims) {
|
|
43
|
+
return (typeof claims.iss === 'string' &&
|
|
44
|
+
typeof claims.aud === 'string' &&
|
|
45
|
+
typeof claims.sub === 'string' &&
|
|
46
|
+
typeof claims.email === 'string' &&
|
|
47
|
+
claims.email !== '' &&
|
|
48
|
+
typeof claims.name === 'string' &&
|
|
49
|
+
typeof claims.htm === 'string' &&
|
|
50
|
+
typeof claims.htu === 'string' &&
|
|
51
|
+
typeof claims.iat === 'number' &&
|
|
52
|
+
typeof claims.exp === 'number' &&
|
|
53
|
+
typeof claims.jti === 'string' &&
|
|
54
|
+
claims.jti !== '');
|
|
55
|
+
}
|
|
56
|
+
static _now() {
|
|
57
|
+
return DateService.nowUnixSeconds();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ValidationConfig } from './constants/validation-config';
|
|
2
|
+
import { PhoneService } from './phone-service';
|
|
3
|
+
export class ValidationService {
|
|
4
|
+
static isEmail(value) {
|
|
5
|
+
return ValidationConfig.email.test(value.trim());
|
|
6
|
+
}
|
|
7
|
+
static withProtocol(value) {
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (trimmed === '' || ValidationConfig.urlProtocolPattern.test(trimmed))
|
|
10
|
+
return trimmed;
|
|
11
|
+
return `${ValidationConfig.urlProtocol}${trimmed}`;
|
|
12
|
+
}
|
|
13
|
+
static isWebUrl(value) {
|
|
14
|
+
try {
|
|
15
|
+
const url = new URL(ValidationService.withProtocol(value));
|
|
16
|
+
return (ValidationConfig.webProtocols.some((protocol) => protocol === url.protocol) &&
|
|
17
|
+
url.hostname.includes(ValidationConfig.hostSeparator));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
static isPhone(value, country) {
|
|
24
|
+
return value.trim() !== '' && PhoneService.isValid(value, country);
|
|
25
|
+
}
|
|
26
|
+
static isUuid(value) {
|
|
27
|
+
return ValidationConfig.uuid.test(value.trim());
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|