jsfe 1.15.9 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/docs/flow-docs.d.ts.map +1 -1
- package/dist/docs/flow-docs.js +4 -2
- package/dist/forms/address-flows.js +1 -1
- package/dist/forms/continuation-compat.d.ts +5 -0
- package/dist/forms/continuation-compat.d.ts.map +1 -0
- package/dist/forms/continuation-compat.js +45 -0
- package/dist/forms/contracts.d.ts.map +1 -1
- package/dist/forms/contracts.js +6 -4
- package/dist/forms/declarations.d.ts +3 -0
- package/dist/forms/declarations.d.ts.map +1 -1
- package/dist/forms/declarations.js +26 -4
- package/dist/forms/flows.d.ts.map +1 -1
- package/dist/forms/flows.js +12 -6
- package/dist/forms/identity-flows.d.ts +3 -0
- package/dist/forms/identity-flows.d.ts.map +1 -1
- package/dist/forms/identity-flows.js +53 -7
- package/dist/forms/index.d.ts +1 -0
- package/dist/forms/index.js +1 -0
- package/dist/helpers/address.d.ts +1 -0
- package/dist/helpers/address.d.ts.map +1 -1
- package/dist/helpers/address.js +4 -1
- package/dist/helpers/form-actions.js +1 -1
- package/dist/helpers/index.d.ts +2 -1
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +2 -2
- package/dist/helpers/otp-check.d.ts +9 -3
- package/dist/helpers/otp-check.d.ts.map +1 -1
- package/dist/helpers/otp-check.js +20 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/runtime-identity.js +1 -1
- package/docs/architecture-guide.md +366 -0
- package/docs/copilot-reference.md +2 -4
- package/docs/development-release.md +7 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ npm i jsfe
|
|
|
12
12
|
- **[JavaScript Flow Engine User Guide](user-guide.md)**
|
|
13
13
|
- Comprehensive tutorials, examples, and best practices
|
|
14
14
|
- **[README.md](README.md)** - Technical API reference (this document)
|
|
15
|
+
- **[Flows architecture guide](docs/architecture-guide.md)** - start here: the concepts, the building blocks, what the caller experiences, and how conversational AI and exact transactional flows work together (the reply ranker and DISPATCH)
|
|
15
16
|
- **[Flow author reference](docs/copilot-reference.md)** - the authoritative engine and JSON authoring specification for flow authors and AI flow generators (shipped in the package; the Flow Designer's CoPilot reads it from the published version, so change it with the engine)
|
|
16
17
|
- **[Development candidate acceptance](docs/development-release.md)** - reproducible package and cross-repository verification
|
|
17
18
|
- **[System Flows](docs/flows/system/README.md)** and **[Shopify Flows](docs/flows/shopify/README.md)** - the bundled flow and tool libraries in `flows/`: a page and a chart per flow, generated from the JSON (`node scripts/flow-docs.mjs`), plus the conventions the system flows share
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"flow-docs.d.ts","sourceRoot":"","sources":["../../src/docs/flow-docs.js"],"names":[],"mappings":"AAkBA;;;;;;;;;;;EAsRC"}
|
package/dist/docs/flow-docs.js
CHANGED
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import fs from 'fs';
|
|
16
16
|
import path from 'path';
|
|
17
|
+
import { resolveSetDataReferences } from '../forms/declarations.js';
|
|
17
18
|
export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log = console.log }) {
|
|
18
|
-
|
|
19
|
+
// A catalog or value written as {"$data": path} is documented as compilation resolves it.
|
|
20
|
+
const SETS = sets.map((s) => ({ ...s, flows: resolveSetDataReferences(s.flows) }));
|
|
19
21
|
// Which set holds a flow id; a page links to a flow in its own set, or in another set's folder.
|
|
20
22
|
const setOf = new Map(), flowOf = new Map();
|
|
21
23
|
for (const s of SETS) {
|
|
@@ -422,7 +424,7 @@ export function generateFlowDocs({ sets, root, check = false, hostRun = {}, log
|
|
|
422
424
|
}
|
|
423
425
|
else { /* not a compiled form */ }
|
|
424
426
|
if (flow.performs) {
|
|
425
|
-
out.push('', '### Performs', '', ...flow.performs.map((a) => `- \`${a.action}\` (${a.effect})
|
|
427
|
+
out.push('', '### Performs', '', ...flow.performs.map((a) => `- \`${a.action}\` (${a.effect})`), '', performsChart(flow));
|
|
426
428
|
}
|
|
427
429
|
else { /* not a performer */ }
|
|
428
430
|
if (flow.call) {
|
|
@@ -28,7 +28,7 @@ const street = C('address-street-validate', 'Validate a proposed street against
|
|
|
28
28
|
W('address_street_malformed', "!/^\\d{1,6}\\s+[A-Za-z\\xC0-\\xFF]/.test(String(value))", S('address_street_invalid', 'noop', "(cargo.inputCheck={ok:false,message:'Please give the street number and street name.',message_es:'Indique el número y nombre de la calle.'},true)")), W('address_street_stop', "!/^\\d{1,6}\\s+[A-Za-z\\xC0-\\xFF]/.test(String(value))", { id: 'address_street_invalid_end', type: 'END' }),
|
|
29
29
|
S('address_street_components', 'components', "'postal_code:'+values.zip+'|country:US'"), { id: 'address_street_tool', type: 'CALL-TOOL', tool: 'geocode-street', variable: 'result', args: { address: '{{value}}', components: '{{components}}' }, onFail: S('address_street_failed', 'result', 'null') },
|
|
30
30
|
S('address_street_projection', 'location', E(streetLocation, 'result', 'value', 'values.zip')),
|
|
31
|
-
S('address_street_result', 'noop', "(cargo.forms[form].data.street=location, cargo.inputCheck={ok:true,value:String(value)}, true)")
|
|
31
|
+
S('address_street_result', 'noop', "(cargo.forms[form].data.street=location, location.city&&cargo.forms[form].accepted&&cargo.forms[form].accepted.city&&cargo.forms[form].accepted.city.origin==='derived'&&cargo.forms[form].values.city!==location.city?(cargo.forms[form].values.city=location.city):null, cargo.inputCheck={ok:true,value:String(value)}, true)")
|
|
32
32
|
], { parameters: [P('form'), P('value'), P('values', 'object')], variables: V({ components: '', result: null, location: null }), contract: { tools: ['geocode-street'], result: { path: 'cargo.inputCheck', type: 'object' } } });
|
|
33
33
|
const zipPrompt = C('address-zip-prompt', 'Render the ZIP retry wording from the owned address instance.', [S('address_zip_prompt', 'noop', "(cargo.forms[form].data.zip_question=(cargo.forms[form].data.retry ? \"Let's try again. \" : '')+'What is the 5-digit zip code of your new address?',cargo.forms[form].data.zip_question_es=(cargo.forms[form].data.retry?'Intentémoslo otra vez. ':'')+'¿Cuál es el código postal de 5 dígitos de su nueva dirección?',true)")], { parameters: [P('form')], contract: { result: { path: 'cargo.forms[form].data', type: 'object' } } });
|
|
34
34
|
const streetPrompt = C('address-street-prompt', 'Include the ZIP-derived location when asking for the street.', [S('address_street_prompt', 'noop', "(cargo.forms[form].data.street_question=(cargo.forms[form].data.location?'Got it, '+(cargo.forms[form].values.city?cargo.forms[form].values.city+', '+cargo.forms[form].data.location.state:cargo.forms[form].data.location.line)+'. ':'')+'What is the street address?',cargo.forms[form].data.street_question_es=(cargo.forms[form].data.location?'Entendido, '+(cargo.forms[form].values.city?cargo.forms[form].values.city+', '+cargo.forms[form].data.location.state:cargo.forms[form].data.location.line)+'. ':'')+'¿Cuál es la dirección?',true)")], { parameters: [P('form')], contract: { result: { path: 'cargo.forms[form].data', type: 'object' } } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"continuation-compat.d.ts","sourceRoot":"","sources":["../../src/forms/continuation-compat.js"],"names":[],"mappings":"AAgBA;;;IAgBC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A deploy check: a change that can break a saved session must bump its declaration's major version. A saved session
|
|
3
|
+
* continues across a minor or patch change, so an entry argument or a form input it may still hold must not disappear
|
|
4
|
+
* without a major bump; neither may a shared data or function name without its entry's major bump. Removing a flow needs
|
|
5
|
+
* no bump: a session standing in a flow that no longer exists is retired anyway.
|
|
6
|
+
*
|
|
7
|
+
* continuationBreaks(previousFlows, nextFlows) → [{ id, removed: { parameters?, inputs?, data?, functions? } }]
|
|
8
|
+
* both arrays are the authored declarations (a set's .flows file); ordinary executable flows are not compared.
|
|
9
|
+
* It reads names only: a change of meaning under the same name is still the author's to declare with a major bump.
|
|
10
|
+
*/
|
|
11
|
+
import { majorOf } from './declarations.js';
|
|
12
|
+
const parameterNames = parameters => Array.isArray(parameters) ? parameters.map(p => p?.name).filter(Boolean) : Object.keys(parameters || {});
|
|
13
|
+
const inputNames = inputs => (inputs || []).flatMap(input => [input?.name, ...inputNames(input?.any_of)]).filter(Boolean);
|
|
14
|
+
const missing = (before, after) => { const keep = new Set(after); return [...new Set(before)].filter(name => !keep.has(name)); };
|
|
15
|
+
export function continuationBreaks(previousFlows, nextFlows) {
|
|
16
|
+
const next = new Map((nextFlows || []).filter(flow => flow && flow.id).map(flow => [flow.id, flow]));
|
|
17
|
+
const breaks = [];
|
|
18
|
+
for (const before of previousFlows || []) {
|
|
19
|
+
const after = before && next.get(before.id);
|
|
20
|
+
if (!after || before.declarationVersion === undefined || majorOf(before.version) !== majorOf(after.version))
|
|
21
|
+
continue;
|
|
22
|
+
const removed = {};
|
|
23
|
+
if (before.library === 'data') {
|
|
24
|
+
const names = missing(Object.keys(before.data || {}), Object.keys(after.data || {}));
|
|
25
|
+
if (names.length)
|
|
26
|
+
removed.data = names;
|
|
27
|
+
}
|
|
28
|
+
else if (before.library === 'functions') {
|
|
29
|
+
const names = missing(Object.keys(before.functions || {}), Object.keys(after.functions || {}));
|
|
30
|
+
if (names.length)
|
|
31
|
+
removed.functions = names;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
const parameters = missing(parameterNames(before.parameters), parameterNames(after.parameters));
|
|
35
|
+
if (parameters.length)
|
|
36
|
+
removed.parameters = parameters;
|
|
37
|
+
const inputs = missing(inputNames(before.form?.inputs), inputNames(after.form?.inputs));
|
|
38
|
+
if (inputs.length)
|
|
39
|
+
removed.inputs = inputs;
|
|
40
|
+
}
|
|
41
|
+
if (Object.keys(removed).length)
|
|
42
|
+
breaks.push({ id: before.id, removed });
|
|
43
|
+
}
|
|
44
|
+
return breaks;
|
|
45
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../src/forms/contracts.js"],"names":[],"mappings":"AAgBA;;;IAGC;AACD;;;;;EAGC;AACD,qGAAqG;AACrG;;QAwCC;AAqBD;;;QAEC;AAgBD;;;;;;;;;QASC;AACD;;;;QAGC;AACD;;;;;;;;QA8BC;AAED,mHAAmH;AACnH,kDAgBC;AAED,+GAA+G;AAC/G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../src/forms/contracts.js"],"names":[],"mappings":"AAgBA;;;IAGC;AACD;;;;;EAGC;AACD,qGAAqG;AACrG;;QAwCC;AAqBD;;;QAEC;AAgBD;;;;;;;;;QASC;AACD;;;;QAGC;AACD;;;;;;;;QA8BC;AAED,mHAAmH;AACnH,kDAgBC;AAED,+GAA+G;AAC/G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4EC"}
|
package/dist/forms/contracts.js
CHANGED
|
@@ -138,7 +138,7 @@ export function defineProvider({ provides, contract, parameters = [], variables
|
|
|
138
138
|
}
|
|
139
139
|
export function defineForm({ contract, provides, ...spec }) {
|
|
140
140
|
const flow = formFlow({ ...spec, ...(provides ? { extraVars: { ...spec.extraVars, provides, questions: providerQuestions(provides) } } : {}) });
|
|
141
|
-
return declared(flow, { role: 'collector', ...contract, form: { inputs: 'inputs' } });
|
|
141
|
+
return declared(flow, { role: 'collector', ...contract, form: { inputs: 'inputs' }, ...(provides ? { catalog: 'provides' } : {}) });
|
|
142
142
|
}
|
|
143
143
|
export function definePerformer({ performs, contract, prepare, present, project = [], provides, ...spec }) {
|
|
144
144
|
validateActionProjection(project, spec.id);
|
|
@@ -170,7 +170,7 @@ export function definePerformer({ performs, contract, prepare, present, project
|
|
|
170
170
|
{ ...set('receipt_applied', 'noop', 'pf_receipt ? consumeActionReceipt(cargo,pf_receipt) : true'), requireTrue: true },
|
|
171
171
|
call('present', present, { form: '{{form_instance}}', request: '{{pf_request}}', result: '{{pf_result}}' }),
|
|
172
172
|
...(spec.after || [])] });
|
|
173
|
-
return declared(flow, { role: 'performer', ...contract, requires: ['guardedActions', ...normalizeHostRequirements(contract?.requires, spec.id)], actions: 'performs', form: { inputs: 'inputs' }, prepares: prepare, presents: present, projects: 'projection' });
|
|
173
|
+
return declared(flow, { role: 'performer', ...contract, requires: ['guardedActions', ...normalizeHostRequirements(contract?.requires, spec.id)], actions: 'performs', form: { inputs: 'inputs' }, prepares: prepare, presents: present, projects: 'projection', ...(provides ? { catalog: 'provides' } : {}) });
|
|
174
174
|
}
|
|
175
175
|
/** A registered tool gets one generated adapter with explicit args; no dynamic tool-name interpreter is needed. */
|
|
176
176
|
export function operationWorkers(tools) {
|
|
@@ -278,6 +278,10 @@ export function validateFlowContracts(flows, tools = [], { externalFlows = [], r
|
|
|
278
278
|
for (const reference of [provides.gate?.flow, provides.source?.flow])
|
|
279
279
|
if (reference && !ids.has(reference))
|
|
280
280
|
fail(flow.id, `unknown provider worker ${reference}`);
|
|
281
|
+
// a FLOW to a name that does not exist is skipped silently at run time, so a menu choice's target is checked here
|
|
282
|
+
for (const choice of provides.menu?.choices || [])
|
|
283
|
+
if (choice?.flow && !ids.has(choice.flow))
|
|
284
|
+
fail(flow.id, `unknown menu worker ${choice.flow}`);
|
|
281
285
|
}
|
|
282
286
|
for (const action of contract.performs || flow.variables?.[contract.actions]?.value || []) {
|
|
283
287
|
if (action.retry?.unknown && !(action.effect === 'send' && action.retry.unknown === 'explicit'))
|
|
@@ -292,8 +296,6 @@ export function validateFlowContracts(flows, tools = [], { externalFlows = [], r
|
|
|
292
296
|
fail(flow.id, `unknown action worker ${action.flow}`);
|
|
293
297
|
if (!action.outcomes || !Object.keys(action.outcomes).length)
|
|
294
298
|
fail(flow.id, `action ${action.action} needs outcomes`);
|
|
295
|
-
if (['charge', 'write', 'send', 'cancel'].includes(action.effect) && !action.idempotency && !action.reconcile)
|
|
296
|
-
fail(flow.id, `action ${action.action} needs idempotency or reconciliation`);
|
|
297
299
|
if ((action.effect === 'charge' || action.effect === 'cancel' || action.reversible === false) && action.confirm?.exact !== true)
|
|
298
300
|
fail(flow.id, `action ${action.action} needs exact confirmation`);
|
|
299
301
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** The set's shared data for expressions, read as $data.<name>: references resolved, deep-frozen (one engine serves many
|
|
2
2
|
* sessions), never copied into session variables. Every $data.<name> an expression reads must exist, checked at load. */
|
|
3
3
|
export function flowSetData(flows: any, compiledFlows?: any[]): any;
|
|
4
|
+
export function resolveSetDataReferences(flows: any): any;
|
|
4
5
|
/** Compile only explicitly versioned declarations. Ordinary executable JSON is unchanged.
|
|
5
6
|
* @param {any[]} flows
|
|
6
7
|
* @param {{tools?: any[], externalFlows?: any[], validate?: boolean}} options
|
|
@@ -20,4 +21,6 @@ export function captureOriginalDeclaration(flow: any): {
|
|
|
20
21
|
export function serializeFlowDeclaration(flow: any, { resolvedFlow }?: {
|
|
21
22
|
resolvedFlow?: any;
|
|
22
23
|
}): any;
|
|
24
|
+
/** The major of a declared version ("2.4.1" → "2"; absent or unreadable → "0"). */
|
|
25
|
+
export function majorOf(version: any): string;
|
|
23
26
|
//# sourceMappingURL=declarations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"declarations.d.ts","sourceRoot":"","sources":["../../src/forms/declarations.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"declarations.d.ts","sourceRoot":"","sources":["../../src/forms/declarations.js"],"names":[],"mappings":"AAyDA;yHACyH;AACzH,oEAaC;AAID,0DAIC;AAmFD;;;;GAIG;AACH,+CAJW,GAAG,EAAE,uCACL;IAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAC;IAAC,aAAa,CAAC,EAAE,GAAG,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAC,GACxD,GAAG,EAAE,CAuEjB;AAED,sGAAsG;AACtG;;;SAAqK;AAuBrK,6FAA6F;AAC7F;;QAuCC;AAnSD,mFAAmF;AACnF,8CAA4G"}
|
|
@@ -12,6 +12,15 @@ const fail = message => { throw new Error('Flow declaration: ' + message); };
|
|
|
12
12
|
function sourceRevision(source) { const text = JSON.stringify(source); const hashes = [2166136261, 2246822507, 3266489909, 668265263]; for (let i = 0; i < text.length; i++)
|
|
13
13
|
for (let lane = 0; lane < hashes.length; lane++)
|
|
14
14
|
hashes[lane] = Math.imul(hashes[lane] ^ text.charCodeAt(i), 16777619 + lane * 2) >>> 0; return hashes.map(n => n.toString(16).padStart(8, '0')).join(''); }
|
|
15
|
+
/** The major of a declared version ("2.4.1" → "2"; absent or unreadable → "0"). */
|
|
16
|
+
const majorOf = version => { const m = /^\s*v?(\d+)/i.exec(String(version ?? '')); return m ? String(Number(m[1])) : '0'; };
|
|
17
|
+
/** A declaration's continuation revision: its major version, plus the major of each shared entry it uses ($data, fn.).
|
|
18
|
+
* A minor or patch change keeps the revision, so a saved session continues; a breaking change bumps the major. */
|
|
19
|
+
function majorRevision(declarationVersion, source, libraries) {
|
|
20
|
+
const text = JSON.stringify(source), uses = { data: /\$data\b/.test(text), functions: /(?<![\w$])fn\./.test(text) };
|
|
21
|
+
const shared = libraries.filter(library => uses[library.library]).map(library => library.library + '@' + library.id + ':' + majorOf(library.version)).sort();
|
|
22
|
+
return 'declaration-v' + declarationVersion + ':major:' + majorOf(source.version) + (shared.length ? '/' + shared.join('/') : '');
|
|
23
|
+
}
|
|
15
24
|
const variableDeclaration = value => value && typeof value === 'object' && !Array.isArray(value) && ['string', 'number', 'integer', 'boolean', 'object', 'array'].includes(value.type) ? value : { type: Array.isArray(value) ? 'array' : value === null || typeof value === 'object' ? 'object' : typeof value, value };
|
|
16
25
|
const DECLARATION_VERSIONS = new Set([1, 2]);
|
|
17
26
|
/** A flow set's shared entries: {id, library:'functions', declarationVersion:2, functions} and
|
|
@@ -29,7 +38,7 @@ function sharedFunctions(libraries) {
|
|
|
29
38
|
fail('a shared functions entry needs an id');
|
|
30
39
|
if (library.declarationVersion !== 2)
|
|
31
40
|
fail('shared functions ' + library.id + ' require declarationVersion 2');
|
|
32
|
-
const extra = Object.keys(library).filter(key => !['id', 'name', 'library', 'declarationVersion', 'description', 'functions'].includes(key));
|
|
41
|
+
const extra = Object.keys(library).filter(key => !['id', 'name', 'library', 'declarationVersion', 'description', 'version', 'functions'].includes(key));
|
|
33
42
|
if (extra.length)
|
|
34
43
|
fail('shared functions ' + library.id + ' cannot declare ' + extra.join(', '));
|
|
35
44
|
if (!library.functions || typeof library.functions !== 'object' || Array.isArray(library.functions) || !Object.keys(library.functions).length)
|
|
@@ -51,7 +60,7 @@ function sharedData(libraries) {
|
|
|
51
60
|
fail('a shared data entry needs an id');
|
|
52
61
|
if (library.declarationVersion !== 2)
|
|
53
62
|
fail('shared data ' + library.id + ' requires declarationVersion 2');
|
|
54
|
-
const extra = Object.keys(library).filter(key => !['id', 'name', 'library', 'declarationVersion', 'description', 'data'].includes(key));
|
|
63
|
+
const extra = Object.keys(library).filter(key => !['id', 'name', 'library', 'declarationVersion', 'description', 'version', 'data'].includes(key));
|
|
55
64
|
if (extra.length)
|
|
56
65
|
fail('shared data ' + library.id + ' cannot declare ' + extra.join(', '));
|
|
57
66
|
if (!library.data || typeof library.data !== 'object' || Array.isArray(library.data) || !Object.keys(library.data).length)
|
|
@@ -88,6 +97,15 @@ export function flowSetData(flows, compiledFlows = []) {
|
|
|
88
97
|
}
|
|
89
98
|
return Object.keys(resolved).length ? resolved : undefined;
|
|
90
99
|
}
|
|
100
|
+
/** A set's version 2 declarations with every {"$data":"path"} replaced by its value, as compilation sees them; the shared
|
|
101
|
+
* data entries are kept. For readers of the declarations (the flow docs); compilation resolves them itself. */
|
|
102
|
+
export { majorOf };
|
|
103
|
+
export function resolveSetDataReferences(flows) {
|
|
104
|
+
const data = sharedData((flows || []).filter(isLibrary));
|
|
105
|
+
if (!Object.keys(data).length)
|
|
106
|
+
return flows;
|
|
107
|
+
return flows.map(flow => flow?.declarationVersion === 2 && !isLibrary(flow) ? resolveDataReferences(flow, data, flow.id) : flow);
|
|
108
|
+
}
|
|
91
109
|
const isDataReference = value => !!value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 1 && typeof value.$data === 'string';
|
|
92
110
|
/** Replace every {"$data":"path"} with a copy of the shared value at that path; an unknown path fails compilation. */
|
|
93
111
|
function resolveDataReferences(value, data, where, active = new Set()) {
|
|
@@ -266,7 +284,11 @@ export function compileFlowDeclarations(flows, { tools, externalFlows = [], vali
|
|
|
266
284
|
return false; parameterNames.add(parameter.name); return true; }), ...(rest.parameterInstructions !== undefined ? { parameterInstructions: rest.parameterInstructions } : {}), ...(functions ? { functions } : {}), metadata: { ...flow.metadata, declaration: { version: declarationVersion, source: source.id } } };
|
|
267
285
|
// Revisions are engine bookkeeping; a stale authored token cannot override source change detection.
|
|
268
286
|
const { safetyRevision: ignoredRevision, ...revisionSource } = source;
|
|
269
|
-
|
|
287
|
+
const revised = withSafetyRevision(flow, majorRevision(declarationVersion, original, libraries));
|
|
288
|
+
// The earlier revision, a fingerprint of the whole declaration: a session saved under it continues while the declaration
|
|
289
|
+
// is unchanged (and takes the major revision), so moving to major revisions retires no session.
|
|
290
|
+
revised.safetyRevisionCompat = 'declaration-v' + declarationVersion + ':' + sourceRevision(declarationVersion === 2 && Object.keys(shared).length ? { ...revisionSource, sharedFunctions: shared } : revisionSource);
|
|
291
|
+
return revised;
|
|
270
292
|
});
|
|
271
293
|
const externalObjects = externalFlows.filter(flow => flow && typeof flow === 'object');
|
|
272
294
|
const formIds = new Set(flows.filter(flow => flow.declarationVersion !== undefined && flow.form).map(flow => flow.id));
|
|
@@ -472,7 +494,7 @@ export function serializeFlowDeclaration(flow, { resolvedFlow = flow } = {}) {
|
|
|
472
494
|
else if (spec.variables)
|
|
473
495
|
result.variables = spec.variables;
|
|
474
496
|
for (const [key, value] of Object.entries(spec))
|
|
475
|
-
if (!sharedKeys.includes(key) && !['extraVars', 'variables', 'helpers', 'safetyRevision'].includes(key) && !(isForm && formKeys.has(key)))
|
|
497
|
+
if (!sharedKeys.includes(key) && !['extraVars', 'variables', 'helpers', 'safetyRevision', 'safetyRevisionCompat'].includes(key) && !(isForm && formKeys.has(key)))
|
|
476
498
|
result[key] = value;
|
|
477
499
|
const out = rewrite(result);
|
|
478
500
|
// Helpers and bodies can reference other explicitly authored expressions as well.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAqIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6DC;AACD;;;GAGG;AACH;;;;;;EAGC;
|
|
1
|
+
{"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/forms/flows.js"],"names":[],"mappings":"AAqIA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6DC;AACD;;;GAGG;AACH;;;;;;EAGC;AAsfD,wHAAwH;AACxH,wEAaC;AACD,4FAA4F;AAC5F;;;EAYC;AACD,sGAAsG;AACtG;;;EAOC;AA3rBM,6CAAuG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAo3B9G;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAOG"}
|
package/dist/forms/flows.js
CHANGED
|
@@ -459,20 +459,26 @@ const readFormChoice = {
|
|
|
459
459
|
// form's other choices (a correction) and a person, never YES. Result: cargo.confirmRanked = 'no' | 'changed' | 'none'
|
|
460
460
|
// (ConfirmForm reads it); `abort` ends every flow; `new_request` goes to intent detection.
|
|
461
461
|
const readConfirmReply = {
|
|
462
|
-
id: 'read-confirm-reply', name: 'ReadConfirmReply', version: '1.
|
|
463
|
-
parameters: [{ name: 'form', type: 'string' }, { name: 'question', type: 'string' }, { name: 'question_es', type: 'string' }],
|
|
464
|
-
variables: vars({ question: '', question_es: '', asked: '', ranked: [], reply: null, chain: null, others: [], picked: null, reading: null }),
|
|
462
|
+
id: 'read-confirm-reply', name: 'ReadConfirmReply', version: '1.3.0', scope: 'isolated', description: 'Rank an unreadable answer to a confirmation: it declines (NO), picks another of the form\'s choices (a correction, read as that input\'s answer), says a named field read back is wrong (confirm.named_fields: that input is asked again), asks for a person, or is asked again; a withdrawal or a new request leaves with saved form progress. Never confirms.',
|
|
463
|
+
parameters: [{ name: 'form', type: 'string' }, { name: 'question', type: 'string' }, { name: 'question_es', type: 'string' }, { name: 'named_fields', type: 'boolean' }],
|
|
464
|
+
variables: vars({ question: '', question_es: '', asked: '', ranked: [], reply: null, chain: null, others: [], fields: [], field: null, picked: null, reading: null, named_fields: false }),
|
|
465
465
|
steps: [SET('confirm_asked', 'asked', "language === 'es' && question_es ? question_es : question"),
|
|
466
466
|
SET('confirm_chain', 'chain', 'formInputChain(cargo.forms, form, cargo)'),
|
|
467
467
|
SET('confirm_others', 'others', otherChoicesJS("''", '[]')),
|
|
468
|
-
|
|
468
|
+
// confirm.named_fields: a field read back may be corrected in the caller's own words ('es Long Beach' at an address
|
|
469
|
+
// read back with the ZIP code's city): each named input of this form that holds a value is offered, and a pick asks
|
|
470
|
+
// that input again, so the caller's value replaces the one read back
|
|
471
|
+
SET('confirm_fields', 'fields', "named_fields === true || named_fields === 'true' ? (chain || []).filter(function(i){var v=cargo.forms[form] && cargo.forms[form].values;return i.form===form && !i.confirmation && Array.isArray(i.names) && i.names.length && v && v[i.name]!==undefined && v[i.name]!==null && v[i.name]!=='';}).map(function(i){var l=String(i.label || i.names[0]);return {key:'field:'+i.name,label:'says the '+l+' read back is wrong, or gives a different '+l,owner:{form:i.form,name:i.name}};}) : []"),
|
|
472
|
+
rankedChoices('confirm_ranked_choices', "[{key:'no',label:'no: declines or refuses what the question asks to confirm'}].concat(others.map(function(c){return {key:c.key,label:c.label};})).concat(fields.map(function(c){return {key:c.key,label:c.label};}))"),
|
|
469
473
|
rankedDispatch('confirm_dispatch', 'confirmation_not_matched'),
|
|
470
474
|
...escapeRoutes('confirm_escape'),
|
|
471
475
|
// a correction picked by the ranker is read as its input's answer: the form changes and is confirmed again, never confirmed
|
|
472
476
|
SET('confirm_picked', 'picked', "others.filter(function(c){return reply && c.key===reply.key;})[0] || null"),
|
|
473
477
|
SET('confirm_read', 'reading', "picked ? enrichFormMatches(readFormAnswer(picked.key, chain, picked.owner, picked.key, []), chain, form, [picked.owner.name], picked.owner.name) : null"),
|
|
474
478
|
WHEN('confirm_change', 'reading && reading.hits.length', FLOW('confirm_change_accept', 'collect-input-accept', 'call', { hits: '{{reading.hits}}' })),
|
|
475
|
-
|
|
479
|
+
SET('confirm_field', 'field', "fields.filter(function(c){return reply && c.key===reply.key;})[0] || null"),
|
|
480
|
+
WHEN('confirm_field_reset', 'field', SIDE('confirm_field_ask', '(resetFormInputs(cargo, field.owner.form, [field.owner.name]), true)')),
|
|
481
|
+
SIDE('confirm_ranked', "(cargo.confirmRanked=reading && reading.hits.length || field ? 'changed' : reply && reply.key==='no' ? 'no' : 'none',true)")]
|
|
476
482
|
};
|
|
477
483
|
const collectContextualAnswer = formFlow({ id: 'collect-contextual-answer', name: 'CollectContextualAnswer', description: 'Collect one open-ended answer using contextual DISPATCH and preserve the caller wording.',
|
|
478
484
|
parameters: [...['question', 'question_es', 'description', 'context', 'context_es'].map(name => ({ name, type: 'string' })), { name: 'choices', type: 'array' }, { name: 'digits', type: 'object' }],
|
|
@@ -597,7 +603,7 @@ const confirmForm = {
|
|
|
597
603
|
'condition: cf_named': SET('cf_named_reset', 'cf_result', "(resetFormInputs(cargo, cf_named.form, [cf_named.name]), 'changed')"),
|
|
598
604
|
"condition: cargo.choice === 'no'": SET('cf_declined', 'cf_result', "'no'"),
|
|
599
605
|
"condition: Array.isArray(clarifications) && pickChoice(String(cargo.choiceText || '').replace(/[?¿]/g,''),[{key:'clarify',phrases:clarifications,exact:true}],false)==='clarify'": SET('cf_clarification', 'cf_result', "'clarification'"),
|
|
600
|
-
'condition: !cf_must && cf_words > 0': FLOW('cf_elsewhere', 'read-confirm-reply', 'call', { form: '{{form}}', question: '{{cf_prompt}}', question_es: '{{cf_prompt_es}}' }),
|
|
606
|
+
'condition: !cf_must && cf_words > 0': FLOW('cf_elsewhere', 'read-confirm-reply', 'call', { form: '{{form}}', question: '{{cf_prompt}}', question_es: '{{cf_prompt_es}}', named_fields: '{{named_fields}}' }),
|
|
601
607
|
default: SET('cf_unclear', 'cf_result', "'unclear'")
|
|
602
608
|
} },
|
|
603
609
|
// the ranker's reading of an unread answer: a decline is the NO; an answer that declines nothing is asked again
|
|
@@ -5,7 +5,10 @@ export function serviceDigits(text: any): string;
|
|
|
5
5
|
export function otpDigits(text: any): string;
|
|
6
6
|
export function contactPrompt(cargo: any, options: any, callerAllowed: any, partial?: boolean, es?: boolean): string;
|
|
7
7
|
export function contactError(kind: any, options: any, es?: boolean): string;
|
|
8
|
+
export function otpWhere(cargo: any, es?: boolean): string;
|
|
8
9
|
export function otpPrompt(cargo: any, es?: boolean): string;
|
|
10
|
+
/** What the caller hears when a second code goes to the account's email: the address masked to its first letter and domain. */
|
|
11
|
+
export function otpAlsoSent(cargo: any, es?: boolean): string;
|
|
9
12
|
export function emailReadback(address: any, voice: any, es?: boolean): string;
|
|
10
13
|
/** Reenter the existing owning FORM or its gate without making the owner its own parent. */
|
|
11
14
|
export function identityResumeContext(forms: any, owner: any, target: any, identifiers?: {}): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"identity-flows.d.ts","sourceRoot":"","sources":["../../src/forms/identity-flows.js"],"names":[],"mappings":"AA2BA,+GAA+G;AAC/G,oFAOC;AAQD,wFAAwF;AACxF,iDAGC;AACD,6CAMC;AACD,qHAQC;AAMD,4EAKC;
|
|
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;AAwFD,2DAOC;AACD,4DAOC;AACD,+HAA+H;AAC/H,8DAGC;AA0ID,8EAGC;AA4BD,4FAA4F;AAC5F;;;;EAOC;AAQD,0BAA4I"}
|
|
@@ -95,6 +95,9 @@ const contact = worker('get-cell-or-email', 'GetCellOrEmail', 'Identify a suppli
|
|
|
95
95
|
when('identity_contact_partial_reset', '!contact_partial', effect('identity_contact_partial_forget', "(cargo.gcePartial='',true)")),
|
|
96
96
|
set('identity_contact_allowed', 'contact_allowed', "!!cargo.callerId && !callerIsInternational(cargo) && !cargo.callerDeclined && contact_options.caller_on_file !== 'no'"),
|
|
97
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
|
+
// a question answered from what is already known (the caller ID, a contact given) is never spoken: its intro (why the
|
|
99
|
+
// account is needed) leads the verification code question instead (otpPrompt). A spoken question said it (or had none).
|
|
100
|
+
effect('identity_contact_intro_unsaid', "(!contact_prefill ? (cargo.otpIntro='',cargo.otpIntro_es='') : contact_options.intro && !cargo.contactIntroSkip && !contact_options.fromMenu ? (cargo.otpIntro=String(contact_options.intro),cargo.otpIntro_es=String(contact_options.intro_es || contact_options.intro)) : true,true)"),
|
|
98
101
|
effect('identity_contact_preselected', "(acct_number || cell_number || email ? true : (cargo.callerPreselected=!!(contact_prefill && contact_options.caller_on_file === 'yes')),true)"),
|
|
99
102
|
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',phrases:['1','yes','si','sí'],exact:true},{key:'no',phrases:['no','nope','nah'],exact:true}]}}] : [])"),
|
|
100
103
|
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};})()"),
|
|
@@ -148,7 +151,7 @@ const authKeys = ['retry_flow', 'cancel_flow', 'email_validator', 'cell_validato
|
|
|
148
151
|
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: [
|
|
149
152
|
set('auth_restart', 'auth_restart', "retrying === true || retrying === 'true'"),
|
|
150
153
|
set('auth_previous_options', 'auth_previous_options', 'cargo.authOptions ? Object.assign({},cargo.authOptions) : null'),
|
|
151
|
-
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)")),
|
|
154
|
+
when('auth_declined_caller', "declined_caller === true || declined_caller === 'true'", effect('auth_decline_state', "(cargo.callerDeclined=true,cargo.callerPreselected=false,cargo.otpHash=null,cargo.otpAltHash=null,cargo.otpAltTimestamp=null,cargo.contactIntroSkip=true,cargo.callerNamed ? (cargo.displayName='',cargo.displayNameSetBy='flow',cargo.callerNamed=false) : true,true)")),
|
|
152
155
|
when('auth_already_verified', 'cargo.otpVerified === true', end('auth_verified_end')),
|
|
153
156
|
...authKeys.map(key => set('auth_option_' + key, key, `auth_restart && cargo.authOptions ? cargo.authOptions.${key} || ${key === 'retry_returns' ? 'false' : "''"} : ${key}`)),
|
|
154
157
|
set('auth_default_retry', 'retry_flow', "retry_flow || 'authenticate-user'"), set('auth_default_cancel', 'cancel_flow', "cancel_flow || 'contact-support'"),
|
|
@@ -165,22 +168,53 @@ const auth = worker('authenticate-user', 'AuthenticateUser', 'Authenticate a con
|
|
|
165
168
|
when('auth_join_challenge', 'auth_join', call('auth_join_collect', 'get-and-validate-otp-code')),
|
|
166
169
|
when('auth_join_release', 'auth_join', call('auth_join_gate_end', 'task-gate', { form: '{{parent_form}}', gate: 'authenticate-user', active: false })),
|
|
167
170
|
when('auth_join_return', 'auth_join', end('auth_join_complete')),
|
|
168
|
-
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)"),
|
|
171
|
+
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 || '',cargo.otpAltSent=false,cargo.otpAltHash=null,cargo.otpAltTimestamp=null,true)"),
|
|
169
172
|
call('auth_send', 'send-otp-code'), when('auth_send_failed', 'cargo.otpSendFailure', call('auth_send_failure', 'otp-send-failed', undefined, 'replace')),
|
|
170
173
|
call('auth_code', 'get-and-validate-otp-code'),
|
|
171
174
|
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', "I couldn't verify your account. You can try again or use another contact.", 'No pude verificar su cuenta. Puede intentarlo de nuevo o usar otro contacto.', true) }),
|
|
172
175
|
call('auth_gate_end', 'task-gate', { form: '{{parent_form}}', gate: 'authenticate-user', active: false })
|
|
173
176
|
] });
|
|
177
|
+
// Where the code went, for a caller who asks ('¿dónde está?'): the channel and the masked destination, never the code.
|
|
178
|
+
export function otpWhere(cargo, es = false) {
|
|
179
|
+
const number = String(cargo.otp_cell_number || '').replace(/\D/g, '').slice(-4), tail = cargo.voice ? number.split('').join(', ') : number;
|
|
180
|
+
if (cargo.otpSentOn === 'whatsapp')
|
|
181
|
+
return es ? 'Se lo acabo de enviar aquí en este chat.' : 'I just sent it to you here in this chat.';
|
|
182
|
+
const mail = String(cargo.otpAltEmail || ''), at = mail.indexOf('@'), masked = cargo.voice ? (es ? 'que empieza con ' : 'starting with ') + mail.charAt(0).toUpperCase() + (es ? ', arroba ' : ', at ') + mail.slice(at + 1) : mail.charAt(0) + '…@' + mail.slice(at + 1);
|
|
183
|
+
if (cargo.otp_cell_number && cargo.otpAltSent === true)
|
|
184
|
+
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
|
+
if (cargo.otp_cell_number)
|
|
186
|
+
return es ? 'Se lo envié por mensaje de texto al celular que termina con ' + tail + '. Revise sus mensajes de texto.' : 'I sent it by text message to the cell ending with ' + tail + '. Check your text messages.';
|
|
187
|
+
return es ? 'Se lo envié por correo electrónico a ' + String(cargo.otp_email || '') + '. Revise su correo.' : 'I sent it by email to ' + String(cargo.otp_email || '') + '. Check your email.';
|
|
188
|
+
}
|
|
174
189
|
export function otpPrompt(cargo, es = false) {
|
|
175
190
|
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;
|
|
176
191
|
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 mail = String(cargo.otpAltEmail || ''), at = mail.indexOf('@'), also = cargo.otp_cell_number && cargo.otpAltSent === true ? (es ? ' o en su correo ' : ' or at your email ') + (cargo.voice ? (es ? 'que empieza con ' : 'starting with ') + mail.charAt(0).toUpperCase() + (es ? ', arroba ' : ', at ') + mail.slice(at + 1) : mail.charAt(0) + '…@' + mail.slice(at + 1)) : '';
|
|
177
193
|
const name = cargo.callerPreselected && cargo.callerNamed && cargo.displayName ? cargo.displayName : '';
|
|
178
|
-
|
|
194
|
+
const lead = String((es ? cargo.otpIntro_es : cargo.otpIntro) || '');
|
|
195
|
+
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
|
+
}
|
|
197
|
+
/** What the caller hears when a second code goes to the account's email: the address masked to its first letter and domain. */
|
|
198
|
+
export function otpAlsoSent(cargo, es = false) {
|
|
199
|
+
const mail = String(cargo.otpAltEmail || ''), at = mail.indexOf('@'), masked = cargo.voice ? (es ? 'que empieza con ' : 'starting with ') + mail.charAt(0).toUpperCase() + (es ? ', arroba ' : ', at ') + mail.slice(at + 1) : mail.charAt(0) + '…@' + mail.slice(at + 1);
|
|
200
|
+
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 + '.';
|
|
179
201
|
}
|
|
202
|
+
// The caller cannot find the texted code (or asks for another way, or for email): a second code goes to the account's email
|
|
203
|
+
// (cargo.otpAltEmail, set by the tenant's contact step when the code goes to a cell), once per challenge, without a question.
|
|
204
|
+
// The texted code stays valid beside it (send-email-otp additional: otpAltHash). A refused address says where the code went.
|
|
205
|
+
const emailToo = worker('send-otp-email-too', 'SendOtpEmailToo', "Email a second one-time code to the account's email beside the texted one, and say so.", { variables: vars({ alt_hash: '', also_sent: '', also_sent_es: '', otp_where: '', otp_where_es: '' }), contract: { role: 'worker', summary: "Second one-time code to the account's email", tools: ['send-email-otp'] }, steps: [
|
|
206
|
+
{ id: 'otp_email_too_send', type: 'CALL-TOOL', tool: 'send-email-otp', variable: 'alt_hash', args: { to: '{{cargo.otpAltEmail}}', container: '{{cargo}}', language: '{{language}}', additional: true }, onFail: set('otp_email_too_failed', 'alt_hash', "''") },
|
|
207
|
+
effect('otp_email_too_state', "(cargo.otpAltSent=/^[0-9a-f]{64}$/.test(String(alt_hash || ''))?true:'failed',true)"),
|
|
208
|
+
set('otp_email_too_said', 'also_sent', expression(otpAlsoSent, 'cargo')), set('otp_email_too_said_es', 'also_sent_es', expression(otpAlsoSent, 'cargo', 'true')),
|
|
209
|
+
set('otp_email_too_where', 'otp_where', expression(otpWhere, 'cargo')), set('otp_email_too_where_es', 'otp_where_es', expression(otpWhere, 'cargo', 'true')),
|
|
210
|
+
cases('otp_email_too_say', { 'condition: cargo.otpAltSent === true': say('otp_email_too_sent', '{{also_sent}}', '{{also_sent_es}}'), default: say('otp_email_too_refused', '{{otp_where}}', '{{otp_where_es}}') })
|
|
211
|
+
] });
|
|
180
212
|
const OTP_PROFILES = [{ name: 'code', label: 'verification code', label_es: 'código de verificación', profile: { kind: 'digits', min: 6, max: 6 } }, raw];
|
|
181
213
|
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: [
|
|
182
214
|
when('otp_already_verified', 'cargo.otpVerified === true', end('otp_existing_verification')),
|
|
183
215
|
set('otp_question', 'otp_question', expression(otpPrompt, 'cargo')), set('otp_question_es', 'otp_question_es', expression(otpPrompt, 'cargo', 'true')),
|
|
216
|
+
// a contact question's unsaid intro (identity_contact_intro_unsaid) leads the first code question only
|
|
217
|
+
effect('otp_intro_said', "(cargo.otpIntro='',cargo.otpIntro_es='',true)"),
|
|
184
218
|
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' }),
|
|
185
219
|
when('otp_verified_by_join', 'cargo.otpVerified === true', end('otp_join_finished')),
|
|
186
220
|
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')),
|
|
@@ -193,6 +227,9 @@ const OTP_WORDS = {
|
|
|
193
227
|
other_alone: ['otra', 'otro', 'another', 'other'],
|
|
194
228
|
email: ['email', 'e-mail', 'e mail', 'emails', 'correo', 'correos', 'correo electrónico', 'correo electronico', 'imeil'],
|
|
195
229
|
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'],
|
|
230
|
+
// the caller cannot find the texted code, or asks for another way to get it or to verify
|
|
231
|
+
cannot_find: ['don t have it', 'i don t have it', 'don t have the code', 'i don t have the code', 'can t find', 'cannot find', 'can not find', 'couldn t find', 'could not find', 'can t find it', 'cannot find it', 'i can t find', 'i cannot find', 'not there', 'isn t there', 'it s not there', 'there s no code', 'no encuentro', 'no lo encuentro', 'no la encuentro', 'no encuentro el código', 'no encuentro el codigo', 'no aparece', 'no hay código', 'no hay codigo'],
|
|
232
|
+
another_way: ['another way', 'different way', 'other way', 'some other way', 'a different way', 'another option', 'other option', 'otra forma', 'otra manera', 'de otra forma', 'de otra manera', 'otra opción', 'otra opcion', 'hay otra forma', 'hay otra manera'],
|
|
196
233
|
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'],
|
|
197
234
|
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']
|
|
198
235
|
};
|
|
@@ -206,24 +243,29 @@ function otpPhoneUnavailable(value) {
|
|
|
206
243
|
// What the reply ranker may take at the code question beside the code: the step's offers. Email and another account are
|
|
207
244
|
// offered only where the step offers them (a code sent by text; a caller-ID preselection).
|
|
208
245
|
const OTP_REPLY_CHOICES = [{ key: 'email', label: 'use the email on the account instead of the phone' }, { key: 'another_account', label: 'use another account' },
|
|
209
|
-
{ key: 'resend', label: 'the code did not arrive; send it again' }, { key: 'no_phone', label: 'no longer has that phone or number' }, { key: '__agent', label: 'speak with a person (a live agent)' }];
|
|
210
|
-
const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a challenge response with explicit bounded retries, alternate-contact handling and tenant callbacks.', { variables: vars({ otp_phrases: 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: '', otp_reply_choices: OTP_REPLY_CHOICES, otp_offers: [], otp_reply: {} }), contract: { role: 'gate', summary: 'Validate the active one-time challenge', tools: ['validate-otp', 'send-whatsapp-invite'], result: { path: 'cargo.otpVerified', type: 'boolean' } }, steps: [
|
|
246
|
+
{ key: 'where', label: 'asks where the code was sent' }, { key: 'cannot_find', label: 'cannot find the code, or it is not there' }, { key: 'another_way', label: 'asks for another way to get the code or to verify' }, { key: 'resend', label: 'the code did not arrive; send it again' }, { key: 'no_phone', label: 'no longer has that phone or number' }, { key: '__agent', label: 'speak with a person (a live agent)' }];
|
|
247
|
+
const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a challenge response with explicit bounded retries, alternate-contact handling and tenant callbacks.', { variables: vars({ otp_phrases: OTP_WORDS, email_too: false, 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: '', otp_reply_choices: OTP_REPLY_CHOICES, otp_offers: [], otp_reply: {}, otp_where: '', otp_where_es: '' }), contract: { role: 'gate', summary: 'Validate the active one-time challenge', tools: ['validate-otp', 'send-whatsapp-invite'], result: { path: 'cargo.otpVerified', type: 'boolean' } }, steps: [
|
|
211
248
|
set('otp_cancel', 'otp_cancel', "cargo.authCancelFlow || 'contact-support'"),
|
|
249
|
+
set('otp_email_too_ready', 'email_too', "!!(cargo.otp_cell_number && cargo.otpAltSent !== true && cargo.otpAltSent !== 'failed' && validateEmail(String(cargo.otpAltEmail || '')))"),
|
|
212
250
|
set('otp_reset_no_code', 'no_code_given', 'false'), set('otp_reset_invite', 'whatsapp_invite_sent', 'false'), set('otp_reset_callback', 'answer_key', "''"),
|
|
213
251
|
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}}' })),
|
|
214
252
|
set('otp_other_given', 'other_account_given', "answer_key === 'another_account' ? (cargo.authOtherAccount=String(answer_value || ''),true) : false"),
|
|
215
253
|
set('otp_other_requested', 'wants_other_account', 'other_account_given || !!(cargo.callerPreselected && normalized_otp_code.length !== 6 && (matchesChoice(user_choice,otp_phrases.other) || otp_phrases.other_alone.includes(user_choice.replace(/[.,!?¡¿]/g,\'\').trim())))'),
|
|
216
254
|
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}}' })),
|
|
255
|
+
set('otp_where_early', 'otp_where', expression(otpWhere, 'cargo')), set('otp_where_early_es', 'otp_where_es', expression(otpWhere, 'cargo', 'true')),
|
|
217
256
|
cases('otp_response', {
|
|
218
257
|
'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') },
|
|
219
258
|
'condition: wants_other_account': call('otp_change_account', 'authenticate-user', { cell_number: '', email: '', retrying: true, declined_caller: true }, 'replace'),
|
|
220
259
|
// Email asked for at a code sent by text: authentication is an input step, so the contact question is asked again
|
|
221
260
|
// (without the caller-ID offer) and the caller gives the email; a code sent by email keeps "no me llegó el correo" a resend.
|
|
261
|
+
['condition: email_too && normalized_otp_code.length !== 6 && (' + 'matchesChoice(user_choice,otp_phrases.resend) || matchesChoice(user_choice,otp_phrases.cannot_find) || matchesChoice(user_choice,otp_phrases.another_way) || matchesChoice(user_choice,otp_phrases.email)' + ')']: call('otp_email_too', 'send-otp-email-too'),
|
|
222
262
|
'condition: !!cargo.otp_cell_number && normalized_otp_code.length !== 6 && matchesChoice(user_choice,otp_phrases.email)': call('otp_use_email', 'authenticate-user', { cell_number: '', email: '', retrying: true, declined_caller: true }, 'replace'),
|
|
223
263
|
"condition: isExitRequest(user_choice)": call('otp_exit', '{{otp_cancel}}', undefined, 'reboot'),
|
|
224
264
|
"condition: isAnswer(user_choice,'agent')": call('otp_agent', 'live-agent-requested', undefined, 'reboot'),
|
|
225
265
|
"condition: isAnswer(user_choice,'text')": call('otp_text', 'switch-to-text', undefined, 'reboot'),
|
|
226
266
|
'condition: matchesChoice(user_choice,otp_phrases.resend)': call('otp_resend', 'resend-otp-code', { resend_reason: 'requested' }),
|
|
267
|
+
'condition: matchesChoice(user_choice,otp_phrases.cannot_find)': say('otp_cannot_find', '{{otp_where}}', '{{otp_where_es}}'),
|
|
268
|
+
'condition: matchesChoice(user_choice,otp_phrases.another_way)': say('otp_no_other_way', '{{otp_where}} To reach a person, {{cargo.verb}} AGENT.', '{{otp_where_es}} Para hablar con una persona, {{cargo.verb_es}} AGENTE.'),
|
|
227
269
|
'condition: matchesChoice(user_choice,otp_phrases.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.'),
|
|
228
270
|
'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."),
|
|
229
271
|
'condition: normalized_otp_code.length > 0': effect('otp_format_cap_next', 'true'),
|
|
@@ -243,10 +285,14 @@ const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a
|
|
|
243
285
|
+ ".concat(((typeof $callers !== 'undefined' && $callers && $callers['authenticate-user']) || []).map(function(c){return {key:'usecase:'+c.id,label:'states the reason for the call: '+c.label};}))"
|
|
244
286
|
+ ".concat(((typeof $gateExits !== 'undefined' && $gateExits && $gateExits['authenticate-user']) || []).map(function(a){return {key:'exit:'+a.key,label:a.label,route:'new_request'};}))"),
|
|
245
287
|
when('otp_off_topic', 'no_code_given', { id: 'otp_reply_rank', type: 'DISPATCH', onResponse: 'continue', responseContext: 'Please say the 6-digit verification code I sent you. (Por favor diga el código de verificación de 6 dígitos que le envié.)', responseChoices: 'otp_offers', responseVariable: 'otp_reply', onNoIntent: 'stay' }),
|
|
288
|
+
set('otp_where', 'otp_where', expression(otpWhere, 'cargo')), set('otp_where_es', 'otp_where_es', expression(otpWhere, 'cargo', 'true')),
|
|
246
289
|
set('otp_reply_picked', 'no_code_given', "no_code_given && !(otp_reply && otp_reply.key && String(otp_reply.key).indexOf('usecase:') !== 0)"),
|
|
247
290
|
cases('otp_reply_route', {
|
|
291
|
+
"condition: !no_code_given && email_too && otp_reply && ['email','resend','cannot_find','another_way'].includes(otp_reply.key)": call('otp_ranked_email_too', 'send-otp-email-too'),
|
|
248
292
|
"condition: !no_code_given && otp_reply && otp_reply.key === 'email'": call('otp_ranked_email', 'authenticate-user', { cell_number: '', email: '', retrying: true, declined_caller: true }, 'replace'),
|
|
249
293
|
"condition: !no_code_given && otp_reply && otp_reply.key === 'another_account'": call('otp_ranked_account', 'authenticate-user', { cell_number: '', email: '', retrying: true, declined_caller: true }, 'replace'),
|
|
294
|
+
"condition: !no_code_given && otp_reply && (otp_reply.key === 'where' || otp_reply.key === 'cannot_find')": say('otp_ranked_where', '{{otp_where}}', '{{otp_where_es}}'),
|
|
295
|
+
"condition: !no_code_given && otp_reply && otp_reply.key === 'another_way'": say('otp_ranked_no_other_way', '{{otp_where}} To reach a person, {{cargo.verb}} AGENT.', '{{otp_where_es}} Para hablar con una persona, {{cargo.verb_es}} AGENTE.'),
|
|
250
296
|
"condition: !no_code_given && otp_reply && otp_reply.key === 'resend'": call('otp_ranked_resend', 'resend-otp-code', { resend_reason: 'requested' }),
|
|
251
297
|
"condition: !no_code_given && otp_reply && otp_reply.key === 'no_phone'": otpMenu('otp_ranked_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),
|
|
252
298
|
"condition: !no_code_given && otp_reply && otp_reply.key === '__agent'": call('otp_ranked_agent', 'live-agent-requested', undefined, 'reboot'),
|
|
@@ -261,7 +307,7 @@ const validateCode = worker('validate-otp-code', 'ValidateOtpCode', 'Validate a
|
|
|
261
307
|
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)),
|
|
262
308
|
when('otp_format_retry', 'normalized_otp_code.length !== 6', call('otp_collect_again', 'get-and-validate-otp-code', undefined, 'replace')),
|
|
263
309
|
effect('otp_mismatch_count', "(cargo.otpMismatches=Number(cargo.otpMismatches || 0)+(!cargo.otpVerified && cargo.otpFailure === 'mismatch'?1:0),true)"),
|
|
264
|
-
when('otp_mismatch_cap', "!cargo.otpVerified && cargo.otpFailure === 'mismatch' && cargo.otpMismatches >= 3", effect('otp_withdraw', '(cargo.otpHash=null,true)')),
|
|
310
|
+
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)')),
|
|
265
311
|
cases('otp_verified_result', {
|
|
266
312
|
'condition: cargo.otpVerified === true': effect('otp_verified_record', '(cargo.otpLateCode=String(normalized_otp_code),true)'),
|
|
267
313
|
"condition: cargo.otpFailure === 'expired' || cargo.otpFailure === 'missing'": call('otp_refresh', 'resend-otp-code', { resend_reason: '{{cargo.otpFailure}}' }),
|
|
@@ -349,4 +395,4 @@ const resume = worker('resume-identity-caller', 'ResumeIdentityCaller', 'Resume
|
|
|
349
395
|
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}')),
|
|
350
396
|
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')]
|
|
351
397
|
});
|
|
352
|
-
export const FLOWS = [taskGate, resume, service, rawTransport, contact, declined, splitFailed, rejected, auth, code, validateCode, retry, email];
|
|
398
|
+
export const FLOWS = [taskGate, resume, service, rawTransport, contact, declined, splitFailed, rejected, auth, code, validateCode, emailToo, retry, email];
|
package/dist/forms/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export const FLOWS: any[];
|
|
|
2
2
|
export { FLOWS as IDENTITY_FLOWS } from "./identity-flows.js";
|
|
3
3
|
export { compileFormPrompts } from "./prompt-compiler.js";
|
|
4
4
|
export { explainSession } from "./inspection.js";
|
|
5
|
+
export { continuationBreaks } from "./continuation-compat.js";
|
|
5
6
|
export { inheritFlowDeclaration } from "./declaration-capture.js";
|
|
6
7
|
export { withSafetyRevision, CONTINUATION_SAFETY_REQUIREMENT } from "./continuation-safety.js";
|
|
7
8
|
export { FIELD_PRESETS, formFlow, declinedFlow, textValue } from "./flows.js";
|
package/dist/forms/index.js
CHANGED
|
@@ -13,4 +13,5 @@ export { addressDraft, zipLocation, streetLocation, resolvedAddress } from '../h
|
|
|
13
13
|
export { explainSession } from './inspection.js';
|
|
14
14
|
export { Form, Provider, Performer, Worker, compileExpression } from './authoring.js';
|
|
15
15
|
export { compileFlowDeclarations, serializeFlowDeclaration, captureOriginalDeclaration } from './declarations.js';
|
|
16
|
+
export { continuationBreaks } from './continuation-compat.js';
|
|
16
17
|
export { inheritFlowDeclaration } from './declaration-capture.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../../src/helpers/address.js"],"names":[],"mappings":"AAAA,uFAAuF;AACvF;;;;;EAQC;AACD;;;;;SASC;AACD,gDAEC;AACD
|
|
1
|
+
{"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../../src/helpers/address.js"],"names":[],"mappings":"AAAA,uFAAuF;AACvF;;;;;EAQC;AACD;;;;;SASC;AACD,gDAEC;AACD;;;;;EASC;AACD;;;;;;;;EAIC"}
|
package/dist/helpers/address.js
CHANGED
|
@@ -27,7 +27,10 @@ export function streetLocation(result, said, zip) {
|
|
|
27
27
|
const part = kind => components.find(c => (c.types || []).includes(kind))?.long_name || '';
|
|
28
28
|
const types = row.types || [], locationType = row.location_type || row.geometry?.location_type || '';
|
|
29
29
|
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);
|
|
30
|
-
|
|
30
|
+
// An exact street match names the place its mail carries; it corrects a city read from the ZIP code alone (90805 is
|
|
31
|
+
// Lakewood to the ZIP lookup, while its streets are Long Beach and Lakewood)
|
|
32
|
+
const city = exact ? part('locality') || part('postal_town') || part('sublocality') : '';
|
|
33
|
+
return { street: exact ? part('street_number') + ' ' + part('route') : String(said).trim(), said: String(said).trim(), verified: !!exact, city };
|
|
31
34
|
}
|
|
32
35
|
export function resolvedAddress(values, data) {
|
|
33
36
|
const unit = /^(none|no|ninguno|ninguna|sin|no hay)$/i.test(String(values.unit || '')) ? '' : String(values.unit || '').replace(/^(apt\.?|unit|#)\s*/i, '');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Session-owned action state.
|
|
1
|
+
/** Session-owned action state. */
|
|
2
2
|
import { nextFormInput, formPath, formInputApplies } from './form-state.js';
|
|
3
3
|
export function formSignature(value) {
|
|
4
4
|
return JSON.stringify(value, (_key, v) => v && typeof v === 'object' && !Array.isArray(v)
|