@datalayer/personal-agent-protocol 0.3.0 → 0.3.2

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 (65) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +300 -19
  3. package/lib/auth.d.ts +35 -1
  4. package/lib/auth.d.ts.map +1 -1
  5. package/lib/auth.js +230 -2
  6. package/lib/auth.js.map +1 -1
  7. package/lib/browser-server.d.ts +26 -0
  8. package/lib/browser-server.d.ts.map +1 -0
  9. package/lib/browser-server.js +61 -0
  10. package/lib/browser-server.js.map +1 -0
  11. package/lib/browser.d.ts +43 -0
  12. package/lib/browser.d.ts.map +1 -0
  13. package/lib/browser.js +103 -0
  14. package/lib/browser.js.map +1 -0
  15. package/lib/chunk-TU6B5MQ7.js +4863 -0
  16. package/lib/chunk-TU6B5MQ7.js.map +7 -0
  17. package/lib/conversations.d.ts +48 -0
  18. package/lib/conversations.d.ts.map +1 -0
  19. package/lib/conversations.js +240 -0
  20. package/lib/conversations.js.map +1 -0
  21. package/lib/crypto.d.ts +1 -1
  22. package/lib/crypto.d.ts.map +1 -1
  23. package/lib/crypto.js +2 -3
  24. package/lib/crypto.js.map +1 -1
  25. package/lib/discovery.d.ts +1 -0
  26. package/lib/discovery.d.ts.map +1 -1
  27. package/lib/discovery.js +4 -3
  28. package/lib/discovery.js.map +1 -1
  29. package/lib/errors.d.ts +23 -0
  30. package/lib/errors.d.ts.map +1 -1
  31. package/lib/errors.js +24 -0
  32. package/lib/errors.js.map +1 -1
  33. package/lib/identity.d.ts +19 -0
  34. package/lib/identity.d.ts.map +1 -0
  35. package/lib/identity.js +111 -0
  36. package/lib/identity.js.map +1 -0
  37. package/lib/index.d.ts +11 -6
  38. package/lib/index.d.ts.map +1 -1
  39. package/lib/index.js +79 -3734
  40. package/lib/index.js.map +4 -4
  41. package/lib/models.d.ts +15 -0
  42. package/lib/models.d.ts.map +1 -1
  43. package/lib/models.js +53 -0
  44. package/lib/models.js.map +1 -1
  45. package/lib/reactor.d.ts +20 -1
  46. package/lib/reactor.d.ts.map +1 -1
  47. package/lib/reactor.js +7 -1
  48. package/lib/reactor.js.map +1 -1
  49. package/lib/secret.d.ts +9 -0
  50. package/lib/secret.d.ts.map +1 -0
  51. package/lib/secret.js +21 -0
  52. package/lib/secret.js.map +1 -0
  53. package/lib/server.d.ts +16 -0
  54. package/lib/server.d.ts.map +1 -0
  55. package/lib/server.js +136 -0
  56. package/lib/server.js.map +7 -0
  57. package/lib/sessions.d.ts +6 -7
  58. package/lib/sessions.d.ts.map +1 -1
  59. package/lib/sessions.js +6 -19
  60. package/lib/sessions.js.map +1 -1
  61. package/lib/transport.d.ts +24 -0
  62. package/lib/transport.d.ts.map +1 -0
  63. package/lib/transport.js +112 -0
  64. package/lib/transport.js.map +1 -0
  65. package/package.json +14 -5
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025, Datalayer
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -1,22 +1,303 @@
1
- # @datalayer/personal-agent-protocol
1
+ <!--
2
+ ~ Copyright (c) 2025-2026 Datalayer, Inc.
3
+ ~
4
+ ~ BSD 3-Clause License
5
+ -->
2
6
 
3
- TypeScript SDK for Personal Agent Protocol draft 0.1.
7
+ [![Datalayer](https://images.datalayer.io/legacy/datalayer-25.svg)](https://datalayer.ai)
8
+
9
+ # 🤖 🌸 Personal Agent Protocol
10
+
11
+ ![Personal Agent Protocol demo](https://images.datalayer.io/products/personal-agent-protocol/pap-demo.gif)
12
+
13
+ Python and TypeScript SDKs for the
14
+ [Personal Agent Protocol](https://personalagentprotocol.org), currently
15
+ targeting [draft 0.1](https://personalagentprotocol.org/docs/spec).
16
+
17
+ PAP lets a personal agent discover and talk to a company's agent on behalf of
18
+ a person. The company remains responsible for its identity, authorization,
19
+ data, and actions; the personal agent does not need the person's company
20
+ password or a copy of their account data.
21
+
22
+ ## Understand PAP in one minute
23
+
24
+ A typical interaction has five parts:
25
+
26
+ 1. **Discover** — the personal agent reads the company's public
27
+ `/.well-known/poppy.json` document.
28
+ 1. **Start a Session** — the two agents establish a pairwise, initially
29
+ signed-out relationship.
30
+ 1. **Converse** — the personal agent can ask questions without disclosing the
31
+ person's company identity.
32
+ 1. **Sign in when required** — the company opens its own authorization page.
33
+ Credentials remain between the person and the company.
34
+ 1. **Continue safely** — the same conversation resumes with proof-bound access
35
+ and the scopes the person approved.
36
+
37
+ ```mermaid
38
+ sequenceDiagram
39
+ actor Person
40
+ participant PA as Personal agent
41
+ participant Company as Company agent
42
+ participant Auth as Company authorization server
43
+
44
+ PA->>Company: Discover /.well-known/poppy.json
45
+ Company-->>PA: Agent, Session, and authorization endpoints
46
+ PA->>Auth: Start a signed-out Session
47
+ Auth-->>PA: DPoP-bound Session Token
48
+ PA->>Company: Start a conversation
49
+ Company-->>PA: Sign-in required for account data
50
+ PA-->>Person: Open the company's sign-in page
51
+ Person->>Auth: Sign in and approve scopes
52
+ Auth-->>PA: Signed-in Session
53
+ PA->>Company: Continue the same conversation
54
+ Company-->>PA: Account-aware response
55
+ ```
56
+
57
+ The SDKs implement these protocol and security mechanics. They do not decide
58
+ what an agent should say, which model it should use, or whether a proposed
59
+ action is appropriate for a person.
60
+
61
+ ## Install and discover a company
62
+
63
+ ### Python
64
+
65
+ ```bash
66
+ python -m pip install personal-agent-protocol
67
+ ```
68
+
69
+ ```python
70
+ import asyncio
71
+
72
+ from personal_agent_protocol import DiscoveryClient
73
+
74
+
75
+ async def main() -> None:
76
+ async with DiscoveryClient() as client:
77
+ company = await client.discover("example.com")
78
+ print(company.document.organization.name)
79
+ print(company.document.agent.protocols)
80
+
81
+
82
+ asyncio.run(main())
83
+ ```
84
+
85
+ ### TypeScript
86
+
87
+ ```bash
88
+ npm install @datalayer/personal-agent-protocol
89
+ ```
90
+
91
+ ```typescript
92
+ import { discoverCompany } from '@datalayer/personal-agent-protocol';
93
+
94
+ const company = await discoverCompany('example.com');
95
+ console.log(company.document.organization.name);
96
+ console.log(company.document.agent.protocols);
97
+ ```
98
+
99
+ Discovery is deliberately a small first example. Production Session,
100
+ sign-in, and conversation flows also require host-owned signing keys, DPoP
101
+ proof generation, token storage, and browser handoff. Those capabilities are
102
+ provided through Reactor rather than hidden global state.
103
+
104
+ ## Reactor is the extension foundation
105
+
106
+ Both SDKs are built on
107
+ [Datalayer Reactor](https://github.com/datalayer/reactor), a typed plugin and
108
+ contribution system. An application assembles a PAP host from explicit
109
+ contributions for:
110
+
111
+ - discovery transport and URL-safety policy;
112
+ - protocol profiles and extension handlers;
113
+ - clocks and cryptographic randomness;
114
+ - signing, protected keys, and DPoP proofs;
115
+ - pairwise identity, token storage, and caching;
116
+ - browser handoff and authorization-state storage;
117
+ - CLI commands supplied by PAP or third-party extensions.
118
+
119
+ This architecture keeps the protocol core portable while allowing a desktop
120
+ application, browser application, server, or test harness to provide the
121
+ security and platform behavior appropriate to its environment. Missing or
122
+ ambiguous security contributions fail closed.
123
+
124
+ Python accepts a host-assembled Reactor `PluginPlatform`:
125
+
126
+ ```python
127
+ from reactor import PluginManifest
128
+
129
+ from personal_agent_protocol import DiscoveryClient, build_pap_reactor
130
+
131
+
132
+ async def discover_with_host() -> None:
133
+ platform, owned_http = build_pap_reactor(
134
+ plugins=[
135
+ (PluginManifest(name="my-security", version="1"), my_security_plugin)
136
+ ]
137
+ )
138
+ try:
139
+ async with DiscoveryClient(reactor=platform) as client:
140
+ company = await client.discover("example.com")
141
+ print(company.document.organization.name)
142
+ finally:
143
+ platform.stop()
144
+ if owned_http is not None:
145
+ await owned_http.aclose()
146
+ ```
147
+
148
+ TypeScript can build a host or receive an existing `ReactorPlatformView`:
149
+
150
+ ```typescript
151
+ import {
152
+ buildPapReactor,
153
+ discoverCompany,
154
+ } from '@datalayer/personal-agent-protocol';
155
+
156
+ const reactor = buildPapReactor({}, [mySecurityPlugin]);
157
+ const company = await discoverCompany('example.com', { reactor });
158
+ ```
159
+
160
+ See the [Reactor repository](https://github.com/datalayer/reactor) for the
161
+ plugin model and contribution lifecycle.
162
+
163
+ ## What is implemented
164
+
165
+ Python and TypeScript share fixtures and cover the same principal protocol
166
+ surface:
167
+
168
+ | Area | Current support |
169
+ | --------------- | --------------------------------------------------------------------------------------------------------------------------------- |
170
+ | Models | Forward-compatible discovery, OAuth metadata, client metadata, messages, events, and Operations v1 models |
171
+ | Discovery | `poppy.json`, company-domain and issuer validation, `poppy_domains`, bounded responses, and redirect controls |
172
+ | Sessions | Pairwise user IDs, PAP 4.2 assertions, signed-out start and renewal, redacted tokens, and DPoP nonce retry |
173
+ | Cryptography | ES256/RS256 JOSE, RFC 9449 DPoP creation and verification, freshness, token hash, nonce, and replay checks |
174
+ | Direct Sign-In | State, S256 PKCE, callback validation, browser handoff, and one-use token exchange orchestration |
175
+ | Browser Session | Assertion creation, controlled form POST, replay-safe verification, and company-domain return validation |
176
+ | Transport | Host-owned authorization headers, fresh redirect-bound proofs, one nonce retry, and cross-origin credential isolation |
177
+ | Conversations | Start and continue, random user-global message IDs, ordered long polling, cursor recovery, duplicate suppression, and safe errors |
178
+ | Extensibility | Reactor contribution points for platform, security, protocol, storage, and CLI behavior |
179
+ | CLI | Non-secret profiles, company verification, extension diagnostics, and versioned JSON output |
180
+
181
+ Python additionally provides a SQLite pairwise-identity reference provider.
182
+ The portable TypeScript entry rejects obvious local and private literal
183
+ addresses. Its explicit server entry adds `NodeUrlSafetyPolicy`, `JoseSigner`,
184
+ and DPoP verification with A/AAAA resolution checks:
4
185
 
5
186
  ```typescript
6
- import { discoverCompany } from "@datalayer/personal-agent-protocol";
7
-
8
- const company = await discoverCompany("example.com");
9
- ```
10
-
11
- The current release provides forward-compatible discovery, OAuth, client
12
- metadata, conversation-event, and Operations v1 models; strict
13
- duplicate-member rejection; local Direct Sign-In primitives; signed-out
14
- session start and renewal; and safe company discovery built on Datalayer
15
- Reactor. It also provides ES256/RS256 JOSE signing and DPoP proof creation and
16
- verification through the maintained `jose` package. Use
17
- `buildPapReactor(options, plugins)` to install typed transport, URL-policy,
18
- protocol-profile, clock, randomness, signer, DPoP proof, protected-key,
19
- token-store, cache, browser-handoff, and extension-handler contributions, or
20
- pass an existing Reactor host to `discoverCompany`. Security-dependent
21
- services have no fallback and fail closed when missing or ambiguous. See the
22
- repository README for current limits and security guidance.
187
+ import {
188
+ JoseSigner,
189
+ NodeUrlSafetyPolicy,
190
+ } from '@datalayer/personal-agent-protocol/server';
191
+ ```
192
+
193
+ Duplicate JSON members are rejected at Python and TypeScript trust boundaries.
194
+
195
+ ## What is not implemented yet
196
+
197
+ The following work remains before the SDKs cover the complete planned PAP
198
+ surface:
199
+
200
+ - managed key custody and durable replay storage;
201
+ - Account Token exchange;
202
+ - resource-bound MCP Bearer transport;
203
+ - device and mediated sign-in;
204
+ - conversation SSE streaming, handoff, and explicit close requests;
205
+ - the Operations client and server;
206
+ - transport-level address pinning to close the DNS-rebinding interval.
207
+
208
+ Models for Operations v1 exist, but a complete Operations workflow does not.
209
+ Applications should not infer client support from model availability alone.
210
+
211
+ ## Security boundary
212
+
213
+ PAP credentials belong to the host, not to an agent model or tool call.
214
+
215
+ - Session Tokens, Account Tokens, proofs, nonces, authorization codes, and
216
+ pairwise identifiers must not enter prompts or model-visible tool results.
217
+ - Direct Sign-In requires host-provided state storage, browser handoff,
218
+ signing, and DPoP contributions.
219
+ - Session requests fail closed until suitable signer and DPoP providers are
220
+ installed.
221
+ - Redirects receive fresh, target-bound proofs; caller-supplied
222
+ `Authorization` and `DPoP` headers are rejected.
223
+ - CLI profiles store configuration only—never tokens or private keys.
224
+
225
+ The guides explain these boundaries at each flow rather than treating them as
226
+ application conventions.
227
+
228
+ ## Command line
229
+
230
+ The Python distribution installs an extensible `pap` command. Its command
231
+ groups use Reactor's `datalayer.reactor.cli` entry-point contract.
232
+
233
+ ```bash
234
+ pap config set domain example.com --profile work
235
+ pap config show --profile work --json
236
+ pap company verify --profile work --json
237
+ pap extensions list --profile work
238
+ ```
239
+
240
+ See the [CLI guide](https://personalagentprotocol.org/docs/guides/cli) for
241
+ configuration precedence, security boundaries, machine-readable output, and
242
+ third-party extensions.
243
+
244
+ ## Examples and first consumer
245
+
246
+ The [`examples/pydantic-ai`](./examples/pydantic-ai/) directory contains small,
247
+ focused Python examples. Each example teaches one feature—discovery, identity,
248
+ Sessions, Direct Sign-In, Browser Session, cryptography, conversations, or
249
+ proof-bound transport—without giving protocol secrets to the model.
250
+
251
+ [Datalayer Agent Runtimes](https://github.com/datalayer/agent-runtimes) is the
252
+ first consumer of these SDKs. Its Personal Agent Protocol gallery turns the
253
+ same features into interactive Reactor/Loop applications, including a guided
254
+ person-to-company journey rendered with the runtime's real message UI. The
255
+ gallery's local company simulators are deterministic and clearly identified;
256
+ the PAP clients, transports, validation, and parsed exchanges are real.
257
+
258
+ Each implemented feature should have all three learning layers:
259
+
260
+ 1. a focused SDK example in this repository;
261
+ 1. a Docusaurus guide with an explanation and Mermaid sequence diagram;
262
+ 1. an interactive Agent Runtimes example when a UI materially clarifies the
263
+ flow.
264
+
265
+ ## Develop the SDKs
266
+
267
+ Clone the repository, then install the dependencies for the SDK you are
268
+ changing.
269
+
270
+ ### Python
271
+
272
+ ```bash
273
+ python -m pip install -e '.[test,lint,typing,examples]'
274
+ python -m pytest personal_agent_protocol/__tests__
275
+ ```
276
+
277
+ ### TypeScript
278
+
279
+ The TypeScript package is at the repository root. Sources and tests live under
280
+ `src/`; published JavaScript and declarations are emitted to `lib/`.
281
+
282
+ ```bash
283
+ npm install
284
+ npm test
285
+ ```
286
+
287
+ The shared fixtures are in `fixtures/`. Documentation is a Docusaurus site in
288
+ `docs/`.
289
+
290
+ ## Learn more
291
+
292
+ - [Documentation](https://personalagentprotocol.org/docs)
293
+ - [Overview](https://personalagentprotocol.org/docs/overview)
294
+ - [Specification](https://personalagentprotocol.org/docs/spec)
295
+ - [Implementation guides](https://personalagentprotocol.org/docs/guides)
296
+ - [Extensions](https://personalagentprotocol.org/docs/extensions)
297
+ - [Open topics](https://personalagentprotocol.org/docs/open-topics)
298
+ - [Datalayer Reactor](https://github.com/datalayer/reactor)
299
+ - [Datalayer Agent Runtimes](https://github.com/datalayer/agent-runtimes)
300
+
301
+ ## License
302
+
303
+ BSD 3-Clause. See [`LICENSE`](./LICENSE).
package/lib/auth.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { SecretValue, type RandomBytes } from "./sessions.js";
1
+ import type { ReactorPlatformView } from "@datalayer/reactor";
2
+ import { SecretValue } from "./secret.js";
3
+ import { type RandomBytes, type SessionToken } from "./sessions.js";
2
4
  export interface DirectSignInOptions {
3
5
  readonly authorizationEndpoint: string;
4
6
  readonly issuer: string;
@@ -41,4 +43,36 @@ export declare class AuthorizationCodeGrant {
41
43
  toForm(): URLSearchParams;
42
44
  toJSON(): Record<string, unknown>;
43
45
  }
46
+ export interface DirectSignInStart {
47
+ readonly expiresAt: Date;
48
+ readonly requestedScopes: readonly string[];
49
+ }
50
+ export interface DirectSignInResult {
51
+ readonly token: SessionToken;
52
+ readonly grantedScopes: readonly string[];
53
+ readonly missingScopes: readonly string[];
54
+ }
55
+ export interface DirectSignInBeginOptions extends DirectSignInOptions {
56
+ readonly tokenEndpoint: string;
57
+ readonly tenantId: string;
58
+ readonly pairwiseUserId: string;
59
+ readonly sessionId: string;
60
+ readonly lifetimeSeconds?: number;
61
+ }
62
+ export interface DirectSignInCompleteOptions {
63
+ readonly callbackUrl: string;
64
+ readonly signal?: AbortSignal;
65
+ }
66
+ export interface DirectSignInClientOptions {
67
+ readonly reactor: ReactorPlatformView;
68
+ readonly fetch?: typeof globalThis.fetch;
69
+ readonly maxResponseBytes?: number;
70
+ }
71
+ /** Orchestrate one-use browser authorization and proof-bound code exchange. */
72
+ export declare class DirectSignInClient {
73
+ #private;
74
+ constructor(options: DirectSignInClientOptions);
75
+ begin(options: DirectSignInBeginOptions): Promise<DirectSignInStart>;
76
+ complete(options: DirectSignInCompleteOptions): Promise<DirectSignInResult>;
77
+ }
44
78
  //# sourceMappingURL=auth.d.ts.map
package/lib/auth.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAwB,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AA6DpF,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,qBAAqB,EAAE,MAAM,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;CACpC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,gBAAgB,EAAE,WAAW,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC;CACpC;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,mBAAmB,CAAC,CA6B9B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAWD,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,mBAAmB,EAC5B,WAAW,EAAE,MAAM,GAClB,oBAAoB,CAuCtB;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED,qBAAa,sBAAsB;IACjC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,eAAe,EAAE,WAAW,CAAC;gBAE1B,OAAO,EAAE,6BAA6B;IAYlD,MAAM,IAAI,eAAe;IAazB,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAYlC"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
package/lib/auth.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { AuthorizationError } from "./errors.js";
2
- import { JWT_CLIENT_ASSERTION, SecretValue } from "./sessions.js";
2
+ import { parseJsonStrict } from "./json.js";
3
+ import { SecretValue } from "./secret.js";
4
+ import { JWT_CLIENT_ASSERTION, parseSessionToken, } from "./sessions.js";
5
+ import { AuthorizationStateStores, BrowserHandoffs, Clocks, DpopProofProviders, RandomSources, Signers, firstContribution, } from "./reactor.js";
3
6
  function defaultRandomBytes(length) {
4
7
  if (!globalThis.crypto) {
5
8
  throw new AuthorizationError("crypto_unavailable", "Web Crypto is required");
@@ -56,7 +59,11 @@ export async function buildDirectSignInRequest(options) {
56
59
  const randomBytes = options.randomBytes ?? defaultRandomBytes;
57
60
  const state = base64Url(entropy(16, randomBytes));
58
61
  const verifier = base64Url(entropy(32, randomBytes));
59
- const challenge = base64Url(new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
62
+ const cryptoApi = globalThis.crypto;
63
+ if (!cryptoApi) {
64
+ throw new AuthorizationError("crypto_unavailable", "Web Crypto is required");
65
+ }
66
+ const challenge = base64Url(new Uint8Array(await cryptoApi.subtle.digest("SHA-256", new TextEncoder().encode(verifier))));
60
67
  const endpoint = httpsUrl(options.authorizationEndpoint, "authorizationEndpoint");
61
68
  const scopes = validScopes(options.scopes);
62
69
  const clientId = httpsUrl(options.clientId, "clientId").toString();
@@ -165,4 +172,225 @@ export class AuthorizationCodeGrant {
165
172
  };
166
173
  }
167
174
  }
175
+ async function stateReference(state) {
176
+ const bytes = new TextEncoder().encode(state);
177
+ if ([...bytes].some((byte) => byte > 0x7f)) {
178
+ throw new AuthorizationError("invalid_callback", "Authorization state is invalid");
179
+ }
180
+ const cryptoApi = globalThis.crypto;
181
+ if (!cryptoApi) {
182
+ throw new AuthorizationError("crypto_unavailable", "Web Crypto is required");
183
+ }
184
+ const digest = await cryptoApi.subtle.digest("SHA-256", bytes);
185
+ return `direct:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
186
+ }
187
+ function callbackState(callbackUrl) {
188
+ const callback = httpsUrl(callbackUrl, "callbackUrl");
189
+ if (callback.hash) {
190
+ throw new AuthorizationError("invalid_callback", "Authorization callback has a fragment");
191
+ }
192
+ const states = callback.searchParams.getAll("state");
193
+ if (states.length !== 1 || !states[0]) {
194
+ throw new AuthorizationError("invalid_callback", "Authorization callback must contain one state value");
195
+ }
196
+ return states[0];
197
+ }
198
+ function isPendingDirectSignIn(value) {
199
+ if (typeof value !== "object" || value === null)
200
+ return false;
201
+ const pending = value;
202
+ return (pending.kind === "pap.direct-sign-in.v1" &&
203
+ typeof pending.tokenEndpoint === "string" &&
204
+ typeof pending.tenantId === "string" &&
205
+ typeof pending.pairwiseUserId === "string" &&
206
+ typeof pending.sessionId === "string" &&
207
+ pending.expiresAt instanceof Date &&
208
+ typeof pending.request === "object" &&
209
+ pending.request !== null);
210
+ }
211
+ /** Orchestrate one-use browser authorization and proof-bound code exchange. */
212
+ export class DirectSignInClient {
213
+ #reactor;
214
+ #fetch;
215
+ #maxResponseBytes;
216
+ constructor(options) {
217
+ const fetcher = options.fetch ?? globalThis.fetch;
218
+ if (!fetcher) {
219
+ throw new AuthorizationError("fetch_unavailable", "A fetch implementation is required");
220
+ }
221
+ const maxResponseBytes = options.maxResponseBytes ?? 100 * 1024;
222
+ if (!Number.isInteger(maxResponseBytes) || maxResponseBytes <= 0) {
223
+ throw new RangeError("maxResponseBytes must be a positive integer");
224
+ }
225
+ this.#reactor = options.reactor;
226
+ this.#fetch = fetcher;
227
+ this.#maxResponseBytes = maxResponseBytes;
228
+ }
229
+ async begin(options) {
230
+ const lifetimeSeconds = options.lifetimeSeconds ?? 600;
231
+ if (!Number.isInteger(lifetimeSeconds) || lifetimeSeconds < 1 || lifetimeSeconds > 900) {
232
+ throw new RangeError("Direct Sign-In state lifetime must be between 1 and 900 seconds");
233
+ }
234
+ if (!options.tenantId || options.tenantId.length > 256) {
235
+ throw new RangeError("tenantId must contain between 1 and 256 characters");
236
+ }
237
+ if (!/^[A-Za-z0-9_-]{1,256}$/.test(options.pairwiseUserId)) {
238
+ throw new AuthorizationError("invalid_user_id", "Pairwise user ID must be an opaque URL-safe identifier");
239
+ }
240
+ if (!/^[A-Za-z0-9_-]{1,256}$/.test(options.sessionId)) {
241
+ throw new AuthorizationError("invalid_session_id", "Invalid PAP session ID");
242
+ }
243
+ const clock = firstContribution(this.#reactor, Clocks, "clock");
244
+ const random = firstContribution(this.#reactor, RandomSources, "random source");
245
+ const store = firstContribution(this.#reactor, AuthorizationStateStores, "authorization state store");
246
+ const browser = firstContribution(this.#reactor, BrowserHandoffs, "browser handoff");
247
+ const request = await buildDirectSignInRequest({
248
+ authorizationEndpoint: options.authorizationEndpoint,
249
+ issuer: options.issuer,
250
+ clientId: options.clientId,
251
+ redirectUri: options.redirectUri,
252
+ scopes: options.scopes,
253
+ randomBytes: (length) => random.bytes(length),
254
+ });
255
+ const tokenEndpoint = httpsUrl(options.tokenEndpoint, "tokenEndpoint").toString();
256
+ const expiresAt = new Date(clock.now().getTime() + lifetimeSeconds * 1000);
257
+ const reference = await stateReference(request.state.reveal());
258
+ const pending = {
259
+ kind: "pap.direct-sign-in.v1",
260
+ request,
261
+ tokenEndpoint,
262
+ tenantId: options.tenantId,
263
+ pairwiseUserId: options.pairwiseUserId,
264
+ sessionId: options.sessionId,
265
+ expiresAt,
266
+ };
267
+ await store.put(reference, pending, { expiresAt });
268
+ try {
269
+ await browser.open(request.authorizationUrl);
270
+ }
271
+ catch (cause) {
272
+ await store.take(reference);
273
+ throw new AuthorizationError("browser_handoff_failed", "Browser handoff failed", { cause });
274
+ }
275
+ return { expiresAt, requestedScopes: request.scopes };
276
+ }
277
+ async complete(options) {
278
+ const state = callbackState(options.callbackUrl);
279
+ const store = firstContribution(this.#reactor, AuthorizationStateStores, "authorization state store");
280
+ const pending = await store.take(await stateReference(state));
281
+ if (!isPendingDirectSignIn(pending)) {
282
+ throw new AuthorizationError("invalid_or_expired_state", "Authorization state is unknown, expired, or already used");
283
+ }
284
+ const clock = firstContribution(this.#reactor, Clocks, "clock");
285
+ if (clock.now().getTime() > pending.expiresAt.getTime()) {
286
+ throw new AuthorizationError("invalid_or_expired_state", "Authorization state has expired");
287
+ }
288
+ const callback = validateDirectSignInCallback(pending.request, options.callbackUrl);
289
+ return this.#exchange(pending, callback, options.signal);
290
+ }
291
+ async #exchange(pending, callback, signal) {
292
+ const clock = firstContribution(this.#reactor, Clocks, "clock");
293
+ const random = firstContribution(this.#reactor, RandomSources, "random source");
294
+ const signer = firstContribution(this.#reactor, Signers, "signer");
295
+ const dpop = firstContribution(this.#reactor, DpopProofProviders, "DPoP proof provider");
296
+ let nonce;
297
+ for (let attempt = 0; attempt < 2; attempt += 1) {
298
+ const iat = Math.floor(clock.now().getTime() / 1000);
299
+ const clientAssertion = await signer.sign({
300
+ iss: pending.request.clientId,
301
+ sub: pending.request.clientId,
302
+ aud: pending.tokenEndpoint,
303
+ iat,
304
+ exp: iat + 60,
305
+ jti: `jti_${base64Url(entropy(16, (length) => random.bytes(length)))}`,
306
+ }, { purpose: "client_assertion" });
307
+ const proof = await dpop.create({
308
+ method: "POST",
309
+ url: pending.tokenEndpoint,
310
+ tenantId: pending.tenantId,
311
+ subject: pending.pairwiseUserId,
312
+ issuer: pending.request.issuer,
313
+ ...(nonce === undefined ? {} : { nonce }),
314
+ });
315
+ const grant = new AuthorizationCodeGrant({
316
+ code: callback.code.reveal(),
317
+ redirectUri: pending.request.redirectUri,
318
+ codeVerifier: pending.request.codeVerifier.reveal(),
319
+ sessionId: pending.sessionId,
320
+ clientId: pending.request.clientId,
321
+ clientAssertion: clientAssertion.reveal(),
322
+ });
323
+ let response;
324
+ try {
325
+ response = await this.#fetch(pending.tokenEndpoint, {
326
+ method: "POST",
327
+ headers: { Accept: "application/json", DPoP: proof.reveal() },
328
+ body: grant.toForm(),
329
+ credentials: "omit",
330
+ redirect: "error",
331
+ ...(signal === undefined ? {} : { signal }),
332
+ });
333
+ }
334
+ catch (cause) {
335
+ throw new AuthorizationError("request_failed", "Direct Sign-In token exchange failed", { cause });
336
+ }
337
+ const payload = await this.#decode(response);
338
+ if (response.ok)
339
+ return this.#result(response, payload, pending);
340
+ const source = typeof payload === "object" && payload !== null ? payload : {};
341
+ const remoteCode = "error" in source ? source.error : undefined;
342
+ const code = typeof remoteCode === "string" ? remoteCode : "http_error";
343
+ if (code === "use_dpop_nonce" && attempt === 0) {
344
+ nonce = response.headers.get("dpop-nonce") ?? undefined;
345
+ if (!nonce) {
346
+ throw new AuthorizationError("missing_dpop_nonce", "Company requested a DPoP nonce but did not send one", { statusCode: response.status });
347
+ }
348
+ continue;
349
+ }
350
+ throw new AuthorizationError(code, "Company refused the Direct Sign-In token exchange", {
351
+ statusCode: response.status,
352
+ });
353
+ }
354
+ throw new AuthorizationError("use_dpop_nonce", "Company rejected the refreshed DPoP proof");
355
+ }
356
+ async #decode(response) {
357
+ const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim();
358
+ if (contentType !== "application/json") {
359
+ throw new AuthorizationError("invalid_content_type", "Direct Sign-In token endpoint must return JSON", { statusCode: response.status });
360
+ }
361
+ const body = await response.text();
362
+ if (new TextEncoder().encode(body).byteLength > this.#maxResponseBytes) {
363
+ throw new AuthorizationError("response_too_large", "Direct Sign-In response exceeds size limit", { statusCode: response.status });
364
+ }
365
+ try {
366
+ return parseJsonStrict(body);
367
+ }
368
+ catch (cause) {
369
+ throw new AuthorizationError("invalid_json", "Direct Sign-In token endpoint returned invalid JSON", { statusCode: response.status, cause });
370
+ }
371
+ }
372
+ #result(response, payload, pending) {
373
+ let token;
374
+ try {
375
+ token = parseSessionToken(payload);
376
+ }
377
+ catch (cause) {
378
+ throw new AuthorizationError("invalid_token_response", "Direct Sign-In token response is malformed", { statusCode: response.status, cause });
379
+ }
380
+ if (token.tokenType !== "DPoP" || !token.signedIn) {
381
+ throw new AuthorizationError("invalid_token_response", "Direct Sign-In must return a signed-in DPoP Session Token", { statusCode: response.status });
382
+ }
383
+ if (token.sessionId !== pending.sessionId) {
384
+ throw new AuthorizationError("session_mismatch", "Direct Sign-In response changed the existing Session", { statusCode: response.status });
385
+ }
386
+ if (token.scopes.some((scope) => !pending.request.scopes.includes(scope))) {
387
+ throw new AuthorizationError("scope_escalation", "Company returned a scope that was not requested", { statusCode: response.status });
388
+ }
389
+ return {
390
+ token,
391
+ grantedScopes: token.scopes,
392
+ missingScopes: pending.request.scopes.filter((scope) => !token.scopes.includes(scope)),
393
+ };
394
+ }
395
+ }
168
396
  //# sourceMappingURL=auth.js.map