@xema/omni-protocol 0.1.116 → 0.1.117

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
@@ -246,7 +246,8 @@ export interface SecretStore {
246
246
  * A small store for one login's operational state -- what an adapter holds about open work that
247
247
  * its platform cannot hold for it, such as the interaction legs a host reported. Kept by the host for
248
248
  * the life of the login, across a reload of the host, and cleared at sign-out. Never for anything
249
- * sensitive: that is `SecretStore`, whose contract is that a host may clear it aggressively.
249
+ * sensitive: that is `SecretStore`, whose contract is that a host may clear it aggressively, and
250
+ * clears it once the session has ended.
250
251
  */
251
252
  export interface LoginStore {
252
253
  get(key: string): Promise<string | undefined>;
@@ -318,6 +319,13 @@ export interface UserCapabilities {
318
319
  * Only `authenticated` and `refreshing` know who the agent is and what they may do, and only
319
320
  * `authenticated` has something to expire. A state carrying more than it knows is a state Omni
320
321
  * would render as fact.
322
+ *
323
+ * `signed-out` is the agent's own Sign out on this agent computer, and nothing else. `terminated`
324
+ * is a session ended from outside it -- a supervisor, the agent on another screen, the platform
325
+ * itself -- and says who: a person by id, or `provider` where no person was behind it, with the
326
+ * reason as a failure that is never retryable. `expired` is a session whose time ran out or whose
327
+ * refresh is failing. The stored secrets go with every ending: signed out, terminated, and expired
328
+ * unless its failure says `retryable: true`.
321
329
  */
322
330
  export type AuthenticationState = {
323
331
  status: "signed-out";
@@ -336,6 +344,13 @@ export type AuthenticationState = {
336
344
  status: "expired";
337
345
  identity?: User;
338
346
  failure?: AuthenticationFailure;
347
+ } | {
348
+ status: "terminated";
349
+ by: UserId | "provider";
350
+ identity?: User;
351
+ failure: AuthenticationFailure & {
352
+ retryable: false;
353
+ };
339
354
  };
340
355
  export interface CredentialField {
341
356
  name: string;
@@ -401,6 +416,11 @@ export interface AuthenticationSession {
401
416
  complete(request: CompleteAuthenticationRequest): Promise<CompleteAuthenticationResult>;
402
417
  /** Cancels an abandoned SSO window or credentials form. */
403
418
  cancelAuthentication(flowId: string): Promise<AuthenticationActionResult>;
419
+ /**
420
+ * The agent's own Sign out. Tells the provider where it can, deletes the stored secrets whatever
421
+ * happened, and moves to `signed-out`; answers `failed` when the provider could not be told, so
422
+ * the host can show that a session may remain there. Never called for a `terminated` session.
423
+ */
404
424
  signOut(): Promise<AuthenticationActionResult>;
405
425
  close(): Promise<void>;
406
426
  }
package/dist/testing.d.ts CHANGED
@@ -87,7 +87,7 @@ export declare function assertAuthenticationRestoreAndExpiry(states: readonly Au
87
87
  * Capabilities are current, not fixed. A provider that withdraws one republishes `authenticated`
88
88
  * with the new set, and the next snapshot agrees with it. `states` is what the authentication
89
89
  * session published, first to last: beginning and ending `authenticated` for the same identity,
90
- * passing only through usable states (`expired` or `signed-out` ends the login instead), with at
90
+ * passing only through usable states (`expired`, `terminated` or `signed-out` ends the login instead), with at
91
91
  * least one capability gone by the end. `snapshot` is the first snapshot published after the last
92
92
  * state, validated against that login -- so a team member list still published to a login that no longer
93
93
  * leads, or requests to one that may no longer join, is the failure.
package/dist/testing.js CHANGED
@@ -309,6 +309,17 @@ export async function testAdapter(adapter, context, options = {}) {
309
309
  violations.push(...own);
310
310
  if (own.length > 0)
311
311
  return;
312
+ // A session that has ended takes its secrets with it: the adapter deletes them before it
313
+ // publishes the ending, so a reload has nothing to restore. Only an expiry whose failure is
314
+ // retryable keeps them, for the retry.
315
+ if (sessionEnded(state)) {
316
+ ruleTested("authentication.secrets.retained");
317
+ if (storedSecrets.size > 0) {
318
+ violations.push({ rule: "authentication.secrets.retained", path: "authentication.status",
319
+ message: `the session is ${state.status} and the adapter still holds ${storedSecrets.size} stored secret(s): ` +
320
+ "an ended session's secrets are deleted before the ending is published, or a reload restores it" });
321
+ }
322
+ }
312
323
  if (state.status === "refreshing") {
313
324
  violations.push(...refreshingCarriesOver(current(), state, "authentication"));
314
325
  }
@@ -904,6 +915,11 @@ export function assertAuthenticationRestoreAndExpiry(states) {
904
915
  }
905
916
  }
906
917
  /** The tasks an envelope carries: the one a task event names, or a snapshot event's list. */
918
+ /** Whether a published state ends the session, so its stored secrets must already be gone. */
919
+ function sessionEnded(state) {
920
+ return state.status === "signed-out" || state.status === "terminated"
921
+ || (state.status === "expired" && state.failure?.retryable !== true);
922
+ }
907
923
  /** Whether a store key names the task: the id as a delimited token, never as a run of characters inside another id. */
908
924
  function namesTask(key, taskId) {
909
925
  return key.split(/[^A-Za-z0-9_-]+/).includes(taskId);
@@ -940,7 +956,7 @@ function notYetReadTasks(tasks, path) {
940
956
  * Capabilities are current, not fixed. A provider that withdraws one republishes `authenticated`
941
957
  * with the new set, and the next snapshot agrees with it. `states` is what the authentication
942
958
  * session published, first to last: beginning and ending `authenticated` for the same identity,
943
- * passing only through usable states (`expired` or `signed-out` ends the login instead), with at
959
+ * passing only through usable states (`expired`, `terminated` or `signed-out` ends the login instead), with at
944
960
  * least one capability gone by the end. `snapshot` is the first snapshot published after the last
945
961
  * state, validated against that login -- so a team member list still published to a login that no longer
946
962
  * leads, or requests to one that may no longer join, is the failure.
@@ -49,7 +49,7 @@ const TRANSPORT_STATUSES = membersOf({ connecting: true, active: true, error: tr
49
49
  const AUTHENTICATION_METHODS = membersOf({ "browser-sso": true, credentials: true });
50
50
  const AUTHENTICATION_BROWSERS = membersOf({ system: true, omni: true });
51
51
  const AUTHENTICATION_STATUSES = membersOf({
52
- "signed-out": true, authenticating: true, authenticated: true, refreshing: true, expired: true,
52
+ "signed-out": true, authenticating: true, authenticated: true, refreshing: true, expired: true, terminated: true,
53
53
  });
54
54
  const BREAK_APPROVALS = membersOf({
55
55
  "not-requested": true, "awaiting-approval": true, granted: true, "starting-after-task": true, "on-break": true,
@@ -3131,18 +3131,28 @@ export function validateAuthenticationState(state, path = "authentication", cont
3131
3131
  validateUser(state.identity, "authentication.identity", `${path}.identity`, into);
3132
3132
  validateUserCapabilitiesInto(state.capabilities, `${path}.capabilities`, into, context.levels);
3133
3133
  }
3134
- else if (state.status === "expired") {
3134
+ else if (state.status === "expired" || state.status === "terminated") {
3135
3135
  if (state.identity !== undefined)
3136
3136
  validateUser(state.identity, "authentication.identity", `${path}.identity`, into);
3137
3137
  if (state.failure !== undefined) {
3138
3138
  validateAuthenticationFailureInto(state.failure, `${path}.failure`, into);
3139
3139
  }
3140
+ if (state.status === "terminated") {
3141
+ // Ended from outside this agent computer: who did it, and why, and nothing to retry.
3142
+ into.require(isUserId(state.by) || state.by === "provider", "authentication.terminated.by", `${path}.by`, "a terminated session says who ended it: the person by user id, or provider where the platform did");
3143
+ if (into.require(isPlainObject(state.failure), "authentication.terminated.failure", `${path}.failure`, "a terminated session carries the reason it was ended")) {
3144
+ into.require(state.failure.retryable === false, "authentication.terminated.retryable", `${path}.failure.retryable`, "a session ended from outside is over: its failure is not retryable");
3145
+ }
3146
+ }
3140
3147
  }
3141
3148
  else {
3142
3149
  into.require(state.identity === undefined, "authentication.identity.unexpected", `${path}.identity`, `${state.status} must not carry an identity`);
3143
3150
  }
3144
3151
  if (state.failure !== undefined) {
3145
- into.require(state.status === "expired", "authentication.failure.unexpected", `${path}.failure`, "only an expired state may carry a failure");
3152
+ into.require(state.status === "expired" || state.status === "terminated", "authentication.failure.unexpected", `${path}.failure`, "only an expired or terminated state may carry a failure");
3153
+ }
3154
+ if (state.by !== undefined) {
3155
+ into.require(state.status === "terminated", "authentication.by.unexpected", `${path}.by`, "only a terminated session says who ended it; the agent's own sign-out is signed-out");
3146
3156
  }
3147
3157
  if (state.expiresAt !== undefined) {
3148
3158
  into.require(state.status === "authenticated", "authentication.expiresAt.unexpected", `${path}.expiresAt`, "only an authenticated state may carry an expiry");
package/guide/agent.md CHANGED
@@ -388,6 +388,7 @@ Migration from the earlier spellings:
388
388
  | the handset spelled by its place on a desk | `hardphone`, on the manifest's `phones` and the login's `phone`; the former spelling is refused |
389
389
  | the desk | the agent computer, in the guide's prose |
390
390
  | the agent application's Mute on hold | unavailable while the task is `paused`, promised at login by a softphone login's agent application (`host.guarantees.muteUnavailableOnHold`, required there and refused of a hardphone login); a host mute beginning on a paused task is refused (`historyReport.muted.held`) and the test names an adapter that records one (`test.recordStep.held`) |
391
+ | a session ended from outside the agent computer, reported as `expired` | `terminated`, with `by` (a person or `provider`) and a reason never retryable; `signed-out` is only the agent's own Sign out here; an ended session's secrets are deleted before the ending is published (`authentication.secrets.retained`), kept only for an `expired` whose failure is retryable; the agent application clears the store after, and resumes no stored token |
391
392
 
392
393
  Update producers, consumers, saved task snapshots, and validation-rule assertions together.
393
394
  History and report rule names use `history` and `historyReport`; assignment rules use
package/guide.md CHANGED
@@ -70,7 +70,7 @@ state use `status`; name the object when discussing them to avoid ambiguity.
70
70
  | `phase` | A task | `pending`, `confirmed`, `preview`, `in-progress`, `paused`, `completing` |
71
71
  | `audio` | A task's audio | `started`, `ended` |
72
72
  | `transport` | A connection | `connecting`, `active`, `error` |
73
- | `status` | An authentication session | `signed-out`, `authenticating`, `authenticated`, `refreshing`, `expired` |
73
+ | `status` | An authentication session | `signed-out`, `authenticating`, `authenticated`, `refreshing`, `expired`, `terminated` |
74
74
  | `status` | The agent’s break lifecycle | `not-requested`, `awaiting-approval`, `granted`, `starting-after-task`, `on-break` |
75
75
  | `availability` | A team member | `ready`, `on-task`, `on-break`, `elsewhere`, `signed-out` |
76
76
 
@@ -301,7 +301,8 @@ type AuthenticationState =
301
301
  | { status: "authenticating" }
302
302
  | { status: "authenticated"; identity: User; capabilities: UserCapabilities; expiresAt?: IsoTimestamp }
303
303
  | { status: "refreshing"; identity: User; capabilities: UserCapabilities }
304
- | { status: "expired"; identity?: User; failure?: AuthenticationFailure };
304
+ | { status: "expired"; identity?: User; failure?: AuthenticationFailure }
305
+ | { status: "terminated"; by: UserId | "provider"; identity?: User; failure: AuthenticationFailure & { retryable: false } };
305
306
 
306
307
  type AuthenticationFailure = {
307
308
  code: string;
@@ -1991,11 +1992,12 @@ reports later changes.
1991
1992
 
1992
1993
  | Status | Contract |
1993
1994
  | --- | --- |
1994
- | `signed-out` | No usable provider session exists. |
1995
+ | `signed-out` | No usable provider session exists, because the agent signed out on this agent computer, or never signed in. Only the agent's own Sign out here leads to it. |
1995
1996
  | `authenticating` | An interactive `browser-sso` or `credentials` flow is active. |
1996
1997
  | `authenticated` | A usable session exists. Includes the provider identity, the login's `capabilities`, and optional token expiry time. |
1997
1998
  | `refreshing` | The adapter is refreshing its session. Existing provider identity and capabilities remain available, unchanged: a change to either is published as `authenticated`. |
1998
- | `expired` | The session cannot currently be used. It may include an identity and typed failure. |
1999
+ | `expired` | The session's time ran out, or its refresh is failing. It may include an identity and typed failure. The agent signs in again on the same login, and the workspace stays. |
2000
+ | `terminated` | The session was ended from outside this agent computer: by a supervisor, by the agent on another screen, or by the platform itself. `by` says who, a person by `UserId` or `provider` where no person was behind it (`authentication.terminated.by`), and `failure` says why and is never retryable (`authentication.terminated.failure`, `.retryable`). It is never reported as `expired`, which would hide who ended it, nor as `signed-out`, which is the agent's own act. |
1999
2001
 
2000
2002
  Omni calls `connect()` only after authentication reaches `authenticated`. Token refresh remains
2001
2003
  adapter-owned; the adapter publishes `refreshing`, followed by `authenticated` or `expired`.
@@ -2137,16 +2139,50 @@ authorization codes, tokens, or provider responses containing secrets.
2137
2139
 
2138
2140
  ### Sign-out
2139
2141
 
2140
- `signOut()` revokes or invalidates the provider session where supported, deletes stored
2141
- session secrets, and moves state to `signed-out`.
2142
- `close()` stops authentication-state observation but does not sign the agent out.
2142
+ `signOut()` is the agent's own Sign out on this agent computer. It tells the provider where
2143
+ supported, revoking or invalidating the session, then **deletes the stored session secrets
2144
+ whatever happened**, and moves state to `signed-out`. Telling the provider may be retried within a
2145
+ short bound the adapter states, and then given up: a provider that cannot be reached never keeps a
2146
+ token on the agent's machine. If it was not told, `signOut()` still deletes and still reaches
2147
+ `signed-out`, and answers `failed`, so the agent application can show that a session may remain at
2148
+ the provider. `close()` stops authentication-state observation but does not sign the agent out.
2149
+
2150
+ **The final moments are the agent application's to finish.** Its own steps before signing out --
2151
+ stopping capacity, putting tasks down -- may be retried within a short bound it states, and then
2152
+ given up; a step that failed never skips `signOut()`. A desk that skipped the adapter's sign-out
2153
+ because its own retirement failed left the token behind, and the next reload restored the session.
2154
+
2155
+ ### How a session's secrets end
2156
+
2157
+ A token that can no longer start a session does not outlive it. The adapter deletes the stored
2158
+ secrets **before** it publishes the ending, so that nothing is left for a reload to restore:
2159
+
2160
+ | Ending | Secrets | The provider |
2161
+ | --- | --- | --- |
2162
+ | `signed-out` | Deleted, whether or not the provider was told. | Told where supported, as above. |
2163
+ | `terminated` | Deleted. | Never told: the session is already over, and a sign-out sent now could end the agent's newer session somewhere else. |
2164
+ | `expired` with a failure that says `retryable: true` | Kept, for the retry: a network outage does not sign out a floor. | Retried by the adapter. |
2165
+ | `expired` otherwise | Deleted: the provider refused the token, or its time ran out. | Not told. |
2166
+
2167
+ **The agent application makes sure after the adapter.** Once a session is `signed-out`,
2168
+ `terminated`, or `expired` with anything but a retryable failure, the agent application clears that
2169
+ provider's partition of the store, and a sign-in after it always starts a fresh flow and never
2170
+ resumes a stored token. The adapter deletes; the agent application makes sure. `testAdapter` holds
2171
+ the adapter's half: when a published state ends the session, the store it handed the adapter must
2172
+ already be empty (`authentication.secrets.retained`).
2143
2173
 
2144
2174
  ### Secure-storage boundary
2145
2175
 
2146
- Omni provides an OS-backed `SecretStore` scoped to the provider manifest ID. It exposes only
2147
- `get`, `set`, and `delete`; adapters cannot enumerate another provider's secrets. Adapters may store
2148
- refresh tokens or equivalent session material, but never raw submitted credentials. Secrets must
2149
- not appear in manifests, logs, events, snapshots, task attributes, errors, or browser storage.
2176
+ The agent application provides the `SecretStore`, scoped to the provider manifest ID. It exposes
2177
+ only `get`, `set`, and `delete`; adapters cannot enumerate another provider's secrets. **What backs
2178
+ it is the agent application's, and depends on what it is**: a native agent application uses the
2179
+ operating system's keychain; a browser agent application has none, and keeps the store in the
2180
+ tab's session storage, which survives a reload and ends with the tab. A browser store is readable
2181
+ by any script running on its page, so a browser agent application is only as safe as its own page.
2182
+
2183
+ Adapters may store refresh tokens or equivalent session material, but never raw submitted
2184
+ credentials, and they put a secret nowhere but the `SecretStore`: never in manifests, logs,
2185
+ events, snapshots, task attributes, errors, the login's `store`, or browser storage of their own.
2150
2186
 
2151
2187
  ## Connecting to a provider
2152
2188
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.116",
3
+ "version": "0.1.117",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",