@pulseid/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @pulseid/client
2
+
3
+ TypeScript SDK for [PULSE ID](https://id.pulserunning.at).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @pulseid/client
9
+ ```
10
+
11
+ ## Server (Recommended)
12
+
13
+ ```typescript
14
+ import { PulseIdServer } from '@pulseid/client/server';
15
+
16
+ const server = new PulseIdServer({
17
+ issuer: 'https://id.pulserunning.at',
18
+ clientId: process.env.PULSEID_CLIENT_ID!,
19
+ clientSecret: process.env.PULSEID_CLIENT_SECRET!,
20
+ storage: tokenStorage,
21
+ });
22
+
23
+ // Get a user-scoped client with auto-refresh
24
+ const pulse = server.forUser(userId);
25
+ const profile = await pulse.profile.get();
26
+ await pulse.profile.update({ displayName: 'New Name' });
27
+ ```
28
+
29
+ ## Browser
30
+
31
+ ```typescript
32
+ import { PulseIdClient } from '@pulseid/client';
33
+
34
+ const client = new PulseIdClient({
35
+ issuer: 'https://id.pulserunning.at',
36
+ clientId: 'your-client-id',
37
+ });
38
+
39
+ // Build auth URL
40
+ const authUrl = client.buildAuthorizationUrl({
41
+ redirectUri: 'https://yourapp.com/callback',
42
+ scope: 'openid profile email offline_access',
43
+ state: crypto.randomUUID(),
44
+ nonce: crypto.randomUUID(),
45
+ codeChallenge,
46
+ codeChallengeMethod: 'S256',
47
+ });
48
+
49
+ // After callback
50
+ const profile = await client.getProfile(accessToken);
51
+ ```
52
+
53
+ ## Token Storage
54
+
55
+ ```typescript
56
+ import type { TokenStorage, StoredTokens } from '@pulseid/client/server';
57
+
58
+ const tokenStorage: TokenStorage = {
59
+ async getTokens(userId) { /* fetch from db */ },
60
+ async setTokens(userId, tokens) { /* save to db */ },
61
+ async deleteTokens(userId) { /* delete from db */ },
62
+ };
63
+ ```
64
+
65
+ ## Error Handling
66
+
67
+ ```typescript
68
+ import { InvalidTokenError, InsufficientScopeError } from '@pulseid/client';
69
+
70
+ try {
71
+ await pulse.profile.get();
72
+ } catch (error) {
73
+ if (error instanceof InvalidTokenError) redirect('/login');
74
+ if (error instanceof InsufficientScopeError) console.log(error.requiredScope);
75
+ throw error;
76
+ }
77
+ ```
78
+
79
+ ## Docs
80
+
81
+ See [docs](./docs) or run `pnpm docs:dev`.
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,406 @@
1
+ // src/errors.ts
2
+ var PulseIdError = class _PulseIdError extends Error {
3
+ code;
4
+ statusCode;
5
+ constructor(code, message, statusCode = 400) {
6
+ super(message);
7
+ this.name = "PulseIdError";
8
+ this.code = code;
9
+ this.statusCode = statusCode;
10
+ if (Error.captureStackTrace) {
11
+ Error.captureStackTrace(this, _PulseIdError);
12
+ }
13
+ }
14
+ /**
15
+ * Create an error from an API response.
16
+ */
17
+ static fromResponse(response, statusCode) {
18
+ const code = mapErrorCode(response.error);
19
+ switch (code) {
20
+ case "invalid_token":
21
+ case "expired_token":
22
+ return new InvalidTokenError(response.error_description);
23
+ case "insufficient_scope":
24
+ return new InsufficientScopeError(
25
+ response.error_description,
26
+ response.required_scope
27
+ );
28
+ case "not_found":
29
+ return new NotFoundError(response.error_description);
30
+ default:
31
+ return new _PulseIdError(code, response.error_description, statusCode);
32
+ }
33
+ }
34
+ };
35
+ var InvalidTokenError = class extends PulseIdError {
36
+ constructor(message = "Invalid or expired access token") {
37
+ super("invalid_token", message, 401);
38
+ this.name = "InvalidTokenError";
39
+ }
40
+ };
41
+ var InsufficientScopeError = class extends PulseIdError {
42
+ requiredScope;
43
+ constructor(message, requiredScope) {
44
+ super("insufficient_scope", message, 403);
45
+ this.name = "InsufficientScopeError";
46
+ this.requiredScope = requiredScope;
47
+ }
48
+ };
49
+ var NotFoundError = class extends PulseIdError {
50
+ constructor(message = "Resource not found") {
51
+ super("not_found", message, 404);
52
+ this.name = "NotFoundError";
53
+ }
54
+ };
55
+ var NetworkError = class extends PulseIdError {
56
+ cause;
57
+ constructor(message, cause) {
58
+ super("server_error", message, 0);
59
+ this.name = "NetworkError";
60
+ this.cause = cause;
61
+ }
62
+ };
63
+ var TimeoutError = class extends PulseIdError {
64
+ constructor(message = "Request timed out") {
65
+ super("server_error", message, 0);
66
+ this.name = "TimeoutError";
67
+ }
68
+ };
69
+ function mapErrorCode(apiError) {
70
+ switch (apiError) {
71
+ case "invalid_token":
72
+ return "invalid_token";
73
+ case "expired_token":
74
+ return "expired_token";
75
+ case "insufficient_scope":
76
+ return "insufficient_scope";
77
+ case "not_found":
78
+ return "not_found";
79
+ case "invalid_request":
80
+ return "invalid_request";
81
+ default:
82
+ return "server_error";
83
+ }
84
+ }
85
+
86
+ // src/http.ts
87
+ var DEFAULT_TIMEOUT = 3e4;
88
+ function validateIssuerUrl(issuer) {
89
+ let url;
90
+ try {
91
+ url = new URL(issuer);
92
+ } catch {
93
+ throw new Error(`Invalid issuer URL: ${issuer}`);
94
+ }
95
+ if (url.username || url.password) {
96
+ throw new Error(`Issuer URL must not include credentials: ${issuer}`);
97
+ }
98
+ const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
99
+ if (url.protocol !== "https:" && !isLocalhost) {
100
+ throw new Error(`Issuer URL must use HTTPS: ${issuer}`);
101
+ }
102
+ return url.origin + url.pathname.replace(/\/$/, "");
103
+ }
104
+ function createHttpClient(config) {
105
+ const baseUrl = validateIssuerUrl(config.issuer);
106
+ const fetchFn = config.fetch ?? globalThis.fetch;
107
+ const timeout = config.timeout ?? DEFAULT_TIMEOUT;
108
+ async function request(path, options) {
109
+ const url = `${baseUrl}${path}`;
110
+ const controller = new AbortController();
111
+ const timeoutId = setTimeout(() => {
112
+ controller.abort();
113
+ }, options.timeout ?? timeout);
114
+ try {
115
+ const response = await fetchFn(url, {
116
+ method: options.method,
117
+ headers: {
118
+ "Content-Type": "application/json",
119
+ Accept: "application/json",
120
+ ...options.headers
121
+ },
122
+ ...options.body !== void 0 && { body: JSON.stringify(options.body) },
123
+ signal: controller.signal
124
+ });
125
+ clearTimeout(timeoutId);
126
+ const contentType = response.headers.get("content-type");
127
+ const isJson = contentType?.includes("application/json");
128
+ if (!response.ok) {
129
+ if (isJson) {
130
+ const errorBody = await response.json();
131
+ throw PulseIdError.fromResponse(errorBody, response.status);
132
+ }
133
+ const errorText = await response.text();
134
+ throw new PulseIdError(
135
+ "server_error",
136
+ errorText || `HTTP ${response.status}`,
137
+ response.status
138
+ );
139
+ }
140
+ if (response.status === 204 || !isJson) {
141
+ return {};
142
+ }
143
+ return await response.json();
144
+ } catch (error) {
145
+ clearTimeout(timeoutId);
146
+ if (error instanceof PulseIdError) {
147
+ throw error;
148
+ }
149
+ if (error instanceof DOMException && error.name === "AbortError") {
150
+ throw new TimeoutError(`Request to ${path} timed out after ${timeout}ms`);
151
+ }
152
+ if (error instanceof TypeError) {
153
+ throw new NetworkError(`Network request to ${path} failed`, error);
154
+ }
155
+ throw new NetworkError(
156
+ `Unknown error during request to ${path}`,
157
+ error instanceof Error ? error : void 0
158
+ );
159
+ }
160
+ }
161
+ return {
162
+ /**
163
+ * Make a GET request.
164
+ */
165
+ get(path, headers) {
166
+ return request(path, { method: "GET", ...headers && { headers } });
167
+ },
168
+ /**
169
+ * Make a POST request.
170
+ */
171
+ post(path, body, headers) {
172
+ return request(path, { method: "POST", body, ...headers && { headers } });
173
+ },
174
+ /**
175
+ * Make a PATCH request.
176
+ */
177
+ patch(path, body, headers) {
178
+ return request(path, { method: "PATCH", body, ...headers && { headers } });
179
+ },
180
+ /**
181
+ * Make a DELETE request.
182
+ */
183
+ delete(path, headers) {
184
+ return request(path, { method: "DELETE", ...headers && { headers } });
185
+ },
186
+ /**
187
+ * Make a form-encoded POST request (for OAuth token endpoints).
188
+ */
189
+ async postForm(path, data, headers) {
190
+ const url = `${baseUrl}${path}`;
191
+ const controller = new AbortController();
192
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
193
+ try {
194
+ const response = await fetchFn(url, {
195
+ method: "POST",
196
+ headers: {
197
+ "Content-Type": "application/x-www-form-urlencoded",
198
+ Accept: "application/json",
199
+ ...headers
200
+ },
201
+ body: new URLSearchParams(data).toString(),
202
+ signal: controller.signal
203
+ });
204
+ clearTimeout(timeoutId);
205
+ const contentType = response.headers.get("content-type");
206
+ const isJson = contentType?.includes("application/json");
207
+ if (!response.ok) {
208
+ if (isJson) {
209
+ const errorBody = await response.json();
210
+ throw PulseIdError.fromResponse(errorBody, response.status);
211
+ }
212
+ const errorText = await response.text();
213
+ throw new PulseIdError(
214
+ "server_error",
215
+ errorText || `HTTP ${response.status}`,
216
+ response.status
217
+ );
218
+ }
219
+ return await response.json();
220
+ } catch (error) {
221
+ clearTimeout(timeoutId);
222
+ if (error instanceof PulseIdError) throw error;
223
+ if (error instanceof DOMException && error.name === "AbortError") {
224
+ throw new TimeoutError();
225
+ }
226
+ throw new NetworkError(
227
+ `Request to ${path} failed`,
228
+ error instanceof Error ? error : void 0
229
+ );
230
+ }
231
+ }
232
+ };
233
+ }
234
+
235
+ // src/client.ts
236
+ var PulseIdClient = class {
237
+ http;
238
+ config;
239
+ constructor(config) {
240
+ this.config = config;
241
+ this.http = createHttpClient(config);
242
+ }
243
+ // ===========================================================================
244
+ // PROFILE
245
+ // ===========================================================================
246
+ /**
247
+ * Get the authenticated user's profile.
248
+ *
249
+ * The fields returned depend on the scopes granted to the access token:
250
+ * - `profile`: name, avatar, birthday, etc.
251
+ * - `email`: email address and verification status
252
+ * - `address`: address information
253
+ * - `phone`: phone number
254
+ *
255
+ * @param accessToken - A valid access token
256
+ * @returns The user's profile
257
+ *
258
+ * @example
259
+ * ```typescript
260
+ * const profile = await client.getProfile(accessToken);
261
+ * console.log(`Hello, ${profile.displayName}!`);
262
+ * ```
263
+ */
264
+ async getProfile(accessToken) {
265
+ return this.http.get("/api/v1/me", {
266
+ Authorization: `Bearer ${accessToken}`
267
+ });
268
+ }
269
+ /**
270
+ * Update the authenticated user's profile.
271
+ *
272
+ * Requires the `profile:write` scope.
273
+ *
274
+ * @param accessToken - A valid access token with `profile:write` scope
275
+ * @param data - The profile fields to update
276
+ * @returns The updated profile
277
+ *
278
+ * @example
279
+ * ```typescript
280
+ * const updated = await client.updateProfile(accessToken, {
281
+ * displayName: 'Max Runner',
282
+ * height: 180,
283
+ * });
284
+ * ```
285
+ */
286
+ async updateProfile(accessToken, data) {
287
+ return this.http.patch("/api/v1/me", data, {
288
+ Authorization: `Bearer ${accessToken}`
289
+ });
290
+ }
291
+ /**
292
+ * Get the authenticated user's OIDC userinfo.
293
+ *
294
+ * Requires the `openid` scope. Additional fields depend on granted scopes.
295
+ *
296
+ * @param accessToken - A valid access token
297
+ * @returns The user's OIDC userinfo
298
+ *
299
+ * @example
300
+ * ```typescript
301
+ * const info = await client.getUserInfo(accessToken);
302
+ * console.log(info.sub);
303
+ * ```
304
+ */
305
+ async getUserInfo(accessToken) {
306
+ return this.http.get("/userinfo", {
307
+ Authorization: `Bearer ${accessToken}`
308
+ });
309
+ }
310
+ // ===========================================================================
311
+ // UTILITY METHODS
312
+ // ===========================================================================
313
+ /**
314
+ * Get the configured issuer URL.
315
+ */
316
+ get issuer() {
317
+ return this.config.issuer;
318
+ }
319
+ /**
320
+ * Get the configured client ID.
321
+ */
322
+ get clientId() {
323
+ return this.config.clientId;
324
+ }
325
+ /**
326
+ * Build an authorization URL for the OAuth flow.
327
+ *
328
+ * @param options - Authorization options
329
+ * @returns The authorization URL to redirect the user to
330
+ *
331
+ * @example
332
+ * ```typescript
333
+ * const authUrl = client.buildAuthorizationUrl({
334
+ * redirectUri: 'https://myapp.com/callback',
335
+ * scope: 'openid profile email',
336
+ * state: 'random-state-string',
337
+ * });
338
+ * window.location.href = authUrl;
339
+ * ```
340
+ */
341
+ buildAuthorizationUrl(options) {
342
+ if (options.codeChallengeMethod && options.codeChallengeMethod !== "S256") {
343
+ throw new Error("code_challenge_method must be S256");
344
+ }
345
+ if (!options.state) {
346
+ throw new Error("state is required for authorization requests");
347
+ }
348
+ if (!options.codeChallenge) {
349
+ throw new Error("code_challenge is required for authorization requests");
350
+ }
351
+ const scopes = options.scope.split(" ").filter(Boolean);
352
+ if (scopes.includes("openid") && !options.nonce) {
353
+ throw new Error("nonce is required when requesting openid scope");
354
+ }
355
+ const url = new URL(`${this.config.issuer}/authorize`);
356
+ url.searchParams.set("response_type", "code");
357
+ url.searchParams.set("client_id", this.config.clientId);
358
+ url.searchParams.set("redirect_uri", options.redirectUri);
359
+ url.searchParams.set("scope", options.scope);
360
+ url.searchParams.set("state", options.state);
361
+ if (options.nonce) {
362
+ url.searchParams.set("nonce", options.nonce);
363
+ }
364
+ url.searchParams.set("code_challenge", options.codeChallenge);
365
+ url.searchParams.set("code_challenge_method", options.codeChallengeMethod ?? "S256");
366
+ if (options.prompt) {
367
+ url.searchParams.set("prompt", options.prompt);
368
+ }
369
+ if (options.loginHint) {
370
+ url.searchParams.set("login_hint", options.loginHint);
371
+ }
372
+ return url.toString();
373
+ }
374
+ /**
375
+ * Build a logout URL for ending the session.
376
+ *
377
+ * @param options - Logout options
378
+ * @returns The logout URL to redirect the user to
379
+ *
380
+ * @example
381
+ * ```typescript
382
+ * const logoutUrl = client.buildLogoutUrl({
383
+ * idTokenHint: idToken,
384
+ * postLogoutRedirectUri: 'https://myapp.com',
385
+ * });
386
+ * window.location.href = logoutUrl;
387
+ * ```
388
+ */
389
+ buildLogoutUrl(options) {
390
+ const url = new URL(`${this.config.issuer}/logout`);
391
+ if (options?.idTokenHint) {
392
+ url.searchParams.set("id_token_hint", options.idTokenHint);
393
+ }
394
+ if (options?.postLogoutRedirectUri) {
395
+ url.searchParams.set("post_logout_redirect_uri", options.postLogoutRedirectUri);
396
+ }
397
+ if (options?.state) {
398
+ url.searchParams.set("state", options.state);
399
+ }
400
+ return url.toString();
401
+ }
402
+ };
403
+
404
+ export { InsufficientScopeError, InvalidTokenError, NetworkError, NotFoundError, PulseIdClient, PulseIdError, TimeoutError };
405
+ //# sourceMappingURL=chunk-BRQ2T53Z.js.map
406
+ //# sourceMappingURL=chunk-BRQ2T53Z.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/http.ts","../src/client.ts"],"names":[],"mappings":"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","file":"chunk-BRQ2T53Z.js","sourcesContent":["/**\n * PULSE ID SDK Errors\n *\n * Custom error classes for better error handling.\n */\n\nimport type { ApiErrorResponse, ErrorCode } from './types.js';\n\n/**\n * Base error class for PULSE ID SDK errors.\n */\nexport class PulseIdError extends Error {\n readonly code: ErrorCode;\n readonly statusCode: number;\n\n constructor(code: ErrorCode, message: string, statusCode = 400) {\n super(message);\n this.name = 'PulseIdError';\n this.code = code;\n this.statusCode = statusCode;\n\n // Maintains proper stack trace in V8 environments\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, PulseIdError);\n }\n }\n\n /**\n * Create an error from an API response.\n */\n static fromResponse(response: ApiErrorResponse, statusCode: number): PulseIdError {\n const code = mapErrorCode(response.error);\n\n switch (code) {\n case 'invalid_token':\n case 'expired_token':\n return new InvalidTokenError(response.error_description);\n case 'insufficient_scope':\n return new InsufficientScopeError(\n response.error_description,\n response.required_scope\n );\n case 'not_found':\n return new NotFoundError(response.error_description);\n default:\n return new PulseIdError(code, response.error_description, statusCode);\n }\n }\n}\n\n/**\n * Error thrown when the access token is invalid or expired.\n */\nexport class InvalidTokenError extends PulseIdError {\n constructor(message = 'Invalid or expired access token') {\n super('invalid_token', message, 401);\n this.name = 'InvalidTokenError';\n }\n}\n\n/**\n * Error thrown when the access token lacks required scopes.\n */\nexport class InsufficientScopeError extends PulseIdError {\n readonly requiredScope: string | undefined;\n\n constructor(message: string, requiredScope?: string) {\n super('insufficient_scope', message, 403);\n this.name = 'InsufficientScopeError';\n this.requiredScope = requiredScope;\n }\n}\n\n/**\n * Error thrown when a resource is not found.\n */\nexport class NotFoundError extends PulseIdError {\n constructor(message = 'Resource not found') {\n super('not_found', message, 404);\n this.name = 'NotFoundError';\n }\n}\n\n/**\n * Error thrown when a network request fails.\n */\nexport class NetworkError extends PulseIdError {\n readonly cause: Error | undefined;\n\n constructor(message: string, cause?: Error) {\n super('server_error', message, 0);\n this.name = 'NetworkError';\n this.cause = cause;\n }\n}\n\n/**\n * Error thrown when a request times out.\n */\nexport class TimeoutError extends PulseIdError {\n constructor(message = 'Request timed out') {\n super('server_error', message, 0);\n this.name = 'TimeoutError';\n }\n}\n\n/**\n * Map API error codes to SDK error codes.\n */\nfunction mapErrorCode(apiError: string): ErrorCode {\n switch (apiError) {\n case 'invalid_token':\n return 'invalid_token';\n case 'expired_token':\n return 'expired_token';\n case 'insufficient_scope':\n return 'insufficient_scope';\n case 'not_found':\n return 'not_found';\n case 'invalid_request':\n return 'invalid_request';\n default:\n return 'server_error';\n }\n}\n","/**\n * HTTP Client Utilities\n *\n * Internal utilities for making HTTP requests with proper error handling.\n */\n\nimport { NetworkError, PulseIdError, TimeoutError } from './errors.js';\nimport type { ApiErrorResponse, PulseIdConfig } from './types.js';\n\nconst DEFAULT_TIMEOUT = 30_000; // 30 seconds\n\ntype HttpMethod = 'GET' | 'POST' | 'PATCH' | 'DELETE';\n\ntype RequestOptions = {\n method: HttpMethod;\n headers?: Record<string, string>;\n body?: unknown;\n timeout?: number;\n};\n\n/**\n * Validate that a URL is a valid HTTPS URL.\n * Prevents SSRF and ensures secure communication.\n */\nfunction validateIssuerUrl(issuer: string): string {\n let url: URL;\n try {\n url = new URL(issuer);\n } catch {\n throw new Error(`Invalid issuer URL: ${issuer}`);\n }\n\n if (url.username || url.password) {\n throw new Error(`Issuer URL must not include credentials: ${issuer}`);\n }\n\n // Only allow HTTPS in production (allow HTTP for localhost in development)\n const isLocalhost =\n url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '::1';\n if (url.protocol !== 'https:' && !isLocalhost) {\n throw new Error(`Issuer URL must use HTTPS: ${issuer}`);\n }\n\n // Remove trailing slash for consistent URL building\n return url.origin + url.pathname.replace(/\\/$/, '');\n}\n\n/**\n * Create a configured HTTP client for the PULSE ID API.\n */\nexport function createHttpClient(config: PulseIdConfig) {\n const baseUrl = validateIssuerUrl(config.issuer);\n const fetchFn = config.fetch ?? globalThis.fetch;\n const timeout = config.timeout ?? DEFAULT_TIMEOUT;\n\n /**\n * Make an HTTP request to the PULSE ID API.\n */\n async function request<T>(path: string, options: RequestOptions): Promise<T> {\n const url = `${baseUrl}${path}`;\n const controller = new AbortController();\n\n // Set up timeout\n const timeoutId = setTimeout(() => {\n controller.abort();\n }, options.timeout ?? timeout);\n\n try {\n const response = await fetchFn(url, {\n method: options.method,\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n ...options.headers,\n },\n ...(options.body !== undefined && { body: JSON.stringify(options.body) }),\n signal: controller.signal,\n });\n\n // Clear the timeout\n clearTimeout(timeoutId);\n\n // Parse response body\n const contentType = response.headers.get('content-type');\n const isJson = contentType?.includes('application/json');\n\n if (!response.ok) {\n if (isJson) {\n const errorBody = (await response.json()) as ApiErrorResponse;\n throw PulseIdError.fromResponse(errorBody, response.status);\n }\n\n const errorText = await response.text();\n throw new PulseIdError(\n 'server_error',\n errorText || `HTTP ${response.status}`,\n response.status\n );\n }\n\n // Return parsed JSON or empty object for 204\n if (response.status === 204 || !isJson) {\n return {} as T;\n }\n\n return (await response.json()) as T;\n } catch (error) {\n clearTimeout(timeoutId);\n\n // Re-throw SDK errors as-is\n if (error instanceof PulseIdError) {\n throw error;\n }\n\n // Handle abort (timeout)\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new TimeoutError(`Request to ${path} timed out after ${timeout}ms`);\n }\n\n // Handle network errors\n if (error instanceof TypeError) {\n throw new NetworkError(`Network request to ${path} failed`, error);\n }\n\n // Unknown error\n throw new NetworkError(\n `Unknown error during request to ${path}`,\n error instanceof Error ? error : undefined\n );\n }\n }\n\n return {\n /**\n * Make a GET request.\n */\n get<T>(path: string, headers?: Record<string, string>): Promise<T> {\n return request<T>(path, { method: 'GET', ...(headers && { headers }) });\n },\n\n /**\n * Make a POST request.\n */\n post<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T> {\n return request<T>(path, { method: 'POST', body, ...(headers && { headers }) });\n },\n\n /**\n * Make a PATCH request.\n */\n patch<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T> {\n return request<T>(path, { method: 'PATCH', body, ...(headers && { headers }) });\n },\n\n /**\n * Make a DELETE request.\n */\n delete<T>(path: string, headers?: Record<string, string>): Promise<T> {\n return request<T>(path, { method: 'DELETE', ...(headers && { headers }) });\n },\n\n /**\n * Make a form-encoded POST request (for OAuth token endpoints).\n */\n async postForm<T>(\n path: string,\n data: Record<string, string>,\n headers?: Record<string, string>\n ): Promise<T> {\n const url = `${baseUrl}${path}`;\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n try {\n const response = await fetchFn(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n ...headers,\n },\n body: new URLSearchParams(data).toString(),\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n const contentType = response.headers.get('content-type');\n const isJson = contentType?.includes('application/json');\n\n if (!response.ok) {\n if (isJson) {\n const errorBody = (await response.json()) as ApiErrorResponse;\n throw PulseIdError.fromResponse(errorBody, response.status);\n }\n // Handle non-JSON error responses (e.g., HTML from proxy)\n const errorText = await response.text();\n throw new PulseIdError(\n 'server_error',\n errorText || `HTTP ${response.status}`,\n response.status\n );\n }\n\n return (await response.json()) as T;\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof PulseIdError) throw error;\n if (error instanceof DOMException && error.name === 'AbortError') {\n throw new TimeoutError();\n }\n throw new NetworkError(\n `Request to ${path} failed`,\n error instanceof Error ? error : undefined\n );\n }\n },\n };\n}\n\nexport type HttpClient = ReturnType<typeof createHttpClient>;\n","/**\n * PULSE ID Client\n *\n * Client-side SDK for interacting with the PULSE ID API.\n * Use this in browser environments where you have an access token.\n *\n * For server-side usage with refresh token management, use `PulseIdServer` from './server'.\n */\n\nimport { createHttpClient, type HttpClient } from './http.js';\nimport type { Profile, ProfileUpdate, PulseIdConfig, UserInfo } from './types.js';\n\n/**\n * PULSE ID API Client.\n *\n * @example\n * ```typescript\n * const client = new PulseIdClient({\n * issuer: 'https://id.pulserunning.at',\n * clientId: 'your-client-id',\n * });\n *\n * const profile = await client.getProfile(accessToken);\n * console.log(profile.displayName);\n * ```\n */\nexport class PulseIdClient {\n protected readonly http: HttpClient;\n protected readonly config: PulseIdConfig;\n\n constructor(config: PulseIdConfig) {\n this.config = config;\n this.http = createHttpClient(config);\n }\n\n // ===========================================================================\n // PROFILE\n // ===========================================================================\n\n /**\n * Get the authenticated user's profile.\n *\n * The fields returned depend on the scopes granted to the access token:\n * - `profile`: name, avatar, birthday, etc.\n * - `email`: email address and verification status\n * - `address`: address information\n * - `phone`: phone number\n *\n * @param accessToken - A valid access token\n * @returns The user's profile\n *\n * @example\n * ```typescript\n * const profile = await client.getProfile(accessToken);\n * console.log(`Hello, ${profile.displayName}!`);\n * ```\n */\n async getProfile(accessToken: string): Promise<Profile> {\n return this.http.get<Profile>('/api/v1/me', {\n Authorization: `Bearer ${accessToken}`,\n });\n }\n\n /**\n * Update the authenticated user's profile.\n *\n * Requires the `profile:write` scope.\n *\n * @param accessToken - A valid access token with `profile:write` scope\n * @param data - The profile fields to update\n * @returns The updated profile\n *\n * @example\n * ```typescript\n * const updated = await client.updateProfile(accessToken, {\n * displayName: 'Max Runner',\n * height: 180,\n * });\n * ```\n */\n async updateProfile(accessToken: string, data: ProfileUpdate): Promise<Profile> {\n return this.http.patch<Profile>('/api/v1/me', data, {\n Authorization: `Bearer ${accessToken}`,\n });\n }\n\n /**\n * Get the authenticated user's OIDC userinfo.\n *\n * Requires the `openid` scope. Additional fields depend on granted scopes.\n *\n * @param accessToken - A valid access token\n * @returns The user's OIDC userinfo\n *\n * @example\n * ```typescript\n * const info = await client.getUserInfo(accessToken);\n * console.log(info.sub);\n * ```\n */\n async getUserInfo(accessToken: string): Promise<UserInfo> {\n return this.http.get<UserInfo>('/userinfo', {\n Authorization: `Bearer ${accessToken}`,\n });\n }\n\n // ===========================================================================\n // UTILITY METHODS\n // ===========================================================================\n\n /**\n * Get the configured issuer URL.\n */\n get issuer(): string {\n return this.config.issuer;\n }\n\n /**\n * Get the configured client ID.\n */\n get clientId(): string {\n return this.config.clientId;\n }\n\n /**\n * Build an authorization URL for the OAuth flow.\n *\n * @param options - Authorization options\n * @returns The authorization URL to redirect the user to\n *\n * @example\n * ```typescript\n * const authUrl = client.buildAuthorizationUrl({\n * redirectUri: 'https://myapp.com/callback',\n * scope: 'openid profile email',\n * state: 'random-state-string',\n * });\n * window.location.href = authUrl;\n * ```\n */\n buildAuthorizationUrl(options: {\n redirectUri: string;\n scope: string;\n state: string;\n nonce?: string;\n codeChallenge: string;\n codeChallengeMethod?: 'S256';\n prompt?: 'none' | 'login' | 'consent' | 'create';\n loginHint?: string;\n }): string {\n if (options.codeChallengeMethod && options.codeChallengeMethod !== 'S256') {\n throw new Error('code_challenge_method must be S256');\n }\n\n if (!options.state) {\n throw new Error('state is required for authorization requests');\n }\n\n if (!options.codeChallenge) {\n throw new Error('code_challenge is required for authorization requests');\n }\n\n const scopes = options.scope.split(' ').filter(Boolean);\n if (scopes.includes('openid') && !options.nonce) {\n throw new Error('nonce is required when requesting openid scope');\n }\n\n const url = new URL(`${this.config.issuer}/authorize`);\n\n url.searchParams.set('response_type', 'code');\n url.searchParams.set('client_id', this.config.clientId);\n url.searchParams.set('redirect_uri', options.redirectUri);\n url.searchParams.set('scope', options.scope);\n\n url.searchParams.set('state', options.state);\n if (options.nonce) {\n url.searchParams.set('nonce', options.nonce);\n }\n url.searchParams.set('code_challenge', options.codeChallenge);\n url.searchParams.set('code_challenge_method', options.codeChallengeMethod ?? 'S256');\n if (options.prompt) {\n url.searchParams.set('prompt', options.prompt);\n }\n if (options.loginHint) {\n url.searchParams.set('login_hint', options.loginHint);\n }\n\n return url.toString();\n }\n\n /**\n * Build a logout URL for ending the session.\n *\n * @param options - Logout options\n * @returns The logout URL to redirect the user to\n *\n * @example\n * ```typescript\n * const logoutUrl = client.buildLogoutUrl({\n * idTokenHint: idToken,\n * postLogoutRedirectUri: 'https://myapp.com',\n * });\n * window.location.href = logoutUrl;\n * ```\n */\n buildLogoutUrl(options?: {\n idTokenHint?: string;\n postLogoutRedirectUri?: string;\n state?: string;\n }): string {\n const url = new URL(`${this.config.issuer}/logout`);\n\n if (options?.idTokenHint) {\n url.searchParams.set('id_token_hint', options.idTokenHint);\n }\n if (options?.postLogoutRedirectUri) {\n url.searchParams.set('post_logout_redirect_uri', options.postLogoutRedirectUri);\n }\n if (options?.state) {\n url.searchParams.set('state', options.state);\n }\n\n return url.toString();\n }\n}\n"]}
@@ -0,0 +1,404 @@
1
+ /**
2
+ * PULSE ID SDK Types
3
+ *
4
+ * Type definitions for the PULSE ID API.
5
+ */
6
+ /**
7
+ * Configuration for the PULSE ID client.
8
+ */
9
+ type PulseIdConfig = {
10
+ /**
11
+ * The PULSE ID issuer URL (e.g., 'https://id.pulserunning.at').
12
+ * Do not include a trailing slash.
13
+ */
14
+ issuer: string;
15
+ /**
16
+ * OAuth client ID registered with PULSE ID.
17
+ */
18
+ clientId: string;
19
+ /**
20
+ * OAuth client secret. Only required for server-side operations.
21
+ * Never expose this in client-side code.
22
+ */
23
+ clientSecret?: string;
24
+ /**
25
+ * Custom fetch implementation. Defaults to global fetch.
26
+ * Useful for testing or custom HTTP handling.
27
+ */
28
+ fetch?: typeof fetch;
29
+ /**
30
+ * Request timeout in milliseconds. Defaults to 30000 (30 seconds).
31
+ */
32
+ timeout?: number;
33
+ };
34
+ /**
35
+ * User profile returned by PULSE ID.
36
+ */
37
+ type Profile = {
38
+ /** Unique user identifier (UUID) */
39
+ id: string;
40
+ /** User's email address (requires 'email' scope) */
41
+ email?: string;
42
+ /** Whether the email has been verified */
43
+ emailVerified?: boolean;
44
+ /** User's first name */
45
+ firstName?: string | null;
46
+ /** User's last name */
47
+ lastName?: string | null;
48
+ /** Display name (how the user appears to others) */
49
+ displayName?: string | null;
50
+ /** URL to the user's avatar image */
51
+ avatar?: string | null;
52
+ /** User's birthday in ISO date format (YYYY-MM-DD) */
53
+ birthday?: string | null;
54
+ /** User's gender */
55
+ gender?: 'male' | 'female' | 'other' | null;
56
+ /** Height in centimeters */
57
+ height?: number | null;
58
+ /** Weight in kilograms */
59
+ weight?: number | null;
60
+ /** User's address (requires 'address' scope) */
61
+ address?: {
62
+ street?: string | null;
63
+ city?: string | null;
64
+ postalCode?: string | null;
65
+ country?: string | null;
66
+ };
67
+ /** User's phone number (requires 'phone' scope) */
68
+ phone?: string | null;
69
+ /** When the account was created */
70
+ createdAt: string;
71
+ /** When the profile was last updated */
72
+ updatedAt: string;
73
+ };
74
+ /**
75
+ * Fields that can be updated on a user profile.
76
+ */
77
+ type ProfileUpdate = {
78
+ firstName?: string | null;
79
+ lastName?: string | null;
80
+ displayName?: string | null;
81
+ avatar?: string | null;
82
+ birthday?: string | null;
83
+ gender?: 'male' | 'female' | 'other' | null;
84
+ height?: number | null;
85
+ weight?: number | null;
86
+ phone?: string | null;
87
+ street?: string | null;
88
+ city?: string | null;
89
+ postalCode?: string | null;
90
+ country?: string | null;
91
+ };
92
+ /**
93
+ * Standard OIDC userinfo response from PULSE ID.
94
+ */
95
+ type UserInfo = {
96
+ /** Subject identifier (user ID) */
97
+ sub: string;
98
+ /** User's email address (requires 'email' scope) */
99
+ email?: string;
100
+ /** Whether the email has been verified */
101
+ email_verified?: boolean;
102
+ /** Full name */
103
+ name?: string;
104
+ /** Given name */
105
+ given_name?: string;
106
+ /** Family name */
107
+ family_name?: string;
108
+ /** Preferred username */
109
+ preferred_username?: string;
110
+ /** Profile picture URL */
111
+ picture?: string;
112
+ /** Locale */
113
+ locale?: string;
114
+ /** When the userinfo was last updated (seconds since epoch) */
115
+ updated_at?: number;
116
+ };
117
+ /**
118
+ * OAuth token response from PULSE ID.
119
+ */
120
+ type TokenResponse = {
121
+ /** The access token for API requests */
122
+ access_token: string;
123
+ /** Token type (always 'Bearer') */
124
+ token_type: 'Bearer';
125
+ /** Time until the access token expires (in seconds) */
126
+ expires_in: number;
127
+ /** Refresh token for obtaining new access tokens */
128
+ refresh_token?: string;
129
+ /** ID token containing user claims (JWT) */
130
+ id_token?: string;
131
+ /** Granted scopes (space-separated) */
132
+ scope?: string;
133
+ };
134
+ /**
135
+ * Token refresh options.
136
+ */
137
+ type RefreshOptions = {
138
+ /** The refresh token to exchange */
139
+ refreshToken: string;
140
+ /** Optionally request a subset of the original scopes */
141
+ scope?: string;
142
+ };
143
+ /**
144
+ * Standard OAuth error response.
145
+ */
146
+ type ApiErrorResponse = {
147
+ /** Error code (e.g., 'invalid_token', 'insufficient_scope') */
148
+ error: string;
149
+ /** Human-readable error description */
150
+ error_description: string;
151
+ /** Required scope (for 'insufficient_scope' errors) */
152
+ required_scope?: string;
153
+ };
154
+ /**
155
+ * Error codes returned by PULSE ID.
156
+ */
157
+ type ErrorCode = 'invalid_request' | 'invalid_token' | 'expired_token' | 'insufficient_scope' | 'not_found' | 'server_error';
158
+ /**
159
+ * Available OAuth scopes for PULSE ID.
160
+ */
161
+ declare const SCOPES: {
162
+ /** Required for OIDC. Returns the user's ID. */
163
+ readonly OPENID: "openid";
164
+ /** Read profile information (name, avatar, etc.) */
165
+ readonly PROFILE: "profile";
166
+ /** Update profile information */
167
+ readonly PROFILE_WRITE: "profile:write";
168
+ /** Read email address */
169
+ readonly EMAIL: "email";
170
+ /** Read address information */
171
+ readonly ADDRESS: "address";
172
+ /** Read phone number */
173
+ readonly PHONE: "phone";
174
+ /** Request a refresh token */
175
+ readonly OFFLINE_ACCESS: "offline_access";
176
+ /** Read activities (future) */
177
+ readonly ACTIVITIES: "activities";
178
+ /** Create/update activities (future) */
179
+ readonly ACTIVITIES_WRITE: "activities:write";
180
+ /** Manage external sync connections (future) */
181
+ readonly CONNECTIONS: "connections";
182
+ };
183
+ type Scope = (typeof SCOPES)[keyof typeof SCOPES];
184
+
185
+ /**
186
+ * HTTP Client Utilities
187
+ *
188
+ * Internal utilities for making HTTP requests with proper error handling.
189
+ */
190
+
191
+ /**
192
+ * Create a configured HTTP client for the PULSE ID API.
193
+ */
194
+ declare function createHttpClient(config: PulseIdConfig): {
195
+ /**
196
+ * Make a GET request.
197
+ */
198
+ get<T>(path: string, headers?: Record<string, string>): Promise<T>;
199
+ /**
200
+ * Make a POST request.
201
+ */
202
+ post<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
203
+ /**
204
+ * Make a PATCH request.
205
+ */
206
+ patch<T>(path: string, body?: unknown, headers?: Record<string, string>): Promise<T>;
207
+ /**
208
+ * Make a DELETE request.
209
+ */
210
+ delete<T>(path: string, headers?: Record<string, string>): Promise<T>;
211
+ /**
212
+ * Make a form-encoded POST request (for OAuth token endpoints).
213
+ */
214
+ postForm<T>(path: string, data: Record<string, string>, headers?: Record<string, string>): Promise<T>;
215
+ };
216
+ type HttpClient = ReturnType<typeof createHttpClient>;
217
+
218
+ /**
219
+ * PULSE ID Client
220
+ *
221
+ * Client-side SDK for interacting with the PULSE ID API.
222
+ * Use this in browser environments where you have an access token.
223
+ *
224
+ * For server-side usage with refresh token management, use `PulseIdServer` from './server'.
225
+ */
226
+
227
+ /**
228
+ * PULSE ID API Client.
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * const client = new PulseIdClient({
233
+ * issuer: 'https://id.pulserunning.at',
234
+ * clientId: 'your-client-id',
235
+ * });
236
+ *
237
+ * const profile = await client.getProfile(accessToken);
238
+ * console.log(profile.displayName);
239
+ * ```
240
+ */
241
+ declare class PulseIdClient {
242
+ protected readonly http: HttpClient;
243
+ protected readonly config: PulseIdConfig;
244
+ constructor(config: PulseIdConfig);
245
+ /**
246
+ * Get the authenticated user's profile.
247
+ *
248
+ * The fields returned depend on the scopes granted to the access token:
249
+ * - `profile`: name, avatar, birthday, etc.
250
+ * - `email`: email address and verification status
251
+ * - `address`: address information
252
+ * - `phone`: phone number
253
+ *
254
+ * @param accessToken - A valid access token
255
+ * @returns The user's profile
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * const profile = await client.getProfile(accessToken);
260
+ * console.log(`Hello, ${profile.displayName}!`);
261
+ * ```
262
+ */
263
+ getProfile(accessToken: string): Promise<Profile>;
264
+ /**
265
+ * Update the authenticated user's profile.
266
+ *
267
+ * Requires the `profile:write` scope.
268
+ *
269
+ * @param accessToken - A valid access token with `profile:write` scope
270
+ * @param data - The profile fields to update
271
+ * @returns The updated profile
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * const updated = await client.updateProfile(accessToken, {
276
+ * displayName: 'Max Runner',
277
+ * height: 180,
278
+ * });
279
+ * ```
280
+ */
281
+ updateProfile(accessToken: string, data: ProfileUpdate): Promise<Profile>;
282
+ /**
283
+ * Get the authenticated user's OIDC userinfo.
284
+ *
285
+ * Requires the `openid` scope. Additional fields depend on granted scopes.
286
+ *
287
+ * @param accessToken - A valid access token
288
+ * @returns The user's OIDC userinfo
289
+ *
290
+ * @example
291
+ * ```typescript
292
+ * const info = await client.getUserInfo(accessToken);
293
+ * console.log(info.sub);
294
+ * ```
295
+ */
296
+ getUserInfo(accessToken: string): Promise<UserInfo>;
297
+ /**
298
+ * Get the configured issuer URL.
299
+ */
300
+ get issuer(): string;
301
+ /**
302
+ * Get the configured client ID.
303
+ */
304
+ get clientId(): string;
305
+ /**
306
+ * Build an authorization URL for the OAuth flow.
307
+ *
308
+ * @param options - Authorization options
309
+ * @returns The authorization URL to redirect the user to
310
+ *
311
+ * @example
312
+ * ```typescript
313
+ * const authUrl = client.buildAuthorizationUrl({
314
+ * redirectUri: 'https://myapp.com/callback',
315
+ * scope: 'openid profile email',
316
+ * state: 'random-state-string',
317
+ * });
318
+ * window.location.href = authUrl;
319
+ * ```
320
+ */
321
+ buildAuthorizationUrl(options: {
322
+ redirectUri: string;
323
+ scope: string;
324
+ state: string;
325
+ nonce?: string;
326
+ codeChallenge: string;
327
+ codeChallengeMethod?: 'S256';
328
+ prompt?: 'none' | 'login' | 'consent' | 'create';
329
+ loginHint?: string;
330
+ }): string;
331
+ /**
332
+ * Build a logout URL for ending the session.
333
+ *
334
+ * @param options - Logout options
335
+ * @returns The logout URL to redirect the user to
336
+ *
337
+ * @example
338
+ * ```typescript
339
+ * const logoutUrl = client.buildLogoutUrl({
340
+ * idTokenHint: idToken,
341
+ * postLogoutRedirectUri: 'https://myapp.com',
342
+ * });
343
+ * window.location.href = logoutUrl;
344
+ * ```
345
+ */
346
+ buildLogoutUrl(options?: {
347
+ idTokenHint?: string;
348
+ postLogoutRedirectUri?: string;
349
+ state?: string;
350
+ }): string;
351
+ }
352
+
353
+ /**
354
+ * PULSE ID SDK Errors
355
+ *
356
+ * Custom error classes for better error handling.
357
+ */
358
+
359
+ /**
360
+ * Base error class for PULSE ID SDK errors.
361
+ */
362
+ declare class PulseIdError extends Error {
363
+ readonly code: ErrorCode;
364
+ readonly statusCode: number;
365
+ constructor(code: ErrorCode, message: string, statusCode?: number);
366
+ /**
367
+ * Create an error from an API response.
368
+ */
369
+ static fromResponse(response: ApiErrorResponse, statusCode: number): PulseIdError;
370
+ }
371
+ /**
372
+ * Error thrown when the access token is invalid or expired.
373
+ */
374
+ declare class InvalidTokenError extends PulseIdError {
375
+ constructor(message?: string);
376
+ }
377
+ /**
378
+ * Error thrown when the access token lacks required scopes.
379
+ */
380
+ declare class InsufficientScopeError extends PulseIdError {
381
+ readonly requiredScope: string | undefined;
382
+ constructor(message: string, requiredScope?: string);
383
+ }
384
+ /**
385
+ * Error thrown when a resource is not found.
386
+ */
387
+ declare class NotFoundError extends PulseIdError {
388
+ constructor(message?: string);
389
+ }
390
+ /**
391
+ * Error thrown when a network request fails.
392
+ */
393
+ declare class NetworkError extends PulseIdError {
394
+ readonly cause: Error | undefined;
395
+ constructor(message: string, cause?: Error);
396
+ }
397
+ /**
398
+ * Error thrown when a request times out.
399
+ */
400
+ declare class TimeoutError extends PulseIdError {
401
+ constructor(message?: string);
402
+ }
403
+
404
+ export { type ApiErrorResponse, type ErrorCode, InsufficientScopeError, InvalidTokenError, NetworkError, NotFoundError, type Profile, type ProfileUpdate, PulseIdClient, type PulseIdConfig, PulseIdError, type RefreshOptions, SCOPES, type Scope, TimeoutError, type TokenResponse, type UserInfo };
package/dist/index.js ADDED
@@ -0,0 +1,29 @@
1
+ export { InsufficientScopeError, InvalidTokenError, NetworkError, NotFoundError, PulseIdClient, PulseIdError, TimeoutError } from './chunk-BRQ2T53Z.js';
2
+
3
+ // src/types.ts
4
+ var SCOPES = {
5
+ /** Required for OIDC. Returns the user's ID. */
6
+ OPENID: "openid",
7
+ /** Read profile information (name, avatar, etc.) */
8
+ PROFILE: "profile",
9
+ /** Update profile information */
10
+ PROFILE_WRITE: "profile:write",
11
+ /** Read email address */
12
+ EMAIL: "email",
13
+ /** Read address information */
14
+ ADDRESS: "address",
15
+ /** Read phone number */
16
+ PHONE: "phone",
17
+ /** Request a refresh token */
18
+ OFFLINE_ACCESS: "offline_access",
19
+ /** Read activities (future) */
20
+ ACTIVITIES: "activities",
21
+ /** Create/update activities (future) */
22
+ ACTIVITIES_WRITE: "activities:write",
23
+ /** Manage external sync connections (future) */
24
+ CONNECTIONS: "connections"
25
+ };
26
+
27
+ export { SCOPES };
28
+ //# sourceMappingURL=index.js.map
29
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"names":[],"mappings":";;;AAiOO,IAAM,MAAA,GAAS;AAAA;AAAA,EAEpB,MAAA,EAAQ,QAAA;AAAA;AAAA,EAGR,OAAA,EAAS,SAAA;AAAA;AAAA,EAGT,aAAA,EAAe,eAAA;AAAA;AAAA,EAGf,KAAA,EAAO,OAAA;AAAA;AAAA,EAGP,OAAA,EAAS,SAAA;AAAA;AAAA,EAGT,KAAA,EAAO,OAAA;AAAA;AAAA,EAGP,cAAA,EAAgB,gBAAA;AAAA;AAAA,EAGhB,UAAA,EAAY,YAAA;AAAA;AAAA,EAGZ,gBAAA,EAAkB,kBAAA;AAAA;AAAA,EAGlB,WAAA,EAAa;AACf","file":"index.js","sourcesContent":["/**\n * PULSE ID SDK Types\n *\n * Type definitions for the PULSE ID API.\n */\n\n// =============================================================================\n// CLIENT CONFIGURATION\n// =============================================================================\n\n/**\n * Configuration for the PULSE ID client.\n */\nexport type PulseIdConfig = {\n /**\n * The PULSE ID issuer URL (e.g., 'https://id.pulserunning.at').\n * Do not include a trailing slash.\n */\n issuer: string;\n\n /**\n * OAuth client ID registered with PULSE ID.\n */\n clientId: string;\n\n /**\n * OAuth client secret. Only required for server-side operations.\n * Never expose this in client-side code.\n */\n clientSecret?: string;\n\n /**\n * Custom fetch implementation. Defaults to global fetch.\n * Useful for testing or custom HTTP handling.\n */\n fetch?: typeof fetch;\n\n /**\n * Request timeout in milliseconds. Defaults to 30000 (30 seconds).\n */\n timeout?: number;\n};\n\n// =============================================================================\n// USER PROFILE\n// =============================================================================\n\n/**\n * User profile returned by PULSE ID.\n */\nexport type Profile = {\n /** Unique user identifier (UUID) */\n id: string;\n\n /** User's email address (requires 'email' scope) */\n email?: string;\n\n /** Whether the email has been verified */\n emailVerified?: boolean;\n\n /** User's first name */\n firstName?: string | null;\n\n /** User's last name */\n lastName?: string | null;\n\n /** Display name (how the user appears to others) */\n displayName?: string | null;\n\n /** URL to the user's avatar image */\n avatar?: string | null;\n\n /** User's birthday in ISO date format (YYYY-MM-DD) */\n birthday?: string | null;\n\n /** User's gender */\n gender?: 'male' | 'female' | 'other' | null;\n\n /** Height in centimeters */\n height?: number | null;\n\n /** Weight in kilograms */\n weight?: number | null;\n\n /** User's address (requires 'address' scope) */\n address?: {\n street?: string | null;\n city?: string | null;\n postalCode?: string | null;\n country?: string | null;\n };\n\n /** User's phone number (requires 'phone' scope) */\n phone?: string | null;\n\n /** When the account was created */\n createdAt: string;\n\n /** When the profile was last updated */\n updatedAt: string;\n};\n\n/**\n * Fields that can be updated on a user profile.\n */\nexport type ProfileUpdate = {\n firstName?: string | null;\n lastName?: string | null;\n displayName?: string | null;\n avatar?: string | null;\n birthday?: string | null;\n gender?: 'male' | 'female' | 'other' | null;\n height?: number | null;\n weight?: number | null;\n phone?: string | null;\n street?: string | null;\n city?: string | null;\n postalCode?: string | null;\n country?: string | null;\n};\n\n// =============================================================================\n// OIDC USERINFO\n// =============================================================================\n\n/**\n * Standard OIDC userinfo response from PULSE ID.\n */\nexport type UserInfo = {\n /** Subject identifier (user ID) */\n sub: string;\n /** User's email address (requires 'email' scope) */\n email?: string;\n /** Whether the email has been verified */\n email_verified?: boolean;\n /** Full name */\n name?: string;\n /** Given name */\n given_name?: string;\n /** Family name */\n family_name?: string;\n /** Preferred username */\n preferred_username?: string;\n /** Profile picture URL */\n picture?: string;\n /** Locale */\n locale?: string;\n /** When the userinfo was last updated (seconds since epoch) */\n updated_at?: number;\n};\n\n// =============================================================================\n// OAUTH / TOKENS\n// =============================================================================\n\n/**\n * OAuth token response from PULSE ID.\n */\nexport type TokenResponse = {\n /** The access token for API requests */\n access_token: string;\n\n /** Token type (always 'Bearer') */\n token_type: 'Bearer';\n\n /** Time until the access token expires (in seconds) */\n expires_in: number;\n\n /** Refresh token for obtaining new access tokens */\n refresh_token?: string;\n\n /** ID token containing user claims (JWT) */\n id_token?: string;\n\n /** Granted scopes (space-separated) */\n scope?: string;\n};\n\n/**\n * Token refresh options.\n */\nexport type RefreshOptions = {\n /** The refresh token to exchange */\n refreshToken: string;\n\n /** Optionally request a subset of the original scopes */\n scope?: string;\n};\n\n// =============================================================================\n// API ERRORS\n// =============================================================================\n\n/**\n * Standard OAuth error response.\n */\nexport type ApiErrorResponse = {\n /** Error code (e.g., 'invalid_token', 'insufficient_scope') */\n error: string;\n\n /** Human-readable error description */\n error_description: string;\n\n /** Required scope (for 'insufficient_scope' errors) */\n required_scope?: string;\n};\n\n/**\n * Error codes returned by PULSE ID.\n */\nexport type ErrorCode =\n | 'invalid_request'\n | 'invalid_token'\n | 'expired_token'\n | 'insufficient_scope'\n | 'not_found'\n | 'server_error';\n\n// =============================================================================\n// SCOPES\n// =============================================================================\n\n/**\n * Available OAuth scopes for PULSE ID.\n */\nexport const SCOPES = {\n /** Required for OIDC. Returns the user's ID. */\n OPENID: 'openid',\n\n /** Read profile information (name, avatar, etc.) */\n PROFILE: 'profile',\n\n /** Update profile information */\n PROFILE_WRITE: 'profile:write',\n\n /** Read email address */\n EMAIL: 'email',\n\n /** Read address information */\n ADDRESS: 'address',\n\n /** Read phone number */\n PHONE: 'phone',\n\n /** Request a refresh token */\n OFFLINE_ACCESS: 'offline_access',\n\n /** Read activities (future) */\n ACTIVITIES: 'activities',\n\n /** Create/update activities (future) */\n ACTIVITIES_WRITE: 'activities:write',\n\n /** Manage external sync connections (future) */\n CONNECTIONS: 'connections',\n} as const;\n\nexport type Scope = (typeof SCOPES)[keyof typeof SCOPES];\n"]}
@@ -0,0 +1,44 @@
1
+ import { Profile, ProfileUpdate, PulseIdClient, PulseIdConfig, UserInfo, TokenResponse, RefreshOptions } from './index.js';
2
+ export { InsufficientScopeError, InvalidTokenError, PulseIdError } from './index.js';
3
+
4
+ type ServerConfig = PulseIdConfig & {
5
+ clientSecret: string;
6
+ storage?: TokenStorage;
7
+ };
8
+ interface TokenStorage {
9
+ getTokens(userId: string): Promise<StoredTokens | null>;
10
+ setTokens(userId: string, tokens: StoredTokens): Promise<void>;
11
+ deleteTokens(userId: string): Promise<void>;
12
+ }
13
+ type StoredTokens = {
14
+ accessToken: string;
15
+ refreshToken: string;
16
+ expiresAt: Date;
17
+ scope?: string;
18
+ };
19
+ interface ProfileResource {
20
+ get(): Promise<Profile>;
21
+ update(data: ProfileUpdate): Promise<Profile>;
22
+ }
23
+ interface UserInfoResource {
24
+ get(): Promise<UserInfo>;
25
+ }
26
+ interface UserClient {
27
+ profile: ProfileResource;
28
+ userInfo: UserInfoResource;
29
+ }
30
+ declare class PulseIdServer extends PulseIdClient {
31
+ private readonly clientSecret;
32
+ private readonly storage;
33
+ constructor(config: ServerConfig);
34
+ forUser(userId: string): UserClient;
35
+ exchangeCode(code: string, redirectUri: string, codeVerifier?: string): Promise<TokenResponse>;
36
+ refreshTokens(options: RefreshOptions): Promise<TokenResponse>;
37
+ revokeToken(token: string, tokenTypeHint?: 'access_token' | 'refresh_token'): Promise<void>;
38
+ getProfileWithRefresh(storage: TokenStorage, userId: string): Promise<Profile>;
39
+ updateProfileWithRefresh(storage: TokenStorage, userId: string, data: ProfileUpdate): Promise<Profile>;
40
+ getUserInfoWithRefresh(storage: TokenStorage, userId: string): Promise<UserInfo>;
41
+ private ensureValidTokens;
42
+ }
43
+
44
+ export { Profile, type ProfileResource, ProfileUpdate, PulseIdConfig, PulseIdServer, type ServerConfig, type StoredTokens, TokenResponse, type TokenStorage, type UserClient, UserInfo, type UserInfoResource };
package/dist/server.js ADDED
@@ -0,0 +1,108 @@
1
+ import { PulseIdClient, PulseIdError } from './chunk-BRQ2T53Z.js';
2
+ export { InsufficientScopeError, InvalidTokenError, PulseIdError } from './chunk-BRQ2T53Z.js';
3
+
4
+ // src/server.ts
5
+ var PulseIdServer = class extends PulseIdClient {
6
+ clientSecret;
7
+ storage;
8
+ constructor(config) {
9
+ if (!config.clientSecret) {
10
+ throw new Error("clientSecret is required for server-side operations");
11
+ }
12
+ super(config);
13
+ this.clientSecret = config.clientSecret;
14
+ this.storage = config.storage;
15
+ }
16
+ forUser(userId) {
17
+ if (!this.storage) {
18
+ throw new Error("storage must be configured to use forUser()");
19
+ }
20
+ const storage = this.storage;
21
+ return {
22
+ profile: {
23
+ get: () => this.getProfileWithRefresh(storage, userId),
24
+ update: (data) => this.updateProfileWithRefresh(storage, userId, data)
25
+ },
26
+ userInfo: {
27
+ get: () => this.getUserInfoWithRefresh(storage, userId)
28
+ }
29
+ };
30
+ }
31
+ async exchangeCode(code, redirectUri, codeVerifier) {
32
+ const data = {
33
+ grant_type: "authorization_code",
34
+ code,
35
+ redirect_uri: redirectUri,
36
+ client_id: this.config.clientId,
37
+ client_secret: this.clientSecret
38
+ };
39
+ if (codeVerifier) {
40
+ data["code_verifier"] = codeVerifier;
41
+ }
42
+ return this.http.postForm("/token", data);
43
+ }
44
+ async refreshTokens(options) {
45
+ const data = {
46
+ grant_type: "refresh_token",
47
+ refresh_token: options.refreshToken,
48
+ client_id: this.config.clientId,
49
+ client_secret: this.clientSecret
50
+ };
51
+ if (options.scope) {
52
+ data["scope"] = options.scope;
53
+ }
54
+ return this.http.postForm("/token", data);
55
+ }
56
+ async revokeToken(token, tokenTypeHint) {
57
+ const data = {
58
+ token,
59
+ client_id: this.config.clientId,
60
+ client_secret: this.clientSecret
61
+ };
62
+ if (tokenTypeHint) {
63
+ data["token_type_hint"] = tokenTypeHint;
64
+ }
65
+ await this.http.postForm("/revoke", data);
66
+ }
67
+ async getProfileWithRefresh(storage, userId) {
68
+ const tokens = await this.ensureValidTokens(storage, userId);
69
+ return this.getProfile(tokens.accessToken);
70
+ }
71
+ async updateProfileWithRefresh(storage, userId, data) {
72
+ const tokens = await this.ensureValidTokens(storage, userId);
73
+ return this.updateProfile(tokens.accessToken, data);
74
+ }
75
+ async getUserInfoWithRefresh(storage, userId) {
76
+ const tokens = await this.ensureValidTokens(storage, userId);
77
+ return this.getUserInfo(tokens.accessToken);
78
+ }
79
+ async ensureValidTokens(storage, userId) {
80
+ const tokens = await storage.getTokens(userId);
81
+ if (!tokens) {
82
+ throw new PulseIdError("invalid_token", "No tokens found. User needs to re-authenticate.", 401);
83
+ }
84
+ const bufferMs = 5 * 60 * 1e3;
85
+ const isExpired = tokens.expiresAt.getTime() - bufferMs < Date.now();
86
+ if (!isExpired) {
87
+ return tokens;
88
+ }
89
+ try {
90
+ const newTokens = await this.refreshTokens({ refreshToken: tokens.refreshToken });
91
+ const updatedTokens = {
92
+ accessToken: newTokens.access_token,
93
+ refreshToken: newTokens.refresh_token ?? tokens.refreshToken,
94
+ expiresAt: new Date(Date.now() + newTokens.expires_in * 1e3),
95
+ ...newTokens.scope ?? tokens.scope ? { scope: newTokens.scope ?? tokens.scope } : {}
96
+ };
97
+ await storage.setTokens(userId, updatedTokens);
98
+ return updatedTokens;
99
+ } catch {
100
+ await storage.deleteTokens(userId);
101
+ throw new PulseIdError("invalid_token", "Token refresh failed. User needs to re-authenticate.", 401);
102
+ }
103
+ }
104
+ };
105
+
106
+ export { PulseIdServer };
107
+ //# sourceMappingURL=server.js.map
108
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts"],"names":[],"mappings":"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","file":"server.js","sourcesContent":["import { PulseIdClient } from './client.js';\nimport { PulseIdError } from './errors.js';\nimport type {\n Profile,\n ProfileUpdate,\n PulseIdConfig,\n RefreshOptions,\n TokenResponse,\n UserInfo,\n} from './types.js';\n\nexport type ServerConfig = PulseIdConfig & {\n clientSecret: string;\n storage?: TokenStorage;\n};\n\nexport interface TokenStorage {\n getTokens(userId: string): Promise<StoredTokens | null>;\n setTokens(userId: string, tokens: StoredTokens): Promise<void>;\n deleteTokens(userId: string): Promise<void>;\n}\n\nexport type StoredTokens = {\n accessToken: string;\n refreshToken: string;\n expiresAt: Date;\n scope?: string;\n};\n\nexport interface ProfileResource {\n get(): Promise<Profile>;\n update(data: ProfileUpdate): Promise<Profile>;\n}\n\nexport interface UserInfoResource {\n get(): Promise<UserInfo>;\n}\n\nexport interface UserClient {\n profile: ProfileResource;\n userInfo: UserInfoResource;\n}\n\nexport class PulseIdServer extends PulseIdClient {\n private readonly clientSecret: string;\n private readonly storage: TokenStorage | undefined;\n\n constructor(config: ServerConfig) {\n if (!config.clientSecret) {\n throw new Error('clientSecret is required for server-side operations');\n }\n super(config);\n this.clientSecret = config.clientSecret;\n this.storage = config.storage;\n }\n\n forUser(userId: string): UserClient {\n if (!this.storage) {\n throw new Error('storage must be configured to use forUser()');\n }\n const storage = this.storage;\n\n return {\n profile: {\n get: () => this.getProfileWithRefresh(storage, userId),\n update: (data: ProfileUpdate) => this.updateProfileWithRefresh(storage, userId, data),\n },\n userInfo: {\n get: () => this.getUserInfoWithRefresh(storage, userId),\n },\n };\n }\n\n async exchangeCode(code: string, redirectUri: string, codeVerifier?: string): Promise<TokenResponse> {\n const data: Record<string, string> = {\n grant_type: 'authorization_code',\n code,\n redirect_uri: redirectUri,\n client_id: this.config.clientId,\n client_secret: this.clientSecret,\n };\n if (codeVerifier) {\n data['code_verifier'] = codeVerifier;\n }\n return this.http.postForm<TokenResponse>('/token', data);\n }\n\n async refreshTokens(options: RefreshOptions): Promise<TokenResponse> {\n const data: Record<string, string> = {\n grant_type: 'refresh_token',\n refresh_token: options.refreshToken,\n client_id: this.config.clientId,\n client_secret: this.clientSecret,\n };\n if (options.scope) {\n data['scope'] = options.scope;\n }\n return this.http.postForm<TokenResponse>('/token', data);\n }\n\n async revokeToken(token: string, tokenTypeHint?: 'access_token' | 'refresh_token'): Promise<void> {\n const data: Record<string, string> = {\n token,\n client_id: this.config.clientId,\n client_secret: this.clientSecret,\n };\n if (tokenTypeHint) {\n data['token_type_hint'] = tokenTypeHint;\n }\n await this.http.postForm<void>('/revoke', data);\n }\n\n async getProfileWithRefresh(storage: TokenStorage, userId: string): Promise<Profile> {\n const tokens = await this.ensureValidTokens(storage, userId);\n return this.getProfile(tokens.accessToken);\n }\n\n async updateProfileWithRefresh(storage: TokenStorage, userId: string, data: ProfileUpdate): Promise<Profile> {\n const tokens = await this.ensureValidTokens(storage, userId);\n return this.updateProfile(tokens.accessToken, data);\n }\n\n async getUserInfoWithRefresh(storage: TokenStorage, userId: string): Promise<UserInfo> {\n const tokens = await this.ensureValidTokens(storage, userId);\n return this.getUserInfo(tokens.accessToken);\n }\n\n private async ensureValidTokens(storage: TokenStorage, userId: string): Promise<StoredTokens> {\n const tokens = await storage.getTokens(userId);\n if (!tokens) {\n throw new PulseIdError('invalid_token', 'No tokens found. User needs to re-authenticate.', 401);\n }\n\n const bufferMs = 5 * 60 * 1000;\n const isExpired = tokens.expiresAt.getTime() - bufferMs < Date.now();\n if (!isExpired) {\n return tokens;\n }\n\n try {\n const newTokens = await this.refreshTokens({ refreshToken: tokens.refreshToken });\n const updatedTokens: StoredTokens = {\n accessToken: newTokens.access_token,\n refreshToken: newTokens.refresh_token ?? tokens.refreshToken,\n expiresAt: new Date(Date.now() + newTokens.expires_in * 1000),\n ...(newTokens.scope ?? tokens.scope ? { scope: newTokens.scope ?? tokens.scope } : {}),\n };\n await storage.setTokens(userId, updatedTokens);\n return updatedTokens;\n } catch {\n await storage.deleteTokens(userId);\n throw new PulseIdError('invalid_token', 'Token refresh failed. User needs to re-authenticate.', 401);\n }\n }\n}\n\nexport type { TokenResponse, Profile, ProfileUpdate, PulseIdConfig, UserInfo } from './types.js';\nexport { PulseIdError, InvalidTokenError, InsufficientScopeError } from './errors.js';\n"]}
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@pulseid/client",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript SDK for PULSE ID - Your athlete identity",
5
+ "author": "Patrick Hübl-Neschkudla <patrick@pulserunning.at>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ },
16
+ "./server": {
17
+ "import": "./dist/server.js",
18
+ "types": "./dist/server.d.ts"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md"
24
+ ],
25
+ "engines": {
26
+ "node": ">=22.0.0"
27
+ },
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "dev": "tsup --watch",
31
+ "test": "vitest run",
32
+ "test:watch": "vitest",
33
+ "test:coverage": "vitest run --coverage",
34
+ "lint": "eslint src --ext .ts",
35
+ "typecheck": "tsc --noEmit",
36
+ "clean": "rm -rf dist",
37
+ "changeset": "changeset",
38
+ "version": "changeset version",
39
+ "release": "pnpm build && changeset publish",
40
+ "prepublishOnly": "npm run build",
41
+ "docs:dev": "cd docs && pnpm dev",
42
+ "docs:build": "cd docs && pnpm build"
43
+ },
44
+ "devDependencies": {
45
+ "@changesets/changelog-github": "0.5.2",
46
+ "@changesets/cli": "2.29.8",
47
+ "@types/node": "22.15.3",
48
+ "@vitest/coverage-v8": "4.0.18",
49
+ "eslint": "9.39.2",
50
+ "tsup": "8.5.1",
51
+ "typescript": "5.8.3",
52
+ "vitest": "4.0.18"
53
+ },
54
+ "keywords": [
55
+ "pulse",
56
+ "pulseid",
57
+ "oauth",
58
+ "oidc",
59
+ "authentication",
60
+ "identity",
61
+ "sports",
62
+ "athlete"
63
+ ],
64
+ "repository": {
65
+ "type": "git",
66
+ "url": "git+https://github.com/flipace/pulse-id-sdk-ts.git"
67
+ },
68
+ "bugs": {
69
+ "url": "https://github.com/flipace/pulse-id-sdk-ts/issues"
70
+ },
71
+ "homepage": "https://github.com/flipace/pulse-id-sdk-ts#readme",
72
+ "directories": {
73
+ "doc": "docs"
74
+ }
75
+ }