@xema/omni-protocol 0.1.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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/design.d.ts +49 -0
- package/dist/design.js +27 -0
- package/dist/index.d.ts +894 -0
- package/dist/index.js +185 -0
- package/dist/testing.d.ts +62 -0
- package/dist/testing.js +294 -0
- package/dist/validation.d.ts +21 -0
- package/dist/validation.js +940 -0
- package/guide.md +2935 -0
- package/package.json +52 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
/** The protocol version implemented by this package. */
|
|
2
|
+
export declare const OMNI_PROTOCOL_VERSION: 1;
|
|
3
|
+
/** Every version this package can interoperate with. */
|
|
4
|
+
export declare const OMNI_SUPPORTED_PROTOCOL_VERSIONS: readonly number[];
|
|
5
|
+
/**
|
|
6
|
+
* Highest version supported by both the adapter and the host, or `undefined` when they cannot
|
|
7
|
+
* interoperate. Omni must refuse to connect on `undefined` rather than attempting partial
|
|
8
|
+
* compatibility.
|
|
9
|
+
*/
|
|
10
|
+
export declare function negotiateProtocolVersion(adapterVersions: readonly number[], hostVersions?: readonly number[]): number | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* An RFC-3339 timestamp with `Z` or an explicit numeric offset.
|
|
13
|
+
*
|
|
14
|
+
* Timezone-less values are invalid: the same string read on two hosts would be two different
|
|
15
|
+
* instants. A JavaScript `Date` never crosses this boundary.
|
|
16
|
+
*/
|
|
17
|
+
export type IsoTimestamp = string;
|
|
18
|
+
/**
|
|
19
|
+
* A non-empty, opaque, stable identifier for a person, issued by the provider.
|
|
20
|
+
*
|
|
21
|
+
* It names agents and managers alike; the role comes from where the value appears, not from its
|
|
22
|
+
* type. Compare it exactly and only within one provider -- there is no Omni-wide user identity,
|
|
23
|
+
* so two providers will eventually issue the same string for different people. Scope it with the
|
|
24
|
+
* provider id through `userKey()` before storing or comparing.
|
|
25
|
+
*/
|
|
26
|
+
export type UserId = string;
|
|
27
|
+
/** A non-empty, opaque task identifier unique within one provider. Scope it with `taskKey()`. */
|
|
28
|
+
export type TaskId = string;
|
|
29
|
+
/** A non-negative whole number of seconds. */
|
|
30
|
+
export type DurationSeconds = number;
|
|
31
|
+
/** New channels require a later protocol version. */
|
|
32
|
+
export type Channel = "voice" | "chat" | "email";
|
|
33
|
+
export interface User {
|
|
34
|
+
id: UserId;
|
|
35
|
+
displayName: string;
|
|
36
|
+
}
|
|
37
|
+
/** Key/value detail on a `Contact` or a `ScheduledActivity`. A task's attributes are typed. */
|
|
38
|
+
export interface Attribute {
|
|
39
|
+
key: string;
|
|
40
|
+
value: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ProtocolFailure {
|
|
43
|
+
code: string;
|
|
44
|
+
message: string;
|
|
45
|
+
retryable: boolean;
|
|
46
|
+
retryAfterMs?: number;
|
|
47
|
+
}
|
|
48
|
+
/** One broken rule. Validators return every violation they find rather than throwing. */
|
|
49
|
+
export interface ProtocolViolation {
|
|
50
|
+
/** Stable machine-readable rule id, such as `task.browser.url.scheme`. */
|
|
51
|
+
rule: string;
|
|
52
|
+
/** Dotted path to the offending value, such as `snapshot.tasks[0].browsers[1].url`. */
|
|
53
|
+
path: string;
|
|
54
|
+
message: string;
|
|
55
|
+
}
|
|
56
|
+
export type AuthenticationMethod = "browser-sso" | "credentials";
|
|
57
|
+
export interface BrowserAccessPolicy {
|
|
58
|
+
/** The decision when no list entry matches. */
|
|
59
|
+
mode: "allow-all" | "block-all";
|
|
60
|
+
allowList?: string[];
|
|
61
|
+
blockList?: string[];
|
|
62
|
+
}
|
|
63
|
+
export interface PersonalBrowserCapability {
|
|
64
|
+
access: BrowserAccessPolicy;
|
|
65
|
+
accessPolicyScope?: "initial-url" | "all-navigation";
|
|
66
|
+
}
|
|
67
|
+
export type DialDestinationPolicy = "contacts-only" | "any-number";
|
|
68
|
+
export interface DialCapability {
|
|
69
|
+
destinationPolicy: DialDestinationPolicy;
|
|
70
|
+
}
|
|
71
|
+
/** Every idle capability a provider may declare. Only voice may `dial`; the channel arm says so. */
|
|
72
|
+
export declare const IDLE_CAPABILITIES: readonly ["dial", "personalBrowser", "calendar", "contacts"];
|
|
73
|
+
export type IdleCapability = (typeof IDLE_CAPABILITIES)[number];
|
|
74
|
+
/** What Omni calls each idle capability. */
|
|
75
|
+
export declare const IDLE_CAPABILITY_UI: {
|
|
76
|
+
readonly dial: "Dialpad";
|
|
77
|
+
readonly personalBrowser: "Browser";
|
|
78
|
+
readonly calendar: "Calendar";
|
|
79
|
+
readonly contacts: "Contacts";
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Actions Omni may offer while the agent has no active task.
|
|
83
|
+
*
|
|
84
|
+
* The channel arm is what makes `dial` a compile error on chat and email rather than a runtime
|
|
85
|
+
* one. Presence is the declaration: `calendar: true` says offer it, and omitting it says do not.
|
|
86
|
+
*/
|
|
87
|
+
export type IdleCapabilities<C extends Channel = Channel> = {
|
|
88
|
+
personalBrowser?: PersonalBrowserCapability;
|
|
89
|
+
calendar?: true;
|
|
90
|
+
contacts?: true;
|
|
91
|
+
} & (C extends "voice" ? {
|
|
92
|
+
dial?: DialCapability;
|
|
93
|
+
} : {
|
|
94
|
+
dial?: never;
|
|
95
|
+
});
|
|
96
|
+
export type TaskPhaseLabels = Readonly<Partial<Record<TaskPhase, string>>>;
|
|
97
|
+
export interface TaskTypePresentation {
|
|
98
|
+
singular: string;
|
|
99
|
+
plural: string;
|
|
100
|
+
referenceLabel?: string;
|
|
101
|
+
}
|
|
102
|
+
export interface Manifest<C extends Channel = Channel> {
|
|
103
|
+
/** Stable, installation-wide unique, and unchanged between launches. */
|
|
104
|
+
id: string;
|
|
105
|
+
displayName: string;
|
|
106
|
+
channel: C;
|
|
107
|
+
/** Every version this adapter can speak, not only the one it was compiled against. */
|
|
108
|
+
supportedProtocolVersions: number[];
|
|
109
|
+
authenticationMethods: AuthenticationMethod[];
|
|
110
|
+
idleCapabilities?: IdleCapabilities<C>;
|
|
111
|
+
phaseLabels?: TaskPhaseLabels;
|
|
112
|
+
/** Keyed by `taskType`. An entry replaces the channel default outright rather than merging. */
|
|
113
|
+
taskTypePresentation?: Record<string, TaskTypePresentation>;
|
|
114
|
+
}
|
|
115
|
+
export interface SecretStore {
|
|
116
|
+
get(key: string): Promise<string | undefined>;
|
|
117
|
+
set(key: string, value: string): Promise<void>;
|
|
118
|
+
delete(key: string): Promise<void>;
|
|
119
|
+
}
|
|
120
|
+
export interface AuthenticationContext {
|
|
121
|
+
protocolVersion: number;
|
|
122
|
+
/** Omni's identity for this login. The same value arrives later as `ConnectContext.sessionId`. */
|
|
123
|
+
sessionId: string;
|
|
124
|
+
/** Scoped to this provider's manifest id. */
|
|
125
|
+
secrets: SecretStore;
|
|
126
|
+
signal?: AbortSignal;
|
|
127
|
+
/** Never include credentials, tokens, or contact data. */
|
|
128
|
+
log?: (entry: unknown) => void;
|
|
129
|
+
}
|
|
130
|
+
export interface AuthenticationFailure {
|
|
131
|
+
code: string;
|
|
132
|
+
message: string;
|
|
133
|
+
retryable: boolean;
|
|
134
|
+
retryAfterMs?: number;
|
|
135
|
+
/** Names a declared credentials field when the failure belongs to one. */
|
|
136
|
+
field?: string;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Only `authenticated` and `refreshing` know who the agent is, and only `authenticated` has
|
|
140
|
+
* something to expire. A state carrying more than it knows is a state Omni would render as fact.
|
|
141
|
+
*/
|
|
142
|
+
export type AuthenticationState = {
|
|
143
|
+
status: "signed-out";
|
|
144
|
+
} | {
|
|
145
|
+
status: "authenticating";
|
|
146
|
+
} | {
|
|
147
|
+
status: "authenticated";
|
|
148
|
+
identity: User;
|
|
149
|
+
expiresAt?: IsoTimestamp;
|
|
150
|
+
} | {
|
|
151
|
+
status: "refreshing";
|
|
152
|
+
identity: User;
|
|
153
|
+
} | {
|
|
154
|
+
status: "expired";
|
|
155
|
+
identity?: User;
|
|
156
|
+
failure?: AuthenticationFailure;
|
|
157
|
+
};
|
|
158
|
+
export interface CredentialField {
|
|
159
|
+
name: string;
|
|
160
|
+
label: string;
|
|
161
|
+
type: "text" | "password";
|
|
162
|
+
required?: boolean;
|
|
163
|
+
autocomplete?: string;
|
|
164
|
+
}
|
|
165
|
+
export type AuthenticationChallenge = {
|
|
166
|
+
flowId: string;
|
|
167
|
+
method: "browser-sso";
|
|
168
|
+
authorizationUrl: string;
|
|
169
|
+
browser: "system" | "omni";
|
|
170
|
+
} | {
|
|
171
|
+
flowId: string;
|
|
172
|
+
method: "credentials";
|
|
173
|
+
fields: CredentialField[];
|
|
174
|
+
};
|
|
175
|
+
export type StartAuthenticationRequest = {
|
|
176
|
+
requestId: string;
|
|
177
|
+
method: "browser-sso";
|
|
178
|
+
callbackUrl: string;
|
|
179
|
+
} | {
|
|
180
|
+
requestId: string;
|
|
181
|
+
method: "credentials";
|
|
182
|
+
};
|
|
183
|
+
export type StartAuthenticationResult = {
|
|
184
|
+
status: "interaction-required";
|
|
185
|
+
challenge: AuthenticationChallenge;
|
|
186
|
+
} | {
|
|
187
|
+
status: "rejected";
|
|
188
|
+
failure: AuthenticationFailure;
|
|
189
|
+
};
|
|
190
|
+
export type CompleteAuthenticationRequest = {
|
|
191
|
+
flowId: string;
|
|
192
|
+
method: "browser-sso";
|
|
193
|
+
callbackUrl: string;
|
|
194
|
+
} | {
|
|
195
|
+
flowId: string;
|
|
196
|
+
method: "credentials";
|
|
197
|
+
values: Readonly<Record<string, string>>;
|
|
198
|
+
};
|
|
199
|
+
export type CompleteAuthenticationResult = {
|
|
200
|
+
status: "authenticated";
|
|
201
|
+
identity: User;
|
|
202
|
+
expiresAt?: IsoTimestamp;
|
|
203
|
+
} | {
|
|
204
|
+
status: "rejected";
|
|
205
|
+
failure: AuthenticationFailure;
|
|
206
|
+
};
|
|
207
|
+
export type AuthenticationActionResult = {
|
|
208
|
+
status: "accepted";
|
|
209
|
+
} | {
|
|
210
|
+
status: "failed";
|
|
211
|
+
failure: AuthenticationFailure;
|
|
212
|
+
};
|
|
213
|
+
export type Unsubscribe = () => void;
|
|
214
|
+
export interface AuthenticationSession {
|
|
215
|
+
state(): AuthenticationState | Promise<AuthenticationState>;
|
|
216
|
+
subscribe(listener: (state: AuthenticationState) => void): Unsubscribe;
|
|
217
|
+
start(request: StartAuthenticationRequest): Promise<StartAuthenticationResult>;
|
|
218
|
+
complete(request: CompleteAuthenticationRequest): Promise<CompleteAuthenticationResult>;
|
|
219
|
+
/** Cancels an abandoned SSO window or credentials form. */
|
|
220
|
+
cancelAuthentication(flowId: string): Promise<AuthenticationActionResult>;
|
|
221
|
+
signOut(): Promise<AuthenticationActionResult>;
|
|
222
|
+
close(): Promise<void>;
|
|
223
|
+
}
|
|
224
|
+
export interface ConnectContext {
|
|
225
|
+
protocolVersion: number;
|
|
226
|
+
/** The session that authenticated this connection. */
|
|
227
|
+
sessionId: string;
|
|
228
|
+
/** Omni-side policy: whether the agent's tasks are accepted without asking them. */
|
|
229
|
+
autoAcceptTasks?: boolean;
|
|
230
|
+
signal?: AbortSignal;
|
|
231
|
+
log?: (entry: unknown) => void;
|
|
232
|
+
}
|
|
233
|
+
export type ConnectionStatus = "connecting" | "active" | "error";
|
|
234
|
+
/** Every field is optional: a provider sends what it knows and omits what it does not. */
|
|
235
|
+
export interface Contact {
|
|
236
|
+
name?: string;
|
|
237
|
+
number?: string;
|
|
238
|
+
email?: string;
|
|
239
|
+
attributes?: Attribute[];
|
|
240
|
+
}
|
|
241
|
+
export interface ScheduledActivity {
|
|
242
|
+
id: string;
|
|
243
|
+
title: string;
|
|
244
|
+
startsAt: IsoTimestamp;
|
|
245
|
+
endsAt?: IsoTimestamp;
|
|
246
|
+
contact?: Contact;
|
|
247
|
+
attributes?: Attribute[];
|
|
248
|
+
}
|
|
249
|
+
export interface DispositionCode {
|
|
250
|
+
id: string;
|
|
251
|
+
label: string;
|
|
252
|
+
group?: string;
|
|
253
|
+
}
|
|
254
|
+
export interface DispositionPolicy {
|
|
255
|
+
required?: boolean;
|
|
256
|
+
notes?: "required" | "optional" | "hidden";
|
|
257
|
+
codes?: DispositionCode[];
|
|
258
|
+
}
|
|
259
|
+
export interface Destination {
|
|
260
|
+
id: string;
|
|
261
|
+
label: string;
|
|
262
|
+
address: string;
|
|
263
|
+
/** `agent` here is a routing target, not the person signed in. */
|
|
264
|
+
kind: "queue" | "agent" | "external";
|
|
265
|
+
}
|
|
266
|
+
export interface DestinationDirectory {
|
|
267
|
+
destinations?: Destination[];
|
|
268
|
+
allowManualEntry: boolean;
|
|
269
|
+
}
|
|
270
|
+
export interface CustomCapability {
|
|
271
|
+
id: string;
|
|
272
|
+
ui: {
|
|
273
|
+
kind: "button" | "toggle" | "menu-item";
|
|
274
|
+
label: string;
|
|
275
|
+
placement: "primary" | "secondary" | "overflow";
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
export interface SharedTaskCapabilities {
|
|
279
|
+
browsers?: true;
|
|
280
|
+
dispositions?: true | DispositionPolicy;
|
|
281
|
+
custom?: CustomCapability[];
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* A capability says *offer this control*, and nothing about who carries it out.
|
|
285
|
+
*
|
|
286
|
+
* The channel arms are why `Task<"email">` rejects `hold` at compile time rather than at runtime.
|
|
287
|
+
*/
|
|
288
|
+
export type TaskCapabilities<C extends Channel = Channel> = C extends "voice" ? SharedTaskCapabilities & {
|
|
289
|
+
decline?: true;
|
|
290
|
+
mute?: true;
|
|
291
|
+
hold?: true;
|
|
292
|
+
agentDisconnect?: true;
|
|
293
|
+
blindTransfer?: true | DestinationDirectory;
|
|
294
|
+
conference?: true | DestinationDirectory;
|
|
295
|
+
recording?: true;
|
|
296
|
+
} : C extends "chat" ? SharedTaskCapabilities & {
|
|
297
|
+
reject?: true;
|
|
298
|
+
hold?: true;
|
|
299
|
+
} : SharedTaskCapabilities & {
|
|
300
|
+
reject?: true;
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* How a reusing browser's session is keyed.
|
|
304
|
+
*
|
|
305
|
+
* Named constants rather than a bare union because the values are structured strings: easy to
|
|
306
|
+
* mistype and unreadable as an argument.
|
|
307
|
+
*/
|
|
308
|
+
export declare const BROWSER_ISOLATION_SCHEMES: {
|
|
309
|
+
readonly PROVIDER_NAME__TASK_ID__TAB_NAME: "ProviderName.TaskId.TabName";
|
|
310
|
+
readonly TAB_NAME: "TabName";
|
|
311
|
+
readonly PROVIDER_NAME__TASK_TYPE_NAME__TAB_NAME: "ProviderName.TaskTypeName.TabName";
|
|
312
|
+
readonly PROVIDER_NAME__TAB_NAME: "ProviderName.TabName";
|
|
313
|
+
readonly PROVIDER_NAME__TASK_TYPE_NAME: "ProviderName.TaskTypeName";
|
|
314
|
+
readonly TASK_TYPE_NAME__TAB_NAME: "TaskTypeName.TabName";
|
|
315
|
+
};
|
|
316
|
+
export type BrowserIsolationScheme = (typeof BROWSER_ISOLATION_SCHEMES)[keyof typeof BROWSER_ISOLATION_SCHEMES];
|
|
317
|
+
export interface TaskBrowserBase {
|
|
318
|
+
id: string;
|
|
319
|
+
name: string;
|
|
320
|
+
purpose: string;
|
|
321
|
+
/** `http:` or `https:` only. */
|
|
322
|
+
url: string;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* The union is what makes a reusing browser with no scheme fail to compile rather than inherit
|
|
326
|
+
* a default -- which is how two tasks end up sharing a session nobody intended.
|
|
327
|
+
*/
|
|
328
|
+
export type TaskBrowser = TaskBrowserBase & ({
|
|
329
|
+
reuse: false;
|
|
330
|
+
isolationScheme?: never;
|
|
331
|
+
} | {
|
|
332
|
+
reuse: true;
|
|
333
|
+
isolationScheme: BrowserIsolationScheme;
|
|
334
|
+
});
|
|
335
|
+
export declare const ALLOWED_BROWSER_URL_SCHEMES: readonly ["http:", "https:"];
|
|
336
|
+
export declare function isAllowedBrowserUrl(url: string): boolean;
|
|
337
|
+
export type TaskPhase =
|
|
338
|
+
/** Allocated to this agent and not yet accepted. */
|
|
339
|
+
"pending"
|
|
340
|
+
/** Accepted, and not yet started. */
|
|
341
|
+
| "confirmed"
|
|
342
|
+
/** Being made ready -- a preview the agent reads before the work begins. */
|
|
343
|
+
| "preparing" | "in-progress" | "paused"
|
|
344
|
+
/** The work is over and the agent is finishing up. */
|
|
345
|
+
| "completing";
|
|
346
|
+
/** Who ends the task: the agent issuing `complete`, or the provider deciding it is over. */
|
|
347
|
+
export type CompletionMode = "agent-command" | "provider-automatic";
|
|
348
|
+
export interface TaskAttributeBase {
|
|
349
|
+
key: string;
|
|
350
|
+
label?: string;
|
|
351
|
+
}
|
|
352
|
+
export type TaskAttribute = TaskAttributeBase & ({
|
|
353
|
+
type: "text";
|
|
354
|
+
value: string;
|
|
355
|
+
} | {
|
|
356
|
+
type: "contact";
|
|
357
|
+
contact: Contact;
|
|
358
|
+
} | {
|
|
359
|
+
type: "timestamp";
|
|
360
|
+
at: IsoTimestamp;
|
|
361
|
+
});
|
|
362
|
+
export type HandlingStep = "queued" | "offered" | "answered" | "held" | "muted" | "transferred" | "conferenced" | "unanswered";
|
|
363
|
+
export interface TaskHandlingStep {
|
|
364
|
+
step: HandlingStep;
|
|
365
|
+
at: IsoTimestamp;
|
|
366
|
+
/**
|
|
367
|
+
* Reported, never derived. An entry may be written while its leg is still running, so there is
|
|
368
|
+
* no end to subtract from -- and omitted is the honest report of that, where nought would
|
|
369
|
+
* claim it took no time.
|
|
370
|
+
*/
|
|
371
|
+
seconds?: DurationSeconds;
|
|
372
|
+
/**
|
|
373
|
+
* Who took part. Absent on `queued`, where nobody does; absent on any other step means the
|
|
374
|
+
* provider could not attribute it, which is a different claim and a legitimate one.
|
|
375
|
+
*/
|
|
376
|
+
by?: UserId;
|
|
377
|
+
}
|
|
378
|
+
export interface Task<C extends Channel = Channel> {
|
|
379
|
+
id: TaskId;
|
|
380
|
+
title: string;
|
|
381
|
+
channel: C;
|
|
382
|
+
/** The provider's own name for a category of work. Finer-grained than a channel. */
|
|
383
|
+
taskType: string;
|
|
384
|
+
capabilities: TaskCapabilities<C>;
|
|
385
|
+
browsers: TaskBrowser[];
|
|
386
|
+
contact?: Contact;
|
|
387
|
+
phase: TaskPhase;
|
|
388
|
+
/** The identifier an agent reads back to a customer, where the provider has one. */
|
|
389
|
+
reference?: string;
|
|
390
|
+
completionMode: CompletionMode;
|
|
391
|
+
completionAllowance: DurationSeconds;
|
|
392
|
+
attributes?: TaskAttribute[];
|
|
393
|
+
handlingHistory?: TaskHandlingStep[];
|
|
394
|
+
}
|
|
395
|
+
/** What the provider wants of Omni's acceptance policy for one offer. */
|
|
396
|
+
export type AcceptanceMode = "no-preference" | "require-agent-acceptance" | "require-automatic-acceptance";
|
|
397
|
+
export type TaskOutcome = {
|
|
398
|
+
type: "completed";
|
|
399
|
+
by: "agent" | "provider";
|
|
400
|
+
} | {
|
|
401
|
+
type: "transferred";
|
|
402
|
+
destination?: string;
|
|
403
|
+
} | {
|
|
404
|
+
type: "cancelled";
|
|
405
|
+
reason?: string;
|
|
406
|
+
}
|
|
407
|
+
/** Only the phases in which somebody is still being waited on can expire. */
|
|
408
|
+
| {
|
|
409
|
+
type: "expired";
|
|
410
|
+
phase: "pending" | "confirmed" | "preparing";
|
|
411
|
+
} | {
|
|
412
|
+
type: "failed";
|
|
413
|
+
failure: ProtocolFailure;
|
|
414
|
+
};
|
|
415
|
+
export declare const TASK_COMMAND_NAMES: {
|
|
416
|
+
readonly voice: readonly ["answer", "decline", "start-call", "mute", "hold", "resume", "disconnect", "transfer", "conference", "recording", "complete"];
|
|
417
|
+
readonly chat: readonly ["accept", "reject", "pause", "resume", "complete"];
|
|
418
|
+
readonly email: readonly ["accept", "reject", "complete"];
|
|
419
|
+
};
|
|
420
|
+
export type TaskCommandName<C extends keyof typeof TASK_COMMAND_NAMES> = (typeof TASK_COMMAND_NAMES)[C][number];
|
|
421
|
+
export interface DispositionPayload {
|
|
422
|
+
disposition?: string;
|
|
423
|
+
notes?: string;
|
|
424
|
+
}
|
|
425
|
+
export type VoiceTaskCommand = {
|
|
426
|
+
type: "answer";
|
|
427
|
+
} | {
|
|
428
|
+
type: "decline";
|
|
429
|
+
} | {
|
|
430
|
+
type: "start-call";
|
|
431
|
+
} | {
|
|
432
|
+
type: "mute";
|
|
433
|
+
muted: boolean;
|
|
434
|
+
} | {
|
|
435
|
+
type: "hold";
|
|
436
|
+
} | {
|
|
437
|
+
type: "resume";
|
|
438
|
+
} | {
|
|
439
|
+
type: "disconnect";
|
|
440
|
+
} | {
|
|
441
|
+
type: "transfer";
|
|
442
|
+
destination: string;
|
|
443
|
+
} | {
|
|
444
|
+
type: "conference";
|
|
445
|
+
participant: string;
|
|
446
|
+
action: "add" | "remove";
|
|
447
|
+
} | {
|
|
448
|
+
type: "recording";
|
|
449
|
+
action: "start" | "pause" | "resume" | "stop";
|
|
450
|
+
} | ({
|
|
451
|
+
type: "complete";
|
|
452
|
+
} & DispositionPayload);
|
|
453
|
+
export type ChatTaskCommand = {
|
|
454
|
+
type: "accept";
|
|
455
|
+
} | {
|
|
456
|
+
type: "reject";
|
|
457
|
+
} | {
|
|
458
|
+
type: "pause";
|
|
459
|
+
} | {
|
|
460
|
+
type: "resume";
|
|
461
|
+
} | ({
|
|
462
|
+
type: "complete";
|
|
463
|
+
} & DispositionPayload);
|
|
464
|
+
export type EmailTaskCommand = {
|
|
465
|
+
type: "accept";
|
|
466
|
+
} | {
|
|
467
|
+
type: "reject";
|
|
468
|
+
} | ({
|
|
469
|
+
type: "complete";
|
|
470
|
+
} & DispositionPayload);
|
|
471
|
+
export interface CustomTaskCommand {
|
|
472
|
+
type: "custom";
|
|
473
|
+
name: string;
|
|
474
|
+
[key: string]: unknown;
|
|
475
|
+
}
|
|
476
|
+
export type TaskCommand<C extends Channel = Channel> = (C extends "voice" ? VoiceTaskCommand : C extends "chat" ? ChatTaskCommand : EmailTaskCommand) | CustomTaskCommand;
|
|
477
|
+
export interface TaskCommandRequest<C extends Channel = Channel> {
|
|
478
|
+
/** Stable across retries. Processing one twice must not repeat its side effects. */
|
|
479
|
+
commandId: string;
|
|
480
|
+
taskId: TaskId;
|
|
481
|
+
command: TaskCommand<C>;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* `applied` and `already-applied` rather than a verb per command: the command travels in the
|
|
485
|
+
* request, so `execute({ command: { type: "hold" } })` returning `applied` already says the hold
|
|
486
|
+
* applied.
|
|
487
|
+
*/
|
|
488
|
+
export type TaskCommandResult = {
|
|
489
|
+
commandId: string;
|
|
490
|
+
status: "applied" | "already-applied";
|
|
491
|
+
} | {
|
|
492
|
+
commandId: string;
|
|
493
|
+
status: "failed";
|
|
494
|
+
failure: ProtocolFailure;
|
|
495
|
+
};
|
|
496
|
+
export interface DialRequest {
|
|
497
|
+
commandId: string;
|
|
498
|
+
destination: string;
|
|
499
|
+
}
|
|
500
|
+
export type DialResult = {
|
|
501
|
+
commandId: string;
|
|
502
|
+
status: "dialled" | "already-dialled";
|
|
503
|
+
} | {
|
|
504
|
+
commandId: string;
|
|
505
|
+
status: "failed";
|
|
506
|
+
failure: ProtocolFailure;
|
|
507
|
+
};
|
|
508
|
+
export type BreakApproval = "not-requested"
|
|
509
|
+
/** Somebody has to decide. The agent is waiting on a person. */
|
|
510
|
+
| "awaiting-decision"
|
|
511
|
+
/** Granted, and a promise to honour a later commit. */
|
|
512
|
+
| "granted"
|
|
513
|
+
/** Granted and begins when the current task ends. Nobody needs to act. */
|
|
514
|
+
| "starting-after-task" | "in-effect";
|
|
515
|
+
export declare const BREAK_KINDS: readonly ["short-break", "meal", "rest", "training", "coaching", "meeting", "administrative", "technical", "personal", "other"];
|
|
516
|
+
/**
|
|
517
|
+
* The breaks a contact centre runs, named once so providers can agree.
|
|
518
|
+
*
|
|
519
|
+
* An agent takes one break, not one per platform, so Omni has to know when two providers mean
|
|
520
|
+
* the same thing -- and it cannot tell from the labels, which are each deployment's own words.
|
|
521
|
+
* Every member's meaning is defined in the guide; ten undefined strings would be the label
|
|
522
|
+
* problem one level up. `other` matches nothing, including another provider's `other`.
|
|
523
|
+
*/
|
|
524
|
+
export type BreakKind = (typeof BREAK_KINDS)[number];
|
|
525
|
+
export interface BreakReason {
|
|
526
|
+
id: string;
|
|
527
|
+
label: string;
|
|
528
|
+
group?: string;
|
|
529
|
+
kind?: BreakKind;
|
|
530
|
+
/** Survives `accepting: false`: a mandatory rest is not something a busy hour can cancel. */
|
|
531
|
+
alwaysAvailable?: true;
|
|
532
|
+
}
|
|
533
|
+
export interface BreakRequest {
|
|
534
|
+
/** Stable across retries, and what makes `already-requested` recognisable. */
|
|
535
|
+
requestId: string;
|
|
536
|
+
reason?: string;
|
|
537
|
+
/** The chosen `BreakReason.id`, where the provider publishes codes. */
|
|
538
|
+
reasonId?: string;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* A break placed on the agent rather than requested by them.
|
|
542
|
+
*
|
|
543
|
+
* `by` is required in both arms. Who put somebody off the floor survives whether or not the
|
|
544
|
+
* break ends on a clock -- an imposed break with no origin is a state the agent cannot reason
|
|
545
|
+
* about, and one that ends on a condition is still somebody's decision.
|
|
546
|
+
*/
|
|
547
|
+
export type ImposedBreak = {
|
|
548
|
+
by: UserId;
|
|
549
|
+
endsAutomatically: true;
|
|
550
|
+
endsAt: IsoTimestamp;
|
|
551
|
+
} | {
|
|
552
|
+
by: UserId;
|
|
553
|
+
endsAutomatically: false;
|
|
554
|
+
endsAt?: never;
|
|
555
|
+
};
|
|
556
|
+
export interface BreakState {
|
|
557
|
+
approval: BreakApproval;
|
|
558
|
+
requestId?: string;
|
|
559
|
+
/** Whether the agent may ask at all. Distinct from the fate of a request already made. */
|
|
560
|
+
accepting: boolean;
|
|
561
|
+
/** Shown when `accepting` is false, such as "Busy hours". */
|
|
562
|
+
refusedReason?: string;
|
|
563
|
+
decisionReason?: string;
|
|
564
|
+
retryAfterMs?: number;
|
|
565
|
+
/** Not-ready codes this provider offers. Omitted when it defines none. */
|
|
566
|
+
reasons?: BreakReason[];
|
|
567
|
+
/** Which reason the current break is on. Omitted when there is no break. */
|
|
568
|
+
activeReasonId?: string;
|
|
569
|
+
imposed?: ImposedBreak;
|
|
570
|
+
}
|
|
571
|
+
export type CapacityResult = {
|
|
572
|
+
status: "accepted";
|
|
573
|
+
} | {
|
|
574
|
+
status: "failed";
|
|
575
|
+
failure: ProtocolFailure;
|
|
576
|
+
};
|
|
577
|
+
/** Succeeding is not the outcome: `requested` says the provider holds it, not that it was granted. */
|
|
578
|
+
export type BreakRequestResult = {
|
|
579
|
+
requestId: string;
|
|
580
|
+
status: "requested" | "already-requested";
|
|
581
|
+
} | {
|
|
582
|
+
requestId: string;
|
|
583
|
+
status: "failed";
|
|
584
|
+
failure: ProtocolFailure;
|
|
585
|
+
};
|
|
586
|
+
export type BreakCommitResult = {
|
|
587
|
+
requestId: string;
|
|
588
|
+
status: "committed" | "already-committed";
|
|
589
|
+
} | {
|
|
590
|
+
requestId: string;
|
|
591
|
+
status: "failed";
|
|
592
|
+
failure: ProtocolFailure;
|
|
593
|
+
};
|
|
594
|
+
export type BreakCancelResult = {
|
|
595
|
+
requestId: string;
|
|
596
|
+
status: "cancelled" | "already-cancelled";
|
|
597
|
+
} | {
|
|
598
|
+
requestId: string;
|
|
599
|
+
status: "failed";
|
|
600
|
+
failure: ProtocolFailure;
|
|
601
|
+
};
|
|
602
|
+
export type BreakEndResult = {
|
|
603
|
+
status: "ended" | "already-ended";
|
|
604
|
+
} | {
|
|
605
|
+
status: "failed";
|
|
606
|
+
failure: ProtocolFailure;
|
|
607
|
+
};
|
|
608
|
+
export type TeamMemberAvailability = "ready" | "on-task" | "on-break" | "signed-out";
|
|
609
|
+
export interface TeamMember {
|
|
610
|
+
id: UserId;
|
|
611
|
+
availability: TeamMemberAvailability;
|
|
612
|
+
/** Omitted rather than invented: Omni renders it as a duration. */
|
|
613
|
+
since?: IsoTimestamp;
|
|
614
|
+
break?: BreakApproval;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Published only to an agent entitled to one. Its presence is the permission -- nothing else
|
|
618
|
+
* makes somebody a lead, and there is no separate flag to fall out of step with the data.
|
|
619
|
+
*/
|
|
620
|
+
export interface TeamRoster {
|
|
621
|
+
members: TeamMember[];
|
|
622
|
+
breakControl?: true;
|
|
623
|
+
}
|
|
624
|
+
export type TeamBreakCommand = {
|
|
625
|
+
type: "decide";
|
|
626
|
+
memberId: UserId;
|
|
627
|
+
decision: "granted" | "denied";
|
|
628
|
+
reason?: string;
|
|
629
|
+
} | {
|
|
630
|
+
type: "policy";
|
|
631
|
+
policy: "ask" | "auto-approve" | "suspended";
|
|
632
|
+
} | {
|
|
633
|
+
type: "place";
|
|
634
|
+
memberId: UserId;
|
|
635
|
+
reason?: string;
|
|
636
|
+
} | {
|
|
637
|
+
type: "release";
|
|
638
|
+
memberId: UserId;
|
|
639
|
+
};
|
|
640
|
+
export type TeamCommandResult = {
|
|
641
|
+
commandId: string;
|
|
642
|
+
status: "applied" | "already-applied";
|
|
643
|
+
} | {
|
|
644
|
+
commandId: string;
|
|
645
|
+
status: "failed";
|
|
646
|
+
failure: ProtocolFailure;
|
|
647
|
+
};
|
|
648
|
+
export interface TeamBreakCommandRequest {
|
|
649
|
+
commandId: string;
|
|
650
|
+
command: TeamBreakCommand;
|
|
651
|
+
}
|
|
652
|
+
export interface VoiceMediaSession {
|
|
653
|
+
remoteAudio: MediaStream;
|
|
654
|
+
setMuted(muted: boolean): void;
|
|
655
|
+
close(): void;
|
|
656
|
+
}
|
|
657
|
+
export interface OpenMediaRequest {
|
|
658
|
+
taskId: TaskId;
|
|
659
|
+
localAudio: MediaStream;
|
|
660
|
+
}
|
|
661
|
+
export type OpenMediaResult = {
|
|
662
|
+
status: "opened";
|
|
663
|
+
session: VoiceMediaSession;
|
|
664
|
+
} | {
|
|
665
|
+
status: "unavailable";
|
|
666
|
+
failure: ProtocolFailure;
|
|
667
|
+
};
|
|
668
|
+
export interface SessionCapabilities {
|
|
669
|
+
breaks?: true;
|
|
670
|
+
teamBreakControl?: true;
|
|
671
|
+
}
|
|
672
|
+
/** The provider's complete state at one moment. It replaces what Omni holds; never a patch. */
|
|
673
|
+
export interface Snapshot<C extends Channel = Channel> {
|
|
674
|
+
status: ConnectionStatus;
|
|
675
|
+
sessionId: string;
|
|
676
|
+
sessionCapabilities: SessionCapabilities;
|
|
677
|
+
break: BreakState;
|
|
678
|
+
/** Every task currently owned by this agent for this provider. */
|
|
679
|
+
tasks: Task<C>[];
|
|
680
|
+
contacts?: Contact[];
|
|
681
|
+
scheduledActivities?: ScheduledActivity[];
|
|
682
|
+
team?: TeamRoster;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* How many tasks this provider may have allocated to the agent at once.
|
|
686
|
+
*
|
|
687
|
+
* An absolute ceiling, never less than 1, standing until Omni restates it. The provider counts
|
|
688
|
+
* its own outstanding tasks against it and needs no new signal when one ends. What the agent
|
|
689
|
+
* holds at other providers is not this provider's concern -- Omni set `count` knowing it.
|
|
690
|
+
*/
|
|
691
|
+
export interface AgentCapacity {
|
|
692
|
+
count: number;
|
|
693
|
+
}
|
|
694
|
+
export interface SummaryMetric {
|
|
695
|
+
id: string;
|
|
696
|
+
label: string;
|
|
697
|
+
value: string;
|
|
698
|
+
}
|
|
699
|
+
export interface ProviderSummary {
|
|
700
|
+
title: string;
|
|
701
|
+
subtitle?: string;
|
|
702
|
+
waitingCount: number;
|
|
703
|
+
updatedAt: IsoTimestamp;
|
|
704
|
+
metrics?: SummaryMetric[];
|
|
705
|
+
}
|
|
706
|
+
export type ProviderEvent<C extends Channel = Channel> = {
|
|
707
|
+
type: "snapshot";
|
|
708
|
+
reason: "reconnected" | "provider-requested";
|
|
709
|
+
snapshot: Snapshot<C>;
|
|
710
|
+
} | {
|
|
711
|
+
type: "provider-status";
|
|
712
|
+
status: ConnectionStatus;
|
|
713
|
+
message?: string;
|
|
714
|
+
} | {
|
|
715
|
+
type: "break-state";
|
|
716
|
+
break: BreakState;
|
|
717
|
+
} | {
|
|
718
|
+
type: "task-offered";
|
|
719
|
+
task: Task<C>;
|
|
720
|
+
acceptanceMode?: AcceptanceMode;
|
|
721
|
+
allocationExpiresAt?: IsoTimestamp;
|
|
722
|
+
preparationEndsAt?: IsoTimestamp;
|
|
723
|
+
} | {
|
|
724
|
+
type: "task-updated";
|
|
725
|
+
task: Task<C>;
|
|
726
|
+
} | {
|
|
727
|
+
type: "task-media-ended";
|
|
728
|
+
taskId: TaskId;
|
|
729
|
+
} | {
|
|
730
|
+
type: "task-ended";
|
|
731
|
+
taskId: TaskId;
|
|
732
|
+
outcome: TaskOutcome;
|
|
733
|
+
} | {
|
|
734
|
+
type: "announcement";
|
|
735
|
+
text: string;
|
|
736
|
+
html?: string;
|
|
737
|
+
announcedAt: IsoTimestamp;
|
|
738
|
+
expiresAt?: IsoTimestamp;
|
|
739
|
+
} | {
|
|
740
|
+
type: "provider-summary";
|
|
741
|
+
summary: ProviderSummary;
|
|
742
|
+
} | {
|
|
743
|
+
type: "team-updated";
|
|
744
|
+
team: TeamRoster;
|
|
745
|
+
} | {
|
|
746
|
+
type: "contacts-updated";
|
|
747
|
+
contacts: Contact[];
|
|
748
|
+
} | {
|
|
749
|
+
type: "calendar-updated";
|
|
750
|
+
scheduledActivities: ScheduledActivity[];
|
|
751
|
+
};
|
|
752
|
+
/**
|
|
753
|
+
* The `Provider` prefix survives here for a mechanical reason rather than a naming one: `Event`
|
|
754
|
+
* is a DOM global, and a bare one would shadow it for every adapter compiled against the browser
|
|
755
|
+
* lib.
|
|
756
|
+
*/
|
|
757
|
+
export interface ProviderEventEnvelope<C extends Channel = Channel> {
|
|
758
|
+
id: string;
|
|
759
|
+
/** The login this belongs to. */
|
|
760
|
+
sessionId: string;
|
|
761
|
+
occurredAt: IsoTimestamp;
|
|
762
|
+
event: ProviderEvent<C>;
|
|
763
|
+
}
|
|
764
|
+
export declare const OMNI_FAILURE_CODES: readonly ["omni.not-authenticated", "omni.capability-not-enabled", "omni.task-not-found", "omni.destination-not-permitted", "omni.rate-limited", "omni.unavailable", "omni.break-already-committed"];
|
|
765
|
+
export type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
|
|
766
|
+
export interface Connection<C extends Channel = Channel> {
|
|
767
|
+
snapshot(): Snapshot<C> | Promise<Snapshot<C>>;
|
|
768
|
+
/** Delivery order must match the order the provider observes changes. Never replay. */
|
|
769
|
+
subscribe(listener: (envelope: ProviderEventEnvelope<C>) => void): Unsubscribe;
|
|
770
|
+
/** Nothing may be allocated until a capacity is stated, so every connection receives it. */
|
|
771
|
+
setCapacity(capacity: AgentCapacity): Promise<CapacityResult>;
|
|
772
|
+
execute(request: TaskCommandRequest<C>): Promise<TaskCommandResult>;
|
|
773
|
+
disconnect(): Promise<void>;
|
|
774
|
+
/** Required of any adapter publishing a `UserId` -- on an imposed break, a roster, or history. */
|
|
775
|
+
describeUsers?(ids: UserId[]): Promise<User[]>;
|
|
776
|
+
/** Required when the manifest declares `idleCapabilities.dial`. */
|
|
777
|
+
dial?(request: DialRequest): Promise<DialResult>;
|
|
778
|
+
/**
|
|
779
|
+
* The four break methods stand or fall together. Declaring `sessionCapabilities.breaks` and
|
|
780
|
+
* implementing `requestBreak` without `commitBreak` leaves an agent granted a break that can
|
|
781
|
+
* never start, and the two-phase coordination has no way to report it.
|
|
782
|
+
*/
|
|
783
|
+
requestBreak?(request: BreakRequest): Promise<BreakRequestResult>;
|
|
784
|
+
commitBreak?(requestId: string): Promise<BreakCommitResult>;
|
|
785
|
+
cancelBreak?(requestId: string): Promise<BreakCancelResult>;
|
|
786
|
+
endBreak?(): Promise<BreakEndResult>;
|
|
787
|
+
/** Required when the adapter publishes a `TeamRoster` carrying `breakControl`. */
|
|
788
|
+
executeTeamBreak?(request: TeamBreakCommandRequest): Promise<TeamCommandResult>;
|
|
789
|
+
/** Required of every voice adapter: all voice audio lands in Omni. */
|
|
790
|
+
openMedia?(request: OpenMediaRequest): Promise<OpenMediaResult>;
|
|
791
|
+
}
|
|
792
|
+
export interface Adapter<C extends Channel = Channel> {
|
|
793
|
+
manifest: Manifest<C>;
|
|
794
|
+
createAuthenticationSession(context: AuthenticationContext): Promise<AuthenticationSession> | AuthenticationSession;
|
|
795
|
+
connect(context: ConnectContext): Promise<Connection<C>>;
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Preserves the adapter's inferred concrete type while checking it implements `Adapter`.
|
|
799
|
+
* No connection, no runtime side effects.
|
|
800
|
+
*/
|
|
801
|
+
export declare function defineAdapter<C extends Channel, A extends Adapter<C>>(adapter: A): A;
|
|
802
|
+
export declare const DEFAULT_TASK_PHASE_LABELS: {
|
|
803
|
+
readonly voice: {
|
|
804
|
+
readonly pending: "Offered";
|
|
805
|
+
readonly confirmed: "Accepted";
|
|
806
|
+
readonly preparing: "Preview";
|
|
807
|
+
readonly "in-progress": "On Call";
|
|
808
|
+
readonly paused: "On Hold";
|
|
809
|
+
readonly completing: "After Call Work";
|
|
810
|
+
};
|
|
811
|
+
readonly chat: {
|
|
812
|
+
readonly pending: "Incoming Chat";
|
|
813
|
+
readonly confirmed: "Accepted";
|
|
814
|
+
readonly preparing: "Preparing";
|
|
815
|
+
readonly "in-progress": "In Chat";
|
|
816
|
+
readonly paused: "Paused";
|
|
817
|
+
readonly completing: "Wrap-up";
|
|
818
|
+
};
|
|
819
|
+
readonly email: {
|
|
820
|
+
readonly pending: "Assigned";
|
|
821
|
+
readonly confirmed: "Accepted";
|
|
822
|
+
readonly preparing: "Reviewing";
|
|
823
|
+
readonly "in-progress": "Working";
|
|
824
|
+
readonly paused: "Paused";
|
|
825
|
+
readonly completing: "Completing";
|
|
826
|
+
};
|
|
827
|
+
};
|
|
828
|
+
export declare const DEFAULT_TASK_TYPE_PRESENTATION: {
|
|
829
|
+
readonly voice: {
|
|
830
|
+
readonly singular: "Call";
|
|
831
|
+
readonly plural: "Calls";
|
|
832
|
+
readonly referenceLabel: "Call ID";
|
|
833
|
+
};
|
|
834
|
+
readonly chat: {
|
|
835
|
+
readonly singular: "Chat";
|
|
836
|
+
readonly plural: "Chats";
|
|
837
|
+
readonly referenceLabel: "Chat ID";
|
|
838
|
+
};
|
|
839
|
+
readonly email: {
|
|
840
|
+
readonly singular: "Email";
|
|
841
|
+
readonly plural: "Emails";
|
|
842
|
+
readonly referenceLabel: "Email ID";
|
|
843
|
+
};
|
|
844
|
+
};
|
|
845
|
+
/**
|
|
846
|
+
* A collision-safe global task key.
|
|
847
|
+
*
|
|
848
|
+
* Task ids are unique only within one provider, so two providers will eventually issue the same
|
|
849
|
+
* string. Encoding before joining is what stops a provider id containing a separator from
|
|
850
|
+
* forging another provider's key.
|
|
851
|
+
*/
|
|
852
|
+
export declare const taskKey: (providerId: string, taskId: TaskId) => string;
|
|
853
|
+
/**
|
|
854
|
+
* The same treatment for a `UserId`, and needed for the same reason.
|
|
855
|
+
*
|
|
856
|
+
* There is no Omni-wide user identity: one person on several providers has several identities
|
|
857
|
+
* and nothing here pairs them. A bare `UserId` is only ever compared against another from the
|
|
858
|
+
* same provider; anything wider goes through this.
|
|
859
|
+
*/
|
|
860
|
+
export declare const userKey: (providerId: string, userId: UserId) => string;
|
|
861
|
+
/** Every handling step somebody takes part in. `queued` is the one nobody does. */
|
|
862
|
+
export declare const HANDLING_STEPS_WITH_A_PERSON: readonly ["offered", "answered", "held", "muted", "transferred", "conferenced", "unanswered"];
|
|
863
|
+
/** Whether an absent `by` means "could not attribute" rather than "nobody was involved". */
|
|
864
|
+
export declare function handlingStepExpectsAPerson(step: HandlingStep): boolean;
|
|
865
|
+
export interface BrowserSessionKeyInput {
|
|
866
|
+
/** `Manifest.id`, never `displayName`: only the id is unique across an installation and stable. */
|
|
867
|
+
providerId: string;
|
|
868
|
+
taskId: TaskId;
|
|
869
|
+
/** `Task.taskType`. */
|
|
870
|
+
taskType: string;
|
|
871
|
+
browser: TaskBrowser;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* The storage-profile key a reusing browser shares, or `undefined` where it shares nothing.
|
|
875
|
+
*
|
|
876
|
+
* Fails closed. A browser with `reuse: false` has no key; nor does a reusing one whose scheme is
|
|
877
|
+
* missing or unknown -- the type forbids that, but an adapter compiled against another version can
|
|
878
|
+
* still send it, and the safe reading is "do not share", never "share with everyone named the
|
|
879
|
+
* same". Every part is encoded, separator included, before joining, so a tab called `a.b`
|
|
880
|
+
* cannot collide with a provider called `a` and a tab called `b`.
|
|
881
|
+
*/
|
|
882
|
+
export declare function browserSessionKey(input: BrowserSessionKeyInput): string | undefined;
|
|
883
|
+
/**
|
|
884
|
+
* The comparison key for a contact number. Never for display: keep the original value for that.
|
|
885
|
+
*
|
|
886
|
+
* Applies NFKC, strips whitespace, brackets, slashes, periods and every Unicode dash, and rewrites
|
|
887
|
+
* a leading `00` to `+`, so `+1 (415) 555-0100`, `+1.415.555.0100` and `0014155550100` all merge.
|
|
888
|
+
* Cross-provider merging is reliable only for E.164 input: a national-format number carries no
|
|
889
|
+
* country context, nothing in this protocol supplies one, and so it does not merge with its
|
|
890
|
+
* `+`-prefixed twin.
|
|
891
|
+
*/
|
|
892
|
+
export declare function normalizeContactNumber(number: string): string;
|
|
893
|
+
/** The comparison key for a contact email. Never for display. */
|
|
894
|
+
export declare function normalizeContactEmail(email: string): string;
|