@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 +21 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +17 -1
- package/dist/validation.js +13 -3
- package/guide/agent.md +1 -0
- package/guide.md +47 -11
- package/package.json +1 -1
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.
|
package/dist/validation.js
CHANGED
|
@@ -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
|
|
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()`
|
|
2141
|
-
session
|
|
2142
|
-
|
|
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
|
-
|
|
2147
|
-
`get`, `set`, and `delete`; adapters cannot enumerate another provider's secrets.
|
|
2148
|
-
|
|
2149
|
-
|
|
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
|
|