jsfe 1.6.0 → 1.7.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.
Files changed (100) hide show
  1. package/README.md +27 -4
  2. package/dist/forms/action-projection.d.ts +4 -0
  3. package/dist/forms/action-projection.d.ts.map +1 -0
  4. package/dist/forms/action-projection.js +21 -0
  5. package/dist/forms/action-recovery.d.ts +12 -0
  6. package/dist/forms/action-recovery.d.ts.map +1 -0
  7. package/dist/forms/action-recovery.js +125 -0
  8. package/dist/forms/action-tools.d.ts +29 -0
  9. package/dist/forms/action-tools.d.ts.map +1 -0
  10. package/dist/forms/action-tools.js +6 -0
  11. package/dist/forms/address-flows.d.ts +2 -0
  12. package/dist/forms/address-flows.d.ts.map +1 -0
  13. package/dist/forms/address-flows.js +39 -0
  14. package/dist/forms/choice-flow.d.ts +419 -0
  15. package/dist/forms/choice-flow.d.ts.map +1 -0
  16. package/dist/forms/choice-flow.js +707 -0
  17. package/dist/forms/contracts.d.ts +237 -0
  18. package/dist/forms/contracts.d.ts.map +1 -0
  19. package/dist/forms/contracts.js +278 -0
  20. package/dist/forms/flows.d.ts +611 -0
  21. package/dist/forms/flows.d.ts.map +1 -0
  22. package/dist/forms/flows.js +675 -0
  23. package/dist/forms/identity-flows.d.ts +17 -0
  24. package/dist/forms/identity-flows.d.ts.map +1 -0
  25. package/dist/forms/identity-flows.js +302 -0
  26. package/dist/forms/index.d.ts +7 -0
  27. package/dist/forms/index.d.ts.map +1 -0
  28. package/dist/forms/index.js +10 -0
  29. package/dist/forms/prompt-compiler.d.ts +3 -0
  30. package/dist/forms/prompt-compiler.d.ts.map +1 -0
  31. package/dist/forms/prompt-compiler.js +30 -0
  32. package/dist/helpers/address.d.ts +27 -0
  33. package/dist/helpers/address.d.ts.map +1 -0
  34. package/dist/helpers/address.js +29 -0
  35. package/dist/helpers/answer.d.ts +5 -0
  36. package/dist/helpers/answer.d.ts.map +1 -1
  37. package/dist/helpers/answer.js +5 -3
  38. package/dist/helpers/choice-offer.d.ts +3 -0
  39. package/dist/helpers/choice-offer.d.ts.map +1 -0
  40. package/dist/helpers/choice-offer.js +6 -0
  41. package/dist/helpers/form-actions.d.ts +13 -0
  42. package/dist/helpers/form-actions.d.ts.map +1 -0
  43. package/dist/helpers/form-actions.js +144 -0
  44. package/dist/helpers/form-sources.d.ts +7 -0
  45. package/dist/helpers/form-sources.d.ts.map +1 -0
  46. package/dist/helpers/form-sources.js +47 -0
  47. package/dist/helpers/form-state.d.ts +94 -0
  48. package/dist/helpers/form-state.d.ts.map +1 -0
  49. package/dist/helpers/form-state.js +870 -0
  50. package/dist/helpers/index.d.ts +114 -1
  51. package/dist/helpers/index.d.ts.map +1 -1
  52. package/dist/helpers/index.js +9 -4
  53. package/dist/helpers/match-inputs.d.ts +14 -0
  54. package/dist/helpers/match-inputs.d.ts.map +1 -1
  55. package/dist/helpers/match-inputs.js +154 -41
  56. package/dist/helpers/money.d.ts +12 -0
  57. package/dist/helpers/money.d.ts.map +1 -0
  58. package/dist/helpers/money.js +54 -0
  59. package/dist/helpers/spoken-money.d.ts +20 -0
  60. package/dist/helpers/spoken-money.d.ts.map +1 -0
  61. package/dist/helpers/spoken-money.js +143 -0
  62. package/dist/host/action-journal.d.ts +98 -0
  63. package/dist/host/action-journal.d.ts.map +1 -0
  64. package/dist/host/action-journal.js +218 -0
  65. package/dist/host/action-session.d.ts +12 -0
  66. package/dist/host/action-session.d.ts.map +1 -0
  67. package/dist/host/action-session.js +29 -0
  68. package/dist/host/capabilities.d.ts +12 -0
  69. package/dist/host/capabilities.d.ts.map +1 -0
  70. package/dist/host/capabilities.js +43 -0
  71. package/dist/host/checkpoint-codec-error.d.ts +6 -0
  72. package/dist/host/checkpoint-codec-error.d.ts.map +1 -0
  73. package/dist/host/checkpoint-codec-error.js +4 -0
  74. package/dist/host/index.d.ts +6 -0
  75. package/dist/host/index.d.ts.map +1 -1
  76. package/dist/host/index.js +6 -0
  77. package/dist/host/intent-stages.d.ts +1 -0
  78. package/dist/host/intent-stages.d.ts.map +1 -1
  79. package/dist/host/intent-stages.js +3 -3
  80. package/dist/host/node.d.ts +17 -0
  81. package/dist/host/node.d.ts.map +1 -0
  82. package/dist/host/node.js +64 -0
  83. package/dist/host/recovery-identity.d.ts +16 -0
  84. package/dist/host/recovery-identity.d.ts.map +1 -0
  85. package/dist/host/recovery-identity.js +73 -0
  86. package/dist/host/recovery-policy.d.ts +5 -0
  87. package/dist/host/recovery-policy.d.ts.map +1 -0
  88. package/dist/host/recovery-policy.js +54 -0
  89. package/dist/host/runtime-manifest.d.ts +3 -0
  90. package/dist/host/runtime-manifest.d.ts.map +1 -0
  91. package/dist/host/runtime-manifest.js +38 -0
  92. package/dist/index.d.ts +55 -0
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +155 -45
  95. package/dist/runtime-identity.d.ts +3 -0
  96. package/dist/runtime-identity.d.ts.map +1 -0
  97. package/dist/runtime-identity.js +1 -0
  98. package/docs/development-release.md +23 -0
  99. package/docs/forms.md +122 -0
  100. package/package.json +15 -4
@@ -0,0 +1,17 @@
1
+ /** Bind an idempotent gate lifecycle marker to its owning task, independent of the authentication protocol. */
2
+ export function setTaskGate(cargo: any, form: any, gate: any, active: any): boolean;
3
+ /** Normalize spoken digits without interpreting numbers embedded in unrelated words. */
4
+ export function serviceDigits(text: any): string;
5
+ export function otpDigits(text: any): string;
6
+ export function contactPrompt(cargo: any, options: any, callerAllowed: any, partial?: boolean, es?: boolean): string;
7
+ export function contactError(kind: any, options: any, es?: boolean): string;
8
+ export function otpPrompt(cargo: any, es?: boolean): string;
9
+ export function emailReadback(address: any, voice: any, es?: boolean): string;
10
+ /** Reenter the existing owning FORM or its gate without making the owner its own parent. */
11
+ export function identityResumeContext(forms: any, owner: any, target: any, identifiers?: {}): {
12
+ form_instance: any;
13
+ parent_form: any;
14
+ result_input: any;
15
+ };
16
+ export const FLOWS: any[];
17
+ //# sourceMappingURL=identity-flows.d.ts.map
@@ -0,0 +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;AA8ED,4DAKC;AAwED,8EAGC;AA4BD,4FAA4F;AAC5F;;;;EAOC;AAQD,0BAAmI"}
@@ -0,0 +1,302 @@
1
+ /** Shared identity protocols. Every conversational answer enters the ordinary FORM acceptance/matching pipeline. */
2
+ import { defineWorker, defineForm } from './contracts.js';
3
+ import { OTP_EXPIRY_MS } from '../helpers/otp-check.js';
4
+ const vars = o => Object.fromEntries(Object.entries(o).map(([k, v]) => [k, { type: Array.isArray(v) ? 'array' : v === null || typeof v === 'object' ? 'object' : typeof v, value: v }]));
5
+ const param = (name, description, type = 'string') => ({ name, type, description });
6
+ const set = (id, variable, value) => ({ id, type: 'SET', variable, value });
7
+ const effect = (id, value) => set(id, 'noop', value);
8
+ const call = (id, value, parameters, callType = 'call') => ({ id, type: 'FLOW', value, callType, ...(parameters ? { parameters } : {}) });
9
+ const when = (id, condition, step) => ({ id, type: 'CASE', branches: { [`condition: ${condition}`]: step, default: effect(id + '_skip', 'true') } });
10
+ const cases = (id, branches) => ({ id, type: 'CASE', branches });
11
+ const say = (id, value, value_es) => ({ id, type: 'SAY', value, value_es });
12
+ const end = id => ({ id, type: 'END' });
13
+ const expression = (fn, ...args) => `(${fn.toString()})(${args.join(',')})`;
14
+ const worker = (id, name, summary, extra) => defineWorker({ id, name, scope: 'shared', description: summary, contract: { role: 'worker', summary }, ...extra });
15
+ const otpMenuMessages = [];
16
+ const raw = { name: 'raw', label: 'answer', label_es: 'respuesta', profile: { kind: 'text' } };
17
+ const SERVICE_RESULT = 'cargo.serviceAnswer';
18
+ const service = defineForm({ id: 'collect-service-answer', name: 'CollectServiceAnswer', description: 'Collect one protocol answer against explicit profiles and the owning FORM ancestry; return its accepted readings and original text.', contract: { summary: 'Shared FORM adapter for field-specific protocol services', result: { path: SERVICE_RESULT, type: 'object' } }, parameters: [param('profiles', 'Declared alternative input profiles', 'array'), param('question', 'English prompt'), param('question_es', 'Spanish prompt'), param('keypad', 'Keypad completion metadata', 'object'), param('prefill', 'An activation answer to validate through the same profiles'), param('context', 'Exit context'), param('context_es', 'Spanish exit context'), param('stop_path', 'State path that completes this protocol without another answer')], extraVars: { profiles: [], question: '', question_es: '', keypad: null, prefill: '', context: 'your request', context_es: 'su solicitud', stop_path: '', service_initial: [], service_raw: '', service_chain: [], service_matches: null, service_prefilled: false }, stop: '{{stop_path}}', close_on_stop: true, inputs: [{ name: 'answer', any_of: [raw], question_from: 'question', answer_precheck: 'service-answer-raw' }],
19
+ before: [
20
+ effect('service_declaration', "(function () {var state=cargo.forms[form_instance];state.data={question:question,question_es:question_es};state.specs=[{name:'answer',any_of:Array.isArray(profiles) && profiles.length?profiles:[{name:'raw',label:'answer',label_es:'respuesta',profile:{kind:'text'}}],question_from:'question',digits:keypad,answer_precheck:'service-answer-raw',as_spoken:true}];return true;})()"),
21
+ set('service_chain', 'service_chain', 'formInputChain(cargo.forms,form_instance,cargo)'),
22
+ set('service_prefill', 'service_matches', "prefill ? enrichFormMatches(matchInputs(prefill,service_chain,{form:form_instance,names:cargo.forms[form_instance].specs[0].any_of.map(function(f){return f.name;})},''),service_chain,form_instance,cargo.forms[form_instance].specs[0].any_of.map(function(f){return f.name;}),'answer') : {hits:[],ambiguous:[]}"),
23
+ when('service_prefill_ambiguous', 'service_matches.ambiguous.length > 0', call('service_prefill_clarify', 'disambiguate-input', { readings: '{{service_matches.ambiguous}}', hits: '{{service_matches.hits}}' })),
24
+ when('service_accept_prefill', '!service_matches.ambiguous.length && service_matches.hits.length > 0', call('service_accept_initial', 'collect-input-accept', { hits: '{{service_matches.hits}}' })),
25
+ set('service_prefill_taken', 'service_prefilled', "!!prefill && nextFormInput(cargo.forms[form_instance],cargo) === ''"),
26
+ set('service_context', 'exit_context', 'context'), set('service_context_es', 'exit_context_es', 'context_es')
27
+ ],
28
+ after: [effect('service_return', "(cargo.serviceAnswer={raw:service_prefilled ? prefill : String(cargo.choiceText || ''),values:Object.assign({},cargo.forms[form_instance].values),instance:form_instance},true)")] });
29
+ /** Bind an idempotent gate lifecycle marker to its owning task, independent of the authentication protocol. */
30
+ export function setTaskGate(cargo, form, gate, active) {
31
+ const state = cargo.forms && cargo.forms[form];
32
+ const owner = state && cargo.forms[state.task];
33
+ if (!owner || !gate)
34
+ return false;
35
+ if (active) {
36
+ owner.activeGates ||= {};
37
+ owner.activeGates[gate] = true;
38
+ }
39
+ else if (owner.activeGates) {
40
+ delete owner.activeGates[gate];
41
+ if (!Object.keys(owner.activeGates).length)
42
+ delete owner.activeGates;
43
+ }
44
+ return true;
45
+ }
46
+ const taskGate = worker('task-gate', 'TaskGate', 'Mark the lifecycle of a task-owned gate so private provider questions wait for the existing gate rather than opening another.', {
47
+ scope: 'isolated', parameters: [param('form', 'Owning FORM instance'), param('gate', 'Stable gate service identifier'), param('active', 'Whether this gate is active', 'boolean')],
48
+ variables: vars({ form: '', gate: '', active: false }), contract: { role: 'worker', summary: 'Task-owned gate lifecycle for deferred provider requests' },
49
+ steps: [effect('task_gate_state', expression(setTaskGate, 'cargo', 'form', 'gate', 'active === true || active === \'true\''))]
50
+ });
51
+ const rawTransport = worker('service-answer-raw', 'ServiceAnswerRaw', 'Leave protocol commands to their owning service rather than the generic menu transport.', { parameters: [param('answer', 'Raw answer')], steps: [set('service_raw_key', 'answer_key', "'service:raw'")] });
52
+ /** Normalize spoken digits without interpreting numbers embedded in unrelated words. */
53
+ export function serviceDigits(text) {
54
+ const words = { zero: '0', cero: '0', one: '1', uno: '1', two: '2', dos: '2', three: '3', tres: '3', four: '4', cuatro: '4', five: '5', cinco: '5', six: '6', seis: '6', seven: '7', siete: '7', eight: '8', ocho: '8', nine: '9', nueve: '9' };
55
+ return String(text || '').replace(/[a-záéíóúñ]+/gi, word => Object.hasOwn(words, word.toLowerCase()) ? words[word.toLowerCase()] : word);
56
+ }
57
+ export function otpDigits(text) {
58
+ const map = { zero: '0', cero: '0', one: '1', uno: '1', two: '2', dos: '2', three: '3', tres: '3', four: '4', cuatro: '4', five: '5', cinco: '5', six: '6', seis: '6', seven: '7', siete: '7', eight: '8', ocho: '8', nine: '9', nueve: '9' };
59
+ const filler = new Set(['el', 'mi', 'es', 'codigo', 'code', 'is', 'my', 'the', 'numero', 'number', 'a', 'ver']);
60
+ const tokens = String(text || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').split(/[\s,.;:!?-]+/).filter(t => t && !filler.has(t));
61
+ const spoken = tokens.length && tokens.some(t => Object.hasOwn(map, t)) && tokens.every(t => /^\d+$/.test(t) || Object.hasOwn(map, t)) ? tokens.map(t => map[t] ?? t).join('') : '';
62
+ return spoken.length === 6 ? spoken : String(text || '').replace(/\D/g, '');
63
+ }
64
+ export function contactPrompt(cargo, options, callerAllowed, partial = false, es = false) {
65
+ if (partial)
66
+ return es ? 'Continúe.' : 'Go on.';
67
+ const verb = es ? cargo.verb_es : cargo.verb;
68
+ const account = options.account_pattern ? (es ? options.account_name_es || options.account_name : options.account_name) : '';
69
+ const alternative = account ? (es ? ', o ' : ', or ') + account : '';
70
+ const lead = cargo.contactIntroSkip || options.fromMenu ? '' : (es ? options.intro_es || options.intro : options.intro) || '';
71
+ const key = cargo.voice ? (es ? callerAllowed ? ' seguido de la tecla numeral' : ' seguido de la tecla numeral si se ingresó usando el teclado de marcación' : callerAllowed ? ' followed by the pound key' : ' followed by the pound key if entered using the dial pad') : '';
72
+ const question = callerAllowed ? (es ? `Para usar su identificador de llamadas, por favor ${verb} sí. De lo contrario, por favor ${verb}${cargo.voice ? ' o ingrese' : ''} el teléfono o correo electrónico asociado con su cuenta${alternative}${key}.` : `To use your caller id, please ${verb} yes. Otherwise, please ${verb}${cargo.voice ? ' or enter' : ''} the phone or email associated with your account${alternative}${key}.`) : (es ? `Por favor ${verb}${cargo.voice ? ' o ingrese' : ''} el teléfono o correo electrónico asociado con su cuenta${alternative}${key}.` : `Please ${verb}${cargo.voice ? ' or enter' : ''} the phone or email associated with your account${alternative}${key}.`);
73
+ return (lead ? lead + ' ' : '') + question + (es ? ` Para salir en cualquier momento, puede ${cargo.voice ? 'presionar la tecla de asterisco o decir' : 'escribir'} SALIR.` : ` To exit anytime you can ${cargo.voice ? 'press the asterisk key or ' : ''}${verb} EXIT.`);
74
+ }
75
+ const contactParams = [param('retry_flow', 'Retry destination'), param('cancel_flow', 'Exit destination'), param('cell_number', 'Phone already supplied'), param('email', 'Email already supplied'), param('acct_number', 'Account identifier already supplied'), param('account_pattern', 'Tenant account identifier pattern'), param('account_name', 'English account identifier label'), param('account_name_es', 'Spanish account identifier label'), param('account_shape', 'Tenant keypad identifier shapes', 'array'), param('intro', 'English introduction'), param('intro_es', 'Spanish introduction'), param('caller_on_file', 'yes, no or unknown caller-ID eligibility'), param('retry_call_type', 'Retry destination invocation policy'), param('validate_flow', 'Contact existence validator'), param('merge_partials', 'Allow split account identifiers', 'boolean'), param('parent_form', 'Owning task FORM instance'), param('contact_options', 'Internal options across protocol transitions', 'object'), param('retrying', 'Retry-menu continuation', 'boolean'), param('gce_partial', 'Internal split identifier continuation', 'boolean'), param('gce_rejected', 'Internal rejected contact continuation', 'boolean')];
76
+ const contactDefaults = { retry_flow: 'authenticate-user', cancel_flow: 'contact-support', cell_number: '', email: '', acct_number: '', account_pattern: '', account_name: '', account_name_es: '', account_shape: [], intro: '', intro_es: '', caller_on_file: '', retry_call_type: 'reboot', validate_flow: '', merge_partials: false, parent_form: '', contact_options: null, retrying: false, gce_partial: false, gce_rejected: false, contact_allowed: false, contact_prefill: '', contact_partial: false, contact_text: '', contact_digits: '', contact_mail: '', contact_phone: '', contact_id: null, contact_profiles: [], contact_keypad: null, contact_question: '', contact_question_es: '', contact_check_key: '', contact_error: '', contact_error_es: '', contact_kind: '' };
77
+ const contactProfiles = [{ 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' } }, { name: 'account', label: 'account number', label_es: 'número de cuenta', profile: { kind: 'identifier', pattern: '(?!)' } }, raw];
78
+ const contactReplay = (id, extra = {}) => call(id, 'get-cell-or-email', { contact_options: '{{contact_options}}', cell_number: '', email: '', acct_number: '', ...extra }, 'replace');
79
+ const contactMenu = id => call(id, 'generic-retry-with-options', { error_message: '{{contact_error}}', error_message_es: '{{contact_error_es}}', retry_flow: '{{contact_options.retry_flow}}', cancel_flow: '{{contact_options.cancel_flow}}', capture_patterns: '{{cargo.authCapture}}', retry_call_type: '{{contact_options.retry_call_type}}', parent_form: '{{contact_options.parent_form}}', contact_options: '{{contact_options}}', gce_menu: true }, 'replace');
80
+ export function contactError(kind, options, es = false) {
81
+ const account = options.account_pattern ? (es ? options.account_name_es || options.account_name : options.account_name) : '';
82
+ if (kind === 'international')
83
+ return es ? "Un número fuera de EE. UU. y Canadá no se puede usar aquí. Por favor indique el correo de su cuenta" + (account ? ', o ' + account : '') + ", o un celular de EE. UU. o Canadá." : "A number outside the US and Canada can't be used here. Please give the email on your account" + (account ? ', or ' + account : '') + ", or a US or Canadian mobile number.";
84
+ if (kind === 'invalid_phone')
85
+ return es ? 'El número de teléfono que proporcionó no es válido.' : "Sorry, the phone number you provided isn't valid.";
86
+ return es ? 'Lo siento, necesito el teléfono o correo electrónico asociado con su cuenta' + (account ? ', o ' + account : '') + ' para continuar.' : 'Sorry, I do need the phone or email associated with your account' + (account ? ', or ' + account : '') + ' to proceed.';
87
+ }
88
+ const contact = worker('get-cell-or-email', 'GetCellOrEmail', 'Identify a supplied contact or account number; collect missing data through a FORM, with caller-ID eligibility and bounded contact validation.', {
89
+ parameters: contactParams, variables: vars({ ...contactDefaults, contact_profiles_base: contactProfiles }), contract: { role: 'collector', summary: 'Contact identification with FORM ancestry', result: { paths: ['cell_number', 'email', 'acct_number', 'cargo.contactKind', 'cargo.contactValue'], type: 'object' } },
90
+ steps: [
91
+ set('identity_contact_options', 'contact_options', "contact_options || (retrying && cargo.contactOptions ? Object.assign({},cargo.contactOptions,{fromMenu:true,intro:'',intro_es:''}) : {retry_flow:retry_flow || 'authenticate-user',cancel_flow:cancel_flow || 'contact-support',account_pattern:account_pattern,account_name:account_name,account_name_es:account_name_es,account_shape:account_shape,intro:intro,intro_es:intro_es,caller_on_file:caller_on_file,retry_call_type:retry_call_type || 'reboot',validate_flow:validate_flow,merge_partials:merge_partials === true || merge_partials === 'true',parent_form:parent_form})"),
92
+ effect('identity_contact_remember', '(cargo.contactOptions=contact_options,cargo.authCapture=[{variable:"cell_number",reader:"phone"},{variable:"email",reader:"email"}].concat(contact_options.account_pattern?[{variable:"acct_number",regex:contact_options.account_pattern,normalizer:"[^0-9]"}]:[]),true)'),
93
+ when('identity_contact_new_rejection_series', '!gce_rejected', effect('identity_contact_rejections_reset', '(cargo.gceRejects=0,true)')),
94
+ set('identity_contact_partial', 'contact_partial', "!!(contact_options.merge_partials && gce_partial && cargo.gcePartial)"),
95
+ when('identity_contact_partial_reset', '!contact_partial', effect('identity_contact_partial_forget', "(cargo.gcePartial='',true)")),
96
+ set('identity_contact_allowed', 'contact_allowed', "!!cargo.callerId && !callerIsInternational(cargo) && !cargo.callerDeclined && contact_options.caller_on_file !== 'no'"),
97
+ set('identity_contact_prefill', 'contact_prefill', "String(acct_number || cell_number || email || (!contact_partial && contact_allowed && contact_options.caller_on_file === 'yes' ? cargo.callerId : ''))"),
98
+ effect('identity_contact_preselected', "(acct_number || cell_number || email ? true : (cargo.callerPreselected=!!(contact_prefill && contact_options.caller_on_file === 'yes')),true)"),
99
+ set('identity_contact_profiles', 'contact_profiles', "contact_profiles_base.map(function (field) {return field.name === 'account' ? Object.assign({},field,{profile:{kind:'identifier',pattern:contact_options.account_pattern || '(?!)'}}) : field;}).concat(contact_allowed ? [{name:'caller_decision',label:'caller ID choice',label_es:'opción del identificador de llamadas',profile:{kind:'choice',choices:[{key:'yes',words:['1','yes','si','sí'],exact:true},{key:'no',words:['no','nope','nah'],exact:true}]}}] : [])"),
100
+ set('identity_contact_keypad', 'contact_keypad', "(function () {var shapes=[{first:'23456789',lengths:[10]},{first:'1',lengths:[11]}].concat(contact_options.account_shape || []);if(contact_partial){var left=[];(contact_options.account_shape || []).forEach(function(s){(s.lengths || []).forEach(function(n){var rest=n-String(cargo.gcePartial).length;if(rest>0 && left.indexOf(rest)<0)left.push(rest);});});return {min:1,max:12,autoSubmitMs:3000,complete:[{first:'0123456789',lengths:left.sort(function(a,b){return a-b;})}]};}return {min:7,max:12,autoSubmitMs:3000,autoSubmitChars:contact_allowed?['1']:[],complete:shapes};})()"),
101
+ set('identity_contact_question', 'contact_question', expression(contactPrompt, 'cargo', 'contact_options', 'contact_allowed', 'contact_partial')),
102
+ set('identity_contact_question_es', 'contact_question_es', expression(contactPrompt, 'cargo', 'contact_options', 'contact_allowed', 'contact_partial', 'true')),
103
+ call('identity_contact_collect', 'collect-service-answer', { profiles: '{{contact_profiles}}', question: '{{contact_question}}', question_es: '{{contact_question_es}}', keypad: '{{contact_keypad}}', prefill: '{{contact_prefill}}', parent_form: '{{contact_options.parent_form}}', context: 'account verification', context_es: 'verificación de cuenta' }),
104
+ set('identity_contact_answer', 'contact_text', 'String(cargo.serviceAnswer.raw || "")'),
105
+ set('identity_contact_spoken', 'contact_digits', `(${serviceDigits.toString()})(contact_text)`),
106
+ set('identity_contact_piece', 'contact_digits', "!/@/.test(contact_text) && !readEmail(contact_text) && /^[^0-9]*[0-9][0-9\\s.,-]*[^0-9]*$/.test(contact_digits) ? contact_digits.replace(/\\D/g,'') : ''"),
107
+ set('identity_contact_join', 'contact_text', "contact_partial && contact_digits ? String(cargo.gcePartial)+contact_digits : contact_text"),
108
+ set('identity_contact_join_digits', 'contact_digits', "contact_partial && contact_digits ? String(cargo.gcePartial)+contact_digits : contact_digits"), effect('identity_contact_piece_consumed', "(cargo.gcePartial='',cargo.contactIntroSkip=false,true)"),
109
+ when('identity_contact_split', "contact_options.merge_partials && contact_digits && (contact_options.account_shape || []).some(function(s){return String(s.first || '').includes(contact_digits.charAt(0)) && contact_digits.length < Math.min.apply(null,s.lengths || [0]);})", effect('identity_contact_hold', '(cargo.gcePartial=contact_digits,true)')),
110
+ when('identity_contact_split_continue', '!!cargo.gcePartial', contactReplay('identity_contact_next_piece', { gce_partial: true, gce_rejected: '{{gce_rejected}}' })),
111
+ when('identity_contact_split_overshoot', "contact_partial && contact_digits.length > (contact_options.account_shape || []).reduce(function(max,s){return Math.max(max,Math.max.apply(null,s.lengths || [0]));},0)", call('identity_contact_restart_split', 'contact-split-failed', { contact_options: '{{contact_options}}' }, 'replace')),
112
+ set('identity_contact_identifier', 'contact_id', "contact_options.account_pattern ? readIdentifier(contact_text,contact_options.account_pattern) : null"),
113
+ set('identity_contact_phone', 'contact_phone', "readPhone(contact_text) || (validatePhone(contact_digits) ? contact_digits : '')"), set('identity_contact_email', 'contact_mail', 'readEmail(contact_text)'),
114
+ set('identity_contact_classify', 'contact_kind', "contact_id && contact_id.kind === 'account' ? 'account' : contact_phone ? 'phone' : contact_mail ? 'email' : readInternationalPhone(contact_text) ? 'international' : isAgentRequest(contact_text) ? 'agent' : isAnswer(contact_text,'exit') && contact_text.trim().split(/\\s+/).length <= 2 ? 'exit' : contact_allowed && /^(?:no|nope|nah)\\b/i.test(contact_text.trim()) && !/[0-9@]/.test(contact_text) ? 'decline' : contact_allowed ? 'caller' : contact_digits ? 'invalid_phone' : contact_text.trim().split(/\\s+/).length>=3 && !/[0-9@]/.test(contact_text) ? 'off_topic' : 'invalid'"),
115
+ set('identity_contact_error', 'contact_error', expression(contactError, 'contact_kind', 'contact_options')), set('identity_contact_error_es', 'contact_error_es', expression(contactError, 'contact_kind', 'contact_options', 'true')),
116
+ cases('identity_contact_route', {
117
+ "condition: contact_kind === 'agent'": call('identity_contact_agent', 'live-agent-requested', undefined, 'reboot'),
118
+ "condition: contact_kind === 'exit'": call('identity_contact_exit', '{{contact_options.cancel_flow}}', undefined, 'reboot'),
119
+ "condition: contact_kind === 'decline'": call('identity_contact_declined', 'contact-caller-declined', { contact_options: '{{contact_options}}' }, 'replace'),
120
+ "condition: contact_kind === 'international'": contactMenu('identity_contact_international'),
121
+ "condition: contact_kind === 'invalid_phone'": contactMenu('identity_contact_invalid_phone'),
122
+ "condition: contact_kind === 'off_topic'": { id: 'identity_contact_off_topic', type: 'DISPATCH', outcome: 'unresolved', reason: 'auth_prompt_off_topic' },
123
+ "condition: contact_kind === 'invalid'": contactMenu('identity_contact_missing'), default: effect('identity_contact_typed', 'true')
124
+ }),
125
+ set('identity_contact_number', 'acct_number', "contact_kind === 'account' ? String(contact_id.value) : ''"),
126
+ set('identity_contact_cell', 'cell_number', "contact_kind === 'caller' ? String(cargo.callerId) : contact_kind === 'phone' ? contact_phone : ''"), set('identity_contact_mail', 'email', "contact_kind === 'email' ? contact_mail : ''"),
127
+ effect('identity_contact_record', "(cargo.contactKind=acct_number?'account':email?'email':String(cell_number).replace(/\\D/g,'').slice(-10)===String(cargo.callerId || '').replace(/\\D/g,'').slice(-10)?'caller':'phone',cargo.contactValue=String(acct_number || email || cell_number),cargo.contactAlternatives=cargo.serviceAnswer.values,true)"),
128
+ set('identity_contact_check_key', 'contact_check_key', "contact_options.validate_flow && !acct_number && !cargo.callerPreselected ? contact_options.validate_flow+'|'+(email?'email:'+email.toLowerCase():'phone:'+String(cell_number).replace(/\\D/g,'').slice(-10)) : ''"),
129
+ effect('identity_contact_check_reset', "(cargo.contactCheck=contact_check_key && cargo.gceChecks && cargo.gceChecks[contact_check_key] || '',true)"),
130
+ when('identity_contact_validate', 'contact_check_key && !cargo.contactCheck', call('identity_contact_validation', '{{contact_options.validate_flow}}')),
131
+ effect('identity_contact_check_record', "(contact_check_key ? (cargo.gceChecks=cargo.gceChecks || {},cargo.gceChecks[contact_check_key]=String(cargo.contactCheck || '')) : true,cargo.gceRejects=cargo.contactCheck === 'none'?Number(cargo.gceRejects || 0)+1:0,true)"),
132
+ set('identity_contact_rejected_message', 'contact_error', "cargo.contactKind === 'email' ? 'I could not find an account with that email.' : cargo.contactKind === 'caller' ? 'I could not find an account with the number you are calling from.' : 'I could not find an account with that phone number.'"),
133
+ set('identity_contact_rejected_message_es', 'contact_error_es', "cargo.contactKind === 'email' ? 'No encontré una cuenta con ese correo.' : cargo.contactKind === 'caller' ? 'No encontré una cuenta con el número desde el que llama.' : 'No encontré una cuenta con ese número de teléfono.'"),
134
+ when('identity_contact_reject', 'cargo.contactCheck === "none"', call('identity_contact_rejection', 'contact-not-on-file', { contact_options: '{{contact_options}}', error_message: '{{contact_error}}', error_message_es: '{{contact_error_es}}' }, 'replace'))
135
+ ]
136
+ });
137
+ const declined = worker('contact-caller-declined', 'ContactCallerDeclined', 'Repeat contact collection without offering caller ID.', { parameters: [param('contact_options', 'Current contact service options', 'object')], steps: [set('contact_declined_options', 'contact_options', "Object.assign({},contact_options,{caller_on_file:'no',intro:'',intro_es:''})"), contactReplay('contact_declined_repeat')] });
138
+ const splitFailed = worker('contact-split-failed', 'ContactSplitFailed', 'Restart a split identifier after too many digits.', { parameters: [param('contact_options', 'Current contact service options', 'object')], steps: [set('contact_split_options', 'contact_options', "Object.assign({},contact_options,{caller_on_file:'no',intro:cargo.voice?'I could not put that number together. Please enter it on your keypad.':'I could not put that number together. Please type the whole number.',intro_es:cargo.voice?'No pude juntar ese número. Por favor ingréselo con el teclado.':'No pude juntar ese número. Por favor escriba el número completo.'})"), contactReplay('contact_split_repeat')] });
139
+ const rejected = worker('contact-not-on-file', 'ContactNotOnFile', 'Re-collect a rejected account contact, then offer the common retry menu at the rejection limit.', { parameters: [param('contact_options', 'Current contact service options', 'object'), param('error_message', 'Rejection sentence'), param('error_message_es', 'Spanish rejection sentence')], steps: [set('contact_rejected_options', 'contact_options', "Object.assign({},contact_options,{caller_on_file:cargo.contactKind === 'caller' || contact_options.caller_on_file === 'no' ? 'no':'',fromMenu:false,intro:error_message,intro_es:error_message_es})"), when('contact_rejected_again', 'cargo.gceRejects <= 2', contactReplay('contact_rejected_collect', { gce_rejected: true })), set('contact_rejected_error', 'contact_error', 'error_message'), set('contact_rejected_error_es', 'contact_error_es', 'error_message_es'), contactMenu('contact_rejected_menu')] });
140
+ const AUTH_DEFAULTS = { retry_flow: '', cancel_flow: '', cell_number: '', email: '', email_validator: '', cell_validator: '', code_validator: '', answer_handler: '', retry_returns: false, retrying: false, declined_caller: false, parent_form: '', contact_valid: false, auth_result: false, auth_contact_error: '', auth_contact_error_es: '', auth_previous_options: null, auth_restart: false, auth_join: false, otp_expiry_ms: OTP_EXPIRY_MS };
141
+ const authKeys = ['retry_flow', 'cancel_flow', 'email_validator', 'cell_validator', 'code_validator', 'answer_handler', 'retry_returns', 'parent_form'];
142
+ const auth = worker('authenticate-user', 'AuthenticateUser', 'Authenticate a contact using shared FORM collection and one-time-code services.', { parameters: Object.keys(AUTH_DEFAULTS).filter(k => !['contact_valid', 'auth_result', 'auth_contact_error', 'auth_contact_error_es', 'auth_previous_options', 'auth_restart', 'auth_join', 'otp_expiry_ms'].includes(k)).map(k => param(k, k.replace(/_/g, ' '), ['retry_returns', 'retrying', 'declined_caller'].includes(k) ? 'boolean' : 'string')).concat([param('caller_on_file', 'Caller-ID eligibility inherited from the invoking service')]), variables: vars(AUTH_DEFAULTS), contract: { role: 'gate', summary: 'Generic one-time-code identity gate', result: { paths: ['cargo.otpVerified', 'cargo.authRetry'], type: 'object' } }, steps: [
143
+ set('auth_restart', 'auth_restart', "retrying === true || retrying === 'true'"),
144
+ set('auth_previous_options', 'auth_previous_options', 'cargo.authOptions ? Object.assign({},cargo.authOptions) : null'),
145
+ when('auth_declined_caller', "declined_caller === true || declined_caller === 'true'", effect('auth_decline_state', "(cargo.callerDeclined=true,cargo.callerPreselected=false,cargo.otpHash=null,cargo.contactIntroSkip=true,cargo.callerNamed ? (cargo.displayName='',cargo.displayNameSetBy='flow',cargo.callerNamed=false) : true,true)")),
146
+ when('auth_already_verified', 'cargo.otpVerified === true', end('auth_verified_end')),
147
+ ...authKeys.map(key => set('auth_option_' + key, key, `auth_restart && cargo.authOptions ? cargo.authOptions.${key} || ${key === 'retry_returns' ? 'false' : "''"} : ${key}`)),
148
+ set('auth_default_retry', 'retry_flow', "retry_flow || 'authenticate-user'"), set('auth_default_cancel', 'cancel_flow', "cancel_flow || 'contact-support'"),
149
+ effect('auth_options', `(cargo.authOptions={${authKeys.map(key => key + ':' + key).join(',')}},cargo.authCancelFlow=cancel_flow,cargo.authRetry=!!(auth_restart && !cell_number && !email && retry_returns),true)`),
150
+ when('auth_return_to_caller', 'cargo.authRetry', end('auth_retry_end')),
151
+ call('auth_gate_begin', 'task-gate', { form: '{{parent_form}}', gate: 'authenticate-user', active: true }),
152
+ call('auth_contact', 'get-cell-or-email', { retry_flow: '{{retry_flow}}', cancel_flow: '{{cancel_flow}}', cell_number: '{{cell_number}}', email: '{{email}}', parent_form: '{{parent_form}}', caller_on_file: '{{caller_on_file}}' }),
153
+ set('auth_validator_default', 'contact_valid', '!((cell_number && cell_validator) || (email && email_validator))'),
154
+ cases('auth_validate_contact', { 'condition: cell_number && cell_validator': call('auth_cell_validator', '{{cell_validator}}', { cell_number: '{{cell_number}}' }), 'condition: email && email_validator': call('auth_email_validator', '{{email_validator}}', { email: '{{email}}' }), default: effect('auth_contact_no_validator', 'true') }),
155
+ set('auth_contact_error', 'auth_contact_error', "'Sorry, I could not verify '+(cell_number?'the number '+cell_number:'the email '+email)+'.'"), set('auth_contact_error_es', 'auth_contact_error_es', "'Lo siento, no pude verificar '+(cell_number?'el número '+cell_number:'el correo electrónico '+email)+'.'"),
156
+ when('auth_contact_blocked', 'contact_valid !== true', call('auth_contact_retry', 'generic-retry-with-options', { error_message: '{{auth_contact_error}}', error_message_es: '{{auth_contact_error_es}}', retry_flow: '{{retry_flow}}', cancel_flow: '{{cancel_flow}}', capture_patterns: '{{cargo.authCapture}}', parent_form: '{{parent_form}}' }, 'replace')),
157
+ set('auth_join', 'auth_join', "!!(!auth_restart && cargo.otpHash && Number(cargo.otpTimestamp)>0 && Date.now()-Number(cargo.otpTimestamp)<=otp_expiry_ms && (cell_number ? String(cell_number).replace(/\\D/g,'').slice(-10)===String(cargo.otp_cell_number || '').replace(/\\D/g,'').slice(-10) : email && String(email).toLowerCase()===String(cargo.otp_email || '').toLowerCase()))"),
158
+ when('auth_join_options', 'auth_join', effect('auth_join_context', "(cargo.authOptions=Object.assign({},auth_previous_options || cargo.authOptions,{parent_form:parent_form || auth_previous_options && auth_previous_options.parent_form || ''}),cargo.authCancelFlow=cargo.authOptions.cancel_flow || cancel_flow,true)")),
159
+ when('auth_join_challenge', 'auth_join', call('auth_join_collect', 'get-and-validate-otp-code')),
160
+ when('auth_join_release', 'auth_join', call('auth_join_gate_end', 'task-gate', { form: '{{parent_form}}', gate: 'authenticate-user', active: false })),
161
+ when('auth_join_return', 'auth_join', end('auth_join_complete')),
162
+ effect('auth_challenge_init', "(cargo.otpLateCode='',cargo.otpResends=0,cargo.otpMismatches=0,cargo.otpFormatMisses=0,cargo.otpWordMisses=0,cargo.otp_cell_number=cell_number || '',cargo.otp_email=email || '',true)"),
163
+ call('auth_send', 'send-otp-code'), when('auth_send_failed', 'cargo.otpSendFailure', call('auth_send_failure', 'otp-send-failed', undefined, 'replace')),
164
+ call('auth_code', 'get-and-validate-otp-code'),
165
+ cases('auth_complete', { 'condition: cargo.otpVerified === true': set('auth_ok', 'auth_result', 'true'), 'condition: cargo.authRetry': end('auth_return_retry'), default: otpMenu('auth_retry_code', 'Sorry, the code you entered was invalid or expired.', 'Lo siento, el código que ingresó era inválido o había expirado.', true) }),
166
+ call('auth_gate_end', 'task-gate', { form: '{{parent_form}}', gate: 'authenticate-user', active: false })
167
+ ] });
168
+ export function otpPrompt(cargo, es = false) {
169
+ const verb = es ? cargo.verb_es : cargo.verb, number = String(cargo.otp_cell_number || '').replace(/\D/g, '').slice(-4), tail = cargo.voice ? number.split('').join(', ') : number;
170
+ const where = cargo.otpSentOn === 'whatsapp' ? (es ? 'que le acabo de enviar aquí en este chat' : 'I just sent you here in this chat') : (es ? 'que recibió en ' : 'you received at ') + (cargo.otp_cell_number ? (es ? 'celular que termina con ' : 'cell ending with ') + tail : (es ? 'correo electrónico: ' : 'email: ') + String(cargo.otp_email || ''));
171
+ const name = cargo.callerPreselected && cargo.callerNamed && cargo.displayName ? cargo.displayName : '';
172
+ return (name ? name + (es ? ', por favor' : ', please') : es ? 'Por favor' : 'Please') + ' ' + verb + ' ' + (cargo.voice ? es ? 'o ingrese ' : 'or enter ' : '') + (es ? 'el código de verificación de 6 dígitos ' : 'the 6-digit verification code ') + where + '. ' + (es ? `También puede ${cargo.voice ? 'presionar * o decir' : 'escribir'} SALIR para cancelar.` : `You can also ${cargo.voice ? 'press * or ' : ''}${verb} EXIT to cancel.`) + (cargo.callerPreselected ? (es ? ` Para usar otra cuenta, ${verb} OTRA CUENTA.` : ` To use another account, ${verb} ANOTHER ACCOUNT.`) : '');
173
+ }
174
+ const OTP_PROFILES = [{ name: 'code', label: 'verification code', label_es: 'código de verificación', profile: { kind: 'digits', min: 6, max: 6 } }, raw];
175
+ const code = worker('get-and-validate-otp-code', 'GetAndValidateOtpCode', 'Collect a one-time-code answer through the owning FORM ancestry, preserving protocol commands for validation.', { variables: vars({ otp_code: '', user_choice: '', normalized_otp_code: '', otp_question: '', otp_question_es: '', otp_profiles: OTP_PROFILES }), contract: { role: 'collector', summary: 'One-time-code answer collector using shared FORM matching' }, steps: [
176
+ when('otp_already_verified', 'cargo.otpVerified === true', end('otp_existing_verification')),
177
+ set('otp_question', 'otp_question', expression(otpPrompt, 'cargo')), set('otp_question_es', 'otp_question_es', expression(otpPrompt, 'cargo', 'true')),
178
+ call('otp_collect', 'collect-service-answer', { profiles: '{{otp_profiles}}', question: '{{otp_question}}', question_es: '{{otp_question_es}}', keypad: { min: 6, max: 6 }, parent_form: '{{cargo.authOptions.parent_form}}', stop_path: 'cargo.otpVerified', context: 'account verification', context_es: 'verificación de cuenta' }),
179
+ when('otp_verified_by_join', 'cargo.otpVerified === true', end('otp_join_finished')),
180
+ set('otp_answer', 'otp_code', "String(cargo.serviceAnswer.values.code || cargo.serviceAnswer.raw || '')"), set('otp_choice', 'user_choice', 'otp_code.trim().toLowerCase()'), set('otp_digits', 'normalized_otp_code', expression(otpDigits, 'otp_code')),
181
+ call('otp_validate', 'validate-otp-code', undefined, 'replace')
182
+ ] });
183
+ function otpMenu(id, error, error_es, allow_code_entry = false) { const en = id + '_message', es = id + '_message_es'; otpMenuMessages.push([en, error], [es, error_es]); return call(id, 'generic-retry-with-options', { error_message: '{{' + en + '}}', error_message_es: '{{' + es + '}}', retry_flow: 'authenticate-user', retry_call_type: 'replace', cancel_flow: '{{cargo.authCancelFlow}}', allow_code_entry, capture_patterns: '{{cargo.authCapture}}', parent_form: '{{cargo.authOptions.parent_form}}', question: 'Would you like to try again to get access to your account?', question_es: '¿Le gustaría intentar nuevamente obtener acceso a su cuenta?' }, 'replace'); }
184
+ const OTP_WORDS = {
185
+ 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'],
186
+ 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'],
187
+ 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'],
188
+ 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']
189
+ };
190
+ const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a challenge response with explicit bounded retries, alternate-contact handling and tenant callbacks.', { variables: vars({ otp_words: OTP_WORDS, no_code_given: false, wants_other_account: false, other_account_given: false, otp_validation_result: false, whatsapp_invite_sent: false, invite_text: '', otp_cancel: 'contact-support', answer_key: '', answer_value: '' }), contract: { role: 'gate', summary: 'Validate the active one-time challenge', tools: ['validate-otp', 'send-whatsapp-invite'], result: { path: 'cargo.otpVerified', type: 'boolean' } }, steps: [
191
+ set('otp_cancel', 'otp_cancel', "cargo.authCancelFlow || 'contact-support'"),
192
+ set('otp_reset_no_code', 'no_code_given', 'false'), set('otp_reset_invite', 'whatsapp_invite_sent', 'false'), set('otp_reset_callback', 'answer_key', "''"),
193
+ when('otp_tenant_code', 'cargo.authOptions && cargo.authOptions.code_validator && normalized_otp_code.length !== 6 && /[0-9]/.test(user_choice)', call('otp_tenant_code_call', '{{cargo.authOptions.code_validator}}', { answer: '{{user_choice}}' })),
194
+ set('otp_other_given', 'other_account_given', "answer_key === 'another_account' ? (cargo.authOtherAccount=String(answer_value || ''),true) : false"),
195
+ set('otp_other_requested', 'wants_other_account', 'other_account_given || !!(cargo.callerPreselected && normalized_otp_code.length !== 6 && matchesChoice(user_choice,otp_words.other))'),
196
+ when('otp_tenant_answer', 'cargo.authOptions && cargo.authOptions.answer_handler && normalized_otp_code.length !== 6 && !wants_other_account', call('otp_tenant_answer_call', '{{cargo.authOptions.answer_handler}}', { answer: '{{user_choice}}' })),
197
+ cases('otp_response', {
198
+ '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') },
199
+ 'condition: wants_other_account': call('otp_change_account', 'authenticate-user', { cell_number: '', email: '', retrying: true, declined_caller: true }, 'replace'),
200
+ "condition: isAnswer(user_choice,'exit')": call('otp_exit', '{{otp_cancel}}', undefined, 'reboot'),
201
+ "condition: isAnswer(user_choice,'agent')": call('otp_agent', 'live-agent-requested', undefined, 'reboot'),
202
+ "condition: isAnswer(user_choice,'text')": call('otp_text', 'switch-to-text', undefined, 'reboot'),
203
+ 'condition: matchesChoice(user_choice,otp_words.resend)': call('otp_resend', 'resend-otp-code', { resend_reason: 'requested' }),
204
+ 'condition: matchesChoice(user_choice,otp_words.later)': say('otp_later', "No problem. Say TEXT and I'll continue with you by message so you can do this when you are free, or {{cargo.verb}} the code whenever you are ready.", 'No hay problema. Diga TEXTO y sigo con usted por mensaje para que lo haga cuando esté libre, o {{cargo.verb_es}} el código cuando pueda.'),
205
+ 'condition: normalized_otp_code.length > 0 && Number(cargo.otpFormatMisses || 0)<2': say('otp_bad_format', "I was expecting a 6-digit code. Let's try again. You can also {{cargo.voice ? 'press * or ' : ''}}{{cargo.verb}} EXIT to cancel.", "Esperaba un código de 6 dígitos. Intentemos de nuevo. También puede {{cargo.voice ? 'presionar * o decir' : 'escribir'}} SALIR para cancelar."),
206
+ 'condition: normalized_otp_code.length > 0': effect('otp_format_cap_next', 'true'),
207
+ 'condition: matchesChoice(user_choice,otp_words.noPhone)': otpMenu('otp_no_phone', "No problem. Then the code can't reach you there. You can use the email on your account instead, or {{cargo.verb}} AGENT to reach a person.", 'No hay problema. Entonces el código no le puede llegar ahí. Puede usar el correo electrónico de su cuenta, o {{cargo.verb_es}} AGENTE para hablar con una persona.', false),
208
+ default: set('otp_words_missing', 'no_code_given', 'true')
209
+ }),
210
+ effect('otp_word_count', '(cargo.otpWordMisses=Number(cargo.otpWordMisses || 0)+(no_code_given?1:0),true)'),
211
+ when('otp_word_cap', 'no_code_given && cargo.otpWordMisses >= 3', otpMenu('otp_word_menu', "I still don't have the code. To reach a person, {{cargo.verb}} AGENT.", 'Todavía no tengo el código. Para hablar con una persona, {{cargo.verb_es}} AGENTE.', true)),
212
+ when('otp_explanation', 'no_code_given', say('otp_explain', 'Let me explain. I sent you a message with a six-digit code in it. Read those six digits back to me. You can also {{cargo.verb}} AGENT to reach a person, or EXIT to cancel.', "Le explico. Le envié un mensaje con un código de seis dígitos. Dígame esos seis dígitos. También puede {{cargo.voice ? 'decir' : 'escribir'}} AGENTE para hablar con una persona, o SALIR para cancelar.")),
213
+ effect('otp_format_count', '(cargo.otpFormatMisses=Number(cargo.otpFormatMisses || 0)+(normalized_otp_code.length > 0 && normalized_otp_code.length !== 6?1:0),true)'),
214
+ when('otp_format_cap', 'normalized_otp_code.length !== 6 && cargo.otpFormatMisses >= 3', otpMenu('otp_format_menu', "I still don't have a 6-digit code. To reach a person, {{cargo.verb}} AGENT.", 'Todavía no tengo un código de 6 dígitos. Para hablar con una persona, {{cargo.verb_es}} AGENTE.', true)),
215
+ when('otp_format_retry', 'normalized_otp_code.length !== 6', call('otp_collect_again', 'get-and-validate-otp-code', undefined, 'replace')),
216
+ effect('otp_mismatch_count', "(cargo.otpMismatches=Number(cargo.otpMismatches || 0)+(!cargo.otpVerified && cargo.otpFailure === 'mismatch'?1:0),true)"),
217
+ when('otp_mismatch_cap', "!cargo.otpVerified && cargo.otpFailure === 'mismatch' && cargo.otpMismatches >= 3", effect('otp_withdraw', '(cargo.otpHash=null,true)')),
218
+ cases('otp_verified_result', {
219
+ 'condition: cargo.otpVerified === true': effect('otp_verified_record', '(cargo.otpLateCode=String(normalized_otp_code),true)'),
220
+ "condition: cargo.otpFailure === 'expired' || cargo.otpFailure === 'missing'": call('otp_refresh', 'resend-otp-code', { resend_reason: '{{cargo.otpFailure}}' }),
221
+ "condition: cargo.otpFailure === 'mismatch' && cargo.otpMismatches >= 3": otpMenu('otp_mismatch_menu', "That code didn't match three times, so I can't accept another try with it. To reach a person, {{cargo.verb}} AGENT.", 'Ese código no coincidió tres veces, así que no puedo aceptar otro intento con él. Para hablar con una persona, {{cargo.verb_es}} AGENTE.', false),
222
+ default: say('otp_mismatch', "That code didn't match. Let's try again.", 'Ese código no coincide. Intentemos de nuevo.')
223
+ }),
224
+ set('otp_invite_text', 'invite_text', "language === 'es' ? typeof global_whatsapp_invite_es !== 'undefined' ? global_whatsapp_invite_es : '' : typeof global_whatsapp_invite !== 'undefined' ? global_whatsapp_invite : ''"),
225
+ when('otp_invite', "!cargo.otpVerified && cargo.otpFailure === 'mismatch'", { id: 'otp_invite_tool', type: 'CALL-TOOL', tool: 'send-whatsapp-invite', variable: 'whatsapp_invite_sent', args: { accountSid: null, from: '{{cargo.twilioNumber}}', to: '{{cargo.otp_cell_number}}', inviteText: '{{invite_text}}', language: '{{language}}', container: '{{cargo}}' }, onFail: set('otp_invite_failed', 'whatsapp_invite_sent', 'false') }),
226
+ when('otp_invite_mention', 'whatsapp_invite_sent === true', say('otp_invite_sent', 'I texted you a WhatsApp link so you can try again on WhatsApp to help overcome voice limitations.', 'Te envié por texto un enlace de WhatsApp para que puedas intentarlo de nuevo allí y así evitar las limitaciones de la voz.')),
227
+ when('otp_not_verified', 'cargo.otpVerified !== true', call('otp_retry_challenge', 'get-and-validate-otp-code', undefined, 'replace'))
228
+ ] });
229
+ // Menu sentences are evaluated as SET values, so FLOW arguments remain plain paths.
230
+ for (const flow of [auth, validateCode]) {
231
+ for (const [name] of otpMenuMessages)
232
+ flow.variables[name] = { type: 'string', value: '' };
233
+ flow.steps.unshift(...otpMenuMessages.map(([name, value]) => set(name, name, value.includes('{{') ? value : JSON.stringify(value))));
234
+ }
235
+ 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')];
236
+ 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: [
237
+ set('retry_parent', 'parent_form', "parent_form || cargo.authOptions && cargo.authOptions.parent_form || cargo.contactOptions && cargo.contactOptions.parent_form || ''"),
238
+ 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.'"),
239
+ 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.'"),
240
+ call('retry_collect', 'collect-service-answer', { profiles: '{{retry_profiles}}', question: '{{retry_question}}', question_es: '{{retry_question_es}}', keypad: { min: 1, max: 1 }, parent_form: '{{parent_form}}' }),
241
+ set('retry_answer', 'user_choice', 'String(cargo.serviceAnswer.raw || "")'),
242
+ set('retry_default_cancel', 'cancel_flow', "cancel_flow || 'contact-support'"),
243
+ when('retry_unknown_key_question', '/^\\s*[2-8]\\s*$/.test(user_choice)', set('retry_unknown_question', 'retry_question', "'Sorry, that is not an option. To retry '+(cargo.voice?'Press 1 or ':'')+cargo.verb+' YES. '+(callerTextNumber(cargo)?'To switch to text press 9 or '+cargo.verb+' TEXT. ':'')+(cargo.voice?'For an agent press 0. ':'')+'To abort '+(cargo.voice?'press the asterisk key or ':'')+cargo.verb+' EXIT.'")),
244
+ when('retry_unknown_key_question_es', '/^\\s*[2-8]\\s*$/.test(user_choice)', set('retry_unknown_question_es', 'retry_question_es', "'Lo siento, esa no es una opción. Para reintentar '+(cargo.voice?'Presione 1 o ':'')+cargo.verb_es+' SÍ. '+(callerTextNumber(cargo)?'Para cambiar a texto presione 9 o '+cargo.verb_es+' TEXTO. ':'')+(cargo.voice?'Para un agente presione 0. ':'')+'Para salir '+(cargo.voice?'presione la tecla de asterisco o ':'')+cargo.verb_es+' SALIR.'")),
245
+ when('retry_unknown_key', '/^\\s*[2-8]\\s*$/.test(user_choice)', call('retry_bad_key_collect', 'collect-service-answer', { profiles: '{{retry_profiles}}', question: '{{retry_question}}', question_es: '{{retry_question_es}}', keypad: { min: 1, max: 1 }, parent_form: '{{parent_form}}' })),
246
+ 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'),
247
+ 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') }),
248
+ 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')),
249
+ 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()) || isAnswer(user_choice,'exit')": call('retry_cancel', '{{cancel_flow}}', undefined, 'reboot'), default: { id: 'retry_dispatch', type: 'DISPATCH', outcome: '{{caller_outcome}}', reason: '{{caller_reason}}' } })
250
+ ] });
251
+ 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'] };
252
+ export function emailReadback(address, voice, es = false) {
253
+ let said = String(address || '');
254
+ if (voice)
255
+ said = said.split('').join(' ').replace(/ @ /, es ? ' arroba ' : ' at ').replace(/ \. /g, es ? ' punto ' : ' dot ').replace(/ _ /g, es ? ' guion bajo ' : ' underscore ').replace(/ - /g, es ? ' guion ' : ' dash ');
256
+ return (es ? 'Tengo ' : 'I have ') + said + (es ? '. ¿Es correcto? ' : '. Is that right? ') + (es ? voice ? 'Presione 1 o diga' : 'Escriba' : voice ? 'Press 1 or say' : 'Type') + (es ? ' SÍ, o ' : ' YES, or ') + (es ? voice ? 'presione 2 o diga' : 'escriba' : voice ? 'press 2 or say' : 'type') + (es ? ' NO.' : ' NO.');
257
+ }
258
+ const email = worker('ask-email', 'AskEmail', 'Collect an email through shared FORM matching, retaining field-specific spelling and readback behavior.', { scope: 'isolated', parameters: [param('question', 'English email question'), param('question_es', 'Spanish email question'), param('on_file', 'Known email offered by OK'), param('attempt', 'Retry attempt'), param('why', 'invalid, retry or more'), param('confirm', 'Read back a caller-supplied known address'), param('parent_form', 'Owning task FORM instance'), param('email_state', 'Internal field protocol state', 'object')], variables: vars({ question: '', question_es: '', on_file: '', attempt: '', why: '', confirm: '', parent_form: '', email_state: null, email_question: '', email_question_es: '', email_raw: '', email_found: '', email_spelling: false, email_new: false, email_choice: '', email_readback: '', email_readback_es: '', email_choices: EMAIL_CHOICES, email_profiles_base: [{ name: 'email_address', label: 'email address', label_es: 'correo electrónico', profile: { kind: 'email' } }, raw], email_profiles: [], email_confirm_profiles: [{ name: 'decision', label: 'email confirmation', label_es: 'confirmación de correo', profile: { kind: 'choice', choices: [{ key: 'no', words: ['2', 'no', 'wrong', 'incorrect', 'incorrecto'] }, { key: 'yes', words: ['1', 'yes', 'sí', 'si', 'correct', 'correcto'] }] } }, raw] }), contract: { role: 'collector', summary: 'Email field spelling and readback service', result: { path: 'cargo.email', type: 'string' } }, steps: [
259
+ set('email_protocol', 'email_state', "email_state || {question:question,question_es:question_es || question,on_file:on_file,confirm:confirm === true || confirm === 'true',attempt:Number(attempt || 0),why:why,draft:why === 'more'?String(cargo.emailDraft || ''):'',parent_form:parent_form}"),
260
+ set('email_profiles', 'email_profiles', "email_profiles_base.concat(email_state.on_file ? [{name:'known_email_choice',label:'email destination choice',label_es:'opción de destino del correo',profile:{kind:'choice',choices:[{key:'ok',words:email_choices.ok},{key:'another',words:email_choices.another}]}}] : [])"),
261
+ set('email_question', 'email_question', "email_state.why === 'more' ? 'Got it. Go on.' : (email_state.attempt>0 ? email_state.why === 'retry' ? \"Okay, let's try again. \" : \"That doesn't look like an email address. \" : '')+email_state.question"),
262
+ set('email_question_es', 'email_question_es', "email_state.why === 'more' ? 'Entendido. Continúe.' : (email_state.attempt>0 ? email_state.why === 'retry' ? 'De acuerdo, intentemos de nuevo. ' : 'Eso no parece un correo electrónico. ' : '')+email_state.question_es"),
263
+ call('email_collect', 'collect-service-answer', { profiles: '{{email_profiles}}', question: '{{email_question}}', question_es: '{{email_question_es}}', keypad: { min: 1, max: 1 }, parent_form: '{{email_state.parent_form}}', context: 'email', context_es: 'correo' }),
264
+ set('email_raw', 'email_raw', 'String(cargo.serviceAnswer.raw || "")'),
265
+ cases('email_command', { "condition: isAgentRequest(email_raw)": call('email_agent', 'live-agent-requested', undefined, 'reboot'), "condition: isAnswer(email_raw,'exit')": call('email_exit', 'contact-support', { support_context: 'email', support_context_es: 'correo' }, 'reboot'), default: effect('email_is_answer', 'true') }),
266
+ set('email_choice', 'email_choice', "pickChoice(email_raw,[{key:'ok',words:email_choices.ok},{key:'another',words:email_choices.another}],false)"),
267
+ when('email_other_request_clear', "!email_choice && !readEmail(email_raw) && isOtherRequest(email_raw,'email')", effect('email_other_request_draft', "(email_state.draft='',cargo.emailDraft='',cargo.email='',true)")),
268
+ when('email_other_request', "!email_choice && !readEmail(email_raw) && isOtherRequest(email_raw,'email')", { id: 'email_dispatch_request', type: 'DISPATCH' }),
269
+ set('email_parse', 'email_found', "(email_state.draft ? readEmail(email_state.draft+' '+email_raw) : '') || readEmail(email_raw) || (email_choice === 'ok' && email_state.on_file ? String(email_state.on_file).toLowerCase() : '')"),
270
+ set('email_fragment', 'email_spelling', "!email_found && !email_choice && isSpelledFragment(email_raw) && email_state.draft.length<200"),
271
+ when('email_keep_spelling', 'email_spelling', effect('email_draft', "(email_state.draft=(email_state.draft+' '+email_raw).trim(),email_state.why='more',cargo.emailDraft=email_state.draft,true)")),
272
+ when('email_more', 'email_spelling', call('email_more_field', 'ask-email', { email_state: '{{email_state}}' }, 'replace')),
273
+ set('email_new', 'email_new', '!!email_found && (email_state.confirm || String(email_state.on_file || "").toLowerCase() !== email_found)'),
274
+ set('email_readback', 'email_readback', expression(emailReadback, 'email_found', 'cargo.voice')), set('email_readback_es', 'email_readback_es', expression(emailReadback, 'email_found', 'cargo.voice', 'true')),
275
+ when('email_verify_new', 'email_new', call('email_readback_collect', 'collect-service-answer', { profiles: '{{email_confirm_profiles}}', question: '{{email_readback}}', question_es: '{{email_readback_es}}', keypad: { min: 1, max: 1 }, parent_form: '{{email_state.parent_form}}', context: 'email', context_es: 'correo' })),
276
+ when('email_readback_other_clear', "email_new && isOtherRequest(cargo.serviceAnswer.raw,'email') && !pickChoice(cargo.serviceAnswer.raw,[{key:'yes',words:['1','yes','sí','si','correct','correcto']},{key:'no',words:['2','no','wrong','incorrect','incorrecto']}],false)", effect('email_readback_other_draft', "(email_state.draft='',cargo.emailDraft='',cargo.email='',true)")),
277
+ when('email_readback_other', "email_new && isOtherRequest(cargo.serviceAnswer.raw,'email') && !pickChoice(cargo.serviceAnswer.raw,[{key:'yes',words:['1','yes','sí','si','correct','correcto']},{key:'no',words:['2','no','wrong','incorrect','incorrecto']}],false)", { id: 'email_readback_dispatch', type: 'DISPATCH' }),
278
+ when('email_readback_wrong', "email_new && pickChoice(cargo.serviceAnswer.raw,[{key:'no',words:['2','no','wrong','incorrect','incorrecto']},{key:'yes',words:['1','yes','sí','si','correct','correcto']}],false) !== 'yes'", set('email_rejected', 'email_found', "''")),
279
+ when('email_got_address', '!!email_found', effect('email_store', "(cargo.email=email_found,cargo.emailDraft='',true)")), when('email_return_address', '!!email_found', end('email_complete')),
280
+ when('email_other_address', "email_choice === 'another' && !!email_state.on_file", effect('email_change_offer', "(email_state.question='What email address should I send it to?',email_state.question_es='¿A qué correo electrónico se lo envío?',email_state.on_file='',email_state.confirm=false,email_state.attempt=-1,true)")),
281
+ effect('email_retry_state', "(email_state.attempt+=1,email_state.why=email_new?'retry':'invalid',email_state.draft='',cargo.emailDraft='',true)"),
282
+ when('email_retry', 'email_state.attempt<2', call('email_retry_field', 'ask-email', { email_state: '{{email_state}}' }, 'replace')),
283
+ effect('email_no_value', "(cargo.email='',cargo.emailDraft='',true)")
284
+ ] });
285
+ /** Reenter the existing owning FORM or its gate without making the owner its own parent. */
286
+ export function identityResumeContext(forms, owner, target, identifiers = {}) {
287
+ const state = forms && forms[owner];
288
+ if (state && state.definition === target && ['acct_number', 'cell_number', 'email'].some(key => !!identifiers[key])) {
289
+ state.arguments ||= {};
290
+ for (const key of ['acct_number', 'cell_number', 'email'])
291
+ state.arguments[key] = String(identifiers[key] || '');
292
+ }
293
+ return state && state.definition === target ? { form_instance: owner, parent_form: state.parent || '', result_input: state.result_input || '' } : { form_instance: '', parent_form: owner || '', result_input: '' };
294
+ }
295
+ const resume = worker('resume-identity-caller', 'ResumeIdentityCaller', 'Resume the caller of an account recovery with its original FORM instance and gate policy.', {
296
+ parameters: [param('retry_flow', 'Caller definition to resume'), param('parent_form', 'Owning task FORM instance'), param('gate_options', 'Gate policy retained across recovery', 'object'), param('cell_number', 'New phone identifier'), param('email', 'New email identifier'), param('acct_number', 'New account identifier')],
297
+ variables: vars({ retry_flow: 'get-account', parent_form: '', gate_options: null, cell_number: '', email: '', acct_number: '', resume_context: null }),
298
+ contract: { role: 'router', summary: 'Identity recovery resumes the existing owner and preserves its ancestry' },
299
+ steps: [set('identity_resume_context', 'resume_context', expression(identityResumeContext, 'cargo.forms', 'parent_form', 'retry_flow', '{acct_number:acct_number,cell_number:cell_number,email:email}')),
300
+ call('identity_resume_caller', '{{retry_flow}}', { form_instance: '{{resume_context.form_instance}}', parent_form: '{{resume_context.parent_form}}', result_input: '{{resume_context.result_input}}', gate_options: '{{gate_options}}', cell_number: '{{cell_number}}', email: '{{email}}', acct_number: '{{acct_number}}' }, 'replace')]
301
+ });
302
+ export const FLOWS = [taskGate, resume, service, rawTransport, contact, declined, splitFailed, rejected, auth, code, validateCode, retry, email];
@@ -0,0 +1,7 @@
1
+ export const FLOWS: any[];
2
+ export { FLOWS as IDENTITY_FLOWS } from "./identity-flows.js";
3
+ export { compileFormPrompts } from "./prompt-compiler.js";
4
+ export { FIELD_PRESETS, formFlow, declinedFlow, textValue } from "./flows.js";
5
+ export { linkFormSchemas, defineWorker, defineProvider, defineForm, definePerformer, validateFlowContracts, operationWorkers, providerQuestions, providerParameter } from "./contracts.js";
6
+ export { addressDraft, zipLocation, streetLocation, resolvedAddress } from "../helpers/address.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/forms/index.js"],"names":[],"mappings":"AAIA,0BAAgF"}
@@ -0,0 +1,10 @@
1
+ import { FLOWS as ADDRESS_FLOWS } from './address-flows.js';
2
+ import { FLOWS as FORM_FLOWS } from './flows.js';
3
+ import { FLOWS as CHOICE_FLOWS } from './choice-flow.js';
4
+ import { FLOWS as IDENTITY } from './identity-flows.js';
5
+ export const FLOWS = [...FORM_FLOWS, ...ADDRESS_FLOWS, ...CHOICE_FLOWS, ...IDENTITY];
6
+ export { FIELD_PRESETS, formFlow, declinedFlow, textValue } from './flows.js';
7
+ export { FLOWS as IDENTITY_FLOWS } from './identity-flows.js';
8
+ export { linkFormSchemas, defineWorker, defineProvider, defineForm, definePerformer, validateFlowContracts, operationWorkers, providerQuestions, providerParameter } from './contracts.js';
9
+ export { compileFormPrompts } from './prompt-compiler.js';
10
+ export { addressDraft, zipLocation, streetLocation, resolvedAddress } from '../helpers/address.js';
@@ -0,0 +1,3 @@
1
+ /** Compile contextual field prompts into workers; source declarations stay immutable. */
2
+ export function compileFormPrompts(forms: any): any[];
3
+ //# sourceMappingURL=prompt-compiler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt-compiler.d.ts","sourceRoot":"","sources":["../../src/forms/prompt-compiler.js"],"names":[],"mappings":"AAAA,yFAAyF;AACzF,sDAuBC"}
@@ -0,0 +1,30 @@
1
+ /** Compile contextual field prompts into workers; source declarations stay immutable. */
2
+ export function compileFormPrompts(forms) {
3
+ const workers = [];
4
+ const flows = forms.map(source => {
5
+ const flow = structuredClone(source);
6
+ const visit = input => {
7
+ if (input.any_of) {
8
+ input.any_of.forEach(visit);
9
+ return;
10
+ }
11
+ if (!String(input.question || '').includes('{{') && !String(input.question_es || '').includes('{{'))
12
+ return;
13
+ const id = flow.id + '-' + input.name + '-question';
14
+ const set = (suffix, variable, value) => ({ id: id + '_' + suffix, type: 'SET', variable, value });
15
+ const literal = text => String(text).includes('{{') ? String(text) : JSON.stringify(String(text));
16
+ const steps = [...(input.prepare ? [{ id: id + '_prepare', type: 'FLOW', value: input.prepare, callType: 'call', parameters: { form: '{{form}}', input: '{{input}}' } }] : []),
17
+ set('data', 'data', 'cargo.forms[form].data || {}'),
18
+ set('en', 'en', literal(input.question || '')), set('es', 'es', literal(input.question_es || input.question || '')),
19
+ set('store', 'noop', `(cargo.forms[form].data=Object.assign({},cargo.forms[form].data,{${JSON.stringify(input.name + '_question')}:en,${JSON.stringify(input.name + '_question_es')}:es}),true)`)];
20
+ workers.push({ id, name: flow.name + input.name + 'Question', version: '1.0.0', scope: 'isolated', description: 'Render the declared field question in its owning form context.',
21
+ parameters: [{ name: 'form', type: 'string' }, { name: 'input', type: 'string' }],
22
+ variables: { data: { type: 'object', value: null }, en: { type: 'string', value: '' }, es: { type: 'string', value: '' }, contract: { type: 'object', value: { version: 1, role: 'worker', summary: 'Render a contextual form prompt', parametersFrom: 'parameters', result: { path: 'cargo.forms[form].data', type: 'object' } } } }, steps });
23
+ input.prepare = id;
24
+ input.question_from = input.name + '_question';
25
+ };
26
+ flow.variables.inputs.value.forEach(visit);
27
+ return flow;
28
+ });
29
+ return [...flows, ...workers];
30
+ }
@@ -0,0 +1,27 @@
1
+ /** Address interpretation and geocoder projection, independent of collection order. */
2
+ export function addressDraft(raw: any): {
3
+ zip: string;
4
+ street: string;
5
+ unit: string;
6
+ said: string;
7
+ };
8
+ export function zipLocation(result: any): {
9
+ city: string;
10
+ state: string;
11
+ line: string;
12
+ } | null;
13
+ export function streetLocation(result: any, said: any, zip: any): {
14
+ street: string;
15
+ said: string;
16
+ verified: boolean;
17
+ };
18
+ export function resolvedAddress(values: any, data: any): {
19
+ street: any;
20
+ unit: string;
21
+ city: any;
22
+ zip: string;
23
+ line: string;
24
+ verified: boolean;
25
+ said: any;
26
+ };
27
+ //# sourceMappingURL=address.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../../src/helpers/address.js"],"names":[],"mappings":"AAAA,uFAAuF;AACvF;;;;;EAQC;AACD;;;;SAKC;AACD;;;;EAMC;AACD;;;;;;;;EAIC"}
@@ -0,0 +1,29 @@
1
+ /** Address interpretation and geocoder projection, independent of collection order. */
2
+ export function addressDraft(raw) {
3
+ const said = String(raw || '').trim();
4
+ const zip = /(?:^|\D)(\d{5})(?:-\d{4})?(?:\D|$)/.exec(said)?.[1] || '';
5
+ const unit = /(?:\bapt\.?|\bunit|#)\s*([\w-]+)/i.exec(said)?.[1] || '';
6
+ let street = said.split(/[,\n]/)[0].replace(/\s*(?:apt\.?|unit|#)\s*[\w-]+.*$/i, '').trim();
7
+ street = street.replace(/\s+\d{5}(?:-\d{4})?$/, '').replace(/\s+[A-Z]{2}$/, '').trim();
8
+ if (!/^\d{1,6}\s+\S/.test(street))
9
+ street = '';
10
+ return { zip, street, unit, said: street };
11
+ }
12
+ export function zipLocation(result) {
13
+ const components = result?.components || result?.results?.[0]?.address_components || [];
14
+ const part = (kind, short = false) => { const row = components.find(c => (c.types || []).includes(kind)); return row ? String(short ? row.short_name || row.long_name : row.long_name || row.short_name) : ''; };
15
+ const city = part('locality') || part('postal_town') || part('sublocality'), state = part('administrative_area_level_1', true);
16
+ return result?.status === 'OK' && city && state ? { city, state, line: city + ', ' + state } : null;
17
+ }
18
+ export function streetLocation(result, said, zip) {
19
+ const row = result?.results?.[0] || result || {}, components = row.components || row.address_components || [];
20
+ const part = kind => components.find(c => (c.types || []).includes(kind))?.long_name || '';
21
+ const types = row.types || [], locationType = row.location_type || row.geometry?.location_type || '';
22
+ const exact = result?.status === 'OK' && row.partial_match !== true && types.includes('street_address') && locationType === 'ROOFTOP' && part('street_number') && part('route') && (!part('postal_code') || part('postal_code') === zip);
23
+ return { street: exact ? part('street_number') + ' ' + part('route') : String(said).trim(), said: String(said).trim(), verified: !!exact };
24
+ }
25
+ export function resolvedAddress(values, data) {
26
+ const unit = /^(none|no|ninguno|ninguna|sin|no hay)$/i.test(String(values.unit || '')) ? '' : String(values.unit || '').replace(/^(apt\.?|unit|#)\s*/i, '');
27
+ const street = data.street?.street || values.street || '', city = data.location?.line || values.city_state || '';
28
+ return { street, unit, city, zip: String(values.zip || ''), line: street + (unit ? ' Apt ' + unit : '') + ', ' + city + ' ' + values.zip, verified: data.street?.verified === true, said: data.street?.said || values.street || '' };
29
+ }
@@ -28,6 +28,11 @@ export function isAmountAnswer(input: any): boolean;
28
28
  * dollars, cents or decimals). "50 cents" alone is $0.50; "82 dollars and 50" and "82 con 50" are $82.50. Amounts said only in words are NaN (not parsed yet).
29
29
  */
30
30
  export function amountInAnswer(input: any): number;
31
+ /** Internal numeric evidence shared by the opt-in money reader; legacy results stay unchanged. */
32
+ export function numericAmountReading(input: any): {
33
+ value: number;
34
+ candidates: number;
35
+ };
31
36
  /**
32
37
  * ask-choice's matcher: the key of the choice the answer picks, '__agent', '__exit', or '' (no choice).
33
38
  * choices: [{ key, words, negative?, neutral?, exact? }]. A key (digit, * or #) and a YES word pick a choice only as the whole
@@ -1 +1 @@
1
- {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAyIA;;;GAGG;AACH,wEAgBC;AAED,+HAA+H;AAC/H,oDAIC;AAcD,2GAA2G;AAC3G,uDAGC;AAOD;;;;;GAKG;AACH,+DAUC;AAKD;;;;GAIG;AACH,oDAEC;AAiBD;;;;;;GAMG;AACH,mDA6BC;AAUD;;;;;;;;;GASG;AACH,6EAyGC;;;;;;;;AAjWD,qCAKiD"}
1
+ {"version":3,"file":"answer.d.ts","sourceRoot":"","sources":["../../src/helpers/answer.js"],"names":[],"mappings":"AAyIA;;;GAGG;AACH,wEAgBC;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;;;EA6BC;AAUD;;;;;;;;;GASG;AACH,6EAyGC;;;;;;;;AApWD,qCAKiD"}