jsfe 1.12.0 → 1.13.1

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.
@@ -228,6 +228,7 @@ export function validateFlowContracts(flows: any, tools?: any[], { externalFlows
228
228
  max: number;
229
229
  };
230
230
  pii: boolean;
231
+ validator: string;
231
232
  question: string;
232
233
  question_es: string;
233
234
  };
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../src/forms/contracts.js"],"names":[],"mappings":"AAgBA;;;IAGC;AACD;;;;;EAGC;AACD,qGAAqG;AACrG;;QAwCC;AAqBD;;;QAEC;AAgBD;;;;;;;;;QASC;AACD;;;;QAGC;AACD;;;;;;;;QA8BC;AAED,mHAAmH;AACnH,kDAgBC;AAED,+GAA+G;AAC/G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2EC"}
1
+ {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../src/forms/contracts.js"],"names":[],"mappings":"AAgBA;;;IAGC;AACD;;;;;EAGC;AACD,qGAAqG;AACrG;;QAwCC;AAqBD;;;QAEC;AAgBD;;;;;;;;;QASC;AACD;;;;QAGC;AACD;;;;;;;;QA8BC;AAED,mHAAmH;AACnH,kDAgBC;AAED,+GAA+G;AAC/G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2EC"}
@@ -365,6 +365,8 @@ export namespace FIELD_PRESETS {
365
365
  export { digits_6 as digits };
366
366
  let pii_2: boolean;
367
367
  export { pii_2 as pii };
368
+ let validator_2: string;
369
+ export { validator_2 as validator };
368
370
  let question_11: string;
369
371
  export { question_11 as question };
370
372
  let question_es_11: string;
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA4HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAgcD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA3nBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmzB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AA6HA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAocD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AAhoBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyzB9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
@@ -82,6 +82,7 @@ export const FIELD_PRESETS = {
82
82
  retry: 'Please say or enter the expiration as four digits, month then year, like 0 9 2 8.', retry_es: 'Por favor diga o ingrese el vencimiento en cuatro dígitos, mes y año, por ejemplo 0 9 2 8.',
83
83
  retry_text: 'Please type the expiration as four digits, month then year, like 0928.', retry_text_es: 'Por favor escriba el vencimiento en cuatro dígitos, mes y año, por ejemplo 0928.' },
84
84
  card_cvv: { label: 'security code', label_es: 'código de seguridad', profile: { kind: 'digits', min: 3, max: 4 }, digits: { min: 3, max: 4 }, pii: true,
85
+ validator: 'validate-card-cvv',
85
86
  question: 'What is the security code on the back of the card?', question_es: '¿Cuál es el código de seguridad al reverso de la tarjeta?' },
86
87
  card_zip: { label: 'billing ZIP code', label_es: 'código postal de facturación', profile: { kind: 'digits', min: 5, max: 5 }, digits: { min: 5, max: 5 },
87
88
  question: 'What is the billing ZIP code for the card?', question_es: '¿Cuál es el código postal de facturación de la tarjeta?' },
@@ -602,6 +603,10 @@ const validators = [
602
603
  description: 'Validator: month and year, a real month, not expired (validateExpiration). Normalizes to MMYY. Params: input, value.',
603
604
  parameters: [{ name: 'input', type: 'string', description: 'input name' }, { name: 'value', type: 'string', description: 'the reading' }],
604
605
  steps: [SIDE('vce_check', "(function (v) { var m = /^(0?[1-9]|1[0-2])\\D*(?:20)?(\\d{2})$/.exec(String(v).replace(/\\s+/g, '').replace(/de/gi, '')) || /^(\\d{2})(\\d{2})$/.exec(String(v).replace(/\\D/g, '')) || /^(0[1-9])0(\\d{2})$/.exec(String(v).replace(/\\D/g, '')); var mm = m ? ('0' + m[1]).slice(-2) : ''; var yy = m ? m[2] : ''; cargo.inputCheck = m && validateExpiration(mm, yy) ? { ok: true, value: mm + yy } : { ok: false, message: \"That expiration date doesn't look right.\", message_es: 'Esa fecha de vencimiento no parece correcta.' }; return true; })(value)")] },
606
+ { id: 'validate-card-cvv', name: 'ValidateCardCvv', version: '1.0.0', scope: 'isolated',
607
+ description: 'Validator: the security code has the length of its card brand, 4 digits for American Express (34, 37) and 3 for every other brand; with no card number yet, 3 or 4. Params: input, value, values.',
608
+ parameters: [{ name: 'input', type: 'string', description: 'input name' }, { name: 'value', type: 'string', description: 'the reading' }, { name: 'values', type: 'object', description: "the form's values, for the card number" }],
609
+ steps: [SIDE('vcc_check', "(function (v, n) { var d = String(v).replace(/\\D/g, ''); var pan = String(n || '').replace(/\\D/g, ''); var want = pan ? (/^3[47]/.test(pan) ? 4 : 3) : 0; var ok = want ? d.length === want : d.length === 3 || d.length === 4; cargo.inputCheck = ok ? { ok: true, value: d } : want === 4 ? { ok: false, message: \"That security code doesn't look right. It is the 4 digits on the front of the card.\", message_es: 'Ese código de seguridad no parece correcto. Son los 4 dígitos al frente de la tarjeta.' } : { ok: false, message: \"That security code doesn't look right. It is the 3 digits on the back of the card.\", message_es: 'Ese código de seguridad no parece correcto. Son los 3 dígitos al reverso de la tarjeta.' }; return true; })(value, typeof values !== 'undefined' && values ? values.card_number : '')")] },
605
610
  ];
606
611
  // ── field collectors and sub-forms ──────────────────────────────────────────────────────────────────────────────────
607
612
  const fieldCollectors = Object.entries(FIELD_COLLECTORS).map(([id, field]) => formFlow({
@@ -857,6 +862,7 @@ const LIBRARY_CONTRACTS = {
857
862
  'load-source': { role: 'worker' }, 'answer-facts': { role: 'worker' },
858
863
  'validate-card-number': { role: 'worker', result: { path: 'cargo.inputCheck', type: 'object' } },
859
864
  'validate-card-expiration': { role: 'worker', result: { path: 'cargo.inputCheck', type: 'object' } },
865
+ 'validate-card-cvv': { role: 'worker', result: { path: 'cargo.inputCheck', type: 'object' } },
860
866
  'collect-phone': { role: 'collector' }, 'collect-email': { role: 'collector' }, 'collect-zip': { role: 'collector' },
861
867
  'collect-street-address': { role: 'collector' }, 'collect-city': { role: 'collector' }, 'collect-card-number': { role: 'collector' },
862
868
  'collect-card-expiration': { role: 'collector' }, 'collect-card-cvv': { role: 'collector' }, 'collect-card-zip': { role: 'collector' },
@@ -1 +1 @@
1
- {"version":3,"file":"identity-flows.d.ts","sourceRoot":"","sources":["../../src/forms/identity-flows.js"],"names":[],"mappings":"AA2BA,+GAA+G;AAC/G,oFAOC;AAQD,wFAAwF;AACxF,iDAGC;AACD,6CAMC;AACD,qHAQC;AAMD,4EAKC;AAoFD,4DAKC;AAiFD,8EAGC;AA4BD,4FAA4F;AAC5F;;;;EAOC;AAQD,0BAAmI"}
1
+ {"version":3,"file":"identity-flows.d.ts","sourceRoot":"","sources":["../../src/forms/identity-flows.js"],"names":[],"mappings":"AA2BA,+GAA+G;AAC/G,oFAOC;AAQD,wFAAwF;AACxF,iDAGC;AACD,6CAMC;AACD,qHAQC;AAMD,4EAKC;AAoFD,4DAKC;AAqFD,8EAGC;AA4BD,4FAA4F;AAC5F;;;;EAOC;AAQD,0BAAmI"}
@@ -191,6 +191,7 @@ const OTP_WORDS = {
191
191
  other: ['otra cuenta', 'otro número', 'otro numero', 'cuenta diferente', 'another account', 'different account', 'other account', 'another number', 'different number', 'not my account', 'no es mi cuenta', 'número de cuenta', 'numero de cuenta', 'account number'],
192
192
  // the offered OTRA CUENTA / ANOTHER ACCOUNT said by its first word, as the whole answer (prod 2026-10-25 00:19: "Otra")
193
193
  other_alone: ['otra', 'otro', 'another', 'other'],
194
+ email: ['email', 'e-mail', 'e mail', 'emails', 'correo', 'correos', 'correo electrónico', 'correo electronico', 'imeil'],
194
195
  resend: ['resend', 'send again', 'send it again', 'not received', 'never received', 'did not receive', 'didn t receive', 'didn t get', 'haven t received', 'nothing came', 'no code', 'no text', 'no llegó', 'no llego', 'no llega', 'no ha llegado', 'no me llegó', 'no me llego', 'no le llegó', 'no le llego', 'no me ha llegado', 'no recibí', 'no recibi', 'no lo recibí', 'no lo recibi', 'no tengo el código', 'no tengo el codigo', 'no lo tengo', 'no alcancé', 'no alcance', 'mándame el código', 'mandame el codigo', 'envíame el código', 'enviame el codigo', 'reenviar', 'reenvíe', 'reenvie'],
195
196
  later: ['driving', 'i am driving', 'i m busy', 'im busy', 'not now', 'later', 'call me later', 'call back', 'cant right now', 'can t right now', 'can t now', 'cannot right now', 'manejando', 'conduciendo', 'estoy manejando', 'voy manejando', 'ahora no', 'ahora no puedo', 'no puedo ahora', 'más tarde', 'mas tarde', 'estoy ocupado', 'estoy ocupada'],
196
197
  noPhone: ['don t have that phone', 'don t have that number', 'don t have that phone anymore', 'don t have the phone', 'not my number anymore', 'not my phone anymore', 'that s an old number', 'old number', 'changed my number', 'changed my phone', 'lost my phone', 'no tengo ese teléfono', 'no tengo ese telefono', 'no tengo ese número', 'no tengo ese numero', 'ya no tengo ese', 'ya no tengo el teléfono', 'ya no tengo el telefono', 'ya no es mi número', 'ya no es mi numero', 'número viejo', 'numero viejo', 'cambié de número', 'cambie de numero', 'cambié mi número', 'perdí mi teléfono', 'perdi mi telefono', 'don t have a phone', 'don t have phone', 'don t have my phone', 'don t have a cell', 'don t have the cell', 'don t have my cell', 'don t have a cell phone', 'don t have access to that phone', 'don t have access to the phone', 'no tengo teléfono', 'no tengo telefono', 'no tengo celular', 'no tengo el celular', 'no tengo mi teléfono', 'no tengo mi telefono', 'no tengo mi celular', 'no tengo un teléfono', 'no tengo un telefono', 'no tengo acceso a ese teléfono', 'no tengo acceso a ese telefono']
@@ -212,6 +213,9 @@ const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a
212
213
  cases('otp_response', {
213
214
  'condition: normalized_otp_code.length === 6': { id: 'otp_check_tool', type: 'CALL-TOOL', tool: 'validate-otp', variable: 'otp_validation_result', args: { otp: '{{normalized_otp_code}}', container: '{{cargo}}' }, onFail: call('otp_check_failed', 'get-and-validate-otp-code', undefined, 'replace') },
214
215
  'condition: wants_other_account': call('otp_change_account', 'authenticate-user', { cell_number: '', email: '', retrying: true, declined_caller: true }, 'replace'),
216
+ // Email asked for at a code sent by text: authentication is an input step, so the contact question is asked again
217
+ // (without the caller-ID offer) and the caller gives the email; a code sent by email keeps "no me llegó el correo" a resend.
218
+ 'condition: !!cargo.otp_cell_number && normalized_otp_code.length !== 6 && matchesChoice(user_choice,otp_phrases.email)': call('otp_use_email', 'authenticate-user', { cell_number: '', email: '', retrying: true, declined_caller: true }, 'replace'),
215
219
  "condition: isExitRequest(user_choice)": call('otp_exit', '{{otp_cancel}}', undefined, 'reboot'),
216
220
  "condition: isAnswer(user_choice,'agent')": call('otp_agent', 'live-agent-requested', undefined, 'reboot'),
217
221
  "condition: isAnswer(user_choice,'text')": call('otp_text', 'switch-to-text', undefined, 'reboot'),
@@ -248,7 +252,7 @@ for (const flow of [auth, validateCode]) {
248
252
  flow.steps.unshift(...otpMenuMessages.map(([name, value]) => set(name, name, value.includes('{{') ? value : JSON.stringify(value))));
249
253
  }
250
254
  const retryParams = [param('error_message', 'Failure sentence'), param('error_message_es', 'Spanish failure sentence'), param('retry_flow', 'Retry destination'), param('cancel_flow', 'Exit destination'), param('capture_patterns', 'Declared contact capture readers', 'array'), param('retry_call_type', 'reboot or replace'), param('allow_code_entry', 'Accept the active six-digit challenge', 'boolean'), param('question', 'Retry question'), param('question_es', 'Spanish retry question'), param('caller_outcome', 'Unresolved outcome'), param('caller_reason', 'Unresolved reason'), param('parent_form', 'Owning task FORM instance'), param('contact_options', 'Contact field protocol declaration', 'object')];
251
- const retry = worker('generic-retry-with-options', 'GenericRetryWithOptions', 'Collect a retry, alternate contact, code or exit through the shared FORM adapter.', { scope: 'isolated', parameters: retryParams, variables: vars({ error_message: '', error_message_es: '', retry_flow: 'authenticate-user', cancel_flow: 'contact-support', capture_patterns: [], retry_call_type: 'reboot', allow_code_entry: false, question: '', question_es: '', caller_outcome: '', caller_reason: '', parent_form: '', contact_options: null, user_choice: '', smart_capture_result: null, normalized_otp_code: '', retry_question: '', retry_question_es: '', retry_profiles: [{ name: 'phone', label: 'phone number', label_es: 'número de teléfono', profile: { kind: 'phone' } }, { name: 'email_address', label: 'email address', label_es: 'correo electrónico', profile: { kind: 'email' } }, ...OTP_PROFILES] }), contract: { role: 'collector', summary: 'Common bounded service retry menu' }, steps: [
255
+ const retry = worker('generic-retry-with-options', 'GenericRetryWithOptions', 'Collect a retry, alternate contact, code or exit through the shared FORM adapter.', { scope: 'isolated', parameters: retryParams, variables: vars({ error_message: '', error_message_es: '', retry_flow: 'authenticate-user', cancel_flow: 'contact-support', capture_patterns: [], retry_call_type: 'reboot', allow_code_entry: false, question: '', question_es: '', caller_outcome: '', caller_reason: '', parent_form: '', contact_options: null, user_choice: '', smart_capture_result: null, normalized_otp_code: '', retry_question: '', retry_question_es: '', retry_yes_words: ['phone', 'email', 'cell', 'mobile', 'teléfono', 'telefono', 'celular', 'móvil', 'movil', 'correo electrónico', 'correo', 'try again', 'try it again', 'retry', 'intentar', 'intentarlo', 'intentar de nuevo', 'intentar nuevamente', 'intentarlo de nuevo', 'intentarlo otra vez', 'de nuevo', 'nuevamente', 'otra vez', 'reintentar', 'volver a intentar', 'volver a intentarlo'], retry_profiles: [{ name: 'phone', label: 'phone number', label_es: 'número de teléfono', profile: { kind: 'phone' } }, { name: 'email_address', label: 'email address', label_es: 'correo electrónico', profile: { kind: 'email' } }, ...OTP_PROFILES] }), contract: { role: 'collector', summary: 'Common bounded service retry menu' }, steps: [
252
256
  set('retry_parent', 'parent_form', "parent_form || cargo.authOptions && cargo.authOptions.parent_form || cargo.contactOptions && cargo.contactOptions.parent_form || ''"),
253
257
  set('retry_question', 'retry_question', "error_message+' '+(question || 'Would you like to try again?')+' To retry '+(cargo.voice?'Press 1 or ':'')+cargo.verb+' YES. '+(callerTextNumber(cargo)?'To switch to text press 9 or '+cargo.verb+' TEXT. ':'')+'To abort '+(cargo.voice?'press the asterisk key or ':'')+cargo.verb+' EXIT.'"),
254
258
  set('retry_question_es', 'retry_question_es', "error_message_es+' '+(question_es || '¿Le gustaría intentar nuevamente?')+' Para reintentar '+(cargo.voice?'Presione 1 o ':'')+cargo.verb_es+' SÍ. '+(callerTextNumber(cargo)?'Para cambiar a texto presione 9 o '+cargo.verb_es+' TEXTO. ':'')+'Para salir '+(cargo.voice?'presione la tecla de asterisco o ':'')+cargo.verb_es+' SALIR.'"),
@@ -261,7 +265,7 @@ const retry = worker('generic-retry-with-options', 'GenericRetryWithOptions', 'C
261
265
  set('retry_latest_answer', 'user_choice', 'String(cargo.serviceAnswer.raw || "")'), set('retry_capture', 'smart_capture_result', 'capture_patterns && capture_patterns.length ? normalizeAndFindCapture(user_choice,capture_patterns) : null'),
262
266
  cases('retry_capture_route', { 'condition: smart_capture_result && retry_call_type === "replace"': call('retry_captured_replace', '{{retry_flow}}', { cell_number: '', email: '', '{{smart_capture_result.variable}}': '{{smart_capture_result.value}}', retry_flow: '{{retry_flow}}', cancel_flow: '{{cancel_flow}}', retrying: true, contact_options: null, parent_form: '{{parent_form}}' }, 'replace'), 'condition: smart_capture_result': call('retry_captured_reboot', '{{retry_flow}}', { '{{smart_capture_result.variable}}': '{{smart_capture_result.value}}', retry_flow: '{{retry_flow}}', cancel_flow: '{{cancel_flow}}', retrying: true, contact_options: null, parent_form: '{{parent_form}}' }, 'reboot'), default: effect('retry_no_capture', 'true') }),
263
267
  set('retry_typed_code', 'normalized_otp_code', "allow_code_entry && user_choice.replace(/\\D/g,'').length === 6 ? user_choice.replace(/\\D/g,'') : ''"), when('retry_validate_code', '!!normalized_otp_code', call('retry_code_validate', 'validate-otp-code')),
264
- cases('retry_decision', { 'condition: normalized_otp_code && cargo.otpVerified === true': end('retry_verified'), "condition: (user_choice.trim() === '1' || isAnswer(user_choice,'yes',['phone','email','cell','mobile','teléfono','telefono','celular','móvil','movil','correo electrónico','correo'])) && retry_call_type === 'replace'": call('retry_yes_replace', '{{retry_flow}}', { retrying: true, cell_number: '', email: '', contact_options: null, parent_form: '{{parent_form}}' }, 'replace'), "condition: user_choice.trim() === '1' || isAnswer(user_choice,'yes',['phone','email','cell','mobile','teléfono','telefono','celular','móvil','movil','correo electrónico','correo'])": call('retry_yes_reboot', '{{retry_flow}}', { retrying: true, contact_options: null, parent_form: '{{parent_form}}' }, 'reboot'), "condition: user_choice.trim() === '9' || isAnswer(user_choice,'text')": call('retry_text', 'switch-to-text', undefined, 'reboot'), "condition: isAgentRequest(user_choice)": call('retry_agent', 'live-agent-requested', undefined, 'reboot'), "condition: !user_choice.trim() || /^\\d$/.test(user_choice.trim()) || isExitRequest(user_choice)": call('retry_cancel', '{{cancel_flow}}', undefined, 'reboot'), default: { id: 'retry_dispatch', type: 'DISPATCH', outcome: '{{caller_outcome}}', reason: '{{caller_reason}}' } })
268
+ cases('retry_decision', { 'condition: normalized_otp_code && cargo.otpVerified === true': end('retry_verified'), "condition: (user_choice.trim() === '1' || isAnswer(user_choice,'yes',retry_yes_words)) && retry_call_type === 'replace'": call('retry_yes_replace', '{{retry_flow}}', { retrying: true, cell_number: '', email: '', contact_options: null, parent_form: '{{parent_form}}' }, 'replace'), "condition: user_choice.trim() === '1' || isAnswer(user_choice,'yes',retry_yes_words)": call('retry_yes_reboot', '{{retry_flow}}', { retrying: true, contact_options: null, parent_form: '{{parent_form}}' }, 'reboot'), "condition: user_choice.trim() === '9' || isAnswer(user_choice,'text')": call('retry_text', 'switch-to-text', undefined, 'reboot'), "condition: isAgentRequest(user_choice)": call('retry_agent', 'live-agent-requested', undefined, 'reboot'), "condition: !user_choice.trim() || /^\\d$/.test(user_choice.trim()) || isExitRequest(user_choice)": call('retry_cancel', '{{cancel_flow}}', undefined, 'reboot'), default: { id: 'retry_dispatch', type: 'DISPATCH', outcome: '{{caller_outcome}}', reason: '{{caller_reason}}' } })
265
269
  ] });
266
270
  const EMAIL_CHOICES = { ok: ['1', 'ok', 'okay', 'yes', 'send', 'send it', 'si', 'sí', 'enviar', 'envíalo', 'correcto'], another: ['otro correo', 'otro correo electrónico', 'otro correo electronico', 'otro email', 'otra dirección', 'otra direccion', 'a otro correo', 'another email', 'a different email', 'different email', 'other email', 'another address', 'a different address'] };
267
271
  export function emailReadback(address, voice, es = false) {
@@ -6,6 +6,7 @@ export function isAnswer(input: any, kind: any, extra?: any[]): boolean;
6
6
  /**
7
7
  * Whether the WHOLE answer is an exit word ("exit", "Cancelar.", "salir por favor", "*"), never a sentence that contains
8
8
  * one: "Cancelar el pedido" asks for something; it does not withdraw (prod 2026-10-26, at a verification-code prompt).
9
+ * The word may be repeated ("Salir salir."), echo the prompt ("Salir para cancelar") or end in "now" (prod 2026-10-28).
9
10
  */
10
11
  export function isExitRequest(input: any): boolean;
11
12
  /** Whether the WHOLE answer asks for a person ("agent", "I want to talk to a representative", "0") — for free-text prompts. */
@@ -1 +1 @@
1
- {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAsNA;;;GAGG;AACH,wEAkBC;AAED;;;GAGG;AACH,mDAGC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAiBD;;;;;;GAMG;AACH,mDAAmF;AAEnF,kGAAkG;AAClG;;;EAmCC;AAUD;;;;;;;;;GASG;AACH,6EA4GC;;;;;;;;AAjcD,qCAKwE"}
1
+ {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAsNA;;;GAGG;AACH,wEAkBC;AAED;;;;GAIG;AACH,mDAIC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAiBD;;;;;;GAMG;AACH,mDAAmF;AAEnF,kGAAkG;AAClG;;;EAmCC;AAUD;;;;;;;;;GASG;AACH,6EA4GC;;;;;;;;AAncD,qCAKwE"}
@@ -253,10 +253,12 @@ export function isAnswer(input, kind, extra = []) {
253
253
  /**
254
254
  * Whether the WHOLE answer is an exit word ("exit", "Cancelar.", "salir por favor", "*"), never a sentence that contains
255
255
  * one: "Cancelar el pedido" asks for something; it does not withdraw (prod 2026-10-26, at a verification-code prompt).
256
+ * The word may be repeated ("Salir salir."), echo the prompt ("Salir para cancelar") or end in "now" (prod 2026-10-28).
256
257
  */
257
258
  export function isExitRequest(input) {
258
259
  const b = normalize(input).replace(/^[.\s]+|[.\s]+$/g, '');
259
- return b === '*' || /^(exit|quit|salir|abort|cancel|cancelar)( please| por favor)?$/.test(b);
260
+ const word = '(?:exit|quit|salir|abort|cancel|cancelar)';
261
+ return b === '*' || new RegExp(`^${word}(?:[ .]+${word})*(?: para cancelar| to cancel)?(?: please| por favor| ahora| now)?$`).test(b);
260
262
  }
261
263
  /** Whether the WHOLE answer asks for a person ("agent", "I want to talk to a representative", "0") — for free-text prompts. */
262
264
  export function isAgentRequest(input) {
@@ -347,8 +349,8 @@ export function numericAmountReading(input) {
347
349
  // Mixed answers with an explicit monetary marker or payment verb still parse their independent amount.
348
350
  if (/\b(?:cards?|tarjetas?|ending|termina(?:d[ao]s?)?|visa|mastercard|amex|discover)\b/i.test(raw) && !isAmountAnswer(raw) && !/\b(?:pay|paying|pagar|abonar)(?:\b|(?=\d))/i.test(raw))
349
351
  return { value: NaN, candidates: 0 };
350
- // 1.234,56 (European) → 1234.56; 1,234.56 → 1234.56; 12,50 → 12.50
351
- let t = /\d{1,3}(\.\d{3})+,\d{1,2}\b/.test(raw) ? raw.replace(/\./g, '').replace(',', '.') : raw.replace(/,(?=\d{3}\b)/g, '').replace(/(\d),(\d{1,2})\b/g, '$1.$2');
352
+ // 1.234,56 (European) → 1234.56; 1,234.56 → 1234.56; 12,50 → 12.50; 1.500 → 1500 (three digits after a dot are never cents)
353
+ let t = /\d{1,3}(\.\d{3})+,\d{1,2}\b/.test(raw) ? raw.replace(/\./g, '').replace(',', '.') : raw.replace(/,(?=\d{3}\b)/g, '').replace(/\b\d{1,3}(?:\.\d{3})+\b(?![.,]\d)/g, (m) => m.replace(/\./g, '')).replace(/(\d),(\d{1,2})\b/g, '$1.$2');
352
354
  const dc = t.match(DOLLARS_AND_CENTS);
353
355
  if (dc) {
354
356
  t = t.replace(DOLLARS_AND_CENTS, () => ` $${dc[1]}.${dc[2].padStart(2, '0')} `);
@@ -1 +1 @@
1
- {"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAQA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEA8BC;AAuCD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEA0CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAWC;AACD,uGAAuG;AACvG;;;;;;;;;;;;QAkCC;AAED,0FAA0F;AAC1F;;;SAIC;AACD,gGAAgG;AAChG;;;;;;;;;;;EAQC;AAED,sGAAsG;AACtG,2DAIC;AACD;;;GAGG;AACH;;;;;;;SAkBC;AAED,2FAA2F;AAC3F,oGAIC;AACD,gFA4BC;AAED,2GAA2G;AAC3G;;;EASC;AACD,mEAoBC;AACD,mEAGC;AACD,sEAUC;AACD,+EAEC;AAoCD,qFAYC;AACD,oGAqCC;AACD;;;SAKC;AACD,qEAGC;AACD,gEAgBC;AACD;;uGAEuG;AACvG,sEAGC;AACD,2DAQC;AACD,oGAUC;AAED,uEAAuE;AACvE,wHA+BC;AAED,uHAAuH;AACvH,6EAaC;AACD,mHAAmH;AACnH,iGAsDC;AACD;;;GAGG;AACH;;;SAcC;AACD,+EASC;AACD,8EAeC;AA7oBM,8CAAmE"}
1
+ {"version":3,"file":"form-state.d.ts","sourceRoot":"","sources":["../../src/helpers/form-state.js"],"names":[],"mappings":"AAQA,qDAKC;AAED,oDAEC;AAED,qFAAqF;AACrF,oEA8BC;AA0CD,2FAEC;AACD,8FAcC;AACD,2DAOC;AACD,uEA6CC;AAED,kGAAkG;AAClG,2GAOC;AACD,0HAA0H;AAC1H,uEAwBC;AACD,8HAA8H;AAC9H;;;;;EAWC;AACD,uGAAuG;AACvG;;;;;;;;;;;;QAkCC;AAED,0FAA0F;AAC1F;;;SAIC;AACD,gGAAgG;AAChG;;;;;;;;;;;EAQC;AAED,sGAAsG;AACtG,2DAIC;AACD;;;GAGG;AACH;;;;;;;SAkBC;AAED,2FAA2F;AAC3F,oGAIC;AACD,gFA4BC;AAED,2GAA2G;AAC3G;;;EASC;AACD,mEAoBC;AACD,mEAGC;AACD,sEAUC;AACD,+EAEC;AAoCD,qFAYC;AACD,oGAqCC;AACD;;;SAKC;AACD,qEAGC;AACD,gEAgBC;AACD;;uGAEuG;AACvG,sEAGC;AACD,2DAQC;AACD,oGAUC;AAED,uEAAuE;AACvE,wHA+BC;AAED,uHAAuH;AACvH,6EAaC;AACD,mHAAmH;AACnH,iGAsDC;AACD;;;GAGG;AACH;;;SAcC;AACD,+EASC;AACD,8EAeC;AAnpBM,8CAAmE"}
@@ -70,10 +70,13 @@ export function resolveFormInputs(inputs, presets = {}) {
70
70
  validateFormActivation(input, resolved);
71
71
  return resolved;
72
72
  }
73
- /** Dormant entry is explicit: named descendant fields may propose local controllers, never ambient state. */
73
+ /** Dormant entry is explicit: named descendant fields may propose local controllers, never ambient state. A subform
74
+ * names its entry fields (activate_on); a plain input is its own entry ("NEW CARD, LINK, or a different amount": an
75
+ * amount said there chooses the amount option), so it declares no activate_on. */
74
76
  function validateFormActivation(input, specs) {
75
- if (!input.form || !input.activate || typeof input.activate !== 'object' || Array.isArray(input.activate) || !Object.keys(input.activate).length || !Array.isArray(input.activate_on) || !input.activate_on.length)
76
- throw new Error(`FORM activation ${input.name} needs a subform, controllers and activate_on fields`);
77
+ const plain = !input.form && !!input.profile && input.activate_on === undefined;
78
+ if (!input.activate || typeof input.activate !== 'object' || Array.isArray(input.activate) || !Object.keys(input.activate).length || !plain && (!input.form || !Array.isArray(input.activate_on) || !input.activate_on.length))
79
+ throw new Error(`FORM activation ${input.name} needs controllers, and a subform with activate_on fields or a plain input without them`);
77
80
  for (const [name, value] of Object.entries(input.activate)) {
78
81
  const controller = specs.flatMap(s => s.any_of || [s]).find(s => s.name === name);
79
82
  if (!controller?.profile || controller.form || controller.bind || name === input.name || !hasFormValue(value) || typeof value === 'object')
@@ -82,7 +85,7 @@ function validateFormActivation(input, specs) {
82
85
  if (!reading || !own(reading, 'value') || !hasFormValue(reading.value) || !formValuesEqual(reading.value, value))
83
86
  throw new Error(`FORM activation ${input.name} has invalid controller value ${name}`);
84
87
  }
85
- for (const path of input.activate_on) {
88
+ for (const path of input.activate_on || []) {
86
89
  if (typeof path !== 'string' || !path || path.split('.').some(name => !name || forbidden.has(name)))
87
90
  throw new Error(`FORM activation ${input.name} has invalid entry field`);
88
91
  if (input.schema) {
@@ -188,11 +191,14 @@ export function formInputChain(forms, id, cargo = {}) {
188
191
  for (const input of spec.any_of || [spec]) {
189
192
  if (entries && !entries.includes(input.name))
190
193
  continue;
191
- if (formCondition(input, state.values, cargo, state) !== false && !input.form && input.profile)
194
+ const inputCondition = formCondition(input, state.values, cargo, state);
195
+ // A dormant plain input that declares activate proposes its controllers when it is answered.
196
+ const inputDormant = inputCondition !== true && !activation && !input.form && input.activate !== undefined ? dormantActivation(input, state, cargo) : null;
197
+ if ((inputCondition !== false || inputDormant) && !input.form && input.profile)
192
198
  out.push({ ...input,
193
199
  ...(prefix ? { label: `${prefix}: ${input.label || input.name}`, label_es: `${prefixEs || prefix}: ${input.label_es || input.label || input.name}` } : {}),
194
200
  ...(input.choices_from && Array.isArray(state.data?.[input.choices_from]) ? { profile: { ...input.profile, choices: state.data[input.choices_from] } } : {}),
195
- ...(activation ? { activation } : {}), form: target, scope, ...(spec.any_of ? { group: spec.name, exclusive: spec.exclusive === true } : {}), value: state.values[input.name],
201
+ ...(activation || inputDormant ? { activation: activation || inputDormant } : {}), form: target, scope, ...(spec.any_of ? { group: spec.name, exclusive: spec.exclusive === true } : {}), value: state.values[input.name],
196
202
  // its declared prerequisites hold accepted values: a field that cannot be validated yet is not a context for
197
203
  // an unnamed out-of-order answer (contract, reference §5)
198
204
  ready: (input.depends_on || spec.depends_on || []).every(name => hasFormValue(state.values[name])) });
@@ -211,7 +217,7 @@ export function enrichFormMatches(matches, chain, form, names = [], asked = '')
211
217
  const enrich = h => {
212
218
  const spec = chain.find(c => c.form === h.form && c.name === h.name) || {};
213
219
  return { ...h, label: h.option_label || spec.label || h.name, label_es: h.option_label_es || spec.label_es || spec.label || h.name, ...(h.option_label ? { option: true } : {}),
214
- validator: spec.validator || '', ...(spec.response ? { response: spec.response } : {}), after: spec.after || '', validate_always: spec.validate_always === true, ...(spec.activation ? { activation: spec.activation } : {}), pii: spec.pii === true, current: h.form === form && names.includes(h.name), asked };
220
+ validator: spec.validator || '', ...(spec.response ? { response: spec.response } : {}), after: spec.after || '', validate_always: spec.validate_always === true, ...(spec.activation ? { activation: spec.activation } : {}), ...(spec.silent === true ? { silent: true } : {}), pii: spec.pii === true, current: h.form === form && names.includes(h.name), asked };
215
221
  };
216
222
  return { ...matches, hits: matches.hits.map(enrich), ambiguous: matches.ambiguous.map(enrich), remaining: (matches.remaining || []).map(group => group.map(enrich)) };
217
223
  }
@@ -1 +1 @@
1
- {"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;AAqED;;;GAGG;AACH,sFAwGC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;mHACmH;AACnH;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;EAoLC"}
1
+ {"version":3,"file":"match-inputs.d.ts","sourceRoot":"","sources":["../../src/helpers/match-inputs.js"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,sEAQC;AAqED;;;GAGG;AACH,sFAwGC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;mHACmH;AACnH;;;;;;;;;;;;;;;;;;;;;EAeC;AACD;;;;;;;;;;;;;;;;;;;;;;EAoMC"}
@@ -299,6 +299,9 @@ export function matchInputs(answer, candidates, current, choiceKey) {
299
299
  const spanText = (reading) => (reading.spans || []).map(([a, b]) => raw.slice(a, b)).join(' ');
300
300
  const refused = [];
301
301
  let readings = [];
302
+ // An invited input is read as in order while it is asked (docs/copilot-reference.md §5): its asked signature, not
303
+ // its narrower out-of-order one ("40" at "NEW CARD, LINK, or a different amount" is the amount).
304
+ const invitedInput = new Set([...list.filter(isCur).flatMap((c) => Array.isArray(c.invites) ? c.invites : []), ...(Array.isArray(cur.invites) ? cur.invites : [])]);
302
305
  for (const c of list) {
303
306
  // Rule 2: a confirmation is answered only when asked, firm by CONFIRM_WORDS, soft by the wider YES vocabulary.
304
307
  const confirmation = ['firm', 'soft'].includes(c.confirmation) ? c.confirmation : '';
@@ -313,7 +316,7 @@ export function matchInputs(answer, candidates, current, choiceKey) {
313
316
  }
314
317
  const vocabulary = confirmation ? { ...c.profile, choices: (c.profile.choices || []).map((x) => x.key === 'yes' ? { ...x, phrases: [...(x.phrases ?? x.words ?? []), ...(confirmation === 'firm' ? CONFIRM_WORDS : ANSWER_WORDS.yes)], exact: confirmation === 'firm' || x.exact === true } : x.key === 'no' ? { ...x, phrases: [...(x.phrases ?? x.words ?? []), ...ANSWER_WORDS.no] } : x) } : c.profile;
315
318
  const profile = narrativeCurrent && vocabulary.kind === 'email' ? { ...vocabulary, literal: true } : vocabulary;
316
- let r = readInputProfile(profile, raw, isCur(c));
319
+ let r = readInputProfile(profile, raw, isCur(c) || invitedInput.has(c.name) && c.confirmation === undefined);
317
320
  // A semantic choice supplied by ask-choice (e.g. spoken keypad keys) is a reading, not an early return.
318
321
  if (!r && c === own && choiceKey && c.profile.kind === 'choice' && (c.profile.choices || []).some(x => x.key === choiceKey)) {
319
322
  r = { ...(r || {}), value: choiceKey, spans: r?.spans?.length ? r.spans : [[0, raw.length]] };
@@ -346,6 +349,20 @@ export function matchInputs(answer, candidates, current, choiceKey) {
346
349
  const wholeRuns = (reading) => runs.every(([a, b]) => !(reading.spans || []).some(([s, e]) => s < b && a < e) || (reading.spans || []).some(([s, e]) => s <= a && e >= b));
347
350
  // Rule 4: a bare number has no money mark and no word in its evidence.
348
351
  const bare = (reading) => !isAmountAnswer(raw) && (reading.spans || []).length > 0 && (reading.spans || []).every(([a, b]) => /\d/.test(raw.slice(a, b)) && !/\p{L}/u.test(raw.slice(a, b)));
352
+ // Rule 1 by shape: a bare number whose length is the current question's own keypad length (`digits`) answers that
353
+ // question even when its reader rejects it (an expiration "1320" is month 13); its validation decides and the
354
+ // question is asked again. It is never read out of order as another field (that "1320" is not a security code).
355
+ // A choice is excluded (a digit it does not offer reaches the invited amount, rule 4), and so is an amount.
356
+ const bareDigits = raw.replace(/\D/g, '');
357
+ const shapeOf = own && own.digits && !['choice', 'amount', 'text'].includes(own.profile.kind) ? own.digits : null;
358
+ if (shapeOf && bareDigits && /^[\d\s./-]+$/.test(raw.trim()) && !isAmountAnswer(raw) && bareDigits.length >= Number(shapeOf.min || 1) && bareDigits.length <= Number(shapeOf.max || shapeOf.min || 1)
359
+ && !readings.some((reading) => reading.current || invited(reading))) {
360
+ readings.push({ form: own.form, name: own.name, value: bareDigits, ...(own.validator ? {} : { invalid: true }), known: false, repeatedChoice: false, wholeChoice: false, choice: false, narrative: false, resetOn: [], spans: [[0, raw.length]], keypad: false, fallback: false,
361
+ group: own.group, exclusive: own.exclusive === true, level: forms.indexOf(own.scope || own.form), current: true, confirm: Array.isArray(own.confirm_against) ? own.confirm_against : [] });
362
+ }
363
+ else {
364
+ // not a bare number of the current question's shape, or the current question read it
365
+ }
349
366
  readings = readings.filter((reading) => {
350
367
  if (inOrder(reading)) {
351
368
  if (!reading.current)
@@ -1 +1 @@
1
- export const JSFE_RUNTIME_FINGERPRINT = "sha256:d2fe25c463500cc6a524418c9ccb5ac710130ac6cce764894ca522f6fe83a22c";
1
+ export const JSFE_RUNTIME_FINGERPRINT = "sha256:32c2bede7bd93be3b95aaa402b64aaf747b89e7827f81566e9bba9876f044823";
@@ -149,7 +149,7 @@ Use one of these input shapes:
149
149
 
150
150
  | Shape | Fields |
151
151
  | --- | --- |
152
- | Preset field | `name`, `field`; optional overrides. Presets: `phone`, `email`, `zip`, `account_number`, `ssn_last4`, `amount`, `street_address`, `city`, `state`, `card_number`, `card_expiration`, `card_cvv`, `card_zip`. |
152
+ | Preset field | `name`, `field`; optional overrides. Presets: `phone`, `email`, `zip`, `account_number`, `ssn_last4`, `amount`, `street_address`, `city`, `state`, `card_number`, `card_expiration`, `card_cvv`, `card_zip`. `card_cvv` takes its card brand's length (`validate-card-cvv`: 4 digits for American Express, 3 for every other brand). |
153
153
  | Inline field | `name`, `profile`, questions and business validation. |
154
154
  | Child form | `name`, `form:"installed-child-id"`, optional `parameters`. JSFE links its schema; do not copy the derived `schema` into editable source. |
155
155
  | Alternative group | `name`, `any_of:[leaf-or-child declarations]`; optional `exclusive:true`. At least one applicable member satisfies the group; exclusive competing answers require clarification. |
@@ -172,7 +172,8 @@ Field options:
172
172
  | `choices_from`, `question_from` | Read prepared choices/question from the owner's `data`; corresponding localized question storage is handled by shared workers. |
173
173
  | `digits`, `pii`, `as_spoken` | Collection hints. FORM preserves original speech by default; false explicitly opts into cleanup. |
174
174
  | `confirm_against` | Declare overlapping field interpretations that require clarification even when normal current-field priority applies. |
175
- | `activate`, `activate_on` | Narrow dormant-child activation: proposed controller assignments plus nominated descendant entry paths. Controller and entry validate atomically; a rejected entry must not flip a controller. |
175
+ | `activate`, `activate_on` | Narrow dormant activation: proposed controller assignments plus, for a subform, nominated descendant entry paths (`activate_on`). A dormant plain input declares `activate` alone and is its own entry: an amount said at "NEW CARD, LINK, or a different amount" chooses the amount option (`revised_amount` with `when:{next:'amount'}`, `activate:{next:'amount'}`). Controller and entry validate atomically; a rejected entry must not flip a controller. |
176
+ | `silent` | An accepted answer is not acknowledged. For an input that hands its value on to another form's input (its `after` flow), whose own acceptance or rejection is what the caller hears. |
176
177
  | `response` | `{description}` on a readable leaf; use existing contextual DISPATCH extraction before accepting an open-ended answer. |
177
178
  | `proposal` | Optional approval of an inferred field candidate, independent of final action consent. |
178
179
  | `max_misses`, `max_attempts`, `on_exhausted` | Bounds for unreadable responses, validator rejections and deliberate branch recovery. |
@@ -214,7 +215,10 @@ Declarations:
214
215
  Rules, in order:
215
216
 
216
217
  1. **The current question first.** The whole answer is read against the current input's signature and the inputs it
217
- invites. A match is that answer.
218
+ invites (an invited input with its asked signature, not its out-of-order one). A match is that answer. A bare number
219
+ whose length is the current input's keypad length (`digits`) is its answer even when its reader rejects it: its
220
+ validation decides and the question is asked again, so an expiration "1320" (month 13) is never a security code.
221
+ A choice keeps rule 4, and an amount is read by its own profile.
218
222
  2. **A confirmation is answered only when it is asked.** Out of order it never makes sense, so no words, YES, NO or the
219
223
  option's own phrases, fill a confirmation that is not the current question.
220
224
  3. **Out of order requires a ready field and the whole signature.** A bare number never fills, out of order, a field whose
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsfe",
3
- "version": "1.12.0",
3
+ "version": "1.13.1",
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",