@happyview/oauth-client 1.0.0-dev.2 → 1.0.1

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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Lexicon Community
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # @happyview/oauth-client
2
+
3
+ Core OAuth client for authenticating with a [HappyView](https://github.com/gamesgamesgamesgamesgames/happyview) instance.
4
+
5
+ This is a platform-agnostic package. If you're building a browser app, use [`@happyview/oauth-client-browser`](https://www.npmjs.com/package/@happyview/oauth-client-browser) instead. It wraps this package with Web Crypto, localStorage, and a complete OAuth redirect flow.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @happyview/oauth-client
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ `HappyViewOAuthClient` manages DPoP key provisioning, session registration, and session restoration lifecycle. You provide a `CryptoAdapter` and optional `StorageAdapter` for your platform.
16
+
17
+ ```typescript
18
+ import { HappyViewOAuthClient } from "@happyview/oauth-client";
19
+
20
+ const client = new HappyViewOAuthClient({
21
+ instanceUrl: "https://happyview.example.com",
22
+ clientKey: "hvc_your_client_key",
23
+ clientSecret: "hvs_your_secret", // optional, for confidential clients (server-to-server)
24
+ crypto: myCryptoAdapter,
25
+ storage: myStorageAdapter, // optional, defaults to in-memory
26
+ });
27
+ ```
28
+
29
+ ### DPoP Key Provisioning
30
+
31
+ Request a DPoP keypair from the HappyView instance:
32
+
33
+ ```typescript
34
+ const { provisionId, dpopKey, pkceVerifier } = await client.provisionDpopKey();
35
+ ```
36
+
37
+ ### Session Registration
38
+
39
+ After completing OAuth authorization with the user's PDS, register the session with HappyView:
40
+
41
+ ```typescript
42
+ const session = await client.registerSession({
43
+ provisionId,
44
+ pkceVerifier,
45
+ did: "did:plc:abc123",
46
+ accessToken: tokens.access_token,
47
+ refreshToken: tokens.refresh_token,
48
+ scopes: "atproto",
49
+ pdsUrl: "https://pds.example.com",
50
+ issuer: tokens.iss,
51
+ dpopKey,
52
+ });
53
+ ```
54
+
55
+ ### Making Authenticated Requests
56
+
57
+ The returned `HappyViewSession` provides a `fetchHandler` that automatically attaches DPoP proof headers:
58
+
59
+ ```typescript
60
+ const response = await session.fetchHandler("/xrpc/com.example.getStuff", {
61
+ method: "GET",
62
+ });
63
+ ```
64
+
65
+ ### Session Restoration
66
+
67
+ Restore a previously stored session:
68
+
69
+ ```typescript
70
+ // Restore the last active session
71
+ const session = await client.restore();
72
+
73
+ // Or restore a specific user's session
74
+ const session = await client.restoreSession("did:plc:abc123");
75
+ ```
76
+
77
+ ### Logout
78
+
79
+ ```typescript
80
+ await client.deleteSession("did:plc:abc123");
81
+ ```
82
+
83
+ ## Adapters
84
+
85
+ ### CryptoAdapter
86
+
87
+ Implement this interface for your platform's cryptographic primitives:
88
+
89
+ ```typescript
90
+ interface CryptoAdapter {
91
+ generatePkceVerifier(): Promise<string>;
92
+ computePkceChallenge(verifier: string): Promise<string>;
93
+ signEs256(privateKey: JsonWebKey, payload: Uint8Array): Promise<Uint8Array>;
94
+ sha256(data: Uint8Array): Promise<Uint8Array>;
95
+ getRandomValues(length: number): Uint8Array;
96
+ }
97
+ ```
98
+
99
+ ### StorageAdapter
100
+
101
+ Implement this interface to persist sessions across restarts:
102
+
103
+ ```typescript
104
+ interface StorageAdapter {
105
+ get(key: string): Promise<string | null>;
106
+ set(key: string, value: string): Promise<void>;
107
+ delete(key: string): Promise<void>;
108
+ }
109
+ ```
110
+
111
+ If no `StorageAdapter` is provided, sessions are stored in memory and will not survive page reloads or process restarts.
package/dist/index.cjs CHANGED
@@ -29,8 +29,7 @@ __export(index_exports, {
29
29
  MemoryStorage: () => MemoryStorage,
30
30
  ResolutionError: () => ResolutionError,
31
31
  TokenExchangeError: () => TokenExchangeError,
32
- base64urlEncode: () => base64urlEncode,
33
- generateDpopProof: () => generateDpopProof
32
+ importJwk: () => importJwk
34
33
  });
35
34
  module.exports = __toCommonJS(index_exports);
36
35
 
@@ -82,63 +81,46 @@ var ResolutionError = class extends HappyViewError {
82
81
  }
83
82
  };
84
83
 
85
- // src/dpop-proof.ts
86
- function base64urlEncode(data) {
87
- let binary = "";
88
- for (let i = 0; i < data.length; i++) {
89
- binary += String.fromCharCode(data[i]);
90
- }
91
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
92
- }
93
- async function generateDpopProof(crypto, params) {
94
- const { privateKey, method, url, accessToken, nonce } = params;
95
- const publicJwk = {
96
- kty: privateKey.kty,
97
- crv: privateKey.crv,
98
- x: privateKey.x,
99
- y: privateKey.y
100
- };
101
- const header = {
102
- alg: "ES256",
103
- typ: "dpop+jwt",
104
- jwk: publicJwk
105
- };
106
- const jtiBytes = crypto.getRandomValues(16);
107
- const jti = Array.from(jtiBytes, (b) => b.toString(16).padStart(2, "0")).join("");
108
- const payload = {
109
- htm: method,
110
- htu: url,
111
- iat: Math.floor(Date.now() / 1e3),
112
- jti
113
- };
114
- if (accessToken) {
115
- const tokenBytes = new TextEncoder().encode(accessToken);
116
- const hashBuf = await crypto.sha256(tokenBytes);
117
- payload.ath = base64urlEncode(hashBuf);
118
- }
119
- if (nonce !== void 0) {
120
- payload.nonce = nonce;
121
- }
122
- const headerB64 = base64urlEncode(
123
- new TextEncoder().encode(JSON.stringify(header))
84
+ // src/import-jwk.ts
85
+ var import_jwk_webcrypto = require("@atproto/jwk-webcrypto");
86
+ async function importJwk(jwk) {
87
+ const privateKey = await crypto.subtle.importKey(
88
+ "jwk",
89
+ jwk,
90
+ { name: "ECDSA", namedCurve: "P-256" },
91
+ true,
92
+ ["sign"]
124
93
  );
125
- const payloadB64 = base64urlEncode(
126
- new TextEncoder().encode(JSON.stringify(payload))
94
+ const { d: _, ...publicJwkFields } = jwk;
95
+ const publicKey = await crypto.subtle.importKey(
96
+ "jwk",
97
+ publicJwkFields,
98
+ { name: "ECDSA", namedCurve: "P-256" },
99
+ true,
100
+ ["verify"]
127
101
  );
128
- const message = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
129
- const signature = await crypto.signEs256(privateKey, message);
130
- const sigB64 = base64urlEncode(signature);
131
- return `${headerB64}.${payloadB64}.${sigB64}`;
102
+ return import_jwk_webcrypto.WebcryptoKey.fromKeypair({ privateKey, publicKey });
132
103
  }
133
104
 
134
105
  // src/session.ts
106
+ function randomHex(byteLength) {
107
+ const bytes = crypto.getRandomValues(new Uint8Array(byteLength));
108
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
109
+ }
110
+ function base64url(buffer) {
111
+ const bytes = new Uint8Array(buffer);
112
+ let binary = "";
113
+ for (let i = 0; i < bytes.length; i++) {
114
+ binary += String.fromCharCode(bytes[i]);
115
+ }
116
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
117
+ }
135
118
  var HappyViewSession = class {
136
119
  did;
137
120
  dpopKey;
138
121
  accessToken;
139
122
  clientKey;
140
123
  instanceUrl;
141
- crypto;
142
124
  _fetch;
143
125
  dpopNonce;
144
126
  constructor(options) {
@@ -147,29 +129,61 @@ var HappyViewSession = class {
147
129
  this.accessToken = options.accessToken;
148
130
  this.clientKey = options.clientKey;
149
131
  this.instanceUrl = options.instanceUrl.replace(/\/+$/, "");
150
- this.crypto = options.crypto;
151
- this._fetch = options.fetch ?? globalThis.fetch;
132
+ this._fetch = options.fetch ?? ((input, init) => fetch(input, init));
152
133
  }
153
134
  async fetchHandler(url, init) {
154
135
  const fullUrl = /^https?:\/\//i.test(url) ? url : `${this.instanceUrl}${url}`;
155
136
  const method = (init.method ?? "GET").toUpperCase();
156
- const proof = await generateDpopProof(this.crypto, {
157
- privateKey: this.dpopKey,
158
- method,
159
- url: fullUrl,
160
- accessToken: this.accessToken,
161
- nonce: this.dpopNonce
162
- });
163
- const headers = new Headers(init.headers);
164
- headers.set("authorization", `DPoP ${this.accessToken}`);
165
- headers.set("dpop", proof);
166
- headers.set("x-client-key", this.clientKey);
167
- const response = await this._fetch(fullUrl, {
168
- ...init,
169
- headers
170
- });
137
+ const htu = fullUrl.split("?")[0];
138
+ const sendWithProof = async () => {
139
+ const tokenHash = await crypto.subtle.digest(
140
+ "SHA-256",
141
+ new TextEncoder().encode(this.accessToken)
142
+ );
143
+ const ath = base64url(tokenHash);
144
+ const proof = await this.dpopKey.createJwt(
145
+ {
146
+ alg: "ES256",
147
+ typ: "dpop+jwt",
148
+ jwk: this.dpopKey.publicJwk
149
+ },
150
+ {
151
+ htm: method,
152
+ htu,
153
+ iat: Math.floor(Date.now() / 1e3),
154
+ jti: randomHex(16),
155
+ ath,
156
+ nonce: this.dpopNonce
157
+ }
158
+ );
159
+ const headers = new Headers(init.headers);
160
+ headers.set("authorization", `DPoP ${this.accessToken}`);
161
+ headers.set("dpop", proof);
162
+ headers.set("x-client-key", this.clientKey);
163
+ return this._fetch(fullUrl, { ...init, headers });
164
+ };
165
+ let response;
166
+ try {
167
+ response = await sendWithProof();
168
+ } catch (e) {
169
+ const info = `_fetch type: ${typeof this._fetch}, toString: ${String(this._fetch).slice(0, 120)}, url: ${fullUrl}`;
170
+ throw new Error(`fetchHandler failed: ${e.message}
171
+
172
+ DEBUG: ${info}`);
173
+ }
171
174
  const nonce = response.headers.get("dpop-nonce");
172
- if (nonce) {
175
+ if (nonce && nonce !== this.dpopNonce && (response.status === 401 || response.status === 400)) {
176
+ this.dpopNonce = nonce;
177
+ try {
178
+ response = await sendWithProof();
179
+ } catch (e) {
180
+ throw new Error(`fetchHandler retry failed: ${e.message}`);
181
+ }
182
+ const retryNonce = response.headers.get("dpop-nonce");
183
+ if (retryNonce) {
184
+ this.dpopNonce = retryNonce;
185
+ }
186
+ } else if (nonce) {
173
187
  this.dpopNonce = nonce;
174
188
  }
175
189
  return response;
@@ -196,7 +210,6 @@ var LAST_ACTIVE_KEY = "happyview:last-active-did";
196
210
  var HappyViewOAuthClient = class {
197
211
  instanceUrl;
198
212
  clientKey;
199
- crypto;
200
213
  storage;
201
214
  clientSecret;
202
215
  _fetch;
@@ -204,9 +217,8 @@ var HappyViewOAuthClient = class {
204
217
  this.instanceUrl = options.instanceUrl.replace(/\/+$/, "");
205
218
  this.clientKey = options.clientKey;
206
219
  this.clientSecret = options.clientSecret;
207
- this.crypto = options.crypto;
208
220
  this.storage = options.storage ?? new MemoryStorage();
209
- this._fetch = options.fetch ?? globalThis.fetch;
221
+ this._fetch = options.fetch ?? ((input, init) => fetch(input, init));
210
222
  }
211
223
  get isConfidential() {
212
224
  return this.clientSecret !== void 0;
@@ -222,8 +234,8 @@ var HappyViewOAuthClient = class {
222
234
  const body = {};
223
235
  let pkceVerifier;
224
236
  if (!this.isConfidential) {
225
- pkceVerifier = await this.crypto.generatePkceVerifier();
226
- const challenge = await this.crypto.computePkceChallenge(pkceVerifier);
237
+ pkceVerifier = generatePkceVerifier();
238
+ const challenge = await computePkceChallenge(pkceVerifier);
227
239
  body.pkce_challenge = challenge;
228
240
  }
229
241
  const resp = await this._fetch(`${this.instanceUrl}/oauth/dpop-keys`, {
@@ -240,9 +252,11 @@ var HappyViewOAuthClient = class {
240
252
  );
241
253
  }
242
254
  const data = await resp.json();
255
+ const dpopKey = await importJwk(data.dpop_key);
243
256
  return {
244
257
  provisionId: data.provision_id,
245
- dpopKey: data.dpop_key,
258
+ dpopKey,
259
+ rawJwk: data.dpop_key,
246
260
  pkceVerifier
247
261
  };
248
262
  }
@@ -280,6 +294,7 @@ var HappyViewOAuthClient = class {
280
294
  );
281
295
  }
282
296
  const data = await resp.json();
297
+ const dpopKey = await importJwk(params.dpopKey);
283
298
  const storedSession = {
284
299
  did: data.did,
285
300
  dpopKey: params.dpopKey,
@@ -294,34 +309,28 @@ var HappyViewOAuthClient = class {
294
309
  await this.storage.set(LAST_ACTIVE_KEY, data.did);
295
310
  return new HappyViewSession({
296
311
  did: data.did,
297
- dpopKey: params.dpopKey,
312
+ dpopKey,
298
313
  accessToken: params.accessToken,
299
314
  clientKey: this.clientKey,
300
315
  instanceUrl: this.instanceUrl,
301
- crypto: this.crypto
316
+ fetch: this._fetch
302
317
  });
303
318
  }
304
319
  async deleteSession(did) {
305
- const headers = {
306
- "x-client-key": this.clientKey
307
- };
308
- if (this.clientSecret) {
309
- headers["x-client-secret"] = this.clientSecret;
310
- }
311
- const resp = await this._fetch(
312
- `${this.instanceUrl}/oauth/sessions/${did}`,
313
- {
314
- method: "DELETE",
315
- headers
316
- }
317
- );
318
- if (!resp.ok && resp.status !== 404) {
319
- const body = await resp.json().catch(() => ({}));
320
- throw new ApiError(
321
- `Failed to delete session: ${resp.status} ${body.message ?? resp.statusText}`,
322
- resp.status,
323
- body
320
+ const session = await this.restoreSession(did);
321
+ if (session) {
322
+ const resp = await session.fetchHandler(
323
+ `${this.instanceUrl}/oauth/sessions/${did}`,
324
+ { method: "DELETE" }
324
325
  );
326
+ if (!resp.ok && resp.status !== 404) {
327
+ const body = await resp.json().catch(() => ({}));
328
+ throw new ApiError(
329
+ `Failed to delete session: ${resp.status} ${body.message ?? resp.statusText}`,
330
+ resp.status,
331
+ body
332
+ );
333
+ }
325
334
  }
326
335
  await this.storage.delete(`${STORAGE_PREFIX}${did}`);
327
336
  const lastActive = await this.storage.get(LAST_ACTIVE_KEY);
@@ -333,13 +342,14 @@ var HappyViewOAuthClient = class {
333
342
  const stored = await this.storage.get(`${STORAGE_PREFIX}${did}`);
334
343
  if (!stored) return null;
335
344
  const data = JSON.parse(stored);
345
+ const dpopKey = await importJwk(data.dpopKey);
336
346
  return new HappyViewSession({
337
347
  did: data.did,
338
- dpopKey: data.dpopKey,
348
+ dpopKey,
339
349
  accessToken: data.accessToken,
340
350
  clientKey: data.clientKey,
341
351
  instanceUrl: data.instanceUrl,
342
- crypto: this.crypto
352
+ fetch: this._fetch
343
353
  });
344
354
  }
345
355
  async restore() {
@@ -348,6 +358,26 @@ var HappyViewOAuthClient = class {
348
358
  return this.restoreSession(did);
349
359
  }
350
360
  };
361
+ function generatePkceVerifier() {
362
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
363
+ let binary = "";
364
+ for (let i = 0; i < bytes.length; i++) {
365
+ binary += String.fromCharCode(bytes[i]);
366
+ }
367
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
368
+ }
369
+ async function computePkceChallenge(verifier) {
370
+ const hash = await crypto.subtle.digest(
371
+ "SHA-256",
372
+ new TextEncoder().encode(verifier)
373
+ );
374
+ const bytes = new Uint8Array(hash);
375
+ let binary = "";
376
+ for (let i = 0; i < bytes.length; i++) {
377
+ binary += String.fromCharCode(bytes[i]);
378
+ }
379
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
380
+ }
351
381
  // Annotate the CommonJS export names for ESM import in node:
352
382
  0 && (module.exports = {
353
383
  ApiError,
@@ -359,7 +389,6 @@ var HappyViewOAuthClient = class {
359
389
  MemoryStorage,
360
390
  ResolutionError,
361
391
  TokenExchangeError,
362
- base64urlEncode,
363
- generateDpopProof
392
+ importJwk
364
393
  });
365
394
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/dpop-proof.ts","../src/session.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["export { HappyViewOAuthClient } from \"./client\";\nexport { base64urlEncode, generateDpopProof } from \"./dpop-proof\";\nexport type { DpopProofParams } from \"./dpop-proof\";\nexport {\n ApiError,\n AuthenticationError,\n HappyViewError,\n InvalidStateError,\n ResolutionError,\n TokenExchangeError,\n} from \"./errors\";\nexport { HappyViewSession } from \"./session\";\nexport type { HappyViewSessionOptions } from \"./session\";\nexport { MemoryStorage } from \"./storage\";\nexport type {\n CryptoAdapter,\n DpopProvision,\n HappyViewOAuthClientOptions,\n ProvisionKeyResponse,\n RegisterSessionParams,\n RegisterSessionResponse,\n StorageAdapter,\n StoredSession,\n} from \"./types\";\n","export class HappyViewError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"HappyViewError\";\n }\n}\n\nexport class ApiError extends HappyViewError {\n readonly status: number;\n readonly body: unknown;\n\n constructor(message: string, status: number, body?: unknown) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.body = body;\n }\n}\n\nexport class AuthenticationError extends HappyViewError {\n readonly status: number;\n\n constructor(message: string, status: number = 401) {\n super(message);\n this.name = \"AuthenticationError\";\n this.status = status;\n }\n}\n\nexport class InvalidStateError extends HappyViewError {\n constructor(message: string) {\n super(message);\n this.name = \"InvalidStateError\";\n }\n}\n\nexport class TokenExchangeError extends HappyViewError {\n readonly status: number;\n readonly body: string;\n\n constructor(message: string, status: number, body: string) {\n super(message);\n this.name = \"TokenExchangeError\";\n this.status = status;\n this.body = body;\n }\n}\n\nexport class ResolutionError extends HappyViewError {\n constructor(message: string) {\n super(message);\n this.name = \"ResolutionError\";\n }\n}\n","import type { CryptoAdapter } from \"./types\";\n\nexport function base64urlEncode(data: Uint8Array): string {\n let binary = \"\";\n for (let i = 0; i < data.length; i++) {\n binary += String.fromCharCode(data[i]);\n }\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nexport interface DpopProofParams {\n privateKey: JsonWebKey;\n method: string;\n url: string;\n accessToken?: string;\n nonce?: string;\n}\n\nexport async function generateDpopProof(\n crypto: CryptoAdapter,\n params: DpopProofParams,\n): Promise<string> {\n const { privateKey, method, url, accessToken, nonce } = params;\n\n const publicJwk = {\n kty: privateKey.kty,\n crv: privateKey.crv,\n x: privateKey.x,\n y: privateKey.y,\n };\n\n const header = {\n alg: \"ES256\",\n typ: \"dpop+jwt\",\n jwk: publicJwk,\n };\n\n // Generate jti\n const jtiBytes = crypto.getRandomValues(16);\n const jti = Array.from(jtiBytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n\n const payload: Record<string, unknown> = {\n htm: method,\n htu: url,\n iat: Math.floor(Date.now() / 1000),\n jti,\n };\n\n // Compute ath: base64url(SHA-256(access_token)) — only when token is present\n if (accessToken) {\n const tokenBytes = new TextEncoder().encode(accessToken);\n const hashBuf = await crypto.sha256(tokenBytes);\n payload.ath = base64urlEncode(hashBuf);\n }\n\n if (nonce !== undefined) {\n payload.nonce = nonce;\n }\n\n const headerB64 = base64urlEncode(\n new TextEncoder().encode(JSON.stringify(header)),\n );\n const payloadB64 = base64urlEncode(\n new TextEncoder().encode(JSON.stringify(payload)),\n );\n\n const message = new TextEncoder().encode(`${headerB64}.${payloadB64}`);\n const signature = await crypto.signEs256(privateKey, message);\n const sigB64 = base64urlEncode(signature);\n\n return `${headerB64}.${payloadB64}.${sigB64}`;\n}\n","import { generateDpopProof } from \"./dpop-proof\";\nimport type { CryptoAdapter } from \"./types\";\n\nexport interface HappyViewSessionOptions {\n did: string;\n dpopKey: JsonWebKey;\n accessToken: string;\n clientKey: string;\n instanceUrl: string;\n crypto: CryptoAdapter;\n fetch?: typeof globalThis.fetch;\n}\n\nexport class HappyViewSession {\n readonly did: string;\n\n private readonly dpopKey: JsonWebKey;\n private readonly accessToken: string;\n private readonly clientKey: string;\n private readonly instanceUrl: string;\n private readonly crypto: CryptoAdapter;\n private readonly _fetch: typeof globalThis.fetch;\n private dpopNonce: string | undefined;\n\n constructor(options: HappyViewSessionOptions) {\n this.did = options.did;\n this.dpopKey = options.dpopKey;\n this.accessToken = options.accessToken;\n this.clientKey = options.clientKey;\n this.instanceUrl = options.instanceUrl.replace(/\\/+$/, \"\");\n this.crypto = options.crypto;\n this._fetch = options.fetch ?? globalThis.fetch;\n }\n\n async fetchHandler(url: string, init: RequestInit): Promise<Response> {\n const fullUrl = /^https?:\\/\\//i.test(url) ? url : `${this.instanceUrl}${url}`;\n const method = (init.method ?? \"GET\").toUpperCase();\n\n const proof = await generateDpopProof(this.crypto, {\n privateKey: this.dpopKey,\n method,\n url: fullUrl,\n accessToken: this.accessToken,\n nonce: this.dpopNonce,\n });\n\n const headers = new Headers(init.headers);\n headers.set(\"authorization\", `DPoP ${this.accessToken}`);\n headers.set(\"dpop\", proof);\n headers.set(\"x-client-key\", this.clientKey);\n\n const response = await this._fetch(fullUrl, {\n ...init,\n headers,\n });\n\n const nonce = response.headers.get(\"dpop-nonce\");\n if (nonce) {\n this.dpopNonce = nonce;\n }\n\n return response;\n }\n}\n","import type { StorageAdapter } from \"./types\";\n\nexport class MemoryStorage implements StorageAdapter {\n private data = new Map<string, string>();\n\n async get(key: string): Promise<string | null> {\n return this.data.get(key) ?? null;\n }\n\n async set(key: string, value: string): Promise<void> {\n this.data.set(key, value);\n }\n\n async delete(key: string): Promise<void> {\n this.data.delete(key);\n }\n}\n","import { ApiError } from \"./errors\";\nimport { HappyViewSession } from \"./session\";\nimport { MemoryStorage } from \"./storage\";\nimport type {\n CryptoAdapter,\n HappyViewOAuthClientOptions,\n ProvisionKeyResponse,\n RegisterSessionParams,\n RegisterSessionResponse,\n StorageAdapter,\n StoredSession,\n} from \"./types\";\n\nconst STORAGE_PREFIX = \"happyview:session:\";\nconst LAST_ACTIVE_KEY = \"happyview:last-active-did\";\n\nexport class HappyViewOAuthClient {\n protected readonly instanceUrl: string;\n protected readonly clientKey: string;\n protected readonly crypto: CryptoAdapter;\n protected readonly storage: StorageAdapter;\n private readonly clientSecret: string | undefined;\n private readonly _fetch: typeof globalThis.fetch;\n\n constructor(\n options: HappyViewOAuthClientOptions & {\n fetch?: typeof globalThis.fetch;\n },\n ) {\n this.instanceUrl = options.instanceUrl.replace(/\\/+$/, \"\");\n this.clientKey = options.clientKey;\n this.clientSecret = options.clientSecret;\n this.crypto = options.crypto;\n this.storage = options.storage ?? new MemoryStorage();\n this._fetch = options.fetch ?? globalThis.fetch;\n }\n\n get isConfidential(): boolean {\n return this.clientSecret !== undefined;\n }\n\n async provisionDpopKey(): Promise<{\n provisionId: string;\n dpopKey: JsonWebKey;\n pkceVerifier?: string;\n }> {\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"x-client-key\": this.clientKey,\n };\n if (this.clientSecret) {\n headers[\"x-client-secret\"] = this.clientSecret;\n }\n\n const body: Record<string, unknown> = {};\n let pkceVerifier: string | undefined;\n\n if (!this.isConfidential) {\n pkceVerifier = await this.crypto.generatePkceVerifier();\n const challenge = await this.crypto.computePkceChallenge(pkceVerifier);\n body.pkce_challenge = challenge;\n }\n\n const resp = await this._fetch(`${this.instanceUrl}/oauth/dpop-keys`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n\n if (!resp.ok) {\n const body = await resp.json().catch(() => ({}));\n throw new ApiError(\n `Failed to provision DPoP key: ${resp.status} ${(body as any).message ?? resp.statusText}`,\n resp.status,\n body,\n );\n }\n\n const data: ProvisionKeyResponse = await resp.json();\n return {\n provisionId: data.provision_id,\n dpopKey: data.dpop_key,\n pkceVerifier,\n };\n }\n\n async registerSession(\n params: RegisterSessionParams,\n ): Promise<HappyViewSession> {\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"x-client-key\": this.clientKey,\n };\n if (this.clientSecret) {\n headers[\"x-client-secret\"] = this.clientSecret;\n }\n\n const body: Record<string, unknown> = {\n provision_id: params.provisionId,\n did: params.did,\n access_token: params.accessToken,\n refresh_token: params.refreshToken,\n scopes: params.scopes,\n pds_url: params.pdsUrl,\n issuer: params.issuer,\n };\n\n if (!this.isConfidential && params.pkceVerifier) {\n body.pkce_verifier = params.pkceVerifier;\n }\n\n const resp = await this._fetch(`${this.instanceUrl}/oauth/sessions`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n\n if (!resp.ok) {\n const body = await resp.json().catch(() => ({}));\n throw new ApiError(\n `Failed to register session: ${resp.status} ${(body as any).message ?? resp.statusText}`,\n resp.status,\n body,\n );\n }\n\n const data: RegisterSessionResponse = await resp.json();\n\n const storedSession: StoredSession = {\n did: data.did,\n dpopKey: params.dpopKey,\n accessToken: params.accessToken,\n clientKey: this.clientKey,\n instanceUrl: this.instanceUrl,\n };\n await this.storage.set(\n `${STORAGE_PREFIX}${data.did}`,\n JSON.stringify(storedSession),\n );\n await this.storage.set(LAST_ACTIVE_KEY, data.did);\n\n return new HappyViewSession({\n did: data.did,\n dpopKey: params.dpopKey,\n accessToken: params.accessToken,\n clientKey: this.clientKey,\n instanceUrl: this.instanceUrl,\n crypto: this.crypto,\n });\n }\n\n async deleteSession(did: string): Promise<void> {\n const headers: Record<string, string> = {\n \"x-client-key\": this.clientKey,\n };\n if (this.clientSecret) {\n headers[\"x-client-secret\"] = this.clientSecret;\n }\n\n const resp = await this._fetch(\n `${this.instanceUrl}/oauth/sessions/${did}`,\n {\n method: \"DELETE\",\n headers,\n },\n );\n\n if (!resp.ok && resp.status !== 404) {\n const body = await resp.json().catch(() => ({}));\n throw new ApiError(\n `Failed to delete session: ${resp.status} ${(body as any).message ?? resp.statusText}`,\n resp.status,\n body,\n );\n }\n\n await this.storage.delete(`${STORAGE_PREFIX}${did}`);\n\n const lastActive = await this.storage.get(LAST_ACTIVE_KEY);\n if (lastActive === did) {\n await this.storage.delete(LAST_ACTIVE_KEY);\n }\n }\n\n async restoreSession(did: string): Promise<HappyViewSession | null> {\n const stored = await this.storage.get(`${STORAGE_PREFIX}${did}`);\n if (!stored) return null;\n\n const data: StoredSession = JSON.parse(stored);\n return new HappyViewSession({\n did: data.did,\n dpopKey: data.dpopKey,\n accessToken: data.accessToken,\n clientKey: data.clientKey,\n instanceUrl: data.instanceUrl,\n crypto: this.crypto,\n });\n }\n\n async restore(): Promise<HappyViewSession | null> {\n const did = await this.storage.get(LAST_ACTIVE_KEY);\n if (!did) return null;\n return this.restoreSession(did);\n }\n}\n"],"mappings":"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","names":["body"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/import-jwk.ts","../src/session.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["export { HappyViewOAuthClient } from \"./client\";\nexport { importJwk } from \"./import-jwk\";\nexport {\n ApiError,\n AuthenticationError,\n HappyViewError,\n InvalidStateError,\n ResolutionError,\n TokenExchangeError,\n} from \"./errors\";\nexport { HappyViewSession } from \"./session\";\nexport type { HappyViewSessionOptions } from \"./session\";\nexport { MemoryStorage } from \"./storage\";\nexport type {\n DpopProvision,\n HappyViewOAuthClientOptions,\n ProvisionKeyResponse,\n RegisterSessionParams,\n RegisterSessionResponse,\n StorageAdapter,\n StoredSession,\n} from \"./types\";\n","export class HappyViewError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"HappyViewError\";\n }\n}\n\nexport class ApiError extends HappyViewError {\n readonly status: number;\n readonly body: unknown;\n\n constructor(message: string, status: number, body?: unknown) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.body = body;\n }\n}\n\nexport class AuthenticationError extends HappyViewError {\n readonly status: number;\n\n constructor(message: string, status: number = 401) {\n super(message);\n this.name = \"AuthenticationError\";\n this.status = status;\n }\n}\n\nexport class InvalidStateError extends HappyViewError {\n constructor(message: string) {\n super(message);\n this.name = \"InvalidStateError\";\n }\n}\n\nexport class TokenExchangeError extends HappyViewError {\n readonly status: number;\n readonly body: string;\n\n constructor(message: string, status: number, body: string) {\n super(message);\n this.name = \"TokenExchangeError\";\n this.status = status;\n this.body = body;\n }\n}\n\nexport class ResolutionError extends HappyViewError {\n constructor(message: string) {\n super(message);\n this.name = \"ResolutionError\";\n }\n}\n","import { WebcryptoKey } from \"@atproto/jwk-webcrypto\";\n\n/**\n * Import a raw ES256 JWK (as returned by HappyView's /oauth/dpop-keys)\n * into a WebcryptoKey that can sign JWTs.\n */\nexport async function importJwk(jwk: JsonWebKey): Promise<WebcryptoKey> {\n const privateKey = await crypto.subtle.importKey(\n \"jwk\",\n jwk,\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n true,\n [\"sign\"],\n );\n\n // Derive public key by stripping the private component\n const { d: _, ...publicJwkFields } = jwk;\n const publicKey = await crypto.subtle.importKey(\n \"jwk\",\n publicJwkFields,\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n true,\n [\"verify\"],\n );\n\n return WebcryptoKey.fromKeypair({ privateKey, publicKey });\n}\n","import type { Key } from \"@atproto/jwk\";\n\nexport interface HappyViewSessionOptions {\n did: string;\n dpopKey: Key;\n accessToken: string;\n clientKey: string;\n instanceUrl: string;\n fetch?: typeof globalThis.fetch;\n}\n\nfunction randomHex(byteLength: number): string {\n const bytes = crypto.getRandomValues(new Uint8Array(byteLength));\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nfunction base64url(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nexport class HappyViewSession {\n readonly did: string;\n\n private readonly dpopKey: Key;\n private readonly accessToken: string;\n private readonly clientKey: string;\n private readonly instanceUrl: string;\n private readonly _fetch: typeof globalThis.fetch;\n private dpopNonce: string | undefined;\n\n constructor(options: HappyViewSessionOptions) {\n this.did = options.did;\n this.dpopKey = options.dpopKey;\n this.accessToken = options.accessToken;\n this.clientKey = options.clientKey;\n this.instanceUrl = options.instanceUrl.replace(/\\/+$/, \"\");\n this._fetch = options.fetch ?? ((input: RequestInfo | URL, init?: RequestInit) => fetch(input, init)) as typeof globalThis.fetch;\n }\n\n async fetchHandler(url: string, init: RequestInit): Promise<Response> {\n const fullUrl = /^https?:\\/\\//i.test(url)\n ? url\n : `${this.instanceUrl}${url}`;\n const method = (init.method ?? \"GET\").toUpperCase();\n const htu = fullUrl.split(\"?\")[0];\n\n const sendWithProof = async (): Promise<Response> => {\n const tokenHash = await crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(this.accessToken),\n );\n const ath = base64url(tokenHash);\n\n const proof = await this.dpopKey.createJwt(\n {\n alg: \"ES256\",\n typ: \"dpop+jwt\",\n jwk: this.dpopKey.publicJwk!,\n },\n {\n htm: method,\n htu,\n iat: Math.floor(Date.now() / 1000),\n jti: randomHex(16),\n ath,\n nonce: this.dpopNonce,\n },\n );\n\n const headers = new Headers(init.headers);\n headers.set(\"authorization\", `DPoP ${this.accessToken}`);\n headers.set(\"dpop\", proof);\n headers.set(\"x-client-key\", this.clientKey);\n\n return this._fetch(fullUrl, { ...init, headers });\n };\n\n let response: Response;\n try {\n response = await sendWithProof();\n } catch (e) {\n const info = `_fetch type: ${typeof this._fetch}, toString: ${String(this._fetch).slice(0, 120)}, url: ${fullUrl}`;\n throw new Error(`fetchHandler failed: ${(e as Error).message}\\n\\nDEBUG: ${info}`);\n }\n\n // Retry once if the server requires a DPoP nonce we didn't have\n const nonce = response.headers.get(\"dpop-nonce\");\n if (nonce && nonce !== this.dpopNonce && (response.status === 401 || response.status === 400)) {\n this.dpopNonce = nonce;\n try {\n response = await sendWithProof();\n } catch (e) {\n throw new Error(`fetchHandler retry failed: ${(e as Error).message}`);\n }\n const retryNonce = response.headers.get(\"dpop-nonce\");\n if (retryNonce) {\n this.dpopNonce = retryNonce;\n }\n } else if (nonce) {\n this.dpopNonce = nonce;\n }\n\n return response;\n }\n}\n","import type { StorageAdapter } from \"./types\";\n\nexport class MemoryStorage implements StorageAdapter {\n private data = new Map<string, string>();\n\n async get(key: string): Promise<string | null> {\n return this.data.get(key) ?? null;\n }\n\n async set(key: string, value: string): Promise<void> {\n this.data.set(key, value);\n }\n\n async delete(key: string): Promise<void> {\n this.data.delete(key);\n }\n}\n","import type { Key } from \"@atproto/jwk\";\nimport { ApiError } from \"./errors\";\nimport { importJwk } from \"./import-jwk\";\nimport { HappyViewSession } from \"./session\";\nimport { MemoryStorage } from \"./storage\";\nimport type {\n HappyViewOAuthClientOptions,\n ProvisionKeyResponse,\n RegisterSessionParams,\n RegisterSessionResponse,\n StorageAdapter,\n StoredSession,\n} from \"./types\";\n\nconst STORAGE_PREFIX = \"happyview:session:\";\nconst LAST_ACTIVE_KEY = \"happyview:last-active-did\";\n\nexport class HappyViewOAuthClient {\n protected readonly instanceUrl: string;\n protected readonly clientKey: string;\n protected readonly storage: StorageAdapter;\n private readonly clientSecret: string | undefined;\n protected readonly _fetch: typeof globalThis.fetch;\n\n constructor(\n options: HappyViewOAuthClientOptions & {\n fetch?: typeof globalThis.fetch;\n },\n ) {\n this.instanceUrl = options.instanceUrl.replace(/\\/+$/, \"\");\n this.clientKey = options.clientKey;\n this.clientSecret = options.clientSecret;\n this.storage = options.storage ?? new MemoryStorage();\n this._fetch = options.fetch ?? ((input: RequestInfo | URL, init?: RequestInit) => fetch(input, init)) as typeof globalThis.fetch;\n }\n\n get isConfidential(): boolean {\n return this.clientSecret !== undefined;\n }\n\n async provisionDpopKey(): Promise<{\n provisionId: string;\n dpopKey: Key;\n rawJwk: JsonWebKey;\n pkceVerifier?: string;\n }> {\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"x-client-key\": this.clientKey,\n };\n if (this.clientSecret) {\n headers[\"x-client-secret\"] = this.clientSecret;\n }\n\n const body: Record<string, unknown> = {};\n let pkceVerifier: string | undefined;\n\n if (!this.isConfidential) {\n pkceVerifier = generatePkceVerifier();\n const challenge = await computePkceChallenge(pkceVerifier);\n body.pkce_challenge = challenge;\n }\n\n const resp = await this._fetch(`${this.instanceUrl}/oauth/dpop-keys`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n\n if (!resp.ok) {\n const body = await resp.json().catch(() => ({}));\n throw new ApiError(\n `Failed to provision DPoP key: ${resp.status} ${(body as any).message ?? resp.statusText}`,\n resp.status,\n body,\n );\n }\n\n const data: ProvisionKeyResponse = await resp.json();\n const dpopKey = await importJwk(data.dpop_key);\n return {\n provisionId: data.provision_id,\n dpopKey,\n rawJwk: data.dpop_key,\n pkceVerifier,\n };\n }\n\n async registerSession(\n params: RegisterSessionParams,\n ): Promise<HappyViewSession> {\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"x-client-key\": this.clientKey,\n };\n if (this.clientSecret) {\n headers[\"x-client-secret\"] = this.clientSecret;\n }\n\n const body: Record<string, unknown> = {\n provision_id: params.provisionId,\n did: params.did,\n access_token: params.accessToken,\n refresh_token: params.refreshToken,\n scopes: params.scopes,\n pds_url: params.pdsUrl,\n issuer: params.issuer,\n };\n\n if (!this.isConfidential && params.pkceVerifier) {\n body.pkce_verifier = params.pkceVerifier;\n }\n\n const resp = await this._fetch(`${this.instanceUrl}/oauth/sessions`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n\n if (!resp.ok) {\n const body = await resp.json().catch(() => ({}));\n throw new ApiError(\n `Failed to register session: ${resp.status} ${(body as any).message ?? resp.statusText}`,\n resp.status,\n body,\n );\n }\n\n const data: RegisterSessionResponse = await resp.json();\n const dpopKey = await importJwk(params.dpopKey);\n\n const storedSession: StoredSession = {\n did: data.did,\n dpopKey: params.dpopKey,\n accessToken: params.accessToken,\n clientKey: this.clientKey,\n instanceUrl: this.instanceUrl,\n };\n await this.storage.set(\n `${STORAGE_PREFIX}${data.did}`,\n JSON.stringify(storedSession),\n );\n await this.storage.set(LAST_ACTIVE_KEY, data.did);\n\n return new HappyViewSession({\n did: data.did,\n dpopKey,\n accessToken: params.accessToken,\n clientKey: this.clientKey,\n instanceUrl: this.instanceUrl,\n fetch: this._fetch,\n });\n }\n\n async deleteSession(did: string): Promise<void> {\n const session = await this.restoreSession(did);\n if (session) {\n const resp = await session.fetchHandler(\n `${this.instanceUrl}/oauth/sessions/${did}`,\n { method: \"DELETE\" },\n );\n\n if (!resp.ok && resp.status !== 404) {\n const body = await resp.json().catch(() => ({}));\n throw new ApiError(\n `Failed to delete session: ${resp.status} ${(body as any).message ?? resp.statusText}`,\n resp.status,\n body,\n );\n }\n }\n\n await this.storage.delete(`${STORAGE_PREFIX}${did}`);\n\n const lastActive = await this.storage.get(LAST_ACTIVE_KEY);\n if (lastActive === did) {\n await this.storage.delete(LAST_ACTIVE_KEY);\n }\n }\n\n async restoreSession(did: string): Promise<HappyViewSession | null> {\n const stored = await this.storage.get(`${STORAGE_PREFIX}${did}`);\n if (!stored) return null;\n\n const data: StoredSession = JSON.parse(stored);\n const dpopKey = await importJwk(data.dpopKey);\n return new HappyViewSession({\n did: data.did,\n dpopKey,\n accessToken: data.accessToken,\n clientKey: data.clientKey,\n instanceUrl: data.instanceUrl,\n fetch: this._fetch,\n });\n }\n\n async restore(): Promise<HappyViewSession | null> {\n const did = await this.storage.get(LAST_ACTIVE_KEY);\n if (!did) return null;\n return this.restoreSession(did);\n }\n}\n\n// PKCE helpers — inline since they're just a few lines of Web Crypto\nfunction generatePkceVerifier(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(32));\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nasync function computePkceChallenge(verifier: string): Promise<string> {\n const hash = await crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(verifier),\n );\n const bytes = new Uint8Array(hash);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n"],"mappings":"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","names":["body"]}
package/dist/index.d.cts CHANGED
@@ -1,10 +1,26 @@
1
- interface CryptoAdapter {
2
- generatePkceVerifier(): Promise<string>;
3
- computePkceChallenge(verifier: string): Promise<string>;
4
- signEs256(privateKey: JsonWebKey, payload: Uint8Array): Promise<Uint8Array>;
5
- sha256(data: Uint8Array): Promise<Uint8Array>;
6
- getRandomValues(length: number): Uint8Array;
1
+ import { Key } from '@atproto/jwk';
2
+ import { WebcryptoKey } from '@atproto/jwk-webcrypto';
3
+
4
+ interface HappyViewSessionOptions {
5
+ did: string;
6
+ dpopKey: Key;
7
+ accessToken: string;
8
+ clientKey: string;
9
+ instanceUrl: string;
10
+ fetch?: typeof globalThis.fetch;
7
11
  }
12
+ declare class HappyViewSession {
13
+ readonly did: string;
14
+ private readonly dpopKey;
15
+ private readonly accessToken;
16
+ private readonly clientKey;
17
+ private readonly instanceUrl;
18
+ private readonly _fetch;
19
+ private dpopNonce;
20
+ constructor(options: HappyViewSessionOptions);
21
+ fetchHandler(url: string, init: RequestInit): Promise<Response>;
22
+ }
23
+
8
24
  interface StorageAdapter {
9
25
  get(key: string): Promise<string | null>;
10
26
  set(key: string, value: string): Promise<void>;
@@ -14,7 +30,6 @@ interface HappyViewOAuthClientOptions {
14
30
  instanceUrl: string;
15
31
  clientKey: string;
16
32
  clientSecret?: string;
17
- crypto: CryptoAdapter;
18
33
  storage?: StorageAdapter;
19
34
  }
20
35
  interface DpopProvision {
@@ -54,42 +69,20 @@ interface RegisterSessionResponse {
54
69
  did: string;
55
70
  }
56
71
 
57
- interface HappyViewSessionOptions {
58
- did: string;
59
- dpopKey: JsonWebKey;
60
- accessToken: string;
61
- clientKey: string;
62
- instanceUrl: string;
63
- crypto: CryptoAdapter;
64
- fetch?: typeof globalThis.fetch;
65
- }
66
- declare class HappyViewSession {
67
- readonly did: string;
68
- private readonly dpopKey;
69
- private readonly accessToken;
70
- private readonly clientKey;
71
- private readonly instanceUrl;
72
- private readonly crypto;
73
- private readonly _fetch;
74
- private dpopNonce;
75
- constructor(options: HappyViewSessionOptions);
76
- fetchHandler(url: string, init: RequestInit): Promise<Response>;
77
- }
78
-
79
72
  declare class HappyViewOAuthClient {
80
73
  protected readonly instanceUrl: string;
81
74
  protected readonly clientKey: string;
82
- protected readonly crypto: CryptoAdapter;
83
75
  protected readonly storage: StorageAdapter;
84
76
  private readonly clientSecret;
85
- private readonly _fetch;
77
+ protected readonly _fetch: typeof globalThis.fetch;
86
78
  constructor(options: HappyViewOAuthClientOptions & {
87
79
  fetch?: typeof globalThis.fetch;
88
80
  });
89
81
  get isConfidential(): boolean;
90
82
  provisionDpopKey(): Promise<{
91
83
  provisionId: string;
92
- dpopKey: JsonWebKey;
84
+ dpopKey: Key;
85
+ rawJwk: JsonWebKey;
93
86
  pkceVerifier?: string;
94
87
  }>;
95
88
  registerSession(params: RegisterSessionParams): Promise<HappyViewSession>;
@@ -98,15 +91,11 @@ declare class HappyViewOAuthClient {
98
91
  restore(): Promise<HappyViewSession | null>;
99
92
  }
100
93
 
101
- declare function base64urlEncode(data: Uint8Array): string;
102
- interface DpopProofParams {
103
- privateKey: JsonWebKey;
104
- method: string;
105
- url: string;
106
- accessToken?: string;
107
- nonce?: string;
108
- }
109
- declare function generateDpopProof(crypto: CryptoAdapter, params: DpopProofParams): Promise<string>;
94
+ /**
95
+ * Import a raw ES256 JWK (as returned by HappyView's /oauth/dpop-keys)
96
+ * into a WebcryptoKey that can sign JWTs.
97
+ */
98
+ declare function importJwk(jwk: JsonWebKey): Promise<WebcryptoKey>;
110
99
 
111
100
  declare class HappyViewError extends Error {
112
101
  constructor(message: string);
@@ -139,4 +128,4 @@ declare class MemoryStorage implements StorageAdapter {
139
128
  delete(key: string): Promise<void>;
140
129
  }
141
130
 
142
- export { ApiError, AuthenticationError, type CryptoAdapter, type DpopProofParams, type DpopProvision, HappyViewError, HappyViewOAuthClient, type HappyViewOAuthClientOptions, HappyViewSession, type HappyViewSessionOptions, InvalidStateError, MemoryStorage, type ProvisionKeyResponse, type RegisterSessionParams, type RegisterSessionResponse, ResolutionError, type StorageAdapter, type StoredSession, TokenExchangeError, base64urlEncode, generateDpopProof };
131
+ export { ApiError, AuthenticationError, type DpopProvision, HappyViewError, HappyViewOAuthClient, type HappyViewOAuthClientOptions, HappyViewSession, type HappyViewSessionOptions, InvalidStateError, MemoryStorage, type ProvisionKeyResponse, type RegisterSessionParams, type RegisterSessionResponse, ResolutionError, type StorageAdapter, type StoredSession, TokenExchangeError, importJwk };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,26 @@
1
- interface CryptoAdapter {
2
- generatePkceVerifier(): Promise<string>;
3
- computePkceChallenge(verifier: string): Promise<string>;
4
- signEs256(privateKey: JsonWebKey, payload: Uint8Array): Promise<Uint8Array>;
5
- sha256(data: Uint8Array): Promise<Uint8Array>;
6
- getRandomValues(length: number): Uint8Array;
1
+ import { Key } from '@atproto/jwk';
2
+ import { WebcryptoKey } from '@atproto/jwk-webcrypto';
3
+
4
+ interface HappyViewSessionOptions {
5
+ did: string;
6
+ dpopKey: Key;
7
+ accessToken: string;
8
+ clientKey: string;
9
+ instanceUrl: string;
10
+ fetch?: typeof globalThis.fetch;
7
11
  }
12
+ declare class HappyViewSession {
13
+ readonly did: string;
14
+ private readonly dpopKey;
15
+ private readonly accessToken;
16
+ private readonly clientKey;
17
+ private readonly instanceUrl;
18
+ private readonly _fetch;
19
+ private dpopNonce;
20
+ constructor(options: HappyViewSessionOptions);
21
+ fetchHandler(url: string, init: RequestInit): Promise<Response>;
22
+ }
23
+
8
24
  interface StorageAdapter {
9
25
  get(key: string): Promise<string | null>;
10
26
  set(key: string, value: string): Promise<void>;
@@ -14,7 +30,6 @@ interface HappyViewOAuthClientOptions {
14
30
  instanceUrl: string;
15
31
  clientKey: string;
16
32
  clientSecret?: string;
17
- crypto: CryptoAdapter;
18
33
  storage?: StorageAdapter;
19
34
  }
20
35
  interface DpopProvision {
@@ -54,42 +69,20 @@ interface RegisterSessionResponse {
54
69
  did: string;
55
70
  }
56
71
 
57
- interface HappyViewSessionOptions {
58
- did: string;
59
- dpopKey: JsonWebKey;
60
- accessToken: string;
61
- clientKey: string;
62
- instanceUrl: string;
63
- crypto: CryptoAdapter;
64
- fetch?: typeof globalThis.fetch;
65
- }
66
- declare class HappyViewSession {
67
- readonly did: string;
68
- private readonly dpopKey;
69
- private readonly accessToken;
70
- private readonly clientKey;
71
- private readonly instanceUrl;
72
- private readonly crypto;
73
- private readonly _fetch;
74
- private dpopNonce;
75
- constructor(options: HappyViewSessionOptions);
76
- fetchHandler(url: string, init: RequestInit): Promise<Response>;
77
- }
78
-
79
72
  declare class HappyViewOAuthClient {
80
73
  protected readonly instanceUrl: string;
81
74
  protected readonly clientKey: string;
82
- protected readonly crypto: CryptoAdapter;
83
75
  protected readonly storage: StorageAdapter;
84
76
  private readonly clientSecret;
85
- private readonly _fetch;
77
+ protected readonly _fetch: typeof globalThis.fetch;
86
78
  constructor(options: HappyViewOAuthClientOptions & {
87
79
  fetch?: typeof globalThis.fetch;
88
80
  });
89
81
  get isConfidential(): boolean;
90
82
  provisionDpopKey(): Promise<{
91
83
  provisionId: string;
92
- dpopKey: JsonWebKey;
84
+ dpopKey: Key;
85
+ rawJwk: JsonWebKey;
93
86
  pkceVerifier?: string;
94
87
  }>;
95
88
  registerSession(params: RegisterSessionParams): Promise<HappyViewSession>;
@@ -98,15 +91,11 @@ declare class HappyViewOAuthClient {
98
91
  restore(): Promise<HappyViewSession | null>;
99
92
  }
100
93
 
101
- declare function base64urlEncode(data: Uint8Array): string;
102
- interface DpopProofParams {
103
- privateKey: JsonWebKey;
104
- method: string;
105
- url: string;
106
- accessToken?: string;
107
- nonce?: string;
108
- }
109
- declare function generateDpopProof(crypto: CryptoAdapter, params: DpopProofParams): Promise<string>;
94
+ /**
95
+ * Import a raw ES256 JWK (as returned by HappyView's /oauth/dpop-keys)
96
+ * into a WebcryptoKey that can sign JWTs.
97
+ */
98
+ declare function importJwk(jwk: JsonWebKey): Promise<WebcryptoKey>;
110
99
 
111
100
  declare class HappyViewError extends Error {
112
101
  constructor(message: string);
@@ -139,4 +128,4 @@ declare class MemoryStorage implements StorageAdapter {
139
128
  delete(key: string): Promise<void>;
140
129
  }
141
130
 
142
- export { ApiError, AuthenticationError, type CryptoAdapter, type DpopProofParams, type DpopProvision, HappyViewError, HappyViewOAuthClient, type HappyViewOAuthClientOptions, HappyViewSession, type HappyViewSessionOptions, InvalidStateError, MemoryStorage, type ProvisionKeyResponse, type RegisterSessionParams, type RegisterSessionResponse, ResolutionError, type StorageAdapter, type StoredSession, TokenExchangeError, base64urlEncode, generateDpopProof };
131
+ export { ApiError, AuthenticationError, type DpopProvision, HappyViewError, HappyViewOAuthClient, type HappyViewOAuthClientOptions, HappyViewSession, type HappyViewSessionOptions, InvalidStateError, MemoryStorage, type ProvisionKeyResponse, type RegisterSessionParams, type RegisterSessionResponse, ResolutionError, type StorageAdapter, type StoredSession, TokenExchangeError, importJwk };
package/dist/index.js CHANGED
@@ -46,63 +46,46 @@ var ResolutionError = class extends HappyViewError {
46
46
  }
47
47
  };
48
48
 
49
- // src/dpop-proof.ts
50
- function base64urlEncode(data) {
51
- let binary = "";
52
- for (let i = 0; i < data.length; i++) {
53
- binary += String.fromCharCode(data[i]);
54
- }
55
- return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
56
- }
57
- async function generateDpopProof(crypto, params) {
58
- const { privateKey, method, url, accessToken, nonce } = params;
59
- const publicJwk = {
60
- kty: privateKey.kty,
61
- crv: privateKey.crv,
62
- x: privateKey.x,
63
- y: privateKey.y
64
- };
65
- const header = {
66
- alg: "ES256",
67
- typ: "dpop+jwt",
68
- jwk: publicJwk
69
- };
70
- const jtiBytes = crypto.getRandomValues(16);
71
- const jti = Array.from(jtiBytes, (b) => b.toString(16).padStart(2, "0")).join("");
72
- const payload = {
73
- htm: method,
74
- htu: url,
75
- iat: Math.floor(Date.now() / 1e3),
76
- jti
77
- };
78
- if (accessToken) {
79
- const tokenBytes = new TextEncoder().encode(accessToken);
80
- const hashBuf = await crypto.sha256(tokenBytes);
81
- payload.ath = base64urlEncode(hashBuf);
82
- }
83
- if (nonce !== void 0) {
84
- payload.nonce = nonce;
85
- }
86
- const headerB64 = base64urlEncode(
87
- new TextEncoder().encode(JSON.stringify(header))
49
+ // src/import-jwk.ts
50
+ import { WebcryptoKey } from "@atproto/jwk-webcrypto";
51
+ async function importJwk(jwk) {
52
+ const privateKey = await crypto.subtle.importKey(
53
+ "jwk",
54
+ jwk,
55
+ { name: "ECDSA", namedCurve: "P-256" },
56
+ true,
57
+ ["sign"]
88
58
  );
89
- const payloadB64 = base64urlEncode(
90
- new TextEncoder().encode(JSON.stringify(payload))
59
+ const { d: _, ...publicJwkFields } = jwk;
60
+ const publicKey = await crypto.subtle.importKey(
61
+ "jwk",
62
+ publicJwkFields,
63
+ { name: "ECDSA", namedCurve: "P-256" },
64
+ true,
65
+ ["verify"]
91
66
  );
92
- const message = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
93
- const signature = await crypto.signEs256(privateKey, message);
94
- const sigB64 = base64urlEncode(signature);
95
- return `${headerB64}.${payloadB64}.${sigB64}`;
67
+ return WebcryptoKey.fromKeypair({ privateKey, publicKey });
96
68
  }
97
69
 
98
70
  // src/session.ts
71
+ function randomHex(byteLength) {
72
+ const bytes = crypto.getRandomValues(new Uint8Array(byteLength));
73
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
74
+ }
75
+ function base64url(buffer) {
76
+ const bytes = new Uint8Array(buffer);
77
+ let binary = "";
78
+ for (let i = 0; i < bytes.length; i++) {
79
+ binary += String.fromCharCode(bytes[i]);
80
+ }
81
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
82
+ }
99
83
  var HappyViewSession = class {
100
84
  did;
101
85
  dpopKey;
102
86
  accessToken;
103
87
  clientKey;
104
88
  instanceUrl;
105
- crypto;
106
89
  _fetch;
107
90
  dpopNonce;
108
91
  constructor(options) {
@@ -111,29 +94,61 @@ var HappyViewSession = class {
111
94
  this.accessToken = options.accessToken;
112
95
  this.clientKey = options.clientKey;
113
96
  this.instanceUrl = options.instanceUrl.replace(/\/+$/, "");
114
- this.crypto = options.crypto;
115
- this._fetch = options.fetch ?? globalThis.fetch;
97
+ this._fetch = options.fetch ?? ((input, init) => fetch(input, init));
116
98
  }
117
99
  async fetchHandler(url, init) {
118
100
  const fullUrl = /^https?:\/\//i.test(url) ? url : `${this.instanceUrl}${url}`;
119
101
  const method = (init.method ?? "GET").toUpperCase();
120
- const proof = await generateDpopProof(this.crypto, {
121
- privateKey: this.dpopKey,
122
- method,
123
- url: fullUrl,
124
- accessToken: this.accessToken,
125
- nonce: this.dpopNonce
126
- });
127
- const headers = new Headers(init.headers);
128
- headers.set("authorization", `DPoP ${this.accessToken}`);
129
- headers.set("dpop", proof);
130
- headers.set("x-client-key", this.clientKey);
131
- const response = await this._fetch(fullUrl, {
132
- ...init,
133
- headers
134
- });
102
+ const htu = fullUrl.split("?")[0];
103
+ const sendWithProof = async () => {
104
+ const tokenHash = await crypto.subtle.digest(
105
+ "SHA-256",
106
+ new TextEncoder().encode(this.accessToken)
107
+ );
108
+ const ath = base64url(tokenHash);
109
+ const proof = await this.dpopKey.createJwt(
110
+ {
111
+ alg: "ES256",
112
+ typ: "dpop+jwt",
113
+ jwk: this.dpopKey.publicJwk
114
+ },
115
+ {
116
+ htm: method,
117
+ htu,
118
+ iat: Math.floor(Date.now() / 1e3),
119
+ jti: randomHex(16),
120
+ ath,
121
+ nonce: this.dpopNonce
122
+ }
123
+ );
124
+ const headers = new Headers(init.headers);
125
+ headers.set("authorization", `DPoP ${this.accessToken}`);
126
+ headers.set("dpop", proof);
127
+ headers.set("x-client-key", this.clientKey);
128
+ return this._fetch(fullUrl, { ...init, headers });
129
+ };
130
+ let response;
131
+ try {
132
+ response = await sendWithProof();
133
+ } catch (e) {
134
+ const info = `_fetch type: ${typeof this._fetch}, toString: ${String(this._fetch).slice(0, 120)}, url: ${fullUrl}`;
135
+ throw new Error(`fetchHandler failed: ${e.message}
136
+
137
+ DEBUG: ${info}`);
138
+ }
135
139
  const nonce = response.headers.get("dpop-nonce");
136
- if (nonce) {
140
+ if (nonce && nonce !== this.dpopNonce && (response.status === 401 || response.status === 400)) {
141
+ this.dpopNonce = nonce;
142
+ try {
143
+ response = await sendWithProof();
144
+ } catch (e) {
145
+ throw new Error(`fetchHandler retry failed: ${e.message}`);
146
+ }
147
+ const retryNonce = response.headers.get("dpop-nonce");
148
+ if (retryNonce) {
149
+ this.dpopNonce = retryNonce;
150
+ }
151
+ } else if (nonce) {
137
152
  this.dpopNonce = nonce;
138
153
  }
139
154
  return response;
@@ -160,7 +175,6 @@ var LAST_ACTIVE_KEY = "happyview:last-active-did";
160
175
  var HappyViewOAuthClient = class {
161
176
  instanceUrl;
162
177
  clientKey;
163
- crypto;
164
178
  storage;
165
179
  clientSecret;
166
180
  _fetch;
@@ -168,9 +182,8 @@ var HappyViewOAuthClient = class {
168
182
  this.instanceUrl = options.instanceUrl.replace(/\/+$/, "");
169
183
  this.clientKey = options.clientKey;
170
184
  this.clientSecret = options.clientSecret;
171
- this.crypto = options.crypto;
172
185
  this.storage = options.storage ?? new MemoryStorage();
173
- this._fetch = options.fetch ?? globalThis.fetch;
186
+ this._fetch = options.fetch ?? ((input, init) => fetch(input, init));
174
187
  }
175
188
  get isConfidential() {
176
189
  return this.clientSecret !== void 0;
@@ -186,8 +199,8 @@ var HappyViewOAuthClient = class {
186
199
  const body = {};
187
200
  let pkceVerifier;
188
201
  if (!this.isConfidential) {
189
- pkceVerifier = await this.crypto.generatePkceVerifier();
190
- const challenge = await this.crypto.computePkceChallenge(pkceVerifier);
202
+ pkceVerifier = generatePkceVerifier();
203
+ const challenge = await computePkceChallenge(pkceVerifier);
191
204
  body.pkce_challenge = challenge;
192
205
  }
193
206
  const resp = await this._fetch(`${this.instanceUrl}/oauth/dpop-keys`, {
@@ -204,9 +217,11 @@ var HappyViewOAuthClient = class {
204
217
  );
205
218
  }
206
219
  const data = await resp.json();
220
+ const dpopKey = await importJwk(data.dpop_key);
207
221
  return {
208
222
  provisionId: data.provision_id,
209
- dpopKey: data.dpop_key,
223
+ dpopKey,
224
+ rawJwk: data.dpop_key,
210
225
  pkceVerifier
211
226
  };
212
227
  }
@@ -244,6 +259,7 @@ var HappyViewOAuthClient = class {
244
259
  );
245
260
  }
246
261
  const data = await resp.json();
262
+ const dpopKey = await importJwk(params.dpopKey);
247
263
  const storedSession = {
248
264
  did: data.did,
249
265
  dpopKey: params.dpopKey,
@@ -258,34 +274,28 @@ var HappyViewOAuthClient = class {
258
274
  await this.storage.set(LAST_ACTIVE_KEY, data.did);
259
275
  return new HappyViewSession({
260
276
  did: data.did,
261
- dpopKey: params.dpopKey,
277
+ dpopKey,
262
278
  accessToken: params.accessToken,
263
279
  clientKey: this.clientKey,
264
280
  instanceUrl: this.instanceUrl,
265
- crypto: this.crypto
281
+ fetch: this._fetch
266
282
  });
267
283
  }
268
284
  async deleteSession(did) {
269
- const headers = {
270
- "x-client-key": this.clientKey
271
- };
272
- if (this.clientSecret) {
273
- headers["x-client-secret"] = this.clientSecret;
274
- }
275
- const resp = await this._fetch(
276
- `${this.instanceUrl}/oauth/sessions/${did}`,
277
- {
278
- method: "DELETE",
279
- headers
280
- }
281
- );
282
- if (!resp.ok && resp.status !== 404) {
283
- const body = await resp.json().catch(() => ({}));
284
- throw new ApiError(
285
- `Failed to delete session: ${resp.status} ${body.message ?? resp.statusText}`,
286
- resp.status,
287
- body
285
+ const session = await this.restoreSession(did);
286
+ if (session) {
287
+ const resp = await session.fetchHandler(
288
+ `${this.instanceUrl}/oauth/sessions/${did}`,
289
+ { method: "DELETE" }
288
290
  );
291
+ if (!resp.ok && resp.status !== 404) {
292
+ const body = await resp.json().catch(() => ({}));
293
+ throw new ApiError(
294
+ `Failed to delete session: ${resp.status} ${body.message ?? resp.statusText}`,
295
+ resp.status,
296
+ body
297
+ );
298
+ }
289
299
  }
290
300
  await this.storage.delete(`${STORAGE_PREFIX}${did}`);
291
301
  const lastActive = await this.storage.get(LAST_ACTIVE_KEY);
@@ -297,13 +307,14 @@ var HappyViewOAuthClient = class {
297
307
  const stored = await this.storage.get(`${STORAGE_PREFIX}${did}`);
298
308
  if (!stored) return null;
299
309
  const data = JSON.parse(stored);
310
+ const dpopKey = await importJwk(data.dpopKey);
300
311
  return new HappyViewSession({
301
312
  did: data.did,
302
- dpopKey: data.dpopKey,
313
+ dpopKey,
303
314
  accessToken: data.accessToken,
304
315
  clientKey: data.clientKey,
305
316
  instanceUrl: data.instanceUrl,
306
- crypto: this.crypto
317
+ fetch: this._fetch
307
318
  });
308
319
  }
309
320
  async restore() {
@@ -312,6 +323,26 @@ var HappyViewOAuthClient = class {
312
323
  return this.restoreSession(did);
313
324
  }
314
325
  };
326
+ function generatePkceVerifier() {
327
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
328
+ let binary = "";
329
+ for (let i = 0; i < bytes.length; i++) {
330
+ binary += String.fromCharCode(bytes[i]);
331
+ }
332
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
333
+ }
334
+ async function computePkceChallenge(verifier) {
335
+ const hash = await crypto.subtle.digest(
336
+ "SHA-256",
337
+ new TextEncoder().encode(verifier)
338
+ );
339
+ const bytes = new Uint8Array(hash);
340
+ let binary = "";
341
+ for (let i = 0; i < bytes.length; i++) {
342
+ binary += String.fromCharCode(bytes[i]);
343
+ }
344
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
345
+ }
315
346
  export {
316
347
  ApiError,
317
348
  AuthenticationError,
@@ -322,7 +353,6 @@ export {
322
353
  MemoryStorage,
323
354
  ResolutionError,
324
355
  TokenExchangeError,
325
- base64urlEncode,
326
- generateDpopProof
356
+ importJwk
327
357
  };
328
358
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/dpop-proof.ts","../src/session.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["export class HappyViewError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"HappyViewError\";\n }\n}\n\nexport class ApiError extends HappyViewError {\n readonly status: number;\n readonly body: unknown;\n\n constructor(message: string, status: number, body?: unknown) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.body = body;\n }\n}\n\nexport class AuthenticationError extends HappyViewError {\n readonly status: number;\n\n constructor(message: string, status: number = 401) {\n super(message);\n this.name = \"AuthenticationError\";\n this.status = status;\n }\n}\n\nexport class InvalidStateError extends HappyViewError {\n constructor(message: string) {\n super(message);\n this.name = \"InvalidStateError\";\n }\n}\n\nexport class TokenExchangeError extends HappyViewError {\n readonly status: number;\n readonly body: string;\n\n constructor(message: string, status: number, body: string) {\n super(message);\n this.name = \"TokenExchangeError\";\n this.status = status;\n this.body = body;\n }\n}\n\nexport class ResolutionError extends HappyViewError {\n constructor(message: string) {\n super(message);\n this.name = \"ResolutionError\";\n }\n}\n","import type { CryptoAdapter } from \"./types\";\n\nexport function base64urlEncode(data: Uint8Array): string {\n let binary = \"\";\n for (let i = 0; i < data.length; i++) {\n binary += String.fromCharCode(data[i]);\n }\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nexport interface DpopProofParams {\n privateKey: JsonWebKey;\n method: string;\n url: string;\n accessToken?: string;\n nonce?: string;\n}\n\nexport async function generateDpopProof(\n crypto: CryptoAdapter,\n params: DpopProofParams,\n): Promise<string> {\n const { privateKey, method, url, accessToken, nonce } = params;\n\n const publicJwk = {\n kty: privateKey.kty,\n crv: privateKey.crv,\n x: privateKey.x,\n y: privateKey.y,\n };\n\n const header = {\n alg: \"ES256\",\n typ: \"dpop+jwt\",\n jwk: publicJwk,\n };\n\n // Generate jti\n const jtiBytes = crypto.getRandomValues(16);\n const jti = Array.from(jtiBytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n\n const payload: Record<string, unknown> = {\n htm: method,\n htu: url,\n iat: Math.floor(Date.now() / 1000),\n jti,\n };\n\n // Compute ath: base64url(SHA-256(access_token)) — only when token is present\n if (accessToken) {\n const tokenBytes = new TextEncoder().encode(accessToken);\n const hashBuf = await crypto.sha256(tokenBytes);\n payload.ath = base64urlEncode(hashBuf);\n }\n\n if (nonce !== undefined) {\n payload.nonce = nonce;\n }\n\n const headerB64 = base64urlEncode(\n new TextEncoder().encode(JSON.stringify(header)),\n );\n const payloadB64 = base64urlEncode(\n new TextEncoder().encode(JSON.stringify(payload)),\n );\n\n const message = new TextEncoder().encode(`${headerB64}.${payloadB64}`);\n const signature = await crypto.signEs256(privateKey, message);\n const sigB64 = base64urlEncode(signature);\n\n return `${headerB64}.${payloadB64}.${sigB64}`;\n}\n","import { generateDpopProof } from \"./dpop-proof\";\nimport type { CryptoAdapter } from \"./types\";\n\nexport interface HappyViewSessionOptions {\n did: string;\n dpopKey: JsonWebKey;\n accessToken: string;\n clientKey: string;\n instanceUrl: string;\n crypto: CryptoAdapter;\n fetch?: typeof globalThis.fetch;\n}\n\nexport class HappyViewSession {\n readonly did: string;\n\n private readonly dpopKey: JsonWebKey;\n private readonly accessToken: string;\n private readonly clientKey: string;\n private readonly instanceUrl: string;\n private readonly crypto: CryptoAdapter;\n private readonly _fetch: typeof globalThis.fetch;\n private dpopNonce: string | undefined;\n\n constructor(options: HappyViewSessionOptions) {\n this.did = options.did;\n this.dpopKey = options.dpopKey;\n this.accessToken = options.accessToken;\n this.clientKey = options.clientKey;\n this.instanceUrl = options.instanceUrl.replace(/\\/+$/, \"\");\n this.crypto = options.crypto;\n this._fetch = options.fetch ?? globalThis.fetch;\n }\n\n async fetchHandler(url: string, init: RequestInit): Promise<Response> {\n const fullUrl = /^https?:\\/\\//i.test(url) ? url : `${this.instanceUrl}${url}`;\n const method = (init.method ?? \"GET\").toUpperCase();\n\n const proof = await generateDpopProof(this.crypto, {\n privateKey: this.dpopKey,\n method,\n url: fullUrl,\n accessToken: this.accessToken,\n nonce: this.dpopNonce,\n });\n\n const headers = new Headers(init.headers);\n headers.set(\"authorization\", `DPoP ${this.accessToken}`);\n headers.set(\"dpop\", proof);\n headers.set(\"x-client-key\", this.clientKey);\n\n const response = await this._fetch(fullUrl, {\n ...init,\n headers,\n });\n\n const nonce = response.headers.get(\"dpop-nonce\");\n if (nonce) {\n this.dpopNonce = nonce;\n }\n\n return response;\n }\n}\n","import type { StorageAdapter } from \"./types\";\n\nexport class MemoryStorage implements StorageAdapter {\n private data = new Map<string, string>();\n\n async get(key: string): Promise<string | null> {\n return this.data.get(key) ?? null;\n }\n\n async set(key: string, value: string): Promise<void> {\n this.data.set(key, value);\n }\n\n async delete(key: string): Promise<void> {\n this.data.delete(key);\n }\n}\n","import { ApiError } from \"./errors\";\nimport { HappyViewSession } from \"./session\";\nimport { MemoryStorage } from \"./storage\";\nimport type {\n CryptoAdapter,\n HappyViewOAuthClientOptions,\n ProvisionKeyResponse,\n RegisterSessionParams,\n RegisterSessionResponse,\n StorageAdapter,\n StoredSession,\n} from \"./types\";\n\nconst STORAGE_PREFIX = \"happyview:session:\";\nconst LAST_ACTIVE_KEY = \"happyview:last-active-did\";\n\nexport class HappyViewOAuthClient {\n protected readonly instanceUrl: string;\n protected readonly clientKey: string;\n protected readonly crypto: CryptoAdapter;\n protected readonly storage: StorageAdapter;\n private readonly clientSecret: string | undefined;\n private readonly _fetch: typeof globalThis.fetch;\n\n constructor(\n options: HappyViewOAuthClientOptions & {\n fetch?: typeof globalThis.fetch;\n },\n ) {\n this.instanceUrl = options.instanceUrl.replace(/\\/+$/, \"\");\n this.clientKey = options.clientKey;\n this.clientSecret = options.clientSecret;\n this.crypto = options.crypto;\n this.storage = options.storage ?? new MemoryStorage();\n this._fetch = options.fetch ?? globalThis.fetch;\n }\n\n get isConfidential(): boolean {\n return this.clientSecret !== undefined;\n }\n\n async provisionDpopKey(): Promise<{\n provisionId: string;\n dpopKey: JsonWebKey;\n pkceVerifier?: string;\n }> {\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"x-client-key\": this.clientKey,\n };\n if (this.clientSecret) {\n headers[\"x-client-secret\"] = this.clientSecret;\n }\n\n const body: Record<string, unknown> = {};\n let pkceVerifier: string | undefined;\n\n if (!this.isConfidential) {\n pkceVerifier = await this.crypto.generatePkceVerifier();\n const challenge = await this.crypto.computePkceChallenge(pkceVerifier);\n body.pkce_challenge = challenge;\n }\n\n const resp = await this._fetch(`${this.instanceUrl}/oauth/dpop-keys`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n\n if (!resp.ok) {\n const body = await resp.json().catch(() => ({}));\n throw new ApiError(\n `Failed to provision DPoP key: ${resp.status} ${(body as any).message ?? resp.statusText}`,\n resp.status,\n body,\n );\n }\n\n const data: ProvisionKeyResponse = await resp.json();\n return {\n provisionId: data.provision_id,\n dpopKey: data.dpop_key,\n pkceVerifier,\n };\n }\n\n async registerSession(\n params: RegisterSessionParams,\n ): Promise<HappyViewSession> {\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"x-client-key\": this.clientKey,\n };\n if (this.clientSecret) {\n headers[\"x-client-secret\"] = this.clientSecret;\n }\n\n const body: Record<string, unknown> = {\n provision_id: params.provisionId,\n did: params.did,\n access_token: params.accessToken,\n refresh_token: params.refreshToken,\n scopes: params.scopes,\n pds_url: params.pdsUrl,\n issuer: params.issuer,\n };\n\n if (!this.isConfidential && params.pkceVerifier) {\n body.pkce_verifier = params.pkceVerifier;\n }\n\n const resp = await this._fetch(`${this.instanceUrl}/oauth/sessions`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n\n if (!resp.ok) {\n const body = await resp.json().catch(() => ({}));\n throw new ApiError(\n `Failed to register session: ${resp.status} ${(body as any).message ?? resp.statusText}`,\n resp.status,\n body,\n );\n }\n\n const data: RegisterSessionResponse = await resp.json();\n\n const storedSession: StoredSession = {\n did: data.did,\n dpopKey: params.dpopKey,\n accessToken: params.accessToken,\n clientKey: this.clientKey,\n instanceUrl: this.instanceUrl,\n };\n await this.storage.set(\n `${STORAGE_PREFIX}${data.did}`,\n JSON.stringify(storedSession),\n );\n await this.storage.set(LAST_ACTIVE_KEY, data.did);\n\n return new HappyViewSession({\n did: data.did,\n dpopKey: params.dpopKey,\n accessToken: params.accessToken,\n clientKey: this.clientKey,\n instanceUrl: this.instanceUrl,\n crypto: this.crypto,\n });\n }\n\n async deleteSession(did: string): Promise<void> {\n const headers: Record<string, string> = {\n \"x-client-key\": this.clientKey,\n };\n if (this.clientSecret) {\n headers[\"x-client-secret\"] = this.clientSecret;\n }\n\n const resp = await this._fetch(\n `${this.instanceUrl}/oauth/sessions/${did}`,\n {\n method: \"DELETE\",\n headers,\n },\n );\n\n if (!resp.ok && resp.status !== 404) {\n const body = await resp.json().catch(() => ({}));\n throw new ApiError(\n `Failed to delete session: ${resp.status} ${(body as any).message ?? resp.statusText}`,\n resp.status,\n body,\n );\n }\n\n await this.storage.delete(`${STORAGE_PREFIX}${did}`);\n\n const lastActive = await this.storage.get(LAST_ACTIVE_KEY);\n if (lastActive === did) {\n await this.storage.delete(LAST_ACTIVE_KEY);\n }\n }\n\n async restoreSession(did: string): Promise<HappyViewSession | null> {\n const stored = await this.storage.get(`${STORAGE_PREFIX}${did}`);\n if (!stored) return null;\n\n const data: StoredSession = JSON.parse(stored);\n return new HappyViewSession({\n did: data.did,\n dpopKey: data.dpopKey,\n accessToken: data.accessToken,\n clientKey: data.clientKey,\n instanceUrl: data.instanceUrl,\n crypto: this.crypto,\n });\n }\n\n async restore(): Promise<HappyViewSession | null> {\n const did = await this.storage.get(LAST_ACTIVE_KEY);\n if (!did) return null;\n return this.restoreSession(did);\n }\n}\n"],"mappings":"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","names":["body"]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/import-jwk.ts","../src/session.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["export class HappyViewError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"HappyViewError\";\n }\n}\n\nexport class ApiError extends HappyViewError {\n readonly status: number;\n readonly body: unknown;\n\n constructor(message: string, status: number, body?: unknown) {\n super(message);\n this.name = \"ApiError\";\n this.status = status;\n this.body = body;\n }\n}\n\nexport class AuthenticationError extends HappyViewError {\n readonly status: number;\n\n constructor(message: string, status: number = 401) {\n super(message);\n this.name = \"AuthenticationError\";\n this.status = status;\n }\n}\n\nexport class InvalidStateError extends HappyViewError {\n constructor(message: string) {\n super(message);\n this.name = \"InvalidStateError\";\n }\n}\n\nexport class TokenExchangeError extends HappyViewError {\n readonly status: number;\n readonly body: string;\n\n constructor(message: string, status: number, body: string) {\n super(message);\n this.name = \"TokenExchangeError\";\n this.status = status;\n this.body = body;\n }\n}\n\nexport class ResolutionError extends HappyViewError {\n constructor(message: string) {\n super(message);\n this.name = \"ResolutionError\";\n }\n}\n","import { WebcryptoKey } from \"@atproto/jwk-webcrypto\";\n\n/**\n * Import a raw ES256 JWK (as returned by HappyView's /oauth/dpop-keys)\n * into a WebcryptoKey that can sign JWTs.\n */\nexport async function importJwk(jwk: JsonWebKey): Promise<WebcryptoKey> {\n const privateKey = await crypto.subtle.importKey(\n \"jwk\",\n jwk,\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n true,\n [\"sign\"],\n );\n\n // Derive public key by stripping the private component\n const { d: _, ...publicJwkFields } = jwk;\n const publicKey = await crypto.subtle.importKey(\n \"jwk\",\n publicJwkFields,\n { name: \"ECDSA\", namedCurve: \"P-256\" },\n true,\n [\"verify\"],\n );\n\n return WebcryptoKey.fromKeypair({ privateKey, publicKey });\n}\n","import type { Key } from \"@atproto/jwk\";\n\nexport interface HappyViewSessionOptions {\n did: string;\n dpopKey: Key;\n accessToken: string;\n clientKey: string;\n instanceUrl: string;\n fetch?: typeof globalThis.fetch;\n}\n\nfunction randomHex(byteLength: number): string {\n const bytes = crypto.getRandomValues(new Uint8Array(byteLength));\n return Array.from(bytes, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nfunction base64url(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nexport class HappyViewSession {\n readonly did: string;\n\n private readonly dpopKey: Key;\n private readonly accessToken: string;\n private readonly clientKey: string;\n private readonly instanceUrl: string;\n private readonly _fetch: typeof globalThis.fetch;\n private dpopNonce: string | undefined;\n\n constructor(options: HappyViewSessionOptions) {\n this.did = options.did;\n this.dpopKey = options.dpopKey;\n this.accessToken = options.accessToken;\n this.clientKey = options.clientKey;\n this.instanceUrl = options.instanceUrl.replace(/\\/+$/, \"\");\n this._fetch = options.fetch ?? ((input: RequestInfo | URL, init?: RequestInit) => fetch(input, init)) as typeof globalThis.fetch;\n }\n\n async fetchHandler(url: string, init: RequestInit): Promise<Response> {\n const fullUrl = /^https?:\\/\\//i.test(url)\n ? url\n : `${this.instanceUrl}${url}`;\n const method = (init.method ?? \"GET\").toUpperCase();\n const htu = fullUrl.split(\"?\")[0];\n\n const sendWithProof = async (): Promise<Response> => {\n const tokenHash = await crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(this.accessToken),\n );\n const ath = base64url(tokenHash);\n\n const proof = await this.dpopKey.createJwt(\n {\n alg: \"ES256\",\n typ: \"dpop+jwt\",\n jwk: this.dpopKey.publicJwk!,\n },\n {\n htm: method,\n htu,\n iat: Math.floor(Date.now() / 1000),\n jti: randomHex(16),\n ath,\n nonce: this.dpopNonce,\n },\n );\n\n const headers = new Headers(init.headers);\n headers.set(\"authorization\", `DPoP ${this.accessToken}`);\n headers.set(\"dpop\", proof);\n headers.set(\"x-client-key\", this.clientKey);\n\n return this._fetch(fullUrl, { ...init, headers });\n };\n\n let response: Response;\n try {\n response = await sendWithProof();\n } catch (e) {\n const info = `_fetch type: ${typeof this._fetch}, toString: ${String(this._fetch).slice(0, 120)}, url: ${fullUrl}`;\n throw new Error(`fetchHandler failed: ${(e as Error).message}\\n\\nDEBUG: ${info}`);\n }\n\n // Retry once if the server requires a DPoP nonce we didn't have\n const nonce = response.headers.get(\"dpop-nonce\");\n if (nonce && nonce !== this.dpopNonce && (response.status === 401 || response.status === 400)) {\n this.dpopNonce = nonce;\n try {\n response = await sendWithProof();\n } catch (e) {\n throw new Error(`fetchHandler retry failed: ${(e as Error).message}`);\n }\n const retryNonce = response.headers.get(\"dpop-nonce\");\n if (retryNonce) {\n this.dpopNonce = retryNonce;\n }\n } else if (nonce) {\n this.dpopNonce = nonce;\n }\n\n return response;\n }\n}\n","import type { StorageAdapter } from \"./types\";\n\nexport class MemoryStorage implements StorageAdapter {\n private data = new Map<string, string>();\n\n async get(key: string): Promise<string | null> {\n return this.data.get(key) ?? null;\n }\n\n async set(key: string, value: string): Promise<void> {\n this.data.set(key, value);\n }\n\n async delete(key: string): Promise<void> {\n this.data.delete(key);\n }\n}\n","import type { Key } from \"@atproto/jwk\";\nimport { ApiError } from \"./errors\";\nimport { importJwk } from \"./import-jwk\";\nimport { HappyViewSession } from \"./session\";\nimport { MemoryStorage } from \"./storage\";\nimport type {\n HappyViewOAuthClientOptions,\n ProvisionKeyResponse,\n RegisterSessionParams,\n RegisterSessionResponse,\n StorageAdapter,\n StoredSession,\n} from \"./types\";\n\nconst STORAGE_PREFIX = \"happyview:session:\";\nconst LAST_ACTIVE_KEY = \"happyview:last-active-did\";\n\nexport class HappyViewOAuthClient {\n protected readonly instanceUrl: string;\n protected readonly clientKey: string;\n protected readonly storage: StorageAdapter;\n private readonly clientSecret: string | undefined;\n protected readonly _fetch: typeof globalThis.fetch;\n\n constructor(\n options: HappyViewOAuthClientOptions & {\n fetch?: typeof globalThis.fetch;\n },\n ) {\n this.instanceUrl = options.instanceUrl.replace(/\\/+$/, \"\");\n this.clientKey = options.clientKey;\n this.clientSecret = options.clientSecret;\n this.storage = options.storage ?? new MemoryStorage();\n this._fetch = options.fetch ?? ((input: RequestInfo | URL, init?: RequestInit) => fetch(input, init)) as typeof globalThis.fetch;\n }\n\n get isConfidential(): boolean {\n return this.clientSecret !== undefined;\n }\n\n async provisionDpopKey(): Promise<{\n provisionId: string;\n dpopKey: Key;\n rawJwk: JsonWebKey;\n pkceVerifier?: string;\n }> {\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"x-client-key\": this.clientKey,\n };\n if (this.clientSecret) {\n headers[\"x-client-secret\"] = this.clientSecret;\n }\n\n const body: Record<string, unknown> = {};\n let pkceVerifier: string | undefined;\n\n if (!this.isConfidential) {\n pkceVerifier = generatePkceVerifier();\n const challenge = await computePkceChallenge(pkceVerifier);\n body.pkce_challenge = challenge;\n }\n\n const resp = await this._fetch(`${this.instanceUrl}/oauth/dpop-keys`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n\n if (!resp.ok) {\n const body = await resp.json().catch(() => ({}));\n throw new ApiError(\n `Failed to provision DPoP key: ${resp.status} ${(body as any).message ?? resp.statusText}`,\n resp.status,\n body,\n );\n }\n\n const data: ProvisionKeyResponse = await resp.json();\n const dpopKey = await importJwk(data.dpop_key);\n return {\n provisionId: data.provision_id,\n dpopKey,\n rawJwk: data.dpop_key,\n pkceVerifier,\n };\n }\n\n async registerSession(\n params: RegisterSessionParams,\n ): Promise<HappyViewSession> {\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n \"x-client-key\": this.clientKey,\n };\n if (this.clientSecret) {\n headers[\"x-client-secret\"] = this.clientSecret;\n }\n\n const body: Record<string, unknown> = {\n provision_id: params.provisionId,\n did: params.did,\n access_token: params.accessToken,\n refresh_token: params.refreshToken,\n scopes: params.scopes,\n pds_url: params.pdsUrl,\n issuer: params.issuer,\n };\n\n if (!this.isConfidential && params.pkceVerifier) {\n body.pkce_verifier = params.pkceVerifier;\n }\n\n const resp = await this._fetch(`${this.instanceUrl}/oauth/sessions`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n });\n\n if (!resp.ok) {\n const body = await resp.json().catch(() => ({}));\n throw new ApiError(\n `Failed to register session: ${resp.status} ${(body as any).message ?? resp.statusText}`,\n resp.status,\n body,\n );\n }\n\n const data: RegisterSessionResponse = await resp.json();\n const dpopKey = await importJwk(params.dpopKey);\n\n const storedSession: StoredSession = {\n did: data.did,\n dpopKey: params.dpopKey,\n accessToken: params.accessToken,\n clientKey: this.clientKey,\n instanceUrl: this.instanceUrl,\n };\n await this.storage.set(\n `${STORAGE_PREFIX}${data.did}`,\n JSON.stringify(storedSession),\n );\n await this.storage.set(LAST_ACTIVE_KEY, data.did);\n\n return new HappyViewSession({\n did: data.did,\n dpopKey,\n accessToken: params.accessToken,\n clientKey: this.clientKey,\n instanceUrl: this.instanceUrl,\n fetch: this._fetch,\n });\n }\n\n async deleteSession(did: string): Promise<void> {\n const session = await this.restoreSession(did);\n if (session) {\n const resp = await session.fetchHandler(\n `${this.instanceUrl}/oauth/sessions/${did}`,\n { method: \"DELETE\" },\n );\n\n if (!resp.ok && resp.status !== 404) {\n const body = await resp.json().catch(() => ({}));\n throw new ApiError(\n `Failed to delete session: ${resp.status} ${(body as any).message ?? resp.statusText}`,\n resp.status,\n body,\n );\n }\n }\n\n await this.storage.delete(`${STORAGE_PREFIX}${did}`);\n\n const lastActive = await this.storage.get(LAST_ACTIVE_KEY);\n if (lastActive === did) {\n await this.storage.delete(LAST_ACTIVE_KEY);\n }\n }\n\n async restoreSession(did: string): Promise<HappyViewSession | null> {\n const stored = await this.storage.get(`${STORAGE_PREFIX}${did}`);\n if (!stored) return null;\n\n const data: StoredSession = JSON.parse(stored);\n const dpopKey = await importJwk(data.dpopKey);\n return new HappyViewSession({\n did: data.did,\n dpopKey,\n accessToken: data.accessToken,\n clientKey: data.clientKey,\n instanceUrl: data.instanceUrl,\n fetch: this._fetch,\n });\n }\n\n async restore(): Promise<HappyViewSession | null> {\n const did = await this.storage.get(LAST_ACTIVE_KEY);\n if (!did) return null;\n return this.restoreSession(did);\n }\n}\n\n// PKCE helpers — inline since they're just a few lines of Web Crypto\nfunction generatePkceVerifier(): string {\n const bytes = crypto.getRandomValues(new Uint8Array(32));\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\nasync function computePkceChallenge(verifier: string): Promise<string> {\n const hash = await crypto.subtle.digest(\n \"SHA-256\",\n new TextEncoder().encode(verifier),\n );\n const bytes = new Uint8Array(hash);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n"],"mappings":"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","names":["body"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyview/oauth-client",
3
- "version": "1.0.0-dev.2",
3
+ "version": "1.0.1",
4
4
  "description": "HappyView OAuth client for ATProto DPoP authentication",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -8,6 +8,8 @@
8
8
  "url": "https://github.com/gamesgamesgamesgamesgames/happyview.git",
9
9
  "directory": "packages/oauth-client"
10
10
  },
11
+ "bugs": "https://github.com/gamesgamesgamesgamesgames/happyview/issues",
12
+ "homepage": "https://happyview.dev",
11
13
  "publishConfig": {
12
14
  "access": "public"
13
15
  },
@@ -30,6 +32,10 @@
30
32
  "test": "bun test",
31
33
  "typecheck": "tsc --noEmit"
32
34
  },
35
+ "dependencies": {
36
+ "@atproto/jwk": "^0.6.0",
37
+ "@atproto/jwk-webcrypto": "^0.2.0"
38
+ },
33
39
  "devDependencies": {
34
40
  "@semantic-release/commit-analyzer": "^13.0.1",
35
41
  "@semantic-release/github": "^12.0.6",