@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.
- package/API.md +9 -1
- package/AUTH.md +97 -90
- package/CHANGELOG.md +13 -1
- package/CONFORMANCE.md +73 -0
- package/CONTRIBUTING.md +12 -11
- package/DELIVERY.md +19 -3
- package/INTEGRATIONS.md +35 -23
- package/OPERATIONS.md +120 -0
- package/PROVIDER.md +20 -12
- package/README.md +6 -17
- package/RELEASING.md +2 -0
- package/SECURITY.md +2 -0
- package/STORAGE.md +14 -7
- package/dist/adapter-contract.d.ts +6 -0
- package/dist/adapter-contract.js +30 -0
- package/dist/auth/challenge.d.ts +2 -0
- package/dist/auth/challenge.js +8 -8
- package/dist/auth/client-auth.d.ts +20 -7
- package/dist/auth/client-auth.js +386 -334
- package/dist/auth/sqlite-store.js +13 -4
- package/dist/auth/store.d.ts +6 -1
- package/dist/conversation.d.ts +6 -0
- package/dist/conversation.js +18 -14
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/integrations.d.ts +11 -4
- package/dist/integrations.js +30 -38
- package/dist/network/discovery.d.ts +31 -13
- package/dist/network/discovery.js +103 -66
- package/dist/operations.d.ts +103 -0
- package/dist/operations.js +261 -0
- package/dist/postgres.d.ts +11 -0
- package/dist/postgres.js +40 -3
- package/dist/protocol/operations.d.ts +51 -0
- package/dist/protocol/operations.js +98 -0
- package/dist/protocol/session.d.ts +28 -0
- package/dist/protocol/session.js +93 -0
- package/dist/protocol/sse.d.ts +1 -0
- package/dist/protocol/sse.js +1 -1
- package/dist/protocol/wire.d.ts +8 -4
- package/dist/protocol/wire.js +37 -15
- package/dist/provider-postgres.js +3 -3
- package/dist/provider-storage.js +4 -4
- package/dist/provider.d.ts +26 -3
- package/dist/provider.js +94 -13
- package/dist/sqlite.d.ts +12 -0
- package/dist/sqlite.js +42 -0
- package/examples/operations.mjs +25 -0
- package/examples/quickstart.mjs +1 -0
- package/package.json +5 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { validRedirect } from './redirect.js';
|
|
2
|
-
import { object, string, member,
|
|
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)
|
|
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
|
-
|
|
48
|
-
|
|
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 ('
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
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 };
|