@poppykit/client 0.4.1 → 0.5.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 CHANGED
@@ -5,9 +5,10 @@ for complete option/result types. This map describes the supported integration s
5
5
 
6
6
  | Entry point | Main exports | Guide |
7
7
  | --- | --- | --- |
8
- | `@poppykit/client` | `createMetadataResolver`, `createNodeTransport` | [README](README.md), [delivery](DELIVERY.md) |
8
+ | `@poppykit/client` | `createMetadataResolver`, `signInMethods`, `createNodeTransport` | [README](README.md), [delivery](DELIVERY.md) |
9
9
  | `@poppykit/client` | `createClientAuth`, `AuthFailure`, authentication/identity/input types | [authentication](AUTH.md) |
10
10
  | `@poppykit/client` | `createConversationClient`, `ConversationError`, `parseEventStream` | [delivery](DELIVERY.md) |
11
+ | `@poppykit/client` | `Author`, `AuthorType`, `Message`, `RecordedMessage`, conversation/event types | [delivery](DELIVERY.md) |
11
12
  | `@poppykit/client` | `ApiTarget`, `AgentBrowserController`, `FeedbackReport` types | [integrations](INTEGRATIONS.md) |
12
13
  | `@poppykit/client/sqlite` | `SqliteAuthStore`, `SqliteConversationStore` | [storage](STORAGE.md) |
13
14
  | `@poppykit/client/memory` | `MemoryConversationStore` (non-durable) | [storage](STORAGE.md) |
@@ -17,7 +18,7 @@ for complete option/result types. This map describes the supported integration s
17
18
  ClientAuth methods include `start/get/list/target`, `authorization`,
18
19
  `beginSignIn/continueSignIn/completeSignIn/cancelSignIn/operation`, `renew`,
19
20
  `signOut/retryCleanup/recover`, `authorizedFetch`, `apiTarget/apiFetch`,
20
- `joinBrowser` and `feedback`. Conversation operations bind one connection, issuer
21
+ `joinBrowser`, `feedback` and `excludedTasks`. Conversation operations bind one connection, issuer
21
22
  and endpoint; never substitute profile names or model-provided URLs for that binding.
22
23
 
23
24
  The [quickstart](examples/quickstart.mjs) is executed by CI. Integration examples
package/AUTH.md CHANGED
@@ -5,9 +5,34 @@ replacement, revocation and abandoned-operation recovery. Call `recover()` on
5
5
  startup. Another live process is never interrupted; an ambiguous dispatched
6
6
  exchange is uncertain and must not be replayed.
7
7
 
8
+ Each `start` creates a new Session Key; only the issuer-specific User ID is reused.
9
+ Renewal uses that Session Key. `Connection.sessionId` remains a local identifier
10
+ for the key's lifetime, not the company's internal Session ID. Token requests
11
+ carry no `session_id`, and token responses need neither it nor `signed_in`.
12
+ The grant determines sign-in state. Omitted signed-in scope inherits the request's
13
+ scope context; an explicit response can narrow it but cannot expand it.
14
+
15
+ After a refresh returns definite `invalid_grant`, the client diagnoses the Session
16
+ with a signed-out assertion before discarding the Account Token. A live Session
17
+ means the Account Token is unusable. An ended key is retired durably before a fresh
18
+ key attempts replacement. Ambiguous refresh responses remain uncertain and are
19
+ never resent. Sign-out still disconnects locally before remote cleanup.
20
+
21
+ Discovery accepts only draft 0.3, checks the final well-known path (including
22
+ cache revalidation), exact issuer and reciprocal `protected_resources`.
23
+ `signInMethods(metadata)` derives Direct/Device from issuer grants and endpoints
24
+ and uses `scopes_supported`; Mediated keeps `auth.mediated.scopes`. Company scope
25
+ names cannot start with `poppy:`. Descriptions come from `auth.scopes` or a listed
26
+ OpenAPI document. Prefer Direct when the user can receive its callback; Device is
27
+ for appropriate device-flow situations. No sign-in method is user consent.
28
+
8
29
  Ask `authorization(connectionId, scopes)` for the current grant, missing scopes
9
- and advertised methods. Choose a method explicitly in `beginSignIn`. Direct
10
- requires an HTTPS registered callback and an authenticated browser binding.
30
+ and advertised methods, with `scopeDescriptions` from `auth.scopes`. Choose a method explicitly in `beginSignIn`. Direct
31
+ requires a registered HTTPS callback on the client domain, or an RFC 8252 HTTP
32
+ loopback IP callback (`127.0.0.1` or `[::1]`), and an authenticated browser binding.
33
+ Only a loopback registration's port may vary; callback completion and code exchange
34
+ use the exact port selected at initiation. Metadata/network endpoint address
35
+ protections do not change. The URL's `dpop_jkt` binds the code to this Session Key.
11
36
  Display its URL in the user's browser. Device returns a checked link/code and
12
37
  company identity; call `continueSignIn(operationId)` when its deadline is due.
13
38
  Do not automate user approval. Mediated calls the configured `TrustedInput`
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 — 2026-10-28
4
+
5
+ Target pinned Personal Agent Protocol draft 0.3. Break discovery, Session Key/token, author, browser-controller and feedback contracts; retain explicit storage subpaths. Upgrade normalized auth state 1→2; stop old writers and reset incompatible conversation/outbox state before cutover.
6
+
7
+ Local migration candidate only. Publication and deployment require separate approval.
8
+
3
9
  ## 0.4.1
4
10
 
5
11
  Support Node 24.21.0 and later within Node 24 under the owner-accepted runtime policy.
package/CONTRIBUTING.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Contributing
2
2
 
3
- Follow the live draft 0.2 [specification](https://personalagentprotocol.org/docs/0.2/spec)
4
- and [guides](https://personalagentprotocol.org/docs/0.2/guides). Tests/snapshots are
5
- supporting evidence. Demo/business workflows and individual extensions are excluded.
3
+ Follow the pinned draft 0.3 [specification](https://personalagentprotocol.org/docs/0.3/spec)
4
+ and [guides](https://personalagentprotocol.org/docs/0.3/guides). Tests/snapshots are
5
+ supporting evidence. This unreleased checkout has completed only the discovery
6
+ stage of migration; auth, wire/storage and integrations still need their stages.
7
+ Demo/business workflows and individual extensions are excluded.
6
8
  Code is [MIT](LICENSE); confirm rights for third-party contributions.
7
9
 
8
10
  Use Node 24.21.0, TypeScript 5.9.3 and @types/node 24.10.0.
package/DELIVERY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Conversation delivery and transport
2
2
 
3
- The SDK targets Personal Agent Protocol draft 0.2. Company content, messages,
3
+ Conversation messages and authorization events now use draft 0.3. Company content, messages,
4
4
  unknown events and partial text remain untrusted data for the host application.
5
5
 
6
6
  ## Client
@@ -92,3 +92,16 @@ Pruning is explicit operator policy, never automatic data loss. It preserves old
92
92
  message receipts and cursor identity. Concurrent reads see a coherent snapshot or
93
93
  `cursor_expired`. Invalid/expired waited-POST cursors are rejected before accepting
94
94
  the new write; an existing message ID still returns its original receipt.
95
+
96
+ ## Unreleased draft 0.3 breaking change
97
+
98
+ Messages now require `author: { type: 'agent' | 'human' }`; protocol `sender` is
99
+ unsupported. Human text must remain the person's exact words. Company authors
100
+ may include `id`, `name` and an HTTPS `image`. Authorization events now contain
101
+ `error: 'insufficient_scope'` and nonempty required `scope`.
102
+
103
+ No backward compatibility is provided. Stop writers and explicitly reset old
104
+ history, outboxes, checkpoints and caller retry handles before cutover, preserving
105
+ configuration. Old pending `sender` requests fail `legacy_message_uncertain`
106
+ without dispatch or mutation. Reconcile uncertain delivery before clearing it;
107
+ never rebuild the body or allocate a replacement ID to hide uncertainty.
package/INTEGRATIONS.md CHANGED
@@ -32,8 +32,11 @@ endpoint; extra paths or queries are refused. Query-bearing discovery endpoints
32
32
  not supported. Protected-resource metadata must name that exact resource and the
33
33
  verified company issuer. ES256 DPoP metadata selects DPoP; absence selects Bearer.
34
34
  A Bearer-only MCP resource cannot also authorize a conversation. Resource-specific
35
- credential slots use the shared renewal, rotation, nonce, cooldown and revocation
36
- lifecycle. The wire `token_type` must match verified metadata; there is no fallback
35
+ credential slots use the shared nonce, cooldown and revocation
36
+ lifecycle. Bearer-only MCP credentials come only from Token Exchange of a current
37
+ DPoP Session Token, preserving its Session and limiting scopes to that subject. The
38
+ issuer must advertise the exchange grant; exchange does not rotate the Account Token.
39
+ DPoP-capable MCP keeps the normal token grants. The wire `token_type` must match verified metadata; there is no fallback
37
40
  from DPoP to Bearer. Redirects never forward tokens or proofs. Auth 401 rejection may
38
41
  cause one fresh-token retry; unknown results are not automatically retried.
39
42
 
@@ -42,6 +45,12 @@ cause one fresh-token retry; unknown results are not automatically retried.
42
45
  ```ts
43
46
  await auth.joinBrowser(connectionId, 'https://company.example/account', {
44
47
  kind: 'agent-controlled',
48
+ async loadJoinForm(url) {
49
+ // Top-level GET in this isolated browser; verify the final URL and form action
50
+ // are exactly url. Read the hidden code only here, outside model context.
51
+ return controlledBrowser.loadJoinForm(url);
52
+ },
53
+ navigate: url => controlledBrowser.navigateWithoutPostBody(url),
45
54
  async postForm({ url, fields, validateRedirect }) {
46
55
  // Bind your existing browser controller here. It must enforce this contract:
47
56
  // isolated per-session storage; verified TLS/actual peer addresses; no logging;
@@ -56,13 +65,15 @@ await auth.joinBrowser(connectionId, 'https://company.example/account', {
56
65
  `controlledBrowser` is the host's existing controller, not an SDK driver. This hook
57
66
  is trusted code. Its network layer must reject loopback/private/link-local actual
58
67
  connected addresses unless narrowly configured, validate TLS and enforce redirect
59
- interception. Never replay the assertion on 307/308 or send it in a URL. The supplied
60
- validator permits only 303 to the exact validated `returnTo`. HttpTransport cannot
68
+ interception. Never replay the code or proof on 307/308 or send it in a URL. The supplied
69
+ validator permits only 303 to an HTTPS company page. After that redirect, the SDK
70
+ asks the controller to navigate to the validated `returnTo`. HttpTransport cannot
61
71
  enforce network policy inside an unrelated browser engine. Keep this capability
62
72
  separate from the human consent-browser opener. Never join the user's browser.
63
73
  Use separate cookies/local storage for each session/user, join just before browsing,
64
74
  rejoin on lost recognition and close the context when the session ends. Custom
65
- controllers own those guarantees; the SDK validates the assertion destination and
75
+ controllers own those guarantees; the SDK signs a Session Key proof with the form
76
+ code as nonce (no access token or identity assertion), and validates the destination and
66
77
  navigation contract but cannot inspect their internal browser implementation.
67
78
 
68
79
  ## Feedback
@@ -77,7 +88,11 @@ await auth.feedback(connectionId, {
77
88
  Signed-out sessions are allowed and no account scope is required. Task and
78
89
  description are nonempty strings of at most 200 and 2000 Unicode codepoints. Only
79
90
  `missing` is supported client-side. Omit identifiers, personal data and secrets;
80
- report task limitations rather than individual errors. Success is an empty 202.
91
+ report task limitations rather than individual errors. Success is an empty 202
92
+ (returning undefined) or HTTP 200 with an `Alternative`. The SDK validates the latter
93
+ and returns instructions/references as company data, never as user consent. Unlisted
94
+ OpenAPI/MCP references and unknown reference types are ignored; known malformed
95
+ references fail. Web links must be HTTPS. No alternative is executed automatically.
81
96
  There are no automatic POST retries (including nonce/auth rejection). A lost
82
97
  response can mean accepted; callers decide whether to repeat because the protocol
83
98
  defines no deduplication key. Prefer one report per unfulfilled task.
@@ -85,3 +100,18 @@ defines no deduplication key. Prefer one report per unfulfilled task.
85
100
  The executable generic caller/controller fixtures are `tests/integrations.test.mts`.
86
101
  They exercise the same hooks used by the CLI; installed CLI integration is
87
102
  verified separately against immutable candidate tarballs.
103
+
104
+ ## Task metadata
105
+
106
+ Call `auth.excludedTasks(connectionId)` before choosing how to perform a task.
107
+ It uses `MetadataResolver.resolveExcludedTasks(document)` (optional for custom
108
+ resolvers), fetches the advertised document without credentials and shares the
109
+ resolver's bounded HTTP cache/revalidation. Alternatives use the feedback validator;
110
+ cached references are checked against current discovery on every read.
111
+
112
+ Discovery exposes `auth.scope_hints`: both fields are required, unknown changes
113
+ become irreversible, and unknown fields are ignored. Missing hints remain unknown.
114
+ Hints and excluded task phrases are company claims, never user approval. The host
115
+ agent decides whether a phrase matches its task. The SDK/CLI intentionally omit the
116
+ SHOULD-level automatic pre-task check: they have no task planner; callers choose
117
+ when to read the metadata. No task-policy engine or alternative executor is supplied.
package/PROVIDER.md CHANGED
@@ -75,7 +75,6 @@ const provider = createProvider({
75
75
  const human = await operatorLogin.authenticateEnrollment(request);
76
76
  return human ? { subject: human.stableSubject, allowedCompanies: human.companyDomains } : null;
77
77
  },
78
- verifySession: binding => operatorSessionVerifier.owns(binding),
79
78
  });
80
79
  // Route Fetch Requests under /v1/{operation}, plus the exact public URLs above:
81
80
  // await provider.handle(request)
@@ -99,11 +98,11 @@ but cannot recover another installation's result. Enrollment never accepts calle
99
98
  subjects, arbitrary audiences, lifetimes, key IDs, raw bytes or signing claims.
100
99
 
101
100
  `identity` takes domain and exact issuer; `begin` registers immutable operation,
102
- issuer/domain, purpose (`session`, `client-authentication`, `browser`, `direct`),
103
- optional session and browser return destination. Company metadata is independently
101
+ issuer/domain, purpose (`session`, `client-authentication`, `direct`) and Direct Session Key binding. Company metadata is independently
104
102
  resolved and enrollment company policy checked. `assert` takes operation/purpose
105
- only and constructs a 60-second assertion with fresh 256-bit jti. Browser assertions
106
- are audience-bound to the verified browser endpoint. `createProviderClient` binds
103
+ only and constructs a 60-second assertion with fresh 256-bit jti. Browser-purpose
104
+ operations/assertions and verifySession are removed; browser joining uses the
105
+ locally held Session Key and browser-bound form code. `createProviderClient` binds
107
106
  enrollment credentials and automatically registers SDK assertion operations.
108
107
 
109
108
  Direct sign-in uses SDK-generated 256-bit state. Trusted CLI code privately
@@ -112,7 +111,10 @@ authorization URL. Only its hash is indexed. The callback accepts state, exact
112
111
  issuer, and exactly one code/error; result ciphertext is sealed atomically with
113
112
  the immutable binding. Identical pending callbacks are harmless; conflicting,
114
113
  canceled, expired and consumed callbacks fail. `takeResult` requires enrollment,
115
- installation, operation, issuer and exact session; it consumes within the same
114
+ installation, operation, issuer and exact `sessionKey` thumbprint from the SDK's
115
+ authorization URL `dpop_jkt`; the local `Connection.sessionId` cannot identify a
116
+ company Session. Old Direct operations without the key binding must restart.
117
+ Retrieval consumes within the same
116
118
  storage transaction before returning private callback data. Lost retrieval delivery
117
119
  is uncertain, never a reason to exchange or retrieve again. SDK code owns PKCE and
118
120
  the exchange. Never log request URLs or responses on this route.
package/README.md CHANGED
@@ -1,14 +1,19 @@
1
1
  # PoppyKit client
2
2
 
3
+ Draft 0.3 migration (2026-10-28), pinned to the public source captured that day.
4
+ Client/CLI 0.5.0 and server 0.4.0 break the earlier protocol contracts. Stop old
5
+ writers and follow the documented storage/reset procedure before compatible
6
+ writers resume. Registry, deployment and live acceptance have separate receipts.
7
+
3
8
  The Node 24.21.0 minimum is an owner-accepted compatibility baseline. Use a current patched Node 24 runtime where available; this minimum does not establish that older runtimes contain later security fixes. Build and release CI read `.node-version`.
4
9
 
5
- TypeScript SDK for [Personal Agent Protocol draft 0.2](https://personalagentprotocol.org/docs/0.2/spec):
10
+ TypeScript SDK for [Personal Agent Protocol draft 0.3](https://personalagentprotocol.org/docs/0.3/spec):
6
11
  discovery, authentication, conversations and API/browser/feedback hooks. Your
7
12
  application supplies the agent and trusted infrastructure. [MIT](LICENSE).
8
13
 
9
14
  ## Install
10
15
 
11
- This source contains unreleased repairs; final version selection and publication are pending.
16
+ Use the exact version verified by its protected release workflow; check registry publication before installation.
12
17
 
13
18
  Requires Node **24.x, at least 24.21.0**, ESM and HTTPS. CI tests Node 24.21.0 on Linux, macOS and
14
19
  Windows; other Node majors are unsupported.
@@ -20,7 +25,7 @@ npm install @poppykit/client
20
25
  ## Discover a company
21
26
 
22
27
  No credentials or database are needed. Replace `company.example` with a company
23
- publishing draft 0.2 discovery:
28
+ publishing draft 0.3 discovery:
24
29
 
25
30
  ```js
26
31
  import { createMetadataResolver, createNodeTransport } from '@poppykit/client';
package/STORAGE.md CHANGED
@@ -88,6 +88,18 @@ recovery of a token that the issuer rotated but never delivered.
88
88
 
89
89
  ## Development reset
90
90
 
91
+ Normalized auth state version 1 upgrades to version 2 atomically on first access
92
+ in both SQLite and PostgreSQL, under the namespace's writer lock. Stop old SDK
93
+ processes before opening the candidate adapters; mixed-version writers are not
94
+ supported. This is a record upgrade, with no SQL schema change. Owner, epoch,
95
+ stable User IDs, grants, scopes, cleanup and internal connection/operation IDs stay.
96
+ Legacy connections discard their volatile token slots and require a fresh key on
97
+ next renewal. Old shared-key references remain retained, but are never used for
98
+ another token request. Prepared old operations are cancelled; dispatched/staged
99
+ ones still pass through `recover()`, preserving rotated tokens and uncertainty.
100
+ Custom targeted adapters must provide the same complete-namespace upgrade under
101
+ their writer lock; the client's fallback also upgrades whole-state snapshots.
102
+
91
103
  There is no automatic migration from old whole-state development stores. Stop all
92
104
  users of a disposable store, resolve uncertain remote outcomes, and explicitly move
93
105
  aside its database and WAL/SHM files and the matching host-managed secret namespace
@@ -6,7 +6,7 @@ export function runClientConversationAdapterContract(fixture) {
6
6
  const f = await fixture();
7
7
  try {
8
8
  const key = { connectionId: 'contract', issuer: 'https://issuer.example/one', endpoint: 'https://company.example/conversations' };
9
- const row = { ...key, id: 'message', fingerprint: 'original', phase: 'pending', target: null, request: { message: { id: 'message', sender: 'agent', text: 'hello' } } };
9
+ const row = { ...key, id: 'message', fingerprint: 'original', phase: 'pending', target: null, request: { message: { id: 'message', author: { type: 'agent' }, text: 'hello' } } };
10
10
  const saved = await Promise.all([f.store.prepareOutbound(key, row), f.second.prepareOutbound(key, { ...row, fingerprint: 'changed' })]);
11
11
  assert.equal(saved[0].fingerprint, saved[1].fingerprint);
12
12
  saved[0].request.message.text = 'mutated';
@@ -1,3 +1,4 @@
1
+ import type { Alternative, ExcludedTask } from '../network/tasks.js';
1
2
  import type { AuthSelection, AuthStore } from './store.js';
2
3
  import type { OperationOwner } from './ownership.js';
3
4
  import type { JsonObject } from '../protocol/wire.js';
@@ -22,10 +23,6 @@ export type AssertionRequest = {
22
23
  purpose: 'client-authentication';
23
24
  } | {
24
25
  purpose: 'session';
25
- } | {
26
- purpose: 'browser';
27
- sessionId: string;
28
- returnTo: string;
29
26
  });
30
27
  export interface AssertionProvider {
31
28
  assert(request: AssertionRequest): Promise<{
@@ -41,6 +38,7 @@ export interface ProviderIdentity {
41
38
  }
42
39
  export type SignInMethod = 'direct' | 'device' | 'mediated';
43
40
  export interface AuthorizationRequirement {
41
+ scopeDescriptions?: Record<string, string>;
44
42
  requiredScopes: string[];
45
43
  grantedScopes: string[];
46
44
  missingScopes: string[];
@@ -173,7 +171,8 @@ export interface ClientAuth {
173
171
  apiTarget(connectionId: string, apiUrl: string, destination?: string): Promise<ApiTarget>;
174
172
  apiFetch(connectionId: string, request: Request, target: ApiTarget, requiredScopes?: readonly string[]): Promise<Response>;
175
173
  joinBrowser(connectionId: string, returnTo: string, controller: AgentBrowserController): Promise<void>;
176
- feedback(connectionId: string, report: FeedbackReport): Promise<void>;
174
+ feedback(connectionId: string, report: FeedbackReport): Promise<Alternative | void>;
175
+ excludedTasks(connectionId: string): Promise<ExcludedTask[]>;
177
176
  }
178
177
  export interface CredentialRecord {
179
178
  secretRef: string;
@@ -197,6 +196,7 @@ export interface ConnectionRecord {
197
196
  slots: Record<string, CredentialRecord>;
198
197
  anonymous?: boolean;
199
198
  notBefore?: string;
199
+ needsSessionKey?: boolean;
200
200
  }
201
201
  export interface GrantRecord {
202
202
  id: string;
@@ -239,6 +239,7 @@ export interface OperationRecord {
239
239
  method?: SignInMethod;
240
240
  requiredExistingScopes?: string[];
241
241
  signInSessionId?: string;
242
+ signInSessionKey?: string;
242
243
  device?: {
243
244
  codeRef: string;
244
245
  intervalMs: number;
@@ -270,7 +271,7 @@ export interface CleanupRecord {
270
271
  notBefore?: string;
271
272
  }
272
273
  export interface ClientAuthState {
273
- version: 1;
274
+ version: 1 | 2;
274
275
  epoch: string;
275
276
  owner: {
276
277
  namespace: string;
@@ -279,7 +280,7 @@ export interface ClientAuthState {
279
280
  } | null;
280
281
  identities: Record<string, {
281
282
  userId: string;
282
- dpopKeyRef: string;
283
+ dpopKeyRef?: string;
283
284
  }>;
284
285
  connections: Record<string, ConnectionRecord>;
285
286
  grants: Record<string, GrantRecord>;
@@ -294,4 +295,6 @@ export declare class AuthFailure extends Error {
294
295
  constructor(code: string, connectionId?: string | undefined, retryAfterMs?: number | undefined, retryAt?: string | undefined);
295
296
  }
296
297
  export declare function initialClientAuthState(): ClientAuthState;
298
+ /** Upgrade a complete namespace under its writer lock; dispatched/staged work still goes through recover(). */
299
+ export declare function upgradeClientAuthState(state: ClientAuthState): void;
297
300
  export declare function createClientAuth(options: ClientAuthOptions): ClientAuth;