@poppykit/server 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 +21 -0
- package/AUTH.md +60 -0
- package/CHANGELOG.md +22 -0
- package/CONTRIBUTING.md +50 -0
- package/DELIVERY.md +113 -0
- package/INTEGRATIONS.md +84 -0
- package/LICENSE +21 -0
- package/README.md +116 -2
- package/RELEASING.md +193 -0
- package/SECURITY.md +13 -0
- package/STORAGE.md +119 -0
- package/dist/adapter-contract.d.ts +12 -0
- package/dist/adapter-contract.js +140 -0
- package/dist/auth/authenticator.d.ts +6 -0
- package/dist/auth/authenticator.js +51 -0
- package/dist/auth/authority.d.ts +55 -0
- package/dist/auth/authority.js +16 -0
- package/dist/auth/dpop.d.ts +15 -0
- package/dist/auth/dpop.js +49 -0
- package/dist/browser.d.ts +44 -0
- package/dist/browser.js +110 -0
- package/dist/conversation/service.d.ts +126 -0
- package/dist/conversation/service.js +754 -0
- package/dist/conversation/store.d.ts +26 -0
- package/dist/conversation/store.js +14 -0
- package/dist/feedback.d.ts +13 -0
- package/dist/feedback.js +44 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +7 -0
- package/dist/mcp.d.ts +7 -0
- package/dist/mcp.js +14 -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/reference-auth/index.d.ts +209 -0
- package/dist/reference-auth/index.js +999 -0
- package/dist/reference-auth/store.d.ts +53 -0
- package/dist/reference-auth/store.js +2 -0
- package/dist/sqlite-init.d.ts +2 -0
- package/dist/sqlite-init.js +17 -0
- package/dist/sqlite.d.ts +27 -0
- package/dist/sqlite.js +369 -0
- package/dist/store.d.ts +1 -0
- package/dist/store.js +1 -0
- package/examples/quickstart.mjs +29 -0
- package/package.json +67 -4
package/RELEASING.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Release preparation and owner runbook
|
|
2
|
+
|
|
3
|
+
The owner selected the **MIT** license and initial development version **0.1.0**.
|
|
4
|
+
`private: false`, `publishConfig.access: public` and the shipped [LICENSE](LICENSE)
|
|
5
|
+
prepare these candidates for a public release; they do not publish anything.
|
|
6
|
+
No package has been published. Tags, npm publication and external settings remain
|
|
7
|
+
separate owner-authorized actions. This is an initial 0.x release, not a stable 1.0
|
|
8
|
+
compatibility promise.
|
|
9
|
+
|
|
10
|
+
## Verification graph
|
|
11
|
+
|
|
12
|
+
Every push, pull request and manual CI run calls the same repository-local
|
|
13
|
+
`.github/workflows/verify.yml`. Documentation-only changes run the graph too;
|
|
14
|
+
there are no path filters. A clean GitHub-hosted Linux job builds and packs once.
|
|
15
|
+
The release manifest binds full source SHAs, package/version, filenames, byte
|
|
16
|
+
counts, SHA256 and npm SHA512 integrity to those unchanged tarballs.
|
|
17
|
+
|
|
18
|
+
| Job | Responsibility |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| candidate | Typecheck/build from exact source, pack once, record/upload identity |
|
|
21
|
+
| fast | Documentation links/fences and small unit checks |
|
|
22
|
+
| contracts | Storage, authentication, network and conversation contracts |
|
|
23
|
+
| integration | API/browser/feedback and independent peers; CLI installs all pinned candidates |
|
|
24
|
+
| packaged-linux/windows/macos | Install unchanged tarballs; startup, exports, SQLite and representative platform behavior |
|
|
25
|
+
| validation | Release failure simulations, registry/toolchain guards and actual workflow wiring assertions |
|
|
26
|
+
| gate | Always report; require each allowlisted job result to equal success and recheck artifact identity |
|
|
27
|
+
|
|
28
|
+
The candidate job strictly typechecks and compiles the complete TypeScript test
|
|
29
|
+
harness once before packing. It uploads a separate same-run `test-harness`
|
|
30
|
+
artifact, never inside an npm candidate. Each unprivileged test/OS job checks the
|
|
31
|
+
full source commit, complete canonical source inventory, exact LF source hashes
|
|
32
|
+
and exact emitted-output hashes before using that harness. Runtime/packaged checks
|
|
33
|
+
still import the unchanged candidate's installed JavaScript and execute real npm
|
|
34
|
+
shims. The publisher consumes only tested npm tarballs and never the harness.
|
|
35
|
+
|
|
36
|
+
All deterministic source tests run once on Linux, partitioned by
|
|
37
|
+
`.github/test-groups.json`. Its runner rejects unassigned/duplicate test files.
|
|
38
|
+
The CLI does not rerun complete SDK suites. It checks out the exact full client
|
|
39
|
+
and server SHAs in its candidate JSON files, builds/installs their tarballs, and
|
|
40
|
+
tests the combined package boundary. The SDKs have no dependency on one another;
|
|
41
|
+
the server candidate is a CLI integration peer, not a production CLI dependency.
|
|
42
|
+
Private cross-repository checkouts require owner-provided read access; no local
|
|
43
|
+
untracked sibling directory is assumed.
|
|
44
|
+
|
|
45
|
+
Node 24.12.0 is the supported CI baseline on GitHub-hosted ubuntu-latest,
|
|
46
|
+
windows-latest and macos-latest. Later Node majors are not separately certified.
|
|
47
|
+
The CLI checks unsupported Node 22.17.1 once on Linux. Most JavaScript/type checks
|
|
48
|
+
do not need an OS-by-runtime matrix. Packaged OS checks exercise installed shims,
|
|
49
|
+
paths/SQLite and the CLI's generic encrypted-backend/lifecycle fixture. They do
|
|
50
|
+
not certify a production OS keyring, physical terminal, browser engine or KMS.
|
|
51
|
+
|
|
52
|
+
SDK ordinary CI may restore a pnpm cache keyed by its frozen lockfile; installed
|
|
53
|
+
dependencies are never shared across OSes. Release-context verification restores
|
|
54
|
+
no dependency cache. The unpublished CLI dependency is supplied explicitly as a
|
|
55
|
+
tarball; SDK lockfiles and full commit/version pins govern inputs, with exact
|
|
56
|
+
TypeScript 5.9.3/@types-node 24.10.0/undici-types 7.16.0 tooling. No privileged
|
|
57
|
+
release consumes a pull-request cache, artifact or check-name lookup. Artifacts
|
|
58
|
+
are downloaded from this run only, and their manifest digest comes from the
|
|
59
|
+
candidate job output. Failure logs retain seven days; candidates retain fourteen.
|
|
60
|
+
Expired/missing artifacts require a new complete verification run.
|
|
61
|
+
|
|
62
|
+
## Release workflow and fail-closed gate
|
|
63
|
+
|
|
64
|
+
`release.yml` is manual dispatch only. Dispatch an existing owner-approved
|
|
65
|
+
`vVERSION` tag, supply the full tagged commit, matching version/tag, and
|
|
66
|
+
leave `publish=false` for verification only. A branch dispatch, wrong SHA,
|
|
67
|
+
version/tag mismatch, moved tag or missing/changed artifact is rejected. Do not
|
|
68
|
+
push a tag merely to test workflow syntax. All actions are pinned to immutable
|
|
69
|
+
commits; reusable verification is the same commit as its caller.
|
|
70
|
+
|
|
71
|
+
The allowlist is candidate, fast, contracts, integration, packaged-linux,
|
|
72
|
+
packaged-windows, packaged-macos and validation. Missing, empty, failed, canceled
|
|
73
|
+
and skipped results all fail. The always-running gate has no permission to
|
|
74
|
+
publish. The publisher needs the reusable verification workflow to succeed AND
|
|
75
|
+
its ready output to be exactly true AND explicit publish selection. It then
|
|
76
|
+
rechecks source/commit/ref/version, manifest digest, every tarball, unchanged
|
|
77
|
+
packed metadata, private/license policy, npm/toolchain and registry state.
|
|
78
|
+
|
|
79
|
+
Only the publishing job requests `id-token: write`. It runs on GitHub-hosted
|
|
80
|
+
Linux in protected environment `npm`. There is no publish credential in PR CI,
|
|
81
|
+
no workflow_run artifact promotion, no checkout of PR code in a privileged job,
|
|
82
|
+
no build/install/pack in the publisher, and no automatic publication on push/tag.
|
|
83
|
+
Publication uses `npm publish TESTED_TARBALL --access public --provenance --ignore-scripts`.
|
|
84
|
+
It publishes the unchanged bytes downloaded and checked in that job. Publication
|
|
85
|
+
is serialized per repository and is never canceled by a newer release run.
|
|
86
|
+
|
|
87
|
+
Safe local tests execute the gate with failed/canceled/missing/skipped checks,
|
|
88
|
+
wrong commit/version/tag/ref, missing artifact, tampered tarball/manifest and an
|
|
89
|
+
absent trusted digest. Registry tests reject an existing version, unauthorized or
|
|
90
|
+
uncertain registry responses, unsupported tools and mismatched dependency bytes.
|
|
91
|
+
These simulations and actionlint validate code/wiring. They do not prove GitHub
|
|
92
|
+
environment settings, npm ownership or OIDC exchange work.
|
|
93
|
+
|
|
94
|
+
Any documentation/package-metadata/source change after J6 creates a new candidate.
|
|
95
|
+
J6 evidence applies only to J6's recorded commits/digests. Re-run this complete
|
|
96
|
+
graph on final commits/tarballs and retain the new manifests/platform results.
|
|
97
|
+
Never label changed bytes publishable using old evidence. A passing Windows
|
|
98
|
+
local run does not substitute for actual hosted Linux/macOS jobs.
|
|
99
|
+
|
|
100
|
+
## Owner configuration — external, not performed by this repository
|
|
101
|
+
|
|
102
|
+
Rechecked against [npm trusted publishers](https://docs.npmjs.com/trusted-publishers/),
|
|
103
|
+
[GitHub secure use](https://docs.github.com/en/actions/reference/security/secure-use),
|
|
104
|
+
[OIDC](https://docs.github.com/en/actions/concepts/security/openid-connect), and
|
|
105
|
+
[deployment environments](https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments)
|
|
106
|
+
on **2026-10-23**. Recheck those live requirements before first publication:
|
|
107
|
+
|
|
108
|
+
1. The owner confirmed the `@poppykit` scope and selected MIT/version 0.1.0.
|
|
109
|
+
Confirm the actual publishing account has package-name/maintainer rights and
|
|
110
|
+
the chosen version is unused. The complete LICENSE is in the package allowlist
|
|
111
|
+
and release notes describe this initial development candidate.
|
|
112
|
+
Confirm repository.url exactly matches the actual GitHub repository.
|
|
113
|
+
2. Protect main, release tags, Actions files/scripts and candidate pins with
|
|
114
|
+
repository rules and review/CODEOWNERS appropriate to the actual team. Require
|
|
115
|
+
the complete verification result for the exact commit. Prevent unreviewed
|
|
116
|
+
workflow changes or tag replacement. Read-only PR permissions and trusted
|
|
117
|
+
publishing cannot enforce these owner settings themselves.
|
|
118
|
+
3. Create environment `npm` in each package repository with required reviewers,
|
|
119
|
+
self-approval restrictions, protected release-tag rules and no admin bypass
|
|
120
|
+
where the repository plan supports them. Confirm feature availability; an
|
|
121
|
+
unprotected environment name alone provides no approval barrier. Add environment
|
|
122
|
+
variable `NPM_PUBLISH_ENABLED=true` only after setup/authorization. Missing
|
|
123
|
+
or false configuration fails the publisher's check.
|
|
124
|
+
4. Configure each npm package's trusted publisher for GitHub owner `LMol-4`,
|
|
125
|
+
the corresponding `poppykit-client`, `poppykit-server` or `poppykit-cli`
|
|
126
|
+
repository, workflow filename **release.yml**, and environment **npm**.
|
|
127
|
+
Match spelling/case and the initiating workflow identity. verify.yml is a
|
|
128
|
+
reusable verifier, not the publishing workflow. Reassess identity if introducing
|
|
129
|
+
a reusable publisher or changing callers; workflow_call/dispatch identity has
|
|
130
|
+
documented caller nuances. This setup authorizes a workflow identity, not its
|
|
131
|
+
test results. The gate supplies the independent verification condition.
|
|
132
|
+
5. npm currently requires npm >=11.5.1 and Node >=22.14.0 for trusted publishing;
|
|
133
|
+
this workflow uses Node24.12.0 and checks npm's version without upgrading it.
|
|
134
|
+
GitHub-hosted runners are supported; self-hosted runners are not supported for
|
|
135
|
+
this npm path. GitHub permissions must permit OIDC. No NODE_AUTH_TOKEN is set.
|
|
136
|
+
Do not silently fall back to a long-lived publish token when OIDC fails.
|
|
137
|
+
6. Verify provenance requirements, repository visibility and package visibility
|
|
138
|
+
in the current npm documentation. Trusted publishing generates provenance
|
|
139
|
+
automatically for supported public packages/public repositories; explicit
|
|
140
|
+
--provenance must not be treated as proof without checking the registry record.
|
|
141
|
+
A private-source repository may need an owner-approved alternative or visibility
|
|
142
|
+
decision; do not claim provenance supported where it is not.
|
|
143
|
+
7. Current npm publisher configurations require first successful use within two
|
|
144
|
+
days or expire; configure near the authorized release. npm supports up to ten
|
|
145
|
+
configurations, with separate direct-publish/staging/dist-tag permissions.
|
|
146
|
+
Grant only this workflow's required direct publication capability, review other
|
|
147
|
+
publishers/tokens and restrict alternative access according to owner policy.
|
|
148
|
+
Keep recovery access separate from the normal workflow.
|
|
149
|
+
|
|
150
|
+
## First publication and bootstrap
|
|
151
|
+
|
|
152
|
+
A new package may need first publication using authorized npm-owner authentication
|
|
153
|
+
before package settings/trusted publishing are available. npm name availability
|
|
154
|
+
has not been established by creating a GitHub repository. The owner must confirm
|
|
155
|
+
the current bootstrap flow and explicitly authorize it. Do not place bootstrap
|
|
156
|
+
tokens in PR workflows or temporarily make publication unconditional.
|
|
157
|
+
|
|
158
|
+
Prepare a license/version-ready candidate, run the exact full verification graph,
|
|
159
|
+
retain/download the unchanged manifest/tarball and verify its digest. If the first
|
|
160
|
+
publication must be manual, an authorized owner uses their own current npm login/
|
|
161
|
+
2FA or narrowly scoped supported bootstrap credential to publish those exact bytes,
|
|
162
|
+
then removes temporary credentials and configures the trusted publisher. Record
|
|
163
|
+
the resulting version/integrity/provenance. A dry run is not proof of bootstrap
|
|
164
|
+
permissions or production OIDC authentication. Current 0.1.0 candidates have public-ready metadata, but publication still
|
|
165
|
+
requires all exact-byte checks, owner settings and separate authorization.
|
|
166
|
+
|
|
167
|
+
## Package order, existing versions and partial recovery
|
|
168
|
+
|
|
169
|
+
Release by actual production dependencies: client and server are independent;
|
|
170
|
+
either SDK can publish first. Publish the client before a CLI version requiring
|
|
171
|
+
that exact client version. The CLI registry guard requires the published client's
|
|
172
|
+
dist.integrity to match its tested candidate tarball. If it differs, stop and
|
|
173
|
+
reconcile/retest the actual dependency; do not silently replace tested bytes.
|
|
174
|
+
|
|
175
|
+
Before publishing, npm view must confirm that this package version is absent.
|
|
176
|
+
Only a structured E404 is considered absent; auth/network/registry failures stop.
|
|
177
|
+
An existing version stops publication even after an uncertain earlier response.
|
|
178
|
+
Inspect npm's published version, integrity, provenance and intended dist-tag using
|
|
179
|
+
read-only registry commands. Never overwrite or blindly republish. If the exact
|
|
180
|
+
bytes already published, record that success and resume the remaining package's
|
|
181
|
+
separate verified release. Multi-repository releases are not atomic. Do not unpublish
|
|
182
|
+
a successful dependency as rollback. If correction is required, choose a new
|
|
183
|
+
version, update dependent pins/lockfiles/release notes and verify a new candidate.
|
|
184
|
+
|
|
185
|
+
This runbook does not authorize publication, tag pushes, package ownership changes,
|
|
186
|
+
external settings, deployment or demo implementation.
|
|
187
|
+
|
|
188
|
+
Candidate metadata is inspected with the archive basename and its directory as
|
|
189
|
+
`cwd`. This avoids GNU tar interpreting a Windows drive colon as a remote archive;
|
|
190
|
+
stderr is preserved in failure diagnostics. Negative checks exercise GNU tar on
|
|
191
|
+
Windows with space/Unicode candidate paths and retain wrong/missing/changed bytes
|
|
192
|
+
and fail-closed gate cases. Local success does not certify a future hosted run or
|
|
193
|
+
any newly packed bytes.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Security reporting
|
|
2
|
+
|
|
3
|
+
Do not put credentials, tokens, private keys, personal records or exploit details
|
|
4
|
+
in a public issue. Use this repository's GitHub **Security → Report a vulnerability**
|
|
5
|
+
when private reporting is enabled. If it is unavailable, request a private contact
|
|
6
|
+
from the repository owner without disclosing the vulnerability publicly. The owner
|
|
7
|
+
must enable/confirm that route before public release; no response SLA is promised.
|
|
8
|
+
|
|
9
|
+
Include affected commit/version, a minimal reproduction using synthetic data,
|
|
10
|
+
impact and the trust boundary involved. These unpublished draft packages have no
|
|
11
|
+
released-version support window yet. Custom vaults, issuers, browser controllers,
|
|
12
|
+
proxy routing and deployment policy remain operator responsibilities. Redaction
|
|
13
|
+
and scoped authority do not isolate an unrestricted same-user program.
|
package/STORAGE.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Company persistence and authorization
|
|
2
|
+
|
|
3
|
+
J1 replaces whole-state storage on request paths with domain selections. This is a
|
|
4
|
+
breaking adapter change in an unpublished package. Node 24.12.0 or later is required.
|
|
5
|
+
The optional `@poppykit/server/sqlite` entry point exports `SqliteCompanyDatabase`.
|
|
6
|
+
The core and reference issuer entry points do not import SQLite. Consumers can
|
|
7
|
+
implement `ConversationStore`, `IssuerStore` and `AuthorityCoordinator` themselves.
|
|
8
|
+
The tested Node 24.12.0 runtime marks `node:sqlite` experimental.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { SqliteCompanyDatabase } from '@poppykit/server/sqlite';
|
|
12
|
+
const database = new SqliteCompanyDatabase('/private/path/company.db');
|
|
13
|
+
const issuer = 'https://company.example/issuer';
|
|
14
|
+
const issuerStore = database.issuer(issuer);
|
|
15
|
+
const conversationStore = database.conversations(issuer);
|
|
16
|
+
// Reference issuer options: store: issuerStore, coordinator: database.
|
|
17
|
+
// Conversation options: store: conversationStore, policy: referenceIssuer.authority.policy.
|
|
18
|
+
// Close database only after all in-flight requests/work have ended.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Use the same database instance for the reference issuer, its coordinator, and its
|
|
22
|
+
conversation storage. A policy check and the protected conversation mutation join
|
|
23
|
+
one SQLite transaction. Revocation/rotation/acceptance cannot interleave between
|
|
24
|
+
authorization and commit. External issuers still supply their own policy and must
|
|
25
|
+
state revocation latency and how their checks are ordered with protected writes.
|
|
26
|
+
Never replace the coordinator with a process-local mutex for a multi-process service.
|
|
27
|
+
|
|
28
|
+
SQLite uses WAL and FULL synchronous commits. A database serializes writers; this
|
|
29
|
+
reference adapter does not promise parallel SQLite writers. The short per-instance
|
|
30
|
+
queue protects its single connection, and SQLite coordinates separate processes.
|
|
31
|
+
There is no application-wide static lock: separate databases are independent. The
|
|
32
|
+
coordinator is a database transaction boundary, not a lock held during external
|
|
33
|
+
model, vault, or HTTP calls. SQL busy retries are bounded. Use a local filesystem
|
|
34
|
+
with correct SQLite locks; network-mounted multi-host SQLite is unsupported.
|
|
35
|
+
|
|
36
|
+
## Domain records and guarantees
|
|
37
|
+
|
|
38
|
+
Every primary key includes a namespace (use the exact full issuer). Sessions,
|
|
39
|
+
grants, current/previous token hashes, authorization requests, codes, access-token
|
|
40
|
+
hashes, replay keys, conversation metadata, shared account bindings, message
|
|
41
|
+
receipts and work intent are separate records. Ordered events are a separate table
|
|
42
|
+
keyed by namespace/conversation/sequence. Indexes cover client, account, grant,
|
|
43
|
+
conversation, binding, phase and expiry. User identity and authorization checks
|
|
44
|
+
still apply inside the SDK; a namespace name alone is not authentication.
|
|
45
|
+
|
|
46
|
+
Conversation selections resolve an ID, message receipt, work ID, or next eligible
|
|
47
|
+
claim. Reads include the related parent/Direct Conversation binding group, not
|
|
48
|
+
unrelated conversations. A worker query selects the oldest eligible message for a
|
|
49
|
+
conversation; publication rechecks lease and fence. Event delivery currently reads
|
|
50
|
+
the selected conversation group's retained events. J3 owns bounded page queries,
|
|
51
|
+
notification-backed waits, streaming and retention; this does not claim those jobs
|
|
52
|
+
are complete. `ConversationNotifications` defines subscribe/unsubscribe/publish:
|
|
53
|
+
subscribe before reading, treat wakes as hints, and always reconcile durable state
|
|
54
|
+
after a wake or bounded timeout. Publishing after commit is best-effort, not an outbox.
|
|
55
|
+
|
|
56
|
+
Issuer selections address metadata, token admission, a principal, a token hash, an
|
|
57
|
+
authorization, or a client/account disconnect. Session and authorization limits
|
|
58
|
+
use indexed counts rather than loading other clients' records. Related grant
|
|
59
|
+
sessions are loaded when revocation must sign them out atomically.
|
|
60
|
+
|
|
61
|
+
All mutations run synchronous callbacks over detached domain records. Async
|
|
62
|
+
callbacks and non-JSON values are rejected. Exceptions roll back; domain savepoints
|
|
63
|
+
also roll back if an enclosing policy caller catches an error. Returned values and
|
|
64
|
+
retained callback objects cannot mutate persisted state after commit. Only changed
|
|
65
|
+
rows and newly appended events are written. Conversation deletion, event rewriting,
|
|
66
|
+
and generation reset are not silently accepted through a targeted transaction.
|
|
67
|
+
|
|
68
|
+
Acceptance, event append, message deduplication receipt and pending work commit in
|
|
69
|
+
one transaction. There is no second outbox. The same message ID with different
|
|
70
|
+
content/target conflicts. Claim recovery can execute application work again after
|
|
71
|
+
a lease expires: the fence prevents stale SDK publication, but cannot undo or
|
|
72
|
+
deduplicate arbitrary external effects. The application's downstream system needs
|
|
73
|
+
its own idempotency key (such as the stable work ID), or the application must retain
|
|
74
|
+
an uncertain result for reconciliation. Never claim universal exactly-once execution.
|
|
75
|
+
|
|
76
|
+
`cleanup(namespace, now, limit)` bounds deletion to at most 1000 expired short-lived
|
|
77
|
+
codes, authorizations, access tokens or replay entries. It does not delete grants,
|
|
78
|
+
work intent, message receipts or conversation events. J3 must preserve receipt
|
|
79
|
+
identity through retention for the conversation lifetime. Finished work pruning is
|
|
80
|
+
per selected conversation binding group; unrelated conversation growth cannot
|
|
81
|
+
force another conversation's receipts out of storage.
|
|
82
|
+
|
|
83
|
+
## Non-durable use and resets
|
|
84
|
+
|
|
85
|
+
`new SqliteCompanyDatabase(':memory:')` is an explicit non-durable adapter for tests
|
|
86
|
+
and development. Each instance is separate, is lost at close/process death, and
|
|
87
|
+
offers no cross-process persistence. The test fixture's object store is also
|
|
88
|
+
non-durable and is not evidence of database atomicity.
|
|
89
|
+
|
|
90
|
+
No old whole-state files are migrated or deleted automatically. For a disposable
|
|
91
|
+
development reset, stop all processes, resolve uncertain external effects, and
|
|
92
|
+
explicitly move aside the database and WAL/SHM files. Reopen a fresh database to
|
|
93
|
+
obtain new generations. Never reset a live database behind active processes.
|
|
94
|
+
|
|
95
|
+
`tests/storage.test.mts` exercises real child-process contention and termination
|
|
96
|
+
before commit, after acceptance, and after a synthetic external side effect. It
|
|
97
|
+
also checks publication fencing/deduplication, detached values, rollback, account
|
|
98
|
+
token rotation versus revocation, namespace isolation, and unchanged read counts
|
|
99
|
+
after adding unrelated conversations. These are local Windows/Node results;
|
|
100
|
+
other OS runner evidence belongs to J5/J6.
|
|
101
|
+
|
|
102
|
+
## Bounded conversation delivery and retention
|
|
103
|
+
|
|
104
|
+
`ConversationSelection.events` loads a cursor-relative, count/byte-bounded event
|
|
105
|
+
window without work/history aggregates; it is read-only inside transactions.
|
|
106
|
+
`append` loads operational records without historical event payloads. `cursorCheck`
|
|
107
|
+
checks retained cursor identities for waited POSTs. These selectors preserve the
|
|
108
|
+
shared authority coordinator. Custom adapters must implement equivalent targeted
|
|
109
|
+
reads before claiming the same operation-count guarantees.
|
|
110
|
+
|
|
111
|
+
SQLite `pruneEvents(conversationId, beforeUnixMilliseconds, limit=100)` deletes at
|
|
112
|
+
most 1000 old payloads per call and refuses active held/pending/claimed work.
|
|
113
|
+
Message receipts, event cursor identities and the latest user message/context stay
|
|
114
|
+
with the conversation for its lifetime. Closure is not deletion. Retention uses
|
|
115
|
+
one database transaction; concurrent writers/readers see a coherent before/after
|
|
116
|
+
state. Default payload retention is indefinite; operators should retain at least
|
|
117
|
+
30 days and explicitly choose any shorter policy. See [DELIVERY.md](DELIVERY.md)
|
|
118
|
+
for distinct lifetimes and client recovery behavior. No automatic destructive
|
|
119
|
+
reset or history deletion was added.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ConversationOptions } from './conversation/service.js';
|
|
2
|
+
type Store = ConversationOptions['store'] & {
|
|
3
|
+
inspect(): Promise<import('./conversation/service.js').ConversationStoreState>;
|
|
4
|
+
adminTransaction<T>(change: (draft: import('./conversation/service.js').ConversationStoreState) => T): Promise<T>;
|
|
5
|
+
};
|
|
6
|
+
export type ConversationAdapterFixture = () => Promise<{
|
|
7
|
+
store: Store;
|
|
8
|
+
second: Store;
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function runConversationAdapterContract(fixture: ConversationAdapterFixture): void;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Contract tests for a host's conversation storage adapter. Call runConversationAdapterContract(fixture) from a
|
|
2
|
+
// node:test file; each case gets a fresh fixture: two handles (e.g. two connections) on one fresh namespace that the
|
|
3
|
+
// host created from initialConversationState. Process kill and engine atomicity stay the host's own tests.
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { AuthorityError } from './auth/authority.js';
|
|
7
|
+
import { createConversationService, resetConversationState } from './conversation/service.js';
|
|
8
|
+
const endpoint = 'https://contract.example.test/poppy/conversations';
|
|
9
|
+
const base = { issuer: 'https://auth.contract.test', clientId: 'https://agent.contract.test/client', sessionId: 's1', userId: 'u_1', scopes: [], tokenExpiresAt: 9e12, sessionVersion: 1 };
|
|
10
|
+
const principals = {
|
|
11
|
+
signedOut: base,
|
|
12
|
+
account1: { ...base, accountId: 'acct_1', scopes: ['poppy:read'] },
|
|
13
|
+
account2: { ...base, accountId: 'acct_2', scopes: ['poppy:read'] },
|
|
14
|
+
};
|
|
15
|
+
const message = { id: 'msg_1', sender: 'agent', text: 'Where is order 1042?' };
|
|
16
|
+
const reply = { id: 'msg_r1', sender: 'agent', text: 'Shipped.' };
|
|
17
|
+
function check(held, needed, action) {
|
|
18
|
+
if (!needed.every(scope => held.includes(scope)))
|
|
19
|
+
return Promise.reject(new AuthorityError('insufficient_scope'));
|
|
20
|
+
return action();
|
|
21
|
+
}
|
|
22
|
+
const policy = {
|
|
23
|
+
withCurrent: (principal, scopes, action) => check(principal.scopes, scopes, action),
|
|
24
|
+
withAccepted: (authority, scopes, action) => check(authority.principal.scopes, scopes, action),
|
|
25
|
+
};
|
|
26
|
+
function serviceOn(store, clock) {
|
|
27
|
+
const authenticator = {
|
|
28
|
+
async authenticate(request) {
|
|
29
|
+
const principal = principals[request.headers.get('authorization')?.slice('DPoP '.length) ?? ''];
|
|
30
|
+
if (!principal)
|
|
31
|
+
throw new AuthorityError('invalid_token');
|
|
32
|
+
return principal;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
return createConversationService({ endpoint, store, authenticator, policy, clock, leaseMs: 1000 });
|
|
36
|
+
}
|
|
37
|
+
async function call(service, path, as, body, query = '') {
|
|
38
|
+
const response = await service.handle(new Request(`${endpoint}${path}${query}`, {
|
|
39
|
+
method: body === undefined ? 'GET' : 'POST', headers: { authorization: `DPoP ${as}` }, body: body === undefined ? undefined : JSON.stringify(body),
|
|
40
|
+
}));
|
|
41
|
+
return { status: response.status, body: await response.json() };
|
|
42
|
+
}
|
|
43
|
+
export function runConversationAdapterContract(fixture) {
|
|
44
|
+
const clock = { t: Date.UTC(2026, 9, 19, 12), now: () => clock.t };
|
|
45
|
+
async function withFixture(body) {
|
|
46
|
+
const opened = await fixture();
|
|
47
|
+
try {
|
|
48
|
+
await body(opened);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await opened.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
test('adapter contract: reads and results are detached; failed, non-JSON and async changes roll back', () => withFixture(async ({ store, second }) => {
|
|
55
|
+
const snapshot = await store.inspect();
|
|
56
|
+
snapshot.bindings.leaked = {};
|
|
57
|
+
assert.deepEqual((await store.inspect()).bindings, {});
|
|
58
|
+
let draftAfterCommit;
|
|
59
|
+
const result = await store.adminTransaction(draft => { draft.bindings.kept = {}; draftAfterCommit = draft; return draft.bindings; });
|
|
60
|
+
result.kept.accountId = 'leaked';
|
|
61
|
+
draftAfterCommit.bindings.late = {};
|
|
62
|
+
assert.deepEqual((await second.inspect()).bindings, { kept: {} });
|
|
63
|
+
await assert.rejects(store.adminTransaction(draft => { draft.bindings.thrown = {}; throw new Error('abort'); }), { message: 'abort' });
|
|
64
|
+
await assert.rejects(store.adminTransaction(draft => { draft.bindings.undefinedField = { accountId: undefined }; }));
|
|
65
|
+
await assert.rejects(store.adminTransaction((async (draft) => { draft.bindings.async = {}; })));
|
|
66
|
+
assert.deepEqual((await second.inspect()).bindings, { kept: {} });
|
|
67
|
+
}));
|
|
68
|
+
test('adapter contract: concurrent transactions on both handles lose no update', () => withFixture(async ({ store, second }) => {
|
|
69
|
+
await Promise.all(Array.from({ length: 20 }, (_, i) => (i % 2 ? store : second).adminTransaction(draft => { draft.bindings[`b${i}`] = {}; })));
|
|
70
|
+
assert.equal(Object.keys((await store.inspect()).bindings).length, 20);
|
|
71
|
+
}));
|
|
72
|
+
test('adapter contract: simultaneous first-message retries on both handles start one conversation and one job', () => withFixture(async ({ store, second }) => {
|
|
73
|
+
const [a, b] = [serviceOn(store, clock), serviceOn(second, clock)];
|
|
74
|
+
const results = await Promise.all([a, b, a, b].map(service => call(service, '', 'signedOut', { message })));
|
|
75
|
+
assert.ok(results.every(result => result.status === 201));
|
|
76
|
+
assert.equal(new Set(results.map(result => result.body.conversation_id)).size, 1);
|
|
77
|
+
const state = await second.inspect();
|
|
78
|
+
assert.equal(Object.keys(state.conversations).length, 1);
|
|
79
|
+
assert.equal(state.work.length, 1);
|
|
80
|
+
}));
|
|
81
|
+
test('adapter contract: racing account use across handles binds parent and Direct Conversation to exactly one account', () => withFixture(async ({ store, second }) => {
|
|
82
|
+
const [a, b] = [serviceOn(store, clock), serviceOn(second, clock)];
|
|
83
|
+
const parent = (await call(a, '', 'account1', { message })).body.conversation_id;
|
|
84
|
+
const parentWork = (await a.claim());
|
|
85
|
+
const outcome = (promise) => promise.then(value => value.accountId, error => String(error.code));
|
|
86
|
+
const results = await Promise.all([
|
|
87
|
+
outcome(a.requireAccount(parentWork, ['poppy:read'])),
|
|
88
|
+
(async () => {
|
|
89
|
+
const opened = await call(b, '', 'account2', { parent_conversation_id: parent, message: { id: 'msg_c', sender: 'human', text: 'Hi.' } });
|
|
90
|
+
if (opened.status !== 201)
|
|
91
|
+
return String(opened.body.error);
|
|
92
|
+
return outcome(b.claim().then(work => b.requireAccount(work, ['poppy:read'])));
|
|
93
|
+
})(),
|
|
94
|
+
]);
|
|
95
|
+
const winners = results.filter(result => result.startsWith('acct_'));
|
|
96
|
+
assert.equal(winners.length, 1, String(results));
|
|
97
|
+
const state = await store.inspect();
|
|
98
|
+
assert.ok(Object.values(state.conversations).every(conversation => conversation.bindingId === parent));
|
|
99
|
+
assert.deepEqual(state.bindings[parent], { accountId: winners[0] });
|
|
100
|
+
const loser = winners[0] === 'acct_1' ? 'account2' : 'account1';
|
|
101
|
+
for (const id of Object.keys(state.conversations))
|
|
102
|
+
assert.deepEqual((await call(b, `/${id}/events`, loser)).body, { error: 'sign_in_required' });
|
|
103
|
+
}));
|
|
104
|
+
test('adapter contract: lease reclaim on the other handle fences the old worker; output IDs dedupe', () => withFixture(async ({ store, second }) => {
|
|
105
|
+
const [a, b] = [serviceOn(store, clock), serviceOn(second, clock)];
|
|
106
|
+
const id = (await call(a, '', 'signedOut', { message })).body.conversation_id;
|
|
107
|
+
const old = (await a.claim());
|
|
108
|
+
clock.t += 1000;
|
|
109
|
+
const fresh = (await b.claim());
|
|
110
|
+
assert.deepEqual([fresh.workId === old.workId, fresh.fence === old.fence], [true, false]);
|
|
111
|
+
await assert.rejects(a.reply(old, 'out_1', reply), { code: 'stale_work' });
|
|
112
|
+
await b.reply(fresh, 'out_1', reply);
|
|
113
|
+
await a.reply(fresh, 'out_1', reply);
|
|
114
|
+
await assert.rejects(a.reply(fresh, 'out_1', { ...reply, text: 'changed' }), { code: 'output_conflict' });
|
|
115
|
+
const events = (await call(b, `/${id}/events`, 'signedOut')).body.events;
|
|
116
|
+
assert.equal(events.filter(event => event.message?.id === reply.id).length, 1);
|
|
117
|
+
}));
|
|
118
|
+
test('adapter contract: a waited POST committed before a crash is recovered unchanged on the other handle', () => withFixture(async ({ store, second }) => {
|
|
119
|
+
const crashing = serviceOn({ read: () => store.inspect(), transaction: (_selection, change) => store.adminTransaction(change).then(() => { throw new Error('crash after commit'); }) }, clock);
|
|
120
|
+
await assert.rejects(call(crashing, '', 'signedOut', { message }, '?wait=10'), { message: 'crash after commit' });
|
|
121
|
+
const b = serviceOn(second, clock);
|
|
122
|
+
const recovered = await call(b, '', 'signedOut', { message }, '?wait=1');
|
|
123
|
+
assert.equal(recovered.status, 201);
|
|
124
|
+
assert.equal(recovered.body.events.length, 2);
|
|
125
|
+
await b.reply((await b.claim()), 'out_1', reply);
|
|
126
|
+
assert.deepEqual(await call(b, '', 'signedOut', { message }), recovered);
|
|
127
|
+
assert.equal((await store.inspect()).work.length, 1);
|
|
128
|
+
}));
|
|
129
|
+
test('adapter contract: a reset through one handle invalidates fences and conversations used through the other', () => withFixture(async ({ store, second }) => {
|
|
130
|
+
const a = serviceOn(store, clock);
|
|
131
|
+
const id = (await call(a, '', 'signedOut', { message })).body.conversation_id;
|
|
132
|
+
const work = (await a.claim());
|
|
133
|
+
const { generation } = await store.inspect();
|
|
134
|
+
await second.adminTransaction(resetConversationState);
|
|
135
|
+
assert.notEqual((await store.inspect()).generation, generation);
|
|
136
|
+
await assert.rejects(a.reply(work, 'out_1', reply), { code: 'stale_work' });
|
|
137
|
+
assert.equal(await a.claim(), null);
|
|
138
|
+
assert.equal((await call(a, `/${id}/events`, 'signedOut')).status, 404);
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ExternalAuthorityAdapter, RequestAuthenticator, ResourceEndpoint } from './authority.js';
|
|
2
|
+
export declare function createRequestAuthenticator(options: {
|
|
3
|
+
authority: Pick<ExternalAuthorityAdapter, 'introspect'>;
|
|
4
|
+
endpoints: ResourceEndpoint[];
|
|
5
|
+
now: () => number;
|
|
6
|
+
}): RequestAuthenticator;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { secureUrl } from '../network/transport.js';
|
|
2
|
+
import { AuthorityError } from './authority.js';
|
|
3
|
+
import { verifyDpopProof } from './dpop.js';
|
|
4
|
+
const DPOP_AUTHORIZATION = /^DPoP ([A-Za-z0-9\-._~+/]+=*)$/i;
|
|
5
|
+
// Only introspect is used here; the policy half of the adapter is consumed by the conversation service.
|
|
6
|
+
export function createRequestAuthenticator(options) {
|
|
7
|
+
for (const endpoint of options.endpoints) {
|
|
8
|
+
if (secureUrl(endpoint.origin).origin !== endpoint.origin || !endpoint.pathPrefix.startsWith('/'))
|
|
9
|
+
throw new TypeError('Invalid resource endpoint');
|
|
10
|
+
if (endpoint.mcp && (endpoint.resource !== `${endpoint.origin}${endpoint.pathPrefix}` || secureUrl(endpoint.mcp.metadataUrl).href !== endpoint.mcp.metadataUrl))
|
|
11
|
+
throw new TypeError('Invalid MCP resource');
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
async authenticate(request) {
|
|
15
|
+
// The Request URL must already be the trusted public URL; forwarding headers are never read.
|
|
16
|
+
const url = new URL(request.url);
|
|
17
|
+
const endpoint = options.endpoints
|
|
18
|
+
.filter(item => url.origin === item.origin && (url.pathname === item.pathPrefix || (!item.mcp && url.pathname.startsWith(item.pathPrefix.endsWith('/') ? item.pathPrefix : `${item.pathPrefix}/`))))
|
|
19
|
+
.sort((a, b) => b.pathPrefix.length - a.pathPrefix.length)[0];
|
|
20
|
+
if (!endpoint)
|
|
21
|
+
throw new AuthorityError('invalid_token');
|
|
22
|
+
if (endpoint.mcp && request.url !== endpoint.resource)
|
|
23
|
+
throw new AuthorityError('invalid_token');
|
|
24
|
+
if (endpoint.mcp && /^Bearer /i.test(request.headers.get('authorization') ?? '')) {
|
|
25
|
+
const bearer = /^Bearer ([A-Za-z0-9\-._~+/]+=*)$/i.exec(request.headers.get('authorization') ?? '')?.[1];
|
|
26
|
+
if (!bearer || bearer.length > 4096)
|
|
27
|
+
throw new AuthorityError('invalid_token');
|
|
28
|
+
const { principal, dpopJkt } = await options.authority.introspect(bearer, endpoint.resource);
|
|
29
|
+
// Ordinary MCP issuers return null only for unbound Bearer tokens. A bound token never downgrades.
|
|
30
|
+
if (dpopJkt !== null || principal.tokenExpiresAt <= options.now())
|
|
31
|
+
throw new AuthorityError('invalid_token');
|
|
32
|
+
if (!principal.userId && !principal.accountId)
|
|
33
|
+
throw new AuthorityError('invalid_token');
|
|
34
|
+
return principal;
|
|
35
|
+
}
|
|
36
|
+
if (endpoint.mcp && !endpoint.mcp.dpop)
|
|
37
|
+
throw new AuthorityError('invalid_token');
|
|
38
|
+
const token = DPOP_AUTHORIZATION.exec(request.headers.get('authorization') ?? '')?.[1];
|
|
39
|
+
if (!token || token.length > 4096)
|
|
40
|
+
throw new AuthorityError('invalid_token');
|
|
41
|
+
const nowMs = options.now();
|
|
42
|
+
const proof = await verifyDpopProof(request.headers.get('dpop'), { method: request.method, url: `${url.origin}${url.pathname}`, nowMs, accessToken: token });
|
|
43
|
+
const { principal, dpopJkt } = await options.authority.introspect(token, endpoint.resource);
|
|
44
|
+
if (principal.tokenExpiresAt <= nowMs)
|
|
45
|
+
throw new AuthorityError('invalid_token');
|
|
46
|
+
if (dpopJkt !== proof.jkt)
|
|
47
|
+
throw new AuthorityError('invalid_dpop_proof');
|
|
48
|
+
return principal;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export interface Principal {
|
|
2
|
+
issuer: string;
|
|
3
|
+
clientId: string;
|
|
4
|
+
userId: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
accountId?: string;
|
|
7
|
+
grantId?: string;
|
|
8
|
+
scopes: string[];
|
|
9
|
+
tokenExpiresAt: number;
|
|
10
|
+
sessionVersion: number;
|
|
11
|
+
issuerGeneration?: string;
|
|
12
|
+
}
|
|
13
|
+
export type WorkRevocationPolicy = 'stop-account-work' | 'allow-accepted-work-to-finish';
|
|
14
|
+
export interface AcceptedAuthority {
|
|
15
|
+
principal: Principal;
|
|
16
|
+
acceptedAt: number;
|
|
17
|
+
revocationPolicy: WorkRevocationPolicy;
|
|
18
|
+
}
|
|
19
|
+
export interface AuthorityCoordinator {
|
|
20
|
+
run<T>(action: () => Promise<T>): Promise<T>;
|
|
21
|
+
}
|
|
22
|
+
export interface AuthorityPolicy {
|
|
23
|
+
withCurrent<T>(principal: Principal, scopes: readonly string[], action: () => Promise<T>): Promise<T>;
|
|
24
|
+
withAccepted<T>(authority: AcceptedAuthority, scopes: readonly string[], action: () => Promise<T>): Promise<T>;
|
|
25
|
+
}
|
|
26
|
+
export interface RequestAuthenticator {
|
|
27
|
+
authenticate(request: Request): Promise<Principal>;
|
|
28
|
+
}
|
|
29
|
+
export interface ResourceEndpoint {
|
|
30
|
+
origin: string;
|
|
31
|
+
pathPrefix: string;
|
|
32
|
+
resource?: string;
|
|
33
|
+
mcp?: {
|
|
34
|
+
dpop: boolean;
|
|
35
|
+
metadataUrl: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export interface ExternalAuthorityAdapter {
|
|
39
|
+
issuer: string;
|
|
40
|
+
revocationVisibility: string;
|
|
41
|
+
introspect(token: string, resource?: string): Promise<{
|
|
42
|
+
principal: Principal;
|
|
43
|
+
dpopJkt: string | null;
|
|
44
|
+
}>;
|
|
45
|
+
policy: AuthorityPolicy;
|
|
46
|
+
}
|
|
47
|
+
export type AuthorityErrorCode = 'invalid_token' | 'invalid_dpop_proof' | 'use_dpop_nonce' | 'sign_in_required' | 'insufficient_scope';
|
|
48
|
+
export declare class AuthorityError extends Error {
|
|
49
|
+
readonly code: AuthorityErrorCode;
|
|
50
|
+
readonly status: 401 | 403;
|
|
51
|
+
readonly headers: Headers;
|
|
52
|
+
constructor(code: AuthorityErrorCode, options?: {
|
|
53
|
+
scope?: string;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Resource-side error (spec section 6): carried in WWW-Authenticate, never echoing tokens or peer values.
|
|
2
|
+
export class AuthorityError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
headers;
|
|
6
|
+
constructor(code, options = {}) {
|
|
7
|
+
super(`Authorization failed: ${code}`);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.name = 'AuthorityError';
|
|
10
|
+
this.status = code === 'sign_in_required' || code === 'insufficient_scope' ? 403 : 401;
|
|
11
|
+
if (options.scope !== undefined && !/^[\x21\x23-\x5B\x5D-\x7E ]*$/.test(options.scope))
|
|
12
|
+
throw new TypeError('scope');
|
|
13
|
+
const scope = options.scope === undefined ? '' : `, scope="${options.scope}"`;
|
|
14
|
+
this.headers = new Headers({ 'WWW-Authenticate': `DPoP error="${code}"${scope}, algs="ES256"` });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DpopClaims } from '../protocol/wire.js';
|
|
2
|
+
export declare const PROOF_WINDOW_SECONDS = 60;
|
|
3
|
+
export declare const CLOCK_SKEW_SECONDS = 30;
|
|
4
|
+
export declare function base64urlSha256(value: string): string;
|
|
5
|
+
export declare function canonicalHtu(url: string): string;
|
|
6
|
+
export declare function assertUniqueJwtMembers(jwt: string): void;
|
|
7
|
+
export declare function verifyDpopProof(proof: string | null, expected: {
|
|
8
|
+
method: string;
|
|
9
|
+
url: string;
|
|
10
|
+
nowMs: number;
|
|
11
|
+
accessToken?: string;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
claims: DpopClaims;
|
|
14
|
+
jkt: string;
|
|
15
|
+
}>;
|