@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 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
@@ -102,6 +102,7 @@ export const OMNI_FAILURE_CODES = [
102
102
  "omni.capability-not-enabled",
103
103
  "omni.task-not-found",
104
104
  "omni.destination-not-permitted",
105
+ "omni.phone-not-permitted",
105
106
  "omni.rate-limited",
106
107
  "omni.unavailable",
107
108
  "omni.break-already-committed",
@@ -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[];
@@ -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
- if (!isPlainObject(state.failure)) {
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.54",
3
+ "version": "0.1.55",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",