@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,10 @@
|
|
|
1
|
+
export const PostalCodeFormats = {
|
|
2
|
+
CA: { allowed: /[^A-Z0-9]/g, groups: [3, 3], separator: ' ' },
|
|
3
|
+
GB: { allowed: /[^A-Z0-9]/g, groups: [], separator: ' ' },
|
|
4
|
+
IN: { allowed: /[^0-9]/g, groups: [6], separator: '' },
|
|
5
|
+
NL: { allowed: /[^A-Z0-9]/g, groups: [4, 2], separator: ' ' },
|
|
6
|
+
US: { allowed: /[^0-9]/g, groups: [5, 4], separator: '-' },
|
|
7
|
+
};
|
|
8
|
+
export const PostalCodeConfig = {
|
|
9
|
+
fallbackAllowed: /[^A-Z0-9 -]/g,
|
|
10
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const RelativeTimeConfig = {
|
|
2
|
+
futureToleranceSeconds: 300,
|
|
3
|
+
justNowSeconds: 45,
|
|
4
|
+
millisecondsPerSecond: 1000,
|
|
5
|
+
format: { numeric: 'auto' },
|
|
6
|
+
units: [
|
|
7
|
+
{ limitSeconds: 3600, seconds: 60, unit: 'minute' },
|
|
8
|
+
{ limitSeconds: 86400, seconds: 3600, unit: 'hour' },
|
|
9
|
+
{ limitSeconds: 604800, seconds: 86400, unit: 'day' },
|
|
10
|
+
],
|
|
11
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const SignedTokenConfig = {
|
|
2
|
+
algorithm: 'ES256',
|
|
3
|
+
type: 'JWT',
|
|
4
|
+
keyAlgorithm: { name: 'ECDSA', namedCurve: 'P-256' },
|
|
5
|
+
signAlgorithm: { name: 'ECDSA', hash: 'SHA-256' },
|
|
6
|
+
privateKeyFormat: 'pkcs8',
|
|
7
|
+
publicKeyFormat: 'spki',
|
|
8
|
+
separator: '.',
|
|
9
|
+
partCount: 3,
|
|
10
|
+
keyCacheLimit: 20,
|
|
11
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const UserAgentConfig = {
|
|
2
|
+
browsers: [
|
|
3
|
+
{ name: 'Edge', pattern: /Edg\//i },
|
|
4
|
+
{ name: 'Opera', pattern: /OPR\//i },
|
|
5
|
+
{ name: 'Firefox', pattern: /Firefox\//i },
|
|
6
|
+
{ name: 'Chrome', pattern: /(Chrome|CriOS)\//i },
|
|
7
|
+
{ name: 'Safari', pattern: /Safari\//i },
|
|
8
|
+
],
|
|
9
|
+
systems: [
|
|
10
|
+
{ name: 'iOS', pattern: /iPhone|iPad|iPod/i },
|
|
11
|
+
{ name: 'Android', pattern: /Android/i },
|
|
12
|
+
{ name: 'Windows', pattern: /Windows/i },
|
|
13
|
+
{ name: 'macOS', pattern: /Mac OS X|Macintosh/i },
|
|
14
|
+
{ name: 'Linux', pattern: /Linux/i },
|
|
15
|
+
],
|
|
16
|
+
unknown: 'Unknown device',
|
|
17
|
+
joiner: ' on ',
|
|
18
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const ValidationConfig = {
|
|
2
|
+
email: /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
|
|
3
|
+
webProtocols: ['http:', 'https:'],
|
|
4
|
+
hostSeparator: '.',
|
|
5
|
+
urlProtocol: 'https://',
|
|
6
|
+
urlProtocolPattern: /^[a-z][a-z0-9+.-]*:\/\//i,
|
|
7
|
+
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
8
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Base64Service } from './base64-service';
|
|
2
|
+
export class CryptoService {
|
|
3
|
+
static requireKey(key, name) {
|
|
4
|
+
if (typeof key !== 'string' || key.trim().length === 0) {
|
|
5
|
+
throw new Error(`${name} is required and must not be empty`);
|
|
6
|
+
}
|
|
7
|
+
return key;
|
|
8
|
+
}
|
|
9
|
+
static _getAdditionalData() {
|
|
10
|
+
return this._textEncoder.encode(this._aad);
|
|
11
|
+
}
|
|
12
|
+
static _parsePayload(encryptedPayload) {
|
|
13
|
+
const payload = JSON.parse(encryptedPayload);
|
|
14
|
+
const isDerived = payload.v === this._payloadVersion && Boolean(payload.salt);
|
|
15
|
+
const isDataKey = payload.v === this._dataKeyPayloadVersion;
|
|
16
|
+
if (payload.alg !== this._payloadAlgorithm ||
|
|
17
|
+
!payload.iv ||
|
|
18
|
+
!payload.ciphertext ||
|
|
19
|
+
(!isDerived && !isDataKey)) {
|
|
20
|
+
throw new Error('Encrypted payload is malformed');
|
|
21
|
+
}
|
|
22
|
+
return payload;
|
|
23
|
+
}
|
|
24
|
+
static async _deriveKey(cryptoKey, salt) {
|
|
25
|
+
const baseKey = await crypto.subtle.importKey('raw', this._textEncoder.encode(cryptoKey), 'PBKDF2', false, ['deriveKey']);
|
|
26
|
+
return await crypto.subtle.deriveKey({
|
|
27
|
+
name: 'PBKDF2',
|
|
28
|
+
hash: 'SHA-256',
|
|
29
|
+
salt: salt,
|
|
30
|
+
iterations: this._iterations,
|
|
31
|
+
}, baseKey, {
|
|
32
|
+
name: this._algorithm,
|
|
33
|
+
length: this._keyLength,
|
|
34
|
+
}, false, ['encrypt', 'decrypt']);
|
|
35
|
+
}
|
|
36
|
+
static async encryptPayload(payload, cryptoKey) {
|
|
37
|
+
this.requireKey(cryptoKey, 'Encryption key');
|
|
38
|
+
const iv = crypto.getRandomValues(new Uint8Array(this._ivLength));
|
|
39
|
+
const salt = crypto.getRandomValues(new Uint8Array(this._saltLength));
|
|
40
|
+
const key = await this._deriveKey(cryptoKey, salt);
|
|
41
|
+
const encryptedBuffer = await crypto.subtle.encrypt({
|
|
42
|
+
name: this._algorithm,
|
|
43
|
+
iv,
|
|
44
|
+
additionalData: this._getAdditionalData(),
|
|
45
|
+
}, key, this._textEncoder.encode(JSON.stringify(payload)));
|
|
46
|
+
const serializedPayload = {
|
|
47
|
+
v: this._payloadVersion,
|
|
48
|
+
alg: this._payloadAlgorithm,
|
|
49
|
+
iv: Base64Service.encode(iv),
|
|
50
|
+
salt: Base64Service.encode(salt),
|
|
51
|
+
ciphertext: Base64Service.encode(new Uint8Array(encryptedBuffer)),
|
|
52
|
+
};
|
|
53
|
+
return JSON.stringify(serializedPayload);
|
|
54
|
+
}
|
|
55
|
+
static async _importDataKey(dataKey) {
|
|
56
|
+
return await crypto.subtle.importKey('raw', Base64Service.decode(dataKey), { name: this._algorithm, length: this._keyLength }, false, ['encrypt', 'decrypt']);
|
|
57
|
+
}
|
|
58
|
+
static generateDataKey() {
|
|
59
|
+
return Base64Service.encode(crypto.getRandomValues(new Uint8Array(this._dataKeyLength)));
|
|
60
|
+
}
|
|
61
|
+
static async encryptPayloadWithDataKey(payload, dataKey) {
|
|
62
|
+
this.requireKey(dataKey, 'Data key');
|
|
63
|
+
const iv = crypto.getRandomValues(new Uint8Array(this._ivLength));
|
|
64
|
+
const encryptedBuffer = await crypto.subtle.encrypt({ name: this._algorithm, iv, additionalData: this._getAdditionalData() }, await this._importDataKey(dataKey), this._textEncoder.encode(JSON.stringify(payload)));
|
|
65
|
+
return JSON.stringify({
|
|
66
|
+
v: this._dataKeyPayloadVersion,
|
|
67
|
+
alg: this._payloadAlgorithm,
|
|
68
|
+
iv: Base64Service.encode(iv),
|
|
69
|
+
ciphertext: Base64Service.encode(new Uint8Array(encryptedBuffer)),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
static async decryptPayloadWithDataKey(encryptedPayload, dataKey) {
|
|
73
|
+
this.requireKey(dataKey, 'Data key');
|
|
74
|
+
const payload = this._parsePayload(encryptedPayload);
|
|
75
|
+
const decryptedBuffer = await crypto.subtle.decrypt({
|
|
76
|
+
name: this._algorithm,
|
|
77
|
+
iv: Base64Service.decode(payload.iv),
|
|
78
|
+
additionalData: this._getAdditionalData(),
|
|
79
|
+
}, await this._importDataKey(dataKey), Base64Service.decode(payload.ciphertext));
|
|
80
|
+
return JSON.parse(this._textDecoder.decode(decryptedBuffer));
|
|
81
|
+
}
|
|
82
|
+
static async encryptValueWithDataKey(value, dataKey) {
|
|
83
|
+
return await this.encryptPayloadWithDataKey({ value }, dataKey);
|
|
84
|
+
}
|
|
85
|
+
static async decryptValueWithDataKey(encryptedPayload, dataKey) {
|
|
86
|
+
const { value } = await this.decryptPayloadWithDataKey(encryptedPayload, dataKey);
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
static async encryptValue(value, cryptoKey) {
|
|
90
|
+
return await this.encryptPayload({ value }, cryptoKey);
|
|
91
|
+
}
|
|
92
|
+
static async decryptValue(encryptedPayload, cryptoKey) {
|
|
93
|
+
const { value } = await this.decryptPayload(encryptedPayload, cryptoKey);
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
static isEncryptedPayload(candidate) {
|
|
97
|
+
try {
|
|
98
|
+
this._parsePayload(candidate);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
static async decryptPayload(encryptedPayload, cryptoKey) {
|
|
106
|
+
this.requireKey(cryptoKey, 'Encryption key');
|
|
107
|
+
const payload = this._parsePayload(encryptedPayload);
|
|
108
|
+
const key = await this._deriveKey(cryptoKey, Base64Service.decode(payload.salt));
|
|
109
|
+
const decryptedBuffer = await crypto.subtle.decrypt({
|
|
110
|
+
name: this._algorithm,
|
|
111
|
+
iv: Base64Service.decode(payload.iv),
|
|
112
|
+
additionalData: this._getAdditionalData(),
|
|
113
|
+
}, key, Base64Service.decode(payload.ciphertext));
|
|
114
|
+
return JSON.parse(this._textDecoder.decode(decryptedBuffer));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
CryptoService._algorithm = 'AES-GCM';
|
|
118
|
+
CryptoService._keyLength = 256;
|
|
119
|
+
CryptoService._iterations = 100000;
|
|
120
|
+
CryptoService._ivLength = 12;
|
|
121
|
+
CryptoService._saltLength = 16;
|
|
122
|
+
CryptoService._textEncoder = new TextEncoder();
|
|
123
|
+
CryptoService._textDecoder = new TextDecoder();
|
|
124
|
+
CryptoService._payloadVersion = 1;
|
|
125
|
+
CryptoService._dataKeyPayloadVersion = 2;
|
|
126
|
+
CryptoService._dataKeyLength = 32;
|
|
127
|
+
CryptoService._payloadAlgorithm = 'A256GCM';
|
|
128
|
+
CryptoService._aad = 'encrypted-form:v1:a256gcm';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
import timezone from 'dayjs/plugin/timezone.js';
|
|
3
|
+
import utc from 'dayjs/plugin/utc.js';
|
|
4
|
+
dayjs.extend(utc);
|
|
5
|
+
dayjs.extend(timezone);
|
|
6
|
+
export class DateService {
|
|
7
|
+
static getDate(date) {
|
|
8
|
+
if (date)
|
|
9
|
+
return dayjs(date);
|
|
10
|
+
return dayjs();
|
|
11
|
+
}
|
|
12
|
+
static getToday() {
|
|
13
|
+
return dayjs().toISOString();
|
|
14
|
+
}
|
|
15
|
+
static getTomorrow() {
|
|
16
|
+
return dayjs().add(1, 'day').toISOString();
|
|
17
|
+
}
|
|
18
|
+
static formatSecondsToTime(seconds) {
|
|
19
|
+
const hours = Math.floor(seconds / 3600);
|
|
20
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
21
|
+
const remainingSeconds = Math.floor(seconds % 60);
|
|
22
|
+
if (hours > 0) {
|
|
23
|
+
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
|
|
24
|
+
}
|
|
25
|
+
else if (minutes > 0) {
|
|
26
|
+
return `${String(minutes).padStart(2, '0')}:${String(remainingSeconds).padStart(2, '0')}`;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return `00:${String(remainingSeconds).padStart(2, '0')}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
static calculateTimeRemaining(date, placeholder) {
|
|
33
|
+
const currentDate = dayjs();
|
|
34
|
+
const targetDate = dayjs(date);
|
|
35
|
+
const diffSeconds = targetDate.diff(currentDate, 'second');
|
|
36
|
+
if (diffSeconds <= 0) {
|
|
37
|
+
return placeholder;
|
|
38
|
+
}
|
|
39
|
+
const hours = Math.floor(diffSeconds / 3600);
|
|
40
|
+
const minutes = Math.floor((diffSeconds % 3600) / 60);
|
|
41
|
+
const seconds = diffSeconds % 60;
|
|
42
|
+
if (hours)
|
|
43
|
+
return `${hours}h ${minutes}m ${seconds}s`;
|
|
44
|
+
if (minutes)
|
|
45
|
+
return `${minutes}m ${seconds}s`;
|
|
46
|
+
return `${seconds}s`;
|
|
47
|
+
}
|
|
48
|
+
static getNextWeek() {
|
|
49
|
+
return dayjs().add(1, 'week').toISOString();
|
|
50
|
+
}
|
|
51
|
+
static getNextMonth() {
|
|
52
|
+
return dayjs().add(1, 'month').toISOString();
|
|
53
|
+
}
|
|
54
|
+
static getNextYear() {
|
|
55
|
+
return dayjs().add(1, 'year').toISOString();
|
|
56
|
+
}
|
|
57
|
+
static formatDate(date, format) {
|
|
58
|
+
return dayjs(date).format(format);
|
|
59
|
+
}
|
|
60
|
+
static formatDateInTimezone(date, zone, format) {
|
|
61
|
+
return dayjs(date).tz(zone).format(format);
|
|
62
|
+
}
|
|
63
|
+
static calendarDateInTimezone(date, zone) {
|
|
64
|
+
return dayjs(date).tz(zone).format('YYYY-MM-DD');
|
|
65
|
+
}
|
|
66
|
+
static todayInTimezone(zone) {
|
|
67
|
+
return dayjs().tz(zone).format('YYYY-MM-DD');
|
|
68
|
+
}
|
|
69
|
+
static formatDateToString(date, short) {
|
|
70
|
+
return dayjs(date).format(short ? 'DD MMM, YYYY' : 'DD MMMM, YYYY');
|
|
71
|
+
}
|
|
72
|
+
static formatTimeToString(date, short) {
|
|
73
|
+
return dayjs(date).format(short ? 'DD MMM, YYYY @ hh:MM:ss A' : 'DD MMMM, YYYY @ hh:MM:ss A');
|
|
74
|
+
}
|
|
75
|
+
static diffInMins(startDate, endDate) {
|
|
76
|
+
const start = dayjs(startDate);
|
|
77
|
+
const end = dayjs(endDate);
|
|
78
|
+
return end.diff(start, 'minutes');
|
|
79
|
+
}
|
|
80
|
+
static nowMilliseconds() {
|
|
81
|
+
return dayjs().valueOf();
|
|
82
|
+
}
|
|
83
|
+
static nowUnixSeconds() {
|
|
84
|
+
return dayjs().unix();
|
|
85
|
+
}
|
|
86
|
+
static fromParts(year, monthIndex, day) {
|
|
87
|
+
return dayjs(`${String(year).padStart(4, '0')}-${String(monthIndex + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
|
|
88
|
+
}
|
|
89
|
+
static isValid(date) {
|
|
90
|
+
return dayjs(date).isValid();
|
|
91
|
+
}
|
|
92
|
+
static addDays(date, days) {
|
|
93
|
+
return dayjs.utc(date).add(days, 'day').format('YYYY-MM-DD');
|
|
94
|
+
}
|
|
95
|
+
static addDaysToInstant(instant, days) {
|
|
96
|
+
return dayjs.utc(instant).add(days, 'day').toDate();
|
|
97
|
+
}
|
|
98
|
+
static startOfUtcDay(instant) {
|
|
99
|
+
return dayjs.utc(instant).startOf('day').toDate();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { EmailPreviewConfig } from './constants/email-preview-config';
|
|
2
|
+
import { DateService } from './date-service';
|
|
3
|
+
export class EmailPreviewService {
|
|
4
|
+
static escape(value) {
|
|
5
|
+
return value
|
|
6
|
+
.replace(/&/g, '&')
|
|
7
|
+
.replace(/</g, '<')
|
|
8
|
+
.replace(/>/g, '>')
|
|
9
|
+
.replace(/"/g, '"')
|
|
10
|
+
.replace(/'/g, ''');
|
|
11
|
+
}
|
|
12
|
+
static hasContent(input) {
|
|
13
|
+
return ((input.html !== null && input.html.trim() !== '') ||
|
|
14
|
+
(input.text !== null && input.text.trim() !== ''));
|
|
15
|
+
}
|
|
16
|
+
static receivedOf(input) {
|
|
17
|
+
return DateService.formatDateInTimezone(input.receivedAt, input.timezone, EmailPreviewConfig.dateFormat);
|
|
18
|
+
}
|
|
19
|
+
static subjectOf(input) {
|
|
20
|
+
return input.subject === null || input.subject.trim() === ''
|
|
21
|
+
? EmailPreviewConfig.fallbackSubject
|
|
22
|
+
: input.subject.trim();
|
|
23
|
+
}
|
|
24
|
+
static toText(input) {
|
|
25
|
+
if (input.text !== null && input.text.trim() !== '')
|
|
26
|
+
return input.text.trim();
|
|
27
|
+
if (input.html === null)
|
|
28
|
+
return '';
|
|
29
|
+
return EmailPreviewService.fromHtml(input.html);
|
|
30
|
+
}
|
|
31
|
+
static toDocument(input) {
|
|
32
|
+
const subject = EmailPreviewService.subjectOf(input);
|
|
33
|
+
const body = input.html !== null && input.html.trim() !== ''
|
|
34
|
+
? EmailPreviewService.body(input.html)
|
|
35
|
+
: `<pre style="white-space:pre-wrap;font-family:inherit;margin:0;">${EmailPreviewService.escape(input.text === null ? '' : input.text)}</pre>`;
|
|
36
|
+
return `<!doctype html>
|
|
37
|
+
<html lang="en">
|
|
38
|
+
<head>
|
|
39
|
+
<meta charset="utf-8" />
|
|
40
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
41
|
+
<title>${EmailPreviewService.escape(subject)}</title>
|
|
42
|
+
</head>
|
|
43
|
+
<body style="margin:0;padding:24px;background:#f5f7fa;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#101828;">
|
|
44
|
+
<div style="max-width:760px;margin:0 auto;background:#ffffff;border:1px solid #e4e8ee;border-radius:12px;overflow:hidden;">
|
|
45
|
+
<div style="padding:16px 20px;border-bottom:1px solid #e4e8ee;background:#f9fafb;">
|
|
46
|
+
<div style="font-size:15px;font-weight:700;">${EmailPreviewService.escape(subject)}</div>
|
|
47
|
+
<div style="margin-top:4px;font-size:12px;color:#667085;">From ${EmailPreviewService.escape(input.from)} · ${EmailPreviewService.escape(EmailPreviewService.receivedOf(input))}</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div style="padding:20px;">${body}</div>
|
|
50
|
+
</div>
|
|
51
|
+
</body>
|
|
52
|
+
</html>`;
|
|
53
|
+
}
|
|
54
|
+
static toImageMarkup(input) {
|
|
55
|
+
const { page } = EmailPreviewConfig;
|
|
56
|
+
const width = page.width - page.padding * 2;
|
|
57
|
+
const subject = EmailPreviewService.clip(EmailPreviewService.subjectOf(input), Math.floor(width / (page.charWidth * page.subjectWidthRatio)));
|
|
58
|
+
const meta = EmailPreviewService.clip(`From ${input.from} · ${EmailPreviewService.receivedOf(input)}`, Math.floor(width / (page.charWidth * page.metaWidthRatio)));
|
|
59
|
+
const lines = EmailPreviewService.wrap(EmailPreviewService.toText(input), Math.floor(width / page.charWidth)).slice(0, page.maxLines);
|
|
60
|
+
const metaBaseline = page.padding + page.subjectSize + 10 + page.metaSize;
|
|
61
|
+
const bodyTop = metaBaseline + page.headerGap;
|
|
62
|
+
const height = Math.max(page.minHeight, bodyTop + lines.length * page.lineHeight + page.padding);
|
|
63
|
+
const body = lines
|
|
64
|
+
.map((line, index) => `<text x="${page.padding}" y="${bodyTop + index * page.lineHeight}" font-size="${page.bodySize}" fill="${page.bodyColour}">${EmailPreviewService.escape(line)}</text>`)
|
|
65
|
+
.join('');
|
|
66
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${page.width}" height="${height}" viewBox="0 0 ${page.width} ${height}" font-family="${page.fontFamily}">
|
|
67
|
+
<rect width="${page.width}" height="${height}" fill="${page.paperColour}" />
|
|
68
|
+
<rect x="0" y="0" width="${page.width}" height="${page.bandHeight}" fill="${page.bandColour}" />
|
|
69
|
+
<text x="${page.padding}" y="${page.padding + page.subjectSize}" font-size="${page.subjectSize}" font-weight="700" fill="${page.subjectColour}">${EmailPreviewService.escape(subject)}</text>
|
|
70
|
+
<text x="${page.padding}" y="${metaBaseline}" font-size="${page.metaSize}" fill="${page.metaColour}">${EmailPreviewService.escape(meta)}</text>
|
|
71
|
+
<line x1="${page.padding}" y1="${metaBaseline + 8}" x2="${page.width - page.padding}" y2="${metaBaseline + 8}" stroke="${page.ruleColour}" stroke-width="1" />
|
|
72
|
+
${body}
|
|
73
|
+
</svg>`;
|
|
74
|
+
}
|
|
75
|
+
static clip(value, limit) {
|
|
76
|
+
return value.length <= limit ? value : `${value.slice(0, Math.max(limit - 1, 1))}…`;
|
|
77
|
+
}
|
|
78
|
+
static wrap(text, limit) {
|
|
79
|
+
const lines = [];
|
|
80
|
+
for (const paragraph of text.split('\n')) {
|
|
81
|
+
if (paragraph.trim() === '') {
|
|
82
|
+
lines.push('');
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
let current = '';
|
|
86
|
+
for (const word of paragraph.trim().split(/\s+/)) {
|
|
87
|
+
const candidate = current === '' ? word : `${current} ${word}`;
|
|
88
|
+
if (candidate.length <= limit) {
|
|
89
|
+
current = candidate;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (current !== '')
|
|
93
|
+
lines.push(current);
|
|
94
|
+
current = word.length <= limit ? word : EmailPreviewService.push(lines, word, limit);
|
|
95
|
+
}
|
|
96
|
+
lines.push(current);
|
|
97
|
+
}
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
100
|
+
static push(lines, word, limit) {
|
|
101
|
+
let rest = word;
|
|
102
|
+
while (rest.length > limit) {
|
|
103
|
+
lines.push(rest.slice(0, limit));
|
|
104
|
+
rest = rest.slice(limit);
|
|
105
|
+
}
|
|
106
|
+
return rest;
|
|
107
|
+
}
|
|
108
|
+
static fromHtml(html) {
|
|
109
|
+
return EmailPreviewService.decode(html
|
|
110
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
111
|
+
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
112
|
+
.replace(/<head\b[^>]*>[\s\S]*?<\/head>/gi, '')
|
|
113
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
114
|
+
.replace(/<\/(p|div|tr|li|h[1-6]|table|section|header|footer)>/gi, '\n')
|
|
115
|
+
.replace(/<[^>]+>/g, ' '))
|
|
116
|
+
.replace(/[^\S\n]+/g, ' ')
|
|
117
|
+
.replace(/ *\n */g, '\n')
|
|
118
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
119
|
+
.trim();
|
|
120
|
+
}
|
|
121
|
+
static decode(value) {
|
|
122
|
+
return value
|
|
123
|
+
.replace(/ /gi, ' ')
|
|
124
|
+
.replace(/&/gi, '&')
|
|
125
|
+
.replace(/</gi, '<')
|
|
126
|
+
.replace(/>/gi, '>')
|
|
127
|
+
.replace(/"/gi, '"')
|
|
128
|
+
.replace(/�?39;|'|'/gi, "'")
|
|
129
|
+
.replace(/’|’/gi, '’')
|
|
130
|
+
.replace(/—|—/gi, '—')
|
|
131
|
+
.replace(/–|–/gi, '–');
|
|
132
|
+
}
|
|
133
|
+
static body(html) {
|
|
134
|
+
const withoutScripts = html
|
|
135
|
+
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
136
|
+
.replace(/<iframe\b[^>]*>[\s\S]*?<\/iframe>/gi, '')
|
|
137
|
+
.replace(/\son[a-z]+\s*=\s*"[^"]*"/gi, '')
|
|
138
|
+
.replace(/\son[a-z]+\s*=\s*'[^']*'/gi, '');
|
|
139
|
+
const inner = withoutScripts.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
140
|
+
return inner === null ? withoutScripts : inner[1];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { EnvelopeCryptoService } from './envelope-crypto-service';
|
|
2
|
+
export class EncryptedFieldsService {
|
|
3
|
+
static _isBlank(value) {
|
|
4
|
+
return value === null || value === undefined || value === '';
|
|
5
|
+
}
|
|
6
|
+
static async encrypt(row, spec, wrappedDataKey, masterKey) {
|
|
7
|
+
const encrypted = { ...row };
|
|
8
|
+
for (const field of (spec.values !== null && spec.values !== undefined
|
|
9
|
+
? spec.values
|
|
10
|
+
: [])) {
|
|
11
|
+
const value = encrypted[field];
|
|
12
|
+
if (this._isBlank(value))
|
|
13
|
+
continue;
|
|
14
|
+
encrypted[field] = await EnvelopeCryptoService.encryptValue(value, wrappedDataKey, masterKey);
|
|
15
|
+
}
|
|
16
|
+
for (const field of (spec.json !== null && spec.json !== undefined
|
|
17
|
+
? spec.json
|
|
18
|
+
: [])) {
|
|
19
|
+
const value = encrypted[field];
|
|
20
|
+
if (this._isBlank(value))
|
|
21
|
+
continue;
|
|
22
|
+
encrypted[field] = await EnvelopeCryptoService.encryptPayload(value, wrappedDataKey, masterKey);
|
|
23
|
+
}
|
|
24
|
+
return encrypted;
|
|
25
|
+
}
|
|
26
|
+
static async decrypt(row, spec, wrappedDataKey, masterKey) {
|
|
27
|
+
const decrypted = { ...row };
|
|
28
|
+
for (const field of (spec.values !== null && spec.values !== undefined
|
|
29
|
+
? spec.values
|
|
30
|
+
: [])) {
|
|
31
|
+
const value = decrypted[field];
|
|
32
|
+
if (this._isBlank(value))
|
|
33
|
+
continue;
|
|
34
|
+
decrypted[field] = await EnvelopeCryptoService.decryptValue(value, wrappedDataKey, masterKey);
|
|
35
|
+
}
|
|
36
|
+
for (const field of (spec.json !== null && spec.json !== undefined
|
|
37
|
+
? spec.json
|
|
38
|
+
: [])) {
|
|
39
|
+
const value = decrypted[field];
|
|
40
|
+
if (this._isBlank(value))
|
|
41
|
+
continue;
|
|
42
|
+
decrypted[field] = await EnvelopeCryptoService.decryptPayload(value, wrappedDataKey, masterKey);
|
|
43
|
+
}
|
|
44
|
+
return decrypted;
|
|
45
|
+
}
|
|
46
|
+
static async decryptMany(rows, spec, wrappedDataKey, masterKey) {
|
|
47
|
+
return await Promise.all(rows.map(async (row) => await this.decrypt(row, spec, wrappedDataKey, masterKey)));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { CryptoService } from './crypto-service';
|
|
2
|
+
export class EnvelopeCryptoService {
|
|
3
|
+
static _remember(wrappedDataKey, dataKey) {
|
|
4
|
+
if (this._cache.size >= this._cacheLimit) {
|
|
5
|
+
const oldest = this._cache.keys().next().value;
|
|
6
|
+
if (oldest)
|
|
7
|
+
this._cache.delete(oldest);
|
|
8
|
+
}
|
|
9
|
+
this._cache.set(wrappedDataKey, dataKey);
|
|
10
|
+
}
|
|
11
|
+
static async createDataKey(masterKey) {
|
|
12
|
+
CryptoService.requireKey(masterKey, 'Master key');
|
|
13
|
+
const dataKey = CryptoService.generateDataKey();
|
|
14
|
+
const wrappedDataKey = await CryptoService.encryptValue(dataKey, masterKey);
|
|
15
|
+
this._remember(wrappedDataKey, dataKey);
|
|
16
|
+
return { dataKey, wrappedDataKey };
|
|
17
|
+
}
|
|
18
|
+
static async unwrapDataKey(wrappedDataKey, masterKey) {
|
|
19
|
+
CryptoService.requireKey(masterKey, 'Master key');
|
|
20
|
+
const cached = this._cache.get(wrappedDataKey);
|
|
21
|
+
if (cached)
|
|
22
|
+
return cached;
|
|
23
|
+
const dataKey = await CryptoService.decryptValue(wrappedDataKey, masterKey);
|
|
24
|
+
this._remember(wrappedDataKey, dataKey);
|
|
25
|
+
return dataKey;
|
|
26
|
+
}
|
|
27
|
+
static async rewrapDataKey(wrappedDataKey, currentMasterKey, nextMasterKey) {
|
|
28
|
+
const dataKey = await this.unwrapDataKey(wrappedDataKey, currentMasterKey);
|
|
29
|
+
return await CryptoService.encryptValue(dataKey, nextMasterKey);
|
|
30
|
+
}
|
|
31
|
+
static async encryptValue(value, wrappedDataKey, masterKey) {
|
|
32
|
+
return await CryptoService.encryptValueWithDataKey(value, await this.unwrapDataKey(wrappedDataKey, masterKey));
|
|
33
|
+
}
|
|
34
|
+
static async decryptValue(encryptedPayload, wrappedDataKey, masterKey) {
|
|
35
|
+
return await CryptoService.decryptValueWithDataKey(encryptedPayload, await this.unwrapDataKey(wrappedDataKey, masterKey));
|
|
36
|
+
}
|
|
37
|
+
static async encryptPayload(payload, wrappedDataKey, masterKey) {
|
|
38
|
+
return await CryptoService.encryptPayloadWithDataKey(payload, await this.unwrapDataKey(wrappedDataKey, masterKey));
|
|
39
|
+
}
|
|
40
|
+
static async decryptPayload(encryptedPayload, wrappedDataKey, masterKey) {
|
|
41
|
+
return await CryptoService.decryptPayloadWithDataKey(encryptedPayload, await this.unwrapDataKey(wrappedDataKey, masterKey));
|
|
42
|
+
}
|
|
43
|
+
static forget(wrappedDataKey) {
|
|
44
|
+
this._cache.delete(wrappedDataKey);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
EnvelopeCryptoService._cache = new Map();
|
|
48
|
+
EnvelopeCryptoService._cacheLimit = 500;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ErrorResponseConfig } from './constants/error-response-config';
|
|
2
|
+
export class ErrorResponseService {
|
|
3
|
+
static create(message, code) {
|
|
4
|
+
return Object.assign(new Error(message), { code });
|
|
5
|
+
}
|
|
6
|
+
static code(error) {
|
|
7
|
+
if (typeof error !== 'object' || error === null || !('code' in error))
|
|
8
|
+
return null;
|
|
9
|
+
return typeof error.code === 'number' ? error.code : null;
|
|
10
|
+
}
|
|
11
|
+
static isClientError(status) {
|
|
12
|
+
return (status >= ErrorResponseConfig.clientErrorStart &&
|
|
13
|
+
status < ErrorResponseConfig.serverErrorStart);
|
|
14
|
+
}
|
|
15
|
+
static pageContent(status, message, base) {
|
|
16
|
+
return ErrorResponseService.isClientError(status) && message !== undefined && message !== ''
|
|
17
|
+
? { ...base, description: message }
|
|
18
|
+
: base;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FileSizeConfig } from './constants/file-size-config';
|
|
2
|
+
export class FileSizeService {
|
|
3
|
+
static format(bytes) {
|
|
4
|
+
let value = Math.max(0, bytes);
|
|
5
|
+
let unit = 0;
|
|
6
|
+
while (value >= FileSizeConfig.step && unit < FileSizeConfig.units.length - 1) {
|
|
7
|
+
value /= FileSizeConfig.step;
|
|
8
|
+
unit += 1;
|
|
9
|
+
}
|
|
10
|
+
const shown = unit === 0 || value >= FileSizeConfig.wholeFrom
|
|
11
|
+
? Math.round(value)
|
|
12
|
+
: Math.round(value * 10) / 10;
|
|
13
|
+
return `${shown} ${FileSizeConfig.units[unit]}`;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export * from './amount-service';
|
|
2
|
+
export * from './base64-service';
|
|
3
|
+
export * from './color-service';
|
|
4
|
+
export * from './constants';
|
|
5
|
+
export * from './crypto-service';
|
|
6
|
+
export * from './date-service';
|
|
7
|
+
export * from './email-preview-service';
|
|
8
|
+
export * from './encrypted-fields-service';
|
|
9
|
+
export * from './file-size-service';
|
|
10
|
+
export * from './envelope-crypto-service';
|
|
11
|
+
export * from './number-format-service';
|
|
12
|
+
export * from './phone-service';
|
|
13
|
+
export * from './postal-code-service';
|
|
14
|
+
export * from './recaptcha-service';
|
|
15
|
+
export * from './signed-token-service';
|
|
16
|
+
export * from './socket-token-service';
|
|
17
|
+
export * from './string-service';
|
|
18
|
+
export * from './transform-service';
|
|
19
|
+
export * from './url-service';
|
|
20
|
+
export * from './user-agent-service';
|
|
21
|
+
export * from './user-token-service';
|
|
22
|
+
export * from './validation-service';
|
|
23
|
+
export * from './error-response-service';
|
|
24
|
+
export * from './relative-time-service';
|
|
25
|
+
export * from './request-client-service';
|
|
26
|
+
export * from './json-service';
|
|
27
|
+
export * from './portable-text-service';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export class JsonService {
|
|
2
|
+
static read(text) {
|
|
3
|
+
if (typeof text !== 'string')
|
|
4
|
+
return undefined;
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(text);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
static parse(text, guard) {
|
|
13
|
+
const value = this.read(text);
|
|
14
|
+
return value !== undefined && guard(value) ? value : null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class NumberFormatService {
|
|
2
|
+
static formatCurrency(value, currencyCode, locale = 'en-US', minimumFractionDigits = 0, maximumFractionDigits = 2) {
|
|
3
|
+
return new Intl.NumberFormat(locale, {
|
|
4
|
+
currency: currencyCode,
|
|
5
|
+
maximumFractionDigits,
|
|
6
|
+
minimumFractionDigits,
|
|
7
|
+
style: 'currency',
|
|
8
|
+
}).format(value);
|
|
9
|
+
}
|
|
10
|
+
static formatCompact(value, locale = 'en-US') {
|
|
11
|
+
return new Intl.NumberFormat(locale, {
|
|
12
|
+
compactDisplay: 'short',
|
|
13
|
+
notation: 'compact',
|
|
14
|
+
}).format(value);
|
|
15
|
+
}
|
|
16
|
+
static formatPercent(value, locale = 'en-US') {
|
|
17
|
+
return new Intl.NumberFormat(locale, {
|
|
18
|
+
maximumFractionDigits: 1,
|
|
19
|
+
style: 'percent',
|
|
20
|
+
}).format(value);
|
|
21
|
+
}
|
|
22
|
+
}
|