@xema/omni-protocol 0.1.54 → 0.1.55
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 +1 -1
- package/dist/index.js +1 -0
- package/dist/validation.d.ts +7 -0
- package/dist/validation.js +56 -8
- package/guide.md +22 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1394,7 +1394,7 @@ export interface ProviderEventEnvelope<C extends Channel = Channel> {
|
|
|
1394
1394
|
occurredAt: IsoTimestamp;
|
|
1395
1395
|
event: ProviderEvent<C>;
|
|
1396
1396
|
}
|
|
1397
|
-
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"];
|
|
1397
|
+
export declare const OMNI_FAILURE_CODES: readonly ["omni.not-authenticated", "omni.capability-not-enabled", "omni.task-not-found", "omni.destination-not-permitted", "omni.phone-not-permitted", "omni.rate-limited", "omni.unavailable", "omni.break-already-committed"];
|
|
1398
1398
|
export type OmniFailureCode = (typeof OMNI_FAILURE_CODES)[number];
|
|
1399
1399
|
export interface Connection<C extends Channel = Channel> {
|
|
1400
1400
|
snapshot(): Snapshot<C> | Promise<Snapshot<C>>;
|
package/dist/index.js
CHANGED
package/dist/validation.d.ts
CHANGED
|
@@ -91,4 +91,11 @@ export interface LoginValidationContext {
|
|
|
91
91
|
/** The level ids in force, from the manifest. The defaults when absent. */
|
|
92
92
|
levels?: readonly string[];
|
|
93
93
|
}
|
|
94
|
+
export declare function validateAuthenticationFailure(failure: unknown, path?: string): ProtocolViolation[];
|
|
95
|
+
/**
|
|
96
|
+
* What `start()` or `complete()` answered: a challenge or a rejection from `start`, a login or a
|
|
97
|
+
* rejection from `complete`. A rejection carries a failure the host can show, and a login is held
|
|
98
|
+
* to the same rules as any `authenticated` state.
|
|
99
|
+
*/
|
|
100
|
+
export declare function validateAuthenticationResult(result: unknown, method: "start" | "complete", path?: string, context?: LoginValidationContext): ProtocolViolation[];
|
|
94
101
|
export declare function validateAuthenticationState(state: unknown, path?: string, context?: LoginValidationContext): ProtocolViolation[];
|
package/dist/validation.js
CHANGED
|
@@ -2053,6 +2053,61 @@ function validateMonitorControl(value, path, into) {
|
|
|
2053
2053
|
});
|
|
2054
2054
|
into.require(value.includes("monitor"), "authentication.capability.team.monitorControl.monitor", path, "whisper and barge begin from a monitor: a lead who may listen in any mode may monitor");
|
|
2055
2055
|
}
|
|
2056
|
+
/**
|
|
2057
|
+
* A failure an authentication reports: on a `rejected` start or completion, or on an `expired`
|
|
2058
|
+
* state. A provider names its own codes freely; an `omni.` code is the contract's and has to be one
|
|
2059
|
+
* it lists. `omni.phone-not-permitted` is never retryable: the agent's station is administrator
|
|
2060
|
+
* configuration, and trying again does not reconfigure it.
|
|
2061
|
+
*/
|
|
2062
|
+
function validateAuthenticationFailureInto(value, path, into) {
|
|
2063
|
+
if (!isPlainObject(value)) {
|
|
2064
|
+
into.add("authentication.failure.shape", path, "a failure must be an object");
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2067
|
+
if (into.filled(value.code, "authentication.failure.code", `${path}.code`, "a failure needs a code") && value.code.startsWith("omni.")) {
|
|
2068
|
+
into.require(OMNI_FAILURE_CODES.includes(value.code), "failure.code.unknown", `${path}.code`, `${String(value.code)} is not a failure code this contract defines`);
|
|
2069
|
+
}
|
|
2070
|
+
into.filled(value.message, "authentication.failure.message", `${path}.message`, "a failure needs a message");
|
|
2071
|
+
into.require(typeof value.retryable === "boolean", "authentication.failure.retryable", `${path}.retryable`, "a failure must say whether it is retryable");
|
|
2072
|
+
if (value.code === "omni.phone-not-permitted") {
|
|
2073
|
+
into.require(value.retryable === false, "authentication.failure.phone.retryable", `${path}.retryable`, "a phone the platform does not permit for this agent is configuration: trying again does not change it, so the refusal is not retryable");
|
|
2074
|
+
}
|
|
2075
|
+
if (value.retryAfterMs !== undefined) {
|
|
2076
|
+
into.require(typeof value.retryAfterMs === "number" && value.retryAfterMs >= 0, "authentication.failure.retryAfterMs", `${path}.retryAfterMs`, "retryAfterMs must be a non-negative number when present");
|
|
2077
|
+
}
|
|
2078
|
+
if (value.field !== undefined)
|
|
2079
|
+
into.filled(value.field, "authentication.failure.field", `${path}.field`, "field names a declared credentials field when present");
|
|
2080
|
+
}
|
|
2081
|
+
export function validateAuthenticationFailure(failure, path = "failure") {
|
|
2082
|
+
const into = new Collector();
|
|
2083
|
+
validateAuthenticationFailureInto(failure, path, into);
|
|
2084
|
+
return into.violations;
|
|
2085
|
+
}
|
|
2086
|
+
/**
|
|
2087
|
+
* What `start()` or `complete()` answered: a challenge or a rejection from `start`, a login or a
|
|
2088
|
+
* rejection from `complete`. A rejection carries a failure the host can show, and a login is held
|
|
2089
|
+
* to the same rules as any `authenticated` state.
|
|
2090
|
+
*/
|
|
2091
|
+
export function validateAuthenticationResult(result, method, path = "result", context = {}) {
|
|
2092
|
+
const into = new Collector();
|
|
2093
|
+
if (!isPlainObject(result)) {
|
|
2094
|
+
into.add("authentication.result.shape", path, `${method} must answer an object`);
|
|
2095
|
+
return into.violations;
|
|
2096
|
+
}
|
|
2097
|
+
if (result.status === "rejected") {
|
|
2098
|
+
validateAuthenticationFailureInto(result.failure, `${path}.failure`, into);
|
|
2099
|
+
}
|
|
2100
|
+
else if (method === "start" && result.status === "interaction-required") {
|
|
2101
|
+
into.require(isPlainObject(result.challenge), "authentication.result.challenge", `${path}.challenge`, "interaction-required carries the challenge to put to the person");
|
|
2102
|
+
}
|
|
2103
|
+
else if (method === "complete" && result.status === "authenticated") {
|
|
2104
|
+
into.violations.push(...validateAuthenticationState(result, path, context));
|
|
2105
|
+
}
|
|
2106
|
+
else {
|
|
2107
|
+
into.add("authentication.result.status", `${path}.status`, `${method} answers ${method === "start" ? "interaction-required" : "authenticated"} or rejected, not ${String(result.status)}`);
|
|
2108
|
+
}
|
|
2109
|
+
return into.violations;
|
|
2110
|
+
}
|
|
2056
2111
|
export function validateAuthenticationState(state, path = "authentication", context = {}) {
|
|
2057
2112
|
const into = new Collector();
|
|
2058
2113
|
if (!isPlainObject(state)) {
|
|
@@ -2072,14 +2127,7 @@ export function validateAuthenticationState(state, path = "authentication", cont
|
|
|
2072
2127
|
if (state.identity !== undefined)
|
|
2073
2128
|
validateUser(state.identity, "authentication.identity", `${path}.identity`, into);
|
|
2074
2129
|
if (state.failure !== undefined) {
|
|
2075
|
-
|
|
2076
|
-
into.add("authentication.failure.shape", `${path}.failure`, "a failure must be an object when present");
|
|
2077
|
-
}
|
|
2078
|
-
else {
|
|
2079
|
-
into.filled(state.failure.code, "authentication.failure.code", `${path}.failure.code`, "a failure needs a code");
|
|
2080
|
-
into.filled(state.failure.message, "authentication.failure.message", `${path}.failure.message`, "a failure needs a message");
|
|
2081
|
-
into.require(typeof state.failure.retryable === "boolean", "authentication.failure.retryable", `${path}.failure.retryable`, "a failure must say whether it is retryable");
|
|
2082
|
-
}
|
|
2130
|
+
validateAuthenticationFailureInto(state.failure, `${path}.failure`, into);
|
|
2083
2131
|
}
|
|
2084
2132
|
}
|
|
2085
2133
|
else {
|
package/guide.md
CHANGED
|
@@ -1120,6 +1120,7 @@ const OMNI_FAILURE_CODES = [
|
|
|
1120
1120
|
"omni.capability-not-enabled",
|
|
1121
1121
|
"omni.task-not-found",
|
|
1122
1122
|
"omni.destination-not-permitted",
|
|
1123
|
+
"omni.phone-not-permitted",
|
|
1123
1124
|
"omni.rate-limited",
|
|
1124
1125
|
"omni.unavailable",
|
|
1125
1126
|
"omni.break-already-committed",
|
|
@@ -3799,6 +3800,21 @@ that is a telephone. The harness requires no `openMedia` of such an adapter and
|
|
|
3799
3800
|
this wire never asks the agent for it: a host declares `phone` and nothing more. What a platform
|
|
3800
3801
|
asks on surfaces of its own is its own decision.
|
|
3801
3802
|
|
|
3803
|
+
**A declared phone the platform does not permit for this agent is a login that cannot be
|
|
3804
|
+
established.** The manifest says what the platform can do; the agent's record, kept by an
|
|
3805
|
+
administrator, says what this agent is configured for, and the two can disagree with the host's
|
|
3806
|
+
declaration -- a softphone declared for an agent whose record says desk phone. Honouring that
|
|
3807
|
+
declaration on a switch that allows one registration per endpoint would evict the handset: the
|
|
3808
|
+
agent's phone stops ringing and their calls land in a tab, and nobody is told. So the provider
|
|
3809
|
+
refuses the login at authentication, with `omni.phone-not-permitted` and a message the host shows
|
|
3810
|
+
-- "this agent is configured for a desk phone" -- and refusing is correct, not an override of the
|
|
3811
|
+
host. It is a refusal, never a negotiation afterwards and never a quiet substitution, and it is
|
|
3812
|
+
never retryable: trying again does not reconfigure the agent, and a host validating the answer
|
|
3813
|
+
(`validateAuthenticationResult`) refuses a phone refusal marked otherwise
|
|
3814
|
+
(`authentication.failure.phone.retryable`). And **a
|
|
3815
|
+
provider never makes a declared phone true by changing the platform's configuration**: the record
|
|
3816
|
+
is the administrator's, and a login is not a request to reconfigure an agent.
|
|
3817
|
+
|
|
3802
3818
|
### Opening the audio
|
|
3803
3819
|
|
|
3804
3820
|
`openMedia` hands Omni the remote audio for one task. Every adapter whose manifest lists
|
|
@@ -3963,6 +3979,7 @@ react rather than only display the message:
|
|
|
3963
3979
|
| `omni.not-authenticated` | The provider session is no longer usable. The adapter has published `expired` at or before this answer — the state is what Omni surfaces reauthentication from; the code says why this action failed, and is never the only signal. |
|
|
3964
3980
|
| `omni.capability-not-enabled` | The action targets a capability this task, manifest, or login did not declare — including a lead command from a login whose `capabilities` no longer carry it. |
|
|
3965
3981
|
| `omni.task-not-found` | The provider-local task id is unknown, typically after the task already ended. |
|
|
3982
|
+
| `omni.phone-not-permitted` | The host declared a `phone` the platform does not permit for this agent -- a softphone for an agent configured for a desk phone, or the reverse. The login is refused at authentication, and the provider never reconfigures the agent to make the declaration true. See **How the agent hears the call**. |
|
|
3966
3983
|
| `omni.destination-not-permitted` | The dialled number, or the `destinationId` named, is not one the provider offers this agent. |
|
|
3967
3984
|
| `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
|
|
3968
3985
|
| `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `transport-status` is not `active`. |
|
|
@@ -4270,6 +4287,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
4270
4287
|
| `validateHostGuarantees(guarantees)` | What a host promises: only the guarantees this contract names, each declared by presence and never `false`. The harness validates the guarantees of whatever host a test hands the adapter. |
|
|
4271
4288
|
| `validateHandlingReport(report, path?, manifest?)` | What the host reports of a leg it performed, for an adapter to check before forwarding: a task, a step, when it began, a positive `seconds` where stated, and an explicit `ended` that carries the final duration. Given the manifest, a running report is refused unless it declares `runningStepReports`. |
|
|
4272
4289
|
| `validateHostReport(report)` | The host's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever host a test hands the adapter; `stillHost(report)` builds one that never changes. |
|
|
4290
|
+
| `validateAuthenticationResult(result, method)` | What `start()` or `complete()` answered: a challenge or a rejection, a login or a rejection. A rejection's failure is held to its rules -- an `omni.` code the contract lists, and `omni.phone-not-permitted` never retryable, since the agent's station is configuration. `validateAuthenticationFailure(failure)` is the same check on a failure alone. |
|
|
4273
4291
|
| `validateTaskCommand(command, task?)` | What a command needs to be issuable, against the task it names: its own shape -- a dial's `dialId`, a transfer's item, a remove naming exactly one person -- and, with the task, the capability the table above gates it on (`command.capability.<name>`, `.locked`), the phase it belongs to (`command.phase.*`), and the state that has to stand: a consulted entry, a lead requested, somebody else still on the call (`command.conference.remove.alone`). A host validates before sending and an adapter before acting. |
|
|
4274
4292
|
| `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
|
|
4275
4293
|
| `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. Omni applies it to every state a session publishes — the republished as much as the first. |
|
|
@@ -4358,6 +4376,10 @@ assertReached(driven, ["task.onCall", "task.media", "event.task-ended"]);
|
|
|
4358
4376
|
```
|
|
4359
4377
|
`assertReached(result, subjects)` is the paired assertion: it throws naming every subject the run
|
|
4360
4378
|
never met, so a test that meant to check a roster cannot pass on a fixture that never produced one.
|
|
4379
|
+
It reads like a guarantee and is a claim the adopter keeps making: it catches an adapter that
|
|
4380
|
+
stopped reaching a subject, not a list that stopped asking, so a list can rot to nothing and stay
|
|
4381
|
+
green. Keep it honest with a control beside it -- one subject the run genuinely cannot reach,
|
|
4382
|
+
asserted to throw -- so the assertion is shown to be looking rather than agreeing.
|
|
4361
4383
|
|
|
4362
4384
|
Three properties of the harness matter to adapter authors:
|
|
4363
4385
|
|