jsfe 1.17.0 → 1.18.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.
|
@@ -7,7 +7,7 @@ export function contactPrompt(cargo: any, options: any, callerAllowed: any, part
|
|
|
7
7
|
export function contactError(kind: any, options: any, es?: boolean): string;
|
|
8
8
|
export function otpWhere(cargo: any, es?: boolean): string;
|
|
9
9
|
export function otpPrompt(cargo: any, es?: boolean): string;
|
|
10
|
-
/** What the caller hears when a second code goes to the account's email
|
|
10
|
+
/** What the caller hears when a second code goes to the account's email, masked by maskEmail (jsfe/helpers). */
|
|
11
11
|
export function otpAlsoSent(cargo: any, es?: boolean): string;
|
|
12
12
|
export function emailReadback(address: any, voice: any, es?: boolean): string;
|
|
13
13
|
/** Reenter the existing owning FORM or its gate without making the owner its own parent. */
|
|
@@ -16,5 +16,6 @@ export function identityResumeContext(forms: any, owner: any, target: any, ident
|
|
|
16
16
|
parent_form: any;
|
|
17
17
|
result_input: any;
|
|
18
18
|
};
|
|
19
|
+
export function verificationFailedText(cargo: any, ended: any, es?: boolean, part?: string): string;
|
|
19
20
|
export const FLOWS: any[];
|
|
20
21
|
//# sourceMappingURL=identity-flows.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-flows.d.ts","sourceRoot":"","sources":["../../src/forms/identity-flows.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"identity-flows.d.ts","sourceRoot":"","sources":["../../src/forms/identity-flows.js"],"names":[],"mappings":"AA6BA,+GAA+G;AAC/G,oFAOC;AAQD,wFAAwF;AACxF,iDAGC;AACD,6CAMC;AACD,qHAQC;AAMD,4EAKC;AAwFD,2DAOC;AACD,4DAOC;AACD,gHAAgH;AAChH,8DAGC;AA4ID,8EAGC;AA4BD,4FAA4F;AAC5F;;;;EAOC;AAWD,oGAOC;AAaD,0BAAmJ"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Shared identity protocols. Every conversational answer enters the ordinary FORM acceptance/matching pipeline. */
|
|
2
2
|
import { defineWorker, defineForm } from './contracts.js';
|
|
3
3
|
import { OTP_EXPIRY_MS } from '../helpers/otp-check.js';
|
|
4
|
+
// maskEmail is a standard function: these functions run as flow expressions too, where it is in scope by name.
|
|
5
|
+
import { maskEmail } from '../helpers/contact.js';
|
|
4
6
|
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
7
|
const param = (name, description, type = 'string') => ({ name, type, description });
|
|
6
8
|
const set = (id, variable, value) => ({ id, type: 'SET', variable, value });
|
|
@@ -146,8 +148,8 @@ const contact = worker('get-cell-or-email', 'GetCellOrEmail', 'Identify a suppli
|
|
|
146
148
|
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')] });
|
|
147
149
|
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')] });
|
|
148
150
|
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')] });
|
|
149
|
-
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 };
|
|
150
|
-
const authKeys = ['retry_flow', 'cancel_flow', 'email_validator', 'cell_validator', 'code_validator', 'answer_handler', 'retry_returns', 'parent_form'];
|
|
151
|
+
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: '', task: '', task_es: '', failed_flow: '', 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 };
|
|
152
|
+
const authKeys = ['retry_flow', 'cancel_flow', 'email_validator', 'cell_validator', 'code_validator', 'answer_handler', 'retry_returns', 'parent_form', 'task', 'task_es', 'failed_flow'];
|
|
151
153
|
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: [
|
|
152
154
|
set('auth_restart', 'auth_restart', "retrying === true || retrying === 'true'"),
|
|
153
155
|
set('auth_previous_options', 'auth_previous_options', 'cargo.authOptions ? Object.assign({},cargo.authOptions) : null'),
|
|
@@ -179,7 +181,7 @@ export function otpWhere(cargo, es = false) {
|
|
|
179
181
|
const number = String(cargo.otp_cell_number || '').replace(/\D/g, '').slice(-4), tail = cargo.voice ? number.split('').join(', ') : number;
|
|
180
182
|
if (cargo.otpSentOn === 'whatsapp')
|
|
181
183
|
return es ? 'Se lo acabo de enviar aquí en este chat.' : 'I just sent it to you here in this chat.';
|
|
182
|
-
const
|
|
184
|
+
const masked = maskEmail(cargo.otpAltEmail, cargo.voice, es ? 'es' : 'en');
|
|
183
185
|
if (cargo.otp_cell_number && cargo.otpAltSent === true)
|
|
184
186
|
return es ? 'Se lo envié por mensaje de texto al celular que termina con ' + tail + ', y también le envié un código a su correo ' + masked + '. Revise sus mensajes de texto y su correo.' : 'I sent it by text message to the cell ending with ' + tail + ', and I also sent a code to your email ' + masked + '. Check your text messages and your email.';
|
|
185
187
|
if (cargo.otp_cell_number)
|
|
@@ -189,14 +191,14 @@ export function otpWhere(cargo, es = false) {
|
|
|
189
191
|
export function otpPrompt(cargo, es = false) {
|
|
190
192
|
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;
|
|
191
193
|
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 || ''));
|
|
192
|
-
const
|
|
194
|
+
const also = cargo.otp_cell_number && cargo.otpAltSent === true ? (es ? ' o en su correo ' : ' or at your email ') + maskEmail(cargo.otpAltEmail, cargo.voice, es ? 'es' : 'en') : '';
|
|
193
195
|
const name = cargo.callerPreselected && cargo.callerNamed && cargo.displayName ? cargo.displayName : '';
|
|
194
196
|
const lead = String((es ? cargo.otpIntro_es : cargo.otpIntro) || '');
|
|
195
197
|
return (lead ? lead + ' ' : '') + (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 + also + '. ' + (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.`) : '');
|
|
196
198
|
}
|
|
197
|
-
/** What the caller hears when a second code goes to the account's email
|
|
199
|
+
/** What the caller hears when a second code goes to the account's email, masked by maskEmail (jsfe/helpers). */
|
|
198
200
|
export function otpAlsoSent(cargo, es = false) {
|
|
199
|
-
const
|
|
201
|
+
const masked = maskEmail(cargo.otpAltEmail, cargo.voice, es ? 'es' : 'en');
|
|
200
202
|
return es ? 'Muy bien, también le envié un código a su correo ' + masked + '.' : 'Okay, I also sent a code to your email ' + masked + '.';
|
|
201
203
|
}
|
|
202
204
|
// The caller cannot find the texted code (or asks for another way, or for email): a second code goes to the account's email
|
|
@@ -220,6 +222,8 @@ const code = worker('get-and-validate-otp-code', 'GetAndValidateOtpCode', 'Colle
|
|
|
220
222
|
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')),
|
|
221
223
|
call('otp_validate', 'validate-otp-code', undefined, 'replace')
|
|
222
224
|
] });
|
|
225
|
+
// A verification that ran out of attempts ends cleanly (owner 2026-10-30): no retry question is left open for the next turn.
|
|
226
|
+
const verificationEnd = id => call(id, 'verification-failed', { form: '{{cargo.authOptions.parent_form}}' }, 'reboot');
|
|
223
227
|
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'); }
|
|
224
228
|
const OTP_WORDS = {
|
|
225
229
|
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'],
|
|
@@ -299,19 +303,19 @@ const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a
|
|
|
299
303
|
default: effect('otp_reply_route_none', 'true')
|
|
300
304
|
}),
|
|
301
305
|
effect('otp_word_count', '(cargo.otpWordMisses=Number(cargo.otpWordMisses || 0)+(no_code_given?1:0),true)'),
|
|
302
|
-
when('otp_word_cap', 'no_code_given && cargo.otpWordMisses >= 3',
|
|
306
|
+
when('otp_word_cap', 'no_code_given && cargo.otpWordMisses >= 3', verificationEnd('otp_word_end')),
|
|
303
307
|
// a reason for the call authentication serves is acknowledged: verification comes first
|
|
304
308
|
when('otp_usecase_ack', "no_code_given && otp_reply && String(otp_reply.key || '').indexOf('usecase:') === 0", say('otp_usecase_explain', 'Understood. First we need to finish verifying your identity. 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.', "Entendido. Primero necesitamos completar la verificación de su identidad. 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.")),
|
|
305
309
|
when('otp_explanation', "no_code_given && !(no_code_given && otp_reply && String(otp_reply.key || '').indexOf('usecase:') === 0)", 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.")),
|
|
306
310
|
effect('otp_format_count', '(cargo.otpFormatMisses=Number(cargo.otpFormatMisses || 0)+(normalized_otp_code.length > 0 && normalized_otp_code.length !== 6?1:0),true)'),
|
|
307
|
-
when('otp_format_cap', 'normalized_otp_code.length !== 6 && cargo.otpFormatMisses >= 3',
|
|
311
|
+
when('otp_format_cap', 'normalized_otp_code.length !== 6 && cargo.otpFormatMisses >= 3', verificationEnd('otp_format_end')),
|
|
308
312
|
when('otp_format_retry', 'normalized_otp_code.length !== 6', call('otp_collect_again', 'get-and-validate-otp-code', undefined, 'replace')),
|
|
309
313
|
effect('otp_mismatch_count', "(cargo.otpMismatches=Number(cargo.otpMismatches || 0)+(!cargo.otpVerified && cargo.otpFailure === 'mismatch'?1:0),true)"),
|
|
310
314
|
when('otp_mismatch_cap', "!cargo.otpVerified && cargo.otpFailure === 'mismatch' && cargo.otpMismatches >= 3", effect('otp_withdraw', '(cargo.otpHash=null,cargo.otpAltHash=null,cargo.otpAltTimestamp=null,true)')),
|
|
311
315
|
cases('otp_verified_result', {
|
|
312
316
|
'condition: cargo.otpVerified === true': effect('otp_verified_record', '(cargo.otpLateCode=String(normalized_otp_code),true)'),
|
|
313
317
|
"condition: cargo.otpFailure === 'expired' || cargo.otpFailure === 'missing'": call('otp_refresh', 'resend-otp-code', { resend_reason: '{{cargo.otpFailure}}' }),
|
|
314
|
-
"condition: cargo.otpFailure === 'mismatch' && cargo.otpMismatches >= 3":
|
|
318
|
+
"condition: cargo.otpFailure === 'mismatch' && cargo.otpMismatches >= 3": verificationEnd('otp_mismatch_end'),
|
|
315
319
|
default: say('otp_mismatch', "That code didn't match. Let's try again.", 'Ese código no coincide. Intentemos de nuevo.')
|
|
316
320
|
}),
|
|
317
321
|
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 : ''"),
|
|
@@ -395,4 +399,29 @@ const resume = worker('resume-identity-caller', 'ResumeIdentityCaller', 'Resume
|
|
|
395
399
|
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}')),
|
|
396
400
|
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')]
|
|
397
401
|
});
|
|
398
|
-
|
|
402
|
+
// The closing a verification out of attempts says: the task it was for (authenticate-user's task), and that the caller can try
|
|
403
|
+
// again once the code can reach them (owner wording 2026-10-30); part 'agent' is its last sentence, said after the tenant's
|
|
404
|
+
// failed_flow. An unknown action result keeps its hold notice, with no agent sentence.
|
|
405
|
+
export function verificationFailedText(cargo, ended, es = false, part = '') {
|
|
406
|
+
const hold = !!(ended && ended.recovery && ended.recovery.mode === 'hold');
|
|
407
|
+
if (part === 'agent')
|
|
408
|
+
return hold ? '' : es ? 'Para hablar con una persona, ' + cargo.verb_es + ' AGENTE.' : 'To reach a person, ' + cargo.verb + ' AGENT.';
|
|
409
|
+
if (hold)
|
|
410
|
+
return es ? 'El resultado de su solicitud anterior sigue sin confirmarse. Nuestro equipo debe revisarlo antes de otro intento.' : 'The result of your earlier request is still unknown. Our team needs to check it before another attempt.';
|
|
411
|
+
const task = String((cargo.authOptions && (es ? cargo.authOptions.task_es : cargo.authOptions.task)) || '');
|
|
412
|
+
return es ? 'No pude verificar su cuenta, así que no puedo ' + (task || 'continuar') + ' en este momento. Cuando pueda recibir el código para esta cuenta, podemos intentarlo de nuevo.'
|
|
413
|
+
: "I wasn't able to verify your account, so I can't " + (task || 'continue') + ' right now. Whenever you have access to receive the code for this account, we can try again.';
|
|
414
|
+
}
|
|
415
|
+
const failed = worker('verification-failed', 'VerificationFailed', 'End a verification that ran out of attempts: retire the owning task, set cargo.verificationFailed, report it unresolved (verification_failed), say so, run the failed_flow given to authenticate-user, then offer an agent.', {
|
|
416
|
+
parameters: [param('form', 'Owning task FORM instance')], variables: vars({ form: '', ended: null, vf_text: '', vf_text_es: '', vf_agent: '', vf_agent_es: '' }),
|
|
417
|
+
contract: { role: 'worker', summary: 'Clean end of a failed verification' },
|
|
418
|
+
steps: [set('vf_retire', 'ended', "form && cargo.forms && cargo.forms[form] ? cancelFormTask(cargo,form) : null"),
|
|
419
|
+
effect('vf_clear_code', "(cargo.otpHash=null,cargo.otpTimestamp=null,cargo.otpAltHash=null,cargo.otpAltTimestamp=null,cargo.verificationFailed=true,true)"),
|
|
420
|
+
set('vf_text', 'vf_text', expression(verificationFailedText, 'cargo', 'ended')), set('vf_text_es', 'vf_text_es', expression(verificationFailedText, 'cargo', 'ended', 'true')),
|
|
421
|
+
{ id: 'vf_say', type: 'SAY', value: '{{vf_text}}', value_es: '{{vf_text_es}}', outcome: "{{ended && ended.recovery && ended.recovery.mode === 'hold' ? ended.outcome : 'unresolved'}}", reason: "{{ended && ended.recovery && ended.recovery.mode === 'hold' ? ended.reason : 'verification_failed'}}" },
|
|
422
|
+
{ id: 'vf_failed_flow', type: 'CASE', branches: { "condition: !(ended && ended.recovery && ended.recovery.mode === 'hold') && !!(cargo.authOptions && cargo.authOptions.failed_flow)": call('vf_run_failed_flow', '{{cargo.authOptions.failed_flow}}', {}), default: set('vf_failed_flow_skip', 'noop', 'true') } },
|
|
423
|
+
set('vf_agent', 'vf_agent', expression(verificationFailedText, 'cargo', 'ended', 'false', "'agent'")), set('vf_agent_es', 'vf_agent_es', expression(verificationFailedText, 'cargo', 'ended', 'true', "'agent'")),
|
|
424
|
+
{ id: 'vf_offer_agent', type: 'CASE', branches: { 'condition: !!vf_agent': { id: 'vf_agent_say', type: 'SAY', value: '{{vf_agent}}', value_es: '{{vf_agent_es}}' }, default: set('vf_agent_skip', 'noop', 'true') } },
|
|
425
|
+
end('vf_end')]
|
|
426
|
+
});
|
|
427
|
+
export const FLOWS = [taskGate, resume, service, rawTransport, contact, declined, splitFailed, rejected, auth, code, validateCode, emailToo, retry, email, failed];
|
package/dist/runtime-identity.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const JSFE_RUNTIME_FINGERPRINT = "sha256:
|
|
1
|
+
export const JSFE_RUNTIME_FINGERPRINT = "sha256:a7ebbdf90eec8ba02813fa2f92b0c93f1282dfd46be3ade0fc9b789d96663198";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jsfe",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.0",
|
|
4
4
|
"description": "TypeScript workflow engine for conversational/automation apps: multi-flow orchestration, interrupt/resume, stack-based control, robust errors, and flexible tool/REST integration.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ron Pinkas",
|