jsfe 1.4.2 → 1.5.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/README.md +25 -0
- package/dist/helpers/amount-to-speech.d.ts +6 -0
- package/dist/helpers/amount-to-speech.d.ts.map +1 -0
- package/dist/helpers/amount-to-speech.js +195 -0
- package/dist/helpers/answer.d.ts +50 -0
- package/dist/helpers/answer.d.ts.map +1 -0
- package/dist/helpers/answer.js +409 -0
- package/dist/helpers/capture.d.ts +5 -0
- package/dist/helpers/capture.d.ts.map +1 -0
- package/dist/helpers/capture.js +50 -0
- package/dist/helpers/card-checks.d.ts +8 -0
- package/dist/helpers/card-checks.d.ts.map +1 -0
- package/dist/helpers/card-checks.js +46 -0
- package/dist/helpers/contact.d.ts +50 -0
- package/dist/helpers/contact.d.ts.map +1 -0
- package/dist/helpers/contact.js +178 -0
- package/dist/helpers/date-words.d.ts +2 -0
- package/dist/helpers/date-words.d.ts.map +1 -0
- package/dist/helpers/date-words.js +29 -0
- package/dist/helpers/index.d.ts +72 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/index.js +34 -0
- package/dist/helpers/match-inputs.d.ts +28 -0
- package/dist/helpers/match-inputs.d.ts.map +1 -0
- package/dist/helpers/match-inputs.js +223 -0
- package/dist/helpers/matches-choice.d.ts +26 -0
- package/dist/helpers/matches-choice.d.ts.map +1 -0
- package/dist/helpers/matches-choice.js +61 -0
- package/dist/helpers/otp-check.d.ts +36 -0
- package/dist/helpers/otp-check.d.ts.map +1 -0
- package/dist/helpers/otp-check.js +75 -0
- package/dist/helpers/phrases.d.ts +12 -0
- package/dist/helpers/phrases.d.ts.map +1 -0
- package/dist/helpers/phrases.js +31 -0
- package/dist/helpers/url-speech.d.ts +8 -0
- package/dist/helpers/url-speech.d.ts.map +1 -0
- package/dist/helpers/url-speech.js +28 -0
- package/dist/helpers/validators.d.ts +13 -0
- package/dist/helpers/validators.d.ts.map +1 -0
- package/dist/helpers/validators.js +32 -0
- package/package.json +5 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strict word-aware option matching for flow CASE conditions (APPROVED_FUNCTIONS.matchesChoice) —
|
|
3
|
+
* replaces the substring `.some(choice => input.includes(choice))` idiom, whose false positives broke
|
|
4
|
+
* branch routing ('si' inside "imposible"/"así" matched YES, prod-observed 2026-08-09).
|
|
5
|
+
*
|
|
6
|
+
* Rules (deliberately NO fuzzy/prefix matching):
|
|
7
|
+
* - single-word choice: exact word membership
|
|
8
|
+
* - multi-word choice: exact word-sequence containment
|
|
9
|
+
* - plurals/inflections: enumerate them explicitly in the choice list
|
|
10
|
+
* - a keypad key (one digit, * or #) matches ONLY an answer that is exactly that key, never a key
|
|
11
|
+
* inside a longer answer (owner rule): "I made payments by 1," at "press 1 to pay" charged the full
|
|
12
|
+
* amount due on dev, 2026-10-14
|
|
13
|
+
* - an ELONGATED word the user typed is forgiven (2026-10-01): a repeated vowel or any letter typed
|
|
14
|
+
* 3+ times collapses — "Sii" matches 'si', "nadaaaa" 'nada', "yesss" 'yes'. A typed "Sii" at the
|
|
15
|
+
* authentication retry menu ended the conversation in contact-support. Only the user's word is
|
|
16
|
+
* collapsed, and only against an option word with no doubled letter of its own. Measured on a
|
|
17
|
+
* week of prod prompts (scratch replay, 98 option lists × 23,692 prompts):
|
|
18
|
+
* · collapsing BOTH sides: rejected — 'all' → 'al' made "servicio al cliente" (138 turns) pick
|
|
19
|
+
* the "all" option, and 'mm' → 'm' made "I'm" a filler;
|
|
20
|
+
* · collapsing every doubled letter of the user's word: rejected — "mess" matched Spanish
|
|
21
|
+
* 'mes' and "Mall" 'mal'. A doubled consonant is real spelling, so "okk" stays unmatched.
|
|
22
|
+
*
|
|
23
|
+
* Zero imports, so scripts/tests/matches-choice.mjs runs the real implementation.
|
|
24
|
+
*/
|
|
25
|
+
export function matchesChoice(input, choices) {
|
|
26
|
+
if (!input || !Array.isArray(choices)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
// One tokenizer for both sides: letters/digits/'*' are word chars, everything
|
|
30
|
+
// else (commas, ¿?¡!, periods) separates — so callers need no pre-normalization.
|
|
31
|
+
const tokenize = (s) => String(s).toLowerCase().split(/[^\p{L}\p{N}*]+/u).filter(Boolean);
|
|
32
|
+
// Elongation, not spelling: a repeated VOWEL, or any letter typed 3+ times. A doubled consonant
|
|
33
|
+
// is real spelling too often ("mess" would become Spanish 'mes', "Mall" 'mal').
|
|
34
|
+
const collapse = (word) => word.replace(/([aeiouáéíóúü])\1+/gu, '$1').replace(/(\p{L})\1{2,}/gu, '$1');
|
|
35
|
+
const hasDoubledLetter = (word) => /(\p{L})\1/u.test(word);
|
|
36
|
+
const words = tokenize(input);
|
|
37
|
+
const collapsedWords = words.map(collapse);
|
|
38
|
+
const sameWord = (i, choiceWord) => words[i] === choiceWord
|
|
39
|
+
|| (!hasDoubledLetter(choiceWord) && collapsedWords[i] === choiceWord);
|
|
40
|
+
return choices.some(choice => {
|
|
41
|
+
const choiceWords = tokenize(choice);
|
|
42
|
+
if (/^[0-9*#]$/.test(String(choice).trim())) {
|
|
43
|
+
return String(input).replace(/^[\s.,;:!?¿¡'"]+|[\s.,;:!?¿¡'"]+$/g, '') === String(choice).trim();
|
|
44
|
+
}
|
|
45
|
+
else if (choiceWords.length === 0) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// a single word, or a multi-word entry ('thank you', 'por favor') as a consecutive run
|
|
50
|
+
for (let i = 0; i <= words.length - choiceWords.length; i++) {
|
|
51
|
+
if (choiceWords.every((c, j) => sameWord(i + j, c))) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// no run starting here
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{otpHash?: string|null, otpTimestamp?: number|null}} stored the container's pending code
|
|
3
|
+
* @param {string} providedHash sha256 of what the customer entered
|
|
4
|
+
* @param {number} now
|
|
5
|
+
* @returns {{ok: boolean, failure: null|'missing'|'expired'|'mismatch'}}
|
|
6
|
+
*/
|
|
7
|
+
export function checkOtp(stored: {
|
|
8
|
+
otpHash?: string | null;
|
|
9
|
+
otpTimestamp?: number | null;
|
|
10
|
+
}, providedHash: string, now: number): {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
failure: null | "missing" | "expired" | "mismatch";
|
|
13
|
+
};
|
|
14
|
+
/** A destination for a log line: the last 4 digits of a phone, or the domain of an email. */
|
|
15
|
+
export function maskDestination(to: any): string;
|
|
16
|
+
/**
|
|
17
|
+
* A flow's code check (validateOTP): hashes what the customer entered (SHA-256, the hash the code senders store in
|
|
18
|
+
* container.otpHash) and checks it with checkOtp. Accepted: the pending code is cleared and container.otpVerified /
|
|
19
|
+
* otpVerifiedAt are set. Expired: the code is cleared (it can never be accepted again). Every refusal leaves its reason
|
|
20
|
+
* in container.otpFailure ('missing' | 'expired' | 'mismatch'). Uses the platform's Web Crypto, so it needs no host.
|
|
21
|
+
*/
|
|
22
|
+
export function validateOTP(otp: any, container: any): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Is this one-time code accepted — and if not, WHY? (validateOTP)
|
|
25
|
+
*
|
|
26
|
+
* validateOTP returns a boolean, which every flow condition depends on. Until 2026-10-05 the reason
|
|
27
|
+
* a code was refused was lost: an EXPIRED code (which validateOTP also clears) and a wrong one
|
|
28
|
+
* both read as "That code didn't match. Let's try again." — and the flow asked again for a code
|
|
29
|
+
* that could no longer exist. A WhatsApp customer looped for 19 minutes on
|
|
30
|
+
* exactly that (2026-10-05). The reason now rides on the container as `otpFailure`, so the
|
|
31
|
+
* flow can send a fresh code for 'expired' / 'missing' and re-ask only on 'mismatch'.
|
|
32
|
+
*
|
|
33
|
+
* Pure, no imports. Tested by scripts/tests/otp-check.mjs in CI.
|
|
34
|
+
*/
|
|
35
|
+
export const OTP_EXPIRY_MS: number;
|
|
36
|
+
//# sourceMappingURL=otp-check.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"otp-check.d.ts","sourceRoot":"","sources":["../../src/helpers/otp-check.js"],"names":[],"mappings":"AAeA;;;;;GAKG;AACH,iCALW;IAAC,OAAO,CAAC,EAAE,MAAM,GAAC,IAAI,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,GAAC,IAAI,CAAA;CAAC,gBACnD,MAAM,OACN,MAAM,GACJ;IAAC,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,IAAI,GAAC,SAAS,GAAC,SAAS,GAAC,UAAU,CAAA;CAAC,CAYvE;AAED,6FAA6F;AAC7F,iDAQC;AAED;;;;;GAKG;AACH,wEAsBC;AAxED;;;;;;;;;;;GAWG;AAEH,mCAA4C"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is this one-time code accepted — and if not, WHY? (validateOTP)
|
|
3
|
+
*
|
|
4
|
+
* validateOTP returns a boolean, which every flow condition depends on. Until 2026-10-05 the reason
|
|
5
|
+
* a code was refused was lost: an EXPIRED code (which validateOTP also clears) and a wrong one
|
|
6
|
+
* both read as "That code didn't match. Let's try again." — and the flow asked again for a code
|
|
7
|
+
* that could no longer exist. A WhatsApp customer looped for 19 minutes on
|
|
8
|
+
* exactly that (2026-10-05). The reason now rides on the container as `otpFailure`, so the
|
|
9
|
+
* flow can send a fresh code for 'expired' / 'missing' and re-ask only on 'mismatch'.
|
|
10
|
+
*
|
|
11
|
+
* Pure, no imports. Tested by scripts/tests/otp-check.mjs in CI.
|
|
12
|
+
*/
|
|
13
|
+
export const OTP_EXPIRY_MS = 10 * 60 * 1000;
|
|
14
|
+
/**
|
|
15
|
+
* @param {{otpHash?: string|null, otpTimestamp?: number|null}} stored the container's pending code
|
|
16
|
+
* @param {string} providedHash sha256 of what the customer entered
|
|
17
|
+
* @param {number} now
|
|
18
|
+
* @returns {{ok: boolean, failure: null|'missing'|'expired'|'mismatch'}}
|
|
19
|
+
*/
|
|
20
|
+
export function checkOtp(stored, providedHash, now) {
|
|
21
|
+
if (!stored || !stored.otpHash || !stored.otpTimestamp) {
|
|
22
|
+
return { ok: false, failure: 'missing' };
|
|
23
|
+
}
|
|
24
|
+
else if (now - stored.otpTimestamp > OTP_EXPIRY_MS) {
|
|
25
|
+
return { ok: false, failure: 'expired' };
|
|
26
|
+
}
|
|
27
|
+
else if (providedHash !== stored.otpHash) {
|
|
28
|
+
return { ok: false, failure: 'mismatch' };
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
return { ok: true, failure: null };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** A destination for a log line: the last 4 digits of a phone, or the domain of an email. */
|
|
35
|
+
export function maskDestination(to) {
|
|
36
|
+
const s = String(to || '');
|
|
37
|
+
if (s.includes('@')) {
|
|
38
|
+
return `…@${s.split('@').pop()}`;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
const digits = s.replace(/\D/g, '');
|
|
42
|
+
return digits ? `…${digits.slice(-4)}` : '(none)';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A flow's code check (validateOTP): hashes what the customer entered (SHA-256, the hash the code senders store in
|
|
47
|
+
* container.otpHash) and checks it with checkOtp. Accepted: the pending code is cleared and container.otpVerified /
|
|
48
|
+
* otpVerifiedAt are set. Expired: the code is cleared (it can never be accepted again). Every refusal leaves its reason
|
|
49
|
+
* in container.otpFailure ('missing' | 'expired' | 'mismatch'). Uses the platform's Web Crypto, so it needs no host.
|
|
50
|
+
*/
|
|
51
|
+
export async function validateOTP(otp, container) {
|
|
52
|
+
const bytes = new TextEncoder().encode(String(otp ?? ''));
|
|
53
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
|
|
54
|
+
const providedHash = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
55
|
+
const verdict = checkOtp(container, providedHash, Date.now());
|
|
56
|
+
if (verdict.ok) {
|
|
57
|
+
container.otpHash = null;
|
|
58
|
+
container.otpTimestamp = null;
|
|
59
|
+
delete container.otpFailure;
|
|
60
|
+
container.otpVerified = true;
|
|
61
|
+
container.otpVerifiedAt = Date.now();
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
else if (verdict.failure === 'expired') {
|
|
65
|
+
container.otpHash = null;
|
|
66
|
+
container.otpTimestamp = null;
|
|
67
|
+
container.otpVerified = false;
|
|
68
|
+
container.otpFailure = 'expired';
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
container.otpFailure = verdict.failure;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentences the flows say in many places, written once and addressed to the customer by name when the session knows it
|
|
3
|
+
* (APPROVED_FUNCTIONS anythingElse, goodbye). Before it, one flow set's closing question was typed into 30 English and 30
|
|
4
|
+
* Spanish SAYs, none of them using the name the account lookup had found. Part of jsfe/helpers; zero imports.
|
|
5
|
+
*/
|
|
6
|
+
/** A name as said to the customer: trimmed; an all-capitals name (as account systems store it) in title case. */
|
|
7
|
+
export function sayName(name: any): string;
|
|
8
|
+
/** The closing question after a task is done, in the turn's language ('en' when there is none), with the name if known. */
|
|
9
|
+
export function anythingElse(lang: any, name: any): any;
|
|
10
|
+
/** The parting wish when a conversation ends, in the turn's language, with the name if known. */
|
|
11
|
+
export function goodbye(lang: any, name: any): any;
|
|
12
|
+
//# sourceMappingURL=phrases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"phrases.d.ts","sourceRoot":"","sources":["../../src/helpers/phrases.js"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,iHAAiH;AACjH,2CAOC;AAWD,2HAA2H;AAC3H,wDAEC;AAED,iGAAiG;AACjG,mDAEC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentences the flows say in many places, written once and addressed to the customer by name when the session knows it
|
|
3
|
+
* (APPROVED_FUNCTIONS anythingElse, goodbye). Before it, one flow set's closing question was typed into 30 English and 30
|
|
4
|
+
* Spanish SAYs, none of them using the name the account lookup had found. Part of jsfe/helpers; zero imports.
|
|
5
|
+
*/
|
|
6
|
+
/** A name as said to the customer: trimmed; an all-capitals name (as account systems store it) in title case. */
|
|
7
|
+
export function sayName(name) {
|
|
8
|
+
const n = String(name ?? '').trim();
|
|
9
|
+
if (n && n === n.toUpperCase() && /[A-ZÁÉÍÓÚÑ]/.test(n)) {
|
|
10
|
+
return n.toLowerCase().replace(/(^|[\s'-])(\p{L})/gu, (m, sep, ch) => sep + ch.toUpperCase());
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
return n;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const ANYTHING_ELSE = {
|
|
17
|
+
en: (n) => (n ? `Is there anything else I can help you with, ${n}?` : 'Is there anything else I can help you with?'),
|
|
18
|
+
es: (n) => (n ? `¿Hay algo más en lo que pueda ayudarle, ${n}?` : '¿Hay algo más en lo que pueda ayudarle?'),
|
|
19
|
+
};
|
|
20
|
+
const GOODBYE = {
|
|
21
|
+
en: (n) => (n ? `Have a great day, ${n}!` : 'Have a great day!'),
|
|
22
|
+
es: (n) => (n ? `¡Que tenga un buen día, ${n}!` : '¡Que tenga un buen día!'),
|
|
23
|
+
};
|
|
24
|
+
/** The closing question after a task is done, in the turn's language ('en' when there is none), with the name if known. */
|
|
25
|
+
export function anythingElse(lang, name) {
|
|
26
|
+
return (ANYTHING_ELSE[lang] || ANYTHING_ELSE.en)(sayName(name));
|
|
27
|
+
}
|
|
28
|
+
/** The parting wish when a conversation ends, in the turn's language, with the name if known. */
|
|
29
|
+
export function goodbye(lang, name) {
|
|
30
|
+
return (GOODBYE[lang] || GOODBYE.en)(sayName(name));
|
|
31
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A URL said aloud (urlToSpeech, textWithUrlToSpeech): protocol, query and fragment dropped, "www." dropped, dots,
|
|
3
|
+
* slashes, dashes and underscores as words, in English or Spanish. Pure; part of jsfe/helpers.
|
|
4
|
+
*/
|
|
5
|
+
export function urlToSpeech(url: any, language?: string): string;
|
|
6
|
+
/** Every URL in a text said aloud. */
|
|
7
|
+
export function textWithUrlToSpeech(text: any, language?: string): any;
|
|
8
|
+
//# sourceMappingURL=url-speech.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url-speech.d.ts","sourceRoot":"","sources":["../../src/helpers/url-speech.js"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,iEAaC;AAED,sCAAsC;AACtC,uEAMC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A URL said aloud (urlToSpeech, textWithUrlToSpeech): protocol, query and fragment dropped, "www." dropped, dots,
|
|
3
|
+
* slashes, dashes and underscores as words, in English or Spanish. Pure; part of jsfe/helpers.
|
|
4
|
+
*/
|
|
5
|
+
export function urlToSpeech(url, language = 'en') {
|
|
6
|
+
if (!url || typeof url !== 'string') {
|
|
7
|
+
return '';
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
const words = language === 'es'
|
|
11
|
+
? { dot: 'punto', slash: 'diagonal', dash: 'guión', underscore: 'guión bajo' }
|
|
12
|
+
: { dot: 'dot', slash: 'slash', dash: 'dash', underscore: 'underscore' };
|
|
13
|
+
const bare = url.replace(/^https?:\/\//i, '').replace(/\?.*$/, '').replace(/#.*$/, '').replace(/\/+$/, '');
|
|
14
|
+
const slashIdx = bare.indexOf('/');
|
|
15
|
+
const domain = (slashIdx >= 0 ? bare.substring(0, slashIdx) : bare).replace(/^www\./i, '').replace(/\./g, ` ${words.dot} `);
|
|
16
|
+
const path = (slashIdx >= 0 ? bare.substring(slashIdx) : '').replace(/\//g, ` ${words.slash} `).replace(/-/g, ` ${words.dash} `).replace(/_/g, ` ${words.underscore} `);
|
|
17
|
+
return (domain + ' ' + path).replace(/\s+/g, ' ').trim();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Every URL in a text said aloud. */
|
|
21
|
+
export function textWithUrlToSpeech(text, language = 'en') {
|
|
22
|
+
if (!text || typeof text !== 'string') {
|
|
23
|
+
return text;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
return text.replace(/https?:\/\/[^\s]+/gi, (url) => urlToSpeech(url, language));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape checks flows call in conditions (validateDigits, validatePhone, validateEmail). Pure; part of jsfe/helpers.
|
|
3
|
+
*/
|
|
4
|
+
/** Only digits, and between minDigits and maxDigits of them. */
|
|
5
|
+
export function validateDigits(input: any, minDigits: any, maxDigits: any): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* A US phone number (10 digits, or 11 starting with 1), or with acceptInternational any 11–15 digits. String() first:
|
|
8
|
+
* a flow can hand over a number (an all-digit answer evaluated to Number).
|
|
9
|
+
*/
|
|
10
|
+
export function validatePhone(phone: any, acceptInternational?: boolean): boolean;
|
|
11
|
+
/** Something@something.tld, no spaces. */
|
|
12
|
+
export function validateEmail(email: any): boolean;
|
|
13
|
+
//# sourceMappingURL=validators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../../src/helpers/validators.js"],"names":[],"mappings":"AAAA;;GAEG;AAEH,gEAAgE;AAChE,oFAMC;AAED;;;GAGG;AACH,kFASC;AAED,0CAA0C;AAC1C,mDAEC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shape checks flows call in conditions (validateDigits, validatePhone, validateEmail). Pure; part of jsfe/helpers.
|
|
3
|
+
*/
|
|
4
|
+
/** Only digits, and between minDigits and maxDigits of them. */
|
|
5
|
+
export function validateDigits(input, minDigits, maxDigits) {
|
|
6
|
+
if (!/^\d+$/.test(input)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
return input.length >= minDigits && input.length <= maxDigits; // as the host had it: a Number input has no length
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A US phone number (10 digits, or 11 starting with 1), or with acceptInternational any 11–15 digits. String() first:
|
|
15
|
+
* a flow can hand over a number (an all-digit answer evaluated to Number).
|
|
16
|
+
*/
|
|
17
|
+
export function validatePhone(phone, acceptInternational = false) {
|
|
18
|
+
const cleaned = String(phone ?? '').replace(/\D/g, '');
|
|
19
|
+
if (cleaned.length === 10 || (cleaned.length === 11 && cleaned.startsWith('1'))) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
else if (acceptInternational && cleaned.length >= 11 && cleaned.length <= 15) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Something@something.tld, no spaces. */
|
|
30
|
+
export function validateEmail(email) {
|
|
31
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(email ?? ''));
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsfe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "TypeScript workflow engine for conversational/automation apps: multi-flow orchestration, interrupt/resume, stack-based control, robust errors, and flexible tool/REST integration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ron Pinkas",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"./host": {
|
|
19
19
|
"types": "./dist/host/index.d.ts",
|
|
20
20
|
"import": "./dist/host/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./helpers": {
|
|
23
|
+
"types": "./dist/helpers/index.d.ts",
|
|
24
|
+
"import": "./dist/helpers/index.js"
|
|
21
25
|
}
|
|
22
26
|
},
|
|
23
27
|
"main": "./dist/index.js",
|