@happyview/oauth-client 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,365 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ApiError: () => ApiError,
24
+ AuthenticationError: () => AuthenticationError,
25
+ HappyViewError: () => HappyViewError,
26
+ HappyViewOAuthClient: () => HappyViewOAuthClient,
27
+ HappyViewSession: () => HappyViewSession,
28
+ InvalidStateError: () => InvalidStateError,
29
+ MemoryStorage: () => MemoryStorage,
30
+ ResolutionError: () => ResolutionError,
31
+ TokenExchangeError: () => TokenExchangeError,
32
+ base64urlEncode: () => base64urlEncode,
33
+ generateDpopProof: () => generateDpopProof
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/errors.ts
38
+ var HappyViewError = class extends Error {
39
+ constructor(message) {
40
+ super(message);
41
+ this.name = "HappyViewError";
42
+ }
43
+ };
44
+ var ApiError = class extends HappyViewError {
45
+ status;
46
+ body;
47
+ constructor(message, status, body) {
48
+ super(message);
49
+ this.name = "ApiError";
50
+ this.status = status;
51
+ this.body = body;
52
+ }
53
+ };
54
+ var AuthenticationError = class extends HappyViewError {
55
+ status;
56
+ constructor(message, status = 401) {
57
+ super(message);
58
+ this.name = "AuthenticationError";
59
+ this.status = status;
60
+ }
61
+ };
62
+ var InvalidStateError = class extends HappyViewError {
63
+ constructor(message) {
64
+ super(message);
65
+ this.name = "InvalidStateError";
66
+ }
67
+ };
68
+ var TokenExchangeError = class extends HappyViewError {
69
+ status;
70
+ body;
71
+ constructor(message, status, body) {
72
+ super(message);
73
+ this.name = "TokenExchangeError";
74
+ this.status = status;
75
+ this.body = body;
76
+ }
77
+ };
78
+ var ResolutionError = class extends HappyViewError {
79
+ constructor(message) {
80
+ super(message);
81
+ this.name = "ResolutionError";
82
+ }
83
+ };
84
+
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))
124
+ );
125
+ const payloadB64 = base64urlEncode(
126
+ new TextEncoder().encode(JSON.stringify(payload))
127
+ );
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}`;
132
+ }
133
+
134
+ // src/session.ts
135
+ var HappyViewSession = class {
136
+ did;
137
+ dpopKey;
138
+ accessToken;
139
+ clientKey;
140
+ instanceUrl;
141
+ crypto;
142
+ _fetch;
143
+ dpopNonce;
144
+ constructor(options) {
145
+ this.did = options.did;
146
+ this.dpopKey = options.dpopKey;
147
+ this.accessToken = options.accessToken;
148
+ this.clientKey = options.clientKey;
149
+ this.instanceUrl = options.instanceUrl.replace(/\/+$/, "");
150
+ this.crypto = options.crypto;
151
+ this._fetch = options.fetch ?? globalThis.fetch;
152
+ }
153
+ async fetchHandler(url, init) {
154
+ const fullUrl = /^https?:\/\//i.test(url) ? url : `${this.instanceUrl}${url}`;
155
+ 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
+ });
171
+ const nonce = response.headers.get("dpop-nonce");
172
+ if (nonce) {
173
+ this.dpopNonce = nonce;
174
+ }
175
+ return response;
176
+ }
177
+ };
178
+
179
+ // src/storage.ts
180
+ var MemoryStorage = class {
181
+ data = /* @__PURE__ */ new Map();
182
+ async get(key) {
183
+ return this.data.get(key) ?? null;
184
+ }
185
+ async set(key, value) {
186
+ this.data.set(key, value);
187
+ }
188
+ async delete(key) {
189
+ this.data.delete(key);
190
+ }
191
+ };
192
+
193
+ // src/client.ts
194
+ var STORAGE_PREFIX = "happyview:session:";
195
+ var LAST_ACTIVE_KEY = "happyview:last-active-did";
196
+ var HappyViewOAuthClient = class {
197
+ instanceUrl;
198
+ clientKey;
199
+ crypto;
200
+ storage;
201
+ clientSecret;
202
+ _fetch;
203
+ constructor(options) {
204
+ this.instanceUrl = options.instanceUrl.replace(/\/+$/, "");
205
+ this.clientKey = options.clientKey;
206
+ this.clientSecret = options.clientSecret;
207
+ this.crypto = options.crypto;
208
+ this.storage = options.storage ?? new MemoryStorage();
209
+ this._fetch = options.fetch ?? globalThis.fetch;
210
+ }
211
+ get isConfidential() {
212
+ return this.clientSecret !== void 0;
213
+ }
214
+ async provisionDpopKey() {
215
+ const headers = {
216
+ "content-type": "application/json",
217
+ "x-client-key": this.clientKey
218
+ };
219
+ if (this.clientSecret) {
220
+ headers["x-client-secret"] = this.clientSecret;
221
+ }
222
+ const body = {};
223
+ let pkceVerifier;
224
+ if (!this.isConfidential) {
225
+ pkceVerifier = await this.crypto.generatePkceVerifier();
226
+ const challenge = await this.crypto.computePkceChallenge(pkceVerifier);
227
+ body.pkce_challenge = challenge;
228
+ }
229
+ const resp = await this._fetch(`${this.instanceUrl}/oauth/dpop-keys`, {
230
+ method: "POST",
231
+ headers,
232
+ body: JSON.stringify(body)
233
+ });
234
+ if (!resp.ok) {
235
+ const body2 = await resp.json().catch(() => ({}));
236
+ throw new ApiError(
237
+ `Failed to provision DPoP key: ${resp.status} ${body2.message ?? resp.statusText}`,
238
+ resp.status,
239
+ body2
240
+ );
241
+ }
242
+ const data = await resp.json();
243
+ return {
244
+ provisionId: data.provision_id,
245
+ dpopKey: data.dpop_key,
246
+ pkceVerifier
247
+ };
248
+ }
249
+ async registerSession(params) {
250
+ const headers = {
251
+ "content-type": "application/json",
252
+ "x-client-key": this.clientKey
253
+ };
254
+ if (this.clientSecret) {
255
+ headers["x-client-secret"] = this.clientSecret;
256
+ }
257
+ const body = {
258
+ provision_id: params.provisionId,
259
+ did: params.did,
260
+ access_token: params.accessToken,
261
+ refresh_token: params.refreshToken,
262
+ scopes: params.scopes,
263
+ pds_url: params.pdsUrl,
264
+ issuer: params.issuer
265
+ };
266
+ if (!this.isConfidential && params.pkceVerifier) {
267
+ body.pkce_verifier = params.pkceVerifier;
268
+ }
269
+ const resp = await this._fetch(`${this.instanceUrl}/oauth/sessions`, {
270
+ method: "POST",
271
+ headers,
272
+ body: JSON.stringify(body)
273
+ });
274
+ if (!resp.ok) {
275
+ const body2 = await resp.json().catch(() => ({}));
276
+ throw new ApiError(
277
+ `Failed to register session: ${resp.status} ${body2.message ?? resp.statusText}`,
278
+ resp.status,
279
+ body2
280
+ );
281
+ }
282
+ const data = await resp.json();
283
+ const storedSession = {
284
+ did: data.did,
285
+ dpopKey: params.dpopKey,
286
+ accessToken: params.accessToken,
287
+ clientKey: this.clientKey,
288
+ instanceUrl: this.instanceUrl
289
+ };
290
+ await this.storage.set(
291
+ `${STORAGE_PREFIX}${data.did}`,
292
+ JSON.stringify(storedSession)
293
+ );
294
+ await this.storage.set(LAST_ACTIVE_KEY, data.did);
295
+ return new HappyViewSession({
296
+ did: data.did,
297
+ dpopKey: params.dpopKey,
298
+ accessToken: params.accessToken,
299
+ clientKey: this.clientKey,
300
+ instanceUrl: this.instanceUrl,
301
+ crypto: this.crypto
302
+ });
303
+ }
304
+ 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
324
+ );
325
+ }
326
+ await this.storage.delete(`${STORAGE_PREFIX}${did}`);
327
+ const lastActive = await this.storage.get(LAST_ACTIVE_KEY);
328
+ if (lastActive === did) {
329
+ await this.storage.delete(LAST_ACTIVE_KEY);
330
+ }
331
+ }
332
+ async restoreSession(did) {
333
+ const stored = await this.storage.get(`${STORAGE_PREFIX}${did}`);
334
+ if (!stored) return null;
335
+ const data = JSON.parse(stored);
336
+ return new HappyViewSession({
337
+ did: data.did,
338
+ dpopKey: data.dpopKey,
339
+ accessToken: data.accessToken,
340
+ clientKey: data.clientKey,
341
+ instanceUrl: data.instanceUrl,
342
+ crypto: this.crypto
343
+ });
344
+ }
345
+ async restore() {
346
+ const did = await this.storage.get(LAST_ACTIVE_KEY);
347
+ if (!did) return null;
348
+ return this.restoreSession(did);
349
+ }
350
+ };
351
+ // Annotate the CommonJS export names for ESM import in node:
352
+ 0 && (module.exports = {
353
+ ApiError,
354
+ AuthenticationError,
355
+ HappyViewError,
356
+ HappyViewOAuthClient,
357
+ HappyViewSession,
358
+ InvalidStateError,
359
+ MemoryStorage,
360
+ ResolutionError,
361
+ TokenExchangeError,
362
+ base64urlEncode,
363
+ generateDpopProof
364
+ });
365
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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"]}
@@ -0,0 +1,142 @@
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;
7
+ }
8
+ interface StorageAdapter {
9
+ get(key: string): Promise<string | null>;
10
+ set(key: string, value: string): Promise<void>;
11
+ delete(key: string): Promise<void>;
12
+ }
13
+ interface HappyViewOAuthClientOptions {
14
+ instanceUrl: string;
15
+ clientKey: string;
16
+ clientSecret?: string;
17
+ crypto: CryptoAdapter;
18
+ storage?: StorageAdapter;
19
+ }
20
+ interface DpopProvision {
21
+ provisionId: string;
22
+ dpopKey: JsonWebKey;
23
+ }
24
+ interface RegisterSessionParams {
25
+ provisionId: string;
26
+ pkceVerifier?: string;
27
+ did: string;
28
+ accessToken: string;
29
+ refreshToken?: string;
30
+ scopes: string;
31
+ pdsUrl?: string;
32
+ issuer?: string;
33
+ dpopKey: JsonWebKey;
34
+ }
35
+ /**
36
+ * Session data persisted to storage. Note: tokens and keys are stored as-is
37
+ * via the StorageAdapter. In browser environments using LocalStorageAdapter,
38
+ * this means they are accessible to any JS on the same origin. Consider using
39
+ * a StorageAdapter with encryption if XSS is a concern for your application.
40
+ */
41
+ interface StoredSession {
42
+ did: string;
43
+ dpopKey: JsonWebKey;
44
+ accessToken: string;
45
+ clientKey: string;
46
+ instanceUrl: string;
47
+ }
48
+ interface ProvisionKeyResponse {
49
+ provision_id: string;
50
+ dpop_key: JsonWebKey;
51
+ }
52
+ interface RegisterSessionResponse {
53
+ session_id: string;
54
+ did: string;
55
+ }
56
+
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
+ declare class HappyViewOAuthClient {
80
+ protected readonly instanceUrl: string;
81
+ protected readonly clientKey: string;
82
+ protected readonly crypto: CryptoAdapter;
83
+ protected readonly storage: StorageAdapter;
84
+ private readonly clientSecret;
85
+ private readonly _fetch;
86
+ constructor(options: HappyViewOAuthClientOptions & {
87
+ fetch?: typeof globalThis.fetch;
88
+ });
89
+ get isConfidential(): boolean;
90
+ provisionDpopKey(): Promise<{
91
+ provisionId: string;
92
+ dpopKey: JsonWebKey;
93
+ pkceVerifier?: string;
94
+ }>;
95
+ registerSession(params: RegisterSessionParams): Promise<HappyViewSession>;
96
+ deleteSession(did: string): Promise<void>;
97
+ restoreSession(did: string): Promise<HappyViewSession | null>;
98
+ restore(): Promise<HappyViewSession | null>;
99
+ }
100
+
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>;
110
+
111
+ declare class HappyViewError extends Error {
112
+ constructor(message: string);
113
+ }
114
+ declare class ApiError extends HappyViewError {
115
+ readonly status: number;
116
+ readonly body: unknown;
117
+ constructor(message: string, status: number, body?: unknown);
118
+ }
119
+ declare class AuthenticationError extends HappyViewError {
120
+ readonly status: number;
121
+ constructor(message: string, status?: number);
122
+ }
123
+ declare class InvalidStateError extends HappyViewError {
124
+ constructor(message: string);
125
+ }
126
+ declare class TokenExchangeError extends HappyViewError {
127
+ readonly status: number;
128
+ readonly body: string;
129
+ constructor(message: string, status: number, body: string);
130
+ }
131
+ declare class ResolutionError extends HappyViewError {
132
+ constructor(message: string);
133
+ }
134
+
135
+ declare class MemoryStorage implements StorageAdapter {
136
+ private data;
137
+ get(key: string): Promise<string | null>;
138
+ set(key: string, value: string): Promise<void>;
139
+ delete(key: string): Promise<void>;
140
+ }
141
+
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 };
@@ -0,0 +1,142 @@
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;
7
+ }
8
+ interface StorageAdapter {
9
+ get(key: string): Promise<string | null>;
10
+ set(key: string, value: string): Promise<void>;
11
+ delete(key: string): Promise<void>;
12
+ }
13
+ interface HappyViewOAuthClientOptions {
14
+ instanceUrl: string;
15
+ clientKey: string;
16
+ clientSecret?: string;
17
+ crypto: CryptoAdapter;
18
+ storage?: StorageAdapter;
19
+ }
20
+ interface DpopProvision {
21
+ provisionId: string;
22
+ dpopKey: JsonWebKey;
23
+ }
24
+ interface RegisterSessionParams {
25
+ provisionId: string;
26
+ pkceVerifier?: string;
27
+ did: string;
28
+ accessToken: string;
29
+ refreshToken?: string;
30
+ scopes: string;
31
+ pdsUrl?: string;
32
+ issuer?: string;
33
+ dpopKey: JsonWebKey;
34
+ }
35
+ /**
36
+ * Session data persisted to storage. Note: tokens and keys are stored as-is
37
+ * via the StorageAdapter. In browser environments using LocalStorageAdapter,
38
+ * this means they are accessible to any JS on the same origin. Consider using
39
+ * a StorageAdapter with encryption if XSS is a concern for your application.
40
+ */
41
+ interface StoredSession {
42
+ did: string;
43
+ dpopKey: JsonWebKey;
44
+ accessToken: string;
45
+ clientKey: string;
46
+ instanceUrl: string;
47
+ }
48
+ interface ProvisionKeyResponse {
49
+ provision_id: string;
50
+ dpop_key: JsonWebKey;
51
+ }
52
+ interface RegisterSessionResponse {
53
+ session_id: string;
54
+ did: string;
55
+ }
56
+
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
+ declare class HappyViewOAuthClient {
80
+ protected readonly instanceUrl: string;
81
+ protected readonly clientKey: string;
82
+ protected readonly crypto: CryptoAdapter;
83
+ protected readonly storage: StorageAdapter;
84
+ private readonly clientSecret;
85
+ private readonly _fetch;
86
+ constructor(options: HappyViewOAuthClientOptions & {
87
+ fetch?: typeof globalThis.fetch;
88
+ });
89
+ get isConfidential(): boolean;
90
+ provisionDpopKey(): Promise<{
91
+ provisionId: string;
92
+ dpopKey: JsonWebKey;
93
+ pkceVerifier?: string;
94
+ }>;
95
+ registerSession(params: RegisterSessionParams): Promise<HappyViewSession>;
96
+ deleteSession(did: string): Promise<void>;
97
+ restoreSession(did: string): Promise<HappyViewSession | null>;
98
+ restore(): Promise<HappyViewSession | null>;
99
+ }
100
+
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>;
110
+
111
+ declare class HappyViewError extends Error {
112
+ constructor(message: string);
113
+ }
114
+ declare class ApiError extends HappyViewError {
115
+ readonly status: number;
116
+ readonly body: unknown;
117
+ constructor(message: string, status: number, body?: unknown);
118
+ }
119
+ declare class AuthenticationError extends HappyViewError {
120
+ readonly status: number;
121
+ constructor(message: string, status?: number);
122
+ }
123
+ declare class InvalidStateError extends HappyViewError {
124
+ constructor(message: string);
125
+ }
126
+ declare class TokenExchangeError extends HappyViewError {
127
+ readonly status: number;
128
+ readonly body: string;
129
+ constructor(message: string, status: number, body: string);
130
+ }
131
+ declare class ResolutionError extends HappyViewError {
132
+ constructor(message: string);
133
+ }
134
+
135
+ declare class MemoryStorage implements StorageAdapter {
136
+ private data;
137
+ get(key: string): Promise<string | null>;
138
+ set(key: string, value: string): Promise<void>;
139
+ delete(key: string): Promise<void>;
140
+ }
141
+
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 };
package/dist/index.js ADDED
@@ -0,0 +1,328 @@
1
+ // src/errors.ts
2
+ var HappyViewError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "HappyViewError";
6
+ }
7
+ };
8
+ var ApiError = class extends HappyViewError {
9
+ status;
10
+ body;
11
+ constructor(message, status, body) {
12
+ super(message);
13
+ this.name = "ApiError";
14
+ this.status = status;
15
+ this.body = body;
16
+ }
17
+ };
18
+ var AuthenticationError = class extends HappyViewError {
19
+ status;
20
+ constructor(message, status = 401) {
21
+ super(message);
22
+ this.name = "AuthenticationError";
23
+ this.status = status;
24
+ }
25
+ };
26
+ var InvalidStateError = class extends HappyViewError {
27
+ constructor(message) {
28
+ super(message);
29
+ this.name = "InvalidStateError";
30
+ }
31
+ };
32
+ var TokenExchangeError = class extends HappyViewError {
33
+ status;
34
+ body;
35
+ constructor(message, status, body) {
36
+ super(message);
37
+ this.name = "TokenExchangeError";
38
+ this.status = status;
39
+ this.body = body;
40
+ }
41
+ };
42
+ var ResolutionError = class extends HappyViewError {
43
+ constructor(message) {
44
+ super(message);
45
+ this.name = "ResolutionError";
46
+ }
47
+ };
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))
88
+ );
89
+ const payloadB64 = base64urlEncode(
90
+ new TextEncoder().encode(JSON.stringify(payload))
91
+ );
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}`;
96
+ }
97
+
98
+ // src/session.ts
99
+ var HappyViewSession = class {
100
+ did;
101
+ dpopKey;
102
+ accessToken;
103
+ clientKey;
104
+ instanceUrl;
105
+ crypto;
106
+ _fetch;
107
+ dpopNonce;
108
+ constructor(options) {
109
+ this.did = options.did;
110
+ this.dpopKey = options.dpopKey;
111
+ this.accessToken = options.accessToken;
112
+ this.clientKey = options.clientKey;
113
+ this.instanceUrl = options.instanceUrl.replace(/\/+$/, "");
114
+ this.crypto = options.crypto;
115
+ this._fetch = options.fetch ?? globalThis.fetch;
116
+ }
117
+ async fetchHandler(url, init) {
118
+ const fullUrl = /^https?:\/\//i.test(url) ? url : `${this.instanceUrl}${url}`;
119
+ 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
+ });
135
+ const nonce = response.headers.get("dpop-nonce");
136
+ if (nonce) {
137
+ this.dpopNonce = nonce;
138
+ }
139
+ return response;
140
+ }
141
+ };
142
+
143
+ // src/storage.ts
144
+ var MemoryStorage = class {
145
+ data = /* @__PURE__ */ new Map();
146
+ async get(key) {
147
+ return this.data.get(key) ?? null;
148
+ }
149
+ async set(key, value) {
150
+ this.data.set(key, value);
151
+ }
152
+ async delete(key) {
153
+ this.data.delete(key);
154
+ }
155
+ };
156
+
157
+ // src/client.ts
158
+ var STORAGE_PREFIX = "happyview:session:";
159
+ var LAST_ACTIVE_KEY = "happyview:last-active-did";
160
+ var HappyViewOAuthClient = class {
161
+ instanceUrl;
162
+ clientKey;
163
+ crypto;
164
+ storage;
165
+ clientSecret;
166
+ _fetch;
167
+ constructor(options) {
168
+ this.instanceUrl = options.instanceUrl.replace(/\/+$/, "");
169
+ this.clientKey = options.clientKey;
170
+ this.clientSecret = options.clientSecret;
171
+ this.crypto = options.crypto;
172
+ this.storage = options.storage ?? new MemoryStorage();
173
+ this._fetch = options.fetch ?? globalThis.fetch;
174
+ }
175
+ get isConfidential() {
176
+ return this.clientSecret !== void 0;
177
+ }
178
+ async provisionDpopKey() {
179
+ const headers = {
180
+ "content-type": "application/json",
181
+ "x-client-key": this.clientKey
182
+ };
183
+ if (this.clientSecret) {
184
+ headers["x-client-secret"] = this.clientSecret;
185
+ }
186
+ const body = {};
187
+ let pkceVerifier;
188
+ if (!this.isConfidential) {
189
+ pkceVerifier = await this.crypto.generatePkceVerifier();
190
+ const challenge = await this.crypto.computePkceChallenge(pkceVerifier);
191
+ body.pkce_challenge = challenge;
192
+ }
193
+ const resp = await this._fetch(`${this.instanceUrl}/oauth/dpop-keys`, {
194
+ method: "POST",
195
+ headers,
196
+ body: JSON.stringify(body)
197
+ });
198
+ if (!resp.ok) {
199
+ const body2 = await resp.json().catch(() => ({}));
200
+ throw new ApiError(
201
+ `Failed to provision DPoP key: ${resp.status} ${body2.message ?? resp.statusText}`,
202
+ resp.status,
203
+ body2
204
+ );
205
+ }
206
+ const data = await resp.json();
207
+ return {
208
+ provisionId: data.provision_id,
209
+ dpopKey: data.dpop_key,
210
+ pkceVerifier
211
+ };
212
+ }
213
+ async registerSession(params) {
214
+ const headers = {
215
+ "content-type": "application/json",
216
+ "x-client-key": this.clientKey
217
+ };
218
+ if (this.clientSecret) {
219
+ headers["x-client-secret"] = this.clientSecret;
220
+ }
221
+ const body = {
222
+ provision_id: params.provisionId,
223
+ did: params.did,
224
+ access_token: params.accessToken,
225
+ refresh_token: params.refreshToken,
226
+ scopes: params.scopes,
227
+ pds_url: params.pdsUrl,
228
+ issuer: params.issuer
229
+ };
230
+ if (!this.isConfidential && params.pkceVerifier) {
231
+ body.pkce_verifier = params.pkceVerifier;
232
+ }
233
+ const resp = await this._fetch(`${this.instanceUrl}/oauth/sessions`, {
234
+ method: "POST",
235
+ headers,
236
+ body: JSON.stringify(body)
237
+ });
238
+ if (!resp.ok) {
239
+ const body2 = await resp.json().catch(() => ({}));
240
+ throw new ApiError(
241
+ `Failed to register session: ${resp.status} ${body2.message ?? resp.statusText}`,
242
+ resp.status,
243
+ body2
244
+ );
245
+ }
246
+ const data = await resp.json();
247
+ const storedSession = {
248
+ did: data.did,
249
+ dpopKey: params.dpopKey,
250
+ accessToken: params.accessToken,
251
+ clientKey: this.clientKey,
252
+ instanceUrl: this.instanceUrl
253
+ };
254
+ await this.storage.set(
255
+ `${STORAGE_PREFIX}${data.did}`,
256
+ JSON.stringify(storedSession)
257
+ );
258
+ await this.storage.set(LAST_ACTIVE_KEY, data.did);
259
+ return new HappyViewSession({
260
+ did: data.did,
261
+ dpopKey: params.dpopKey,
262
+ accessToken: params.accessToken,
263
+ clientKey: this.clientKey,
264
+ instanceUrl: this.instanceUrl,
265
+ crypto: this.crypto
266
+ });
267
+ }
268
+ 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
288
+ );
289
+ }
290
+ await this.storage.delete(`${STORAGE_PREFIX}${did}`);
291
+ const lastActive = await this.storage.get(LAST_ACTIVE_KEY);
292
+ if (lastActive === did) {
293
+ await this.storage.delete(LAST_ACTIVE_KEY);
294
+ }
295
+ }
296
+ async restoreSession(did) {
297
+ const stored = await this.storage.get(`${STORAGE_PREFIX}${did}`);
298
+ if (!stored) return null;
299
+ const data = JSON.parse(stored);
300
+ return new HappyViewSession({
301
+ did: data.did,
302
+ dpopKey: data.dpopKey,
303
+ accessToken: data.accessToken,
304
+ clientKey: data.clientKey,
305
+ instanceUrl: data.instanceUrl,
306
+ crypto: this.crypto
307
+ });
308
+ }
309
+ async restore() {
310
+ const did = await this.storage.get(LAST_ACTIVE_KEY);
311
+ if (!did) return null;
312
+ return this.restoreSession(did);
313
+ }
314
+ };
315
+ export {
316
+ ApiError,
317
+ AuthenticationError,
318
+ HappyViewError,
319
+ HappyViewOAuthClient,
320
+ HappyViewSession,
321
+ InvalidStateError,
322
+ MemoryStorage,
323
+ ResolutionError,
324
+ TokenExchangeError,
325
+ base64urlEncode,
326
+ generateDpopProof
327
+ };
328
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":";AAAO,IAAM,iBAAN,cAA6B,MAAM;AAAA,EACxC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,WAAN,cAAuB,eAAe;AAAA,EAClC;AAAA,EACA;AAAA,EAET,YAAY,SAAiB,QAAgB,MAAgB;AAC3D,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,sBAAN,cAAkC,eAAe;AAAA,EAC7C;AAAA,EAET,YAAY,SAAiB,SAAiB,KAAK;AACjD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAEO,IAAM,oBAAN,cAAgC,eAAe;AAAA,EACpD,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,qBAAN,cAAiC,eAAe;AAAA,EAC5C;AAAA,EACA;AAAA,EAET,YAAY,SAAiB,QAAgB,MAAc;AACzD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AACd,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,kBAAN,cAA8B,eAAe;AAAA,EAClD,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;;;ACnDO,SAAS,gBAAgB,MAA0B;AACxD,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;AACpC,cAAU,OAAO,aAAa,KAAK,CAAC,CAAC;AAAA,EACvC;AACA,SAAO,KAAK,MAAM,EACf,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,EAAE;AACtB;AAUA,eAAsB,kBACpB,QACA,QACiB;AACjB,QAAM,EAAE,YAAY,QAAQ,KAAK,aAAa,MAAM,IAAI;AAExD,QAAM,YAAY;AAAA,IAChB,KAAK,WAAW;AAAA,IAChB,KAAK,WAAW;AAAA,IAChB,GAAG,WAAW;AAAA,IACd,GAAG,WAAW;AAAA,EAChB;AAEA,QAAM,SAAS;AAAA,IACb,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,EACP;AAGA,QAAM,WAAW,OAAO,gBAAgB,EAAE;AAC1C,QAAM,MAAM,MAAM,KAAK,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAEhF,QAAM,UAAmC;AAAA,IACvC,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAAA,IACjC;AAAA,EACF;AAGA,MAAI,aAAa;AACf,UAAM,aAAa,IAAI,YAAY,EAAE,OAAO,WAAW;AACvD,UAAM,UAAU,MAAM,OAAO,OAAO,UAAU;AAC9C,YAAQ,MAAM,gBAAgB,OAAO;AAAA,EACvC;AAEA,MAAI,UAAU,QAAW;AACvB,YAAQ,QAAQ;AAAA,EAClB;AAEA,QAAM,YAAY;AAAA,IAChB,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,MAAM,CAAC;AAAA,EACjD;AACA,QAAM,aAAa;AAAA,IACjB,IAAI,YAAY,EAAE,OAAO,KAAK,UAAU,OAAO,CAAC;AAAA,EAClD;AAEA,QAAM,UAAU,IAAI,YAAY,EAAE,OAAO,GAAG,SAAS,IAAI,UAAU,EAAE;AACrE,QAAM,YAAY,MAAM,OAAO,UAAU,YAAY,OAAO;AAC5D,QAAM,SAAS,gBAAgB,SAAS;AAExC,SAAO,GAAG,SAAS,IAAI,UAAU,IAAI,MAAM;AAC7C;;;AC7DO,IAAM,mBAAN,MAAuB;AAAA,EACnB;AAAA,EAEQ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACT;AAAA,EAER,YAAY,SAAkC;AAC5C,SAAK,MAAM,QAAQ;AACnB,SAAK,UAAU,QAAQ;AACvB,SAAK,cAAc,QAAQ;AAC3B,SAAK,YAAY,QAAQ;AACzB,SAAK,cAAc,QAAQ,YAAY,QAAQ,QAAQ,EAAE;AACzD,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,QAAQ,SAAS,WAAW;AAAA,EAC5C;AAAA,EAEA,MAAM,aAAa,KAAa,MAAsC;AACpE,UAAM,UAAU,gBAAgB,KAAK,GAAG,IAAI,MAAM,GAAG,KAAK,WAAW,GAAG,GAAG;AAC3E,UAAM,UAAU,KAAK,UAAU,OAAO,YAAY;AAElD,UAAM,QAAQ,MAAM,kBAAkB,KAAK,QAAQ;AAAA,MACjD,YAAY,KAAK;AAAA,MACjB;AAAA,MACA,KAAK;AAAA,MACL,aAAa,KAAK;AAAA,MAClB,OAAO,KAAK;AAAA,IACd,CAAC;AAED,UAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AACxC,YAAQ,IAAI,iBAAiB,QAAQ,KAAK,WAAW,EAAE;AACvD,YAAQ,IAAI,QAAQ,KAAK;AACzB,YAAQ,IAAI,gBAAgB,KAAK,SAAS;AAE1C,UAAM,WAAW,MAAM,KAAK,OAAO,SAAS;AAAA,MAC1C,GAAG;AAAA,MACH;AAAA,IACF,CAAC;AAED,UAAM,QAAQ,SAAS,QAAQ,IAAI,YAAY;AAC/C,QAAI,OAAO;AACT,WAAK,YAAY;AAAA,IACnB;AAEA,WAAO;AAAA,EACT;AACF;;;AC7DO,IAAM,gBAAN,MAA8C;AAAA,EAC3C,OAAO,oBAAI,IAAoB;AAAA,EAEvC,MAAM,IAAI,KAAqC;AAC7C,WAAO,KAAK,KAAK,IAAI,GAAG,KAAK;AAAA,EAC/B;AAAA,EAEA,MAAM,IAAI,KAAa,OAA8B;AACnD,SAAK,KAAK,IAAI,KAAK,KAAK;AAAA,EAC1B;AAAA,EAEA,MAAM,OAAO,KAA4B;AACvC,SAAK,KAAK,OAAO,GAAG;AAAA,EACtB;AACF;;;ACHA,IAAM,iBAAiB;AACvB,IAAM,kBAAkB;AAEjB,IAAM,uBAAN,MAA2B;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACF;AAAA,EACA;AAAA,EAEjB,YACE,SAGA;AACA,SAAK,cAAc,QAAQ,YAAY,QAAQ,QAAQ,EAAE;AACzD,SAAK,YAAY,QAAQ;AACzB,SAAK,eAAe,QAAQ;AAC5B,SAAK,SAAS,QAAQ;AACtB,SAAK,UAAU,QAAQ,WAAW,IAAI,cAAc;AACpD,SAAK,SAAS,QAAQ,SAAS,WAAW;AAAA,EAC5C;AAAA,EAEA,IAAI,iBAA0B;AAC5B,WAAO,KAAK,iBAAiB;AAAA,EAC/B;AAAA,EAEA,MAAM,mBAIH;AACD,UAAM,UAAkC;AAAA,MACtC,gBAAgB;AAAA,MAChB,gBAAgB,KAAK;AAAA,IACvB;AACA,QAAI,KAAK,cAAc;AACrB,cAAQ,iBAAiB,IAAI,KAAK;AAAA,IACpC;AAEA,UAAM,OAAgC,CAAC;AACvC,QAAI;AAEJ,QAAI,CAAC,KAAK,gBAAgB;AACxB,qBAAe,MAAM,KAAK,OAAO,qBAAqB;AACtD,YAAM,YAAY,MAAM,KAAK,OAAO,qBAAqB,YAAY;AACrE,WAAK,iBAAiB;AAAA,IACxB;AAEA,UAAM,OAAO,MAAM,KAAK,OAAO,GAAG,KAAK,WAAW,oBAAoB;AAAA,MACpE,QAAQ;AAAA,MACR;AAAA,MACA,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B,CAAC;AAED,QAAI,CAAC,KAAK,IAAI;AACZ,YAAMA,QAAO,MAAM,KAAK,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC/C,YAAM,IAAI;AAAA,QACR,iCAAiC,KAAK,MAAM,IAAKA,MAAa,WAAW,KAAK,UAAU;AAAA,QACxF,KAAK;AAAA,QACLA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,OAA6B,MAAM,KAAK,KAAK;AACnD,WAAO;AAAA,MACL,aAAa,KAAK;AAAA,MAClB,SAAS,KAAK;AAAA,MACd;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,gBACJ,QAC2B;AAC3B,UAAM,UAAkC;AAAA,MACtC,gBAAgB;AAAA,MAChB,gBAAgB,KAAK;AAAA,IACvB;AACA,QAAI,KAAK,cAAc;AACrB,cAAQ,iBAAiB,IAAI,KAAK;AAAA,IACpC;AAEA,UAAM,OAAgC;AAAA,MACpC,cAAc,OAAO;AAAA,MACrB,KAAK,OAAO;AAAA,MACZ,cAAc,OAAO;AAAA,MACrB,eAAe,OAAO;AAAA,MACtB,QAAQ,OAAO;AAAA,MACf,SAAS,OAAO;AAAA,MAChB,QAAQ,OAAO;AAAA,IACjB;AAEA,QAAI,CAAC,KAAK,kBAAkB,OAAO,cAAc;AAC/C,WAAK,gBAAgB,OAAO;AAAA,IAC9B;AAEA,UAAM,OAAO,MAAM,KAAK,OAAO,GAAG,KAAK,WAAW,mBAAmB;AAAA,MACnE,QAAQ;AAAA,MACR;AAAA,MACA,MAAM,KAAK,UAAU,IAAI;AAAA,IAC3B,CAAC;AAED,QAAI,CAAC,KAAK,IAAI;AACZ,YAAMA,QAAO,MAAM,KAAK,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC/C,YAAM,IAAI;AAAA,QACR,+BAA+B,KAAK,MAAM,IAAKA,MAAa,WAAW,KAAK,UAAU;AAAA,QACtF,KAAK;AAAA,QACLA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,OAAgC,MAAM,KAAK,KAAK;AAEtD,UAAM,gBAA+B;AAAA,MACnC,KAAK,KAAK;AAAA,MACV,SAAS,OAAO;AAAA,MAChB,aAAa,OAAO;AAAA,MACpB,WAAW,KAAK;AAAA,MAChB,aAAa,KAAK;AAAA,IACpB;AACA,UAAM,KAAK,QAAQ;AAAA,MACjB,GAAG,cAAc,GAAG,KAAK,GAAG;AAAA,MAC5B,KAAK,UAAU,aAAa;AAAA,IAC9B;AACA,UAAM,KAAK,QAAQ,IAAI,iBAAiB,KAAK,GAAG;AAEhD,WAAO,IAAI,iBAAiB;AAAA,MAC1B,KAAK,KAAK;AAAA,MACV,SAAS,OAAO;AAAA,MAChB,aAAa,OAAO;AAAA,MACpB,WAAW,KAAK;AAAA,MAChB,aAAa,KAAK;AAAA,MAClB,QAAQ,KAAK;AAAA,IACf,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,cAAc,KAA4B;AAC9C,UAAM,UAAkC;AAAA,MACtC,gBAAgB,KAAK;AAAA,IACvB;AACA,QAAI,KAAK,cAAc;AACrB,cAAQ,iBAAiB,IAAI,KAAK;AAAA,IACpC;AAEA,UAAM,OAAO,MAAM,KAAK;AAAA,MACtB,GAAG,KAAK,WAAW,mBAAmB,GAAG;AAAA,MACzC;AAAA,QACE,QAAQ;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,MAAM,KAAK,WAAW,KAAK;AACnC,YAAM,OAAO,MAAM,KAAK,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC/C,YAAM,IAAI;AAAA,QACR,6BAA6B,KAAK,MAAM,IAAK,KAAa,WAAW,KAAK,UAAU;AAAA,QACpF,KAAK;AAAA,QACL;AAAA,MACF;AAAA,IACF;AAEA,UAAM,KAAK,QAAQ,OAAO,GAAG,cAAc,GAAG,GAAG,EAAE;AAEnD,UAAM,aAAa,MAAM,KAAK,QAAQ,IAAI,eAAe;AACzD,QAAI,eAAe,KAAK;AACtB,YAAM,KAAK,QAAQ,OAAO,eAAe;AAAA,IAC3C;AAAA,EACF;AAAA,EAEA,MAAM,eAAe,KAA+C;AAClE,UAAM,SAAS,MAAM,KAAK,QAAQ,IAAI,GAAG,cAAc,GAAG,GAAG,EAAE;AAC/D,QAAI,CAAC,OAAQ,QAAO;AAEpB,UAAM,OAAsB,KAAK,MAAM,MAAM;AAC7C,WAAO,IAAI,iBAAiB;AAAA,MAC1B,KAAK,KAAK;AAAA,MACV,SAAS,KAAK;AAAA,MACd,aAAa,KAAK;AAAA,MAClB,WAAW,KAAK;AAAA,MAChB,aAAa,KAAK;AAAA,MAClB,QAAQ,KAAK;AAAA,IACf,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,UAA4C;AAChD,UAAM,MAAM,MAAM,KAAK,QAAQ,IAAI,eAAe;AAClD,QAAI,CAAC,IAAK,QAAO;AACjB,WAAO,KAAK,eAAe,GAAG;AAAA,EAChC;AACF;","names":["body"]}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@happyview/oauth-client",
3
+ "version": "1.0.0",
4
+ "description": "HappyView OAuth client for ATProto DPoP authentication",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/gamesgamesgamesgamesgames/happyview.git",
9
+ "directory": "packages/oauth-client"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "type": "module",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js",
19
+ "require": "./dist/index.cjs"
20
+ }
21
+ },
22
+ "main": "./dist/index.cjs",
23
+ "module": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "files": ["dist"],
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "test": "bun test",
29
+ "typecheck": "tsc --noEmit"
30
+ },
31
+ "devDependencies": {
32
+ "@semantic-release/commit-analyzer": "^13.0.1",
33
+ "@semantic-release/github": "^12.0.6",
34
+ "@semantic-release/npm": "^13.1.5",
35
+ "@semantic-release/release-notes-generator": "^14.1.0",
36
+ "@types/bun": "^1.3.12",
37
+ "semantic-release": "^25.0.3",
38
+ "semantic-release-monorepo": "^8.0.2",
39
+ "tsup": "^8.0.0",
40
+ "typescript": "^5.7.0"
41
+ }
42
+ }