@poppykit/client 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 +25 -0
- package/AUTH.md +51 -0
- package/CHANGELOG.md +22 -0
- package/CONTRIBUTING.md +50 -0
- package/DELIVERY.md +113 -0
- package/INTEGRATIONS.md +87 -0
- package/LICENSE +21 -0
- package/PROVIDER.md +99 -0
- package/README.md +123 -2
- package/RELEASING.md +193 -0
- package/SECURITY.md +13 -0
- package/STORAGE.md +99 -0
- package/dist/auth/challenge.d.ts +19 -0
- package/dist/auth/challenge.js +68 -0
- package/dist/auth/client-auth.d.ts +297 -0
- package/dist/auth/client-auth.js +1583 -0
- package/dist/auth/ownership.d.ts +9 -0
- package/dist/auth/ownership.js +23 -0
- package/dist/auth/sqlite-store.d.ts +14 -0
- package/dist/auth/sqlite-store.js +123 -0
- package/dist/auth/store.d.ts +19 -0
- package/dist/auth/store.js +1 -0
- package/dist/conversation-store.d.ts +15 -0
- package/dist/conversation-store.js +1 -0
- package/dist/conversation.d.ts +118 -0
- package/dist/conversation.js +404 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +7 -0
- package/dist/integrations.d.ts +30 -0
- package/dist/integrations.js +58 -0
- package/dist/memory.d.ts +15 -0
- package/dist/memory.js +32 -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/provider-sqlite.d.ts +8 -0
- package/dist/provider-sqlite.js +30 -0
- package/dist/provider.d.ts +121 -0
- package/dist/provider.js +342 -0
- package/dist/sqlite-init.d.ts +2 -0
- package/dist/sqlite-init.js +17 -0
- package/dist/sqlite.d.ts +20 -0
- package/dist/sqlite.js +80 -0
- package/examples/quickstart.mjs +19 -0
- package/package.json +72 -4
package/API.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
Every export has an accompanying `dist/*.d.ts` declaration; use editor completion
|
|
4
|
+
for complete option/result types. This map describes the supported integration seams.
|
|
5
|
+
|
|
6
|
+
| Entry point | Main exports | Guide |
|
|
7
|
+
| --- | --- | --- |
|
|
8
|
+
| `@poppykit/client` | `createMetadataResolver`, `createNodeTransport` | [README](README.md), [delivery](DELIVERY.md) |
|
|
9
|
+
| `@poppykit/client` | `createClientAuth`, `AuthFailure`, authentication/identity/input types | [authentication](AUTH.md) |
|
|
10
|
+
| `@poppykit/client` | `createConversationClient`, `ConversationError`, `parseEventStream` | [delivery](DELIVERY.md) |
|
|
11
|
+
| `@poppykit/client` | `ApiTarget`, `AgentBrowserController`, `FeedbackReport` types | [integrations](INTEGRATIONS.md) |
|
|
12
|
+
| `@poppykit/client/sqlite` | `SqliteAuthStore`, `SqliteConversationStore` | [storage](STORAGE.md) |
|
|
13
|
+
| `@poppykit/client/memory` | `MemoryConversationStore` (non-durable) | [storage](STORAGE.md) |
|
|
14
|
+
| `@poppykit/client/provider` | `createProvider`, `createProviderClient`, `createResultCipher` | [provider](PROVIDER.md) |
|
|
15
|
+
| `@poppykit/client/provider/sqlite` | `SqliteProviderStore` | [provider](PROVIDER.md) |
|
|
16
|
+
|
|
17
|
+
ClientAuth methods include `start/get/list/target`, `authorization`,
|
|
18
|
+
`beginSignIn/continueSignIn/completeSignIn/cancelSignIn/operation`, `renew`,
|
|
19
|
+
`signOut/retryCleanup/recover`, `authorizedFetch`, `apiTarget/apiFetch`,
|
|
20
|
+
`joinBrowser` and `feedback`. Conversation operations bind one connection, issuer
|
|
21
|
+
and endpoint; never substitute profile names or model-provided URLs for that binding.
|
|
22
|
+
|
|
23
|
+
The [quickstart](examples/quickstart.mjs) is executed by CI. Integration examples
|
|
24
|
+
describe adapters your application supplies; they are not built-in browser drivers
|
|
25
|
+
or general API executors.
|
package/AUTH.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Authentication lifecycle
|
|
2
|
+
|
|
3
|
+
`createClientAuth` owns Direct, Device and Mediated sign-in, token renewal,
|
|
4
|
+
replacement, revocation and abandoned-operation recovery. Call `recover()` on
|
|
5
|
+
startup. Another live process is never interrupted; an ambiguous dispatched
|
|
6
|
+
exchange is uncertain and must not be replayed.
|
|
7
|
+
|
|
8
|
+
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.
|
|
11
|
+
Display its URL in the user's browser. Device returns a checked link/code and
|
|
12
|
+
company identity; call `continueSignIn(operationId)` when its deadline is due.
|
|
13
|
+
Do not automate user approval. Mediated calls the configured `TrustedInput`
|
|
14
|
+
adapter for declared fields and correlated one-time codes. Only trusted UI code
|
|
15
|
+
implements this adapter. Values are cleared after each submission and never
|
|
16
|
+
persisted by the SDK or exposed in public results. Input adapters must not log
|
|
17
|
+
values, keep copies, or return them through model tools.
|
|
18
|
+
|
|
19
|
+
First-time partial grants complete with `grantedScopes` and `missingScopes`.
|
|
20
|
+
Replacement preserves every old grant scope by default. An explicit
|
|
21
|
+
`requiredExistingScopes` subset permits dropping other old scopes. A rejected
|
|
22
|
+
replacement retains the previous grant and queues the new token for revocation;
|
|
23
|
+
accepted replacements queue the previous token instead. Grants are never unioned.
|
|
24
|
+
`force: true` represents an explicit user request to sign in again. Otherwise a
|
|
25
|
+
sufficient current grant is reused. Run `retryCleanup` to retry durable retirement.
|
|
26
|
+
|
|
27
|
+
Metadata is refreshed through the supplied resolver. A changed exact issuer or
|
|
28
|
+
saved destination fails with `metadata_changed`; start a new connection after
|
|
29
|
+
reviewing the new metadata. Saved work is never rerouted. Server Retry-After
|
|
30
|
+
deadlines are persisted as absolute decimal milliseconds, including enormous
|
|
31
|
+
values, and survive restarts without a local delay cap.
|
|
32
|
+
|
|
33
|
+
`start(domain, { anonymous: true })` uses a fresh identity and DPoP key. Ending
|
|
34
|
+
it deletes that private key and local identity; sign-in starts a new regular
|
|
35
|
+
connection/session. Consume the returned connection ID and do not transfer old
|
|
36
|
+
conversations. Regular issuer identity remains stable and separate.
|
|
37
|
+
|
|
38
|
+
Provider adapters implement J1's `AssertionProvider` and `ProviderIdentity`;
|
|
39
|
+
their authenticated identity supplies the issuer-specific UserID. Only purpose,
|
|
40
|
+
issuer and operation binding are passed for assertions, never arbitrary bytes
|
|
41
|
+
or claims. The operator must publish the signing key. For anonymous sessions a
|
|
42
|
+
separate local `IdentitySigner` is required and its key must also be published in
|
|
43
|
+
the client's JWKS. A provider-only configuration reports `anonymous_unavailable`;
|
|
44
|
+
it never substitutes the provider's stable identity for an anonymous one.
|
|
45
|
+
|
|
46
|
+
Account Tokens, device codes, PKCE and DPoP keys belong in the configured
|
|
47
|
+
SecretStore; Session Tokens stay in memory. SQLite and secret storage are separate
|
|
48
|
+
commit boundaries. A host must sweep orphan staged secrets only after checking
|
|
49
|
+
ownership and a grace period; crashes between stage and durable reference can
|
|
50
|
+
leave such orphans. Cancellation/revocation generation checks defeat late results.
|
|
51
|
+
Revocation stops future protected work and never automatically reconnects.
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — initial development release candidate (unpublished)
|
|
4
|
+
|
|
5
|
+
Discovery, safe HTTPS, all three sign-in methods, partial grants/replacement,
|
|
6
|
+
owner-aware recovery, JSON/SSE conversations, API/browser/feedback hooks, targeted
|
|
7
|
+
SQLite adapters and optional constrained hosted-identity provider.
|
|
8
|
+
|
|
9
|
+
- Select MIT licensing and ship the complete LICENSE.
|
|
10
|
+
- Prepare public package metadata and the initial 0.1.0 version.
|
|
11
|
+
- Preserve J8 strict TypeScript tests, real subprocess/security regressions and
|
|
12
|
+
immutable installed-package verification on Linux, macOS and Windows.
|
|
13
|
+
|
|
14
|
+
No npm publication has occurred. Version 0.x APIs/storage may still change; record
|
|
15
|
+
compatibility and migration/reset implications with future changes. This release
|
|
16
|
+
does not promise stable 1.0 compatibility or certify external host integrations.
|
|
17
|
+
|
|
18
|
+
## 0.0.0 — historical private development candidates
|
|
19
|
+
|
|
20
|
+
J1–J8 established protocol capabilities, targeted storage, durable recovery,
|
|
21
|
+
documentation, strict tests and fail-closed release verification. Their prior
|
|
22
|
+
commits/artifact results remain historical evidence for those exact candidates.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
This package targets Personal Agent Protocol draft 0.2. Check the current
|
|
4
|
+
[specification](https://personalagentprotocol.org/docs/0.2/spec) and
|
|
5
|
+
[guides](https://personalagentprotocol.org/docs/0.2/guides); retained snapshots and
|
|
6
|
+
tests are supporting evidence. Distinguish MUST requirements, SHOULD recommendations
|
|
7
|
+
and product choices. Individual extensions and demo/business workflows are excluded.
|
|
8
|
+
|
|
9
|
+
Use Node 24.12.0, TypeScript 5.9.3 and @types/node 24.10.0.
|
|
10
|
+
Install with the pinned pnpm version in package.json and `pnpm install --frozen-lockfile`.
|
|
11
|
+
Run `npm run typecheck`, `npm run build`, `npm test` and `npm run check:docs`.
|
|
12
|
+
CI partitions tests without omitting any test file; full `npm test` remains useful
|
|
13
|
+
locally. Read [RELEASING.md](RELEASING.md) before changing workflow/release behavior.
|
|
14
|
+
|
|
15
|
+
Keep changes small and complete: include callers, declarations, regression checks
|
|
16
|
+
and documentation. Do not log credentials, weaken destination validation or silently
|
|
17
|
+
reset persisted data. Pull requests must explain checks actually run and limitations.
|
|
18
|
+
Use the repository's Issues for non-sensitive defects and feature discussions.
|
|
19
|
+
Code is licensed under [MIT](LICENSE). No separate contribution agreement is
|
|
20
|
+
configured; confirm permission for third-party contributions and obtain owner
|
|
21
|
+
authorization before publishing.
|
|
22
|
+
|
|
23
|
+
## TypeScript test harness and coverage
|
|
24
|
+
|
|
25
|
+
First-party tests, executable fixtures, process workers and packaged verification
|
|
26
|
+
scripts are canonical `.mts` files. Run `npm run typecheck:tests` for the complete
|
|
27
|
+
strict program and `npm run build:tests` before individual
|
|
28
|
+
`node scripts/run-checks.mjs GROUP` commands (unit, contracts, integration or
|
|
29
|
+
validation). `npm test` performs that checked harness build and runs all groups.
|
|
30
|
+
Build production first with `npm run build`; tests import compiled `dist/*.js`,
|
|
31
|
+
not TypeScript source. No loader or native type stripping replaces the compiler.
|
|
32
|
+
|
|
33
|
+
The existing TypeScript compiler emits ignored adjacent `.mjs` files only after
|
|
34
|
+
all strict diagnostics pass. Failed builds remove stale canonical outputs.
|
|
35
|
+
`scripts/build-tests.mjs` is compiler/bootstrap tooling, not an unchecked test
|
|
36
|
+
runner. Canonical test sources use LF, enforced by `.gitattributes`, so Linux and
|
|
37
|
+
Windows validate the same exact source bytes. Test output stays outside the npm
|
|
38
|
+
package allowlist. Retained JavaScript release/build tools and packaged examples
|
|
39
|
+
are genuine production tooling/examples; retained protocol captures are references.
|
|
40
|
+
|
|
41
|
+
Run `npm run coverage` for a production build, checked harness and native Node
|
|
42
|
+
24.12.0 line/branch/function report. `coverage/summary.txt`, `coverage/lcov.info`,
|
|
43
|
+
`coverage/report.json` and `coverage/raw/` are generated local reports. Metrics
|
|
44
|
+
refer to first-party compiled `dist` JavaScript; production emits no source maps
|
|
45
|
+
and no TypeScript remapping is claimed. Third-party `node_modules` is excluded;
|
|
46
|
+
unloaded modules are listed explicitly. Raw child-process reports are retained,
|
|
47
|
+
but arbitrary subprocess execution is not guaranteed to be merged into the native
|
|
48
|
+
runner summary, and killed/crashed workers may not flush counters. Actual installed
|
|
49
|
+
package/OS checks remain separate evidence. No percentage threshold substitutes
|
|
50
|
+
for protocol, negative/security, process and platform assertions.
|
package/DELIVERY.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Conversation delivery and transport
|
|
2
|
+
|
|
3
|
+
The SDK targets Personal Agent Protocol draft 0.2. Company content, messages,
|
|
4
|
+
unknown events and partial text remain untrusted data for the host application.
|
|
5
|
+
|
|
6
|
+
## Client
|
|
7
|
+
|
|
8
|
+
Bind `createConversationClient` to the existing auth lifecycle:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
const target = await auth.target(connectionId);
|
|
12
|
+
const conversations = createConversationClient({
|
|
13
|
+
...target, connectionId, store, clock: { now: Date.now },
|
|
14
|
+
authorizedRequest: request => auth.authorizedFetch(connectionId, request, target),
|
|
15
|
+
authorization: (id, scopes) => auth.authorization(id, scopes),
|
|
16
|
+
});
|
|
17
|
+
for await (const item of conversations.watch(conversationId, { consumer: 'ui', signal })) {
|
|
18
|
+
await processEvent(item.event);
|
|
19
|
+
await item.ack();
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`authorization(eventOrError)` returns the single auth lifecycle's verified
|
|
24
|
+
required/granted/missing scopes and eligible methods. The UI requests consent and
|
|
25
|
+
chooses an advertised method; Device or Mediated does not require Direct support.
|
|
26
|
+
|
|
27
|
+
`read` returns one page; `drain` follows `has_more` immediately. Named consumers
|
|
28
|
+
advance only on explicit acknowledgement. `watch` accepts SSE or JSON fallback,
|
|
29
|
+
skips unknown event types, deduplicates up to 10,000 event IDs per invocation, and
|
|
30
|
+
never rewinds its cursor on a duplicate. JSON pagination has its separate
|
|
31
|
+
`maxPages` bound (default 1000), without consuming reconnect attempts. Reconnects
|
|
32
|
+
are bounded (default 4); exhaustion is explicit `reconnect_exhausted`.
|
|
33
|
+
|
|
34
|
+
`lastEventId` takes precedence over the starting cursor. On reconnect the watcher
|
|
35
|
+
sends `Last-Event-ID`. `onPartial` is display-only: a null value clears unfinished
|
|
36
|
+
text on completion/disconnect; never execute or reason from an incomplete delta.
|
|
37
|
+
Messages are actionable only after a complete `message` event. SSE frames and
|
|
38
|
+
JSON fallback are limited to 1 MiB, streams to 16 MiB, and partial text per stream
|
|
39
|
+
to 1 MiB. UTF-8 fragmentation and CR/LF/CRLF are supported.
|
|
40
|
+
|
|
41
|
+
`cursor_expired` is an explicit history gap. Inform the user, then reset a named
|
|
42
|
+
consumer and reread retained history, or supply `onHistoryGap` returning true.
|
|
43
|
+
Automatic recovery is limited to one reset per watch; it does not recover missing
|
|
44
|
+
events. Checkpoints and pending sends survive process restart with durable stores.
|
|
45
|
+
Send retries keep their original connection, issuer, endpoint, ID, body and read
|
|
46
|
+
parameters. Abort signals are invocation-local and are never persisted. Canceling
|
|
47
|
+
delivery does not cancel an accepted company action or prove a lost send failed.
|
|
48
|
+
|
|
49
|
+
## Node transport
|
|
50
|
+
|
|
51
|
+
`createNodeTransport` uses Node HTTPS agents, with separate discovery, JSON and
|
|
52
|
+
stream pools. Defaults: 8 pools total, 2 connections per pool (including pending
|
|
53
|
+
TLS), 32 active operations, 15-second JSON timeout, and 5-second discovery timeout.
|
|
54
|
+
There is no hidden request queue: excess pool capacity fails `overloaded`, so a
|
|
55
|
+
later attempt performs fresh DNS and policy checks. Hosts decide scheduling.
|
|
56
|
+
Streams use `transport.stream` with a 5-minute/16-MiB lifetime budget. Call
|
|
57
|
+
`transport.close()` to cancel outstanding reads, DNS waiters, connects and streams.
|
|
58
|
+
|
|
59
|
+
Every request rechecks DNS and current address policy before pool assignment.
|
|
60
|
+
Changed approved addresses retire incompatible pools; actual TLS peers and reused
|
|
61
|
+
sockets are checked before HTTP bytes. TLS certificates remain verified. Only
|
|
62
|
+
explicit hostname/address exceptions permit private peers. Discovery never carries
|
|
63
|
+
credentials and follows only HTTPS redirects; credential-bearing requests never
|
|
64
|
+
follow redirects. Pool retirement may interrupt an active stream; reconnect uses
|
|
65
|
+
the durable cursor and fresh authorization.
|
|
66
|
+
|
|
67
|
+
Metadata caching remains bounded and honors freshness, Age, validators, no-cache
|
|
68
|
+
and no-store, with a 300-second upper bound. Expired entries revalidate; 304s retain
|
|
69
|
+
prior caching directives. `resolver.invalidate(target?)` fences old pending cache
|
|
70
|
+
writes without bypassing fetch concurrency/rate limits. J2 rejects changed saved
|
|
71
|
+
issuer/endpoint/resource bindings rather than rerouting persisted work.
|
|
72
|
+
|
|
73
|
+
## Company service and retention
|
|
74
|
+
|
|
75
|
+
The server accepts `supportedScopes`, the union offered by its actual sign-in
|
|
76
|
+
methods. `directScopes` remains a compatibility alias. One coordinator must still
|
|
77
|
+
cover authority checks and durable conversation reads/mutations.
|
|
78
|
+
|
|
79
|
+
Default JSON/request/frame budget is 1 MiB; the maximum encoded event is
|
|
80
|
+
1 MiB minus 1025 bytes. Page sizing reserves 1024 bytes for worst-case IDs, cursors
|
|
81
|
+
and envelopes and counts UTF-8 bytes. Company events and client messages that
|
|
82
|
+
cannot fit are rejected before commit; no receipt/work survives a rejected write.
|
|
83
|
+
Do not reduce configured budgets below those used for retained accepted events.
|
|
84
|
+
|
|
85
|
+
Notifications are post-commit hints. Subscribe before reading; reconcile durable
|
|
86
|
+
state on wake or every 1000 ms by default. `createConversationNotifications` is
|
|
87
|
+
local acceleration only. Multiple processes work without a broker through targeted
|
|
88
|
+
SQLite reconciliation; deployments can implement the same notification contract
|
|
89
|
+
with their own distributed adapter. Lost/duplicate hints cannot erase a commit.
|
|
90
|
+
SQLite event windows and append operations do not reload complete event histories.
|
|
91
|
+
|
|
92
|
+
Server streams default to 30 seconds and apply pull backpressure, rechecking
|
|
93
|
+
current authority before further pages. This bounded-connection policy omits
|
|
94
|
+
periodic idle comments; proxies may close an idle stream sooner, and the client
|
|
95
|
+
reconnects. Longer-lived deployments should supply heartbeat handling at their
|
|
96
|
+
HTTP adapter. Hosts must propagate disconnect/AbortSignal and body cancellation.
|
|
97
|
+
|
|
98
|
+
| Record | Lifetime / deletion rule |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| Conversation and message receipts | Entire conversation lifetime, including after closure and payload pruning; no automatic deletion |
|
|
101
|
+
| Event payloads | Retained indefinitely by default; host may call SQLite `pruneEvents(id, before, limit)` in bounded batches; recommend at least 30 days |
|
|
102
|
+
| Event cursor identities | Conversation lifetime; distinguish expired payloads (410) from foreign cursors (400) |
|
|
103
|
+
| Active work | Never pruned while held/pending/claimed; event pruning protects these conversations |
|
|
104
|
+
| Completed work | Bounded inspection retention; stale fences cannot publish |
|
|
105
|
+
| Latest user message, context and account binding | Operational conversation record, retained for its lifetime, including handoff after payload pruning |
|
|
106
|
+
| Client outbound/checkpoints | Retained until explicit local store deletion/reset; preserve pending/uncertain sends until reconciled |
|
|
107
|
+
| Pending auth and replay records | Separate auth validity/owner rules in AUTH.md and STORAGE.md; not the event TTL |
|
|
108
|
+
| CLI transcripts/retries | CLI ownership; local deletion never deletes company history or revokes authorization |
|
|
109
|
+
|
|
110
|
+
Pruning is explicit operator policy, never automatic data loss. It preserves old
|
|
111
|
+
message receipts and cursor identity. Concurrent reads see a coherent snapshot or
|
|
112
|
+
`cursor_expired`. Invalid/expired waited-POST cursors are rejected before accepting
|
|
113
|
+
the new write; an existing message ID still returns its original receipt.
|
package/INTEGRATIONS.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# API, browser and feedback hooks
|
|
2
|
+
|
|
3
|
+
Use the same `ClientAuth` instance and trusted adapters as sign-in and conversations.
|
|
4
|
+
Do not expose credential-bearing hooks, request headers, browser form fields or
|
|
5
|
+
private responses as printable model tools. Company descriptions and responses are
|
|
6
|
+
untrusted data. These hooks do not execute OpenAPI operations, MCP tools or browser
|
|
7
|
+
automation for you.
|
|
8
|
+
|
|
9
|
+
## Existing API clients
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// Read the API description without credentials using the configured safe transport.
|
|
13
|
+
const description = await transport.fetch(new Request(descriptionUrl), { mode: 'discovery' });
|
|
14
|
+
// An existing OpenAPI client selects its server and constructs its final Request.
|
|
15
|
+
const target = await auth.apiTarget(connectionId, descriptionUrl, 'https://company.example/api');
|
|
16
|
+
const fetchForClient = (request: Request) => auth.apiFetch(connectionId, request, target, ['records:read']);
|
|
17
|
+
const result = await fetchForClient(new Request('https://company.example/api/records'));
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
OpenAPI `apiUrl` is the description URL from verified company discovery. The third
|
|
21
|
+
argument is the actual destination base; its host must independently serve verified
|
|
22
|
+
company discovery naming the same issuer. This conservative policy requires API
|
|
23
|
+
hosts to publish discovery; a document's `servers` field alone grants no authority.
|
|
24
|
+
Final requests must stay within the verified origin and slash-delimited base path.
|
|
25
|
+
The proof binds the actual final method and URL. Destination changes require a new
|
|
26
|
+
explicit connection; saved work never moves silently.
|
|
27
|
+
|
|
28
|
+
For MCP, use `auth.apiTarget(connectionId, mcpUrl)` and the same fetch closure in an
|
|
29
|
+
existing Streamable HTTP MCP client (version 2025-06-18 or later). The SDK does not
|
|
30
|
+
encode JSON-RPC or manage MCP sessions. The exact discovery URL is the resource and
|
|
31
|
+
endpoint; extra paths or queries are refused. Query-bearing discovery endpoints are
|
|
32
|
+
not supported. Protected-resource metadata must name that exact resource and the
|
|
33
|
+
verified company issuer. ES256 DPoP metadata selects DPoP; absence selects Bearer.
|
|
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
|
|
37
|
+
from DPoP to Bearer. Redirects never forward tokens or proofs. Auth 401 rejection may
|
|
38
|
+
cause one fresh-token retry; unknown results are not automatically retried.
|
|
39
|
+
|
|
40
|
+
## Agent-controlled browser
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
await auth.joinBrowser(connectionId, 'https://company.example/account', {
|
|
44
|
+
kind: 'agent-controlled',
|
|
45
|
+
async postForm({ url, fields, validateRedirect }) {
|
|
46
|
+
// Bind your existing browser controller here. It must enforce this contract:
|
|
47
|
+
// isolated per-session storage; verified TLS/actual peer addresses; no logging;
|
|
48
|
+
// top-level form POST; intercept redirects BEFORE following or replaying a body.
|
|
49
|
+
const response = await controlledBrowser.postFormWithoutFollowing(url, fields);
|
|
50
|
+
const destination = validateRedirect(response.status, response.location);
|
|
51
|
+
await controlledBrowser.navigateWithoutPostBody(destination);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`controlledBrowser` is the host's existing controller, not an SDK driver. This hook
|
|
57
|
+
is trusted code. Its network layer must reject loopback/private/link-local actual
|
|
58
|
+
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
|
|
61
|
+
enforce network policy inside an unrelated browser engine. Keep this capability
|
|
62
|
+
separate from the human consent-browser opener. Never join the user's browser.
|
|
63
|
+
Use separate cookies/local storage for each session/user, join just before browsing,
|
|
64
|
+
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
|
|
66
|
+
navigation contract but cannot inspect their internal browser implementation.
|
|
67
|
+
|
|
68
|
+
## Feedback
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
await auth.feedback(connectionId, {
|
|
72
|
+
kind: 'missing', task: 'Export records',
|
|
73
|
+
description: 'There is no export operation in the API, conversation or website.',
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Signed-out sessions are allowed and no account scope is required. Task and
|
|
78
|
+
description are nonempty strings of at most 200 and 2000 Unicode codepoints. Only
|
|
79
|
+
`missing` is supported client-side. Omit identifiers, personal data and secrets;
|
|
80
|
+
report task limitations rather than individual errors. Success is an empty 202.
|
|
81
|
+
There are no automatic POST retries (including nonce/auth rejection). A lost
|
|
82
|
+
response can mean accepted; callers decide whether to repeat because the protocol
|
|
83
|
+
defines no deduplication key. Prefer one report per unfulfilled task.
|
|
84
|
+
|
|
85
|
+
The executable generic caller/controller fixtures are `tests/integrations.test.mts`.
|
|
86
|
+
They exercise the same hooks used by the CLI; installed CLI integration is
|
|
87
|
+
verified separately against immutable candidate tarballs.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PoppyKit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/PROVIDER.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Optional identity provider
|
|
2
|
+
|
|
3
|
+
Import `createProvider`, `createProviderClient`, and `createResultCipher` from
|
|
4
|
+
`@poppykit/client/provider`; durable `SqliteProviderStore` is separately exported
|
|
5
|
+
from `@poppykit/client/provider/sqlite`. Neither is loaded by the core import.
|
|
6
|
+
This is PoppyKit provider contract **v1**, not a Personal Agent Protocol extension.
|
|
7
|
+
Node 24.12+ is required for the SQLite reference adapter.
|
|
8
|
+
|
|
9
|
+
An operator supplies HTTPS hosting, a provider client metadata URL, JWKS URL,
|
|
10
|
+
HTTPS callback URL on that same domain, an ES256 signer/KMS, a verified company
|
|
11
|
+
metadata resolver, authenticated enrollment and encrypted callback-result storage.
|
|
12
|
+
The provider has no company account-token vault, token-exchange proxy, login UI,
|
|
13
|
+
deployment dependency or cloud dependency.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { createProvider, createResultCipher } from '@poppykit/client/provider';
|
|
17
|
+
import { SqliteProviderStore } from '@poppykit/client/provider/sqlite';
|
|
18
|
+
const provider = createProvider({
|
|
19
|
+
clientId: 'https://identity.example/client.json',
|
|
20
|
+
jwksUri: 'https://identity.example/jwks',
|
|
21
|
+
callbackUrl: 'https://identity.example/callback', clientName: 'Operator agent',
|
|
22
|
+
store: new SqliteProviderStore('/private/provider.sqlite'),
|
|
23
|
+
cipher: createResultCipher(await operatorKeys.readResultEncryptionKey()),
|
|
24
|
+
signer: operatorKeys.signer, metadata: verifiedCompanyResolver,
|
|
25
|
+
authenticateEnrollment: async request => {
|
|
26
|
+
// Authenticate the human using the operator's existing login; validate CSRF.
|
|
27
|
+
const human = await operatorLogin.authenticateEnrollment(request);
|
|
28
|
+
return human ? { subject: human.stableSubject, allowedCompanies: human.companyDomains } : null;
|
|
29
|
+
},
|
|
30
|
+
verifySession: binding => operatorSessionVerifier.owns(binding),
|
|
31
|
+
});
|
|
32
|
+
// Route Fetch Requests under /v1/{operation}, plus the exact public URLs above:
|
|
33
|
+
// await provider.handle(request)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`operatorKeys`, `operatorLogin`, `operatorSessionVerifier` and
|
|
37
|
+
`verifiedCompanyResolver` are operator-owned adapters, not globals supplied by
|
|
38
|
+
the package. A session verifier must independently verify both the provider client
|
|
39
|
+
and derived UserID; an opaque session ID alone does not prove ownership. Browser
|
|
40
|
+
signing fails closed without that verifier. Starting a session signs a derived
|
|
41
|
+
UserID assertion; the company checks session/client/UserID association on renewals.
|
|
42
|
+
|
|
43
|
+
POST JSON operations use `version: 1` and `installationId`. Enroll additionally
|
|
44
|
+
takes a fresh `idempotencyKey`; private operations carry `Authorization: Bearer
|
|
45
|
+
<enrollmentId.secret>`. The secret is generated once and delivered only in that
|
|
46
|
+
authenticated enrollment response. A repeated enrollment key returns `consumed`;
|
|
47
|
+
lost credential delivery requires fresh human authentication and enrollment.
|
|
48
|
+
Installations have independent credentials and callback operations. Re-enrollment
|
|
49
|
+
can recover a visitor identity through the same authenticated operator subject,
|
|
50
|
+
but cannot recover another installation's result. Enrollment never accepts caller
|
|
51
|
+
subjects, arbitrary audiences, lifetimes, key IDs, raw bytes or signing claims.
|
|
52
|
+
|
|
53
|
+
`identity` takes domain and exact issuer; `begin` registers immutable operation,
|
|
54
|
+
issuer/domain, purpose (`session`, `client-authentication`, `browser`, `direct`),
|
|
55
|
+
optional session and browser return destination. Company metadata is independently
|
|
56
|
+
resolved and enrollment company policy checked. `assert` takes operation/purpose
|
|
57
|
+
only and constructs a 60-second assertion with fresh 256-bit jti. Browser assertions
|
|
58
|
+
are audience-bound to the verified browser endpoint. `createProviderClient` binds
|
|
59
|
+
enrollment credentials and automatically registers SDK assertion operations.
|
|
60
|
+
|
|
61
|
+
Direct sign-in uses SDK-generated 256-bit state. Trusted CLI code privately
|
|
62
|
+
registers it with `beginDirect` after SDK preparation and before displaying the
|
|
63
|
+
authorization URL. Only its hash is indexed. The callback accepts state, exact
|
|
64
|
+
issuer, and exactly one code/error; result ciphertext is sealed atomically with
|
|
65
|
+
the immutable binding. Identical pending callbacks are harmless; conflicting,
|
|
66
|
+
canceled, expired and consumed callbacks fail. `takeResult` requires enrollment,
|
|
67
|
+
installation, operation, issuer and exact session; it consumes within the same
|
|
68
|
+
SQLite transaction before returning private callback data. Lost retrieval delivery
|
|
69
|
+
is uncertain, never a reason to exchange or retrieve again. SDK code owns PKCE and
|
|
70
|
+
the exchange. Never log request URLs or responses on this route.
|
|
71
|
+
|
|
72
|
+
SQLite serializes writers across processes. Provider transactions must be
|
|
73
|
+
synchronous, targeted and atomic; custom stores implement `ProviderStore` with
|
|
74
|
+
the same guarantees. The AES-GCM result key comes from an independent secure
|
|
75
|
+
backend and must never be stored beside the database. Cipher failures roll back
|
|
76
|
+
consumption. KMS failures fail closed; a revoke/cancel racing signing is rechecked
|
|
77
|
+
before delivery. External stores/KMS are separate failure boundaries.
|
|
78
|
+
|
|
79
|
+
Defaults bound enrollments globally to 10,000, pending operations to ten per
|
|
80
|
+
enrollment, assertions to 60/minute, operation lifetime to ten minutes and request
|
|
81
|
+
bodies to 16 KiB. A hard cap of 1,000 retained operations per enrollment keeps
|
|
82
|
+
quota/cleanup/revocation scans complete even under tombstone flooding. Expired
|
|
83
|
+
operations are cleaned in bounded batches on begin;
|
|
84
|
+
tombstones survive expiry plus 30 seconds. Hosts must additionally enforce
|
|
85
|
+
per-human enrollment rate limits, ingress rate/connection/time limits, total
|
|
86
|
+
database retention, and authenticated account recovery. Rate-limit responses
|
|
87
|
+
include a retry timestamp. Errors contain fixed codes only.
|
|
88
|
+
|
|
89
|
+
Rotate signer configuration only after publishing the new key. Supply
|
|
90
|
+
`publishedKeys` during rotation and retain old public keys until every assertion
|
|
91
|
+
expires plus 30-second skew; never publish private JWK fields. The operator owns
|
|
92
|
+
the publication grace period and KMS key lifecycle.
|
|
93
|
+
|
|
94
|
+
Enrollment revocation stops new signing/retrieval and cancels pending results.
|
|
95
|
+
It cannot invalidate delivered assertions, existing company Session Tokens,
|
|
96
|
+
Account Tokens or DPoP keys. Revoke company authorization **before** disabling
|
|
97
|
+
provider signing, or use authenticated company account controls afterward.
|
|
98
|
+
This is scoped authority, redaction and provider key custody; it does not isolate
|
|
99
|
+
authority from unrestricted programs running as the same OS user.
|
package/README.md
CHANGED
|
@@ -1,3 +1,124 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PoppyKit client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TypeScript SDK for personal-agent providers integrating [Personal Agent Protocol
|
|
4
|
+
draft 0.2](https://personalagentprotocol.org/docs/0.2/spec). Discover companies,
|
|
5
|
+
start sessions, use Direct/Device/Mediated sign-in, exchange JSON/SSE conversations,
|
|
6
|
+
authorize API requests, join an agent browser and send feedback. Your application
|
|
7
|
+
supplies the agent and trusted infrastructure.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Requires Node **24.12.0 or later**, ESM and HTTPS. CI certifies Node 24.12.0;
|
|
12
|
+
later majors are not separately certified. Optional SQLite uses experimental
|
|
13
|
+
`node:sqlite` and local filesystem locking.
|
|
14
|
+
|
|
15
|
+
Version **0.1.0** is an initial development release candidate under the [MIT license](LICENSE).
|
|
16
|
+
It has not been published. Install a verified tarball:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm install ./poppykit-client-0.1.0.tgz
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The only production dependency is `jose@6.2.12`. No server SDK or CLI is required.
|
|
23
|
+
Registry installation instructions will accompany the first public release.
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
Discovery needs no signing key, account token or database:
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { createMetadataResolver, createNodeTransport } from '@poppykit/client';
|
|
31
|
+
|
|
32
|
+
const transport = createNodeTransport();
|
|
33
|
+
try {
|
|
34
|
+
const company = await createMetadataResolver(transport).resolveCompany('company.example');
|
|
35
|
+
console.log(company.document.organization);
|
|
36
|
+
} finally {
|
|
37
|
+
transport.close?.();
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Replace `company.example` with a company publishing draft 0.2 discovery.
|
|
42
|
+
Run the complete [quickstart](examples/quickstart.mjs) with a real domain, or
|
|
43
|
+
`node examples/quickstart.mjs --self-check` for a deterministic offline example.
|
|
44
|
+
|
|
45
|
+
## Configuration and authentication
|
|
46
|
+
|
|
47
|
+
`createClientAuth(options)` owns sessions, all three sign-in methods, renewal,
|
|
48
|
+
scope replacement, sign-out and recovery. Supply `namespace`, `localUserId`,
|
|
49
|
+
`clientId`, registered `redirectUris`, an `AuthStore`, `SecretStore`, safe transport
|
|
50
|
+
and metadata resolver. Run `recover()` at startup. Publish HTTPS client metadata
|
|
51
|
+
and public JWKS; private signing keys stay in your signer or provider.
|
|
52
|
+
|
|
53
|
+
Choose an `IdentitySigner`, or authenticated `AssertionProvider` and
|
|
54
|
+
`ProviderIdentity` adapters. The optional `/provider` reference is documented in
|
|
55
|
+
[PROVIDER.md](PROVIDER.md). Operators supply HTTPS hosting, enrollment authentication,
|
|
56
|
+
company policy and signing/KMS. Anonymous sessions require a separate local signer
|
|
57
|
+
with a published key; provider-only custody reports `anonymous_unavailable`.
|
|
58
|
+
|
|
59
|
+
Call `start(domain)`, then `authorization(connectionId, scopes)` to inspect grants,
|
|
60
|
+
missing scopes and advertised methods. `beginSignIn(connectionId, { method, scopes })`
|
|
61
|
+
selects `direct`, `device` or `mediated`. Direct requires an authenticated browser
|
|
62
|
+
binding and HTTPS callback. Device displays a verified human link/code and resumes
|
|
63
|
+
with `continueSignIn`. Mediated invokes your `TrustedInput.collect` UI. Keep that UI,
|
|
64
|
+
passwords, codes and credential adapters outside model tools. See [AUTH.md](AUTH.md)
|
|
65
|
+
for partial consent, scope upgrades, cancellation and revocation.
|
|
66
|
+
|
|
67
|
+
## Examples and common operations
|
|
68
|
+
|
|
69
|
+
Configure conversations using the verified target and the same auth lifecycle:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { createConversationClient } from '@poppykit/client';
|
|
73
|
+
const target = await auth.target(connectionId);
|
|
74
|
+
const conversations = createConversationClient({
|
|
75
|
+
...target, connectionId, store: conversationStore, clock: { now: Date.now },
|
|
76
|
+
authorizedRequest: request => auth.authorizedFetch(connectionId, request, target),
|
|
77
|
+
authorization: (id, scopes) => auth.authorization(id, scopes),
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`auth`, `connectionId` and `conversationStore` are your configured instances.
|
|
82
|
+
`start`, `send`, `read`, `watch`, `ack`, `handoff` and `close` manage the lifecycle.
|
|
83
|
+
Retain message IDs for retries; acknowledge durable deliveries after processing.
|
|
84
|
+
[DELIVERY.md](DELIVERY.md) explains cursors, streaming, pagination and retention.
|
|
85
|
+
|
|
86
|
+
Use `apiTarget`/`apiFetch` with an existing OpenAPI or MCP client. The final URL
|
|
87
|
+
must be independently authorized; an OpenAPI `servers` entry grants no authority.
|
|
88
|
+
`joinBrowser` accepts a trusted agent-controlled controller separate from human
|
|
89
|
+
consent. `feedback` sends a single `missing` report expecting empty HTTP 202.
|
|
90
|
+
[INTEGRATIONS.md](INTEGRATIONS.md) documents the adapter contracts and examples.
|
|
91
|
+
|
|
92
|
+
## Errors, cancellation, retries and limits
|
|
93
|
+
|
|
94
|
+
Handle `AuthFailure.code` and `ConversationError.code`; do not parse messages or
|
|
95
|
+
log credential headers/private adapter errors. Pending operations are resumable;
|
|
96
|
+
an uncertain remote exchange requires reconciliation and is never silently replayed.
|
|
97
|
+
Declined upgrades preserve old access; sign-out never automatically reconnects.
|
|
98
|
+
|
|
99
|
+
Conversation operations accept `AbortSignal`. Cancellation cannot undo bytes sent.
|
|
100
|
+
Conversation retries retain message IDs/payloads with bounded backoff and Retry-After.
|
|
101
|
+
Auth deadlines persist across restarts. Feedback has no automatic POST retry or
|
|
102
|
+
deduplication promise. Pools, bodies, event/page sizes and reconnects are bounded;
|
|
103
|
+
see [DELIVERY.md](DELIVERY.md). External effects need application idempotency.
|
|
104
|
+
|
|
105
|
+
## Storage and production responsibilities
|
|
106
|
+
|
|
107
|
+
Optional `/sqlite` exports `SqliteAuthStore` and `SqliteConversationStore`;
|
|
108
|
+
`/memory` exports non-durable `MemoryConversationStore`. Consumers can implement
|
|
109
|
+
the targeted contracts using their own databases. Store Account Tokens, DPoP keys
|
|
110
|
+
and PKCE in a secure `SecretStore`; Session Tokens stay in memory. SQLite stores
|
|
111
|
+
secret references and private message data, not vault encryption. Database, vault
|
|
112
|
+
and remote exchange are separate boundaries; implement orphan-secret reconciliation
|
|
113
|
+
and deployment ownership checks. [STORAGE.md](STORAGE.md) defines atomicity,
|
|
114
|
+
recovery and explicit development reset.
|
|
115
|
+
|
|
116
|
+
## Reference and support
|
|
117
|
+
|
|
118
|
+
[API.md](API.md) maps public exports and TypeScript declarations to deeper guides.
|
|
119
|
+
This independent draft implementation has deterministic and packaged tests;
|
|
120
|
+
production vaults, browsers, external issuers and hosting need operator validation.
|
|
121
|
+
Individual protocol extensions and application business logic are outside the SDK.
|
|
122
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md),
|
|
123
|
+
[CHANGELOG.md](CHANGELOG.md) and [RELEASING.md](RELEASING.md). No public license grant
|
|
124
|
+
has been selected; publication remains blocked pending the owner's license decision.
|