@poppykit/server 0.3.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 +11 -3
- package/AUTH.md +120 -95
- package/CHANGELOG.md +18 -0
- package/CONFORMANCE.md +73 -0
- package/CONTRIBUTING.md +13 -10
- package/DELIVERY.md +21 -1
- package/INTEGRATIONS.md +72 -69
- package/OPERATIONS.md +142 -0
- package/README.md +14 -17
- package/RELEASING.md +2 -0
- package/SECURITY.md +2 -0
- package/STORAGE.md +57 -2
- package/dist/adapter-contract.d.ts +18 -0
- package/dist/adapter-contract.js +90 -11
- package/dist/auth/authenticator.d.ts +3 -1
- package/dist/auth/authenticator.js +34 -29
- package/dist/auth/authority.d.ts +8 -2
- package/dist/auth/authority.js +7 -2
- package/dist/auth/session.d.ts +47 -0
- package/dist/auth/session.js +94 -0
- package/dist/browser.d.ts +15 -13
- package/dist/browser.js +80 -46
- package/dist/conversation/service.d.ts +18 -8
- package/dist/conversation/service.js +60 -28
- package/dist/feedback.d.ts +1 -1
- package/dist/feedback.js +5 -5
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +8 -4
- 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 +115 -0
- package/dist/operations.js +212 -0
- package/dist/postgres.d.ts +6 -0
- package/dist/postgres.js +92 -6
- 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/reference-auth/index.d.ts +17 -12
- package/dist/reference-auth/index.js +252 -386
- package/dist/reference-auth/store.d.ts +2 -1
- package/dist/sqlite.d.ts +11 -0
- package/dist/sqlite.js +104 -6
- package/examples/operations.mjs +18 -0
- package/examples/quickstart.mjs +10 -6
- package/migrations/RETIREMENT.md +77 -0
- package/package.json +5 -2
package/INTEGRATIONS.md
CHANGED
|
@@ -1,84 +1,87 @@
|
|
|
1
1
|
# API, browser and feedback integration
|
|
2
2
|
|
|
3
|
-
Use
|
|
4
|
-
|
|
5
|
-
headers.
|
|
6
|
-
revocation, address-validation and storage guarantees.
|
|
3
|
+
Use trusted configured public URLs, the resource Session authenticator and its
|
|
4
|
+
combined Session/account policy. Never derive public targets from arbitrary
|
|
5
|
+
forwarding headers. The host supplies framework routing and ingress limits.
|
|
7
6
|
|
|
8
|
-
## MCP
|
|
7
|
+
## APIs and MCP
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// Serve metadata publicly without credentials. In the existing MCP handler:
|
|
17
|
-
try {
|
|
18
|
-
const principal = await authenticator.authenticate(request);
|
|
19
|
-
return await authority.policy.withCurrent(principal, ['tools:read'], () => handleMcp(request));
|
|
20
|
-
} catch (error) {
|
|
21
|
-
if (error instanceof AuthorityError) return mcpAuthorizationError(error, metadataUrl, true);
|
|
22
|
-
throw error;
|
|
23
|
-
}
|
|
24
|
-
```
|
|
9
|
+
Publish RFC 9728 metadata for a service that needs narrower tokens. Its resource
|
|
10
|
+
identifies that service; normal sign-in/refresh accepts the configured resource.
|
|
11
|
+
MCP resource metadata must exactly identify the advertised URL and name the
|
|
12
|
+
Company issuer when account access exists. `mcpResourceMetadata({resource,issuer?,
|
|
13
|
+
dpop})` supports issuerless resources too. Serve metadata without credentials.
|
|
14
|
+
RFC 9728 inserts the well-known path before the resource path and preserves a query.
|
|
25
15
|
|
|
26
|
-
MCP
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
resource-bound; DPoP-bound tokens cannot be presented as Bearer.
|
|
16
|
+
Configure an MCP ResourceEndpoint with the resource's exact origin, pathPrefix,
|
|
17
|
+
resource and mcp: {dpop,metadataUrl}. Query-bearing resources are allowed, but the
|
|
18
|
+
whole request URL must match exactly: child paths or changed queries are rejected.
|
|
19
|
+
Bearer is MCP's baseline. DPoP is accepted only when advertised; introspection
|
|
20
|
+
returns dpopJkt:null only for an unbound token. Normal OAuth grants issue Bearer
|
|
21
|
+
MCP tokens without token exchange. A token for one service is refused elsewhere.
|
|
33
22
|
|
|
34
|
-
An
|
|
35
|
-
Its
|
|
36
|
-
|
|
37
|
-
the
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
implements personal-agent grants; it does not add a general OAuth login product.
|
|
23
|
+
An ordinary MCP client without Poppy-Session can use its account access token.
|
|
24
|
+
Its Principal has no Poppy User or Session. A Personal Agent sends its Session
|
|
25
|
+
header on every API/MCP request; when present, the authenticator verifies it and
|
|
26
|
+
joins the token's client identity. Poppy Session state is independent of both older
|
|
27
|
+
MCP transport lifecycles and MCP 2026-07-28 per-request transport metadata. The SDK
|
|
28
|
+
does not implement JSON-RPC, initialization or Mcp-Session-Id management.
|
|
41
29
|
|
|
42
|
-
|
|
30
|
+
Catch AuthorityError and use mcpAuthorizationError(error,metadataUrl,dpop) for OAuth
|
|
31
|
+
challenges containing resource_metadata. invalid_session remains JSON/400 and rate
|
|
32
|
+
limits JSON/429 with Retry-After. Use sessions.policy.withCurrent for protected work.
|
|
33
|
+
|
|
34
|
+
## Browser joining
|
|
43
35
|
|
|
44
36
|
```ts
|
|
45
|
-
const browser = createBrowserSessions({
|
|
46
|
-
|
|
47
|
-
policy:
|
|
37
|
+
const browser = createBrowserSessions({
|
|
38
|
+
endpoint: browserEndpoint, domain: companyDomain, authenticator,
|
|
39
|
+
policy: sessions.policy, cookies: database.browserCookies(companyId), now: Date.now,
|
|
40
|
+
clearLogin: async (request, headers) => hostLogin.clear(request, headers),
|
|
48
41
|
});
|
|
49
|
-
//
|
|
50
|
-
|
|
42
|
+
// GET/POST browserEndpoint -> browser.join
|
|
43
|
+
// POST browserEndpoint + '/authorize' -> browser.authorize (preserve endpoint query)
|
|
51
44
|
```
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
46
|
+
GET creates only a short-lived Strict join cookie and hidden random code, with
|
|
47
|
+
no-store, framing and opener protection. The trusted agent reads its own isolated
|
|
48
|
+
browser's code and separately POSTs JSON {code} to /authorize with Session/account
|
|
49
|
+
credentials. Success is 204. It then submits only code in the browser form. Never
|
|
50
|
+
accept a code from Company/model text, another browser or a user message.
|
|
51
|
+
|
|
52
|
+
Browser POST atomically consumes the code, including failed submissions, checks its
|
|
53
|
+
cookie/origin/site/expiry and prior authorization, and sets a new opaque cookie.
|
|
54
|
+
No token, JWT or proof is carried by the browser form or URL. Success clears the
|
|
55
|
+
join cookie and redirects 303 to the Company root. A signed-out join preserves an
|
|
56
|
+
existing human website login; a signed-in join requires clearLogin to invalidate
|
|
57
|
+
and clear host login, and replaces a previous protocol cookie.
|
|
58
|
+
|
|
59
|
+
BrowserCookieStore supplies put/get/remove plus putJoin/authorizeJoin/consumeJoin.
|
|
60
|
+
Stores persist only cookie/code hashes. authorizeJoin and consumeJoin serialize
|
|
61
|
+
one-use code decisions across replicas. Bundled SQLite/PostgreSQL implementations
|
|
62
|
+
are available through database.browserCookies(namespace). Rate-limit the public
|
|
63
|
+
GET/POST routes and run bounded expiry cleanup.
|
|
64
|
+
|
|
65
|
+
A browser cookie stores the authorization snapshot approved at join: same Session,
|
|
66
|
+
account and scopes until rejoin. It does not gain later scopes or sign-in authority.
|
|
67
|
+
Admission-token expiry alone does not end the approved cookie; Session expiry,
|
|
68
|
+
revocation and current account policy still apply. Use browser.withSession for
|
|
69
|
+
scoped work ordered by the same policy/coordinator. Defaults are Secure, HttpOnly,
|
|
70
|
+
Path=/ and SameSite=Lax, at most one hour capped by Session lifetime.
|
|
70
71
|
|
|
71
72
|
## Feedback
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
own report retention, privacy filtering
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
createFeedbackHandler validates the Session before attributing a report and checks
|
|
75
|
+
current authority without account scopes. Reports require only task/description,
|
|
76
|
+
nonempty and at most 200/2000 Unicode codepoints, within a bounded JSON/UTF8 body.
|
|
77
|
+
Unknown fields are inert data. accept receives a frozen report and verified Principal;
|
|
78
|
+
never treat report text as instructions or put secrets/personal data into it.
|
|
79
|
+
|
|
80
|
+
The optional allow hook returning false yields 429 with Retry-After:60. Malformed
|
|
81
|
+
reports are 400; successful acceptance is 202 with no body. The client also supports
|
|
82
|
+
a Company's 200 alternative response. Hosts own report retention, privacy filtering
|
|
83
|
+
and downstream effect handling; feedback has no deduplication identifier.
|
|
84
|
+
|
|
85
|
+
## Operations v2
|
|
86
|
+
|
|
87
|
+
[OPERATIONS.md](OPERATIONS.md) defines API403/Poppy-Approval, MCP structuredContent/_meta and conversation operation/approval integration. Publish support only after all interfaces in use have trusted consent and durable effect recovery. Ordinary MCP clients retain their OAuth flow; one unsupported tool returns a tool error. No v1 extension endpoint is added.
|
package/OPERATIONS.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Business operations (Operations v2)
|
|
2
|
+
|
|
3
|
+
Enable `extensions.operations: operationsV2` in Company discovery only after
|
|
4
|
+
installing these mechanics on every interface offering approval-gated actions.
|
|
5
|
+
There is no v1 operations endpoint. The SDK does not auto-advertise support.
|
|
6
|
+
`createBusinessOperations({companyId,metadata,policy,store,now?,retentionMs?,
|
|
7
|
+
maxRevisions?,effectTimeoutMs?})` verifies the client's exact v2 metadata before returning an
|
|
8
|
+
operation. Use the same verified Session/account policy and database coordinator
|
|
9
|
+
as request authentication. OAuth-only MCP identity cannot approve Poppy work.
|
|
10
|
+
|
|
11
|
+
The host chooses a stable URL-safe `operationId` for one business action, from
|
|
12
|
+
its own records. It must never derive another action identity from the submitted
|
|
13
|
+
approval, allocate a fresh action on retry, or reuse an ID for different work.
|
|
14
|
+
The service stores every issued, unexpired revision with its exact request.
|
|
15
|
+
A newer revision does not invalidate an older one. Summary/terms/expiry changes
|
|
16
|
+
always receive a fresh opaque revision. Default admission is 1000 live revisions
|
|
17
|
+
per action; overflow is 429/Retry-After, preserving issued revisions.
|
|
18
|
+
|
|
19
|
+
## Propose and execute
|
|
20
|
+
|
|
21
|
+
`OperationInput` contains `operationId`, authenticated `principal`, required
|
|
22
|
+
`scopes`, `request`, optional parsed `approval`, and `proposal()`. The proposal
|
|
23
|
+
callback supplies the current `summary`, optional `terms`, and `expires_at`.
|
|
24
|
+
Describe every action condition, amount, recipient and date in the summary and
|
|
25
|
+
terms. Give the User at least 15 minutes where business conditions permit. Use
|
|
26
|
+
request/resource field names in structured terms, and document `approval_required`
|
|
27
|
+
in your OpenAPI responses. These business-semantic choices belong to the host.
|
|
28
|
+
|
|
29
|
+
Call `propose(input)` to publish terms without an effect, or route every action
|
|
30
|
+
through `local`/`external`: without a valid approval they return
|
|
31
|
+
`{kind:'approval_required',operation}` and execute nothing. Current authorization,
|
|
32
|
+
scopes and ownership precede receipt replay. Signed-out ownership includes the
|
|
33
|
+
exact Session instance/generation, not just a reused sid. Signed-in ownership
|
|
34
|
+
includes Company, client, User and account; another Session of that same identity
|
|
35
|
+
can approve. Signed-out access to account work receives an OAuth account challenge.
|
|
36
|
+
Foreign identities see the same `operation_not_found` error. Recheck current
|
|
37
|
+
policy before exposing protected results. Model text and Company conversation
|
|
38
|
+
text cannot create the approval field.
|
|
39
|
+
|
|
40
|
+
Bind requests with `captureApiOperationRequest(request)` (complete method/URL,
|
|
41
|
+
base64 exact body bytes), `{channel:'mcp',url,name,arguments}`, or
|
|
42
|
+
`{channel:'conversation',endpoint,conversationId}`. Use `parseApprovalHeader`,
|
|
43
|
+
`mcpApproval(params)` or the accepted conversation message's `approval`.
|
|
44
|
+
A mismatched/expired/unknown revision or wrong request performs nothing and
|
|
45
|
+
returns freshly generated terms, never the rejected approval's revision.
|
|
46
|
+
Execution is serialized by business action ID across all revisions and workers.
|
|
47
|
+
|
|
48
|
+
`OperationOutcome` is `approval_required`, `pending` or `completed` (result and
|
|
49
|
+
replay flag). `operationApiResponse(outcome,success?)` maps proposal to403 JSON and
|
|
50
|
+
pending to503/Retry-After. The default success is JSON200; pass a stable response
|
|
51
|
+
builder for your API's status/headers. Replay returns the first committed result
|
|
52
|
+
or an equivalent response. No approval goes in WWW-Authenticate.
|
|
53
|
+
`operationMcpResponse` produces the structured tool error or successful tool
|
|
54
|
+
result, and HTTP503/Retry-After for unresolved work. Pass a caught
|
|
55
|
+
`BusinessOperationError('extension_required')` to it for an ordinary tool error;
|
|
56
|
+
only refusing the entire MCP client uses the OAuth extension_required challenge.
|
|
57
|
+
|
|
58
|
+
## Local and external effect contracts
|
|
59
|
+
|
|
60
|
+
`local(input,async approvedRevision => result)` executes only same-database
|
|
61
|
+
mutations. Use `database.withTransaction` inside the callback, on the SAME
|
|
62
|
+
`SqliteCompanyDatabase`/`PostgresCompanyDatabase` instance as the operation store
|
|
63
|
+
and authorization coordinator. The host mutation and operation receipt commit or
|
|
64
|
+
roll back together, including process death. Never put a network call, another
|
|
65
|
+
database or nontransactional filesystem effect in `local`. It receives the actual
|
|
66
|
+
approved revision: execute those exact terms, not a freshly recomputed quote.
|
|
67
|
+
SQLite supplies scoped `run/get`; PostgreSQL supplies a scoped query connection.
|
|
68
|
+
Handles reject use after their transaction ends.
|
|
69
|
+
|
|
70
|
+
`external(input,adapter)` commits an uncertain execution claim BEFORE invoking
|
|
71
|
+
`adapter.execute({key,operation})`. The key is derived from Company/action ID,
|
|
72
|
+
not revision or message ID. The host MUST guarantee durable idempotency for that
|
|
73
|
+
key in the external system, including concurrent calls and old paused workers.
|
|
74
|
+
It must preserve the approved terms and authenticate/authorize the action at its
|
|
75
|
+
submission boundary. Already accepted effects may finish during subsequent
|
|
76
|
+
revocation; revocation cannot retroactively undo an external system's commit.
|
|
77
|
+
Arbitrary network effects are not exactly-once, and a local receipt alone cannot
|
|
78
|
+
make them so. Do not use this API with a downstream lacking this contract.
|
|
79
|
+
|
|
80
|
+
Execution/reconciliation has a default30s deadline (`effectTimeoutMs`). It returns pending on a deadline, without cancelling or freeing a possibly running effect. A timeout/throw/process death leaves the claim uncertain indefinitely. Retrying
|
|
81
|
+
returns pending and never calls execute again automatically. Lease expiration,
|
|
82
|
+
new terms, process restart and receipt cleanup cannot release the claim.
|
|
83
|
+
`reconcile(input,adapter)` asks the host's durable system by the same key:
|
|
84
|
+
|
|
85
|
+
- `pending`: remain fenced, answer503 with Retry-After.
|
|
86
|
+
- `completed,result`: record the actual outcome and replay it.
|
|
87
|
+
- `absent`: assert the old worker cannot later commit outside that idempotency
|
|
88
|
+
fence; the SDK may submit the same key and original approved revision again.
|
|
89
|
+
|
|
90
|
+
Reconciliation itself must be safe concurrently with execute and other
|
|
91
|
+
reconcilers. The first committed result is immutable. Preserve a meaningful
|
|
92
|
+
business failure as an equivalent terminal outcome when definitively known;
|
|
93
|
+
never describe unknown effect state as failure. Hosts enforce ingress quotas,
|
|
94
|
+
worker deadlines and external reconciliation scheduling. A deadline must return
|
|
95
|
+
pending without deleting the durable claim, even if its worker is still alive.
|
|
96
|
+
|
|
97
|
+
## Conversations
|
|
98
|
+
|
|
99
|
+
Configure `createConversationService({operations:metadata,...})` only on an
|
|
100
|
+
endpoint with complete v2 execution. It refuses unsupported clients with the
|
|
101
|
+
extension challenge. It accepts approval-only user-side messages and operation-only
|
|
102
|
+
Company messages, gated by current client metadata on requests/publication.
|
|
103
|
+
`operationContext(work)` provides the accepted principal, conversation binding
|
|
104
|
+
and only the actual message approval. Use that context with the business service,
|
|
105
|
+
then `reply` with `operation` when proposing or the actual business outcome after
|
|
106
|
+
execution. A conversation's lease/message receipts do not deduplicate effects.
|
|
107
|
+
Repeated approval in a new message adds no event or work; same message-ID retries
|
|
108
|
+
retain the ordinary original receipt. Cross-conversation approval fails binding.
|
|
109
|
+
|
|
110
|
+
## Store contract, retention and migration
|
|
111
|
+
|
|
112
|
+
`database.businessOperations(companyNamespace)` implements
|
|
113
|
+
`BusinessOperationStore`: an asynchronous, serializable transaction on one action
|
|
114
|
+
slot, including absent-row serialization across processes. Local host effects
|
|
115
|
+
and policy checks must enlist in the same coordinator. Exceptions roll back the
|
|
116
|
+
slot and effects even inside an enclosing transaction. Snapshots/results detach;
|
|
117
|
+
unknown record versions and changed IDs are refused. Run
|
|
118
|
+
`runBusinessOperationAdapterContract` from the adapter-contract subpath for a
|
|
119
|
+
custom store, plus your engine's process-death/effect-enlistment tests.
|
|
120
|
+
|
|
121
|
+
Business record version1 uses the existing SQLite/PostgreSQL `records` table,
|
|
122
|
+
kind `business_operations`. No applied SQL migration is rewritten, no old writer
|
|
123
|
+
can overwrite this kind through existing targeted auth/conversation selectors,
|
|
124
|
+
and no credentials or user rows are removed. Authentication lifecycle records
|
|
125
|
+
remain separate. Stop incompatible future writers before any version transition.
|
|
126
|
+
|
|
127
|
+
Each execution retains its first protected result at least through the approved
|
|
128
|
+
revision's expiry (default also24h after claim). `prune(operationId)` only drops a
|
|
129
|
+
completed result and terms strictly AFTER retention. It keeps a minimal owner/
|
|
130
|
+
action tombstone permanently; later attempts can get fresh terms but cannot
|
|
131
|
+
execute that business action again. Uncertain claims/terms never prune. If you
|
|
132
|
+
need to retire tombstones, an authoritative permanent business fence must still
|
|
133
|
+
prevent re-execution; deleting the namespace is not retention. Protect stored
|
|
134
|
+
terms/results with your database access/encryption policy. Approval attribution
|
|
135
|
+
records the owner, approved revision and claim time; this states what the agent
|
|
136
|
+
asserted, not proof of what a human saw.
|
|
137
|
+
|
|
138
|
+
`node examples/operations.mjs --self-check` uses only synthetic identities and a
|
|
139
|
+
private temporary SQLite file. Real hosts use the request authenticator and
|
|
140
|
+
combined Session/account policy, not the example's test policy. Process-crash,
|
|
141
|
+
concurrent-process, independent wire and PostgreSQL acceptance is in
|
|
142
|
+
`tests/operations.test.mts`.
|
package/README.md
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
# PoppyKit server
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
TypeScript SDK for companies implementing [Personal Agent Protocol draft 0.2](https://personalagentprotocol.org/docs/0.2/spec).
|
|
6
|
-
Verify requests, serve conversations and enforce current authority. Your company
|
|
7
|
-
supplies accounts, consent UI, agent, business tools and HTTPS routing. [MIT](LICENSE).
|
|
3
|
+
TypeScript SDK for companies implementing [Personal Agent Protocol draft 0.3](https://personalagentprotocol.org/docs/0.3/spec).
|
|
4
|
+
Verify requests and serve conversations using your accounts, agent and business tools. [MIT](LICENSE).
|
|
8
5
|
|
|
9
6
|
## Install
|
|
10
7
|
|
|
11
|
-
This source contains unreleased repairs; final version selection and publication are pending.
|
|
12
|
-
|
|
13
|
-
Requires Node **24.x, at least 24.21.0**, and ESM. CI tests Node 24.21.0 on Linux, macOS and Windows;
|
|
14
|
-
other Node majors are unsupported. SQLite is optional/experimental and
|
|
15
|
-
requires local filesystem locking.
|
|
16
|
-
|
|
17
8
|
```sh
|
|
18
9
|
npm install @poppykit/server
|
|
19
10
|
```
|
|
@@ -26,7 +17,7 @@ in production. The reference issuer is optional; [existing issuers](AUTH.md) use
|
|
|
26
17
|
an ExternalAuthorityAdapter instead.
|
|
27
18
|
|
|
28
19
|
```js
|
|
29
|
-
import { createRequestAuthenticator, createConversationService } from '@poppykit/server';
|
|
20
|
+
import { createRequestAuthenticator, createConversationService, createSessionAuthority } from '@poppykit/server';
|
|
30
21
|
import { createMetadataResolver, createNodeTransport, createReferenceIssuer } from '@poppykit/server/reference-auth';
|
|
31
22
|
import { SqliteCompanyDatabase } from '@poppykit/server/sqlite';
|
|
32
23
|
|
|
@@ -35,20 +26,24 @@ const transport = createNodeTransport();
|
|
|
35
26
|
try {
|
|
36
27
|
const issuer = 'https://company.example/issuer';
|
|
37
28
|
const endpoint = 'https://company.example/conversations';
|
|
29
|
+
const metadata = createMetadataResolver(transport);
|
|
38
30
|
const auth = createReferenceIssuer({
|
|
39
31
|
issuer, tokenEndpoint: issuer + '/token', revocationEndpoint: issuer + '/revoke',
|
|
40
|
-
|
|
41
|
-
metadata
|
|
32
|
+
protectedResources: ['https://company.example'], supportedScopes: [], resources: [endpoint],
|
|
33
|
+
metadata, store: database.issuer(issuer),
|
|
42
34
|
coordinator: database, now: Date.now,
|
|
43
35
|
});
|
|
36
|
+
const sessions = createSessionAuthority({companyId:issuer,metadata,store:database.sessions(issuer),
|
|
37
|
+
coordinator:database,accountPolicy:auth.authority.policy,now:Date.now});
|
|
44
38
|
const service = createConversationService({
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
historyScopes: ['orders:read'],
|
|
40
|
+
endpoint, store: database.conversations(issuer), policy: sessions.policy,
|
|
41
|
+
authenticator: createRequestAuthenticator({ sessions, authority: auth.authority,
|
|
47
42
|
endpoints: [{ origin: 'https://company.example', pathPrefix: '/conversations', resource: endpoint }],
|
|
48
43
|
now: Date.now }), clock: { now: Date.now },
|
|
49
44
|
});
|
|
50
45
|
console.log((await service.handle(new Request(endpoint, { method: 'POST',
|
|
51
|
-
headers: { 'content-type': 'application/json' }, body: '{}' }))).status); //
|
|
46
|
+
headers: { 'content-type': 'application/json' }, body: '{}' }))).status); // 400
|
|
52
47
|
} finally {
|
|
53
48
|
transport.close?.();
|
|
54
49
|
database.close();
|
|
@@ -57,6 +52,8 @@ try {
|
|
|
57
52
|
|
|
58
53
|
## Guides
|
|
59
54
|
|
|
55
|
+
- [Business operations](OPERATIONS.md): exact per-operation approval, durable retries and effect contracts.
|
|
56
|
+
|
|
60
57
|
- [Authentication](AUTH.md): external/reference issuers, consent and revocation.
|
|
61
58
|
- [Storage](STORAGE.md): shared authority coordinator, cross-process transactions and reset.
|
|
62
59
|
- [Delivery](DELIVERY.md): workers, streaming, cursors, retention and effect idempotency.
|
package/RELEASING.md
CHANGED
|
@@ -89,3 +89,5 @@ certify a production keyring, physical terminal, browser engine, KMS or TLS depl
|
|
|
89
89
|
Windows GNU tar is given archive basename plus directory cwd to avoid drive-colon
|
|
90
90
|
remote parsing. Failure logs retain seven days. Expired/missing artifacts require a
|
|
91
91
|
new full run, not relabeling old results. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
92
|
+
|
|
93
|
+
The local Operations SDK checkpoint does not satisfy release acceptance. Complete the block3 CLI business-approval experience and stable all-tests/package/native gates before publication. Operations metadata must remain opt-in to fully configured hosts. Include OPERATIONS.md and both operation examples in candidate archives.
|
package/SECURITY.md
CHANGED
|
@@ -11,3 +11,5 @@ impact and the trust boundary involved. These draft packages have no promised
|
|
|
11
11
|
support window or response SLA. Custom vaults, issuers, browser controllers,
|
|
12
12
|
proxy routing and deployment policy remain operator responsibilities. Redaction
|
|
13
13
|
and scoped authority do not isolate an unrestricted same-user program.
|
|
14
|
+
|
|
15
|
+
Business-operation approval requires the trusted per-operation UI described in [OPERATIONS.md](OPERATIONS.md). Keep Company/model text separate from user consent. Local effects must share the receipt transaction; arbitrary network effects require host idempotency and reconciliation. Retain uncertain claims until the actual outcome is known.
|
package/STORAGE.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Request paths use targeted domain selections, not whole-state rewrites. Node 24.x, at least 24.21.0, is required.
|
|
4
4
|
The optional `@poppykit/server/sqlite` entry point exports `SqliteCompanyDatabase`.
|
|
5
5
|
The core and reference issuer entry points do not import SQLite. Consumers can
|
|
6
|
-
implement `ConversationStore`, `IssuerStore`
|
|
6
|
+
implement `ConversationStore`, `IssuerStore`, `SessionStore`, `BrowserCookieStore`
|
|
7
|
+
and `AuthorityCoordinator` themselves.
|
|
7
8
|
The tested Node 24.21.0 runtime marks `node:sqlite` experimental.
|
|
8
9
|
|
|
9
10
|
```ts
|
|
@@ -71,7 +72,8 @@ its own idempotency key (such as the stable work ID), or the application must re
|
|
|
71
72
|
an uncertain result for reconciliation. Never claim universal exactly-once execution.
|
|
72
73
|
|
|
73
74
|
`cleanup(namespace, now, limit)` bounds deletion to at most 1000 expired short-lived
|
|
74
|
-
codes, authorizations, access tokens,
|
|
75
|
+
codes, authorizations, access tokens, browser cookies/join codes, expired resource
|
|
76
|
+
Sessions, replay entries or expired/revoked/orphaned
|
|
75
77
|
Account Token hashes. Every used hash for a live lineage remains replay-detectable
|
|
76
78
|
through its fixed expiry; retained size follows rotation volume. It does not delete grants,
|
|
77
79
|
work intent, message receipts or conversation events. Receipt identity remains
|
|
@@ -81,6 +83,24 @@ force another conversation's receipts out of storage.
|
|
|
81
83
|
|
|
82
84
|
## Non-durable use and resets
|
|
83
85
|
|
|
86
|
+
Normalized issuer state versions 3 and 4 upgrade to version 5 once, under the
|
|
87
|
+
existing SQLite writer/PostgreSQL namespace lock. Stop old writers before opening
|
|
88
|
+
the new adapters; their version checks reject the upgraded namespace. Applied SQL
|
|
89
|
+
migration history is unchanged: this is a JSON record-format transition.
|
|
90
|
+
|
|
91
|
+
The upgrade preserves the generation, Sessions and key mappings, grants and every
|
|
92
|
+
retained refresh-token hash, pending authorizations/codes, verifying sign-in hooks,
|
|
93
|
+
replay fences and nonces. It does not expire Sessions or clear pending consent.
|
|
94
|
+
Access-token records gain client/account identity only where an existing grant or
|
|
95
|
+
Session proves it; conflicting or missing associations remain unresolved. Unknown
|
|
96
|
+
earlier versions fail closed. Custom issuer adapters must upgrade the complete
|
|
97
|
+
namespace under their writer lock before normal targeted reads.
|
|
98
|
+
|
|
99
|
+
OAuth account access is independent of resource Sessions. The record upgrade does
|
|
100
|
+
not replay or reinterpret old pending requests. Keep uncertain
|
|
101
|
+
work and encrypted delivery references for reconciliation; do not reset a store or
|
|
102
|
+
reinterpret an unresolved record as a fresh request.
|
|
103
|
+
|
|
84
104
|
`new SqliteCompanyDatabase(':memory:')` is an explicit non-durable adapter for tests
|
|
85
105
|
and development. Each instance is separate, is lost at close/process death, and
|
|
86
106
|
offers no cross-process persistence. The test fixture's object store is also
|
|
@@ -180,3 +200,38 @@ never a production migration command.
|
|
|
180
200
|
`runIssuerAdapterContract` joins the existing reusable conversation contract in
|
|
181
201
|
`@poppykit/server/adapter-contract`. Run both against two fresh shared handles;
|
|
182
202
|
process death, lock ordering and DB durability still require backend-specific tests.
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
## Resource Sessions and browser cookies
|
|
206
|
+
|
|
207
|
+
Use database.sessions(companyId) and database.browserCookies(companyId), also for
|
|
208
|
+
issuerless Companies. Resource Session records and their namespace generation are
|
|
209
|
+
separate from legacy issuer Sessions and account grants. No applied SQL history is
|
|
210
|
+
rewritten; these records use the existing namespaced JSON table and indexes.
|
|
211
|
+
|
|
212
|
+
SessionStore.read(clientId,sid) returns version, generation and the stored record.
|
|
213
|
+
transaction(clientId,sid,at,change) must serialize admission across all sids for that
|
|
214
|
+
client, including absent records, and supply SessionAdmission.activeCount and
|
|
215
|
+
earliestExpiry for non-ended records with expiresAt > at. Counts and record commit
|
|
216
|
+
are one transaction. Callbacks are synchronous/detached; rollback and binding checks
|
|
217
|
+
apply. PostgreSQL takes client-before-sid advisory locks, SQLite its writer lock.
|
|
218
|
+
The default authority cap is 10,000 live Sessions per client, lifetime one day.
|
|
219
|
+
Expired records may be explicitly forgotten by bounded cleanup; a recreated sid
|
|
220
|
+
has a fresh instanceId, distinct from its prior resource Session. Operations v2
|
|
221
|
+
must preserve this distinction when defining signed-out approval ownership.
|
|
222
|
+
|
|
223
|
+
BrowserCookieStore stores opaque hashes and immutable approved Principal snapshots.
|
|
224
|
+
authorizeJoin(codeHash,principal) conditionally authorizes one live join;
|
|
225
|
+
consumeJoin atomically removes/returns it, including expired entries. remove deletes
|
|
226
|
+
a superseded browser-cookie hash. The host must implement these atomic operations
|
|
227
|
+
across replicas and use current account/Session policy when serving cookies.
|
|
228
|
+
Uncertain mediated verifying records are excluded from expiry cleanup and remain
|
|
229
|
+
in pending capacity counts until host reconciliation; never delete them to free quota.
|
|
230
|
+
|
|
231
|
+
Custom Session/browser adapters can run runSessionAdapterContract and
|
|
232
|
+
runBrowserCookieAdapterContract from @poppykit/server/adapter-contract against two
|
|
233
|
+
shared handles. See [legacy retirement](migrations/RETIREMENT.md) before any deletion.
|
|
234
|
+
|
|
235
|
+
## Business operation persistence
|
|
236
|
+
|
|
237
|
+
[Operations storage contracts](OPERATIONS.md#store-contract-retention-and-migration) are separate from authentication lifecycle operations and message receipts. BusinessOperationStore uses an asynchronous transaction for same-database effects, unlike synchronous issuer/conversation callbacks. SQLite and PostgreSQL use the separate business_operations record kind. Use the same company database instance for policy, operation storage and local effects. No existing SQL history, credentials or uncertain work is deleted. Unknown versions fail closed. External effects require a durable downstream idempotency/reconciliation contract. Never prune or release an uncertain business claim after a worker deadline.
|
|
@@ -7,6 +7,18 @@ export type IssuerAdapterFixture = () => Promise<{
|
|
|
7
7
|
}>;
|
|
8
8
|
/** A fresh shared namespace, including absent-row serialization; no vendor-specific fixture is required. */
|
|
9
9
|
export declare function runIssuerAdapterContract(fixture: IssuerAdapterFixture): void;
|
|
10
|
+
export type SessionAdapterFixture = () => Promise<{
|
|
11
|
+
store: import('./auth/session.js').SessionStore;
|
|
12
|
+
second: import('./auth/session.js').SessionStore;
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function runSessionAdapterContract(fixture: SessionAdapterFixture): void;
|
|
16
|
+
export type BrowserCookieAdapterFixture = () => Promise<{
|
|
17
|
+
store: import('./browser.js').BrowserCookieStore;
|
|
18
|
+
second: import('./browser.js').BrowserCookieStore;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function runBrowserCookieAdapterContract(fixture: BrowserCookieAdapterFixture): void;
|
|
10
22
|
type Store = ConversationOptions['store'] & {
|
|
11
23
|
inspect(): Promise<import('./conversation/service.js').ConversationStoreState>;
|
|
12
24
|
adminTransaction<T>(change: (draft: import('./conversation/service.js').ConversationStoreState) => T): Promise<T>;
|
|
@@ -17,4 +29,10 @@ export type ConversationAdapterFixture = () => Promise<{
|
|
|
17
29
|
close(): Promise<void>;
|
|
18
30
|
}>;
|
|
19
31
|
export declare function runConversationAdapterContract(fixture: ConversationAdapterFixture): void;
|
|
32
|
+
export type BusinessOperationAdapterFixture = () => Promise<{
|
|
33
|
+
store: import('./operations.js').BusinessOperationStore;
|
|
34
|
+
second: import('./operations.js').BusinessOperationStore;
|
|
35
|
+
close(): Promise<void>;
|
|
36
|
+
}>;
|
|
37
|
+
export declare function runBusinessOperationAdapterContract(fixture: BusinessOperationAdapterFixture): void;
|
|
20
38
|
export {};
|