@xema/omni-protocol 0.1.26 → 0.1.27
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 +11 -9
- package/dist/index.js +2 -2
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +5 -5
- package/dist/validation.js +11 -11
- package/guide.md +19 -19
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -342,7 +342,8 @@ export interface ScheduledActivity {
|
|
|
342
342
|
title: string;
|
|
343
343
|
startsAt: IsoTimestamp;
|
|
344
344
|
endsAt?: IsoTimestamp;
|
|
345
|
-
|
|
345
|
+
/** The person the activity reaches -- a callback's customer. */
|
|
346
|
+
party?: Contact;
|
|
346
347
|
attributes?: Attribute[];
|
|
347
348
|
}
|
|
348
349
|
export interface DispositionCode {
|
|
@@ -443,10 +444,10 @@ export type TaskBrowser = Browser & {
|
|
|
443
444
|
/** Hide this tab's URL from the agent in Omni's chrome. Omitted, the URL shows as any browser's does; a provider says `hidden` where the URL carries what the agent may not read. */
|
|
444
445
|
urlVisibility?: UrlVisibility;
|
|
445
446
|
} & ({
|
|
446
|
-
|
|
447
|
+
sharedSession: false;
|
|
447
448
|
isolationScheme?: never;
|
|
448
449
|
} | {
|
|
449
|
-
|
|
450
|
+
sharedSession: true;
|
|
450
451
|
isolationScheme: BrowserIsolationScheme;
|
|
451
452
|
});
|
|
452
453
|
/** A tab the agent opened in the personal workspace: theirs, as many as they like, and never on the wire. */
|
|
@@ -473,7 +474,7 @@ export type TaskAttribute = TaskAttributeBase & ({
|
|
|
473
474
|
value: string;
|
|
474
475
|
} | {
|
|
475
476
|
type: "contact";
|
|
476
|
-
|
|
477
|
+
party: Contact;
|
|
477
478
|
} | {
|
|
478
479
|
type: "timestamp";
|
|
479
480
|
at: IsoTimestamp;
|
|
@@ -590,7 +591,8 @@ export type Task<C extends Channel = Channel> = {
|
|
|
590
591
|
taskType: string;
|
|
591
592
|
capabilities: TaskCapabilities<C>;
|
|
592
593
|
browsers: TaskBrowser[];
|
|
593
|
-
|
|
594
|
+
/** The person or entity on the other end of this task. Who the task is with; `contacts` on the snapshot is the directory. */
|
|
595
|
+
party?: Contact;
|
|
594
596
|
phase: TaskPhase;
|
|
595
597
|
/** The identifier an agent reads back to a customer, where the provider has one. */
|
|
596
598
|
reference?: string;
|
|
@@ -1031,7 +1033,7 @@ export interface SummaryMetric {
|
|
|
1031
1033
|
label: string;
|
|
1032
1034
|
value: string;
|
|
1033
1035
|
}
|
|
1034
|
-
export interface
|
|
1036
|
+
export interface QueueSummary {
|
|
1035
1037
|
title: string;
|
|
1036
1038
|
subtitle?: string;
|
|
1037
1039
|
waitingCount: number;
|
|
@@ -1080,8 +1082,8 @@ export type ProviderEvent<C extends Channel = Channel> = {
|
|
|
1080
1082
|
announcedAt: IsoTimestamp;
|
|
1081
1083
|
expiresAt?: IsoTimestamp;
|
|
1082
1084
|
} | {
|
|
1083
|
-
type: "
|
|
1084
|
-
summary:
|
|
1085
|
+
type: "queue-summary";
|
|
1086
|
+
summary: QueueSummary;
|
|
1085
1087
|
} | {
|
|
1086
1088
|
type: "team-updated";
|
|
1087
1089
|
team: TeamRoster;
|
|
@@ -1229,7 +1231,7 @@ export declare function sameCapabilities(a: UserCapabilities, b: UserCapabilitie
|
|
|
1229
1231
|
/**
|
|
1230
1232
|
* The storage-profile key a reusing browser shares, or `undefined` where it shares nothing.
|
|
1231
1233
|
*
|
|
1232
|
-
* Fails closed. A browser with `
|
|
1234
|
+
* Fails closed. A browser with `sharedSession: false` has no key; nor does a reusing one whose scheme is
|
|
1233
1235
|
* missing or unknown -- the type forbids that, but an adapter compiled against another version can
|
|
1234
1236
|
* still send it, and the safe reading is "do not share", never "share with everyone named the
|
|
1235
1237
|
* same". Every part is encoded, separator included, before joining, so a tab called `a.b`
|
package/dist/index.js
CHANGED
|
@@ -157,7 +157,7 @@ export function sameCapabilities(a, b) {
|
|
|
157
157
|
/**
|
|
158
158
|
* The storage-profile key a reusing browser shares, or `undefined` where it shares nothing.
|
|
159
159
|
*
|
|
160
|
-
* Fails closed. A browser with `
|
|
160
|
+
* Fails closed. A browser with `sharedSession: false` has no key; nor does a reusing one whose scheme is
|
|
161
161
|
* missing or unknown -- the type forbids that, but an adapter compiled against another version can
|
|
162
162
|
* still send it, and the safe reading is "do not share", never "share with everyone named the
|
|
163
163
|
* same". Every part is encoded, separator included, before joining, so a tab called `a.b`
|
|
@@ -165,7 +165,7 @@ export function sameCapabilities(a, b) {
|
|
|
165
165
|
*/
|
|
166
166
|
export function browserSessionKey(input) {
|
|
167
167
|
const { providerId, taskId, taskType, browser } = input;
|
|
168
|
-
if (browser.
|
|
168
|
+
if (browser.sharedSession !== true)
|
|
169
169
|
return undefined;
|
|
170
170
|
// `encodeURIComponent` leaves `.` untouched, and `.` is the separator: a raw join would let
|
|
171
171
|
// provider `Acme.Voice` with type `Support` forge the key of `Acme` with `Voice.Support`.
|
package/dist/testing.d.ts
CHANGED
|
@@ -166,7 +166,7 @@ export declare function assertWrapTimeout(task: Pick<TaskCompletion, "completion
|
|
|
166
166
|
/** One browser in one task of one provider. `providerId` is `Manifest.id`, never `displayName`. */
|
|
167
167
|
export type BrowserIsolationScenario = BrowserSessionKeyInput;
|
|
168
168
|
/** Validates whether two task-browser definitions should share one browser session. */
|
|
169
|
-
export declare function
|
|
169
|
+
export declare function assertBrowserSessionIsolation(left: BrowserIsolationScenario, right: BrowserIsolationScenario, expectedReuse: boolean): void;
|
|
170
170
|
/**
|
|
171
171
|
* Asserts that no two distinct scenarios in `scenarios` derive the same session key.
|
|
172
172
|
* Feed it adversarial names — a provider called `A.B` against a task type called
|
package/dist/testing.js
CHANGED
|
@@ -32,7 +32,7 @@ const STATE_SUBJECTS = [
|
|
|
32
32
|
// `ProviderEvent` without a row here, or a row it lacks, is a compile error.
|
|
33
33
|
const EVENT_TYPES = {
|
|
34
34
|
snapshot: true, "transport-status": true, "break-state": true, "task-offered": true, "task-updated": true,
|
|
35
|
-
"task-media-started": true, "task-media-ended": true, "task-ended": true, announcement: true, "
|
|
35
|
+
"task-media-started": true, "task-media-ended": true, "task-ended": true, announcement: true, "queue-summary": true,
|
|
36
36
|
"team-updated": true, "contacts-updated": true, "calendar-updated": true,
|
|
37
37
|
};
|
|
38
38
|
const CONTRACT_SUBJECTS = [
|
|
@@ -854,14 +854,14 @@ export function assertWrapTimeout(task, mediaEndedAt, observedDeadline, toleranc
|
|
|
854
854
|
/** The session key one scenario derives, or `undefined` where the browser shares nothing. */
|
|
855
855
|
const sessionKeyFor = (scenario) => browserSessionKey(scenario);
|
|
856
856
|
/** Validates whether two task-browser definitions should share one browser session. */
|
|
857
|
-
export function
|
|
857
|
+
export function assertBrowserSessionIsolation(left, right, expectedReuse) {
|
|
858
858
|
const leftKey = sessionKeyFor(left);
|
|
859
859
|
const rightKey = sessionKeyFor(right);
|
|
860
|
-
// A browser that does not
|
|
861
|
-
// Treating "no key" as a match would report
|
|
860
|
+
// A browser that does not share its session has no session key at all, so two of them never share one.
|
|
861
|
+
// Treating "no key" as a match would report sharing nobody asked for.
|
|
862
862
|
const actualReuse = leftKey !== undefined && leftKey === rightKey;
|
|
863
863
|
if (actualReuse !== expectedReuse) {
|
|
864
|
-
throw new Error(`Browser
|
|
864
|
+
throw new Error(`Browser session sharing mismatch: expected ${expectedReuse}, received ${actualReuse} (${String(leftKey)} vs ${String(rightKey)})`);
|
|
865
865
|
}
|
|
866
866
|
}
|
|
867
867
|
/**
|
package/dist/validation.js
CHANGED
|
@@ -200,8 +200,8 @@ function validateScheduledActivityInto(activity, path, into) {
|
|
|
200
200
|
into.require(Date.parse(activity.endsAt) >= Date.parse(activity.startsAt), "activity.endsAt.order", `${path}.endsAt`, "endsAt must not precede startsAt");
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
|
-
if (activity.
|
|
204
|
-
validateContactInto(activity.
|
|
203
|
+
if (activity.party !== undefined)
|
|
204
|
+
validateContactInto(activity.party, `${path}.party`, into);
|
|
205
205
|
validateAttributes(activity.attributes, `${path}.attributes`, into);
|
|
206
206
|
}
|
|
207
207
|
// ---------------------------------------------------------------------------
|
|
@@ -572,7 +572,7 @@ function validateBrowsers(value, path, into) {
|
|
|
572
572
|
// Reuse and its scheme travel together. A reusing browser with no scheme would otherwise
|
|
573
573
|
// inherit whatever a host happened to default to, which is how two tasks end up sharing a
|
|
574
574
|
// session nobody intended. The guide names the rule for the missing case.
|
|
575
|
-
if (browser.
|
|
575
|
+
if (browser.sharedSession === true) {
|
|
576
576
|
if (browser.isolationScheme === undefined) {
|
|
577
577
|
into.add("task.browser.isolationScheme.required", `${at}.isolationScheme`, `a reusing browser must declare one of: ${ISOLATION_SCHEME_VALUES.join(", ")}`);
|
|
578
578
|
}
|
|
@@ -580,11 +580,11 @@ function validateBrowsers(value, path, into) {
|
|
|
580
580
|
into.require(ISOLATION_SCHEME_VALUES.includes(browser.isolationScheme), "task.browser.isolationScheme", `${at}.isolationScheme`, `an isolation scheme must be one of: ${ISOLATION_SCHEME_VALUES.join(", ")}`);
|
|
581
581
|
}
|
|
582
582
|
}
|
|
583
|
-
else if (browser.
|
|
584
|
-
into.require(browser.isolationScheme === undefined, "task.browser.isolationScheme.unexpected", `${at}.isolationScheme`, "a browser that does not
|
|
583
|
+
else if (browser.sharedSession === false) {
|
|
584
|
+
into.require(browser.isolationScheme === undefined, "task.browser.isolationScheme.unexpected", `${at}.isolationScheme`, "a browser that does not share its session must not declare an isolation scheme");
|
|
585
585
|
}
|
|
586
586
|
else {
|
|
587
|
-
into.add("task.browser.
|
|
587
|
+
into.add("task.browser.sharedSession", `${at}.sharedSession`, "a browser must say whether its session is shared across tasks");
|
|
588
588
|
}
|
|
589
589
|
});
|
|
590
590
|
}
|
|
@@ -745,8 +745,8 @@ function validateTaskInto(task, context, path, into) {
|
|
|
745
745
|
if (task.reference !== undefined) {
|
|
746
746
|
into.filled(task.reference, "task.reference", `${path}.reference`, "a reference must not be empty when present");
|
|
747
747
|
}
|
|
748
|
-
if (task.
|
|
749
|
-
validateContactInto(task.
|
|
748
|
+
if (task.party !== undefined)
|
|
749
|
+
validateContactInto(task.party, `${path}.party`, into, context.levels);
|
|
750
750
|
validateBrowsers(task.browsers, `${path}.browsers`, into);
|
|
751
751
|
validateTaskAttributes(task.attributes, `${path}.attributes`, into);
|
|
752
752
|
validateHandlingHistory(task.handlingHistory, `${path}.handlingHistory`, into);
|
|
@@ -1117,7 +1117,7 @@ function validateTaskOutcome(value, path, into) {
|
|
|
1117
1117
|
into.add("event.taskEnded.outcome.type", `${path}.type`, `unsupported outcome: ${String(value.type)}`);
|
|
1118
1118
|
}
|
|
1119
1119
|
}
|
|
1120
|
-
function
|
|
1120
|
+
function validateQueueSummary(value, path, into) {
|
|
1121
1121
|
if (!isPlainObject(value)) {
|
|
1122
1122
|
into.add("event.summary.shape", path, "a provider summary must be an object");
|
|
1123
1123
|
return;
|
|
@@ -1238,8 +1238,8 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
|
|
|
1238
1238
|
into.require(typeof event.html === "string", "event.announcement.html", `${at}.html`, "html must be a string when present");
|
|
1239
1239
|
}
|
|
1240
1240
|
break;
|
|
1241
|
-
case "
|
|
1242
|
-
|
|
1241
|
+
case "queue-summary":
|
|
1242
|
+
validateQueueSummary(event.summary, `${at}.summary`, into);
|
|
1243
1243
|
break;
|
|
1244
1244
|
case "team-updated":
|
|
1245
1245
|
validateTeamRosterInto(event.team, `${at}.team`, { ...context, levels }, into);
|
package/guide.md
CHANGED
|
@@ -393,7 +393,7 @@ type ScheduledActivity = {
|
|
|
393
393
|
title: string;
|
|
394
394
|
startsAt: IsoTimestamp;
|
|
395
395
|
endsAt?: IsoTimestamp;
|
|
396
|
-
|
|
396
|
+
party?: Contact;
|
|
397
397
|
attributes?: Attribute[];
|
|
398
398
|
};
|
|
399
399
|
|
|
@@ -492,8 +492,8 @@ type TaskBrowser = Browser & {
|
|
|
492
492
|
purpose: string;
|
|
493
493
|
urlVisibility?: UrlVisibility;
|
|
494
494
|
} & (
|
|
495
|
-
| {
|
|
496
|
-
| {
|
|
495
|
+
| { sharedSession: false; isolationScheme?: never }
|
|
496
|
+
| { sharedSession: true; isolationScheme: BrowserIsolationScheme }
|
|
497
497
|
);
|
|
498
498
|
|
|
499
499
|
type PersonalBrowser = Browser;
|
|
@@ -507,7 +507,7 @@ type BrowserSessionKeyInput = {
|
|
|
507
507
|
```
|
|
508
508
|
|
|
509
509
|
That union is what makes a reusing browser with no scheme fail to compile rather than inherit a
|
|
510
|
-
default — see **Choosing
|
|
510
|
+
default — see **Choosing an isolation scheme**.
|
|
511
511
|
|
|
512
512
|
### Task
|
|
513
513
|
|
|
@@ -529,7 +529,7 @@ type TaskAttributeBase = {
|
|
|
529
529
|
|
|
530
530
|
type TaskAttribute = TaskAttributeBase & (
|
|
531
531
|
| { type: "text"; value: string }
|
|
532
|
-
| { type: "contact";
|
|
532
|
+
| { type: "contact"; party: Contact }
|
|
533
533
|
| { type: "timestamp"; at: IsoTimestamp }
|
|
534
534
|
);
|
|
535
535
|
|
|
@@ -596,7 +596,7 @@ type Task<C extends Channel = Channel> = {
|
|
|
596
596
|
taskType: string;
|
|
597
597
|
capabilities: TaskCapabilities<C>;
|
|
598
598
|
browsers: TaskBrowser[];
|
|
599
|
-
|
|
599
|
+
party?: Contact;
|
|
600
600
|
phase: TaskPhase;
|
|
601
601
|
reference?: string;
|
|
602
602
|
attributes?: TaskAttribute[];
|
|
@@ -917,7 +917,7 @@ type OpenMediaRequest = {
|
|
|
917
917
|
```ts
|
|
918
918
|
type SummaryMetric = { id: string; label: string; value: string };
|
|
919
919
|
|
|
920
|
-
type
|
|
920
|
+
type QueueSummary = {
|
|
921
921
|
title: string;
|
|
922
922
|
subtitle?: string;
|
|
923
923
|
waitingCount: number;
|
|
@@ -944,7 +944,7 @@ type ProviderEvent =
|
|
|
944
944
|
| { type: "task-media-ended"; taskId: TaskId }
|
|
945
945
|
| { type: "task-ended"; taskId: TaskId; outcome: TaskOutcome }
|
|
946
946
|
| { type: "announcement"; text: string; html?: string; announcedAt: IsoTimestamp; expiresAt?: IsoTimestamp }
|
|
947
|
-
| { type: "
|
|
947
|
+
| { type: "queue-summary"; summary: QueueSummary }
|
|
948
948
|
| { type: "team-updated"; team: TeamRoster }
|
|
949
949
|
| { type: "contacts-updated"; contacts: Contact[] }
|
|
950
950
|
| { type: "calendar-updated"; scheduledActivities: ScheduledActivity[] };
|
|
@@ -1526,7 +1526,7 @@ changes.
|
|
|
1526
1526
|
| `title` | Required agent-facing activity title. |
|
|
1527
1527
|
| `startsAt` | Required RFC-3339 start time with an explicit timezone. |
|
|
1528
1528
|
| `endsAt` | Optional RFC-3339 end time with an explicit timezone. |
|
|
1529
|
-
| `
|
|
1529
|
+
| `party` | The person the activity reaches — a callback's customer — as a `Contact`. Optional. |
|
|
1530
1530
|
| `attributes` | Optional ordered `Attribute` entries. Keys must be non-empty. |
|
|
1531
1531
|
|
|
1532
1532
|
**There is no `type` field**, for the reason there is none on `Contact`: an open category is
|
|
@@ -2071,7 +2071,7 @@ time. Runtime conformance checks also require the task channel to match its prov
|
|
|
2071
2071
|
| `taskType` | Required provider-defined source or category of work, such as a voice `Queue Name`, `Mailbox Folder`, `Chat Source`, `Support`, `Billing`, or `Returns`. |
|
|
2072
2072
|
| `capabilities` | Controls and workspace features available for this specific task. |
|
|
2073
2073
|
| `browsers` | Named browser definitions for the task workspace: at least one when the task declares the `browsers` capability, empty when it does not. |
|
|
2074
|
-
| `
|
|
2074
|
+
| `party` | The person or entity on the other end of this task, as a `Contact`: often a name and one address; a withheld caller ID may leave nothing to send at all. Optional. The party is who the task is *with*; `contacts` is the directory. |
|
|
2075
2075
|
| `phase` | Current canonical task phase: `pending`, `confirmed`, `preparing`, `in-progress`, `paused`, or `completing`. |
|
|
2076
2076
|
| `media` | Voice only. The task's real-time audio as the provider holds it: `started` while audio is attached, `ended` once it ended, omitted while none is. The provider's word — see **`task-media-started`**. |
|
|
2077
2077
|
| `reference` | Optional agent-facing reference such as a case, call, conversation, ticket, or message number. It is distinct from the protocol `id`. |
|
|
@@ -2091,7 +2091,7 @@ const attributes: TaskAttribute[] = [
|
|
|
2091
2091
|
key: "related-contact",
|
|
2092
2092
|
label: "Related contact",
|
|
2093
2093
|
type: "contact",
|
|
2094
|
-
|
|
2094
|
+
party: { name: "Asha Rao", number: "+919876543210" },
|
|
2095
2095
|
},
|
|
2096
2096
|
{
|
|
2097
2097
|
key: "answered",
|
|
@@ -2329,14 +2329,14 @@ Each `TaskBrowser` defines one named browser in the task workspace.
|
|
|
2329
2329
|
| `name` | Agent-facing tab label, unique within the task, and an input to schemes containing `TAB_NAME`. |
|
|
2330
2330
|
| `purpose` | Human-readable explanation of the browser's role. |
|
|
2331
2331
|
| `url` | Initial URL. Must use `http:` or `https:`; see below. Later navigation comes from Chromium. |
|
|
2332
|
-
| `
|
|
2333
|
-
| `isolationScheme` | **Required when `
|
|
2332
|
+
| `sharedSession` | Required. `false` creates a task-specific browser session. |
|
|
2333
|
+
| `isolationScheme` | **Required when `sharedSession` is `true`**, and rejected when it is `false`. There is no default: see below. |
|
|
2334
2334
|
| `urlVisibility` | What the agent sees of this tab's URL in Omni's chrome: `hidden`, `domain`, or `full`. Omitted, the URL shows as any browser's does; a provider says `hidden` where the URL carries what the agent may not read — a caller's number, a CRM token. Per browser, on the provider's word; Omni honours it tab by tab. |
|
|
2335
2335
|
|
|
2336
|
-
##### Choosing
|
|
2336
|
+
##### Choosing an isolation scheme
|
|
2337
2337
|
|
|
2338
2338
|
Every scheme is supported and the provider picks the one its deployment needs. There is no
|
|
2339
|
-
default, and a `
|
|
2339
|
+
default, and a `sharedSession: true` browser that declares none is invalid — the type will not compile
|
|
2340
2340
|
it and `validateSnapshot` reports `task.browser.isolationScheme.required`.
|
|
2341
2341
|
|
|
2342
2342
|
That is deliberate. Sharing a signed-in session decides **who else may see those credentials**,
|
|
@@ -2358,7 +2358,7 @@ page rather than following a disallowed URL, and `isAllowedBrowserUrl()` is the
|
|
|
2358
2358
|
|
|
2359
2359
|
##### Reuse and isolation
|
|
2360
2360
|
|
|
2361
|
-
With `
|
|
2361
|
+
With `sharedSession: true`, definitions producing the same isolation key share one **storage profile**:
|
|
2362
2362
|
cookies, local storage, session storage, permissions, and cached credentials. Different keys are
|
|
2363
2363
|
isolated from one another.
|
|
2364
2364
|
|
|
@@ -2382,7 +2382,7 @@ browsers: [
|
|
|
2382
2382
|
name: "CRM",
|
|
2383
2383
|
purpose: "Contact record",
|
|
2384
2384
|
url: "https://crm.example.com/contact/42",
|
|
2385
|
-
|
|
2385
|
+
sharedSession: true,
|
|
2386
2386
|
isolationScheme: BROWSER_ISOLATION_SCHEMES.PROVIDER_NAME__TASK_TYPE_NAME__TAB_NAME,
|
|
2387
2387
|
}
|
|
2388
2388
|
]
|
|
@@ -3580,7 +3580,7 @@ Publishes an agent-facing message. `text` is always required and is the accessib
|
|
|
3580
3580
|
Optional HTML is sanitized by Omni. `announcedAt` and optional `expiresAt` are RFC-3339 times with
|
|
3581
3581
|
explicit timezones.
|
|
3582
3582
|
|
|
3583
|
-
### `
|
|
3583
|
+
### `queue-summary`
|
|
3584
3584
|
|
|
3585
3585
|
Publishes the provider's current dashboard contribution. Omni combines only the latest summary from
|
|
3586
3586
|
each connected provider.
|
|
@@ -3742,7 +3742,7 @@ cannot be established from TypeScript structure alone.
|
|
|
3742
3742
|
| `assertBreakBeginsAfterTask(steps)` | A break asked for on a task is committed as `starting-after-task` while work remains and reaches `in-effect` only once nothing is outstanding — never beside a task, never later than the step that has none. |
|
|
3743
3743
|
| `assertDeniedAndRetriedBreak(states)` | A denial transitions directly to `not-requested`; a later request can still be granted. |
|
|
3744
3744
|
| `assertWrapTimeout(task, mediaEndedAt, deadline, toleranceMs?)` | The wrap deadline equals media end plus the task allowance, within a tolerance that defaults to 1000ms; a task with no allowance has no deadline, and one observed is the violation. |
|
|
3745
|
-
| `
|
|
3745
|
+
| `assertBrowserSessionIsolation(left, right, expected)` | Browser reuse follows only the declared isolation scheme. |
|
|
3746
3746
|
| `assertNoBrowserSessionKeyCollisions(scenarios)` | No two distinct scenarios derive the same session key. Feed it adversarial names. |
|
|
3747
3747
|
|
|
3748
3748
|
Adapters should run the relevant scenarios against deterministic test state before publishing.
|