@poppykit/client 0.5.0 → 0.6.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.
Files changed (50) hide show
  1. package/API.md +9 -1
  2. package/AUTH.md +97 -90
  3. package/CHANGELOG.md +13 -1
  4. package/CONFORMANCE.md +73 -0
  5. package/CONTRIBUTING.md +12 -11
  6. package/DELIVERY.md +19 -3
  7. package/INTEGRATIONS.md +35 -23
  8. package/OPERATIONS.md +120 -0
  9. package/PROVIDER.md +20 -12
  10. package/README.md +6 -17
  11. package/RELEASING.md +2 -0
  12. package/SECURITY.md +2 -0
  13. package/STORAGE.md +14 -7
  14. package/dist/adapter-contract.d.ts +6 -0
  15. package/dist/adapter-contract.js +30 -0
  16. package/dist/auth/challenge.d.ts +2 -0
  17. package/dist/auth/challenge.js +8 -8
  18. package/dist/auth/client-auth.d.ts +20 -7
  19. package/dist/auth/client-auth.js +386 -334
  20. package/dist/auth/sqlite-store.js +13 -4
  21. package/dist/auth/store.d.ts +6 -1
  22. package/dist/conversation.d.ts +6 -0
  23. package/dist/conversation.js +18 -14
  24. package/dist/index.d.ts +2 -0
  25. package/dist/index.js +2 -0
  26. package/dist/integrations.d.ts +11 -4
  27. package/dist/integrations.js +30 -38
  28. package/dist/network/discovery.d.ts +31 -13
  29. package/dist/network/discovery.js +103 -66
  30. package/dist/operations.d.ts +103 -0
  31. package/dist/operations.js +261 -0
  32. package/dist/postgres.d.ts +11 -0
  33. package/dist/postgres.js +40 -3
  34. package/dist/protocol/operations.d.ts +51 -0
  35. package/dist/protocol/operations.js +98 -0
  36. package/dist/protocol/session.d.ts +28 -0
  37. package/dist/protocol/session.js +93 -0
  38. package/dist/protocol/sse.d.ts +1 -0
  39. package/dist/protocol/sse.js +1 -1
  40. package/dist/protocol/wire.d.ts +8 -4
  41. package/dist/protocol/wire.js +37 -15
  42. package/dist/provider-postgres.js +3 -3
  43. package/dist/provider-storage.js +4 -4
  44. package/dist/provider.d.ts +26 -3
  45. package/dist/provider.js +94 -13
  46. package/dist/sqlite.d.ts +12 -0
  47. package/dist/sqlite.js +42 -0
  48. package/examples/operations.mjs +25 -0
  49. package/examples/quickstart.mjs +1 -0
  50. package/package.json +5 -2
@@ -1,5 +1,5 @@
1
1
  import { validRedirect } from './redirect.js';
2
- import { object, string, member, canonical, parseJson, ValidationError } from '../protocol/wire.js';
2
+ import { object, string, member, parseJson, ValidationError } from '../protocol/wire.js';
3
3
  import { secureUrl } from './transport.js';
4
4
  import { parseExcludedTasks } from './tasks.js';
5
5
  function url(value, field) { const valueString = string(value, field); secureUrl(valueString); return valueString; }
@@ -8,7 +8,7 @@ function strings(value, field, allowEmpty = false) { if (!Array.isArray(value) |
8
8
  export function companyScopes(value, allowEmpty = false) {
9
9
  const result = strings(value, 'scopes', allowEmpty);
10
10
  for (const scope of result)
11
- if (!/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope) || scope.startsWith('poppy:'))
11
+ if (!/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope))
12
12
  throw new ValidationError('scopes');
13
13
  return result;
14
14
  }
@@ -40,24 +40,10 @@ export function parsePoppyDocument(value, servingUrl) {
40
40
  const web = 'web' in doc ? object(doc.web, 'web') : undefined;
41
41
  if (web && 'browser_session_endpoint' in web)
42
42
  url(web.browser_session_endpoint, 'browser_session_endpoint');
43
- if (['agent', 'apis', 'feedback'].some(key => key in doc) || (web && 'browser_session_endpoint' in web))
44
- object(doc.auth, 'auth');
45
43
  if ('auth' in doc) {
46
44
  const auth = object(doc.auth, 'auth');
47
- authorizationMetadataUrl(url(auth.issuer, 'issuer'));
48
- if ('mediated' in auth) {
49
- const mediated = object(auth.mediated, 'mediated');
50
- url(mediated.endpoint, 'mediated.endpoint');
51
- companyScopes(mediated.scopes);
52
- if (!Array.isArray(mediated.fields) || !mediated.fields.length)
53
- throw new ValidationError('mediated.fields');
54
- for (const field of mediated.fields) {
55
- const item = object(field);
56
- string(item.name, 'field.name');
57
- string(item.label, 'field.label');
58
- member(item.secret, [true, false], 'field.secret');
59
- }
60
- }
45
+ if ('issuer' in auth)
46
+ authorizationMetadataUrl(url(auth.issuer, 'issuer'));
61
47
  if ('scope_hints' in auth) {
62
48
  const hints = object(auth.scope_hints, 'scope_hints');
63
49
  for (const [scope, value] of Object.entries(hints)) {
@@ -82,8 +68,8 @@ export function parsePoppyDocument(value, servingUrl) {
82
68
  const item = object(protocol);
83
69
  string(item.type, 'protocol.type');
84
70
  url(item.endpoint, 'protocol.endpoint');
85
- if ('resource' in item)
86
- url(item.resource, 'protocol.resource');
71
+ if (item.type === 'poppy' && 'access' in item)
72
+ string(item.access, 'protocol.access');
87
73
  }
88
74
  }
89
75
  if ('apis' in doc) {
@@ -94,8 +80,6 @@ export function parsePoppyDocument(value, servingUrl) {
94
80
  string(item.type, 'api.type');
95
81
  string(item.description, 'api.description');
96
82
  url(item.url, 'api.url');
97
- if ('resource' in item)
98
- url(item.resource, 'api.resource');
99
83
  }
100
84
  }
101
85
  if ('excluded_tasks' in doc)
@@ -109,21 +93,22 @@ export function parsePoppyDocument(value, servingUrl) {
109
93
  if ('endpoint' in item)
110
94
  url(item.endpoint, 'extension.endpoint');
111
95
  }
96
+ const mediated = doc.extensions?.['mediated-sign-in'];
97
+ if (mediated && object(mediated).version === '1')
98
+ parseMediatedSignIn(mediated);
112
99
  return doc;
113
100
  }
114
- export function parseAuthorizationMetadata(value, document) {
101
+ export function parseAuthorizationMetadata(value, document, issuer = document.auth?.issuer) {
115
102
  const metadata = object(value);
116
- const auth = document.auth;
117
- if (metadata.issuer !== auth.issuer)
103
+ if (!issuer || metadata.issuer !== issuer)
118
104
  throw new ValidationError('issuer');
119
105
  const resources = strings(metadata.protected_resources, 'protected_resources');
120
106
  resources.forEach(value => url(value, 'protected_resources'));
121
107
  if (!resources.some(value => /^https:\/\/[^/:?#]+$/.test(value) && value.slice(8).toLowerCase() === document.organization.domain.toLowerCase()))
122
108
  throw new ValidationError('protected_resources');
123
109
  const grants = strings(metadata.grant_types_supported, 'grant_types_supported');
124
- for (const grant of ['urn:ietf:params:oauth:grant-type:jwt-bearer', 'refresh_token'])
125
- if (!grants.includes(grant))
126
- throw new ValidationError('grant_types_supported');
110
+ if (!grants.includes('refresh_token'))
111
+ throw new ValidationError('grant_types_supported');
127
112
  for (const key of ['token_endpoint', 'revocation_endpoint'])
128
113
  url(metadata[key], key);
129
114
  for (const [key, grant] of [['authorization_endpoint', 'authorization_code'], ['device_authorization_endpoint', 'urn:ietf:params:oauth:grant-type:device_code']])
@@ -131,15 +116,13 @@ export function parseAuthorizationMetadata(value, document) {
131
116
  url(metadata[key], key);
132
117
  if ('pushed_authorization_request_endpoint' in metadata)
133
118
  url(metadata.pushed_authorization_request_endpoint, 'pushed_authorization_request_endpoint');
134
- const signIn = grants.includes('authorization_code') || grants.includes('urn:ietf:params:oauth:grant-type:device_code') || !!auth.mediated;
135
- const supported = signIn || 'scopes_supported' in metadata ? companyScopes(metadata.scopes_supported, true) : [];
136
- if (auth.mediated && !auth.mediated.scopes.every(scope => supported.includes(scope)))
137
- throw new ValidationError('mediated.scopes');
138
- // Scope descriptions may instead live in a listed OpenAPI document.
139
- if (!document.apis?.some(api => api.type === 'openapi') && supported.some(scope => !Object.hasOwn(auth.scopes ?? {}, scope)))
140
- throw new ValidationError('auth.scopes');
141
- if ('dpop_signing_alg_values_supported' in metadata && !strings(metadata.dpop_signing_alg_values_supported, 'dpop_signing_alg_values_supported').includes('ES256'))
142
- throw new ValidationError('dpop_signing_alg_values_supported');
119
+ if ('scopes_supported' in metadata)
120
+ companyScopes(metadata.scopes_supported, true);
121
+ const mediated = mediatedSignIn(document);
122
+ if (mediated && !grants.includes(MEDIATED_GRANT))
123
+ throw new ValidationError('mediated.grant');
124
+ if ('dpop_signing_alg_values_supported' in metadata)
125
+ strings(metadata.dpop_signing_alg_values_supported, 'dpop_signing_alg_values_supported');
143
126
  if ('client_id_metadata_document_supported' in metadata)
144
127
  member(metadata.client_id_metadata_document_supported, [true, false], 'client_id_metadata_document_supported');
145
128
  return metadata;
@@ -152,10 +135,53 @@ export function signInMethods(metadata) {
152
135
  methods.push({ method: 'direct', scopes: [...auth.scopes_supported ?? []] });
153
136
  if (auth.grant_types_supported.includes('urn:ietf:params:oauth:grant-type:device_code') && auth.device_authorization_endpoint)
154
137
  methods.push({ method: 'device', scopes: [...auth.scopes_supported ?? []] });
155
- if (metadata.document.auth?.mediated)
156
- methods.push({ method: 'mediated', scopes: [...metadata.document.auth.mediated.scopes] });
138
+ const mediated = mediatedSignIn(metadata.document);
139
+ if (mediated && auth.grant_types_supported.includes(MEDIATED_GRANT))
140
+ methods.push({ method: 'mediated', scopes: [...mediated.scopes] });
157
141
  return methods;
158
142
  }
143
+ export const MEDIATED_GRANT = 'https://personalagentprotocol.org/grant/mediated-sign-in';
144
+ export function parseMediatedSignIn(value) {
145
+ const extension = object(value, 'mediated-sign-in');
146
+ member(extension.version, ['1'], 'mediated.version');
147
+ companyScopes(extension.scopes);
148
+ if (!Array.isArray(extension.fields) || !extension.fields.length)
149
+ throw new ValidationError('mediated.fields');
150
+ const names = new Set();
151
+ const reserved = new Set(['grant_type', 'scope', 'resource', 'client_id', 'client_secret', 'client_assertion', 'client_assertion_type', 'sign_in_id', 'one_time_code']);
152
+ for (const value of extension.fields) {
153
+ const field = object(value);
154
+ const name = string(field.name, 'field.name');
155
+ if (reserved.has(name) || names.has(name))
156
+ throw new ValidationError('field.name');
157
+ names.add(name);
158
+ string(field.label, 'field.label');
159
+ member(field.secret, [true, false], 'field.secret');
160
+ }
161
+ return extension;
162
+ }
163
+ export function mediatedSignIn(document) {
164
+ const extension = document.extensions?.['mediated-sign-in'];
165
+ return extension?.version === '1' ? parseMediatedSignIn(extension) : undefined;
166
+ }
167
+ export function companyIdentifier(metadata) {
168
+ return metadata.authorization?.issuer ?? `https://${metadata.document.organization.domain.toLowerCase()}`;
169
+ }
170
+ export function protectedResourceMetadataUrl(resource) {
171
+ const parsed = secureUrl(resource);
172
+ return `${parsed.origin}/.well-known/oauth-protected-resource${parsed.pathname === '/' ? '' : parsed.pathname}${parsed.search}`;
173
+ }
174
+ export function parseProtectedResourceMetadata(value) {
175
+ const metadata = object(value, 'resource_metadata');
176
+ url(metadata.resource, 'resource');
177
+ if ('authorization_servers' in metadata)
178
+ strings(metadata.authorization_servers, 'authorization_servers', true).forEach(value => authorizationMetadataUrl(url(value, 'authorization_servers')));
179
+ if ('bearer_methods_supported' in metadata)
180
+ strings(metadata.bearer_methods_supported, 'bearer_methods_supported', true);
181
+ if ('dpop_signing_alg_values_supported' in metadata)
182
+ strings(metadata.dpop_signing_alg_values_supported, 'dpop_signing_alg_values_supported', true);
183
+ return metadata;
184
+ }
159
185
  export function parseClientMetadata(value, clientId) {
160
186
  const parsed = validateClientId(clientId);
161
187
  const metadata = object(value);
@@ -166,6 +192,9 @@ export function parseClientMetadata(value, clientId) {
166
192
  const redirects = strings(metadata.redirect_uris, 'redirect_uris', true);
167
193
  if (secureUrl(url(metadata.jwks_uri, 'jwks_uri')).hostname !== parsed.hostname || redirects.some(endpoint => !validRedirect(endpoint, clientId)))
168
194
  throw new ValidationError('client_endpoint_domain');
195
+ if ('extensions' in metadata)
196
+ for (const extension of Object.values(object(metadata.extensions)))
197
+ string(object(extension).version, 'extension.version');
169
198
  if ('logo_uri' in metadata)
170
199
  url(metadata.logo_uri, 'logo_uri');
171
200
  return metadata;
@@ -186,7 +215,7 @@ function remember(map, key, value, max, expired) {
186
215
  }
187
216
  }
188
217
  // Defaults: 1000 entries per map; at most 16 cold fetch operations in flight and 120 started per 60 s (excess is rejected, not queued).
189
- // Invariant: maxFetchesPerMinute * 5 (300 s TTL cap) must stay below maxEntries, or a flood can evict live authDocuments/refreshes entries.
218
+ // Refresh throttling entries share the same bound as metadata cache entries.
190
219
  // shortcut: limits are global per resolver, not per caller, and cap total cold-fetch throughput; move per-caller limiting to the server HTTP layer before production use.
191
220
  export function createMetadataResolver(transport, options = {}) {
192
221
  const now = options.now ?? Date.now;
@@ -198,13 +227,12 @@ export function createMetadataResolver(transport, options = {}) {
198
227
  const cache = new Map();
199
228
  const pending = new Map();
200
229
  const refreshes = new Map();
201
- const authDocuments = new Map();
202
- async function get(target, validate, mode, refresh = false) {
230
+ async function get(target, validate, mode, refresh = false, allowMissing = false) {
203
231
  secureUrl(target);
204
232
  const key = `${mode}:${target}`;
205
233
  const cached = cache.get(key);
206
234
  if (!refresh && cached && cached.expires > now()) {
207
- validate(cached.value, cached.url);
235
+ validate(cached.value, cached.url, cached.missing);
208
236
  return structuredClone(cached);
209
237
  }
210
238
  let task = pending.get(key);
@@ -225,24 +253,25 @@ export function createMetadataResolver(transport, options = {}) {
225
253
  else if (cached?.modified)
226
254
  headers.set('if-modified-since', cached.modified);
227
255
  const response = await transport.fetch(new Request(target, { headers }), { mode });
228
- if (response.status !== 200 && !(response.status === 304 && cached))
256
+ if (response.status !== 200 && !(allowMissing && response.status === 404) && !(response.status === 304 && cached))
229
257
  throw new ValidationError('metadata.status');
230
258
  let value;
231
259
  try {
232
- value = response.status === 304 ? cached.value : parseJson(await response.text());
260
+ value = response.status === 304 ? cached.value : response.status === 404 ? null : parseJson(await response.text());
233
261
  }
234
262
  catch {
235
263
  throw new ValidationError('metadata.json');
236
264
  }
265
+ const missing = response.status === 404 || (response.status === 304 && cached?.missing === true);
237
266
  const servedUrl = response.url || (response.status === 304 ? cached.url : target);
238
- validate(value, servedUrl);
267
+ validate(value, servedUrl, missing);
239
268
  if (response.status === 304)
240
- validate(value, cached.url);
269
+ validate(value, cached.url, missing);
241
270
  const control = response.headers.get('cache-control') ?? (response.status === 304 ? cached?.control : undefined) ?? '';
242
271
  const age = Number(response.headers.get('age') ?? 0);
243
272
  const seconds = Number(/(?:^|,)\s*max-age=(\d+)/i.exec(control)?.[1] ?? 60); // no max-age: 60 s floor (guide: at least a minute)
244
273
  const expires = now() + (/\b(?:no-store|no-cache)\b/i.test(control) ? 0 : Math.max(0, Math.min(300, seconds - (Number.isFinite(age) ? age : 0)))) * 1000;
245
- const result = { value, control, url: servedUrl, expires, etag: response.headers.get('etag') ?? cached?.etag, modified: response.headers.get('last-modified') ?? cached?.modified };
274
+ const result = { value, control, missing, url: servedUrl, expires, etag: response.headers.get('etag') ?? cached?.etag, modified: response.headers.get('last-modified') ?? cached?.modified };
246
275
  if (startedEpoch !== epoch)
247
276
  return result;
248
277
  if (!/\bno-store\b/i.test(control))
@@ -255,7 +284,7 @@ export function createMetadataResolver(transport, options = {}) {
255
284
  }
256
285
  try {
257
286
  const result = await task;
258
- validate(result.value, result.url);
287
+ validate(result.value, result.url, result.missing);
259
288
  return structuredClone(result);
260
289
  }
261
290
  finally {
@@ -266,36 +295,44 @@ export function createMetadataResolver(transport, options = {}) {
266
295
  return {
267
296
  invalidate(target) { epoch++; pending.clear(); if (target === undefined) {
268
297
  cache.clear();
269
- authDocuments.clear();
270
298
  }
271
299
  else {
272
300
  for (const key of cache.keys())
273
301
  if (key.endsWith(`:${target}`))
274
302
  cache.delete(key);
275
- authDocuments.clear();
276
303
  } },
277
304
  async resolveCompany(input) {
278
- const startedEpoch = epoch;
279
305
  if (!/^[a-z0-9.-]+(?::\d{1,5})?$/i.test(input))
280
306
  throw new ValidationError('domain');
281
307
  const target = `https://${input}/.well-known/poppy.json`;
282
308
  const found = await get(target, parsePoppyDocument, 'discovery');
283
309
  const document = parsePoppyDocument(found.value, found.url);
284
- if (!document.auth)
310
+ const resource = `https://${document.organization.domain.toLowerCase()}`;
311
+ const protectedMetadata = await get(protectedResourceMetadataUrl(resource), (value, _servedUrl, missing) => {
312
+ if (!missing) {
313
+ const metadata = parseProtectedResourceMetadata(value);
314
+ if (!/^https:\/\/[^/:?#]+$/.test(metadata.resource) || metadata.resource.slice(8).toLowerCase() !== document.organization.domain.toLowerCase())
315
+ throw new ValidationError('resource');
316
+ }
317
+ }, 'discovery', false, true);
318
+ const servers = protectedMetadata.missing ? [] : parseProtectedResourceMetadata(protectedMetadata.value).authorization_servers ?? [];
319
+ const selected = document.auth?.issuer;
320
+ if (!servers.length) {
321
+ if (selected || mediatedSignIn(document) || document.agent?.protocols.some(item => item.type === 'poppy' && item.access === 'signed_in'))
322
+ throw new ValidationError('issuer');
285
323
  return { document, url: found.url };
286
- const auth = await get(authorizationMetadataUrl(document.auth.issuer), value => parseAuthorizationMetadata(value, document), 'discovery');
287
- if (startedEpoch !== epoch)
288
- return { document, authorization: parseAuthorizationMetadata(auth.value, document), url: found.url };
289
- const entries = authDocuments.get(document.auth.issuer) ?? new Map();
290
- const spelling = canonical(document.auth);
291
- const currentDomain = document.organization.domain.toLowerCase();
292
- for (const [otherDomain, entry] of entries)
293
- if (otherDomain !== currentDomain && entry.expires > now() && entry.auth !== spelling)
294
- throw new ValidationError('auth.consistency');
295
- const expiredEntry = (entry) => entry.expires <= now();
296
- remember(entries, currentDomain, { auth: spelling, expires: found.expires }, maxEntries, expiredEntry);
297
- remember(authDocuments, document.auth.issuer, entries, maxEntries, inner => [...inner.values()].every(expiredEntry));
298
- return { document, authorization: parseAuthorizationMetadata(auth.value, document), url: found.url };
324
+ }
325
+ const issuer = selected ?? (servers.length === 1 ? servers[0] : undefined);
326
+ if (!issuer || !servers.includes(issuer))
327
+ throw new ValidationError('issuer');
328
+ const auth = await get(authorizationMetadataUrl(issuer), value => parseAuthorizationMetadata(value, document, issuer), 'discovery');
329
+ return { document, authorization: parseAuthorizationMetadata(auth.value, document, issuer), url: found.url };
330
+ },
331
+ async resolveResource(resource, settings = {}) {
332
+ const target = settings.metadataUrl ?? protectedResourceMetadataUrl(resource);
333
+ const found = await get(target, (value, _servedUrl, missing) => { if (!missing)
334
+ parseProtectedResourceMetadata(value); }, 'discovery', settings.refresh, true);
335
+ return found.missing ? null : parseProtectedResourceMetadata(found.value);
299
336
  },
300
337
  async resolveExcludedTasks(document) {
301
338
  if (!document.excluded_tasks)
@@ -0,0 +1,103 @@
1
+ import type { Json, JsonObject, Message } from './protocol/wire.js';
2
+ import { supportsOperations } from './protocol/operations.js';
3
+ import type { BusinessOperation, OperationApproval, OperationRequest } from './protocol/operations.js';
4
+ export interface BusinessClientContext {
5
+ companyId: string;
6
+ companyName: string;
7
+ clientId: string;
8
+ userId: string;
9
+ /** Local grant identity when signed in; exact Session identity otherwise. Never a token. */
10
+ authority: string;
11
+ operations: boolean;
12
+ }
13
+ export interface BusinessAttempt {
14
+ version: 1;
15
+ id: string;
16
+ context: BusinessClientContext;
17
+ request: OperationRequest;
18
+ operation: BusinessOperation;
19
+ phase: 'pending_consent' | 'consenting' | 'approved' | 'declined' | 'withdrawn' | 'expired' | 'uncertain' | 'completed';
20
+ consentToken?: string;
21
+ result?: Json;
22
+ retryAt?: number;
23
+ headers?: [string, string][];
24
+ messageId: string;
25
+ withdrawalRequested?: true;
26
+ previousDecisions?: {
27
+ revision: string;
28
+ phase: BusinessAttempt['phase'];
29
+ }[];
30
+ }
31
+ /** Atomic, cross-process synchronous mutation of one detached attempt; rollback on throw; reject unknown versions. */
32
+ export interface BusinessAttemptStore {
33
+ pending(after: string | undefined, limit: number): Promise<string[]>;
34
+ transaction<T>(id: string, action: (slot: {
35
+ record?: BusinessAttempt;
36
+ }) => T): Promise<T>;
37
+ }
38
+ export interface TrustedOperationConsent {
39
+ /** Trusted UI only: show companyName and summary as inert literal text, plus all differences. Ask for this operation.
40
+ * This adapter MUST collect the actual user's answer. Never expose it, or a consent result, as a model tool.
41
+ */
42
+ confirm(input: {
43
+ attemptId: string;
44
+ companyId: string;
45
+ companyName: string;
46
+ summary: string;
47
+ terms?: JsonObject;
48
+ differences: string[];
49
+ }): Promise<'approve' | 'decline'>;
50
+ }
51
+ export type OperationDelivery = {
52
+ kind: 'completed';
53
+ result: Json;
54
+ } | {
55
+ kind: 'pending';
56
+ retryAfterMs?: number;
57
+ };
58
+ export declare class OperationClientError extends Error {
59
+ readonly code: string;
60
+ constructor(code: string);
61
+ }
62
+ export declare function createOperationClient(options: {
63
+ store: BusinessAttemptStore;
64
+ context(): Promise<BusinessClientContext>;
65
+ consent: TrustedOperationConsent;
66
+ /** Trusted application comparison, independent of Company/model claims. Return differences to show to the User. */
67
+ checkTerms(request: OperationRequest, operation: BusinessOperation): Promise<string[]>;
68
+ send(attempt: Readonly<BusinessAttempt>, approval: OperationApproval, signal: AbortSignal): Promise<OperationDelivery>;
69
+ now?: () => number;
70
+ }): {
71
+ receive: (id: string, request: OperationRequest, value: unknown, headers?: [string, string][]) => Promise<BusinessAttempt>;
72
+ receiveApi(id: string, request: Request, response: Response): Promise<BusinessAttempt>;
73
+ receiveMcp(id: string, request: Extract<OperationRequest, {
74
+ channel: "mcp";
75
+ }>, result: unknown): Promise<BusinessAttempt>;
76
+ receiveConversation(id: string, request: Extract<OperationRequest, {
77
+ channel: "conversation";
78
+ }>, message: unknown): Promise<BusinessAttempt>;
79
+ recover(after?: string, limit?: number): Promise<(BusinessAttempt | {
80
+ id: string;
81
+ error: string;
82
+ })[]>;
83
+ get(id: string): Promise<BusinessAttempt>;
84
+ requestConsent(id: string): Promise<BusinessAttempt>;
85
+ /** Explicitly dismiss the old UI first. Its late callback is fenced; a fresh answer is required. */
86
+ resumeConsent(id: string): Promise<BusinessAttempt>;
87
+ withdraw(id: string): Promise<BusinessAttempt>;
88
+ retry(id: string): Promise<BusinessAttempt>;
89
+ /** Trusted host lookup of actual business state; an operation returned on retry is not reconciliation. */
90
+ reconcile(id: string, lookup: (attempt: Readonly<BusinessAttempt>) => Promise<OperationDelivery>): Promise<BusinessAttempt>;
91
+ };
92
+ export declare function approvedApiRequest(attempt: BusinessAttempt, approval: OperationApproval, signal?: AbortSignal): Request;
93
+ export declare function approvedMcpParams(attempt: BusinessAttempt, approval: OperationApproval): JsonObject;
94
+ export declare function approvedConversationMessage(attempt: BusinessAttempt, approval: OperationApproval): Message;
95
+ /** Adapters must use verified SDK channel targets and current authorization; a conversation acceptance is still pending work. */
96
+ export declare function createOperationSender(channels: {
97
+ api(request: Request, context: BusinessClientContext): Promise<Response>;
98
+ mcp(url: string, params: JsonObject, signal: AbortSignal, context: BusinessClientContext): Promise<JsonObject>;
99
+ conversation(request: Extract<OperationRequest, {
100
+ channel: 'conversation';
101
+ }>, message: Message, signal: AbortSignal, context: BusinessClientContext): Promise<void>;
102
+ }): (attempt: Readonly<BusinessAttempt>, approval: OperationApproval, signal: AbortSignal) => Promise<OperationDelivery>;
103
+ export { supportsOperations };