@xema/omni-protocol 0.1.116 → 0.1.118
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/dist/index.d.ts +73 -30
- package/dist/index.js +10 -1
- package/dist/testing.d.ts +15 -1
- package/dist/testing.js +75 -1
- package/dist/validation.d.ts +19 -9
- package/dist/validation.js +126 -88
- package/guide/agent.md +19 -1
- package/guide/phone.md +1 -1
- package/guide/voice.md +115 -109
- package/guide.md +72 -35
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -246,7 +246,8 @@ export interface SecretStore {
|
|
|
246
246
|
* A small store for one login's operational state -- what an adapter holds about open work that
|
|
247
247
|
* its platform cannot hold for it, such as the interaction legs a host reported. Kept by the host for
|
|
248
248
|
* the life of the login, across a reload of the host, and cleared at sign-out. Never for anything
|
|
249
|
-
* sensitive: that is `SecretStore`, whose contract is that a host may clear it aggressively
|
|
249
|
+
* sensitive: that is `SecretStore`, whose contract is that a host may clear it aggressively, and
|
|
250
|
+
* clears it once the session has ended.
|
|
250
251
|
*/
|
|
251
252
|
export interface LoginStore {
|
|
252
253
|
get(key: string): Promise<string | undefined>;
|
|
@@ -318,6 +319,13 @@ export interface UserCapabilities {
|
|
|
318
319
|
* Only `authenticated` and `refreshing` know who the agent is and what they may do, and only
|
|
319
320
|
* `authenticated` has something to expire. A state carrying more than it knows is a state Omni
|
|
320
321
|
* would render as fact.
|
|
322
|
+
*
|
|
323
|
+
* `signed-out` is the agent's own Sign out on this agent computer, and nothing else. `terminated`
|
|
324
|
+
* is a session ended from outside it -- a supervisor, the agent on another screen, the platform
|
|
325
|
+
* itself -- and says who: a person by id, or `provider` where no person was behind it, with the
|
|
326
|
+
* reason as a failure that is never retryable. `expired` is a session whose time ran out or whose
|
|
327
|
+
* refresh is failing. The stored secrets go with every ending: signed out, terminated, and expired
|
|
328
|
+
* unless its failure says `retryable: true`.
|
|
321
329
|
*/
|
|
322
330
|
export type AuthenticationState = {
|
|
323
331
|
status: "signed-out";
|
|
@@ -336,6 +344,13 @@ export type AuthenticationState = {
|
|
|
336
344
|
status: "expired";
|
|
337
345
|
identity?: User;
|
|
338
346
|
failure?: AuthenticationFailure;
|
|
347
|
+
} | {
|
|
348
|
+
status: "terminated";
|
|
349
|
+
by: UserId | "provider";
|
|
350
|
+
identity?: User;
|
|
351
|
+
failure: AuthenticationFailure & {
|
|
352
|
+
retryable: false;
|
|
353
|
+
};
|
|
339
354
|
};
|
|
340
355
|
export interface CredentialField {
|
|
341
356
|
name: string;
|
|
@@ -401,6 +416,11 @@ export interface AuthenticationSession {
|
|
|
401
416
|
complete(request: CompleteAuthenticationRequest): Promise<CompleteAuthenticationResult>;
|
|
402
417
|
/** Cancels an abandoned SSO window or credentials form. */
|
|
403
418
|
cancelAuthentication(flowId: string): Promise<AuthenticationActionResult>;
|
|
419
|
+
/**
|
|
420
|
+
* The agent's own Sign out. Tells the provider where it can, deletes the stored secrets whatever
|
|
421
|
+
* happened, and moves to `signed-out`; answers `failed` when the provider could not be told, so
|
|
422
|
+
* the host can show that a session may remain there. Never called for a `terminated` session.
|
|
423
|
+
*/
|
|
404
424
|
signOut(): Promise<AuthenticationActionResult>;
|
|
405
425
|
close(): Promise<void>;
|
|
406
426
|
}
|
|
@@ -493,40 +513,61 @@ export interface TaskRecordingPolicy {
|
|
|
493
513
|
storageId: string;
|
|
494
514
|
};
|
|
495
515
|
}
|
|
496
|
-
/**
|
|
497
|
-
export type
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
516
|
+
/** Whose side of the call a recording captures: the party's channel, or the agent's own. */
|
|
517
|
+
export type RecordingSide = "party" | "agent";
|
|
518
|
+
export declare const RECORDING_SIDES: readonly ["party", "agent"];
|
|
519
|
+
/**
|
|
520
|
+
* Where a recording stands, as its recorder last stated it. `starting` is asked for and not yet
|
|
521
|
+
* confirmed. A stop or a pause in flight stays `recording` until the recorder confirms it, since
|
|
522
|
+
* telling the agent they are not recorded when they may be is the one thing this must never say.
|
|
523
|
+
*/
|
|
524
|
+
export type RecordingStatus = "starting" | "recording" | "paused" | "not-recording" | "unknown";
|
|
525
|
+
export declare const RECORDING_STATUSES: readonly ["starting", "recording", "paused", "not-recording", "unknown"];
|
|
526
|
+
/** Why a recording is paused: someone asked, or the platform paused it while sensitive details are taken. */
|
|
527
|
+
export type RecordingPausedReason = "requested" | "sensitive-details";
|
|
528
|
+
export declare const RECORDING_PAUSED_REASONS: readonly ["requested", "sensitive-details"];
|
|
529
|
+
/** Why a recording's state is unknown: its recorder cannot be reached, or a command could not be settled either way. */
|
|
530
|
+
export type RecordingUnknownReason = "recorder-unreachable" | "unsettled";
|
|
531
|
+
export declare const RECORDING_UNKNOWN_REASONS: readonly ["recorder-unreachable", "unsettled"];
|
|
532
|
+
/**
|
|
533
|
+
* One recording of one side of the call, published whenever it changes and restated whole with
|
|
534
|
+
* every publication of the task and on every snapshot. Nothing expires: the mark stands as last
|
|
535
|
+
* stated until its recorder says otherwise. `id` is the recorder's, minted when the start is
|
|
536
|
+
* accepted -- before the recorder has answered -- and kept through pause and resume; a later
|
|
537
|
+
* start is a new recording with a new id.
|
|
538
|
+
*/
|
|
539
|
+
export type Recording = {
|
|
540
|
+
id: string;
|
|
541
|
+
follows: RecordingSide;
|
|
507
542
|
} & ({
|
|
508
|
-
status: "
|
|
509
|
-
|
|
543
|
+
status: "starting" | "recording" | "not-recording";
|
|
544
|
+
reason?: never;
|
|
510
545
|
} | {
|
|
511
|
-
status: "
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
|
|
546
|
+
status: "paused";
|
|
547
|
+
reason: RecordingPausedReason;
|
|
548
|
+
} | {
|
|
549
|
+
status: "unknown";
|
|
550
|
+
reason: RecordingUnknownReason;
|
|
551
|
+
});
|
|
552
|
+
/**
|
|
553
|
+
* A start names the side to record; every other action names the recording by its id, and the
|
|
554
|
+
* recorder refuses one that no longer stands where the action needs it.
|
|
555
|
+
*/
|
|
515
556
|
export type RecordingCommand = {
|
|
516
557
|
type: "recording";
|
|
517
558
|
source: RecordingSource;
|
|
518
|
-
/** Compare-and-set against current evidence; never act on a replacement recording. */
|
|
519
|
-
observationId: string;
|
|
520
559
|
} & ({
|
|
521
560
|
action: "start";
|
|
561
|
+
follows: RecordingSide;
|
|
522
562
|
recordingId?: never;
|
|
523
563
|
} | {
|
|
524
564
|
action: "pause" | "resume" | "stop" | "cancel";
|
|
525
565
|
recordingId: string;
|
|
566
|
+
follows?: never;
|
|
526
567
|
});
|
|
527
568
|
export interface HostRecordingReport {
|
|
528
569
|
assignmentId: AssignmentId;
|
|
529
|
-
|
|
570
|
+
recordings: Recording[];
|
|
530
571
|
}
|
|
531
572
|
/** Host declaration in ConnectContext, not the provider-owned Manifest. */
|
|
532
573
|
export interface HostRecording {
|
|
@@ -814,7 +855,7 @@ export type TaskAttribute = TaskAttributeBase & ({
|
|
|
814
855
|
type: "timestamp";
|
|
815
856
|
at: IsoTimestamp;
|
|
816
857
|
});
|
|
817
|
-
export type HistoryStep = "queued" | "offered" | "answered" | "held" | "muted" | "transferred" | "conferenced" | "unanswered";
|
|
858
|
+
export type HistoryStep = "queued" | "offered" | "answered" | "held" | "muted" | "transferred" | "conferenced" | "unanswered" | "callback-requested";
|
|
818
859
|
/**
|
|
819
860
|
* The call record: the steps that brought the task here, one entry per occurrence and oldest
|
|
820
861
|
* first, and what they add up to before this agent -- stated by the provider from its own record,
|
|
@@ -849,8 +890,9 @@ export interface TaskHistoryStep {
|
|
|
849
890
|
*/
|
|
850
891
|
seconds?: DurationSeconds;
|
|
851
892
|
/**
|
|
852
|
-
* Who took part. Absent on `queued`, where nobody does; absent on
|
|
853
|
-
*
|
|
893
|
+
* Who took part. Absent on `queued`, where nobody does; absent on `callback-requested`, where the
|
|
894
|
+
* caller chose it in the queue, and present there when an agent arranged it; absent on any other
|
|
895
|
+
* step means the provider could not attribute it, which is a different claim and a legitimate one.
|
|
854
896
|
*/
|
|
855
897
|
by?: UserId;
|
|
856
898
|
/** On a `muted` step, and only there: whose the silence was, as the host reported it. */
|
|
@@ -1044,15 +1086,13 @@ export type Task<C extends Channel = Channel> = {
|
|
|
1044
1086
|
attributes?: TaskAttribute[];
|
|
1045
1087
|
history?: TaskHistory;
|
|
1046
1088
|
} & TaskCompletion & (C extends "voice" ? {
|
|
1047
|
-
|
|
1048
|
-
provider?: RecordingState;
|
|
1049
|
-
};
|
|
1089
|
+
recordings?: Recording[];
|
|
1050
1090
|
onCall?: OnCall[];
|
|
1051
1091
|
leadAssist?: TaskLeadAssist;
|
|
1052
1092
|
takenOver?: TaskTakenOver;
|
|
1053
1093
|
audio?: TaskAudioState;
|
|
1054
1094
|
} : {
|
|
1055
|
-
|
|
1095
|
+
recordings?: never;
|
|
1056
1096
|
onCall?: never;
|
|
1057
1097
|
leadAssist?: never;
|
|
1058
1098
|
takenOver?: never;
|
|
@@ -1858,7 +1898,7 @@ export interface ProviderEventEnvelope<C extends Channel = Channel> {
|
|
|
1858
1898
|
occurredAt: IsoTimestamp;
|
|
1859
1899
|
event: ProviderEvent<C>;
|
|
1860
1900
|
}
|
|
1861
|
-
export declare const OMNI_FAILURE_CODES: readonly ["omni.not-authenticated", "omni.capability-not-enabled", "omni.assignment-not-found", "omni.destination-not-permitted", "omni.phone-not-permitted", "omni.rate-limited", "omni.unavailable", "omni.break-already-committed", "omni.recording-unsettled", "omni.break-forced-by-provider"];
|
|
1901
|
+
export declare const OMNI_FAILURE_CODES: readonly ["omni.not-authenticated", "omni.capability-not-enabled", "omni.assignment-not-found", "omni.destination-not-permitted", "omni.phone-not-permitted", "omni.rate-limited", "omni.unavailable", "omni.break-already-committed", "omni.recording-unsettled", "omni.on-hold", "omni.break-forced-by-provider"];
|
|
1862
1902
|
export type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
|
|
1863
1903
|
/**
|
|
1864
1904
|
* What the host refused, told to the adapter that published it: a snapshot the host would not
|
|
@@ -2002,7 +2042,10 @@ export declare const assignmentKey: (providerId: string, assignmentId: Assignmen
|
|
|
2002
2042
|
* same provider; anything wider goes through this.
|
|
2003
2043
|
*/
|
|
2004
2044
|
export declare const userKey: (providerId: string, userId: UserId) => string;
|
|
2005
|
-
/**
|
|
2045
|
+
/**
|
|
2046
|
+
* Every history step somebody from the provider takes part in. `queued` is the one nobody does, and
|
|
2047
|
+
* `callback-requested` is the caller's choice unless an agent arranged it, so neither is here.
|
|
2048
|
+
*/
|
|
2006
2049
|
export declare const HISTORY_STEPS_WITH_A_PERSON: readonly ["offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered"];
|
|
2007
2050
|
/** The steps a dial writes, and so the only ones that carry a `dialId` and a `destinationId`. */
|
|
2008
2051
|
export declare const HISTORY_STEPS_THAT_DIAL: readonly ["transferred", "conferenced", "unanswered"];
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,10 @@ export const LISTENING_MODES = ["listen", "coach", "join-call"];
|
|
|
32
32
|
export const MUTED_BY = ["host", "station"];
|
|
33
33
|
export const HOST_MUTES = ["stream", "station"];
|
|
34
34
|
export const RECORDING_ACTIONS = ["start", "pause", "resume", "stop", "cancel"];
|
|
35
|
+
export const RECORDING_SIDES = ["party", "agent"];
|
|
36
|
+
export const RECORDING_STATUSES = ["starting", "recording", "paused", "not-recording", "unknown"];
|
|
37
|
+
export const RECORDING_PAUSED_REASONS = ["requested", "sensitive-details"];
|
|
38
|
+
export const RECORDING_UNKNOWN_REASONS = ["recorder-unreachable", "unsettled"];
|
|
35
39
|
// ---------------------------------------------------------------------------
|
|
36
40
|
// Task workspace.
|
|
37
41
|
// ---------------------------------------------------------------------------
|
|
@@ -107,6 +111,8 @@ export const OMNI_FAILURE_CODES = [
|
|
|
107
111
|
"omni.break-already-committed",
|
|
108
112
|
/** A recording command the provider could not settle either way: no start, stop, pause or resume it can vouch for. */
|
|
109
113
|
"omni.recording-unsettled",
|
|
114
|
+
/** Refused because the caller is on hold: the host's Mute, or end-call, waits for resume. */
|
|
115
|
+
"omni.on-hold",
|
|
110
116
|
/** A lead asked to lift a break the platform itself imposed; the platform lifts its own. */
|
|
111
117
|
"omni.break-forced-by-provider",
|
|
112
118
|
];
|
|
@@ -158,7 +164,10 @@ export const assignmentKey = (providerId, assignmentId) => `${encodeURIComponent
|
|
|
158
164
|
* same provider; anything wider goes through this.
|
|
159
165
|
*/
|
|
160
166
|
export const userKey = (providerId, userId) => `${encodeURIComponent(providerId)}:${encodeURIComponent(userId)}`;
|
|
161
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* Every history step somebody from the provider takes part in. `queued` is the one nobody does, and
|
|
169
|
+
* `callback-requested` is the caller's choice unless an agent arranged it, so neither is here.
|
|
170
|
+
*/
|
|
162
171
|
export const HISTORY_STEPS_WITH_A_PERSON = [
|
|
163
172
|
"offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered",
|
|
164
173
|
];
|
package/dist/testing.d.ts
CHANGED
|
@@ -87,7 +87,7 @@ export declare function assertAuthenticationRestoreAndExpiry(states: readonly Au
|
|
|
87
87
|
* Capabilities are current, not fixed. A provider that withdraws one republishes `authenticated`
|
|
88
88
|
* with the new set, and the next snapshot agrees with it. `states` is what the authentication
|
|
89
89
|
* session published, first to last: beginning and ending `authenticated` for the same identity,
|
|
90
|
-
* passing only through usable states (`expired` or `signed-out` ends the login instead), with at
|
|
90
|
+
* passing only through usable states (`expired`, `terminated` or `signed-out` ends the login instead), with at
|
|
91
91
|
* least one capability gone by the end. `snapshot` is the first snapshot published after the last
|
|
92
92
|
* state, validated against that login -- so a team member list still published to a login that no longer
|
|
93
93
|
* leads, or requests to one that may no longer join, is the failure.
|
|
@@ -160,8 +160,13 @@ export declare class TaskStream {
|
|
|
160
160
|
*/
|
|
161
161
|
private holdCountdowns;
|
|
162
162
|
private readonly dials;
|
|
163
|
+
private readonly placedHere;
|
|
163
164
|
/** The host placed a dial: its outcome, whenever it arrives, is one the stream expects. */
|
|
164
165
|
dialled(dialId: string): void;
|
|
166
|
+
/** A dial the test placed itself and the provider answered dialling: it is owed exactly one outcome. */
|
|
167
|
+
placedByTest(dialId: string): void;
|
|
168
|
+
/** The dials the test placed that never got their outcome. */
|
|
169
|
+
dialsWithoutOutcome(): string[];
|
|
165
170
|
private noteDials;
|
|
166
171
|
/**
|
|
167
172
|
* Why an event names no open task: the assignment ended, so this is the late event that must not
|
|
@@ -250,6 +255,15 @@ export declare function stillHost(report?: HostReport, guarantees?: HostGuarante
|
|
|
250
255
|
mute?: never;
|
|
251
256
|
recording?: never;
|
|
252
257
|
};
|
|
258
|
+
/**
|
|
259
|
+
* One call's history as it reaches a second task: an earlier agent's task and a later one of the same
|
|
260
|
+
* call -- a hand-over, a take-over, a callback. The later task restates every earlier step, in order,
|
|
261
|
+
* as the earlier task had it: the same step, instant, person, dial and destination, and the same
|
|
262
|
+
* duration wherever the earlier task had already stated one (a leg still open there may be closed
|
|
263
|
+
* here). Throws on the first step that disagrees, and when either task carries no history, since
|
|
264
|
+
* two absent records agree about nothing.
|
|
265
|
+
*/
|
|
266
|
+
export declare function assertHistoryAgreesAcrossTasks(earlier: Task, later: Task): void;
|
|
253
267
|
/** One provider as the host sees it when freezing the providers a break attempt asks. */
|
|
254
268
|
export interface BreakCandidate {
|
|
255
269
|
id: string;
|
package/dist/testing.js
CHANGED
|
@@ -309,6 +309,17 @@ export async function testAdapter(adapter, context, options = {}) {
|
|
|
309
309
|
violations.push(...own);
|
|
310
310
|
if (own.length > 0)
|
|
311
311
|
return;
|
|
312
|
+
// A session that has ended takes its secrets with it: the adapter deletes them before it
|
|
313
|
+
// publishes the ending, so a reload has nothing to restore. Only an expiry whose failure is
|
|
314
|
+
// retryable keeps them, for the retry.
|
|
315
|
+
if (sessionEnded(state)) {
|
|
316
|
+
ruleTested("authentication.secrets.retained");
|
|
317
|
+
if (storedSecrets.size > 0) {
|
|
318
|
+
violations.push({ rule: "authentication.secrets.retained", path: "authentication.status",
|
|
319
|
+
message: `the session is ${state.status} and the adapter still holds ${storedSecrets.size} stored secret(s): ` +
|
|
320
|
+
"an ended session's secrets are deleted before the ending is published, or a reload restores it" });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
312
323
|
if (state.status === "refreshing") {
|
|
313
324
|
violations.push(...refreshingCarriesOver(current(), state, "authentication"));
|
|
314
325
|
}
|
|
@@ -678,6 +689,12 @@ export async function testAdapter(adapter, context, options = {}) {
|
|
|
678
689
|
violations.push({ rule: "stream.taskOffered.unended", path: "event.task-ended",
|
|
679
690
|
message: `${id} was offered and never ended: every offer is owed a task-ended, whatever became of the call` });
|
|
680
691
|
}
|
|
692
|
+
// Every dial the test placed, and the provider answered dialling, is owed exactly one outcome.
|
|
693
|
+
ruleTested("stream.dialOutcome.missing");
|
|
694
|
+
for (const id of stream.dialsWithoutOutcome()) {
|
|
695
|
+
violations.push({ rule: "stream.dialOutcome.missing", path: "event.dial-outcome",
|
|
696
|
+
message: `${id} was answered dialling and never got its dial-outcome: every dial ends in exactly one, however it ended` });
|
|
697
|
+
}
|
|
681
698
|
}
|
|
682
699
|
// Capacity supersedes rather than accumulates, so a decrease is as ordinary as an increase: the
|
|
683
700
|
// host raises it, lowers it, and takes it away, and the provider takes each as the ceiling it is.
|
|
@@ -904,6 +921,15 @@ export function assertAuthenticationRestoreAndExpiry(states) {
|
|
|
904
921
|
}
|
|
905
922
|
}
|
|
906
923
|
/** The tasks an envelope carries: the one a task event names, or a snapshot event's list. */
|
|
924
|
+
/** The failure code of a failed answer, as the provider stated it. */
|
|
925
|
+
function failureCode(answer) {
|
|
926
|
+
return isRecord(answer.failure) ? answer.failure.code : undefined;
|
|
927
|
+
}
|
|
928
|
+
/** Whether a published state ends the session, so its stored secrets must already be gone. */
|
|
929
|
+
function sessionEnded(state) {
|
|
930
|
+
return state.status === "signed-out" || state.status === "terminated"
|
|
931
|
+
|| (state.status === "expired" && state.failure?.retryable !== true);
|
|
932
|
+
}
|
|
907
933
|
/** Whether a store key names the task: the id as a delimited token, never as a run of characters inside another id. */
|
|
908
934
|
function namesTask(key, taskId) {
|
|
909
935
|
return key.split(/[^A-Za-z0-9_-]+/).includes(taskId);
|
|
@@ -940,7 +966,7 @@ function notYetReadTasks(tasks, path) {
|
|
|
940
966
|
* Capabilities are current, not fixed. A provider that withdraws one republishes `authenticated`
|
|
941
967
|
* with the new set, and the next snapshot agrees with it. `states` is what the authentication
|
|
942
968
|
* session published, first to last: beginning and ending `authenticated` for the same identity,
|
|
943
|
-
* passing only through usable states (`expired` or `signed-out` ends the login instead), with at
|
|
969
|
+
* passing only through usable states (`expired`, `terminated` or `signed-out` ends the login instead), with at
|
|
944
970
|
* least one capability gone by the end. `snapshot` is the first snapshot published after the last
|
|
945
971
|
* state, validated against that login -- so a team member list still published to a login that no longer
|
|
946
972
|
* leads, or requests to one that may no longer join, is the failure.
|
|
@@ -1173,11 +1199,19 @@ export class TaskStream {
|
|
|
1173
1199
|
// task carried on `onCall` or in its record -- which is how a dial made before a take-over is known
|
|
1174
1200
|
// to whoever holds the task now. `answered` or `ended` once its outcome arrived, since it comes once.
|
|
1175
1201
|
dials = new Map();
|
|
1202
|
+
placedHere = new Set();
|
|
1176
1203
|
/** The host placed a dial: its outcome, whenever it arrives, is one the stream expects. */
|
|
1177
1204
|
dialled(dialId) {
|
|
1178
1205
|
if (!this.dials.has(dialId))
|
|
1179
1206
|
this.dials.set(dialId, "placed");
|
|
1180
1207
|
}
|
|
1208
|
+
/** A dial the test placed itself and the provider answered dialling: it is owed exactly one outcome. */
|
|
1209
|
+
placedByTest(dialId) {
|
|
1210
|
+
this.dialled(dialId);
|
|
1211
|
+
this.placedHere.add(dialId);
|
|
1212
|
+
}
|
|
1213
|
+
/** The dials the test placed that never got their outcome. */
|
|
1214
|
+
dialsWithoutOutcome() { return [...this.placedHere].filter(id => this.dials.get(id) === "placed"); }
|
|
1181
1215
|
noteDials(task) {
|
|
1182
1216
|
if (!isRecord(task))
|
|
1183
1217
|
return;
|
|
@@ -1840,6 +1874,7 @@ async function testOneCall(call) {
|
|
|
1840
1874
|
call.stream.dialled(dialId);
|
|
1841
1875
|
if (await send({ type: "dial", dialId }, dialId) === undefined)
|
|
1842
1876
|
return found;
|
|
1877
|
+
call.stream.placedByTest(dialId);
|
|
1843
1878
|
}
|
|
1844
1879
|
if (latestTask().phase === "preview" && await updated(t => t.phase === "in-progress" || t.phase === "completing", pressCall ? "the task leaving preview after Call" : "the task leaving preview after the provider's dial") === undefined)
|
|
1845
1880
|
return found;
|
|
@@ -1976,6 +2011,9 @@ async function testOneCall(call) {
|
|
|
1976
2011
|
if (isRecord(answer) && answer.status !== "failed") {
|
|
1977
2012
|
refuse("test.recordStep.held", "test.recordStep", "the provider recorded the host's mute begun with the caller on hold: the Mute waits for resume, and the adapter is the second gate");
|
|
1978
2013
|
}
|
|
2014
|
+
else if (isRecord(answer) && failureCode(answer) !== "omni.on-hold") {
|
|
2015
|
+
refuse("test.recordStep.held", "test.recordStep", `the provider refused the mute begun on hold as ${String(failureCode(answer))}: a refusal because the caller is on hold answers omni.on-hold, so the agent application can say resume first`);
|
|
2016
|
+
}
|
|
1979
2017
|
};
|
|
1980
2018
|
// Ending my part from hold would leave the caller in the hold with nobody coming back to them: a
|
|
1981
2019
|
// host holds it back (command.endCall.held), and an adapter that receives it anyway must refuse
|
|
@@ -1995,6 +2033,9 @@ async function testOneCall(call) {
|
|
|
1995
2033
|
// Applied, the task leaves paused; the run reads where the adapter put it rather than resuming a call it ended.
|
|
1996
2034
|
await updated(t => t.phase !== "paused", "the task after an end-call applied from hold");
|
|
1997
2035
|
}
|
|
2036
|
+
else if (isRecord(answer) && failureCode(answer) !== "omni.on-hold") {
|
|
2037
|
+
refuse("test.command.held", "test.command.end-call", `the provider refused end-call on hold as ${String(failureCode(answer))}: a refusal because the caller is on hold answers omni.on-hold, so the agent application can say resume first`);
|
|
2038
|
+
}
|
|
1998
2039
|
};
|
|
1999
2040
|
// 4. Hold and resume, where offered.
|
|
2000
2041
|
if (latestTask().phase === "in-progress" && offers("hold")) {
|
|
@@ -2327,6 +2368,39 @@ export function memoryStore() {
|
|
|
2327
2368
|
export function stillHost(report = { online: true }, guarantees = {}, mute) {
|
|
2328
2369
|
return { guarantees, ...(mute === undefined ? {} : { mute }), report: () => report, subscribe: () => () => undefined };
|
|
2329
2370
|
}
|
|
2371
|
+
/**
|
|
2372
|
+
* One call's history as it reaches a second task: an earlier agent's task and a later one of the same
|
|
2373
|
+
* call -- a hand-over, a take-over, a callback. The later task restates every earlier step, in order,
|
|
2374
|
+
* as the earlier task had it: the same step, instant, person, dial and destination, and the same
|
|
2375
|
+
* duration wherever the earlier task had already stated one (a leg still open there may be closed
|
|
2376
|
+
* here). Throws on the first step that disagrees, and when either task carries no history, since
|
|
2377
|
+
* two absent records agree about nothing.
|
|
2378
|
+
*/
|
|
2379
|
+
export function assertHistoryAgreesAcrossTasks(earlier, later) {
|
|
2380
|
+
const steps = (task, which) => {
|
|
2381
|
+
const history = task.history;
|
|
2382
|
+
if (history === undefined || !Array.isArray(history.steps)) {
|
|
2383
|
+
throw new Error(`The ${which} task ${task.assignmentId} carries no history to compare`);
|
|
2384
|
+
}
|
|
2385
|
+
return history.steps;
|
|
2386
|
+
};
|
|
2387
|
+
const before = steps(earlier, "earlier");
|
|
2388
|
+
const after = steps(later, "later");
|
|
2389
|
+
if (after.length < before.length) {
|
|
2390
|
+
throw new Error(`${later.assignmentId} restates ${after.length} step(s) of the call and ${earlier.assignmentId} already had ${before.length}: a later task carries every earlier step`);
|
|
2391
|
+
}
|
|
2392
|
+
before.forEach((step, index) => {
|
|
2393
|
+
const restated = after[index];
|
|
2394
|
+
for (const field of ["step", "at", "by", "dialId", "destinationId", "mutedBy"]) {
|
|
2395
|
+
if (step[field] !== restated[field]) {
|
|
2396
|
+
throw new Error(`steps[${index}].${field} is ${String(step[field])} on ${earlier.assignmentId} and ${String(restated[field])} on ${later.assignmentId}: one call has one history`);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
if (step.seconds !== undefined && restated.seconds !== step.seconds) {
|
|
2400
|
+
throw new Error(`steps[${index}].seconds is ${step.seconds} on ${earlier.assignmentId} and ${String(restated.seconds)} on ${later.assignmentId}: a stated duration does not change`);
|
|
2401
|
+
}
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2330
2404
|
const usableLogin = (status) => status === "authenticated" || status === "refreshing";
|
|
2331
2405
|
/**
|
|
2332
2406
|
* A break one provider forces stops the agent everywhere: the host states a capacity of zero on
|
package/dist/validation.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Channel, type Phone, type ProtocolViolation, type UserCapabilities, type UserId } from "./index.js";
|
|
2
2
|
export type { ProtocolViolation } from "./index.js";
|
|
3
3
|
export declare class ProtocolConformanceError extends Error {
|
|
4
4
|
readonly violations: readonly ProtocolViolation[];
|
|
@@ -198,24 +198,34 @@ export declare function validateAuthenticationFailure(failure: unknown, path?: s
|
|
|
198
198
|
*/
|
|
199
199
|
export declare function validateAuthenticationResult(result: unknown, method: "start" | "complete", path?: string, context?: LoginValidationContext): ProtocolViolation[];
|
|
200
200
|
export declare function validateAuthenticationState(state: unknown, path?: string, context?: LoginValidationContext): ProtocolViolation[];
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
/**
|
|
202
|
+
* One recorder's recordings for one task, as published: each with its id, the side it follows,
|
|
203
|
+
* its status, and a reason where it is paused or unknown; ids unique, and at most one recording
|
|
204
|
+
* live on each side at once.
|
|
205
|
+
*/
|
|
206
|
+
export declare function validateRecordings(value: unknown, path?: string): ProtocolViolation[];
|
|
204
207
|
export declare function validateRecordingPolicy(value: unknown, path?: string): ProtocolViolation[];
|
|
205
208
|
export declare function validateHostRecording(value: unknown, softphone: boolean, path?: string): ProtocolViolation[];
|
|
206
209
|
export declare function validateHostRecordings(value: unknown, path?: string): ProtocolViolation[];
|
|
207
210
|
export declare function validateRecordingCommandShape(value: unknown, source: "provider" | "host", path?: string): ProtocolViolation[];
|
|
208
|
-
/**
|
|
209
|
-
|
|
210
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Whether the action is permitted on this recorder, and whether the recordings as last published
|
|
213
|
+
* let it act: a start on a side with nothing capturing, every other action on the recording it
|
|
214
|
+
* names, standing where the action needs it. Dispatch MUST also call validateRecordingRequest.
|
|
215
|
+
*/
|
|
216
|
+
export declare function validateRecordingCommandState(command: unknown, policy: unknown, recordings: unknown, path?: string): ProtocolViolation[];
|
|
217
|
+
/** Validate immediately before dispatch, against the current task and, for the host's recorder, its declaration and report. */
|
|
211
218
|
export declare function validateRecordingRequest(request: unknown, task: unknown, context: {
|
|
212
219
|
source: "provider" | "host";
|
|
213
|
-
now: number | undefined;
|
|
214
220
|
host?: unknown;
|
|
215
221
|
hostReport?: unknown;
|
|
216
222
|
softphone?: boolean;
|
|
217
223
|
/** The same provider/task context used to validate the published task. */
|
|
218
224
|
taskContext?: Omit<TaskValidationContext, "channel">;
|
|
219
225
|
}, path?: string): ProtocolViolation[];
|
|
220
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* A recording command's answer against the recordings published before and after it: applied
|
|
228
|
+
* means the recorder confirmed what was asked -- a new recording on the side for a start, the
|
|
229
|
+
* same recording in its new status for anything else -- and failed means nothing changed there.
|
|
230
|
+
*/
|
|
221
231
|
export declare function validateRecordingOutcome(command: unknown, before: unknown, after: unknown, result: unknown, path?: string): ProtocolViolation[];
|