@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.
Files changed (50) hide show
  1. package/API.md +25 -0
  2. package/AUTH.md +51 -0
  3. package/CHANGELOG.md +22 -0
  4. package/CONTRIBUTING.md +50 -0
  5. package/DELIVERY.md +113 -0
  6. package/INTEGRATIONS.md +87 -0
  7. package/LICENSE +21 -0
  8. package/PROVIDER.md +99 -0
  9. package/README.md +123 -2
  10. package/RELEASING.md +193 -0
  11. package/SECURITY.md +13 -0
  12. package/STORAGE.md +99 -0
  13. package/dist/auth/challenge.d.ts +19 -0
  14. package/dist/auth/challenge.js +68 -0
  15. package/dist/auth/client-auth.d.ts +297 -0
  16. package/dist/auth/client-auth.js +1583 -0
  17. package/dist/auth/ownership.d.ts +9 -0
  18. package/dist/auth/ownership.js +23 -0
  19. package/dist/auth/sqlite-store.d.ts +14 -0
  20. package/dist/auth/sqlite-store.js +123 -0
  21. package/dist/auth/store.d.ts +19 -0
  22. package/dist/auth/store.js +1 -0
  23. package/dist/conversation-store.d.ts +15 -0
  24. package/dist/conversation-store.js +1 -0
  25. package/dist/conversation.d.ts +118 -0
  26. package/dist/conversation.js +404 -0
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.js +7 -0
  29. package/dist/integrations.d.ts +30 -0
  30. package/dist/integrations.js +58 -0
  31. package/dist/memory.d.ts +15 -0
  32. package/dist/memory.js +32 -0
  33. package/dist/network/discovery.d.ts +101 -0
  34. package/dist/network/discovery.js +276 -0
  35. package/dist/network/transport.d.ts +31 -0
  36. package/dist/network/transport.js +305 -0
  37. package/dist/protocol/sse.d.ts +15 -0
  38. package/dist/protocol/sse.js +90 -0
  39. package/dist/protocol/wire.d.ts +128 -0
  40. package/dist/protocol/wire.js +278 -0
  41. package/dist/provider-sqlite.d.ts +8 -0
  42. package/dist/provider-sqlite.js +30 -0
  43. package/dist/provider.d.ts +121 -0
  44. package/dist/provider.js +342 -0
  45. package/dist/sqlite-init.d.ts +2 -0
  46. package/dist/sqlite-init.js +17 -0
  47. package/dist/sqlite.d.ts +20 -0
  48. package/dist/sqlite.js +80 -0
  49. package/examples/quickstart.mjs +19 -0
  50. package/package.json +72 -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,99 @@
1
+ # Persistence and recovery
2
+
3
+ This unpublished package uses targeted storage contracts. Node 24.12.0
4
+ or later is required. The optional `@poppykit/client/sqlite` entry point exports
5
+ `SqliteAuthStore` and `SqliteConversationStore`; importing the client core does not
6
+ load SQLite. `node:sqlite` is experimental in the tested Node 24.12.0 runtime.
7
+ Consumers can implement `AuthStore` and `ConversationStore` using their own database.
8
+
9
+ ```ts
10
+ import { SqliteAuthStore, SqliteConversationStore } from '@poppykit/client/sqlite';
11
+ const authStore = new SqliteAuthStore('/private/path/auth.db');
12
+ const conversationStore = new SqliteConversationStore('/private/path/messages.db');
13
+ // Pass authStore to createClientAuth and conversationStore to createConversationClient.
14
+ // Close both after all operations have finished.
15
+ ```
16
+
17
+ Use one auth database per namespace/local user/client identity. The owner tuple is
18
+ checked on every SDK auth read and mutation. Identities are keyed by the exact full
19
+ issuer, connections by immutable IDs, operations by ID and callback state hash,
20
+ cleanup by connection, and token slots by session/resource. Secret values are never
21
+ stored in auth records: immutable references point to the host's `SecretStore`.
22
+ Session Tokens remain in process memory. Conversation databases contain message
23
+ bodies; operators must provide private storage and their required encryption.
24
+
25
+ Auth selections load an identity or a connection aggregate (its grants, operations,
26
+ and cleanup), never unrelated connections. Operation/callback selectors resolve the
27
+ owning connection through indexes. `list` explicitly enumerates connections.
28
+ Recovery pages nonterminal operation IDs in batches of 100, then rechecks each
29
+ operation and its owner inside its transaction. `cleanup(before, limit)` removes
30
+ only terminal, non-uncertain auth history; the limit is at most 1000. It does not
31
+ erase grants, pending exchanges, uncertain results, or secrets.
32
+
33
+ Transactions must serialize conflicting domain updates across processes, roll back
34
+ on exceptions, reject asynchronous mutation callbacks and non-JSON persisted values,
35
+ enforce unique keys, and return detached values. SQLite uses WAL, FULL synchronous
36
+ commits and a bounded busy wait. It serializes writers; parallel writers are not a
37
+ promise. The database file must be on a local filesystem that supports SQLite's
38
+ locking; this is not a distributed database. Create adapters before starting work,
39
+ and keep network/vault calls outside transaction callbacks.
40
+
41
+ Conversation storage has separate prepare/get/settle operations and checkpoint
42
+ compare-and-set. An outbound ID is unique per connection/full issuer, and also
43
+ records its endpoint. Retry rejects an endpoint change before sending. Preparation
44
+ returns the existing record instead of replacing content; terminal outcomes cannot
45
+ be overwritten by a late sender. Checkpoints include endpoint, conversation and
46
+ consumer. Receipts remain available for retries: the old `keepCompleted` deletion
47
+ behavior is removed. J3 owns retention with preserved message identity and bounded
48
+ payload cleanup; no automatic age-based deletion is currently performed.
49
+
50
+ `MemoryConversationStore` from `@poppykit/client/memory` is non-durable and confined
51
+ to one process. The SQLite adapters also accept `:memory:` for non-durable tests;
52
+ each such adapter has its own database. Neither mode demonstrates process recovery.
53
+
54
+ ## Operation ownership
55
+
56
+ Each active rotation or code exchange records a random execution owner, host name,
57
+ and PID before dispatch. The default liveness check keeps other running local
58
+ processes live; within this process it tracks actual active executions. Unknown
59
+ remote hosts and permission errors are conservatively live. A new invocation cannot
60
+ invalidate an active exchange just by starting recovery. A reused PID can delay
61
+ recovery; it cannot authorize replay. Multi-host adapters must supply a trustworthy
62
+ `ownerAlive` hook backed by their deployment's ownership/fencing mechanism. Returning
63
+ false is an assertion of abandonment, not a request to cancel an active operation.
64
+
65
+ Recovery rechecks phase/generation and liveness under the database transaction. A
66
+ prepared abandoned rotation can release its claim; a dispatched exchange without a
67
+ durably staged response becomes uncertain. A staged rotation can activate locally
68
+ only if its connection/grant generations still match. Cancellation, disconnection,
69
+ or another generation wins over late activation. No remote token exchange is replayed
70
+ by recovery, and lease expiry would not prove that a request was never sent.
71
+
72
+ The vault and remote issuer are outside the database transaction. Stage immutable
73
+ secrets first, conditionally activate references, then remove superseded secrets.
74
+ Vault failure or process death can leave an unreferenced staged secret; the host
75
+ needs an orphan-secret sweep that respects active owners and a grace period. A lost
76
+ remote rotation response remains uncertain. This adapter does not promise automatic
77
+ recovery of a token that the issuer rotated but never delivered.
78
+
79
+ ## Development reset
80
+
81
+ There is no automatic migration from old whole-state development stores. Stop all
82
+ users of a disposable store, resolve uncertain remote outcomes, and explicitly move
83
+ aside its database and WAL/SHM files and the matching host-managed secret namespace
84
+ before creating a fresh store. Never reset live credentials by deleting a database
85
+ behind a running process. Old stores are not silently deleted or reinterpreted.
86
+
87
+ `tests/storage.test.mts` uses actual child processes, SQLite contention and forced
88
+ termination. It covers rollback, durable prepare, live-owner recovery and an external
89
+ synthetic effect followed by owner death. The latter becomes uncertain, not an
90
+ exactly-once execution claim. Auth unit tests also retain staged-secret, generation,
91
+ revocation-race and vault-failure coverage.
92
+
93
+ ## Conversation delivery lifetime
94
+
95
+ See [DELIVERY.md](DELIVERY.md) for the retention table. Outbound records and
96
+ checkpoints are retained until explicit local deletion; pending/uncertain retries
97
+ must first be reconciled. Checkpoint acknowledgements use compare-and-set and do
98
+ not acknowledge merely because a page or stream frame was read. Local deletion
99
+ never deletes company history, revokes access or cancels accepted business work.
@@ -0,0 +1,19 @@
1
+ export declare function schemeChallenge(header: string | null, scheme: 'DPoP' | 'Bearer'): {
2
+ error?: string;
3
+ scope?: string;
4
+ extension?: string;
5
+ conversation_id?: string;
6
+ } | undefined;
7
+ export declare const dpopChallenge: (header: string | null) => {
8
+ error?: string;
9
+ scope?: string;
10
+ extension?: string;
11
+ conversation_id?: string;
12
+ } | undefined;
13
+ export interface AuthChallenge {
14
+ error?: string;
15
+ requiredScopes: string[];
16
+ extensions: string[];
17
+ conversationId?: string;
18
+ }
19
+ export declare function authChallenge(header: string | null, body?: unknown): AuthChallenge;
@@ -0,0 +1,68 @@
1
+ // Reads the DPoP challenge from a WWW-Authenticate header (RFC 9110 11.6.1, spec 6); other schemes' params are ignored.
2
+ const TOKEN = /[A-Za-z0-9!#$%&'*+.^_`|~-]+/y;
3
+ export function schemeChallenge(header, scheme) {
4
+ if (!header)
5
+ return undefined;
6
+ let i = 0;
7
+ let current;
8
+ let found;
9
+ const skip = (chars) => { while (i < header.length && chars.test(header[i]))
10
+ i++; };
11
+ while (i < header.length) {
12
+ skip(/[\s,]/);
13
+ TOKEN.lastIndex = i;
14
+ const match = TOKEN.exec(header);
15
+ if (!match) {
16
+ i++;
17
+ continue;
18
+ }
19
+ i += match[0].length;
20
+ const afterToken = i;
21
+ skip(/\s/);
22
+ if (header[i] !== '=' || !current) {
23
+ if (header[i] === '=') {
24
+ i = afterToken;
25
+ skip(/[=\s]/);
26
+ continue;
27
+ } // token68 or stray param before any scheme
28
+ current = { scheme: match[0].toLowerCase(), params: {} };
29
+ if (current.scheme === scheme.toLowerCase())
30
+ found = current.params;
31
+ i = afterToken;
32
+ continue;
33
+ }
34
+ i++;
35
+ skip(/\s/);
36
+ let value = '';
37
+ if (header[i] === '"') {
38
+ i++;
39
+ while (i < header.length && header[i] !== '"') {
40
+ if (header[i] === '\\')
41
+ i++;
42
+ value += header[i] ?? '';
43
+ i++;
44
+ }
45
+ i++;
46
+ }
47
+ else {
48
+ TOKEN.lastIndex = i;
49
+ value = TOKEN.exec(header)?.[0] ?? '';
50
+ i += value.length;
51
+ }
52
+ const name = match[0].toLowerCase();
53
+ if (current.scheme === scheme.toLowerCase() && ['error', 'scope', 'extension', 'conversation_id'].includes(name))
54
+ current.params[name] = value;
55
+ }
56
+ return found;
57
+ }
58
+ export const dpopChallenge = (header) => schemeChallenge(header, 'DPoP');
59
+ // Combine independent detail channels: a sparse JSON error must not erase header scopes/extensions.
60
+ export function authChallenge(header, body) {
61
+ const challenge = dpopChallenge(header), json = body && typeof body === 'object' && !Array.isArray(body) ? body : {};
62
+ const scopes = [challenge?.scope, json.scope].flatMap(value => typeof value === 'string' ? value.split(' ').filter(scope => /^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)) : []);
63
+ const extensions = [challenge?.extension, json.extension].filter((value) => typeof value === 'string' && value.length <= 512 && /^(?:[A-Za-z0-9._-]+|(?:[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*)$/.test(value));
64
+ const safe = new Set(['sign_in_required', 'insufficient_scope', 'invalid_token', 'invalid_dpop_proof', 'use_dpop_nonce', 'extension_required', 'rate_limited', 'invalid_scope']);
65
+ const error = [challenge?.error, json.error].find((value) => typeof value === 'string' && safe.has(value));
66
+ const conversationId = [json.conversation_id, challenge?.conversation_id].find((value) => typeof value === 'string' && value.length > 0 && value.length <= 256);
67
+ return { ...(error ? { error } : {}), requiredScopes: [...new Set(scopes)], extensions: [...new Set(extensions)], ...(conversationId ? { conversationId } : {}) };
68
+ }