@xema/omni-protocol 0.1.54 → 0.1.56

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",
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostGuarantees, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
1
+ import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type Task, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostGuarantees, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
2
2
  import { type ProtocolViolation } from "./validation.js";
3
3
  export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation } from "./validation.js";
4
4
  /**
@@ -84,6 +84,20 @@ export declare function assertCommandRefusedAfterWithdrawal(result: {
84
84
  code: string;
85
85
  };
86
86
  }): void;
87
+ /**
88
+ * A task's capabilities are current, not fixed. A provider whose platform withdraws a permission
89
+ * mid-task republishes the task with the set as it now stands, and a command under the withdrawn
90
+ * capability is refused from then on. `tasks` is what the provider published for one task, first
91
+ * to last -- the offer, then each republish -- every one validated against the manifest, all the
92
+ * same id, with at least one capability offered by the first absent from the last. A locked
93
+ * control is present without permission, not withdrawn: it is still drawn, so it does not count.
94
+ * `command` is the command a host issues under a withdrawn capability: it must be clean against
95
+ * the first task and refused against the last for want of that capability and for nothing else.
96
+ * That pairing is what proves the refusal comes from the withdrawal rather than from a command
97
+ * that was never issuable. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's
98
+ * answer to the same command.
99
+ */
100
+ export declare function assertTaskCapabilityWithdrawal(tasks: readonly Task[], manifest: Manifest, command: Record<string, unknown>): void;
87
101
  /**
88
102
  * Throws unless the run reached every subject named: the paired assertion beside a clean result,
89
103
  * so a fixture that never produced a roster cannot pass a test that meant to check one.
package/dist/testing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { browserSessionKey, effectiveLevels, sameCapabilities, } from "./index.js";
2
- import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateManifest, validateResult, validateSnapshot, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
2
+ import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostGuarantees, validateHostReport, validateManifest, validateResult, validateSnapshot, validateTask, validateTimeZone, validatePhone, validateTaskCommand, isTimeZone, sameTimeZone, } from "./validation.js";
3
3
  export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
4
4
  /**
5
5
  * A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
@@ -575,6 +575,54 @@ export function assertCommandRefusedAfterWithdrawal(result) {
575
575
  throw new Error(`A command after its capability was withdrawn fails with omni.capability-not-enabled, not ${result.failure?.code}`);
576
576
  }
577
577
  }
578
+ /**
579
+ * A task's capabilities are current, not fixed. A provider whose platform withdraws a permission
580
+ * mid-task republishes the task with the set as it now stands, and a command under the withdrawn
581
+ * capability is refused from then on. `tasks` is what the provider published for one task, first
582
+ * to last -- the offer, then each republish -- every one validated against the manifest, all the
583
+ * same id, with at least one capability offered by the first absent from the last. A locked
584
+ * control is present without permission, not withdrawn: it is still drawn, so it does not count.
585
+ * `command` is the command a host issues under a withdrawn capability: it must be clean against
586
+ * the first task and refused against the last for want of that capability and for nothing else.
587
+ * That pairing is what proves the refusal comes from the withdrawal rather than from a command
588
+ * that was never issuable. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's
589
+ * answer to the same command.
590
+ */
591
+ export function assertTaskCapabilityWithdrawal(tasks, manifest, command) {
592
+ const first = tasks[0];
593
+ const last = tasks.at(-1);
594
+ if (tasks.length < 2 || first === undefined || last === undefined) {
595
+ throw new Error("Task capability withdrawal needs the task as offered and at least one republish of it");
596
+ }
597
+ const context = {
598
+ channel: manifest.channel,
599
+ levels: effectiveLevels(manifest.orgLevels).map(level => level.id),
600
+ dialOutcomesDeclared: manifest.dialOutcomes !== undefined,
601
+ };
602
+ tasks.forEach((task, index) => {
603
+ assertNoViolations(validateTask(task, context, `tasks[${index}]`), `Task capability withdrawal: tasks[${index}] must be a task the wire could carry`);
604
+ if (task.id !== first.id) {
605
+ throw new Error(`Task capability withdrawal must keep the task: tasks[${index}] is ${task.id}, and the offer was ${first.id}`);
606
+ }
607
+ });
608
+ const withdrawn = Object.keys(first.capabilities).filter(name => last.capabilities[name] === undefined);
609
+ if (withdrawn.length === 0) {
610
+ throw new Error("Task capability withdrawal must end with at least one capability the offer declared withdrawn; a locked control is present without permission, not withdrawn");
611
+ }
612
+ const before = validateTaskCommand(command, first, "command");
613
+ if (before.length > 0) {
614
+ throw new Error(`Task capability withdrawal: the command must be issuable against the task as offered, or its later refusal proves nothing (${before.map(v => v.rule).join(", ")})`);
615
+ }
616
+ const after = validateTaskCommand(command, last, "command");
617
+ const forWant = after.filter(v => withdrawn.some(name => v.rule === `command.capability.${name}`));
618
+ if (forWant.length === 0) {
619
+ throw new Error(`Task capability withdrawal: the command must be refused against the republished task for want of ${withdrawn.join(" or ")}; it was ${after.length === 0 ? "accepted" : `refused for ${after.map(v => v.rule).join(", ")}`}`);
620
+ }
621
+ const other = after.filter(v => !forWant.includes(v));
622
+ if (other.length > 0) {
623
+ throw new Error(`Task capability withdrawal: the republished task refuses the command for more than the withdrawal (${other.map(v => v.rule).join(", ")}); change one thing at a time`);
624
+ }
625
+ }
578
626
  /**
579
627
  * Throws unless the run reached every subject named: the paired assertion beside a clean result,
580
628
  * so a fixture that never produced a roster cannot pass a test that meant to check one.
@@ -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",
@@ -2663,6 +2664,44 @@ browser that used it.
2663
2664
  Task capabilities belong to each `Task`. If `hold` is omitted on one task, Omni
2664
2665
  must not show or issue hold for that task even if another task from the same provider supports it.
2665
2666
 
2667
+ **A capability is a property of the task, and of nothing the task names.** A provider derives it
2668
+ from no other source -- not the queue the task came from, not the login, not its own configuration
2669
+ -- because each of those answers a question about this task from somewhere that does not know
2670
+ about this task, and a client with two sources and a rule for choosing between them is the shape
2671
+ that produces two consumers disagreeing about one fact. The task is the one source, and the
2672
+ provider puts on the task what the platform permits for it.
2673
+
2674
+ **A permission that changes while the task is open is republished on the task at the moment it
2675
+ changes.** An agent on a billing dispute may refund up to their own limit and no further. The
2676
+ customer wants more, the agent asks for a lead, and the moment the lead joins the call the task is
2677
+ republished with the Refund control the agent could not have a minute ago; when the lead leaves, it
2678
+ is republished without it. Nothing about the agent changed -- who is on the call did -- and the
2679
+ task said so both times, at the moment it became true. A call moves queues the same way: a caller
2680
+ identified as a priority customer is transferred to the priority queue, and the task arrives under
2681
+ that queue's terms -- a longer wrap allowance, a discount control the general queue never offered --
2682
+ restated on the task at the hand-over, not inferred from the queue it left. A capability stated
2683
+ once at offer or answer and never corrected is a fact with a shelf life and no expiry, and a host
2684
+ draws a control the provider will now refuse -- or withholds one the agent now has. So the provider republishes
2685
+ the task, with its capabilities as they now stand, and a host treats the last statement as current
2686
+ rather than re-deriving anything. This is the same shape as a room left full after the call ends
2687
+ -- a field describing the present, carried past the moment it stopped being true -- carried past
2688
+ this time not by a spread but by nobody sending the correction. A provider whose platform can
2689
+ change a permission mid-task and does not republish is in breach, however conformant its offer was.
2690
+ A capability set can shrink as well as grow, and a host that has only ever seen it grow meets a
2691
+ control that was there a moment ago and is gone; a command that arrives after its capability was
2692
+ withdrawn is refused with a reason, never acted on. The asymmetry decides which direction a
2693
+ provider gets right first: a control gained late is a nicety, a control withdrawn and not
2694
+ republished is a button that fails when pressed. `assertTaskCapabilityWithdrawal` is the test for that
2695
+ direction: the task as offered and as republished, and one command that was issuable under the
2696
+ first and is refused under the last for want of the capability withdrawn -- and nothing else.
2697
+
2698
+ **An empty capability set is a statement, not a shrug.** `capabilities: {}` says the platform
2699
+ permits nothing on this task. A provider that has not yet learned what the platform permits -- a
2700
+ queue's configuration that has not reached it -- knows nothing of the kind, and must not publish
2701
+ the task as if it did: it holds the task until it knows, or reports the gap as a `diagnostic`,
2702
+ because "no queue governs this call" is a fact and "the configuration has not arrived" is a fault,
2703
+ and a host cannot tell them apart from a value that carries neither.
2704
+
2666
2705
  ```ts
2667
2706
  const taskCapabilities = {
2668
2707
  channel: "voice",
@@ -3799,6 +3838,21 @@ that is a telephone. The harness requires no `openMedia` of such an adapter and
3799
3838
  this wire never asks the agent for it: a host declares `phone` and nothing more. What a platform
3800
3839
  asks on surfaces of its own is its own decision.
3801
3840
 
3841
+ **A declared phone the platform does not permit for this agent is a login that cannot be
3842
+ established.** The manifest says what the platform can do; the agent's record, kept by an
3843
+ administrator, says what this agent is configured for, and the two can disagree with the host's
3844
+ declaration -- a softphone declared for an agent whose record says desk phone. Honouring that
3845
+ declaration on a switch that allows one registration per endpoint would evict the handset: the
3846
+ agent's phone stops ringing and their calls land in a tab, and nobody is told. So the provider
3847
+ refuses the login at authentication, with `omni.phone-not-permitted` and a message the host shows
3848
+ -- "this agent is configured for a desk phone" -- and refusing is correct, not an override of the
3849
+ host. It is a refusal, never a negotiation afterwards and never a quiet substitution, and it is
3850
+ never retryable: trying again does not reconfigure the agent, and a host validating the answer
3851
+ (`validateAuthenticationResult`) refuses a phone refusal marked otherwise
3852
+ (`authentication.failure.phone.retryable`). And **a
3853
+ provider never makes a declared phone true by changing the platform's configuration**: the record
3854
+ is the administrator's, and a login is not a request to reconfigure an agent.
3855
+
3802
3856
  ### Opening the audio
3803
3857
 
3804
3858
  `openMedia` hands Omni the remote audio for one task. Every adapter whose manifest lists
@@ -3963,6 +4017,7 @@ react rather than only display the message:
3963
4017
  | `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
4018
  | `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
4019
  | `omni.task-not-found` | The provider-local task id is unknown, typically after the task already ended. |
4020
+ | `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
4021
  | `omni.destination-not-permitted` | The dialled number, or the `destinationId` named, is not one the provider offers this agent. |
3967
4022
  | `omni.rate-limited` | The action was throttled. Pair with `retryAfterMs`. |
3968
4023
  | `omni.unavailable` | The provider is temporarily unable to serve the action, including any command sent while `transport-status` is not `active`. |
@@ -4270,6 +4325,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
4270
4325
  | `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
4326
  | `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
4327
  | `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. |
4328
+ | `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
4329
  | `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
4330
  | `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
4331
  | `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 +4414,10 @@ assertReached(driven, ["task.onCall", "task.media", "event.task-ended"]);
4358
4414
  ```
4359
4415
  `assertReached(result, subjects)` is the paired assertion: it throws naming every subject the run
4360
4416
  never met, so a test that meant to check a roster cannot pass on a fixture that never produced one.
4417
+ It reads like a guarantee and is a claim the adopter keeps making: it catches an adapter that
4418
+ stopped reaching a subject, not a list that stopped asking, so a list can rot to nothing and stay
4419
+ green. Keep it honest with a control beside it -- one subject the run genuinely cannot reach,
4420
+ asserted to throw -- so the assertion is shown to be looking rather than agreeing.
4361
4421
 
4362
4422
  Three properties of the harness matter to adapter authors:
4363
4423
 
@@ -4379,6 +4439,7 @@ cannot be established from TypeScript structure alone.
4379
4439
  | Helper | Contract checked |
4380
4440
  | --- | --- |
4381
4441
  | `assertCapabilityWithdrawal(states, snapshot, manifest)` | A capability withdrawn by a later `authenticated` state is gone from the next snapshot: no roster for a login that no longer leads, no requests for one that may no longer join. Every state is validated on the way, `refreshing` must carry the login over, and the sequence passes only through usable states. |
4442
+ | `assertTaskCapabilityWithdrawal(tasks, manifest, command)` | A capability withdrawn by a republish of the task is gone from the task: every task in the sequence is validated, all carry the offer's id, at least one capability the offer declared is absent at the end (a locked control is present, not withdrawn), and `command` is clean against the first task and refused against the last for want of a withdrawn capability and nothing else. Pair it with `assertCommandRefusedAfterWithdrawal` on the provider's answer. |
4382
4443
  | `assertCommandRefusedAfterWithdrawal(result)` | A command that arrives after its capability was withdrawn fails with `omni.capability-not-enabled`, named by the provider. The same assertion serves a command the provider never supported under a capability it declares. |
4383
4444
  | `assertReached(result, subjects)` | The exercise met every subject named; throws listing those it did not. Pair it with a clean `exerciseAdapter` result. |
4384
4445
  | `assertAuthenticationRestoreAndExpiry(states)` | A restored authenticated session can refresh and ends in expiry. Every state is validated. |
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.56",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",