@poppykit/client 0.4.1 → 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 +12 -3
- package/AUTH.md +97 -65
- package/CHANGELOG.md +18 -0
- package/CONFORMANCE.md +73 -0
- package/CONTRIBUTING.md +13 -10
- package/DELIVERY.md +30 -1
- package/INTEGRATIONS.md +63 -21
- package/OPERATIONS.md +120 -0
- package/PROVIDER.md +24 -14
- package/README.md +7 -13
- package/RELEASING.md +2 -0
- package/SECURITY.md +2 -0
- package/STORAGE.md +24 -5
- package/dist/adapter-contract.d.ts +6 -0
- package/dist/adapter-contract.js +31 -1
- package/dist/auth/challenge.d.ts +2 -0
- package/dist/auth/challenge.js +8 -8
- package/dist/auth/client-auth.d.ts +29 -13
- package/dist/auth/client-auth.js +498 -318
- package/dist/auth/sqlite-store.js +27 -4
- package/dist/auth/store.d.ts +6 -1
- package/dist/conversation.d.ts +6 -0
- package/dist/conversation.js +28 -14
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -1
- package/dist/integrations.d.ts +16 -4
- package/dist/integrations.js +30 -38
- package/dist/network/discovery.d.ts +51 -22
- package/dist/network/discovery.js +159 -69
- package/dist/network/redirect.d.ts +3 -0
- package/dist/network/redirect.js +38 -0
- package/dist/network/tasks.d.ts +23 -0
- package/dist/network/tasks.js +45 -0
- package/dist/operations.d.ts +103 -0
- package/dist/operations.js +261 -0
- package/dist/postgres.d.ts +11 -0
- package/dist/postgres.js +52 -2
- 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 +20 -15
- package/dist/protocol/wire.js +70 -22
- package/dist/provider-postgres.js +3 -3
- package/dist/provider-storage.js +4 -4
- package/dist/provider.d.ts +26 -10
- package/dist/provider.js +103 -32
- package/dist/sqlite.d.ts +12 -0
- package/dist/sqlite.js +42 -0
- package/examples/operations.mjs +25 -0
- package/examples/quickstart.mjs +2 -1
- package/package.json +5 -2
package/dist/sqlite.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BusinessAttemptStore } from './operations.js';
|
|
1
2
|
export { SqliteAuthStore } from './auth/sqlite-store.js';
|
|
2
3
|
import type { ConversationKey, ConversationStore } from './conversation-store.js';
|
|
3
4
|
import type { ConsumerCheckpoint, OutboundRecord } from './conversation.js';
|
|
@@ -18,3 +19,14 @@ export declare class SqliteConversationStore implements ConversationStore {
|
|
|
18
19
|
advanceCheckpoint(key: ConversationKey, row: ConsumerCheckpoint, from: readonly (string | undefined)[]): Promise<void>;
|
|
19
20
|
resetCheckpoint(key: ConversationKey, conversation: string, name: string): Promise<void>;
|
|
20
21
|
}
|
|
22
|
+
/** Separate from authentication lifecycle operations. Never discard uncertain business attempts. */
|
|
23
|
+
export declare class SqliteBusinessAttemptStore implements BusinessAttemptStore {
|
|
24
|
+
readonly namespace: string;
|
|
25
|
+
private readonly db;
|
|
26
|
+
constructor(path: string, namespace: string);
|
|
27
|
+
close(): void;
|
|
28
|
+
pending(after: string | undefined, limit: number): Promise<string[]>;
|
|
29
|
+
transaction<T>(id: string, action: (slot: {
|
|
30
|
+
record?: import('./operations.js').BusinessAttempt;
|
|
31
|
+
}) => T): Promise<T>;
|
|
32
|
+
}
|
package/dist/sqlite.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateJson } from './protocol/wire.js';
|
|
1
2
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
3
|
import { initializeSqlite } from './sqlite-init.js';
|
|
3
4
|
export { SqliteAuthStore } from './auth/sqlite-store.js';
|
|
@@ -78,3 +79,44 @@ export class SqliteConversationStore {
|
|
|
78
79
|
this.db.prepare('DELETE FROM checkpoints WHERE connection=? AND issuer=? AND endpoint=? AND conversation=? AND name=?').run(key.connectionId, key.issuer, key.endpoint, conversation, name);
|
|
79
80
|
}
|
|
80
81
|
}
|
|
82
|
+
/** Separate from authentication lifecycle operations. Never discard uncertain business attempts. */
|
|
83
|
+
export class SqliteBusinessAttemptStore {
|
|
84
|
+
namespace;
|
|
85
|
+
db;
|
|
86
|
+
constructor(path, namespace) {
|
|
87
|
+
this.namespace = namespace;
|
|
88
|
+
this.db = new DatabaseSync(path);
|
|
89
|
+
initializeSqlite(this.db, () => this.db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;
|
|
90
|
+
CREATE TABLE IF NOT EXISTS business_attempts (ns TEXT NOT NULL, id TEXT NOT NULL, body TEXT NOT NULL, PRIMARY KEY(ns,id));`));
|
|
91
|
+
}
|
|
92
|
+
close() { this.db.close(); }
|
|
93
|
+
async pending(after, limit) {
|
|
94
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000)
|
|
95
|
+
throw new TypeError('business recovery limit');
|
|
96
|
+
return this.db.prepare("SELECT id FROM business_attempts WHERE ns=? AND id>? AND json_extract(body,'$.phase') IN ('pending_consent','consenting','approved','uncertain') ORDER BY id LIMIT ?").all(this.namespace, after ?? '', limit).map(row => row.id);
|
|
97
|
+
}
|
|
98
|
+
async transaction(id, action) {
|
|
99
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
100
|
+
try {
|
|
101
|
+
const row = this.db.prepare('SELECT body FROM business_attempts WHERE ns=? AND id=?').get(this.namespace, id);
|
|
102
|
+
const slot = { record: row ? JSON.parse(row.body) : undefined };
|
|
103
|
+
if (slot.record && slot.record.version !== 1)
|
|
104
|
+
throw new Error('unsupported business attempt version');
|
|
105
|
+
const result = action(slot);
|
|
106
|
+
if (result && typeof result.then === 'function')
|
|
107
|
+
throw new TypeError('async business attempt transaction');
|
|
108
|
+
if (slot.record) {
|
|
109
|
+
if (slot.record.version !== 1 || slot.record.id !== id)
|
|
110
|
+
throw new TypeError('business attempt binding');
|
|
111
|
+
validateJson(slot.record);
|
|
112
|
+
this.db.prepare('INSERT INTO business_attempts VALUES (?,?,?) ON CONFLICT(ns,id) DO UPDATE SET body=excluded.body').run(this.namespace, id, JSON.stringify(slot.record));
|
|
113
|
+
}
|
|
114
|
+
this.db.exec('COMMIT');
|
|
115
|
+
return structuredClone(result);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
this.db.exec('ROLLBACK');
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Synthetic Company flow. The trusted terminal asks each time; --self-check always declines.
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {mkdtemp,rm} from 'node:fs/promises';
|
|
4
|
+
import {tmpdir} from 'node:os';
|
|
5
|
+
import {join} from 'node:path';
|
|
6
|
+
import {createInterface} from 'node:readline/promises';
|
|
7
|
+
import {createOperationClient} from '../dist/index.js';
|
|
8
|
+
import {SqliteBusinessAttemptStore} from '../dist/sqlite.js';
|
|
9
|
+
const dir=await mkdtemp(join(tmpdir(),'poppy-operation-example-')),store=new SqliteBusinessAttemptStore(join(dir,'attempts.db'),'synthetic-user');
|
|
10
|
+
const selfCheck=process.argv.includes('--self-check');let sends=0;
|
|
11
|
+
try{
|
|
12
|
+
const client=createOperationClient({store,context:async()=>({companyId:'https://example.com',companyName:'Example Company',clientId:'https://agent.example/client',userId:'synthetic',authority:'session:synthetic',operations:true}),
|
|
13
|
+
checkTerms:async(request,operation)=>operation.terms?.order_id===request.arguments?.order_id?[]:['Order differs from your request.'],
|
|
14
|
+
consent:{confirm:async({attemptId,companyId,companyName,summary,terms,differences})=>{
|
|
15
|
+
if(selfCheck)return 'decline';
|
|
16
|
+
if(!process.stdin.isTTY||!process.stderr.isTTY)throw new Error('Trusted terminal required');
|
|
17
|
+
const saved=await client.get(attemptId),display=JSON.stringify({companyId,companyName,summary,terms,differences,request:saved.request,expiresAt:saved.operation.expires_at},null,2).replace(/[\u007f-\u009f\u2028-\u202e\u2066-\u2069]/g,c=>'\\u'+c.charCodeAt(0).toString(16).padStart(4,'0'));
|
|
18
|
+
const terminal=createInterface({input:process.stdin,output:process.stderr});
|
|
19
|
+
try{const answer=await terminal.question(`${display}\nApprove these exact terms? Type yes: `);return answer==='yes'?'approve':'decline';}finally{terminal.close();}
|
|
20
|
+
}},send:async()=>{sends++;return {kind:'completed',result:{synthetic:true,done:true}};}});
|
|
21
|
+
await client.receiveMcp('exchange-order-one',{channel:'mcp',url:'https://example.com/mcp',name:'exchange',arguments:{order_id:'one'}},{isError:true,structuredContent:{operation:{operation_id:'exchange_one',revision:'revision_one',summary:'Exchange order one for the blue jacket for $70.',terms:{order_id:'one',amount:'70.00'},expires_at:new Date(Date.now()+900000).toISOString()}}});
|
|
22
|
+
const decision=await client.requestConsent('exchange-order-one');
|
|
23
|
+
if(decision.phase==='approved')console.log((await client.retry(decision.id)).phase);else console.log(decision.phase);
|
|
24
|
+
if(selfCheck){assert.equal(decision.phase,'declined');assert.equal(sends,0);}
|
|
25
|
+
}finally{store.close();await rm(dir,{recursive:true,force:true});}
|
package/examples/quickstart.mjs
CHANGED
|
@@ -6,8 +6,9 @@ const domain = selfCheck ? 'company.example' : process.argv[2];
|
|
|
6
6
|
if (!domain) throw new Error('usage: node examples/quickstart.mjs DOMAIN|--self-check');
|
|
7
7
|
const transport = selfCheck ? {
|
|
8
8
|
async fetch(request) {
|
|
9
|
+
if (request.url === 'https://company.example/.well-known/oauth-protected-resource') return new Response(null, {status:404});
|
|
9
10
|
assert.equal(request.url, 'https://company.example/.well-known/poppy.json');
|
|
10
|
-
return Response.json({ protocol_version: '0.
|
|
11
|
+
return Response.json({ protocol_version: '0.3', organization: { name: 'Example Company', domain }, web: {} });
|
|
11
12
|
},
|
|
12
13
|
} : createNodeTransport();
|
|
13
14
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poppykit/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Personal Agent Protocol client SDK",
|
|
6
6
|
"repository": {
|
|
@@ -43,7 +43,9 @@
|
|
|
43
43
|
"RELEASING.md",
|
|
44
44
|
"examples",
|
|
45
45
|
"LICENSE",
|
|
46
|
-
"migrations"
|
|
46
|
+
"migrations",
|
|
47
|
+
"CONFORMANCE.md",
|
|
48
|
+
"OPERATIONS.md"
|
|
47
49
|
],
|
|
48
50
|
"exports": {
|
|
49
51
|
".": {
|
|
@@ -80,6 +82,7 @@
|
|
|
80
82
|
}
|
|
81
83
|
},
|
|
82
84
|
"dependencies": {
|
|
85
|
+
"@agentsig/structured-fields": "0.1.1",
|
|
83
86
|
"jose": "6.2.12"
|
|
84
87
|
},
|
|
85
88
|
"license": "MIT",
|