@poppykit/server 0.0.0-stage → 0.1.0
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/API.md +21 -0
- package/AUTH.md +60 -0
- package/CHANGELOG.md +22 -0
- package/CONTRIBUTING.md +50 -0
- package/DELIVERY.md +113 -0
- package/INTEGRATIONS.md +84 -0
- package/LICENSE +21 -0
- package/README.md +116 -2
- package/RELEASING.md +193 -0
- package/SECURITY.md +13 -0
- package/STORAGE.md +119 -0
- package/dist/adapter-contract.d.ts +12 -0
- package/dist/adapter-contract.js +140 -0
- package/dist/auth/authenticator.d.ts +6 -0
- package/dist/auth/authenticator.js +51 -0
- package/dist/auth/authority.d.ts +55 -0
- package/dist/auth/authority.js +16 -0
- package/dist/auth/dpop.d.ts +15 -0
- package/dist/auth/dpop.js +49 -0
- package/dist/browser.d.ts +44 -0
- package/dist/browser.js +110 -0
- package/dist/conversation/service.d.ts +126 -0
- package/dist/conversation/service.js +754 -0
- package/dist/conversation/store.d.ts +26 -0
- package/dist/conversation/store.js +14 -0
- package/dist/feedback.d.ts +13 -0
- package/dist/feedback.js +44 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +7 -0
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +14 -0
- package/dist/network/discovery.d.ts +101 -0
- package/dist/network/discovery.js +276 -0
- package/dist/network/transport.d.ts +31 -0
- package/dist/network/transport.js +305 -0
- package/dist/protocol/sse.d.ts +15 -0
- package/dist/protocol/sse.js +90 -0
- package/dist/protocol/wire.d.ts +128 -0
- package/dist/protocol/wire.js +278 -0
- package/dist/reference-auth/index.d.ts +209 -0
- package/dist/reference-auth/index.js +999 -0
- package/dist/reference-auth/store.d.ts +53 -0
- package/dist/reference-auth/store.js +2 -0
- package/dist/sqlite-init.d.ts +2 -0
- package/dist/sqlite-init.js +17 -0
- package/dist/sqlite.d.ts +27 -0
- package/dist/sqlite.js +369 -0
- package/dist/store.d.ts +1 -0
- package/dist/store.js +1 -0
- package/examples/quickstart.mjs +29 -0
- package/package.json +67 -4
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { calculateJwkThumbprint, compactVerify, decodeProtectedHeader, importJWK } from 'jose';
|
|
3
|
+
import { parseDpopClaims, parseJson } from '../protocol/wire.js';
|
|
4
|
+
import { AuthorityError } from './authority.js';
|
|
5
|
+
export const PROOF_WINDOW_SECONDS = 60;
|
|
6
|
+
export const CLOCK_SKEW_SECONDS = 30;
|
|
7
|
+
const COMPACT_JWT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
8
|
+
export function base64urlSha256(value) { return createHash('sha256').update(value).digest('base64url'); }
|
|
9
|
+
// Origin plus path: the htu form RFC 9449 compares (no query or fragment).
|
|
10
|
+
export function canonicalHtu(url) { const parsed = new URL(url); return `${parsed.origin}${parsed.pathname}`; }
|
|
11
|
+
// Header and payload both go through parseJson: jose's JSON.parse keeps the last of any duplicate member.
|
|
12
|
+
export function assertUniqueJwtMembers(jwt) { for (const segment of jwt.split('.').slice(0, 2))
|
|
13
|
+
parseJson(Buffer.from(segment, 'base64url').toString('utf8')); }
|
|
14
|
+
// RFC 9449 section 4.3 checks except nonce and replay, which only the token endpoint enforces.
|
|
15
|
+
// accessToken absent means a token-endpoint proof, which must not carry ath.
|
|
16
|
+
export async function verifyDpopProof(proof, expected) {
|
|
17
|
+
const invalid = () => new AuthorityError('invalid_dpop_proof');
|
|
18
|
+
// Several DPoP headers arrive joined by ", ", which fails the compact-JWT pattern.
|
|
19
|
+
if (!proof || proof.length > 8192 || !COMPACT_JWT.test(proof))
|
|
20
|
+
throw invalid();
|
|
21
|
+
let claims;
|
|
22
|
+
let jkt;
|
|
23
|
+
let htu;
|
|
24
|
+
try {
|
|
25
|
+
assertUniqueJwtMembers(proof);
|
|
26
|
+
const header = decodeProtectedHeader(proof);
|
|
27
|
+
const jwk = header.jwk;
|
|
28
|
+
if (header.typ !== 'dpop+jwt' || header.alg !== 'ES256' || !jwk || jwk.kty !== 'EC' || jwk.crv !== 'P-256' || 'd' in jwk)
|
|
29
|
+
throw invalid();
|
|
30
|
+
const { payload } = await compactVerify(proof, await importJWK(jwk, 'ES256'), { algorithms: ['ES256'] });
|
|
31
|
+
claims = parseDpopClaims(JSON.parse(new TextDecoder().decode(payload)));
|
|
32
|
+
htu = canonicalHtu(claims.htu);
|
|
33
|
+
jkt = await calculateJwkThumbprint(jwk);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw invalid();
|
|
37
|
+
}
|
|
38
|
+
const nowSeconds = expected.nowMs / 1000;
|
|
39
|
+
// jti is retained by the token-endpoint replay store, so bound it here (parseDpopClaims does not).
|
|
40
|
+
if (claims.jti.length > 256 || (claims.nonce?.length ?? 0) > 256)
|
|
41
|
+
throw invalid();
|
|
42
|
+
if (claims.htm !== expected.method || htu !== canonicalHtu(expected.url))
|
|
43
|
+
throw invalid();
|
|
44
|
+
if (claims.iat > nowSeconds + CLOCK_SKEW_SECONDS || claims.iat < nowSeconds - PROOF_WINDOW_SECONDS)
|
|
45
|
+
throw invalid();
|
|
46
|
+
if (expected.accessToken === undefined ? 'ath' in claims : claims.ath !== base64urlSha256(expected.accessToken))
|
|
47
|
+
throw invalid();
|
|
48
|
+
return { claims, jkt };
|
|
49
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { MetadataResolver } from './network/discovery.js';
|
|
2
|
+
import type { Principal, AuthorityPolicy } from './auth/authority.js';
|
|
3
|
+
export interface BrowserBinding {
|
|
4
|
+
clientId: string;
|
|
5
|
+
userId: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
issuerGeneration?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface BrowserSession extends BrowserBinding {
|
|
10
|
+
principal: Principal;
|
|
11
|
+
expiresAt: number;
|
|
12
|
+
}
|
|
13
|
+
export interface BrowserSessionAuthority {
|
|
14
|
+
join(binding: BrowserBinding, assertion: {
|
|
15
|
+
jti: string;
|
|
16
|
+
expiresAt: number;
|
|
17
|
+
}): Promise<BrowserSession>;
|
|
18
|
+
current(binding: BrowserBinding): Promise<BrowserSession>;
|
|
19
|
+
}
|
|
20
|
+
export interface BrowserCookieStore {
|
|
21
|
+
put(hash: string, binding: BrowserBinding & {
|
|
22
|
+
expiresAt: number;
|
|
23
|
+
}): Promise<void>;
|
|
24
|
+
get(hash: string): Promise<(BrowserBinding & {
|
|
25
|
+
expiresAt: number;
|
|
26
|
+
}) | null>;
|
|
27
|
+
}
|
|
28
|
+
export declare function boundedText(request: Request, max: number): Promise<string>;
|
|
29
|
+
export declare function createBrowserSessions(options: {
|
|
30
|
+
endpoint: string;
|
|
31
|
+
domain: string;
|
|
32
|
+
metadata: MetadataResolver;
|
|
33
|
+
authority: BrowserSessionAuthority;
|
|
34
|
+
cookies: BrowserCookieStore;
|
|
35
|
+
policy: AuthorityPolicy;
|
|
36
|
+
now: () => number;
|
|
37
|
+
cookieName?: string;
|
|
38
|
+
sameSite?: 'Lax' | 'None';
|
|
39
|
+
cookieLifetimeSeconds?: number;
|
|
40
|
+
}): {
|
|
41
|
+
authenticate: (request: Request) => Promise<Principal>;
|
|
42
|
+
withSession<T>(request: Request, scopes: readonly string[], action: (principal: Principal) => Promise<T>): Promise<T>;
|
|
43
|
+
join(request: Request): Promise<Response>;
|
|
44
|
+
};
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { compactVerify, createLocalJWKSet, decodeJwt, errors } from 'jose';
|
|
3
|
+
import { secureUrl } from './network/transport.js';
|
|
4
|
+
import { parseJson, object } from './protocol/wire.js';
|
|
5
|
+
import { assertUniqueJwtMembers } from './auth/dpop.js';
|
|
6
|
+
import { AuthorityError } from './auth/authority.js';
|
|
7
|
+
const hash = (value) => createHash('sha256').update(value).digest('base64url');
|
|
8
|
+
export async function boundedText(request, max) {
|
|
9
|
+
if (!request.body)
|
|
10
|
+
throw new TypeError('body');
|
|
11
|
+
const reader = request.body.getReader();
|
|
12
|
+
let size = 0;
|
|
13
|
+
const parts = [];
|
|
14
|
+
try {
|
|
15
|
+
for (;;) {
|
|
16
|
+
const next = await reader.read();
|
|
17
|
+
if (next.done)
|
|
18
|
+
break;
|
|
19
|
+
size += next.value.byteLength;
|
|
20
|
+
if (size > max)
|
|
21
|
+
throw new TypeError('body');
|
|
22
|
+
parts.push(next.value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
await reader.cancel().catch(() => { });
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
reader.releaseLock();
|
|
31
|
+
}
|
|
32
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(parts));
|
|
33
|
+
}
|
|
34
|
+
export function createBrowserSessions(options) {
|
|
35
|
+
secureUrl(options.endpoint);
|
|
36
|
+
const name = options.cookieName ?? '__Host-poppy-session', sameSite = options.sameSite ?? 'Lax', lifetime = options.cookieLifetimeSeconds ?? 3600;
|
|
37
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name) || !['Lax', 'None'].includes(sameSite) || !Number.isSafeInteger(lifetime) || lifetime < 1 || !/^[a-z0-9.-]+$/i.test(options.domain))
|
|
38
|
+
throw new TypeError('browser options');
|
|
39
|
+
const invalid = () => new Error('Invalid browser session');
|
|
40
|
+
async function authenticate(request) {
|
|
41
|
+
const cookies = (request.headers.get('cookie') ?? '').split(';').map(part => part.trim().split('='));
|
|
42
|
+
const matches = cookies.filter(([key]) => key === name);
|
|
43
|
+
if (matches.length !== 1 || matches[0].length !== 2 || !/^[A-Za-z0-9_-]{43}$/.test(matches[0][1]))
|
|
44
|
+
throw invalid();
|
|
45
|
+
const binding = await options.cookies.get(hash(matches[0][1]));
|
|
46
|
+
if (!binding || binding.expiresAt <= options.now())
|
|
47
|
+
throw invalid();
|
|
48
|
+
const session = await options.authority.current(binding);
|
|
49
|
+
if (session.expiresAt <= options.now() || session.clientId !== binding.clientId || session.userId !== binding.userId || session.sessionId !== binding.sessionId || session.issuerGeneration !== binding.issuerGeneration)
|
|
50
|
+
throw invalid();
|
|
51
|
+
return session.principal;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
authenticate,
|
|
55
|
+
async withSession(request, scopes, action) {
|
|
56
|
+
const principal = await authenticate(request);
|
|
57
|
+
return options.policy.withCurrent(principal, [], async () => {
|
|
58
|
+
const current = await options.authority.current(principal);
|
|
59
|
+
if (!scopes.every(scope => current.principal.scopes.includes(scope)))
|
|
60
|
+
throw new AuthorityError('insufficient_scope');
|
|
61
|
+
return action(current.principal);
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
async join(request) {
|
|
65
|
+
try {
|
|
66
|
+
if (request.method !== 'POST' || request.url !== options.endpoint || request.headers.get('content-type')?.split(';')[0] !== 'application/x-www-form-urlencoded')
|
|
67
|
+
throw invalid();
|
|
68
|
+
const fields = new URLSearchParams(await boundedText(request, 16384));
|
|
69
|
+
if (fields.getAll('assertion').length !== 1 || [...fields.keys()].some(key => key !== 'assertion'))
|
|
70
|
+
throw invalid();
|
|
71
|
+
const jwt = fields.get('assertion');
|
|
72
|
+
if (jwt.length > 8192)
|
|
73
|
+
throw invalid();
|
|
74
|
+
assertUniqueJwtMembers(jwt);
|
|
75
|
+
const untrusted = decodeJwt(jwt);
|
|
76
|
+
if (typeof untrusted.iss !== 'string')
|
|
77
|
+
throw invalid();
|
|
78
|
+
const client = await options.metadata.resolveClient(untrusted.iss);
|
|
79
|
+
let keys = await options.metadata.resolveJwks(client.jwks_uri);
|
|
80
|
+
let verified;
|
|
81
|
+
try {
|
|
82
|
+
verified = await compactVerify(jwt, createLocalJWKSet(keys), { algorithms: ['ES256', 'RS256'] });
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (!(error instanceof errors.JWKSNoMatchingKey))
|
|
86
|
+
throw error;
|
|
87
|
+
keys = await options.metadata.resolveJwks(client.jwks_uri, { refresh: true });
|
|
88
|
+
verified = await compactVerify(jwt, createLocalJWKSet(keys), { algorithms: ['ES256', 'RS256'] });
|
|
89
|
+
}
|
|
90
|
+
const claims = object(parseJson(new TextDecoder().decode(verified.payload)));
|
|
91
|
+
if (verified.protectedHeader.typ?.toLowerCase().replace(/^application\//, '') !== 'poppy-browser+jwt' || claims.iss !== client.client_id || claims.aud !== options.endpoint || typeof claims.sub !== 'string' || !claims.sub || claims.sub.length > 256 || typeof claims.session_id !== 'string' || !claims.session_id || typeof claims.jti !== 'string' || !claims.jti || claims.jti.length > 256 || !Number.isSafeInteger(claims.iat) || !Number.isSafeInteger(claims.exp) || claims.exp <= options.now() / 1000 || claims.iat > options.now() / 1000 + 30 || claims.exp <= claims.iat || claims.exp - claims.iat > 60 || typeof claims.return_to !== 'string')
|
|
92
|
+
throw invalid();
|
|
93
|
+
const destination = secureUrl(claims.return_to);
|
|
94
|
+
if (destination.hostname !== options.domain.toLowerCase() && !destination.hostname.endsWith(`.${options.domain.toLowerCase()}`))
|
|
95
|
+
throw invalid();
|
|
96
|
+
const session = await options.authority.join({ clientId: client.client_id, userId: claims.sub, sessionId: claims.session_id }, { jti: claims.jti, expiresAt: claims.exp * 1000 });
|
|
97
|
+
const seconds = Math.floor(Math.min(session.expiresAt - options.now(), lifetime * 1000) / 1000);
|
|
98
|
+
if (seconds < 1)
|
|
99
|
+
throw invalid();
|
|
100
|
+
const value = randomBytes(32).toString('base64url');
|
|
101
|
+
const { clientId, userId, sessionId, issuerGeneration } = session;
|
|
102
|
+
await options.cookies.put(hash(value), { clientId, userId, sessionId, ...(issuerGeneration === undefined ? {} : { issuerGeneration }), expiresAt: options.now() + seconds * 1000 });
|
|
103
|
+
return new Response(null, { status: 303, headers: { Location: claims.return_to, 'Set-Cookie': `${name}=${value}; Path=/; Secure; HttpOnly; SameSite=${sameSite}; Max-Age=${seconds}`, 'Cache-Control': 'no-store' } });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return new Response('Invalid browser session', { status: 400, headers: { 'Cache-Control': 'no-store', 'Content-Type': 'text/plain' } });
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { ConversationEvent, JsonObject, Message, Sender, Status } from '../protocol/wire.js';
|
|
2
|
+
import type { ConversationStore, ConversationNotifications } from './store.js';
|
|
3
|
+
import type { AcceptedAuthority, AuthorityPolicy, RequestAuthenticator, WorkRevocationPolicy } from '../auth/authority.js';
|
|
4
|
+
export interface ConversationRecord {
|
|
5
|
+
id: string;
|
|
6
|
+
issuer: string;
|
|
7
|
+
clientId: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
bindingId: string;
|
|
10
|
+
lastUserMessage?: Message;
|
|
11
|
+
status: Status;
|
|
12
|
+
responder: Sender;
|
|
13
|
+
context: JsonObject;
|
|
14
|
+
events: ConversationEvent[];
|
|
15
|
+
eventWindow?: {
|
|
16
|
+
invalid?: boolean;
|
|
17
|
+
expired?: boolean;
|
|
18
|
+
hasMore: boolean;
|
|
19
|
+
};
|
|
20
|
+
authority: AcceptedAuthority;
|
|
21
|
+
parentId?: string;
|
|
22
|
+
childId?: string;
|
|
23
|
+
humanUntil?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface WorkRecord {
|
|
26
|
+
id: string;
|
|
27
|
+
conversationId: string;
|
|
28
|
+
message: Message;
|
|
29
|
+
authority: AcceptedAuthority;
|
|
30
|
+
phase: 'held' | 'pending' | 'claimed' | 'completed' | 'suspended' | 'failed' | 'cancelled';
|
|
31
|
+
fence?: string;
|
|
32
|
+
leaseUntil?: number;
|
|
33
|
+
human?: true;
|
|
34
|
+
outputs: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
export interface MessageRecord {
|
|
37
|
+
conversationId: string;
|
|
38
|
+
fingerprint: string;
|
|
39
|
+
receipt: {
|
|
40
|
+
status: number;
|
|
41
|
+
body: JsonObject;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export interface ConversationStoreState {
|
|
45
|
+
cursorStatus?: 'expired' | 'invalid';
|
|
46
|
+
version: 1;
|
|
47
|
+
generation: string;
|
|
48
|
+
conversations: Record<string, ConversationRecord>;
|
|
49
|
+
bindings: Record<string, {
|
|
50
|
+
accountId?: string;
|
|
51
|
+
}>;
|
|
52
|
+
messages: Record<string, MessageRecord>;
|
|
53
|
+
work: WorkRecord[];
|
|
54
|
+
}
|
|
55
|
+
export interface ConversationOptions {
|
|
56
|
+
endpoint: string;
|
|
57
|
+
store: ConversationStore;
|
|
58
|
+
authenticator: RequestAuthenticator;
|
|
59
|
+
policy: AuthorityPolicy;
|
|
60
|
+
clock: {
|
|
61
|
+
now(): number;
|
|
62
|
+
};
|
|
63
|
+
revocationPolicy?: WorkRevocationPolicy;
|
|
64
|
+
pageSize?: number;
|
|
65
|
+
leaseMs?: number;
|
|
66
|
+
operatorLeaseMs?: number;
|
|
67
|
+
finishedWorkLimit?: number;
|
|
68
|
+
notifications?: ConversationNotifications;
|
|
69
|
+
maxPageBytes?: number;
|
|
70
|
+
maxEventBytes?: number;
|
|
71
|
+
maxFrameBytes?: number;
|
|
72
|
+
streamMs?: number;
|
|
73
|
+
supportedScopes?: readonly string[];
|
|
74
|
+
pollIntervalMs?: number;
|
|
75
|
+
/** Mandatory for staff methods: authenticate and authorize this company operator for the conversation, or throw. */
|
|
76
|
+
authorizeOperator?: (operator: unknown, conversationId: string) => Promise<void>;
|
|
77
|
+
/** Mandatory for requestAuthorization: the scopes the issuer's Direct Sign-In grants; work may ask only for these. */
|
|
78
|
+
directScopes?: readonly string[];
|
|
79
|
+
/** Scopes that allow reading account-bound history; default ['poppy:read']. A Company offering only custom scopes sets its own. */
|
|
80
|
+
historyScopes?: readonly string[];
|
|
81
|
+
}
|
|
82
|
+
export interface WorkRef {
|
|
83
|
+
workId: string;
|
|
84
|
+
fence: string;
|
|
85
|
+
}
|
|
86
|
+
export interface WorkClaim extends WorkRef {
|
|
87
|
+
conversationId: string;
|
|
88
|
+
message: Message;
|
|
89
|
+
context: JsonObject;
|
|
90
|
+
}
|
|
91
|
+
export interface ConversationService {
|
|
92
|
+
/** Protocol errors become responses; anything else is re-thrown, so hosts must catch it and answer 500. */
|
|
93
|
+
handle(request: Request): Promise<Response>;
|
|
94
|
+
claim(): Promise<WorkClaim | null>;
|
|
95
|
+
renew(work: WorkRef): Promise<void>;
|
|
96
|
+
/** Conversation history for a model or handler; account-bound history needs the bound account and poppy:read. */
|
|
97
|
+
history(work: WorkRef): Promise<ConversationEvent[]>;
|
|
98
|
+
/** Account-use gate: call before any account read or tool. Checks accepted authority, then binds the shared ownership once. */
|
|
99
|
+
requireAccount(work: WorkRef, scopes: readonly string[]): Promise<{
|
|
100
|
+
accountId: string;
|
|
101
|
+
}>;
|
|
102
|
+
reply(work: WorkRef, outputId: string, message: Message): Promise<void>;
|
|
103
|
+
/** Spec 7.11: asks the Personal Agent to sign in or grant these scopes. Uses no account, so it still publishes after revocation. */
|
|
104
|
+
requestAuthorization(work: WorkRef, outputId: string, scopes: readonly string[]): Promise<void>;
|
|
105
|
+
/** Spec 7.10: asks to talk with the User directly; status and responder are unchanged. */
|
|
106
|
+
requestUser(work: WorkRef, outputId: string, reason: string): Promise<void>;
|
|
107
|
+
finish(work: WorkRef, outcome: 'completed' | 'suspended' | 'failed'): Promise<void>;
|
|
108
|
+
/** The company asks for a person on its own: this attempt ends and the conversation is queued. */
|
|
109
|
+
handoff(work: WorkRef): Promise<void>;
|
|
110
|
+
/** The company closes the conversation (and its open Direct Conversation); idempotent. */
|
|
111
|
+
close(work: WorkRef): Promise<void>;
|
|
112
|
+
/** A person joins: automated work is fenced and the returned claim publishes with sender human. Renew it within operatorLeaseMs. */
|
|
113
|
+
joinHuman(operator: unknown, conversationId: string): Promise<WorkClaim>;
|
|
114
|
+
/** Nobody can join a queued conversation: the agent says why and the conversation leaves the queue. No-op unless queued. */
|
|
115
|
+
noHumanAvailable(operator: unknown, conversationId: string, reason: string): Promise<void>;
|
|
116
|
+
/** The person hands back to automation; their claim stops working. No-op unless a person is responding. */
|
|
117
|
+
returnToAgent(operator: unknown, conversationId: string): Promise<void>;
|
|
118
|
+
}
|
|
119
|
+
export declare class PublicationError extends Error {
|
|
120
|
+
readonly code: 'stale_work' | 'output_conflict';
|
|
121
|
+
constructor(code: 'stale_work' | 'output_conflict');
|
|
122
|
+
}
|
|
123
|
+
export declare function initialConversationState(): ConversationStoreState;
|
|
124
|
+
/** For the host's reset transaction: a fresh state with a new generation, so every old fence is stale. */
|
|
125
|
+
export declare function resetConversationState(draft: ConversationStoreState): void;
|
|
126
|
+
export declare function createConversationService(options: ConversationOptions): ConversationService;
|